Local outbox, claim shape, and the unresolved consumption ordering

From the v0.5 review round.

- The outbox is local, in this engine's own transactional store, with no
  synchronous audit-core dependency inside the state-change transaction. Raised
  by audit-core: "or equivalent" would otherwise let an implementer satisfy
  atomicity by emitting synchronously, turning an audit outage into an
  inability to revoke.
- The claim must carry the approval identifier and a digest over the same
  canonical binding access-engine already computes, so "approved" and "approved
  for this exact request" are distinguishable at decision time. Raised by
  flex-auth, needed for FLEX-WP-0017 T05.
- Consumption ordering is recorded as UNRESOLVED rather than guessed. The
  decision precedes the action and the action precedes consumption, so an
  allow never consumed, a double consumption by racing callers, and consumption
  after a failed action are gaps neither engine closes alone. Nothing may be
  implemented by assuming that contract.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 2564823@bnt-lap001
Assistant-Session: 2a7ed827-4928-4b9f-8613-9135c9cadfe9
This commit is contained in:
tegwick 2026-08-29 02:54:49 +02:00
parent 35fee0ea84
commit ab5d69e8f1

View file

@ -2,7 +2,7 @@
> **Layer: Engine.** NetKingdom Security Layer Model §4 catalog — a deterministic
> API for a modeled concept, where the same authoritative input state yields the
> same result. Standard: `net-kingdom/canon/standards/security-layer-model_v0.4.md`.
> same result. Standard: `net-kingdom/canon/standards/security-layer-model_v0.5.md`.
>
> This file captures why this repository exists and the kind of system it is
> meant to become. It is aspirational and stable, not a description of current
@ -98,6 +98,15 @@ An approval MUST NOT be issued, consumed, superseded, or revoked without the
corresponding event being durably queued in the same transaction that changed
the state. Transactional outbox, or an equivalent that cannot separate the two.
**The queue is local.** The outbox lives in this engine's own transactional
store. No synchronous dependency on `audit-core` sits inside the state-change
transaction. With a local outbox, fail-closed triggers only when this engine's
own store is unavailable — where the change could not have been recorded anyway
— and an `audit-core` outage does not block a revocation. Emitting synchronously
to `audit-core` inside the transaction would also be atomic, and would turn an
audit outage into an inability to revoke: the operation least tolerable to block
during an incident. Standard §9.4, raised by `audit-core`.
This is a condition of `audit-core`'s assent (`AUDIT-IN-0001`) and is in the
standard at §9.4. The reasoning is worth carrying here rather than by reference,
because it constrains the implementation from the first commit:
@ -117,6 +126,31 @@ Consequence for design: emit-after-commit is a defect, not an optimization. A
best-effort publish, a fire-and-forget queue, or any path where the state change
can succeed while the event is lost is out of contract.
## The Claim Shape
`access-engine` consumes approvals as input claims. The claim MUST carry the
approval identifier **and a digest over the same canonical binding the decision
point already computes**, so that *approved* and *approved for this exact
request* are distinguishable at decision time. Without it, wrong-action,
wrong-target, and wrong-scope proofs have nothing to compare against.
## Consumption Ordering — Unresolved
The decision precedes the action, and the action precedes consumption. Three
states follow that neither engine closes alone:
- an allow rendered against an approval that is then never consumed;
- an approval consumed twice by racing callers;
- an approval consumed after the action it authorized has already failed.
`approval-engine` performs the mutation, because `access-engine` never mutates.
But *who signals consumption, at what point relative to the decision, and what
happens on each of the three states above* is a contract between the two engines
and is **not yet settled**. It is recorded in the standard's §16 and is required
before `FLEX-WP-0017` T05. Raised by `flex-auth`.
Nothing here may be implemented by guessing that contract.
## What approval-engine Does Not Own
- **the decision**`access-engine`, always and only (standard §6);
@ -185,5 +219,8 @@ word under standard §8, and this engine must never claim it.
4. distinct-approver requirements are enforced by the engine rather than by
convention;
5. no state change can be committed without its event — demonstrated by a test
that fails the transaction when emission fails, not by inspection;
6. `FLEX-WP-0017` T03 and T05 are unblocked.
that fails the transaction when emission fails, not by inspection, and with
the queue local so an `audit-core` outage never blocks a revocation;
6. the claim carries a binding digest, and a decision rendered against approval
A for request R cannot be replayed for request R';
7. `FLEX-WP-0017` T03 and T05 are unblocked.