Dock
Sign in & remix
REMIX PREVIEWEngineering· MAY 28

Auditing agent behavior: a walkthrough

The audit log is the artifact you reach for when an agent does something unexpected, and most teams have a log but no runbook for it. Here is how Mike's team actually uses Dock's dual-keyed audit log day to day: three reading patterns, a drift-detection setup, and a walkthrough of a real-shaped incident from log query to runbook decision.

By mike· 5 min read· from trydock.ai

The audit log is the artifact you reach for when an agent does something unexpected. A row got rewritten, a workspace got touched outside its scope, a teammate asks "was this me or my agent?" Every shop has a log. Very few have a runbook for it. The log sits untouched until the incident, at which point you're learning your own schema under pressure.

This is the runbook side of agent audit and compliance. That essay covers the dual-keyed model: every privileged action carries agent_principal_id and owner_user_id, written through one gate, no side doors. This is what you do with it on a Tuesday when something looks off. Three reading patterns, a drift cron that catches scope creep, a walkthrough from first query to runbook decision.

The schema we're querying, lifted straight from the audit gate:

{ ts, agent_principal_id, owner_user_id, target_id, action, scopes, consent_token, request_id }

Three reading patterns

Who did this?

You're staring at a row that changed and you don't know why. Start with the target. Every action on a resource is in the log, in order.

SELECT ts, agent_principal_id, owner_user_id, action
FROM audit_events
WHERE target_id = 'row_8e2c1f'
ORDER BY ts DESC
LIMIT 50;

The resource's full action history, dual key on every row:

2026-05-26T14:02:11Z  agt_argus_qa     usr_mike    update_row
2026-05-26T11:47:03Z  agt_scout_ops    usr_mike    update_row
2026-05-26T09:15:22Z  NULL             usr_mike    create_row

Populated agent_principal_id means an agent did it. NULL means the human did it directly. The owner_user_id column tells you which human signs for it either way. That last fact does most of the work in a real investigation.

What did this agent touch?

The shape that catches scope creep. An agent meant for one job starts brushing up against work it shouldn't. Filter by principal over a window.

SELECT date_trunc('hour', ts) AS hour,
       action,
       COUNT(*) AS n,
       COUNT(DISTINCT target_id) AS distinct_targets
FROM audit_events
WHERE agent_principal_id = 'agt_scout_ops'
  AND ts > now() - interval '24 hours'
GROUP BY 1, 2
ORDER BY 1 DESC;

You're looking for surprise. New action verbs, new target classes, traffic at hours the agent shouldn't be awake. An agent whose job is "ingest CRM rows" should not be issuing update_doc on workspaces it never wrote to before.

Did the human or the AI take action X?

The "was this me or my agent" query. Same target filter, constrain the principal.

SELECT ts, owner_user_id, action
FROM audit_events
WHERE target_id  = 'ws_launch_brief'
  AND agent_principal_id IS NULL
ORDER BY ts DESC;

NULL principal is the proof: the action came from a human session, no agent in the chain. Drop IS NULL and you get the inverse, every agent-driven action on that workspace.

Drift detection: a cron, not a dashboard

Dashboards are for incidents in progress. Drift is the slower thing, an agent whose action mix changes over weeks. Treat it like an SLO breach: daily cron, query the audit table, page when the shape moves.

WITH weekly AS (
  SELECT agent_principal_id,
         action,
         date_trunc('week', ts) AS wk,
         COUNT(*) AS n
  FROM audit_events
  WHERE ts > now() - interval '14 days'
  GROUP BY 1, 2, 3
),
shape AS (
  SELECT agent_principal_id, wk, action,
         n::float / SUM(n) OVER (PARTITION BY agent_principal_id, wk) AS pct
  FROM weekly
)
SELECT this.agent_principal_id, this.action,
       last.pct AS last_week, this.pct AS this_week,
       (this.pct - last.pct) AS delta
FROM shape this
JOIN shape last
  ON this.agent_principal_id = last.agent_principal_id
 AND this.action = last.action
 AND this.wk = date_trunc('week', now())
 AND last.wk = date_trunc('week', now() - interval '7 days')
WHERE ABS(this.pct - last.pct) > 0.20;

Anything with a >20% shift lands in a workspace called dock/agent-drift. Most weeks it's empty. When it isn't, an owner reads the row and decides: expected (new task, shape should shift) or not (open an investigation).

Incident walkthrough: an agent off-leash

Tuesday, 14:15. A teammate flags recent edits in a workspace nobody scheduled work in. Three queries decide what we do.

Query one, target history. Pattern A on target_id = 'ws_partner_review'. Five rows in the last hour, all update_row, all from agt_ingest_crm, owner usr_govind. The agent isn't supposed to write here. It has discovery via signed-agent inheritance because the owner is a member, but its task scope is the CRM workspace.

Query two, agent behavior. Pattern B on agt_ingest_crm, last 24 hours. Normal CRM traffic plus a tail of writes to ws_partner_review starting at 13:48. New target class. Drift would have caught this on tomorrow's run; we caught it today because a human noticed.

Query three, scope vs. action. Pull scopes and consent_token for the off-target writes. Scopes valid for the surface, no consent token (not dangerous ops, see dangerous-ops-contract). Nothing strictly disallowed. The agent is acting inside its permissions but outside its job. That distinction is the whole reason this runbook exists.

Runbook says: suspend, investigate, decide. Following the agent identity lifecycle, we suspend agt_ingest_crm (key revoked, writes 403, log keeps flowing). The owner reads the conversation that produced the off-target writes, finds a prompt that ambiguously pointed the agent at "the review workspace," rewrites the task. Reactivate, narrow scope, document the near-miss.

Flag to suspension: about eight minutes. The audit log did the heavy lifting, the runbook did the rest.

Where this fits

The audit log is the after-the-fact tool. Prevention lives upstream in consent gates for dangerous operations and the scope issued at provisioning. Identity lifecycle (suspend / rotate / revoke) is what you reach for during. The substrate, agents as principals with owning humans, is the parent essay. Architectural model under /docs/mcp/overview.

Google's SRE book has been saying for years that the runbook turns an incident into a procedure (Managing Incidents). NIST says the same in the compliance register (PR.PT-1, audit-log review). The log exists so you can answer questions under pressure. Write the queries before you need them.

Remix this into Dock

Make this yours. Edit, extend, run agents on it.

Sign in (free, 20 workspaces) — Dock mints a copy of this in your own workspace. The original stays untouched.

No Dock account? Sign-in is signup. Magic-link in 30 seconds.