You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Custom durable evidence recorders now get the durable capability-configuration store (#310).
With verdict.capability_configurations.store unset, the store was selected by checking the
recorder class against a literal list of the two shipped durable recorders — a deployment with
its own durable recorder silently fell through to the no-op store, and configuration
fingerprints on its retained evidence became permanently unexpandable, with nothing warning.
Selection is now by declared capability: recorders implementing the new Fissible\Verdict\Contracts\DurableEvidenceRecorder marker contract (both shipped durable
recorders do) select the durable store; a custom durable recorder opts in by implementing the
marker, or keeps setting the store key explicitly. verdict:validate now warns when a
non-no-op recorder falls through to the no-op configuration store with the store key unset —
the silent-mismatch case, named at deploy time (an explicitly configured no-op store is a
declared choice and does not warn). No behavior changes for the shipped recorders or for explicit store configuration.
Canonical fingerprints no longer mutate PHP process state (#308). Float tokens are now emitted
locally in the same shortest round-trippable form as the previous serialize_precision=-1 path,
including its JSON exponent and zero-fraction conventions. CanonicalJson no longer reads or writes serialize_precision, so concurrent encodes cannot observe one another's temporary INI setting. The
compatibility suite pins existing fingerprints and checks identical output under 17, -1, 3, and 0.
Per-receipt authorization is now expressible — and required (#305). Receipts capture the
application's binding identifiers at issue time: whatever the application places in ActionContext(approvalContext: ['tenant_id' => …, 'conversation_id' => …]) is carried
verbatim on the receipt in a new nullable approval_context column, so "does this receipt
belong to a conversation this reviewer may decide" — the check the published controller could
only leave as a TODO, because the receipt didn't know — is writable for the first time. On top
of it, ApprovalManager::approve()/reject() now consult a required ApprovalDecisionAuthorizer (verdict.approvals.authorizer): with none configured they refuse
every decision (ApprovalAuthorizerMissing, fail-closed, consistent with the package posture
everywhere else), and when the configured authorizer denies they return the new unauthorized
outcome without touching the receipt. The store remains the single authority on receipt state —
the authorizer runs only against a found, id-matching receipt, and the fetch-then-transition
race is benign because it reads only fields immutable after issue. verdict:validate warns at
the wiring audit when confirmation-gated capabilities exist with no authorizer configured, and verdict:make-approval-flow now publishes a working App\Support\VerdictApprovalAuthorizer
(fail-closed on receipts that name no conversation) instead of a TODO. approved_by is
documented for what it is — attestation by the application — and claims' resolvedBy shares
that trust model at the artisan-only resolve surface. See who may decide a receipt. Upgrade note — approve()/reject() refuse until an authorizer is configured. An approve()
that succeeded on 0.11 will throw ApprovalAuthorizerMissing after upgrading, deliberately: set verdict.approvals.authorizer to a class implementing Fissible\Verdict\Contracts\ApprovalDecisionAuthorizer (re-run php artisan verdict:make-approval-flow for the working example), publish and run the new add_approval_context_to_verdict_approval_receipts_table migration, and pass the identifiers
your authorizer checks via ActionContext(approvalContext: [...]). Receipts issued before the
migration carry null context; the example authorizer refuses them, so decide-before-migrate
backlogs should be drained or handled explicitly in your authorizer. This also reaches tests: CapabilitySecurityTestKit::assertApprovalBindingInvalidation() decides a receipt, so test
suites using the kit need an authorizer configured — Verdict ships Fissible\Verdict\Testing\AllowAllApprovalAuthorizer for test environments (and verdict:validate warns when it is configured outside local/testing). Applications that adopt approvalContext should also drain receipts issued before the upgrade: the context now
participates in the binding fingerprint, so a pending pre-upgrade receipt will not validate
once the same action is proposed with a context attached.
Post-review hardening (external review, 2026-08-24), folded in before merge: decisions address
the receipt by id (ApprovalReceiptStore::find(), new contract method) rather than via findForToolCall(), whose null is ambiguous — absent or a colliding tool-call id — and would
have let a second receipt on the same call bypass the authorizer while the store still
finalized by id. approval_context participates in the binding fingerprint when supplied, so a
colliding tool-call id from a different conversation gets its own receipt instead of reusing —
and later consuming — one authorized against another conversation's context; an empty context
is omitted from the fingerprint, so an application that has not adopted approvalContext
produces the exact pre-capture fingerprint and its pending receipts survive the upgrade. The
authorizer is container-resolved lazily at decision time, so a misconfigured class breaks only
the decision path (verdict:validate reports a nonexistent or non-implementing class as an
error). The database store tolerates a missing approval_context column — writes omit it and
receipts hydrate as never-captured rather than hard-failing every confirmation-gated issue()
— and verdict:validate warns until the migration runs. Upgrade note — custom ApprovalReceiptStore implementations.ApprovalReceipt's
constructor gains a required approvalContext parameter (the @internal constructor reserves
exactly this right), the contract gains find(string $receiptId): ?ApprovalReceipt (unique-id
lookup; decisions authorize against it), and both shipped stores map the new column; a custom
store must implement find(), construct receipts with approvalContext (null for rows that
predate the column), and persist it on issue. The contract now documents each method's
invariants.
Verdict evidence now has a configuration-aware verification entry point (#307). php artisan verdict:evidence:verify resolves Verdict's configured fixed Attest chain and delegates
signature, chain, and anchor verification to Attest's attest:verify command. Its output makes the
configured coverage explicit: decisions and context releases are chained; provenance is included only
when chain_provenance is enabled, and approval receipts are never evidence-layer records. Deployments
using a tenant chain resolver must schedule one explicit --chain invocation for each concrete chain.
The integration suite records a real Verdict decision, verifies it, then corrupts the resulting upstream attest_envelopes artifact and proves verification fails.