Skip to content

Audit log and bundle export

The Sill dashboard’s audit log surfaces every governed interaction Sill has observed for your account — Discovery beacons today, signed mandates and policy outcomes under Transactional. Each row corresponds to one record in Sill’s append-only, ed25519-signed, Merkle-chained audit envelope. From any record you can export the signed audit bundle that anchors it: a single signed envelope covering every record in the same per-batch scope, downloadable as JSON or as a server-rendered HTML document with the canonical signed JSON embedded.

The audit log surfaces records as Sill writes them to the envelope. Each row carries:

  • The visiting agent’s identity (matched against Sill’s identity registry, e.g. Anthropic, OpenAI, Google), or unknown_agent / human_likely when the request did not match a registered agent.
  • The decision token — observed for Discovery records, approved / rejected / escalated_approved / escalated_rejected / verification_rejected / rejected_post_verify for Transactional records.
  • The site the interaction was scoped to, the policy version that ran (Transactional), and the evaluation timestamp in ISO-8601 UTC.
  • The per-record ed25519 envelope signature, the previous-record hash that chains the record into the per-(site, decision_class, day) Merkle batch, and a signing_key_id identifying the KMS key version that produced the signature.

The visible mix is summarized in the header; rows are colored by decision. Detail panes expand a record into its full canonical shape, including the rule trace for Transactional records and the discovery context for Discovery records.

Audit log — app.sill.so Mandates view, dark theme. The OBSERVED pill is Discovery; APPROVED / REJECTED / ESCALATED are Transactional.

Every audit record can be exported as an audit bundle. The bundle is the signed cryptographic primitive: a single ed25519 envelope over the canonical body, covering every record in the same per-batch scope. The scope is one signed envelope per (site, decision_class, UTC date), so exporting from any record in a batch returns the same set of records.

The dashboard exposes the export from a record’s detail panel as Export JSON. The bundle is also retrievable directly from the audit-bundle endpoint described in the API reference; see GET /v1/audit/{record_id}/bundle.

Record detail panel — Export JSON triggers the signed-bundle download. The HTML variant is requested by appending ?format=html to the same endpoint.

Two formats are available today:

FormatEndpointFilenameWhat you get
Signed JSONGET /v1/audit/{record_id}/bundleaudit-bundle-{site_id}-{utc_date}-batch.jsonThe signed SignedAuditBundle envelope. The wire body is the cryptographic primitive.
HTML with embedded JSONGET /v1/audit/{record_id}/bundle?format=htmlaudit-bundle-{site_id}-{utc_date}-batch.htmlA server-rendered HTML document. The canonical signed JSON is embedded verbatim in a <script type="application/sill+json" id="sill-bundle"> block; cryptographic standing is identical to the JSON branch.

The package primitive toNdjson (one canonical record per line) lives inside @sill/audit for internal export composition, but only the JSON and HTML formats are exposed as merchant-downloadable bundle formats today. Additional formats are on the roadmap.

A bundle can be exported before its Merkle batch has closed. When that happens, the bundle is interim: the batch_roots[0].merkle_root carries a designated PENDING_MERKLE_ROOT sentinel and the dashboard renders a Pending anchor badge. The per-record envelope signatures verify normally; only the batch anchor is provisional. Re-exporting the same record after the batch closes returns the finalized root.

The bundle body — the bytes the signature is computed over — is the following shape (extra fields for envelope_signature and signing_key_id are added by the signer):

{
"bundle_id": "01K…",
"site_id": "01EXAMPLE00000000000000000",
"decision_class": "discovery",
"exported_at": "2026-06-22T15:42:10.123Z",
"record_count": 17,
"records": [
{
"record_id": "01K…",
"site_id": "01EXAMPLE00000000000000000",
"evaluated_at": "2026-06-22T15:38:02.001Z",
"policy_version": "2026-05-30",
"rules_evaluated": [],
"decision": "observed",
"envelope_signature": "…base64url…",
"prev_record_hash": "…base64url…",
"merkle_root": "…base64url-sentinel-or-batch-root…",
"retention_class": "discovery_default"
}
],
"batch_roots": [
{
"utc_date": "2026-06-22",
"merkle_root": "…base64url…",
"leaf_count": 17
}
],
"envelope_signature": "…base64url…",
"signing_key_id": "sill-audit-envelope-v1"
}

All hashes and signatures are base64url-encoded; timestamps are ISO-8601 UTC; IDs are ULIDs.

The bundle envelope is signed by the audit-envelope signing key (sill-audit-envelope-v1), distinct from the agent-card / ARD edge signing key. The signing input is built with a versioned domain separator so a bundle signature cannot be replayed as a per-record signature:

signing_input = SHA-256(
utf8("sill-audit-bundle-v1") || 0x00 || JCS-canonicalize(body)
)
signature = Ed25519-Sign(signing_input, audit_envelope_private_key)
  • body is the bundle object with envelope_signature and signing_key_id removed.
  • Canonicalization is RFC 8785 (JCS).
  • The signature is Ed25519 (JWS-EdDSA / RFC 8037).
  • The audit-envelope public key for verifying bundles is published at https://sill.so/.well-known/sill-audit-envelope-v1.pub.

Per-record envelope_signature values are signed using the same key but without the bundle domain-separator prefix — they sign the canonical record bytes directly. A verifier walks prev_record_hash from record to record to reconstruct the Merkle batch and confirm each record’s position in the chain. The general signature recipe is detailed in Verify a signature.

flowchart LR
  E[Edge mandate engine] -->|signed record| Q[Origin queue]
  Q -->|per-record ed25519 sign| AR[(audit_record)]
  AR -->|UTC-day batch closer| AB[(audit_batch · Merkle root)]
  D[Dashboard / API client] -->|GET /v1/audit/:id/bundle| API[Bundle endpoint]
  API -->|RLS-scoped SELECT| AR
  API -->|load batch row| AB
  API -->|sign bundle envelope| KMS[(Signing key)]
  KMS -->|envelope_signature| API
  API -->|JSON or HTML+embedded JSON| D
sequenceDiagram
  autonumber
  participant V as Verifier
  participant S as sill.so/.well-known
  participant F as Audit bundle file
  V->>F: Open bundle (JSON, or extract embedded JSON from HTML script block)
  V->>V: body = bundle without envelope_signature + signing_key_id
  V->>V: signing_input = SHA-256("sill-audit-bundle-v1" || 0x00 || JCS(body))
  V->>S: GET /.well-known/sill-audit-envelope-v1.pub
  V->>V: Ed25519-Verify(envelope_signature, signing_input, pub)
  V->>V: For each record: verify per-record signature + walk prev_record_hash

If any step fails — a single flipped byte in the body, a wrong public key, or a broken prev_record_hash chain — verification fails. Treat any record or bundle whose signature does not verify as untrusted.

The bundle endpoint is tenant-scoped via Postgres row-level security. A record_id that does not belong to the authenticated account returns 404 Not Found, never 403, so the existence of a foreign record is not leakable. Bundle composition, signing, and download all happen server-side; the dashboard fetches the body as a Blob and triggers the download client-side.

Does the HTML bundle have a different cryptographic standing than the JSON bundle? No. The HTML variant embeds the canonical signed JSON verbatim in a <script type="application/sill+json" id="sill-bundle"> block. A verifier extracts the contents of that block and runs the same recipe as it would for the JSON download.

What does the Pending anchor badge mean? The per-(site, decision_class, UTC date) Merkle batch had not been closed yet when the bundle was exported. The bundle is valid and the per-record signatures verify, but the batch root carries a sentinel (PENDING_MERKLE_ROOT) rather than a finalized root. Re-exporting after the batch closes returns the finalized anchor.

Which key signs the bundle? The audit-envelope signing key (sill-audit-envelope-v1). This is a different key from the agent-card / ARD edge signing key. The bundle’s signing_key_id records the key version that produced the signature; a future key rotation is dispatched on this value.

Is there an NDJSON or signed-PDF export today? Not as a merchant-downloadable bundle format. The dashboard ships signed JSON and the HTML variant with the canonical JSON embedded. An NDJSON line format exists inside the @sill/audit package as a primitive but is not exposed at the wire. Additional export formats are on the roadmap.

Can I export a single record? The bundle scope is per-batch (one signed envelope per (site, decision_class, UTC date)) — exporting from any record in a batch returns every record in that batch under one signature. This is intentional: the batch is the cryptographic anchor unit. The bundle’s HTML rendering highlights the record you opened so the position in the batch (“record M of N”) is unambiguous.

Does Sill provide an SDK to verify a bundle? No SDK is required. Verification uses only off-the-shelf primitives — an RFC 8785 JCS canonicalizer, an ed25519 verifier, and SHA-256. See Verify a signature for the worked recipe.

  • Audit envelope — the append-only, Merkle-chained record store the bundle is exported from.
  • Verify a signature — end-to-end verification recipe with a minimal JavaScript sketch.
  • Public JWKS — the JWKS endpoint that publishes Sill’s edge signing keys (note: bundle verification uses the audit-envelope key at /.well-known/sill-audit-envelope-v1.pub, distinct from the edge JWKS).
  • API endpoints — the wire surface for GET /v1/audit/{record_id}/bundle.
  • Sites and onboarding — how a site is registered before the audit log starts receiving records.
  • What is Sill — the identity / intent / proof framing the audit envelope provides “proof” for.