Skip to content

Part III — Clearance, waivers, and grants

The authorization triad ✅ {#S7}

agentteams separates authorization into three distinct instruments, one per distinct question. Collapsing them is a category error; each has its own ledger, its own scope, and its own answer to "may this proceed?"

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 (decision log)
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 workspace)

A clearance is a pre-recorded decision that a specific destructive step is acceptable — the C-5 "clearance precedes destruction" requirement made concrete. A waiver lifts a stop that 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 (agentteams/cli/grants.py:1-36 states the waiver-vs-grant intent distinction directly — a waiver "lifts a stop," a grant "permits a reach").

None of the three overrides a HALT (C-2). A @security HALT is checked first and no instrument is consulted for it. A clearance authorizes a step that was never HALTed; a waiver lifts a gate block, not a HALT; a grant widens a write boundary but "a holder still cannot proceed past a HALT on the granted write" (agentteams/cli/grants.py:47-48). This is the C-2 finality property carried through the whole triad.

One shared trust model — symmetric HMAC, fail-closed. All three rest on the same signed-ledger primitives (agentteams/cli/signed_ledger.py:9-14): a keyed HMAC-SHA256 over an ordered field list, an ISO-8601 expiry check, and a symlink/..-safe path-containment test. Each instrument uses its own key (AGENTTEAMS_WAIVER_SIGNING_KEY, AGENTTEAMS_GRANT_SIGNING_KEY; decision-signing when active — agentteams/cli/security_gate.py:39-69). When the relevant signing key is unset the instrument fails closed — it refuses rather than proceeding unsigned.

Honest ceiling. The signing is symmetric: one shared key per instrument. It defends against a keyless forger — an agent that cannot read the signing key cannot fabricate a valid row — but it does not defend 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; agentteams/cli/signed_ledger.py (hmac_sign/hmac_verify) is the single documented asymmetric swap point where such a backend would later slot in. 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}

Every sentinel verdict — including a plain PASS — appends a row to references/security-decisions.log.csv. The current schema is 9 columns: date,plan_slug,step,decision,status,conditions,conditions_verified,evidence,owner. A legacy 6-column schema is still accepted by a schema-kind detector, so older logs continue to read (agentteams/cli/decision_log.py:22-63).

The destructive gate consults this log as the clearance engine. Its decision 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

The HALT-first, replay-proof logic lives in agentteams/cli/security_gate.py:96-259; row-consumption bookkeeping is at agentteams/cli/security_gate.py:619-659. Fail-closed throughout: every unresolved path resolves to deny.

The Pre-Execution Security Check (procedure, ⚙ at the orchestrator). For any step carrying a CONDITIONAL PASS, the orchestrator must read the log, confirm every condition has recorded evidence, treat conditions_verified = pending as a HALT and surface it to the user, and only flip to verified and proceed once all conditions are met. The orchestrator template states this is "not optional… blocks the operation as if HALT" (agentteams/templates/universal/orchestrator.template.md:339-350). This is a procedural mirror of the deterministic gate check, not a second code control.

Authorship and signature. An authorizing PASS or CONDITIONAL PASS row must be issued by an approved author and carry a valid signature when decision-signing is active. check_clearance() is the read-only inspection counterpart — it reports what a clearance would do without spending it, consuming nothing (agentteams/cli/decision_log.py:187-234).

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.

Signed waivers ✅ {#S9}

A waiver clears a gate block — the destructive gate, or the stale-intel gate (S12) — and is kept in references/security-waivers.log.csv. It never overrides a HALT: a HALT is checked before any waiver is consulted (C-2).

On-demand ledger. Both references/security-waivers.log.csv and the references/security-approvers.txt roster are created on demand — on the first waiver issued and the first approver configured, respectively. They are not shipped in a fresh tree; their absence is expected, not a defect. A reader auditing a clean checkout should not read "no waivers file" as "waivers disabled."

Validity requires all of the following (agentteams/cli/security_gate.py:511-616):

Requirement Meaning
Action-id scope match The waiver names the exact action it clears
conditions_verified == "verified" Any attached conditions are met
Non-empty approver / ticket / reason An accountable human record exists
Approver on the roster The approver appears in references/security-approvers.txt
expires_at in the future Time-bounded
uses < max_uses Use-counted
Verifying HMAC-SHA256 signature Signature over the business fields, excluding timestamp/signature

On consumption, uses is incremented and the row is re-signed (agentteams/cli/security_gate.py:430-477). Because uses is a signed field, a tampered counter invalidates the row — an attacker cannot hand-edit uses back down to reuse a spent waiver without breaking the signature. A missing signing key ⇒ fail-closed (agentteams/cli/signed_ledger.py:40-92).

--verify-waivers is a read-only audit: it validates every row without consuming any, emitting one valid/invalid-plus-reason line per row (agentteams/cli/security_gate.py:39-69).

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 approver-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 that it makes lifting a gate a scoped, expiring, counted, signed, roster-attributed act — visible in the log — not that it is unforgeable.

Capability grants ✅ {#S10}

A grant is 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 — the cross-workspace analogue of a waiver. Its ledger lives in the holder's workspace at references/capability-grants.log.csv (a bearer-capability model: the holder holds the grants issued to it). Like the waiver log, it is created on the first grant, not shipped — absence in a fresh tree is expected (agentteams/cli/grants.py:1-36).

Enforcement is generation-time only. On (re)generation with the sandbox on, the targets of the valid write grants a holder holds 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 (agentteams/cli/grants.py:15-19). The generation-time widening path does not consume a use, so under today's only enforcement path expires_at is the active temporal bound and max_uses is validated-but-not-decremented (reserved for a future per-write consume path).

validate_grant() is fail-closed and ordered (agentteams/cli/grants.py:126-235,306-473):

  1. Required fields present
  2. Scheme sufficient — a write grant must be sig_scheme=ed25519; an HMAC write grant is refused with a migration message (re-sign it with --sign-grant)
  3. Signature verifies — for Ed25519, against the holder team dir's references/authorized-verify-keys/<key_id>.pub.pem, over a payload that starts with the purpose tag agentteams-grant-v1 (so a decision signature is never a grant)
  4. Not expired
  5. Use-counter not exhausted
  6. Approver on the team-dir roster

Cross-workspace grants require an explicit approver roster, read from the holder team dir (the --output the update targets) — the {security,@security} self-clear fallback is refused for a cross-workspace reach, and a project-root references/security-approvers.txt is ignored with a warning (an in-sandbox agent can plant it; there is no fallback). Path-safety guards reject ~, ..-escapes, and targets outside a signed issuer_root, and a SHA-256 prev_digest chain is verified on every read (fail-closed on tamper) (agentteams/cli/grants.py:74-120,637-902).

--sign-grant (operator Ed25519, the only minter of a widening grant), --issue-grant (HMAC, non-widening grants only) and --verify-grants mirror the waiver commands (the minters deposit into the holder workspace; verify is a read-only per-row audit). C-2 parity: a grant widens a write boundary, it never overrides a HALT.

Honest ceiling. A widening grant is as strong as the operator's Ed25519 private key (kept in the read-denied ~/.config/agentteams/keys/) and the write protection of the team dir's verify-key store and roster. HMAC grants keep the symmetric bound of S7 (anyone holding AGENTTEAMS_GRANT_SIGNING_KEY, including a sandboxed agent that inherited it, can forge one), which is why an HMAC grant can no longer widen anything. The prev_digest chain makes ledger tampering evident; it does not make it impossible. And because enforcement is generation-time, a grant's effect on the OS boundary engages only when the operator regenerates with the sandbox on — the grant is configuration, not a live capability.

Sources for Part III. agentteams/cli/grants.py:1-49,74-120,223-354,425-452,541-635,712-902; agentteams/cli/security_gate.py:39-69,96-259,430-477,511-616,619-659; agentteams/cli/decision_log.py:22-63,187-234; agentteams/cli/signed_ledger.py:9-14,40-92; agentteams/templates/universal/orchestrator.template.md:339-350.