5.1 KiB
@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 resolvedpermissions.fs[]allowlist,permissions.fetch[]allowlist, andpermissions.budgets(cpuPercent / memoryMb / wallClockMs). Built fromPermissionsManifest(api-types). - ADDED:
PermissionChecker.checkFs(path, mode)— verifies a host-proxied filesystem path matches the allowlist (glob match againstpermissions.fs[]); returnsAllow | Deny<reason>. - ADDED:
PermissionChecker.checkFetch(url)— verifies an outbound destination host matchespermissions.fetch[]; supports exact host and*.suffixpatterns. - ADDED:
PermissionChecker.checkCapability(capabilityId, payload)— fordb/secrets/eventspayload-shape gating per §5.2b (secret name in manifest, event topic in declaredpublishes/subscribes, DB query scoped to plugin's declareddatabase). - ADDED:
BudgetTracker— tracks per-request CPU sample, resident-memory sample, and wall-clock elapsed; raisesBUDGET_EXCEEDEDwhen any budget is breached. Counters are caller-supplied perClock/ sampling hooks. - ADDED:
PERMISSION_DENIEDandBUDGET_EXCEEDEDerror codes (SAND_PERM_*registry) plus a redaction-safedetailsschema. - ADDED:
plugin.permission_denied_totalandplugin.budget_exceeded_totalinstrument descriptors so both sandbox hosts emit identical metric names with identical label sets (plugin,capability,reason). - ADDED:
runPermissionsConformance(label, makeChecker)testing harness (subpath./testing) soapi-sandbox-vm,api-sandbox-wasm, and any future sandbox host exercise the same allow / deny / budget invariants.
Out of scope
- The actual
vm.Contextruntime (api-sandbox-vm) and the WASI runtime (api-sandbox-wasm). Both consume this package; neither is bundled here. - The host-proxy implementations for
fsandfetch— they live inapi-host(main-process side) and only callPermissionCheckerbefore 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—PermissionPolicyconstruction 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 incrementsplugin.permission_denied_totalexactly once AND raisesPERMISSION_DENIEDwith the documenteddetailsshape (verified against a real in-memoryMeterProvider).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 beyondapi-types,error,types, andopentelemetry.
Migration impact
None — additive only. api-sandbox-vm and api-sandbox-wasm (Stage 8 siblings) take this as a dependency.