3.3 KiB
Install capability proxies
(no submodules)
Purpose
installCapabilityProxies wires the host-supplied capability bags
into the sandbox's vm.Context. The sandbox sees one consolidated
__suaveplan global containing fs, fetch, and a capabilities
map; every method on the map is wrapped so the permission checker is
consulted before the host adapter is reached. The receiver and every
nested bag are frozen so a sandboxed plugin cannot replace a
permission-gated method with an open one.
This module is the single source of truth for how capabilities cross the sandbox-host boundary — both the fs/fetch proxies and the wider capability surface enter through this entry point so the permission gate is applied uniformly.
Features
- One
__suaveplanglobal withfs,fetch,capabilities— installed as non-writable, non-configurable, enumerable so the sandbox can read it but never replace it. - Permission gating on every capability method: the checker decides before the host adapter is called.
- Denials emit
plugin.permission_denied_total{plugin, capability}with stable label cardinality (no per-method label). - Frozen receivers prevent monkey-patching from inside the sandbox.
Usage: Basic
import { installCapabilityProxies } from "@suaveplan/api-sandbox-vm/proxies/install-capability-proxies/install-capability-proxies";
import { createContext } from "node:vm";
const context = createContext({});
installCapabilityProxies({
context,
checker,
fs,
fetch,
pluginId: "demo",
capabilities: {
db: {
query: async (sql: unknown) => realQuery(String(sql)),
},
},
});
Usage: Advanced
Compose with a host-side audit sink so every denial is reproducible:
import { installCapabilityProxies } from "@suaveplan/api-sandbox-vm/proxies/install-capability-proxies/install-capability-proxies";
const wrapAudit = (checker: PermissionChecker, audit: AuditSink) => ({
...checker,
checkCapability(id: string, p: Record<string, unknown>) {
const d = checker.checkCapability(id, p);
if (!d.allow) audit.record({ capability: id, reason: d.reason });
return d;
},
});
installCapabilityProxies({
context,
checker: wrapAudit(checker, audit),
fs,
fetch,
capabilities,
pluginId: "demo",
});
API Reference
installCapabilityProxies(options)— wire the proxy surface intooptions.context. Returns nothing; side-effects the context.interface InstallCapabilityProxiesOptions—context,checker,capabilities,fs,fetch,pluginId.type CapabilityMap— frozen record of capability id → method bag.type CapabilityBag— frozen record of method name → async fn.
Implementation Notes
The receiver is installed via Object.defineProperty with
writable: false, configurable: false so the sandbox cannot replace
or delete __suaveplan. Each method wrapper closes over the host
function reference; the closure is the only path to the host call, so
patching the wrapper from inside the sandbox would not bypass the
permission gate (it would only break the path that exists). The
method argument is not added to the OTel attribute set so
cardinality stays bounded; method-level granularity belongs in logs,
not metrics.