The sandbox ignores sandbox_execute_bash's timeout and runs every call in one shared shell session, so a command that never ended (a network read without a timeout, a prompt) kept running after the call gave up, and every later command on that computer, from any run, timed out behind it. Each command now runs in its own bash under coreutils timeout (at most ComputerCommandMaxSeconds, below McpToolTimeoutMs), reading no input, and says when it was stopped. Replies' env list says where each key comes from and that workspace, computer and install-wide envs reach every bot there, so bots stop telling users a workspace's keys must be set per bot. The computer prompt says installed programs may be gone after a restart. An empty open-only task list says closed tasks aren't shown, and update_task says when a status was already set. Co-Authored-By: Claude Opus 5.5 <[email protected]>
65 lines
2.8 KiB
TypeScript
65 lines
2.8 KiB
TypeScript
import { AppData } from "@/src/data/app-data";
|
|
import type { HarnessTool } from "../../types";
|
|
import { shellQuote } from "./download-computer-file";
|
|
|
|
type Params = {
|
|
tool: HarnessTool;
|
|
};
|
|
|
|
/**
|
|
* Makes the computer's bash tool stop its commands on the computer. The
|
|
* sandbox ignores the tool's `timeout` and runs every call in one shared
|
|
* shell session, so a command that never ends (a network read without a
|
|
* timeout, a password prompt) used to keep running after the call gave up,
|
|
* and every later command on that computer, from any run, waited behind
|
|
* it. Each command now runs under `timeout` (its own, at most
|
|
* `ComputerCommandMaxSeconds`), reading no input, in its own bash, and
|
|
* says so when it was stopped. Other tools pass through.
|
|
*/
|
|
export default function withCommandTimeout({ tool }: Params): HarnessTool {
|
|
if (tool.name != "sandbox_execute_bash") return tool;
|
|
|
|
const max = AppData.ComputerCommandMaxSeconds;
|
|
|
|
return {
|
|
...tool,
|
|
description: `${tool.description}\n\nEvery command runs in a fresh shell (cd and export don't carry over to the next call), reads no input (anything waiting for a prompt fails at once: pass -y, a password through its env, and so on) and is stopped after timeout seconds (at most ${max}, ${max} when not given). Start longer work in the background (nohup ... > log 2>&1 &) and check its log in later calls.`,
|
|
run: async ({ args, signal }) => {
|
|
if (typeof args?.cmd != "string") return tool.run({ args, signal });
|
|
|
|
const seconds = commandSeconds({ timeout: args.timeout });
|
|
|
|
return tool.run({
|
|
args: { ...args, timeout: seconds + 10, cmd: timedCommand({ cmd: args.cmd, seconds }) },
|
|
signal,
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* The command's own timeout in whole seconds, kept to 1..`ComputerCommandMaxSeconds`.
|
|
*/
|
|
function commandSeconds({ timeout }: { timeout?: unknown }) {
|
|
const max = AppData.ComputerCommandMaxSeconds;
|
|
const asked = typeof timeout == "number" ? timeout : Number(timeout);
|
|
|
|
if (!Number.isFinite(asked) || asked <= 0) return max;
|
|
|
|
return Math.min(max, Math.max(1, Math.ceil(asked)));
|
|
}
|
|
|
|
/**
|
|
* `cmd` under `timeout` (TERM, then KILL 5 seconds later, to its whole
|
|
* process group), with its exit code kept. Never `exit`s: the sandbox's
|
|
* session shell would go with it.
|
|
*/
|
|
function timedCommand({ cmd, seconds }: { cmd: string; seconds: number }) {
|
|
return [
|
|
`timeout -k 5 ${seconds} bash -c ${shellQuote({ text: cmd })} < /dev/null`,
|
|
`_cobble_status=$?`,
|
|
`if [ $_cobble_status -eq 124 ] || [ $_cobble_status -eq 137 ]; then echo "[Stopped after ${seconds}s: the command timed out. Run long work in the background and check its log.]"; fi`,
|
|
`(exit $_cobble_status)`,
|
|
].join("; ");
|
|
}
|