Skip to content

[design] A "tmux Operator" can drive iTerm2 — a creator cannot declare which backend an agent may touch, or that it owns exactly one terminal #402

Description

@serge-ivo

Why the "tmux Operator" shows iTerm2 windows

Because it is not a tmux agent. It is a generic terminal agent with a tmux name.

Its declared tools (live, list_instance_tools on cda75e28) are the generic connector's:

terminal_list_targets · terminal_capture · terminal_run_command
terminal_send_keys · terminal_new_target · terminal_kill_target

terminal_list_targets takes backend: "all" | "tmux" | "kitty" | "iterm2", defaulting to
all
(connectors/terminal.ts:50), and backend(input) (:22) simply reads back whatever the
model passed. Nothing in the agent's capabilities constrains it. So the name is a label and the
capability is "every local terminal on the machine" — the agent behaved correctly and reported two
iTerm2 windows, because that is genuinely what it can reach.

There is an immediate fix that needs no code. A tmux-only connector already exists in the
registry (registry.ts id: "tmux") with six backend-exclusive tools — tmux_list_sessions,
tmux_capture_pane, tmux_run_command, tmux_send_keys, tmux_new_session, tmux_kill_session.
Declaring THOSE in the agent's capabilities.tools instead of the generic six makes it a tmux agent
by construction: there is no iTerm2 code path behind them to reach. That is a config change on the
agent row.

That fixes one agent. The rest of this ticket is why the general case still needs building.

The gap: there is no ceiling a creator can declare

capabilities already lets a creator declare surfaces, runtime, workflow and a tool allowlist. What
it cannot express is a constraint on a tool's arguments — "this agent may use the terminal
connector, but only the tmux backend". Today the only ways to narrow an agent are to pick different
tools (works here only because a parallel tmux connector happens to exist) or to write it in the
prompt (which is a request, not a boundary — #386 and ADR 0001 are this month's reminder that a
constraint the model can talk around is not a constraint).

The design — two different things, and conflating them is the mistake

The platform already draws this line and it should be applied rather than re-invented:

who decides what it is
capabilities (surfaces/runtime/workflow/tools) creator, immutable by the subscriber what the agent IS
instance config / settings (runnerNode, settings, consent) subscriber how this instance is used

(a) The ceiling — creator-declared, subscriber CANNOT widen.
capabilities.surfaceOptions.terminal.backends: ["tmux"]. A tmux Operator can never see or touch
iTerm2, on any instance, ever. This belongs with capabilities because it is a claim the catalog
makes: the agent is named tmux Operator and lintAgentClaims (#362) already checks descriptions
against capabilities. If a subscriber could widen it, the description would become false by
configuration — which is exactly what that lint exists to prevent.

(b) The binding — subscriber-chosen, within the ceiling.
Which tmux session this instance drives. This is the same shape as config.runnerNode: that the
agent uses a machine is the creator's declaration; which machine is the subscriber's.

"Should an agent connect to only one terminal?" — right for some, wrong as a rule

Making one-target universal would be a mistake, and the platform already has the precedent for the
correct form: surfaceOptions.coding.repos: "single" — declared per agent, not imposed on all
of them, because a Repo Coder owns one repo while the Coder surveys many.

The same reasoning applies exactly:

  • A named operator ("Heartfull tmux") should be targets: "single". One pane, one job. A
    mis-selected target here is a shell command on a real machine, so ambiguity is not a cosmetic
    problem.
  • The generic Terminal Operator must stay many — surveying every backend is its entire
    purpose; it is the agent that exercises the connector.

So: terminal: { backends: [...], targets: "single" | "many" }, default many so nothing
changes for existing agents, and a single-target agent gets a subscriber-bound target the way it
gets a bound runner node.

The part to insist on: enforce it server-side

With targets: "single" and a bound target, a tool call naming a different target must be
refused in the handler, not merely hidden in the console and not merely discouraged in the
prompt. Same for a backend outside the declared set. Otherwise this is a labelling convention that
the first confidently-wrong model call walks straight through — and the value here is the guarantee,
not the tidiness.

Natural home: runRegistryTool, beside the write-consent gate (#90), which is already the place a
tool call is refused for reasons the model does not control.

Acceptance

  • A creator can declare terminal.backends and terminal.targets; both are sanitised and
    persisted with the rest of capabilities.
  • A subscriber can bind a target within the ceiling and cannot widen the backends.
  • A call naming an out-of-scope backend or a non-bound target is refused server-side, with a
    message that says which constraint applied.
  • The seeded tmux/kitty/iTerm2 Operator agents declare their own backend, so their names become
    true.
  • Default (backends absent, targets: "many") is byte-identical to today.

Files: workers/api/src/lib/connectors/terminal.ts:22,50,
workers/api/src/lib/connectors/registry.ts (terminal + tmux),
workers/api/src/lib/agent-capabilities.ts (sanitizeDeclaredCapabilities, surfaceOptions),
workers/api/src/lib/tool-registry.ts (runRegistryTool). Related: #362 (claims lint), #90
(write consent), ADR 0001 (why a prompt is not a boundary).

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

    backendBackend / Worker / API workenhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions