@vm0/runner 2.8.4 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/index.js +406 -2788
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -12,6 +12,15 @@ import { dirname, join } from "path";
12
12
  import { z } from "zod";
13
13
  import fs from "fs";
14
14
  import yaml from "yaml";
15
+ var SANDBOX_DEFAULTS = {
16
+ max_concurrent: 1,
17
+ vcpu: 2,
18
+ memory_mb: 2048,
19
+ poll_interval_ms: 5e3
20
+ };
21
+ var PROXY_DEFAULTS = {
22
+ port: 8080
23
+ };
15
24
  var runnerConfigSchema = z.object({
16
25
  name: z.string().min(1, "Name is required"),
17
26
  group: z.string().regex(
@@ -23,58 +32,45 @@ var runnerConfigSchema = z.object({
23
32
  token: z.string().min(1, "Server token is required")
24
33
  }),
25
34
  sandbox: z.object({
26
- max_concurrent: z.number().int().min(1).default(1),
27
- vcpu: z.number().int().min(1).default(2),
28
- memory_mb: z.number().int().min(128).default(2048),
29
- poll_interval_ms: z.number().int().min(1e3).default(5e3)
30
- }).default({
31
- max_concurrent: 1,
32
- vcpu: 2,
33
- memory_mb: 2048,
34
- poll_interval_ms: 5e3
35
- }),
35
+ max_concurrent: z.number().int().min(1).default(SANDBOX_DEFAULTS.max_concurrent),
36
+ vcpu: z.number().int().min(1).default(SANDBOX_DEFAULTS.vcpu),
37
+ memory_mb: z.number().int().min(128).default(SANDBOX_DEFAULTS.memory_mb),
38
+ poll_interval_ms: z.number().int().min(1e3).default(SANDBOX_DEFAULTS.poll_interval_ms)
39
+ }).default(SANDBOX_DEFAULTS),
36
40
  firecracker: z.object({
37
41
  binary: z.string().min(1, "Firecracker binary path is required"),
38
42
  kernel: z.string().min(1, "Kernel path is required"),
39
43
  rootfs: z.string().min(1, "Rootfs path is required")
40
44
  }),
41
45
  proxy: z.object({
42
- port: z.number().int().min(1024).max(65535).default(8080)
43
- }).default({
44
- port: 8080
45
- })
46
+ port: z.number().int().min(1024).max(65535).default(PROXY_DEFAULTS.port)
47
+ }).default(PROXY_DEFAULTS)
46
48
  });
49
+ var DEBUG_SERVER_DEFAULTS = {
50
+ url: "http://localhost:3000",
51
+ token: "debug-token"
52
+ };
47
53
  var debugConfigSchema = z.object({
48
54
  name: z.string().default("debug-runner"),
49
55
  group: z.string().default("debug/local"),
50
56
  server: z.object({
51
- url: z.string().url().default("http://localhost:3000"),
52
- token: z.string().default("debug-token")
53
- }).default({
54
- url: "http://localhost:3000",
55
- token: "debug-token"
56
- }),
57
+ url: z.string().url().default(DEBUG_SERVER_DEFAULTS.url),
58
+ token: z.string().default(DEBUG_SERVER_DEFAULTS.token)
59
+ }).default(DEBUG_SERVER_DEFAULTS),
57
60
  sandbox: z.object({
58
- max_concurrent: z.number().int().min(1).default(1),
59
- vcpu: z.number().int().min(1).default(2),
60
- memory_mb: z.number().int().min(128).default(2048),
61
- poll_interval_ms: z.number().int().min(1e3).default(5e3)
62
- }).default({
63
- max_concurrent: 1,
64
- vcpu: 2,
65
- memory_mb: 2048,
66
- poll_interval_ms: 5e3
67
- }),
61
+ max_concurrent: z.number().int().min(1).default(SANDBOX_DEFAULTS.max_concurrent),
62
+ vcpu: z.number().int().min(1).default(SANDBOX_DEFAULTS.vcpu),
63
+ memory_mb: z.number().int().min(128).default(SANDBOX_DEFAULTS.memory_mb),
64
+ poll_interval_ms: z.number().int().min(1e3).default(SANDBOX_DEFAULTS.poll_interval_ms)
65
+ }).default(SANDBOX_DEFAULTS),
68
66
  firecracker: z.object({
69
67
  binary: z.string().min(1, "Firecracker binary path is required"),
70
68
  kernel: z.string().min(1, "Kernel path is required"),
71
69
  rootfs: z.string().min(1, "Rootfs path is required")
72
70
  }),
73
71
  proxy: z.object({
74
- port: z.number().int().min(1024).max(65535).default(8080)
75
- }).default({
76
- port: 8080
77
- })
72
+ port: z.number().int().min(1024).max(65535).default(PROXY_DEFAULTS.port)
73
+ }).default(PROXY_DEFAULTS)
78
74
  });
79
75
  function loadDebugConfig(configPath) {
80
76
  if (!fs.existsSync(configPath)) {
@@ -192,7 +188,6 @@ async function completeJob(apiUrl, context, exitCode, error) {
192
188
 
193
189
  // src/lib/executor.ts
194
190
  import path4 from "path";
195
- import fs6 from "fs";
196
191
 
197
192
  // src/lib/firecracker/vm.ts
198
193
  import { execSync as execSync2, spawn } from "child_process";
@@ -5020,15 +5015,33 @@ var volumeConfigSchema = z5.object({
5020
5015
  version: z5.string().min(1, "Volume version is required")
5021
5016
  });
5022
5017
  var SUPPORTED_APPS = ["github"];
5023
- var appStringSchema = z5.string().regex(
5024
- /^[a-z]+(:(?:latest|dev))?$/,
5025
- "App must be in format 'app' or 'app:tag' (e.g., 'github', 'github:dev')"
5026
- ).refine(
5027
- (val) => {
5028
- const [app] = val.split(":");
5029
- return SUPPORTED_APPS.includes(app);
5030
- },
5031
- `Unsupported app. Supported apps: ${SUPPORTED_APPS.join(", ")}`
5018
+ var SUPPORTED_APP_TAGS = ["latest", "dev"];
5019
+ var APP_NAME_REGEX = /^[a-z0-9-]+$/;
5020
+ var appStringSchema = z5.string().superRefine((val, ctx) => {
5021
+ const [appName, tag] = val.split(":");
5022
+ if (!appName || !APP_NAME_REGEX.test(appName)) {
5023
+ ctx.addIssue({
5024
+ code: z5.ZodIssueCode.custom,
5025
+ message: "App name must contain only lowercase letters, numbers, and hyphens"
5026
+ });
5027
+ return;
5028
+ }
5029
+ if (!SUPPORTED_APPS.includes(appName)) {
5030
+ ctx.addIssue({
5031
+ code: z5.ZodIssueCode.custom,
5032
+ message: `Invalid app: "${appName}". Supported apps: ${SUPPORTED_APPS.join(", ")}`
5033
+ });
5034
+ return;
5035
+ }
5036
+ if (tag !== void 0 && !SUPPORTED_APP_TAGS.includes(tag)) {
5037
+ ctx.addIssue({
5038
+ code: z5.ZodIssueCode.custom,
5039
+ message: `Invalid app tag: "${tag}". Supported tags: ${SUPPORTED_APP_TAGS.join(", ")}`
5040
+ });
5041
+ }
5042
+ });
5043
+ var nonEmptyStringArraySchema = z5.array(
5044
+ z5.string().min(1, "Array entries cannot be empty strings")
5032
5045
  );
5033
5046
  var agentDefinitionSchema = z5.object({
5034
5047
  description: z5.string().optional(),
@@ -5053,7 +5066,7 @@ var agentDefinitionSchema = z5.object({
5053
5066
  * Path to instructions file (e.g., AGENTS.md).
5054
5067
  * Auto-uploaded as volume and mounted at /home/user/.claude/CLAUDE.md
5055
5068
  */
5056
- instructions: z5.string().optional(),
5069
+ instructions: z5.string().min(1, "Instructions path cannot be empty").optional(),
5057
5070
  /**
5058
5071
  * Array of GitHub tree URLs for agent skills.
5059
5072
  * Each skill is auto-downloaded and mounted at /home/user/.claude/skills/{skillName}/
@@ -5074,7 +5087,17 @@ var agentDefinitionSchema = z5.object({
5074
5087
  * Requires experimental_runner to be configured.
5075
5088
  * When enabled, filters outbound traffic by domain/IP rules.
5076
5089
  */
5077
- experimental_firewall: experimentalFirewallSchema.optional()
5090
+ experimental_firewall: experimentalFirewallSchema.optional(),
5091
+ /**
5092
+ * Array of secret names to inject from the scope's secret store.
5093
+ * Each entry must be a non-empty string.
5094
+ */
5095
+ experimental_secrets: nonEmptyStringArraySchema.optional(),
5096
+ /**
5097
+ * Array of variable names to inject from the scope's variable store.
5098
+ * Each entry must be a non-empty string.
5099
+ */
5100
+ experimental_vars: nonEmptyStringArraySchema.optional()
5078
5101
  });
5079
5102
  var agentComposeContentSchema = z5.object({
5080
5103
  version: z5.string().min(1, "Version is required"),
@@ -7095,2766 +7118,266 @@ var publicVolumeDownloadContract = c15.router({
7095
7118
  }
7096
7119
  });
7097
7120
 
7098
- // ../../packages/core/src/sandbox/scripts/lib/__init__.py.ts
7099
- var INIT_SCRIPT = `#!/usr/bin/env python3
7100
- """
7101
- VM0 Agent Scripts Library
7121
+ // ../../packages/core/src/sandbox/scripts/dist/bundled.ts
7122
+ var RUN_AGENT_SCRIPT = '#!/usr/bin/env node\n\n// src/sandbox/scripts/src/run-agent.ts\nimport * as fs7 from "fs";\nimport { spawn, execSync as execSync4 } from "child_process";\nimport * as readline from "readline";\n\n// src/sandbox/scripts/src/lib/common.ts\nimport * as fs from "fs";\nvar RUN_ID = process.env.VM0_RUN_ID ?? "";\nvar API_URL = process.env.VM0_API_URL ?? "";\nvar API_TOKEN = process.env.VM0_API_TOKEN ?? "";\nvar PROMPT = process.env.VM0_PROMPT ?? "";\nvar VERCEL_BYPASS = process.env.VERCEL_PROTECTION_BYPASS ?? "";\nvar RESUME_SESSION_ID = process.env.VM0_RESUME_SESSION_ID ?? "";\nvar CLI_AGENT_TYPE = process.env.CLI_AGENT_TYPE ?? "claude-code";\nvar OPENAI_MODEL = process.env.OPENAI_MODEL ?? "";\nvar WORKING_DIR = process.env.VM0_WORKING_DIR ?? "";\nvar ARTIFACT_DRIVER = process.env.VM0_ARTIFACT_DRIVER ?? "";\nvar ARTIFACT_MOUNT_PATH = process.env.VM0_ARTIFACT_MOUNT_PATH ?? "";\nvar ARTIFACT_VOLUME_NAME = process.env.VM0_ARTIFACT_VOLUME_NAME ?? "";\nvar ARTIFACT_VERSION_ID = process.env.VM0_ARTIFACT_VERSION_ID ?? "";\nvar WEBHOOK_URL = `${API_URL}/api/webhooks/agent/events`;\nvar CHECKPOINT_URL = `${API_URL}/api/webhooks/agent/checkpoints`;\nvar COMPLETE_URL = `${API_URL}/api/webhooks/agent/complete`;\nvar HEARTBEAT_URL = `${API_URL}/api/webhooks/agent/heartbeat`;\nvar TELEMETRY_URL = `${API_URL}/api/webhooks/agent/telemetry`;\nvar PROXY_URL = `${API_URL}/api/webhooks/agent/proxy`;\nvar STORAGE_PREPARE_URL = `${API_URL}/api/webhooks/agent/storages/prepare`;\nvar STORAGE_COMMIT_URL = `${API_URL}/api/webhooks/agent/storages/commit`;\nvar HEARTBEAT_INTERVAL = 60;\nvar TELEMETRY_INTERVAL = 30;\nvar HTTP_CONNECT_TIMEOUT = 10;\nvar HTTP_MAX_TIME = 30;\nvar HTTP_MAX_TIME_UPLOAD = 60;\nvar HTTP_MAX_RETRIES = 3;\nvar SESSION_ID_FILE = `/tmp/vm0-session-${RUN_ID}.txt`;\nvar SESSION_HISTORY_PATH_FILE = `/tmp/vm0-session-history-${RUN_ID}.txt`;\nvar EVENT_ERROR_FLAG = `/tmp/vm0-event-error-${RUN_ID}`;\nvar SYSTEM_LOG_FILE = `/tmp/vm0-main-${RUN_ID}.log`;\nvar AGENT_LOG_FILE = `/tmp/vm0-agent-${RUN_ID}.log`;\nvar METRICS_LOG_FILE = `/tmp/vm0-metrics-${RUN_ID}.jsonl`;\nvar NETWORK_LOG_FILE = `/tmp/vm0-network-${RUN_ID}.jsonl`;\nvar TELEMETRY_LOG_POS_FILE = `/tmp/vm0-telemetry-log-pos-${RUN_ID}.txt`;\nvar TELEMETRY_METRICS_POS_FILE = `/tmp/vm0-telemetry-metrics-pos-${RUN_ID}.txt`;\nvar TELEMETRY_NETWORK_POS_FILE = `/tmp/vm0-telemetry-network-pos-${RUN_ID}.txt`;\nvar TELEMETRY_SANDBOX_OPS_POS_FILE = `/tmp/vm0-telemetry-sandbox-ops-pos-${RUN_ID}.txt`;\nvar SANDBOX_OPS_LOG_FILE = `/tmp/vm0-sandbox-ops-${RUN_ID}.jsonl`;\nvar METRICS_INTERVAL = 5;\nfunction validateConfig() {\n if (!WORKING_DIR) {\n throw new Error("VM0_WORKING_DIR is required but not set");\n }\n return true;\n}\nfunction recordSandboxOp(actionType, durationMs, success, error) {\n const entry = {\n ts: (/* @__PURE__ */ new Date()).toISOString(),\n action_type: actionType,\n duration_ms: durationMs,\n success\n };\n if (error) {\n entry.error = error;\n }\n fs.appendFileSync(SANDBOX_OPS_LOG_FILE, JSON.stringify(entry) + "\\n");\n}\n\n// src/sandbox/scripts/src/lib/log.ts\nvar SCRIPT_NAME = process.env.LOG_SCRIPT_NAME ?? "run-agent";\nvar DEBUG_MODE = process.env.VM0_DEBUG === "1";\nfunction timestamp() {\n return (/* @__PURE__ */ new Date()).toISOString().replace(/\\.\\d{3}Z$/, "Z");\n}\nfunction logInfo(msg) {\n console.error(`[${timestamp()}] [INFO] [sandbox:${SCRIPT_NAME}] ${msg}`);\n}\nfunction logWarn(msg) {\n console.error(`[${timestamp()}] [WARN] [sandbox:${SCRIPT_NAME}] ${msg}`);\n}\nfunction logError(msg) {\n console.error(`[${timestamp()}] [ERROR] [sandbox:${SCRIPT_NAME}] ${msg}`);\n}\nfunction logDebug(msg) {\n if (DEBUG_MODE) {\n console.error(`[${timestamp()}] [DEBUG] [sandbox:${SCRIPT_NAME}] ${msg}`);\n }\n}\n\n// src/sandbox/scripts/src/lib/events.ts\nimport * as fs2 from "fs";\n\n// src/sandbox/scripts/src/lib/http-client.ts\nimport { execSync } from "child_process";\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\nasync function httpPostJson(url, data, maxRetries = HTTP_MAX_RETRIES) {\n const headers = {\n "Content-Type": "application/json",\n Authorization: `Bearer ${API_TOKEN}`\n };\n if (VERCEL_BYPASS) {\n headers["x-vercel-protection-bypass"] = VERCEL_BYPASS;\n }\n for (let attempt = 1; attempt <= maxRetries; attempt++) {\n logDebug(`HTTP POST attempt ${attempt}/${maxRetries} to ${url}`);\n try {\n const controller = new AbortController();\n const timeoutId = setTimeout(\n () => controller.abort(),\n HTTP_MAX_TIME * 1e3\n );\n const response = await fetch(url, {\n method: "POST",\n headers,\n body: JSON.stringify(data),\n signal: controller.signal\n });\n clearTimeout(timeoutId);\n if (response.ok) {\n const text = await response.text();\n if (text) {\n return JSON.parse(text);\n }\n return {};\n }\n logWarn(\n `HTTP POST failed (attempt ${attempt}/${maxRetries}): HTTP ${response.status}`\n );\n if (attempt < maxRetries) {\n await sleep(1e3);\n }\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : String(error);\n if (errorMsg.includes("abort")) {\n logWarn(`HTTP POST failed (attempt ${attempt}/${maxRetries}): Timeout`);\n } else {\n logWarn(\n `HTTP POST failed (attempt ${attempt}/${maxRetries}): ${errorMsg}`\n );\n }\n if (attempt < maxRetries) {\n await sleep(1e3);\n }\n }\n }\n logError(`HTTP POST failed after ${maxRetries} attempts to ${url}`);\n return null;\n}\nasync function httpPutPresigned(presignedUrl, filePath, contentType = "application/octet-stream", maxRetries = HTTP_MAX_RETRIES) {\n for (let attempt = 1; attempt <= maxRetries; attempt++) {\n logDebug(`HTTP PUT presigned attempt ${attempt}/${maxRetries}`);\n try {\n const curlCmd = [\n "curl",\n "-f",\n "-X",\n "PUT",\n "-H",\n `Content-Type: ${contentType}`,\n "--data-binary",\n `@${filePath}`,\n "--connect-timeout",\n String(HTTP_CONNECT_TIMEOUT),\n "--max-time",\n String(HTTP_MAX_TIME_UPLOAD),\n "--silent",\n `"${presignedUrl}"`\n ].join(" ");\n execSync(curlCmd, {\n timeout: HTTP_MAX_TIME_UPLOAD * 1e3,\n stdio: ["pipe", "pipe", "pipe"]\n });\n return true;\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : String(error);\n if (errorMsg.includes("ETIMEDOUT") || errorMsg.includes("timeout")) {\n logWarn(\n `HTTP PUT presigned failed (attempt ${attempt}/${maxRetries}): Timeout`\n );\n } else {\n logWarn(\n `HTTP PUT presigned failed (attempt ${attempt}/${maxRetries}): ${errorMsg}`\n );\n }\n if (attempt < maxRetries) {\n await sleep(1e3);\n }\n }\n }\n logError(`HTTP PUT presigned failed after ${maxRetries} attempts`);\n return false;\n}\n\n// src/sandbox/scripts/src/lib/secret-masker.ts\nvar MASK_PLACEHOLDER = "***";\nvar MIN_SECRET_LENGTH = 5;\nvar _masker = null;\nvar SecretMasker = class {\n patterns;\n /**\n * Initialize masker with secret values.\n *\n * @param secretValues - List of secret values to mask\n */\n constructor(secretValues) {\n this.patterns = /* @__PURE__ */ new Set();\n for (const secret of secretValues) {\n if (!secret || secret.length < MIN_SECRET_LENGTH) {\n continue;\n }\n this.patterns.add(secret);\n try {\n const b64 = Buffer.from(secret).toString("base64");\n if (b64.length >= MIN_SECRET_LENGTH) {\n this.patterns.add(b64);\n }\n } catch {\n }\n try {\n const urlEnc = encodeURIComponent(secret);\n if (urlEnc !== secret && urlEnc.length >= MIN_SECRET_LENGTH) {\n this.patterns.add(urlEnc);\n }\n } catch {\n }\n }\n }\n /**\n * Recursively mask all occurrences of secrets in the data.\n *\n * @param data - Data to mask (string, list, dict, or primitive)\n * @returns Masked data with the same structure\n */\n mask(data) {\n return this.deepMask(data);\n }\n deepMask(data) {\n if (typeof data === "string") {\n let result = data;\n for (const pattern of this.patterns) {\n result = result.split(pattern).join(MASK_PLACEHOLDER);\n }\n return result;\n }\n if (Array.isArray(data)) {\n return data.map((item) => this.deepMask(item));\n }\n if (data !== null && typeof data === "object") {\n const result = {};\n for (const [key, value] of Object.entries(\n data\n )) {\n result[key] = this.deepMask(value);\n }\n return result;\n }\n return data;\n }\n};\nfunction createMasker() {\n const secretValuesStr = process.env.VM0_SECRET_VALUES ?? "";\n if (!secretValuesStr) {\n return new SecretMasker([]);\n }\n const secretValues = [];\n for (const encodedValue of secretValuesStr.split(",")) {\n const trimmed = encodedValue.trim();\n if (trimmed) {\n try {\n const decoded = Buffer.from(trimmed, "base64").toString("utf-8");\n if (decoded) {\n secretValues.push(decoded);\n }\n } catch {\n }\n }\n }\n return new SecretMasker(secretValues);\n}\nfunction getMasker() {\n if (_masker === null) {\n _masker = createMasker();\n }\n return _masker;\n}\nfunction maskData(data) {\n return getMasker().mask(data);\n}\n\n// src/sandbox/scripts/src/lib/events.ts\nasync function sendEvent(event, sequenceNumber) {\n const eventType = event.type ?? "";\n const eventSubtype = event.subtype ?? "";\n let sessionId = null;\n if (CLI_AGENT_TYPE === "codex") {\n if (eventType === "thread.started") {\n sessionId = event.thread_id ?? "";\n }\n } else {\n if (eventType === "system" && eventSubtype === "init") {\n sessionId = event.session_id ?? "";\n }\n }\n if (sessionId && !fs2.existsSync(SESSION_ID_FILE)) {\n logInfo(`Captured session ID: ${sessionId}`);\n fs2.writeFileSync(SESSION_ID_FILE, sessionId);\n const homeDir = process.env.HOME ?? "/home/user";\n let sessionHistoryPath;\n if (CLI_AGENT_TYPE === "codex") {\n const codexHome = process.env.CODEX_HOME ?? `${homeDir}/.codex`;\n sessionHistoryPath = `CODEX_SEARCH:${codexHome}/sessions:${sessionId}`;\n } else {\n const projectName = WORKING_DIR.replace(/^\\//, "").replace(/\\//g, "-");\n sessionHistoryPath = `${homeDir}/.claude/projects/-${projectName}/${sessionId}.jsonl`;\n }\n fs2.writeFileSync(SESSION_HISTORY_PATH_FILE, sessionHistoryPath);\n logInfo(`Session history will be at: ${sessionHistoryPath}`);\n }\n const eventWithSequence = {\n ...event,\n sequenceNumber\n };\n const maskedEvent = maskData(eventWithSequence);\n const payload = {\n runId: RUN_ID,\n events: [maskedEvent]\n };\n const result = await httpPostJson(WEBHOOK_URL, payload);\n if (result === null) {\n logError("Failed to send event after retries");\n fs2.writeFileSync(EVENT_ERROR_FLAG, "1");\n return false;\n }\n return true;\n}\n\n// src/sandbox/scripts/src/lib/checkpoint.ts\nimport * as fs4 from "fs";\nimport * as path2 from "path";\n\n// src/sandbox/scripts/src/lib/direct-upload.ts\nimport * as fs3 from "fs";\nimport * as path from "path";\nimport * as crypto from "crypto";\nimport { execSync as execSync2 } from "child_process";\nfunction computeFileHash(filePath) {\n const hash = crypto.createHash("sha256");\n const buffer = fs3.readFileSync(filePath);\n hash.update(buffer);\n return hash.digest("hex");\n}\nfunction collectFileMetadata(dirPath) {\n const files = [];\n function walkDir(currentPath, relativePath) {\n const items = fs3.readdirSync(currentPath);\n for (const item of items) {\n if (item === ".git" || item === ".vm0") {\n continue;\n }\n const fullPath = path.join(currentPath, item);\n const relPath = relativePath ? path.join(relativePath, item) : item;\n const stat = fs3.statSync(fullPath);\n if (stat.isDirectory()) {\n walkDir(fullPath, relPath);\n } else if (stat.isFile()) {\n try {\n const fileHash = computeFileHash(fullPath);\n files.push({\n path: relPath,\n hash: fileHash,\n size: stat.size\n });\n } catch (error) {\n logWarn(`Could not process file ${relPath}: ${error}`);\n }\n }\n }\n }\n walkDir(dirPath, "");\n return files;\n}\nfunction createArchive(dirPath, tarPath) {\n try {\n execSync2(\n `tar -czf "${tarPath}" --exclude=\'.git\' --exclude=\'.vm0\' -C "${dirPath}" .`,\n { stdio: ["pipe", "pipe", "pipe"] }\n );\n return true;\n } catch (error) {\n logError(`Failed to create archive: ${error}`);\n return false;\n }\n}\nfunction createManifest(files, manifestPath) {\n try {\n const manifest = {\n version: 1,\n files,\n createdAt: (/* @__PURE__ */ new Date()).toISOString()\n };\n fs3.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));\n return true;\n } catch (error) {\n logError(`Failed to create manifest: ${error}`);\n return false;\n }\n}\nasync function createDirectUploadSnapshot(mountPath, storageName, storageType = "artifact", runId, message) {\n logInfo(\n `Creating direct upload snapshot for \'${storageName}\' (type: ${storageType})`\n );\n logInfo("Computing file hashes...");\n const hashStart = Date.now();\n const files = collectFileMetadata(mountPath);\n recordSandboxOp("artifact_hash_compute", Date.now() - hashStart, true);\n logInfo(`Found ${files.length} files`);\n if (files.length === 0) {\n logInfo("No files to upload, creating empty version");\n }\n logInfo("Calling prepare endpoint...");\n const prepareStart = Date.now();\n const preparePayload = {\n storageName,\n storageType,\n files\n };\n if (runId) {\n preparePayload.runId = runId;\n }\n const prepareResponse = await httpPostJson(\n STORAGE_PREPARE_URL,\n preparePayload\n );\n if (!prepareResponse) {\n logError("Failed to call prepare endpoint");\n recordSandboxOp("artifact_prepare_api", Date.now() - prepareStart, false);\n return null;\n }\n const versionId = prepareResponse.versionId;\n if (!versionId) {\n logError(`Invalid prepare response: ${JSON.stringify(prepareResponse)}`);\n recordSandboxOp("artifact_prepare_api", Date.now() - prepareStart, false);\n return null;\n }\n recordSandboxOp("artifact_prepare_api", Date.now() - prepareStart, true);\n if (prepareResponse.existing) {\n logInfo(`Version already exists (deduplicated): ${versionId.slice(0, 8)}`);\n logInfo("Updating HEAD pointer...");\n const commitPayload = {\n storageName,\n storageType,\n versionId,\n files\n };\n if (runId) {\n commitPayload.runId = runId;\n }\n const commitResponse = await httpPostJson(\n STORAGE_COMMIT_URL,\n commitPayload\n );\n if (!commitResponse || !commitResponse.success) {\n logError(`Failed to update HEAD: ${JSON.stringify(commitResponse)}`);\n return null;\n }\n return { versionId, deduplicated: true };\n }\n const uploads = prepareResponse.uploads;\n if (!uploads) {\n logError("No upload URLs in prepare response");\n return null;\n }\n const archiveInfo = uploads.archive;\n const manifestInfo = uploads.manifest;\n if (!archiveInfo || !manifestInfo) {\n logError("Missing archive or manifest upload info");\n return null;\n }\n const tempDir = fs3.mkdtempSync(`/tmp/direct-upload-${storageName}-`);\n try {\n logInfo("Creating archive...");\n const archiveStart = Date.now();\n const archivePath = path.join(tempDir, "archive.tar.gz");\n if (!createArchive(mountPath, archivePath)) {\n logError("Failed to create archive");\n recordSandboxOp(\n "artifact_archive_create",\n Date.now() - archiveStart,\n false\n );\n return null;\n }\n recordSandboxOp("artifact_archive_create", Date.now() - archiveStart, true);\n logInfo("Creating manifest...");\n const manifestPath = path.join(tempDir, "manifest.json");\n if (!createManifest(files, manifestPath)) {\n logError("Failed to create manifest");\n return null;\n }\n logInfo("Uploading archive to S3...");\n const s3UploadStart = Date.now();\n if (!await httpPutPresigned(\n archiveInfo.presignedUrl,\n archivePath,\n "application/gzip"\n )) {\n logError("Failed to upload archive to S3");\n recordSandboxOp("artifact_s3_upload", Date.now() - s3UploadStart, false);\n return null;\n }\n logInfo("Uploading manifest to S3...");\n if (!await httpPutPresigned(\n manifestInfo.presignedUrl,\n manifestPath,\n "application/json"\n )) {\n logError("Failed to upload manifest to S3");\n recordSandboxOp("artifact_s3_upload", Date.now() - s3UploadStart, false);\n return null;\n }\n recordSandboxOp("artifact_s3_upload", Date.now() - s3UploadStart, true);\n logInfo("Calling commit endpoint...");\n const commitStart = Date.now();\n const commitPayload = {\n storageName,\n storageType,\n versionId,\n files\n };\n if (runId) {\n commitPayload.runId = runId;\n }\n if (message) {\n commitPayload.message = message;\n }\n const commitResponse = await httpPostJson(\n STORAGE_COMMIT_URL,\n commitPayload\n );\n if (!commitResponse) {\n logError("Failed to call commit endpoint");\n recordSandboxOp("artifact_commit_api", Date.now() - commitStart, false);\n return null;\n }\n if (!commitResponse.success) {\n logError(`Commit failed: ${JSON.stringify(commitResponse)}`);\n recordSandboxOp("artifact_commit_api", Date.now() - commitStart, false);\n return null;\n }\n recordSandboxOp("artifact_commit_api", Date.now() - commitStart, true);\n logInfo(`Direct upload snapshot created: ${versionId.slice(0, 8)}`);\n return { versionId };\n } finally {\n try {\n fs3.rmSync(tempDir, { recursive: true, force: true });\n } catch {\n }\n }\n}\n\n// src/sandbox/scripts/src/lib/checkpoint.ts\nfunction findJsonlFiles(dir) {\n const files = [];\n function walk(currentDir) {\n try {\n const items = fs4.readdirSync(currentDir);\n for (const item of items) {\n const fullPath = path2.join(currentDir, item);\n const stat = fs4.statSync(fullPath);\n if (stat.isDirectory()) {\n walk(fullPath);\n } else if (item.endsWith(".jsonl")) {\n files.push(fullPath);\n }\n }\n } catch {\n }\n }\n walk(dir);\n return files;\n}\nfunction findCodexSessionFile(sessionsDir, sessionId) {\n const files = findJsonlFiles(sessionsDir);\n logInfo(`Searching for Codex session ${sessionId} in ${files.length} files`);\n for (const filepath of files) {\n const filename = path2.basename(filepath);\n if (filename.includes(sessionId) || filename.replace(/-/g, "").includes(sessionId.replace(/-/g, ""))) {\n logInfo(`Found Codex session file: ${filepath}`);\n return filepath;\n }\n }\n if (files.length > 0) {\n files.sort((a, b) => {\n const statA = fs4.statSync(a);\n const statB = fs4.statSync(b);\n return statB.mtimeMs - statA.mtimeMs;\n });\n const mostRecent = files[0] ?? null;\n if (mostRecent) {\n logInfo(\n `Session ID not found in filenames, using most recent: ${mostRecent}`\n );\n }\n return mostRecent;\n }\n return null;\n}\nasync function createCheckpoint() {\n const checkpointStart = Date.now();\n logInfo("Creating checkpoint...");\n const sessionIdStart = Date.now();\n if (!fs4.existsSync(SESSION_ID_FILE)) {\n logError("No session ID found, checkpoint creation failed");\n recordSandboxOp(\n "session_id_read",\n Date.now() - sessionIdStart,\n false,\n "Session ID file not found"\n );\n recordSandboxOp("checkpoint_total", Date.now() - checkpointStart, false);\n return false;\n }\n const cliAgentSessionId = fs4.readFileSync(SESSION_ID_FILE, "utf-8").trim();\n recordSandboxOp("session_id_read", Date.now() - sessionIdStart, true);\n const sessionHistoryStart = Date.now();\n if (!fs4.existsSync(SESSION_HISTORY_PATH_FILE)) {\n logError("No session history path found, checkpoint creation failed");\n recordSandboxOp(\n "session_history_read",\n Date.now() - sessionHistoryStart,\n false,\n "Session history path file not found"\n );\n recordSandboxOp("checkpoint_total", Date.now() - checkpointStart, false);\n return false;\n }\n const sessionHistoryPathRaw = fs4.readFileSync(SESSION_HISTORY_PATH_FILE, "utf-8").trim();\n let sessionHistoryPath;\n if (sessionHistoryPathRaw.startsWith("CODEX_SEARCH:")) {\n const parts = sessionHistoryPathRaw.split(":");\n if (parts.length !== 3) {\n logError(`Invalid Codex search marker format: ${sessionHistoryPathRaw}`);\n recordSandboxOp(\n "session_history_read",\n Date.now() - sessionHistoryStart,\n false,\n "Invalid Codex search marker"\n );\n recordSandboxOp("checkpoint_total", Date.now() - checkpointStart, false);\n return false;\n }\n const sessionsDir = parts[1] ?? "";\n const codexSessionId = parts[2] ?? "";\n logInfo(`Searching for Codex session in ${sessionsDir}`);\n const foundPath = findCodexSessionFile(sessionsDir, codexSessionId);\n if (!foundPath) {\n logError(\n `Could not find Codex session file for ${codexSessionId} in ${sessionsDir}`\n );\n recordSandboxOp(\n "session_history_read",\n Date.now() - sessionHistoryStart,\n false,\n "Codex session file not found"\n );\n recordSandboxOp("checkpoint_total", Date.now() - checkpointStart, false);\n return false;\n }\n sessionHistoryPath = foundPath;\n } else {\n sessionHistoryPath = sessionHistoryPathRaw;\n }\n if (!fs4.existsSync(sessionHistoryPath)) {\n logError(\n `Session history file not found at ${sessionHistoryPath}, checkpoint creation failed`\n );\n recordSandboxOp(\n "session_history_read",\n Date.now() - sessionHistoryStart,\n false,\n "Session history file not found"\n );\n recordSandboxOp("checkpoint_total", Date.now() - checkpointStart, false);\n return false;\n }\n let cliAgentSessionHistory;\n try {\n cliAgentSessionHistory = fs4.readFileSync(sessionHistoryPath, "utf-8");\n } catch (error) {\n logError(`Failed to read session history: ${error}`);\n recordSandboxOp(\n "session_history_read",\n Date.now() - sessionHistoryStart,\n false,\n String(error)\n );\n recordSandboxOp("checkpoint_total", Date.now() - checkpointStart, false);\n return false;\n }\n if (!cliAgentSessionHistory.trim()) {\n logError("Session history is empty, checkpoint creation failed");\n recordSandboxOp(\n "session_history_read",\n Date.now() - sessionHistoryStart,\n false,\n "Session history empty"\n );\n recordSandboxOp("checkpoint_total", Date.now() - checkpointStart, false);\n return false;\n }\n const lineCount = cliAgentSessionHistory.trim().split("\\n").length;\n logInfo(`Session history loaded (${lineCount} lines)`);\n recordSandboxOp(\n "session_history_read",\n Date.now() - sessionHistoryStart,\n true\n );\n let artifactSnapshot = null;\n if (ARTIFACT_DRIVER && ARTIFACT_VOLUME_NAME) {\n logInfo(`Processing artifact with driver: ${ARTIFACT_DRIVER}`);\n if (ARTIFACT_DRIVER !== "vas") {\n logError(\n `Unknown artifact driver: ${ARTIFACT_DRIVER} (only \'vas\' is supported)`\n );\n recordSandboxOp("checkpoint_total", Date.now() - checkpointStart, false);\n return false;\n }\n logInfo(\n `Creating VAS snapshot for artifact \'${ARTIFACT_VOLUME_NAME}\' at ${ARTIFACT_MOUNT_PATH}`\n );\n logInfo("Using direct S3 upload...");\n const snapshot = await createDirectUploadSnapshot(\n ARTIFACT_MOUNT_PATH,\n ARTIFACT_VOLUME_NAME,\n "artifact",\n RUN_ID,\n `Checkpoint from run ${RUN_ID}`\n );\n if (!snapshot) {\n logError("Failed to create VAS snapshot for artifact");\n recordSandboxOp("checkpoint_total", Date.now() - checkpointStart, false);\n return false;\n }\n const artifactVersion = snapshot.versionId;\n if (!artifactVersion) {\n logError("Failed to extract versionId from snapshot");\n recordSandboxOp("checkpoint_total", Date.now() - checkpointStart, false);\n return false;\n }\n artifactSnapshot = {\n artifactName: ARTIFACT_VOLUME_NAME,\n artifactVersion\n };\n logInfo(\n `VAS artifact snapshot created: ${ARTIFACT_VOLUME_NAME}@${artifactVersion}`\n );\n } else {\n logInfo(\n "No artifact configured, creating checkpoint without artifact snapshot"\n );\n }\n logInfo("Calling checkpoint API...");\n const checkpointPayload = {\n runId: RUN_ID,\n cliAgentType: CLI_AGENT_TYPE,\n cliAgentSessionId,\n cliAgentSessionHistory\n };\n if (artifactSnapshot) {\n checkpointPayload.artifactSnapshot = artifactSnapshot;\n }\n const apiCallStart = Date.now();\n const result = await httpPostJson(\n CHECKPOINT_URL,\n checkpointPayload\n );\n if (result && result.checkpointId) {\n const checkpointId = result.checkpointId;\n logInfo(`Checkpoint created successfully: ${checkpointId}`);\n recordSandboxOp("checkpoint_api_call", Date.now() - apiCallStart, true);\n recordSandboxOp("checkpoint_total", Date.now() - checkpointStart, true);\n return true;\n } else {\n logError(\n `Checkpoint API returned invalid response: ${JSON.stringify(result)}`\n );\n recordSandboxOp(\n "checkpoint_api_call",\n Date.now() - apiCallStart,\n false,\n "Invalid API response"\n );\n recordSandboxOp("checkpoint_total", Date.now() - checkpointStart, false);\n return false;\n }\n}\n\n// src/sandbox/scripts/src/lib/metrics.ts\nimport * as fs5 from "fs";\nimport { execSync as execSync3 } from "child_process";\nvar shutdownRequested = false;\nfunction getCpuPercent() {\n try {\n const content = fs5.readFileSync("/proc/stat", "utf-8");\n const line = content.split("\\n")[0];\n if (!line) {\n return 0;\n }\n const parts = line.split(/\\s+/);\n if (parts[0] !== "cpu") {\n return 0;\n }\n const values = parts.slice(1).map((x) => parseInt(x, 10));\n const idleVal = values[3];\n const iowaitVal = values[4];\n if (idleVal === void 0 || iowaitVal === void 0) {\n return 0;\n }\n const idle = idleVal + iowaitVal;\n const total = values.reduce((a, b) => a + b, 0);\n if (total === 0) {\n return 0;\n }\n const cpuPercent = 100 * (1 - idle / total);\n return Math.round(cpuPercent * 100) / 100;\n } catch (error) {\n logDebug(`Failed to get CPU percent: ${error}`);\n return 0;\n }\n}\nfunction getMemoryInfo() {\n try {\n const result = execSync3("free -b", {\n encoding: "utf-8",\n timeout: 5e3,\n stdio: ["pipe", "pipe", "pipe"]\n });\n const lines = result.trim().split("\\n");\n for (const line of lines) {\n if (line.startsWith("Mem:")) {\n const parts = line.split(/\\s+/);\n const totalStr = parts[1];\n const usedStr = parts[2];\n if (!totalStr || !usedStr) {\n return [0, 0];\n }\n const total = parseInt(totalStr, 10);\n const used = parseInt(usedStr, 10);\n return [used, total];\n }\n }\n return [0, 0];\n } catch (error) {\n logDebug(`Failed to get memory info: ${error}`);\n return [0, 0];\n }\n}\nfunction getDiskInfo() {\n try {\n const result = execSync3("df -B1 /", {\n encoding: "utf-8",\n timeout: 5e3,\n stdio: ["pipe", "pipe", "pipe"]\n });\n const lines = result.trim().split("\\n");\n if (lines.length < 2) {\n return [0, 0];\n }\n const dataLine = lines[1];\n if (!dataLine) {\n return [0, 0];\n }\n const parts = dataLine.split(/\\s+/);\n const totalStr = parts[1];\n const usedStr = parts[2];\n if (!totalStr || !usedStr) {\n return [0, 0];\n }\n const total = parseInt(totalStr, 10);\n const used = parseInt(usedStr, 10);\n return [used, total];\n } catch (error) {\n logDebug(`Failed to get disk info: ${error}`);\n return [0, 0];\n }\n}\nfunction collectMetrics() {\n const cpu = getCpuPercent();\n const [memUsed, memTotal] = getMemoryInfo();\n const [diskUsed, diskTotal] = getDiskInfo();\n return {\n ts: (/* @__PURE__ */ new Date()).toISOString(),\n cpu,\n mem_used: memUsed,\n mem_total: memTotal,\n disk_used: diskUsed,\n disk_total: diskTotal\n };\n}\nfunction metricsCollectorLoop() {\n logInfo(`Metrics collector started, writing to ${METRICS_LOG_FILE}`);\n const writeMetrics = () => {\n if (shutdownRequested) {\n logInfo("Metrics collector stopped");\n return;\n }\n try {\n const metrics = collectMetrics();\n fs5.appendFileSync(METRICS_LOG_FILE, JSON.stringify(metrics) + "\\n");\n logDebug(\n `Metrics collected: cpu=${metrics.cpu}%, mem=${metrics.mem_used}/${metrics.mem_total}`\n );\n } catch (error) {\n logError(`Failed to collect/write metrics: ${error}`);\n }\n setTimeout(writeMetrics, METRICS_INTERVAL * 1e3);\n };\n writeMetrics();\n}\nfunction startMetricsCollector() {\n shutdownRequested = false;\n setTimeout(metricsCollectorLoop, 0);\n}\nfunction stopMetricsCollector() {\n shutdownRequested = true;\n}\n\n// src/sandbox/scripts/src/lib/upload-telemetry.ts\nimport * as fs6 from "fs";\nvar shutdownRequested2 = false;\nfunction readFileFromPosition(filePath, posFile) {\n let lastPos = 0;\n if (fs6.existsSync(posFile)) {\n try {\n const content = fs6.readFileSync(posFile, "utf-8").trim();\n lastPos = parseInt(content, 10) || 0;\n } catch {\n lastPos = 0;\n }\n }\n let newContent = "";\n let newPos = lastPos;\n if (fs6.existsSync(filePath)) {\n try {\n const fd = fs6.openSync(filePath, "r");\n const stats = fs6.fstatSync(fd);\n const bufferSize = stats.size - lastPos;\n if (bufferSize > 0) {\n const buffer = Buffer.alloc(bufferSize);\n fs6.readSync(fd, buffer, 0, bufferSize, lastPos);\n newContent = buffer.toString("utf-8");\n newPos = stats.size;\n }\n fs6.closeSync(fd);\n } catch (error) {\n logDebug(`Failed to read ${filePath}: ${error}`);\n }\n }\n return [newContent, newPos];\n}\nfunction savePosition(posFile, position) {\n try {\n fs6.writeFileSync(posFile, String(position));\n } catch (error) {\n logDebug(`Failed to save position to ${posFile}: ${error}`);\n }\n}\nfunction readJsonlFromPosition(filePath, posFile) {\n const [content, newPos] = readFileFromPosition(filePath, posFile);\n const entries = [];\n if (content) {\n for (const line of content.trim().split("\\n")) {\n if (line) {\n try {\n entries.push(JSON.parse(line));\n } catch {\n }\n }\n }\n }\n return [entries, newPos];\n}\nfunction readMetricsFromPosition(posFile) {\n return readJsonlFromPosition(METRICS_LOG_FILE, posFile);\n}\nfunction readNetworkLogsFromPosition(posFile) {\n return readJsonlFromPosition(NETWORK_LOG_FILE, posFile);\n}\nfunction readSandboxOpsFromPosition(posFile) {\n return readJsonlFromPosition(SANDBOX_OPS_LOG_FILE, posFile);\n}\nasync function uploadTelemetry() {\n const [systemLog, logPos] = readFileFromPosition(\n SYSTEM_LOG_FILE,\n TELEMETRY_LOG_POS_FILE\n );\n const [metrics, metricsPos] = readMetricsFromPosition(\n TELEMETRY_METRICS_POS_FILE\n );\n const [networkLogs, networkPos] = readNetworkLogsFromPosition(\n TELEMETRY_NETWORK_POS_FILE\n );\n const [sandboxOps, sandboxOpsPos] = readSandboxOpsFromPosition(\n TELEMETRY_SANDBOX_OPS_POS_FILE\n );\n if (!systemLog && metrics.length === 0 && networkLogs.length === 0 && sandboxOps.length === 0) {\n logDebug("No new telemetry data to upload");\n return true;\n }\n const maskedSystemLog = systemLog ? maskData(systemLog) : "";\n const maskedNetworkLogs = networkLogs.length > 0 ? maskData(networkLogs) : [];\n const payload = {\n runId: RUN_ID,\n systemLog: maskedSystemLog,\n metrics,\n // Metrics don\'t contain secrets (just numbers)\n networkLogs: maskedNetworkLogs,\n sandboxOperations: sandboxOps\n // Sandbox ops don\'t contain secrets (just timing data)\n };\n logDebug(\n `Uploading telemetry: ${systemLog.length} bytes log, ${metrics.length} metrics, ${networkLogs.length} network logs, ${sandboxOps.length} sandbox ops`\n );\n const result = await httpPostJson(TELEMETRY_URL, payload, 1);\n if (result) {\n savePosition(TELEMETRY_LOG_POS_FILE, logPos);\n savePosition(TELEMETRY_METRICS_POS_FILE, metricsPos);\n savePosition(TELEMETRY_NETWORK_POS_FILE, networkPos);\n savePosition(TELEMETRY_SANDBOX_OPS_POS_FILE, sandboxOpsPos);\n logDebug(\n `Telemetry uploaded successfully: ${result.id ?? "unknown"}`\n );\n return true;\n } else {\n logWarn("Failed to upload telemetry (will retry next interval)");\n return false;\n }\n}\nasync function telemetryUploadLoop() {\n logInfo(`Telemetry upload started (interval: ${TELEMETRY_INTERVAL}s)`);\n const runUpload = async () => {\n if (shutdownRequested2) {\n logInfo("Telemetry upload stopped");\n return;\n }\n try {\n await uploadTelemetry();\n } catch (error) {\n logError(`Telemetry upload error: ${error}`);\n }\n setTimeout(() => void runUpload(), TELEMETRY_INTERVAL * 1e3);\n };\n await runUpload();\n}\nfunction startTelemetryUpload() {\n shutdownRequested2 = false;\n setTimeout(() => void telemetryUploadLoop(), 0);\n}\nfunction stopTelemetryUpload() {\n shutdownRequested2 = true;\n}\nasync function finalTelemetryUpload() {\n logInfo("Performing final telemetry upload...");\n return uploadTelemetry();\n}\n\n// src/sandbox/scripts/src/run-agent.ts\nvar shutdownRequested3 = false;\nfunction heartbeatLoop() {\n const sendHeartbeat = async () => {\n if (shutdownRequested3) {\n return;\n }\n try {\n if (await httpPostJson(HEARTBEAT_URL, { runId: RUN_ID })) {\n logInfo("Heartbeat sent");\n } else {\n logWarn("Heartbeat failed");\n }\n } catch (error) {\n logWarn(`Heartbeat error: ${error}`);\n }\n setTimeout(() => {\n sendHeartbeat().catch(() => {\n });\n }, HEARTBEAT_INTERVAL * 1e3);\n };\n sendHeartbeat().catch(() => {\n });\n}\nasync function cleanup(exitCode, errorMessage) {\n logInfo("\\u25B7 Cleanup");\n const telemetryStart = Date.now();\n let telemetrySuccess = true;\n try {\n await finalTelemetryUpload();\n } catch (error) {\n telemetrySuccess = false;\n logError(`Final telemetry upload failed: ${error}`);\n }\n recordSandboxOp(\n "final_telemetry_upload",\n Date.now() - telemetryStart,\n telemetrySuccess\n );\n logInfo(`Calling complete API with exitCode=${exitCode}`);\n const completePayload = {\n runId: RUN_ID,\n exitCode\n };\n if (errorMessage) {\n completePayload.error = errorMessage;\n }\n const completeStart = Date.now();\n let completeSuccess = false;\n try {\n if (await httpPostJson(COMPLETE_URL, completePayload)) {\n logInfo("Complete API called successfully");\n completeSuccess = true;\n } else {\n logError("Failed to call complete API (sandbox may not be cleaned up)");\n }\n } catch (error) {\n logError(`Complete API call failed: ${error}`);\n }\n recordSandboxOp(\n "complete_api_call",\n Date.now() - completeStart,\n completeSuccess\n );\n shutdownRequested3 = true;\n stopMetricsCollector();\n stopTelemetryUpload();\n logInfo("Background processes stopped");\n if (exitCode === 0) {\n logInfo("\\u2713 Sandbox finished successfully");\n } else {\n logInfo(`\\u2717 Sandbox failed (exit code ${exitCode})`);\n }\n}\nasync function run() {\n validateConfig();\n logInfo(`\\u25B6 VM0 Sandbox ${RUN_ID}`);\n logInfo("\\u25B7 Initialization");\n const initStartTime = Date.now();\n logInfo(`Working directory: ${WORKING_DIR}`);\n const heartbeatStart = Date.now();\n heartbeatLoop();\n logInfo("Heartbeat started");\n recordSandboxOp("heartbeat_start", Date.now() - heartbeatStart, true);\n const metricsStart = Date.now();\n startMetricsCollector();\n logInfo("Metrics collector started");\n recordSandboxOp("metrics_collector_start", Date.now() - metricsStart, true);\n const telemetryStart = Date.now();\n startTelemetryUpload();\n logInfo("Telemetry upload started");\n recordSandboxOp("telemetry_upload_start", Date.now() - telemetryStart, true);\n const workingDirStart = Date.now();\n try {\n fs7.mkdirSync(WORKING_DIR, { recursive: true });\n process.chdir(WORKING_DIR);\n } catch (error) {\n recordSandboxOp(\n "working_dir_setup",\n Date.now() - workingDirStart,\n false,\n String(error)\n );\n throw new Error(\n `Failed to create/change to working directory: ${WORKING_DIR} - ${error}`\n );\n }\n recordSandboxOp("working_dir_setup", Date.now() - workingDirStart, true);\n if (CLI_AGENT_TYPE === "codex") {\n const homeDir = process.env.HOME ?? "/home/user";\n const codexHome = `${homeDir}/.codex`;\n fs7.mkdirSync(codexHome, { recursive: true });\n process.env.CODEX_HOME = codexHome;\n logInfo(`Codex home directory: ${codexHome}`);\n const codexLoginStart = Date.now();\n let codexLoginSuccess = false;\n const apiKey = process.env.OPENAI_API_KEY ?? "";\n if (apiKey) {\n try {\n execSync4("codex login --with-api-key", {\n input: apiKey,\n encoding: "utf-8",\n stdio: ["pipe", "pipe", "pipe"]\n });\n logInfo("Codex authenticated with API key");\n codexLoginSuccess = true;\n } catch (error) {\n logError(`Codex login failed: ${error}`);\n }\n } else {\n logError("OPENAI_API_KEY not set");\n }\n recordSandboxOp(\n "codex_login",\n Date.now() - codexLoginStart,\n codexLoginSuccess\n );\n }\n const initDurationMs = Date.now() - initStartTime;\n recordSandboxOp("init_total", initDurationMs, true);\n logInfo(`\\u2713 Initialization complete (${Math.floor(initDurationMs / 1e3)}s)`);\n logInfo("\\u25B7 Execution");\n const execStartTime = Date.now();\n logInfo(`Starting ${CLI_AGENT_TYPE} execution...`);\n logInfo(`Prompt: ${PROMPT}`);\n const useMock = process.env.USE_MOCK_CLAUDE === "true";\n let cmd;\n if (CLI_AGENT_TYPE === "codex") {\n if (useMock) {\n throw new Error("Mock mode not supported for Codex");\n }\n const codexArgs = [\n "exec",\n "--json",\n "--dangerously-bypass-approvals-and-sandbox",\n "--skip-git-repo-check",\n "-C",\n WORKING_DIR\n ];\n if (OPENAI_MODEL) {\n codexArgs.push("-m", OPENAI_MODEL);\n }\n if (RESUME_SESSION_ID) {\n logInfo(`Resuming session: ${RESUME_SESSION_ID}`);\n codexArgs.push("resume", RESUME_SESSION_ID, PROMPT);\n } else {\n logInfo("Starting new session");\n codexArgs.push(PROMPT);\n }\n cmd = ["codex", ...codexArgs];\n } else {\n const claudeArgs = [\n "--print",\n "--verbose",\n "--output-format",\n "stream-json",\n "--dangerously-skip-permissions"\n ];\n if (RESUME_SESSION_ID) {\n logInfo(`Resuming session: ${RESUME_SESSION_ID}`);\n claudeArgs.push("--resume", RESUME_SESSION_ID);\n } else {\n logInfo("Starting new session");\n }\n const claudeBin = useMock ? "/usr/local/bin/vm0-agent/mock-claude.mjs" : "claude";\n if (useMock) {\n logInfo("Using mock-claude for testing");\n }\n cmd = [claudeBin, ...claudeArgs, PROMPT];\n }\n let agentExitCode = 0;\n const stderrLines = [];\n let logFile = null;\n try {\n logFile = fs7.createWriteStream(AGENT_LOG_FILE);\n const cmdExe = cmd[0];\n if (!cmdExe) {\n throw new Error("Empty command");\n }\n const proc = spawn(cmdExe, cmd.slice(1), {\n stdio: ["pipe", "pipe", "pipe"]\n });\n const exitPromise = new Promise((resolve) => {\n let resolved = false;\n proc.on("error", (err) => {\n if (!resolved) {\n resolved = true;\n logError(`Failed to spawn ${CLI_AGENT_TYPE}: ${err.message}`);\n stderrLines.push(`Spawn error: ${err.message}`);\n resolve(1);\n }\n });\n proc.on("close", (code) => {\n if (!resolved) {\n resolved = true;\n resolve(code ?? 1);\n }\n });\n });\n if (proc.stderr) {\n const stderrRl = readline.createInterface({ input: proc.stderr });\n stderrRl.on("line", (line) => {\n stderrLines.push(line);\n if (logFile && !logFile.destroyed) {\n logFile.write(`[STDERR] ${line}\n`);\n }\n });\n }\n if (proc.stdout) {\n const stdoutRl = readline.createInterface({ input: proc.stdout });\n let eventSequence = 0;\n for await (const line of stdoutRl) {\n if (logFile && !logFile.destroyed) {\n logFile.write(line + "\\n");\n }\n const stripped = line.trim();\n if (!stripped) {\n continue;\n }\n try {\n const event = JSON.parse(stripped);\n eventSequence++;\n await sendEvent(event, eventSequence);\n if (event.type === "result") {\n const resultContent = event.result;\n if (resultContent) {\n console.log(resultContent);\n }\n }\n } catch {\n logDebug(`Non-JSON line from agent: ${stripped.slice(0, 100)}`);\n }\n }\n }\n agentExitCode = await exitPromise;\n } catch (error) {\n logError(`Failed to execute ${CLI_AGENT_TYPE}: ${error}`);\n agentExitCode = 1;\n } finally {\n if (logFile && !logFile.destroyed) {\n logFile.end();\n }\n }\n console.log();\n let finalExitCode = agentExitCode;\n let errorMessage = "";\n if (fs7.existsSync(EVENT_ERROR_FLAG)) {\n logError("Some events failed to send, marking run as failed");\n finalExitCode = 1;\n errorMessage = "Some events failed to send";\n }\n const execDurationMs = Date.now() - execStartTime;\n recordSandboxOp("cli_execution", execDurationMs, agentExitCode === 0);\n if (agentExitCode === 0 && finalExitCode === 0) {\n logInfo(`\\u2713 Execution complete (${Math.floor(execDurationMs / 1e3)}s)`);\n } else {\n logInfo(`\\u2717 Execution failed (${Math.floor(execDurationMs / 1e3)}s)`);\n }\n if (agentExitCode === 0 && finalExitCode === 0) {\n logInfo(`${CLI_AGENT_TYPE} completed successfully`);\n logInfo("\\u25B7 Checkpoint");\n const checkpointStartTime = Date.now();\n const checkpointSuccess = await createCheckpoint();\n const checkpointDuration = Math.floor(\n (Date.now() - checkpointStartTime) / 1e3\n );\n if (checkpointSuccess) {\n logInfo(`\\u2713 Checkpoint complete (${checkpointDuration}s)`);\n } else {\n logInfo(`\\u2717 Checkpoint failed (${checkpointDuration}s)`);\n }\n if (!checkpointSuccess) {\n logError("Checkpoint creation failed, marking run as failed");\n finalExitCode = 1;\n errorMessage = "Checkpoint creation failed";\n }\n } else {\n if (agentExitCode !== 0) {\n logInfo(`${CLI_AGENT_TYPE} failed with exit code ${agentExitCode}`);\n if (stderrLines.length > 0) {\n errorMessage = stderrLines.map((line) => line.trim()).join(" ");\n logInfo(`Captured stderr: ${errorMessage}`);\n } else {\n errorMessage = `Agent exited with code ${agentExitCode}`;\n }\n }\n }\n return [finalExitCode, errorMessage];\n}\nasync function main() {\n let exitCode = 1;\n let errorMessage = "Unexpected termination";\n try {\n [exitCode, errorMessage] = await run();\n } catch (error) {\n if (error instanceof Error) {\n exitCode = 1;\n errorMessage = error.message;\n logError(`Error: ${errorMessage}`);\n } else {\n exitCode = 1;\n errorMessage = `Unexpected error: ${error}`;\n logError(errorMessage);\n }\n } finally {\n await cleanup(exitCode, errorMessage);\n }\n return exitCode;\n}\nmain().then((code) => process.exit(code)).catch((error) => {\n console.error("Fatal error:", error);\n process.exit(1);\n});\n';
7123
+ var DOWNLOAD_SCRIPT = '#!/usr/bin/env node\n\n// src/sandbox/scripts/src/download.ts\nimport * as fs2 from "fs";\nimport * as path from "path";\nimport * as os from "os";\nimport { execSync as execSync2 } from "child_process";\n\n// src/sandbox/scripts/src/lib/common.ts\nimport * as fs from "fs";\nvar RUN_ID = process.env.VM0_RUN_ID ?? "";\nvar API_URL = process.env.VM0_API_URL ?? "";\nvar API_TOKEN = process.env.VM0_API_TOKEN ?? "";\nvar PROMPT = process.env.VM0_PROMPT ?? "";\nvar VERCEL_BYPASS = process.env.VERCEL_PROTECTION_BYPASS ?? "";\nvar RESUME_SESSION_ID = process.env.VM0_RESUME_SESSION_ID ?? "";\nvar CLI_AGENT_TYPE = process.env.CLI_AGENT_TYPE ?? "claude-code";\nvar OPENAI_MODEL = process.env.OPENAI_MODEL ?? "";\nvar WORKING_DIR = process.env.VM0_WORKING_DIR ?? "";\nvar ARTIFACT_DRIVER = process.env.VM0_ARTIFACT_DRIVER ?? "";\nvar ARTIFACT_MOUNT_PATH = process.env.VM0_ARTIFACT_MOUNT_PATH ?? "";\nvar ARTIFACT_VOLUME_NAME = process.env.VM0_ARTIFACT_VOLUME_NAME ?? "";\nvar ARTIFACT_VERSION_ID = process.env.VM0_ARTIFACT_VERSION_ID ?? "";\nvar WEBHOOK_URL = `${API_URL}/api/webhooks/agent/events`;\nvar CHECKPOINT_URL = `${API_URL}/api/webhooks/agent/checkpoints`;\nvar COMPLETE_URL = `${API_URL}/api/webhooks/agent/complete`;\nvar HEARTBEAT_URL = `${API_URL}/api/webhooks/agent/heartbeat`;\nvar TELEMETRY_URL = `${API_URL}/api/webhooks/agent/telemetry`;\nvar PROXY_URL = `${API_URL}/api/webhooks/agent/proxy`;\nvar STORAGE_PREPARE_URL = `${API_URL}/api/webhooks/agent/storages/prepare`;\nvar STORAGE_COMMIT_URL = `${API_URL}/api/webhooks/agent/storages/commit`;\nvar HTTP_MAX_TIME_UPLOAD = 60;\nvar HTTP_MAX_RETRIES = 3;\nvar SESSION_ID_FILE = `/tmp/vm0-session-${RUN_ID}.txt`;\nvar SESSION_HISTORY_PATH_FILE = `/tmp/vm0-session-history-${RUN_ID}.txt`;\nvar EVENT_ERROR_FLAG = `/tmp/vm0-event-error-${RUN_ID}`;\nvar SYSTEM_LOG_FILE = `/tmp/vm0-main-${RUN_ID}.log`;\nvar AGENT_LOG_FILE = `/tmp/vm0-agent-${RUN_ID}.log`;\nvar METRICS_LOG_FILE = `/tmp/vm0-metrics-${RUN_ID}.jsonl`;\nvar NETWORK_LOG_FILE = `/tmp/vm0-network-${RUN_ID}.jsonl`;\nvar TELEMETRY_LOG_POS_FILE = `/tmp/vm0-telemetry-log-pos-${RUN_ID}.txt`;\nvar TELEMETRY_METRICS_POS_FILE = `/tmp/vm0-telemetry-metrics-pos-${RUN_ID}.txt`;\nvar TELEMETRY_NETWORK_POS_FILE = `/tmp/vm0-telemetry-network-pos-${RUN_ID}.txt`;\nvar TELEMETRY_SANDBOX_OPS_POS_FILE = `/tmp/vm0-telemetry-sandbox-ops-pos-${RUN_ID}.txt`;\nvar SANDBOX_OPS_LOG_FILE = `/tmp/vm0-sandbox-ops-${RUN_ID}.jsonl`;\nfunction recordSandboxOp(actionType, durationMs, success, error) {\n const entry = {\n ts: (/* @__PURE__ */ new Date()).toISOString(),\n action_type: actionType,\n duration_ms: durationMs,\n success\n };\n if (error) {\n entry.error = error;\n }\n fs.appendFileSync(SANDBOX_OPS_LOG_FILE, JSON.stringify(entry) + "\\n");\n}\n\n// src/sandbox/scripts/src/lib/log.ts\nvar SCRIPT_NAME = process.env.LOG_SCRIPT_NAME ?? "run-agent";\nvar DEBUG_MODE = process.env.VM0_DEBUG === "1";\nfunction timestamp() {\n return (/* @__PURE__ */ new Date()).toISOString().replace(/\\.\\d{3}Z$/, "Z");\n}\nfunction logInfo(msg) {\n console.error(`[${timestamp()}] [INFO] [sandbox:${SCRIPT_NAME}] ${msg}`);\n}\nfunction logWarn(msg) {\n console.error(`[${timestamp()}] [WARN] [sandbox:${SCRIPT_NAME}] ${msg}`);\n}\nfunction logError(msg) {\n console.error(`[${timestamp()}] [ERROR] [sandbox:${SCRIPT_NAME}] ${msg}`);\n}\nfunction logDebug(msg) {\n if (DEBUG_MODE) {\n console.error(`[${timestamp()}] [DEBUG] [sandbox:${SCRIPT_NAME}] ${msg}`);\n }\n}\n\n// src/sandbox/scripts/src/lib/http-client.ts\nimport { execSync } from "child_process";\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\nasync function httpDownload(url, destPath, maxRetries = HTTP_MAX_RETRIES) {\n for (let attempt = 1; attempt <= maxRetries; attempt++) {\n logDebug(`HTTP download attempt ${attempt}/${maxRetries} from ${url}`);\n try {\n const curlCmd = ["curl", "-fsSL", "-o", destPath, `"${url}"`].join(" ");\n execSync(curlCmd, {\n timeout: HTTP_MAX_TIME_UPLOAD * 1e3,\n stdio: ["pipe", "pipe", "pipe"]\n });\n return true;\n } catch (error) {\n const errorMsg = error instanceof Error ? error.message : String(error);\n if (errorMsg.includes("ETIMEDOUT") || errorMsg.includes("timeout")) {\n logWarn(\n `HTTP download failed (attempt ${attempt}/${maxRetries}): Timeout`\n );\n } else {\n logWarn(\n `HTTP download failed (attempt ${attempt}/${maxRetries}): ${errorMsg}`\n );\n }\n if (attempt < maxRetries) {\n await sleep(1e3);\n }\n }\n }\n logError(`HTTP download failed after ${maxRetries} attempts from ${url}`);\n return false;\n}\n\n// src/sandbox/scripts/src/download.ts\nasync function downloadStorage(mountPath, archiveUrl) {\n logInfo(`Downloading storage to ${mountPath}`);\n const tempTar = path.join(\n os.tmpdir(),\n `storage-${Date.now()}-${Math.random().toString(36).slice(2)}.tar.gz`\n );\n try {\n if (!await httpDownload(archiveUrl, tempTar)) {\n logError(`Failed to download archive for ${mountPath}`);\n return false;\n }\n fs2.mkdirSync(mountPath, { recursive: true });\n try {\n execSync2(`tar -xzf "${tempTar}" -C "${mountPath}"`, {\n stdio: ["pipe", "pipe", "pipe"]\n });\n } catch {\n logInfo(`Archive appears empty for ${mountPath}`);\n }\n logInfo(`Successfully extracted to ${mountPath}`);\n return true;\n } finally {\n try {\n fs2.unlinkSync(tempTar);\n } catch {\n }\n }\n}\nasync function main() {\n const args = process.argv.slice(2);\n if (args.length < 1) {\n logError("Usage: node download.mjs <manifest_path>");\n process.exit(1);\n }\n const manifestPath = args[0] ?? "";\n if (!manifestPath || !fs2.existsSync(manifestPath)) {\n logError(`Manifest file not found: ${manifestPath}`);\n process.exit(1);\n }\n logInfo(`Starting storage download from manifest: ${manifestPath}`);\n let manifest;\n try {\n const content = fs2.readFileSync(manifestPath, "utf-8");\n manifest = JSON.parse(content);\n } catch (error) {\n logError(`Failed to load manifest: ${error}`);\n process.exit(1);\n }\n const storages = manifest.storages ?? [];\n const artifact = manifest.artifact;\n const storageCount = storages.length;\n const hasArtifact = artifact !== void 0;\n logInfo(`Found ${storageCount} storages, artifact: ${hasArtifact}`);\n const downloadTotalStart = Date.now();\n let downloadSuccess = true;\n for (const storage of storages) {\n const mountPath = storage.mountPath;\n const archiveUrl = storage.archiveUrl;\n if (archiveUrl && archiveUrl !== "null") {\n const storageStart = Date.now();\n const success = await downloadStorage(mountPath, archiveUrl);\n recordSandboxOp("storage_download", Date.now() - storageStart, success);\n if (!success) {\n downloadSuccess = false;\n }\n }\n }\n if (artifact) {\n const artifactMount = artifact.mountPath;\n const artifactUrl = artifact.archiveUrl;\n if (artifactUrl && artifactUrl !== "null") {\n const artifactStart = Date.now();\n const success = await downloadStorage(artifactMount, artifactUrl);\n recordSandboxOp("artifact_download", Date.now() - artifactStart, success);\n if (!success) {\n downloadSuccess = false;\n }\n }\n }\n recordSandboxOp(\n "download_total",\n Date.now() - downloadTotalStart,\n downloadSuccess\n );\n logInfo("All storages downloaded successfully");\n}\nmain().catch((error) => {\n logError(`Fatal error: ${error}`);\n process.exit(1);\n});\n';
7124
+ var MOCK_CLAUDE_SCRIPT = '#!/usr/bin/env node\n\n// src/sandbox/scripts/src/mock-claude.ts\nimport * as fs from "fs";\nimport * as path from "path";\nimport { execSync } from "child_process";\nfunction parseArgs(args) {\n const result = {\n outputFormat: "text",\n print: false,\n verbose: false,\n dangerouslySkipPermissions: false,\n resume: null,\n prompt: ""\n };\n const remaining = [];\n let i = 0;\n while (i < args.length) {\n const arg = args[i];\n if (arg === "--output-format" && i + 1 < args.length) {\n result.outputFormat = args[i + 1] ?? "text";\n i += 2;\n } else if (arg === "--print") {\n result.print = true;\n i++;\n } else if (arg === "--verbose") {\n result.verbose = true;\n i++;\n } else if (arg === "--dangerously-skip-permissions") {\n result.dangerouslySkipPermissions = true;\n i++;\n } else if (arg === "--resume" && i + 1 < args.length) {\n result.resume = args[i + 1] ?? null;\n i += 2;\n } else if (arg) {\n remaining.push(arg);\n i++;\n } else {\n i++;\n }\n }\n if (remaining.length > 0) {\n result.prompt = remaining[0] ?? "";\n }\n return result;\n}\nfunction createSessionHistory(sessionId, cwd) {\n const projectName = cwd.replace(/^\\//, "").replace(/\\//g, "-");\n const homeDir = process.env.HOME ?? "/home/user";\n const sessionDir = `${homeDir}/.claude/projects/-${projectName}`;\n fs.mkdirSync(sessionDir, { recursive: true });\n return path.join(sessionDir, `${sessionId}.jsonl`);\n}\nfunction main() {\n const sessionId = `mock-${Date.now() * 1e3 + Math.floor(Math.random() * 1e3)}`;\n const args = parseArgs(process.argv.slice(2));\n const prompt = args.prompt;\n const outputFormat = args.outputFormat;\n if (prompt.startsWith("@fail:")) {\n const errorMsg = prompt.slice(6);\n console.error(errorMsg);\n process.exit(1);\n }\n const cwd = process.cwd();\n if (outputFormat === "stream-json") {\n const sessionHistoryFile = createSessionHistory(sessionId, cwd);\n const events = [];\n const initEvent = {\n type: "system",\n subtype: "init",\n cwd,\n session_id: sessionId,\n tools: ["Bash"],\n model: "mock-claude"\n };\n console.log(JSON.stringify(initEvent));\n events.push(initEvent);\n const textEvent = {\n type: "assistant",\n message: {\n role: "assistant",\n content: [{ type: "text", text: "Executing command..." }]\n },\n session_id: sessionId\n };\n console.log(JSON.stringify(textEvent));\n events.push(textEvent);\n const toolUseEvent = {\n type: "assistant",\n message: {\n role: "assistant",\n content: [\n {\n type: "tool_use",\n id: "toolu_mock_001",\n name: "Bash",\n input: { command: prompt }\n }\n ]\n },\n session_id: sessionId\n };\n console.log(JSON.stringify(toolUseEvent));\n events.push(toolUseEvent);\n let output;\n let exitCode;\n try {\n output = execSync(`bash -c ${JSON.stringify(prompt)}`, {\n encoding: "utf-8",\n stdio: ["pipe", "pipe", "pipe"]\n });\n exitCode = 0;\n } catch (error) {\n const execError = error;\n output = (execError.stdout ?? "") + (execError.stderr ?? "");\n exitCode = execError.status ?? 1;\n }\n const isError = exitCode !== 0;\n const toolResultEvent = {\n type: "user",\n message: {\n role: "user",\n content: [\n {\n type: "tool_result",\n tool_use_id: "toolu_mock_001",\n content: output,\n is_error: isError\n }\n ]\n },\n session_id: sessionId\n };\n console.log(JSON.stringify(toolResultEvent));\n events.push(toolResultEvent);\n const resultEvent = {\n type: "result",\n subtype: exitCode === 0 ? "success" : "error",\n is_error: exitCode !== 0,\n duration_ms: 100,\n num_turns: 1,\n result: output,\n session_id: sessionId,\n total_cost_usd: 0,\n usage: { input_tokens: 0, output_tokens: 0 }\n };\n console.log(JSON.stringify(resultEvent));\n events.push(resultEvent);\n const historyContent = events.map((e) => JSON.stringify(e)).join("\\n") + "\\n";\n fs.writeFileSync(sessionHistoryFile, historyContent);\n process.exit(exitCode);\n } else {\n try {\n execSync(`bash -c ${JSON.stringify(prompt)}`, {\n stdio: "inherit"\n });\n process.exit(0);\n } catch (error) {\n const execError = error;\n process.exit(execError.status ?? 1);\n }\n }\n}\nvar isMainModule = process.argv[1]?.endsWith("mock-claude.mjs") || process.argv[1]?.endsWith("mock-claude.ts");\nif (isMainModule) {\n main();\n}\nexport {\n createSessionHistory,\n parseArgs\n};\n';
7125
+ var ENV_LOADER_SCRIPT = '#!/usr/bin/env node\n\n// src/sandbox/scripts/src/env-loader.ts\nimport * as fs from "fs";\nimport { spawn } from "child_process";\nvar ENV_JSON_PATH = "/tmp/vm0-env.json";\nconsole.log("[env-loader] Starting...");\nif (fs.existsSync(ENV_JSON_PATH)) {\n console.log(`[env-loader] Loading environment from ${ENV_JSON_PATH}`);\n try {\n const content = fs.readFileSync(ENV_JSON_PATH, "utf-8");\n const envData = JSON.parse(content);\n for (const [key, value] of Object.entries(envData)) {\n process.env[key] = value;\n }\n console.log(\n `[env-loader] Loaded ${Object.keys(envData).length} environment variables`\n );\n } catch (error) {\n console.error(`[env-loader] ERROR loading JSON: ${error}`);\n process.exit(1);\n }\n} else {\n console.error(\n `[env-loader] ERROR: Environment file not found: ${ENV_JSON_PATH}`\n );\n process.exit(1);\n}\nvar criticalVars = [\n "VM0_RUN_ID",\n "VM0_API_URL",\n "VM0_WORKING_DIR",\n "VM0_PROMPT"\n];\nfor (const varName of criticalVars) {\n const val = process.env[varName] ?? "";\n if (val) {\n const display = val.length > 50 ? val.substring(0, 50) + "..." : val;\n console.log(`[env-loader] ${varName}=${display}`);\n } else {\n console.log(`[env-loader] WARNING: ${varName} is empty`);\n }\n}\nvar runAgentPath = "/usr/local/bin/vm0-agent/run-agent.mjs";\nconsole.log(`[env-loader] Executing ${runAgentPath}`);\nvar child = spawn("node", [runAgentPath], {\n stdio: "inherit",\n env: process.env\n});\nchild.on("close", (code) => {\n process.exit(code ?? 1);\n});\n';
7102
7126
 
7103
- This package contains utility modules for the VM0 agent execution in E2B sandbox.
7104
- """
7105
- `;
7127
+ // ../../packages/core/src/sandbox/scripts/index.ts
7128
+ var SCRIPT_PATHS = {
7129
+ /** Base directory for agent scripts */
7130
+ baseDir: "/usr/local/bin/vm0-agent",
7131
+ /** Main agent orchestrator - handles CLI execution, events, checkpoints */
7132
+ runAgent: "/usr/local/bin/vm0-agent/run-agent.mjs",
7133
+ /** Storage download - downloads volumes/artifacts from S3 via presigned URLs */
7134
+ download: "/usr/local/bin/vm0-agent/download.mjs",
7135
+ /** Mock Claude CLI for testing - executes prompt as bash, outputs Claude-compatible JSONL */
7136
+ mockClaude: "/usr/local/bin/vm0-agent/mock-claude.mjs",
7137
+ /** Environment loader for runner - loads env from JSON file before running agent */
7138
+ envLoader: "/usr/local/bin/vm0-agent/env-loader.mjs"
7139
+ };
7140
+
7141
+ // src/lib/scripts/index.ts
7142
+ var ENV_LOADER_PATH = "/usr/local/bin/vm0-agent/env-loader.mjs";
7143
+
7144
+ // src/lib/proxy/vm-registry.ts
7145
+ import fs4 from "fs";
7146
+ var DEFAULT_REGISTRY_PATH = "/tmp/vm0-vm-registry.json";
7147
+ var VMRegistry = class {
7148
+ registryPath;
7149
+ data;
7150
+ constructor(registryPath = DEFAULT_REGISTRY_PATH) {
7151
+ this.registryPath = registryPath;
7152
+ this.data = this.load();
7153
+ }
7154
+ /**
7155
+ * Load registry data from file
7156
+ */
7157
+ load() {
7158
+ try {
7159
+ if (fs4.existsSync(this.registryPath)) {
7160
+ const content = fs4.readFileSync(this.registryPath, "utf-8");
7161
+ return JSON.parse(content);
7162
+ }
7163
+ } catch {
7164
+ }
7165
+ return { vms: {}, updatedAt: Date.now() };
7166
+ }
7167
+ /**
7168
+ * Save registry data to file atomically
7169
+ */
7170
+ save() {
7171
+ this.data.updatedAt = Date.now();
7172
+ const content = JSON.stringify(this.data, null, 2);
7173
+ const tempPath = `${this.registryPath}.tmp`;
7174
+ fs4.writeFileSync(tempPath, content, { mode: 420 });
7175
+ fs4.renameSync(tempPath, this.registryPath);
7176
+ }
7177
+ /**
7178
+ * Register a VM with its IP address
7179
+ */
7180
+ register(vmIp, runId, sandboxToken, options) {
7181
+ this.data.vms[vmIp] = {
7182
+ runId,
7183
+ sandboxToken,
7184
+ registeredAt: Date.now(),
7185
+ firewallRules: options?.firewallRules,
7186
+ mitmEnabled: options?.mitmEnabled,
7187
+ sealSecretsEnabled: options?.sealSecretsEnabled
7188
+ };
7189
+ this.save();
7190
+ const firewallInfo = options?.firewallRules ? ` with ${options.firewallRules.length} firewall rules` : "";
7191
+ const mitmInfo = options?.mitmEnabled ? ", MITM enabled" : "";
7192
+ console.log(
7193
+ `[VMRegistry] Registered VM ${vmIp} for run ${runId}${firewallInfo}${mitmInfo}`
7194
+ );
7195
+ }
7196
+ /**
7197
+ * Unregister a VM by IP address
7198
+ */
7199
+ unregister(vmIp) {
7200
+ if (this.data.vms[vmIp]) {
7201
+ const registration = this.data.vms[vmIp];
7202
+ delete this.data.vms[vmIp];
7203
+ this.save();
7204
+ console.log(
7205
+ `[VMRegistry] Unregistered VM ${vmIp} (run ${registration.runId})`
7206
+ );
7207
+ }
7208
+ }
7209
+ /**
7210
+ * Look up registration by VM IP
7211
+ */
7212
+ lookup(vmIp) {
7213
+ return this.data.vms[vmIp];
7214
+ }
7215
+ /**
7216
+ * Get all registered VMs
7217
+ */
7218
+ getAll() {
7219
+ return { ...this.data.vms };
7220
+ }
7221
+ /**
7222
+ * Clear all registrations
7223
+ */
7224
+ clear() {
7225
+ this.data.vms = {};
7226
+ this.save();
7227
+ console.log("[VMRegistry] Cleared all registrations");
7228
+ }
7229
+ /**
7230
+ * Get the path to the registry file
7231
+ */
7232
+ getRegistryPath() {
7233
+ return this.registryPath;
7234
+ }
7235
+ };
7236
+ var globalRegistry = null;
7237
+ function getVMRegistry() {
7238
+ if (!globalRegistry) {
7239
+ globalRegistry = new VMRegistry();
7240
+ }
7241
+ return globalRegistry;
7242
+ }
7243
+ function initVMRegistry(registryPath) {
7244
+ globalRegistry = new VMRegistry(registryPath);
7245
+ return globalRegistry;
7246
+ }
7247
+
7248
+ // src/lib/proxy/proxy-manager.ts
7249
+ import { spawn as spawn2 } from "child_process";
7250
+ import fs5 from "fs";
7251
+ import path3 from "path";
7106
7252
 
7107
- // ../../packages/core/src/sandbox/scripts/lib/common.py.ts
7108
- var COMMON_SCRIPT = `#!/usr/bin/env python3
7253
+ // src/lib/proxy/mitm-addon-script.ts
7254
+ var RUNNER_MITM_ADDON_SCRIPT = `#!/usr/bin/env python3
7109
7255
  """
7110
- Common environment variables and utilities for VM0 agent scripts.
7111
- This module should be imported by other scripts.
7256
+ mitmproxy addon for VM0 runner-level network security mode.
7257
+
7258
+ This addon runs on the runner HOST (not inside VMs) and:
7259
+ 1. Intercepts all HTTPS requests from VMs
7260
+ 2. Looks up the source VM's runId and firewall rules from the VM registry
7261
+ 3. Evaluates firewall rules (first-match-wins) to ALLOW or DENY
7262
+ 4. For MITM mode: Rewrites requests to go through VM0 Proxy endpoint
7263
+ 5. For SNI-only mode: Passes through or blocks without decryption
7264
+ 6. Logs network activity per-run to JSONL files
7112
7265
  """
7113
7266
  import os
7267
+ import json
7268
+ import time
7269
+ import urllib.parse
7270
+ import ipaddress
7271
+ import socket
7272
+ from mitmproxy import http, ctx, tls
7114
7273
 
7115
- # Environment variables
7116
- RUN_ID = os.environ.get("VM0_RUN_ID", "")
7117
- API_URL = os.environ.get("VM0_API_URL", "")
7118
- API_TOKEN = os.environ.get("VM0_API_TOKEN", "")
7119
- PROMPT = os.environ.get("VM0_PROMPT", "")
7120
- VERCEL_BYPASS = os.environ.get("VERCEL_PROTECTION_BYPASS", "")
7121
- RESUME_SESSION_ID = os.environ.get("VM0_RESUME_SESSION_ID", "")
7122
-
7123
- # CLI agent type - determines which CLI to invoke (claude-code or codex)
7124
- CLI_AGENT_TYPE = os.environ.get("CLI_AGENT_TYPE", "claude-code")
7125
-
7126
- # OpenAI model override - used for OpenRouter/custom endpoints with Codex
7127
- OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "")
7128
-
7129
- # Working directory is required - no fallback allowed
7130
- WORKING_DIR = os.environ.get("VM0_WORKING_DIR", "")
7131
-
7132
- # Artifact configuration (replaces GIT_VOLUMES and VM0_VOLUMES)
7133
- ARTIFACT_DRIVER = os.environ.get("VM0_ARTIFACT_DRIVER", "")
7134
- ARTIFACT_MOUNT_PATH = os.environ.get("VM0_ARTIFACT_MOUNT_PATH", "")
7135
- ARTIFACT_VOLUME_NAME = os.environ.get("VM0_ARTIFACT_VOLUME_NAME", "")
7136
- ARTIFACT_VERSION_ID = os.environ.get("VM0_ARTIFACT_VERSION_ID", "")
7137
-
7138
- # Construct webhook endpoint URLs
7139
- WEBHOOK_URL = f"{API_URL}/api/webhooks/agent/events"
7140
- CHECKPOINT_URL = f"{API_URL}/api/webhooks/agent/checkpoints"
7141
- COMPLETE_URL = f"{API_URL}/api/webhooks/agent/complete"
7142
- HEARTBEAT_URL = f"{API_URL}/api/webhooks/agent/heartbeat"
7143
- TELEMETRY_URL = f"{API_URL}/api/webhooks/agent/telemetry"
7144
- PROXY_URL = f"{API_URL}/api/webhooks/agent/proxy"
7145
-
7146
- # Direct S3 upload endpoints (webhook versions for sandbox - uses JWT auth)
7147
- STORAGE_PREPARE_URL = f"{API_URL}/api/webhooks/agent/storages/prepare"
7148
- STORAGE_COMMIT_URL = f"{API_URL}/api/webhooks/agent/storages/commit"
7149
7274
 
7150
- # Heartbeat configuration
7151
- HEARTBEAT_INTERVAL = 60 # seconds
7275
+ # VM0 Proxy configuration from environment
7276
+ API_URL = os.environ.get("VM0_API_URL", "https://www.vm0.ai")
7277
+ REGISTRY_PATH = os.environ.get("VM0_REGISTRY_PATH", "/tmp/vm0-vm-registry.json")
7278
+ VERCEL_BYPASS = os.environ.get("VERCEL_AUTOMATION_BYPASS_SECRET", "")
7152
7279
 
7153
- # Telemetry upload configuration
7154
- TELEMETRY_INTERVAL = 30 # seconds
7280
+ # Construct proxy URL
7281
+ PROXY_URL = f"{API_URL}/api/webhooks/agent/proxy"
7155
7282
 
7156
- # HTTP request configuration
7157
- HTTP_CONNECT_TIMEOUT = 10
7158
- HTTP_MAX_TIME = 30
7159
- HTTP_MAX_TIME_UPLOAD = 60
7160
- HTTP_MAX_RETRIES = 3
7283
+ # Cache for VM registry (reloaded periodically)
7284
+ _registry_cache = {}
7285
+ _registry_cache_time = 0
7286
+ REGISTRY_CACHE_TTL = 2 # seconds
7161
7287
 
7162
- # Variables for checkpoint (use temp files to persist across subprocesses)
7163
- SESSION_ID_FILE = f"/tmp/vm0-session-{RUN_ID}.txt"
7164
- SESSION_HISTORY_PATH_FILE = f"/tmp/vm0-session-history-{RUN_ID}.txt"
7288
+ # Track request start times for latency calculation
7289
+ request_start_times = {}
7165
7290
 
7166
- # Event error flag file - used to track if any events failed to send
7167
- EVENT_ERROR_FLAG = f"/tmp/vm0-event-error-{RUN_ID}"
7168
7291
 
7169
- # Log file for persistent logging (directly in /tmp with vm0- prefix)
7170
- SYSTEM_LOG_FILE = f"/tmp/vm0-main-{RUN_ID}.log"
7171
- AGENT_LOG_FILE = f"/tmp/vm0-agent-{RUN_ID}.log"
7292
+ def load_registry() -> dict:
7293
+ """Load the VM registry from file, with caching."""
7294
+ global _registry_cache, _registry_cache_time
7172
7295
 
7173
- # Metrics log file for system resource metrics (JSONL format)
7174
- METRICS_LOG_FILE = f"/tmp/vm0-metrics-{RUN_ID}.jsonl"
7296
+ now = time.time()
7297
+ if now - _registry_cache_time < REGISTRY_CACHE_TTL:
7298
+ return _registry_cache
7175
7299
 
7176
- # Network log file for proxy request logs (JSONL format)
7177
- NETWORK_LOG_FILE = f"/tmp/vm0-network-{RUN_ID}.jsonl"
7300
+ try:
7301
+ if os.path.exists(REGISTRY_PATH):
7302
+ with open(REGISTRY_PATH, "r") as f:
7303
+ data = json.load(f)
7304
+ _registry_cache = data.get("vms", {})
7305
+ _registry_cache_time = now
7306
+ return _registry_cache
7307
+ except Exception as e:
7308
+ ctx.log.warn(f"Failed to load VM registry: {e}")
7178
7309
 
7179
- # Telemetry position tracking files (to avoid duplicate uploads)
7180
- TELEMETRY_LOG_POS_FILE = f"/tmp/vm0-telemetry-log-pos-{RUN_ID}.txt"
7181
- TELEMETRY_METRICS_POS_FILE = f"/tmp/vm0-telemetry-metrics-pos-{RUN_ID}.txt"
7182
- TELEMETRY_NETWORK_POS_FILE = f"/tmp/vm0-telemetry-network-pos-{RUN_ID}.txt"
7183
- TELEMETRY_SANDBOX_OPS_POS_FILE = f"/tmp/vm0-telemetry-sandbox-ops-pos-{RUN_ID}.txt"
7310
+ return _registry_cache
7184
7311
 
7185
- # Sandbox operations log file (JSONL format)
7186
- SANDBOX_OPS_LOG_FILE = f"/tmp/vm0-sandbox-ops-{RUN_ID}.jsonl"
7187
7312
 
7188
- # Metrics collection configuration
7189
- METRICS_INTERVAL = 5 # seconds
7313
+ def get_vm_info(client_ip: str) -> dict | None:
7314
+ """Look up VM info by client IP address."""
7315
+ registry = load_registry()
7316
+ return registry.get(client_ip)
7190
7317
 
7191
- def validate_config() -> bool:
7192
- """
7193
- Validate required configuration.
7194
- Raises ValueError if configuration is invalid.
7195
- Returns True if valid.
7196
- """
7197
- if not WORKING_DIR:
7198
- raise ValueError("VM0_WORKING_DIR is required but not set")
7199
- return True
7200
-
7201
- def record_sandbox_op(
7202
- action_type: str,
7203
- duration_ms: int,
7204
- success: bool,
7205
- error: str = None
7206
- ) -> None:
7207
- """
7208
- Record a sandbox operation to JSONL file for telemetry upload.
7209
7318
 
7210
- Args:
7211
- action_type: Operation name (e.g., "init_total", "storage_download", "cli_execution")
7212
- duration_ms: Duration in milliseconds
7213
- success: Whether the operation succeeded
7214
- error: Optional error message if failed
7215
- """
7216
- from datetime import datetime, timezone
7217
- import json
7319
+ def get_network_log_path(run_id: str) -> str:
7320
+ """Get the network log file path for a run."""
7321
+ return f"/tmp/vm0-network-{run_id}.jsonl"
7218
7322
 
7219
- entry = {
7220
- "ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z",
7221
- "action_type": action_type,
7222
- "duration_ms": duration_ms,
7223
- "success": success,
7224
- }
7225
- if error:
7226
- entry["error"] = error
7227
7323
 
7228
- with open(SANDBOX_OPS_LOG_FILE, "a") as f:
7229
- f.write(json.dumps(entry) + "\\n")
7230
- `;
7324
+ def log_network_entry(run_id: str, entry: dict) -> None:
7325
+ """Write a network log entry to the per-run JSONL file."""
7326
+ if not run_id:
7327
+ return
7231
7328
 
7232
- // ../../packages/core/src/sandbox/scripts/lib/log.py.ts
7233
- var LOG_SCRIPT = `#!/usr/bin/env python3
7234
- """
7235
- Unified logging functions for VM0 agent scripts.
7236
- Format: [TIMESTAMP] [LEVEL] [sandbox:SCRIPT_NAME] message
7237
- """
7238
- import os
7239
- import sys
7240
- from datetime import datetime, timezone
7329
+ log_path = get_network_log_path(run_id)
7330
+ try:
7331
+ fd = os.open(log_path, os.O_CREAT | os.O_APPEND | os.O_WRONLY, 0o644)
7332
+ try:
7333
+ os.write(fd, (json.dumps(entry) + "\\n").encode())
7334
+ finally:
7335
+ os.close(fd)
7336
+ except Exception as e:
7337
+ ctx.log.warn(f"Failed to write network log: {e}")
7241
7338
 
7242
- # Default script name, can be overridden by setting LOG_SCRIPT_NAME env var
7243
- SCRIPT_NAME = os.environ.get("LOG_SCRIPT_NAME", "run-agent")
7244
- DEBUG_MODE = os.environ.get("VM0_DEBUG", "") == "1"
7245
7339
 
7340
+ def get_original_url(flow: http.HTTPFlow) -> str:
7341
+ """Reconstruct the original target URL from the request."""
7342
+ scheme = "https" if flow.request.port == 443 else "http"
7343
+ host = flow.request.pretty_host
7344
+ port = flow.request.port
7246
7345
 
7247
- def _timestamp() -> str:
7248
- """Get current UTC timestamp in ISO 8601 format."""
7249
- return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
7346
+ if (scheme == "https" and port != 443) or (scheme == "http" and port != 80):
7347
+ host_with_port = f"{host}:{port}"
7348
+ else:
7349
+ host_with_port = host
7250
7350
 
7351
+ path = flow.request.path
7352
+ return f"{scheme}://{host_with_port}{path}"
7251
7353
 
7252
- def log_info(msg: str) -> None:
7253
- """Log info message to stderr."""
7254
- print(f"[{_timestamp()}] [INFO] [sandbox:{SCRIPT_NAME}] {msg}", file=sys.stderr)
7255
7354
 
7355
+ # ============================================================================
7356
+ # Firewall Rule Matching
7357
+ # ============================================================================
7256
7358
 
7257
- def log_warn(msg: str) -> None:
7258
- """Log warning message to stderr."""
7259
- print(f"[{_timestamp()}] [WARN] [sandbox:{SCRIPT_NAME}] {msg}", file=sys.stderr)
7359
+ def match_domain(pattern: str, hostname: str) -> bool:
7360
+ """
7361
+ Match hostname against domain pattern.
7362
+ Supports exact match and wildcard prefix (*.example.com).
7363
+ """
7364
+ if not pattern or not hostname:
7365
+ return False
7260
7366
 
7367
+ pattern = pattern.lower()
7368
+ hostname = hostname.lower()
7261
7369
 
7262
- def log_error(msg: str) -> None:
7263
- """Log error message to stderr."""
7264
- print(f"[{_timestamp()}] [ERROR] [sandbox:{SCRIPT_NAME}] {msg}", file=sys.stderr)
7370
+ if pattern.startswith("*."):
7371
+ # Wildcard: *.example.com matches sub.example.com, www.example.com
7372
+ # Also matches example.com itself (without subdomain)
7373
+ suffix = pattern[1:] # .example.com
7374
+ base = pattern[2:] # example.com
7375
+ return hostname.endswith(suffix) or hostname == base
7265
7376
 
7377
+ return hostname == pattern
7266
7378
 
7267
- def log_debug(msg: str) -> None:
7268
- """Log debug message to stderr (only if VM0_DEBUG=1)."""
7269
- if DEBUG_MODE:
7270
- print(f"[{_timestamp()}] [DEBUG] [sandbox:{SCRIPT_NAME}] {msg}", file=sys.stderr)
7271
- `;
7272
7379
 
7273
- // ../../packages/core/src/sandbox/scripts/lib/http_client.py.ts
7274
- var HTTP_SCRIPT = `#!/usr/bin/env python3
7275
- """
7276
- Unified HTTP request functions for VM0 agent scripts.
7277
- Uses urllib (standard library) with retry logic.
7278
- """
7279
- import json
7280
- import time
7281
- import subprocess
7282
- import os
7283
- from urllib.request import Request, urlopen
7284
- from urllib.error import HTTPError, URLError
7285
- from typing import Optional, Dict, Any
7286
-
7287
- from common import (
7288
- API_TOKEN, VERCEL_BYPASS,
7289
- HTTP_CONNECT_TIMEOUT, HTTP_MAX_TIME, HTTP_MAX_TIME_UPLOAD, HTTP_MAX_RETRIES
7290
- )
7291
- from log import log_debug, log_warn, log_error
7292
-
7293
-
7294
- def http_post_json(
7295
- url: str,
7296
- data: Dict[str, Any],
7297
- max_retries: int = HTTP_MAX_RETRIES
7298
- ) -> Optional[Dict[str, Any]]:
7299
- """
7300
- HTTP POST with JSON body and retry logic.
7301
-
7302
- Args:
7303
- url: Target URL
7304
- data: Dictionary to send as JSON
7305
- max_retries: Maximum retry attempts
7306
-
7307
- Returns:
7308
- Response JSON as dict on success, None on failure
7309
- """
7310
- headers = {
7311
- "Content-Type": "application/json",
7312
- "Authorization": f"Bearer {API_TOKEN}",
7313
- }
7314
- if VERCEL_BYPASS:
7315
- headers["x-vercel-protection-bypass"] = VERCEL_BYPASS
7316
-
7317
- body = json.dumps(data).encode("utf-8")
7318
-
7319
- for attempt in range(1, max_retries + 1):
7320
- log_debug(f"HTTP POST attempt {attempt}/{max_retries} to {url}")
7321
- try:
7322
- req = Request(url, data=body, headers=headers, method="POST")
7323
- with urlopen(req, timeout=HTTP_MAX_TIME) as resp:
7324
- response_body = resp.read().decode("utf-8")
7325
- if response_body:
7326
- return json.loads(response_body)
7327
- return {}
7328
- except HTTPError as e:
7329
- log_warn(f"HTTP POST failed (attempt {attempt}/{max_retries}): HTTP {e.code}")
7330
- if attempt < max_retries:
7331
- time.sleep(1)
7332
- except URLError as e:
7333
- log_warn(f"HTTP POST failed (attempt {attempt}/{max_retries}): {e.reason}")
7334
- if attempt < max_retries:
7335
- time.sleep(1)
7336
- except TimeoutError:
7337
- log_warn(f"HTTP POST failed (attempt {attempt}/{max_retries}): Timeout")
7338
- if attempt < max_retries:
7339
- time.sleep(1)
7340
- except Exception as e:
7341
- log_warn(f"HTTP POST failed (attempt {attempt}/{max_retries}): {e}")
7342
- if attempt < max_retries:
7343
- time.sleep(1)
7344
-
7345
- log_error(f"HTTP POST failed after {max_retries} attempts to {url}")
7346
- return None
7347
-
7348
-
7349
- def http_post_form(
7350
- url: str,
7351
- form_fields: Dict[str, str],
7352
- file_path: Optional[str] = None,
7353
- file_field: str = "file",
7354
- max_retries: int = HTTP_MAX_RETRIES
7355
- ) -> Optional[Dict[str, Any]]:
7356
- """
7357
- HTTP POST with multipart form data and retry logic.
7358
- Uses curl for multipart uploads as urllib doesn't support it well.
7359
-
7360
- Args:
7361
- url: Target URL
7362
- form_fields: Dictionary of form field name -> value
7363
- file_path: Optional path to file to upload
7364
- file_field: Form field name for the file
7365
- max_retries: Maximum retry attempts
7366
-
7367
- Returns:
7368
- Response JSON as dict on success, None on failure
7369
- """
7370
- for attempt in range(1, max_retries + 1):
7371
- log_debug(f"HTTP POST form attempt {attempt}/{max_retries} to {url}")
7372
-
7373
- # Build curl command
7374
- # -f flag makes curl return non-zero exit code on HTTP 4xx/5xx errors
7375
- curl_cmd = [
7376
- "curl", "-f", "-X", "POST", url,
7377
- "-H", f"Authorization: Bearer {API_TOKEN}",
7378
- "--connect-timeout", str(HTTP_CONNECT_TIMEOUT),
7379
- "--max-time", str(HTTP_MAX_TIME_UPLOAD),
7380
- "--silent"
7381
- ]
7382
-
7383
- if VERCEL_BYPASS:
7384
- curl_cmd.extend(["-H", f"x-vercel-protection-bypass: {VERCEL_BYPASS}"])
7385
-
7386
- # Add form fields
7387
- for key, value in form_fields.items():
7388
- curl_cmd.extend(["-F", f"{key}={value}"])
7389
-
7390
- # Add file if provided
7391
- if file_path:
7392
- curl_cmd.extend(["-F", f"{file_field}=@{file_path}"])
7393
-
7394
- result = None # Initialize for use in except blocks
7395
- try:
7396
- result = subprocess.run(
7397
- curl_cmd,
7398
- capture_output=True,
7399
- text=True,
7400
- timeout=HTTP_MAX_TIME_UPLOAD
7401
- )
7402
-
7403
- if result.returncode == 0:
7404
- if result.stdout:
7405
- return json.loads(result.stdout)
7406
- return {}
7407
-
7408
- # Log curl exit code and stderr for better debugging
7409
- error_msg = f"curl exit {result.returncode}"
7410
- if result.stderr:
7411
- error_msg += f": {result.stderr.strip()}"
7412
- log_warn(f"HTTP POST form failed (attempt {attempt}/{max_retries}): {error_msg}")
7413
- if attempt < max_retries:
7414
- time.sleep(1)
7415
-
7416
- except subprocess.TimeoutExpired:
7417
- log_warn(f"HTTP POST form failed (attempt {attempt}/{max_retries}): Timeout")
7418
- if attempt < max_retries:
7419
- time.sleep(1)
7420
- except json.JSONDecodeError as e:
7421
- log_warn(f"HTTP POST form failed (attempt {attempt}/{max_retries}): Invalid JSON response: {e}")
7422
- # Log raw response for debugging (truncate to avoid log spam)
7423
- if result and result.stdout:
7424
- log_debug(f"Raw response: {result.stdout[:500]}")
7425
- if attempt < max_retries:
7426
- time.sleep(1)
7427
- except Exception as e:
7428
- log_warn(f"HTTP POST form failed (attempt {attempt}/{max_retries}): {e}")
7429
- if attempt < max_retries:
7430
- time.sleep(1)
7431
-
7432
- log_error(f"HTTP POST form failed after {max_retries} attempts to {url}")
7433
- return None
7434
-
7435
-
7436
- def http_put_presigned(
7437
- presigned_url: str,
7438
- file_path: str,
7439
- content_type: str = "application/octet-stream",
7440
- max_retries: int = HTTP_MAX_RETRIES
7441
- ) -> bool:
7442
- """
7443
- HTTP PUT to a presigned S3 URL with retry logic.
7444
- Used for direct S3 uploads bypassing Vercel's 4.5MB limit.
7445
-
7446
- Args:
7447
- presigned_url: S3 presigned PUT URL
7448
- file_path: Path to file to upload
7449
- content_type: Content-Type header value
7450
- max_retries: Maximum retry attempts
7451
-
7452
- Returns:
7453
- True on success, False on failure
7454
- """
7455
- for attempt in range(1, max_retries + 1):
7456
- log_debug(f"HTTP PUT presigned attempt {attempt}/{max_retries}")
7457
-
7458
- try:
7459
- # Use curl for reliable large file uploads
7460
- curl_cmd = [
7461
- "curl", "-f", "-X", "PUT",
7462
- "-H", f"Content-Type: {content_type}",
7463
- "--data-binary", f"@{file_path}",
7464
- "--connect-timeout", str(HTTP_CONNECT_TIMEOUT),
7465
- "--max-time", str(HTTP_MAX_TIME_UPLOAD),
7466
- "--silent",
7467
- presigned_url
7468
- ]
7469
-
7470
- result = subprocess.run(
7471
- curl_cmd,
7472
- capture_output=True,
7473
- text=True,
7474
- timeout=HTTP_MAX_TIME_UPLOAD
7475
- )
7476
-
7477
- if result.returncode == 0:
7478
- return True
7479
-
7480
- error_msg = f"curl exit {result.returncode}"
7481
- if result.stderr:
7482
- error_msg += f": {result.stderr.strip()}"
7483
- log_warn(f"HTTP PUT presigned failed (attempt {attempt}/{max_retries}): {error_msg}")
7484
- if attempt < max_retries:
7485
- time.sleep(1)
7486
-
7487
- except subprocess.TimeoutExpired:
7488
- log_warn(f"HTTP PUT presigned failed (attempt {attempt}/{max_retries}): Timeout")
7489
- if attempt < max_retries:
7490
- time.sleep(1)
7491
- except Exception as e:
7492
- log_warn(f"HTTP PUT presigned failed (attempt {attempt}/{max_retries}): {e}")
7493
- if attempt < max_retries:
7494
- time.sleep(1)
7495
-
7496
- log_error(f"HTTP PUT presigned failed after {max_retries} attempts")
7497
- return False
7498
-
7499
-
7500
- def http_download(
7501
- url: str,
7502
- dest_path: str,
7503
- max_retries: int = HTTP_MAX_RETRIES
7504
- ) -> bool:
7505
- """
7506
- Download a file from URL with retry logic.
7507
-
7508
- Args:
7509
- url: Source URL
7510
- dest_path: Destination file path
7511
- max_retries: Maximum retry attempts
7512
-
7513
- Returns:
7514
- True on success, False on failure
7515
- """
7516
- for attempt in range(1, max_retries + 1):
7517
- log_debug(f"HTTP download attempt {attempt}/{max_retries} from {url}")
7518
-
7519
- try:
7520
- curl_cmd = [
7521
- "curl", "-fsSL",
7522
- "-o", dest_path,
7523
- url
7524
- ]
7525
-
7526
- result = subprocess.run(
7527
- curl_cmd,
7528
- capture_output=True,
7529
- timeout=HTTP_MAX_TIME_UPLOAD
7530
- )
7531
-
7532
- if result.returncode == 0:
7533
- return True
7534
-
7535
- log_warn(f"HTTP download failed (attempt {attempt}/{max_retries}): curl exit {result.returncode}")
7536
- if attempt < max_retries:
7537
- time.sleep(1)
7538
-
7539
- except subprocess.TimeoutExpired:
7540
- log_warn(f"HTTP download failed (attempt {attempt}/{max_retries}): Timeout")
7541
- if attempt < max_retries:
7542
- time.sleep(1)
7543
- except Exception as e:
7544
- log_warn(f"HTTP download failed (attempt {attempt}/{max_retries}): {e}")
7545
- if attempt < max_retries:
7546
- time.sleep(1)
7547
-
7548
- log_error(f"HTTP download failed after {max_retries} attempts from {url}")
7549
- return False
7550
- `;
7551
-
7552
- // ../../packages/core/src/sandbox/scripts/lib/events.py.ts
7553
- var EVENTS_SCRIPT = `#!/usr/bin/env python3
7554
- """
7555
- Event sending module for VM0 agent scripts.
7556
- Sends JSONL events to the webhook endpoint.
7557
- Masks secrets before sending using client-side masking.
7558
- """
7559
- import os
7560
- from typing import Dict, Any
7561
-
7562
- from common import (
7563
- RUN_ID, WORKING_DIR, WEBHOOK_URL, CLI_AGENT_TYPE,
7564
- SESSION_ID_FILE, SESSION_HISTORY_PATH_FILE, EVENT_ERROR_FLAG
7565
- )
7566
- from log import log_info, log_error
7567
- from http_client import http_post_json
7568
- from secret_masker import mask_data
7569
-
7570
-
7571
- def send_event(event: Dict[str, Any], sequence_number: int) -> bool:
7572
- """
7573
- Send single event immediately to webhook.
7574
- Masks secrets before sending.
7575
-
7576
- Args:
7577
- event: Event dictionary to send
7578
- sequence_number: Sequence number for this event (1-based, maintained by caller)
7579
-
7580
- Returns:
7581
- True on success, False on failure
7582
- """
7583
- # Extract session ID from init event based on CLI agent type
7584
- event_type = event.get("type", "")
7585
- event_subtype = event.get("subtype", "")
7586
-
7587
- # Claude Code: session_id from system/init event
7588
- # Codex: thread_id from thread.started event
7589
- session_id = None
7590
- if CLI_AGENT_TYPE == "codex":
7591
- if event_type == "thread.started":
7592
- session_id = event.get("thread_id", "")
7593
- else:
7594
- if event_type == "system" and event_subtype == "init":
7595
- session_id = event.get("session_id", "")
7596
-
7597
- if session_id and not os.path.exists(SESSION_ID_FILE):
7598
- log_info(f"Captured session ID: {session_id}")
7599
-
7600
- # Save to temp file to persist across subprocesses
7601
- with open(SESSION_ID_FILE, "w") as f:
7602
- f.write(session_id)
7603
-
7604
- # Calculate session history path based on CLI agent type
7605
- home_dir = os.environ.get("HOME", "/home/user")
7606
-
7607
- if CLI_AGENT_TYPE == "codex":
7608
- # Codex stores sessions in ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl
7609
- # We'll store a marker path here; checkpoint.py will search for the actual file
7610
- codex_home = os.environ.get("CODEX_HOME", f"{home_dir}/.codex")
7611
- # Use special marker format that checkpoint.py will recognize
7612
- session_history_path = f"CODEX_SEARCH:{codex_home}/sessions:{session_id}"
7613
- else:
7614
- # Claude Code uses ~/.claude (default, no CLAUDE_CONFIG_DIR override)
7615
- # Path encoding: e.g., /home/user/workspace -> -home-user-workspace
7616
- project_name = WORKING_DIR.lstrip("/").replace("/", "-")
7617
- session_history_path = f"{home_dir}/.claude/projects/-{project_name}/{session_id}.jsonl"
7618
-
7619
- with open(SESSION_HISTORY_PATH_FILE, "w") as f:
7620
- f.write(session_history_path)
7621
-
7622
- log_info(f"Session history will be at: {session_history_path}")
7623
-
7624
- # Add sequence number to event
7625
- event["sequenceNumber"] = sequence_number
7626
-
7627
- # Mask secrets in event data before sending
7628
- # This ensures secrets are never sent to the server in plaintext
7629
- masked_event = mask_data(event)
7630
-
7631
- # Build payload with masked event
7632
- payload = {
7633
- "runId": RUN_ID,
7634
- "events": [masked_event]
7635
- }
7636
-
7637
- # Send event using HTTP request function
7638
- result = http_post_json(WEBHOOK_URL, payload)
7639
-
7640
- if result is None:
7641
- log_error("Failed to send event after retries")
7642
- # Mark that event sending failed - run-agent will check this
7643
- with open(EVENT_ERROR_FLAG, "w") as f:
7644
- f.write("1")
7645
- return False
7646
-
7647
- return True
7648
- `;
7649
-
7650
- // ../../packages/core/src/sandbox/scripts/lib/direct_upload.py.ts
7651
- var DIRECT_UPLOAD_SCRIPT = `#!/usr/bin/env python3
7652
- """
7653
- Direct S3 upload module for VAS (Versioned Artifact Storage).
7654
- Bypasses Vercel's 4.5MB request body limit by uploading directly to S3.
7655
-
7656
- Flow:
7657
- 1. Compute file hashes locally
7658
- 2. Call /api/storages/prepare to get presigned URLs
7659
- 3. Upload archive and manifest directly to S3
7660
- 4. Call /api/storages/commit to finalize
7661
- """
7662
- import os
7663
- import json
7664
- import hashlib
7665
- import tarfile
7666
- import tempfile
7667
- import shutil
7668
- import time
7669
- from typing import Optional, Dict, Any, List
7670
- from datetime import datetime
7671
-
7672
- from common import RUN_ID, STORAGE_PREPARE_URL, STORAGE_COMMIT_URL, record_sandbox_op
7673
- from log import log_info, log_warn, log_error, log_debug
7674
- from http_client import http_post_json, http_put_presigned
7675
-
7676
-
7677
- def compute_file_hash(file_path: str) -> str:
7678
- """Compute SHA-256 hash for a file."""
7679
- sha256_hash = hashlib.sha256()
7680
- with open(file_path, "rb") as f:
7681
- for byte_block in iter(lambda: f.read(4096), b""):
7682
- sha256_hash.update(byte_block)
7683
- return sha256_hash.hexdigest()
7684
-
7685
-
7686
- def collect_file_metadata(dir_path: str) -> List[Dict[str, Any]]:
7687
- """
7688
- Collect file metadata with hashes for a directory.
7689
-
7690
- Args:
7691
- dir_path: Directory to scan
7692
-
7693
- Returns:
7694
- List of file entries: [{path, hash, size}, ...]
7695
- """
7696
- files = []
7697
- original_dir = os.getcwd()
7698
-
7699
- try:
7700
- os.chdir(dir_path)
7701
-
7702
- for root, dirs, filenames in os.walk("."):
7703
- # Exclude .git and .vm0 directories
7704
- dirs[:] = [d for d in dirs if d not in (".git", ".vm0")]
7705
-
7706
- for filename in filenames:
7707
- rel_path = os.path.join(root, filename)
7708
- # Remove leading ./
7709
- if rel_path.startswith("./"):
7710
- rel_path = rel_path[2:]
7711
-
7712
- full_path = os.path.join(dir_path, rel_path)
7713
- try:
7714
- file_hash = compute_file_hash(full_path)
7715
- file_size = os.path.getsize(full_path)
7716
- files.append({
7717
- "path": rel_path,
7718
- "hash": file_hash,
7719
- "size": file_size
7720
- })
7721
- except (IOError, OSError) as e:
7722
- log_warn(f"Could not process file {rel_path}: {e}")
7723
-
7724
- finally:
7725
- os.chdir(original_dir)
7726
-
7727
- return files
7728
-
7729
-
7730
- def create_archive(dir_path: str, tar_path: str) -> bool:
7731
- """
7732
- Create tar.gz archive of directory contents.
7733
-
7734
- Args:
7735
- dir_path: Source directory
7736
- tar_path: Destination tar.gz path
7737
-
7738
- Returns:
7739
- True on success, False on failure
7740
- """
7741
- original_dir = os.getcwd()
7742
-
7743
- try:
7744
- os.chdir(dir_path)
7745
-
7746
- # Get files to archive (exclude .git and .vm0)
7747
- items = [item for item in os.listdir(".") if item not in (".git", ".vm0")]
7748
-
7749
- with tarfile.open(tar_path, "w:gz") as tar:
7750
- for item in items:
7751
- tar.add(item)
7752
-
7753
- return True
7754
-
7755
- except Exception as e:
7756
- log_error(f"Failed to create archive: {e}")
7757
- return False
7758
-
7759
- finally:
7760
- os.chdir(original_dir)
7761
-
7762
-
7763
- def create_manifest(files: List[Dict[str, Any]], manifest_path: str) -> bool:
7764
- """
7765
- Create manifest JSON file.
7766
-
7767
- Args:
7768
- files: List of file entries
7769
- manifest_path: Destination path for manifest
7770
-
7771
- Returns:
7772
- True on success, False on failure
7773
- """
7774
- try:
7775
- manifest = {
7776
- "version": 1,
7777
- "files": files,
7778
- "createdAt": datetime.utcnow().isoformat() + "Z"
7779
- }
7780
- with open(manifest_path, "w") as f:
7781
- json.dump(manifest, f, indent=2)
7782
- return True
7783
- except Exception as e:
7784
- log_error(f"Failed to create manifest: {e}")
7785
- return False
7786
-
7787
-
7788
- def create_direct_upload_snapshot(
7789
- mount_path: str,
7790
- storage_name: str,
7791
- storage_type: str = "artifact",
7792
- run_id: str = None,
7793
- message: str = None
7794
- ) -> Optional[Dict[str, Any]]:
7795
- """
7796
- Create VAS snapshot using direct S3 upload.
7797
- Bypasses Vercel's 4.5MB request body limit.
7798
-
7799
- Args:
7800
- mount_path: Path to the storage directory
7801
- storage_name: VAS storage name
7802
- storage_type: Storage type ("volume" or "artifact")
7803
- run_id: Optional run ID for sandbox auth
7804
- message: Optional commit message
7805
-
7806
- Returns:
7807
- Dict with versionId on success, None on failure
7808
- """
7809
- log_info(f"Creating direct upload snapshot for '{storage_name}' (type: {storage_type})")
7810
-
7811
- # Step 1: Collect file metadata
7812
- log_info("Computing file hashes...")
7813
- hash_start = time.time()
7814
- files = collect_file_metadata(mount_path)
7815
- record_sandbox_op("artifact_hash_compute", int((time.time() - hash_start) * 1000), True)
7816
- log_info(f"Found {len(files)} files")
7817
-
7818
- if not files:
7819
- log_info("No files to upload, creating empty version")
7820
-
7821
- # Step 2: Call prepare endpoint
7822
- log_info("Calling prepare endpoint...")
7823
- prepare_start = time.time()
7824
- prepare_payload = {
7825
- "storageName": storage_name,
7826
- "storageType": storage_type,
7827
- "files": files
7828
- }
7829
- if run_id:
7830
- prepare_payload["runId"] = run_id
7831
-
7832
- prepare_response = http_post_json(STORAGE_PREPARE_URL, prepare_payload)
7833
- if not prepare_response:
7834
- log_error("Failed to call prepare endpoint")
7835
- record_sandbox_op("artifact_prepare_api", int((time.time() - prepare_start) * 1000), False)
7836
- return None
7837
-
7838
- version_id = prepare_response.get("versionId")
7839
- if not version_id:
7840
- log_error(f"Invalid prepare response: {prepare_response}")
7841
- record_sandbox_op("artifact_prepare_api", int((time.time() - prepare_start) * 1000), False)
7842
- return None
7843
- record_sandbox_op("artifact_prepare_api", int((time.time() - prepare_start) * 1000), True)
7844
-
7845
- # Step 3: Check if version already exists (deduplication)
7846
- # Still call commit to update HEAD pointer (fixes #649)
7847
- if prepare_response.get("existing"):
7848
- log_info(f"Version already exists (deduplicated): {version_id[:8]}")
7849
- log_info("Updating HEAD pointer...")
7850
-
7851
- commit_payload = {
7852
- "storageName": storage_name,
7853
- "storageType": storage_type,
7854
- "versionId": version_id,
7855
- "files": files
7856
- }
7857
- if run_id:
7858
- commit_payload["runId"] = run_id
7859
-
7860
- commit_response = http_post_json(STORAGE_COMMIT_URL, commit_payload)
7861
- if not commit_response or not commit_response.get("success"):
7862
- log_error(f"Failed to update HEAD: {commit_response}")
7863
- return None
7864
-
7865
- return {"versionId": version_id, "deduplicated": True}
7866
-
7867
- # Step 4: Get presigned URLs
7868
- uploads = prepare_response.get("uploads")
7869
- if not uploads:
7870
- log_error("No upload URLs in prepare response")
7871
- return None
7872
-
7873
- archive_info = uploads.get("archive")
7874
- manifest_info = uploads.get("manifest")
7875
-
7876
- if not archive_info or not manifest_info:
7877
- log_error("Missing archive or manifest upload info")
7878
- return None
7879
-
7880
- # Step 5: Create and upload files
7881
- temp_dir = tempfile.mkdtemp(prefix=f"direct-upload-{storage_name}-")
7882
-
7883
- try:
7884
- # Create archive
7885
- log_info("Creating archive...")
7886
- archive_start = time.time()
7887
- archive_path = os.path.join(temp_dir, "archive.tar.gz")
7888
- if not create_archive(mount_path, archive_path):
7889
- log_error("Failed to create archive")
7890
- record_sandbox_op("artifact_archive_create", int((time.time() - archive_start) * 1000), False)
7891
- return None
7892
- record_sandbox_op("artifact_archive_create", int((time.time() - archive_start) * 1000), True)
7893
-
7894
- # Create manifest
7895
- log_info("Creating manifest...")
7896
- manifest_path = os.path.join(temp_dir, "manifest.json")
7897
- if not create_manifest(files, manifest_path):
7898
- log_error("Failed to create manifest")
7899
- return None
7900
-
7901
- # Upload archive to S3
7902
- log_info("Uploading archive to S3...")
7903
- s3_upload_start = time.time()
7904
- if not http_put_presigned(
7905
- archive_info["presignedUrl"],
7906
- archive_path,
7907
- "application/gzip"
7908
- ):
7909
- log_error("Failed to upload archive to S3")
7910
- record_sandbox_op("artifact_s3_upload", int((time.time() - s3_upload_start) * 1000), False)
7911
- return None
7912
-
7913
- # Upload manifest to S3
7914
- log_info("Uploading manifest to S3...")
7915
- if not http_put_presigned(
7916
- manifest_info["presignedUrl"],
7917
- manifest_path,
7918
- "application/json"
7919
- ):
7920
- log_error("Failed to upload manifest to S3")
7921
- record_sandbox_op("artifact_s3_upload", int((time.time() - s3_upload_start) * 1000), False)
7922
- return None
7923
- record_sandbox_op("artifact_s3_upload", int((time.time() - s3_upload_start) * 1000), True)
7924
-
7925
- # Step 6: Call commit endpoint
7926
- log_info("Calling commit endpoint...")
7927
- commit_start = time.time()
7928
- commit_payload = {
7929
- "storageName": storage_name,
7930
- "storageType": storage_type,
7931
- "versionId": version_id,
7932
- "files": files
7933
- }
7934
- if run_id:
7935
- commit_payload["runId"] = run_id
7936
- if message:
7937
- commit_payload["message"] = message
7938
-
7939
- commit_response = http_post_json(STORAGE_COMMIT_URL, commit_payload)
7940
- if not commit_response:
7941
- log_error("Failed to call commit endpoint")
7942
- record_sandbox_op("artifact_commit_api", int((time.time() - commit_start) * 1000), False)
7943
- return None
7944
-
7945
- if not commit_response.get("success"):
7946
- log_error(f"Commit failed: {commit_response}")
7947
- record_sandbox_op("artifact_commit_api", int((time.time() - commit_start) * 1000), False)
7948
- return None
7949
- record_sandbox_op("artifact_commit_api", int((time.time() - commit_start) * 1000), True)
7950
-
7951
- log_info(f"Direct upload snapshot created: {version_id[:8]}")
7952
- return {"versionId": version_id}
7953
-
7954
- finally:
7955
- # Cleanup temp files
7956
- shutil.rmtree(temp_dir, ignore_errors=True)
7957
- `;
7958
-
7959
- // ../../packages/core/src/sandbox/scripts/lib/download.py.ts
7960
- var DOWNLOAD_SCRIPT = `#!/usr/bin/env python3
7961
- """
7962
- Download storages script for E2B sandbox.
7963
- Downloads tar.gz archives directly from S3 using presigned URLs.
7964
-
7965
- Usage: python download.py <manifest_path>
7966
- """
7967
- import os
7968
- import sys
7969
- import json
7970
- import tarfile
7971
- import tempfile
7972
- import time
7973
-
7974
- # Add lib to path for imports
7975
- sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
7976
-
7977
- from common import validate_config, record_sandbox_op
7978
- from log import log_info, log_error
7979
- from http_client import http_download
7980
-
7981
-
7982
- def download_storage(mount_path: str, archive_url: str) -> bool:
7983
- """
7984
- Download and extract a single storage/artifact.
7985
-
7986
- Args:
7987
- mount_path: Destination mount path
7988
- archive_url: Presigned S3 URL for tar.gz archive
7989
-
7990
- Returns:
7991
- True on success, False on failure
7992
- """
7993
- log_info(f"Downloading storage to {mount_path}")
7994
-
7995
- # Create temp file for download
7996
- temp_tar = tempfile.mktemp(suffix=".tar.gz", prefix="storage-")
7997
-
7998
- try:
7999
- # Download tar.gz with retry
8000
- if not http_download(archive_url, temp_tar):
8001
- log_error(f"Failed to download archive for {mount_path}")
8002
- return False
8003
-
8004
- # Create mount path directory
8005
- os.makedirs(mount_path, exist_ok=True)
8006
-
8007
- # Extract to mount path (handle empty archive gracefully)
8008
- try:
8009
- with tarfile.open(temp_tar, "r:gz") as tar:
8010
- tar.extractall(path=mount_path)
8011
- except tarfile.ReadError:
8012
- # Empty or invalid archive - not a fatal error
8013
- log_info(f"Archive appears empty for {mount_path}")
8014
-
8015
- log_info(f"Successfully extracted to {mount_path}")
8016
- return True
8017
-
8018
- finally:
8019
- # Cleanup temp file
8020
- try:
8021
- os.remove(temp_tar)
8022
- except OSError:
8023
- pass
8024
-
8025
-
8026
- def main():
8027
- """Main entry point for download storages script."""
8028
- if len(sys.argv) < 2:
8029
- log_error("Usage: python download.py <manifest_path>")
8030
- sys.exit(1)
8031
-
8032
- manifest_path = sys.argv[1]
8033
-
8034
- if not os.path.exists(manifest_path):
8035
- log_error(f"Manifest file not found: {manifest_path}")
8036
- sys.exit(1)
8037
-
8038
- log_info(f"Starting storage download from manifest: {manifest_path}")
8039
-
8040
- # Load manifest
8041
- try:
8042
- with open(manifest_path) as f:
8043
- manifest = json.load(f)
8044
- except (IOError, json.JSONDecodeError) as e:
8045
- log_error(f"Failed to load manifest: {e}")
8046
- sys.exit(1)
8047
-
8048
- # Count total storages
8049
- storages = manifest.get("storages", [])
8050
- artifact = manifest.get("artifact")
8051
-
8052
- storage_count = len(storages)
8053
- has_artifact = artifact is not None
8054
-
8055
- log_info(f"Found {storage_count} storages, artifact: {has_artifact}")
8056
-
8057
- # Track total download time
8058
- download_total_start = time.time()
8059
- download_success = True
8060
-
8061
- # Process storages
8062
- for storage in storages:
8063
- mount_path = storage.get("mountPath")
8064
- archive_url = storage.get("archiveUrl")
8065
-
8066
- if archive_url and archive_url != "null":
8067
- storage_start = time.time()
8068
- success = download_storage(mount_path, archive_url)
8069
- record_sandbox_op("storage_download", int((time.time() - storage_start) * 1000), success)
8070
- if not success:
8071
- download_success = False
8072
-
8073
- # Process artifact
8074
- if artifact:
8075
- artifact_mount = artifact.get("mountPath")
8076
- artifact_url = artifact.get("archiveUrl")
8077
-
8078
- if artifact_url and artifact_url != "null":
8079
- artifact_start = time.time()
8080
- success = download_storage(artifact_mount, artifact_url)
8081
- record_sandbox_op("artifact_download", int((time.time() - artifact_start) * 1000), success)
8082
- if not success:
8083
- download_success = False
8084
-
8085
- # Record total download time
8086
- record_sandbox_op("download_total", int((time.time() - download_total_start) * 1000), download_success)
8087
- log_info("All storages downloaded successfully")
8088
-
8089
-
8090
- if __name__ == "__main__":
8091
- main()
8092
- `;
8093
-
8094
- // ../../packages/core/src/sandbox/scripts/lib/checkpoint.py.ts
8095
- var CHECKPOINT_SCRIPT = `#!/usr/bin/env python3
8096
- """
8097
- Checkpoint creation module.
8098
- Creates checkpoints with conversation history and optional artifact snapshot (VAS only).
8099
- Uses direct S3 upload exclusively (no fallback to legacy methods).
8100
- """
8101
- import os
8102
- import glob
8103
- import time
8104
- from typing import Optional, Dict, Any
8105
-
8106
- from common import (
8107
- RUN_ID, CHECKPOINT_URL,
8108
- SESSION_ID_FILE, SESSION_HISTORY_PATH_FILE,
8109
- ARTIFACT_DRIVER, ARTIFACT_MOUNT_PATH, ARTIFACT_VOLUME_NAME,
8110
- record_sandbox_op
8111
- )
8112
- from log import log_info, log_error
8113
- from http_client import http_post_json
8114
- from direct_upload import create_direct_upload_snapshot
8115
-
8116
-
8117
- def find_codex_session_file(sessions_dir: str, session_id: str) -> Optional[str]:
8118
- """
8119
- Find Codex session file by searching in date-organized directories.
8120
- Codex stores sessions in: ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl
8121
-
8122
- Args:
8123
- sessions_dir: Base sessions directory (e.g., ~/.codex/sessions)
8124
- session_id: Session ID to find (e.g., 019b3aca-2df2-7573-8f88-4240b7bc350a)
8125
-
8126
- Returns:
8127
- Full path to session file, or None if not found
8128
- """
8129
- # Search for session file containing the session ID
8130
- # Pattern: sessions/YYYY/MM/DD/rollout-*-{session_id_parts}.jsonl
8131
- # The session ID parts may be separated by dashes in the filename
8132
-
8133
- # First, try searching all JSONL files recursively
8134
- search_pattern = os.path.join(sessions_dir, "**", "*.jsonl")
8135
- files = glob.glob(search_pattern, recursive=True)
8136
-
8137
- log_info(f"Searching for Codex session {session_id} in {len(files)} files")
8138
-
8139
- # The session ID in Codex filenames uses the format with dashes
8140
- # e.g., rollout-2025-12-20T08-04-44-019b3aca-2df2-7573-8f88-4240b7bc350a.jsonl
8141
- for filepath in files:
8142
- filename = os.path.basename(filepath)
8143
- # Check if session ID is in the filename
8144
- if session_id in filename or session_id.replace("-", "") in filename.replace("-", ""):
8145
- log_info(f"Found Codex session file: {filepath}")
8146
- return filepath
8147
-
8148
- # If not found by ID match, get the most recent file (fallback)
8149
- if files:
8150
- # Sort by modification time, newest first
8151
- files.sort(key=lambda x: os.path.getmtime(x), reverse=True)
8152
- most_recent = files[0]
8153
- log_info(f"Session ID not found in filenames, using most recent: {most_recent}")
8154
- return most_recent
8155
-
8156
- return None
8157
-
8158
-
8159
- def create_checkpoint() -> bool:
8160
- """
8161
- Create checkpoint after successful run.
8162
-
8163
- Returns:
8164
- True on success, False on failure
8165
- """
8166
- checkpoint_start = time.time()
8167
- log_info("Creating checkpoint...")
8168
-
8169
- # Read session ID from temp file
8170
- session_id_start = time.time()
8171
- if not os.path.exists(SESSION_ID_FILE):
8172
- log_error("No session ID found, checkpoint creation failed")
8173
- record_sandbox_op("session_id_read", int((time.time() - session_id_start) * 1000), False, "Session ID file not found")
8174
- record_sandbox_op("checkpoint_total", int((time.time() - checkpoint_start) * 1000), False)
8175
- return False
8176
-
8177
- with open(SESSION_ID_FILE) as f:
8178
- cli_agent_session_id = f.read().strip()
8179
- record_sandbox_op("session_id_read", int((time.time() - session_id_start) * 1000), True)
8180
-
8181
- # Read session history path from temp file
8182
- session_history_start = time.time()
8183
- if not os.path.exists(SESSION_HISTORY_PATH_FILE):
8184
- log_error("No session history path found, checkpoint creation failed")
8185
- record_sandbox_op("session_history_read", int((time.time() - session_history_start) * 1000), False, "Session history path file not found")
8186
- record_sandbox_op("checkpoint_total", int((time.time() - checkpoint_start) * 1000), False)
8187
- return False
8188
-
8189
- with open(SESSION_HISTORY_PATH_FILE) as f:
8190
- session_history_path_raw = f.read().strip()
8191
-
8192
- # Handle Codex session search marker format: CODEX_SEARCH:{sessions_dir}:{session_id}
8193
- if session_history_path_raw.startswith("CODEX_SEARCH:"):
8194
- parts = session_history_path_raw.split(":", 2)
8195
- if len(parts) != 3:
8196
- log_error(f"Invalid Codex search marker format: {session_history_path_raw}")
8197
- record_sandbox_op("session_history_read", int((time.time() - session_history_start) * 1000), False, "Invalid Codex search marker")
8198
- record_sandbox_op("checkpoint_total", int((time.time() - checkpoint_start) * 1000), False)
8199
- return False
8200
- sessions_dir = parts[1]
8201
- codex_session_id = parts[2]
8202
- log_info(f"Searching for Codex session in {sessions_dir}")
8203
- session_history_path = find_codex_session_file(sessions_dir, codex_session_id)
8204
- if not session_history_path:
8205
- log_error(f"Could not find Codex session file for {codex_session_id} in {sessions_dir}")
8206
- record_sandbox_op("session_history_read", int((time.time() - session_history_start) * 1000), False, "Codex session file not found")
8207
- record_sandbox_op("checkpoint_total", int((time.time() - checkpoint_start) * 1000), False)
8208
- return False
8209
- else:
8210
- session_history_path = session_history_path_raw
8211
-
8212
- # Check if session history file exists
8213
- if not os.path.exists(session_history_path):
8214
- log_error(f"Session history file not found at {session_history_path}, checkpoint creation failed")
8215
- record_sandbox_op("session_history_read", int((time.time() - session_history_start) * 1000), False, "Session history file not found")
8216
- record_sandbox_op("checkpoint_total", int((time.time() - checkpoint_start) * 1000), False)
8217
- return False
8218
-
8219
- # Read session history
8220
- try:
8221
- with open(session_history_path) as f:
8222
- cli_agent_session_history = f.read()
8223
- except IOError as e:
8224
- log_error(f"Failed to read session history: {e}")
8225
- record_sandbox_op("session_history_read", int((time.time() - session_history_start) * 1000), False, str(e))
8226
- record_sandbox_op("checkpoint_total", int((time.time() - checkpoint_start) * 1000), False)
8227
- return False
8228
-
8229
- if not cli_agent_session_history.strip():
8230
- log_error("Session history is empty, checkpoint creation failed")
8231
- record_sandbox_op("session_history_read", int((time.time() - session_history_start) * 1000), False, "Session history empty")
8232
- record_sandbox_op("checkpoint_total", int((time.time() - checkpoint_start) * 1000), False)
8233
- return False
8234
-
8235
- line_count = len(cli_agent_session_history.strip().split("\\n"))
8236
- log_info(f"Session history loaded ({line_count} lines)")
8237
- record_sandbox_op("session_history_read", int((time.time() - session_history_start) * 1000), True)
8238
-
8239
- # CLI agent type (default to claude-code)
8240
- cli_agent_type = os.environ.get("CLI_AGENT_TYPE", "claude-code")
8241
-
8242
- # Create artifact snapshot (VAS only, optional)
8243
- # If artifact is not configured, checkpoint is created without artifact snapshot
8244
- artifact_snapshot = None
8245
-
8246
- if ARTIFACT_DRIVER and ARTIFACT_VOLUME_NAME:
8247
- log_info(f"Processing artifact with driver: {ARTIFACT_DRIVER}")
8248
-
8249
- if ARTIFACT_DRIVER != "vas":
8250
- log_error(f"Unknown artifact driver: {ARTIFACT_DRIVER} (only 'vas' is supported)")
8251
- record_sandbox_op("checkpoint_total", int((time.time() - checkpoint_start) * 1000), False)
8252
- return False
8253
-
8254
- # VAS artifact: create snapshot using direct S3 upload (bypasses Vercel 4.5MB limit)
8255
- log_info(f"Creating VAS snapshot for artifact '{ARTIFACT_VOLUME_NAME}' at {ARTIFACT_MOUNT_PATH}")
8256
- log_info("Using direct S3 upload...")
8257
-
8258
- snapshot = create_direct_upload_snapshot(
8259
- ARTIFACT_MOUNT_PATH,
8260
- ARTIFACT_VOLUME_NAME,
8261
- "artifact",
8262
- RUN_ID,
8263
- f"Checkpoint from run {RUN_ID}"
8264
- )
8265
-
8266
- if not snapshot:
8267
- log_error("Failed to create VAS snapshot for artifact")
8268
- record_sandbox_op("checkpoint_total", int((time.time() - checkpoint_start) * 1000), False)
8269
- return False
8270
-
8271
- # Extract versionId from snapshot response
8272
- artifact_version = snapshot.get("versionId")
8273
- if not artifact_version:
8274
- log_error("Failed to extract versionId from snapshot")
8275
- record_sandbox_op("checkpoint_total", int((time.time() - checkpoint_start) * 1000), False)
8276
- return False
8277
-
8278
- # Build artifact snapshot JSON with new format (artifactName + artifactVersion)
8279
- artifact_snapshot = {
8280
- "artifactName": ARTIFACT_VOLUME_NAME,
8281
- "artifactVersion": artifact_version
8282
- }
8283
-
8284
- log_info(f"VAS artifact snapshot created: {ARTIFACT_VOLUME_NAME}@{artifact_version}")
8285
- else:
8286
- log_info("No artifact configured, creating checkpoint without artifact snapshot")
8287
-
8288
- log_info("Calling checkpoint API...")
8289
-
8290
- # Build checkpoint payload with new schema
8291
- checkpoint_payload = {
8292
- "runId": RUN_ID,
8293
- "cliAgentType": cli_agent_type,
8294
- "cliAgentSessionId": cli_agent_session_id,
8295
- "cliAgentSessionHistory": cli_agent_session_history
8296
- }
8297
-
8298
- # Only add artifact snapshot if present
8299
- if artifact_snapshot:
8300
- checkpoint_payload["artifactSnapshot"] = artifact_snapshot
8301
-
8302
- # Call checkpoint API
8303
- api_call_start = time.time()
8304
- result = http_post_json(CHECKPOINT_URL, checkpoint_payload)
8305
-
8306
- # Validate response contains checkpointId to confirm checkpoint was actually created
8307
- # Note: result can be {} (empty dict) on network issues, which is not None but invalid
8308
- if result and result.get("checkpointId"):
8309
- checkpoint_id = result.get("checkpointId")
8310
- log_info(f"Checkpoint created successfully: {checkpoint_id}")
8311
- record_sandbox_op("checkpoint_api_call", int((time.time() - api_call_start) * 1000), True)
8312
- record_sandbox_op("checkpoint_total", int((time.time() - checkpoint_start) * 1000), True)
8313
- return True
8314
- else:
8315
- log_error(f"Checkpoint API returned invalid response: {result}")
8316
- record_sandbox_op("checkpoint_api_call", int((time.time() - api_call_start) * 1000), False, "Invalid API response")
8317
- record_sandbox_op("checkpoint_total", int((time.time() - checkpoint_start) * 1000), False)
8318
- return False
8319
- `;
8320
-
8321
- // ../../packages/core/src/sandbox/scripts/lib/mock_claude.py.ts
8322
- var MOCK_CLAUDE_SCRIPT = `#!/usr/bin/env python3
8323
- """
8324
- Mock Claude CLI for testing.
8325
- Executes prompt as bash and outputs Claude-compatible JSONL.
8326
-
8327
- Usage: mock_claude.py [options] <prompt>
8328
- The prompt is executed as a bash command.
8329
-
8330
- Special test prefixes:
8331
- @fail:<message> - Output message to stderr and exit with code 1
8332
- """
8333
- import os
8334
- import sys
8335
- import json
8336
- import subprocess
8337
- import time
8338
- import argparse
8339
-
8340
-
8341
- def json_escape(s: str) -> str:
8342
- """Escape string for JSON."""
8343
- return json.dumps(s)
8344
-
8345
-
8346
- def create_session_history(session_id: str, cwd: str) -> str:
8347
- """
8348
- Create session history file for checkpoint compatibility.
8349
- Claude Code stores session history at: ~/.claude/projects/-{path}/{session_id}.jsonl
8350
- """
8351
- project_name = cwd.lstrip("/").replace("/", "-")
8352
- home_dir = os.environ.get("HOME", "/home/user")
8353
- session_dir = f"{home_dir}/.claude/projects/-{project_name}"
8354
- os.makedirs(session_dir, exist_ok=True)
8355
- return f"{session_dir}/{session_id}.jsonl"
8356
-
8357
-
8358
- def main():
8359
- """Main entry point for mock Claude."""
8360
- # Generate session ID
8361
- session_id = f"mock-{int(time.time() * 1000000)}"
8362
-
8363
- # Parse arguments (same as real claude CLI)
8364
- parser = argparse.ArgumentParser(add_help=False)
8365
- parser.add_argument("--output-format", default="text")
8366
- parser.add_argument("--print", action="store_true")
8367
- parser.add_argument("--verbose", action="store_true")
8368
- parser.add_argument("--dangerously-skip-permissions", action="store_true")
8369
- parser.add_argument("--resume", default=None)
8370
- parser.add_argument("prompt", nargs="?", default="")
8371
-
8372
- args, unknown = parser.parse_known_args()
8373
-
8374
- # Get prompt from remaining args if not set
8375
- prompt = args.prompt
8376
- if not prompt and unknown:
8377
- prompt = unknown[0]
8378
-
8379
- output_format = args.output_format
8380
-
8381
- # Special test prefix: @fail:<message> - simulate Claude failure with stderr output
8382
- # Usage: mock-claude "@fail:Session not found"
8383
- # This outputs the message to stderr and exits with code 1
8384
- if prompt.startswith("@fail:"):
8385
- error_msg = prompt[6:] # Remove "@fail:" prefix
8386
- print(error_msg, file=sys.stderr)
8387
- sys.exit(1)
8388
-
8389
- # Get current working directory
8390
- cwd = os.getcwd()
8391
-
8392
- if output_format == "stream-json":
8393
- # Create session history file path
8394
- session_history_file = create_session_history(session_id, cwd)
8395
-
8396
- events = []
8397
-
8398
- # 1. System init event
8399
- init_event = {
8400
- "type": "system",
8401
- "subtype": "init",
8402
- "cwd": cwd,
8403
- "session_id": session_id,
8404
- "tools": ["Bash"],
8405
- "model": "mock-claude"
8406
- }
8407
- print(json.dumps(init_event))
8408
- events.append(init_event)
8409
-
8410
- # 2. Assistant text event
8411
- text_event = {
8412
- "type": "assistant",
8413
- "message": {
8414
- "role": "assistant",
8415
- "content": [{"type": "text", "text": "Executing command..."}]
8416
- },
8417
- "session_id": session_id
8418
- }
8419
- print(json.dumps(text_event))
8420
- events.append(text_event)
8421
-
8422
- # 3. Assistant tool_use event
8423
- tool_use_event = {
8424
- "type": "assistant",
8425
- "message": {
8426
- "role": "assistant",
8427
- "content": [{
8428
- "type": "tool_use",
8429
- "id": "toolu_mock_001",
8430
- "name": "Bash",
8431
- "input": {"command": prompt}
8432
- }]
8433
- },
8434
- "session_id": session_id
8435
- }
8436
- print(json.dumps(tool_use_event))
8437
- events.append(tool_use_event)
8438
-
8439
- # 4. Execute prompt as bash and capture output
8440
- try:
8441
- result = subprocess.run(
8442
- ["bash", "-c", prompt],
8443
- capture_output=True,
8444
- text=True
8445
- )
8446
- output = result.stdout + result.stderr
8447
- exit_code = result.returncode
8448
- except Exception as e:
8449
- output = str(e)
8450
- exit_code = 1
8451
-
8452
- # 5. User tool_result event
8453
- is_error = exit_code != 0
8454
- tool_result_event = {
8455
- "type": "user",
8456
- "message": {
8457
- "role": "user",
8458
- "content": [{
8459
- "type": "tool_result",
8460
- "tool_use_id": "toolu_mock_001",
8461
- "content": output,
8462
- "is_error": is_error
8463
- }]
8464
- },
8465
- "session_id": session_id
8466
- }
8467
- print(json.dumps(tool_result_event))
8468
- events.append(tool_result_event)
8469
-
8470
- # 6. Result event
8471
- if exit_code == 0:
8472
- result_event = {
8473
- "type": "result",
8474
- "subtype": "success",
8475
- "is_error": False,
8476
- "duration_ms": 100,
8477
- "num_turns": 1,
8478
- "result": output,
8479
- "session_id": session_id,
8480
- "total_cost_usd": 0,
8481
- "usage": {"input_tokens": 0, "output_tokens": 0}
8482
- }
8483
- else:
8484
- result_event = {
8485
- "type": "result",
8486
- "subtype": "error",
8487
- "is_error": True,
8488
- "duration_ms": 100,
8489
- "num_turns": 1,
8490
- "result": output,
8491
- "session_id": session_id,
8492
- "total_cost_usd": 0,
8493
- "usage": {"input_tokens": 0, "output_tokens": 0}
8494
- }
8495
- print(json.dumps(result_event))
8496
- events.append(result_event)
8497
-
8498
- # Write all events to session history file
8499
- with open(session_history_file, "w") as f:
8500
- for event in events:
8501
- f.write(json.dumps(event) + "\\n")
8502
-
8503
- sys.exit(exit_code)
8504
-
8505
- else:
8506
- # Plain text output - just execute the prompt
8507
- try:
8508
- result = subprocess.run(
8509
- ["bash", "-c", prompt],
8510
- capture_output=False
8511
- )
8512
- sys.exit(result.returncode)
8513
- except Exception as e:
8514
- print(str(e), file=sys.stderr)
8515
- sys.exit(1)
8516
-
8517
-
8518
- if __name__ == "__main__":
8519
- main()
8520
- `;
8521
-
8522
- // ../../packages/core/src/sandbox/scripts/lib/metrics.py.ts
8523
- var METRICS_SCRIPT = `#!/usr/bin/env python3
8524
- """
8525
- Metrics collection module for VM0 sandbox.
8526
- Collects system resource metrics (CPU, memory, disk) and writes to JSONL file.
8527
- """
8528
- import json
8529
- import subprocess
8530
- import threading
8531
- from datetime import datetime, timezone
8532
-
8533
- from common import METRICS_LOG_FILE, METRICS_INTERVAL
8534
- from log import log_info, log_error, log_debug
8535
-
8536
-
8537
- def get_cpu_percent() -> float:
8538
- """
8539
- Get CPU usage percentage by parsing /proc/stat.
8540
- Returns the CPU usage as a percentage (0-100).
8541
- """
8542
- try:
8543
- with open("/proc/stat", "r") as f:
8544
- line = f.readline()
8545
-
8546
- # cpu user nice system idle iowait irq softirq steal guest guest_nice
8547
- parts = line.split()
8548
- if parts[0] != "cpu":
8549
- return 0.0
8550
-
8551
- values = [int(x) for x in parts[1:]]
8552
- idle = values[3] + values[4] # idle + iowait
8553
- total = sum(values)
8554
-
8555
- # Store for next calculation (we need delta)
8556
- # For simplicity, just return instantaneous value based on idle ratio
8557
- # This gives a rough estimate; for accurate CPU%, we'd need to track deltas
8558
- if total == 0:
8559
- return 0.0
8560
-
8561
- cpu_percent = 100.0 * (1.0 - idle / total)
8562
- return round(cpu_percent, 2)
8563
- except Exception as e:
8564
- log_debug(f"Failed to get CPU percent: {e}")
8565
- return 0.0
8566
-
8567
-
8568
- def get_memory_info() -> tuple[int, int]:
8569
- """
8570
- Get memory usage using 'free -b' command.
8571
- Returns (used, total) in bytes.
8572
- """
8573
- try:
8574
- result = subprocess.run(
8575
- ["free", "-b"],
8576
- capture_output=True,
8577
- text=True,
8578
- timeout=5
8579
- )
8580
-
8581
- if result.returncode != 0:
8582
- return (0, 0)
8583
-
8584
- # Parse output:
8585
- # Mem: total used free shared buff/cache available
8586
- lines = result.stdout.strip().split("\\n")
8587
- for line in lines:
8588
- if line.startswith("Mem:"):
8589
- parts = line.split()
8590
- total = int(parts[1])
8591
- used = int(parts[2])
8592
- return (used, total)
8593
-
8594
- return (0, 0)
8595
- except Exception as e:
8596
- log_debug(f"Failed to get memory info: {e}")
8597
- return (0, 0)
8598
-
8599
-
8600
- def get_disk_info() -> tuple[int, int]:
8601
- """
8602
- Get disk usage using 'df -B1 /' command.
8603
- Returns (used, total) in bytes.
8604
- """
8605
- try:
8606
- result = subprocess.run(
8607
- ["df", "-B1", "/"],
8608
- capture_output=True,
8609
- text=True,
8610
- timeout=5
8611
- )
8612
-
8613
- if result.returncode != 0:
8614
- return (0, 0)
8615
-
8616
- # Parse output:
8617
- # Filesystem 1B-blocks Used Available Use% Mounted
8618
- lines = result.stdout.strip().split("\\n")
8619
- if len(lines) < 2:
8620
- return (0, 0)
8621
-
8622
- # Skip header, parse data line
8623
- parts = lines[1].split()
8624
- total = int(parts[1])
8625
- used = int(parts[2])
8626
- return (used, total)
8627
- except Exception as e:
8628
- log_debug(f"Failed to get disk info: {e}")
8629
- return (0, 0)
8630
-
8631
-
8632
- def collect_metrics() -> dict:
8633
- """
8634
- Collect all system metrics and return as a dictionary.
8635
- """
8636
- cpu = get_cpu_percent()
8637
- mem_used, mem_total = get_memory_info()
8638
- disk_used, disk_total = get_disk_info()
8639
-
8640
- return {
8641
- "ts": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
8642
- "cpu": cpu,
8643
- "mem_used": mem_used,
8644
- "mem_total": mem_total,
8645
- "disk_used": disk_used,
8646
- "disk_total": disk_total
8647
- }
8648
-
8649
-
8650
- def metrics_collector_loop(shutdown_event: threading.Event) -> None:
8651
- """
8652
- Background loop that collects metrics every METRICS_INTERVAL seconds.
8653
- Writes metrics as JSONL to METRICS_LOG_FILE.
8654
- """
8655
- log_info(f"Metrics collector started, writing to {METRICS_LOG_FILE}")
8656
-
8657
- try:
8658
- with open(METRICS_LOG_FILE, "a") as f:
8659
- while not shutdown_event.is_set():
8660
- try:
8661
- metrics = collect_metrics()
8662
- f.write(json.dumps(metrics) + "\\n")
8663
- f.flush()
8664
- log_debug(f"Metrics collected: cpu={metrics['cpu']}%, mem={metrics['mem_used']}/{metrics['mem_total']}")
8665
- except Exception as e:
8666
- log_error(f"Failed to collect/write metrics: {e}")
8667
-
8668
- # Wait for interval or shutdown
8669
- shutdown_event.wait(METRICS_INTERVAL)
8670
- except Exception as e:
8671
- log_error(f"Metrics collector error: {e}")
8672
-
8673
- log_info("Metrics collector stopped")
8674
-
8675
-
8676
- def start_metrics_collector(shutdown_event: threading.Event) -> threading.Thread:
8677
- """
8678
- Start the metrics collector as a daemon thread.
8679
-
8680
- Args:
8681
- shutdown_event: Threading event to signal shutdown
8682
-
8683
- Returns:
8684
- The started thread (for joining if needed)
8685
- """
8686
- thread = threading.Thread(
8687
- target=metrics_collector_loop,
8688
- args=(shutdown_event,),
8689
- daemon=True,
8690
- name="metrics-collector"
8691
- )
8692
- thread.start()
8693
- return thread
8694
- `;
8695
-
8696
- // ../../packages/core/src/sandbox/scripts/lib/upload_telemetry.py.ts
8697
- var UPLOAD_TELEMETRY_SCRIPT = `#!/usr/bin/env python3
8698
- """
8699
- Telemetry upload module for VM0 sandbox.
8700
- Reads system log and metrics files, tracks position to avoid duplicates,
8701
- and uploads to the telemetry webhook endpoint.
8702
- Masks secrets before sending using client-side masking.
8703
- """
8704
- import json
8705
- import os
8706
- import threading
8707
- from typing import List, Dict, Any
8708
-
8709
- from common import (
8710
- RUN_ID, TELEMETRY_URL, TELEMETRY_INTERVAL,
8711
- SYSTEM_LOG_FILE, METRICS_LOG_FILE, NETWORK_LOG_FILE, SANDBOX_OPS_LOG_FILE,
8712
- TELEMETRY_LOG_POS_FILE, TELEMETRY_METRICS_POS_FILE, TELEMETRY_NETWORK_POS_FILE,
8713
- TELEMETRY_SANDBOX_OPS_POS_FILE
8714
- )
8715
- from log import log_info, log_error, log_debug, log_warn
8716
- from http_client import http_post_json
8717
- from secret_masker import mask_data
8718
-
8719
-
8720
- def read_file_from_position(file_path: str, pos_file: str) -> tuple[str, int]:
8721
- """
8722
- Read new content from file starting from last position.
8723
-
8724
- Args:
8725
- file_path: Path to the file to read
8726
- pos_file: Path to position tracking file
8727
-
8728
- Returns:
8729
- Tuple of (new_content, new_position)
8730
- """
8731
- # Get last read position
8732
- last_pos = 0
8733
- if os.path.exists(pos_file):
8734
- try:
8735
- with open(pos_file, "r") as f:
8736
- last_pos = int(f.read().strip())
8737
- except (ValueError, IOError):
8738
- last_pos = 0
8739
-
8740
- # Read new content
8741
- new_content = ""
8742
- new_pos = last_pos
8743
-
8744
- if os.path.exists(file_path):
8745
- try:
8746
- with open(file_path, "r") as f:
8747
- f.seek(last_pos)
8748
- new_content = f.read()
8749
- new_pos = f.tell()
8750
- except IOError as e:
8751
- log_debug(f"Failed to read {file_path}: {e}")
8752
-
8753
- return new_content, new_pos
8754
-
8755
-
8756
- def save_position(pos_file: str, position: int) -> None:
8757
- """Save file read position for next iteration."""
8758
- try:
8759
- with open(pos_file, "w") as f:
8760
- f.write(str(position))
8761
- except IOError as e:
8762
- log_debug(f"Failed to save position to {pos_file}: {e}")
8763
-
8764
-
8765
- def read_jsonl_from_position(file_path: str, pos_file: str) -> tuple[List[Dict[str, Any]], int]:
8766
- """
8767
- Read new entries from JSONL file starting from last position.
8768
-
8769
- Args:
8770
- file_path: Path to the JSONL file to read
8771
- pos_file: Path to position tracking file
8772
-
8773
- Returns:
8774
- Tuple of (entries list, new_position)
8775
- """
8776
- content, new_pos = read_file_from_position(file_path, pos_file)
8777
-
8778
- entries = []
8779
- if content:
8780
- for line in content.strip().split("\\n"):
8781
- if line:
8782
- try:
8783
- entries.append(json.loads(line))
8784
- except json.JSONDecodeError:
8785
- pass
8786
-
8787
- return entries, new_pos
8788
-
8789
-
8790
- def read_metrics_from_position(pos_file: str) -> tuple[List[Dict[str, Any]], int]:
8791
- """
8792
- Read new metrics from JSONL file starting from last position.
8793
-
8794
- Args:
8795
- pos_file: Path to position tracking file
8796
-
8797
- Returns:
8798
- Tuple of (metrics list, new_position)
8799
- """
8800
- return read_jsonl_from_position(METRICS_LOG_FILE, pos_file)
8801
-
8802
-
8803
- def read_network_logs_from_position(pos_file: str) -> tuple[List[Dict[str, Any]], int]:
8804
- """
8805
- Read new network logs from JSONL file starting from last position.
8806
-
8807
- Args:
8808
- pos_file: Path to position tracking file
8809
-
8810
- Returns:
8811
- Tuple of (network logs list, new_position)
8812
- """
8813
- return read_jsonl_from_position(NETWORK_LOG_FILE, pos_file)
8814
-
8815
-
8816
- def read_sandbox_ops_from_position(pos_file: str) -> tuple[List[Dict[str, Any]], int]:
8817
- """
8818
- Read new sandbox operations from JSONL file starting from last position.
8819
-
8820
- Args:
8821
- pos_file: Path to position tracking file
8822
-
8823
- Returns:
8824
- Tuple of (sandbox operations list, new_position)
8825
- """
8826
- return read_jsonl_from_position(SANDBOX_OPS_LOG_FILE, pos_file)
8827
-
8828
-
8829
- def upload_telemetry() -> bool:
8830
- """
8831
- Upload telemetry data to VM0 API.
8832
-
8833
- Returns:
8834
- True if upload succeeded or no data to upload, False on failure
8835
- """
8836
- # Read new system log content
8837
- system_log, log_pos = read_file_from_position(SYSTEM_LOG_FILE, TELEMETRY_LOG_POS_FILE)
8838
-
8839
- # Read new metrics
8840
- metrics, metrics_pos = read_metrics_from_position(TELEMETRY_METRICS_POS_FILE)
8841
-
8842
- # Read new network logs
8843
- network_logs, network_pos = read_network_logs_from_position(TELEMETRY_NETWORK_POS_FILE)
8844
-
8845
- # Read new sandbox operations
8846
- sandbox_ops, sandbox_ops_pos = read_sandbox_ops_from_position(TELEMETRY_SANDBOX_OPS_POS_FILE)
8847
-
8848
- # Skip if nothing new
8849
- if not system_log and not metrics and not network_logs and not sandbox_ops:
8850
- log_debug("No new telemetry data to upload")
8851
- return True
8852
-
8853
- # Mask secrets in telemetry data before sending
8854
- # System log and network logs may contain sensitive information
8855
- masked_system_log = mask_data(system_log) if system_log else ""
8856
- masked_network_logs = mask_data(network_logs) if network_logs else []
8857
-
8858
- # Upload to API
8859
- payload = {
8860
- "runId": RUN_ID,
8861
- "systemLog": masked_system_log,
8862
- "metrics": metrics, # Metrics don't contain secrets (just numbers)
8863
- "networkLogs": masked_network_logs,
8864
- "sandboxOperations": sandbox_ops # Sandbox ops don't contain secrets (just timing data)
8865
- }
8866
-
8867
- log_debug(f"Uploading telemetry: {len(system_log)} bytes log, {len(metrics)} metrics, {len(network_logs)} network logs, {len(sandbox_ops)} sandbox ops")
8868
-
8869
- result = http_post_json(TELEMETRY_URL, payload, max_retries=1)
8870
-
8871
- if result:
8872
- # Save positions only on successful upload
8873
- save_position(TELEMETRY_LOG_POS_FILE, log_pos)
8874
- save_position(TELEMETRY_METRICS_POS_FILE, metrics_pos)
8875
- save_position(TELEMETRY_NETWORK_POS_FILE, network_pos)
8876
- save_position(TELEMETRY_SANDBOX_OPS_POS_FILE, sandbox_ops_pos)
8877
- log_debug(f"Telemetry uploaded successfully: {result.get('id', 'unknown')}")
8878
- return True
8879
- else:
8880
- log_warn("Failed to upload telemetry (will retry next interval)")
8881
- return False
8882
-
8883
-
8884
- def telemetry_upload_loop(shutdown_event: threading.Event) -> None:
8885
- """
8886
- Background loop that uploads telemetry every TELEMETRY_INTERVAL seconds.
8887
- """
8888
- log_info(f"Telemetry upload started (interval: {TELEMETRY_INTERVAL}s)")
8889
-
8890
- while not shutdown_event.is_set():
8891
- try:
8892
- upload_telemetry()
8893
- except Exception as e:
8894
- log_error(f"Telemetry upload error: {e}")
8895
-
8896
- # Wait for interval or shutdown
8897
- shutdown_event.wait(TELEMETRY_INTERVAL)
8898
-
8899
- log_info("Telemetry upload stopped")
8900
-
8901
-
8902
- def start_telemetry_upload(shutdown_event: threading.Event) -> threading.Thread:
8903
- """
8904
- Start the telemetry uploader as a daemon thread.
8905
-
8906
- Args:
8907
- shutdown_event: Threading event to signal shutdown
8908
-
8909
- Returns:
8910
- The started thread (for joining if needed)
8911
- """
8912
- thread = threading.Thread(
8913
- target=telemetry_upload_loop,
8914
- args=(shutdown_event,),
8915
- daemon=True,
8916
- name="telemetry-upload"
8917
- )
8918
- thread.start()
8919
- return thread
8920
-
8921
-
8922
- def final_telemetry_upload() -> bool:
8923
- """
8924
- Perform final telemetry upload before agent completion.
8925
- This ensures all remaining data is captured.
8926
-
8927
- Returns:
8928
- True if upload succeeded, False on failure
8929
- """
8930
- log_info("Performing final telemetry upload...")
8931
- return upload_telemetry()
8932
- `;
8933
-
8934
- // ../../packages/core/src/sandbox/scripts/lib/secret_masker.py.ts
8935
- var SECRET_MASKER_SCRIPT = `#!/usr/bin/env python3
8936
- """
8937
- Secret masking module for VM0 sandbox.
8938
-
8939
- Masks secrets in event data before sending to server.
8940
- Similar to GitHub Actions secret masking.
8941
- """
8942
- import os
8943
- import base64
8944
- from urllib.parse import quote as url_encode
8945
- from typing import Any, Set, List, Optional
8946
-
8947
- # Placeholder for masked secrets
8948
- MASK_PLACEHOLDER = "***"
8949
-
8950
- # Minimum length for secrets (avoid false positives on short strings)
8951
- MIN_SECRET_LENGTH = 5
8952
-
8953
- # Global masker instance (initialized lazily)
8954
- _masker: Optional["SecretMasker"] = None
8955
-
8956
-
8957
- class SecretMasker:
8958
- """
8959
- Masks secret values in data structures.
8960
- Pre-computes encoding variants for efficient matching.
8961
- """
8962
-
8963
- def __init__(self, secret_values: List[str]):
8964
- """
8965
- Initialize masker with secret values.
8966
-
8967
- Args:
8968
- secret_values: List of secret values to mask
8969
- """
8970
- self.patterns: Set[str] = set()
8971
-
8972
- for secret in secret_values:
8973
- if not secret or len(secret) < MIN_SECRET_LENGTH:
8974
- continue
8975
-
8976
- # Original value
8977
- self.patterns.add(secret)
8978
-
8979
- # Base64 encoded
8980
- try:
8981
- b64 = base64.b64encode(secret.encode()).decode()
8982
- if len(b64) >= MIN_SECRET_LENGTH:
8983
- self.patterns.add(b64)
8984
- except Exception:
8985
- pass
8986
-
8987
- # URL encoded (only if different from original)
8988
- try:
8989
- url_enc = url_encode(secret, safe="")
8990
- if url_enc != secret and len(url_enc) >= MIN_SECRET_LENGTH:
8991
- self.patterns.add(url_enc)
8992
- except Exception:
8993
- pass
8994
-
8995
- def mask(self, data: Any) -> Any:
8996
- """
8997
- Recursively mask all occurrences of secrets in the data.
8998
-
8999
- Args:
9000
- data: Data to mask (string, list, dict, or primitive)
9001
-
9002
- Returns:
9003
- Masked data with the same structure
9004
- """
9005
- return self._deep_mask(data)
9006
-
9007
- def _deep_mask(self, data: Any) -> Any:
9008
- """Recursively mask data."""
9009
- if isinstance(data, str):
9010
- result = data
9011
- for pattern in self.patterns:
9012
- # Use split/join for global replacement
9013
- result = result.replace(pattern, MASK_PLACEHOLDER)
9014
- return result
9015
-
9016
- if isinstance(data, list):
9017
- return [self._deep_mask(item) for item in data]
9018
-
9019
- if isinstance(data, dict):
9020
- return {key: self._deep_mask(value) for key, value in data.items()}
9021
-
9022
- # Primitives (int, float, bool, None) pass through unchanged
9023
- return data
9024
-
9025
-
9026
- def get_masker() -> SecretMasker:
9027
- """
9028
- Get the global masker instance.
9029
- Initializes on first call using VM0_SECRET_VALUES env var.
9030
-
9031
- Returns:
9032
- SecretMasker instance
9033
- """
9034
- global _masker
9035
-
9036
- if _masker is None:
9037
- _masker = _create_masker()
9038
-
9039
- return _masker
9040
-
9041
-
9042
- def _create_masker() -> SecretMasker:
9043
- """
9044
- Create a masker from VM0_SECRET_VALUES env var.
9045
-
9046
- VM0_SECRET_VALUES contains comma-separated base64-encoded secret values.
9047
- This avoids exposing plaintext secrets in environment variable names.
9048
- """
9049
- secret_values_str = os.environ.get("VM0_SECRET_VALUES", "")
9050
-
9051
- if not secret_values_str:
9052
- # No secrets to mask
9053
- return SecretMasker([])
9054
-
9055
- # Parse base64-encoded values
9056
- secret_values = []
9057
- for encoded_value in secret_values_str.split(","):
9058
- encoded_value = encoded_value.strip()
9059
- if encoded_value:
9060
- try:
9061
- decoded = base64.b64decode(encoded_value).decode()
9062
- if decoded:
9063
- secret_values.append(decoded)
9064
- except Exception:
9065
- # Skip invalid base64 values
9066
- pass
9067
-
9068
- return SecretMasker(secret_values)
9069
-
9070
-
9071
- def mask_data(data: Any) -> Any:
9072
- """
9073
- Convenience function to mask data using global masker.
9074
-
9075
- Args:
9076
- data: Data to mask
9077
-
9078
- Returns:
9079
- Masked data
9080
- """
9081
- return get_masker().mask(data)
9082
- `;
9083
-
9084
- // ../../packages/core/src/sandbox/scripts/run-agent.py.ts
9085
- var RUN_AGENT_SCRIPT = `#!/usr/bin/env python3
9086
- """
9087
- Main agent execution orchestrator for VM0.
9088
- This script coordinates the execution of Claude Code and handles:
9089
- - Working directory setup
9090
- - Claude CLI execution with JSONL streaming
9091
- - Event sending to webhook
9092
- - Checkpoint creation on success
9093
- - Complete API call on finish
9094
-
9095
- Design principles:
9096
- - Never call sys.exit() in the middle of execution - use raise instead
9097
- - Single exit point at the very end of if __name__ == "__main__"
9098
- - finally block guarantees cleanup runs regardless of success/failure
9099
- - Complete API passes error message for CLI to display
9100
- """
9101
- import os
9102
- import sys
9103
- import subprocess
9104
- import json
9105
- import threading
9106
- import time
9107
-
9108
- # Add lib to path for imports
9109
- sys.path.insert(0, "/usr/local/bin/vm0-agent/lib")
9110
-
9111
- from common import (
9112
- WORKING_DIR, PROMPT, RESUME_SESSION_ID, COMPLETE_URL, RUN_ID,
9113
- EVENT_ERROR_FLAG, HEARTBEAT_URL, HEARTBEAT_INTERVAL, AGENT_LOG_FILE,
9114
- CLI_AGENT_TYPE, OPENAI_MODEL, validate_config, record_sandbox_op
9115
- )
9116
- from log import log_info, log_error, log_warn
9117
- from events import send_event
9118
- from checkpoint import create_checkpoint
9119
- from http_client import http_post_json
9120
- from metrics import start_metrics_collector
9121
- from upload_telemetry import start_telemetry_upload, final_telemetry_upload
9122
-
9123
- # Global shutdown event for heartbeat thread
9124
- shutdown_event = threading.Event()
9125
-
9126
-
9127
- def heartbeat_loop():
9128
- """Send periodic heartbeat signals to indicate agent is still alive."""
9129
- while not shutdown_event.is_set():
9130
- try:
9131
- if http_post_json(HEARTBEAT_URL, {"runId": RUN_ID}):
9132
- log_info("Heartbeat sent")
9133
- else:
9134
- log_warn("Heartbeat failed")
9135
- except Exception as e:
9136
- log_warn(f"Heartbeat error: {e}")
9137
- # Wait for interval or until shutdown
9138
- shutdown_event.wait(HEARTBEAT_INTERVAL)
9139
-
9140
-
9141
- def _cleanup(exit_code: int, error_message: str):
9142
- """
9143
- Cleanup and notify server.
9144
- This function is called in the finally block to ensure it always runs.
9145
- """
9146
- log_info("\u25B7 Cleanup")
9147
-
9148
- # Perform final telemetry upload before completion
9149
- # This ensures all remaining data is captured
9150
- telemetry_start = time.time()
9151
- telemetry_success = True
9152
- try:
9153
- final_telemetry_upload()
9154
- except Exception as e:
9155
- telemetry_success = False
9156
- log_error(f"Final telemetry upload failed: {e}")
9157
- record_sandbox_op("final_telemetry_upload", int((time.time() - telemetry_start) * 1000), telemetry_success)
9158
-
9159
- # Always call complete API at the end
9160
- # This sends vm0_result (on success) or vm0_error (on failure) and kills the sandbox
9161
- log_info(f"Calling complete API with exitCode={exit_code}")
9162
-
9163
- complete_payload = {
9164
- "runId": RUN_ID,
9165
- "exitCode": exit_code
9166
- }
9167
- if error_message:
9168
- complete_payload["error"] = error_message
9169
-
9170
- complete_start = time.time()
9171
- complete_success = False
9172
- try:
9173
- if http_post_json(COMPLETE_URL, complete_payload):
9174
- log_info("Complete API called successfully")
9175
- complete_success = True
9176
- else:
9177
- log_error("Failed to call complete API (sandbox may not be cleaned up)")
9178
- except Exception as e:
9179
- log_error(f"Complete API call failed: {e}")
9180
- record_sandbox_op("complete_api_call", int((time.time() - complete_start) * 1000), complete_success)
9181
-
9182
- # Stop heartbeat thread
9183
- shutdown_event.set()
9184
- log_info("Heartbeat thread stopped")
9185
-
9186
- # Log final status
9187
- if exit_code == 0:
9188
- log_info("\u2713 Sandbox finished successfully")
9189
- else:
9190
- log_info(f"\u2717 Sandbox failed (exit code {exit_code})")
9191
-
9192
-
9193
- def _run() -> tuple[int, str]:
9194
- """
9195
- Main execution logic.
9196
- Raises exceptions on failure instead of calling sys.exit().
9197
- Returns (exit_code, error_message) tuple on completion.
9198
- """
9199
- # Validate configuration - raises ValueError if invalid
9200
- validate_config()
9201
-
9202
- # Lifecycle: Header
9203
- log_info(f"\u25B6 VM0 Sandbox {RUN_ID}")
9204
-
9205
- # Lifecycle: Initialization
9206
- log_info("\u25B7 Initialization")
9207
- init_start_time = time.time()
9208
-
9209
- log_info(f"Working directory: {WORKING_DIR}")
9210
-
9211
- # Start heartbeat thread
9212
- heartbeat_start = time.time()
9213
- heartbeat_thread = threading.Thread(target=heartbeat_loop, daemon=True)
9214
- heartbeat_thread.start()
9215
- log_info("Heartbeat thread started")
9216
- record_sandbox_op("heartbeat_start", int((time.time() - heartbeat_start) * 1000), True)
9217
-
9218
- # Start metrics collector thread
9219
- metrics_start = time.time()
9220
- start_metrics_collector(shutdown_event)
9221
- log_info("Metrics collector thread started")
9222
- record_sandbox_op("metrics_collector_start", int((time.time() - metrics_start) * 1000), True)
9223
-
9224
- # Start telemetry upload thread
9225
- telemetry_start = time.time()
9226
- start_telemetry_upload(shutdown_event)
9227
- log_info("Telemetry upload thread started")
9228
- record_sandbox_op("telemetry_upload_start", int((time.time() - telemetry_start) * 1000), True)
9229
-
9230
- # Create and change to working directory - raises RuntimeError if fails
9231
- # Directory may not exist if no artifact/storage was downloaded (e.g., first run)
9232
- working_dir_start = time.time()
9233
- working_dir_success = True
9234
- try:
9235
- os.makedirs(WORKING_DIR, exist_ok=True)
9236
- os.chdir(WORKING_DIR)
9237
- except OSError as e:
9238
- working_dir_success = False
9239
- record_sandbox_op("working_dir_setup", int((time.time() - working_dir_start) * 1000), False, str(e))
9240
- raise RuntimeError(f"Failed to create/change to working directory: {WORKING_DIR} - {e}") from e
9241
- record_sandbox_op("working_dir_setup", int((time.time() - working_dir_start) * 1000), working_dir_success)
9242
-
9243
- # Set up Codex configuration if using Codex CLI
9244
- # Claude Code uses ~/.claude by default (no configuration needed)
9245
- if CLI_AGENT_TYPE == "codex":
9246
- home_dir = os.environ.get("HOME", "/home/user")
9247
- # Codex uses ~/.codex for configuration and session storage
9248
- codex_home = f"{home_dir}/.codex"
9249
- os.makedirs(codex_home, exist_ok=True)
9250
- os.environ["CODEX_HOME"] = codex_home
9251
- log_info(f"Codex home directory: {codex_home}")
9252
-
9253
- # Login with API key via stdin (recommended method)
9254
- codex_login_start = time.time()
9255
- codex_login_success = False
9256
- api_key = os.environ.get("OPENAI_API_KEY", "")
9257
- if api_key:
9258
- result = subprocess.run(
9259
- ["codex", "login", "--with-api-key"],
9260
- input=api_key,
9261
- text=True,
9262
- capture_output=True
9263
- )
9264
- if result.returncode == 0:
9265
- log_info("Codex authenticated with API key")
9266
- codex_login_success = True
9267
- else:
9268
- log_error(f"Codex login failed: {result.stderr}")
9269
- else:
9270
- log_error("OPENAI_API_KEY not set")
9271
- record_sandbox_op("codex_login", int((time.time() - codex_login_start) * 1000), codex_login_success)
9272
-
9273
- init_duration_ms = int((time.time() - init_start_time) * 1000)
9274
- record_sandbox_op("init_total", init_duration_ms, True)
9275
- log_info(f"\u2713 Initialization complete ({init_duration_ms // 1000}s)")
9276
-
9277
- # Lifecycle: Execution
9278
- log_info("\u25B7 Execution")
9279
- exec_start_time = time.time()
9280
-
9281
- # Execute CLI agent with JSONL output
9282
- log_info(f"Starting {CLI_AGENT_TYPE} execution...")
9283
- log_info(f"Prompt: {PROMPT}")
9284
-
9285
- # Build command based on CLI agent type
9286
- use_mock = os.environ.get("USE_MOCK_CLAUDE") == "true"
9287
-
9288
- if CLI_AGENT_TYPE == "codex":
9289
- # Build Codex command
9290
- if use_mock:
9291
- # Mock mode not yet supported for Codex
9292
- raise RuntimeError("Mock mode not supported for Codex")
9293
-
9294
- if RESUME_SESSION_ID:
9295
- # Codex resume uses subcommand: codex exec resume <session_id> <prompt>
9296
- log_info(f"Resuming session: {RESUME_SESSION_ID}")
9297
- codex_args = [
9298
- "exec",
9299
- "--json",
9300
- "--dangerously-bypass-approvals-and-sandbox",
9301
- "--skip-git-repo-check",
9302
- "-C", WORKING_DIR,
9303
- ]
9304
- if OPENAI_MODEL:
9305
- codex_args.extend(["-m", OPENAI_MODEL])
9306
- codex_args.extend(["resume", RESUME_SESSION_ID, PROMPT])
9307
- cmd = ["codex"] + codex_args
9308
- else:
9309
- log_info("Starting new session")
9310
- codex_args = [
9311
- "exec",
9312
- "--json",
9313
- "--dangerously-bypass-approvals-and-sandbox",
9314
- "--skip-git-repo-check",
9315
- "-C", WORKING_DIR,
9316
- ]
9317
- if OPENAI_MODEL:
9318
- log_info(f"Using model: {OPENAI_MODEL}")
9319
- codex_args.extend(["-m", OPENAI_MODEL])
9320
- cmd = ["codex"] + codex_args + [PROMPT]
9321
- else:
9322
- # Build Claude command - unified for both new and resume sessions
9323
- claude_args = [
9324
- "--print", "--verbose",
9325
- "--output-format", "stream-json",
9326
- "--dangerously-skip-permissions"
9327
- ]
9328
-
9329
- if RESUME_SESSION_ID:
9330
- log_info(f"Resuming session: {RESUME_SESSION_ID}")
9331
- claude_args.extend(["--resume", RESUME_SESSION_ID])
9332
- else:
9333
- log_info("Starting new session")
9334
-
9335
- # Select Claude binary - use mock-claude for testing if USE_MOCK_CLAUDE is set
9336
- if use_mock:
9337
- claude_bin = "/usr/local/bin/vm0-agent/lib/mock_claude.py"
9338
- log_info("Using mock-claude for testing")
9339
- else:
9340
- claude_bin = "claude"
9341
-
9342
- # Build full command
9343
- cmd = [claude_bin] + claude_args + [PROMPT]
9344
-
9345
- # Execute CLI agent and process output stream
9346
- # Capture both stdout and stderr, write to log file, keep stderr in memory for error extraction
9347
- agent_exit_code = 0
9348
- stderr_lines = [] # Keep stderr in memory for error message extraction
9349
- log_file = None
9350
-
9351
- try:
9352
- # Open log file directly in /tmp (no need to create directory)
9353
- log_file = open(AGENT_LOG_FILE, "w")
9354
-
9355
- # Python subprocess.PIPE can deadlock if buffer fills up
9356
- # Use a background thread to drain stderr while we read stdout
9357
- proc = subprocess.Popen(
9358
- cmd,
9359
- stdout=subprocess.PIPE,
9360
- stderr=subprocess.PIPE,
9361
- text=True,
9362
- bufsize=1 # Line buffered for real-time processing
9363
- )
9364
-
9365
- # Read stderr in background to prevent buffer deadlock
9366
- def read_stderr():
9367
- try:
9368
- for line in proc.stderr:
9369
- stderr_lines.append(line)
9370
- if log_file and not log_file.closed:
9371
- log_file.write(f"[STDERR] {line}")
9372
- log_file.flush()
9373
- except Exception:
9374
- pass # Ignore errors if file closed
9375
-
9376
- stderr_thread = threading.Thread(target=read_stderr, daemon=True)
9377
- stderr_thread.start()
9378
-
9379
- # Sequence counter for events (1-based)
9380
- event_sequence = 0
9381
-
9382
- # Process JSONL output line by line from stdout
9383
- for line in proc.stdout:
9384
- # Write raw line to log file
9385
- if log_file and not log_file.closed:
9386
- log_file.write(line)
9387
- log_file.flush()
9388
-
9389
- stripped = line.strip()
9390
-
9391
- # Skip empty lines
9392
- if not stripped:
9393
- continue
9394
-
9395
- # Check if line is valid JSON (stdout should only contain JSONL)
9396
- try:
9397
- event = json.loads(stripped)
9398
-
9399
- # Valid JSONL - send immediately with sequence number
9400
- event_sequence += 1
9401
- send_event(event, event_sequence)
9402
-
9403
- # Extract result from "result" event for stdout
9404
- if event.get("type") == "result":
9405
- result_content = event.get("result", "")
9406
- if result_content:
9407
- print(result_content)
9408
-
9409
- except json.JSONDecodeError:
9410
- # Not valid JSON, skip
9411
- pass
9412
-
9413
- # Wait for process to complete
9414
- proc.wait()
9415
- # Wait for stderr thread to finish (with timeout to avoid hanging)
9416
- stderr_thread.join(timeout=10)
9417
- agent_exit_code = proc.returncode
9418
-
9419
- except Exception as e:
9420
- log_error(f"Failed to execute {CLI_AGENT_TYPE}: {e}")
9421
- agent_exit_code = 1
9422
- finally:
9423
- if log_file and not log_file.closed:
9424
- log_file.close()
9425
-
9426
- # Print newline after output
9427
- print()
9428
-
9429
- # Track final exit code for complete API
9430
- final_exit_code = agent_exit_code
9431
- error_message = ""
9432
-
9433
- # Check if any events failed to send
9434
- if os.path.exists(EVENT_ERROR_FLAG):
9435
- log_error("Some events failed to send, marking run as failed")
9436
- final_exit_code = 1
9437
- error_message = "Some events failed to send"
9438
-
9439
- # Log execution result and record metric
9440
- exec_duration_ms = int((time.time() - exec_start_time) * 1000)
9441
- record_sandbox_op("cli_execution", exec_duration_ms, agent_exit_code == 0)
9442
- if agent_exit_code == 0 and final_exit_code == 0:
9443
- log_info(f"\u2713 Execution complete ({exec_duration_ms // 1000}s)")
9444
- else:
9445
- log_info(f"\u2717 Execution failed ({exec_duration_ms // 1000}s)")
9446
-
9447
- # Handle completion
9448
- if agent_exit_code == 0 and final_exit_code == 0:
9449
- log_info(f"{CLI_AGENT_TYPE} completed successfully")
9450
-
9451
- # Lifecycle: Checkpoint
9452
- log_info("\u25B7 Checkpoint")
9453
- checkpoint_start_time = time.time()
9454
-
9455
- # Create checkpoint - this is mandatory for successful runs
9456
- checkpoint_success = create_checkpoint()
9457
- checkpoint_duration = int(time.time() - checkpoint_start_time)
9458
-
9459
- if checkpoint_success:
9460
- log_info(f"\u2713 Checkpoint complete ({checkpoint_duration}s)")
9461
- else:
9462
- log_info(f"\u2717 Checkpoint failed ({checkpoint_duration}s)")
9463
-
9464
- if not checkpoint_success:
9465
- log_error("Checkpoint creation failed, marking run as failed")
9466
- final_exit_code = 1
9467
- error_message = "Checkpoint creation failed"
9468
- else:
9469
- if agent_exit_code != 0:
9470
- log_info(f"{CLI_AGENT_TYPE} failed with exit code {agent_exit_code}")
9471
-
9472
- # Get detailed error from captured stderr lines in memory
9473
- if stderr_lines:
9474
- error_message = " ".join(line.strip() for line in stderr_lines)
9475
- log_info(f"Captured stderr: {error_message}")
9476
- else:
9477
- error_message = f"Agent exited with code {agent_exit_code}"
9478
-
9479
- # Note: Keep all temp files for debugging (SESSION_ID_FILE, SESSION_HISTORY_PATH_FILE, EVENT_ERROR_FLAG)
9480
-
9481
- return final_exit_code, error_message
9482
-
9483
-
9484
- def main() -> int:
9485
- """
9486
- Main entry point for agent execution.
9487
- Uses try/except/finally to ensure cleanup always runs.
9488
- Returns exit code (0 for success, non-zero for failure).
9489
- """
9490
- exit_code = 1 # Default to failure
9491
- error_message = "Unexpected termination"
9492
-
9493
- try:
9494
- exit_code, error_message = _run()
9495
-
9496
- except ValueError as e:
9497
- # Configuration validation errors
9498
- exit_code = 1
9499
- error_message = str(e)
9500
- log_error(f"Configuration error: {error_message}")
9501
-
9502
- except RuntimeError as e:
9503
- # Runtime errors (e.g., working directory not found)
9504
- exit_code = 1
9505
- error_message = str(e)
9506
- log_error(f"Runtime error: {error_message}")
9507
-
9508
- except Exception as e:
9509
- # Catch-all for unexpected exceptions
9510
- exit_code = 1
9511
- error_message = f"Unexpected error: {e}"
9512
- log_error(error_message)
9513
-
9514
- finally:
9515
- # Always cleanup and notify server
9516
- _cleanup(exit_code, error_message)
9517
-
9518
- return exit_code
9519
-
9520
-
9521
- if __name__ == "__main__":
9522
- sys.exit(main())
9523
- `;
9524
-
9525
- // ../../packages/core/src/sandbox/scripts/index.ts
9526
- var SCRIPT_PATHS = {
9527
- baseDir: "/usr/local/bin/vm0-agent",
9528
- libDir: "/usr/local/bin/vm0-agent/lib",
9529
- libInit: "/usr/local/bin/vm0-agent/lib/__init__.py",
9530
- runAgent: "/usr/local/bin/vm0-agent/run-agent.py",
9531
- common: "/usr/local/bin/vm0-agent/lib/common.py",
9532
- log: "/usr/local/bin/vm0-agent/lib/log.py",
9533
- httpClient: "/usr/local/bin/vm0-agent/lib/http_client.py",
9534
- events: "/usr/local/bin/vm0-agent/lib/events.py",
9535
- directUpload: "/usr/local/bin/vm0-agent/lib/direct_upload.py",
9536
- download: "/usr/local/bin/vm0-agent/lib/download.py",
9537
- checkpoint: "/usr/local/bin/vm0-agent/lib/checkpoint.py",
9538
- mockClaude: "/usr/local/bin/vm0-agent/lib/mock_claude.py",
9539
- metrics: "/usr/local/bin/vm0-agent/lib/metrics.py",
9540
- uploadTelemetry: "/usr/local/bin/vm0-agent/lib/upload_telemetry.py",
9541
- secretMasker: "/usr/local/bin/vm0-agent/lib/secret_masker.py"
9542
- };
9543
-
9544
- // src/lib/scripts/index.ts
9545
- var ENV_LOADER_PATH = "/usr/local/bin/vm0-agent/env-loader.py";
9546
- var ENV_LOADER_SCRIPT = `#!/usr/bin/env python3
9547
- """
9548
- Environment loader wrapper for VM0 runner.
9549
- Loads environment variables from JSON file before executing run-agent.py.
9550
-
9551
- This is needed because the runner passes environment variables via SCP (JSON file)
9552
- rather than directly setting them (which E2B sandbox API supports).
9553
- """
9554
- import os
9555
- import sys
9556
- import json
9557
-
9558
- # Environment JSON file path
9559
- ENV_JSON_PATH = "/tmp/vm0-env.json"
9560
-
9561
- print("[env-loader] Starting...", flush=True)
9562
-
9563
- # Load environment from JSON file
9564
- if os.path.exists(ENV_JSON_PATH):
9565
- print(f"[env-loader] Loading environment from {ENV_JSON_PATH}", flush=True)
9566
- try:
9567
- with open(ENV_JSON_PATH, "r") as f:
9568
- env_data = json.load(f)
9569
- for key, value in env_data.items():
9570
- os.environ[key] = value
9571
- print(f"[env-loader] Loaded {len(env_data)} environment variables", flush=True)
9572
- except Exception as e:
9573
- print(f"[env-loader] ERROR loading JSON: {e}", flush=True)
9574
- sys.exit(1)
9575
- else:
9576
- print(f"[env-loader] ERROR: Environment file not found: {ENV_JSON_PATH}", flush=True)
9577
- sys.exit(1)
9578
-
9579
- # Verify critical environment variables
9580
- critical_vars = ["VM0_RUN_ID", "VM0_API_URL", "VM0_WORKING_DIR", "VM0_PROMPT"]
9581
- for var in critical_vars:
9582
- val = os.environ.get(var, "")
9583
- if val:
9584
- print(f"[env-loader] {var}={val[:50]}{'...' if len(val) > 50 else ''}", flush=True)
9585
- else:
9586
- print(f"[env-loader] WARNING: {var} is empty", flush=True)
9587
-
9588
- # Execute run-agent.py in the same process
9589
- # Using exec to replace this process with run-agent.py
9590
- run_agent_path = "/usr/local/bin/vm0-agent/run-agent.py"
9591
- print(f"[env-loader] Executing {run_agent_path}", flush=True)
9592
-
9593
- # Read and execute the script
9594
- with open(run_agent_path, "r") as f:
9595
- code = f.read()
9596
-
9597
- exec(compile(code, run_agent_path, "exec"))
9598
- `;
9599
-
9600
- // src/lib/scripts/utils.ts
9601
- function getAllScripts() {
9602
- return [
9603
- { content: INIT_SCRIPT, path: SCRIPT_PATHS.libInit },
9604
- { content: COMMON_SCRIPT, path: SCRIPT_PATHS.common },
9605
- { content: LOG_SCRIPT, path: SCRIPT_PATHS.log },
9606
- { content: HTTP_SCRIPT, path: SCRIPT_PATHS.httpClient },
9607
- { content: EVENTS_SCRIPT, path: SCRIPT_PATHS.events },
9608
- { content: DIRECT_UPLOAD_SCRIPT, path: SCRIPT_PATHS.directUpload },
9609
- { content: DOWNLOAD_SCRIPT, path: SCRIPT_PATHS.download },
9610
- { content: CHECKPOINT_SCRIPT, path: SCRIPT_PATHS.checkpoint },
9611
- { content: MOCK_CLAUDE_SCRIPT, path: SCRIPT_PATHS.mockClaude },
9612
- { content: METRICS_SCRIPT, path: SCRIPT_PATHS.metrics },
9613
- { content: UPLOAD_TELEMETRY_SCRIPT, path: SCRIPT_PATHS.uploadTelemetry },
9614
- { content: SECRET_MASKER_SCRIPT, path: SCRIPT_PATHS.secretMasker },
9615
- { content: RUN_AGENT_SCRIPT, path: SCRIPT_PATHS.runAgent },
9616
- // Env loader is runner-specific (loads env from JSON before executing run-agent.py)
9617
- { content: ENV_LOADER_SCRIPT, path: ENV_LOADER_PATH }
9618
- ];
9619
- }
9620
-
9621
- // src/lib/proxy/vm-registry.ts
9622
- import fs4 from "fs";
9623
- var DEFAULT_REGISTRY_PATH = "/tmp/vm0-vm-registry.json";
9624
- var VMRegistry = class {
9625
- registryPath;
9626
- data;
9627
- constructor(registryPath = DEFAULT_REGISTRY_PATH) {
9628
- this.registryPath = registryPath;
9629
- this.data = this.load();
9630
- }
9631
- /**
9632
- * Load registry data from file
9633
- */
9634
- load() {
9635
- try {
9636
- if (fs4.existsSync(this.registryPath)) {
9637
- const content = fs4.readFileSync(this.registryPath, "utf-8");
9638
- return JSON.parse(content);
9639
- }
9640
- } catch {
9641
- }
9642
- return { vms: {}, updatedAt: Date.now() };
9643
- }
9644
- /**
9645
- * Save registry data to file atomically
9646
- */
9647
- save() {
9648
- this.data.updatedAt = Date.now();
9649
- const content = JSON.stringify(this.data, null, 2);
9650
- const tempPath = `${this.registryPath}.tmp`;
9651
- fs4.writeFileSync(tempPath, content, { mode: 420 });
9652
- fs4.renameSync(tempPath, this.registryPath);
9653
- }
9654
- /**
9655
- * Register a VM with its IP address
9656
- */
9657
- register(vmIp, runId, sandboxToken, options) {
9658
- this.data.vms[vmIp] = {
9659
- runId,
9660
- sandboxToken,
9661
- registeredAt: Date.now(),
9662
- firewallRules: options?.firewallRules,
9663
- mitmEnabled: options?.mitmEnabled,
9664
- sealSecretsEnabled: options?.sealSecretsEnabled
9665
- };
9666
- this.save();
9667
- const firewallInfo = options?.firewallRules ? ` with ${options.firewallRules.length} firewall rules` : "";
9668
- const mitmInfo = options?.mitmEnabled ? ", MITM enabled" : "";
9669
- console.log(
9670
- `[VMRegistry] Registered VM ${vmIp} for run ${runId}${firewallInfo}${mitmInfo}`
9671
- );
9672
- }
9673
- /**
9674
- * Unregister a VM by IP address
9675
- */
9676
- unregister(vmIp) {
9677
- if (this.data.vms[vmIp]) {
9678
- const registration = this.data.vms[vmIp];
9679
- delete this.data.vms[vmIp];
9680
- this.save();
9681
- console.log(
9682
- `[VMRegistry] Unregistered VM ${vmIp} (run ${registration.runId})`
9683
- );
9684
- }
9685
- }
9686
- /**
9687
- * Look up registration by VM IP
9688
- */
9689
- lookup(vmIp) {
9690
- return this.data.vms[vmIp];
9691
- }
9692
- /**
9693
- * Get all registered VMs
9694
- */
9695
- getAll() {
9696
- return { ...this.data.vms };
9697
- }
9698
- /**
9699
- * Clear all registrations
9700
- */
9701
- clear() {
9702
- this.data.vms = {};
9703
- this.save();
9704
- console.log("[VMRegistry] Cleared all registrations");
9705
- }
9706
- /**
9707
- * Get the path to the registry file
9708
- */
9709
- getRegistryPath() {
9710
- return this.registryPath;
9711
- }
9712
- };
9713
- var globalRegistry = null;
9714
- function getVMRegistry() {
9715
- if (!globalRegistry) {
9716
- globalRegistry = new VMRegistry();
9717
- }
9718
- return globalRegistry;
9719
- }
9720
- function initVMRegistry(registryPath) {
9721
- globalRegistry = new VMRegistry(registryPath);
9722
- return globalRegistry;
9723
- }
9724
-
9725
- // src/lib/proxy/proxy-manager.ts
9726
- import { spawn as spawn2 } from "child_process";
9727
- import fs5 from "fs";
9728
- import path3 from "path";
9729
-
9730
- // src/lib/proxy/mitm-addon-script.ts
9731
- var RUNNER_MITM_ADDON_SCRIPT = `#!/usr/bin/env python3
9732
- """
9733
- mitmproxy addon for VM0 runner-level network security mode.
9734
-
9735
- This addon runs on the runner HOST (not inside VMs) and:
9736
- 1. Intercepts all HTTPS requests from VMs
9737
- 2. Looks up the source VM's runId and firewall rules from the VM registry
9738
- 3. Evaluates firewall rules (first-match-wins) to ALLOW or DENY
9739
- 4. For MITM mode: Rewrites requests to go through VM0 Proxy endpoint
9740
- 5. For SNI-only mode: Passes through or blocks without decryption
9741
- 6. Logs network activity per-run to JSONL files
9742
- """
9743
- import os
9744
- import json
9745
- import time
9746
- import urllib.parse
9747
- import ipaddress
9748
- import socket
9749
- from mitmproxy import http, ctx, tls
9750
-
9751
-
9752
- # VM0 Proxy configuration from environment
9753
- API_URL = os.environ.get("VM0_API_URL", "https://www.vm0.ai")
9754
- REGISTRY_PATH = os.environ.get("VM0_REGISTRY_PATH", "/tmp/vm0-vm-registry.json")
9755
- VERCEL_BYPASS = os.environ.get("VERCEL_PROTECTION_BYPASS", "")
9756
-
9757
- # Construct proxy URL
9758
- PROXY_URL = f"{API_URL}/api/webhooks/agent/proxy"
9759
-
9760
- # Cache for VM registry (reloaded periodically)
9761
- _registry_cache = {}
9762
- _registry_cache_time = 0
9763
- REGISTRY_CACHE_TTL = 2 # seconds
9764
-
9765
- # Track request start times for latency calculation
9766
- request_start_times = {}
9767
-
9768
-
9769
- def load_registry() -> dict:
9770
- """Load the VM registry from file, with caching."""
9771
- global _registry_cache, _registry_cache_time
9772
-
9773
- now = time.time()
9774
- if now - _registry_cache_time < REGISTRY_CACHE_TTL:
9775
- return _registry_cache
9776
-
9777
- try:
9778
- if os.path.exists(REGISTRY_PATH):
9779
- with open(REGISTRY_PATH, "r") as f:
9780
- data = json.load(f)
9781
- _registry_cache = data.get("vms", {})
9782
- _registry_cache_time = now
9783
- return _registry_cache
9784
- except Exception as e:
9785
- ctx.log.warn(f"Failed to load VM registry: {e}")
9786
-
9787
- return _registry_cache
9788
-
9789
-
9790
- def get_vm_info(client_ip: str) -> dict | None:
9791
- """Look up VM info by client IP address."""
9792
- registry = load_registry()
9793
- return registry.get(client_ip)
9794
-
9795
-
9796
- def get_network_log_path(run_id: str) -> str:
9797
- """Get the network log file path for a run."""
9798
- return f"/tmp/vm0-network-{run_id}.jsonl"
9799
-
9800
-
9801
- def log_network_entry(run_id: str, entry: dict) -> None:
9802
- """Write a network log entry to the per-run JSONL file."""
9803
- if not run_id:
9804
- return
9805
-
9806
- log_path = get_network_log_path(run_id)
9807
- try:
9808
- fd = os.open(log_path, os.O_CREAT | os.O_APPEND | os.O_WRONLY, 0o644)
9809
- try:
9810
- os.write(fd, (json.dumps(entry) + "\\n").encode())
9811
- finally:
9812
- os.close(fd)
9813
- except Exception as e:
9814
- ctx.log.warn(f"Failed to write network log: {e}")
9815
-
9816
-
9817
- def get_original_url(flow: http.HTTPFlow) -> str:
9818
- """Reconstruct the original target URL from the request."""
9819
- scheme = "https" if flow.request.port == 443 else "http"
9820
- host = flow.request.pretty_host
9821
- port = flow.request.port
9822
-
9823
- if (scheme == "https" and port != 443) or (scheme == "http" and port != 80):
9824
- host_with_port = f"{host}:{port}"
9825
- else:
9826
- host_with_port = host
9827
-
9828
- path = flow.request.path
9829
- return f"{scheme}://{host_with_port}{path}"
9830
-
9831
-
9832
- # ============================================================================
9833
- # Firewall Rule Matching
9834
- # ============================================================================
9835
-
9836
- def match_domain(pattern: str, hostname: str) -> bool:
9837
- """
9838
- Match hostname against domain pattern.
9839
- Supports exact match and wildcard prefix (*.example.com).
9840
- """
9841
- if not pattern or not hostname:
9842
- return False
9843
-
9844
- pattern = pattern.lower()
9845
- hostname = hostname.lower()
9846
-
9847
- if pattern.startswith("*."):
9848
- # Wildcard: *.example.com matches sub.example.com, www.example.com
9849
- # Also matches example.com itself (without subdomain)
9850
- suffix = pattern[1:] # .example.com
9851
- base = pattern[2:] # example.com
9852
- return hostname.endswith(suffix) or hostname == base
9853
-
9854
- return hostname == pattern
9855
-
9856
-
9857
- def match_ip(cidr: str, ip_str: str) -> bool:
7380
+ def match_ip(cidr: str, ip_str: str) -> bool:
9858
7381
  """
9859
7382
  Match IP address against CIDR range.
9860
7383
  Supports single IPs (1.2.3.4) and ranges (10.0.0.0/8).
@@ -10602,10 +8125,9 @@ async function withSandboxTiming(actionType, fn) {
10602
8125
  }
10603
8126
  }
10604
8127
 
10605
- // src/lib/executor.ts
10606
- function getVmIdFromRunId(runId) {
10607
- return runId.split("-")[0] || runId.substring(0, 8);
10608
- }
8128
+ // src/lib/executor-env.ts
8129
+ var ENV_JSON_PATH = "/tmp/vm0-env.json";
8130
+ var PROXY_CA_CERT_PATH = "/opt/vm0-runner/proxy/mitmproxy-ca-cert.pem";
10609
8131
  function buildEnvironmentVariables(context, apiUrl) {
10610
8132
  const envVars = {
10611
8133
  VM0_API_URL: apiUrl,
@@ -10649,7 +8171,9 @@ function buildEnvironmentVariables(context, apiUrl) {
10649
8171
  }
10650
8172
  return envVars;
10651
8173
  }
10652
- var ENV_JSON_PATH = "/tmp/vm0-env.json";
8174
+
8175
+ // src/lib/network-logs/network-logs.ts
8176
+ import fs6 from "fs";
10653
8177
  function getNetworkLogPath(runId) {
10654
8178
  return `/tmp/vm0-network-${runId}.jsonl`;
10655
8179
  }
@@ -10714,16 +8238,30 @@ async function uploadNetworkLogs(apiUrl, sandboxToken, runId) {
10714
8238
  console.log(`[Executor] Network logs uploaded successfully for ${runId}`);
10715
8239
  cleanupNetworkLogs(runId);
10716
8240
  }
8241
+
8242
+ // src/lib/vm-setup/vm-setup.ts
8243
+ import fs7 from "fs";
8244
+
8245
+ // src/lib/scripts/utils.ts
8246
+ function getAllScripts() {
8247
+ return [
8248
+ { content: RUN_AGENT_SCRIPT, path: SCRIPT_PATHS.runAgent },
8249
+ { content: DOWNLOAD_SCRIPT, path: SCRIPT_PATHS.download },
8250
+ { content: MOCK_CLAUDE_SCRIPT, path: SCRIPT_PATHS.mockClaude },
8251
+ // Env loader is runner-specific (loads env from JSON before executing run-agent.mjs)
8252
+ { content: ENV_LOADER_SCRIPT, path: ENV_LOADER_PATH }
8253
+ ];
8254
+ }
8255
+
8256
+ // src/lib/vm-setup/vm-setup.ts
10717
8257
  async function uploadScripts(ssh) {
10718
8258
  const scripts = getAllScripts();
10719
- await ssh.execOrThrow(
10720
- `sudo mkdir -p ${SCRIPT_PATHS.baseDir} ${SCRIPT_PATHS.libDir}`
10721
- );
8259
+ await ssh.execOrThrow(`sudo mkdir -p ${SCRIPT_PATHS.baseDir}`);
10722
8260
  for (const script of scripts) {
10723
8261
  await ssh.writeFileWithSudo(script.path, script.content);
10724
8262
  }
10725
8263
  await ssh.execOrThrow(
10726
- `sudo chmod +x ${SCRIPT_PATHS.baseDir}/*.py ${SCRIPT_PATHS.libDir}/*.py 2>/dev/null || true`
8264
+ `sudo chmod +x ${SCRIPT_PATHS.baseDir}/*.mjs 2>/dev/null || true`
10727
8265
  );
10728
8266
  }
10729
8267
  async function downloadStorages(ssh, manifest) {
@@ -10736,7 +8274,7 @@ async function downloadStorages(ssh, manifest) {
10736
8274
  const manifestJson = JSON.stringify(manifest);
10737
8275
  await ssh.writeFile("/tmp/storage-manifest.json", manifestJson);
10738
8276
  const result = await ssh.exec(
10739
- `python3 ${SCRIPT_PATHS.download} /tmp/storage-manifest.json`
8277
+ `node ${SCRIPT_PATHS.download} /tmp/storage-manifest.json`
10740
8278
  );
10741
8279
  if (result.exitCode !== 0) {
10742
8280
  throw new Error(`Storage download failed: ${result.stderr}`);
@@ -10763,14 +8301,13 @@ async function restoreSessionHistory(ssh, resumeSession, workingDir, cliAgentTyp
10763
8301
  `[Executor] Session history restored (${sessionHistory.split("\n").length} lines)`
10764
8302
  );
10765
8303
  }
10766
- var PROXY_CA_CERT_PATH = "/opt/vm0-runner/proxy/mitmproxy-ca-cert.pem";
10767
8304
  async function installProxyCA(ssh) {
10768
- if (!fs6.existsSync(PROXY_CA_CERT_PATH)) {
8305
+ if (!fs7.existsSync(PROXY_CA_CERT_PATH)) {
10769
8306
  throw new Error(
10770
8307
  `Proxy CA certificate not found at ${PROXY_CA_CERT_PATH}. Run generate-proxy-ca.sh first.`
10771
8308
  );
10772
8309
  }
10773
- const caCert = fs6.readFileSync(PROXY_CA_CERT_PATH, "utf-8");
8310
+ const caCert = fs7.readFileSync(PROXY_CA_CERT_PATH, "utf-8");
10774
8311
  console.log(
10775
8312
  `[Executor] Installing proxy CA certificate (${caCert.length} bytes)`
10776
8313
  );
@@ -10789,6 +8326,61 @@ nameserver 1.1.1.1`;
10789
8326
  `sudo sh -c 'rm -f /etc/resolv.conf && echo "${dnsConfig}" > /etc/resolv.conf'`
10790
8327
  );
10791
8328
  }
8329
+
8330
+ // src/lib/executor.ts
8331
+ function getVmIdFromRunId(runId) {
8332
+ return runId.split("-")[0] || runId.substring(0, 8);
8333
+ }
8334
+ var CURL_ERROR_MESSAGES = {
8335
+ 6: "DNS resolution failed",
8336
+ 7: "Connection refused",
8337
+ 28: "Connection timeout",
8338
+ 60: "TLS certificate error (proxy CA not trusted)",
8339
+ 22: "HTTP error from server"
8340
+ };
8341
+ async function runPreflightCheck(ssh, apiUrl, runId, sandboxToken, bypassSecret) {
8342
+ const heartbeatUrl = `${apiUrl}/api/webhooks/agent/heartbeat`;
8343
+ const bypassHeader = bypassSecret ? ` -H "x-vercel-protection-bypass: ${bypassSecret}"` : "";
8344
+ const curlCmd = `curl -sf --connect-timeout 5 --max-time 10 "${heartbeatUrl}" -X POST -H "Content-Type: application/json" -H "Authorization: Bearer ${sandboxToken}"${bypassHeader} -d '{"runId":"${runId}"}'`;
8345
+ const result = await ssh.exec(curlCmd, 2e4);
8346
+ if (result.exitCode === 0) {
8347
+ return { success: true };
8348
+ }
8349
+ const errorDetail = CURL_ERROR_MESSAGES[result.exitCode] ?? `curl exit code ${result.exitCode}`;
8350
+ const stderrInfo = result.stderr?.trim() ? ` (${result.stderr.trim()})` : "";
8351
+ return {
8352
+ success: false,
8353
+ error: `Preflight check failed: ${errorDetail}${stderrInfo} - VM cannot reach VM0 API at ${apiUrl}`
8354
+ };
8355
+ }
8356
+ async function reportPreflightFailure(apiUrl, runId, sandboxToken, error, bypassSecret) {
8357
+ const completeUrl = `${apiUrl}/api/webhooks/agent/complete`;
8358
+ const headers = {
8359
+ "Content-Type": "application/json",
8360
+ Authorization: `Bearer ${sandboxToken}`
8361
+ };
8362
+ if (bypassSecret) {
8363
+ headers["x-vercel-protection-bypass"] = bypassSecret;
8364
+ }
8365
+ try {
8366
+ const response = await fetch(completeUrl, {
8367
+ method: "POST",
8368
+ headers,
8369
+ body: JSON.stringify({
8370
+ runId,
8371
+ exitCode: 1,
8372
+ error
8373
+ })
8374
+ });
8375
+ if (!response.ok) {
8376
+ console.error(
8377
+ `[Executor] Failed to report preflight failure: HTTP ${response.status}`
8378
+ );
8379
+ }
8380
+ } catch (err) {
8381
+ console.error(`[Executor] Failed to report preflight failure: ${err}`);
8382
+ }
8383
+ }
10792
8384
  async function executeJob(context, config, options = {}) {
10793
8385
  const vmId = getVmIdFromRunId(context.runId);
10794
8386
  let vm = null;
@@ -10867,6 +8459,32 @@ async function executeJob(context, config, options = {}) {
10867
8459
  `[Executor] Writing env JSON (${envJson.length} bytes) to ${ENV_JSON_PATH}`
10868
8460
  );
10869
8461
  await ssh.writeFile(ENV_JSON_PATH, envJson);
8462
+ if (!options.benchmarkMode) {
8463
+ log(`[Executor] Running preflight connectivity check...`);
8464
+ const bypassSecret = process.env.VERCEL_AUTOMATION_BYPASS_SECRET;
8465
+ const preflight = await runPreflightCheck(
8466
+ ssh,
8467
+ config.server.url,
8468
+ context.runId,
8469
+ context.sandboxToken,
8470
+ bypassSecret
8471
+ );
8472
+ if (!preflight.success) {
8473
+ log(`[Executor] Preflight check failed: ${preflight.error}`);
8474
+ await reportPreflightFailure(
8475
+ config.server.url,
8476
+ context.runId,
8477
+ context.sandboxToken,
8478
+ preflight.error,
8479
+ bypassSecret
8480
+ );
8481
+ return {
8482
+ exitCode: 1,
8483
+ error: preflight.error
8484
+ };
8485
+ }
8486
+ log(`[Executor] Preflight check passed`);
8487
+ }
10870
8488
  const systemLogFile = `/tmp/vm0-main-${context.runId}.log`;
10871
8489
  const exitCodeFile = `/tmp/vm0-exit-${context.runId}`;
10872
8490
  const startTime = Date.now();
@@ -10879,7 +8497,7 @@ async function executeJob(context, config, options = {}) {
10879
8497
  } else {
10880
8498
  log(`[Executor] Running agent via env-loader (background)...`);
10881
8499
  await ssh.exec(
10882
- `nohup sh -c 'python3 -u ${ENV_LOADER_PATH}; echo $? > ${exitCodeFile}' > ${systemLogFile} 2>&1 &`
8500
+ `nohup sh -c 'node ${ENV_LOADER_PATH}; echo $? > ${exitCodeFile}' > ${systemLogFile} 2>&1 &`
10883
8501
  );
10884
8502
  log(`[Executor] Agent started in background`);
10885
8503
  }
@@ -10898,7 +8516,7 @@ async function executeJob(context, config, options = {}) {
10898
8516
  }
10899
8517
  if (!options.benchmarkMode) {
10900
8518
  const processCheck = await ssh.exec(
10901
- `pgrep -f "env-loader.py" > /dev/null 2>&1 && echo "RUNNING" || echo "DEAD"`
8519
+ `pgrep -f "env-loader.mjs" > /dev/null 2>&1 && echo "RUNNING" || echo "DEAD"`
10902
8520
  );
10903
8521
  if (processCheck.stdout.trim() === "DEAD") {
10904
8522
  log(
@@ -11331,7 +8949,7 @@ var benchmarkCommand = new Command3("benchmark").description(
11331
8949
  });
11332
8950
 
11333
8951
  // src/index.ts
11334
- var version = true ? "2.8.4" : "0.1.0";
8952
+ var version = true ? "2.10.0" : "0.1.0";
11335
8953
  program.name("vm0-runner").version(version).description("Self-hosted runner for VM0 agents");
11336
8954
  program.addCommand(startCommand);
11337
8955
  program.addCommand(statusCommand);