secrets-engine.catalog-lane.lifecycle v1 contained no reference to input.tenant — not in well_formed, not in the denial ladder, not in a test. A rotate on lane:glas-primary under tenant:coulomb returned allow against the deployed package (decision:066e629bbf0c0924). Found while answering glas-harness's tenant-alignment request, which had asked for wrong-tenant denial evidence. There was none to return. Three covers failed the same way: every one of the 29 fixtures carried tenant:platform, so the suite could not report on the field; T02's own gate named "wrong-tenant deny" and was recorded done unmet; and the engine hashes tenant into request_digest but never compares it. Four other published packages carry the branch — this one was the outlier. v2 adds wrong_tenant above wrong_system, three Rego tests and three fixtures (28/28, 32/32). The absent-tenant test caught a second defect in the first draft: a bare input.tenant != comparison is undefined on a missing key, so the branch dropped and the ladder reported the wrong rung. request_tenant := object.get(input, "tenant", "") fixes it. v2 supersedes rather than amends v1 because the defect failed open: a consumer pinned to _VERSION=v1 would keep receiving allows with no signal the rule beneath the version string had changed. The earlier dual-control correction stayed at v1 because it denied everything. Replay envelopes regenerated at v2; both request_digest values are byte-identical, so secrets-engine's digest join needs no re-pinning. The sweep this prompted found tenant-engine unscoped on tenant as well — deployed, and verified allowing tenant:coulomb. Not the same fix: its request tenant names the target rather than the caller, so a constant would break it. Recorded and carried by FLEX-WP-0022 rather than patched unilaterally. FLEX-WP-0021 closes at T05; the pin still serves v1 until a redeploy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014aQMM1dPXaPiXVn6DwwtLd Assistant: claude-code Assistant-Model: opus Assistant-Process: 715613@bnt-lap001 Assistant-Session: fabd95c1-4c9e-4080-8849-8707ae025f80
4.2 KiB
T03 replay fixtures
Real DecisionEnvelopes emitted from the published package, for secrets-engine
to verify its digest join (627810b) unchanged. FLEX-WP-0021-T03.
| File | From |
|---|---|
decision_rotate.json |
../check_request_allow_rotate.json — plain allow, empty context |
decision_destroy_dual_control.json |
../check_request_allow_destroy_dual_control.json — dual control, valid approval-claim |
decision_wrong_tenant_deny.json |
../check_request_deny_wrong_tenant.json — foreign tenant, denied wrong_tenant |
Regenerate either with:
go run ./cmd/flex-auth check \
-policy examples/secrets-engine/policy_package.md \
-registry examples/secrets-engine/registry_snapshot.json \
-request examples/secrets-engine/check_request_allow_rotate.json
What is stable and what is not
Stable across runs — these are the fields to pin a contract test against:
| Field | rotate |
destroy |
|---|---|---|
binding.request_digest |
sha256:de67324f…4345 |
sha256:c749ee2…091a |
provenance.policy_package_digest |
sha256:bd11c5fe…c643 |
same |
provenance.registry_snapshot_digest |
sha256:f5a309bc…40bb |
same |
provenance.input_claim_digests.context |
absent (empty context) | sha256:8b73d29…2800 |
Verified identical across two runs.
Regenerated at v2, 2026-09-06. The package gained the tenant rule it had
been missing, so provenance.policy_version is now v2 and
policy_package_digest moved from sha256:fe0070b7…bd8c to
sha256:bd11c5fe…c643. Both request_digest values are unchanged — the
canonical digest covers tenant/subject/action/resource/context and not the
package, so a consumer that pinned the digest join has nothing to re-pin. Only
the two provenance fields moved, and they moved because the policy did.
decision_wrong_tenant_deny.json is the denial evidence for the tenant
question: the same rotate request as decision_rotate.json with
tenant: tenant:coulomb substituted. Its request_digest differs from the
allow's, which is the replay identity working — the tenant is hashed material,
so the two requests are not the same request. A deny carries no lifetime.
| binding.approval_binding_digest | absent (no claim) | sha256:fa07bec…cd56 |
That last row is the value an approval's pdp_digest must equal — never
request_digest, which a claim can never match on the request that carries it.
It equals the request_digest of the same request with context.approval
removed, which is verifiable from this directory: strip the context from
../check_request_allow_destroy_dual_control.json, re-run check, and the
claim-free request digest is the same value (it denies dual_control_required,
which is the point).
This fixture verifies itself. The claim inside the request carries
binding.pdp_digest equal to the envelope's approval_binding_digest, and
binding.pdp_path: true. Compare the two values in the two files. Note that
request_digest moved when the claim's contents changed while
approval_binding_digest did not — that is the stability property, demonstrated
in the artifact rather than asserted in prose.
Not stable: id, provenance.decision_time, and lifetime.not_before /
lifetime.expires_at move with the clock. lifetime.ttl is 15m from the
package's allow_ttl. Do not pin the record as a whole.
input_claim_digests.context appears only when the request carries a non-empty
context — which is why both fixtures are here rather than just one. A consumer
asserting the field is always present would pass on destroy and fail on
rotate.
The destroy request's context.approval is a complete approval-claim,
valid against approval-engine/schemas/approval_claim.schema.json including the
now-required binding.pdp_digest. It was regenerated on 2026-09-06 when that
claim was completed, so its digests differ from the first emission — a partial
claim in a fixture is how a consumer learns the wrong shape.
Not a deployment
These come from flex-auth/local in standalone mode
(provenance.evaluator / mode), not from a cluster pin. No
flex-auth-secrets-engine pin exists yet (FLEX-WP-0021-T04), and the consumer
policy pin stays unset until T05.