Files
cobble/src/harness/tools/computer/with-computer-dirs.ts
T
tbenandClaude Opus 5.5 86becd50bc Keep every bot's computer work in its own workspace folder
Computers now mount only /home/gem/workspace from DATA_DIR/computers/<id>,
so only the workspace persists and goes into the data archive. An older
computer's whole home is moved aside to <id>-home-<ts> (kept, never
archived) and its container recreated once by sync-computers.

Each run works in bots/<bot>/users/<user> (also its cwd and HOME), with
bots/<bot> for work every user shares and workspaces/<id> shared by a
workspace's bots; the coordinator reaches the whole workspace. Computer
tools are gated by withComputerDirs: bash and code start there, file tool
paths and file: URLs outside the run's folders are refused, and so is sudo.
search_computers, import_file and script tools start there too.

The files browser shows members only the bots' folders they reach, their
own user folder in each and their workspaces', with names on the id folders.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-25 08:24:49 +01:00

115 lines
3.8 KiB
TypeScript

import AppError from "@/src/utils/app-error";
import { resolveComputerPath, type ComputerRunDirs } from "@/src/utils/computer-work-dirs";
import type { HarnessTool } from "../../types";
type Params = {
tool: HarnessTool;
dirs: ComputerRunDirs;
};
/**
* Tools whose `path` argument names a file on the computer.
*/
const PathTools = ["sandbox_file_operations", "sandbox_str_replace_editor"];
/**
* Tools that open a URL, which may be a `file:` one.
*/
const UrlTools: Record<string, string> = {
browser_navigate: "url",
browser_new_tab: "url",
sandbox_convert_to_markdown: "uri",
};
/**
* Keeps a computer tool in the run's folders (`computerRunDirs`): bash
* runs in the user's folder with it as `HOME`, code starts there, a file
* tool's path is resolved against it and refused outside the run's folders
* (`resolveComputerPath`, its `sudo` too), and so is a `file:` URL. The
* model gets a refusal back as the tool's error; nothing reaches the
* computer. Other tools pass through.
*/
export default function withComputerDirs({ tool, dirs }: Params): HarnessTool {
if (tool.name == "sandbox_execute_bash") {
return {
...tool,
run: async ({ args, signal }) => {
const cwd = resolveComputerPath({ path: typeof args?.cwd == "string" ? args.cwd : "", dirs });
return tool.run({
args: {
...args,
cwd,
cmd: typeof args?.cmd == "string" ? `export HOME=${JSON.stringify(dirs.cwd)}; ${args.cmd}` : args?.cmd,
},
signal,
});
},
};
}
if (tool.name == "sandbox_execute_code") {
return {
...tool,
run: async ({ args, signal }) =>
tool.run({
args:
typeof args?.code == "string"
? { ...args, code: `${codePrefix({ language: args.language, cwd: dirs.cwd })}${args.code}` }
: args,
signal,
}),
};
}
if (PathTools.includes(tool.name)) {
return {
...tool,
run: async ({ args, signal }) => {
if (args?.sudo) throw new AppError(`sudo isn't available here: work in your own folders.`);
return tool.run({
args: { ...args, path: resolveComputerPath({ path: args?.path, dirs }) },
signal,
});
},
};
}
const url_key = UrlTools[tool.name];
if (url_key) {
return {
...tool,
run: async ({ args, signal }) => {
const url = typeof args?.[url_key] == "string" ? args[url_key].trim() : "";
if (/^file:/i.test(url)) {
const file_path = decodeURIComponent(url.replace(/^file:(\/\/[^/]*)?/i, ""));
resolveComputerPath({ path: file_path.startsWith("/") ? file_path : `/${file_path}`, dirs });
}
return tool.run({ args, signal });
},
};
}
return tool;
}
function codePrefix({ language, cwd }: { language?: unknown; cwd: string }) {
const dir = JSON.stringify(cwd);
const lang = typeof language == "string" ? language.toLowerCase() : "python";
if (lang.startsWith("py")) {
return `import os as _cobble_os; _cobble_os.makedirs(${dir}, exist_ok=True); _cobble_os.chdir(${dir}); _cobble_os.environ["HOME"] = ${dir}\n`;
}
if (lang.startsWith("js") || lang.startsWith("javascript") || lang.startsWith("node")) {
return `try { require("fs").mkdirSync(${dir}, { recursive: true }); process.chdir(${dir}); process.env.HOME = ${dir}; } catch {}\n`;
}
return "";
}