Chapter 20 of 30

Capability Is Not Authority

Concepts

CHAPTER 20 β€” Capability Is Not Authority

STATUS

Pinned Stage 20 bundle executed 2026-09-14 and independently verified. CodeAI baseline 7a0d43b plus Chapter 19 observation and Chapter 20 registration revisions at a1b562a plus the uncommitted repair under review.

EDITORIAL PASS (2026-09-14)

  • Visual-mechanism pass 2026-09-14: added twin-gate flowchart (policy permit/deny diamond; child-narrows-parent diamond with ledger-rebuilt parent). Matches the chapter’s precise claim and delegation sections.
  • STALE CLAIM FIXED: chapter said replay returns before PolicyEngine.require and skips current authority. True at 7a0d43b (demo README method note); current CodeAI a1b562a _replay_action_result checks authority FIRST, then fingerprint. Section renamed “The replay path, then and now”; weakness #4, established bullet, failure mode, and new-action paragraph updated.
  • “working-tree revision” -> a1b562a; footer distinguishes 7a0d43b demo from a1b562a source (Ch19 observation, Ch20 registration, Ch22 replay).
  • Unchanged and still true: execute_action takes caller-supplied authority, not derived from the recorded directive (Ch29 capstone exhibits this).
  • Score ~905 -> ~950; evidence capped by owed Stage 20.

CENTRAL QUESTION

Who is allowed to authorize an available operation?

THESIS

Can != may != may accept. Check the supplied grant outside the model before new execution, connect declared-child registration to recorded-parent narrowing, and state exactly which paths still trust the caller. Policy membership does not establish containment or identity.

SECTION OUTLINE

  • Constructed opening: sensible correction under inspection-only authority.
  • Ability, action grant, acceptance grant; prompt compliance versus invocation gate.
  • Saltzer/Schroeder and Greshake: bounded primary-source support, mappings the book’s.
  • Capability enum, membership, small actual-API example; separate budget checks.
  • New-action branch and counting writer; separately supplied authority is not directive binding.
  • Subset rule and budget narrowing; helper existed outside registration at baseline.
  • Chapter 20 revision connects open_directive to validation and causal parent reference.
  • Preserved demo table, cumulative versus new counts, measured 29B and Stage 14 evidence.
  • Independent verifier’s actual limits; owed stage and changed-decision-basis proposal.
  • Replay exception; containment/authentication/ledger limitations; exercise; next to 21.

LOAD-BEARING CLAIMS

  1. Authority is membership in an explicit capability set, with ACCEPT separate from WRITE. [source: src/codeai/domain.py Capability, Authority]
  2. New execution checks supplied authority before adapter.execute; reuse returns earlier. [source: src/codeai/runtime.py Runtime.execute_action]
  3. Authority narrowing permits equality; child validation checks budget and authority. [source: src/codeai/domain.py Authority.narrows, Directive.validate_child]
  4. Baseline open_directive did not validate children. Chapter 20 revision reconstructs the unambiguous recorded parent, validates before append, and links the child causally. [source: src/codeai/runtime.py Runtime.open_directive]
  5. The registration check does not authenticate roots, validate task authority, or bind the separately supplied action grant to its directive. [source: src/codeai/runtime.py Runtime.open_directive, Runtime.create_task, Runtime.execute_action]
  6. The demo records 0 new invocations for denied WRITE, narrowed-child WRITE, and unrelated EXECUTE. Allowed WRITE has 1 cumulative call; fresh allowed action has 2 cumulative calls. [demo: experiments/applied-ai/evidence/authority]
  7. Measured 29B READ control denied with 0 apply calls and no file; WRITE applied once. [measured: experiments/applied-ai/evidence/execution-ladder/2026-09-14-7a0d43b]
  8. Stage 14 refused unauthorized acceptance and producing-actor self-acceptance. [measured: experiments/applied-ai/evidence/task-completion]
  9. Pinned Stage 20 grant-provenance run executed 2026-09-14: valid narrowing registered with causation to the recorded parent; widening, budget expansion, missing parent, and forged-parent cases refused before any event; WRITE-under-READ durably DENIED with 0 invocations; reopen reconstructs linkage and enforcement. Independent verifier recomputes from ledger plus receipts and rejects four seeded corruptions. [measured: experiments/applied-ai/evidence/grant-provenance/2026-09-14-a1b562a]
  10. Decision-standing-bound action authorization is a proposed seam, not implemented. [owed: stage20-decision-basis-authority]

EVIDENCE

  • Measured: experiments/applied-ai/evidence/grant-provenance/2026-09-14-a1b562a; preregistration.json (frozen before execution), run_grant_provenance.py, verify_grant_provenance.py (stdlib-only; –check-corruptions), ledger.sqlite, invocation_receipts.json, chapter-evidence-report.md. Seven cases as above; verifier checks pass and 4 seeded corruptions rejected. CodeAI a1b562a plus uncommitted repair (hashed in codebase.json).
  • authority/: README.md, results.json, run_authority_demo.py, verify_authority.py. Demo only, unpinned, in-memory ledger; summary flags rather than preserved target bytes. Read-only verifier rerun during drafting passed and caught its in-memory seeded corruption. The producer was not rerun and the bundle was not modified.
  • execution-ladder/2026-09-14-7a0d43b/authority/: denied/granted JSON and surrounding README/report. Measured narrow control; the broader bundle’s L-D spend failure remains historical and unresolved.
  • task-completion/: README.md; pinned offline-fe0797d acceptance evidence. Source inspected current acceptance.py separately. No upgrading historical evidence to the new revision.
  • Local development validation: new test_directive_registration.py (8 cases); combined policy, runtime, observation, registration checks; full suite 420 passed, one existing pytest warning.
  • Constructed code uses inspected APIs. Membership, counter, child helper, and registration continuation are teaching examples, not Stage 20 evidence.

PAPERS

  • Saltzer & Schroeder. The Protection of Information in Computer Systems. Proc. IEEE 1975. https://web.mit.edu/saltzer/www/publications/protection/ https://web.mit.edu/saltzer/www/publications/protection/Basic.html Read glossary and principles: technical capability, least privilege, complete mediation, remembered authorization decisions. No direct quote; not a certification of CodeAI.
  • Greshake et al. Not what you’ve signed up for: Compromising Real-World LLM-Integrated Applications with Indirect Prompt Injection. arXiv:2302.12173, 2023. https://arxiv.org/abs/2302.12173 β€” primary abstract opened. Attacks motivate external enforcement; no CodeAI mitigation claim. No direct quote.
  • Omitted autonomy-duration statistics and lethal-trifecta discussion: not needed to earn this mechanism; no unverified statistics or [verify-source] placeholders retained.

CONTROLS / LIMITATIONS

Fresh keys isolate new execution; replay-before-policy remains Chapter 22’s repair. The caller supplies authority; action.requested does not retain that supplied authority set. Root grants and task grants remain caller-controlled; declared-child registration is not complete delegation mediation. Direct ledger access is outside these application checks. Child registration refusals raise before append and are not durable refusal events. Actor labels are unauthenticated; ledger unsigned; no path scope, confinement, or injection defense. Authority demo verifier checks summary fields, not independent target bytes; it omits the allowed file_changed flag and fresh-action row despite its broader introductory description.

DEPENDENCIES

Chapter 19: reports and observations separate. Chapter 18: decision standing. Chapter 14: ACCEPT and self-acceptance boundaries. Chapter 16: recovery paths belong in a boundary audit.

FORWARD BRIDGE

Permission to invoke a worker and an observed effect do not establish the desired property. Chapter 21 owns verification adequacy; Chapter 22 owns repetition and replay authorization.

OPEN ITEMS

  • stage20-authority-registration: EXECUTED 2026-09-14. Follow-ups that remain open: durable registration-refusal records, caller-supplied-grant binding to recorded directives, root/task entry points.

  • stage20-decision-basis-authority. Question: should a request naming a changed decision be refused or require renewed approval? This is a proposal, not implementation. Cases: intact, changed, missing, unnamed decision; later basis change after already-performed effect. Controls: same permitted capability, target, instruction and fresh-key policy; vary only decision standing; freeze the proposed policy before running. Count invocations, refusals, standing snapshots, authority basis and event/byte deltas. Falsifiers: a changed/missing named decision runs under a preregistered refusal rule, an intact permitted case is improperly denied, or history is rewritten. Do not decide unnamed-request policy after seeing results. If supported: state the exact rule. If challenged: name counterexamples. If inconclusive: do not claim decision-bound authorization; retain as owed.

  • Capstone: carry the exact supplied grant and parent lineage to an independently observed effect and separate acceptance. Show root/task/replay bypass controls, not only denials.

  • No commits before the Chapters 19–22 consolidation review and explicit commit instruction.

  • Follow-up regression: caller-created wider parent passes its own helper but cannot replace the durable parent used by open_directive. Failed widening leaves only the original parent event, and show_run(child) stays absent after reopen. Refusal is not itself durably recorded.

  • Epistemically conditioned authority remains the separate stage20-decision-basis-authority proposal. Parent-grant narrowing does not decide whether authorization survives basis change.

Explain this chapter with AI

Copy this prompt into ChatGPT, Claude, Gemini, a local model, or another AI.

Apply this chapter with AI

Copy this prompt into ChatGPT, Claude, Gemini, a local model, or another AI.

Part 4 β€” Make It Safe and Verifiable

A sensible correction, refused

Constructed scene. A reviewer finds an incorrect cache setting and proposes the right replacement. Its worker can edit files. The person who opened the review asked for inspection only.

The correction’s quality is beside the point. An accurate proposal grants no permission to apply it. The presence of an editing tool grants none either.

Chapter 19 separated the actor’s report from evidence about the effect. Now move back to the moment before that effect: the process is about to call a worker that can change something. Which part of the system decides whether it may?

Who is allowed to authorize an available operation?

The answer must be more specific than β€œthe human.” It needs an operation, a grant, an entry point that checks the grant, and a stated trust boundary around whoever supplies it.

Can, may, may accept

The distinction is can β‰  may β‰  may accept.

Question What would answer it? What it leaves open
Can this worker edit the file? An available implementation with access to the file Permission for this request
May this request cause a write? A grant checked before the execution boundary Whether the write is correct
May this actor accept the result? A separate acceptance grant and completion contract Whether the actor’s identity is authenticated

An instruction such as β€œdo not write” can guide a model. Refusing to invoke the worker is a different mechanism. The first depends on how the instruction affects generation; the second is a branch in the surrounding program.

Greshake and colleagues demonstrated indirect prompt injection through data retrieved by LLM-integrated applications. Their attacks exploit the blurred boundary between data and instructions and can redirect application behavior. That motivates checking permission outside generated text. Showing that CodeAI’s policy layer defeats those attacks remains the book’s design argument, not the paper’s result. Greshake et al., 2023

Saltzer and Schroeder’s principles sharpen the question. Least privilege limits grants to what the job needs. Complete mediation asks that access be checked throughout the system, including recovery paths; remembered authorization decisions need scrutiny when authority changes. These are design principles, not a certification of this implementation. Their application to CodeAI’s execution and reuse paths is the book’s mapping. Saltzer and Schroeder, 1975

There is also a vocabulary trap. In their technical terminology, a capability is an unforgeable authorization ticket for an object. This chapter uses β€œcan” for technical ability. An ordinary enum value must never be mistaken for such a ticket. Saltzer and Schroeder, 1975

The grant is a set

CodeAI’s Capability enum names READ, WRITE, EXECUTE, VERSION_CONTROL, REMOTE, DESTRUCTIVE, and ACCEPT. NETWORK and VCS are absent from the members. Authority holds a set of these values; allows checks membership. WRITE stays separate from EXECUTE, and neither carries ACCEPT with it. 1

Constructed example using the actual API. These assertions illustrate membership; they are no evidence from the preserved authority stage. 2

from codeai.domain import Authority, Capability

review = Authority(frozenset({Capability.READ}))
edit = Authority(frozenset({Capability.WRITE}))

assert review.allows(Capability.READ)
assert not review.allows(Capability.WRITE)
assert not edit.allows(Capability.EXECUTE)
assert not edit.allows(Capability.ACCEPT)

This is deliberately smaller than a permissions system for a whole operating environment. The set says which category of operation is granted. File path, repository identity, network destination, expiry, and authenticated principal all live outside it. 3

A sensible application might give a reviewer READ, an editor WRITE, and a separate reviewer ACCEPT. Those are constructed role choices. An actor’s name, a tool’s installation, or the persuasive quality of its proposed change produces none of them automatically.

PolicyEngine.require is the small check that turns membership into refusal. If the requested capability is absent, it raises AuthorityDenied with a message naming the capability. No model is consulted. 4

Budget is another dimension. check_budget compares usage with declared limits and raises BudgetExceeded when a limit is exceeded. Permission to write and permission to spend remain distinct questions; the budget helper exists whether or not every action path invokes it. 5

Follow a new request to the worker

The source-inspected execute_action first appends action.requested, then looks for a prior result by idempotency key. On the new-execution branch it converts the capability and calls policy.require before the precondition check and adapter.execute; on an authority refusal it constructs a DENIED result and appends action.completed. The replay branch has its own authority check, discussed at the end of the chapter. 6

Constructed example using actual APIs. This counter makes invocation visible without a model or subprocess. The two requests use different keys so the example reaches the new-action policy check. It is teaching code, not a pinned stage run. 6

from dataclasses import replace
from codeai.adapters import ActionRequest, ActionResult, ActionStatus
from codeai.domain import Authority, Capability
from codeai.ledger import SQLiteLedger
from codeai.runtime import Runtime

class CountingWorker:
    def __init__(self):
        self.calls = 0

    def execute(self, request):
        self.calls += 1
        return ActionResult(request.action_id, ActionStatus.SUCCEEDED)

runtime = Runtime(SQLiteLedger())
worker = CountingWorker()
request = ActionRequest(
    action_id="review-edit", task_id="review", capability="write",
    instruction="Apply the proposed correction.", precondition_hash=None,
    idempotency_key="review-edit", requested_by="reviewer",
    actor_id="worker", adapter="counting-worker",
)
denied = runtime.execute_action(
    request, authority=Authority(frozenset({Capability.READ})), adapter=worker,
)
assert denied.status == ActionStatus.DENIED
assert worker.calls == 0

allowed = runtime.execute_action(
    replace(request, action_id="approved-edit", idempotency_key="approved-edit"),
    authority=Authority(frozenset({Capability.WRITE})), adapter=worker,
)
assert allowed.status == ActionStatus.SUCCEEDED
assert worker.calls == 1

The counter establishes only invocation within this constructed program. The worker edits no file. Chapter 19’s independent reading is still needed to establish a resulting file state.

Notice what crosses the API: the caller supplies authority as an argument. execute_action derives that argument from neither the recorded directive nor an authenticated requester. The supplied authority set is also missing from action.requested; that event serializes the request, which lacks an authority field. A successful denial leaves those boundary limits in place. 6

The precise claim is therefore: the new-action branch checks the supplied grant before invoking its adapter. It is not yet β€œevery action is authorized by its recorded directive.” 6

The two gates side by side β€” permission at the moment of action, narrowing at the moment of delegation:

    flowchart TD
    OP["operation available<br/><i>can: the worker exists</i>"] --> G{"grant checked<br/><i>may: policy gate<br/>before any effect</i>"}
    G -->|"permit"| AD["adapter invoked"]
    G -->|"deny"| DN["DENIED recorded<br/><i>adapter never runs</i>"]
    P["recorded parent grant<br/><i>rebuilt from the ledger,<br/>not caller assertions</i>"] --> C{"child narrows parent?<br/><i>capabilities βŠ† Β· budget ≀</i>"}
    C -->|"yes"| REG["child registered<br/><i>causation linked</i>"]
    C -->|"no"| REF["refused before append<br/><i>control flow only</i>"]
  

Delegation must not enlarge the grant

A review may be split into smaller jobs. The child needs some subset of the parent’s permissions. Passing a task to another worker is no occasion to manufacture a permission that the parent lacked.

Authority.narrows(parent) is a subset test. Equality counts as narrowing here: it means β€œdoes not expand,” not β€œmust remove at least one capability.” Directive.validate_child checks both authority and budget narrowing. A finite parent budget cannot become an unlimited child budget under Budget.narrows. 7

Constructed example using actual APIs. The numbers are chosen fixture limits, not measured usage. This fragment shows the existing validation helper, not registration in the ledger. 8

from codeai.domain import Authority, Budget, Capability, Directive

parent = Directive(
    directive_id="review-parent", objective="Review the change",
    success_criteria=(), budget=Budget(max_tokens=1000),
    authority=Authority(frozenset({Capability.READ, Capability.WRITE})),
)
child = Directive(
    directive_id="review-child", parent_directive_id="review-parent",
    objective="Inspect only", success_criteria=(),
    budget=Budget(max_tokens=500),
    authority=Authority(frozenset({Capability.READ})),
)
parent.validate_child(child)

But the helper’s existence was not enough. At baseline 7a0d43b, Runtime.open_directive appended a directive without calling validate_child, even when it declared a parent. The runtime registration path left the narrowing rule unenforced. 9

The authority demo calls parent.validate_child(child) explicitly before using the child’s grant. It exercises the helper and a subsequent action denial. Registration itself performing validation is something it leaves undemonstrated. 10

This is the named gap: child validation existed outside the registration path.

Connect the check to registration

The Chapter 20 revision, now in CodeAI a1b562a, changes open_directive for directives that declare parent_directive_id. It reads the recorded parent, reconstructs its budget and authority, and calls parent.validate_child before appending the child. The child event’s causation_id names the parent event used for validation. 9

Grant provenance comes from the ledger. The caller supplies the child and its parent ID, not an alternative parent object for validation. The runtime reconstructs the parent from its recorded event. 9

The revision requires an unambiguous recorded parent. Missing or multiple matching parent records, self-parenting, and an already-registered child ID raise before a child event is appended. A narrowing failure also raises before append. Refusals on this registration path are exceptions; durable refusal events for them exist nowhere yet. 9

Refused in control flow β‰  refusal durably recorded. Here an invalid child produces no child registration; mistaking that absent registration for a durable account of why an attempt was refused would be an error. 9

Constructed continuation using the actual revised API. Using the parent and child from the preceding example, registration now performs the check itself. This is an implementation example, not a result borrowed from the older demo. 9

from codeai.ledger import SQLiteLedger
from codeai.runtime import Runtime

runtime = Runtime(SQLiteLedger())
parent_event = runtime.open_directive(parent)
child_event = runtime.open_directive(child)
assert child_event.causation_id == parent_event.event_id

This is a small connection, not a complete delegation system. A caller can still submit a root directive without a parent, and root registration authenticates no grantor. create_task enforces no task-authority narrowing, and action execution still receives its authority separately. Direct ledger writes remain outside these entry-point checks. 11

The registration change therefore answers one specific question: does a declared child opened through this runtime narrow its recorded parent? The pinned Stage 20 run below validates the revised registration across seven cases with an independent ledger-based verifier.

What the preserved demo says

The existing authority demonstration used a counting adapter that appended markers to a temporary file. It used fresh idempotency keys, no model calls, and an in-memory ledger. Its preserved output is a summary in results.json, not a durable exported action history with the target bytes attached. 10

Case Recorded result Scope of observation
WRITE under WRITE succeeded, 1 adapter call, file changed First allowed invocation
WRITE under READ denied, 0 new invocations, file unchanged Cumulative adapter count remains 1
Narrowed child Helper accepted; WRITE denied; 0 new invocations Parent READ/WRITE, child READ
Widened child Helper raised authority-narrowing error Explicit helper call, not registration
EXECUTE under WRITE denied, 0 new invocations Different capability is not implicitly granted
Fresh permitted action succeeded, cumulative adapter count 2 A fresh key reaches the execution path
ACCEPT separation WRITE cannot require ACCEPT; ACCEPT can Policy-level check, not full task acceptance

These are the values and distinctions in the preserved summary. In particular, the denied case’s adapter_calls: 1 is cumulative; its new_invocations: 0 is the relevant denial measurement. Mixing those columns would turn a successful refusal into an apparent violation. 10

The exact refusal message for the denied WRITE is capability denied: write. The widened child reports child directive authority must narrow the parent authority. Neither result depends on whether the proposed edit was useful. 10

A measured effect, and a separate acceptance

Stage 29B includes a commit-pinned authority control. Under READ, the resolved answer’s WRITE effect was denied with 0 apply-adapter calls, no file written, and a person-ask reason of authority_missing:write. Under WRITE, the effect ran once and wrote a configuration containing ttl_seconds = 300. The control files retain authority, invocation counts, and resulting target state. 12

This is a bounded control from another stage. It tests neither the Chapter 20 registration revision; that test is the pinned Stage 20 run below. Neither establishes comprehensive authority enforcement.

Acceptance is a different boundary. In the Stage 14 negative cases, an acceptor holding every capability except ACCEPT was refused for unauthorized, and the producing actor’s self-acceptance was refused for self_acceptance. Those are acceptance results from that pinned run, not extra cases invented for the authority demo. 13

By current source inspection, accept_task checks ACCEPT before its prior-acceptance lookup. Its source validation compares the acceptor label with the producing actor label. That comparison separates named roles, not authenticated people. 14

An edit can therefore be authorized without being accepted as finished work. Whether the result meets an adequate check is Chapter 21’s question; this chapter establishes why permission to produce carries no silent permission to accept.

Checking it without trusting it

The authority demo’s verifier imports no CodeAI code for its predicates. It reads the summary fields, checks denied-path invocation counts, the denied-file unchanged flag, narrowing flags, and ACCEPT separation. Its seeded corruption changes the denied case’s new invocation count to one and is designed to be rejected. 10

Independence of the predicate is useful, but its input is still the producer’s summary. An independently preserved target file goes unexamined, and permission is never re-derived from exported request and grant records. The allowed case’s file_changed flag and the fresh-action row go unchecked as well. Its coverage is narrower than its introductory description. 10

The pinned grant-provenance run

The Stage 20 bundle in experiments/applied-ai/evidence/grant-provenance/2026-09-14-a1b562a/ ran under a protocol frozen before execution. Two durable parents were recorded (READ+WRITE with a 1000-token budget; READ-only with 500), and seven cases ran against them on one ledger, with a second Runtime reopening the same file for the reconstruction case. A stdlib-only verifier recomputes from the ledger and the external invocation receipts.

Case Outcome
Valid narrowing (READ child under READ+WRITE parent) registered, with causation to the parent event
Capability widening (EXECUTE under READ+WRITE) refused at registration; no event appended
Budget expansion (2000 tokens under 1000) refused at registration; no event appended
Missing parent refused at registration; no event appended
Caller-forged broader parent (WRITE child under the recorded READ-only parent) refused: the recorded parent governs, not the caller’s broader story
Unauthorized action (WRITE under READ authority) DENIED as a durable action.completed, 0 adapter invocations
Reopen reconstruction (grandchild under the recorded child) registered with causation intact; a denial after reopen stays denied with 0 invocations

The forged-parent case is the load-bearing one for this chapter’s question: a WRITE child that would be valid under a broader imagined parent is refused because the recorded parent holds READ. Registration reads the parent from the recorded directive.opened payload; the caller supplies only the child and a parent ID.

Seeded corruptions mark where enforcement ends as well as where it holds: a deleted durable denial, a forged registration for the widened child, a rewritten grandchild causation link, and an inflated denial receipt are each rejected with the failure named. The retained limitation is stated plainly: a refused widening raises before appending, so the refusal itself lives only in control flow β€” control-flow refusal is not durable refusal evidence β€” while action denials, by contrast, are durable completions.

The replay path, then and now

At baseline 7a0d43b, a reused action result was returned before PolicyEngine.require ran. The adapter was not invoked again, but the current request’s authority went unchecked either, so “every request reaches the policy check” was false for that path. The authority demo’s own method note records the ordering; its fresh keys avoided the path rather than testing it. 10

Current CodeAI checks authority first on the replay path as well. A request that reuses a key under a grant lacking the capability receives its own DENIED completion before anything recorded is consulted, and learns nothing about the recorded operation. Chapter 22 builds that repair together with the operation-identity check beside it. Here it matters for one reason: complete mediation has to cover the path that returns old results, not only the path that creates new ones. 15

The same care applies to a changed decision basis. One proposed seam would require an action that names a Stage 18 decision to check its current standing before execution. A basis_changed result could then trigger refusal or renewed approval under a declared policy. That is a proposed rule; capability membership alone leaves it unimplemented, and it was deliberately kept out of the pinned run above. It remains future construction, not a result.

Such a test would have to distinguish intact, changed, missing, and unnamed decisions, record whether the adapter ran, and retain the historical decision and its basis. Consent must never be inferred from the mere presence of a decision ID in a payload.

What this is not

  • Containment is missing. The action entry point runs the adapter with no operating-system sandbox around it. 6
  • Authentication is missing. Requester and actor fields arrive as caller-supplied strings. 16
  • A tamper-evident ledger is missing. The event definition and append path sign no records. 17
  • Prompt-injection protection is unclaimed. The cited attacks motivate the boundary; no attack-resistance experiment is claimed here, and correctness verdicts belong to the next chapter, since permission plus an observed effect still leaves verification open.

Where it is still weak

  1. The caller supplies the grant. Action execution does not resolve and enforce the recorded directive’s authority. 6
  2. Declared children are only one entry point. Root registration and task creation still leave other ways to express broader work. 18
  3. Action authorization lacks a complete durable basis. The request event does not capture the authority argument. 6
  4. Replay authority is newer than its evidence. The replay path now checks current authority, shown by source inspection and regression tests but by no pinned stage. 15
  5. New registration refusals are not recorded. They raise before append; a later reader needs external evidence to reconstruct the attempt. 9
  6. The pinned registration stage has run. What remains: registration refusals still raise without a durable refusal event, and decision-basis gating of actions is unbuilt future work.

Do this now

Thirty minutes. Find the last branch before one effect.

  1. Choose a disposable file action. Write down the capability it requests and where its authority comes from. Do not infer either from the instruction text.
  2. Supply a grant that lacks the capability. Count adapter invocations and read the file independently. Preserve the request, supplied grant, status, counter, and bytes.
  3. Delegate to a child with fewer permissions, then attempt a wider child. Trace the actual registration path; do not stop at finding a validation function.
  4. Separate authority to edit from authority to accept. List the remaining trust assumptions: caller, actor identity, replay, file scope, and ledger access.

If you are building with an assistant:

Make authorization visible at a named application boundary.
Use explicit grants, and check new actions before invoking their adapters.
Keep technical ability, authority to act, and authority to accept separate.
Connect declared-child registration to validation against the recorded parent.
Preserve which parent was used; refuse ambiguous parentage.
Test with counters and independently read bytes, using fresh keys for new
execution and a separate replay control. Preserve failures and missing records.
Do not claim authentication, sandboxing, or complete mediation from a set
membership check. Report every path that still trusts a caller-supplied grant.

Failure modes

  • Letting a good proposal authorize itself. Evidence for a correction is not permission to apply it.
  • Treating an enum as a protected ticket. A name for an operation is not an authenticated grant.
  • Finding a validator and stopping the audit. The path that records the child must actually call it.
  • Counting cumulative calls as new calls. A denied request can follow an earlier permitted effect.
  • Giving the editor acceptance by default. Producing and accepting are separate responsibilities.
  • Auditing only the new-action path. A check that guards creation but not replay is not complete mediation.

What this chapter established

  • Can, may, and may accept are different questions; a prompt instruction is not an execution gate.
  • The preserved authority demo shows denied new actions with no new adapter invocations, within its declared fixture. 10
  • The measured Stage 29B control pairs a denial with no write and a grant with an observed write. 12
  • The Chapter 20 revision connects declared-child registration to recorded-parent validation, and the pinned grant-provenance run in experiments/applied-ai/evidence/grant-provenance/2026-09-14-a1b562a/ validates it across narrowing, widening, budget, parentage, forgery, denial, and reopen cases with an independent verifier that rejects four seeded corruptions. 9
  • Caller-supplied grants, unauthenticated labels, and missing containment keep the authority claim bounded. The replay path skipped the check at 7a0d43b and checks current authority now. 19

Next

The split matters just as much when the thing being changed is your own tooling. An assistant can propose how your software should adapt; applying that change is an authorization, not a capability, and Chapter 30 keeps it in your hands.

The process may now have permission to invoke the worker. The worker may have changed the file. Neither answers whether the resulting state satisfies the property the task actually cares about.

Continue with The Agent Cannot Grade Its Own Homework.

References

  • Jerome H. Saltzer and Michael D. Schroeder. The Protection of Information in Computer Systems. Proceedings of the IEEE 63(9):1278–1308, 1975. Paper and glossary, basic principles.
  • Kai Greshake, Sahar Abdelnabi, Shailesh Mishra, Christoph Endres, Thorsten Holz, and Mario Fritz. Not what you’ve signed up for: Compromising Real-World LLM-Integrated Applications with Indirect Prompt Injection. arXiv:2302.12173, 2023. Paper.

Implementation sources: CodeAI baseline 7a0d43b for the historical demo; a1b562a, containing the Chapter 19 observation, Chapter 20 child-registration, and Chapter 22 replay-authority revisions, for current source. src/codeai/domain.py: Capability, Authority, Budget, Directive.validate_child; src/codeai/policy.py: PolicyEngine.require, check_budget, AuthorityDenied; src/codeai/runtime.py: open_directive, create_task, execute_action; src/codeai/acceptance.py: accept_task, _validate_source; src/codeai/adapters.py: ActionRequest; src/codeai/ledger.py: Event, SQLiteLedger.append. Tests: tests/test_directive_registration.py, tests/test_policy.py, tests/test_runtime.py, tests/test_task_acceptance.py, and Chapter 19’s tests/test_action_observation.py. Book-repository evidence: experiments/applied-ai/evidence/authority/ (README, producer, results, verifier), experiments/applied-ai/evidence/execution-ladder/2026-09-14-7a0d43b/authority/, and experiments/applied-ai/evidence/task-completion/. Historical evidence does not establish the later working-tree revision.


  1. Read in src/codeai/domain.py β€” Capability, Authority↩︎

  2. Read in src/codeai/domain.py β€” Authority.allows↩︎

  3. src/codeai/domain.py defines Authority as a plain set wrapper. ↩︎

  4. The gate lives in src/codeai/policy.py β€” PolicyEngine.require↩︎

  5. Budget logic in src/codeai/policy.py β€” PolicyEngine.check_budget↩︎

  6. Traced in src/codeai/runtime.py β€” Runtime.execute_action↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  7. Narrowing helpers in src/codeai/domain.py β€” Authority.narrows, Directive.validate_child, Budget.narrows↩︎

  8. Directive.validate_child in src/codeai/domain.py↩︎

  9. Registration path in src/codeai/runtime.py β€” Runtime.open_directive↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  10. Unpinned demonstration: experiments/applied-ai/evidence/authority↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  11. Entry points in src/codeai/runtime.py β€” Runtime.open_directive, Runtime.create_task, Runtime.execute_action↩︎

  12. Measured run: experiments/applied-ai/evidence/execution-ladder/2026-09-14-7a0d43b↩︎ ↩︎

  13. Measured run: experiments/applied-ai/evidence/task-completion↩︎

  14. Acceptance check in src/codeai/acceptance.py β€” accept_task, _validate_source↩︎

  15. Replay logic in src/codeai/runtime.py β€” Runtime._replay_action_result↩︎ ↩︎

  16. Request shape in src/codeai/adapters.py β€” ActionRequest↩︎

  17. Append path in src/codeai/ledger.py β€” Event, SQLiteLedger.append↩︎

  18. Two entry points in src/codeai/runtime.py β€” Runtime.open_directive, Runtime.create_task↩︎

  19. Both action paths in src/codeai/runtime.py β€” Runtime.execute_action, Runtime._replay_action_result↩︎