Declare the layer per GH-DEC-2026-012; close T02
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 <noreply@anthropic.com> 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
This commit is contained in:
parent
f36e2b789a
commit
f6376ddff5
9 changed files with 614 additions and 20 deletions
24
GOAL.md
24
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`,
|
||||
|
|
|
|||
76
INTENT.md
76
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
|
||||
|
||||
|
|
|
|||
BIN
informed_decision/__pycache__/stance.cpython-312.pyc
Normal file
BIN
informed_decision/__pycache__/stance.cpython-312.pyc
Normal file
Binary file not shown.
56
informed_decision/stance.py
Normal file
56
informed_decision/stance.py
Normal file
|
|
@ -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
|
||||
184
layer.yaml
Normal file
184
layer.yaml
Normal file
|
|
@ -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": []
|
||||
110
pep-stance.yaml
Normal file
110
pep-stance.yaml
Normal file
|
|
@ -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.
|
||||
Binary file not shown.
182
tests/test_layer_conformance.py
Normal file
182
tests/test_layer_conformance.py
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
"""Layer and stance conformance.
|
||||
|
||||
GH-DEC-2026-012 confirmed this repository is PEP-shaped and told it to build to
|
||||
v0.8 obligation 3 rather than migrate to it later. These tests pin the parts of
|
||||
that obligation a test can actually hold:
|
||||
|
||||
- the published map equals the shipped map (obligation 3, a MUST);
|
||||
- the axis is enumerated, not defaulted;
|
||||
- ``unknown`` resolves to ``fail_closed``;
|
||||
- an absent scope is distinguishable in the record from an unknown one;
|
||||
- the inherited GH-DEC-2026-010 attributability gap is declared open rather
|
||||
than described as satisfied.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
|
||||
import pytest
|
||||
|
||||
yaml = pytest.importorskip("yaml")
|
||||
|
||||
from informed_decision.stance import AXIS, AXIS_VALUES, STANCE, BindingLevelState, resolve
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def load(name: str) -> 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)
|
||||
|
|
@ -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"
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue