Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012viPor8WJNCbV64ipwewrm Assistant: claude-code Assistant-Model: opus Assistant-Process: 1754332@bnt-lap001 Assistant-Session: 9c8ac536-ff5e-46a3-8ab1-a548bde25fc0
2670 lines
139 KiB
Markdown
2670 lines
139 KiB
Markdown
# Decision records
|
||
|
||
## GH-DEC-2026-001 — NetKingdom security layer model, the gate-house re-cut, and the access-engine reframing
|
||
|
||
```yaml
|
||
id: GH-DEC-2026-001
|
||
kind: decision
|
||
title: NetKingdom security layer model, the gate-house re-cut, and the access-engine
|
||
reframing
|
||
status: resolved
|
||
owner: Bernd Worsch
|
||
repo: gate-house
|
||
standard: net-kingdom/canon/standards/security-layer-model_v0.1.md
|
||
source_note: history/2026-08-28-security-layer-model-and-gate-house-recut.md
|
||
requested_dispositions:
|
||
- approved
|
||
- revised
|
||
- rejected
|
||
affects:
|
||
- gate-house
|
||
- flex-auth
|
||
- ops-warden
|
||
- ops-mason
|
||
- kings-guard
|
||
- whitehat-security
|
||
- net-kingdom
|
||
- zone-engine
|
||
created: '2026-08-28T19:15:15.607849Z'
|
||
updated: '2026-08-28T19:16:01.456127Z'
|
||
rationale: 'Approved in session on 2026-08-28. The three rulings were taken interactively:
|
||
Staff as the layer name, net-kingdom canon as the model''s home, and access-engine
|
||
as the rename target with the lane/rule demarcation accepted as its cost. Approval
|
||
covers the doctrine and documents only; the flex-auth rename remains a separate
|
||
governed migration, and the standard stays proposed pending assent from flex-auth,
|
||
kings-guard, and ops-warden.'
|
||
decided_by: Bernd Worsch
|
||
decided_at: '2026-08-28T19:16:01.456127Z'
|
||
state_hub_decision_id: "2d6509d0-ffd2-4209-89ab-bf68a4945ada"
|
||
```
|
||
|
||
## Context
|
||
|
||
The security estate acquired overlapping claims to the same responsibility, and
|
||
the overlap was invisible in each repository's own documents. `gate-house` was
|
||
seeded as a deterministic authority plane — a policy decision point with an
|
||
`/authorize` API, grant storage, and a revocation service — while `flex-auth`
|
||
already described itself as the authorization control plane and was actively
|
||
delivering `FLEX-WP-0017`, an approval contract binding approvals to action,
|
||
actor, target, and validity window. Neither repository's INTENT named the other.
|
||
`zone-engine` had already been ruled against on the same question.
|
||
|
||
Full review and evidence: `history/2026-08-28-security-layer-model-and-gate-house-recut.md`.
|
||
|
||
## Decision requested
|
||
|
||
Ratify three linked rulings. They are one decision because the second does not
|
||
hold without the first, and the third is the first applied to the repository
|
||
that was already right.
|
||
|
||
### Ruling 1 — Adopt the NetKingdom security layer model
|
||
|
||
The estate is layered **Taxonomy → Tooling → Engines → Staff**, distinguished by
|
||
determinism and by the kind of artifact each layer produces. The top layer is
|
||
named **Staff** — the general-staff sense of plans and doctrine without
|
||
execution — not "Helpers", which undersold a layer holding architecture,
|
||
controlling, and change.
|
||
|
||
The model is published as `net-kingdom/canon/standards/security-layer-model_v0.1.md`,
|
||
owned by gate-house, status `proposed`. It belongs in net-kingdom canon rather
|
||
than info-tech-canon because it is NetKingdom-flavored security architecture,
|
||
not general semantic contract.
|
||
|
||
It carries two normative rules:
|
||
|
||
- **Staff never touches Tooling directly. It acts only through Engine APIs.**
|
||
- **`access-engine` is the only policy decision point**, generalizing to the
|
||
whole estate the ruling first drawn in `zone-engine/INTENT.md` §5.
|
||
|
||
### Ruling 2 — Re-cut gate-house as the doctrine council
|
||
|
||
Gate House is Staff: the council where NetKingdom's security and defence
|
||
doctrine is established, documented, taught, and supervised. It holds no
|
||
runtime position.
|
||
|
||
The decisive argument is gate-house's own: **a decision point inside gate-house
|
||
would place the deterministic authority boundary inside the non-deterministic
|
||
management layer, violating INV-02 — the first invariant the repository exists
|
||
to defend.** The repository would have been the clearest available
|
||
counterexample to the canon it hosts.
|
||
|
||
Boundary: *the mandate and the operating mode are gate-house's; the decision is
|
||
access-engine's; the credential is secrets-engine's; the perimeter is
|
||
ops-mason's and ops-warden's.*
|
||
|
||
gate-house keeps what no other repository owns — the operating modes, the
|
||
principal/actor/runtime triple, mandates and authority ceilings, the change
|
||
dynamics envelope, the MCP doctrine, the posture asymmetry, the assurance
|
||
specifications, and the curriculum. It gives up `/authorize`, policy evaluation,
|
||
policy engine selection, grant storage, and revocation.
|
||
|
||
### Ruling 3 — Reframe flex-auth as an Engine and rename it access-engine
|
||
|
||
`flex-auth` is Engine-layer and remains the only policy decision point. It is
|
||
renamed **`access-engine`**. `auth-engine` was rejected: key-cape owns
|
||
authentication, and `auth-` preserves the ambiguity the rename exists to remove.
|
||
`permission-engine` was rejected as ageing badly against a future `role-engine`.
|
||
|
||
The name's one cost is that "access" is already spoken for operationally by
|
||
ops-warden and ops-mason. It is paid by a demarcation, now normative in §8 of
|
||
the standard: **ops-warden and ops-mason own access lanes — how a worker reaches
|
||
a host; access-engine owns access rules — whether they may.**
|
||
|
||
Sequence is binding: **reframe the INTENT first, rename second**, as a governed
|
||
migration. The rename touches `FLEX-WP` prefix ownership, State Hub identifiers,
|
||
ops-warden's routing tables, zone-engine's binding boundary text, and
|
||
secrets-engine integrations.
|
||
|
||
The reframing also splits a responsibility flex-auth currently holds whole:
|
||
**authoring and governing policy is Staff work (gate-house); evaluating it
|
||
deterministically and in-path is access-engine's, exclusively.** This is the
|
||
constructive resolution of the `FLEX-WP-0017` overlap — gate-house designs the
|
||
approval contract, access-engine validates approvals at decision time.
|
||
|
||
## What approval authorizes
|
||
|
||
- Publication of the layer model as a proposed net-kingdom standard.
|
||
- The gate-house INTENT re-cut, already applied at `7f13f72`.
|
||
- The layering review notes placed at the top of twelve estate INTENT files.
|
||
- Starting the flex-auth INTENT reframe.
|
||
- Retiring the gate-house artifacts that describe an engine: ADR-003 (policy
|
||
engine selection), milestones M0, M3, and M4, and `GH-WP-0001-T04`
|
||
(`/authorize` skeleton). `GH-WP-0001` is rewritten against the re-cut before
|
||
it is promoted to active.
|
||
|
||
## What approval does not authorize
|
||
|
||
- The `flex-auth` → `access-engine` rename itself. That is a separate governed
|
||
migration with its own record, and it must not begin before the INTENT
|
||
reframe lands.
|
||
- Any change to `zone-engine`'s 2026-08-23 disposition.
|
||
- Promoting the standard from `proposed` to `accepted`.
|
||
- Any change to another repository's workplans. Work structure stays with the
|
||
repository doing the work.
|
||
|
||
## Assent still required
|
||
|
||
Two adaptations move vocabulary away from repositories that currently use it,
|
||
and follow the estate's precedent that a boundary is drawn on review by the
|
||
other side rather than asserted — as flex-auth did to zone-engine:
|
||
|
||
1. **flex-auth** — Engine framing, the rename, and the authoring/evaluation split.
|
||
2. **kings-guard** and **ops-warden** — releasing "control plane" and the
|
||
security curriculum respectively to the layers that own them.
|
||
|
||
## Reversal
|
||
|
||
The rulings are documents; nothing executable depends on them yet, and no code
|
||
exists in gate-house. Reversal is reverting the INTENT and standard commits.
|
||
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'
|
||
state_hub_decision_id: "3b7c8336-ed0b-48ed-84e4-dee8cfdb50ae"
|
||
```
|
||
|
||
## 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'
|
||
state_hub_decision_id: "f768c894-7e68-4e23-93b5-f475f4b072e5"
|
||
```
|
||
|
||
## 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.
|
||
|
||
## GH-DEC-2026-004 — Layer emission-cadence ownership between Info Tech Canon and Net Kingdom
|
||
|
||
```yaml
|
||
id: GH-DEC-2026-004
|
||
kind: decision
|
||
title: Layer emission-cadence ownership between Info Tech Canon and Net Kingdom
|
||
status: resolved
|
||
owner: Bernd Worsch
|
||
repo: gate-house
|
||
standard: net-kingdom/canon/standards/security-layer-model_v0.7.md
|
||
source_note: kings-guard/specs/EmissionCadenceDeclaration.md
|
||
requested_dispositions:
|
||
- approved
|
||
- revised
|
||
- rejected
|
||
affects:
|
||
- gate-house
|
||
- info-tech-canon
|
||
- net-kingdom
|
||
- kings-guard
|
||
- qonto-assistant
|
||
created: '2026-09-04T00:55:29Z'
|
||
updated: '2026-09-04T00:55:29Z'
|
||
rationale: >-
|
||
Approved interactively on 2026-09-04. The layered split gives each artifact
|
||
one owner: ecosystem-wide semantics belong to info-tech-canon; NetKingdom-
|
||
specific MUST/SHOULD rules and the low-volume load-bearing heartbeat-plus-
|
||
reconciliation profile belong to net-kingdom. This avoids both a Staff-local
|
||
consumer schema and competing Taxonomy definitions.
|
||
decided_by: Bernd Worsch
|
||
decided_at: '2026-09-04T00:55:29Z'
|
||
state_hub_decision_id: "89c73f45-d07b-40ea-b428-5a7375ce4b09"
|
||
```
|
||
|
||
## Context
|
||
|
||
Security-layer-model v0.7 §§9.6 and 17 require evidence sources to declare an
|
||
expected emission cadence so silence can become a finding. King's Guard is the
|
||
only current consumer and drafted `EmissionCadenceDeclaration.md` against the
|
||
real `qonto-assistant` source, but correctly declined to keep a Staff-local
|
||
schema. The standard left ownership between the two Taxonomy repositories:
|
||
`info-tech-canon` for ecosystem-wide semantic contracts and `net-kingdom` for
|
||
NetKingdom standards of record.
|
||
|
||
Treating that as joint ownership would leave version authority ambiguous.
|
||
Giving the entire artifact to either repository would instead conflate a
|
||
reusable evidence/observability contract with the security obligations of one
|
||
estate. The split is therefore made by artifact, with one owner for each.
|
||
|
||
## Decision
|
||
|
||
**`info-tech-canon` owns the versioned, ecosystem-wide
|
||
`EmissionCadenceDeclaration` semantic contract.** It owns the generic forms,
|
||
fields, vocabulary, validation semantics, compatibility rules, and evolution
|
||
of the contract.
|
||
|
||
**`net-kingdom` imports that contract and owns the NetKingdom security
|
||
profile.** It owns which evidence classes MUST or SHOULD declare cadence, the
|
||
rule that rate monitoring is forbidden for rare load-bearing classes, and the
|
||
heartbeat-plus-reconciliation obligations that satisfy security-layer-model
|
||
§9.6.
|
||
|
||
The remaining ownership boundaries are unchanged:
|
||
|
||
- each source repository owns its declaration instance and emission behavior;
|
||
- `kings-guard` owns stream evaluation and silence findings, not the schema;
|
||
- Gate House owns doctrine and conformance supervision, not publication or
|
||
runtime behavior.
|
||
|
||
The King's Guard draft remains the provenance and handover input. Once the
|
||
Taxonomy artifacts are published, it becomes historical rather than a second
|
||
contract.
|
||
|
||
## What this authorizes
|
||
|
||
- Requesting `info-tech-canon` to adopt, revise, or contest the generic
|
||
contract and create its own owning work record.
|
||
- Requesting `net-kingdom` to adopt, revise, or contest the security profile
|
||
and create its own owning work record.
|
||
- Sources such as `qonto-assistant`, `approval-engine`, and `secrets-engine`
|
||
publishing instances against the eventual canonical contract and profile.
|
||
- King's Guard replacing its draft-shaped fixture with the published contract
|
||
without changing its consumer ownership.
|
||
|
||
## What this does not authorize
|
||
|
||
- Joint ownership or two independently versioned definitions of the generic
|
||
schema.
|
||
- Gate House or King's Guard retaining the schema as a local contract.
|
||
- Gate House creating workplans in either Taxonomy repository.
|
||
- Treating ownership as source conformance: `qonto-assistant` remains
|
||
incomplete until it publishes and emits the required positive claim.
|
||
|
||
## Assent and completion
|
||
|
||
The ownership ruling is resolved. Publication is complete only when both
|
||
Taxonomy owners reply in their own voice and publish or explicitly schedule
|
||
their respective artifacts. Until then the prior handover residual remains
|
||
open, now with an unambiguous requested disposition.
|
||
|
||
## Reversal
|
||
|
||
Supersede this record if the generic contract proves inseparable from the
|
||
NetKingdom security profile, or if Info Tech Canon cannot provide a stable
|
||
cross-domain contract boundary. Reversal must still name one owner for every
|
||
artifact; returning to undifferentiated "Taxonomy ownership" is not an
|
||
acceptable outcome.
|
||
|
||
## GH-DEC-2026-005 — The approval-claim is the step-1 artifact on the PEP consumption path
|
||
|
||
```yaml
|
||
id: GH-DEC-2026-005
|
||
kind: decision
|
||
title: The approval-claim is the step-1 artifact on the PEP consumption path
|
||
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
|
||
origin: GH-IN-0002
|
||
requested_dispositions:
|
||
- approved
|
||
- revised
|
||
- rejected
|
||
affects:
|
||
- gate-house
|
||
- approval-engine
|
||
- flex-auth
|
||
- secrets-engine
|
||
- ops-warden
|
||
rationale: 'Confirmed, with one addition. GH-DEC-2026-003 already named step 1 by
|
||
endpoint and by field; the approval-claim is what that endpoint serves and valid_now
|
||
is its field. ActionAuthorization is a proposed, unratified shape carrying no doctrine
|
||
standing here. The addition is that the split validation is stated as doctrine rather
|
||
than left implicit: each artifact is checked by the consumer against the layer that
|
||
owns its data, and no PIP republishes a PDP decision. The state-hub authority requirement
|
||
in the secrets-engine validator is struck because State Hub is a read model and
|
||
holds no runtime approval authority.'
|
||
decided_by: Bernd Worsch
|
||
state_hub_decision_id: "b606e8ce-f1fa-4b39-bfe9-c11e88a3cde6"
|
||
created: '2026-09-05T23:28:23.931441Z'
|
||
updated: '2026-09-05T23:28:23.931441Z'
|
||
```
|
||
|
||
## Context
|
||
|
||
`GH-DEC-2026-003` settled the consumption sequence and named step 1 by endpoint and
|
||
by field: `GET /v1/approvals/{id}/claim` → `valid_now` (a fact, not permission).
|
||
`approval-engine` serves the approval-claim at that endpoint
|
||
(`approval-engine/docs/approval-claim.md`). `secrets-engine`'s PEP validator
|
||
(`validate_action_authorization`, `src/secrets_engine/authorization.py`) calls the
|
||
same endpoint but expects a flex-auth `ActionAuthorization`. Two different objects
|
||
were being assumed on one path, and the divergence would have surfaced at first live
|
||
consume rather than at deployment.
|
||
|
||
`ActionAuthorization` originates in `flex-auth/docs/action-bound-authorization-contract.md`
|
||
as a proposed shape for durable approval storage that the same document assigns to
|
||
`approval-engine`. It was never ratified, appears nowhere in this repository's
|
||
doctrine, and has no `valid_now` field. `GH-DEC-2026-003` was issued after it without
|
||
reference to it. There is therefore nothing to reconcile: the claim was the step-1
|
||
artifact all along, and this record says so where implementers will find it.
|
||
|
||
`approval-engine` raised the question rather than guessing, which is the behaviour
|
||
§12 asks for. The three substantive assertions in the request were checked here
|
||
independently before confirmation.
|
||
|
||
## Decision
|
||
|
||
**1. The approval-claim is the step-1 artifact on the `GH-DEC-2026-003` path.**
|
||
It carries the approval fact: binding digest, validity window, consumption state,
|
||
observation freshness, and issuer. Holding one with `valid_now: true` remains not
|
||
authority to act.
|
||
|
||
**2. `ActionAuthorization` is not required on that path.** `approval-engine` is not
|
||
expected to serve it and MUST NOT serve it from the claim endpoint. A step-1 response
|
||
cannot contain a step-2 decision; any composed post-decision artifact would be a new
|
||
artifact at a new endpoint. The proposal is shelved on this path, not withdrawn.
|
||
|
||
**3. A PEP validates across the two artifacts it already fetches.** The claim is
|
||
checked for the approval fact; the flex-auth `DecisionEnvelope` from step 2 is checked
|
||
for exact `CheckRequest` match and policy package/version pin. This is doctrine, not
|
||
merely an implementation convenience: **each artifact is validated against the layer
|
||
that owns its data.** A PIP republishing the PDP's decision would put a copy of the
|
||
authority decision in a repository that did not render it — the same shape of error
|
||
the re-cut removed from Gate House itself. No safety property is lost, because
|
||
`ActionAuthorization` is a bundle of exactly these two checks.
|
||
|
||
**4. The `provenance.authority == "state-hub"` requirement is struck.** State Hub is
|
||
a read model. It is not the runtime approval authority and never was; flex-auth's own
|
||
contract says so. A validator that requires it would fail closed against every
|
||
correctly issued claim. `secrets-engine` removes the constant with the validator split.
|
||
|
||
Consequent changes: `approval-engine` none — the claim schema stands as published;
|
||
`secrets-engine` splits its validator and drops the authority constant; `flex-auth`
|
||
records that the proposal is shelved on this path. Nothing here gates
|
||
`APPROVAL-WP-0002-T03` — `secrets-engine` is fail-closed until its own gates land,
|
||
and this record governs first live consume, not deployment.
|
||
|
||
## Deferred
|
||
|
||
A ratified post-decision `ActionAuthorization` — one composed, signed, forwardable
|
||
object — remains coherent and is recorded here so it is not rediscovered as new. It
|
||
is not adopted now because it has no named issuer or lifecycle owner and the sequence
|
||
is already settled without one. Revisit on any of: a third or fourth PEP-shaped
|
||
consumer; a requirement for a single signed forwardable artifact (offline
|
||
verification, or an audit that must replay one object rather than a join);
|
||
~~resolution of flex-auth's open G3 finding by composition rather than by adding a
|
||
lifetime to the envelope~~ — **struck, see the amendment below**; or assent of the
|
||
statute §17 Taxonomy request-claim schema
|
||
(`APPROVAL-IN-0001`) — in which case converge there and do **not** revive this
|
||
separately.
|
||
|
||
Also rejected, so they are not re-proposed: serving `ActionAuthorization` at the claim
|
||
endpoint (structurally impossible for step 1, and it would assert an authority
|
||
`approval-engine` does not hold); and extending the claim with `approvals.required_count`
|
||
or `entries` (publishing approver identities is a deliberate least-disclosure non-goal,
|
||
and it would still not satisfy the validator).
|
||
|
||
### Amendment, 2026-09-06 — the G3 trigger is struck
|
||
|
||
Recorded the day after this decision, on corrections volunteered independently by
|
||
`approval-engine` and `access-engine`, and verified here against
|
||
`flex-auth/schemas/decision_envelope.schema.json`.
|
||
|
||
`FLEX-WP-0019` closed G3 on 2026-09-02 by **adding** the field: the schema requires
|
||
`lifetime` whenever `effect` is `allow`, sourced from the policy package's
|
||
`allow_ttl` with a 15-minute engine default, and `allow_ttl: none` renders a DENY
|
||
with reason `allow_lifetime_unstated` rather than a standing grant.
|
||
|
||
The trigger was therefore already spent when this decision was written, and it
|
||
resolved on the branch that removes the argument rather than the one that supports
|
||
it. This is not bookkeeping. Carrying its own end was the one structural thing the
|
||
composed bundle did that the split does not, so the strongest available case for
|
||
ratifying option D is not merely unspent — **it is answered, and the answer is no.**
|
||
A `DecisionEnvelope` now states its own lifetime without borrowing an
|
||
`ActionAuthorizationValidity`.
|
||
|
||
Three revisit triggers remain: a third or fourth PEP-shaped consumer; a requirement
|
||
for a single signed forwardable artifact; and assent of the §17 request-claim schema,
|
||
where the instruction is to converge rather than revive.
|
||
|
||
The error came from sourcing the trigger from a 2026-08-29 alignment record rather
|
||
than from current state — the same class of mistake as reading a change log for what
|
||
the body says, which the v0.6 findings audit hit on the same day
|
||
(`docs/conformance/2026-09-06-v06-findings-audit.md`). Both repositories reported it
|
||
against their own interest, which is why it was caught within a day.
|
||
|
||
### Stated consequence, 2026-09-06 — what the PEP stopped checking
|
||
|
||
Raised by `secrets-engine` on implementing the split, against its own position, and
|
||
recorded here rather than left in a commit message.
|
||
|
||
The distinct-approver threshold is no longer verified independently at the PEP. The
|
||
claim exposes no approver entries — publishing approver identities is the deliberate
|
||
least-disclosure non-goal this decision upheld — so `approval-engine` folds the
|
||
threshold into `valid_now`. That is the correct layer under this doctrine and it is
|
||
also **a genuine reduction in what the PEP checks on its own**, and the second thing
|
||
does not stop being true because the first one is.
|
||
|
||
The estate should hold this consciously. `valid_now` is a summary predicate, and a
|
||
consumer of a summary trusts the issuer's evaluation of everything folded into it.
|
||
The compensating property is not a second check at the PEP — that is the duplication
|
||
the split removes — but reconstructability at the issuer: `approval-engine`'s state
|
||
transitions and its `use` outbox row must make the threshold evaluation recoverable
|
||
after the fact under §9.6. Detection, not prevention, and it should be named as such
|
||
in the same register as §9.6's other residual.
|
||
|
||
A second consequence, also from `secrets-engine` and also correct: two digests now
|
||
cover the same proposed action and are never compared — `approval-engine`'s binding
|
||
digest over `{action, actor, principal, purpose, target}` and the flex-auth
|
||
`CheckRequest` digest, with `claim.binding.pdp_digest` preferred where the issuer
|
||
recorded one. They answer different questions at different layers and collapsing them
|
||
would be a layer violation wearing the costume of a refactor. `secrets-engine` holds
|
||
a test asserting the two functions disagree, which is the right way to defend a
|
||
distinction that looks like duplication.
|
||
|
||
**Narrowed, 2026-09-06, by `approval-engine` on implementing the §9.6 obligation.**
|
||
The framing above was too broad. `entries` is UNIQUE on `(approval_id, subject_id)`,
|
||
so a repeat approver is refused at insert: distinctness is a **storage invariant** in
|
||
that engine, not something a consumer or an auditor recomputes. What the PEP stopped
|
||
checking is therefore that *this engine applied its own invariant correctly* — not
|
||
whether dual control could be forged by a repeated signature, which the schema
|
||
forecloses. The reduction is real and it is narrower than "the threshold is now
|
||
unverified". `approval-engine` found this by writing a test that expected duplicate
|
||
entries to collapse to one distinct approver, watching it fail, and reporting the
|
||
result against its own earlier framing; it also dropped an `entry_count` field rather
|
||
than ship a number that by construction can never differ from the distinct count.
|
||
|
||
The compensating property is now implemented rather than asserted: `approval.issuance`
|
||
and `approval.use` both carry a `threshold` object — `required_count`,
|
||
`distinct_approver_count`, `threshold_met`, and approver `subject_id`, `approved_at`
|
||
and evidence reference — with tests asserting reconstruction from the `use` row
|
||
**alone**, performing the recomputation an auditor would. The disclosure boundary is
|
||
pinned by test: identities are on the outbox, never on the claim. The evidence path
|
||
carries them because that is what audit is for; the consumer-facing artifact discloses
|
||
the least it can. The split did not push identities to consumers by another route.
|
||
|
||
**Hub row provenance.** `approval-engine`'s `fix-consistency` C-32 check registered
|
||
the drafted record block in its own request document as a hub decision
|
||
(`b606e8ce-f1fa-4b39-bfe9-c11e88a3cde6`), which it then resolved rather than leave an
|
||
open row for a settled question. Gate House **adopts that row as canonical** and
|
||
records its id above, rather than registering a second one — the row's content is
|
||
correct and a duplicate would be worse than an imperfectly-provenanced single row. A
|
||
drafted decision block inside a requesting repository's document is machine-readable
|
||
to that repository's own consistency checks, which is a trap in this workflow; the
|
||
disposition is recorded here so the next request does not rediscover it.
|
||
|
||
## Reversal
|
||
|
||
Revert this record and the amendment to `docs/contracts/approval-consumption.md`. The
|
||
falsifier is a demonstrated check that two-artifact validation cannot perform and a
|
||
single composed artifact can — most plausibly offline or forwarded verification with
|
||
no access to the PDP. That is a trigger for the deferred option above, not for
|
||
returning `ActionAuthorization` to the claim endpoint, which stays forbidden on
|
||
structural grounds regardless.
|
||
|
||
## GH-DEC-2026-006 — The §13 registers become pointers only once maturity-engine publishes a readable export
|
||
|
||
```yaml
|
||
id: GH-DEC-2026-006
|
||
kind: decision
|
||
title: The §13 registers become pointers only once maturity-engine publishes a readable
|
||
export
|
||
status: resolved
|
||
owner: Bernd Worsch
|
||
repo: gate-house
|
||
standard: net-kingdom/canon/standards/security-layer-model_v0.7.md
|
||
source_note: workplans/GH-WP-0003-statute-v08-amendment-set.md (T03)
|
||
requested_dispositions:
|
||
- approved
|
||
- revised
|
||
- rejected
|
||
affects:
|
||
- gate-house
|
||
- net-kingdom
|
||
- maturity-engine
|
||
- ops-warden
|
||
- ops-mason
|
||
- user-engine
|
||
- tenant-engine
|
||
- access-engine
|
||
rationale: 'A statute of record should not carry state, and §13 and §13.1 are state:
|
||
they change without the doctrine changing, and a hand-maintained table inside a
|
||
standard is a register that drifts. maturity-engine is the right holder. But a pointer
|
||
is only an improvement once there is something readable to point at — a standard
|
||
that can only be understood by querying a live engine has traded one defect for
|
||
a worse one, and it would make the statute unreadable to exactly the auditor it
|
||
exists for. The migration is therefore conditioned on a committed, versioned export
|
||
rather than declared outright. Until that export exists the tables stay and rows
|
||
are transcribed, so no repository is left without an answer while the condition
|
||
is outstanding.'
|
||
decided_by: Bernd Worsch
|
||
created: '2026-09-05T23:38:49.777532Z'
|
||
updated: '2026-09-05T23:38:49.777532Z'
|
||
state_hub_decision_id: "780e1117-57aa-49c7-bf59-f69df2a67a4e"
|
||
```
|
||
|
||
## Context
|
||
|
||
`security-layer-model` v0.7 §13 holds the open-gap register and §13.1 the PEP
|
||
stance-map register. Both were created because the obligations above them named a
|
||
register that did not exist — §9.1's own logic, that a requirement whose register is
|
||
missing is a capability catalogued without a surface. They did their job.
|
||
|
||
`maturity-engine` (MAT-WP-0001) now holds both as queryable data: the §13 snapshot
|
||
with state and owner-status intact, the §13.1 inventory, `ASM-0`…`ASM-6` registered
|
||
as data owned by gate-house, `pep-stance-publication` owned by ops-warden, and
|
||
blocked-clean scored equal to conforming by test. It proposes that the statute table
|
||
become a pointer.
|
||
|
||
Four repositories are waiting behind the answer. `user-engine` and `tenant-engine`
|
||
have published stance maps and asked for §13.1 rows; ops-warden is published and
|
||
ops-mason is not. §6.4 obliges every PEP-shaped consumer to publish and obliges those
|
||
maps to be inventoried. Each week the question stays open, the register either
|
||
accumulates transcription or accumulates silence.
|
||
|
||
## Decision
|
||
|
||
**§13 and §13.1 become pointers to `maturity-engine`, and not before it publishes a
|
||
committed, versioned export of both registers that is readable without a live query.**
|
||
|
||
Two things are true at once and the ruling holds both.
|
||
|
||
**A statute should not carry state.** §13 and §13.1 change without the doctrine
|
||
changing. A table maintained by hand inside a standard of record drifts, and every
|
||
row transcribed into it is the manual step `maturity-engine` exists to remove. This
|
||
is the same rule as `GH-DEC-2026-005`: the artifact is held by the layer that owns
|
||
its data, and a repository that neither computes nor owns a fact should not be its
|
||
register of record.
|
||
|
||
**A standard must be readable on its own.** A pointer to a live engine is not a
|
||
register an auditor can read; it is an instruction to run software. Whatever else the
|
||
statute is, it must be legible in git, at a version, to a reader with no cluster
|
||
access — including a reader auditing the estate precisely because they do not trust
|
||
its running systems. Replacing a drifting table with an unreadable reference trades a
|
||
known defect for a worse one.
|
||
|
||
The condition reconciles them. `maturity-engine` computes and remembers; it also
|
||
publishes the result as a committed artifact at a stated path and version, and the
|
||
statute cites that path. The rows are then computed rather than transcribed, and
|
||
still readable without a query. **Publication is the migration's precondition, not
|
||
its follow-up.**
|
||
|
||
### What happens meanwhile
|
||
|
||
The tables stay, and rows continue to be transcribed into §13.1 at each version cut.
|
||
No repository waits on the export to get an answer. `user-engine`, `tenant-engine`,
|
||
ops-warden and ops-mason are inventoried in v0.8 whether or not the export has
|
||
landed by then.
|
||
|
||
This is deliberately the slower path. The alternative — strike the tables now and
|
||
point at the engine — would close four open requests immediately and leave the
|
||
statute stating an obligation whose register no reader can open, which is the exact
|
||
defect §13.1 was created to fix. Repeating a defect in the other direction is not
|
||
progress.
|
||
|
||
### What is asked of `maturity-engine`
|
||
|
||
Not a new capability, a published form of one it has. The export must be a file in
|
||
the repository, versioned, generated rather than hand-edited, carrying the same
|
||
fields the statute tables carry today (§13: capability, kind, declarer, owner, state,
|
||
owner-status; §13.1: consumer, stance map path, shape), and regenerable so drift
|
||
between the export and the engine's own state is detectable. `layer.yaml` and
|
||
`pep-stance.yaml` are the shape to follow: published, machine-readable, and asserted
|
||
equal to shipped behaviour by a test. §6.4 obligation 3 already requires that
|
||
equality of every PEP; a register of those maps should not hold itself to less.
|
||
|
||
When it exists, the §13 and §13.1 tables are replaced by a citation of it, and
|
||
`maturity-engine` becomes the register of record.
|
||
|
||
## Reversal
|
||
|
||
Revert to tables if the export proves to drift from the engine's computed state
|
||
faster than a hand-maintained table drifted from reality — that is, if generation and
|
||
publication decouple. The falsifier is mechanical: a regeneration that changes the
|
||
committed export without any underlying evidence having changed, or an underlying
|
||
change that does not reach the export.
|
||
|
||
## GH-DEC-2026-007 — The posture/maturity boundary is recomputability, bounded by a criteria-grounding rule
|
||
|
||
```yaml
|
||
id: GH-DEC-2026-007
|
||
kind: decision
|
||
title: The posture/maturity boundary is recomputability, bounded by a criteria-grounding
|
||
rule
|
||
status: resolved
|
||
owner: Bernd Worsch
|
||
repo: gate-house
|
||
standard: net-kingdom/canon/standards/security-layer-model_v0.7.md
|
||
source_note: kings-guard/docs/PostureMaturityBoundary.md (KG-DEC-2026-002, KG-COM-0001)
|
||
requested_dispositions:
|
||
- approved
|
||
- revised
|
||
- rejected
|
||
affects:
|
||
- gate-house
|
||
- net-kingdom
|
||
- kings-guard
|
||
- maturity-engine
|
||
- access-engine
|
||
rationale: 'Adopted with one addition. kings-guard is right that volatility describes
|
||
the two categories without partitioning them, and right that the discriminator is
|
||
already in §9.5''s determinism clause rather than being a new rule. The addition
|
||
closes the loophole the test leaves open: recomputability is assessed over the stated
|
||
criteria, so any judgment can be made to look recomputable by writing a criterion
|
||
that dereferences it. Criteria must bottom out in evidence about the subject, not
|
||
in another party''s opinion recorded as evidence. Without that clause the test is
|
||
satisfiable in form by exactly the inference it exists to exclude.'
|
||
decided_by: Bernd Worsch
|
||
created: '2026-09-06T06:07:34.017316Z'
|
||
updated: '2026-09-06T06:07:34.017316Z'
|
||
state_hub_decision_id: "9ece3338-0fc7-4741-bdfc-7fdea170dc0b"
|
||
```
|
||
|
||
## Context
|
||
|
||
`GH-WP-0003-T04`. v0.7 §9.5 states the maturity half of the boundary — *"Given the
|
||
same criteria and the same evidence it MUST return the same level; that determinism
|
||
is what makes it an Engine rather than an opinion"*, and *"A criterion that cannot be
|
||
evaluated by rule is not yet a criterion."* It does not state the posture half.
|
||
|
||
Gate House had proposed drawing the line on volatility: posture is fast-moving,
|
||
maturity is slow. `kings-guard` answered `KG-IN-0003` with a proposed revision
|
||
(`KG-DEC-2026-002`, argued in `kings-guard/docs/PostureMaturityBoundary.md`) rejecting
|
||
that line and offering recomputability instead. The commentary was also sent to
|
||
`maturity-engine`.
|
||
|
||
The concern behind the original question stands and is why the line has to be exact:
|
||
a second grading authority would be the same shape of mistake as a second decision
|
||
point, and §6 says it would arrive the same way — gradually, each time for a good
|
||
local reason.
|
||
|
||
## Decision
|
||
|
||
**The boundary is recomputability, not volatility.**
|
||
|
||
> Given the same criteria and the same evidence, recompute. If you MUST get the same
|
||
> answer, it is maturity and it belongs in an engine. If you CANNOT promise the same
|
||
> answer, it is posture and it belongs in Staff.
|
||
|
||
The volatility line is withdrawn. `kings-guard`'s argument against it is accepted in
|
||
full: volatility is an observation about how a value has behaved, not a definition of
|
||
what it is. It fails at both edges — a maturity criterion moves fast when evidence
|
||
lands in a burst, and a posture sits unchanged for months on a healthy subject — and,
|
||
more seriously, it *describes* the two categories without *partitioning* them. Every
|
||
case it does not obviously cover becomes an argument, and arguments at a boundary are
|
||
the mechanism §6 exists to prevent.
|
||
|
||
Recomputability is not a new rule. It is §9.5's own determinism clause pointed at the
|
||
one boundary where it had not been pointed, which is why it can be adopted without
|
||
enlarging the standard. §9.5 already states the maturity half; the posture half is its
|
||
mirror, and the pair partitions: **a criterion that cannot be evaluated by rule is not
|
||
yet a criterion; a judgment that can be evaluated by rule is not posture — it is a
|
||
criterion sitting in the wrong repository.**
|
||
|
||
### The addition — criteria must be grounded
|
||
|
||
Adopted with one clause `kings-guard` did not propose, and the reason is the loophole
|
||
their own framing opens.
|
||
|
||
Recomputability is assessed **over the stated criteria**. That makes it mechanical,
|
||
which is its virtue, and it also makes it satisfiable in form by the thing it exists
|
||
to exclude. Any judgment can be made to look recomputable by writing a criterion that
|
||
dereferences it: *"level 2 iff the reviewer marked the control adequate"* is perfectly
|
||
deterministic — recompute it and you get the same answer every time — and it has
|
||
smuggled an opinion into an engine wearing a rule's clothes. The engine would then be
|
||
grading, deterministically, on someone's judgment, which is precisely the second
|
||
grading authority the boundary is drawn to prevent.
|
||
|
||
Therefore:
|
||
|
||
> **A criterion MUST bottom out in evidence about the subject, not in another party's
|
||
> conclusion about the subject.** A recorded judgment may be evidence *that the
|
||
> judgment was made* — a fact with an issuer and a timestamp. It MUST NOT be evidence
|
||
> *that the thing judged is so*.
|
||
|
||
This is the same distinction §9.6 draws between what an archive proves and what it is
|
||
read as proving, and the same one this repository applies to `valid_now` in
|
||
`GH-DEC-2026-005`: a summary predicate is a fact about the issuer's evaluation, not a
|
||
substitute for the evaluation. Without the clause the test is mechanical and
|
||
circumventable; with it, the test is mechanical and the circumvention is itself
|
||
checkable.
|
||
|
||
### Consequences carried
|
||
|
||
The three `kings-guard` names are adopted as stated.
|
||
|
||
1. **The migration direction is permanent.** Anything called posture that turns out to
|
||
be recomputable moves to `maturity-engine` as a criterion; anything in
|
||
`maturity-engine` that needs judgment is not yet a criterion and moves back to
|
||
Staff. The boundary maintains itself under change because the test applies to each
|
||
item rather than to the category.
|
||
2. **Two authorities cannot grade the same subject property**, because a property is
|
||
either recomputable or it is not, and that fact does not depend on which repository
|
||
claims it. The failure becomes structurally prevented rather than conventionally
|
||
avoided.
|
||
3. **Capability readiness MUST NOT be an input to posture.** Readiness is
|
||
deterministic; posture is not; feeding one into the other would make posture partly
|
||
recomputable and blur the boundary from the `kings-guard` side. Volunteered by
|
||
`kings-guard` as a constraint on itself and accepted as normative.
|
||
|
||
Consequence 3 also answers the second half of `KG-IN-0003`: tracking kings-guard's
|
||
own gaps in `maturity-engine` creates no incident-time dependency, because posture
|
||
evaluation never consults readiness. The dependency would exist only if the mistake
|
||
consequence 3 forbids had already been made.
|
||
|
||
### The limit is recorded, not resolved
|
||
|
||
`kings-guard` flagged, against its own proposal, that *"the same evidence"* is not
|
||
well defined anywhere in the estate, so until §17's request-claim and gap-record
|
||
schemas exist, recomputability is a thought experiment rather than a check. That is
|
||
recorded in the statute alongside the test rather than left in the commentary. A
|
||
boundary that is correct but not yet mechanically checkable does beat one that is
|
||
checkable and wrong — but a reader is entitled to know which of the two they are
|
||
holding. It makes §17 load-bearing for this rule.
|
||
|
||
The caution is also recorded: *"posture"* has the drift profile *"control plane"* had
|
||
— it sounds specific and quietly absorbs whatever sits next to it. §8 exists because
|
||
the estate has been bitten by exactly that, and this repository described itself as a
|
||
control plane before the re-cut. The recomputability test is a defence against that
|
||
drift because it can be applied to a candidate *before* the word is stretched to cover
|
||
it. `kings-guard` asked to be held to it rather than trusted about it, and that is the
|
||
standing it gets.
|
||
|
||
No change to §8's three-way split and no §4 catalog change. Posture stays
|
||
non-deterministic and stays Staff; the test explains why, which is what was missing.
|
||
|
||
## Reversal
|
||
|
||
Revert if the grounding clause proves to exclude criteria the estate needs — that is,
|
||
if a legitimate maturity criterion cannot be expressed without dereferencing a
|
||
judgment. The falsifier is a concrete criterion, not an argument that one might exist.
|
||
The likely candidates are human-attested controls (a policy was reviewed, a drill was
|
||
run); the expected resolution is that the *attestation event* is the evidence and the
|
||
criterion grades on its existence, freshness, and issuer rather than on its verdict.
|
||
If that resolution does not hold for some real criterion, this clause is wrong.
|
||
|
||
## GH-DEC-2026-008 — The PDP digest is the binding correspondence; no cross-engine vocabulary mapping is published
|
||
|
||
```yaml
|
||
id: GH-DEC-2026-008
|
||
kind: decision
|
||
title: The PDP digest is the binding correspondence; no cross-engine vocabulary mapping
|
||
is published
|
||
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; approval-engine/docs/approval-claim.md
|
||
requested_dispositions:
|
||
- approved
|
||
- revised
|
||
- rejected
|
||
affects:
|
||
- gate-house
|
||
- approval-engine
|
||
- access-engine
|
||
- secrets-engine
|
||
- ops-warden
|
||
rationale: 'Raised by access-engine. approval-claim verification item 4 is a disjunction:
|
||
the consumer recomputes approval-engine''s native binding from the proposed action,
|
||
OR compares binding.pdp_digest to the decision''s request digest. The first limb
|
||
needs a cross-engine vocabulary mapping nobody owns and nobody has published; the
|
||
second is optional in the schema. Together that leaves ''approved for THIS request''
|
||
unestablished whenever the PDP digest is absent, while the claim still reads valid_now
|
||
true. The mapping is rejected because a translation between two engines'' vocabularies
|
||
can be silently wrong and would be owned by neither, and because recomputing another
|
||
layer''s binding is the re-derivation GH-DEC-2026-005 forbids. The digest is identity
|
||
rather than translation, so it is made required on the PDP path and consumers fail
|
||
closed without it.'
|
||
decided_by: Bernd Worsch
|
||
created: '2026-09-06T07:30:42.213794Z'
|
||
updated: '2026-09-06T07:30:42.213794Z'
|
||
state_hub_decision_id: "a36b3f02-fb08-47b5-9de3-c2d9e37678a4"
|
||
```
|
||
|
||
## Context
|
||
|
||
Raised by `access-engine` while implementing `GH-DEC-2026-005`, and declined by it
|
||
rather than solved locally — the right call, and the reason it reached a decision
|
||
record instead of a commit.
|
||
|
||
`approval-engine/docs/approval-claim.md` verification item 4 is a **disjunction**:
|
||
|
||
> `binding.digest` equals the digest of the binding the consumer computed from the
|
||
> proposed action, **or** `binding.pdp_digest` equals the `NewDecisionBinding` digest
|
||
> of that request.
|
||
|
||
Both limbs are load-bearing for one property — *approved for **this** request*, as
|
||
distinct from *approved*. Neither limb currently delivers it on the PDP path.
|
||
|
||
**Limb one requires a mapping nobody owns.** The claim's `binding.action` and
|
||
`binding.target` are in `approval-engine`'s vocabulary, over
|
||
`{action, actor, principal, purpose, target}`. `access-engine`'s policy package and
|
||
`CheckRequest` are in its own. To compute limb one, a consumer must translate between
|
||
them. No mapping is published. `access-engine` declined to invent one on the correct
|
||
ground that a wrong mapping fails **open** in the worst way available: it silently
|
||
accepts a claim approved for something else.
|
||
|
||
**Limb two is optional.** `binding.pdp_digest` is present only *"when recorded at
|
||
issue"*.
|
||
|
||
So on the path this estate actually runs, correspondence rests entirely on an optional
|
||
field, and where that field is absent a consumer can hold a claim with
|
||
`valid_now: true`, receive an ALLOW, consume, and act — with nothing having
|
||
established that the approval and the decision concern the same action and target.
|
||
The claim would not be lying; `valid_now` answers a different question.
|
||
|
||
## Decision
|
||
|
||
**`binding.pdp_digest` is the binding correspondence on the `GH-DEC-2026-003` path,
|
||
and it is required there. No cross-engine vocabulary mapping is published.**
|
||
|
||
1. **A consumer on the PDP path MUST verify `claim.binding.pdp_digest` equals the
|
||
decision's `NewDecisionBinding.request_digest`.** It is no longer a preferred
|
||
comparison; it is the comparison.
|
||
2. **A claim carrying no `pdp_digest` MUST NOT be used on that path.** The consumer
|
||
fails closed. It is not an error to be worked around, and specifically it is not a
|
||
licence to fall back to limb one.
|
||
3. **Limb one survives only where the consumer is natively in `approval-engine`'s
|
||
vocabulary** — that engine's own tests, and the `T-06` assurance case, which uses
|
||
the native binding precisely because no PDP digest exists there. It is not a
|
||
fallback for a PDP-path consumer.
|
||
4. **`approval-engine` MUST record the PDP digest at issue for any approval intended
|
||
for use on the PDP path**, and the claim schema states which approvals those are
|
||
rather than leaving it to the requester's memory.
|
||
|
||
### Why not publish the mapping
|
||
|
||
`access-engine` offered to co-author one. Declining is the substantive half of this
|
||
record.
|
||
|
||
**A mapping is a translation; a digest is an identity.** A translation can be wrong in
|
||
a way that still produces a confident answer — the failure mode `access-engine` named,
|
||
and it fails open. A digest comparison has no wrong-but-plausible result: it matches
|
||
or it does not.
|
||
|
||
**A published mapping would be owned by neither engine.** It would sit between two
|
||
vocabularies, each of which is its own repository's to evolve under §2, and it would
|
||
have to be revised whenever either side changed. That artifact is a third authority on
|
||
what a request *is* — the shape §6 exists to prevent, arriving as plumbing rather than
|
||
as a decision point.
|
||
|
||
**And computing limb one is the re-derivation `GH-DEC-2026-005` forbids.** A consumer
|
||
recomputing `approval-engine`'s native binding from its own vocabulary is a consumer
|
||
deriving another layer's data instead of validating against the layer that owns it.
|
||
The rule this estate just adopted answers this question on its own terms.
|
||
|
||
### The cost, stated
|
||
|
||
An approval requested without a known `CheckRequest` cannot be used on the PDP path,
|
||
because there was no PDP digest to record. That is a real constraint and it is
|
||
**correct behaviour, not a defect**: an approval granted against an unspecified action
|
||
does not become an approval for a specific one because a consumer later found a use
|
||
for it. Any workflow needing detached approval must either bind the request at issue
|
||
or accept that its approvals are not usable here.
|
||
|
||
This narrows what `approval-engine` may issue for this path. It does not narrow what
|
||
it may issue.
|
||
|
||
## Reversal
|
||
|
||
Revert if a legitimate workflow genuinely cannot bind the `CheckRequest` at approval
|
||
time — a standing human authorization for an action whose exact parameters are not
|
||
knowable until execution is the plausible candidate. The falsifier is a concrete
|
||
workflow, not the possibility of one. The expected resolution is that such a case
|
||
needs a *class* binding with its own contract, not a relaxation of this one to an
|
||
optional field, because an optional correspondence is indistinguishable at the
|
||
consumer from no correspondence at all.
|
||
|
||
## GH-DEC-2026-009 — Unknown is not a zone and fails closed; stance maps declare their scoping axis
|
||
|
||
```yaml
|
||
id: GH-DEC-2026-009
|
||
kind: decision
|
||
title: Unknown is not a zone and fails closed; stance maps declare their scoping axis
|
||
status: resolved
|
||
owner: Bernd Worsch
|
||
repo: gate-house
|
||
standard: net-kingdom/canon/standards/security-layer-model_v0.7.md
|
||
source_note: flex-auth/docs/stance-register-review.md
|
||
requested_dispositions:
|
||
- approved
|
||
- revised
|
||
- rejected
|
||
affects:
|
||
- gate-house
|
||
- net-kingdom
|
||
- ops-warden
|
||
- secrets-engine
|
||
- user-engine
|
||
- tenant-engine
|
||
- ops-mason
|
||
- access-engine
|
||
- zone-engine
|
||
rationale: 'Raised by access-engine on the first occasion the §13.1 register held
|
||
enough rows to diverge. Two rulings. First: an unreachable engine and an unclassifiable
|
||
subject are different failure cases, and §9.3''s per-zone availability trade is
|
||
only available for the first. Trading openness for a zone requires knowing the zone;
|
||
where the scope is unknown the trade cannot have been made for it, and fail_open
|
||
on unknown hands the most permissive stance to exactly the request an attacker can
|
||
most easily make unclassifiable. unknown is therefore not a zone and MUST fail closed.
|
||
Second: the register cannot aggregate across incommensurable scoping axes, so each
|
||
map declares its axis and its relationship to zone, and the register states what
|
||
it cannot answer rather than implying it can.'
|
||
decided_by: Bernd Worsch
|
||
created: '2026-09-06T12:17:19.104531Z'
|
||
updated: '2026-09-06T12:17:19.104531Z'
|
||
state_hub_decision_id: "009bc960-9029-4052-aaba-4c5e27a328dc"
|
||
```
|
||
|
||
## Context
|
||
|
||
On 2026-08-29 `access-engine` told this repository it was the party positioned to
|
||
notice when the aggregate of consumer stances diverged from what the policy packages
|
||
say — and then recorded that the claim was sound but the data was not there, because
|
||
§13.1 held one row and one row cannot diverge from anything. `secrets-engine`'s
|
||
publication made it two, and the divergence appeared immediately.
|
||
|
||
That is worth noting on its own: the register earned its existence the first time it
|
||
had two entries.
|
||
|
||
**`ops-warden` declares `unknown` → `fail_open`.** Explicit, versioned, under
|
||
`ADR-0009`. **`secrets-engine` declares `unknown` → `fail_closed`.** Both maps are
|
||
total, published, test-pinned, and §6.4-conformant. Neither is a defect under v0.7.
|
||
They disagree about the one case that by construction is the one nobody planned for.
|
||
|
||
They are also **not comparable**: `ops-warden` scopes by security zone,
|
||
`secrets-engine` by catalog stage — explicitly as the "equivalent scope" §6.4 permits,
|
||
until zone membership arrives as a claim. So the register cannot answer *"what is the
|
||
estate's stance for a `z2`-protected workload"*, which is the question an inventory
|
||
exists to answer.
|
||
|
||
`access-engine` reported both as observations and explicitly **not** as a request that
|
||
either repository change its stance, on the ground that a PDP does not set a
|
||
consumer's stance — §9.3's two-owner split, which was its own finding, cutting against
|
||
its own convenience. That restraint is correct and it is why the question arrives here:
|
||
setting a consumer's stance is not the PDP's to do, but doctrine is Gate House's.
|
||
|
||
## Decision
|
||
|
||
### 1. `unknown` is not a zone, and it fails closed
|
||
|
||
**A stance map MUST resolve `unknown` to `fail_closed`.** It is not a scope over which
|
||
the §9.3 availability trade may be made.
|
||
|
||
The reasoning is a distinction v0.7 does not draw, and the two maps are the proof it
|
||
is needed. §9.3 sanctions trading availability for openness **per zone** — knowingly,
|
||
for a named scope, at a declared cost. `ops-warden`'s `fail_open` for `z0`–`z2` is
|
||
exactly that and remains sanctioned: it is a *known* request in a *degraded* system.
|
||
The consumer knows what it is being asked and cannot reach the engine to ask.
|
||
|
||
`unknown` is a different failure. It is not a degraded system; it is an
|
||
**unclassified subject**. The consumer does not know what it is being asked.
|
||
|
||
**A per-zone trade requires knowing the zone.** Where the scope is unknown, the trade
|
||
cannot have been made *for* it — there was no zone in hand when the profile was
|
||
written, so no one weighed this request's cost. Resolving `unknown` to the permissive
|
||
answer therefore does not extend a considered decision; it invents one, and it invents
|
||
the most permissive one available.
|
||
|
||
The adversarial reading settles it. `unknown` is the cheapest state for an attacker to
|
||
induce: an unregistered workload, a malformed label, a resource created before
|
||
classification, a race against registry propagation. `fail_open` on `unknown` makes
|
||
"be unclassifiable" a privilege escalation, and it is the one escalation that requires
|
||
no credential. This is the §8 asymmetry — nothing may manufacture privilege — applied
|
||
where the estate had left a gap open by symmetry with a rule that does not reach it.
|
||
|
||
`ops-warden`'s map is conformant today and this makes one cell of it non-conformant at
|
||
v0.8. That is a doctrine change with a cost to a repository that did everything asked
|
||
of it — published early, built the reference form, and offered it estate-wide — and it
|
||
should be argued in the assent round rather than imposed quietly. Their `z0`–`z2`
|
||
zone stances are untouched.
|
||
|
||
### 2. A stance map declares its scoping axis and its relation to zone
|
||
|
||
**Each published map MUST name the axis it scopes over, and state its relationship to
|
||
security zone: a mapping, or an explicit declaration that none exists yet and why.**
|
||
|
||
The register is not repaired by forcing everyone onto zones. `secrets-engine` chose
|
||
catalog stage because zone membership is not yet available to it as a claim; making it
|
||
assert zones now would produce a fiction, and a fiction in a runtime-read,
|
||
test-pinned file is worse than an honest incommensurability.
|
||
|
||
**The register instead states what it cannot answer.** §13.1 carries the axis per row
|
||
and records that cross-axis aggregation is not available. An inventory that silently
|
||
cannot answer its own question is the §9.1 defect again — a surface catalogued without
|
||
the capability behind it.
|
||
|
||
`access-engine` notes the convergence path and it is the right one: zone *membership*
|
||
compiles into the registry snapshot it already consumes, while per-zone *stance*
|
||
belongs to the consumer. If membership reaches the decision as a claim,
|
||
`secrets-engine`'s axis converges onto zones with neither side inventing a mapping.
|
||
Until then the axes are recorded, not reconciled.
|
||
|
||
The cost of two incommensurable axes is small; the cost of five is not. This lands
|
||
before the third row rather than after the fifth.
|
||
|
||
### What is not decided here
|
||
|
||
Neither repository is asked to change its zone stances. `secrets-engine`'s stale
|
||
reference to a durable `ActionAuthorization` record in its map — the artifact
|
||
`GH-DEC-2026-005` shelved — is theirs to correct and `access-engine` has already told
|
||
them; it is a naming error in a runtime-read file, where a stale name outlives a stale
|
||
comment, and it does not change the stance.
|
||
|
||
## Reversal
|
||
|
||
Reverse ruling 1 if a consumer demonstrates a scope that is genuinely unknown *and*
|
||
genuinely low-consequence, where failing closed on it costs availability with no
|
||
security gain — the plausible candidate is a read-only diagnostic path under §5.1. The
|
||
expected resolution is that such a path is not a protected side effect and therefore
|
||
has no PEP obligation at all, rather than that it needs a permissive `unknown`. If
|
||
that resolution fails on a real path, this ruling is too broad.
|
||
|
||
Reverse ruling 2 if declaring the axis proves to be all the register needed, in which
|
||
case the "states what it cannot answer" half is redundant rather than wrong.
|
||
|
||
---
|
||
|
||
### Amendment to GH-DEC-2026-008, 2026-09-06 — the comparison as written could never pass
|
||
|
||
Found by `secrets-engine` re-verifying a replay fixture within hours of the ruling,
|
||
and reported independently by `access-engine` and `approval-engine`. Verified here
|
||
against `flex-auth/schemas/decision_envelope.schema.json` and `pkg/api/canonical.go`
|
||
before amending.
|
||
|
||
**The defect.** The ruling required `claim.binding.pdp_digest` to equal the decision's
|
||
`NewDecisionBinding.request_digest`. `access-engine` hashes context into the request
|
||
digest, and the dual-control pattern carries the claim in `context.approval`.
|
||
Embedding the claim therefore changes the digest of the request that carries it, so a
|
||
digest recorded at issue can never equal the final one.
|
||
|
||
This is a **hash cycle**, and `approval-engine` put the resolution correctly: it is
|
||
forced rather than chosen. A claim cannot carry the digest of a document containing
|
||
that claim — the value would have to be known before it could be computed. The
|
||
recorded digest is necessarily of the underlying action *before* any claim was
|
||
embedded.
|
||
|
||
**The consequence had it shipped.** A consumer obeying the rule and failing closed
|
||
would have denied `destroy` permanently — failing closed forever on a check that
|
||
cannot pass. The ruling's own fail-closed requirement is what would have made it
|
||
harmful rather than merely wrong.
|
||
|
||
**The fix, and whose it was.** `access-engine` publishes
|
||
`binding.approval_binding_digest` — the same canonical material with
|
||
`context.approval` removed, emitted only where a claim was carried
|
||
(`FLEX-DEC-2026-007`). An approval issued against a claim-free `Check` records that
|
||
`Check`'s digest, and the claim-bearing request reproduces it. The comparison becomes
|
||
implementable without changing what it requires: correspondence by identity, no
|
||
translation, nobody asserting an equivalence between vocabularies. **The A5 property
|
||
holds exactly as drafted.**
|
||
|
||
`approval-engine` was right that what was missing was a published statement of which
|
||
fields the PDP excludes, and right not to make that statement on `access-engine`'s
|
||
behalf. **A consumer MUST NOT guess the exclusion rule**: a digest computed under an
|
||
assumed rule produces a confident wrong answer, and comparing two digests derived
|
||
under different rules fails open toward accepting a claim bound to a different
|
||
request — the same failure direction as the vocabulary mapping this decision rejected,
|
||
reached by another road. Until a PDP publishes its exclusion rule, the path is
|
||
correctly fail-closed rather than complete: a gap in a dependency, not in the ruling.
|
||
|
||
**What this changes in the record.** The ruling stands. Its four numbered obligations
|
||
stand. Obligation 1's comparison target is corrected from the request digest to the
|
||
PDP's published exclusion-scoped digest. v0.8 §9.7.3 carries the corrected form.
|
||
|
||
**And a general property, flagged by `access-engine` as a near miss it does not ask
|
||
for.** The obvious fix is to drop `context.approval` from the request digest
|
||
entirely. That is unsafe, and the reason generalises: **an evidence-bearing input may
|
||
be excluded from a correspondence digest but never from the replay identity.** Two
|
||
requests differing only in which approval was presented decide differently — one
|
||
allows, one denies `dual_control_required` — so collapsing them would let an allow
|
||
obtained with a valid claim be replayed against a request carrying none. A fail-open
|
||
hole reached by a refactor that looks like simplification. It is in v0.8 §6.4
|
||
obligation 5 because the shape recurs wherever evidence travels inside a hashed
|
||
request, and because a property discovered by nearly getting it wrong is worth more
|
||
written down than remembered.
|
||
|
||
**Implementation status.** `access-engine`: `FLEX-DEC-2026-007`, `dd3ce4c`.
|
||
`approval-engine`: schema v3 adds a declared `pdp_path`; `create()` refuses
|
||
`pdp_path: true` without a `pdp_digest`, so an approval that would be unusable on the
|
||
path fails **at issue** rather than at the protected side effect. Intent is declared,
|
||
never inferred — a digest that happens to be present is not a declaration anybody
|
||
made, legacy rows migrate to `false` rather than being back-filled, and a successor
|
||
inherits its predecessor's declaration. Back-filling would have manufactured a
|
||
statement no requester made, which is this decision's own objection to translation in
|
||
another form.
|
||
|
||
**What this says about the ruling process.** The ruling was right and its mechanism
|
||
was missing, and that combination is only survivable because the repositories
|
||
implementing it read it against their own fixtures rather than accepting it. The
|
||
defect was found in hours by the repository that would have been broken by it, and
|
||
reported by two others who had no obligation to look.
|
||
|
||
## GH-DEC-2026-010 — A PEP must be able to attribute a decision to access-engine; the digest test does not do it
|
||
|
||
```yaml
|
||
id: GH-DEC-2026-010
|
||
kind: decision
|
||
title: A PEP must be able to attribute a decision to access-engine; the digest test
|
||
does not do it
|
||
status: resolved
|
||
owner: Bernd Worsch
|
||
repo: gate-house
|
||
standard: net-kingdom/canon/standards/security-layer-model_v0.8.md
|
||
source_note: flex-auth FLEX-DEC-2026-010; approval-engine docs/reviews/2026-09-07-security-layer-model-v08.md
|
||
requested_dispositions:
|
||
- approved
|
||
- revised
|
||
- rejected
|
||
affects:
|
||
- gate-house
|
||
- net-kingdom
|
||
- access-engine
|
||
- approval-engine
|
||
- secrets-engine
|
||
- ops-warden
|
||
- ops-mason
|
||
- user-engine
|
||
- tenant-engine
|
||
- audit-core
|
||
decided_by: Bernd Worsch
|
||
rationale: 'Raised by access-engine against its own artifact during the v0.8 assent
|
||
round, having recorded it as its own defect (FLEX-DEC-2026-010) before reviewing
|
||
this text. Section 6.4 obligation 1 required a PEP to hold a decision from access-engine,
|
||
and obligation 2 supplied a test emphatic that it was mechanical rather than a matter
|
||
of implementer judgement. That test establishes which request a decision was rendered
|
||
for and nothing about who rendered it, and it cannot: every input to it is either
|
||
sent by the caller or published, so a responder knowing a published package id and
|
||
version reproduces all of it and returns a well-formed allow. Fail-closed protects
|
||
against a decision point that is absent, not against one that lies. The asymmetry
|
||
with obligation 5 is the sharp form, since section 9.4 requires the approval object
|
||
to have authenticated entries and nothing required it of the decision, leaving an
|
||
obligation written over a pair of artifacts satisfiable for only one half. The obligation
|
||
now requires attribution and states that the digest comparison does not discharge
|
||
it; the mechanism is the owner s under section 17 and the gap is declared in section
|
||
13 rather than papered by a rule the standard does not get to choose.'
|
||
created: '2026-09-09T18:11:20.071372Z'
|
||
updated: '2026-09-09T18:11:20.071372Z'
|
||
state_hub_decision_id: "046aa117-5b33-4fc2-b88c-313ad40f8e45"
|
||
```
|
||
|
||
## Context
|
||
|
||
`access-engine` returned its v0.8 review on 2026-09-07 and ranked this finding
|
||
blocking for calling §6.4's PEP obligations complete — explicitly **not** blocking
|
||
anyone's adoption, since the condition already exists under v0.7 and v0.8 does not
|
||
create it. It had already recorded the defect against its own artifact as
|
||
`FLEX-DEC-2026-010`, with `FLEX-WP-0024` carrying the fix, before it read this text.
|
||
|
||
The finding is that the standard is not merely silent. Obligation 1 says a PEP must
|
||
hold *"a decision from `access-engine` identifying the request it was rendered for"*,
|
||
and obligation 2 supplies a test and is emphatic that it is not a judgement call:
|
||
replay is permitted iff the canonical request digest matches and the lifetime holds,
|
||
*"mechanical, not a matter of implementer judgement"*. A careful implementer reads the
|
||
mechanical test as discharging the obligation above it. It does not.
|
||
|
||
Every input to that comparison is either sent by the caller or published by the PDP.
|
||
The request material is what the PEP transmitted; `policy_package_digest` and
|
||
`registry_snapshot_digest` are computable from files in a public repository. A
|
||
responder that knows the package id and version — both published — reproduces all
|
||
three and returns a well-formed allow. Publishing more digests makes a forged envelope
|
||
look **more** authenticated, not less.
|
||
|
||
`secrets-engine` put the consequence better than either of us: its posture *"silently
|
||
assumes the PDP is the PDP"*. `approval-engine` carried it into the correspondence
|
||
chain — an approval whose `pdp_digest` matches a **forged** decision matches perfectly.
|
||
Identity comparison proves two artifacts describe one request. It proves nothing about
|
||
whether the decision is authentic.
|
||
|
||
**The gap was visible from inside and was recorded against the wrong artifact.** §16
|
||
already carried *"publication integrity of the Taxonomy layer itself… while its own
|
||
publication path has no digest, freeze, or rollback discipline"*. That is this
|
||
observation one layer up. Applied to the artifact the standard regulates rather than to
|
||
the standard, it is this decision.
|
||
|
||
## Decision
|
||
|
||
### 1. Attribution is required, and is a separate property from identity
|
||
|
||
**A PEP MUST hold a decision that is *attributable to* `access-engine`, not merely
|
||
present and well-formed.** Obligation 2's digest test establishes **which request** a
|
||
decision was rendered for. It establishes nothing about **who rendered it**.
|
||
|
||
**Obligation 1 is not discharged by a digest comparison**, and an implementer MUST NOT
|
||
read obligation 2's mechanical test as discharging it. This is the whole of the change:
|
||
the obligation stops reading as satisfied by a check that does not satisfy it.
|
||
|
||
### 2. Fail-closed does not reach this
|
||
|
||
§9.3's apparatus — two failure cases, two owners, published stance maps — addresses
|
||
**absence**. An unreachable PDP denies. A responder impersonating one allows. Nothing
|
||
in the standard addressed a responder, and no stance value can: the stance is consulted
|
||
when the engine is unreachable, and a lying responder is reachable by construction.
|
||
|
||
### 3. Obligation 5 was written over a pair it could only half satisfy
|
||
|
||
§9.4 requires the approval object to have *"durable, authenticated entries"*. §6.4
|
||
obligation 5 then requires that where a PEP's decision rests on more than one artifact,
|
||
each MUST be validated against the layer that owns its data — and names the
|
||
approval-claim / `DecisionEnvelope` pair as the live instance.
|
||
|
||
So the standard required authenticity of the **PIP's** artifact and not of the
|
||
**PDP's**, and wrote an obligation across both. That asymmetry was argued nowhere. It
|
||
is oversight, not position, and it is corrected here rather than defended.
|
||
|
||
### 4. The mechanism is not the standard's to choose
|
||
|
||
`access-engine` did not ask for a signature scheme in the statute, and it does not get
|
||
one. The shape is the owner's under §17. What the standard names is the property.
|
||
|
||
Until a mechanism ships, the condition is a **declared §13 gap** with `access-engine`
|
||
as owner (`FLEX-DEC-2026-010`, `FLEX-WP-0024`; `FLEX-DEC-2026-009` puts the
|
||
authenticated caller in provenance, so a record eventually attests both ends of the
|
||
channel rather than neither). A declared gap is honest. An unstated assumption inside a
|
||
test called mechanical is not.
|
||
|
||
## What this does not do
|
||
|
||
It does not close the gap. `access-engine`'s note that Glas completed operator
|
||
caller-auth mid-review is correctly self-limited: an authenticated API-server
|
||
port-forward authenticates the responder for one operator path and produces no signed
|
||
portable artifact.
|
||
|
||
It also does not create an adoption blocker. The condition predates v0.8 and every
|
||
PEP-shaped consumer is in it today. What changes is that they can now see they are.
|
||
|
||
## Reversal condition
|
||
|
||
If a PEP can verify the decision's origin by a mechanism that does not itself rest on
|
||
material the caller supplies or the PDP publishes openly, the §13 gap closes and this
|
||
record's second half becomes historical. The first half — that identity is not
|
||
attribution — does not revert; it is a property of hashing, not of the current channel.
|
||
|
||
## Provenance
|
||
|
||
Raised by `access-engine` (`FLEX-DEC-2026-011`, v0.8 assent, F1), against its own
|
||
artifact and having recorded the defect on its own side first. Independently framed by
|
||
`secrets-engine`. Carried into the correspondence chain by `approval-engine`, which
|
||
noted its own `docs/approval-claim.md` said what `pdp_digest` cannot cover without
|
||
saying it cannot detect a forged decision. Applied to
|
||
`security-layer-model_v0.8.md` §6.4 obligation 1 and §13 at `net-kingdom@64394e9`.
|
||
|
||
## GH-DEC-2026-011 — Classification coverage is published beside the stance; a transitional fail_open is declined
|
||
|
||
```yaml
|
||
id: GH-DEC-2026-011
|
||
kind: decision
|
||
title: Classification coverage is published beside the stance; a transitional fail_open
|
||
is declined
|
||
status: resolved
|
||
owner: Bernd Worsch
|
||
repo: gate-house
|
||
standard: net-kingdom/canon/standards/security-layer-model_v0.8.md
|
||
source_note: ops-warden history/2026-09-09-layer-model-v08-review.md @ 8e1b621; flex-auth
|
||
FLEX-DEC-2026-011 F2
|
||
requested_dispositions:
|
||
- approved
|
||
- revised
|
||
- rejected
|
||
affects:
|
||
- gate-house
|
||
- net-kingdom
|
||
- ops-warden
|
||
- ops-mason
|
||
- secrets-engine
|
||
- user-engine
|
||
- tenant-engine
|
||
- access-engine
|
||
- zone-engine
|
||
decided_by: Bernd Worsch
|
||
rationale: 'ops-warden assented to GH-DEC-2026-009 on its own terms and then reported
|
||
what adopting it costs: zero of three signing targets and three of twenty-one routing
|
||
lanes resolve to a zone, so unknown fail_closed adopted today would fail closed
|
||
on essentially every certificate it issues whenever access-engine is unreachable,
|
||
including the continuity path an operator needs to repair that unreachability. It
|
||
asked for a dated transitional unknown fail_open converting on coverage rather than
|
||
calendar. Declined: a sanctioned transitional fail_open is indistinguishable at
|
||
runtime from the stance the rule forbids and would make the rule optional at the
|
||
only moment it costs anything, while section 11 s declared-gap mark already expresses
|
||
a correct rule whose adoption is not yet affordable. Its second preference is adopted
|
||
instead. Section 13.1 records a dated classification-coverage figure beside each
|
||
stance, because unknown fail_closed over an entirely unclassified population is
|
||
conformant and materially misleading. access-engine reached the same place from
|
||
the other side: totality satisfied by a catch-all is satisfied vacuously, so a map
|
||
must enumerate its axis and an absent scope must be distinguishable in the record
|
||
from an unknown one and surface as a conformance failure.'
|
||
created: '2026-09-09T18:12:02.553370Z'
|
||
updated: '2026-09-09T18:12:02.553370Z'
|
||
state_hub_decision_id: "6777f9d1-566d-4bc7-815e-e08582b8719a"
|
||
```
|
||
|
||
## Context
|
||
|
||
`GH-DEC-2026-009` made `unknown` → `fail_closed` normative and made one cell of
|
||
`ops-warden`'s published stance map non-conformant. That ruling was circulated with
|
||
v0.8 rather than imposed, precisely because the repository bearing its cost had not
|
||
reviewed it. `ops-warden` returned the review on 2026-09-09
|
||
(`history/2026-09-09-layer-model-v08-review.md`, `8e1b621`).
|
||
|
||
**It assented to the rule, on the falsifier's own terms.** The reversal clause
|
||
anticipated that such a path would be a §5.1 read-only diagnostic carrying no PEP
|
||
obligation. `ops-warden` went looking for that escape hatch on its own side and
|
||
reported that it does not have one: the stance map governs exactly one protected
|
||
action, `warden sign`, which is a credential-issuing side effect that §5.1 does not
|
||
reach. The predicted defence is absent and the argument stands unrebutted.
|
||
|
||
**Then it priced the adoption.** Measured 2026-09-09:
|
||
|
||
```text
|
||
signing targets (actor resources in snapshot): 0 resolved, 3 unknown, 1 n/a
|
||
routing catalog lanes: 3 resolved, 18 unknown, 12 n/a
|
||
```
|
||
|
||
Zero of its signing targets resolve to a zone. So `unknown` → `fail_closed` adopted
|
||
today does not fail closed on an edge case; it fails closed on essentially every
|
||
certificate `ops-warden` issues, whenever `access-engine` is unreachable. That is the
|
||
configuration `ADR-0006` rejected, reached by another route: one value making the
|
||
decision engine a uniform dependency of every signing path, **including the continuity
|
||
path needed to repair that dependency**. Engine down → operator needs an SSH
|
||
certificate to reach the host and restore it → the repair target is unknown because
|
||
nobody classified it → denied.
|
||
|
||
Eighteen of its eighteen unknown lanes are unknown because **another** repository has
|
||
not published a workload-identity declaration, and `ADR-0009` rule 3 forbids closing
|
||
that by inference.
|
||
|
||
`access-engine` arrived at the same register from the opposite side (`FLEX-DEC-2026-011`,
|
||
F2): a map carrying `unknown` → `fail_closed` satisfies obligation 3's totality
|
||
requirement **vacuously**. Every scope the author never enumerated lands in `unknown`,
|
||
fails closed, and nobody ever learns which those were. It recognised the shape because
|
||
it published it — `FLEX-DEC-2026-008`, a policy package shipped with no tenant rule at
|
||
all while 29 fixtures passed, because every fixture carried the same tenant.
|
||
|
||
## Decision
|
||
|
||
### 1. A dated transitional `unknown: fail_open` is declined
|
||
|
||
`ops-warden`'s first preference was that obligation 3 name a transition: a consumer may
|
||
declare `unknown: fail_open` as a dated, published transitional state with a coverage
|
||
figure attached, converting on coverage rather than on calendar.
|
||
|
||
**Declined.** A sanctioned transitional `fail_open` is indistinguishable at runtime from
|
||
the stance the rule forbids. It makes the rule optional at the moment of adoption — the
|
||
only moment it costs anything — and `unknown` is the cheapest state for an attacker to
|
||
induce whether or not the consumer has dated its intention to stop being permissive.
|
||
|
||
The mechanism asked for already exists and does not have that defect: §11's
|
||
**declared-gap** mark says *"correct rule, adoption not yet affordable"* without
|
||
inverting the rule's effect. `ops-warden` proposed exactly that as its own second
|
||
preference and named `WARDEN-WP-0040` as the route.
|
||
|
||
The deadlock is real and the answer is classification, not a permissive axis: continuity
|
||
paths are classified into a scope whose stance is open, and the deadlock does not arise.
|
||
`ops-warden` accepted that answer and correctly observed that it is work not yet done.
|
||
Work not yet done is a gap. It is not a reason to hold the axis open.
|
||
|
||
**And a stricter stance is not a licence to manufacture the membership that makes it
|
||
survivable.** `ADR-0009` rule 3 is right and must hold under pressure from this ruling.
|
||
A consumer MUST NOT infer a scope another repository has not declared.
|
||
|
||
### 2. Classification coverage is published beside the stance
|
||
|
||
**§13.1 records a dated classification-coverage figure for each consumer.** A row
|
||
reading `unknown` → `fail_closed` while 100% of that consumer's targets are unknown is
|
||
conformant and materially misleading: a reader cannot distinguish a strict consumer from
|
||
an unclassified one. It is §11's published-map-equals-shipped-behaviour rule one level
|
||
up — the map becomes accurate about itself and inaccurate about its effect.
|
||
|
||
Coverage is **disclosure, not a licence**. It does not soften the stance, gate it, or
|
||
create a state in which a non-conformant cell becomes conformant. It reports.
|
||
|
||
Coverage is reported by the consumer and never computed here. A blank means *"not
|
||
reported"*, never *"complete"*.
|
||
|
||
### 3. A map must enumerate its axis; `absent` is not `unknown`
|
||
|
||
**Totality MUST NOT be satisfied by a catch-all.** A published map enumerates the axis
|
||
it scopes over. Otherwise obligation 3's totality requirement is unfalsifiable and its
|
||
drift test passes by exercising the default rather than the axis.
|
||
|
||
**`unknown` and `absent` are one runtime behaviour and two meanings.** Both fail closed
|
||
— `absent` for the stronger reason, since it is the branch reached by discovering that
|
||
the author's model of their own axis was wrong, and §8's asymmetry forbids being more
|
||
permissive on surprise. But an `unknown` hit is normal operation under a considered
|
||
stance, while an `absent` hit is evidence this obligation is violated. **An `absent` hit
|
||
MUST be distinguishable in the record and MUST surface as a conformance failure** rather
|
||
than be absorbed by the catch-all.
|
||
|
||
This closes the question §16 opened at the v0.8 cut.
|
||
|
||
### 4. `ops-mason` is marked
|
||
|
||
An unpublished stance map is a plainer violation of obligation 3 than a wrongly-valued
|
||
cell in a published one, and §13.1 stated it as bare fact while bolding the other. A
|
||
reader scanning for marks found one row and concluded the other four were fine. That is
|
||
Gate House's own §11 marking obligation applied to Gate House's own register, returned
|
||
unchanged by `access-engine` after receiving the same argument about a stale row of its
|
||
own. `ops-mason`'s row is now marked, with no route recorded.
|
||
|
||
## Reversal condition
|
||
|
||
If a consumer demonstrates a scope that is genuinely unclassifiable, genuinely
|
||
low-consequence, and whose permissive resolution cannot be induced by an unprivileged
|
||
party, §1 is open to revision — that falsifier is `GH-DEC-2026-009`'s and survives here.
|
||
`ops-warden` went looking for it and reported its absence, which is evidence and not
|
||
proof.
|
||
|
||
§2 reverses if coverage figures start being read as a stance qualifier rather than as
|
||
disclosure. If a row is ever argued to be conformant *because* its coverage is low, the
|
||
column is doing harm and comes out.
|
||
|
||
## Provenance
|
||
|
||
Raised by `ops-warden` (v0.8 assent, two findings and one agreement), which bears the
|
||
entire cost of the ruling it assented to and asked for the transition it did not get.
|
||
Its measured figures are the first entries in the coverage column. §3 raised by
|
||
`access-engine` (`FLEX-DEC-2026-011`, F2); §4 by `access-engine` (F4). Applied to
|
||
`security-layer-model_v0.8.md` §6.4 obligation 3 and §13.1 at `net-kingdom@64394e9`.
|
||
|
||
`access-engine` added, unprompted, that its concurrence with `GH-DEC-2026-009` cost it
|
||
nothing — it is not PEP-shaped and publishes no stance map — and that a PDP's assent to
|
||
a ruling falling entirely on other repositories is weak evidence for it. That is
|
||
recorded because it is right: the strength here is the asymmetry argument and
|
||
`ops-warden`'s assent against its own interest, not the count of repositories agreeing.
|
||
|
||
## GH-DEC-2026-012 — informed-decision is PEP-shaped and may emit a presentation claim; view_hash and the binding digest are distinct and non-substitutable
|
||
|
||
```yaml
|
||
id: GH-DEC-2026-012
|
||
kind: decision
|
||
title: informed-decision is PEP-shaped and may emit a presentation claim; view_hash
|
||
and the binding digest are distinct and non-substitutable
|
||
status: resolved
|
||
owner: Bernd Worsch
|
||
repo: gate-house
|
||
standard: net-kingdom/canon/standards/security-layer-model_v0.8.md
|
||
source_note: informed-decision/docs/gate-house-decision-request-layer-placement.md
|
||
(INFD-IN-0001; hub intake 01a08610)
|
||
requested_dispositions:
|
||
- approved
|
||
- revised
|
||
- rejected
|
||
affects:
|
||
- gate-house
|
||
- informed-decision
|
||
- approval-engine
|
||
- access-engine
|
||
- audit-core
|
||
- key-cape
|
||
- net-kingdom
|
||
decided_by: Bernd Worsch
|
||
rationale: 'Three rulings requested before any architecture was written. R1: informed-decision
|
||
is PEP-shaped under section 6.4 and owes companion section 5, because it causes
|
||
a protected side effect on the far side of a decision. It is not an Engine and holds
|
||
no state another layer reads at runtime for a verdict. R2: a component may be PEP-shaped
|
||
and also emit a claim; PEP and PIP are shapes a repository has, not layers it occupies,
|
||
and the catalog rows are layers. The presentation claim is permitted as a claim
|
||
about presentation only, subject to three limits: it must not carry, restate or
|
||
imply the decision or the approval verdict, it must not be an input to the decision
|
||
it presents for, and its evidence copy reaches audit-core independently of the emitter.
|
||
R3: candidate (b). view_hash and the approval binding digest are distinct attestations
|
||
with an explicit authority rule. (a) is refused because merging them would make
|
||
one repository the authority on what another layer computes over a request, which
|
||
is the objection GH-DEC-2026-008 made to translation. (c) is refused because nesting
|
||
the binding digest inside view_hash creates an ordering dependency and reproduces
|
||
the hash-cycle shape that made GH-DEC-2026-008 unimplementable. The binding digest
|
||
is authoritative for what the request is; view_hash is authoritative only for what
|
||
was shown; neither may be substituted for the other, and a disagreement between
|
||
them is a finding against the presenting surface rather than a fact about the request.'
|
||
created: '2026-09-09T20:09:49.402289Z'
|
||
updated: '2026-09-09T20:09:49.402289Z'
|
||
state_hub_decision_id: "8c3c0d14-91ca-43fd-bef3-3bd01ff4213f"
|
||
```
|
||
|
||
## Context
|
||
|
||
`informed-decision` is a new repository claiming the browser-facing approver UI that
|
||
`approval-engine` names under its own Non-Goals. It filed `INFD-IN-0001` **before
|
||
writing any architecture**, asking for three rulings and stating what it would do on
|
||
each possible answer, including the answers it did not want. That order — ruling first,
|
||
architecture second — is what §17 exists to produce and it is worth recording as the
|
||
reason this was answered inside a day.
|
||
|
||
It also argued the self-dealing objection against itself in §3 of its request, and asked
|
||
that no ruling credit it with closing the residual it named. That request is honoured
|
||
below.
|
||
|
||
## Decision
|
||
|
||
### R1 — `informed-decision` is PEP-shaped, and is not an Engine
|
||
|
||
**Confirmed as proposed.** It causes a protected side effect on the far side of a
|
||
decision: it submits an approval entry carrying an authenticated human's identity. §6.4
|
||
applies in full, and companion §5 is owed.
|
||
|
||
It is **not** an Engine and takes no catalog row as one. It holds no state another layer
|
||
reads at runtime to reach a verdict, which is the test, and it renders no decision.
|
||
|
||
Being PEP-shaped does not move a repository out of its layer (§6.4). Its layer is its
|
||
own to declare in its own voice; this ruling settles its *shape*, which is what was
|
||
asked and what was blocking.
|
||
|
||
Consequences it should expect, stated so they are not discovered later:
|
||
|
||
- A published unreachable-engine stance map at a path named in its layer declaration,
|
||
and a row in statute §13.1. Build to **v0.8's** obligation 3 rather than v0.7's:
|
||
`unknown` MUST resolve to `fail_closed`, the map MUST enumerate its axis rather than
|
||
lean on a catch-all, an `absent` scope MUST be distinguishable in the record from an
|
||
`unknown` one, and the test asserting published-equals-shipped is a `MUST`. Publish a
|
||
classification-coverage figure beside the stance (`GH-DEC-2026-011`).
|
||
- §6.4 obligation 1 in its v0.8 form, including the attribution property it cannot
|
||
satisfy today (`GH-DEC-2026-010`). It inherits a declared gap, not a clean path, and
|
||
its own documents must say so rather than describing validation as complete.
|
||
|
||
### R2 — Yes, and it is not a second catalog row
|
||
|
||
**A component may be PEP-shaped and also emit a claim.** PEP and PIP are *shapes* a
|
||
repository has; the §4 catalog records *layers* a repository occupies. Nothing in the
|
||
statute forbids one component having two shapes, and the rule that looks like it does —
|
||
§6.4 obligation 5's *"a PIP MUST NOT republish the PDP's decision"* — is a prohibition on
|
||
**republishing a decision**, not on holding two shapes. `informed-decision` proposed
|
||
exactly this reading and it is right.
|
||
|
||
Three limits, and they are the substance of the permission:
|
||
|
||
1. **The presentation claim carries presentation and nothing else.** It MUST NOT carry,
|
||
restate, summarise, or imply the decision, the approval verdict, or whether the act
|
||
was permitted. A consumer must not be able to learn from this claim anything about
|
||
what was *decided* — only about what was *shown*. This is obligation 5 applied at
|
||
issue rather than at consumption.
|
||
2. **It MUST NOT be an input to the decision it presents for.** Presentation is evidence
|
||
about conduct, never a source of authority. A policy that reads `view_hash` to decide
|
||
whether an act is permitted would let the presenting surface contribute to its own
|
||
authorization, which is the estate's oldest rule in a new place: cognition proposes,
|
||
authority disposes.
|
||
3. **The evidence copy reaches `audit-core` independently of the emitter.** The claim
|
||
endpoint and the evidence path are different things and one does not substitute for
|
||
the other. Audit evidence is protected from the actor being audited, and here the
|
||
actor and the source are the same component — so the copy that is evidence must not
|
||
be reachable only through the party it is evidence about.
|
||
|
||
**The §17 yield is accepted as offered.** Whatever shape `informed-decision` publishes
|
||
yields to the shared request-claim schema when that schema has an owner, which it does
|
||
not yet (§17 records request-claim and gap-record as the two unsettled artifacts). The
|
||
position matches `approval-engine`'s in `APPROVAL-IN-0001` and is the right one: a local
|
||
invention declared temporary is tolerable, a local invention declared permanent is the
|
||
drift §17 exists to prevent.
|
||
|
||
### R3 — Candidate (b): distinct attestations, with the authority rule stated
|
||
|
||
**`view_hash` and `approval-engine`'s binding digest are two attestations over one act,
|
||
and neither may be substituted for the other.**
|
||
|
||
- **The binding digest is authoritative for what the request *is*.** Replay identity,
|
||
correspondence with the decision, and everything §6.4 obligation 5 governs.
|
||
- **`view_hash` is authoritative only for what was *shown*.** It attests presentation:
|
||
brief, packet, highlights, locale, UI release, and the binding material as rendered.
|
||
- **A disagreement between them is a finding against the presenting surface, never a
|
||
fact about the request.** This is the rule that keeps the two from becoming rival
|
||
answers to one question, and it is the thing `informed-decision` asked us to prevent.
|
||
|
||
**(a) is refused, and for a doctrinal reason rather than the cost given.** One digest
|
||
means one canonicalization authoritative for both, and the material is owned by
|
||
different layers: what a request *is* is computed by the decision path, what was *shown*
|
||
is known only to the presenting surface. Merging them makes one repository the authority
|
||
on what another layer computes over a request — a third authority on what a request is,
|
||
which is precisely the objection `GH-DEC-2026-008` made to cross-engine translation. The
|
||
cost `informed-decision` names is real and secondary: the binding digest must exist where
|
||
no human is in the loop, and `view_hash` cannot exist there at all.
|
||
|
||
**(c) is refused for two reasons, the second the stronger.** First, the ordering
|
||
dependency it admits: an approval would have to exist before a memo could be rendered,
|
||
which inverts the flow this repository exists to serve. Second, and this is why we will
|
||
not take it even if the ordering were acceptable — nesting one hash inside the other
|
||
reproduces the shape that made `GH-DEC-2026-008` unimplementable. That ruling required a
|
||
claim to name the digest of a request that would come to contain the claim: a hash cycle,
|
||
found by `secrets-engine` and reported independently by two engines within hours. If
|
||
`view_hash` ever travels inside hashed request material while containing the binding
|
||
digest, the same cycle reappears with the same fail-closed consequence. We have paid for
|
||
that lesson once this quarter and are not buying it a second time.
|
||
|
||
**How the two are linked, since they are no longer linked by nesting.** By
|
||
**co-reference, not by translation**: the presentation record carries the approval or
|
||
binding identifier explicitly, and both attestations are read against that one reference.
|
||
`informed-decision` MUST NOT recompute or restate `approval-engine`'s binding digest from
|
||
its own vocabulary — it references the digest the other layer computed and recorded. That
|
||
is `GH-DEC-2026-008`'s identity-not-translation rule applied here, and it is why the two
|
||
canonicalizations covering overlapping material is not a defect: they answer different
|
||
questions and are compared to nothing.
|
||
|
||
## The residual, not closed and not credited
|
||
|
||
`informed-decision` stated it against itself and asked not to be credited with closing
|
||
it. **It is not closed, and this ruling does not close it.** A compromised surface can
|
||
present X and attest Y, because `view_hash` is computed by the component that renders.
|
||
|
||
That is structurally the same residual `approval-engine` names for adversarial omission
|
||
at a compromised source, and it takes the same disposition: attestation and atomicity
|
||
cover accident and later tampering, never a compromised source. It belongs in the same
|
||
register as §6.4 obligation 5's summary-predicate residual and §9.6's — detection, not
|
||
prevention.
|
||
|
||
The §3 argument that this is *not* the failure that kept the approval object out of
|
||
`access-engine` is accepted. An evaluator owning what it evaluates can grade its own
|
||
inputs; a renderer attesting its own rendering is the only party able to produce the
|
||
record at all. The distinction holds — but limit 2 above is what keeps it holding, and
|
||
without it the two collapse.
|
||
|
||
## Reversal condition
|
||
|
||
R2's permission reverses if a presentation claim is ever found carrying decision
|
||
content, or if a policy is found reading `view_hash` as an authorization input. Either
|
||
would mean the shape cannot be held safely by one component and the claim moves to
|
||
`audit-core` as the request's own R2-refused branch describes.
|
||
|
||
R3 §(b) reverses if the shared request-claim schema (§17) arrives with a presentation
|
||
attestation already in it, in which case the local shape yields as offered.
|
||
|
||
## Provenance
|
||
|
||
Requested by `informed-decision` (`INFD-IN-0001`, hub intake `01a08610`) before writing
|
||
its architecture, with three candidate rulings, their costs, the self-dealing objection
|
||
argued against itself, and an explicit list of what it was *not* asking for. Unblocks
|
||
`INFD-WP-0001` T05 and T07, and `KEY-WP-0013-T02`.
|
||
|
||
## GH-DEC-2026-013 — A client registration may supply a human principal's tenant only as a declared bounded gap, and the claim must carry its provenance
|
||
|
||
```yaml
|
||
id: GH-DEC-2026-013
|
||
kind: decision
|
||
title: A client registration may supply a human principal's tenant only as a declared
|
||
bounded gap, and the claim must carry its provenance
|
||
status: resolved
|
||
owner: Bernd Worsch
|
||
repo: gate-house
|
||
standard: net-kingdom/canon/standards/security-layer-model_v0.8.md
|
||
source_note: key-cape/docs/tenant-claim-contract.md and src/internal/server/oidc/token.go
|
||
humanTenant (KEY-WP-0013-T05); relayed by informed-decision
|
||
requested_dispositions:
|
||
- approved
|
||
- revised
|
||
- rejected
|
||
affects:
|
||
- gate-house
|
||
- key-cape
|
||
- approval-engine
|
||
- access-engine
|
||
- user-engine
|
||
- informed-decision
|
||
- net-kingdom
|
||
- ops-warden
|
||
decided_by: Bernd Worsch
|
||
rationale: 'key-cape has no adapter populating the directory user tenant, so every
|
||
human token falls back to the default tenant and approval-engine refuses it by exact
|
||
match. Two resolutions were possible: source the tenant from the directory, or let
|
||
a client registration bind it. Directory-sourced is the terminal state, because
|
||
a tenant is a property of the principal and a registration is a statement about
|
||
the actor; sourcing a principal attribute from the actor collapses two identities
|
||
the estate keeps distinct, and supplying a tenant the directory has not assigned
|
||
widens rather than attenuates. The registration-bound shape is nonetheless admissible
|
||
as a declared bounded gap rather than refused, because its distinguishing case fails
|
||
closed: where the registration and the directory disagree, issuance is refused rather
|
||
than resolved in either direction. That is the general property this record states
|
||
— a transitional shape is admissible where it fails closed on exactly the case that
|
||
distinguishes it from the correct resolution, and inadmissible where it fails open
|
||
there, which is why GH-DEC-2026-011 declined ops-warden a transitional fail_open
|
||
and this record grants key-cape its transitional binding. Three conditions attach:
|
||
refusal on disagreement is normative rather than a design choice, the exclusion
|
||
of dynamic client registration is a normative precondition whose lifting voids the
|
||
rule rather than reopening it, and the gap is registered with an owner for the directory
|
||
adapter. One finding is added that was not asked for: the tenant claim is emitted
|
||
as a bare string, so a consumer cannot tell a tenant the directory asserted about
|
||
the person from one a registration supplied about the client, and a consumer relying
|
||
on it for the first is relying on the second. The claim MUST carry its provenance.'
|
||
created: '2026-09-09T21:18:11.679012Z'
|
||
updated: '2026-09-09T21:18:11.679012Z'
|
||
state_hub_decision_id: "fcc0c595-7081-404e-b2aa-1536e91a371c"
|
||
```
|
||
|
||
## Context
|
||
|
||
`key-cape` binds the approval chain to the landlord zone (`tenant:platform`), and
|
||
`approval-engine` compares the tenant claim by exact string equality. **No adapter
|
||
populates `domain.User.Tenant`**, so every human token fell back to `tenant:coulomb` and
|
||
an approver token would have been refused downstream — presenting as a *failed approval*
|
||
rather than as a *registration defect*, which is the failure mode that makes this worth
|
||
ruling on rather than leaving to implementation.
|
||
|
||
Two resolutions were available: source the tenant from the directory, or let a client
|
||
registration declare it. `key-cape` declined to choose unilaterally on the ground that
|
||
the second writes a cross-tenant capability into the issuer, which reads as doctrine
|
||
rather than implementation. That reading is correct and the question is Gate House's:
|
||
it is about what may be a source of a principal's identity, which is a Core Rule, not a
|
||
runbook.
|
||
|
||
It reached us relayed by `informed-decision`, which was asking about its own client
|
||
registration and passed on a question that was not its own. `key-cape` has already
|
||
built the registration-bound shape, and built it carefully — this record rules on a
|
||
design that exists rather than on a proposal.
|
||
|
||
## Decision
|
||
|
||
### 1. Directory-sourced is the terminal state
|
||
|
||
**A principal's tenant is sourced from the identity layer's record of the principal.**
|
||
A client registration is a statement about the **actor** — the client the person
|
||
authenticated through. A tenant is a property of the **principal**. Sourcing the second
|
||
from the first collapses two of the three identities the estate keeps distinct
|
||
(principal, actor, runtime identity), and it collapses them in the direction that
|
||
widens: a registration supplying a tenant the directory has not assigned gives the
|
||
principal an attribute the identity layer never asserted about them. **Delegation
|
||
attenuates and never widens.**
|
||
|
||
The condition being routed around is a missing adapter, not an absent owner. A missing
|
||
implementation in the identity layer is fixed in the identity layer.
|
||
|
||
### 2. The registration-bound shape is admissible as a declared, bounded gap
|
||
|
||
**Not refused, and not blessed as permanent.** `key-cape` may ship what it has built,
|
||
under §3's conditions and registered as a gap.
|
||
|
||
The reason it is admissible rather than forbidden is precise, and is not that the design
|
||
is careful. It is that **the case distinguishing it from the correct resolution fails
|
||
closed.** Where the registration declares a zone and the directory has placed the user
|
||
in a different one, `humanTenant` refuses to issue rather than picking a winner. Either
|
||
answer would be a silent cross-tenant assertion, and the design says so. A registration
|
||
can bind a zone for a user the directory has not placed; it can never relabel one it has.
|
||
|
||
`key-cape`'s further observation is what makes the gap bounded rather than open-ended:
|
||
the same code turns from *supplying* the zone into *enforcing agreement with* it the
|
||
moment the directory carries tenants — no second migration, and no window in which a
|
||
stale registration silently wins. A transitional shape that converges on the terminal
|
||
state by subtraction is a different object from one that will have to be unwound.
|
||
|
||
### 3. The general property, which is why this is granted and `GH-DEC-2026-011` was declined
|
||
|
||
**A transitional shape is admissible where it fails closed on exactly the case that
|
||
distinguishes it from the correct resolution, and inadmissible where it fails open
|
||
there.**
|
||
|
||
This is §8's asymmetry applied to transitions, and it is the rule that keeps these two
|
||
decisions consistent rather than merely both defensible:
|
||
|
||
- `ops-warden` asked for a dated transitional `unknown: fail_open` (`GH-DEC-2026-011`).
|
||
**Declined.** The distinguishing case — an unclassified subject reaching an unreachable
|
||
engine — is exactly where the transitional shape fails **open**. It is behaviourally
|
||
identical to the violation for the whole of its life, so the transition licenses the
|
||
thing the rule forbids and merely dates it.
|
||
- `key-cape` binds a tenant at registration. **Granted.** The distinguishing case —
|
||
registration and directory disagreeing — fails **closed**. The transitional shape and
|
||
the terminal state differ only where nobody is served either way.
|
||
|
||
A transition is a promise about the future. **A promise that fails open is a permission;
|
||
a promise that fails closed is a gap.** Only the second is a thing a register can hold.
|
||
|
||
### 4. Three conditions
|
||
|
||
1. **Refusal on disagreement is normative.** Row four of `key-cape`'s table is not a
|
||
design choice it may later optimise. A registration MUST NOT relabel a
|
||
directory-placed principal, and MUST NOT resolve the conflict in either direction.
|
||
Any future change that picks a winner — in either direction, including preferring the
|
||
directory — voids this permission, because it converts a refusal into a silent
|
||
cross-tenant assertion.
|
||
2. **The exclusion of dynamic client registration is a normative precondition, and
|
||
lifting it VOIDS this rule rather than reopening it.** `key-cape` writes that a
|
||
self-service client able to name its users' tenant would be a straightforward
|
||
escalation, and that the rule "must be revisited" if the exclusion is lifted. That is
|
||
too weak and we are strengthening it: revisiting implies the answer might survive
|
||
review. It does not. If dynamic client registration is ever admitted, the
|
||
registration-bound shape is void on that day and the directory becomes the only
|
||
source, whatever state the adapter is in. Stated that way so that lifting the
|
||
exclusion cannot be a small change made by someone who never reads this record.
|
||
3. **Registered as a declared gap under §13**, with an intended owner for the directory
|
||
adapter, so it cannot quietly become the permanent answer by nobody minding it. Per
|
||
§13, an intended owner is a proposal **to** a repository, not an assignment **onto**
|
||
it; this record does not name one.
|
||
|
||
### 5. The finding that was not asked for: the claim must carry its provenance
|
||
|
||
`key-cape` emits `tenant` as a bare string. A consumer cannot tell a tenant **the
|
||
directory asserted about the person** from a tenant **a registration supplied about the
|
||
client they came through**. `approval-engine` admits an approver by exact-matching that
|
||
string, and it is therefore relying on the second while its contract reads as though it
|
||
relies on the first.
|
||
|
||
**This is the shape of `GH-DEC-2026-010`, one layer down.** There, a PEP held a decision
|
||
and could establish which request it was for but not who issued it, while a mechanical
|
||
test read as though it discharged both. Here a consumer holds a tenant and can establish
|
||
that the string matches, but not whether the identity layer ever said it about this
|
||
person. In both cases the check is sound and the property a reader infers from it is not
|
||
present.
|
||
|
||
**The `tenant` claim MUST carry its provenance** — whether the value was asserted by the
|
||
directory about the principal, or supplied by the registration — and a consumer MUST NOT
|
||
treat the two as equivalent for any decision that turns on a fact about the *person*.
|
||
|
||
**Amended 2026-09-10 — `key-cape` emitted three values where this record named two, and
|
||
it was right to.** Its third is `default`: nobody asserted a zone and the profile's
|
||
non-empty fallback supplied one. Folding that into `directory` would have reproduced this
|
||
very finding one level down, since a consumer would read an assertion the identity layer
|
||
never made. `key-cape` applied A-16 to a route this record did not examine, and asked to
|
||
be corrected rather than assume — the correction is that there was nothing to correct.
|
||
|
||
Its second judgement is also endorsed: where registration and directory **agree**, the
|
||
value resolves to `directory`, because the directory did assert it about the person and
|
||
reporting the weaker source would understate what is known. That also makes the claim
|
||
strengthen on its own the day the adapter lands, with no reissue.
|
||
Exact-match admission on a registration-supplied tenant is admission on the client's
|
||
say-so.
|
||
|
||
The mechanism is `key-cape`'s: this record names the property, not the field. Note that
|
||
the same rule from `GH-DEC-2026-011` §3 applies to how it is recorded — two sources, one
|
||
claim value, and the record must distinguish them even though the string is identical.
|
||
It is `unknown` and `absent` again, in the identity layer.
|
||
|
||
**Sequencing:** this does not gate `KEY-WP-0013-T02`. The registration may be created
|
||
under §2 today. What must not happen is the path being described as validated while a
|
||
consumer cannot see which fact it is relying on.
|
||
|
||
### 6. `informed-decision`'s argument for registration-bound, answered
|
||
|
||
`INFD-IN-0002` arrived after this record was written, arguing for the registration-bound
|
||
resolution on a ground `key-cape` did not raise. `informed-decision`'s object model
|
||
separates a **pre-sign binding slice** — which commits which identity and which
|
||
scope/tenant is being entered — from awareness material shown but never signed. Under
|
||
registration-bound, it argues, the tenant is a property of the surface and its
|
||
registration, which is exactly what the binding slice commits; under directory-sourced it
|
||
describes the person, which sits closer to awareness than to binding.
|
||
|
||
**The argument is accepted, and it does not change §1. It sharpens the defect.**
|
||
|
||
What `informed-decision` describes is a real fact that deserves to be committed: *which
|
||
scope is this act being entered into*. That is a property of the **act**. It is not the
|
||
same fact as *which tenant is this person a member of*, which is a property of the
|
||
**principal** and is what `approval-engine` exact-matches to admit an approver.
|
||
|
||
The trouble is that one claim named `tenant` is being asked to carry both. That is why
|
||
the registration-bound shape feels correct to `informed-decision` and wrong to the
|
||
identity layer: each is looking at a different fact through the same field.
|
||
|
||
**Amended 2026-09-10: there are three facts, not two.** `approval-engine` answered this
|
||
record by naming the one it actually uses, and it is neither of the two above:
|
||
|
||
1. **Store isolation** — *did this caller arrive through a channel this store serves.* A
|
||
property of the **channel**. This is what `approval-engine`'s exact-match gate tests,
|
||
and it is what a registration-supplied tenant **is** adequate for: admission means
|
||
arrived through a registered channel, the approver client is static and
|
||
deployment-owned, and dynamic client registration is excluded.
|
||
2. **Act scope** — *which scope is this act being entered into.* A property of the
|
||
**act**. `informed-decision`'s binding slice needs this, and it resolved the point by
|
||
finding that its own `binding.target` already commits it — so the ruling required no
|
||
new field, only a statement that `binding.target` is the act-scope and MUST NOT be
|
||
derived from the token's `tenant` claim.
|
||
3. **Principal membership** — *is this person a member of this zone.* A property of the
|
||
**principal**. This is the one a registration-supplied tenant cannot carry, and the
|
||
one §1 reserves to the directory.
|
||
|
||
`approval-engine` committed to the consequence without being asked: a registration-
|
||
supplied tenant is admissible for (1) and **not** for any doctrine turning on (3), and it
|
||
asked to be held to that as *"the point at which a sound check silently becomes an unsound
|
||
one"*. Held. Note `GH-DEC-2026-016` §5 is the first place it bites.
|
||
|
||
Three facts in one field is a stronger case for §5 than two was. It also explains why
|
||
each party's intuition was locally correct: `approval-engine` reading (1) was right that
|
||
the registration suffices, `informed-decision` reading (2) was right that the fact is
|
||
act-scoped, and the identity layer reading (3) was right that a registration cannot supply
|
||
it. Nobody was wrong about their own fact; the field was wrong to hold all three. A binding
|
||
slice that must commit the scope being entered should **commit that scope**, not borrow
|
||
the principal's membership claim to stand in for it.
|
||
|
||
So `informed-decision` is right that its binding needs the fact, right that the fact is
|
||
act-scoped rather than person-scoped, and wrong that this makes the principal's `tenant`
|
||
claim the place to put it. Its argument is the best available evidence that §5's
|
||
provenance requirement is necessary: two facts sharing one field is exactly the condition
|
||
under which a consumer cannot tell what it is relying on.
|
||
|
||
This record does not design the second field. Whether the act-scope commitment is a
|
||
separate claim, part of the binding document, or something the request-claim schema
|
||
carries when §17 finds it an owner, is not doctrine and is not settled here.
|
||
|
||
## What this does not rule
|
||
|
||
Not ruled: whether `approval-engine`'s exact-match admission is the right gate, which is
|
||
`approval-engine`'s. Not ruled: which repository owns the directory adapter. Not ruled:
|
||
the choice of `tenant:platform` itself, which is settled by operator decision
|
||
`5ed3fb35-eca9-413a-82b9-95171ba85bf6` and is not doctrine.
|
||
|
||
## Reversal condition
|
||
|
||
§2 reverses on the day the directory populates the principal's tenant: the permission is
|
||
spent, not merely unused, and §1 governs alone.
|
||
|
||
§3's general property reverses if a case is found where a transitional shape failing
|
||
closed on its distinguishing case is nonetheless the more dangerous option — which would
|
||
mean the asymmetry rule has an exception, and §8 would be the thing under review rather
|
||
than this record.
|
||
|
||
§5 reverses if the two provenances are ever proven indistinguishable in effect, i.e. if
|
||
no consumer's decision turns on the difference. That is a claim about every present and
|
||
future consumer and we do not expect it to be made good.
|
||
|
||
## Provenance
|
||
|
||
Raised by `key-cape` (`KEY-WP-0013-T05`, `docs/tenant-claim-contract.md`), which built
|
||
the shape, documented its own escalation risk, and declined to ratify its own design.
|
||
Relayed by `informed-decision` while asking about its own registration — a repository
|
||
passing on a question that was not its to carry. `GH-DEC-2026-011`'s decline and this
|
||
record's grant are the two halves of §3, which neither request asked for and which is
|
||
the part of this record most likely to matter later.
|
||
|
||
## GH-DEC-2026-014 — A commitment-only evidence record satisfies non-alteration, never reconstructability, and its erasure must be detectable
|
||
|
||
```yaml
|
||
id: GH-DEC-2026-014
|
||
kind: decision
|
||
title: A commitment-only evidence record satisfies non-alteration, never reconstructability,
|
||
and its erasure must be detectable
|
||
status: resolved
|
||
owner: Bernd Worsch
|
||
repo: gate-house
|
||
standard: net-kingdom/canon/standards/security-layer-model_v0.8.md
|
||
source_note: informed-decision INFD-IN-0003
|
||
requested_dispositions:
|
||
- approved
|
||
- revised
|
||
- rejected
|
||
affects:
|
||
- gate-house
|
||
- informed-decision
|
||
- audit-core
|
||
- approval-engine
|
||
- net-kingdom
|
||
decided_by: Bernd Worsch
|
||
rationale: 'informed-decision asked what travels on the independent evidence path
|
||
that GH-DEC-2026-012 limit 3 requires, proposing commitment-only for stage 1 rather
|
||
than the full binding document, because the binding document carries commercial
|
||
and personal material that would enter the audit fabric under retention and export
|
||
entitlements designed for audit events. The payload schema and its retention are
|
||
audit-core custody and are not ruled here. Two things are doctrine. First, a commitment-only
|
||
record satisfies non-alteration and never reconstructability, and MUST NOT be described
|
||
as satisfying the second; this is the existing bound that an archive proves records
|
||
were not altered or truncated after arrival and never that one was never sent, applied
|
||
to a record that additionally does not carry what it commits to. Second, the gap
|
||
commitment-only leaves is availability rather than integrity, and it sits with the
|
||
actor being audited, so erasure or non-production of the committed content MUST
|
||
be detectable as a finding rather than present as an unremarkable absence. The independent
|
||
path therefore carries a declaration that content exists and where custody sits,
|
||
so that absence at retrieval is a conformance failure. Commitment-only is admissible
|
||
on the GH-DEC-2026-013 test because its distinguishing case fails closed: a reviewer
|
||
who cannot obtain the content gets no reconstruction rather than a wrong one. informed-decision
|
||
refusal of a separate evidence store is endorsed, on its own ground that it would
|
||
route around the section 16 archival-custody question by building a parallel archive
|
||
under another name.'
|
||
created: '2026-09-09T21:23:46.190170Z'
|
||
updated: '2026-09-09T21:23:46.190170Z'
|
||
state_hub_decision_id: "ae933280-873e-4226-b001-df94a1f00f50"
|
||
```
|
||
|
||
## Context
|
||
|
||
`GH-DEC-2026-012` limit 3 requires the evidence copy of a presentation record to reach
|
||
`audit-core` **independently of the emitter**, because in `informed-decision`'s case the
|
||
actor being audited and the evidence source are the same component. `INFD-IN-0003` asks
|
||
what travels on that path.
|
||
|
||
The candidate it rejects is emitting the **full binding document**, which would put
|
||
commercial and personal material — at L4, contract text — into the audit fabric under
|
||
retention and export entitlements designed for audit events. That objection is sound and
|
||
is not merely operational: an evidence path that forces content into a custody regime
|
||
built for a different class of data is a defect in the evidence path, not a cost of using
|
||
it.
|
||
|
||
It proposes **commitment-only** for Stage 1: hashes, principal, timestamps,
|
||
acknowledgements, co-referenced approval id, disposition verb, stance application. It
|
||
declares plainly that this leaves it able to **erase** the content, and asks not to be
|
||
credited with closing that.
|
||
|
||
It also refused a third option on its own initiative — a separate evidence store — on
|
||
the ground that it has no owner and would route around §16's open question on stronger
|
||
archival custody by building a parallel archive under another name.
|
||
|
||
## What is ruled here, and what is not
|
||
|
||
**Not ruled: the payload.** Which fields travel, in what schema, under what retention, is
|
||
`audit-core`'s custody question and it has it. This record does not design the record.
|
||
|
||
**Ruled: what may be claimed from a record of that shape, and what must remain visible
|
||
when it fails.** That is doctrine because it governs what a later reader is entitled to
|
||
conclude, and a reader's entitlement is not a custody decision.
|
||
|
||
## Decision
|
||
|
||
### 1. Commitment-only is admissible for Stage 1
|
||
|
||
Granted, on `GH-DEC-2026-013` §3's test: **its distinguishing case fails closed.** Where
|
||
the committed content is needed and cannot be obtained, a reviewer gets *no
|
||
reconstruction* rather than a *wrong* one. The record does not produce a confident
|
||
answer built on material nobody can check. Compare the shape refused there — a
|
||
transitional state that fails open on the case that distinguishes it is a permission
|
||
wearing a date.
|
||
|
||
The data-protection reason is accepted as a reason of the right kind. Forcing L4 contract
|
||
text into an audit fabric to satisfy an evidence obligation would trade one control for a
|
||
breach of another, and doctrine that produces that trade is wrong rather than merely
|
||
expensive.
|
||
|
||
### 2. It satisfies non-alteration. It does not satisfy reconstructability, and MUST NOT be described as doing so
|
||
|
||
This repository already holds that **an archive proves records were not altered or
|
||
truncated after arrival, never that one was never sent.** A commitment-only record is
|
||
weaker again: it additionally does not carry what it commits to. So it establishes that
|
||
the *commitment* was made when it says and has not changed since. It establishes nothing
|
||
about what was committed to, except conditionally — *if* a document is later produced,
|
||
whether it is the one.
|
||
|
||
**A commitment-only record MUST NOT be described as satisfying reconstructability**, in
|
||
`informed-decision`'s documents, in `audit-core`'s, or in any conformance claim. Every
|
||
privileged action being reconstructable from the evidence is a property of the estate's
|
||
audit obligation; a hash of an absent document does not have it and no amount of
|
||
integrity on the hash supplies it.
|
||
|
||
This is the same discipline the §6.4 obligation-1 gap has: the check is sound, and the
|
||
property a reader infers from it is not present unless it is stated to be absent.
|
||
|
||
### 3. The gap is availability, not integrity — and it sits with the audited party
|
||
|
||
Say it in those words, because "we can erase the content" understates where the problem
|
||
is. Commitment-only moves **integrity** out of the emitter's control and leaves
|
||
**availability** entirely inside it. The party that can withhold the content is the party
|
||
the evidence is about.
|
||
|
||
That is precisely the condition limit 3 exists to prevent, reduced but not removed. The
|
||
reduction is real — the emitter can no longer alter the record, which is the more common
|
||
failure — and it is not sufficient on its own.
|
||
|
||
### 4. Non-production MUST be detectable as a finding, not present as an absence
|
||
|
||
**This is the condition that makes §1's grant safe, and it is the part `INFD-IN-0003` did
|
||
not propose.**
|
||
|
||
If the independent path holds only a commitment, a reviewer who asks for the content and
|
||
receives nothing cannot distinguish *erased*, *withheld*, *lost*, and *never held*. The
|
||
absence reads as an unremarkable blank rather than as evidence of anything.
|
||
|
||
The independent path MUST therefore carry, alongside the commitment, **a declaration that
|
||
committed content exists and where custody sits**, such that failure to produce it at
|
||
retrieval is a **conformance failure** attributable to the custodian rather than a silent
|
||
gap in the record. A commitment with no accompanying assertion that something is being
|
||
committed to is indistinguishable from a commitment to nothing.
|
||
|
||
**Amended 2026-09-10 — detection sits with the reviewer, not with the archive.**
|
||
`audit-core` accepted this condition and corrected its wording, and the correction is
|
||
load-bearing enough that leaving it as an assumption would have made this section wrong.
|
||
The archive **carries** the declaration; it does not **detect** non-production. It
|
||
performs no retrieval, holds no client for the emitting repository, and its egress policy
|
||
permits nothing that would let it try — asserted by a test, because the claim silently
|
||
stops being true the day someone adds one.
|
||
|
||
So the mechanism is: the **reviewer** discovers non-production at retrieval, and the
|
||
stored declaration is what makes that discovery a **finding** rather than a blank —
|
||
because the reviewer holds a chained, timestamped statement that content existed and where
|
||
custody sat. Nothing in this section requires an archive to chase content, and it MUST NOT
|
||
be read as requiring it. An archive that fetched from the parties it audits would be
|
||
acquiring exactly the dependency that makes it corruptible by them.
|
||
|
||
**And the residual this creates, stated rather than left to be found.** A custodian that
|
||
never held the content can emit a false `content_exists`; the archive validates the
|
||
declaration's **shape**, never its **truth**. That is the same class as omission at source
|
||
— an archive cannot retrofit a property the boundary did not have — and it is closed by
|
||
neither the hash chain, nor attestation, nor `T-04`/`T-06`.
|
||
|
||
What §4 actually buys is narrower than it first reads, and this is the honest statement of
|
||
it: **the declaration converts an unattributable absence into an attributable false
|
||
statement.** Strictly better, and not the same as proof. Raised by `audit-core` against
|
||
its own delivered work, with the observation that the distinction is better in this text
|
||
now than in a conformance argument later.
|
||
|
||
This is `GH-DEC-2026-011` §3's rule in its third setting: two states, one observable
|
||
appearance, and the record must distinguish them. There it was `unknown` versus `absent`
|
||
in a stance map; in `GH-DEC-2026-013` §5 it was directory-asserted versus
|
||
registration-supplied in an identity claim; here it is *erased* versus *never held* in an
|
||
evidence path. The recurrence is the point — wherever a system can reach one appearance
|
||
by two routes, the record must say which route, or the safer reading of the appearance
|
||
becomes unavailable to everyone.
|
||
|
||
### 5. The refusal of a separate evidence store is endorsed
|
||
|
||
`informed-decision` refused it on the ground that it has no owner and would route around
|
||
§16's archival-custody question by building a parallel archive under another name. That
|
||
reasoning is correct and we would keep it as written.
|
||
|
||
Add one thing to it: an evidence store owned by the party whose conduct it evidences is
|
||
not an evidence store, whatever its integrity properties. The ownership objection is
|
||
prior to the custody one.
|
||
|
||
### 6. The residual is not closed and is not credited
|
||
|
||
`informed-decision` asked, again, not to be credited with closing what it has not closed.
|
||
Honoured, again. The erasure gap stands alongside the compromised-surface residual from
|
||
`GH-DEC-2026-012`, and §4 narrows the first without closing it: a detectable
|
||
non-production tells a reviewer that something is missing and who owed it. It does not
|
||
produce the missing thing.
|
||
|
||
## Reversal condition
|
||
|
||
§1 reverses if commitment-only is found to be load-bearing for a reconstruction that
|
||
actually had to be performed and could not be — i.e. if the availability gap is realised
|
||
rather than theorised. Stage 1 is a stage; the burden is on the shape to keep earning it.
|
||
|
||
§4 is not a reversal candidate. If it turns out to be expensive, the answer is a cheaper
|
||
mechanism for the same property, never the property's removal.
|
||
|
||
## Provenance
|
||
|
||
Raised by `informed-decision` (`INFD-IN-0003`), which proposed the shape, declared the
|
||
gap it leaves, refused a third option against its own convenience, and asked which half
|
||
of the question was doctrine. The half that was is ruled here; the payload is
|
||
`audit-core`'s and it has it. §4 was not requested by anyone.
|
||
|
||
## GH-DEC-2026-015 — GH-DEC-2026-012 R3 revised: nesting is permitted for this pair, conditioned on the exclusion being normative
|
||
|
||
```yaml
|
||
id: GH-DEC-2026-015
|
||
kind: decision
|
||
title: 'GH-DEC-2026-012 R3 revised: nesting is permitted for this pair, conditioned
|
||
on the exclusion being normative'
|
||
status: resolved
|
||
owner: Bernd Worsch
|
||
repo: gate-house
|
||
standard: net-kingdom/canon/standards/security-layer-model_v0.8.md
|
||
source_note: informed-decision/docs/finding-r3-linkage-conflict.md (INFD-IN-0004);
|
||
approval-engine docs/approval-claim.md @ 62233c7
|
||
requested_dispositions:
|
||
- approved
|
||
- revised
|
||
- rejected
|
||
affects:
|
||
- gate-house
|
||
- informed-decision
|
||
- approval-engine
|
||
- access-engine
|
||
- net-kingdom
|
||
decided_by: Bernd Worsch
|
||
rationale: 'GH-DEC-2026-012 R3 refused option (c) and required linkage by co-reference.
|
||
approval-engine, answering the same question from the layer that owns the digest,
|
||
recommended exactly (c). informed-decision could not comply with both and raised
|
||
it as a finding rather than choosing, which is the correct handling. The refusal
|
||
is revised on two grounds. First, the hash-cycle objection requires mutual containment,
|
||
and approval-engine has since published that its binding digest covers exactly five
|
||
act fields and that widening it to cover presentation would be a defect for an independent
|
||
reason - a new UI release would invalidate every prior approval - so one-directional
|
||
nesting raises no cycle. Second and decisively, co-reference by identifier alone
|
||
leaves informed-decision independently canonicalizing principal and target, two
|
||
of the five fields in the other layer digest, which is a partial recomputation of
|
||
one act in a second vocabulary and is closer to the translation R3 forbade than
|
||
nesting is. The original ruling therefore worked against its own rule. Nesting is
|
||
permitted for this pair, conditioned on approval-engine stating the presentation
|
||
exclusion as normative and testable rather than as design intent, because the cycle
|
||
cannot arise here is a belief and the cycle may not arise here is a rule with an
|
||
owner. The ordering objection is withdrawn as mistaken: the digest is over act material
|
||
and is computable before a human is presented anything.'
|
||
created: '2026-09-10T13:19:48.436344Z'
|
||
updated: '2026-09-10T13:19:48.436344Z'
|
||
state_hub_decision_id: "5063b6a3-5f99-4558-beb6-f4c494a46bfc"
|
||
```
|
||
|
||
## Context
|
||
|
||
`GH-DEC-2026-012` R3 ruled that `view_hash` and `approval-engine`'s binding digest are
|
||
distinct attestations linked by **co-reference**, and refused option (c) — `view_hash`
|
||
carrying the binding digest as a field — on two grounds: an ordering dependency, and the
|
||
`GH-DEC-2026-008` hash cycle.
|
||
|
||
`approval-engine` then answered the same question in `docs/approval-claim.md` (`62233c7`)
|
||
and recommended **exactly (c)**: that `informed-decision`'s binding document carry
|
||
`binding.digest` as a field rather than re-canonicalize `action`/`actor`/`principal`/
|
||
`purpose`/`target` itself, so there is one canonicalization of the act, computed by the
|
||
layer that owns it.
|
||
|
||
`informed-decision` could not comply with both, **made no design change**, and raised it
|
||
as a finding. That is the correct handling and it is worth naming: it declined to adopt a
|
||
recommendation that came from the layer owning the artifact, having previously offered to
|
||
let that layer settle R3 bilaterally, on the ground that a bilateral agreement produces
|
||
agreement rather than an authority rule. It applied that reasoning against its own
|
||
convenience twice.
|
||
|
||
## Decision
|
||
|
||
### 1. The refusal of (c) is revised. Nesting is permitted for this pair
|
||
|
||
**`view_hash` MAY carry `binding.digest` as a field**, and `informed-decision`'s binding
|
||
slice then stops independently canonicalizing act material. One canonicalization of the
|
||
act, computed by the layer that owns the act, committed to by reference.
|
||
|
||
### 2. The cycle objection does not hold here, and the reason it does not is narrow
|
||
|
||
`GH-DEC-2026-008`'s cycle required **mutual containment**: a claim had to name the digest
|
||
of a request that would come to contain that claim. `approval-engine` has since published
|
||
that `binding.digest` covers exactly five act fields, and that widening it to cover
|
||
presentation material **would be a defect** — a new UI release would change the digest of
|
||
an act whose act did not change, invalidating every prior approval. So the containment is
|
||
one-directional and no cycle arises.
|
||
|
||
`informed-decision` declined to assert that this settles it, on the ground that *"the
|
||
cycle cannot arise here"* is precisely the belief such failures punish. That caution is
|
||
correct and is why §4 exists rather than a bare permission.
|
||
|
||
### 3. The decisive ground is that the original ruling worked against its own rule
|
||
|
||
R3 required: *"You MUST NOT recompute or restate `approval-engine`'s binding digest from
|
||
your own vocabulary."* But `informed-decision`'s binding slice canonicalizes `principal`
|
||
and `target` — **two of the five fields in that digest**. Under co-reference by
|
||
identifier alone, two independent canonicalizations of one act exist, linked by a shared
|
||
id, and the presenting surface is performing a partial recomputation of the other layer's
|
||
material in its own vocabulary.
|
||
|
||
**That is closer to the translation R3 forbade than nesting is.** Co-reference by id
|
||
*manages* the duplication with an authority rule; nesting *removes* it. Removing a
|
||
failure mode beats managing one, and the original ruling reached for the management
|
||
option while stating the rule that recommends the removal.
|
||
|
||
This is the second time this month a Gate House ruling has been correct in substance and
|
||
wrong in the mechanism it prescribed, found by the repository that had to build on it.
|
||
The pattern is recorded rather than smoothed over: `GH-DEC-2026-008` mandated a
|
||
comparison that could never pass; this one mandated a linkage that reintroduced the thing
|
||
it forbade.
|
||
|
||
### 4. Condition: the exclusion becomes normative, not intentional
|
||
|
||
**Permission under §1 is conditioned on `approval-engine` stating, as a rule rather than
|
||
as design intent, that `binding.digest` MUST NOT cover presentation material** — and on
|
||
that constraint being asserted by a test, so that widening fails loudly rather than
|
||
silently.
|
||
|
||
`approval-engine` already holds the reason, and it is a good one that does not depend on
|
||
this ruling: widening would invalidate every prior approval on a UI release. What it does
|
||
not yet hold is the *rule*. The difference is exactly A-17's precondition — the
|
||
distinguishing case here is *someone widens the digest*, and today that case is
|
||
unobservable until approvals start failing. Making it observable is a precondition of the
|
||
permission, not a follow-up to it.
|
||
|
||
Until that lands, `GH-DEC-2026-012` R3's co-reference form remains in force.
|
||
`informed-decision` changes nothing on its own initiative; the permission activates when
|
||
the condition is met.
|
||
|
||
### 5. What does not change
|
||
|
||
- **The authority rule stands.** The binding digest is authoritative for what the request
|
||
*is*; `view_hash` is authoritative only for what was *shown*. Under §1 the two can no
|
||
longer disagree about the act, which retires the disagreement case rather than
|
||
reversing the rule that governed it.
|
||
- **(a) remains refused**, on its original and undisturbed ground: merging the two
|
||
canonicalizations would make one repository authoritative over another layer's
|
||
material.
|
||
- **`view_hash` still MUST NOT travel inside hashed request material while containing the
|
||
binding digest.** §2's one-directionality is a fact about the present pair, not a
|
||
licence. If a future design puts `view_hash` inside a hashed request, the cycle
|
||
reappears and this permission is void for that path.
|
||
|
||
### 6. The ordering objection is withdrawn as mistaken
|
||
|
||
`GH-DEC-2026-012` gave an ordering dependency as (c)'s first cost, taking it from
|
||
`informed-decision`'s own statement of it. It does not hold: `binding.digest` is computed
|
||
over **act material**, which is determined before a human is presented anything. The
|
||
approval need not exist before the memo is rendered — only the act need be determined,
|
||
which it is by construction.
|
||
|
||
Recorded rather than dropped, because a cost accepted from the requester and never
|
||
checked is how a wrong reason survives into a ruling.
|
||
|
||
## On the exposure `informed-decision` asked to have visible
|
||
|
||
It asked that if co-reference stood, the record show that it would be the party a
|
||
disagreement was a finding against, for a disagreement arising from **two correct
|
||
canonicalizations** rather than from any defect of its own — the design creating the
|
||
exposure, not its conduct.
|
||
|
||
That request is granted in a better form: §1 removes the exposure rather than documenting
|
||
it. The asymmetry it accepted was right in principle and it should not have had to carry
|
||
it in practice.
|
||
|
||
## Reversal condition
|
||
|
||
§1 reverses if `approval-engine`'s digest ever widens to cover presentation material,
|
||
which §4's rule exists to prevent and which would restore mutual containment. It also
|
||
reverses for any path that places `view_hash` inside hashed request material.
|
||
|
||
## Provenance
|
||
|
||
Raised by `informed-decision` (`INFD-IN-0004`), which made no design change, declined a
|
||
recommendation from the layer owning the artifact, applied Gate House's own bilateral-
|
||
settlement reasoning against its own convenience, and supplied the published-coverage
|
||
fact the original refusal did not have. `approval-engine` supplied the recommendation and
|
||
the structural reason behind it.
|
||
|
||
## GH-DEC-2026-016 — Where an approval discharges a human-in-the-loop control, the approver must be a human principal and the engine must refuse at bind time
|
||
|
||
```yaml
|
||
id: GH-DEC-2026-016
|
||
kind: decision
|
||
title: Where an approval discharges a human-in-the-loop control, the approver must
|
||
be a human principal and the engine must refuse at bind time
|
||
status: resolved
|
||
owner: Bernd Worsch
|
||
repo: gate-house
|
||
standard: net-kingdom/canon/standards/security-layer-model_v0.8.md
|
||
source_note: informed-decision INFD-IN-0004 section 6 (NC-03); approval-engine, which
|
||
declined to invent the rule
|
||
requested_dispositions:
|
||
- approved
|
||
- revised
|
||
- rejected
|
||
affects:
|
||
- gate-house
|
||
- approval-engine
|
||
- informed-decision
|
||
- access-engine
|
||
- audit-core
|
||
- net-kingdom
|
||
decided_by: Bernd Worsch
|
||
rationale: 'approval-engine records the approver principal type as auditable metadata
|
||
but restricts only /consume by principal type, and its own operator service client
|
||
holds approval:approve, so a service principal can supply approver evidence for
|
||
a control whose purpose is to put human judgment in the path. informed-decision
|
||
enforces humans-bind-agents-draft at its own surface and correctly observed that
|
||
without an engine-side refusal its enforcement is one component policy rather than
|
||
a property of the approval object. Both referred the question up rather than settling
|
||
it. Ruled: where an approval is declared as discharging a human-in-the-loop or dual-control
|
||
requirement, the approver MUST be a human principal and approval-engine MUST refuse
|
||
a non-human bind at issue rather than record it. Recording the principal type after
|
||
the fact is detection and not prevention, which is the distinction this repository
|
||
applies everywhere else, and an approval control satisfiable by the same class of
|
||
actor it exists to check is theatre. The rule attaches to approvals declared as
|
||
discharging such a control rather than to all approvals, because service-to-service
|
||
approvals are legitimate and the requirement must be declared at issue and never
|
||
inferred - the same shape approval-engine established for pdp_path. Enforcement
|
||
at one surface is insufficient because a second caller reaching the engine directly
|
||
bypasses it, and the property belongs to the object rather than to whichever component
|
||
happens to create it.'
|
||
created: '2026-09-10T13:20:35.560626Z'
|
||
updated: '2026-09-10T13:20:35.560626Z'
|
||
state_hub_decision_id: "1139bb2f-7774-4559-bd71-e9d5376b58f4"
|
||
```
|
||
|
||
## Context
|
||
|
||
`approval-engine` put the question rather than answering it, which is the right instinct:
|
||
|
||
> `entries[].principal_type` is the auditable half only. It tells you what bound an
|
||
> approval after the fact; it does not stop anything. If your doctrine needs a service
|
||
> principal refused at bind time, that is a `gate-house` question and we would implement
|
||
> a ruling — we simply will not invent one.
|
||
|
||
`informed-decision` enforces *"humans bind, agents draft"* at its own surface and observed
|
||
that there is **no upstream backstop**: `approval-engine` restricts only `/consume` by
|
||
principal type, and its own operator service client holds `approval:approve`. It is not
|
||
asking for the refusal on its own account — its surface enforces it for its own callers
|
||
regardless. Its question is whether the estate wants *approver evidence itself* to be
|
||
human-only at the engine, in which case its enforcement is a property of the approval
|
||
object rather than one component's policy.
|
||
|
||
Both referred it up rather than settling it between them. That is the same restraint that
|
||
produced `GH-DEC-2026-015`, and it is the reason this question is answerable at all.
|
||
|
||
## Decision
|
||
|
||
### 1. Where an approval discharges a human-in-the-loop control, the approver MUST be a human principal
|
||
|
||
**And `approval-engine` MUST refuse a non-human bind at issue, rather than record it.**
|
||
|
||
An approval control exists to put a judgment of a particular kind into the path of an
|
||
act. Where that kind is *human*, an approval bound by a service principal does not
|
||
weakly satisfy the control — it does not engage it at all. A control satisfiable by the
|
||
same class of actor it exists to check is theatre, and the estate's first rule is that
|
||
**no privilege comes from cognition**: an agent determining that an action is appropriate
|
||
never makes it authorized, and it does not become authorized because the agent recorded
|
||
its determination in an approval object.
|
||
|
||
### 2. Recording is detection. Refusal is prevention. The distinction is not new here
|
||
|
||
`entries[].principal_type` is exactly right as a record and is not the control.
|
||
`approval-engine`'s own framing — *"the auditable half only… it does not stop anything"* —
|
||
is the same distinction this repository applies to reconstructability at the issuer, to
|
||
audit completeness, and to `GH-DEC-2026-014`'s commitment-only records. Detection belongs
|
||
in the same register as the other residuals. It is not a substitute for a refusal that
|
||
can be made cheaply at issue.
|
||
|
||
The bind is the cheap moment. Refusing at `/consume` is refusing after the approval has
|
||
been presented as satisfying a control it never satisfied, and after anything relying on
|
||
its existence has relied on it.
|
||
|
||
### 3. The rule attaches to declared approvals, not to all approvals
|
||
|
||
**Service-to-service approvals are legitimate** and this ruling does not disturb them.
|
||
The requirement attaches where an approval is **declared** as discharging a human-in-the-
|
||
loop or dual-control requirement.
|
||
|
||
**Declared at issue, never inferred.** A human approver who happens to be present is not
|
||
a declaration, and an approval MUST NOT be retroactively read as human-discharging
|
||
because its entries turn out to be human. This is the shape `approval-engine` itself
|
||
established for `pdp_path`: intent is declared, `create()` refuses the declaration
|
||
without what it requires, legacy rows migrate to the negative rather than being
|
||
back-filled, and a successor inherits its predecessor's declaration. Back-filling would
|
||
manufacture a statement no requester made — `GH-DEC-2026-008`'s objection to translation,
|
||
in another form, and `approval-engine`'s own argument returned to it.
|
||
|
||
Where the requirement is declared and the binding principal is not human, `create()`
|
||
refuses — the same failure-at-issue shape that already refuses `pdp_path: true` without a
|
||
`pdp_digest`, so an approval that would be unusable fails when it is made rather than at
|
||
the protected side effect.
|
||
|
||
### 4. One surface enforcing it is not the property being held
|
||
|
||
`informed-decision`'s enforcement is correct and insufficient, exactly as it said. A
|
||
second caller reaching `approval-engine` directly bypasses it, and the estate's operator
|
||
service client holds `approval:approve` today. **The property belongs to the approval
|
||
object, not to whichever component happened to create it** — otherwise the guarantee is
|
||
"human-approved unless someone used a different client", which is not a guarantee.
|
||
|
||
`informed-decision` should keep its surface enforcement. Defence at the surface and at
|
||
the engine are not redundant: the surface refuses earlier and with a better error, and
|
||
the engine is what makes the refusal a property of the object.
|
||
|
||
### 5. What this does not rule
|
||
|
||
Not ruled: what makes a principal *human* — that is `key-cape`'s and the identity
|
||
layer's, and note it interacts with `GH-DEC-2026-013` §5. A principal-type claim is
|
||
subject to A-16 like any other: if `human` is reachable by two routes — asserted by the
|
||
directory about the person, or supplied by a registration about the client — the record
|
||
must say which, and a human-in-the-loop control MUST NOT be discharged on a
|
||
registration-supplied claim. Refusing a service principal while accepting an unverified
|
||
assertion of humanity would move the defect rather than close it.
|
||
|
||
Not ruled: which acts require human approval. That is doctrine, it is Gate House's, and
|
||
it is not settled here — this record says what a *declared* human control requires, not
|
||
where such controls are mandatory.
|
||
|
||
Not ruled: `approval-engine`'s scope model, or whether `approval:approve` on an operator
|
||
service client is itself correct. Flagged as worth their review, since it is the concrete
|
||
path by which the gap exists today.
|
||
|
||
## Reversal condition
|
||
|
||
§1 reverses if a case is found where a human-in-the-loop control is legitimately
|
||
discharged by a non-human principal — which would mean the control was misdeclared rather
|
||
than that the rule is wrong, and the correct repair is the declaration.
|
||
|
||
§3's declared-only scoping reverses if declarations turn out to be systematically omitted
|
||
where they are needed, making the rule vacuous in practice. That would be evidence for
|
||
requiring the declaration rather than for widening the enforcement.
|
||
|
||
## Provenance
|
||
|
||
Raised by `approval-engine`, which named the gap in its own enforcement, stated plainly
|
||
that its own operator client holds the permission that makes it reachable, and declined
|
||
to invent the rule. Carried by `informed-decision`, which enforces the property already,
|
||
does not need the ruling for itself, and asked anyway because a property held by one
|
||
component is not a property of the object. Neither repository asked for anything that
|
||
benefits it.
|