# Concepts

The full contract is the protocol specification; this page explains it in order.

## Records

A record says what an agent decided or what happened. There are three kinds:

- `decision`: written **before** a tool runs: the tool name and its input.
- `outcome`: written after it: the output, or the error. It points at its decision.
- `note`: anything else worth keeping, such as why the agent chose a tool.

The record's **content** is any JSON object, for example `{"input": ..., "output": ..., "model": ..., "reason": ...}`. The **envelope** around it holds the kind, the agent, a sequence number, the hash of the previous record, the time, the name and the salted hash of the content: `sha256(salt || JCS(content))`, where the salt is 16 random bytes drawn for each record and kept on your machine next to the content. The agent signs the hash of the envelope with its own Ed25519 key, which is created on your machine and never leaves it.

## Agent chains

Each agent's records form a chain: record 5 holds the hash of record 4. Removing, inserting or reordering a record breaks the chain at that point. A chain belongs to one project, one environment (`test` for the sandbox, `live`) and one agent name. The agent name defaults to the project folder name.

## Batches and receipts

The SDK ships records in batches. The service checks each one, builds a Merkle tree over the batch, and signs:

- the **batch**, which also chains to the project's previous batch, so the service keeps its own independent copy of the order;
- the agent's new **head** (its latest sequence number and record hash). Your SDK keeps the newest head it was given, so a service that later showed a shorter chain would be caught.

Each record gets a **receipt**: its Merkle proof, the signed batch and the signed head. The receipt is written next to the record in your local chain file.

## Timestamps

- **Not before:** each batch includes the newest round of the public [drand](https://drand.love) randomness beacon, which nobody could know in advance.
- **Not after:** every hour the service puts the hashes of all new batches (live and sandbox) in a Merkle tree, signs an anchor over its root, and gets an independent public timestamp for that anchor. Only a hash is published, never customer data; anyone can check the public timestamp independently ([Verify](/docs/verify) says how).

Together: the drand round says "not before", the anchor says "not after", and the two signatures say "both agreed".

**Pending** means co-signed, waiting for the public timestamp (an anchor is made every hour and usually confirms within a few hours); never counted as passed. The same word is used everywhere: in `signafaide verify`, the console, audit packs and verification reports. While an anchor waits, the timestamp servers have promised to include the root, and the proof is upgraded once it is confirmed. The service marks it **confirmed** only after checking the proof against the public record itself, and `signafaide verify` checks it again on your machine. Pending is normal for recent records and needs nothing from you.

Each anchor also commits to the service's key list at that moment (`keys_hash`), so the service cannot later rewrite when a key became valid.

## Agent keys

Each agent's key is made on your machine and never leaves it. The SDK registers it with a proof that it holds the key, so nobody can register your agent's public key in another project. Adding a second key to an agent needs a signature by its current key (`signafaide keys rotate` does this), or a developer or admin revoking the old key in the console first. A rotation retires the old key at the moment the service receives it, so it cannot sign a later rotation or add records. A retired or revoked key cannot add records, whatever time they carry.

An auditor checks records against **your** copy of the agent keys, not the service's: `signafaide keys export -o agent-keys.json` writes them, each self-signed, and the auditor passes `--agent-keys agent-keys.json`. Otherwise a service could add a key of its own. See [Verify](/docs/verify).

## Hash-only mode

By default the service receives only envelopes: the tool name, the agent name, the time, the chain links and the content hash, never inputs or outputs. The content hash is salted and the salt stays with you, so the service cannot learn the content from the hash, even content with few possible values such as `{"approved": true}`. Agent and tool names are sent in plain text as you write them, so keep personal data out of them. Inputs and outputs, with their salts, stay in `.signafaide/chain/<project>/<env>/<agent>.jsonl`, which is your evidence store. To prove what a record said, you show its content and salt; anyone can hash them and compare. When you store content with the service, its salt goes with it. To keep content with the service as well, turn on `store_content` (Python) or `storeContent` (TypeScript), for all tools or one: it is encrypted at rest, and live records need the Team plan.

```python
signafaide.init(store_content=True)          # all tools

@signafaide.record(store_content=True)       # one tool
def approve_payout(claim_id: str, amount: float): ...
```

```ts
await init({ storeContent: true });           // all tools

const approvePayout = record(async (claimId: string, amount: number) => {
  /* ... */
}, { name: "approve_payout", storeContent: true }); // one tool
```

## Strict mode

Normally nothing waits for the network: records are signed locally and, once you opted in, shipped in the background; they wait on disk when the service can't be reached. In strict mode the decision is shipped first and the tool runs only after the service has returned its receipt. Use it where an action must never run unrecorded. Strict mode needs an API key: without one the tool does not run (`no_key`).

```python
@signafaide.record(strict=True)
def wire_funds(account: str, amount: float): ...
```

```ts
const wireFunds = record(async (account: string, amount: number) => {
  /* ... */
}, { name: "wire_funds", strict: true });
```

If the receipt does not come, the tool does not run and the SDK raises `not_recorded`.

## Local first, then a witness

With no API key, records are signed with the agent's key, chained and kept in `.signafaide/`, and nothing is sent: no record, no request, no sandbox. `signafaide verify` checks the signatures, the chain links and the content hashes there, and says what is still missing:

```
co-signature, public timestamp: not yet. Run `signafaide init` to send hashes, or self-host (SIGNAFAIDE_URL)
PASS: 4 of 12 checks passed, 8 not checked (nothing co-signed yet)
```

Co-signature and the public timestamp need a witness. A person opts in once: `signafaide init`, or setting `SIGNAFAIDE_API_KEY` (with `SIGNAFAIDE_URL` for a [self-hosted witness](/docs/self-host)). Records written before that ship afterwards with their original times. The service accepts records up to 7 days old (an agent key's validity starts at most 7 days before it is registered); a local chain that starts earlier stays local, still verifies, and is reported as `too_old`.

A record sent late has two times: the claimed time (the agent's own clock, signed into the record) and the proven window (not before the randomness round of the batch it arrived in, not after its public timestamp). The console, the auditor view and audit packs show both, with the note "written offline, sent later" when the claimed time is before the window. This 7-day backdating window is the limit of what an agent can claim: a record dated more than 7 days before its key was registered, or more than 5 minutes after the service received it, is refused.

## Sandbox and claiming

`signafaide init` creates an anonymous sandbox project (no account, email or card), saves `SIGNAFAIDE_API_KEY` to `.env`, adds `.env` to `.gitignore` and prints a claim link. Records never create one by themselves, and neither does `init()` in code. A person opens the link, signs in and claims the project into their organization, which keeps its records. Live keys are created in the [console](/app/). A live key starts its own chains; the sandbox chains stay on disk.

## Local state

`.signafaide/` in the project root holds the agent keys (`agents/`), the chains with their receipts (`chain/`; records written before the opt-in are in `chain/_pending/nokey/`), the records not yet shipped (`outbox/`) and the newest signed heads. The SDK adds it to `.gitignore`, and adds `.env` too when it saves a key there. Back up the keys. The Python and TypeScript SDKs share this folder and its formats.

## What is sent

Besides records, each request carries one header naming the SDK and its version, the runtime, the framework adapter, the coding agent that set the project up and whether the call came from the SDK, the CLI or the MCP server. `signafaide verify` and `export` also report that they ran, when there is an API key. No content, names, paths or hostnames. With no API key nothing at all is sent. `SIGNAFAIDE_TELEMETRY=0` turns the optional fields and the events off.
