Executable deployment-actions
AuditTrail is a control plane, not an execution host. It proposes, versions, approves, and governs agent operations — and for the subset of actions it can safely carry out, it performs them. For everything else it is honest: the action is recorded, approved, and handed off to your runner.
This page describes which deployment actions AuditTrail performs itself, how, and the guardrails around each.
Execution runs on YOUR runners.
AuditTrail never runs your agents in our cloud. The only things the control plane can do on its own are: rewrite / throttle / disable traffic that flows through its gateway proxy, and send a kill frame to your own connected runner daemon. Anything that touches your infrastructure (scaling, redeploys) stays a recorded approval plus a hand-off.
The 3-tier safety model is unchanged
Executable actions do not weaken the approval gate:
| Tier | Actions | Approval |
|---|---|---|
| 1 | alert, tag_trace, create_incident | autonomous-safe (auto-executed on propose in autonomous mode) |
| 2 | throttle, switch_model, disable_flag | supervised approval always required |
| 3 | kill_run, rollback_deploy, scale_down | explicit approval always required |
The executor only ever acts on an action that already reached
status = "approved". It never approves anything. Autonomous mode
still auto-executes Tier 1 only.
The performance matrix
| Action | What AuditTrail does |
|---|---|
kill_run | Sends a kill frame to your connected runner daemon for the target dispatch. The dispatch must be owned by you; an unknown or cross-tenant dispatch, or no connected daemon, records a clear failure (never a silent success). |
throttle | Writes an active runtime control that caps the per-minute rate for gateway-routed traffic. (gateway-routed only) |
switch_model | Writes an active runtime control that rewrites the upstream model id in the gateway forward path. (gateway-routed only) |
disable_flag | Writes an active runtime control that short-circuits gateway-routed traffic for the target with a clear 4xx. (gateway-routed only) |
scale_down | External handoff. Approve here, perform it on your runner, record via mark-executed. |
rollback_deploy | External handoff. Approve here, perform it on your runner, record via mark-executed. |
| Tier 1 | Recorded in the audit trail (and fanned out via your webhooks). |
GET /api/v1/deployments/actions/capabilities returns this matrix so the
/deployments UI can badge each action AuditTrail performs vs
External handoff, with the gateway-only caveat where it applies.
How the executor runs
The executor is a single pass on the existing online-eval worker loop — no new always-on poller (this instance runs SQLite, and a tight poll would burn the write budget). Each pass:
- Selects every action with
status = "approved"andexecuted_at IS NULL. - Performs the ones it can, records the result in
outcome, stampsexecuted_at, and commits per action.
Idempotency survives a restart
Idempotency is DB-persisted, not in-memory. The tag-deploy cron
restarts the API container regularly; an in-process "already done" set
would be wiped and the action would run twice. Instead, a row is eligible
for execution only while status = "approved" AND executed_at IS NULL.
The first pass stamps executed_at and commits, so a restart mid-loop can
never re-perform an action.
A successful perform also flips status to executed. A refusal
or failure leaves status = "approved" but stamps executed_at and
sets outcome.execution_failed = true, so the operator sees an honest
result and the executor doesn't retry it forever.
switch_model refuses loudly
switch_model is gateway-routed only. A swap is enforceable only when
the action carries a replacement model (outcome.to_model) — the gateway
then rewrites body["model"] before forwarding upstream.
If no to_model is supplied, the swap is not gateway-routable, so the
executor refuses loudly: it writes no runtime control, records a
failure with a human-readable reason, and never marks the action
successful. Re-propose with outcome.to_model set, or perform the swap on
your own runner and record it with mark-executed.
kill_run targets only your own dispatch
kill_run resolves its target from outcome.dispatch_id (or
target_ref) and verifies the dispatch is owned by the action's user
before sending the kill frame. A dispatch that doesn't exist for you — or
belongs to another tenant — yields a recorded failure (404-style, never
leaks existence). The daemon tracks live subprocesses by dispatch id and,
on the kill frame, sends terminate(), waits a short grace period, then
escalates to kill().
Runtime controls
A performed Tier-2 action writes a runtime_controls row scoped to your
user. The gateway consults your active controls on every request:
disable_flagmatching the model → the call is short-circuited (403).throttlematching the model whose per-minute ceiling is exceeded → 429.switch_modelmatching the model → the upstream model is rewritten.
A control owned by another user can never affect your traffic. The throttle counter is in-process (best-effort, single-replica); the control itself lives in the DB row, so a restart just resets the rate window — the safe direction for an availability control.
REST surface
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/deployments/actions/capabilities | Per action_type: performs vs handoff + gateway-only caveat |
GET | /api/v1/deployments/actions | List your actions |
POST | /api/v1/deployments/actions | Propose |
POST | /api/v1/deployments/actions/{id}/approve | Approve |
POST | /api/v1/deployments/actions/{id}/reject | Reject |
POST | /api/v1/deployments/actions/{id}/mark-executed | Record an external-handoff result |
WebSocket events
The executor broadcasts to the action owner's live feed:
deployment.action.executed— a performed action.deployment.action.failed— a refused / failed / handoff action.