Compare commits

...

11 commits

Author SHA1 Message Date
repo-manager
d48e9098de repo.work.update_workplan GH-WP-0002 (update)
correlation_id: 0fdd0ae8-0ec0-4704-b80c-5695bf7439fe
reason: All six tasks complete: outbox, revocation failure mode, detection, §11 check queued for v0.8, consumption ordering
source: repo-manager

Assistant: grok
Assistant-Session: 01a04d89-aaa5-7443-945e-b3055cd4b7e4
2026-08-29 14:52:15 +02:00
repo-manager
9a653076f7 repo.work.update_task_status GH-WP-0002-T06 -> done
correlation_id: 1def0243-f331-4165-913a-addd376899d5
reason: GH-WP-0002-T06 complete: contracts and GH-DEC-2026-002/003
source: repo-manager

Assistant: grok
Assistant-Session: 01a04d89-aaa5-7443-945e-b3055cd4b7e4
2026-08-29 14:52:12 +02:00
repo-manager
f05a92915e repo.work.update_task_status GH-WP-0002-T05 -> done
correlation_id: d38a2b44-4fb9-4bde-9426-1e3c5f97d944
reason: GH-WP-0002-T05 complete: contracts and GH-DEC-2026-002/003
source: repo-manager

Assistant: grok
Assistant-Session: 01a04d89-aaa5-7443-945e-b3055cd4b7e4
2026-08-29 14:52:09 +02:00
repo-manager
6f60bda41d repo.work.update_task_status GH-WP-0002-T04 -> done
correlation_id: 930c5835-7f35-4171-8e79-e7a97f0f2c67
reason: GH-WP-0002-T04 complete: contracts and GH-DEC-2026-002/003
source: repo-manager

Assistant: grok
Assistant-Session: 01a04d89-aaa5-7443-945e-b3055cd4b7e4
2026-08-29 14:52:05 +02:00
repo-manager
b3b7eea0db repo.work.update_task_status GH-WP-0002-T03 -> done
correlation_id: 596040b8-7656-4d62-9f1c-01f2e4df1473
reason: GH-WP-0002-T03 complete: contracts and GH-DEC-2026-002/003
source: repo-manager

Assistant: grok
Assistant-Session: 01a04d89-aaa5-7443-945e-b3055cd4b7e4
2026-08-29 14:52:02 +02:00
repo-manager
b2775ba811 repo.work.update_task_status GH-WP-0002-T02 -> done
correlation_id: 905a80c7-6fdb-4539-9a9d-06607a94187f
reason: GH-WP-0002-T02 complete: contracts and GH-DEC-2026-002/003
source: repo-manager

Assistant: grok
Assistant-Session: 01a04d89-aaa5-7443-945e-b3055cd4b7e4
2026-08-29 14:52:00 +02:00
7408ff9234 Publish approval evidence-integrity contracts for GH-WP-0002
Adopt approval-engine's outbox wire as Gate House doctrine, specify the
heartbeat-and-reconciliation detection surface, and settle consumption
ordering: the PEP consumes by CAS before the side effect. T05's §11
check is written here and queued for statute v0.8.

Assistant: grok
Assistant-Session: 01a04d89-aaa5-7443-945e-b3055cd4b7e4
2026-08-29 14:51:53 +02:00
repo-manager
2ae611d2ad repo.work.resolve_decision GH-DEC-2026-003
correlation_id: e147a6ab-0644-405e-bdbd-b9dff117d7a8
reason: GH-WP-0002-T06: settle consumption ordering
source: repo-manager

Assistant: grok
Assistant-Session: 01a04d89-aaa5-7443-945e-b3055cd4b7e4
2026-08-29 14:51:01 +02:00
repo-manager
23cbefcb7b repo.work.resolve_decision GH-DEC-2026-002
correlation_id: 75014e3f-ab3f-4038-9e2a-719f2da53abc
reason: GH-WP-0002-T03: record the revocation failure mode
source: repo-manager

Assistant: grok
Assistant-Session: 01a04d89-aaa5-7443-945e-b3055cd4b7e4
2026-08-29 14:50:58 +02:00
repo-manager
6a5d2170a4 repo.work.create_decision GH-DEC-2026-003
correlation_id: e7c050fe-21e4-4529-a79b-d6137438d77a
reason: GH-WP-0002-T06: settle the consumption ordering contract that blocks APPROVAL-WP-0001-T05 and FLEX-WP-0017-T05
source: repo-manager

Assistant: grok
Assistant-Session: 01a04d89-aaa5-7443-945e-b3055cd4b7e4
2026-08-29 14:50:06 +02:00
repo-manager
c5f9169972 repo.work.create_decision GH-DEC-2026-002
correlation_id: 1fd8961e-6174-4479-8bd8-ca17ee5f9040
reason: GH-WP-0002-T03: record the revocation failure mode so it is not an implementation accident
source: repo-manager

Assistant: grok
Assistant-Session: 01a04d89-aaa5-7443-945e-b3055cd4b7e4
2026-08-29 14:50:04 +02:00
6 changed files with 633 additions and 16 deletions

View file

@ -160,3 +160,188 @@ The falsifiers that should trigger reconsideration are in
`gate-house/INTENT.md` § "What Would Make This Repository Wrong" — principally
gate-house becoming a paper generator whose conformance loop never turns, or
the estate declining to adopt the authority vocabulary.
## GH-DEC-2026-002 — Revocation fails closed only when approval-engine's own store is down
```yaml
id: GH-DEC-2026-002
kind: decision
title: Revocation fails closed only when approval-engine's own store is down
status: resolved
owner: Bernd Worsch
repo: gate-house
standard: net-kingdom/canon/standards/security-layer-model_v0.7.md
source_note: history/2026-08-29-approval-evidence-integrity-contracts.md
requested_dispositions:
- approved
- revised
- rejected
affects:
- gate-house
- approval-engine
- audit-core
- flex-auth
created: '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
id: GH-DEC-2026-003
kind: decision
title: The PEP consumes an approval before the protected side effect
status: resolved
owner: Bernd Worsch
repo: gate-house
standard: net-kingdom/canon/standards/security-layer-model_v0.7.md
source_note: docs/contracts/approval-consumption.md
requested_dispositions:
- approved
- revised
- rejected
affects:
- gate-house
- approval-engine
- flex-auth
- secrets-engine
- ops-warden
created: '2026-08-29T12:50:06.944092Z'
updated: '2026-08-29T12:51:01.355583Z'
rationale: Approved as GH-WP-0002-T06. The PEP consumes by compare-and-swap before
the protected side effect; the PDP never mutates; there is no unconsume. Same request
digest is idempotent success; a different digest against a consumed object is conflict.
This closes the three named races and unblocks APPROVAL-WP-0001-T05 and FLEX-WP-0017-T05.
decided_by: Bernd Worsch
decided_at: '2026-08-29T12:51:01.355583Z'
```
## 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.

View file

@ -0,0 +1,148 @@
# Approval consumption ordering
**Owner:** gate-house
**PIP:** approval-engine
**PDP:** access-engine (currently flex-auth)
**Status:** normative for GH-WP-0002-T06; recorded as `GH-DEC-2026-003`
**Date:** 2026-08-29
**Statute:** `net-kingdom/canon/standards/security-layer-model_v0.7.md` §9.4, §9.7.34, §16
**Blocks:** `APPROVAL-WP-0001-T05`, `FLEX-WP-0017-T05`
This contract settles who marks an approval consumed, at what point relative
to the decision, and who owns each of the three named failure modes. Nothing
in `approval-engine` may implement a public consume by guessing this.
## Sequence
```text
1. PIP GET /v1/approvals/{id}/claim → valid_now (a fact, not permission)
2. PDP Check / decide → ALLOW | DENY | …
3. PEP POST /v1/approvals/{id}/consume → CAS valid → consumed
4. PEP protected side effect → only after consume succeeds
```
The PDP **never mutates**. Staff **never** calls consume. `audit-core` **never**
calls consume. The signaler is the PEP-shaped consumer that is about to cause
the protected side effect named in the binding — today `secrets-engine` for
OpenBao writes, `ops-warden` for the SSH signing lane, and any other consumer
companion §5 already treats as PEP-shaped.
Holding a claim with `valid_now: true`, or an ALLOW against that claim, is not
authority to act. The consume is.
## Why consume is before the side effect
§9.7.3 says consumption MUST NOT be inferred from a decision record. That
stands. A decision record proves an intent to act, not an act. The consume
mutation is the act's evidence at this engine.
The same paragraph currently reads as if the *action* must precede the
*consume call*. That reading is a protocol, and it is the wrong one. If the
PEP acts and then consumes, two racing PEPs can both observe `valid_now`,
both receive ALLOW, and both act; CAS then prevents only the second
*record*, not the second *side effect*. Single consumption would be theatre.
**The PEP MUST obtain a successful consume before the protected side effect.**
In-flight duplicate ALLOWs are expected; the CAS serializes use. A later
CheckRequest sees `valid_now: false` and cannot mint a new ALLOW against the
same object.
This is a protocol clarification of §9.7.3, not a retraction of the forensic
claim. It lands in statute v0.8. Until then this contract governs the
implementers who were blocked on it.
## Consume request
```text
POST /v1/approvals/{id}/consume
```
```json
{
"request_digest": "<canonical request digest from the decision binding>",
"decision_id": "<optional; the ALLOW this consume discharges>"
}
```
`request_digest` is the same digest companion §5.2 already uses as the
mechanical replay test (`NewDecisionBinding.request_digest` in flex-auth).
Go's `json.Marshal` of a CheckRequest is **not** this digest. Compare to the
binding the PDP already computed, not to a re-serialized CheckRequest.
The mutation is compare-and-swap:
| Current status | Digest on the object | Result |
| --- | --- | --- |
| `approved`, inside validity window | none | consumed; store this digest; emit `use` |
| `consumed` | same digest | **idempotent success** — same logical request, a retry |
| `consumed` | different digest | **conflict** — already used for another request |
| `revoked` / `superseded` / `expired` / outside window / not `approved` | — | **conflict**; MUST NOT act |
The `use` outbox row is inserted in the same transaction as the status change
([`approval-outbox.md`](approval-outbox.md)). A failed insert rolls the
consume back. An `audit-core` outage does not.
There is **no unconsume**. There is no reserve/release. An approval authorizes
one *attempt*, not one *success*.
## The three failure modes
### 1. Allow rendered, never consumed
**Cause.** PEP crash or abandonment between ALLOW and consume.
**Effect.** The object stays `approved`. A later PEP can still consume it,
inside the remaining validity window and inside the ALLOW's own lifetime
(§9.7.1). That is a delayed first use, not a replay of a completed one.
**Owner.** The PEP is obligated to consume before the side effect. The
**approval-engine** does not auto-consume from the existence of a decision.
The **PDP** bounds the window with the decision lifetime. Detection of a
stale ALLOW with no matching `use` is a finding on this surface
([`approval-emission-detection.md`](approval-emission-detection.md)), not a
consume.
### 2. Double consumption by racing callers
**Cause.** Two PEPs, two ALLOWs in flight, both presenting consume.
**Effect.** CAS: one success; the other is idempotent success if and only if
the digest matches, otherwise conflict.
**Owner.** `approval-engine` performs the CAS. The PEP that receives conflict
MUST NOT perform the side effect. The PDP that rendered the second ALLOW has
done nothing wrong — it decided on a still-valid claim.
Same-digest idempotent success is a retry of one logical request. Companion
§5.2 already permits reusing an ALLOW inside its binding and lifetime. The
side effect MUST itself be idempotent under that digest; that is the PEP's
obligation, not this engine's.
### 3. Consumed, then the authorized action fails
**Cause.** Consume succeeded; the protected side effect then failed or was
never reached (crash after consume).
**Effect.** The approval is spent. Retry requires a new approval object.
**Owner.** Accepted here as the cost of closing (2). Unconsume would reopen
replay. A flake is an issuance problem, not a reason to make consumption
reversible. The PEP MAY request a new approval; it MUST NOT infer that the
spent object is still valid.
## What this is not
- Not a second decision. Consume answers "did this engine accept the use",
never "may this actor do X".
- Not an inference from `audit-core`. The archive reports what it received.
- Not a PDP duty. `access-engine` reads the claim and never mutates.
- Not implemented until `approval-engine` wires `POST /consume` to the CAS
that already exists internally, with the digest column this contract adds.
## Acceptance
`APPROVAL-WP-0001-T05` may implement the public consume against this
document. `FLEX-WP-0017-T05` may require 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 — becomes in-scope with
that implementation.

View file

@ -0,0 +1,111 @@
# Approval emission detection surface
**Owner:** gate-house
**Source:** approval-engine
**Archive:** audit-core
**Observer:** kings-guard (silence-as-signal; offered)
**Status:** normative for GH-WP-0002-T04
**Date:** 2026-08-29
**Statute:** `net-kingdom/canon/standards/security-layer-model_v0.7.md` §9.6
**Source declaration:** `approval-engine/cadence.yaml`
A hash chain proves accepted records were not altered or truncated. It proves
nothing about an event never emitted. Atomicity (the outbox contract) prevents
accidental omission. This surface is what makes a *silent* loss visible.
It does not prevent a compromised source from suppressing. That residual is
stated in §9.6 and is not closed here.
## Form
Approval evidence is **load-bearing** and **low-volume**. Rate monitoring is
forbidden: a handful of revocations a month has no rate to drop below, and
suppression is indistinguishable from a quiet month.
The required form is **heartbeat plus reconciliation**. Reference instance:
`approval-engine/cadence.yaml` and `approval-engine/docs/emission-cadence.md`.
### Heartbeat
A signed positive claim: *nothing to report*, together with per-class
transition counts since the previous heartbeat (or since process start on
the first). The claim can itself go missing, which is the point.
| Field | Value |
| --- | --- |
| Class | `heartbeat` |
| Interval | 24 hours (SHOULD also be emittable on demand) |
| Assertion | `nothing-to-report` |
| Counts | `{issuance, use, supersession, revocation}` of committed outbox rows |
| Missing heartbeat | **finding** |
Heartbeat rows use the outbox table and the same at-least-once drain. They are
not coupled to an object mutation.
### Reconciliation
Compare `approval-engine`'s committed outbox counts per class to `audit-core`'s
accepted event counts for `source=approval-engine` and the corresponding
`action`. Divergence is a **finding**, not a log line.
| Source class | `audit-core` `action` |
| --- | --- |
| `issuance` | `approval.issuance` |
| `use` | `approval.use` |
| `supersession` | `approval.supersession` |
| `revocation` | `approval.revocation` |
| `heartbeat` | `approval.heartbeat` |
Undrained local rows are lag, not yet a divergence. A row with `drained_at` set
that `audit-core` does not hold is the omission case §9.6 names.
### Lag bound
| Bound | Default (reference) | Exceed |
| --- | --- | --- |
| Outbox depth | 100 undrained rows | **finding** |
| Outbox age | 1 hour for the oldest undrained row | **finding** |
These detect a stuck drain. They are not a completeness proof.
## What is a finding
- No heartbeat arrives for more than one interval.
- `audit-core` count for a class is less than the source's drained count for
that class.
- Outbox depth or age exceeds the lag bound.
None of these are rate drops. None of these are informational logs.
`kings-guard` consumes the finding as observation of the stream, not of its
contents. Until it reports that it is watching in production, no argument may
assume this surface is live (§12). The source MUST still publish the
declaration; a detector that is not yet watching does not excuse an undeclared
source.
## Who owns which half
| Half | Owner |
| --- | --- |
| Publish cadence and expose committed counts / lag | approval-engine |
| Hold accepted events and answer per-source per-class counts | audit-core |
| Raise silence, divergence, and lag as findings | kings-guard |
| Name the form and the residual | gate-house |
## Conformance check (queued for statute v0.8 §11)
GH-WP-0002-T05. Mechanically checkable, in the same list as the existing §11
bullets:
- every repository catalogued as an evidence source, or that emits
**load-bearing** evidence, declares its emission guarantee in a
machine-readable form at a path named in its layer declaration:
- **load-bearing:** local transactional outbox (this contract's shape) **and**
a heartbeat-or-reconciliation cadence declaration;
- **attributive, deliberately non-atomic:** the named trade, and a statement
that completeness is not claimed.
A new engine catalogued under §9.4 / §4 as an evidence source that ships
without this declaration reintroduces the approval omission gap silently.
Until v0.8 lands, this paragraph is doctrine; `approval-engine`'s
`cadence.yaml` plus `layer.yaml` is the reference form.

View file

@ -0,0 +1,105 @@
# Approval-engine transactional outbox contract
**Owner:** gate-house
**Implementer:** approval-engine
**Evidence engine:** audit-core
**Status:** normative for GH-WP-0002-T02
**Date:** 2026-08-29
**Statute:** `net-kingdom/canon/standards/security-layer-model_v0.7.md` §9.4, §9.6
**Failure mode:** `GH-DEC-2026-002`
**Source draft:** `approval-engine/docs/outbox-contract.md` (adopted)
This is the wire. Boundary and locality are in the statute and in
`approval-engine/INTENT.md`. An implementer MUST NOT satisfy this contract by
emitting synchronously to `audit-core` inside the state-change transaction.
That path is atomic and is forbidden: an audit outage would become an inability
to revoke.
## Rule
Every issuance, use, supersession, and revocation **inserts an outbox row in
the same transaction** that mutates the approval object. The durable queue
lives in `approval-engine`'s own store. Drain is asynchronous, at-least-once.
`audit-core` dedupes on `event_id`; a replay does not fork the chain.
If the outbox insert cannot be committed, the mutation does not commit.
Emit-after-commit is a defect.
Heartbeat rows (see [`approval-emission-detection.md`](approval-emission-detection.md))
use the same table and the same at-least-once drain. They are not coupled to an
object mutation.
## Event classes
| Class | When | `audit-core` `action` |
| --- | --- | --- |
| `issuance` | object becomes `approved` (threshold met) | `approval.issuance` |
| `use` | object becomes `consumed` (public consume under [`approval-consumption.md`](approval-consumption.md)) | `approval.use` |
| `supersession` | object becomes `superseded` | `approval.supersession` |
| `revocation` | object becomes `revoked` | `approval.revocation` |
| `heartbeat` | signed *nothing to report* plus counts | `approval.heartbeat` |
Expiry is a clock crossing, persisted on observation, and is **not** an
emitted class. The validity window is already on the object.
## Outbox row
| Field | Type | Notes |
| --- | --- | --- |
| `event_id` | UUID | Stable. Drain retries reuse it. |
| `class` | enum above | |
| `approval_id` | UUID or null | Null only for heartbeat. |
| `payload` | object | The `audit-core.event.v1alpha1` record, ready to POST. |
| `created_at` | RFC 3339 UTC | |
| `drained_at` | RFC 3339 UTC or null | Set after a successful `audit-core` ack. |
## Payload (`audit-core` v1alpha1)
```json
{
"schema_version": "audit-core.event.v1alpha1",
"event_id": "<same as outbox.event_id>",
"observed_at": "<created_at>",
"tenant": "platform",
"scope": "netkingdom-approvals",
"source": "approval-engine",
"actor": "<actor or null for heartbeat>",
"action": "approval.revocation",
"resource": "approval:<approval_id>",
"outcome": "success",
"reason": null,
"details": {
"class": "revocation",
"approval_id": "<uuid>",
"binding_digest": "sha256:…",
"request_digest": null,
"superseded_by": null
}
}
```
No secret values. `details` may add non-secret identifiers; it MUST NOT add a
validity verdict for consumers to branch on. `audit-core` MUST NOT expose an
approval-validity query; this payload does not invite one.
For `use` events, `details.request_digest` is the digest the PEP presented at
consume (see the consumption contract). It is null on other classes.
## Drain
1. Select undrained rows, oldest first.
2. POST each payload to `audit-core`.
3. On success, set `drained_at`.
4. On failure, leave the row; retry later. **Do not** roll back the object
mutation — it already committed with the row.
An `audit-core` outage therefore cannot block a revocation. This engine's
own store being unavailable can, and should: the change could not have been
recorded anyway. That is the whole of `GH-DEC-2026-002`.
## Forbidden shapes
- `audit-core` HTTP (or any client) inside `BEGIN``COMMIT` of a mutation.
- Best-effort publish after commit with no row.
- A second, non-local queue as the durability mechanism.
- Deduping in `approval-engine` instead of relying on `event_id` at `audit-core`.

View file

@ -0,0 +1,57 @@
# Approval evidence integrity — contracts and rulings
**Recorded:** 2026-08-29
**Workplan:** GH-WP-0002
**Audience:** approval-engine, access-engine / flex-auth, audit-core, kings-guard, secrets-engine, ops-warden
**Status:** Reference note. The normative artifacts are the contracts under
`docs/contracts/` and the decision records GH-DEC-2026-002 and GH-DEC-2026-003.
---
## Why this landed as contracts, not a statute bump
`approval-engine` shipped the object, the local outbox, and a cadence
declaration, and stopped at a public consume because GH-WP-0002-T06 was unset.
`FLEX-WP-0017-T05` is wait on the same unset. The workplan's remaining tasks
were doctrine, not another version of the layer model.
v0.7 stays the accepted statute. Two protocol clarifications and one §11
bullet are queued for v0.8 rather than silently rewritten into an accepted
text:
1. §9.7.3 — consumption is never inferred from a decision record (stands);
the PEP consumes *before* the protected side effect (clarifies the
protocol the paragraph currently implies the other way).
2. §16 — "who marks an approval consumed" is no longer open.
3. §11 — evidence sources declare their emission guarantee (GH-WP-0002-T05).
Inbox items that also belong in v0.8 (pep-stance inventory rows, the §13
table becoming a pointer into `maturity-engine`, `railiance-master`'s own-voice
declaration) are not mixed into this workplan.
## What each task produced
| Task | Artifact |
| --- | --- |
| T02 | `docs/contracts/approval-outbox.md` — adopts `approval-engine`'s wire |
| T03 | GH-DEC-2026-002 — fail-closed only on the local store |
| T04 | `docs/contracts/approval-emission-detection.md` — heartbeat + reconciliation |
| T05 | detection contract, last section — queued for v0.8 §11 |
| T06 | `docs/contracts/approval-consumption.md` and GH-DEC-2026-003 |
## The two rulings in one line each
- **Revocation:** if the local outbox insert cannot commit, the revocation
does not commit. An `audit-core` outage MUST NOT block a revocation.
Synchronous emission to `audit-core` inside the mutation is forbidden.
- **Consumption:** the PEP consumes by CAS before the side effect. The PDP
never mutates. There is no unconsume. Same-digest retry is idempotent;
a different digest against a consumed object is conflict.
## Assent still required of the implementers, not of the estate
These contracts bind `approval-engine` and `access-engine` because they asked
for them and were blocked on them. They do not move a vocabulary away from a
repository that currently uses it, so they do not follow the GH-DEC-2026-001
assent pattern. Contest is still welcome: a protocol that cannot be
implemented is a success of the loop, not a courtesy.

View file

@ -4,9 +4,10 @@ type: workplan
title: "Approval evidence integrity"
domain: infotech
repo: gate-house
status: active
status: finished
origin: GH-IN-0001
state_hub_workstream_id: "393d46ad-4f8c-5578-b63d-91cb0e8b9502"
updated: "2026-08-29"
---
# Approval evidence integrity
@ -37,7 +38,7 @@ versus attributive distinction.
```task
id: GH-WP-0002-T02
status: progress
status: done
priority: high
state_hub_task_id: "73a8feb0-02bd-5191-bdfd-82bcc42c6756"
```
@ -45,47 +46,55 @@ state_hub_task_id: "73a8feb0-02bd-5191-bdfd-82bcc42c6756"
Specify the transactional-outbox contract for `approval-engine`: same
transaction as the object mutation, queue local to the engine, at-least-once
into the outbox since `audit-core` dedupes on event id and a replay does not
fork the chain. Boundary and locality are in `approval-engine/INTENT.md` and
§9.4; the wire contract is not yet written.
fork the chain. Done — `docs/contracts/approval-outbox.md` adopts
`approval-engine`'s wire (`docs/outbox-contract.md`) as Gate House doctrine.
```task
id: GH-WP-0002-T03
status: todo
status: done
priority: high
state_hub_task_id: "1fc8fd3c-7af4-50e7-a07c-f45016f8b1f3"
```
Decide the revocation failure mode explicitly: fail closed, or proceed with a
detectable gap. Both are defensible; undecided is not, and an implementation
accident is the worst outcome. Note v0.5's local-outbox rule narrows this
considerably — fail-closed now triggers only when the engine's own store is
down — but the ruling is still owed.
detectable gap. Done — GH-DEC-2026-002. Fail-closed only when
`approval-engine`'s own store cannot insert the outbox row. An `audit-core`
outage MUST NOT block a revocation. Synchronous emission inside the mutation
is forbidden. Proceed-with-gap is rejected for load-bearing approval evidence.
```task
id: GH-WP-0002-T04
status: todo
status: done
priority: medium
state_hub_task_id: "3d62dbf9-786e-58c1-bd87-fb43658ca865"
```
Give the gap a detection surface: outbox depth and age, or reconciliation of
`approval-engine` object counts against `audit-core` event counts per class.
Today nothing would surface a silent loss. Relate to §9.6's silence-as-signal
rule, which `kings-guard` offered to implement at its own layer.
Done — `docs/contracts/approval-emission-detection.md`. Form is heartbeat plus
reconciliation, not rate monitoring; lag bounds on outbox depth and age.
`approval-engine/cadence.yaml` is the reference source declaration.
`kings-guard` remains the observer; until it reports watching in production,
the declaration is still required.
```task
id: GH-WP-0002-T05
status: todo
status: done
priority: medium
state_hub_task_id: "2a2a73ea-2778-59d2-94ef-7a70a22bb169"
```
Add a §11 conformance check so the next engine catalogued as an evidence source
declares its emission guarantee rather than reintroducing this silently.
Done as doctrine — the check is the last section of
`docs/contracts/approval-emission-detection.md`. Queued for statute v0.8 §11
rather than patched into accepted v0.7. Load-bearing sources declare a local
outbox plus heartbeat-or-reconciliation; attributive non-atomic sources declare
the trade and do not claim completeness.
```task
id: GH-WP-0002-T06
status: todo
status: done
priority: high
state_hub_task_id: "250cb9b3-713d-5607-ad1b-225e339693bf"
```
@ -93,5 +102,7 @@ state_hub_task_id: "250cb9b3-713d-5607-ad1b-225e339693bf"
Settle the consumption ordering contract between `approval-engine` and
`access-engine` — who signals consumed, at what point relative to the decision,
and the handling of an allow never consumed, a double consumption by racing
callers, and consumption after a failed action. Raised by `flex-auth`; required
before `FLEX-WP-0017` T05. Recorded unresolved in `approval-engine/INTENT.md`.
callers, and consumption after a failed action. Done — GH-DEC-2026-003 and
`docs/contracts/approval-consumption.md`. The PEP consumes by CAS before the
side effect; the PDP never mutates; there is no unconsume. Unblocks
`APPROVAL-WP-0001-T05` and `FLEX-WP-0017-T05`.