# Production guide

The SDK keeps a small local state for each project: the agents' private keys, each agent's chain of records, and the outbox of records waiting for the witness. Everything below follows from two rules: an agent name is one chain, written by one running instance, and that instance needs its key and the end of its chain.

## Where the state lives

By default the state is `.signafaide/` in the project folder. Set `SIGNAFAIDE_HOME` to put it anywhere else, for example a mounted volume:

```bash
SIGNAFAIDE_HOME=/var/lib/signafaide
```

`signafaide init` and `signafaide agent setup` add `.signafaide/` (and `.env`, which holds the API key after `init`) to `.gitignore`. The SDK never edits `.gitignore` while your program runs, so a CI checkout stays clean.

If the folder cannot be written, a wrapped tool does not run: the call raises `StateError` (strict mode or not), with a message that names the folder and what to do, never a raw `EACCES`. signafAIde fails closed here, because a tool that ran with no record would be an action nobody can show later:

```
StateError: cannot write signafAIde's local state in /app/.signafaide (read-only file system). Set SIGNAFAIDE_HOME to a writable folder ...
```

`SIGNAFAIDE_DISABLED=1` is the deliberate way to run tools without recording.

## Agent names and replicas

Give every agent an explicit name. Without one, the SDK uses the project folder's name, which in a container is often `app`:

```python
tools = signafaide.wrap_tools(tools, agent="claims-bot")
```

```ts
const tools = wrapTools(tools, { agent: "claims-bot" });
```

One agent name is one chain, and a chain has one writer. When you run several replicas of the same agent, give each its own name, for example with the instance or pod name: `claims-bot-0`, `claims-bot-1`. Two processes on one machine sharing one `SIGNAFAIDE_HOME` may use the same name: they take turns through a lock file. Two machines with the same name and separate state would fork the chain, and the witness refuses the second (`chain_conflict`).

## Keys from a secret store

Each agent's private key is a small JSON file, `.signafaide/agents/<agent>.key`. In production, keep it in your secret store and hand it to the SDK through the environment. Make a live key for the store with `keys create`: it generates the key, registers it with the witness under your live API key and prints the key file on one line, without writing a record or saving anything on the machine:

```bash
export SIGNAFAIDE_API_KEY=sfa_live_...
signafaide keys create --agent claims-bot-0 --env live --print > claims-bot-0.key   # store it, then delete the file
# in the secret store: SIGNAFAIDE_AGENT_KEY_LIVE_CLAIMS_BOT_0 = the line in claims-bot-0.key
```

Run it once per agent name (one per replica). It is refused when the agent already has an active key on the witness: replace that one with `signafaide keys rotate` where it is, or revoke it in the console first. For a key file you already have:

```bash
SIGNAFAIDE_AGENT_KEY_CLAIMS_BOT="$(cat .signafaide/agents/claims-bot.key)"
```

The variable name is the agent name in capitals, with every character other than a letter or digit turned into `_`. A live chain looks for `SIGNAFAIDE_AGENT_KEY_LIVE_<NAME>` first, then the plain name. In code, pass a key provider instead:

```python
signafaide.init(key_provider=lambda agent, scope: secrets.get(f"signafaide/{agent}"))
```

```ts
await init({ keyProvider: (agent, scope) => secrets.get(`signafaide/${agent}`) });
```

Sandbox and live chains use separate keys: the live key is `.signafaide/agents/live/<agent>.key`. A key exposed while testing in the sandbox never signs live records. `signafaide keys export` lists both for your auditors. To rotate a key that lives in a secret store, run `signafaide keys rotate` where the key file is, store the new file, and keep the old one in `.signafaide/agents/retired/` (or `agents/live/retired/`) so the export still lists it.

## Containers

Mount a volume and point `SIGNAFAIDE_HOME` at it. The keys can come from the volume or from the environment. With a volume, a restarted container continues its chains where they stopped.

## Ephemeral and read-only disks {#ephemeral}

Containers without a persistent disk, serverless functions and autoscaled replicas lose `.signafaide/` when they stop. Check these five before you go live:

1. **Content.** By default the inputs and outputs of each call stay only in `.signafaide/chain/`. On an ephemeral disk they go with the instance, and an auditor can then check that records are intact but cannot read what they say. Mount a volume for `SIGNAFAIDE_HOME`, or turn on stored content (`store_content=True`, `storeContent: true`; live records need Team), so the witness keeps the content encrypted.
2. **Heads.** `head.json` holds the newest co-signed receipt of each chain, which lets an auditor prove nothing was cut from the end. Keep it outside the instance with `SIGNAFAIDE_HEAD_FILE=/mnt/state/head.json` (a volume or mounted bucket; several instances can share one file), or the `on_head` / `onHead` hook in code. Without it, that check rests on the witness's own heads.
3. **Strict mode for actions that cannot be undone.** Tools that move money, freeze or close accounts, delete data or send messages: wrap them with `strict=True` (`strict: true`). The tool runs only once the witness has co-signed its decision record, so no such action can happen without a record that outlives the instance. If the witness cannot be reached, the tool does not run.
4. **One agent name per replica.** Give each instance its own name (`claims-bot-0`, `claims-bot-1`), each with its own key from `keys create`.
5. **A clean stop.** End each instance with `signafaide.stop()` (`await stop()`), for example in its SIGTERM handler: it sends what is left and writes a signed stop note. The next instance's start is then a clean restart. Without it every routine restart shows as an unclean start, a warning that earlier unsent records are not known.

```python
signafaide.init(on_head=lambda heads: bucket.put("signafaide/heads.json", json.dumps(heads)))
refunds = signafaide.wrap_tools([issue_refund, freeze_account], agent="risk-0", strict=True)
```

```ts
await init({ onHead: async (heads) => bucket.put("signafaide/heads.json", JSON.stringify(heads)) });
const refunds = wrapTools({ issueRefund, freezeAccount }, { agent: "risk-0", strict: true });
```

On a read-only disk, set `SIGNAFAIDE_HOME` to a writable folder: otherwise wrapped tools do not run (see above). With no persistent volume, set `SIGNAFAIDE_HOME` to a writable temporary folder (`/tmp/signafaide`) and give the keys through the environment or a key provider. A new instance does not know the end of its chain, so it writes signed drafts, even when `init()` already knows the project; when it first reaches the witness, it asks for the agent's newest receipt and continues the chain after it (a brand-new agent starts at 1). This needs one writer per agent name: run one instance per name.

The first record such an instance writes is a signed note, `signafaide.instance_start`, saying that it continues after seq N. What it means depends on the record before it:

- **Clean restart.** The earlier instance ended with `stop()` (or ended by itself with `SIGNAFAIDE_HEAD_FILE` set): it sent everything, then wrote a signed note, `signafaide.instance_stop`, as its last record. A start right after that note needs no warning, and the verifier, the console and the reports count it as a clean restart.
- **Unclean start.** There is no stop note right before the start: the earlier instance crashed, was killed, or could not reach the witness. Records it wrote but had not sent are not known, and nothing can show whether there were any. The verifier reports it under agent chains, and the console (Overview and Chain health), the auditor view and the verification report count it, so such a chain is never presented as complete without saying so.

```python
import signal, sys, signafaide

def on_term(signum, frame):
    signafaide.stop()   # sends what is left, then the signed stop note
    sys.exit(0)

signal.signal(signal.SIGTERM, on_term)
```

```ts
import { stop } from "signafaide";

process.on("SIGTERM", async () => {
  await stop();
  process.exit(0);
});
```

A stop note is written only when nothing is left unsent. If the witness cannot be reached at shutdown, no stop note reaches it, and the next start is unclean, as it should be.

`signafaide verify` on such an instance passes and says where its local chain starts: `continues after a co-signed record this machine does not hold: <chain> after seq N`. The earlier records are on the witness and in the verification pack.

Records not yet sent when an ephemeral instance stops are lost with its disk. Send them before it stops (see Serverless), and use strict mode for the actions that matter most (checklist above).

## Serverless

On AWS Lambda, Google Cloud Run and similar, use `SIGNAFAIDE_HOME=/tmp/signafaide`, keys from the environment, and send the outbox before the handler returns:

```python
signafaide.flush()
```

```ts
await flush();
```

When a program ends with records still waiting, the SDK makes one last attempt, holding the process at most 3 seconds (`SIGNAFAIDE_EXIT_FLUSH_MS`, `0` turns it off). Background requests never keep a finished program open, so a witness that stops answering cannot hold your job.

## CI and tests

- **Tests** never send records. The SDK turns test mode on by itself under pytest, Vitest, Jest and `node --test`: records go to a temporary folder and nothing is sent. `SIGNAFAIDE_TEST=1` forces it, `SIGNAFAIDE_TEST=0` turns it off.
- **Off switch.** `SIGNAFAIDE_DISABLED=1` makes `record()` and `wrapTools()` call your functions and write nothing.
- **A CI gate.** `signafaide verify --fail-on-pending-send` fails while any record waits to be co-signed, and a BLOCKED chain always fails (see [Verify](/docs/verify#blocked-chains-and-ci)). `signafaide status` exits 1 when a chain is BLOCKED.

## Sending and errors

Records ship in the background, one request at a time, with a backoff that grows to a minute when the witness cannot be reached. A refusal that retrying cannot fix (quota used up, a wrong or revoked API key, a revoked agent key) stops sending until the API key, `SIGNAFAIDE_URL` or the agent keys change; `signafaide flush` tries again at once. A malformed `SIGNAFAIDE_API_KEY` is caught on your machine and never sent. Outside strict mode nothing in a tool call waits for the network or raises because of it; problems are one warning line each.

## Clocks

A record's time is never earlier than the record before it in its chain, or than its key's creation. If the machine's clock steps back (an NTP correction, a resumed VM), the record gets the previous record's time and the clock's own reading is kept in its content as `meta.wall_time`. A clock that is ahead of the witness's by more than 5 minutes makes the witness refuse records; fix the clock. `signafaide repair` handles chains that can never ship: it sets those records aside (kept, still verifiable), gives the agent a new key and starts a new segment. See [Verify](/docs/verify#blocked-chains-and-ci).

## Settings

| variable | meaning |
|---|---|
| `SIGNAFAIDE_API_KEY` | the project's API key (also read from `.env`) |
| `SIGNAFAIDE_URL` | the witness; default `https://api.signafaide.co.uk` |
| `SIGNAFAIDE_HOME` | where the local state lives; default `.signafaide/` in the project |
| `SIGNAFAIDE_AGENT_KEY_<NAME>` | an agent's key file content, from your secret store (`SIGNAFAIDE_AGENT_KEY_LIVE_<NAME>` for a live key; `signafaide keys create --print` makes one) |
| `SIGNAFAIDE_HEAD_FILE` | also keep the newest signed heads in this file, outside the instance (a volume or mounted bucket) |
| `SIGNAFAIDE_INSTANCE_STOP` | `1`: write the signed stop note when a program ends with nothing left to send, even without a head file; `0`: never (with a head file set it is on) |
| `SIGNAFAIDE_EXIT_FLUSH_MS` | the longest a finished program is kept open to send (default 3000) |
| `SIGNAFAIDE_TEST` | `1` forces test mode, `0` turns it off under a test runner |
| `SIGNAFAIDE_DISABLED` | `1` turns recording off |
| `SIGNAFAIDE_SEGMENT_BYTES` | a shipped chain file larger than this rolls into a sealed segment (default 64 MiB) |
| `SIGNAFAIDE_TELEMETRY` | `0` leaves the adapter, the coding agent and usage events out of the client header |
