Skip to main content
Sanning holds a commitment to what your agent did. It never holds the thing itself. This page covers the half you keep, because a commitment to bytes nobody kept cannot be checked later. The log store is the SDK’s answer to that. Configure one and the retention is handled for you, in a layout the hand-over pack and an auditor can both read.

What the store holds

Two things, written before the event is anchored:
  • The content you passed to anchor, which was hashed in your process and never sent.
  • The canonical event record, the bytes payload_hash commits to. In TypeScript they are also on the result as eventBytes, in Python as event_bytes.
If the store write fails, nothing is anchored. There is no best-effort mode, because under this write path nothing else holds those bytes. The layout is a versioned contract named sanning.logstore/v1. A store root looks like this:
Anyone holding that directory can resolve an event’s bytes with no connector and no call to Sanning.
From 0.16.0, the store also keeps each record’s signed envelope, next to the record itself under envelopes/<payload_hash> (proof#57). A store written before this release has no envelopes/ directory and reads exactly as before; the layout marker stays sanning.logstore/v1.

Configure a store

Pass a store when you build the anchorer. FsLogStore writes to a local directory and is the development destination.
The plane refuses a production envelope that names no log store (400 no-log-store). Both SDKs check this for every production record, before it is signed. The record’s locator is the payloadRef (TypeScript) or payload_ref (Python) you pass on the call if you pass one, and otherwise the answer from the store’s locatorFor (TypeScript) or locator_for (Python). ObjectLogStore and FsLogStore have that method. CallbackLogStore takes a locator function as its second argument (locator= in Python). A production record with no locator from either place throws ProductionConfigError: the anchorer has no store and the call passes no payloadRef, the store has no locator method and the call has no payloadRef, or the method returns nothing (an empty or blank string, a non-string) or throws. The message names which one. Nothing is signed or sent, the chain does not move and no gap is recorded, so the next record you anchor links to the last one that was accepted. Building the anchorer never throws for this, because the SDK cannot know whether your calls will carry a payloadRef. A payloadRef that is not a valid URI, including an empty string, throws InvalidRefError in dev and production alike. dev is otherwise unaffected: a locator method that returns nothing (null, undefined or None) anchors with no payload_ref, and one that throws throws its own error. A payloadRef you pass is your own assertion: the SDK signs it as given and cannot vouch that it points at bytes you kept. With no store configured, a production call that carries one succeeds and logs a warning saying so. If you drain the bytes yourself instead, keep them keyed by event id. They are what an auditor’s check runs against.

Why the pack command reads the store

A hand-over pack is assembled by the bundle command, months after the run. It reads the proofs from the Sanning index and the bytes from your store.
Replace CONSOLE_API_KEY with the key from the console. There is no flag for it, because a credential in argv lands in shell history and this is a command people paste into tickets. The flags mean the same in both languages: --logs is optional in the flag grammar and required in practice. The verb needs your record objects to work out which events are this agent’s: subject.agent_id lives in the record, and a minimum-disclosure envelope does not carry it. Without the store the command exits 2 and says so. The verb verifies the whole pack, bundle and disclosed logs, before it writes anything, so a pack that would fail at the auditor is never produced.

When a pack can be built

Sanning commits your records to the public record on an interval, rather than one transaction per event. Until the interval holding your records is sealed, there is no inclusion proof to put in a pack, and the command tells you so:
That means not yet, rather than you have no evidence. A second line names how many records in the window are anchored and waiting for a stamp. Those records exist, they are signed, and they are retained. Read the second line before treating the first as a failure. Both languages print that pair, and the command exits 3 (undetermined): run it again after the interval seals. When nothing in the window matches at all, it exits 1. The partial case exits 3 too, and for a different reason:
A pack carries no record of what it left out, so a recipient cannot tell a short pack from a complete one. The verb refuses rather than hand you an artifact that looks complete and is not. Waiting for the interval to seal is usually the whole fix. To hand over the stamped records anyway, pass --allow-unstamped. The summary then tells you the pack is short, and passing that on is your job, because the artifact does not carry it:

Store the bytes in a bucket you own

For production, put the store behind object storage you control. The seam is the same in both languages: an object store inside a log store.
It is your bucket and Sanning never sees it. The store takes your own client, so any credential arrangement works, and an endpoint with path-style addressing makes S3-compatible backends behave alike. On first use the store checks the bucket once, before it writes a byte, and reports what it could not establish rather than assuming. A bucket that already exists cannot have Object Lock turned on afterwards, so the check tells you to create a fresh one instead of sending you to a console page that cannot help.
A locator points at where the anchored bytes live, and it rides inside the signed envelope, which is published permanently. Bucket names and object keys routinely carry tenant, agent and model names, so treat anything that can reach a locator as public. A ref or payloadRef you pass yourself is signed verbatim.

Build a pack straight from the bucket

--logs takes a local store root or an s3:// URL:
Reading a bucket needs SANNING_S3_ENDPOINT, SANNING_S3_ACCESS_KEY_ID and SANNING_S3_SECRET_ACCESS_KEY in the environment. There is no flag for them, because a credential in argv lands in shell history. A missing one is named individually rather than reported as “credentials are missing”, so you know which to set. The reader credential is enough: s3:GetObject and s3:ListBucket, and nothing that writes. See Set up the bucket. A local copy still works, so syncing the prefix down first remains a valid route when you would rather hand the command a directory.

API keys and scopes

What a key carries, and why the pack command needs no extra grant.

Dev and production

What production means, and why a pack carries one environment.