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

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 *, **, ?) and permissions.fetch[] (exact host or *.suffix pattern).
  • Path normalisation rejects ..-traversal before matching so "/srv/data/../../etc/passwd" is matched as "/etc/passwd", not fooled into "/srv/data/passwd".
  • Returns frozen PermissionDecision objects 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 a PermissionsManifest. options.policy is the manifest block exactly as declared in plugin.json.
  • PermissionChecker — structural type with synchronous checkFs, checkFetch, checkCapability methods.
  • 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.