@codexhost/cli-linux-arm64 0.3.5
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 +47 -0
- package/THIRD_PARTY_NOTICES.txt +29 -0
- package/app/codexhost-distribution.json +1 -0
- package/app/desktop-controller.mjs +1888 -0
- package/app/host-runtime.mjs +77018 -0
- package/app/renderer-extension.js +23404 -0
- package/bin/codexhost +0 -0
- package/libexec/codexhost-shim +0 -0
- package/libexec/codexhost-updater +0 -0
- package/licenses/Anthropic-SDK-LICENSE.txt +8 -0
- package/licenses/Claude-Agent-SDK-LICENSE.md +1 -0
- package/licenses/MCP-SDK-LICENSE.txt +21 -0
- package/licenses/diff-LICENSE.txt +29 -0
- package/licenses/lucide-LICENSE.txt +43 -0
- package/licenses/ws-LICENSE.txt +20 -0
- package/licenses/zod-LICENSE.txt +21 -0
- package/package.json +42 -0
|
@@ -0,0 +1,1888 @@
|
|
|
1
|
+
// packages/desktop-control/src/production-controller.ts
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
// packages/desktop-control/src/controller-attachment-server.ts
|
|
6
|
+
import { createServer } from "node:net";
|
|
7
|
+
var MAX_REQUEST_BYTES = 96;
|
|
8
|
+
function validPort(value) {
|
|
9
|
+
return Number.isInteger(value) && value >= 1 && value <= 65535;
|
|
10
|
+
}
|
|
11
|
+
function validNonce(value) {
|
|
12
|
+
return /^[0-9a-f]{32}$/.test(value);
|
|
13
|
+
}
|
|
14
|
+
function closeServer(server) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
server.close((error) => {
|
|
17
|
+
if (error) reject(error);
|
|
18
|
+
else resolve();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function respond(socket, value) {
|
|
23
|
+
socket.end(`${value}
|
|
24
|
+
`);
|
|
25
|
+
}
|
|
26
|
+
async function startControllerAttachmentServer(options) {
|
|
27
|
+
if (!validPort(options.port)) throw new Error("attachment port must be a valid TCP port");
|
|
28
|
+
if (!validNonce(options.nonce)) {
|
|
29
|
+
throw new Error("attachment nonce must be 32 lowercase hexadecimal characters");
|
|
30
|
+
}
|
|
31
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
32
|
+
const server = createServer((socket) => {
|
|
33
|
+
sockets.add(socket);
|
|
34
|
+
socket.once("close", () => sockets.delete(socket));
|
|
35
|
+
socket.setEncoding("utf8");
|
|
36
|
+
socket.setTimeout(5e3, () => socket.destroy());
|
|
37
|
+
let request = "";
|
|
38
|
+
let handled = false;
|
|
39
|
+
socket.on("data", (chunk) => {
|
|
40
|
+
if (handled) return;
|
|
41
|
+
request += chunk;
|
|
42
|
+
if (request.length > MAX_REQUEST_BYTES) {
|
|
43
|
+
handled = true;
|
|
44
|
+
respond(socket, "rejected");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const newline = request.indexOf("\n");
|
|
48
|
+
if (newline < 0) return;
|
|
49
|
+
handled = true;
|
|
50
|
+
const line = request.slice(0, newline).replace(/\r$/, "");
|
|
51
|
+
if (line === `ATTACH ${options.nonce}`) {
|
|
52
|
+
void options.attach().then(
|
|
53
|
+
() => respond(socket, "ready"),
|
|
54
|
+
() => respond(socket, "failed")
|
|
55
|
+
);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
respond(socket, "rejected");
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
await new Promise((resolve, reject) => {
|
|
62
|
+
const onError = (error) => reject(error);
|
|
63
|
+
server.once("error", onError);
|
|
64
|
+
server.listen(options.port, "127.0.0.1", () => {
|
|
65
|
+
server.off("error", onError);
|
|
66
|
+
resolve();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
let closed = false;
|
|
70
|
+
return {
|
|
71
|
+
async close() {
|
|
72
|
+
if (closed) return;
|
|
73
|
+
closed = true;
|
|
74
|
+
for (const socket of sockets) socket.destroy();
|
|
75
|
+
await closeServer(server);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// packages/desktop-control/src/cdp-client.ts
|
|
81
|
+
function isRecord(value) {
|
|
82
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
83
|
+
}
|
|
84
|
+
function nonEmptyString(value, field) {
|
|
85
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
86
|
+
throw new Error(`CDP target '${field}' must be non-empty text`);
|
|
87
|
+
}
|
|
88
|
+
return value;
|
|
89
|
+
}
|
|
90
|
+
function loopbackUrl(value, protocols) {
|
|
91
|
+
const url = new URL(value);
|
|
92
|
+
if (!protocols.includes(url.protocol)) {
|
|
93
|
+
throw new Error(`CDP endpoint must use ${protocols.join(" or ")}`);
|
|
94
|
+
}
|
|
95
|
+
if (!["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)) {
|
|
96
|
+
throw new Error("CDP endpoint must use a loopback host");
|
|
97
|
+
}
|
|
98
|
+
return url;
|
|
99
|
+
}
|
|
100
|
+
function parseTarget(value) {
|
|
101
|
+
if (!isRecord(value)) throw new Error("CDP target must be an object");
|
|
102
|
+
const target = {
|
|
103
|
+
id: nonEmptyString(value.id, "id"),
|
|
104
|
+
type: nonEmptyString(value.type, "type"),
|
|
105
|
+
title: typeof value.title === "string" ? value.title : "",
|
|
106
|
+
url: nonEmptyString(value.url, "url"),
|
|
107
|
+
webSocketDebuggerUrl: nonEmptyString(value.webSocketDebuggerUrl, "webSocketDebuggerUrl")
|
|
108
|
+
};
|
|
109
|
+
loopbackUrl(target.webSocketDebuggerUrl, ["ws:", "wss:"]);
|
|
110
|
+
return target;
|
|
111
|
+
}
|
|
112
|
+
function defaultFetch(url) {
|
|
113
|
+
return fetch(url);
|
|
114
|
+
}
|
|
115
|
+
function defaultSocketFactory(url) {
|
|
116
|
+
return new WebSocket(url);
|
|
117
|
+
}
|
|
118
|
+
function messageText(value) {
|
|
119
|
+
if (typeof value === "string") return value;
|
|
120
|
+
if (value instanceof ArrayBuffer) return new TextDecoder().decode(value);
|
|
121
|
+
if (ArrayBuffer.isView(value)) {
|
|
122
|
+
return new TextDecoder().decode(
|
|
123
|
+
new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
throw new Error("CDP WebSocket returned a non-text message");
|
|
127
|
+
}
|
|
128
|
+
function parseResponse(value) {
|
|
129
|
+
if (!isRecord(value) || typeof value.id !== "number") return null;
|
|
130
|
+
const response = { id: value.id };
|
|
131
|
+
if ("result" in value) response.result = value.result;
|
|
132
|
+
if (isRecord(value.error)) {
|
|
133
|
+
response.error = {
|
|
134
|
+
...typeof value.error.code === "number" ? { code: value.error.code } : {},
|
|
135
|
+
...typeof value.error.message === "string" ? { message: value.error.message } : {}
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return response;
|
|
139
|
+
}
|
|
140
|
+
async function listCdpTargets(endpoint, fetchImpl = defaultFetch) {
|
|
141
|
+
const baseUrl = loopbackUrl(endpoint, ["http:", "https:"]);
|
|
142
|
+
const targetsUrl = new URL("/json/list", baseUrl).toString();
|
|
143
|
+
const response = await fetchImpl(targetsUrl);
|
|
144
|
+
if (!response.ok) throw new Error(`CDP target discovery failed with HTTP ${response.status}`);
|
|
145
|
+
const value = await response.json();
|
|
146
|
+
if (!Array.isArray(value)) throw new Error("CDP target discovery did not return an array");
|
|
147
|
+
return value.map(parseTarget);
|
|
148
|
+
}
|
|
149
|
+
var CdpClient = class _CdpClient {
|
|
150
|
+
#commandTimeoutMs;
|
|
151
|
+
#socket;
|
|
152
|
+
#closed = false;
|
|
153
|
+
#nextId = 1;
|
|
154
|
+
#pending = /* @__PURE__ */ new Map();
|
|
155
|
+
#listeners = /* @__PURE__ */ new Map();
|
|
156
|
+
constructor(socket, commandTimeoutMs) {
|
|
157
|
+
this.#socket = socket;
|
|
158
|
+
this.#commandTimeoutMs = commandTimeoutMs;
|
|
159
|
+
socket.addEventListener("message", (event) => this.#onMessage(event));
|
|
160
|
+
socket.addEventListener("error", () => this.#fail(new Error("CDP WebSocket failed")));
|
|
161
|
+
socket.addEventListener("close", () => this.#fail(new Error("CDP WebSocket closed")));
|
|
162
|
+
}
|
|
163
|
+
static async connect(url, options = {}) {
|
|
164
|
+
loopbackUrl(url, ["ws:", "wss:"]);
|
|
165
|
+
const socketFactory = options.socketFactory ?? defaultSocketFactory;
|
|
166
|
+
const socket = socketFactory(url);
|
|
167
|
+
const connectTimeoutMs = options.connectTimeoutMs ?? 1e4;
|
|
168
|
+
await new Promise((resolve, reject) => {
|
|
169
|
+
const timeout = setTimeout(() => {
|
|
170
|
+
cleanup();
|
|
171
|
+
socket.close();
|
|
172
|
+
reject(new Error("CDP WebSocket connection timed out"));
|
|
173
|
+
}, connectTimeoutMs);
|
|
174
|
+
const onOpen = () => {
|
|
175
|
+
cleanup();
|
|
176
|
+
resolve();
|
|
177
|
+
};
|
|
178
|
+
const onError = () => {
|
|
179
|
+
cleanup();
|
|
180
|
+
reject(new Error("CDP WebSocket connection failed"));
|
|
181
|
+
};
|
|
182
|
+
const cleanup = () => {
|
|
183
|
+
clearTimeout(timeout);
|
|
184
|
+
socket.removeEventListener("open", onOpen);
|
|
185
|
+
socket.removeEventListener("error", onError);
|
|
186
|
+
};
|
|
187
|
+
socket.addEventListener("open", onOpen);
|
|
188
|
+
socket.addEventListener("error", onError);
|
|
189
|
+
});
|
|
190
|
+
return new _CdpClient(socket, options.commandTimeoutMs ?? 1e4);
|
|
191
|
+
}
|
|
192
|
+
command(method, params = {}) {
|
|
193
|
+
return this.#send(method, params);
|
|
194
|
+
}
|
|
195
|
+
sessionCommand(sessionId, method, params = {}) {
|
|
196
|
+
if (sessionId.length === 0) return Promise.reject(new Error("CDP session ID is required"));
|
|
197
|
+
return this.#send(method, params, sessionId);
|
|
198
|
+
}
|
|
199
|
+
on(method, listener) {
|
|
200
|
+
const listeners = this.#listeners.get(method) ?? /* @__PURE__ */ new Set();
|
|
201
|
+
listeners.add(listener);
|
|
202
|
+
this.#listeners.set(method, listeners);
|
|
203
|
+
return () => {
|
|
204
|
+
listeners.delete(listener);
|
|
205
|
+
if (listeners.size === 0) this.#listeners.delete(method);
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
#send(method, params, sessionId) {
|
|
209
|
+
if (this.#closed) return Promise.reject(new Error("CDP client is closed"));
|
|
210
|
+
const id = this.#nextId++;
|
|
211
|
+
return new Promise((resolve, reject) => {
|
|
212
|
+
const timeout = setTimeout(() => {
|
|
213
|
+
this.#pending.delete(id);
|
|
214
|
+
reject(new Error(`CDP command '${method}' timed out`));
|
|
215
|
+
}, this.#commandTimeoutMs);
|
|
216
|
+
this.#pending.set(id, { resolve, reject, timeout });
|
|
217
|
+
try {
|
|
218
|
+
this.#socket.send(
|
|
219
|
+
JSON.stringify({ id, method, params, ...sessionId ? { sessionId } : {} })
|
|
220
|
+
);
|
|
221
|
+
} catch (error) {
|
|
222
|
+
clearTimeout(timeout);
|
|
223
|
+
this.#pending.delete(id);
|
|
224
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
async evaluate(expression) {
|
|
229
|
+
const response = await this.command("Runtime.evaluate", {
|
|
230
|
+
expression,
|
|
231
|
+
awaitPromise: true,
|
|
232
|
+
returnByValue: true
|
|
233
|
+
});
|
|
234
|
+
if (!isRecord(response)) throw new Error("Runtime.evaluate returned an invalid result");
|
|
235
|
+
if (isRecord(response.exceptionDetails)) {
|
|
236
|
+
const text = typeof response.exceptionDetails.text === "string" ? response.exceptionDetails.text : "Renderer evaluation failed";
|
|
237
|
+
throw new Error(text);
|
|
238
|
+
}
|
|
239
|
+
if (!isRecord(response.result) || !("value" in response.result)) {
|
|
240
|
+
throw new Error("Runtime.evaluate did not return a value");
|
|
241
|
+
}
|
|
242
|
+
return response.result.value;
|
|
243
|
+
}
|
|
244
|
+
close() {
|
|
245
|
+
if (this.#closed) return;
|
|
246
|
+
this.#closed = true;
|
|
247
|
+
this.#fail(new Error("CDP client closed"));
|
|
248
|
+
this.#listeners.clear();
|
|
249
|
+
this.#socket.close();
|
|
250
|
+
}
|
|
251
|
+
#onMessage(event) {
|
|
252
|
+
try {
|
|
253
|
+
const value = JSON.parse(messageText(event.data));
|
|
254
|
+
const response = parseResponse(value);
|
|
255
|
+
if (!response) {
|
|
256
|
+
if (!isRecord(value) || typeof value.method !== "string") return;
|
|
257
|
+
const sessionId = typeof value.sessionId === "string" ? value.sessionId : void 0;
|
|
258
|
+
for (const listener of this.#listeners.get(value.method) ?? []) {
|
|
259
|
+
listener(value.params, sessionId);
|
|
260
|
+
}
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const pending = this.#pending.get(response.id);
|
|
264
|
+
if (!pending) return;
|
|
265
|
+
clearTimeout(pending.timeout);
|
|
266
|
+
this.#pending.delete(response.id);
|
|
267
|
+
if (response.error) {
|
|
268
|
+
const code = response.error.code === void 0 ? "unknown" : String(response.error.code);
|
|
269
|
+
pending.reject(
|
|
270
|
+
new Error(response.error.message ?? `CDP command failed with error ${code}`)
|
|
271
|
+
);
|
|
272
|
+
} else {
|
|
273
|
+
pending.resolve(response.result);
|
|
274
|
+
}
|
|
275
|
+
} catch (error) {
|
|
276
|
+
this.#fail(error instanceof Error ? error : new Error(String(error)));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
#fail(error) {
|
|
280
|
+
for (const pending of this.#pending.values()) {
|
|
281
|
+
clearTimeout(pending.timeout);
|
|
282
|
+
pending.reject(error);
|
|
283
|
+
}
|
|
284
|
+
this.#pending.clear();
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// packages/desktop-control/src/main-process-title-policy.ts
|
|
289
|
+
function isRecord2(value) {
|
|
290
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
291
|
+
}
|
|
292
|
+
function resultRecord(value, command) {
|
|
293
|
+
if (!isRecord2(value)) throw new Error(`${command} returned an invalid result`);
|
|
294
|
+
if (isRecord2(value.exceptionDetails)) {
|
|
295
|
+
const exception = value.exceptionDetails.exception;
|
|
296
|
+
const description = isRecord2(exception) ? exception.description : null;
|
|
297
|
+
const text = value.exceptionDetails.text;
|
|
298
|
+
throw new Error(
|
|
299
|
+
typeof description === "string" ? description : typeof text === "string" ? text : `${command} failed`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
return value;
|
|
303
|
+
}
|
|
304
|
+
function remoteObjectId(value, label) {
|
|
305
|
+
if (!isRecord2(value) || typeof value.objectId !== "string") {
|
|
306
|
+
throw new Error(`${label} is unavailable`);
|
|
307
|
+
}
|
|
308
|
+
return value.objectId;
|
|
309
|
+
}
|
|
310
|
+
function properties(value, command) {
|
|
311
|
+
const result = resultRecord(value, command).result;
|
|
312
|
+
if (!Array.isArray(result)) throw new Error(`${command} returned invalid properties`);
|
|
313
|
+
return result.filter(isRecord2);
|
|
314
|
+
}
|
|
315
|
+
var ELECTRON_MODULE_EXPRESSION = `(() => {
|
|
316
|
+
const mainModule = process.mainModule;
|
|
317
|
+
if (mainModule != null && typeof mainModule.require === 'function') {
|
|
318
|
+
return mainModule.require('electron');
|
|
319
|
+
}
|
|
320
|
+
const { createRequire } = process.getBuiltinModule('module');
|
|
321
|
+
return createRequire(process.execPath)('electron');
|
|
322
|
+
})()`;
|
|
323
|
+
var CONNECT_APP_HOST_CHANNEL = "codex_desktop:connect-app-host";
|
|
324
|
+
var POLICY_STATE_SYMBOL = "codexhost.main-process-title-policy.v1";
|
|
325
|
+
var SERVICE_OWNER_SYMBOL = "codexhost.main-process-title-policy.owner.v1";
|
|
326
|
+
var RENDERER_READY_EXPRESSION = "(() => { Object.defineProperty(window, '__codexhostMainProcessTitlePolicyV1', { configurable: true, value: { state: 'ready' } }); return 'ready'; })()";
|
|
327
|
+
var INSTALL_POLICY_FUNCTION = `async function (rendererWebContentsId) {
|
|
328
|
+
const mainModule = process.mainModule;
|
|
329
|
+
const electron = mainModule != null && typeof mainModule.require === 'function'
|
|
330
|
+
? mainModule.require('electron')
|
|
331
|
+
: process.getBuiltinModule('module').createRequire(process.execPath)('electron');
|
|
332
|
+
const selected = electron.webContents.fromId(rendererWebContentsId);
|
|
333
|
+
if (selected == null || selected.isDestroyed() || selected.getType() !== 'window') {
|
|
334
|
+
throw new Error('Owned Renderer unavailable for title policy');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const context = this(selected);
|
|
338
|
+
if (context == null || typeof context.createAppHost !== 'function') {
|
|
339
|
+
throw new Error('WindowContext unavailable for title policy');
|
|
340
|
+
}
|
|
341
|
+
const stateSymbol = Symbol.for(${JSON.stringify(POLICY_STATE_SYMBOL)});
|
|
342
|
+
const ownerSymbol = Symbol.for(${JSON.stringify(SERVICE_OWNER_SYMBOL)});
|
|
343
|
+
globalThis[stateSymbol]?.dispose?.();
|
|
344
|
+
|
|
345
|
+
const originalCreateAppHost = context.createAppHost;
|
|
346
|
+
const sampleHost = originalCreateAppHost.call(context, selected);
|
|
347
|
+
const sampleService = sampleHost?.services?.threadMetadataGeneration;
|
|
348
|
+
const servicePrototype = sampleService == null ? null : Object.getPrototypeOf(sampleService);
|
|
349
|
+
const originalGenerateTitle = servicePrototype?.generateTitle;
|
|
350
|
+
if (
|
|
351
|
+
servicePrototype == null ||
|
|
352
|
+
typeof originalGenerateTitle !== 'function' ||
|
|
353
|
+
!Function.prototype.toString.call(originalGenerateTitle).includes('Failed to generate thread title')
|
|
354
|
+
) {
|
|
355
|
+
throw new Error('ThreadMetadataGenerationService signature mismatch');
|
|
356
|
+
}
|
|
357
|
+
const counters = {
|
|
358
|
+
codexTitleCalls: 0,
|
|
359
|
+
piTitleSkips: 0,
|
|
360
|
+
externalTitleSkips: 0,
|
|
361
|
+
ambiguousTitleSkips: 0,
|
|
362
|
+
};
|
|
363
|
+
const ownedWebContentsIds = new Set();
|
|
364
|
+
const ownService = (service, contents) => {
|
|
365
|
+
const existingOwner = service[ownerSymbol];
|
|
366
|
+
if (existingOwner != null && existingOwner !== contents) {
|
|
367
|
+
throw new Error('Thread metadata service ownership mismatch');
|
|
368
|
+
}
|
|
369
|
+
if (existingOwner == null) {
|
|
370
|
+
Object.defineProperty(service, ownerSymbol, { value: contents });
|
|
371
|
+
}
|
|
372
|
+
ownedWebContentsIds.add(contents.id);
|
|
373
|
+
};
|
|
374
|
+
ownService(sampleService, selected);
|
|
375
|
+
const wrappedCreateAppHost = function (contents) {
|
|
376
|
+
const host = originalCreateAppHost.call(this, contents);
|
|
377
|
+
const service = host?.services?.threadMetadataGeneration;
|
|
378
|
+
if (service != null) ownService(service, contents);
|
|
379
|
+
return host;
|
|
380
|
+
};
|
|
381
|
+
const wrappedGenerateTitle = async function (params) {
|
|
382
|
+
const owner = this[ownerSymbol];
|
|
383
|
+
if (owner == null || owner.isDestroyed()) {
|
|
384
|
+
counters.ambiguousTitleSkips += 1;
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
let selection = null;
|
|
388
|
+
try {
|
|
389
|
+
selection = await owner.executeJavaScript(
|
|
390
|
+
"window.__codexhostRendererBindingProbeV1?.lockedSelection() ?? null",
|
|
391
|
+
true,
|
|
392
|
+
);
|
|
393
|
+
} catch {}
|
|
394
|
+
if (selection?.phase !== 'locked') {
|
|
395
|
+
counters.ambiguousTitleSkips += 1;
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
if (selection.agent === 'pi') {
|
|
399
|
+
counters.piTitleSkips += 1;
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
if (selection.agent !== 'codex') {
|
|
403
|
+
counters.externalTitleSkips += 1;
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
counters.codexTitleCalls += 1;
|
|
407
|
+
return originalGenerateTitle.call(this, params);
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
context.createAppHost = wrappedCreateAppHost;
|
|
411
|
+
servicePrototype.generateTitle = wrappedGenerateTitle;
|
|
412
|
+
const state = {
|
|
413
|
+
counters,
|
|
414
|
+
ownedWebContentsIds,
|
|
415
|
+
dispose() {
|
|
416
|
+
if (context.createAppHost === wrappedCreateAppHost) {
|
|
417
|
+
context.createAppHost = originalCreateAppHost;
|
|
418
|
+
}
|
|
419
|
+
if (servicePrototype.generateTitle === wrappedGenerateTitle) {
|
|
420
|
+
servicePrototype.generateTitle = originalGenerateTitle;
|
|
421
|
+
}
|
|
422
|
+
if (globalThis[stateSymbol] === state) delete globalThis[stateSymbol];
|
|
423
|
+
},
|
|
424
|
+
};
|
|
425
|
+
globalThis[stateSymbol] = state;
|
|
426
|
+
return {
|
|
427
|
+
state: 'ready',
|
|
428
|
+
reason: 'ready',
|
|
429
|
+
requiresRendererReload: true,
|
|
430
|
+
};
|
|
431
|
+
}`;
|
|
432
|
+
async function installMainProcessTitlePolicy(inspector, rendererWebContentsId) {
|
|
433
|
+
if (!Number.isInteger(rendererWebContentsId) || rendererWebContentsId <= 0) {
|
|
434
|
+
throw new Error("Renderer webContents ID must be a positive integer");
|
|
435
|
+
}
|
|
436
|
+
const listenerResponse = resultRecord(
|
|
437
|
+
await inspector.command("Runtime.evaluate", {
|
|
438
|
+
expression: `(${ELECTRON_MODULE_EXPRESSION}).ipcMain.listeners(${JSON.stringify(
|
|
439
|
+
CONNECT_APP_HOST_CHANNEL
|
|
440
|
+
)})[0]`
|
|
441
|
+
}),
|
|
442
|
+
"Runtime.evaluate"
|
|
443
|
+
);
|
|
444
|
+
const listener = listenerResponse.result;
|
|
445
|
+
const listenerId = remoteObjectId(listener, "connect-app-host listener");
|
|
446
|
+
const listenerProperties = resultRecord(
|
|
447
|
+
await inspector.command("Runtime.getProperties", { objectId: listenerId }),
|
|
448
|
+
"Runtime.getProperties"
|
|
449
|
+
);
|
|
450
|
+
const internalProperties = listenerProperties.internalProperties;
|
|
451
|
+
if (!Array.isArray(internalProperties)) {
|
|
452
|
+
throw new Error("connect-app-host listener scopes are unavailable");
|
|
453
|
+
}
|
|
454
|
+
const scopes = internalProperties.find(
|
|
455
|
+
(property) => isRecord2(property) && property.name === "[[Scopes]]"
|
|
456
|
+
);
|
|
457
|
+
const scopesId = remoteObjectId(
|
|
458
|
+
isRecord2(scopes) ? scopes.value : null,
|
|
459
|
+
"connect-app-host listener scopes"
|
|
460
|
+
);
|
|
461
|
+
const scopeProperties = properties(
|
|
462
|
+
await inspector.command("Runtime.getProperties", {
|
|
463
|
+
objectId: scopesId,
|
|
464
|
+
ownProperties: true
|
|
465
|
+
}),
|
|
466
|
+
"Runtime.getProperties"
|
|
467
|
+
);
|
|
468
|
+
const localScope = scopeProperties.find((property) => property.name === "0");
|
|
469
|
+
const localScopeId = remoteObjectId(localScope?.value, "connect-app-host local scope");
|
|
470
|
+
const localProperties = properties(
|
|
471
|
+
await inspector.command("Runtime.getProperties", {
|
|
472
|
+
objectId: localScopeId,
|
|
473
|
+
ownProperties: true
|
|
474
|
+
}),
|
|
475
|
+
"Runtime.getProperties"
|
|
476
|
+
);
|
|
477
|
+
const getContext = localProperties.find(
|
|
478
|
+
(property) => property.name === "f" && typeof property.value?.objectId === "string"
|
|
479
|
+
);
|
|
480
|
+
const getContextId = remoteObjectId(getContext?.value, "getContextForWebContents");
|
|
481
|
+
const installPromise = resultRecord(
|
|
482
|
+
await inspector.command("Runtime.callFunctionOn", {
|
|
483
|
+
objectId: getContextId,
|
|
484
|
+
functionDeclaration: INSTALL_POLICY_FUNCTION,
|
|
485
|
+
arguments: [{ value: rendererWebContentsId }]
|
|
486
|
+
}),
|
|
487
|
+
"Runtime.callFunctionOn"
|
|
488
|
+
);
|
|
489
|
+
const promiseObjectId = remoteObjectId(
|
|
490
|
+
installPromise.result,
|
|
491
|
+
"Main-process title policy installation promise"
|
|
492
|
+
);
|
|
493
|
+
const installResponse = resultRecord(
|
|
494
|
+
await inspector.command("Runtime.awaitPromise", {
|
|
495
|
+
promiseObjectId,
|
|
496
|
+
returnByValue: true
|
|
497
|
+
}),
|
|
498
|
+
"Runtime.awaitPromise"
|
|
499
|
+
);
|
|
500
|
+
const remoteResult = installResponse.result;
|
|
501
|
+
const value = isRecord2(remoteResult) ? remoteResult.value : null;
|
|
502
|
+
if (!isRecord2(value) || value.state !== "ready" || value.reason !== "ready" || value.requiresRendererReload !== true || Object.keys(value).length !== 3) {
|
|
503
|
+
throw new Error("Main-process title policy returned an invalid status");
|
|
504
|
+
}
|
|
505
|
+
return value;
|
|
506
|
+
}
|
|
507
|
+
async function markRendererTitlePolicyReady(inspector, rendererWebContentsId) {
|
|
508
|
+
if (!Number.isInteger(rendererWebContentsId) || rendererWebContentsId <= 0) {
|
|
509
|
+
throw new Error("Renderer webContents ID must be a positive integer");
|
|
510
|
+
}
|
|
511
|
+
const value = await inspector.evaluate(`(async () => {
|
|
512
|
+
const state = globalThis[Symbol.for(${JSON.stringify(POLICY_STATE_SYMBOL)})];
|
|
513
|
+
if (state == null) throw new Error('Main-process title policy is unavailable');
|
|
514
|
+
const mainModule = process.mainModule;
|
|
515
|
+
const electron = mainModule != null && typeof mainModule.require === 'function'
|
|
516
|
+
? mainModule.require('electron')
|
|
517
|
+
: process.getBuiltinModule('module').createRequire(process.execPath)('electron');
|
|
518
|
+
const selected = electron.webContents.fromId(${rendererWebContentsId});
|
|
519
|
+
if (selected == null || selected.isDestroyed() || selected.getType() !== 'window') {
|
|
520
|
+
throw new Error('Owned Renderer unavailable for title policy readiness');
|
|
521
|
+
}
|
|
522
|
+
if (!state.ownedWebContentsIds.has(selected.id)) {
|
|
523
|
+
throw new Error('Renderer metadata service ownership is unavailable');
|
|
524
|
+
}
|
|
525
|
+
const marker = selected.executeJavaScript(
|
|
526
|
+
${JSON.stringify(RENDERER_READY_EXPRESSION)},
|
|
527
|
+
true,
|
|
528
|
+
);
|
|
529
|
+
const markerTimeout = new Promise((_, reject) => {
|
|
530
|
+
setTimeout(() => reject(new Error('Renderer readiness marker timed out')), 2_000);
|
|
531
|
+
});
|
|
532
|
+
await Promise.race([marker, markerTimeout]);
|
|
533
|
+
return { state: 'ready', reason: 'owned-metadata-service' };
|
|
534
|
+
})()`);
|
|
535
|
+
if (!isRecord2(value) || value.state !== "ready" || value.reason !== "owned-metadata-service") {
|
|
536
|
+
throw new Error("Renderer title policy returned an invalid readiness status");
|
|
537
|
+
}
|
|
538
|
+
return value;
|
|
539
|
+
}
|
|
540
|
+
async function readMainProcessTitlePolicyCounters(inspector) {
|
|
541
|
+
const value = await inspector.evaluate(`(() => {
|
|
542
|
+
const state = globalThis[Symbol.for(${JSON.stringify(POLICY_STATE_SYMBOL)})];
|
|
543
|
+
return state == null ? null : { ...state.counters };
|
|
544
|
+
})()`);
|
|
545
|
+
if (value === null) return null;
|
|
546
|
+
if (!isRecord2(value) || !Number.isInteger(value.codexTitleCalls) || !Number.isInteger(value.piTitleSkips) || !Number.isInteger(value.externalTitleSkips) || !Number.isInteger(value.ambiguousTitleSkips)) {
|
|
547
|
+
throw new Error("Main-process title policy returned invalid counters");
|
|
548
|
+
}
|
|
549
|
+
return value;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// packages/desktop-control/src/renderer-draft-prewarm-runtime.ts
|
|
553
|
+
function installDraftPrewarmPolicyBridge(manager, bridge, hostId, target, prewarmedThreadManager) {
|
|
554
|
+
const existing = target.__codexhostDraftPrewarmPolicyV1;
|
|
555
|
+
if (existing?.owns?.length === 4 && existing.owns(manager, bridge, hostId, prewarmedThreadManager) === true) {
|
|
556
|
+
return { state: "ready", reason: "owned-request-bridge" };
|
|
557
|
+
}
|
|
558
|
+
existing?.dispose?.();
|
|
559
|
+
const originalSend = bridge.sendRequest;
|
|
560
|
+
const originalPrewarm = bridge.prewarmThreadStart;
|
|
561
|
+
const originalOnNotification = manager.onNotification;
|
|
562
|
+
const originalDispatchAppServerResponse = manager.dispatchAppServerResponse;
|
|
563
|
+
let selectedModel = null;
|
|
564
|
+
const isRecord5 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
565
|
+
const isRemoteControlHost = hostId.startsWith("remote-control:");
|
|
566
|
+
const knownExternalThreadIds = /* @__PURE__ */ new Set();
|
|
567
|
+
const knownOfficialThreadIds = /* @__PURE__ */ new Set();
|
|
568
|
+
const threadOwnershipResolutions = /* @__PURE__ */ new Map();
|
|
569
|
+
const createBridgeProcessHandle = () => `codexhost-${typeof crypto?.randomUUID === "function" ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(16).slice(2)}`}`;
|
|
570
|
+
let bridgeProcessHandle = createBridgeProcessHandle();
|
|
571
|
+
const bridgeReadyMethod = "codexhost/remote-control-bridge/ready";
|
|
572
|
+
const bridgeRequests = /* @__PURE__ */ new Map();
|
|
573
|
+
const bridgeServerRequestIdPrefix = "codexhost/remote-control-bridge/server-request/";
|
|
574
|
+
const bridgeServerRequests = /* @__PURE__ */ new Map();
|
|
575
|
+
let nextBridgeServerRequestOrdinal = 1;
|
|
576
|
+
let outputDecoders = {
|
|
577
|
+
stdout: new TextDecoder(),
|
|
578
|
+
stderr: new TextDecoder()
|
|
579
|
+
};
|
|
580
|
+
const outputBuffers = { stdout: "", stderr: "" };
|
|
581
|
+
let bridgeState = "idle";
|
|
582
|
+
let bridgeReadyPromise = null;
|
|
583
|
+
let bridgeReadyResolve = null;
|
|
584
|
+
let bridgeReadyReject = null;
|
|
585
|
+
let bridgeReadyTimeout = null;
|
|
586
|
+
let bridgeInitialization = null;
|
|
587
|
+
let writeTail = Promise.resolve();
|
|
588
|
+
const transportError = (message, cause) => {
|
|
589
|
+
const error = new Error(`codexhost Remote Control bridge: ${message}`);
|
|
590
|
+
if (cause !== void 0) Object.assign(error, { cause });
|
|
591
|
+
return error;
|
|
592
|
+
};
|
|
593
|
+
const utf8Base64 = (value) => {
|
|
594
|
+
const bytes = new TextEncoder().encode(value);
|
|
595
|
+
let binary = "";
|
|
596
|
+
for (let index = 0; index < bytes.length; index += 32768) {
|
|
597
|
+
binary += String.fromCharCode(...bytes.subarray(index, index + 32768));
|
|
598
|
+
}
|
|
599
|
+
return btoa(binary);
|
|
600
|
+
};
|
|
601
|
+
const utf16LeBase64 = (value) => {
|
|
602
|
+
let binary = "";
|
|
603
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
604
|
+
const codeUnit = value.charCodeAt(index);
|
|
605
|
+
binary += String.fromCharCode(codeUnit & 255, codeUnit >>> 8);
|
|
606
|
+
}
|
|
607
|
+
return btoa(binary);
|
|
608
|
+
};
|
|
609
|
+
const failBridge = (cause, terminateProcess = true) => {
|
|
610
|
+
if (bridgeState === "failed" || bridgeState === "disposed") return;
|
|
611
|
+
const failedProcessHandle = bridgeProcessHandle;
|
|
612
|
+
const terminate = terminateProcess && (bridgeState === "starting" || bridgeState === "ready");
|
|
613
|
+
bridgeState = "failed";
|
|
614
|
+
if (bridgeReadyTimeout !== null) globalThis.clearTimeout(bridgeReadyTimeout);
|
|
615
|
+
bridgeReadyTimeout = null;
|
|
616
|
+
const stderr = outputBuffers.stderr.trim().slice(-1e3);
|
|
617
|
+
const error = transportError(
|
|
618
|
+
`${cause instanceof Error ? cause.message : String(cause)}${stderr ? `; ${stderr}` : ""}`,
|
|
619
|
+
cause
|
|
620
|
+
);
|
|
621
|
+
bridgeReadyReject?.(error);
|
|
622
|
+
bridgeReadyReject = null;
|
|
623
|
+
bridgeReadyResolve = null;
|
|
624
|
+
for (const requestId of bridgeRequests.keys()) bridge.onError(requestId, error);
|
|
625
|
+
bridgeRequests.clear();
|
|
626
|
+
if (isRemoteControlHost && terminate) {
|
|
627
|
+
void Promise.resolve(
|
|
628
|
+
originalSend.call(bridge, "process/kill", { processHandle: failedProcessHandle })
|
|
629
|
+
).catch(() => void 0);
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
const resetFailedBridge = () => {
|
|
633
|
+
if (bridgeState !== "failed") return;
|
|
634
|
+
bridgeProcessHandle = createBridgeProcessHandle();
|
|
635
|
+
outputDecoders = { stdout: new TextDecoder(), stderr: new TextDecoder() };
|
|
636
|
+
outputBuffers.stdout = "";
|
|
637
|
+
outputBuffers.stderr = "";
|
|
638
|
+
bridgeReadyPromise = null;
|
|
639
|
+
bridgeReadyResolve = null;
|
|
640
|
+
bridgeReadyReject = null;
|
|
641
|
+
bridgeInitialization = null;
|
|
642
|
+
writeTail = Promise.resolve();
|
|
643
|
+
bridgeServerRequests.clear();
|
|
644
|
+
nextBridgeServerRequestOrdinal = 1;
|
|
645
|
+
bridgeState = "idle";
|
|
646
|
+
};
|
|
647
|
+
const rememberExternalThread = (value) => {
|
|
648
|
+
if (!isRecord5(value) || typeof value.id !== "string") return;
|
|
649
|
+
if (value.modelProvider === "codexhost" || value.cliVersion === "codexhost") {
|
|
650
|
+
knownExternalThreadIds.add(value.id);
|
|
651
|
+
knownOfficialThreadIds.delete(value.id);
|
|
652
|
+
}
|
|
653
|
+
};
|
|
654
|
+
const observeBridgeResult = (request, result) => {
|
|
655
|
+
if (!request || !isRecord5(result)) return;
|
|
656
|
+
if (request.method === "thread/list" && Array.isArray(result.data)) {
|
|
657
|
+
for (const thread of result.data) rememberExternalThread(thread);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
if (request.method === "thread/start" || request.method === "thread/read" || request.method === "thread/resume") {
|
|
661
|
+
rememberExternalThread(result.thread);
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
if (request.method === "codexhost/thread/inspect" && isRecord5(request.parameters) && typeof request.parameters.threadId === "string") {
|
|
665
|
+
if (result.owner === "external") {
|
|
666
|
+
knownExternalThreadIds.add(request.parameters.threadId);
|
|
667
|
+
knownOfficialThreadIds.delete(request.parameters.threadId);
|
|
668
|
+
} else if (result.owner === "codex") {
|
|
669
|
+
knownOfficialThreadIds.add(request.parameters.threadId);
|
|
670
|
+
knownExternalThreadIds.delete(request.parameters.threadId);
|
|
671
|
+
}
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
if (request.method === "thread/delete" && isRecord5(request.parameters) && typeof request.parameters.threadId === "string") {
|
|
675
|
+
knownExternalThreadIds.delete(request.parameters.threadId);
|
|
676
|
+
knownOfficialThreadIds.delete(request.parameters.threadId);
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
const handleBridgeFrame = (value) => {
|
|
680
|
+
if (!isRecord5(value)) {
|
|
681
|
+
failBridge("received a non-object app-server frame");
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (value.method === bridgeReadyMethod && value.id === void 0) {
|
|
685
|
+
if (isRecord5(value.params) && value.params.protocolVersion === 1) {
|
|
686
|
+
bridgeState = "ready";
|
|
687
|
+
if (bridgeReadyTimeout !== null) globalThis.clearTimeout(bridgeReadyTimeout);
|
|
688
|
+
bridgeReadyTimeout = null;
|
|
689
|
+
bridgeReadyResolve?.();
|
|
690
|
+
bridgeReadyResolve = null;
|
|
691
|
+
bridgeReadyReject = null;
|
|
692
|
+
} else {
|
|
693
|
+
failBridge("reported an unsupported protocol version");
|
|
694
|
+
}
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if (typeof value.method === "string" && value.id === void 0) {
|
|
698
|
+
if (value.method === "thread/started" && isRecord5(value.params)) {
|
|
699
|
+
rememberExternalThread(value.params.thread);
|
|
700
|
+
}
|
|
701
|
+
originalOnNotification.call(manager, value.method, value.params);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (typeof value.method === "string" && value.id !== void 0) {
|
|
705
|
+
const outerRequestId = `${bridgeServerRequestIdPrefix}${bridgeProcessHandle}/${nextBridgeServerRequestOrdinal}`;
|
|
706
|
+
nextBridgeServerRequestOrdinal += 1;
|
|
707
|
+
bridgeServerRequests.set(outerRequestId, value.id);
|
|
708
|
+
manager.onRequest({ ...value, id: outerRequestId });
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
if (value.id === void 0) {
|
|
712
|
+
failBridge("received an app-server response without an id");
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const request = bridgeRequests.get(value.id);
|
|
716
|
+
bridgeRequests.delete(value.id);
|
|
717
|
+
if ("error" in value) {
|
|
718
|
+
bridge.onError(value.id, value.error, value.metrics);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
observeBridgeResult(request, value.result);
|
|
722
|
+
bridge.onResult(value.id, value.result, value.metrics);
|
|
723
|
+
};
|
|
724
|
+
const consumeBridgeOutput = (stream, base64) => {
|
|
725
|
+
const binary = atob(base64);
|
|
726
|
+
const bytes = Uint8Array.from(binary, (character) => character.charCodeAt(0));
|
|
727
|
+
outputBuffers[stream] += outputDecoders[stream].decode(bytes, { stream: true });
|
|
728
|
+
if (stream === "stderr") {
|
|
729
|
+
outputBuffers.stderr = outputBuffers.stderr.slice(-4e3);
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
while (true) {
|
|
733
|
+
const newline = outputBuffers.stdout.indexOf("\n");
|
|
734
|
+
if (newline < 0) break;
|
|
735
|
+
const line = outputBuffers.stdout.slice(0, newline).trim();
|
|
736
|
+
outputBuffers.stdout = outputBuffers.stdout.slice(newline + 1);
|
|
737
|
+
if (!line) continue;
|
|
738
|
+
try {
|
|
739
|
+
handleBridgeFrame(JSON.parse(line));
|
|
740
|
+
} catch (error) {
|
|
741
|
+
failBridge(
|
|
742
|
+
`emitted invalid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
743
|
+
);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
};
|
|
748
|
+
const handleOuterNotification = (method, parameters) => {
|
|
749
|
+
if (method === "process/exited" && isRecord5(parameters) && parameters.processHandle === bridgeProcessHandle) {
|
|
750
|
+
const exitCode = typeof parameters.exitCode === "number" ? parameters.exitCode : "unknown";
|
|
751
|
+
const stderr = typeof parameters.stderr === "string" ? parameters.stderr.trim() : "";
|
|
752
|
+
failBridge(
|
|
753
|
+
`process exited unexpectedly with code ${exitCode}${stderr ? `: ${stderr}` : ""}`,
|
|
754
|
+
false
|
|
755
|
+
);
|
|
756
|
+
return true;
|
|
757
|
+
}
|
|
758
|
+
if (method !== "process/outputDelta" || !isRecord5(parameters) || parameters.processHandle !== bridgeProcessHandle || parameters.stream !== "stdout" && parameters.stream !== "stderr" || typeof parameters.deltaBase64 !== "string") {
|
|
759
|
+
return false;
|
|
760
|
+
}
|
|
761
|
+
if (parameters.capReached === true) {
|
|
762
|
+
failBridge(`${parameters.stream} was truncated`);
|
|
763
|
+
return true;
|
|
764
|
+
}
|
|
765
|
+
consumeBridgeOutput(parameters.stream, parameters.deltaBase64);
|
|
766
|
+
return true;
|
|
767
|
+
};
|
|
768
|
+
const startBridge = () => {
|
|
769
|
+
if (!isRemoteControlHost) return Promise.resolve();
|
|
770
|
+
if (bridgeReadyPromise) return bridgeReadyPromise;
|
|
771
|
+
bridgeState = "starting";
|
|
772
|
+
bridgeReadyPromise = new Promise((resolve, reject) => {
|
|
773
|
+
bridgeReadyResolve = resolve;
|
|
774
|
+
bridgeReadyReject = reject;
|
|
775
|
+
});
|
|
776
|
+
bridgeReadyTimeout = globalThis.setTimeout(
|
|
777
|
+
() => failBridge("startup timed out after 15000ms"),
|
|
778
|
+
15e3
|
|
779
|
+
);
|
|
780
|
+
const powershellScript = "$ErrorActionPreference = 'Stop'; $descriptorPath = Join-Path $env:LOCALAPPDATA 'codexhost\\remote-control-bridge-v1.json'; $descriptor = Get-Content -LiteralPath $descriptorPath -Raw | ConvertFrom-Json; if ($descriptor.schemaVersion -ne 1) { throw 'Unsupported codexhost Remote Control descriptor' }; $nodePath = [string]$descriptor.nodePath; $runtimePath = [string]$descriptor.runtimePath; if (-not [System.IO.Path]::IsPathRooted($nodePath) -or -not [System.IO.Path]::IsPathRooted($runtimePath)) { throw 'Invalid codexhost Remote Control runtime path' }; $owner = Get-Process -Id ([int]$descriptor.ownerPid) -ErrorAction SilentlyContinue; if ($null -eq $owner) { throw 'CodexHost Remote Control runtime is not running' }; $env:CODEXHOST_REMOTE_CONTROL_BRIDGE_PIPE = [string]$descriptor.pipePath; & $nodePath $runtimePath '--codexhost-remote-control-bridge'; exit $LASTEXITCODE";
|
|
781
|
+
const command = [
|
|
782
|
+
"powershell.exe",
|
|
783
|
+
"-NoLogo",
|
|
784
|
+
"-NoProfile",
|
|
785
|
+
"-NonInteractive",
|
|
786
|
+
"-EncodedCommand",
|
|
787
|
+
utf16LeBase64(powershellScript)
|
|
788
|
+
];
|
|
789
|
+
const startedProcessHandle = bridgeProcessHandle;
|
|
790
|
+
try {
|
|
791
|
+
const started = originalSend.call(bridge, "process/spawn", {
|
|
792
|
+
command,
|
|
793
|
+
processHandle: startedProcessHandle,
|
|
794
|
+
cwd: "C:\\",
|
|
795
|
+
streamStdin: true,
|
|
796
|
+
streamStdoutStderr: true,
|
|
797
|
+
outputBytesCap: null,
|
|
798
|
+
timeoutMs: null
|
|
799
|
+
});
|
|
800
|
+
void Promise.resolve(started).catch((error) => {
|
|
801
|
+
if (bridgeProcessHandle === startedProcessHandle) failBridge(error, false);
|
|
802
|
+
});
|
|
803
|
+
} catch (error) {
|
|
804
|
+
failBridge(error);
|
|
805
|
+
}
|
|
806
|
+
return bridgeReadyPromise;
|
|
807
|
+
};
|
|
808
|
+
const writeBridgeFrame = (value) => {
|
|
809
|
+
const operation = async () => {
|
|
810
|
+
await startBridge();
|
|
811
|
+
if (bridgeState !== "ready") throw transportError("is not ready");
|
|
812
|
+
await originalSend.call(bridge, "process/writeStdin", {
|
|
813
|
+
processHandle: bridgeProcessHandle,
|
|
814
|
+
deltaBase64: utf8Base64(`${JSON.stringify(value)}
|
|
815
|
+
`)
|
|
816
|
+
});
|
|
817
|
+
};
|
|
818
|
+
const next = writeTail.then(operation, operation);
|
|
819
|
+
writeTail = next.catch(() => void 0);
|
|
820
|
+
return next;
|
|
821
|
+
};
|
|
822
|
+
const enqueueBridgeRequest = (method, parameters, options) => bridge.enqueueRequest(method, parameters, options, (request) => {
|
|
823
|
+
bridgeRequests.set(request.id, { method, parameters });
|
|
824
|
+
void writeBridgeFrame(request).catch((error) => failBridge(error));
|
|
825
|
+
});
|
|
826
|
+
const initializeBridgeProtocol = () => {
|
|
827
|
+
const initialization = enqueueBridgeRequest("initialize", {
|
|
828
|
+
clientInfo: {
|
|
829
|
+
name: "codexhost_remote_control_bridge",
|
|
830
|
+
title: "codexhost Remote Control bridge",
|
|
831
|
+
version: "1"
|
|
832
|
+
},
|
|
833
|
+
capabilities: {
|
|
834
|
+
experimentalApi: true,
|
|
835
|
+
mcpServerOpenaiFormElicitation: true
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
return new Promise((resolve, reject) => {
|
|
839
|
+
const timeout = globalThis.setTimeout(() => {
|
|
840
|
+
const message = "initialization timed out after 15000ms";
|
|
841
|
+
failBridge(message);
|
|
842
|
+
reject(transportError(message));
|
|
843
|
+
}, 15e3);
|
|
844
|
+
void Promise.resolve(initialization).then(
|
|
845
|
+
(value) => {
|
|
846
|
+
globalThis.clearTimeout(timeout);
|
|
847
|
+
resolve(value);
|
|
848
|
+
},
|
|
849
|
+
(error) => {
|
|
850
|
+
globalThis.clearTimeout(timeout);
|
|
851
|
+
reject(error);
|
|
852
|
+
}
|
|
853
|
+
);
|
|
854
|
+
});
|
|
855
|
+
};
|
|
856
|
+
const initializeBridge = () => {
|
|
857
|
+
resetFailedBridge();
|
|
858
|
+
if (bridgeInitialization) return bridgeInitialization;
|
|
859
|
+
bridgeInitialization = startBridge().then(() => initializeBridgeProtocol()).then(() => writeBridgeFrame({ method: "initialized", params: {} }));
|
|
860
|
+
return bridgeInitialization;
|
|
861
|
+
};
|
|
862
|
+
const threadIdFromParameters = (parameters) => {
|
|
863
|
+
if (!isRecord5(parameters)) return null;
|
|
864
|
+
const value = parameters.threadId ?? parameters.conversationId;
|
|
865
|
+
return typeof value === "string" ? value : null;
|
|
866
|
+
};
|
|
867
|
+
const isThreadScopedMethod = (method) => method.startsWith("thread/") || method.startsWith("turn/") || method.startsWith("review/");
|
|
868
|
+
const resolveThreadOwnership = (threadId) => {
|
|
869
|
+
if (knownExternalThreadIds.has(threadId)) return Promise.resolve("external");
|
|
870
|
+
if (knownOfficialThreadIds.has(threadId)) return Promise.resolve("codex");
|
|
871
|
+
const pending = threadOwnershipResolutions.get(threadId);
|
|
872
|
+
if (pending) return pending;
|
|
873
|
+
const resolution = initializeBridge().then(
|
|
874
|
+
() => enqueueBridgeRequest("codexhost/thread/ownership/list", {
|
|
875
|
+
threadIds: [threadId]
|
|
876
|
+
})
|
|
877
|
+
).then((value) => {
|
|
878
|
+
const ownerships = isRecord5(value) && Array.isArray(value.threads) ? value.threads : null;
|
|
879
|
+
const ownership = ownerships?.[0];
|
|
880
|
+
if (!ownerships || !isRecord5(ownership) || ownerships.length !== 1 || ownership.threadId !== threadId || ownership.owner !== "external" && ownership.owner !== "codex") {
|
|
881
|
+
throw transportError("Thread ownership lookup returned an invalid result");
|
|
882
|
+
}
|
|
883
|
+
if (ownership.owner === "external") {
|
|
884
|
+
knownExternalThreadIds.add(threadId);
|
|
885
|
+
knownOfficialThreadIds.delete(threadId);
|
|
886
|
+
return "external";
|
|
887
|
+
}
|
|
888
|
+
knownOfficialThreadIds.add(threadId);
|
|
889
|
+
knownExternalThreadIds.delete(threadId);
|
|
890
|
+
return "codex";
|
|
891
|
+
});
|
|
892
|
+
threadOwnershipResolutions.set(threadId, resolution);
|
|
893
|
+
const clearResolution = () => {
|
|
894
|
+
if (threadOwnershipResolutions.get(threadId) === resolution) {
|
|
895
|
+
threadOwnershipResolutions.delete(threadId);
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
void resolution.then(clearResolution, clearResolution);
|
|
899
|
+
return resolution;
|
|
900
|
+
};
|
|
901
|
+
const shouldResolveThreadOwnership = (method, parameters) => {
|
|
902
|
+
if (!isRemoteControlHost || method.startsWith("codexhost/") || !isThreadScopedMethod(method)) {
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
const threadId = threadIdFromParameters(parameters);
|
|
906
|
+
if (!threadId || knownExternalThreadIds.has(threadId) || knownOfficialThreadIds.has(threadId)) {
|
|
907
|
+
return null;
|
|
908
|
+
}
|
|
909
|
+
return threadId;
|
|
910
|
+
};
|
|
911
|
+
const shouldUseBridge = (method, parameters) => {
|
|
912
|
+
if (!isRemoteControlHost) return false;
|
|
913
|
+
if (method.startsWith("codexhost/")) return true;
|
|
914
|
+
if (method === "thread/list") return true;
|
|
915
|
+
if (method === "thread/start") {
|
|
916
|
+
return isRecord5(parameters) && typeof parameters.model === "string" && parameters.model.startsWith("codexhost/");
|
|
917
|
+
}
|
|
918
|
+
const threadId = threadIdFromParameters(parameters);
|
|
919
|
+
if (threadId && knownExternalThreadIds.has(threadId)) return true;
|
|
920
|
+
if (threadId && knownOfficialThreadIds.has(threadId)) return false;
|
|
921
|
+
return selectedModel !== null && (method.startsWith("thread/") || method.startsWith("turn/") || method.startsWith("review/"));
|
|
922
|
+
};
|
|
923
|
+
const routeThreadStart = (parameters) => {
|
|
924
|
+
if (selectedModel === null || !isRecord5(parameters) || parameters.ephemeral === true) {
|
|
925
|
+
return parameters;
|
|
926
|
+
}
|
|
927
|
+
return { ...parameters, model: selectedModel };
|
|
928
|
+
};
|
|
929
|
+
const routedSend = (method, parameters, options) => {
|
|
930
|
+
const routedParameters = method === "thread/start" ? routeThreadStart(parameters) : parameters;
|
|
931
|
+
const sendBridged = () => initializeBridge().then(
|
|
932
|
+
() => enqueueBridgeRequest(method, routedParameters, options)
|
|
933
|
+
);
|
|
934
|
+
const sendDirect = () => options === void 0 ? originalSend.call(bridge, method, routedParameters) : originalSend.call(bridge, method, routedParameters, options);
|
|
935
|
+
const unresolvedThreadId = shouldResolveThreadOwnership(method, routedParameters);
|
|
936
|
+
if (unresolvedThreadId) {
|
|
937
|
+
return resolveThreadOwnership(unresolvedThreadId).then(
|
|
938
|
+
(owner) => owner === "external" ? sendBridged() : sendDirect()
|
|
939
|
+
);
|
|
940
|
+
}
|
|
941
|
+
return shouldUseBridge(method, routedParameters) ? sendBridged() : sendDirect();
|
|
942
|
+
};
|
|
943
|
+
const routedPrewarm = (parameters, options) => {
|
|
944
|
+
const routedParameters = routeThreadStart(parameters);
|
|
945
|
+
if (shouldUseBridge("thread/start", routedParameters)) {
|
|
946
|
+
return routedSend("thread/start", routedParameters, options);
|
|
947
|
+
}
|
|
948
|
+
return options === void 0 ? originalPrewarm.call(bridge, routedParameters) : originalPrewarm.call(bridge, routedParameters, options);
|
|
949
|
+
};
|
|
950
|
+
bridge.sendRequest = routedSend;
|
|
951
|
+
bridge.prewarmThreadStart = routedPrewarm;
|
|
952
|
+
const routedWindowMessage = (event) => {
|
|
953
|
+
const message = event.data;
|
|
954
|
+
if (!isRecord5(message) || message.type !== "mcp-notification" || message.hostId !== hostId || typeof message.method !== "string") {
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
957
|
+
handleOuterNotification(message.method, message.params);
|
|
958
|
+
};
|
|
959
|
+
const observesWindowNotifications = isRemoteControlHost && typeof target.addEventListener === "function" && typeof target.removeEventListener === "function";
|
|
960
|
+
if (observesWindowNotifications) target.addEventListener?.("message", routedWindowMessage);
|
|
961
|
+
const routedOnNotification = (method, parameters) => {
|
|
962
|
+
if (!handleOuterNotification(method, parameters)) {
|
|
963
|
+
originalOnNotification.call(manager, method, parameters);
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
const routedDispatchAppServerResponse = (method, response) => {
|
|
967
|
+
if (isRemoteControlHost && typeof response.id === "string") {
|
|
968
|
+
const innerRequestId = bridgeServerRequests.get(response.id);
|
|
969
|
+
if (innerRequestId !== void 0 || bridgeServerRequests.has(response.id)) {
|
|
970
|
+
bridgeServerRequests.delete(response.id);
|
|
971
|
+
void writeBridgeFrame({ ...response, id: innerRequestId }).catch(
|
|
972
|
+
(error) => failBridge(error)
|
|
973
|
+
);
|
|
974
|
+
return void 0;
|
|
975
|
+
}
|
|
976
|
+
if (response.id.startsWith(bridgeServerRequestIdPrefix)) return void 0;
|
|
977
|
+
}
|
|
978
|
+
return originalDispatchAppServerResponse.call(manager, method, response);
|
|
979
|
+
};
|
|
980
|
+
if (!observesWindowNotifications) manager.onNotification = routedOnNotification;
|
|
981
|
+
manager.dispatchAppServerResponse = routedDispatchAppServerResponse;
|
|
982
|
+
const policy = Object.freeze({
|
|
983
|
+
state: "ready",
|
|
984
|
+
hostId,
|
|
985
|
+
owns(candidateManager, candidate, candidateHostId, candidatePrewarmedThreadManager) {
|
|
986
|
+
return candidateManager === manager && candidate === bridge && candidateHostId === hostId && candidatePrewarmedThreadManager === prewarmedThreadManager;
|
|
987
|
+
},
|
|
988
|
+
select(model) {
|
|
989
|
+
if (model !== null && (typeof model !== "string" || !model.startsWith("codexhost/"))) {
|
|
990
|
+
throw new Error("Draft route Model must be a codexhost transport carrier");
|
|
991
|
+
}
|
|
992
|
+
if (selectedModel === model) return false;
|
|
993
|
+
selectedModel = model;
|
|
994
|
+
return true;
|
|
995
|
+
},
|
|
996
|
+
clear() {
|
|
997
|
+
prewarmedThreadManager.discardAllPrewarmedThreads();
|
|
998
|
+
return Promise.resolve();
|
|
999
|
+
},
|
|
1000
|
+
dispose() {
|
|
1001
|
+
if (bridge.sendRequest === routedSend) bridge.sendRequest = originalSend;
|
|
1002
|
+
if (bridge.prewarmThreadStart === routedPrewarm) {
|
|
1003
|
+
bridge.prewarmThreadStart = originalPrewarm;
|
|
1004
|
+
}
|
|
1005
|
+
if (observesWindowNotifications) {
|
|
1006
|
+
target.removeEventListener?.("message", routedWindowMessage);
|
|
1007
|
+
} else if (manager.onNotification === routedOnNotification) {
|
|
1008
|
+
manager.onNotification = originalOnNotification;
|
|
1009
|
+
}
|
|
1010
|
+
if (manager.dispatchAppServerResponse === routedDispatchAppServerResponse) {
|
|
1011
|
+
manager.dispatchAppServerResponse = originalDispatchAppServerResponse;
|
|
1012
|
+
}
|
|
1013
|
+
if (bridgeReadyTimeout !== null) globalThis.clearTimeout(bridgeReadyTimeout);
|
|
1014
|
+
bridgeReadyTimeout = null;
|
|
1015
|
+
if (isRemoteControlHost && (bridgeState === "starting" || bridgeState === "ready")) {
|
|
1016
|
+
void Promise.resolve(
|
|
1017
|
+
originalSend.call(bridge, "process/kill", { processHandle: bridgeProcessHandle })
|
|
1018
|
+
).catch(() => void 0);
|
|
1019
|
+
}
|
|
1020
|
+
bridgeState = "disposed";
|
|
1021
|
+
const disposedError = transportError("was disposed");
|
|
1022
|
+
bridgeReadyReject?.(disposedError);
|
|
1023
|
+
for (const requestId of bridgeRequests.keys()) bridge.onError(requestId, disposedError);
|
|
1024
|
+
bridgeRequests.clear();
|
|
1025
|
+
bridgeServerRequests.clear();
|
|
1026
|
+
knownExternalThreadIds.clear();
|
|
1027
|
+
knownOfficialThreadIds.clear();
|
|
1028
|
+
threadOwnershipResolutions.clear();
|
|
1029
|
+
selectedModel = null;
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
Object.defineProperty(target, "__codexhostDraftPrewarmPolicyV1", {
|
|
1033
|
+
configurable: true,
|
|
1034
|
+
value: policy
|
|
1035
|
+
});
|
|
1036
|
+
if (typeof target.dispatchEvent === "function" && typeof CustomEvent === "function") {
|
|
1037
|
+
target.dispatchEvent(new CustomEvent("codexhost:draft-prewarm-policy-changed"));
|
|
1038
|
+
}
|
|
1039
|
+
return { state: "ready", reason: "owned-request-bridge" };
|
|
1040
|
+
}
|
|
1041
|
+
async function installDraftPrewarmPolicyInRenderer(contents, findRequestManagerExpression, installRendererPolicyFunction) {
|
|
1042
|
+
if (contents === null || contents.isDestroyed() || contents.getType() !== "window") {
|
|
1043
|
+
throw new Error("Owned Renderer is unavailable for draft prewarm policy");
|
|
1044
|
+
}
|
|
1045
|
+
let attachedHere = false;
|
|
1046
|
+
try {
|
|
1047
|
+
if (!contents.debugger.isAttached()) {
|
|
1048
|
+
contents.debugger.attach("1.3");
|
|
1049
|
+
attachedHere = true;
|
|
1050
|
+
}
|
|
1051
|
+
await contents.debugger.sendCommand("Runtime.enable");
|
|
1052
|
+
const managerResult = await contents.debugger.sendCommand("Runtime.evaluate", {
|
|
1053
|
+
expression: findRequestManagerExpression
|
|
1054
|
+
});
|
|
1055
|
+
const managerResultId = managerResult.result?.objectId;
|
|
1056
|
+
if (typeof managerResultId !== "string") {
|
|
1057
|
+
throw new Error("Renderer request manager inspection failed");
|
|
1058
|
+
}
|
|
1059
|
+
const managerProperties = await contents.debugger.sendCommand("Runtime.getProperties", {
|
|
1060
|
+
objectId: managerResultId,
|
|
1061
|
+
ownProperties: true
|
|
1062
|
+
});
|
|
1063
|
+
const candidateCount = managerProperties.result?.find(
|
|
1064
|
+
(property) => property.name === "candidateCount"
|
|
1065
|
+
)?.value?.value;
|
|
1066
|
+
const hostId = managerProperties.result?.find((property) => property.name === "hostId")?.value?.value;
|
|
1067
|
+
const manager = managerProperties.result?.find(
|
|
1068
|
+
(property) => property.name === "manager"
|
|
1069
|
+
)?.value;
|
|
1070
|
+
const requestClient = managerProperties.result?.find(
|
|
1071
|
+
(property) => property.name === "requestClient"
|
|
1072
|
+
)?.value;
|
|
1073
|
+
const prewarmedThreadManager = managerProperties.result?.find(
|
|
1074
|
+
(property) => property.name === "prewarmedThreadManager"
|
|
1075
|
+
)?.value;
|
|
1076
|
+
if (candidateCount !== 1 || typeof hostId !== "string" || hostId.length === 0 || typeof manager?.objectId !== "string" || typeof requestClient?.objectId !== "string") {
|
|
1077
|
+
throw new Error("Renderer request manager is ambiguous");
|
|
1078
|
+
}
|
|
1079
|
+
if (typeof prewarmedThreadManager?.objectId !== "string") {
|
|
1080
|
+
throw new Error("Renderer prewarmed Thread manager is unavailable");
|
|
1081
|
+
}
|
|
1082
|
+
const installed = await contents.debugger.sendCommand("Runtime.callFunctionOn", {
|
|
1083
|
+
objectId: manager.objectId,
|
|
1084
|
+
functionDeclaration: installRendererPolicyFunction,
|
|
1085
|
+
arguments: [
|
|
1086
|
+
{ objectId: requestClient.objectId },
|
|
1087
|
+
{ value: hostId },
|
|
1088
|
+
{ objectId: prewarmedThreadManager.objectId }
|
|
1089
|
+
],
|
|
1090
|
+
awaitPromise: true,
|
|
1091
|
+
returnByValue: true
|
|
1092
|
+
});
|
|
1093
|
+
return installed.result?.value;
|
|
1094
|
+
} finally {
|
|
1095
|
+
if (attachedHere && contents.debugger.isAttached()) contents.debugger.detach();
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
// packages/desktop-control/src/renderer-draft-prewarm-policy.ts
|
|
1100
|
+
function selectRendererRequestManager(candidates, activeHostIds) {
|
|
1101
|
+
const unique = /* @__PURE__ */ new Map();
|
|
1102
|
+
for (const candidate of candidates) unique.set(candidate.manager, candidate);
|
|
1103
|
+
const hosts = new Set(
|
|
1104
|
+
activeHostIds.filter((value) => typeof value === "string" && value.length > 0)
|
|
1105
|
+
);
|
|
1106
|
+
if (hosts.size > 1) return null;
|
|
1107
|
+
const activeHostId = hosts.values().next().value;
|
|
1108
|
+
const eligible = [...unique.values()].filter(
|
|
1109
|
+
(candidate) => activeHostId === void 0 || candidate.hostId === activeHostId
|
|
1110
|
+
);
|
|
1111
|
+
return eligible.length === 1 ? eligible[0] ?? null : null;
|
|
1112
|
+
}
|
|
1113
|
+
function isRecord3(value) {
|
|
1114
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1115
|
+
}
|
|
1116
|
+
var FIND_REQUEST_MANAGER_EXPRESSION = `(() => {
|
|
1117
|
+
const editors = [...document.querySelectorAll(
|
|
1118
|
+
'[data-codex-composer], [contenteditable="true"][role="textbox"]',
|
|
1119
|
+
)];
|
|
1120
|
+
if (editors.length !== 1) {
|
|
1121
|
+
return { candidateCount: 0, hostId: null, sendRequest: null };
|
|
1122
|
+
}
|
|
1123
|
+
let element = editors[0];
|
|
1124
|
+
let fiber = null;
|
|
1125
|
+
while (element != null && fiber == null) {
|
|
1126
|
+
const key = Object.getOwnPropertyNames(element).find((name) =>
|
|
1127
|
+
name.startsWith('__reactFiber$'),
|
|
1128
|
+
);
|
|
1129
|
+
if (key != null) fiber = element[key];
|
|
1130
|
+
element = element.parentElement;
|
|
1131
|
+
}
|
|
1132
|
+
const managers = new Set();
|
|
1133
|
+
const activeHostIds = new Set();
|
|
1134
|
+
for (let depth = 0; fiber != null && depth < 200; depth += 1, fiber = fiber.return) {
|
|
1135
|
+
const props = fiber.memoizedProps;
|
|
1136
|
+
if (props != null && typeof props === 'object') {
|
|
1137
|
+
for (const name of ['executionTargetHostId', 'permissionsHostId']) {
|
|
1138
|
+
const value = props[name];
|
|
1139
|
+
if (typeof value === 'string' && value.length > 0) activeHostIds.add(value);
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
let hook = fiber.memoizedState;
|
|
1143
|
+
for (let index = 0; hook != null && index < 120; index += 1, hook = hook.next) {
|
|
1144
|
+
const value = hook.memoizedState;
|
|
1145
|
+
if (
|
|
1146
|
+
value != null &&
|
|
1147
|
+
typeof value === 'object' &&
|
|
1148
|
+
value.requestClient != null &&
|
|
1149
|
+
typeof value.requestClient.prewarmThreadStart === 'function' &&
|
|
1150
|
+
typeof value.requestClient.sendRequest === 'function' &&
|
|
1151
|
+
typeof value.requestClient.enqueueRequest === 'function' &&
|
|
1152
|
+
typeof value.prewarmedThreadManager?.discardAllPrewarmedThreads === 'function' &&
|
|
1153
|
+
typeof value.sendRequest === 'function'
|
|
1154
|
+
) {
|
|
1155
|
+
managers.add(value);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
const candidates = [...managers].map((manager) => {
|
|
1160
|
+
const requestClient =
|
|
1161
|
+
typeof manager.requestClient?.sendRequest === 'function' &&
|
|
1162
|
+
typeof manager.requestClient?.prewarmThreadStart === 'function' &&
|
|
1163
|
+
typeof manager.requestClient?.enqueueRequest === 'function'
|
|
1164
|
+
? manager.requestClient
|
|
1165
|
+
: manager;
|
|
1166
|
+
return {
|
|
1167
|
+
manager,
|
|
1168
|
+
requestClient,
|
|
1169
|
+
hostId: manager?.getHostId?.() ?? requestClient?.hostId ?? null,
|
|
1170
|
+
prewarmedThreadManager: manager?.prewarmedThreadManager ?? null,
|
|
1171
|
+
};
|
|
1172
|
+
});
|
|
1173
|
+
const selected = (${selectRendererRequestManager.toString()})(candidates, [...activeHostIds]);
|
|
1174
|
+
return {
|
|
1175
|
+
candidateCount: selected == null ? candidates.length : 1,
|
|
1176
|
+
hostId: selected?.hostId ?? null,
|
|
1177
|
+
manager: selected?.manager ?? null,
|
|
1178
|
+
requestClient: selected?.requestClient ?? null,
|
|
1179
|
+
prewarmedThreadManager: selected?.prewarmedThreadManager ?? null,
|
|
1180
|
+
};
|
|
1181
|
+
})()`;
|
|
1182
|
+
var INSTALL_RENDERER_POLICY_FUNCTION = `function(requestClient, hostId, prewarmedThreadManager) {
|
|
1183
|
+
return (${installDraftPrewarmPolicyBridge.toString()})(
|
|
1184
|
+
this,
|
|
1185
|
+
requestClient,
|
|
1186
|
+
hostId,
|
|
1187
|
+
window,
|
|
1188
|
+
prewarmedThreadManager,
|
|
1189
|
+
);
|
|
1190
|
+
}`;
|
|
1191
|
+
var REQUEST_MANAGER_WAIT_TIMEOUT_MS = 6e4;
|
|
1192
|
+
var REQUEST_MANAGER_POLL_INTERVAL_MS = 25;
|
|
1193
|
+
function mainProcessInstaller(rendererWebContentsId) {
|
|
1194
|
+
return `async function () {
|
|
1195
|
+
const mainModule = process.mainModule;
|
|
1196
|
+
const electron = mainModule != null && typeof mainModule.require === 'function'
|
|
1197
|
+
? mainModule.require('electron')
|
|
1198
|
+
: process.getBuiltinModule('module').createRequire(process.execPath)('electron');
|
|
1199
|
+
const contents = electron.webContents.fromId(${rendererWebContentsId});
|
|
1200
|
+
return (${installDraftPrewarmPolicyInRenderer.toString()})(
|
|
1201
|
+
contents,
|
|
1202
|
+
${JSON.stringify(FIND_REQUEST_MANAGER_EXPRESSION)},
|
|
1203
|
+
${JSON.stringify(INSTALL_RENDERER_POLICY_FUNCTION)}
|
|
1204
|
+
);
|
|
1205
|
+
}`;
|
|
1206
|
+
}
|
|
1207
|
+
async function installRendererDraftPrewarmPolicy(inspector, rendererWebContentsId) {
|
|
1208
|
+
if (!Number.isInteger(rendererWebContentsId) || rendererWebContentsId <= 0) {
|
|
1209
|
+
throw new Error("Renderer webContents ID must be a positive integer");
|
|
1210
|
+
}
|
|
1211
|
+
const installer = mainProcessInstaller(rendererWebContentsId);
|
|
1212
|
+
const deadline = Date.now() + REQUEST_MANAGER_WAIT_TIMEOUT_MS;
|
|
1213
|
+
while (true) {
|
|
1214
|
+
try {
|
|
1215
|
+
const value = await inspector.evaluate(`(${installer})()`);
|
|
1216
|
+
if (!isRecord3(value) || value.state !== "ready" || value.reason !== "owned-request-bridge") {
|
|
1217
|
+
throw new Error("Renderer draft prewarm policy returned an invalid status");
|
|
1218
|
+
}
|
|
1219
|
+
return value;
|
|
1220
|
+
} catch (error) {
|
|
1221
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1222
|
+
const remaining = deadline - Date.now();
|
|
1223
|
+
if (!message.includes("Renderer request manager is ambiguous") || remaining <= 0) throw error;
|
|
1224
|
+
await new Promise((resolve) => {
|
|
1225
|
+
setTimeout(resolve, Math.min(REQUEST_MANAGER_POLL_INTERVAL_MS, remaining));
|
|
1226
|
+
});
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
// packages/desktop-control/src/renderer-control-session.ts
|
|
1232
|
+
function isRecord4(value) {
|
|
1233
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1234
|
+
}
|
|
1235
|
+
function sleep(milliseconds) {
|
|
1236
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
1237
|
+
}
|
|
1238
|
+
function sameAgents(actual, expected) {
|
|
1239
|
+
return actual.length === expected.length && actual.every((agent, index) => agent === expected[index]);
|
|
1240
|
+
}
|
|
1241
|
+
var RendererAdapterReadinessError = class extends Error {
|
|
1242
|
+
constructor(state, reason) {
|
|
1243
|
+
super(`Production Renderer Adapter is ${state}: ${reason}`);
|
|
1244
|
+
this.state = state;
|
|
1245
|
+
this.reason = reason;
|
|
1246
|
+
this.name = "RendererAdapterReadinessError";
|
|
1247
|
+
}
|
|
1248
|
+
state;
|
|
1249
|
+
reason;
|
|
1250
|
+
};
|
|
1251
|
+
function validateBindingStatus(value, expectedAgents) {
|
|
1252
|
+
if (!isRecord4(value) || value.version !== 2 || !Array.isArray(value.enabledAgents) || value.enabledAgents.some((agent) => typeof agent !== "string") || !sameAgents(value.enabledAgents, expectedAgents) || !isRecord4(value.adapter)) {
|
|
1253
|
+
throw new Error("Production Renderer binding returned an invalid status");
|
|
1254
|
+
}
|
|
1255
|
+
if (value.adapter.state !== "ready" || typeof value.adapter.reason !== "string") {
|
|
1256
|
+
const state = typeof value.adapter.state === "string" ? value.adapter.state : "invalid";
|
|
1257
|
+
const reason = typeof value.adapter.reason === "string" ? value.adapter.reason : "unknown";
|
|
1258
|
+
throw new RendererAdapterReadinessError(state, reason);
|
|
1259
|
+
}
|
|
1260
|
+
return value;
|
|
1261
|
+
}
|
|
1262
|
+
function selectRendererWebContents(contents, preferredRendererId) {
|
|
1263
|
+
const candidates = contents.filter(
|
|
1264
|
+
(item) => item.type === "window" && item.surface === "primary" && item.runtime.available && item.runtime.elementCount !== null
|
|
1265
|
+
).toSorted(
|
|
1266
|
+
(left, right) => (right.runtime.elementCount ?? 0) - (left.runtime.elementCount ?? 0)
|
|
1267
|
+
);
|
|
1268
|
+
const preferred = candidates.find(
|
|
1269
|
+
(candidate) => candidate.id === preferredRendererId && (candidate.runtime.elementCount ?? 0) > 0
|
|
1270
|
+
);
|
|
1271
|
+
if (preferred) return preferred;
|
|
1272
|
+
return candidates.find((candidate) => (candidate.runtime.elementCount ?? 0) > 0) ?? null;
|
|
1273
|
+
}
|
|
1274
|
+
async function waitForRendererTitlePolicyReady(markReadiness, options = {}) {
|
|
1275
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
1276
|
+
const pollIntervalMs = options.pollIntervalMs ?? 250;
|
|
1277
|
+
const now = options.now ?? Date.now;
|
|
1278
|
+
const wait = options.sleep ?? sleep;
|
|
1279
|
+
const deadline = now() + timeoutMs;
|
|
1280
|
+
let lastError;
|
|
1281
|
+
while (now() < deadline) {
|
|
1282
|
+
try {
|
|
1283
|
+
return await markReadiness();
|
|
1284
|
+
} catch (error) {
|
|
1285
|
+
lastError = error;
|
|
1286
|
+
}
|
|
1287
|
+
await wait(pollIntervalMs);
|
|
1288
|
+
}
|
|
1289
|
+
const detail = lastError instanceof Error ? `: ${lastError.message}` : "";
|
|
1290
|
+
throw new Error(`Renderer title policy ownership did not become ready${detail}`);
|
|
1291
|
+
}
|
|
1292
|
+
async function waitForInspectorTarget(endpoint, options = {}) {
|
|
1293
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
1294
|
+
const pollIntervalMs = options.pollIntervalMs ?? 250;
|
|
1295
|
+
const deadline = Date.now() + timeoutMs;
|
|
1296
|
+
let lastError;
|
|
1297
|
+
while (Date.now() < deadline) {
|
|
1298
|
+
try {
|
|
1299
|
+
const targets = await listCdpTargets(endpoint, options.fetchImpl);
|
|
1300
|
+
const inspector = targets.find((target) => target.type === "node");
|
|
1301
|
+
if (inspector) return inspector;
|
|
1302
|
+
lastError = new Error("Inspector has no Node target");
|
|
1303
|
+
} catch (error) {
|
|
1304
|
+
lastError = error;
|
|
1305
|
+
}
|
|
1306
|
+
await sleep(pollIntervalMs);
|
|
1307
|
+
}
|
|
1308
|
+
const detail = lastError instanceof Error ? `: ${lastError.message}` : "";
|
|
1309
|
+
throw new Error(`Electron main-process Inspector did not become ready${detail}`);
|
|
1310
|
+
}
|
|
1311
|
+
var electronModuleExpression = `(() => {
|
|
1312
|
+
const mainModule = process.mainModule;
|
|
1313
|
+
if (mainModule != null && typeof mainModule.require === 'function') {
|
|
1314
|
+
return mainModule.require('electron');
|
|
1315
|
+
}
|
|
1316
|
+
const { createRequire } = process.getBuiltinModule('module');
|
|
1317
|
+
return createRequire(process.execPath)('electron');
|
|
1318
|
+
})()`;
|
|
1319
|
+
var webContentsRuntimeExpression = "(() => ({ elementCount: document.querySelectorAll('*').length }))()";
|
|
1320
|
+
async function inspectElectronWebContents(inspector) {
|
|
1321
|
+
const value = await inspector.evaluate(`(async () => {
|
|
1322
|
+
const { webContents } = ${electronModuleExpression};
|
|
1323
|
+
const result = await Promise.all(webContents.getAllWebContents().map(async (contents) => {
|
|
1324
|
+
let runtime = { available: false, elementCount: null };
|
|
1325
|
+
try {
|
|
1326
|
+
const evaluation = contents.executeJavaScript(${JSON.stringify(webContentsRuntimeExpression)}, true);
|
|
1327
|
+
const timeout = new Promise((_, reject) => {
|
|
1328
|
+
setTimeout(() => reject(new Error('Renderer inspection timed out')), 2_000);
|
|
1329
|
+
});
|
|
1330
|
+
runtime = { available: true, ...(await Promise.race([evaluation, timeout])) };
|
|
1331
|
+
} catch {}
|
|
1332
|
+
return {
|
|
1333
|
+
id: contents.id,
|
|
1334
|
+
type: contents.getType(),
|
|
1335
|
+
surface: contents.getURL().includes('avatar-overlay') ? 'overlay' : 'primary',
|
|
1336
|
+
runtime,
|
|
1337
|
+
};
|
|
1338
|
+
}));
|
|
1339
|
+
return result;
|
|
1340
|
+
})()`);
|
|
1341
|
+
if (!Array.isArray(value)) throw new Error("Electron webContents inspection returned an array");
|
|
1342
|
+
return value.map((item) => {
|
|
1343
|
+
if (!isRecord4(item) || !Number.isInteger(item.id) || typeof item.type !== "string" || !["primary", "overlay"].includes(String(item.surface)) || !isRecord4(item.runtime)) {
|
|
1344
|
+
throw new Error("Electron webContents inspection returned an invalid item");
|
|
1345
|
+
}
|
|
1346
|
+
return {
|
|
1347
|
+
id: item.id,
|
|
1348
|
+
type: item.type,
|
|
1349
|
+
surface: item.surface,
|
|
1350
|
+
runtime: {
|
|
1351
|
+
available: item.runtime.available === true,
|
|
1352
|
+
elementCount: Number.isInteger(item.runtime.elementCount) ? item.runtime.elementCount : null
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
async function activateElectronDesktop(inspector) {
|
|
1358
|
+
const value = await inspector.evaluate(`(() => {
|
|
1359
|
+
const { BrowserWindow } = ${electronModuleExpression};
|
|
1360
|
+
const windows = BrowserWindow.getAllWindows().filter((window) => !window.isDestroyed());
|
|
1361
|
+
for (const window of windows) {
|
|
1362
|
+
if (window.isMinimized()) window.restore();
|
|
1363
|
+
window.show();
|
|
1364
|
+
window.focus();
|
|
1365
|
+
}
|
|
1366
|
+
return windows.length;
|
|
1367
|
+
})()`);
|
|
1368
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
1369
|
+
throw new Error("Electron Desktop activation found no live window");
|
|
1370
|
+
}
|
|
1371
|
+
return value;
|
|
1372
|
+
}
|
|
1373
|
+
async function executeInWebContents(inspector, rendererWebContentsId, source) {
|
|
1374
|
+
return inspector.evaluate(`(async () => {
|
|
1375
|
+
const { webContents } = ${electronModuleExpression};
|
|
1376
|
+
const contents = webContents.fromId(${rendererWebContentsId});
|
|
1377
|
+
if (contents == null || contents.isDestroyed()) throw new Error('Renderer webContents is unavailable');
|
|
1378
|
+
const result = await contents.executeJavaScript(${JSON.stringify(source)}, true);
|
|
1379
|
+
return result === undefined ? null : result;
|
|
1380
|
+
})()`);
|
|
1381
|
+
}
|
|
1382
|
+
async function reloadRenderer(inspector, rendererWebContentsId) {
|
|
1383
|
+
await executeInWebContents(inspector, rendererWebContentsId, "location.reload(); null");
|
|
1384
|
+
}
|
|
1385
|
+
var defaultOperations = {
|
|
1386
|
+
inspect: inspectElectronWebContents,
|
|
1387
|
+
installTitlePolicy: installMainProcessTitlePolicy,
|
|
1388
|
+
markTitlePolicyReady: markRendererTitlePolicyReady,
|
|
1389
|
+
installDraftPrewarmPolicy: installRendererDraftPrewarmPolicy,
|
|
1390
|
+
reload: reloadRenderer,
|
|
1391
|
+
execute: executeInWebContents,
|
|
1392
|
+
readBinding: (inspector, rendererWebContentsId) => executeInWebContents(
|
|
1393
|
+
inspector,
|
|
1394
|
+
rendererWebContentsId,
|
|
1395
|
+
"window.__codexhostRendererBindingProbeV1?.status() ?? null"
|
|
1396
|
+
),
|
|
1397
|
+
readTitlePolicyCounters: readMainProcessTitlePolicyCounters
|
|
1398
|
+
};
|
|
1399
|
+
async function waitForRenderer(inspector, operations, timeoutMs, pollIntervalMs, preferredRendererId) {
|
|
1400
|
+
const deadline = Date.now() + timeoutMs;
|
|
1401
|
+
let candidateCount = 0;
|
|
1402
|
+
while (Date.now() < deadline) {
|
|
1403
|
+
const inventory = await operations.inspect(inspector);
|
|
1404
|
+
candidateCount = inventory.length;
|
|
1405
|
+
const renderer = selectRendererWebContents(inventory, preferredRendererId);
|
|
1406
|
+
if (renderer) return renderer;
|
|
1407
|
+
await sleep(pollIntervalMs);
|
|
1408
|
+
}
|
|
1409
|
+
throw new Error(`Inspector did not find a live Electron Renderer (${candidateCount} seen)`);
|
|
1410
|
+
}
|
|
1411
|
+
async function waitForBinding(inspector, operations, rendererWebContentsId, enabledAgents, timeoutMs, pollIntervalMs) {
|
|
1412
|
+
const deadline = Date.now() + timeoutMs;
|
|
1413
|
+
let lastError;
|
|
1414
|
+
while (Date.now() < deadline) {
|
|
1415
|
+
try {
|
|
1416
|
+
const value = await operations.readBinding(inspector, rendererWebContentsId);
|
|
1417
|
+
if (value !== null) return validateBindingStatus(value, enabledAgents);
|
|
1418
|
+
} catch (error) {
|
|
1419
|
+
lastError = error;
|
|
1420
|
+
if (error instanceof RendererAdapterReadinessError && error.state !== "installing") {
|
|
1421
|
+
throw error;
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
await sleep(pollIntervalMs);
|
|
1425
|
+
}
|
|
1426
|
+
const detail = lastError instanceof Error ? `: ${lastError.message}` : "";
|
|
1427
|
+
throw new Error(`Production Renderer binding did not become ready${detail}`);
|
|
1428
|
+
}
|
|
1429
|
+
var InstalledRendererControlSession = class {
|
|
1430
|
+
constructor(inspector, rendererSource, enabledAgents, timeoutMs, pollIntervalMs, operations, snapshot) {
|
|
1431
|
+
this.inspector = inspector;
|
|
1432
|
+
this.rendererSource = rendererSource;
|
|
1433
|
+
this.enabledAgents = enabledAgents;
|
|
1434
|
+
this.timeoutMs = timeoutMs;
|
|
1435
|
+
this.pollIntervalMs = pollIntervalMs;
|
|
1436
|
+
this.operations = operations;
|
|
1437
|
+
this.#snapshot = snapshot;
|
|
1438
|
+
}
|
|
1439
|
+
inspector;
|
|
1440
|
+
rendererSource;
|
|
1441
|
+
enabledAgents;
|
|
1442
|
+
timeoutMs;
|
|
1443
|
+
pollIntervalMs;
|
|
1444
|
+
operations;
|
|
1445
|
+
#closed = false;
|
|
1446
|
+
#snapshot;
|
|
1447
|
+
get snapshot() {
|
|
1448
|
+
return this.#snapshot;
|
|
1449
|
+
}
|
|
1450
|
+
async ensureInstalled() {
|
|
1451
|
+
if (this.#closed) throw new Error("Renderer Control Session is closed");
|
|
1452
|
+
const selected = await waitForRenderer(
|
|
1453
|
+
this.inspector,
|
|
1454
|
+
this.operations,
|
|
1455
|
+
this.timeoutMs,
|
|
1456
|
+
this.pollIntervalMs,
|
|
1457
|
+
this.#snapshot.renderer.id
|
|
1458
|
+
);
|
|
1459
|
+
const existing = await this.operations.readBinding(this.inspector, selected.id).catch(() => null);
|
|
1460
|
+
if (existing !== null) {
|
|
1461
|
+
const draftPrewarmPolicy2 = await this.operations.installDraftPrewarmPolicy(
|
|
1462
|
+
this.inspector,
|
|
1463
|
+
selected.id
|
|
1464
|
+
);
|
|
1465
|
+
let binding2;
|
|
1466
|
+
try {
|
|
1467
|
+
binding2 = validateBindingStatus(existing, this.enabledAgents);
|
|
1468
|
+
} catch (error) {
|
|
1469
|
+
if (!(error instanceof RendererAdapterReadinessError)) throw error;
|
|
1470
|
+
await this.operations.execute(this.inspector, selected.id, this.rendererSource);
|
|
1471
|
+
binding2 = await waitForBinding(
|
|
1472
|
+
this.inspector,
|
|
1473
|
+
this.operations,
|
|
1474
|
+
selected.id,
|
|
1475
|
+
this.enabledAgents,
|
|
1476
|
+
this.timeoutMs,
|
|
1477
|
+
this.pollIntervalMs
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
this.#snapshot = {
|
|
1481
|
+
...this.#snapshot,
|
|
1482
|
+
renderer: selected,
|
|
1483
|
+
draftPrewarmPolicy: draftPrewarmPolicy2,
|
|
1484
|
+
binding: binding2
|
|
1485
|
+
};
|
|
1486
|
+
return this.#snapshot;
|
|
1487
|
+
}
|
|
1488
|
+
const titlePolicyReadiness = await waitForRendererTitlePolicyReady(
|
|
1489
|
+
() => this.operations.markTitlePolicyReady(this.inspector, selected.id),
|
|
1490
|
+
{ timeoutMs: this.timeoutMs, pollIntervalMs: this.pollIntervalMs }
|
|
1491
|
+
);
|
|
1492
|
+
await activateElectronDesktop(this.inspector);
|
|
1493
|
+
await this.operations.execute(this.inspector, selected.id, this.rendererSource);
|
|
1494
|
+
const draftPrewarmPolicy = await this.operations.installDraftPrewarmPolicy(
|
|
1495
|
+
this.inspector,
|
|
1496
|
+
selected.id
|
|
1497
|
+
);
|
|
1498
|
+
const binding = await waitForBinding(
|
|
1499
|
+
this.inspector,
|
|
1500
|
+
this.operations,
|
|
1501
|
+
selected.id,
|
|
1502
|
+
this.enabledAgents,
|
|
1503
|
+
this.timeoutMs,
|
|
1504
|
+
this.pollIntervalMs
|
|
1505
|
+
);
|
|
1506
|
+
this.#snapshot = {
|
|
1507
|
+
...this.#snapshot,
|
|
1508
|
+
renderer: selected,
|
|
1509
|
+
titlePolicyReadiness,
|
|
1510
|
+
draftPrewarmPolicy,
|
|
1511
|
+
binding
|
|
1512
|
+
};
|
|
1513
|
+
return this.#snapshot;
|
|
1514
|
+
}
|
|
1515
|
+
activateDesktop() {
|
|
1516
|
+
if (this.#closed) return Promise.reject(new Error("Renderer Control Session is closed"));
|
|
1517
|
+
return activateElectronDesktop(this.inspector);
|
|
1518
|
+
}
|
|
1519
|
+
executeRenderer(expression) {
|
|
1520
|
+
if (this.#closed) return Promise.reject(new Error("Renderer Control Session is closed"));
|
|
1521
|
+
return this.operations.execute(
|
|
1522
|
+
this.inspector,
|
|
1523
|
+
this.#snapshot.renderer.id,
|
|
1524
|
+
expression
|
|
1525
|
+
);
|
|
1526
|
+
}
|
|
1527
|
+
readTitlePolicyCounters() {
|
|
1528
|
+
if (this.#closed) return Promise.reject(new Error("Renderer Control Session is closed"));
|
|
1529
|
+
return this.operations.readTitlePolicyCounters(this.inspector);
|
|
1530
|
+
}
|
|
1531
|
+
close() {
|
|
1532
|
+
if (this.#closed) return;
|
|
1533
|
+
this.#closed = true;
|
|
1534
|
+
this.inspector.close();
|
|
1535
|
+
}
|
|
1536
|
+
};
|
|
1537
|
+
var startupTraceStartedAt = Date.now();
|
|
1538
|
+
function startupTrace(stage) {
|
|
1539
|
+
if (process.env.CODEXHOST_STARTUP_TRACE !== "1") return;
|
|
1540
|
+
console.error(
|
|
1541
|
+
`[codexhost startup +${Date.now() - startupTraceStartedAt}ms] renderer-session: ${stage}`
|
|
1542
|
+
);
|
|
1543
|
+
}
|
|
1544
|
+
async function createRendererControlSession(options) {
|
|
1545
|
+
const enabledAgents = options.enabledAgents ?? ["codex", "pi"];
|
|
1546
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
1547
|
+
const pollIntervalMs = options.pollIntervalMs ?? 250;
|
|
1548
|
+
const operations = options.operations ?? defaultOperations;
|
|
1549
|
+
startupTrace("waiting for initial Renderer");
|
|
1550
|
+
const initial = await waitForRenderer(options.inspector, operations, timeoutMs, pollIntervalMs);
|
|
1551
|
+
startupTrace("installing title policy");
|
|
1552
|
+
const titlePolicy = await operations.installTitlePolicy(options.inspector, initial.id);
|
|
1553
|
+
startupTrace("reloading Renderer");
|
|
1554
|
+
await operations.reload(options.inspector, initial.id);
|
|
1555
|
+
startupTrace("waiting for reloaded Renderer");
|
|
1556
|
+
const selected = await waitForRenderer(
|
|
1557
|
+
options.inspector,
|
|
1558
|
+
operations,
|
|
1559
|
+
timeoutMs,
|
|
1560
|
+
pollIntervalMs,
|
|
1561
|
+
initial.id
|
|
1562
|
+
);
|
|
1563
|
+
startupTrace("waiting for title policy readiness");
|
|
1564
|
+
const titlePolicyReadiness = await waitForRendererTitlePolicyReady(
|
|
1565
|
+
() => operations.markTitlePolicyReady(options.inspector, selected.id),
|
|
1566
|
+
{
|
|
1567
|
+
timeoutMs,
|
|
1568
|
+
pollIntervalMs
|
|
1569
|
+
}
|
|
1570
|
+
);
|
|
1571
|
+
startupTrace("injecting Renderer bundle");
|
|
1572
|
+
await operations.execute(options.inspector, selected.id, options.rendererSource);
|
|
1573
|
+
startupTrace("installing draft routing policy");
|
|
1574
|
+
const draftPrewarmPolicy = await operations.installDraftPrewarmPolicy(
|
|
1575
|
+
options.inspector,
|
|
1576
|
+
selected.id
|
|
1577
|
+
);
|
|
1578
|
+
startupTrace("waiting for Renderer binding");
|
|
1579
|
+
const binding = await waitForBinding(
|
|
1580
|
+
options.inspector,
|
|
1581
|
+
operations,
|
|
1582
|
+
selected.id,
|
|
1583
|
+
enabledAgents,
|
|
1584
|
+
timeoutMs,
|
|
1585
|
+
pollIntervalMs
|
|
1586
|
+
);
|
|
1587
|
+
return new InstalledRendererControlSession(
|
|
1588
|
+
options.inspector,
|
|
1589
|
+
options.rendererSource,
|
|
1590
|
+
enabledAgents,
|
|
1591
|
+
timeoutMs,
|
|
1592
|
+
pollIntervalMs,
|
|
1593
|
+
operations,
|
|
1594
|
+
{
|
|
1595
|
+
renderer: selected,
|
|
1596
|
+
titlePolicy,
|
|
1597
|
+
titlePolicyReadiness,
|
|
1598
|
+
draftPrewarmPolicy,
|
|
1599
|
+
binding
|
|
1600
|
+
}
|
|
1601
|
+
);
|
|
1602
|
+
}
|
|
1603
|
+
async function installRendererControlSession(options) {
|
|
1604
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
1605
|
+
const pollIntervalMs = options.pollIntervalMs ?? 250;
|
|
1606
|
+
startupTrace("waiting for Electron Inspector target");
|
|
1607
|
+
const target = await waitForInspectorTarget(options.inspectorEndpoint, {
|
|
1608
|
+
timeoutMs,
|
|
1609
|
+
pollIntervalMs
|
|
1610
|
+
});
|
|
1611
|
+
startupTrace("connecting to Electron Inspector");
|
|
1612
|
+
const inspector = await CdpClient.connect(target.webSocketDebuggerUrl);
|
|
1613
|
+
try {
|
|
1614
|
+
startupTrace("enabling Inspector Runtime domain");
|
|
1615
|
+
await inspector.command("Runtime.enable");
|
|
1616
|
+
return await createRendererControlSession({
|
|
1617
|
+
...options,
|
|
1618
|
+
inspector,
|
|
1619
|
+
timeoutMs,
|
|
1620
|
+
pollIntervalMs
|
|
1621
|
+
});
|
|
1622
|
+
} catch (error) {
|
|
1623
|
+
inspector.close();
|
|
1624
|
+
throw error;
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
// packages/desktop-control/src/production-controller.ts
|
|
1629
|
+
var PRODUCTION_INSTALL_TIMEOUT_MS = 9e4;
|
|
1630
|
+
var DESKTOP_CONTROLLER_READINESS_MAX_BYTES = 512;
|
|
1631
|
+
var TRANSIENT_INSTALL_ATTEMPTS = 3;
|
|
1632
|
+
var TRANSIENT_INSTALL_RETRY_MS = 250;
|
|
1633
|
+
var RECOVERY_RETRY_INITIAL_MS = 3e4;
|
|
1634
|
+
var RECOVERY_RETRY_MAX_MS = 3e5;
|
|
1635
|
+
var startupTraceStartedAt2 = Date.now();
|
|
1636
|
+
function startupTrace2(stage, detail) {
|
|
1637
|
+
if (process.env.CODEXHOST_STARTUP_TRACE !== "1") return;
|
|
1638
|
+
const suffix = detail === void 0 ? "" : `: ${detail instanceof Error ? detail.message : String(detail)}`;
|
|
1639
|
+
console.error(
|
|
1640
|
+
`[codexhost startup +${Date.now() - startupTraceStartedAt2}ms] controller: ${stage}${suffix}`
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
function serializeDesktopControllerReadiness(readiness) {
|
|
1644
|
+
if (readiness.schemaVersion !== 2 || readiness.state !== "compatible" || !Array.isArray(readiness.issues) || readiness.issues.length !== 0 || Object.keys(readiness).length !== 3) {
|
|
1645
|
+
throw new Error("Desktop Controller readiness is invalid");
|
|
1646
|
+
}
|
|
1647
|
+
const line = JSON.stringify(readiness);
|
|
1648
|
+
if (Buffer.byteLength(line, "utf8") > DESKTOP_CONTROLLER_READINESS_MAX_BYTES) {
|
|
1649
|
+
throw new Error("Desktop Controller readiness exceeds its size limit");
|
|
1650
|
+
}
|
|
1651
|
+
return line;
|
|
1652
|
+
}
|
|
1653
|
+
var defaultDependencies = {
|
|
1654
|
+
readRenderer: (filePath) => readFile(filePath, "utf8"),
|
|
1655
|
+
install: installRendererControlSession,
|
|
1656
|
+
startAttachmentServer: startControllerAttachmentServer,
|
|
1657
|
+
ready: (readiness) => {
|
|
1658
|
+
process.stdout.write(`${serializeDesktopControllerReadiness(readiness)}
|
|
1659
|
+
`);
|
|
1660
|
+
},
|
|
1661
|
+
sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
1662
|
+
monitorIntervalMs: 500
|
|
1663
|
+
};
|
|
1664
|
+
function inspectorEndpoint(value) {
|
|
1665
|
+
const url = new URL(value);
|
|
1666
|
+
if (url.protocol !== "http:" || !["127.0.0.1", "localhost", "[::1]"].includes(url.hostname) || !url.port || url.username || url.password || url.pathname !== "/" && url.pathname !== "" || url.search || url.hash) {
|
|
1667
|
+
throw new Error("--inspector-endpoint must be a loopback HTTP origin with an explicit port");
|
|
1668
|
+
}
|
|
1669
|
+
return url.origin;
|
|
1670
|
+
}
|
|
1671
|
+
function parseDesktopControllerArguments(arguments_) {
|
|
1672
|
+
let endpoint;
|
|
1673
|
+
let rendererPath;
|
|
1674
|
+
let defaultAgent;
|
|
1675
|
+
let attachmentPort;
|
|
1676
|
+
let attachmentNonce;
|
|
1677
|
+
for (let index = 0; index < arguments_.length; index += 1) {
|
|
1678
|
+
const argument = arguments_[index];
|
|
1679
|
+
const value = arguments_[index + 1];
|
|
1680
|
+
if (argument === "--inspector-endpoint") {
|
|
1681
|
+
if (endpoint !== void 0) throw new Error("--inspector-endpoint may only be provided once");
|
|
1682
|
+
if (!value) throw new Error("--inspector-endpoint requires a value");
|
|
1683
|
+
endpoint = inspectorEndpoint(value);
|
|
1684
|
+
index += 1;
|
|
1685
|
+
continue;
|
|
1686
|
+
}
|
|
1687
|
+
if (argument === "--renderer") {
|
|
1688
|
+
if (rendererPath !== void 0) throw new Error("--renderer may only be provided once");
|
|
1689
|
+
if (!value) throw new Error("--renderer requires a value");
|
|
1690
|
+
if (!path.isAbsolute(value)) throw new Error("--renderer must be an absolute path");
|
|
1691
|
+
rendererPath = path.normalize(value);
|
|
1692
|
+
index += 1;
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
if (argument === "--default-agent") {
|
|
1696
|
+
if (defaultAgent !== void 0) throw new Error("--default-agent may only be provided once");
|
|
1697
|
+
if (value !== "codex" && value !== "pi") {
|
|
1698
|
+
throw new Error("--default-agent must be 'codex' or 'pi'");
|
|
1699
|
+
}
|
|
1700
|
+
defaultAgent = value;
|
|
1701
|
+
index += 1;
|
|
1702
|
+
continue;
|
|
1703
|
+
}
|
|
1704
|
+
if (argument === "--attachment-port") {
|
|
1705
|
+
if (attachmentPort !== void 0) {
|
|
1706
|
+
throw new Error("--attachment-port may only be provided once");
|
|
1707
|
+
}
|
|
1708
|
+
const port = Number(value);
|
|
1709
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
1710
|
+
throw new Error("--attachment-port must be a valid TCP port");
|
|
1711
|
+
}
|
|
1712
|
+
attachmentPort = port;
|
|
1713
|
+
index += 1;
|
|
1714
|
+
continue;
|
|
1715
|
+
}
|
|
1716
|
+
if (argument === "--attachment-nonce") {
|
|
1717
|
+
if (attachmentNonce !== void 0) {
|
|
1718
|
+
throw new Error("--attachment-nonce may only be provided once");
|
|
1719
|
+
}
|
|
1720
|
+
if (value === void 0 || !/^[0-9a-f]{32}$/.test(value)) {
|
|
1721
|
+
throw new Error("--attachment-nonce must be 32 lowercase hexadecimal characters");
|
|
1722
|
+
}
|
|
1723
|
+
attachmentNonce = value;
|
|
1724
|
+
index += 1;
|
|
1725
|
+
continue;
|
|
1726
|
+
}
|
|
1727
|
+
throw new Error(`unknown Desktop Controller option: ${argument}`);
|
|
1728
|
+
}
|
|
1729
|
+
if (endpoint === void 0) throw new Error("--inspector-endpoint is required");
|
|
1730
|
+
if (rendererPath === void 0) throw new Error("--renderer is required");
|
|
1731
|
+
if (defaultAgent === void 0) throw new Error("--default-agent is required");
|
|
1732
|
+
if (attachmentPort === void 0) throw new Error("--attachment-port is required");
|
|
1733
|
+
if (attachmentNonce === void 0) throw new Error("--attachment-nonce is required");
|
|
1734
|
+
return {
|
|
1735
|
+
inspectorEndpoint: endpoint,
|
|
1736
|
+
rendererPath,
|
|
1737
|
+
defaultAgent,
|
|
1738
|
+
attachmentPort,
|
|
1739
|
+
attachmentNonce
|
|
1740
|
+
};
|
|
1741
|
+
}
|
|
1742
|
+
function isTransientElectronInstallError(error) {
|
|
1743
|
+
let current = error;
|
|
1744
|
+
for (let depth = 0; depth < 4; depth += 1) {
|
|
1745
|
+
const message = current instanceof Error ? current.message : String(current);
|
|
1746
|
+
if (message.includes("Execution context was destroyed") || message.includes("Promise was collected")) {
|
|
1747
|
+
return true;
|
|
1748
|
+
}
|
|
1749
|
+
current = current instanceof Error ? current.cause : void 0;
|
|
1750
|
+
if (current === void 0) break;
|
|
1751
|
+
}
|
|
1752
|
+
return false;
|
|
1753
|
+
}
|
|
1754
|
+
async function installProductionSession(options, dependencies) {
|
|
1755
|
+
for (let attempt = 1; attempt <= TRANSIENT_INSTALL_ATTEMPTS; attempt += 1) {
|
|
1756
|
+
try {
|
|
1757
|
+
return await dependencies.install(options);
|
|
1758
|
+
} catch (error) {
|
|
1759
|
+
if (attempt === TRANSIENT_INSTALL_ATTEMPTS || !isTransientElectronInstallError(error)) {
|
|
1760
|
+
throw error;
|
|
1761
|
+
}
|
|
1762
|
+
await dependencies.sleep(TRANSIENT_INSTALL_RETRY_MS);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
throw new Error("Desktop Controller exhausted Renderer installation attempts");
|
|
1766
|
+
}
|
|
1767
|
+
async function runDesktopController(options, signal, dependencies = defaultDependencies) {
|
|
1768
|
+
const configuration = `Object.defineProperty(window, "__codexhostProductionConfigV1", { configurable: true, value: { defaultAgent: ${JSON.stringify(options.defaultAgent)} } });`;
|
|
1769
|
+
const now = dependencies.now ?? Date.now;
|
|
1770
|
+
let session;
|
|
1771
|
+
let nextRecoveryAt = 0;
|
|
1772
|
+
let recoveryDelayMs = RECOVERY_RETRY_INITIAL_MS;
|
|
1773
|
+
const recordRecoveryFailure = () => {
|
|
1774
|
+
nextRecoveryAt = now() + recoveryDelayMs;
|
|
1775
|
+
recoveryDelayMs = Math.min(recoveryDelayMs * 2, RECOVERY_RETRY_MAX_MS);
|
|
1776
|
+
};
|
|
1777
|
+
const recordRecoverySuccess = () => {
|
|
1778
|
+
nextRecoveryAt = 0;
|
|
1779
|
+
recoveryDelayMs = RECOVERY_RETRY_INITIAL_MS;
|
|
1780
|
+
};
|
|
1781
|
+
const createSession = async () => {
|
|
1782
|
+
startupTrace2("reading Renderer bundle");
|
|
1783
|
+
const rendererSource = await dependencies.readRenderer(options.rendererPath);
|
|
1784
|
+
if (rendererSource.trim().length === 0) throw new Error("production Renderer Bundle is empty");
|
|
1785
|
+
startupTrace2("installing Renderer Session");
|
|
1786
|
+
const installed = await installProductionSession(
|
|
1787
|
+
{
|
|
1788
|
+
inspectorEndpoint: options.inspectorEndpoint,
|
|
1789
|
+
rendererSource: `${configuration}
|
|
1790
|
+
${rendererSource}`,
|
|
1791
|
+
enabledAgents: ["codex", "pi", "claude-code", "deepseek-harness", "grok", "omp"],
|
|
1792
|
+
timeoutMs: PRODUCTION_INSTALL_TIMEOUT_MS
|
|
1793
|
+
},
|
|
1794
|
+
dependencies
|
|
1795
|
+
);
|
|
1796
|
+
startupTrace2("Renderer Session installed");
|
|
1797
|
+
return installed;
|
|
1798
|
+
};
|
|
1799
|
+
startupTrace2("initialization started");
|
|
1800
|
+
try {
|
|
1801
|
+
session = await createSession();
|
|
1802
|
+
recordRecoverySuccess();
|
|
1803
|
+
} catch (error) {
|
|
1804
|
+
startupTrace2("initial Renderer Session unavailable", error);
|
|
1805
|
+
session = void 0;
|
|
1806
|
+
recordRecoveryFailure();
|
|
1807
|
+
}
|
|
1808
|
+
let operation = Promise.resolve(void 0);
|
|
1809
|
+
const useSession = (callback) => {
|
|
1810
|
+
const next = operation.then(callback, callback);
|
|
1811
|
+
operation = next.then(
|
|
1812
|
+
() => void 0,
|
|
1813
|
+
() => void 0
|
|
1814
|
+
);
|
|
1815
|
+
return next;
|
|
1816
|
+
};
|
|
1817
|
+
const resetSession = () => {
|
|
1818
|
+
session?.close();
|
|
1819
|
+
session = void 0;
|
|
1820
|
+
};
|
|
1821
|
+
const ensureSession = async () => {
|
|
1822
|
+
if (!session) session = await createSession();
|
|
1823
|
+
else await session.ensureInstalled();
|
|
1824
|
+
return session;
|
|
1825
|
+
};
|
|
1826
|
+
const recoverSession = async () => {
|
|
1827
|
+
try {
|
|
1828
|
+
const current = await ensureSession();
|
|
1829
|
+
recordRecoverySuccess();
|
|
1830
|
+
return current;
|
|
1831
|
+
} catch (error) {
|
|
1832
|
+
resetSession();
|
|
1833
|
+
recordRecoveryFailure();
|
|
1834
|
+
throw error;
|
|
1835
|
+
}
|
|
1836
|
+
};
|
|
1837
|
+
let attachmentServer;
|
|
1838
|
+
try {
|
|
1839
|
+
startupTrace2("starting attachment server");
|
|
1840
|
+
attachmentServer = await dependencies.startAttachmentServer({
|
|
1841
|
+
port: options.attachmentPort,
|
|
1842
|
+
nonce: options.attachmentNonce,
|
|
1843
|
+
attach: () => useSession(async () => {
|
|
1844
|
+
const current = await recoverSession();
|
|
1845
|
+
await current.activateDesktop();
|
|
1846
|
+
})
|
|
1847
|
+
});
|
|
1848
|
+
startupTrace2("attachment server ready");
|
|
1849
|
+
startupTrace2("publishing readiness");
|
|
1850
|
+
dependencies.ready({
|
|
1851
|
+
schemaVersion: 2,
|
|
1852
|
+
state: "compatible",
|
|
1853
|
+
issues: []
|
|
1854
|
+
});
|
|
1855
|
+
while (!signal.aborted) {
|
|
1856
|
+
await dependencies.sleep(dependencies.monitorIntervalMs);
|
|
1857
|
+
if (signal.aborted) continue;
|
|
1858
|
+
await useSession(async () => {
|
|
1859
|
+
if (!session && now() < nextRecoveryAt) return;
|
|
1860
|
+
try {
|
|
1861
|
+
await recoverSession();
|
|
1862
|
+
} catch {
|
|
1863
|
+
}
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
} finally {
|
|
1867
|
+
await attachmentServer?.close();
|
|
1868
|
+
await operation;
|
|
1869
|
+
resetSession();
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
// packages/desktop-control/src/release-main.ts
|
|
1874
|
+
var abort = new AbortController();
|
|
1875
|
+
var stop = () => abort.abort();
|
|
1876
|
+
process.once("SIGINT", stop);
|
|
1877
|
+
process.once("SIGTERM", stop);
|
|
1878
|
+
try {
|
|
1879
|
+
await runDesktopController(parseDesktopControllerArguments(process.argv.slice(2)), abort.signal);
|
|
1880
|
+
} catch (error) {
|
|
1881
|
+
console.error(
|
|
1882
|
+
`codexhost Desktop Controller: ${error instanceof Error ? error.message : String(error)}`
|
|
1883
|
+
);
|
|
1884
|
+
process.exitCode = 1;
|
|
1885
|
+
} finally {
|
|
1886
|
+
process.removeListener("SIGINT", stop);
|
|
1887
|
+
process.removeListener("SIGTERM", stop);
|
|
1888
|
+
}
|