Files
2026-05-27 23:29:53 +01:00

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 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.