Skip to content

Commit b33340f

Browse files
authored
fix(sandbox): use Daytona code execution API (#175)
## Why Production research PDF generation still returned `Sandbox returned invalid artifact metadata` after moving binary output out of stdout. Live sandbox diagnostics showed the deeper cause: the document renderer never executed. The custom `runCode` transport base64-encoded source into environment chunks and reconstructed it through a shell pipeline, but Daytona process execution did not inject those chunks, so Node received an empty program and exited successfully with no output. ## What changed - replace the custom base64/environment/shell reconstruction with Daytona`s native `/process/code-run` API - map command and code environment variables to Daytona`s documented `envs` wire field - preserve cwd explicitly for JavaScript and Python code execution - preserve Python module semantics by compiling the original source after changing cwd - retain bounded input validation, timeout behavior, audit records, usage metering, and provider error mapping - remove the chunking constants and transport helpers entirely - document the native code-execution boundary ## Architecture / migration effects - No database or migration changes - No new dependency or deployment configuration - Generated code is now sent as code over Daytona`s dedicated API instead of being disguised as shell environment data ## Verification - `pnpm lint` - `pnpm typecheck` - `pnpm turbo build --force` - `pnpm deadcode` (passes; four existing Knip configuration hints remain) - `pnpm architecture:check` - `pnpm turbo skills:build` - Live Daytona: 90,126-character JavaScript program executed with env and cwd intact - Live Daytona: Python executed with env, cwd, and `from __future__` semantics intact - Live Daytona: full Markdown PDF renderer returned the artifact marker and created a valid 7,143-byte PDF file Local verification used Node 26.4.0 while the repository pins Node 24.18.0; pnpm emitted the existing engine warning, and all checks passed.
1 parent 5a8bc61 commit b33340f

3 files changed

Lines changed: 71 additions & 34 deletions

File tree

apps/agent-worker/src/durable-objects/project-sandbox-processes.ts

Lines changed: 45 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import type {
66
SandboxRunCodeResult,
77
} from "@cheatcode/sandbox-contracts";
88
import type { SandboxConsoleSnapshot } from "@cheatcode/types/api";
9-
import { encodeBase64 } from "../sandbox-support";
109
import { sandboxExecProcessName } from "./project-sandbox-audit";
1110
import { WORKSPACE_DIR } from "./project-sandbox-content-support";
1211
import { recordSandboxUsageBestEffort } from "./project-sandbox-metering";
@@ -56,8 +55,6 @@ import {
5655
import type { SandboxRuntime } from "./project-sandbox-runtime-handle";
5756

5857
const DEFAULT_EXEC_TIMEOUT_MS = 60_000;
59-
const RUN_CODE_ENV_CHUNK_CHARACTERS = 24_000;
60-
const RUN_CODE_ENV_PREFIX = "CHEATCODE_RUN_CODE_";
6158

6259
export interface ProjectSandboxStatus {
6360
healthy: boolean;
@@ -149,11 +146,11 @@ async function runCode(
149146
input: ProjectRunCodeInput,
150147
): Promise<SandboxRunCodeResult> {
151148
const parsed = ProjectRunCodeInputSchema.parse(input);
152-
const encodedChunks = chunkRunCode(encodeBase64(new TextEncoder().encode(parsed.code)));
153-
const result = await executeCommand(context.runtime, {
154-
command: runCodeCommand(parsed.language, encodedChunks.length),
149+
const result = await executeCode(context.runtime, {
150+
code: parsed.code,
155151
cwd: parsed.cwd ?? WORKSPACE_DIR,
156-
env: runCodeEnvironment(parsed.env, encodedChunks),
152+
env: parsed.env,
153+
language: parsed.language,
157154
timeoutMs: parsed.timeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS,
158155
});
159156
return {
@@ -164,6 +161,38 @@ async function runCode(
164161
};
165162
}
166163

164+
interface ExecutableCode {
165+
code: string;
166+
cwd: string;
167+
env: Record<string, string> | undefined;
168+
language: "javascript" | "python";
169+
timeoutMs: number;
170+
}
171+
172+
async function executeCode(
173+
runtime: ProcessRuntime,
174+
input: ExecutableCode,
175+
): Promise<SandboxExecResult> {
176+
const startedAt = Date.now();
177+
const processName = input.language === "python" ? "python3" : "node";
178+
const id = await runtime.ensureSandbox();
179+
const env = projectPackageEnvironment(input.cwd, input.env);
180+
try {
181+
const completed = await runtime.client().runCode(id, {
182+
code: codeWithWorkingDirectory(input.language, input.cwd, input.code),
183+
language: input.language,
184+
timeout: timeoutSeconds(input.timeoutMs),
185+
...(env === undefined ? {} : { env }),
186+
});
187+
const result = execResult(processName, completed, startedAt);
188+
await recordExecAudit(runtime, [processName], input.cwd, result, completed.exitCode, startedAt);
189+
await recordSandboxUsageBestEffort(await runtime.meteringContext());
190+
return result;
191+
} catch (error) {
192+
throw runtime.toUpstreamError(error, "Sandbox code execution failed.");
193+
}
194+
}
195+
167196
async function exec(runtime: ProcessRuntime, input: ProjectExecInput): Promise<SandboxExecResult> {
168197
const parsed = ProjectExecInputSchema.parse(input);
169198
return executeCommand(runtime, {
@@ -211,32 +240,16 @@ async function executeCommand(
211240
}
212241
}
213242

214-
function chunkRunCode(encoded: string): string[] {
215-
const chunks: string[] = [];
216-
for (let offset = 0; offset < encoded.length; offset += RUN_CODE_ENV_CHUNK_CHARACTERS) {
217-
chunks.push(encoded.slice(offset, offset + RUN_CODE_ENV_CHUNK_CHARACTERS));
218-
}
219-
return chunks;
220-
}
221-
222-
function runCodeCommand(language: "javascript" | "python", chunkCount: number): string[] {
223-
const inputs = Array.from(
224-
{ length: chunkCount },
225-
(_, index) => `"$${RUN_CODE_ENV_PREFIX}${index}"`,
226-
).join(" ");
227-
const interpreter = language === "python" ? "python3 -" : "node --input-type=module";
228-
return ["sh", "-c", `printf %s ${inputs} | base64 -d | ${interpreter}`];
229-
}
230-
231-
function runCodeEnvironment(
232-
requested: Record<string, string> | undefined,
233-
chunks: readonly string[],
234-
): Record<string, string> {
235-
const environment = { ...requested };
236-
for (const [index, chunk] of chunks.entries()) {
237-
environment[`${RUN_CODE_ENV_PREFIX}${index}`] = chunk;
243+
function codeWithWorkingDirectory(
244+
language: "javascript" | "python",
245+
cwd: string,
246+
code: string,
247+
): string {
248+
const serializedCwd = JSON.stringify(cwd);
249+
if (language === "python") {
250+
return `import os\nos.chdir(${serializedCwd})\nexec(compile(${JSON.stringify(code)}, "<cheatcode>", "exec"))`;
238251
}
239-
return environment;
252+
return `process.chdir(${serializedCwd});\n${code}`;
240253
}
241254

242255
function packageManagerPolicyResult(

packages/agent-core/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ workspace-backed file, shell, document, chart, or artifact work resolves the thr
3939
project lazily when durable project storage is actually needed. Code tools expose `/workspace`
4040
as a virtual project root and remap path references inside argv, shell payloads, and inline code
4141
to the canonical project folder. Projectless calculations and environment probes run from `/tmp`,
42-
so a weaker model cannot accidentally leave durable files outside a project.
42+
so a weaker model cannot accidentally leave durable files outside a project. The bounded Daytona
43+
REST adapter maps request-scoped command environment variables to the provider's `envs` wire field;
44+
generated code can therefore cross the process boundary without entering argv or persistent files.
4345

4446
## Code Checks
4547

packages/agent-core/src/tools/code/daytona-client.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,14 @@ interface ExecuteParams {
199199
timeout?: number;
200200
}
201201

202+
interface CodeRunParams {
203+
code: string;
204+
env?: Record<string, string>;
205+
language: "javascript" | "python";
206+
/** seconds */
207+
timeout?: number;
208+
}
209+
202210
// ---------------------------------------------------------------------------
203211
// Client
204212
// ---------------------------------------------------------------------------
@@ -360,7 +368,7 @@ export class DaytonaClient {
360368
async execute(id: string, params: ExecuteParams): Promise<DaytonaExecuteResponse> {
361369
const body: Record<string, unknown> = { command: params.command };
362370
if (params.cwd !== undefined) body["cwd"] = params.cwd;
363-
if (params.env !== undefined) body["env"] = params.env;
371+
if (params.env !== undefined) body["envs"] = params.env;
364372
if (params.timeout !== undefined) body["timeout"] = params.timeout;
365373
const json = await this.toolbox("POST", id, "/process/execute", {
366374
body,
@@ -369,6 +377,20 @@ export class DaytonaClient {
369377
return ExecuteResponseSchema.parse(json);
370378
}
371379

380+
async runCode(id: string, params: CodeRunParams): Promise<DaytonaExecuteResponse> {
381+
const body: Record<string, unknown> = {
382+
code: params.code,
383+
language: params.language,
384+
};
385+
if (params.env !== undefined) body["envs"] = params.env;
386+
if (params.timeout !== undefined) body["timeout"] = params.timeout;
387+
const json = await this.toolbox("POST", id, "/process/code-run", {
388+
body,
389+
timeoutMs: (params.timeout ?? 600) * 1_000 + DAYTONA_EXEC_OVERHEAD_MS,
390+
});
391+
return ExecuteResponseSchema.parse(json);
392+
}
393+
372394
async createSession(id: string, sessionId: string): Promise<void> {
373395
await this.toolbox("POST", id, "/process/session", {
374396
body: { sessionId },

0 commit comments

Comments
 (0)