agentbox-sdk 0.1.503 → 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
- }
2664
- continue;
2665
- }
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;
2670
+ const control = item;
2671
+ if (!control._permission) {
2672
+ yield item;
2702
2673
  continue;
2703
2674
  }
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}`);
2704
2681
  }
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
- });
2746
- }
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,9 +3365,9 @@ 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 },
3124
3371
  // We only need the thread id back; we never read `thread.turns`.
3125
3372
  // Without this Codex hydrates the full history into the response and
3126
3373
  // emits a `deprecationNotice` ("Full-history hydration is deprecated
@@ -3134,15 +3381,22 @@ function buildForkParams(cwd, options, request) {
3134
3381
  lastTurnId: request.run.forkAtMessageId ?? null,
3135
3382
  cwd,
3136
3383
  model: request.run.model ?? null,
3137
- approvalPolicy: isInteractiveApproval(options) ? "untrusted" : "never",
3384
+ ...options.configuration === "native" && !options.fullAccess ? {} : { approvalPolicy: !options.fullAccess && isInteractiveApproval(options) ? "untrusted" : "never" },
3138
3385
  sandbox: buildCodexSandboxMode(options),
3139
- developerInstructions: request.run.systemPrompt ?? null,
3386
+ ...request.run.systemPrompt ? { developerInstructions: request.run.systemPrompt } : options.configuration === "native" ? {} : { developerInstructions: null },
3140
3387
  excludeTurns: true
3141
3388
  };
3142
3389
  }
3143
3390
  function buildTurnSandboxPolicy(options) {
3391
+ if (options.fullAccess || options.provider?.sandboxMode === "danger-full-access") return { type: "dangerFullAccess" };
3144
3392
  if (!options.sandbox) {
3145
- 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
+ };
3146
3400
  }
3147
3401
  if (options.sandbox.provider === SandboxProvider.LocalDocker) {
3148
3402
  return {
@@ -3161,10 +3415,16 @@ function buildCodexTurnStartParams(params) {
3161
3415
  return {
3162
3416
  threadId,
3163
3417
  input: inputItems,
3164
- 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
+ },
3165
3421
  ...sandboxPolicy ? { sandboxPolicy } : {},
3166
3422
  model: request.run.model ?? null,
3167
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
+ } } : {},
3168
3428
  outputSchema: null
3169
3429
  };
3170
3430
  }
@@ -3189,7 +3449,7 @@ function buildCodexCommandArgs(binary, args, options) {
3189
3449
  overrides.push(["supports_websockets", "false"]);
3190
3450
  }
3191
3451
  const overrideArgs = overrides.flatMap(([k, v]) => ["-c", `${k}=${v}`]);
3192
- return ["-u", "XDG_CONFIG_HOME", binary, ...overrideArgs, ...args];
3452
+ return [...options?.configuration === "native" ? [] : ["-u", "XDG_CONFIG_HOME"], binary, ...overrideArgs, ...args];
3193
3453
  }
3194
3454
  function toNormalizedCodexEvents(runId, notification) {
3195
3455
  const base = {
@@ -3261,8 +3521,8 @@ function toNormalizedCodexEvents(runId, notification) {
3261
3521
  }
3262
3522
  if (notification.method === "turn/completed") {
3263
3523
  const turn = notification.params?.turn;
3264
- const text = typeof turn?.lastAgentMessage === "string" ? turn.lastAgentMessage : void 0;
3265
- 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 })];
3266
3526
  }
3267
3527
  if (notification.method === "error") {
3268
3528
  const error = notification.params?.error;
@@ -3274,7 +3534,7 @@ function toNormalizedCodexEvents(runId, notification) {
3274
3534
  }
3275
3535
  return [];
3276
3536
  }
3277
- function createCodexPermissionEvent(request, notification) {
3537
+ function createCodexPermissionEvent(request, notification, fileChanges) {
3278
3538
  const raw = toRawEvent2(request.runId, notification, notification.method);
3279
3539
  const params = notification.params;
3280
3540
  const requestId = notification.id;
@@ -3317,7 +3577,7 @@ function createCodexPermissionEvent(request, notification) {
3317
3577
  kind: "file-change",
3318
3578
  title: "Approve file changes",
3319
3579
  message: typeof params.reason === "string" ? params.reason : "Codex wants to modify files.",
3320
- input: params,
3580
+ input: fileChanges ? { ...params, changes: fileChanges } : params,
3321
3581
  canRemember: availableDecisions.includes("acceptForSession")
3322
3582
  }
3323
3583
  );
@@ -3371,7 +3631,7 @@ async function materializeCodexImage(options, part, index) {
3371
3631
  if (data.length === 0) {
3372
3632
  throw new Error("Cannot attach an empty image to Codex.");
3373
3633
  }
3374
- const root = agentboxRoot(AgentProvider.Codex, Boolean(options.sandbox));
3634
+ const root = agentboxRoot(AgentProvider.Codex, Boolean(options.sandbox), options.stateDirectory);
3375
3635
  const imagePath = path9.join(
3376
3636
  root,
3377
3637
  "inputs",
@@ -3518,6 +3778,7 @@ async function connectRemoteCodexAppServer(url, headers = {}) {
3518
3778
  }
3519
3779
  async function setupCodex(request) {
3520
3780
  const options = request.options;
3781
+ if (options.configuration === "native") return;
3521
3782
  const provider = request.provider;
3522
3783
  const hooks = assertHooksSupported(provider, options);
3523
3784
  assertCommandsSupported(provider, options.commands);
@@ -3715,10 +3976,10 @@ async function createRuntime(request, inputParts) {
3715
3976
  const codexDir = codexConfigDir(options);
3716
3977
  const env = compactEnv({
3717
3978
  ...options.env ?? {},
3718
- CODEX_HOME: codexDir,
3979
+ ...options.configuration === "native" ? {} : { CODEX_HOME: codexDir },
3719
3980
  ...options.provider?.env ?? {}
3720
3981
  });
3721
- const runtimeCwd = path9.dirname(codexDir);
3982
+ const runtimeCwd = options.configuration === "native" ? options.cwd : path9.dirname(codexDir);
3722
3983
  const inputItems = await buildCodexInputItems(options, inputParts);
3723
3984
  const usesRemoteWebSocket = options.sandbox && options.sandbox.provider !== SandboxProvider.LocalDocker;
3724
3985
  if (usesRemoteWebSocket && options.sandbox) {
@@ -3788,6 +4049,7 @@ async function createRuntime(request, inputParts) {
3788
4049
  };
3789
4050
  }
3790
4051
  const processHandle = spawnCommand({
4052
+ processGroup: options.processGroup !== "inherited",
3791
4053
  command: "env",
3792
4054
  args: codexArgs,
3793
4055
  cwd: runtimeCwd,
@@ -3796,6 +4058,7 @@ async function createRuntime(request, inputParts) {
3796
4058
  ...env
3797
4059
  }
3798
4060
  });
4061
+ processHandle.child.stderr.resume();
3799
4062
  return {
3800
4063
  source: linesFromNodeStream(processHandle.child.stdout),
3801
4064
  writeLine: async (line) => {
@@ -3916,10 +4179,10 @@ var CodexAgentAdapter = class {
3916
4179
  throw new Error("Cannot send message before thread is started.");
3917
4180
  }
3918
4181
  const parts = normalizeUserInput(content);
3919
- 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("");
3920
4183
  const inputItems = [];
3921
- if (text.trim().length > 0) {
3922
- inputItems.push({ type: "text", text, text_elements: [] });
4184
+ if (text2.trim().length > 0) {
4185
+ inputItems.push({ type: "text", text: text2, text_elements: [] });
3923
4186
  }
3924
4187
  const response = await client.request(
3925
4188
  "turn/start",
@@ -3935,6 +4198,8 @@ var CodexAgentAdapter = class {
3935
4198
  };
3936
4199
  sink.onMessage(sendTurn);
3937
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;
3938
4203
  let streamedText = "";
3939
4204
  const completion = new Promise((resolve, reject) => {
3940
4205
  void (async () => {
@@ -3951,15 +4216,39 @@ var CodexAgentAdapter = class {
3951
4216
  const raw = toRawEvent2(request.runId, message, message.method);
3952
4217
  rawPayloads.push(message);
3953
4218
  sink.emitRaw(raw);
3954
- if (message.method === "tool/requestUserInput" && message.id !== void 0) {
3955
- reject(
3956
- new Error(
3957
- "Codex tool/requestUserInput approvals are not yet supported by AgentBox."
3958
- )
3959
- );
3960
- 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;
3961
4245
  }
3962
- 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
+ );
3963
4252
  if (permissionEvent && message.id !== void 0) {
3964
4253
  const response = interactiveApproval ? await sink.requestPermission(permissionEvent) : {
3965
4254
  requestId: permissionEvent.requestId,
@@ -3968,8 +4257,15 @@ var CodexAgentAdapter = class {
3968
4257
  await client.respond(message.id, {
3969
4258
  decision: toCodexApprovalDecision(message, response)
3970
4259
  });
4260
+ if (approvalKey) pendingFileChanges.delete(approvalKey);
3971
4261
  continue;
3972
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
+ }
3973
4269
  for (const event of toNormalizedCodexEvents(request.runId, message)) {
3974
4270
  sink.emitEvent(event);
3975
4271
  if (event.type === "text.delta") {
@@ -4049,6 +4345,13 @@ var CodexAgentAdapter = class {
4049
4345
  sink.emitRaw(
4050
4346
  toRawEvent2(request.runId, threadResponse, threadResultEventName)
4051
4347
  );
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" });
4354
+ }
4052
4355
  await client.request(
4053
4356
  "turn/start",
4054
4357
  buildCodexTurnStartParams({
@@ -4078,19 +4381,19 @@ var CodexAgentAdapter = class {
4078
4381
  sink.fail(completionError);
4079
4382
  }
4080
4383
  } else {
4081
- const { text, interrupted } = completionResult;
4384
+ const { text: text2, interrupted } = completionResult;
4082
4385
  if (abortInvoked || interrupted) {
4083
4386
  debugCodex(
4084
4387
  "\u2605 run.cancelled (%dms since execute start) interrupted=%s",
4085
4388
  Date.now() - executeStartedAt,
4086
4389
  interrupted
4087
4390
  );
4088
- sink.cancel({ text, costData: extractCodexCostData(rawPayloads) });
4391
+ sink.cancel({ text: text2, costData: extractCodexCostData(rawPayloads) });
4089
4392
  } else {
4090
4393
  debugCodex(
4091
4394
  "\u2605 run.completed (%dms since execute start) chars=%d",
4092
4395
  Date.now() - executeStartedAt,
4093
- text?.length ?? 0
4396
+ text2?.length ?? 0
4094
4397
  );
4095
4398
  sink.complete({ costData: extractCodexCostData(rawPayloads) });
4096
4399
  }
@@ -4152,14 +4455,14 @@ var CodexAgentAdapter = class {
4152
4455
  );
4153
4456
  }
4154
4457
  const parts = normalizeUserInput(content);
4155
- const text = joinTextParts(
4458
+ const text2 = joinTextParts(
4156
4459
  parts.filter(
4157
4460
  (part) => part.type === "text"
4158
4461
  )
4159
4462
  );
4160
4463
  const inputItems = [];
4161
- if (text.trim().length > 0) {
4162
- inputItems.push({ type: "text", text, text_elements: [] });
4464
+ if (text2.trim().length > 0) {
4465
+ inputItems.push({ type: "text", text: text2, text_elements: [] });
4163
4466
  }
4164
4467
  await withCodexAppServer(request, async (client) => {
4165
4468
  await client.request("turn/start", {
@@ -4175,10 +4478,10 @@ var CodexAgentAdapter = class {
4175
4478
  };
4176
4479
 
4177
4480
  // src/agents/providers/opencode.ts
4178
- import { createHash as createHash2 } from "crypto";
4481
+ import { createHash as createHash2, randomBytes, randomUUID as randomUUID2 } from "crypto";
4482
+ import os2 from "os";
4179
4483
  import path10 from "path";
4180
4484
  var SANDBOX_OPENCODE_PORT = 4096;
4181
- var LOCAL_OPENCODE_PORT = 4096;
4182
4485
  var SANDBOX_OPENCODE_READY_TIMEOUT_MS = 9e4;
4183
4486
  var LOCAL_OPENCODE_READY_TIMEOUT_MS = 2e4;
4184
4487
  var SHARED_OPENCODE_TARGET_ID = "shared-opencode-server";
@@ -4230,30 +4533,13 @@ function hashLlmApiKeys(env) {
4230
4533
  }
4231
4534
  return hasher.digest("hex");
4232
4535
  }
4233
- async function killLocalOpenCodeServer() {
4234
- await time(debugOpencode, "kill local opencode server", async () => {
4235
- const killer = spawnCommand({
4236
- command: "sh",
4237
- args: [
4238
- "-c",
4239
- `lsof -ti tcp:${LOCAL_OPENCODE_PORT} | xargs kill 2>/dev/null || true`
4240
- ]
4241
- });
4242
- await killer.wait().catch(() => void 0);
4243
- await waitFor(
4244
- async () => {
4245
- try {
4246
- const res = await fetch(
4247
- `http://127.0.0.1:${LOCAL_OPENCODE_PORT}/global/health`
4248
- );
4249
- return !res.ok;
4250
- } catch {
4251
- return true;
4252
- }
4253
- },
4254
- { timeoutMs: 5e3, intervalMs: 200 }
4255
- ).catch(() => void 0);
4256
- });
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);
4257
4543
  }
4258
4544
  async function killSandboxOpenCodeServer(sandbox, pidFilePath, cwd, port) {
4259
4545
  await time(debugOpencode, "kill sandbox opencode server", async () => {
@@ -4332,6 +4618,7 @@ function createOpenCodePermissionEvent(request, raw, payload) {
4332
4618
  },
4333
4619
  {
4334
4620
  requestId: String(properties.id ?? ""),
4621
+ toolName: permission,
4335
4622
  kind: permission === "bash" ? "bash" : permission === "edit" ? "edit" : permission === "external_directory" ? "file-change" : permission === "webfetch" ? "network" : permission === "task" ? "tool" : "unknown",
4336
4623
  title: `Approve ${permission} permission`,
4337
4624
  message: typeof properties.metadata === "object" && properties.metadata !== null ? JSON.stringify(properties.metadata) : `OpenCode requested ${permission} permission.`,
@@ -4353,6 +4640,7 @@ function buildOpenCodeConfig(options, interactiveApproval) {
4353
4640
  prompt: options.systemPrompt || FALLBACK_OPEN_CODE_AGENT_PROMPT,
4354
4641
  permission: buildOpenCodePermissionConfig(interactiveApproval),
4355
4642
  tools: {
4643
+ question: hasInteractiveQuestions(options),
4356
4644
  write: true,
4357
4645
  edit: true,
4358
4646
  bash: true,
@@ -4407,7 +4695,7 @@ async function ensureSandboxOpenCodeServer(request) {
4407
4695
  const port = SANDBOX_OPENCODE_PORT;
4408
4696
  const plugins = assertHooksSupported(request.provider, options);
4409
4697
  assertCommandsSupported(request.provider, options.commands);
4410
- const interactiveApproval = isInteractiveApproval(options);
4698
+ const interactiveApproval = !options.fullAccess && isInteractiveApproval(options);
4411
4699
  const target = await createSetupTarget(
4412
4700
  request.provider,
4413
4701
  SHARED_OPENCODE_TARGET_ID,
@@ -4475,7 +4763,8 @@ async function ensureSandboxOpenCodeServer(request) {
4475
4763
  const commonEnv = {
4476
4764
  OPENCODE_CONFIG: configPath,
4477
4765
  OPENCODE_CONFIG_DIR: target.layout.opencodeDir,
4478
- OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
4766
+ OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
4767
+ OPENCODE_ENABLE_QUESTION_TOOL: hasInteractiveQuestions(options) ? "true" : "false"
4479
4768
  };
4480
4769
  await applyDifferentialSetup(target, allArtifacts, installCommands);
4481
4770
  if (enableRtk) {
@@ -4604,85 +4893,91 @@ ${lastLog}` : "")
4604
4893
  await markSetupComplete(target, setupId);
4605
4894
  });
4606
4895
  }
4607
- async function ensureLocalOpenCodeServer(request) {
4608
- const options = request.options;
4609
- const plugins = assertHooksSupported(request.provider, options);
4610
- assertCommandsSupported(request.provider, options.commands);
4611
- const interactiveApproval = isInteractiveApproval(options);
4612
- const target = await createSetupTarget(
4613
- request.provider,
4614
- "shared-setup",
4615
- options
4616
- );
4617
- const { artifacts: skillArtifacts, installCommands } = await prepareSkillArtifacts(
4618
- request.provider,
4619
- options.skills,
4620
- target.layout
4621
- );
4622
- const pluginArtifacts = buildOpenCodePluginArtifacts(
4623
- plugins,
4624
- target.layout.opencodeDir
4625
- );
4626
- const configPath = path10.join(target.layout.opencodeDir, "agentbox.json");
4627
- const openCodeConfig = buildOpenCodeConfig(options, interactiveApproval);
4628
- const commonEnv = {
4629
- OPENCODE_CONFIG: configPath,
4630
- OPENCODE_CONFIG_DIR: target.layout.opencodeDir,
4631
- OPENCODE_DISABLE_DEFAULT_PLUGINS: "true"
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())
4632
4901
  };
4633
- const allArtifacts = [
4634
- ...skillArtifacts,
4635
- ...pluginArtifacts,
4636
- {
4637
- path: configPath,
4638
- content: JSON.stringify(openCodeConfig, null, 2)
4639
- }
4640
- ];
4641
- const daemonInfo = {
4642
- port: LOCAL_OPENCODE_PORT,
4643
- healthPath: "/global/health"
4644
- };
4645
- const setupId = computeSetupId({
4646
- artifacts: allArtifacts,
4647
- installCommands,
4648
- daemon: daemonInfo,
4649
- extras: [`apiKeys:${hashLlmApiKeys(options.env)}`]
4650
- });
4651
- if (await preflightSetup(target, setupId, daemonInfo)) {
4652
- debugOpencode("local opencode server up-to-date \u2014 reusing");
4653
- return;
4654
- }
4655
- if (await isLocalOpenCodeServerHealthy()) {
4656
- debugOpencode(
4657
- "local opencode server already running but setup drifted \u2014 reusing it without restart; call agent.killServer() to apply the new config"
4658
- );
4659
- 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
+ };
4660
4921
  }
4661
- debugOpencode("local opencode server absent \u2014 spawning");
4662
- await applyDifferentialSetup(target, allArtifacts, installCommands);
4663
- await killLocalOpenCodeServer();
4664
- 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({
4665
4927
  command: options.provider?.binary ?? "opencode",
4666
- args: [
4667
- "serve",
4668
- "--hostname",
4669
- "127.0.0.1",
4670
- "--port",
4671
- String(LOCAL_OPENCODE_PORT),
4672
- ...options.provider?.args ?? []
4673
- ],
4928
+ args: ["serve", ...options.provider?.args ?? [], "--hostname", "127.0.0.1", "--port", String(port)],
4674
4929
  cwd: options.cwd,
4930
+ processGroup: options.processGroup !== "inherited",
4675
4931
  env: {
4676
4932
  ...process.env,
4677
- ...options.env ?? {},
4678
- ...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
4679
4939
  }
4680
4940
  });
4681
- await waitForHttpReady(
4682
- `http://127.0.0.1:${LOCAL_OPENCODE_PORT}/global/health`,
4683
- { timeoutMs: LOCAL_OPENCODE_READY_TIMEOUT_MS }
4684
- );
4685
- 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
+ }
4686
4981
  }
4687
4982
  async function setupOpenCode(request) {
4688
4983
  if (request.options.sandbox) {
@@ -4695,16 +4990,6 @@ async function isSandboxOpenCodeServerHealthy(sandbox, cwd, port) {
4695
4990
  const probe = await sandbox.run(opencodeHealthCurl(port), { cwd, timeoutMs: 5e3 }).catch(() => void 0);
4696
4991
  return probe?.exitCode === 0;
4697
4992
  }
4698
- async function isLocalOpenCodeServerHealthy() {
4699
- try {
4700
- const res = await fetch(
4701
- `http://127.0.0.1:${LOCAL_OPENCODE_PORT}/global/health`
4702
- );
4703
- return res.ok;
4704
- } catch {
4705
- return false;
4706
- }
4707
- }
4708
4993
  async function killOpenCodeServer(request) {
4709
4994
  const { options } = request;
4710
4995
  if (options.sandbox) {
@@ -4725,24 +5010,22 @@ async function killOpenCodeServer(request) {
4725
5010
  );
4726
5011
  return;
4727
5012
  }
4728
- await killLocalOpenCodeServer();
5013
+ await killLocalOpenCodeServer(options);
4729
5014
  }
4730
5015
  async function buildOpenCodeRuntime(options) {
4731
5016
  if (options.sandbox) {
4732
5017
  const sandbox = options.sandbox;
4733
- const baseUrl2 = (await sandbox.getPreviewLink(SANDBOX_OPENCODE_PORT)).replace(/\/$/, "");
5018
+ const baseUrl = (await sandbox.getPreviewLink(SANDBOX_OPENCODE_PORT)).replace(/\/$/, "");
4734
5019
  return {
4735
- baseUrl: baseUrl2,
5020
+ baseUrl,
4736
5021
  previewHeaders: await opencodeAuthHeaders(sandbox),
4737
- raw: { baseUrl: baseUrl2, port: SANDBOX_OPENCODE_PORT }
5022
+ raw: { baseUrl, port: SANDBOX_OPENCODE_PORT }
4738
5023
  };
4739
5024
  }
4740
- const baseUrl = `http://127.0.0.1:${LOCAL_OPENCODE_PORT}`;
4741
- return {
4742
- baseUrl,
4743
- previewHeaders: {},
4744
- raw: { baseUrl, port: LOCAL_OPENCODE_PORT }
4745
- };
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 } };
4746
5029
  }
4747
5030
  var OpenCodeAgentAdapter = class {
4748
5031
  async setup(request) {
@@ -4841,7 +5124,7 @@ var OpenCodeAgentAdapter = class {
4841
5124
  resolveSessionTerminal();
4842
5125
  });
4843
5126
  try {
4844
- const interactiveApproval = isInteractiveApproval(request.options);
5127
+ const interactiveApproval = !request.options.fullAccess && isInteractiveApproval(request.options);
4845
5128
  let forkedSession = null;
4846
5129
  if (request.run.forkSessionId) {
4847
5130
  forkedSession = await fetchJson(
@@ -4982,6 +5265,32 @@ var OpenCodeAgentAdapter = class {
4982
5265
  }
4983
5266
  }
4984
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
+ }
4985
5294
  if (eventType === "permission.asked") {
4986
5295
  const properties = payload.properties;
4987
5296
  if (properties && typeof properties.sessionID === "string" && await resolveRunSession(properties.sessionID)) {
@@ -5129,6 +5438,12 @@ var OpenCodeAgentAdapter = class {
5129
5438
  })
5130
5439
  );
5131
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
+ }
5132
5447
  const dispatchPrompt = async (parts) => {
5133
5448
  const body = JSON.stringify({
5134
5449
  ...request.run.model ? { model: toOpenCodeModel(request.run.model) } : {},
@@ -5144,7 +5459,8 @@ var OpenCodeAgentAdapter = class {
5144
5459
  // instead. This per-message field stays as a per-run
5145
5460
  // override path that's effective for codex/GPT/Gemini.
5146
5461
  ...request.run.systemPrompt ? { system: request.run.systemPrompt } : {},
5147
- 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" } : {},
5148
5464
  parts
5149
5465
  });
5150
5466
  const url = `${runtime.baseUrl}/session/${sessionId}/prompt_async`;
@@ -5243,8 +5559,8 @@ var OpenCodeAgentAdapter = class {
5243
5559
  streamedTextFromSse.length
5244
5560
  );
5245
5561
  let lastAssistantText = "";
5246
- for (const [messageId, text] of assistantTextByMessageId) {
5247
- lastAssistantText = text;
5562
+ for (const [messageId, text2] of assistantTextByMessageId) {
5563
+ lastAssistantText = text2;
5248
5564
  if (!announcedAssistantCompletions.has(messageId)) {
5249
5565
  announcedAssistantCompletions.add(messageId);
5250
5566
  sink.emitEvent(
@@ -5254,7 +5570,7 @@ var OpenCodeAgentAdapter = class {
5254
5570
  provider: request.provider,
5255
5571
  runId: request.runId
5256
5572
  },
5257
- { text }
5573
+ { text: text2 }
5258
5574
  )
5259
5575
  );
5260
5576
  }
@@ -5423,6 +5739,17 @@ function createAdapter(provider) {
5423
5739
  }
5424
5740
  }
5425
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
+ }
5426
5753
  return options;
5427
5754
  }
5428
5755
  var AgentRunController = class {
@@ -5552,12 +5879,15 @@ var AgentRunController = class {
5552
5879
  `Permission request ${response.requestId} is not pending for this run.`
5553
5880
  );
5554
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");
5555
5884
  this.pendingPermissions.delete(response.requestId);
5556
5885
  const remember = pending.event.canRemember ? response.remember : void 0;
5557
5886
  const resolvedResponse = {
5558
5887
  requestId: response.requestId,
5559
5888
  decision: response.decision,
5560
- ...remember !== void 0 ? { remember } : {}
5889
+ ...remember !== void 0 ? { remember } : {},
5890
+ ...answers ? { answers } : {}
5561
5891
  };
5562
5892
  this.pushEvent(
5563
5893
  createNormalizedEvent(
@@ -5569,7 +5899,8 @@ var AgentRunController = class {
5569
5899
  {
5570
5900
  requestId: response.requestId,
5571
5901
  decision: response.decision,
5572
- ...remember !== void 0 ? { remember } : {}
5902
+ ...remember !== void 0 ? { remember } : {},
5903
+ ...answers ? { answers } : {}
5573
5904
  }
5574
5905
  )
5575
5906
  );
@@ -5666,6 +5997,10 @@ var AgentRunController = class {
5666
5997
  if (this.settled) {
5667
5998
  return;
5668
5999
  }
6000
+ if (this.abortRequested) {
6001
+ this.cancel();
6002
+ return;
6003
+ }
5669
6004
  const normalizedError = asError(error);
5670
6005
  this.clearPendingPermissions(normalizedError);
5671
6006
  this.emitEvent(
@@ -5700,6 +6035,7 @@ var AgentRunController = class {
5700
6035
  }
5701
6036
  async abort() {
5702
6037
  this.abortRequested = true;
6038
+ this.clearPendingPermissions(new Error("Agent run cancelled"));
5703
6039
  await this.abortHandler();
5704
6040
  }
5705
6041
  rawEvents() {
@@ -5812,7 +6148,7 @@ var Agent = class {
5812
6148
  if (runConfig.forkAtMessageId && !runConfig.forkSessionId) {
5813
6149
  throw new Error("AgentRunConfig.forkAtMessageId requires forkSessionId.");
5814
6150
  }
5815
- const runId = runConfig.runId ?? randomUUID2();
6151
+ const runId = runConfig.runId ?? randomUUID3();
5816
6152
  const streamCalledAt = Date.now();
5817
6153
  debugAgent("stream() provider=%s runId=%s", this.provider, runId);
5818
6154
  const run = new AgentRunController(this.provider, runId);
@@ -5880,8 +6216,33 @@ var Agent = class {
5880
6216
  }
5881
6217
  };
5882
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
+
5883
6242
  export {
5884
6243
  agentboxRoot,
5885
6244
  getAgentLayout,
5886
- Agent
6245
+ Agent,
6246
+ harnessCapabilities,
6247
+ resolveHarnessCommand
5887
6248
  };