diff --git a/apps/agent-worker/src/durable-objects/project-sandbox-processes.ts b/apps/agent-worker/src/durable-objects/project-sandbox-processes.ts index 0cb99424..d7f4fcf6 100644 --- a/apps/agent-worker/src/durable-objects/project-sandbox-processes.ts +++ b/apps/agent-worker/src/durable-objects/project-sandbox-processes.ts @@ -6,7 +6,6 @@ import type { SandboxRunCodeResult, } from "@cheatcode/sandbox-contracts"; import type { SandboxConsoleSnapshot } from "@cheatcode/types/api"; -import { encodeBase64 } from "../sandbox-support"; import { sandboxExecProcessName } from "./project-sandbox-audit"; import { WORKSPACE_DIR } from "./project-sandbox-content-support"; import { recordSandboxUsageBestEffort } from "./project-sandbox-metering"; @@ -56,8 +55,6 @@ import { import type { SandboxRuntime } from "./project-sandbox-runtime-handle"; const DEFAULT_EXEC_TIMEOUT_MS = 60_000; -const RUN_CODE_ENV_CHUNK_CHARACTERS = 24_000; -const RUN_CODE_ENV_PREFIX = "CHEATCODE_RUN_CODE_"; export interface ProjectSandboxStatus { healthy: boolean; @@ -149,11 +146,11 @@ async function runCode( input: ProjectRunCodeInput, ): Promise { const parsed = ProjectRunCodeInputSchema.parse(input); - const encodedChunks = chunkRunCode(encodeBase64(new TextEncoder().encode(parsed.code))); - const result = await executeCommand(context.runtime, { - command: runCodeCommand(parsed.language, encodedChunks.length), + const result = await executeCode(context.runtime, { + code: parsed.code, cwd: parsed.cwd ?? WORKSPACE_DIR, - env: runCodeEnvironment(parsed.env, encodedChunks), + env: parsed.env, + language: parsed.language, timeoutMs: parsed.timeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS, }); return { @@ -164,6 +161,38 @@ async function runCode( }; } +interface ExecutableCode { + code: string; + cwd: string; + env: Record | undefined; + language: "javascript" | "python"; + timeoutMs: number; +} + +async function executeCode( + runtime: ProcessRuntime, + input: ExecutableCode, +): Promise { + const startedAt = Date.now(); + const processName = input.language === "python" ? "python3" : "node"; + const id = await runtime.ensureSandbox(); + const env = projectPackageEnvironment(input.cwd, input.env); + try { + const completed = await runtime.client().runCode(id, { + code: codeWithWorkingDirectory(input.language, input.cwd, input.code), + language: input.language, + timeout: timeoutSeconds(input.timeoutMs), + ...(env === undefined ? {} : { env }), + }); + const result = execResult(processName, completed, startedAt); + await recordExecAudit(runtime, [processName], input.cwd, result, completed.exitCode, startedAt); + await recordSandboxUsageBestEffort(await runtime.meteringContext()); + return result; + } catch (error) { + throw runtime.toUpstreamError(error, "Sandbox code execution failed."); + } +} + async function exec(runtime: ProcessRuntime, input: ProjectExecInput): Promise { const parsed = ProjectExecInputSchema.parse(input); return executeCommand(runtime, { @@ -211,32 +240,16 @@ async function executeCommand( } } -function chunkRunCode(encoded: string): string[] { - const chunks: string[] = []; - for (let offset = 0; offset < encoded.length; offset += RUN_CODE_ENV_CHUNK_CHARACTERS) { - chunks.push(encoded.slice(offset, offset + RUN_CODE_ENV_CHUNK_CHARACTERS)); - } - return chunks; -} - -function runCodeCommand(language: "javascript" | "python", chunkCount: number): string[] { - const inputs = Array.from( - { length: chunkCount }, - (_, index) => `"$${RUN_CODE_ENV_PREFIX}${index}"`, - ).join(" "); - const interpreter = language === "python" ? "python3 -" : "node --input-type=module"; - return ["sh", "-c", `printf %s ${inputs} | base64 -d | ${interpreter}`]; -} - -function runCodeEnvironment( - requested: Record | undefined, - chunks: readonly string[], -): Record { - const environment = { ...requested }; - for (const [index, chunk] of chunks.entries()) { - environment[`${RUN_CODE_ENV_PREFIX}${index}`] = chunk; +function codeWithWorkingDirectory( + language: "javascript" | "python", + cwd: string, + code: string, +): string { + const serializedCwd = JSON.stringify(cwd); + if (language === "python") { + return `import os\nos.chdir(${serializedCwd})\nexec(compile(${JSON.stringify(code)}, "", "exec"))`; } - return environment; + return `process.chdir(${serializedCwd});\n${code}`; } function packageManagerPolicyResult( diff --git a/packages/agent-core/README.md b/packages/agent-core/README.md index c0dad032..d38ab4f9 100644 --- a/packages/agent-core/README.md +++ b/packages/agent-core/README.md @@ -39,7 +39,9 @@ workspace-backed file, shell, document, chart, or artifact work resolves the thr project lazily when durable project storage is actually needed. Code tools expose `/workspace` as a virtual project root and remap path references inside argv, shell payloads, and inline code to the canonical project folder. Projectless calculations and environment probes run from `/tmp`, -so a weaker model cannot accidentally leave durable files outside a project. +so a weaker model cannot accidentally leave durable files outside a project. The bounded Daytona +REST adapter maps request-scoped command environment variables to the provider's `envs` wire field; +generated code can therefore cross the process boundary without entering argv or persistent files. ## Code Checks diff --git a/packages/agent-core/src/tools/code/daytona-client.ts b/packages/agent-core/src/tools/code/daytona-client.ts index 504f561c..9b6e0754 100644 --- a/packages/agent-core/src/tools/code/daytona-client.ts +++ b/packages/agent-core/src/tools/code/daytona-client.ts @@ -199,6 +199,14 @@ interface ExecuteParams { timeout?: number; } +interface CodeRunParams { + code: string; + env?: Record; + language: "javascript" | "python"; + /** seconds */ + timeout?: number; +} + // --------------------------------------------------------------------------- // Client // --------------------------------------------------------------------------- @@ -360,7 +368,7 @@ export class DaytonaClient { async execute(id: string, params: ExecuteParams): Promise { const body: Record = { command: params.command }; if (params.cwd !== undefined) body["cwd"] = params.cwd; - if (params.env !== undefined) body["env"] = params.env; + if (params.env !== undefined) body["envs"] = params.env; if (params.timeout !== undefined) body["timeout"] = params.timeout; const json = await this.toolbox("POST", id, "/process/execute", { body, @@ -369,6 +377,20 @@ export class DaytonaClient { return ExecuteResponseSchema.parse(json); } + async runCode(id: string, params: CodeRunParams): Promise { + const body: Record = { + code: params.code, + language: params.language, + }; + if (params.env !== undefined) body["envs"] = params.env; + if (params.timeout !== undefined) body["timeout"] = params.timeout; + const json = await this.toolbox("POST", id, "/process/code-run", { + body, + timeoutMs: (params.timeout ?? 600) * 1_000 + DAYTONA_EXEC_OVERHEAD_MS, + }); + return ExecuteResponseSchema.parse(json); + } + async createSession(id: string, sessionId: string): Promise { await this.toolbox("POST", id, "/process/session", { body: { sessionId },