agentbox-sdk 0.1.502 → 0.1.508

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.
@@ -15,11 +15,12 @@ import {
15
15
  debugRelay,
16
16
  debugRuntime,
17
17
  debugSetup,
18
+ getAvailablePort,
18
19
  linesFromTextChunks,
19
20
  sleep,
20
21
  time,
21
22
  waitFor
22
- } from "./chunk-AVXJMCBC.js";
23
+ } from "./chunk-MTQ2S46C.js";
23
24
  import {
24
25
  shellQuote
25
26
  } from "./chunk-NSJM57Z4.js";
@@ -29,16 +30,86 @@ import {
29
30
  } from "./chunk-GOFJNFAD.js";
30
31
 
31
32
  // src/agents/Agent.ts
32
- import { randomUUID as randomUUID2 } from "crypto";
33
+ import { randomUUID as randomUUID3 } from "crypto";
34
+ import path11 from "path";
33
35
 
34
36
  // src/agents/providers/claude-code.ts
35
37
  import { randomUUID } from "crypto";
38
+
39
+ // src/agents/transports/spawn.ts
40
+ import { spawn } from "child_process";
41
+ import { createInterface } from "readline";
42
+ function spawnCommand(options) {
43
+ const child = spawn(options.command, options.args ?? [], {
44
+ cwd: options.cwd,
45
+ env: options.env,
46
+ stdio: "pipe",
47
+ shell: process.platform === "win32",
48
+ windowsHide: true,
49
+ detached: options.processGroup === true && process.platform !== "win32"
50
+ });
51
+ const exitPromise = new Promise((resolve, reject) => {
52
+ child.once("error", reject);
53
+ child.once("close", (code) => resolve(code ?? 0));
54
+ });
55
+ void exitPromise.catch(() => void 0);
56
+ let killPromise;
57
+ const signalProcess = (signal) => {
58
+ try {
59
+ if (options.processGroup && process.platform !== "win32" && child.pid) process.kill(-child.pid, signal);
60
+ else child.kill(signal);
61
+ } catch (error) {
62
+ if (error.code !== "ESRCH") throw error;
63
+ }
64
+ };
65
+ const waitForExit = async (timeoutMs) => {
66
+ let timer;
67
+ try {
68
+ return await Promise.race([
69
+ exitPromise.then(() => true, () => true),
70
+ new Promise((resolve) => {
71
+ timer = setTimeout(() => resolve(false), timeoutMs);
72
+ })
73
+ ]);
74
+ } finally {
75
+ if (timer) clearTimeout(timer);
76
+ }
77
+ };
78
+ return {
79
+ child,
80
+ wait: () => exitPromise,
81
+ kill: (signal = "SIGTERM") => killPromise ??= (async () => {
82
+ signalProcess(signal);
83
+ if (await waitForExit(options.terminationTimeoutMs ?? 3e3)) {
84
+ if (options.processGroup && process.platform !== "win32") signalProcess("SIGKILL");
85
+ return;
86
+ }
87
+ signalProcess("SIGKILL");
88
+ if (!await waitForExit(3e3)) throw new Error("The owned agent process did not stop");
89
+ })()
90
+ };
91
+ }
92
+ async function* linesFromNodeStream(stream) {
93
+ const lineReader = createInterface({ input: stream });
94
+ try {
95
+ for await (const line of lineReader) {
96
+ yield line;
97
+ }
98
+ } finally {
99
+ lineReader.close();
100
+ }
101
+ }
102
+
103
+ // src/agents/providers/claude-code.ts
36
104
  import path8 from "path";
37
105
 
38
106
  // src/agents/approval.ts
39
107
  function getApprovalMode(options) {
40
108
  return options.approvalMode ?? "auto";
41
109
  }
110
+ function hasInteractiveQuestions(options) {
111
+ return options.interactiveQuestions ?? isInteractiveApproval(options);
112
+ }
42
113
  function isInteractiveApproval(options) {
43
114
  return getApprovalMode(options) === "interactive";
44
115
  }
@@ -49,6 +120,58 @@ function shouldAutoApproveClaudeTools(options) {
49
120
  return !isInteractiveApproval(options);
50
121
  }
51
122
 
123
+ // src/agents/questions.ts
124
+ function record(value) {
125
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid agent question");
126
+ return value;
127
+ }
128
+ function text(value, max) {
129
+ if (typeof value !== "string" || !value.trim() || value.length > max) throw new Error("Invalid agent question text");
130
+ return value;
131
+ }
132
+ function normalizeUserQuestions(provider, input) {
133
+ const questions = record(input).questions;
134
+ if (!Array.isArray(questions) || !questions.length || questions.length > 10) throw new Error("Unsupported agent question count");
135
+ return questions.map((value, index) => {
136
+ const item = record(value);
137
+ if (item.isSecret === true) throw new Error("Secret questions cannot be answered through task history");
138
+ const options = item.options ?? [];
139
+ if (!Array.isArray(options) || options.length > 30) throw new Error("Unsupported agent question options");
140
+ const result = {
141
+ id: String(index),
142
+ question: text(item.question, 1e4),
143
+ ...item.header ? { header: text(item.header, 200) } : {},
144
+ options: options.map((value2) => {
145
+ const option = record(value2);
146
+ return { label: text(option.label, 1e3), ...option.description ? { description: text(option.description, 1e4) } : {} };
147
+ }),
148
+ multiple: provider === "claude-code" ? item.multiSelect === true : provider === "open-code" && item.multiple === true,
149
+ allowCustom: provider === "open-code" ? item.custom !== false : true
150
+ };
151
+ if (new Set(result.options.map((option) => option.label)).size !== result.options.length) throw new Error("Duplicate agent question options");
152
+ if (!result.options.length && !result.allowCustom) throw new Error("Agent question has no available answers");
153
+ return result;
154
+ });
155
+ }
156
+ function validateUserAnswers(questions, answers) {
157
+ if (!questions?.length || !answers || answers.length !== questions.length || new Set(answers.map((answer) => answer.questionId)).size !== answers.length) throw new Error("Answer each agent question once");
158
+ return questions.map((question) => {
159
+ const answer = answers.find((answer2) => answer2.questionId === question.id);
160
+ if (!answer || !Array.isArray(answer.values) || !answer.values.length || answer.values.length > (question.multiple ? 30 : 1)) throw new Error("Choose an answer for each question");
161
+ if (new Set(answer.values).size !== answer.values.length || answer.values.some((value) => typeof value !== "string" || !value.trim() || value.length > 1e4 || !question.allowCustom && !question.options.some((option) => option.label === value))) throw new Error("Invalid answer to agent question");
162
+ return { questionId: question.id, values: [...answer.values] };
163
+ });
164
+ }
165
+ function questionReply(provider, input, answers) {
166
+ const normalized = normalizeUserQuestions(provider, input);
167
+ const ordered = validateUserAnswers(normalized, answers);
168
+ if (provider === "open-code") return ordered.map((answer) => answer.values);
169
+ const original = record(input).questions;
170
+ const keys = original.map((item) => text(record(item)[provider === "codex" ? "id" : "question"], 1e4));
171
+ if (new Set(keys).size !== keys.length) throw new Error("Duplicate agent question IDs");
172
+ return Object.fromEntries(keys.map((key, index) => [key, provider === "codex" ? { answers: ordered[index].values } : ordered[index].values.join(", ")]));
173
+ }
174
+
52
175
  // src/agents/input.ts
53
176
  import { readFile } from "fs/promises";
54
177
  import path from "path";
@@ -894,64 +1017,16 @@ function buildCodexConfigToml(opts = {}) {
894
1017
  import { mkdir, chmod, rm, writeFile } from "fs/promises";
895
1018
  import os from "os";
896
1019
  import path4 from "path";
897
-
898
- // src/agents/transports/spawn.ts
899
- import { spawn } from "child_process";
900
- import { createInterface } from "readline";
901
- function spawnCommand(options) {
902
- const child = spawn(options.command, options.args ?? [], {
903
- cwd: options.cwd,
904
- env: options.env,
905
- stdio: "pipe",
906
- shell: process.platform === "win32",
907
- windowsHide: true
908
- });
909
- const exitPromise = new Promise((resolve, reject) => {
910
- child.once("error", reject);
911
- child.once("close", (code) => resolve(code ?? 0));
912
- });
913
- return {
914
- child,
915
- wait: () => exitPromise,
916
- kill: async (signal = "SIGTERM") => {
917
- child.kill(signal);
918
- await exitPromise.catch(() => void 0);
919
- }
920
- };
921
- }
922
- async function waitForHttpReady(url, options) {
923
- await waitFor(
924
- async () => {
925
- try {
926
- const response = await fetch(url, options?.init);
927
- return response.ok;
928
- } catch {
929
- return false;
930
- }
931
- },
932
- {
933
- timeoutMs: options?.timeoutMs,
934
- intervalMs: options?.intervalMs
935
- }
936
- );
937
- }
938
- async function* linesFromNodeStream(stream) {
939
- const lineReader = createInterface({ input: stream });
940
- try {
941
- for await (const line of lineReader) {
942
- yield line;
943
- }
944
- } finally {
945
- lineReader.close();
946
- }
947
- }
948
-
949
- // src/agents/config/setup.ts
950
1020
  function shortLabel(command) {
951
1021
  const oneLine = command.replace(/\s+/g, " ").trim();
952
1022
  return oneLine.length > 60 ? `${oneLine.slice(0, 60)}\u2026` : oneLine;
953
1023
  }
954
- function agentboxRoot(provider, hasSandbox = true) {
1024
+ function agentboxRoot(provider, hasSandbox = true, stateDirectory) {
1025
+ if (stateDirectory !== void 0) {
1026
+ if (hasSandbox) throw new Error("stateDirectory is only supported for host execution.");
1027
+ if (!path4.isAbsolute(stateDirectory)) throw new Error("stateDirectory must be an absolute path.");
1028
+ return path4.join(stateDirectory, provider);
1029
+ }
955
1030
  return hasSandbox ? `/tmp/agentbox/${provider}` : path4.join(os.tmpdir(), `agentbox-${provider}`);
956
1031
  }
957
1032
  function getAgentLayout(rootDir) {
@@ -1004,9 +1079,9 @@ var HostSetupTarget = class {
1004
1079
  async () => {
1005
1080
  await Promise.all(
1006
1081
  files.map(async (entry) => {
1007
- await mkdir(path4.dirname(entry.path), { recursive: true });
1082
+ await mkdir(path4.dirname(entry.path), { recursive: true, mode: 448 });
1008
1083
  const content = typeof entry.content === "string" ? entry.content : entry.content;
1009
- await writeFile(entry.path, content);
1084
+ await writeFile(entry.path, content, { mode: entry.mode ?? 384 });
1010
1085
  if (entry.mode && (entry.mode & 73) !== 0) {
1011
1086
  await chmod(entry.path, entry.mode);
1012
1087
  }
@@ -1167,12 +1242,13 @@ async function createSetupTarget(provider, setupId, options) {
1167
1242
  return time(debugRuntime, `createSetupTarget ${provider}`, async () => {
1168
1243
  void setupId;
1169
1244
  const layout = getAgentLayout(
1170
- agentboxRoot(provider, Boolean(options.sandbox))
1245
+ agentboxRoot(provider, Boolean(options.sandbox), options.stateDirectory)
1171
1246
  );
1172
1247
  if (options.sandbox) {
1173
1248
  return new SandboxSetupTarget(provider, layout, options);
1174
1249
  }
1175
- await mkdir(layout.homeDir, { recursive: true });
1250
+ await mkdir(layout.homeDir, { recursive: true, mode: 448 });
1251
+ await chmod(layout.homeDir, 448);
1176
1252
  await mkdir(layout.xdgConfigHome, { recursive: true });
1177
1253
  await mkdir(layout.agentsDir, { recursive: true });
1178
1254
  await mkdir(layout.claudeDir, { recursive: true });
@@ -1750,7 +1826,7 @@ function extractOpenCodeCostData(events) {
1750
1826
  }
1751
1827
 
1752
1828
  // src/agents/providers/claude-code.ts
1753
- var DAEMON_PROTOCOL_VERSION = "3";
1829
+ var DAEMON_PROTOCOL_VERSION = "4";
1754
1830
  var DAEMON_PORT = 43180;
1755
1831
  var DAEMON_PATH = "/tmp/agentbox/claude-code/daemon.mjs";
1756
1832
  var DAEMON_LOG_PATH = "/tmp/agentbox/claude-code/daemon.log";
@@ -1760,7 +1836,7 @@ var DAEMON_READY_TIMEOUT_MS = 3e4;
1760
1836
  var DAEMON_READY_POLL_INTERVAL_MS = 250;
1761
1837
  function claudeConfigDir(options) {
1762
1838
  return path8.join(
1763
- agentboxRoot(AgentProvider.ClaudeCode, Boolean(options.sandbox)),
1839
+ agentboxRoot(AgentProvider.ClaudeCode, Boolean(options.sandbox), options.stateDirectory),
1764
1840
  ".claude"
1765
1841
  );
1766
1842
  }
@@ -1768,7 +1844,7 @@ function buildClaudeQueryOptions(params) {
1768
1844
  const provider = params.request.options.provider;
1769
1845
  const run = params.request.run;
1770
1846
  const extraArgs = {
1771
- "mcp-config": params.mcpConfigPath
1847
+ ...params.mcpConfigPath ? { "mcp-config": params.mcpConfigPath } : {}
1772
1848
  };
1773
1849
  for (const arg of provider?.args ?? []) {
1774
1850
  if (typeof arg !== "string") continue;
@@ -1785,7 +1861,11 @@ function buildClaudeQueryOptions(params) {
1785
1861
  cwd: params.cwd ?? params.request.options.cwd,
1786
1862
  env: params.env,
1787
1863
  pathToClaudeCodeExecutable: provider?.binary ?? "claude",
1788
- settings: params.settingsPath,
1864
+ ...params.settingsPath ? { settings: params.settingsPath } : {},
1865
+ ...params.request.options.configuration === "native" ? {
1866
+ settingSources: ["user", "project", "local"],
1867
+ systemPrompt: { type: "preset", preset: "claude_code" }
1868
+ } : {},
1789
1869
  extraArgs,
1790
1870
  includePartialMessages: true,
1791
1871
  forwardSubagentText: true,
@@ -1794,8 +1874,8 @@ function buildClaudeQueryOptions(params) {
1794
1874
  ...provider?.additionalDirectories?.length ? { additionalDirectories: provider.additionalDirectories } : {},
1795
1875
  ...run.model ? { model: run.model } : {},
1796
1876
  ...effort ? { effort } : {},
1797
- ...provider?.permissionMode ? { permissionMode: provider.permissionMode } : {},
1798
- ...provider?.permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {},
1877
+ ...run.mode === "plan" ? { permissionMode: "plan" } : params.request.options.fullAccess ? { permissionMode: "bypassPermissions" } : run.mode === "default" ? { permissionMode: "default" } : provider?.permissionMode ? { permissionMode: provider.permissionMode } : {},
1878
+ ...params.request.options.fullAccess || provider?.permissionMode === "bypassPermissions" ? { allowDangerouslySkipPermissions: true } : {},
1799
1879
  ...provider?.allowedTools?.length ? { allowedTools: provider.allowedTools } : {},
1800
1880
  ...run.resumeSessionId ? { resume: run.resumeSessionId } : {},
1801
1881
  // Fork-at-message: claude-agent-sdk natively supports slicing a
@@ -1941,8 +2021,14 @@ function readJsonBody(req) {
1941
2021
  });
1942
2022
  }
1943
2023
 
1944
- function autoApproveCanUseTool(_toolName, input) {
1945
- return { behavior: "allow", updatedInput: input };
2024
+ async function handlePermission(req, res, runId) {
2025
+ const run = liveRuns.get(runId);
2026
+ const body = await readJsonBody(req);
2027
+ const resolve = run?.permissions.get(body.requestId);
2028
+ if (!resolve) { res.writeHead(409); res.end("Request is no longer pending"); return; }
2029
+ run.permissions.delete(body.requestId);
2030
+ resolve(body.response);
2031
+ res.writeHead(204); res.end();
1946
2032
  }
1947
2033
 
1948
2034
  async function handleStart(req, res, runId) {
@@ -1982,6 +2068,25 @@ async function handleStart(req, res, runId) {
1982
2068
  const opts = { ...(options || {}) };
1983
2069
  const autoApprove = !!opts.autoApproveTools;
1984
2070
  delete opts.autoApproveTools;
2071
+ const interactiveQuestions = !!opts.interactiveQuestions;
2072
+ delete opts.interactiveQuestions;
2073
+ let planning = opts.permissionMode === "plan";
2074
+ const permissions = new Map();
2075
+ const clearPermissions = () => { for (const resolve of permissions.values()) resolve({ behavior: "deny", message: "Run ended", interrupt: true }); permissions.clear(); };
2076
+ const canUseTool = async (toolName, input, context) => {
2077
+ const isQuestion = toolName === "AskUserQuestion";
2078
+ const isPlan = toolName === "ExitPlanMode";
2079
+ if (context.signal.aborted) return { behavior: "deny", message: "Run cancelled", interrupt: true };
2080
+ if ((isQuestion || isPlan) && !interactiveQuestions) return { behavior: "deny", message: "No interactive user is available." };
2081
+ if (!isQuestion && !isPlan && planning && toolName !== "EnterPlanMode") return { behavior: "deny", message: "Finish planning before requesting write access." };
2082
+ if (!isQuestion && !isPlan && autoApprove) return { behavior: "allow", updatedInput: input };
2083
+ return new Promise((resolve) => {
2084
+ const abort = () => { permissions.delete(context.toolUseID); resolve({ behavior: "deny", message: "Run cancelled", interrupt: true }); };
2085
+ permissions.set(context.toolUseID, (response) => { context.signal.removeEventListener("abort", abort); if (isPlan && response.behavior === "allow") planning = false; resolve(response); });
2086
+ context.signal.addEventListener("abort", abort, { once: true });
2087
+ res.write(JSON.stringify({ _permission: { requestId: context.toolUseID, toolName, input, title: context.title } }) + "\\n");
2088
+ });
2089
+ };
1985
2090
  opts.pathToClaudeCodeExecutable = resolveClaudeBinary(
1986
2091
  opts.pathToClaudeCodeExecutable,
1987
2092
  );
@@ -2022,7 +2127,12 @@ async function handleStart(req, res, runId) {
2022
2127
  prompt: promptStream,
2023
2128
  options: {
2024
2129
  ...opts,
2025
- ...(autoApprove ? { canUseTool: autoApproveCanUseTool } : {}),
2130
+ canUseTool,
2131
+ hooks: { PreToolUse: [{ hooks: [async (input) => {
2132
+ planning = input.permission_mode === "plan";
2133
+ return interactiveQuestions && ["AskUserQuestion", "ExitPlanMode"].includes(input.tool_name)
2134
+ ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask" } } : {};
2135
+ }] }] },
2026
2136
  },
2027
2137
  });
2028
2138
  } catch (e) {
@@ -2032,11 +2142,12 @@ async function handleStart(req, res, runId) {
2032
2142
  return;
2033
2143
  }
2034
2144
 
2035
- liveRuns.set(runId, { query: queryHandle, prompt: promptStream });
2145
+ liveRuns.set(runId, { query: queryHandle, prompt: promptStream, permissions });
2036
2146
 
2037
2147
  // Client disconnected (e.g. host process killed) \u2192 tear down.
2038
2148
  req.on("close", () => {
2039
2149
  clearInterval(heartbeat);
2150
+ clearPermissions();
2040
2151
  if (!liveRuns.has(runId)) return;
2041
2152
  liveRuns.delete(runId);
2042
2153
  promptStream.end();
@@ -2052,6 +2163,7 @@ async function handleStart(req, res, runId) {
2052
2163
  res.write(JSON.stringify({ _error: String(e?.message ?? e) }) + "\\n");
2053
2164
  } finally {
2054
2165
  clearInterval(heartbeat);
2166
+ clearPermissions();
2055
2167
  liveRuns.delete(runId);
2056
2168
  promptStream.end();
2057
2169
  res.end();
@@ -2119,6 +2231,11 @@ const server = http.createServer((req, res) => {
2119
2231
  return;
2120
2232
  }
2121
2233
  const url = req.url ?? "";
2234
+ const permissionRoute = url.match(/^\\/runs\\/([^/]+)\\/permission$/);
2235
+ if (req.method === "POST" && permissionRoute) {
2236
+ handlePermission(req, res, decodeURIComponent(permissionRoute[1])).catch(() => { if (!res.headersSent) res.writeHead(400); res.end(); });
2237
+ return;
2238
+ }
2122
2239
  let m;
2123
2240
  if (req.method === "POST" && (m = url.match(/^\\/runs\\/([^/]+)\\/start$/))) {
2124
2241
  handleStart(req, res, decodeURIComponent(m[1]));
@@ -2355,15 +2472,11 @@ var ClaudeCodeAgentAdapter = class {
2355
2472
  ).catch(() => void 0);
2356
2473
  }
2357
2474
  async setup(request) {
2475
+ if (request.options.configuration === "native") return;
2358
2476
  await time(debugClaude, "claude-code setup()", async () => {
2359
2477
  const options = request.options;
2360
2478
  const provider = request.provider;
2361
2479
  const sandbox = options.sandbox;
2362
- if (!sandbox) {
2363
- throw new Error(
2364
- "claude-code requires a sandbox (the SDK transport runs as a daemon inside the sandbox)."
2365
- );
2366
- }
2367
2480
  const target = await createSetupTarget(provider, "shared-setup", options);
2368
2481
  const settingsPath = path8.join(target.layout.claudeDir, "settings.json");
2369
2482
  const mcpConfigPath = path8.join(
@@ -2384,6 +2497,7 @@ var ClaudeCodeAgentAdapter = class {
2384
2497
  const claudeSettings = { ...hookSettings, ...workflowSettings };
2385
2498
  const mcpConfigJson = buildClaudeMcpConfig(options.mcps) ?? JSON.stringify({ mcpServers: {} }, null, 2);
2386
2499
  const artifacts = [
2500
+ ...!sandbox ? [{ path: path8.join(target.layout.claudeDir, ".claude-plugin", "plugin.json"), content: JSON.stringify({ name: "agentbox", version: "1.0.0" }) }] : [],
2387
2501
  ...skillArtifacts,
2388
2502
  ...buildClaudeCommandArtifacts(options.commands, target.layout),
2389
2503
  ...buildClaudeSubagentArtifacts(options.subAgents, target.layout),
@@ -2394,11 +2508,11 @@ var ClaudeCodeAgentAdapter = class {
2394
2508
  { path: mcpConfigPath, content: mcpConfigJson }
2395
2509
  ];
2396
2510
  const enableRtk = options.enableRtk === true;
2397
- const daemonInfo = {
2511
+ const daemonInfo = sandbox ? {
2398
2512
  port: DAEMON_PORT,
2399
2513
  healthPath: "/__version",
2400
2514
  expectedVersionMatch: DAEMON_PROTOCOL_VERSION
2401
- };
2515
+ } : void 0;
2402
2516
  const setupId = computeSetupId({
2403
2517
  artifacts,
2404
2518
  installCommands,
@@ -2416,7 +2530,7 @@ var ClaudeCodeAgentAdapter = class {
2416
2530
  "applyDifferentialSetup",
2417
2531
  () => applyDifferentialSetup(target, artifacts, installCommands)
2418
2532
  ),
2419
- ensureClaudeCodeDaemon(options, env)
2533
+ ...sandbox ? [ensureClaudeCodeDaemon(options, env)] : []
2420
2534
  ]);
2421
2535
  if (enableRtk) {
2422
2536
  await time(debugClaude, "activateRtk", () => activateRtk(target));
@@ -2428,11 +2542,7 @@ var ClaudeCodeAgentAdapter = class {
2428
2542
  const executeStartedAt = Date.now();
2429
2543
  debugClaude("execute() start runId=%s", request.runId);
2430
2544
  const sandbox = request.options.sandbox;
2431
- if (!sandbox) {
2432
- throw new Error(
2433
- "claude-code requires a sandbox (the SDK transport runs as a daemon inside the sandbox)."
2434
- );
2435
- }
2545
+ if (!sandbox) return executeNativeClaude(request, sink);
2436
2546
  const claudeDir = claudeConfigDir(request.options);
2437
2547
  const settingsPath = path8.join(claudeDir, "settings.json");
2438
2548
  const mcpConfigPath = path8.join(claudeDir, "agentbox-mcp.json");
@@ -2489,7 +2599,8 @@ ${serialized}` : serialized;
2489
2599
  // already set `resume` for the resume path, so only stamp `sessionId` for
2490
2600
  // fresh runs.
2491
2601
  ...request.run.resumeSessionId ? {} : { sessionId: presetSessionId },
2492
- autoApproveTools
2602
+ autoApproveTools,
2603
+ interactiveQuestions: hasInteractiveQuestions(request.options)
2493
2604
  }
2494
2605
  };
2495
2606
  const fetchAbort = new AbortController();
@@ -2535,9 +2646,9 @@ ${serialized}` : serialized;
2535
2646
  })
2536
2647
  );
2537
2648
  if (!response.ok || !response.body) {
2538
- const text = await response.text().catch(() => "");
2649
+ const text2 = await response.text().catch(() => "");
2539
2650
  throw new Error(
2540
- `claude-code daemon /start failed: ${response.status} ${text}`
2651
+ `claude-code daemon /start failed: ${response.status} ${text2}`
2541
2652
  );
2542
2653
  }
2543
2654
  sink.setRaw({ baseUrl, runId: request.runId, claudeDir });
@@ -2554,199 +2665,22 @@ ${serialized}` : serialized;
2554
2665
  { messageId: initialUuid }
2555
2666
  )
2556
2667
  );
2557
- let accumulatedText = "";
2558
- let streamedThinkingChars = 0;
2559
- let pendingMessages = 1;
2560
- let firstStreamEventLogged = false;
2561
- let firstTextDeltaLogged = false;
2562
- let lastTerminalReason;
2563
- let lastIsError = false;
2564
- const rawPayloads = [];
2565
- try {
2668
+ const permissionMessages = async function* () {
2566
2669
  for await (const item of parseNdjsonStream(response.body)) {
2567
- if (item && typeof item === "object") {
2568
- const ctrl = item;
2569
- if ("_error" in ctrl) {
2570
- throw new Error(
2571
- String(item._error ?? "daemon error")
2572
- );
2573
- }
2574
- if ("_notice" in ctrl) {
2575
- debugClaude("daemon notice: %o", ctrl);
2576
- sink.emitRaw(
2577
- toRawEvent(
2578
- request.runId,
2579
- ctrl,
2580
- `daemon.${String(ctrl._notice ?? "notice")}`
2581
- )
2582
- );
2583
- continue;
2584
- }
2585
- }
2586
- const message = item;
2587
- rawPayloads.push(message);
2588
- sink.emitRaw(toRawEvent(request.runId, message, message.type));
2589
- if (message.type === "system") {
2590
- const sub = message.subtype;
2591
- if (sub === "init") {
2592
- const sys = message;
2593
- if (sys.session_id) {
2594
- debugClaude(
2595
- "\u2605 session.init session_id=%s (%dms)",
2596
- sys.session_id.slice(0, 8),
2597
- Date.now() - executeStartedAt
2598
- );
2599
- }
2600
- } else if (sub === "hook_started") {
2601
- const h = message;
2602
- debugClaude(
2603
- "hook.started name=%s event=%s hook_id=%s",
2604
- h.hook_name,
2605
- h.hook_event,
2606
- h.hook_id
2607
- );
2608
- } else if (sub === "hook_response") {
2609
- const h = message;
2610
- const stderr = h.stderr && h.stderr.length > 0 ? h.stderr.replace(/\s+$/, "") : void 0;
2611
- debugClaude(
2612
- "hook.response name=%s exit=%s outcome=%s%s",
2613
- h.hook_name,
2614
- h.exit_code,
2615
- h.outcome,
2616
- stderr ? ` stderr=${JSON.stringify(stderr).slice(0, 200)}` : ""
2617
- );
2618
- }
2619
- continue;
2620
- }
2621
- if (message.type === "stream_event") {
2622
- if (!firstStreamEventLogged) {
2623
- firstStreamEventLogged = true;
2624
- debugClaude(
2625
- "\u2605 first stream_event (%dms since execute start)",
2626
- Date.now() - executeStartedAt
2627
- );
2628
- }
2629
- const partial = message;
2630
- if (partial.parent_tool_use_id) continue;
2631
- const streamType = partial.event?.type;
2632
- if (streamType === "message_start") {
2633
- accumulatedText = "";
2634
- streamedThinkingChars = 0;
2635
- }
2636
- const { text, thinking } = extractStreamDeltas(partial);
2637
- if (thinking) {
2638
- streamedThinkingChars += thinking.length;
2639
- sink.emitEvent(
2640
- createNormalizedEvent(
2641
- "reasoning.delta",
2642
- { provider: request.provider, runId: request.runId },
2643
- { delta: thinking }
2644
- )
2645
- );
2646
- }
2647
- if (text) {
2648
- if (!firstTextDeltaLogged) {
2649
- firstTextDeltaLogged = true;
2650
- debugClaude(
2651
- "\u2605 first text delta (%dms since execute start)",
2652
- Date.now() - executeStartedAt
2653
- );
2654
- }
2655
- accumulatedText += text;
2656
- sink.emitEvent(
2657
- createNormalizedEvent(
2658
- "text.delta",
2659
- { provider: request.provider, runId: request.runId },
2660
- { delta: text }
2661
- )
2662
- );
2663
- }
2670
+ const control = item;
2671
+ if (!control._permission) {
2672
+ yield item;
2664
2673
  continue;
2665
2674
  }
2666
- if (message.type === "assistant") {
2667
- const asst = message;
2668
- if (asst.parent_tool_use_id) continue;
2669
- const thinking = extractAssistantThinking(asst);
2670
- if (thinking && streamedThinkingChars === 0) {
2671
- sink.emitEvent(
2672
- createNormalizedEvent(
2673
- "reasoning.delta",
2674
- { provider: request.provider, runId: request.runId },
2675
- { delta: thinking }
2676
- )
2677
- );
2678
- }
2679
- const text = extractAssistantText(asst);
2680
- sink.emitEvent(
2681
- createNormalizedEvent(
2682
- "message.completed",
2683
- { provider: request.provider, runId: request.runId },
2684
- {
2685
- text,
2686
- ...asst.uuid ? { messageId: String(asst.uuid) } : {}
2687
- }
2688
- )
2689
- );
2690
- continue;
2691
- }
2692
- if (message.type === "result") {
2693
- const result = message;
2694
- lastTerminalReason = result.terminal_reason;
2695
- lastIsError = result.is_error;
2696
- const resultText = result.subtype === "success" ? result.result : accumulatedText;
2697
- if (resultText && resultText !== accumulatedText) {
2698
- accumulatedText = resultText;
2699
- }
2700
- pendingMessages--;
2701
- if (pendingMessages <= 0) break;
2702
- continue;
2703
- }
2704
- }
2705
- const finalText = accumulatedText;
2706
- const isCancelled = lastTerminalReason === "aborted_streaming" || lastTerminalReason === "aborted_tools";
2707
- const isError = !isCancelled && lastIsError;
2708
- if (isCancelled) {
2709
- debugClaude(
2710
- "\u2605 run.cancelled (%dms since execute start) reason=%s",
2711
- Date.now() - executeStartedAt,
2712
- lastTerminalReason
2713
- );
2714
- sink.cancel({
2715
- text: finalText,
2716
- costData: extractClaudeCostData(rawPayloads)
2717
- });
2718
- } else if (isError) {
2719
- debugClaude(
2720
- "\u2605 run.error (%dms since execute start) reason=%s",
2721
- Date.now() - executeStartedAt,
2722
- lastTerminalReason
2723
- );
2724
- sink.fail(
2725
- new Error(
2726
- finalText || `claude-code run failed (terminal_reason: ${lastTerminalReason})`
2727
- )
2728
- );
2729
- } else {
2730
- debugClaude(
2731
- "\u2605 run.completed (%dms since execute start) chars=%d",
2732
- Date.now() - executeStartedAt,
2733
- finalText.length
2734
- );
2735
- sink.emitEvent(
2736
- createNormalizedEvent(
2737
- "run.completed",
2738
- { provider: request.provider, runId: request.runId },
2739
- { text: finalText }
2740
- )
2741
- );
2742
- sink.complete({
2743
- text: finalText,
2744
- costData: extractClaudeCostData(rawPayloads)
2745
- });
2675
+ const ask = control._permission;
2676
+ const isQuestion = ask.toolName === "AskUserQuestion";
2677
+ const isPlan = ask.toolName === "ExitPlanMode";
2678
+ const answer = await sink.requestPermission({ type: "permission.requested", provider: request.provider, runId: request.runId, timestamp: (/* @__PURE__ */ new Date()).toISOString(), requestId: ask.requestId, kind: isQuestion ? "question" : isPlan ? "plan" : "tool", toolName: ask.toolName, title: isQuestion ? "Your input is needed" : isPlan ? "Review the plan" : ask.title ?? `Allow ${ask.toolName}?`, input: ask.input, ...isQuestion ? { questions: normalizeUserQuestions("claude-code", ask.input) } : {} });
2679
+ const reply = await fetch(`${baseUrl}/runs/${encodeURIComponent(request.runId)}/permission`, { method: "POST", headers: { "content-type": "application/json", ...authHeaders }, signal: fetchAbort.signal, body: JSON.stringify({ requestId: ask.requestId, response: answer.decision === "allow" ? { behavior: "allow", updatedInput: isQuestion ? { ...ask.input, answers: questionReply("claude-code", ask.input, answer.answers ?? []) } : ask.input } : { behavior: "deny", message: "The user declined this request." } }) });
2680
+ if (!reply.ok) throw new Error(`Claude permission response failed: ${reply.status}`);
2746
2681
  }
2747
- } finally {
2748
- fetchAbort.abort();
2749
- }
2682
+ };
2683
+ await consumeClaudeMessages(request, sink, permissionMessages(), executeStartedAt, cleanup);
2750
2684
  return async () => void 0;
2751
2685
  }
2752
2686
  /**
@@ -2807,6 +2741,319 @@ ${serialized}` : serialized;
2807
2741
  }
2808
2742
  }
2809
2743
  };
2744
+ async function consumeClaudeMessages(request, sink, messages, executeStartedAt, cleanup, wasCancelled = () => false) {
2745
+ let accumulatedText = "";
2746
+ let streamedThinkingChars = 0;
2747
+ let pendingMessages = 1;
2748
+ let sawResult = false;
2749
+ let firstStreamEventLogged = false;
2750
+ let firstTextDeltaLogged = false;
2751
+ let lastTerminalReason;
2752
+ let lastIsError = false;
2753
+ const rawPayloads = [];
2754
+ try {
2755
+ for await (const item of messages) {
2756
+ if (item && typeof item === "object") {
2757
+ const ctrl = item;
2758
+ if ("_error" in ctrl) {
2759
+ throw new Error(
2760
+ String(item._error ?? "daemon error")
2761
+ );
2762
+ }
2763
+ if ("_notice" in ctrl) {
2764
+ debugClaude("daemon notice: %o", ctrl);
2765
+ sink.emitRaw(
2766
+ toRawEvent(
2767
+ request.runId,
2768
+ ctrl,
2769
+ `daemon.${String(ctrl._notice ?? "notice")}`
2770
+ )
2771
+ );
2772
+ continue;
2773
+ }
2774
+ }
2775
+ const message = item;
2776
+ rawPayloads.push(message);
2777
+ sink.emitRaw(toRawEvent(request.runId, message, message.type));
2778
+ if (message.type === "system") {
2779
+ const sub = message.subtype;
2780
+ if (sub === "init") {
2781
+ const sys = message;
2782
+ if (request.run.goal && !sys.slash_commands.some((command) => command.replace(/^\//, "") === "goal")) {
2783
+ await cleanup();
2784
+ throw new Error("This Claude Code installation does not expose the native /goal command.");
2785
+ }
2786
+ if (sys.session_id) {
2787
+ debugClaude(
2788
+ "\u2605 session.init session_id=%s (%dms)",
2789
+ sys.session_id.slice(0, 8),
2790
+ Date.now() - executeStartedAt
2791
+ );
2792
+ }
2793
+ } else if (sub === "hook_started") {
2794
+ const h = message;
2795
+ debugClaude(
2796
+ "hook.started name=%s event=%s hook_id=%s",
2797
+ h.hook_name,
2798
+ h.hook_event,
2799
+ h.hook_id
2800
+ );
2801
+ } else if (sub === "hook_response") {
2802
+ const h = message;
2803
+ const stderr = h.stderr && h.stderr.length > 0 ? h.stderr.replace(/\s+$/, "") : void 0;
2804
+ debugClaude(
2805
+ "hook.response name=%s exit=%s outcome=%s%s",
2806
+ h.hook_name,
2807
+ h.exit_code,
2808
+ h.outcome,
2809
+ stderr ? ` stderr=${JSON.stringify(stderr).slice(0, 200)}` : ""
2810
+ );
2811
+ }
2812
+ continue;
2813
+ }
2814
+ if (message.type === "stream_event") {
2815
+ if (!firstStreamEventLogged) {
2816
+ firstStreamEventLogged = true;
2817
+ debugClaude(
2818
+ "\u2605 first stream_event (%dms since execute start)",
2819
+ Date.now() - executeStartedAt
2820
+ );
2821
+ }
2822
+ const partial = message;
2823
+ if (partial.parent_tool_use_id) continue;
2824
+ const streamType = partial.event?.type;
2825
+ if (streamType === "message_start") {
2826
+ accumulatedText = "";
2827
+ streamedThinkingChars = 0;
2828
+ }
2829
+ const { text: text2, thinking } = extractStreamDeltas(partial);
2830
+ if (thinking) {
2831
+ streamedThinkingChars += thinking.length;
2832
+ sink.emitEvent(
2833
+ createNormalizedEvent(
2834
+ "reasoning.delta",
2835
+ { provider: request.provider, runId: request.runId },
2836
+ { delta: thinking }
2837
+ )
2838
+ );
2839
+ }
2840
+ if (text2) {
2841
+ if (!firstTextDeltaLogged) {
2842
+ firstTextDeltaLogged = true;
2843
+ debugClaude(
2844
+ "\u2605 first text delta (%dms since execute start)",
2845
+ Date.now() - executeStartedAt
2846
+ );
2847
+ }
2848
+ accumulatedText += text2;
2849
+ sink.emitEvent(
2850
+ createNormalizedEvent(
2851
+ "text.delta",
2852
+ { provider: request.provider, runId: request.runId },
2853
+ { delta: text2 }
2854
+ )
2855
+ );
2856
+ }
2857
+ continue;
2858
+ }
2859
+ if (message.type === "assistant") {
2860
+ const asst = message;
2861
+ if (asst.parent_tool_use_id) continue;
2862
+ const thinking = extractAssistantThinking(asst);
2863
+ if (thinking && streamedThinkingChars === 0) {
2864
+ sink.emitEvent(
2865
+ createNormalizedEvent(
2866
+ "reasoning.delta",
2867
+ { provider: request.provider, runId: request.runId },
2868
+ { delta: thinking }
2869
+ )
2870
+ );
2871
+ }
2872
+ const text2 = extractAssistantText(asst);
2873
+ sink.emitEvent(
2874
+ createNormalizedEvent(
2875
+ "message.completed",
2876
+ { provider: request.provider, runId: request.runId },
2877
+ {
2878
+ text: text2,
2879
+ ...asst.uuid ? { messageId: String(asst.uuid) } : {}
2880
+ }
2881
+ )
2882
+ );
2883
+ continue;
2884
+ }
2885
+ if (message.type === "result") {
2886
+ sawResult = true;
2887
+ const result = message;
2888
+ lastTerminalReason = result.terminal_reason;
2889
+ lastIsError = result.is_error;
2890
+ const resultText = result.subtype === "success" ? result.result : accumulatedText;
2891
+ if (resultText && resultText !== accumulatedText) {
2892
+ accumulatedText = resultText;
2893
+ }
2894
+ pendingMessages--;
2895
+ if (pendingMessages <= 0) break;
2896
+ continue;
2897
+ }
2898
+ }
2899
+ await cleanup();
2900
+ if (!sawResult && !wasCancelled()) throw new Error("Claude Code closed before reporting a result");
2901
+ const finalText = accumulatedText;
2902
+ const isCancelled = wasCancelled() || lastTerminalReason === "aborted_streaming" || lastTerminalReason === "aborted_tools";
2903
+ const isError = !isCancelled && lastIsError;
2904
+ if (isCancelled) {
2905
+ debugClaude(
2906
+ "\u2605 run.cancelled (%dms since execute start) reason=%s",
2907
+ Date.now() - executeStartedAt,
2908
+ lastTerminalReason
2909
+ );
2910
+ sink.cancel({
2911
+ text: finalText,
2912
+ costData: extractClaudeCostData(rawPayloads)
2913
+ });
2914
+ } else if (isError) {
2915
+ debugClaude(
2916
+ "\u2605 run.error (%dms since execute start) reason=%s",
2917
+ Date.now() - executeStartedAt,
2918
+ lastTerminalReason
2919
+ );
2920
+ sink.fail(
2921
+ new Error(
2922
+ finalText || `claude-code run failed (terminal_reason: ${lastTerminalReason})`
2923
+ )
2924
+ );
2925
+ } else {
2926
+ debugClaude(
2927
+ "\u2605 run.completed (%dms since execute start) chars=%d",
2928
+ Date.now() - executeStartedAt,
2929
+ finalText.length
2930
+ );
2931
+ sink.emitEvent(
2932
+ createNormalizedEvent(
2933
+ "run.completed",
2934
+ { provider: request.provider, runId: request.runId },
2935
+ { text: finalText }
2936
+ )
2937
+ );
2938
+ sink.complete({
2939
+ text: finalText,
2940
+ costData: extractClaudeCostData(rawPayloads)
2941
+ });
2942
+ }
2943
+ } finally {
2944
+ await cleanup();
2945
+ }
2946
+ }
2947
+ async function executeNativeClaude(request, sink) {
2948
+ const { query } = await import("@anthropic-ai/claude-agent-sdk");
2949
+ const claudeDir = claudeConfigDir(request.options);
2950
+ const input = await validateProviderUserInput(request.provider, request.run.input);
2951
+ const prompt = new AsyncQueue();
2952
+ const sessionId = request.run.resumeSessionId ?? randomUUID();
2953
+ const controller = new AbortController();
2954
+ let handle;
2955
+ let processHandle;
2956
+ let stopped;
2957
+ let cancelled = false;
2958
+ const stop = () => stopped ??= (async () => {
2959
+ prompt.finish();
2960
+ handle?.close();
2961
+ controller.abort();
2962
+ if (processHandle) await processHandle.kill();
2963
+ })();
2964
+ sink.setAbort(async () => {
2965
+ cancelled = true;
2966
+ await stop();
2967
+ });
2968
+ sink.setSessionId(sessionId);
2969
+ const messageId = randomUUID();
2970
+ prompt.push({ type: "user", uuid: messageId, message: { role: "user", content: mapToClaudeUserContent(input) }, parent_tool_use_id: null });
2971
+ const hostEnv = Object.fromEntries(Object.entries({ ...process.env, ...request.options.env }).filter((entry) => entry[1] !== void 0));
2972
+ if (request.options.customHeaders) {
2973
+ const headers = Object.entries(request.options.customHeaders).map(([name, value]) => `${name}: ${value}`).join("\n");
2974
+ hostEnv.ANTHROPIC_CUSTOM_HEADERS = [hostEnv.ANTHROPIC_CUSTOM_HEADERS, headers].filter(Boolean).join("\n");
2975
+ }
2976
+ const options = buildClaudeQueryOptions({
2977
+ request,
2978
+ ...request.options.configuration === "native" ? {} : {
2979
+ settingsPath: path8.join(claudeDir, "settings.json"),
2980
+ mcpConfigPath: path8.join(claudeDir, "agentbox-mcp.json")
2981
+ },
2982
+ // Auth is deliberately CLI-owned. Never copy the user's credential files
2983
+ // into the generated configuration directory or a task artifact.
2984
+ env: hostEnv
2985
+ });
2986
+ const autoApprove = shouldAutoApproveClaudeTools(request.options);
2987
+ const interactiveQuestions = hasInteractiveQuestions(request.options);
2988
+ let planning = options.permissionMode === "plan";
2989
+ try {
2990
+ handle = query({ prompt, options: {
2991
+ ...options,
2992
+ // Use the SDK-matched CLI by default; an installed CLI is an explicit override.
2993
+ pathToClaudeCodeExecutable: request.options.provider?.binary,
2994
+ abortController: controller,
2995
+ // `sessionId` is rejected alongside `resume` unless `forkSession` is
2996
+ // set, where it names the forked session. Stamping it on forks keeps
2997
+ // the pre-minted id reported via `sink.setSessionId` truthful, so a
2998
+ // later run can resume the fork.
2999
+ ...request.run.resumeSessionId ? {} : { sessionId },
3000
+ ...request.options.configuration === "native" ? {} : { plugins: [{ type: "local", path: claudeDir }] },
3001
+ spawnClaudeCodeProcess(spawnOptions) {
3002
+ if (cancelled || controller.signal.aborted) throw new Error("Local run was cancelled before startup");
3003
+ processHandle = spawnCommand({ ...spawnOptions, processGroup: request.options.processGroup !== "inherited" });
3004
+ return processHandle.child;
3005
+ },
3006
+ hooks: { ...options.hooks, PreToolUse: [...options.hooks?.PreToolUse ?? [], { hooks: [async (input2) => {
3007
+ if (input2.hook_event_name !== "PreToolUse") return {};
3008
+ planning = input2.permission_mode === "plan";
3009
+ return interactiveQuestions && ["AskUserQuestion", "ExitPlanMode"].includes(input2.tool_name) ? { hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask" } } : {};
3010
+ }] }] },
3011
+ async canUseTool(toolName, input2, context) {
3012
+ if (cancelled || context.signal.aborted) return { behavior: "deny", message: "Run cancelled", interrupt: true };
3013
+ const isQuestion = toolName === "AskUserQuestion";
3014
+ const isPlan = toolName === "ExitPlanMode";
3015
+ if ((isQuestion || isPlan) && !interactiveQuestions) return { behavior: "deny", message: "No interactive user is available." };
3016
+ if (!isQuestion && !isPlan && planning && toolName !== "EnterPlanMode") return { behavior: "deny", message: "Finish planning before requesting write access." };
3017
+ if (!isQuestion && !isPlan && autoApprove) return { behavior: "allow", updatedInput: input2 };
3018
+ try {
3019
+ const response = await sink.requestPermission({
3020
+ type: "permission.requested",
3021
+ provider: request.provider,
3022
+ runId: request.runId,
3023
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3024
+ requestId: context.toolUseID,
3025
+ kind: isQuestion ? "question" : isPlan ? "plan" : "tool",
3026
+ toolName,
3027
+ ...isQuestion ? { questions: normalizeUserQuestions("claude-code", input2) } : {},
3028
+ title: isQuestion ? "Your input is needed" : isPlan ? "Review the plan" : context.title ?? `Allow ${toolName}?`,
3029
+ message: context.description ?? context.decisionReason,
3030
+ input: input2,
3031
+ canRemember: false
3032
+ });
3033
+ if (isPlan && response.decision === "allow") planning = false;
3034
+ if (!cancelled && !context.signal.aborted && response.decision === "allow") return {
3035
+ behavior: "allow",
3036
+ updatedInput: isQuestion ? { ...input2, answers: questionReply("claude-code", input2, response.answers ?? []) } : input2
3037
+ };
3038
+ return { behavior: "deny", message: "The user denied this action" };
3039
+ } catch {
3040
+ return { behavior: "deny", message: "Run cancelled", interrupt: true };
3041
+ }
3042
+ }
3043
+ } });
3044
+ sink.setRaw({ query: handle, claudeDir, runId: request.runId });
3045
+ sink.emitEvent(createNormalizedEvent("run.started", { provider: request.provider, runId: request.runId }));
3046
+ sink.emitEvent(createNormalizedEvent("message.started", { provider: request.provider, runId: request.runId }, { messageId }));
3047
+ await consumeClaudeMessages(request, sink, handle, Date.now(), stop, () => cancelled);
3048
+ } catch (error) {
3049
+ await stop();
3050
+ if (cancelled) sink.cancel();
3051
+ else throw error;
3052
+ } finally {
3053
+ await stop();
3054
+ }
3055
+ return stop;
3056
+ }
2810
3057
 
2811
3058
  // src/agents/providers/codex.ts
2812
3059
  import crypto2 from "crypto";
@@ -2820,16 +3067,16 @@ async function fetchJson(url, init) {
2820
3067
  if (!response.ok) {
2821
3068
  throw new Error(`Request to ${url} failed with ${response.status}.`);
2822
3069
  }
2823
- const text = await response.text();
2824
- if (text.length === 0) {
3070
+ const text2 = await response.text();
3071
+ if (text2.length === 0) {
2825
3072
  throw new Error(
2826
3073
  `Request to ${url} returned status ${response.status} with an empty body.`
2827
3074
  );
2828
3075
  }
2829
3076
  try {
2830
- return JSON.parse(text);
3077
+ return JSON.parse(text2);
2831
3078
  } catch (error) {
2832
- const preview = text.length > 200 ? `${text.slice(0, 200)}\u2026` : text;
3079
+ const preview = text2.length > 200 ? `${text2.slice(0, 200)}\u2026` : text2;
2833
3080
  const cause = error instanceof Error ? error.message : String(error);
2834
3081
  throw new Error(
2835
3082
  `Could not parse JSON response from ${url} (status ${response.status}): ${cause}. Body: ${preview}`
@@ -3030,7 +3277,7 @@ var JsonRpcLineClient = class {
3030
3277
  // src/agents/providers/codex.ts
3031
3278
  function codexConfigDir(options) {
3032
3279
  return path9.join(
3033
- agentboxRoot(AgentProvider.Codex, Boolean(options.sandbox)),
3280
+ agentboxRoot(AgentProvider.Codex, Boolean(options.sandbox), options.stateDirectory),
3034
3281
  ".codex"
3035
3282
  );
3036
3283
  }
@@ -3097,20 +3344,20 @@ function compactEnv(values) {
3097
3344
  );
3098
3345
  }
3099
3346
  function buildCodexSandboxMode(options) {
3100
- return options.sandbox ? "workspace-write" : "read-only";
3347
+ return options.fullAccess ? "danger-full-access" : options.provider?.sandboxMode ?? (options.configuration === "native" ? void 0 : options.sandbox ? "workspace-write" : "read-only");
3101
3348
  }
3102
3349
  function buildThreadParams(cwd, options, request) {
3103
3350
  return {
3104
3351
  cwd,
3105
3352
  model: request.run.model ?? null,
3106
- approvalPolicy: isInteractiveApproval(options) ? "untrusted" : "never",
3353
+ ...options.configuration === "native" && !options.fullAccess ? {} : { approvalPolicy: !options.fullAccess && isInteractiveApproval(options) ? "untrusted" : "never" },
3107
3354
  sandbox: buildCodexSandboxMode(options),
3108
3355
  serviceName: "agentbox",
3109
3356
  // Persist the rollout on disk so follow-up runs can call `thread/resume`.
3110
3357
  // `ephemeral: true` threads have no rollout file and resume fails with
3111
3358
  // "no rollout found for thread id ...".
3112
3359
  experimentalRawEvents: true,
3113
- developerInstructions: request.run.systemPrompt ?? null
3360
+ ...request.run.systemPrompt ? { developerInstructions: request.run.systemPrompt } : options.configuration === "native" ? {} : { developerInstructions: null }
3114
3361
  };
3115
3362
  }
3116
3363
  function buildResumeParams(cwd, options, request) {
@@ -3118,24 +3365,38 @@ function buildResumeParams(cwd, options, request) {
3118
3365
  threadId: request.run.resumeSessionId,
3119
3366
  cwd,
3120
3367
  model: request.run.model ?? null,
3121
- approvalPolicy: isInteractiveApproval(options) ? "untrusted" : "never",
3368
+ ...options.configuration === "native" && !options.fullAccess ? {} : { approvalPolicy: !options.fullAccess && isInteractiveApproval(options) ? "untrusted" : "never" },
3122
3369
  sandbox: buildCodexSandboxMode(options),
3123
- developerInstructions: request.run.systemPrompt ?? null
3370
+ ...request.run.systemPrompt ? { developerInstructions: request.run.systemPrompt } : options.configuration === "native" ? {} : { developerInstructions: null },
3371
+ // We only need the thread id back; we never read `thread.turns`.
3372
+ // Without this Codex hydrates the full history into the response and
3373
+ // emits a `deprecationNotice` ("Full-history hydration is deprecated
3374
+ // for paginated threads").
3375
+ excludeTurns: true
3124
3376
  };
3125
3377
  }
3126
3378
  function buildForkParams(cwd, options, request) {
3127
3379
  return {
3128
3380
  threadId: request.run.forkSessionId,
3381
+ lastTurnId: request.run.forkAtMessageId ?? null,
3129
3382
  cwd,
3130
3383
  model: request.run.model ?? null,
3131
- approvalPolicy: isInteractiveApproval(options) ? "untrusted" : "never",
3384
+ ...options.configuration === "native" && !options.fullAccess ? {} : { approvalPolicy: !options.fullAccess && isInteractiveApproval(options) ? "untrusted" : "never" },
3132
3385
  sandbox: buildCodexSandboxMode(options),
3133
- developerInstructions: request.run.systemPrompt ?? null
3386
+ ...request.run.systemPrompt ? { developerInstructions: request.run.systemPrompt } : options.configuration === "native" ? {} : { developerInstructions: null },
3387
+ excludeTurns: true
3134
3388
  };
3135
3389
  }
3136
3390
  function buildTurnSandboxPolicy(options) {
3391
+ if (options.fullAccess || options.provider?.sandboxMode === "danger-full-access") return { type: "dangerFullAccess" };
3137
3392
  if (!options.sandbox) {
3138
- return void 0;
3393
+ if (buildCodexSandboxMode(options) === void 0) return void 0;
3394
+ if (buildCodexSandboxMode(options) === "read-only") return void 0;
3395
+ return {
3396
+ type: "workspaceWrite",
3397
+ networkAccess: options.provider?.networkAccess ?? false,
3398
+ ...options.provider?.writableRoots?.length ? { writableRoots: options.provider.writableRoots } : {}
3399
+ };
3139
3400
  }
3140
3401
  if (options.sandbox.provider === SandboxProvider.LocalDocker) {
3141
3402
  return {
@@ -3154,10 +3415,16 @@ function buildCodexTurnStartParams(params) {
3154
3415
  return {
3155
3416
  threadId,
3156
3417
  input: inputItems,
3157
- approvalPolicy: isInteractiveApproval(request.options) ? "untrusted" : "never",
3418
+ ...request.options.configuration === "native" && !request.options.fullAccess ? {} : {
3419
+ approvalPolicy: !request.options.fullAccess && isInteractiveApproval(request.options) ? "untrusted" : "never"
3420
+ },
3158
3421
  ...sandboxPolicy ? { sandboxPolicy } : {},
3159
3422
  model: request.run.model ?? null,
3160
3423
  effort: request.run.reasoning ?? null,
3424
+ ...request.run.mode ? { collaborationMode: {
3425
+ mode: request.run.mode,
3426
+ settings: { model: request.run.model, reasoning_effort: request.run.reasoning ?? null, developer_instructions: null }
3427
+ } } : {},
3161
3428
  outputSchema: null
3162
3429
  };
3163
3430
  }
@@ -3182,7 +3449,7 @@ function buildCodexCommandArgs(binary, args, options) {
3182
3449
  overrides.push(["supports_websockets", "false"]);
3183
3450
  }
3184
3451
  const overrideArgs = overrides.flatMap(([k, v]) => ["-c", `${k}=${v}`]);
3185
- return ["-u", "XDG_CONFIG_HOME", binary, ...overrideArgs, ...args];
3452
+ return [...options?.configuration === "native" ? [] : ["-u", "XDG_CONFIG_HOME"], binary, ...overrideArgs, ...args];
3186
3453
  }
3187
3454
  function toNormalizedCodexEvents(runId, notification) {
3188
3455
  const base = {
@@ -3254,8 +3521,8 @@ function toNormalizedCodexEvents(runId, notification) {
3254
3521
  }
3255
3522
  if (notification.method === "turn/completed") {
3256
3523
  const turn = notification.params?.turn;
3257
- const text = typeof turn?.lastAgentMessage === "string" ? turn.lastAgentMessage : void 0;
3258
- return [createNormalizedEvent("run.completed", base, { text })];
3524
+ const text2 = typeof turn?.lastAgentMessage === "string" ? turn.lastAgentMessage : void 0;
3525
+ return [createNormalizedEvent("run.completed", base, { text: text2 })];
3259
3526
  }
3260
3527
  if (notification.method === "error") {
3261
3528
  const error = notification.params?.error;
@@ -3267,7 +3534,7 @@ function toNormalizedCodexEvents(runId, notification) {
3267
3534
  }
3268
3535
  return [];
3269
3536
  }
3270
- function createCodexPermissionEvent(request, notification) {
3537
+ function createCodexPermissionEvent(request, notification, fileChanges) {
3271
3538
  const raw = toRawEvent2(request.runId, notification, notification.method);
3272
3539
  const params = notification.params;
3273
3540
  const requestId = notification.id;
@@ -3310,7 +3577,7 @@ function createCodexPermissionEvent(request, notification) {
3310
3577
  kind: "file-change",
3311
3578
  title: "Approve file changes",
3312
3579
  message: typeof params.reason === "string" ? params.reason : "Codex wants to modify files.",
3313
- input: params,
3580
+ input: fileChanges ? { ...params, changes: fileChanges } : params,
3314
3581
  canRemember: availableDecisions.includes("acceptForSession")
3315
3582
  }
3316
3583
  );
@@ -3364,7 +3631,7 @@ async function materializeCodexImage(options, part, index) {
3364
3631
  if (data.length === 0) {
3365
3632
  throw new Error("Cannot attach an empty image to Codex.");
3366
3633
  }
3367
- const root = agentboxRoot(AgentProvider.Codex, Boolean(options.sandbox));
3634
+ const root = agentboxRoot(AgentProvider.Codex, Boolean(options.sandbox), options.stateDirectory);
3368
3635
  const imagePath = path9.join(
3369
3636
  root,
3370
3637
  "inputs",
@@ -3511,6 +3778,7 @@ async function connectRemoteCodexAppServer(url, headers = {}) {
3511
3778
  }
3512
3779
  async function setupCodex(request) {
3513
3780
  const options = request.options;
3781
+ if (options.configuration === "native") return;
3514
3782
  const provider = request.provider;
3515
3783
  const hooks = assertHooksSupported(provider, options);
3516
3784
  assertCommandsSupported(provider, options.commands);
@@ -3708,10 +3976,10 @@ async function createRuntime(request, inputParts) {
3708
3976
  const codexDir = codexConfigDir(options);
3709
3977
  const env = compactEnv({
3710
3978
  ...options.env ?? {},
3711
- CODEX_HOME: codexDir,
3979
+ ...options.configuration === "native" ? {} : { CODEX_HOME: codexDir },
3712
3980
  ...options.provider?.env ?? {}
3713
3981
  });
3714
- const runtimeCwd = path9.dirname(codexDir);
3982
+ const runtimeCwd = options.configuration === "native" ? options.cwd : path9.dirname(codexDir);
3715
3983
  const inputItems = await buildCodexInputItems(options, inputParts);
3716
3984
  const usesRemoteWebSocket = options.sandbox && options.sandbox.provider !== SandboxProvider.LocalDocker;
3717
3985
  if (usesRemoteWebSocket && options.sandbox) {
@@ -3781,6 +4049,7 @@ async function createRuntime(request, inputParts) {
3781
4049
  };
3782
4050
  }
3783
4051
  const processHandle = spawnCommand({
4052
+ processGroup: options.processGroup !== "inherited",
3784
4053
  command: "env",
3785
4054
  args: codexArgs,
3786
4055
  cwd: runtimeCwd,
@@ -3789,6 +4058,7 @@ async function createRuntime(request, inputParts) {
3789
4058
  ...env
3790
4059
  }
3791
4060
  });
4061
+ processHandle.child.stderr.resume();
3792
4062
  return {
3793
4063
  source: linesFromNodeStream(processHandle.child.stdout),
3794
4064
  writeLine: async (line) => {
@@ -3909,10 +4179,10 @@ var CodexAgentAdapter = class {
3909
4179
  throw new Error("Cannot send message before thread is started.");
3910
4180
  }
3911
4181
  const parts = normalizeUserInput(content);
3912
- const text = parts.filter((p) => p.type === "text").map((p) => p.text).join("");
4182
+ const text2 = parts.filter((p) => p.type === "text").map((p) => p.text).join("");
3913
4183
  const inputItems = [];
3914
- if (text.trim().length > 0) {
3915
- inputItems.push({ type: "text", text, text_elements: [] });
4184
+ if (text2.trim().length > 0) {
4185
+ inputItems.push({ type: "text", text: text2, text_elements: [] });
3916
4186
  }
3917
4187
  const response = await client.request(
3918
4188
  "turn/start",
@@ -3928,6 +4198,8 @@ var CodexAgentAdapter = class {
3928
4198
  };
3929
4199
  sink.onMessage(sendTurn);
3930
4200
  const rawPayloads = [];
4201
+ const pendingFileChanges = /* @__PURE__ */ new Map();
4202
+ const fileItemKey = (params, itemId) => typeof params?.threadId === "string" && typeof params.turnId === "string" && typeof itemId === "string" ? `${params.threadId}:${params.turnId}:${itemId}` : void 0;
3931
4203
  let streamedText = "";
3932
4204
  const completion = new Promise((resolve, reject) => {
3933
4205
  void (async () => {
@@ -3944,15 +4216,39 @@ var CodexAgentAdapter = class {
3944
4216
  const raw = toRawEvent2(request.runId, message, message.method);
3945
4217
  rawPayloads.push(message);
3946
4218
  sink.emitRaw(raw);
3947
- if (message.method === "tool/requestUserInput" && message.id !== void 0) {
3948
- reject(
3949
- new Error(
3950
- "Codex tool/requestUserInput approvals are not yet supported by AgentBox."
3951
- )
3952
- );
3953
- return;
4219
+ const item = message.params?.item;
4220
+ const itemKey = fileItemKey(message.params, item?.id);
4221
+ if (itemKey && item?.type === "fileChange") {
4222
+ if (message.method === "item/completed") pendingFileChanges.delete(itemKey);
4223
+ else if (message.method === "item/started" && Array.isArray(item.changes)) {
4224
+ if (pendingFileChanges.size >= 128) pendingFileChanges.delete(pendingFileChanges.keys().next().value);
4225
+ pendingFileChanges.set(itemKey, item.changes);
4226
+ }
4227
+ }
4228
+ if ((message.method === "item/tool/requestUserInput" || message.method === "tool/requestUserInput") && message.id !== void 0) {
4229
+ const questions = normalizeUserQuestions("codex", message.params);
4230
+ const response = hasInteractiveQuestions(request.options) ? await sink.requestPermission(createNormalizedEvent("permission.requested", {
4231
+ provider: request.provider,
4232
+ runId: request.runId,
4233
+ raw
4234
+ }, {
4235
+ requestId: String(message.id),
4236
+ kind: "question",
4237
+ toolName: "request_user_input",
4238
+ title: "Your input is needed",
4239
+ input: message.params,
4240
+ questions,
4241
+ canRemember: false
4242
+ })) : void 0;
4243
+ await client.respond(message.id, { answers: response?.decision === "allow" ? questionReply("codex", message.params, response.answers ?? []) : {} });
4244
+ continue;
3954
4245
  }
3955
- const permissionEvent = createCodexPermissionEvent(request, message);
4246
+ const approvalKey = fileItemKey(message.params, message.params?.itemId);
4247
+ const permissionEvent = createCodexPermissionEvent(
4248
+ request,
4249
+ message,
4250
+ approvalKey ? pendingFileChanges.get(approvalKey) : void 0
4251
+ );
3956
4252
  if (permissionEvent && message.id !== void 0) {
3957
4253
  const response = interactiveApproval ? await sink.requestPermission(permissionEvent) : {
3958
4254
  requestId: permissionEvent.requestId,
@@ -3961,8 +4257,15 @@ var CodexAgentAdapter = class {
3961
4257
  await client.respond(message.id, {
3962
4258
  decision: toCodexApprovalDecision(message, response)
3963
4259
  });
4260
+ if (approvalKey) pendingFileChanges.delete(approvalKey);
3964
4261
  continue;
3965
4262
  }
4263
+ if (message.method === "item/completed") {
4264
+ const item2 = message.params?.item;
4265
+ if (item2?.type === "plan" && typeof item2.text === "string") {
4266
+ sink.emitEvent(createNormalizedEvent("plan.completed", { provider: request.provider, runId: request.runId }, { text: item2.text }));
4267
+ }
4268
+ }
3966
4269
  for (const event of toNormalizedCodexEvents(request.runId, message)) {
3967
4270
  sink.emitEvent(event);
3968
4271
  if (event.type === "text.delta") {
@@ -4042,30 +4345,12 @@ var CodexAgentAdapter = class {
4042
4345
  sink.emitRaw(
4043
4346
  toRawEvent2(request.runId, threadResponse, threadResultEventName)
4044
4347
  );
4045
- if (request.run.forkSessionId) {
4046
- const targetTurnId = request.run.forkAtMessageId;
4047
- const turns = threadResponse.thread.turns ?? [];
4048
- const targetIndex = turns.findIndex((turn) => turn.id === targetTurnId);
4049
- if (targetIndex < 0) {
4050
- throw new Error(
4051
- `Codex fork: turn id ${String(targetTurnId)} not found in source thread ${request.run.forkSessionId}.`
4052
- );
4053
- }
4054
- const numTurns = turns.length - 1 - targetIndex;
4055
- if (numTurns > 0) {
4056
- const rollbackResponse = await client.request(
4057
- "thread/rollback",
4058
- { threadId: rootThreadId, numTurns }
4059
- );
4060
- rawPayloads.push(rollbackResponse);
4061
- sink.emitRaw(
4062
- toRawEvent2(
4063
- request.runId,
4064
- rollbackResponse,
4065
- "thread/rollback:result"
4066
- )
4067
- );
4068
- }
4348
+ if (request.run.mode) {
4349
+ const modes = await client.request("collaborationMode/list", {});
4350
+ if (!modes.data.some((mode) => mode.mode === request.run.mode)) throw new Error("This Codex installation does not support the requested planning mode.");
4351
+ }
4352
+ if (request.run.goal) {
4353
+ await client.request("thread/goal/set", { threadId: threadResponse.thread.id, objective: request.run.goal, status: "active" });
4069
4354
  }
4070
4355
  await client.request(
4071
4356
  "turn/start",
@@ -4096,19 +4381,19 @@ var CodexAgentAdapter = class {
4096
4381
  sink.fail(completionError);
4097
4382
  }
4098
4383
  } else {
4099
- const { text, interrupted } = completionResult;
4384
+ const { text: text2, interrupted } = completionResult;
4100
4385
  if (abortInvoked || interrupted) {
4101
4386
  debugCodex(
4102
4387
  "\u2605 run.cancelled (%dms since execute start) interrupted=%s",
4103
4388
  Date.now() - executeStartedAt,
4104
4389
  interrupted
4105
4390
  );
4106
- sink.cancel({ text, costData: extractCodexCostData(rawPayloads) });
4391
+ sink.cancel({ text: text2, costData: extractCodexCostData(rawPayloads) });
4107
4392
  } else {
4108
4393
  debugCodex(
4109
4394
  "\u2605 run.completed (%dms since execute start) chars=%d",
4110
4395
  Date.now() - executeStartedAt,
4111
- text?.length ?? 0
4396
+ text2?.length ?? 0
4112
4397
  );
4113
4398
  sink.complete({ costData: extractCodexCostData(rawPayloads) });
4114
4399
  }
@@ -4170,14 +4455,14 @@ var CodexAgentAdapter = class {
4170
4455
  );
4171
4456
  }
4172
4457
  const parts = normalizeUserInput(content);
4173
- const text = joinTextParts(
4458
+ const text2 = joinTextParts(
4174
4459
  parts.filter(
4175
4460
  (part) => part.type === "text"
4176
4461
  )
4177
4462
  );
4178
4463
  const inputItems = [];
4179
- if (text.trim().length > 0) {
4180
- inputItems.push({ type: "text", text, text_elements: [] });
4464
+ if (text2.trim().length > 0) {
4465
+ inputItems.push({ type: "text", text: text2, text_elements: [] });
4181
4466
  }
4182
4467
  await withCodexAppServer(request, async (client) => {
4183
4468
  await client.request("turn/start", {
@@ -4193,10 +4478,10 @@ var CodexAgentAdapter = class {
4193
4478
  };
4194
4479
 
4195
4480
  // src/agents/providers/opencode.ts
4196
- import { createHash as createHash2 } from "crypto";
4481
+ import { createHash as createHash2, randomBytes, randomUUID as randomUUID2 } from "crypto";
4482
+ import os2 from "os";
4197
4483
  import path10 from "path";
4198
4484
  var SANDBOX_OPENCODE_PORT = 4096;
4199
- var LOCAL_OPENCODE_PORT = 4096;
4200
4485
  var SANDBOX_OPENCODE_READY_TIMEOUT_MS = 9e4;
4201
4486
  var LOCAL_OPENCODE_READY_TIMEOUT_MS = 2e4;
4202
4487
  var SHARED_OPENCODE_TARGET_ID = "shared-opencode-server";
@@ -4248,30 +4533,13 @@ function hashLlmApiKeys(env) {
4248
4533
  }
4249
4534
  return hasher.digest("hex");
4250
4535
  }
4251
- async function killLocalOpenCodeServer() {
4252
- await time(debugOpencode, "kill local opencode server", async () => {
4253
- const killer = spawnCommand({
4254
- command: "sh",
4255
- args: [
4256
- "-c",
4257
- `lsof -ti tcp:${LOCAL_OPENCODE_PORT} | xargs kill 2>/dev/null || true`
4258
- ]
4259
- });
4260
- await killer.wait().catch(() => void 0);
4261
- await waitFor(
4262
- async () => {
4263
- try {
4264
- const res = await fetch(
4265
- `http://127.0.0.1:${LOCAL_OPENCODE_PORT}/global/health`
4266
- );
4267
- return !res.ok;
4268
- } catch {
4269
- return true;
4270
- }
4271
- },
4272
- { timeoutMs: 5e3, intervalMs: 200 }
4273
- ).catch(() => void 0);
4274
- });
4536
+ var localOpenCodeServers = /* @__PURE__ */ new WeakMap();
4537
+ async function killLocalOpenCodeServer(options) {
4538
+ const pending = localOpenCodeServers.get(options);
4539
+ if (!pending) return;
4540
+ const server = await pending;
4541
+ await server.process.kill();
4542
+ localOpenCodeServers.delete(options);
4275
4543
  }
4276
4544
  async function killSandboxOpenCodeServer(sandbox, pidFilePath, cwd, port) {
4277
4545
  await time(debugOpencode, "kill sandbox opencode server", async () => {
@@ -4350,6 +4618,7 @@ function createOpenCodePermissionEvent(request, raw, payload) {
4350
4618
  },
4351
4619
  {
4352
4620
  requestId: String(properties.id ?? ""),
4621
+ toolName: permission,
4353
4622
  kind: permission === "bash" ? "bash" : permission === "edit" ? "edit" : permission === "external_directory" ? "file-change" : permission === "webfetch" ? "network" : permission === "task" ? "tool" : "unknown",
4354
4623
  title: `Approve ${permission} permission`,
4355
4624
  message: typeof properties.metadata === "object" && properties.metadata !== null ? JSON.stringify(properties.metadata) : `OpenCode requested ${permission} permission.`,
@@ -4371,6 +4640,7 @@ function buildOpenCodeConfig(options, interactiveApproval) {
4371
4640
  prompt: options.systemPrompt || FALLBACK_OPEN_CODE_AGENT_PROMPT,
4372
4641
  permission: buildOpenCodePermissionConfig(interactiveApproval),
4373
4642
  tools: {
4643
+ question: hasInteractiveQuestions(options),
4374
4644
  write: true,
4375
4645
  edit: true,
4376
4646
  bash: true,
@@ -4425,7 +4695,7 @@ async function ensureSandboxOpenCodeServer(request) {
4425
4695
  const port = SANDBOX_OPENCODE_PORT;
4426
4696
  const plugins = assertHooksSupported(request.provider, options);
4427
4697
  assertCommandsSupported(request.provider, options.commands);
4428
- const interactiveApproval = isInteractiveApproval(options);
4698
+ const interactiveApproval = !options.fullAccess && isInteractiveApproval(options);
4429
4699
  const target = await createSetupTarget(
4430
4700
  request.provider,
4431
4701
  SHARED_OPENCODE_TARGET_ID,
@@ -4493,7 +4763,8 @@ async function ensureSandboxOpenCodeServer(request) {
4493
4763
  const commonEnv = {
4494
4764
  OPENCODE_CONFIG: configPath,
4495
4765
  OPENCODE_CONFIG_DIR: target.layout.opencodeDir,
4496
- OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
4766
+ OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
4767
+ OPENCODE_ENABLE_QUESTION_TOOL: hasInteractiveQuestions(options) ? "true" : "false"
4497
4768
  };
4498
4769
  await applyDifferentialSetup(target, allArtifacts, installCommands);
4499
4770
  if (enableRtk) {
@@ -4622,85 +4893,91 @@ ${lastLog}` : "")
4622
4893
  await markSetupComplete(target, setupId);
4623
4894
  });
4624
4895
  }
4625
- async function ensureLocalOpenCodeServer(request) {
4626
- const options = request.options;
4627
- const plugins = assertHooksSupported(request.provider, options);
4628
- assertCommandsSupported(request.provider, options.commands);
4629
- const interactiveApproval = isInteractiveApproval(options);
4630
- const target = await createSetupTarget(
4631
- request.provider,
4632
- "shared-setup",
4633
- options
4634
- );
4635
- const { artifacts: skillArtifacts, installCommands } = await prepareSkillArtifacts(
4636
- request.provider,
4637
- options.skills,
4638
- target.layout
4639
- );
4640
- const pluginArtifacts = buildOpenCodePluginArtifacts(
4641
- plugins,
4642
- target.layout.opencodeDir
4643
- );
4644
- const configPath = path10.join(target.layout.opencodeDir, "agentbox.json");
4645
- const openCodeConfig = buildOpenCodeConfig(options, interactiveApproval);
4646
- const commonEnv = {
4647
- OPENCODE_CONFIG: configPath,
4648
- OPENCODE_CONFIG_DIR: target.layout.opencodeDir,
4649
- OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
4650
- };
4651
- const allArtifacts = [
4652
- ...skillArtifacts,
4653
- ...pluginArtifacts,
4654
- {
4655
- path: configPath,
4656
- content: JSON.stringify(openCodeConfig, null, 2)
4657
- }
4658
- ];
4659
- const daemonInfo = {
4660
- port: LOCAL_OPENCODE_PORT,
4661
- healthPath: "/global/health"
4896
+ async function startLocalOpenCodeServer(request) {
4897
+ const originalOptions = request.options;
4898
+ const options = {
4899
+ ...originalOptions,
4900
+ stateDirectory: path10.join(originalOptions.stateDirectory ?? path10.join(os2.tmpdir(), "agentbox-native"), "instances", randomUUID2())
4662
4901
  };
4663
- const setupId = computeSetupId({
4664
- artifacts: allArtifacts,
4665
- installCommands,
4666
- daemon: daemonInfo,
4667
- extras: [`apiKeys:${hashLlmApiKeys(options.env)}`]
4668
- });
4669
- if (await preflightSetup(target, setupId, daemonInfo)) {
4670
- debugOpencode("local opencode server up-to-date \u2014 reusing");
4671
- return;
4672
- }
4673
- if (await isLocalOpenCodeServerHealthy()) {
4674
- debugOpencode(
4675
- "local opencode server already running but setup drifted \u2014 reusing it without restart; call agent.killServer() to apply the new config"
4676
- );
4677
- return;
4902
+ let generatedEnv = {};
4903
+ if (options.configuration !== "native") {
4904
+ const plugins = assertHooksSupported(request.provider, options);
4905
+ assertCommandsSupported(request.provider, options.commands);
4906
+ const target = await createSetupTarget(request.provider, "shared-setup", options);
4907
+ const { artifacts: skillArtifacts, installCommands } = await prepareSkillArtifacts(request.provider, options.skills, target.layout);
4908
+ const configPath = path10.join(target.layout.opencodeDir, "agentbox.json");
4909
+ const artifacts = [
4910
+ ...skillArtifacts,
4911
+ ...buildOpenCodePluginArtifacts(plugins, target.layout.opencodeDir),
4912
+ { path: configPath, content: JSON.stringify(buildOpenCodeConfig(options, isInteractiveApproval(options)), null, 2) }
4913
+ ];
4914
+ await applyDifferentialSetup(target, artifacts, installCommands);
4915
+ generatedEnv = {
4916
+ OPENCODE_CONFIG: configPath,
4917
+ OPENCODE_CONFIG_DIR: target.layout.opencodeDir,
4918
+ OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
4919
+ OPENCODE_ENABLE_QUESTION_TOOL: hasInteractiveQuestions(options) ? "true" : "false"
4920
+ };
4678
4921
  }
4679
- debugOpencode("local opencode server absent \u2014 spawning");
4680
- await applyDifferentialSetup(target, allArtifacts, installCommands);
4681
- await killLocalOpenCodeServer();
4682
- spawnCommand({
4922
+ const port = await getAvailablePort();
4923
+ const password = randomBytes(32).toString("base64url");
4924
+ const baseUrl = `http://127.0.0.1:${port}`;
4925
+ const headers = { Authorization: `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}` };
4926
+ const processHandle = spawnCommand({
4683
4927
  command: options.provider?.binary ?? "opencode",
4684
- args: [
4685
- "serve",
4686
- "--hostname",
4687
- "127.0.0.1",
4688
- "--port",
4689
- String(LOCAL_OPENCODE_PORT),
4690
- ...options.provider?.args ?? []
4691
- ],
4928
+ args: ["serve", ...options.provider?.args ?? [], "--hostname", "127.0.0.1", "--port", String(port)],
4692
4929
  cwd: options.cwd,
4930
+ processGroup: options.processGroup !== "inherited",
4693
4931
  env: {
4694
4932
  ...process.env,
4695
- ...options.env ?? {},
4696
- ...commonEnv
4933
+ ...options.env,
4934
+ ...generatedEnv,
4935
+ ...options.fullAccess ? { OPENCODE_PERMISSION: JSON.stringify({ "*": "allow", question: "ask" }) } : {},
4936
+ ...options.interactiveQuestions === true ? { OPENCODE_ENABLE_QUESTION_TOOL: "true" } : {},
4937
+ OPENCODE_SERVER_USERNAME: "opencode",
4938
+ OPENCODE_SERVER_PASSWORD: password
4697
4939
  }
4698
4940
  });
4699
- await waitForHttpReady(
4700
- `http://127.0.0.1:${LOCAL_OPENCODE_PORT}/global/health`,
4701
- { timeoutMs: LOCAL_OPENCODE_READY_TIMEOUT_MS }
4702
- );
4703
- await markSetupComplete(target, setupId);
4941
+ processHandle.child.stdout.resume();
4942
+ processHandle.child.stderr.resume();
4943
+ try {
4944
+ let startupError;
4945
+ void processHandle.wait().then(
4946
+ (code) => {
4947
+ startupError = new Error(`Local OpenCode server exited before startup (${code})`);
4948
+ },
4949
+ (error) => {
4950
+ startupError = error instanceof Error ? error : new Error(String(error));
4951
+ }
4952
+ );
4953
+ await waitFor(async () => {
4954
+ if (startupError) throw startupError;
4955
+ try {
4956
+ return (await fetch(`${baseUrl}/global/health`, { headers, signal: AbortSignal.timeout(1e3) })).ok;
4957
+ } catch {
4958
+ return false;
4959
+ }
4960
+ }, { timeoutMs: LOCAL_OPENCODE_READY_TIMEOUT_MS });
4961
+ const unauthenticated = await fetch(`${baseUrl}/global/health`, { signal: AbortSignal.timeout(3e3) });
4962
+ if (unauthenticated.status !== 401) throw new Error("This OpenCode version does not enforce local server authentication. Upgrade OpenCode before running it through AgentBox.");
4963
+ return { baseUrl, headers, process: processHandle };
4964
+ } catch (error) {
4965
+ await processHandle.kill();
4966
+ throw error;
4967
+ }
4968
+ }
4969
+ async function ensureLocalOpenCodeServer(request) {
4970
+ let pending = localOpenCodeServers.get(request.options);
4971
+ if (!pending) {
4972
+ pending = startLocalOpenCodeServer(request);
4973
+ localOpenCodeServers.set(request.options, pending);
4974
+ }
4975
+ try {
4976
+ await pending;
4977
+ } catch (error) {
4978
+ localOpenCodeServers.delete(request.options);
4979
+ throw error;
4980
+ }
4704
4981
  }
4705
4982
  async function setupOpenCode(request) {
4706
4983
  if (request.options.sandbox) {
@@ -4713,16 +4990,6 @@ async function isSandboxOpenCodeServerHealthy(sandbox, cwd, port) {
4713
4990
  const probe = await sandbox.run(opencodeHealthCurl(port), { cwd, timeoutMs: 5e3 }).catch(() => void 0);
4714
4991
  return probe?.exitCode === 0;
4715
4992
  }
4716
- async function isLocalOpenCodeServerHealthy() {
4717
- try {
4718
- const res = await fetch(
4719
- `http://127.0.0.1:${LOCAL_OPENCODE_PORT}/global/health`
4720
- );
4721
- return res.ok;
4722
- } catch {
4723
- return false;
4724
- }
4725
- }
4726
4993
  async function killOpenCodeServer(request) {
4727
4994
  const { options } = request;
4728
4995
  if (options.sandbox) {
@@ -4743,24 +5010,22 @@ async function killOpenCodeServer(request) {
4743
5010
  );
4744
5011
  return;
4745
5012
  }
4746
- await killLocalOpenCodeServer();
5013
+ await killLocalOpenCodeServer(options);
4747
5014
  }
4748
5015
  async function buildOpenCodeRuntime(options) {
4749
5016
  if (options.sandbox) {
4750
5017
  const sandbox = options.sandbox;
4751
- const baseUrl2 = (await sandbox.getPreviewLink(SANDBOX_OPENCODE_PORT)).replace(/\/$/, "");
5018
+ const baseUrl = (await sandbox.getPreviewLink(SANDBOX_OPENCODE_PORT)).replace(/\/$/, "");
4752
5019
  return {
4753
- baseUrl: baseUrl2,
5020
+ baseUrl,
4754
5021
  previewHeaders: await opencodeAuthHeaders(sandbox),
4755
- raw: { baseUrl: baseUrl2, port: SANDBOX_OPENCODE_PORT }
5022
+ raw: { baseUrl, port: SANDBOX_OPENCODE_PORT }
4756
5023
  };
4757
5024
  }
4758
- const baseUrl = `http://127.0.0.1:${LOCAL_OPENCODE_PORT}`;
4759
- return {
4760
- baseUrl,
4761
- previewHeaders: {},
4762
- raw: { baseUrl, port: LOCAL_OPENCODE_PORT }
4763
- };
5025
+ const pending = localOpenCodeServers.get(options);
5026
+ if (!pending) throw new Error("Local OpenCode server has not been set up by this Agent instance");
5027
+ const server = await pending;
5028
+ return { baseUrl: server.baseUrl, previewHeaders: server.headers, raw: { baseUrl: server.baseUrl } };
4764
5029
  }
4765
5030
  var OpenCodeAgentAdapter = class {
4766
5031
  async setup(request) {
@@ -4859,7 +5124,7 @@ var OpenCodeAgentAdapter = class {
4859
5124
  resolveSessionTerminal();
4860
5125
  });
4861
5126
  try {
4862
- const interactiveApproval = isInteractiveApproval(request.options);
5127
+ const interactiveApproval = !request.options.fullAccess && isInteractiveApproval(request.options);
4863
5128
  let forkedSession = null;
4864
5129
  if (request.run.forkSessionId) {
4865
5130
  forkedSession = await fetchJson(
@@ -5000,6 +5265,32 @@ var OpenCodeAgentAdapter = class {
5000
5265
  }
5001
5266
  }
5002
5267
  }
5268
+ if (eventType === "question.asked") {
5269
+ const properties = payload.properties;
5270
+ if (properties && typeof properties.id === "string" && typeof properties.sessionID === "string" && await resolveRunSession(properties.sessionID)) {
5271
+ const questions = normalizeUserQuestions("open-code", properties);
5272
+ const response = hasInteractiveQuestions(request.options) ? await sink.requestPermission(createNormalizedEvent("permission.requested", {
5273
+ provider: request.provider,
5274
+ runId: request.runId,
5275
+ raw
5276
+ }, {
5277
+ requestId: properties.id,
5278
+ kind: "question",
5279
+ toolName: "question",
5280
+ title: "Your input is needed",
5281
+ input: properties,
5282
+ questions,
5283
+ canRemember: false
5284
+ })) : void 0;
5285
+ const allowed = response?.decision === "allow";
5286
+ await fetchJson(`${runtime.baseUrl}/question/${encodeURIComponent(properties.id)}/${allowed ? "reply" : "reject"}`, {
5287
+ method: "POST",
5288
+ headers: { "content-type": "application/json", ...runtime.previewHeaders },
5289
+ body: JSON.stringify(allowed ? { answers: questionReply("open-code", properties, response.answers ?? []) } : {})
5290
+ });
5291
+ }
5292
+ continue;
5293
+ }
5003
5294
  if (eventType === "permission.asked") {
5004
5295
  const properties = payload.properties;
5005
5296
  if (properties && typeof properties.sessionID === "string" && await resolveRunSession(properties.sessionID)) {
@@ -5147,6 +5438,12 @@ var OpenCodeAgentAdapter = class {
5147
5438
  })
5148
5439
  );
5149
5440
  const agentSlug = openCodeAgentSlug(request.run.reasoning);
5441
+ if (request.run.goal) throw new Error("Native goals are not supported by OpenCode.");
5442
+ if (request.run.mode) {
5443
+ const agents = await fetchJson(`${runtime.baseUrl}/agent`, { headers: runtime.previewHeaders });
5444
+ const name = request.run.mode === "plan" ? "plan" : "build";
5445
+ if (!agents.some((agent) => agent.name === name)) throw new Error(`This OpenCode installation does not expose the ${name} agent.`);
5446
+ }
5150
5447
  const dispatchPrompt = async (parts) => {
5151
5448
  const body = JSON.stringify({
5152
5449
  ...request.run.model ? { model: toOpenCodeModel(request.run.model) } : {},
@@ -5162,7 +5459,8 @@ var OpenCodeAgentAdapter = class {
5162
5459
  // instead. This per-message field stays as a per-run
5163
5460
  // override path that's effective for codex/GPT/Gemini.
5164
5461
  ...request.run.systemPrompt ? { system: request.run.systemPrompt } : {},
5165
- agent: agentSlug,
5462
+ ...request.options.configuration === "native" ? request.run.reasoning ? { variant: request.run.reasoning } : {} : { agent: agentSlug },
5463
+ ...request.run.mode ? { agent: request.run.mode === "plan" ? "plan" : "build" } : {},
5166
5464
  parts
5167
5465
  });
5168
5466
  const url = `${runtime.baseUrl}/session/${sessionId}/prompt_async`;
@@ -5261,8 +5559,8 @@ var OpenCodeAgentAdapter = class {
5261
5559
  streamedTextFromSse.length
5262
5560
  );
5263
5561
  let lastAssistantText = "";
5264
- for (const [messageId, text] of assistantTextByMessageId) {
5265
- lastAssistantText = text;
5562
+ for (const [messageId, text2] of assistantTextByMessageId) {
5563
+ lastAssistantText = text2;
5266
5564
  if (!announcedAssistantCompletions.has(messageId)) {
5267
5565
  announcedAssistantCompletions.add(messageId);
5268
5566
  sink.emitEvent(
@@ -5272,7 +5570,7 @@ var OpenCodeAgentAdapter = class {
5272
5570
  provider: request.provider,
5273
5571
  runId: request.runId
5274
5572
  },
5275
- { text }
5573
+ { text: text2 }
5276
5574
  )
5277
5575
  );
5278
5576
  }
@@ -5441,6 +5739,17 @@ function createAdapter(provider) {
5441
5739
  }
5442
5740
  }
5443
5741
  function prepareAgentOptions(_provider, options) {
5742
+ if (options.stateDirectory !== void 0) {
5743
+ if (options.sandbox) throw new Error("stateDirectory is only supported for host execution.");
5744
+ if (!path11.isAbsolute(options.stateDirectory)) throw new Error("stateDirectory must be an absolute path.");
5745
+ }
5746
+ if (options.sandbox && options.processGroup !== void 0) throw new Error("processGroup is only supported for host execution.");
5747
+ if (options.configuration === "native") {
5748
+ if (options.sandbox) throw new Error("Native configuration is only supported for host execution.");
5749
+ if (options.mcps?.length || options.skills?.length || options.subAgents?.length || options.commands?.length || options.enableRtk) {
5750
+ throw new Error("Native configuration uses the harness's own skills, MCPs, commands, and hooks.");
5751
+ }
5752
+ }
5444
5753
  return options;
5445
5754
  }
5446
5755
  var AgentRunController = class {
@@ -5570,12 +5879,15 @@ var AgentRunController = class {
5570
5879
  `Permission request ${response.requestId} is not pending for this run.`
5571
5880
  );
5572
5881
  }
5882
+ const answers = pending.event.kind === "question" && response.decision === "allow" ? validateUserAnswers(pending.event.questions, response.answers) : void 0;
5883
+ if (response.answers && !answers) throw new Error("Answers are only accepted for an allowed question request");
5573
5884
  this.pendingPermissions.delete(response.requestId);
5574
5885
  const remember = pending.event.canRemember ? response.remember : void 0;
5575
5886
  const resolvedResponse = {
5576
5887
  requestId: response.requestId,
5577
5888
  decision: response.decision,
5578
- ...remember !== void 0 ? { remember } : {}
5889
+ ...remember !== void 0 ? { remember } : {},
5890
+ ...answers ? { answers } : {}
5579
5891
  };
5580
5892
  this.pushEvent(
5581
5893
  createNormalizedEvent(
@@ -5587,7 +5899,8 @@ var AgentRunController = class {
5587
5899
  {
5588
5900
  requestId: response.requestId,
5589
5901
  decision: response.decision,
5590
- ...remember !== void 0 ? { remember } : {}
5902
+ ...remember !== void 0 ? { remember } : {},
5903
+ ...answers ? { answers } : {}
5591
5904
  }
5592
5905
  )
5593
5906
  );
@@ -5684,6 +5997,10 @@ var AgentRunController = class {
5684
5997
  if (this.settled) {
5685
5998
  return;
5686
5999
  }
6000
+ if (this.abortRequested) {
6001
+ this.cancel();
6002
+ return;
6003
+ }
5687
6004
  const normalizedError = asError(error);
5688
6005
  this.clearPendingPermissions(normalizedError);
5689
6006
  this.emitEvent(
@@ -5718,6 +6035,7 @@ var AgentRunController = class {
5718
6035
  }
5719
6036
  async abort() {
5720
6037
  this.abortRequested = true;
6038
+ this.clearPendingPermissions(new Error("Agent run cancelled"));
5721
6039
  await this.abortHandler();
5722
6040
  }
5723
6041
  rawEvents() {
@@ -5830,7 +6148,7 @@ var Agent = class {
5830
6148
  if (runConfig.forkAtMessageId && !runConfig.forkSessionId) {
5831
6149
  throw new Error("AgentRunConfig.forkAtMessageId requires forkSessionId.");
5832
6150
  }
5833
- const runId = runConfig.runId ?? randomUUID2();
6151
+ const runId = runConfig.runId ?? randomUUID3();
5834
6152
  const streamCalledAt = Date.now();
5835
6153
  debugAgent("stream() provider=%s runId=%s", this.provider, runId);
5836
6154
  const run = new AgentRunController(this.provider, runId);
@@ -5898,8 +6216,33 @@ var Agent = class {
5898
6216
  }
5899
6217
  };
5900
6218
 
6219
+ // src/agents/commands.ts
6220
+ function harnessCapabilities(provider) {
6221
+ return { commands: provider === "open-code" ? ["plan", "agent"] : ["plan", "agent", "goal"], planning: provider === "codex" ? "explicit" : "agent-directed", questions: true, fullAccess: true };
6222
+ }
6223
+ function resolveHarnessCommand(provider, input) {
6224
+ const first = typeof input === "string" ? input : input.find((part) => part.type === "text")?.text;
6225
+ const match = first?.match(/^\s*\/(plan|agent|goal)(?:\s+([\s\S]*))?$/);
6226
+ if (!match) return { input };
6227
+ const command = match[1];
6228
+ if (!harnessCapabilities(provider).commands.includes(command)) throw new Error(`/${command} is not supported by ${provider}.`);
6229
+ const body = match[2]?.trim() ?? "";
6230
+ if (command === "goal" && (!body || body.length > 4e3)) throw new Error("/goal requires an objective of 1\u20134000 characters.");
6231
+ if (command === "goal" && provider === "claude-code") return { input, goal: body };
6232
+ const text2 = body || (command === "plan" ? "Plan the requested work." : "Continue with implementation.");
6233
+ let replaced = false;
6234
+ const cleaned = typeof input === "string" ? text2 : input.map((part) => {
6235
+ if (part.type !== "text" || replaced) return part;
6236
+ replaced = true;
6237
+ return { ...part, text: text2 };
6238
+ });
6239
+ return command === "goal" ? { input: cleaned, goal: body } : { input: cleaned, mode: command === "plan" ? "plan" : "default" };
6240
+ }
6241
+
5901
6242
  export {
5902
6243
  agentboxRoot,
5903
6244
  getAgentLayout,
5904
- Agent
6245
+ Agent,
6246
+ harnessCapabilities,
6247
+ resolveHarnessCommand
5905
6248
  };