@codexhost/cli-darwin-arm64 0.1.0-test.2 → 0.1.0
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 +1 -1
- package/app/codexhost-distribution.json +1 -0
- package/app/desktop-controller.mjs +322 -173
- package/app/host-runtime.mjs +1628 -326
- package/app/renderer-extension.js +1404 -483
- package/bin/codexhost +0 -0
- package/libexec/codexhost-shim +0 -0
- package/libexec/codexhost-updater +0 -0
- package/package.json +1 -1
|
@@ -48,14 +48,27 @@ async function startControllerAttachmentServer(options) {
|
|
|
48
48
|
if (newline < 0) return;
|
|
49
49
|
handled = true;
|
|
50
50
|
const line = request.slice(0, newline).replace(/\r$/, "");
|
|
51
|
-
if (line
|
|
52
|
-
|
|
51
|
+
if (line === `ATTACH ${options.nonce}`) {
|
|
52
|
+
void options.attach().then(
|
|
53
|
+
() => respond(socket, "ready"),
|
|
54
|
+
() => respond(socket, "failed")
|
|
55
|
+
);
|
|
53
56
|
return;
|
|
54
57
|
}
|
|
55
|
-
|
|
56
|
-
()
|
|
57
|
-
()
|
|
58
|
-
|
|
58
|
+
if (line === `COMPATIBILITY_UPDATE ${options.nonce}`) {
|
|
59
|
+
socket.setTimeout(2e4);
|
|
60
|
+
void options.compatibilityUpdate().then(
|
|
61
|
+
(outcome) => respond(socket, outcome),
|
|
62
|
+
() => respond(socket, "failed")
|
|
63
|
+
);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (line === `SHUTDOWN ${options.nonce}`) {
|
|
67
|
+
respond(socket, "ready");
|
|
68
|
+
queueMicrotask(() => void options.shutdown().catch(() => void 0));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
respond(socket, "rejected");
|
|
59
72
|
});
|
|
60
73
|
});
|
|
61
74
|
await new Promise((resolve, reject) => {
|
|
@@ -321,32 +334,18 @@ var ELECTRON_MODULE_EXPRESSION = `(() => {
|
|
|
321
334
|
return createRequire(process.execPath)('electron');
|
|
322
335
|
})()`;
|
|
323
336
|
var CONNECT_APP_HOST_CHANNEL = "codex_desktop:connect-app-host";
|
|
337
|
+
var REVIEWED_TITLE_SERVICE_IDENTITIES = ["Dhe", "Nye", "wbe", "nxe"];
|
|
324
338
|
var POLICY_STATE_SYMBOL = "codexhost.main-process-title-policy.v1";
|
|
325
339
|
var SERVICE_OWNER_SYMBOL = "codexhost.main-process-title-policy.owner.v1";
|
|
326
340
|
var RENDERER_READY_EXPRESSION = "(() => { Object.defineProperty(window, '__codexhostMainProcessTitlePolicyV1', { configurable: true, value: { state: 'ready' } }); return 'ready'; })()";
|
|
327
|
-
var INSTALL_POLICY_FUNCTION = `async function () {
|
|
341
|
+
var INSTALL_POLICY_FUNCTION = `async function (rendererWebContentsId) {
|
|
328
342
|
const mainModule = process.mainModule;
|
|
329
343
|
const electron = mainModule != null && typeof mainModule.require === 'function'
|
|
330
344
|
? mainModule.require('electron')
|
|
331
345
|
: process.getBuiltinModule('module').createRequire(process.execPath)('electron');
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (contents.isDestroyed() || contents.getType() !== 'window') continue;
|
|
336
|
-
let elementCount = 0;
|
|
337
|
-
try {
|
|
338
|
-
elementCount = await contents.executeJavaScript(
|
|
339
|
-
"document.querySelectorAll('*').length",
|
|
340
|
-
true,
|
|
341
|
-
);
|
|
342
|
-
} catch {}
|
|
343
|
-
if (Number.isInteger(elementCount) && elementCount > largestElementCount) {
|
|
344
|
-
selected = contents;
|
|
345
|
-
largestElementCount = elementCount;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
if (selected == null || largestElementCount < 50) {
|
|
349
|
-
throw new Error('Populated Renderer unavailable for title policy');
|
|
346
|
+
const selected = electron.webContents.fromId(rendererWebContentsId);
|
|
347
|
+
if (selected == null || selected.isDestroyed() || selected.getType() !== 'window') {
|
|
348
|
+
throw new Error('Owned Renderer unavailable for title policy');
|
|
350
349
|
}
|
|
351
350
|
|
|
352
351
|
const context = this(selected);
|
|
@@ -364,12 +363,22 @@ var INSTALL_POLICY_FUNCTION = `async function () {
|
|
|
364
363
|
const originalGenerateTitle = servicePrototype?.generateTitle;
|
|
365
364
|
if (
|
|
366
365
|
servicePrototype == null ||
|
|
367
|
-
!['Dhe', 'Nye'].includes(sampleService?.constructor?.name) ||
|
|
368
366
|
typeof originalGenerateTitle !== 'function' ||
|
|
369
367
|
!Function.prototype.toString.call(originalGenerateTitle).includes('Failed to generate thread title')
|
|
370
368
|
) {
|
|
371
369
|
throw new Error('ThreadMetadataGenerationService signature mismatch');
|
|
372
370
|
}
|
|
371
|
+
const rawServiceClass = sampleService?.constructor?.name;
|
|
372
|
+
const serviceClass = typeof rawServiceClass === 'string' && /^[A-Za-z_$][A-Za-z0-9_$]{0,63}$/.test(rawServiceClass)
|
|
373
|
+
? rawServiceClass
|
|
374
|
+
: 'unknown';
|
|
375
|
+
const warnings = ${JSON.stringify(REVIEWED_TITLE_SERVICE_IDENTITIES)}.includes(serviceClass)
|
|
376
|
+
? []
|
|
377
|
+
: [{
|
|
378
|
+
capability: 'title-isolation',
|
|
379
|
+
reason: 'unreviewed-title-service-identity',
|
|
380
|
+
observedIdentity: serviceClass,
|
|
381
|
+
}];
|
|
373
382
|
|
|
374
383
|
const counters = {
|
|
375
384
|
codexTitleCalls: 0,
|
|
@@ -443,12 +452,14 @@ var INSTALL_POLICY_FUNCTION = `async function () {
|
|
|
443
452
|
return {
|
|
444
453
|
state: 'ready',
|
|
445
454
|
reason: 'ready',
|
|
446
|
-
contextClass: context.constructor.name,
|
|
447
|
-
serviceClass: sampleService.constructor.name,
|
|
448
455
|
requiresRendererReload: true,
|
|
456
|
+
warnings,
|
|
449
457
|
};
|
|
450
458
|
}`;
|
|
451
|
-
async function installMainProcessTitlePolicy(inspector) {
|
|
459
|
+
async function installMainProcessTitlePolicy(inspector, rendererWebContentsId) {
|
|
460
|
+
if (!Number.isInteger(rendererWebContentsId) || rendererWebContentsId <= 0) {
|
|
461
|
+
throw new Error("Renderer webContents ID must be a positive integer");
|
|
462
|
+
}
|
|
452
463
|
const listenerResponse = resultRecord(
|
|
453
464
|
await inspector.command("Runtime.evaluate", {
|
|
454
465
|
expression: `(${ELECTRON_MODULE_EXPRESSION}).ipcMain.listeners(${JSON.stringify(
|
|
@@ -497,7 +508,8 @@ async function installMainProcessTitlePolicy(inspector) {
|
|
|
497
508
|
const installPromise = resultRecord(
|
|
498
509
|
await inspector.command("Runtime.callFunctionOn", {
|
|
499
510
|
objectId: getContextId,
|
|
500
|
-
functionDeclaration: INSTALL_POLICY_FUNCTION
|
|
511
|
+
functionDeclaration: INSTALL_POLICY_FUNCTION,
|
|
512
|
+
arguments: [{ value: rendererWebContentsId }]
|
|
501
513
|
}),
|
|
502
514
|
"Runtime.callFunctionOn"
|
|
503
515
|
);
|
|
@@ -514,12 +526,17 @@ async function installMainProcessTitlePolicy(inspector) {
|
|
|
514
526
|
);
|
|
515
527
|
const remoteResult = installResponse.result;
|
|
516
528
|
const value = isRecord2(remoteResult) ? remoteResult.value : null;
|
|
517
|
-
if (!isRecord2(value) || value.state !== "ready" || value.reason !== "ready" ||
|
|
529
|
+
if (!isRecord2(value) || value.state !== "ready" || value.reason !== "ready" || value.requiresRendererReload !== true || !Array.isArray(value.warnings) || value.warnings.length > 1 || value.warnings.some(
|
|
530
|
+
(warning) => !isRecord2(warning) || warning.capability !== "title-isolation" || warning.reason !== "unreviewed-title-service-identity" || typeof warning.observedIdentity !== "string" || !/^[A-Za-z_$][A-Za-z0-9_$]{0,63}$/.test(warning.observedIdentity) || Object.keys(warning).length !== 3
|
|
531
|
+
)) {
|
|
518
532
|
throw new Error("Main-process title policy returned an invalid status");
|
|
519
533
|
}
|
|
520
534
|
return value;
|
|
521
535
|
}
|
|
522
|
-
async function markRendererTitlePolicyReady(inspector) {
|
|
536
|
+
async function markRendererTitlePolicyReady(inspector, rendererWebContentsId) {
|
|
537
|
+
if (!Number.isInteger(rendererWebContentsId) || rendererWebContentsId <= 0) {
|
|
538
|
+
throw new Error("Renderer webContents ID must be a positive integer");
|
|
539
|
+
}
|
|
523
540
|
const value = await inspector.evaluate(`(async () => {
|
|
524
541
|
const state = globalThis[Symbol.for(${JSON.stringify(POLICY_STATE_SYMBOL)})];
|
|
525
542
|
if (state == null) throw new Error('Main-process title policy is unavailable');
|
|
@@ -527,28 +544,9 @@ async function markRendererTitlePolicyReady(inspector) {
|
|
|
527
544
|
const electron = mainModule != null && typeof mainModule.require === 'function'
|
|
528
545
|
? mainModule.require('electron')
|
|
529
546
|
: process.getBuiltinModule('module').createRequire(process.execPath)('electron');
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
if (contents.isDestroyed() || contents.getType() !== 'window') continue;
|
|
534
|
-
let elementCount = 0;
|
|
535
|
-
try {
|
|
536
|
-
const evaluation = contents.executeJavaScript(
|
|
537
|
-
"document.querySelectorAll('*').length",
|
|
538
|
-
true,
|
|
539
|
-
);
|
|
540
|
-
const timeout = new Promise((_, reject) => {
|
|
541
|
-
setTimeout(() => reject(new Error('Renderer readiness inspection timed out')), 2_000);
|
|
542
|
-
});
|
|
543
|
-
elementCount = await Promise.race([evaluation, timeout]);
|
|
544
|
-
} catch {}
|
|
545
|
-
if (Number.isInteger(elementCount) && elementCount > largestElementCount) {
|
|
546
|
-
selected = contents;
|
|
547
|
-
largestElementCount = elementCount;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
if (selected == null || largestElementCount < 50) {
|
|
551
|
-
throw new Error('Populated Renderer unavailable for title policy readiness');
|
|
547
|
+
const selected = electron.webContents.fromId(${rendererWebContentsId});
|
|
548
|
+
if (selected == null || selected.isDestroyed() || selected.getType() !== 'window') {
|
|
549
|
+
throw new Error('Owned Renderer unavailable for title policy readiness');
|
|
552
550
|
}
|
|
553
551
|
if (!state.ownedWebContentsIds.has(selected.id)) {
|
|
554
552
|
throw new Error('Renderer metadata service ownership is unavailable');
|
|
@@ -626,55 +624,78 @@ async function installDraftPrewarmPolicyInRenderer(contents, findRequestManagerE
|
|
|
626
624
|
(property) => property.name === "candidateCount"
|
|
627
625
|
)?.value?.value;
|
|
628
626
|
const hostId = managerProperties.result?.find((property) => property.name === "hostId")?.value?.value;
|
|
629
|
-
const
|
|
630
|
-
(property) => property.name === "
|
|
627
|
+
const manager = managerProperties.result?.find(
|
|
628
|
+
(property) => property.name === "manager"
|
|
631
629
|
)?.value;
|
|
632
|
-
if (candidateCount !== 1 || hostId !== "local" || typeof
|
|
630
|
+
if (candidateCount !== 1 || hostId !== "local" || typeof manager?.objectId !== "string") {
|
|
633
631
|
throw new Error("Renderer request manager is ambiguous");
|
|
634
632
|
}
|
|
633
|
+
const managerFunctions = await contents.debugger.sendCommand("Runtime.getProperties", {
|
|
634
|
+
objectId: manager.objectId
|
|
635
|
+
});
|
|
636
|
+
let managerSendRequestId = managerFunctions.result?.find(
|
|
637
|
+
(property) => property.name === "sendRequest"
|
|
638
|
+
)?.value?.objectId;
|
|
639
|
+
const managerPrototypeId = managerFunctions.internalProperties?.find(
|
|
640
|
+
(property) => property.name === "[[Prototype]]"
|
|
641
|
+
)?.value?.objectId;
|
|
642
|
+
if (typeof managerSendRequestId !== "string" && typeof managerPrototypeId === "string") {
|
|
643
|
+
const prototypeFunctions = await contents.debugger.sendCommand("Runtime.getProperties", {
|
|
644
|
+
objectId: managerPrototypeId,
|
|
645
|
+
ownProperties: true
|
|
646
|
+
});
|
|
647
|
+
managerSendRequestId = prototypeFunctions.result?.find(
|
|
648
|
+
(property) => property.name === "sendRequest"
|
|
649
|
+
)?.value?.objectId;
|
|
650
|
+
}
|
|
651
|
+
if (typeof managerSendRequestId !== "string") {
|
|
652
|
+
throw new Error("Renderer request manager sendRequest is unavailable");
|
|
653
|
+
}
|
|
635
654
|
const functionProperties = await contents.debugger.sendCommand("Runtime.getProperties", {
|
|
636
|
-
objectId:
|
|
655
|
+
objectId: managerSendRequestId
|
|
637
656
|
});
|
|
638
657
|
const scopesId = functionProperties.internalProperties?.find(
|
|
639
658
|
(property) => property.name === "[[Scopes]]"
|
|
640
659
|
)?.value?.objectId;
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
objectId: scopesId,
|
|
646
|
-
ownProperties: true
|
|
647
|
-
});
|
|
648
|
-
const bridgeCandidates = [];
|
|
649
|
-
for (const scope of scopes.result ?? []) {
|
|
650
|
-
const scopeId = scope.value?.objectId;
|
|
651
|
-
if (typeof scopeId !== "string") continue;
|
|
652
|
-
const scopeProperties = await contents.debugger.sendCommand("Runtime.getProperties", {
|
|
653
|
-
objectId: scopeId,
|
|
660
|
+
const hostBridgeCandidates = [];
|
|
661
|
+
if (typeof scopesId === "string") {
|
|
662
|
+
const scopes = await contents.debugger.sendCommand("Runtime.getProperties", {
|
|
663
|
+
objectId: scopesId,
|
|
654
664
|
ownProperties: true
|
|
655
665
|
});
|
|
656
|
-
const
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
666
|
+
for (const scope of scopes.result ?? []) {
|
|
667
|
+
if (typeof scope.value?.objectId !== "string") continue;
|
|
668
|
+
const scopeProperties = await contents.debugger.sendCommand("Runtime.getProperties", {
|
|
669
|
+
objectId: scope.value.objectId,
|
|
670
|
+
ownProperties: true
|
|
671
|
+
});
|
|
672
|
+
for (const property of scopeProperties.result ?? []) {
|
|
673
|
+
if (property.value?.type !== "object" || typeof property.value.objectId !== "string") {
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
const candidateSignature = await contents.debugger.sendCommand(
|
|
677
|
+
"Runtime.callFunctionOn",
|
|
678
|
+
{
|
|
679
|
+
objectId: property.value.objectId,
|
|
680
|
+
functionDeclaration: "function(){const send=this.sendRequest;return typeof send==='function'&&Function.prototype.toString.call(send).includes('messageHandler')}",
|
|
681
|
+
returnByValue: true
|
|
682
|
+
}
|
|
683
|
+
);
|
|
684
|
+
if (candidateSignature.result?.value === true) {
|
|
685
|
+
hostBridgeCandidates.push({
|
|
686
|
+
name: String(property.name),
|
|
687
|
+
objectId: property.value.objectId
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
}
|
|
661
691
|
}
|
|
662
692
|
}
|
|
663
|
-
const
|
|
664
|
-
if (
|
|
665
|
-
throw new Error("Renderer request bridge is ambiguous");
|
|
666
|
-
}
|
|
667
|
-
const signature = await contents.debugger.sendCommand("Runtime.callFunctionOn", {
|
|
668
|
-
objectId: bridge.objectId,
|
|
669
|
-
functionDeclaration: "function(){return {arity:this.length,source:Function.prototype.toString.call(this)}}",
|
|
670
|
-
returnByValue: true
|
|
671
|
-
});
|
|
672
|
-
const signatureValue = signature.result?.value;
|
|
673
|
-
if (signatureValue?.arity !== 2 || typeof signatureValue.source !== "string" || !signatureValue.source.includes(".sendRequest")) {
|
|
674
|
-
throw new Error("Renderer request bridge signature mismatch");
|
|
693
|
+
const hostBridge = hostBridgeCandidates[0];
|
|
694
|
+
if (hostBridgeCandidates.length !== 1 || !hostBridge) {
|
|
695
|
+
throw new Error("Renderer Host request bridge is ambiguous");
|
|
675
696
|
}
|
|
676
697
|
const installed = await contents.debugger.sendCommand("Runtime.callFunctionOn", {
|
|
677
|
-
objectId:
|
|
698
|
+
objectId: hostBridge.objectId,
|
|
678
699
|
functionDeclaration: installRendererPolicyFunction,
|
|
679
700
|
arguments: [{ value: hostId }],
|
|
680
701
|
awaitPromise: true,
|
|
@@ -732,11 +753,13 @@ var FIND_REQUEST_MANAGER_EXPRESSION = `(() => {
|
|
|
732
753
|
return {
|
|
733
754
|
candidateCount: managers.size,
|
|
734
755
|
hostId: manager?.getHostId?.() ?? null,
|
|
735
|
-
|
|
756
|
+
manager,
|
|
736
757
|
};
|
|
737
758
|
})()`;
|
|
738
759
|
var INSTALL_RENDERER_POLICY_FUNCTION = `function(hostId) {
|
|
739
|
-
|
|
760
|
+
const bridge = this;
|
|
761
|
+
const send = (method, parameters) => bridge.sendRequest(method, parameters);
|
|
762
|
+
return (${installDraftPrewarmPolicyBridge.toString()})(send, hostId, window);
|
|
740
763
|
}`;
|
|
741
764
|
function mainProcessInstaller(rendererWebContentsId) {
|
|
742
765
|
return `async function () {
|
|
@@ -765,21 +788,30 @@ async function installRendererDraftPrewarmPolicy(inspector, rendererWebContentsI
|
|
|
765
788
|
}
|
|
766
789
|
|
|
767
790
|
// packages/desktop-control/src/renderer-control-session.ts
|
|
791
|
+
var RendererCompatibilityError = class extends Error {
|
|
792
|
+
constructor(capability, reason, cause) {
|
|
793
|
+
super(`Renderer compatibility boundary unavailable: ${capability}`, { cause });
|
|
794
|
+
this.capability = capability;
|
|
795
|
+
this.reason = reason;
|
|
796
|
+
this.name = "RendererCompatibilityError";
|
|
797
|
+
}
|
|
798
|
+
capability;
|
|
799
|
+
reason;
|
|
800
|
+
};
|
|
768
801
|
function isRecord4(value) {
|
|
769
802
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
770
803
|
}
|
|
771
|
-
function safeTargetUrl(value) {
|
|
772
|
-
if (typeof value !== "string" || value.length === 0) return "unknown";
|
|
773
|
-
try {
|
|
774
|
-
const url = new URL(value);
|
|
775
|
-
return url.protocol === "app:" ? `${url.protocol}//${url.host}${url.pathname}` : url.protocol;
|
|
776
|
-
} catch {
|
|
777
|
-
return "unknown";
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
804
|
function sleep(milliseconds) {
|
|
781
805
|
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
782
806
|
}
|
|
807
|
+
async function requireCompatibilityBoundary(capability, reason, operation) {
|
|
808
|
+
try {
|
|
809
|
+
return await operation();
|
|
810
|
+
} catch (error) {
|
|
811
|
+
if (error instanceof RendererCompatibilityError) throw error;
|
|
812
|
+
throw new RendererCompatibilityError(capability, reason, error);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
783
815
|
function sameAgents(actual, expected) {
|
|
784
816
|
return actual.length === expected.length && actual.every((agent, index) => agent === expected[index]);
|
|
785
817
|
}
|
|
@@ -810,8 +842,7 @@ function selectRendererWebContents(contents) {
|
|
|
810
842
|
).toSorted(
|
|
811
843
|
(left, right) => (right.runtime.elementCount ?? 0) - (left.runtime.elementCount ?? 0)
|
|
812
844
|
);
|
|
813
|
-
|
|
814
|
-
return selected && (selected.runtime.elementCount ?? 0) >= 50 ? selected : null;
|
|
845
|
+
return candidates.find((candidate) => (candidate.runtime.elementCount ?? 0) > 0) ?? null;
|
|
815
846
|
}
|
|
816
847
|
async function waitForRendererTitlePolicyReady(markReadiness, options = {}) {
|
|
817
848
|
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
@@ -858,17 +889,13 @@ var electronModuleExpression = `(() => {
|
|
|
858
889
|
const { createRequire } = process.getBuiltinModule('module');
|
|
859
890
|
return createRequire(process.execPath)('electron');
|
|
860
891
|
})()`;
|
|
861
|
-
var webContentsRuntimeExpression =
|
|
862
|
-
elementCount: document.querySelectorAll('*').length,
|
|
863
|
-
editorCandidates: document.querySelectorAll('textarea, [contenteditable="true"], [role="textbox"]').length,
|
|
864
|
-
sendButtonCandidates: [...document.querySelectorAll('button')].filter((button) => button.type === 'submit').length
|
|
865
|
-
}))()`;
|
|
892
|
+
var webContentsRuntimeExpression = "(() => ({ elementCount: document.querySelectorAll('*').length }))()";
|
|
866
893
|
async function inspectElectronWebContents(inspector) {
|
|
867
894
|
const value = await inspector.evaluate(`(async () => {
|
|
868
895
|
const { webContents } = ${electronModuleExpression};
|
|
869
896
|
const result = [];
|
|
870
897
|
for (const contents of webContents.getAllWebContents()) {
|
|
871
|
-
let runtime = { available: false, elementCount: null
|
|
898
|
+
let runtime = { available: false, elementCount: null };
|
|
872
899
|
try {
|
|
873
900
|
const evaluation = contents.executeJavaScript(${JSON.stringify(webContentsRuntimeExpression)}, true);
|
|
874
901
|
const timeout = new Promise((_, reject) => {
|
|
@@ -880,7 +907,6 @@ async function inspectElectronWebContents(inspector) {
|
|
|
880
907
|
id: contents.id,
|
|
881
908
|
type: contents.getType(),
|
|
882
909
|
surface: contents.getURL().includes('avatar-overlay') ? 'overlay' : 'primary',
|
|
883
|
-
url: contents.getURL(),
|
|
884
910
|
runtime,
|
|
885
911
|
});
|
|
886
912
|
}
|
|
@@ -895,12 +921,9 @@ async function inspectElectronWebContents(inspector) {
|
|
|
895
921
|
id: item.id,
|
|
896
922
|
type: item.type,
|
|
897
923
|
surface: item.surface,
|
|
898
|
-
url: safeTargetUrl(item.url),
|
|
899
924
|
runtime: {
|
|
900
925
|
available: item.runtime.available === true,
|
|
901
|
-
elementCount: Number.isInteger(item.runtime.elementCount) ? item.runtime.elementCount : null
|
|
902
|
-
editorCandidates: Number.isInteger(item.runtime.editorCandidates) ? item.runtime.editorCandidates : null,
|
|
903
|
-
sendButtonCandidates: Number.isInteger(item.runtime.sendButtonCandidates) ? item.runtime.sendButtonCandidates : null
|
|
926
|
+
elementCount: Number.isInteger(item.runtime.elementCount) ? item.runtime.elementCount : null
|
|
904
927
|
}
|
|
905
928
|
};
|
|
906
929
|
});
|
|
@@ -921,6 +944,14 @@ async function activateElectronDesktop(inspector) {
|
|
|
921
944
|
}
|
|
922
945
|
return value;
|
|
923
946
|
}
|
|
947
|
+
async function quitElectronDesktop(inspector) {
|
|
948
|
+
const accepted = await inspector.evaluate(`(() => {
|
|
949
|
+
const { app } = ${electronModuleExpression};
|
|
950
|
+
setTimeout(() => app.quit(), 100);
|
|
951
|
+
return true;
|
|
952
|
+
})()`);
|
|
953
|
+
if (accepted !== true) throw new Error("Electron Desktop did not accept the quit request");
|
|
954
|
+
}
|
|
924
955
|
async function executeInWebContents(inspector, rendererWebContentsId, source) {
|
|
925
956
|
return inspector.evaluate(`(async () => {
|
|
926
957
|
const { webContents } = ${electronModuleExpression};
|
|
@@ -945,20 +976,20 @@ var defaultOperations = {
|
|
|
945
976
|
rendererWebContentsId,
|
|
946
977
|
"window.__codexhostRendererBindingProbeV1?.status() ?? null"
|
|
947
978
|
),
|
|
948
|
-
readTitlePolicyCounters: readMainProcessTitlePolicyCounters
|
|
979
|
+
readTitlePolicyCounters: readMainProcessTitlePolicyCounters,
|
|
980
|
+
quitDesktop: quitElectronDesktop
|
|
949
981
|
};
|
|
950
982
|
async function waitForRenderer(inspector, operations, timeoutMs, pollIntervalMs) {
|
|
951
983
|
const deadline = Date.now() + timeoutMs;
|
|
952
|
-
let
|
|
984
|
+
let candidateCount = 0;
|
|
953
985
|
while (Date.now() < deadline) {
|
|
954
|
-
inventory = await operations.inspect(inspector);
|
|
986
|
+
const inventory = await operations.inspect(inspector);
|
|
987
|
+
candidateCount = inventory.length;
|
|
955
988
|
const renderer = selectRendererWebContents(inventory);
|
|
956
|
-
if (renderer) return
|
|
989
|
+
if (renderer) return renderer;
|
|
957
990
|
await sleep(pollIntervalMs);
|
|
958
991
|
}
|
|
959
|
-
throw new Error(
|
|
960
|
-
`Inspector did not find a populated Electron Renderer (${inventory.length} seen)`
|
|
961
|
-
);
|
|
992
|
+
throw new Error(`Inspector did not find a live Electron Renderer (${candidateCount} seen)`);
|
|
962
993
|
}
|
|
963
994
|
async function waitForBinding(inspector, operations, rendererWebContentsId, enabledAgents, timeoutMs, pollIntervalMs) {
|
|
964
995
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -1007,32 +1038,45 @@ var InstalledRendererControlSession = class {
|
|
|
1007
1038
|
this.timeoutMs,
|
|
1008
1039
|
this.pollIntervalMs
|
|
1009
1040
|
);
|
|
1010
|
-
const existing = await this.operations.readBinding(this.inspector, selected.
|
|
1041
|
+
const existing = await this.operations.readBinding(this.inspector, selected.id).catch(() => null);
|
|
1011
1042
|
if (existing !== null) {
|
|
1012
|
-
const binding2 =
|
|
1013
|
-
|
|
1043
|
+
const binding2 = await requireCompatibilityBoundary(
|
|
1044
|
+
"agent-routing",
|
|
1045
|
+
"agent-routing-structure-unavailable",
|
|
1046
|
+
() => validateBindingStatus(existing, this.enabledAgents)
|
|
1047
|
+
);
|
|
1048
|
+
this.#snapshot = { ...this.#snapshot, renderer: selected, binding: binding2 };
|
|
1014
1049
|
return this.#snapshot;
|
|
1015
1050
|
}
|
|
1016
|
-
const titlePolicyReadiness = await
|
|
1017
|
-
|
|
1018
|
-
|
|
1051
|
+
const titlePolicyReadiness = await requireCompatibilityBoundary(
|
|
1052
|
+
"title-isolation",
|
|
1053
|
+
"title-isolation-structure-unavailable",
|
|
1054
|
+
() => waitForRendererTitlePolicyReady(
|
|
1055
|
+
() => this.operations.markTitlePolicyReady(this.inspector, selected.id),
|
|
1056
|
+
{ timeoutMs: this.timeoutMs, pollIntervalMs: this.pollIntervalMs }
|
|
1057
|
+
)
|
|
1019
1058
|
);
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1059
|
+
await this.operations.execute(this.inspector, selected.id, this.rendererSource);
|
|
1060
|
+
const binding = await requireCompatibilityBoundary(
|
|
1061
|
+
"agent-routing",
|
|
1062
|
+
"agent-routing-structure-unavailable",
|
|
1063
|
+
() => waitForBinding(
|
|
1064
|
+
this.inspector,
|
|
1065
|
+
this.operations,
|
|
1066
|
+
selected.id,
|
|
1067
|
+
this.enabledAgents,
|
|
1068
|
+
this.timeoutMs,
|
|
1069
|
+
this.pollIntervalMs
|
|
1070
|
+
)
|
|
1023
1071
|
);
|
|
1024
|
-
await
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
this.operations,
|
|
1028
|
-
selected.renderer.id,
|
|
1029
|
-
this.enabledAgents,
|
|
1030
|
-
this.timeoutMs,
|
|
1031
|
-
this.pollIntervalMs
|
|
1072
|
+
const draftPrewarmPolicy = await requireCompatibilityBoundary(
|
|
1073
|
+
"draft-routing",
|
|
1074
|
+
"draft-routing-structure-unavailable",
|
|
1075
|
+
() => this.operations.installDraftPrewarmPolicy(this.inspector, selected.id)
|
|
1032
1076
|
);
|
|
1033
1077
|
this.#snapshot = {
|
|
1034
1078
|
...this.#snapshot,
|
|
1035
|
-
|
|
1079
|
+
renderer: selected,
|
|
1036
1080
|
titlePolicyReadiness,
|
|
1037
1081
|
draftPrewarmPolicy,
|
|
1038
1082
|
binding
|
|
@@ -1043,6 +1087,19 @@ var InstalledRendererControlSession = class {
|
|
|
1043
1087
|
if (this.#closed) return Promise.reject(new Error("Renderer Control Session is closed"));
|
|
1044
1088
|
return activateElectronDesktop(this.inspector);
|
|
1045
1089
|
}
|
|
1090
|
+
quitDesktop() {
|
|
1091
|
+
if (this.#closed) return Promise.reject(new Error("Renderer Control Session is closed"));
|
|
1092
|
+
return this.operations.quitDesktop(this.inspector);
|
|
1093
|
+
}
|
|
1094
|
+
async requestCompatibilityUpdate() {
|
|
1095
|
+
const value = await this.executeRenderer(
|
|
1096
|
+
"window.__codexhostRendererBindingProbeV1?.requestCompatibilityUpdate?.() ?? 'unavailable'"
|
|
1097
|
+
);
|
|
1098
|
+
if (value !== "update-started" && value !== "current" && value !== "unavailable") {
|
|
1099
|
+
throw new Error("Renderer returned an invalid compatibility update outcome");
|
|
1100
|
+
}
|
|
1101
|
+
return value;
|
|
1102
|
+
}
|
|
1046
1103
|
executeRenderer(expression) {
|
|
1047
1104
|
if (this.#closed) return Promise.reject(new Error("Renderer Control Session is closed"));
|
|
1048
1105
|
return this.operations.execute(
|
|
@@ -1067,25 +1124,41 @@ async function createRendererControlSession(options) {
|
|
|
1067
1124
|
const pollIntervalMs = options.pollIntervalMs ?? 250;
|
|
1068
1125
|
const operations = options.operations ?? defaultOperations;
|
|
1069
1126
|
const initial = await waitForRenderer(options.inspector, operations, timeoutMs, pollIntervalMs);
|
|
1070
|
-
const titlePolicy = await
|
|
1071
|
-
|
|
1127
|
+
const titlePolicy = await requireCompatibilityBoundary(
|
|
1128
|
+
"title-isolation",
|
|
1129
|
+
"title-isolation-structure-unavailable",
|
|
1130
|
+
() => operations.installTitlePolicy(options.inspector, initial.id)
|
|
1131
|
+
);
|
|
1132
|
+
await operations.reload(options.inspector, initial.id);
|
|
1072
1133
|
const selected = await waitForRenderer(options.inspector, operations, timeoutMs, pollIntervalMs);
|
|
1073
|
-
const titlePolicyReadiness = await
|
|
1074
|
-
|
|
1075
|
-
|
|
1134
|
+
const titlePolicyReadiness = await requireCompatibilityBoundary(
|
|
1135
|
+
"title-isolation",
|
|
1136
|
+
"title-isolation-structure-unavailable",
|
|
1137
|
+
() => waitForRendererTitlePolicyReady(
|
|
1138
|
+
() => operations.markTitlePolicyReady(options.inspector, selected.id),
|
|
1139
|
+
{
|
|
1140
|
+
timeoutMs,
|
|
1141
|
+
pollIntervalMs
|
|
1142
|
+
}
|
|
1143
|
+
)
|
|
1076
1144
|
);
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1145
|
+
await operations.execute(options.inspector, selected.id, options.rendererSource);
|
|
1146
|
+
const binding = await requireCompatibilityBoundary(
|
|
1147
|
+
"agent-routing",
|
|
1148
|
+
"agent-routing-structure-unavailable",
|
|
1149
|
+
() => waitForBinding(
|
|
1150
|
+
options.inspector,
|
|
1151
|
+
operations,
|
|
1152
|
+
selected.id,
|
|
1153
|
+
enabledAgents,
|
|
1154
|
+
timeoutMs,
|
|
1155
|
+
pollIntervalMs
|
|
1156
|
+
)
|
|
1080
1157
|
);
|
|
1081
|
-
await
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
operations,
|
|
1085
|
-
selected.renderer.id,
|
|
1086
|
-
enabledAgents,
|
|
1087
|
-
timeoutMs,
|
|
1088
|
-
pollIntervalMs
|
|
1158
|
+
const draftPrewarmPolicy = await requireCompatibilityBoundary(
|
|
1159
|
+
"draft-routing",
|
|
1160
|
+
"draft-routing-structure-unavailable",
|
|
1161
|
+
() => operations.installDraftPrewarmPolicy(options.inspector, selected.id)
|
|
1089
1162
|
);
|
|
1090
1163
|
return new InstalledRendererControlSession(
|
|
1091
1164
|
options.inspector,
|
|
@@ -1095,7 +1168,7 @@ async function createRendererControlSession(options) {
|
|
|
1095
1168
|
pollIntervalMs,
|
|
1096
1169
|
operations,
|
|
1097
1170
|
{
|
|
1098
|
-
|
|
1171
|
+
renderer: selected,
|
|
1099
1172
|
titlePolicy,
|
|
1100
1173
|
titlePolicyReadiness,
|
|
1101
1174
|
draftPrewarmPolicy,
|
|
@@ -1127,14 +1200,56 @@ async function installRendererControlSession(options) {
|
|
|
1127
1200
|
|
|
1128
1201
|
// packages/desktop-control/src/production-controller.ts
|
|
1129
1202
|
var PRODUCTION_INSTALL_TIMEOUT_MS = 9e4;
|
|
1203
|
+
var DESKTOP_CONTROLLER_READINESS_MAX_BYTES = 512;
|
|
1130
1204
|
var TRANSIENT_INSTALL_ATTEMPTS = 3;
|
|
1131
1205
|
var TRANSIENT_INSTALL_RETRY_MS = 250;
|
|
1206
|
+
function validCompatibilityIssue(state, issue) {
|
|
1207
|
+
const keys = Object.keys(issue);
|
|
1208
|
+
if (state === "compatible-with-warning") {
|
|
1209
|
+
return issue.capability === "title-isolation" && issue.reason === "unreviewed-title-service-identity" && typeof issue.observedIdentity === "string" && /^[A-Za-z_$][A-Za-z0-9_$]{0,63}$/.test(issue.observedIdentity) && keys.length === 3;
|
|
1210
|
+
}
|
|
1211
|
+
if (state === "degraded") {
|
|
1212
|
+
return [
|
|
1213
|
+
"permission-control",
|
|
1214
|
+
"sidebar-decoration",
|
|
1215
|
+
"fork-control",
|
|
1216
|
+
"usage-surface",
|
|
1217
|
+
"settings-surface"
|
|
1218
|
+
].includes(issue.capability) && issue.reason === "capability-unavailable" && issue.observedIdentity === void 0 && keys.length === 2;
|
|
1219
|
+
}
|
|
1220
|
+
if (state === "incompatible") {
|
|
1221
|
+
const pair = `${issue.capability}:${issue.reason}`;
|
|
1222
|
+
return [
|
|
1223
|
+
"title-isolation:title-isolation-structure-unavailable",
|
|
1224
|
+
"draft-routing:draft-routing-structure-unavailable",
|
|
1225
|
+
"agent-routing:agent-routing-structure-unavailable"
|
|
1226
|
+
].includes(pair) && issue.observedIdentity === void 0 && keys.length === 2;
|
|
1227
|
+
}
|
|
1228
|
+
return state === "detection-failed" && issue.capability === "compatibility-detection" && issue.reason === "inspection-failed" && issue.observedIdentity === void 0 && keys.length === 2;
|
|
1229
|
+
}
|
|
1230
|
+
function serializeDesktopControllerReadiness(readiness) {
|
|
1231
|
+
if (readiness.schemaVersion !== 2 || ![
|
|
1232
|
+
"compatible",
|
|
1233
|
+
"compatible-with-warning",
|
|
1234
|
+
"degraded",
|
|
1235
|
+
"incompatible",
|
|
1236
|
+
"detection-failed"
|
|
1237
|
+
].includes(readiness.state) || !Array.isArray(readiness.issues) || readiness.issues.length > 1 || readiness.state === "compatible" && readiness.issues.length !== 0 || readiness.state !== "compatible" && (readiness.issues.length !== 1 || !readiness.issues[0] || !validCompatibilityIssue(readiness.state, readiness.issues[0])) || Object.keys(readiness).length !== 3) {
|
|
1238
|
+
throw new Error("Desktop Controller readiness is invalid");
|
|
1239
|
+
}
|
|
1240
|
+
const line = JSON.stringify(readiness);
|
|
1241
|
+
if (Buffer.byteLength(line, "utf8") > DESKTOP_CONTROLLER_READINESS_MAX_BYTES) {
|
|
1242
|
+
throw new Error("Desktop Controller readiness exceeds its size limit");
|
|
1243
|
+
}
|
|
1244
|
+
return line;
|
|
1245
|
+
}
|
|
1132
1246
|
var defaultDependencies = {
|
|
1133
1247
|
readRenderer: (filePath) => readFile(filePath, "utf8"),
|
|
1134
1248
|
install: installRendererControlSession,
|
|
1135
1249
|
startAttachmentServer: startControllerAttachmentServer,
|
|
1136
|
-
ready: () => {
|
|
1137
|
-
process.stdout.write(
|
|
1250
|
+
ready: (readiness) => {
|
|
1251
|
+
process.stdout.write(`${serializeDesktopControllerReadiness(readiness)}
|
|
1252
|
+
`);
|
|
1138
1253
|
},
|
|
1139
1254
|
sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
1140
1255
|
monitorIntervalMs: 500
|
|
@@ -1218,8 +1333,16 @@ function parseDesktopControllerArguments(arguments_) {
|
|
|
1218
1333
|
};
|
|
1219
1334
|
}
|
|
1220
1335
|
function isTransientElectronInstallError(error) {
|
|
1221
|
-
|
|
1222
|
-
|
|
1336
|
+
let current = error;
|
|
1337
|
+
for (let depth = 0; depth < 4; depth += 1) {
|
|
1338
|
+
const message = current instanceof Error ? current.message : String(current);
|
|
1339
|
+
if (message.includes("Execution context was destroyed") || message.includes("Promise was collected")) {
|
|
1340
|
+
return true;
|
|
1341
|
+
}
|
|
1342
|
+
current = current instanceof Error ? current.cause : void 0;
|
|
1343
|
+
if (current === void 0) break;
|
|
1344
|
+
}
|
|
1345
|
+
return false;
|
|
1223
1346
|
}
|
|
1224
1347
|
async function installProductionSession(options, dependencies) {
|
|
1225
1348
|
for (let attempt = 1; attempt <= TRANSIENT_INSTALL_ATTEMPTS; attempt += 1) {
|
|
@@ -1235,19 +1358,35 @@ async function installProductionSession(options, dependencies) {
|
|
|
1235
1358
|
throw new Error("Desktop Controller exhausted Renderer installation attempts");
|
|
1236
1359
|
}
|
|
1237
1360
|
async function runDesktopController(options, signal, dependencies = defaultDependencies) {
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
{
|
|
1243
|
-
|
|
1244
|
-
|
|
1361
|
+
let session;
|
|
1362
|
+
try {
|
|
1363
|
+
const rendererSource = await dependencies.readRenderer(options.rendererPath);
|
|
1364
|
+
if (rendererSource.trim().length === 0) throw new Error("production Renderer Bundle is empty");
|
|
1365
|
+
const configuration = `Object.defineProperty(window, "__codexhostProductionConfigV1", { configurable: true, value: { defaultAgent: ${JSON.stringify(options.defaultAgent)} } });`;
|
|
1366
|
+
session = await installProductionSession(
|
|
1367
|
+
{
|
|
1368
|
+
inspectorEndpoint: options.inspectorEndpoint,
|
|
1369
|
+
rendererSource: `${configuration}
|
|
1245
1370
|
${rendererSource}`,
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1371
|
+
enabledAgents: ["codex", "pi", "claude-code"],
|
|
1372
|
+
timeoutMs: PRODUCTION_INSTALL_TIMEOUT_MS
|
|
1373
|
+
},
|
|
1374
|
+
dependencies
|
|
1375
|
+
);
|
|
1376
|
+
} catch (error) {
|
|
1377
|
+
dependencies.ready(
|
|
1378
|
+
error instanceof RendererCompatibilityError && !isTransientElectronInstallError(error) ? {
|
|
1379
|
+
schemaVersion: 2,
|
|
1380
|
+
state: "incompatible",
|
|
1381
|
+
issues: [{ capability: error.capability, reason: error.reason }]
|
|
1382
|
+
} : {
|
|
1383
|
+
schemaVersion: 2,
|
|
1384
|
+
state: "detection-failed",
|
|
1385
|
+
issues: [{ capability: "compatibility-detection", reason: "inspection-failed" }]
|
|
1386
|
+
}
|
|
1387
|
+
);
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1251
1390
|
let operation = Promise.resolve(void 0);
|
|
1252
1391
|
const useSession = (callback) => {
|
|
1253
1392
|
const next = operation.then(callback, callback);
|
|
@@ -1265,9 +1404,19 @@ ${rendererSource}`,
|
|
|
1265
1404
|
attach: () => useSession(async () => {
|
|
1266
1405
|
await session.ensureInstalled();
|
|
1267
1406
|
await session.activateDesktop();
|
|
1268
|
-
})
|
|
1407
|
+
}),
|
|
1408
|
+
compatibilityUpdate: () => useSession(async () => {
|
|
1409
|
+
await session.ensureInstalled();
|
|
1410
|
+
return session.requestCompatibilityUpdate();
|
|
1411
|
+
}),
|
|
1412
|
+
shutdown: () => useSession(() => session.quitDesktop())
|
|
1413
|
+
});
|
|
1414
|
+
const issues = session.snapshot.titlePolicy.warnings;
|
|
1415
|
+
dependencies.ready({
|
|
1416
|
+
schemaVersion: 2,
|
|
1417
|
+
state: issues.length === 0 ? "compatible" : "compatible-with-warning",
|
|
1418
|
+
issues
|
|
1269
1419
|
});
|
|
1270
|
-
dependencies.ready();
|
|
1271
1420
|
while (!signal.aborted) {
|
|
1272
1421
|
await dependencies.sleep(dependencies.monitorIntervalMs);
|
|
1273
1422
|
if (!signal.aborted) await useSession(() => session.ensureInstalled());
|