Relates to: #1357, #492, #265, #386, #1384
Problem:
Enumerating the sandbox tools proxy throws an error directing the agent to tools.search(). But tools.search({ namespace, query: "" }) returns zero items, so the tool the hint points at cannot enumerate either. There is no way to ask what tools an integration actually exposes.
PR #1357 added the hint, and it is explicit about where to go next:
// packages/kernel/runtime-deno-subprocess/src/deno-subprocess-worker.mjs:44
const toolsEnumerationMessage = (path) =>
(path.length === 0 ? "tools" : "tools." + path.join(".")) +
' is a lazy proxy and cannot be enumerated. Use tools.search({ query: "..." }) to find tools, or tools.executor.coreTools.connections.list({}) to list saved connections.';
Follow it, and searchTools short-circuits before it ever lists anything:
// packages/core/execution/src/tool-invoker.ts:665
if (normalizeSearchText(query).length === 0) {
return empty;
}
scoreToolMatch refuses the same input independently, so bypassing the guard would rank nothing. That is coherent for a keyword-ranked search: an empty query carries no ranking signal. The gap is that nothing else fills the role. The sandbox exposes exactly three discovery tools: search, which needs a query; describe.tool, which needs a path you already know; and executor.sources.list, which reports a toolCount per integration but never the paths behind it.
So the count is available and the contents are not. An agent that wants to know whether an operation exists on a connection must already suspect its name.
This is the same class of failure as #265, where a model mistook the first page of sources.list for the full inventory. PR #492 fixed that by paginating and returning total, so a caller could tell truncation had happened and page past it. Enumeration has the inverse problem: search reports total: 0 truthfully, and the caller has no way to page past a query they cannot express.
This surfaced while auditing #1384, tool catalogs exceeding their connection's granted OAuth scope. Establishing that two integrations exposed identical tool sets required unioning eight generic verbs (list, get, create, update, delete, query, send, search) and paging each to exhaustion. The resulting count of 239 is a floor. Any tool whose path, name, and description miss all eight verbs is invisible to the method, and no way exists to find out how many did.
Reproduction:
Inside the execute sandbox, with any integration connected:
const empty = await tools.search({ namespace: "google_gmail", query: "" });
// { items: [], total: 0, hasMore: false, nextOffset: null }
const sources = await tools["executor.sources.list"]({});
// the matching entry reports toolCount: 239, but no paths
const unioned = new Set<string>();
for (const query of ["list", "get", "create", "update", "delete"]) {
const res = await tools.search({ namespace: "google_gmail", query, limit: 50 });
for (const item of res.items) unioned.add(item.path);
}
// unioned.size is a lower bound; nothing reconciles it against toolCount
Expected vs actual:
Expected: some call returns every tool path in a namespace, so a catalog can be audited, diffed across integrations, or reconciled against the toolCount that executor.sources.list already reports.
Actual: search requires a non-empty query and returns nothing without one. describe.tool requires a path. sources.list gives a count with no way to enumerate what it counted. Coverage of a namespace can only be approximated, never confirmed.
Proposed solution:
The narrow fix is to treat an empty query as "match everything" when a namespace is supplied, returning tools sorted by path rather than score, still paged through the existing limit and offset. executor.tools.list is already called inside searchTools just below the guard, so the data is in hand; only the guard and the null return in scoreToolMatch stand in the way.
The alternative is a separate tools.list({ namespace }) alongside search, which keeps ranked search honest about being ranked search and gives enumeration its own name and output shape. This is probably cleaner, since a scored result whose scores are all zero is a confusing thing to return. It would also let #1357's hint text name a tool that can actually do what the hint promises.
Either way the hint in the runtime proxies should be updated, since today it sends an agent to a dead end.
Worth deciding whether empty-query enumeration should be allowed with no namespace at all. Returning every tool in the workspace may be a large response, though the existing pagination already bounds each page. Issue #386 asks the broader version of this question, how an agent should discover what Executor offers at all, and a maintainer may want to answer both together.
Relates to: #1357, #492, #265, #386, #1384
Problem:
Enumerating the sandbox
toolsproxy throws an error directing the agent totools.search(). Buttools.search({ namespace, query: "" })returns zero items, so the tool the hint points at cannot enumerate either. There is no way to ask what tools an integration actually exposes.PR #1357 added the hint, and it is explicit about where to go next:
Follow it, and
searchToolsshort-circuits before it ever lists anything:scoreToolMatchrefuses the same input independently, so bypassing the guard would rank nothing. That is coherent for a keyword-ranked search: an empty query carries no ranking signal. The gap is that nothing else fills the role. The sandbox exposes exactly three discovery tools:search, which needs a query;describe.tool, which needs a path you already know; andexecutor.sources.list, which reports atoolCountper integration but never the paths behind it.So the count is available and the contents are not. An agent that wants to know whether an operation exists on a connection must already suspect its name.
This is the same class of failure as #265, where a model mistook the first page of
sources.listfor the full inventory. PR #492 fixed that by paginating and returningtotal, so a caller could tell truncation had happened and page past it. Enumeration has the inverse problem:searchreportstotal: 0truthfully, and the caller has no way to page past a query they cannot express.This surfaced while auditing #1384, tool catalogs exceeding their connection's granted OAuth scope. Establishing that two integrations exposed identical tool sets required unioning eight generic verbs (
list,get,create,update,delete,query,send,search) and paging each to exhaustion. The resulting count of 239 is a floor. Any tool whose path, name, and description miss all eight verbs is invisible to the method, and no way exists to find out how many did.Reproduction:
Inside the
executesandbox, with any integration connected:Expected vs actual:
Expected: some call returns every tool path in a namespace, so a catalog can be audited, diffed across integrations, or reconciled against the
toolCountthatexecutor.sources.listalready reports.Actual:
searchrequires a non-empty query and returns nothing without one.describe.toolrequires a path.sources.listgives a count with no way to enumerate what it counted. Coverage of a namespace can only be approximated, never confirmed.Proposed solution:
The narrow fix is to treat an empty query as "match everything" when a
namespaceis supplied, returning tools sorted by path rather than score, still paged through the existinglimitandoffset.executor.tools.listis already called insidesearchToolsjust below the guard, so the data is in hand; only the guard and the null return inscoreToolMatchstand in the way.The alternative is a separate
tools.list({ namespace })alongsidesearch, which keeps ranked search honest about being ranked search and gives enumeration its own name and output shape. This is probably cleaner, since a scored result whose scores are all zero is a confusing thing to return. It would also let #1357's hint text name a tool that can actually do what the hint promises.Either way the hint in the runtime proxies should be updated, since today it sends an agent to a dead end.
Worth deciding whether empty-query enumeration should be allowed with no namespace at all. Returning every tool in the workspace may be a large response, though the existing pagination already bounds each page. Issue #386 asks the broader version of this question, how an agent should discover what Executor offers at all, and a maintainer may want to answer both together.