cicy-desktop 2.1.217 → 2.1.218

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cicy-desktop",
3
- "version": "2.1.217",
3
+ "version": "2.1.218",
4
4
  "description": "CiCy - AI-powered operating system browser",
5
5
  "main": "src/main.js",
6
6
  "bin": {
package/src/main.js CHANGED
@@ -22,6 +22,18 @@ const appUpdater = require("./app-updater");
22
22
  const { attachContextMenu } = require("./utils/context-menu-options");
23
23
  electronApp.on("web-contents-created", (_e, wc) => {
24
24
  attachContextMenu(wc);
25
+ // Security backstop for window.open. createWindow (setupWindowHandlers) and the
26
+ // tab-browser install their OWN windowOpenHandler LATER, which replaces this one
27
+ // for those windows. This default therefore only stays in force for webContents
28
+ // nobody else configured (e.g. a raw agent-electron open). It guarantees
29
+ // window.open can never drop an external site into the opener's session — profile
30
+ // 0 / the default session shares cookies and may carry the electronRPC bridge, so
31
+ // an external popup there is an RCE/creds-leak surface. External links go to the
32
+ // system browser instead; trusted/local/about:blank popups are still allowed.
33
+ try {
34
+ const { windowOpenDecision } = require("./utils/window-utils");
35
+ wc.setWindowOpenHandler(({ url }) => windowOpenDecision(url, { wc }));
36
+ } catch (_) {}
25
37
  // Ctrl/Cmd+C inside a <webview> guest: on Windows the application menu's
26
38
  // role:"copy" accelerator doesn't reach the focused guest, so a selection
27
39
  // couldn't be copied with the keyboard (right-click 复制 is fixed separately via
@@ -65,7 +77,7 @@ if (process.platform === "linux") {
65
77
  const http = require("http");
66
78
  const log = require("electron-log");
67
79
  const { config } = require("./config");
68
- const { createWindow } = require("./utils/window-utils");
80
+ const { createWindow, accountIdxOfWebContents } = require("./utils/window-utils");
69
81
  const { AuthManager } = require("./utils/auth");
70
82
  const { setupElectronFlags, setupErrorHandlers } = require("./server/electron-setup");
71
83
  const { parseArgs } = require("./server/args-parser");
@@ -1329,10 +1341,7 @@ electronApp.whenReady().then(async () => {
1329
1341
  BrowserWindow.getAllWindows()
1330
1342
  .map((w) => {
1331
1343
  try {
1332
- const part = w.webContents.session.partition || "";
1333
- const acc = part.startsWith("persist:sandbox-")
1334
- ? parseInt(part.replace("persist:sandbox-", ""), 10)
1335
- : 0;
1344
+ const acc = accountIdxOfWebContents(w.webContents);
1336
1345
  return `${acc}::${registry.normalizeUrl(w.webContents.getURL())}`;
1337
1346
  } catch {
1338
1347
  return null;
@@ -214,6 +214,12 @@ class TabManager {
214
214
  const view = new BrowserView({ webPreferences: wp });
215
215
  const wc = view.webContents;
216
216
  const id = wc.id;
217
+ // Tag the tab's webContents with its profile so anything holding just the wc
218
+ // (context menu, window helpers) can resolve the REAL profile deterministically.
219
+ // BrowserView guests don't expose a readable partition (session.partition and
220
+ // getWebPreferences().partition both come back empty), so this tag — set from
221
+ // the owning TabManager's accountIdx — is the only reliable source.
222
+ try { wc.cicyAccountIdx = this.accountIdx; } catch (e) {}
217
223
  // BrowserView tabs aren't auto-covered by the global contextMenu() (it only
218
224
  // attaches to BrowserWindows + webviews), so give each tab exactly one
219
225
  // right-click menu (copy/paste/inspect) — without this, right-click did nothing.
@@ -59,8 +59,20 @@ const OPTIONS = {
59
59
  const url = (() => { try { return wc && wc.getURL ? wc.getURL() : ""; } catch (e) { return ""; } })();
60
60
  const title = (() => { try { return wc && wc.getTitle ? wc.getTitle() : ""; } catch (e) { return ""; } })();
61
61
  // profile id = 它所在 session 的 accountIdx:partition `persist:sandbox-<N>` → N,否则 0(系统槽)。
62
- const part = (() => { try { return wc && wc.session ? (wc.session.partition || "") : ""; } catch (e) { return ""; } })();
63
- const profile = part.startsWith("persist:sandbox-") ? (parseInt(part.replace("persist:sandbox-", ""), 10) || 0) : 0;
62
+ // profile id = the webContents' account. Tab-browser BrowserView guests do NOT
63
+ // expose a readable partition (session.partition AND getWebPreferences().partition
64
+ // both come back empty → everything mis-reported as profile 0), so read the
65
+ // `cicyAccountIdx` tag stamped on the wc at tab creation. Fall back to partition
66
+ // parsing only for non-tab surfaces (BrowserWindow / <webview>) without the tag.
67
+ const profile = (() => {
68
+ try {
69
+ if (wc && typeof wc.cicyAccountIdx === "number") return wc.cicyAccountIdx;
70
+ const wp = wc && wc.getWebPreferences ? wc.getWebPreferences() : null;
71
+ const part = (wp && wp.partition) || (wc && wc.session && wc.session.partition) || "";
72
+ const m = /^persist:sandbox-(\d+)$/.exec(part);
73
+ return m ? parseInt(m[1], 10) : 0;
74
+ } catch (e) { return 0; }
75
+ })();
64
76
  // 给 agent 用的一句话指令:带 webContents id + profile id + url + title,让它用 agent-electron 操作。
65
77
  const skillPrompt = t("ctxMenu.skillPrompt", { id: wcId, profile, url, title });
66
78
  // 导航:优先用 Electron 新 navigationHistory API,旧版兜底 wc.canGoBack/goBack。
@@ -31,7 +31,11 @@ function accountIdxOf(win) {
31
31
  // session (homepage / system windows) maps to 0.
32
32
  if (typeof win.cicyAccountIdx === "number") return win.cicyAccountIdx;
33
33
  try {
34
- const p = win.webContents.session.partition || "";
34
+ // Session has no readable `.partition`; read it off WebPreferences (was
35
+ // session.partition → always undefined → every window mis-tagged profile 0).
36
+ const wc = win.webContents;
37
+ const wp = wc && wc.getWebPreferences ? wc.getWebPreferences() : null;
38
+ const p = (wp && wp.partition) || (wc && wc.session && wc.session.partition) || "";
35
39
  const m = /^persist:sandbox-(\d+)$/.exec(p);
36
40
  if (m) return parseInt(m[1], 10);
37
41
  } catch (_) {}
@@ -1,4 +1,4 @@
1
- const { app, BrowserWindow, Menu, dialog, shell } = require("electron");
1
+ const { app, BrowserWindow, Menu, shell } = require("electron");
2
2
  const { default: contextMenu } = require("electron-context-menu");
3
3
  const contextMenuOptions = require("./context-menu-options");
4
4
  const path = require("path");
@@ -15,39 +15,78 @@ if (app) {
15
15
  app.name = "CiCy Desktop";
16
16
  }
17
17
 
18
- function setupWindowHandlers(win) {
19
- // Hook window.open. App/internal popups (about:blank or trusted/local hosts
20
- // e.g. a popped-out ttyd terminal from CiCy Code) must open as REAL Electron
21
- // windows WITH webviewTag, or any <webview> inside them (terminal / artifact
22
- // guest) collapses to 0x0. Returning action:"deny" (the old behavior) routed
23
- // every popup through a dialog; an *allowed* window.open window inherits
24
- // DEFAULT webPreferences (webviewTag=false) unless we override it here.
25
- // External links (other websites) keep the open-in-browser/app dialog.
26
- win.webContents.setWindowOpenHandler(({ url }) => {
27
- log.info(`[WindowOpen] Intercepted: ${url}`);
28
- if (!url || url === "about:blank" || isTrustedUrl(url)) {
29
- return {
30
- action: "allow",
31
- overrideBrowserWindowOptions: {
32
- autoHideMenuBar: true,
33
- icon: require("./app-icon").appIconPath(),
34
- webPreferences: {
35
- webviewTag: true, // embedded <webview> (ttyd/artifact) must render
36
- // hole #4: trusted pages no longer get raw Node. Their electronRPC
37
- // bridge comes from webview-preload via contextBridge, so a trusted-origin
38
- // XSS can't `require('child_process')` to bypass the rpc:guarded gate.
39
- nodeIntegration: false,
40
- contextIsolation: true,
41
- preload: path.join(__dirname, "../backends/webview-preload.js"),
42
- webSecurity: false,
43
- enableClipboard: true,
44
- },
18
+ // Resolve the profile (accountIdx) a webContents lives in, from its session
19
+ // partition: persist:sandbox-N → N; the default/"" session 0 (profile 0, the
20
+ // privileged/shared session). Mirrors getWindowInfo's derivation.
21
+ function partitionOfWebContents(wc) {
22
+ // Electron's Session has NO readable `.partition`; the partition string lives on
23
+ // the webContents' WebPreferences. Reading session.partition (always undefined)
24
+ // made every profile resolve to 0. Prefer getWebPreferences(), fall back to
25
+ // session.partition just in case.
26
+ try {
27
+ const wp = wc && wc.getWebPreferences ? wc.getWebPreferences() : null;
28
+ if (wp && wp.partition) return wp.partition;
29
+ return (wc && wc.session && wc.session.partition) || "";
30
+ } catch (_) { return ""; }
31
+ }
32
+
33
+ function accountIdxOfWebContents(wc) {
34
+ // Prefer the cicyAccountIdx tag stamped at creation (the only reliable source for
35
+ // BrowserView tab guests, whose partition is never readable). Fall back to
36
+ // partition parsing for surfaces that carry a real partition.
37
+ if (wc && typeof wc.cicyAccountIdx === "number") return wc.cicyAccountIdx;
38
+ const partition = partitionOfWebContents(wc);
39
+ if (partition.startsWith("persist:sandbox-")) {
40
+ const n = parseInt(partition.slice("persist:sandbox-".length), 10);
41
+ return Number.isFinite(n) ? n : 0;
42
+ }
43
+ return 0;
44
+ }
45
+
46
+ // Single source of truth for the window.open policy. App/internal popups
47
+ // (about:blank or trusted/local hosts — e.g. a popped-out ttyd terminal from CiCy
48
+ // Code) open in-place as REAL Electron windows WITH webviewTag (any <webview>
49
+ // inside them — terminal / artifact guest — otherwise collapses to 0x0).
50
+ //
51
+ // Any OTHER (external) URL is routed DETERMINISTICALLY by the OPENER's profile,
52
+ // and never lands in profile 0: an opener in profile 0 (the privileged/shared
53
+ // session — team cookies + trusted-origin RPC surface) has the link FORCED into
54
+ // sandbox profile 1; an opener already in a sandbox profile N opens the link in
55
+ // its OWN profile N. This mirrors the tab-browser policy (profile 0 link →
56
+ // openTab(1); else same profile) — no dialog, no leak into profile 0.
57
+ function windowOpenDecision(url, { wc } = {}) {
58
+ log.info(`[WindowOpen] Intercepted: ${url}`);
59
+ if (!url || url === "about:blank" || isTrustedUrl(url)) {
60
+ return {
61
+ action: "allow",
62
+ overrideBrowserWindowOptions: {
63
+ autoHideMenuBar: true,
64
+ icon: require("./app-icon").appIconPath(),
65
+ webPreferences: {
66
+ webviewTag: true, // embedded <webview> (ttyd/artifact) must render
67
+ // hole #4: trusted pages no longer get raw Node. Their electronRPC
68
+ // bridge comes from webview-preload via contextBridge, so a trusted-origin
69
+ // XSS can't `require('child_process')` to bypass the rpc:guarded gate.
70
+ nodeIntegration: false,
71
+ contextIsolation: true,
72
+ preload: path.join(__dirname, "../backends/webview-preload.js"),
73
+ webSecurity: false,
74
+ enableClipboard: true,
45
75
  },
46
- };
47
- }
48
- showOpenLinkDialog(win, url);
49
- return { action: "deny" };
50
- });
76
+ },
77
+ };
78
+ }
79
+ // External URL: profile 0 → forced sandbox profile 1; any sandbox profile N →
80
+ // its own profile N. Never in-session for profile 0.
81
+ const openerIdx = accountIdxOfWebContents(wc);
82
+ const target = openerIdx === 0 ? 1 : openerIdx;
83
+ try { createWindow({ url }, target, true); }
84
+ catch (e) { log.warn(`[WindowOpen] open external in profile ${target} failed: ${e && e.message}`); }
85
+ return { action: "deny" };
86
+ }
87
+
88
+ function setupWindowHandlers(win) {
89
+ win.webContents.setWindowOpenHandler(({ url }) => windowOpenDecision(url, { wc: win.webContents }));
51
90
  if (!win.webContents.debugger.isAttached()) {
52
91
  win.webContents.debugger.attach("1.3");
53
92
  }
@@ -301,6 +340,12 @@ function createWindow(options = {}, accountIdx = 1, forceNew = false) {
301
340
  },
302
341
  });
303
342
 
343
+ // Stamp the profile on the window + its webContents so any holder of just the wc
344
+ // (context menu, accountIdxOfWebContents, thumbnails) resolves the REAL profile
345
+ // deterministically — session.partition/getWebPreferences().partition are not
346
+ // reliably readable back.
347
+ try { win.cicyAccountIdx = accountIdx; win.webContents.cicyAccountIdx = accountIdx; } catch (e) {}
348
+
304
349
  // 监听窗口状态变化并自动保存(基于URL)
305
350
  watchWindowState(win, accountIdx);
306
351
 
@@ -427,7 +472,7 @@ function getWindowInfo(win) {
427
472
  try {
428
473
  const wc = win.webContents;
429
474
  if (!wc || !wc.session) return null;
430
- const partition = wc.session.partition || "";
475
+ const partition = partitionOfWebContents(wc);
431
476
  // persist:sandbox-N → account N (N>=1 are user profiles). Anything on the
432
477
  // default session (homepage / platform system windows, partition "") maps
433
478
  // to account 0 — the reserved system slot.
@@ -517,34 +562,13 @@ if (app) {
517
562
  });
518
563
  }
519
564
 
520
- function showOpenLinkDialog(parentWin, url) {
521
- const i18n = require("../i18n");
522
- dialog
523
- .showMessageBox(parentWin, {
524
- type: "question",
525
- buttons: [
526
- i18n.t("dialog.openLink.openInBrowser"),
527
- i18n.t("dialog.openLink.openInApp"),
528
- i18n.t("dialog.openLink.cancel"),
529
- ],
530
- defaultId: 0,
531
- title: i18n.t("dialog.openLink.title"),
532
- message: i18n.t("dialog.openLink.message"),
533
- detail: url,
534
- })
535
- .then(({ response }) => {
536
- if (response === 0) {
537
- shell.openExternal(url);
538
- } else if (response === 1) {
539
- createWindow({ url }, 0, true);
540
- }
541
- });
542
- }
543
-
544
565
  module.exports = {
545
566
  createWindow,
546
567
  setupWindowHandlers,
568
+ windowOpenDecision,
547
569
  getWindowInfo,
570
+ partitionOfWebContents,
571
+ accountIdxOfWebContents,
548
572
  refreshTrustedOrigins,
549
573
  isTrustedUrl,
550
574
  };