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

Closed

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

  1. added
    bugSomething isn't working
    connectorsConnector + tool framework
    on Aug 7, 2026
  2. serge-ivo commented on Aug 7, 2026

    @serge-ivo
    ContributorAuthor

    Landed in 1c1fb95 (+ f347fee docs, 044f898 follow-through).

    Claims checked, all held. resolveToolPolicy had no consent knowledge at all, so every write-scoped tool on an ungranted connector read allowed:true, reason:"ok". The terminal vs tmux trap is real too — the entry already carries the tool's registry connector, and consent is now matched on exactly that, so tmux write on a tmux-surface agent visibly does not satisfy terminal_*.

    Shape: writeConsent: "n/a" | "granted" | "required" | "per_call", additive. allowed, disabled and reason are untouched, and the gates stay separate in runRegistryTool exactly as documented.

    The per-call gates. A listing that reads only the declared scope cannot see them, and there are two:

    Console: an amber chip on the tool row — "needs terminal write access" / "writes need http access" / "granted per server + tool". Amber, not red: an ungranted write tool is a switch the owner has not flipped, not a fault. Following that through exposed a second dead end and fixed it in 044f898: the write-access checkboxes were built from write-scoped tools only, so http had no checkbox anywhere — the chip would have named a grant with no control to give it.

    MCP: list_instance_tools' description documents the new field and what each value means for a call about to be made, so the reading agent learns the gate before spending a turn on it. Injecting it into the chat prompt was deliberately left out of scope — that is a change to agent-think's context builder, not to this listing.

    Not addressed here, as the ticket says it is a separate decision: retiring the superseded tmux connector in favour of terminal, or aliasing their consent.

    CI green.

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