mindexec-ai 0.2.385
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 +354 -0
- package/codex-runtime.js +1339 -0
- package/launch-bridge.cjs +236 -0
- package/package.json +77 -0
- package/port-guard.cjs +232 -0
- package/remote-fast/osx-arm64/mindexec-remote-fast +0 -0
- package/remote-fast/osx-arm64/mindexec-remote-fast.deps.json +24 -0
- package/remote-fast/osx-arm64/mindexec-remote-fast.dll +0 -0
- package/remote-fast/osx-arm64/mindexec-remote-fast.runtimeconfig.json +13 -0
- package/remote-fast/osx-x64/mindexec-remote-fast +0 -0
- package/remote-fast/osx-x64/mindexec-remote-fast.deps.json +24 -0
- package/remote-fast/osx-x64/mindexec-remote-fast.dll +0 -0
- package/remote-fast/osx-x64/mindexec-remote-fast.runtimeconfig.json +13 -0
- package/remote-fast/win-x64/mindexec-remote-fast.deps.json +24 -0
- package/remote-fast/win-x64/mindexec-remote-fast.dll +0 -0
- package/remote-fast/win-x64/mindexec-remote-fast.exe +0 -0
- package/remote-fast/win-x64/mindexec-remote-fast.runtimeconfig.json +20 -0
- package/remote-hub.js +3106 -0
- package/scripts/auth-session-smoke.mjs +262 -0
- package/scripts/remote-agent-managed-smoke.mjs +291 -0
- package/scripts/remote-agent-package-smoke.mjs +64 -0
- package/scripts/remote-agent-ws-smoke.mjs +202 -0
- package/scripts/remote-fast-live-rate-smoke.mjs +355 -0
- package/scripts/remote-fast-mdm-browser-smoke.mjs +476 -0
- package/scripts/remote-fleet-render-smoke.mjs +1491 -0
- package/scripts/remote-frame-ws-smoke.mjs +234 -0
- package/scripts/remote-http-smoke.mjs +592 -0
- package/scripts/remote-hub-identity-smoke.mjs +146 -0
- package/scripts/remote-hub-scale-smoke.mjs +124 -0
- package/scripts/remote-hub-smoke.mjs +631 -0
- package/scripts/remote-input-ws-smoke.mjs +263 -0
- package/scripts/remote-registry-follower-smoke.mjs +752 -0
- package/scripts/setup-tree-sitter-grammars.mjs +80 -0
- package/server.js +15709 -0
- package/start-bridge.bat +32 -0
- package/start-bridge.sh +81 -0
- package/tree-sitter-grammars/README.md +18 -0
- package/tree-sitter-grammars/tree-sitter-c_sharp.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-go.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-java.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-javascript.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-python.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-rust.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-tsx.wasm +0 -0
- package/tree-sitter-grammars/tree-sitter-typescript.wasm +0 -0
- package/wwwroot/_headers +73 -0
- package/wwwroot/_redirects +1 -0
- package/wwwroot/appsettings.json +83 -0
- package/wwwroot/assets/AdminDashboardPage-B2vz2Px9.css +1 -0
- package/wwwroot/assets/AdminDashboardPage-DnuCHywn.js +1 -0
- package/wwwroot/assets/AppSidebar-DU2OgSiv.js +2 -0
- package/wwwroot/assets/AuthPages-BrH6kRcv.css +1 -0
- package/wwwroot/assets/AuthPages-Dgezl7Vj.js +1 -0
- package/wwwroot/assets/CodePage-7kgZlB3O.js +87 -0
- package/wwwroot/assets/CodePage-Bncc352E.css +1 -0
- package/wwwroot/assets/CompanyCorePage-ChBnq1ve.css +1 -0
- package/wwwroot/assets/CompanyCorePage-CzIZIIU_.js +13 -0
- package/wwwroot/assets/ExecutionModePage-B-etp_mc.js +18 -0
- package/wwwroot/assets/ExecutionModePage-TLuld9l3.css +1 -0
- package/wwwroot/assets/LaunchLeadCapture-Bx9LM0IX.js +1 -0
- package/wwwroot/assets/LaunchLeadCapture-CiRI1shz.css +1 -0
- package/wwwroot/assets/MarketingHome-BsyerRpe.js +1 -0
- package/wwwroot/assets/MarketingHome-DPzaYzA_.css +1 -0
- package/wwwroot/assets/MindCanvas-DtqOZnoW.css +1 -0
- package/wwwroot/assets/MindCanvas-zEDXzaxW.js +49 -0
- package/wwwroot/assets/PlanMasterPage-CJ36rep-.css +1 -0
- package/wwwroot/assets/PlanMasterPage-NZ_mPvaE.js +4 -0
- package/wwwroot/assets/PricingPage-Cg_0i_ZR.css +1 -0
- package/wwwroot/assets/PricingPage-Ylrn8l2g.js +1 -0
- package/wwwroot/assets/ToolPages-3M2KqA9k.js +28 -0
- package/wwwroot/assets/ToolPages-DIB187pZ.css +1 -0
- package/wwwroot/assets/YouTubeSearchPage-COv1oAA7.js +4 -0
- package/wwwroot/assets/YouTubeSearchPage-IPPa_BIH.css +1 -0
- package/wwwroot/assets/app-runtime-xD2Z3NdN.js +1 -0
- package/wwwroot/assets/canvas-runtime-BbicBcOj.js +44 -0
- package/wwwroot/assets/code-agent-runtime-B5PPZd1t.js +74 -0
- package/wwwroot/assets/executionModeSettings-NJqurj-o.js +1 -0
- package/wwwroot/assets/index-CQMKCp-t.js +2 -0
- package/wwwroot/assets/index-yNpEK-gp.css +1 -0
- package/wwwroot/assets/marketingTools-DN_rnHeB.js +4 -0
- package/wwwroot/assets/mindCanvasSearchWorker-BzPMsHOB.js +1 -0
- package/wwwroot/assets/mindexecution-mindcanvas.png +0 -0
- package/wwwroot/assets/mindexecution-prod-home-current.png +0 -0
- package/wwwroot/assets/mindexecution-prod-pricing-current.png +0 -0
- package/wwwroot/assets/pricingCheckoutShell-O-DnwmbU.js +1 -0
- package/wwwroot/assets/productionAdapterConfig-C5jfk6oG.js +1 -0
- package/wwwroot/assets/runtimeSettingsPersistenceProjection-BoNWmYjU.js +1 -0
- package/wwwroot/assets/storage-TM3YrWaj.js +1 -0
- package/wwwroot/assets/supabaseAuthAdapter-DA43DeSY.js +44 -0
- package/wwwroot/assets/toolHandoff-D5e5f7t5.js +4 -0
- package/wwwroot/assets/vendor-icons-DE3gIReG.js +681 -0
- package/wwwroot/assets/vendor-msgpack-BE8aAsr3.js +1 -0
- package/wwwroot/assets/vendor-react-BXzpOyCS.js +40 -0
- package/wwwroot/favicon.svg +7 -0
- package/wwwroot/index.html +22 -0
- package/wwwroot/manifest.webmanifest +19 -0
- package/wwwroot/robots.txt +4 -0
- package/wwwroot/service-worker.js +7 -0
- package/wwwroot/sitemap.xml +39 -0
package/codex-runtime.js
ADDED
|
@@ -0,0 +1,1339 @@
|
|
|
1
|
+
import { promises as fs, readFileSync } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import { exec, execFile, spawn } from 'child_process';
|
|
5
|
+
import { promisify } from 'util';
|
|
6
|
+
import crypto from 'crypto';
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
|
|
11
|
+
const PROVIDER_KIND = Object.freeze({
|
|
12
|
+
fixtureReadOnly: 'FixtureReadOnly',
|
|
13
|
+
typeScriptSdk: 'TypeScriptSdk',
|
|
14
|
+
appServerJsonRpc: 'AppServerJsonRpc',
|
|
15
|
+
legacyExec: 'LegacyExec',
|
|
16
|
+
unavailable: 'Unavailable'
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const SDK_SANDBOX_BY_API_VALUE = Object.freeze({
|
|
20
|
+
read_only: 'read-only',
|
|
21
|
+
'read-only': 'read-only',
|
|
22
|
+
readonly: 'read-only',
|
|
23
|
+
workspace_write: 'workspace-write',
|
|
24
|
+
'workspace-write': 'workspace-write',
|
|
25
|
+
workspacewrite: 'workspace-write',
|
|
26
|
+
full_access: 'danger-full-access',
|
|
27
|
+
fullaccess: 'danger-full-access',
|
|
28
|
+
danger_full_access: 'danger-full-access',
|
|
29
|
+
'danger-full-access': 'danger-full-access'
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const API_SANDBOX_BY_SDK_VALUE = Object.freeze({
|
|
33
|
+
'read-only': 'read_only',
|
|
34
|
+
'workspace-write': 'workspace_write',
|
|
35
|
+
'danger-full-access': 'full_access'
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const DEFAULT_TIMEOUT_MS = 8 * 60 * 1000;
|
|
39
|
+
const MAX_LOG_CHARS = 4000;
|
|
40
|
+
const MAX_EVENT_LOG = 120;
|
|
41
|
+
const TEMP_DIR = '.ai/codex';
|
|
42
|
+
const CODEX_CONFIG_PATH = path.join(os.homedir(), '.codex', 'config.toml');
|
|
43
|
+
const CODEX_SOURCE_HOME = path.join(os.homedir(), '.codex');
|
|
44
|
+
const CODEX_RUNTIME_HOME_ENV = 'MINDEXEC_CODEX_HOME';
|
|
45
|
+
const CODEX_PATH_ENV = 'MINDEXEC_CODEX_PATH';
|
|
46
|
+
const CODEX_USE_SOURCE_HOME_ENV = 'MINDEXEC_CODEX_USE_SOURCE_HOME';
|
|
47
|
+
const DEFAULT_CODEX_RUNTIME_HOME = path.join(os.homedir(), '.mindexec', 'codex-runtime');
|
|
48
|
+
const CODEX_RUNTIME_CONFIG_MARKER = '# Generated by MindExec LocalBridge for isolated AI node runs.';
|
|
49
|
+
const CODEX_RUNTIME_AUTH_FILES = ['auth.json'];
|
|
50
|
+
|
|
51
|
+
let cachedSdkModule = null;
|
|
52
|
+
let cachedSdkLoadError = null;
|
|
53
|
+
let cachedLegacyAvailability = null;
|
|
54
|
+
let cachedLegacyAvailabilityExpiresAt = 0;
|
|
55
|
+
|
|
56
|
+
function safeReadPackageVersion(packageRoot, packageName) {
|
|
57
|
+
try {
|
|
58
|
+
const packageJsonPath = path.join(packageRoot, 'node_modules', ...packageName.split('/'), 'package.json');
|
|
59
|
+
const parsed = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
60
|
+
return parsed.version || null;
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function loadCodexSdk() {
|
|
67
|
+
if (cachedSdkModule) {
|
|
68
|
+
return cachedSdkModule;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
cachedSdkModule = await import('@openai/codex-sdk');
|
|
73
|
+
cachedSdkLoadError = null;
|
|
74
|
+
return cachedSdkModule;
|
|
75
|
+
} catch (err) {
|
|
76
|
+
cachedSdkLoadError = err?.message || String(err);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeProviderKind(value) {
|
|
82
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
83
|
+
if (!normalized || normalized === 'auto' || normalized === 'sdk') {
|
|
84
|
+
return '';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (normalized === 'fixture' || normalized === 'fixturereadonly' || normalized === 'fixture-readonly' || normalized === 'fixture_readonly' || normalized === 'test-fixture') {
|
|
88
|
+
return PROVIDER_KIND.fixtureReadOnly;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (normalized === 'typescriptsdk' || normalized === 'typescript-sdk' || normalized === 'type_script_sdk') {
|
|
92
|
+
return PROVIDER_KIND.typeScriptSdk;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (normalized === 'legacyexec' || normalized === 'legacy-exec' || normalized === 'exec') {
|
|
96
|
+
return PROVIDER_KIND.legacyExec;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (normalized === 'appserverjsonrpc' || normalized === 'app-server-json-rpc') {
|
|
100
|
+
return PROVIDER_KIND.appServerJsonRpc;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return '';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isEnabledEnv(value) {
|
|
107
|
+
return /^(1|true|yes|on)$/i.test(String(value || '').trim());
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isFixtureProviderEnabled() {
|
|
111
|
+
return isEnabledEnv(process.env.MINDEXEC_CODEX_FIXTURE_PROVIDER);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function unquoteTomlKey(value) {
|
|
115
|
+
const text = String(value || '').trim();
|
|
116
|
+
if (text.length >= 2 && ((text.startsWith('"') && text.endsWith('"')) || (text.startsWith("'") && text.endsWith("'")))) {
|
|
117
|
+
return text.slice(1, -1).replace(/\\(["\\])/g, '$1').trim();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return text;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function readConfiguredMcpServerNames() {
|
|
124
|
+
if (!isEnabledEnv(process.env.MINDEXEC_CODEX_DISABLE_SOURCE_MCP)) {
|
|
125
|
+
return [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const names = new Set();
|
|
129
|
+
try {
|
|
130
|
+
const content = readFileSync(CODEX_CONFIG_PATH, 'utf8');
|
|
131
|
+
for (const rawLine of content.split(/\r?\n/)) {
|
|
132
|
+
const line = rawLine.trim();
|
|
133
|
+
const match = line.match(/^\[mcp_servers\.((?:"(?:[^"\\]|\\.)+"|'(?:[^'\\]|\\.)+'|[^\]\s]+))\]$/);
|
|
134
|
+
if (!match) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const name = unquoteTomlKey(match[1]);
|
|
139
|
+
if (/^[A-Za-z0-9_-]+$/.test(name)) {
|
|
140
|
+
names.add(name);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} catch {
|
|
144
|
+
// Missing Codex config is fine; SDK auth still reports its own error if login is required.
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (const name of String(process.env.MINDEXEC_CODEX_DISABLED_MCP_SERVERS || 'cloudflare,supabase')
|
|
148
|
+
.split(',')
|
|
149
|
+
.map(item => item.trim())
|
|
150
|
+
.filter(Boolean)) {
|
|
151
|
+
if (/^[A-Za-z0-9_-]+$/.test(name)) {
|
|
152
|
+
names.add(name);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return Array.from(names).sort();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function buildCodexSdkConfigOverrides() {
|
|
160
|
+
const mcpServerNames = readConfiguredMcpServerNames();
|
|
161
|
+
if (mcpServerNames.length === 0) {
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const mcpServers = {};
|
|
166
|
+
for (const name of mcpServerNames) {
|
|
167
|
+
mcpServers[name] = { enabled: false };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return { mcp_servers: mcpServers };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function resolveCodexRuntimeHome() {
|
|
174
|
+
if (isEnabledEnv(process.env[CODEX_USE_SOURCE_HOME_ENV])) {
|
|
175
|
+
return CODEX_SOURCE_HOME;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const configured = String(process.env[CODEX_RUNTIME_HOME_ENV] || '').trim();
|
|
179
|
+
return path.resolve(configured || DEFAULT_CODEX_RUNTIME_HOME);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isUsingSourceCodexHome() {
|
|
183
|
+
return path.resolve(resolveCodexRuntimeHome()).toLowerCase() === path.resolve(CODEX_SOURCE_HOME).toLowerCase();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function resolveCodexPathOverride() {
|
|
187
|
+
const configured = String(process.env[CODEX_PATH_ENV] || '').trim();
|
|
188
|
+
return configured ? path.resolve(configured) : null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function copyCodexRuntimeFileIfPresent(fileName, runtimeHome) {
|
|
192
|
+
const source = path.join(CODEX_SOURCE_HOME, fileName);
|
|
193
|
+
const target = path.join(runtimeHome, fileName);
|
|
194
|
+
try {
|
|
195
|
+
const sourceStat = await fs.stat(source);
|
|
196
|
+
if (!sourceStat.isFile()) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
await fs.copyFile(source, target);
|
|
201
|
+
return true;
|
|
202
|
+
} catch {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function buildCodexRuntimeConfig(trustedDirectory = '') {
|
|
208
|
+
const lines = [
|
|
209
|
+
CODEX_RUNTIME_CONFIG_MARKER,
|
|
210
|
+
'# User MCP server definitions are intentionally not inherited here.',
|
|
211
|
+
'# LocalBridge passes model, sandbox, and reasoning options per run.'
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
const resolvedTrustedDirectory = String(trustedDirectory || '').trim()
|
|
215
|
+
? path.resolve(trustedDirectory)
|
|
216
|
+
: '';
|
|
217
|
+
if (resolvedTrustedDirectory) {
|
|
218
|
+
lines.push(
|
|
219
|
+
'',
|
|
220
|
+
`[projects.${JSON.stringify(resolvedTrustedDirectory)}]`,
|
|
221
|
+
'trust_level = "trusted"'
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return `${lines.join('\n')}\n`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function ensureCodexRuntimeHome(trustedDirectory = '') {
|
|
229
|
+
const runtimeHome = resolveCodexRuntimeHome();
|
|
230
|
+
if (isUsingSourceCodexHome()) {
|
|
231
|
+
return runtimeHome;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
await fs.mkdir(runtimeHome, { recursive: true });
|
|
235
|
+
await fs.mkdir(path.join(runtimeHome, 'sessions'), { recursive: true });
|
|
236
|
+
await fs.mkdir(path.join(runtimeHome, 'generated_images'), { recursive: true });
|
|
237
|
+
|
|
238
|
+
for (const fileName of CODEX_RUNTIME_AUTH_FILES) {
|
|
239
|
+
await copyCodexRuntimeFileIfPresent(fileName, runtimeHome);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const configPath = path.join(runtimeHome, 'config.toml');
|
|
243
|
+
let shouldWriteConfig = true;
|
|
244
|
+
try {
|
|
245
|
+
const existing = await fs.readFile(configPath, 'utf8');
|
|
246
|
+
shouldWriteConfig = existing.trim().length === 0 || existing.includes(CODEX_RUNTIME_CONFIG_MARKER);
|
|
247
|
+
} catch {
|
|
248
|
+
shouldWriteConfig = true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (shouldWriteConfig) {
|
|
252
|
+
await fs.writeFile(configPath, buildCodexRuntimeConfig(trustedDirectory), 'utf8');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return runtimeHome;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function buildCodexChildEnv() {
|
|
259
|
+
const env = {};
|
|
260
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
261
|
+
if (value !== undefined) {
|
|
262
|
+
env[key] = value;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
env.CODEX_HOME = resolveCodexRuntimeHome();
|
|
267
|
+
if (isUsingSourceCodexHome()) {
|
|
268
|
+
delete env.MINDEXEC_CODEX_ISOLATED_HOME;
|
|
269
|
+
} else {
|
|
270
|
+
env.MINDEXEC_CODEX_ISOLATED_HOME = '1';
|
|
271
|
+
}
|
|
272
|
+
return env;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function createCodexSdkClient(sdk) {
|
|
276
|
+
return new sdk.Codex({
|
|
277
|
+
codexPathOverride: resolveCodexPathOverride(),
|
|
278
|
+
env: buildCodexChildEnv(),
|
|
279
|
+
config: buildCodexSdkConfigOverrides()
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function appendCodexIsolationConfigArgs(args) {
|
|
284
|
+
for (const name of readConfiguredMcpServerNames()) {
|
|
285
|
+
args.push('--config', `mcp_servers.${name}.enabled=false`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function normalizeReasoningEffort(value) {
|
|
290
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
291
|
+
return ['minimal', 'low', 'medium', 'high', 'xhigh'].includes(normalized)
|
|
292
|
+
? normalized
|
|
293
|
+
: 'medium';
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function inferSandboxFromIntent(intent, prompt) {
|
|
297
|
+
const text = `${intent || ''}\n${prompt || ''}`.toLowerCase();
|
|
298
|
+
if (/(full[_ -]?access|unrestricted|danger[_ -]?full)/.test(text)) {
|
|
299
|
+
return 'workspace-write';
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (/(code[-_\s]?edit|project[-_\s]?fix|generate[-_\s]?files|implement|modify files|write files|patch|apply fix|refactor)/.test(text)) {
|
|
303
|
+
return 'workspace-write';
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (/(research|review|analy[sz]e|summari[sz]e|diagnose|inspect|audit|compare|read[-_\s]?only)/.test(text)) {
|
|
307
|
+
return 'read-only';
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return 'workspace-write';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function normalizeSandbox(value, intent, prompt) {
|
|
314
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
315
|
+
if (normalized && SDK_SANDBOX_BY_API_VALUE[normalized]) {
|
|
316
|
+
return SDK_SANDBOX_BY_API_VALUE[normalized];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return inferSandboxFromIntent(intent, prompt);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function normalizeApprovalPolicy(value, sandboxMode) {
|
|
323
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
324
|
+
if (['on-request', 'on-failure', 'untrusted'].includes(normalized)) {
|
|
325
|
+
return normalized;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (normalized === 'never' && sandboxMode !== 'danger-full-access') {
|
|
329
|
+
return 'never';
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return 'on-request';
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function isPathInside(parentPath, candidatePath) {
|
|
336
|
+
const relativePath = path.relative(path.resolve(parentPath), path.resolve(candidatePath));
|
|
337
|
+
return relativePath === '' || (relativePath && !relativePath.startsWith('..') && !path.isAbsolute(relativePath));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function normalizeAdditionalDirectories(value, workingDirectory, sandboxMode) {
|
|
341
|
+
const resolvedWorkingDirectory = path.resolve(workingDirectory);
|
|
342
|
+
const candidates = sandboxMode === 'workspace-write'
|
|
343
|
+
? [resolvedWorkingDirectory]
|
|
344
|
+
: [];
|
|
345
|
+
|
|
346
|
+
if (Array.isArray(value)) {
|
|
347
|
+
for (const item of value) {
|
|
348
|
+
const raw = String(item || '').trim();
|
|
349
|
+
if (!raw) {
|
|
350
|
+
continue;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const resolved = path.resolve(path.isAbsolute(raw)
|
|
354
|
+
? raw
|
|
355
|
+
: path.join(resolvedWorkingDirectory, raw));
|
|
356
|
+
candidates.push(resolved);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const normalized = [];
|
|
361
|
+
for (const candidate of candidates) {
|
|
362
|
+
if (!isPathInside(resolvedWorkingDirectory, candidate)) {
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (!normalized.includes(candidate)) {
|
|
367
|
+
normalized.push(candidate);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return normalized;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function normalizeOutputSchema(value) {
|
|
375
|
+
if (value == null) {
|
|
376
|
+
return undefined;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (typeof value === 'object') {
|
|
380
|
+
return value;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const text = String(value || '').trim();
|
|
384
|
+
if (!text) {
|
|
385
|
+
return undefined;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return JSON.parse(text);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function truncateLog(value, maxChars = MAX_LOG_CHARS) {
|
|
392
|
+
const text = String(value ?? '');
|
|
393
|
+
if (text.length <= maxChars) {
|
|
394
|
+
return text;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return `${text.slice(0, maxChars)}...`;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function makeEmptyResult(overrides = {}) {
|
|
401
|
+
const now = new Date().toISOString();
|
|
402
|
+
return {
|
|
403
|
+
success: false,
|
|
404
|
+
providerKind: PROVIDER_KIND.unavailable,
|
|
405
|
+
threadId: '',
|
|
406
|
+
turnId: '',
|
|
407
|
+
finalResponse: '',
|
|
408
|
+
summary: '',
|
|
409
|
+
error: '',
|
|
410
|
+
status: 'failed',
|
|
411
|
+
startedAtUtc: now,
|
|
412
|
+
completedAtUtc: now,
|
|
413
|
+
sandboxMode: '',
|
|
414
|
+
changedFiles: [],
|
|
415
|
+
commandLogs: [],
|
|
416
|
+
approvalEvents: [],
|
|
417
|
+
rawEventCount: 0,
|
|
418
|
+
...overrides
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function collectItem(result, item) {
|
|
423
|
+
if (!item || typeof item !== 'object') {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (item.type === 'agent_message' && typeof item.text === 'string') {
|
|
428
|
+
result.finalResponse = item.text;
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (item.type === 'reasoning' && typeof item.text === 'string') {
|
|
433
|
+
result.summary = result.summary
|
|
434
|
+
? `${result.summary}\n${item.text}`.trim()
|
|
435
|
+
: item.text.trim();
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (item.type === 'file_change' && Array.isArray(item.changes)) {
|
|
440
|
+
for (const change of item.changes) {
|
|
441
|
+
const changePath = String(change?.path || '').trim();
|
|
442
|
+
if (!changePath) {
|
|
443
|
+
continue;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (!result.changedFiles.some(existing => existing.path === changePath)) {
|
|
447
|
+
result.changedFiles.push({
|
|
448
|
+
path: changePath,
|
|
449
|
+
kind: String(change?.kind || '').trim(),
|
|
450
|
+
status: String(item.status || '').trim()
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (item.type === 'command_execution') {
|
|
458
|
+
result.commandLogs.push({
|
|
459
|
+
id: String(item.id || ''),
|
|
460
|
+
command: String(item.command || ''),
|
|
461
|
+
status: String(item.status || ''),
|
|
462
|
+
exitCode: Number.isFinite(Number(item.exit_code)) ? Number(item.exit_code) : null,
|
|
463
|
+
output: truncateLog(item.aggregated_output)
|
|
464
|
+
});
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (String(item.type || '').toLowerCase().includes('approval')) {
|
|
469
|
+
result.approvalEvents.push({
|
|
470
|
+
id: String(item.id || ''),
|
|
471
|
+
type: String(item.type || ''),
|
|
472
|
+
status: String(item.status || ''),
|
|
473
|
+
summary: truncateLog(JSON.stringify(item), 1200)
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function extractLegacyEvents(stdout, stderr) {
|
|
479
|
+
const result = {
|
|
480
|
+
finalResponse: '',
|
|
481
|
+
summary: '',
|
|
482
|
+
error: '',
|
|
483
|
+
threadId: '',
|
|
484
|
+
changedFiles: [],
|
|
485
|
+
commandLogs: [],
|
|
486
|
+
approvalEvents: [],
|
|
487
|
+
rawEventCount: 0
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
const lines = String(stdout || '')
|
|
491
|
+
.replace(/\r\n/g, '\n')
|
|
492
|
+
.replace(/\r/g, '\n')
|
|
493
|
+
.split('\n')
|
|
494
|
+
.filter(line => line.trim().length > 0);
|
|
495
|
+
|
|
496
|
+
for (const line of lines) {
|
|
497
|
+
let event;
|
|
498
|
+
try {
|
|
499
|
+
event = JSON.parse(line);
|
|
500
|
+
} catch {
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
result.rawEventCount += 1;
|
|
505
|
+
if (event.type === 'thread.started' && event.thread_id) {
|
|
506
|
+
result.threadId = String(event.thread_id);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (event.type === 'turn.failed' && event.error?.message) {
|
|
510
|
+
result.error = String(event.error.message);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (event.type === 'error' && event.message) {
|
|
514
|
+
result.error = String(event.message);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const item = event.item;
|
|
518
|
+
if (item?.type === 'agent_message' && typeof item.text === 'string') {
|
|
519
|
+
result.finalResponse = item.text;
|
|
520
|
+
} else if (item?.type === 'reasoning' && typeof item.text === 'string') {
|
|
521
|
+
result.summary = result.summary
|
|
522
|
+
? `${result.summary}\n${item.text}`.trim()
|
|
523
|
+
: item.text.trim();
|
|
524
|
+
} else if (item?.type === 'file_change' && Array.isArray(item.changes)) {
|
|
525
|
+
for (const change of item.changes) {
|
|
526
|
+
const changePath = String(change?.path || '').trim();
|
|
527
|
+
if (changePath) {
|
|
528
|
+
result.changedFiles.push({
|
|
529
|
+
path: changePath,
|
|
530
|
+
kind: String(change?.kind || ''),
|
|
531
|
+
status: String(item.status || '')
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
} else if (item?.type === 'command_execution') {
|
|
536
|
+
result.commandLogs.push({
|
|
537
|
+
id: String(item.id || ''),
|
|
538
|
+
command: String(item.command || ''),
|
|
539
|
+
status: String(item.status || ''),
|
|
540
|
+
exitCode: Number.isFinite(Number(item.exit_code)) ? Number(item.exit_code) : null,
|
|
541
|
+
output: truncateLog(item.aggregated_output)
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (!result.finalResponse && lines.length > 0) {
|
|
547
|
+
result.finalResponse = String(stdout || '').trim();
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (!result.error && stderr) {
|
|
551
|
+
result.error = String(stderr).trim();
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return result;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function buildThreadOptions(body, workingDirectory) {
|
|
558
|
+
const prompt = String(body?.instruction || body?.prompt || '').trim();
|
|
559
|
+
const sandboxMode = normalizeSandbox(body?.sandbox || body?.sandboxMode, body?.intent || body?.nodeKind, prompt);
|
|
560
|
+
const approvalPolicy = normalizeApprovalPolicy(body?.approvalPolicy, sandboxMode);
|
|
561
|
+
const additionalDirectories = normalizeAdditionalDirectories(body?.additionalDirectories, workingDirectory, sandboxMode);
|
|
562
|
+
const options = {
|
|
563
|
+
workingDirectory,
|
|
564
|
+
skipGitRepoCheck: true,
|
|
565
|
+
sandboxMode,
|
|
566
|
+
modelReasoningEffort: normalizeReasoningEffort(body?.reasoningEffort),
|
|
567
|
+
approvalPolicy
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
if (additionalDirectories.length > 0) {
|
|
571
|
+
options.additionalDirectories = additionalDirectories;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const model = String(body?.model || '').trim();
|
|
575
|
+
if (model) {
|
|
576
|
+
options.model = model;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (body?.networkAccessEnabled !== undefined) {
|
|
580
|
+
options.networkAccessEnabled = Boolean(body.networkAccessEnabled);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const webSearchMode = String(body?.webSearchMode || '').trim().toLowerCase();
|
|
584
|
+
if (['disabled', 'cached', 'live'].includes(webSearchMode)) {
|
|
585
|
+
options.webSearchMode = webSearchMode;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return options;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function serializeThreadOptionsForStatus(options) {
|
|
592
|
+
return {
|
|
593
|
+
model: options?.model || null,
|
|
594
|
+
sandboxMode: API_SANDBOX_BY_SDK_VALUE[options?.sandboxMode] || options?.sandboxMode || null,
|
|
595
|
+
reasoningEffort: options?.modelReasoningEffort || null,
|
|
596
|
+
approvalPolicy: options?.approvalPolicy || null,
|
|
597
|
+
workingDirectory: options?.workingDirectory || null,
|
|
598
|
+
additionalDirectories: Array.isArray(options?.additionalDirectories)
|
|
599
|
+
? [...options.additionalDirectories]
|
|
600
|
+
: [],
|
|
601
|
+
networkAccessEnabled: options?.networkAccessEnabled === undefined ? null : Boolean(options.networkAccessEnabled),
|
|
602
|
+
webSearchMode: options?.webSearchMode || null
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
async function checkSdkAvailability(packageRoot) {
|
|
607
|
+
const sdk = await loadCodexSdk();
|
|
608
|
+
return {
|
|
609
|
+
kind: PROVIDER_KIND.typeScriptSdk,
|
|
610
|
+
available: Boolean(sdk?.Codex),
|
|
611
|
+
version: safeReadPackageVersion(packageRoot, '@openai/codex-sdk'),
|
|
612
|
+
error: sdk?.Codex ? null : (cachedSdkLoadError || 'Codex SDK module did not export Codex.')
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
async function checkLegacyAvailability() {
|
|
617
|
+
const now = Date.now();
|
|
618
|
+
if (cachedLegacyAvailability && cachedLegacyAvailabilityExpiresAt > now) {
|
|
619
|
+
return cachedLegacyAvailability;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
try {
|
|
623
|
+
const { stdout, stderr } = await execFileAsync(resolveCodexPathOverride() || 'codex', ['--version'], {
|
|
624
|
+
timeout: 6000,
|
|
625
|
+
windowsHide: true,
|
|
626
|
+
maxBuffer: 512 * 1024
|
|
627
|
+
});
|
|
628
|
+
cachedLegacyAvailability = {
|
|
629
|
+
kind: PROVIDER_KIND.legacyExec,
|
|
630
|
+
available: true,
|
|
631
|
+
version: String(stdout || stderr || '').trim(),
|
|
632
|
+
error: null
|
|
633
|
+
};
|
|
634
|
+
} catch (err) {
|
|
635
|
+
cachedLegacyAvailability = {
|
|
636
|
+
kind: PROVIDER_KIND.legacyExec,
|
|
637
|
+
available: false,
|
|
638
|
+
version: null,
|
|
639
|
+
error: err?.message || String(err)
|
|
640
|
+
};
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
cachedLegacyAvailabilityExpiresAt = now + 30 * 1000;
|
|
644
|
+
return cachedLegacyAvailability;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function extractPrompt(body) {
|
|
648
|
+
const prompt = String(body?.instruction || body?.prompt || '').trim();
|
|
649
|
+
if (!prompt) {
|
|
650
|
+
const err = new Error('Codex prompt is required.');
|
|
651
|
+
err.statusCode = 400;
|
|
652
|
+
throw err;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
return prompt;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
export function createCodexRuntime(options) {
|
|
659
|
+
const packageRoot = options.packageRoot;
|
|
660
|
+
const resolveWorkingDirectory = options.resolveWorkingDirectory;
|
|
661
|
+
const getCurrentRuntime = options.getCurrentRuntime;
|
|
662
|
+
const getModelCatalog = options.getModelCatalog;
|
|
663
|
+
const log = typeof options.log === 'function' ? options.log : () => {};
|
|
664
|
+
|
|
665
|
+
const threads = new Map();
|
|
666
|
+
const activeTurns = new Map();
|
|
667
|
+
|
|
668
|
+
async function getCapabilities() {
|
|
669
|
+
const fixtureEnabled = isFixtureProviderEnabled();
|
|
670
|
+
const [sdk, legacy, modelCatalog] = await Promise.all([
|
|
671
|
+
checkSdkAvailability(packageRoot),
|
|
672
|
+
checkLegacyAvailability(),
|
|
673
|
+
getModelCatalog()
|
|
674
|
+
]);
|
|
675
|
+
const fixture = {
|
|
676
|
+
kind: PROVIDER_KIND.fixtureReadOnly,
|
|
677
|
+
available: fixtureEnabled,
|
|
678
|
+
implemented: true,
|
|
679
|
+
version: fixtureEnabled ? 'local-smoke' : null,
|
|
680
|
+
error: fixtureEnabled ? null : 'Enable MINDEXEC_CODEX_FIXTURE_PROVIDER=1 for local contract smokes only.'
|
|
681
|
+
};
|
|
682
|
+
const preferredProviderKind = fixtureEnabled
|
|
683
|
+
? PROVIDER_KIND.fixtureReadOnly
|
|
684
|
+
: sdk.available
|
|
685
|
+
? PROVIDER_KIND.typeScriptSdk
|
|
686
|
+
: legacy.available
|
|
687
|
+
? PROVIDER_KIND.legacyExec
|
|
688
|
+
: PROVIDER_KIND.unavailable;
|
|
689
|
+
|
|
690
|
+
return {
|
|
691
|
+
success: preferredProviderKind !== PROVIDER_KIND.unavailable,
|
|
692
|
+
preferredProviderKind,
|
|
693
|
+
providers: [
|
|
694
|
+
fixture,
|
|
695
|
+
sdk,
|
|
696
|
+
{
|
|
697
|
+
kind: PROVIDER_KIND.appServerJsonRpc,
|
|
698
|
+
available: false,
|
|
699
|
+
implemented: false,
|
|
700
|
+
error: 'DTO scaffold only. Use TypeScriptSdk for LocalBridge execution.'
|
|
701
|
+
},
|
|
702
|
+
legacy
|
|
703
|
+
],
|
|
704
|
+
sandboxModes: ['read_only', 'workspace_write', 'full_access'],
|
|
705
|
+
endpoints: [
|
|
706
|
+
'GET /api/codex/capabilities',
|
|
707
|
+
'POST /api/codex/thread/start',
|
|
708
|
+
'POST /api/codex/thread/run',
|
|
709
|
+
'POST /api/codex/thread/resume',
|
|
710
|
+
'POST /api/codex/thread/cancel',
|
|
711
|
+
'GET /api/codex/thread/{threadId}/status'
|
|
712
|
+
],
|
|
713
|
+
runtime: getCurrentRuntime(),
|
|
714
|
+
models: modelCatalog.models || [],
|
|
715
|
+
modelCatalogSource: modelCatalog.source,
|
|
716
|
+
modelCatalogUpdatedAt: modelCatalog.updatedAt,
|
|
717
|
+
modelCatalogError: modelCatalog.error || null
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
async function resolveProvider(requestedProviderKind) {
|
|
722
|
+
const requested = normalizeProviderKind(requestedProviderKind);
|
|
723
|
+
if (isFixtureProviderEnabled() && (!requested || requested === PROVIDER_KIND.fixtureReadOnly)) {
|
|
724
|
+
return PROVIDER_KIND.fixtureReadOnly;
|
|
725
|
+
}
|
|
726
|
+
if (requested === PROVIDER_KIND.fixtureReadOnly) {
|
|
727
|
+
return PROVIDER_KIND.unavailable;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const sdk = await checkSdkAvailability(packageRoot);
|
|
731
|
+
const legacy = requested === PROVIDER_KIND.typeScriptSdk
|
|
732
|
+
? null
|
|
733
|
+
: await checkLegacyAvailability();
|
|
734
|
+
|
|
735
|
+
if ((!requested || requested === PROVIDER_KIND.typeScriptSdk) && sdk.available) {
|
|
736
|
+
return PROVIDER_KIND.typeScriptSdk;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if ((!requested || requested === PROVIDER_KIND.legacyExec) && legacy?.available) {
|
|
740
|
+
return PROVIDER_KIND.legacyExec;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
const fallbackLegacy = legacy || await checkLegacyAvailability();
|
|
744
|
+
if (requested === PROVIDER_KIND.typeScriptSdk && fallbackLegacy?.available) {
|
|
745
|
+
return PROVIDER_KIND.legacyExec;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
return PROVIDER_KIND.unavailable;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async function startThread(body = {}) {
|
|
752
|
+
const providerKind = await resolveProvider(body.providerKind || body.runtime);
|
|
753
|
+
const workingDirectory = await resolveWorkingDirectory(body.workingDir || body.workingDirectory || '');
|
|
754
|
+
const threadOptions = buildThreadOptions(body, workingDirectory);
|
|
755
|
+
const now = new Date().toISOString();
|
|
756
|
+
|
|
757
|
+
if (providerKind === PROVIDER_KIND.fixtureReadOnly) {
|
|
758
|
+
const localThreadId = `fixture_${crypto.randomUUID()}`;
|
|
759
|
+
threads.set(localThreadId, {
|
|
760
|
+
providerKind,
|
|
761
|
+
createdAtUtc: now,
|
|
762
|
+
updatedAtUtc: now,
|
|
763
|
+
options: threadOptions,
|
|
764
|
+
officialThreadId: localThreadId,
|
|
765
|
+
lastStatus: 'created'
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
return makeEmptyResult({
|
|
769
|
+
success: true,
|
|
770
|
+
providerKind,
|
|
771
|
+
threadId: localThreadId,
|
|
772
|
+
status: 'created',
|
|
773
|
+
startedAtUtc: now,
|
|
774
|
+
completedAtUtc: now,
|
|
775
|
+
sandboxMode: API_SANDBOX_BY_SDK_VALUE[threadOptions.sandboxMode] || threadOptions.sandboxMode
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (providerKind === PROVIDER_KIND.typeScriptSdk) {
|
|
780
|
+
const sdk = await loadCodexSdk();
|
|
781
|
+
await ensureCodexRuntimeHome(workingDirectory);
|
|
782
|
+
const codex = createCodexSdkClient(sdk);
|
|
783
|
+
const thread = codex.startThread(threadOptions);
|
|
784
|
+
const localThreadId = `local_${crypto.randomUUID()}`;
|
|
785
|
+
threads.set(localThreadId, {
|
|
786
|
+
providerKind,
|
|
787
|
+
thread,
|
|
788
|
+
createdAtUtc: now,
|
|
789
|
+
updatedAtUtc: now,
|
|
790
|
+
options: threadOptions,
|
|
791
|
+
officialThreadId: null,
|
|
792
|
+
lastStatus: 'created'
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
return makeEmptyResult({
|
|
796
|
+
success: true,
|
|
797
|
+
providerKind,
|
|
798
|
+
threadId: localThreadId,
|
|
799
|
+
status: 'created',
|
|
800
|
+
startedAtUtc: now,
|
|
801
|
+
completedAtUtc: now,
|
|
802
|
+
sandboxMode: API_SANDBOX_BY_SDK_VALUE[threadOptions.sandboxMode] || threadOptions.sandboxMode
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (providerKind === PROVIDER_KIND.legacyExec) {
|
|
807
|
+
const localThreadId = `legacy_${crypto.randomUUID()}`;
|
|
808
|
+
threads.set(localThreadId, {
|
|
809
|
+
providerKind,
|
|
810
|
+
createdAtUtc: now,
|
|
811
|
+
updatedAtUtc: now,
|
|
812
|
+
options: threadOptions,
|
|
813
|
+
officialThreadId: localThreadId,
|
|
814
|
+
lastStatus: 'created'
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
return makeEmptyResult({
|
|
818
|
+
success: true,
|
|
819
|
+
providerKind,
|
|
820
|
+
threadId: localThreadId,
|
|
821
|
+
status: 'created',
|
|
822
|
+
startedAtUtc: now,
|
|
823
|
+
completedAtUtc: now,
|
|
824
|
+
sandboxMode: API_SANDBOX_BY_SDK_VALUE[threadOptions.sandboxMode] || threadOptions.sandboxMode
|
|
825
|
+
});
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
return makeEmptyResult({
|
|
829
|
+
providerKind,
|
|
830
|
+
error: 'Codex local runtime is not available. Install LocalBridge dependencies or Codex CLI, then run codex login.'
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
async function getSdkThread(body, threadOptions) {
|
|
835
|
+
const requestedThreadId = String(body?.threadId || '').trim();
|
|
836
|
+
if (requestedThreadId && threads.has(requestedThreadId)) {
|
|
837
|
+
return {
|
|
838
|
+
key: requestedThreadId,
|
|
839
|
+
record: threads.get(requestedThreadId)
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const sdk = await loadCodexSdk();
|
|
844
|
+
await ensureCodexRuntimeHome(threadOptions?.workingDirectory);
|
|
845
|
+
const codex = createCodexSdkClient(sdk);
|
|
846
|
+
const officialId = requestedThreadId && !requestedThreadId.startsWith('local_')
|
|
847
|
+
? requestedThreadId
|
|
848
|
+
: '';
|
|
849
|
+
const thread = officialId
|
|
850
|
+
? codex.resumeThread(officialId, threadOptions)
|
|
851
|
+
: codex.startThread(threadOptions);
|
|
852
|
+
const key = officialId || `local_${crypto.randomUUID()}`;
|
|
853
|
+
const record = {
|
|
854
|
+
providerKind: PROVIDER_KIND.typeScriptSdk,
|
|
855
|
+
thread,
|
|
856
|
+
createdAtUtc: new Date().toISOString(),
|
|
857
|
+
updatedAtUtc: new Date().toISOString(),
|
|
858
|
+
options: threadOptions,
|
|
859
|
+
officialThreadId: officialId || null,
|
|
860
|
+
lastStatus: 'created'
|
|
861
|
+
};
|
|
862
|
+
threads.set(key, record);
|
|
863
|
+
return { key, record };
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
async function runSdkThread(body, workingDirectory, prompt, startedAtUtc) {
|
|
867
|
+
const threadOptions = buildThreadOptions(body, workingDirectory);
|
|
868
|
+
const { key, record } = await getSdkThread(body, threadOptions);
|
|
869
|
+
const turnId = `turn_${crypto.randomUUID()}`;
|
|
870
|
+
const abortController = new AbortController();
|
|
871
|
+
const activeKey = `${key}:${turnId}`;
|
|
872
|
+
activeTurns.set(activeKey, {
|
|
873
|
+
providerKind: PROVIDER_KIND.typeScriptSdk,
|
|
874
|
+
threadId: key,
|
|
875
|
+
turnId,
|
|
876
|
+
abortController,
|
|
877
|
+
startedAtUtc
|
|
878
|
+
});
|
|
879
|
+
|
|
880
|
+
const result = makeEmptyResult({
|
|
881
|
+
providerKind: PROVIDER_KIND.typeScriptSdk,
|
|
882
|
+
threadId: record.officialThreadId || key,
|
|
883
|
+
turnId,
|
|
884
|
+
status: 'running',
|
|
885
|
+
startedAtUtc,
|
|
886
|
+
completedAtUtc: '',
|
|
887
|
+
sandboxMode: API_SANDBOX_BY_SDK_VALUE[threadOptions.sandboxMode] || threadOptions.sandboxMode
|
|
888
|
+
});
|
|
889
|
+
let timeout = null;
|
|
890
|
+
|
|
891
|
+
try {
|
|
892
|
+
timeout = setTimeout(() => {
|
|
893
|
+
try {
|
|
894
|
+
abortController.abort();
|
|
895
|
+
} catch {
|
|
896
|
+
// Ignore duplicate aborts.
|
|
897
|
+
}
|
|
898
|
+
}, DEFAULT_TIMEOUT_MS);
|
|
899
|
+
|
|
900
|
+
log('Codex SDK run start', {
|
|
901
|
+
threadId: result.threadId,
|
|
902
|
+
turnId,
|
|
903
|
+
sandbox: result.sandboxMode,
|
|
904
|
+
cwd: workingDirectory
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
const outputSchema = normalizeOutputSchema(body.outputSchema || body.outputSchemaJson);
|
|
908
|
+
const { events } = await record.thread.runStreamed(prompt, {
|
|
909
|
+
outputSchema,
|
|
910
|
+
signal: abortController.signal
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
for await (const event of events) {
|
|
914
|
+
result.rawEventCount += 1;
|
|
915
|
+
if (result.rawEventCount <= MAX_EVENT_LOG) {
|
|
916
|
+
record.lastEvent = event;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
if (event.type === 'thread.started' && event.thread_id) {
|
|
920
|
+
record.officialThreadId = String(event.thread_id);
|
|
921
|
+
result.threadId = record.officialThreadId;
|
|
922
|
+
threads.set(record.officialThreadId, record);
|
|
923
|
+
} else if (event.type === 'item.completed' || event.type === 'item.updated' || event.type === 'item.started') {
|
|
924
|
+
collectItem(result, event.item);
|
|
925
|
+
} else if (event.type === 'turn.failed') {
|
|
926
|
+
result.error = event.error?.message || 'Codex turn failed.';
|
|
927
|
+
result.status = 'failed';
|
|
928
|
+
} else if (event.type === 'error') {
|
|
929
|
+
result.error = event.message || 'Codex stream failed.';
|
|
930
|
+
result.status = 'failed';
|
|
931
|
+
} else if (event.type === 'turn.completed') {
|
|
932
|
+
result.status = 'completed';
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (!result.error && result.status !== 'failed') {
|
|
937
|
+
result.success = true;
|
|
938
|
+
result.status = result.status === 'running' ? 'completed' : result.status;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
if (!result.finalResponse && result.success) {
|
|
942
|
+
result.finalResponse = result.summary;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
if (!result.finalResponse && !result.error) {
|
|
946
|
+
result.error = 'Codex returned an empty response.';
|
|
947
|
+
result.status = 'failed';
|
|
948
|
+
result.success = false;
|
|
949
|
+
}
|
|
950
|
+
} catch (err) {
|
|
951
|
+
const aborted = abortController.signal.aborted;
|
|
952
|
+
result.error = aborted ? 'Codex turn was cancelled.' : (err?.message || String(err));
|
|
953
|
+
result.status = aborted ? 'cancelled' : 'failed';
|
|
954
|
+
result.success = false;
|
|
955
|
+
} finally {
|
|
956
|
+
if (timeout) {
|
|
957
|
+
clearTimeout(timeout);
|
|
958
|
+
}
|
|
959
|
+
result.completedAtUtc = new Date().toISOString();
|
|
960
|
+
record.updatedAtUtc = result.completedAtUtc;
|
|
961
|
+
record.lastStatus = result.status;
|
|
962
|
+
record.lastResult = result;
|
|
963
|
+
activeTurns.delete(activeKey);
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
return result;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
async function runFixtureThread(body, workingDirectory, prompt, startedAtUtc) {
|
|
970
|
+
const threadOptions = buildThreadOptions(body, workingDirectory);
|
|
971
|
+
const threadId = String(body.threadId || `fixture_${crypto.randomUUID()}`);
|
|
972
|
+
const turnId = `turn_${crypto.randomUUID()}`;
|
|
973
|
+
const sandboxMode = API_SANDBOX_BY_SDK_VALUE[threadOptions.sandboxMode] || threadOptions.sandboxMode;
|
|
974
|
+
const violations = [];
|
|
975
|
+
|
|
976
|
+
if (sandboxMode !== 'read_only') {
|
|
977
|
+
violations.push(`sandbox=${sandboxMode || 'missing'}`);
|
|
978
|
+
}
|
|
979
|
+
if (threadOptions.webSearchMode !== 'disabled') {
|
|
980
|
+
violations.push(`webSearchMode=${threadOptions.webSearchMode || 'missing'}`);
|
|
981
|
+
}
|
|
982
|
+
if (threadOptions.networkAccessEnabled !== false) {
|
|
983
|
+
violations.push(`networkAccessEnabled=${String(threadOptions.networkAccessEnabled)}`);
|
|
984
|
+
}
|
|
985
|
+
if (threadOptions.approvalPolicy !== 'on-request') {
|
|
986
|
+
violations.push(`approvalPolicy=${threadOptions.approvalPolicy || 'missing'}`);
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const result = makeEmptyResult({
|
|
990
|
+
providerKind: PROVIDER_KIND.fixtureReadOnly,
|
|
991
|
+
threadId,
|
|
992
|
+
turnId,
|
|
993
|
+
status: violations.length > 0 ? 'failed' : 'completed',
|
|
994
|
+
startedAtUtc,
|
|
995
|
+
completedAtUtc: new Date().toISOString(),
|
|
996
|
+
sandboxMode,
|
|
997
|
+
rawEventCount: 1
|
|
998
|
+
});
|
|
999
|
+
|
|
1000
|
+
if (violations.length > 0) {
|
|
1001
|
+
result.error = `FixtureReadOnly refused unsafe Codex request: ${violations.join(', ')}`;
|
|
1002
|
+
result.summary = 'The local fixture provider blocks non-read-only CodeMaster execution contracts.';
|
|
1003
|
+
} else {
|
|
1004
|
+
result.success = true;
|
|
1005
|
+
result.summary = 'FixtureReadOnly validated the CodeMaster LocalBridge Codex handoff.';
|
|
1006
|
+
result.finalResponse = [
|
|
1007
|
+
'LocalBridge Codex fixture completed a read-only CodeMaster run.',
|
|
1008
|
+
`Sandbox=${sandboxMode}; approval=${threadOptions.approvalPolicy}; networkAccess=${threadOptions.networkAccessEnabled}; webSearch=${threadOptions.webSearchMode}.`,
|
|
1009
|
+
`WorkingDirectory=${workingDirectory}.`,
|
|
1010
|
+
`InstructionChars=${prompt.length}; changedFiles=0; commandLogs=0.`
|
|
1011
|
+
].join('\n');
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
threads.set(threadId, {
|
|
1015
|
+
providerKind: PROVIDER_KIND.fixtureReadOnly,
|
|
1016
|
+
createdAtUtc: startedAtUtc,
|
|
1017
|
+
updatedAtUtc: result.completedAtUtc,
|
|
1018
|
+
options: threadOptions,
|
|
1019
|
+
officialThreadId: threadId,
|
|
1020
|
+
lastStatus: result.status,
|
|
1021
|
+
lastResult: result
|
|
1022
|
+
});
|
|
1023
|
+
|
|
1024
|
+
log('Codex fixture run complete', {
|
|
1025
|
+
threadId,
|
|
1026
|
+
turnId,
|
|
1027
|
+
sandbox: sandboxMode,
|
|
1028
|
+
cwd: workingDirectory,
|
|
1029
|
+
success: result.success
|
|
1030
|
+
});
|
|
1031
|
+
|
|
1032
|
+
return result;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
async function runLegacyExec(body, workingDirectory, prompt, startedAtUtc) {
|
|
1036
|
+
const threadOptions = buildThreadOptions(body, workingDirectory);
|
|
1037
|
+
const turnId = `turn_${crypto.randomUUID()}`;
|
|
1038
|
+
const threadId = String(body.threadId || `legacy_${crypto.randomUUID()}`);
|
|
1039
|
+
const abortController = new AbortController();
|
|
1040
|
+
const activeKey = `${threadId}:${turnId}`;
|
|
1041
|
+
activeTurns.set(activeKey, {
|
|
1042
|
+
providerKind: PROVIDER_KIND.legacyExec,
|
|
1043
|
+
threadId,
|
|
1044
|
+
turnId,
|
|
1045
|
+
abortController,
|
|
1046
|
+
startedAtUtc
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
const tempDir = path.join(workingDirectory, TEMP_DIR);
|
|
1050
|
+
const schema = normalizeOutputSchema(body.outputSchema || body.outputSchemaJson);
|
|
1051
|
+
let schemaPath = null;
|
|
1052
|
+
await ensureCodexRuntimeHome(workingDirectory);
|
|
1053
|
+
const args = [
|
|
1054
|
+
'exec',
|
|
1055
|
+
'-',
|
|
1056
|
+
'--skip-git-repo-check',
|
|
1057
|
+
'--json',
|
|
1058
|
+
'--disable',
|
|
1059
|
+
'plugins',
|
|
1060
|
+
'--disable',
|
|
1061
|
+
'shell_snapshot',
|
|
1062
|
+
'--cd',
|
|
1063
|
+
workingDirectory,
|
|
1064
|
+
'-s',
|
|
1065
|
+
threadOptions.sandboxMode,
|
|
1066
|
+
'--config',
|
|
1067
|
+
`model_reasoning_effort=${threadOptions.modelReasoningEffort}`,
|
|
1068
|
+
'--config',
|
|
1069
|
+
`approval_policy="${threadOptions.approvalPolicy}"`
|
|
1070
|
+
];
|
|
1071
|
+
appendCodexIsolationConfigArgs(args);
|
|
1072
|
+
|
|
1073
|
+
if (threadOptions.model) {
|
|
1074
|
+
args.push('-m', threadOptions.model);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
for (const directory of threadOptions.additionalDirectories || []) {
|
|
1078
|
+
args.push('--add-dir', directory);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (threadOptions.networkAccessEnabled !== undefined) {
|
|
1082
|
+
args.push('--config', `sandbox_workspace_write.network_access=${Boolean(threadOptions.networkAccessEnabled)}`);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
if (threadOptions.webSearchMode) {
|
|
1086
|
+
args.push('--config', `web_search="${threadOptions.webSearchMode}"`);
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
if (schema) {
|
|
1090
|
+
await fs.mkdir(tempDir, { recursive: true });
|
|
1091
|
+
schemaPath = path.join(tempDir, `${crypto.randomUUID()}.schema.json`);
|
|
1092
|
+
await fs.writeFile(schemaPath, JSON.stringify(schema), 'utf8');
|
|
1093
|
+
args.push('--output-schema', schemaPath);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const result = makeEmptyResult({
|
|
1097
|
+
providerKind: PROVIDER_KIND.legacyExec,
|
|
1098
|
+
threadId,
|
|
1099
|
+
turnId,
|
|
1100
|
+
status: 'running',
|
|
1101
|
+
startedAtUtc,
|
|
1102
|
+
completedAtUtc: '',
|
|
1103
|
+
sandboxMode: API_SANDBOX_BY_SDK_VALUE[threadOptions.sandboxMode] || threadOptions.sandboxMode
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
try {
|
|
1107
|
+
const childResult = await new Promise((resolve) => {
|
|
1108
|
+
const child = spawn(resolveCodexPathOverride() || 'codex', args, {
|
|
1109
|
+
cwd: workingDirectory,
|
|
1110
|
+
env: buildCodexChildEnv(),
|
|
1111
|
+
windowsHide: true,
|
|
1112
|
+
signal: abortController.signal
|
|
1113
|
+
});
|
|
1114
|
+
let stdout = '';
|
|
1115
|
+
let stderr = '';
|
|
1116
|
+
let settled = false;
|
|
1117
|
+
const timeout = setTimeout(() => {
|
|
1118
|
+
try {
|
|
1119
|
+
abortController.abort();
|
|
1120
|
+
} catch {
|
|
1121
|
+
// Ignore duplicate aborts.
|
|
1122
|
+
}
|
|
1123
|
+
try {
|
|
1124
|
+
child.kill('SIGTERM');
|
|
1125
|
+
} catch {
|
|
1126
|
+
// Ignore if already exited.
|
|
1127
|
+
}
|
|
1128
|
+
}, DEFAULT_TIMEOUT_MS);
|
|
1129
|
+
|
|
1130
|
+
const settle = (payload) => {
|
|
1131
|
+
if (settled) return;
|
|
1132
|
+
settled = true;
|
|
1133
|
+
clearTimeout(timeout);
|
|
1134
|
+
resolve(payload);
|
|
1135
|
+
};
|
|
1136
|
+
|
|
1137
|
+
child.stdout?.on('data', chunk => {
|
|
1138
|
+
stdout += chunk.toString();
|
|
1139
|
+
});
|
|
1140
|
+
child.stderr?.on('data', chunk => {
|
|
1141
|
+
stderr += chunk.toString();
|
|
1142
|
+
});
|
|
1143
|
+
child.on('error', err => {
|
|
1144
|
+
settle({ exitCode: -1, stdout, stderr, error: err?.message || String(err) });
|
|
1145
|
+
});
|
|
1146
|
+
child.on('close', code => {
|
|
1147
|
+
settle({ exitCode: Number.isFinite(code) ? code : -1, stdout, stderr, error: '' });
|
|
1148
|
+
});
|
|
1149
|
+
child.stdin?.write(prompt);
|
|
1150
|
+
child.stdin?.end();
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
const parsed = extractLegacyEvents(childResult.stdout, childResult.stderr);
|
|
1154
|
+
result.threadId = parsed.threadId || threadId;
|
|
1155
|
+
result.finalResponse = parsed.finalResponse;
|
|
1156
|
+
result.summary = parsed.summary;
|
|
1157
|
+
result.changedFiles = parsed.changedFiles;
|
|
1158
|
+
result.commandLogs = parsed.commandLogs;
|
|
1159
|
+
result.approvalEvents = parsed.approvalEvents;
|
|
1160
|
+
result.rawEventCount = parsed.rawEventCount;
|
|
1161
|
+
result.error = childResult.error || parsed.error;
|
|
1162
|
+
result.success = childResult.exitCode === 0 && Boolean(result.finalResponse);
|
|
1163
|
+
result.status = result.success ? 'completed' : 'failed';
|
|
1164
|
+
if (!result.error && !result.success) {
|
|
1165
|
+
result.error = parsed.error || 'Codex legacy exec returned an empty response.';
|
|
1166
|
+
}
|
|
1167
|
+
} catch (err) {
|
|
1168
|
+
const aborted = abortController.signal.aborted;
|
|
1169
|
+
result.error = aborted ? 'Codex turn was cancelled.' : (err?.message || String(err));
|
|
1170
|
+
result.status = aborted ? 'cancelled' : 'failed';
|
|
1171
|
+
result.success = false;
|
|
1172
|
+
} finally {
|
|
1173
|
+
result.completedAtUtc = new Date().toISOString();
|
|
1174
|
+
activeTurns.delete(activeKey);
|
|
1175
|
+
if (schemaPath) {
|
|
1176
|
+
await fs.unlink(schemaPath).catch(() => {});
|
|
1177
|
+
}
|
|
1178
|
+
threads.set(result.threadId, {
|
|
1179
|
+
providerKind: PROVIDER_KIND.legacyExec,
|
|
1180
|
+
createdAtUtc: startedAtUtc,
|
|
1181
|
+
updatedAtUtc: result.completedAtUtc,
|
|
1182
|
+
options: threadOptions,
|
|
1183
|
+
officialThreadId: result.threadId,
|
|
1184
|
+
lastStatus: result.status,
|
|
1185
|
+
lastResult: result
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
return result;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
async function runThread(body = {}) {
|
|
1193
|
+
const prompt = extractPrompt(body);
|
|
1194
|
+
const startedAtUtc = new Date().toISOString();
|
|
1195
|
+
const workingDirectory = await resolveWorkingDirectory(body.workingDir || body.workingDirectory || '');
|
|
1196
|
+
const providerKind = await resolveProvider(body.providerKind || body.runtime);
|
|
1197
|
+
|
|
1198
|
+
if (providerKind === PROVIDER_KIND.fixtureReadOnly) {
|
|
1199
|
+
return runFixtureThread(body, workingDirectory, prompt, startedAtUtc);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
if (providerKind === PROVIDER_KIND.typeScriptSdk) {
|
|
1203
|
+
return runSdkThread(body, workingDirectory, prompt, startedAtUtc);
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
if (providerKind === PROVIDER_KIND.legacyExec) {
|
|
1207
|
+
return runLegacyExec(body, workingDirectory, prompt, startedAtUtc);
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
return makeEmptyResult({
|
|
1211
|
+
providerKind,
|
|
1212
|
+
startedAtUtc,
|
|
1213
|
+
completedAtUtc: new Date().toISOString(),
|
|
1214
|
+
error: 'Codex local runtime is not available. TypeScript SDK and legacy codex exec are unavailable.'
|
|
1215
|
+
});
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
async function resumeThread(body = {}) {
|
|
1219
|
+
const providerKind = await resolveProvider(body.providerKind || body.runtime);
|
|
1220
|
+
const threadId = String(body.threadId || '').trim();
|
|
1221
|
+
if (!threadId) {
|
|
1222
|
+
const err = new Error('threadId is required.');
|
|
1223
|
+
err.statusCode = 400;
|
|
1224
|
+
throw err;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
if (threads.has(threadId)) {
|
|
1228
|
+
const record = threads.get(threadId);
|
|
1229
|
+
record.lastStatus = 'resumed';
|
|
1230
|
+
record.updatedAtUtc = new Date().toISOString();
|
|
1231
|
+
return makeEmptyResult({
|
|
1232
|
+
success: true,
|
|
1233
|
+
providerKind: record.providerKind,
|
|
1234
|
+
threadId,
|
|
1235
|
+
status: 'resumed',
|
|
1236
|
+
sandboxMode: API_SANDBOX_BY_SDK_VALUE[record.options?.sandboxMode] || record.options?.sandboxMode || ''
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (providerKind === PROVIDER_KIND.typeScriptSdk) {
|
|
1241
|
+
const workingDirectory = await resolveWorkingDirectory(body.workingDir || body.workingDirectory || '');
|
|
1242
|
+
const threadOptions = buildThreadOptions(body, workingDirectory);
|
|
1243
|
+
const sdk = await loadCodexSdk();
|
|
1244
|
+
await ensureCodexRuntimeHome(workingDirectory);
|
|
1245
|
+
const codex = createCodexSdkClient(sdk);
|
|
1246
|
+
const thread = codex.resumeThread(threadId, threadOptions);
|
|
1247
|
+
threads.set(threadId, {
|
|
1248
|
+
providerKind,
|
|
1249
|
+
thread,
|
|
1250
|
+
createdAtUtc: new Date().toISOString(),
|
|
1251
|
+
updatedAtUtc: new Date().toISOString(),
|
|
1252
|
+
options: threadOptions,
|
|
1253
|
+
officialThreadId: threadId,
|
|
1254
|
+
lastStatus: 'resumed'
|
|
1255
|
+
});
|
|
1256
|
+
return makeEmptyResult({
|
|
1257
|
+
success: true,
|
|
1258
|
+
providerKind,
|
|
1259
|
+
threadId,
|
|
1260
|
+
status: 'resumed',
|
|
1261
|
+
sandboxMode: API_SANDBOX_BY_SDK_VALUE[threadOptions.sandboxMode] || threadOptions.sandboxMode
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
return makeEmptyResult({
|
|
1266
|
+
providerKind,
|
|
1267
|
+
threadId,
|
|
1268
|
+
error: 'Thread resume is only supported by the TypeScript SDK provider.'
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function cancelThread(body = {}) {
|
|
1273
|
+
const threadId = String(body.threadId || '').trim();
|
|
1274
|
+
const turnId = String(body.turnId || '').trim();
|
|
1275
|
+
const matches = [];
|
|
1276
|
+
for (const [key, turn] of activeTurns.entries()) {
|
|
1277
|
+
if (threadId && turn.threadId !== threadId) {
|
|
1278
|
+
continue;
|
|
1279
|
+
}
|
|
1280
|
+
if (turnId && turn.turnId !== turnId) {
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
matches.push([key, turn]);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
for (const [, turn] of matches) {
|
|
1287
|
+
try {
|
|
1288
|
+
turn.abortController.abort();
|
|
1289
|
+
} catch {
|
|
1290
|
+
// Ignore duplicate aborts.
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
return {
|
|
1295
|
+
success: matches.length > 0,
|
|
1296
|
+
cancelled: matches.length,
|
|
1297
|
+
threadId,
|
|
1298
|
+
turnId,
|
|
1299
|
+
status: matches.length > 0 ? 'cancel-requested' : 'not-found'
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function getThreadStatus(threadId) {
|
|
1304
|
+
const id = String(threadId || '').trim();
|
|
1305
|
+
const record = threads.get(id);
|
|
1306
|
+
const active = Array.from(activeTurns.values())
|
|
1307
|
+
.filter(turn => turn.threadId === id || record?.officialThreadId === turn.threadId)
|
|
1308
|
+
.map(turn => ({
|
|
1309
|
+
providerKind: turn.providerKind,
|
|
1310
|
+
threadId: turn.threadId,
|
|
1311
|
+
turnId: turn.turnId,
|
|
1312
|
+
startedAtUtc: turn.startedAtUtc,
|
|
1313
|
+
status: 'running'
|
|
1314
|
+
}));
|
|
1315
|
+
|
|
1316
|
+
return {
|
|
1317
|
+
success: Boolean(record || active.length > 0),
|
|
1318
|
+
threadId: id,
|
|
1319
|
+
providerKind: record?.providerKind || active[0]?.providerKind || PROVIDER_KIND.unavailable,
|
|
1320
|
+
officialThreadId: record?.officialThreadId || null,
|
|
1321
|
+
status: active.length > 0 ? 'running' : (record?.lastStatus || 'not-found'),
|
|
1322
|
+
createdAtUtc: record?.createdAtUtc || null,
|
|
1323
|
+
updatedAtUtc: record?.updatedAtUtc || null,
|
|
1324
|
+
options: serializeThreadOptionsForStatus(record?.options),
|
|
1325
|
+
activeTurns: active,
|
|
1326
|
+
lastResult: record?.lastResult || null
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
return {
|
|
1331
|
+
providerKinds: PROVIDER_KIND,
|
|
1332
|
+
getCapabilities,
|
|
1333
|
+
startThread,
|
|
1334
|
+
runThread,
|
|
1335
|
+
resumeThread,
|
|
1336
|
+
cancelThread,
|
|
1337
|
+
getThreadStatus
|
|
1338
|
+
};
|
|
1339
|
+
}
|