secrets-engine's first live request rejected our allow: binding. request_digest is computed over material they never sent, because we enrich subject and resource from the registry before hashing. Answering that meant reading the enrichment path, which had a worse defect in it. Enrichment was additive-if-absent — addAttribute wrote a registry value only where the request had no value for that key. So where a caller supplied a key, the caller's value won and the registry's never applied. Every registry ceiling and allowlist was advisory. Verified against the shipped ops-warden package, each one added key on an otherwise-denied request: max_ttl_hours: 99 registry says 8 -> allowed a 12h certificate allowed_principals registry allowlist -> disallowed_principal bypassed allowed_subjects registry allowlist -> unknown_subject bypassed The third is the one to read twice: a subject the registry does not know authorized itself by naming itself in the allowlist it was being checked against. Not remotely reachable today — the PEP builds the CheckRequest, ops-warden sends no resource.attributes, and enforce admits one identity. It is a defence-in-depth failure: any path that lets attacker-influenced data into a CheckRequest field became a full policy bypass rather than a bounded input problem. Callers sending resource.attributes is not hypothetical; secrets-engine does it on every request. Registry facts now win, and diagnostics.registry_overrode names every displaced key, because a registry that silently discards a contradicting claim hides that a caller asserted authority it did not have. subject.type is carved out, and the reason is a finding of its own. Making the registry win there denied every secrets-engine allow: the registry's type is CARING vocabulary (Human, Agent, Automation, Service) and the request's is the protected system's actor vocabulary (service, adm, agt, atm). Two fields sharing a name; substituting one for the other is translation rather than identity, which GH-DEC-2026-008 ruled against. Note what surfaced it — the registry's type had been dead data since the field existed, because the caller's value always won. Also publishes binding.submitted_request_digest, over the request exactly as sent. request_digest was published as the consumer replay test and cannot be one. Nothing is lost hashing the pre-enrichment form: enrichment is a function of the request and the snapshot, and registry_snapshot_digest already pins the snapshot. Existing pins do not move. All three replay fixtures' request_digest and approval_binding_digest values are byte-identical — those requests contradict no registry fact. A field to add, not a value to correct. Regression tests verified failing against the old behaviour before being kept. FLEX-DEC-2026-012; FLEX-WP-0025 carries the residual, that a policy still cannot tell a fact from an assertion. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014aQMM1dPXaPiXVn6DwwtLd Assistant: claude-code Assistant-Model: opus Assistant-Process: 715613@bnt-lap001 Assistant-Session: fabd95c1-4c9e-4080-8849-8707ae025f80
91 KiB
Decision records
FLEX-DEC-2026-001 — Assent to GH-DEC-2026-001: Engine framing, access-engine rename, authoring/evaluation split
id: FLEX-DEC-2026-001
kind: decision
title: 'Assent to GH-DEC-2026-001: Engine framing, access-engine rename, authoring/evaluation
split'
status: resolved
origin: cross-repo
origin_ref: gate-house GH-DEC-2026-001
standard: net-kingdom/canon/standards/security-layer-model_v0.1.md
intake_ref: FLEX-IN-0001
owner: flex-auth
affects:
- flex-auth
- gate-house
- net-kingdom
- ops-warden
- secrets-engine
- zone-engine
requested_dispositions:
- assent
- revise
- reject
created: '2026-08-28T19:43:38.443788Z'
updated: '2026-08-28T19:44:29.505314Z'
rationale: 'Assent to all three items of GH-DEC-2026-001. Item 1: flex-auth is Engine-layer
and the sole decision point; the INTENT reframe at fe46122 stands. flex-auth accepts
one conformance debt of its own — DecisionProvenance carries no registry snapshot
digest, so a decision turning on registry content is not replayable from its own
provenance (standard section 6). Item 2: access-engine is the right name; execution
is a separate governed migration, conditioned on renaming repository identity and
runtime identity in separate revertible steps (the enforcing ops-warden pin binds
tokens to the protected-system name flex-auth, and a single-step rename would 401
every warden sign) and on FLEX-WP prefix ownership staying with the repository.
Item 3: the authoring/evaluation split is accepted; gate-house authority ceilings
must reach the decision as input claims or as rules in the versioned policy package
so their application is reconstructable from the decision record — the same section
6 test flex-auth applied to zone-engine and now to itself. FLEX-WP-0017-T03/T05
stay wait: the design half is re-routed to gate-house, the durable storage half
remains unowned and is raised as an engine gap.'
decided_by: flex-auth (reviewing side)
decided_at: '2026-08-28T19:44:29.505314Z'
state_hub_decision_id: "c990e442-77c2-4556-a40b-61f65eded10b"
Context
gate-house asked flex-auth, via FLEX-IN-0001, to assent to the three items
ratified in GH-DEC-2026-001. The request follows the estate precedent that a
boundary is drawn on review by the other side rather than asserted — the
precedent flex-auth itself set when it reviewed a zone-engine draft and ruled
that flex-auth is the policy decision point and stays the only one.
This record is flex-auth's answer. It is written from the reviewing side: the questions asked were not "is this flattering to flex-auth" but "does this hold against what flex-auth actually is, and does flex-auth actually conform".
Disposition
Assent to all three items, with one accepted conformance debt and two
conditions on the rename. Nothing here revises or rejects any part of
GH-DEC-2026-001.
Item 1 — Engine framing and the sole decision point: assent
flex-auth is Engine-layer under §3.3 and §4 of the standard. The defining
property holds: the same authoritative input state yields the same result. The
INTENT reframe is applied at fe46122 and is correct as written — "control
plane" is dropped as Staff vocabulary, and the exclusive decision-point role of
§6 is stated.
The generalization in §6 is the ruling flex-auth drew against zone-engine,
applied to the estate. flex-auth cannot consistently hold that boundary against
another repository and decline it as a general rule.
Accepted conformance debt — registry provenance. §6 states that compiled
data determining an outcome is still deciding, and that provenance must remain
reconstructable from the engine's decision. flex-auth does not fully satisfy
this today. DecisionProvenance (pkg/api/canonical.go) carries the evaluator,
mode, policy package, policy version, and a directory ETag — but no digest of
the registry snapshot that supplied resource, subject, and relationship facts.
A decision that turned on registry content therefore cannot be replayed from
its own provenance.
flex-auth accepts this as its own gap rather than claiming conformance it does
not have. It is the same argument flex-auth made to zone-engine — zone
membership compiles into the registry snapshot, per-zone stance belongs in
the versioned policy package, precisely because registry content is absent from
decision provenance. The clean fix is to make registry content provenanced, and
a workplan will carry it. Until it lands, the flex-auth position that
outcome-determining content belongs in the versioned policy package stands
unchanged, and stands for gate-house's inputs on the same terms (item 3).
Item 2 — The flex-auth → access-engine rename: assent to the name, not yet to execution
access-engine is the right name. The rejection of auth-engine is correct —
key-cape owns authentication, and auth- preserves exactly the ambiguity the
rename exists to remove. The §8 lane/rule demarcation is an acceptable cost and
flex-auth adopts it: ops-warden and ops-mason own access lanes, flex-auth
owns access rules.
flex-auth agrees the rename is a separate governed migration and does not treat
GH-DEC-2026-001 as authorizing it. Two conditions, from the consumer side
rather than the documentary side:
- Repository identity and runtime identity must be renamed in separate,
independently revertible steps, repository first. The name
flex-authis not only a repo slug: it is a deployed service, an in-cluster DNS name, a chart and image name, and — sinceFLEX-WP-0016moved the ops-warden pin tocallerAuth.mode: enforce— a protected-system identifier that inbound tokens are bound to. A single-step rename would make everywarden signa 401, including the certificate theops-bridgetunnels depend on. This is the same dependency shape that causedops-warden'sADR-0006to deferpolicy.enabledpermanently. FLEX-WPprefix ownership stays with the repository across the rename. ExistingFLEX-WP-0001..0018identifiers are referenced by State Hub records, by consumer repositories, and by decision provenance in shipped audit records. They are not renamed retroactively; whether new work takes a new prefix is a question for the migration record, not for this assent.
The migration also touches State Hub identifiers, ops-warden's routing
tables, zone-engine's boundary text, and secrets-engine integrations, as
GH-DEC-2026-001 states. flex-auth will not begin any of it under this record.
Item 3 — The authoring/evaluation split: assent, with the §6 test applied symmetrically
The split is correct and flex-auth gives up the authoring half willingly: gate-house owns the doctrine, the invariants, the authority ceilings, the operating modes, and the authority context; flex-auth owns evaluation exclusively plus the policy-as-code mechanism; policy content stays with the protected system's owner.
flex-auth draws one boundary back, which is the §6 test applied to gate-house's
inputs on the same terms flex-auth applies it to zone-engine and to itself:
An authority ceiling that determines an outcome must reach the decision as either an input claim on the request or a rule in the versioned policy package — and its application must be reconstructable from the decision record. A ceiling that resolves an outcome before evaluation runs has decided early, and gate-house is Staff, where §6 forbids a decision point.
Concretely: gate-house authors the ceiling; flex-auth renders it. The principal /actor/runtime triple, the mandate, and the operating mode arrive as input claims and appear in the decision binding. Where a ceiling constrains an outcome, it is expressed in the versioned policy package so that the policy version in provenance identifies the ceiling that applied. This is not a restriction on gate-house's authorship; it is what keeps the authorship auditable at decision time.
Effect on FLEX-WP-0017. The split resolves the overlap and re-routes the
design half of the blocked work: gate-house designs the approval contract,
flex-auth validates approvals at decision time. It does not unblock
FLEX-WP-0017-T03 — the durable approval object, authenticated approval
entries, and atomic supersession still need an owner for storage and
lifecycle, which is neither gate-house's (Staff holds no state another layer
depends on at runtime, §3.4) nor flex-auth's (flex-auth does not own the
organizational approval lifecycle). T03 and T05 stay wait, with the design
half now addressed to gate-house. That unowned half is flagged to gate-house as
an engine gap under §5, not solved locally.
Consequences
- The standard's §11 blocker "flex-auth reframed as an Engine … not yet
assented" is answered for flex-auth.
kings-guardandops-wardenassent remains outstanding and is not flex-auth's to give. INTENT.mdrecords the assent and the registry-provenance debt.- A workplan will carry the registry snapshot digest into
DecisionProvenance. - The rename is not started, and must not start before a migration record exists that satisfies the two conditions above.
FLEX-DEC-2026-002 — Review of security layer model v0.4: assent with findings, one rule contested
id: FLEX-DEC-2026-002
kind: decision
title: 'Review of security layer model v0.4: assent with findings, one rule contested'
status: resolved
origin: cross-repo
origin_ref: net-kingdom security-layer-model_v0.4
standard: net-kingdom/canon/standards/security-layer-model_v0.4.md
intake_ref: FLEX-IN-0002
owner: flex-auth
affects:
- flex-auth
- gate-house
- net-kingdom
- ops-warden
- approval-engine
- maturity-engine
requested_dispositions:
- assent
- revise
- reject
created: '2026-08-29T00:41:21.171665Z'
updated: '2026-08-29T00:42:13.009799Z'
rationale: 'Assent to security-layer-model v0.4, with one rule contested and two capability
assignments not accepted as assented. Section 9.3 conflicts with shipped assented
behavior: it rules engine-unreachability fallback into the engine, where it cannot
live, and collides with ops-warden ADR-0009''s per-zone consumer PEP map. Section
13 names access-engine as intended owner of containment (accept as proposed owner
only, pending per 9.2) and of authentication/assurance evidence (declined as stated;
the identity layer and audit-core own that). Three consistency defects: frontmatter
status proposed contradicts section 14 ''accepted''; the adoption count reads seven
of fifteen with remaining eight against sixteen estate-authored repositories and
nine listed; section 14 says three repositories above a table of four. FLEX-IN-0002
answered: the approval boundary unblocks T03 design, T05 additionally needs the
approval claim bound to the NewDecisionBinding request digest and a named owner
and ordering for single consumption; the maturity claim route is practical as a
request claim but not as registry content until the self-declared provenance digest
gap closes.'
decided_by: flex-auth (reviewing side)
decided_at: '2026-08-29T00:42:13.009799Z'
state_hub_decision_id: "2e96321d-c7bf-4127-9c30-f3def32e5bee"
Context
FLEX-IN-0002 asked flex-auth to review v0.3. By the time of review the current
text is v0.4 (plus the in-place §11 amendment at 2aaf46c), which supersedes
v0.3. This record reviews v0.4 and answers the two questions FLEX-IN-0002 put.
Three versions have landed since FLEX-DEC-2026-001, which answered v0.1.
Disposition
Assent to v0.4, with one rule contested, two capability assignments not accepted as assented, and three consistency defects. Nothing here blocks adoption; §9.3 needs correction before it is relied on.
What v0.4 gets right, from flex-auth's side
§6.2 adopts flex-auth's boundary from FLEX-DEC-2026-001 in substance and binds
gate-house on the same terms. §9.4 upholds the self-dealing objection: the
evaluator does not own the object it evaluates, access-engine consumes
approvals as input claims and never mutates them. §13 attributes the
registry-snapshot digest gap to flex-auth as self-declared, which is correct.
§9.5's guardrail — a maturity level MUST NOT gate a decision directly — is §6.1
applied to a new engine, and flex-auth endorses it.
Contested — §9.3 conflicts with shipped, assented behavior
"The deterministic fail to reduced authority default is
access-engine's, applied when it cannot reach its own inputs."
Two different failure cases are conflated:
- access-engine is reachable but cannot reach its own inputs. The fallback is flex-auth's, it is deterministic, and §9.3 is right. flex-auth accepts it.
- access-engine is not reachable at all. The engine applies nothing, because it is not running. Whatever happens next is the consumer's behavior, necessarily. flex-auth has held since 2026-08-19 that fail-open is not expressible by a PDP at all — not as a preference, but because there is no evaluator in the path to express it.
§9.3 as written rules case 2 into the engine, where it cannot live. It also
collides with shipped behavior in a repository that has assented to this
standard: ops-warden ADR-0009 (accepted 2026-08-22, superseding ADR-0006)
retires the global policy.enabled and policy.fail_closed and replaces them
with a total per-zone map in the consumer PEP — fails open for
z0-experimental, z1-operational, z2-protected, z2-continuity and
unknown; fails closed for z3-critical. That map is a Staff-layer
degraded-mode fallback, and it is the correct design: the alternative makes
flex-auth a hard dependency of every warden sign, including the SSH
certificate the ops-bridge tunnel carrying the policy call depends on.
Proposed correction: §9.3 should rule the input-degradation fallback into the
engine and state that engine-unreachability residue is the consumer's,
bounded by a requirement that the consumer's stance be declared per zone and
auditable — which ADR-0009 already satisfies. The sentence "engine-unavailable
is not grounds for a Staff break-glass path" is sound and should be kept: a
bypass path around a reachable engine is a second decision point. A consumer
choosing its own behavior when there is no engine to ask is not.
Not accepted as assented — two capability assignments
v0.4's frontmatter lists flex-auth FLEX-DEC-2026-001 under assented_by, and
§14 presents it as adoption of the current text. That record answered v0.1.
Since then §13 names access-engine as intended owner of two capabilities
flex-auth has never reviewed:
| Gap row | Position |
|---|---|
Containment surface → access-engine + runtime engines |
Plausible and consistent with §8's posture asymmetry — rendering reduced authority is what a PDP does. Not yet reviewed, and §9.2 marks it pending anyway. Record it as proposed owner, not owner. |
Authentication / assurance evidence → user-engine, access-engine |
Declined as stated. flex-auth consumes assurance claims as input and never re-defines them; INTENT.md is explicit that the identity layer owns authentication. Evidence of authentication belongs to the identity layer and audit-core. flex-auth owns evidence of the decision, which it already emits. |
An intended_owner in a gap register is a proposal to the named repository, not
an assignment to it — §2 keeps what a repository owns in that repository's own
INTENT.md. flex-auth suggests §13 gain a column distinguishing a proposed owner
from an assented one, so the register does not accumulate silent assignments the
way §14's assent row nearly did.
Consistency defects
- Frontmatter
status: proposedcontradicts §14 "Status is accepted." Material, because §2 makes this standard the authority for layer assignment. The change log records v0.2 as accepted and v0.3/v0.4 as proposed, so §14 appears to be a carryover. - §14's adoption count does not add up. "seven of fifteen" and "the
remaining eight" against a §4 catalog of 17 rows, 16 of them estate-authored
(
OpenBaoexcepted by §11's own who-must-declare rule). Seven declared plus the nine repositories actually listed is sixteen. Should read seven of sixteen and the remaining nine. - §14 says "All three repositories whose boundaries moved" above a table of
four.
audit-coreis the fourth.
Answers to FLEX-IN-0002
(1) Is the boundary enough to unblock FLEX-WP-0017 T03/T05 design?
Yes for T03. Not quite for T05, which needs two things specified in
approval-engine's contract first:
- Claim shape must bind to the request digest flex-auth already computes.
The approval claim should carry the approval id plus the digest of the action
it approves, computed over the same canonical binding as
NewDecisionBinding(FLEX-WP-0017-T01). Otherwise "approved" and "approved for this exact request" are not distinguishable at decision time, and T05's wrong-action/lane/stage/targets proofs have nothing to compare against. - Single consumption needs an owner and an ordering. flex-auth never mutates
approvals, so consumption is
approval-engine's. But the decision precedes the action, and the action precedes consumption — so an allow rendered against an approval that is then never consumed, or consumed twice by a racing caller, is a gap neither engine closes alone. Naming who marks consumed and at what point relative to the decision belongs in the contract before T05, not after.
(2) Is the maturity claim route practical from where flex-auth sits?
Yes, with one caveat that is flex-auth's own fault rather than
maturity-engine's. A level reaching flex-auth as a request claim is
practical today and lands in the decision binding. A level reaching flex-auth as
registry content is not reconstructable, because DecisionProvenance carries
no registry-snapshot digest — the gap flex-auth self-declared in §13. Until that
closes, maturity levels should arrive as request claims or versioned policy
rules, never compiled into the registry snapshot. This is the same constraint
flex-auth placed on zone stance, for the same reason.
Consequences
FLEX-IN-0002is closed; the assessment covers v0.4 rather than v0.3.- flex-auth's assent now attaches to v0.4 for §§1–8, §9.1–9.2, §9.4–9.6, and §§10–16, and to §9.3 only in its input-degradation half.
FLEX-WP-0017T03/T05 staywaitonapproval-engine, with the two contract items above raised as prerequisites for T05.SCOPE.mdcites ops-wardenADR-0006as current; it is superseded byADR-0009. Correcting that is flex-auth's own housekeeping.
FLEX-DEC-2026-003 — Review of security layer model v0.6 and companion v0.1: assent, two answers, five findings
id: FLEX-DEC-2026-003
kind: decision
title: 'Review of security layer model v0.6 and companion v0.1: assent, two answers,
five findings'
status: resolved
origin: cross-repo
origin_ref: net-kingdom security-layer-model_v0.6 + companion_v0.1
standard: net-kingdom/canon/standards/security-layer-model_v0.6.md
owner: flex-auth
affects:
- flex-auth
- gate-house
- net-kingdom
- info-tech-canon
- ops-warden
- approval-engine
requested_dispositions:
- assent
- revise
- reject
created: '2026-08-29T08:18:39.602701Z'
updated: '2026-08-29T08:19:41.832549Z'
rationale: 'Assent to v0.6 and companion v0.1, with two answers and five findings,
none blocking. Q1: section 6.4.2 is right but collides with 9.7.1''s session-bound
allow, needs the request digest as its mechanical replay test, and should rule explicitly
on deny-caching. Q2: the visibility deadline does land on a PDP and harder than
at a PEP, but must be per input class rather than one number, and it makes flex-auth''s
registry-provenance gap load-bearing rather than untidy. Findings: 6.4''s stance-map
register does not exist in section 13 and neither document says where a map is published;
the companion omits it too, which is the sufficiency gap gate-house asked for; section
17 puts the decision-record schema in Taxonomy when it is the PDP''s output artifact,
inverting the section 2 rule flex-auth used to decline authentication evidence;
sections 17-19 are H1 outside the hierarchy; section 19 grades the document it lives
in and will age.'
decided_by: flex-auth (reviewing side)
decided_at: '2026-08-29T08:19:41.832549Z'
state_hub_decision_id: "def6a31b-4cd7-4474-8cfa-339b0fc9fcc6"
Context
gate-house published v0.5 (answering FLEX-DEC-2026-002), then v0.6 with a new
two-page companion, and asked flex-auth to review two rules specifically. This
record reviews v0.6 + companion v0.1 and answers both questions.
Every finding in FLEX-DEC-2026-002 was honored: §9.3 rewritten along the two-
owner split, §13 given an owner-status column with containment recorded as
proposed and authentication/assurance evidence as declined with flex-auth's
reasoning, all three consistency defects fixed, and both T05 answers folded into
approval-engine's INTENT and GH-WP-0002-T06. gate-house also recorded
plainly that v0.4 had ruled against shipped assented behavior and that neither
it nor ops-warden caught it. That is the loop working.
Disposition
Assent to v0.6 and companion v0.1, with two answers and five findings. None blocks adoption.
The Engine typing in §3.3 is a real improvement and flex-auth endorses it, particularly "a new engine is a PIP unless this standard is amended" — that sentence is the structural form of §6, and it forecloses the drift §6 was written to prevent.
Answer to question 1 — §6.4.2, no local recaching of the verdict
The rule is right and flex-auth supports it. Caching the answer is deciding early at the consumer; caching an input claim under its own freshness rule is not. Three refinements, one of which is a genuine collision:
(a) It contradicts §9.7.1 as written. §9.7.1 permits an allow bound to "a session or obligation that ends." A session-bound allow is used across later requests by construction — that is what binding to a session means. §6.4.2 forbids replaying a stored verdict "for a later request." Taken together, a session-bound allow is both permitted and prohibited. The fix is small: scope §6.4.2 to requests outside the decision's own stated binding and lifetime, which preserves the rule and legalizes exactly the case §9.7.1 already intends.
(b) "Later request" needs a mechanical test, and flex-auth already ships one.
A PEP retrying an identical action after a transport failure is not a later
request, but nothing in the standard says how to tell. flex-auth computes a
canonical request digest over the normalized subject, action, resource, and
context (NewDecisionBinding, FLEX-WP-0017-T01), and it is already in every
decision binding. flex-auth offers it as the normative test: replay is
permitted iff the request digest matches and the decision's lifetime holds.
That makes §6.4.2 checkable rather than a matter of implementer judgment, and it
costs the estate nothing new.
(c) Deny-caching is the one real pattern the rule outlaws without naming it. A consumer caching a deny under load cannot manufacture authority — §8's asymmetry holds, and it protects the PDP from retry storms, which is a real operational need. But it does breach §6.4.4: an action refused with no decision record naming that request is not reconstructable, and a stale deny is an availability failure that will be misdiagnosed as a policy one. flex-auth's recommendation is to rule on it explicitly rather than leave implementers to infer: permit short-lived negative caching only where the refusal is still recorded, or forbid it in terms. Either is defensible; silence is not, because this is the pattern an implementer under load reaches for first.
Answer to question 2 — §9.7.2, does the visibility deadline land on a PDP
Yes, and it lands harder on the PDP than on the PEP — but one number is the wrong shape for a decision point.
A PEP has one boundary and can state one deadline. A PDP's revocation visibility
is per input class, because a decision is a join over sources with unrelated
refresh behavior: approval-claim freshness from approval-engine, registry
snapshot cadence, policy package activation, and directory ETag. A single
flex-auth number would be either a fiction or the worst case, and the worst case
is the registry snapshot — which is the slowest and the least visible. Proposed:
§9.7.2 requires a per-input-class deadline at a PDP and a single boundary
deadline at a PEP.
The consequence for flex-auth is worth stating plainly, because it changes
flex-auth's own priorities. §9.7.2 makes the self-declared registry-provenance
gap load-bearing rather than untidy. Without a snapshot digest in
DecisionProvenance, nobody can determine after the fact which snapshot a
decision read, so a stated visibility deadline for registry-borne facts is
unfalsifiable — the deadline and the digest are the same gap seen from two
sides. flex-auth accepts that this raises the gap from housekeeping to a
conformance prerequisite and will plan it as one.
Finding 1 — §6.4's stance-map register does not exist
§6.4 requires every PEP-shaped consumer to publish its stance map and requires
those maps to be "inventoried — in maturity-engine once it exists, in §13
until then." §13 contains no stance rows and does not mention stance maps.
Neither document says where a map is published or in what form.
This matters for precisely the reason §6.4 gives: without the register,
"z0–z2 and unknown fail open" is the estate's effective policy with nobody
having compiled it. ops-warden ADR-0009 is cited three times across the two
documents as the reference shape and appears in no register. flex-auth has a
direct interest here — the aggregate of consumer stances is the estate's real
authorization behavior, and flex-auth is the only repository positioned to
notice when it diverges from what the policy packages say.
Finding 2 — the companion omits it too, which answers the question asked
gate-house asked what would show the companion is not sufficient on its own. Companion §5.3 says "publish your unreachable-engine stance" but never says where, and omits the inventory obligation entirely. A repository satisfying the companion faithfully would publish a stance map into its own repo and believe itself conforming, and no register would learn of it. On every other point reviewed the companion is faithful to the statute; this is the one gap.
Finding 3 — §17 puts the decision-record schema in the wrong layer
The four artifacts are the right four, and Taxonomy is right for three of them. The request-claim schema is genuinely cross-engine vocabulary and belongs there; so do the gap-record and emission-cadence artifacts.
The decision-record schema does not. A decision record is the PDP's output
artifact — the one thing in the estate that only access-engine produces — and
§2 keeps what a repository owns in that repository's own INTENT.md. flex-auth
already publishes its shape: the binding in DecisionEnvelope, the request
digest, and schemas/action_authorization.schema.json. Taxonomy authoring the
schema for an artifact only flex-auth emits inverts the ownership rule the
standard applies everywhere else.
This is the same §2 argument flex-auth used to decline authentication and
assurance evidence in FLEX-DEC-2026-002, and gate-house accepted it there.
Symmetry requires flex-auth to apply it against its own interest as well as for
it: proposed split is claim / gap-record / emission-cadence to Taxonomy,
decision-record schema to access-engine as a published contract, with
Taxonomy holding only the shared field vocabulary the claim schema needs to
reference. flex-auth will publish that contract; it should not receive it.
Finding 4 — §§17–19 are H1, outside the section hierarchy
They use # where every other section uses ##, so they render as siblings of
the document title rather than sections of it. Mechanical, but the standard asks
repositories to cite section numbers, and §§17–18 carry normative content.
Finding 5 — §19 grades the document it lives in
A "Verdict on fitness" inside a standard of record makes the assessment normative by adjacency and will age against the text it grades — v0.7 will either restate it or leave a stale verdict in force. Suggest it live in the 2026-08-29 assessment history document and be cited from §16, where the open questions already carry the same content as questions rather than as a grade.
Consequences
- flex-auth's assent attaches to v0.6 + companion v0.1, with §6.4.2 and §9.7.2 read as refined above pending gate-house's disposition.
- The registry-snapshot digest gap is reclassified from housekeeping to a conformance prerequisite under §9.7.2, and will be planned as one.
- flex-auth offers the canonical request digest as the §6.4.2 replay test and offers to publish the decision-record schema as its own contract.
FLEX-DEC-2026-004 — Decision lifetime does not reach past issuance: answer to ops-warden WARDEN-WP-0034-T05
id: FLEX-DEC-2026-004
kind: decision
title: 'Decision lifetime does not reach past issuance: answer to ops-warden WARDEN-WP-0034-T05'
status: resolved
origin: cross-repo
origin_ref: WARDEN-WP-0034-T05
owner: flex-auth
affects:
- flex-auth
- ops-warden
requested_dispositions:
- answer
- decline
decided_by: flex-auth (access-engine / PDP)
rationale: 'Answered, not declined. A decision lifetime shorter than the SSH certificate
TTL is meaningful, but only as an authority-to-issue window, never as an authority-to-use
window over an already-issued certificate. flex-auth lifetime.expires_at bounds
how long that one allow may be relied on to authorise a sign; it cannot bound an
artifact ops-warden issued under it, and flex-auth does not claim it does. The downstream
contract that consumes the shorter lifetime is the pre-sign gate itself: no replay
of an allow past expires_at, and a fresh Check per sign. The lever that actually
shortens effective access is the requested TTL as a policy input, which is already
deployed as the ttl_out_of_bounds deny; ops-warden section 9.7.2 window through
cert TTL is correctly stated and correctly owned by the PEP.'
created: '2026-09-05T23:08:38.877787Z'
updated: '2026-09-06T00:00:00.000000Z'
decided_at: '2026-09-06T00:00:00.000000Z'
state_hub_decision_id: "f6bfb02a-bbf3-4488-acf9-0e1cb57ce19d"
Context
ops-warden now states its §9.7.2 revocation window explicitly: an allow can
remain effective through an already-issued SSH certificate until TTL expiry
(adm 48h, agt 24h, atm 8h), because there is no CRL/KRL recall channel.
It asked flex-auth, as the access engine, whether a decision lifetime shorter
than the certificate TTL is meaningful under that PEP behaviour, and if so what
downstream contract should consume the shorter lifetime.
Disposition
Answered, not declined. The lifetime is meaningful, and the mismatch ops-warden noticed is not a defect on either side. It is two different objects with two different revocation stories.
A decision lifetime is authority to issue, not authority to use
lifetime.expires_at (docs/decision-record-contract.md, default TTL 15m)
bounds one thing: how long that particular allow may be relied on as the
authority for the action it decided. For ops-warden the action is sign, and
the action completes at issuance. Once the certificate exists, the decision has
been consumed; nothing in the decision record reaches the artifact.
flex-auth does not claim otherwise, and a PDP that did would be lying. The estate already ruled that fail-open is not expressible by a PDP because there is no evaluator in that path; recall of an issued credential is the same shape of claim. There is no flex-auth in the path of an SSH session already authenticated by a valid certificate.
So a 15m decision lifetime against a 48h adm certificate is not an
inconsistency to reconcile. It says the gate's answer goes stale in 15 minutes;
it says nothing about the certificate, and was never asked to.
The downstream contract is the pre-sign gate itself
That is the only consumer of the shorter lifetime, and it consumes it two ways:
- No replay past
expires_at. An allow may not be reused to authorise a secondwarden sign, and a cached verdict must not outlive its TTL. This is already the published rule (docs/decision-input-freshness.md, "How to read this as a consumer") and it is the whole of what the lifetime binds. - Fresh Check per sign. Because the lifetime is short and claims are not cached PDP-side, revocation of a subject, actor, or approval claim is visible to the next sign immediately — deadline 0 for the claim class. That is the real value of the short lifetime: it bounds how long a revoked principal can keep obtaining new certificates, which is the part flex-auth can bound.
The lever that shortens effective access is already deployed
If the goal is to shrink the window in which a revoked principal retains live
access, the mechanism is not a shorter decision lifetime — it is a shorter
certificate TTL, and the requested TTL is already a policy input. The
shipped ops-warden.ssh-certificate.sign package denies ttl_out_of_bounds
before OpenBao is reached (verified 2026-06-29). Capping the requested TTL per
actor class or per zone in the policy package is a policy change flex-auth can
make and explain; asking the decision lifetime to reach past issuance is not.
flex-auth is not proposing that change here. Certificate TTLs are ops-warden's to set, and this record does not reopen them.
Consequences
- ops-warden's §9.7.2 statement — allow effective through certificate TTL, bounded by 48h/24h/8h, no recall channel — is correct as written and correctly owned by the PEP. flex-auth does not want that residue moved to the PDP's side of the line.
- No change to
flex-auth.decision-record.v1.lifetimekeeps its published meaning; this record states the boundary it already had rather than adding one. - If ops-warden ever wants a certificate TTL bound to policy rather than to actor class, that is a policy-package change under a new workplan, not a lifetime semantics change.
FLEX-DEC-2026-005 — secrets-engine policy package is expected but unpublished; /v1/check has no estate-wide endpoint by design
id: FLEX-DEC-2026-005
kind: decision
title: secrets-engine policy package is expected but unpublished; /v1/check has no
estate-wide endpoint by design
status: resolved
origin: cross-repo
origin_ref: secrets-engine SECRETS-WP-0009-T03
owner: flex-auth
affects:
- flex-auth
- secrets-engine
- approval-engine
requested_dispositions:
- answer
decided_by: flex-auth (access-engine / PDP)
rationale: 'Two answers. (1) Yes, a real package is expected, and flex-auth authors
it in this repo as it did for every other consumer; the reserved coordinate is secrets-engine.catalog-lane.lifecycle
at version v1, and it does not exist yet. Until it is published and pinned, secrets-engine
keeping SECRETS_ENGINE_AUTHORIZATION_POLICY_PACKAGE and _VERSION as required configuration
with no fallback is the correct shape and flex-auth endorses it. (2) POST /v1/check
is deployed, but there is no estate-wide PDP address and there is not meant to be
one: each consumer gets its own cluster-local pin whose NetworkPolicy default-denies
ingress except from that one approved workload. The 2026-09-06 probe finding no
reachable PDP is the design working, not an outage. A reachable endpoint for secrets-engine
is a per-consumer pin that follows its policy package.'
created: '2026-09-05T23:08:51.144287Z'
updated: '2026-09-06T00:00:00.000000Z'
decided_at: '2026-09-06T00:00:00.000000Z'
state_hub_decision_id: "f318e4e2-6195-4db8-b8c0-c71eeecad766"
Context
secrets-engine applied flex-auth's earlier PDP answers in commit 627810b
(resource.type: secret-catalog-lane, resource.system: secrets-engine,
catalog id as request.resource.id, and a join that validates
ActionAuthorization.decision.binding.request_digest against
flex-auth.decision-record.v1). It then asked two questions, with Glas
real-key execution (SECRETS-WP-0009-T03) waiting on the answers.
Disposition
Answer 1 — yes, a package is expected; it does not exist yet
Every shipped consumer's policy package is authored in this repository, not
in the consumer's, and baked into the image: ops-warden.ssh-certificate.sign
(v2), tenant-engine.write-api.mutate, user-engine.portal.authorize,
railiance-platform.credential-grant.issue, qonto-assistant.finance-read.
secrets-engine follows the same path. The reserved coordinate is:
| Package | secrets-engine.catalog-lane.lifecycle |
| Version | v1 |
This is a reservation, not a publication. No such package exists in
examples/ today, and until one is published and pinned, a Check naming it
evaluates nothing. FLEX-WP-0021 carries the work.
secrets-engine's decision not to default the pin was the right call and
flex-auth endorses it explicitly: keeping
SECRETS_ENGINE_AUTHORIZATION_POLICY_PACKAGE and _VERSION as required
configuration with no fallback is exactly correct. Defaulting them would have
pinned production to example vocabulary nobody publishes, and the failure would
have been a silent evaluation against a package that is not there rather than a
loud missing-configuration error.
Answer 2 — /v1/check is deployed; there is no estate-wide address, by design
The 2026-09-06 probe finding no reachable PDP found the design working.
flex-auth runs as per-consumer cluster-local pins, not as one shared
service. Three are live (flex-auth-ops-warden, flex-auth-tenant-engine,
flex-auth-user-engine). Each is a three-document manifest whose third document
is a default-deny NetworkPolicy admitting ingress from exactly one approved
consumer workload, and each is pinned to its own image digest so one consumer's
policy can roll without moving another's (deploy/README.md). Caller
authentication is per-pin: on the ops-warden pin it is enforce, so an
anonymous /v1/check is 401 and a token bound to another protected system is
403 (FLEX-WP-0016).
There is therefore no address for secrets-engine to reach because none has been created for it. A reachable endpoint is a per-consumer pin, and a pin follows its policy package — so both questions resolve to the same piece of work, in that order: publish the package, then pin it.
Consequences
FLEX-WP-0021is opened in this repo to publishsecrets-engine.catalog-lane.lifecyclev1 and stand up aflex-auth-secrets-enginepin. It needs secrets-engine's real action vocabulary as input; the examplesecrets-engine.lifecycle/v1vocabulary is explicitly not it.- secrets-engine should keep the pin required and unset until the package is published. Nothing in this record authorises a fallback value.
- No change to
flex-auth.decision-record.v1. The digest join secrets-engine built against it is correct as described.
FLEX-DEC-2026-006 — ActionAuthorization deferral accepted; G3 is closed and does not argue for ratifying it
id: FLEX-DEC-2026-006
kind: decision
title: ActionAuthorization deferral accepted; G3 is closed and does not argue for
ratifying it
status: resolved
origin: cross-repo
origin_ref: APPROVAL-IN-0002 / GH-DEC-2026-003
owner: flex-auth
affects:
- flex-auth
- approval-engine
- gate-house
- secrets-engine
requested_dispositions:
- accept
- contest
decided_by: flex-auth (access-engine / PDP)
rationale: 'Accepted, and flex-auth argues against its own proposal. The composed
ActionAuthorization object was never ratified; the approval-claim plus DecisionEnvelope
split lands each check on the layer that owns it and drops none. approval-engine
asked whether the open G3 finding (DecisionEnvelope carries no lifetime) argues
for ratifying the composed object now. It does not, because G3 is already closed
the other way: FLEX-WP-0019 added lifetime to the DecisionEnvelope itself, required
on every allow by schema conditional, published 2026-09-02. The revisit trigger
is spent, and it resolved by adding a field rather than by composition, so the decision
now stands alone and needs no bundle. Separately, the provenance.authority == state-hub
constant is flex-auth defect: it came from examples/caring/action_authorization.json,
which contradicted our own ownership section. Corrected at source, with the schema
and contract doc marked deferred-not-withdrawn so no other consumer builds a validator
against them.'
created: '2026-09-05T23:29:20.156770Z'
updated: '2026-09-05T23:29:20.156770Z'
state_hub_decision_id: "762e5d75-2377-4e1a-bcea-c57398ef636b"
Context
approval-engine filed APPROVAL-IN-0002 with gate-house and raised it here
directly, because the request touches flex-auth's artifact and accepting the
shelving is flex-auth's call.
The trigger was concrete damage: secrets-engine built its PEP validator against
the ActionAuthorization schema in
docs/action-bound-authorization-contract.md, then pointed it at
GET /v1/approvals/{id}/claim. It rejects every response. Both envelopes
declare schema_version: 0.1, so the mismatch surfaces late as a field or
authority error that reads like an approval-engine outage rather than a
contract mismatch.
Disposition
Accepted. The approval-claim is the step-1 artifact; ActionAuthorization
is not required on that path; a PEP validates across the claim (approval fact)
and flex-auth's step-2 DecisionEnvelope (exact CheckRequest match and policy
pin).
flex-auth argues against its own proposal here. The composed object had the
PIP republish flex-auth's decision, which is the part that was wrong: it moves an
artifact across a layer boundary that the estate's own §2 rule says it should not
cross. flex-auth used that rule to decline authentication evidence in
FLEX-DEC-2026-002 and to win §17's decision-record schema in
FLEX-DEC-2026-003. It applies the same way when it costs us the object.
The split drops no check. Each of the five verifications in the contract's
"Required verification" section lands on the layer that owns it — status,
validity window, supersession, and distinct authenticated approvers on the
approval-claim; effect, binding match, digest, and policy pin on the
DecisionEnvelope.
Answer to the question asked before the decision: G3 does not argue for it
approval-engine named the open G3 finding — DecisionEnvelope carries no
lifetime — as a revisit trigger, if it is settled by composition rather than
by adding a lifetime field, and asked flex-auth to say so before the
decision if G3 is the strongest case for ratifying now.
It is not, and the reason is that G3 is already closed, the other way.
FLEX-WP-0019 closed it by adding the field. schemas/decision_envelope.schema.json
now carries a conditional requiring lifetime whenever effect is allow; the
duration comes from the policy package's allow_ttl with a 15m engine default,
and a package declaring allow_ttl: none produces a deny with reason
allow_lifetime_unstated rather than a standing grant. This was published in
docs/decision-record-contract.md on 2026-09-02.
So the trigger is spent, and it resolved on the branch that removes the argument
rather than the one that supports it. A DecisionEnvelope now states its own
end without borrowing ActionAuthorizationValidity. The bundle is not needed to
give an allow a lifetime, which was the only structural thing it did that the
split does not.
The 2026-08-29 alignment review's table still shows G3 open. That file is a
dated review record and is not rewritten; this record supersedes its status.
The provenance.authority == "state-hub" constant is flex-auth's defect
Correct on the merits, and correctable regardless of how the deferral landed.
approval-engine located a real contradiction inside one flex-auth document
set, and the source is narrower than the report suggested — it is a fixture, not
prose:
"provenance": { "authority": "state-hub" }
That was the tail of examples/caring/action_authorization.json, directly
contradicting the same contract's ownership section: "State Hub decision
records are coordination and provenance evidence; they are not the runtime
approval authority." A consumer reading the example rather than the paragraph
gets the wrong constant, which is what happened.
Corrected at source. The example now names approval-engine as the approval
fact's authority and flex-auth as the decision's, and says State Hub is neither.
Its stale secrets-engine.lifecycle policy pin is also corrected to the
reserved-but-unpublished coordinate from FLEX-DEC-2026-005.
Consequences
docs/action-bound-authorization-contract.mdstatus is now deferred, not withdrawn, with the two-step split stated up front and an explicit "do not build a validator against it" warning. ThePROPOSEDmaterial is retained as the record of what was proposed.schemas/action_authorization.schema.jsoncarries the same deferral in itsdescription, so a machine reader sees it too.schema_versionstays0.1; bumping it would imply a ratified successor that does not exist.- The execute-time half is untouched and stays published:
POST /v1/check,binding, the canonical request digest, andflex-auth.decision-record.v1. Nothing secrets-engine built against the digest join is invalidated. - No revisit trigger remains open on flex-auth's side. If the composed object is ever revisited, it needs a fresh argument, not G3.
FLEX-DEC-2026-007 — A claim cannot name the request that carries it: publish approval_binding_digest
id: FLEX-DEC-2026-007
kind: decision
title: 'A claim cannot name the request that carries it: publish approval_binding_digest'
status: resolved
origin: cross-repo
origin_ref: secrets-engine T03 re-verification / GH-DEC-2026-008
owner: flex-auth
affects:
- flex-auth
- secrets-engine
- approval-engine
- gate-house
requested_dispositions:
- resolve
decided_by: flex-auth (access-engine / PDP)
rationale: 'secrets-engine found that an approval pdp_digest recorded at issue time
can never equal the request_digest of a request that carries the claim inside its
hashed context, because the claim is part of the context that is hashed. The circularity
is structural, not a fixture defect, and it made GH-DEC-2026-008 unimplementable
for exactly the dual-control case it was written for. flex-auth owns the canonical
request digest, so the resolution is ours. Publishing binding.approval_binding_digest:
the same digest computed with context.approval removed, present only when a claim
was carried, stable across attaching the claim, and therefore nameable by a pdp_digest
recorded at issue. Deliberately additive rather than a redefinition: request_digest
keeps covering the claim and remains the replay identity, because two requests differing
only in which approval was presented must not share a replay identity when one allows
and the other denies dual_control_required. Tests assert the two functions disagree
on a claim-bearing request and agree on a claim-free one.'
created: '2026-09-06T12:52:06.960329Z'
updated: '2026-09-06T12:52:06.960329Z'
state_hub_decision_id: "4f0a0de3-1b21-48d1-b157-53bc1f6d03d8"
Context
secrets-engine confirmed FLEX-WP-0021-T03 — both replay envelopes reproduce
— and in doing so found a defect in their own digest join: they were hashing
id, policy_version, and caring_context, which
canonical-request-digest.md excludes.
Their previously pinned constant had been computed with id inside the
material, so it was wrong and its passing proved nothing. Two real envelopes
caught it.
Re-verifying against the regenerated destroy fixture, they then found
something neither repository can fix alone.
The finding
A
pdp_digestrecorded at issue time cannot equal therequest_digestof a request that carries the claim in its context.
Completing context.approval to a full approval-claim is what moved the
destroy fixture's digest — because context is part of the digest material.
That is correct behaviour, and it is circular: embedding the claim changes the
very digest the claim would need to name.
It is not fixture staleness. It holds for every dual-control request whose claim
travels in context, which is the shape GH-DEC-2026-008 had just ruled
mandatory: claim.binding.pdp_digest must equal the decision's request digest,
and a claim without one must not be used on the PDP path. Left unresolved, the
ruling would have been unimplementable for exactly the case it was written for —
and destroy would have been permanently un-allowable in production, failing
closed forever on a check that could never pass.
Disposition
flex-auth resolves it, because flex-auth owns the digest. secrets-engine
raised it correctly and could not fix it; approval-engine records the value but
does not define it; gate-house ruled on the correspondence but the mechanism is
the PDP's.
binding.approval_binding_digest is published: the same canonical material with
context.approval removed, emitted only when the request carried a claim.
claim.binding.pdp_digest == decision.binding.approval_binding_digest
An approval issued against a claim-free Check records that Check's
request_digest; the later claim-bearing request reproduces the same value in
approval_binding_digest. The circularity is broken because the excluded key is
exactly the one whose contents would otherwise perturb it.
Why this is a second digest and not a redefinition
The tempting fix is to drop context.approval from request_digest entirely.
That is wrong, and the reason is worth recording because it is not obvious.
request_digest is the §6.4.2 replay identity. Two requests differing only in
which approval was presented must not share one, because their decisions
differ: one allows, the other denies dual_control_required. Collapsing the two
digests would let an allow obtained with a valid claim be replayed against a
request carrying none. The decision is not a function of the approval-excluded
material, so the approval-excluded material cannot be the replay identity.
So request_digest still covers the claim and still moves when it changes.
approval_binding_digest deliberately does not move, and is documented as not
a replay identity. pkg/api/approval_binding_test.go asserts the two
functions disagree on a claim-bearing request — a test that a distinction is
real is how a distinction that looks like duplication is defended, which is
approval-engine's formulation and it applies here.
The fixture demonstrates it rather than asserting it
examples/secrets-engine/replay/decision_destroy_dual_control.json now carries a
claim whose pdp_digest equals the envelope's approval_binding_digest, with
pdp_path: true. Changing the claim's contents moved request_digest and left
approval_binding_digest untouched, which is the whole property, visible in two
files a consumer can diff.
Consequences
- Additive to
flex-auth.decision-record.v1. No existing digest changed meaning, and no consumer that never sendscontext.approvalsees the field at all —approval_binding_digestis omitted rather than duplicated onto every ordinary decision. secrets-enginemust comparepdp_digesttoapproval_binding_digest, never torequest_digest. Their fixture pins move because the fixture's claim changed; the property does not.approval-engineneeds no change. The value they record at issue is already the right one.- The three shapes
secrets-engineoffered are answered by the first: the digest an approval names is computed withoutcontext.approval, and it is now stated in the contract rather than left to be discovered.
FLEX-DEC-2026-008 — secrets-engine.catalog-lane.lifecycle v1 had no tenant rule and allowed a foreign tenant; v2 supersedes it
Date: 2026-09-06
Status: accepted
Workplan: FLEX-WP-0021 (T02 reopened, T05)
Raised by: flex-auth, answering glas-harness's tenant-alignment request
Context
glas-harness asked flex-auth to reconcile three tenant values before real
credentials are materialized — approval store platform, proposed client JWT
tenant:coulomb, policy tenant tenant:platform — and to return
wrong-tenant denial evidence.
There was none to return. secrets-engine.catalog-lane.lifecycle v1 contained
no reference to input.tenant anywhere: not in well_formed, not in the
denial ladder, not in a test. Run against the deployed package, a rotate on
lane:glas-primary under tenant: tenant:coulomb returned:
decision:066e629bbf0c0924 effect: allow reason: catalog_lane_policy_matched
policy_version: v1 binding.tenant: tenant:coulomb
The exact value in the JWT that glas-harness flagged as needing reconciliation
was allowed by the package it was being reconciled against.
Why nothing caught it
Three independent covers failed in the same direction.
- Every fixture carried
tenant:platform. 29 fixtures, 11 of them allows, and not one varied the field. A suite that never varies an input cannot report on it, and its passing rate says nothing about it. FLEX-WP-0021-T02's own gate named it. The task text required "wrong- tenant deny" among the minimum fixtures. The task was recorded done with the gate unmet — the gate was written, read, and not executed.- The engine does not supply the check.
tenantis hashed intobinding.request_digestand rendered in the decision record, so it is visible in every envelope. Visible is not enforced. Nothing in the evaluation path compares the request tenant to anything.
railiance-platform, qonto-assistant, ops-warden, and user-engine all
carry the branch. This package was the single outlier, which is the strongest
argument that the omission was an oversight rather than a scoping decision.
Decision
Tenant scoping is the policy package's responsibility, and a package with no
tenant rule is not tenant-scoped at all. v2 adds known_tenant to
well_formed and wrong_tenant as the first rung of the denial ladder, above
wrong_system.
Three tests and three fixtures, chosen so each can only pass for the intended reason:
- wrong tenant on an otherwise-valid request — proves the tenant alone carried the denial.
- absent tenant — the first draft read
input.tenant != known_tenantdirectly, which is undefined on a missing key in Rego, so the branch dropped and the ladder reportedno_matching_rule. Still a deny, so still fail-closed, but it named the wrong cause.request_tenant := object.get(input, "tenant", "")fixes it. A denial ladder that reports the wrong rung is a diagnostic defect even when the effect is right — a consumer debuggingno_matching_rulelooks at their action, not their tenant. - wrong tenant carrying a fully valid approval-claim — asserts the ordering:
a foreign tenant with perfect dual-control evidence is denied
wrong_tenant, not invited to present a better claim.
Why v2 rather than an amended v1
This package was corrected once before, at v1, without a version bump: the dual-control rule had been written against an invented claim shape and was unsatisfiable. That correction stayed v1 because the defective rule denied everything — nothing had been wrongly allowed and no consumer could have relied on it.
This one runs the other way. A consumer pinned to _VERSION=v1 would keep
receiving allows it should never have had, with no signal that the rule beneath
the version string had changed.
A fail-open correction must be visible to a consumer as a version change; a fail-closed one need not be. v1 is superseded, not amended.
The tenant reconciliation itself
flex-auth answers only for the values it owns and does not map the other two.
| Value | Owner | flex-auth's position |
|---|---|---|
CheckRequest tenant |
flex-auth | tenant:platform — the only accepted value on this lane, now enforced |
Approval store platform |
approval-engine |
not ours to interpret; flex-auth reads approvals as claims and never mutates them |
Client JWT tenant:coulomb |
key-cape | denied by policy today, with the receipt above |
Resolved by the operator, 2026-09-06. glas-harness decision
5ed3fb35-eca9-413a-82b9-95171ba85bf6
(glas-harness/docs/platform-tenant-decision.md): tenant:platform is accepted
as the platform management, administration and services tenant — the landlord
zone — with exact spelling required across the approval store, both proposed
service-client JWT claims, and the lifecycle CheckRequest. No alias to
platform or tenant:coulomb, and no implicit cross-tenant grant.
The operator's choice is what v2 already enforces, and flex-auth widens
nothing. Verified rather than asserted: known_tenant := "tenant:platform"
compared by exact string equality, no case folding and no normalisation of
tenant anywhere in the evaluation path. The only occurrences of
tenant:coulomb in the package are the correction narrative and the three deny
tests. tenant:platform is a single constant, so registering a second tenant
would be a visible edit to it rather than a configuration change.
This resolves the choice. It does not resolve verification or the remaining
admission gates, as glas-harness was explicit about — the caller access path
is FLEX-WP-0023.
No mapping is published, and spelling similarity is not one. This is the
same refusal as the action-vocabulary mapping ruled out in GH-DEC-2026-008,
and for the same reason: a mapping asserted between two vocabularies by
resemblance produces a confident wrong answer, and here it would fail open —
admitting a foreign tenant on the strength of a shared word. If tenant:coulomb
is intended to reach this lane, the owner-reviewed answer is either a JWT
carrying tenant:platform or a policy change registering a second tenant. Both
are decisions with named owners; neither is a spelling observation.
Consequences
- The deployed pin serves v1 until a redeploy lands. The over-permissive package is live now. Deployment approval is the user's; this record does not grant it.
- No consumer re-pinning is needed for the digest join:
request_digestis computed over tenant/subject/action/resource/context and not over the package, so bothFLEX-WP-0021-T03replay digests are byte-identical at v2. Onlypolicy_versionandpolicy_package_digestmoved. FLEX-WP-0021-T02's gate is now actually met rather than recorded as met.- Generalizable beyond this package: a fixture suite that holds an input constant provides no coverage of it, however many fixtures pass.
The sweep, run rather than recommended
Every published package's fixture suite was checked for a constant tenant:
| Package | Fixture tenants | Tenant rule in package |
|---|---|---|
secrets-engine |
now 3 distinct | added at v2 |
qonto-assistant |
2 distinct | yes, wrong_tenant |
user-engine |
2 distinct | yes, cross_tenant |
ops-warden |
constant | yes, wrong_tenant — rule covered by Rego tests, not fixtures |
railiance-platform |
constant | yes, wrong_tenant — same |
tenant-engine |
constant | none |
ops-warden and railiance-platform have the rule and exercise it only in
Rego tests, which is thin but not a hole.
tenant-engine has no tenant rule and is deployed. Verified: a
tenant.create allow re-sent under tenant: tenant:coulomb returns
allow / write_api_policy_matched.
It is not the same fix. secrets-engine's request tenant is the calling
identity's own tenant, so a constant is correct. tenant-engine's subjects all
sit in tenant:platform while its fixtures send tenant:friendly:binky — the
request tenant there names the target of the operation, and a service whose
whole purpose is creating tenants legitimately acts across them. A constant
known_tenant would break it. The correct rule is a relation between the
request tenant and the resource, and it needs tenant-engine to say which.
So the finding is recorded and not unilaterally patched: what is wrong today
is that the omission is undocumented, and a reader cannot tell a deliberate
cross-tenant scope from a missing rule. Carried as FLEX-WP-0022.
FLEX-DEC-2026-009 — The decision record cannot show who called: caller identity is absent from flex-auth.decision-record.v1
Date: 2026-09-06
Status: accepted, gap open
Workplan: FLEX-WP-0023
Raised by: flex-auth, while designing the operator access path glas-harness asked for
Context
glas-harness asked for "a supported owner access path and authenticated caller
binding/lifetime with positive and negative tests", and ruled out Service DNS
and a permanent operator token. Designing that path
(docs/operator-caller-access-path.md) produced the answer — a TokenRequest
token, short-lived, audience-scoped, bound by exact sub to one
ServiceAccount — and then produced a question the design cannot answer.
Where does the authenticated caller appear in the artifact? It does not.
flex-auth.decision-record.v1 has no caller field. provenance carries
evaluator, mode, policy_package_digest, registry_snapshot_digest,
policy_version, and decision_time. binding carries the normalized tenant,
subject, action, resource, context and the request digest. The word caller
does not occur in the schema.
Why this is not the same as the subject
It is tempting to answer that the subject is the caller, and for the
secrets-engine lane it happens to be: subject.id is secrets-engine and the
bound principal is system:serviceaccount:secrets-engine:secrets-engine. That
coincidence is exactly what makes the gap easy to miss.
They are different claims, and callerAuth exists because they are:
subjectis an input to the decision. It is asserted by the caller and the policy reasons about it.- the caller principal is authenticated by flex-auth against the cluster and is not an input to the decision. It gates whether the request may be made at all, per resource system.
A caller bound to secrets-engine may present any subject its policy
permits, and a request whose subject is secrets-engine may arrive from an
unauthenticated caller — which is precisely the state under warn. Reading the
subject as evidence of the caller reads an input as if it were a verification.
The consequence, in the terms the consumer asked in
glas-harness wants a scoped delivery receipt. What the record supports today
is:
this decision permits this action on this resource under this policy version
What it does not support, and what was asked for:
this caller was authenticated, under this identity, with this lifetime, when it obtained this decision
The four negative tests in the access-path design (missing header, wrong ServiceAccount, wrong audience, expired token) can all pass, and no artifact retains the fact that they passed for the request that mattered. The verification is real and leaves no trace. That is the defect.
It is also the same shape as FLEX-DEC-2026-008, one layer up: there, a tenant
was carried into the digest and never compared — visible but not enforced. Here,
a caller is authenticated and never recorded — enforced but not visible. A
control that leaves no evidence and evidence that exerts no control are the two
ways the same seam opens.
Decision
The caller principal belongs in provenance, not in binding.
binding is the §6.4.2 replay identity: the material the decision is a function
of. The caller is deliberately not that material — the same request from a
different authenticated caller must decide identically, or the caller has become
a policy input by the back door. Putting it in binding would change
request_digest, break every consumer's replay join, and assert something
false about what the decision depends on.
provenance is where flex-auth records how the decision was produced, which is
exactly what an authentication event is. The shape, additive:
"provenance": {
"caller": {
"mode": "enforce",
"principal": "system:serviceaccount:secrets-engine:secrets-engine",
"audience": "flex-auth",
"not_after": "2026-09-06T20:10:00Z"
}
}
mode is required and load-bearing: a record from a warn-mode pin must say
so, because a principal recorded under warn was observed and not enforced,
and a reader who cannot tell those apart will read an unverified string as
verification. Under disabled the object is {"mode": "disabled"} with no
principal — absence stated rather than a missing key, following the
pdp_digest precedent from approval-engine (FLEX-DEC-2026-007).
not_after comes from the reviewed token's exp. internal/callerauth.Identity
carries only Username and Audiences today, so the expiry is validated by
TokenReview and then discarded. It has to be captured to be recorded, which is
the one real code change here.
Consequences
- Additive to
flex-auth.decision-record.v1. No digest changes meaning and no existing consumer field moves — the same reasoning that letapproval_binding_digestbe additive inFLEX-DEC-2026-007. - Every consumer's replay join is unaffected, because
request_digestis computed overbindingand the caller is not inbinding. Stated explicitly so nobody re-pins in response to this record, as they nearly did for v2. security-layer-model§17 makes the decision-record schema flex-auth's, so this is ours to change and ours to have missed. Worth reporting togate-houseas a finding against the v0.8 round rather than only fixing: §9.7.2 promoted registry-snapshot provenance to a conformance prerequisite on the argument that a decision turning on registry content must be replayable from its own record. A decision gated by caller authentication is not auditable from its own record by the same argument, and the standard does not ask for it.- Does not block the access path. The
TokenRequestshape stands on its own; this makes its result visible in the artifact rather than only in the pin's logs.
FLEX-DEC-2026-010 — The response channel is unauthenticated: stated as a stance, and the envelope will be signed
Date: 2026-09-06
Status: accepted, stance recorded, fix open
Workplan: FLEX-WP-0024
Raised by: secrets-engine, who asked rather than mitigating it locally
Context
secrets-engine probed the address this repo handed them under
FLEX-WP-0021-T05 and found that on the workstation a bare
*.svc.cluster.local name resolves through search ad.binect.de to one
unrelated public host. Reproduced here:
$ getent hosts flex-auth-secrets-engine.flex-auth.svc.cluster.local
80.158.43.29 ...svc.cluster.local.ad.binect.de
$ getent hosts this-service-does-not-exist.flex-auth.svc.cluster.local
80.158.43.29 (identical — suffix expansion, not a record)
$ getent hosts flex-auth-secrets-engine.flex-auth.svc.cluster.local.
(no resolution)
They then asked the question the misdirection makes urgent: how is the response channel authenticated? And they declined to answer it locally, on the grounds that choosing a transport control for flex-auth's service is not a consumer's call and inventing one is how a consumer ends up authoring semantics it does not own. That is the right boundary — the same one that kept them from authoring a tenant mapping — and it is why this record exists instead of a mitigation in their repo.
The stance, stated because an unstated stance is an assumption
The response channel is unauthenticated. The pin serves plain HTTP on
:8080; flex-auth.decision-record.v1 carries no signature field. A responder
that knows the package id and version — both published in this repo — can return
a well-formed effect: allow with a matching binding, and every check a
consumer performs will pass it.
callerAuth authenticates the caller to the PDP. Nothing authenticates the PDP
to the caller. Those are not two views of one control.
Why the digests do not help, which is the part that would be misread
A consumer that recomputes request_digest, policy_package_digest, and
registry_snapshot_digest, finds all three correct, and concludes the responder
is genuine has verified nothing about who answered.
Every input to those digests is either sent by the caller or published. The request material is what the caller just transmitted. Both the package and registry digests are computable from files in a public repo. A forger reproduces all three exactly, and the more digests flex-auth publishes the more authenticated a forged envelope looks.
The digests establish integrity of the binding, never authenticity of the
source. This is the third instance of one seam in as many decisions:
FLEX-DEC-2026-008 had a tenant carried into the digest and never compared —
visible, not enforced; FLEX-DEC-2026-009 has a caller authenticated and never
recorded — enforced, not visible; here the record is verifiable and unauthentic —
checkable, but not evidence of anything. Each time, an artifact invited a
conclusion one step stronger than it supports.
The consequence for a fail-closed consumer
secrets-engine runs fail-closed in production, and stated that its posture
"silently assumes the PDP is the PDP". Precisely:
Fail-closed protects against a PDP that is absent, not against one that lies. An unreachable PDP denies. A lying PDP allows. The posture that makes the first safe does nothing about the second, and a consumer reading its own fail-closed configuration as covering both has drawn exactly the wrong inference.
Today that risk is carried entirely by their pin being unset — which is to say, by nothing structural.
Decision
Three parts, in increasing durability.
1. Never publish a bare Service name again. Publish the trailing-dot FQDN
and say in-cluster only. The trailing dot makes resolution fail rather than
succeed at the wrong place, which is the behaviour a fail-closed consumer needs
from a name. Corrected in examples/secrets-engine/README.md, SCOPE.md, and
the FLEX-WP-0021-T05 handover note.
2. For the operator shape, kubectl port-forward is the recommended path, and
not only for reachability. It resolves no DNS name, targets one named pod
explicitly, and runs over the API server's TLS with the operator's cluster
credentials — so it is currently the only path on which a consumer knows it
is talking to the real pin.
Note this is the exact reverse of the caller direction, where the port-forward
bypasses the NetworkPolicy entirely (FLEX-WP-0023). The two properties are
independent and point opposite ways. They must be stated separately; summarising
either as "the network protects it" gets one of them backwards.
3. Sign the decision envelope. §17 makes the decision-record schema flex-auth's, so responder authenticity is ours to provide rather than a deployment concern to delegate. A detached signature over the canonical envelope is preferred to transport-only mTLS, for two reasons:
- It survives the transport. A decision recorded now must still be attributable when it is audited later, and mTLS authenticates a connection that no longer exists.
- It composes with
FLEX-DEC-2026-009. A record that names its authenticated caller and carries flex-auth's signature is an audit artifact. Either alone is a claim.
mTLS remains worth having for the live channel and is not excluded; it is not a substitute.
Consequences
- flex-auth published a hazardous address. The handover was ours, the probe was theirs, and the finding is against this repo. Recorded that way.
- Until signing lands, a consumer must not treat digest recomputation as
responder verification.
docs/decision-record-contract.mdsays so explicitly — the contract is the only place a consumer is guaranteed to read. FLEX-WP-0024carries signing. Key custody is not flex-auth's to invent: route it throughwarden routeand OpenBao rather than minting a key in this repo, per the estate credential-routing rule.- Worth carrying into the v0.8 assent round. §6.4 obligates the PEP to act on a decision and §9.3 splits degraded mode by owner, but nothing in the standard requires a PDP's response to be authenticable, so a conformant PEP can be fully conformant while trusting a forged allow. That is a gap in the standard and not only in this implementation.
FLEX-DEC-2026-011 — Review of security layer model v0.8: assent with four findings, one of them fail-open
Date: 2026-09-07
Status: accepted — assent to v0.8 with findings
Reviewed: net-kingdom/canon/standards/security-layer-model_v0.8.md (status proposed)
Workplan: FLEX-WP-0023-T05, FLEX-WP-0024-T04
flex-auth (access-engine) assents to v0.8 as a boundary. Four findings follow,
ordered by severity rather than by section. F1 is fail-open and we consider it
blocking for acceptance; the rest are corrections that do not.
Two of the five contributions credited to us in §15 are confirmed dispositioned
against the body rather than the change log: §6.4 obligation 5's replay-identity
property is stated as we reported it, and §9.7.3's exclusion-scoped comparison
now names binding.approval_binding_digest and is implementable. §13.1's axis
column and the cross-axis statement are as described.
F1 — "a decision from access-engine" is not a checkable predicate, and §6.4's mechanical test does not make it one
Severity: fail-open. A conformant PEP performing exactly the prescribed checks accepts a forged allow.
§6.4 obligation 1 requires a PEP to hold "a decision from access-engine
identifying the request it was rendered for". Obligation 2 supplies the test and
is emphatic that it is not a judgement call: "replay is permitted iff the
canonical request digest matches and the decision's lifetime holds… The test is
mechanical, not a matter of implementer judgement."
That test establishes which request a decision is for. It establishes nothing about who issued it, and it cannot.
Every input to every digest a PEP can check is either sent by the caller or
published. The request material is what the PEP itself transmitted.
policy_package_digest and registry_snapshot_digest are computable from files
in a public repository. A responder that knows the package id and version —
both published — reproduces all three exactly and returns a well-formed
effect: allow. The digests establish integrity of the binding, never
authenticity of the source, and the more of them a PDP publishes the more
authenticated a forged envelope looks.
This is ours before it is the standard's: flex-auth.decision-record.v1 carries
no signature and pins serve plain HTTP. We recorded it as FLEX-DEC-2026-010
before writing this review, and FLEX-WP-0024 carries the fix. But the standard
is not merely silent — it uses the phrase "a decision from access-engine" as
though it named a verifiable property, and prescribes a mechanical test that a
careful implementer will reasonably read as discharging it.
The internal inconsistency that makes this sharp. §9.4 requires the approval
object to have "durable, authenticated entries". §6.4 obligation 5 then
requires that where a PEP's decision rests on more than one artifact, "each
artifact MUST be validated against the layer that owns its data", and the live
instance it names is exactly the approval-claim / DecisionEnvelope pair.
So the standard requires authenticity of the PIP's artifact and not of the PDP's, and then writes an obligation over both. Obligation 5 is unsatisfiable for half the pair it was written about: a PEP can validate the approval's authenticity and cannot validate the decision's. The asymmetry is not argued anywhere; we believe it is an oversight rather than a position.
Consequence for every fail-closed consumer, and it is the operative sentence:
fail-closed protects against a PDP that is absent, not against one that lies.
An unreachable PDP denies. A lying PDP allows. §9.3's whole degraded-mode
apparatus — two failure cases, two owners, published stance maps — addresses
absence. Nothing in the standard addresses a responder. secrets-engine put it
better than we did: their production posture "silently assumes the PDP is the
PDP", and today that assumption is carried by their pin being unset rather than
by anything structural.
§16 has already noticed this one layer up and not seen it here. It carries "Publication integrity of the Taxonomy layer itself. This standard demands reconstructability of decisions while its own publication path has no digest, freeze, or rollback discipline." That is precisely the same observation about the standard's own text. Applied to the artifact the standard regulates, it is F1. The gap was visible from inside and got recorded against the wrong artifact.
Proposed: §6.4 obligation 1 should require the decision be attributable to
access-engine, not merely held; §13 should carry the gap with access-engine
as owner until signing ships. We are not asking for a signature scheme in the
statute — the shape is ours under §17 — only that the obligation stop reading as
satisfied by a digest comparison. A declared gap is honest; an unstated
assumption inside a mechanical test is not.
F2 — An unknown cell can launder an incomplete map into an apparently total one
This answers §16's new question — "whether §6.4's unknown ruling should
extend to other total-map scopes that are absent rather than unknown" — which
gate-house flagged as ours. The distinction is real, and it is not a
distinction in the stance.
Runtime behaviour must be identical: absent fails closed too, and for a
stronger reason. A consumer cannot tell the two apart at the point of failure —
in both cases it holds a scope value with no stance for it, on one code path.
Worse, absent is the case in which the consumer's model of its own scope axis
has just been demonstrated wrong. Being more permissive on the branch reached
by discovering you were mistaken about your own coverage inverts the appropriate
response to surprise. §8's asymmetry forbids it on the same grounds §6.4 already
gives for unknown: the cheapest state for an attacker to induce must not be the
most permissive.
But the distinction belongs in reporting, and omitting it makes the totality obligation unfalsifiable. This is the finding.
§6.4 obligation 3 requires the map be total. A map carrying an unknown cell set
to fail_closed satisfies totality vacuously: every scope the author never
enumerated lands in unknown, fails closed, and no one ever learns which scopes
those were. The map is total by having a catch-all rather than by covering its
axis. The obligation that the published map equal shipped behaviour, asserted by
a test, passes — because the test exercises the catch-all, not the axis.
We have just published the same defect in our own repository and it is the
reason we recognise this one. FLEX-DEC-2026-008: a policy package shipped with
no tenant rule at all while 29 fixtures passed, because every fixture carried the
same tenant. A suite that never varies an input cannot report on it, however many
assertions pass. A stance map with a catch-all cannot report which scopes were
never enumerated, however green its test. Coverage counted rather than
executed, in a stance map instead of a fixture suite.
approval-engine independently reached the remedy that works, and it is better
than ours: vary the value across the near misses that actually occur and require
each to fail, plus a positive asserting the exact value succeeds — otherwise a
deny-everything defect passes the entire negative set.
Proposed: two cells, one behaviour, two meanings. Require a published map to
enumerate its axis's values explicitly rather than relying on a catch-all, and
require an absent-scope hit to be distinguishable in the record from an
unknown-scope hit. Same stance, different finding: an unknown hit is normal
operation under a considered stance; an absent hit is evidence that obligation
3's totality requirement is violated and must surface as a conformance failure
rather than be absorbed silently.
F3 — The strongest obligation in §6.4 has the weakest verification
Obligation 3 states: "The published map MUST equal the shipped behaviour, and that equality SHOULD be asserted by a test." And then, in its own words: "A published map free to drift from the code is worse than none, because it invites reliance it cannot support."
If drift is worse than no publication, the check that detects drift is not a SHOULD. The property is MUST and its only verification is optional, which leaves the sanctioned failure exactly where the paragraph says it is worst: a map published, believed, drifted, and silently wrong.
The cost of promoting it is zero. §13.1's own Shape column records a test for four of the five rows; the SHOULD is already met wherever a map exists. Making it MUST closes the case where a consumer publishes a map and never pins it, and changes nothing for anyone currently conformant.
F4 — §13.1 marks one violation and not the plainer one in the same table
ops-warden's row carries an explicit mark: "unknown → fail_open is
non-conformant under §6.4 obligation 3 as of this version". Correct, and the
kind of marking §11 exists to require.
ops-mason's row reads "— | — | not published; catalogued PEP-shaped in
§4". §6.4 states "Every PEP-shaped consumer MUST publish its stance map at a
path named in its layer declaration." An unpublished map is a plainer violation
of that obligation than a wrongly-valued cell in a published one, and the
register states it as a fact without marking it as non-conformance.
A reader scanning the table for the bolded non-conformance marks finds one row and concludes the other four are fine. That is the §11 marking obligation applied to the standard's own register — and it is the argument gate-house made to us about our stale G3 row, returned unchanged. We are the worked example for this class, which is why we recognise it rather than why we are entitled to raise it.
On whether you over-read our divergence report
You asked directly, and the answer is no, and the record is right as written.
GH-DEC-2026-009 is grounded in doctrine we did not supply: §8's asymmetry, and
the argument that unknown is the cheapest state for an attacker to induce so a
permissive unknown makes being unclassifiable an escalation requiring no
credential. That reasoning is yours. We reported two conformant maps taking
opposite stances and said explicitly it was not a request that either consumer
change. The distinction between "access-engine reported a divergence" and
"access-engine's preferred stance won" is preserved correctly in both §6.4 and
§13.1.
One thing worth adding to that record rather than leaving implied: the ruling
costs us nothing. flex-auth is not PEP-shaped and publishes no stance map, so
concurring was free, and a PDP's concurrence with a ruling that falls entirely on
other repositories is weak evidence for it. The strength of the ruling is the
asymmetry argument, not our agreement with it. ops-warden bears the cost and
its assent is worth more than ours here.
Disposition
Assent to the boundary at v0.8, with F1 outstanding. We will not treat F1 as
blocking anyone else's adoption — it describes a condition that already exists
under v0.7 and is not created by this version. It is blocking for calling the
PEP obligations complete, and the fix is substantially ours: FLEX-WP-0024
signs the envelope, and FLEX-DEC-2026-009 puts the authenticated caller in
provenance so a record eventually attests both ends of the channel rather
than neither.
FLEX-DEC-2026-012 — A caller could raise its own ceiling: registry facts now win, and the replay digest a consumer was told to compute was never computable
Date: 2026-09-07
Status: accepted
Workplan: FLEX-WP-0025
Raised by: secrets-engine (the digest half, on its first live request); flex-auth (the escalation half, found while answering it)
Context
secrets-engine adopted the operator access path, sent a real CheckRequest
through the enforcing pin, and its validator rejected the allow. Not a near
miss — unsatisfiable. binding.request_digest is computed over material the
consumer never sent: subject.attributes, subject.tenant, resource.tenant,
all contributed by flex-auth's registry.
They declined to fix it locally. Their reasoning is the reason this record exists: they could have compared only the fields they sent and treated additions as benign, but "additions I decided were benign" is a rule authored over flex-auth's registry semantics, and it fails open by accepting a binding that differs from the proposal in some way they did not think to check.
Answering it meant reading the enrichment path, and the enrichment path had a worse defect in it.
The escalation
Enrichment was additive-if-absent. addAttribute wrote a registry value only
where the request had no value for that key:
if _, exists := target[key]; !exists { target[key] = value }
So where a caller supplied a key, the caller's value won and the registry's
never applied. Every registry ceiling and allowlist was advisory. Verified
against the shipped ops-warden package — each is one added key on a request
that otherwise denies:
| Sent by the caller | Registry says | Result before |
|---|---|---|
resource.attributes.max_ttl_hours: 99 |
8 |
allow — a 12-hour certificate the registry caps at 8 |
resource.attributes.allowed_principals: [root, …] |
[platform, root] |
allow — disallowed_principal bypassed |
resource.attributes.allowed_subjects: [<caller>] |
[platform-steward, …] |
allow — unknown_subject bypassed |
The third is the one to read twice: a subject the registry does not know authorized itself by naming itself in the allowlist it was being checked against.
Exploitability, stated honestly. This is not remotely reachable today. The
PEP builds the CheckRequest, ops-warden sends no resource.attributes, and
callerAuth: enforce admits only its bound identity. It is a defence-in-depth
failure: the registry's ceilings were not binding, so any path that lets
attacker-influenced data into a CheckRequest field becomes a full policy
bypass rather than a bounded input problem. And callers sending
resource.attributes is not hypothetical — secrets-engine does it on every
request.
Decision: the registry wins. A registry fact is an authority statement; the
same key on the request is the caller's proposal about itself. Where they
disagree the registry's value reaches policy.
diagnostics.registry_overrode names every displaced key, because a registry
that silently discards a contradicting claim hides that a caller asserted
authority it did not have.
The exception that had to be carved out, and it is a finding of its own
Making the registry win on subject.type denied every secrets-engine allow,
and the reason is worth recording rather than patching around.
The registry's type is CARING vocabulary — Human, Agent, Automation,
Service. The request's subject.type is the protected system's actor
vocabulary — service, adm, agt, atm. Policies compare against the
latter. "Service" is not "service". These are two different fields
sharing a name, and overwriting one with the other is translation rather than
identity, which is exactly what GH-DEC-2026-008 ruled against.
So subject.type keeps fill-if-empty. The residual is real and stated: a caller
can still assert its own subject.type, and packages branch on it. It is not
fixable by substituting a value from another vocabulary — that is strictly worse,
as the denials proved. FLEX-WP-0025 separates the two fields.
Note what surfaced this: the registry's type had been dead data for as long
as the field has existed. Nothing read it, because the caller's value always won.
A defect is invisible while the value it produces is never used.
The digest
binding.request_digest is over the enriched request, and was published as
"the canonical request digest as the published replay test for consumers"
(SCOPE.md, §6.4.2). It cannot be that. The registry is flex-auth's, so a
consumer recomputing over what it sent gets a different value on every request
whose subject or resource the registry knows.
binding.submitted_request_digest is published: RequestDigest over the
request exactly as received, before any enrichment. This is the digest a PEP
compares for §6.4 obligation 2.
Nothing is lost by hashing the pre-enrichment form. Enrichment is a function
of the request and the registry snapshot, and
provenance.registry_snapshot_digest already pins the snapshot — so the
submitted digest together with that digest identifies the evaluated request
completely. Registry staleness between issue and replay is bounded by the
decision's own lifetime and by §9.7.2's per-input-class visibility deadline,
which is the mechanism that already covers exactly this.
request_digest keeps its value and its meaning: what the decision was actually
a function of, and flex-auth's own audit-replay identity. It is now documented as
not a consumer check.
approval_binding_digest needs no submitted form. It is compared to
claim.binding.pdp_digest, which approval-engine recorded from flex-auth's own
output at issue — a flex-auth value against a flex-auth value. But a consumer
must not compute either side itself, and secrets-engine's described pre-flight
step does exactly that, which reintroduces this defect on the dual-control path.
Flagged to them.
Why three rounds of digest work missed it
secrets-engine's replay tests rebuilt the request from envelope["binding"] —
the enriched form. So every digest assertion passed by hashing our output and
comparing it to our output. It survived the excluded-fields fix, the
approval_binding_digest fix, and the tenant fix, because all three were tested
the same way.
That is the fourth instance this week of one mechanism: a self-consistent
artifact agreeing with itself. FLEX-DEC-2026-008 was a fixture suite that
never varied a field; approval-engine's was a fixture built by the function
that omitted the field; ours was a registry value nothing read. Each time the
thing that caught it was a real artifact crossing a repository boundary, not a
review of the assertions.
Consequences
- Existing pins do not move. The three replay fixtures'
request_digestandapproval_binding_digestvalues are byte-identical after this change — those requests contradict no registry fact, so nothing they hash moved.submitted_request_digestis a field to add, not a value to correct. Stated explicitly because the last two corrections both prompted re-pinning. ops-wardenshould be told directly. All three demonstrated escalations are against its package and its pin is enforcing in production.- Regression tests assert the registry value reaches policy on a contradicting request, against attribute keys real packages branch on — a test over an unused key would pass while the escalation stayed open. Verified failing against the old behaviour before being kept.
- The residual is published in
docs/request-enrichment.md: a caller-supplied attribute for a key the registry does not define still reaches policy. A policy enforcing a ceiling or an allowlist MUST read a key its manifest declares, and that is now a review obligation on every package. - Bears on
FLEX-DEC-2026-011F1. A PEP cannot verify who answered; it now at least can verify that the answer is about the request it sent. Those are different properties and neither substitutes for the other.