Skip to content

list_instance_tools says "a tool absent from the allowed set cannot be invoked, by chat or by call_instance_tool" — 11 tools every agent runs in chat are absent, and 2 of them ran on the audited instance #525

Description

@serge-ivo

The claim

list_instance_tools describes itself, verbatim (workers/mcp/src/instance-tools/base.ts:29):

"Returns EVERY registry tool with this instance's verdict … Use this to verify an agent is read-only before trusting it with sensitive dataa tool absent from the allowed set cannot be invoked, by chat or by call_instance_tool."

The second half is a security claim, and it is half wrong. It is true for call_instance_tool. It is false for chat.

Measured

Instance bd43f4de-ef35-4051-bdec-43f8571414a1 (Repo Coder, capabilities.tools declared), 2026-08-12:

  • GET /v1/instances/:id/tools returns 70 entries; ?allowed=true returns 22.

  • toolNamesFor() grants every agent the 19-name BASE set (workers/api/src/agent-do-tools.ts:15-…, and toolNamesFor at :203-205 starts from new Set(BASE) whenever an allowlist is declared).

  • 11 of those 19 appear nowhere in the listing, allowed or otherwise:

    read_memory · write_memory · delete_memory · get_tasks · create_task · update_task · fetch_url · get_activity · get_user_context · set_user_preference · configure_board

Six of the eleven write (memory create/delete, task create/update, user preference, board layout). One — fetch_url — makes outbound HTTP.

Two of them were invoked on this very instance within the audit window, both visible in agent_trace:

  • 2026-08-11 21:45:52❌ **fetch_url** HTTP 401 Unauthorized: … (it ran; the remote refused)
  • 2026-08-12 03:22:21✅ **write_memory** Stored memory: fact:Staging environment:decision. All memory keys: …

So an auditor who did exactly what the tool's description tells them to do — call list_instance_tools, confirm nothing that writes is allowed — would conclude this agent cannot write to its own memory. It wrote to its own memory nineteen hours later.

Mechanism

Two layers, each correct in isolation:

  • The listing enumerates the registry only. resolveToolPolicy (workers/api/src/lib/instance-tool-policy.ts:118-143) maps over registryTools()[...REGISTRY.values()] (lib/tool-registry.ts:467). The DO's own catalog (lib/tools.ts AGENT_TOOLS, lib/storage-tools.ts STORAGE_TOOLS) is a different collection and is never walked. That is why start_work, check_work, get_behaviour, run_pipeline, record_feedback and get_stats DO appear — they happen to be registry tools — while write_memory and fetch_url do not.
  • The gate the description describes is real, and it is only on one route. POST /v1/instances/:id/tools/:name calls getRegistryTool(name) and 404s a non-registry name (routes/tools.ts:141-146), so call_instance_tool genuinely cannot reach them. The chat runtime uses toolNamesFor + buildAgentToolDefinitions, which include them.

instance-tool-policy.ts's own header states the invariant this breaks: "'This agent is read-only' has to be a property of the INSTANCE, not of one surface's prompt-building." The policy module honours that for the registry; the listing built on it silently reports on one collection and is described as reporting on the agent.

What to do, cheapest first

  1. Fix the sentence. One-line docs change, ships today, removes the false assurance:

    "Returns every connector/registry tool… A tool absent from the allowed set cannot be invoked through call_instance_tool. This listing does not cover the agent's built-in facilities (memory, tasks, board, fetch_url), which every agent has; use it to audit an agent's reach into external systems, not to establish that it cannot write at all."
    Do this even if (2) is taken, because (2) is a behaviour change and this is a correctness fix to a security claim.

  2. Make the listing exhaustive. Include the BASE/storage tools with a tier field (base vs connector) so a caller can still ask the narrow question. ToolDef already carries tier: "connector" (see repo-local.ts:362), so the vocabulary exists. resolveToolPolicy takes its tool list as a parameter — the change is at the call site in instanceToolPolicy, not in the pure function.
  3. Decide whether disabledTools should cover them. PUT /v1/instances/:id/tools/:name also gates on getRegistryTool, so the owner's veto — described as "the one control a creator's declaration must not outrank" — cannot currently switch off write_memory or fetch_url. If (2) ships without this, the listing will show a tool the owner is shown no way to disable.

Open question for the owner

Is (2) wanted, or is the honest scoping in (1) enough? I would take (1) now and (2) next. The listing's audience is an auditor asking "what can this thing reach", and fetch_url — arbitrary outbound HTTP, SSRF-guarded but not consent-gated — is squarely inside that question. But (2) drags in (3), which is a real permissions decision, and the false sentence should not wait for it.

Alternatives rejected

  • Leave it; the base tools are "universal so they don't count." The description does not say that, it says the opposite, and it says it in the context of trusting an agent with sensitive data. A universal capability is still a capability an auditor has to know about.
  • Remove the security sentence entirely. It is answering a real question and the answer is nearly right; scoping it is better than deleting it.

Acceptance

  • The description no longer asserts that absence from the list implies the tool cannot be invoked by chat.
  • If (2): GET /v1/instances/:id/tools for bd43f4de-…-shaped instance includes all 19 BASE names, and a test asserts toolNamesFor(caps) ⊆ {names in the listing} for a representative capability set — the invariant that would have caught this.

Regression risk

resolveToolPolicy is consumed by the agent runtime, the REST invoker and MCP (its header says all three share it). Adding entries changes what ?allowed=true returns, which the console's tool list and explainWriteConsent render — a base tool with no connector resolves to writeConsent: "n/a" by writeConsentOf's first branch, so it must not start rendering as ungated external access. contract.test.ts and tools.integration.test.ts are the guards.

Related: #351 (the last time this listing was confidently wrong in the permissive direction), #493 (the agent's own tool list never stated to be exhaustive — same failure, one surface over).

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 workingsecuritySecurity hardening / audit finding

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions