diff --git a/canon/standards/security-layer-model_v0.8.md b/canon/standards/security-layer-model_v0.8.md index da22557..8061b8a 100644 --- a/canon/standards/security-layer-model_v0.8.md +++ b/canon/standards/security-layer-model_v0.8.md @@ -9,8 +9,8 @@ supersedes: canon/standards/security-layer-model_v0.7.md owner: gate-house publication_owner: net-kingdom created: "2026-08-28" -updated: "2026-09-07" -last_reviewed: "2026-09-07" +updated: "2026-09-09" +last_reviewed: "2026-09-09" review_interval: 3m source_revision: "gate-house@516ed4e" standard_token: security-layer-model_v0.8 @@ -69,8 +69,14 @@ requiring no credential. Three close a gap between rules already made. §6.4 gains validation by owning layer and the rule that correspondence between two artifacts is established by identity rather than translation. §11 gains an emission-guarantee declaration, so -`GH-IN-0001` cannot recur unnoticed. §13.1 gains three rows, a scoping axis, and -an honest statement of what it cannot answer. +`GH-IN-0001` cannot recur unnoticed. §13.1 gains three rows, a scoping axis, a +coverage figure, and an honest statement of what it cannot answer. + +Nine more corrections landed during circulation, listed as §15 items 12–20 and +dispositioned in `gate-house/docs/conformance/2026-09-06-v08-assent-round.md`. The +load-bearing one is §6.4 obligation 1: a PEP must be able to *attribute* a +decision to `access-engine`, and no digest comparison does that. Fail-closed +protects against a decision point that is absent, not against one that lies. One is a defect of the same kind this standard has now corrected repeatedly: §17 stated that emission-cadence ownership was unassigned and unassented after it had @@ -439,7 +445,7 @@ not a repository: `ops-warden` issuing a certificate, `ops-mason` opening a route, and any protected system acting on a verdict are all PEP-shaped. Being PEP-shaped does not move a repository out of its layer. -Four obligations, and they are normative: +Five obligations, and they are normative: 1. **No side effect without a decision record, or a recorded stance.** A PEP MUST NOT perform the protected action unless it holds a decision from @@ -450,6 +456,46 @@ Four obligations, and they are normative: metadata, never an absent record. `ops-warden` `ca.py` writes the zone, the failure mode, and a decision id present only where a decision was rendered. + **Held is not attributed, and this obligation requires both.** The decision a + PEP holds MUST be *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**, + and it cannot. Every input to that comparison is either sent by the caller or + published: the request material is what the PEP transmitted, and the policy + and registry digests are computable from a public repository. A responder + knowing a published package id and version reproduces all of it and returns a + well-formed allow, and each further digest published makes a forged envelope + look more authenticated rather than less. + + **Fail-closed protects against a decision point that is absent, not against + one that lies.** §9.3's whole apparatus — two failure cases, two owners, + published stance maps — addresses absence. An unreachable PDP denies; a + responder impersonating one allows. The asymmetry with obligation 5 is the + sharp form: §9.4 requires the approval object to have durable, authenticated + entries, so a PEP can validate the approval artifact's authenticity and + cannot yet validate the decision artifact's. Obligation 5 is written over + both halves of that pair and is today satisfiable for only one of them. + + This obligation is therefore **not discharged by a digest comparison**, and an + implementer MUST NOT read obligation 2's mechanical test as discharging it. + The channel is unauthenticated today — `flex-auth.decision-record.v1` carries + no signature and pins serve plain HTTP — so the obligation stands with a + declared gap in §13 rather than with a shipped mechanism, owner + `access-engine` (`FLEX-DEC-2026-010`, `FLEX-WP-0024`). The signature scheme is + the owner's under §17; the standard names the property, not the mechanism. A + declared gap is honest; an unstated assumption inside a test called mechanical + is not. Raised by `access-engine` against its own artifact, having recorded it + as its own defect before reviewing this text; put the same way independently by + `secrets-engine`, whose posture *"silently assumes the PDP is the PDP"*, and + carried forward by `approval-engine`, which observed that an approval whose + `pdp_digest` matches a **forged** decision matches perfectly. + + §16 had already recorded this observation one layer up, against this + standard's own publication path having no digest, freeze, or rollback + discipline. Applied to the artifact the standard regulates rather than to the + standard, it is this paragraph: the gap was visible from inside and was + recorded against the wrong artifact. + v0.6's unqualified form made the shipped stance §9.3 sanctions into a violation — the same defect as v0.5's §9.1, a rule written for the clean case producing a false result on the adjacent case already sanctioned elsewhere. @@ -482,11 +528,18 @@ Four obligations, and they are normative: rather than held in code comments or in a dataclass default. `ops-warden` `ADR-0009` and its `pep-stance.yaml` are the reference shape. - The published map MUST equal the shipped behaviour, and that equality SHOULD - be asserted by a test. A published map free to drift from the code is worse - than none, because it invites reliance it cannot support. Raised by - `ops-warden`, which found its own map unpublished while being cited as the - reference for this obligation. + The published map MUST equal the shipped behaviour, and that equality MUST be + asserted by a test. A published map free to drift from the code is worse than + none, because it invites reliance it cannot support. Raised by `ops-warden`, + which found its own map unpublished while being cited as the reference for + this obligation. + + The test was a `SHOULD` through v0.8's circulation, which left the strongest + obligation in this section with the weakest verification: a paragraph arguing + that drift is worse than no publication, whose only detector of drift was + optional, sanctioning the failure exactly where the argument says it is worst. + Promotion costs nothing — §13.1's `Shape` column already records such a test + for four of five rows. Raised by `access-engine`. **`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, @@ -504,6 +557,57 @@ Four obligations, and they are normative: privilege escalation requiring no credential. §8's asymmetry forbids that wherever it appears. Zone stances are untouched by this rule. + **A map MUST enumerate its axis, and an `unknown` cell MUST NOT be what makes + it total.** Totality satisfied by a catch-all is satisfied *vacuously*: every + scope the author never enumerated lands in `unknown`, fails closed, and nobody + ever learns which those were. The map is then total by having a default rather + than by covering its axis, and the drift test above passes by exercising the + catch-all instead of the axis. `access-engine` recognised the shape because it + published it — a policy package shipped with no tenant rule at all while 29 + fixtures passed, because every fixture carried the same tenant + (`FLEX-DEC-2026-008`). A suite that never varies an input cannot report on it, + however many assertions pass; a stance map with a catch-all cannot report which + scopes were never enumerated, however green its test. + + **`unknown` and `absent` are one runtime behaviour and two meanings, and the + record MUST distinguish them.** A scope value the map classifies as `unknown` + and a scope value the map does not enumerate at all both fail closed — + `absent` for a stronger reason than `unknown`, 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 + that this obligation is violated. An `absent` hit MUST therefore be + distinguishable in the record from an `unknown` hit, and MUST surface as a + conformance failure rather than be absorbed by the catch-all. This answers the + question §16 opened at the v0.8 cut, and closes it. Raised by `access-engine`. + + **Classification coverage is published alongside the stance, and does not + soften it.** A row reading `unknown` → `fail_closed` while most of that + consumer's targets resolve to no scope at all is conformant and materially + misleading: a reader cannot distinguish a strict consumer from an unclassified + one, and the map becomes accurate about itself while inaccurate about its + effect — §11's published-map-equals-shipped-behaviour rule one level up. + §13.1 therefore records a dated coverage figure beside each stance. + + Coverage is disclosure, **not** a transitional licence. `ops-warden` asked + whether this obligation could name a dated, published transitional + `unknown: fail_open` converting on coverage rather than on calendar, having + measured zero of three signing targets and three of twenty-one routing lanes + resolved to a zone: adopting the cell today would fail closed on essentially + every certificate it issues whenever `access-engine` is unreachable, including + the continuity path an operator needs in order to repair that unreachability. + **Declined.** A sanctioned transitional `fail_open` is indistinguishable at + runtime from the stance this rule forbids, and would make the rule optional at + exactly the moment of adoption — the only moment it costs anything. §11's + declared-gap mark already expresses *"correct rule, adoption not yet + affordable"* without inverting the rule's effect, and `ops-warden` proposed + that outcome as its own second preference. The deadlock it names is real, and + is closed by classifying continuity paths into a scope whose stance is open: + that is classification work, not a reason to hold the axis open. A stricter + stance is equally not a licence to manufacture the membership that makes it + survivable — where a scope is unknown because another repository has published + no workload-identity declaration, the consumer MUST NOT infer one. + **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. *"Per zone or equivalent scope"* permits axes @@ -1214,6 +1318,7 @@ register distinguishes proposed from assented. | Identity and secret observation | unowned-capability | kings-guard | as above | proposed | | Stance-map register had no implementation | declared-contact | ops-warden, access-engine | gate-house | resolved in §13.1 | | Registry-snapshot digest in decision provenance | declared-contact | flex-auth | flex-auth | self-declared | +| Decision-record authenticity — a PEP cannot attribute a decision to `access-engine` (§6.4 obligation 1); unsigned envelope, plain-HTTP pins | declared-contact | flex-auth | flex-auth | self-declared (`FLEX-DEC-2026-010`, `FLEX-WP-0024`) | | Approval storage and lifecycle | — | flex-auth | approval-engine | assigned (§9.4) | | Approval evidence | — | gate-house | audit-core | **assented** (`AUDIT-IN-0001`) | | Approval evidence custody stronger than the shipped bound — WORM, object lock, transparency log | unowned-capability | audit-core | — | unassigned | @@ -1254,13 +1359,28 @@ the register's real rate of change. Every PEP-shaped consumer publishes an unreachable-engine stance map (§6.4, obligation 3). This is the inventory until `maturity-engine` can hold it. -| Consumer | Stance map | Axis | Shape | -| --- | --- | --- | --- | -| `ops-warden` | `ops-warden/pep-stance.yaml` | security zone | total per zone; open `z0`–`z2`, closed `z3-critical`; test asserts the published map equals the shipped default (`ADR-0009`). **`unknown` → `fail_open` is non-conformant under §6.4 obligation 3 as of this version** | -| `user-engine` | `user-engine/pep-stance.yaml` | security zone | total per zone; `fail_closed` for `z0`–`z3`, unknown, and not-applicable; test asserts the 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 the 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` | — | — | **not published**; catalogued PEP-shaped in §4 | +| Consumer | Stance map | Axis | Coverage | Shape | +| --- | --- | --- | --- | --- | +| `ops-warden` | `ops-warden/pep-stance.yaml` | security zone | signing targets 0/3 resolved; routing lanes 3/21 resolved (2026-09-09, self-measured) | total per zone; open `z0`–`z2`, closed `z3-critical`; test asserts the published map equals the shipped default (`ADR-0009`). **`unknown` → `fail_open` is non-conformant under §6.4 obligation 3 as of this version**; route `WARDEN-WP-0040` | +| `user-engine` | `user-engine/pep-stance.yaml` | security zone | not reported | total per zone; `fail_closed` for `z0`–`z3`, unknown, and not-applicable; test asserts the published map equals `user_engine.pep_stance` | +| `tenant-engine` | `tenant-engine/pep-stance.yaml` | security zone | not reported | total; `fail_closed` for unset, unreachable, non-allow, unknown; test asserts the file equals shipped behaviour | +| `secrets-engine` | `secrets-engine/pep-stance.yaml` | catalog stage — interim, pending zone membership as a claim | not reported | total over stage plus unknown; no implicit default; runtime-read; pinned to `SHIPPED_STANCE` by test | +| `ops-mason` | — | — | — | **not published — non-conformant under §6.4 obligation 3**; catalogued PEP-shaped in §4; no route recorded | + +**Two rows are marked, and the marking is the point.** `ops-warden`'s cell is +wrongly valued in a published map; `ops-mason` has published no map at all, which +is the plainer violation of the same obligation and read as a bare statement of +fact through v0.8's circulation. A reader scanning for the bold marks would have +found one row and concluded the other four were fine. That is §11's marking +obligation applied to this standard's own register, and it was returned to +`gate-house` unchanged by `access-engine`, which had received the same argument +about a stale row of its own. + +**Coverage is reported by the consumer and is not inferred here.** A blank is +"not reported", never "complete": the register does not compute another +repository's classification coverage, and a stance with no coverage figure beside +it says less than it appears to. §6.4 obligation 3 states why the figure belongs +next to the stance, and why it does not soften it. **Cross-axis aggregation is unavailable.** Three maps scope by security zone and one by catalog stage, which §6.4 permits as an equivalent scope. The register @@ -1300,13 +1420,20 @@ Two things that acceptance does and does not mean, kept apart because | --- | --- | | **Boundary assent** | given by the four repositories below, at the version named in each record, and undisturbed since | | **Revision review** | v0.7's changes were each the adopted remedy of a v0.6 finding. All fifteen v0.6 findings were subsequently audited against the v0.7 body — not against its change log — and confirmed dispositioned (`gate-house/docs/conformance/2026-09-06-v06-findings-audit.md`) | -| **Not claimed** | no repository has yet reviewed v0.8 *as text*, apart from `net-kingdom`'s review of §11 and §17 against the profile it owns (2026-09-07: §17 confirmed in its own voice, §11 corrected). This version is circulated for that review before acceptance | +| **Reviewed as text** | v0.8's circulated round returned findings from `access-engine`, `approval-engine`, `ops-warden`, and `net-kingdom`, and every finding is dispositioned in `gate-house/docs/conformance/2026-09-06-v08-assent-round.md`. Nine corrections landed in this text during circulation (§15 items 12–20) and one request was declined with its reasons in §6.4 | +| **Not claimed** | `kings-guard` and `audit-core` have not returned a text review; the sections each was asked to attack — §9.5's criteria-grounding clause and §12's step-four paragraph for the first, §11's emission-guarantee wording for the second — carry no assent from the repository best placed to test them. `approval-engine` named the sections it did **not** read (§1–§4, §7, §8, §9.1–§9.2, §9.5–§9.6, §9.8, §10, §18, §20) rather than let a three-finding review read as a clean bill | -**Ten of this version's eleven changes were requested by another repository**, -seven of them by a repository arguing against its own interest or reporting its -own error. The exception is §17's ownership correction, which `gate-house` found -by auditing its own accepted text. That ratio is the reason the standard is +**Ten of this version's twelve changes as cut were requested by another +repository**, seven of them by a repository arguing against its own interest or +reporting its own error. There are two exceptions, not one: §17's ownership +correction, which `gate-house` found by auditing its own accepted text, and item +2b, which `access-engine` flagged as a near miss it was explicitly *not* asking to +have written down. The count was stated as ten of eleven until `approval-engine` +observed that item 2b's numbering left a reader unable to tell whether it was a +change or a sub-clause — and that the ambiguity moved both halves of a ratio §14 +calls the argument rather than the background. The nine corrections applied during +circulation (items 12–20) were all requested by another repository. That ratio is the reason the standard is circulated rather than accepted on the owner's decision as v0.7 was: this version imposes costs on named repositories — `ops-warden` acquires a non-conformant stance cell, `approval-engine` acquires an issue-time obligation — and a cost @@ -1359,7 +1486,10 @@ v0.7 → v0.8: within hours of the ruling, which as first written mandated a check that could never pass (`GH-DEC-2026-008`, raised by `access-engine`, which declined to close it locally). -2b. **§6.4 obligation 5 gains the replay-identity property** — an evidence-bearing +2b. **A change in its own right, not a sub-clause of 2** — the label is retained + rather than renumbered so that references written against this list do not + silently repoint. §14's tally counts it. **§6.4 obligation 5 gains the + replay-identity property** — an evidence-bearing input may be excluded from a correspondence digest but never from the replay identity, because two requests differing only in which approval was presented decide differently. Flagged by `access-engine` as a near miss rather than a @@ -1406,6 +1536,53 @@ v0.7 → v0.8: 11. **§16 reconciled** against the decision log; two questions closed, one opened on whether an absent scope differs from an unknown one. +Applied during circulation, after the round returned findings against this text +(`gate-house/docs/conformance/2026-09-06-v08-assent-round.md`). The version stayed +`proposed` throughout, so these are corrections to an uncut standard rather than +amendments to an accepted one: + +12. **§6.4 obligation 1 gains attribution** — a decision MUST be attributable to + `access-engine`, and obligation 2's mechanical digest test does not discharge + that. Fail-closed protects against a decision point that is absent, not + against one that lies, and obligation 5 was written over a pair of artifacts + whose authenticity only one half of could be validated. Carried as a declared + §13 gap with `access-engine` as owner rather than as a shipped mechanism + (raised by `access-engine` against its own unsigned envelope, having recorded + it as `FLEX-DEC-2026-010` before reviewing this text). +13. **§6.4 obligation 3's drift test promoted `SHOULD` → `MUST`** — the strongest + obligation in the section had the weakest verification, in a paragraph arguing + that drift is worse than no publication. Already met by four of five §13.1 + rows (raised by `access-engine`). +14. **§6.4 obligation 3 forbids totality by catch-all**, and requires an `absent` + scope to be distinguishable in the record from an `unknown` one and to surface + as a conformance failure. This closes the question item 11 opened (raised by + `access-engine`, from its own 29-fixture tenant defect). +15. **§6.4 obligation 3 and §13.1 gain classification coverage** — published + beside the stance, because `unknown` → `fail_closed` over a population that is + entirely unclassified is conformant and misleading. A transitional + `unknown: fail_open` was asked for and **declined**: it is indistinguishable at + runtime from the stance the rule forbids (requested by `ops-warden`, which + proposed the declared-gap outcome as its own second preference and measured the + coverage figure now in the register). +16. **§13.1 marks `ops-mason`** — an unpublished map is a plainer violation of + obligation 3 than a wrongly-valued cell in a published one, and was stated as + bare fact while the other was bolded (raised by `access-engine`, returning + `gate-house`'s own §11 marking argument unchanged). +17. **§14's tally corrected and §15 item 2b clarified** — 2b is a change in its own + right and is the second item not requested by another repository, so the ratio + is ten of twelve with two exceptions (raised by `approval-engine`). +18. **§6.4's count sentence corrected** — it announced four obligations and listed + five, leaving a defensible reading under which an implementer omits the one + governing how an approval is compared to a decision (raised by + `approval-engine`). +19. **§17's artifact count and closing status corrected** — v0.8's item 10 reached + the emission-cadence paragraph and not the count sentence, which still called + ownership of the decision-record schema unsettled after `access-engine` took it + on (raised by `approval-engine`). +20. **§19 gains a stub** — the heading gap was explained in a §16 bullet, where a + reader checking whether an edit had dropped a section does not look (raised by + `approval-engine`). + v0.1 → v0.2: 1. **§5 restructured** into three sanctioned shapes. Added §5.2 conduit @@ -1597,10 +1774,15 @@ v0.3 → v0.4: both registers as data; the migration waits on a committed, versioned export readable without a live query, because a standard must stay legible in git to a reader with no cluster access. `GH-DEC-2026-006`. -- Whether §6.4's `unknown` ruling should extend to other total-map scopes that - are absent rather than unknown — a scope a consumer has never enumerated is - not the same as one it cannot classify, and the standard does not yet - distinguish them. +- ~~Whether §6.4's `unknown` ruling should extend to other total-map scopes that + are absent rather than unknown.~~ **Closed** during v0.8's circulation. It + extends, and the distinction is not a distinction in the stance: `absent` fails + closed too, for the stronger reason that it is the branch reached by discovering + the author's model of their own axis was wrong. The distinction is in the + record — an `absent` hit is a conformance failure and MUST be distinguishable + from an `unknown` hit, or obligation 3's totality requirement is satisfied + vacuously by a catch-all. §6.4 obligation 3; answered by `access-engine`, which + raised the question's real cost from its own 29-fixture tenant defect. - Whether Tooling warrants subdivision between third-party and homegrown. - How a future `role-engine` divides responsibility with `access-engine`. - Whether declared gaps need an estate-wide register rather than per-repository @@ -1613,7 +1795,12 @@ v0.3 → v0.4: the two rules stated. - Publication integrity of the Taxonomy layer itself. This standard demands reconstructability of decisions while its own publication path has no digest, - freeze, or rollback discipline. + freeze, or rollback discipline. `access-engine` observed during v0.8's + circulation that the same observation applied to the artifact this standard + *regulates* is §6.4 obligation 1's attribution gap — the defect was visible from + inside and had been recorded against the wrong artifact. That half is now a + declared §13 gap; this entry is the half that remains open, and it is this + standard's own. - The fitness verdict formerly at §19 now lives in `net-kingdom/history/2026-08-29-layering-standard-assessment.md`. A grade inside a standard of record becomes normative by adjacency and ages against the @@ -1637,8 +1824,11 @@ rule. As prose that is a rule a reviewer can apply. As an interface it does not exist, because nothing defines what a claim *is*. §11 calls itself mechanically checkable while resting on that gap. -Four artifacts are therefore required, owned by Taxonomy and versioned like any -standard: +Four artifacts are therefore required and versioned like any standard. **Two of +the four are not Taxonomy's**, and each is settled below rather than open: the +decision-record schema is `access-engine`'s, and the emission-cadence declaration +is split between `info-tech-canon` and `net-kingdom`. The table names all four +because §6.2 needs all four to exist, not because Taxonomy owns all four. | Artifact | Contents | | --- | --- | @@ -1686,11 +1876,19 @@ voice (`ITC-WP-0018`, publishing `ITC-EMISSION-CADENCE 0.1` in canon `0.7.0`; `NK-WP-0035`, publishing `emission-cadence-security-profile_v0.1.md`). The `kings-guard` draft is frozen as assimilation provenance. -**Ownership of the remaining three artifacts is still proposed.** The -request-claim and gap-record schemas sit between `info-tech-canon` and -`net-kingdom` on the same unsettled line; the decision-record schema is -`access-engine`'s, per above. §2 keeps ownership in the owning repository's -`INTENT.md`. +**Exactly two artifacts are unsettled: the request-claim schema and the +gap-record schema.** Both sit between `info-tech-canon` and `net-kingdom` on the +same line, and neither has an owner in its own voice yet. The other two are +settled above — the decision-record schema is `access-engine`'s, and the +emission-cadence declaration is assigned and accepted by both its owners. §2 keeps +ownership in the owning repository's `INTENT.md`. + +This paragraph read *"the remaining three artifacts is still proposed"* while +settling one of the three in its own next clause, so a reader checking whether the +decision-record schema needed an owner found it listed among the unsettled — after +`access-engine` had taken it on against its own interest. Raised by +`approval-engine`; v0.8's item 10 corrected the emission-cadence paragraph and did +not reach this one. ## 18. Composition with the sibling standards @@ -1726,6 +1924,17 @@ appears in the decision record.** --- +## 19. *(retired)* + +There is no §19. It held a maturity grade for `qonto-assistant`, removed because a +grade inside a standard of record becomes normative by adjacency — it is an +assessment, and assessments belong in `maturity-engine` and in dated review +records. The number is not reused, so references written against earlier versions +do not silently repoint. Retirement recorded in §16 and in +`gate-house/history/`; raised by `access-engine`, and the stub added because a +reader who sees §18 followed by §20 cannot otherwise tell whether an edit dropped +a section (raised by `approval-engine`). + ## 20. The Railiance interaction boundary Operations is not NetKingdom's. Workload operations are organized by