3.8 KiB
Permission checker (in-package default)
(no submodules)
Purpose
createPermissionChecker compiles a PermissionsManifest
(from @suaveplan/api-types) into a structural PermissionChecker that
the vm.Context sandbox host consults before forwarding any
filesystem, outbound-HTTP, or capability call to the host. The default
implementation lives here so the package ships functionally complete
even before the full @suaveplan/api-sandbox-permissions package
arrives .
This module exists to make policy enforcement a first-class operation
of the sandbox host without depending on a heavier policy engine.
Callers that need budget tracking or per-capability payload gating
construct a stricter checker and inject it through
createVmSandbox({ checker }).
Features
- Glob-compiled allowlists for
permissions.fs[](path globs with*,**,?) andpermissions.fetch[](exact host or*.suffixpattern). - Path normalisation rejects
..-traversal before matching so"/srv/data/../../etc/passwd"is matched as"/etc/passwd", not fooled into"/srv/data/passwd". - Returns frozen
PermissionDecisionobjects so the proxy hot path never accidentally mutates the policy answer. - All decisions are synchronous; the policy answer never performs IO.
Usage: Basic
import { createPermissionChecker } from "@suaveplan/api-sandbox-vm/permissions/checker";
const checker = createPermissionChecker({
policy: {
fs: ["/srv/data/**"],
fetch: ["api.example.com", "*.cdn.example.com"],
},
});
checker.checkFs("/srv/data/users.json", "read").allow; // true
checker.checkFetch("https://api.example.com/v1/users").allow; // true
checker.checkFetch("https://evil.example.org/leak").allow; // false
Usage: Advanced
Compose with a host-supplied audit sink so every denial is observable:
import { createPermissionChecker } from "@suaveplan/api-sandbox-vm/permissions/checker";
import type { PermissionChecker } from "@suaveplan/api-sandbox-vm/permissions/checker";
function withAudit(
inner: PermissionChecker,
audit: (capability: string, reason: string) => void
): PermissionChecker {
return {
checkFs(path, mode) {
const d = inner.checkFs(path, mode);
if (!d.allow) audit(`fs.${mode}`, d.reason);
return d;
},
checkFetch(url) {
const d = inner.checkFetch(url);
if (!d.allow) audit("fetch", d.reason);
return d;
},
checkCapability(id, payload) {
const d = inner.checkCapability(id, payload);
if (!d.allow) audit(id, d.reason);
return d;
},
};
}
API Reference
createPermissionChecker(options)— build a checker from aPermissionsManifest.options.policyis the manifest block exactly as declared inplugin.json.PermissionChecker— structural type with synchronouscheckFs,checkFetch,checkCapabilitymethods.PermissionDecision— discriminated union of{ allow: true }or{ allow: false; reason: string }.
Implementation Notes
The glob compiler escapes regex meta-characters first, then performs
the ** → .*, * → [^/]*, ? → [^/] substitution in three
passes via a sentinel token to keep the ordering safe (replacing **
to .* before * to [^/]* would otherwise mis-translate **).
Host patterns use a string-suffix match rather than a regex to keep
the hot path branch-predictable. The checkCapability default is
permissive precisely because the full payload-shape contract is owned
by api-sandbox-permissions; hosts SHOULD compose a stricter checker
once that package ships. The in-package fallback still rejects an
empty capability id so that an obviously-malformed call fails closed
rather than silently succeeding.