Skip to main content
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:
PathContent
.sec0/audit-YYYY-MM-DD.ndjsonDaily audit log
.sec0/agents/<nodeId>/<YYYY-MM-DD>/<agentRef>.ndjsonPer-agent logs (when nodeId + agentRef present)
.sec0/raw/raw-YYYY-MM-DD.ndjsonRaw payload dataset (when enabled)
.sec0/.sec0-presign-state.jsonUpload checkpoint state

Audit Envelope Schema

FieldTypeDescription
tsstringISO 8601 timestamp
trace_idstringTrace ID (hex)
span_idstringSpan ID (hex)
tenantstringTenant/workspace
environmentstringEnvironment (dev/staging/prod)
client_namestringClient application name
client_versionstringClient version
serverstringServer name@version
toolstringTool name@version
statusok or errorOutcome
latency_msnumberLatency in milliseconds
retriesnumberRetry count
input_sha256string or nullSHA-256 of canonicalized input
output_sha256string or nullSHA-256 of tool output
policy.decisionallow or denyPolicy decision
policy.retentionstringRetention class
idempotency_keystring or nullIdempotency key
nodeIdstring or nullNode identifier
agentRefstring or nullAgent run reference
agentVariablesobject or nullAgent state variables
sigstringed25519: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

EndpointPurpose
POST /api/auth/validate-keyResolve tenant/client/env metadata from API key
POST /api/sec0/upload-urlGet 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

MemberTypeDescription
keyIdstringKey identifier
sign(data: Uint8Array) => Uint8ArraySign bytes
verify(data: Uint8Array, sig: Uint8Array) => booleanVerify signature

Helpers

FunctionDescription
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.