diff --git a/decisions/decisions.md b/decisions/decisions.md index 06ee617..80bbfa7 100644 --- a/decisions/decisions.md +++ b/decisions/decisions.md @@ -1219,3 +1219,79 @@ 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. diff --git a/docs/conformance/2026-09-06-v08-assent-round.md b/docs/conformance/2026-09-06-v08-assent-round.md new file mode 100644 index 0000000..d79df96 --- /dev/null +++ b/docs/conformance/2026-09-06-v08-assent-round.md @@ -0,0 +1,112 @@ +# v0.8 assent round — findings and dispositions + +**Repository:** gate-house +**Project family:** NetKingdom security layer +**Status:** open — round in progress +**Version:** 0.1 +**Date:** 2026-09-06 +**Subject:** `net-kingdom/canon/standards/security-layer-model_v0.8.md` (proposed) +**Workplan:** `GH-WP-0003-T06` + +*Derived artifact: this record states the round's status as of the date above, not +current state. Authoritative bodies are the decision records and the cut itself.* + +## Circulated + +`ops-warden`, `approval-engine`, `access-engine`, `kings-guard`, `audit-core`, and +`net-kingdom` (publisher). Each was told what the version costs them and what to +attack, rather than sent a summary to nod at. + +## Findings returned + +### F1 — `GH-DEC-2026-008` was unimplementable as written · **adopted, corrected** + +Reported by `access-engine`, independently by `approval-engine`, originally found by +`secrets-engine` re-verifying a replay fixture — within hours of the ruling and before +the assent round had properly opened. + +The ruling required `claim.binding.pdp_digest` to equal the decision's +`NewDecisionBinding.request_digest`. `access-engine` hashes context into that digest +and the dual-control pattern carries the claim in `context.approval`, so embedding the +claim changes the digest of the request carrying it. A hash cycle: a claim cannot name +the digest of a document containing that claim. + +**A fail-closed consumer obeying the rule would have denied `destroy` permanently.** +The ruling's own fail-closed requirement is what would have made it harmful rather +than merely wrong — the rule and the failure mode compounded instead of cancelling. + +**Disposition.** Ruling stands; its four obligations stand; obligation 1's comparison +target is corrected to the PDP's published exclusion-scoped digest +(`binding.approval_binding_digest`, `FLEX-DEC-2026-007`). v0.8 §9.7.3 rewritten. A +consumer MUST NOT guess the exclusion rule, and until a PDP publishes one the path is +fail-closed rather than complete — a gap in a dependency, not in the ruling. + +Verified independently against `flex-auth/schemas/decision_envelope.schema.json` and +`pkg/api/canonical.go` before amending. + +### F2 — replay identity versus correspondence digest · **adopted, added** + +Flagged by `access-engine` explicitly as a near miss it was *not* asking to have +written down. The obvious fix to F1 is to drop `context.approval` from the request +digest entirely, and that is unsafe: two requests differing only in which approval was +presented decide differently, so collapsing them lets an allow obtained with a valid +claim be replayed against a request carrying none. + +**An evidence-bearing input may be excluded from a correspondence digest but never +from the replay identity.** Added to v0.8 §6.4 obligation 5. A property discovered by +nearly getting it wrong is worth more written down than remembered, and the shape +recurs wherever evidence travels inside a hashed request. + +### F3 — `GH-DEC-2026-009` adopted, with a reason we had not stated + +`access-engine` records that the `unknown` ruling is right for the adversarial reason +— `unknown` is the cheapest state to induce, so failing open on it makes being +unclassifiable a credential-free escalation — and notes it had reported the divergence +without reaching that reading. No change; recorded because the reasoning, not the +conclusion, is what a later reader needs. + +It also endorses sending `ops-warden`'s non-conformant cell to the round rather than +imposing it. + +### F4 — A7 accepted by its own worked example + +`access-engine` accepts its dual-control instance as the one carrying the argument, +and notes the §11 marking obligation would have caught the source: the stale G3 row in +its own 2026-08-29 review, handed on as current. No change. + +## Implementation ahead of acceptance + +Both engines implemented against the ruling before the round closed. + +- `access-engine` — `FLEX-DEC-2026-007`, `dd3ce4c`: publishes + `binding.approval_binding_digest`, and asserts by test that the two digests + **disagree** on a claim-bearing request, so a later refactor cannot collapse them. +- `approval-engine` — `7e75677`, 102 tests: schema v3 declares `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 and never inferred — a digest that happens to be present is not a + declaration, 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 `GH-DEC-2026-008`'s own + objection to translation in another form. + +## Still outstanding + +`ops-warden` (the `unknown` cell it acquires), `kings-guard` (the criteria-grounding +clause, and whether §12's step-four paragraph is now stale), `audit-core` (whether +§11's emission-guarantee wording lets a source imply completeness), `net-kingdom` +(whether §17 says what it would say in its own voice, and whether §11 tracks its +published profile). + +## What the round has shown so far + +The first finding against v0.8 was a rule that was **right and unimplementable**, and +it was found by the repository that would have been broken by it, then reported by two +others under no obligation to look. That combination is only survivable because the +implementers read the ruling against their own fixtures instead of accepting it — the +behaviour §12 describes, arriving without being asked for. + +It is also an argument for the round itself. `GH-DEC-2026-008` was correct in +substance, argued from doctrine, and verified against another repository's schema +before it was issued. None of that caught a defect that three repositories found in +hours of trying to build on it. diff --git a/docs/contracts/approval-consumption.md b/docs/contracts/approval-consumption.md index 2839683..c2ad247 100644 --- a/docs/contracts/approval-consumption.md +++ b/docs/contracts/approval-consumption.md @@ -5,7 +5,7 @@ **PDP:** access-engine (currently flex-auth) **Status:** normative for GH-WP-0002-T06; recorded as `GH-DEC-2026-003`, amended by `GH-DEC-2026-005` (step-1 artifact and split validation) and -`GH-DEC-2026-008` (the PDP digest is the binding correspondence) +`GH-DEC-2026-008` (the binding correspondence, amended for implementability) **Date:** 2026-09-06 **Statute:** `net-kingdom/canon/standards/security-layer-model_v0.7.md` §9.4, §9.7.3–4, §16 **Blocks:** `APPROVAL-WP-0001-T05`, `FLEX-WP-0017-T05` @@ -61,10 +61,26 @@ claim. holds no runtime approval authority; the issuer to check is the engine that served the claim. -**The correspondence is the PDP digest.** Amended by `GH-DEC-2026-008`. A consumer -on this path MUST verify that `claim.binding.pdp_digest` equals the decision's -`NewDecisionBinding.request_digest`, and MUST NOT use a claim that carries no -`pdp_digest`. Recomputing `approval-engine`'s native `binding.digest` from a +**The correspondence is a digest, and it is the PDP's to define.** Amended by +`GH-DEC-2026-008`. A consumer on this path MUST verify that the approval's recorded +PDP digest equals the digest the PDP publishes **for the request with the approval +evidence excluded** — `binding.approval_binding_digest` in `access-engine` — and MUST +NOT use a claim carrying no such digest. + +The comparison is **not** against the full request digest. Where the claim travels +inside the hashed request, embedding it changes that request's digest, so a digest +recorded at issue can never equal the final one: a hash cycle whose resolution is +forced, not chosen. The first form of this rule mandated a check that could never +pass, and a fail-closed consumer obeying it would have denied the action permanently. + +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. Until a PDP +publishes its exclusion rule, this path is correctly fail-closed rather than complete. + +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, so one digest cannot serve both purposes. Recomputing `approval-engine`'s native `binding.digest` from a `CheckRequest` is **not** a permitted fallback here: it requires translating between two engines' vocabularies, no mapping is published, and a wrong translation fails open by silently accepting a claim approved for something else. The native limb