Skip to content

The console tells 10 of 34 instances they have "no tool that reaches outside the platform" while fetch_url is allowed on every one of them #584

Description

@serge-ivo

The console tells the owner an agent cannot reach outside the platform, while fetch_url is switched on

Measured on the deployed API, 2026-08-15, over all 34 instances on the operator account:
10 of them render

"This agent can change its own data — its memory, tasks and stored documents — but has no tool
that reaches outside the platform."

and fetch_url is allowed: true on every one of the ten (and on all 34 — it is a BASE
tool). fetch_url takes a caller-chosen method and body
(workers/api/src/lib/tools.ts:368-376), so it POSTs arbitrary payloads to arbitrary public HTTPS
hosts. The ten: Repo Chat, AIPA repo chat, Job Application Assistant, Language Buddy, Lead Outreach
Assistant, Creator OS, Facebook Friends, Fridges, Google Drive Doc Chat Test, Codex MCP Browser Test.

The other 24 render the "…each one also needs its connector granted below" sentence, which is
correct. Nothing renders the read-only sentence — that is #577's finding and it is confirmed
separately in a comment there.

This is the sentence an owner reads when deciding what to put in an agent's knowledge base. It is
rendered by store/console/src/components/ToolPermissions.tsx:123, directly above the tool
switches, in the Settings tab of every instance.

Where it is

store/console/src/lib/toolPolicy.ts:122-130:

export function toolScopeSummary(policy: readonly ToolPolicyEntry[]): string {
	if (writeConnectors(policy).length > 0) {  }
	if (writesOwnData(policy)) {
		return " This agent can change its own data — its memory, tasks and stored documents — but has no tool that reaches outside the platform.";
	}
	return " This agent has no tools that can change anything — it can only read.";
}

and :90-94:

export function writeConnectors(policy: readonly ToolPolicyEntry[]): string[] {
	const out = new Set<string>();
	for (const t of listedTools(policy)) if (t.connector && mayWrite(t)) out.add(t.connector);
	return [...out];
}

Mechanism — two correct decisions composing

  1. writeConnectors uses "names a connector" as the proxy for "reaches outside the platform."
    That was exactly true while the listing held connector tools and nothing else, and the function's
    own docstring still describes its job as "other people's systems".

  2. 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 made the listing exhaustive. The built-in facilities now appear too, and they carry
    connector: null. So connector-less no longer implies platform-internal. Measured on the live
    listing for 6d3b28f1-… (Language Buddy):

    • fetch_url{scope:"read", mutates:true, allowed:true, connector:null, tier:"base"}
    • send_to_cli{scope:"write", mutates:true, allowed:true, connector:null, tier:"runtime"}
      (it is in FULL, so every agent that declares no capabilities.tools receives it)
    • submit_job_application{scope:"write", mutates:true, allowed:true, connector:null, tier:"standard"}

    None of the three is visible to writeConnectors, and all three reach outside the platform:
    arbitrary outbound HTTP, typing into a CLI on the owner's machine, and posting a form on a
    third-party site.

#525's own regression note anticipated the mirror of this — "a base tool with no connector
resolves to writeConsent:"n/a" … so it must not start rendering as ungated external access"
.
The listing was made exhaustive without a corresponding change to the predicate that reads it, and
the error shipped in the opposite direction: not "internal shown as external", but external shown
as absent
.

What to do — cheapest first

  1. Stop claiming the negative. Delete the "…but has no tool that reaches outside the platform"
    clause and state only what the agent has. One line, no new data on the wire, cannot be wrong,
    ships today. The claim it removes is the one an auditor should never have taken from a field that
    does not answer it.
  2. Then make the claim answerable, server-side. Add a reach fact to each listing row
    ("platform" | "machine" | "internet") beside mutates, and derive the sentence from it. This
    is list_instance_tools reports nine mutating tools as scope: "read" — one field carries two incompatible meanings, and its sibling deliberately fails the other way #563's precedent applied again: when the console needs a fact the server owns, the server
    sends it rather than the console inferring it from a field that means something else.
    writeConnectors keeps its existing job — deciding which connectors get a write-access
    checkbox — untouched.

Alternatives considered and rejected

Acceptance criteria

  1. No instance renders a sentence asserting the agent has no reach outside the platform while it
    holds fetch_url, send_to_cli or submit_job_application.
  2. A test builds a policy containing fetch_url (allowed:true, connector:null) and no
    connector tool, and asserts the rendered sentence does not claim the negative. It must fail on
    today's code.
  3. writeConnectors's output is unchanged for every policy — the grant checkboxes it feeds are
    [bug] The tool listing reports write-gated tools as allowed=true, reason=ok — agents burn a turn discovering the consent gate #351's promise and are not part of this change.

Regression risk

writeConnectors feeds both the sentence and the write-access checkbox set
(toolPolicy.ts:90-94, consumed by ToolPermissions.tsx). Changing the sentence must not change
which connectors get a checkbox, or #351's fix ("http_request gets a switch for the grant its
mutating calls are refused for") regresses silently. store/console/src/lib/toolPolicy.test.ts is
the guard; it currently has no case with a connector-less outward-reaching tool.

Verified vs inferred

  • Verified: the 10/34 and 24/34 split, and fetch_url: allowed:true on all ten — measured live
    via GET /v1/instances/:id/tools for every instance on the account; the three tool rows quoted
    above, from that same response; every file:line; that the sentence renders in the Settings tab.
  • Inferred: nothing load-bearing. send_to_cli on a runtime:null instance would refuse at
    call time for want of a runner — but it is listed allowed:true, and the sentence is a claim
    about what the agent holds. fetch_url alone falsifies the clause on all ten regardless.

Related: #577 (the read-only clause of the same function — latent, not observed), #525 (made the
listing exhaustive), #563 (mutates, the precedent for sending the fact instead of inferring it),
#351 (the write-consent chip and checkbox set this must not disturb).

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions