From cdb0719f25b5c911400d72a9768ff54c97b81a8a Mon Sep 17 00:00:00 2001 From: ParsaKhaz Date: Thu, 14 May 2026 12:54:32 -0700 Subject: [PATCH 1/3] test: harden daemon bridge boundary checks --- main/src/core/importBoundary.test.ts | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/main/src/core/importBoundary.test.ts b/main/src/core/importBoundary.test.ts index 4881387..c22ea43 100644 --- a/main/src/core/importBoundary.test.ts +++ b/main/src/core/importBoundary.test.ts @@ -46,11 +46,28 @@ describe('daemon/client import boundary', () => { }); it('keeps the daemon server free of Electron bootstrap imports', () => { - const source = readMainSrcFile('daemon/server.ts'); + const daemonBoundaryFiles = [ + 'daemon/server.ts', + 'ipc/daemon.ts', + ]; + + for (const relativePath of daemonBoundaryFiles) { + const source = readMainSrcFile(relativePath); + + expect(source, relativePath).not.toContain("from 'electron'"); + expect(source, relativePath).not.toContain('mainWindow'); + expect(source, relativePath).not.toMatch(/from ['"](?:\.\.\/)+(?:index)['"]/); + expect(source, relativePath).not.toMatch(/from ['"](?:\.\.\/)+(?:index)\.ts['"]/); + } + }); + + it('routes daemon-owned preload invokes through the shared bridge helper', () => { + const source = readMainSrcFile('preload.ts'); - expect(source).not.toContain("from 'electron'"); - expect(source).not.toContain('mainWindow'); - expect(source).not.toMatch(/from ['"](?:\.\.\/)+(?:index)['"]/); - expect(source).not.toMatch(/from ['"](?:\.\.\/)+(?:index)\.ts['"]/); + expect(source).toContain('isDaemonOwnedChannel'); + expect(source).toContain("ipcRenderer.invoke('daemon:invoke', channel, ...args)"); + expect(source).not.toMatch( + /ipcRenderer\.invoke\('(sessions:|projects:|folders:|prompts:|resource-monitor:|panels:|terminal:|logs:|git:(cancel-status-for-project|clone-repo|commit|execute-project|file-status|get-github-remote|restore|revert)|file:(copy|delete|duplicate|exists|getPath|list|move|read|read-binary|read-project|readAtRevision|rename|resolveAbsolutePath|search|write|write-binary|write-project))/, + ); }); }); From f964621eb1baa522712bb559af6712342d1d3859 Mon Sep 17 00:00:00 2001 From: ParsaKhaz Date: Thu, 14 May 2026 13:29:58 -0700 Subject: [PATCH 2/3] fix: restore live Electron daemon bridge boot --- main/src/core/importBoundary.test.ts | 5 ++- main/src/daemon/commandRegistry.ts | 2 +- main/src/daemon/daemonChannels.ts | 57 +++++++++++++++++++++++++ main/src/daemon/socketFraming.test.ts | 2 +- main/src/index.ts | 9 ++-- main/src/ipc/daemon.ts | 2 +- main/src/preload.ts | 61 ++++++++++++++++++++++++++- shared/types/daemon.ts | 58 ------------------------- 8 files changed, 128 insertions(+), 68 deletions(-) create mode 100644 main/src/daemon/daemonChannels.ts diff --git a/main/src/core/importBoundary.test.ts b/main/src/core/importBoundary.test.ts index c22ea43..91284ba 100644 --- a/main/src/core/importBoundary.test.ts +++ b/main/src/core/importBoundary.test.ts @@ -61,10 +61,13 @@ describe('daemon/client import boundary', () => { } }); - it('routes daemon-owned preload invokes through the shared bridge helper', () => { + it('routes daemon-owned preload invokes through the runtime-safe bridge helper', () => { const source = readMainSrcFile('preload.ts'); + expect(source).toContain('function isDaemonOwnedChannel'); expect(source).toContain('isDaemonOwnedChannel'); + expect(source).not.toContain("../../shared/types/daemon"); + expect(source).not.toContain("from './daemon/daemonChannels'"); expect(source).toContain("ipcRenderer.invoke('daemon:invoke', channel, ...args)"); expect(source).not.toMatch( /ipcRenderer\.invoke\('(sessions:|projects:|folders:|prompts:|resource-monitor:|panels:|terminal:|logs:|git:(cancel-status-for-project|clone-repo|commit|execute-project|file-status|get-github-remote|restore|revert)|file:(copy|delete|duplicate|exists|getPath|list|move|read|read-binary|read-project|readAtRevision|rename|resolveAbsolutePath|search|write|write-binary|write-project))/, diff --git a/main/src/daemon/commandRegistry.ts b/main/src/daemon/commandRegistry.ts index 2de857f..b34062c 100644 --- a/main/src/daemon/commandRegistry.ts +++ b/main/src/daemon/commandRegistry.ts @@ -1,4 +1,4 @@ -import { isDaemonOwnedChannel } from '../../../shared/types/daemon'; +import { isDaemonOwnedChannel } from './daemonChannels'; export type PaneCommandHandler = ( ...args: TArgs diff --git a/main/src/daemon/daemonChannels.ts b/main/src/daemon/daemonChannels.ts new file mode 100644 index 0000000..e13ae65 --- /dev/null +++ b/main/src/daemon/daemonChannels.ts @@ -0,0 +1,57 @@ +const DAEMON_OWNED_CHANNEL_PREFIXES = [ + 'folders:', + 'logs:', + 'panels:', + 'projects:', + 'prompts:', + 'resource-monitor:', + 'sessions:', + 'terminal:', +] as const; + +const DAEMON_OWNED_EXACT_CHANNELS = [ + 'git:cancel-status-for-project', + 'git:clone-repo', + 'git:commit', + 'git:execute-project', + 'git:file-status', + 'git:get-github-remote', + 'git:restore', + 'git:revert', + 'file:copy', + 'file:delete', + 'file:duplicate', + 'file:exists', + 'file:getPath', + 'file:list', + 'file:move', + 'file:read', + 'file:read-binary', + 'file:read-project', + 'file:readAtRevision', + 'file:rename', + 'file:resolveAbsolutePath', + 'file:search', + 'file:write', + 'file:write-binary', + 'file:write-project', +] as const; + +const ELECTRON_ADAPTER_ONLY_CHANNELS = new Set([ + 'file:showInFolder', + 'sessions:open-ide', + 'sessions:set-active-session', + 'terminal:clipboard-paste-image', +]); + +export function isDaemonOwnedChannel(channel: string): boolean { + if (ELECTRON_ADAPTER_ONLY_CHANNELS.has(channel)) { + return false; + } + + if (DAEMON_OWNED_EXACT_CHANNELS.includes(channel as (typeof DAEMON_OWNED_EXACT_CHANNELS)[number])) { + return true; + } + + return DAEMON_OWNED_CHANNEL_PREFIXES.some((prefix) => channel.startsWith(prefix)); +} diff --git a/main/src/daemon/socketFraming.test.ts b/main/src/daemon/socketFraming.test.ts index eb9a480..181e4e9 100644 --- a/main/src/daemon/socketFraming.test.ts +++ b/main/src/daemon/socketFraming.test.ts @@ -4,7 +4,6 @@ import { PaneDaemonFrameDecoder, } from './socketFraming'; import { - isDaemonOwnedChannel, isPaneDaemonEventFrame, isPaneDaemonFrame, isPaneDaemonRequestFrame, @@ -13,6 +12,7 @@ import { type PaneDaemonRequestFrame, type PaneDaemonResponseFrame, } from '../../../shared/types/daemon'; +import { isDaemonOwnedChannel } from './daemonChannels'; describe('Pane daemon framing', () => { it('encodes frames as newline-delimited JSON', () => { diff --git a/main/src/index.ts b/main/src/index.ts index f172056..9189da9 100644 --- a/main/src/index.ts +++ b/main/src/index.ts @@ -952,11 +952,6 @@ async function createWindow() { resourceMonitorService.handleVisibilityChange(true); }); - // Pull-path query so the renderer can get the authoritative focus state on - // mount without waiting for the next focus-change event. Default to true - // (focused) if mainWindow is somehow null at call time. - ipcMain.handle('window:is-focused', () => mainWindow?.isFocused() ?? true); - mainWindow.on('restore', () => { // Don't assume restore = focused. The OS will fire 'focus' if/when the user // actually focuses the window; that is what restarts git/resource work. @@ -1212,6 +1207,10 @@ app.whenReady().then(async () => { await initializeServices(); console.log('[Main] Services initialized, creating window...'); + // Register before any renderer loads. useNotifications pulls this on mount + // and will race a late registration inside createWindow/loadURL. + ipcMain.handle('window:is-focused', () => mainWindow?.isFocused() ?? true); + // Start the ptyHost supervisor before the window opens so the renderer's // preload listener for 'ptyHost-port' has a port to receive when the window // finishes loading. Gated on the `usePtyHost` setting: when off (default), diff --git a/main/src/ipc/daemon.ts b/main/src/ipc/daemon.ts index d9e7f7d..a28f7d3 100644 --- a/main/src/ipc/daemon.ts +++ b/main/src/ipc/daemon.ts @@ -1,4 +1,4 @@ -import { isDaemonOwnedChannel } from '../../../shared/types/daemon'; +import { isDaemonOwnedChannel } from '../daemon/daemonChannels'; import type { PaneCommandRegistry } from '../daemon/commandRegistry'; interface IpcMainHandleLike { diff --git a/main/src/preload.ts b/main/src/preload.ts index a7198ab..fcde800 100644 --- a/main/src/preload.ts +++ b/main/src/preload.ts @@ -3,7 +3,6 @@ import type { CreateSessionRequest, Session } from './types/session'; import type { AppConfig, UpdateConfigRequest } from './types/config'; import type { CreateProjectRequest, UpdateProjectRequest, Project } from '../../frontend/src/types/project'; import type { ToolPanel } from '../../shared/types/panels'; -import { isDaemonOwnedChannel } from '../../shared/types/daemon'; interface LogEntry { timestamp: string; @@ -88,6 +87,66 @@ interface UpdaterInfo { size?: number; } +const DAEMON_OWNED_CHANNEL_PREFIXES = [ + 'folders:', + 'logs:', + 'panels:', + 'projects:', + 'prompts:', + 'resource-monitor:', + 'sessions:', + 'terminal:', +] as const; + +const DAEMON_OWNED_EXACT_CHANNELS = [ + 'git:cancel-status-for-project', + 'git:clone-repo', + 'git:commit', + 'git:execute-project', + 'git:file-status', + 'git:get-github-remote', + 'git:restore', + 'git:revert', + 'file:copy', + 'file:delete', + 'file:duplicate', + 'file:exists', + 'file:getPath', + 'file:list', + 'file:move', + 'file:read', + 'file:read-binary', + 'file:read-project', + 'file:readAtRevision', + 'file:rename', + 'file:resolveAbsolutePath', + 'file:search', + 'file:write', + 'file:write-binary', + 'file:write-project', +] as const; + +const ELECTRON_ADAPTER_ONLY_CHANNELS = new Set([ + 'file:showInFolder', + 'sessions:open-ide', + 'sessions:set-active-session', + 'terminal:clipboard-paste-image', +]); + +// Sandboxed Electron preload scripts cannot reliably require local runtime +// modules, so the daemon-owned channel classifier stays inline here. +function isDaemonOwnedChannel(channel: string): boolean { + if (ELECTRON_ADAPTER_ONLY_CHANNELS.has(channel)) { + return false; + } + + if (DAEMON_OWNED_EXACT_CHANNELS.includes(channel as (typeof DAEMON_OWNED_EXACT_CHANNELS)[number])) { + return true; + } + + return DAEMON_OWNED_CHANNEL_PREFIXES.some((prefix) => channel.startsWith(prefix)); +} + // Increase max listeners for ipcRenderer to prevent warnings when many components listen to events ipcRenderer.setMaxListeners(50); diff --git a/shared/types/daemon.ts b/shared/types/daemon.ts index fb6e7cc..99b7349 100644 --- a/shared/types/daemon.ts +++ b/shared/types/daemon.ts @@ -46,64 +46,6 @@ interface PaneDaemonResponseFrameCandidate { error?: unknown; } -const DAEMON_OWNED_CHANNEL_PREFIXES = [ - 'folders:', - 'logs:', - 'panels:', - 'projects:', - 'prompts:', - 'resource-monitor:', - 'sessions:', - 'terminal:', -] as const; - -const DAEMON_OWNED_EXACT_CHANNELS = [ - 'git:cancel-status-for-project', - 'git:clone-repo', - 'git:commit', - 'git:execute-project', - 'git:file-status', - 'git:get-github-remote', - 'git:restore', - 'git:revert', - 'file:copy', - 'file:delete', - 'file:duplicate', - 'file:exists', - 'file:getPath', - 'file:list', - 'file:move', - 'file:read', - 'file:read-binary', - 'file:read-project', - 'file:readAtRevision', - 'file:rename', - 'file:resolveAbsolutePath', - 'file:search', - 'file:write', - 'file:write-binary', - 'file:write-project', -] as const; - -const ELECTRON_ADAPTER_ONLY_CHANNELS = new Set([ - 'file:showInFolder', - 'sessions:open-ide', - 'sessions:set-active-session', - 'terminal:clipboard-paste-image', -]); - -export function isDaemonOwnedChannel(channel: string): boolean { - if (ELECTRON_ADAPTER_ONLY_CHANNELS.has(channel)) { - return false; - } - - if (DAEMON_OWNED_EXACT_CHANNELS.includes(channel as (typeof DAEMON_OWNED_EXACT_CHANNELS)[number])) { - return true; - } - - return DAEMON_OWNED_CHANNEL_PREFIXES.some((prefix) => channel.startsWith(prefix)); -} - export function isPaneDaemonRequestFrame(frame: unknown): frame is PaneDaemonRequestFrame { if (typeof frame !== 'object' || frame === null) { return false; From d8214a77c9c465040717df2b200d0d6b0f34b9c3 Mon Sep 17 00:00:00 2001 From: ParsaKhaz Date: Thu, 14 May 2026 13:33:18 -0700 Subject: [PATCH 3/3] test: lock preload daemon channel parity --- main/src/core/importBoundary.test.ts | 21 ++++++++++ main/src/daemon/daemonChannels.ts | 63 +++------------------------- shared/types/daemon.ts | 60 ++++++++++++++++++++++++++ 3 files changed, 87 insertions(+), 57 deletions(-) diff --git a/main/src/core/importBoundary.test.ts b/main/src/core/importBoundary.test.ts index 91284ba..cbf39df 100644 --- a/main/src/core/importBoundary.test.ts +++ b/main/src/core/importBoundary.test.ts @@ -1,6 +1,11 @@ import fs from 'fs'; import path from 'path'; import { describe, expect, it } from 'vitest'; +import { + DAEMON_OWNED_CHANNEL_PREFIXES, + DAEMON_OWNED_EXACT_CHANNELS, + ELECTRON_ADAPTER_ONLY_CHANNELS, +} from '../../../shared/types/daemon'; const MAIN_SRC_ROOT = path.resolve(process.cwd(), 'src'); @@ -73,4 +78,20 @@ describe('daemon/client import boundary', () => { /ipcRenderer\.invoke\('(sessions:|projects:|folders:|prompts:|resource-monitor:|panels:|terminal:|logs:|git:(cancel-status-for-project|clone-repo|commit|execute-project|file-status|get-github-remote|restore|revert)|file:(copy|delete|duplicate|exists|getPath|list|move|read|read-binary|read-project|readAtRevision|rename|resolveAbsolutePath|search|write|write-binary|write-project))/, ); }); + + it('keeps preload channel ownership literals aligned with the shared daemon contract', () => { + const source = readMainSrcFile('preload.ts'); + + for (const prefix of DAEMON_OWNED_CHANNEL_PREFIXES) { + expect(source).toContain(`'${prefix}'`); + } + + for (const channel of DAEMON_OWNED_EXACT_CHANNELS) { + expect(source).toContain(`'${channel}'`); + } + + for (const channel of ELECTRON_ADAPTER_ONLY_CHANNELS) { + expect(source).toContain(`'${channel}'`); + } + }); }); diff --git a/main/src/daemon/daemonChannels.ts b/main/src/daemon/daemonChannels.ts index e13ae65..572c221 100644 --- a/main/src/daemon/daemonChannels.ts +++ b/main/src/daemon/daemonChannels.ts @@ -1,57 +1,6 @@ -const DAEMON_OWNED_CHANNEL_PREFIXES = [ - 'folders:', - 'logs:', - 'panels:', - 'projects:', - 'prompts:', - 'resource-monitor:', - 'sessions:', - 'terminal:', -] as const; - -const DAEMON_OWNED_EXACT_CHANNELS = [ - 'git:cancel-status-for-project', - 'git:clone-repo', - 'git:commit', - 'git:execute-project', - 'git:file-status', - 'git:get-github-remote', - 'git:restore', - 'git:revert', - 'file:copy', - 'file:delete', - 'file:duplicate', - 'file:exists', - 'file:getPath', - 'file:list', - 'file:move', - 'file:read', - 'file:read-binary', - 'file:read-project', - 'file:readAtRevision', - 'file:rename', - 'file:resolveAbsolutePath', - 'file:search', - 'file:write', - 'file:write-binary', - 'file:write-project', -] as const; - -const ELECTRON_ADAPTER_ONLY_CHANNELS = new Set([ - 'file:showInFolder', - 'sessions:open-ide', - 'sessions:set-active-session', - 'terminal:clipboard-paste-image', -]); - -export function isDaemonOwnedChannel(channel: string): boolean { - if (ELECTRON_ADAPTER_ONLY_CHANNELS.has(channel)) { - return false; - } - - if (DAEMON_OWNED_EXACT_CHANNELS.includes(channel as (typeof DAEMON_OWNED_EXACT_CHANNELS)[number])) { - return true; - } - - return DAEMON_OWNED_CHANNEL_PREFIXES.some((prefix) => channel.startsWith(prefix)); -} +export { + DAEMON_OWNED_CHANNEL_PREFIXES, + DAEMON_OWNED_EXACT_CHANNELS, + ELECTRON_ADAPTER_ONLY_CHANNELS, + isDaemonOwnedChannel, +} from '../../../shared/types/daemon'; diff --git a/shared/types/daemon.ts b/shared/types/daemon.ts index 99b7349..e91ca3a 100644 --- a/shared/types/daemon.ts +++ b/shared/types/daemon.ts @@ -46,6 +46,66 @@ interface PaneDaemonResponseFrameCandidate { error?: unknown; } +export const DAEMON_OWNED_CHANNEL_PREFIXES = [ + 'folders:', + 'logs:', + 'panels:', + 'projects:', + 'prompts:', + 'resource-monitor:', + 'sessions:', + 'terminal:', +] as const; + +export const DAEMON_OWNED_EXACT_CHANNELS = [ + 'git:cancel-status-for-project', + 'git:clone-repo', + 'git:commit', + 'git:execute-project', + 'git:file-status', + 'git:get-github-remote', + 'git:restore', + 'git:revert', + 'file:copy', + 'file:delete', + 'file:duplicate', + 'file:exists', + 'file:getPath', + 'file:list', + 'file:move', + 'file:read', + 'file:read-binary', + 'file:read-project', + 'file:readAtRevision', + 'file:rename', + 'file:resolveAbsolutePath', + 'file:search', + 'file:write', + 'file:write-binary', + 'file:write-project', +] as const; + +export const ELECTRON_ADAPTER_ONLY_CHANNELS = [ + 'file:showInFolder', + 'sessions:open-ide', + 'sessions:set-active-session', + 'terminal:clipboard-paste-image', +] as const; + +const ELECTRON_ADAPTER_ONLY_CHANNEL_SET = new Set(ELECTRON_ADAPTER_ONLY_CHANNELS); + +export function isDaemonOwnedChannel(channel: string): boolean { + if (ELECTRON_ADAPTER_ONLY_CHANNEL_SET.has(channel)) { + return false; + } + + if (DAEMON_OWNED_EXACT_CHANNELS.includes(channel as (typeof DAEMON_OWNED_EXACT_CHANNELS)[number])) { + return true; + } + + return DAEMON_OWNED_CHANNEL_PREFIXES.some((prefix) => channel.startsWith(prefix)); +} + export function isPaneDaemonRequestFrame(frame: unknown): frame is PaneDaemonRequestFrame { if (typeof frame !== 'object' || frame === null) { return false;