56 lines
5.1 KiB
Markdown
56 lines
5.1 KiB
Markdown
# `@suaveplan/api-sandbox-permissions` — permission model for sandboxed-tier plugins
|
|
|
|
**Package**: `@suaveplan/api-sandbox-permissions`
|
|
**Status**: PROPOSED
|
|
**Type**: new-package
|
|
**Stage**: 8 (Sandboxing + cross-cutting middleware + authoring helpers) — see umbrella `openspec/changes/api-plugin-foundation/`
|
|
|
|
## Why
|
|
|
|
Sandboxed-tier plugins (manifest `isolation: "sandboxed"`) execute untrusted or third-party code and MUST NOT have ambient access to the filesystem, the network, or unbounded compute. Per design doc §2.2 the sandboxed tier is implemented as a worker thread plus either `vm.Context` (`api-sandbox-vm`) or WASI (`api-sandbox-wasm`); both hosts share the same permission model. Centralising that model in a single package keeps the policy in one place, makes denial behaviour observable, and lets both sandbox hosts gate capability calls through the same code path.
|
|
|
|
Per §5.2b every sandboxed capability call passes through this layer before reaching the proxy surface — denials emit `plugin.permission_denied_total{plugin, capability, reason}` and surface as `PERMISSION_DENIED` errors inside the plugin.
|
|
|
|
## What changes
|
|
|
|
- ADDED: `PermissionPolicy` — value object holding the resolved `permissions.fs[]` allowlist, `permissions.fetch[]` allowlist, and `permissions.budgets` (cpuPercent / memoryMb / wallClockMs). Built from `PermissionsManifest` (`api-types`).
|
|
- ADDED: `PermissionChecker.checkFs(path, mode)` — verifies a host-proxied filesystem path matches the allowlist (glob match against `permissions.fs[]`); returns `Allow | Deny<reason>`.
|
|
- ADDED: `PermissionChecker.checkFetch(url)` — verifies an outbound destination host matches `permissions.fetch[]`; supports exact host and `*.suffix` patterns.
|
|
- ADDED: `PermissionChecker.checkCapability(capabilityId, payload)` — for `db` / `secrets` / `events` payload-shape gating per §5.2b (secret name in manifest, event topic in declared `publishes` / `subscribes`, DB query scoped to plugin's declared `database`).
|
|
- ADDED: `BudgetTracker` — tracks per-request CPU sample, resident-memory sample, and wall-clock elapsed; raises `BUDGET_EXCEEDED` when any budget is breached. Counters are caller-supplied per `Clock` / sampling hooks.
|
|
- ADDED: `PERMISSION_DENIED` and `BUDGET_EXCEEDED` error codes (`SAND_PERM_*` registry) plus a redaction-safe `details` schema.
|
|
- ADDED: `plugin.permission_denied_total` and `plugin.budget_exceeded_total` instrument descriptors so both sandbox hosts emit identical metric names with identical label sets (`plugin`, `capability`, `reason`).
|
|
- ADDED: `runPermissionsConformance(label, makeChecker)` testing harness (subpath `./testing`) so `api-sandbox-vm`, `api-sandbox-wasm`, and any future sandbox host exercise the same allow / deny / budget invariants.
|
|
|
|
## Out of scope
|
|
|
|
- The actual `vm.Context` runtime (`api-sandbox-vm`) and the WASI runtime (`api-sandbox-wasm`). Both consume this package; neither is bundled here.
|
|
- The host-proxy implementations for `fs` and `fetch` — they live in `api-host` (main-process side) and only call `PermissionChecker` before performing the underlying operation.
|
|
- Authorisation across plugins (that is `api-rbac`). This package gates a plugin's own capability calls; RBAC gates a request's principal scopes.
|
|
- CPU/memory sampling primitives — supplied by the worker host; this package only owns the budget tracker that aggregates samples and decides allow / deny.
|
|
|
|
## Source mapping
|
|
|
|
Genesis-original. Source of design intent: `docs/superpowers/specs/2026-05-13-api-plugin-foundation-design.md`.
|
|
|
|
| Requirement | Design doc section |
|
|
|---|---|
|
|
| `permissions.fs` / `permissions.fetch` allowlists | §3 (`permissions` block) |
|
|
| Budget enforcement (CPU / memory / wall-clock) | §3 (`permissions.budgets`) |
|
|
| Per-capability gating (db / secrets / events / fs / fetch) | §5.2b |
|
|
| `plugin.permission_denied_total{plugin, capability, reason}` metric | §5.2b, §12.4 |
|
|
| `PERMISSION_DENIED` error code surfaced inside the plugin | §5.2b |
|
|
|
|
## Verification approach
|
|
|
|
- `src/policy/policy.test.ts` — `PermissionPolicy` construction from manifest, normalisation of paths, glob compilation.
|
|
- `src/checker/fs.test.ts`, `src/checker/fetch.test.ts`, `src/checker/capability.test.ts` — allow / deny matrices, including edge cases (path traversal, port-in-host, scheme rejection).
|
|
- `src/budget/budget.test.ts` — budget breach behaviour, monotonic clock progression, multi-budget interaction.
|
|
- `src/checker/checker.spec-conformance.test.ts` — pins the observable contract: every deny path increments `plugin.permission_denied_total` exactly once AND raises `PERMISSION_DENIED` with the documented `details` shape (verified against a real in-memory `MeterProvider`).
|
|
- `src/testing/run-permissions-conformance.test.ts` — the shipped harness exercised against an in-package fixture checker, so adapters that consume it know exactly what they must satisfy.
|
|
- `tests/dep-graph.test.ts` — pins zero `@suaveplan/*` runtime deps beyond `api-types`, `error`, `types`, and `opentelemetry`.
|
|
|
|
## Migration impact
|
|
|
|
None — additive only. `api-sandbox-vm` and `api-sandbox-wasm` (Stage 8 siblings) take this as a dependency.
|