AERDocumentation

Guide

Operating AER

The controls a platform team runs day to day: webhooks and digests for findings, retention and visibility for records, share links for auditors and the export surface for everything else.

How a deviation is decided

A baseline is the set of tools, hosts and tool orderings an agent used across the sessions you trained it on. A later session that steps outside that set raises a finding naming what it did that the baseline had never seen.

No language model is involved in any of this. Training and detection are deterministic rules over the metadata already in the record, they run on our infrastructure, and nothing is sent to a model provider. The same sessions always produce the same baseline and the same verdict, which is what lets us tell you why a run was flagged rather than that it scored badly. The engine that does it depends on nothing but our own schemas, and a test in the build fails if that ever stops being true.

Retraining replaces the policy every later session is judged against, so it is previewed before it is applied: the console shows what the new baseline would start and stop allowing, and how many of the findings the current one raises would go away.

Findings, pushed to you

Register a webhook and AER delivers findings and session completions (and, if you opt in, usage policy breaches and the agent pause events) as signed HTTP posts: each delivery carries an HMAC signature header (x-aer-signature-v2) computed under a per-webhook secret you can rotate at any time, plus a delivery history endpoint for audit and replay. For a daily summary instead of a push per event, set a digest address in Settings: at most one email per 24-hour window listing the new findings, cleared by removing the address.

POST /v1/webhooks                       {url, event_types?}
POST /v1/webhooks/:id/test              fire a test delivery
POST /v1/webhooks/:id/rotate-secret     rotate the HMAC secret
GET  /v1/webhooks/:id/deliveries        delivery history

Usage policy breaches

A breach of an agent’s usage policy is a finding of class policy. Its subtype names the rule (model_denied, model_not_allowed, call_budget, token_budget) and its value names the model, or the count against the cap. A denied or unlisted model is medium in report mode and high in block mode, because a call that ran under block mode means the block failed or the collector cannot block; a cap breach is low, or medium in block mode. A call the collector refused before it was made is recorded at info, as evidence the block held. At most 16 distinct models are recorded per rule in a session, and one more finding counts the rest.

Each of these findings, a refused call included, is sent once as a policy.violation webhook when it is first recorded. Subscribe to that event type: a policy finding is not repeated in findings.created. The count against a cap in the finding is the count when the cap was first crossed, updated to the final count when the session completes. A breach never stops recording. To stop an agent after one, pause it (below), for example from the receiver of that webhook.

A usage policy can also carry a mandate: closed-set rules over verbs, path classes, hosts, permission mode and delegation depth, set with the policy’s authority field. Mandate breaches are the same policy finding class, with subtypes verb_denied, verb_not_allowed, path_class_denied, host_not_allowed, bypass_not_allowed and delegation_depth_exceeded. These are recorded by the server only: no collector blocks a mandate rule before the action it judges, so severity does not change between report and block mode. At most 16 distinct values are recorded per rule in a session, and one more finding (authority_rule_truncated) counts the rest.

Pause an agent

Pausing an agent stops AER from accepting anything new from its collectors until an admin resumes it: new sessions, events, OTLP spans and attestation tokens are refused with 403 agent_paused. It stops ingestion; it does not stop the agent process, which keeps running. The one exception is a protected resource set to enforcement: block with on_unavailable: fail_closed in the collector, where a refused attestation token blocks that traffic once any cached token expires.

A session that was running at the pause can still be closed with /complete, and AER signs a record of what it received before the pause; if the collector was refused during the pause, that record carries a finding saying an admin paused ingestion, not that the collector failed. Collectors released today do not act on the pause itself. The MCP recorder stops sending for that process after the first refusal and leaves the session open. The hooks drop the session on the first refusal, keep the events queued on disk and try to open again with each later event, backing off to every five minutes; the queue is sent once the agent is resumed. The Node collector keeps trying each batch of a session opened before the pause and closes it when the process exits, which signs a record; a process started during the pause records nothing. The TypeScript and Python SDKs keep retrying and are refused each time, and their complete() fails while events are still buffered. A session nobody closes is ended by AER once the pause is 30 minutes old (checked every 15 minutes), without a record. Every refused attempt is counted on the agent (shown as at least N, since the count is a lower bound), and the first one sends the agent.ingest_refused webhook once per pause.

Pause and end running sessions (the stop) also closes every running session at once. Their events stay visible in the console, but no signed record is produced for them. Resuming lets collectors open sessions and send events again. AER does not replay a refused request; only a collector that queued its events (the hooks) sends them afterwards.

Pausing, stopping and resuming need the admin role. In the console that means a sign-in in the last 15 minutes; an admin API key works for automation, a key bound to one agent only for that agent, and a key from aer login never. Each lands in the audit chain as agent.paused, agent.stopped or agent.resumed. A pause is never held up by the audit log; a resume is refused unless its audit entry is written first.

POST /v1/agents/:id/pause               {reason?, end_running_sessions?}
POST /v1/agents/:id/stop                {reason?}
POST /v1/agents/:id/resume              {}

Usage caps

Every tenant has a monthly plan for events and sessions, checked in the same Durable Object the per-minute rate limits already use. At 100 percent of the plan, new sessions are refused with an upgrade link while running sessions keep recording.POST /v1/sessions/:id/complete is never refused, so a collector that keeps calling it finishes and signs the record it already has. Only at 125 percent, the hard ceiling, are events on a running session refused; if that session is later completed, its record carries a finding explaining the cut. A collector that never calls complete has that session closed automatically after 30 minutes with no signed record for it, the same shape as an abandoned session under any other rule. GET /v1/usage (any key, the read role) reports the plan, this period's usage against it and whether new sessions are currently allowed; while usage caps run in report-only mode, that is always true, whatever the counts say.

GET /v1/usage                           {plan, events, sessions, state, enforcement, new_sessions_allowed, upgrade}

Retention and visibility

Records are retained indefinitely by default. Setting bundle_retention_days (1 to 3650) auto-expires the bundle bytes after the window while keeping the searchable metadata; the transparency-log entry is permanent by design. Setting bundle_visibility to private gates every bundle-content endpoint behind tenant auth, and per-record share links re-open exactly one record to whoever holds the link: scoped, expiring (24 hours by default, 30 days at most), revocable and audited.

PATCH /v1/tenant/settings               {bundle_visibility?, bundle_retention_days?, anchoring_enabled?, notification_email?}
POST  /v1/aers/:id/share-links          {expires_in_seconds?} → single-record URL
GET   /v1/aers/:id/share-links          list, with active flags
DELETE /v1/aers/:id/share-links/:sid    revoke

Exports

Every record exports as canonical JSON (what is signed), W3C PROV-JSON for provenance tooling, a self-contained evidence pack ZIP (canonical bundle, provenance, verification script, public key, anchor evidence and a manifest with the SHA-256 of every file) and OTLP logs and traces for your observability stack. OTLP is interop, not the analysis path: the console's search and feeds work on the records first-hand.

GET /v1/aers/:id/bundle           canonical JSON (the signed artifact)
GET /v1/aers/:id/prov.json        W3C PROV-JSON
GET /v1/aers/:id/evidence.zip     self-contained offline audit pack
GET /v1/aers/:id/otlp-logs.json   OTLP logs (tenant auth)
GET /v1/aers/:id/otlp-traces.json OTLP traces (tenant auth)

The audit chain

Every privileged action on your tenant lands in a tamper-evident hash chain you can verify yourself: GET /v1/audit lists it and GET /v1/audit/verify recomputes the chain and cross-checks its anchor. A broken link is loud, not silent.

Keys and roles

API keys carry a role: read, write or admin. Reads need read (every key passes), session and agent writes need write, and anything that mints credentials or is destructive needs admin. Keys can be pinned to one agent and principal at mint, and a pinned key can never mint a wider one. Mint scoped keys from your root admin credential for agents and teammates; revoke any key except the one you are using. Mint one under Settings in the console, choosing the role there. A key of any role goes in the same AER_API_KEY: the role travels with the key, not with the variable, so there is no separate name for a read key.