> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sec0.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Audit & Custody

> Append-only signed audit logs with daily rotation, presigned uploads, and chain-of-custody metadata.

`sec0-sdk/audit` provides `Sec0Appender`, the audit log writer used by middleware, gateway, and instrumentation packages. It writes **append-only, signed NDJSON** audit files with daily rotation and optional presigned uploads to the Sec0 control plane.

## How Audit Works

Every hop in the Sec0 runtime produces an **audit envelope**, a structured JSON record that captures:

* **Timing**: Timestamp, latency
* **Identity**: Tenant, server, tool, node ID, agent reference
* **Tracing**: Trace ID, span ID, cause trace/span
* **Integrity**: SHA-256 hashes of inputs and outputs
* **Policy**: Decision (allow/deny), retention class
* **Signature**: Ed25519 signature over the canonicalized envelope
* **Agent state**: Variables, metadata, risk tags, findings

## Setting Up the Appender

```typescript theme={null}
import { Sec0Appender } from "sec0-sdk/audit";
import { LocalDevSigner } from "sec0-sdk/signer";

const signer = LocalDevSigner.fromKeyRef("file://./.sec0/keys/ed25519.key");
const appender = new Sec0Appender({
  config: { dir: ".sec0", retentionDays: 30 },
  signer,
});
```

## Writing Audit Envelopes

```typescript theme={null}
await appender.append({
  ts: new Date().toISOString(),
  trace_id: "0123456789abcdef0123456789abcdef",
  span_id: "0123456789abcdef",
  tenant: "my-app",
  server: "vision-mcp@1.0.0",
  tool: "fetch@1.0",
  status: "ok",
  latency_ms: 12,
  retries: 0,
  input_sha256: "e3b0c4...",
  output_sha256: "a1b2c3...",
  policy: { decision: "allow", retention: "30d" },
  idempotency_key: null,
  nodeId: "planner",
  agentRef: "run-123",
});
```

## File Layout

Given `config.dir = ".sec0"`, the appender writes:

| Path                                                   | Content                                         |
| ------------------------------------------------------ | ----------------------------------------------- |
| `.sec0/audit-YYYY-MM-DD.ndjson`                        | Daily audit log                                 |
| `.sec0/agents/<nodeId>/<YYYY-MM-DD>/<agentRef>.ndjson` | Per-agent logs (when nodeId + agentRef present) |
| `.sec0/raw/raw-YYYY-MM-DD.ndjson`                      | Raw payload dataset (when enabled)              |
| `.sec0/.sec0-presign-state.json`                       | Upload checkpoint state                         |

## Audit Envelope Schema

| Field              | Type              | Description                    |
| ------------------ | ----------------- | ------------------------------ |
| `ts`               | string            | ISO 8601 timestamp             |
| `trace_id`         | string            | Trace ID (hex)                 |
| `span_id`          | string            | Span ID (hex)                  |
| `tenant`           | string            | Tenant/workspace               |
| `environment`      | string            | Environment (dev/staging/prod) |
| `client_name`      | string            | Client application name        |
| `client_version`   | string            | Client version                 |
| `server`           | string            | Server name\@version           |
| `tool`             | string            | Tool name\@version             |
| `status`           | `ok` or `error`   | Outcome                        |
| `latency_ms`       | number            | Latency in milliseconds        |
| `retries`          | number            | Retry count                    |
| `input_sha256`     | string or null    | SHA-256 of canonicalized input |
| `output_sha256`    | string or null    | SHA-256 of tool output         |
| `policy.decision`  | `allow` or `deny` | Policy decision                |
| `policy.retention` | string            | Retention class                |
| `idempotency_key`  | string or null    | Idempotency key                |
| `nodeId`           | string or null    | Node identifier                |
| `agentRef`         | string or null    | Agent run reference            |
| `agentVariables`   | object or null    | Agent state variables          |
| `sig`              | string            | `ed25519:BASE64` signature     |

## Presigned Uploads

When `config.presign` is provided, the appender automatically uploads **only new bytes** since the last successful upload to the Sec0 control plane:

```typescript theme={null}
const appender = new Sec0Appender({
  config: {
    dir: ".sec0",
    presign: {
      apiBaseUrl: process.env.SEC0_CONTROL_PLANE_URL,
      auditKey: `Bearer ${process.env.SEC0_API_KEY}`,
      tenant: "my-app",
      environment: "prod",
      clientName: "my-agent-system",
      clientVersion: "1.0.0",
      timeoutMs: 5000,
    },
  },
  signer,
});
```

<Note>
  `presign.auditKey` is the exact value sent as the `Authorization` header to the control plane. For Sec0 API keys, include the `Bearer ` prefix (for example: `Bearer ${process.env.SEC0_API_KEY}`), not the raw key by itself. If you use an OIDC access token instead of an API key, set `tenant`, `environment`, `clientName`, and `clientVersion` explicitly because `/api/auth/validate-key` only accepts API keys.
</Note>

### Control-Plane Endpoints

| Endpoint                      | Purpose                                            |
| ----------------------------- | -------------------------------------------------- |
| `POST /api/auth/validate-key` | Resolve tenant/client/env metadata from API key    |
| `POST /api/sec0/upload-url`   | Get presigned upload URL for S3/GCS-compatible PUT |

## Raw Payload Capture

Write a separate dataset for payload analysis pipelines:

```typescript theme={null}
await appender.appendRawPayload({
  ts: new Date().toISOString(),
  trace_id: "0123456789abcdef0123456789abcdef",
  span_id: "0123456789abcdef",
  runId: "run-123",
  tenant: "my-app",
  direction: "input",
  payload_preview: "redacted preview...",
  payload_truncated: true,
  payload_bytes: 12345,
});
```

<Note>
  Raw payload recording requires `config.presign` so the appender can enforce canonical tenant/env/client metadata.
</Note>

## Signing

Every audit envelope is signed with Ed25519 using `sec0-sdk/signer`:

```typescript theme={null}
import { LocalDevSigner, canonicalize, toBase64 } from "sec0-sdk/signer";

const signer = LocalDevSigner.fromKeyRef("file://./.sec0/keys/ed25519.key");

// Sign arbitrary data
const payload = { ts: new Date().toISOString(), tenant: "my-app" };
const msg = Buffer.from(canonicalize(payload));
const sig = signer.sign(msg);
console.log(`ed25519:${toBase64(sig)}`);

// Verify a signature
const ok = signer.verify(msg, sig);
```

### Signer Interface

| Member   | Type                                             | Description      |
| -------- | ------------------------------------------------ | ---------------- |
| `keyId`  | string                                           | Key identifier   |
| `sign`   | `(data: Uint8Array) => Uint8Array`               | Sign bytes       |
| `verify` | `(data: Uint8Array, sig: Uint8Array) => boolean` | Verify signature |

### Helpers

| Function              | Description                             |
| --------------------- | --------------------------------------- |
| `sha256Hex(data)`     | SHA-256 hex digest                      |
| `canonicalize(value)` | Deterministic JSON string (sorted keys) |
| `toBase64(u8)`        | Base64 encoder                          |
| `fromBase64(b64)`     | Base64 decoder                          |

<Warning>
  Always use `canonicalize()` before signing JSON objects. Without deterministic key ordering, signatures break across serialization boundaries.
</Warning>
