Skip to content

v0.12.0

Latest

Choose a tag to compare

@github-actions github-actions released this 25 Aug 06:29
· 16 commits to main since this release
v0.12.0
  • 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.