# `checker/fs` — filesystem allowlist _(no submodules)_ ## Purpose This module owns the `permissions.fs[]` enforcement path. Every filesystem capability call routed through the sandbox host MUST pass through `checkFs` before the underlying I/O runs. The check is synchronous, deterministic, and O(rules) — never network, never disk, no closure state beyond the compiled rule list on the policy. The function is the only enforcement layer in the package that has to defend against path-traversal: a manifest rule like `./uploads/*` is intended to scope access to the uploads dir, and a request supplying `./uploads/../etc/passwd` MUST resolve to `./etc/passwd` and be denied even though the literal string still starts with `./uploads/`. ## Features - `checkFs(policy, path, mode)` — returns `{ outcome: "allow" }`, `{ outcome: "deny", reason: "path-not-in-allowlist" }`, or `{ outcome: "deny", reason: "mode-not-permitted" }`. - Mode-aware — read access defaults are wide, write access requires the rule to be authored as `rw:`. - Traversal-safe — paths are normalised through the policy's `normalisePath` helper before the rule's compiled regex sees them. ## Usage: Basic ```ts import { PermissionPolicy } from "@suaveplan/api-sandbox-permissions/policy"; import { checkFs } from "@suaveplan/api-sandbox-permissions/checker"; const policy = new PermissionPolicy({ permissions: { fs: ["./uploads/*"] }, }); checkFs(policy, "./uploads/avatar.png", "read"); // { outcome: "allow" } checkFs(policy, "./etc/passwd", "read"); // { outcome: "deny", reason: "path-not-in-allowlist" } ``` ## Usage: Advanced ```ts import { PermissionPolicy } from "@suaveplan/api-sandbox-permissions/policy"; import { checkFs } from "@suaveplan/api-sandbox-permissions/checker"; import { SAND_PERM_CODES, SandboxPermissionError, } from "@suaveplan/api-sandbox-permissions/error-codes"; const policy = new PermissionPolicy({ permissions: { fs: ["rw:./uploads/**", "./readonly/*.txt"], }, }); function gatedFsCall( plugin: string, path: string, mode: "read" | "write" ): void { const result = checkFs(policy, path, mode); if (result.outcome === "deny") { throw new SandboxPermissionError( SAND_PERM_CODES.DENIED, "fs capability denied", { context: { capability: "fs", reason: result.reason } } ); } } gatedFsCall("billing", "./uploads/temp/log.txt", "write"); // allowed by rw: rule gatedFsCall("billing", "./readonly/config.txt", "read"); // allowed read-only ``` Hosts MAY also wire `checkFs` directly into the worker capability proxy without throwing — when the result is `Deny`, the host fabricates a `{ ok: false, error: ... }` envelope so the worker boundary never sees a stack trace from the main process. ## API Reference - `checkFs(policy: PermissionPolicy, path: string, mode: FsAccessMode): CheckResult` - `mode = "read" | "write"` - `CheckResult = { outcome: "allow" } | { outcome: "deny"; reason: string }` - `reason` is one of `"path-not-in-allowlist"`, `"mode-not-permitted"`. ## Implementation Notes The literal-root check (`startsAtRoot`) is what defends against `..` escape: a rule like `./uploads/*` has `root = "./uploads"`, and a resolved input that doesn't start with `"./uploads"` (or equal it) fails the gate before the regex runs. The mode check is a soft veto inside the loop — a write attempt against a read-only matching rule sets `modeBlocked` and keeps scanning, so a later `rw:` rule on the same path still wins. Only when every matching rule was read-only AND write was requested does the deny reason become `"mode-not-permitted"`. This keeps the rule list order-insensitive.