2026-08-29 11:58:28 +02:00
---
layer: Engine
role: PIP
standard: net-kingdom/canon/standards/security-layer-model_v0.7.md
companion: net-kingdom/SECURITY-COMPANION.md
---
2026-08-28 22:33:50 +02:00
# INTENT
2026-08-29 11:58:28 +02:00
> **Layer: Engine. Role: PIP.** NetKingdom Security Layer Model §3.3 / §4
> catalog — a deterministic API for a modeled concept, where the same
> authoritative input state yields the same result. This engine **supplies
> facts a decision consumes as claims**; it never renders one. A new engine is
> a PIP unless the statute is amended, and §6 means it can never be a second
> PDP.
>
> Statute (accepted 2026-08-29):
> `net-kingdom/canon/standards/security-layer-model_v0.7.md`.
> Operative form: `net-kingdom/SECURITY-COMPANION.md` v0.2. The statute governs
> on disagreement; a disagreement is a finding for `gate-house`.
>
> Machine-readable declaration: `layer.yaml`.
2026-08-28 22:33:50 +02:00
>
> 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
> implementation.
## Purpose
> **approval-engine owns the approval as a durable, authenticated, consumable
> object — issued before an action, verified at the moment of use, and provably
> not replayable.**
It answers one question, totally and decidably:
> **Is this approval valid right now — for this exact action, target, actor, and
> purpose — and has it already been used?**
Nothing else. It does not decide whether the action is permitted; that is
`access-engine` 's and it stays the only decision point. An approval is one
2026-08-29 11:58:28 +02:00
*input* to that decision. An outage here is **input degradation** , which is this
engine's fallback to own (§9.3) — never a consumer-invented substitute for the
object.
2026-08-28 22:33:50 +02:00
## Why This Exists
The gap was raised by `flex-auth` on 2026-08-28 (`FLEX-DEC-2026-001` ) while
assenting to the security layer model. Splitting policy authoring from policy
evaluation left one half of the approval contract without an owner:
- the **design** of the approval contract is `gate-house` 's (Staff — doctrine);
- the **validation** of an approval at decision time is `access-engine` 's;
- the **durable object, authenticated entries, and atomic supersession** were
nobody's.
It is not `gate-house` 's: Staff holds no state another layer depends on at
runtime (standard §3.4). It is not `access-engine` 's: it does not own the
organizational approval lifecycle, and an evaluator that also owns the object it
evaluates is self-dealing. It is not `audit-core` 's: an approval needs mutable,
in-path, current-state semantics, and an append-only audit fabric is built for
the opposite property.
2026-08-29 11:58:28 +02:00
So it is a concept, and concepts get engines. The catalog types it **PIP**
because "we need an engine for X" must not drift into "X now decides".
2026-08-28 22:33:50 +02:00
## The Concept
An **approval** is an independent authorization artifact required for actions
that exceed normal policy. It binds, at issue time, to:
- the **action** and the **target** it authorizes;
- the **actor** who may use it, and the **principal** on whose behalf;
- the **purpose** or task it was granted for;
- a **validity window** ;
- the **approvers** — identified, authenticated, and countable, so that
distinct-approver requirements are enforceable rather than aspirational.
And it carries state that is deliberately not a maturity ladder:
```text
requested → approved → valid → consumed
↓
superseded / revoked / expired
```
2026-08-29 11:58:28 +02:00
An approval object is **not a Railiance workload** and never becomes one
(standard §20.1). Human commands, broker actions, and approvals that are not
themselves deployables sit outside the `railiance-*` / `rail-*` / `rapp-*` /
`reef-*` axes. This engine does not acquire an operational axis by being
consumed from one.
2026-08-28 22:33:50 +02:00
## Rigidity Is the Point
This engine is deliberately small, boring, and strict. Its state model is a
closed machine rather than a graded progression, because at the moment of use
the answer must be binary, total, and reconstructable.
**Atomic supersession** and **single consumption** are the security properties
that make Canon test `T-06 — Approval Replay` passable. An approval that is
partly valid, or whose validity must be inferred by replaying a log, is an
approval that can be replayed. Flexibility here is a defect.
2026-08-29 11:58:28 +02:00
**Consumption is a state change, never an inference** (standard §9.7.3). An
approval is consumed by a mutation in this engine. It MUST NOT be inferred from
the existence of a decision record — the decision precedes the action and the
action precedes consumption, so a decision record proves an intent to act, not
an act.
2026-08-28 22:33:50 +02:00
Progression modelling — graded, evidence-based, open-ended — belongs to
`maturity-engine` . The two engines are deliberate opposites and neither should
drift toward the other.
## What approval-engine Owns
- the durable approval object and its identifiers;
- authenticated approval entries, and distinct-approver counting;
- the state machine above, with **atomic** supersession and consumption;
- validity windows and expiry;
- binding to action, target, actor, principal, and purpose;
- revocation that does not require the holder's cooperation;
2026-08-29 11:58:28 +02:00
- an introspection API that `access-engine` consumes as an **input claim** ,
carrying issuer and freshness so the PDP can state a deadline for this input
class (§9.7.2);
2026-08-28 22:54:37 +02:00
- **emission atomicity** — every issuance, use, supersession, and revocation is
2026-08-29 11:58:28 +02:00
queued in this engine's **local** transactional store in the **same
transaction** as the state change. See below; this is a contract, not an
implementation detail;
- **emission cadence** for load-bearing classes, in the form that actually
works for rare events: positive reconciliation or a heartbeat, not rate
monitoring.
2026-08-28 22:54:37 +02:00
## Emission Atomicity
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.
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
2026-08-29 02:54:49 +02:00
**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` .
2026-08-28 22:54:37 +02:00
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:
An append-only archive with a verified hash chain proves that records were not
**altered or truncated after arrival**. It cannot prove that a record was never
sent (standard §9.6). A suppressed event leaves the chain perfectly intact and
verification reports intact. `audit-core` reports what it received and will not
imply it is everything that happened — correctly, because it cannot.
For an approval object this is not a corner case. **The single most valuable
event for an adversary to suppress is a revocation**, and its absence is exactly
what chain verification cannot see. Completeness is therefore this engine's
obligation and nothing downstream can retrofit it.
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.
2026-08-28 22:33:50 +02:00
2026-08-29 11:58:28 +02:00
## Which threat atomicity closes — and which it does not
v0.6 read as though a local outbox closed §9.6's opening sentence. It does not.
The decomposition is now in the statute and is this engine's to carry, because
approval evidence is **load-bearing** :
| Threat | Covered by | When |
| --- | --- | --- |
| Accidental omission — crash between mutation and emit | local outbox (§9.4) | **prevented** |
| Adversarial omission — a compromised source declines to insert, deletes before drain, or drains to nowhere | cadence and reconciliation | **detected, after the fact** |
| Adversarial omission at a compromised source | — | **nothing in the model prevents it** |
The outbox sits inside this engine's blast radius. It makes emission atomic
against crash and partial failure, and nothing more. That residual is real.
Approval events are **low-volume load-bearing** . Rate monitoring cannot work:
a handful of revocations a month has no rate to drop below, and suppression is
indistinguishable from a quiet month. The required form is **positive
reconciliation or a heartbeat** — compare this engine's object-state transitions
against `audit-core` 's event count per class, or assert *nothing to report* as
a signed positive claim that can itself go missing. `GH-WP-0002-T04` is the
reference instance. A source of load-bearing evidence **MUST** declare this
(standard §9.6).
**Custody is not the residual's answer.** Whether approvals warrant archival
custody stronger than every other source is **decided: no** (standard §16).
Approval evidence carries the same bound as every other source. Archival
custody does not address omission at all. If a future requirement genuinely
needs WORM or a transparency log, that is a different store with a different
owner, raised then. This engine must not plan around a stronger archive.
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
2026-08-29 02:54:49 +02:00
## The Claim Shape
2026-08-29 11:58:28 +02:00
`access-engine` consumes approvals as input claims. The claim MUST carry:
- the approval identifier;
- 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;
- **issuer** (this engine) and a **freshness** rule, so the PDP can state a
deadline for this input class rather than a single fiction covering every
source (§9.7.2, §17 request-claim schema).
Without the digest, wrong-action, wrong-target, and wrong-scope proofs have
nothing to compare against. Without issuer and freshness, a stated deadline
for the approval input class is unfalsifiable.
Until Taxonomy publishes the shared request-claim schema (§17; ownership
proposed, not assigned), this engine publishes the contract at its own
boundary and yields to the schema when it exists. Inventing a permanent local
shape is the drift §17 exists to prevent.
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
2026-08-29 02:54:49 +02:00
2026-09-01 23:45:48 +02:00
## Consumption Ordering — Resolved by GH-DEC-2026-003
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
2026-08-29 02:54:49 +02:00
2026-09-01 23:45:48 +02:00
The decision precedes consumption, and consumption precedes the protected
side effect. Gate House settled this in `GH-DEC-2026-003` and
`docs/contracts/approval-consumption.md` . Three failure modes are named:
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
2026-08-29 02:54:49 +02:00
- 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.
2026-09-01 23:45:48 +02:00
The PEP signals consumption by presenting the decision binding's canonical
request digest to this engine. approval-engine performs the CAS before the PEP
acts; `access-engine` never mutates. Same-digest retries are idempotent success,
different digests conflict, and a failure after consume leaves the approval
spent. There is no unconsume.
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
2026-08-29 02:54:49 +02:00
2026-08-28 22:33:50 +02:00
## What approval-engine Does Not Own
- **the decision** — `access-engine` , always and only (standard §6);
- **approval policy** — which actions require approval, how many approvers, and
which separations of duty apply: that is doctrine, and doctrine is
`gate-house` 's (standard §6.2, reaching decisions as input claims or
versioned policy rules);
- **identity and authentication of approvers** — `key-cape` , consumed as claims;
2026-08-28 22:54:37 +02:00
- **the evidence record** — `audit-core` holds the archive; this engine holds
2026-08-29 11:58:28 +02:00
only the operative current state. The guarantee is bounded: `audit-core` is
not WORM or object-locked, and its tamper evidence is conditional on live
preconditions. Stronger custody is decided against (§16); this engine must
not assume it has been reopened;
2026-08-28 22:33:50 +02:00
- **credentials** materialized after a decision — `secrets-engine` ;
2026-08-29 11:58:28 +02:00
- **maturity, readiness, or progression of anything** — `maturity-engine` ;
- **an unreachable-engine stance map** — this engine is not PEP-shaped. It
does not cause a protected side effect on the far side of a decision. Callers
that do are PEP-shaped and owe companion §5 / statute §6.4;
- **Railiance operational axes** — an approval is not a workload (§20.1). How
those axes meet this model beyond §20's first statement is unwritten; do not
assume a mapping.
2026-08-28 22:33:50 +02:00
The repository's seed README described this as "an engine for modelling and
managing decisions". That wording is withdrawn: **decision** is `access-engine` 's
word under standard §8, and this engine must never claim it.
## Design Principles
2026-08-29 11:58:28 +02:00
1. **PIP, never PDP.** Valid or not, consumed or not. No grades, and no
"may this actor do X".
2026-08-28 22:33:50 +02:00
2. **Atomic or it does not count.** Supersession and consumption are
compare-and-swap operations, never read-then-write.
3. **The evaluator is not the owner.** `access-engine` reads; it does not mutate.
4. **Approvals are inputs, never authority.** Holding one is not permission; it
is one claim the decision point weighs.
5. **Revocation beats cooperation.** An approval can be killed without the
holder's participation.
2026-08-29 11:58:28 +02:00
6. **Every transition is evidence, atomically, and locally.** Issuance, use,
supersession, and revocation are queued in this engine's own store in the
same transaction that made them true. An event that can be lost is a state
change that never happened, as far as anyone auditing can tell.
7. **Atomicity prevents accidents; it does not prevent a compromised source.**
Cadence for load-bearing classes is a heartbeat or a reconciliation, and it
is a MUST.
8. **Consumption is a mutation.** Inferring it from a decision record is a
defect.
9. **Small on purpose.** Scope growth here is a security regression.
2026-08-28 22:33:50 +02:00
## Non-Goals
- Not a policy decision point, and never one.
- Not a workflow or ticketing system; it stores the artifact, not the meeting.
- Not an approvals *inbox* or notification surface.
- Not an audit archive.
- Not a general state-machine service for other concepts.
2026-08-29 11:58:28 +02:00
- Not a Railiance axis, a PEP, or an actuation surface.
2026-08-28 22:33:50 +02:00
## What Would Make This Repository Wrong
- **It starts deciding.** Any endpoint that answers "may this actor do X" rather
than "is this approval valid" is the failure standard §6 names.
- **Supersession is not atomic.** If two supersessions can interleave, the
replay protection is theatre.
- **It grows a policy layer.** If it starts holding rules about who may approve
what, it has taken doctrine from `gate-house` .
- **Nobody consumes it.** If `access-engine` does not read approvals as input
claims, the object is decorative.
2026-08-28 22:54:37 +02:00
- **Emission is best-effort.** If a revocation can succeed while its event is
lost, the evidence half is silently incomplete and the most security-relevant
transition is the one most likely to vanish.
2026-08-29 11:58:28 +02:00
- **It claims the outbox closes omission.** Atomicity covers crash. Treating it
as covering a compromised source, or skipping the heartbeat, is the v0.6
overclaim this engine must not reintroduce.
2026-09-01 23:45:48 +02:00
- **It diverges from the consumption contract.** PDP mutation,
action-before-consume, different-digest reuse, or unconsume violates
`GH-DEC-2026-003` .
2026-08-29 11:58:28 +02:00
- **It cites observation or containment that has not happened.** Nothing is
observed in production, and nothing can be contained automatically
(companion §10).
2026-08-28 22:33:50 +02:00
## Success Criteria
1. `access-engine` renders approval-bound decisions using this engine's claims,
with the approval identifier reconstructable from the decision record;
2. Canon test `T-06 — Approval Replay` passes against a live implementation;
3. an approval can be revoked without the holder's cooperation, and the
revocation is effective at the next use;
4. distinct-approver requirements are enforced by the engine rather than by
convention;
2026-08-28 22:54:37 +02:00
5. no state change can be committed without its event — demonstrated by a test
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
2026-08-29 02:54:49 +02:00
that fails the transaction when emission fails, not by inspection, and with
the queue local so an `audit-core` outage never blocks a revocation;
2026-08-29 11:58:28 +02:00
6. the claim carries a binding digest, issuer, and freshness, and a decision
rendered against approval A for request R cannot be replayed for request R';
7. load-bearing classes declare a heartbeat or reconciliation, and divergence
from `audit-core` 's event count is a finding;
2026-09-01 23:45:48 +02:00
8. consumption implements `GH-DEC-2026-003` , including atomic use evidence,
same-digest idempotency, different-digest conflict, and no unconsume;
2026-08-29 11:58:28 +02:00
9. `FLEX-WP-0017` T03 and T05 are unblocked.