Part III — Clearance, waivers, and grants¶
These three instruments are how the system says "yes" to something risky in a recorded, bounded way. For a reviewer, the key facts are (a) they are three different answers to three different questions, (b) none of them overrides a HALT, and (c) they all rest on the same symmetric-signing trust model whose ceiling is the same throughout.
The authorization triad ✅ {#S7}¶
Collapsing these three into "permissions" is a category error. Each answers a distinct question and lives in its own ledger:
| Instrument | Answers | Authorizes | Ledger |
|---|---|---|---|
| Clearance | May this destructive action run here, now? | A destructive action locally, before it runs (C-5) | references/security-decisions.log.csv |
| Waiver | May this proceed past a stop? | Lifts a gate block (destructive gate, or stale-intel gate) | references/security-waivers.log.csv |
| Grant | May this reach into another workspace? | Widens a cross-workspace write boundary | references/capability-grants.log.csv (holder's) |
A clearance is a pre-recorded decision that a specific destructive step is acceptable — C-5 made concrete. A waiver lifts a stop a gate has already raised. A grant permits a reach: it widens a boundary rather than lifting a block, and is the cross-workspace analogue of a waiver.
None of the three overrides a HALT (C-2). A @security HALT is checked
first, and no instrument is consulted for it. This is the C-2 finality property
carried through the whole triad, and it is the reason a reviewer can reason about
HALT independently of the authorization machinery: no amount of valid
authorization reaches past a stop.
Honest ceiling — one shared symmetric-HMAC trust model. All three rest on
keyed HMAC-SHA256 over an ordered field list, an expiry check, and a
symlink/..-safe path-containment test, each using its own key, and each
fails closed when its key is unset (it refuses rather than proceeding
unsigned). The ceiling is exact and applies to every instrument below: symmetric
signing defends against a keyless forger — an agent that cannot read the key
cannot fabricate a valid row — but not against an actor who holds the key (an
adversarial peer team, or an agent that has captured the key). The key must be
issued out-of-band and never enter an agent session. Cross-trust-boundary
unforgeability would require asymmetric signatures, which the Python standard
library does not provide; signed_ledger.py is the single documented asymmetric
swap point. The triad engages as tested against a keyless forger; it is not a
guarantee against a key-holder.
Security decisions log and the CONDITIONAL PASS lifecycle ✅ {#S8}¶
The decision log is where the sentinel's verdicts become durable, ordered, and replay-proof — turning judgment into bookkeeping the gate can trust.
Every verdict, including a plain PASS, appends a row (current schema is 9 columns; a legacy 6-column schema is still read). The destructive gate consults this log as its clearance engine, in this order:
| Log state | Gate behaviour |
|---|---|
| Unretracted HALT anywhere | Blocks — checked first, over the whole log |
| PASS (matching, unconsumed) | Allows |
CONDITIONAL PASS, conditions_verified == "verified" |
Allows |
| CONDITIONAL PASS, conditions not verified | Blocks "as if HALT" |
| Row already consumed/used | Skipped — a clearance cannot be replayed |
Two adversary-relevant properties: HALT is checked first (a stop cannot be outvoted), and a consumed row is skipped (a clearance cannot be replayed to authorize a second action).
The Pre-Execution Security Check (⚙ procedure). For any step carrying a
CONDITIONAL PASS, the orchestrator must read the log, confirm every condition has
recorded evidence, treat a pending verification as a HALT and surface it, and
only proceed once all conditions are met. This is a procedural mirror of the
deterministic gate check, not a second code control — a reviewer should count
it as instruction-level (S2), not as a guarantee.
Honest ceiling. The verdicts recorded here are, except where the S-1/S-8 scanner backs them, non-deterministic model judgments (S5). The log makes those judgments durable, ordered, and replay-proof; it does not make them correct. Its guarantee is bookkeeping integrity — a spent clearance cannot be reused, a HALT cannot be outvoted — not the soundness of the underlying verdict. Full schema and line-precise gate logic: Edition R, S8.
Signed waivers ✅ {#S9}¶
A waiver is the only sanctioned way past a gate block — and, by C-2, never past a HALT. Its whole value is that lifting a stop becomes a scoped, expiring, counted, signed, roster-attributed, logged act rather than a silent override.
Validity requires all of: an action-id scope match; verified conditions; a
non-empty approver/ticket/reason; the approver present on the roster; an
expires_at in the future (time-bounded); uses < max_uses
(use-counted); and a verifying HMAC-SHA256 signature over the business
fields. On consumption, uses is incremented and the row re-signed — and
because uses is a signed field, an attacker cannot hand-edit the counter back
down to replay a spent waiver without breaking the signature. A missing signing
key fails closed.
On-demand ledger — a note for auditors of a clean checkout. Both the waiver
log and the approver roster are created on demand (first waiver, first
approver), not shipped. Their absence in a fresh tree is expected — do not
read "no waivers file" as "waivers disabled." A read-only audit,
--verify-waivers, validates every row without consuming any.
Honest ceiling. Every waiver guarantee is symmetric-HMAC-bounded (S7): the signature stops a keyless forger, not a key-holder. Scope, expiry, use-count, and roster checks constrain what a valid waiver can do; they do nothing against an actor who can mint a valid signature. The waiver's value is visibility and bounding, not unforgeability. Full validity table and line numbers: Edition R, S9.
Capability grants ✅ {#S10}¶
A grant is the cross-workspace analogue of a waiver: a signed, scoped,
time-bounded authorization by an issuer_team for a holder_team to perform an
operation (e.g. write) on a target_path in the issuer's workspace. Its ledger
lives in the holder's workspace (a bearer-capability model) and, like the
waiver log, is created on first grant, not shipped.
The property a reviewer should note first: enforcement is generation-time
only. On (re)generation with the sandbox on, the targets of a holder's valid
write grants are merged into the sandbox allowWrite set. A freshly issued
grant is therefore inert until the operator re-runs an update — there is
deliberately no runtime path by which an agent widens its own OS boundary. A
grant is configuration, not a live capability. This is a deliberate trust
boundary: self-granting at runtime is the escalation it forecloses.
validate_grant() is fail-closed and ordered: required fields → scheme
sufficient (a write grant must be operator Ed25519-signed; an HMAC one is
refused) → signature → not expired → use-counter not exhausted → approver on the
holder team dir's roster. Cross-workspace grants require an explicit
approver roster — the self-clear fallback is refused for a cross-workspace reach,
and a project-root roster, which an in-sandbox agent could plant, is ignored — and
path-safety guards reject ~,
..-escapes, and targets outside a signed issuer_root. A SHA-256 prev_digest
chain is verified on every read (fail-closed on tamper), so ledger tampering is
evident.
Honest ceiling. A grant that widens a write boundary is signed with the
operator's Ed25519 key (--sign-grant), because a sandboxed agent inherits the
shared HMAC grant key from its environment: HMAC defends only against a keyless
forger, so HMAC grants can no longer widen anything. The Ed25519 payload carries
the purpose tag agentteams-grant-v1, so a signature over a security decision
cannot be replayed as a grant. A widening grant is as strong as that private key
and the write protection of the team dir's verify-key store. The prev_digest chain makes ledger tampering evident, not
impossible. And because enforcement is generation-time, a grant affects the OS
boundary only when the operator regenerates with the sandbox on. Full validation
order and line numbers: Edition R, S10.
Sources for Part III. agentteams/cli/grants.py;
agentteams/cli/security_gate.py; agentteams/cli/decision_log.py;
agentteams/cli/signed_ledger.py;
agentteams/templates/universal/orchestrator.template.md. Line-precise
provenance: SOURCES.md (S27).