Skip to content

Commit 4d6f818

Browse files
authored
fix(sandbox): support large run-code payloads (#172)
## Why Rich Markdown PDF generation builds a renderer script larger than the public exec argv-element limit. ProjectSandbox.runCode reused that exec path, so the validated 8,790-character renderer failed before execution with an 8,192-character Zod limit. ## What changed - keep public exec argv limits unchanged - transport validated run-code source as bounded base64 request-environment chunks - pipe source to Node or Python over stdin, preserving top-level module behavior - execute inside the existing runCode lease without writing source onto the persistent workspace volume - document the separate runCode and exec transport contracts ## Verification - pnpm lint - pnpm typecheck - pnpm turbo build --force - pnpm deadcode - pnpm architecture:check - pnpm turbo skills:build - confirmed the production renderer script is 8,790 characters and the prior production error identifies command element 3 exceeding 8,192 characters Production research and PDF acceptance will be rerun after deployment.
1 parent 2d1a609 commit 4d6f818

2 files changed

Lines changed: 65 additions & 16 deletions

File tree

apps/agent-worker/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,10 @@ Each user has one durable Daytona sandbox. Projects are lexically confined to th
181181
folders under `/workspace`, and run leases keep the sandbox active while the agent is
182182
working. Project folders share the sandbox's Unix identity, so this prevents accidental
183183
cross-project access but is not an operating-system security boundary within one user.
184+
The validated `runCode` source contract remains separate from the smaller public `exec`
185+
argv contract: the Worker base64-chunks source into bounded, reserved request environment
186+
variables and pipes it to the selected interpreter over stdin. Large code therefore does
187+
not exceed an argv element limit or leave a temporary source file on the persistent volume.
184188
Sandbox lookup validates canonical ownership labels before trusting a cached Daytona
185189
resource ID. A missing/stale Durable Object cache therefore recovers the one canonical
186190
sandbox by labels, while duplicate live canonical matches fail closed. New sandboxes pin

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

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
SandboxRunCodeResult,
77
} from "@cheatcode/sandbox-contracts";
88
import type { SandboxConsoleSnapshot } from "@cheatcode/types/api";
9+
import { encodeBase64 } from "../sandbox-support";
910
import { sandboxExecProcessName } from "./project-sandbox-audit";
1011
import { WORKSPACE_DIR } from "./project-sandbox-content-support";
1112
import { recordSandboxUsageBestEffort } from "./project-sandbox-metering";
@@ -55,6 +56,8 @@ import {
5556
import type { SandboxRuntime } from "./project-sandbox-runtime-handle";
5657

5758
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_";
5861

5962
export interface ProjectSandboxStatus {
6063
healthy: boolean;
@@ -146,14 +149,11 @@ async function runCode(
146149
input: ProjectRunCodeInput,
147150
): Promise<SandboxRunCodeResult> {
148151
const parsed = ProjectRunCodeInputSchema.parse(input);
149-
const command =
150-
parsed.language === "python"
151-
? ["python3", "-c", parsed.code]
152-
: ["node", "--input-type=module", "-e", parsed.code];
153-
const result = await context.coordinated.exec({
154-
command,
152+
const encodedChunks = chunkRunCode(encodeBase64(new TextEncoder().encode(parsed.code)));
153+
const result = await executeCommand(context.runtime, {
154+
command: runCodeCommand(parsed.language, encodedChunks.length),
155155
cwd: parsed.cwd ?? WORKSPACE_DIR,
156-
env: parsed.env,
156+
env: runCodeEnvironment(parsed.env, encodedChunks),
157157
timeoutMs: parsed.timeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS,
158158
});
159159
return {
@@ -166,34 +166,79 @@ async function runCode(
166166

167167
async function exec(runtime: ProcessRuntime, input: ProjectExecInput): Promise<SandboxExecResult> {
168168
const parsed = ProjectExecInputSchema.parse(input);
169+
return executeCommand(runtime, {
170+
command: parsed.command,
171+
cwd: parsed.cwd ?? WORKSPACE_DIR,
172+
env: parsed.env,
173+
timeoutMs: parsed.timeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS,
174+
});
175+
}
176+
177+
interface ExecutableCommand {
178+
command: string[];
179+
cwd: string;
180+
env: Record<string, string> | undefined;
181+
timeoutMs: number;
182+
}
183+
184+
async function executeCommand(
185+
runtime: ProcessRuntime,
186+
input: ExecutableCommand,
187+
): Promise<SandboxExecResult> {
169188
const startedAt = Date.now();
170-
const command = commandToShellString(parsed.command);
171-
const cwd = parsed.cwd ?? WORKSPACE_DIR;
172-
const timeoutMs = parsed.timeoutMs ?? DEFAULT_EXEC_TIMEOUT_MS;
173-
const unsupportedManager = unsupportedProjectPackageManager(cwd, parsed.command);
189+
const command = commandToShellString(input.command);
190+
const unsupportedManager = unsupportedProjectPackageManager(input.cwd, input.command);
174191
if (unsupportedManager) {
175192
const result = packageManagerPolicyResult(command, unsupportedManager, startedAt);
176-
await recordExecAudit(runtime, parsed.command, cwd, result, result.exitCode, startedAt);
193+
await recordExecAudit(runtime, input.command, input.cwd, result, result.exitCode, startedAt);
177194
return result;
178195
}
179196
const id = await runtime.ensureSandbox();
180-
const env = projectPackageEnvironment(cwd, parsed.env);
197+
const env = projectPackageEnvironment(input.cwd, input.env);
181198
try {
182199
const completed = await runtime.client().execute(id, {
183200
command,
184-
cwd,
185-
timeout: timeoutSeconds(timeoutMs),
201+
cwd: input.cwd,
202+
timeout: timeoutSeconds(input.timeoutMs),
186203
...(env === undefined ? {} : { env }),
187204
});
188205
const result = execResult(command, completed, startedAt);
189-
await recordExecAudit(runtime, parsed.command, cwd, result, completed.exitCode, startedAt);
206+
await recordExecAudit(runtime, input.command, input.cwd, result, completed.exitCode, startedAt);
190207
await recordSandboxUsageBestEffort(await runtime.meteringContext());
191208
return result;
192209
} catch (error) {
193210
throw runtime.toUpstreamError(error, "Sandbox command failed.");
194211
}
195212
}
196213

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;
238+
}
239+
return environment;
240+
}
241+
197242
function packageManagerPolicyResult(
198243
command: string,
199244
manager: string,

0 commit comments

Comments
 (0)