Problem
Every MCP write tool — including the two that decide whether ORB auto-merges at all — leaves no audit
record, while the identical HTTP write is audited.
grep -n "recordAuditEvent" src/mcp/server.ts returns zero hits.
Compare: PUT /v1/repos/:owner/:repo/settings (src/api/routes.ts:2946-2953) writes the row and then
records eventType: "repo.settings_updated" with the actor, targetKey: fullName, and the changed
fields. The MCP counterparts call upsertRepositorySettings and return; upsertRepositorySettings
itself (src/db/repositories.ts:898) records nothing.
Unaudited MCP write tools:
| tool |
site |
effect |
loopover_set_agent_paused |
src/mcp/server.ts:4871-4888 |
the kill switch |
loopover_set_action_autonomy |
:4894-4906 |
the autonomy dial (merge: auto) |
loopover_admin_write_config |
:4017-4059 |
rewrites the instance's private .loopover.yml fleet-wide |
loopover_admin_trigger_redeploy |
:4080-4106 |
redeploys the instance |
The only MCP-side trace is recordProductUsageEvent (src/mcp/server.ts:1902, :1920), which is not a
substitute: it writes to product_usage_events, not audit_events — so loopover_get_agent_audit_feed
and listAgentAuditEvents never see it; it is .catch(() => undefined) best-effort; and it carries only
toolName, with repoFullName/targetKey unset. It cannot tell you which repo was un-paused or what
level merge was set to.
Trigger
A session holding plain GitHub write permission (REPO_WRITE_PERMISSIONS = {admin, maintain, write},
src/mcp/server.ts:592), or any holder of LOOPOVER_MCP_TOKEN for a repo in
MCP_ACTUATION_REPO_ALLOWLIST, calls loopover_set_action_autonomy {action:"merge", level:"auto"} then
loopover_set_agent_paused {paused:false}. The gate then auto-merges with no human, and the audit feed
shows only the resulting agent.* executions — never the config change that authorised them.
Impact
After a bad auto-merge there is no way to answer "who turned this on, and when" from audit_events. For
a system whose entire product claim is an accountability layer, the two settings that govern autonomy are
mutable through a surface with no forensic record. loopover_admin_write_config is the sharpest case: it
rewrites gate thresholds, autonomy, and packs for the whole fleet and leaves only a .bak-<timestamp>
file on disk.
Requirements
- Add
recordAuditEvent to all four handlers, mirroring the HTTP route's shape
(repo.settings_updated, plus new config.private_write and instance.redeploy_triggered types),
with actor: this.identity.actor and before/after values.
- Better: record the audit event inside
upsertRepositorySettings so no future caller can skip it —
the HTTP/MCP split is exactly how this gap arose.
- Add an invariant test asserting every mutating MCP tool produces an
audit_events row.
Test Coverage Requirements
99%+ patch coverage, branch-counted; one test per write tool asserting the audit row and its actor.
Links & Resources
maintainer-only — accountability surface.
Problem
Every MCP write tool — including the two that decide whether ORB auto-merges at all — leaves no audit
record, while the identical HTTP write is audited.
grep -n "recordAuditEvent" src/mcp/server.tsreturns zero hits.Compare:
PUT /v1/repos/:owner/:repo/settings(src/api/routes.ts:2946-2953) writes the row and thenrecords
eventType: "repo.settings_updated"with the actor,targetKey: fullName, and the changedfields. The MCP counterparts call
upsertRepositorySettingsand return;upsertRepositorySettingsitself (
src/db/repositories.ts:898) records nothing.Unaudited MCP write tools:
loopover_set_agent_pausedsrc/mcp/server.ts:4871-4888loopover_set_action_autonomy:4894-4906merge: auto)loopover_admin_write_config:4017-4059.loopover.ymlfleet-wideloopover_admin_trigger_redeploy:4080-4106The only MCP-side trace is
recordProductUsageEvent(src/mcp/server.ts:1902,:1920), which is not asubstitute: it writes to
product_usage_events, notaudit_events— soloopover_get_agent_audit_feedand
listAgentAuditEventsnever see it; it is.catch(() => undefined)best-effort; and it carries onlytoolName, withrepoFullName/targetKeyunset. It cannot tell you which repo was un-paused or whatlevel
mergewas set to.Trigger
A session holding plain GitHub
writepermission (REPO_WRITE_PERMISSIONS = {admin, maintain, write},src/mcp/server.ts:592), or any holder ofLOOPOVER_MCP_TOKENfor a repo inMCP_ACTUATION_REPO_ALLOWLIST, callsloopover_set_action_autonomy {action:"merge", level:"auto"}thenloopover_set_agent_paused {paused:false}. The gate then auto-merges with no human, and the audit feedshows only the resulting
agent.*executions — never the config change that authorised them.Impact
After a bad auto-merge there is no way to answer "who turned this on, and when" from
audit_events. Fora system whose entire product claim is an accountability layer, the two settings that govern autonomy are
mutable through a surface with no forensic record.
loopover_admin_write_configis the sharpest case: itrewrites gate thresholds, autonomy, and packs for the whole fleet and leaves only a
.bak-<timestamp>file on disk.
Requirements
recordAuditEventto all four handlers, mirroring the HTTP route's shape(
repo.settings_updated, plus newconfig.private_writeandinstance.redeploy_triggeredtypes),with
actor: this.identity.actorand before/after values.upsertRepositorySettingsso no future caller can skip it —the HTTP/MCP split is exactly how this gap arose.
audit_eventsrow.Test Coverage Requirements
99%+ patch coverage, branch-counted; one test per write tool asserting the audit row and its actor.
Links & Resources
src/mcp/server.ts~592, ~1902, ~1920, ~4017-4059, ~4080-4106, ~4871-4888, ~4894-4906;src/api/routes.ts~2946-2953;src/db/repositories.ts~898maintainer-only — accountability surface.