Skip to content

A routine's run is governed as though somebody were watching it #484

Description

@Hotragn

What is missing

Routines gave this deployment its first runs that nobody is watching. The boundary cannot tell them
apart from a person at the keyboard, and neither can the trail.

PolicyContext (server/src/computer/policy.ts:44) binds tool, bot, page, actor, element,
key, intent, file, mcp and command. There is no field for whether a person is present.

A routine's turn is built through the same governed path as a chat turn. buildAgentFor
(server/src/index.ts:631-655) resolves the owner and calls loadToolsForActor(actor.id) — the same
loader mountCopilotRuntime uses — so grants, policy and audit are all exactly where they are for a
person's own run. That is the right design. The consequence is that at 3am, with nobody watching:

  • every policy decision sees actor.id = the routine's owner, and nothing else that differs;
  • every audit row is attributed to that owner and reads as though they were driving.

So a deployment cannot write the rule it most obviously wants once routines exist:

deny: intent == "run_command" && unattended

There is nothing to put on the right-hand side.

Why this is the same argument the context has already accepted twice

This is not a new principle, it is an existing one arriving at a new surface. Two fields exist for
exactly this reason, and both docstrings say so.

key, added because a deny rule on clicking was bypassed by pressing Enter:

nothing in the context could tell one keypress from another

intent, added because tool.name describes mechanism and an operator thinks in effects.

Same shape here: actor.id describes whose authority the run carries, which is genuinely the
owner's. It does not describe whether anybody is there, and those are different facts. An operator
who is happy for Finance Bot to file a Jira ticket while they watch may not be happy for it to do so
unattended at 3am, and today that distinction cannot be written down.

The trail has the same gap, from the other side

routine_runs (server/src/db/schema/coworker.ts:134) records that a firing happened, when, and
whether it succeeded. audit_events records what was done. Nothing joins them. An investigator
asking "this routine ran overnight — what did it touch" correlates by timestamp against rows
attributed to a person who was asleep.

routines.dispatch_refused is the only routines event in auditEventTypes, and it is about a worker
presenting a bad secret rather than about a run.

I want to be careful not to overstate this half: the actions are on the trail, through the gateway,
with the right Bot and the right grants. What is absent is provenance — that it was a schedule, and
which routine.

What I would build, if you want it

The boundary half is the part I would not invent unasked, because it is a context field and those are
yours to name. Roughly:

  • A boolean on PolicyContextunattended, or attended if you would rather the safe reading be
    the true one. Bound everywhere, with a neutral value on every path, the way mcp is: the whole
    point of Keep a rule about one action surface from refusing another #115 is that a rule naming a field must not throw on an action that has no opinion about
    it. False on a chat turn, true on a routine's.
  • Threaded from the one place that knows. run-turn.ts is the only caller that knows nobody is
    there; a chat turn is attended by construction. That is a parameter through buildAgentFor to the
    tool loader and the gateway, not a new source of truth.
  • The same fact on the audit row, so a rule and a row agree about what happened.

Open questions I would rather you answered than guessed:

  1. unattended or attended? I lean unattended, so an omitted field reads false and an old
    rule keeps meaning what it meant.
  2. Is a hop attended? A Bot handing work to another Bot (agents/handoff*) has a person behind
    it somewhere, but not watching that run. I would call it unattended and let a rule say so, but it
    is genuinely arguable and it changes what the field means.
  3. Does the routine id belong on the audit row too? That closes the join above. It is a wider
    change and might be its own issue.

Happy to build it once 1 and 2 are settled — or to leave it if you would rather routines stay
governed exactly as a person's run is, which is a coherent position and would be worth saying in the
docstring so the next person does not re-file this.

Found by reading routines/ and work/ after they landed, not from a deployment — I have no live
routine to point at, and every claim above is a file and a line rather than an observation.

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