fissible / verdict-storefront
Clone-and-run reference application for fissible/verdict — a storefront support agent demonstrating correct Verdict wiring end-to-end. Replay mode by default (no model key required); live mode by opt-in.
Package info
github.com/fissible/verdict-storefront
Type:project
pkg:composer/fissible/verdict-storefront
Requires
- php: ^8.3
- fissible/verdict: ^0.10.0
- laravel/framework: ^12.0
- laravel/tinker: ^2.10.1
Requires (Dev)
- fakerphp/faker: ^1.23
- laravel/pail: ^1.2.2
- laravel/pint: ^1.24
- laravel/sail: ^1.41
- mockery/mockery: ^1.6
- nunomaduro/collision: ^8.6
- phpunit/phpunit: ^11.5.50
README
A clone-and-run storefront support agent demonstrating correct fissible/verdict wiring end-to-end — models propose, applications authorize. This app is the executable form of Verdict's adoption guide: when a question arises about how a piece fits, the answer is a file in this repo.
Spec: fissible/verdict#237. Build history: PROJECT.md. CI runs the exact clone-and-run path below on every push and fails on any
verdict:validateadvisory.
Five-minute start
PHP 8.3+ and Composer are the only requirements. No Node, no model API key, no database server.
git clone https://github.com/fissible/verdict-storefront.git cd verdict-storefront composer install composer run setup # .env, app key, SQLite database, migrations, synthetic seed data php artisan serve
The five walkthroughs
Every page carries a mode banner — check it first: in replay mode the model's proposals are recorded fixtures and everything downstream (Verdict, approvals, evidence) is live. Each walkthrough ends at a visible evidence row.
Start at http://127.0.0.1:8000 and sign in as Alice Storey.
1. An allowed owned-order read. In chat, click "Where is my order ORD-1001?".
The agent's turn shows the proposed tool call (LookupOrderTool({"order_number":"ORD-1001"}))
and the reply describes Alice's delivered order. Open evidence: the top rows are
orders.lookup / permit at the proposal and execution stages, with target strategy
refresh ✓ — the executor acted on a re-loaded row, resolved inside Alice's own orders.
The model's argument was a filter within her authority, never a global key.
2. A denied cross-principal read. Click "Show me the details of order ORD-2001."
(Bruno's order). The reply refuses. In evidence: orders.lookup / deny with
reason "Capability target could not be resolved." — outside Alice's scope the target
does not resolve, so the denial happened before the policy layer was even needed, and
it was recorded, not swallowed.
3. A denied injected-argument redirect. Open try to break it and send
Injected-argument redirect. The proposed tool call visibly carries the injected
number (ORD-2002) — and evidence shows orders.refund / deny, with no refund
row anywhere. An injected instruction can influence what is proposed, never what is
reachable.
4. Confirmation pause → approval → exactly-once completion. In chat, click
"Please refund order ORD-1001 — it arrived damaged." The chat shows the action
waiting for human approval and nothing has moved (no refund row in evidence — but a
pending approval receipt). Sign out, sign in as Sam Reyes (reviewer), open
approvals: the challenge shows the capability, the binding reason, and the
application's display context. Click Approve. Back as Alice: the conversation has
resumed and completed. In evidence: the receipt is consumed by user:<sam>,
orders.refund shows permit rows through the approval phases, and there is exactly one
refund. Submitting the decision a second time reports an outcome error — never a second
refund.
5. A filtered permit on a set-returning search. Open try to break it and send
Cross-principal order search. The prompt carries a filter, not an ID — "every order
containing the Aurora Desk Lamp" — and both Alice's ORD-1002 and Bruno's ORD-2001
contain that lamp. There is nothing to deny: the tool runs. In evidence:
orders.search / permit with target source context — the target is a search
scope resolved from Alice's session before the model's filter was applied inside it.
The reply lists only ORD-1002; Bruno's order is simply absent. (In the recorded fixture
the model then calls ORD-1002 "Bruno's" — its own confabulation, over the only data it
was given. A model can be wrong; it cannot be wrong about another customer's order.)
This is the set-shaped pattern from verdict#251:
app/Capabilities/Orders/SearchCapability.php resolves an OrderSearchScope via
Capability::usingPolicyForContextTarget(), OrderSearchScopePolicy authorizes the
scope, and the executor applies it as the query predicate.
Two modes, one integrity rule
- Replay (default, zero model dependencies). An app-owned gateway drives the real laravel/ai pipeline from recorded model proposals. Everything downstream of the model is live: Verdict evaluation, the confirmation pause, the approval round-trip, and real evidence rows in SQLite.
- Live (opt-in).
DEMO_MODE=liveplus ordinary laravel/ai provider configuration — an Anthropic key, or local Ollama with a tools-capable model.
Replay is never presented as live: every page carries a mode banner, and replay fixtures carry provenance (what produced the recorded proposal, when, and under what prompt). This app demonstrates Verdict's boundary and its evidence, not model susceptibility — susceptibility numbers live in Verdict's docs/evaluation.md and are not restated here.
Running live
# Anthropic (Laravel AI's default hosted provider) DEMO_MODE=live AI_PROVIDER=anthropic ANTHROPIC_API_KEY=sk-ant-... php artisan serve # Local Ollama — the model must report the `tools` capability DEMO_MODE=live AI_PROVIDER=ollama DEMO_MODEL=qwen2.5:7b php artisan serve
The provider wiring is ordinary laravel/ai configuration (config/ai.php) — the
replay gateway is simply not installed when DEMO_MODE=live. Two caveats, both
inherited from Verdict's evaluation notes:
- Ollama models must report the
toolscapability.gemma3:4breports onlycompletionand will never call a tool;gpt-oss:20breportstoolsand works. - An aligned model may refuse the bait on the "try to break it" page. That
refusal is a result, not a malfunction: it measures the model's alignment, while
Verdict's denial is deterministic policy. This app uses no forcing techniques
(no
tool_choice, no prefill, no comply-instructions) in either mode.
Versioning
This app pins tagged Verdict releases only — never dev-main. Each Verdict
release includes bumping this app, which is what makes it an upgrade-path fixture.
Current pin: fissible/verdict:^0.8.0.
License
MIT — see LICENSE.md.