@jskit-ai/assistant-core 0.1.199 → 0.1.201
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/examples/conversation/README.md +8 -1
- package/examples/conversation/package.json +5 -5
- package/fixtures/responsive-assistant/App.vue +4 -1
- package/package.json +99 -36
- package/src/client/conversation/AssistantConversationElement.vue +27 -4
- package/src/client/conversation/AssistantConversationStatus.vue +24 -0
- package/src/client/conversation/AssistantGoalControl.vue +8 -5
- package/src/client/conversation/AssistantTranscript.vue +10 -3
- package/src/client/conversation/index.js +2 -0
- package/src/client/conversation/messageDelivery.js +115 -32
- package/src/client/conversation/retainedConversation.js +32 -0
- package/src/client/conversation/useAssistantQuestions.js +76 -0
- package/src/client/lib/assistantApi.js +72 -2
- package/src/server/conversation/agentRun.js +222 -0
- package/src/server/conversation/attachments.js +93 -0
- package/src/server/conversation/claudeClient.js +221 -0
- package/src/server/conversation/claudeHistory.js +256 -0
- package/src/server/conversation/claudeProcess.js +327 -0
- package/src/server/conversation/claudeTools.js +42 -0
- package/src/server/conversation/claudeTurn.js +1082 -0
- package/src/server/conversation/codexConfiguration.js +1183 -0
- package/src/server/conversation/codexConversations.js +1082 -0
- package/src/server/conversation/codexDetachedTurn.js +332 -0
- package/src/server/conversation/codexEvents.js +194 -1
- package/src/server/conversation/codexGoalCommands.js +124 -0
- package/src/server/conversation/codexHelperExecution.js +306 -0
- package/src/server/conversation/codexHelperThreadLedger.js +804 -0
- package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
- package/src/server/conversation/codexHelperThreads.js +638 -0
- package/src/server/conversation/codexHistoryAdapter.js +369 -0
- package/src/server/conversation/codexMessageCommands.js +663 -0
- package/src/server/conversation/codexMessageDelivery.js +211 -0
- package/src/server/conversation/codexNativeHistoryExport.js +95 -0
- package/src/server/conversation/codexNotificationObserver.js +482 -0
- package/src/server/conversation/codexNotificationQueue.js +103 -0
- package/src/server/conversation/codexObservationControl.js +174 -0
- package/src/server/conversation/codexOutput.js +931 -0
- package/src/server/conversation/codexProcess.js +1559 -0
- package/src/server/conversation/codexProcessLeader.js +14 -0
- package/src/server/conversation/codexProvider.js +2710 -0
- package/src/server/conversation/codexProviderOwner.js +1058 -0
- package/src/server/conversation/codexProviderSelection.js +218 -0
- package/src/server/conversation/codexReasoning.js +314 -0
- package/src/server/conversation/codexRenewalCommands.js +290 -0
- package/src/server/conversation/codexResultSettlement.js +409 -0
- package/src/server/conversation/codexRunJournal.js +658 -0
- package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
- package/src/server/conversation/codexSessionCleanup.js +96 -0
- package/src/server/conversation/codexSessionReconciliation.js +284 -0
- package/src/server/conversation/codexThreadReadiness.js +397 -0
- package/src/server/conversation/codexThreadStatus.js +354 -0
- package/src/server/conversation/codexTurn.js +701 -255
- package/src/server/conversation/codexTurnControl.js +317 -0
- package/src/server/conversation/codexTurnRecovery.js +990 -0
- package/src/server/conversation/codexTurnState.js +549 -0
- package/src/server/conversation/commandHook.js +18 -0
- package/src/server/conversation/commandWrapper.js +37 -0
- package/src/server/conversation/configuration.js +43 -0
- package/src/server/conversation/continuity.js +289 -0
- package/src/server/conversation/fileStorage.js +55 -0
- package/src/server/conversation/hookBridge.js +19 -0
- package/src/server/conversation/index.js +13 -0
- package/src/server/conversation/localExecution.js +665 -0
- package/src/server/conversation/memoryStorage.js +7 -72
- package/src/server/conversation/nativeHistoryExport.js +90 -0
- package/src/server/conversation/nativeStorage.js +23 -0
- package/src/server/conversation/openCodeClient.js +209 -19
- package/src/server/conversation/openCodePlugin.js +84 -0
- package/src/server/conversation/openCodeProcess.js +476 -0
- package/src/server/conversation/openCodeRuntime.js +1578 -0
- package/src/server/conversation/openCodeTools.js +60 -0
- package/src/server/conversation/openCodeTurn.js +557 -0
- package/src/server/conversation/provider.js +30 -0
- package/src/server/conversation/providers/api.js +136 -0
- package/src/server/conversation/providers/claude.js +63 -0
- package/src/server/conversation/providers/claudeDriver.js +425 -0
- package/src/server/conversation/providers/codex.js +248 -0
- package/src/server/conversation/providers/codexDriver.js +1078 -0
- package/src/server/conversation/providers/opencode.js +49 -0
- package/src/server/conversation/providers/opencodeDriver.js +650 -0
- package/src/server/conversation/providers/scoped.js +312 -0
- package/src/server/conversation/runtime.js +2220 -0
- package/src/server/conversation/runtimeStateUpgrade.js +125 -0
- package/src/server/conversation/storage.js +184 -0
- package/src/server/conversation/streams.js +7 -2
- package/src/server/conversation/structuredOutput.js +240 -0
- package/src/server/conversation/systemPrompt.js +54 -0
- package/src/server/conversation/tools.js +84 -0
- package/src/server/conversation/transcript.js +5 -4
- package/src/server/hosts/claudeHistory.js +5 -0
- package/src/server/hosts/claudeProcess.js +11 -0
- package/src/server/hosts/claudeTurn.js +4 -0
- package/src/server/hosts/codexConfiguration.js +20 -0
- package/src/server/hosts/codexEvents.js +13 -0
- package/src/server/hosts/codexProcess.js +12 -0
- package/src/server/hosts/codexProvider.js +23 -0
- package/src/server/hosts/codexTurn.js +20 -0
- package/src/server/hosts/nativeHistory.js +3 -0
- package/src/server/hosts/openCodeClient.js +3 -0
- package/src/server/hosts/openCodeProcess.js +23 -0
- package/src/server/hosts/openCodeTurn.js +7 -0
- package/src/server/index.js +2 -0
- package/src/server/lib/aiConnectionClient.js +13 -6
- package/src/server/lib/assistantCompletion.js +190 -0
- package/src/server/lib/assistantToolLoop.js +314 -0
- package/src/server/lib/serviceToolCatalog.js +25 -10
- package/src/shared/assistantResponseText.js +29 -0
- package/src/shared/conversation/attachments.js +19 -0
- package/src/shared/conversation/index.js +3 -0
- package/src/shared/conversation/pagination.js +78 -0
- package/src/shared/conversation/patches.js +150 -0
- package/src/shared/conversation/questions.js +48 -0
- package/src/shared/conversation/streaming.js +7 -4
- package/src/shared/index.js +2 -0
- package/src/shared/nativeProviders.js +45 -0
- package/src/testing/conversationStorageContract.js +31 -1
- package/src/testing/nativeCodex.js +11 -0
- package/src/testing/nativeOpenCode.js +3 -0
- package/test/aiConnectionClient.test.js +2 -0
- package/test/apiConversationDriver.test.js +73 -0
- package/test/assistantApiSurfaceHeader.test.js +85 -0
- package/test/assistantBoundedToolLoop.test.js +285 -0
- package/test/assistantScroll.browser.test.js +16 -0
- package/test/claudeClient.test.js +189 -0
- package/test/claudeConversationSession.test.js +1173 -0
- package/test/claudeInstructionResume.test.js +82 -0
- package/test/claudeProcess.test.js +204 -0
- package/test/claudeTurn.test.js +253 -0
- package/test/codexConversationSession.test.js +2122 -0
- package/test/codexGoal.test.js +37 -0
- package/test/codexHelperDeletion.test.js +163 -0
- package/test/codexHelperThreadLedger.test.js +494 -0
- package/test/codexHistoryAdapter.test.js +541 -0
- package/test/codexInteractiveArguments.test.js +30 -0
- package/test/codexIsolation.test.js +756 -0
- package/test/codexModelCatalog.test.js +362 -0
- package/test/codexNativeHistoryExport.test.js +81 -0
- package/test/codexThreadSelection.test.js +136 -0
- package/test/codexTurn.test.js +2158 -0
- package/test/conversation.test.js +110 -0
- package/test/conversationAgentRun.test.js +326 -0
- package/test/conversationApiFacilities.test.js +90 -0
- package/test/conversationAttachments.test.js +238 -0
- package/test/conversationCommandWrapper.test.js +48 -0
- package/test/conversationConfiguration.test.js +174 -0
- package/test/conversationNativeStorage.test.js +25 -0
- package/test/conversationReplacement.test.js +172 -0
- package/test/conversationRuntime.test.js +241 -0
- package/test/conversationRuntimeStateUpgrade.test.js +144 -0
- package/test/conversationSession.test.js +1080 -0
- package/test/conversationStreaming.test.js +68 -0
- package/test/conversationSystemPrompt.test.js +84 -0
- package/test/conversationTools.test.js +416 -0
- package/test/fileConversationStorage.test.js +73 -0
- package/test/fixtures/codexHelperThreadLedger.js +39 -0
- package/test/localConversationExecution.test.js +90 -0
- package/test/messageDelivery.test.js +234 -1
- package/test/nativeConversationRetirement.test.js +215 -0
- package/test/openCodeClient.test.js +515 -0
- package/test/openCodeProcess.test.js +177 -0
- package/test/openCodeRuntime.test.js +322 -0
- package/test/openCodeTools.test.js +287 -0
- package/test/opencodeConversationSession.test.js +789 -0
- package/test/serviceToolCatalog.test.js +24 -0
|
@@ -0,0 +1,1559 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import WebSocket from "ws";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { chmod, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
5
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
6
|
+
import os from "node:os";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
9
|
+
import { CodexAppServerJsonRpcClient, socketPathFromCodexAppServerEndpoint } from "./codexClient.js";
|
|
10
|
+
import { createConversationProcessIdentity } from "./localExecution.js";
|
|
11
|
+
import { isPlainObject, normalizeText } from "./normalize.js";
|
|
12
|
+
import { prepareCodexModelCatalog } from "./codexConfiguration.js";
|
|
13
|
+
import { startCodexHistoryAdapter } from "./codexHistoryAdapter.js";
|
|
14
|
+
|
|
15
|
+
const CODEX_APP_SERVER_METADATA_FILE = "runtime.json";
|
|
16
|
+
const CODEX_APP_SERVER_LOCK_DIR = "runtime.lock";
|
|
17
|
+
const CODEX_APP_SERVER_LOCK_TIMEOUT_MS = 10000;
|
|
18
|
+
const CODEX_APP_SERVER_LOCK_STALE_MS = 120000;
|
|
19
|
+
const CODEX_APP_SERVER_EXECUTION_MODES = Object.freeze({ HELPER: "helper", INTERACTIVE: "interactive" });
|
|
20
|
+
const runtimeProcessIdentity = createConversationProcessIdentity();
|
|
21
|
+
|
|
22
|
+
// Run in the host-owned execution's leader, independently of subscribers.
|
|
23
|
+
export async function runCodexAppServerProcess({
|
|
24
|
+
runtimeDir, command, args = [], runtimeToken: token, bundled = false, additionalModels = []
|
|
25
|
+
}) {
|
|
26
|
+
const descriptor = path.join(runtimeDir, "history-adapter.json");
|
|
27
|
+
const startup = new AbortController();
|
|
28
|
+
const abortStartup = () => startup.abort();
|
|
29
|
+
process.once("SIGTERM", abortStartup);
|
|
30
|
+
process.once("SIGINT", abortStartup);
|
|
31
|
+
const catalogPath = await prepareCodexModelCatalog({
|
|
32
|
+
command,
|
|
33
|
+
runtimeDir,
|
|
34
|
+
bundled,
|
|
35
|
+
additionalModels,
|
|
36
|
+
signal: startup.signal
|
|
37
|
+
});
|
|
38
|
+
const adapter = await startCodexHistoryAdapter({ token, codexHome: process.env.CODEX_HOME || path.join(os.homedir(), ".codex") });
|
|
39
|
+
await writeFile(`${descriptor}.tmp`, JSON.stringify({ baseUrl: adapter.baseUrl, runtimeToken: token }), { mode: 0o600 });
|
|
40
|
+
await rename(`${descriptor}.tmp`, descriptor);
|
|
41
|
+
const child = spawn(command, [...args, "-c", `model_catalog_json=${JSON.stringify(catalogPath)}`], { stdio: "inherit" });
|
|
42
|
+
let stopping = false;
|
|
43
|
+
let forceExit;
|
|
44
|
+
function stop(signal = "SIGTERM") {
|
|
45
|
+
if (stopping) return;
|
|
46
|
+
stopping = true;
|
|
47
|
+
void adapter.close();
|
|
48
|
+
child.kill(signal);
|
|
49
|
+
forceExit = setTimeout(() => child.kill("SIGKILL"), 5000);
|
|
50
|
+
forceExit.unref();
|
|
51
|
+
}
|
|
52
|
+
process.on("SIGTERM", () => stop());
|
|
53
|
+
process.on("SIGINT", () => stop("SIGINT"));
|
|
54
|
+
process.removeListener("SIGTERM", abortStartup);
|
|
55
|
+
process.removeListener("SIGINT", abortStartup);
|
|
56
|
+
if (startup.signal.aborted) stop();
|
|
57
|
+
adapter.server.on("error", () => { process.exitCode = 1; stop(); });
|
|
58
|
+
const result = await new Promise((resolve) => {
|
|
59
|
+
child.once("error", () => resolve({ code: 1 }));
|
|
60
|
+
child.once("exit", (code, signal) => resolve({ code, signal }));
|
|
61
|
+
});
|
|
62
|
+
clearTimeout(forceExit);
|
|
63
|
+
await adapter.close();
|
|
64
|
+
await rm(descriptor, { force: true });
|
|
65
|
+
process.exitCode ||= result.code ?? (stopping ? 0 : 1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function ensurePrivateDirectory(dirPath = "") {
|
|
69
|
+
await mkdir(dirPath, {
|
|
70
|
+
mode: 0o700,
|
|
71
|
+
recursive: true
|
|
72
|
+
});
|
|
73
|
+
await chmod(dirPath, 0o700).catch(() => null);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function ensureWritablePrivateDirectory(dirPath = "") {
|
|
77
|
+
await ensurePrivateDirectory(dirPath);
|
|
78
|
+
const probePath = path.join(dirPath, `.jskit-write-check-${process.pid}-${randomUUID()}`);
|
|
79
|
+
try {
|
|
80
|
+
await writeFile(probePath, "", {
|
|
81
|
+
mode: 0o600
|
|
82
|
+
});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Codex app-server runtime directory is not writable: ${dirPath}. ${String(error?.message || error)}`
|
|
86
|
+
);
|
|
87
|
+
} finally {
|
|
88
|
+
await rm(probePath, {
|
|
89
|
+
force: true
|
|
90
|
+
}).catch(() => null);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function codexAppServerMetadataPath(runtimeDir = "") {
|
|
95
|
+
return path.join(runtimeDir, CODEX_APP_SERVER_METADATA_FILE);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function codexAppServerLockDir(runtimeDir = "") {
|
|
99
|
+
return path.join(runtimeDir, CODEX_APP_SERVER_LOCK_DIR);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function normalizeCodexAppServerMetadata(metadata = {}) {
|
|
103
|
+
const normalized = isPlainObject(metadata) ? metadata : {};
|
|
104
|
+
const endpoint = normalizeText(normalized.endpoint);
|
|
105
|
+
return {
|
|
106
|
+
accountIdentitySignature: normalizeText(normalized.accountIdentitySignature),
|
|
107
|
+
attachmentHostRoot: normalizeText(normalized.attachmentHostRoot),
|
|
108
|
+
authStateSignature: normalizeText(normalized.authStateSignature),
|
|
109
|
+
endpoint,
|
|
110
|
+
executionId: normalizeText(normalized.executionId),
|
|
111
|
+
executionMode: normalizeText(normalized.executionMode) || CODEX_APP_SERVER_EXECUTION_MODES.INTERACTIVE,
|
|
112
|
+
executionContextHash: normalizeText(normalized.executionContextHash),
|
|
113
|
+
healthz: normalizeText(normalized.healthz),
|
|
114
|
+
historyAdapterBaseUrl: normalizeText(normalized.historyAdapterBaseUrl),
|
|
115
|
+
logPath: normalizeText(normalized.logPath),
|
|
116
|
+
pid: Number.isSafeInteger(Number(normalized.pid)) ? Number(normalized.pid) : null,
|
|
117
|
+
processCwd: normalizeText(normalized.processCwd),
|
|
118
|
+
processExitVerifiedAt: normalizeText(normalized.processExitVerifiedAt),
|
|
119
|
+
processIdentity: runtimeProcessIdentity.normalize(normalized.processIdentity),
|
|
120
|
+
processState: normalizeText(normalized.processState),
|
|
121
|
+
provider: normalizeText(normalized.provider),
|
|
122
|
+
readyz: normalizeText(normalized.readyz),
|
|
123
|
+
runtimeDir: normalizeText(normalized.runtimeDir),
|
|
124
|
+
runtimesHash: normalizeText(normalized.runtimesHash),
|
|
125
|
+
schemaVersion: Number(normalized.schemaVersion || 0),
|
|
126
|
+
socketPath: normalizeText(normalized.socketPath),
|
|
127
|
+
startedAt: normalizeText(normalized.startedAt),
|
|
128
|
+
terminalEnvHash: normalizeText(normalized.terminalEnvHash),
|
|
129
|
+
toolHomeSource: normalizeText(normalized.toolHomeSource),
|
|
130
|
+
transport: normalizeText(normalized.transport)
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function readCodexAppServerMetadata(runtimeDir = "") {
|
|
135
|
+
try {
|
|
136
|
+
const metadata = JSON.parse(await readFile(codexAppServerMetadataPath(runtimeDir), "utf8"));
|
|
137
|
+
return normalizeCodexAppServerMetadata(metadata);
|
|
138
|
+
} catch {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function writeCodexAppServerMetadata(runtimeDir = "", metadata = {}) {
|
|
144
|
+
await ensureWritablePrivateDirectory(runtimeDir);
|
|
145
|
+
const metadataPath = codexAppServerMetadataPath(runtimeDir);
|
|
146
|
+
const tempPath = `${metadataPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
147
|
+
await writeFile(tempPath, `${JSON.stringify(metadata, null, 2)}\n`, {
|
|
148
|
+
mode: 0o600
|
|
149
|
+
});
|
|
150
|
+
await chmod(tempPath, 0o600).catch(() => null);
|
|
151
|
+
await rename(tempPath, metadataPath);
|
|
152
|
+
await chmod(metadataPath, 0o600).catch(() => null);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export async function fileExists(filePath = "") {
|
|
156
|
+
try {
|
|
157
|
+
const entry = await stat(filePath);
|
|
158
|
+
return entry.isSocket() || entry.isFile();
|
|
159
|
+
} catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function tailTextFile(filePath = "", maxBytes = 4096) {
|
|
165
|
+
try {
|
|
166
|
+
const text = await readFile(filePath, "utf8");
|
|
167
|
+
return text.slice(-maxBytes);
|
|
168
|
+
} catch {
|
|
169
|
+
return "";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function readLockOwner(lockDir = "") {
|
|
174
|
+
try {
|
|
175
|
+
return JSON.parse(await readFile(path.join(lockDir, "owner.json"), "utf8"));
|
|
176
|
+
} catch {
|
|
177
|
+
return {};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function lockIsStale(lockDir = "") {
|
|
182
|
+
const owner = await readLockOwner(lockDir);
|
|
183
|
+
if (owner.pid && runtimeProcessIdentity.isProcessAlive(owner.pid)) {
|
|
184
|
+
const createdAtMs = Date.parse(owner.createdAt || "");
|
|
185
|
+
return Number.isFinite(createdAtMs) && Date.now() - createdAtMs > CODEX_APP_SERVER_LOCK_STALE_MS;
|
|
186
|
+
}
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export async function acquireCodexRuntimeLock(runtimeDir = "", {
|
|
191
|
+
busyCode = "codex_runtime_busy",
|
|
192
|
+
timeoutMs = CODEX_APP_SERVER_LOCK_TIMEOUT_MS
|
|
193
|
+
} = {}) {
|
|
194
|
+
await ensureWritablePrivateDirectory(runtimeDir);
|
|
195
|
+
const lockDir = codexAppServerLockDir(runtimeDir);
|
|
196
|
+
const startedAt = Date.now();
|
|
197
|
+
while (Date.now() - startedAt <= timeoutMs) {
|
|
198
|
+
try {
|
|
199
|
+
await mkdir(lockDir, {
|
|
200
|
+
mode: 0o700
|
|
201
|
+
});
|
|
202
|
+
await writeFile(path.join(lockDir, "owner.json"), `${JSON.stringify({
|
|
203
|
+
createdAt: new Date().toISOString(),
|
|
204
|
+
pid: process.pid
|
|
205
|
+
})}\n`, {
|
|
206
|
+
mode: 0o600
|
|
207
|
+
});
|
|
208
|
+
return async () => {
|
|
209
|
+
await rm(lockDir, {
|
|
210
|
+
force: true,
|
|
211
|
+
recursive: true
|
|
212
|
+
});
|
|
213
|
+
};
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (error?.code !== "EEXIST") {
|
|
216
|
+
throw error;
|
|
217
|
+
}
|
|
218
|
+
if (await lockIsStale(lockDir)) {
|
|
219
|
+
await rm(lockDir, {
|
|
220
|
+
force: true,
|
|
221
|
+
recursive: true
|
|
222
|
+
});
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
await delay(100);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const error = new Error("Codex is still reconnecting: its shared runtime is busy starting or stopping. Please try again shortly.");
|
|
229
|
+
error.code = busyCode;
|
|
230
|
+
error.retryable = true;
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export const CODEX_APP_SERVER_METADATA_SCHEMA_VERSION = 19;
|
|
235
|
+
|
|
236
|
+
const CODEX_APP_SERVER_PROVIDER_ID = "codex_app_server";
|
|
237
|
+
|
|
238
|
+
export const CODEX_APP_SERVER_TRANSPORT = Object.freeze({
|
|
239
|
+
UNIX: "unix"
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
export const CODEX_APP_SERVER_RUNTIME_DIR_NAME = "codex-app-server";
|
|
243
|
+
|
|
244
|
+
const CODEX_APP_SERVER_LOG_FILE = "app-server.log";
|
|
245
|
+
|
|
246
|
+
const CODEX_APP_SERVER_SOCKET_FILE = "app-server.sock";
|
|
247
|
+
|
|
248
|
+
const CODEX_APP_SERVER_READY_TIMEOUT_MS = 60000;
|
|
249
|
+
|
|
250
|
+
const CODEX_APP_SERVER_LIVENESS_TIMEOUT_MS = 2000;
|
|
251
|
+
|
|
252
|
+
const CODEX_APP_SERVER_DESKTOP_BUS_ENV_NAMES = new Set([
|
|
253
|
+
"DBUS_SESSION_BUS_ADDRESS",
|
|
254
|
+
"DBUS_STARTER_ADDRESS",
|
|
255
|
+
"DBUS_STARTER_BUS_TYPE"
|
|
256
|
+
]);
|
|
257
|
+
|
|
258
|
+
const CODEX_APP_SERVER_HELPER_HOME_DIR = "codex-home";
|
|
259
|
+
|
|
260
|
+
const CODEX_APP_SERVER_HELPER_WORKSPACE_DIR = "workspace";
|
|
261
|
+
|
|
262
|
+
const CODEX_APP_SERVER_UNIX_SOCKET_PATH_MAX_BYTES = process.platform === "linux" ? 107 : 103;
|
|
263
|
+
|
|
264
|
+
const CODEX_APP_SERVER_ENDPOINT_STATUS = Object.freeze({
|
|
265
|
+
MISSING: "missing",
|
|
266
|
+
RESPONSIVE: "responsive",
|
|
267
|
+
TIMEOUT: "timeout",
|
|
268
|
+
UNREACHABLE: "unreachable"
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const CODEX_APP_SERVER_RUNTIME_STATUS = Object.freeze({
|
|
272
|
+
EXITED: "exited",
|
|
273
|
+
INCOMPATIBLE: "incompatible",
|
|
274
|
+
LIVE: "live",
|
|
275
|
+
MISSING: "missing",
|
|
276
|
+
SUSPECT: "suspect"
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
export const CODEX_APP_SERVER_PROCESS_STATE = Object.freeze({
|
|
280
|
+
RUNNING: "running",
|
|
281
|
+
STOPPED: "stopped"
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS = Object.freeze({
|
|
285
|
+
ABSENT: "absent",
|
|
286
|
+
AMBIGUOUS: "ambiguous",
|
|
287
|
+
EXACT: "exact",
|
|
288
|
+
INVALID: "invalid",
|
|
289
|
+
MISMATCH: "mismatch"
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
function normalizePositiveInteger(value, fallback) {
|
|
293
|
+
const number = Number(value);
|
|
294
|
+
return Number.isSafeInteger(number) && number > 0 ? number : fallback;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function stableHash(value) {
|
|
298
|
+
return createHash("sha256")
|
|
299
|
+
.update(String(value || ""))
|
|
300
|
+
.digest("hex")
|
|
301
|
+
.slice(0, 12);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function codexAppServerExecutionMode(options = {}) {
|
|
305
|
+
return normalizeText(options.executionMode) === CODEX_APP_SERVER_EXECUTION_MODES.HELPER
|
|
306
|
+
? CODEX_APP_SERVER_EXECUTION_MODES.HELPER
|
|
307
|
+
: CODEX_APP_SERVER_EXECUTION_MODES.INTERACTIVE;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function codexAppServerIsHelper(options = {}) {
|
|
311
|
+
return codexAppServerExecutionMode(options) === CODEX_APP_SERVER_EXECUTION_MODES.HELPER;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export async function assertExistingDirectory(dirPath = "", label = "directory") {
|
|
315
|
+
const normalizedPath = normalizeText(dirPath);
|
|
316
|
+
if (!normalizedPath) {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const stats = await stat(normalizedPath);
|
|
320
|
+
if (!stats.isDirectory()) {
|
|
321
|
+
throw new Error(`${label} is not a directory: ${normalizedPath}`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function codexAppServerHelperHomeDir(runtimeDir = "") {
|
|
326
|
+
return path.join(runtimeDir, CODEX_APP_SERVER_HELPER_HOME_DIR);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function codexAppServerHelperWorkspaceDir(runtimeDir = "") {
|
|
330
|
+
return path.join(runtimeDir, CODEX_APP_SERVER_HELPER_WORKSPACE_DIR);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function codexAppServerHelperCommandBaseEnv(env = process.env, codexHome = "") {
|
|
334
|
+
const source = isPlainObject(env) ? env : {};
|
|
335
|
+
const cleared = Object.fromEntries(Object.keys({
|
|
336
|
+
...process.env,
|
|
337
|
+
...source
|
|
338
|
+
}).map((name) => [name, ""]));
|
|
339
|
+
const allowedNames = [
|
|
340
|
+
"LANG",
|
|
341
|
+
"LC_ALL",
|
|
342
|
+
"LC_CTYPE",
|
|
343
|
+
"PATH",
|
|
344
|
+
"SSL_CERT_DIR",
|
|
345
|
+
"SSL_CERT_FILE",
|
|
346
|
+
"TERM",
|
|
347
|
+
"TMPDIR",
|
|
348
|
+
"TZ"
|
|
349
|
+
];
|
|
350
|
+
return {
|
|
351
|
+
...cleared,
|
|
352
|
+
...Object.fromEntries(allowedNames
|
|
353
|
+
.filter((name) => typeof source[name] === "string" && source[name])
|
|
354
|
+
.map((name) => [name, source[name]])),
|
|
355
|
+
CODEX_HOME: codexHome
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function codexAppServerLogPath(runtimeDir = "") {
|
|
360
|
+
return path.join(runtimeDir, CODEX_APP_SERVER_LOG_FILE);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function codexAppServerSocketPath(runtimeDir = "") {
|
|
364
|
+
return path.join(runtimeDir, CODEX_APP_SERVER_SOCKET_FILE);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function codexAppServerSocketPathBytes(socketPath = "") {
|
|
368
|
+
return Buffer.byteLength(String(socketPath || ""), "utf8");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function codexAppServerSocketPathTooLong(socketPath = "") {
|
|
372
|
+
return codexAppServerSocketPathBytes(socketPath) > CODEX_APP_SERVER_UNIX_SOCKET_PATH_MAX_BYTES;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function codexAppServerUnixEndpoint(socketPath = "") {
|
|
376
|
+
return `unix://${socketPath}`;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function codexAppServerProjectTrustOverride(workdir = "") {
|
|
380
|
+
const normalizedWorkdir = normalizeText(workdir);
|
|
381
|
+
if (!normalizedWorkdir) {
|
|
382
|
+
return "";
|
|
383
|
+
}
|
|
384
|
+
const projectRoot = path.resolve(normalizedWorkdir);
|
|
385
|
+
return `projects={${JSON.stringify(projectRoot)}={trust_level="trusted"}}`;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function codexAppServerProcessCwd({
|
|
389
|
+
executionRoot = "",
|
|
390
|
+
runtimeDir = "",
|
|
391
|
+
workdir = ""
|
|
392
|
+
} = {}) {
|
|
393
|
+
const normalizedWorkdir = normalizeText(workdir) ? path.resolve(workdir) : "";
|
|
394
|
+
if (normalizedWorkdir) {
|
|
395
|
+
return normalizedWorkdir;
|
|
396
|
+
}
|
|
397
|
+
const normalizedExecutionRoot = normalizeText(executionRoot) ? path.resolve(executionRoot) : "";
|
|
398
|
+
if (normalizedExecutionRoot) {
|
|
399
|
+
return normalizedExecutionRoot;
|
|
400
|
+
}
|
|
401
|
+
return normalizeText(runtimeDir) ? path.resolve(runtimeDir) : "";
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function codexAppServerRuntimeDirIsManaged(runtimeDir = "") {
|
|
405
|
+
const normalizedRuntimeDir = normalizeText(runtimeDir);
|
|
406
|
+
if (!normalizedRuntimeDir) {
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
const basename = path.basename(path.resolve(normalizedRuntimeDir));
|
|
410
|
+
return basename === CODEX_APP_SERVER_RUNTIME_DIR_NAME ||
|
|
411
|
+
basename.startsWith(`${CODEX_APP_SERVER_RUNTIME_DIR_NAME}-`);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
async function removeCodexAppServerRuntimeDir(runtimeDir = "") {
|
|
415
|
+
const normalizedRuntimeDir = normalizeText(runtimeDir);
|
|
416
|
+
if (!codexAppServerRuntimeDirIsManaged(normalizedRuntimeDir)) {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
await rm(normalizedRuntimeDir, {
|
|
420
|
+
force: true,
|
|
421
|
+
recursive: true
|
|
422
|
+
});
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function removeCodexAppServerMetadataTemps(runtimeDir = "") {
|
|
427
|
+
let names = [];
|
|
428
|
+
try {
|
|
429
|
+
names = await readdir(runtimeDir);
|
|
430
|
+
} catch {
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
await Promise.all(names
|
|
434
|
+
.filter((name) => name.startsWith(`${CODEX_APP_SERVER_METADATA_FILE}.`) && name.endsWith(".tmp"))
|
|
435
|
+
.map((name) => rm(path.join(runtimeDir, name), { force: true })));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function codexAppServerRuntimeCleanupCanSkip(error) {
|
|
439
|
+
return ["EACCES", "EPERM", "ENOENT"].includes(String(error?.code || ""));
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function codexAppServerRuntimeIdentity(runtime = {}) {
|
|
443
|
+
return [
|
|
444
|
+
normalizeText(runtime.accountIdentitySignature),
|
|
445
|
+
normalizeText(runtime.authStateSignature),
|
|
446
|
+
normalizeText(runtime.endpoint),
|
|
447
|
+
normalizeText(runtime.executionId),
|
|
448
|
+
normalizeText(runtime.executionMode),
|
|
449
|
+
normalizeText(runtime.runtimesHash),
|
|
450
|
+
normalizeText(runtime.terminalEnvHash),
|
|
451
|
+
normalizeText(runtime.socketPath),
|
|
452
|
+
normalizeText(runtime.startedAt),
|
|
453
|
+
normalizeText(runtime.pid)
|
|
454
|
+
].join("\0");
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export function normalizeCodexAppServerTerminalEnv(terminalEnv = {}) {
|
|
458
|
+
if (!isPlainObject(terminalEnv)) {
|
|
459
|
+
return {};
|
|
460
|
+
}
|
|
461
|
+
return Object.fromEntries(Object.entries(terminalEnv)
|
|
462
|
+
.map(([name, value]) => [
|
|
463
|
+
normalizeText(name),
|
|
464
|
+
String(value ?? "")
|
|
465
|
+
])
|
|
466
|
+
.filter(([name, value]) => (
|
|
467
|
+
name &&
|
|
468
|
+
String(value || "") &&
|
|
469
|
+
!CODEX_APP_SERVER_DESKTOP_BUS_ENV_NAMES.has(name)
|
|
470
|
+
)));
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function codexAppServerCommandBaseEnv({
|
|
474
|
+
env = process.env,
|
|
475
|
+
terminalEnv = {}
|
|
476
|
+
} = {}) {
|
|
477
|
+
const baseEnv = {
|
|
478
|
+
...env,
|
|
479
|
+
...normalizeCodexAppServerTerminalEnv(terminalEnv)
|
|
480
|
+
};
|
|
481
|
+
for (const name of CODEX_APP_SERVER_DESKTOP_BUS_ENV_NAMES) {
|
|
482
|
+
delete baseEnv[name];
|
|
483
|
+
}
|
|
484
|
+
return baseEnv;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
export function codexAppServerCredentialHome(toolHomeSource = "", baseEnv = {}) {
|
|
488
|
+
const home = normalizeText(toolHomeSource);
|
|
489
|
+
if (!home) {
|
|
490
|
+
return {};
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
home,
|
|
494
|
+
username: normalizeText(baseEnv.USER || baseEnv.LOGNAME)
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
export function codexAppServerTerminalEnvHash(terminalEnv = {}) {
|
|
499
|
+
return stableHash(JSON.stringify(Object.entries(normalizeCodexAppServerTerminalEnv(terminalEnv))
|
|
500
|
+
.sort(([left], [right]) => left.localeCompare(right))));
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function codexHistoryAdapterUrlIsValid(baseUrl, token) {
|
|
504
|
+
// The previous adapter cannot serve thread-bound compaction recovery. Keep
|
|
505
|
+
// its process identity readable, but retire it through normal owned cleanup.
|
|
506
|
+
const match = typeof baseUrl === "string" && /^http:\/\/127\.0\.0\.1:([1-9]\d{0,4})\/[0-9a-f-]{36}\/v2$/iu.exec(baseUrl);
|
|
507
|
+
return Boolean(match && Number(match[1]) <= 65535 && baseUrl.endsWith(`/${token}/v2`));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function codexAppServerLivenessTimeoutMs(options = {}) {
|
|
511
|
+
return normalizePositiveInteger(
|
|
512
|
+
options.livenessTimeoutMs,
|
|
513
|
+
normalizePositiveInteger(options.timeoutMs, CODEX_APP_SERVER_LIVENESS_TIMEOUT_MS)
|
|
514
|
+
);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
function codexAppServerProcessCommandHash({
|
|
518
|
+
codexArgs = [],
|
|
519
|
+
codexCommand = "",
|
|
520
|
+
executionMode = "",
|
|
521
|
+
processCwd = "",
|
|
522
|
+
runtimeDir = ""
|
|
523
|
+
} = {}) {
|
|
524
|
+
return stableHash(JSON.stringify({
|
|
525
|
+
codexArgs,
|
|
526
|
+
codexCommand: normalizeText(codexCommand),
|
|
527
|
+
executionMode: codexAppServerExecutionMode({ executionMode }),
|
|
528
|
+
processCwd: normalizeText(processCwd),
|
|
529
|
+
runtimeDir: normalizeText(runtimeDir)
|
|
530
|
+
}));
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const codexAppServerRuntimePreparations = new Map();
|
|
534
|
+
|
|
535
|
+
/** The production shared runtime: one process owner per stable runtime directory.
|
|
536
|
+
* Execution and credential facilities retain host policy; threads do not own this process.
|
|
537
|
+
*/
|
|
538
|
+
export function createCodexAppServerRuntime({ execution, credentials, configuration = {} }) {
|
|
539
|
+
const {
|
|
540
|
+
clientInfo = { name: "jskit", title: "JSKIT", version: "0.1.0" },
|
|
541
|
+
commandHookCommand,
|
|
542
|
+
errorPrefix = "",
|
|
543
|
+
modelCatalogEnvironmentName = "JSKIT_CODEX_MODEL_CATALOG_SOURCE",
|
|
544
|
+
processLabel = "jskit-codex-app-server",
|
|
545
|
+
processPath = fileURLToPath(new URL("./codexProcessLeader.js", import.meta.url)),
|
|
546
|
+
runtimeTokenEnvironmentName = "JSKIT_EXECUTION_RUNTIME_TOKEN",
|
|
547
|
+
commandHashEnvironmentName = "JSKIT_EXECUTION_COMMAND_HASH",
|
|
548
|
+
socketPathHelp = "Configure a shorter host runtime directory.",
|
|
549
|
+
umask = "0007"
|
|
550
|
+
} = configuration;
|
|
551
|
+
const CODEX_APP_SERVER_PROCESS_PATH = processPath;
|
|
552
|
+
const CODEX_APP_SERVER_PROCESS_RUNTIME_TOKEN_ENV = runtimeTokenEnvironmentName;
|
|
553
|
+
const CODEX_APP_SERVER_PROCESS_COMMAND_HASH_ENV = commandHashEnvironmentName;
|
|
554
|
+
const codexProcessIdentity = createConversationProcessIdentity({
|
|
555
|
+
runtimeTokenEnvironmentName, commandHashEnvironmentName,
|
|
556
|
+
label: "Codex app-server",
|
|
557
|
+
unavailableCode: `${errorPrefix}codex_app_server_process_identity_unavailable`
|
|
558
|
+
});
|
|
559
|
+
const codexAppServerProcessIdentityIsWellFormed = codexProcessIdentity.isValid;
|
|
560
|
+
const captureCodexAppServerProcessIdentity = codexProcessIdentity.capture;
|
|
561
|
+
const currentCodexAccountIdentitySignature = credentials.identity;
|
|
562
|
+
const currentCodexAuthStateSignature = credentials.generation;
|
|
563
|
+
const assertCodexAuthGenerationCurrent = credentials.assertCurrent;
|
|
564
|
+
|
|
565
|
+
function codexAppServerSessionCommandHookConfig() {
|
|
566
|
+
const command = commandHookCommand;
|
|
567
|
+
return `hooks.PreToolUse=[{matcher="^Bash$",hooks=[{type="command",command=${JSON.stringify(command)},timeout=30}]}]`;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function acquireRuntimeLock(runtimeDir = "", options = {}) {
|
|
571
|
+
await execution.prepare?.();
|
|
572
|
+
return acquireCodexRuntimeLock(runtimeDir, { ...options, busyCode: `${errorPrefix}codex_app_server_runtime_busy` });
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const CODEX_APP_SERVER_MANAGED_UMASK = umask;
|
|
576
|
+
|
|
577
|
+
const CODEX_APP_SERVER_MANAGED_SHELL = "/bin/sh";
|
|
578
|
+
|
|
579
|
+
const CODEX_APP_SERVER_MANAGED_STARTUP_SCRIPT = [
|
|
580
|
+
`umask ${CODEX_APP_SERVER_MANAGED_UMASK}`,
|
|
581
|
+
`unset ${[...CODEX_APP_SERVER_DESKTOP_BUS_ENV_NAMES].join(" ")}`,
|
|
582
|
+
'exec "$@"'
|
|
583
|
+
].join("\n");
|
|
584
|
+
|
|
585
|
+
const CODEX_APP_SERVER_HELPER_STARTUP_SCRIPT = [
|
|
586
|
+
`umask ${CODEX_APP_SERVER_MANAGED_UMASK}`,
|
|
587
|
+
"exec /usr/bin/env -i \\",
|
|
588
|
+
' HOME="$HOME" LOGNAME="$LOGNAME" USER="$USER" PATH="$PATH" \\',
|
|
589
|
+
' CODEX_HOME="$CODEX_HOME" LANG="$LANG" LC_ALL="$LC_ALL" LC_CTYPE="$LC_CTYPE" \\',
|
|
590
|
+
` ${CODEX_APP_SERVER_PROCESS_RUNTIME_TOKEN_ENV}="$${CODEX_APP_SERVER_PROCESS_RUNTIME_TOKEN_ENV}" ${CODEX_APP_SERVER_PROCESS_COMMAND_HASH_ENV}="$${CODEX_APP_SERVER_PROCESS_COMMAND_HASH_ENV}" \\`,
|
|
591
|
+
' SSL_CERT_DIR="$SSL_CERT_DIR" SSL_CERT_FILE="$SSL_CERT_FILE" TERM="$TERM" TMPDIR="$TMPDIR" TZ="$TZ" \\',
|
|
592
|
+
' "$@"'
|
|
593
|
+
].join("\n");
|
|
594
|
+
|
|
595
|
+
function codexAppServerProcessMetadataIsIdentifiable(metadata = {}, runtimeDir = "") {
|
|
596
|
+
const normalizedRuntimeDir = normalizeText(runtimeDir);
|
|
597
|
+
return Boolean(
|
|
598
|
+
Number(metadata.schemaVersion) === CODEX_APP_SERVER_METADATA_SCHEMA_VERSION &&
|
|
599
|
+
metadata.provider === CODEX_APP_SERVER_PROVIDER_ID &&
|
|
600
|
+
Number.isSafeInteger(Number(metadata.pid)) && Number(metadata.pid) > 0 &&
|
|
601
|
+
(!normalizedRuntimeDir || metadata.runtimeDir === normalizedRuntimeDir) &&
|
|
602
|
+
codexAppServerProcessIdentityIsWellFormed(metadata.processIdentity) &&
|
|
603
|
+
[
|
|
604
|
+
CODEX_APP_SERVER_PROCESS_STATE.RUNNING,
|
|
605
|
+
CODEX_APP_SERVER_PROCESS_STATE.STOPPED
|
|
606
|
+
].includes(metadata.processState)
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
async function inspectCodexAppServerProcessIdentity(metadata = {}, options = {}) {
|
|
611
|
+
if (typeof options.processIdentityInspector === "function") {
|
|
612
|
+
return codexProcessIdentity.normalizeInspection(await options.processIdentityInspector(metadata));
|
|
613
|
+
}
|
|
614
|
+
if (!codexAppServerProcessMetadataIsIdentifiable(metadata)) {
|
|
615
|
+
return { processGroupIds: [], status: CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS.INVALID };
|
|
616
|
+
}
|
|
617
|
+
return codexProcessIdentity.inspect(metadata);
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function stopCodexAppServerProcessGroup(metadata = {}, options = {}) {
|
|
621
|
+
if (!codexAppServerProcessMetadataIsIdentifiable(metadata)) {
|
|
622
|
+
return {
|
|
623
|
+
identityStatus: CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS.INVALID,
|
|
624
|
+
processExitVerified: false,
|
|
625
|
+
stopped: false
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
return codexProcessIdentity.stop(metadata, {
|
|
629
|
+
...options,
|
|
630
|
+
processIdentityInspector: (current) => inspectCodexAppServerProcessIdentity(current, options)
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function assertCodexAppServerSocketPathSupported(socketPath = "") {
|
|
635
|
+
if (!codexAppServerSocketPathTooLong(socketPath)) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
throw new Error(
|
|
639
|
+
`Codex app-server Unix socket path is too long for this OS: ${socketPath} ` +
|
|
640
|
+
`(${codexAppServerSocketPathBytes(socketPath)} bytes, max ${CODEX_APP_SERVER_UNIX_SOCKET_PATH_MAX_BYTES}). ` +
|
|
641
|
+
socketPathHelp
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
async function stopOwnedCodexAppServerExecution(metadata = {}, options = {}) {
|
|
646
|
+
if (
|
|
647
|
+
metadata.processState === CODEX_APP_SERVER_PROCESS_STATE.STOPPED &&
|
|
648
|
+
metadata.processExitVerifiedAt
|
|
649
|
+
) {
|
|
650
|
+
return stopCodexAppServerProcessGroup(metadata, options);
|
|
651
|
+
}
|
|
652
|
+
const executionId = normalizeText(metadata.executionId);
|
|
653
|
+
if (executionId) {
|
|
654
|
+
const stopExecution = typeof options.stopExecution === "function"
|
|
655
|
+
? options.stopExecution
|
|
656
|
+
: execution.stop;
|
|
657
|
+
let executionStop;
|
|
658
|
+
try {
|
|
659
|
+
executionStop = await stopExecution(executionId, {
|
|
660
|
+
allowMissingRecordScopeRecovery: true,
|
|
661
|
+
killTimeoutMs: options.killTimeoutMs,
|
|
662
|
+
reason: options.reason || "codex-app-server-stop",
|
|
663
|
+
termTimeoutMs: options.termTimeoutMs
|
|
664
|
+
});
|
|
665
|
+
} catch (error) {
|
|
666
|
+
return {
|
|
667
|
+
error: String(error?.message || error || "The owned execution could not be stopped."),
|
|
668
|
+
executionId,
|
|
669
|
+
processExitVerified: false,
|
|
670
|
+
scopeEmpty: false,
|
|
671
|
+
stopped: false
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
if (executionStop?.ok === true && executionStop?.scopeEmpty === true) {
|
|
675
|
+
return {
|
|
676
|
+
...executionStop,
|
|
677
|
+
executionId,
|
|
678
|
+
processExitVerified: true,
|
|
679
|
+
stopped: executionStop.stopped === true
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
if (typeof options.stopExecution === "function" || execution.authoritative) {
|
|
683
|
+
return {
|
|
684
|
+
...(executionStop && typeof executionStop === "object" ? executionStop : {}),
|
|
685
|
+
executionId,
|
|
686
|
+
processExitVerified: false,
|
|
687
|
+
scopeEmpty: false,
|
|
688
|
+
stopped: false
|
|
689
|
+
};
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
return stopCodexAppServerProcessGroup(metadata, options);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
async function stopCodexAppServerProcess(runtimeDir = "", options = {}) {
|
|
696
|
+
const normalizedRuntimeDir = normalizeText(runtimeDir);
|
|
697
|
+
if (!normalizedRuntimeDir) {
|
|
698
|
+
return {
|
|
699
|
+
processExitVerified: false,
|
|
700
|
+
stopped: false
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
const metadata = await readCodexAppServerMetadata(normalizedRuntimeDir);
|
|
704
|
+
if (!codexAppServerProcessMetadataIsIdentifiable(metadata, normalizedRuntimeDir)) {
|
|
705
|
+
return {
|
|
706
|
+
identityStatus: CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS.INVALID,
|
|
707
|
+
processExitVerified: false,
|
|
708
|
+
stopped: false
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
return stopOwnedCodexAppServerExecution(metadata, options);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async function cleanupFailedCodexAppServerStart(runtimeDir = "", {
|
|
715
|
+
helper = false,
|
|
716
|
+
executionId = "",
|
|
717
|
+
neverStarted = false,
|
|
718
|
+
pid = null,
|
|
719
|
+
processIdentity = null
|
|
720
|
+
} = {}, options = {}) {
|
|
721
|
+
let processStop = {
|
|
722
|
+
processExitVerified: neverStarted === true,
|
|
723
|
+
scopeEmpty: neverStarted === true,
|
|
724
|
+
stopped: false
|
|
725
|
+
};
|
|
726
|
+
let processCleanupFailed = false;
|
|
727
|
+
if (!neverStarted) {
|
|
728
|
+
try {
|
|
729
|
+
processStop = await stopOwnedCodexAppServerExecution({
|
|
730
|
+
executionId: normalizeText(executionId),
|
|
731
|
+
pid: Number(pid),
|
|
732
|
+
processIdentity,
|
|
733
|
+
processState: CODEX_APP_SERVER_PROCESS_STATE.RUNNING,
|
|
734
|
+
provider: CODEX_APP_SERVER_PROVIDER_ID,
|
|
735
|
+
runtimeDir: normalizeText(runtimeDir),
|
|
736
|
+
schemaVersion: CODEX_APP_SERVER_METADATA_SCHEMA_VERSION
|
|
737
|
+
}, options);
|
|
738
|
+
} catch {
|
|
739
|
+
processCleanupFailed = true;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
if (processCleanupFailed || processStop.processExitVerified !== true) {
|
|
743
|
+
return {
|
|
744
|
+
...processStop,
|
|
745
|
+
cleanupFailed: true
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
const helperRemovals = helper
|
|
749
|
+
? [
|
|
750
|
+
rm(codexAppServerHelperHomeDir(runtimeDir), { force: true, recursive: true }),
|
|
751
|
+
rm(codexAppServerHelperWorkspaceDir(runtimeDir), { force: true, recursive: true }),
|
|
752
|
+
rm(codexAppServerLogPath(runtimeDir), { force: true })
|
|
753
|
+
]
|
|
754
|
+
: [];
|
|
755
|
+
const removals = await Promise.allSettled([
|
|
756
|
+
...helperRemovals,
|
|
757
|
+
rm(codexAppServerSocketPath(runtimeDir), { force: true }),
|
|
758
|
+
rm(codexAppServerMetadataPath(runtimeDir), { force: true }),
|
|
759
|
+
removeCodexAppServerMetadataTemps(runtimeDir)
|
|
760
|
+
]);
|
|
761
|
+
return {
|
|
762
|
+
...processStop,
|
|
763
|
+
cleanupFailed: removals.some(({ status }) => status === "rejected")
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
async function stopCodexAppServerRuntime(options = {}) {
|
|
768
|
+
const runtimeDir = normalizeText(options.runtimeDir);
|
|
769
|
+
const preserveProcessExitProof = options.preserveProcessExitProof === true;
|
|
770
|
+
if (!runtimeDir || !codexAppServerRuntimeDirIsManaged(runtimeDir)) {
|
|
771
|
+
return {
|
|
772
|
+
processExitVerified: false,
|
|
773
|
+
runtimeDirPreserved: false,
|
|
774
|
+
runtimeDirRemoved: false,
|
|
775
|
+
stopped: false
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
let releaseLock;
|
|
779
|
+
try {
|
|
780
|
+
releaseLock = await acquireRuntimeLock(runtimeDir, options);
|
|
781
|
+
} catch (error) {
|
|
782
|
+
if (!codexAppServerRuntimeCleanupCanSkip(error)) {
|
|
783
|
+
throw error;
|
|
784
|
+
}
|
|
785
|
+
return {
|
|
786
|
+
processExitVerified: false,
|
|
787
|
+
runtimeDirCleanupError: String(error?.message || error || ""),
|
|
788
|
+
runtimeDirCleanupSkipped: String(error?.code || "") !== "ENOENT",
|
|
789
|
+
runtimeDirPreserved: false,
|
|
790
|
+
runtimeDirRemoved: false,
|
|
791
|
+
stopped: false
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
let runtimeDirRemoved = false;
|
|
795
|
+
let runtimeDirCleanupSkipped = false;
|
|
796
|
+
let runtimeDirCleanupError = "";
|
|
797
|
+
let runtimeDirPreserved = false;
|
|
798
|
+
let processStop = {
|
|
799
|
+
processExitVerified: false,
|
|
800
|
+
stopped: false
|
|
801
|
+
};
|
|
802
|
+
try {
|
|
803
|
+
const existing = await readCodexAppServerMetadata(runtimeDir);
|
|
804
|
+
const expectedAccount = normalizeText(options.expectedAccountIdentitySignature);
|
|
805
|
+
const ownedRuntime = options.ownedRuntime;
|
|
806
|
+
if (
|
|
807
|
+
ownedRuntime &&
|
|
808
|
+
codexAppServerProcessMetadataIsIdentifiable(ownedRuntime, runtimeDir) &&
|
|
809
|
+
(!expectedAccount || ownedRuntime.accountIdentitySignature === expectedAccount) &&
|
|
810
|
+
(!existing || codexAppServerRuntimeIdentity(existing) !== codexAppServerRuntimeIdentity(ownedRuntime))
|
|
811
|
+
) {
|
|
812
|
+
// Another owner may already have removed or replaced the shared runtime.
|
|
813
|
+
// Prove this provider's exact execution stopped without touching its successor.
|
|
814
|
+
return {
|
|
815
|
+
...await stopOwnedCodexAppServerExecution(ownedRuntime, options),
|
|
816
|
+
runtimeDirPreserved: Boolean(existing),
|
|
817
|
+
runtimeDirRemoved: false
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
if (!existing) {
|
|
821
|
+
// Changeover after a server restart may have no runtime.json left. The
|
|
822
|
+
// managed execution owner can still drain and prove its exact scope empty.
|
|
823
|
+
// A missing file alone is never process-exit proof, and account-specific
|
|
824
|
+
// cleanup must keep using its retained execution identity above.
|
|
825
|
+
const owned = options.verifyOwnerScope === true && !expectedAccount
|
|
826
|
+
? await execution.stopOwned({
|
|
827
|
+
kind: "assistant",
|
|
828
|
+
operationId: "codex-app-server",
|
|
829
|
+
ownerId: normalizeText(options.ownerId) || stableHash(runtimeDir)
|
|
830
|
+
}, { reason: "codex-app-server-changeover" })
|
|
831
|
+
: null;
|
|
832
|
+
const ownerExitVerified = owned?.supported === true && owned?.ok === true && owned?.scopeEmpty === true;
|
|
833
|
+
return {
|
|
834
|
+
processExitVerified: ownerExitVerified,
|
|
835
|
+
runtimeDirPreserved: false,
|
|
836
|
+
runtimeDirRemoved: false,
|
|
837
|
+
stopped: ownerExitVerified && Number(owned?.closed) > 0
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
if (expectedAccount) {
|
|
841
|
+
// A replacement runtime is installed only after the previous process has
|
|
842
|
+
// drained under this same lock. Never stop that replacement for stale
|
|
843
|
+
// ephemeral-thread cleanup.
|
|
844
|
+
if (!codexAppServerProcessMetadataIsIdentifiable(existing, runtimeDir) ||
|
|
845
|
+
!/^sha256:[a-f0-9]{64}$/u.test(existing.accountIdentitySignature)) {
|
|
846
|
+
return { processExitVerified: false, runtimeDirRemoved: false, stopped: false };
|
|
847
|
+
}
|
|
848
|
+
if (existing.accountIdentitySignature !== expectedAccount) {
|
|
849
|
+
return { ownershipSuperseded: true, processExitVerified: false, runtimeDirRemoved: false, stopped: false };
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
processStop = await stopCodexAppServerProcess(runtimeDir, options);
|
|
853
|
+
if (processStop.processExitVerified === true && preserveProcessExitProof) {
|
|
854
|
+
const metadata = await readCodexAppServerMetadata(runtimeDir);
|
|
855
|
+
if (!codexAppServerProcessMetadataIsIdentifiable(metadata, runtimeDir)) {
|
|
856
|
+
return {
|
|
857
|
+
...processStop,
|
|
858
|
+
processExitVerified: false,
|
|
859
|
+
runtimeDirPreserved: false,
|
|
860
|
+
runtimeDirRemoved: false
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
await writeCodexAppServerMetadata(runtimeDir, {
|
|
864
|
+
...metadata,
|
|
865
|
+
processExitVerifiedAt: metadata.processExitVerifiedAt || new Date().toISOString(),
|
|
866
|
+
processState: CODEX_APP_SERVER_PROCESS_STATE.STOPPED
|
|
867
|
+
});
|
|
868
|
+
runtimeDirPreserved = true;
|
|
869
|
+
} else if (processStop.processExitVerified === true) {
|
|
870
|
+
try {
|
|
871
|
+
runtimeDirRemoved = await removeCodexAppServerRuntimeDir(runtimeDir);
|
|
872
|
+
} catch (error) {
|
|
873
|
+
if (!codexAppServerRuntimeCleanupCanSkip(error)) {
|
|
874
|
+
throw error;
|
|
875
|
+
}
|
|
876
|
+
runtimeDirCleanupSkipped = true;
|
|
877
|
+
runtimeDirCleanupError = String(error?.message || error || "");
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
} finally {
|
|
881
|
+
await releaseLock();
|
|
882
|
+
}
|
|
883
|
+
return {
|
|
884
|
+
...processStop,
|
|
885
|
+
runtimeDirCleanupError,
|
|
886
|
+
runtimeDirCleanupSkipped,
|
|
887
|
+
runtimeDirPreserved,
|
|
888
|
+
runtimeDirRemoved
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
function codexAppServerMetadataIsWellFormed(metadata = {}, options = {}) {
|
|
893
|
+
const expectedAttachmentHostRoot = options.compatibility.attachmentHostRoot;
|
|
894
|
+
const expectedAccountIdentitySignature = normalizeText(options.accountIdentitySignature);
|
|
895
|
+
const expectedExecutionMode = codexAppServerExecutionMode(options);
|
|
896
|
+
const expectedToolHomeSource = codexAppServerIsHelper(options) ? "" : normalizeText(options.toolHomeSource);
|
|
897
|
+
const expectedTerminalEnvHash = codexAppServerTerminalEnvHash(codexAppServerIsHelper(options) ? {} : options.terminalEnv);
|
|
898
|
+
const expectedRuntimesHash = options.compatibility.runtimesHash;
|
|
899
|
+
const expectedExecutionContextHash = options.compatibility.executionContextHash;
|
|
900
|
+
const expectedHelperProcessCwd = expectedExecutionMode === CODEX_APP_SERVER_EXECUTION_MODES.HELPER
|
|
901
|
+
? codexAppServerHelperWorkspaceDir(metadata.runtimeDir)
|
|
902
|
+
: "";
|
|
903
|
+
return Boolean(
|
|
904
|
+
metadata.schemaVersion === CODEX_APP_SERVER_METADATA_SCHEMA_VERSION &&
|
|
905
|
+
metadata.accountIdentitySignature === expectedAccountIdentitySignature &&
|
|
906
|
+
metadata.attachmentHostRoot === expectedAttachmentHostRoot &&
|
|
907
|
+
metadata.authStateSignature &&
|
|
908
|
+
metadata.executionId &&
|
|
909
|
+
metadata.executionContextHash === expectedExecutionContextHash &&
|
|
910
|
+
metadata.executionMode === expectedExecutionMode &&
|
|
911
|
+
metadata.processCwd &&
|
|
912
|
+
codexAppServerProcessMetadataIsIdentifiable(metadata) &&
|
|
913
|
+
metadata.processState === CODEX_APP_SERVER_PROCESS_STATE.RUNNING &&
|
|
914
|
+
(expectedExecutionMode === CODEX_APP_SERVER_EXECUTION_MODES.HELPER ||
|
|
915
|
+
codexHistoryAdapterUrlIsValid(metadata.historyAdapterBaseUrl, metadata.processIdentity.runtimeToken)) &&
|
|
916
|
+
(!expectedHelperProcessCwd || metadata.processCwd === expectedHelperProcessCwd) &&
|
|
917
|
+
metadata.provider === CODEX_APP_SERVER_PROVIDER_ID &&
|
|
918
|
+
metadata.runtimesHash === expectedRuntimesHash &&
|
|
919
|
+
metadata.terminalEnvHash === expectedTerminalEnvHash &&
|
|
920
|
+
metadata.toolHomeSource === expectedToolHomeSource &&
|
|
921
|
+
metadata.transport === CODEX_APP_SERVER_TRANSPORT.UNIX &&
|
|
922
|
+
metadata.endpoint &&
|
|
923
|
+
metadata.socketPath
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
async function codexAppServerEndpointStatus(endpoint = "", {
|
|
928
|
+
timeoutMs = CODEX_APP_SERVER_LIVENESS_TIMEOUT_MS,
|
|
929
|
+
WebSocketImpl = WebSocket
|
|
930
|
+
} = {}) {
|
|
931
|
+
const normalizedEndpoint = normalizeText(endpoint);
|
|
932
|
+
const socketPath = socketPathFromCodexAppServerEndpoint(normalizedEndpoint);
|
|
933
|
+
if (!socketPath || !await fileExists(socketPath)) {
|
|
934
|
+
return {
|
|
935
|
+
status: CODEX_APP_SERVER_ENDPOINT_STATUS.MISSING
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
const normalizedTimeoutMs = normalizePositiveInteger(timeoutMs, CODEX_APP_SERVER_LIVENESS_TIMEOUT_MS);
|
|
939
|
+
const client = new CodexAppServerJsonRpcClient({
|
|
940
|
+
endpoint: normalizedEndpoint,
|
|
941
|
+
requestTimeoutMs: normalizedTimeoutMs,
|
|
942
|
+
WebSocketImpl
|
|
943
|
+
});
|
|
944
|
+
let timeout = null;
|
|
945
|
+
const probe = (async () => {
|
|
946
|
+
await client.connect();
|
|
947
|
+
await client.initialize({ clientInfo: clientInfo });
|
|
948
|
+
return CODEX_APP_SERVER_ENDPOINT_STATUS.RESPONSIVE;
|
|
949
|
+
})();
|
|
950
|
+
probe.catch(() => null);
|
|
951
|
+
try {
|
|
952
|
+
const status = await Promise.race([
|
|
953
|
+
probe,
|
|
954
|
+
new Promise((resolve) => {
|
|
955
|
+
timeout = setTimeout(() => resolve(CODEX_APP_SERVER_ENDPOINT_STATUS.TIMEOUT), normalizedTimeoutMs);
|
|
956
|
+
timeout.unref?.();
|
|
957
|
+
})
|
|
958
|
+
]);
|
|
959
|
+
return {
|
|
960
|
+
status
|
|
961
|
+
};
|
|
962
|
+
} catch {
|
|
963
|
+
return {
|
|
964
|
+
status: CODEX_APP_SERVER_ENDPOINT_STATUS.UNREACHABLE
|
|
965
|
+
};
|
|
966
|
+
} finally {
|
|
967
|
+
clearTimeout(timeout);
|
|
968
|
+
client.close();
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
async function codexAppServerMetadataIsLive(metadata = {}, options = {}) {
|
|
973
|
+
return (await codexAppServerRuntimeStatus(metadata, options)).status === CODEX_APP_SERVER_RUNTIME_STATUS.LIVE;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
async function codexAppServerRuntimeStatus(metadata = {}, options = {}) {
|
|
977
|
+
const normalized = normalizeCodexAppServerMetadata(metadata);
|
|
978
|
+
if (
|
|
979
|
+
codexAppServerProcessMetadataIsIdentifiable(normalized) &&
|
|
980
|
+
normalized.processState === CODEX_APP_SERVER_PROCESS_STATE.STOPPED &&
|
|
981
|
+
normalized.processExitVerifiedAt
|
|
982
|
+
) {
|
|
983
|
+
return {
|
|
984
|
+
metadata: normalized,
|
|
985
|
+
replace: true,
|
|
986
|
+
reusable: false,
|
|
987
|
+
status: CODEX_APP_SERVER_RUNTIME_STATUS.EXITED
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
if (!codexAppServerMetadataIsWellFormed(normalized, options)) {
|
|
991
|
+
return {
|
|
992
|
+
metadata: normalized,
|
|
993
|
+
replace: true,
|
|
994
|
+
reusable: false,
|
|
995
|
+
status: CODEX_APP_SERVER_RUNTIME_STATUS.INCOMPATIBLE
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
const authStateSignature = await currentCodexAuthStateSignature(options);
|
|
999
|
+
if (normalized.authStateSignature !== authStateSignature) {
|
|
1000
|
+
return {
|
|
1001
|
+
metadata: normalized,
|
|
1002
|
+
replace: true,
|
|
1003
|
+
reusable: false,
|
|
1004
|
+
status: CODEX_APP_SERVER_RUNTIME_STATUS.INCOMPATIBLE
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
let processIdentity = null;
|
|
1008
|
+
if (typeof options.processGroupIsAlive === "function") {
|
|
1009
|
+
const alive = options.processGroupIsAlive(normalized.pid);
|
|
1010
|
+
processIdentity = {
|
|
1011
|
+
processGroupIds: alive ? [normalized.pid] : [],
|
|
1012
|
+
status: alive
|
|
1013
|
+
? CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS.EXACT
|
|
1014
|
+
: CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS.ABSENT
|
|
1015
|
+
};
|
|
1016
|
+
} else {
|
|
1017
|
+
processIdentity = await inspectCodexAppServerProcessIdentity(normalized, options);
|
|
1018
|
+
}
|
|
1019
|
+
if (
|
|
1020
|
+
processIdentity.status === CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS.ABSENT ||
|
|
1021
|
+
processIdentity.status === CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS.MISMATCH
|
|
1022
|
+
) {
|
|
1023
|
+
return {
|
|
1024
|
+
metadata: normalized,
|
|
1025
|
+
replace: true,
|
|
1026
|
+
reusable: false,
|
|
1027
|
+
status: CODEX_APP_SERVER_RUNTIME_STATUS.EXITED
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
if (processIdentity.status !== CODEX_APP_SERVER_PROCESS_IDENTITY_STATUS.EXACT) {
|
|
1031
|
+
return {
|
|
1032
|
+
metadata: normalized,
|
|
1033
|
+
replace: false,
|
|
1034
|
+
reusable: true,
|
|
1035
|
+
status: CODEX_APP_SERVER_RUNTIME_STATUS.SUSPECT
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
const endpoint = await codexAppServerEndpointStatus(normalized.endpoint, {
|
|
1039
|
+
timeoutMs: codexAppServerLivenessTimeoutMs(options),
|
|
1040
|
+
WebSocketImpl: options.WebSocketImpl
|
|
1041
|
+
});
|
|
1042
|
+
if (endpoint.status === CODEX_APP_SERVER_ENDPOINT_STATUS.RESPONSIVE) {
|
|
1043
|
+
return {
|
|
1044
|
+
metadata: normalized,
|
|
1045
|
+
replace: false,
|
|
1046
|
+
reusable: true,
|
|
1047
|
+
status: CODEX_APP_SERVER_RUNTIME_STATUS.LIVE
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
1050
|
+
if (endpoint.status === CODEX_APP_SERVER_ENDPOINT_STATUS.MISSING) {
|
|
1051
|
+
return {
|
|
1052
|
+
metadata: normalized,
|
|
1053
|
+
replace: true,
|
|
1054
|
+
reusable: false,
|
|
1055
|
+
status: CODEX_APP_SERVER_RUNTIME_STATUS.MISSING
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
if (endpoint.status === CODEX_APP_SERVER_ENDPOINT_STATUS.TIMEOUT) {
|
|
1059
|
+
return {
|
|
1060
|
+
metadata: normalized,
|
|
1061
|
+
replace: false,
|
|
1062
|
+
reusable: true,
|
|
1063
|
+
status: CODEX_APP_SERVER_RUNTIME_STATUS.SUSPECT
|
|
1064
|
+
};
|
|
1065
|
+
}
|
|
1066
|
+
return {
|
|
1067
|
+
metadata: normalized,
|
|
1068
|
+
replace: true,
|
|
1069
|
+
reusable: false,
|
|
1070
|
+
status: CODEX_APP_SERVER_RUNTIME_STATUS.MISSING
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
async function waitForCodexAppServer(endpoint = "", {
|
|
1075
|
+
timeoutMs = CODEX_APP_SERVER_READY_TIMEOUT_MS,
|
|
1076
|
+
WebSocketImpl = WebSocket
|
|
1077
|
+
} = {}) {
|
|
1078
|
+
const socketPath = socketPathFromCodexAppServerEndpoint(endpoint);
|
|
1079
|
+
if (!socketPath) {
|
|
1080
|
+
return false;
|
|
1081
|
+
}
|
|
1082
|
+
const startedAt = Date.now();
|
|
1083
|
+
while (!await fileExists(socketPath) && Date.now() - startedAt <= timeoutMs) {
|
|
1084
|
+
await delay(100);
|
|
1085
|
+
}
|
|
1086
|
+
const remainingMs = Math.max(1, timeoutMs - (Date.now() - startedAt));
|
|
1087
|
+
if (!await fileExists(socketPath) || remainingMs <= 0) {
|
|
1088
|
+
return false;
|
|
1089
|
+
}
|
|
1090
|
+
const client = new CodexAppServerJsonRpcClient({
|
|
1091
|
+
endpoint,
|
|
1092
|
+
requestTimeoutMs: remainingMs,
|
|
1093
|
+
WebSocketImpl
|
|
1094
|
+
});
|
|
1095
|
+
let timeout = null;
|
|
1096
|
+
const handshake = (async () => {
|
|
1097
|
+
await client.connect();
|
|
1098
|
+
await client.initialize({ clientInfo: clientInfo });
|
|
1099
|
+
return true;
|
|
1100
|
+
})();
|
|
1101
|
+
handshake.catch(() => null);
|
|
1102
|
+
try {
|
|
1103
|
+
return await Promise.race([
|
|
1104
|
+
handshake,
|
|
1105
|
+
new Promise((resolve) => {
|
|
1106
|
+
timeout = setTimeout(() => resolve(false), remainingMs);
|
|
1107
|
+
timeout.unref?.();
|
|
1108
|
+
})
|
|
1109
|
+
]);
|
|
1110
|
+
} catch {
|
|
1111
|
+
return false;
|
|
1112
|
+
} finally {
|
|
1113
|
+
clearTimeout(timeout);
|
|
1114
|
+
client.close();
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
async function startCodexAppServerProcess({
|
|
1119
|
+
providerHome = false,
|
|
1120
|
+
accountIdentitySignature = "",
|
|
1121
|
+
authStateSignature = "",
|
|
1122
|
+
codexCommand = "codex",
|
|
1123
|
+
env = process.env,
|
|
1124
|
+
executionRoot = "",
|
|
1125
|
+
executionMode = CODEX_APP_SERVER_EXECUTION_MODES.INTERACTIVE,
|
|
1126
|
+
killTimeoutMs = 0,
|
|
1127
|
+
processIdentityInspector = null,
|
|
1128
|
+
readyTimeoutMs = CODEX_APP_SERVER_READY_TIMEOUT_MS,
|
|
1129
|
+
signalProcessGroup: signalProcessGroupOverride = null,
|
|
1130
|
+
stopExecution = null,
|
|
1131
|
+
terminalEnv = {},
|
|
1132
|
+
termTimeoutMs = 0,
|
|
1133
|
+
toolHomeSource = "",
|
|
1134
|
+
WebSocketImpl = WebSocket,
|
|
1135
|
+
workdir = "",
|
|
1136
|
+
socketOwnerDrained = false,
|
|
1137
|
+
runtimeDir,
|
|
1138
|
+
ownerId = "",
|
|
1139
|
+
compatibility
|
|
1140
|
+
} = {}) {
|
|
1141
|
+
const processLifecycleOptions = {
|
|
1142
|
+
killTimeoutMs,
|
|
1143
|
+
processIdentityInspector,
|
|
1144
|
+
signalProcessGroup: signalProcessGroupOverride,
|
|
1145
|
+
stopExecution,
|
|
1146
|
+
termTimeoutMs
|
|
1147
|
+
};
|
|
1148
|
+
await ensureWritablePrivateDirectory(runtimeDir);
|
|
1149
|
+
const helper = codexAppServerExecutionMode({ executionMode }) ===
|
|
1150
|
+
CODEX_APP_SERVER_EXECUTION_MODES.HELPER;
|
|
1151
|
+
const normalizedToolHomeSource = normalizeText(toolHomeSource);
|
|
1152
|
+
if (normalizedToolHomeSource) {
|
|
1153
|
+
await assertExistingDirectory(normalizedToolHomeSource, "Codex credential home");
|
|
1154
|
+
}
|
|
1155
|
+
const resolvedAuthStateSignature = await currentCodexAuthStateSignature({ authStateSignature });
|
|
1156
|
+
await assertCodexAuthGenerationCurrent(resolvedAuthStateSignature);
|
|
1157
|
+
const socketPath = codexAppServerSocketPath(runtimeDir);
|
|
1158
|
+
assertCodexAppServerSocketPathSupported(socketPath);
|
|
1159
|
+
const endpoint = codexAppServerUnixEndpoint(socketPath);
|
|
1160
|
+
const logPath = codexAppServerLogPath(runtimeDir);
|
|
1161
|
+
const helperHome = helper ? codexAppServerHelperHomeDir(runtimeDir) : "";
|
|
1162
|
+
const helperWorkspace = helper ? codexAppServerHelperWorkspaceDir(runtimeDir) : "";
|
|
1163
|
+
if (helper) {
|
|
1164
|
+
await Promise.all([
|
|
1165
|
+
rm(helperHome, { force: true, recursive: true }),
|
|
1166
|
+
rm(helperWorkspace, { force: true, recursive: true })
|
|
1167
|
+
]);
|
|
1168
|
+
await Promise.all([
|
|
1169
|
+
ensureWritablePrivateDirectory(helperHome),
|
|
1170
|
+
ensureWritablePrivateDirectory(helperWorkspace)
|
|
1171
|
+
]);
|
|
1172
|
+
if (providerHome) {
|
|
1173
|
+
// The canonical home contains only our curated provider projection. Copy
|
|
1174
|
+
// it into the existing isolated helper home; never load OpenAI auth here.
|
|
1175
|
+
const sourceHome = path.join(normalizedToolHomeSource, ".codex");
|
|
1176
|
+
const catalogPath = path.join(helperHome, "models.json");
|
|
1177
|
+
const config = await readFile(path.join(sourceHome, "config.toml"), "utf8");
|
|
1178
|
+
await writeFile(catalogPath, await readFile(path.join(sourceHome, "models.json")), { mode: 0o600 });
|
|
1179
|
+
await writeFile(path.join(helperHome, "config.toml"), config.replace(
|
|
1180
|
+
JSON.stringify(path.join(sourceHome, "models.json")), JSON.stringify(catalogPath)
|
|
1181
|
+
), { mode: 0o600 });
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
const processCwd = helper
|
|
1185
|
+
? helperWorkspace
|
|
1186
|
+
: codexAppServerProcessCwd({
|
|
1187
|
+
executionRoot,
|
|
1188
|
+
runtimeDir,
|
|
1189
|
+
workdir
|
|
1190
|
+
});
|
|
1191
|
+
const projectTrustOverride = helper ? "" : codexAppServerProjectTrustOverride(workdir);
|
|
1192
|
+
const normalizedTerminalEnv = normalizeCodexAppServerTerminalEnv(helper ? {} : terminalEnv);
|
|
1193
|
+
const commandBaseEnv = helper
|
|
1194
|
+
? codexAppServerHelperCommandBaseEnv(env, helperHome)
|
|
1195
|
+
: codexAppServerCommandBaseEnv({
|
|
1196
|
+
env,
|
|
1197
|
+
terminalEnv: normalizedTerminalEnv
|
|
1198
|
+
});
|
|
1199
|
+
if (await fileExists(socketPath) && !socketOwnerDrained) {
|
|
1200
|
+
const endpointStatus = await codexAppServerEndpointStatus(endpoint, {
|
|
1201
|
+
timeoutMs: CODEX_APP_SERVER_LIVENESS_TIMEOUT_MS,
|
|
1202
|
+
WebSocketImpl
|
|
1203
|
+
});
|
|
1204
|
+
if (![CODEX_APP_SERVER_ENDPOINT_STATUS.MISSING, CODEX_APP_SERVER_ENDPOINT_STATUS.UNREACHABLE]
|
|
1205
|
+
.includes(endpointStatus.status)) {
|
|
1206
|
+
const error = new Error("The existing Codex app-server socket still has an unretired owner.");
|
|
1207
|
+
error.code = `${errorPrefix}codex_app_server_socket_owner_unverified`;
|
|
1208
|
+
error.retryable = false;
|
|
1209
|
+
throw error;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
await rm(socketPath, {
|
|
1213
|
+
force: true
|
|
1214
|
+
});
|
|
1215
|
+
const codexArgs = [
|
|
1216
|
+
// Keep every override after app-server: a trailing -c (including the model
|
|
1217
|
+
// catalogue) otherwise drops the earlier ones. See openai/codex#39012.
|
|
1218
|
+
"app-server",
|
|
1219
|
+
"--listen",
|
|
1220
|
+
endpoint,
|
|
1221
|
+
// App-server uses config overrides, not the top-level sandbox bypass flag.
|
|
1222
|
+
...(helper ? [] : [
|
|
1223
|
+
"-c",
|
|
1224
|
+
'approval_policy="never"',
|
|
1225
|
+
"-c",
|
|
1226
|
+
'sandbox_mode="danger-full-access"',
|
|
1227
|
+
"-c",
|
|
1228
|
+
`features.hooks=${Boolean(commandHookCommand)}`,
|
|
1229
|
+
...(commandHookCommand ? ["-c", codexAppServerSessionCommandHookConfig()] : [])
|
|
1230
|
+
]),
|
|
1231
|
+
"-c",
|
|
1232
|
+
"check_for_update_on_startup=false",
|
|
1233
|
+
...(projectTrustOverride
|
|
1234
|
+
? [
|
|
1235
|
+
"-c",
|
|
1236
|
+
projectTrustOverride
|
|
1237
|
+
]
|
|
1238
|
+
: [])
|
|
1239
|
+
];
|
|
1240
|
+
const runtimeToken = randomUUID();
|
|
1241
|
+
const commandHash = codexAppServerProcessCommandHash({
|
|
1242
|
+
codexArgs,
|
|
1243
|
+
codexCommand,
|
|
1244
|
+
executionMode,
|
|
1245
|
+
processCwd,
|
|
1246
|
+
runtimeDir
|
|
1247
|
+
});
|
|
1248
|
+
const baseEnv = {
|
|
1249
|
+
...commandBaseEnv,
|
|
1250
|
+
// A curated provider home contains a provider-only static catalogue. Use
|
|
1251
|
+
// the binary's native definitions as the base for its shared runtime.
|
|
1252
|
+
[modelCatalogEnvironmentName]: providerHome ? "bundled" : "native",
|
|
1253
|
+
[CODEX_APP_SERVER_PROCESS_COMMAND_HASH_ENV]: commandHash,
|
|
1254
|
+
[CODEX_APP_SERVER_PROCESS_RUNTIME_TOKEN_ENV]: runtimeToken
|
|
1255
|
+
};
|
|
1256
|
+
const profileBinary = path.isAbsolute(codexCommand) ? await stat(codexCommand).catch(() => null) : null;
|
|
1257
|
+
const startResult = await execution.run({
|
|
1258
|
+
args: [
|
|
1259
|
+
"-c",
|
|
1260
|
+
helper ? CODEX_APP_SERVER_HELPER_STARTUP_SCRIPT : CODEX_APP_SERVER_MANAGED_STARTUP_SCRIPT,
|
|
1261
|
+
processLabel,
|
|
1262
|
+
...(helper ? [] : [process.execPath, CODEX_APP_SERVER_PROCESS_PATH, runtimeDir]),
|
|
1263
|
+
codexCommand,
|
|
1264
|
+
...codexArgs
|
|
1265
|
+
],
|
|
1266
|
+
baseEnv,
|
|
1267
|
+
command: CODEX_APP_SERVER_MANAGED_SHELL,
|
|
1268
|
+
credentialHome: codexAppServerCredentialHome(
|
|
1269
|
+
helper ? helperHome : normalizedToolHomeSource,
|
|
1270
|
+
baseEnv
|
|
1271
|
+
),
|
|
1272
|
+
cwd: processCwd || process.cwd(),
|
|
1273
|
+
execution: {
|
|
1274
|
+
kind: "assistant",
|
|
1275
|
+
label: "Codex assistant",
|
|
1276
|
+
lifecycle: "service",
|
|
1277
|
+
operationId: "codex-app-server",
|
|
1278
|
+
ownerId: normalizeText(ownerId) || stableHash(runtimeDir)
|
|
1279
|
+
},
|
|
1280
|
+
processProfile: profileBinary?.isFile() ? {
|
|
1281
|
+
command: codexCommand, helper, executionMode,
|
|
1282
|
+
binary: [profileBinary.dev, profileBinary.ino, profileBinary.size, profileBinary.mtimeMs, profileBinary.ctimeMs],
|
|
1283
|
+
platform: process.platform, architecture: process.arch,
|
|
1284
|
+
startup: helper ? CODEX_APP_SERVER_HELPER_STARTUP_SCRIPT : CODEX_APP_SERVER_MANAGED_STARTUP_SCRIPT,
|
|
1285
|
+
historyAdapter: !helper
|
|
1286
|
+
} : null,
|
|
1287
|
+
logPath,
|
|
1288
|
+
inheritProcessEnv: false,
|
|
1289
|
+
mode: "detached",
|
|
1290
|
+
timeout: readyTimeoutMs
|
|
1291
|
+
});
|
|
1292
|
+
if (!startResult.ok) {
|
|
1293
|
+
const failedProcessIdentity = await captureCodexAppServerProcessIdentity({
|
|
1294
|
+
commandHash,
|
|
1295
|
+
pid: startResult.pid,
|
|
1296
|
+
reportedIdentity: startResult.processIdentity,
|
|
1297
|
+
runtimeToken
|
|
1298
|
+
}).catch(() => null);
|
|
1299
|
+
const cleanup = await cleanupFailedCodexAppServerStart(runtimeDir, {
|
|
1300
|
+
helper,
|
|
1301
|
+
executionId: startResult.execution?.id,
|
|
1302
|
+
neverStarted: !Number.isSafeInteger(Number(startResult.pid)),
|
|
1303
|
+
pid: startResult.pid,
|
|
1304
|
+
processIdentity: failedProcessIdentity
|
|
1305
|
+
}, processLifecycleOptions);
|
|
1306
|
+
const error = new Error(
|
|
1307
|
+
startResult.output || startResult.error ||
|
|
1308
|
+
(helper ? "Codex isolated helper runtime failed to start." : "Codex app-server failed to start.")
|
|
1309
|
+
);
|
|
1310
|
+
error.code = cleanup.cleanupFailed
|
|
1311
|
+
? (helper
|
|
1312
|
+
? `${errorPrefix}codex_helper_runtime_cleanup_required`
|
|
1313
|
+
: `${errorPrefix}codex_app_server_cleanup_required`)
|
|
1314
|
+
: (startResult.code || (helper
|
|
1315
|
+
? `${errorPrefix}codex_helper_runtime_start_failed`
|
|
1316
|
+
: `${errorPrefix}codex_app_server_start_failed`));
|
|
1317
|
+
error.cleanupRequired = cleanup.cleanupFailed;
|
|
1318
|
+
error.execution = startResult.execution;
|
|
1319
|
+
error.retryable = cleanup.cleanupFailed ? false : startResult.retryable === true;
|
|
1320
|
+
throw error;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
let processIdentity;
|
|
1324
|
+
try {
|
|
1325
|
+
processIdentity = await captureCodexAppServerProcessIdentity({
|
|
1326
|
+
commandHash,
|
|
1327
|
+
pid: startResult.pid,
|
|
1328
|
+
reportedIdentity: startResult.processIdentity,
|
|
1329
|
+
runtimeToken
|
|
1330
|
+
});
|
|
1331
|
+
} catch (cause) {
|
|
1332
|
+
const cleanup = await cleanupFailedCodexAppServerStart(runtimeDir, {
|
|
1333
|
+
helper,
|
|
1334
|
+
executionId: startResult.execution?.id,
|
|
1335
|
+
pid: startResult.pid,
|
|
1336
|
+
processIdentity: startResult.processIdentity
|
|
1337
|
+
}, processLifecycleOptions);
|
|
1338
|
+
const error = new Error("Codex app-server process ownership could not be recorded.", {
|
|
1339
|
+
cause
|
|
1340
|
+
});
|
|
1341
|
+
error.code = cleanup.cleanupFailed
|
|
1342
|
+
? `${errorPrefix}codex_app_server_cleanup_required`
|
|
1343
|
+
: `${errorPrefix}codex_app_server_process_identity_unavailable`;
|
|
1344
|
+
error.cleanupRequired = cleanup.cleanupFailed;
|
|
1345
|
+
throw error;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
const ready = await waitForCodexAppServer(endpoint, {
|
|
1349
|
+
timeoutMs: readyTimeoutMs,
|
|
1350
|
+
WebSocketImpl
|
|
1351
|
+
});
|
|
1352
|
+
if (!ready) {
|
|
1353
|
+
const logTail = await tailTextFile(logPath);
|
|
1354
|
+
const cleanup = await cleanupFailedCodexAppServerStart(runtimeDir, {
|
|
1355
|
+
helper,
|
|
1356
|
+
executionId: startResult.execution?.id,
|
|
1357
|
+
pid: startResult.pid,
|
|
1358
|
+
processIdentity
|
|
1359
|
+
}, processLifecycleOptions);
|
|
1360
|
+
if (!cleanup.cleanupFailed && !helper) {
|
|
1361
|
+
await credentials.observeFailure(logTail);
|
|
1362
|
+
}
|
|
1363
|
+
const error = new Error([
|
|
1364
|
+
helper
|
|
1365
|
+
? "Codex isolated helper runtime did not become ready."
|
|
1366
|
+
: `Codex app-server did not become ready at ${endpoint}.`,
|
|
1367
|
+
logTail ? `Recent log output:\n${logTail}` : ""
|
|
1368
|
+
].filter(Boolean).join("\n"));
|
|
1369
|
+
error.code = cleanup.cleanupFailed
|
|
1370
|
+
? (helper
|
|
1371
|
+
? `${errorPrefix}codex_helper_runtime_cleanup_required`
|
|
1372
|
+
: `${errorPrefix}codex_app_server_cleanup_required`)
|
|
1373
|
+
: (helper
|
|
1374
|
+
? `${errorPrefix}codex_helper_runtime_start_failed`
|
|
1375
|
+
: `${errorPrefix}codex_app_server_ready_timeout`);
|
|
1376
|
+
error.cleanupRequired = cleanup.cleanupFailed;
|
|
1377
|
+
error.retryable = false;
|
|
1378
|
+
throw error;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
let historyAdapterBaseUrl = "";
|
|
1382
|
+
try {
|
|
1383
|
+
if (!helper) {
|
|
1384
|
+
const descriptor = JSON.parse(await readFile(path.join(runtimeDir, "history-adapter.json"), "utf8"));
|
|
1385
|
+
if (descriptor.runtimeToken !== runtimeToken || !codexHistoryAdapterUrlIsValid(descriptor.baseUrl, runtimeToken)) {
|
|
1386
|
+
throw new Error("Codex history adapter did not confirm the current runtime.");
|
|
1387
|
+
}
|
|
1388
|
+
historyAdapterBaseUrl = descriptor.baseUrl;
|
|
1389
|
+
}
|
|
1390
|
+
await assertCodexAuthGenerationCurrent(resolvedAuthStateSignature);
|
|
1391
|
+
} catch (cause) {
|
|
1392
|
+
const cleanup = await cleanupFailedCodexAppServerStart(runtimeDir, {
|
|
1393
|
+
helper,
|
|
1394
|
+
executionId: startResult.execution?.id,
|
|
1395
|
+
pid: startResult.pid,
|
|
1396
|
+
processIdentity
|
|
1397
|
+
}, processLifecycleOptions);
|
|
1398
|
+
if (cleanup.cleanupFailed) {
|
|
1399
|
+
const error = new Error("Codex app-server authentication changed and its execution could not be drained.", {
|
|
1400
|
+
cause
|
|
1401
|
+
});
|
|
1402
|
+
error.code = `${errorPrefix}codex_app_server_cleanup_required`;
|
|
1403
|
+
error.cleanupRequired = true;
|
|
1404
|
+
error.retryable = false;
|
|
1405
|
+
throw error;
|
|
1406
|
+
}
|
|
1407
|
+
throw cause;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
return {
|
|
1411
|
+
accountIdentitySignature: normalizeText(accountIdentitySignature),
|
|
1412
|
+
attachmentHostRoot: compatibility.attachmentHostRoot,
|
|
1413
|
+
authStateSignature: resolvedAuthStateSignature,
|
|
1414
|
+
endpoint,
|
|
1415
|
+
executionId: normalizeText(startResult.execution?.id),
|
|
1416
|
+
executionMode: helper
|
|
1417
|
+
? CODEX_APP_SERVER_EXECUTION_MODES.HELPER
|
|
1418
|
+
: CODEX_APP_SERVER_EXECUTION_MODES.INTERACTIVE,
|
|
1419
|
+
executionContextHash: compatibility.executionContextHash,
|
|
1420
|
+
healthz: "",
|
|
1421
|
+
historyAdapterBaseUrl,
|
|
1422
|
+
logPath,
|
|
1423
|
+
pid: Number.isSafeInteger(Number(startResult.pid)) ? Number(startResult.pid) : null,
|
|
1424
|
+
processCwd,
|
|
1425
|
+
processIdentity,
|
|
1426
|
+
processState: CODEX_APP_SERVER_PROCESS_STATE.RUNNING,
|
|
1427
|
+
provider: CODEX_APP_SERVER_PROVIDER_ID,
|
|
1428
|
+
readyz: "",
|
|
1429
|
+
runtimeDir,
|
|
1430
|
+
runtimesHash: compatibility.runtimesHash,
|
|
1431
|
+
schemaVersion: CODEX_APP_SERVER_METADATA_SCHEMA_VERSION,
|
|
1432
|
+
socketPath,
|
|
1433
|
+
startedAt: new Date().toISOString(),
|
|
1434
|
+
terminalEnvHash: codexAppServerTerminalEnvHash(normalizedTerminalEnv),
|
|
1435
|
+
toolHomeSource: helper ? "" : normalizedToolHomeSource,
|
|
1436
|
+
transport: CODEX_APP_SERVER_TRANSPORT.UNIX
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
async function ensureCodexAppServerRuntime(options = {}) {
|
|
1441
|
+
const runtimeDir = path.resolve(options.runtimeDir);
|
|
1442
|
+
while (codexAppServerRuntimePreparations.has(runtimeDir)) {
|
|
1443
|
+
await codexAppServerRuntimePreparations.get(runtimeDir);
|
|
1444
|
+
}
|
|
1445
|
+
// Each waiter rechecks its own auth and runtime configuration after startup.
|
|
1446
|
+
const operation = prepareCodexAppServerRuntime({ ...options, runtimeDir });
|
|
1447
|
+
codexAppServerRuntimePreparations.set(runtimeDir, operation);
|
|
1448
|
+
try {
|
|
1449
|
+
return await operation;
|
|
1450
|
+
} finally {
|
|
1451
|
+
codexAppServerRuntimePreparations.delete(runtimeDir);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
async function prepareCodexAppServerRuntime(options) {
|
|
1456
|
+
const { runtimeDir } = options;
|
|
1457
|
+
const [accountIdentitySignature, authStateSignature] = await Promise.all([
|
|
1458
|
+
currentCodexAccountIdentitySignature(options),
|
|
1459
|
+
currentCodexAuthStateSignature(options)
|
|
1460
|
+
]);
|
|
1461
|
+
const runtimeOptions = {
|
|
1462
|
+
...options,
|
|
1463
|
+
accountIdentitySignature,
|
|
1464
|
+
authStateSignature
|
|
1465
|
+
};
|
|
1466
|
+
const runtimeMetadataWriter = typeof options.runtimeMetadataWriter === "function"
|
|
1467
|
+
? options.runtimeMetadataWriter
|
|
1468
|
+
: writeCodexAppServerMetadata;
|
|
1469
|
+
await ensureWritablePrivateDirectory(runtimeDir);
|
|
1470
|
+
|
|
1471
|
+
const existing = await readCodexAppServerMetadata(runtimeDir);
|
|
1472
|
+
const existingStatus = existing ? await codexAppServerRuntimeStatus(existing, runtimeOptions) : null;
|
|
1473
|
+
if (existingStatus?.reusable) {
|
|
1474
|
+
return {
|
|
1475
|
+
...existingStatus.metadata,
|
|
1476
|
+
reused: true,
|
|
1477
|
+
runtimeStatus: existingStatus.status
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
const releaseLock = await acquireRuntimeLock(runtimeDir, runtimeOptions);
|
|
1482
|
+
try {
|
|
1483
|
+
let socketOwnerDrained = false;
|
|
1484
|
+
const afterLock = await readCodexAppServerMetadata(runtimeDir);
|
|
1485
|
+
const afterLockStatus = afterLock ? await codexAppServerRuntimeStatus(afterLock, runtimeOptions) : null;
|
|
1486
|
+
if (afterLockStatus?.reusable) {
|
|
1487
|
+
return {
|
|
1488
|
+
...afterLockStatus.metadata,
|
|
1489
|
+
reused: true,
|
|
1490
|
+
runtimeStatus: afterLockStatus.status
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
if (afterLock && (!afterLockStatus || afterLockStatus.replace !== false)) {
|
|
1495
|
+
const stopped = await stopCodexAppServerProcess(runtimeDir, runtimeOptions);
|
|
1496
|
+
if (stopped.processExitVerified !== true) {
|
|
1497
|
+
const error = new Error(
|
|
1498
|
+
`${clientInfo.title} found an earlier Codex app-server but could not prove that its owned execution was stopped. It refused to start a replacement because two assistant processes could damage the session or worsen resource pressure.`
|
|
1499
|
+
);
|
|
1500
|
+
error.code = `${errorPrefix}codex_app_server_process_identity_unverified`;
|
|
1501
|
+
error.cleanupRequired = true;
|
|
1502
|
+
error.retryable = false;
|
|
1503
|
+
throw error;
|
|
1504
|
+
}
|
|
1505
|
+
socketOwnerDrained = true;
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
const started = await startCodexAppServerProcess({
|
|
1509
|
+
...runtimeOptions,
|
|
1510
|
+
runtimeDir,
|
|
1511
|
+
socketOwnerDrained
|
|
1512
|
+
});
|
|
1513
|
+
try {
|
|
1514
|
+
await assertCodexAuthGenerationCurrent(started.authStateSignature, runtimeOptions);
|
|
1515
|
+
await runtimeMetadataWriter(runtimeDir, started);
|
|
1516
|
+
await assertCodexAuthGenerationCurrent(started.authStateSignature, runtimeOptions);
|
|
1517
|
+
} catch (error) {
|
|
1518
|
+
const helper = codexAppServerIsHelper(runtimeOptions);
|
|
1519
|
+
const cleanup = await cleanupFailedCodexAppServerStart(runtimeDir, {
|
|
1520
|
+
helper,
|
|
1521
|
+
executionId: started.executionId,
|
|
1522
|
+
pid: started.pid,
|
|
1523
|
+
processIdentity: started.processIdentity
|
|
1524
|
+
}, runtimeOptions);
|
|
1525
|
+
const failure = new Error(
|
|
1526
|
+
helper
|
|
1527
|
+
? "Codex isolated helper runtime metadata could not be recorded."
|
|
1528
|
+
: "Codex app-server runtime could not be published safely.",
|
|
1529
|
+
{ cause: error }
|
|
1530
|
+
);
|
|
1531
|
+
failure.code = cleanup.cleanupFailed
|
|
1532
|
+
? (helper
|
|
1533
|
+
? `${errorPrefix}codex_helper_runtime_cleanup_required`
|
|
1534
|
+
: `${errorPrefix}codex_app_server_cleanup_required`)
|
|
1535
|
+
: (error?.code || (helper
|
|
1536
|
+
? `${errorPrefix}codex_helper_runtime_metadata_failed`
|
|
1537
|
+
: `${errorPrefix}codex_app_server_metadata_failed`));
|
|
1538
|
+
failure.cleanupRequired = cleanup.cleanupFailed;
|
|
1539
|
+
failure.retryable = false;
|
|
1540
|
+
throw failure;
|
|
1541
|
+
}
|
|
1542
|
+
return {
|
|
1543
|
+
...started,
|
|
1544
|
+
reused: false
|
|
1545
|
+
};
|
|
1546
|
+
} finally {
|
|
1547
|
+
await releaseLock();
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
return {
|
|
1552
|
+
ensure: ensureCodexAppServerRuntime,
|
|
1553
|
+
start: startCodexAppServerProcess,
|
|
1554
|
+
stop: stopCodexAppServerRuntime,
|
|
1555
|
+
isLive: codexAppServerMetadataIsLive,
|
|
1556
|
+
cleanupFailedStart: cleanupFailedCodexAppServerStart
|
|
1557
|
+
};
|
|
1558
|
+
}
|
|
1559
|
+
|