Audit events
Every meaningful action in an Evershell tenant lands an immutable row in the audit log: configuration mutations, member changes, proxy-decided HTTP allows and denies, workspace lifecycle events, task submissions. This page is the reference catalog — every event type the runtime emits, what fires it, and what’s in its detail.
The customer-facing query endpoint is GET /v1/audit. See
Querying below.
Categories
Section titled “Categories”Every event carries a category discriminator:
| Category | Meaning |
|---|---|
audit | Configuration mutations, credential state transitions, and HTTP / DNS policy decisions. The compliance signal: “Alice deleted role X at 14:23.” |
activity | Workspace and task lifecycle events — what’s running, what completed. The narrative signal: “task task_123 completed in 42s.” |
Both ride the same /v1/audit endpoint; filter on category
to split them.
Row shape
Section titled “Row shape”Every row carries the same set of columns, regardless of event type. Most columns are populated only for certain events:
| Column | Always populated? | Meaning |
|---|---|---|
id | yes | UUIDv7. Pagination tiebreaker. |
org_id | yes | Tenant identifier. |
user_id | yes | Actor attribution. A real WorkOS user id on session-authenticated requests and proxy-emitted events (lifted from the workspace’s creator); "system" on API-key-authenticated requests and WorkOS webhook-driven events. |
timestamp | yes | Event emission time (set at the source, not at storage). RFC3339. |
event_type | yes | The closed-enum slug — see catalog below. |
category | yes | audit or activity. |
actor | yes | "control-plane", "policy-engine" (proxy), or "agent". |
workspace_id | when scoped | The workspace this event belongs to. |
task_id | when scoped | The task within the workspace. |
agent_role | workspace events | Role name, auto-derived from workspace → role. Empty on non-workspace events (config mutations, membership changes). |
agent_provider | workspace events | Provider name, auto-derived from workspace → role → provider. Empty on non-workspace events. |
destination | proxy events | Upstream hostname (proxy policy_decision). |
method | proxy events | HTTP verb. |
path | proxy events | HTTP path. |
decision | proxy events | allow or deny. |
reason | proxy denies | Deny reason — closed enum, see Deny reasons. |
latency_ms | proxy events | Wall-clock latency across all callbacks. |
transforms_applied | proxy events | Count of transforms fired. |
validations_applied | proxy events | Count of schema validations run. |
policy_type | proxy events | http or dns. |
phase | proxy events | request_headers, request_body, response_headers, response_body, dns. |
capability_name | proxy events | Matched capability from the workspace’s caps.yaml. |
detail | most events | Event-specific JSON payload (see catalog). |
debits | proxy allows | Per-counter amounts committed at terminal-allow. |
budget_state | proxy allows | Per-counter snapshot at commit point. |
floored | proxy allows | Per-counter “ceiling applied” reasons. |
transforms_failed | proxy events | Per-transform failure reasons. |
auth_used | proxy events | Credentials whose mint succeeded on the request. |
auth_failures | proxy events | Per-credential mint failures. |
Event catalog
Section titled “Event catalog”Configuration mutations (category: audit, actor: control-plane)
Section titled “Configuration mutations (category: audit, actor: control-plane)”Each fires when an operator changes the corresponding resource via
the control plane API. Detail always carries resource_id (the
canonical id of the mutated resource) plus a per-event summary.
| Event type | Fires when | Detail keys (besides resource_id) |
|---|---|---|
provider_created | POST /v1/providers | name, display_name, kind, domain |
provider_updated | PATCH /v1/providers/{id} | Same + patched_fields |
provider_deleted | DELETE /v1/providers/{id} | name |
provider_archived | POST /v1/providers/{id}/archive | name, cascaded_roles (count of agent roles cascade-archived) |
provider_unarchived | POST /v1/providers/{id}/unarchive | name |
agent_role_created | POST /v1/agent-roles | name, display_name, provider_id, model |
agent_role_updated | PATCH /v1/agent-roles/{id} | Same + patched_fields |
agent_role_deleted | DELETE /v1/agent-roles/{id} | name |
agent_role_archived | POST /v1/agent-roles/{id}/archive | name |
agent_role_unarchived | POST /v1/agent-roles/{id}/unarchive | name |
content_pack_created | POST /v1/packs or /upload | name, display_name, version, visibility, tier, image_origin |
content_pack_updated | PUT /v1/packs/{id}, file edits, visibility flips | name, version, image_ref, kind (one of reupload / image_ref / file_edit / visibility), edited (file path on file_edit only) |
content_pack_deleted | DELETE /v1/packs/{id} | name, version |
content_pack_cloned | POST /v1/packs/{id}/clone | name, source_pack_id, source_name |
credential_created | POST /v1/credentials | name, provider, kind, scopes, status |
credential_deleted | DELETE /v1/credentials/{id} | name, provider, kind |
credential_authorized | OAuth callback completes | name, provider, kind, scopes, status |
credential_status | Proxy-side mint status flips | name, provider, kind, from_status, to_status, reason (proxy terminal-error string), terminal (bool) |
secret_created | POST /v1/secrets | name (= resource_id) |
secret_deleted | DELETE /v1/secrets/{name} | name |
api_key_created | POST /v1/api-keys | label, prefix, permission_set |
api_key_revoked | DELETE /v1/api-keys/{id} | label, prefix, permission_set |
membership_invited | POST /v1/orgs/{id}/memberships | email, role, invitation_id |
membership_accepted | WorkOS invite-accepted webhook | email, role, workos_user_id, invitation_id |
membership_updated | PATCH /v1/orgs/{id}/memberships/{user_id} | email, from_role, to_role |
membership_removed | DELETE /v1/orgs/{id}/memberships/{user_id} | email, role, was_pending, credentials_reaped |
membership_first_seen | First /v1/* request after invite-accept | email, role, source (currently always "self_heal") |
workspace_forked | POST /v1/workspaces/{id}/fork (parent row) | child_workspace_id, fork_point_task_count, fork_source_snapshot_ref |
workspace_fork_created | POST /v1/workspaces/{id}/fork (child row) | parent_workspace_id, fork_point_task_count, fork_source_snapshot_ref |
The detail payload never carries secret values — API key secrets, credential refresh tokens, vault content. Only metadata.
Workspace + task lifecycle (category: activity)
Section titled “Workspace + task lifecycle (category: activity)”| Event type | Actor | Fires when | Detail keys |
|---|---|---|---|
workspace_created | control-plane | POST /v1/workspaces or POST /v1/tasks | forked, parent_workspace_id, fork_point_task_count, fork_source_snapshot_ref (forks only) |
workspace_provisioned | control-plane | Kubernetes pod becomes ready and the workspace finishes provisioning | — |
workspace_activated | policy-engine | First inbound request lands on the proxy for this workspace | — |
workspace_deactivated | policy-engine | Workspace’s proxy state torn down (stop / archive) | — |
workspace_evicted | policy-engine | Workspace’s in-memory state aged out of the proxy (TTL expiry) | — |
workspace_restarted | control-plane | Pod respawned to pick up role / pack changes | — |
workspace_stopped | control-plane | Pod terminated and snapshot saved | — |
workspace_archived | control-plane | POST /v1/workspaces/{id}/archive | — |
workspace_unarchived | control-plane | POST /v1/workspaces/{id}/unarchive | — |
task_submitted | control-plane | POST /v1/tasks or POST /v1/workspaces/{id}/tasks | description |
task_completed | control-plane | Task reaches a terminal state (completed / failed / cancelled) | status, optional reason |
Proxy HTTP + DNS decisions (category: audit, actor: policy-engine)
Section titled “Proxy HTTP + DNS decisions (category: audit, actor: policy-engine)”The single most-queried event type. Every request that hits the
proxy fires exactly one policy_decision event at its terminal
phase — allow if it made it through, deny if not.
Event type: policy_decision
Phase values (where in the request lifecycle the decision was made):
phase | Meaning |
|---|---|
request_headers | Early admission gate (token verification, workspace lookup). |
request_body | Body-shape checks (schema, format, body-matching CEL, body transforms, budget reservations). |
response_headers | Response-side counters that don’t need body inspection. |
response_body | Streaming / body-inspecting counters (token usage, etc.). |
dns | DNS-level policy decision (policy_type: dns). |
Allow events populate: decision: allow, debits,
budget_state, floored (when extraction failure forced the
FailureCharge floor), transforms_applied, validations_applied,
auth_used, transforms_failed, auth_failures, capability_name.
Deny events populate: decision: deny, reason (see below),
plus detail.allowrule_eval for no_match / schema_violation /
format_mismatch denies, plus detail.counter (+ optional
detail.scope) for budget_exhausted / concurrency_exhausted
denies.
Deny reasons
Section titled “Deny reasons”Closed enum on the reason column:
reason | Triggered by |
|---|---|
no_scheme | Proxy bypass — the request reached policy enforcement without passing through the proxy’s TLS front-end. |
no_token | The request carried no capability token. |
invalid_token | Capability token failed verification. |
workspace_not_activated | Workspace id from token isn’t registered on this proxy. |
workspace_gone | Workspace deactivated mid-request. |
no_policy | Workspace has no OPA policy attached. |
policy_error | OPA evaluation panicked or returned a type error. |
policy_empty | Policy returned no result. |
policy_type_error | Policy returned an unexpected result shape. |
no_match | No allow rule matched the request (details in detail.allowrule_eval). |
invalid_json | Request body not valid JSON when a JSON schema is declared. |
format_mismatch | Content-Type disagreed with rule’s request_format. |
schema_violation | Request body failed request_schema validation. |
budget_exhausted | Counter hit its ceiling (detail.counter, detail.scope). |
concurrency_exhausted | Counter hit its max_concurrent (detail.counter). |
internal_error | gRPC handler panic. |
DNS-phase audit rows are emitted only on allow — the proxy’s
DNS resolver responds to denied lookups with a synthetic A record
so the agent’s connect() succeeds, then the eventual HTTP request
lands at the proxy’s request-phase handler and the actual deny is
recorded as a standard policy_decision event with policy_type: http. There is no separate DNS-phase deny vocabulary; all 16
reasons above apply to HTTP and DNS-redirected denies alike.
Transform failure reasons
Section titled “Transform failure reasons”When a header / path / query / body / auth transform fails to apply
but the request is still allowed (transform mutations are
observation, not policy), the proxy stamps
transforms_failed.<transform_name> with one of:
transforms_failed.<name> | Meaning |
|---|---|
compile_error | Runtime compile failure: a nil compiled CEL program reached Apply. Rare — programs compile at save time. |
eval_error | CEL Program.Eval returned an error (timeout, runtime CEL error). |
type_mismatch | CEL evaluated but produced a value the variant can’t consume (header expected string, body merge expected map, prefix target field non-string). |
apply_error | Go-side apply step failed after eval (secret fetch, JSON parse / remarshal, query parse). |
body_absent | Body transform skipped because the request carried no body. Distinguishes “nothing went wrong, just nothing to rewrite” from real failures. |
auth_unavailable | Auth broker couldn’t mint a token for the referenced credential. Covers terminal failures (invalid_grant, revoked, key removed) and transient ones (network error to provider, 5xx). Coarse on purpose — the audit never echoes provider error text. |
The key is the transform’s operator-assigned name when set,
otherwise the positional transform[N] identifier.
Auth failure reasons
Section titled “Auth failure reasons”auth_failures.<credential_name> uses the same closed enum as
transforms_failed, but auth transforms carry no operator CEL so
only auth_unavailable surfaces in practice. The key is the
credential name (the value an auth: { credential: <name> }
transform references), matching the broker’s resolve key.
When an auth transform fails, both maps are populated on the
same audit row: transforms_failed[<transform-name-or-position>]
records the failure class with the transform’s identifier, and
auth_failures[<credential-name>] records the same class keyed
by credential. The dual entry exists so operators can look up the
failure either by which transform broke (timeline / debugging) or
by which credential to repair (operations). Both carry the same
reason string.
Floored reasons
Section titled “Floored reasons”When a response counter’s extraction fails but the request is
still allowed, the proxy charges FailureCharge against the
counter and stamps floored.<counter_name> with one of:
floored.<counter> | Meaning |
|---|---|
framing_mismatch | Observed Content-Type framing (sse vs single_document) disagreed with counter.framing. |
format_mismatch | Observed format disagreed with counter.format. |
encoding_mismatch | Observed encoding disagreed with counter.encoding. |
parse_failed | JSON decode failure on the counter body payload. |
cel_eval_error | counter.value CEL expression returned an error. |
extracted_below_failure_charge | Extraction succeeded but produced a value below FailureCharge. |
terminator_not_observed | Stream closed before the terminal SSE marker. |
Agent events (category: activity, actor: agent)
Section titled “Agent events (category: activity, actor: agent)”Agents call POST /task/{id}/event (proxied to the CP) with a
type-schemed body. The proxy validates the body against the
workspace’s AgentEventSchemas, gates on the agent_events budget
counter (default name requests, 1 unit), and forwards to the
audit pipeline.
Event type: one of a closed enum the platform ships:
session_start, session_end, session_wrap_up, task_start,
task_end, iteration_start, iteration_end, agent_thinking,
agent_response, current_status, tool_call, tool_result,
compaction_start, compaction_end, step_status,
progress_status, file_change. The agent’s callback body must
include the matching type field; the proxy validates it against
the schema for that type and rejects anything else.
Detail: the raw callback body. The exact shape is workspace-policy-defined; treat it as opaque if you’re shipping the audit log to a SIEM.
Querying
Section titled “Querying”GET /v1/audit returns events. Query parameters:
?filter=<col>=<val>[,<val2>...] exact-match (IN list)?filter=<col>!=<val>[,<val2>...] not-match?filter=<col>!= non-empty (column populated)&from=<RFC3339> lower time bound&to=<RFC3339> upper time bound&order=asc|desc default desc&limit=<int> page size&cursor=<opaque> continue from previous pageRepeat filter for AND across columns. The whitelisted columns
(server-side) are:
workspace_id, task_id, decision, destination,
capability_name, event_type, category, agent_role,
agent_provider, method, path, actor, counter, phase,
policy_type.
counter is a virtual dimension that filters on JSONB key
existence across debits and budget_state — handy for “show me
every event that touched the tokens counter.”
Permission gating: callers with audit:read see every event in
the org; callers with only audit:read:own see events attributed
to their own user_id.
Examples
Section titled “Examples”Every deny in the last hour:
curl -H "Authorization: Bearer $TOKEN" \ "https://<slug>.evershell.ai/v1/audit?filter=decision=deny&from=$(date -u -d '1 hour ago' +%FT%TZ)"Every budget exhausted on the tokens counter:
curl "https://<slug>.evershell.ai/v1/audit?filter=event_type=policy_decision&filter=decision=deny&filter=counter=tokens"Every API key creation:
curl "https://<slug>.evershell.ai/v1/audit?filter=event_type=api_key_created"Sample raw rows
Section titled “Sample raw rows”provider_created:
{ "id": "0191234d-25fa-7abc-be23-8e7f4abc1234", "org_id": "org_acme", "user_id": "user_alice", "timestamp": "2026-05-23T14:32:15.123456Z", "event_type": "provider_created", "category": "audit", "actor": "control-plane", "detail": { "resource_id": "prov_anthropic_01", "name": "anthropic", "display_name": "Anthropic", "kind": "anthropic", "domain": "api.anthropic.com" }}policy_decision (allow, terminal at response_body):
{ "id": "0191234e-3a17-72cd-8f44-9b8e5dcd5678", "org_id": "org_acme", "user_id": "user_alice", "workspace_id": "ws_test_01", "task_id": "task_abc123", "timestamp": "2026-05-23T15:10:42.987654Z", "event_type": "policy_decision", "category": "audit", "actor": "policy-engine", "agent_role": "researcher", "agent_provider": "anthropic", "policy_type": "http", "phase": "response_body", "decision": "allow", "method": "POST", "path": "/v1/messages", "destination": "api.anthropic.com", "capability_name": "anthropic-api", "latency_ms": 1247, "transforms_applied": 3, "validations_applied": 1, "auth_used": ["anthropic-api-key"], "debits": { "requests": 1, "input_tokens": 1842, "output_tokens": 376 }, "budget_state": { "input_tokens": { "workspace": { "used": 18420, "max": 1000000 } } }}policy_decision (deny, budget exhausted):
{ "id": "0191234e-3a17-72cd-9012-a3c4e7f6abcd", "org_id": "org_acme", "user_id": "user_alice", "workspace_id": "ws_test_01", "timestamp": "2026-05-23T15:11:03.456789Z", "event_type": "policy_decision", "category": "audit", "actor": "policy-engine", "agent_role": "researcher", "agent_provider": "anthropic", "policy_type": "http", "phase": "request_body", "decision": "deny", "method": "POST", "path": "/v1/messages", "destination": "api.anthropic.com", "capability_name": "anthropic-api", "reason": "budget_exhausted", "detail": { "counter": "input_tokens", "scope": "workspace" }}