2026-09-01 23:45:48 +02:00
|
|
|
---
|
|
|
|
|
id: APPROVAL-WP-0002
|
|
|
|
|
type: workplan
|
|
|
|
|
title: "Production readiness and consumer adoption"
|
|
|
|
|
domain: infotech
|
|
|
|
|
repo: approval-engine
|
2026-09-02 00:52:04 +02:00
|
|
|
status: active
|
2026-09-01 23:45:48 +02:00
|
|
|
owner: codex
|
|
|
|
|
topic_slug: netkingdom
|
|
|
|
|
created: "2026-09-01"
|
2026-09-10 18:39:30 +02:00
|
|
|
updated: "2026-09-10"
|
2026-09-02 00:52:04 +02:00
|
|
|
reviewed_at: "2026-09-01"
|
|
|
|
|
reviewed_against_commit: "ebce5abb276c01ab29ce2526f3b8abb332dc9e90"
|
|
|
|
|
reviewed_note: >-
|
|
|
|
|
Reviewed against approval-engine's finished spine, KeyCape's RS256/JWKS and
|
|
|
|
|
service-token contract, access-engine caller-auth/binding surface,
|
|
|
|
|
audit-core's deployed authenticated ingestion plus open AUDIT-WP-0009
|
|
|
|
|
approval-source/cadence/reconciliation tasks, and secrets-engine's waiting
|
|
|
|
|
exact-action/decision-consumption tasks. Repo-owned implementation can
|
|
|
|
|
proceed; live T03-T05 closure remains evidence-gated on those owners.
|
2026-09-01 23:45:48 +02:00
|
|
|
origin: residual
|
|
|
|
|
origin_ref: APPROVAL-WP-0001
|
2026-09-01 23:47:54 +02:00
|
|
|
state_hub_workstream_id: "4fa25ad5-f5d0-5592-aa59-085f8ee3edaf"
|
2026-09-01 23:45:48 +02:00
|
|
|
---
|
|
|
|
|
|
|
|
|
|
# APPROVAL-WP-0002 — Production readiness and consumer adoption
|
|
|
|
|
|
|
|
|
|
Move the completed first-cut engine spine into an authenticated, durable,
|
|
|
|
|
observable production service and prove one PEP integration end to end. This is
|
|
|
|
|
the residual production scope deliberately excluded from APPROVAL-WP-0001.
|
|
|
|
|
|
|
|
|
|
The workplan is proposed pending review against the deployment estate and the
|
|
|
|
|
current key-cape, access-engine, audit-core, and secrets-engine contracts.
|
|
|
|
|
|
2026-09-02 00:52:04 +02:00
|
|
|
Review completed 2026-09-01. The plan is active. Production mode will verify
|
|
|
|
|
KeyCape JWT signatures and exact issuer/audience/scopes; local caller-supplied
|
|
|
|
|
identity never becomes authenticated evidence. SQLite remains the first
|
|
|
|
|
production store only as a single-replica StatefulSet with explicit migration,
|
|
|
|
|
backup, integrity, and restore gates. audit-core delivery can be implemented
|
|
|
|
|
against its existing authenticated idempotent ingest, while heartbeat findings,
|
|
|
|
|
count reconciliation, and sender registration remain external gates in
|
|
|
|
|
`AUDIT-WP-0009` T04/T06/T09. The first live PEP proof remains jointly gated on
|
|
|
|
|
secrets-engine T04/T02 and deployment of this service.
|
|
|
|
|
|
2026-09-01 23:45:48 +02:00
|
|
|
## Authenticate lifecycle mutations and approver evidence
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
id: APPROVAL-WP-0002-T01
|
2026-09-02 00:52:04 +02:00
|
|
|
status: progress
|
2026-09-01 23:45:48 +02:00
|
|
|
priority: high
|
2026-09-01 23:47:54 +02:00
|
|
|
state_hub_task_id: "dc4523f5-e0af-5734-a0ef-07aa7f2b2a27"
|
2026-09-01 23:45:48 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Bind create, approval-entry, revoke, supersede, and consume callers to
|
|
|
|
|
authenticated identities. An API-supplied `subject_id`, `actor`, or
|
|
|
|
|
`decision_id` is provenance only until independently authenticated. Keep
|
|
|
|
|
authorization decisions in access-engine and approval doctrine in gate-house.
|
|
|
|
|
|
2026-09-02 00:52:04 +02:00
|
|
|
Acceptance: production startup requires a signature-valid KeyCape JWT verifier;
|
|
|
|
|
every non-health API route requires an explicit scope; approval entry identity
|
|
|
|
|
and assurance come only from verified claims; create binds `binding.actor` to
|
|
|
|
|
the authenticated subject; consume is restricted to service/agent principals;
|
|
|
|
|
missing, expired, wrong-issuer, wrong-audience, wrong-scope, or unverifiable
|
|
|
|
|
tokens fail closed without mutation.
|
|
|
|
|
|
|
|
|
|
Repository implementation complete 2026-09-02: RS256/JWKS verification,
|
|
|
|
|
issuer/audience/time/profile validation, exact scopes, store-tenant isolation,
|
2026-09-02 15:46:06 +02:00
|
|
|
verified approver evidence, and a deny-all default are covered by tests.
|
|
|
|
|
|
|
|
|
|
2026-09-02 follow-up: fail-closed coverage now includes wrong signature, HS256,
|
|
|
|
|
empty/invalid profile claims, deny-all mutation refusal, human-principal consume
|
|
|
|
|
rejection, and production CLI refusal of static tokens / missing audit
|
|
|
|
|
delivery. Requested KeyCape registrations are in
|
|
|
|
|
`docs/keycape-service-registrations.md` (`aud` MUST be the resource server
|
|
|
|
|
`approval-engine`, not the OAuth client id). Remains `progress` until KeyCape
|
|
|
|
|
owns and proves those audience/client/scope registrations.
|
2026-09-02 00:52:04 +02:00
|
|
|
|
2026-09-06 20:35:59 +02:00
|
|
|
2026-09-06 follow-up: a tenant collision in the deployment inputs is now
|
|
|
|
|
recorded in `docs/keycape-service-registrations.md`. The manifest serves
|
|
|
|
|
`--tenant platform` while the requested client registrations issue
|
|
|
|
|
`tenant: tenant:coulomb`, and `ApiApplication.identity` compares the two with
|
|
|
|
|
exact string equality before any object lookup — so tokens issued under the
|
|
|
|
|
current registrations would be denied `403` on every non-health route.
|
|
|
|
|
flex-auth's `tenant:platform` CheckRequest subject is a PDP input this engine
|
|
|
|
|
never reads and cannot participate in the comparison. Denial evidence:
|
|
|
|
|
`tests/test_auth.py::test_wrong_tenant_is_forbidden`. The values are left as-is
|
|
|
|
|
deliberately: resolving it requires an owner statement on whether `platform` and
|
|
|
|
|
`tenant:coulomb` name the same layer, and guessing grants cross-tenant access to
|
|
|
|
|
the approval store. The registrations doc's stale issuer
|
|
|
|
|
(`https://auth.netkingdom.local`) is corrected to the live
|
|
|
|
|
`https://kc.coulomb.social` from `06544b0`. T01 stays `progress`.
|
|
|
|
|
|
Set the approval store tenant to exact tenant:platform
Operator decision 5ed3fb35-eca9-413a-82b9-95171ba85bf6 accepts tenant:platform
as the platform management, administration and services tenant, with no alias
to platform or tenant:coulomb and no implicit cross-tenant grant. This closes
the collision recorded in 5c87ba8, where the manifest served --tenant platform
while the requested registrations issued tenant:coulomb.
The store tenant is now exactly tenant:platform in the manifest, the CLI
default, and the Engine default, and the requested client registrations ask for
the same spelling. Exact JWT/store equality is retained: no mapping table, no
normalisation, no prefix handling.
Moving the defaults rather than only the manifest is deliberate. A default of
platform under a sanctioned value of tenant:platform is a trap, because a serve
that omits --tenant would come up healthy and then refuse every authenticated
call -- the exact failure this decision exists to prevent.
That default change broke ten tests whose identity fixtures hard-coded
platform. This is the hazard flex-auth reported as FLEX-DEC-2026-008: fixtures
that all carry one tenant prove nothing about the tenant field. Fixtures are
aligned to the exact spelling, and the field is now varied rather than merely
present. test_near_miss_tenant_spellings_are_forbidden refuses platform,
tenant:coulomb, case variants, whitespace variants and empty against a
tenant:platform store; test_exact_sanctioned_tenant_is_admitted pins the other
half so a reject-everything bug cannot pass it. 111 tests pass.
Also records the credential-independent half of the GLAS-WP-0015 image request:
the image builds non-root uid 10001 off the pinned base, carries schema v3 and
the new tenant default, migrates and verifies a fresh store to schema_version 3
with integrity ok, and refuses production without a persistent database or
authenticated audit delivery. No scan was run -- no scanner is installed here --
and no release digest exists, so T01 and T03 both stay open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PM5HnEAhokxdfcPqBNpT7D
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 715850@bnt-lap001
Assistant-Session: eb557e93-7cb1-45d0-9e57-7d15b3edc60e
2026-09-06 22:33:50 +02:00
|
|
|
2026-09-06 resolution: the operator accepted `tenant:platform` as the platform
|
|
|
|
|
management/administration/services tenant (landlord zone) — decision
|
|
|
|
|
`5ed3fb35-eca9-413a-82b9-95171ba85bf6`,
|
|
|
|
|
`glas-harness/docs/platform-tenant-decision.md`, relayed by `glas-harness`. The
|
|
|
|
|
collision above is closed by setting the store tenant to exactly
|
|
|
|
|
`tenant:platform`: `deploy/approval-engine.yaml` `--tenant`, the
|
|
|
|
|
`approval_engine/cli.py` `--tenant` default, and the `Engine(tenant=…)` default
|
|
|
|
|
all move off bare `platform`, and the requested client registrations now ask for
|
|
|
|
|
`tenant: tenant:platform`. Exact JWT/store equality is retained — no alias, no
|
|
|
|
|
normalisation, no prefix handling, and no implicit cross-tenant grant.
|
|
|
|
|
|
|
|
|
|
Moving the *defaults* rather than only the manifest is deliberate: a default of
|
|
|
|
|
`platform` under a sanctioned value of `tenant:platform` is a trap, because a
|
|
|
|
|
`serve` that omits `--tenant` would come up healthy and then refuse every
|
|
|
|
|
authenticated call.
|
|
|
|
|
|
|
|
|
|
The default change broke ten tests whose identity fixtures hard-coded
|
|
|
|
|
`platform`, which is the fixture-consistency hazard flex-auth reported as
|
|
|
|
|
`FLEX-DEC-2026-008` (29 fixtures all carrying one tenant proved nothing about
|
|
|
|
|
the field). Fixtures are aligned to the exact spelling, and the field is now
|
|
|
|
|
*varied* rather than merely present:
|
|
|
|
|
`tests/test_auth.py::test_near_miss_tenant_spellings_are_forbidden` refuses
|
|
|
|
|
`platform`, `tenant:coulomb`, case variants, whitespace variants and empty
|
|
|
|
|
against a `tenant:platform` store, and
|
|
|
|
|
`test_exact_sanctioned_tenant_is_admitted` pins the other half so a
|
|
|
|
|
reject-everything bug cannot pass. 111 tests pass.
|
|
|
|
|
|
|
|
|
|
This resolves the choice of value only. T01 stays `progress`: KeyCape still has
|
|
|
|
|
to own and prove these registrations, and credential materialization is
|
|
|
|
|
unchanged.
|
|
|
|
|
|
2026-09-08 14:47:40 +02:00
|
|
|
2026-09-08 follow-up: key-cape (`KEY-WP-0013-T02`) asked for the human approver
|
|
|
|
|
client's exact `client_id` and full callback URI. Neither is supplied, and
|
|
|
|
|
neither is guessed: this engine is a bearer-token resource server with no
|
|
|
|
|
redirect endpoint, no authorization-code/PKCE code and no Ingress or external
|
|
|
|
|
origin (`deploy/approval-engine.yaml` is Namespace + ClusterIP + StatefulSet),
|
|
|
|
|
so no callback URI exists here to register — and redirects match exactly at
|
|
|
|
|
`/authorize`, so an invented value either fails closed or registers an origin no
|
|
|
|
|
component owns. The human browser client belongs to an approver UI that this
|
|
|
|
|
repo does not contain and `APPROVAL-WP-0002` does not name; the two strings must
|
|
|
|
|
come from that component's owner. Confirmed to key-cape in the same reply: this
|
|
|
|
|
resource validates the **access** token (`Authorization: Bearer`, RS256 via the
|
|
|
|
|
issuer's `/jwks`, checking `iss`, `aud=approval-engine`, expiry, `sub`,
|
|
|
|
|
`principal_type`, `tenant`, `roles`, `scope`, `assurance`); `id_token` appears
|
|
|
|
|
nowhere in the codebase. Recorded in `docs/keycape-service-registrations.md`.
|
|
|
|
|
T01 stays `progress`.
|
|
|
|
|
|
2026-09-09 12:48:45 +02:00
|
|
|
2026-09-09 follow-up: the operator established `informed-decision`
|
|
|
|
|
(`INFD-WP-0001`) as the owner of the browser-facing approver surface, closing
|
|
|
|
|
the unassigned dependency recorded the previous day. Requirements issued to it
|
|
|
|
|
in `docs/approver-surface-requirements.md`: the two routes an approver surface
|
|
|
|
|
needs, the hard token claims, error/disposition mapping, and the standing
|
|
|
|
|
refusal to expose an inbox listing or a decision. Three items need an owner's
|
|
|
|
|
answer rather than an implementation: (A) the human client's specified
|
|
|
|
|
`allowedScopes` `[openid, approval:approve]` cannot `GET /v1/approvals/{id}`, so
|
|
|
|
|
the surface could submit an entry it was never able to render — we recommend
|
|
|
|
|
adding `approval:read` to the human client over reading through a BFF service
|
|
|
|
|
client, and this needs a key-cape decision; (B) `assurance` is persisted
|
|
|
|
|
verbatim and is the only place `mfaRequired: true` survives into the approval
|
|
|
|
|
record, so key-cape and informed-decision owe it a documented shape; (C)
|
|
|
|
|
`informed-decision`'s `view_hash` has nowhere to ride into an entry, whose body
|
|
|
|
|
is deliberately discarded — v1 correlates by `(approval_id, subject,
|
|
|
|
|
approved_at)` via audit-core, and binding the hash into the entry itself would
|
|
|
|
|
be a gate-house doctrine question before it is a change here. T01 stays
|
|
|
|
|
`progress`; none of these block this repository's half.
|
|
|
|
|
|
2026-09-09 14:10:31 +02:00
|
|
|
2026-09-09 (second): took T01 as far as this repository can reach. Writing the
|
|
|
|
|
approver-surface requirements exposed a gap in T01's own "approver evidence"
|
|
|
|
|
clause: an entry recorded `subject_id`, `assurance` and `evidence_ref` but
|
|
|
|
|
nothing about *what kind of principal* bound the approval, and `subject_id` is a
|
|
|
|
|
naming convention rather than a verified claim. `/entries` is not restricted by
|
|
|
|
|
principal type — only `/consume` is, to service/agent — and the
|
|
|
|
|
`approval-engine-operator` client holds `approval:approve`, so a service
|
|
|
|
|
principal can supply approver evidence today and was until now indistinguishable
|
|
|
|
|
from a human in the evidence chain. Whether a non-human may approve at all is
|
|
|
|
|
approval doctrine and belongs to `gate-house`; making it legible is ours.
|
|
|
|
|
Schema v4 adds `entries.principal_type`, populated only from the verified token,
|
|
|
|
|
surfaced on the object and on the audit evidence path but deliberately not on
|
|
|
|
|
the least-disclosure claim. Legacy entries stay `null` rather than being
|
|
|
|
|
back-filled into a `human` claim nobody made — the same reasoning as `pdp_path`
|
|
|
|
|
under `GH-DEC-2026-008`. Tests:
|
|
|
|
|
`test_entry_records_the_verified_principal_type`,
|
|
|
|
|
`test_non_human_approver_is_recorded_as_such`,
|
|
|
|
|
`test_v3_entries_migrate_to_v4_without_inventing_a_principal_type`, and the
|
|
|
|
|
v2 migration test now asserts the current version rather than a hard-coded 3.
|
|
|
|
|
124 tests pass. The existing `migrate` initContainer in
|
|
|
|
|
`deploy/approval-engine.yaml` covers the upgrade, but the manifest's pinned
|
|
|
|
|
image digest predates v4 — this change ships only with a re-pinned release
|
|
|
|
|
image. Also corrected two errors in the requirements issued earlier today: agent
|
|
|
|
|
tokens are *not* barred from `approval:approve`, and an empty `assurance` object
|
|
|
|
|
is accepted rather than refused. T01 remains `progress`: what is left is
|
|
|
|
|
key-cape owning the registrations, credential custody from railiance-platform,
|
|
|
|
|
and the open scope question (A) above — none of them reachable from here.
|
|
|
|
|
|
2026-09-09 23:17:43 +02:00
|
|
|
2026-09-09 (third): `railiance-platform` (`RPF-WP-0035-T06`) asked who presents
|
|
|
|
|
`approval-engine-operator` and refused to guess — correctly, since it carries
|
|
|
|
|
the widest scope set of the pair. The answer is that nobody does. Nothing in
|
|
|
|
|
this repository obtains an OAuth token: the engine only verifies them, its one
|
|
|
|
|
outbound credential is the mounted `audit-token` for audit-core delivery, and
|
|
|
|
|
`pep.py` is a consumer-side helper for PEPs that present their own. Scope by
|
|
|
|
|
scope the bundle does not describe a single actor — `approval:approve` now
|
|
|
|
|
belongs to `informed-decision`'s human client, `approval:emit` is redundant
|
|
|
|
|
because the server emits its own heartbeat with no token, and no requester
|
|
|
|
|
identity has ever been settled for `approval:create`. It was a convenience
|
|
|
|
|
bundle written when this repo assumed a lifecycle operator would exist.
|
|
|
|
|
Answered (b): cancel `CCR-2026-0020` as an owner decision rather than leave it
|
|
|
|
|
`in_flight`; verifier custody `CCR-2026-0018` is unaffected and stays correct.
|
|
|
|
|
Recorded in `docs/keycape-service-registrations.md`, with the rule that a future
|
|
|
|
|
presenter gets a registration narrowed to its own scopes and that
|
|
|
|
|
`approval:approve` must not travel with the operational scopes — schema v4 makes
|
|
|
|
|
a service bind legible, but legibility is not permission, and the should-it
|
|
|
|
|
question remains gate-house's. `secrets-engine-approval` is unaffected: it has a
|
|
|
|
|
real presenter and is the client that unblocks T05. T01 stays `progress`.
|
|
|
|
|
|
2026-09-10 07:56:28 +02:00
|
|
|
2026-09-10: three inbound items, all answered; one is a live blocker on the
|
|
|
|
|
human client and it is not ours to fix. (i) `key-cape` reports that a human
|
|
|
|
|
access token **cannot carry `tenant:platform` today**: `effectiveTenant(user)`
|
|
|
|
|
resolves the claim from the directory record, `domain.User.Tenant` is populated
|
|
|
|
|
by no adapter, and every human token falls back to `tenant:coulomb`; the
|
|
|
|
|
per-client `tenant` field is read only on the client-credentials path. Our exact
|
|
|
|
|
equality would refuse such a token, surfacing as a rejected approval rather than
|
|
|
|
|
a registration defect. We answered **(2) registration-bound**, matching decision
|
|
|
|
|
`5ed3fb35-eca9-413a-82b9-95171ba85bf6` and symmetric with the service clients,
|
|
|
|
|
and only because registrations are static, deployment-owned, and dynamic client
|
|
|
|
|
registration is excluded by design. (ii) `gate-house` `GH-DEC-2026-013` names
|
|
|
|
|
the consequence of exactly that: a registration-supplied tenant is the client's
|
|
|
|
|
say-so, not the directory's, and exact matching cannot see the difference — the
|
|
|
|
|
same shape as our own `pdp_digest` finding one layer down. They were right that
|
|
|
|
|
our wording implied a fact about the person. `docs/caller-authentication.md` now
|
|
|
|
|
states that the check is **store isolation** and not membership, that a
|
|
|
|
|
registration-supplied tenant is admissible for admission but **not** for any
|
|
|
|
|
future doctrine turning on the approver's own membership, and that provenance
|
|
|
|
|
will be recorded on the entry the way v4 records `principal_type` once the claim
|
|
|
|
|
carries it. (iii) `informed-decision` R3, answered in `docs/approval-claim.md`:
|
|
|
|
|
`view_hash` and `binding.digest` are **not** the same hash and must not be
|
|
|
|
|
merged. Three hashes, three questions — which act (five fields), which decision
|
|
|
|
|
request (`pdp_digest`), what a person was shown (`view_hash`). Recommended that
|
|
|
|
|
their binding document *carry* our digest as a field rather than re-canonicalize
|
|
|
|
|
the same five fields, so the estate has one canonicalization of the act with a
|
|
|
|
|
detectable mismatch instead of two independently correct answers. Also accepted
|
|
|
|
|
from key-cape: `approval:read` added to the human client, and a documented
|
|
|
|
|
`assurance` shape (`level`/`methods`/`mfa`/`source`/`at`) — with `at` fixed to
|
|
|
|
|
carry the original authentication instant through session reuse, which matters
|
|
|
|
|
here because this engine stores that object verbatim as the only downstream
|
|
|
|
|
record that MFA happened. T01 stays `progress`; the human client cannot be
|
|
|
|
|
registered until the tenant source is settled by its owner.
|
|
|
|
|
|
2026-09-10 18:39:30 +02:00
|
|
|
2026-09-10: GH-DEC-2026-016 supplies the previously missing NC-03 doctrine.
|
|
|
|
|
Approvals explicitly declared as discharging human-in-the-loop or dual-control
|
|
|
|
|
requirements must refuse non-human binds at issue. T01 owns the declaration,
|
|
|
|
|
engine/API enforcement, predecessor inheritance and negative legacy migration,
|
|
|
|
|
verified human identity provenance, tests and subsequent deployed proof. This
|
|
|
|
|
return changes neither existing rows nor production admission. The new rule is
|
|
|
|
|
not satisfied merely by the already-recorded principal_type field. The separate
|
|
|
|
|
R3 digest-exclusion condition is completed in T06 below.
|
|
|
|
|
|
2026-09-10 19:26:12 +02:00
|
|
|
### Declared human-control implementation — 2026-09-10
|
|
|
|
|
|
|
|
|
|
GH-DEC-2026-016 source implementation now records strict `human_control` intent
|
|
|
|
|
at request creation and refuses non-human binding at `add_entry` / `/entries`.
|
|
|
|
|
The creator may still be a service drafting a request; approval judgment comes
|
|
|
|
|
from the verified entry identity. This maps the ruling's bind/issue boundary to
|
|
|
|
|
this engine's two-step API and does not confer approval on an unapproved draft.
|
|
|
|
|
Schema v5 defaults legacy rows to undeclared, preserves act digests and recorded
|
|
|
|
|
entries, and inherits the declaration on supersession. An existing successor
|
|
|
|
|
with a mismatched declaration conflicts atomically. Spoofed request-body identity
|
|
|
|
|
cannot change the verified JWT type; invalid persisted human evidence also fails
|
|
|
|
|
claim/consume. The outbox retains the declaration with its existing atomic write.
|
|
|
|
|
|
|
|
|
|
All 22 new cases fail on the previous implementation and pass after correction;
|
|
|
|
|
the full suite passes 152 tests. Persistent v4 migration, RS256/JWKS HTTP calls,
|
|
|
|
|
ordinary service compatibility, human quorum, rollback and successor cases are
|
|
|
|
|
included. Actual Secrets Engine claim parsing accepts both new flag values;
|
|
|
|
|
this is wire compatibility, not adoption of the new requirement. KeyCape's
|
|
|
|
|
current source fixes human type on its PKCE/user path and service type on its
|
|
|
|
|
client-credentials path; native issuer/human proof is still outstanding.
|
|
|
|
|
|
|
|
|
|
T01 remains progress for exact requester/human registration and native proof.
|
|
|
|
|
T03 owns the matching schema-v5 image and admitted deployment. T05 with
|
|
|
|
|
SECRETS-WP-0009-T03 and INFD-WP-0001-T08 must explicitly declare and require
|
|
|
|
|
human_control for the factory human-approval workflow; old generic approvals
|
|
|
|
|
must not be treated as satisfying it. Evidence:
|
|
|
|
|
`docs/evidence/2026-09-10-human-control.json`. No production approval or database
|
|
|
|
|
migration occurred during these tests.
|
|
|
|
|
|
2026-09-01 23:45:48 +02:00
|
|
|
## Harden durable storage and migrations
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
id: APPROVAL-WP-0002-T02
|
2026-09-02 00:52:04 +02:00
|
|
|
status: done
|
2026-09-01 23:45:48 +02:00
|
|
|
priority: high
|
2026-09-01 23:47:54 +02:00
|
|
|
state_hub_task_id: "2bef94ca-9482-5eff-9bd9-39adef292a78"
|
2026-09-01 23:45:48 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Define the production persistence, backup/restore, migration, concurrency, and
|
|
|
|
|
recovery posture. Prove schema upgrades preserve existing approvals and that
|
|
|
|
|
crash recovery cannot separate mutations from outbox evidence.
|
|
|
|
|
|
2026-09-02 00:52:04 +02:00
|
|
|
Acceptance: schema version is explicit; production serve refuses an unmigrated
|
|
|
|
|
or in-memory store; migration is a separate repeatable command; backup uses
|
|
|
|
|
SQLite's online backup API and integrity verification; restore is documented as
|
|
|
|
|
a stopped-single-writer operation; tests cover legacy upgrade, backup/restore,
|
|
|
|
|
and outbox atomicity after restart.
|
|
|
|
|
|
|
|
|
|
Completed 2026-09-02: schema v2 migration, production no-auto-migrate gate,
|
|
|
|
|
integrity/status surface, online mode-0600 backup, stopped-writer restore runbook,
|
|
|
|
|
retry-attempt state, and migration/backup/atomicity tests are in place.
|
|
|
|
|
|
2026-09-01 23:45:48 +02:00
|
|
|
## Package and deploy the service
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
id: APPROVAL-WP-0002-T03
|
2026-09-02 00:52:04 +02:00
|
|
|
status: wait
|
2026-09-01 23:45:48 +02:00
|
|
|
priority: high
|
2026-09-01 23:47:54 +02:00
|
|
|
state_hub_task_id: "f0aa2e6d-19e6-5b43-886c-efa4e3de5f22"
|
2026-09-01 23:45:48 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Add the governed image/deployment surface, health and readiness behavior,
|
|
|
|
|
resource bounds, and fail-closed caller configuration. A local WSGI development
|
|
|
|
|
server is not production evidence.
|
|
|
|
|
|
2026-09-02 00:52:04 +02:00
|
|
|
Acceptance: a digest-pin-ready image and single-writer StatefulSet manifest
|
|
|
|
|
exist with non-root/read-only-root controls, PVC, migration init container,
|
|
|
|
|
resource bounds, probes, and default-deny network policy. Live completion also
|
|
|
|
|
requires an immutable image digest, KeyCape registrations, audit sender
|
|
|
|
|
credential, successful rollout, and restart/restore evidence.
|
|
|
|
|
|
|
|
|
|
Repository implementation complete 2026-09-02: the digest-pin-ready non-root
|
|
|
|
|
image builds and runs; the single-writer StatefulSet, PVC, migration init,
|
|
|
|
|
read-only root, resources, probes, and default-deny policies pass client dry-run.
|
|
|
|
|
Waiting on release digest, KeyCape/audit registrations and credentials, rollout,
|
|
|
|
|
restart, and restore evidence.
|
|
|
|
|
|
Set the approval store tenant to exact tenant:platform
Operator decision 5ed3fb35-eca9-413a-82b9-95171ba85bf6 accepts tenant:platform
as the platform management, administration and services tenant, with no alias
to platform or tenant:coulomb and no implicit cross-tenant grant. This closes
the collision recorded in 5c87ba8, where the manifest served --tenant platform
while the requested registrations issued tenant:coulomb.
The store tenant is now exactly tenant:platform in the manifest, the CLI
default, and the Engine default, and the requested client registrations ask for
the same spelling. Exact JWT/store equality is retained: no mapping table, no
normalisation, no prefix handling.
Moving the defaults rather than only the manifest is deliberate. A default of
platform under a sanctioned value of tenant:platform is a trap, because a serve
that omits --tenant would come up healthy and then refuse every authenticated
call -- the exact failure this decision exists to prevent.
That default change broke ten tests whose identity fixtures hard-coded
platform. This is the hazard flex-auth reported as FLEX-DEC-2026-008: fixtures
that all carry one tenant prove nothing about the tenant field. Fixtures are
aligned to the exact spelling, and the field is now varied rather than merely
present. test_near_miss_tenant_spellings_are_forbidden refuses platform,
tenant:coulomb, case variants, whitespace variants and empty against a
tenant:platform store; test_exact_sanctioned_tenant_is_admitted pins the other
half so a reject-everything bug cannot pass it. 111 tests pass.
Also records the credential-independent half of the GLAS-WP-0015 image request:
the image builds non-root uid 10001 off the pinned base, carries schema v3 and
the new tenant default, migrates and verifies a fresh store to schema_version 3
with integrity ok, and refuses production without a persistent database or
authenticated audit delivery. No scan was run -- no scanner is installed here --
and no release digest exists, so T01 and T03 both stay open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PM5HnEAhokxdfcPqBNpT7D
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 715850@bnt-lap001
Assistant-Session: eb557e93-7cb1-45d0-9e57-7d15b3edc60e
2026-09-06 22:33:50 +02:00
|
|
|
2026-09-06 image preparation (GLAS-WP-0015 request; credential-independent half):
|
|
|
|
|
built and validated locally against the agreed tenant and current schema.
|
|
|
|
|
|
|
|
|
|
- Build: `make image-build` off the digest-pinned base
|
|
|
|
|
`python:3.12-slim@sha256:d764629c…`. Local manifest-list digest
|
|
|
|
|
`sha256:85e46ddf47b3ac0cdab620163a7b034027451dff286b63ecff6a2493d7a57ecc`.
|
|
|
|
|
**This is a local build digest, not a release digest** — the manifest still
|
|
|
|
|
carries `REPLACE_WITH_RELEASE_DIGEST` because pinning requires a push to
|
|
|
|
|
`forgejo.coulomb.social`, which needs registry credentials this session does
|
|
|
|
|
not hold.
|
|
|
|
|
- Runtime identity: `uid=10001(approval) gid=10001(approval)`, non-root as
|
|
|
|
|
required.
|
|
|
|
|
- Schema: image carries `LATEST_SCHEMA_VERSION = 3`, matching the migrated
|
|
|
|
|
store, and `Engine` tenant default `tenant:platform`.
|
|
|
|
|
- First-install migration (no prior DB): `migrate` then `verify` on a fresh
|
|
|
|
|
volume both report `schema_version: 3`, `schema_current: true`,
|
|
|
|
|
`integrity: ["ok"]`, `foreign_key_violations: 0`, `persistent: true`.
|
|
|
|
|
- Fail-closed configuration proven in the image, not only in tests:
|
|
|
|
|
`serve --production --db :memory:` refuses with "production requires a
|
|
|
|
|
persistent database"; `serve --production` on a real DB without audit
|
|
|
|
|
configuration refuses with "production requires authenticated audit
|
|
|
|
|
delivery".
|
|
|
|
|
- Manifest inputs: `kubectl apply --dry-run=client` passes for the namespace,
|
|
|
|
|
service, and StatefulSet with the new `--tenant tenant:platform`.
|
|
|
|
|
|
|
|
|
|
Scan gate NOT met: no scanner (`trivy`, `grype`, `docker scout`) is installed on
|
|
|
|
|
this workstation, so no vulnerability scan was run and none is claimed. The
|
|
|
|
|
inventory a scanner needs is the pinned base above plus
|
|
|
|
|
`cryptography==50.0.1`, `PyJWT==2.13.0`, `waitress==3.0.2`, `cffi==2.1.1`,
|
|
|
|
|
`pycparser==3.0`, `pip==25.0.1`.
|
|
|
|
|
|
|
|
|
|
T03 stays `wait`: still no release digest, no KeyCape/audit credentials, no
|
|
|
|
|
rollout, and no restart/restore evidence. Nothing here is a deploy.
|
|
|
|
|
|
Run the scan gate; harden the image and stop short of release
glas-harness asked for a scanned immutable image. The scan ran, and it failed
on the base we had sanctioned, so no image was pushed.
Every finding is inherited from the base image or its distro packages; none is
in approval_engine code. Two fixes applied here:
The base pin was stale. It named a Debian 13.5 build of python:3.12-slim while
upstream is 13.6. Bumping the digest removes 30 HIGH and 47 MEDIUM on its own.
Still a digest, not a floating tag.
pip is gone from the runtime image. All 10 MEDIUM Python findings were in pip
itself, a build-time tool with no business in a running approval service. The
build is now two-stage, and pip is removed from both the venv and the base's
/usr/local, so command -v pip returns nothing.
What remains is a decision rather than a task. Three CRITICALs persist on
Debian, all perl-base (CVE-2026-13221, CVE-2026-42496, CVE-2026-8376), none
with an upstream fix, in a package this service never invokes and that Debian
marks Essential. An Alpine variant carries no perl and scans 0 CRITICAL / 7
HIGH / 1 MEDIUM against Debian's 3 / 51 / 56.
Alpine is proven viable rather than asserted: musl wheels resolve with no
toolchain, the full suite passes on musl at 111 tests, and non-root uid 10001,
schema v3, tenant:platform, fresh-store migrate/verify and both production
fail-closed gates all hold in the built image. It is parked in
Containerfile.alpine as a candidate; Containerfile remains sanctioned.
Nothing was released. Pushing the Debian variant would pin three unfixable
CRITICALs into a release digest, and choosing the runtime C library for this
service is not a call to make silently. The manifest still carries
REPLACE_WITH_RELEASE_DIGEST. Full record in docs/image-scan-2026-09-06.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PM5HnEAhokxdfcPqBNpT7D
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 715850@bnt-lap001
Assistant-Session: eb557e93-7cb1-45d0-9e57-7d15b3edc60e
2026-09-06 22:52:20 +02:00
|
|
|
2026-09-06 scan gate — run, and it failed on the sanctioned base. Full record in
|
|
|
|
|
`docs/image-scan-2026-09-06.md`. Scanned with trivy (no scanner was installed;
|
|
|
|
|
this one needed none). Findings are all inherited from the base image, none in
|
|
|
|
|
`approval_engine` code.
|
|
|
|
|
|
|
|
|
|
Two fixes applied. The base pin was stale at Debian 13.5 while upstream is
|
|
|
|
|
13.6 — bumping the digest removed 30 HIGH and 47 MEDIUM. And pip, which held all
|
|
|
|
|
10 MEDIUM Python findings, is now absent from the runtime image via a two-stage
|
|
|
|
|
build; it is a build-time tool and had no business in a running approval
|
|
|
|
|
service.
|
|
|
|
|
|
|
|
|
|
What remains is a decision, not a task. Three CRITICALs persist on Debian, all
|
|
|
|
|
`perl-base` (`CVE-2026-13221`, `CVE-2026-42496`, `CVE-2026-8376`), **none with
|
|
|
|
|
an upstream fix**, in a package this service never invokes and which Debian
|
|
|
|
|
marks `Essential: yes`. An Alpine variant carries no perl and scans 0 CRITICAL /
|
|
|
|
|
7 HIGH / 1 MEDIUM against Debian's 3 / 51 / 56. It is proven viable — musl
|
|
|
|
|
wheels resolve without a toolchain, the full suite passes on musl at 111, and
|
|
|
|
|
non-root identity, schema v3, `tenant:platform`, fresh-store migrate/verify and
|
|
|
|
|
both production fail-closed gates all hold in the image. It is parked in
|
|
|
|
|
`Containerfile.alpine` as a candidate; `Containerfile` remains the sanctioned
|
|
|
|
|
base.
|
|
|
|
|
|
|
|
|
|
Nothing was pushed. Pushing the Debian variant would pin three unfixable
|
|
|
|
|
CRITICALs into a release digest, and choosing the runtime C library for this
|
|
|
|
|
service is not a call to make silently. `deploy/approval-engine.yaml` still
|
|
|
|
|
carries `REPLACE_WITH_RELEASE_DIGEST`. Awaiting an owner answer on the base
|
|
|
|
|
(Alpine, a documented perl exception on glibc, or distroless — unevaluated) and
|
|
|
|
|
on which scanner is sanctioned.
|
|
|
|
|
|
Adopt Alpine as the sanctioned base; release candidate scans clean
Operator adopted Alpine/musl as the base and trivy as the scanner.
Containerfile.alpine is promoted to Containerfile and the Debian slim variant
is retired rather than kept as an option -- it shipped three perl-base
CRITICALs with no upstream fix, in a package this service never invokes.
Promoting Alpine left 7 HIGH and 1 MEDIUM, all libuuid 2.42.1-r0 as shipped by
the pinned Alpine 3.24.1, and all with fixes in 2.42.3. The runtime stage now
requires libuuid>=2.42.3-r1. That is a version floor, not a floating upgrade:
the base stays digest-pinned and reproducible, and a vulnerable libuuid fails
the build instead of shipping.
The candidate scans 0 CRITICAL / 0 HIGH / 0 MEDIUM / 0 LOW. Verified on that
exact artifact: non-root uid 10001, pip absent, schema v3, tenant default
tenant:platform, fresh-store migrate and verify clean, restart persistence via
re-verify on the same volume, both production fail-closed refusals, 111 tests
on musl, kubectl dry-run passing.
The gate is now reproducible instead of a one-off. make image-scan fails on any
CRITICAL or HIGH, and make image-release runs build then scan then push, so a
failing scan blocks the push by construction rather than by whoever remembers
to look.
Not released. docker push was attempted and refused by this session's sandbox
as an outward-facing publish, and was not worked around. No release digest
exists, so the manifest deliberately keeps REPLACE_WITH_RELEASE_DIGEST -- it
must be pinned to the registry manifest digest, never the tag and never the
local image id. T03 stays wait on that push plus the still-unmaterialized
KeyCape registrations and audit sender credential.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PM5HnEAhokxdfcPqBNpT7D
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 715850@bnt-lap001
Assistant-Session: eb557e93-7cb1-45d0-9e57-7d15b3edc60e
2026-09-06 23:01:41 +02:00
|
|
|
2026-09-06 both answered; release candidate scans clean. The operator adopted
|
|
|
|
|
Alpine/musl as the sanctioned base and trivy as the sanctioned scanner.
|
|
|
|
|
`Containerfile.alpine` is promoted to `Containerfile` and the Debian variant is
|
|
|
|
|
retired rather than kept as an option.
|
|
|
|
|
|
|
|
|
|
Promoting Alpine left 7 HIGH and 1 MEDIUM, all `libuuid` 2.42.1-r0 and all with
|
|
|
|
|
fixes in 2.42.3. The runtime stage now requires `libuuid>=2.42.3-r1` — a version
|
|
|
|
|
floor rather than a floating upgrade, so the base stays digest-pinned and a
|
|
|
|
|
vulnerable libuuid fails the build instead of shipping. **The candidate now
|
|
|
|
|
scans 0 CRITICAL / 0 HIGH / 0 MEDIUM / 0 LOW.**
|
|
|
|
|
|
|
|
|
|
Verified on that exact artifact: non-root `uid=10001`, pip absent, schema v3,
|
|
|
|
|
tenant default `tenant:platform`, fresh-store `migrate`+`verify` clean, restart
|
|
|
|
|
persistence via re-`verify` on the same volume, both production fail-closed
|
|
|
|
|
refusals, 111 tests on musl, and `kubectl --dry-run` passing.
|
|
|
|
|
|
|
|
|
|
The gate is now reproducible rather than a one-off: `make image-scan` fails on
|
|
|
|
|
any CRITICAL or HIGH, and `make image-release` runs build then scan then push so
|
|
|
|
|
a failing scan blocks the push by construction.
|
|
|
|
|
|
|
|
|
|
Still `wait`, and now for exactly one reason: `docker push` was attempted and
|
|
|
|
|
refused by this session's sandbox as an outward-facing publish. It was not
|
|
|
|
|
worked around. No release digest exists, so the manifest keeps
|
|
|
|
|
`REPLACE_WITH_RELEASE_DIGEST`; an operator must run `make image-release` (or the
|
|
|
|
|
push and `docker inspect`) and pin the returned registry manifest digest — never
|
|
|
|
|
the tag, never the local image id
|
|
|
|
|
`sha256:73333f5ceb55e48192e3095cb2e2a741cdc6ff0be2f18128301072b4a6b6eb9d`.
|
|
|
|
|
Rollout, restart and restore evidence remain gated on the KeyCape registrations
|
|
|
|
|
and audit sender credential.
|
|
|
|
|
|
Reconcile the release record; guard the image pin with tests
The publication at b51d174 pinned the manifest and appended a note, but left
three documents asserting the opposite. docs/image-scan-2026-09-06.md said "It
has not been pushed" in its Outcome block at the top while recording the
successful push at the bottom; its "Release status" section still said no
release digest existed and the manifest carried REPLACE_WITH_RELEASE_DIGEST;
and deploy/README.md still instructed an operator to replace a placeholder that
is now a real digest -- an instruction to undo the pin.
This is the derived-artifact staleness this repo argued for a rule about in
v0.8 §12 and then produced in its own release evidence, in the file another
repository would read to confirm what shipped. Superseded sections are now
marked in place with what replaced them rather than deleted, since they record
why the base changed and why the release was held. The scan doc carries a
status marker naming which parts state current state, including that a clean
scan is point-in-time and ages as advisories are published.
The workplan gains the publication entry it never got, and a re-check of T03's
full acceptance: a published image reads like a finished task, but the digest
was one of five requirements. Nothing is deployed -- the namespace is empty and
no secrets exist -- and production serve refuses to start without authenticated
audit delivery, so a rollout today would fail closed and prove nothing. T03
stays wait on T01's registrations and the audit credential.
Also corrects an earlier caution in the workplan that the digest was "a local
image id, not a release digest". That was wrong: with the containerd/OCI store
the build id is the manifest digest, and the registry resolves it.
tests/test_deploy_manifest.py holds the pin mechanically instead of by review:
both image references exist, are digest-pinned rather than tagged, are
identical, and no placeholder remains. The identity check matters because the
init container migrates the database the server then serves -- two different
digests there is a split-brain migration. Verified the guard fails on the drift
it targets, not only that it passes today. 116 tests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PM5HnEAhokxdfcPqBNpT7D
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 715850@bnt-lap001
Assistant-Session: eb557e93-7cb1-45d0-9e57-7d15b3edc60e
2026-09-07 09:04:34 +02:00
|
|
|
2026-09-06 published (`glas-harness`, standing production authorization). The
|
|
|
|
|
push above succeeded outside this repo's session. Registry tag `0.1.0` resolves
|
|
|
|
|
to OCI index digest
|
|
|
|
|
`sha256:73333f5ceb55e48192e3095cb2e2a741cdc6ff0be2f18128301072b4a6b6eb9d`, with
|
|
|
|
|
linux/amd64 manifest `sha256:2064d5378e01801ecf9966ccec9c93aadd16d6b404ef4890983d89fa738c164c`.
|
|
|
|
|
Both `image:` references in `deploy/approval-engine.yaml` pin the index digest
|
|
|
|
|
at `b51d174`.
|
|
|
|
|
|
|
|
|
|
The index digest equals the local image id recorded above, which is a property
|
|
|
|
|
of the containerd/OCI image store rather than evidence of anything — the
|
|
|
|
|
publication evidence is the successful push and the remote index lookup, and
|
|
|
|
|
this repo verified the digest resolves against the registry independently. An
|
|
|
|
|
earlier caution in this workplan that the value was "a local image id, not a
|
|
|
|
|
release digest" was wrong on that point; the digest is correct and the pin is
|
|
|
|
|
sound.
|
|
|
|
|
|
|
|
|
|
2026-09-07 acceptance re-checked against the whole list, because a published
|
|
|
|
|
image reads like a finished task. T03's acceptance requires an immutable digest
|
|
|
|
|
**and** KeyCape registrations, an audit sender credential, a successful rollout,
|
|
|
|
|
and restart/restore evidence. Only the first is now met. `kubectl -n
|
|
|
|
|
approval-engine get all` returns nothing and no secrets exist, so nothing is
|
|
|
|
|
deployed; production `serve` refuses to start without authenticated audit
|
|
|
|
|
delivery, so a rollout attempted today would fail closed by design and would
|
|
|
|
|
prove nothing. **T03 stays `wait`** — on T01's registrations and the audit
|
|
|
|
|
credential, not on anything this repo owns.
|
|
|
|
|
|
2026-09-09 14:38:21 +02:00
|
|
|
2026-09-09: schema v4 (`entries.principal_type`, T01) gave this task a new
|
|
|
|
|
drift surface, now closed in the only way that survives review. The manifest's
|
|
|
|
|
pinned pair is self-consistent — the published artifact migrates to 3 and serves
|
|
|
|
|
3 — so nothing fails, and that is exactly the dangerous shape: the deployment
|
|
|
|
|
reads healthy while the assumption "the deployment records approver principal
|
|
|
|
|
type" is false. `deploy/README.md` now states that the pin predates schema v4
|
|
|
|
|
and that carrying the new evidence requires cutting a new image at gate 3, and
|
|
|
|
|
`tests/test_deploy_manifest.py::test_pinned_image_schema_drift_is_acknowledged`
|
|
|
|
|
holds the statement: while the manifest pins the artifact
|
|
|
|
|
`docs/image-scan-2026-09-06.md` describes, that record's schema version must
|
|
|
|
|
match this repository or the runbook must say plainly that it does not. Verified
|
|
|
|
|
to fail when the acknowledgement is removed. `docs/storage-operations.md` gains
|
|
|
|
|
the v4 section (additive `ALTER TABLE`, legacy entries stay `NULL` and are never
|
|
|
|
|
back-filled to `human`, downgrade unsupported). T03 stays `wait`: gates 1 and 2
|
|
|
|
|
are unchanged and nothing is deployed. 125 tests pass.
|
|
|
|
|
|
2026-09-10 19:35:05 +02:00
|
|
|
### Schema-v5 release candidate — 2026-09-10
|
|
|
|
|
|
|
|
|
|
The earlier schema drift is resolved in the published candidate, source
|
|
|
|
|
`be1a388a848cf59e9ff3a5420f88a70ca0a52e7e`, registry tag
|
|
|
|
|
`0.1.0-hfact-be1a388`, immutable digest
|
|
|
|
|
`sha256:251941a5cb2724b57cc32cff6b693b1ab0be695bee4f56f02d51961189c0fa49`.
|
|
|
|
|
Both migration and serving references now pin it. The sanctioned HIGH/CRITICAL
|
|
|
|
|
scan passes with zero findings. Disposable container checks prove schema v5,
|
|
|
|
|
v4 migration without inferred human declaration, nonhuman bind refusal,
|
|
|
|
|
human bind/claim/consume and restart persistence; packaged store bytes match
|
|
|
|
|
committed source. These checks use synthetic local identities and no network.
|
|
|
|
|
See `docs/evidence/2026-09-10-human-control-image.json` for the exact evidence.
|
|
|
|
|
|
|
|
|
|
T03 remains `wait`: native KeyCape requester/approver registration and audit
|
|
|
|
|
sender custody still precede rollout, live persistence/outbox/restore proof.
|
|
|
|
|
The factory requester and PEP must explicitly declare/require human_control in
|
|
|
|
|
T05 with SECRETS-WP-0009-T03 and INFD-WP-0001-T08. No production database was
|
|
|
|
|
migrated, no workload deployed and no factory/model request admitted.
|
|
|
|
|
|
2026-09-01 23:45:48 +02:00
|
|
|
## Wire outbox delivery and reconciliation
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
id: APPROVAL-WP-0002-T04
|
2026-09-02 00:52:04 +02:00
|
|
|
status: wait
|
2026-09-01 23:45:48 +02:00
|
|
|
priority: high
|
2026-09-01 23:47:54 +02:00
|
|
|
state_hub_task_id: "777a5cf5-c1db-5e07-8de6-ab109db5ccdc"
|
2026-09-01 23:45:48 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Deliver the local outbox asynchronously to audit-core, preserve event-id
|
|
|
|
|
deduplication, publish lag/depth signals, emit the declared heartbeat, and prove
|
|
|
|
|
the Gate House reconciliation contract against accepted event counts.
|
|
|
|
|
|
2026-09-02 00:52:04 +02:00
|
|
|
Acceptance: the sender adapts the local event to audit-core's authenticated
|
|
|
|
|
HTTP ingest, reuses event id as idempotency key, rereads a mounted token file,
|
|
|
|
|
marks drained only on accepted/duplicate, retains retryable failures, exposes
|
|
|
|
|
attempt/lag state, and emits the declared heartbeat. Live reconciliation waits
|
|
|
|
|
on `AUDIT-WP-0009-T04/T06/T09`; do not invent that receiver surface here.
|
|
|
|
|
|
|
|
|
|
Repository implementation complete 2026-09-02: audit-core envelope adaptation,
|
|
|
|
|
event-id idempotency, mounted-token reread, accepted/duplicate handling,
|
2026-09-02 15:46:06 +02:00
|
|
|
retry-attempt/lag metrics, and periodic heartbeats are tested.
|
|
|
|
|
|
|
|
|
|
2026-09-02 follow-up: drain tests now cover HTTP 200 duplicate as drained and
|
|
|
|
|
urllib `HTTPError` 503 as pending. Waiting on the audit-core sender
|
|
|
|
|
registration/ingress and receiver-owned reconciliation work
|
|
|
|
|
(`AUDIT-WP-0009-T04/T06/T09`).
|
2026-09-02 00:52:04 +02:00
|
|
|
|
2026-09-08 17:01:42 +02:00
|
|
|
2026-09-08 critical-path return: `docs/audit-source-registration.md` resolves
|
|
|
|
|
Audit Core's tenant and redaction questions from the existing sanctioned store
|
|
|
|
|
contract: exact `tenant:platform`, source `approval-engine`, write-only,
|
|
|
|
|
load-bearing, explicit `redact`. The outbox example and envelope test now pin
|
|
|
|
|
the same tenant. This closes those source-input waits, not live registration.
|
|
|
|
|
Platform still owes linked receiver/sender custody and the protected token;
|
|
|
|
|
T04 stays wait for admission and live drain/reconciliation evidence.
|
|
|
|
|
|
2026-09-10 20:36:21 +02:00
|
|
|
2026-09-10: audit-core landed the detection half (`AUDIT-WP-0009` T04/T06/T07
|
|
|
|
|
at `b098fb1`, contract in their `docs/stream-completeness.md`), so the sender
|
|
|
|
|
side is no longer describing a cadence that does not operate. Implemented the
|
|
|
|
|
per-class heartbeat contract: `HEARTBEAT_CLASSES` covers `issuance`, `use`,
|
|
|
|
|
`supersession` and `revocation`, each asserting `nothing-to-report` for itself,
|
|
|
|
|
all four written in one transaction. The previous single per-source beat was
|
|
|
|
|
exactly the shape audit-core rules inadequate — it is discharged by whichever
|
|
|
|
|
class is busy, so a silent `revocation` stream hides behind `issuance`, and
|
|
|
|
|
`revocation` is the only silence here that matters. Heartbeats now carry
|
|
|
|
|
`type: audit-core.heartbeat` with `class` and `assertion` on `data` rather than
|
|
|
|
|
inside our details object. The first beat is emitted at startup, not one
|
|
|
|
|
interval later, because a declared-but-never-sent class is their
|
|
|
|
|
`no_heartbeat_since_registration` finding rather than a skip — `heartbeat_due`
|
|
|
|
|
already returned true on an empty outbox and that is now pinned by test.
|
|
|
|
|
`docs/audit-source-registration.md` declares `heartbeat_classes` at 172800s for
|
|
|
|
|
all four (twice the deployed 86400s default, so one missed cycle is not a
|
|
|
|
|
finding and two are), records the reconciliation surface and both bounds, and
|
|
|
|
|
restates the residual audit-core stated on every response: counts and
|
|
|
|
|
heartbeats cover loss, outage, drain failure and accident, and neither covers
|
|
|
|
|
this engine lying about itself, since a compromised emitter suppresses the
|
|
|
|
|
event and its own heartbeat together. Tests: per-class emission including
|
|
|
|
|
`revocation`, atomicity under outbox failure, the envelope placement, and that
|
|
|
|
|
an ordinary event gains no heartbeat fields. 155 pass. T04 stays `wait`: live
|
|
|
|
|
drain still needs the sender registration and credential admission, and the
|
|
|
|
|
`heartbeat_classes` values above are a request until audit-core accepts them.
|
|
|
|
|
|
2026-09-01 23:45:48 +02:00
|
|
|
## Prove one live PEP consumption path
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
id: APPROVAL-WP-0002-T05
|
2026-09-02 00:52:04 +02:00
|
|
|
status: wait
|
2026-09-01 23:45:48 +02:00
|
|
|
priority: high
|
2026-09-01 23:47:54 +02:00
|
|
|
state_hub_task_id: "3196fb77-3df0-5358-ae12-2d07bac12041"
|
2026-09-01 23:45:48 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Integrate one protected-system consumer under `GH-DEC-2026-003`: claim before
|
|
|
|
|
decision, CAS consume after ALLOW and before side effect, same-digest retry,
|
|
|
|
|
different-digest conflict, spent-on-failure behavior, and no protected action
|
|
|
|
|
when approval-engine is unavailable.
|
2026-09-02 00:52:04 +02:00
|
|
|
|
|
|
|
|
Acceptance: a repeatable harness proves the PEP sequence against the real HTTP
|
|
|
|
|
surface without performing a protected action; live closure requires a
|
|
|
|
|
secrets-engine-owned handler and evidence that no OpenBao call occurs on every
|
|
|
|
|
failure case. This repo may supply the protocol client and fixture, but may not
|
|
|
|
|
claim the consumer's side effect.
|
|
|
|
|
|
|
|
|
|
Repository implementation complete 2026-09-02: the HTTP PEP client and
|
|
|
|
|
fail-closed sequencing harness prove claim-before-decision and CAS-consume-before
|
|
|
|
|
callback, including unavailable, DENY, digest mismatch, and conflict paths.
|
2026-09-02 15:46:06 +02:00
|
|
|
|
|
|
|
|
2026-09-02 follow-up: secrets-engine shipped the PEP consume-before-OpenBao
|
|
|
|
|
handler (`src/secrets_engine/approval_consume.py`; inbox `0e04b4e2`). This
|
|
|
|
|
engine's client now maps HTTP 409/401/403/404/503 and unreachability, requires
|
|
|
|
|
the canonical digest, and refuses a decision-shaped consume payload. The
|
|
|
|
|
repeatable harness in `tests/test_pep.py` drives the real HTTP surface: first
|
|
|
|
|
consume then same-digest retry, different-digest conflict, spent-on-failure
|
|
|
|
|
claim refusal, and unreachable-engine callback suppression. Waiting on live
|
|
|
|
|
closure: this service deployed (T03) and a durable consume binding served
|
|
|
|
|
(`SECRETS-WP-0007-T04` / `SECRETS-WP-0008-T02`). This repo does not claim the
|
|
|
|
|
OpenBao side effect.
|
Record the claim vs ActionAuthorization envelope divergence
secrets-engine reports its PIP join is implemented and blocked on
deployment rather than contract. Reviewing its
validate_action_authorization against what this engine actually serves
shows that is not the whole story: it expects a state-hub-authority
ActionAuthorization (id, status, superseded_by, request,
approvals.entries, policy pin) while GET /v1/approvals/{id}/claim serves
the governed approval-claim (approval_id, state/valid_now, binding,
freshness, reason_code, issuer approval-engine).
Both envelopes declare schema_version 0.1, so the version check passes
and the mismatch surfaces as a field or authority error that reads like
an approval-engine outage.
Document the field-by-field divergence and why the omissions are
deliberate: a claim is a fact about an approval object, not a decision,
so approver identities and policy pins are not republished. Reconciling
the envelopes is a GH-DEC-2026-003 cross-repo change, so the governed
claim schema is left unchanged here.
No code change; 84 tests still pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvyJPAaVCGsVheVhcCwNND
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 411227@bnt-lap001
Assistant-Session: d566f6d3-bcaf-43c3-bc5e-3ddd0f64b535
2026-09-06 01:09:23 +02:00
|
|
|
|
|
|
|
|
2026-09-06 follow-up: secrets-engine reports its PIP join is implemented and
|
|
|
|
|
blocked on deployment, not contract (inbox `61ae1174`). Review of its
|
|
|
|
|
`validate_action_authorization` shows a real envelope divergence: it expects a
|
|
|
|
|
`state-hub`-authority `ActionAuthorization` (`id`, `status`, `superseded_by`,
|
|
|
|
|
`request`, `approvals.entries`, policy pin) while this engine serves the
|
|
|
|
|
governed approval-claim (`approval_id`, `state`/`valid_now`, `binding`,
|
|
|
|
|
`freshness`, `reason_code`, `issuer: approval-engine`). Both declare
|
|
|
|
|
`schema_version` `0.1`, so the mismatch surfaces as a field/authority error
|
|
|
|
|
rather than a version error. Recorded in `docs/approval-consumption.md`;
|
|
|
|
|
reconciling the envelopes is a `GH-DEC-2026-003` cross-repo change, not a
|
|
|
|
|
unilateral edit here. T05 stays `wait`: still no deployed base URL (T03).
|
2026-09-06 01:36:03 +02:00
|
|
|
|
|
|
|
|
2026-09-06 ruling: the claim-envelope question is settled. `GH-DEC-2026-005`
|
|
|
|
|
confirms all three requested dispositions — the approval-claim is the step-1
|
|
|
|
|
artifact, `ActionAuthorization` is not required and MUST NOT be served from the
|
|
|
|
|
claim endpoint, and a PEP validates across the claim and the step-2
|
|
|
|
|
`DecisionEnvelope`. Gate House recorded the split as doctrine (a PIP must not
|
|
|
|
|
republish the PDP's decision) and struck the `provenance.authority ==
|
|
|
|
|
"state-hub"` requirement explicitly. flex-auth accepted as `FLEX-DEC-2026-006`,
|
|
|
|
|
argued against its own proposal, and traced the bad authority constant to a
|
|
|
|
|
fixture rather than prose. **This engine changes nothing: the claim schema
|
|
|
|
|
stands as published.** `APPROVAL-IN-0002` is closed. T05 remains `wait` on T03
|
|
|
|
|
deployment plus the secrets-engine validator split and its
|
|
|
|
|
`secrets-engine-approval` KeyCape registration (already requested verbatim in
|
|
|
|
|
`docs/keycape-service-registrations.md`).
|
Carry threshold evidence on issuance and use events
GH-DEC-2026-005 moved the distinct-approver check off the PEP onto this
engine's valid_now. secrets-engine has implemented the split and reports
it no longer verifies the threshold independently. Gate House accepted
that as correct on layering AND as a genuine reduction in defence in
depth, and named the compensating control: not a second check at the PEP,
which is the duplication the split removes, but reconstructability at the
issuer under §9.6.
The emitted events could not support that. approval.issuance carried
required_count but never who satisfied it, and approval.use carried no
threshold evidence at all, so an auditor replaying the stream could not
recompute the evaluation without reading live rows -- rows that may since
have been superseded, revoked, or expired.
Both events now carry a threshold object: required_count,
distinct_approver_count, threshold_met, and approvers with approved_at
plus assurance and evidence_ref when recorded. Tests prove reconstruction
from the use row alone, and that the claim still discloses no approver
identities -- they are evidence for audit-core, not consumer-facing, and
the claim keeps disclosing the least it can.
Writing the tests showed distinctness is already a storage invariant:
entries is UNIQUE on (approval_id, subject_id), so a repeat approver is
refused at insert and a separate entry_count could never differ from the
distinct count. Dropped that field rather than ship a number that cannot
vary, and the test now asserts the refusal instead.
88 tests pass (4 new).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvyJPAaVCGsVheVhcCwNND
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 411227@bnt-lap001
Assistant-Session: d566f6d3-bcaf-43c3-bc5e-3ddd0f64b535
2026-09-06 08:10:21 +02:00
|
|
|
|
|
|
|
|
2026-09-06 follow-on (T01): `GH-DEC-2026-005` assigned this engine a
|
|
|
|
|
compensating obligation. secrets-engine has implemented the split and reports
|
|
|
|
|
it no longer verifies the distinct-approver threshold independently; Gate House
|
|
|
|
|
accepted that as correct on layering and as a real reduction in defence in
|
|
|
|
|
depth, requiring instead that the threshold evaluation be reconstructable from
|
|
|
|
|
this engine's state transitions and its use outbox row under §9.6. Implemented:
|
|
|
|
|
`approval.issuance` and `approval.use` now carry a `threshold` object
|
|
|
|
|
(`required_count`, `distinct_approver_count`, `threshold_met`, and `approvers`
|
|
|
|
|
with `approved_at` plus assurance/evidence refs). Tests prove reconstruction
|
|
|
|
|
from the use row alone and that the claim still discloses no approver
|
|
|
|
|
identities. Documented in `docs/outbox-contract.md`.
|
State pdp_digest explicitly; decline to publish a vocabulary mapping
flex-auth asked whether this engine should publish an action/target
mapping between the claim binding's vocabulary (secrets.kv.destroy,
{"id": "lane-openbao-root"}) and a policy package's (destroy, lane:...),
since their package makes no cross-check that a claim was approved for
the action being decided.
Answered no. A PIP asserting that one vocabulary's action means
another's would author policy semantics it does not own, over
vocabularies it does not own, and the failure mode is asymmetric: a wrong
mapping silently accepts a claim approved for a different action, which
is worse than no mapping. binding.pdp_digest is the correspondence and
sidesteps vocabulary entirely -- it compares the PDP's own digest to the
PDP's own digest, with no translation by anyone.
Implemented the part that was ours. pdp_digest was emitted only when
recorded, so a consumer could not distinguish "not issued against a
decision" from "we forgot to look". It is now always present and null in
that case, required-but-nullable in the schema, and documented as
something a PEP on a privileged lane must refuse. This engine states the
fact; enforcing the lane's policy stays with the consumer.
Both published examples were already contradicting the updated schema by
omitting the field -- the same fixture-versus-contract defect flex-auth
hit twice this week and that secrets-engine implemented. Fixed both, made
them cover the PDP-bound and unbound shapes so neither is inferred from
the other, and added tests/test_examples.py to validate every example
against the schema so the class cannot recur here. jsonschema added as a
dev dependency.
94 tests pass (6 new).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvyJPAaVCGsVheVhcCwNND
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 411227@bnt-lap001
Assistant-Session: d566f6d3-bcaf-43c3-bc5e-3ddd0f64b535
2026-09-06 09:32:11 +02:00
|
|
|
|
|
|
|
|
2026-09-06 follow-on (T05): flex-auth asked whether this engine should publish
|
|
|
|
|
an action/target vocabulary mapping between the claim binding and their policy
|
|
|
|
|
package before `SECRETS-WP-0007-T04` makes destroy reachable. Answered no, and
|
|
|
|
|
recorded why in `docs/approval-claim.md`: a PIP asserting that one vocabulary's
|
|
|
|
|
action *means* another's would author policy semantics it does not own, and a
|
|
|
|
|
wrong mapping silently accepts a claim approved for a different action.
|
|
|
|
|
`binding.pdp_digest` is the correspondence — it compares the PDP's digest to
|
|
|
|
|
the PDP's digest with no translation. Implemented the part that was ours:
|
|
|
|
|
`pdp_digest` is now always present on the claim and `null` when the approval
|
|
|
|
|
was not issued against a PDP decision, so absence is a stated fact rather than
|
|
|
|
|
a missing key, and the schema requires it as nullable. A PEP on a privileged
|
|
|
|
|
lane must refuse a null. Both published examples were contradicting the schema;
|
|
|
|
|
fixed, and `tests/test_examples.py` now validates every example against it.
|
Implement GH-DEC-2026-008: declared PDP-path intent, enforced at issue
Gate House ruled binding.pdp_digest is the binding correspondence on the
GH-DEC-2026-003 path and is required there, having rejected a vocabulary
mapping for the reasons we gave. It asked this engine to record the PDP
digest at issue for approvals intended for that path, and to have the
claim state which approvals those are rather than leaving it to the
requester's memory.
Schema v3 adds approvals.pdp_path. create() refuses pdp_path true without
a pdp_digest, so an approval that would be unusable on the path fails at
issue rather than at the protected side effect. The claim exposes
binding.pdp_path, which makes it a guarantee rather than a hint: pdp_path
true implies pdp_digest is non-null.
Intent is declared and never inferred. A pdp_digest that happens to be
present is not a declaration anybody made, so a recorded digest alone
leaves pdp_path false, legacy rows migrate to false rather than being
back-filled from their digests, and a successor inherits its
predecessor's declaration. Approvals issued before the ruling stay usable
by consumers in this engine's own vocabulary and are simply not usable on
the PDP path -- the ruling's intended cost, stated as such.
Schema, both published examples, a v2-to-v3 migration test asserting
survivors keep their digest while declaring no path intent, and tests for
refusal at issue, claim exposure, non-inference, and successor
inheritance. 102 tests pass (8 new).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvyJPAaVCGsVheVhcCwNND
Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 411227@bnt-lap001
Assistant-Session: d566f6d3-bcaf-43c3-bc5e-3ddd0f64b535
2026-09-06 14:51:23 +02:00
|
|
|
|
|
|
|
|
2026-09-06 follow-on (T01/T05): implemented `GH-DEC-2026-008`, which requires
|
|
|
|
|
`binding.pdp_digest` on the `GH-DEC-2026-003` path and directed this engine to
|
|
|
|
|
record the PDP digest at issue and have the claim state which approvals those
|
|
|
|
|
are. Schema v3 adds `approvals.pdp_path`; `create` refuses `pdp_path: true`
|
|
|
|
|
without a `pdp_digest`, so the failure lands at issue rather than at the
|
|
|
|
|
protected side effect. The claim exposes `binding.pdp_path`, making
|
|
|
|
|
`pdp_path: true` a guarantee that `pdp_digest` is non-null. Intent is declared,
|
|
|
|
|
never inferred from an incidental digest, and never back-filled: legacy rows
|
|
|
|
|
migrate to `false` and a successor inherits its predecessor's declaration.
|
|
|
|
|
Schema, both examples, and a v2→v3 migration test cover it (102 tests).
|
2026-09-06 19:45:40 +02:00
|
|
|
|
2026-09-06 20:35:59 +02:00
|
|
|
2026-09-06 follow-up (envelope target): flex-auth published
|
|
|
|
|
`binding.approval_binding_digest` (`FLEX-DEC-2026-007`) — the request digest
|
|
|
|
|
material with `context.approval` removed — because a claim-bearing request's
|
|
|
|
|
`request_digest` covers the carried claim and can therefore never equal a
|
|
|
|
|
`pdp_digest` recorded at issue. A consumer obeying `GH-DEC-2026-008` against
|
|
|
|
|
`request_digest` would have failed closed permanently on every claim, which is
|
|
|
|
|
the defect secrets-engine and flex-auth both hit. The value this engine records
|
|
|
|
|
at issue is unchanged and correct; only the consumer-side comparison target
|
|
|
|
|
needed naming. `schemas/approval_claim.schema.json` and `docs/approval-claim.md`
|
|
|
|
|
now name `approval_binding_digest` as the execute-time target and state that
|
|
|
|
|
`request_digest` is never it. No code change was required, so T05 stays `wait`
|
|
|
|
|
on the deployed base URL (T03).
|
|
|
|
|
|
2026-09-06 19:45:40 +02:00
|
|
|
## Production preflight — 2026-09-06 Glas deployment session
|
|
|
|
|
|
|
|
|
|
User authorized production deployment. Live cluster inspection confirms no
|
|
|
|
|
approval-engine workload/service. KeyCape exists at service keycape in sso;
|
|
|
|
|
live issuer is https://kc.coulomb.social. Corrected those two stale deployment
|
|
|
|
|
inputs. Neither secrets-engine-approval nor approval-engine-operator appears
|
|
|
|
|
in the live KeyCape client configuration. The audit sender-scope ConfigMap
|
|
|
|
|
currently registers only user-engine. No credential values were emitted.
|
|
|
|
|
|
|
|
|
|
T03 remains wait: provision the two KeyCape registrations and protected client
|
|
|
|
|
credentials, register/custody the approval-engine audit sender, align the
|
|
|
|
|
store/client tenant contract, then build/scan and pin the release image. The
|
|
|
|
|
manifest currently says platform while requested KeyCape registrations say
|
|
|
|
|
tenant:coulomb (and the secrets-engine policy says tenant:platform); exact
|
|
|
|
|
claim/store comparison requires an owner-consistent choice before activation.
|
|
|
|
|
Do not substitute guessed values or start production without audit delivery.
|
|
|
|
|
The secrets-engine PDP was independently deployed by FLEX-WP-0021-T04; this
|
|
|
|
|
does not satisfy approval service readiness.
|
2026-09-06 23:32:19 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
## Registry publication — 2026-09-06, Glas deployment session
|
|
|
|
|
|
|
|
|
|
The previously blocked push succeeded under the operator's standing production
|
|
|
|
|
deployment authorization. Registry tag `0.1.0` resolves to OCI index digest
|
|
|
|
|
`sha256:73333f5ceb55e48192e3095cb2e2a741cdc6ff0be2f18128301072b4a6b6eb9d`.
|
|
|
|
|
The linux/amd64 image manifest is
|
|
|
|
|
`sha256:2064d5378e01801ecf9966ccec9c93aadd16d6b404ef4890983d89fa738c164c`.
|
|
|
|
|
Both deployment images now pin the registry index digest, verified remotely
|
|
|
|
|
following publication. Its equality to the earlier local image identifier is
|
|
|
|
|
not the evidence of publication; the successful push and remote index lookup are.
|
|
|
|
|
|
|
|
|
|
No StatefulSet rollout yet: live KeyCape registrations and audit sender/token
|
|
|
|
|
admission remain outstanding. APPROVAL-WP-0002-T03 stays wait for those gates.
|
2026-09-10 18:39:30 +02:00
|
|
|
|
|
|
|
|
## Make presentation exclusion a tested binding contract
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
id: APPROVAL-WP-0002-T06
|
|
|
|
|
status: done
|
|
|
|
|
priority: high
|
2026-09-10 18:39:31 +02:00
|
|
|
state_hub_task_id: "1c6d6936-1a16-5f41-ba7d-5cfbd4231bf9"
|
2026-09-10 18:39:30 +02:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
GH-DEC-2026-015 §4 conditions informed-decision's nested binding.digest on an
|
|
|
|
|
explicit normative presentation exclusion and a regression that exposes widening.
|
|
|
|
|
The native digest MUST cover exactly the five act fields and MUST NOT acquire
|
|
|
|
|
presentation fields. Callers keep presentation outside effect scope; arbitrary
|
|
|
|
|
nested target scope is preserved, not silently filtered.
|
|
|
|
|
|
|
|
|
|
Completed 2026-09-10: docs/approval-claim.md states the rule. The durable issue/claim
|
|
|
|
|
regression varies view_hash, brief, packet, highlights, locale, UI release and a
|
|
|
|
|
presentation wrapper while proving the stored act/digest remain unchanged. It
|
|
|
|
|
also varies all five act fields independently and requires the digest to change.
|
|
|
|
|
A deliberate whole-binding canonicalization mutation fails the regression; the
|
|
|
|
|
production algorithm is unchanged. Evidence: docs/evidence/2026-09-10-presentation-exclusion.json.
|
|
|
|
|
This supplies the engine condition for INFD-IN-0004; actual informed-decision
|
|
|
|
|
adoption remains its existing work. T01 retains GH-DEC-2026-016 and native identity,
|
|
|
|
|
T03/T05 deployment/real consumption remain open. No live approval was issued.
|