From f44fbe32254868414c108906a77e71e45e7168b9 Mon Sep 17 00:00:00 2001 From: nullxnothing Date: Tue, 9 Jun 2026 12:33:49 -0600 Subject: [PATCH 1/5] feat(bridge): MCP server exposing gated wallet/launch/memory tools to external agents Claude Code and Cursor can now call DAEMON tools through a stdio shim that forwards to a loopback HTTP server in the main process. Every write tool blocks on an approval card inside DAEMON; sensitive tools keep typed confirm and approval summaries now carry the [MAINNET] mark. Token auth, 13-tool allowlist intersected with enabled packs, cwd-based project resolution, 120s approval timeout, and a per-project .mcp.json registration flow. --- electron-builder.json | 6 + electron/ipc/bridge.ts | 145 +++++++++++ electron/main/index.ts | 9 + electron/preload/index.ts | 12 + electron/services/AriaAgentService.ts | 17 +- .../services/bridge/BridgeServerService.ts | 226 ++++++++++++++++++ electron/services/bridge/BridgeToolGateway.ts | 171 +++++++++++++ electron/services/bridge/bridgeManifest.ts | 37 +++ electron/services/bridge/bridgeToken.ts | 85 +++++++ electron/services/bridge/shim.ts | 203 ++++++++++++++++ electron/shared/types.ts | 34 +++ package.json | 5 +- pnpm-lock.yaml | 67 ++---- scripts/smoke/bridge-shim.mjs | 163 +++++++++++++ src/App.tsx | 2 + src/components/BridgeApprovalHost.module.css | 39 +++ src/components/BridgeApprovalHost.tsx | 31 +++ src/panels/AgentWorkbench/ApprovalCard.tsx | 9 +- src/panels/SettingsPanel/BridgeSection.tsx | 101 ++++++++ src/panels/SettingsPanel/SettingsPanel.tsx | 10 +- src/store/bridge.ts | 46 ++++ src/types/daemon.d.ts | 11 + test/panels/BridgeApprovalHost.dom.test.tsx | 83 +++++++ .../AriaAgentService.executeToolCall.test.ts | 109 +++++++++ test/services/BridgeServerService.test.ts | 140 +++++++++++ test/services/BridgeToolGateway.test.ts | 183 ++++++++++++++ test/services/bridgeToken.test.ts | 56 +++++ vite.bridge.config.ts | 31 +++ 28 files changed, 1984 insertions(+), 47 deletions(-) create mode 100644 electron/ipc/bridge.ts create mode 100644 electron/services/bridge/BridgeServerService.ts create mode 100644 electron/services/bridge/BridgeToolGateway.ts create mode 100644 electron/services/bridge/bridgeManifest.ts create mode 100644 electron/services/bridge/bridgeToken.ts create mode 100644 electron/services/bridge/shim.ts create mode 100644 scripts/smoke/bridge-shim.mjs create mode 100644 src/components/BridgeApprovalHost.module.css create mode 100644 src/components/BridgeApprovalHost.tsx create mode 100644 src/panels/SettingsPanel/BridgeSection.tsx create mode 100644 src/store/bridge.ts create mode 100644 test/panels/BridgeApprovalHost.dom.test.tsx create mode 100644 test/services/AriaAgentService.executeToolCall.test.ts create mode 100644 test/services/BridgeServerService.test.ts create mode 100644 test/services/BridgeToolGateway.test.ts create mode 100644 test/services/bridgeToken.test.ts create mode 100644 vite.bridge.config.ts diff --git a/electron-builder.json b/electron-builder.json index c7151e6b..6614b1ee 100644 --- a/electron-builder.json +++ b/electron-builder.json @@ -16,6 +16,12 @@ "node_modules/better-sqlite3/**", "node_modules/node-pty/**" ], + "extraResources": [ + { + "from": "dist-bridge", + "to": "bridge" + } + ], "publish": [ { "provider": "github", diff --git a/electron/ipc/bridge.ts b/electron/ipc/bridge.ts new file mode 100644 index 00000000..6620e981 --- /dev/null +++ b/electron/ipc/bridge.ts @@ -0,0 +1,145 @@ +/** + * Bridge IPC — approval round-trips between the bridge gateway (main) and the + * renderer approval surface, plus server lifecycle and project registration. + * + * Parallel to ipc/aria.ts on purpose: aria's pending-approval map is keyed to + * chat-transcript turns and its events route to the initiating sender; bridge + * approvals have no turn, broadcast to every window, and carry a timeout. + * `bridge:approve` is renderer-only (trusted sender) — the shim can never + * resolve an approval. + */ +import fs from 'node:fs' +import path from 'node:path' +import { app, ipcMain, BrowserWindow } from 'electron' +import { ipcHandler } from '../services/IpcHandlerFactory' +import { isTrustedSender } from '../security/ipcSender' +import * as McpConfig from '../services/McpConfig' +import { ensureBridgeToken, writeBridgeRuntimeInfo, rotateBridgeToken, bridgeInfoFile } from '../services/bridge/bridgeToken' +import { startBridgeServer, stopBridgeServer, getBridgeStatus } from '../services/bridge/BridgeServerService' +import { listBridgeTools, executeBridgeCall, type BridgeCallRequest } from '../services/bridge/BridgeToolGateway' +import type { BridgeToolEvent } from '../shared/types' + +const pendingBridgeApprovals = new Map void>() + +function emitToRenderer(event: BridgeToolEvent) { + for (const win of BrowserWindow.getAllWindows()) { + if (!win.isDestroyed()) win.webContents.send('bridge:event', event) + } +} + +function focusAppWindow() { + const win = BrowserWindow.getAllWindows()[0] + if (!win || win.isDestroyed()) return + if (win.isMinimized()) win.restore() + win.focus() +} + +/** Block until the user decides in DAEMON's UI. The gateway adds the timeout. */ +function requestBridgeApproval(req: { callId: string; name: string; risk: 'read' | 'write' | 'sensitive'; summary: string; input: unknown }): Promise { + return new Promise((resolve) => { + pendingBridgeApprovals.set(req.callId, resolve) + emitToRenderer({ kind: 'approval-request', callId: req.callId, name: req.name, risk: req.risk, summary: req.summary, input: req.input, source: 'bridge' }) + focusAppWindow() + }) +} + +export function cancelBridgeApproval(callId: string): void { + pendingBridgeApprovals.delete(callId) + emitToRenderer({ kind: 'approval-expired', callId }) +} + +export function cancelAllBridgeApprovals(): void { + for (const [callId, resolve] of pendingBridgeApprovals) { + resolve(false) + emitToRenderer({ kind: 'approval-expired', callId }) + } + pendingBridgeApprovals.clear() +} + +function executeCall(req: BridgeCallRequest) { + return executeBridgeCall(req, { + requestApproval: requestBridgeApproval, + cancelApproval: cancelBridgeApproval, + emit: emitToRenderer, + }) +} + +export async function startBridge(): Promise { + const userData = app.getPath('userData') + const { token, file } = ensureBridgeToken(userData) + const status = await startBridgeServer({ + token, + tokenFile: file, + version: app.getVersion(), + listTools: listBridgeTools, + executeCall, + }) + if (status.running) { + writeBridgeRuntimeInfo(userData, { port: status.port, token, version: app.getVersion() }) + } +} + +export async function stopBridge(): Promise { + cancelAllBridgeApprovals() + await stopBridgeServer() +} + +function resolveShimPath(): string { + return app.isPackaged + ? path.join(process.resourcesPath, 'bridge', 'daemon-bridge-shim.mjs') + : path.join(process.env.APP_ROOT ?? app.getAppPath(), 'dist-bridge', 'daemon-bridge-shim.mjs') +} + +/** Offline tool list so the shim can complete the MCP handshake before DAEMON is up. */ +function writeToolsSnapshot(userData: string): void { + const file = path.join(path.dirname(bridgeInfoFile(userData)), 'bridge-tools.json') + fs.writeFileSync(file, JSON.stringify(listBridgeTools(), null, 2), 'utf8') +} + +export function registerBridgeHandlers(): void { + ipcMain.handle('bridge:status', ipcHandler(async () => getBridgeStatus())) + + ipcMain.handle('bridge:rotate-token', ipcHandler(async () => { + const userData = app.getPath('userData') + const token = rotateBridgeToken(userData) + await stopBridge() + const status = await startBridgeServer({ + token, + tokenFile: bridgeInfoFile(userData), + version: app.getVersion(), + listTools: listBridgeTools, + executeCall, + }) + if (status.running) writeBridgeRuntimeInfo(userData, { port: status.port, token, version: app.getVersion() }) + return status + })) + + ipcMain.handle('bridge:register-project', ipcHandler(async (_event, projectPath: string) => { + if (!projectPath || typeof projectPath !== 'string') throw new Error('Project path required') + const shimPath = resolveShimPath() + if (!fs.existsSync(shimPath)) { + throw new Error('Bridge shim not built — run "pnpm run build:bridge" first.') + } + const userData = app.getPath('userData') + McpConfig.addRegistryMcp( + 'daemon-bridge', + JSON.stringify({ command: 'node', args: [shimPath], env: { DAEMON_BRIDGE_INFO: bridgeInfoFile(userData) } }), + 'DAEMON Bridge — user-gated wallet, launch, and memory tools', + false, + ) + McpConfig.toggleProjectMcp(projectPath, 'daemon-bridge', true) + writeToolsSnapshot(userData) + return getBridgeStatus() + })) + + // Raw channel (not ipcHandler): mirrors aria:approve. Guard the sender frame — + // only DAEMON's own renderer may resolve approvals. + ipcMain.on('bridge:approve', (event, callId: string, approved: boolean) => { + if (!isTrustedSender(event)) return + const resolve = pendingBridgeApprovals.get(callId) + if (resolve) { + pendingBridgeApprovals.delete(callId) + resolve(Boolean(approved)) + } + }) +} diff --git a/electron/main/index.ts b/electron/main/index.ts index a86366bb..3ba9d3f5 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -39,6 +39,7 @@ import { registerShiplineHandlers } from '../ipc/shipline' import { registerEmailHandlers } from '../ipc/email' import { registerImageHandlers } from '../ipc/images' import { registerAriaHandlers } from '../ipc/aria' +import { registerBridgeHandlers, startBridge, stopBridge } from '../ipc/bridge' import { registerSwarmHandlers } from '../ipc/swarm' import { registerMemoryHandlers } from '../ipc/memory' import { killAll as killAllSwarmLanes, } from '../services/SwarmOrchestrator' @@ -206,6 +207,7 @@ function cleanupRuntimeState() { killAllSessions() killAllSwarmLanes() shutdownAllLspSessions() + void stopBridge() clearLoadedWallets() closeDb() } @@ -371,6 +373,7 @@ function registerAllIpc() { registerBrowserHandlers() registerEmailHandlers() registerAriaHandlers() + registerBridgeHandlers() registerDashboardHandlers() registerRegistryHandlers() registerSaidHandlers() @@ -393,6 +396,12 @@ function registerAllIpc() { void reconcileSwarmOnBoot().catch(() => {}) } + // Bridge server is always on (loopback + bearer token); the tool catalog + // itself re-filters against enabled packs on every request. + void startBridge().catch((error) => { + console.warn('[bridge] failed to start:', error instanceof Error ? error.message : String(error)) + }) + // Window controls — raw channels (not wrapped by ipcHandler), so guard the // sender frame inline. Embedded/cross-origin frames must not drive the window. ipcMain.on('window:minimize', (event) => { if (isTrustedSender(event)) win?.minimize() }) diff --git a/electron/preload/index.ts b/electron/preload/index.ts index c2d5b919..13137399 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -643,6 +643,18 @@ contextBridge.exposeInMainWorld('daemon', { }, }, + bridge: { + status: () => ipcRenderer.invoke('bridge:status'), + rotateToken: () => ipcRenderer.invoke('bridge:rotate-token'), + registerProject: (projectPath: string) => ipcRenderer.invoke('bridge:register-project', projectPath), + approve: (callId: string, approved: boolean) => ipcRenderer.send('bridge:approve', callId, approved), + onEvent: (handler: (event: unknown) => void) => { + const listener = (_e: unknown, ev: unknown) => handler(ev) + ipcRenderer.on('bridge:event', listener) + return () => ipcRenderer.removeListener('bridge:event', listener) + }, + }, + swarm: { launch: (req: unknown) => ipcRenderer.invoke('swarm:launch', req), list: (limit?: number) => ipcRenderer.invoke('swarm:list', limit), diff --git a/electron/services/AriaAgentService.ts b/electron/services/AriaAgentService.ts index cff5ecd1..52dc9d97 100644 --- a/electron/services/AriaAgentService.ts +++ b/electron/services/AriaAgentService.ts @@ -16,6 +16,7 @@ import { recordLocalAiUsage } from './DaemonAIService' import { ARIA_TOOLS, getTool } from './aria/toolCatalog' import * as MemoryService from './MemoryService' import { assembleSystemPrompt } from './aria/contextAssembler' +import { clusterMark } from './aria/tools/shared' import { toAnthropicTools, type AriaTool, type AriaContextSnapshot, type AriaUiEffect } from './aria/AriaTool' import { laneToClaudeModel, buildPlanSteps, buildPatchProposal } from './aria/patchUtils' import type { AgentMessage } from './providers/agentTurn' @@ -336,6 +337,18 @@ function parseCaptureJson(text: string): { kind: MemoryKind; title: string; valu } } +/** Execute one tool through the standard risk gate, outside the chat loop. + * Used by the Bridge: `planApproved` is pinned false so every write/sensitive + * call pauses on `transport.requestApproval` — external agents never get the + * plan-mode auto-run path. */ +export async function executeToolCall( + use: { id: string; name: string; input: Record }, + ctx: { sessionId: string; snapshot: AriaContextSnapshot; runUiEffect: AriaTransport['runUiEffect'] }, + transport: AriaTransport, +): Promise { + return executeTool(use, ctx, transport, { plan: [], patch: null, planApproved: false }) +} + async function executeTool( use: { id: string; name: string; input: Record }, ctx: { sessionId: string; snapshot: AriaContextSnapshot; runUiEffect: AriaTransport['runUiEffect'] }, @@ -488,7 +501,9 @@ async function handleProposePatch( function describeIntent(tool: AriaTool, input: Record): string { const arg = Object.values(input)[0] - return `${tool.name}${arg !== undefined ? `: ${String(arg).slice(0, 80)}` : ''}` + const intent = `${tool.name}${arg !== undefined ? `: ${String(arg).slice(0, 80)}` : ''}` + // Approval cards must make the network unmistakable before the user decides. + return tool.risk === 'read' ? intent : clusterMark(intent) } /** Non-Claude providers: single-shot text answer, no tools. */ diff --git a/electron/services/bridge/BridgeServerService.ts b/electron/services/bridge/BridgeServerService.ts new file mode 100644 index 00000000..11c1c7f4 --- /dev/null +++ b/electron/services/bridge/BridgeServerService.ts @@ -0,0 +1,226 @@ +/** + * Bridge HTTP server — the loopback endpoint the MCP shim talks to. + * + * Modeled on SeekerRelayService with two deliberate deltas: it binds + * 127.0.0.1 (never the LAN), and it has no import-side-effect auto-start — + * main/index.ts starts it explicitly. Auth failures return a generic 404 so + * the server doesn't advertise itself to local port scanners. + */ +import http, { type IncomingMessage, type ServerResponse } from 'node:http' +import crypto from 'node:crypto' +import type { BridgeCallResult, BridgeStatus, BridgeToolDescriptor } from '../../shared/types' +import type { BridgeCallRequest } from './BridgeToolGateway' +import { getApprovalTimeoutMs } from './BridgeToolGateway' + +export interface BridgeServerOptions { + port?: number + token: string + tokenFile: string + version?: string + listTools: () => BridgeToolDescriptor[] + executeCall: (req: BridgeCallRequest) => Promise +} + +const DEFAULT_PORT = Number(process.env.DAEMON_BRIDGE_PORT ?? 7337) +const MAX_BODY_BYTES = 512 * 1024 +const MAX_IN_FLIGHT = 4 +/** Headroom past the approval timeout for the tool handler itself to run. */ +const EXECUTION_ALLOWANCE_MS = 180_000 + +let server: http.Server | null = null +let boundPort = DEFAULT_PORT +let options: BridgeServerOptions | null = null +let lastError: string | null = null +const inFlight = new Set() + +function json(res: ServerResponse, statusCode: number, payload: unknown) { + if (res.writableEnded) return + res.writeHead(statusCode, { + 'content-type': 'application/json; charset=utf-8', + 'cache-control': 'no-store', + 'x-content-type-options': 'nosniff', + }) + res.end(JSON.stringify(payload)) +} + +function readBody(req: IncomingMessage): Promise { + return new Promise((resolve, reject) => { + let body = '' + req.on('data', (chunk) => { + body += chunk + if (body.length > MAX_BODY_BYTES) { + reject(new Error('Request body too large')) + req.destroy() + } + }) + req.on('end', () => { + if (!body.trim()) return resolve({}) + try { resolve(JSON.parse(body)) } catch { reject(new Error('Invalid JSON')) } + }) + req.on('error', reject) + }) +} + +function isLoopbackRequest(req: IncomingMessage): boolean { + const addr = req.socket.remoteAddress ?? '' + return addr === '127.0.0.1' || addr === '::1' || addr === '::ffff:127.0.0.1' +} + +/** Web pages must not reach the bridge even from localhost; the shim sends no Origin. */ +function hasBrowserOrigin(req: IncomingMessage): boolean { + const origin = req.headers['origin'] + return typeof origin === 'string' && origin.length > 0 +} + +/** Constant-time bearer check against the bridge token. */ +function isAuthorized(req: IncomingMessage, token: string): boolean { + const header = req.headers['authorization'] + const presented = typeof header === 'string' && header.startsWith('Bearer ') + ? header.slice('Bearer '.length).trim() + : '' + if (!presented || presented.length !== token.length) return false + try { + return crypto.timingSafeEqual(Buffer.from(presented), Buffer.from(token)) + } catch { + return false + } +} + +function parseCallRequest(body: unknown): BridgeCallRequest | null { + if (!body || typeof body !== 'object') return null + const record = body as Record + if (typeof record.toolName !== 'string' || !record.toolName.trim()) return null + const input = record.input + if (input !== undefined && (typeof input !== 'object' || input === null || Array.isArray(input))) return null + return { + toolName: record.toolName.trim(), + input: (input as Record) ?? {}, + cwd: typeof record.cwd === 'string' ? record.cwd : undefined, + } +} + +async function handleCall(req: IncomingMessage, res: ServerResponse, opts: BridgeServerOptions) { + if (inFlight.size >= MAX_IN_FLIGHT) { + return json(res, 429, { ok: false, error: 'Too many concurrent bridge calls' }) + } + let body: unknown + try { + body = await readBody(req) + } catch (error) { + return json(res, 400, { ok: false, error: error instanceof Error ? error.message : 'Invalid request' }) + } + const call = parseCallRequest(body) + if (!call) return json(res, 400, { ok: false, error: 'Expected { toolName, input?, cwd? }' }) + + inFlight.add(res) + const timeoutMs = getApprovalTimeoutMs() + EXECUTION_ALLOWANCE_MS + const timer = setTimeout(() => { + json(res, 504, { ok: false, error: 'Bridge call timed out' }) + inFlight.delete(res) + }, timeoutMs) + try { + const result = await opts.executeCall(call) + clearTimeout(timer) + json(res, 200, { ok: true, data: result }) + } catch (error) { + clearTimeout(timer) + json(res, 500, { ok: false, error: error instanceof Error ? error.message : 'Bridge call failed' }) + } finally { + inFlight.delete(res) + } +} + +async function handleRequest(req: IncomingMessage, res: ServerResponse, opts: BridgeServerOptions) { + if (!isLoopbackRequest(req)) return json(res, 403, { ok: false, error: 'Forbidden' }) + if (hasBrowserOrigin(req)) return json(res, 403, { ok: false, error: 'Forbidden' }) + const url = new URL(req.url ?? '/', `http://${req.headers.host ?? '127.0.0.1'}`) + + if (req.method === 'GET' && url.pathname === '/bridge/ping') { + return json(res, 200, { ok: true, data: { app: 'daemon', version: opts.version ?? '0.0.0', running: true } }) + } + + // Auth failures fall through to a generic 404: don't confirm the bridge exists. + if (!isAuthorized(req, opts.token)) return json(res, 404, { ok: false, error: 'Not found' }) + + if (req.method === 'GET' && url.pathname === '/bridge/tools') { + return json(res, 200, { ok: true, data: opts.listTools() }) + } + if (req.method === 'POST' && url.pathname === '/bridge/call') { + return handleCall(req, res, opts) + } + return json(res, 404, { ok: false, error: 'Not found' }) +} + +async function probeExistingBridge(port: number): Promise { + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), 600) + try { + const res = await fetch(`http://127.0.0.1:${port}/bridge/ping`, { signal: controller.signal }) + const body = await res.json().catch(() => null) as { ok?: boolean; data?: { app?: string } } | null + return Boolean(res.ok && body?.ok && body.data?.app === 'daemon') + } catch { + return false + } finally { + clearTimeout(timeout) + } +} + +function isAddressInUse(error: unknown): boolean { + return Boolean(error && typeof error === 'object' && (error as NodeJS.ErrnoException).code === 'EADDRINUSE') +} + +export async function startBridgeServer(opts: BridgeServerOptions): Promise { + if (server?.listening) return getBridgeStatus() + options = opts + lastError = null + const port = opts.port ?? DEFAULT_PORT + + try { + await new Promise((resolve, reject) => { + const nextServer = http.createServer((req, res) => { + void handleRequest(req, res, opts).catch((error) => { + json(res, 500, { ok: false, error: error instanceof Error ? error.message : 'Bridge error' }) + }) + }) + nextServer.once('error', reject) + nextServer.listen(port, '127.0.0.1', () => { + server = nextServer + const address = nextServer.address() + boundPort = address && typeof address === 'object' ? address.port : port + resolve() + }) + }) + } catch (error) { + if (isAddressInUse(error)) { + const isOurs = await probeExistingBridge(port) + lastError = isOurs + ? `Port ${port} is already served by another DAEMON instance` + : `Port ${port} is in use by another process — set DAEMON_BRIDGE_PORT` + return getBridgeStatus() + } + lastError = error instanceof Error ? error.message : String(error) + return getBridgeStatus() + } + return getBridgeStatus() +} + +export async function stopBridgeServer(): Promise { + if (!server) return + const current = server + server = null + for (const res of inFlight) { + json(res, 503, { ok: false, error: 'DAEMON is shutting down' }) + } + inFlight.clear() + await new Promise((resolve) => current.close(() => resolve())) +} + +export function getBridgeStatus(): BridgeStatus { + return { + running: Boolean(server?.listening), + port: boundPort, + tokenFile: options?.tokenFile ?? '', + toolCount: options ? options.listTools().length : 0, + ...(lastError ? { error: lastError } : {}), + } +} diff --git a/electron/services/bridge/BridgeToolGateway.ts b/electron/services/bridge/BridgeToolGateway.ts new file mode 100644 index 00000000..f4979039 --- /dev/null +++ b/electron/services/bridge/BridgeToolGateway.ts @@ -0,0 +1,171 @@ +/** + * Bridge gateway — the single path from an external MCP agent to an ARIA tool. + * + * Electron-free on purpose (DB + settings only) so Vitest can drive it directly. + * Risk gating is NOT re-implemented here: every call goes through + * AriaAgentService.executeToolCall with planApproved pinned false, so write and + * sensitive tools always pause on the injected requestApproval. The approval + * itself can only be resolved from DAEMON's renderer — never by the caller. + */ +import crypto from 'node:crypto' +import path from 'node:path' +import { getDb } from '../../db/db' +import * as SettingsService from '../SettingsService' +import { executeToolCall, type AriaTransport } from '../AriaAgentService' +import { getTool } from '../aria/toolCatalog' +import type { AriaTool } from '../aria/AriaTool' +import { BRIDGE_TOOL_ALLOWLIST } from './bridgeManifest' +import type { BridgeCallResult, BridgeToolDescriptor, BridgeToolEvent } from '../../shared/types' + +const DEFAULT_APPROVAL_TIMEOUT_MS = 120_000 + +/** Tools that cannot run without a resolved DAEMON project. */ +const PROJECT_REQUIRED_TOOLS = new Set([ + 'remember_fact', 'recall_memories', 'forget_memory', 'update_memory', + 'assign_project_wallet', +]) + +export interface BridgeCallRequest { + toolName: string + input: Record + cwd?: string +} + +export interface BridgeGatewayDeps { + requestApproval: AriaTransport['requestApproval'] + cancelApproval: (callId: string) => void + emit?: (event: BridgeToolEvent) => void + approvalTimeoutMs?: number +} + +export function getApprovalTimeoutMs(): number { + const raw = Number(process.env.DAEMON_BRIDGE_APPROVAL_TIMEOUT_MS) + return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_APPROVAL_TIMEOUT_MS +} + +/** Allowlist ∩ enabled packs, resolved against the live catalog. */ +export function listBridgeTools(): BridgeToolDescriptor[] { + const packs = SettingsService.getEnabledPacks() + const tools: BridgeToolDescriptor[] = [] + for (const entry of BRIDGE_TOOL_ALLOWLIST) { + if (entry.packId && packs[entry.packId] === false) continue + const tool = getTool(entry.name) + if (!tool) continue + tools.push({ + name: tool.name, + description: tool.description, + risk: tool.risk, + inputSchema: tool.input as Record, + }) + } + return tools +} + +/** Resolve a tool name through the allowlist + pack filter, with a caller-safe reason. */ +export function findBridgeTool(name: string): { ok: true; tool: AriaTool } | { ok: false; error: string } { + const entry = BRIDGE_TOOL_ALLOWLIST.find((t) => t.name === name) + if (!entry) return { ok: false, error: `Unknown tool "${name}" — not exposed over the DAEMON Bridge.` } + if (entry.packId && SettingsService.getEnabledPacks()[entry.packId] === false) { + return { ok: false, error: `Tool unavailable: the ${entry.packId} pack is disabled in DAEMON.` } + } + const tool = getTool(name) + if (!tool) return { ok: false, error: `Unknown tool "${name}".` } + return { ok: true, tool } +} + +interface ProjectMatch { id: string; path: string } + +/** Longest-prefix match of the caller's cwd against registered project paths. */ +export function resolveProjectForCwd(cwd: string | undefined): ProjectMatch | null { + if (!cwd) return null + const rows = getDb().prepare('SELECT id, path FROM projects').all() as ProjectMatch[] + const target = normalizeFsPath(cwd) + let best: ProjectMatch | null = null + let bestLen = -1 + for (const row of rows) { + const root = normalizeFsPath(row.path) + const isMatch = target === root || target.startsWith(root + path.sep) + if (isMatch && root.length > bestLen) { + best = row + bestLen = root.length + } + } + return best +} + +function normalizeFsPath(value: string): string { + const resolved = path.resolve(value) + return process.platform === 'win32' ? resolved.toLowerCase() : resolved +} + +/** Execute one external tool call through the standard ARIA risk gate. */ +export async function executeBridgeCall(req: BridgeCallRequest, deps: BridgeGatewayDeps): Promise { + const found = findBridgeTool(req.toolName) + if (!found.ok) return { status: 'error', summary: found.error } + if (!req.input || typeof req.input !== 'object' || Array.isArray(req.input)) { + return { status: 'error', summary: 'Tool input must be a JSON object.' } + } + + const project = resolveProjectForCwd(req.cwd) + if (!project && PROJECT_REQUIRED_TOOLS.has(req.toolName)) { + return { + status: 'error', + summary: `No DAEMON project matches "${req.cwd ?? '(no cwd)'}" — add this folder as a project in DAEMON first.`, + } + } + + const callId = crypto.randomUUID() + const timeoutMs = deps.approvalTimeoutMs ?? getApprovalTimeoutMs() + let approvalTimedOut = false + + const transport: AriaTransport = { + emit: () => {}, + requestApproval: (approval) => + new Promise((resolve) => { + const timer = setTimeout(() => { + approvalTimedOut = true + deps.cancelApproval(approval.callId) + resolve(false) + }, timeoutMs) + deps.requestApproval(approval).then((approved) => { + clearTimeout(timer) + resolve(approved) + }) + }), + requestPatchDecision: async () => 'discard', + runUiEffect: async () => { + // Tripwire: no allowlisted tool uses UI effects. If one ever does, fail loudly + // instead of silently no-oping a renderer round-trip. + throw new Error('UI effects are not available over the bridge.') + }, + } + + const record = await executeToolCall( + { id: callId, name: req.toolName, input: req.input }, + { + sessionId: `bridge:${crypto.randomUUID()}`, + snapshot: { + activeProjectId: project?.id ?? null, + activeProjectPath: project?.path ?? null, + currentPanelId: null, + openFilePath: null, + chips: { activeFile: false, projectTree: false, gitDiff: false, terminalLogs: false, walletContext: false }, + planMode: false, + }, + runUiEffect: transport.runUiEffect, + }, + transport, + ) + + const status: BridgeCallResult['status'] = + record.status === 'rejected' && approvalTimedOut ? 'timeout' + : record.status === 'rejected' ? 'rejected' + : record.status === 'error' ? 'error' + : 'done' + const summary = + status === 'timeout' + ? `Approval timed out — no response in DAEMON within ${Math.round(timeoutMs / 1000)}s.` + : record.summary + deps.emit?.({ kind: 'call', callId, name: req.toolName, risk: found.tool.risk, status, summary }) + return { status, summary, result: record.result } +} diff --git a/electron/services/bridge/bridgeManifest.ts b/electron/services/bridge/bridgeManifest.ts new file mode 100644 index 00000000..2f6ff0ff --- /dev/null +++ b/electron/services/bridge/bridgeManifest.ts @@ -0,0 +1,37 @@ +/** + * Bridge tool allowlist — the only ARIA tools reachable by external MCP agents. + * + * Deliberately import-free (no services) so the gateway, tests, and the shim + * build can all consume it. `packId: null` marks core tools that are always + * available; everything else is filtered against the enabled packs at runtime. + * + * Excluded by design: anything touching `runUiEffect` (renderer round-trips), + * `present_plan`/`propose_patch` (transcript UI, not side effects), and all + * domains beyond wallet/launch/memory for v1. + */ +import type { PackId } from '../../shared/packManifest' + +export interface BridgeAllowlistEntry { + name: string + packId: PackId | null +} + +export const BRIDGE_TOOL_ALLOWLIST: readonly BridgeAllowlistEntry[] = [ + // wallet pack + { name: 'read_wallet', packId: 'wallet' }, + { name: 'generate_wallet', packId: 'wallet' }, + { name: 'set_default_wallet', packId: 'wallet' }, + { name: 'assign_project_wallet', packId: 'wallet' }, + { name: 'store_helius_key', packId: 'wallet' }, + // launch pack + { name: 'tokenlaunch_list_launchpads', packId: 'launch' }, + { name: 'tokenlaunch_preflight', packId: 'launch' }, + { name: 'tokenlaunch_create', packId: 'launch' }, + // memory pack + { name: 'remember_fact', packId: 'memory' }, + { name: 'recall_memories', packId: 'memory' }, + { name: 'forget_memory', packId: 'memory' }, + { name: 'update_memory', packId: 'memory' }, + // core + { name: 'read_project_status', packId: null }, +] diff --git a/electron/services/bridge/bridgeToken.ts b/electron/services/bridge/bridgeToken.ts new file mode 100644 index 00000000..924e5c5d --- /dev/null +++ b/electron/services/bridge/bridgeToken.ts @@ -0,0 +1,85 @@ +/** + * Bridge token + runtime-info file. The shim authenticates to the loopback + * bridge server with a bearer token it reads from `/bridge/bridge.json`. + * + * Windows note: chmod 0o600 is a no-op on NTFS. The file lives under the user's + * profile (%APPDATA%), whose default ACLs already block other non-admin users; + * explicit `icacls` hardening is a documented follow-up, not v1. + */ +import crypto from 'node:crypto' +import fs from 'node:fs' +import path from 'node:path' + +export interface BridgeRuntimeInfo { + token: string + port: number + pid: number + version: string + updatedAt: number +} + +function bridgeDir(userDataDir: string): string { + return path.join(userDataDir, 'bridge') +} + +export function bridgeInfoFile(userDataDir: string): string { + return path.join(bridgeDir(userDataDir), 'bridge.json') +} + +function readInfo(file: string): Partial { + try { + return JSON.parse(fs.readFileSync(file, 'utf8')) as Partial + } catch { + return {} + } +} + +function writeInfo(file: string, info: BridgeRuntimeInfo): void { + fs.mkdirSync(path.dirname(file), { recursive: true }) + fs.writeFileSync(file, JSON.stringify(info, null, 2), { encoding: 'utf8', mode: 0o600 }) +} + +/** Return the persisted token, creating one on first run. */ +export function ensureBridgeToken(userDataDir: string): { token: string; file: string } { + const file = bridgeInfoFile(userDataDir) + const existing = readInfo(file) + if (typeof existing.token === 'string' && existing.token.length >= 32) { + return { token: existing.token, file } + } + const token = crypto.randomBytes(32).toString('hex') + writeInfo(file, { + token, + port: typeof existing.port === 'number' ? existing.port : 0, + pid: process.pid, + version: process.env.npm_package_version ?? '0.0.0', + updatedAt: Date.now(), + }) + return { token, file } +} + +/** Record the live port/pid so shims can discover the running server. */ +export function writeBridgeRuntimeInfo(userDataDir: string, info: { port: number; token: string; version?: string }): void { + const file = bridgeInfoFile(userDataDir) + writeInfo(file, { + token: info.token, + port: info.port, + pid: process.pid, + version: info.version ?? process.env.npm_package_version ?? '0.0.0', + updatedAt: Date.now(), + }) +} + +/** Replace the token. Shims re-read bridge.json per process start, so rotation is safe. */ +export function rotateBridgeToken(userDataDir: string): string { + const file = bridgeInfoFile(userDataDir) + const existing = readInfo(file) + const token = crypto.randomBytes(32).toString('hex') + writeInfo(file, { + token, + port: typeof existing.port === 'number' ? existing.port : 0, + pid: process.pid, + version: typeof existing.version === 'string' ? existing.version : '0.0.0', + updatedAt: Date.now(), + }) + return token +} diff --git a/electron/services/bridge/shim.ts b/electron/services/bridge/shim.ts new file mode 100644 index 00000000..aa8c1b48 --- /dev/null +++ b/electron/services/bridge/shim.ts @@ -0,0 +1,203 @@ +/** + * DAEMON Bridge shim — the stdio MCP server external agents spawn. + * + * Thin by design: it advertises DAEMON's bridge tools and forwards every call + * to the loopback bridge server in the running DAEMON app, where risk gating + * and user approval happen. The shim holds no secrets beyond the bearer token + * it reads from bridge.json, and it can approve nothing. + * + * Built standalone (vite.bridge.config.ts → dist-bridge/daemon-bridge-shim.mjs, + * SDK bundled) and run by the system node (>= 18 for global fetch). + * stdout is MCP protocol — log to stderr only. + */ +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' +import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js' +import type { BridgeCallResult, BridgeToolDescriptor } from '../../shared/types' + +const SHIM_VERSION = '1.0.0' +const TOOLS_FETCH_TIMEOUT_MS = 800 +const CALL_HARD_CAP_MS = 10 * 60 * 1000 +const PROGRESS_INTERVAL_MS = 10_000 + +const NOT_RUNNING = 'DAEMON is not running — open the DAEMON app and try again.' +const TOKEN_MISMATCH = 'Bridge token mismatch — re-register the DAEMON Bridge from DAEMON settings.' +const BUSY = 'DAEMON is busy — too many concurrent bridge calls. Try again in a moment.' + +interface BridgeInfo { + token: string + port: number +} + +function log(message: string): void { + process.stderr.write(`[daemon-bridge-shim] ${message}\n`) +} + +function bridgeInfoCandidates(): string[] { + const fromEnv = process.env.DAEMON_BRIDGE_INFO + const home = os.homedir() + const roots = process.platform === 'win32' + ? [process.env.APPDATA ?? path.join(home, 'AppData', 'Roaming')] + : process.platform === 'darwin' + ? [path.join(home, 'Library', 'Application Support')] + : [process.env.XDG_CONFIG_HOME ?? path.join(home, '.config')] + const defaults = roots.flatMap((root) => [ + path.join(root, 'daemon', 'bridge', 'bridge.json'), + path.join(root, 'DAEMON', 'bridge', 'bridge.json'), + ]) + return fromEnv ? [fromEnv, ...defaults] : defaults +} + +function readBridgeInfo(): { info: BridgeInfo; file: string } | null { + for (const file of bridgeInfoCandidates()) { + try { + const parsed = JSON.parse(fs.readFileSync(file, 'utf8')) as Partial + if (typeof parsed.token === 'string' && typeof parsed.port === 'number' && parsed.port > 0) { + return { info: { token: parsed.token, port: parsed.port }, file } + } + } catch { + // try the next candidate + } + } + return null +} + +async function fetchJson(url: string, init: RequestInit, timeoutMs: number): Promise<{ status: number; body: unknown }> { + const controller = new AbortController() + const timer = setTimeout(() => controller.abort(), timeoutMs) + try { + const res = await fetch(url, { ...init, signal: controller.signal }) + const body = await res.json().catch(() => null) + return { status: res.status, body } + } finally { + clearTimeout(timer) + } +} + +/** Live tool list from DAEMON, falling back to the snapshot written at registration. */ +async function loadTools(located: { info: BridgeInfo; file: string } | null): Promise { + if (located) { + const url = `http://127.0.0.1:${located.info.port}/bridge/tools` + const headers = { authorization: `Bearer ${located.info.token}` } + for (let attempt = 0; attempt < 2; attempt++) { + try { + const { status, body } = await fetchJson(url, { headers }, TOOLS_FETCH_TIMEOUT_MS) + const data = (body as { ok?: boolean; data?: BridgeToolDescriptor[] } | null)?.data + if (status === 200 && Array.isArray(data)) { + try { + fs.writeFileSync(path.join(path.dirname(located.file), 'bridge-tools.json'), JSON.stringify(data, null, 2), 'utf8') + } catch { + // cache write is best-effort + } + return data + } + } catch { + // retry once, then fall through to the cache + } + } + try { + const cached = JSON.parse(fs.readFileSync(path.join(path.dirname(located.file), 'bridge-tools.json'), 'utf8')) as BridgeToolDescriptor[] + if (Array.isArray(cached)) return cached + } catch { + // no cache either + } + } + log('no tool list available — DAEMON not reachable and no cached bridge-tools.json') + return [] +} + +function describeFailure(status: number, body: unknown): string { + if (status === 401 || status === 403 || status === 404) return TOKEN_MISMATCH + if (status === 429) return BUSY + const error = (body as { error?: string } | null)?.error + return error ? `DAEMON bridge error: ${error}` : `DAEMON bridge error (HTTP ${status}).` +} + +function toContent(result: BridgeCallResult): { content: Array<{ type: 'text'; text: string }>; isError?: boolean } { + if (result.status === 'rejected') { + return { content: [{ type: 'text', text: 'The user rejected this action in DAEMON.' }], isError: true } + } + if (result.status !== 'done') { + return { content: [{ type: 'text', text: result.summary }], isError: true } + } + const detail = result.result !== undefined && result.result !== result.summary + ? `\n${JSON.stringify(result.result, null, 2)}` + : '' + return { content: [{ type: 'text', text: `${result.summary}${detail}` }] } +} + +async function main(): Promise { + const located = readBridgeInfo() + if (!located) log('bridge.json not found — calls will fail until DAEMON registers the bridge') + const tools = await loadTools(located) + + const server = new Server( + { name: 'daemon-bridge', version: SHIM_VERSION }, + { capabilities: { tools: {} } }, + ) + + server.setRequestHandler(ListToolsRequestSchema, async () => ({ + tools: tools.map((tool) => ({ + name: tool.name, + description: `${tool.description}${tool.risk === 'read' ? '' : ' Requires user approval inside the DAEMON app.'}`, + inputSchema: tool.inputSchema, + })), + })) + + server.setRequestHandler(CallToolRequestSchema, async (request, extra) => { + // Re-read per call: the port or token may have changed since the shim started. + const live = readBridgeInfo() ?? located + if (!live) { + return { content: [{ type: 'text', text: NOT_RUNNING }], isError: true } + } + + const progressToken = request.params._meta?.progressToken + let progress = 0 + const progressTimer = progressToken === undefined ? null : setInterval(() => { + progress += 1 + void extra.sendNotification({ + method: 'notifications/progress', + params: { progressToken, progress, message: 'Waiting on DAEMON (approval may be pending)…' }, + }).catch(() => {}) + }, PROGRESS_INTERVAL_MS) + + try { + const { status, body } = await fetchJson( + `http://127.0.0.1:${live.info.port}/bridge/call`, + { + method: 'POST', + headers: { 'content-type': 'application/json', authorization: `Bearer ${live.info.token}` }, + body: JSON.stringify({ + toolName: request.params.name, + input: request.params.arguments ?? {}, + cwd: process.cwd(), + }), + }, + CALL_HARD_CAP_MS, + ) + if (status !== 200) { + return { content: [{ type: 'text', text: describeFailure(status, body) }], isError: true } + } + const result = (body as { data?: BridgeCallResult } | null)?.data + if (!result || typeof result.summary !== 'string') { + return { content: [{ type: 'text', text: 'DAEMON returned an unexpected bridge response.' }], isError: true } + } + return toContent(result) + } catch { + return { content: [{ type: 'text', text: NOT_RUNNING }], isError: true } + } finally { + if (progressTimer) clearInterval(progressTimer) + } + }) + + await server.connect(new StdioServerTransport()) + log(`ready — ${tools.length} tools, target port ${located?.info.port ?? 'unknown'}`) +} + +main().catch((error) => { + log(`fatal: ${error instanceof Error ? error.message : String(error)}`) + process.exit(1) +}) diff --git a/electron/shared/types.ts b/electron/shared/types.ts index 0329ea48..3f4c5338 100644 --- a/electron/shared/types.ts +++ b/electron/shared/types.ts @@ -2689,6 +2689,40 @@ export interface AriaMemorySuggestionLite { value: string } +// --- Bridge (external MCP agents) --- + +export type BridgeCallStatus = 'done' | 'error' | 'rejected' | 'timeout' + +/** Events streamed from the Bridge to the renderer approval surface. */ +export type BridgeToolEvent = + | { kind: 'approval-request'; callId: string; name: string; risk: AriaToolRiskTier; summary: string; input: unknown; source: 'bridge' } + | { kind: 'approval-expired'; callId: string } + | { kind: 'call'; callId: string; name: string; risk: AriaToolRiskTier; status: BridgeCallStatus; summary: string } + +/** Bridge server status surfaced in Settings. */ +export interface BridgeStatus { + running: boolean + port: number + tokenFile: string + toolCount: number + error?: string +} + +/** One tool as advertised to external MCP clients. */ +export interface BridgeToolDescriptor { + name: string + description: string + risk: AriaToolRiskTier + inputSchema: Record +} + +/** Result of one bridge tool call, returned to the shim. */ +export interface BridgeCallResult { + status: BridgeCallStatus + summary: string + result?: unknown +} + // --- Onboarding --- export type OnboardingStepStatus = 'pending' | 'complete' | 'skipped' diff --git a/package.json b/package.json index 58f315de..8c9d5ccc 100644 --- a/package.json +++ b/package.json @@ -17,12 +17,13 @@ "dev:debug": "powershell -NoProfile -Command \"$env:DAEMON_OPEN_DEVTOOLS='1'; vite\"", "build": "tsc && vite build", "build:daemon-ai-cloud": "vite build --config vite.cloud.config.ts", + "build:bridge": "vite build --config vite.bridge.config.ts", "typecheck": "tsc --noEmit", "typecheck:watch": "tsc --noEmit --watch --preserveWatchOutput", "mobile:seeker": "npm --prefix apps/seeker-mobile run start", "mobile:seeker:android": "npm --prefix apps/seeker-mobile run android", "mobile:seeker:typecheck": "npm --prefix apps/seeker-mobile run typecheck", - "package": "pnpm run build && pnpm run rebuild && electron-builder", + "package": "pnpm run build && pnpm run build:bridge && pnpm run rebuild && electron-builder", "postinstall": "pnpm run rebuild:native", "rebuild": "pnpm run rebuild:native", "rebuild:sqlite": "electron-rebuild -f --only better-sqlite3", @@ -56,6 +57,7 @@ "test:release": "pnpm run release:check:v4:local", "test:all": "pnpm run test:ci && pnpm run test:smoke:core && pnpm run test:smoke:ui", "test:smoke": "pnpm run build && pnpm run rebuild && node scripts/smoke/electron-smoke.mjs", + "test:bridge-shim": "pnpm run build:bridge && node scripts/smoke/bridge-shim.mjs", "test:mcp-stress": "pnpm run build && node scripts/smoke/daemon-mcp-stress.mjs", "test:pro-entitlement": "pnpm run build && node scripts/smoke/pro-entitlement-flow.mjs", "test:scaffold-smoke": "pnpm run build && node scripts/smoke/project-scaffold.mjs", @@ -81,6 +83,7 @@ "@metaplex-foundation/umi": "^1.5.1", "@metaplex-foundation/umi-bundle-defaults": "^1.5.1", "@meteora-ag/dynamic-bonding-curve-sdk": "1.5.7", + "@modelcontextprotocol/sdk": "1.29.0", "@nirholas/pump-sdk": "1.30.0", "@oobe-protocol-labs/synapse-sap-sdk": "0.19.8", "@payai/facilitator": "^2.3.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6bde7b2c..5f975112 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,10 +37,10 @@ importers: dependencies: '@anthropic-ai/sdk': specifier: ^0.98.0 - version: 0.98.0(zod@3.25.76) + version: 0.98.0(zod@4.4.3) '@google/genai': specifier: ^1.52.0 - version: 1.52.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(utf-8-validate@6.0.6) + version: 1.52.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@6.0.6) '@metaplex-foundation/mpl-agent-registry': specifier: ^0.2.5 version: 0.2.5(@metaplex-foundation/umi@1.5.1)(@noble/hashes@1.8.0) @@ -56,6 +56,9 @@ importers: '@meteora-ag/dynamic-bonding-curve-sdk': specifier: 1.5.7 version: 1.5.7(bufferutil@4.1.0)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6) + '@modelcontextprotocol/sdk': + specifier: 1.29.0 + version: 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3) '@nirholas/pump-sdk': specifier: 1.30.0 version: 1.30.0(bufferutil@4.1.0)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(puppeteer-core@24.40.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(typescript@5.9.3)(utf-8-validate@6.0.6) @@ -76,7 +79,7 @@ importers: version: 6.9.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6) '@solana/mpp': specifier: ^0.6.0 - version: 0.6.0(@solana/kit@6.9.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6))(mppx@0.5.5(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(express@5.2.1)(hono@4.12.21)(typescript@5.9.3)(viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))) + version: 0.6.0(@solana/kit@6.9.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6))(mppx@0.5.5(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(express@5.2.1)(hono@4.12.21)(typescript@5.9.3)(viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))) '@solana/spl-token': specifier: ^0.4.9 version: 0.4.14(@solana/web3.js@1.98.4(bufferutil@4.1.0)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6))(bufferutil@4.1.0)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6) @@ -7048,13 +7051,6 @@ snapshots: '@adraffy/ens-normalize@1.11.1': {} - '@anthropic-ai/sdk@0.98.0(zod@3.25.76)': - dependencies: - json-schema-to-ts: 3.1.1 - standardwebhooks: 1.0.0 - optionalDependencies: - zod: 3.25.76 - '@anthropic-ai/sdk@0.98.0(zod@4.4.3)': dependencies: json-schema-to-ts: 3.1.1 @@ -8206,14 +8202,14 @@ snapshots: chalk: 4.1.2 js-yaml: 4.1.1 - '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)(utf-8-validate@6.0.6)': + '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@6.0.6)': dependencies: google-auth-library: 10.6.2 p-retry: 4.6.2 protobufjs: 7.5.8 ws: 8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) optionalDependencies: - '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) + '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3) transitivePeerDependencies: - bufferutil - supports-color @@ -8222,7 +8218,6 @@ snapshots: '@hono/node-server@1.19.14(hono@4.12.21)': dependencies: hono: 4.12.21 - optional: true '@isaacs/fs-minipass@4.0.1': dependencies: @@ -8420,7 +8415,7 @@ snapshots: - fastestsmallesttextencoderdecoder - utf-8-validate - '@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76)': + '@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3)': dependencies: '@hono/node-server': 1.19.14(hono@4.12.21) ajv: 8.20.0 @@ -8437,13 +8432,12 @@ snapshots: json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 raw-body: 3.0.2 - zod: 3.25.76 - zod-to-json-schema: 3.25.2(zod@3.25.76) + zod: 4.4.3 + zod-to-json-schema: 3.25.2(zod@4.4.3) optionalDependencies: '@cfworker/json-schema': 4.1.1 transitivePeerDependencies: - supports-color - optional: true '@modelcontextprotocol/server@2.0.0-alpha.2(@cfworker/json-schema@4.1.1)': dependencies: @@ -9157,7 +9151,7 @@ snapshots: '@signinwithethereum/siwe-parser': 4.2.0 optionalDependencies: ethers: 6.16.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) - viem: 2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.4.3) '@simple-git/args-pathspec@1.0.3': {} @@ -9434,13 +9428,13 @@ snapshots: - fastestsmallesttextencoderdecoder - utf-8-validate - '@solana/mpp@0.6.0(@solana/kit@6.9.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6))(mppx@0.5.5(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(express@5.2.1)(hono@4.12.21)(typescript@5.9.3)(viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)))': + '@solana/mpp@0.6.0(@solana/kit@6.9.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6))(mppx@0.5.5(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(express@5.2.1)(hono@4.12.21)(typescript@5.9.3)(viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)))': dependencies: '@solana-program/compute-budget': 0.15.0(@solana/kit@6.9.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6)) '@solana-program/system': 0.12.2(@solana/kit@6.9.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6)) '@solana-program/token': 0.11.0(@solana/kit@6.9.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6)) '@solana/kit': 6.9.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6) - mppx: 0.5.5(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(express@5.2.1)(hono@4.12.21)(typescript@5.9.3)(viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + mppx: 0.5.5(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(express@5.2.1)(hono@4.12.21)(typescript@5.9.3)(viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) '@solana/nominal-types@6.9.0(typescript@5.9.3)': optionalDependencies: @@ -10156,7 +10150,7 @@ snapshots: ajv: 8.20.0 jose: 5.10.0 tweetnacl: 1.0.3 - viem: 2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.4.3) zod: 3.25.76 transitivePeerDependencies: - bufferutil @@ -10249,7 +10243,6 @@ snapshots: ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 - optional: true ajv-keywords@3.5.2(ajv@6.15.0): dependencies: @@ -10915,7 +10908,6 @@ snapshots: dependencies: object-assign: 4.1.1 vary: 1.1.2 - optional: true cosmiconfig@9.0.1(typescript@5.9.3): dependencies: @@ -11350,13 +11342,11 @@ snapshots: transitivePeerDependencies: - bare-abort-controller - eventsource-parser@3.1.0: - optional: true + eventsource-parser@3.1.0: {} eventsource@3.0.7: dependencies: eventsource-parser: 3.1.0 - optional: true expand-template@2.0.3: {} @@ -11976,8 +11966,7 @@ snapshots: dependencies: hermes-estree: 0.35.0 - hono@4.12.21: - optional: true + hono@4.12.21: {} hosted-git-info@4.1.0: dependencies: @@ -12239,8 +12228,7 @@ snapshots: jose@5.10.0: {} - jose@6.2.3: - optional: true + jose@6.2.3: {} js-sha256@0.11.1: {} @@ -12273,8 +12261,7 @@ snapshots: json-schema-traverse@1.0.0: {} - json-schema-typed@8.0.2: - optional: true + json-schema-typed@8.0.2: {} json-stable-stringify@1.3.0: dependencies: @@ -13288,16 +13275,16 @@ snapshots: dompurify: 3.4.0 marked: 14.0.0 - mppx@0.5.5(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(express@5.2.1)(hono@4.12.21)(typescript@5.9.3)(viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)): + mppx@0.5.5(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(express@5.2.1)(hono@4.12.21)(typescript@5.9.3)(viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)): dependencies: '@remix-run/fetch-proxy': 0.7.1 '@remix-run/node-fetch-server': 0.13.3 incur: 0.3.25 ox: 0.14.7(typescript@5.9.3)(zod@4.4.3) - viem: 2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) + viem: 2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.4.3) zod: 4.4.3 optionalDependencies: - '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) + '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3) express: 5.2.1 hono: 4.12.21 transitivePeerDependencies: @@ -13623,8 +13610,7 @@ snapshots: dependencies: pngjs: 7.0.0 - pkce-challenge@5.0.1: - optional: true + pkce-challenge@5.0.1: {} playwright-core@1.59.0: {} @@ -14930,7 +14916,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76): + viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.4.3): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 @@ -15150,10 +15136,9 @@ snapshots: yocto-queue@0.1.0: {} - zod-to-json-schema@3.25.2(zod@3.25.76): + zod-to-json-schema@3.25.2(zod@4.4.3): dependencies: - zod: 3.25.76 - optional: true + zod: 4.4.3 zod@3.25.76: {} diff --git a/scripts/smoke/bridge-shim.mjs b/scripts/smoke/bridge-shim.mjs new file mode 100644 index 00000000..f020964f --- /dev/null +++ b/scripts/smoke/bridge-shim.mjs @@ -0,0 +1,163 @@ +/** + * Bridge shim smoke — drives the built MCP shim (dist-bridge/daemon-bridge-shim.mjs) + * over real stdio against an in-process fake of the DAEMON bridge server. + * No Electron involved: this validates the shim half of the contract — + * MCP handshake, tools/list from the live endpoint, tools/call round-trip + * (including bearer auth), and the "DAEMON is not running" path. + * + * Run: pnpm run test:bridge-shim + */ +import assert from 'node:assert/strict' +import { spawn } from 'node:child_process' +import http from 'node:http' +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import path from 'node:path' +import { fileURLToPath } from 'node:url' + +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const repoRoot = path.resolve(__dirname, '..', '..') +const shimPath = path.join(repoRoot, 'dist-bridge', 'daemon-bridge-shim.mjs') + +const TOKEN = 'f'.repeat(64) +const TOOLS = [ + { name: 'read_wallet', description: 'List wallets', risk: 'read', inputSchema: { type: 'object', properties: {} } }, + { name: 'remember_fact', description: 'Store a fact', risk: 'write', inputSchema: { type: 'object', properties: { title: { type: 'string' }, value: { type: 'string' } }, required: ['title', 'value'] } }, +] + +function log(message) { + console.log(`[bridge-shim-smoke] ${message}`) +} + +// --- Fake DAEMON bridge server --------------------------------------------- +const seenCalls = [] +const fakeServer = http.createServer((req, res) => { + const send = (code, payload) => { + res.writeHead(code, { 'content-type': 'application/json' }) + res.end(JSON.stringify(payload)) + } + const authed = req.headers.authorization === `Bearer ${TOKEN}` + if (req.method === 'GET' && req.url === '/bridge/ping') { + return send(200, { ok: true, data: { app: 'daemon', version: 'smoke', running: true } }) + } + if (!authed) return send(404, { ok: false, error: 'Not found' }) + if (req.method === 'GET' && req.url === '/bridge/tools') { + return send(200, { ok: true, data: TOOLS }) + } + if (req.method === 'POST' && req.url === '/bridge/call') { + let body = '' + req.on('data', (chunk) => { body += chunk }) + req.on('end', () => { + const call = JSON.parse(body) + seenCalls.push(call) + if (call.toolName === 'remember_fact') { + return send(200, { ok: true, data: { status: 'rejected', summary: 'User rejected this action.' } }) + } + return send(200, { ok: true, data: { status: 'done', summary: '2 wallets', result: { count: 2 } } }) + }) + return + } + return send(404, { ok: false, error: 'Not found' }) +}) + +await new Promise((resolve) => fakeServer.listen(0, '127.0.0.1', resolve)) +const port = fakeServer.address().port +log(`fake bridge listening on ${port}`) + +// --- Temp bridge.json -------------------------------------------------------- +const sandbox = mkdtempSync(path.join(tmpdir(), 'daemon-bridge-smoke-')) +const infoFile = path.join(sandbox, 'bridge.json') +writeFileSync(infoFile, JSON.stringify({ token: TOKEN, port, pid: 0, version: 'smoke', updatedAt: Date.now() })) + +// --- Spawn shim and speak newline-delimited JSON-RPC ------------------------- +const shim = spawn(process.execPath, [shimPath], { + env: { ...process.env, DAEMON_BRIDGE_INFO: infoFile }, + stdio: ['pipe', 'pipe', 'pipe'], +}) +shim.stderr.on('data', (chunk) => process.stderr.write(`[shim] ${chunk}`)) + +let buffer = '' +const pending = new Map() +shim.stdout.on('data', (chunk) => { + buffer += chunk.toString() + let index + while ((index = buffer.indexOf('\n')) >= 0) { + const line = buffer.slice(0, index).trim() + buffer = buffer.slice(index + 1) + if (!line) continue + const message = JSON.parse(line) + if (message.id !== undefined && pending.has(message.id)) { + pending.get(message.id)(message) + pending.delete(message.id) + } + } +}) + +let nextId = 0 +function rpc(method, params, timeoutMs = 15_000) { + const id = ++nextId + const payload = { jsonrpc: '2.0', id, method, params } + shim.stdin.write(JSON.stringify(payload) + '\n') + return new Promise((resolve, reject) => { + const timer = setTimeout(() => reject(new Error(`timeout waiting for ${method}`)), timeoutMs) + pending.set(id, (message) => { clearTimeout(timer); resolve(message) }) + }) +} + +function notify(method, params) { + shim.stdin.write(JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n') +} + +let exitCode = 0 +try { + // 1. MCP handshake + const init = await rpc('initialize', { + protocolVersion: '2024-11-05', + capabilities: {}, + clientInfo: { name: 'bridge-shim-smoke', version: '1.0.0' }, + }) + assert.equal(init.result.serverInfo.name, 'daemon-bridge') + notify('notifications/initialized', {}) + log('handshake ok') + + // 2. tools/list reflects the live endpoint, with approval note on write tools + const list = await rpc('tools/list', {}) + const names = list.result.tools.map((t) => t.name) + assert.deepEqual(names, ['read_wallet', 'remember_fact']) + assert.match( + list.result.tools.find((t) => t.name === 'remember_fact').description, + /Requires user approval inside the DAEMON app/, + ) + log(`tools/list ok (${names.join(', ')})`) + + // 3. tools/call round-trip carries cwd + auth and renders the result + const call = await rpc('tools/call', { name: 'read_wallet', arguments: {} }) + assert.equal(call.result.isError, undefined) + assert.match(call.result.content[0].text, /2 wallets/) + assert.equal(seenCalls[0].toolName, 'read_wallet') + assert.ok(typeof seenCalls[0].cwd === 'string' && seenCalls[0].cwd.length > 0, 'cwd forwarded') + log('tools/call ok') + + // 4. rejection surfaces as a clear isError result + const rejected = await rpc('tools/call', { name: 'remember_fact', arguments: { title: 't', value: 'v' } }) + assert.equal(rejected.result.isError, true) + assert.match(rejected.result.content[0].text, /rejected this action in DAEMON/) + log('rejection path ok') + + // 5. DAEMON down → clear "not running" error + await new Promise((resolve) => fakeServer.close(resolve)) + const down = await rpc('tools/call', { name: 'read_wallet', arguments: {} }) + assert.equal(down.result.isError, true) + assert.match(down.result.content[0].text, /DAEMON is not running/) + log('not-running path ok') + + log('PASS') +} catch (error) { + exitCode = 1 + console.error('[bridge-shim-smoke] FAIL:', error) +} finally { + shim.kill() + await new Promise((resolve) => fakeServer.close(() => resolve())).catch(() => {}) + rmSync(sandbox, { recursive: true, force: true }) +} +process.exit(exitCode) diff --git a/src/App.tsx b/src/App.tsx index 27b0e2fd..0ea122d8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,6 +12,7 @@ import { OnboardingWizard } from './panels/Onboarding/OnboardingWizard' import { LaunchWizard } from './panels/LaunchWizard/LaunchWizard' import { TourOverlay } from './components/Tour/TourOverlay' import { ToastHost } from './components/ToastHost' +import { BridgeApprovalHost } from './components/BridgeApprovalHost' import { ConfirmDialog } from './components/ConfirmDialog' import { KeyboardShortcutsOverlay } from './components/KeyboardShortcutsOverlay' import { useNotificationsStore } from './store/notifications' @@ -657,6 +658,7 @@ function App() { {tourActive && } {showShortcuts && setShowShortcuts(false)} />} + diff --git a/src/components/BridgeApprovalHost.module.css b/src/components/BridgeApprovalHost.module.css new file mode 100644 index 00000000..d4d297c5 --- /dev/null +++ b/src/components/BridgeApprovalHost.module.css @@ -0,0 +1,39 @@ +/* Fixed overlay for external-agent (bridge) approval requests. Sits above the + workspace but below toasts; cards reuse the AgentWorkbench approval styles. */ +.host { + position: fixed; + right: var(--space-lg); + bottom: calc(var(--space-2xl) + var(--space-sm)); + width: 320px; + z-index: 900; + display: flex; + flex-direction: column; + gap: var(--space-sm); +} + +.header { + display: flex; + align-items: center; + gap: var(--space-sm); + padding: var(--space-sm) var(--space-md); + background: var(--bg-overlay); + border: 1px solid var(--s6); + border-radius: var(--radius-md); + color: var(--t1); + font-size: var(--fs-12); + font-weight: 600; +} + +.headerDot { + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--amber); + flex: none; +} + +.headerHint { + color: var(--t3); + font-weight: 400; + margin-left: auto; +} diff --git a/src/components/BridgeApprovalHost.tsx b/src/components/BridgeApprovalHost.tsx new file mode 100644 index 00000000..a68b5e35 --- /dev/null +++ b/src/components/BridgeApprovalHost.tsx @@ -0,0 +1,31 @@ +import { useEffect } from 'react' +import { useBridgeStore } from '../store/bridge' +import { ApprovalCard } from '../panels/AgentWorkbench/ApprovalCard' +// The approval card styles live in the (lazy-loaded) workbench stylesheet — +// import it here so bridge approvals render even if the console never opened. +import '../panels/AgentWorkbench/AgentWorkbench.css' +import styles from './BridgeApprovalHost.module.css' + +/** Overlay for approval requests arriving from external MCP agents (Claude + * Code, Cursor) via the DAEMON Bridge. Mounted once at the app root. */ +export function BridgeApprovalHost() { + const approvals = useBridgeStore((s) => s.approvals) + const approve = useBridgeStore((s) => s.approve) + const subscribe = useBridgeStore((s) => s.subscribe) + + useEffect(() => subscribe(), [subscribe]) + + if (approvals.length === 0) return null + return ( +
+
+ + External agent request + via DAEMON Bridge +
+ {approvals.map((approval) => ( + + ))} +
+ ) +} diff --git a/src/panels/AgentWorkbench/ApprovalCard.tsx b/src/panels/AgentWorkbench/ApprovalCard.tsx index 3e274ea6..628eca68 100644 --- a/src/panels/AgentWorkbench/ApprovalCard.tsx +++ b/src/panels/AgentWorkbench/ApprovalCard.tsx @@ -7,8 +7,13 @@ const RISK_LABEL: Record = { sensitive: 'SENSITIVE', } -export function ApprovalCard({ approval }: { approval: AriaApproval }) { - const approve = useAriaStore((s) => s.approve) +export function ApprovalCard({ approval, onDecide }: { + approval: AriaApproval + /** Override the decision sink (the bridge surface routes to bridge:approve). Defaults to the ARIA store. */ + onDecide?: (callId: string, approved: boolean) => void +}) { + const ariaApprove = useAriaStore((s) => s.approve) + const approve = onDecide ?? ariaApprove const [typed, setTyped] = useState('') // Plan-mode gate: one approval to run the whole plan (sentinel name). diff --git a/src/panels/SettingsPanel/BridgeSection.tsx b/src/panels/SettingsPanel/BridgeSection.tsx new file mode 100644 index 00000000..b6bb7fe0 --- /dev/null +++ b/src/panels/SettingsPanel/BridgeSection.tsx @@ -0,0 +1,101 @@ +import { useCallback, useEffect, useState } from 'react' +import { daemon } from '../../lib/daemonBridge' +import { runIpc } from '../../lib/runIpc' +import { useBridgeStore } from '../../store/bridge' + +/** + * DAEMON Bridge settings — status of the loopback MCP bridge, token rotation, + * and per-project registration so external agents (Claude Code, Cursor) can + * call DAEMON's gated tools. Rendered inside the Integrations tab; reuses the + * global settings-* classes from SettingsPanel.css. + */ +export function BridgeSection({ projectPath }: { projectPath: string | null }) { + const [status, setStatus] = useState(null) + const [busy, setBusy] = useState(false) + const [registered, setRegistered] = useState(false) + const activity = useBridgeStore((s) => s.activity) + + const load = useCallback(async () => { + const data = await runIpc(daemon.bridge.status(), { context: 'Bridge', silent: true }) + if (data) setStatus(data) + }, []) + + useEffect(() => { void load() }, [load]) + + const handleRotate = async () => { + setBusy(true) + const data = await runIpc(daemon.bridge.rotateToken(), { context: 'Bridge' }) + if (data) setStatus(data) + setBusy(false) + } + + const handleRegister = async () => { + if (!projectPath) return + setBusy(true) + const data = await runIpc(daemon.bridge.registerProject(projectPath), { context: 'Bridge' }) + if (data) { + setStatus(data) + setRegistered(true) + } + setBusy(false) + } + + return ( +
+
DAEMON Bridge
+
+ Lets agents in other tools (Claude Code, Cursor) call DAEMON's wallet, launch, and + memory tools over MCP. Every write action still requires your approval inside DAEMON; + sensitive actions keep typed confirmation. +
+ +
+
+
+ + {status?.running ? `Listening on 127.0.0.1:${status.port}` : status?.error ?? 'Not running'} + + + {status ? `${status.toolCount} tools exposed` : ''} + +
+ {status?.tokenFile ? ( +
+
+ Token: {status.tokenFile} +
+ ) : null} +
+ +
+ + +
+ + {activity.length > 0 ? ( + <> +
Recent bridge calls
+
+ {activity.slice(0, 8).map((entry) => ( +
+
+ {entry.name} + {entry.summary} +
+ ))} +
+ + ) : null} +
+ ) +} diff --git a/src/panels/SettingsPanel/SettingsPanel.tsx b/src/panels/SettingsPanel/SettingsPanel.tsx index 5ed15878..de6c30e1 100644 --- a/src/panels/SettingsPanel/SettingsPanel.tsx +++ b/src/panels/SettingsPanel/SettingsPanel.tsx @@ -10,6 +10,7 @@ import { Toggle } from '../../components/Toggle' import { PanelHeader } from '../../components/Panel' import { BUILTIN_TOOLS, TOOL_NAMES } from '../../components/CommandDrawer/CommandDrawer' import { KeyboardShortcuts } from '../../components/KeyboardShortcuts' +import { BridgeSection } from './BridgeSection' import { NavigationGuide } from '../../components/NavigationGuide' import { isToolDisableable } from '../../constants/toolRegistry' import { @@ -75,7 +76,7 @@ interface AgentRow { // Lookup table mapping common search keywords to the tab they live in const SEARCH_INDEX: { tab: SettingsTab; keywords: string[] }[] = [ { tab: 'keys', keywords: ['key', 'api', 'token', 'secret', 'helius', 'openai', 'anthropic', 'birdeye', 'gemini'] }, - { tab: 'integrations', keywords: ['integration', 'claude', 'codex', 'mcp', 'voight', 'observability', 'sign in', 'login', 'connect', 'subscription', 'cli'] }, + { tab: 'integrations', keywords: ['integration', 'claude', 'codex', 'mcp', 'voight', 'observability', 'sign in', 'login', 'connect', 'subscription', 'cli', 'bridge', 'cursor', 'external agent'] }, { tab: 'aiProviders', keywords: ['ai provider', 'provider', 'default provider', 'aria', 'daemon ai', 'codex', 'claude', 'model'] }, { tab: 'agents', keywords: ['agent', 'provider', 'default provider', 'model', 'system prompt'] }, { tab: 'tools', keywords: ['tool', 'tools', 'extra tools', 'disable tools', 'sidebar', 'command drawer', 'profile', 'workspace'] }, @@ -196,7 +197,12 @@ export function SettingsPanel() {
{tab === 'keys' && } - {tab === 'integrations' && } + {tab === 'integrations' && ( + <> + + + + )} {tab === 'aiProviders' && } {tab === 'agents' && } {tab === 'tools' && } diff --git a/src/store/bridge.ts b/src/store/bridge.ts new file mode 100644 index 00000000..85e13622 --- /dev/null +++ b/src/store/bridge.ts @@ -0,0 +1,46 @@ +import { create } from 'zustand' +import type { BridgeToolEvent } from '../../electron/shared/types' +import { daemon } from '../lib/daemonBridge' +import type { AriaApproval } from './aria' + +const MAX_ACTIVITY = 50 + +interface BridgeState { + /** External-agent approvals awaiting a decision, oldest first. */ + approvals: AriaApproval[] + /** Recent completed bridge calls, newest first (activity feed for Settings). */ + activity: Array> + approve: (callId: string, approved: boolean) => void + subscribe: () => () => void +} + +export const useBridgeStore = create((set) => ({ + approvals: [], + activity: [], + + approve: (callId, approved) => { + daemon.bridge.approve(callId, approved) + set((s) => ({ approvals: s.approvals.filter((a) => a.callId !== callId) })) + }, + + subscribe: () => { + return daemon.bridge.onEvent((event) => { + switch (event.kind) { + case 'approval-request': + set((s) => ({ + approvals: [...s.approvals, { + callId: event.callId, name: event.name, risk: event.risk, + summary: event.summary, input: event.input, + }], + })) + break + case 'approval-expired': + set((s) => ({ approvals: s.approvals.filter((a) => a.callId !== event.callId) })) + break + case 'call': + set((s) => ({ activity: [event, ...s.activity].slice(0, MAX_ACTIVITY) })) + break + } + }) + }, +})) diff --git a/src/types/daemon.d.ts b/src/types/daemon.d.ts index 1459c31e..be9cd7dd 100644 --- a/src/types/daemon.d.ts +++ b/src/types/daemon.d.ts @@ -542,6 +542,8 @@ declare global { type AriaPlanStep = import('../../electron/shared/types').AriaPlanStep type AriaPatchProposalLite = import('../../electron/shared/types').AriaPatchProposalLite type AriaPatchAction = import('../../electron/shared/types').AriaPatchAction + type BridgeToolEvent = import('../../electron/shared/types').BridgeToolEvent + type BridgeStatus = import('../../electron/shared/types').BridgeStatus type DaemonAiModelLane = import('../../electron/shared/types').DaemonAiModelLane type OnboardingProgress = import('../../electron/shared/types').OnboardingProgress type OnboardingStepStatus = import('../../electron/shared/types').OnboardingStepStatus @@ -1092,6 +1094,14 @@ declare global { onUiEffect: (handler: (payload: { callId: string; effect: AriaUiEffectPayload; awaitData: boolean }) => void) => () => void } + interface DaemonBridge { + status: () => Promise> + rotateToken: () => Promise> + registerProject: (projectPath: string) => Promise> + approve: (callId: string, approved: boolean) => void + onEvent: (handler: (event: BridgeToolEvent) => void) => () => void + } + interface SwarmRun { id: string session_id: string | null @@ -1767,6 +1777,7 @@ declare global { email: DaemonEmail images: DaemonImages aria: DaemonAria + bridge: DaemonBridge swarm: DaemonSwarm memory: DaemonMemory launch: DaemonLaunch diff --git a/test/panels/BridgeApprovalHost.dom.test.tsx b/test/panels/BridgeApprovalHost.dom.test.tsx new file mode 100644 index 00000000..22af7409 --- /dev/null +++ b/test/panels/BridgeApprovalHost.dom.test.tsx @@ -0,0 +1,83 @@ +// @vitest-environment happy-dom + +import { render, screen, act } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { BridgeApprovalHost } from '../../src/components/BridgeApprovalHost' +import { useBridgeStore } from '../../src/store/bridge' +import type { BridgeToolEvent } from '../../electron/shared/types' + +let emitBridgeEvent: (event: BridgeToolEvent) => void = () => {} +const approveSpy = vi.fn() + +beforeEach(() => { + approveSpy.mockClear() + ;(window as unknown as { daemon: unknown }).daemon = { + bridge: { + approve: approveSpy, + onEvent: (handler: (event: BridgeToolEvent) => void) => { + emitBridgeEvent = handler + return () => { emitBridgeEvent = () => {} } + }, + }, + } + useBridgeStore.setState({ approvals: [], activity: [] }) +}) + +function pushApproval(partial: Partial> = {}) { + act(() => { + emitBridgeEvent({ + kind: 'approval-request', + callId: 'call-1', + name: 'remember_fact', + risk: 'write', + summary: 'remember_fact: Use pnpm', + input: { title: 'Use pnpm' }, + source: 'bridge', + ...partial, + }) + }) +} + +describe('BridgeApprovalHost', () => { + it('renders nothing until an external approval arrives', () => { + const { container } = render() + expect(container.firstChild).toBeNull() + + pushApproval() + expect(screen.getByText('External agent request')).toBeTruthy() + expect(screen.getByText('remember_fact: Use pnpm')).toBeTruthy() + }) + + it('routes approve to daemon.bridge.approve and drops the card', async () => { + render() + pushApproval() + + await userEvent.click(screen.getByRole('button', { name: 'Approve' })) + expect(approveSpy).toHaveBeenCalledWith('call-1', true) + expect(screen.queryByText('External agent request')).toBeNull() + }) + + it('requires typed confirmation for sensitive tools', async () => { + render() + pushApproval({ callId: 'call-2', name: 'generate_wallet', risk: 'sensitive', summary: 'generate_wallet: hot', input: { name: 'hot' } }) + + const approveBtn = screen.getByRole('button', { name: 'Approve' }) as HTMLButtonElement + expect(approveBtn.disabled).toBe(true) + + await userEvent.type(screen.getByPlaceholderText('hot'), 'hot') + expect((screen.getByRole('button', { name: 'Approve' }) as HTMLButtonElement).disabled).toBe(false) + + await userEvent.click(screen.getByRole('button', { name: 'Approve' })) + expect(approveSpy).toHaveBeenCalledWith('call-2', true) + }) + + it('removes the card when the approval expires server-side', () => { + render() + pushApproval() + expect(screen.getByText('External agent request')).toBeTruthy() + + act(() => emitBridgeEvent({ kind: 'approval-expired', callId: 'call-1' })) + expect(screen.queryByText('External agent request')).toBeNull() + }) +}) diff --git a/test/services/AriaAgentService.executeToolCall.test.ts b/test/services/AriaAgentService.executeToolCall.test.ts new file mode 100644 index 00000000..f9d6f728 --- /dev/null +++ b/test/services/AriaAgentService.executeToolCall.test.ts @@ -0,0 +1,109 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { AriaTransport } from '../../electron/services/AriaAgentService' + +// Mock the operator loop's heavy import chain — executeToolCall only needs the +// tool catalog, the risk gate, and describeIntent's clusterMark. +vi.mock('../../electron/db/db', () => ({ getDb: vi.fn() })) +vi.mock('../../electron/services/providers/ClaudeProvider', () => ({ runClaudeAgentTurn: vi.fn() })) +vi.mock('../../electron/services/providers/ProviderRegistry', () => ({ getFeatureProvider: vi.fn() })) +vi.mock('../../electron/services/providers/glmConfig', () => ({ + resolveOperatorBackend: vi.fn(), + getGlmEndpoint: vi.fn(), +})) +vi.mock('../../electron/services/DaemonAIService', () => ({ recordLocalAiUsage: vi.fn() })) +vi.mock('../../electron/services/MemoryService', () => ({ createSuggestion: vi.fn() })) +vi.mock('../../electron/services/aria/contextAssembler', () => ({ assembleSystemPrompt: vi.fn(() => '') })) +vi.mock('../../electron/services/aria/patchUtils', () => ({ + laneToClaudeModel: vi.fn(), + buildPlanSteps: vi.fn(() => []), + buildPatchProposal: vi.fn(), +})) + +const isMainnet = vi.fn(() => false) +vi.mock('../../electron/services/aria/tools/shared', () => ({ + clusterMark: (summary: string) => (isMainnet() ? `[MAINNET] ${summary}` : summary), +})) + +const writeHandler = vi.fn(async () => ({ ok: true, summary: 'fact stored' })) +const sensitiveHandler = vi.fn(async () => ({ ok: true, summary: 'wallet created' })) +const readHandler = vi.fn(async () => ({ ok: true, summary: 'two wallets' })) + +vi.mock('../../electron/services/aria/toolCatalog', () => { + const tools = [ + { name: 'read_tool', description: '', kind: 'read', risk: 'read', input: {}, handler: (...a: unknown[]) => readHandler(...a) }, + { name: 'write_tool', description: '', kind: 'edit', risk: 'write', input: {}, handler: (...a: unknown[]) => writeHandler(...a) }, + { name: 'sensitive_tool', description: '', kind: 'run', risk: 'sensitive', input: {}, handler: (...a: unknown[]) => sensitiveHandler(...a) }, + ] + return { ARIA_TOOLS: tools, getTool: (name: string) => tools.find((t) => t.name === name) } +}) + +import { executeToolCall } from '../../electron/services/AriaAgentService' + +const snapshot = { + activeProjectId: null, + activeProjectPath: null, + currentPanelId: null, + openFilePath: null, + chips: { activeFile: false, projectTree: false, gitDiff: false, terminalLogs: false, walletContext: false }, +} + +function makeTransport(approve: boolean): AriaTransport & { requestApproval: ReturnType } { + return { + emit: vi.fn(), + requestApproval: vi.fn(async () => approve), + requestPatchDecision: vi.fn(async () => 'discard' as const), + runUiEffect: vi.fn(async () => undefined), + } +} + +function ctx(transport: AriaTransport) { + return { sessionId: 'bridge:test', snapshot, runUiEffect: transport.runUiEffect } +} + +beforeEach(() => { + vi.clearAllMocks() + isMainnet.mockReturnValue(false) +}) + +describe('executeToolCall (bridge entry into the risk gate)', () => { + it('runs read tools without requesting approval', async () => { + const transport = makeTransport(true) + const record = await executeToolCall({ id: 'c1', name: 'read_tool', input: {} }, ctx(transport), transport) + + expect(record.status).toBe('done') + expect(transport.requestApproval).not.toHaveBeenCalled() + expect(readHandler).toHaveBeenCalled() + }) + + it('always gates write tools — planApproved can never be smuggled in', async () => { + const transport = makeTransport(true) + const record = await executeToolCall({ id: 'c2', name: 'write_tool', input: { title: 'x' } }, ctx(transport), transport) + + expect(transport.requestApproval).toHaveBeenCalledTimes(1) + expect(record.status).toBe('done') + }) + + it('returns rejected without running the handler when the user declines', async () => { + const transport = makeTransport(false) + const record = await executeToolCall({ id: 'c3', name: 'sensitive_tool', input: { name: 'w' } }, ctx(transport), transport) + + expect(record.status).toBe('rejected') + expect(sensitiveHandler).not.toHaveBeenCalled() + }) + + it('marks gated approval summaries with [MAINNET] on mainnet', async () => { + isMainnet.mockReturnValue(true) + const transport = makeTransport(true) + await executeToolCall({ id: 'c4', name: 'sensitive_tool', input: { name: 'hot' } }, ctx(transport), transport) + + const req = transport.requestApproval.mock.calls[0][0] as { summary: string } + expect(req.summary).toMatch(/^\[MAINNET\] /) + }) + + it('never cluster-marks read tools and returns unknown-tool errors cleanly', async () => { + const transport = makeTransport(true) + const record = await executeToolCall({ id: 'c5', name: 'nope', input: {} }, ctx(transport), transport) + expect(record.status).toBe('error') + expect(record.summary).toContain('Unknown tool') + }) +}) diff --git a/test/services/BridgeServerService.test.ts b/test/services/BridgeServerService.test.ts new file mode 100644 index 00000000..c1002d10 --- /dev/null +++ b/test/services/BridgeServerService.test.ts @@ -0,0 +1,140 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { BridgeCallResult, BridgeToolDescriptor } from '../../electron/shared/types' + +// The server only pulls getApprovalTimeoutMs from the gateway; mock it so this +// test never loads the AriaAgentService import chain. +vi.mock('../../electron/services/bridge/BridgeToolGateway', () => ({ + getApprovalTimeoutMs: () => 120_000, +})) + +import { + getBridgeStatus, + startBridgeServer, + stopBridgeServer, + type BridgeServerOptions, +} from '../../electron/services/bridge/BridgeServerService' + +const TOKEN = 'a'.repeat(64) +const TOOLS: BridgeToolDescriptor[] = [ + { name: 'read_wallet', description: 'Read wallets', risk: 'read', inputSchema: { type: 'object', properties: {} } }, +] + +function makeOptions(overrides: Partial = {}): BridgeServerOptions { + return { + port: 0, + token: TOKEN, + tokenFile: 'C:/tmp/bridge.json', + version: '0.0.0-test', + listTools: () => TOOLS, + executeCall: async () => ({ status: 'done', summary: 'ok' } satisfies BridgeCallResult), + ...overrides, + } +} + +async function request( + port: number, + pathname: string, + init: { method?: string; token?: string; origin?: string; body?: unknown } = {}, +) { + const headers: Record = { 'content-type': 'application/json' } + if (init.token) headers['authorization'] = `Bearer ${init.token}` + if (init.origin) headers['origin'] = init.origin + const res = await fetch(`http://127.0.0.1:${port}${pathname}`, { + method: init.method ?? 'GET', + headers, + body: init.body != null ? JSON.stringify(init.body) : undefined, + }) + return { status: res.status, json: await res.json().catch(() => null) as { ok?: boolean; data?: unknown; error?: string } | null } +} + +describe('BridgeServerService', () => { + afterEach(async () => { + await stopBridgeServer() + }) + + it('answers ping without auth and reports status', async () => { + const status = await startBridgeServer(makeOptions()) + expect(status.running).toBe(true) + expect(status.toolCount).toBe(1) + + const res = await request(status.port, '/bridge/ping') + expect(res.status).toBe(200) + expect((res.json?.data as { app?: string })?.app).toBe('daemon') + }) + + it('returns a generic 404 for missing or wrong bearer tokens', async () => { + const { port } = await startBridgeServer(makeOptions()) + + const missing = await request(port, '/bridge/tools') + expect(missing.status).toBe(404) + + const wrong = await request(port, '/bridge/tools', { token: 'b'.repeat(64) }) + expect(wrong.status).toBe(404) + + const right = await request(port, '/bridge/tools', { token: TOKEN }) + expect(right.status).toBe(200) + expect(right.json?.data).toEqual(TOOLS) + }) + + it('rejects any request carrying a browser Origin header', async () => { + const { port } = await startBridgeServer(makeOptions()) + const res = await request(port, '/bridge/tools', { token: TOKEN, origin: 'http://localhost:5173' }) + expect(res.status).toBe(403) + }) + + it('round-trips a call through the executor', async () => { + const executeCall = vi.fn(async () => ({ status: 'done', summary: 'balance: 1 SOL', result: { sol: 1 } } satisfies BridgeCallResult)) + const { port } = await startBridgeServer(makeOptions({ executeCall })) + + const res = await request(port, '/bridge/call', { + method: 'POST', + token: TOKEN, + body: { toolName: 'read_wallet', input: {}, cwd: 'C:/work/project' }, + }) + expect(res.status).toBe(200) + expect((res.json?.data as BridgeCallResult).summary).toBe('balance: 1 SOL') + expect(executeCall).toHaveBeenCalledWith({ toolName: 'read_wallet', input: {}, cwd: 'C:/work/project' }) + }) + + it('rejects malformed call bodies with 400', async () => { + const { port } = await startBridgeServer(makeOptions()) + const res = await request(port, '/bridge/call', { method: 'POST', token: TOKEN, body: { input: {} } }) + expect(res.status).toBe(400) + }) + + it('caps concurrent calls at 4 with 429', async () => { + let release: () => void = () => {} + const blocked = new Promise((resolve) => { release = resolve }) + const executeCall = vi.fn(async () => { + await blocked + return { status: 'done', summary: 'ok' } satisfies BridgeCallResult + }) + const { port } = await startBridgeServer(makeOptions({ executeCall })) + + const body = { toolName: 'read_wallet', input: {} } + const pending = Array.from({ length: 4 }, () => + request(port, '/bridge/call', { method: 'POST', token: TOKEN, body })) + // Give the four in-flight requests time to register before the fifth. + await vi.waitFor(() => expect(executeCall).toHaveBeenCalledTimes(4)) + + const fifth = await request(port, '/bridge/call', { method: 'POST', token: TOKEN, body }) + expect(fifth.status).toBe(429) + + release() + const results = await Promise.all(pending) + for (const res of results) expect(res.status).toBe(200) + }) + + it('responds 503 to in-flight calls on shutdown', async () => { + const executeCall = vi.fn(() => new Promise(() => {})) // never resolves + const { port } = await startBridgeServer(makeOptions({ executeCall })) + + const hanging = request(port, '/bridge/call', { method: 'POST', token: TOKEN, body: { toolName: 'read_wallet', input: {} } }) + await vi.waitFor(() => expect(executeCall).toHaveBeenCalled()) + + await stopBridgeServer() + const res = await hanging + expect(res.status).toBe(503) + expect(getBridgeStatus().running).toBe(false) + }) +}) diff --git a/test/services/BridgeToolGateway.test.ts b/test/services/BridgeToolGateway.test.ts new file mode 100644 index 00000000..7505714d --- /dev/null +++ b/test/services/BridgeToolGateway.test.ts @@ -0,0 +1,183 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' +import type { AriaToolCallRecord } from '../../electron/shared/types' + +// Fake catalog: one tool per risk tier, names matching the real allowlist so +// the allowlist ∩ catalog intersection is exercised. vi.hoisted so the vi.mock +// factories (hoisted above imports) can reference it. +const FAKE_TOOLS = vi.hoisted(() => [ + { name: 'read_wallet', description: 'Read wallets', kind: 'read', risk: 'read', input: { type: 'object', properties: {} } }, + { name: 'remember_fact', description: 'Store a fact', kind: 'edit', risk: 'write', input: { type: 'object', properties: { title: { type: 'string' } } } }, + { name: 'generate_wallet', description: 'New wallet', kind: 'run', risk: 'sensitive', input: { type: 'object', properties: { name: { type: 'string' } } } }, + { name: 'tokenlaunch_preflight', description: 'Preflight', kind: 'read', risk: 'read', input: { type: 'object', properties: {} } }, +]) + +vi.mock('../../electron/services/aria/toolCatalog', () => ({ + ARIA_TOOLS: FAKE_TOOLS, + getTool: (name: string) => FAKE_TOOLS.find((t) => t.name === name), +})) + +const executeToolCall = vi.hoisted(() => vi.fn()) +vi.mock('../../electron/services/AriaAgentService', () => ({ + executeToolCall: (...args: unknown[]) => executeToolCall(...args), +})) + +const getEnabledPacks = vi.hoisted(() => vi.fn<() => Record>(() => ({}))) +vi.mock('../../electron/services/SettingsService', () => ({ + getEnabledPacks: () => getEnabledPacks(), +})) + +const projectRows = vi.hoisted(() => [] as Array<{ id: string; path: string }>) +vi.mock('../../electron/db/db', () => ({ + getDb: () => ({ + prepare: () => ({ all: () => projectRows }), + }), +})) + +import { + executeBridgeCall, + findBridgeTool, + listBridgeTools, + resolveProjectForCwd, + type BridgeGatewayDeps, +} from '../../electron/services/bridge/BridgeToolGateway' + +function makeDeps(overrides: Partial = {}): BridgeGatewayDeps { + return { + requestApproval: vi.fn(async () => true), + cancelApproval: vi.fn(), + emit: vi.fn(), + approvalTimeoutMs: 50, + ...overrides, + } +} + +function doneRecord(partial: Partial = {}): AriaToolCallRecord { + return { + callId: 'call-1', name: 'read_wallet', toolKind: 'read', risk: 'read', + status: 'done', summary: 'ok', input: {}, ...partial, + } +} + +beforeEach(() => { + executeToolCall.mockReset() + getEnabledPacks.mockReturnValue({}) + projectRows.length = 0 +}) + +describe('listBridgeTools / findBridgeTool', () => { + it('exposes only allowlisted tools present in the catalog', () => { + const names = listBridgeTools().map((t) => t.name) + expect(names).toEqual(['read_wallet', 'generate_wallet', 'tokenlaunch_preflight', 'remember_fact']) + }) + + it('drops tools whose pack is disabled and explains on call', () => { + getEnabledPacks.mockReturnValue({ wallet: false }) + const names = listBridgeTools().map((t) => t.name) + expect(names).toEqual(['tokenlaunch_preflight', 'remember_fact']) + + const found = findBridgeTool('read_wallet') + expect(found.ok).toBe(false) + if (!found.ok) expect(found.error).toContain('wallet pack is disabled') + }) + + it('refuses tools outside the allowlist even if they exist in the catalog', () => { + const found = findBridgeTool('present_plan') + expect(found.ok).toBe(false) + if (!found.ok) expect(found.error).toContain('not exposed over the DAEMON Bridge') + }) +}) + +describe('resolveProjectForCwd', () => { + it('matches case-insensitively on win32 and prefers the longest prefix', () => { + projectRows.push( + { id: 'outer', path: 'C:\\Work' }, + { id: 'inner', path: 'C:\\Work\\daemon' }, + ) + expect(resolveProjectForCwd('c:\\work\\DAEMON\\src')?.id).toBe('inner') + expect(resolveProjectForCwd('C:\\Work\\other')?.id).toBe('outer') + expect(resolveProjectForCwd('D:\\elsewhere')).toBeNull() + expect(resolveProjectForCwd(undefined)).toBeNull() + }) + + it('does not match sibling directories sharing a name prefix', () => { + projectRows.push({ id: 'p', path: 'C:\\Work\\daemon' }) + expect(resolveProjectForCwd('C:\\Work\\daemon-other')).toBeNull() + }) +}) + +describe('executeBridgeCall', () => { + it('requires a resolved project for project-scoped tools', async () => { + const result = await executeBridgeCall( + { toolName: 'remember_fact', input: { title: 'x' }, cwd: 'D:\\nowhere' }, + makeDeps(), + ) + expect(result.status).toBe('error') + expect(result.summary).toContain('No DAEMON project matches') + expect(executeToolCall).not.toHaveBeenCalled() + }) + + it('runs through executeToolCall with a project-scoped snapshot', async () => { + projectRows.push({ id: 'proj-1', path: 'C:\\Work\\daemon' }) + executeToolCall.mockResolvedValue(doneRecord({ summary: '2 wallets', result: { count: 2 } })) + const deps = makeDeps() + + const result = await executeBridgeCall( + { toolName: 'read_wallet', input: {}, cwd: 'C:\\Work\\daemon\\packages' }, + deps, + ) + + expect(result).toEqual({ status: 'done', summary: '2 wallets', result: { count: 2 } }) + const [, ctx] = executeToolCall.mock.calls[0] as [unknown, { sessionId: string; snapshot: { activeProjectId: string | null } }] + expect(ctx.sessionId).toMatch(/^bridge:/) + expect(ctx.snapshot.activeProjectId).toBe('proj-1') + expect(deps.emit).toHaveBeenCalledWith(expect.objectContaining({ kind: 'call', status: 'done' })) + }) + + it('maps a user rejection to status rejected', async () => { + executeToolCall.mockImplementation(async (_use, _ctx, transport: { requestApproval: (r: unknown) => Promise }) => { + const approved = await transport.requestApproval({ callId: 'c1', name: 'remember_fact', risk: 'write', summary: 's', input: {} }) + return doneRecord(approved ? {} : { status: 'rejected', summary: 'User rejected this action.' }) + }) + const deps = makeDeps({ requestApproval: vi.fn(async () => false) }) + projectRows.push({ id: 'p', path: 'C:\\Work\\daemon' }) + + const result = await executeBridgeCall({ toolName: 'remember_fact', input: { title: 'x' }, cwd: 'C:\\Work\\daemon' }, deps) + expect(result.status).toBe('rejected') + }) + + it('auto-rejects as timeout when no one answers the approval', async () => { + executeToolCall.mockImplementation(async (_use, _ctx, transport: { requestApproval: (r: unknown) => Promise }) => { + const approved = await transport.requestApproval({ callId: 'c2', name: 'generate_wallet', risk: 'sensitive', summary: 's', input: {} }) + return doneRecord(approved ? {} : { status: 'rejected', summary: 'User rejected this action.' }) + }) + const deps = makeDeps({ + requestApproval: vi.fn(() => new Promise(() => {})), // never answered + approvalTimeoutMs: 30, + }) + + const result = await executeBridgeCall({ toolName: 'generate_wallet', input: { name: 'w' } }, deps) + expect(result.status).toBe('timeout') + expect(result.summary).toContain('Approval timed out') + expect(deps.cancelApproval).toHaveBeenCalledWith('c2') + }) + + it('hands tools a runUiEffect that throws (bridge tripwire)', async () => { + executeToolCall.mockImplementation(async (_use, ctx: { runUiEffect: (e: unknown, a: boolean) => Promise }) => { + await expect(ctx.runUiEffect({ type: 'open_tool', toolId: 'x' }, false)).rejects.toThrow('not available over the bridge') + return doneRecord() + }) + + const result = await executeBridgeCall({ toolName: 'read_wallet', input: {} }, makeDeps()) + expect(result.status).toBe('done') + }) + + it('rejects non-object input before reaching the executor', async () => { + const result = await executeBridgeCall( + { toolName: 'read_wallet', input: [1, 2] as unknown as Record }, + makeDeps(), + ) + expect(result.status).toBe('error') + expect(result.summary).toContain('JSON object') + expect(executeToolCall).not.toHaveBeenCalled() + }) +}) diff --git a/test/services/bridgeToken.test.ts b/test/services/bridgeToken.test.ts new file mode 100644 index 00000000..e1dd9a55 --- /dev/null +++ b/test/services/bridgeToken.test.ts @@ -0,0 +1,56 @@ +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { + bridgeInfoFile, + ensureBridgeToken, + rotateBridgeToken, + writeBridgeRuntimeInfo, +} from '../../electron/services/bridge/bridgeToken' + +describe('bridgeToken', () => { + let userData: string + + beforeEach(() => { + userData = fs.mkdtempSync(path.join(os.tmpdir(), 'daemon-bridge-token-')) + }) + + afterEach(() => { + fs.rmSync(userData, { recursive: true, force: true }) + }) + + it('creates a 256-bit hex token on first run and persists it', () => { + const first = ensureBridgeToken(userData) + expect(first.token).toMatch(/^[0-9a-f]{64}$/) + expect(first.file).toBe(bridgeInfoFile(userData)) + expect(fs.existsSync(first.file)).toBe(true) + + const second = ensureBridgeToken(userData) + expect(second.token).toBe(first.token) + }) + + it('records the live port without losing the token', () => { + const { token, file } = ensureBridgeToken(userData) + writeBridgeRuntimeInfo(userData, { port: 7337, token }) + + const parsed = JSON.parse(fs.readFileSync(file, 'utf8')) + expect(parsed.token).toBe(token) + expect(parsed.port).toBe(7337) + expect(parsed.pid).toBe(process.pid) + expect(typeof parsed.updatedAt).toBe('number') + }) + + it('rotates to a fresh token while keeping the recorded port', () => { + const { token } = ensureBridgeToken(userData) + writeBridgeRuntimeInfo(userData, { port: 4242, token }) + + const rotated = rotateBridgeToken(userData) + expect(rotated).toMatch(/^[0-9a-f]{64}$/) + expect(rotated).not.toBe(token) + + const parsed = JSON.parse(fs.readFileSync(bridgeInfoFile(userData), 'utf8')) + expect(parsed.token).toBe(rotated) + expect(parsed.port).toBe(4242) + }) +}) diff --git a/vite.bridge.config.ts b/vite.bridge.config.ts new file mode 100644 index 00000000..5494c97e --- /dev/null +++ b/vite.bridge.config.ts @@ -0,0 +1,31 @@ +import { builtinModules } from 'node:module' +import { defineConfig } from 'vite' + +// Unlike vite.cloud.config.ts, dependencies are NOT external: the shim must be +// a single self-contained .mjs the system node can run from app resources, +// so the MCP SDK is bundled in. Only node builtins stay external. +const external = [ + ...builtinModules, + ...builtinModules.map((name) => `node:${name}`), +] + +export default defineConfig({ + ssr: { + // Bundle the MCP SDK (and everything else) — only node builtins stay external. + noExternal: true, + }, + build: { + target: 'node18', + ssr: 'electron/services/bridge/shim.ts', + outDir: 'dist-bridge', + emptyOutDir: true, + rollupOptions: { + external, + output: { + format: 'es', + entryFileNames: 'daemon-bridge-shim.mjs', + inlineDynamicImports: true, + }, + }, + }, +}) From 59ea7f5a868f37c9493f11b9f28d503022ad10fa Mon Sep 17 00:00:00 2001 From: nullxnothing Date: Sat, 20 Jun 2026 08:14:15 -0600 Subject: [PATCH 2/5] =?UTF-8?q?fix(bridge):=20make=20cwd=E2=86=92project?= =?UTF-8?q?=20resolution=20OS-independent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit resolveProjectForCwd leaned on path.resolve + path.sep, which only behave correctly on Windows. On Linux CI path.resolve mangles C:\ paths and path.sep is /, so the longest-prefix match returned nothing and two gateway tests failed. Normalize separators and case ourselves instead of trusting host path semantics. --- electron/services/bridge/BridgeToolGateway.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/electron/services/bridge/BridgeToolGateway.ts b/electron/services/bridge/BridgeToolGateway.ts index f4979039..e75a9295 100644 --- a/electron/services/bridge/BridgeToolGateway.ts +++ b/electron/services/bridge/BridgeToolGateway.ts @@ -8,7 +8,6 @@ * itself can only be resolved from DAEMON's renderer — never by the caller. */ import crypto from 'node:crypto' -import path from 'node:path' import { getDb } from '../../db/db' import * as SettingsService from '../SettingsService' import { executeToolCall, type AriaTransport } from '../AriaAgentService' @@ -84,7 +83,7 @@ export function resolveProjectForCwd(cwd: string | undefined): ProjectMatch | nu let bestLen = -1 for (const row of rows) { const root = normalizeFsPath(row.path) - const isMatch = target === root || target.startsWith(root + path.sep) + const isMatch = target === root || target.startsWith(root + '/') if (isMatch && root.length > bestLen) { best = row bestLen = root.length @@ -93,9 +92,20 @@ export function resolveProjectForCwd(cwd: string | undefined): ProjectMatch | nu return best } +/** + * Normalize a caller-supplied path for prefix comparison. DAEMON runs on + * Windows, so cwds are Windows paths — but this also executes on Linux CI, where + * `path.resolve` would mangle `C:\...`. So we normalize separators ourselves + * instead of leaning on the host's `path` semantics: backslashes → `/`, collapse + * repeats, strip a trailing slash, and lowercase (Windows paths are + * case-insensitive). Keeps comparison stable regardless of the runtime OS. + */ function normalizeFsPath(value: string): string { - const resolved = path.resolve(value) - return process.platform === 'win32' ? resolved.toLowerCase() : resolved + const unified = value + .replace(/\\/g, '/') + .replace(/\/+/g, '/') + .replace(/\/$/, '') + return unified.toLowerCase() } /** Execute one external tool call through the standard ARIA risk gate. */ From d7283cb70ca92e56de44743a06fc4ac1296115bd Mon Sep 17 00:00:00 2001 From: nullxnothing Date: Sat, 20 Jun 2026 08:33:17 -0600 Subject: [PATCH 3/5] chore(deps): patch supply-chain advisories flagged by OSV Bump vulnerable transitive deps to their fixed versions via pnpm overrides, and the two direct deps (vite, nodemailer) in place: - dompurify 3.4.0 -> 3.4.7 (XSS via hook tag/attr mutation) - hono 4.12.21 -> 4.12.25 (CORS credential reflection) - protobufjs 7.5.8 -> 7.6.1 (Any-expansion DoS) - tmp 0.2.6 -> 0.2.7 (path traversal via _assertPath bypass) - ws 8.20.1 -> 8.21.0 (fragment memory-exhaustion DoS) - undici 6.25.0 -> 6.27.0 (websocket fragment DoS) - tar 7.5.13 -> 7.5.16 (PAX size override) - form-data 4.0.5 -> 4.0.6 (CRLF injection in field names) - js-yaml 4.1.1 -> 4.2.0 (merge-key quadratic DoS) - nodemailer 8.x -> 9.0.1 (raw option file-access bypass) - vite 6.4.2 -> 6.4.3 (server.fs.deny bypass on Windows) @babel/core (CVSS 3.2, build-time only, no stable 7.x fix) is suppressed in osv-scanner.toml with justification and a review date. --- osv-scanner.toml | 12 ++ package.json | 19 +-- pnpm-lock.yaml | 295 +++++++++++++++++++++++++++-------------------- 3 files changed, 193 insertions(+), 133 deletions(-) diff --git a/osv-scanner.toml b/osv-scanner.toml index c1771262..ed60edca 100644 --- a/osv-scanner.toml +++ b/osv-scanner.toml @@ -12,3 +12,15 @@ id = "GHSA-5xrq-8626-4rwp" # CVE-2026-47429 — vitest < 4.1.0 # separately to avoid breaking the test suite on a release. ignoreUntil = 2026-09-01 reason = "Dev-only: vitest UI/Browser-mode server not used; we run headless `vitest run`. Bump to vitest 4.x tracked post-release." + +[[IgnoredVulns]] +id = "GHSA-4x5r-pxfx-6jf8" # @babel/core arbitrary file read via sourceMappingURL +# Why ignored: build-time only. @babel/core is pulled in transitively by +# @vitejs/plugin-react and runs against our own first-party source during the +# Vite build — it never processes untrusted input at runtime and is not shipped +# in the Electron bundle. The advisory requires compiling attacker-controlled +# code containing a crafted sourceMappingURL, which never happens in our pipeline. +# The only fix is @babel/core 8.0.0-rc (no stable 7.x patch); we will not pin a +# release-candidate major into the build. Re-evaluate when 8.x ships stable. +ignoreUntil = 2026-09-01 +reason = "Build-time devDep (vite-plugin-react); compiles only first-party source, not shipped at runtime. No stable 7.x fix — only 8.0.0-rc." diff --git a/package.json b/package.json index 8c9d5ccc..4e572b33 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "imapflow": "^1.3.3", "mailparser": "^3.9.8", "node-pty": "1.1.0", - "nodemailer": "^8.0.7", + "nodemailer": "^9.0.1", "pidusage": "^4.0.1", "ps-list": "^8.1.1", "pyright": "^1.1.409", @@ -150,7 +150,7 @@ "pngjs": "^7.0.0", "react": "^19.2.5", "react-dom": "^19.2.5", - "vite": "^6.4.2", + "vite": "^6.4.3", "vite-plugin-electron": "^0.29.0", "vite-plugin-electron-renderer": "^0.14.7", "vitest": "^3.2.4", @@ -163,18 +163,23 @@ "basic-ftp": "5.3.1", "bigint-buffer": "workspace:*", "brace-expansion": "5.0.6", - "dompurify": "3.4.0", + "dompurify": "3.4.7", "esbuild": "0.25.12", - "hono": "4.12.21", + "form-data": "4.0.6", + "hono": "4.12.25", + "js-yaml": "4.2.0", "lodash": "4.18.1", + "nodemailer": "9.0.1", "follow-redirects": "1.16.0", "postcss": "8.5.10", - "protobufjs": "7.5.8", + "protobufjs": "7.6.1", "qs": "6.15.2", "shell-quote": "1.8.4", - "tmp": "0.2.6", + "tar": "7.5.16", + "tmp": "0.2.7", + "undici": "6.27.0", "uuid": "11.1.1", - "ws": "8.20.1" + "ws": "8.21.0" }, "peerDependencyRules": { "allowedVersions": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5f975112..0ecaee68 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,18 +10,23 @@ overrides: basic-ftp: 5.3.1 bigint-buffer: workspace:* brace-expansion: 5.0.6 - dompurify: 3.4.0 + dompurify: 3.4.7 esbuild: 0.25.12 - hono: 4.12.21 + form-data: 4.0.6 + hono: 4.12.25 + js-yaml: 4.2.0 lodash: 4.18.1 + nodemailer: 9.0.1 follow-redirects: 1.16.0 postcss: 8.5.10 - protobufjs: 7.5.8 + protobufjs: 7.6.1 qs: 6.15.2 shell-quote: 1.8.4 - tmp: 0.2.6 + tar: 7.5.16 + tmp: 0.2.7 + undici: 6.27.0 uuid: 11.1.1 - ws: 8.20.1 + ws: 8.21.0 patchedDependencies: '@oobe-protocol-labs/synapse-sap-sdk@0.19.8': @@ -79,7 +84,7 @@ importers: version: 6.9.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6) '@solana/mpp': specifier: ^0.6.0 - version: 0.6.0(@solana/kit@6.9.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6))(mppx@0.5.5(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(express@5.2.1)(hono@4.12.21)(typescript@5.9.3)(viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))) + version: 0.6.0(@solana/kit@6.9.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6))(mppx@0.5.5(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(express@5.2.1)(hono@4.12.25)(typescript@5.9.3)(viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.4.3))) '@solana/spl-token': specifier: ^0.4.9 version: 0.4.14(@solana/web3.js@1.98.4(bufferutil@4.1.0)(encoding@0.1.13)(typescript@5.9.3)(utf-8-validate@6.0.6))(bufferutil@4.1.0)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6) @@ -129,8 +134,8 @@ importers: specifier: 1.1.0 version: 1.1.0(patch_hash=f41f3f1b27203d2dfc08a004c1c51bc4a56ca84fce763607028e80840c5bcc3e) nodemailer: - specifier: ^8.0.7 - version: 8.0.7 + specifier: 9.0.1 + version: 9.0.1 pidusage: specifier: ^4.0.1 version: 4.0.1 @@ -203,7 +208,7 @@ importers: version: 19.2.3(@types/react@19.2.14) '@vitejs/plugin-react': specifier: ^5.2.0 - version: 5.2.0(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.9.0)) + version: 5.2.0(vite@6.4.3(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.9.0)) '@xterm/addon-fit': specifier: ^0.10.0 version: 0.10.0(@xterm/xterm@5.5.0) @@ -253,8 +258,8 @@ importers: specifier: ^19.2.5 version: 19.2.5(react@19.2.5) vite: - specifier: ^6.4.2 - version: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.9.0) + specifier: ^6.4.3 + version: 6.4.3(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.9.0) vite-plugin-electron: specifier: ^0.29.0 version: 0.29.1(vite-plugin-electron-renderer@0.14.7) @@ -1241,7 +1246,7 @@ packages: resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} engines: {node: '>=18.14.1'} peerDependencies: - hono: 4.12.21 + hono: 4.12.25 '@isaacs/fs-minipass@4.0.1': resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} @@ -1486,17 +1491,17 @@ packages: '@protobufjs/codegen@2.0.5': resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} - '@protobufjs/eventemitter@1.1.0': - resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} - '@protobufjs/fetch@1.1.0': - resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} '@protobufjs/float@1.0.2': resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} - '@protobufjs/inquire@1.1.1': - resolution: {integrity: sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==} + '@protobufjs/inquire@1.1.2': + resolution: {integrity: sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==} '@protobufjs/path@1.1.2': resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} @@ -3706,8 +3711,8 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.4.0: - resolution: {integrity: sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==} + dompurify@3.4.7: + resolution: {integrity: sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==} domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -4208,8 +4213,8 @@ packages: resolution: {integrity: sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==} engines: {node: '>=0.10.0'} - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} formdata-polyfill@4.0.10: @@ -4372,6 +4377,10 @@ packages: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + hast-util-to-jsx-runtime@2.3.6: resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} @@ -4409,8 +4418,8 @@ packages: hermes-parser@0.35.0: resolution: {integrity: sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA==} - hono@4.12.21: - resolution: {integrity: sha512-uV63apnb0kyPtAUwoWgaGh9HyIFcv8lgmzPZSiTBQAFOFGIzka5EZ1dZocmGnn0XdX0+XTqJ6Tqv7selMuGLRQ==} + hono@4.12.25: + resolution: {integrity: sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==} engines: {node: '>=16.9.0'} hosted-git-info@4.1.0: @@ -4614,12 +4623,12 @@ packages: isomorphic-ws@4.0.1: resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==} peerDependencies: - ws: 8.20.1 + ws: 8.21.0 isows@1.0.7: resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} peerDependencies: - ws: 8.20.1 + ws: 8.21.0 jake@10.9.4: resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} @@ -4669,8 +4678,8 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - js-yaml@4.1.1: - resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + js-yaml@4.2.0: + resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} hasBin: true jsc-safe-url@0.2.4: @@ -5321,7 +5330,7 @@ packages: '@modelcontextprotocol/sdk': '>=1.25.0' elysia: '>=1' express: '>=5' - hono: 4.12.21 + hono: 4.12.25 viem: '>=2.47.5' peerDependenciesMeta: '@modelcontextprotocol/sdk': @@ -5426,12 +5435,8 @@ packages: node-releases@2.0.38: resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} - nodemailer@8.0.5: - resolution: {integrity: sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==} - engines: {node: '>=6.0.0'} - - nodemailer@8.0.7: - resolution: {integrity: sha512-pkjE4mkBzQjdJT4/UmlKl3pX0rC9fZmjh7c6C9o7lv66Ac6w9WCnzPzhbPNxwZAzlF4mdq4CSWB5+FbK6FWCow==} + nodemailer@9.0.1: + resolution: {integrity: sha512-Gwv8SQewT616ZM/URn0H54b8PWo/Wum7md3EW2aWy1lO27+WZCX+Xyak3J+NlmHUjDh5ME+uesJUDRbR3Ye8Bw==} engines: {node: '>=6.0.0'} nopt@9.0.0: @@ -5726,8 +5731,8 @@ packages: property-information@7.2.0: resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} - protobufjs@7.5.8: - resolution: {integrity: sha512-dvpCIeLPbXZS/Ete7yLaO7RenOdken2NHKykBXbsaGxZT0UTltcarBciw+A78SRQs9iMAAVpsYA+l8b1hTePIA==} + protobufjs@7.6.1: + resolution: {integrity: sha512-4K0myLaWL5EteuSAro91EGFgcfVgxb64Jx+7oDAY6GOkXD4M69yuSEljNcInGVCA5sOPxmZ/EqDLj2x0Q0+Ygg==} engines: {node: '>=12.0.0'} proxy-addr@2.0.7: @@ -6457,8 +6462,8 @@ packages: tar-stream@3.1.8: resolution: {integrity: sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==} - tar@7.5.13: - resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==} + tar@7.5.16: + resolution: {integrity: sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w==} engines: {node: '>=18'} teex@1.0.1: @@ -6528,8 +6533,8 @@ packages: tmp-promise@3.0.3: resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} - tmp@0.2.6: - resolution: {integrity: sha512-5sJPdPjfI5Kx+qbrDesxkglRBxW//g7hCsqspEjwkewGvBMGIKMOTKzLt1hFVJzyadba3lDUN20O9qhvbQUSTA==} + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} engines: {node: '>=14.14'} tmpl@1.0.5: @@ -6637,8 +6642,8 @@ packages: undici-types@8.3.0: resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} - undici@6.25.0: - resolution: {integrity: sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==} + undici@6.27.0: + resolution: {integrity: sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==} engines: {node: '>=18.17'} unicode-canonical-property-names-ecmascript@2.0.1: @@ -6792,8 +6797,8 @@ packages: vite-plugin-electron-renderer: optional: true - vite@6.4.2: - resolution: {integrity: sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==} + vite@6.4.3: + resolution: {integrity: sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==} engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: @@ -6936,8 +6941,8 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.20.1: - resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -7907,7 +7912,7 @@ snapshots: terminal-link: 2.1.1 toqr: 0.1.1 wrap-ansi: 7.0.0 - ws: 8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) zod: 3.25.76 optionalDependencies: expo-router: 55.0.13(@expo/log-box@55.0.11)(@expo/metro-runtime@55.0.10)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(expo-constants@55.0.15)(expo-font@55.0.6)(expo-linking@55.0.15)(expo@55.0.20)(react-dom@19.2.5(react@19.2.5))(react-native-safe-area-context@5.8.0(react-native@0.85.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@6.0.6))(react@19.2.5))(react-native-screens@4.25.2(react-native@0.85.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@6.0.6))(react@19.2.5))(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(react-native@0.85.2(@babel/core@7.29.0)(@types/react@19.2.14)(bufferutil@4.1.0)(react@19.2.5)(utf-8-validate@6.0.6))(react@19.2.5) @@ -8200,14 +8205,14 @@ snapshots: dependencies: '@babel/code-frame': 7.29.0 chalk: 4.1.2 - js-yaml: 4.1.1 + js-yaml: 4.2.0 '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(bufferutil@4.1.0)(utf-8-validate@6.0.6)': dependencies: google-auth-library: 10.6.2 p-retry: 4.6.2 - protobufjs: 7.5.8 - ws: 8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + protobufjs: 7.6.1 + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) optionalDependencies: '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3) transitivePeerDependencies: @@ -8215,9 +8220,9 @@ snapshots: - supports-color - utf-8-validate - '@hono/node-server@1.19.14(hono@4.12.21)': + '@hono/node-server@1.19.14(hono@4.12.25)': dependencies: - hono: 4.12.21 + hono: 4.12.25 '@isaacs/fs-minipass@4.0.1': dependencies: @@ -8417,7 +8422,7 @@ snapshots: '@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3)': dependencies: - '@hono/node-server': 1.19.14(hono@4.12.21) + '@hono/node-server': 1.19.14(hono@4.12.25) ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) content-type: 1.0.5 @@ -8427,7 +8432,7 @@ snapshots: eventsource-parser: 3.1.0 express: 5.2.1 express-rate-limit: 8.5.2(express@5.2.1) - hono: 4.12.21 + hono: 4.12.25 jose: 6.2.3 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 @@ -8534,16 +8539,15 @@ snapshots: '@protobufjs/codegen@2.0.5': {} - '@protobufjs/eventemitter@1.1.0': {} + '@protobufjs/eventemitter@1.1.1': {} - '@protobufjs/fetch@1.1.0': + '@protobufjs/fetch@1.1.1': dependencies: '@protobufjs/aspromise': 1.1.2 - '@protobufjs/inquire': 1.1.1 '@protobufjs/float@1.0.2': {} - '@protobufjs/inquire@1.1.1': {} + '@protobufjs/inquire@1.1.2': {} '@protobufjs/path@1.1.2': {} @@ -8931,7 +8935,7 @@ snapshots: nullthrows: 1.1.1 open: 7.4.2 serve-static: 1.16.3 - ws: 8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - bufferutil - supports-color @@ -8950,7 +8954,7 @@ snapshots: nullthrows: 1.1.1 open: 7.4.2 serve-static: 1.16.3 - ws: 8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - bufferutil - supports-color @@ -9146,7 +9150,7 @@ snapshots: '@noble/hashes': 1.8.0 apg-js: 4.4.0 - '@signinwithethereum/siwe@4.2.0(ethers@6.16.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76))': + '@signinwithethereum/siwe@4.2.0(ethers@6.16.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.4.3))': dependencies: '@signinwithethereum/siwe-parser': 4.2.0 optionalDependencies: @@ -9428,13 +9432,13 @@ snapshots: - fastestsmallesttextencoderdecoder - utf-8-validate - '@solana/mpp@0.6.0(@solana/kit@6.9.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6))(mppx@0.5.5(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(express@5.2.1)(hono@4.12.21)(typescript@5.9.3)(viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)))': + '@solana/mpp@0.6.0(@solana/kit@6.9.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6))(mppx@0.5.5(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(express@5.2.1)(hono@4.12.25)(typescript@5.9.3)(viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.4.3)))': dependencies: '@solana-program/compute-budget': 0.15.0(@solana/kit@6.9.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6)) '@solana-program/system': 0.12.2(@solana/kit@6.9.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6)) '@solana-program/token': 0.11.0(@solana/kit@6.9.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6)) '@solana/kit': 6.9.0(bufferutil@4.1.0)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.3)(utf-8-validate@6.0.6) - mppx: 0.5.5(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(express@5.2.1)(hono@4.12.21)(typescript@5.9.3)(viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + mppx: 0.5.5(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(express@5.2.1)(hono@4.12.25)(typescript@5.9.3)(viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.4.3)) '@solana/nominal-types@6.9.0(typescript@5.9.3)': optionalDependencies: @@ -9578,7 +9582,7 @@ snapshots: '@solana/functional': 6.9.0(typescript@5.9.3) '@solana/rpc-subscriptions-spec': 6.9.0(typescript@5.9.3) '@solana/subscribable': 6.9.0(typescript@5.9.3) - ws: 8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -10068,7 +10072,7 @@ snapshots: '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-react@5.2.0(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.9.0))': + '@vitejs/plugin-react@5.2.0(vite@6.4.3(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.9.0))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) @@ -10076,7 +10080,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-rc.3 '@types/babel__core': 7.20.5 react-refresh: 0.18.0 - vite: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.9.0) + vite: 6.4.3(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -10088,13 +10092,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.9.0))': + '@vitest/mocker@3.2.4(vite@6.4.3(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.9.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.9.0) + vite: 6.4.3(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.9.0) '@vitest/pretty-format@3.2.4': dependencies: @@ -10145,12 +10149,12 @@ snapshots: dependencies: '@noble/curves': 1.9.7 '@scure/base': 1.2.6 - '@signinwithethereum/siwe': 4.2.0(ethers@6.16.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)) + '@signinwithethereum/siwe': 4.2.0(ethers@6.16.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))(viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.4.3)) '@x402/core': 2.12.0 ajv: 8.20.0 jose: 5.10.0 tweetnacl: 1.0.3 - viem: 2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.4.3) + viem: 2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76) zod: 3.25.76 transitivePeerDependencies: - bufferutil @@ -10199,6 +10203,11 @@ snapshots: typescript: 5.9.3 zod: 3.25.76 + abitype@1.2.3(typescript@5.9.3)(zod@4.4.3): + optionalDependencies: + typescript: 5.9.3 + zod: 4.4.3 + abitype@1.2.4(typescript@5.9.3)(zod@3.25.76): optionalDependencies: typescript: 5.9.3 @@ -10314,7 +10323,7 @@ snapshots: hosted-git-info: 4.1.0 isbinaryfile: 5.0.7 jiti: 2.6.1 - js-yaml: 4.1.1 + js-yaml: 4.2.0 json5: 2.2.3 lazy-val: 1.0.5 minimatch: 10.2.5 @@ -10322,7 +10331,7 @@ snapshots: proper-lockfile: 4.1.2 resedit: 1.7.2 semver: 7.7.4 - tar: 7.5.13 + tar: 7.5.16 temp-file: 3.4.0 tiny-async-pool: 1.3.0 which: 5.0.0 @@ -10374,7 +10383,7 @@ snapshots: axios@1.16.1: dependencies: follow-redirects: 1.16.0 - form-data: 4.0.5 + form-data: 4.0.6 https-proxy-agent: 5.0.1 proxy-from-env: 2.1.0 transitivePeerDependencies: @@ -10650,7 +10659,7 @@ snapshots: fs-extra: 10.1.0 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 - js-yaml: 4.1.1 + js-yaml: 4.2.0 sanitize-filename: 1.6.4 source-map-support: 0.5.21 stat-mode: 1.0.0 @@ -10913,7 +10922,7 @@ snapshots: dependencies: env-paths: 2.2.1 import-fresh: 3.3.1 - js-yaml: 4.1.1 + js-yaml: 4.2.0 parse-json: 5.2.0 optionalDependencies: typescript: 5.9.3 @@ -11047,7 +11056,7 @@ snapshots: builder-util: 26.8.1 fs-extra: 10.1.0 iconv-lite: 0.6.3 - js-yaml: 4.1.1 + js-yaml: 4.2.0 optionalDependencies: dmg-license: 1.0.11 transitivePeerDependencies: @@ -11084,7 +11093,7 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.4.0: + dompurify@3.4.7: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -11152,7 +11161,7 @@ snapshots: builder-util: 26.8.1 builder-util-runtime: 9.5.1 chalk: 4.1.2 - form-data: 4.0.5 + form-data: 4.0.6 fs-extra: 10.1.0 lazy-val: 1.0.5 mime: 2.6.0 @@ -11165,7 +11174,7 @@ snapshots: dependencies: builder-util-runtime: 9.5.1 fs-extra: 10.1.0 - js-yaml: 4.1.1 + js-yaml: 4.2.0 lazy-val: 1.0.5 lodash.escaperegexp: 4.1.2 lodash.isequal: 4.5.0 @@ -11242,7 +11251,7 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 has-tostringtag: 1.0.2 - hasown: 2.0.3 + hasown: 2.0.4 es6-error@4.1.1: optional: true @@ -11322,7 +11331,7 @@ snapshots: '@types/node': 22.7.5 aes-js: 4.0.0-beta.5 tslib: 2.7.0 - ws: 8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -11705,12 +11714,12 @@ snapshots: dependencies: for-in: 1.0.2 - form-data@4.0.5: + form-data@4.0.6: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 es-set-tostringtag: 2.1.0 - hasown: 2.0.3 + hasown: 2.0.4 mime-types: 2.1.35 formdata-polyfill@4.0.10: @@ -11891,7 +11900,7 @@ snapshots: '@types/ws': 8.18.1 entities: 7.0.1 whatwg-mimetype: 3.0.0 - ws: 8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -11914,6 +11923,10 @@ snapshots: dependencies: function-bind: 1.1.2 + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + hast-util-to-jsx-runtime@2.3.6: dependencies: '@types/estree': 1.0.8 @@ -11966,7 +11979,7 @@ snapshots: dependencies: hermes-estree: 0.35.0 - hono@4.12.21: {} + hono@4.12.25: {} hosted-git-info@4.1.0: dependencies: @@ -12065,7 +12078,7 @@ snapshots: libbase64: 1.3.0 libmime: 5.3.8 libqp: 2.1.1 - nodemailer: 8.0.7 + nodemailer: 9.0.1 pino: 10.3.1 socks: 2.8.8 @@ -12163,13 +12176,13 @@ snapshots: isobject@3.0.1: {} - isomorphic-ws@4.0.1(ws@8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6)): + isomorphic-ws@4.0.1(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)): dependencies: - ws: 8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) - isows@1.0.7(ws@8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6)): + isows@1.0.7(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)): dependencies: - ws: 8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) jake@10.9.4: dependencies: @@ -12186,11 +12199,11 @@ snapshots: delay: 5.0.0 es6-promisify: 5.0.0 eyes: 0.1.8 - isomorphic-ws: 4.0.1(ws@8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + isomorphic-ws: 4.0.1(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) json-stringify-safe: 5.0.1 stream-json: 1.9.1 uuid: 11.1.1 - ws: 8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -12236,7 +12249,7 @@ snapshots: js-tokens@9.0.1: {} - js-yaml@4.1.1: + js-yaml@4.2.0: dependencies: argparse: 2.0.1 @@ -12473,7 +12486,7 @@ snapshots: iconv-lite: 0.7.2 libmime: 5.3.8 linkify-it: 5.0.0 - nodemailer: 8.0.5 + nodemailer: 9.0.1 punycode.js: 2.3.1 tlds: 1.261.0 @@ -12958,7 +12971,7 @@ snapshots: serialize-error: 2.1.0 source-map: 0.5.7 throat: 5.0.0 - ws: 8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) yargs: 17.7.2 transitivePeerDependencies: - bufferutil @@ -13004,7 +13017,7 @@ snapshots: serialize-error: 2.1.0 source-map: 0.5.7 throat: 5.0.0 - ws: 8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) yargs: 17.7.2 transitivePeerDependencies: - bufferutil @@ -13272,10 +13285,10 @@ snapshots: monaco-editor@0.55.1: dependencies: - dompurify: 3.4.0 + dompurify: 3.4.7 marked: 14.0.0 - mppx@0.5.5(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(express@5.2.1)(hono@4.12.21)(typescript@5.9.3)(viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76)): + mppx@0.5.5(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(express@5.2.1)(hono@4.12.25)(typescript@5.9.3)(viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.4.3)): dependencies: '@remix-run/fetch-proxy': 0.7.1 '@remix-run/node-fetch-server': 0.13.3 @@ -13286,7 +13299,7 @@ snapshots: optionalDependencies: '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3) express: 5.2.1 - hono: 4.12.21 + hono: 4.12.25 transitivePeerDependencies: - typescript @@ -13357,9 +13370,9 @@ snapshots: nopt: 9.0.0 proc-log: 6.1.0 semver: 7.7.4 - tar: 7.5.13 + tar: 7.5.16 tinyglobby: 0.2.16 - undici: 6.25.0 + undici: 6.27.0 which: 6.0.1 node-int64@0.4.0: {} @@ -13370,9 +13383,7 @@ snapshots: node-releases@2.0.38: {} - nodemailer@8.0.5: {} - - nodemailer@8.0.7: {} + nodemailer@9.0.1: {} nopt@9.0.0: dependencies: @@ -13458,6 +13469,21 @@ snapshots: transitivePeerDependencies: - zod + ox@0.14.22(typescript@5.9.3)(zod@4.4.3): + dependencies: + '@adraffy/ens-normalize': 1.11.1 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.4(typescript@5.9.3)(zod@4.4.3) + eventemitter3: 5.0.1 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - zod + ox@0.14.7(typescript@5.9.3)(zod@4.4.3): dependencies: '@adraffy/ens-normalize': 1.11.1 @@ -13550,7 +13576,7 @@ snapshots: open: 7.4.2 semver: 7.7.4 slash: 2.0.0 - tmp: 0.2.6 + tmp: 0.2.7 yaml: 2.8.3 path-is-absolute@1.0.1: {} @@ -13710,15 +13736,15 @@ snapshots: property-information@7.2.0: {} - protobufjs@7.5.8: + protobufjs@7.6.1: dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/base64': 1.1.2 '@protobufjs/codegen': 2.0.5 - '@protobufjs/eventemitter': 1.1.0 - '@protobufjs/fetch': 1.1.0 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 '@protobufjs/float': 1.0.2 - '@protobufjs/inquire': 1.1.1 + '@protobufjs/inquire': 1.1.2 '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.1 @@ -13766,7 +13792,7 @@ snapshots: devtools-protocol: 0.0.1581282 typed-query-selector: 2.12.1 webdriver-bidi-protocol: 0.4.1 - ws: 8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - bare-abort-controller - bare-buffer @@ -13887,7 +13913,7 @@ snapshots: react-devtools-core@6.1.5(bufferutil@4.1.0)(utf-8-validate@6.0.6): dependencies: shell-quote: 1.8.4 - ws: 8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - bufferutil - utf-8-validate @@ -13992,7 +14018,7 @@ snapshots: stacktrace-parser: 0.1.11 tinyglobby: 0.2.16 whatwg-fetch: 3.6.20 - ws: 8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) yargs: 17.7.2 optionalDependencies: '@types/react': 19.2.14 @@ -14218,7 +14244,7 @@ snapshots: buffer: 6.0.3 eventemitter3: 5.0.4 uuid: 11.1.1 - ws: 8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) optionalDependencies: bufferutil: 4.1.0 utf-8-validate: 6.0.6 @@ -14626,7 +14652,7 @@ snapshots: - bare-buffer - react-native-b4a - tar@7.5.13: + tar@7.5.16: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 @@ -14702,9 +14728,9 @@ snapshots: tmp-promise@3.0.3: dependencies: - tmp: 0.2.6 + tmp: 0.2.7 - tmp@0.2.6: {} + tmp@0.2.7: {} tmpl@1.0.5: {} @@ -14788,7 +14814,7 @@ snapshots: undici-types@8.3.0: {} - undici@6.25.0: {} + undici@6.27.0: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -14916,16 +14942,33 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.4.3): + viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@3.25.76): dependencies: '@noble/curves': 1.9.1 '@noble/hashes': 1.8.0 '@scure/bip32': 1.7.0 '@scure/bip39': 1.6.0 abitype: 1.2.3(typescript@5.9.3)(zod@3.25.76) - isows: 1.0.7(ws@8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + isows: 1.0.7(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) ox: 0.14.22(typescript@5.9.3)(zod@3.25.76) - ws: 8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6) + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + - zod + + viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.4.3): + dependencies: + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 + abitype: 1.2.3(typescript@5.9.3)(zod@4.4.3) + isows: 1.0.7(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) + ox: 0.14.22(typescript@5.9.3)(zod@4.4.3) + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) optionalDependencies: typescript: 5.9.3 transitivePeerDependencies: @@ -14939,7 +14982,7 @@ snapshots: debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.9.0) + vite: 6.4.3(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -14960,7 +15003,7 @@ snapshots: optionalDependencies: vite-plugin-electron-renderer: 0.14.7 - vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.9.0): + vite@6.4.3(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.9.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -14980,7 +15023,7 @@ snapshots: dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.9.0)) + '@vitest/mocker': 3.2.4(vite@6.4.3(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.9.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -14998,7 +15041,7 @@ snapshots: tinyglobby: 0.2.16 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.9.0) + vite: 6.4.3(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.9.0) vite-node: 3.2.4(@types/node@25.6.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.2)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: @@ -15084,7 +15127,7 @@ snapshots: wrappy@1.0.2: {} - ws@8.20.1(bufferutil@4.1.0)(utf-8-validate@6.0.6): + ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6): optionalDependencies: bufferutil: 4.1.0 utf-8-validate: 6.0.6 From d348589dba9d137ebe7c255df417cdb5bd50458c Mon Sep 17 00:00:00 2001 From: nullxnothing Date: Sat, 20 Jun 2026 08:37:20 -0600 Subject: [PATCH 4/5] chore(deps): bump dompurify and protobufjs to fully-patched versions OSV flagged newer advisories against the interim pins: - dompurify 3.4.7 -> 3.4.11 (ALLOWED_ATTR pollution, SAFE_FOR_TEMPLATES bypass, Trusted Types policy persistence) - protobufjs 7.6.1 -> 7.6.3 (schema-name property shadowing) --- package.json | 4 ++-- pnpm-lock.yaml | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 4e572b33..7378fe19 100644 --- a/package.json +++ b/package.json @@ -163,7 +163,7 @@ "basic-ftp": "5.3.1", "bigint-buffer": "workspace:*", "brace-expansion": "5.0.6", - "dompurify": "3.4.7", + "dompurify": "3.4.11", "esbuild": "0.25.12", "form-data": "4.0.6", "hono": "4.12.25", @@ -172,7 +172,7 @@ "nodemailer": "9.0.1", "follow-redirects": "1.16.0", "postcss": "8.5.10", - "protobufjs": "7.6.1", + "protobufjs": "7.6.3", "qs": "6.15.2", "shell-quote": "1.8.4", "tar": "7.5.16", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ecaee68..2537a4b1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,7 @@ overrides: basic-ftp: 5.3.1 bigint-buffer: workspace:* brace-expansion: 5.0.6 - dompurify: 3.4.7 + dompurify: 3.4.11 esbuild: 0.25.12 form-data: 4.0.6 hono: 4.12.25 @@ -19,7 +19,7 @@ overrides: nodemailer: 9.0.1 follow-redirects: 1.16.0 postcss: 8.5.10 - protobufjs: 7.6.1 + protobufjs: 7.6.3 qs: 6.15.2 shell-quote: 1.8.4 tar: 7.5.16 @@ -3711,8 +3711,8 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.4.7: - resolution: {integrity: sha512-2jBxDJY4RR06tQNy4w5FlFH7kfxsQZlufd0sbv+chfHCxeJwrFw2baUDsSwvBISD4K4RDbd0PTfy3uNXsR6siA==} + dompurify@3.4.11: + resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==} domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -5731,8 +5731,8 @@ packages: property-information@7.2.0: resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==} - protobufjs@7.6.1: - resolution: {integrity: sha512-4K0myLaWL5EteuSAro91EGFgcfVgxb64Jx+7oDAY6GOkXD4M69yuSEljNcInGVCA5sOPxmZ/EqDLj2x0Q0+Ygg==} + protobufjs@7.6.3: + resolution: {integrity: sha512-+k0vdJKNdW+Vu+dYe8tZA/VvQb6XKNWexC6URwBFXxNnjLJz9nQJCemGyNgRAWD+B7+nGNc9qMPGwcD7s4nzUw==} engines: {node: '>=12.0.0'} proxy-addr@2.0.7: @@ -8211,7 +8211,7 @@ snapshots: dependencies: google-auth-library: 10.6.2 p-retry: 4.6.2 - protobufjs: 7.6.1 + protobufjs: 7.6.3 ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) optionalDependencies: '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3) @@ -11093,7 +11093,7 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.4.7: + dompurify@3.4.11: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -13285,7 +13285,7 @@ snapshots: monaco-editor@0.55.1: dependencies: - dompurify: 3.4.7 + dompurify: 3.4.11 marked: 14.0.0 mppx@0.5.5(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(express@5.2.1)(hono@4.12.25)(typescript@5.9.3)(viem@2.50.4(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@6.0.6)(zod@4.4.3)): @@ -13736,7 +13736,7 @@ snapshots: property-information@7.2.0: {} - protobufjs@7.6.1: + protobufjs@7.6.3: dependencies: '@protobufjs/aspromise': 1.1.2 '@protobufjs/base64': 1.1.2 From d800c83fa07fa71d0d050df8e21196d5117a6761 Mon Sep 17 00:00:00 2001 From: nullxnothing Date: Sat, 20 Jun 2026 09:01:28 -0600 Subject: [PATCH 5/5] test(visual): refresh agent-panel baselines for Plan/Build composer The rebase onto main pulled in the BUILD/PLAN toggle and one-row composer layout, which legitimately changes the agent panel header and composer renders. Update only those 6 baselines; the rest match within tolerance. --- .../win32/agent-panel-composer.png | Bin 8662 -> 8264 bytes .../win32/agent-panel-header.png | Bin 2856 -> 2496 bytes .../win32/compact/agent-panel-composer.png | Bin 8307 -> 7590 bytes .../win32/compact/agent-panel-header.png | Bin 2663 -> 2436 bytes .../win32/wide/agent-panel-composer.png | Bin 8635 -> 8088 bytes .../win32/wide/agent-panel-header.png | Bin 2798 -> 2451 bytes 6 files changed, 0 insertions(+), 0 deletions(-) diff --git a/scripts/smoke/visual-baselines/win32/agent-panel-composer.png b/scripts/smoke/visual-baselines/win32/agent-panel-composer.png index 472d713823b4cffb13e9a17d603b8e7f05dad3ed..cc85b1831db5e311cd02657c75cbb4768e6bbee1 100644 GIT binary patch literal 8264 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 U%=*N={$~DyC?%;{3FDCe1?1WH1^@s6 diff --git a/scripts/smoke/visual-baselines/win32/agent-panel-header.png b/scripts/smoke/visual-baselines/win32/agent-panel-header.png index 46b34ae5e0900a615a8053e31fb507218b11f7ac..ac6615c87d5d1e80dbc18fd2db84b66509e0d6e2 100644 GIT binary patch literal 2496 zcma*pS2!CC8wTKn*ojeM)Sk6kK?OnWs@h7>K?Ff5MeP}C#3(h2rf7{?jajj4)lO~I znl)<2D9Tss?|1ec{wMGCT<`UqJZCq~)L0Kff0Z5p06_Hb=$HclK!(3rf|la%C5Ye? z0RTE*eH~4UfH&K-!KSSKe1N??{crAf`UV{6K&+N3eFF_M~hV3G%T9JZGcUIxw zL8V^})9-g39%uWWk*5|e{G1gR&egyp%m`i$=0l#FAWat_5H*Ar`(^ax91v*>xw;># z^)5m=LC$mj&8M)w;ZXocbC^aR6xo91qR^ag2V(_1Q85}RiZE?~KJF|*<`oEunf4bo zh87e^`QKU?l84c`BeFyQIg&ni3n4c)H||hv9l>89^bC;!k39e_qS6q;5odhSS~mD5 zxQ&R=fY>-FT22OIPZ}$tgX5n#I)YB5^4g$y+mLr=^8^dOdi~*0fqfm0?F*gh(NdA6 z8kqt5f_~KEcj|u#@oKp>hubQS56y$BkFo~@$S48{y6qyap0eZG@R7Kf*6Dm>X`~1q z*n8-=&%DQ*gRcoZ9g2KJr2?-^c1o4aRZ@&re?%E7EUY5&EJzVONu0edbf(YnEtNDo ztGf0o?fMI+#$6tDQU9jb2PuYvw=cBgV8Wig6_Jq(t52+ZuN?M^yaWCOL>IUOU|3R)5;tU+h_bkPQ#%8TZ%QOE*8KOf@H=RQh0 z?^++-Nb65cOEY7m3hS>2>tAQqm3SOKUMxY#qrQJz2G*s zgm+EEnAr-ch)T{68hEb>g?R=4IgbrIS}8Af5>sFE2A#k4{uS7^ucPk_M46SG?Ic^qr{bC9P1sCR~blZ zC#k+#JZBrPegpLH#($vP<6A2`yhr;B&aJ^?HfUju zDZ4Gk@UpMfUf8v`cP4m^yJi=O@PSLq;ww8FE+5o_pPEcZEwx0BA(psc8`@F1F{kO; zxBUW|jtx3M{24g{^`SG^N^pE|Q&!@vyO)-QxZLR6*z_)meeij9caOC+tcZd9z)T#a-jMdGkJbz;<18SahOB)Gwbxr zSgw~_-Q2Yd$#&!RT+PEsqAfyPu#0_$IRNNx+XBm-Mce&2$84Rio<(o@_l}CXdHcH? z;e@VHI}{|l9>U;rZ;NFcrAcsCVYlIAoR`aOz++xsv{ykMI7P^+X;sj(cpu_FllPFQ z>D(@~^z=^^rjyls-6>POvUQFrU&khJMn1aZ2S=ioZy4#lSaEEXM)9PWm{^64Fh({< zLP^1?*sS!2Xm=rW`)-04bWLVZqEG+zE*(eVmroAAo!_FA050BS@+~u|ujy>O zZd85@qVGOTPiyL=u9pRBC*H*LS0)lL}=eD4XU zc8*ifI@4U8WNNjGFT17UrXFC{us3C|t%#tWeh>$-%V-Yc3R`Rz)Q~QKbqSM~=g1!$ zvQpaj`Dz|$G@4<-JxxZR$wedUE@1CVU^XJz!!G6bm2hD%?gOiXV2^;XTebzV#qlSb zo^6ynFF6J{=&<>9Z3gc*Qb(8JEAjKDVPucbyY`()o;T=(=-EIcojwPtxTSVN83r1l z&tB`~?JcJiaB=v0@(bSRY7*SbCjT^%GdLiC^F(yvPs(PK-@TAkSzPTKTVw+r%>Xh# zpv;ElQ+{8aWF5*sH*U^=0byuTP$BKr$ zr}eTaT5CSBv`{QZyi5^znw&|()T*f3!{vpQ{0c@)=yLaU z$l^g>E@tNAbZ@S5W1548%8SG=0X?tub*fZ>*LHVS8omQpnY*I6E2wZ%a=asRtxJ;i zj1e|wr52wp95IE>&CP$%M-FK?Ru=4X!~OFy=XFA*lGVbNY6hAnZ0^EMy%@3 zdF{aOoHHMuZyYv3y&h+I@%^+H=VIBfPPE`FNnc~t+%!F3xclBGF~@wCAT2i7vns4l z<@`uj^ON~T?mQ>{LQ?WfaW7iet2U|DYi8&toLg3scyu#F|F>UID;-Xb8!U!Uh*O?i z3+fjheP}qoxB;P|j-Bd#imf@`5X%;ix&t*i&6z4k;AE!{r}qoK#&keDS3T>E5aK{K z?HQ|up_h!GT?f!!kT@~&P#xVMD67@45qqrk*R|Da66ID$zmHh%vHZwf zt&WTgxyZ2T{Kx}bFC(-aM~Tab-eYrjZ=GH-klFsgV{eHx_$gmtAo{?eV_b6qmif!@ zID~vD0cP|1CjB*UE5lgRG#y=Q&Er$Xlub0Pk$OLM5R|LHm)CZ~_qy!J=H}+uR<8^2 zribIoQg?#y{YR}9n|o8ihgw`q$+qI!Z>JKHs#%wo166G!wyZv~B(&)e#o2h%6b&IA0fA^sm~oV*sCZN*2})j&2Ek89pq^-h850^OAN z3<~UKfy{>kBnCQ&6Ns9EHZqKK2?PKxFVj&b_`eRL{uf}B0G-~PSZr?(^zR3tuWPJR Ip@oY4AArrXj{pDw literal 2856 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 literal 2663 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 QzdH!Z@)~jtZ!IGJ4PLs}-~a#s diff --git a/scripts/smoke/visual-baselines/win32/wide/agent-panel-header.png b/scripts/smoke/visual-baselines/win32/wide/agent-panel-header.png index 84322abb44ee9f9d97224722f38a846dbdcdfa8f..08b6075b26b7c3ccda5a730720fd93be3ef217c6 100644 GIT binary patch literal 2451 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 literal 2798 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