Who owns which authority question¶
MAC has several primitives that all look like "is this allowed?" and answer different questions. That ambiguity is not theoretical: it produced two confirmed holes in one week, and a 34-route audit found that nothing at all was blocking a non-admin token on the routes that relied on the wrong one.
This document fixes the boundary. It is descriptive of what the code does
today, and tests/api/test_authority_boundary.py pins the parts that must not
drift.
The framing is borrowed from ~/Src/literate-ai's
docs/architecture/agent-ledger-boundary.md, which draws the same line between
a ledger and a derivation engine:
The ledger's customer or organizational consent is not the execution grant that authorizes a build, test, or publication step.
MAC is both systems at once, so the line has to be drawn inside it.
The table¶
| Question | Owner | Not the owner |
|---|---|---|
| Who asked for this work, and under whose organizational authority? | the task ledger — actor, origin, directives, approvals |
— |
| May this principal reach this route at all? | _required_scope(method, path) |
anything in the handler body |
| Is this principal confined to a tenant? | TokenPrincipal.refuse_tenant_bound |
— it answers only this |
| Is this principal acting as itself? | TokenPrincipal.assert_actor |
— |
| Does this call need an operator rather than a service? | TokenPrincipal.require_admin |
— |
| May this agent execute anything at all on a host? | the allocator's execution-boundary gate (AGENT_NO_EXECUTION_BOUNDARY) |
capability matching |
| Under what confinement does the work run? | the OpenShell policy, resolved per host and (optionally) per task | the task body |
| May this run escape confinement, once? | a lease-bound break-glass authorization | any token scope |
| What may the sandbox reach on the network? | the project's declared egress + the reviewed registry allowlist | anything the repo says about itself |
| What did the agent actually do? | action_events, command_audit, evidence blobs |
— |
The invariant¶
Authorization is decided by _required_scope. Everything else narrows.
A route's privilege is the scope it is given there. In-handler guards can refuse
more — a tenant-bound token, a different agent, a non-admin — but they cannot
supply privilege a route was never given. A route that needs to be operator-only
says so in _required_scope; it does not acquire that property by calling
something in its body.
There is one deliberate exception, and it is the reason the executable half
asserts behaviour and not only scope. The debug-terminal routes must stay
reachable by a worker acting on itself — a blanket admin scope would forbid
the legitimate case — so they sit at read/write and are narrowed in the
handler by _require_terminal_principal (admin, or an agent acting on itself).
Scope alone therefore does not prove those routes are safe; only a request does.
Writing this document's tests is what surfaced that, having first asserted the
tidier claim and watched it fail.
How this was learned¶
refuse_tenant_bound was called require_global_fleet, which reads as "require
fleet-level authority". It is:
It refuses only tenant-bound non-admin tokens. An untenanted client token —
the ordinary read/write credential — passes untouched. It was relied on at
61 call sites.
An audit probed all 34 routes where it was the only in-handler gate below admin,
using the fact that FastAPI validates the request body before the handler runs
(so a 422 proves auth already passed):
Two were real holes and are fixed:
- an ordinary
writetoken could open a debug shell on any fleet host and send it keystrokes (PR #300); - the same token could create, update, delete and assign the OpenShell
guardrail policy every
--yoloagent runs under — and then be refused when it tried to read back what it had just written (PR #303).
The remaining 22 have legitimate non-admin callers — bootstrap registers
machines, CI records integration findings, the deploy scope promotes runtime
deltas — so write/deploy is their correct level and they were never
vulnerabilities. The method was renamed in PR #305 so the assumption cannot be
made a 62nd time.
Consent is not an execution grant¶
These are separate and must stay separate:
| Answers | Carried by | |
|---|---|---|
| Consent | "this work was asked for, by someone entitled to ask" | the ledger: task, actor, origin, approval, directive |
| Execution grant | "this exact operation may run, here, now, with these resources" | scope + confinement policy + break-glass, per call |
A task existing in the ledger is not permission to run it on a host. An agent holding a lease is not permission to escape its sandbox. The ledger's record of who wanted it and the runtime's record of what was allowed answer different questions and are stored separately on purpose.
The clearest live example is break-glass: an operator's organizational decision to allow one host execution becomes a lease-bound, single-use, audited authorization, projected onto exactly one assignment and stripped from every other. Consent is durable and human; the grant is narrow, typed and expiring.
MAC enforces this more strictly than "strip it later": a task that tries to
carry runtime.break_glass_authorization in its own metadata is refused at
creation with "break-glass execution metadata is control-plane-owned". The
assignment-time strip is the second line, not the first. (This document
originally claimed only the strip; the test found the stronger guarantee.)
Trust tiers for anything a repository says about itself¶
Repository content is attacker-controllable — anyone who can open a pull request can change it. So a repo's own statements are proposals, never grants:
| Tier | Source | Granted when |
|---|---|---|
derived_trusted_registry |
the repository working tree (.npmrc, lockfiles) |
it matches a small reviewed registry allowlist |
hub_declared |
control-plane state an operator set | it is well-formed |
This is why sandbox egress is declared on the project, not read from the
repository's own contract: project_repositories loads that contract from
.mac/project.yaml inside the checkout, which is repo content and belongs in
the untrusted tier.
Current status — what is NOT true yet¶
Stated plainly, in the spirit of the document this borrows from:
_required_scopeis a path-matching function, not a registry. It is correct today and pinned by tests, but a new route inherits a scope by where its path happens to fall in a chain ofifs. Nothing forces an author to make a decision.- 22 routes still rely on
refuse_tenant_boundalone. That is correct for them — they have legitimate non-admin callers — but "correct by accident of who happens to hold a token" is weaker than "correct by declaration". - There is no derived explanation layer. MAC has the forensic half —
action_events,command_audit, evidence blobs — and no human-facing account of what was decided and why. literate-ai's boundary document is explicit that the raw journal is forensic evidence, not the default user interface. - Execution grants are not uniformly typed. Break-glass is a typed, lease-bound record. Confinement is a policy file. Network reach is a projected contract. They are three shapes for one idea.
References¶
~/Src/literate-ai/docs/architecture/agent-ledger-boundary.md— the ownership framing, and the consent-versus-grant distinctiondocs/structured-task-bodies.md— the related question (should a task body be a component transition?) and why the answer was nodocs/openshell-sandbox.md— confinement, policy delivery, per-repo egresstests/api/test_authority_boundary.py— the executable half of this document