ADR-0031: Component permissions: what is enforced, and what is not¶
- Status: Accepted
- Date: 2026-10-07
- Deciders: Jian (product owner)
Context¶
ADR-0026 says every component carries a permission manifest, enforced on first-party components too: an egress allow-list, secret slots bound to hosts, and declared filesystem, env and exec permissions. KAN-1835 built the rest of that. The egress list and the slot hosts were already enforced by the op-call pipeline (KAN-1832). The open question was how much of "fs, env and exec" can honestly be enforced while a module runs in the engine's own process.
Decision¶
- Enforced on every call, for every component, first-party included: a request to a host outside
permissions.egressis refused before any network call; a secret is sent only to the hosts its slot is bound to; secrets come only from declared slots; a module'sctx.fetchrefuses an undeclared host, and after reading a secret refuses any host that secret is not bound to. - Enforced by what a module is handed: a module receives
ctx, notprocess.env.ctx.env.get(name)returns only the variables named inpermissions.env; any other name is apermissionerror. Plain env is for configuration: a secret slot's own variable cannot be named there, and neither can a name that reads as a credential (KEY,TOKEN,SECRET,PASSW,CREDENTIAL,PRIVATE), so a secret can only be read through its slot, bound to its hosts. - Checked statically, not enforced at run time:
scanModuleSources(in@kampong/spec) reads every JavaScript file of a module and refusesprocess,globalThis/global,eval,new Function,.constructor(,require, the globalfetch/XMLHttpRequest/WebSocket/EventSource, a dynamic import that is not a plain string literal (a concatenation or a template is refused), an import that leaves the component's directory or names a URL, and any import of a built-in or package the manifest does not allow: network built-ins always (usectx.fetch),fswithoutpermissions.fs,child_process/vm/worker_threadswithoutpermissions.exec, an npm package not indeps.fs: [read]is held to its word: in a file that importsfs, a call that changes the file system (writeFile,mkdir,rm,rename,open...) needsfs: [write]. The check reads code, not scope, so a barefetchorprocessis refused even when it is a local variable with that name; rename it. Comments, strings, template text and regular-expression literals are blanked first so a forbidden word there is not mistaken for code. It runs when a component is resolved (so before any of it is imported), when a listing is built, when an author pins it, and when an export is made. - This is not a sandbox and does not claim to be. A module is code running in the engine's process.
The static check catches mistakes and unsophisticated misbehaviour in reviewed code; code written to
evade it can. What the manifest declares for
fsandexecis therefore a statement a reviewer can hold the code to, not a wall. The wall is aModuleRunnerthat is not in-process. - The Runner interface says how much isolation it gives.
ModuleRunner.isolationisnoneorsandbox(absent meansnone); the in-process runner isnone.invokeOptakesrequireIsolation: "sandbox"and then refuses a module op on a weaker runner, before running anything. A host that runs components with someone else's secrets, such as the hosted server, sets it, so modules do not run there until a sandboxed runner exists. A rest component runs no code and is unaffected. No host sets it yet: the hosted server does not run components (hosted execution of custom code is deferred, ADR-0026 section 5) and the local CLI runs the author's own code by design. The option exists so that wiring it is one line when that changes. - A wider grant needs explicit re-approval.
kampong.lockrecords what each component could do when it was pinned.kampong lockprints that for a new pin.kampong lock --updatethat would widen it (a new host, env name, fs mode, exec, or a secret reaching a new host) is refused, with the list of what widened, unless--allow-wider-permissionsis also given. A secret slot re-pointed at a different environment variable counts as widening. A narrower grant or a code-only change updates normally. A pin made without a record gets one filled in by the nextkampong lock(its files are the ones that were pinned, so the record is accurate); if it is updated before that there is nothing to compare with, so anything the new version may do counts as new and needs--allow-wider-permissions, unless it may do nothing. - A rest component runs no code, so it cannot declare
env,fsorexec; a module may omitegressif it makes no request.
Consequences¶
- The sandbox choice (isolates, Deno permissions, a container) stays open, as ADR-0026 section 5 says. When
one exists it implements
ModuleRunnerwithisolation: "sandbox"and the hosted server turns onrequireIsolation; component code does not change. - Because the scan runs at resolution, a module that violates its manifest shows up as a problem in the canvas component list and cannot be pinned, run or exported.
- A first-party module is held to the same checks as any other:
kampong/gmailpasses them unchanged. - Not covered here: vetting of
deps(KAN-1838), startup re-verification of an export's files (KAN-1837), and the permission diff inkampong doctor(KAN-1836).