Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,101 @@
<!-- markdownlint-disable MD024 -->
# Capability Spec Delta: api-sandbox-vm (api-sandbox-vm)
**Package**: `@suaveplan/api-sandbox-vm`
**Status**: PROPOSED
> Every requirement below carries a `**Verified by:**` line at authoring time.
## ADDED Requirements
### Requirement: Sandbox boots inside a frozen `vm.Context`
The package SHALL expose `createVmSandbox({ pluginModulePath, policy, proxies })` that constructs a `vm.Context` whose `globalThis` is sealed and frozen. The sandbox MUST expose only the curated `FrozenGlobals` allowlist (`URL`, `URLSearchParams`, `TextEncoder`, `TextDecoder`, `structuredClone`, logger-routed `console`, subset of `crypto`). Any other global (e.g. `process`, `Buffer`, raw outbound HTTP client, raw `require`, `import.meta.url` outside the sandbox path) MUST be absent.
#### Scenario: process global is unreachable
- WHEN code inside the sandbox references `process`
- THEN evaluation throws a `ReferenceError` AND the sandbox surfaces it as a `SAND_VM_*`-prefixed `SuaveplanError`
#### Scenario: globalThis is frozen
- WHEN code inside the sandbox attempts `globalThis.foo = 1`
- THEN the assignment throws under strict mode AND no property is added to the sandbox globals
**Verified by:** `src/sandbox/sandbox.test.ts::"process global is unreachable inside sandbox"` AND `src/sandbox/sandbox.test.ts::"globalThis is frozen"`
---
### Requirement: Dynamic code constructors are rejected
The package SHALL ensure direct calls to dynamic-code constructors (the `Function` family — sync, async, generator), direct dynamic-code execution, and indirect dynamic-code execution all throw inside the sandbox. The thrown error MUST be a `SuaveplanError` with code `SAND_VM_EVAL_REJECTED` or `SAND_VM_INDIRECT_EVAL_REJECTED`.
#### Scenario: direct dynamic execution is rejected
- WHEN sandboxed code attempts to evaluate a string of source via the built-in dynamic-evaluator
- THEN the call throws `SAND_VM_EVAL_REJECTED`
#### Scenario: indirect dynamic execution is rejected
- WHEN sandboxed code attempts indirect dynamic evaluation through the comma-operator trick
- THEN the call throws `SAND_VM_INDIRECT_EVAL_REJECTED`
**Verified by:** `src/sandbox/sandbox.test.ts::"direct dynamic execution is rejected"` AND `src/sandbox/sandbox.test.ts::"indirect dynamic execution is rejected"`
---
### Requirement: fs and fetch are host-proxied only
The package SHALL install `fs` and outbound-HTTP proxy stubs that route every call to the host via `installCapabilityProxies`. Every proxy call MUST consult `PermissionChecker` from `api-sandbox-permissions` BEFORE forwarding to the host. Denied calls MUST raise `PERMISSION_DENIED` inside the sandbox without contacting the host.
#### Scenario: allowed fs read proxies to host
- WHEN sandboxed code calls the fs-proxy with a path matching `permissions.fs[]`
- THEN the request reaches the host adapter once AND the response is delivered to the sandbox
#### Scenario: denied outbound HTTP does not reach the host
- WHEN sandboxed code calls the outbound-HTTP proxy with a host outside `permissions.fetch[]`
- THEN the call raises `PERMISSION_DENIED` inside the sandbox AND no host-side outbound request is initiated AND `plugin.permission_denied_total{capability="fetch"}` increments
**Verified by:** `src/proxies/fs-proxy/fs-proxy.test.ts::"allowed read proxies once to host"` AND `src/proxies/fetch-proxy/fetch-proxy.test.ts::"denied fetch never contacts host AND increments denial metric"`
---
### Requirement: Module loading is restricted to the plugin's own module
The package SHALL refuse `vm.SourceTextModule` import specifiers that resolve outside the plugin module graph. Only relative imports rooted at `pluginModulePath` and a fixed allowlist of platform globals (no node built-ins) are loadable. Forbidden imports raise `SAND_VM_MODULE_IMPORT_FORBIDDEN`.
#### Scenario: node built-in import is rejected
- WHEN sandboxed code imports `"node:fs"`
- THEN module instantiation throws `SAND_VM_MODULE_IMPORT_FORBIDDEN`
**Verified by:** `src/sandbox/sandbox.test.ts::"node built-in imports are forbidden"`
---
### Requirement: Conformance harness exported
The package SHALL export `runVmSandboxConformance(label, makeSandbox)` on `./testing` and the in-package fixture sandbox MUST pass it. The harness re-runs every scenario above so a future vm-based host (e.g. an alternative isolate library) is gate-checked against the same contract.
#### Scenario: in-package fixture passes harness
- WHEN `runVmSandboxConformance("self", makeFixtureSandbox)` runs
- THEN every harness `it(...)` passes
**Verified by:** `src/testing/run-vm-conformance.test.ts::"in-package fixture passes runVmSandboxConformance"`
---
### Requirement: Disposal is idempotent
The package SHALL ensure `VmSandboxHandle.dispose()` releases the `vm.Context`, cancels every in-flight proxy call, and is safe to invoke multiple times. A second call MUST be a no-op (no error, no double-free).
#### Scenario: dispose twice is a no-op
- WHEN `dispose()` is called twice on the same handle
- THEN the second call returns without error AND no second teardown side-effect is observed
**Verified by:** `src/sandbox/sandbox.spec-conformance.test.ts::"dispose is idempotent"`