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