context.case_id. Each side hands over a pack
of its own records, and a case reading drops every party’s pack together
and returns one case verdict, with every exception named.
This page uses
@sanning/anchor 0.16.0 and sanning-anchor 0.16.0 to build
packs, and @sanning/proof and sanning-proof 0.14.0 to read a case. Where
a behavior is new at one of those versions, this page says so.The path
Each side does the first three steps on its own records, and the reader does the last two:- Name the case on every record in it.
- Close each run.
- Bundle one pack per agent, with a declaration.
- Hand every pack to the reader.
- Read the case at the command line, and read the verdict.
Name the case on every record
createHandoff writes the case id into the two hand-off records for you. Any
other record that belongs to the case needs it passed by hand:
bundle --case selects records by the case id signed into each one, so a
record that names no case cannot be placed. When the pack carries a
declaration, bundle refuses a window that holds such a record rather than
guess, and names how many it found.
Close each run
A closing record says how a run ended. It lists the records the run delivered, what was observed (completed, failed or unknown), and, separately, what
your code states. The two are never merged, so a run that ended cleanly inside
a case that failed says both.
With the LangChain adapter, name the case when you build the handler and state
the outcome when you close it:
langchain.run_closed,
with closing.outcome.observed from the run’s own end and
closing.outcome.stated from your close(). The Vercel middleware takes the
same caseId and close({ outcome, reason }); it cannot see how a run ended, so
its observed status is always unknown.
Without an adapter, anchor a closing record yourself. promotedClosing builds
the same closing field the adapters write:
deliveredEventIds (delivered_event_ids) with the event ids this run
anchored. Pass unknown as the observed status unless something watched the
run end, and put what your code knows in stated. The case rides in caseId
on the anchor() call itself, the same option every other record in the case
uses; a closing record carries no case field of its own.
No kernel reads a closing record’s metadata. A case reading names it as a
step record, as Read the verdict shows.
Bundle one pack per agent
Wait until the interval holding the case’s records is sealed, which takes minutes. Then build each agent’s pack where that agent’s log store and seed are:API_KEYwith the organization’s API key.AGENT_SIGNING_SEEDwith the seed theassessoragent signs its records with.bundlesigns the declaration and the pack with it.assessor, the dates and./logstorewith the agent, the days and the log store root that hold the case.
--from and --to select on the time Sanning received each record, which can
sit a moment before the time your code captured. Widen the window rather than
cut it close.
Every declared pack signs sanning.declaration/v2: each record it holds
carries a role, handoff or step. A hand-off record’s message travels as
logs/<event id>.json, exactly JCS(message), the bytes its message_hash
commits to; a step record, such as the closing record above, is declared and
signed as a step, and a reader names it as one rather than treating it as an
unclaimed record. A full pack reads cleanly as a case, closing records and
all: you do not need to leave anything out to get a clean verdict.
Minimum disclosure across organizations.
--handoffs-only (with
--case and SANNING_SIGNING_SEED) packs only the case’s hand-off records
and claims each one in the declaration’s handoffs[], leaving step records
out and counted. Reach for it when your step records are not the other
organization’s to read; a full pack does not need it to read verified.--agent more than once, with --case, and bundle writes one pack per
agent, each signed by that agent’s own key, into one zip:
SEED_A and SEED_B with each agent’s 64-hex signing seed.
SANNING_SIGNING_SEEDS holds one <agent>=<seed> entry per --agent
(SANNING_SIGNING_SEED is refused beside it), and --logs is given once for
every agent, or once per --agent in the same order. This is the case
file: one zip holding packs/<agent>/bundle.json and its logs/, per
agent, beside contents.json (an unsigned index of every file and its
SHA-256) and HOW-TO-VERIFY.txt. Nobody signs the case file itself; each pack
inside it is still checked on its own, and the case reading below never reads
the index as evidence.
Two agents in two organizations cannot share one bundle call, because each
organization holds its own API key and log store: build each side’s pack
separately, as the single-agent command above shows, and hand both packs to
the reader.
Hand every pack to the reader
Send each pack, or the case file, to the reader by whatever channel you use for evidence. Each organization sends its own: the reader needs every party’s pack, because a pack holds only one side of each hand-off. Nothing about this step touches Sanning.Read the case at the command line
Each pack verifies on its own, from inside the unzipped pack:--logs logs/ when the pack has a logs/ directory. A pack of hand-off
records has none, and pointing --logs at a directory that does not exist is
an input error that exits 2.
None of a hand-off record’s content is disclosed, so its content binding is
undetermined rather than proven. A pack built with SANNING_SIGNING_SEED, as
above, says so in its own declaration, and reads verified (exit 0) even
though nothing in it discloses content. A pack built with no signing seed
carries no declaration to say the absence was on purpose, and the same pack
then reads incomplete (exit 3) instead. Either way the signatures and the
inclusion proofs still pass, and the output says so on its own lines.
A single-pack check does not pair anything. To read the case, pass every pack,
or the case file, to case:
case reads a folder holding one pack, a folder holding several packs, or a
case file named <name>.zip, unzipped as it reads it. It does not open a
.zip that is not a case file: given one, it exits 2 and asks you to unzip it
first and pass the folder. Pass --json for the full reading as data, and
--gateways <url,url> to also look up each pack’s witnesses on the public
record; the lookup is reported on its own line and never moves the verdict.
The following table lists the exit codes every verb of @sanning/proof and
sanning-proof uses, verify, export and case alike:
Read the verdict
A case reading reaches six of the kernel’s eight verdict words, and each one carries a fixed reliance line, printed with the headline:
Under the headline, the scope sentence names how many packs and signing
keys are in the reading, how many hand-off records, how many exceptions, and
that the reading was made offline, plus how many records’ content is withheld
by their holder and how many packs do not say what they hold, when either is
true:
verified, and the sentence says
so alongside it, never instead of it.
Below the scope sentence, the witness line says how far a record travelled
on the public record, independent of the headline: confirmed, not_checked
(no lookup ran, or one did not settle), or differs. It never moves the
headline. --gateways is what makes the lookup run; without it the line
always reads not_checked.
A one-sided hand-off, or several messages in one direction, is a named
exception, never a failure by itself. An honest exchange of several messages
in one direction, or a message that is fanning out to more than one receiver,
reads verified once every candidate is accounted for; only a genuine mismatch
reads failed, naming the record on each side:
What to read next
What a verdict means
Every verdict word, and reading a case from your own code.
What a hand-off record does not prove
Integrity, not accuracy, and the other limits to state to a reader.