From 22915cf2a0b87885466855f42581794e95838b1e Mon Sep 17 00:00:00 2001 From: tegwick Date: Sun, 6 Sep 2026 14:18:36 +0200 Subject: [PATCH] Rule on unknown and the scoping axis as GH-DEC-2026-009 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit access-engine exercised the divergence capability it claimed in the v0.6 round, on the first occasion §13.1 held two rows. ops-warden resolves unknown to fail_open, secrets-engine to fail_closed; both conformant, both total, both test-pinned, disagreeing about the one case that by construction nobody planned for. They also scope over different axes, so the register cannot answer what an inventory exists to answer. Ruling 1: unknown is not a zone and MUST fail closed. §9.3 permits trading availability for openness per zone — and that trade requires knowing the zone. Where the scope is unknown the trade cannot have been made for it, so a permissive unknown does not extend a considered decision, it invents the most permissive one. An unreachable engine is a known request in a degraded system; an unclassified subject is not. unknown is the cheapest state for an attacker to induce, so failing open on it makes being unclassifiable a privilege escalation requiring no credential, which §8's asymmetry forbids wherever it appears. Ruling 2: each map declares its scoping axis and its relation to zone. Forcing everyone onto zones would make secrets-engine assert a zone it cannot know, and a fiction in a runtime-read test-pinned file is worse than an honest incommensurability. The register records the axes and states that cross-axis aggregation is unavailable. ops-warden acquires one non-conformant cell at v0.8. It did everything asked — published first, built the reference form, offered it estate-wide — so this goes to the assent round rather than being imposed quietly. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WtJBr77gMFLrN93iEevqQJ Assistant: claude-code Assistant-Model: opus Assistant-Process: 425128@bnt-lap001 Assistant-Session: f5944d8b-dac4-4e1a-87eb-8b3d8f314a63 --- decisions/decisions.md | 107 ++++++++++++++++++ docs/amendments/v0.8-amendment-set.md | 66 ++++++++++- .../GH-WP-0003-statute-v08-amendment-set.md | 20 ++++ 3 files changed, 187 insertions(+), 6 deletions(-) diff --git a/decisions/decisions.md b/decisions/decisions.md index 64ca393..06ee617 100644 --- a/decisions/decisions.md +++ b/decisions/decisions.md @@ -1112,3 +1112,110 @@ decided_by: Bernd Worsch created: '2026-09-06T12:17:19.104531Z' updated: '2026-09-06T12:17:19.104531Z' ``` + +## 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. diff --git a/docs/amendments/v0.8-amendment-set.md b/docs/amendments/v0.8-amendment-set.md index b3fba4e..686c8be 100644 --- a/docs/amendments/v0.8-amendment-set.md +++ b/docs/amendments/v0.8-amendment-set.md @@ -30,6 +30,7 @@ Assembly and circulation are `GH-WP-0003-T06`. | A5 | §6.4, §8 | `GH-DEC-2026-005`, `GH-DEC-2026-008` | T05 | | A6 | §17 | `GH-DEC-2026-004` | T07 | | A7 | §11, §12 | four observed instances; proposed by `approval-engine` | T08 | +| A8 | §6.4, §13.1 | `GH-DEC-2026-009` | T09 | --- @@ -130,12 +131,13 @@ one. **§13.1 rows to transcribe at the cut** — carried here so the cut is mechanical: -| Consumer | Stance map | Shape | -| --- | --- | --- | -| `ops-warden` | `ops-warden/pep-stance.yaml` | total per zone; `fail_open` for `z0`–`z2` and unknown, sanctioned by §9.3 and recorded per §6.4 obligation 1 limb two; test asserts published map equals shipped default | -| `user-engine` | `user-engine/pep-stance.yaml` | total per zone; `fail_closed` for `z0`–`z3`, unknown, and not-applicable; test asserts published map equals `user_engine.pep_stance` | -| `tenant-engine` | `tenant-engine/pep-stance.yaml` | total; `fail_closed` for unset, unreachable, non-allow, unknown; test asserts file equals shipped behaviour | -| `ops-mason` | — | **unpublished**; PEP-shaped under §6.4 (opening a route). Recorded as a gap, not a stance | +| Consumer | Stance map | Axis | Shape | +| --- | --- | --- | --- | +| `ops-warden` | `ops-warden/pep-stance.yaml` | security zone | total per zone; `fail_open` for `z0`–`z2` and unknown, sanctioned by §9.3 and recorded per §6.4 obligation 1 limb two; test asserts published map equals shipped default | +| `user-engine` | `user-engine/pep-stance.yaml` | security zone | total per zone; `fail_closed` for `z0`–`z3`, unknown, and not-applicable; test asserts published map equals `user_engine.pep_stance` | +| `tenant-engine` | `tenant-engine/pep-stance.yaml` | security zone | total; `fail_closed` for unset, unreachable, non-allow, unknown; test asserts file equals shipped behaviour | +| `secrets-engine` | `secrets-engine/pep-stance.yaml` | catalog stage — interim, pending zone membership as a claim | total over stage plus unknown; no implicit default; runtime-read; pinned to `SHIPPED_STANCE` by test | +| `ops-mason` | — | — | **unpublished**; PEP-shaped under §6.4 (opening a route). Recorded as a gap, not a stance | Three of four are total and fail-closed. The `ops-mason` row is the finding §6.4's closing paragraph anticipated, and it is more useful in the register than absent from @@ -338,6 +340,58 @@ as stronger than it is, which would be the same defect one layer up. --- +## A8 — §6.4 and §13.1, `unknown` and the scoping axis (T09) + +Settled by `GH-DEC-2026-009`, on the first occasion §13.1 held enough rows to diverge. + +**Add to §6.4 obligation 3:** + +> **`unknown` is not a zone and MUST resolve to `fail_closed`.** §9.3 permits trading +> availability for openness *per zone* — knowingly, for a named scope, at a declared +> cost. That trade requires knowing the zone. Where the scope is unknown it cannot +> have been made for this request, so resolving `unknown` permissively does not extend +> a considered decision, it invents the most permissive one available. +> +> An unreachable engine and an unclassified subject are different failure cases. The +> first is a known request in a degraded system and the trade is available for it. The +> second is not: `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 — and a permissive `unknown` makes being +> unclassifiable a privilege escalation requiring no credential. §8's asymmetry +> forbids that wherever it appears. +> +> Zone stances are untouched by this. Raised by `access-engine` as an observation on +> two conformant maps that disagree, and explicitly not as a request that either +> consumer change its stance — a PDP does not set a consumer's stance. + +**Add to §6.4 obligation 3, and to the §13.1 register columns:** + +> A published map MUST name the **axis** it scopes over and state its relationship to +> security zone: either a mapping, or an explicit declaration that none exists yet and +> why. §13.1 carries the axis per row. +> +> "Per zone **or equivalent scope**" permits axes that cannot be aggregated, and the +> register cannot then answer *"what is the estate's stance for a `z2`-protected +> workload"* — the question an inventory exists to answer. Forcing every consumer onto +> zones is the wrong repair where zone membership is not yet available as a claim; +> asserting a zone one cannot know is a fiction, and a fiction in a runtime-read, +> test-pinned file is worse than an honest incommensurability. **The register therefore +> records the axes and states that cross-axis aggregation is unavailable**, rather than +> implying an answer it does not have. An inventory that silently cannot answer its own +> question is §9.1's defect in another place. +> +> The convergence path is zone membership reaching the decision as a claim, after +> which a stage-scoped map converges onto zones without either side inventing a +> mapping. Until then the axes are recorded, not reconciled. + +**Register impact at the cut.** `ops-warden`'s `unknown` → `fail_open` cell becomes +non-conformant under this amendment; its `z0`–`z2` stances do not. That is a real cost +to the repository that published first, built the reference form, and offered it +estate-wide, and it is put to the assent round rather than imposed — the A3 row stays +as written until v0.8 is accepted. + +--- + ## Not in this set Recorded so the omissions are deliberate rather than forgotten. diff --git a/workplans/GH-WP-0003-statute-v08-amendment-set.md b/workplans/GH-WP-0003-statute-v08-amendment-set.md index 3485d07..842d252 100644 --- a/workplans/GH-WP-0003-statute-v08-amendment-set.md +++ b/workplans/GH-WP-0003-statute-v08-amendment-set.md @@ -183,3 +183,23 @@ them and no staleness marker on the derivatives. Drafted as A7 — a §11 marking obligation on the publisher and a §12 paragraph on the consumer. The limit is stated in the draft: marking makes staleness visible, it does not detect a marked derivative that is still wrong. + +```task +id: GH-WP-0003-T09 +status: done +priority: high +``` + +**`unknown` and the scoping axis (A8).** `access-engine` exercised the divergence +capability it claimed in the v0.6 round, on the first occasion §13.1 held two rows. +`ops-warden` resolves `unknown` to `fail_open` and `secrets-engine` to `fail_closed`; +both maps are conformant, total and test-pinned, and they disagree about the case +nobody planned for. The two maps also scope over different axes, so the register +cannot answer the question an inventory exists to answer. + +Settled as `GH-DEC-2026-009` and drafted as A8: `unknown` is not a zone and fails +closed, because the §9.3 trade requires knowing the zone and a permissive `unknown` +makes being unclassifiable a privilege escalation needing no credential; and each map +declares its axis and its relation to zone, with the register stating what it cannot +answer rather than implying it can. `ops-warden` acquires one non-conformant cell at +v0.8 and it goes to the assent round rather than being imposed.