local-operator-ui 0.34.32 → 0.34.34
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/out/main/index.js +855 -501
- package/out/preload/index.js +4 -0
- package/out/renderer/assets/{_basePickBy-B66HBZHq.js → _basePickBy-CNMIid3L.js} +1 -1
- package/out/renderer/assets/{_baseUniq-CSRHNS5X.js → _baseUniq-BesnMR63.js} +1 -1
- package/out/renderer/assets/{agent-details-page--_gYi2lc.js → agent-details-page-B1wPZHYw.js} +1 -1
- package/out/renderer/assets/{agent-hub-page-B4CYHqQ7.js → agent-hub-page-zLDCcJ0-.js} +1 -1
- package/out/renderer/assets/{agents-page-Cjo3iaWD.js → agents-page-8cT_dI_3.js} +3 -3
- package/out/renderer/assets/{arc-4AcyjvSp.js → arc-MuuzF-kz.js} +1 -1
- package/out/renderer/assets/{architectureDiagram-IEHRJDOE-BE7p-gy-.js → architectureDiagram-IEHRJDOE-Cla6EwFi.js} +1 -1
- package/out/renderer/assets/{blockDiagram-JOT3LUYC-DgS-ikSj.js → blockDiagram-JOT3LUYC-Bja9j7dE.js} +1 -1
- package/out/renderer/assets/{browser-page-ql5R-cDs.js → browser-page-BYjoph3Z.js} +1 -1
- package/out/renderer/assets/{browser-webauthn-prompt-CWr4qL8A.js → browser-webauthn-prompt-B3uYCxFo.js} +1 -1
- package/out/renderer/assets/{c4Diagram-VJAJSXHY-CpMtpl2f.js → c4Diagram-VJAJSXHY-DVoi6Fpy.js} +1 -1
- package/out/renderer/assets/channel-BQiffY-M.js +1 -0
- package/out/renderer/assets/{chunk-4BMEZGHF-85Yusluf.js → chunk-4BMEZGHF-Cih6BNjs.js} +1 -1
- package/out/renderer/assets/{chunk-A2AXSNBT-D6BmMNue.js → chunk-A2AXSNBT-DJo0efSa.js} +1 -1
- package/out/renderer/assets/{chunk-AEK57VVT-C26q1R--.js → chunk-AEK57VVT-DvwXfXNo.js} +1 -1
- package/out/renderer/assets/{chunk-D6G4REZN-Dy4HYhmA.js → chunk-D6G4REZN-HuVYXeWr.js} +1 -1
- package/out/renderer/assets/{chunk-RZ5BOZE2-Dnxr3jB8.js → chunk-RZ5BOZE2-BPLh7jSP.js} +1 -1
- package/out/renderer/assets/{chunk-XZIHB7SX-BGzUtiqV.js → chunk-XZIHB7SX-Di-TZ5J_.js} +1 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-DDObgTCR.js +1 -0
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-DDObgTCR.js +1 -0
- package/out/renderer/assets/clone-C0j0lwrL.js +1 -0
- package/out/renderer/assets/{conversation-input-sync-CBw8uiPT.js → conversation-input-sync-Dvqtl3Fr.js} +247 -243
- package/out/renderer/assets/{dagre-OKDRZEBW-D6CK2WVv.js → dagre-OKDRZEBW-BRG1M9oi.js} +1 -1
- package/out/renderer/assets/{diagram-SSKATNLV-C3TpIKTy.js → diagram-SSKATNLV-DsHLzFSC.js} +1 -1
- package/out/renderer/assets/{diagram-VNBRO52H-z0vxgigp.js → diagram-VNBRO52H-DuDF21bn.js} +1 -1
- package/out/renderer/assets/{erDiagram-Q7BY3M3F-DBCh3SrN.js → erDiagram-Q7BY3M3F-BS7Xa-1R.js} +1 -1
- package/out/renderer/assets/{flowDiagram-4HSFHLVR-BgKCzSfN.js → flowDiagram-4HSFHLVR-A_BvmOjq.js} +1 -1
- package/out/renderer/assets/{ganttDiagram-APWFNJXF-BwsHywQN.js → ganttDiagram-APWFNJXF-BQX036BE.js} +1 -1
- package/out/renderer/assets/{gitGraphDiagram-7IBYFJ6S-DYgILe4-.js → gitGraphDiagram-7IBYFJ6S-YpfcZ3NA.js} +1 -1
- package/out/renderer/assets/{graph-Vw1kbjlt.js → graph-lKMrX7qR.js} +1 -1
- package/out/renderer/assets/{index-DYrqbn3V.js → index-BEYFd0J9.js} +90 -90
- package/out/renderer/assets/{index-DBZqVg5O.js → index-CYvymE2l.js} +1 -1
- package/out/renderer/assets/{index-CBEysjAK.js → index-DGSndO-f.js} +1 -1
- package/out/renderer/assets/{infoDiagram-PH2N3AL5-Cmpi3PDq.js → infoDiagram-PH2N3AL5-Dg-ZnTmu.js} +1 -1
- package/out/renderer/assets/{journeyDiagram-U35MCT3I-BVMD25G1.js → journeyDiagram-U35MCT3I-DRzUPcQU.js} +1 -1
- package/out/renderer/assets/{kanban-definition-NDS4AKOZ-CVijSM-H.js → kanban-definition-NDS4AKOZ-Q474R71W.js} +1 -1
- package/out/renderer/assets/{layout-yGyBho08.js → layout-DTC_hgF6.js} +1 -1
- package/out/renderer/assets/{legacy-agents-page-C9F6Anar.js → legacy-agents-page-k28FA6cr.js} +1 -1
- package/out/renderer/assets/{mermaid.core-eKt0AAEa.js → mermaid.core-C9rYU-0l.js} +5 -5
- package/out/renderer/assets/{mesh-page-V7TXOqL8.js → mesh-page-DfZZjDNB.js} +1 -1
- package/out/renderer/assets/{mindmap-definition-ALO5MXBD-7OE97AHg.js → mindmap-definition-ALO5MXBD-Bib16-Jn.js} +1 -1
- package/out/renderer/assets/{mini-CAwodgo3.js → mini-BJOZ3ClI.js} +1 -1
- package/out/renderer/assets/{org-surface-gate-BnAoMqVG.js → org-surface-gate-C3vaLLOm.js} +1 -1
- package/out/renderer/assets/{pieDiagram-IB7DONF6-BXLSPmEJ.js → pieDiagram-IB7DONF6-CFKHjx11.js} +1 -1
- package/out/renderer/assets/{projects-page-BHtKptz-.js → projects-page-BCQMniR-.js} +1 -1
- package/out/renderer/assets/{quadrantDiagram-7GDLP6J5-Csn6f8f3.js → quadrantDiagram-7GDLP6J5-B-9tF5TA.js} +1 -1
- package/out/renderer/assets/{radar-MK3ICKWK-BZZUrPnK.js → radar-MK3ICKWK-hPJOSxyO.js} +1 -1
- package/out/renderer/assets/{radient-auth-buttons-B0TYTsdi.js → radient-auth-buttons-CGDfKjvU.js} +1 -1
- package/out/renderer/assets/{requirementDiagram-KVF5MWMF-CidyP0gb.js → requirementDiagram-KVF5MWMF-DapE7E6j.js} +1 -1
- package/out/renderer/assets/{sankeyDiagram-QLVOVGJD-RqzFypY6.js → sankeyDiagram-QLVOVGJD-dQnFf6xh.js} +1 -1
- package/out/renderer/assets/{schedules-page-gb6zHM13.js → schedules-page-CTF8k7kQ.js} +1 -1
- package/out/renderer/assets/{sequenceDiagram-X6HHIX6F-hoGno_Xs.js → sequenceDiagram-X6HHIX6F-DpQcD4s7.js} +1 -1
- package/out/renderer/assets/{settings-page-D7QXHWhp.js → settings-page-Cjj6lVqM.js} +1 -1
- package/out/renderer/assets/{stateDiagram-DGXRK772-CGfBWbub.js → stateDiagram-DGXRK772-BH86RKPp.js} +1 -1
- package/out/renderer/assets/{stateDiagram-v2-YXO3MK2T-BuXMQAvq.js → stateDiagram-v2-YXO3MK2T-CPbeGHOQ.js} +1 -1
- package/out/renderer/assets/{timeline-definition-BDJGKUSR-CS5EDd47.js → timeline-definition-BDJGKUSR-I9WIfb3F.js} +1 -1
- package/out/renderer/assets/{use-download-agent-mutation-DSU0LPqD.js → use-download-agent-mutation-DZKciluh.js} +1 -1
- package/out/renderer/assets/{use-memberships-query-CN1cjAr2.js → use-memberships-query-CouwB8Jj.js} +1 -1
- package/out/renderer/assets/{use-session-snapshot-Da-uFU-c.js → use-session-snapshot-BwZz7qKM.js} +1 -1
- package/out/renderer/assets/{xychartDiagram-VJFVF3MP-D44Ss3rC.js → xychartDiagram-VJFVF3MP-RQXd6xeu.js} +1 -1
- package/out/renderer/index.html +2 -2
- package/out/renderer/mini.html +3 -3
- package/package.json +2 -2
- package/out/renderer/assets/channel-BdIPJXOo.js +0 -1
- package/out/renderer/assets/classDiagram-GIVACNV2-DiGQhWtx.js +0 -1
- package/out/renderer/assets/classDiagram-v2-COTLJTTW-DiGQhWtx.js +0 -1
- package/out/renderer/assets/clone-BhPlGAQ6.js +0 -1
package/out/main/index.js
CHANGED
|
@@ -4618,6 +4618,55 @@ const serverChannelVerdict = ({
|
|
|
4618
4618
|
return { status: "restart-required", state: "restart-required" };
|
|
4619
4619
|
return { status: "current", state: "current" };
|
|
4620
4620
|
};
|
|
4621
|
+
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
4622
|
+
const isInstallerWheel = (entry, version, platform) => {
|
|
4623
|
+
if (!entry || typeof entry !== "object") return false;
|
|
4624
|
+
const file = entry;
|
|
4625
|
+
if (file.packagetype !== "bdist_wheel" || file.yanked) return false;
|
|
4626
|
+
if (typeof file.url !== "string") return false;
|
|
4627
|
+
let url;
|
|
4628
|
+
try {
|
|
4629
|
+
url = new URL(file.url);
|
|
4630
|
+
} catch {
|
|
4631
|
+
return false;
|
|
4632
|
+
}
|
|
4633
|
+
if (url.protocol !== "https:" || url.hostname !== "files.pythonhosted.org")
|
|
4634
|
+
return false;
|
|
4635
|
+
const digests = file.digests;
|
|
4636
|
+
const sha256 = digests && typeof digests === "object" ? digests.sha256 : null;
|
|
4637
|
+
if (typeof sha256 !== "string" || !SHA256_HEX.test(sha256)) return false;
|
|
4638
|
+
if (typeof file.filename !== "string") return false;
|
|
4639
|
+
if (!file.filename.startsWith(`local_operator-${version}-`)) return false;
|
|
4640
|
+
return platform === "darwin" ? file.filename.endsWith("-py3-none-macosx_11_0_universal2.whl") : file.filename.endsWith("-py3-none-any.whl");
|
|
4641
|
+
};
|
|
4642
|
+
const pypiReleaseReading = (payload, platform) => {
|
|
4643
|
+
const unreadable = {
|
|
4644
|
+
namedVersion: null,
|
|
4645
|
+
readyVersion: null
|
|
4646
|
+
};
|
|
4647
|
+
if (!payload || typeof payload !== "object") return unreadable;
|
|
4648
|
+
const info = payload.info;
|
|
4649
|
+
if (!info || typeof info !== "object") return unreadable;
|
|
4650
|
+
const version = info.version;
|
|
4651
|
+
if (typeof version !== "string" || !version.trim()) return unreadable;
|
|
4652
|
+
const trimmed = version.trim();
|
|
4653
|
+
const downloadable = (files) => Array.isArray(files) && files.some((entry) => isInstallerWheel(entry, trimmed, platform));
|
|
4654
|
+
let ready = downloadable(payload.urls);
|
|
4655
|
+
if (!ready) {
|
|
4656
|
+
const releases = payload.releases;
|
|
4657
|
+
if (releases && typeof releases === "object") {
|
|
4658
|
+
ready = downloadable(releases[trimmed]);
|
|
4659
|
+
}
|
|
4660
|
+
}
|
|
4661
|
+
return { namedVersion: trimmed, readyVersion: ready ? trimmed : null };
|
|
4662
|
+
};
|
|
4663
|
+
const readPypiReleaseBody = (body, platform) => {
|
|
4664
|
+
try {
|
|
4665
|
+
return pypiReleaseReading(JSON.parse(body), platform);
|
|
4666
|
+
} catch {
|
|
4667
|
+
return { namedVersion: null, readyVersion: null };
|
|
4668
|
+
}
|
|
4669
|
+
};
|
|
4621
4670
|
const updateCheckVerdict = (channels) => ({
|
|
4622
4671
|
app: channels.app,
|
|
4623
4672
|
server: channels.server,
|
|
@@ -8043,6 +8092,21 @@ async function withUserShellPath(env, userShellPath2) {
|
|
|
8043
8092
|
if (!path2) return { ...env };
|
|
8044
8093
|
return { ...env, PATH: mergeShellPath(path2, env.PATH) };
|
|
8045
8094
|
}
|
|
8095
|
+
const AGENT_SHELL_ENV = "LOCAL_OPERATOR_AGENT_SHELL";
|
|
8096
|
+
const MAY_DELEGATE_ENV = "LOCAL_OPERATOR_AGENT_MAY_DELEGATE";
|
|
8097
|
+
const ALLOW_NESTED_SESSION_ENV = "LOCAL_OPERATOR_ALLOW_NESTED_SESSION";
|
|
8098
|
+
const AGENT_SHELL_MARKERS = [
|
|
8099
|
+
AGENT_SHELL_ENV,
|
|
8100
|
+
MAY_DELEGATE_ENV,
|
|
8101
|
+
ALLOW_NESTED_SESSION_ENV
|
|
8102
|
+
];
|
|
8103
|
+
function withoutAgentShellMarker(env) {
|
|
8104
|
+
const stripped = { ...env };
|
|
8105
|
+
for (const name of AGENT_SHELL_MARKERS) {
|
|
8106
|
+
delete stripped[name];
|
|
8107
|
+
}
|
|
8108
|
+
return stripped;
|
|
8109
|
+
}
|
|
8046
8110
|
const launchEnv = {
|
|
8047
8111
|
...process.env
|
|
8048
8112
|
};
|
|
@@ -10540,7 +10604,7 @@ class BackendServiceManager {
|
|
|
10540
10604
|
* operator (see the comment on that entry, and `./notification-launch`).
|
|
10541
10605
|
*/
|
|
10542
10606
|
backendSpawnEnv() {
|
|
10543
|
-
return {
|
|
10607
|
+
return withoutAgentShellMarker({
|
|
10544
10608
|
...withPythonBytecodeCache(this.shellEnv, this.appDataPath),
|
|
10545
10609
|
// Managed starts rotate the token; set after the spread so the spawn
|
|
10546
10610
|
// can never inherit a stale one from the shell environment. `start()`
|
|
@@ -10563,7 +10627,7 @@ class BackendServiceManager {
|
|
|
10563
10627
|
* unstated key is left alone.
|
|
10564
10628
|
*/
|
|
10565
10629
|
...resolveNotificationLaunch(launchEnv)
|
|
10566
|
-
};
|
|
10630
|
+
});
|
|
10567
10631
|
}
|
|
10568
10632
|
/**
|
|
10569
10633
|
* Load environment variables from macOS shell configuration files
|
|
@@ -14800,12 +14864,14 @@ class BrowserStateWriter {
|
|
|
14800
14864
|
this.onError = options.onError ?? (() => {
|
|
14801
14865
|
});
|
|
14802
14866
|
this.appVersion = options.appVersion;
|
|
14867
|
+
this.instanceId = options.instanceId;
|
|
14803
14868
|
}
|
|
14804
14869
|
now;
|
|
14805
14870
|
onError;
|
|
14806
14871
|
timer = null;
|
|
14807
14872
|
written = false;
|
|
14808
14873
|
appVersion;
|
|
14874
|
+
instanceId;
|
|
14809
14875
|
/** Publish the record and start the heartbeat. The heartbeat is the
|
|
14810
14876
|
* difference between "the app is running and can open a tab on demand" and a
|
|
14811
14877
|
* record that names a process that died without cleaning up. */
|
|
@@ -14837,6 +14903,7 @@ class BrowserStateWriter {
|
|
|
14837
14903
|
proto: PROTO_VERSION,
|
|
14838
14904
|
host: "ui",
|
|
14839
14905
|
app_version: this.appVersion,
|
|
14906
|
+
...this.instanceId && { instance_id: this.instanceId },
|
|
14840
14907
|
profile_dir: facts.profileDir,
|
|
14841
14908
|
// Advertised from the ONE list the `/health` route also reads (§6.3), so the
|
|
14842
14909
|
// record a session reads and the probe Python acquits a stale record with
|
|
@@ -24117,6 +24184,57 @@ function restrictMode(path2, log2) {
|
|
|
24117
24184
|
);
|
|
24118
24185
|
}
|
|
24119
24186
|
}
|
|
24187
|
+
class StartupRollback {
|
|
24188
|
+
owners = [];
|
|
24189
|
+
add(cleanup) {
|
|
24190
|
+
this.owners.push(cleanup);
|
|
24191
|
+
}
|
|
24192
|
+
commit() {
|
|
24193
|
+
this.owners = [];
|
|
24194
|
+
}
|
|
24195
|
+
async run(log2) {
|
|
24196
|
+
for (const cleanup of this.owners.splice(0).reverse()) {
|
|
24197
|
+
try {
|
|
24198
|
+
await cleanup();
|
|
24199
|
+
} catch (error) {
|
|
24200
|
+
log2(`[browser] startup cleanup failed: ${String(error)}`);
|
|
24201
|
+
}
|
|
24202
|
+
}
|
|
24203
|
+
}
|
|
24204
|
+
}
|
|
24205
|
+
const MAX_START_ATTEMPTS = 3;
|
|
24206
|
+
async function startWithRetries(options) {
|
|
24207
|
+
let failure = new Error("The browser window closed before startup.");
|
|
24208
|
+
for (let attempt = 1; attempt <= MAX_START_ATTEMPTS; attempt += 1) {
|
|
24209
|
+
if (options.cancelled()) break;
|
|
24210
|
+
options.publish({
|
|
24211
|
+
status: attempt === 1 ? "starting" : "retrying",
|
|
24212
|
+
attempt,
|
|
24213
|
+
maxAttempts: MAX_START_ATTEMPTS,
|
|
24214
|
+
message: null,
|
|
24215
|
+
canRetry: false
|
|
24216
|
+
});
|
|
24217
|
+
try {
|
|
24218
|
+
return await options.start();
|
|
24219
|
+
} catch (error) {
|
|
24220
|
+
failure = error;
|
|
24221
|
+
options.log(
|
|
24222
|
+
`[browser] startup attempt ${attempt}/${MAX_START_ATTEMPTS} failed: ${String(error)}`
|
|
24223
|
+
);
|
|
24224
|
+
}
|
|
24225
|
+
}
|
|
24226
|
+
options.publish({
|
|
24227
|
+
status: "unavailable",
|
|
24228
|
+
attempt: MAX_START_ATTEMPTS,
|
|
24229
|
+
maxAttempts: MAX_START_ATTEMPTS,
|
|
24230
|
+
message: "The browser could not start. Your saved tabs and site approvals are kept; sessions you were signed into may need signing in again.",
|
|
24231
|
+
// The window is still open and no host is running once the sequence
|
|
24232
|
+
// exhausts, so the renderer may offer a retry. `startBrowserHost` is
|
|
24233
|
+
// idempotent on re-registration, which is what makes the control safe.
|
|
24234
|
+
canRetry: true
|
|
24235
|
+
});
|
|
24236
|
+
throw failure;
|
|
24237
|
+
}
|
|
24120
24238
|
const BROWSER_IPC_CHANNELS = [
|
|
24121
24239
|
"browser-state",
|
|
24122
24240
|
"browser-new-tab",
|
|
@@ -24138,19 +24256,53 @@ const BROWSER_IPC_CHANNELS = [
|
|
|
24138
24256
|
"browser-clear-data",
|
|
24139
24257
|
"browser-webauthn-respond",
|
|
24140
24258
|
"browser-webauthn-pending",
|
|
24141
|
-
"browser-reveal-downloads"
|
|
24259
|
+
"browser-reveal-downloads",
|
|
24260
|
+
/** The user's own recovery click on the `unavailable` state (round 1, D1a).
|
|
24261
|
+
* Named here so the namespace stays the one place that lists its channels. */
|
|
24262
|
+
"browser-retry"
|
|
24142
24263
|
];
|
|
24143
24264
|
function registerBrowserIpc(options) {
|
|
24144
|
-
function
|
|
24265
|
+
function authorizeSender(event) {
|
|
24145
24266
|
const owner = options.window();
|
|
24146
24267
|
if (!owner || owner.isDestroyed() || event.sender !== owner.webContents || event.senderFrame !== owner.webContents.mainFrame || !trustedDesktopFrame(event.senderFrame.url, options.expectedUrl)) {
|
|
24147
24268
|
throw new Error("This window cannot use the browser controls.");
|
|
24148
24269
|
}
|
|
24270
|
+
}
|
|
24271
|
+
function authorize(event) {
|
|
24272
|
+
authorizeSender(event);
|
|
24149
24273
|
const host = options.host();
|
|
24150
|
-
if (!host)
|
|
24274
|
+
if (!host)
|
|
24275
|
+
throw new Error("browser_unavailable: The browser is not ready.");
|
|
24151
24276
|
return host;
|
|
24152
24277
|
}
|
|
24153
|
-
electron.ipcMain.handle("browser-state", (event) =>
|
|
24278
|
+
electron.ipcMain.handle("browser-state", (event) => {
|
|
24279
|
+
authorizeSender(event);
|
|
24280
|
+
const host = options.host();
|
|
24281
|
+
const availability = options.availability?.();
|
|
24282
|
+
if (host)
|
|
24283
|
+
return { ...host.chromeState(), ...availability && { availability } };
|
|
24284
|
+
return {
|
|
24285
|
+
tabs: [],
|
|
24286
|
+
activeTabId: null,
|
|
24287
|
+
url: "",
|
|
24288
|
+
title: "",
|
|
24289
|
+
loading: false,
|
|
24290
|
+
canGoBack: false,
|
|
24291
|
+
canGoForward: false,
|
|
24292
|
+
pendingConsent: [],
|
|
24293
|
+
approvals: [],
|
|
24294
|
+
navFailure: null,
|
|
24295
|
+
availability: availability ?? {
|
|
24296
|
+
status: "unavailable",
|
|
24297
|
+
attempt: 0,
|
|
24298
|
+
maxAttempts: MAX_START_ATTEMPTS,
|
|
24299
|
+
message: "The browser is not running.",
|
|
24300
|
+
// No host invokes this fallback (it is the pre-start projection), and
|
|
24301
|
+
// there is nothing to retry before a start was asked for.
|
|
24302
|
+
canRetry: false
|
|
24303
|
+
}
|
|
24304
|
+
};
|
|
24305
|
+
});
|
|
24154
24306
|
electron.ipcMain.handle("browser-new-tab", async (event, sessionId2) => {
|
|
24155
24307
|
const host = authorize(event);
|
|
24156
24308
|
return host.newTab(optionalConversationOrThrow(sessionId2));
|
|
@@ -24276,6 +24428,13 @@ function registerBrowserIpc(options) {
|
|
|
24276
24428
|
);
|
|
24277
24429
|
return { opened: message === "", message };
|
|
24278
24430
|
});
|
|
24431
|
+
electron.ipcMain.handle("browser-retry", async (event) => {
|
|
24432
|
+
authorizeSender(event);
|
|
24433
|
+
if (!options.retry)
|
|
24434
|
+
throw new Error("browser_unavailable: Retry is not available.");
|
|
24435
|
+
await options.retry();
|
|
24436
|
+
return { retried: true };
|
|
24437
|
+
});
|
|
24279
24438
|
}
|
|
24280
24439
|
function unregisterBrowserIpc() {
|
|
24281
24440
|
for (const channel of BROWSER_IPC_CHANNELS) electron.ipcMain.removeHandler(channel);
|
|
@@ -24698,7 +24857,7 @@ function installBrowserSessionHandlers(browserSession, hooks = {}) {
|
|
|
24698
24857
|
return false;
|
|
24699
24858
|
}
|
|
24700
24859
|
);
|
|
24701
|
-
|
|
24860
|
+
const onDownload = (event, item, webContents) => {
|
|
24702
24861
|
const webContentsId = webContents?.id ?? -1;
|
|
24703
24862
|
const decision = hooks.onDownload?.(item, webContentsId) ?? {
|
|
24704
24863
|
cancel: true,
|
|
@@ -24718,7 +24877,11 @@ function installBrowserSessionHandlers(browserSession, hooks = {}) {
|
|
|
24718
24877
|
);
|
|
24719
24878
|
}
|
|
24720
24879
|
hooks.onDownloadDecided?.(outcome);
|
|
24721
|
-
}
|
|
24880
|
+
};
|
|
24881
|
+
browserSession.on("will-download", onDownload);
|
|
24882
|
+
return () => {
|
|
24883
|
+
browserSession.removeListener("will-download", onDownload);
|
|
24884
|
+
};
|
|
24722
24885
|
}
|
|
24723
24886
|
async function clearBrowsingData(browserSession, what) {
|
|
24724
24887
|
browserSession.flushStorageData();
|
|
@@ -24769,6 +24932,7 @@ function flushBrowserStorage(browserSession) {
|
|
|
24769
24932
|
const RPC_PATH = "/rpc";
|
|
24770
24933
|
const HEALTH_PATH = "/health";
|
|
24771
24934
|
const KEY_HEADER = "x-bridge-key";
|
|
24935
|
+
const INSTANCE_HEADER = "x-ui-browser-instance";
|
|
24772
24936
|
const MAX_BODY_BYTES = 1 << 20;
|
|
24773
24937
|
const LOOPBACK_HOST = "127.0.0.1";
|
|
24774
24938
|
function send(res, status2, body, contentType = "application/json") {
|
|
@@ -24895,6 +25059,7 @@ async function handle(req, res, options) {
|
|
|
24895
25059
|
host: "ui",
|
|
24896
25060
|
proto: PROTO_VERSION,
|
|
24897
25061
|
pid: process.pid,
|
|
25062
|
+
...options.instanceId && { instance_id: options.instanceId },
|
|
24898
25063
|
// Additive: an older reader requires `host` and `pid` and ignores the
|
|
24899
25064
|
// rest, and a newer one learns whether the console is up without a
|
|
24900
25065
|
// second probe (design 10.1). `false` when nobody reported a
|
|
@@ -24921,6 +25086,11 @@ async function handle(req, res, options) {
|
|
|
24921
25086
|
send(res, 401, { detail: "unauthorized" });
|
|
24922
25087
|
return;
|
|
24923
25088
|
}
|
|
25089
|
+
const expectedInstance = req.headers[INSTANCE_HEADER];
|
|
25090
|
+
if (expectedInstance !== void 0 && expectedInstance !== options.instanceId) {
|
|
25091
|
+
send(res, 409, { detail: "host instance changed" });
|
|
25092
|
+
return;
|
|
25093
|
+
}
|
|
24924
25094
|
let raw;
|
|
24925
25095
|
try {
|
|
24926
25096
|
raw = JSON.parse(await readBody(req));
|
|
@@ -26040,13 +26210,28 @@ function refusesNavigation(rawUrl) {
|
|
|
26040
26210
|
let activeHost = null;
|
|
26041
26211
|
let stopping = null;
|
|
26042
26212
|
function browserHostStopPending() {
|
|
26043
|
-
return activeHost !== null || stopping !== null;
|
|
26213
|
+
return activeHost !== null || stopping !== null || starting !== null;
|
|
26044
26214
|
}
|
|
26045
26215
|
async function stopBrowserHost() {
|
|
26046
26216
|
if (stopping) return stopping;
|
|
26217
|
+
if (starting) {
|
|
26218
|
+
cancelStartup = true;
|
|
26219
|
+
stopping = starting.catch(() => null).then(async () => {
|
|
26220
|
+
const current = activeHost;
|
|
26221
|
+
activeHost = null;
|
|
26222
|
+
if (current) await current.stop();
|
|
26223
|
+
else unregisterBrowserIpc();
|
|
26224
|
+
}).finally(() => {
|
|
26225
|
+
stopping = null;
|
|
26226
|
+
});
|
|
26227
|
+
return stopping;
|
|
26228
|
+
}
|
|
26047
26229
|
const handle2 = activeHost;
|
|
26048
26230
|
activeHost = null;
|
|
26049
|
-
if (!handle2)
|
|
26231
|
+
if (!handle2) {
|
|
26232
|
+
unregisterBrowserIpc();
|
|
26233
|
+
return;
|
|
26234
|
+
}
|
|
26050
26235
|
stopping = handle2.stop().finally(() => {
|
|
26051
26236
|
stopping = null;
|
|
26052
26237
|
});
|
|
@@ -26056,524 +26241,650 @@ function browserHostEnabled(env = process.env) {
|
|
|
26056
26241
|
const flag = env.LOCAL_OPERATOR_UI_BROWSER_HOST?.trim().toLowerCase();
|
|
26057
26242
|
return !(flag === "0" || flag === "false" || flag === "off");
|
|
26058
26243
|
}
|
|
26059
|
-
|
|
26060
|
-
|
|
26061
|
-
|
|
26062
|
-
|
|
26063
|
-
|
|
26064
|
-
|
|
26065
|
-
|
|
26066
|
-
|
|
26067
|
-
|
|
26068
|
-
|
|
26069
|
-
// (a tab already gone), which the row renders the old way rather than guessing.
|
|
26070
|
-
ownerKindFor: (tabId) => registryForDownloads?.get(tabId)?.owner ?? null,
|
|
26071
|
-
log: log2,
|
|
26072
|
-
onActivity: () => {
|
|
26073
|
-
if (options.window.isDestroyed()) return;
|
|
26074
|
-
options.window.webContents.send("browser-state-changed");
|
|
26075
|
-
}
|
|
26076
|
-
});
|
|
26077
|
-
installBrowserSessionHandlers(browserSession, {
|
|
26078
|
-
onPermissionRequested: (details) => {
|
|
26079
|
-
log2(
|
|
26080
|
-
`[browser] denied a ${details.permission} permission request from ${details.origin || "(unknown origin)"}`
|
|
26081
|
-
);
|
|
26082
|
-
},
|
|
26083
|
-
onDownload: (item, webContentsId) => downloads.decide(item, webContentsId),
|
|
26084
|
-
onDownloadDecided: (outcome) => {
|
|
26085
|
-
if (outcome.savePath) {
|
|
26086
|
-
log2(
|
|
26087
|
-
`[browser] saved a download from ${outcome.url} to ${outcome.savePath}`
|
|
26088
|
-
);
|
|
26089
|
-
}
|
|
26090
|
-
},
|
|
26091
|
-
log: log2
|
|
26092
|
-
});
|
|
26093
|
-
configurePslRules(PSL_RULES);
|
|
26094
|
-
await installWebauthn({ log: log2 });
|
|
26095
|
-
const describeSource = (frame) => {
|
|
26096
|
-
if (!frame || typeof frame !== "object") {
|
|
26097
|
-
return { tabId: null, pageTitle: null };
|
|
26098
|
-
}
|
|
26099
|
-
const contents = electron.webContents.fromFrame(frame);
|
|
26100
|
-
const record = contents === void 0 ? void 0 : registry.byWebContentsId(contents.id);
|
|
26101
|
-
if (!record) return { tabId: null, pageTitle: null };
|
|
26102
|
-
const title = record.view.webContents.isDestroyed() ? "" : record.view.webContents.getTitle();
|
|
26103
|
-
return { tabId: record.tabId, pageTitle: title || null };
|
|
26104
|
-
};
|
|
26105
|
-
const webauthn = new WebauthnChooser({
|
|
26106
|
-
notify: (request) => {
|
|
26107
|
-
if (options.window.isDestroyed()) return;
|
|
26108
|
-
options.window.webContents.send("browser-webauthn-request", request);
|
|
26109
|
-
},
|
|
26110
|
-
/*
|
|
26111
|
-
* AND THE OTHER DIRECTION: every settle is pushed, not only every arrival.
|
|
26112
|
-
*
|
|
26113
|
-
* Without this the renderer cannot tell a live request from one main has
|
|
26114
|
-
* already cancelled, so a chooser whose request expired while the user was
|
|
26115
|
-
* away stays on screen offering rows that can no longer do anything (design
|
|
26116
|
-
* round 1, D2) — and the click is then discarded silently. The outcome travels
|
|
26117
|
-
* with the push because the two cases differ for the user: their own answer is
|
|
26118
|
-
* already off the screen, an expiry has to be explained.
|
|
26119
|
-
*/
|
|
26120
|
-
onSettled: (requestId2, outcome) => {
|
|
26121
|
-
if (options.window.isDestroyed()) return;
|
|
26122
|
-
options.window.webContents.send("browser-webauthn-settled", {
|
|
26123
|
-
requestId: requestId2,
|
|
26124
|
-
outcome
|
|
26125
|
-
});
|
|
26126
|
-
},
|
|
26127
|
-
describeSource,
|
|
26128
|
-
log: log2
|
|
26129
|
-
});
|
|
26130
|
-
const detachWebauthn = attachWebauthnChooser(browserSession, webauthn, log2);
|
|
26131
|
-
const cookieJar = createHiddenCookieJarTarget(
|
|
26132
|
-
new electron.WebContentsView({
|
|
26133
|
-
webPreferences: { ...HIDDEN_COOKIE_JAR_WEB_PREFERENCES }
|
|
26134
|
-
})
|
|
26135
|
-
);
|
|
26136
|
-
const sessionCookies = new SessionCookieVault({
|
|
26137
|
-
jar: cookieJar.jar,
|
|
26138
|
-
cipher: browserProfileCipher(),
|
|
26139
|
-
...sessionCookiePaths(options.userDataDir),
|
|
26140
|
-
flushStore: () => browserSession.cookies.flushStore(),
|
|
26141
|
-
clearSessionData: (what) => clearBrowsingData(browserSession, what),
|
|
26142
|
-
log: log2
|
|
26143
|
-
});
|
|
26144
|
-
let restoreReport = null;
|
|
26145
|
-
try {
|
|
26146
|
-
restoreReport = await sessionCookies.restore();
|
|
26147
|
-
} catch (error) {
|
|
26148
|
-
log2(`[browser] session cookies: the restore failed (${String(error)})`);
|
|
26149
|
-
}
|
|
26150
|
-
const cdp = new CdpPool({ log: log2 });
|
|
26151
|
-
const views = /* @__PURE__ */ new Map();
|
|
26152
|
-
let stateWriter = null;
|
|
26153
|
-
const sessionStore = new BrowserSessionStore({
|
|
26154
|
-
dir: path$1.join(options.userDataDir, "browser"),
|
|
26155
|
-
log: log2
|
|
26156
|
-
});
|
|
26157
|
-
const consentNotifier = new ConsentNotifier({
|
|
26158
|
-
show: options.windowShow,
|
|
26159
|
-
/*
|
|
26160
|
-
* The click's own rule lives in `consentClickHandler` (see it: the delivered
|
|
26161
|
-
* payload, the order, and the raise are all asserted there), and this is the
|
|
26162
|
-
* wiring that gives it the real window and the real plan. `windowShow` is the
|
|
26163
|
-
* plan the click comes forward under — this module still never decides whether a
|
|
26164
|
-
* window comes forward (design 11.4), it hands the plan to `window-raise.ts`.
|
|
26165
|
-
*/
|
|
26166
|
-
onAttention: consentClickHandler({
|
|
26167
|
-
/*
|
|
26168
|
-
* THE WINDOW IS ASKED FOR, NOT CAPTURED, and the difference is the U2 fix:
|
|
26169
|
-
* this host is stopped the moment its window closes (`src/main/index.ts`'s
|
|
26170
|
-
* `closed` hook), but a banner already raised is still in Notification
|
|
26171
|
-
* Center and its click still lands here. A captured window would be a
|
|
26172
|
-
* destroyed one by then, and reading it throws.
|
|
26173
|
-
*/
|
|
26174
|
-
window: () => options.window,
|
|
26175
|
-
show: options.windowShow,
|
|
26176
|
-
report: options.reportRaise,
|
|
26177
|
-
reopen: options.reopenConsent
|
|
26178
|
-
}),
|
|
26179
|
-
log: log2
|
|
26180
|
-
});
|
|
26181
|
-
const captureSession = () => {
|
|
26182
|
-
sessionStore.record(
|
|
26183
|
-
captureTabs(
|
|
26184
|
-
registry.list(),
|
|
26185
|
-
registry.activeTab?.tabId ?? null,
|
|
26186
|
-
host.failedTabIds()
|
|
26187
|
-
)
|
|
26188
|
-
);
|
|
26189
|
-
};
|
|
26190
|
-
const notifyChanged = () => {
|
|
26191
|
-
stateWriter?.publishNow();
|
|
26192
|
-
options.window.webContents.send("browser-state-changed");
|
|
26193
|
-
captureSession();
|
|
26194
|
-
};
|
|
26195
|
-
const registry = new TabRegistry(
|
|
26196
|
-
(_createOptions, tabId) => {
|
|
26197
|
-
const view = buildView(options.window);
|
|
26198
|
-
views.set(tabId, view);
|
|
26199
|
-
wireView(view, tabId);
|
|
26200
|
-
return view;
|
|
26201
|
-
},
|
|
26202
|
-
releaseTab,
|
|
26203
|
-
notifyChanged
|
|
26204
|
-
);
|
|
26205
|
-
registryForDownloads = registry;
|
|
26206
|
-
function releaseTab(tabId, webContentsId) {
|
|
26207
|
-
const view = views.get(tabId);
|
|
26208
|
-
views.delete(tabId);
|
|
26209
|
-
downloads.forget(tabId);
|
|
26210
|
-
void cdp.detach(webContentsId).finally(() => {
|
|
26211
|
-
releaseView(options.window, view);
|
|
26244
|
+
const hostInstanceId = node_crypto.randomUUID();
|
|
26245
|
+
let starting = null;
|
|
26246
|
+
let startingWindow = null;
|
|
26247
|
+
let cancelStartup = false;
|
|
26248
|
+
function startBrowserHost(options) {
|
|
26249
|
+
if (starting) {
|
|
26250
|
+
if (startingWindow === options.window) return starting;
|
|
26251
|
+
return starting.catch(() => null).then(async () => {
|
|
26252
|
+
await stopBrowserHost();
|
|
26253
|
+
return startBrowserHost(options);
|
|
26212
26254
|
});
|
|
26213
26255
|
}
|
|
26214
|
-
|
|
26215
|
-
|
|
26216
|
-
|
|
26217
|
-
|
|
26218
|
-
|
|
26219
|
-
|
|
26220
|
-
|
|
26221
|
-
|
|
26222
|
-
|
|
26223
|
-
|
|
26224
|
-
} catch {
|
|
26225
|
-
}
|
|
26226
|
-
return true;
|
|
26227
|
-
},
|
|
26228
|
-
isLive: (token) => {
|
|
26229
|
-
try {
|
|
26230
|
-
registry.requireSurface(token);
|
|
26231
|
-
return true;
|
|
26232
|
-
} catch {
|
|
26233
|
-
return false;
|
|
26234
|
-
}
|
|
26235
|
-
}
|
|
26236
|
-
});
|
|
26237
|
-
const approvals = new ApprovalStore({
|
|
26238
|
-
dir: path$1.join(options.userDataDir, "browser"),
|
|
26239
|
-
onChanged: () => {
|
|
26240
|
-
options.window.webContents.send("browser-consent-changed");
|
|
26241
|
-
consentNotifier.announce(approvals.pendingEntries());
|
|
26242
|
-
},
|
|
26243
|
-
log: log2
|
|
26244
|
-
});
|
|
26245
|
-
const profileDir = browserStoragePath(browserSession) ?? "";
|
|
26246
|
-
const facts = {
|
|
26247
|
-
proto: 1,
|
|
26248
|
-
appVersion: options.appVersion,
|
|
26249
|
-
profileDir,
|
|
26250
|
-
profilePersistent: browserSession.isPersistent(),
|
|
26251
|
-
userAgent: browserUserAgent(),
|
|
26252
|
-
domainScope: domainScopeAvailable()
|
|
26256
|
+
if (stopping) return stopping.then(() => startBrowserHost(options));
|
|
26257
|
+
if (activeHost) return Promise.resolve(activeHost);
|
|
26258
|
+
cancelStartup = false;
|
|
26259
|
+
startingWindow = options.window;
|
|
26260
|
+
let availability = {
|
|
26261
|
+
status: "starting",
|
|
26262
|
+
attempt: 1,
|
|
26263
|
+
maxAttempts: MAX_START_ATTEMPTS,
|
|
26264
|
+
message: null,
|
|
26265
|
+
canRetry: false
|
|
26253
26266
|
};
|
|
26254
|
-
const
|
|
26255
|
-
|
|
26256
|
-
|
|
26257
|
-
|
|
26258
|
-
|
|
26259
|
-
|
|
26260
|
-
log: log2,
|
|
26261
|
-
onChanged: notifyChanged,
|
|
26262
|
-
facts: () => facts,
|
|
26263
|
-
forgetSiteData: (origin) => clearOriginData(browserSession, origin)
|
|
26264
|
-
});
|
|
26265
|
-
const recorded = readSession(sessionStore.filePath, log2);
|
|
26266
|
-
host.restoreTabs(recorded);
|
|
26267
|
-
if (recorded.length) {
|
|
26268
|
-
log2(
|
|
26269
|
-
`[browser] restored ${recorded.length} tab(s) from ${SESSION_FILENAME}; every restored tab is user-owned and holds no handle (design 7.3)`
|
|
26270
|
-
);
|
|
26271
|
-
}
|
|
26272
|
-
const consoleStartup = await startConsoleHost({
|
|
26273
|
-
window: options.window,
|
|
26274
|
-
expectedUrl: options.expectedUrl,
|
|
26275
|
-
appVersion: options.appVersion,
|
|
26276
|
-
notifier: options.notifier,
|
|
26277
|
-
/*
|
|
26278
|
-
* The capture view's document and preload, forwarded rather than re-derived:
|
|
26279
|
-
* only the app knows where its own bundle lives, and a console host that
|
|
26280
|
-
* guessed would answer a screenshot with a blank frame rather than with its
|
|
26281
|
-
* own typed refusal. WITHOUT THESE TWO the host builds no capture view at all,
|
|
26282
|
-
* which the live proof rig caught as `capture_unavailable` on a surface with
|
|
26283
|
-
* no displayed pane — the exact shape of a seam that is wired in tests and
|
|
26284
|
-
* forgotten in the one call site that matters.
|
|
26285
|
-
*/
|
|
26286
|
-
consoleCaptureUrl: options.consoleCaptureUrl,
|
|
26287
|
-
preloadPath: options.preloadPath,
|
|
26288
|
-
userShellPath: options.userShellPath,
|
|
26289
|
-
log: log2
|
|
26290
|
-
});
|
|
26291
|
-
const sessionKey = mintSessionKey();
|
|
26292
|
-
const server = await startRpcServer({
|
|
26293
|
-
key: sessionKey,
|
|
26294
|
-
dispatch: async (method, params, requestId2) => {
|
|
26295
|
-
if (!isConsoleDispatchMethod(method)) {
|
|
26296
|
-
return host.dispatch(method, params, requestId2);
|
|
26297
|
-
}
|
|
26298
|
-
if (!consoleStartup.ok) {
|
|
26299
|
-
throw new ConsoleError(
|
|
26300
|
-
"console_unavailable",
|
|
26301
|
-
`this app's console is not available (${consoleStartup.reason}): ${consoleStartup.detail}`,
|
|
26302
|
-
{ reason: consoleStartup.reason }
|
|
26303
|
-
);
|
|
26304
|
-
}
|
|
26305
|
-
return consoleStartup.dispatch(method, params);
|
|
26306
|
-
},
|
|
26307
|
-
log: log2,
|
|
26308
|
-
capabilities: () => ({ console: consoleStartup.ok })
|
|
26309
|
-
});
|
|
26310
|
-
stateWriter = new BrowserStateWriter(
|
|
26311
|
-
stateFilePath(),
|
|
26312
|
-
() => {
|
|
26313
|
-
const consoleCounts = consoleStartup.ok ? consoleStartup.counts() : { total: 0, agent: 0 };
|
|
26314
|
-
return {
|
|
26315
|
-
tabs: registry.count(),
|
|
26316
|
-
agentTabs: registry.agentTabCount(),
|
|
26317
|
-
profileDir,
|
|
26318
|
-
console: consoleStartup.ok,
|
|
26319
|
-
consoleSurfaces: consoleCounts.total,
|
|
26320
|
-
consoleAgentSurfaces: consoleCounts.agent
|
|
26321
|
-
};
|
|
26322
|
-
},
|
|
26323
|
-
{
|
|
26324
|
-
appVersion: options.appVersion,
|
|
26325
|
-
onError: (error) => log2(`[browser] state file: ${String(error)}`)
|
|
26326
|
-
}
|
|
26327
|
-
);
|
|
26328
|
-
stateWriter.start(server.port, sessionKey);
|
|
26267
|
+
const bindings = {};
|
|
26268
|
+
const notify = () => {
|
|
26269
|
+
if (!options.window.isDestroyed())
|
|
26270
|
+
options.window.webContents.send("browser-state-changed");
|
|
26271
|
+
};
|
|
26272
|
+
unregisterBrowserIpc();
|
|
26329
26273
|
registerBrowserIpc({
|
|
26330
26274
|
window: () => options.window,
|
|
26331
26275
|
expectedUrl: options.expectedUrl,
|
|
26332
|
-
host: () => host,
|
|
26333
|
-
webauthn: () => webauthn,
|
|
26334
|
-
|
|
26335
|
-
|
|
26336
|
-
|
|
26337
|
-
|
|
26338
|
-
|
|
26339
|
-
|
|
26340
|
-
// download.
|
|
26276
|
+
host: () => bindings.host?.() ?? null,
|
|
26277
|
+
webauthn: () => bindings.webauthn?.() ?? null,
|
|
26278
|
+
availability: () => availability,
|
|
26279
|
+
clearData: async (what) => {
|
|
26280
|
+
if (!bindings.clearData)
|
|
26281
|
+
throw new Error("browser_unavailable: The browser is not ready.");
|
|
26282
|
+
return bindings.clearData(what);
|
|
26283
|
+
},
|
|
26341
26284
|
revealDownloads: async () => {
|
|
26342
|
-
|
|
26343
|
-
|
|
26344
|
-
return
|
|
26285
|
+
if (!bindings.revealDownloads)
|
|
26286
|
+
throw new Error("browser_unavailable: The browser is not ready.");
|
|
26287
|
+
return bindings.revealDownloads();
|
|
26345
26288
|
},
|
|
26346
|
-
|
|
26289
|
+
// The recovery click on the `unavailable` state (round 1, D1a). It restarts
|
|
26290
|
+
// THIS module's host for the same window — the same call `createWindow`
|
|
26291
|
+
// makes — so the retry travels the one lifecycle path rather than a second,
|
|
26292
|
+
// parallel start. A doubled click needs no guard of its own:
|
|
26293
|
+
// `startBrowserHost` is already single-flight (a second call for the same
|
|
26294
|
+
// window returns the in-flight `starting` promise), which QA's concurrent
|
|
26295
|
+
// probe confirmed — three simultaneous retries produced ONE startup
|
|
26296
|
+
// sequence. Adding a local guard beside that would be a second mechanism
|
|
26297
|
+
// for a rule the module already owns (round 2, Q-R2-2).
|
|
26298
|
+
retry: async () => {
|
|
26299
|
+
await startBrowserHost(options).catch(() => {
|
|
26300
|
+
});
|
|
26301
|
+
},
|
|
26302
|
+
log: options.log
|
|
26347
26303
|
});
|
|
26348
|
-
|
|
26349
|
-
|
|
26350
|
-
|
|
26351
|
-
|
|
26352
|
-
|
|
26353
|
-
|
|
26354
|
-
|
|
26355
|
-
|
|
26356
|
-
|
|
26357
|
-
|
|
26358
|
-
|
|
26359
|
-
|
|
26360
|
-
|
|
26361
|
-
|
|
26362
|
-
|
|
26363
|
-
|
|
26364
|
-
|
|
26365
|
-
log2(
|
|
26366
|
-
`[browser] not overwriting ${SESSION_FILENAME} at stop: ${decision.reason}`
|
|
26367
|
-
);
|
|
26368
|
-
}
|
|
26369
|
-
sessionStore.flush();
|
|
26370
|
-
registry.destroyAll();
|
|
26371
|
-
await cdp.close();
|
|
26372
|
-
await server.close();
|
|
26373
|
-
await consoleStartup.stop();
|
|
26374
|
-
unregisterBrowserIpc();
|
|
26375
|
-
detachWebauthn();
|
|
26376
|
-
webauthn.dispose();
|
|
26377
|
-
stateWriter?.clear();
|
|
26378
|
-
approvals.resetPending();
|
|
26379
|
-
ownership.clear();
|
|
26380
|
-
flushBrowserStorage(browserSession);
|
|
26381
|
-
try {
|
|
26382
|
-
await sessionCookies.snapshot();
|
|
26383
|
-
} catch (error) {
|
|
26384
|
-
log2(
|
|
26385
|
-
`[browser] session cookies: the snapshot failed (${String(error)})`
|
|
26386
|
-
);
|
|
26387
|
-
}
|
|
26388
|
-
cookieJar.dispose();
|
|
26389
|
-
log2("[browser] host stopped");
|
|
26304
|
+
starting = startWithRetries({
|
|
26305
|
+
cancelled: () => cancelStartup || options.window.isDestroyed(),
|
|
26306
|
+
publish: (next) => {
|
|
26307
|
+
availability = next;
|
|
26308
|
+
notify();
|
|
26309
|
+
},
|
|
26310
|
+
log: options.log,
|
|
26311
|
+
start: async () => {
|
|
26312
|
+
const handle2 = await initializeBrowserHost(options, bindings);
|
|
26313
|
+
availability = {
|
|
26314
|
+
...availability,
|
|
26315
|
+
status: "ready",
|
|
26316
|
+
message: null,
|
|
26317
|
+
canRetry: false
|
|
26318
|
+
};
|
|
26319
|
+
notify();
|
|
26320
|
+
return handle2;
|
|
26390
26321
|
}
|
|
26322
|
+
}).finally(() => {
|
|
26323
|
+
starting = null;
|
|
26324
|
+
startingWindow = null;
|
|
26325
|
+
});
|
|
26326
|
+
return starting;
|
|
26327
|
+
}
|
|
26328
|
+
async function initializeBrowserHost(options, bindings) {
|
|
26329
|
+
const { log: log2 } = options;
|
|
26330
|
+
const rollback = new StartupRollback();
|
|
26331
|
+
let published = false;
|
|
26332
|
+
const checkpoint = () => {
|
|
26333
|
+
if (cancelStartup || options.window.isDestroyed())
|
|
26334
|
+
throw new Error("The browser window closed during startup.");
|
|
26391
26335
|
};
|
|
26392
|
-
|
|
26393
|
-
|
|
26394
|
-
|
|
26395
|
-
|
|
26396
|
-
|
|
26397
|
-
|
|
26398
|
-
|
|
26399
|
-
|
|
26400
|
-
|
|
26401
|
-
|
|
26402
|
-
|
|
26403
|
-
|
|
26404
|
-
|
|
26405
|
-
|
|
26406
|
-
|
|
26407
|
-
|
|
26408
|
-
|
|
26409
|
-
|
|
26410
|
-
|
|
26411
|
-
|
|
26412
|
-
|
|
26413
|
-
|
|
26414
|
-
|
|
26415
|
-
|
|
26416
|
-
|
|
26417
|
-
|
|
26418
|
-
|
|
26419
|
-
|
|
26336
|
+
try {
|
|
26337
|
+
let releaseTab = function(tabId, webContentsId) {
|
|
26338
|
+
const view = views.get(tabId);
|
|
26339
|
+
views.delete(tabId);
|
|
26340
|
+
downloads.forget(tabId);
|
|
26341
|
+
const release = cdp.detach(webContentsId).finally(() => releaseView(options.window, view));
|
|
26342
|
+
releases.add(release);
|
|
26343
|
+
void release.catch(() => {
|
|
26344
|
+
}).finally(() => releases.delete(release));
|
|
26345
|
+
}, buildView = function(window) {
|
|
26346
|
+
const view = new electron.WebContentsView({
|
|
26347
|
+
webPreferences: {
|
|
26348
|
+
partition: BROWSER_PARTITION,
|
|
26349
|
+
contextIsolation: true,
|
|
26350
|
+
nodeIntegration: false,
|
|
26351
|
+
webSecurity: true,
|
|
26352
|
+
allowRunningInsecureContent: false,
|
|
26353
|
+
sandbox: true
|
|
26354
|
+
// No `preload`: a driven page has no bridge to anything.
|
|
26355
|
+
}
|
|
26356
|
+
});
|
|
26357
|
+
window.contentView.addChildView(view);
|
|
26358
|
+
view.setBounds(BACKGROUND_VIEWPORT);
|
|
26359
|
+
view.setVisible(false);
|
|
26360
|
+
return view;
|
|
26361
|
+
}, wireView = function(view, tabId) {
|
|
26362
|
+
const contents = view.webContents;
|
|
26363
|
+
const popupChildren = /* @__PURE__ */ new Set();
|
|
26364
|
+
contents.setWindowOpenHandler((details) => {
|
|
26365
|
+
const decision = decidePopup(
|
|
26366
|
+
{ url: details.url, disposition: details.disposition },
|
|
26367
|
+
{ mode: options.windowShow, live: popupChildren.size }
|
|
26368
|
+
);
|
|
26369
|
+
if (!decision.allow) {
|
|
26370
|
+
log2(
|
|
26371
|
+
`[browser] refused a popup from a driven page: ${details.url} (${decision.reason})`
|
|
26372
|
+
);
|
|
26373
|
+
return { action: "deny" };
|
|
26374
|
+
}
|
|
26420
26375
|
log2(
|
|
26421
|
-
`[browser]
|
|
26376
|
+
`[browser] tab ${tabId} opened a popup: ${details.url} (disposition=${details.disposition}, presentation=${decision.presentation})`
|
|
26422
26377
|
);
|
|
26423
|
-
return {
|
|
26424
|
-
|
|
26425
|
-
|
|
26426
|
-
|
|
26427
|
-
|
|
26428
|
-
|
|
26429
|
-
|
|
26430
|
-
|
|
26431
|
-
|
|
26432
|
-
|
|
26433
|
-
|
|
26434
|
-
|
|
26435
|
-
|
|
26436
|
-
|
|
26437
|
-
|
|
26438
|
-
|
|
26439
|
-
|
|
26440
|
-
|
|
26441
|
-
|
|
26442
|
-
|
|
26443
|
-
|
|
26444
|
-
|
|
26445
|
-
|
|
26446
|
-
|
|
26447
|
-
|
|
26448
|
-
|
|
26449
|
-
|
|
26450
|
-
|
|
26451
|
-
|
|
26452
|
-
|
|
26453
|
-
|
|
26454
|
-
|
|
26455
|
-
|
|
26456
|
-
|
|
26457
|
-
|
|
26458
|
-
|
|
26459
|
-
|
|
26460
|
-
|
|
26461
|
-
|
|
26462
|
-
|
|
26463
|
-
|
|
26464
|
-
// Not security-inherited either: a hidden popup must render like a
|
|
26465
|
-
// shown one so a capture of it is evidence (design 2.5).
|
|
26466
|
-
backgroundThrottling: options.backgroundThrottling
|
|
26467
|
-
// No `preload` — a popup has no bridge to anything, like the view.
|
|
26378
|
+
return {
|
|
26379
|
+
action: "allow",
|
|
26380
|
+
overrideBrowserWindowOptions: {
|
|
26381
|
+
width: 640,
|
|
26382
|
+
height: 720,
|
|
26383
|
+
minWidth: 480,
|
|
26384
|
+
minHeight: 560,
|
|
26385
|
+
center: true,
|
|
26386
|
+
frame: true,
|
|
26387
|
+
autoHideMenuBar: true,
|
|
26388
|
+
// Nothing shows itself: presentation happens only through
|
|
26389
|
+
// `presentPopupWindow` (window-raise.ts), under the launch plan's own
|
|
26390
|
+
// gate. Under `never` the window exists hidden and drivable over CDP.
|
|
26391
|
+
show: false,
|
|
26392
|
+
// Defence in depth beside the plan gate: a window that will never be
|
|
26393
|
+
// shown must also not be focusable — the same pairing `window-mode.ts`
|
|
26394
|
+
// applies to the main window in `headless`.
|
|
26395
|
+
focusable: decision.presentation !== "never",
|
|
26396
|
+
backgroundColor: "#FFFFFF",
|
|
26397
|
+
webPreferences: {
|
|
26398
|
+
/*
|
|
26399
|
+
* EXPLICIT, and load-bearing (design 2.3): `partition` is NOT among the
|
|
26400
|
+
* security-related prefs Electron merges from the parent, and a popup
|
|
26401
|
+
* created in a different session is not RELATED to its opener —
|
|
26402
|
+
* Chromium severs `window.opener`, which is the entire contract a
|
|
26403
|
+
* popup-based sign-in needs. The `about:blank` case lands in this same
|
|
26404
|
+
* session by prefs-copy instead of by this line.
|
|
26405
|
+
*/
|
|
26406
|
+
partition: BROWSER_PARTITION,
|
|
26407
|
+
sandbox: true,
|
|
26408
|
+
contextIsolation: true,
|
|
26409
|
+
nodeIntegration: false,
|
|
26410
|
+
webSecurity: true,
|
|
26411
|
+
allowRunningInsecureContent: false,
|
|
26412
|
+
spellcheck: false,
|
|
26413
|
+
navigateOnDragDrop: false,
|
|
26414
|
+
// Not security-inherited either: a hidden popup must render like a
|
|
26415
|
+
// shown one so a capture of it is evidence (design 2.5).
|
|
26416
|
+
backgroundThrottling: options.backgroundThrottling
|
|
26417
|
+
// No `preload` — a popup has no bridge to anything, like the view.
|
|
26418
|
+
}
|
|
26468
26419
|
}
|
|
26420
|
+
};
|
|
26421
|
+
});
|
|
26422
|
+
contents.on("did-create-window", (child, details) => {
|
|
26423
|
+
wirePopup(child, details, popupChildren);
|
|
26424
|
+
});
|
|
26425
|
+
const onWillNavigate = (event, rawUrl) => {
|
|
26426
|
+
if (refusesNavigation(rawUrl)) {
|
|
26427
|
+
event.preventDefault();
|
|
26428
|
+
log2(`[browser] refused a navigation to ${rawUrl}`);
|
|
26469
26429
|
}
|
|
26470
26430
|
};
|
|
26471
|
-
|
|
26472
|
-
|
|
26473
|
-
|
|
26474
|
-
|
|
26475
|
-
|
|
26476
|
-
|
|
26431
|
+
contents.on("will-navigate", onWillNavigate);
|
|
26432
|
+
contents.on("will-redirect", onWillNavigate);
|
|
26433
|
+
contents.on("before-input-event", (event, input) => {
|
|
26434
|
+
const direction = paneNavigationDirection(input);
|
|
26435
|
+
if (!direction) return;
|
|
26436
|
+
event.preventDefault();
|
|
26437
|
+
host.historyActive(direction);
|
|
26438
|
+
});
|
|
26439
|
+
const notifyChrome = () => {
|
|
26440
|
+
options.window.webContents.send("browser-state-changed");
|
|
26441
|
+
};
|
|
26442
|
+
contents.on("did-start-loading", () => {
|
|
26443
|
+
host.clearLoadFailure(tabId);
|
|
26444
|
+
notifyChrome();
|
|
26445
|
+
});
|
|
26446
|
+
contents.on("did-stop-loading", notifyChrome);
|
|
26447
|
+
contents.on("page-title-updated", notifyChrome);
|
|
26448
|
+
contents.on(
|
|
26449
|
+
"did-fail-load",
|
|
26450
|
+
(_event, code, description, url, isMainFrame) => {
|
|
26451
|
+
if (!isMainFrame) return;
|
|
26452
|
+
if (isReportableLoadFailure(code)) {
|
|
26453
|
+
host.recordLoadFailure(tabId, {
|
|
26454
|
+
code,
|
|
26455
|
+
description,
|
|
26456
|
+
url
|
|
26457
|
+
});
|
|
26458
|
+
}
|
|
26459
|
+
notifyChrome();
|
|
26460
|
+
}
|
|
26461
|
+
);
|
|
26462
|
+
contents.on("will-attach-webview", (event) => {
|
|
26463
|
+
event.preventDefault();
|
|
26464
|
+
log2("[browser] refused a webview attachment inside a driven page");
|
|
26465
|
+
});
|
|
26466
|
+
contents.on("did-navigate", (_event, url) => {
|
|
26467
|
+
registry.bumpEpoch(tabId);
|
|
26468
|
+
host.clearLoadFailure(tabId);
|
|
26469
|
+
stopLogCapture(contents.id);
|
|
26470
|
+
startLogCapture(contents.id);
|
|
26471
|
+
log2(`[browser] tab ${tabId} navigated to ${url}`);
|
|
26472
|
+
notifyChanged();
|
|
26473
|
+
});
|
|
26474
|
+
contents.on("did-navigate-in-page", (_event, url, isMainFrame) => {
|
|
26475
|
+
if (!isMainFrame) return;
|
|
26476
|
+
registry.bumpEpoch(tabId, false);
|
|
26477
|
+
log2(`[browser] tab ${tabId} navigated in page to ${url}`);
|
|
26478
|
+
notifyChanged();
|
|
26479
|
+
});
|
|
26480
|
+
contents.on("destroyed", () => {
|
|
26481
|
+
const record = registry.get(tabId);
|
|
26482
|
+
if (record) {
|
|
26483
|
+
const token = surfaceToken(record);
|
|
26484
|
+
if (token) approvals.forgetDocument(token);
|
|
26485
|
+
registry.forget(tabId);
|
|
26486
|
+
}
|
|
26487
|
+
host.clearLoadFailure(tabId);
|
|
26488
|
+
releaseTab(tabId, contents.id);
|
|
26489
|
+
});
|
|
26490
|
+
}, wirePopup = function(child, details, popupChildren) {
|
|
26491
|
+
popupChildren.add(child);
|
|
26492
|
+
child.on("closed", () => popupChildren.delete(child));
|
|
26493
|
+
const childContents = child.webContents;
|
|
26494
|
+
childContents.setWindowOpenHandler((childDetails) => {
|
|
26495
|
+
log2(`[browser] refused a popup from a popup: ${childDetails.url}`);
|
|
26496
|
+
return { action: "deny" };
|
|
26497
|
+
});
|
|
26498
|
+
const onWillNavigate = (event, rawUrl) => {
|
|
26499
|
+
if (refusesNavigation(rawUrl)) {
|
|
26500
|
+
event.preventDefault();
|
|
26501
|
+
log2(`[browser] refused a popup navigation to ${rawUrl}`);
|
|
26502
|
+
}
|
|
26503
|
+
};
|
|
26504
|
+
childContents.on("will-navigate", onWillNavigate);
|
|
26505
|
+
childContents.on("will-redirect", onWillNavigate);
|
|
26506
|
+
childContents.on("will-attach-webview", (event) => {
|
|
26507
|
+
event.preventDefault();
|
|
26508
|
+
log2("[browser] refused a webview attachment inside a popup");
|
|
26509
|
+
});
|
|
26510
|
+
childContents.on("before-input-event", (event, input) => {
|
|
26511
|
+
const direction = paneNavigationDirection(input);
|
|
26512
|
+
if (!direction) return;
|
|
26477
26513
|
event.preventDefault();
|
|
26478
|
-
|
|
26514
|
+
const navigation = childContents.navigationHistory;
|
|
26515
|
+
if (direction === "back") navigation.goBack();
|
|
26516
|
+
else navigation.goForward();
|
|
26517
|
+
});
|
|
26518
|
+
if (!options.backgroundThrottling) {
|
|
26519
|
+
childContents.setBackgroundThrottling(false);
|
|
26479
26520
|
}
|
|
26521
|
+
presentPopupWindow(
|
|
26522
|
+
child,
|
|
26523
|
+
effectivePresentation(options.windowShow, details.disposition),
|
|
26524
|
+
{ trigger: "popup-open", report: options.reportRaise }
|
|
26525
|
+
);
|
|
26480
26526
|
};
|
|
26481
|
-
|
|
26482
|
-
|
|
26483
|
-
|
|
26484
|
-
|
|
26485
|
-
|
|
26486
|
-
|
|
26487
|
-
|
|
26527
|
+
const browserSession = resolveBrowserSession();
|
|
26528
|
+
let registryForDownloads = null;
|
|
26529
|
+
const downloads = new DownloadArmer({
|
|
26530
|
+
tabForWebContents: (webContentsId) => registryForDownloads?.byWebContents(webContentsId)?.tabId ?? null,
|
|
26531
|
+
// WHO OWNED THE TAB, recorded on the decision (review round 2, U10). Read from
|
|
26532
|
+
// the registry at the moment the note is written, because the row may render it
|
|
26533
|
+
// after the tab is closed — and "· on another tab" about a tab that no longer
|
|
26534
|
+
// exists is a marker pointing at nothing. `null` when the lookup cannot answer
|
|
26535
|
+
// (a tab already gone), which the row renders the old way rather than guessing.
|
|
26536
|
+
ownerKindFor: (tabId) => registryForDownloads?.get(tabId)?.owner ?? null,
|
|
26537
|
+
log: log2,
|
|
26538
|
+
onActivity: () => {
|
|
26539
|
+
if (options.window.isDestroyed()) return;
|
|
26540
|
+
options.window.webContents.send("browser-state-changed");
|
|
26541
|
+
}
|
|
26542
|
+
});
|
|
26543
|
+
const detachSessionHandlers = installBrowserSessionHandlers(
|
|
26544
|
+
browserSession,
|
|
26545
|
+
{
|
|
26546
|
+
onPermissionRequested: (details) => {
|
|
26547
|
+
log2(
|
|
26548
|
+
`[browser] denied a ${details.permission} permission request from ${details.origin || "(unknown origin)"}`
|
|
26549
|
+
);
|
|
26550
|
+
},
|
|
26551
|
+
onDownload: (item, webContentsId) => downloads.decide(item, webContentsId),
|
|
26552
|
+
onDownloadDecided: (outcome) => {
|
|
26553
|
+
if (outcome.savePath) {
|
|
26554
|
+
log2(
|
|
26555
|
+
`[browser] saved a download from ${outcome.url} to ${outcome.savePath}`
|
|
26556
|
+
);
|
|
26557
|
+
}
|
|
26558
|
+
},
|
|
26559
|
+
log: log2
|
|
26560
|
+
}
|
|
26561
|
+
);
|
|
26562
|
+
rollback.add(detachSessionHandlers);
|
|
26563
|
+
configurePslRules(PSL_RULES);
|
|
26564
|
+
await installWebauthn({ log: log2 });
|
|
26565
|
+
checkpoint();
|
|
26566
|
+
const describeSource = (frame) => {
|
|
26567
|
+
if (!frame || typeof frame !== "object") {
|
|
26568
|
+
return { tabId: null, pageTitle: null };
|
|
26569
|
+
}
|
|
26570
|
+
const contents = electron.webContents.fromFrame(frame);
|
|
26571
|
+
const record = contents === void 0 ? void 0 : registry.byWebContentsId(contents.id);
|
|
26572
|
+
if (!record) return { tabId: null, pageTitle: null };
|
|
26573
|
+
const title = record.view.webContents.isDestroyed() ? "" : record.view.webContents.getTitle();
|
|
26574
|
+
return { tabId: record.tabId, pageTitle: title || null };
|
|
26575
|
+
};
|
|
26576
|
+
const webauthn = new WebauthnChooser({
|
|
26577
|
+
notify: (request) => {
|
|
26578
|
+
if (options.window.isDestroyed()) return;
|
|
26579
|
+
options.window.webContents.send("browser-webauthn-request", request);
|
|
26580
|
+
},
|
|
26581
|
+
/*
|
|
26582
|
+
* AND THE OTHER DIRECTION: every settle is pushed, not only every arrival.
|
|
26583
|
+
*
|
|
26584
|
+
* Without this the renderer cannot tell a live request from one main has
|
|
26585
|
+
* already cancelled, so a chooser whose request expired while the user was
|
|
26586
|
+
* away stays on screen offering rows that can no longer do anything (design
|
|
26587
|
+
* round 1, D2) — and the click is then discarded silently. The outcome travels
|
|
26588
|
+
* with the push because the two cases differ for the user: their own answer is
|
|
26589
|
+
* already off the screen, an expiry has to be explained.
|
|
26590
|
+
*/
|
|
26591
|
+
onSettled: (requestId2, outcome) => {
|
|
26592
|
+
if (options.window.isDestroyed()) return;
|
|
26593
|
+
options.window.webContents.send("browser-webauthn-settled", {
|
|
26594
|
+
requestId: requestId2,
|
|
26595
|
+
outcome
|
|
26596
|
+
});
|
|
26597
|
+
},
|
|
26598
|
+
describeSource,
|
|
26599
|
+
log: log2
|
|
26600
|
+
});
|
|
26601
|
+
rollback.add(() => webauthn.dispose());
|
|
26602
|
+
const detachWebauthn = attachWebauthnChooser(browserSession, webauthn, log2);
|
|
26603
|
+
rollback.add(detachWebauthn);
|
|
26604
|
+
const cookieJar = createHiddenCookieJarTarget(
|
|
26605
|
+
new electron.WebContentsView({
|
|
26606
|
+
webPreferences: { ...HIDDEN_COOKIE_JAR_WEB_PREFERENCES }
|
|
26607
|
+
})
|
|
26608
|
+
);
|
|
26609
|
+
rollback.add(() => cookieJar.dispose());
|
|
26610
|
+
const sessionCookies = new SessionCookieVault({
|
|
26611
|
+
jar: cookieJar.jar,
|
|
26612
|
+
cipher: browserProfileCipher(),
|
|
26613
|
+
...sessionCookiePaths(options.userDataDir),
|
|
26614
|
+
flushStore: () => browserSession.cookies.flushStore(),
|
|
26615
|
+
clearSessionData: (what) => clearBrowsingData(browserSession, what),
|
|
26616
|
+
log: log2
|
|
26617
|
+
});
|
|
26618
|
+
let restoreReport = null;
|
|
26619
|
+
try {
|
|
26620
|
+
restoreReport = await sessionCookies.restore();
|
|
26621
|
+
} catch (error) {
|
|
26622
|
+
log2(`[browser] session cookies: the restore failed (${String(error)})`);
|
|
26623
|
+
}
|
|
26624
|
+
checkpoint();
|
|
26625
|
+
const cdp = new CdpPool({ log: log2 });
|
|
26626
|
+
rollback.add(() => cdp.close());
|
|
26627
|
+
const views = /* @__PURE__ */ new Map();
|
|
26628
|
+
const releases = /* @__PURE__ */ new Set();
|
|
26629
|
+
let stateWriter = null;
|
|
26630
|
+
const sessionStore = new BrowserSessionStore({
|
|
26631
|
+
dir: path$1.join(options.userDataDir, "browser"),
|
|
26632
|
+
log: log2
|
|
26488
26633
|
});
|
|
26489
|
-
const
|
|
26634
|
+
const consentNotifier = new ConsentNotifier({
|
|
26635
|
+
show: options.windowShow,
|
|
26636
|
+
/*
|
|
26637
|
+
* The click's own rule lives in `consentClickHandler` (see it: the delivered
|
|
26638
|
+
* payload, the order, and the raise are all asserted there), and this is the
|
|
26639
|
+
* wiring that gives it the real window and the real plan. `windowShow` is the
|
|
26640
|
+
* plan the click comes forward under — this module still never decides whether a
|
|
26641
|
+
* window comes forward (design 11.4), it hands the plan to `window-raise.ts`.
|
|
26642
|
+
*/
|
|
26643
|
+
onAttention: consentClickHandler({
|
|
26644
|
+
/*
|
|
26645
|
+
* THE WINDOW IS ASKED FOR, NOT CAPTURED, and the difference is the U2 fix:
|
|
26646
|
+
* this host is stopped the moment its window closes (`src/main/index.ts`'s
|
|
26647
|
+
* `closed` hook), but a banner already raised is still in Notification
|
|
26648
|
+
* Center and its click still lands here. A captured window would be a
|
|
26649
|
+
* destroyed one by then, and reading it throws.
|
|
26650
|
+
*/
|
|
26651
|
+
window: () => options.window,
|
|
26652
|
+
show: options.windowShow,
|
|
26653
|
+
report: options.reportRaise,
|
|
26654
|
+
reopen: options.reopenConsent
|
|
26655
|
+
}),
|
|
26656
|
+
log: log2
|
|
26657
|
+
});
|
|
26658
|
+
const captureSession = () => {
|
|
26659
|
+
if (!published) return;
|
|
26660
|
+
sessionStore.record(
|
|
26661
|
+
captureTabs(
|
|
26662
|
+
registry.list(),
|
|
26663
|
+
registry.activeTab?.tabId ?? null,
|
|
26664
|
+
host.failedTabIds()
|
|
26665
|
+
)
|
|
26666
|
+
);
|
|
26667
|
+
};
|
|
26668
|
+
const notifyChanged = () => {
|
|
26669
|
+
stateWriter?.publishNow();
|
|
26490
26670
|
options.window.webContents.send("browser-state-changed");
|
|
26671
|
+
captureSession();
|
|
26491
26672
|
};
|
|
26492
|
-
|
|
26493
|
-
|
|
26494
|
-
|
|
26673
|
+
const registry = new TabRegistry(
|
|
26674
|
+
(_createOptions, tabId) => {
|
|
26675
|
+
const view = buildView(options.window);
|
|
26676
|
+
views.set(tabId, view);
|
|
26677
|
+
wireView(view, tabId);
|
|
26678
|
+
return view;
|
|
26679
|
+
},
|
|
26680
|
+
releaseTab,
|
|
26681
|
+
notifyChanged
|
|
26682
|
+
);
|
|
26683
|
+
registryForDownloads = registry;
|
|
26684
|
+
rollback.add(async () => {
|
|
26685
|
+
published = false;
|
|
26686
|
+
sessionStore.discard();
|
|
26687
|
+
registry.destroyAll();
|
|
26688
|
+
await Promise.allSettled([...releases]);
|
|
26495
26689
|
});
|
|
26496
|
-
|
|
26497
|
-
|
|
26498
|
-
|
|
26499
|
-
|
|
26500
|
-
|
|
26501
|
-
|
|
26502
|
-
|
|
26503
|
-
|
|
26504
|
-
|
|
26505
|
-
|
|
26506
|
-
|
|
26507
|
-
|
|
26690
|
+
const ownership = new OwnershipLedger({
|
|
26691
|
+
mayAdopt: (token, requester) => {
|
|
26692
|
+
const record = registry.requireSurface(token);
|
|
26693
|
+
return record.handedTo === requester.slice("session:".length) && !record.allocationId;
|
|
26694
|
+
},
|
|
26695
|
+
closeTab: async (token) => {
|
|
26696
|
+
try {
|
|
26697
|
+
const record = registry.requireSurface(token);
|
|
26698
|
+
approvals.forgetDocument(token);
|
|
26699
|
+
registry.destroy(record.tabId);
|
|
26700
|
+
} catch {
|
|
26701
|
+
}
|
|
26702
|
+
return true;
|
|
26703
|
+
},
|
|
26704
|
+
isLive: (token) => {
|
|
26705
|
+
try {
|
|
26706
|
+
registry.requireSurface(token);
|
|
26707
|
+
return true;
|
|
26708
|
+
} catch {
|
|
26709
|
+
return false;
|
|
26508
26710
|
}
|
|
26509
|
-
notifyChrome();
|
|
26510
26711
|
}
|
|
26511
|
-
);
|
|
26512
|
-
contents.on("will-attach-webview", (event) => {
|
|
26513
|
-
event.preventDefault();
|
|
26514
|
-
log2("[browser] refused a webview attachment inside a driven page");
|
|
26515
26712
|
});
|
|
26516
|
-
|
|
26517
|
-
|
|
26518
|
-
|
|
26519
|
-
|
|
26520
|
-
|
|
26521
|
-
|
|
26522
|
-
|
|
26713
|
+
const approvals = new ApprovalStore({
|
|
26714
|
+
dir: path$1.join(options.userDataDir, "browser"),
|
|
26715
|
+
onChanged: () => {
|
|
26716
|
+
options.window.webContents.send("browser-consent-changed");
|
|
26717
|
+
consentNotifier.announce(approvals.pendingEntries());
|
|
26718
|
+
},
|
|
26719
|
+
log: log2
|
|
26523
26720
|
});
|
|
26524
|
-
|
|
26525
|
-
|
|
26526
|
-
|
|
26527
|
-
|
|
26528
|
-
|
|
26721
|
+
const profileDir = browserStoragePath(browserSession) ?? "";
|
|
26722
|
+
const facts = {
|
|
26723
|
+
proto: 1,
|
|
26724
|
+
appVersion: options.appVersion,
|
|
26725
|
+
profileDir,
|
|
26726
|
+
profilePersistent: browserSession.isPersistent(),
|
|
26727
|
+
userAgent: browserUserAgent(),
|
|
26728
|
+
domainScope: domainScopeAvailable()
|
|
26729
|
+
};
|
|
26730
|
+
const host = new BrowserHost({
|
|
26731
|
+
registry,
|
|
26732
|
+
cdp,
|
|
26733
|
+
approvals,
|
|
26734
|
+
ownership,
|
|
26735
|
+
downloads,
|
|
26736
|
+
log: log2,
|
|
26737
|
+
onChanged: notifyChanged,
|
|
26738
|
+
facts: () => facts,
|
|
26739
|
+
forgetSiteData: (origin) => clearOriginData(browserSession, origin)
|
|
26529
26740
|
});
|
|
26530
|
-
|
|
26531
|
-
|
|
26532
|
-
|
|
26533
|
-
|
|
26534
|
-
|
|
26535
|
-
|
|
26536
|
-
|
|
26537
|
-
|
|
26538
|
-
|
|
26741
|
+
const recorded = readSession(sessionStore.filePath, log2);
|
|
26742
|
+
host.restoreTabs(recorded);
|
|
26743
|
+
if (recorded.length) {
|
|
26744
|
+
log2(
|
|
26745
|
+
`[browser] restored ${recorded.length} tab(s) from ${SESSION_FILENAME}; every restored tab is user-owned and holds no handle (design 7.3)`
|
|
26746
|
+
);
|
|
26747
|
+
}
|
|
26748
|
+
const consoleStartup = await startConsoleHost({
|
|
26749
|
+
window: options.window,
|
|
26750
|
+
expectedUrl: options.expectedUrl,
|
|
26751
|
+
appVersion: options.appVersion,
|
|
26752
|
+
notifier: options.notifier,
|
|
26753
|
+
/*
|
|
26754
|
+
* The capture view's document and preload, forwarded rather than re-derived:
|
|
26755
|
+
* only the app knows where its own bundle lives, and a console host that
|
|
26756
|
+
* guessed would answer a screenshot with a blank frame rather than with its
|
|
26757
|
+
* own typed refusal. WITHOUT THESE TWO the host builds no capture view at all,
|
|
26758
|
+
* which the live proof rig caught as `capture_unavailable` on a surface with
|
|
26759
|
+
* no displayed pane — the exact shape of a seam that is wired in tests and
|
|
26760
|
+
* forgotten in the one call site that matters.
|
|
26761
|
+
*/
|
|
26762
|
+
consoleCaptureUrl: options.consoleCaptureUrl,
|
|
26763
|
+
preloadPath: options.preloadPath,
|
|
26764
|
+
userShellPath: options.userShellPath,
|
|
26765
|
+
log: log2
|
|
26539
26766
|
});
|
|
26540
|
-
|
|
26541
|
-
|
|
26542
|
-
|
|
26543
|
-
|
|
26544
|
-
|
|
26545
|
-
|
|
26546
|
-
|
|
26547
|
-
|
|
26767
|
+
rollback.add(() => consoleStartup.stop());
|
|
26768
|
+
checkpoint();
|
|
26769
|
+
const sessionKey = mintSessionKey();
|
|
26770
|
+
const server = await startRpcServer({
|
|
26771
|
+
key: sessionKey,
|
|
26772
|
+
instanceId: hostInstanceId,
|
|
26773
|
+
dispatch: async (method, params, requestId2) => {
|
|
26774
|
+
if (!isConsoleDispatchMethod(method)) {
|
|
26775
|
+
return host.dispatch(method, params, requestId2);
|
|
26776
|
+
}
|
|
26777
|
+
if (!consoleStartup.ok) {
|
|
26778
|
+
throw new ConsoleError(
|
|
26779
|
+
"console_unavailable",
|
|
26780
|
+
`this app's console is not available (${consoleStartup.reason}): ${consoleStartup.detail}`,
|
|
26781
|
+
{ reason: consoleStartup.reason }
|
|
26782
|
+
);
|
|
26783
|
+
}
|
|
26784
|
+
return consoleStartup.dispatch(method, params);
|
|
26785
|
+
},
|
|
26786
|
+
log: log2,
|
|
26787
|
+
capabilities: () => ({ console: consoleStartup.ok })
|
|
26548
26788
|
});
|
|
26549
|
-
|
|
26550
|
-
|
|
26551
|
-
|
|
26552
|
-
|
|
26789
|
+
rollback.add(() => server.close());
|
|
26790
|
+
checkpoint();
|
|
26791
|
+
stateWriter = new BrowserStateWriter(
|
|
26792
|
+
stateFilePath(),
|
|
26793
|
+
() => {
|
|
26794
|
+
const consoleCounts = consoleStartup.ok ? consoleStartup.counts() : { total: 0, agent: 0 };
|
|
26795
|
+
return {
|
|
26796
|
+
tabs: registry.count(),
|
|
26797
|
+
agentTabs: registry.agentTabCount(),
|
|
26798
|
+
profileDir,
|
|
26799
|
+
console: consoleStartup.ok,
|
|
26800
|
+
consoleSurfaces: consoleCounts.total,
|
|
26801
|
+
consoleAgentSurfaces: consoleCounts.agent
|
|
26802
|
+
};
|
|
26803
|
+
},
|
|
26804
|
+
{
|
|
26805
|
+
appVersion: options.appVersion,
|
|
26806
|
+
instanceId: hostInstanceId,
|
|
26807
|
+
onError: (error) => log2(`[browser] state file: ${String(error)}`)
|
|
26553
26808
|
}
|
|
26554
|
-
|
|
26555
|
-
|
|
26556
|
-
|
|
26557
|
-
|
|
26558
|
-
|
|
26559
|
-
|
|
26560
|
-
|
|
26561
|
-
|
|
26562
|
-
|
|
26563
|
-
|
|
26564
|
-
|
|
26565
|
-
|
|
26566
|
-
|
|
26567
|
-
|
|
26809
|
+
);
|
|
26810
|
+
rollback.add(() => stateWriter?.clear());
|
|
26811
|
+
stateWriter.start(server.port, sessionKey);
|
|
26812
|
+
Object.assign(bindings, {
|
|
26813
|
+
window: () => options.window,
|
|
26814
|
+
expectedUrl: options.expectedUrl,
|
|
26815
|
+
host: () => host,
|
|
26816
|
+
webauthn: () => webauthn,
|
|
26817
|
+
clearData: (what) => sessionCookies.clearBrowsingData(what),
|
|
26818
|
+
// THE REVEAL TAKES NO PATH FROM THE RENDERER (§16.4): it opens the directory the
|
|
26819
|
+
// host is actually writing into, so the one place a page-derived string could
|
|
26820
|
+
// have become a path stays out of the IPC surface as well. `shell.openPath`
|
|
26821
|
+
// answers "" on success and a message on failure, which is returned rather than
|
|
26822
|
+
// thrown: a reveal that fails is a Finder problem, not a fault in the agent's
|
|
26823
|
+
// download.
|
|
26824
|
+
revealDownloads: async () => {
|
|
26825
|
+
const dir = downloads.downloadDir();
|
|
26826
|
+
if (!dir) return "no download directory yet";
|
|
26827
|
+
return electron.shell.openPath(dir);
|
|
26828
|
+
},
|
|
26829
|
+
log: log2
|
|
26568
26830
|
});
|
|
26569
|
-
|
|
26570
|
-
|
|
26571
|
-
}
|
|
26572
|
-
presentPopupWindow(
|
|
26573
|
-
child,
|
|
26574
|
-
effectivePresentation(options.windowShow, details.disposition),
|
|
26575
|
-
{ trigger: "popup-open", report: options.reportRaise }
|
|
26831
|
+
log2(
|
|
26832
|
+
`[browser] host on ${server.address}:${server.port} (proto ${facts.proto}), profile ${profileDir || "(in-memory)"}, agent tabs ${registry.agentTabCount()}/${8}, console ${consoleStartup.ok ? `on (${consoleStartup.counts().total} surface(s))` : `off (${consoleStartup.reason})`}`
|
|
26576
26833
|
);
|
|
26834
|
+
const handle2 = {
|
|
26835
|
+
host,
|
|
26836
|
+
port: server.port,
|
|
26837
|
+
profileDir,
|
|
26838
|
+
agentTabs: () => registry.agentTabCount(),
|
|
26839
|
+
sessionCookies: () => restoreReport,
|
|
26840
|
+
stop: async () => {
|
|
26841
|
+
const atStop = captureTabs(
|
|
26842
|
+
registry.list(),
|
|
26843
|
+
registry.activeTab?.tabId ?? null,
|
|
26844
|
+
host.failedTabIds()
|
|
26845
|
+
);
|
|
26846
|
+
const decision = sessionStore.commitStopCapture(atStop);
|
|
26847
|
+
if (!decision.write) {
|
|
26848
|
+
log2(
|
|
26849
|
+
`[browser] not overwriting ${SESSION_FILENAME} at stop: ${decision.reason}`
|
|
26850
|
+
);
|
|
26851
|
+
}
|
|
26852
|
+
sessionStore.flush();
|
|
26853
|
+
registry.destroyAll();
|
|
26854
|
+
await Promise.allSettled([...releases]);
|
|
26855
|
+
await cdp.close();
|
|
26856
|
+
await server.close();
|
|
26857
|
+
await consoleStartup.stop();
|
|
26858
|
+
unregisterBrowserIpc();
|
|
26859
|
+
detachSessionHandlers();
|
|
26860
|
+
detachWebauthn();
|
|
26861
|
+
webauthn.dispose();
|
|
26862
|
+
stateWriter?.clear();
|
|
26863
|
+
approvals.resetPending();
|
|
26864
|
+
ownership.clear();
|
|
26865
|
+
flushBrowserStorage(browserSession);
|
|
26866
|
+
try {
|
|
26867
|
+
await sessionCookies.snapshot();
|
|
26868
|
+
} catch (error) {
|
|
26869
|
+
log2(
|
|
26870
|
+
`[browser] session cookies: the snapshot failed (${String(error)})`
|
|
26871
|
+
);
|
|
26872
|
+
}
|
|
26873
|
+
cookieJar.dispose();
|
|
26874
|
+
log2("[browser] host stopped");
|
|
26875
|
+
}
|
|
26876
|
+
};
|
|
26877
|
+
checkpoint();
|
|
26878
|
+
published = true;
|
|
26879
|
+
activeHost = handle2;
|
|
26880
|
+
rollback.commit();
|
|
26881
|
+
return handle2;
|
|
26882
|
+
} catch (error) {
|
|
26883
|
+
published = false;
|
|
26884
|
+
for (const key of Object.keys(bindings))
|
|
26885
|
+
delete bindings[key];
|
|
26886
|
+
await rollback.run(log2);
|
|
26887
|
+
throw error;
|
|
26577
26888
|
}
|
|
26578
26889
|
}
|
|
26579
26890
|
function releaseView(window, view) {
|
|
@@ -37741,6 +38052,15 @@ class UpdateService {
|
|
|
37741
38052
|
* so it is kept rather than fetched again on the click (review U17).
|
|
37742
38053
|
*/
|
|
37743
38054
|
lastPublishedBackendVersion = null;
|
|
38055
|
+
/**
|
|
38056
|
+
* The version PyPI most recently NAMED whose files were not yet
|
|
38057
|
+
* downloadable, or null. Kept beside the published reading because the
|
|
38058
|
+
* manual check's sentence depends on the distinction (UX round 1, U1):
|
|
38059
|
+
* "a release is still publishing" and "the version could not be
|
|
38060
|
+
* determined" are different facts with different next steps, and the
|
|
38061
|
+
* gated read alone collapses them into one null.
|
|
38062
|
+
*/
|
|
38063
|
+
pendingPypiVersion = null;
|
|
37744
38064
|
/** Path of the artifact this run downloaded, when the updater reported one. */
|
|
37745
38065
|
downloadedArtifactPath = null;
|
|
37746
38066
|
/**
|
|
@@ -40550,8 +40870,17 @@ ${heal.removed.join("\n")}`,
|
|
|
40550
40870
|
return (await this.readRunningBackend()).version;
|
|
40551
40871
|
}
|
|
40552
40872
|
/**
|
|
40553
|
-
* Get the latest version from PyPI
|
|
40554
|
-
*
|
|
40873
|
+
* Get the latest READY version from PyPI: the newest release whose
|
|
40874
|
+
* platform wheel an installer can actually download, per
|
|
40875
|
+
* `readPypiReleaseBody`. A version PyPI has named but whose files have
|
|
40876
|
+
* not finished uploading resolves null — the same answer as an
|
|
40877
|
+
* unreadable response, because both mean do not offer yet; the next
|
|
40878
|
+
* scheduled check re-reads. The named-but-not-ready version is kept on
|
|
40879
|
+
* `pendingPypiVersion` so the manual check can say which state it is
|
|
40880
|
+
* in. Reading only `info.version` here was the 2026-10-12 race: the
|
|
40881
|
+
* offer appeared minutes after publication and `lop update` exited 1
|
|
40882
|
+
* against artifacts that were not on PyPI yet.
|
|
40883
|
+
* @returns Promise resolving to the latest ready version string or null if unable to fetch
|
|
40555
40884
|
*/
|
|
40556
40885
|
getLatestPypiVersion() {
|
|
40557
40886
|
return new Promise((resolve) => {
|
|
@@ -40567,14 +40896,26 @@ ${heal.removed.join("\n")}`,
|
|
|
40567
40896
|
{ timeout: PYPI_VERSION_READ_TIMEOUT_MS },
|
|
40568
40897
|
(res) => {
|
|
40569
40898
|
let data = "";
|
|
40899
|
+
const settleAborted = () => {
|
|
40900
|
+
this.pendingPypiVersion = null;
|
|
40901
|
+
resolve(null);
|
|
40902
|
+
};
|
|
40903
|
+
res.on("error", settleAborted);
|
|
40904
|
+
res.on("aborted", settleAborted);
|
|
40570
40905
|
res.on("data", (chunk) => {
|
|
40571
40906
|
data += chunk;
|
|
40572
40907
|
});
|
|
40573
40908
|
res.on("end", () => {
|
|
40574
40909
|
try {
|
|
40575
|
-
const
|
|
40576
|
-
|
|
40577
|
-
|
|
40910
|
+
const reading = readPypiReleaseBody(data, process.platform);
|
|
40911
|
+
this.pendingPypiVersion = reading.readyVersion === null ? reading.namedVersion : null;
|
|
40912
|
+
if (reading.readyVersion === null && reading.namedVersion) {
|
|
40913
|
+
logger.info(
|
|
40914
|
+
`PyPI names version ${reading.namedVersion} but its release has no downloadable wheel for this platform yet; not offering it. The next check will re-read.`,
|
|
40915
|
+
LogFileType.UPDATE_SERVICE
|
|
40916
|
+
);
|
|
40917
|
+
}
|
|
40918
|
+
resolve(reading.readyVersion);
|
|
40578
40919
|
} catch (error) {
|
|
40579
40920
|
logger.error(
|
|
40580
40921
|
"Error parsing PyPI response:",
|
|
@@ -40594,6 +40935,7 @@ ${heal.removed.join("\n")}`,
|
|
|
40594
40935
|
);
|
|
40595
40936
|
});
|
|
40596
40937
|
request.on("error", (error) => {
|
|
40938
|
+
this.pendingPypiVersion = null;
|
|
40597
40939
|
logger.error(
|
|
40598
40940
|
"Error fetching from PyPI:",
|
|
40599
40941
|
LogFileType.UPDATE_SERVICE,
|
|
@@ -41028,7 +41370,19 @@ ${heal.removed.join("\n")}`,
|
|
|
41028
41370
|
);
|
|
41029
41371
|
if (reportFailure) {
|
|
41030
41372
|
this.sendToRenderer("backend-update-error", {
|
|
41031
|
-
|
|
41373
|
+
/*
|
|
41374
|
+
* The not-ready state earns its own sentence (UX round 1, U1):
|
|
41375
|
+
* PyPI answered and named a release whose files are still
|
|
41376
|
+
* publishing, which is a wait, not a fault — the generic
|
|
41377
|
+
* sentence below reads as an installation problem and sends
|
|
41378
|
+
* the reader to debug one. Same producer-sentence shape as
|
|
41379
|
+
* the unreadable-install branch below. No retry action is
|
|
41380
|
+
* attached: the shared classifier gives authored sentences
|
|
41381
|
+
* no control (it names its own next step), and this sentence
|
|
41382
|
+
* does — check again in a few minutes, from the same control
|
|
41383
|
+
* the reader just used.
|
|
41384
|
+
*/
|
|
41385
|
+
message: installedVersion && this.pendingPypiVersion ? `A new server release (${this.pendingPypiVersion}) is still publishing, so no update was offered yet. Check again in a few minutes.` : "Unable to determine backend version.",
|
|
41032
41386
|
phase: "check",
|
|
41033
41387
|
logPath: serverUpdateLogPath()
|
|
41034
41388
|
});
|