docs/agents-tool-bridge.md
Rama main · réplica de lectura del repositorio original.
# Agents tool bridge
Every turn of every bot is sent OpenMausBot's own tool catalog: 27 to 39 tools,
43 to 57 KB of JSON, though most turns call two or three of them. With the
bridge on, a turn is listed a short set of tools it needs in most turns, plus
two more:
- `find_tool({ query })` searches the rest of that turn's tools and returns, for
the best three matches, the name, the description and the full input schema.
- `use_tool({ name, arguments })` runs one of them. The call goes down the same
path as a call to the tool by its own name, so the same refusals, limits and
results apply. Nothing about a tool is checked by `use_tool` that a direct
call is not checked for.
The list is then 14.5 to 15.4 KB, 65 to 74% smaller. What a turn may call does
not change: the tools behind `use_tool` are exactly the ones the turn would
have been listed.
It is off by default, except for a bot on a local model: one picked from a local
server, or one on an OpenAI-compatible engine whose URL is on this machine or
network. A small window cannot spare the full list, so those bots get the bridge
without the switch. `OMB_AGENTS_TOOL_BRIDGE=0` turns that off too, and the rules
under "Which bots and engines get it" still apply.
## Why it stays off by default
A live run of `evals/tool-choice` (6 requests that went wrong in real use, each picking a tool the bridge hides, one real Sonnet
turn each) reached the right tool every time, bridged or not, but did not save money on those turns:
| Tool list | Reached the right tool | Cost of the 6 turns | Looked a tool up first |
|---|---|---|---|
| Full catalog | 6 of 6 | $0.56 | 0 of 6 |
| Bridged | 6 of 6 | $0.58 | 5 of 6, 3 of them through `ToolSearch` as well |
A hidden tool costs one more model call, and `find_tool` brings up to three full schemas into the conversation. A bridged first
request is 13k tokens smaller, which matters most on a turn that uses no hidden tool, but once new threads and new bots
read a shared prompt cache, those 13k tokens cost only about $0.003 a call. By our estimate the saving and the extra calls roughly cancel out on a
busy day, so the small-window case above is the reason to use it, not cost. These are 6 runs on one model, chosen as the bridge's
worst case. Run the eval again before changing this.
## Turning it on
In `~/.openmausbot/config.json`:
```json
{ "features": { "agentsToolBridge": true } }
```
or for one server process, `OMB_AGENTS_TOOL_BRIDGE=1`. The variable wins over
the saved setting when it is exactly `1` or `0`.
## The tools that stay listed
`tool_result_read`, `list_room_targets`, `coordinate_bots`, `list_bots`,
`list_rooms`, `select_computer`, `attach_file`, `post_to_room`,
`request_credential`, `request_sign_in`, `fill_login`, `memory_update`, and
where the bot has them `send_voice_note` and Watcher's `create_options_card`.
Two of these can never be reached through `use_tool`: `post_to_room` and
`send_voice_note`. OpenMausBot recognizes a send by the name of the tool that
made it (so that a turn cut off after posting is not run again), and any tool
it would recognize that way is always listed and answers "call it directly".
## Which bots and engines get it
The switch is necessary, not sufficient. A call made through `use_tool`
reaches OpenMausBot and the engine under the name `use_tool`, so a turn is
bridged only where nothing decides by a tool's own name. The decision is one
function, `agentsToolBridgeRefusal` in `server/agents-bridge-switch.ts`, and
its default is no bridge.
**Bots.** A bot with a [tool selection](tool-selection.md) is never bridged.
Its selection is matched against the name in the call, so it keeps the whole
catalog, as before. This holds even if the selection is saved while a turn is
being set up: wherever a selection is mounted in front of the agents tools,
they are started unbridged (`server/mcp-gate-config.ts`,
`server/drivers/chat-mcp-tools.ts`), and the gate judges a `use_tool` call by
the tool it names as well (`server/mcp-gate.ts`).
**Engines.** Only engines on which OpenMausBot pre-approves all of its own
tools, so that no approval is ever asked for, remembered or refused by one
tool's name, and on which everything the engine itself reads about a tool's
name can be read by OpenMausBot too:
| Engine | Bridged | Why |
|---|---|---|
| Claude Code | Yes, with the exceptions below | The agents server is pre-allowed as a whole (`--allowedTools mcp__agents`, `server/drivers/claude.ts:1673`), and the settings the CLI reads for the turn are checked. |
| API models: OpenAI-compatible, Cerebras, Greenference, ModelGo, Grok API, Groq, Meta, MiniMax, Mistral | Yes | The chat runtime never asks for the agents server (`server/drivers/chat-mcp-tools.ts:371`) and reads no configuration of the person's about a tool. |
| Codex, including the ChatGPT plan | No | Codex reads a layered configuration of its own: `config.toml` under `CODEX_HOME` or `~/.codex`, a Company engine's own home under OpenMausBot's data, a trusted project's `.codex/config.toml`, `/etc/codex/config.toml` and `/etc/codex/managed_config.toml`; and it has `PreToolUse` and `PermissionRequest` hooks of its own. Any of these can take a rule about a tool by name, and this version does not examine them. |
| Pi | No | pi is launched with `-e` for OpenMausBot's extension and nothing that keeps the person's own extensions out (`server/drivers/pi.ts`, `PI_ARGS`), in the person's home. Pi loads the extensions it finds there, and an extension's `tool_call` handler can block a call by the name it sees, as OpenMausBot's own does (`server/drivers/pi-mcp-extension.ts`). This version does not examine them. |
| ACP agents: Grok, Kimi, Droid, Cursor, Antigravity, OpenCode, Qwen, Hermes, Cline, Gemini, custom | No | They keep their own permissions. "Always allow this session" hands the agent its own remembered approval ([approval levels](approval-levels.md)); given once for a harmless `use_tool` call it would cover every hidden tool. A reviewer would also lose the read-only marks the hidden reads carry, and Grok has native tools named `use_tool` and `search_tool`. |
| Muse Code, and any engine not named above | No | Not shown to qualify. |
If the decision itself fails (a file it reads throws, a record it is handed
cannot be read), the turn runs unbridged, exactly as with the switch off, and
one line is logged naming the kind of error and nothing of what was read.
### Claude Code
Claude Code is not bridged when any of these holds:
- The bot inherits this machine's own Claude Code setup
(`OMB_CLAUDE_INHERIT_USER_CONFIG=1`, or **Also use my Claude Code MCP
servers**).
- An entry of the engine's `tools` or `disallowedTools`, which go to the CLI as
written (`server/drivers/claude.ts:1580-1583`), names the agents server or
has a wildcard in a tool's name.
- A Claude settings file the CLI will read for the turn names the agents
server, in its raw text or in any string value or key of what it parses to
(so a spelling with JSON escapes is seen); or sets a `PreToolUse` or
`PermissionRequest` hook: a `hooks` key, in any case, holding one of those
keys, in any case, with any value other than `null`. Any such hook counts,
whatever it matches: an empty or wildcard matcher sees every call, and a
hook's own script can test the tool's name. OpenMausBot's own hooks are none
of these and are in none of these files: it registers `PostToolUse`,
`PreCompact`, `SessionStart` and `Stop` through a private settings file
written for each launch.
- A settings file's `permissions.deny` or `permissions.ask` list (keys in any
case) holds a rule with a wildcard in a tool's name, even one about another
server (`mcp__github__*`, `*exec*`). Claude Code matches a deny or ask
rule's whole tool name as a glob, so such a rule can reach an OpenMausBot
tool by pattern when it is called directly and miss the same call made as
`use_tool`. Allow rules do not glob and are not given this test.
- The machine's managed policy, or a drop-in beside it, holds
`allowManagedPermissionRulesOnly` (any case) with any value other than
`false` or `null`: it may void the pre-approval of the whole agents server
that the bridge on Claude Code rests on.
- One of those files is there and cannot be read as settings: it is not a
regular file (a directory, a FIFO, a socket or a device there is refused
without being opened), it is larger than 1 MB (refused without being read),
it is not JSON, or its top level or its `hooks` is not an object.
- The Claude CLI is older than 1.0.122, the first with `--setting-sources`:
it reads the person's own settings as well. The same goes while OpenMausBot
has not yet read the CLI's version, which it does whenever it describes its
engines (when the app loads, and on the Engines page).
Text **names the agents server** when, compared without regard to case, it
contains `mcp__agents` (so a server whose name merely starts that way counts
too), or contains `mcp__` directly followed by `*` or `?`: a wildcard in the
server position may reach the agents server. That is the whole test, for an
entry of a pattern list and for a settings file alike. A rule **has a
wildcard in a tool's name** when a `*` or `?` occurs outside parentheses,
where "outside" is a single flag set at `(` and cleared at `)`, exactly as the
CLI splits a tool list: `Bash(git *)` and `Read(./src/**)` have none,
`mcp__a*__vm_exec`, `*exec*` and `Bash(a),*` do. No pattern is ever built from
configuration text.
The files read are the ones the CLI reads. A bot's turn is launched with
`--setting-sources project` (`server/drivers/claude.ts:1620`), so that is
`.claude/settings.json` and `.claude/settings.local.json` in the folder the
turn runs in (the person's home folder when the turn has none of its own).
The machine's managed policy always applies:
`/Library/Application Support/ClaudeCode/managed-settings.json` on macOS,
`/etc/claude-code/managed-settings.json` on Linux, and on Windows
`C:\Program Files\ClaudeCode\managed-settings.json` and
`C:\ProgramData\ClaudeCode\managed-settings.json`, each with every `*.json`
file in a `managed-settings.d` folder beside it.
**Not read.** Claude Code can still take a rule about a tool by name from
places OpenMausBot does not read:
- policy an organisation delivers through the operating system: macOS managed
preferences, the Windows registry;
- settings a Claude account's organisation sends from the server;
- a project subagent file under `.claude/agents/`, whose frontmatter takes
`tools`, `disallowedTools` and `hooks` of its own;
- the frontmatter of a skill file (`disallowed-tools` or `disallowedTools`,
and `hooks`), including the skills OpenMausBot itself links into a bot's
workspace under `.claude/skills`, and of a slash-command file, which takes
the same keys;
- hooks that come from a plugin a settings file enables, which live in the
plugin, not in the file.
An organisation or a project that uses any of these to restrict OpenMausBot's
own tools should leave the bridge off: a rule there would see `use_tool`, not
the tool it ran.
## What changes for the person
Tool rows in a conversation, work summaries and the daily log name a bridged
call by the tool it ran (`mcp__agents__list_triggers`, not
`mcp__agents__use_tool`). A `use_tool` call that named no tool, or none that
exists, keeps its own name.
In a conversation this renaming happens only on a turn that was bridged. On
any other turn a tool called `use_tool` is left as it is: it belongs to some
other server. On a bridged turn the engines that are covered report a tool
with its server (`mcp__agents__use_tool` on Claude Code, `agents_use_tool` on
the chat runtime); were one to report a bare `use_tool`, another MCP server's
own tool of that name that happened to name one of OpenMausBot's tools would
be shown under the name it was given.
The bot's Activity log is read back from the event log, which knows only what
each call carried. It names a bridged call by its tool wherever the logged
name says the call was OpenMausBot's (`mcp__agents__use_tool`,
`agents_use_tool`); a bare `use_tool`, which could be any server's, is left
as "Use tool".
Two calls stay named `use_tool` everywhere: one whose record of its arguments
was cut short before the tool's name, which takes a very large call that put
its arguments first, and one on a thread whose mark was dropped because more
than 5,000 threads had bridged turns since.
The bot is told about the two tools in one sentence of its prompt, worded so
that it is true whether or not they are in its list.
`find_tool` matches on words, not meaning. A request that shares no word with
a tool's name, description or schema finds nothing and is given the list of
names instead.
## Checking a change
```sh
pnpm exec vitest run server/drivers/agents-bridge.test.ts server/drivers/agents-bridge-wire.test.ts server/drivers/agents-bridge-chat.test.ts server/drivers/agents-proxy.test.ts server/agents-bridge-switch.test.ts server/agents-bridge-names.test.ts server/agents-bridge-gates.test.ts server/agents-bridge.e2e.test.ts
```
The bridged list is pinned byte for byte, like the catalog itself. After any
change to a catalog tool, run `server/drivers/agents-catalog-wire.test.ts` and
`server/drivers/agents-bridge-wire.test.ts` once with
`UPDATE_AGENTS_CATALOG_GOLDENS=1`, review the golden diff, and move each file's
`BUDGET_BASELINE` by hand.