You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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 PolicyContext — unattended, 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:
unattended or attended? I lean unattended, so an omitted field reads false and an old
rule keeps meaning what it meant.
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.
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.
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) bindstool,bot,page,actor,element,key,intent,file,mcpandcommand. 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 callsloadToolsForActor(actor.id)— the sameloader
mountCopilotRuntimeuses — so grants, policy and audit are all exactly where they are for aperson's own run. That is the right design. The consequence is that at 3am, with nobody watching:
actor.id= the routine's owner, and nothing else that differs;So a deployment cannot write the rule it most obviously wants once routines exist:
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:intent, added becausetool.namedescribes mechanism and an operator thinks in effects.Same shape here:
actor.iddescribes whose authority the run carries, which is genuinely theowner'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, andwhether it succeeded.
audit_eventsrecords what was done. Nothing joins them. An investigatorasking "this routine ran overnight — what did it touch" correlates by timestamp against rows
attributed to a person who was asleep.
routines.dispatch_refusedis the only routines event inauditEventTypes, and it is about a workerpresenting 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:
PolicyContext—unattended, orattendedif you would rather the safe reading bethe true one. Bound everywhere, with a neutral value on every path, the way
mcpis: the wholepoint 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.
run-turn.tsis the only caller that knows nobody isthere; a chat turn is attended by construction. That is a parameter through
buildAgentForto thetool loader and the gateway, not a new source of truth.
Open questions I would rather you answered than guessed:
unattendedorattended? I leanunattended, so an omitted field reads false and an oldrule keeps meaning what it meant.
agents/handoff*) has a person behindit 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.
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/andwork/after they landed, not from a deployment — I have no liveroutine to point at, and every claim above is a file and a line rather than an observation.