diff --git a/decisions/decisions.md b/decisions/decisions.md index ef2d498..72fe607 100644 --- a/decisions/decisions.md +++ b/decisions/decisions.md @@ -167,7 +167,7 @@ the estate declining to adopt the authority vocabulary. id: GH-DEC-2026-002 kind: decision title: Revocation fails closed only when approval-engine's own store is down -status: open +status: resolved owner: Bernd Worsch repo: gate-house standard: net-kingdom/canon/standards/security-layer-model_v0.7.md @@ -182,9 +182,75 @@ affects: - audit-core - flex-auth created: '2026-08-29T12:50:04.747439Z' -updated: '2026-08-29T12:50:04.747439Z' +updated: '2026-08-29T12:50:58.873684Z' +rationale: 'Approved as GH-WP-0002-T03. Local-outbox fail-closed is the only remaining + closed path: if approval-engine cannot insert the outbox row, the revocation does + not commit. An audit-core outage must not block a revocation. Synchronous emission + inside the mutation is forbidden even though it is atomic. Proceed-with-gap is rejected + for load-bearing approval evidence.' +decided_by: Bernd Worsch +decided_at: '2026-08-29T12:50:58.873684Z' ``` +## Context + +`GH-IN-0001` (from `audit-core`) required that the revocation failure mode be a +recorded decision rather than an implementation accident. Both sides are +defensible on their own: fail-closed makes an audit dependency into an +availability risk on the revocation path; proceed-with-gap needs a detectable +marker so the gap is visible rather than silent. + +v0.5's local-outbox rule already narrowed this. With the queue in +`approval-engine`'s own store, fail-closed triggers only when that store is +down — where the change could not have been recorded anyway — and an +`audit-core` outage does not block a revocation. Satisfying atomicity by +emitting synchronously to `audit-core` inside the mutation is also atomic, and +turns an audit outage into an inability to revoke: the operation least +tolerable to block during an incident. + +The statute states the locality rule at §9.4. This record is the decision the +workplan still owed, so an implementer cannot pick the other side by accident. + +Wire: `docs/contracts/approval-outbox.md`. + +## Decision + +**If the local outbox insert cannot commit, the revocation does not commit.** +The same rule applies to issuance, use, and supersession. + +**An `audit-core` outage MUST NOT block a revocation.** Drain is asynchronous +and at-least-once. `audit-core` dedupes on `event_id`. + +**Synchronous emission to `audit-core` inside the state-change transaction is +forbidden**, even though it is atomic. + +Proceed-with-gap is rejected for load-bearing approval evidence. A revocation +that succeeds while its event is lost is the exact case `AUDIT-IN-0001` +conditioned assent on. + +## What this authorizes + +- `approval-engine` failing a revoke (and any other mutation) when its own + store cannot insert the outbox row. +- Continuing to revoke while `audit-core` is down, with the row draining later. +- Treating emit-after-commit, a second non-local queue, or a sync `audit-core` + call inside `BEGIN`…`COMMIT` as out of contract. + +## What this does not authorize + +- Claiming the outbox closes adversarial omission. Atomicity prevents crash; + cadence and reconciliation detect suppression after the fact (§9.6). +- A validity query on `audit-core`. +- Any change to another repository's workplans. + +## Reversal + +Revert this record and the outbox contract. The falsifier is operational: if +failing closed on the local store removes the ability to revoke during the +incidents this engine exists for, the trade needs revisiting — but the +alternative is a silent evidence gap on the event an attacker most wants +missing, which is worse. + ## GH-DEC-2026-003 — The PEP consumes an approval before the protected side effect ```yaml @@ -209,3 +275,67 @@ affects: created: '2026-08-29T12:50:06.944092Z' updated: '2026-08-29T12:50:06.944092Z' ``` + +## Context + +Statute §16 left open who marks an approval consumed, and at what point +relative to the decision. `flex-auth` named three failure modes neither engine +closes alone: an ALLOW never consumed; a double consumption by racing callers; +consumption after a failed action. `approval-engine` performs the mutation +because `access-engine` never mutates, and refused to wire a public consume by +guessing the contract. `APPROVAL-WP-0001-T05` and `FLEX-WP-0017-T05` are wait +on this record. + +§9.7.3 forbids inferring consumption from a decision record. That stands. The +same paragraph currently reads as if the action must precede the consume call. +That reading cannot enforce single use: two racing PEPs can both act, and CAS +then prevents only the second record. + +## Decision + +**The PEP-shaped consumer consumes, by compare-and-swap, before the protected +side effect.** The PDP never mutates. Staff never calls consume. + +**There is no unconsume and no reserve/release.** An approval authorizes one +attempt, not one success. A consume followed by a failed action spends the +object; retry is a new approval. + +**Same `request_digest` against an already-consumed object is idempotent +success** (a retry of one logical request). A different digest is conflict, +and the PEP MUST NOT act. + +The three failure modes have owners: + +1. *Allow never consumed* — PEP duty to consume; PDP lifetime bounds the + window; a stale ALLOW without a matching `use` is a finding, not a consume. +2. *Double consumption* — `approval-engine` CAS; the PEP that receives + conflict does not act. +3. *Consumed then the action fails* — accepted as the cost of closing (2). + +Protocol: `docs/contracts/approval-consumption.md`. Statute v0.8 will replace +the §9.7.3 implication that action precedes the consume call; until then this +record governs the blocked implementers. + +## What this authorizes + +- `approval-engine` exposing `POST /v1/approvals/{id}/consume` against this + contract, including the digest column the internal CAS does not yet store. +- `FLEX-WP-0017-T05` requiring the PEP (secrets-engine, for that task) to + consume before the OpenBao call. +- Canon `T-06` consume-side replay (a second, different digest against a + consumed object) becoming in-scope once the endpoint exists. + +## What this does not authorize + +- The PDP consuming, or inferring consumption from a decision record. +- Unconsume, reserve, or any path that returns a consumed object to `approved`. +- A consume that is a decision ("may this actor do X"). +- Any change to another repository's workplans. Work structure stays with the + repository doing the work. + +## Reversal + +Revert this record and the consumption contract. The falsifier is operational: +if spending an approval on a failed attempt makes the estate unable to +complete the actions this object exists for, a reserve/commit protocol can be +raised then. Unconsume is not the alternative — it reopens replay.