wallet: client-side quote verification on cryptographically authenticated chunks

The Merkle bundle gives the wallet authentic chunk bytes, but the
server still decides what audit_mode to claim. Run the existing
verify_quotes() locally on the bundle's chunks so the wallet has an
independent verdict that doesn't trust the server's verifier at all.

`VerifiedAnswer` now carries `local_audit_mode`, `local_n_verified`,
`local_verifier_method` alongside the server's audit_mode. They can
legitimately differ (server's context is larger), but a wallet-side
STRICT against a server-side UNGROUNDED would be a real "server lied
about not finding grounding" signal — exactly what the SPV pattern
exists to catch.

Opt out with `client.ask(q, verify_locally=False)` for pure-stdlib
SPV ports that can't load the verifier.
This commit is contained in:
russell@unturf.com 2026-05-30 07:47:24 -04:00
parent 7e160584b6
commit 296017295f
No known key found for this signature in database
3 changed files with 94 additions and 8 deletions

View file

@ -221,6 +221,48 @@ def test_server_healthz_and_snapshot_root(corpus_db: Path, tmp_path: Path):
httpd.shutdown()
def test_client_local_verify_quotes_runs_on_authenticated_chunks(
corpus_db: Path, tmp_path: Path
):
"""verify_locally=True re-grounds the answer on the bundle's chunks
(which are now cryptographically authenticated). A verbatim quote
answer reaches STRICT locally; a fabricated answer falls to
UNGROUNDED locally even if the server claims STRICT — that's the
'wallet doesn't have to trust the server's verifier' property.
"""
from arborist.wallet.client import WalletClient
from arborist.wallet.server import WalletServer, serve_in_thread
verbatim = (
'"eight forms of capital include living, social, and intellectual"'
)
def _factory():
return StubClient(answer=f"According to the source: {verbatim}.")
wallet_server = WalletServer(
qa_db=tmp_path / "qa.db",
single_db=corpus_db,
chat_client_factory=_factory,
)
httpd, _t, base_url = serve_in_thread(server=wallet_server, port=0)
try:
conn = connect(corpus_db)
try:
snap, _ = compute_snapshot_root(conn)
finally:
conn.close()
client = WalletClient(server_url=base_url, trust_anchor=snap)
verified = client.ask("What are the forms of capital?")
# Local verifier ran and reached STRICT on the authenticated chunks.
assert verified.local_audit_mode == "STRICT"
assert verified.local_n_verified is not None
assert verified.local_n_verified >= 1
assert verified.local_verifier_method in {"quote", "span", "entity", "paraphrase"}
finally:
httpd.shutdown()
def test_server_ask_end_to_end_returns_verifiable_bundle(
corpus_db: Path, tmp_path: Path
):