Control-plane audit log

Coppice records API/envd decisions at the gateway boundary. The audit feed captures mutating requests, auth failures, scope denials, and HTTP errors with tenant labels when API-key auth is enabled.

What is recorded

Each event contains:

Successful GET/HEAD reads are intentionally not logged to keep the feed useful. Failed reads are logged. Successful POST, PATCH, PUT, and DELETE requests are logged.

API

curl -H "Authorization: Bearer $COPPICE_TOKEN" \
  "http://127.0.0.1:3000/audit/events?limit=50"

The route requires admin scope when API-key auth is enabled. Query parameters:

parametereffect
limitreturn at most this many events, capped at 500
offsetskip events after filtering
orderAsc=trueoldest-first instead of newest-first
tenantID / tenantfilter to one tenant
outcomefilter to allowed, denied, or error
pathPrefixfilter by route prefix

The response is:

{
  "events": [
    {
      "eventID": "8b7...",
      "timestamp": "2026-04-27T00:18:45Z",
      "outcome": "denied",
      "method": "GET",
      "path": "/audit/events",
      "status": 403,
      "tenantID": "team-b",
      "scopes": ["read"],
      "message": "API key lacks required scope: admin"
    }
  ],
  "count": 1
}

Persistence

The in-memory ring is enabled by default and keeps the newest 2048 events. Set COPPICE_AUDIT_LOG_CAP to tune that.

For durable JSONL, set:

export COPPICE_AUDIT_LOG=/var/log/coppice-audit.jsonl
export COPPICE_AUDIT_LOG_CAP=4096
service e2b-compat restart

The gateway creates the parent directory and appends each event as one JSON object per line. If the append fails, the in-memory ring still records the event and the gateway logs a warning.

Admin view

The Astro admin dashboard reads /audit/events?limit=12 and renders the most recent decisions next to lifecycle and metrics panels. In local dev, astro.config.mjs proxies /audit to COPPICE_ADMIN_GATEWAY or http://127.0.0.1:3001.

Receipt: benchmarks/results/api-key-auth/latest.txt.