Skip to content

The audit trail cannot tell an unattended run from a person's own action #385

Description

@zopeVaibhav

What the trail says today

audit_events.actor_user_id records on whose authority an action was taken. That is the right column and it is filled correctly, but two of the three ways a run starts borrow that authority from somebody who was not there.

A routine asserts its owner: server/src/routines/runner.ts:81 takes ownerUserId off the claimed routine and every tool call that turn makes is filed under that person. A hop between Bots asserts the person who began the conversation: server/src/agents/handoff-runner.ts:301 writes actorUserId: work.actorId, and the desk that offers or refuses a hop at server/src/agents/handoff.ts:108 and :387 does the same.

So an mcp.call_succeeded row written at 03:00 by a schedule is indistinguishable, on the row itself, from the same person invoking the same tool at their desk. The row that records a hop beginning says the same person asked for it.

Why this is the row that matters

docs/routines.md already names the case: an interactive run has somebody watching who will notice a wrong tool call, and an unattended one does not. The unattended run is precisely the one nobody is there to catch, and it is the one the trail cannot point at.

Reproduction

  1. Create a routine that calls a granted tool.
  2. Let its schedule fire.
  3. Open Admin > Audit and filter to the routine owner.
  4. The routine's mcp.call_* rows sit beside that person's own, with nothing on either row to separate them.

Telling them apart means exporting rows and correlating created_at against routine_runs by hand. For a hop there is nothing to correlate against at all: no table records which Bot handed work to which at what moment, so the agent.handoff_* rows are the only evidence and every one of them names the person.

The rows with nobody behind them

A third case is worth naming while the column is being designed. Some rows have no person at any point. server/src/index.ts:375 writes computer.policy_loaded at start-up, and server/src/app.ts:837 writes routines.dispatch_refused when a worker's bearer secret does not check out, before the request body is parsed. Both carry a null actor, which today is ambiguous: it reads the same as a person whose id was not recorded.

Any column added here has to have somewhere truthful to put these, or it will assert a person where there was none.

Where the answer has to travel

The thing that knows a routine began a run is the process that claimed the routine. The row is often written elsewhere: a hop is one run on one pod handing to another run on another, and the desk, the escalation tool and the stall guard each hold only the signed RunAssertion minted at server/src/agents/callback-token.ts:116. That assertion already carries depth for exactly this reason. Whatever answers "what started this" has to travel the same way, or the writers that hold only the assertion cannot say it.

Severity

Not data loss and not an escalation: no action becomes permitted that was not permitted before, and no existing row is wrong. It is an accountability gap. The trail is complete and still cannot answer the question an auditor opens it to ask, which for any deployment running routines is the question.

docs/routines.md records this as the first of four follow-ups deliberately left out of the routines pass.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions