# T03 replay fixtures Real `DecisionEnvelope`s 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: ```bash 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 2026-09-07 — `submitted_request_digest` added; every existing digest unchanged.** From `FLEX-DEC-2026-012`, which `secrets-engine` found by running against the live pin. `request_digest` is computed over the **enriched** request — after the evaluator overlays registry facts onto subject and resource — so it is not consumer-computable and never was. Pin **`submitted_request_digest`** instead: it is the digest over the request exactly as sent, and it is the §6.4.2 replay test. `request_digest` keeps both its meaning as flex-auth's own audit-replay identity and its exact value here — `de67324f…`, `c749ee2d…`, `c9c6e6f8…` are unchanged, as is `approval_binding_digest` at `fa07bec…`. The enrichment precedence changed in the same decision but these three requests contradict no registry fact, so nothing they hash moved. **Nothing you have pinned needs re-pinning; there is a new field to add, not an old one to correct.** | Field | `rotate` | `destroy` | `wrong-tenant` | | --- | --- | --- | --- | | `binding.submitted_request_digest` | `sha256:41c8fc08…` | `sha256:c605a9ec…` | `sha256:9aab6de9…` | A consumer that pinned `request_digest` from these files was pinning a value it could not have computed itself — it matched because it was copied from our output, not derived. That is the defect `submitted_request_digest` closes: the same assertion now has a value you can recompute from the request you sent. **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`.