From 7ffb9a6973925565c8df9a7dac8d7380c4799417 Mon Sep 17 00:00:00 2001 From: Logan Johnson Date: Mon, 21 Sep 2026 11:33:32 -0400 Subject: [PATCH] fix(beehive): discover desktop harnesses on startup Signed-off-by: Logan Johnson --- beehive/src/harness-discovery.ts | 34 +++++++++++++++++++++++--- beehive/src/manager-view.ts | 4 ++- beehive/test/harness-inventory.test.ts | 13 ++++++++++ 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/beehive/src/harness-discovery.ts b/beehive/src/harness-discovery.ts index bdf20ecc48..b736a23949 100644 --- a/beehive/src/harness-discovery.ts +++ b/beehive/src/harness-discovery.ts @@ -1,6 +1,6 @@ import contract from './harness-contract.json' with { type: 'json' }; import { accessSync, constants, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs'; -import { delimiter, join } from 'node:path'; +import { delimiter, isAbsolute, join } from 'node:path'; import { homedir } from 'node:os'; import { fileURLToPath } from 'node:url'; import { spawnOwned } from './owned.ts'; @@ -45,9 +45,26 @@ function nvmBin(home: string): string[] { try { const tag = read(join(root, 'alias/default')); if (safe(tag)) { const direct = directory(tag); if (direct.length) return direct; const hop = directory(read(join(root, 'alias', tag))); if (hop.length) return hop; } } catch { /* Missing tool metadata. */ } try { const tags = readdirSync(versions).slice(0, 1024).filter(t => /^v\d+\.\d+\.\d+(?:-.*)?$/.test(t)); tags.sort((a,b) => { const x = version(a.slice(1).split('-')[0]!)!, y = version(b.slice(1).split('-')[0]!)!; for (let i=0;i<3;i++) if (x[i] !== y[i]) return x[i]! > y[i]! ? -1 : 1; return 0; }); return tags[0] ? directory(tags[0]) : []; } catch { return []; } } -/** Explicit form-open discovery; no passive polling or cached stale generations. - * Inputs permit completely isolated fixture roots and fake probe executables. */ -export async function discoverHarnesses(signal: AbortSignal, options: { home?: string; path?: string; bundled?: string[]; common?: string[]; loginShells?: string[]; probe?: typeof harnessProbe } = {}): Promise { +type HarnessDefinition = { id: string; label: string; command: string }; +const validCustomId = (id: string) => /^[a-z0-9_][a-z0-9_-]*$/.test(id); +function customHarnesses(directory: string, reserved: Set): HarnessDefinition[] { + let files: string[]; + try { files = readdirSync(directory).filter(file => file.endsWith('.json')).slice(0, 1024); } catch { return []; } + const result: HarnessDefinition[] = []; + for (const file of files) try { + const path = join(directory, file); + if (statSync(path).size > 64 * 1024) continue; + const value = JSON.parse(readFileSync(path, 'utf8')) as Partial; + if (!value.id || !validCustomId(value.id) || reserved.has(value.id) || typeof value.label !== 'string' || !value.label.trim() || typeof value.command !== 'string' || !value.command.trim()) continue; + reserved.add(value.id); result.push({ id: value.id, label: value.label, command: value.command }); + } catch { /* One malformed Desktop definition must not suppress the rest. */ } + return result; +} + +/** Forced local discovery at startup or explicit refresh. Inputs permit isolated + * fixture roots and fake probe executables. Desktop custom definitions are + * included from the same app-data directory when that directory exists. */ +export async function discoverHarnesses(signal: AbortSignal, options: { home?: string; path?: string; bundled?: string[]; common?: string[]; loginShells?: string[]; customDirectories?: string[]; probe?: typeof harnessProbe } = {}): Promise { const home = options.home ?? homedir(), path = options.path ?? process.env.PATH ?? '', probe = options.probe ?? harnessProbe; const bundled = options.bundled ?? [fileURLToPath(new URL('../bin', import.meta.url)), '/Applications/Buzz.app/Contents/MacOS']; const data = process.platform === 'darwin' ? join(home, 'Library/Application Support') : join(home, '.local/share'); @@ -57,6 +74,7 @@ export async function discoverHarnesses(signal: AbortSignal, options: { home?: s const direct = (command: string, roots: string[]) => roots.filter(p => p.startsWith('/')).slice(0, 128).map(p => executable(join(p,command))).find(Boolean); let loginPath: string | undefined; async function resolve(command: string): Promise { + if (isAbsolute(command)) return executable(command); const found = direct(command, [...bundled, ...(['codex-acp','claude-agent-acp','claude-code-acp','node','npm'].includes(command) ? managed : []), ...path.split(delimiter)]); if (found) return found; if (loginPath === undefined) { loginPath = ''; @@ -88,5 +106,13 @@ export async function discoverHarnesses(signal: AbortSignal, options: { home?: s const reason = state === 'not-installed' ? 'Not installed' : state === 'cli-missing' ? `Underlying ${row.underlyingCli} CLI not installed` : state === 'incompatible' ? 'Codex ACP 1.10.0 or newer required; version unknown/outdated' : providers.length ? 'Detected; provider/model access unverified' : 'Detected; this catalog’s OS-backed providers are not supported by this adapter. Use existing local setup for native authentication.'; result.push({ id: row.id, label: row.label!, executable: binary, cli, ...(detectedVersion ? { version: detectedVersion } : {}), state, providers, reason }); } + const reserved = new Set(result.map(row => row.id)); + for (const directory of options.customDirectories ?? (process.platform === 'darwin' ? [join(data, 'xyz.block.buzz.app/custom_harnesses')] : [])) { + for (const definition of customHarnesses(directory, reserved)) { + signal.throwIfAborted(); + const binary = await resolve(definition.command); + result.push({ id: definition.id, label: definition.label, ...(binary ? { executable: binary } : {}), state: binary ? 'available' : 'not-installed', providers: [], reason: binary ? 'Detected from Buzz Desktop custom harnesses; native configuration is retained by that harness.' : 'Custom harness executable not installed' }); + } + } return result; } diff --git a/beehive/src/manager-view.ts b/beehive/src/manager-view.ts index 74dc68a8bf..e21168b9a6 100644 --- a/beehive/src/manager-view.ts +++ b/beehive/src/manager-view.ts @@ -8,7 +8,9 @@ const renderer = await createCliRenderer({ consoleMode: 'disabled', openConsoleOnError: false, }); -const shell = new OpenTuiShell(renderer, localHarnessInventory()); +const inventory = localHarnessInventory(); +const shell = new OpenTuiShell(renderer, inventory); +void inventory.refresh(); const stop = () => shell.close(); process.once('SIGINT', stop); process.once('SIGTERM', stop); diff --git a/beehive/test/harness-inventory.test.ts b/beehive/test/harness-inventory.test.ts index 81d1ffc68c..97d77d2f00 100644 --- a/beehive/test/harness-inventory.test.ts +++ b/beehive/test/harness-inventory.test.ts @@ -83,6 +83,19 @@ test('production adapter discovers and durably saves only an explicit isolated H } finally { rmSync(home, { recursive: true, force: true }); } }); +test('discovers Buzz Desktop custom harness definitions with absolute commands and skips malformed entries', async () => { + const root = mkdtempSync(join(tmpdir(), 'beehive-custom-harnesses-')); + try { + const command = join(root, 'pic-pi-acp'); writeFileSync(command, '#!/bin/sh\necho custom\n'); chmodSync(command, 0o700); + const definitions = join(root, 'custom_harnesses'); mkdirSync(definitions); + writeFileSync(join(definitions, 'custom.json'), JSON.stringify({ id: 'pic-pi', label: 'Pi custom', command, args: [] })); + writeFileSync(join(definitions, 'bad.json'), '{'); + const { discoverHarnesses } = await import('../src/harness-discovery.ts'); + const rows = await discoverHarnesses(new AbortController().signal, { home: root, path: '', bundled: [], common: [], loginShells: [], customDirectories: [definitions] }); + assert.deepEqual(rows.filter(value => value.id === 'pic-pi').map(value => ({ label: value.label, state: value.state, executable: value.executable })), [{ label: 'Pi custom', state: 'available', executable: realpathSync(command) }]); + } finally { rmSync(root, { recursive: true, force: true }); } +}); + test('isolated production adapter fails closed unless both fixture root and executable path are explicit', () => { assert.throws(() => localHarnessInventory({ BEEHIVE_HARNESS_ISOLATED: '1', BEEHIVE_HARNESS_PATH: '/fixture/bin' }), /explicit BEEHIVE_HOME/); assert.throws(() => localHarnessInventory({ BEEHIVE_HARNESS_ISOLATED: '1', BEEHIVE_HOME: '/fixture/home' }), /explicit BEEHIVE_HOME/);