From 9c6c4fd53bd6f692c97247fc154f6cbe8aee47dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Tue, 4 Aug 2026 13:36:24 +0700 Subject: [PATCH 01/48] test(desktop): specify safe local folder grant --- .../desktop/test/folder-grant-adapter.test.ts | 51 +++++++++++++++++++ .../test/folder-grant-contract.test.ts | 30 +++++++++++ 2 files changed, 81 insertions(+) create mode 100644 apps/desktop/test/folder-grant-adapter.test.ts create mode 100644 apps/desktop/test/folder-grant-contract.test.ts diff --git a/apps/desktop/test/folder-grant-adapter.test.ts b/apps/desktop/test/folder-grant-adapter.test.ts new file mode 100644 index 00000000..0808676a --- /dev/null +++ b/apps/desktop/test/folder-grant-adapter.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it, vi } from 'vitest'; +import { ElectronFolderGrantAdapter } from '../src/main/adapters/electron-folder-grant.adapter.ts'; + +describe('dogfood folder grant adapter', () => { + it('returns a bounded summary without exposing the selected path', async () => { + const adapter = new ElectronFolderGrantAdapter({ + dialog: { + showOpenDialog: vi.fn(() => + Promise.resolve({ canceled: false, filePaths: ['C:\\approved'] }), + ), + }, + readdir: vi.fn(() => + Promise.resolve([ + { isFile: () => true, isSymbolicLink: () => false }, + { isFile: () => false, isSymbolicLink: () => false }, + ]), + ), + now: () => new Date('2026-08-04T00:00:00.000Z'), + }); + + await expect(adapter.grantFolder()).resolves.toEqual({ + fileCount: 1, + lastScanAt: '2026-08-04T00:00:00.000Z', + status: 'granted', + }); + }); + + it('keeps the grant unavailable when selection is cancelled or scanning fails', async () => { + const cancelled = new ElectronFolderGrantAdapter({ + dialog: { showOpenDialog: vi.fn(() => Promise.resolve({ canceled: true, filePaths: [] })) }, + readdir: vi.fn(), + now: () => new Date('2026-08-04T00:00:00.000Z'), + }); + await expect(cancelled.grantFolder()).resolves.toEqual({ + fileCount: 0, + lastScanAt: null, + status: 'not-granted', + }); + + const unavailable = new ElectronFolderGrantAdapter({ + dialog: { showOpenDialog: vi.fn(() => Promise.reject(new Error('dialog unavailable'))) }, + readdir: vi.fn(), + now: () => new Date('2026-08-04T00:00:00.000Z'), + }); + await expect(unavailable.grantFolder()).resolves.toEqual({ + fileCount: 0, + lastScanAt: null, + status: 'not-granted', + }); + }); +}); diff --git a/apps/desktop/test/folder-grant-contract.test.ts b/apps/desktop/test/folder-grant-contract.test.ts new file mode 100644 index 00000000..20a27e92 --- /dev/null +++ b/apps/desktop/test/folder-grant-contract.test.ts @@ -0,0 +1,30 @@ +import { describe, expect, it } from 'vitest'; +import { parseFolderGrantState } from '../src/shared/desktop-contract-v1.ts'; + +describe('dogfood folder grant contract', () => { + it('accepts only bounded, content-free local folder state', () => { + expect( + parseFolderGrantState({ + fileCount: 3, + lastScanAt: '2026-08-04T00:00:00.000Z', + status: 'granted', + }), + ).toEqual({ + fileCount: 3, + lastScanAt: '2026-08-04T00:00:00.000Z', + status: 'granted', + }); + }); + + it('rejects paths, file names, extra fields, and unsafe counts', () => { + for (const value of [ + { fileCount: 1, lastScanAt: null, status: 'not-granted', path: 'C:\\secret' }, + { fileCount: 1, lastScanAt: null, status: 'not-granted', fileName: 'payroll.xlsx' }, + { fileCount: -1, lastScanAt: null, status: 'not-granted' }, + { fileCount: 10_001, lastScanAt: null, status: 'granted' }, + { fileCount: 1, lastScanAt: '2026-08-04T00:00:00Z', status: 'granted' }, + ]) { + expect(() => parseFolderGrantState(value)).toThrow('INVALID_FOLDER_GRANT'); + } + }); +}); From 23a5c462cfd06eee77e16f41817b9e5391f9cf76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Tue, 4 Aug 2026 13:37:56 +0700 Subject: [PATCH 02/48] feat(desktop): add content-free folder grant contract --- .../src/application/folder-grant.port.ts | 3 ++ .../desktop/src/shared/desktop-contract-v1.ts | 41 +++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 apps/desktop/src/application/folder-grant.port.ts diff --git a/apps/desktop/src/application/folder-grant.port.ts b/apps/desktop/src/application/folder-grant.port.ts new file mode 100644 index 00000000..d9840e0e --- /dev/null +++ b/apps/desktop/src/application/folder-grant.port.ts @@ -0,0 +1,3 @@ +export interface FolderGrantPort { + grantFolder(): Promise; +} diff --git a/apps/desktop/src/shared/desktop-contract-v1.ts b/apps/desktop/src/shared/desktop-contract-v1.ts index 71711d34..3805b119 100644 --- a/apps/desktop/src/shared/desktop-contract-v1.ts +++ b/apps/desktop/src/shared/desktop-contract-v1.ts @@ -1,6 +1,7 @@ export const DESKTOP_BRIDGE_GLOBAL = 'databreezeDesktop'; export const DESKTOP_IPC_CHANNELS = Object.freeze({ + folderGrant: 'desktop:v1:folder:grant', sessionGetSafeState: 'desktop:v1:session:get-safe-state', sidecarGetStatus: 'desktop:v1:sidecar:get-status', } as const); @@ -26,8 +27,19 @@ export interface SidecarSafeStatus { readonly engineVersion: string | null; } +export type FolderGrantStatus = 'not-granted' | 'granted'; + +export interface FolderGrantState { + readonly fileCount: number; + readonly lastScanAt: string | null; + readonly status: FolderGrantStatus; +} + export interface DesktopBridgeV1 { readonly v1: { + readonly folder: { + readonly grant: () => Promise; + }; readonly session: { readonly getSafeState: () => Promise; }; @@ -37,6 +49,35 @@ export interface DesktopBridgeV1 { }; } +export function parseFolderGrantState(value: unknown): FolderGrantState { + const record = exactDataRecord(value, ['fileCount', 'lastScanAt', 'status']); + if ( + typeof record.fileCount !== 'number' || + !Number.isSafeInteger(record.fileCount) || + record.fileCount < 0 || + record.fileCount > 10_000 + ) { + throw new Error('INVALID_FOLDER_GRANT'); + } + if (record.lastScanAt !== null) { + if ( + typeof record.lastScanAt !== 'string' || + !/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/.test(record.lastScanAt) + ) { + throw new Error('INVALID_FOLDER_GRANT'); + } + } + if (record.status !== 'not-granted' && record.status !== 'granted') + throw new Error('INVALID_FOLDER_GRANT'); + if (record.status === 'not-granted' && (record.fileCount !== 0 || record.lastScanAt !== null)) + throw new Error('INVALID_FOLDER_GRANT'); + return Object.freeze({ + fileCount: record.fileCount, + lastScanAt: record.lastScanAt, + status: record.status, + }); +} + const SAFE_RESULT_MAX_BYTES = 64 * 1024; function exactDataRecord( From 8458c046afd91539df1adf608ab2ca5d068e164b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Tue, 4 Aug 2026 13:38:35 +0700 Subject: [PATCH 03/48] feat(desktop): scan approved folder locally --- .../adapters/electron-folder-grant.adapter.ts | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 apps/desktop/src/main/adapters/electron-folder-grant.adapter.ts diff --git a/apps/desktop/src/main/adapters/electron-folder-grant.adapter.ts b/apps/desktop/src/main/adapters/electron-folder-grant.adapter.ts new file mode 100644 index 00000000..833e424c --- /dev/null +++ b/apps/desktop/src/main/adapters/electron-folder-grant.adapter.ts @@ -0,0 +1,68 @@ +import { readdir as readDirectory } from 'node:fs/promises'; + +import type { FolderGrantPort } from '../../application/folder-grant.port.ts'; +import { parseFolderGrantState, type FolderGrantState } from '../../shared/desktop-contract-v1.ts'; + +const MAX_FOLDER_FILES = 10_000; + +interface FolderDialogLike { + showOpenDialog(options: { + readonly properties: readonly ['openDirectory']; + }): Promise<{ readonly canceled: boolean; readonly filePaths: readonly string[] }>; +} + +interface FolderEntryLike { + isFile(): boolean; + isSymbolicLink(): boolean; +} + +type ReadDirectory = ( + folderPath: string, + options: { readonly withFileTypes: true }, +) => Promise; + +export interface ElectronFolderGrantAdapterInput { + readonly dialog: FolderDialogLike; + readonly now?: () => Date; + readonly readdir?: ReadDirectory; +} + +function notGranted(): FolderGrantState { + return parseFolderGrantState({ fileCount: 0, lastScanAt: null, status: 'not-granted' }); +} + +/** Selects and audits a local folder while returning no path or file names to the renderer. */ +export class ElectronFolderGrantAdapter implements FolderGrantPort { + readonly #dialog: FolderDialogLike; + readonly #now: () => Date; + readonly #readdir: ReadDirectory; + + public constructor({ + dialog, + now = () => new Date(), + readdir = readDirectory, + }: ElectronFolderGrantAdapterInput) { + this.#dialog = dialog; + this.#now = now; + this.#readdir = readdir; + } + + public async grantFolder(): Promise { + try { + const selection = await this.#dialog.showOpenDialog({ properties: ['openDirectory'] }); + const folderPath = selection.filePaths[0]; + if (selection.canceled || folderPath === undefined || folderPath.length === 0) + return notGranted(); + const entries = await this.#readdir(folderPath, { withFileTypes: true }); + const fileCount = entries.filter((entry) => entry.isFile() && !entry.isSymbolicLink()).length; + if (fileCount > MAX_FOLDER_FILES) return notGranted(); + return parseFolderGrantState({ + fileCount, + lastScanAt: this.#now().toISOString(), + status: 'granted', + }); + } catch { + return notGranted(); + } + } +} From 2d5fc4a42e5040b47b43b4a7a9a8ea00753a85f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Tue, 4 Aug 2026 13:39:02 +0700 Subject: [PATCH 04/48] feat(desktop): expose guarded folder grant IPC --- apps/desktop/src/main/index.ts | 5 ++++- apps/desktop/src/main/ipc-registry.ts | 16 +++++++++++++++- apps/desktop/src/preload/bridge-v1.ts | 9 ++++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 1e343c63..5e1df1f1 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -1,6 +1,7 @@ import { createRequire } from 'node:module'; import path from 'node:path'; -import { app, BrowserWindow, ipcMain, session } from 'electron'; +import { app, BrowserWindow, dialog, ipcMain, session } from 'electron'; +import { ElectronFolderGrantAdapter } from './adapters/electron-folder-grant.adapter.ts'; import { LockedLocalStateAdapter } from './adapters/locked-local-state.adapter.ts'; import { UnavailableSidecarAdapter } from './adapters/unavailable-sidecar.adapter.ts'; import { @@ -26,6 +27,7 @@ async function openDesktopWindow(): Promise { applicationVersion: app.getVersion(), locale: 'vi-VN', }); + const folderGrant = new ElectronFolderGrantAdapter({ dialog }); const sidecar = new UnavailableSidecarAdapter(); await createDesktopWindow({ @@ -36,6 +38,7 @@ async function openDesktopWindow(): Promise { expectedRendererUrl, getActiveWindow: () => activeWindow as unknown as DesktopWindowLike, ipcMain: ipcMain as unknown as DesktopIpcRegistrationInput['ipcMain'], + folderGrant, localState, sidecar, }); diff --git a/apps/desktop/src/main/ipc-registry.ts b/apps/desktop/src/main/ipc-registry.ts index e2dbbfdb..7d603638 100644 --- a/apps/desktop/src/main/ipc-registry.ts +++ b/apps/desktop/src/main/ipc-registry.ts @@ -1,8 +1,10 @@ import type { LocalStatePort } from '../application/local-state.port.ts'; +import type { FolderGrantPort } from '../application/folder-grant.port.ts'; import type { SidecarLifecyclePort } from '../application/sidecar-lifecycle.port.ts'; import { DESKTOP_IPC_CHANNELS, parseDesktopSafeState, + parseFolderGrantState, parseSidecarSafeStatus, type DesktopIpcChannel, } from '../shared/desktop-contract-v1.ts'; @@ -36,6 +38,7 @@ export interface DesktopIpcRegistrationInput { readonly expectedRendererUrl: string; readonly getActiveWindow: () => WindowLike | null; readonly ipcMain: IpcMainLike; + readonly folderGrant?: FolderGrantPort; readonly localState: LocalStatePort; readonly sidecar: SidecarLifecyclePort; } @@ -99,6 +102,7 @@ export function registerDesktopIpcV1({ expectedRendererUrl, getActiveWindow, ipcMain, + folderGrant, localState, sidecar, }: DesktopIpcRegistrationInput): () => void { @@ -106,7 +110,17 @@ export function registerDesktopIpcV1({ if (previous !== undefined) previous.active = false; for (const channel of Object.values(DESKTOP_IPC_CHANNELS)) ipcMain.removeHandler(channel); - const handlers: Record = { + const handlers: Partial> = { + ...(folderGrant === undefined + ? {} + : { + [DESKTOP_IPC_CHANNELS.folderGrant]: guardedHandler( + expectedRendererUrl, + getActiveWindow, + () => folderGrant.grantFolder(), + parseFolderGrantState, + ), + }), [DESKTOP_IPC_CHANNELS.sessionGetSafeState]: guardedHandler( expectedRendererUrl, getActiveWindow, diff --git a/apps/desktop/src/preload/bridge-v1.ts b/apps/desktop/src/preload/bridge-v1.ts index 1e87af80..72df103c 100644 --- a/apps/desktop/src/preload/bridge-v1.ts +++ b/apps/desktop/src/preload/bridge-v1.ts @@ -1,6 +1,7 @@ import { DESKTOP_IPC_CHANNELS, parseDesktopSafeState, + parseFolderGrantState, parseSidecarSafeStatus, type DesktopBridgeV1, type DesktopIpcChannel, @@ -19,11 +20,17 @@ export function createDesktopBridgeV1(invoke: DesktopInvoke): DesktopBridgeV1 { return parseDesktopSafeState(await invoke(DESKTOP_IPC_CHANNELS.sessionGetSafeState)); }, }); + const folder = Object.freeze({ + grant: async (...argumentsList: unknown[]) => { + rejectUnexpectedArguments(argumentsList); + return parseFolderGrantState(await invoke(DESKTOP_IPC_CHANNELS.folderGrant)); + }, + }); const sidecar = Object.freeze({ getStatus: async (...argumentsList: unknown[]) => { rejectUnexpectedArguments(argumentsList); return parseSidecarSafeStatus(await invoke(DESKTOP_IPC_CHANNELS.sidecarGetStatus)); }, }); - return Object.freeze({ v1: Object.freeze({ session, sidecar }) }); + return Object.freeze({ v1: Object.freeze({ folder, session, sidecar }) }); } From 8774191442a802a3152470908cd34b5c5315e690 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Tue, 4 Aug 2026 13:39:34 +0700 Subject: [PATCH 05/48] test(desktop): cover folder grant bridge and IPC --- .../desktop/test/security-bridge-contract.test.ts | 15 +++++++++++++-- apps/desktop/test/security-ipc-registry.test.ts | 15 ++++++++++++++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/apps/desktop/test/security-bridge-contract.test.ts b/apps/desktop/test/security-bridge-contract.test.ts index 431464be..88e6bf69 100644 --- a/apps/desktop/test/security-bridge-contract.test.ts +++ b/apps/desktop/test/security-bridge-contract.test.ts @@ -14,18 +14,22 @@ describe('DSK-002 preload bridge', () => { enrollmentState: 'not-enrolled', locale: 'vi-VN', } - : { engineVersion: null, lifecycle: 'not-installed', protocolVersion: null }, + : channel === DESKTOP_IPC_CHANNELS.folderGrant + ? { fileCount: 0, lastScanAt: null, status: 'not-granted' } + : { engineVersion: null, lifecycle: 'not-installed', protocolVersion: null }, ), ); const bridge = createDesktopBridgeV1(invoke); expect(Object.keys(bridge)).toEqual(['v1']); - expect(Object.keys(bridge.v1)).toEqual(['session', 'sidecar']); + expect(Object.keys(bridge.v1)).toEqual(['folder', 'session', 'sidecar']); + expect(Object.keys(bridge.v1.folder)).toEqual(['grant']); expect(Object.keys(bridge.v1.session)).toEqual(['getSafeState']); expect(Object.keys(bridge.v1.sidecar)).toEqual(['getStatus']); expect(Object.isFrozen(bridge)).toBe(true); expect(Object.isFrozen(bridge.v1)).toBe(true); expect(Object.isFrozen(bridge.v1.session)).toBe(true); + expect(Object.isFrozen(bridge.v1.folder)).toBe(true); expect(bridge).not.toHaveProperty('invoke'); expect(bridge).not.toHaveProperty('send'); expect(bridge).not.toHaveProperty('filesystem'); @@ -38,9 +42,15 @@ describe('DSK-002 preload bridge', () => { await expect(bridge.v1.sidecar.getStatus()).resolves.toMatchObject({ lifecycle: 'not-installed', }); + await expect(bridge.v1.folder.grant()).resolves.toEqual({ + fileCount: 0, + lastScanAt: null, + status: 'not-granted', + }); expect(invoke.mock.calls).toEqual([ [DESKTOP_IPC_CHANNELS.sessionGetSafeState], [DESKTOP_IPC_CHANNELS.sidecarGetStatus], + [DESKTOP_IPC_CHANNELS.folderGrant], ]); }); @@ -61,6 +71,7 @@ describe('DSK-002 preload bridge', () => { 'x'.repeat(70_000), ]; const methods = [ + bridge.v1.folder.grant as (...args: unknown[]) => Promise, bridge.v1.session.getSafeState as (...args: unknown[]) => Promise, bridge.v1.sidecar.getStatus as (...args: unknown[]) => Promise, ]; diff --git a/apps/desktop/test/security-ipc-registry.test.ts b/apps/desktop/test/security-ipc-registry.test.ts index c5cc0f10..ddd8f7e3 100644 --- a/apps/desktop/test/security-ipc-registry.test.ts +++ b/apps/desktop/test/security-ipc-registry.test.ts @@ -54,15 +54,21 @@ function register(overrides: Record = {}) { }), ), }; + const folderGrant = { + grantFolder: vi.fn<() => Promise>(() => + Promise.resolve({ fileCount: 2, lastScanAt: '2026-08-04T00:00:00.000Z', status: 'granted' }), + ), + }; const dispose = registerDesktopIpcV1({ expectedRendererUrl: 'file:///trusted/index.html', getActiveWindow: () => context.activeWindow, ipcMain, + folderGrant, localState, sidecar, ...overrides, }); - return { ...context, dispose, ipcMain, localState, sidecar }; + return { ...context, dispose, folderGrant, ipcMain, localState, sidecar }; } describe('DSK-002 guarded IPC registry', () => { @@ -85,6 +91,13 @@ describe('DSK-002 guarded IPC registry', () => { lifecycle: 'not-installed', protocolVersion: null, }); + await expect( + harness.ipcMain.invoke(DESKTOP_IPC_CHANNELS.folderGrant, harness.event), + ).resolves.toEqual({ + fileCount: 2, + lastScanAt: '2026-08-04T00:00:00.000Z', + status: 'granted', + }); }); it('fails closed for unknown channels and any malformed or oversized arguments', async () => { From f1259ffff2338d9a6878b638da29236d7fa3cb53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Acharn=C3=A9?= Date: Tue, 4 Aug 2026 13:40:43 +0700 Subject: [PATCH 06/48] feat(desktop): add approved-folder control to shell --- apps/desktop/src/renderer/app.tsx | 42 +++++++++++++++++++ apps/desktop/src/renderer/styles.css | 42 ++++++++++++++++++- .../test/boundary-renderer-shell.test.tsx | 8 ++++ 3 files changed, 91 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/app.tsx b/apps/desktop/src/renderer/app.tsx index 9b9d4fa8..f0302f0a 100644 --- a/apps/desktop/src/renderer/app.tsx +++ b/apps/desktop/src/renderer/app.tsx @@ -3,6 +3,7 @@ import wordmarkUrl from '@databreeze/design-tokens/brand/generated/web/navigatio import type { DesktopLocale, DesktopSafeState, + FolderGrantState, SidecarSafeStatus, } from '../shared/desktop-contract-v1.ts'; @@ -18,6 +19,11 @@ const messages = { privacy: 'Không có đường dẫn hoặc nội dung tệp nào được gửi tới giao diện này.', privacyTitle: 'Ranh giới riêng tư', version: 'Phiên bản ứng dụng', + folderTitle: 'Thư mục được cấp quyền', + folderPick: 'Chọn thư mục để kiểm tra', + folderGranted: 'Đã cấp quyền cục bộ', + folderNotGranted: 'Chưa cấp quyền', + folderFiles: 'Tệp đã phát hiện', }, en: { agentDetail: 'The agent shows safe status only and contains no workspace data.', @@ -30,6 +36,11 @@ const messages = { privacy: 'No file path or file content is sent to this interface.', privacyTitle: 'Privacy boundary', version: 'Application version', + folderTitle: 'Approved folder', + folderPick: 'Choose a folder to audit', + folderGranted: 'Local permission granted', + folderNotGranted: 'No folder granted', + folderFiles: 'Files discovered', }, } as const; @@ -45,13 +56,29 @@ const initialSidecar: SidecarSafeStatus = { lifecycle: 'not-installed', protocolVersion: null, }; +const initialFolder: FolderGrantState = { + fileCount: 0, + lastScanAt: null, + status: 'not-granted', +}; export function DesktopApp() { const [locale, setLocale] = useState('vi-VN'); const [safeState, setSafeState] = useState(initialState); const [sidecarStatus, setSidecarStatus] = useState(initialSidecar); + const [folderState, setFolderState] = useState(initialFolder); const copy = messages[locale]; + async function grantFolder(): Promise { + const folder = window.databreezeDesktop?.v1.folder; + if (folder === undefined) return; + try { + setFolderState(await folder.grant()); + } catch { + setFolderState(initialFolder); + } + } + useEffect(() => { let active = true; const bridge = window.databreezeDesktop; @@ -127,6 +154,21 @@ export function DesktopApp() { +
+
+

{copy.folderTitle}

+

{folderState.status === 'granted' ? copy.folderGranted : copy.folderNotGranted}

+
+
+ + {copy.folderFiles}: {new Intl.NumberFormat(locale).format(folderState.fileCount)} + + +
+
+