@openclaw/codex 2026.7.2-beta.7 → 2026.8.1-beta.3
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 +2 -0
- package/dist/api.js +147 -0
- package/dist/app-server-policy-bg5QPSVP.js +25 -0
- package/dist/attempt-client-cleanup-URjUlCHg.js +735 -0
- package/dist/binding-connection-DAvZwp96.js +45 -0
- package/dist/{bounded-turn-D2ZAgZVz.js → bounded-turn-4BjrZEwW.js} +63 -56
- package/dist/{capabilities-BFDTVYr4.js → capabilities-CH3K57v4.js} +8 -2
- package/dist/cli-metadata.js +1 -1
- package/dist/{command-formatters-C336eRVS.js → command-formatters-D0nZ3qtd.js} +18 -18
- package/dist/{command-handlers-DAHUSry9.js → command-handlers-DC86lgT4.js} +507 -101
- package/dist/command-rpc-xUIIonsO.js +90 -0
- package/dist/{compact-D_uvHurS.js → compact-COqgBRny.js} +234 -214
- package/dist/{session-binding-BXpo9ymA.js → config-CpNeo5AE.js} +284 -1163
- package/dist/context-compaction-activity-BOeJf7yo.js +58 -0
- package/dist/{conversation-binding-data-BHRpYbX3.js → conversation-binding-data-CW2GuupV.js} +6 -10
- package/dist/doctor-contract-api.js +48 -50
- package/dist/{dynamic-tools-D2gYkSBO.js → dynamic-tools-fJZhl4CE.js} +3442 -1433
- package/dist/{effective-mcp-catalog-Bbd98Inw.js → effective-mcp-catalog-4YKn51aG.js} +20 -15
- package/dist/elicitation-input-DMkeki8d.js +71 -0
- package/dist/{event-projector-assistant-message-DTgnPPVF.js → event-projector-assistant-message-BwxV4d6N.js} +11 -23
- package/dist/{attempt-notifications-AP-a-Q5k.js → event-projector-usage-CLwNEJqW.js} +113 -29
- package/dist/harness-DoKQcF8n.js +258 -0
- package/dist/harness.js +2 -160
- package/dist/image-payload-sanitizer-CnfoUPCE.js +65 -0
- package/dist/index.js +730 -256
- package/dist/isolated-completion-DPMtlH9r.js +75 -0
- package/dist/local-runtime-attribution-B_2mnqhb.js +19 -0
- package/dist/managed-binary-By4FZC6i.js +212 -0
- package/dist/media-understanding-provider.js +5 -5
- package/dist/model-catalog-CmGoqVKB.js +48 -0
- package/dist/{models-BBMKU4jA.js → models-CZw1wClT.js} +12 -17
- package/dist/native-subagent-monitor-GGq3RnOo.js +3225 -0
- package/dist/node-exec-server.runtime-Bsu1LdFZ.js +192 -0
- package/dist/{plugin-app-cache-key-DztPafOA.js → plugin-app-cache-key-DMNQ8Zi8.js} +18 -6
- package/dist/plugin-approval-roundtrip-C75Yiiwc.js +95 -0
- package/dist/plugin-marketplace-discovery-CSWDkMrP.js +399 -0
- package/dist/{computer-use-CYEc8U3w.js → plugin-metadata-cache-BXH5NF-4.js} +109 -6
- package/dist/protocol-validators-VAVyEnDp.js +2399 -0
- package/dist/{rate-limits-CU-gVCCS.js → rate-limits-BI1dMHDG.js} +17 -35
- package/dist/{request-qKh331yu.js → request-DSK0W6YI.js} +3 -3
- package/dist/{run-attempt-CY9WlDLc.js → run-attempt-BV3HzuCf.js} +3416 -7194
- package/dist/{runtime-artifact-CmtG_t1L.js → runtime-artifact-BvyT1uyd.js} +11 -29
- package/dist/{sandbox-guard-NuhpXmPB.js → sandbox-guard-C1qEeZ5C.js} +29 -30
- package/dist/session-binding-BTrLf9Qm.js +874 -0
- package/dist/{session-catalog-DyOgR4WH.js → session-catalog-ezP_UcRM.js} +1584 -1289
- package/dist/session-catalog-home-id-BNwAJ9D7.js +18 -0
- package/dist/{session-cli-2PZaFLRh.js → session-cli-B1hKvfkI.js} +20 -7
- package/dist/session-retirement-CE4iMiLh.js +36 -0
- package/dist/{session-upstream-marker-B6U7tLw5.js → session-upstream-marker-D_2npHlA.js} +1 -1
- package/dist/{settled-turn-finalizer-CXY2Z9dT.js → settled-turn-finalizer-Dx1onIqg.js} +40 -28
- package/dist/{shared-client-DTbvH-XV.js → shared-client-Dw-rzii0.js} +1315 -469
- package/dist/{side-question-_ulBk1AL.js → side-question-DbXkJZzg.js} +140 -69
- package/dist/{thread-lifecycle-DF53ogiA.js → thread-lifecycle-DKdrnTMX.js} +927 -768
- package/dist/thread-ownership-i2NBwDUL.js +92 -0
- package/dist/thread-resume-CdJk2Ehf.js +30 -0
- package/dist/transcript-mirror-BU0_LKAL.js +588 -0
- package/dist/{transport-stdio-DXgGZ7Ib.js → transport-stdio-DhoGy33Z.js} +13 -2
- package/dist/upstream-prompt-provenance-CFmsKwr9.js +39 -0
- package/dist/{upstream-session-fork-Dy4Xvuoj.js → upstream-session-fork-BDVJCLAD.js} +14 -8
- package/dist/{usage-CQ6GhD5-.js → usage-9dKJCgcy.js} +3 -3
- package/dist/{web-search-provider.runtime-CCFhg2z5.js → web-search-provider.runtime-DCdx4DGu.js} +8 -9
- package/openclaw.plugin.json +49 -4
- package/package.json +6 -5
- package/dist/app-server-policy-BSaO2JIT.js +0 -49
- package/dist/conversation-control-xsIeXbo4.js +0 -287
- package/dist/event-projector-usage-Xp4AlR2k.js +0 -86
- package/dist/notification-correlation-CU6_AnOb.js +0 -67
- package/dist/transcript-mirror-DzsdNCf5.js +0 -6924
|
@@ -1,133 +1,46 @@
|
|
|
1
1
|
import { t as __exportAll } from "./rolldown-runtime-8H4AJuhK.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
2
|
+
import { a as resolveCodexAppServerStartOptionsForAgent, i as resolveCodexAppServerRuntimeOptions, o as resolveCodexComputerUseConfig, t as codexAppServerStartOptionsKey, x as resolveCodexAppServerUserHomeDir } from "./config-CpNeo5AE.js";
|
|
3
|
+
import { o as isCodexAppServerNativeAuthProfile } from "./session-binding-BTrLf9Qm.js";
|
|
4
|
+
import { n as resolveCodexAppServerSpawnEnv, t as createStdioTransport } from "./transport-stdio-DhoGy33Z.js";
|
|
5
|
+
import { a as CODEX_APP_SERVER_VERSION, n as resolveManagedCodexAppServerStartOptions, o as MIN_SUPPORTED_CODEX_APP_SERVER_VERSION, r as resolveManagedCodexNativeCommand } from "./managed-binary-By4FZC6i.js";
|
|
6
|
+
import { isRecord, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
7
|
+
import fs from "node:fs/promises";
|
|
6
8
|
import path from "node:path";
|
|
9
|
+
import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
|
|
7
10
|
import { addTimerTimeoutGraceMs, resolveTimerTimeoutMs } from "openclaw/plugin-sdk/number-runtime";
|
|
8
|
-
import { AgentHarnessPreflightError, OPENCLAW_VERSION, embeddedAgentLog } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
11
|
+
import { AgentHarnessPreflightError, OPENCLAW_VERSION, embeddedAgentLog, formatErrorMessage } from "openclaw/plugin-sdk/agent-harness-runtime";
|
|
9
12
|
import { createHash, randomUUID } from "node:crypto";
|
|
10
|
-
import
|
|
11
|
-
import { createDeferred } from "openclaw/plugin-sdk/extension-shared";
|
|
12
|
-
import fsSync, { constants, existsSync, readFileSync, realpathSync } from "node:fs";
|
|
13
|
+
import fsSync, { constants, existsSync } from "node:fs";
|
|
13
14
|
import os from "node:os";
|
|
14
15
|
import { isDeepStrictEqual } from "node:util";
|
|
16
|
+
import { ensureAuthProfileStore, findPersistedAuthProfileCredential, loadAuthProfileStoreForSecretsRuntime, refreshOAuthCredentialForRuntime, resolveApiKeyForProfile, resolveAuthProfileOrder, resolveDefaultAgentDir, resolvePersistedAuthProfileOwnerAgentDir, resolveProviderIdForAuth } from "openclaw/plugin-sdk/agent-runtime";
|
|
15
17
|
import { hasUsableOAuthCredential } from "openclaw/plugin-sdk/provider-auth";
|
|
16
18
|
import { readSecretFile } from "openclaw/plugin-sdk/secret-file";
|
|
19
|
+
import { pruneMapToMaxSize } from "openclaw/plugin-sdk/collection-runtime";
|
|
17
20
|
import { runExec } from "openclaw/plugin-sdk/process-runtime";
|
|
21
|
+
import { assertNoSymlinkParents, withTimeout } from "openclaw/plugin-sdk/security-runtime";
|
|
22
|
+
import { execFile } from "node:child_process";
|
|
18
23
|
import { createInterface } from "node:readline";
|
|
24
|
+
import { coerceErrorMessage, toStringifiedError } from "openclaw/plugin-sdk/error-runtime";
|
|
19
25
|
import { sliceUtf16Safe, truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
|
26
|
+
import { parse } from "semver";
|
|
20
27
|
import { EventEmitter } from "node:events";
|
|
21
28
|
import net from "node:net";
|
|
22
29
|
import { PassThrough, Writable } from "node:stream";
|
|
23
30
|
import { StringDecoder } from "node:string_decoder";
|
|
24
31
|
import WebSocket from "ws";
|
|
25
|
-
import { fileURLToPath } from "node:url";
|
|
26
|
-
import { expectDefined } from "openclaw/plugin-sdk/expect-runtime";
|
|
27
|
-
import { withTimeout } from "openclaw/plugin-sdk/security-runtime";
|
|
28
|
-
//#region extensions/codex/src/app-server/attempt-timeouts.ts
|
|
29
|
-
/**
|
|
30
|
-
* Timeout defaults and normalizers for Codex app-server startup and turn
|
|
31
|
-
* liveness watches.
|
|
32
|
-
*/
|
|
33
|
-
/** Minimum startup timeout accepted by the Codex app-server harness. */
|
|
34
|
-
const CODEX_APP_SERVER_STARTUP_TIMEOUT_FLOOR_MS = 100;
|
|
35
|
-
/** Default idle timeout while waiting for app-server turn completion. */
|
|
36
|
-
const CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS = 6e4;
|
|
37
|
-
/** Short guard after apparent assistant completion. */
|
|
38
|
-
const CODEX_TURN_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS = 1e4;
|
|
39
|
-
const CODEX_POST_TOOL_RAW_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS = 5 * 6e4;
|
|
40
|
-
/** Guard after reasoning/commentary progress when no tool handoff occurred. */
|
|
41
|
-
const CODEX_POST_REASONING_REPLY_IDLE_TIMEOUT_MS = 5 * 6e4;
|
|
42
|
-
/** Long terminal idle watch for app-server turns that never send completion. */
|
|
43
|
-
const CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS = 30 * 6e4;
|
|
44
|
-
var CodexAppServerStartupError = class extends Error {
|
|
45
|
-
constructor(reason, message = reason === "timed_out" ? "codex app-server startup timed out" : "codex app-server startup aborted") {
|
|
46
|
-
super(message);
|
|
47
|
-
this.reason = reason;
|
|
48
|
-
this.code = "CODEX_APP_SERVER_STARTUP_CANCELLED";
|
|
49
|
-
this.name = "CodexAppServerStartupError";
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
function isCodexAppServerStartupError(error, reason) {
|
|
53
|
-
return error instanceof Error && "code" in error && error.code === "CODEX_APP_SERVER_STARTUP_CANCELLED" && "reason" in error && (error.reason === "aborted" || error.reason === "timed_out") && (reason === void 0 || error.reason === reason);
|
|
54
|
-
}
|
|
55
|
-
function resolvePositiveIntegerTimeoutMs(value, fallbackMs) {
|
|
56
|
-
return resolveTimerTimeoutMs(value, resolveTimerTimeoutMs(fallbackMs, 1));
|
|
57
|
-
}
|
|
58
|
-
/** Runs startup work with abort and timeout handling plus optional cleanup. */
|
|
59
|
-
async function withCodexStartupTimeout(params) {
|
|
60
|
-
if (params.signal.aborted) throw new CodexAppServerStartupError("aborted");
|
|
61
|
-
let timeout;
|
|
62
|
-
let abortCleanup;
|
|
63
|
-
let timeoutError;
|
|
64
|
-
let timeoutCleanup;
|
|
65
|
-
try {
|
|
66
|
-
return await Promise.race([params.operation(), new Promise((_, reject) => {
|
|
67
|
-
const rejectOnce = (error) => {
|
|
68
|
-
if (timeout) {
|
|
69
|
-
clearTimeout(timeout);
|
|
70
|
-
timeout = void 0;
|
|
71
|
-
}
|
|
72
|
-
reject(error);
|
|
73
|
-
};
|
|
74
|
-
timeout = setTimeout(() => {
|
|
75
|
-
timeoutError = new CodexAppServerStartupError("timed_out");
|
|
76
|
-
timeoutCleanup = Promise.resolve(params.onTimeout?.()).then(() => void 0, () => void 0);
|
|
77
|
-
timeoutCleanup.finally(() => {
|
|
78
|
-
rejectOnce(timeoutError);
|
|
79
|
-
});
|
|
80
|
-
}, params.timeoutMs);
|
|
81
|
-
const abortListener = () => rejectOnce(new CodexAppServerStartupError("aborted"));
|
|
82
|
-
params.signal.addEventListener("abort", abortListener, { once: true });
|
|
83
|
-
abortCleanup = () => params.signal.removeEventListener("abort", abortListener);
|
|
84
|
-
})]);
|
|
85
|
-
} catch (error) {
|
|
86
|
-
if (timeoutError) {
|
|
87
|
-
await timeoutCleanup;
|
|
88
|
-
throw timeoutError;
|
|
89
|
-
}
|
|
90
|
-
throw error;
|
|
91
|
-
} finally {
|
|
92
|
-
if (timeout) clearTimeout(timeout);
|
|
93
|
-
abortCleanup?.();
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
/** Resolves startup timeout while honoring the configured floor. */
|
|
97
|
-
function resolveCodexStartupTimeoutMs(params) {
|
|
98
|
-
const timeoutFloorMs = resolvePositiveIntegerTimeoutMs(params.timeoutFloorMs, CODEX_APP_SERVER_STARTUP_TIMEOUT_FLOOR_MS);
|
|
99
|
-
const timeoutMs = resolvePositiveIntegerTimeoutMs(params.timeoutMs, timeoutFloorMs);
|
|
100
|
-
return Math.max(timeoutFloorMs, timeoutMs);
|
|
101
|
-
}
|
|
102
|
-
/** Resolves the completion-idle timeout for an active turn. */
|
|
103
|
-
function resolveCodexTurnCompletionIdleTimeoutMs(value) {
|
|
104
|
-
return resolvePositiveIntegerTimeoutMs(value, CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS);
|
|
105
|
-
}
|
|
106
|
-
/** Resolves the short assistant-completion release timeout. */
|
|
107
|
-
function resolveCodexTurnAssistantCompletionIdleTimeoutMs(value) {
|
|
108
|
-
return resolvePositiveIntegerTimeoutMs(value, CODEX_TURN_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS);
|
|
109
|
-
}
|
|
110
|
-
/** Resolves the conservative post-tool raw assistant guard timeout. */
|
|
111
|
-
function resolveCodexPostToolRawAssistantCompletionIdleTimeoutMs(value, fallbackMs) {
|
|
112
|
-
return resolvePositiveIntegerTimeoutMs(value, Math.max(resolvePositiveIntegerTimeoutMs(void 0, fallbackMs), CODEX_POST_TOOL_RAW_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS));
|
|
113
|
-
}
|
|
114
|
-
/** Resolves the long terminal turn idle timeout. */
|
|
115
|
-
function resolveCodexTurnTerminalIdleTimeoutMs(value, runTimeoutOverrideMs) {
|
|
116
|
-
const explicitRunBudgetMs = resolvePositiveIntegerTimeoutMs(runTimeoutOverrideMs, CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS);
|
|
117
|
-
return resolvePositiveIntegerTimeoutMs(value, Math.max(CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS, explicitRunBudgetMs));
|
|
118
|
-
}
|
|
119
|
-
/** Adds gateway grace time to a caller timeout without overflowing invalid values. */
|
|
120
|
-
function resolveCodexGatewayTimeoutWithGraceMs(timeoutMs, graceMs = 1e4) {
|
|
121
|
-
const timeout = resolvePositiveIntegerTimeoutMs(timeoutMs, 1);
|
|
122
|
-
return addTimerTimeoutGraceMs(timeout, resolveTimerTimeoutMs(graceMs, 0, 0)) ?? timeout;
|
|
123
|
-
}
|
|
124
|
-
//#endregion
|
|
125
32
|
//#region extensions/codex/src/app-server/auth-start-options.ts
|
|
126
33
|
const CODEX_APP_SERVER_HOME_DIRNAME = "codex-home";
|
|
127
34
|
const CODEX_EPHEMERAL_AUTH_STORE_OVERRIDE = "cli_auth_credentials_store=\"ephemeral\"";
|
|
128
35
|
function resolveCodexAppServerHomeDir(agentDir) {
|
|
129
36
|
return path.join(path.resolve(agentDir), CODEX_APP_SERVER_HOME_DIRNAME);
|
|
130
37
|
}
|
|
38
|
+
/** Resolves the local CODEX_HOME used when starting one app-server connection. */
|
|
39
|
+
function resolveCodexAppServerLocalHomeDir(startOptions, agentDir, env = process.env) {
|
|
40
|
+
const configured = startOptions.env?.CODEX_HOME;
|
|
41
|
+
if (configured?.trim()) return configured;
|
|
42
|
+
return startOptions.homeScope === "user" ? resolveCodexAppServerUserHomeDir(env) : resolveCodexAppServerHomeDir(agentDir);
|
|
43
|
+
}
|
|
131
44
|
/** Forces OpenClaw-owned Codex auth to remain process-local. */
|
|
132
45
|
function withEphemeralCodexAuthStore(params) {
|
|
133
46
|
const { startOptions } = params;
|
|
@@ -268,108 +181,385 @@ function skippedCacheResult(status, message) {
|
|
|
268
181
|
};
|
|
269
182
|
}
|
|
270
183
|
//#endregion
|
|
184
|
+
//#region extensions/codex/src/app-server/computer-use-service-path.ts
|
|
185
|
+
/** Filesystem ownership guards for isolated Computer Use service provisioning. */
|
|
186
|
+
async function assertOwnedServicePath(params) {
|
|
187
|
+
await assertOwnedCodexHomePath({
|
|
188
|
+
ownershipRoot: params.ownershipRoot,
|
|
189
|
+
codexHome: params.codexHome,
|
|
190
|
+
allowMissing: true
|
|
191
|
+
});
|
|
192
|
+
assertPathAtOrInside(params.ownershipRoot, params.targetParent, "Computer Use service parent");
|
|
193
|
+
await assertNoSymlinkParents({
|
|
194
|
+
rootDir: params.ownershipRoot,
|
|
195
|
+
targetPath: params.targetParent,
|
|
196
|
+
allowMissing: true,
|
|
197
|
+
requireDirectories: true,
|
|
198
|
+
messagePrefix: "Computer Use service path"
|
|
199
|
+
});
|
|
200
|
+
await assertNotSymlink(params.targetPath, "Computer Use service target");
|
|
201
|
+
}
|
|
202
|
+
async function ensureOwnedCodexHome(codexHomeInput, ownershipRootInput = path.dirname(path.resolve(codexHomeInput))) {
|
|
203
|
+
const codexHome = path.resolve(codexHomeInput);
|
|
204
|
+
const ownershipRoot = path.resolve(ownershipRootInput);
|
|
205
|
+
await fs.mkdir(ownershipRoot, {
|
|
206
|
+
recursive: true,
|
|
207
|
+
mode: 448
|
|
208
|
+
});
|
|
209
|
+
await assertOwnedCodexHomePath({
|
|
210
|
+
ownershipRoot,
|
|
211
|
+
codexHome,
|
|
212
|
+
allowMissing: true
|
|
213
|
+
});
|
|
214
|
+
await ensureRealDirectoryTree(ownershipRoot, codexHome, "isolated Codex home");
|
|
215
|
+
await assertOwnedCodexHomePath({
|
|
216
|
+
ownershipRoot,
|
|
217
|
+
codexHome,
|
|
218
|
+
allowMissing: false
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
async function prepareOwnedServiceParent(params) {
|
|
222
|
+
await ensureOwnedCodexHome(params.codexHome, params.ownershipRoot);
|
|
223
|
+
await ensureRealDirectoryTree(params.ownershipRoot, params.targetParent, "Computer Use service parent");
|
|
224
|
+
await assertNoSymlinkParents({
|
|
225
|
+
rootDir: params.ownershipRoot,
|
|
226
|
+
targetPath: params.targetParent,
|
|
227
|
+
allowMissing: false,
|
|
228
|
+
requireDirectories: true,
|
|
229
|
+
messagePrefix: "Computer Use service path"
|
|
230
|
+
});
|
|
231
|
+
const [rootIdentity, parentIdentity] = await Promise.all([readRealDirectoryIdentity(params.ownershipRoot, "Computer Use ownership root"), readRealDirectoryIdentity(params.targetParent, "Computer Use service parent")]);
|
|
232
|
+
assertPathAtOrInside(rootIdentity.realPath, parentIdentity.realPath, "canonical Computer Use service parent");
|
|
233
|
+
return parentIdentity;
|
|
234
|
+
}
|
|
235
|
+
async function assertOwnedCodexHomePath(params) {
|
|
236
|
+
await readRealDirectoryIdentity(params.ownershipRoot, "Computer Use ownership root");
|
|
237
|
+
assertPathAtOrInside(params.ownershipRoot, params.codexHome, "isolated Codex home");
|
|
238
|
+
await assertNoSymlinkParents({
|
|
239
|
+
rootDir: params.ownershipRoot,
|
|
240
|
+
targetPath: params.codexHome,
|
|
241
|
+
allowMissing: params.allowMissing,
|
|
242
|
+
requireDirectories: true,
|
|
243
|
+
messagePrefix: "Computer Use service path"
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
async function readRealDirectoryIdentity(directoryPath, label) {
|
|
247
|
+
const logicalPath = path.resolve(directoryPath);
|
|
248
|
+
const before = await fs.lstat(logicalPath);
|
|
249
|
+
if (before.isSymbolicLink() || !before.isDirectory()) throw new Error(`${label} must be a real directory: ${logicalPath}`);
|
|
250
|
+
const realPath = await fs.realpath(logicalPath);
|
|
251
|
+
const [after, resolved] = await Promise.all([fs.lstat(logicalPath), fs.lstat(realPath)]);
|
|
252
|
+
if (after.isSymbolicLink() || !after.isDirectory() || !resolved.isDirectory() || before.dev !== after.dev || before.ino !== after.ino || after.dev !== resolved.dev || after.ino !== resolved.ino) throw new Error(`${label} changed while its ownership boundary was being established.`);
|
|
253
|
+
return {
|
|
254
|
+
logicalPath,
|
|
255
|
+
realPath,
|
|
256
|
+
dev: after.dev,
|
|
257
|
+
ino: after.ino
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
async function assertOwnedServiceParentStable(parent) {
|
|
261
|
+
await assertDirectoryIdentityStable(parent, "Computer Use service parent");
|
|
262
|
+
}
|
|
263
|
+
async function assertDirectoryIdentityStable(expected, label) {
|
|
264
|
+
if (!await directoryIdentityIsStable(expected)) throw new Error(`${label} changed during refresh; refusing to mutate the replacement path.`);
|
|
265
|
+
}
|
|
266
|
+
async function ownedServiceParentIsStable(parent) {
|
|
267
|
+
return await directoryIdentityIsStable(parent);
|
|
268
|
+
}
|
|
269
|
+
async function directoryIdentityIsStable(expected) {
|
|
270
|
+
try {
|
|
271
|
+
const current = await fs.lstat(expected.logicalPath);
|
|
272
|
+
if (current.isSymbolicLink() || !current.isDirectory() || current.dev !== expected.dev || current.ino !== expected.ino) return false;
|
|
273
|
+
return await fs.realpath(expected.logicalPath) === expected.realPath;
|
|
274
|
+
} catch {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
async function assertNotSymlink(filePath, label) {
|
|
279
|
+
try {
|
|
280
|
+
if ((await fs.lstat(filePath)).isSymbolicLink()) throw new Error(`${label} must not be a symbolic link: ${filePath}`);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
if (hasNodeErrorCode(error, "ENOENT")) return;
|
|
283
|
+
throw error;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async function ensureRealDirectoryTree(ownershipRoot, directoryPath, label) {
|
|
287
|
+
const root = path.resolve(ownershipRoot);
|
|
288
|
+
const target = path.resolve(directoryPath);
|
|
289
|
+
assertPathAtOrInside(root, target, label);
|
|
290
|
+
const relative = path.relative(root, target);
|
|
291
|
+
let current = root;
|
|
292
|
+
for (const segment of relative.split(path.sep).filter(Boolean)) {
|
|
293
|
+
current = path.join(current, segment);
|
|
294
|
+
const existing = await fs.lstat(current).catch((error) => {
|
|
295
|
+
if (hasNodeErrorCode(error, "ENOENT")) return;
|
|
296
|
+
throw error;
|
|
297
|
+
});
|
|
298
|
+
if (!existing) {
|
|
299
|
+
try {
|
|
300
|
+
await fs.mkdir(current, { mode: 448 });
|
|
301
|
+
} catch (error) {
|
|
302
|
+
if (!hasNodeErrorCode(error, "EEXIST")) throw error;
|
|
303
|
+
}
|
|
304
|
+
const created = await fs.lstat(current);
|
|
305
|
+
if (created.isSymbolicLink() || !created.isDirectory()) throw new Error(`${label} changed while its directory tree was being created: ${current}`);
|
|
306
|
+
} else if (existing.isSymbolicLink() || !existing.isDirectory()) throw new Error(`${label} must traverse real directories: ${current}`);
|
|
307
|
+
}
|
|
308
|
+
await assertNoSymlinkParents({
|
|
309
|
+
rootDir: root,
|
|
310
|
+
targetPath: target,
|
|
311
|
+
allowMissing: false,
|
|
312
|
+
requireDirectories: true,
|
|
313
|
+
messagePrefix: "Computer Use service path"
|
|
314
|
+
});
|
|
315
|
+
await readRealDirectoryIdentity(target, label);
|
|
316
|
+
}
|
|
317
|
+
function assertPathAtOrInside(rootPath, candidatePath, label) {
|
|
318
|
+
const relative = path.relative(path.resolve(rootPath), path.resolve(candidatePath));
|
|
319
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new Error(`${label} must remain inside ${path.resolve(rootPath)}.`);
|
|
320
|
+
}
|
|
321
|
+
function hasNodeErrorCode(error, code) {
|
|
322
|
+
return Boolean(error && typeof error === "object" && "code" in error && error.code === code);
|
|
323
|
+
}
|
|
324
|
+
//#endregion
|
|
271
325
|
//#region extensions/codex/src/app-server/computer-use-service.ts
|
|
272
326
|
/** Native Computer Use service provisioning for isolated Codex homes. */
|
|
273
327
|
const SERVICE_APP_NAME = "Codex Computer Use.app";
|
|
274
|
-
const
|
|
328
|
+
const SERVICE_BUNDLE_ID = "com.openai.sky.CUAService";
|
|
329
|
+
const CLIENT_BUNDLE_ID = "com.openai.sky.CUAService.cli";
|
|
330
|
+
const OPENAI_TEAM_ID = "2DC432GLL2";
|
|
331
|
+
const CLIENT_APP_RELATIVE_PATH = path.join("Contents", "SharedSupport", "SkyComputerUseClient.app");
|
|
332
|
+
const CLIENT_RELATIVE_PATH = path.join(CLIENT_APP_RELATIVE_PATH, "Contents", "MacOS", "SkyComputerUseClient");
|
|
275
333
|
const COPY_TIMEOUT_MS = 12e4;
|
|
334
|
+
const INSPECT_TIMEOUT_MS = 3e4;
|
|
335
|
+
const COMPLETED_SYNC_CACHE_MAX_ENTRIES = 64;
|
|
276
336
|
const activeInstalls = /* @__PURE__ */ new Map();
|
|
277
|
-
|
|
337
|
+
const completedSyncs = /* @__PURE__ */ new Map();
|
|
338
|
+
/** Synchronizes the CODEX_HOME native client with the selected signed desktop distribution. */
|
|
278
339
|
async function ensureCodexComputerUseServiceApp(params) {
|
|
279
340
|
const platform = params.platform ?? process.platform;
|
|
280
341
|
if (platform !== "darwin") return {
|
|
281
342
|
status: "unsupported",
|
|
282
343
|
changed: false
|
|
283
344
|
};
|
|
284
|
-
const
|
|
345
|
+
const codexHome = path.resolve(params.codexHome);
|
|
346
|
+
const ownershipRoot = path.resolve(params.ownershipRoot ?? path.dirname(codexHome));
|
|
347
|
+
const targetParent = path.join(codexHome, "computer-use");
|
|
348
|
+
const targetPath = path.join(targetParent, SERVICE_APP_NAME);
|
|
349
|
+
await ensureOwnedCodexHome(codexHome, ownershipRoot);
|
|
350
|
+
await assertOwnedServicePath({
|
|
351
|
+
ownershipRoot,
|
|
352
|
+
codexHome,
|
|
353
|
+
targetParent,
|
|
354
|
+
targetPath
|
|
355
|
+
});
|
|
356
|
+
const candidates = params.sourceAppCandidates ?? resolveMacOSDesktopCodexComputerUseServiceAppCandidates(platform, params.appServerCommand);
|
|
357
|
+
const syncKey = [targetPath, ...candidates].join("\0");
|
|
358
|
+
const completed = completedSyncs.get(targetPath);
|
|
359
|
+
if (completed?.syncKey === syncKey) {
|
|
360
|
+
completedSyncs.delete(targetPath);
|
|
361
|
+
completedSyncs.set(targetPath, completed);
|
|
362
|
+
return {
|
|
363
|
+
status: "already_current",
|
|
364
|
+
changed: false,
|
|
365
|
+
targetPath: completed.targetPath,
|
|
366
|
+
sourcePath: completed.sourcePath,
|
|
367
|
+
sourceBuild: completed.sourceBuild
|
|
368
|
+
};
|
|
369
|
+
}
|
|
285
370
|
const active = activeInstalls.get(targetPath);
|
|
286
|
-
if (active)
|
|
371
|
+
if (active) {
|
|
372
|
+
if (active.syncKey === syncKey) return await active.promise;
|
|
373
|
+
await active.promise.catch(() => void 0);
|
|
374
|
+
return await ensureCodexComputerUseServiceApp(params);
|
|
375
|
+
}
|
|
376
|
+
if (completed) completedSyncs.delete(targetPath);
|
|
287
377
|
const install = ensureCodexComputerUseServiceAppOnce({
|
|
288
378
|
...params,
|
|
379
|
+
codexHome,
|
|
380
|
+
ownershipRoot,
|
|
381
|
+
targetParent,
|
|
289
382
|
targetPath,
|
|
290
|
-
platform
|
|
383
|
+
platform,
|
|
384
|
+
sourceAppCandidates: candidates
|
|
385
|
+
}).then((result) => {
|
|
386
|
+
if (isCompletedSyncStatus(result.status)) {
|
|
387
|
+
completedSyncs.delete(targetPath);
|
|
388
|
+
completedSyncs.set(targetPath, {
|
|
389
|
+
syncKey,
|
|
390
|
+
targetPath: result.targetPath,
|
|
391
|
+
sourcePath: result.sourcePath,
|
|
392
|
+
sourceBuild: result.sourceBuild
|
|
393
|
+
});
|
|
394
|
+
pruneMapToMaxSize(completedSyncs, COMPLETED_SYNC_CACHE_MAX_ENTRIES);
|
|
395
|
+
}
|
|
396
|
+
return result;
|
|
291
397
|
});
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
|
|
398
|
+
const activeEntry = {
|
|
399
|
+
syncKey,
|
|
400
|
+
promise: install
|
|
401
|
+
};
|
|
402
|
+
activeInstalls.set(targetPath, activeEntry);
|
|
403
|
+
const clearActive = () => {
|
|
404
|
+
if (activeInstalls.get(targetPath) === activeEntry) activeInstalls.delete(targetPath);
|
|
405
|
+
};
|
|
406
|
+
install.then(clearActive, clearActive);
|
|
407
|
+
return await install;
|
|
408
|
+
}
|
|
409
|
+
function isCompletedSyncStatus(status) {
|
|
410
|
+
return status === "installed" || status === "refreshed" || status === "already_current";
|
|
298
411
|
}
|
|
299
412
|
async function ensureCodexComputerUseServiceAppOnce(params) {
|
|
300
|
-
|
|
301
|
-
|
|
413
|
+
const inspectServiceApp = params.inspectServiceApp ?? inspectTrustedServiceApp;
|
|
414
|
+
const source = await findUsableServiceApp(params.sourceAppCandidates ?? [], inspectServiceApp);
|
|
415
|
+
if (!source) return {
|
|
416
|
+
status: "source_missing",
|
|
302
417
|
changed: false,
|
|
303
418
|
targetPath: params.targetPath
|
|
304
419
|
};
|
|
305
|
-
const sourcePath
|
|
306
|
-
|
|
307
|
-
|
|
420
|
+
const { path: sourcePath, identity: sourceIdentity } = source;
|
|
421
|
+
const ownedParent = await prepareOwnedServiceParent({
|
|
422
|
+
ownershipRoot: params.ownershipRoot,
|
|
423
|
+
codexHome: params.codexHome,
|
|
424
|
+
targetParent: params.targetParent
|
|
425
|
+
});
|
|
426
|
+
const operationTargetPath = path.join(ownedParent.realPath, SERVICE_APP_NAME);
|
|
427
|
+
await assertNotSymlink(operationTargetPath, "Computer Use service target");
|
|
428
|
+
const initialTarget = await readServiceAppSnapshot(operationTargetPath, inspectServiceApp);
|
|
429
|
+
if (initialTarget.identity && identitiesMatch(initialTarget.identity, sourceIdentity)) return {
|
|
430
|
+
status: "already_current",
|
|
308
431
|
changed: false,
|
|
309
|
-
targetPath: params.targetPath
|
|
432
|
+
targetPath: params.targetPath,
|
|
433
|
+
sourcePath,
|
|
434
|
+
sourceBuild: sourceIdentity.build
|
|
310
435
|
};
|
|
311
|
-
|
|
312
|
-
await fs.
|
|
313
|
-
const
|
|
436
|
+
await assertOwnedServiceParentStable(ownedParent);
|
|
437
|
+
const stagingRoot = await fs.mkdtemp(path.join(ownedParent.realPath, ".service-app.staging-"));
|
|
438
|
+
const stagingRootIdentity = await readRealDirectoryIdentity(stagingRoot, "Computer Use service staging directory");
|
|
314
439
|
const stagedPath = path.join(stagingRoot, SERVICE_APP_NAME);
|
|
315
|
-
const backupPath = path.join(
|
|
440
|
+
const backupPath = path.join(ownedParent.realPath, `.service-app.backup-${process.pid}-${Date.now()}`);
|
|
316
441
|
let backupCreated = false;
|
|
317
442
|
try {
|
|
318
443
|
await (params.copyServiceApp ?? copyServiceAppWithDitto)(sourcePath, stagedPath);
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
444
|
+
await assertOwnedServiceParentStable(ownedParent);
|
|
445
|
+
await assertDirectoryIdentityStable(stagingRootIdentity, "Computer Use service staging directory");
|
|
446
|
+
await assertNotSymlink(stagedPath, "Copied Computer Use service app");
|
|
447
|
+
const stagedIdentity = await inspectServiceApp(stagedPath);
|
|
448
|
+
if (!stagedIdentity || !identitiesMatch(stagedIdentity, sourceIdentity)) throw new Error(`Copied Computer Use service app at ${stagedPath} does not match its selected signed source.`);
|
|
449
|
+
const stagedSnapshot = await readServiceAppSnapshot(stagedPath, inspectServiceApp);
|
|
450
|
+
const currentSourceIdentity = await inspectServiceApp(sourcePath);
|
|
451
|
+
await assertOwnedServiceParentStable(ownedParent);
|
|
452
|
+
if (!currentSourceIdentity || !identitiesMatch(currentSourceIdentity, sourceIdentity)) throw new Error("Selected Computer Use service source changed during refresh.");
|
|
453
|
+
await assertNotSymlink(operationTargetPath, "Computer Use service target");
|
|
454
|
+
if (await pathExists$1(operationTargetPath)) {
|
|
455
|
+
await assertOwnedServiceParentStable(ownedParent);
|
|
456
|
+
await fs.rename(operationTargetPath, backupPath);
|
|
457
|
+
await assertOwnedServiceParentStable(ownedParent);
|
|
322
458
|
backupCreated = true;
|
|
323
|
-
|
|
324
|
-
|
|
459
|
+
const movedTarget = await readServiceAppSnapshot(backupPath, inspectServiceApp);
|
|
460
|
+
if (movedTarget.identity && identitiesMatch(movedTarget.identity, sourceIdentity)) {
|
|
461
|
+
await assertOwnedServiceParentStable(ownedParent);
|
|
462
|
+
await fs.rename(backupPath, operationTargetPath);
|
|
463
|
+
await assertOwnedServiceParentStable(ownedParent);
|
|
325
464
|
backupCreated = false;
|
|
326
465
|
return {
|
|
327
|
-
status: "
|
|
466
|
+
status: "already_current",
|
|
328
467
|
changed: false,
|
|
329
468
|
targetPath: params.targetPath,
|
|
330
|
-
sourcePath
|
|
469
|
+
sourcePath,
|
|
470
|
+
sourceBuild: sourceIdentity.build
|
|
331
471
|
};
|
|
332
472
|
}
|
|
473
|
+
if (!snapshotsMatch(initialTarget, movedTarget)) {
|
|
474
|
+
await assertOwnedServiceParentStable(ownedParent);
|
|
475
|
+
await fs.rename(backupPath, operationTargetPath);
|
|
476
|
+
await assertOwnedServiceParentStable(ownedParent);
|
|
477
|
+
backupCreated = false;
|
|
478
|
+
throw new Error("Computer Use service target changed to an unexpected generation during refresh.");
|
|
479
|
+
}
|
|
333
480
|
}
|
|
334
481
|
try {
|
|
335
|
-
await
|
|
482
|
+
await assertOwnedServiceParentStable(ownedParent);
|
|
483
|
+
await fs.rename(stagedPath, operationTargetPath);
|
|
484
|
+
await assertOwnedServiceParentStable(ownedParent);
|
|
336
485
|
} catch (error) {
|
|
337
|
-
|
|
486
|
+
await assertOwnedServiceParentStable(ownedParent);
|
|
487
|
+
await assertNotSymlink(operationTargetPath, "Computer Use service target");
|
|
488
|
+
const winnerIdentity = await inspectServiceApp(operationTargetPath);
|
|
489
|
+
if (!winnerIdentity || !identitiesMatch(winnerIdentity, sourceIdentity)) {
|
|
338
490
|
if (backupCreated) {
|
|
339
|
-
await
|
|
340
|
-
|
|
491
|
+
if (!await pathExists$1(operationTargetPath)) {
|
|
492
|
+
await assertOwnedServiceParentStable(ownedParent);
|
|
493
|
+
await fs.rename(backupPath, operationTargetPath);
|
|
494
|
+
await assertOwnedServiceParentStable(ownedParent);
|
|
495
|
+
backupCreated = false;
|
|
496
|
+
}
|
|
341
497
|
}
|
|
342
498
|
throw error;
|
|
343
499
|
}
|
|
344
500
|
if (backupCreated) {
|
|
501
|
+
await assertOwnedServiceParentStable(ownedParent);
|
|
502
|
+
await assertNotSymlink(backupPath, "Computer Use service backup");
|
|
345
503
|
await fs.rm(backupPath, {
|
|
346
504
|
recursive: true,
|
|
347
505
|
force: true
|
|
348
506
|
});
|
|
507
|
+
await assertOwnedServiceParentStable(ownedParent);
|
|
349
508
|
backupCreated = false;
|
|
350
509
|
}
|
|
351
510
|
return {
|
|
352
|
-
status: "
|
|
511
|
+
status: "already_current",
|
|
353
512
|
changed: false,
|
|
354
513
|
targetPath: params.targetPath,
|
|
355
|
-
sourcePath
|
|
514
|
+
sourcePath,
|
|
515
|
+
sourceBuild: sourceIdentity.build
|
|
356
516
|
};
|
|
357
517
|
}
|
|
518
|
+
await assertNotSymlink(operationTargetPath, "Installed Computer Use service app");
|
|
519
|
+
const installedSnapshot = await readServiceAppSnapshot(operationTargetPath, inspectServiceApp);
|
|
520
|
+
if (!installedSnapshot.identity || !identitiesMatch(installedSnapshot.identity, sourceIdentity)) {
|
|
521
|
+
if (filesystemSnapshotsMatch(installedSnapshot, stagedSnapshot)) {
|
|
522
|
+
await assertOwnedServiceParentStable(ownedParent);
|
|
523
|
+
await fs.rm(operationTargetPath, {
|
|
524
|
+
recursive: true,
|
|
525
|
+
force: true
|
|
526
|
+
});
|
|
527
|
+
await assertOwnedServiceParentStable(ownedParent);
|
|
528
|
+
if (backupCreated) {
|
|
529
|
+
await fs.rename(backupPath, operationTargetPath);
|
|
530
|
+
await assertOwnedServiceParentStable(ownedParent);
|
|
531
|
+
backupCreated = false;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
throw new Error("Installed Computer Use service app failed post-install identity verification.");
|
|
535
|
+
}
|
|
358
536
|
if (backupCreated) {
|
|
537
|
+
await assertOwnedServiceParentStable(ownedParent);
|
|
538
|
+
await assertNotSymlink(backupPath, "Computer Use service backup");
|
|
359
539
|
await fs.rm(backupPath, {
|
|
360
540
|
recursive: true,
|
|
361
541
|
force: true
|
|
362
542
|
});
|
|
543
|
+
await assertOwnedServiceParentStable(ownedParent);
|
|
363
544
|
backupCreated = false;
|
|
364
545
|
}
|
|
365
546
|
return {
|
|
366
|
-
status: "installed",
|
|
547
|
+
status: initialTarget.exists ? "refreshed" : "installed",
|
|
367
548
|
changed: true,
|
|
368
549
|
targetPath: params.targetPath,
|
|
369
|
-
sourcePath
|
|
550
|
+
sourcePath,
|
|
551
|
+
sourceBuild: sourceIdentity.build,
|
|
552
|
+
...initialTarget.identity ? { previousBuild: initialTarget.identity.build } : {}
|
|
370
553
|
};
|
|
554
|
+
} catch (error) {
|
|
555
|
+
if (backupCreated && await ownedServiceParentIsStable(ownedParent) && !await pathExists$1(operationTargetPath)) {
|
|
556
|
+
await assertOwnedServiceParentStable(ownedParent);
|
|
557
|
+
await fs.rename(backupPath, operationTargetPath);
|
|
558
|
+
backupCreated = false;
|
|
559
|
+
}
|
|
560
|
+
throw error;
|
|
371
561
|
} finally {
|
|
372
|
-
await fs.rm(stagingRoot, {
|
|
562
|
+
if (await ownedServiceParentIsStable(ownedParent) && await directoryIdentityIsStable(stagingRootIdentity)) await fs.rm(stagingRoot, {
|
|
373
563
|
recursive: true,
|
|
374
564
|
force: true
|
|
375
565
|
});
|
|
@@ -378,8 +568,43 @@ async function ensureCodexComputerUseServiceAppOnce(params) {
|
|
|
378
568
|
async function pathExists$1(filePath) {
|
|
379
569
|
return await fs.lstat(filePath).then(() => true, () => false);
|
|
380
570
|
}
|
|
381
|
-
async function findUsableServiceApp(candidates) {
|
|
382
|
-
for (const candidate of candidates)
|
|
571
|
+
async function findUsableServiceApp(candidates, inspectServiceApp) {
|
|
572
|
+
for (const candidate of candidates) {
|
|
573
|
+
const identity = await inspectServiceApp(candidate);
|
|
574
|
+
if (identity) return {
|
|
575
|
+
path: candidate,
|
|
576
|
+
identity
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
async function readServiceAppSnapshot(appPath, inspectServiceApp) {
|
|
581
|
+
if (!await pathExists$1(appPath)) return { exists: false };
|
|
582
|
+
return {
|
|
583
|
+
exists: true,
|
|
584
|
+
identity: await inspectServiceApp(appPath),
|
|
585
|
+
filesystemKey: await readServiceAppFilesystemKey(appPath)
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
async function readServiceAppFilesystemKey(appPath) {
|
|
589
|
+
const paths = [
|
|
590
|
+
appPath,
|
|
591
|
+
path.join(appPath, "Contents", "Info.plist"),
|
|
592
|
+
path.join(appPath, CLIENT_RELATIVE_PATH)
|
|
593
|
+
];
|
|
594
|
+
return (await Promise.all(paths.map(async (entryPath) => await fs.stat(entryPath).then((stat) => `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}`, () => "missing")))).join("|");
|
|
595
|
+
}
|
|
596
|
+
function snapshotsMatch(left, right) {
|
|
597
|
+
if (left.exists !== right.exists) return false;
|
|
598
|
+
if (!left.exists) return true;
|
|
599
|
+
if (left.filesystemKey && right.filesystemKey && left.filesystemKey !== right.filesystemKey) return false;
|
|
600
|
+
if (left.identity || right.identity) return Boolean(left.identity && right.identity && identitiesMatch(left.identity, right.identity));
|
|
601
|
+
return left.filesystemKey !== void 0 && left.filesystemKey === right.filesystemKey;
|
|
602
|
+
}
|
|
603
|
+
function filesystemSnapshotsMatch(left, right) {
|
|
604
|
+
return Boolean(left.exists && right.exists && left.filesystemKey && right.filesystemKey && left.filesystemKey === right.filesystemKey);
|
|
605
|
+
}
|
|
606
|
+
function identitiesMatch(left, right) {
|
|
607
|
+
return left.bundleId === right.bundleId && left.version === right.version && left.build === right.build && left.cdHash === right.cdHash && left.teamId === right.teamId && left.clientBundleId === right.clientBundleId && left.clientCdHash === right.clientCdHash && left.clientTeamId === right.clientTeamId;
|
|
383
608
|
}
|
|
384
609
|
async function hasExecutableClient(appPath) {
|
|
385
610
|
try {
|
|
@@ -389,6 +614,90 @@ async function hasExecutableClient(appPath) {
|
|
|
389
614
|
return false;
|
|
390
615
|
}
|
|
391
616
|
}
|
|
617
|
+
async function inspectTrustedServiceApp(appPath) {
|
|
618
|
+
if (!await hasExecutableClient(appPath)) return;
|
|
619
|
+
const clientAppPath = path.join(appPath, CLIENT_APP_RELATIVE_PATH);
|
|
620
|
+
try {
|
|
621
|
+
await verifyTrustedBundle(appPath, SERVICE_BUNDLE_ID, true);
|
|
622
|
+
await verifyTrustedBundle(clientAppPath, CLIENT_BUNDLE_ID, false);
|
|
623
|
+
const [info, serviceSignature, clientSignature] = await Promise.all([
|
|
624
|
+
readBundleInfo(appPath),
|
|
625
|
+
readCodeSignature(appPath),
|
|
626
|
+
readCodeSignature(clientAppPath)
|
|
627
|
+
]);
|
|
628
|
+
if (!info || serviceSignature.identifier !== SERVICE_BUNDLE_ID || serviceSignature.teamId !== OPENAI_TEAM_ID || clientSignature.identifier !== CLIENT_BUNDLE_ID || clientSignature.teamId !== OPENAI_TEAM_ID) return;
|
|
629
|
+
return {
|
|
630
|
+
bundleId: serviceSignature.identifier,
|
|
631
|
+
version: info.version,
|
|
632
|
+
build: info.build,
|
|
633
|
+
cdHash: serviceSignature.cdHash,
|
|
634
|
+
teamId: serviceSignature.teamId,
|
|
635
|
+
clientBundleId: clientSignature.identifier,
|
|
636
|
+
clientCdHash: clientSignature.cdHash,
|
|
637
|
+
clientTeamId: clientSignature.teamId
|
|
638
|
+
};
|
|
639
|
+
} catch {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
async function verifyTrustedBundle(appPath, bundleId, deep) {
|
|
644
|
+
const requirement = `anchor apple generic and certificate leaf[subject.OU] = "${OPENAI_TEAM_ID}" and identifier "${bundleId}"`;
|
|
645
|
+
await runExec("/usr/bin/codesign", [
|
|
646
|
+
"--verify",
|
|
647
|
+
"--strict",
|
|
648
|
+
...deep ? ["--deep"] : [],
|
|
649
|
+
`-R=${requirement}`,
|
|
650
|
+
appPath
|
|
651
|
+
], {
|
|
652
|
+
logOutput: false,
|
|
653
|
+
timeoutMs: INSPECT_TIMEOUT_MS
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
async function readBundleInfo(appPath) {
|
|
657
|
+
const result = await runExec("/usr/bin/plutil", [
|
|
658
|
+
"-convert",
|
|
659
|
+
"json",
|
|
660
|
+
"-o",
|
|
661
|
+
"-",
|
|
662
|
+
"--",
|
|
663
|
+
path.join(appPath, "Contents", "Info.plist")
|
|
664
|
+
], {
|
|
665
|
+
logOutput: false,
|
|
666
|
+
timeoutMs: INSPECT_TIMEOUT_MS
|
|
667
|
+
});
|
|
668
|
+
const parsed = JSON.parse(result.stdout);
|
|
669
|
+
if (!isRecord(parsed)) return;
|
|
670
|
+
const version = parsed.CFBundleShortVersionString;
|
|
671
|
+
const build = parsed.CFBundleVersion;
|
|
672
|
+
return typeof version === "string" && version && typeof build === "string" && build ? {
|
|
673
|
+
version,
|
|
674
|
+
build
|
|
675
|
+
} : void 0;
|
|
676
|
+
}
|
|
677
|
+
async function readCodeSignature(appPath) {
|
|
678
|
+
const result = await runExec("/usr/bin/codesign", [
|
|
679
|
+
"-d",
|
|
680
|
+
"--verbose=4",
|
|
681
|
+
appPath
|
|
682
|
+
], {
|
|
683
|
+
logOutput: false,
|
|
684
|
+
timeoutMs: INSPECT_TIMEOUT_MS
|
|
685
|
+
});
|
|
686
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
687
|
+
const identifier = readCodeSignField(output, "Identifier");
|
|
688
|
+
const teamId = readCodeSignField(output, "TeamIdentifier");
|
|
689
|
+
const cdHash = readCodeSignField(output, "CDHash").toLowerCase();
|
|
690
|
+
if (!identifier || !teamId || !/^[a-f0-9]+$/.test(cdHash)) throw new Error(`Could not inspect the signed identity at ${appPath}.`);
|
|
691
|
+
return {
|
|
692
|
+
identifier,
|
|
693
|
+
teamId,
|
|
694
|
+
cdHash
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
function readCodeSignField(output, field) {
|
|
698
|
+
const prefix = `${field}=`;
|
|
699
|
+
return output.split(/\r?\n/u).find((candidate) => candidate.startsWith(prefix))?.slice(prefix.length).trim() ?? "";
|
|
700
|
+
}
|
|
392
701
|
async function copyServiceAppWithDitto(sourcePath, targetPath) {
|
|
393
702
|
await runExec("/usr/bin/ditto", [
|
|
394
703
|
"--noqtn",
|
|
@@ -408,8 +717,8 @@ function flattenCodexDynamicToolFunctions(tools) {
|
|
|
408
717
|
}
|
|
409
718
|
const CODEX_INTERACTIVE_THREAD_SOURCE_KINDS = ["cli", "vscode"];
|
|
410
719
|
const CODEX_INTERACTIVE_CUSTOM_THREAD_SOURCES = ["atlas", "chatgpt"];
|
|
411
|
-
function isJsonObject
|
|
412
|
-
return
|
|
720
|
+
function isJsonObject(value) {
|
|
721
|
+
return isRecord(value);
|
|
413
722
|
}
|
|
414
723
|
function isRpcResponse(message) {
|
|
415
724
|
return "id" in message && !("method" in message);
|
|
@@ -436,24 +745,41 @@ const CODEX_HOME_DIRNAME = ".codex";
|
|
|
436
745
|
const scopedOAuthRefreshQueues = /* @__PURE__ */ new WeakMap();
|
|
437
746
|
async function bridgeCodexAppServerStartOptions(params) {
|
|
438
747
|
if (params.startOptions.transport !== "stdio") return params.startOptions;
|
|
439
|
-
const
|
|
440
|
-
if (params.preparedAuth) return withClearedEnvironmentVariables(
|
|
441
|
-
if (params.authProfileId === null) return
|
|
748
|
+
const scopeStartOptions = () => withCodexHomeEnvironment(withEphemeralCodexAuthStore(params), params.agentDir, params.pluginConfig);
|
|
749
|
+
if (params.preparedAuth) return withClearedEnvironmentVariables(await scopeStartOptions(), CODEX_APP_SERVER_PREPARED_AUTH_ENV_VARS);
|
|
750
|
+
if (params.authProfileId === null) return scopeStartOptions();
|
|
442
751
|
const store = resolveCodexAppServerAuthProfileStore({
|
|
443
752
|
agentDir: params.agentDir,
|
|
444
753
|
authProfileId: params.authProfileId,
|
|
445
754
|
authProfileStore: params.authProfileStore,
|
|
446
755
|
config: params.config
|
|
447
756
|
});
|
|
757
|
+
const authProfileId = resolveCodexAppServerAuthProfileId({
|
|
758
|
+
authProfileId: params.authProfileId,
|
|
759
|
+
store,
|
|
760
|
+
config: params.config
|
|
761
|
+
});
|
|
762
|
+
if (!authProfileId) assertNoUnimportedAgentCodexAuthFile(params);
|
|
763
|
+
const scopedStartOptions = await scopeStartOptions();
|
|
448
764
|
return shouldClearOpenAiApiKeyForCodexAuthProfile({
|
|
449
765
|
store,
|
|
450
|
-
authProfileId
|
|
451
|
-
authProfileId: params.authProfileId,
|
|
452
|
-
store,
|
|
453
|
-
config: params.config
|
|
454
|
-
})
|
|
766
|
+
authProfileId
|
|
455
767
|
}) ? withClearedEnvironmentVariables(scopedStartOptions, CODEX_APP_SERVER_API_KEY_ENV_VARS) : scopedStartOptions;
|
|
456
768
|
}
|
|
769
|
+
function assertNoUnimportedAgentCodexAuthFile(params) {
|
|
770
|
+
if (params.authRequirement === "api-key" && resolveCodexAppServerFallbackApiKeyCacheKey({ startOptions: params.startOptions })) return;
|
|
771
|
+
const message = resolveUnimportedAgentCodexAuthMessage(params);
|
|
772
|
+
if (message) throw new AgentHarnessPreflightError(message);
|
|
773
|
+
}
|
|
774
|
+
function resolveUnimportedAgentCodexAuthMessage(params) {
|
|
775
|
+
const managedCodexCli = params.startOptions.commandSource === "managed" || params.startOptions.commandSource === "resolved-managed";
|
|
776
|
+
if (params.startOptions.transport !== "stdio" || !managedCodexCli || params.startOptions.homeScope === "user") return;
|
|
777
|
+
const codexHome = resolveCodexAppServerHomeDir(params.agentDir);
|
|
778
|
+
const authPath = path.join(codexHome, CODEX_AUTH_JSON_FILENAME);
|
|
779
|
+
if (!fsSync.existsSync(authPath)) return;
|
|
780
|
+
const targetAgentId = params.agentId?.trim() || "<agent-id>";
|
|
781
|
+
return `A Codex auth file exists at ${authPath}, but agent-scoped Codex runs use OpenClaw's auth store and do not read that file. Preview only that credential import with \`openclaw migrate plan codex --from <codex-home> --agent ${targetAgentId} --include-secrets --item auth:openai\`, then run \`openclaw migrate apply codex --from <codex-home> --agent ${targetAgentId} --include-secrets --item auth:openai --yes\`. If the plan finds no credentials, remove the stale auth file.`;
|
|
782
|
+
}
|
|
457
783
|
function resolveCodexAppServerAuthProfileId(params) {
|
|
458
784
|
const requested = params.authProfileId?.trim();
|
|
459
785
|
if (requested) return requested;
|
|
@@ -518,15 +844,20 @@ async function resolveCodexAppServerPreparedAuthProfileSnapshot(params) {
|
|
|
518
844
|
if (!loginParams) return;
|
|
519
845
|
const accountId = loginParams.type === "chatgptAuthTokens" ? loginParams.chatgptAccountId : resolveChatgptAccountId(profileId, credential);
|
|
520
846
|
const stableChatgptAccountId = resolveStableChatgptAccountId(credential);
|
|
847
|
+
const secretFreeCacheKey = credential.type === "api_key" && loginParams.type === "apiKey" ? `${accountId}:${fingerprintApiKeyAuthProfileCacheKey(loginParams.apiKey)}` : loginParams.type === "chatgptAuthTokens" && (credential.type === "token" || !stableChatgptAccountId) ? `${accountId}:${fingerprintTokenAuthProfileCacheKey(loginParams.accessToken)}` : accountId;
|
|
848
|
+
const chatgptAccountId = resolveExplicitChatgptAccountId(credential);
|
|
521
849
|
return {
|
|
522
850
|
loginParams,
|
|
523
|
-
secretFreeCacheKey
|
|
851
|
+
secretFreeCacheKey,
|
|
852
|
+
...chatgptAccountId ? { chatgptAccountId } : {}
|
|
524
853
|
};
|
|
525
854
|
}
|
|
526
855
|
/** Maps one prepared route to one mutually exclusive app-server auth handoff. */
|
|
527
856
|
async function resolveCodexAppServerPreparedAuthHandoff(params) {
|
|
528
857
|
const usesNativeHome = params.homeScope === "user";
|
|
529
|
-
if (params.
|
|
858
|
+
if (params.requirePreparedAuth && usesNativeHome) throw createCodexAppServerAuthError("Codex remote-exec cloud placement requires prepared OpenAI auth. Configure an OpenAI API-key, OAuth, or token profile and use appServer.homeScope=\"agent\"; ambient credentials and native Codex auth are not allowed.");
|
|
859
|
+
if (usesNativeHome) return { nativeAuthProfile: true };
|
|
860
|
+
if (params.authRequirement === "api-key") {
|
|
530
861
|
const apiKey = params.resolvedApiKey?.trim();
|
|
531
862
|
if (!apiKey) throw new Error("Prepared Codex API-key route is missing its resolved API key.");
|
|
532
863
|
return {
|
|
@@ -544,18 +875,18 @@ async function resolveCodexAppServerPreparedAuthHandoff(params) {
|
|
|
544
875
|
agentDir: params.agentDir,
|
|
545
876
|
config: params.config
|
|
546
877
|
});
|
|
547
|
-
if (
|
|
878
|
+
if (params.authRequirement !== "subscription" && !params.requirePreparedAuth) return {
|
|
548
879
|
authProfileId,
|
|
549
880
|
nativeAuthProfile
|
|
550
881
|
};
|
|
551
|
-
if (!authProfileId || !nativeAuthProfile) throw createCodexAppServerAuthError(params.subscriptionProfileRequiredError);
|
|
882
|
+
if (!authProfileId || params.authRequirement === "subscription" && !nativeAuthProfile) throw createCodexAppServerAuthError(params.requirePreparedAuth ? "Codex remote-exec cloud placement requires prepared OpenAI auth. Configure an OpenAI API-key, OAuth, or token profile; ambient CODEX_API_KEY, OPENAI_API_KEY, and native Codex auth are not allowed." : params.subscriptionProfileRequiredError);
|
|
552
883
|
const snapshot = await resolveCodexAppServerPreparedAuthProfileSnapshot({
|
|
553
884
|
authProfileId,
|
|
554
885
|
authProfileStore: params.authProfileStore,
|
|
555
886
|
agentDir: params.agentDir,
|
|
556
887
|
config: params.config
|
|
557
888
|
});
|
|
558
|
-
if (!snapshot) throw createCodexAppServerAuthError(params.subscriptionProfileUnusableError);
|
|
889
|
+
if (!snapshot) throw createCodexAppServerAuthError(params.requirePreparedAuth ? "Codex remote-exec cloud placement could not prepare the selected OpenAI auth profile. Repair or replace the profile, then retry." : params.subscriptionProfileUnusableError);
|
|
559
890
|
return {
|
|
560
891
|
authProfileId,
|
|
561
892
|
nativeAuthProfile,
|
|
@@ -644,7 +975,7 @@ function fingerprintCodexCliAuthFileApiKeyCacheKey(apiKey) {
|
|
|
644
975
|
return `CODEX_AUTH_JSON:sha256:${hash.digest("hex")}`;
|
|
645
976
|
}
|
|
646
977
|
async function withCodexHomeEnvironment(startOptions, agentDir, pluginConfig) {
|
|
647
|
-
const codexHome =
|
|
978
|
+
const codexHome = resolveCodexAppServerLocalHomeDir(startOptions, agentDir);
|
|
648
979
|
const nativeHome = startOptions.env?.[HOME_ENV_VAR]?.trim() ? startOptions.env[HOME_ENV_VAR] : void 0;
|
|
649
980
|
await fs.mkdir(codexHome, { recursive: true });
|
|
650
981
|
const computerUseConfig = resolveCodexComputerUseConfig({ pluginConfig });
|
|
@@ -652,13 +983,18 @@ async function withCodexHomeEnvironment(startOptions, agentDir, pluginConfig) {
|
|
|
652
983
|
codexHome,
|
|
653
984
|
config: computerUseConfig
|
|
654
985
|
});
|
|
655
|
-
|
|
986
|
+
const ownsIsolatedCodexHome = startOptions.homeScope !== "user" && !startOptions.env?.[CODEX_HOME_ENV_VAR]?.trim();
|
|
987
|
+
if (computerUseConfig.enabled && computerUseConfig.autoInstall && ownsIsolatedCodexHome) try {
|
|
656
988
|
await ensureCodexComputerUseServiceApp({
|
|
657
989
|
codexHome,
|
|
990
|
+
ownershipRoot: agentDir,
|
|
658
991
|
appServerCommand: startOptions.command
|
|
659
992
|
});
|
|
660
993
|
} catch (error) {
|
|
661
|
-
throw new AgentHarnessPreflightError("Codex Computer Use client provisioning failed.", {
|
|
994
|
+
throw new AgentHarnessPreflightError("Codex Computer Use client provisioning failed.", {
|
|
995
|
+
cause: error,
|
|
996
|
+
scope: "harness"
|
|
997
|
+
});
|
|
662
998
|
}
|
|
663
999
|
if (nativeHome) await fs.mkdir(nativeHome, { recursive: true });
|
|
664
1000
|
const nextStartOptions = {
|
|
@@ -733,7 +1069,7 @@ async function applyCodexAppServerAuthProfile(params) {
|
|
|
733
1069
|
async function assertNativeCodexAccountMatchesRoute(client, authRequirement) {
|
|
734
1070
|
if (!authRequirement) return;
|
|
735
1071
|
const response = await client.request("account/read", { refreshToken: false });
|
|
736
|
-
const accountType = isJsonObject
|
|
1072
|
+
const accountType = isJsonObject(response.account) ? response.account.type : void 0;
|
|
737
1073
|
if (authRequirement === "subscription") {
|
|
738
1074
|
if (accountType !== "chatgpt") throw createCodexAppServerAuthError("Codex subscription auth profile could not produce login credentials.");
|
|
739
1075
|
return;
|
|
@@ -1019,11 +1355,13 @@ function resolveChatgptAccountId(profileId, credential) {
|
|
|
1019
1355
|
return resolveStableChatgptAccountId(credential) ?? profileId;
|
|
1020
1356
|
}
|
|
1021
1357
|
function resolveStableChatgptAccountId(credential) {
|
|
1358
|
+
return resolveExplicitChatgptAccountId(credential) ?? (credential.email?.trim() || void 0);
|
|
1359
|
+
}
|
|
1360
|
+
function resolveExplicitChatgptAccountId(credential) {
|
|
1022
1361
|
if ("accountId" in credential && typeof credential.accountId === "string") {
|
|
1023
1362
|
const accountId = credential.accountId.trim();
|
|
1024
1363
|
if (accountId) return accountId;
|
|
1025
1364
|
}
|
|
1026
|
-
return credential.email?.trim() || void 0;
|
|
1027
1365
|
}
|
|
1028
1366
|
//#endregion
|
|
1029
1367
|
//#region extensions/codex/src/app-server/rate-limit-cache.ts
|
|
@@ -1048,7 +1386,7 @@ function rememberCodexRateLimitsRead(client, value, nowMs = Date.now()) {
|
|
|
1048
1386
|
}
|
|
1049
1387
|
/** Merges a sparse rolling notification into one physical client's latest read response. */
|
|
1050
1388
|
function mergeCodexRateLimitsUpdate(client, value, nowMs = Date.now()) {
|
|
1051
|
-
const update = isJsonObject
|
|
1389
|
+
const update = isJsonObject(value) && isJsonObject(value.rateLimits) ? value.rateLimits : void 0;
|
|
1052
1390
|
if (!update) return;
|
|
1053
1391
|
const currentState = rateLimitsByClient.get(client);
|
|
1054
1392
|
const current = currentState?.value;
|
|
@@ -1075,13 +1413,13 @@ function readRecentCodexRateLimits(client, options) {
|
|
|
1075
1413
|
return maxAgeMs >= 0 && nowMs - state.updatedAtMs > maxAgeMs ? void 0 : state.value;
|
|
1076
1414
|
}
|
|
1077
1415
|
function mergeRateLimitUpdate(current, update) {
|
|
1078
|
-
const currentEnvelope = isJsonObject
|
|
1079
|
-
const currentPrimary = currentEnvelope && isJsonObject
|
|
1080
|
-
const currentByLimitId = currentEnvelope && isJsonObject
|
|
1416
|
+
const currentEnvelope = isJsonObject(current) ? current : void 0;
|
|
1417
|
+
const currentPrimary = currentEnvelope && isJsonObject(currentEnvelope.rateLimits) ? currentEnvelope.rateLimits : void 0;
|
|
1418
|
+
const currentByLimitId = currentEnvelope && isJsonObject(currentEnvelope.rateLimitsByLimitId) ? currentEnvelope.rateLimitsByLimitId : void 0;
|
|
1081
1419
|
const limitId = readLimitId(update);
|
|
1082
1420
|
const currentPrimaryLimitId = currentPrimary ? readLimitId(currentPrimary) : void 0;
|
|
1083
|
-
const currentForLimit = (currentByLimitId && isJsonObject
|
|
1084
|
-
const merged = mergeSparseSnapshot(isJsonObject
|
|
1421
|
+
const currentForLimit = (currentByLimitId && isJsonObject(currentByLimitId[limitId]) ? currentByLimitId[limitId] : void 0) ?? (currentPrimaryLimitId === limitId ? currentPrimary : void 0);
|
|
1422
|
+
const merged = mergeSparseSnapshot(isJsonObject(currentForLimit) ? currentForLimit : void 0, currentPrimary, update, limitId);
|
|
1085
1423
|
const nextPrimary = !currentPrimary || currentPrimaryLimitId === limitId ? merged : currentPrimary;
|
|
1086
1424
|
let nextByLimitId;
|
|
1087
1425
|
if (currentByLimitId) nextByLimitId = {
|
|
@@ -1099,11 +1437,11 @@ function mergeRateLimitUpdate(current, update) {
|
|
|
1099
1437
|
};
|
|
1100
1438
|
}
|
|
1101
1439
|
function readRateLimitIds(value) {
|
|
1102
|
-
if (!isJsonObject
|
|
1440
|
+
if (!isJsonObject(value)) return [];
|
|
1103
1441
|
const ids = /* @__PURE__ */ new Set();
|
|
1104
|
-
if (isJsonObject
|
|
1105
|
-
if (isJsonObject
|
|
1106
|
-
const snapshotLimitId = isJsonObject
|
|
1442
|
+
if (isJsonObject(value.rateLimits)) ids.add(readLimitId(value.rateLimits));
|
|
1443
|
+
if (isJsonObject(value.rateLimitsByLimitId)) for (const [key, snapshot] of Object.entries(value.rateLimitsByLimitId)) {
|
|
1444
|
+
const snapshotLimitId = isJsonObject(snapshot) && typeof snapshot.limitId === "string" ? snapshot.limitId.trim() : "";
|
|
1107
1445
|
ids.add(snapshotLimitId || key);
|
|
1108
1446
|
}
|
|
1109
1447
|
return [...ids];
|
|
@@ -1126,62 +1464,337 @@ function readLimitId(snapshot) {
|
|
|
1126
1464
|
//#endregion
|
|
1127
1465
|
//#region extensions/codex/src/app-server/client-runtime.ts
|
|
1128
1466
|
/** Client-scoped Codex auth and account observers. */
|
|
1467
|
+
/** Match Codex's native grace window without retaining inactive conversations indefinitely. */
|
|
1468
|
+
const CODEX_APP_SERVER_LIVE_THREAD_IDLE_TIMEOUT_MS = 30 * 6e4;
|
|
1469
|
+
/** Native-child parents are active ownership, so only otherwise-idle threads count against this cap. */
|
|
1470
|
+
const CODEX_APP_SERVER_LIVE_THREAD_MAX_IDLE = 64;
|
|
1129
1471
|
const configuredClients = /* @__PURE__ */ new WeakMap();
|
|
1472
|
+
const physicalThreadReleases = /* @__PURE__ */ new WeakMap();
|
|
1473
|
+
const claimedThreadReleaseTokens = /* @__PURE__ */ new WeakMap();
|
|
1474
|
+
/** Only an initialized, still-open physical client can own retained native subscriptions. */
|
|
1475
|
+
function isCodexAppServerClientRuntimeLive(client) {
|
|
1476
|
+
const runtime = configuredClients.get(client);
|
|
1477
|
+
return runtime !== void 0 && !runtime.closed;
|
|
1478
|
+
}
|
|
1130
1479
|
/** Installs one auth-refresh handler and one rate-limit observer per physical client. */
|
|
1131
1480
|
function ensureCodexAppServerClientRuntime(client, context) {
|
|
1132
1481
|
const existing = configuredClients.get(client);
|
|
1133
1482
|
if (existing) {
|
|
1483
|
+
if (existing.closed) return;
|
|
1134
1484
|
existing.context = context;
|
|
1135
1485
|
return;
|
|
1136
1486
|
}
|
|
1137
|
-
const runtime = {
|
|
1487
|
+
const runtime = {
|
|
1488
|
+
context,
|
|
1489
|
+
closed: false,
|
|
1490
|
+
retainedThreads: /* @__PURE__ */ new Map(),
|
|
1491
|
+
claimedThreads: /* @__PURE__ */ new Map(),
|
|
1492
|
+
releasingThreads: /* @__PURE__ */ new Map(),
|
|
1493
|
+
protectedThreads: /* @__PURE__ */ new Map()
|
|
1494
|
+
};
|
|
1138
1495
|
configuredClients.set(client, runtime);
|
|
1496
|
+
client.addCloseHandler(() => {
|
|
1497
|
+
runtime.closed = true;
|
|
1498
|
+
if (runtime.evictionTimer) {
|
|
1499
|
+
clearTimeout(runtime.evictionTimer);
|
|
1500
|
+
runtime.evictionTimer = void 0;
|
|
1501
|
+
}
|
|
1502
|
+
runtime.retainedThreads.clear();
|
|
1503
|
+
runtime.claimedThreads.clear();
|
|
1504
|
+
runtime.protectedThreads.clear();
|
|
1505
|
+
});
|
|
1139
1506
|
client.addRequestHandler(async (request) => {
|
|
1140
1507
|
if (request.method !== "account/chatgptAuthTokens/refresh") return;
|
|
1141
1508
|
if (runtime.context.authMode === "prepared-api-key") throw new Error("ChatGPT token refresh is unavailable for prepared Codex API-key auth.");
|
|
1142
|
-
return await refreshCodexAppServerAuthTokens({
|
|
1509
|
+
return { ...await refreshCodexAppServerAuthTokens({
|
|
1143
1510
|
agentDir: runtime.context.agentDir,
|
|
1144
1511
|
authProfileId: runtime.context.authProfileId,
|
|
1145
1512
|
...runtime.context.authProfileStore ? { authProfileStore: runtime.context.authProfileStore } : {},
|
|
1146
1513
|
config: runtime.context.config
|
|
1147
|
-
});
|
|
1514
|
+
}) };
|
|
1148
1515
|
});
|
|
1149
1516
|
client.addNotificationHandler((notification) => {
|
|
1150
|
-
if (notification.method === "account/rateLimits/updated")
|
|
1517
|
+
if (notification.method === "account/rateLimits/updated") {
|
|
1518
|
+
mergeCodexRateLimitsUpdate(client, notification.params);
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
if (notification.method === "thread/archived" || notification.method === "thread/deleted" || notification.method === "thread/closed") {
|
|
1522
|
+
const threadId = notification.params?.threadId;
|
|
1523
|
+
if (typeof threadId === "string") {
|
|
1524
|
+
const releasing = runtime.releasingThreads.get(threadId);
|
|
1525
|
+
if (releasing) releasing.invalidated = true;
|
|
1526
|
+
runtime.retainedThreads.delete(threadId);
|
|
1527
|
+
runtime.claimedThreads.delete(threadId);
|
|
1528
|
+
scheduleRetainedThreadEviction(client, runtime);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1151
1531
|
});
|
|
1152
1532
|
}
|
|
1153
|
-
|
|
1154
|
-
|
|
1533
|
+
function scheduleRetainedThreadEviction(client, runtime) {
|
|
1534
|
+
if (runtime.evictionTimer) {
|
|
1535
|
+
clearTimeout(runtime.evictionTimer);
|
|
1536
|
+
runtime.evictionTimer = void 0;
|
|
1537
|
+
}
|
|
1538
|
+
if (runtime.closed) return;
|
|
1539
|
+
let expiresAt = Number.POSITIVE_INFINITY;
|
|
1540
|
+
for (const [threadId, thread] of runtime.retainedThreads) if (!runtime.protectedThreads.has(threadId)) expiresAt = Math.min(expiresAt, thread.expiresAt);
|
|
1541
|
+
if (!Number.isFinite(expiresAt)) return;
|
|
1542
|
+
runtime.evictionTimer = setTimeout(() => {
|
|
1543
|
+
runtime.evictionTimer = void 0;
|
|
1544
|
+
evictExpiredRetainedThreads(client, runtime).catch((error) => {
|
|
1545
|
+
embeddedAgentLog.warn("codex retained thread expiry failed", { reason: formatErrorMessage(error) });
|
|
1546
|
+
});
|
|
1547
|
+
}, Math.max(0, expiresAt - Date.now()));
|
|
1548
|
+
runtime.evictionTimer.unref?.();
|
|
1549
|
+
}
|
|
1550
|
+
async function releaseRetainedThread(client, runtime, threadId) {
|
|
1551
|
+
const pendingRelease = runtime.releasingThreads.get(threadId);
|
|
1552
|
+
if (pendingRelease) {
|
|
1553
|
+
await pendingRelease.completion;
|
|
1554
|
+
return false;
|
|
1555
|
+
}
|
|
1556
|
+
const retained = runtime.retainedThreads.get(threadId);
|
|
1557
|
+
if (!retained) return false;
|
|
1558
|
+
const olderThreadIds = /* @__PURE__ */ new Set();
|
|
1559
|
+
for (const candidateThreadId of runtime.retainedThreads.keys()) {
|
|
1560
|
+
if (candidateThreadId === threadId) break;
|
|
1561
|
+
olderThreadIds.add(candidateThreadId);
|
|
1562
|
+
}
|
|
1563
|
+
runtime.retainedThreads.delete(threadId);
|
|
1564
|
+
scheduleRetainedThreadEviction(client, runtime);
|
|
1565
|
+
const transition = { completion: Promise.resolve().then(() => retained.release(threadId)) };
|
|
1566
|
+
runtime.releasingThreads.set(threadId, transition);
|
|
1567
|
+
try {
|
|
1568
|
+
await transition.completion;
|
|
1569
|
+
return true;
|
|
1570
|
+
} catch (error) {
|
|
1571
|
+
if (!runtime.closed && !transition.invalidated && runtime.releasingThreads.get(threadId) === transition && !runtime.retainedThreads.has(threadId) && !runtime.claimedThreads.has(threadId)) {
|
|
1572
|
+
retained.expiresAt = Date.now() + CODEX_APP_SERVER_LIVE_THREAD_IDLE_TIMEOUT_MS;
|
|
1573
|
+
const newerThreads = [...runtime.retainedThreads.entries()].filter(([candidateThreadId]) => !olderThreadIds.has(candidateThreadId));
|
|
1574
|
+
for (const [candidateThreadId] of newerThreads) runtime.retainedThreads.delete(candidateThreadId);
|
|
1575
|
+
runtime.retainedThreads.set(threadId, retained);
|
|
1576
|
+
for (const [candidateThreadId, newerThread] of newerThreads) runtime.retainedThreads.set(candidateThreadId, newerThread);
|
|
1577
|
+
}
|
|
1578
|
+
throw error;
|
|
1579
|
+
} finally {
|
|
1580
|
+
if (runtime.releasingThreads.get(threadId) === transition) runtime.releasingThreads.delete(threadId);
|
|
1581
|
+
scheduleRetainedThreadEviction(client, runtime);
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
async function evictExpiredRetainedThreads(client, runtime) {
|
|
1585
|
+
const now = Date.now();
|
|
1586
|
+
for (const [threadId, thread] of runtime.retainedThreads) if (thread.expiresAt <= now && !runtime.protectedThreads.has(threadId)) await releaseRetainedThread(client, runtime, threadId);
|
|
1587
|
+
scheduleRetainedThreadEviction(client, runtime);
|
|
1588
|
+
}
|
|
1589
|
+
async function evictExcessIdleThreads(client, runtime) {
|
|
1590
|
+
let idleThreadIds = [...runtime.retainedThreads.keys()].filter((threadId) => !runtime.protectedThreads.has(threadId));
|
|
1591
|
+
while (idleThreadIds.length > CODEX_APP_SERVER_LIVE_THREAD_MAX_IDLE) {
|
|
1592
|
+
await releaseRetainedThread(client, runtime, idleThreadIds[0]);
|
|
1593
|
+
idleThreadIds = [...runtime.retainedThreads.keys()].filter((threadId) => !runtime.protectedThreads.has(threadId));
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
/** Retain separately owned Codex subscriptions; completing B must never cold-restart A. */
|
|
1597
|
+
async function retainCodexAppServerLiveThread(client, threadId, releaseThread, configFingerprint, serviceTier) {
|
|
1155
1598
|
const runtime = configuredClients.get(client);
|
|
1156
|
-
if (!runtime) return;
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1599
|
+
if (!runtime || runtime.closed) return false;
|
|
1600
|
+
const claimed = runtime.claimedThreads.get(threadId);
|
|
1601
|
+
if (claimed !== void 0 && (releaseThread === void 0 || claimedThreadReleaseTokens.get(releaseThread) !== claimed)) return false;
|
|
1602
|
+
const pendingRelease = runtime.releasingThreads.get(threadId);
|
|
1603
|
+
if (pendingRelease) {
|
|
1604
|
+
await pendingRelease.completion;
|
|
1605
|
+
if (runtime.closed || claimed !== void 0 && runtime.claimedThreads.get(threadId) !== claimed) return false;
|
|
1606
|
+
}
|
|
1607
|
+
runtime.retainedThreads.delete(threadId);
|
|
1608
|
+
const retained = {
|
|
1609
|
+
configFingerprint,
|
|
1610
|
+
serviceTier,
|
|
1611
|
+
expiresAt: Date.now() + CODEX_APP_SERVER_LIVE_THREAD_IDLE_TIMEOUT_MS,
|
|
1612
|
+
release: (releaseThread ? physicalThreadReleases.get(releaseThread) ?? releaseThread : void 0) ?? (async (releasedThreadId) => {
|
|
1613
|
+
await unsubscribeCodexAppServerLiveThread(client, releasedThreadId, 5e3);
|
|
1614
|
+
})
|
|
1615
|
+
};
|
|
1616
|
+
runtime.retainedThreads.set(threadId, retained);
|
|
1617
|
+
try {
|
|
1618
|
+
await evictExcessIdleThreads(client, runtime);
|
|
1619
|
+
} catch (error) {
|
|
1620
|
+
if (runtime.retainedThreads.get(threadId) === retained) runtime.retainedThreads.delete(threadId);
|
|
1621
|
+
scheduleRetainedThreadEviction(client, runtime);
|
|
1622
|
+
embeddedAgentLog.warn("codex retained thread capacity eviction failed", {
|
|
1623
|
+
threadId,
|
|
1624
|
+
reason: formatErrorMessage(error)
|
|
1625
|
+
});
|
|
1626
|
+
return false;
|
|
1627
|
+
}
|
|
1628
|
+
if (runtime.closed) return false;
|
|
1629
|
+
if (claimed !== void 0 && runtime.claimedThreads.get(threadId) === claimed) runtime.claimedThreads.delete(threadId);
|
|
1630
|
+
scheduleRetainedThreadEviction(client, runtime);
|
|
1631
|
+
return true;
|
|
1632
|
+
}
|
|
1633
|
+
/** Transfer one idle subscription to its next turn or compaction without touching sibling threads. */
|
|
1634
|
+
async function consumeCodexAppServerLiveThread(client, threadId, configFingerprint) {
|
|
1635
|
+
const runtime = configuredClients.get(client);
|
|
1636
|
+
if (!runtime || runtime.closed) return;
|
|
1637
|
+
const pendingRelease = runtime.releasingThreads.get(threadId);
|
|
1638
|
+
if (pendingRelease) {
|
|
1639
|
+
await pendingRelease.completion;
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
const retained = runtime.retainedThreads.get(threadId);
|
|
1643
|
+
if (!retained || configFingerprint !== void 0 && retained.configFingerprint !== configFingerprint) return;
|
|
1644
|
+
return claimCodexAppServerThreadOwnership(client, runtime, threadId, retained);
|
|
1645
|
+
}
|
|
1646
|
+
/** Claims an observed Codex auto-subscription without exposing a temporarily idle owner. */
|
|
1647
|
+
async function claimCodexAppServerLiveThread(client, threadId) {
|
|
1648
|
+
const runtime = configuredClients.get(client);
|
|
1649
|
+
if (!runtime || runtime.closed || runtime.claimedThreads.has(threadId)) return;
|
|
1650
|
+
const pendingRelease = runtime.releasingThreads.get(threadId);
|
|
1651
|
+
if (pendingRelease) {
|
|
1652
|
+
await pendingRelease.completion;
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
return claimCodexAppServerThreadOwnership(client, runtime, threadId, runtime.retainedThreads.get(threadId) ?? {
|
|
1656
|
+
expiresAt: Date.now() + CODEX_APP_SERVER_LIVE_THREAD_IDLE_TIMEOUT_MS,
|
|
1657
|
+
release: async (releasedThreadId) => {
|
|
1658
|
+
await unsubscribeCodexAppServerLiveThread(client, releasedThreadId, 5e3);
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
function claimCodexAppServerThreadOwnership(client, runtime, threadId, retained) {
|
|
1663
|
+
runtime.retainedThreads.delete(threadId);
|
|
1664
|
+
const claimed = Symbol(threadId);
|
|
1665
|
+
runtime.claimedThreads.set(threadId, claimed);
|
|
1666
|
+
scheduleRetainedThreadEviction(client, runtime);
|
|
1667
|
+
const release = async (releasedThreadId) => {
|
|
1668
|
+
if (releasedThreadId !== threadId || runtime.claimedThreads.get(threadId) !== claimed) return;
|
|
1669
|
+
const pendingRelease = runtime.releasingThreads.get(threadId);
|
|
1670
|
+
if (pendingRelease) {
|
|
1671
|
+
await pendingRelease.completion;
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
const transition = { completion: Promise.resolve().then(async () => {
|
|
1675
|
+
if (runtime.closed || runtime.claimedThreads.get(threadId) !== claimed) return;
|
|
1676
|
+
await retained.release(releasedThreadId);
|
|
1677
|
+
}) };
|
|
1678
|
+
runtime.releasingThreads.set(threadId, transition);
|
|
1162
1679
|
try {
|
|
1163
|
-
await
|
|
1164
|
-
|
|
1165
|
-
runtime.retainedThreadId = void 0;
|
|
1166
|
-
runtime.retainedThreadConfigFingerprint = void 0;
|
|
1167
|
-
throw error;
|
|
1680
|
+
await transition.completion;
|
|
1681
|
+
if (runtime.claimedThreads.get(threadId) === claimed) runtime.claimedThreads.delete(threadId);
|
|
1168
1682
|
} finally {
|
|
1169
|
-
if (runtime.
|
|
1683
|
+
if (runtime.releasingThreads.get(threadId) === transition) runtime.releasingThreads.delete(threadId);
|
|
1170
1684
|
}
|
|
1685
|
+
};
|
|
1686
|
+
physicalThreadReleases.set(release, retained.release);
|
|
1687
|
+
claimedThreadReleaseTokens.set(release, claimed);
|
|
1688
|
+
return {
|
|
1689
|
+
configFingerprint: retained.configFingerprint,
|
|
1690
|
+
serviceTier: retained.serviceTier,
|
|
1691
|
+
release
|
|
1692
|
+
};
|
|
1693
|
+
}
|
|
1694
|
+
/** Distinguish active claimed ownership from an already-evicted idle subscription. */
|
|
1695
|
+
function isCodexAppServerLiveThreadClaimed(client, threadId) {
|
|
1696
|
+
const runtime = configuredClients.get(client);
|
|
1697
|
+
return runtime !== void 0 && !runtime.closed && runtime.claimedThreads.has(threadId);
|
|
1698
|
+
}
|
|
1699
|
+
/** Release the exact physical subscription and finish only its observed claim generation. */
|
|
1700
|
+
async function unsubscribeCodexAppServerLiveThread(client, threadId, timeoutMs) {
|
|
1701
|
+
const runtime = configuredClients.get(client);
|
|
1702
|
+
const claimed = runtime?.claimedThreads.get(threadId);
|
|
1703
|
+
let transition = runtime?.releasingThreads.get(threadId);
|
|
1704
|
+
if (transition?.physicalRelease) {
|
|
1705
|
+
await transition.physicalRelease;
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
const physicalRelease = Promise.resolve().then(async () => {
|
|
1709
|
+
if (claimed !== void 0 && runtime?.claimedThreads.get(threadId) !== claimed) return;
|
|
1710
|
+
await client.request("thread/unsubscribe", { threadId }, { timeoutMs });
|
|
1711
|
+
});
|
|
1712
|
+
const ownsTransition = runtime !== void 0 && transition === void 0;
|
|
1713
|
+
if (transition) transition.physicalRelease = physicalRelease;
|
|
1714
|
+
else if (runtime) {
|
|
1715
|
+
transition = {
|
|
1716
|
+
completion: physicalRelease,
|
|
1717
|
+
physicalRelease
|
|
1718
|
+
};
|
|
1719
|
+
runtime.releasingThreads.set(threadId, transition);
|
|
1720
|
+
}
|
|
1721
|
+
try {
|
|
1722
|
+
await physicalRelease;
|
|
1723
|
+
if (claimed !== void 0 && runtime?.claimedThreads.get(threadId) === claimed) runtime.claimedThreads.delete(threadId);
|
|
1724
|
+
} finally {
|
|
1725
|
+
if (ownsTransition && runtime.releasingThreads.get(threadId) === transition) runtime.releasingThreads.delete(threadId);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
/** Reset/end owns the exact thread; failed generation retirement must never release its successor. */
|
|
1729
|
+
async function releaseCodexAppServerLiveThread(client, threadId) {
|
|
1730
|
+
const runtime = configuredClients.get(client);
|
|
1731
|
+
return runtime ? await releaseRetainedThread(client, runtime, threadId) : false;
|
|
1732
|
+
}
|
|
1733
|
+
/** Native child work pins its parent's subscription even after the foreground parent turn ends. */
|
|
1734
|
+
function protectCodexAppServerLiveThread(client, threadId) {
|
|
1735
|
+
const runtime = configuredClients.get(client);
|
|
1736
|
+
if (!runtime || runtime.closed) return () => void 0;
|
|
1737
|
+
runtime.protectedThreads.set(threadId, (runtime.protectedThreads.get(threadId) ?? 0) + 1);
|
|
1738
|
+
scheduleRetainedThreadEviction(client, runtime);
|
|
1739
|
+
let protectedThread = true;
|
|
1740
|
+
return () => {
|
|
1741
|
+
if (!protectedThread) return;
|
|
1742
|
+
protectedThread = false;
|
|
1743
|
+
if (runtime.closed) return;
|
|
1744
|
+
const count = runtime.protectedThreads.get(threadId) ?? 0;
|
|
1745
|
+
if (count <= 1) {
|
|
1746
|
+
runtime.protectedThreads.delete(threadId);
|
|
1747
|
+
const retained = runtime.retainedThreads.get(threadId);
|
|
1748
|
+
if (retained) {
|
|
1749
|
+
runtime.retainedThreads.delete(threadId);
|
|
1750
|
+
runtime.retainedThreads.set(threadId, {
|
|
1751
|
+
...retained,
|
|
1752
|
+
expiresAt: Date.now() + CODEX_APP_SERVER_LIVE_THREAD_IDLE_TIMEOUT_MS
|
|
1753
|
+
});
|
|
1754
|
+
}
|
|
1755
|
+
} else runtime.protectedThreads.set(threadId, count - 1);
|
|
1756
|
+
scheduleRetainedThreadEviction(client, runtime);
|
|
1757
|
+
evictExcessIdleThreads(client, runtime).catch((error) => {
|
|
1758
|
+
embeddedAgentLog.warn("codex retained thread unpin eviction failed", {
|
|
1759
|
+
threadId,
|
|
1760
|
+
reason: formatErrorMessage(error)
|
|
1761
|
+
});
|
|
1762
|
+
});
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
//#endregion
|
|
1766
|
+
//#region extensions/codex/src/app-server/elicitation-response.ts
|
|
1767
|
+
function createCodexElicitationResponse(action, content = null, meta = null) {
|
|
1768
|
+
return {
|
|
1769
|
+
action,
|
|
1770
|
+
content,
|
|
1771
|
+
_meta: meta
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
//#endregion
|
|
1775
|
+
//#region extensions/codex/src/app-server/rpc-error.ts
|
|
1776
|
+
/** RPC error wrapper that preserves app-server error code and data. */
|
|
1777
|
+
var CodexAppServerRpcError = class extends Error {
|
|
1778
|
+
constructor(error, method) {
|
|
1779
|
+
super(formatCodexAppServerRpcErrorMessage(error, method));
|
|
1780
|
+
this.name = "CodexAppServerRpcError";
|
|
1781
|
+
this.code = error.code;
|
|
1782
|
+
this.data = error.data;
|
|
1783
|
+
this.method = method;
|
|
1171
1784
|
}
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1785
|
+
};
|
|
1786
|
+
function formatCodexAppServerRpcErrorMessage(error, method) {
|
|
1787
|
+
const message = error.message || `${method} failed`;
|
|
1788
|
+
const detail = readCodexAppServerRpcReloginDetail(error.data);
|
|
1789
|
+
return detail && !message.includes(detail) ? `${message}: ${detail}` : message;
|
|
1175
1790
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
const
|
|
1179
|
-
if (!
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
runtime.retainedThreadConfigFingerprint = void 0;
|
|
1184
|
-
return true;
|
|
1791
|
+
function readCodexAppServerRpcReloginDetail(data) {
|
|
1792
|
+
const record = isJsonObject(data) ? data : void 0;
|
|
1793
|
+
const nested = isJsonObject(record?.error) ? record.error : record;
|
|
1794
|
+
if (!nested) return;
|
|
1795
|
+
const isRelogin = nested.action === "relogin" || nested.reason === "cloudRequirements" && nested.errorCode === "Auth";
|
|
1796
|
+
const detail = typeof nested.detail === "string" ? nested.detail.trim() : "";
|
|
1797
|
+
return isRelogin && detail ? detail : void 0;
|
|
1185
1798
|
}
|
|
1186
1799
|
//#endregion
|
|
1187
1800
|
//#region extensions/codex/src/app-server/transport-websocket.ts
|
|
@@ -1189,6 +1802,10 @@ async function consumeCodexAppServerLiveThread(client, threadId, configFingerpri
|
|
|
1189
1802
|
* Adapts a remote Codex app-server WebSocket endpoint to the shared stdio-like
|
|
1190
1803
|
* transport interface.
|
|
1191
1804
|
*/
|
|
1805
|
+
const WEBSOCKET_HANDSHAKE_TIMEOUT_MS = 1e4;
|
|
1806
|
+
const WEBSOCKET_PING_INTERVAL_MS = 2e4;
|
|
1807
|
+
const WEBSOCKET_PONG_TIMEOUT_MS = 2e4;
|
|
1808
|
+
const MAX_CONSECUTIVE_MISSED_WEBSOCKET_PONGS = 5;
|
|
1192
1809
|
/** Opens a WebSocket app-server transport and maps newline-delimited frames to stdout/stdin. */
|
|
1193
1810
|
function createWebSocketTransport(options) {
|
|
1194
1811
|
if (!options.url) throw new Error("codex app-server websocket transport requires plugins.entries.codex.config.appServer.url");
|
|
@@ -1200,7 +1817,8 @@ function createWebSocketTransport(options) {
|
|
|
1200
1817
|
...options.headers,
|
|
1201
1818
|
...options.authToken ? { Authorization: `Bearer ${options.authToken}` } : {}
|
|
1202
1819
|
},
|
|
1203
|
-
perMessageDeflate: false
|
|
1820
|
+
perMessageDeflate: false,
|
|
1821
|
+
...options.transport === "websocket" ? { handshakeTimeout: WEBSOCKET_HANDSHAKE_TIMEOUT_MS } : {}
|
|
1204
1822
|
};
|
|
1205
1823
|
const unixSocketPath = resolveCodexAppServerUnixSocketPath(options);
|
|
1206
1824
|
const socket = unixSocketPath ? new WebSocket("ws://localhost/", {
|
|
@@ -1211,6 +1829,58 @@ function createWebSocketTransport(options) {
|
|
|
1211
1829
|
const stdinDecoder = new StringDecoder("utf8");
|
|
1212
1830
|
let pendingLine = "";
|
|
1213
1831
|
let killed = false;
|
|
1832
|
+
let pingTimeout;
|
|
1833
|
+
let pongTimeout;
|
|
1834
|
+
let expectedPong;
|
|
1835
|
+
let consecutiveMissedPongs = 0;
|
|
1836
|
+
let heartbeatSequence = 0;
|
|
1837
|
+
const clearConnectionHealthTimers = () => {
|
|
1838
|
+
if (pingTimeout) {
|
|
1839
|
+
clearTimeout(pingTimeout);
|
|
1840
|
+
pingTimeout = void 0;
|
|
1841
|
+
}
|
|
1842
|
+
if (pongTimeout) {
|
|
1843
|
+
clearTimeout(pongTimeout);
|
|
1844
|
+
pongTimeout = void 0;
|
|
1845
|
+
}
|
|
1846
|
+
expectedPong = void 0;
|
|
1847
|
+
};
|
|
1848
|
+
const sendHeartbeatPing = () => {
|
|
1849
|
+
if (socket.readyState !== WebSocket.OPEN || pongTimeout) return;
|
|
1850
|
+
const payload = Buffer.from(`openclaw-codex-${++heartbeatSequence}`);
|
|
1851
|
+
expectedPong = payload;
|
|
1852
|
+
pongTimeout = setTimeout(() => {
|
|
1853
|
+
pongTimeout = void 0;
|
|
1854
|
+
expectedPong = void 0;
|
|
1855
|
+
consecutiveMissedPongs += 1;
|
|
1856
|
+
if (consecutiveMissedPongs >= MAX_CONSECUTIVE_MISSED_WEBSOCKET_PONGS) {
|
|
1857
|
+
socket.terminate();
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
sendHeartbeatPing();
|
|
1861
|
+
}, WEBSOCKET_PONG_TIMEOUT_MS);
|
|
1862
|
+
pongTimeout.unref();
|
|
1863
|
+
socket.ping(payload, void 0, (error) => {
|
|
1864
|
+
if (error) socket.terminate();
|
|
1865
|
+
});
|
|
1866
|
+
};
|
|
1867
|
+
const scheduleHeartbeatPing = () => {
|
|
1868
|
+
if (options.transport !== "websocket" || socket.readyState !== WebSocket.OPEN || pingTimeout || pongTimeout) return;
|
|
1869
|
+
pingTimeout = setTimeout(() => {
|
|
1870
|
+
pingTimeout = void 0;
|
|
1871
|
+
sendHeartbeatPing();
|
|
1872
|
+
}, WEBSOCKET_PING_INTERVAL_MS);
|
|
1873
|
+
pingTimeout.unref();
|
|
1874
|
+
};
|
|
1875
|
+
const recordConnectionActivity = () => {
|
|
1876
|
+
consecutiveMissedPongs = 0;
|
|
1877
|
+
if (pongTimeout) {
|
|
1878
|
+
clearTimeout(pongTimeout);
|
|
1879
|
+
pongTimeout = void 0;
|
|
1880
|
+
}
|
|
1881
|
+
expectedPong = void 0;
|
|
1882
|
+
scheduleHeartbeatPing();
|
|
1883
|
+
};
|
|
1214
1884
|
const sendFrame = (frame) => {
|
|
1215
1885
|
const trimmed = frame.trim();
|
|
1216
1886
|
if (!trimmed) return;
|
|
@@ -1222,13 +1892,22 @@ function createWebSocketTransport(options) {
|
|
|
1222
1892
|
};
|
|
1223
1893
|
socket.once("open", () => {
|
|
1224
1894
|
for (const frame of pendingFrames.splice(0)) socket.send(frame);
|
|
1895
|
+
scheduleHeartbeatPing();
|
|
1896
|
+
});
|
|
1897
|
+
socket.on("pong", (payload) => {
|
|
1898
|
+
if (expectedPong?.equals(payload)) recordConnectionActivity();
|
|
1899
|
+
});
|
|
1900
|
+
socket.once("error", (error) => {
|
|
1901
|
+
clearConnectionHealthTimers();
|
|
1902
|
+
events.emit("error", error);
|
|
1225
1903
|
});
|
|
1226
|
-
socket.once("error", (error) => events.emit("error", error));
|
|
1227
1904
|
socket.once("close", (code, reason) => {
|
|
1905
|
+
clearConnectionHealthTimers();
|
|
1228
1906
|
killed = true;
|
|
1229
1907
|
events.emit("exit", code, reason.toString("utf8"));
|
|
1230
1908
|
});
|
|
1231
1909
|
socket.on("message", (data) => {
|
|
1910
|
+
if (options.transport === "websocket") recordConnectionActivity();
|
|
1232
1911
|
const text = websocketFrameToText(data);
|
|
1233
1912
|
stdout.write(text.endsWith("\n") ? text : `${text}\n`);
|
|
1234
1913
|
});
|
|
@@ -1262,6 +1941,7 @@ function createWebSocketTransport(options) {
|
|
|
1262
1941
|
},
|
|
1263
1942
|
kill: () => {
|
|
1264
1943
|
killed = true;
|
|
1944
|
+
clearConnectionHealthTimers();
|
|
1265
1945
|
socket.close();
|
|
1266
1946
|
},
|
|
1267
1947
|
once: (event, listener) => events.once(event, listener)
|
|
@@ -1288,11 +1968,273 @@ function websocketFrameToText(data) {
|
|
|
1288
1968
|
return Buffer.from(data).toString("utf8");
|
|
1289
1969
|
}
|
|
1290
1970
|
//#endregion
|
|
1971
|
+
//#region extensions/codex/src/app-server/transport-process-containment.ts
|
|
1972
|
+
const PROCESS_COLUMNS = "pid=,ppid=,pgid=,stat=,lstart=";
|
|
1973
|
+
const MAX_CONTAINED_PROCESSES = 512;
|
|
1974
|
+
const MAX_PROCESS_CONTAINMENT_MS = 2e3;
|
|
1975
|
+
const MAX_PROCESS_QUIESCE_PASSES = 16;
|
|
1976
|
+
const PROCESS_INSPECTION_MAX_BYTES = 8 * 1024 * 1024;
|
|
1977
|
+
async function terminateCodexAppServerDescendants(child) {
|
|
1978
|
+
const rootPid = child.pid;
|
|
1979
|
+
if (process.platform === "win32" || !rootPid || !child.kill || hasExited(child)) return;
|
|
1980
|
+
const deadline = Date.now() + MAX_PROCESS_CONTAINMENT_MS;
|
|
1981
|
+
const snapshot = await readProcessSnapshot(deadline);
|
|
1982
|
+
if (!snapshot || Date.now() >= deadline) return;
|
|
1983
|
+
const root = snapshot.find((row) => row.pid === rootPid);
|
|
1984
|
+
if (!root || !isSameLiveRoot(root, root)) return;
|
|
1985
|
+
const initialDescendants = collectDescendants(snapshot, [rootPid]);
|
|
1986
|
+
if (initialDescendants.length > MAX_CONTAINED_PROCESSES) return;
|
|
1987
|
+
const stoppedDescendants = /* @__PURE__ */ new Map();
|
|
1988
|
+
if (!await signalSameRoot(root, "SIGSTOP", deadline)) return;
|
|
1989
|
+
let resumeRootOnUnwind = true;
|
|
1990
|
+
try {
|
|
1991
|
+
const descendants = await quiesceDescendants(root, initialDescendants, stoppedDescendants, deadline);
|
|
1992
|
+
if (!descendants) return;
|
|
1993
|
+
for (const descendant of descendants.toReversed()) {
|
|
1994
|
+
if (Date.now() >= deadline) return;
|
|
1995
|
+
if (!descendant.state.startsWith("Z")) {
|
|
1996
|
+
if (!await signalSameProcess(descendant, "SIGKILL", deadline) || Date.now() >= deadline) return;
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
resumeRootOnUnwind = false;
|
|
2000
|
+
let resumed = false;
|
|
2001
|
+
return () => {
|
|
2002
|
+
if (resumed) return;
|
|
2003
|
+
resumed = true;
|
|
2004
|
+
resumeTransportRoot(child, root, false);
|
|
2005
|
+
};
|
|
2006
|
+
} finally {
|
|
2007
|
+
if (resumeRootOnUnwind) {
|
|
2008
|
+
for (const descendant of stoppedDescendants.values()) signalProcess(descendant.pid, "SIGCONT");
|
|
2009
|
+
resumeTransportRoot(child, root, true);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
async function quiesceDescendants(root, initialDescendants, stopped, deadline) {
|
|
2014
|
+
const provenByPid = new Map(initialDescendants.map((descendant) => [descendant.pid, descendant]));
|
|
2015
|
+
const stopFailures = /* @__PURE__ */ new Map();
|
|
2016
|
+
for (let pass = 0; pass < MAX_PROCESS_QUIESCE_PASSES; pass += 1) {
|
|
2017
|
+
if (Date.now() >= deadline) return;
|
|
2018
|
+
const snapshot = await readProcessSnapshot(deadline);
|
|
2019
|
+
if (!snapshot || Date.now() >= deadline) return;
|
|
2020
|
+
const currentRoot = snapshot.find((row) => row.pid === root.pid);
|
|
2021
|
+
if (!currentRoot || !isSameLiveRoot(currentRoot, root)) return;
|
|
2022
|
+
if (!isSameLiveRoot(currentRoot, root, true)) {
|
|
2023
|
+
if (!await signalSameRoot(root, "SIGSTOP", deadline) || Date.now() >= deadline) return;
|
|
2024
|
+
continue;
|
|
2025
|
+
}
|
|
2026
|
+
const snapshotByPid = new Map(snapshot.map((process) => [process.pid, process]));
|
|
2027
|
+
const liveProven = [];
|
|
2028
|
+
for (const proven of provenByPid.values()) {
|
|
2029
|
+
const current = snapshotByPid.get(proven.pid);
|
|
2030
|
+
if (!current) {
|
|
2031
|
+
provenByPid.delete(proven.pid);
|
|
2032
|
+
stopped.delete(identityKey(proven));
|
|
2033
|
+
continue;
|
|
2034
|
+
}
|
|
2035
|
+
if (!hasSameIdentity(proven, current)) return;
|
|
2036
|
+
provenByPid.set(current.pid, current);
|
|
2037
|
+
const key = identityKey(current);
|
|
2038
|
+
if (stopped.has(key)) stopped.set(key, current);
|
|
2039
|
+
liveProven.push(current);
|
|
2040
|
+
}
|
|
2041
|
+
const descendants = collectDescendants(snapshot, [root.pid, ...liveProven.map(({ pid }) => pid)]);
|
|
2042
|
+
for (const descendant of descendants) {
|
|
2043
|
+
const proven = provenByPid.get(descendant.pid);
|
|
2044
|
+
if (proven && !hasSameIdentity(proven, descendant)) return;
|
|
2045
|
+
provenByPid.set(descendant.pid, descendant);
|
|
2046
|
+
}
|
|
2047
|
+
if (provenByPid.size > MAX_CONTAINED_PROCESSES) return;
|
|
2048
|
+
const quiescenceTargets = new Map(liveProven.map((process) => [process.pid, process]));
|
|
2049
|
+
for (const descendant of descendants) quiescenceTargets.set(descendant.pid, descendant);
|
|
2050
|
+
let allStopped = true;
|
|
2051
|
+
for (const descendant of quiescenceTargets.values()) {
|
|
2052
|
+
if (Date.now() >= deadline) return;
|
|
2053
|
+
if (isStoppedState(descendant.state)) continue;
|
|
2054
|
+
const stopQueued = await signalSameProcess(descendant, "SIGSTOP", deadline);
|
|
2055
|
+
if (Date.now() >= deadline) return;
|
|
2056
|
+
if (stopQueued) {
|
|
2057
|
+
stopFailures.delete(identityKey(descendant));
|
|
2058
|
+
stopped.set(identityKey(descendant), descendant);
|
|
2059
|
+
} else {
|
|
2060
|
+
const key = identityKey(descendant);
|
|
2061
|
+
const failures = (stopFailures.get(key) ?? 0) + 1;
|
|
2062
|
+
if (failures >= 2) return;
|
|
2063
|
+
stopFailures.set(key, failures);
|
|
2064
|
+
}
|
|
2065
|
+
if (!isUninterruptibleState(descendant.state) || !stopQueued) allStopped = false;
|
|
2066
|
+
}
|
|
2067
|
+
if (allStopped) return [...provenByPid.values()];
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
async function readProcessSnapshot(deadline) {
|
|
2071
|
+
return await readProcesses(["-axo", PROCESS_COLUMNS], deadline);
|
|
2072
|
+
}
|
|
2073
|
+
async function readProcess(pid, deadline) {
|
|
2074
|
+
return (await readProcesses([
|
|
2075
|
+
"-o",
|
|
2076
|
+
PROCESS_COLUMNS,
|
|
2077
|
+
"-p",
|
|
2078
|
+
String(pid)
|
|
2079
|
+
], deadline))?.find((row) => row.pid === pid);
|
|
2080
|
+
}
|
|
2081
|
+
async function readProcesses(args, deadline) {
|
|
2082
|
+
const remainingMs = deadline - Date.now();
|
|
2083
|
+
if (remainingMs <= 0) return;
|
|
2084
|
+
return await new Promise((resolve) => {
|
|
2085
|
+
let settled = false;
|
|
2086
|
+
const settle = (processes) => {
|
|
2087
|
+
if (settled) return;
|
|
2088
|
+
settled = true;
|
|
2089
|
+
clearTimeout(timer);
|
|
2090
|
+
resolve(processes);
|
|
2091
|
+
};
|
|
2092
|
+
const inspector = execFile("ps", args, {
|
|
2093
|
+
encoding: "utf8",
|
|
2094
|
+
maxBuffer: PROCESS_INSPECTION_MAX_BYTES
|
|
2095
|
+
}, (error, stdout) => {
|
|
2096
|
+
settle(error ? void 0 : parseProcesses(stdout));
|
|
2097
|
+
});
|
|
2098
|
+
const timer = setTimeout(() => {
|
|
2099
|
+
settle(void 0);
|
|
2100
|
+
inspector.stdout?.destroy();
|
|
2101
|
+
inspector.stderr?.destroy();
|
|
2102
|
+
inspector.kill("SIGKILL");
|
|
2103
|
+
inspector.unref();
|
|
2104
|
+
}, Math.max(1, remainingMs));
|
|
2105
|
+
timer.unref?.();
|
|
2106
|
+
});
|
|
2107
|
+
}
|
|
2108
|
+
function parseProcesses(output) {
|
|
2109
|
+
const rows = [];
|
|
2110
|
+
for (const line of output.split("\n")) {
|
|
2111
|
+
const match = /^\s*(\d+)\s+(\d+)\s+(\d+)\s+(\S+)\s+(.+?)\s*$/.exec(line);
|
|
2112
|
+
if (!match) continue;
|
|
2113
|
+
const pid = Number(match[1] ?? "");
|
|
2114
|
+
const ppid = Number(match[2] ?? "");
|
|
2115
|
+
const pgid = Number(match[3] ?? "");
|
|
2116
|
+
const startedAt = (match[5] ?? "").trim().replace(/\s+/g, " ");
|
|
2117
|
+
if (![
|
|
2118
|
+
pid,
|
|
2119
|
+
ppid,
|
|
2120
|
+
pgid
|
|
2121
|
+
].every(Number.isSafeInteger) || pid <= 0 || ppid < 0 || pgid <= 0 || !startedAt) continue;
|
|
2122
|
+
rows.push({
|
|
2123
|
+
pid,
|
|
2124
|
+
ppid,
|
|
2125
|
+
pgid,
|
|
2126
|
+
state: match[4] ?? "",
|
|
2127
|
+
startedAt
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
return rows;
|
|
2131
|
+
}
|
|
2132
|
+
function collectDescendants(snapshot, rootPids) {
|
|
2133
|
+
const childrenByParent = /* @__PURE__ */ new Map();
|
|
2134
|
+
for (const row of snapshot) {
|
|
2135
|
+
const children = childrenByParent.get(row.ppid) ?? [];
|
|
2136
|
+
children.push(row);
|
|
2137
|
+
childrenByParent.set(row.ppid, children);
|
|
2138
|
+
}
|
|
2139
|
+
const descendants = [];
|
|
2140
|
+
const pending = [...new Set(rootPids)];
|
|
2141
|
+
const seen = new Set(pending);
|
|
2142
|
+
for (const parentPid of pending) for (const child of childrenByParent.get(parentPid) ?? []) {
|
|
2143
|
+
if (seen.has(child.pid)) continue;
|
|
2144
|
+
seen.add(child.pid);
|
|
2145
|
+
descendants.push(child);
|
|
2146
|
+
pending.push(child.pid);
|
|
2147
|
+
}
|
|
2148
|
+
return descendants;
|
|
2149
|
+
}
|
|
2150
|
+
function isStoppedState(state) {
|
|
2151
|
+
return state.startsWith("T") || state.startsWith("t") || state.startsWith("Z");
|
|
2152
|
+
}
|
|
2153
|
+
function isQuiescedState(state) {
|
|
2154
|
+
return isStoppedState(state) || isUninterruptibleState(state);
|
|
2155
|
+
}
|
|
2156
|
+
function isUninterruptibleState(state) {
|
|
2157
|
+
return state.startsWith("D") || state.startsWith("U");
|
|
2158
|
+
}
|
|
2159
|
+
function isSameLiveProcess(current, expected) {
|
|
2160
|
+
return current.pgid === expected.pgid && !current.state.startsWith("Z") && hasSameIdentity(current, expected);
|
|
2161
|
+
}
|
|
2162
|
+
function isSameLiveRoot(current, expected, requireStopped = false) {
|
|
2163
|
+
return current.ppid === process.pid && (!requireStopped || isQuiescedState(current.state)) && isSameLiveProcess(current, expected);
|
|
2164
|
+
}
|
|
2165
|
+
async function signalSameRoot(root, signal, deadline) {
|
|
2166
|
+
const current = await readProcess(root.pid, deadline);
|
|
2167
|
+
return Boolean(current && isSameLiveRoot(current, root) && signalProcess(current.pid, signal));
|
|
2168
|
+
}
|
|
2169
|
+
function resumeTransportRoot(child, root, allowSynchronousPidFallback) {
|
|
2170
|
+
try {
|
|
2171
|
+
if (child.kill) {
|
|
2172
|
+
child.kill("SIGCONT");
|
|
2173
|
+
return;
|
|
2174
|
+
}
|
|
2175
|
+
} catch {
|
|
2176
|
+
if (!allowSynchronousPidFallback) return;
|
|
2177
|
+
}
|
|
2178
|
+
if (allowSynchronousPidFallback) signalProcess(root.pid, "SIGCONT");
|
|
2179
|
+
}
|
|
2180
|
+
async function signalSameProcess(expected, signal, deadline) {
|
|
2181
|
+
const current = await readProcess(expected.pid, deadline);
|
|
2182
|
+
return Boolean(current && isSameLiveProcess(current, expected) && signalProcess(current.pid, signal));
|
|
2183
|
+
}
|
|
2184
|
+
function hasSameIdentity(left, right) {
|
|
2185
|
+
return identityKey(left) === identityKey(right);
|
|
2186
|
+
}
|
|
2187
|
+
function identityKey(row) {
|
|
2188
|
+
return `${row.pid}\0${row.startedAt}`;
|
|
2189
|
+
}
|
|
2190
|
+
function hasExited(child) {
|
|
2191
|
+
return child.exitCode != null || child.signalCode != null;
|
|
2192
|
+
}
|
|
2193
|
+
function signalProcess(pid, signal) {
|
|
2194
|
+
try {
|
|
2195
|
+
process.kill(pid, signal);
|
|
2196
|
+
return true;
|
|
2197
|
+
} catch {
|
|
2198
|
+
return false;
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
//#endregion
|
|
1291
2202
|
//#region extensions/codex/src/app-server/transport.ts
|
|
2203
|
+
/**
|
|
2204
|
+
* Shared transport lifecycle helpers for stdio and WebSocket Codex app-server
|
|
2205
|
+
* connections.
|
|
2206
|
+
*/
|
|
2207
|
+
const CODEX_APP_SERVER_TRANSPORT_CLOSES = /* @__PURE__ */ new WeakMap();
|
|
1292
2208
|
/** Starts graceful transport shutdown and schedules a force kill fallback. */
|
|
1293
2209
|
function closeCodexAppServerTransport(child, options = {}) {
|
|
1294
|
-
child
|
|
1295
|
-
|
|
2210
|
+
beginCodexAppServerTransportClose(child, options);
|
|
2211
|
+
}
|
|
2212
|
+
function beginCodexAppServerTransportClose(child, options) {
|
|
2213
|
+
const current = CODEX_APP_SERVER_TRANSPORT_CLOSES.get(child);
|
|
2214
|
+
if (current) return current;
|
|
2215
|
+
if (process.platform === "win32" || !child.pid || !child.kill || hasCodexAppServerTransportExited(child)) {
|
|
2216
|
+
finishCodexAppServerTransportClose(child, options);
|
|
2217
|
+
const completed = Promise.resolve();
|
|
2218
|
+
CODEX_APP_SERVER_TRANSPORT_CLOSES.set(child, completed);
|
|
2219
|
+
return completed;
|
|
2220
|
+
}
|
|
2221
|
+
const closing = (async () => {
|
|
2222
|
+
let resumeRoot;
|
|
2223
|
+
try {
|
|
2224
|
+
resumeRoot = await terminateCodexAppServerDescendants(child);
|
|
2225
|
+
} catch {
|
|
2226
|
+
resumeRoot = void 0;
|
|
2227
|
+
}
|
|
2228
|
+
try {
|
|
2229
|
+
finishCodexAppServerTransportClose(child, options, resumeRoot);
|
|
2230
|
+
} catch {
|
|
2231
|
+
signalCodexAppServerTransport(child, "SIGKILL");
|
|
2232
|
+
}
|
|
2233
|
+
})();
|
|
2234
|
+
CODEX_APP_SERVER_TRANSPORT_CLOSES.set(child, closing);
|
|
2235
|
+
return closing;
|
|
2236
|
+
}
|
|
2237
|
+
function finishCodexAppServerTransportClose(child, options, resumeRoot) {
|
|
1296
2238
|
const forceKillDelayMs = options.forceKillDelayMs ?? 1e3;
|
|
1297
2239
|
const forceKill = setTimeout(() => {
|
|
1298
2240
|
if (hasCodexAppServerTransportExited(child)) return;
|
|
@@ -1304,6 +2246,12 @@ function closeCodexAppServerTransport(child, options = {}) {
|
|
|
1304
2246
|
child.stdout.destroy?.();
|
|
1305
2247
|
child.stderr.destroy?.();
|
|
1306
2248
|
});
|
|
2249
|
+
try {
|
|
2250
|
+
child.stdin.end?.();
|
|
2251
|
+
child.stdin.destroy?.();
|
|
2252
|
+
} finally {
|
|
2253
|
+
resumeRoot?.();
|
|
2254
|
+
}
|
|
1307
2255
|
child.unref?.();
|
|
1308
2256
|
child.stdout.unref?.();
|
|
1309
2257
|
child.stderr.unref?.();
|
|
@@ -1311,7 +2259,7 @@ function closeCodexAppServerTransport(child, options = {}) {
|
|
|
1311
2259
|
}
|
|
1312
2260
|
/** Closes a transport and waits briefly for an exit event. */
|
|
1313
2261
|
async function closeCodexAppServerTransportAndWait(child, options = {}) {
|
|
1314
|
-
if (!hasCodexAppServerTransportExited(child))
|
|
2262
|
+
if (!hasCodexAppServerTransportExited(child)) await beginCodexAppServerTransportClose(child, options);
|
|
1315
2263
|
return await waitForCodexAppServerTransportExit(child, options.exitTimeoutMs ?? 2e3);
|
|
1316
2264
|
}
|
|
1317
2265
|
function hasCodexAppServerTransportExited(child) {
|
|
@@ -1344,15 +2292,6 @@ function signalCodexAppServerTransport(child, signal) {
|
|
|
1344
2292
|
child.kill?.(signal);
|
|
1345
2293
|
}
|
|
1346
2294
|
//#endregion
|
|
1347
|
-
//#region extensions/codex/src/app-server/version.ts
|
|
1348
|
-
/**
|
|
1349
|
-
* Version and package pins for the managed Codex app-server runtime.
|
|
1350
|
-
*/
|
|
1351
|
-
/** Exact Codex app-server version shipped and supported by the OpenClaw Codex bridge. */
|
|
1352
|
-
const CODEX_APP_SERVER_VERSION = "0.146.0";
|
|
1353
|
-
/** npm package name for the managed Codex app-server binary. */
|
|
1354
|
-
const MANAGED_CODEX_APP_SERVER_PACKAGE = "@openai/codex";
|
|
1355
|
-
//#endregion
|
|
1356
2295
|
//#region extensions/codex/src/app-server/client.ts
|
|
1357
2296
|
/**
|
|
1358
2297
|
* JSON-RPC client for Codex app-server transports, including request/response
|
|
@@ -1379,16 +2318,6 @@ function getCodexAppServerClientInstanceId(client) {
|
|
|
1379
2318
|
function resolveCodexAppServerClientInstanceId(client) {
|
|
1380
2319
|
return client.getInstanceId?.call(client) ?? getCodexAppServerClientInstanceId(client);
|
|
1381
2320
|
}
|
|
1382
|
-
/** RPC error wrapper that preserves app-server error code and data. */
|
|
1383
|
-
var CodexAppServerRpcError = class extends Error {
|
|
1384
|
-
constructor(error, method) {
|
|
1385
|
-
super(formatCodexAppServerRpcErrorMessage(error, method));
|
|
1386
|
-
this.name = "CodexAppServerRpcError";
|
|
1387
|
-
this.code = error.code;
|
|
1388
|
-
this.data = error.data;
|
|
1389
|
-
this.method = method;
|
|
1390
|
-
}
|
|
1391
|
-
};
|
|
1392
2321
|
var CodexAppServerLocalRequestCancellationError = class extends Error {
|
|
1393
2322
|
constructor(method, reason, mayHaveWritten) {
|
|
1394
2323
|
super(`${method} ${reason}`);
|
|
@@ -1431,22 +2360,6 @@ function isCodexAppServerPrewriteRequestCancellationError(error) {
|
|
|
1431
2360
|
function isCodexAppServerIndeterminateTransportError(error) {
|
|
1432
2361
|
return error instanceof Error && "code" in error && error.code === "CODEX_APP_SERVER_REQUEST_TRANSPORT_INDETERMINATE" && "mayHaveWritten" in error && error.mayHaveWritten === true;
|
|
1433
2362
|
}
|
|
1434
|
-
function formatCodexAppServerRpcErrorMessage(error, method) {
|
|
1435
|
-
const message = error.message || `${method} failed`;
|
|
1436
|
-
const detail = readCodexAppServerRpcReloginDetail(error.data);
|
|
1437
|
-
return detail && !message.includes(detail) ? `${message}: ${detail}` : message;
|
|
1438
|
-
}
|
|
1439
|
-
function readCodexAppServerRpcReloginDetail(data) {
|
|
1440
|
-
const record = isJsonObject(data) ? data : void 0;
|
|
1441
|
-
const nested = isJsonObject(record?.error) ? record.error : record;
|
|
1442
|
-
if (!nested) return;
|
|
1443
|
-
const isRelogin = nested.action === "relogin" || nested.reason === "cloudRequirements" && nested.errorCode === "Auth";
|
|
1444
|
-
const detail = typeof nested.detail === "string" ? nested.detail.trim() : "";
|
|
1445
|
-
return isRelogin && detail ? detail : void 0;
|
|
1446
|
-
}
|
|
1447
|
-
function isJsonObject(value) {
|
|
1448
|
-
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
1449
|
-
}
|
|
1450
2363
|
/** Returns true for errors that mean the app-server transport is closed. */
|
|
1451
2364
|
function isCodexAppServerConnectionClosedError(error) {
|
|
1452
2365
|
if (!(error instanceof Error)) return false;
|
|
@@ -1469,8 +2382,8 @@ var CodexAppServerClient = class CodexAppServerClient {
|
|
|
1469
2382
|
this.child = child;
|
|
1470
2383
|
this.lines = createInterface({ input: child.stdout });
|
|
1471
2384
|
this.lines.on("line", (line) => this.handleLine(line));
|
|
1472
|
-
this.lines.on("error", (error) => this.closeWithError(
|
|
1473
|
-
child.stdout.on("error", (error) => this.closeWithError(
|
|
2385
|
+
this.lines.on("error", (error) => this.closeWithError(toStringifiedError(error)));
|
|
2386
|
+
child.stdout.on("error", (error) => this.closeWithError(toStringifiedError(error)));
|
|
1474
2387
|
child.stderr.setEncoding("utf8");
|
|
1475
2388
|
child.stderr.on("data", (text) => {
|
|
1476
2389
|
this.stderrTail = appendBoundedTail(this.stderrTail, text, CODEX_APP_SERVER_STDERR_TAIL_MAX);
|
|
@@ -1480,12 +2393,12 @@ var CodexAppServerClient = class CodexAppServerClient {
|
|
|
1480
2393
|
child.stderr.on("error", (error) => {
|
|
1481
2394
|
embeddedAgentLog.warn("codex app-server stderr stream failed", { error });
|
|
1482
2395
|
});
|
|
1483
|
-
child.once("error", (error) => this.closeWithError(
|
|
2396
|
+
child.once("error", (error) => this.closeWithError(toStringifiedError(error)));
|
|
1484
2397
|
child.once("exit", (code, signal) => {
|
|
1485
2398
|
this.transportExited = true;
|
|
1486
2399
|
this.closeWithError(buildCodexAppServerExitError(code, signal, this.stderrTail));
|
|
1487
2400
|
});
|
|
1488
|
-
child.stdin.on?.("error", (error) => this.closeWithError(
|
|
2401
|
+
child.stdin.on?.("error", (error) => this.closeWithError(toStringifiedError(error)));
|
|
1489
2402
|
}
|
|
1490
2403
|
/** Starts a new app-server client using resolved runtime start options. */
|
|
1491
2404
|
static start(options) {
|
|
@@ -1512,7 +2425,13 @@ var CodexAppServerClient = class CodexAppServerClient {
|
|
|
1512
2425
|
title: "OpenClaw",
|
|
1513
2426
|
version: OPENCLAW_VERSION
|
|
1514
2427
|
},
|
|
1515
|
-
capabilities: {
|
|
2428
|
+
capabilities: {
|
|
2429
|
+
experimentalApi: true,
|
|
2430
|
+
extensions: {
|
|
2431
|
+
"openai/standard-form-input": {},
|
|
2432
|
+
"openai/form": {}
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
1516
2435
|
});
|
|
1517
2436
|
this.serverVersion = assertSupportedCodexAppServerVersion(response);
|
|
1518
2437
|
this.runtimeIdentity = buildCodexAppServerRuntimeIdentity(response, this.serverVersion);
|
|
@@ -1531,6 +2450,10 @@ var CodexAppServerClient = class CodexAppServerClient {
|
|
|
1531
2450
|
getStderrDiagnostic() {
|
|
1532
2451
|
return redactCodexAppServerLinePreview(this.stderrTail) || void 0;
|
|
1533
2452
|
}
|
|
2453
|
+
/** Returns the terminal transport error that closed this physical client. */
|
|
2454
|
+
getCloseError() {
|
|
2455
|
+
return this.closeError;
|
|
2456
|
+
}
|
|
1534
2457
|
/** Stable generation id for this exact physical client instance. */
|
|
1535
2458
|
getInstanceId() {
|
|
1536
2459
|
return this.instanceId;
|
|
@@ -1700,7 +2623,7 @@ var CodexAppServerClient = class CodexAppServerClient {
|
|
|
1700
2623
|
onWriteAttempt?.();
|
|
1701
2624
|
this.writeMessage(message, (error) => rejectPending(error));
|
|
1702
2625
|
} catch (error) {
|
|
1703
|
-
rejectPending(
|
|
2626
|
+
rejectPending(toStringifiedError(error));
|
|
1704
2627
|
}
|
|
1705
2628
|
});
|
|
1706
2629
|
}
|
|
@@ -1871,7 +2794,7 @@ var CodexAppServerClient = class CodexAppServerClient {
|
|
|
1871
2794
|
result: defaultServerRequestResponse(request)
|
|
1872
2795
|
});
|
|
1873
2796
|
} catch (error) {
|
|
1874
|
-
const message =
|
|
2797
|
+
const message = coerceErrorMessage(error);
|
|
1875
2798
|
embeddedAgentLog.warn("codex app-server server request handler failed", {
|
|
1876
2799
|
id: request.id,
|
|
1877
2800
|
method: request.method,
|
|
@@ -1958,7 +2881,7 @@ function defaultServerRequestResponse(request) {
|
|
|
1958
2881
|
scope: "turn"
|
|
1959
2882
|
};
|
|
1960
2883
|
if (request.method === "item/tool/requestUserInput") return { answers: {} };
|
|
1961
|
-
if (request.method === "mcpServer/elicitation/request") return {
|
|
2884
|
+
if (request.method === "mcpServer/elicitation/request") return createCodexElicitationResponse("decline", null, { message: "OpenClaw has no interactive handler for this elicitation." });
|
|
1962
2885
|
return {};
|
|
1963
2886
|
}
|
|
1964
2887
|
function stringifyCodexAppServerMessage(message) {
|
|
@@ -1978,24 +2901,30 @@ function timeoutServerRequestResponse(request) {
|
|
|
1978
2901
|
var CodexAppServerVersionError = class extends Error {
|
|
1979
2902
|
constructor(detectedVersion) {
|
|
1980
2903
|
const detected = detectedVersion ? `detected ${detectedVersion}` : "OpenClaw could not determine the running Codex version";
|
|
1981
|
-
super(`Codex app-server ${
|
|
2904
|
+
super(`Codex app-server ${MIN_SUPPORTED_CODEX_APP_SERVER_VERSION} or newer is required, but ${detected}. Update the configured Codex app-server binary, or remove custom command overrides to use the managed binary.`);
|
|
1982
2905
|
this.name = "CodexAppServerVersionError";
|
|
1983
2906
|
this.detectedVersion = detectedVersion;
|
|
1984
2907
|
}
|
|
1985
2908
|
};
|
|
1986
2909
|
function assertSupportedCodexAppServerVersion(response) {
|
|
1987
2910
|
const detectedVersion = readCodexVersionFromUserAgent(response.userAgent);
|
|
1988
|
-
if (detectedVersion
|
|
2911
|
+
if (!detectedVersion) throw new CodexAppServerVersionError(detectedVersion);
|
|
2912
|
+
const detected = parse(detectedVersion);
|
|
2913
|
+
if (!detected || detected.compare("0.147.0") < 0) throw new CodexAppServerVersionError(detectedVersion);
|
|
2914
|
+
if (detected.compare("0.149.1") > 0) embeddedAgentLog.warn("codex app-server is newer than OpenClaw's managed runtime; continuing with normal startup validation", {
|
|
2915
|
+
detectedVersion,
|
|
2916
|
+
validatedVersion: CODEX_APP_SERVER_VERSION
|
|
2917
|
+
});
|
|
1989
2918
|
return detectedVersion;
|
|
1990
2919
|
}
|
|
1991
2920
|
function isUnsupportedCodexAppServerVersionError(error) {
|
|
1992
2921
|
return error instanceof CodexAppServerVersionError;
|
|
1993
2922
|
}
|
|
1994
2923
|
function buildCodexAppServerRuntimeIdentity(response, serverVersion) {
|
|
1995
|
-
const userAgent =
|
|
1996
|
-
const codexHome =
|
|
1997
|
-
const platformFamily =
|
|
1998
|
-
const platformOs =
|
|
2924
|
+
const userAgent = normalizeOptionalString(response.userAgent);
|
|
2925
|
+
const codexHome = normalizeOptionalString(response.codexHome);
|
|
2926
|
+
const platformFamily = normalizeOptionalString(response.platformFamily);
|
|
2927
|
+
const platformOs = normalizeOptionalString(response.platformOs);
|
|
1999
2928
|
return {
|
|
2000
2929
|
serverVersion,
|
|
2001
2930
|
...userAgent ? { userAgent } : {},
|
|
@@ -2004,10 +2933,6 @@ function buildCodexAppServerRuntimeIdentity(response, serverVersion) {
|
|
|
2004
2933
|
...platformOs ? { platformOs } : {}
|
|
2005
2934
|
};
|
|
2006
2935
|
}
|
|
2007
|
-
function readNonEmptyInitializeString(value) {
|
|
2008
|
-
const trimmed = value?.trim();
|
|
2009
|
-
return trimmed ? trimmed : void 0;
|
|
2010
|
-
}
|
|
2011
2936
|
/** Extracts the Codex version from the app-server initialize user-agent field. */
|
|
2012
2937
|
function readCodexVersionFromUserAgent(userAgent) {
|
|
2013
2938
|
return (userAgent?.match(/^[^/]+\/(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)(?:[\s(]|$)/))?.[1];
|
|
@@ -2027,7 +2952,7 @@ function buildCodexAppServerExitError(code, signal, stderrTail) {
|
|
|
2027
2952
|
}
|
|
2028
2953
|
function shouldBufferCodexAppServerParseFailure(value, error) {
|
|
2029
2954
|
if (!value.startsWith("{") && !value.startsWith("[")) return false;
|
|
2030
|
-
const message =
|
|
2955
|
+
const message = coerceErrorMessage(error);
|
|
2031
2956
|
return message.includes("Unterminated string") || message.includes("Unexpected end of JSON input");
|
|
2032
2957
|
}
|
|
2033
2958
|
function logCodexAppServerParseFailure(value, error, fragmentCount) {
|
|
@@ -2035,7 +2960,7 @@ function logCodexAppServerParseFailure(value, error, fragmentCount) {
|
|
|
2035
2960
|
const suffix = fragmentCount > 1 ? ` fragments=${fragmentCount}` : "";
|
|
2036
2961
|
embeddedAgentLog.warn("failed to parse codex app-server message", {
|
|
2037
2962
|
error,
|
|
2038
|
-
errorMessage:
|
|
2963
|
+
errorMessage: coerceErrorMessage(error),
|
|
2039
2964
|
fragmentCount,
|
|
2040
2965
|
linePreview,
|
|
2041
2966
|
consoleMessage: `failed to parse codex app-server message${suffix}: preview=${JSON.stringify(linePreview)}`
|
|
@@ -2056,206 +2981,101 @@ function formatExitValue(value) {
|
|
|
2056
2981
|
return "unknown";
|
|
2057
2982
|
}
|
|
2058
2983
|
//#endregion
|
|
2059
|
-
//#region extensions/codex/src/app-server/
|
|
2984
|
+
//#region extensions/codex/src/app-server/attempt-timeouts.ts
|
|
2060
2985
|
/**
|
|
2061
|
-
*
|
|
2062
|
-
*
|
|
2986
|
+
* Timeout defaults and normalizers for Codex app-server startup and turn
|
|
2987
|
+
* liveness watches.
|
|
2063
2988
|
*/
|
|
2064
|
-
|
|
2065
|
-
const
|
|
2066
|
-
/**
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
const commandPath = expectDefined(commandPaths[0], "resolved managed Codex command path");
|
|
2082
|
-
const managedFallbackCommandPaths = commandPaths.slice(1);
|
|
2083
|
-
return {
|
|
2084
|
-
...startOptions,
|
|
2085
|
-
command: commandPath,
|
|
2086
|
-
commandSource: "resolved-managed",
|
|
2087
|
-
...managedFallbackCommandPaths.length > 0 ? { managedFallbackCommandPaths } : {}
|
|
2088
|
-
};
|
|
2089
|
-
}
|
|
2090
|
-
/** Resolves the native artifact behind a successful managed launcher selection. */
|
|
2091
|
-
function resolveManagedCodexNativeCommand(command, options = {}) {
|
|
2092
|
-
const platform = options.platform ?? process.platform;
|
|
2093
|
-
if (platform === "darwin" && MACOS_DESKTOP_CODEX_APP_SERVER_COMMANDS.some((candidate) => candidate === command)) return command;
|
|
2094
|
-
const target = resolveCodexNativeTarget(platform, options.arch ?? process.arch);
|
|
2095
|
-
if (!target) return;
|
|
2096
|
-
const packageRoot = resolveManagedCodexPackageRootForCommand(command, platform);
|
|
2097
|
-
if (!packageRoot) return;
|
|
2098
|
-
const resolvePackageJson = options.resolvePackageJson ?? resolvePackageJsonFromRoot;
|
|
2099
|
-
const pathExists = options.pathExists ?? existsSync;
|
|
2100
|
-
for (const packageName of [target.packageName, MANAGED_CODEX_APP_SERVER_PACKAGE]) {
|
|
2101
|
-
const packageJsonPath = resolvePackageJson(packageName, packageRoot);
|
|
2102
|
-
if (!packageJsonPath) continue;
|
|
2103
|
-
const candidate = path.join(path.dirname(packageJsonPath), "vendor", target.triple, "bin", platform === "win32" ? "codex.exe" : "codex");
|
|
2104
|
-
if (pathExists(candidate)) return candidate;
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
function resolveManagedCodexPackageRootForCommand(command, platform) {
|
|
2108
|
-
const pathApi = pathForPlatform(platform);
|
|
2109
|
-
const commandPaths = [command];
|
|
2110
|
-
try {
|
|
2111
|
-
commandPaths.unshift(realpathSync(command));
|
|
2112
|
-
} catch {}
|
|
2113
|
-
for (const commandPath of commandPaths) {
|
|
2114
|
-
let current = pathApi.dirname(commandPath);
|
|
2115
|
-
while (true) {
|
|
2116
|
-
if (pathApi.basename(current) === "codex" && pathApi.basename(pathApi.dirname(current)) === "@openai") return current;
|
|
2117
|
-
if (pathApi.basename(current) === ".bin") return pathApi.join(pathApi.dirname(current), "@openai", "codex");
|
|
2118
|
-
const parent = pathApi.dirname(current);
|
|
2119
|
-
if (parent === current) break;
|
|
2120
|
-
current = parent;
|
|
2121
|
-
}
|
|
2989
|
+
/** Minimum startup timeout accepted by the Codex app-server harness. */
|
|
2990
|
+
const CODEX_APP_SERVER_STARTUP_TIMEOUT_FLOOR_MS = 100;
|
|
2991
|
+
/** Default idle timeout while waiting for app-server turn completion. */
|
|
2992
|
+
const CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS = 6e4;
|
|
2993
|
+
/** Short guard after apparent assistant completion. */
|
|
2994
|
+
const CODEX_TURN_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS = 1e4;
|
|
2995
|
+
const CODEX_POST_TOOL_RAW_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS = 5 * 6e4;
|
|
2996
|
+
/** Guard after reasoning/commentary progress when no tool handoff occurred. */
|
|
2997
|
+
const CODEX_POST_REASONING_REPLY_IDLE_TIMEOUT_MS = 5 * 6e4;
|
|
2998
|
+
/** Long terminal idle watch for app-server turns that never send completion. */
|
|
2999
|
+
const CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS = 30 * 6e4;
|
|
3000
|
+
var CodexAppServerStartupError = class extends Error {
|
|
3001
|
+
constructor(reason, message = reason === "timed_out" ? "codex app-server startup timed out" : "codex app-server startup aborted") {
|
|
3002
|
+
super(message);
|
|
3003
|
+
this.reason = reason;
|
|
3004
|
+
this.code = "CODEX_APP_SERVER_STARTUP_CANCELLED";
|
|
3005
|
+
this.name = "CodexAppServerStartupError";
|
|
2122
3006
|
}
|
|
3007
|
+
};
|
|
3008
|
+
function isCodexAppServerStartupError(error, reason) {
|
|
3009
|
+
return error instanceof Error && "code" in error && error.code === "CODEX_APP_SERVER_STARTUP_CANCELLED" && "reason" in error && (error.reason === "aborted" || error.reason === "timed_out") && (reason === void 0 || error.reason === reason);
|
|
2123
3010
|
}
|
|
2124
|
-
function
|
|
2125
|
-
|
|
2126
|
-
packageName: "@openai/codex-linux-x64",
|
|
2127
|
-
triple: "x86_64-unknown-linux-musl"
|
|
2128
|
-
};
|
|
2129
|
-
if ((platform === "linux" || platform === "android") && arch === "arm64") return {
|
|
2130
|
-
packageName: "@openai/codex-linux-arm64",
|
|
2131
|
-
triple: "aarch64-unknown-linux-musl"
|
|
2132
|
-
};
|
|
2133
|
-
if (platform === "darwin" && arch === "x64") return {
|
|
2134
|
-
packageName: "@openai/codex-darwin-x64",
|
|
2135
|
-
triple: "x86_64-apple-darwin"
|
|
2136
|
-
};
|
|
2137
|
-
if (platform === "darwin" && arch === "arm64") return {
|
|
2138
|
-
packageName: "@openai/codex-darwin-arm64",
|
|
2139
|
-
triple: "aarch64-apple-darwin"
|
|
2140
|
-
};
|
|
2141
|
-
if (platform === "win32" && arch === "x64") return {
|
|
2142
|
-
packageName: "@openai/codex-win32-x64",
|
|
2143
|
-
triple: "x86_64-pc-windows-msvc"
|
|
2144
|
-
};
|
|
2145
|
-
if (platform === "win32" && arch === "arm64") return {
|
|
2146
|
-
packageName: "@openai/codex-win32-arm64",
|
|
2147
|
-
triple: "aarch64-pc-windows-msvc"
|
|
2148
|
-
};
|
|
3011
|
+
function resolvePositiveIntegerTimeoutMs(value, fallbackMs) {
|
|
3012
|
+
return resolveTimerTimeoutMs(value, resolveTimerTimeoutMs(fallbackMs, 1));
|
|
2149
3013
|
}
|
|
2150
|
-
|
|
3014
|
+
/** Runs startup work with abort and timeout handling plus optional cleanup. */
|
|
3015
|
+
async function withCodexStartupTimeout(params) {
|
|
3016
|
+
if (params.signal.aborted) throw new CodexAppServerStartupError("aborted");
|
|
3017
|
+
let timeout;
|
|
3018
|
+
let abortCleanup;
|
|
3019
|
+
let timeoutError;
|
|
3020
|
+
let timeoutCleanup;
|
|
2151
3021
|
try {
|
|
2152
|
-
return
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
}
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
function resolveDesktopCodexAppServerCommandCandidates(platform) {
|
|
2176
|
-
return platform === "darwin" ? [...MACOS_DESKTOP_CODEX_APP_SERVER_COMMANDS] : [];
|
|
2177
|
-
}
|
|
2178
|
-
function resolveDefaultCodexPluginRoot(moduleDir) {
|
|
2179
|
-
const moduleBaseName = path.basename(moduleDir);
|
|
2180
|
-
if (moduleBaseName === "dist" || moduleBaseName === "dist-runtime") return path.dirname(moduleDir);
|
|
2181
|
-
return path.resolve(moduleDir, "..", "..");
|
|
2182
|
-
}
|
|
2183
|
-
function resolveManagedCodexAppServerCandidateRoots(pluginRoot, platform) {
|
|
2184
|
-
const pathApi = pathForPlatform(platform);
|
|
2185
|
-
const directRoots = [
|
|
2186
|
-
pluginRoot,
|
|
2187
|
-
pathApi.dirname(pluginRoot),
|
|
2188
|
-
pathApi.dirname(pathApi.dirname(pluginRoot)),
|
|
2189
|
-
isDistExtensionRoot(pluginRoot, platform) ? pathApi.dirname(pathApi.dirname(pathApi.dirname(pluginRoot))) : null
|
|
2190
|
-
].filter((root) => Boolean(root));
|
|
2191
|
-
return [.../* @__PURE__ */ new Set([...directRoots, ...resolveNearestNodeModulesProjectRoots(directRoots, platform)])];
|
|
2192
|
-
}
|
|
2193
|
-
function resolveNearestNodeModulesProjectRoots(roots, platform) {
|
|
2194
|
-
const pathApi = pathForPlatform(platform);
|
|
2195
|
-
const projectRoots = [];
|
|
2196
|
-
for (const root of roots) {
|
|
2197
|
-
let current = pathApi.resolve(root);
|
|
2198
|
-
while (true) {
|
|
2199
|
-
if (pathApi.basename(current) === "node_modules") {
|
|
2200
|
-
projectRoots.push(pathApi.dirname(current));
|
|
2201
|
-
break;
|
|
2202
|
-
}
|
|
2203
|
-
const parent = pathApi.dirname(current);
|
|
2204
|
-
if (parent === current) break;
|
|
2205
|
-
current = parent;
|
|
3022
|
+
return await Promise.race([params.operation(), new Promise((_, reject) => {
|
|
3023
|
+
const rejectOnce = (error) => {
|
|
3024
|
+
if (timeout) {
|
|
3025
|
+
clearTimeout(timeout);
|
|
3026
|
+
timeout = void 0;
|
|
3027
|
+
}
|
|
3028
|
+
reject(error);
|
|
3029
|
+
};
|
|
3030
|
+
timeout = setTimeout(() => {
|
|
3031
|
+
timeoutError = new CodexAppServerStartupError("timed_out");
|
|
3032
|
+
timeoutCleanup = Promise.resolve(params.onTimeout?.()).then(() => void 0, () => void 0);
|
|
3033
|
+
timeoutCleanup.finally(() => {
|
|
3034
|
+
rejectOnce(timeoutError);
|
|
3035
|
+
});
|
|
3036
|
+
}, params.timeoutMs);
|
|
3037
|
+
const abortListener = () => rejectOnce(new CodexAppServerStartupError("aborted"));
|
|
3038
|
+
params.signal.addEventListener("abort", abortListener, { once: true });
|
|
3039
|
+
abortCleanup = () => params.signal.removeEventListener("abort", abortListener);
|
|
3040
|
+
})]);
|
|
3041
|
+
} catch (error) {
|
|
3042
|
+
if (timeoutError) {
|
|
3043
|
+
await timeoutCleanup;
|
|
3044
|
+
throw timeoutError;
|
|
2206
3045
|
}
|
|
3046
|
+
throw error;
|
|
3047
|
+
} finally {
|
|
3048
|
+
if (timeout) clearTimeout(timeout);
|
|
3049
|
+
abortCleanup?.();
|
|
2207
3050
|
}
|
|
2208
|
-
return projectRoots;
|
|
2209
|
-
}
|
|
2210
|
-
function resolveManagedCodexPackageBinCandidates(roots, platform) {
|
|
2211
|
-
if (platform === "win32") return [];
|
|
2212
|
-
const candidates = [];
|
|
2213
|
-
for (const root of roots) {
|
|
2214
|
-
const candidate = resolveManagedCodexPackageBinCandidate(root);
|
|
2215
|
-
if (candidate) candidates.push(candidate);
|
|
2216
|
-
}
|
|
2217
|
-
return candidates;
|
|
2218
3051
|
}
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
const binPath = typeof packageJson.bin === "string" ? packageJson.bin : isRecord(packageJson.bin) && typeof packageJson.bin.codex === "string" ? packageJson.bin.codex : null;
|
|
2225
|
-
return binPath ? path.resolve(packageRoot, binPath) : null;
|
|
2226
|
-
} catch {
|
|
2227
|
-
return null;
|
|
2228
|
-
}
|
|
3052
|
+
/** Resolves startup timeout while honoring the configured floor. */
|
|
3053
|
+
function resolveCodexStartupTimeoutMs(params) {
|
|
3054
|
+
const timeoutFloorMs = resolvePositiveIntegerTimeoutMs(params.timeoutFloorMs, CODEX_APP_SERVER_STARTUP_TIMEOUT_FLOOR_MS);
|
|
3055
|
+
const timeoutMs = resolvePositiveIntegerTimeoutMs(params.timeoutMs, timeoutFloorMs);
|
|
3056
|
+
return Math.max(timeoutFloorMs, timeoutMs);
|
|
2229
3057
|
}
|
|
2230
|
-
|
|
2231
|
-
|
|
3058
|
+
/** Resolves the completion-idle timeout for an active turn. */
|
|
3059
|
+
function resolveCodexTurnCompletionIdleTimeoutMs(value) {
|
|
3060
|
+
return resolvePositiveIntegerTimeoutMs(value, CODEX_TURN_COMPLETION_IDLE_TIMEOUT_MS);
|
|
2232
3061
|
}
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
const distDir = pathApi.dirname(extensionsDir);
|
|
2237
|
-
return pathApi.basename(extensionsDir) === "extensions" && (pathApi.basename(distDir) === "dist" || pathApi.basename(distDir) === "dist-runtime");
|
|
3062
|
+
/** Resolves the short assistant-completion release timeout. */
|
|
3063
|
+
function resolveCodexTurnAssistantCompletionIdleTimeoutMs(value) {
|
|
3064
|
+
return resolvePositiveIntegerTimeoutMs(value, CODEX_TURN_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS);
|
|
2238
3065
|
}
|
|
2239
|
-
|
|
2240
|
-
|
|
3066
|
+
/** Resolves the conservative post-tool raw assistant guard timeout. */
|
|
3067
|
+
function resolveCodexPostToolRawAssistantCompletionIdleTimeoutMs(value, fallbackMs) {
|
|
3068
|
+
return resolvePositiveIntegerTimeoutMs(value, Math.max(resolvePositiveIntegerTimeoutMs(void 0, fallbackMs), CODEX_POST_TOOL_RAW_ASSISTANT_COMPLETION_IDLE_TIMEOUT_MS));
|
|
2241
3069
|
}
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
throw new Error([
|
|
2247
|
-
`Managed Codex app-server binary was not found for ${MANAGED_CODEX_APP_SERVER_PACKAGE}.`,
|
|
2248
|
-
"Reinstall or update OpenClaw, or run pnpm install in a source checkout.",
|
|
2249
|
-
"Set plugins.entries.codex.config.appServer.command or OPENCLAW_CODEX_APP_SERVER_BIN to use a custom Codex binary."
|
|
2250
|
-
].join(" "));
|
|
3070
|
+
/** Resolves the long terminal turn idle timeout. */
|
|
3071
|
+
function resolveCodexTurnTerminalIdleTimeoutMs(value, runTimeoutOverrideMs) {
|
|
3072
|
+
const explicitRunBudgetMs = resolvePositiveIntegerTimeoutMs(runTimeoutOverrideMs, CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS);
|
|
3073
|
+
return resolvePositiveIntegerTimeoutMs(value, Math.max(CODEX_TURN_TERMINAL_IDLE_TIMEOUT_MS, explicitRunBudgetMs));
|
|
2251
3074
|
}
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
} catch {
|
|
2257
|
-
return false;
|
|
2258
|
-
}
|
|
3075
|
+
/** Adds gateway grace time to a caller timeout without overflowing invalid values. */
|
|
3076
|
+
function resolveCodexGatewayTimeoutWithGraceMs(timeoutMs, graceMs = 1e4) {
|
|
3077
|
+
const timeout = resolvePositiveIntegerTimeoutMs(timeoutMs, 1);
|
|
3078
|
+
return addTimerTimeoutGraceMs(timeout, resolveTimerTimeoutMs(graceMs, 0, 0)) ?? timeout;
|
|
2259
3079
|
}
|
|
2260
3080
|
//#endregion
|
|
2261
3081
|
//#region extensions/codex/src/app-server/native-config-fence.ts
|
|
@@ -2325,6 +3145,9 @@ async function waitForPreviousFence(previous, options) {
|
|
|
2325
3145
|
* Thin Codex app-server timeout adapter around OpenClaw's shared security
|
|
2326
3146
|
* runtime timeout helper.
|
|
2327
3147
|
*/
|
|
3148
|
+
function resolveAbortError(signal) {
|
|
3149
|
+
return signal.reason instanceof Error ? signal.reason : new Error("Codex app-server operation aborted", { cause: signal.reason });
|
|
3150
|
+
}
|
|
2328
3151
|
/** Awaits a promise with a Codex-specific timeout error message. */
|
|
2329
3152
|
async function withTimeout$1(promise, timeoutMs, timeoutMessage, createError) {
|
|
2330
3153
|
return await withTimeout(promise, timeoutMs, {
|
|
@@ -2332,6 +3155,22 @@ async function withTimeout$1(promise, timeoutMs, timeoutMessage, createError) {
|
|
|
2332
3155
|
...createError ? { createError } : {}
|
|
2333
3156
|
});
|
|
2334
3157
|
}
|
|
3158
|
+
/** Bounds an operation by both its owner lifecycle and one total wall-clock budget. */
|
|
3159
|
+
async function withAbortableTimeout(params) {
|
|
3160
|
+
const signal = params.signal;
|
|
3161
|
+
if (signal?.aborted) throw resolveAbortError(signal);
|
|
3162
|
+
let removeAbortListener;
|
|
3163
|
+
const operation = signal ? Promise.race([params.promise, new Promise((_, reject) => {
|
|
3164
|
+
const onAbort = () => reject(resolveAbortError(signal));
|
|
3165
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
3166
|
+
removeAbortListener = () => signal.removeEventListener("abort", onAbort);
|
|
3167
|
+
})]) : params.promise;
|
|
3168
|
+
try {
|
|
3169
|
+
return await withTimeout$1(operation, params.timeoutMs, params.timeoutMessage, params.createTimeoutError);
|
|
3170
|
+
} finally {
|
|
3171
|
+
removeAbortListener?.();
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
2335
3174
|
//#endregion
|
|
2336
3175
|
//#region extensions/codex/src/app-server/shared-client.ts
|
|
2337
3176
|
/**
|
|
@@ -2358,7 +3197,6 @@ var shared_client_exports = /* @__PURE__ */ __exportAll({
|
|
|
2358
3197
|
withLeasedCodexAppServerClientStartSelectionRetry: () => withLeasedCodexAppServerClientStartSelectionRetry
|
|
2359
3198
|
});
|
|
2360
3199
|
const CODEX_APP_SERVER_INITIALIZE_TIMEOUT_MESSAGE = "codex app-server initialize timed out";
|
|
2361
|
-
const suspectClosedClients = /* @__PURE__ */ new WeakSet();
|
|
2362
3200
|
const SHARED_CODEX_APP_SERVER_CLIENT_STATE = Symbol.for("openclaw.codexAppServerClientState");
|
|
2363
3201
|
const SHARED_CODEX_APP_SERVER_CLIENT_DISPOSER = Symbol.for("openclaw.codexAppServerClientDisposer");
|
|
2364
3202
|
const CODEX_APP_SERVER_CLIENT_START_METADATA = Symbol.for("openclaw.codexAppServerClientStartMetadata");
|
|
@@ -2366,6 +3204,8 @@ function getSharedCodexAppServerClientState() {
|
|
|
2366
3204
|
const globalState = globalThis;
|
|
2367
3205
|
globalState[SHARED_CODEX_APP_SERVER_CLIENT_STATE] ??= {
|
|
2368
3206
|
clients: /* @__PURE__ */ new Map(),
|
|
3207
|
+
liveClients: /* @__PURE__ */ new Set(),
|
|
3208
|
+
entriesByClient: /* @__PURE__ */ new WeakMap(),
|
|
2369
3209
|
leasedReleases: /* @__PURE__ */ new WeakMap()
|
|
2370
3210
|
};
|
|
2371
3211
|
return globalState[SHARED_CODEX_APP_SERVER_CLIENT_STATE];
|
|
@@ -2480,9 +3320,11 @@ async function resolveCodexAppServerClientStartContext(options) {
|
|
|
2480
3320
|
startOptions: requestedStartOptions,
|
|
2481
3321
|
agentDir
|
|
2482
3322
|
})),
|
|
3323
|
+
agentId: options?.agentId,
|
|
2483
3324
|
agentDir,
|
|
2484
3325
|
authProfileId: usesNativeAuth || preparedAuth?.kind === "api-key" ? null : authProfileId,
|
|
2485
3326
|
...resolvedPreparedAuth ? { preparedAuth: resolvedPreparedAuth } : {},
|
|
3327
|
+
authRequirement,
|
|
2486
3328
|
config: options?.config,
|
|
2487
3329
|
pluginConfig: options?.pluginConfig,
|
|
2488
3330
|
...authProfileStore ? { authProfileStore } : {}
|
|
@@ -2725,14 +3567,21 @@ function createSharedCodexAppServerClientStartup(params) {
|
|
|
2725
3567
|
runtimeArtifactSignal: params.runtimeArtifactSignal,
|
|
2726
3568
|
config: params.config,
|
|
2727
3569
|
onStartedClient: (startedClient) => {
|
|
3570
|
+
const state = getSharedCodexAppServerClientState();
|
|
2728
3571
|
params.entry.client = startedClient;
|
|
3572
|
+
state.liveClients.add(startedClient);
|
|
3573
|
+
startedClient.addCloseHandler((closedClient) => {
|
|
3574
|
+
state.liveClients.delete(closedClient);
|
|
3575
|
+
if (state.entriesByClient.get(closedClient) === params.entry) clearSharedClientEntryIfCurrent(params.key, closedClient);
|
|
3576
|
+
});
|
|
2729
3577
|
for (const callback of params.entry.onStartedClientCallbacks) callback(startedClient);
|
|
2730
3578
|
retirePendingSharedClientEntryIfUnclaimed(params.key, params.entry);
|
|
2731
3579
|
},
|
|
2732
3580
|
onInitializedClient: () => initialized.resolve()
|
|
2733
3581
|
}).then((client) => {
|
|
3582
|
+
const state = getSharedCodexAppServerClientState();
|
|
2734
3583
|
params.entry.client = client;
|
|
2735
|
-
|
|
3584
|
+
state.entriesByClient.set(client, params.entry);
|
|
2736
3585
|
return client;
|
|
2737
3586
|
}, (error) => {
|
|
2738
3587
|
initialized.reject(error);
|
|
@@ -2772,7 +3621,7 @@ async function startInitializedCodexAppServerClient(params) {
|
|
|
2772
3621
|
const timeoutMs = params.timeoutMs ?? 0;
|
|
2773
3622
|
const startOptionsCandidates = resolveManagedFallbackStartOptions(params.startOptions);
|
|
2774
3623
|
for (const [index, startOptions] of startOptionsCandidates.entries()) {
|
|
2775
|
-
const runtimeArtifactModule = params.runtimeArtifactMode ? await import("./runtime-artifact-
|
|
3624
|
+
const runtimeArtifactModule = params.runtimeArtifactMode ? await import("./runtime-artifact-BvyT1uyd.js") : void 0;
|
|
2776
3625
|
const nativeCommandBeforeStart = startOptions.commandSource === "resolved-managed" ? resolveManagedCodexNativeCommand(startOptions.command) : void 0;
|
|
2777
3626
|
const runtimeArtifactBeforeStart = runtimeArtifactModule ? await runtimeArtifactModule.captureCodexAppServerRuntimeArtifactBeforeStart({
|
|
2778
3627
|
startOptions,
|
|
@@ -2922,11 +3771,9 @@ function retireSharedCodexAppServerClientIfCurrent(client, opts) {
|
|
|
2922
3771
|
entry.closeWhenIdle = true;
|
|
2923
3772
|
if (opts?.failActiveLeases) {
|
|
2924
3773
|
entry.closeError = /* @__PURE__ */ new Error("codex app-server client is closed");
|
|
2925
|
-
const closed = closeRetiredSharedClientEntry(entry);
|
|
2926
|
-
if (closed) suspectClosedClients.add(client);
|
|
2927
3774
|
return {
|
|
2928
3775
|
activeLeases: entry.activeLeases,
|
|
2929
|
-
closed
|
|
3776
|
+
closed: closeRetiredSharedClientEntry(entry)
|
|
2930
3777
|
};
|
|
2931
3778
|
}
|
|
2932
3779
|
const closed = closeRetiredSharedClientEntryIfIdle(entry);
|
|
@@ -2935,18 +3782,17 @@ function retireSharedCodexAppServerClientIfCurrent(client, opts) {
|
|
|
2935
3782
|
closed
|
|
2936
3783
|
};
|
|
2937
3784
|
}
|
|
2938
|
-
const
|
|
2939
|
-
if (
|
|
2940
|
-
if (opts?.failActiveLeases && !
|
|
2941
|
-
|
|
2942
|
-
client.close();
|
|
3785
|
+
const detachedEntry = state.entriesByClient.get(client);
|
|
3786
|
+
if (detachedEntry && (detachedEntry.client === client || detachedEntry.closeError)) {
|
|
3787
|
+
if (opts?.failActiveLeases && !detachedEntry.closeError) {
|
|
3788
|
+
detachedEntry.closeError = /* @__PURE__ */ new Error("codex app-server client is closed");
|
|
2943
3789
|
return {
|
|
2944
|
-
activeLeases,
|
|
2945
|
-
closed:
|
|
3790
|
+
activeLeases: detachedEntry.activeLeases,
|
|
3791
|
+
closed: closeRetiredSharedClientEntry(detachedEntry)
|
|
2946
3792
|
};
|
|
2947
3793
|
}
|
|
2948
3794
|
return {
|
|
2949
|
-
activeLeases,
|
|
3795
|
+
activeLeases: detachedEntry.activeLeases,
|
|
2950
3796
|
closed: false
|
|
2951
3797
|
};
|
|
2952
3798
|
}
|
|
@@ -3063,7 +3909,7 @@ function retirePendingSharedClientEntryIfUnclaimed(key, entry) {
|
|
|
3063
3909
|
closeRetiredSharedClientEntry(entry);
|
|
3064
3910
|
}
|
|
3065
3911
|
function collectSharedClients(state) {
|
|
3066
|
-
return [...
|
|
3912
|
+
return [...state.liveClients];
|
|
3067
3913
|
}
|
|
3068
3914
|
//#endregion
|
|
3069
|
-
export {
|
|
3915
|
+
export { rememberCodexRateLimitsRead as $, isCodexAppServerApprovalRequest as A, CodexAppServerRpcError as B, resolveCodexPostToolRawAssistantCompletionIdleTimeoutMs as C, resolveCodexTurnTerminalIdleTimeoutMs as D, resolveCodexTurnCompletionIdleTimeoutMs as E, isCodexAppServerPrewriteRequestCancellationError as F, isCodexAppServerClientRuntimeLive as G, claimCodexAppServerLiveThread as H, isCodexAppServerRequestTimeoutError as I, releaseCodexAppServerLiveThread as J, isCodexAppServerLiveThreadClaimed as K, isUnsupportedCodexAppServerVersionError as L, isCodexAppServerConnectionClosedError as M, isCodexAppServerIndeterminateRequestCancellationError as N, withCodexStartupTimeout as O, isCodexAppServerIndeterminateTransportError as P, readRecentCodexRateLimits as Q, resolveCodexAppServerClientInstanceId as R, resolveCodexGatewayTimeoutWithGraceMs as S, resolveCodexTurnAssistantCompletionIdleTimeoutMs as T, consumeCodexAppServerLiveThread as U, createCodexElicitationResponse as V, ensureCodexAppServerClientRuntime as W, unsubscribeCodexAppServerLiveThread as X, retainCodexAppServerLiveThread as Y, readCodexRateLimitsRevision as Z, withTimeout$1 as _, getLeasedSharedCodexAppServerClient as a, resolveCodexAppServerPreparedAuthHandoff as at, CodexAppServerStartupError as b, releaseCodexAppServerClientLease as c, CODEX_OPENCLAW_DIRECT_DYNAMIC_TOOL_NAMESPACE as ct, retainSharedCodexAppServerClientByInstanceId as d, resolveFirstExistingMacOSDesktopCodexBundledMarketplacePath as dt, resolveCodexAppServerAuthAccountCacheKey as et, retainSharedCodexAppServerClientIfCurrent as f, resolveCodexAppServerHomeDir as ft, withAbortableTimeout as g, withLeasedCodexAppServerClientStartSelectionRetry as h, createIsolatedCodexAppServerClient as i, resolveCodexAppServerPreparedApiKeyCacheKey as it, isCodexAppServerBrokenPipeError as j, getCodexAppServerClientInstanceId as k, releaseLeasedSharedCodexAppServerClient as l, flattenCodexDynamicToolFunctions as lt, shared_client_exports as m, clearSharedCodexAppServerClientIfCurrentAndUnclaimed as n, resolveCodexAppServerAuthProfileIdForAgent as nt, getSharedCodexAppServerClient as o, CODEX_INTERACTIVE_CUSTOM_THREAD_SOURCES as ot, retireSharedCodexAppServerClientIfCurrent as p, resolveCodexAppServerLocalHomeDir as pt, protectCodexAppServerLiveThread as q, clearSharedCodexAppServerClientIfCurrentAndWait as r, resolveCodexAppServerFallbackApiKeyCacheKey as rt, isCodexAppServerStartSelectionChangedError as s, CODEX_INTERACTIVE_THREAD_SOURCE_KINDS as st, clearSharedCodexAppServerClientIfCurrent as t, resolveCodexAppServerAuthProfileId as tt, resolveCodexNativeConfigFenceKey as u, isJsonObject as ut, acquireCodexNativeConfigFence as v, resolveCodexStartupTimeoutMs as w, isCodexAppServerStartupError as x, CODEX_POST_REASONING_REPLY_IDLE_TIMEOUT_MS as y, closeCodexAppServerTransportAndWait as z };
|