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.
- 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-34LDITII.js → chunk-BO4J4KSM.js} +803 -442
- 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-3VRJIiW0.d.ts → types-BQ-_AUfY.d.ts} +57 -4
- 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
|
-
}
|
|
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
|
-
|
|
2706
|
-
|
|
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
|
|
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,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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
4182
|
+
const text2 = parts.filter((p) => p.type === "text").map((p) => p.text).join("");
|
|
3920
4183
|
const inputItems = [];
|
|
3921
|
-
if (
|
|
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
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
4458
|
+
const text2 = joinTextParts(
|
|
4156
4459
|
parts.filter(
|
|
4157
4460
|
(part) => part.type === "text"
|
|
4158
4461
|
)
|
|
4159
4462
|
);
|
|
4160
4463
|
const inputItems = [];
|
|
4161
|
-
if (
|
|
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
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
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
|
|
4608
|
-
const
|
|
4609
|
-
const
|
|
4610
|
-
|
|
4611
|
-
|
|
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
|
-
|
|
4634
|
-
|
|
4635
|
-
|
|
4636
|
-
|
|
4637
|
-
|
|
4638
|
-
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
|
|
4643
|
-
|
|
4644
|
-
|
|
4645
|
-
|
|
4646
|
-
|
|
4647
|
-
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
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
|
-
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
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
|
|
5018
|
+
const baseUrl = (await sandbox.getPreviewLink(SANDBOX_OPENCODE_PORT)).replace(/\/$/, "");
|
|
4734
5019
|
return {
|
|
4735
|
-
baseUrl
|
|
5020
|
+
baseUrl,
|
|
4736
5021
|
previewHeaders: await opencodeAuthHeaders(sandbox),
|
|
4737
|
-
raw: { baseUrl
|
|
5022
|
+
raw: { baseUrl, port: SANDBOX_OPENCODE_PORT }
|
|
4738
5023
|
};
|
|
4739
5024
|
}
|
|
4740
|
-
const
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
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,
|
|
5247
|
-
lastAssistantText =
|
|
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 ??
|
|
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
|
};
|