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
1799 lines
91 KiB
Markdown
1799 lines
91 KiB
Markdown
# Decision records
|
||
|
||
## FLEX-DEC-2026-001 — Assent to GH-DEC-2026-001: Engine framing, access-engine rename, authoring/evaluation split
|
||
|
||
```yaml
|
||
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:
|
||
|
||
1. **Repository identity and runtime identity must be renamed in separate,
|
||
independently revertible steps, repository first.** The name `flex-auth` is
|
||
not only a repo slug: it is a deployed service, an in-cluster DNS name, a
|
||
chart and image name, and — since `FLEX-WP-0016` moved the ops-warden pin to
|
||
`callerAuth.mode: enforce` — a protected-system identifier that inbound
|
||
tokens are bound to. A single-step rename would make every `warden sign` a
|
||
401, including the certificate the `ops-bridge` tunnels depend on. This is
|
||
the same dependency shape that caused `ops-warden`'s `ADR-0006` to defer
|
||
`policy.enabled` permanently.
|
||
2. **`FLEX-WP` prefix ownership stays with the repository across the rename.**
|
||
Existing `FLEX-WP-0001..0018` identifiers 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-guard` and `ops-warden` assent
|
||
remains outstanding and is not flex-auth's to give.
|
||
- `INTENT.md` records 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
|
||
|
||
```yaml
|
||
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:
|
||
|
||
1. **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.
|
||
2. **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
|
||
|
||
1. **Frontmatter `status: proposed` contradicts §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.
|
||
2. **§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
|
||
(`OpenBao` excepted 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**.
|
||
3. **§14 says "All three repositories whose boundaries moved"** above a table of
|
||
four. `audit-core` is 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-0002` is 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-0017` T03/T05 stay `wait` on `approval-engine`, with the two contract
|
||
items above raised as prerequisites for T05.
|
||
- `SCOPE.md` cites ops-warden `ADR-0006` as current; it is superseded by
|
||
`ADR-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
|
||
|
||
```yaml
|
||
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
|
||
|
||
```yaml
|
||
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:
|
||
|
||
1. **No replay past `expires_at`.** An allow may not be reused to authorise a
|
||
second `warden 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.
|
||
2. **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`. `lifetime` keeps 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
|
||
|
||
```yaml
|
||
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-0021` is opened in this repo to publish
|
||
`secrets-engine.catalog-lane.lifecycle` v1 and stand up a
|
||
`flex-auth-secrets-engine` pin. It needs secrets-engine's real action
|
||
vocabulary as input; the example `secrets-engine.lifecycle/v1` vocabulary 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
|
||
|
||
```yaml
|
||
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:
|
||
|
||
```json
|
||
"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.md` status is now **deferred, not
|
||
withdrawn**, with the two-step split stated up front and an explicit "do not
|
||
build a validator against it" warning. The `PROPOSED` material is retained as
|
||
the record of what was proposed.
|
||
- `schemas/action_authorization.schema.json` carries the same deferral in its
|
||
`description`, so a machine reader sees it too. `schema_version` stays `0.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, and `flex-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
|
||
|
||
```yaml
|
||
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`](../docs/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_digest` recorded at issue time cannot equal the `request_digest` of 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.
|
||
|
||
```text
|
||
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 sends `context.approval` sees the field
|
||
at all — `approval_binding_digest` is omitted rather than duplicated onto
|
||
every ordinary decision.
|
||
- `secrets-engine` must compare `pdp_digest` to `approval_binding_digest`, never
|
||
to `request_digest`. Their fixture pins move because the fixture's claim
|
||
changed; the property does not.
|
||
- `approval-engine` needs no change. The value they record at issue is already
|
||
the right one.
|
||
- The three shapes `secrets-engine` offered are answered by the first: the
|
||
digest an approval names is computed without `context.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:
|
||
|
||
```text
|
||
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.
|
||
|
||
1. **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.
|
||
2. **`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.
|
||
3. **The engine does not supply the check.** `tenant` is hashed into
|
||
`binding.request_digest` and 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_tenant`
|
||
directly, which is *undefined* on a missing key in Rego, so the branch
|
||
dropped and the ladder reported `no_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 debugging `no_matching_rule` looks 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_digest` is
|
||
computed over tenant/subject/action/resource/context and not over the
|
||
package, so both `FLEX-WP-0021-T03` replay digests are byte-identical at v2.
|
||
Only `policy_version` and `policy_package_digest` moved.
|
||
- `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:
|
||
|
||
- **`subject`** is 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:
|
||
|
||
```json
|
||
"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 let
|
||
`approval_binding_digest` be additive in `FLEX-DEC-2026-007`.
|
||
- Every consumer's replay join is unaffected, because `request_digest` is
|
||
computed over `binding` and the caller is not in `binding`. 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 to
|
||
`gate-house` as 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 `TokenRequest` shape 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:
|
||
|
||
```text
|
||
$ 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.md` says so
|
||
explicitly — the contract is the only place a consumer is guaranteed to read.
|
||
- `FLEX-WP-0024` carries signing. Key custody is not flex-auth's to invent:
|
||
route it through `warden route` and 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:
|
||
|
||
```go
|
||
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_digest` and
|
||
`approval_binding_digest` values are byte-identical after this change — those
|
||
requests contradict no registry fact, so nothing they hash moved.
|
||
`submitted_request_digest` is a field to add, not a value to correct. Stated
|
||
explicitly because the last two corrections both prompted re-pinning.
|
||
- **`ops-warden` should 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-011` F1. 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.
|