Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b26ef84
Record every tool call in an audit log
midego1 Aug 7, 2026
e98655f
Page the Activity list instead of rendering it whole
midego1 Aug 17, 2026
accee74
Filter and search the Activity log
midego1 Aug 17, 2026
5ce27b6
Integration dropdown on the Activity filters
midego1 Aug 17, 2026
f2bc75e
Merge remote-tracking branch 'origin/main' into claude/tool-call-audi…
midego1 Aug 17, 2026
148ab8b
Merge remote-tracking branch 'origin/main' into claude/tool-call-audi…
midego1 Aug 18, 2026
2c038cc
Merge remote-tracking branch 'origin/main' into claude/tool-call-audi…
midego1 Aug 25, 2026
3c7e074
Merge remote-tracking branch 'origin/main' into claude/tool-call-audi…
midego1 Aug 31, 2026
0cdbcc7
Drop the audit write's timeout — it never worked, and now it deadlocked
midego1 Aug 31, 2026
3afae3b
Purge the audit log with the org, and regenerate packages/app's route…
midego1 Aug 31, 2026
1350345
Floor a timed-out build slot's reported wait at its own timeout
midego1 Sep 1, 2026
0c14df3
Merge remote-tracking branch 'origin/main' into claude/tool-call-audi…
midego1 Sep 1, 2026
01778d1
Format the regenerated cloud migration artifacts
midego1 Sep 1, 2026
2875c0c
Merge origin/main into claude/tool-call-audit-log
midego1 Sep 6, 2026
0aa5b8c
Regenerate the tool_call_log migration as 0018
midego1 Sep 6, 2026
7fb9a8e
Merge origin/main into claude/tool-call-audit-log
midego1 Sep 17, 2026
f050ac2
Regenerate the tool_call_log migration as 0021
midego1 Sep 17, 2026
ddcde3f
Merge origin/main into claude/tool-call-audit-log
midego1 Sep 19, 2026
6b9fafa
Record which client made each tool call, and who
midego1 Sep 19, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .changeset/tool-call-log.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
"executor": minor
---

**New: an audit trail of every tool call — which integration an agent used, when, and how it ended**

Executor kept no record of tool usage. A run that called GitHub or Search Console left one HTTP line (`POST /mcp 200`) and nothing about which integration, which tool, or what came back; the analytics catalog is anonymous by construction and deliberately drops exactly those fields. That made two questions unanswerable after the fact: what did this agent touch, and what did my policies actually stop.

Every call through `execute` now writes a row: the address as called, its integration/connection/tool, the outcome, the policy that governed it, and how long it took. The rows that matter most are the ones with no other trace — a call a `block` policy stopped, and an approval someone declined, both of which end before any request is made. Read them from `executor.toolCalls.list()`, from `GET /api/tool-calls` (filter by integration, connection, client, outcome or time), or on the new **Activity** page in the console.

Each row also says **who** ran the call and **which client** it came in on: the member (kept even on org-owned connections, whose rows are otherwise shared org-wide), and the credential — which API key, which OAuth-connected MCP client by its registered name ("Claude Code", "Cursor"), the browser console, or a CLI login. Only the host sees the credential, so it names the client through a new optional `ExecutorConfig.caller`; self-host resolves it from whichever credential authenticated the request. `GET /api/tool-calls/clients` lists the clients seen, and the Activity page filters by them. A client names itself at OAuth registration, so its label is treated as untrusted text: control characters are stripped and it is bounded before it is stored.

Arguments, results, and any text that came from outside are never stored: an argument can be a credential, and an upstream error message routinely echoes the request back. A failed call keeps its upstream `code`, never its message; a call keeps its top-level argument _names_, and only those that look like parameters rather than payloads. Writing a row can never fail a call: an insert failure is logged and swallowed. The write is awaited (a row exists before the call returns) and deliberately carries no timeout of its own — a finalizer runs uninterruptible, so a timeout there is decorative exactly when the database hangs; bounding a stalled driver is the driver's job.

Retention is left to the host: `executor.toolCalls.prune({ before })` removes old rows, and nothing schedules it for you — an audit log that quietly deletes itself on a default nobody chose is worse than one that grows.
26 changes: 26 additions & 0 deletions apps/cloud/drizzle/0021_tool_call_log.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
CREATE TABLE "tool_call_log" (
"id" varchar(255) NOT NULL,
"address" text NOT NULL,
"integration" varchar(255),
"connection" varchar(255),
"tool" text,
"outcome" varchar(255) NOT NULL,
"error_code" text,
"error_message" text,
"policy_action" text,
"policy_pattern" text,
"duration_ms" bigint NOT NULL,
"arg_keys" json,
"actor" varchar(255),
"actor_label" text,
"client_kind" varchar(255),
"client_id" varchar(255),
"client_name" text,
"created_at" timestamp NOT NULL,
"row_id" varchar(255) PRIMARY KEY NOT NULL,
"tenant" varchar(255) NOT NULL,
"owner" varchar(255) NOT NULL,
"subject" varchar(255) NOT NULL
);
--> statement-breakpoint
CREATE UNIQUE INDEX "tool_call_log_uidx" ON "tool_call_log" USING btree ("tenant","owner","subject","id");
Loading
Loading