Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions services/runner/src/engines/sandbox_agent/run-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,25 @@ export const REMOTE_TOOLS_UNSUPPORTED_MESSAGE =
"sandbox, use the Pi harness, or remove the tools. Tracked in " +
"docs/design/agent-workflows/projects/in-sandbox-tool-mcp/.";

/**
* A non-Pi harness (Claude) on Daytona receives tools through the in-sandbox stdio MCP shim,
* which advertises only EXECUTABLE (gateway/callback) tools. Client tools are intentionally
* omitted: the shim's blocking relay call cannot pause for a browser round-trip yet. So a run
* whose ENTIRE tool set is client-kind has nothing deliverable to advertise on Daytona — it
* would otherwise proceed, drop every tool silently, and return `ok:true` (the F1 zero-tools
* drop). `run-plan.ts` is the documented refusal point for this (see `mcp.ts`); refuse it up
* front, exactly as the non-Daytona remote case is refused. A MIX of client + executable tools
* is fine (the executable ones are delivered; the client ones are dropped), so this fires only
* when NO executable tool remains.
*/
export const DAYTONA_CLIENT_ONLY_TOOLS_UNSUPPORTED_MESSAGE =
"Client tools are not deliverable to a non-Pi harness on Daytona: the in-sandbox stdio MCP " +
"shim advertises only executable (gateway/callback) tools, and a client tool cannot pause " +
"for a browser round-trip through its blocking relay yet. This run's tool set is entirely " +
"client-kind, so nothing would be advertised and the run would silently get zero tools. Add " +
"an executable tool, use the Pi harness, run on the local sandbox, or remove the tools. " +
"Tracked in docs/design/agent-workflows/projects/in-sandbox-tool-mcp/.";

/**
* `runtime_provided` (subscription) auth means the harness authenticates from explicitly prepared
* local runtime state (a mounted Pi/Claude login). That state lives only in the runner container
Expand Down Expand Up @@ -414,6 +433,12 @@ export function buildRunPlan(
if (!isDaytona) {
return { ok: false, error: REMOTE_TOOLS_UNSUPPORTED_MESSAGE };
}
// On Daytona the shim advertises only executable tools; client tools are omitted. If the run
// carries tools but NONE are executable, nothing would be delivered — refuse instead of
// silently advertising an empty tool set (the F1 zero-tools drop mcp.ts's log warns about).
if (executableToolSpecsForRun.length === 0) {
return { ok: false, error: DAYTONA_CLIENT_ONLY_TOOLS_UNSUPPORTED_MESSAGE };
}
}

// Layer 2: even on Daytona, code/gateway tools run on the RUNNER HOST via the relay, not
Expand Down
28 changes: 23 additions & 5 deletions services/runner/tests/unit/sandbox-agent-run-plan.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -704,7 +704,12 @@ describe("buildRunPlan", () => {
);
});

it("allows claude x daytona x client-only tools with no shim specs", () => {
it("refuses claude x daytona x client-ONLY tools (nothing deliverable to advertise)", () => {
// The shim advertises only executable tools; client tools are omitted. A run whose entire
// tool set is client-kind has nothing to deliver on Daytona, so it must fail CLOSED up
// front rather than proceed, drop every tool, and return ok:true (the F1 zero-tools drop
// that mcp.ts's "run-plan should have refused this run" log points at).
let created = false;
const result = buildRunPlan(
{
harness: "claude",
Expand All @@ -713,13 +718,26 @@ describe("buildRunPlan", () => {
customTools: [{ name: "request_connection", kind: "client" }],
} as AgentRunRequest,
{
createDaytonaCwd: () => "/home/sandbox/agenta-fixed",
createDaytonaCwd: () => {
created = true;
return "/home/sandbox/agenta-fixed";
},
},
);

assert.equal(result.ok, true);
if (!result.ok) return;
assert.deepEqual(result.plan.executableToolSpecs, []);
assert.equal(result.ok, false);
if (result.ok) return;
assert.match(result.error, /Client tools are not deliverable/);
assert.match(result.error, /entirely\s+client-kind/);
assert.match(
result.error,
/docs\/design\/agent-workflows\/projects\/in-sandbox-tool-mcp\//,
);
assert.equal(
created,
false,
"fails before any cwd is created (up-front gate)",
);
});

it("allows pi x daytona x client-only tools (Pi's extension + file relay deliver them)", () => {
Expand Down
Loading