audit-core's completeness contract landed, and our single per-source beat is the shape it rules inadequate: it is discharged by whichever class is busy, so a revocation stream that has gone silent looks identical to a quiet one — and revocation is the only silence here that matters. Emit one nothing-to-report assertion per declared class, all four in one transaction so a partial emission cannot report some classes healthy and others stalled. Carry type audit-core.heartbeat with class and assertion on data. Pin that the first beat goes out at startup rather than an interval later, since a declared-but-never-sent class is their no_heartbeat_since_registration finding and not a skip. Declare heartbeat_classes and the reconciliation surface in the source registration, including the residual neither control covers: a compromised emitter suppresses the event and its own heartbeat together. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HyybaE7DUXrWYrhbnESCTe Assistant: claude-code Assistant-Model: opus Assistant-Process: 1275879@bnt-lap001 Assistant-Session: eb464208-f821-41b2-bc5a-a6c33d92a8ad
710 lines
42 KiB
Markdown
710 lines
42 KiB
Markdown
---
|
|
id: APPROVAL-WP-0002
|
|
type: workplan
|
|
title: "Production readiness and consumer adoption"
|
|
domain: infotech
|
|
repo: approval-engine
|
|
status: active
|
|
owner: codex
|
|
topic_slug: netkingdom
|
|
created: "2026-09-01"
|
|
updated: "2026-09-10"
|
|
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.
|
|
origin: residual
|
|
origin_ref: APPROVAL-WP-0001
|
|
state_hub_workstream_id: "4fa25ad5-f5d0-5592-aa59-085f8ee3edaf"
|
|
---
|
|
|
|
# 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.
|
|
|
|
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.
|
|
|
|
## Authenticate lifecycle mutations and approver evidence
|
|
|
|
```task
|
|
id: APPROVAL-WP-0002-T01
|
|
status: progress
|
|
priority: high
|
|
state_hub_task_id: "dc4523f5-e0af-5734-a0ef-07aa7f2b2a27"
|
|
```
|
|
|
|
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.
|
|
|
|
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,
|
|
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-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`.
|
|
|
|
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 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 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 (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 (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: 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: 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.
|
|
|
|
### 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.
|
|
|
|
## Harden durable storage and migrations
|
|
|
|
```task
|
|
id: APPROVAL-WP-0002-T02
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "2bef94ca-9482-5eff-9bd9-39adef292a78"
|
|
```
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
## Package and deploy the service
|
|
|
|
```task
|
|
id: APPROVAL-WP-0002-T03
|
|
status: wait
|
|
priority: high
|
|
state_hub_task_id: "f0aa2e6d-19e6-5b43-886c-efa4e3de5f22"
|
|
```
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
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.
|
|
|
|
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: 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.
|
|
|
|
### 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.
|
|
|
|
## Wire outbox delivery and reconciliation
|
|
|
|
```task
|
|
id: APPROVAL-WP-0002-T04
|
|
status: wait
|
|
priority: high
|
|
state_hub_task_id: "777a5cf5-c1db-5e07-8de6-ab109db5ccdc"
|
|
```
|
|
|
|
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.
|
|
|
|
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,
|
|
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-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: 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.
|
|
|
|
## Prove one live PEP consumption path
|
|
|
|
```task
|
|
id: APPROVAL-WP-0002-T05
|
|
status: wait
|
|
priority: high
|
|
state_hub_task_id: "3196fb77-3df0-5358-ae12-2d07bac12041"
|
|
```
|
|
|
|
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.
|
|
|
|
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 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.
|
|
|
|
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 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`).
|
|
|
|
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`.
|
|
|
|
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.
|
|
|
|
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 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).
|
|
|
|
## 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.
|
|
|
|
|
|
## 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.
|
|
|
|
## Make presentation exclusion a tested binding contract
|
|
|
|
```task
|
|
id: APPROVAL-WP-0002-T06
|
|
status: done
|
|
priority: high
|
|
state_hub_task_id: "1c6d6936-1a16-5f41-ba7d-5cfbd4231bf9"
|
|
```
|
|
|
|
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.
|