Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
9c6c4fd
test(desktop): specify safe local folder grant
BeforeLights Aug 4, 2026
23a5c46
feat(desktop): add content-free folder grant contract
BeforeLights Aug 4, 2026
8458c04
feat(desktop): scan approved folder locally
BeforeLights Aug 4, 2026
2d5fc4a
feat(desktop): expose guarded folder grant IPC
BeforeLights Aug 4, 2026
8774191
test(desktop): cover folder grant bridge and IPC
BeforeLights Aug 4, 2026
f1259ff
feat(desktop): add approved-folder control to shell
BeforeLights Aug 4, 2026
ae16c24
fix(iae): gate evidence resolution on clean scans
BeforeLights Aug 4, 2026
c9b96c3
fix(iae): reject grants for non-clean artifacts
BeforeLights Aug 4, 2026
044f161
test(engine): specify spreadsheet auditor action dispatch
BeforeLights Aug 4, 2026
5664ab3
feat(engine): register typed spreadsheet auditor action
BeforeLights Aug 4, 2026
3d01785
style(engine): format auditor dispatch boundary
BeforeLights Aug 4, 2026
f11ed24
test(engine): cover spreadsheet auditor rpc envelope
BeforeLights Aug 4, 2026
d7277a7
docs(engine): describe spreadsheet auditor action boundary
BeforeLights Aug 4, 2026
f235edd
test(domain): specify spreadsheet audit run admission contract
BeforeLights Aug 4, 2026
3b35b04
feat(domain): add spreadsheet audit run contract
BeforeLights Aug 4, 2026
516a41d
test(api): specify idempotent spreadsheet audit run admission
BeforeLights Aug 4, 2026
d827aec
feat(api): add idempotent spreadsheet audit run service
BeforeLights Aug 4, 2026
11d7337
test(api): specify spreadsheet audit run admission controller
BeforeLights Aug 4, 2026
0d64e02
feat(api): expose spreadsheet audit run admission endpoint
BeforeLights Aug 4, 2026
5f508e1
docs(sa): record spreadsheet audit run admission evidence
BeforeLights Aug 4, 2026
48b69a3
fix(api): type spreadsheet audit run response assertions
BeforeLights Aug 4, 2026
63ba6ee
test(iae): specify local artifact registration HTTP path
BeforeLights Aug 4, 2026
7d171bd
feat(iae): register local artifacts through content-free HTTP
BeforeLights Aug 4, 2026
85f8075
fix(iae): require a non-empty local artifact name
BeforeLights Aug 4, 2026
5cf17ff
docs(iae): record local registration evidence
BeforeLights Aug 4, 2026
b74fe83
feat(web): add safe spreadsheet audit client
BeforeLights Aug 4, 2026
4a089d4
feat(web): add spreadsheet audit review page
BeforeLights Aug 4, 2026
dd23a42
test(repo): specify dogfood vertical readiness gate
BeforeLights Aug 4, 2026
5fec844
feat(repo): add dogfood vertical readiness gate
BeforeLights Aug 4, 2026
56cb32b
fix(repo): keep dogfood gate tests runnable as ESM
BeforeLights Aug 4, 2026
ec791da
docs(dogfood): record first testable product path
BeforeLights Aug 4, 2026
bd9d3ec
chore(repo): run dogfood readiness in repository checks
BeforeLights Aug 4, 2026
c3956fd
fix(desktop): normalize folder grant validation errors
BeforeLights Aug 4, 2026
ae60a19
test(api): include local artifact route in OpenAPI contract
BeforeLights Aug 4, 2026
57cc00c
test(api): cover admitted dogfood routes
BeforeLights Aug 4, 2026
e9bf28c
style(domain): normalize public API contract formatting
BeforeLights Aug 4, 2026
9fc57c0
fix(iae): satisfy local registration lint guards
BeforeLights Aug 4, 2026
345e516
test(iae): type admitted local response
BeforeLights Aug 4, 2026
44b5083
Merge dogfood walking skeleton into dev
BeforeLights Aug 4, 2026
41f91a8
Merge dev dogfood walking skeleton for main promotion
BeforeLights Aug 4, 2026
c4314ef
fix(sa): authorize artifacts before run admission
BeforeLights Aug 4, 2026
8090f2e
fix(engine): bound spreadsheet audit result cardinality
BeforeLights Aug 4, 2026
bc2157b
test(repo): exercise dogfood missing-file gate
BeforeLights Aug 4, 2026
0d7fa56
test(engine): exercise auditor hash mismatch branch
BeforeLights Aug 4, 2026
755b70e
fix(desktop): bound folder scans with async directory iteration
BeforeLights Aug 4, 2026
e246098
fix(desktop): reject noncanonical folder scan dates
BeforeLights Aug 4, 2026
bf5194f
test(desktop): isolate extra-field grant validation
BeforeLights Aug 4, 2026
e00dec1
test(desktop): keep async folder fixtures lint-clean
BeforeLights Aug 4, 2026
646ba27
fix(sa): preserve idempotent replay after artifact changes
BeforeLights Aug 4, 2026
a4db889
fix(api): require explicit local run storage opt-in
BeforeLights Aug 4, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/desktop/src/application/folder-grant.port.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export interface FolderGrantPort {
grantFolder(): Promise<unknown>;
}
83 changes: 83 additions & 0 deletions apps/desktop/src/main/adapters/electron-folder-grant.adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { opendir as openDirectory } 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;
}

interface FolderDirectoryLike extends AsyncIterable<FolderEntryLike> {
close(): Promise<void>;
}

type OpenDirectory = (folderPath: string) => Promise<FolderDirectoryLike>;

export interface ElectronFolderGrantAdapterInput {
readonly dialog: FolderDialogLike;
readonly now?: () => Date;
readonly opendir?: OpenDirectory;
}

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 #opendir: OpenDirectory;

public constructor({
dialog,
now = () => new Date(),
opendir = openDirectory,
}: ElectronFolderGrantAdapterInput) {
this.#dialog = dialog;
this.#now = now;
this.#opendir = opendir;
}

public async grantFolder(): Promise<FolderGrantState> {
let directory: FolderDirectoryLike | undefined;
try {
const selection = await this.#dialog.showOpenDialog({ properties: ['openDirectory'] });
const folderPath = selection.filePaths[0];
if (selection.canceled || folderPath === undefined || folderPath.length === 0)
return notGranted();
directory = await this.#opendir(folderPath);
let fileCount = 0;
for await (const entry of directory) {
if (entry.isFile() && !entry.isSymbolicLink()) {
fileCount += 1;
if (fileCount > MAX_FOLDER_FILES) return notGranted();
}
}
return parseFolderGrantState({
fileCount,
lastScanAt: this.#now().toISOString(),
status: 'granted',
});
} catch {
return notGranted();
} finally {
if (directory !== undefined) {
try {
await directory.close();
} catch {
// The result remains content-free even when cleanup fails.
}
}
}
}
}
9 changes: 8 additions & 1 deletion apps/desktop/src/main/index.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -26,6 +27,11 @@ async function openDesktopWindow(): Promise<void> {
applicationVersion: app.getVersion(),
locale: 'vi-VN',
});
const folderGrant = new ElectronFolderGrantAdapter({
dialog: {
showOpenDialog: (options) => dialog.showOpenDialog({ properties: [...options.properties] }),
},
});
const sidecar = new UnavailableSidecarAdapter();

await createDesktopWindow({
Expand All @@ -36,6 +42,7 @@ async function openDesktopWindow(): Promise<void> {
expectedRendererUrl,
getActiveWindow: () => activeWindow as unknown as DesktopWindowLike,
ipcMain: ipcMain as unknown as DesktopIpcRegistrationInput['ipcMain'],
folderGrant,
localState,
sidecar,
});
Expand Down
16 changes: 15 additions & 1 deletion apps/desktop/src/main/ipc-registry.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -99,14 +102,25 @@ export function registerDesktopIpcV1({
expectedRendererUrl,
getActiveWindow,
ipcMain,
folderGrant,
localState,
sidecar,
}: DesktopIpcRegistrationInput): () => void {
const previous = registrations.get(ipcMain);
if (previous !== undefined) previous.active = false;

for (const channel of Object.values(DESKTOP_IPC_CHANNELS)) ipcMain.removeHandler(channel);
const handlers: Record<DesktopIpcChannel, IpcHandler> = {
const handlers: Partial<Record<DesktopIpcChannel, IpcHandler>> = {
...(folderGrant === undefined
? {}
: {
[DESKTOP_IPC_CHANNELS.folderGrant]: guardedHandler(
expectedRendererUrl,
getActiveWindow,
() => folderGrant.grantFolder(),
parseFolderGrantState,
),
}),
[DESKTOP_IPC_CHANNELS.sessionGetSafeState]: guardedHandler(
expectedRendererUrl,
getActiveWindow,
Expand Down
9 changes: 8 additions & 1 deletion apps/desktop/src/preload/bridge-v1.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
DESKTOP_IPC_CHANNELS,
parseDesktopSafeState,
parseFolderGrantState,
parseSidecarSafeStatus,
type DesktopBridgeV1,
type DesktopIpcChannel,
Expand All @@ -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 }) });
}
42 changes: 42 additions & 0 deletions apps/desktop/src/renderer/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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.',
Expand All @@ -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;

Expand All @@ -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<DesktopLocale>('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<void> {
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;
Expand Down Expand Up @@ -127,6 +154,21 @@ export function DesktopApp() {
</div>
</dl>

<section className="folder-grant" aria-labelledby="folder-title">
<div>
<h2 id="folder-title">{copy.folderTitle}</h2>
<p>{folderState.status === 'granted' ? copy.folderGranted : copy.folderNotGranted}</p>
</div>
<div className="folder-grant__actions">
<span className="numeric">
{copy.folderFiles}: {new Intl.NumberFormat(locale).format(folderState.fileCount)}
</span>
<button className="locale-button" onClick={() => void grantFolder()} type="button">
{copy.folderPick}
</button>
</div>
</section>

<aside className="privacy-note" aria-labelledby="privacy-title">
<span className="privacy-icon" aria-hidden="true">
i
Expand Down
42 changes: 41 additions & 1 deletion apps/desktop/src/renderer/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,36 @@ h1 {
background: var(--db-color-status-info-surface);
}

.folder-grant {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--db-spacing-6);
margin-block-start: var(--db-spacing-8);
padding: var(--db-spacing-4);
border: var(--db-elevation-level1) solid var(--db-color-border);
border-radius: var(--db-radius-medium);
background: var(--db-color-surface);
}

.folder-grant h2 {
font-size: var(--db-typography-font-size-heading-small);
line-height: var(--db-typography-line-height-compact);
}

.folder-grant p {
margin-block-start: var(--db-spacing-1);
color: var(--db-color-text-muted);
}

.folder-grant__actions {
display: flex;
align-items: center;
gap: var(--db-spacing-3);
color: var(--db-color-text-muted);
font-size: var(--db-typography-font-size-label);
}

.privacy-note h2 {
font-size: var(--db-typography-font-size-heading-small);
line-height: var(--db-typography-line-height-compact);
Expand Down Expand Up @@ -216,13 +246,23 @@ h1 {
}

.agent-summary,
.status-row {
.status-row,
.folder-grant {
grid-template-columns: 1fr;
}

.agent-summary {
gap: var(--db-spacing-4);
}

.folder-grant {
align-items: flex-start;
flex-direction: column;
}

.folder-grant__actions {
flex-wrap: wrap;
}
}

@media (forced-colors: active) {
Expand Down
Loading
Loading