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.
- package/README.md +71 -0
- package/dist/agents/index.d.ts +15 -4
- package/dist/agents/index.js +8 -4
- package/dist/{chunk-TUBBWIOM.js → chunk-76AFK7XN.js} +1 -1
- package/dist/{chunk-IEQM5L4K.js → chunk-BO4J4KSM.js} +809 -466
- package/dist/{chunk-AVXJMCBC.js → chunk-MTQ2S46C.js} +22 -0
- package/dist/events/index.d.ts +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +8 -4
- package/dist/sandboxes/index.js +2 -2
- package/dist/{types-1seasIwq.d.ts → types-BQ-_AUfY.d.ts} +58 -6
- package/package.json +5 -3
|
@@ -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-
|
|
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
|
|
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 = "
|
|
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
|
|
1945
|
-
|
|
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
|
-
|
|
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
|
|
2649
|
+
const text2 = await response.text().catch(() => "");
|
|
2539
2650
|
throw new Error(
|
|
2540
|
-
`claude-code daemon /start failed: ${response.status} ${
|
|
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
|
-
|
|
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
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
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
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
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
|
-
}
|
|
2748
|
-
|
|
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
|
|
2824
|
-
if (
|
|
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(
|
|
3077
|
+
return JSON.parse(text2);
|
|
2831
3078
|
} catch (error) {
|
|
2832
|
-
const preview =
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
4182
|
+
const text2 = parts.filter((p) => p.type === "text").map((p) => p.text).join("");
|
|
3913
4183
|
const inputItems = [];
|
|
3914
|
-
if (
|
|
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
|
-
|
|
3948
|
-
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
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
|
|
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.
|
|
4046
|
-
const
|
|
4047
|
-
|
|
4048
|
-
|
|
4049
|
-
|
|
4050
|
-
|
|
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
|
-
|
|
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
|
|
4458
|
+
const text2 = joinTextParts(
|
|
4174
4459
|
parts.filter(
|
|
4175
4460
|
(part) => part.type === "text"
|
|
4176
4461
|
)
|
|
4177
4462
|
);
|
|
4178
4463
|
const inputItems = [];
|
|
4179
|
-
if (
|
|
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
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
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
|
|
4626
|
-
const
|
|
4627
|
-
const
|
|
4628
|
-
|
|
4629
|
-
|
|
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
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
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
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
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
|
|
5018
|
+
const baseUrl = (await sandbox.getPreviewLink(SANDBOX_OPENCODE_PORT)).replace(/\/$/, "");
|
|
4752
5019
|
return {
|
|
4753
|
-
baseUrl
|
|
5020
|
+
baseUrl,
|
|
4754
5021
|
previewHeaders: await opencodeAuthHeaders(sandbox),
|
|
4755
|
-
raw: { baseUrl
|
|
5022
|
+
raw: { baseUrl, port: SANDBOX_OPENCODE_PORT }
|
|
4756
5023
|
};
|
|
4757
5024
|
}
|
|
4758
|
-
const
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
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,
|
|
5265
|
-
lastAssistantText =
|
|
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 ??
|
|
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
|
};
|