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
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
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:
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,
});
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.
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:
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,
});
Raw payload recording requires config.presign so the appender can enforce canonical tenant/env/client metadata.
Signing
Every audit envelope is signed with Ed25519 using sec0-sdk/signer:
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 |
Always use canonicalize() before signing JSON objects. Without deterministic key ordering, signatures break across serialization boundaries.