From aeb49460b78fd2df626227901dda24a33289d967 Mon Sep 17 00:00:00 2001 From: Codex Date: Tue, 9 Jun 2026 01:06:19 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20runner=20Codex=20sh?= =?UTF-8?q?ell=20=E5=B7=A5=E5=85=B7=E7=8E=AF=E5=A2=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/src/cli.ts | 15 ++++++-- src/backend/adapter.ts | 10 +++++- src/backend/codex-stdio.ts | 8 +++++ src/runner/k8s-job.ts | 7 ++++ src/runner/resource-bundle.ts | 36 +++++++++++++++---- src/selftest/cases/20-runner-k8s-job.ts | 3 +- src/selftest/cases/30-codex-stdio.ts | 11 +++++- .../cases/50-hwlab-manual-dispatch.ts | 9 +++-- src/selftest/fake-codex-app-server.ts | 8 +++++ 9 files changed, 92 insertions(+), 15 deletions(-) diff --git a/scripts/src/cli.ts b/scripts/src/cli.ts index 8f47e1d..e56f1d0 100644 --- a/scripts/src/cli.ts +++ b/scripts/src/cli.ts @@ -248,7 +248,7 @@ async function sessionTurn(args: ParsedArgs, positionalSessionId: string | null) copyOptionalFlag(args, runnerBody, "attempt-id", "attemptId"); copyOptionalFlag(args, runnerBody, "runner-id", "runnerId"); copyOptionalFlag(args, runnerBody, "source-commit", "sourceCommit"); - copyOptionalFlag(args, runnerBody, "runner-manager-url", "managerUrl"); + copyRunnerManagerUrlFlag(args, runnerBody); copyOptionalFlag(args, runnerBody, "service-account-name", "serviceAccountName"); const runnerIdempotencyKey = optionalFlag(args, "runner-idempotency-key"); if (runnerIdempotencyKey) runnerBody.idempotencyKey = runnerIdempotencyKey; @@ -314,7 +314,7 @@ async function dispatchQueueTask(args: ParsedArgs, taskId: string): Promise { if (attemptId) body.attemptId = attemptId; if (runnerId) body.runnerId = runnerId; if (sourceCommit) body.sourceCommit = sourceCommit; - if (runnerManagerUrl) body.managerUrl = runnerManagerUrl; + if (runnerManagerUrl) body.managerUrl = resolveRunnerManagerUrlFlag(args, runnerManagerUrl); if (idempotencyKey) body.idempotencyKey = idempotencyKey; return await client(args).post(`/api/v1/runs/${encodeURIComponent(runId)}/runner-jobs`, body) as JsonRecord; } @@ -754,6 +754,15 @@ function copyOptionalFlag(args: ParsedArgs, target: JsonRecord, flagName: string if (value) target[key] = value; } +function copyRunnerManagerUrlFlag(args: ParsedArgs, target: JsonRecord): void { + const value = optionalFlag(args, "runner-manager-url"); + if (value) target.managerUrl = resolveRunnerManagerUrlFlag(args, value); +} + +function resolveRunnerManagerUrlFlag(args: ParsedArgs, value: string): string { + return value === "auto" ? managerUrl(args) : value; +} + function readerQuery(args: ParsedArgs): string { const readerId = optionalFlag(args, "reader-id"); return readerId ? `?readerId=${encodeURIComponent(readerId)}` : ""; diff --git a/src/backend/adapter.ts b/src/backend/adapter.ts index 8ca51bd..d7e82a5 100644 --- a/src/backend/adapter.ts +++ b/src/backend/adapter.ts @@ -46,12 +46,15 @@ export function createBackendSession(run: RunRecord, options: BackendAdapterOpti export function backendTurnOptions(run: RunRecord, command: CommandRecord, options: BackendAdapterOptions = {}): CodexStdioTurnOptions { const prompt = typeof command.payload.prompt === "string" ? command.payload.prompt : JSON.stringify(command.payload); + const sandboxOverride = codexShellSandboxOverride(options.env ?? process.env); const turnOptions: CodexStdioTurnOptions = { backendProfile: run.backendProfile, prompt, cwd: options.workspacePath ?? (typeof run.workspaceRef.path === "string" ? run.workspaceRef.path : process.cwd()), approvalPolicy: run.executionPolicy.approval, - sandbox: run.executionPolicy.sandbox, + sandbox: sandboxOverride ?? run.executionPolicy.sandbox, + requestedSandbox: run.executionPolicy.sandbox, + sandboxOverrideSource: sandboxOverride ? "AGENTRUN_CODEX_SHELL_SANDBOX" : null, timeoutMs: run.executionPolicy.timeoutMs, }; if (typeof command.payload.model === "string") turnOptions.model = command.payload.model; @@ -67,3 +70,8 @@ export function backendTurnOptions(run: RunRecord, command: CommandRecord, optio if (options.onActiveTurn) turnOptions.onActiveTurn = options.onActiveTurn; return turnOptions; } + +function codexShellSandboxOverride(env: NodeJS.ProcessEnv): string | null { + const value = env.AGENTRUN_CODEX_SHELL_SANDBOX?.trim(); + return value && value.length > 0 ? value : null; +} diff --git a/src/backend/codex-stdio.ts b/src/backend/codex-stdio.ts index ced4a2f..b7a5290 100644 --- a/src/backend/codex-stdio.ts +++ b/src/backend/codex-stdio.ts @@ -45,6 +45,8 @@ export interface CodexStdioTurnOptions { threadId?: string; approvalPolicy: string; sandbox: string; + requestedSandbox?: string; + sandboxOverrideSource?: string | null; timeoutMs: number; command?: string; args?: string[]; @@ -1065,6 +1067,12 @@ function backendMetadata(options: CodexStdioTurnOptions): JsonRecord { backendKind: spec?.backendKind ?? "codex-app-server-stdio", protocol: spec?.protocol ?? codexProtocol, transport: spec?.transport ?? "stdio", + sandbox: { + requested: options.requestedSandbox ?? options.sandbox, + effective: options.sandbox, + overrideSource: options.sandboxOverrideSource ?? null, + valuesPrinted: false, + }, }; } diff --git a/src/runner/k8s-job.ts b/src/runner/k8s-job.ts index 8fc93d0..979d2ce 100644 --- a/src/runner/k8s-job.ts +++ b/src/runner/k8s-job.ts @@ -4,6 +4,7 @@ import { backendProfileSpec } from "../common/backend-profiles.js"; const defaultBootRepoUrl = "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/agentrun.git"; const defaultResourceBinPath = "/usr/local/bin"; +const defaultCodexShellSandbox = "danger-full-access"; export interface RunnerJobRenderOptions { run: RunRecord; @@ -177,6 +178,7 @@ function runnerEnv(options: RunnerJobRenderOptions, context: { namespace: string { name: "AGENTRUN_RUNNER_ID", value: context.runnerId }, { name: "AGENTRUN_BACKEND_PROFILE", value: options.run.backendProfile }, { name: "AGENTRUN_EXECUTION_POLICY_JSON", value: JSON.stringify(options.run.executionPolicy) }, + { name: "AGENTRUN_CODEX_SHELL_SANDBOX", value: codexShellSandbox(options.run.executionPolicy) }, { name: "AGENTRUN_SESSION_REF_JSON", value: JSON.stringify(options.run.sessionRef ?? null) }, { name: "AGENTRUN_RESOURCE_BUNDLE_JSON", value: JSON.stringify(options.run.resourceBundleRef ?? null) }, { name: "AGENTRUN_WORKSPACE_ROOT", value: "/home/agentrun/workspaces" }, @@ -205,6 +207,11 @@ function runnerEnv(options: RunnerJobRenderOptions, context: { namespace: string ]; } +function codexShellSandbox(policy: ExecutionPolicy): string { + if (policy.sandbox === "workspace-write") return defaultCodexShellSandbox; + return policy.sandbox; +} + function toolCredentialEnvVars(items: ToolCredentialProjection[]): JsonRecord[] { return items.map((item) => ({ name: item.envName, diff --git a/src/runner/resource-bundle.ts b/src/runner/resource-bundle.ts index 1946b27..795481b 100644 --- a/src/runner/resource-bundle.ts +++ b/src/runner/resource-bundle.ts @@ -90,7 +90,7 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl }; const defaultCheckout = await checkoutFor(defaultSource); const materializedBundles = await materializeGitBundles(workspacePath, resourceBundleRef, defaultSource, defaultCheckout, checkoutFor); - const tools = await prepareGitBundleTools(workspacePath); + const tools = await prepareGitBundleTools(workspacePath, env); const skills = await discoverGitBundleSkills(workspacePath); const prompts = await materializePromptRefs(defaultCheckout.checkoutPath, resourceBundleRef.promptRefs ?? []); const initialPrompt = assembleInitialPrompt(prompts.items, skills.items); @@ -193,28 +193,50 @@ function optionalNonEmpty(value: unknown): string | undefined { return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; } -async function prepareGitBundleTools(workspacePath: string): Promise<{ binPath?: string; event: JsonRecord }> { - const binPath = path.join(workspacePath, "tools"); +async function prepareGitBundleTools(workspacePath: string, env: NodeJS.ProcessEnv): Promise<{ binPath?: string; event: JsonRecord }> { + const sourceBinPath = path.join(workspacePath, "tools"); + const installedBinPath = optionalNonEmpty(env.AGENTRUN_RESOURCE_BIN_PATH); + const runtimeBinPath = installedBinPath ?? sourceBinPath; let entries; try { - entries = await readdir(binPath, { withFileTypes: true }); + entries = await readdir(sourceBinPath, { withFileTypes: true }); } catch (error) { - if (error && typeof error === "object" && "code" in error && (error as { code?: unknown }).code === "ENOENT") return { event: { count: 0, names: [], binPath: null, valuesPrinted: false } }; + if (error && typeof error === "object" && "code" in error && (error as { code?: unknown }).code === "ENOENT") return { event: { count: 0, names: [], binPath: null, sourceBinPath: null, installedBinPath: null, installed: false, valuesPrinted: false } }; throw error; } const names: string[] = []; const items: JsonRecord[] = []; + if (installedBinPath) await mkdir(installedBinPath, { recursive: true }); for (const entry of entries) { if (!entry.isFile()) continue; - const filePath = path.join(binPath, entry.name); + const filePath = path.join(sourceBinPath, entry.name); const text = await readFile(filePath, "utf8"); const firstLine = text.split(/\r?\n/u, 1)[0] ?? ""; if (!firstLine.startsWith("#!")) continue; await chmod(filePath, 0o755); + if (installedBinPath) { + const targetPath = path.join(installedBinPath, entry.name); + if (targetPath !== filePath) { + await cp(filePath, targetPath, { force: true, dereference: false }); + await chmod(targetPath, 0o755); + } + } names.push(entry.name); items.push({ name: entry.name, sha256: sha256Text(text), bytes: Buffer.byteLength(text, "utf8"), shebang: firstLine.slice(0, 80), valuesPrinted: false }); } - return { binPath, event: { count: names.length, names, items, binPath: pathSummary(binPath), valuesPrinted: false } }; + return { + ...(names.length > 0 ? { binPath: runtimeBinPath } : {}), + event: { + count: names.length, + names, + items, + binPath: names.length > 0 ? pathSummary(runtimeBinPath) : null, + sourceBinPath: pathSummary(sourceBinPath), + installedBinPath: installedBinPath ? pathSummary(installedBinPath) : null, + installed: Boolean(installedBinPath && names.length > 0), + valuesPrinted: false, + }, + }; } async function materializePromptRefs(checkoutPath: string, refs: NonNullable): Promise<{ items: MaterializedPromptRef[]; event: JsonRecord }> { diff --git a/src/selftest/cases/20-runner-k8s-job.ts b/src/selftest/cases/20-runner-k8s-job.ts index 228d605..c700eae 100644 --- a/src/selftest/cases/20-runner-k8s-job.ts +++ b/src/selftest/cases/20-runner-k8s-job.ts @@ -42,6 +42,7 @@ const selfTest: SelfTestCase = async (context) => { assertRunnerJobUsesWritableCodexHome(rendered.manifest as JsonRecord, context.codexHome, "codex-0", "/var/run/agentrun/secrets/codex-0"); assertRunnerJobUsesToolCredential(rendered, "GH_TOKEN", "agentrun-v01-tool-github-pr", "GH_TOKEN"); assertRunnerJobUsesToolCredential(rendered, "UNIDESK_SSH_CLIENT_TOKEN", "agentrun-v01-tool-unidesk-ssh", "UNIDESK_SSH_CLIENT_TOKEN"); + assert.equal(runnerEnvValue(rendered.manifest as JsonRecord, "AGENTRUN_CODEX_SHELL_SANDBOX"), "danger-full-access"); assert.equal(runnerEnvValue(rendered.manifest as JsonRecord, "HWLAB_API_KEY"), "REDACTED"); assert.deepEqual((((rendered.transientEnv as JsonRecord).names) as string[]), ["HWLAB_API_KEY"]); assertNoSecretLeak(rendered); @@ -227,7 +228,7 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin assert.equal(envMap.get("AGENTRUN_SESSION_PVC_NAMESPACE"), "agentrun-v01"); assert.equal(envMap.get("AGENTRUN_SESSION_PVC_MOUNT_PATH"), "/home/agentrun/.codex-codex/sessions"); assert.equal(envMap.get("AGENTRUN_CODEX_ROLLOUT_SUBDIR"), "sessions"); - return { name: "runner-k8s-job", tests: ["runner-k8s-job-dry-run", "runner-k8s-job-deepseek-profile-dry-run", "runner-k8s-job-minimax-m3-profile-dry-run", "runner-k8s-job-dsflash-go-profile-dry-run", "runner-k8s-job-dsflash-go-legacy-secretref-normalized", "runner-k8s-job-create-api", "runner-k8s-job-retention-ttl", "runner-job-transient-env", "runner-job-tool-credential-env", "runner-job-unidesk-ssh-tool-credential-env", "runner-job-unidesk-ssh-transient-env-denied", "runner-k8s-job-session-pvc-volume-and-env"] }; + return { name: "runner-k8s-job", tests: ["runner-k8s-job-dry-run", "runner-k8s-job-codex-shell-sandbox-env", "runner-k8s-job-deepseek-profile-dry-run", "runner-k8s-job-minimax-m3-profile-dry-run", "runner-k8s-job-dsflash-go-profile-dry-run", "runner-k8s-job-dsflash-go-legacy-secretref-normalized", "runner-k8s-job-create-api", "runner-k8s-job-retention-ttl", "runner-job-transient-env", "runner-job-tool-credential-env", "runner-job-unidesk-ssh-tool-credential-env", "runner-job-unidesk-ssh-transient-env-denied", "runner-k8s-job-session-pvc-volume-and-env"] }; } finally { await new Promise((resolve) => server.server.close(() => resolve())); } diff --git a/src/selftest/cases/30-codex-stdio.ts b/src/selftest/cases/30-codex-stdio.ts index 8dd8492..66b3af5 100644 --- a/src/selftest/cases/30-codex-stdio.ts +++ b/src/selftest/cases/30-codex-stdio.ts @@ -27,6 +27,15 @@ const selfTest: SelfTestCase = async (context) => { const finalCommand = await client.get(`/api/v1/runs/${happy.runId}/commands/${happy.commandId}`) as { state?: string }; assert.equal(finalCommand.state, "completed"); + const sandboxOverride = await createRunWithCommand(client, context, "hello sandbox override", "selftest-sandbox-override", 15_000); + const sandboxOverrideResult = await runOnce({ managerUrl: server.baseUrl, runId: sandboxOverride.runId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome, AGENTRUN_CODEX_SHELL_SANDBOX: "danger-full-access", AGENTRUN_FAKE_CODEX_MODE: "require-danger-sandbox" }, oneShot: true }); + assert.equal(sandboxOverrideResult.terminalStatus, "completed"); + const sandboxEvents = await client.get(`/api/v1/runs/${sandboxOverride.runId}/events?afterSeq=0&limit=100`) as { items?: Array<{ type: string; payload: JsonRecord }> }; + const sandboxStarting = sandboxEvents.items?.find((event) => event.type === "backend_status" && event.payload.phase === "codex-app-server-starting"); + assert.equal(((sandboxStarting?.payload.sandbox as JsonRecord | undefined)?.requested), "workspace-write"); + assert.equal(((sandboxStarting?.payload.sandbox as JsonRecord | undefined)?.effective), "danger-full-access"); + assert.equal(((sandboxStarting?.payload.sandbox as JsonRecord | undefined)?.overrideSource), "AGENTRUN_CODEX_SHELL_SANDBOX"); + await runLeaseConflictRecoveryCase({ client, managerUrl: server.baseUrl, context }); const projectedHome = path.join(context.tmp, "runtime-codex-home"); @@ -230,7 +239,7 @@ const selfTest: SelfTestCase = async (context) => { await runSessionStorageSubdirCase({ client, managerUrl: server.baseUrl, context }); await runSessionStorageNoSecretLeakCase({ client, managerUrl: server.baseUrl, context }); - return { name: "codex-stdio", tests: ["runner-lease-heartbeat", "runner-lease-conflict-recovery", "codex-stdio-fake-turn", "codex-stdio-projected-writable-home", "codex-stdio-deepseek-profile-fake-turn", "codex-stdio-dsflash-go-profile-fake-turn", "codex-stdio-dsflash-go-config-metadata", "codex-stdio-minimax-m3-profile-fake-turn", "codex-stdio-deepseek-missing-secret-no-fallback", "codex-stdio-minimax-m3-missing-secret-no-fallback", "codex-stdio-config-model-authoritative", "codex-stdio-explicit-model-forwarded", "codex-stdio-final-agent-message-only", "codex-stdio-web-search-progress", "codex-stdio-stale-thread-resume-failed", "codex-stdio-live-tool-events", "codex-stdio-noisy-reasoning-suppression", "codex-stdio-missing-turn-result", "codex-stdio-provider-auth-failed", "codex-stdio-provider-rate-limited", "codex-stdio-provider-invalid-tool-call", "codex-stdio-provider-compact-unsupported", "codex-stdio-provider-503-rpc-error", "codex-stdio-provider-503-terminal", "codex-stdio-provider-503-retry-event", "codex-stdio-invalid-json", "codex-stdio-timeout", "codex-stdio-idle-timeout-progress-refresh", "codex-stdio-command-failure-keeps-run-open", "codex-stdio-secret-unavailable", "codex-stdio-spawn-failure"] }; + return { name: "codex-stdio", tests: ["runner-lease-heartbeat", "runner-lease-conflict-recovery", "codex-stdio-fake-turn", "codex-stdio-k8s-sandbox-override", "codex-stdio-projected-writable-home", "codex-stdio-deepseek-profile-fake-turn", "codex-stdio-dsflash-go-profile-fake-turn", "codex-stdio-dsflash-go-config-metadata", "codex-stdio-minimax-m3-profile-fake-turn", "codex-stdio-deepseek-missing-secret-no-fallback", "codex-stdio-minimax-m3-missing-secret-no-fallback", "codex-stdio-config-model-authoritative", "codex-stdio-explicit-model-forwarded", "codex-stdio-final-agent-message-only", "codex-stdio-web-search-progress", "codex-stdio-stale-thread-resume-failed", "codex-stdio-live-tool-events", "codex-stdio-noisy-reasoning-suppression", "codex-stdio-missing-turn-result", "codex-stdio-provider-auth-failed", "codex-stdio-provider-rate-limited", "codex-stdio-provider-invalid-tool-call", "codex-stdio-provider-compact-unsupported", "codex-stdio-provider-503-rpc-error", "codex-stdio-provider-503-terminal", "codex-stdio-provider-503-retry-event", "codex-stdio-invalid-json", "codex-stdio-timeout", "codex-stdio-idle-timeout-progress-refresh", "codex-stdio-command-failure-keeps-run-open", "codex-stdio-secret-unavailable", "codex-stdio-spawn-failure"] }; } finally { await new Promise((resolve) => server.server.close(() => resolve())); } diff --git a/src/selftest/cases/50-hwlab-manual-dispatch.ts b/src/selftest/cases/50-hwlab-manual-dispatch.ts index d32fa11..f6b3f05 100644 --- a/src/selftest/cases/50-hwlab-manual-dispatch.ts +++ b/src/selftest/cases/50-hwlab-manual-dispatch.ts @@ -1,7 +1,7 @@ import assert from "node:assert/strict"; import { execFile as execFileCallback } from "node:child_process"; import { promisify } from "node:util"; -import { chmod, mkdir, readFile, writeFile } from "node:fs/promises"; +import { access, chmod, mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import { startManagerServer } from "../../mgr/server.js"; import { ManagerClient } from "../../mgr/client.js"; @@ -79,8 +79,12 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin ); const sessionRun = await createHwlabRun(client, context, bundle, "hwlab-session-resume", "hello session", "hwlab-command-session"); - const runResult = await runOnce({ managerUrl: server.baseUrl, runId: sessionRun.runId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome, AGENTRUN_WORKSPACE_ROOT: path.join(context.tmp, "workspaces") }, oneShot: true }); + const resourceBinPath = path.join(context.tmp, "resource-bin"); + const runResult = await runOnce({ managerUrl: server.baseUrl, runId: sessionRun.runId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome, AGENTRUN_WORKSPACE_ROOT: path.join(context.tmp, "workspaces"), AGENTRUN_RESOURCE_BIN_PATH: resourceBinPath }, oneShot: true }); assert.equal(runResult.terminalStatus, "completed"); + await access(path.join(resourceBinPath, "hwpod")); + const resourceBinExec = await execFile(path.join(resourceBinPath, "hwpod"), ["--selftest"]); + assert.match(resourceBinExec.stdout, /hwpod-selftest/u); const session = await store.getSession("hwlab-session-resume"); assert.equal(session?.threadId, "thread_selftest_1"); const resultEnvelope = await client.get(`/api/v1/runs/${sessionRun.runId}/commands/${sessionRun.commandId}/result`) as JsonRecord; @@ -93,6 +97,7 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin assert.deepEqual(resultBundleTargets, ["tools", ".agents/skills"]); const materialized = ((resultEnvelope.resourceBundleRef as JsonRecord).materialized as JsonRecord); assert.deepEqual(((materialized.tools as JsonRecord).names), ["hwpod"]); + assert.equal(((materialized.tools as JsonRecord).installed), true); assert.deepEqual(((materialized.skillDirs as JsonRecord).names), ["hwpod-cli", "hwpod-ctl"]); assertNoSecretLeak(resultEnvelope); diff --git a/src/selftest/fake-codex-app-server.ts b/src/selftest/fake-codex-app-server.ts index ab1b153..8f4205a 100644 --- a/src/selftest/fake-codex-app-server.ts +++ b/src/selftest/fake-codex-app-server.ts @@ -23,6 +23,10 @@ for await (const line of rl) { } if (message.method === "thread/start") { observedThreadModel = Object.hasOwn(message.params ?? {}, "model"); + if (mode === "require-danger-sandbox" && message.params?.sandbox !== "danger-full-access") { + respond(message.id, null, { code: -32000, message: `thread/start expected danger-full-access sandbox, got ${String(message.params?.sandbox ?? "missing")}` }); + continue; + } if (mode === "reject-unexpected-model" && observedThreadModel) { respond(message.id, null, { code: -32000, message: "thread/start unexpectedly included model" }); continue; @@ -39,6 +43,10 @@ for await (const line of rl) { } if (message.method === "thread/resume") { observedThreadModel = Object.hasOwn(message.params ?? {}, "model"); + if (mode === "require-danger-sandbox" && message.params?.sandbox !== "danger-full-access") { + respond(message.id, null, { code: -32000, message: `thread/resume expected danger-full-access sandbox, got ${String(message.params?.sandbox ?? "missing")}` }); + continue; + } if (mode === "resume-no-rollout") { respond(message.id, null, { code: -32000, message: `no rollout found for thread id ${String(message.params?.threadId ?? "unknown")}` }); continue;