@stage5/lumine 0.2.40 → 0.2.41
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 +66 -13
- package/lib/admin-news.js +236 -0
- package/lib/admin-workflows.js +419 -0
- package/lib/admin.js +495 -44
- package/lib/agent/mcp-server.js +138 -0
- package/lib/agent/providers/claude-code.js +214 -0
- package/lib/agent/providers/codex.js +522 -0
- package/lib/agent/providers/environment.js +40 -0
- package/lib/agent/providers/index.js +38 -0
- package/lib/agent/tool-session.js +301 -0
- package/lib/agent/trace.js +138 -0
- package/lib/agent.js +453 -0
- package/lib/api.js +81 -1
- package/lib/build-review.js +493 -0
- package/lib/commands.js +80 -9
- package/lib/constants.js +2 -0
- package/package.json +1 -1
- package/sdk/BUILD_SDK_INDEX.md +24 -3
- package/sdk/LUMINE_ADMIN.md +95 -20
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import readline from "node:readline";
|
|
3
|
+
|
|
4
|
+
import { createSubscriptionAgentEnvironment } from "./environment.js";
|
|
5
|
+
|
|
6
|
+
const CODEX_DISABLED_BUILT_IN_FEATURES = [
|
|
7
|
+
"apps",
|
|
8
|
+
"artifact",
|
|
9
|
+
"auth_elicitation",
|
|
10
|
+
"browser_use",
|
|
11
|
+
"browser_use_external",
|
|
12
|
+
"browser_use_full_cdp_access",
|
|
13
|
+
"code_mode",
|
|
14
|
+
"code_mode_buffered_exec",
|
|
15
|
+
"code_mode_host",
|
|
16
|
+
"code_mode_only",
|
|
17
|
+
"computer_use",
|
|
18
|
+
"default_mode_request_user_input",
|
|
19
|
+
"deferred_executor",
|
|
20
|
+
"deferred_tool_world_state",
|
|
21
|
+
"enable_mcp_apps",
|
|
22
|
+
"exec_permission_approvals",
|
|
23
|
+
"executor_capability_discovery",
|
|
24
|
+
"external_agent_memory_import",
|
|
25
|
+
"goals",
|
|
26
|
+
"guardian_approval",
|
|
27
|
+
"guardianv2",
|
|
28
|
+
"hooks",
|
|
29
|
+
"image_generation",
|
|
30
|
+
"in_app_browser",
|
|
31
|
+
"js_repl",
|
|
32
|
+
"js_repl_tools_only",
|
|
33
|
+
"memories",
|
|
34
|
+
"multi_agent",
|
|
35
|
+
"network_proxy",
|
|
36
|
+
"plugins",
|
|
37
|
+
"plugin_sharing",
|
|
38
|
+
"recommended_plugins",
|
|
39
|
+
"remote_plugin",
|
|
40
|
+
"request_permissions_tool",
|
|
41
|
+
"respect_system_proxy",
|
|
42
|
+
"realtime_conversation",
|
|
43
|
+
"skill_mcp_dependency_install",
|
|
44
|
+
"skill_search",
|
|
45
|
+
"shell_snapshot",
|
|
46
|
+
"shell_tool",
|
|
47
|
+
"standalone_web_search",
|
|
48
|
+
"tool_call_mcp_elicitation",
|
|
49
|
+
"tool_suggest",
|
|
50
|
+
"unified_exec",
|
|
51
|
+
"use_agent_identity",
|
|
52
|
+
"view_image",
|
|
53
|
+
"workspace_dependencies",
|
|
54
|
+
];
|
|
55
|
+
const CODEX_PREFLIGHT_OUTPUT_LIMIT = 1024 * 1024;
|
|
56
|
+
const CODEX_PREFLIGHT_TIMEOUT_MS = 10_000;
|
|
57
|
+
|
|
58
|
+
const CODEX_RUNTIME_BASE_INSTRUCTIONS = `You are the model inside a Lumine external-agent runtime.
|
|
59
|
+
Use only the dynamic Lumine tools supplied by the host for project inspection and changes.
|
|
60
|
+
Do not use shell commands, filesystem tools, patches, web search, subagents, or skills.
|
|
61
|
+
The real project is not mounted in your working directory; Lumine tools are its only source of truth.
|
|
62
|
+
Follow the developer instructions and return only the requested short user-facing final response.
|
|
63
|
+
Never reveal hidden chain-of-thought or private reasoning.`;
|
|
64
|
+
|
|
65
|
+
export async function runCodexAgentPass({
|
|
66
|
+
options,
|
|
67
|
+
runtime,
|
|
68
|
+
toolSession,
|
|
69
|
+
isolationDir,
|
|
70
|
+
}) {
|
|
71
|
+
const result = await runCodexAppServerTurn({
|
|
72
|
+
options,
|
|
73
|
+
isolationDir,
|
|
74
|
+
baseInstructions: CODEX_RUNTIME_BASE_INSTRUCTIONS,
|
|
75
|
+
developerInstructions: runtime.systemPrompt,
|
|
76
|
+
prompt: runtime.initialPrompt,
|
|
77
|
+
tools: runtime.tools,
|
|
78
|
+
toolSession,
|
|
79
|
+
});
|
|
80
|
+
return { finalText: result.finalText };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function reviewCodexAgentLoop({
|
|
84
|
+
options,
|
|
85
|
+
isolationDir,
|
|
86
|
+
prompt,
|
|
87
|
+
systemPrompt,
|
|
88
|
+
outputSchema,
|
|
89
|
+
}) {
|
|
90
|
+
const result = await runCodexAppServerTurn({
|
|
91
|
+
options,
|
|
92
|
+
isolationDir,
|
|
93
|
+
baseInstructions:
|
|
94
|
+
"Review only the supplied observable runtime trace. Do not use tools or expose hidden reasoning. Return JSON matching the requested schema.",
|
|
95
|
+
developerInstructions: systemPrompt,
|
|
96
|
+
prompt,
|
|
97
|
+
tools: [],
|
|
98
|
+
toolSession: null,
|
|
99
|
+
outputSchema,
|
|
100
|
+
});
|
|
101
|
+
return parseStructuredResult(result.finalText);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function runCodexAppServerTurn({
|
|
105
|
+
options,
|
|
106
|
+
isolationDir,
|
|
107
|
+
baseInstructions,
|
|
108
|
+
developerInstructions,
|
|
109
|
+
prompt,
|
|
110
|
+
tools,
|
|
111
|
+
toolSession,
|
|
112
|
+
outputSchema,
|
|
113
|
+
}) {
|
|
114
|
+
const binary =
|
|
115
|
+
options.providerPath || process.env.LUMINE_CODEX_PATH || "codex";
|
|
116
|
+
const providerEnvironment = createSubscriptionAgentEnvironment();
|
|
117
|
+
let isolationArgs;
|
|
118
|
+
try {
|
|
119
|
+
isolationArgs = await loadCodexIsolationArgs({
|
|
120
|
+
binary,
|
|
121
|
+
cwd: isolationDir,
|
|
122
|
+
environment: providerEnvironment,
|
|
123
|
+
});
|
|
124
|
+
} catch (error) {
|
|
125
|
+
throw normalizeProviderLaunchError(error, binary, "Codex");
|
|
126
|
+
}
|
|
127
|
+
const child = spawn(binary, ["app-server", "--stdio", ...isolationArgs], {
|
|
128
|
+
cwd: isolationDir,
|
|
129
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
130
|
+
env: providerEnvironment,
|
|
131
|
+
});
|
|
132
|
+
child.stderr.on("data", (chunk) => process.stderr.write(chunk));
|
|
133
|
+
|
|
134
|
+
let nextRequestId = 1;
|
|
135
|
+
const pendingRequests = new Map();
|
|
136
|
+
let completedTurn = null;
|
|
137
|
+
let completedTurnResolve;
|
|
138
|
+
const completedTurnPromise = new Promise((resolve) => {
|
|
139
|
+
completedTurnResolve = resolve;
|
|
140
|
+
});
|
|
141
|
+
const completedMessages = [];
|
|
142
|
+
let deltaText = "";
|
|
143
|
+
const inheritedMcpServers = new Set();
|
|
144
|
+
|
|
145
|
+
function send(message) {
|
|
146
|
+
// App-server uses JSON-RPC semantics but intentionally omits the jsonrpc
|
|
147
|
+
// header on its JSONL wire format.
|
|
148
|
+
const { jsonrpc: _jsonrpc, ...wireMessage } = message;
|
|
149
|
+
child.stdin.write(`${JSON.stringify(wireMessage)}\n`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function request(method, params) {
|
|
153
|
+
const id = nextRequestId;
|
|
154
|
+
nextRequestId += 1;
|
|
155
|
+
return new Promise((resolve, reject) => {
|
|
156
|
+
pendingRequests.set(id, { resolve, reject });
|
|
157
|
+
send({ jsonrpc: "2.0", id, method, params });
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function handleServerRequest(message) {
|
|
162
|
+
if (message.method !== "item/tool/call") {
|
|
163
|
+
send({
|
|
164
|
+
jsonrpc: "2.0",
|
|
165
|
+
id: message.id,
|
|
166
|
+
error: {
|
|
167
|
+
code: -32601,
|
|
168
|
+
message: `Unsupported Codex server request: ${message.method}`,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (!toolSession) {
|
|
174
|
+
send({
|
|
175
|
+
jsonrpc: "2.0",
|
|
176
|
+
id: message.id,
|
|
177
|
+
result: {
|
|
178
|
+
contentItems: [
|
|
179
|
+
{
|
|
180
|
+
type: "inputText",
|
|
181
|
+
text: JSON.stringify({
|
|
182
|
+
ok: false,
|
|
183
|
+
error: "Tools are disabled for this turn.",
|
|
184
|
+
}),
|
|
185
|
+
},
|
|
186
|
+
],
|
|
187
|
+
success: false,
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
const output = await toolSession.call(
|
|
194
|
+
message.params?.tool,
|
|
195
|
+
message.params?.arguments || {},
|
|
196
|
+
);
|
|
197
|
+
const success = output?.ok !== false && !output?.error;
|
|
198
|
+
send({
|
|
199
|
+
jsonrpc: "2.0",
|
|
200
|
+
id: message.id,
|
|
201
|
+
result: {
|
|
202
|
+
contentItems: [
|
|
203
|
+
{ type: "inputText", text: JSON.stringify(output) },
|
|
204
|
+
],
|
|
205
|
+
success,
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
} catch (error) {
|
|
209
|
+
send({
|
|
210
|
+
jsonrpc: "2.0",
|
|
211
|
+
id: message.id,
|
|
212
|
+
result: {
|
|
213
|
+
contentItems: [
|
|
214
|
+
{
|
|
215
|
+
type: "inputText",
|
|
216
|
+
text: JSON.stringify({
|
|
217
|
+
ok: false,
|
|
218
|
+
error: String(error?.message || error),
|
|
219
|
+
}),
|
|
220
|
+
},
|
|
221
|
+
],
|
|
222
|
+
success: false,
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const output = readline.createInterface({
|
|
229
|
+
input: child.stdout,
|
|
230
|
+
crlfDelay: Infinity,
|
|
231
|
+
terminal: false,
|
|
232
|
+
});
|
|
233
|
+
output.on("line", (line) => {
|
|
234
|
+
if (!line.trim()) return;
|
|
235
|
+
let message;
|
|
236
|
+
try {
|
|
237
|
+
message = JSON.parse(line);
|
|
238
|
+
} catch {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
if (message.id !== undefined && !message.method) {
|
|
242
|
+
const pending = pendingRequests.get(message.id);
|
|
243
|
+
if (!pending) return;
|
|
244
|
+
pendingRequests.delete(message.id);
|
|
245
|
+
if (message.error) {
|
|
246
|
+
pending.reject(
|
|
247
|
+
new Error(
|
|
248
|
+
message.error?.message || "Codex app-server request failed.",
|
|
249
|
+
),
|
|
250
|
+
);
|
|
251
|
+
} else {
|
|
252
|
+
pending.resolve(message.result);
|
|
253
|
+
}
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (message.id !== undefined && message.method) {
|
|
257
|
+
void handleServerRequest(message);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (message.method === "item/agentMessage/delta") {
|
|
261
|
+
deltaText += String(message.params?.delta || "");
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (message.method === "mcpServer/startupStatus/updated") {
|
|
265
|
+
const status = String(message.params?.status || "");
|
|
266
|
+
const name = String(message.params?.name || "").trim();
|
|
267
|
+
if (name && (status === "starting" || status === "ready")) {
|
|
268
|
+
inheritedMcpServers.add(name);
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (
|
|
273
|
+
message.method === "item/completed" &&
|
|
274
|
+
message.params?.item?.type === "agentMessage"
|
|
275
|
+
) {
|
|
276
|
+
const text = String(message.params.item.text || "").trim();
|
|
277
|
+
if (text) completedMessages.push(text);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (message.method === "turn/completed") {
|
|
281
|
+
completedTurn = message.params?.turn || null;
|
|
282
|
+
completedTurnResolve(completedTurn);
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const childErrorPromise = new Promise((_, reject) => {
|
|
287
|
+
child.once("error", reject);
|
|
288
|
+
child.once("exit", (code, signal) => {
|
|
289
|
+
if (completedTurn) return;
|
|
290
|
+
reject(
|
|
291
|
+
new Error(
|
|
292
|
+
`Codex app-server exited before the turn completed (${signal || code}).`,
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
await Promise.race([
|
|
300
|
+
request("initialize", {
|
|
301
|
+
clientInfo: {
|
|
302
|
+
name: "lumine-cli",
|
|
303
|
+
title: "Lumine CLI",
|
|
304
|
+
version: "1",
|
|
305
|
+
},
|
|
306
|
+
// Dynamic tools are an experimental app-server capability. Without
|
|
307
|
+
// this explicit opt-in Codex may silently omit Lumine's tool surface.
|
|
308
|
+
capabilities: { experimentalApi: true },
|
|
309
|
+
}),
|
|
310
|
+
childErrorPromise,
|
|
311
|
+
]);
|
|
312
|
+
send({ jsonrpc: "2.0", method: "initialized", params: {} });
|
|
313
|
+
const threadResult = await request("thread/start", {
|
|
314
|
+
...(options.model ? { model: options.model } : {}),
|
|
315
|
+
cwd: isolationDir,
|
|
316
|
+
approvalPolicy: "never",
|
|
317
|
+
sandbox: "read-only",
|
|
318
|
+
ephemeral: true,
|
|
319
|
+
historyMode: "paginated",
|
|
320
|
+
environments: [],
|
|
321
|
+
baseInstructions,
|
|
322
|
+
developerInstructions,
|
|
323
|
+
dynamicTools: toCodexDynamicTools(tools),
|
|
324
|
+
});
|
|
325
|
+
const threadId = String(threadResult?.thread?.id || "");
|
|
326
|
+
if (!threadId) throw new Error("Codex did not return a thread ID.");
|
|
327
|
+
await delay(250);
|
|
328
|
+
const mcpInventory = await request("mcpServerStatus/list", {
|
|
329
|
+
threadId,
|
|
330
|
+
detail: "toolsAndAuthOnly",
|
|
331
|
+
limit: 100,
|
|
332
|
+
});
|
|
333
|
+
const inheritedToolServers = (Array.isArray(mcpInventory?.data)
|
|
334
|
+
? mcpInventory.data
|
|
335
|
+
: []
|
|
336
|
+
)
|
|
337
|
+
.filter((server) => Object.keys(server?.tools || {}).length > 0)
|
|
338
|
+
.map((server) => String(server?.name || "").trim())
|
|
339
|
+
.filter(Boolean);
|
|
340
|
+
if (mcpInventory?.nextCursor) {
|
|
341
|
+
throw new Error(
|
|
342
|
+
"Codex reported more inherited MCP servers than Lumine could verify safely.",
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
const inheritedServerNames = Array.from(
|
|
346
|
+
new Set([...inheritedMcpServers, ...inheritedToolServers]),
|
|
347
|
+
).sort();
|
|
348
|
+
if (inheritedServerNames.length > 0) {
|
|
349
|
+
throw new Error(
|
|
350
|
+
`Codex still exposed non-Lumine MCP tools (${inheritedServerNames.join(", ")}). The run was stopped before the model received the project prompt.`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
await request("turn/start", {
|
|
354
|
+
threadId,
|
|
355
|
+
input: [{ type: "text", text: prompt, text_elements: [] }],
|
|
356
|
+
...(options.agentEffort ? { effort: options.agentEffort } : {}),
|
|
357
|
+
...(outputSchema ? { outputSchema } : {}),
|
|
358
|
+
});
|
|
359
|
+
const turn = await Promise.race([completedTurnPromise, childErrorPromise]);
|
|
360
|
+
if (turn?.status !== "completed") {
|
|
361
|
+
throw new Error(
|
|
362
|
+
turn?.error?.message || `Codex turn ended with status ${turn?.status}.`,
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
const finalText =
|
|
366
|
+
completedMessages.at(-1) || String(deltaText || "").trim();
|
|
367
|
+
return { finalText };
|
|
368
|
+
} catch (error) {
|
|
369
|
+
throw normalizeProviderLaunchError(error, binary, "Codex");
|
|
370
|
+
} finally {
|
|
371
|
+
for (const pending of pendingRequests.values()) {
|
|
372
|
+
pending.reject(new Error("Codex app-server closed."));
|
|
373
|
+
}
|
|
374
|
+
pendingRequests.clear();
|
|
375
|
+
child.stdin.end();
|
|
376
|
+
if (child.exitCode === null && child.signalCode === null) child.kill();
|
|
377
|
+
output.close();
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function loadCodexIsolationArgs({ binary, cwd, environment }) {
|
|
382
|
+
const [featureOutput, mcpOutput] = await Promise.all([
|
|
383
|
+
runCodexInspectionCommand({
|
|
384
|
+
binary,
|
|
385
|
+
args: ["features", "list"],
|
|
386
|
+
cwd,
|
|
387
|
+
environment,
|
|
388
|
+
label: "feature inventory",
|
|
389
|
+
}),
|
|
390
|
+
runCodexInspectionCommand({
|
|
391
|
+
binary,
|
|
392
|
+
args: ["mcp", "list", "--json"],
|
|
393
|
+
cwd,
|
|
394
|
+
environment,
|
|
395
|
+
label: "MCP inventory",
|
|
396
|
+
}),
|
|
397
|
+
]);
|
|
398
|
+
const availableFeatures = new Set(
|
|
399
|
+
featureOutput
|
|
400
|
+
.split("\n")
|
|
401
|
+
.map((line) => String(line || "").trim().split(/\s+/)[0])
|
|
402
|
+
.filter((name) => /^[a-z][a-z0-9_]*$/.test(name)),
|
|
403
|
+
);
|
|
404
|
+
let mcpServers;
|
|
405
|
+
try {
|
|
406
|
+
mcpServers = JSON.parse(mcpOutput);
|
|
407
|
+
} catch {
|
|
408
|
+
throw new Error(
|
|
409
|
+
"Codex returned an invalid MCP inventory; Lumine cannot prove tool isolation.",
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
if (!Array.isArray(mcpServers)) {
|
|
413
|
+
throw new Error(
|
|
414
|
+
"Codex returned an invalid MCP inventory; Lumine cannot prove tool isolation.",
|
|
415
|
+
);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const args = [];
|
|
419
|
+
for (const feature of CODEX_DISABLED_BUILT_IN_FEATURES) {
|
|
420
|
+
if (availableFeatures.has(feature)) args.push("--disable", feature);
|
|
421
|
+
}
|
|
422
|
+
for (const server of mcpServers) {
|
|
423
|
+
if (server?.enabled !== true) continue;
|
|
424
|
+
const name = String(server?.name || "").trim();
|
|
425
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name)) {
|
|
426
|
+
throw new Error(
|
|
427
|
+
`Codex MCP server ${name || "(unnamed)"} cannot be disabled safely for an isolated Lumine run.`,
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
args.push("-c", `mcp_servers.${name}.enabled=false`);
|
|
431
|
+
}
|
|
432
|
+
return args;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function runCodexInspectionCommand({
|
|
436
|
+
binary,
|
|
437
|
+
args,
|
|
438
|
+
cwd,
|
|
439
|
+
environment,
|
|
440
|
+
label,
|
|
441
|
+
}) {
|
|
442
|
+
return await new Promise((resolve, reject) => {
|
|
443
|
+
const child = spawn(binary, args, {
|
|
444
|
+
cwd,
|
|
445
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
446
|
+
env: environment,
|
|
447
|
+
});
|
|
448
|
+
let stdout = "";
|
|
449
|
+
let settled = false;
|
|
450
|
+
const timeout = setTimeout(() => {
|
|
451
|
+
child.kill();
|
|
452
|
+
finish(
|
|
453
|
+
new Error(`Codex ${label} timed out; Lumine cannot prove tool isolation.`),
|
|
454
|
+
);
|
|
455
|
+
}, CODEX_PREFLIGHT_TIMEOUT_MS);
|
|
456
|
+
const finish = (error, value = "") => {
|
|
457
|
+
if (settled) return;
|
|
458
|
+
settled = true;
|
|
459
|
+
clearTimeout(timeout);
|
|
460
|
+
if (error) reject(error);
|
|
461
|
+
else resolve(value);
|
|
462
|
+
};
|
|
463
|
+
child.stdout.on("data", (chunk) => {
|
|
464
|
+
stdout += chunk.toString("utf8");
|
|
465
|
+
if (stdout.length > CODEX_PREFLIGHT_OUTPUT_LIMIT) {
|
|
466
|
+
child.kill();
|
|
467
|
+
finish(
|
|
468
|
+
new Error(
|
|
469
|
+
`Codex ${label} was unexpectedly large; Lumine cannot prove tool isolation.`,
|
|
470
|
+
),
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
child.stderr.resume();
|
|
475
|
+
child.once("error", (error) => finish(error));
|
|
476
|
+
child.once("exit", (code, signal) => {
|
|
477
|
+
if (code === 0) finish(null, stdout);
|
|
478
|
+
else {
|
|
479
|
+
finish(
|
|
480
|
+
new Error(
|
|
481
|
+
`Codex ${label} failed (${signal || code}); Lumine cannot prove tool isolation.`,
|
|
482
|
+
),
|
|
483
|
+
);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function delay(milliseconds) {
|
|
490
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function toCodexDynamicTools(tools) {
|
|
494
|
+
return (Array.isArray(tools) ? tools : [])
|
|
495
|
+
.filter((tool) => tool?.type === "function" && tool?.name)
|
|
496
|
+
.map((tool) => ({
|
|
497
|
+
type: "function",
|
|
498
|
+
name: tool.name,
|
|
499
|
+
description: tool.description || "",
|
|
500
|
+
inputSchema: tool.parameters || {
|
|
501
|
+
type: "object",
|
|
502
|
+
additionalProperties: true,
|
|
503
|
+
},
|
|
504
|
+
}));
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function parseStructuredResult(text) {
|
|
508
|
+
try {
|
|
509
|
+
return JSON.parse(String(text || "").trim());
|
|
510
|
+
} catch {
|
|
511
|
+
throw new Error("Codex did not return valid loop-review JSON.");
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function normalizeProviderLaunchError(error, binary, label) {
|
|
516
|
+
if (error?.code === "ENOENT") {
|
|
517
|
+
return new Error(
|
|
518
|
+
`${label} CLI was not found at ${binary}. Install it, sign in with your subscription, or pass --provider-path.`,
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
return error;
|
|
522
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const PASSTHROUGH_ENVIRONMENT_KEYS = new Set([
|
|
2
|
+
"CODEX_HOME",
|
|
3
|
+
"CLAUDE_CONFIG_DIR",
|
|
4
|
+
"COLORTERM",
|
|
5
|
+
"HOME",
|
|
6
|
+
"LANG",
|
|
7
|
+
"LC_ALL",
|
|
8
|
+
"LC_CTYPE",
|
|
9
|
+
"LOGNAME",
|
|
10
|
+
"NODE_EXTRA_CA_CERTS",
|
|
11
|
+
"PATH",
|
|
12
|
+
"SHELL",
|
|
13
|
+
"SSL_CERT_DIR",
|
|
14
|
+
"SSL_CERT_FILE",
|
|
15
|
+
"TEMP",
|
|
16
|
+
"TERM",
|
|
17
|
+
"TMP",
|
|
18
|
+
"TMPDIR",
|
|
19
|
+
"USER",
|
|
20
|
+
"XDG_CACHE_HOME",
|
|
21
|
+
"XDG_CONFIG_HOME",
|
|
22
|
+
"XDG_DATA_HOME",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
// Subscription CLIs load their own saved login from their local config or
|
|
26
|
+
// keychain. Give the provider only the environment needed to launch and find
|
|
27
|
+
// that saved login; in particular, do not expose Twinkle auth, cloud keys, or
|
|
28
|
+
// API-key billing credentials to a model-accessible child process.
|
|
29
|
+
export function createSubscriptionAgentEnvironment(source = process.env) {
|
|
30
|
+
const environment = {};
|
|
31
|
+
for (const [key, value] of Object.entries(source || {})) {
|
|
32
|
+
if (
|
|
33
|
+
value !== undefined &&
|
|
34
|
+
(PASSTHROUGH_ENVIRONMENT_KEYS.has(key) || key.startsWith("LC_"))
|
|
35
|
+
) {
|
|
36
|
+
environment[key] = String(value);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return environment;
|
|
40
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {
|
|
2
|
+
reviewClaudeCodeAgentLoop,
|
|
3
|
+
runClaudeCodeAgentPass,
|
|
4
|
+
} from "./claude-code.js";
|
|
5
|
+
import { reviewCodexAgentLoop, runCodexAgentPass } from "./codex.js";
|
|
6
|
+
|
|
7
|
+
const PROVIDERS = {
|
|
8
|
+
codex: {
|
|
9
|
+
id: "codex",
|
|
10
|
+
label: "Codex",
|
|
11
|
+
runPass: runCodexAgentPass,
|
|
12
|
+
reviewLoop: reviewCodexAgentLoop,
|
|
13
|
+
},
|
|
14
|
+
"claude-code": {
|
|
15
|
+
id: "claude-code",
|
|
16
|
+
label: "Claude Code",
|
|
17
|
+
runPass: runClaudeCodeAgentPass,
|
|
18
|
+
reviewLoop: reviewClaudeCodeAgentLoop,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function resolveExternalAgentProvider(value) {
|
|
23
|
+
const normalized = String(value || "")
|
|
24
|
+
.trim()
|
|
25
|
+
.toLowerCase();
|
|
26
|
+
const providerId = normalized === "claude" ? "claude-code" : normalized;
|
|
27
|
+
const provider = PROVIDERS[providerId];
|
|
28
|
+
if (!provider) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
"Choose an external agent with --provider codex or --provider claude-code.",
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return provider;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function listExternalAgentProviderIds() {
|
|
37
|
+
return Object.keys(PROVIDERS);
|
|
38
|
+
}
|