From f6376ddff5e49c0f883369394ee8770638013212 Mon Sep 17 00:00:00 2001 From: tegwick Date: Wed, 9 Sep 2026 22:25:35 +0200 Subject: [PATCH] Declare the layer per GH-DEC-2026-012; close T02 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate House ruled all three questions within a day, attributing the speed to the request being filed before the architecture with candidate answers and their costs. R1 PEP-shaped, confirmed as proposed. The ruling settles the shape; the layer stays ours to declare, so layer.yaml is written in this repository's voice rather than transcribed from the reply. R2 yes to a presentation claim, no second catalog row, under three limits now declared in layer.yaml and tested. Limit 2 — the claim must never be an input to the decision it presents for — is load-bearing: our self-dealing argument was accepted because it holds, not despite it. Limit 3 drives architecture, since here the actor being audited and the evidence source are the same component. R3 (b) with the authority rule: binding digest authoritative for what the request is, view_hash only for what was shown, neither substitutable, and a disagreement between them is a finding against the presenting surface rather than a fact about the request. Linkage is co-reference; nesting was refused because it reproduces the GH-DEC-2026-008 hash cycle. Built to v0.8 obligation 3 rather than migrating later: axis enumerated, unknown resolves to fail_closed, absent distinguishable from unknown in the record, and published-equals-shipped asserted by test rather than claimed. Every stance is fail_closed, which is a conclusion not a shortcut — ops-warden can justify fail_open on a continuity argument that does not exist here. GH-DEC-2026-010 inherited as a declared gap in four documents: a decision cannot today be proven to have come from access-engine. The decision path must not be described as validated while FLEX-WP-0024 is open. 46 tests pass. T05 and T07 unblocked. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01V3W1dQG7GFFM9d94jFx7iR Assistant: claude-code Assistant-Model: opus Assistant-Process: 1565372@bnt-lap001 Assistant-Session: 16bb2f25-b34c-49ef-8e94-5fec3567a568 --- GOAL.md | 24 ++- INTENT.md | 76 ++++++-- .../__pycache__/stance.cpython-312.pyc | Bin 0 -> 2278 bytes informed_decision/stance.py | 56 ++++++ layer.yaml | 184 ++++++++++++++++++ pep-stance.yaml | 110 +++++++++++ ...r_conformance.cpython-312-pytest-7.4.4.pyc | Bin 0 -> 21688 bytes tests/test_layer_conformance.py | 182 +++++++++++++++++ ...ounding-specs-and-approver-ui-ownership.md | 2 +- 9 files changed, 614 insertions(+), 20 deletions(-) create mode 100644 informed_decision/__pycache__/stance.cpython-312.pyc create mode 100644 informed_decision/stance.py create mode 100644 layer.yaml create mode 100644 pep-stance.yaml create mode 100644 tests/__pycache__/test_layer_conformance.cpython-312-pytest-7.4.4.pyc create mode 100644 tests/test_layer_conformance.py diff --git a/GOAL.md b/GOAL.md index 35612ad..e3cbe40 100644 --- a/GOAL.md +++ b/GOAL.md @@ -85,6 +85,11 @@ These hold for Stage 1 and for every stage after it. - **`approved` is never rendered as permission to act.** It is a state of an object. Presenting it as "you may now do the thing" is a policy decision point in the browser. +- **The decision path is not validated, and must not be described as such.** + `GH-DEC-2026-010` requires a decision be attributable to `access-engine`, and + no consumer can satisfy that today — the envelope is unsigned + (`FLEX-WP-0024`). This surface records that a decision was obtained and what + it said, and records `decision_attributable: false`. - **Entitlement to view is `access-engine`'s.** A `200` from `approval-engine` is not permission to see the approval; that engine never answers "may this actor do X". This surface obtains a decision before rendering and never @@ -108,7 +113,9 @@ These hold for Stage 1 and for every stage after it. > **Who owns the approver UI?** -The answer this repository proposes, to be ratified rather than assumed: +**Answered 2026-09-09 by `GH-DEC-2026-012`.** The proposal below was confirmed +in full. Recorded as proposed-then-ratified rather than rewritten as though it +had always been settled: - **informed-decision owns it** — the surface, the presentation record, the browser client, and the evidence of informedness. @@ -117,13 +124,20 @@ The answer this repository proposes, to be ratified rather than assumed: and statute §6.4. - It supplies exactly one PIP-like fact — *what was presented* — as a claim carrying `view_hash`, issuer and freshness, and it never evaluates that fact. -- The catalog row does not exist yet. `INFD-WP-0001-T02` asks `gate-house` and - writes `layer.yaml` from the ruling. If `gate-house` places this component - elsewhere, `INTENT.md` and this file change to match — the ruling wins. +- **Ruled:** PEP-shaped, confirmed as proposed. No second catalog row for the + presentation claim — PEP and PIP are shapes a repository has. The claim is + permitted under three limits (presentation-only; never an input to the + decision it presents for; evidence reaches `audit-core` independently), and + `view_hash` versus the binding digest is settled as distinct attestations with + an authority rule, linked by co-reference and never by nesting. + `layer.yaml` and `pep-stance.yaml` declare it; `INTENT.md` carries the + reasoning. ## Definition of done -1. `gate-house` has ruled on the layer placement and `layer.yaml` reflects it. +1. ~~`gate-house` has ruled on the layer placement and `layer.yaml` reflects + it.~~ **Done 2026-09-09** — `GH-DEC-2026-012`; `layer.yaml`, + `pep-stance.yaml` and `tests/test_layer_conformance.py`. 2. `key-cape` has the `client_id` and callback URI, and `KEY-WP-0013-T02` is unblocked with the answer traceable to this repository. 3. The four specs exist, are reviewed against the current `approval-engine`, diff --git a/INTENT.md b/INTENT.md index 6cf03c1..be508a3 100644 --- a/INTENT.md +++ b/INTENT.md @@ -7,7 +7,7 @@ date: "2026-09-09" domain: infotech topic_slug: netkingdom layer: surface -role: pep-shaped # PROVISIONAL — see "Layer Placement", INFD-WP-0001-T02 +role: pep-shaped # confirmed by GH-DEC-2026-012; see layer.yaml standard: net-kingdom/canon/standards/security-layer-model_v0.7.md companion: net-kingdom/SECURITY-COMPANION.md --- @@ -184,21 +184,69 @@ not failure** — a memo sent back for improvement is the surface working. presented view plus explicit highlight acknowledgment — never keystroke analytics, dwell timers, or attention theater. -## Layer Placement — Provisional +## Layer Placement — Ruled -Under security layer model v0.7 this repository is **not** an Engine. It is -browser-facing and it causes a protected side effect on the far side of a -decision, which is the shape statute §6.4 and companion §5 call **PEP-shaped**. -`approval-engine`'s own INTENT names this expectation: *"Callers that do are -PEP-shaped and owe companion §5 / statute §6.4."* +**PEP-shaped**, confirmed by `GH-DEC-2026-012` (answering `INFD-IN-0001`). +Not an Engine: this repository holds no state another layer reads at runtime for +a verdict, and renders no decision. Companion §5 is owed and statute §6.4 +applies in full. The declaration is `layer.yaml`, in this repository's own voice +— a layer someone else states about you is not a declaration. -This is declared **provisional** because the catalog row does not exist yet and -inventing one is the drift §17 exists to prevent. `INFD-WP-0001-T02` takes the -question to `gate-house` and writes `layer.yaml` from the answer, not from this -paragraph. The framing this repository will argue: informed-decision is a -**presentation and binding surface** — a PEP for the acts it fronts, and a PIP -for exactly one fact, *what was presented*, which it supplies as a claim and -never evaluates. +The ruling also confirmed that emitting a **presentation claim** does not +require a second catalog row: PEP and PIP are shapes a repository has, and §4 +records the layers it occupies. That permission carries three limits, and they +are the substance of it rather than caveats on it: + +1. **The claim carries presentation and nothing else.** It must never carry, + restate, summarise or imply the decision, the verdict, or whether the act was + permitted. A consumer learns from it only what was *shown*, never what was + *decided*. +2. **The claim must not be an input to the decision it presents for.** A policy + reading `view_hash` to decide whether an act is permitted would let the + presenting surface contribute to its own authorization. +3. **The evidence copy reaches `audit-core` independently of this repository.** + The claim endpoint and the evidence path are different things and neither + substitutes for the other. + +Limit 2 is load-bearing. `GH-DEC-2026-012` accepted this repository's argument +that a renderer attesting its own rendering is not the self-dealing that kept +the approval object out of `access-engine` — *but only because that limit +holds*. Without it, the two collapse into the same failure. + +Limit 3 is the one that most shapes the architecture: audit evidence is +protected from the actor being audited, and here the actor and the source are +the same component. + +### `view_hash` and the binding digest + +Ruled as option (b): they are **distinct attestations with an authority rule**. + +- `approval-engine`'s binding digest is authoritative for **what the request + is**. +- `view_hash` is authoritative for **what was shown**, and nothing else. +- Neither may be substituted for the other. +- **A disagreement between them is a finding against the presenting surface, + never a fact about the request.** + +They link by **co-reference**, not nesting: the presentation record carries the +approval or binding identifier explicitly, and both attestations are read +against that one reference. This repository must never recompute or restate +`approval-engine`'s binding digest in its own vocabulary — it references the +digest that layer computed and recorded. + +Nesting was refused for a reason worth carrying: it reproduces the hash cycle +that made `GH-DEC-2026-008` unimplementable, where a claim had to name the +digest of a request that would come to contain it, and a fail-closed consumer +obeying it would deny permanently. + +### An inherited gap, stated rather than glossed + +`GH-DEC-2026-010` requires a decision be **attributable** to `access-engine`. +No consumer can satisfy that today: the decision envelope is unsigned, a +declared §13 gap tracked as `FLEX-WP-0024`. This repository's record can show +that a decision was obtained and what it said; it cannot yet show that +`access-engine` said it. The decision path must not be described as validated +while that is open. ## What Would Make This Repository Wrong diff --git a/informed_decision/__pycache__/stance.cpython-312.pyc b/informed_decision/__pycache__/stance.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..22561511aa630ee4b7c66f06f2849d116010c4c8 GIT binary patch literal 2278 zcmZWqO>7fK6rQy=US}QKfJg{GZ70wQEX58DQq?4=QlM$5KyI6a{7tAbi(&@7?@cMvVIey0DRr=A)nJwQu$^hTjmt{jY>Y6vZ~ z85V6P3>n3d&l$1XWYJ|+<}W!uypqJX8TLq6GtGszBQSg==)g|$6cKL`I6-U~5!dZt z5{9nBL=88aAq%Uifm(-nZnKHQXwh{0ff9nq!ak~91a=BvzMyx&s?mIpB;av zdiL${$;3LjmF9-|k;gcZkWP?o#!cBKKBlZiA{TZ?;l`1F!w*(`xEW_6aF-z-aMoh3U^foVPxmzH)VD-i8yq15m4mwtWKReZ){hgT~2Ft&ydfhEB84D0(B; z>4WeOUPv!@Mcr*)LR|>-6}l9x>{V@Kz;1?QED&*L; z)3UsPMy{F6J%FKhl%{hA+tltCses0?dH*Fwr z?Kwg?(1x8J0?{7yDmh++>!(FOqGlx}RMq(qh>FJh;2~6fF+UD6HhN+3*I`vtFTV!S zMOyoBU)3kpw{hI#o5YkRPMT37BLtTB#L`UA-96GgG z7*^l%iOtgSXGP;!rklH4+D3+!**c8yoYpPewSGgNJkOch@Pq7wh35z+&yShf`NZ{q DHJpu$ literal 0 HcmV?d00001 diff --git a/informed_decision/stance.py b/informed_decision/stance.py new file mode 100644 index 0000000..5e36a15 --- /dev/null +++ b/informed_decision/stance.py @@ -0,0 +1,56 @@ +"""The shipped unreachable-engine stance map. + +This is the map the surface actually applies. ``pep-stance.yaml`` publishes it, +and ``tests/test_layer_conformance.py`` asserts the two are equal — a published +map that may drift from the code invites reliance it cannot support. + +Built to v0.8 obligation 3 per GH-DEC-2026-012: the axis is enumerated rather +than defaulted, ``unknown`` resolves to ``fail_closed``, and an absent scope is +distinguishable in the record from an unknown one. +""" + +from __future__ import annotations + +from enum import Enum + +AXIS = "binding_level" + +#: Total by enumeration, not by catch-all. See ``pep-stance.yaml`` for why every +#: stance is ``fail_closed`` here where ops-warden can justify ``fail_open``. +STANCE: dict[str, str] = { + "acknowledgment": "fail_closed", + "organizational": "fail_closed", + "aes": "fail_closed", + "qes": "fail_closed", + "unknown": "fail_closed", + "absent": "fail_closed", +} + +#: Values of the axis proper — the two non-value outcomes are not axis values. +AXIS_VALUES: tuple[str, ...] = ("acknowledgment", "organizational", "aes", "qes") + + +class BindingLevelState(str, Enum): + """How the axis value was obtained. Recorded; never collapsed. + + ``ABSENT`` and ``UNKNOWN`` resolve to the same stance but must never be + recorded as the same fact: collapsing them hides a schema-drift incident + inside a malformed-input statistic. + """ + + PRESENT = "present" + ABSENT = "absent" + UNKNOWN = "unknown" + + +def resolve(binding_level: str | None) -> tuple[str, BindingLevelState]: + """Return ``(stance, state)`` for a memo's ``binding_level``. + + There is no per-call discretion and no implicit default: an unlisted value + is resolved explicitly to the ``unknown`` stance, never permissively. + """ + if binding_level is None or binding_level == "": + return STANCE["absent"], BindingLevelState.ABSENT + if binding_level not in AXIS_VALUES: + return STANCE["unknown"], BindingLevelState.UNKNOWN + return STANCE[binding_level], BindingLevelState.PRESENT diff --git a/layer.yaml b/layer.yaml new file mode 100644 index 0000000..7af1ea4 --- /dev/null +++ b/layer.yaml @@ -0,0 +1,184 @@ +# informed-decision — NetKingdom security layer declaration +# +# Framework: net-kingdom/canon/standards/security-layer-model_v0.7.md +# Companion: net-kingdom/SECURITY-COMPANION.md v0.2 +# Voice: INTENT.md (this repository's own, per §11 "who must declare") +# Ruling: GH-DEC-2026-012 (gate-house@0a1d1d9) answered INFD-IN-0001 +# +# Reference form: ops-warden's, adopted by audit-core and kings-guard, with +# kings-guard's adaptation for a repository with no Tooling contacts. +# +# GH-DEC-2026-012 R1 confirmed the SHAPE. The layer is declared here, in this +# repository's own voice, because a layer someone else states about you is not +# a declaration. + +schema_version: "0.1" +framework: netkingdom-security-layer-model +standard_version: "0.7" +companion_version: "0.2" +repository: informed-decision +layer: surface +role: pep-shaped +declared_by: INTENT.md +declared_at: "2026-09-09" +ruling: GH-DEC-2026-012 + +# §6.4 — informed-decision is PEP-shaped: it causes a protected side effect on +# the far side of a decision (recording an approver entry against an approval +# object). Companion §5 is owed and §6.4 applies in full. +# +# Built to v0.8 obligation 3, not v0.7, per GH-DEC-2026-011 — see pep-stance.yaml. +pep_stance: pep-stance.yaml + +protected_action: "Approver entry recorded against an approval object (POST /v1/approvals/{id}/entries)" +decision_engine: access-engine + +# §6 — no repository other than access-engine exposes an authorization decision. +# This surface renders a question and records a human's answer. A disposition is +# evidence of an act, never a verdict. +decision_surfaces_exposed: none + +# §3.3 / GH-DEC-2026-012 R2 — YES to a presentation claim, and NO second catalog +# row: PEP and PIP are shapes a repository has; §4 records the layers it +# occupies. The permission carries three limits, and they are the substance of +# it rather than caveats on it. +presentation_claim: + emitted: true + carries: presentation-only + limits: + - id: L1-presentation-only + rule: >- + The claim carries presentation and nothing else. It MUST NOT carry, + restate, summarise or imply the decision, the verdict, or whether the + act was permitted. A consumer learns from it only what was SHOWN, never + what was DECIDED. + - id: L2-not-an-input + rule: >- + The claim MUST NOT be an input to the decision it presents for. A policy + reading view_hash to decide whether an act is permitted would let the + presenting surface contribute to its own authorization. + note: >- + Load-bearing, not a formality. GH-DEC-2026-012 accepted this + repository's argument that a renderer attesting its own rendering is not + the self-dealing that kept the approval object out of access-engine — + but only because this limit holds. Without it the two collapse into the + same failure. + - id: L3-independent-evidence-path + rule: >- + The evidence copy reaches audit-core INDEPENDENTLY of this repository. + The claim endpoint and the evidence path are different things and + neither substitutes for the other. The copy that is evidence MUST NOT be + reachable only through the party it is evidence about. + note: >- + The limit that matters most here: audit evidence is protected from the + actor being audited, and in this component the actor and the source are + the same. Architecture consequence, tracked in + docs/specs/ArchitectureBlueprint.md. + +# §17 — the shared request-claim schema is still unowned. This repository +# publishes at its own boundary and yields to that schema when it exists. +# Position accepted by GH-DEC-2026-012 and matching approval-engine's in +# APPROVAL-IN-0001. +request_claim_schema: + status: unowned-upstream + local_shape: published-at-own-boundary + yields_to: taxonomy-request-claim-schema + +# GH-DEC-2026-012 R3 — (b), with the authority rule written down. +binding_digest_relationship: + ruling: GH-DEC-2026-012 + view_hash_authoritative_for: what-was-shown + binding_digest_authoritative_for: what-the-request-is + binding_digest_owner: approval-engine + substitutable: false + disagreement_is: >- + A finding against the presenting surface, never a fact about the request. + linkage: co-reference + linkage_rule: >- + The presentation record carries the approval or binding identifier + explicitly, and both attestations are read against that one reference. This + repository MUST NOT recompute or restate approval-engine's binding digest + from its own vocabulary — it references the digest that layer computed and + recorded. + nesting_forbidden: >- + view_hash MUST NOT contain the binding digest, and MUST NOT travel inside + hashed request material while containing it. Option (c) was refused because + nesting reproduces the hash cycle that made GH-DEC-2026-008 unimplementable: + a claim required to name the digest of a request that would come to contain + it, where a fail-closed consumer denies permanently. + +# §5 applies to Staff. This is a browser-facing surface with no Tooling contact. +tooling_contacts: [] + +# §11 — record non-Tooling clients so the check is total. +non_tooling_clients: [] + +intended_non_tooling_clients: + - target: approval-engine + layer: engine + rationale: >- + GET /v1/approvals/{id} and /claim (approval:read) to render; POST + /v1/approvals/{id}/entries (approval:approve) to record a binding. Never + /consume. Requirements: approval-engine/docs/approver-surface-requirements.md. + - target: access-engine + layer: engine + rationale: >- + Decision consumed before rendering an approval to a person. A 200 from + approval-engine is not entitlement. This surface consumes a decision and + never renders one. + - target: key-cape + layer: engine + rationale: >- + Identity. Authorization-code + PKCE browser client. Identity is imported, + never invented here. + - target: audit-core + layer: engine + rationale: >- + Evidence destination for presentation records and dispositions. Must be an + independent path per limit L3-independent-evidence-path. + - target: state-hub + layer: not-catalogued + rationale: >- + Progress events. Outside §5 by the v0.5 scope rule. Recorded, not policed. + +# §9.6 — presentation evidence is load-bearing: it is the only record of what a +# human was shown before binding. Atomicity and attestation cover accident and +# later tampering, never a compromised source. +evidence: + kind: load-bearing + residual: compromised-surface-presents-x-attests-y + residual_closed: false + custody: same-bound-as-every-other-source # §16 decided: no stronger archive + note: >- + GH-DEC-2026-012 states the residual is not closed in those words, and this + repository is not credited with closing it. Same disposition as + approval-engine's equivalent residual for adversarial omission at a + compromised source. + +# INHERITED DECLARED GAP — GH-DEC-2026-010. +# +# Obligation 1 now requires a decision be ATTRIBUTABLE to access-engine. No +# consumer can satisfy that today: flex-auth's decision envelope is unsigned. +# This is a declared §13 gap tracked as FLEX-WP-0024, not a clean path this +# repository can walk. +# +# Stated here, and in SCOPE.md and ArchitectureBlueprint.md, because +# GH-DEC-2026-012 requires it be said in this repository's own documents rather +# than describing validation as complete. +inherited_gaps: + - id: GH-DEC-2026-010-attributability + obligation: 1 + gap: >- + A decision consumed from access-engine cannot today be proven to have come + from access-engine — the envelope is unsigned. + tracked_by: FLEX-WP-0024 + consequence_here: >- + This surface's record can show that a decision was obtained and what it + said. It cannot yet show it was access-engine that said it. Validation of + the decision path MUST NOT be described as complete while this is open. + status: open + +declared_shapes: + "5.1": [] + "5.2": [] + "5.3": [] diff --git a/pep-stance.yaml b/pep-stance.yaml new file mode 100644 index 0000000..b078870 --- /dev/null +++ b/pep-stance.yaml @@ -0,0 +1,110 @@ +# informed-decision — PEP unreachable-engine stance map +# +# Framework: net-kingdom/canon/standards/security-layer-model_v0.7.md §6.4, §9.3, §9.7 +# Built to: v0.8 obligation 3 (GH-DEC-2026-011), not v0.7 — per GH-DEC-2026-012, +# so this is built to rather than migrated to. +# Ruling: GH-DEC-2026-012 +# Validate: pytest tests/test_layer_conformance.py +# +# §6.4 obligation 3 requires a declared unreachable-engine stance that is total, +# scoped per zone, carries no implicit default and no per-call discretion, and is +# published rather than held in code comments. A map that lives only in a module +# default is not published — it is merely written down. +# +# The property that makes this worth reading: it is asserted equal to the shipped +# map in informed_decision/stance.py by tests/test_layer_conformance.py. A +# published map that may drift from the code is worse than none, because it +# invites reliance it cannot support. + +schema_version: "0.1" +framework: netkingdom-security-layer-model +standard_version: "0.7" +built_to_obligation_3_of: "0.8" +repository: informed-decision +pep_shape: true +declared_by: layer.yaml +ruling: GH-DEC-2026-012 + +protected_action: "Approver entry recorded against an approval object" +decision_engine: access-engine + +# v0.8 obligation 3: the map MUST enumerate its axis rather than lean on a +# catch-all. The axis here is the memo's binding_level — this repository's own +# graded property, and the one that determines how costly a wrong bind is. +axis: binding_level +axis_source: decision-memo.schema.json#/properties/binding_level + +# Total by enumeration, not by catch-all. Every value of the axis appears +# explicitly, plus the two non-value outcomes required to be distinguishable. +# +# Every stance is fail_closed, and that is a conclusion rather than a shortcut. +# ops-warden can justify fail_open on some zones because refusing removes +# production host access to close a documentation gap — a continuity argument. +# No such argument exists here. Refusing to render or to bind delays a decision; +# it does not remove a capability anyone depends on to keep a system running. +# Binding a human's identity to an act without an authorization decision is the +# precise failure this repository exists to prevent, so there is no level at +# which proceeding-without-a-decision is the safer error. +stance: + acknowledgment: fail_closed + organizational: fail_closed + aes: fail_closed + qes: fail_closed + unknown: fail_closed # v0.8 obligation 3: unknown MUST resolve to fail_closed + absent: fail_closed # distinct from unknown in the record — see below + +# v0.8 obligation 3 requires an absent scope to be DISTINGUISHABLE in the record +# from an unknown one. They resolve to the same stance here but must never be +# recorded as the same fact: +# absent — the memo carried no binding_level at all (a defect in the memo) +# unknown — a binding_level was present but is not a value this map enumerates +# (a version skew between memo and this map) +# Collapsing them would hide a schema-drift incident inside a malformed-input +# statistic. +scope_absence: + distinguishable: true + recorded_as: + absent: "binding_level.absent" + unknown: "binding_level.unknown" + +# §6.4 obligation 1 requires a decision record for a protected side effect. +# Where the engine is unreachable there is no decision to hold, so this surface +# records the APPLICATION OF THE STANCE instead. +on_apply: + recorded_fields: + - memo_id + - memo_version + - binding_level + - binding_level_state # present | absent | unknown + - stance_applied + - decision_id # only where a decision was actually rendered + - decision_attributable # see inherited gap below + - presentation_id + user_visible: >- + The approver is told the act could not be authorized and that nothing was + bound. The surface never presents a fail_closed outcome as a decline by the + approver: no disposition is recorded, because the human did not make one. + +# GH-DEC-2026-010, inherited. Obligation 1 requires a decision be ATTRIBUTABLE to +# access-engine, and no consumer can satisfy that today — flex-auth's envelope is +# unsigned (declared §13 gap, FLEX-WP-0024). `decision_attributable` is therefore +# recorded as false and MUST NOT be reported as satisfied while that is open. +inherited_gap: + id: GH-DEC-2026-010-attributability + decision_attributable_today: false + tracked_by: FLEX-WP-0024 + +# §13.1 / GH-DEC-2026-011 — a dated classification-coverage figure beside the +# stance. Honest figure for a repository at spec stage: the axis is fully +# enumerated against the schema, and no protected action has shipped yet. +classification_coverage: + as_of: "2026-09-09" + axis_values_enumerated: 4 + axis_values_in_schema: 4 + coverage: "4/4 (100%) of binding_level values enumerated" + protected_actions_shipped: 0 + note: >- + Coverage is complete against the schema, not against production behaviour: + nothing is deployed. Re-date this figure when the walking skeleton + (INFD-WP-0001-T08) ships, and treat a coverage figure that predates a schema + change as stale. diff --git a/tests/__pycache__/test_layer_conformance.cpython-312-pytest-7.4.4.pyc b/tests/__pycache__/test_layer_conformance.cpython-312-pytest-7.4.4.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c52c2baa236aa93803eea92aa078c5cb0e88697b GIT binary patch literal 21688 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 k`j~o dict: + return yaml.safe_load((ROOT / name).read_text(encoding="utf-8")) + + +@pytest.fixture(scope="module") +def stance_doc() -> dict: + return load("pep-stance.yaml") + + +@pytest.fixture(scope="module") +def layer_doc() -> dict: + return load("layer.yaml") + + +# -------------------------------------------------------------------------- +# Obligation 3 — published equals shipped. This is the MUST. +# -------------------------------------------------------------------------- + + +def test_published_stance_equals_shipped_stance(stance_doc): + assert stance_doc["stance"] == STANCE, ( + "pep-stance.yaml has drifted from informed_decision/stance.py. " + "A published map that may differ from the code is worse than none." + ) + + +def test_published_axis_equals_shipped_axis(stance_doc): + assert stance_doc["axis"] == AXIS + + +# -------------------------------------------------------------------------- +# Obligation 3 — totality by enumeration, no catch-all, no implicit default. +# -------------------------------------------------------------------------- + + +def test_every_axis_value_has_an_explicit_stance(): + for value in AXIS_VALUES: + assert value in STANCE, f"{value} has no declared stance" + + +def test_stance_map_has_no_entries_beyond_the_axis_and_the_two_outcomes(): + allowed = set(AXIS_VALUES) | {"unknown", "absent"} + assert set(STANCE) == allowed + + +def test_unknown_resolves_to_fail_closed(stance_doc): + """v0.8 obligation 3 makes this a MUST; v0.7 permitted fail_open.""" + assert STANCE["unknown"] == "fail_closed" + assert stance_doc["stance"]["unknown"] == "fail_closed" + + +@pytest.mark.parametrize("value", sorted(STANCE)) +def test_no_stance_is_permissive(value): + """Not required by the standard — required by this repository. + + Binding an identity without an authorization decision is the failure this + surface exists to prevent, so there is no level at which proceeding is the + safer error. If this test is ever relaxed, the reasoning in pep-stance.yaml + must be rewritten first. + """ + assert STANCE[value] == "fail_closed" + + +# -------------------------------------------------------------------------- +# Obligation 3 — absent must be distinguishable from unknown IN THE RECORD, +# even though both resolve to the same stance. +# -------------------------------------------------------------------------- + + +def test_absent_and_unknown_resolve_the_same_but_record_differently(): + absent_stance, absent_state = resolve(None) + unknown_stance, unknown_state = resolve("notalevel") + assert absent_stance == unknown_stance == "fail_closed" + assert absent_state is BindingLevelState.ABSENT + assert unknown_state is BindingLevelState.UNKNOWN + assert absent_state != unknown_state + + +def test_empty_string_is_absent_not_unknown(): + assert resolve("")[1] is BindingLevelState.ABSENT + + +@pytest.mark.parametrize("value", AXIS_VALUES) +def test_known_axis_values_record_as_present(value): + stance, state = resolve(value) + assert state is BindingLevelState.PRESENT + assert stance == "fail_closed" + + +def test_published_map_declares_the_two_states_distinguishable(stance_doc): + absence = stance_doc["scope_absence"] + assert absence["distinguishable"] is True + assert absence["recorded_as"]["absent"] != absence["recorded_as"]["unknown"] + + +# -------------------------------------------------------------------------- +# Inherited gap — GH-DEC-2026-010. Must be declared open, not glossed. +# -------------------------------------------------------------------------- + + +def test_attributability_gap_is_declared_open_in_the_stance(stance_doc): + gap = stance_doc["inherited_gap"] + assert gap["decision_attributable_today"] is False + assert gap["tracked_by"] == "FLEX-WP-0024" + + +def test_attributability_gap_is_declared_open_in_the_layer(layer_doc): + gaps = {g["id"]: g for g in layer_doc["inherited_gaps"]} + gap = gaps["GH-DEC-2026-010-attributability"] + assert gap["status"] == "open" + + +def test_stance_records_whether_the_decision_was_attributable(stance_doc): + assert "decision_attributable" in stance_doc["on_apply"]["recorded_fields"] + + +# -------------------------------------------------------------------------- +# R1 / R2 / R3 — the ruling's limits are declared, not merely remembered. +# -------------------------------------------------------------------------- + + +def test_layer_declares_pep_shaped_and_no_decision_surface(layer_doc): + assert layer_doc["role"] == "pep-shaped" + assert layer_doc["decision_surfaces_exposed"] == "none" + + +def test_presentation_claim_carries_all_three_limits(layer_doc): + limits = {limit["id"] for limit in layer_doc["presentation_claim"]["limits"]} + assert limits == { + "L1-presentation-only", + "L2-not-an-input", + "L3-independent-evidence-path", + } + + +def test_binding_digest_relationship_is_co_reference_not_nesting(layer_doc): + rel = layer_doc["binding_digest_relationship"] + assert rel["linkage"] == "co-reference" + assert rel["substitutable"] is False + assert "nesting_forbidden" in rel + + +def test_residual_is_declared_not_closed(layer_doc): + assert layer_doc["evidence"]["residual_closed"] is False + + +# -------------------------------------------------------------------------- +# §13.1 / GH-DEC-2026-011 — a dated coverage figure beside the stance. +# -------------------------------------------------------------------------- + + +def test_classification_coverage_is_dated_and_complete_against_the_axis(stance_doc): + cov = stance_doc["classification_coverage"] + assert cov["as_of"] + assert cov["axis_values_enumerated"] == cov["axis_values_in_schema"] == len(AXIS_VALUES) diff --git a/workplans/INFD-WP-0001-founding-specs-and-approver-ui-ownership.md b/workplans/INFD-WP-0001-founding-specs-and-approver-ui-ownership.md index 0b26cc3..760da69 100644 --- a/workplans/INFD-WP-0001-founding-specs-and-approver-ui-ownership.md +++ b/workplans/INFD-WP-0001-founding-specs-and-approver-ui-ownership.md @@ -94,7 +94,7 @@ Two corrections made during the same task, recorded rather than silently fixed: ```task id: INFD-WP-0001-T02 -status: progress +status: done priority: high state_hub_task_id: "4f94134b-2260-5404-84e0-12f2b08ef565" ```