feat(beehive): add volatile owner session and protected section gates

Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
Logan Johnson
2026-09-21 15:31:39 -04:00
parent 7e7556c724
commit f635c34bd9
21 changed files with 836 additions and 8 deletions

View File

@@ -174,3 +174,26 @@ credentials, relay, provider, host lifecycle or installed-package change was use
Narrow renderer checks cover the 40-column footer, input and pane routing; this is
not an exhaustive screenshot matrix for every prompt or launch error. Publication
and installed desktop/laptop smoke belong to the next continuation.
### Owner sign-in slice
The current OpenTUI manager uses a separate offline Node owner authority, not the
legacy saved-owner-key manager. Its header is SIGN IN / SIGNED IN. Signed-out
Host/Agents show the access gate. The full-width owner screen offers Desktop
identity first, then a masked nsec form. True first use chooses public relay
routing before sign-in. Existing controller/Host owner and relay bindings cannot
be changed by signing in; a different owner is refused.
Desktop availability is a prompt-free metadata check of one exact entry. Explicit
selection performs the session-only read and validates the identity. Keychain
presence alone cannot prove the blob is valid or access will be granted. See
`native/README.md` for the helper packaging and evidence boundary.
Manual nsec and Desktop session keys stay in the Node authority's memory. Only
public state and input length return to the renderer. First use saves the public
owner/relay binding only. Sign-out and quit discard the session; relaunch starts
signed out. No relay is contacted. Contextual success requires explicit Continue;
standalone success stays on the owner screen. Host/Agents content remains an
explicit unimplemented placeholder after sign-in. Host setup, relay discovery,
registration, lifecycle and agent controls are not part of this slice. Existing
advanced CLI and legacy manager flows are unchanged by this implementation.

View File

@@ -86,3 +86,27 @@ contract explicitly with `python3 beehive/tools/export-harnesses.py <reviewed-SH
from the repository root; its current source is Desktop main
`78618804ec86a014524ad7d1fb55928e8f5c3edf`. Keep the existing helper binary in
`bin/beehive-databricks`; do not build/install anything at runtime.
## Owner session helper
The OpenTUI manager also packages `beehive-owner-key`. Build it with the normal
repository toolchain and default features, scoped to this binary:
```sh
cargo build --locked --release -p buzz-agent --bin beehive-owner-key
install -m 700 target/release/beehive-owner-key "$PACKAGE/beehive/bin/beehive-owner-key"
```
This helper belongs only in the Beehive package. It makes no credential writes.
On macOS its exact query is generic password service `buzz-desktop`, account
`secrets`. The `probe` request returns metadata availability only, with Keychain
interaction disabled and authenticated items skipped. Presence is not proof that
the blob contains a valid owner identity; explicit `read` checks that next. Only
explicit Desktop sign-in reads the `identity` member. No fallback, enumeration,
agent-key selection or migration is permitted. Other platforms report unsupported;
manual owner nsec remains available. The Node authority bounds output, owns the
helper process and kills it on cancel or timeout. No secret returns to Bun.
Tests inject a synthetic executable or Desktop adapter. A fresh HOME is **not**
Keychain isolation. Do not run the native read against real Desktop secrets as a
smoke test. OS access/denial needs a separately authorized isolated Keychain test.

View File

@@ -1,5 +1,6 @@
import { createCliRenderer } from '@opentui/core';
import { OpenTuiShell } from './opentui-shell.ts';
import { localOwner } from './owner-client.ts';
import { localProviders } from './provider-client.ts';
import { localHarnessInventory } from './harness-inventory.ts';
@@ -10,7 +11,7 @@ const renderer = await createCliRenderer({
openConsoleOnError: false,
});
const inventory = localHarnessInventory();
const shell = new OpenTuiShell(renderer, inventory, undefined, localProviders());
const shell = new OpenTuiShell(renderer, inventory, undefined, localProviders(), localOwner());
void inventory.refresh();
const stop = () => shell.close();
process.once('SIGINT', stop);

View File

@@ -1,4 +1,6 @@
import { BoxRenderable, ScrollBoxRenderable, TextRenderable, type CliRenderer, type KeyEvent } from '@opentui/core';
import { OwnerPanel } from './owner-panel.ts';
import type { OwnerClient } from './owner-protocol.ts';
import { ProviderPanel } from './provider-panel.ts';
import type { ProviderClient } from './provider-protocol.ts';
import { brand, destinations, listWidth, ownerLabel, ShellState, type FocusRegion } from './shell-state.ts';
@@ -72,6 +74,7 @@ export class OpenTuiShell {
private unsubscribeInventory: () => void;
private screenGeneration = 0;
private providers?: ProviderPanel;
private owner?: OwnerPanel;
private readonly footerRule: TextRenderable;
private readonly footer: TextRenderable;
private helpScrim?: BoxRenderable;
@@ -81,7 +84,7 @@ export class OpenTuiShell {
private helpTitle?: TextRenderable;
private helpActions?: TextRenderable;
constructor(readonly renderer: CliRenderer, private readonly inventory: HarnessInventoryController, private readonly helpContent = helpBody, providers?: ProviderClient) {
constructor(readonly renderer: CliRenderer, private readonly inventory: HarnessInventoryController, private readonly helpContent = helpBody, providers?: ProviderClient, owner?: OwnerClient) {
this.inventorySnapshot = inventory.snapshot();
this.done = new Promise(resolve => { this.finish = resolve; });
this.root = new BoxRenderable(renderer, { width: '100%', height: '100%', flexDirection: 'column', backgroundColor: palette.surface });
@@ -109,6 +112,7 @@ export class OpenTuiShell {
this.divider = new TextRenderable(renderer, { width: 1, height: '100%', fg: palette.divider, content: '│' });
this.focusPerimeter = new BoxRenderable(renderer, { position: 'absolute', visible: false, border: true, borderColor: palette.focus, backgroundColor: 'transparent',
onMouseDown: event => {
if (this.state.ownerActive || this.state.protectedSection) { this.owner?.pointer(event.y); return; }
if (this.state.activeSection === 3) { this.providers?.pointer(event.y); return; }
if (this.state.focus === 'list') this.clickListSlot(event.y - 6);
else if (this.state.focus === 'detail' && event.y === 15) this.activateSelectedRow();
@@ -137,7 +141,8 @@ export class OpenTuiShell {
this.footer = new TextRenderable(renderer, { height: 1, fg: palette.muted, content: wideFooter }); this.root.add(this.footer);
if (providers) this.providers = new ProviderPanel(renderer, this.listPane, this.detailPane, this.state, providers, () => this.paint());
renderer.keyInput.on('paste', event => { if (this.providers?.paste(new TextDecoder().decode(event.bytes))) event.preventDefault(); });
if (owner) this.owner = new OwnerPanel(renderer, this.detailPane, this.state, owner, () => this.paint());
renderer.keyInput.on('paste', event => { if (this.owner?.paste(new TextDecoder().decode(event.bytes)) || this.providers?.paste(new TextDecoder().decode(event.bytes))) event.preventDefault(); });
renderer.keyInput.on('keypress', key => this.key(key));
renderer.on('resize', () => this.resize());
renderer.on('destroy', () => this.close(false));
@@ -167,7 +172,7 @@ export class OpenTuiShell {
}
private key(key: KeyEvent) {
if (!(key.ctrl && ['q', 'c'].includes(key.name)) && !this.state.belowMinimum && !this.state.helpOpen && this.providers?.key(key)) { key.preventDefault(); return; }
if (!(key.ctrl && ['q', 'c'].includes(key.name)) && !this.state.belowMinimum && !this.state.helpOpen && (this.owner?.key(key) || this.providers?.key(key))) { key.preventDefault(); return; }
const wasHarnesses = this.state.mode === 'section' && this.state.activeSection === 2;
const effect = this.state.key(key.name, { ctrl: key.ctrl, shift: key.shift });
if (wasHarnesses && !(this.state.mode === 'section' && this.state.activeSection === 2)) { this.screenGeneration++; this.inventory.cancel(); }
@@ -178,7 +183,7 @@ export class OpenTuiShell {
}
private activate(index: number) {
if (this.state.belowMinimum || this.state.helpOpen) return;
if (this.state.belowMinimum || this.state.helpOpen || this.owner?.modal) return;
const wasHarnesses = this.state.mode === 'section' && this.state.activeSection === 2;
this.state.activateHeader(index);
if (wasHarnesses && !(this.state.mode === 'section' && this.state.activeSection === 2)) { this.screenGeneration++; this.inventory.cancel(); }
@@ -209,7 +214,9 @@ export class OpenTuiShell {
private paint() {
if (this.closed) return;
const width = this.renderer.width;
this.ownerText.left = Math.max(0, width - ownerLabel.length - 1);
const label = this.owner ? this.state.signedIn ? 'SIGNED IN' : 'SIGN IN' : ownerLabel;
this.ownerText.content = label;
this.ownerText.left = Math.max(0, width - label.length - 1);
this.brandText.width = Math.max(0, this.ownerText.left - 1);
const rule = '─'.repeat(Math.max(1, width));
this.headerRule.content = rule; this.footerRule.content = rule;
@@ -219,7 +226,7 @@ export class OpenTuiShell {
const focused = this.state.focus === 'header' && this.state.headerIndex === index;
const active = this.state.mode === 'section' && this.state.activeSection === index;
item.bg = focused ? palette.selected : palette.surface;
item.fg = focused ? palette.selectedText : active ? palette.focus : index < 2 ? palette.muted : palette.text;
item.fg = focused ? palette.selectedText : active ? palette.focus : index < 2 && !this.state.signedIn ? palette.muted : palette.text;
});
const ownerFocused = this.state.focus === 'header' && this.state.headerIndex === destinations.length;
this.ownerText.bg = ownerFocused ? palette.selected : palette.surface;
@@ -235,6 +242,7 @@ export class OpenTuiShell {
}
this.paintHarnesses();
this.providers?.setActive(!this.state.belowMinimum && this.state.mode === 'section' && this.state.activeSection === 3);
this.owner?.paint();
const focusedPane = this.state.focus === 'list' || this.state.focus === 'detail' ? this.state.focus : undefined;
this.focusPerimeter.visible = Boolean(focusedPane);
if (focusedPane) {
@@ -336,6 +344,7 @@ export class OpenTuiShell {
this.unsubscribeInventory();
this.inventory.dispose();
this.providers?.dispose();
this.owner?.dispose();
if (destroy) this.renderer.destroy();
this.finish();
}

View File

@@ -0,0 +1,20 @@
import { OwnerService } from './owner-service.ts';
const service = new OwnerService(process.env.BEEHIVE_HOME);
service.subscribe(snapshot => { if (process.connected) process.send?.({ type: 'snapshot', snapshot }); });
let pending: Promise<boolean> | undefined;
let exiting = false;
process.on('message', async (message: any) => {
if (message.type === 'secret') service.secret(message.action, message.value);
else if (message.type === 'cancel') service.cancel();
else if (message.type === 'request') {
pending = service.request(message.request);
const ok = await pending;
if (process.connected && !exiting) process.send?.({ type: 'result', id: message.id, ok });
}
});
const stop = () => {
if (exiting) return; exiting = true; service.dispose();
void Promise.resolve(pending).finally(() => process.exit());
};
process.on('disconnect', stop);
process.on('SIGTERM', stop);

View File

@@ -0,0 +1,40 @@
import { fork } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { existsSync } from 'node:fs';
import type { OwnerClient, OwnerRequest, OwnerSnapshot } from './owner-protocol.ts';
/** Renderer transport: only public projections return from the Node authority. */
export function localOwner(options: { helper?: URL; environment?: NodeJS.ProcessEnv } = {}): OwnerClient {
const environment = options.environment ?? process.env;
const packagedNode = fileURLToPath(new URL('../runtime/node', import.meta.url));
const child = fork(fileURLToPath(options.helper ?? new URL('./owner-child.ts', import.meta.url)), [], {
execPath: existsSync(packagedNode) ? packagedNode : process.env.BEEHIVE_NODE || 'node',
execArgv: ['--experimental-transform-types'], silent: true,
env: { PATH: environment.PATH ?? '/usr/bin:/bin', ...(environment.HOME ? { HOME: environment.HOME } : {}), ...(environment.BEEHIVE_HOME ? { BEEHIVE_HOME: environment.BEEHIVE_HOME } : {}), ...(environment.DATABRICKS_HOST ? { DATABRICKS_HOST: environment.DATABRICKS_HOST } : {}) },
});
let snapshot: OwnerSnapshot = { signedIn: false, desktop: 'unknown', desktopReason: 'Checking identity availability.', phase: 'idle', message: 'Loading owner session…', secretLength: 0 };
let serial = 0, disposed = false;
const listeners = new Set<(snapshot: OwnerSnapshot) => void>();
const pending = new Map<number, (ok: boolean) => void>();
const emit = () => { for (const listener of listeners) listener(structuredClone(snapshot)); };
const unavailable = () => {
for (const resolve of pending.values()) resolve(false); pending.clear();
if (disposed) return;
snapshot = { ...snapshot, signedIn: false, owner: undefined, phase: 'error', secretLength: 0, message: 'Owner controller stopped. Reopen Beehive. Saved data was not reset.' }; emit();
};
child.stdout?.resume(); child.stderr?.resume(); child.on('error', unavailable); child.on('close', unavailable);
child.on('message', (message: any) => {
if (disposed) return;
if (message.type === 'snapshot') { snapshot = message.snapshot; emit(); }
else if (message.type === 'result') { pending.get(message.id)?.(message.ok === true); pending.delete(message.id); }
});
const send = (message: object) => { if (!disposed && child.connected) child.send(message, error => { if (error) unavailable(); }); else unavailable(); };
return {
snapshot: () => structuredClone(snapshot),
subscribe(listener) { listeners.add(listener); listener(structuredClone(snapshot)); return () => { listeners.delete(listener); }; },
request(request: OwnerRequest) { return new Promise(resolve => { const id = ++serial; pending.set(id, resolve); send({ type: 'request', id, request }); }); },
secret(action, value) { send({ type: 'secret', action, value }); },
cancel() { send({ type: 'cancel' }); },
dispose() { if (disposed) return; disposed = true; for (const resolve of pending.values()) resolve(false); pending.clear(); listeners.clear(); if (child.connected) child.disconnect(); else child.kill('SIGTERM'); },
};
}

31
beehive/src/owner-key.ts Normal file
View File

@@ -0,0 +1,31 @@
import { spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import { readBuzzOwnerKey } from './buzz-owner-key.ts';
/** Bounded exact-entry read. The no-UI native probe and explicit read never write Keychain. */
export async function desktopOwnerKey(probe: boolean, signal: AbortSignal, executable = fileURLToPath(new URL('../bin/beehive-owner-key', import.meta.url))): Promise<{ available?: boolean; secret?: string; reason?: string }> {
signal.throwIfAborted();
return new Promise(resolve => {
const child = spawn(executable, [], {
stdio: ['pipe', 'pipe', 'ignore'], env: { PATH: '/usr/bin:/bin', ...(process.env.HOME ? { HOME: process.env.HOME } : {}) },
});
let output = '', failed = false;
const stop = () => { failed = true; output = ''; child.kill('SIGKILL'); };
const timer = setTimeout(stop, 10000); signal.addEventListener('abort', stop, { once: true });
child.on('error', () => { failed = true; }); child.stdin.on('error', stop);
child.stdout.on('data', chunk => { if (failed) return; if (output.length + chunk.length > 4096) stop(); else output += chunk.toString(); });
child.on('close', code => {
clearTimeout(timer); signal.removeEventListener('abort', stop);
if (failed || signal.aborted || code !== 0) { resolve({ reason: 'access' }); return; }
try {
const value = JSON.parse(output); output = '';
if (value.reason) { resolve({ reason: value.reason }); return; }
if (probe) { resolve({ available: value.available === true }); return; }
const result = readBuzzOwnerKey(null, () => JSON.stringify({ identity: value.identity }));
resolve(result.ok ? { secret: result.secret } : { reason: result.reason });
} catch { resolve({ reason: 'malformed' }); }
});
child.stdin.end(probe ? 'probe' : 'read');
if (signal.aborted) stop();
});
}

153
beehive/src/owner-panel.ts Normal file
View File

@@ -0,0 +1,153 @@
import { BoxRenderable, TextRenderable, type CliRenderer, type KeyEvent } from '@opentui/core';
import { palette } from './opentui-shell.ts';
import { ProviderDialog } from './provider-dialog.ts';
import type { OwnerClient, OwnerSnapshot } from './owner-protocol.ts';
import { destinations, type ShellState } from './shell-state.ts';
/** Full-width owner screen and access gate. Only public session facts enter Bun. */
export class OwnerPanel {
private snapshot: OwnerSnapshot;
private unsubscribe: () => void;
private box: BoxRenderable;
private title: TextRenderable;
private facts: TextRenderable;
private labels: TextRenderable;
private heading: TextRenderable;
private actions: TextRenderable[] = [];
private message: TextRenderable;
private note: TextRenderable;
private dialog?: ProviderDialog;
private index = 0;
private actionTop = 0;
private relay = '';
private continuation?: number;
private prior?: { mode: ShellState['mode']; activeSection: number; headerIndex: number; focus: ShellState['focus'] };
private shown = false;
private generation = 0;
private wasOwner = false;
constructor(private renderer: CliRenderer, pane: BoxRenderable, private state: ShellState, private client: OwnerClient, private repaint: () => void) {
this.snapshot = client.snapshot();
this.box = new BoxRenderable(renderer, { width: '100%', height: '100%', backgroundColor: palette.surface }); pane.add(this.box);
const text = (fg: string) => { const t = new TextRenderable(renderer, { position: 'absolute', left: 2, fg, wrapMode: 'word' }); this.box.add(t); return t; };
this.title = text(palette.muted); this.facts = text(palette.text); this.labels = text(palette.muted); this.heading = text(palette.focus);
for (let i = 0; i < 4; i++) {
const action = text(palette.text); action.onMouseDown = () => { if (!this.shown || this.dialog || this.state.helpOpen) return; this.index = i; this.state.focus = 'detail'; this.repaint(); void this.run(); }; this.actions.push(action);
}
this.message = text(palette.text); this.note = text(palette.muted);
this.unsubscribe = client.subscribe(snapshot => { this.snapshot = snapshot; this.state.signedIn = snapshot.signedIn; this.dialog?.updateSecretLength(snapshot.secretLength); this.repaint(); });
}
get modal() { return !!this.dialog; }
private commands() {
if (!this.state.ownerActive) return this.snapshot.signedIn ? [] : ['Sign in'];
if (this.snapshot.signedIn) return [...(this.continuation === undefined ? [] : [`Continue to ${this.continuation === 0 ? 'Host' : 'Agents'}`]), 'Sign out'];
if (!this.snapshot.relay && !this.relay) return ['Choose a relay', 'Use Buzz Desktop identity', 'Provide owner nsec'];
return ['Use Buzz Desktop identity', 'Provide owner nsec', ...(this.snapshot.relay ? [] : ['Change relay']), 'Check Desktop identity'];
}
private disabled(command: string) { return command === 'Use Buzz Desktop identity' && (this.snapshot.desktop !== 'available' || !(this.snapshot.relay || this.relay)) || command === 'Provide owner nsec' && !(this.snapshot.relay || this.relay); }
private selectionNote(command: string) {
if (command === 'Use Buzz Desktop identity') return !(this.snapshot.relay || this.relay) ? 'Choose a relay first. No sign-in or connection happens yet.' : this.snapshot.desktopReason;
if (command === 'Provide owner nsec') return !(this.snapshot.relay || this.relay) ? 'Choose a relay first.' : 'Your nsec stays in memory until sign-out or quit.';
if (command === 'Sign out') return 'Signs out of Beehive. Host and agents keep running.';
if (command.startsWith('Continue')) return 'Opens the requested section. No Host or agent operation runs.';
if (command.includes('relay')) return 'Sets public routing on first sign-in. Does not connect or configure a Host.';
return 'Host and Agents require owner sign-in. Harnesses and Providers do not.';
}
private openOwner() {
this.prior = { mode: this.state.mode, activeSection: this.state.activeSection, headerIndex: this.state.headerIndex, focus: this.state.focus };
this.continuation = this.state.activeSection;
this.state.mode = 'owner'; this.state.headerIndex = destinations.length; this.state.focus = 'detail'; this.index = 0; this.repaint();
}
private back() {
this.generation++; this.client.cancel(); this.continuation = undefined;
if (this.prior) { Object.assign(this.state, this.prior); this.prior = undefined; }
else { this.state.mode = 'section'; this.state.headerIndex = this.state.activeSection; this.state.focus = 'header'; }
this.repaint();
}
key(key: KeyEvent) {
if (!this.shown) return false;
if (this.dialog) { this.dialog.key(key); return true; }
if (key.name === 'escape' && this.snapshot.phase === 'busy') { this.client.cancel(); return true; }
if (key.name === 'escape' && this.state.ownerActive) { this.back(); return true; }
if (key.name === 'a' && this.commands().length) { this.state.focus = 'detail'; this.repaint(); return true; }
if (this.state.focus !== 'detail') return false;
if (key.name === 'up') { if (this.index > 0) this.index--; else { this.state.focus = 'header'; this.state.headerIndex = this.state.ownerActive ? destinations.length : this.state.activeSection; } this.repaint(); return true; }
if (key.name === 'down') { this.index = Math.min(this.commands().length - 1, this.index + 1); this.repaint(); return true; }
if (key.name === 'left') { if (this.state.ownerActive) this.back(); else { this.state.focus = 'header'; this.repaint(); } return true; }
if (key.name === 'return') { void this.run(); return true; }
return false;
}
paste(value: string) { if (!this.dialog) return false; this.dialog.paste(value); return true; }
pointer(y: number) {
if (!this.shown || this.dialog || this.state.helpOpen) return;
const index = y - 3 - this.actionTop;
if (index < 0 || index >= this.commands().length) return;
this.index = index; this.state.focus = 'detail'; this.repaint(); void this.run();
}
private async run() {
if (this.dialog || !this.shown || this.snapshot.phase === 'busy') return;
const command = this.commands()[this.index]; if (!command || this.disabled(command)) return;
const generation = this.generation;
if (command === 'Sign in') { this.openOwner(); return; }
if (command.startsWith('Continue')) {
const target = this.continuation; this.continuation = undefined; this.prior = undefined;
if (target !== undefined && this.snapshot.signedIn) this.state.activateHeader(target);
this.repaint(); return;
}
if (command === 'Sign out') { this.continuation = undefined; this.prior = undefined; this.index = 0; await this.client.request({ action: 'signout' }); this.repaint(); return; }
if (command === 'Check Desktop identity') { await this.client.request({ action: 'probe' }); return; }
if (command.includes('relay')) {
const dialog = new ProviderDialog(this.renderer, 'CHOOSE A RELAY', [{ label: 'Relay URL', value: this.relay || 'wss://' }], () => {}, 'Use relay');
this.dialog = dialog; const result = await dialog.done; if (this.dialog === dialog) this.dialog = undefined;
if (result && generation === this.generation) { this.relay = result['Relay URL']!.trim(); this.index = 0; }
this.repaint(); return;
}
if (command === 'Provide owner nsec') {
this.client.secret('clear');
const dialog = new ProviderDialog(this.renderer, 'PROVIDE OWNER NSEC', [{ label: 'Owner nsec', secret: true }], (action, value) => this.client.secret(action, value), 'Sign in');
this.dialog = dialog; const result = await dialog.done; if (this.dialog === dialog) this.dialog = undefined;
if (!result || generation !== this.generation) { this.repaint(); return; }
}
const ok = await this.client.request({ action: command === 'Use Buzz Desktop identity' ? 'signin-desktop' : 'signin-nsec', relay: this.relay });
if (ok && generation === this.generation) { this.index = 0; this.state.focus = 'header'; this.state.headerIndex = destinations.length; }
this.repaint();
}
paint() {
const owner = this.state.ownerActive;
const shown = !this.state.belowMinimum && (owner || this.state.protectedSection);
const left = (this.wasOwner && !owner) || (this.shown && !shown);
const entered = owner && !this.wasOwner;
this.wasOwner = owner; this.shown = shown; this.box.visible = shown;
if (left) {
this.generation++; this.dialog?.cancel(); this.dialog = undefined;
if (this.snapshot.phase === 'busy') this.client.cancel();
this.continuation = undefined; this.prior = undefined; this.index = 0;
}
if (!shown) return;
if (entered && !this.snapshot.signedIn && this.snapshot.desktop === 'unknown') void this.client.request({ action: 'probe' });
const width = this.renderer.width - 4, height = this.renderer.height - 5;
const wrap = (value: string) => { const rows: string[] = []; for (const line of value.split('\n')) { let rest = line; while (rest.length > width) { const at = rest.lastIndexOf(' ', width); const end = at > 0 ? at : width; rows.push(rest.slice(0, end)); rest = rest.slice(end).trimStart(); } rows.push(rest); } return rows; };
const truncate = (value: string) => value.length > width - 8 ? value.slice(0, Math.ceil((width - 9) / 2)) + '…' + value.slice(-Math.floor((width - 9) / 2)) : value;
const commands = this.commands(); this.index = Math.max(0, Math.min(this.index, commands.length - 1));
this.title.top = 1; this.title.width = width; this.title.height = 1;
this.title.content = owner ? this.snapshot.signedIn ? 'SIGNED IN' : this.continuation === undefined ? 'OWNER SIGN-IN' : 'SIGN-IN REQUIRED' : destinations[this.state.activeSection]!;
this.facts.top = 2; this.facts.left = owner ? 10 : 2; this.facts.width = owner ? width - 8 : width;
this.labels.top = 2; this.labels.width = 8; this.labels.height = 2; this.labels.visible = owner; this.labels.content = 'Relay\nOwner';
this.facts.content = owner ? `${truncate(this.snapshot.relay || this.relay || 'Not configured')}\n${truncate(this.snapshot.owner || this.snapshot.boundOwner || 'Not configured')}` : this.snapshot.signedIn ? 'Owner access granted.\nThis section is not implemented in this slice.' : `Sign in to view and manage ${this.state.activeSection === 0 ? 'Host' : 'Agents'}.`;
this.facts.height = owner || this.snapshot.signedIn ? 2 : 1;
this.heading.top = 4; this.heading.width = width; this.heading.height = 1; this.heading.visible = commands.length > 0;
this.heading.content = 'AVAILABLE ACTIONS ' + '─'.repeat(Math.max(1, width - 18));
this.actionTop = 5;
this.actions.forEach((action, i) => { action.visible = i < commands.length; action.top = this.actionTop + i; action.width = width; action.height = 1;
action.content = `${this.state.focus === 'detail' && i === this.index ? '›' : ' '} ${commands[i] ?? ''}`;
action.fg = this.disabled(commands[i] ?? '') ? palette.muted : this.state.focus === 'detail' && i === this.index ? palette.focus : palette.text;
});
const messageTop = this.actionTop + commands.length;
this.message.top = messageTop; this.message.width = width; this.message.height = Math.max(1, height - messageTop - 3);
this.message.content = owner ? this.snapshot.message : '';
this.message.fg = this.snapshot.phase === 'error' ? palette.failure : palette.text;
this.note.top = height - 3; this.note.height = 2; this.note.width = width;
this.note.content = wrap(this.selectionNote(commands[this.index] ?? '')).join('\n');
this.dialog?.paint();
}
dispose() { this.generation++; this.dialog?.cancel(); this.unsubscribe(); this.client.dispose(); }
}

View File

@@ -0,0 +1,15 @@
/** Public owner-session projection. No credential, signer or secret is serializable here. */
export type OwnerSnapshot = {
signedIn: boolean; owner?: string; boundOwner?: string; relay?: string;
desktop: 'unknown' | 'available' | 'unavailable'; desktopReason: string;
phase: 'idle' | 'busy' | 'error'; message: string; secretLength: number;
};
export type OwnerRequest = { action: 'probe' | 'signin-nsec' | 'signin-desktop' | 'signout'; relay?: string };
export interface OwnerClient {
snapshot(): OwnerSnapshot;
subscribe(listener: (snapshot: OwnerSnapshot) => void): () => void;
request(request: OwnerRequest): Promise<boolean>;
secret(action: 'append' | 'backspace' | 'clear', value?: string): void;
cancel(): void;
dispose(): void;
}

View File

@@ -0,0 +1,102 @@
import { existsSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
import { nip19 } from 'nostr-tools';
import { readControllerConfig, createControllerConfig } from './controller-config.ts';
import { readHostIdentityPublic } from './host-identity.ts';
import { agentNsec } from './settings-credentials.ts';
import { publicKey } from './protocol.ts';
import { desktopOwnerKey } from './owner-key.ts';
import type { OwnerClient, OwnerRequest, OwnerSnapshot } from './owner-protocol.ts';
const missing = 'Desktop identity unavailable. Open and unlock Buzz, then check again or provide an owner nsec.';
/** Offline Node authority. Session keys have no persistence, snapshot, or relay path. */
export class OwnerService implements OwnerClient {
private current: OwnerSnapshot = { signedIn: false, desktop: 'unknown', desktopReason: 'Identity availability has not been checked.', phase: 'idle', message: 'Sign in to manage Host and Agents.', secretLength: 0 };
private listeners = new Set<(snapshot: OwnerSnapshot) => void>();
private input = '';
private session?: string;
private active?: AbortController;
private generation = 0;
private disposed = false;
constructor(private home = homedir(), private desktop = desktopOwnerKey) {
try { this.projectBinding(); } catch { this.current.phase = 'error'; this.current.message = 'Saved owner routing is invalid or inconsistent. Repair the retained configuration; nothing was reset.'; }
}
private binding() {
const controller = readControllerConfig(join(this.home, '.beehive', 'owner'));
const directory = join(this.home, '.beehive', 'host');
const host = existsSync(join(directory, 'host-identity.json')) ? readHostIdentityPublic(directory).pairing : undefined;
if (existsSync(join(directory, 'setup.json')) && !host) throw Error('Legacy Host needs explicit migration');
if (controller && host && (controller.owner !== host.owner || controller.relay !== host.relay)) throw Error('Conflicting bindings');
return controller ?? (host ? { owner: host.owner, relay: host.relay } : undefined);
}
private projectBinding() { const b = this.binding(); this.current.boundOwner = b ? nip19.npubEncode(b.owner) : undefined; this.current.relay = b?.relay; }
snapshot() { return structuredClone(this.current); }
subscribe(listener: (snapshot: OwnerSnapshot) => void) { this.listeners.add(listener); listener(this.snapshot()); return () => { this.listeners.delete(listener); }; }
private publish() { if (!this.disposed) for (const listener of this.listeners) listener(this.snapshot()); }
secret(action: 'append' | 'backspace' | 'clear', value = '') {
if (this.disposed || this.active || this.current.signedIn) return;
if (action === 'clear') this.input = '';
else if (action === 'backspace') this.input = this.input.slice(0, -1);
else if (action === 'append') this.input = (this.input + value.replace(/[\x00-\x1f\x7f]/g, '')).slice(0, 256);
this.current.secretLength = this.input.length; this.publish();
}
async request(request: OwnerRequest) {
if (this.disposed) return false;
if (request.action === 'signout') {
this.cancel(); this.session = undefined; this.current.signedIn = false; this.current.owner = undefined;
this.current.message = 'Signed out. Host and agents keep running.'; this.publish(); return true;
}
if (this.active || this.current.signedIn) return false;
const abort = new AbortController(), generation = ++this.generation;
this.active = abort; this.current.phase = 'busy'; this.current.message = request.action === 'probe' ? 'Checking Desktop identity…' : 'Signing in… Esc cancels.'; this.publish();
let error = 'Sign-in failed. No session was opened. Check the retained owner configuration and try again.';
const check = () => { abort.signal.throwIfAborted(); if (generation !== this.generation || this.disposed) throw Error('Cancelled'); };
try {
if (request.action === 'probe') {
const result = await this.desktop(true, abort.signal); check();
this.current.desktop = result.available ? 'available' : 'unavailable';
this.current.desktopReason = result.available ? 'Desktop entry detected. Sign-in checks identity and access.' : missing;
this.current.message = result.available ? 'Buzz Desktop identity is available.' : missing;
} else {
const before = this.binding();
const relay = before?.relay ?? request.relay?.trim();
error = 'Enter a relay URL using wss:// (or ws://localhost for local testing). Nothing was changed.';
if (!relay) throw Error();
const url = new URL(relay);
if (url.username || url.password || url.hash || !(url.protocol === 'wss:' || (url.protocol === 'ws:' && ['localhost', '127.0.0.1', '[::1]'].includes(url.hostname)))) throw Error();
let secret: string;
if (request.action === 'signin-desktop') {
error = 'Buzz Desktop key access failed or was cancelled. Unlock Keychain and check again, or provide an owner nsec. No session was opened.';
const result = await this.desktop(false, abort.signal); check();
if (!result.secret) throw Error();
secret = result.secret;
} else if (request.action === 'signin-nsec') {
error = 'Enter a valid owner nsec. No session was opened and nothing was saved.';
secret = agentNsec(this.input);
} else throw Error('Unsupported action');
const owner = publicKey(secret); check();
error = 'This identity does not match the configured owner. Use the matching identity. Owner and relay were not changed.';
const now = this.binding();
if (now && (now.owner !== owner || now.relay !== relay)) throw Error();
error = 'Owner routing changed or could not be saved. Reopen sign-in and check the retained configuration. No session was opened.';
if (!now) createControllerConfig(join(this.home, '.beehive', 'owner'), owner, relay);
// Read back the sole public authority after first-use activation.
const saved = this.binding();
if (saved?.owner !== owner || saved.relay !== relay) throw Error();
check(); this.session = secret; this.current.signedIn = true; this.current.owner = nip19.npubEncode(owner); this.projectBinding();
this.current.message = 'Signed in. No Host, agent, or relay operation was started.';
}
this.current.phase = 'idle'; return true;
} catch {
if (generation === this.generation && !this.disposed) { this.current.phase = 'error'; this.current.message = error; }
return false;
} finally {
if (generation === this.generation) { this.input = ''; this.current.secretLength = 0; }
if (this.active === abort) this.active = undefined;
this.publish();
}
}
cancel() { this.generation++; this.active?.abort(); this.active = undefined; this.input = ''; this.current.secretLength = 0; this.current.phase = 'idle'; this.current.message = 'Sign-in cancelled. No requested action was run.'; this.publish(); }
dispose() { this.cancel(); this.session = undefined; this.current.owner = undefined; this.current.signedIn = false; this.disposed = true; this.listeners.clear(); }
}

View File

@@ -107,7 +107,7 @@ export class ProviderDialog {
// First input replaces it; the existing empty-secret save contract retains it.
const value = field.secret ? '•'.repeat(Math.min(field.savedSecret ? 12 : this.secretLength, width - 10)) : field.value ?? '';
const focused = this.index === i;
const prompt = field.secret ? 'Enter API key' : `Enter ${field.label.toLowerCase()}`;
const prompt = field.secret && field.label === 'API key' ? 'Enter API key' : `Enter ${field.label.toLowerCase()}`;
const suffix = field.choices ? ' ← →' : '';
row.content = (value || prompt).slice(-(width - 10 - suffix.length)).padEnd(width - 10 - suffix.length) + suffix;
row.fg = value ? palette.text : palette.muted;

View File

@@ -10,6 +10,7 @@ export type HarnessListRow = Readonly<{ id: string; kind: 'harness' | 'command'
/** Framework-independent state for the never-signed-in shell tracer. */
export class ShellState {
signedIn = false;
headerIndex = 0;
activeSection = 0;
focus: FocusRegion = 'header';

View File

@@ -0,0 +1,29 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, writeFileSync, rmSync, existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { nip19 } from 'nostr-tools';
import { localOwner } from '../src/owner-client.ts';
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
test('real private IPC binds public snapshots to session, cancel, signout and restart', async () => {
const home = mkdtempSync(join(tmpdir(), 'owner-tui-fixture-'));
writeFileSync(join(home, 'desktop-mode'), 'available');
const open = () => localOwner({ helper: new URL('./owner-tui-fixture-child.ts', import.meta.url), environment: { PATH: process.env.PATH, BEEHIVE_HOME: home } });
let client = open(); const snapshots: unknown[] = []; client.subscribe(s => snapshots.push(s));
try {
assert.equal(await client.request({ action: 'probe' }), true); assert.equal(client.snapshot().desktop, 'available');
const nsec = nip19.nsecEncode(Buffer.from('1'.repeat(64), 'hex'));
client.secret('append', nsec);
assert.equal(await client.request({ action: 'signin-nsec', relay: 'wss://relay.example' }), true);
assert.equal(client.snapshot().signedIn, true);
assert.doesNotMatch(JSON.stringify(snapshots), new RegExp(nsec));
await client.request({ action: 'signout' }); assert.equal(client.snapshot().signedIn, false);
writeFileSync(join(home, 'desktop-mode'), 'delayed');
const pending = client.request({ action: 'signin-desktop' }); await sleep(80); client.cancel();
assert.equal(await pending, false); assert.equal(client.snapshot().signedIn, false);
client.dispose(); client = open();
await client.request({ action: 'probe' }); assert.equal(client.snapshot().signedIn, false);
assert.equal(existsSync(join(home, '.beehive/owner/controller.json')), true);
} finally { client.dispose(); rmSync(home, { recursive: true, force: true }); }
});

View File

@@ -0,0 +1,24 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { desktopOwnerKey } from '../src/owner-key.ts';
import { nip19 } from 'nostr-tools';
test('native adapter handles metadata, explicit identity, malformed/denied output and abort without a real Keychain', async () => {
const home = mkdtempSync('/tmp/owner-key-adapter-'), executable = home+'/helper';
const nsec = nip19.nsecEncode(Buffer.from('1'.repeat(64),'hex'));
const helper = (body: string) => writeFileSync(executable, `#!${process.execPath}\n${body}`, {mode:0o700});
try {
helper(`let input='';process.stdin.on('data',v=>input+=v);process.stdin.on('end',()=>console.log(JSON.stringify(input==='probe'?{available:true}:{identity:${JSON.stringify(nsec)}})));`);
assert.deepEqual(await desktopOwnerKey(true,new AbortController().signal,executable),{available:true});
assert.deepEqual(await desktopOwnerKey(false,new AbortController().signal,executable),{secret:'1'.repeat(64)});
for(const value of [{reason:'access'},{identity:'malformed'},{agent:nsec}]) {
helper(`console.log(${JSON.stringify(JSON.stringify(value))})`);
assert.equal((await desktopOwnerKey(false,new AbortController().signal,executable)).secret,undefined);
}
helper(`process.stdin.resume();setTimeout(()=>console.log(JSON.stringify({identity:${JSON.stringify(nsec)}})),30000);`);
const abort = new AbortController(), pending=desktopOwnerKey(false,abort.signal,executable);
abort.abort(); assert.deepEqual(await pending,{reason:'access'});
helper(`console.log('x'.repeat(5000))`);
assert.equal((await desktopOwnerKey(false,new AbortController().signal,executable)).secret,undefined);
} finally {rmSync(home,{recursive:true,force:true});}
});

View File

@@ -0,0 +1,115 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync, readFileSync, existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { nip19 } from 'nostr-tools';
import { OwnerService } from '../src/owner-service.ts';
import { publicKey } from '../src/protocol.ts';
import { createControllerConfig } from '../src/controller-config.ts';
import { bootstrapHostIdentity } from '../src/host-identity.ts';
const secret = '1'.repeat(64), other = '2'.repeat(64);
const nsec = nip19.nsecEncode(Buffer.from(secret, 'hex'));
const relay = 'wss://relay.example';
function fixture(desktop: ConstructorParameters<typeof OwnerService>[1] = async probe => probe ? ({ available: true }) : ({ secret })) {
const home = mkdtempSync(join(tmpdir(), 'owner-session-'));
const service = new OwnerService(home, desktop);
return { home, service, close() { service.dispose(); rmSync(home, { recursive: true, force: true }); } };
}
test('nsec session persists only public binding; signout and reopen never restore a signer', async () => {
const f = fixture();
try {
f.service.secret('append', nsec);
assert.equal(await f.service.request({ action: 'signin-nsec', relay }), true);
assert.equal(f.service.snapshot().signedIn, true);
const file = join(f.home, '.beehive/owner/controller.json');
const saved = readFileSync(file, 'utf8');
assert.deepEqual(JSON.parse(saved), { version: 1, owner: publicKey(secret), relay });
assert.ok(!saved.includes(nsec) && !saved.includes(secret));
assert.doesNotMatch(JSON.stringify(f.service.snapshot()), new RegExp(nsec+'|'+secret));
assert.equal(f.service.snapshot().secretLength, 0);
assert.equal(existsSync(join(f.home, '.beehive/host')), false);
await f.service.request({ action: 'signout' });
assert.equal(f.service.snapshot().signedIn, false); assert.equal(f.service.snapshot().owner, undefined);
const reopened = new OwnerService(f.home, async () => { throw Error('No automatic Keychain read'); });
assert.equal(reopened.snapshot().signedIn, false); reopened.dispose();
assert.equal(readFileSync(file, 'utf8'), saved);
} finally { f.close(); }
});
test('Desktop probe never creates session or files; explicit Desktop sign-in derives matching owner', async () => {
const calls: boolean[] = [], f = fixture(async probe => { calls.push(probe); return probe ? { available: true } : { secret }; });
try {
await f.service.request({ action: 'probe' });
assert.deepEqual(calls, [true]); assert.equal(f.service.snapshot().desktop, 'available');
assert.equal(f.service.snapshot().signedIn, false); assert.equal(existsSync(join(f.home, '.beehive')), false);
assert.equal(await f.service.request({ action: 'signin-desktop', relay }), true);
assert.deepEqual(calls, [true, false]); assert.equal(f.service.snapshot().owner, nip19.npubEncode(publicKey(secret)));
} finally { f.close(); }
});
test('invalid nsec, invalid relay and missing/denied Desktop never write routing or leak failure text', async () => {
const f = fixture(async () => ({ reason: 'access secret-never-print' }));
try {
await f.service.request({ action: 'probe' }); assert.equal(f.service.snapshot().desktop, 'unavailable');
for (const action of ['signin-nsec', 'signin-desktop'] as const) {
f.service.secret('append', 'invalid-secret-never-print');
assert.equal(await f.service.request({ action, relay }), false);
assert.equal(f.service.snapshot().signedIn, false);
assert.doesNotMatch(JSON.stringify(f.service.snapshot()), /secret-never-print/);
}
for (const bad of ['', 'https://relay.example', 'ws://remote.example', 'wss://name:password@relay.example']) {
f.service.secret('append', nsec); assert.equal(await f.service.request({ action: 'signin-nsec', relay: bad }), false);
}
assert.equal(existsSync(join(f.home, '.beehive')), false);
} finally { f.close(); }
});
test('existing Host owner and relay are authority; mismatch leaves all public state unchanged', async () => {
const f = fixture(); const keys = new Map<string, string>();
try {
bootstrapHostIdentity(join(f.home, '.beehive/host'), 'Fixture Host', publicKey(other), relay, {
read: ref => keys.get(ref.publicKey) ?? null, create: (ref, value) => { keys.set(ref.publicKey, value); }, remove: ref => { keys.delete(ref.publicKey); },
});
const file = join(f.home, '.beehive/host/host-identity.json'), before = readFileSync(file, 'utf8');
assert.equal(await f.service.request({ action: 'signin-desktop', relay }), false);
assert.match(f.service.snapshot().message, /does not match/);
assert.equal(readFileSync(file, 'utf8'), before);
assert.equal(existsSync(join(f.home, '.beehive/owner')), false);
f.service.secret('append', nip19.nsecEncode(Buffer.from(other, 'hex')));
assert.equal(await f.service.request({ action: 'signin-nsec', relay: 'wss://ignored.example' }), true);
assert.equal(f.service.snapshot().relay, relay);
} finally { f.close(); }
});
test('concurrent first-use winner is rechecked after credential completion', async () => {
let finish!: (value: { secret: string }) => void;
const f = fixture(() => new Promise(resolve => finish = resolve));
try {
const work = f.service.request({ action: 'signin-desktop', relay });
createControllerConfig(join(f.home, '.beehive/owner'), publicKey(other), relay);
finish({ secret }); assert.equal(await work, false);
assert.equal(f.service.snapshot().signedIn, false);
assert.equal(JSON.parse(readFileSync(join(f.home, '.beehive/owner/controller.json'), 'utf8')).owner, publicKey(other));
} finally { f.close(); }
});
for (const stop of ['cancel', 'signout', 'dispose'] as const) test(`${stop} fences late Desktop completion and clears input`, async () => {
let finish!: (value: { secret: string }) => void;
const f = fixture(() => new Promise(resolve => finish = resolve));
try {
const work = f.service.request({ action: 'signin-desktop', relay });
if (stop === 'signout') await f.service.request({ action: 'signout' }); else f.service[stop]();
finish({ secret }); assert.equal(await work, false);
assert.equal(f.service.snapshot().signedIn, false); assert.equal(f.service.snapshot().secretLength, 0);
assert.equal(existsSync(join(f.home, '.beehive')), false);
} finally { f.close(); }
});
test('cancelled read cannot block or overwrite a replacement session', async () => {
let finish!: (value: { secret: string }) => void;
const f = fixture(() => new Promise(resolve => finish = resolve));
try {
const old = f.service.request({ action: 'signin-desktop', relay });
f.service.cancel(); f.service.secret('append', nsec);
assert.equal(await f.service.request({ action: 'signin-nsec', relay }), true);
finish({ secret: other }); assert.equal(await old, false);
assert.equal(f.service.snapshot().owner, nip19.npubEncode(publicKey(secret)));
assert.equal(f.service.snapshot().signedIn, true);
} finally { f.close(); }
});

View File

@@ -0,0 +1,21 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { OwnerService } from '../src/owner-service.ts';
const home = process.env.BEEHIVE_HOME!;
if (!home?.includes('owner-tui-fixture-')) throw Error('Isolated owner fixture required');
const service = new OwnerService(home, async (probe, signal) => {
const mode = readFileSync(join(home, 'desktop-mode'), 'utf8').trim();
if (mode === 'delayed') await new Promise(resolve => setTimeout(resolve, 1200));
signal.throwIfAborted();
return mode === 'missing' || mode === 'denied' ? { reason: mode === 'missing' ? 'missing' : 'access' } : probe ? { available: true } : { secret: mode === 'mismatch' ? '2'.repeat(64) : '1'.repeat(64) };
});
service.subscribe(snapshot => { if (process.connected) process.send?.({ type: 'snapshot', snapshot }); });
process.on('message', async (message: any) => {
if (message.type === 'secret') service.secret(message.action, message.value);
else if (message.type === 'cancel') service.cancel();
else if (message.type === 'request') {
const ok = await service.request(message.request);
if (process.connected) process.send?.({ type: 'result', id: message.id, ok });
}
});
process.on('disconnect', () => { service.dispose(); process.exit(); });

View File

@@ -0,0 +1,11 @@
import { createCliRenderer } from '@opentui/core';
import { OpenTuiShell } from '../src/opentui-shell.ts';
import { localOwner } from '../src/owner-client.ts';
import { HarnessInventoryController } from '../src/harness-inventory.ts';
if (!process.env.BEEHIVE_HOME?.includes('owner-tui-fixture-')) throw Error('Isolated owner fixture required');
const renderer = await createCliRenderer({ exitOnCtrlC: false, screenMode: 'alternate-screen', consoleMode: 'disabled', openConsoleOnError: false });
const inventory = new HarnessInventoryController({ read: () => ({ version: 1, revision: 0, agents: [], providers: [], runtimes: [] }), save: candidate => candidate }, async () => []);
const client = localOwner({ helper: new URL('./owner-tui-fixture-child.ts', import.meta.url) });
const shell = new OpenTuiShell(renderer, inventory, undefined, undefined, client);
process.once('SIGTERM', () => shell.close()); process.once('SIGINT', () => shell.close());
await shell.done;

View File

@@ -0,0 +1,49 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs';
import { createTestRenderer } from '@opentui/core/testing';
import { OpenTuiShell } from '../src/opentui-shell.ts';
import { HarnessInventoryController } from '../src/harness-inventory.ts';
import { localOwner } from '../src/owner-client.ts';
import { nip19 } from 'nostr-tools';
for (const [width, height] of [[120,40],[60,20]]) test(`real owner IPC: form failure/recovery, Desktop denial/mismatch, navigation fencing at ${width}x${height}`, async () => {
const home = mkdtempSync('/tmp/owner-tui-fixture-shell-');
const mode = (value: string) => writeFileSync(home+'/desktop-mode', value);
mode('available');
const ui = await createTestRenderer({width,height,exitOnCtrlC:false});
const client = localOwner({helper:new URL('./owner-tui-fixture-child.ts',import.meta.url),environment:{HOME:home,BEEHIVE_HOME:home,PATH:process.env.PATH}});
const inventory = new HarnessInventoryController({read:()=>({version:1,revision:0,agents:[],providers:[],runtimes:[]}),save:c=>c},async()=>[]);
const shell = new OpenTuiShell(ui.renderer,inventory,undefined,undefined,client);
const wait = async (predicate:()=>boolean) => { for(let i=0;i<200;i++) { if(predicate()) { await ui.renderOnce(); return; } await new Promise(r=>setTimeout(r,10)); } assert.fail('Owner state did not settle'); };
const input = ui.mockInput;
const enter = () => input.pressEnter();
const submit = () => { input.pressTab(); enter(); };
try {
await wait(()=>!client.snapshot().message.includes('Loading'));
for(let i=0;i<4;i++) input.pressArrow('right');
await wait(()=>client.snapshot().desktop==='available');
enter(); enter(); await ui.renderOnce();
input.typeText('relay.example'); submit(); await new Promise(r=>setTimeout(r,0)); await ui.renderOnce(); assert.doesNotMatch(ui.captureCharFrame(),/CHOOSE A RELAY/);
input.pressArrow('down'); enter(); input.typeText('invalid'); submit();
await wait(()=>client.snapshot().phase==='error');
assert.match(ui.captureCharFrame(),/valid owner nsec/);
enter(); input.typeText(nip19.nsecEncode(Buffer.from('1'.repeat(64),'hex')));
await wait(()=>client.snapshot().secretLength>0);
assert.doesNotMatch(ui.captureCharFrame(),/nsec1/); assert.match(ui.captureCharFrame(),/••/);
submit(); await wait(()=>client.snapshot().signedIn && shell.state.focus==='header');
assert.doesNotMatch(ui.captureCharFrame(),/Continue to/);
enter(); enter(); await wait(()=>!client.snapshot().signedIn);
mode('denied'); enter(); await wait(()=>client.snapshot().phase==='error');
assert.match(ui.captureCharFrame(),/key access failed/);
mode('mismatch'); enter(); await wait(()=>client.snapshot().phase==='error' && client.snapshot().message.includes('does not match'));
assert.match(ui.captureCharFrame(),/does not match/);
mode('delayed'); enter(); await wait(()=>client.snapshot().phase==='busy');
input.pressArrow('up'); input.pressArrow('left');
assert.equal(shell.state.activeSection,3);
await new Promise(r=>setTimeout(r,1400));
assert.equal(client.snapshot().signedIn,false); assert.equal(shell.state.activeSection,3);
input.pressArrow('right'); mode('available'); enter(); enter();
await wait(()=>client.snapshot().signedIn && shell.state.focus==='header');
assert.match(ui.captureCharFrame(),/SIGNED IN/);
} finally {shell.close();rmSync(home,{recursive:true,force:true});}
});

View File

@@ -0,0 +1,61 @@
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createTestRenderer } from '@opentui/core/testing';
import { OpenTuiShell } from '../src/opentui-shell.ts';
import { HarnessInventoryController } from '../src/harness-inventory.ts';
import type { OwnerClient, OwnerRequest, OwnerSnapshot } from '../src/owner-protocol.ts';
function fixture() {
const snapshot: OwnerSnapshot = { signedIn: false, desktop: 'available', desktopReason: 'Uses the Buzz Desktop identity in Keychain.', phase: 'idle', message: 'Sign in to manage Host and Agents.', secretLength: 0, relay: 'wss://relay.example' };
const requests: OwnerRequest[] = [], listeners = new Set<(s: OwnerSnapshot) => void>();
const emit = () => { for (const fn of listeners) fn(structuredClone(snapshot)); };
const client: OwnerClient = {
snapshot: () => structuredClone(snapshot), subscribe(fn) { listeners.add(fn); fn(structuredClone(snapshot)); return () => { listeners.delete(fn); }; },
async request(r) { requests.push(r); if (r.action.startsWith('signin')) { snapshot.signedIn = true; snapshot.owner = 'npub1fixture'; snapshot.secretLength = 0; snapshot.message = 'Signed in. No operation was started.'; } else if (r.action === 'signout') { snapshot.signedIn = false; snapshot.owner = undefined; snapshot.message = 'Signed out. Host and agents keep running.'; } emit(); return true; },
secret(action, value) { snapshot.secretLength = action === 'clear' ? 0 : action === 'backspace' ? Math.max(0, snapshot.secretLength - 1) : snapshot.secretLength + (value?.length ?? 0); emit(); },
cancel() {}, dispose() {},
};
const inventory = new HarnessInventoryController({ read: () => ({ version: 1, revision: 0, agents: [], providers: [], runtimes: [] }), save: candidate => candidate }, async () => []);
return { snapshot, requests, client, inventory, emit };
}
for (const [width, height] of [[120,40],[60,20]]) test(`owner standalone sign-in, explicit contextual continuation, and signout at ${width}x${height}`, async () => {
const ui = await createTestRenderer({ width, height, exitOnCtrlC: false }), f = fixture();
const shell = new OpenTuiShell(ui.renderer, f.inventory, undefined, undefined, f.client);
try {
await ui.renderOnce(); assert.match(ui.captureCharFrame(), /Sign in to view and manage Host/);
assert.equal(f.requests.length, 0);
ui.mockInput.pressEnter(); ui.mockInput.pressEnter(); await ui.renderOnce();
assert.match(ui.captureCharFrame(), /SIGN-IN REQUIRED/); assert.match(ui.captureCharFrame(), /Use Buzz Desktop identity/);
ui.mockInput.pressEnter(); await ui.renderOnce();
assert.equal(shell.state.focus, 'header'); assert.match(ui.captureCharFrame(), /SIGNED IN/);
assert.match(ui.captureCharFrame(), /Continue to Host/); assert.equal(shell.state.mode, 'owner');
ui.mockInput.pressEnter(); ui.mockInput.pressEnter(); await ui.renderOnce();
assert.match(ui.captureCharFrame(), /not implemented/); assert.equal(shell.state.activeSection, 0);
ui.mockInput.pressKey('\x1b'); await new Promise(r => setTimeout(r, 50));
for (let i=0;i<4;i++) ui.mockInput.pressArrow('right');
ui.mockInput.pressEnter(); ui.mockInput.pressEnter(); await ui.renderOnce();
assert.equal(shell.state.signedIn, false); assert.match(ui.captureCharFrame(), /Use Buzz Desktop identity/);
ui.mockInput.pressEnter(); await ui.renderOnce();
assert.equal(shell.state.signedIn, true); assert.equal(shell.state.mode, 'owner');
assert.doesNotMatch(ui.captureCharFrame(), /Continue to/);
assert.deepEqual(f.requests.map(r=>r.action), ['signin-desktop','signout','signin-desktop']);
} finally { shell.close(); }
});
for (const [width,height] of [[120,40],[60,20]]) test(`owner unavailable identity, masked input, cancellation and pointer at ${width}x${height}`, async () => {
const ui=await createTestRenderer({width,height,exitOnCtrlC:false}),f=fixture();
f.snapshot.desktop='unavailable'; f.snapshot.desktopReason='No usable Buzz Desktop identity. Open Buzz or use nsec.';
const shell=new OpenTuiShell(ui.renderer,f.inventory,undefined,undefined,f.client);
try {
for(let i=0;i<4;i++) ui.mockInput.pressArrow('right');
ui.mockInput.pressEnter(); ui.mockInput.pressEnter(); await ui.renderOnce();
assert.equal(f.requests.length,0); assert.match(ui.captureCharFrame(),/No usable Buzz/);
const lines=ui.captureCharFrame().split('\n'),y=lines.findIndex(l=>l.includes('Provide owner nsec'));
await ui.mockMouse.click(lines[y]!.indexOf('Provide owner nsec'),y);await ui.renderOnce();
assert.match(ui.captureCharFrame(),/PROVIDE OWNER NSEC/); assert.match(ui.captureCharFrame(),/Enter owner nsec/);
ui.mockInput.typeText('nsec1never-show');await ui.renderOnce();
assert.doesNotMatch(ui.captureCharFrame(),/nsec1never-show/); assert.match(ui.captureCharFrame(),/••/);
ui.mockInput.pressEscape();await new Promise(r=>setTimeout(r,50)); await ui.renderOnce();
assert.equal(f.snapshot.secretLength,0);assert.equal(f.requests.length,0);
ui.mockInput.pressEnter();await ui.renderOnce();ui.mockInput.typeText('fixture');ui.mockInput.pressTab();ui.mockInput.pressEnter();await ui.renderOnce();
assert.equal(f.requests.at(-1)?.action,'signin-nsec');assert.equal(shell.state.signedIn,true);
}finally{shell.close();}
});

View File

@@ -87,3 +87,7 @@ path = "src/bin/beehive_databricks.rs"
[target.'cfg(target_os = "macos")'.dependencies]
security-framework = "=3.7.0"
[[bin]]
name = "beehive-owner-key"
path = "src/bin/beehive_owner_key.rs"

View File

@@ -0,0 +1,95 @@
//! Exact, read-only Desktop identity access. Probe never opens Keychain UI.
use serde_json::{json, Value};
use std::io::{IsTerminal, Read};
fn identity(raw: &[u8]) -> Result<String, &'static str> {
if raw.len() > 1024 * 1024 {
return Err("malformed");
}
let value: Value = serde_json::from_slice(raw).map_err(|_| "malformed")?;
value
.get("identity")
.and_then(Value::as_str)
.map(str::to_owned)
.ok_or("missing")
}
#[cfg(target_os = "macos")]
fn read(probe: bool) -> Result<Value, &'static str> {
use security_framework::{
item::{ItemClass, ItemSearchOptions, SearchResult},
os::macos::keychain::SecKeychain,
};
let _no_ui = if probe {
Some(SecKeychain::disable_user_interaction().map_err(|_| "access")?)
} else {
None
};
let rows = ItemSearchOptions::new()
.class(ItemClass::generic_password())
.service("buzz-desktop")
.account("secrets")
.load_data(!probe)
.load_attributes(probe)
.skip_authenticated_items(probe)
.search()
.map_err(|error| {
if error.code() == -25300 {
"missing"
} else {
"access"
}
})?;
match rows.first() {
Some(SearchResult::Dict(_)) if probe => Ok(json!({"available": true})),
Some(SearchResult::Data(data)) if !probe => {
identity(data).map(|identity| json!({"identity": identity}))
}
_ => Err("missing"),
}
}
#[cfg(not(target_os = "macos"))]
fn read(_: bool) -> Result<Value, &'static str> {
Err("unsupported")
}
fn main() {
if std::io::stdin().is_terminal() || std::io::stdout().is_terminal() {
std::process::exit(2);
}
let mut input = String::new();
let result = if std::io::stdin()
.take(128)
.read_to_string(&mut input)
.is_err()
{
Err("input")
} else {
match input.trim() {
"probe" => read(true),
"read" => read(false),
_ => Err("input"),
}
};
let output = match result {
Ok(value) => value,
Err(reason) => json!({"reason": reason}),
};
println!("{output}");
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extracts_only_identity() {
assert_eq!(
identity(br#"{"identity":"nsec1fixture","agent":"never-return"}"#),
Ok("nsec1fixture".into())
);
assert_eq!(identity(br#"{"agent":"never-return"}"#), Err("missing"));
assert_eq!(identity(b"bad"), Err("malformed"));
assert_eq!(identity(&vec![b' '; 1024 * 1024 + 1]), Err("malformed"));
}
}