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]>
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
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 "";
|
||||
}
|
||||
Reference in New Issue
Block a user