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
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
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
flex-auth published binding.approval_binding_digest (FLEX-DEC-2026-007) after
secrets-engine found that a claim-bearing request's request_digest covers the
carried claim, so it can never equal a pdp_digest recorded before that claim
existed. A consumer obeying GH-DEC-2026-008 against request_digest would have
failed closed permanently on every claim rather than on a bad one.
The value recorded at issue was already correct, so no code changes. What was
wrong was this repo's description of the comparison target: a reader would
reach for request_digest and fail closed forever. The schema and
docs/approval-claim.md now name approval_binding_digest as the execute-time
target and state that request_digest is never it, while leaving the issue-time
description as it stood.
Separately, docs/keycape-service-registrations.md now records a live collision
in the deployment inputs: the manifest serves --tenant platform while the
requested registrations issue tenant:coulomb, and ApiApplication.identity
compares them with exact string equality before any object lookup, so those
tokens would be denied 403 on every non-health route. flex-auth's
tenant:platform is a PDP subject this engine never reads and cannot bridge the
two. The values are left as-is on purpose — resolving it needs an owner
statement on whether the two name the same layer, and guessing grants
cross-tenant access to the approval store. The doc's stale issuer is corrected
to the live https://kc.coulomb.social from 06544b0.
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
secrets-engine found that flex-auth hashes context into the request
digest while the dual-control pattern carries the claim in
context.approval, so a pdp_digest recorded at issue cannot equal the
digest of the request that carries the claim.
The resolution is forced by ordering rather than chosen: issue precedes
the decision, so pdp_digest is necessarily the digest of the underlying
action request before any claim is embedded in it. A claim cannot carry
the digest of a document containing that claim -- the value would have to
be known before it could be computed.
Record that, and record the limit of our authority: the exact exclusion
rule belongs to the PDP's digest contract, not here. This engine stores
what it was given at issue and does not compute it. Warn consumers not to
guess the exclusion, because comparing digests derived under different
rules fails open toward accepting a claim bound to a different request.
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
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
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
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
fix-consistency C-32 registered the outcome block in the request doc as
hub decision b606e8ce and wrote the id back. Resolved it in the hub with
decided_by gate-house, since gate-house decided it on 2026-09-06 and a
record left open misrepresents live state in estate-wide open-decision
queries. The rationale records that this hub row was created from
approval-engine's repo and that gate-house's decisions.md remains the
source of truth for GH-prefixed decisions.
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
The ruling names docs/approval-claim.md as the step-1 artifact on the
GH-DEC-2026-003 path, but the contract did not say so. It listed only
access-engine as consumer, so an implementer reading the governed
artifact alone would not learn that PEP-shaped consumers read it as step
1, that no other envelope may be served from that endpoint, or that a PEP
validates across this claim and the step-2 DecisionEnvelope.
State the two-artifact split and the layer rule at the top, and state
that the approval fact's authority is this engine -- a consumer requiring
a state-hub authority fails closed against every correctly issued
response, which is the defect GH-DEC-2026-005 struck. Point both
consumers at the existing Required verification section.
Also update the request doc's trailing record block from proposed to
resolved with its decision id, so a reader copying it does not
reintroduce a pending record for a settled question.
Docs only; 84 tests 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
Gate House confirmed all three requested dispositions as GH-DEC-2026-005
(GH-IN-0002, closed); flex-auth accepted as FLEX-DEC-2026-006. 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 rather than convenience -- a PIP must not
republish the PDP's decision -- and struck the provenance.authority ==
state-hub requirement explicitly. This engine's claim schema is
unchanged.
Correct the deferred option D trigger list. The G3 trigger was written
conditional on G3 being settled by composition; flex-auth reports
FLEX-WP-0019 closed it by adding a lifetime field to DecisionEnvelope on
2026-09-02, so a decision now states its own end without borrowing
ActionAuthorizationValidity. That was the one structural thing the bundle
did that the split does not, so the trigger is not merely spent -- it
resolved against ratification. Struck with reasons; a future revisit
needs a fresh argument. The trigger came from a stale row in a dated
2026-08-29 review record that flex-auth does not rewrite.
Also record the root cause of the bad authority constant: it came from a
flex-auth fixture, not prose. A contract whose examples contradict its
prose will be implemented as its examples.
Close APPROVAL-IN-0002. Docs only; 84 tests 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
secrets-engine's PEP validator expects a flex-auth ActionAuthorization
but calls the governed claim endpoint. Research shows this is a
confirmation rather than a redesign: GH-DEC-2026-003 already names
GET /v1/approvals/{id}/claim as step 1 by endpoint and by field
(valid_now, which ActionAuthorization does not have), and
ActionAuthorization appears zero times in gate-house and state-hub. It
originates in flex-auth's own doc, which calls it a *proposed* shape for
the durable approval object that the same doc assigns to approval-engine.
Its required authority == state-hub also contradicts flex-auth's prose
that State Hub is not the runtime approval authority.
Request asks gate-house to confirm the claim is the step-1 artifact and
that ActionAuthorization is not required there, with PEPs validating
across the claim and the step-2 DecisionEnvelope they already fetch. No
safety property is lost; each check returns to the layer owning the data.
Records a ratified post-decision ActionAuthorization as a deferred option
with explicit revisit triggers, plus the constraint that such an object
cannot be served from the step-1 call, so it is not rediscovered later.
Also records why serving it at the claim endpoint and additively
extending the claim were rejected.
Files APPROVAL-IN-0002 to track the request. Docs only; 84 tests 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
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
Close remaining in-repo APPROVAL-WP-0002 gaps: drive GH-DEC-2026-003 against
the real HTTP surface, fail closed on JWT/human-consume/static-token paths,
treat audit 200 duplicates as drained, and ask KeyCape for the production
audience and client grants.
Assistant: grok
Assistant-Session: 01a06253-e557-7971-93d9-4f4c2cfbf455
Contracts first (T02–T04): approval claim schema with issuer, freshness,
and binding digest; local transactional outbox wire; load-bearing cadence
as heartbeat or reconciliation (layer.yaml declared).
Then the object (T06–T08): SQLite closed state machine, CAS supersession,
distinct-approver fail-closed, revocation without holder cooperation,
outbox insert in the same transaction. Tests fail the mutation when
emission fails, and revoke while the drain sink is down.
Introspection GET /v1/approvals/{id}/claim is a PIP fact, not a decision.
No public consume (T05 waits on GH-WP-0002-T06). Canon T-06 coverage for
wrong binding, expiry, revoke, and supersede.
FLEX-WP-0017 T03 is unblocked on this object; T05 remains blocked only on
consumption ordering.
Assistant: grok
Assistant-Session: 01a04ceb-2057-7e20-b0f9-c282964d5dd9