Agent¶
Agents are Markdown documents with YAML frontmatter. The filename is the agent
ID. An agent defines the system prompt, permission-controlled tool access, its
model fallback chain, and its runtime limits. The harness supplies four agents:
three internal agents plus the synthesized default coding agent when no
authored root agent is runnable.
Author common agents under ~/.cookie-agent/agents/. The exact workspace may
use the same document format under <cwd>/.cookie-agent/agents/; a same-ID
workspace document takes precedence and replaces the complete user document.
Frontmatter fields and nested permission maps never merge across layers.
Agent document structure¶
Each file has strict YAML frontmatter and a nonempty Markdown body:
The filename is the agent ID: lowercase letters and digits separated by single
hyphens, starting with a letter or digit, at most 64 bytes. Files are limited to
256 KiB; frontmatter and body are each limited to 128 KiB. Nested maps and lists
have at most 256 entries and depth 16. YAML anchors, aliases, tags, merge keys,
and ${env: interpolation are rejected. The examples below are complete agent
documents; replace their model IDs with models available in your runtime.
Agent documents do not declare a schema or version. A leftover schema field is
a hard error directing the author to remove it; every other unknown field,
wrong type, or malformed YAML construct is also rejected.
---
description: Reviews changes for correctness
mode: subagent
enabled: true
models:
- { model: "openai/gpt-5", variant: null }
limits:
max_output_tokens: 2048
permissions:
read: allow
write: deny
bash:
"git diff*": allow
"*": ask
delegate: deny
---
Review the requested change and report concrete findings.
Frontmatter keys¶
| Key | Type | Default | Description |
|---|---|---|---|
description |
string | (required) | 1–512 bytes, no control characters. Shown in the TUI and snapshots. |
mode |
string | (required) | primary, subagent, all, or internal. |
enabled |
boolean | (required) | Disabled agents are never runnable as roots, delegation targets, or internal backends. |
models |
array | (required) | Ordered model chain; must be nonempty for primary. Other modes may declare []. |
limits |
table | defaults below | Timeouts and token bounds. |
permissions |
table | {} |
Ordered action permission map; see Permissions. At most 256 rules. |
agent_md |
boolean | (unset) | Load AGENTS.md context for this agent's root runs. Unset follows [agent_md] enabled; a value overrides it. Rejected on subagent and internal agents, which never load AGENTS.md. |
max_output_tokens applies in every mode and defaults to 0 in all of them. A
nonzero value caps each request at the smaller of the document value and the
model's own output limit. For the non-internal primary, subagent, and all
modes, 0 means no document cap.
Internal agents run on ${parent_model} by default, so 0 there means they
inherit the owner run's output cap the same way they inherit its model. The
effective cap for an internal request is the smaller of the model's own output
limit and the first nonzero value of the internal document's
max_output_tokens and the owner run's max_output_tokens; a request is left
uncapped only when the model's output limit is unknown and neither document
sets one. An explicit nonzero value in the internal document therefore still
wins over inheritance, subject to that model minimum. All three built-in
internal documents declare 0 and inherit.
timeout_ms applies only to internal agents. For other modes, a nonzero value is
a hard error. Internal agents use the configured timeout exactly; zero or an
omitted value uses the 30-second default. The built-in compaction agent has a
3-minute default, and an authored compaction.md can set its own exact timeout.
Tool visibility is derived only from permissions: with no permissions
field, no tools are visible. An action's tools become visible when the agent or
session overlay has any allow or ask rule for it — any resource pattern
counts, even if a competing deny wins at execution time — and unmatched
resources deny by default, so approval requires an explicit ask rule. edit
uses the write action. Delegation tools additionally require a delegate map
naming at least one eligible target. See
Permissions.
The former tools field is removed. Documents that still declare it fail
with an error naming tools and directing the author to permissions; remove
the field and express tool visibility and call policy in the permission map.
The former model_fallback field is also removed. Documents that still declare
it fail with an error directing the author to models.
The former limits.max_input_tokens field is removed. Internal-agent input
budgets now come from each resolved model's context limit minus its effective
output reserve; candidates that cannot fit an invocation are skipped. A model
whose context limit is unknown uses a 16,384-token input budget.
The durable protocol event for advancing through a model chain remains named
model_fallback for wire-schema compatibility. This event name is independent
of the agent-frontmatter models key.
models entries contain model, optional variant, and optional cache.
The variant field is optional: omitted (null) selects the model's configured
default variant, the string "base" selects the base variant explicitly, and any
other string selects that named variant. A primary agent must
declare at least one fallback. The chain may contain up to 256 entries with no
duplicate model keys. Only internal agents may use the ${parent_model} model
expression, and only without a variant.
cache uses the provider-specific shapes documented under
[providers.<id>.cache]. The
resolution order is family default, provider cache, then entry-level cache
for that binding. This is resolved independently for every fallback, including
mixed-provider chains. OpenAI-compatible cache keys are provider-only.
The entry-level table contains one anthropic, bedrock, or openai shape
matching the binding. It replaces the provider policy rather than merging it.
cache: {} explicitly selects no structural strategy; first-party OpenAI and
Azure still send the unconditional session-ID cache key.
Modes¶
primary— runnable as a root session agent. Must declare at least one model fallback.subagent— runnable only as a delegation target. Must be enabled to appear in another agent'sdelegatemap.all— runnable both as a root and as a delegation target.internal— engine-only (see below). Cannot be selected as a root or a delegation target.
See System Prompt Composition for the exact prompt, skill-listing, plugin, cache, and history assembly order.
AGENTS.md context¶
Root sessions automatically load AGENTS.md context at the start of every run
unless it is turned off. The run agent's agent_md frontmatter decides when it is
set; otherwise the global [agent_md] enabled switch
does. The files are read fresh, so edits apply to the next run:
.cookie-agent/agents/AGENTS.mdis the default project file. When the run uses a preset and.cookie-agent/agents/<preset>/AGENTS.mdexists, that file replaces the default project file.<cwd>/AGENTS.mdis loaded in addition when present.
Missing files add no event or model tokens. Files larger than 2 MiB are skipped
and surfaced as AgentMdSkipped warning events. Delegated sessions do not
discover these files for their own runs; explicitly inherited parent text and
forked event prefixes retain their existing behavior. Internal agents never
discover them. Loaded entries are persisted with provenance in
agent_md_loaded and replayed as one user context turn wrapped in a
<system-reminder> block, not as system-prompt text.
Repository-controlled AGENTS.md content enters model context automatically.
Treat it as untrusted instructions when opening unfamiliar workspaces and review
the security guidance. Configure limits in
[agent_md].
Subagent tools¶
delegate_subagent requires a short description, a self-contained prompt,
an allowed agent_type, and accepts optional background, resume_session_id,
and inherit_context arguments. Foreground calls block and return a concise
result teaser. Background calls return immediately with the child's handle;
admission still waits for any required permission approval. A child is
addressed everywhere by its short tree-unique handle
(<agent_type>_<8 hex>, e.g. explore_1a2b3c4d) or a full session UUID; a
partial, foreign-tree, or fabricated reference is rejected with a
self-repairing error listing the live candidates. For a
new child, the description becomes the delegated session title. It is truncated
to session_title.max_chars using the same Unicode-character limit as generated
titles; invalid title text rejects the delegation.
resume_session_id accepts the handle or full UUID of an existing direct child
that was previously created by this same parent session. Top-level, unknown,
foreign, self, and ancestor sessions are rejected. A terminal child starts a new run with the new
prompt; an active child receives the prompt through its pending-input FIFO. The
current delegation link is refreshed, so result, messaging, cancel, queue, slot,
and completion-notification behavior applies to the resumed work. The existing
session title is never replaced by the new description; the description remains
only the delegation call's display argument. A child that already has a queued
or starting delegation cannot be resumed again until that invocation starts or
terminates; the second resume is rejected without replacing the first.
inherit_context = true seeds a newly created child's initial model history
from the parent's assembled history at delegation time. Only user and assistant
text is copied: system content, files, tool calls, and tool outputs/results are
dropped. The retained text is capped at 64 KiB by truncating the oldest content
first. This is a capability and privacy boundary: retained parent text crosses
into the child agent's model context and must be appropriate for that child.
inherit_context and resume_session_id cannot both be set.
Background sessions move through queued, running, and a terminal
completed, failed, interrupted, or cancelled state. Completion appends a
parent event containing the session ID and handle, status, first 20 result lines
(at most 2 KiB), and total line count. Use get_subagent_result with the
child's handle or UUID and zero-based offset/limit to read the last assistant
message of the child's most recent turn in pages; a running child (including one
just woken by send_message) reports as running and returns no text. It never
waits. If a background child is still running, tell the user that you are waiting
and end the turn; its completion notification will wake this parent session. Use
cancel_subagent with the owned handle or UUID and optional reason to cancel
it. Result and cancellation operations reject sessions that are not direct
children of the caller.
The former steer_subagent tool is removed. Agents reach children and other
same-tree peers with send_message, which is governed by the dedicated
message permission action (deny by default) and can also wake finished
sessions. See the tool reference and
the messaging migration notes.
Terminal delegated sessions are paged out of memory when resident child count
exceeds delegation.max_resident_subagents and their last run has been idle for
longer than delegation.idle_eviction_after. This is a soft trigger: running,
queued, recently active, pending-input, pending-approval, and not-yet-notified
children remain resident even above the configured count. Root sessions are
never evicted. Eligible children are selected oldest-idle first.
Paging happens only after event appends have been synced and, for background
work, after the parent completion teaser is durable. Session listings retain
lightweight metadata for paged children. Opening one in the TUI, reading its
result, sending it a message, or using resume_session_id transparently
reopens its event log and rebuilds the in-memory projection and actor.
Layering and replacement¶
User-layer and workspace-layer agent directories merge into one registry by
agent ID. A same-ID workspace agent replaces the user agent completely. The IDs
approval, compaction, and title are reserved for internal agents; an
authored document with one of those IDs must use mode: internal and replaces
the built-in document through normal layering. The ID default is reserved for
the engine-supplied fallback agent and cannot be authored at all.
If no authored agent is runnable as a root, the engine synthesizes the built-in
default coding agent bound to the first available model selection. Its prompt
and explicit permission map are fixed by the engine: read is allowed with
additional asks and secret-file denies, while write, bash, and delegate ask by
default. MCP, webfetch, and every other undeclared action stay hidden. User
agents do not inherit this list and must declare their own tool permissions.
Agent presets¶
Agent presets provide named, complete agent sets without duplicating every
shared document. Markdown files directly under agents/ are shared and are
available when no preset is selected. A directory exactly one level below
agents/ defines a preset:
~/.cookie-agent/agents/
├── primary.md
├── reviewer.md
├── python/
│ ├── primary.md
│ └── test-writer.md
└── rust/
├── primary.md
└── unsafe-reviewer.md
Selecting python produces an effective set containing every shared agent,
then fully replaces shared documents whose IDs also exist in agents/python/,
and finally adds preset-only IDs such as test-writer. Selecting rust applies
the same rule independently. Replacement is whole-document replacement: fields,
permissions, model chains, and prompt bodies never merge between same-ID files.
Preset names use the same lowercase alphanumeric and hyphen grammar as agent
IDs, with at most 64 bytes. Presets may add new IDs. The default ID remains
non-authorable, and authored approval, compaction, and title documents must
remain internal. Every shared and effective preset set is validated separately,
including delegation targets. Internal built-ins and the synthesized default
agent are resolved independently for each effective set.
Only one directory level is supported. Nested directories, non-Markdown entries, invalid names, malformed documents in unselected presets, and files over 256 KiB are hard configuration errors.
No preset is selected by default. In the TUI, run /preset and choose either
None (shared) or a discovered preset. The choice updates the active root
session's draft for its next run and is also used by /new when creating future
root sessions. If the current draft agent is not root-runnable in the new
effective set, the TUI selects that preset's primary or first runnable agent.
The choice is in memory only: it is not written to configuration and resets to
shared when the TUI restarts.
For headless runs, pass the preset explicitly:
cookie run --preset python --agent primary "Implement the data pipeline"
cookie run --preset rust --agent unsafe-reviewer "Review the FFI boundary"
The selected preset is stored in the session's creation selection and is the
default when the session is resumed. Root sessions may select another preset for
any later run; cookie run --resume-session <id> --preset rust ... applies
rust to that run without rewriting the creation selection. Each run records
its exact preset, agent snapshot, and model bindings in its events, so history
always shows what ran.
A later turn resolves against the live configuration. When the selection it
continues from names a preset, agent, model, or variant that no longer exists,
the turn falls back instead of failing: an unknown preset to the shared agents,
a missing agent to primary (or the first root-runnable agent), and a missing
model to the agent's first available model. An explicitly requested agent or
model that does not exist is still an error.
Delegated sessions inherit the preset of the parent run that created them, including when the parent switched presets after session creation, and later runs stay on that preset. Each later run looks the delegated agent up by ID in the live configuration, falling back to the default agent when it is gone.
Internal agents¶
The harness runs three internal agents. They are stateless, tool-less model
calls with a strict text-only output contract, and they emit their own event
family (internal_agent_started, internal_agent_completed, ...).
| ID | Role | Default model | Default limits |
|---|---|---|---|
approval |
Stateless approval classifier for auto_approve mode |
${parent_model} |
30 s timeout; model-derived input budget; max output tokens inherit the parent run's cap |
compaction |
Summarizes context into a checkpoint | ${parent_model} |
3 min timeout; model-derived input budget; max output tokens inherit the parent run's cap |
title |
Generates a concise session title from the opening user messages (the first session_title.max_input_messages, default 4) |
${parent_model} |
30 s timeout; model-derived input budget; max output tokens inherit the parent run's cap |
All three default to ${parent_model}, so they run on the model the parent run
is currently using — including its position in the fallback chain: if the run
has fallen back to its second model, internal agents resolve to that second
model too. An internal agent's input budget is derived from each
resolved model's context limit after reserving its effective maximum output,
with a minimum of one token. A model with an unknown context limit uses a
16,384-token input budget. Agent documents cannot set an input-token cap.
The title agent runs only for root sessions that still need an automatic title.
Delegated sessions already have the delegate_subagent description as their
title, so they never invoke the title agent.
The built-in documents are replaced by authored documents with the same ID,
mode: internal, and an explicit models list. ${parent_model} is
allowed only in internal agents. When an internal agent document is disabled, or
its fallback chain yields no available model, the internal call fails safely
(approval degrades to asking, compaction is skipped, and title falls back to an
input excerpt) and an internal_agent_failed event is recorded.
Example replacement — run compaction on a cheaper model while the primary run keeps its own selection:
---
description: Context compaction on a fast model
mode: internal
enabled: true
models:
- { model: "openai/gpt-5-mini", variant: null }
limits:
timeout_ms: 30000
max_output_tokens: 4096
permissions: {}
---
Summarize conversation context faithfully within the supplied bounds. Return summary text only.
Selecting an agent¶
In the TUI, /new creates a root session after choosing from agents in the
selected effective set that are runnable as a root (primary or all, enabled,
with at least one available model). If none are runnable, that effective set's
built-in default agent is used.
Permissions¶
Agent documents define an ordered permission map for read, write, bash,
delegate, message, mcp, plugin, skill, and webfetch. Each action is either one bare effect or a resource-pattern map:
permissions:
read:
"*": allow
".env*": deny
write: ask
bash:
"git status": allow
"*": ask
delegate:
"reviewer": allow
"*": deny
mcp:
"github_*": allow
github_delete_repo: deny
Effects are allow, ask, and deny. A bare effect is equivalent to mapping
"*" to that effect. For matching patterns, more literal characters win, then
fewer wildcards, then the later declaration on an exact tie. Unmatched resources
are denied by default. ask remains an explicitly writable effect and routes
matching calls through the existing approval flow; it is never an implicit fallback.
Resource labels¶
Tool providers publish a static permission name and an optional resource label:
| Tool | Permission name | Resource label |
|---|---|---|
read |
read |
Filesystem path, or the exact artifact://<digest>[/<stream>] URI for an artifact |
write, edit |
write |
Workspace-relative path inside the workspace; absolute path outside it |
bash |
bash |
Complete command string |
delegate_subagent |
delegate |
Target agent_type |
get_subagent_result, cancel_subagent |
delegate |
Owned subagent's agent_type |
send_message |
message |
Recipient's relationship to the sender: parent, child, sibling, or * |
<server>_<tool> |
mcp |
The complete generated MCP tool name |
skill |
skill |
Skill name |
goal_get, goal_update |
read, write respectively |
goal:current |
webfetch |
webfetch |
Initial URL as given, including its query string |
The edit tool uses the write permission action. Bash is not parsed into file
operations: cat .env is controlled by bash, not read, and a pattern such
as git * also matches a longer command beginning with git. See
Security guarantees for the platform-specific filesystem and
process boundaries behind these tools.
Path resource labels are normalized lexically from the single path requested by
the caller. They are not canonicalized through symbolic links before permission
matching. That one label authorizes the whole prepared read, write, or
edit operation: if an allowed alias resolves outside the workspace, cookie
agent does not perform a second permission check or require a rule for the
resolved destination. Filesystem preparation still binds the traversed route
and destination and can reject the operation independently. In particular,
changing a link target after preparation fails with operation_changed; it
does not cause policy to be reevaluated against the new destination.
Bash remains independent of this path-resource contract. An allowed command has
only its complete command-string resource and does not gain hidden read or
write resource checks when the command traverses a link.
Permission policy does not validate cookie agent's pre-existing private-state
paths. New state is created with owner-only permissions, but existing loose,
foreign-owned, hard-linked, or symlinked state is read and written as-is. This is
part of the local storage threat model, not an allow/ask/deny decision.
MCP checks are always scoped. A rule such as "github_*": allow covers every
tool from that generated server prefix, while a more-specific deny can override
one tool. An unmatched MCP tool is denied.
When a tool has no resource label, only the permission's bare effect or "*"
rule applies. Specific patterns are inapplicable rather than matching or
denying. If neither a bare effect nor "*" exists, the normal unmatched result
is deny.
${workspace_dir} is allowed only in read and write patterns and expands
against the engine workspace root during evaluation. Ordinary absolute patterns
such as /etc/* control outside-workspace paths. Permission patterns do not
expand environment variables. A * matches across directory separators, so
"${workspace_dir}/*" already covers nested paths such as
${workspace_dir}/crates/engine/src/lib.rs; no ** pattern is needed.
Permission evaluation has no implicit filename or resource-name exceptions.
Credential files, .env and .env.*, and non-file resource labels all use the
configured rules for their action, including in session overlays. Broad file
allows such as "*": allow or
"${workspace_dir}/*": allow apply without a protected-file override. Explicit
deny and ask rules follow the same specificity and overlay precedence as any
other file; unmatched resources are denied.
The synthesized default agent still declares explicit dotenv read denies and
.env.example allows in its permission map. These are ordinary policy rules,
not an implicit guard applied to authored agents or session overlays. The same
applies to its explicit credential-file denies (such as store-v3.json,
id_*, .netrc, and application_default_credentials.json).
This contract governs tool permission evaluation. It does not replace filesystem
integrity and resource-binding checks, or govern engine configuration and prompt
loading (such as workspace AGENTS.md admission). Generic approval modes and
turn-scoped skill grants retain their documented behavior; they are not
resource-name exceptions.
Tool availability and delegation¶
Tools are opt-in. A permission action must have at least one allow
or ask rule in the agent document or session overlay before that action's
tools are visible. Any resource pattern counts, not just "*". Visibility does
not resolve rule precedence: an overlay deny does not hide a tool if the agent
still declares an allow or ask rule for that action. An action omitted from
both layers, or with only deny rules, advertises no tools for any provider.
Resource patterns and overlay precedence still decide individual calls once
tools are visible.
For example, this agent exposes read, write/edit, and bash with granular write and command policies while leaving delegation and MCP tools hidden:
---
description: Workspace implementation agent
mode: primary
enabled: true
models:
- { model: "openai/gpt-5", variant: null }
permissions:
read: allow
write:
"src/*": ask
bash:
"cargo test*": allow
"cargo fmt*": allow
---
Implement and verify requested workspace changes.
MCP tools follow the same rule: the mcp action must contain a non-deny rule.
Delegation tools additionally require the delegate map to name at least one
eligible target with allow or ask. There is no separate tool allowlist.
There is also no separate MCP server approval prompt or trust store. Configured,
enabled servers follow their normal lazy or eager connection lifecycle, while
the agent's mcp map remains the sole visibility and call gate. Project MCP
configuration and project agent documents are version-controlled content
equivalent to code: a repository can ship both a server definition and an agent
that permits it. Review them and work only in repositories you trust.
Delegation targets come from the keys in the delegate permission map and must
resolve to enabled subagent or all agents. This action controls
delegate_subagent, get_subagent_result, and
cancel_subagent. All three tools use the target subagent's agent type as their
permission resource.
Result and cancel retain their existing ownership and argument
validation, and permission evaluation uses that agent type. Their approval
display still shows the owned session_id; display text is independent of the
permission resource.
This changes existing mapped delegation policies. For example,
delegate: {reviewer: allow, "*": deny} allows delegate_subagent targeting
reviewer, but denies get_subagent_result and
cancel_subagent for other agent types. Existing tree grants issued for their
former identities do not carry over.
Runtime delegation.max_depth defaults to 3 and max_concurrency defaults to 4.
delegate_subagent replaces the former delegate tool name without an alias.
Old tool calls and prepared-operation grants therefore fail closed. The
delegate spelling above remains the permission action, not a tool alias.
Agent-to-agent messaging is separate from delegation. The removed
steer_subagent tool is replaced by send_message, which uses the message
action with relationship labels (parent, child, sibling, *) rather than
agent types, so a delegate grant no longer authorizes steering. The tool is
hidden until a message rule allows or asks; agents that previously steered
children need an explicit rule such as message: { "child": allow }. The
recipient argument is to; the earlier recipient_session_id spelling has no
alias, and prepared-operation grants issued under the old argument identity
fail closed. See the
messaging migration notes.
Web fetching¶
Web access uses the webfetch action, matched against the exact initial URL as
requested, including its query string:
permissions:
webfetch:
"*https://*.quantumcookie.xyz/*": allow
"https://review.example.org/*": ask
read:
"artifact://*": allow
The read rule permits artifact readback so the model can page long responses.
It does not grant filesystem reads, and workspace-path patterns do not grant artifact reads.
Permission is checked once, against the initial URL; redirect destinations are
not checked, and there is no SSRF protection, host/IP blocklist, or DNS
pinning, so scope patterns accordingly, including access to local network
services. Requests inherit the cookie-agent process's proxy environment and
have a 30-second timeout. See the tool reference
for the output format, HTML rendering, the download cap, and result paging.
Live permission modes¶
Each session tree starts in auto_approve unless changed. The mode is runtime
only, is keyed by the tree root, and can be changed through any session in the
tree:
auto_approveruns the stateless approval classifier and asks the user when it escalates or fails safely.auto_approve_nruns the same classifier but rejects an escalation on the user's behalf without showing a prompt.auto_approve_yruns the same classifier but approves an escalation on the user's behalf without showing a prompt. This is an approve-once decision and does not create a lasting session-tree grant.askskips the classifier and routes policy asks and model-requested approvals to the user.yoloapproves asks immediately.
Hard denies, the doom-loop guard, and existing tree grants are evaluated before
the mode shortcut. Changing a mode does not alter an already pending approval
but does apply to subsequent approvals in every delegated descendant. As a
result, root trees in ask or yolo now gate descendant tool calls with that
same mode.
When one model turn contains multiple tool calls, the engine computes every permission decision before executing any call. Policy asks and model-requested approvals are then resolved one at a time in model order. Auto-allowed calls wait for every ask in the batch to resolve, so no tool side effect begins while a sibling approval is pending. Denied or rejected calls fail independently; after the approval pass, surviving parallel-eligible calls are dispatched together.
Mode-decided escalations use the permission_mode decision source in the audit
trail. Their final reason codes are auto_approve_n_rejected and
auto_approve_y_approved; the classifier's preceding evaluation remains an
internal-agent escalation.
Session permission overlays¶
Run /permissions to edit the selected session's permission overlay. Each
action exposes its effective allow, ask, or deny effect and its individual
resource patterns. Source labels distinguish session_overlay,
agent_document, and default. Left/right changes an effect, n adds a
validated wildcard pattern, and d removes a selected session-overlay rule.
The editor does not accept freeform YAML.
An overlay rule is evaluated before matching rules from the frozen agent
snapshot. If no overlay rule matches, evaluation falls back to the agent
document and then the normal default (deny). This default affects evaluation
only; an action omitted from both layers has no visible tools. Changes affect
subsequent visibility and permission evaluations only. They do not rewrite an
active run's frozen agent/model
identity or retroactively cancel an operation already executing. A pending
tree-approval response is rejected as changed if the session overlay changed
after that approval was requested, so it cannot commit a stale durable grant.
Every change appends a complete session_permission_overlay_set event to the
session log. Overlay state therefore survives daemon restart and follows normal
revert and fork branch semantics. Tightening a rule durably invalidates existing
tree grants for that action under the session root before the overlay event is
committed. Invalidation is action-wide because tree-grant records retain opaque
prepared identities rather than normalized resource labels.