What happened
A fresh tmux Operator instance (cda75e28-cace-4958-ac3e-6a7528e6b719) was asked to create a tmux
session. It planned the work, called the tool, and was refused:
❌ terminal_new_target — Writing via the terminal connector isn't permitted for this agent.
Correct refusal — a new subscription has no write consent (#90, migration 0051). The agent then
reported it accurately, which is the right behaviour.
The problem is what GET /v1/instances/:id/tools had told it a moment earlier:
terminal_run_command connector=terminal scope=write allowed=true reason=ok
terminal_send_keys connector=terminal scope=write allowed=true reason=ok
terminal_new_target connector=terminal scope=write allowed=true reason=ok
terminal_kill_target connector=terminal scope=write allowed=true reason=ok
A field named allowed reading true, with reason: "ok", on four tools that cannot run.
This is deliberate, and the rationale is sound
workers/api/src/lib/instance-tool-policy.ts:17-19:
Write-consent (#90) is a THIRD, separate gate enforced in runRegistryTool. It is deliberately
not merged here: consent answers "may this act on an external system as me", while this answers
"is this tool part of this agent at all". A tool can be allowed here and still be [refused].
Keeping the gates separate is right — merging them would make "is this tool part of this agent"
unanswerable while consent is off, and the fail-closed enforcement in runRegistryTool is where
it belongs. The ask is not to merge the gates. It is that the listing does not report the
second one, so its consumer cannot distinguish "you have this tool" from "you have this tool and
it will work".
The cost is small but repeated: every agent with an ungranted write connector burns a turn
discovering it, and the discovery is a failed side-effecting call rather than a read.
Fix
Add a separate, additive field to each tool in the listing — e.g.
writeConsent: "granted" | "required" | "n/a" — computed from hasConsent()
(lib/connector-consent.ts:18). Leave allowed and reason exactly as they are, so the existing
meaning and the documented separation both survive.
Then the runtime prompt can say "you have this tool but write consent is not granted; ask the
owner to enable it" before spending a call, and the console can render the state without a
second request.
Related trap found while confirming this
The instance's surface is tmux (capabilities.surfaces: ["tmux"]) but its tools come from the
terminal connector, so the consent that unblocks it must be granted against terminal.
Granting tmux write would look right and do nothing.
That is because there are two overlapping connectors in the registry:
| connector |
tools |
status |
terminal |
terminal_list_targets, _capture, _run_command, _send_keys, _new_target, _kill_target |
generic — tmux + kitty + iTerm2 (connectors/terminal.ts) |
tmux |
tmux_list_sessions, tmux_capture_pane, tmux_run_command, tmux_send_keys, tmux_new_session, tmux_kill_session |
tmux-only, superseded (connectors/tmux.ts) |
Same capability, two vocabularies, two consent keys. The operator agents declare only the
terminal_* set, so tmux_* is dead weight that can still be declared by mistake — and a
mismatch between the surface name and the consent key is exactly the kind of thing that gets
diagnosed as "consent is broken". Worth either retiring tmux or documenting it as an alias with
shared consent; separate decision from the field above, but they surfaced together.
Verification
- A tool that will be refused by write consent is distinguishable from one that will run, in a
single listing response.
allowed / reason keep their current meanings; no existing consumer changes behaviour.
- Granting write consent on the connector the tools actually belong to is discoverable from the
listing without reading the source.
What happened
A fresh tmux Operator instance (
cda75e28-cace-4958-ac3e-6a7528e6b719) was asked to create a tmuxsession. It planned the work, called the tool, and was refused:
Correct refusal — a new subscription has no write consent (#90, migration 0051). The agent then
reported it accurately, which is the right behaviour.
The problem is what
GET /v1/instances/:id/toolshad told it a moment earlier:A field named
allowedreadingtrue, withreason: "ok", on four tools that cannot run.This is deliberate, and the rationale is sound
workers/api/src/lib/instance-tool-policy.ts:17-19:Keeping the gates separate is right — merging them would make "is this tool part of this agent"
unanswerable while consent is off, and the fail-closed enforcement in
runRegistryToolis whereit belongs. The ask is not to merge the gates. It is that the listing does not report the
second one, so its consumer cannot distinguish "you have this tool" from "you have this tool and
it will work".
The cost is small but repeated: every agent with an ungranted write connector burns a turn
discovering it, and the discovery is a failed side-effecting call rather than a read.
Fix
Add a separate, additive field to each tool in the listing — e.g.
writeConsent: "granted" | "required" | "n/a"— computed fromhasConsent()(
lib/connector-consent.ts:18). Leaveallowedandreasonexactly as they are, so the existingmeaning and the documented separation both survive.
Then the runtime prompt can say "you have this tool but write consent is not granted; ask the
owner to enable it" before spending a call, and the console can render the state without a
second request.
Related trap found while confirming this
The instance's surface is
tmux(capabilities.surfaces: ["tmux"]) but its tools come from theterminalconnector, so the consent that unblocks it must be granted againstterminal.Granting
tmuxwrite would look right and do nothing.That is because there are two overlapping connectors in the registry:
terminalterminal_list_targets,_capture,_run_command,_send_keys,_new_target,_kill_targetconnectors/terminal.ts)tmuxtmux_list_sessions,tmux_capture_pane,tmux_run_command,tmux_send_keys,tmux_new_session,tmux_kill_sessionconnectors/tmux.ts)Same capability, two vocabularies, two consent keys. The operator agents declare only the
terminal_*set, sotmux_*is dead weight that can still be declared by mistake — and amismatch between the surface name and the consent key is exactly the kind of thing that gets
diagnosed as "consent is broken". Worth either retiring
tmuxor documenting it as an alias withshared consent; separate decision from the field above, but they surfaced together.
Verification
single listing response.
allowed/reasonkeep their current meanings; no existing consumer changes behaviour.listing without reading the source.