Skip to content

[bug] The tool listing reports write-gated tools as allowed=true, reason=ok — agents burn a turn discovering the consent gate #351

Description

@serge-ivo

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.

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

    bugSomething isn't workingconnectorsConnector + tool framework

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions