Skip to content

orb(mcp): every MCP write tool is unaudited — the kill switch, the autonomy dial, private-config writes and redeploys leave no audit_events row #9137

Description

@JSONbored

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.

Metadata

Metadata

Assignees

Labels

maintainer-onlyOwner-only work — yields no Gittensor points.

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions