@quanta-intellect/vessel-browser 0.1.92 → 0.1.94

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 CHANGED
@@ -104,10 +104,23 @@ function canUseSafeStorage$1() {
104
104
  return false;
105
105
  }
106
106
  }
107
+ function writePrivateFile(filePath2, data) {
108
+ fs.writeFileSync(filePath2, data, { mode: 384 });
109
+ try {
110
+ fs.chmodSync(filePath2, 384);
111
+ } catch {
112
+ }
113
+ }
114
+ function assertSafeStorageAvailable() {
115
+ if (!canUseSafeStorage$1()) {
116
+ throw new Error("OS-backed secret storage is unavailable; refusing to store secrets on disk.");
117
+ }
118
+ }
107
119
  function readStoredProviderSecret() {
108
120
  try {
121
+ if (!canUseSafeStorage$1()) return null;
109
122
  const raw = fs.readFileSync(getChatProviderSecretPath());
110
- const decoded = canUseSafeStorage$1() && electron.safeStorage.decryptString ? electron.safeStorage.decryptString(raw) : raw.toString("utf-8");
123
+ const decoded = electron.safeStorage.decryptString(raw);
111
124
  const parsed = JSON.parse(decoded);
112
125
  if (parsed && typeof parsed === "object" && typeof parsed.providerId === "string" && typeof parsed.apiKey === "string") {
113
126
  return parsed;
@@ -117,15 +130,12 @@ function readStoredProviderSecret() {
117
130
  return null;
118
131
  }
119
132
  function writeStoredProviderSecret(secret) {
133
+ assertSafeStorageAvailable();
120
134
  const filePath2 = getChatProviderSecretPath();
121
135
  fs.mkdirSync(path.dirname(filePath2), { recursive: true });
122
136
  const payload = JSON.stringify(secret);
123
- if (canUseSafeStorage$1()) {
124
- const encrypted = electron.safeStorage.encryptString(payload);
125
- fs.writeFileSync(filePath2, encrypted, { mode: 384 });
126
- return;
127
- }
128
- fs.writeFileSync(filePath2, payload, { mode: 384 });
137
+ const encrypted = electron.safeStorage.encryptString(payload);
138
+ writePrivateFile(filePath2, encrypted);
129
139
  }
130
140
  function clearStoredProviderSecret() {
131
141
  try {
@@ -138,8 +148,9 @@ function getCodexTokensPath() {
138
148
  }
139
149
  function readStoredCodexTokens() {
140
150
  try {
151
+ if (!canUseSafeStorage$1()) return null;
141
152
  const raw = fs.readFileSync(getCodexTokensPath());
142
- const decoded = canUseSafeStorage$1() && electron.safeStorage.decryptString ? electron.safeStorage.decryptString(raw) : raw.toString("utf-8");
153
+ const decoded = electron.safeStorage.decryptString(raw);
143
154
  const parsed = JSON.parse(decoded);
144
155
  if (parsed && typeof parsed === "object" && typeof parsed.accessToken === "string" && typeof parsed.refreshToken === "string") {
145
156
  return parsed;
@@ -149,15 +160,12 @@ function readStoredCodexTokens() {
149
160
  return null;
150
161
  }
151
162
  function writeStoredCodexTokens(tokens) {
163
+ assertSafeStorageAvailable();
152
164
  const filePath2 = getCodexTokensPath();
153
165
  fs.mkdirSync(path.dirname(filePath2), { recursive: true });
154
166
  const payload = JSON.stringify(tokens);
155
- if (canUseSafeStorage$1()) {
156
- const encrypted = electron.safeStorage.encryptString(payload);
157
- fs.writeFileSync(filePath2, encrypted, { mode: 384 });
158
- return;
159
- }
160
- fs.writeFileSync(filePath2, payload, { mode: 384 });
167
+ const encrypted = electron.safeStorage.encryptString(payload);
168
+ writePrivateFile(filePath2, encrypted);
161
169
  }
162
170
  function clearStoredCodexTokens() {
163
171
  try {
@@ -375,6 +383,28 @@ function assertPermittedNavigationURL(url) {
375
383
  throw new Error(policyError);
376
384
  }
377
385
  }
386
+ function loadPermittedNavigationURL(wc, url) {
387
+ assertPermittedNavigationURL(url);
388
+ return wc.loadURL(url);
389
+ }
390
+ function loadInternalDataURL(wc, dataUrl) {
391
+ if (!dataUrl.startsWith("data:text/html;charset=utf-8,")) {
392
+ throw new Error("Blocked unexpected internal data URL");
393
+ }
394
+ return wc.loadURL(dataUrl);
395
+ }
396
+ function loadTrustedAppURL(wc, url) {
397
+ const parsed = new URL(url);
398
+ if (!["file:", "http:", "https:"].includes(parsed.protocol)) {
399
+ throw new Error(`Blocked unexpected app URL scheme: ${parsed.protocol}`);
400
+ }
401
+ const isHttp = parsed.protocol === "http:" || parsed.protocol === "https:";
402
+ const isLocalhost = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
403
+ if (isHttp && !isLocalhost) {
404
+ throw new Error(`Blocked unexpected app URL host: ${parsed.hostname}`);
405
+ }
406
+ return wc.loadURL(parsed.toString());
407
+ }
378
408
  const MAX_CUSTOM_HISTORY = 50;
379
409
  const READER_MODE_DATA_URL_PREFIX = "data:text/html;charset=utf-8,";
380
410
  const logger$m = createLogger("Tab");
@@ -1164,9 +1194,9 @@ function save$3() {
1164
1194
  }
1165
1195
  function emit$4() {
1166
1196
  if (!state$5) return;
1167
- const snapshot = { highlights: [...state$5.highlights] };
1197
+ const snapshot2 = { highlights: [...state$5.highlights] };
1168
1198
  for (const listener of listeners$2) {
1169
- listener(snapshot);
1199
+ listener(snapshot2);
1170
1200
  }
1171
1201
  }
1172
1202
  function normalizeUrl$1(rawUrl) {
@@ -1897,9 +1927,9 @@ function save$2() {
1897
1927
  }
1898
1928
  function emit$3() {
1899
1929
  if (!state$4) return;
1900
- const snapshot = { entries: [...state$4.entries] };
1930
+ const snapshot2 = { entries: [...state$4.entries] };
1901
1931
  for (const listener of listeners$1) {
1902
- listener(snapshot);
1932
+ listener(snapshot2);
1903
1933
  }
1904
1934
  }
1905
1935
  function getState$1() {
@@ -2093,6 +2123,8 @@ class DevToolsSession {
2093
2123
  this.tabId = tabId;
2094
2124
  this.wc = wc;
2095
2125
  }
2126
+ tabId;
2127
+ wc;
2096
2128
  attached = false;
2097
2129
  attachingPromise = null;
2098
2130
  consoleDomainEnabled = false;
@@ -3108,11 +3140,11 @@ class TabManager {
3108
3140
  note
3109
3141
  };
3110
3142
  }
3111
- restoreSession(snapshot) {
3112
- const tabs = snapshot.tabs.length > 0 ? snapshot.tabs : [{ id: "", url: "about:blank", title: "New Tab" }];
3143
+ restoreSession(snapshot2) {
3144
+ const tabs = snapshot2.tabs.length > 0 ? snapshot2.tabs : [{ id: "", url: "about:blank", title: "New Tab" }];
3113
3145
  const activeIndex = Math.max(
3114
3146
  0,
3115
- Math.min(snapshot.activeIndex, tabs.length - 1)
3147
+ Math.min(snapshot2.activeIndex, tabs.length - 1)
3116
3148
  );
3117
3149
  this.destroyAllTabs();
3118
3150
  const restoredGroups = /* @__PURE__ */ new Map();
@@ -3375,6 +3407,7 @@ const Channels = {
3375
3407
  SETTINGS_UPDATE: "settings:update",
3376
3408
  SETTINGS_HEALTH_GET: "settings:health:get",
3377
3409
  SETTINGS_HEALTH_UPDATE: "settings:health:update",
3410
+ MCP_REGENERATE_TOKEN: "mcp:regenerate-token",
3378
3411
  // Bookmarks
3379
3412
  BOOKMARKS_GET: "bookmarks:get",
3380
3413
  BOOKMARKS_UPDATE: "bookmarks:update",
@@ -3875,8 +3908,8 @@ function load$2() {
3875
3908
  const next = /* @__PURE__ */ new Map();
3876
3909
  if (!Array.isArray(raw)) return next;
3877
3910
  for (const entry of raw) {
3878
- const snapshot = normalizeStoredSnapshot(entry);
3879
- if (snapshot) next.set(snapshot.url, snapshot);
3911
+ const snapshot2 = normalizeStoredSnapshot(entry);
3912
+ if (snapshot2) next.set(snapshot2.url, snapshot2);
3880
3913
  }
3881
3914
  return next;
3882
3915
  }
@@ -3903,7 +3936,7 @@ function getSnapshot(normalizedUrl) {
3903
3936
  function saveSnapshot(rawUrl, title, textContent, headings) {
3904
3937
  const s = load$2();
3905
3938
  const key2 = normalizeUrl(rawUrl);
3906
- const snapshot = {
3939
+ const snapshot2 = {
3907
3940
  url: key2,
3908
3941
  title,
3909
3942
  textContent: textContent.slice(0, MAX_TEXT_LENGTH),
@@ -3911,9 +3944,9 @@ function saveSnapshot(rawUrl, title, textContent, headings) {
3911
3944
  capturedAt: (/* @__PURE__ */ new Date()).toISOString()
3912
3945
  };
3913
3946
  s.delete(key2);
3914
- s.set(key2, snapshot);
3947
+ s.set(key2, snapshot2);
3915
3948
  persistence$5.schedule();
3916
- return snapshot;
3949
+ return snapshot2;
3917
3950
  }
3918
3951
  function flushPersist$2() {
3919
3952
  return persistence$5.flush();
@@ -4726,6 +4759,7 @@ const POSTHOG_API_KEY = process.env.POSTHOG_API_KEY || "phc_OMeM3P5cxJwl14lOKxYa
4726
4759
  const POSTHOG_HOST = process.env.POSTHOG_HOST || "https://us.i.posthog.com";
4727
4760
  const BATCH_INTERVAL_MS = 6e4;
4728
4761
  const MAX_BATCH_SIZE = 50;
4762
+ const SENSITIVE_PROPERTY_RE = /url|uri|query|prompt|content|text|token|secret|key|password|credential|email|domain/i;
4729
4763
  function getDeviceIdPath() {
4730
4764
  return path.join(electron.app.getPath("userData"), ".vessel-device-id");
4731
4765
  }
@@ -4754,12 +4788,22 @@ function isEnabled() {
4754
4788
  if (process.env.VESSEL_DEV === "1") return false;
4755
4789
  return loadSettings().telemetryEnabled !== false;
4756
4790
  }
4791
+ function sanitizeTelemetryProperties(properties) {
4792
+ const safe = {};
4793
+ for (const [key2, value] of Object.entries(properties)) {
4794
+ if (SENSITIVE_PROPERTY_RE.test(key2)) continue;
4795
+ if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value === null) {
4796
+ safe[key2] = typeof value === "string" ? value.slice(0, 120) : value;
4797
+ }
4798
+ }
4799
+ return safe;
4800
+ }
4757
4801
  function trackEvent(event, properties = {}) {
4758
4802
  if (!isEnabled()) return;
4759
4803
  eventQueue.push({
4760
4804
  event,
4761
4805
  properties: {
4762
- ...properties,
4806
+ ...sanitizeTelemetryProperties(properties),
4763
4807
  premium_status: isPremium() ? "premium" : "free",
4764
4808
  app_version: electron.app.getVersion(),
4765
4809
  platform: process.platform,
@@ -4794,8 +4838,8 @@ function trackBookmarkAction(action) {
4794
4838
  function trackVaultAction(action) {
4795
4839
  trackEvent("vault_action", { action });
4796
4840
  }
4797
- function trackExtractionFailed(domain, reason) {
4798
- trackEvent("extraction_failed", { domain, reason });
4841
+ function trackExtractionFailed(_domain, reason) {
4842
+ trackEvent("extraction_failed", { reason });
4799
4843
  }
4800
4844
  function trackPremiumFunnel(step, context) {
4801
4845
  trackEvent("premium_funnel", { step, ...context });
@@ -6774,9 +6818,9 @@ function getMcpStatus() {
6774
6818
  return state$3.mcp.status;
6775
6819
  }
6776
6820
  function emitRuntimeHealthChange() {
6777
- const snapshot = getRuntimeHealth();
6821
+ const snapshot2 = getRuntimeHealth();
6778
6822
  for (const listener of runtimeHealthChangeListeners) {
6779
- listener(snapshot);
6823
+ listener(snapshot2);
6780
6824
  }
6781
6825
  }
6782
6826
  const state$3 = {
@@ -8473,6 +8517,17 @@ class OpenAICompatProvider {
8473
8517
  this.abortController?.abort();
8474
8518
  }
8475
8519
  }
8520
+ async function openExternalAllowlisted(url, rule) {
8521
+ const parsed = new URL(url);
8522
+ const schemes = rule.schemes ?? ["https:"];
8523
+ if (!schemes.includes(parsed.protocol)) {
8524
+ throw new Error(`Blocked external URL scheme: ${parsed.protocol}`);
8525
+ }
8526
+ if (rule.hosts && !rule.hosts.includes(parsed.hostname)) {
8527
+ throw new Error(`Blocked external URL host: ${parsed.hostname}`);
8528
+ }
8529
+ await electron.shell.openExternal(parsed.toString());
8530
+ }
8476
8531
  const logger$g = createLogger("CodexOAuth");
8477
8532
  const ISSUER = "https://auth.openai.com";
8478
8533
  const CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
@@ -8809,7 +8864,7 @@ async function startCodexOAuth(onStatus) {
8809
8864
  activeFlow.port = port;
8810
8865
  const authUrl = buildAuthorizeUrl(port, pkce, state2);
8811
8866
  safeOnStatus("waiting");
8812
- electron.shell.openExternal(authUrl).catch((err) => {
8867
+ openExternalAllowlisted(authUrl, { hosts: ["auth.openai.com"] }).catch((err) => {
8813
8868
  logger$g.warn("Failed to open browser, user will need the URL:", err);
8814
8869
  });
8815
8870
  }).catch(wrappedReject);
@@ -12476,9 +12531,9 @@ function assignDefinedBookmarkFields(bookmark, fields) {
12476
12531
  }
12477
12532
  function emit$2() {
12478
12533
  if (!state$2) return;
12479
- const snapshot = cloneState(state$2);
12534
+ const snapshot2 = cloneState(state$2);
12480
12535
  for (const listener of listeners) {
12481
- listener(snapshot);
12536
+ listener(snapshot2);
12482
12537
  }
12483
12538
  }
12484
12539
  function escapeBookmarkHtml(value) {
@@ -12999,7 +13054,7 @@ async function captureLiveHighlightSnapshot(wc, savedHighlights = []) {
12999
13054
  savedHighlights.map((highlight) => normalizeText(highlight.text)).filter(Boolean)
13000
13055
  );
13001
13056
  try {
13002
- const snapshot = await wc.executeJavaScript(`(() => {
13057
+ const snapshot2 = await wc.executeJavaScript(`(() => {
13003
13058
  const selection = window.getSelection?.()?.toString().trim() || "";
13004
13059
  const pageHighlights = Array.from(
13005
13060
  document.querySelectorAll("mark.__vessel-highlight-text[data-vessel-highlight]")
@@ -13021,7 +13076,7 @@ async function captureLiveHighlightSnapshot(wc, savedHighlights = []) {
13021
13076
  };
13022
13077
  })()`, true);
13023
13078
  const seen = /* @__PURE__ */ new Set();
13024
- const pageHighlights = (snapshot.pageHighlights ?? []).map((highlight) => ({
13079
+ const pageHighlights = (snapshot2.pageHighlights ?? []).map((highlight) => ({
13025
13080
  text: normalizeText(highlight.text),
13026
13081
  color: highlight.color?.trim() || void 0
13027
13082
  })).filter((highlight) => highlight.text.length > 0).filter((highlight) => {
@@ -13033,24 +13088,24 @@ async function captureLiveHighlightSnapshot(wc, savedHighlights = []) {
13033
13088
  ...highlight,
13034
13089
  persisted: savedTexts.has(highlight.text)
13035
13090
  }));
13036
- const activeSelection = normalizeText(snapshot.activeSelection) || void 0;
13091
+ const activeSelection = normalizeText(snapshot2.activeSelection) || void 0;
13037
13092
  return { activeSelection, pageHighlights };
13038
13093
  } catch {
13039
13094
  return { pageHighlights: [] };
13040
13095
  }
13041
13096
  }
13042
- function formatLiveSelectionSection(snapshot) {
13097
+ function formatLiveSelectionSection(snapshot2) {
13043
13098
  const sections = [];
13044
- if (snapshot.activeSelection) {
13045
- const preview = snapshot.activeSelection.length > 400 ? `${snapshot.activeSelection.slice(0, 397)}...` : snapshot.activeSelection;
13099
+ if (snapshot2.activeSelection) {
13100
+ const preview = snapshot2.activeSelection.length > 400 ? `${snapshot2.activeSelection.slice(0, 397)}...` : snapshot2.activeSelection;
13046
13101
  sections.push(
13047
13102
  `## Active User Selection
13048
13103
  The user currently has this text selected on screen:
13049
13104
  - "${preview}"`
13050
13105
  );
13051
13106
  }
13052
- if (snapshot.pageHighlights.length > 0) {
13053
- const lines = snapshot.pageHighlights.map((highlight) => {
13107
+ if (snapshot2.pageHighlights.length > 0) {
13108
+ const lines = snapshot2.pageHighlights.map((highlight) => {
13054
13109
  const preview = highlight.text.length > 180 ? `${highlight.text.slice(0, 177)}...` : highlight.text;
13055
13110
  const details = [
13056
13111
  highlight.persisted ? "saved" : "visible only",
@@ -13551,7 +13606,7 @@ async function saveNamedSession(tabManager, name) {
13551
13606
  const entries = await captureLocalStorageForOrigin(tabManager, origin);
13552
13607
  localStorage.push({ origin, entries });
13553
13608
  }
13554
- const snapshot = tabManager.snapshotSession(`Named session: ${normalizedName}`);
13609
+ const snapshot2 = tabManager.snapshotSession(`Named session: ${normalizedName}`);
13555
13610
  const domains = [...new Set(cookies.map((cookie) => cookie.domain))].sort();
13556
13611
  const now = (/* @__PURE__ */ new Date()).toISOString();
13557
13612
  const data = {
@@ -13563,7 +13618,7 @@ async function saveNamedSession(tabManager, name) {
13563
13618
  domains,
13564
13619
  cookies,
13565
13620
  localStorage,
13566
- snapshot
13621
+ snapshot: snapshot2
13567
13622
  };
13568
13623
  writeSessionFile(getSessionPath(normalizedName), data);
13569
13624
  return {
@@ -13918,8 +13973,8 @@ function compactSearchLikeResult(text) {
13918
13973
  return limitText(cleaned, 16, 1400);
13919
13974
  }
13920
13975
  const summary = cleaned.slice(0, markerIndex).trim();
13921
- const snapshot = cleaned.slice(markerIndex + marker.length).trim();
13922
- return [summary, compactReadPageResult(snapshot)].filter(Boolean).join("\n\n");
13976
+ const snapshot2 = cleaned.slice(markerIndex + marker.length).trim();
13977
+ return [summary, compactReadPageResult(snapshot2)].filter(Boolean).join("\n\n");
13923
13978
  }
13924
13979
  function compactCurrentTabResult(text) {
13925
13980
  try {
@@ -14968,7 +15023,7 @@ async function inspectElement(wc, selector, limit = 8) {
14968
15023
  return lines.join("\n");
14969
15024
  }
14970
15025
  async function getLocaleSnapshot(wc) {
14971
- const snapshot = await executePageScript(
15026
+ const snapshot2 = await executePageScript(
14972
15027
  wc,
14973
15028
  `
14974
15029
  (function() {
@@ -14987,13 +15042,13 @@ async function getLocaleSnapshot(wc) {
14987
15042
  label: "locale snapshot"
14988
15043
  }
14989
15044
  );
14990
- if (!snapshot || snapshot === PAGE_SCRIPT_TIMEOUT || typeof snapshot !== "object") {
15045
+ if (!snapshot2 || snapshot2 === PAGE_SCRIPT_TIMEOUT || typeof snapshot2 !== "object") {
14991
15046
  return null;
14992
15047
  }
14993
15048
  return {
14994
- lang: typeof snapshot.lang === "string" ? snapshot.lang.trim() : "",
14995
- url: typeof snapshot.url === "string" ? snapshot.url : wc.getURL(),
14996
- title: typeof snapshot.title === "string" ? snapshot.title : wc.getTitle()
15049
+ lang: typeof snapshot2.lang === "string" ? snapshot2.lang.trim() : "",
15050
+ url: typeof snapshot2.url === "string" ? snapshot2.url : wc.getURL(),
15051
+ title: typeof snapshot2.title === "string" ? snapshot2.title : wc.getTitle()
14997
15052
  };
14998
15053
  }
14999
15054
  function primaryLanguageTag(value) {
@@ -15009,31 +15064,31 @@ function localeChanged(before, after) {
15009
15064
  const localeHint = /[?&](lang|locale|language|hl)=|\/(ja|jp|en|fr|de|es|it|ko|zh)(\/|$)/i;
15010
15065
  return before.url !== after.url && localeHint.test(after.url);
15011
15066
  }
15012
- async function restoreLocaleSnapshot(wc, snapshot) {
15013
- if (!snapshot || wc.isDestroyed()) return;
15067
+ async function restoreLocaleSnapshot(wc, snapshot2) {
15068
+ if (!snapshot2 || wc.isDestroyed()) return;
15014
15069
  try {
15015
15070
  if (typeof wc.canGoBack === "function" && wc.canGoBack()) {
15016
15071
  wc.goBack();
15017
15072
  await waitForLoad(wc, 3e3);
15018
15073
  const reverted = await getLocaleSnapshot(wc);
15019
- if (!localeChanged(snapshot, reverted)) {
15074
+ if (!localeChanged(snapshot2, reverted)) {
15020
15075
  return;
15021
15076
  }
15022
15077
  }
15023
15078
  } catch (err) {
15024
15079
  logger$c.warn("Failed to restore locale via history navigation, trying URL reload fallback:", err);
15025
15080
  }
15026
- if (snapshot.url && snapshot.url !== wc.getURL()) {
15081
+ if (snapshot2.url && snapshot2.url !== wc.getURL()) {
15027
15082
  try {
15028
- assertSafeURL(snapshot.url);
15029
- await wc.loadURL(snapshot.url);
15083
+ assertSafeURL(snapshot2.url);
15084
+ await wc.loadURL(snapshot2.url);
15030
15085
  await waitForLoad(wc, 3e3);
15031
15086
  return;
15032
15087
  } catch (err) {
15033
15088
  logger$c.warn("Failed to restore locale via safe URL load, trying page reload fallback:", err);
15034
15089
  }
15035
15090
  }
15036
- if (snapshot.url) {
15091
+ if (snapshot2.url) {
15037
15092
  try {
15038
15093
  await wc.reload();
15039
15094
  await waitForLoad(wc, 3e3);
@@ -18983,9 +19038,9 @@ function capturePageToVault({
18983
19038
  if (page.excerpt.trim()) {
18984
19039
  bodyLines.push("## Excerpt", "", page.excerpt.trim(), "");
18985
19040
  }
18986
- const snapshot = trimContent(page.content);
18987
- if (snapshot) {
18988
- bodyLines.push("## Page Snapshot", "", snapshot, "");
19041
+ const snapshot2 = trimContent(page.content);
19042
+ if (snapshot2) {
19043
+ bodyLines.push("## Page Snapshot", "", snapshot2, "");
18989
19044
  }
18990
19045
  return writeMemoryNote({
18991
19046
  title: noteTitle,
@@ -19410,8 +19465,8 @@ Exception: ${result.exceptionDetails}`);
19410
19465
  {},
19411
19466
  async () => {
19412
19467
  const session = getOrCreateSession(tabManager);
19413
- const snapshot = await session.getPerformanceSnapshot();
19414
- return JSON.stringify(snapshot, null, 2);
19468
+ const snapshot2 = await session.getPerformanceSnapshot();
19469
+ return JSON.stringify(snapshot2, null, 2);
19415
19470
  }
19416
19471
  );
19417
19472
  }
@@ -19487,6 +19542,7 @@ function getOrCreateEncryptionKey(keyFilename) {
19487
19542
  fs$1.mkdirSync(path$1.dirname(keyPath), { recursive: true });
19488
19543
  const encrypted = electron.safeStorage.encryptString(key2.toString("utf-8"));
19489
19544
  fs$1.writeFileSync(keyPath, encrypted, { mode: 384 });
19545
+ fs$1.chmodSync(keyPath, 384);
19490
19546
  return key2;
19491
19547
  }
19492
19548
  function createEncryptDecrypt(keyFilename) {
@@ -19560,15 +19616,21 @@ function createVaultIO(vaultFilename, encrypt2, decrypt2) {
19560
19616
  }
19561
19617
  return { loadVault: loadVault2, saveVault: saveVault2, resetCache };
19562
19618
  }
19563
- function domainMatches(pattern, hostname) {
19564
- const p = pattern.toLowerCase().trim();
19565
- const h = hostname.toLowerCase().trim();
19566
- if (p === h) return true;
19567
- if (p.startsWith("*.")) {
19568
- const suffix = p.slice(2);
19569
- return h === suffix || h.endsWith("." + suffix);
19619
+ function normalizeCredentialHost(value) {
19620
+ try {
19621
+ const parsed = new URL(value.includes("://") ? value : `https://${value}`);
19622
+ return parsed.hostname.toLowerCase().replace(/^www\./, "");
19623
+ } catch {
19624
+ const normalized = value.toLowerCase().trim().replace(/^(https?:\/\/)?(www\.)?/, "").replace(/\/.*$/, "");
19625
+ return normalized && !normalized.includes(" ") ? normalized : null;
19570
19626
  }
19571
- return false;
19627
+ }
19628
+ function domainMatches(pattern, hostname) {
19629
+ const isWildcard = pattern.trim().startsWith("*.");
19630
+ const p = normalizeCredentialHost(isWildcard ? pattern.slice(2) : pattern);
19631
+ const h = normalizeCredentialHost(hostname);
19632
+ if (!p || !h) return false;
19633
+ return isWildcard ? h.endsWith("." + p) : p === h;
19572
19634
  }
19573
19635
  function generateTotpCode(secret) {
19574
19636
  const epoch = Math.floor(Date.now() / 1e3);
@@ -19639,12 +19701,8 @@ function listEntries$1() {
19639
19701
  return loadVault$1().map(({ password, totpSecret, ...rest }) => rest);
19640
19702
  }
19641
19703
  function findEntriesForDomain(url) {
19642
- let hostname;
19643
- try {
19644
- hostname = new URL(url).hostname;
19645
- } catch {
19646
- return [];
19647
- }
19704
+ const hostname = normalizeCredentialHost(url);
19705
+ if (!hostname) return [];
19648
19706
  return loadVault$1().filter((e) => domainMatches(e.domainPattern, hostname)).map(({ password, totpSecret, ...rest }) => rest);
19649
19707
  }
19650
19708
  function addEntry(entry) {
@@ -19767,12 +19825,7 @@ const auditLog = createAuditLog(
19767
19825
  AUDIT_MAX_ENTRIES
19768
19826
  );
19769
19827
  function extractDomain(url) {
19770
- try {
19771
- const parsed = new URL(url.startsWith("http") ? url : `https://${url}`);
19772
- return parsed.hostname.toLowerCase();
19773
- } catch {
19774
- return url.toLowerCase().replace(/^(https?:\/\/)?(www\.)?/, "").replace(/\/.*$/, "");
19775
- }
19828
+ return normalizeCredentialHost(url) ?? "";
19776
19829
  }
19777
19830
  function listEntries() {
19778
19831
  return loadVault().map(({ password, totpSecret, ...rest }) => rest);
@@ -19957,6 +20010,7 @@ function writeMcpAuthFile(endpoint, token) {
19957
20010
  JSON.stringify({ endpoint, token, pid: process.pid }, null, 2) + "\n",
19958
20011
  { mode: 384 }
19959
20012
  );
20013
+ fs$1.chmodSync(filePath2, 384);
19960
20014
  } catch (err) {
19961
20015
  logger$9.warn("Failed to write auth file:", err);
19962
20016
  }
@@ -19982,10 +20036,18 @@ function clearMcpAuthFile() {
19982
20036
  ) + "\n",
19983
20037
  { mode: 384 }
19984
20038
  );
20039
+ fs$1.chmodSync(filePath2, 384);
19985
20040
  } catch (err) {
19986
20041
  logger$9.warn("Failed to clear auth file:", err);
19987
20042
  }
19988
20043
  }
20044
+ function regenerateMcpAuthToken() {
20045
+ const endpoint = getRuntimeHealth().mcp.endpoint;
20046
+ if (!httpServer || !endpoint) return null;
20047
+ mcpAuthToken = crypto$2.randomBytes(32).toString("hex");
20048
+ writeMcpAuthFile(endpoint, mcpAuthToken);
20049
+ return { endpoint };
20050
+ }
19989
20051
  function asTextResponse(text) {
19990
20052
  return { content: [{ type: "text", text }] };
19991
20053
  }
@@ -20011,6 +20073,11 @@ function asPromptResponse(text) {
20011
20073
  function isDangerousMcpAction(name) {
20012
20074
  return name === "close_tab" || isDangerousAction(name);
20013
20075
  }
20076
+ function requiresExplicitMcpApproval(name, args) {
20077
+ if (name === "delete_session" || name === "close_tab" || name === "load_session") return true;
20078
+ if (name === "remove_folder" && args.delete_contents === true) return true;
20079
+ return false;
20080
+ }
20014
20081
  function getActiveTabSummary(tabManager) {
20015
20082
  const activeTab = tabManager.getActiveTab();
20016
20083
  const activeTabId = tabManager.getActiveTabId();
@@ -20099,6 +20166,7 @@ async function withAction(runtime2, tabManager, name, args, executor) {
20099
20166
  args,
20100
20167
  tabId: tabManager.getActiveTabId(),
20101
20168
  dangerous: isDangerousMcpAction(name),
20169
+ requiresApproval: requiresExplicitMcpApproval(name, args),
20102
20170
  executor
20103
20171
  });
20104
20172
  const stateInfo = await getPostActionState(tabManager, name);
@@ -24122,6 +24190,16 @@ function registerScheduleHandlers(windowState, runtime2, sendToAll) {
24122
24190
  return true;
24123
24191
  });
24124
24192
  }
24193
+ const trustedIpcSenderIds = /* @__PURE__ */ new Set();
24194
+ function registerTrustedIpcSender(wc) {
24195
+ trustedIpcSenderIds.add(wc.id);
24196
+ wc.once("destroyed", () => trustedIpcSenderIds.delete(wc.id));
24197
+ }
24198
+ function assertTrustedIpcSender(event) {
24199
+ if (!trustedIpcSenderIds.has(event.sender.id)) {
24200
+ throw new Error("Blocked IPC from untrusted renderer");
24201
+ }
24202
+ }
24125
24203
  function assertString(value, name) {
24126
24204
  if (typeof value !== "string") throw new Error(`${name} must be a string`);
24127
24205
  }
@@ -24537,13 +24615,26 @@ function registerAutofillHandlers(windowState) {
24537
24615
  });
24538
24616
  }
24539
24617
  function registerPageDiffHandlers(windowState, sendToRendererViews) {
24540
- electron.ipcMain.handle(Channels.PAGE_DIFF_GET, () => {
24618
+ const pageEventBuckets = /* @__PURE__ */ new Map();
24619
+ const allowPageEvent = (webContentsId) => {
24620
+ const now = Date.now();
24621
+ const bucket = pageEventBuckets.get(webContentsId);
24622
+ if (!bucket || bucket.resetAt <= now) {
24623
+ pageEventBuckets.set(webContentsId, { count: 1, resetAt: now + 1e3 });
24624
+ return true;
24625
+ }
24626
+ bucket.count += 1;
24627
+ return bucket.count <= 20;
24628
+ };
24629
+ electron.ipcMain.handle(Channels.PAGE_DIFF_GET, (event) => {
24630
+ assertTrustedIpcSender(event);
24541
24631
  const activeTab = windowState.tabManager.getActiveTab();
24542
24632
  const wc = activeTab?.view.webContents;
24543
24633
  if (!wc) return null;
24544
24634
  return getLatestPageDiff(wc.getURL());
24545
24635
  });
24546
- electron.ipcMain.handle(Channels.PAGE_DIFF_HISTORY, () => {
24636
+ electron.ipcMain.handle(Channels.PAGE_DIFF_HISTORY, (event) => {
24637
+ assertTrustedIpcSender(event);
24547
24638
  try {
24548
24639
  if (!isPremiumActiveState(getPremiumState())) {
24549
24640
  return { error: "Premium required" };
@@ -24559,21 +24650,25 @@ function registerPageDiffHandlers(windowState, sendToRendererViews) {
24559
24650
  electron.ipcMain.on(Channels.PAGE_DIFF_ACTIVITY, (event) => {
24560
24651
  const wc = event.sender;
24561
24652
  if (!wc || wc.isDestroyed()) return;
24653
+ if (!allowPageEvent(wc.id)) return;
24562
24654
  notePageMutationActivity(wc, sendToRendererViews);
24563
24655
  });
24564
24656
  electron.ipcMain.on(Channels.PAGE_DIFF_DIRTY, (event) => {
24565
24657
  const wc = event.sender;
24566
24658
  if (!wc || wc.isDestroyed()) return;
24659
+ if (!allowPageEvent(wc.id)) return;
24567
24660
  schedulePageSnapshotCapture(wc, sendToRendererViews);
24568
24661
  });
24569
24662
  }
24570
24663
  function registerVaultHandlers() {
24571
- electron.ipcMain.handle(Channels.VAULT_LIST, () => {
24664
+ electron.ipcMain.handle(Channels.VAULT_LIST, (event) => {
24665
+ assertTrustedIpcSender(event);
24572
24666
  return listEntries$1();
24573
24667
  });
24574
24668
  electron.ipcMain.handle(
24575
24669
  Channels.VAULT_ADD,
24576
- (_, entry) => {
24670
+ (event, entry) => {
24671
+ assertTrustedIpcSender(event);
24577
24672
  if (!entry || typeof entry !== "object") {
24578
24673
  throw new Error("Invalid vault entry");
24579
24674
  }
@@ -24598,7 +24693,8 @@ function registerVaultHandlers() {
24598
24693
  );
24599
24694
  electron.ipcMain.handle(
24600
24695
  Channels.VAULT_UPDATE,
24601
- (_, id, updates) => {
24696
+ (event, id, updates) => {
24697
+ assertTrustedIpcSender(event);
24602
24698
  assertString(id, "id");
24603
24699
  if (!updates || typeof updates !== "object") {
24604
24700
  throw new Error("Invalid updates");
@@ -24606,12 +24702,14 @@ function registerVaultHandlers() {
24606
24702
  return updateEntry$1(id, updates) !== null;
24607
24703
  }
24608
24704
  );
24609
- electron.ipcMain.handle(Channels.VAULT_REMOVE, (_, id) => {
24705
+ electron.ipcMain.handle(Channels.VAULT_REMOVE, (event, id) => {
24706
+ assertTrustedIpcSender(event);
24610
24707
  assertString(id, "id");
24611
24708
  trackVaultAction("credential_removed");
24612
24709
  return removeEntry$1(id);
24613
24710
  });
24614
- electron.ipcMain.handle(Channels.VAULT_AUDIT_LOG, (_, limit) => {
24711
+ electron.ipcMain.handle(Channels.VAULT_AUDIT_LOG, (event, limit) => {
24712
+ assertTrustedIpcSender(event);
24615
24713
  return readAuditLog$1(limit);
24616
24714
  });
24617
24715
  }
@@ -24630,17 +24728,20 @@ function normalizeTags(value) {
24630
24728
  return tags.length > 0 ? tags : void 0;
24631
24729
  }
24632
24730
  function registerHumanVaultHandlers() {
24633
- electron.ipcMain.handle(Channels.HUMAN_VAULT_LIST, (_, domain) => {
24731
+ electron.ipcMain.handle(Channels.HUMAN_VAULT_LIST, (event, domain) => {
24732
+ assertTrustedIpcSender(event);
24634
24733
  if (domain !== void 0) assertString(domain, "domain");
24635
24734
  return domain ? findForDomain(domain) : listEntries();
24636
24735
  });
24637
- electron.ipcMain.handle(Channels.HUMAN_VAULT_GET, (_, id) => {
24736
+ electron.ipcMain.handle(Channels.HUMAN_VAULT_GET, (event, id) => {
24737
+ assertTrustedIpcSender(event);
24638
24738
  assertString(id, "id");
24639
24739
  return getEntrySafe(id);
24640
24740
  });
24641
24741
  electron.ipcMain.handle(
24642
24742
  Channels.HUMAN_VAULT_SAVE,
24643
- (_, input) => {
24743
+ (event, input) => {
24744
+ assertTrustedIpcSender(event);
24644
24745
  if (!input || typeof input !== "object") {
24645
24746
  throw new Error("Invalid credential entry");
24646
24747
  }
@@ -24670,7 +24771,8 @@ function registerHumanVaultHandlers() {
24670
24771
  );
24671
24772
  electron.ipcMain.handle(
24672
24773
  Channels.HUMAN_VAULT_UPDATE,
24673
- (_, id, updates) => {
24774
+ (event, id, updates) => {
24775
+ assertTrustedIpcSender(event);
24674
24776
  assertString(id, "id");
24675
24777
  if (!updates || typeof updates !== "object") {
24676
24778
  throw new Error("Invalid updates");
@@ -24712,26 +24814,31 @@ function registerHumanVaultHandlers() {
24712
24814
  return safe;
24713
24815
  }
24714
24816
  );
24715
- electron.ipcMain.handle(Channels.HUMAN_VAULT_REMOVE, (_, id) => {
24817
+ electron.ipcMain.handle(Channels.HUMAN_VAULT_REMOVE, (event, id) => {
24818
+ assertTrustedIpcSender(event);
24716
24819
  assertString(id, "id");
24717
24820
  return removeEntry(id);
24718
24821
  });
24719
- electron.ipcMain.handle(Channels.HUMAN_VAULT_AUDIT_LOG, (_, limit) => {
24822
+ electron.ipcMain.handle(Channels.HUMAN_VAULT_AUDIT_LOG, (event, limit) => {
24823
+ assertTrustedIpcSender(event);
24720
24824
  return readAuditLog(limit);
24721
24825
  });
24722
24826
  }
24723
24827
  function registerWindowControlHandlers(mainWindow) {
24724
- electron.ipcMain.handle(Channels.WINDOW_MINIMIZE, () => {
24828
+ electron.ipcMain.handle(Channels.WINDOW_MINIMIZE, (event) => {
24829
+ assertTrustedIpcSender(event);
24725
24830
  mainWindow.minimize();
24726
24831
  });
24727
- electron.ipcMain.handle(Channels.WINDOW_MAXIMIZE, () => {
24832
+ electron.ipcMain.handle(Channels.WINDOW_MAXIMIZE, (event) => {
24833
+ assertTrustedIpcSender(event);
24728
24834
  if (mainWindow.isMaximized()) {
24729
24835
  mainWindow.unmaximize();
24730
24836
  } else {
24731
24837
  mainWindow.maximize();
24732
24838
  }
24733
24839
  });
24734
- electron.ipcMain.handle(Channels.WINDOW_CLOSE, () => {
24840
+ electron.ipcMain.handle(Channels.WINDOW_CLOSE, (event) => {
24841
+ assertTrustedIpcSender(event);
24735
24842
  mainWindow.close();
24736
24843
  });
24737
24844
  }
@@ -24870,6 +24977,32 @@ function installAdBlockingForSession(ses, tabManager) {
24870
24977
  });
24871
24978
  }
24872
24979
  const filePath$1 = () => path$1.join(electron.app.getPath("userData"), "vessel-downloads.json");
24980
+ const EXECUTABLE_EXTENSIONS = /* @__PURE__ */ new Set([
24981
+ ".appimage",
24982
+ ".bat",
24983
+ ".cmd",
24984
+ ".command",
24985
+ ".desktop",
24986
+ ".exe",
24987
+ ".msi",
24988
+ ".ps1",
24989
+ ".scr",
24990
+ ".sh"
24991
+ ]);
24992
+ function hasMisleadingDoubleExtension(filename) {
24993
+ return /\.(pdf|docx?|xlsx?|pptx?|png|jpe?g|gif|txt|zip)\.(exe|msi|bat|cmd|ps1|sh|scr|appimage)$/i.test(filename);
24994
+ }
24995
+ function isExecutableDownload(savePath) {
24996
+ return EXECUTABLE_EXTENSIONS.has(path$1.extname(savePath).toLowerCase());
24997
+ }
24998
+ function executableWarningDetail(item) {
24999
+ return [
25000
+ "This file can run code on your computer. Only open it if you trust the source.",
25001
+ item.url ? `Source: ${item.url}` : null,
25002
+ item.mimeType ? `Type: ${item.mimeType}` : null,
25003
+ hasMisleadingDoubleExtension(item.filename) ? "Warning: this filename uses a misleading double extension." : null
25004
+ ].filter(Boolean).join("\n");
25005
+ }
24873
25006
  function parse(raw) {
24874
25007
  if (!raw || typeof raw !== "object") return { items: [] };
24875
25008
  const items = Array.isArray(raw.items) ? raw.items : [];
@@ -24888,13 +25021,13 @@ function persist() {
24888
25021
  persistence$1.schedule();
24889
25022
  }
24890
25023
  function emit$1() {
24891
- broadcaster$1?.(Channels.DOWNLOADS_UPDATE, state.items);
25024
+ broadcaster$1?.(Channels.DOWNLOADS_UPDATE, listDownloads());
24892
25025
  }
24893
25026
  function setDownloadBroadcaster(fn) {
24894
25027
  broadcaster$1 = fn;
24895
25028
  }
24896
25029
  function listDownloads() {
24897
- return state.items;
25030
+ return state.items.map((item) => ({ ...item }));
24898
25031
  }
24899
25032
  function upsertDownload(input) {
24900
25033
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -24918,7 +25051,19 @@ function clearDownloads() {
24918
25051
  }
24919
25052
  async function openDownload(id) {
24920
25053
  const item = state.items.find((d) => d.id === id);
24921
- if (!item || !fs$1.existsSync(item.savePath)) return false;
25054
+ if (!item || item.state !== "completed" || !fs$1.existsSync(item.savePath)) return false;
25055
+ if (isExecutableDownload(item.savePath)) {
25056
+ const result = electron.dialog.showMessageBoxSync({
25057
+ type: "warning",
25058
+ buttons: ["Cancel", "Open Anyway"],
25059
+ defaultId: 0,
25060
+ cancelId: 0,
25061
+ title: "Open executable download?",
25062
+ message: `Open ${item.filename}?`,
25063
+ detail: executableWarningDetail(item)
25064
+ });
25065
+ if (result !== 1) return false;
25066
+ }
24922
25067
  return await electron.shell.openPath(item.savePath) === "";
24923
25068
  }
24924
25069
  async function showDownloadInFolder(id) {
@@ -24969,6 +25114,8 @@ function installDownloadHandlerForSession(targetSession, chromeView) {
24969
25114
  const info = {
24970
25115
  filename,
24971
25116
  savePath,
25117
+ url: item.getURL(),
25118
+ mimeType: typeof item.getMimeType === "function" ? item.getMimeType() : void 0,
24972
25119
  totalBytes: item.getTotalBytes(),
24973
25120
  receivedBytes: 0,
24974
25121
  state: "progressing"
@@ -25016,9 +25163,9 @@ function loadRenderers(chromeView, sidebarView, devtoolsPanelView) {
25016
25163
  const sidebarUrl = rendererUrlFor("sidebar");
25017
25164
  const devtoolsUrl = rendererUrlFor("devtools");
25018
25165
  if (chromeUrl && sidebarUrl && devtoolsUrl) {
25019
- chromeView.webContents.loadURL(chromeUrl);
25020
- sidebarView.webContents.loadURL(sidebarUrl);
25021
- devtoolsPanelView.webContents.loadURL(devtoolsUrl);
25166
+ void loadTrustedAppURL(chromeView.webContents, chromeUrl);
25167
+ void loadTrustedAppURL(sidebarView.webContents, sidebarUrl);
25168
+ void loadTrustedAppURL(devtoolsPanelView.webContents, devtoolsUrl);
25022
25169
  } else {
25023
25170
  const rendererFile = resolveRendererFile();
25024
25171
  chromeView.webContents.loadFile(rendererFile, {
@@ -25244,7 +25391,7 @@ function loadPrivateRenderer(chromeView) {
25244
25391
  const url = new URL(devUrl);
25245
25392
  url.searchParams.set("view", "chrome");
25246
25393
  url.searchParams.set("private", "1");
25247
- chromeView.webContents.loadURL(url.toString());
25394
+ void loadTrustedAppURL(chromeView.webContents, url.toString());
25248
25395
  } else {
25249
25396
  chromeView.webContents.loadFile(resolveRendererFile(), {
25250
25397
  query: { view: "chrome", private: "1" }
@@ -25479,7 +25626,7 @@ function loadSecondaryRenderer(chromeView) {
25479
25626
  const url = new URL(devUrl);
25480
25627
  url.searchParams.set("view", "chrome");
25481
25628
  url.searchParams.set("secondary", "1");
25482
- chromeView.webContents.loadURL(url.toString());
25629
+ void loadTrustedAppURL(chromeView.webContents, url.toString());
25483
25630
  } else {
25484
25631
  chromeView.webContents.loadFile(resolveRendererFile(), {
25485
25632
  query: { view: "chrome", secondary: "1" }
@@ -25699,7 +25846,8 @@ function registerBookmarkHandlers() {
25699
25846
  });
25700
25847
  electron.ipcMain.handle(
25701
25848
  Channels.BOOKMARKS_EXPORT_HTML,
25702
- async (_, options) => {
25849
+ async (event, options) => {
25850
+ assertTrustedIpcSender(event);
25703
25851
  const { canceled, filePath: filePath2 } = await electron.dialog.showSaveDialog({
25704
25852
  title: "Export Bookmarks",
25705
25853
  defaultPath: "vessel-bookmarks.html",
@@ -25717,7 +25865,8 @@ function registerBookmarkHandlers() {
25717
25865
  };
25718
25866
  }
25719
25867
  );
25720
- electron.ipcMain.handle(Channels.BOOKMARKS_EXPORT_JSON, async () => {
25868
+ electron.ipcMain.handle(Channels.BOOKMARKS_EXPORT_JSON, async (event) => {
25869
+ assertTrustedIpcSender(event);
25721
25870
  const { canceled, filePath: filePath2 } = await electron.dialog.showSaveDialog({
25722
25871
  title: "Export Vessel Bookmark Archive",
25723
25872
  defaultPath: "vessel-bookmarks.json",
@@ -25734,7 +25883,8 @@ function registerBookmarkHandlers() {
25734
25883
  });
25735
25884
  electron.ipcMain.handle(
25736
25885
  Channels.FOLDER_EXPORT_HTML,
25737
- async (_, folderId, options) => {
25886
+ async (event, folderId, options) => {
25887
+ assertTrustedIpcSender(event);
25738
25888
  const folder = getFolder(folderId);
25739
25889
  if (!folder) return null;
25740
25890
  const { canceled, filePath: filePath2 } = await electron.dialog.showSaveDialog({
@@ -25755,7 +25905,8 @@ function registerBookmarkHandlers() {
25755
25905
  };
25756
25906
  }
25757
25907
  );
25758
- electron.ipcMain.handle(Channels.BOOKMARKS_IMPORT_HTML, async () => {
25908
+ electron.ipcMain.handle(Channels.BOOKMARKS_IMPORT_HTML, async (event) => {
25909
+ assertTrustedIpcSender(event);
25759
25910
  const { canceled, filePaths } = await electron.dialog.showOpenDialog({
25760
25911
  title: "Import Bookmarks",
25761
25912
  filters: [
@@ -25768,7 +25919,8 @@ function registerBookmarkHandlers() {
25768
25919
  trackBookmarkAction("import");
25769
25920
  return importBookmarksFromHtml(content);
25770
25921
  });
25771
- electron.ipcMain.handle(Channels.BOOKMARKS_IMPORT_JSON, async () => {
25922
+ electron.ipcMain.handle(Channels.BOOKMARKS_IMPORT_JSON, async (event) => {
25923
+ assertTrustedIpcSender(event);
25772
25924
  const { canceled, filePaths } = await electron.dialog.showOpenDialog({
25773
25925
  title: "Import Bookmarks",
25774
25926
  filters: [
@@ -25799,10 +25951,12 @@ function registerHistoryHandlers() {
25799
25951
  electron.ipcMain.handle(Channels.HISTORY_SEARCH, (_, query) => {
25800
25952
  return search(query);
25801
25953
  });
25802
- electron.ipcMain.handle(Channels.HISTORY_CLEAR, () => {
25954
+ electron.ipcMain.handle(Channels.HISTORY_CLEAR, (event) => {
25955
+ assertTrustedIpcSender(event);
25803
25956
  clearAll$1();
25804
25957
  });
25805
- electron.ipcMain.handle(Channels.HISTORY_EXPORT_HTML, async () => {
25958
+ electron.ipcMain.handle(Channels.HISTORY_EXPORT_HTML, async (event) => {
25959
+ assertTrustedIpcSender(event);
25806
25960
  const { canceled, filePath: filePath2 } = await electron.dialog.showSaveDialog({
25807
25961
  title: "Export History",
25808
25962
  defaultPath: "vessel-history.html",
@@ -25813,7 +25967,8 @@ function registerHistoryHandlers() {
25813
25967
  await fs.promises.writeFile(filePath2, content, "utf-8");
25814
25968
  return { filePath: filePath2, count: getState$1().entries.length };
25815
25969
  });
25816
- electron.ipcMain.handle(Channels.HISTORY_EXPORT_JSON, async () => {
25970
+ electron.ipcMain.handle(Channels.HISTORY_EXPORT_JSON, async (event) => {
25971
+ assertTrustedIpcSender(event);
25817
25972
  const { canceled, filePath: filePath2 } = await electron.dialog.showSaveDialog({
25818
25973
  title: "Export History",
25819
25974
  defaultPath: "vessel-history.json",
@@ -25824,7 +25979,8 @@ function registerHistoryHandlers() {
25824
25979
  await fs.promises.writeFile(filePath2, content, "utf-8");
25825
25980
  return { filePath: filePath2, count: getState$1().entries.length };
25826
25981
  });
25827
- electron.ipcMain.handle(Channels.HISTORY_IMPORT, async () => {
25982
+ electron.ipcMain.handle(Channels.HISTORY_IMPORT, async (event) => {
25983
+ assertTrustedIpcSender(event);
25828
25984
  const { canceled, filePaths } = await electron.dialog.showOpenDialog({
25829
25985
  title: "Import History",
25830
25986
  filters: [
@@ -26035,7 +26191,8 @@ function buildCertificateDetailsHtml(state2) {
26035
26191
  </html>`;
26036
26192
  }
26037
26193
  function registerSecurityHandlers(tabManager) {
26038
- electron.ipcMain.handle(Channels.SECURITY_SHOW_DETAILS, async (_, state2) => {
26194
+ electron.ipcMain.handle(Channels.SECURITY_SHOW_DETAILS, async (event, state2) => {
26195
+ assertTrustedIpcSender(event);
26039
26196
  const domain = (() => {
26040
26197
  try {
26041
26198
  return new URL(state2.url).hostname || state2.url;
@@ -26056,13 +26213,15 @@ function registerSecurityHandlers(tabManager) {
26056
26213
  spellcheck: false
26057
26214
  }
26058
26215
  });
26059
- void win.loadURL(`data:text/html;charset=utf-8,${encodeURIComponent(content)}`);
26216
+ void loadInternalDataURL(win.webContents, `data:text/html;charset=utf-8,${encodeURIComponent(content)}`);
26060
26217
  });
26061
- electron.ipcMain.handle(Channels.SECURITY_PROCEED_ANYWAY, (_, tabId) => {
26218
+ electron.ipcMain.handle(Channels.SECURITY_PROCEED_ANYWAY, (event, tabId) => {
26219
+ assertTrustedIpcSender(event);
26062
26220
  assertString(tabId, "tabId");
26063
26221
  tabManager.proceedAnyway(tabId);
26064
26222
  });
26065
- electron.ipcMain.handle(Channels.SECURITY_GO_BACK_TO_SAFETY, (_, tabId) => {
26223
+ electron.ipcMain.handle(Channels.SECURITY_GO_BACK_TO_SAFETY, (event, tabId) => {
26224
+ assertTrustedIpcSender(event);
26066
26225
  assertString(tabId, "tabId");
26067
26226
  tabManager.goBackToSafety(tabId);
26068
26227
  });
@@ -26070,6 +26229,7 @@ function registerSecurityHandlers(tabManager) {
26070
26229
  const logger$5 = createLogger("CodexIPC");
26071
26230
  function registerCodexHandlers() {
26072
26231
  electron.ipcMain.handle(Channels.CODEX_START_AUTH, async (event) => {
26232
+ assertTrustedIpcSender(event);
26073
26233
  const wc = event.sender;
26074
26234
  if (!wc || wc.isDestroyed()) {
26075
26235
  return {
@@ -26100,29 +26260,57 @@ function registerCodexHandlers() {
26100
26260
  };
26101
26261
  }
26102
26262
  });
26103
- electron.ipcMain.handle(Channels.CODEX_CANCEL_AUTH, () => {
26263
+ electron.ipcMain.handle(Channels.CODEX_CANCEL_AUTH, (event) => {
26264
+ assertTrustedIpcSender(event);
26104
26265
  cancelCodexOAuth();
26105
26266
  return { ok: true };
26106
26267
  });
26107
- electron.ipcMain.handle(Channels.CODEX_DISCONNECT, () => {
26268
+ electron.ipcMain.handle(Channels.CODEX_DISCONNECT, (event) => {
26269
+ assertTrustedIpcSender(event);
26108
26270
  clearStoredCodexTokens();
26109
26271
  return { ok: true };
26110
26272
  });
26111
26273
  }
26112
26274
  const filePath = () => path$1.join(electron.app.getPath("userData"), "vessel-permissions.json");
26275
+ const ALLOWED_PERMISSION_TYPES = /* @__PURE__ */ new Set([
26276
+ "clipboard-read",
26277
+ "fullscreen",
26278
+ "geolocation",
26279
+ "media",
26280
+ "midiSysex",
26281
+ "notifications",
26282
+ "pointerLock"
26283
+ ]);
26284
+ function parseOrigin(value) {
26285
+ try {
26286
+ const origin = new URL(value).origin;
26287
+ return origin === "null" ? null : origin;
26288
+ } catch {
26289
+ return null;
26290
+ }
26291
+ }
26292
+ function isPermissionRecord(value) {
26293
+ if (!value || typeof value !== "object") return false;
26294
+ const record = value;
26295
+ return typeof record.origin === "string" && parseOrigin(record.origin) === record.origin && typeof record.permission === "string" && ALLOWED_PERMISSION_TYPES.has(record.permission) && (record.decision === "allow" || record.decision === "deny") && typeof record.updatedAt === "string";
26296
+ }
26113
26297
  let records = loadJsonFile({
26114
26298
  filePath: filePath(),
26115
26299
  fallback: [],
26116
- parse: (raw) => Array.isArray(raw) ? raw : []
26300
+ parse: (raw) => Array.isArray(raw) ? raw.filter(isPermissionRecord) : []
26117
26301
  });
26118
26302
  const persistence = createDebouncedJsonPersistence({ debounceMs: 250, filePath: filePath(), getValue: () => records, logLabel: "permissions" });
26303
+ const sessionDecisions = /* @__PURE__ */ new Map();
26119
26304
  let broadcaster = null;
26120
26305
  function key(origin, permission) {
26121
26306
  return `${origin}
26122
26307
  ${permission}`;
26123
26308
  }
26309
+ function snapshot() {
26310
+ return records.map((record) => ({ ...record }));
26311
+ }
26124
26312
  function emit() {
26125
- broadcaster?.(Channels.PERMISSIONS_GET, records);
26313
+ broadcaster?.(Channels.PERMISSIONS_GET, snapshot());
26126
26314
  }
26127
26315
  function save(origin, permission, decision) {
26128
26316
  const k = key(origin, permission);
@@ -26134,15 +26322,21 @@ function save(origin, permission, decision) {
26134
26322
  emit();
26135
26323
  }
26136
26324
  function listPermissions() {
26137
- return records;
26325
+ return snapshot();
26138
26326
  }
26139
26327
  function clearPermissions() {
26140
26328
  records = [];
26329
+ sessionDecisions.clear();
26141
26330
  persistence.schedule();
26142
26331
  emit();
26143
26332
  }
26144
26333
  function clearPermissionsForOrigin(origin) {
26334
+ if (!parseOrigin(origin)) return;
26145
26335
  records = records.filter((record) => record.origin !== origin);
26336
+ for (const storedKey of sessionDecisions.keys()) {
26337
+ if (storedKey.startsWith(`${origin}
26338
+ `)) sessionDecisions.delete(storedKey);
26339
+ }
26146
26340
  persistence.schedule();
26147
26341
  emit();
26148
26342
  }
@@ -26151,27 +26345,54 @@ function setPermissionBroadcaster(fn) {
26151
26345
  }
26152
26346
  function installPermissionHandler() {
26153
26347
  electron.session.defaultSession.setPermissionRequestHandler((webContents, permission, callback, details) => {
26154
- const origin = new URL(details.requestingUrl || webContents.getURL()).origin;
26348
+ if (!ALLOWED_PERMISSION_TYPES.has(permission)) {
26349
+ callback(false);
26350
+ return;
26351
+ }
26352
+ const origin = parseOrigin(details.requestingUrl || webContents.getURL());
26353
+ if (!origin) {
26354
+ callback(false);
26355
+ return;
26356
+ }
26357
+ const k = key(origin, permission);
26155
26358
  const existing = records.find((r) => r.origin === origin && r.permission === permission);
26156
26359
  if (existing) {
26157
26360
  callback(existing.decision === "allow");
26158
26361
  return;
26159
26362
  }
26363
+ const sessionDecision = sessionDecisions.get(k);
26364
+ if (sessionDecision) {
26365
+ callback(sessionDecision === "allow");
26366
+ return;
26367
+ }
26160
26368
  const result = electron.dialog.showMessageBoxSync({
26161
26369
  type: "question",
26162
- buttons: ["Deny", "Allow"],
26370
+ buttons: ["Deny", "Allow Once", "Allow Until Quit", "Always Allow"],
26163
26371
  defaultId: 0,
26164
26372
  cancelId: 0,
26165
26373
  title: "Site permission request",
26166
26374
  message: `${origin} wants to use ${permission}`,
26167
- detail: "Vessel will remember your choice. You can clear saved permissions in Settings > Privacy."
26375
+ detail: "Temporary choices are safer for camera, microphone, location, and clipboard access. Persistent choices can be cleared in Settings > Privacy."
26168
26376
  });
26169
- const decision = result === 1 ? "allow" : "deny";
26170
- save(origin, permission, decision);
26171
- callback(decision === "allow");
26377
+ if (result === 1) {
26378
+ callback(true);
26379
+ return;
26380
+ }
26381
+ if (result === 2) {
26382
+ sessionDecisions.set(k, "allow");
26383
+ callback(true);
26384
+ return;
26385
+ }
26386
+ if (result === 3) {
26387
+ save(origin, permission, "allow");
26388
+ callback(true);
26389
+ return;
26390
+ }
26391
+ save(origin, permission, "deny");
26392
+ callback(false);
26172
26393
  });
26173
26394
  }
26174
- const NPM_PACKAGE_URL = "https://registry.npmjs.org/@quanta-intellect%2Fvessel-browser/latest";
26395
+ const GITHUB_LATEST_RELEASE_API_URL = "https://api.github.com/repos/unmodeled-tyler/quanta-vessel-browser/releases/latest";
26175
26396
  const RELEASES_URL = "https://github.com/unmodeled-tyler/quanta-vessel-browser/releases/latest";
26176
26397
  function normalizeVersion(version) {
26177
26398
  return version.replace(/^v/i, "").split(/[.-]/).slice(0, 3).map((part) => {
@@ -26192,21 +26413,22 @@ async function checkForUpdates() {
26192
26413
  const currentVersion = electron.app.getVersion();
26193
26414
  const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
26194
26415
  try {
26195
- const response = await fetch(NPM_PACKAGE_URL, {
26196
- headers: { accept: "application/json", "user-agent": `Vessel/${currentVersion}` }
26416
+ const response = await fetch(GITHUB_LATEST_RELEASE_API_URL, {
26417
+ headers: { accept: "application/vnd.github+json", "user-agent": `Vessel/${currentVersion}` }
26197
26418
  });
26198
26419
  if (!response.ok) {
26199
- throw new Error(`Registry responded with ${response.status}`);
26420
+ throw new Error(`GitHub Releases responded with ${response.status}`);
26200
26421
  }
26201
26422
  const body = await response.json();
26202
- const latestVersion = typeof body.version === "string" ? body.version : null;
26203
- if (!latestVersion) throw new Error("Registry response did not include a version");
26423
+ const latestVersion = typeof body.tag_name === "string" ? body.tag_name : null;
26424
+ if (!latestVersion) throw new Error("GitHub release response did not include a tag name");
26425
+ const releaseUrl = typeof body.html_url === "string" && body.html_url.startsWith("https://github.com/") ? body.html_url : RELEASES_URL;
26204
26426
  return {
26205
26427
  currentVersion,
26206
26428
  latestVersion,
26207
26429
  updateAvailable: compareVersions(latestVersion, currentVersion) > 0,
26208
26430
  checkedAt,
26209
- releaseUrl: RELEASES_URL
26431
+ releaseUrl
26210
26432
  };
26211
26433
  } catch (error) {
26212
26434
  return {
@@ -26220,7 +26442,7 @@ async function checkForUpdates() {
26220
26442
  }
26221
26443
  }
26222
26444
  async function openUpdateDownload() {
26223
- await electron.shell.openExternal(RELEASES_URL);
26445
+ await openExternalAllowlisted(RELEASES_URL, { hosts: ["github.com"] });
26224
26446
  }
26225
26447
  let activeChatProvider = null;
26226
26448
  const logger$4 = createLogger("IPC");
@@ -26255,10 +26477,18 @@ async function togglePictureInPicture(tabManager) {
26255
26477
  }
26256
26478
  function registerIpcHandlers(windowState, runtime2) {
26257
26479
  const { tabManager, chromeView, sidebarView, devtoolsPanelView, mainWindow } = windowState;
26258
- electron.ipcMain.handle(Channels.OPEN_PRIVATE_WINDOW, () => {
26480
+ registerTrustedIpcSender(chromeView.webContents);
26481
+ registerTrustedIpcSender(sidebarView.webContents);
26482
+ registerTrustedIpcSender(devtoolsPanelView.webContents);
26483
+ const requireTrusted = (event) => {
26484
+ assertTrustedIpcSender(event);
26485
+ };
26486
+ electron.ipcMain.handle(Channels.OPEN_PRIVATE_WINDOW, (event) => {
26487
+ requireTrusted(event);
26259
26488
  createPrivateWindow();
26260
26489
  });
26261
- electron.ipcMain.handle(Channels.OPEN_NEW_WINDOW, () => {
26490
+ electron.ipcMain.handle(Channels.OPEN_NEW_WINDOW, (event) => {
26491
+ requireTrusted(event);
26262
26492
  createSecondaryWindow();
26263
26493
  });
26264
26494
  electron.ipcMain.handle(Channels.IS_PRIVATE_MODE, () => false);
@@ -26530,14 +26760,15 @@ function registerIpcHandlers(windowState, runtime2) {
26530
26760
  const originalUrl = activeTab.readerOriginalUrl;
26531
26761
  activeTab.setReaderMode(false);
26532
26762
  if (originalUrl) {
26533
- activeTab.view.webContents.loadURL(originalUrl);
26763
+ void loadPermittedNavigationURL(activeTab.view.webContents, originalUrl);
26534
26764
  }
26535
26765
  } else {
26536
26766
  const originalUrl = activeTab.state.url;
26537
26767
  const content = await extractContent(activeTab.view.webContents);
26538
26768
  const html = generateReaderHTML(content);
26539
26769
  activeTab.setReaderMode(true, originalUrl);
26540
- activeTab.view.webContents.loadURL(
26770
+ void loadInternalDataURL(
26771
+ activeTab.view.webContents,
26541
26772
  `data:text/html;charset=utf-8,${encodeURIComponent(html)}`
26542
26773
  );
26543
26774
  }
@@ -26619,7 +26850,12 @@ function registerIpcHandlers(windowState, runtime2) {
26619
26850
  return getRendererSettings();
26620
26851
  });
26621
26852
  electron.ipcMain.handle(Channels.SETTINGS_HEALTH_GET, () => getRuntimeHealth());
26622
- electron.ipcMain.handle(Channels.SETTINGS_SET, async (_, key2, value) => {
26853
+ electron.ipcMain.handle(Channels.MCP_REGENERATE_TOKEN, (event) => {
26854
+ requireTrusted(event);
26855
+ return regenerateMcpAuthToken();
26856
+ });
26857
+ electron.ipcMain.handle(Channels.SETTINGS_SET, async (event, key2, value) => {
26858
+ requireTrusted(event);
26623
26859
  assertString(key2, "key");
26624
26860
  if (!SETTABLE_KEYS.has(key2)) {
26625
26861
  throw new Error(`Unknown setting key: ${key2}`);
@@ -26639,11 +26875,18 @@ function registerIpcHandlers(windowState, runtime2) {
26639
26875
  return rendererSettings;
26640
26876
  });
26641
26877
  electron.ipcMain.handle(Channels.AGENT_RUNTIME_GET, () => runtime2.getState());
26642
- electron.ipcMain.handle(Channels.AGENT_PAUSE, () => runtime2.pause());
26643
- electron.ipcMain.handle(Channels.AGENT_RESUME, () => runtime2.resume());
26878
+ electron.ipcMain.handle(Channels.AGENT_PAUSE, (event) => {
26879
+ requireTrusted(event);
26880
+ return runtime2.pause();
26881
+ });
26882
+ electron.ipcMain.handle(Channels.AGENT_RESUME, (event) => {
26883
+ requireTrusted(event);
26884
+ return runtime2.resume();
26885
+ });
26644
26886
  electron.ipcMain.handle(
26645
26887
  Channels.AGENT_SET_APPROVAL_MODE,
26646
- (_, mode) => {
26888
+ (event, mode) => {
26889
+ requireTrusted(event);
26647
26890
  assertString(mode, "mode");
26648
26891
  if (!VALID_APPROVAL_MODES.includes(mode)) {
26649
26892
  throw new Error(`Invalid approval mode: ${mode}`);
@@ -26655,7 +26898,10 @@ function registerIpcHandlers(windowState, runtime2) {
26655
26898
  );
26656
26899
  electron.ipcMain.handle(
26657
26900
  Channels.AGENT_APPROVAL_RESOLVE,
26658
- (_, approvalId, approved) => runtime2.resolveApproval(approvalId, approved)
26901
+ (event, approvalId, approved) => {
26902
+ requireTrusted(event);
26903
+ return runtime2.resolveApproval(approvalId, approved);
26904
+ }
26659
26905
  );
26660
26906
  electron.ipcMain.handle(
26661
26907
  Channels.AGENT_CHECKPOINT_CREATE,
@@ -26679,7 +26925,10 @@ function registerIpcHandlers(windowState, runtime2) {
26679
26925
  );
26680
26926
  electron.ipcMain.handle(
26681
26927
  Channels.AGENT_SESSION_RESTORE,
26682
- (_, snapshot) => runtime2.restoreSession(snapshot)
26928
+ (event, snapshot2) => {
26929
+ requireTrusted(event);
26930
+ return runtime2.restoreSession(snapshot2);
26931
+ }
26683
26932
  );
26684
26933
  registerBookmarkHandlers();
26685
26934
  electron.ipcMain.handle(Channels.HIGHLIGHT_CAPTURE, async () => {
@@ -26799,17 +27048,20 @@ function registerIpcHandlers(windowState, runtime2) {
26799
27048
  electron.ipcMain.handle(Channels.AUTOMATION_GET_INSTALLED, () => {
26800
27049
  return getInstalledKits();
26801
27050
  });
26802
- electron.ipcMain.handle(Channels.AUTOMATION_INSTALL_FROM_FILE, async () => {
27051
+ electron.ipcMain.handle(Channels.AUTOMATION_INSTALL_FROM_FILE, async (event) => {
27052
+ requireTrusted(event);
26803
27053
  return await installKitFromFile();
26804
27054
  });
26805
- electron.ipcMain.handle(Channels.AUTOMATION_UNINSTALL, (_event, id) => {
27055
+ electron.ipcMain.handle(Channels.AUTOMATION_UNINSTALL, (event, id) => {
27056
+ requireTrusted(event);
26806
27057
  assertString(id, "id");
26807
27058
  return uninstallKit(id, getScheduledKitIds());
26808
27059
  });
26809
27060
  registerScheduleHandlers(windowState, runtime2, sendToRendererViews);
26810
27061
  registerAutofillHandlers(windowState);
26811
27062
  registerPageDiffHandlers(windowState, sendToRendererViews);
26812
- electron.ipcMain.handle(Channels.CLEAR_BROWSING_DATA, async (_, options) => {
27063
+ electron.ipcMain.handle(Channels.CLEAR_BROWSING_DATA, async (event, options) => {
27064
+ requireTrusted(event);
26813
27065
  const { cache, cookies, history, localStorage: clearLs, timeRange } = options;
26814
27066
  if (cache) {
26815
27067
  await electron.session.defaultSession.clearCache();
@@ -26827,23 +27079,35 @@ function registerIpcHandlers(windowState, runtime2) {
26827
27079
  setDownloadBroadcaster(sendToRendererViews);
26828
27080
  setPermissionBroadcaster(sendToRendererViews);
26829
27081
  electron.ipcMain.handle(Channels.DOWNLOADS_GET, () => listDownloads());
26830
- electron.ipcMain.handle(Channels.DOWNLOADS_CLEAR, () => {
27082
+ electron.ipcMain.handle(Channels.DOWNLOADS_CLEAR, (event) => {
27083
+ requireTrusted(event);
26831
27084
  clearDownloads();
26832
27085
  return true;
26833
27086
  });
26834
- electron.ipcMain.handle(Channels.DOWNLOADS_OPEN, (_event, id) => openDownload(id));
26835
- electron.ipcMain.handle(Channels.DOWNLOADS_SHOW_IN_FOLDER, (_event, id) => showDownloadInFolder(id));
27087
+ electron.ipcMain.handle(Channels.DOWNLOADS_OPEN, (event, id) => {
27088
+ requireTrusted(event);
27089
+ return openDownload(id);
27090
+ });
27091
+ electron.ipcMain.handle(Channels.DOWNLOADS_SHOW_IN_FOLDER, (event, id) => {
27092
+ requireTrusted(event);
27093
+ return showDownloadInFolder(id);
27094
+ });
26836
27095
  electron.ipcMain.handle(Channels.PERMISSIONS_GET, () => listPermissions());
26837
- electron.ipcMain.handle(Channels.PERMISSIONS_CLEAR, () => {
27096
+ electron.ipcMain.handle(Channels.PERMISSIONS_CLEAR, (event) => {
27097
+ requireTrusted(event);
26838
27098
  clearPermissions();
26839
27099
  return true;
26840
27100
  });
26841
- electron.ipcMain.handle(Channels.PERMISSIONS_CLEAR_ORIGIN, (_event, origin) => {
27101
+ electron.ipcMain.handle(Channels.PERMISSIONS_CLEAR_ORIGIN, (event, origin) => {
27102
+ requireTrusted(event);
26842
27103
  clearPermissionsForOrigin(origin);
26843
27104
  return true;
26844
27105
  });
26845
27106
  electron.ipcMain.handle(Channels.UPDATES_CHECK, () => checkForUpdates());
26846
- electron.ipcMain.handle(Channels.UPDATES_OPEN_DOWNLOAD, () => openUpdateDownload());
27107
+ electron.ipcMain.handle(Channels.UPDATES_OPEN_DOWNLOAD, (event) => {
27108
+ requireTrusted(event);
27109
+ return openUpdateDownload();
27110
+ });
26847
27111
  electron.ipcMain.handle(Channels.TAB_TOGGLE_PIP, async () => {
26848
27112
  return togglePictureInPicture(tabManager);
26849
27113
  });
@@ -27276,6 +27540,7 @@ class AgentRuntime {
27276
27540
  this.state = this.loadPersistedState();
27277
27541
  onMcpStatusChange(() => this.emit());
27278
27542
  }
27543
+ tabManager;
27279
27544
  state;
27280
27545
  updateListener = null;
27281
27546
  pendingResolvers = /* @__PURE__ */ new Map();
@@ -27287,11 +27552,11 @@ class AgentRuntime {
27287
27552
  }
27288
27553
  }
27289
27554
  getState() {
27290
- const snapshot = clone(this.state);
27291
- snapshot.mcpStatus = getMcpStatus();
27292
- snapshot.canUndo = this.canUndo();
27293
- snapshot.undoInfo = this.getUndoInfo();
27294
- return snapshot;
27555
+ const snapshot2 = clone(this.state);
27556
+ snapshot2.mcpStatus = getMcpStatus();
27557
+ snapshot2.canUndo = this.canUndo();
27558
+ snapshot2.undoInfo = this.getUndoInfo();
27559
+ return snapshot2;
27295
27560
  }
27296
27561
  onTabStateChanged() {
27297
27562
  this.captureSession();
@@ -27327,13 +27592,13 @@ class AgentRuntime {
27327
27592
  return this.getState();
27328
27593
  }
27329
27594
  createCheckpoint(name, note) {
27330
- const snapshot = this.captureSession(note);
27595
+ const snapshot2 = this.captureSession(note);
27331
27596
  const checkpoint = {
27332
27597
  id: crypto$2.randomUUID(),
27333
27598
  name: name?.trim() || `Checkpoint ${this.state.checkpoints.length + 1}`,
27334
27599
  createdAt: (/* @__PURE__ */ new Date()).toISOString(),
27335
27600
  note: note?.trim() || void 0,
27336
- snapshot
27601
+ snapshot: snapshot2
27337
27602
  };
27338
27603
  this.state.checkpoints = [...this.state.checkpoints, checkpoint].slice(
27339
27604
  -20
@@ -27367,26 +27632,26 @@ class AgentRuntime {
27367
27632
  return { actionName: latest.actionName, capturedAt: latest.capturedAt };
27368
27633
  }
27369
27634
  undoLastAction() {
27370
- const snapshot = this.undoSnapshots.at(-1);
27371
- if (!snapshot) return null;
27635
+ const snapshot2 = this.undoSnapshots.at(-1);
27636
+ if (!snapshot2) return null;
27372
27637
  try {
27373
- this.tabManager.restoreSession(snapshot.snapshot);
27638
+ this.tabManager.restoreSession(snapshot2.snapshot);
27374
27639
  this.undoSnapshots.pop();
27375
27640
  } catch (error) {
27376
27641
  logger$3.error("Failed to restore undo snapshot", error);
27377
27642
  return null;
27378
27643
  }
27379
- this.captureSession(`Undid ${snapshot.actionName}`);
27380
- return snapshot.actionName;
27644
+ this.captureSession(`Undid ${snapshot2.actionName}`);
27645
+ return snapshot2.actionName;
27381
27646
  }
27382
27647
  captureSession(note) {
27383
- const snapshot = this.tabManager.snapshotSession(note);
27384
- this.state.session = snapshot;
27648
+ const snapshot2 = this.tabManager.snapshotSession(note);
27649
+ this.state.session = snapshot2;
27385
27650
  this.emit();
27386
- return clone(snapshot);
27651
+ return clone(snapshot2);
27387
27652
  }
27388
- restoreSession(snapshot) {
27389
- const target = snapshot || this.state.session;
27653
+ restoreSession(snapshot2) {
27654
+ const target = snapshot2 || this.state.session;
27390
27655
  if (!target) {
27391
27656
  return this.captureSession("No saved session to restore");
27392
27657
  }
@@ -27528,6 +27793,7 @@ ${progress}
27528
27793
  args = {},
27529
27794
  tabId = null,
27530
27795
  dangerous = false,
27796
+ requiresApproval = false,
27531
27797
  undoable,
27532
27798
  executor
27533
27799
  }) {
@@ -27547,7 +27813,7 @@ ${progress}
27547
27813
  streamId: transcriptStreamId,
27548
27814
  mode: "replace"
27549
27815
  });
27550
- const approvalReason = this.getApprovalReason(dangerous);
27816
+ const approvalReason = this.getApprovalReason(dangerous, requiresApproval);
27551
27817
  if (approvalReason) {
27552
27818
  this.publishTranscript({
27553
27819
  source,
@@ -27625,8 +27891,8 @@ ${progress}
27625
27891
  capturedAt: (/* @__PURE__ */ new Date()).toISOString()
27626
27892
  };
27627
27893
  }
27628
- pushUndoSnapshot(snapshot) {
27629
- this.undoSnapshots = [...this.undoSnapshots, snapshot].slice(
27894
+ pushUndoSnapshot(snapshot2) {
27895
+ this.undoSnapshots = [...this.undoSnapshots, snapshot2].slice(
27630
27896
  -10
27631
27897
  );
27632
27898
  }
@@ -27770,10 +28036,13 @@ ${progress}
27770
28036
  )
27771
28037
  };
27772
28038
  }
27773
- getApprovalReason(dangerous) {
28039
+ getApprovalReason(dangerous, requiresApproval) {
27774
28040
  if (this.state.supervisor.paused) {
27775
28041
  return "Agent execution is paused";
27776
28042
  }
28043
+ if (requiresApproval) {
28044
+ return "Approval required: high-risk action";
28045
+ }
27777
28046
  if (this.state.supervisor.approvalMode === "manual") {
27778
28047
  return "Approval required: ask every time mode";
27779
28048
  }