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
|
- **`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
|
object. Presenting it as "you may now do the thing" is a policy decision point
|
||||||
in the browser.
|
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`
|
- **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
|
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
|
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?**
|
> **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
|
- **informed-decision owns it** — the surface, the presentation record, the
|
||||||
browser client, and the evidence of informedness.
|
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.
|
and statute §6.4.
|
||||||
- It supplies exactly one PIP-like fact — *what was presented* — as a claim
|
- 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.
|
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
|
- **Ruled:** PEP-shaped, confirmed as proposed. No second catalog row for the
|
||||||
writes `layer.yaml` from the ruling. If `gate-house` places this component
|
presentation claim — PEP and PIP are shapes a repository has. The claim is
|
||||||
elsewhere, `INTENT.md` and this file change to match — the ruling wins.
|
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
|
## 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
|
2. `key-cape` has the `client_id` and callback URI, and `KEY-WP-0013-T02` is
|
||||||
unblocked with the answer traceable to this repository.
|
unblocked with the answer traceable to this repository.
|
||||||
3. The four specs exist, are reviewed against the current `approval-engine`,
|
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
|
domain: infotech
|
||||||
topic_slug: netkingdom
|
topic_slug: netkingdom
|
||||||
layer: surface
|
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
|
standard: net-kingdom/canon/standards/security-layer-model_v0.7.md
|
||||||
companion: net-kingdom/SECURITY-COMPANION.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
|
presented view plus explicit highlight acknowledgment — never keystroke
|
||||||
analytics, dwell timers, or attention theater.
|
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
|
**PEP-shaped**, confirmed by `GH-DEC-2026-012` (answering `INFD-IN-0001`).
|
||||||
browser-facing and it causes a protected side effect on the far side of a
|
Not an Engine: this repository holds no state another layer reads at runtime for
|
||||||
decision, which is the shape statute §6.4 and companion §5 call **PEP-shaped**.
|
a verdict, and renders no decision. Companion §5 is owed and statute §6.4
|
||||||
`approval-engine`'s own INTENT names this expectation: *"Callers that do are
|
applies in full. The declaration is `layer.yaml`, in this repository's own voice
|
||||||
PEP-shaped and owe companion §5 / statute §6.4."*
|
— a layer someone else states about you is not a declaration.
|
||||||
|
|
||||||
This is declared **provisional** because the catalog row does not exist yet and
|
The ruling also confirmed that emitting a **presentation claim** does not
|
||||||
inventing one is the drift §17 exists to prevent. `INFD-WP-0001-T02` takes the
|
require a second catalog row: PEP and PIP are shapes a repository has, and §4
|
||||||
question to `gate-house` and writes `layer.yaml` from the answer, not from this
|
records the layers it occupies. That permission carries three limits, and they
|
||||||
paragraph. The framing this repository will argue: informed-decision is a
|
are the substance of it rather than caveats on it:
|
||||||
**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
|
1. **The claim carries presentation and nothing else.** It must never carry,
|
||||||
never evaluates.
|
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
|
## 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
|
```task
|
||||||
id: INFD-WP-0001-T02
|
id: INFD-WP-0001-T02
|
||||||
status: progress
|
status: done
|
||||||
priority: high
|
priority: high
|
||||||
state_hub_task_id: "4f94134b-2260-5404-84e0-12f2b08ef565"
|
state_hub_task_id: "4f94134b-2260-5404-84e0-12f2b08ef565"
|
||||||
```
|
```
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue