feat(beehive): configure runtime environment and type manager completion

Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
Logan Johnson
2026-09-16 08:59:46 -04:00
parent a83a47b19a
commit 63a7be1c67
19 changed files with 284 additions and 37 deletions

View File

@@ -157,3 +157,107 @@ cd beehive
"$BUN" test ./test/opentui-screen.bun.ts
"$NODE" --test test/*.test.ts
```
## Continuation b3d1e450 — environment and typed completion slice
This continuation remains **incomplete**, not a functional UX handback. No push,
export, installation or native change. The earlier missing-work list still applies
except that runtime environment configuration is now implemented. The earlier
navigation and Restart evidence remains valid for its stated scope.
Implemented in this slice:
- Node `ManagerController.request` returns `ManagerResult`: completed, submitted
(with operation ID), failed, cancelled or ignored. `manager-entry.ts` carries
that exact completion across IPC; `manager-view.ts` cannot treat a failed or
cancelled profile lookup as success. Old preview data is cleared before nsec
validation. Runtime discovery failure and model-query cancellation exit the
form. Submitted is **not** a host receipt or successful execution.
- Runtime setup includes a JSON name/value environment field before the name and
confirmation. Validation is shared between form, controller, settings, local
setup and launch. It permits at most 32 entries, 2048 UTF-8 bytes/value and 8192
serialized bytes; blocks controls, identity/provider/harness configuration,
loader hooks and secret-bearing names. These are **nonsecret user-supplied
values**, not an OS credential editor; naming rules cannot detect a secret
disguised as an ordinary value. Provider/model/effort remain separate fields.
- The append-only settings snapshot includes environment; binding fingerprints
therefore include it. `settings-runtime.ts` projects it only into the selected
local binding; `host.ts` passes it into `prepareAgent`. `acp.ts` copies/freezes
it and overlays authoritative managed environment. Save never mutates a prior
definition or active run. Selected runtime detail lists variable names only.
Source-grounded limits / next edits (do not repeat broad discovery):
1. **Private Move is not an available existing manager operation.** There are
three separate refusals: `intents.ts` submit's `privateHosts` allowlist,
`catalog-transport.ts` send allowlist, and `host-transport.ts` authenticated
receive allowlist. Host Move prepare/grant messages also currently have no
private host-to-host authenticated routing. Legacy `move.test.ts` is necessary
but insufficient. Implement authenticated source/destination grant transport
and ownership verification before enabling the manager action. Do not merely
remove these allowlists or chain Stop/Start. Native sources were not needed
or changed in this slice.
2. **Directory still missing.** Current Desktop
`commands/agent_discovery/relay_directory.rs:list_relay_agents_for_selection`
first obtains relay-self identity, paginates kind39002 authored by that relay
and scoped to viewer `#p`, and owner kind30177 coordinates; then exact-agent
kind10100/kind0, verified profile owners and exact-owner `#d=agent` policy.
It retains membership-visible or viewer-owned agents. This is not kind0
enumeration and is not signed Beehive assignment authority. Add a bounded,
cancellable Node adapter and explicit synthetic query transport; wire rows
separately from `ManagerController.inventory` (still private reports).
3. **Registration-first continuation still missing.** `ManagerResult` is now the
executable completion seam, not the requested governing registration/lifecycle
state machine. `registerAgent` remains identity-only. Next introduce one
captured operation state with exact agent/target/revisions and saved runtime;
extend the credential operation atomically with that runtime association;
continue only after verified registration and fresh assignment authority.
Retained key read-back and public-only assignment journals remain unchanged.
Do not manufacture genesis from nsec possession. Cancellation must discard
the captured continuation without claiming OS rollback.
4. **Desktop provider/discovery parity still missing.** Bounded current consumer
inspection: `AgentConfigFields.tsx:advancedEditorBlock` passes `config.env_vars`
into `EnvVarsEditor`, excluding API key and structured keys; the latter keeps
required/file-satisfied/hidden rows separate. `agentConfigCore.ts` owns runtime
descriptors and dependent resets. Beehive's new nonsecret local JSON subset
is intentionally more restrictive, not full Desktop parity; no historical
harness-contract pin was updated. Current provider catalog/auth/model consumer
alignment and remaining concise-copy cleanup still need implementation.
ACP failure investigation: saved prior `full-suite.log:266–272` identifies the
external ACP boundary test and `AgentSession`'s per-RPC timeout, but contains no
fixture mode or method. The a83a47b diff changed only manager/navigation/rendering,
their tests/docs and walkthrough; it did not change ACP/host launch paths. This
excludes a direct ACP edit in that commit, **not** contention or an unrelated bug.
The cause remains unestablished. This slice adds a test-only wrapper identifying
fixture mode while preserving the original error as `cause`, timeout, assertions
and production behavior. Focused ACP passed; that is not a diagnosis or waiver.
Evidence: `/Users/loganj/.buzz/artifacts/beehive-ux-b3d1e450/`
- `focused.log`: 34/34 controller, runtime integration, Pi, environment and ACP.
- `host-environment.log`: 11/11. Actual host Save/Start/Stop through synthetic
transport and owned Buzz/OpenAI, Buzz/Databricks and Codex-shaped processes
assert environment received, old active run unchanged, OS provider seams
synthetic, and exact wire/inventory limits unchanged. Environment test also
observes an actual owned ACP child's environment and immutable launch copy.
- `typecheck-final.log`: strict passed. `renderer.log`: Bun 1.4.2 renderer 4/4.
- `pty-final.log`, `walkthrough.json`: real 100x30 Node/Bun UI. Registration and
environment cancellation preserve settings; environment Save persists exact
`REVIEW_MODE=synthetic`, selecting runtime shows corresponding details; provider
cancellation/save/models and four sections work; hidden keys absent; exit 0.
`11-environment-entered`, `12-runtime-confirmation`, `14-runtime-selected`
have both ANSI streams and reconstructed plain frames. `pty-walkthrough.py`
contains exact key interactions. First successful PTY evidence is retained
separately in sibling `beehive-ux-b3d1e450-first-pty/`.
- Package-wide suite was **not rerun**: requested functional scope is unfinished.
Prior 202 pass / 4 fail / 19 skip remains the last full-suite result, including
all four unclassified failures. No timeout/concurrency/assertion relaxation.
Native unchanged: reuse historical 700/0/1 evidence, not a new run.
Reviewer entry for this slice only: `sh beehive/tools/ux-walkthrough.sh --help`,
then run without arguments. Add runtime -> harness -> provider -> model ->
Environment `{ "REVIEW_MODE": "synthetic" }` -> name -> yes. Select that runtime.
The fixture still deliberately blocks external service, relay and OS operations;
it does not establish registration-first or Move. Final independent UX acceptance
should wait for those implementation tasks, not treat this as completion.

View File

@@ -1,3 +1,4 @@
import { runtimeEnvironment } from './runtime-environment.ts';
import { prepareCodexHome } from './codex-home.ts';
import { PI_ADAPTER, piEnvironment, piConfigEvidence } from './pi.ts';
import { fileURLToPath } from 'node:url';
@@ -12,7 +13,7 @@ import { createHash } from 'node:crypto';
import { spawnOwned, type OwnedProcess } from './owned.ts';
/** Host-prepared launch: executable/provider binding stays local; instructions are a validated public snapshot. Never accept remote env/argv. */
export type AgentLaunch = Readonly<{ resolvedProviderKey?: string; buzzProvider?: BuzzProvider; custom?: CustomAcp; executable: string; args: readonly string[]; workspace: string; home: string; configDirectory: string; databricksHost: string; harness?: 'goose' | 'claude' | 'codex' | 'pi'; piCli?: string; codex?: CodexSetup; claude?: ClaudeSetup; provider?: string; model: string; instructions?: string }>;
export type AgentLaunch = Readonly<{ environment?: Record<string, string>; resolvedProviderKey?: string; buzzProvider?: BuzzProvider; custom?: CustomAcp; executable: string; args: readonly string[]; workspace: string; home: string; configDirectory: string; databricksHost: string; harness?: 'goose' | 'claude' | 'codex' | 'pi'; piCli?: string; codex?: CodexSetup; claude?: ClaudeSetup; provider?: string; model: string; instructions?: string }>;
/** ACP catalogs can be fallback data; even a nonempty result is NOT auth evidence. */
export type Catalog = { state: 'reported' | 'empty' | 'filtered'; models: string[]; authentication: 'unverified' };
/** Same child/session acknowledgement plus completed text response, not provider attestation. */
@@ -28,7 +29,8 @@ const identifier = (v: unknown): string => {
};
/** Validate and snapshot local launch settings. No ambient provider credentials inherited. */
export function prepareAgent(input: AgentLaunch) {
const plan = Object.freeze({ ...input, args: Object.freeze([...input.args]) });
const environment = Object.freeze(runtimeEnvironment(input.environment ?? {}));
const plan = Object.freeze({ ...input, ...(input.environment ? { environment } : {}), args: Object.freeze([...input.args]) });
if (plan.harness === 'codex' && plan.codex?.managedHome) prepareCodexHome(plan.codex.managedHome, plan.home, plan.configDirectory);
for (const p of [plan.executable, plan.workspace, plan.home, plan.configDirectory]) {
if (!isAbsolute(p) || realpathSync(p) !== p) throw Error('Harness setup requires canonical absolute paths');
@@ -41,31 +43,31 @@ export function prepareAgent(input: AgentLaunch) {
}
if (plan.harness === 'pi') {
if (plan.args.length || !plan.piCli || !plan.buzzProvider) throw Error('Invalid Pi launch');
return Object.freeze({ plan, executableHash: hash(readFileSync(plan.executable)), env: Object.freeze(piEnvironment(plan.piCli, plan.buzzProvider, plan.home, plan.model, plan.resolvedProviderKey)), cliExecutableHash: hash(readFileSync(plan.piCli)) });
return Object.freeze({ plan, executableHash: hash(readFileSync(plan.executable)), env: Object.freeze({ ...environment, ...piEnvironment(plan.piCli, plan.buzzProvider, plan.home, plan.model, plan.resolvedProviderKey) }), cliExecutableHash: hash(readFileSync(plan.piCli)) });
}
if (plan.harness === 'codex') {
identifier(plan.model);
if (plan.args.length || !plan.codex || !plan.codex.models.includes(plan.model) || plan.home === plan.configDirectory) throw Error('Codex requires zero-argument adapter, approved model and distinct HOME/CODEX_HOME');
return Object.freeze({ plan, executableHash: hash(readFileSync(plan.executable)), env: Object.freeze(codexEnvironment(plan.codex, plan.home, plan.configDirectory, plan.model, plan.resolvedProviderKey)), cliExecutableHash: hash(readFileSync(plan.codex.cli)) });
return Object.freeze({ plan, executableHash: hash(readFileSync(plan.executable)), env: Object.freeze({ ...environment, ...codexEnvironment(plan.codex, plan.home, plan.configDirectory, plan.model, plan.resolvedProviderKey) }), cliExecutableHash: hash(readFileSync(plan.codex.cli)) });
}
if (plan.harness === 'claude') {
identifier(plan.model);
if (plan.args.length || !plan.claude) throw Error('Claude requires a zero-argument ACP adapter and local CLI/key binding');
return Object.freeze({ plan, executableHash: hash(readFileSync(plan.executable)), env: Object.freeze(claudeEnvironment(plan.claude, plan.home, plan.model)), cliExecutableHash: hash(readFileSync(plan.claude.cli)) });
return Object.freeze({ plan, executableHash: hash(readFileSync(plan.executable)), env: Object.freeze({ ...environment, ...claudeEnvironment(plan.claude, plan.home, plan.model) }), cliExecutableHash: hash(readFileSync(plan.claude.cli)) });
}
if (plan.harness === 'goose') {
identifier(plan.provider);
identifier(plan.model);
return Object.freeze({ plan, executableHash: hash(readFileSync(plan.executable)), env: Object.freeze<Record<string, string>>({ ...plan.custom?.env, PATH: '/usr/bin:/bin', HOME: plan.home, GOOSE_PROVIDER: plan.provider!, GOOSE_MODEL: plan.model, GOOSE_MODE: 'auto' }) });
return Object.freeze({ plan, executableHash: hash(readFileSync(plan.executable)), env: Object.freeze<Record<string, string>>({ ...environment, ...plan.custom?.env, PATH: '/usr/bin:/bin', HOME: plan.home, GOOSE_PROVIDER: plan.provider!, GOOSE_MODEL: plan.model, GOOSE_MODE: 'auto' }) });
}
if (plan.buzzProvider) {
if (plan.args.length || plan.harness || plan.databricksHost) throw Error('Mixed Buzz Agent provider launch contract');
return Object.freeze({ plan, executableHash: hash(readFileSync(plan.executable)), env: Object.freeze<Record<string, string>>({ ...buzzProviderEnvironment(plan.buzzProvider, plan.home, plan.configDirectory, plan.model, plan.resolvedProviderKey), ...(plan.instructions === undefined ? {} : { BUZZ_AGENT_SYSTEM_PROMPT: plan.instructions }) }) });
return Object.freeze({ plan, executableHash: hash(readFileSync(plan.executable)), env: Object.freeze<Record<string, string>>({ ...environment, ...buzzProviderEnvironment(plan.buzzProvider, plan.home, plan.configDirectory, plan.model, plan.resolvedProviderKey), ...(plan.instructions === undefined ? {} : { BUZZ_AGENT_SYSTEM_PROMPT: plan.instructions }) }) });
}
const url = new URL(plan.databricksHost);
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || url.pathname !== '/') throw Error('Databricks workspace must be an HTTPS origin');
identifier(plan.model);
return Object.freeze({ plan, executableHash: hash(readFileSync(plan.executable)), env: Object.freeze<Record<string, string>>({ PATH: '/usr/bin:/bin', HOME: plan.home, BUZZ_AGENT_CONFIG_DIR: plan.configDirectory, BUZZ_AGENT_PROVIDER: 'databricks_v2', DATABRICKS_HOST: url.origin, BUZZ_AGENT_MODEL: plan.model, ...(plan.instructions === undefined ? {} : { BUZZ_AGENT_SYSTEM_PROMPT: plan.instructions }) }) });
return Object.freeze({ plan, executableHash: hash(readFileSync(plan.executable)), env: Object.freeze<Record<string, string>>({ ...environment, PATH: '/usr/bin:/bin', HOME: plan.home, BUZZ_AGENT_CONFIG_DIR: plan.configDirectory, BUZZ_AGENT_PROVIDER: 'databricks_v2', DATABRICKS_HOST: url.origin, BUZZ_AGENT_MODEL: plan.model, ...(plan.instructions === undefined ? {} : { BUZZ_AGENT_SYSTEM_PROMPT: plan.instructions }) }) });
}
/** Spawn-fixed Goose model evidence from native ACP configOptions, not set_model.

View File

@@ -1,3 +1,4 @@
import { runtimeEnvironment } from './runtime-environment.ts';
import { validatePi } from './pi.ts';
import { readSettings } from './settings.ts';
import { runtimeBindings } from './settings-runtime.ts';
@@ -26,7 +27,7 @@ import { spawnOwned, type OwnedProcess } from './owned.ts';
import { AgentSession, prepareAgent, type AgentLaunch, type Catalog, type Evidence } from './acp.ts';
import { prepareConversation, conversationSummary, type ConversationSetup } from './conversation.ts';
export type Setup = { host: string; ownerSecret?: string; ownerPublic?: string; agentSecret?: string; runner: string; args: string[]; workspace: string; allowedWorkspaces?: string[]; custom?: CustomAcp; buzzProvider?: BuzzProvider; mode: 'buzz-agent-api-key' | 'diagnostic-acp' | 'fixture' | 'buzz-agent-databricks-v2' | 'goose' | 'claude' | 'codex' | 'pi'; piCli?: string; codex?: CodexSetup; claude?: ClaudeSetup; gooseProvider?: string; gooseModels?: string[]; databricksHost?: string; serviceHome?: string; configDirectory?: string; conversation?: ConversationSetup };
export type Setup = { environment?: Record<string, string>; host: string; ownerSecret?: string; ownerPublic?: string; agentSecret?: string; runner: string; args: string[]; workspace: string; allowedWorkspaces?: string[]; custom?: CustomAcp; buzzProvider?: BuzzProvider; mode: 'buzz-agent-api-key' | 'diagnostic-acp' | 'fixture' | 'buzz-agent-databricks-v2' | 'goose' | 'claude' | 'codex' | 'pi'; piCli?: string; codex?: CodexSetup; claude?: ClaudeSetup; gooseProvider?: string; gooseModels?: string[]; databricksHost?: string; serviceHome?: string; configDirectory?: string; conversation?: ConversationSetup };
/** Definition-only fingerprint: common host authority and agent key are excluded. */
export function bindingFingerprint(setup: Setup): string {
const { host: _host, ownerSecret: _owner, ownerPublic: _ownerPublic, agentSecret: _agent, ...harness } = setup;
@@ -49,6 +50,7 @@ export function setupOwner(setup: Pick<Setup, 'ownerPublic' | 'ownerSecret'>): s
export function setupModels(s: Setup): string[] { return (s.mode === 'buzz-agent-api-key' || s.mode === 'pi') ? [...s.buzzProvider!.models] : s.mode === 'diagnostic-acp' ? [] : s.mode === 'codex' ? [...s.codex!.models] : s.mode === 'claude' ? [...s.claude!.models] : s.mode === 'goose' ? [...s.gooseModels!] : [s.mode === 'fixture' ? 'fixture-model' : 'databricks-claude-haiku-4-5']; }
export function validateSetup(value: unknown): Setup {
const s = object(value);
if (s.environment !== undefined) runtimeEnvironment(s.environment);
for (const key of ['host','runner','workspace']) text(s[key]);
if (s.allowedWorkspaces !== undefined && (!Array.isArray(s.allowedWorkspaces) || s.allowedWorkspaces.length > 32 || s.allowedWorkspaces.some(w => typeof w !== 'string' || !isAbsolute(w)))) throw Error('Invalid allowed workspaces');
setupOwner(s as Setup);
@@ -254,7 +256,7 @@ function slot(setup: Setup, path: string, agent: string, currentSetup: (id: stri
resolvedProviderKey = result.secret; result.secret = undefined;
} finally { if (credentialRead === controller) credentialRead = undefined; }
}
const launch: AgentLaunch | undefined = setup.mode === 'fixture' ? undefined : { ...(resolvedProviderKey ? { resolvedProviderKey } : {}), executable: setup.runner, args: setup.args, workspace: selected.workspace, home: text(setup.serviceHome), configDirectory: text(setup.configDirectory), ...(setup.buzzProvider ? { buzzProvider: setup.buzzProvider } : {}), databricksHost: setup.mode === 'buzz-agent-api-key' || setup.mode === 'pi' || setup.mode === 'goose' || setup.mode === 'claude' || setup.mode === 'codex' ? '' : text(setup.databricksHost), ...(setup.mode === 'pi' ? { harness: 'pi' as const, piCli: setup.piCli } : {}), ...(setup.mode === 'codex' ? { harness: 'codex' as const, codex: setup.codex } : {}), ...(setup.mode === 'claude' ? { harness: 'claude' as const, claude: setup.claude } : {}), ...(setup.mode === 'goose' ? { harness: 'goose' as const, provider: setup.gooseProvider, ...(setup.custom ? { custom: setup.custom } : {}) } : {}), model: selected.model, ...(selected.behavior ? { instructions: selected.behavior.instructions } : {}) };
const launch: AgentLaunch | undefined = setup.mode === 'fixture' ? undefined : { ...(setup.environment ? { environment: runtimeEnvironment(setup.environment) } : {}), ...(resolvedProviderKey ? { resolvedProviderKey } : {}), executable: setup.runner, args: setup.args, workspace: selected.workspace, home: text(setup.serviceHome), configDirectory: text(setup.configDirectory), ...(setup.buzzProvider ? { buzzProvider: setup.buzzProvider } : {}), databricksHost: setup.mode === 'buzz-agent-api-key' || setup.mode === 'pi' || setup.mode === 'goose' || setup.mode === 'claude' || setup.mode === 'codex' ? '' : text(setup.databricksHost), ...(setup.mode === 'pi' ? { harness: 'pi' as const, piCli: setup.piCli } : {}), ...(setup.mode === 'codex' ? { harness: 'codex' as const, codex: setup.codex } : {}), ...(setup.mode === 'claude' ? { harness: 'claude' as const, claude: setup.claude } : {}), ...(setup.mode === 'goose' ? { harness: 'goose' as const, provider: setup.gooseProvider, ...(setup.custom ? { custom: setup.custom } : {}) } : {}), model: selected.model, ...(selected.behavior ? { instructions: selected.behavior.instructions } : {}) };
const prepared = launch ? prepareAgent(launch) : undefined;
const conversation = setup.conversation && launch ? prepareConversation(setup.conversation, launch, executionSecret, state.binding.owner) : undefined;
// Local hashes only; no setup or secret values leave the host. Include existing

View File

@@ -1,3 +1,4 @@
import { runtimeEnvironment } from './runtime-environment.ts';
import { addDatabricks, databricksNative } from './databricks.ts';
import { existsSync } from 'node:fs';
import { readSettings, saveSettings, settingsId, type Settings, type RegisteredAgent } from './settings.ts';
@@ -18,6 +19,12 @@ import { fetchRelayName } from './relay-name.ts';
import { message, type Message } from './protocol.ts';
import { profileDrafts, editProfileDraft } from './profile-drafts.ts';
/** Controller completion is not a host receipt: submitted work remains pending. */
export type ManagerResult =
| { state: 'completed' }
| { state: 'submitted'; operationId: string }
| { state: 'failed' | 'cancelled' | 'ignored'; reason: string };
export type ManagerRequest = { id: number; action: string; values?: Record<string, string>; target?: string; revision?: number };
export type ManagerItem = { id: string; label: string; detail: string; evidence?: string; disabled?: Record<string, string> };
export type ManagerSnapshot = { local: ManagerItem[]; agents: (ManagerItem & { revision: number; configurations: string[] })[]; routing?: { owner: string; relay: string }; owner?: string; status: string; settings?: Settings; service?: ServiceStatus; hostRelay?: string; relayName?: string; profilePreview?: RegisteredAgent; models?: string[]; runtimeExecutable?: string; harnesses?: DetectedHarness[]; databricksHost?: string };
@@ -236,18 +243,19 @@ A Stop result is not a recent host report that confirms the agent is stopped.` }
}
cancel() { this.generation++; this.active?.abort(); }
close() { this.closed = true; this.cancel(); this.relayNamePending?.abort.abort(); this.client?.close(); this.client = undefined; this.owner = undefined; }
async request(request: ManagerRequest) {
if (this.closed || request.id <= this.lastId || this.active) return;
async request(request: ManagerRequest): Promise<ManagerResult> {
if (this.closed || request.id <= this.lastId || this.active) return { state: 'ignored', reason: 'Request is closed, duplicate, or busy.' };
this.lastId = request.id;
const abort = new AbortController(); this.active = abort;
const generation = ++this.generation;
const check = () => { abort.signal.throwIfAborted(); if (this.closed || generation !== this.generation) throw plain('Stopped waiting. Changes may already be saved or submitted. Inspect before you try again.'); };
const v = request.values ?? {};
let result: ManagerResult = { state: 'completed' };
try {
if (request.action === 'profile-preview') {
this.profilePreview = undefined;
const identity = readHostIdentityPublic(this.hostDirectory);
const key = publicKey(agentNsec(v.secret ?? ''));
this.profilePreview = undefined;
const profile = await fetchAgentProfile(identity.pairing.relay,key,abort.signal); check();
this.profilePreview = { publicKey: key, key: { service: 'beehive', role: 'agent', publicKey: key }, ...profile };
this.status = profile.profileState === 'found' ? 'Signed public profile found.' : profile.profileState === 'none' ? 'No profile found. You can register with the public key.' : 'Profile lookup unavailable. You can register without a profile.';
@@ -277,7 +285,7 @@ A Stop result is not a recent host report that confirms the agent is stopped.` }
const previous = readSettings(this.hostDirectory);
const provider = previous.providers.find(p => p.id === v.provider);
if (!selected?.executable || !provider || !selected.providers.includes(provider.type) || !['buzz-agent','codex','pi'].includes(selected.id)) throw plain('No supported executable/provider combination found.');
saveSettings(this.hostDirectory,{ ...previous, runtimes: [...previous.runtimes,{ id: settingsId(), name: v.name ?? '', harness: selected.id as 'buzz-agent' | 'codex' | 'pi', executable: selected.executable, ...(['codex','pi'].includes(selected.id) ? { cli: selected.cli } : {}), providerId: v.provider ?? '', model: v.model ?? '', ...(v.effort ? { effort: v.effort } : {}) }] },previous.revision);
saveSettings(this.hostDirectory,{ ...previous, runtimes: [...previous.runtimes,{ id: settingsId(), name: v.name ?? '', harness: selected.id as 'buzz-agent' | 'codex' | 'pi', executable: selected.executable, ...(['codex','pi'].includes(selected.id) ? { cli: selected.cli } : {}), providerId: v.provider ?? '', model: v.model ?? '', ...(v.environment ? { environment: runtimeEnvironment(JSON.parse(v.environment)) } : {}), ...(v.effort ? { effort: v.effort } : {}) }] },previous.revision);
this.status = 'Runtime saved for new runs. Running agents did not change. Host loading is reported separately.';
} else if (request.action === 'host-start') {
this.service = await startService(this.hostDirectory); check(); this.status = 'Host running. Registration and settings do not start agents.';
@@ -334,9 +342,11 @@ A Stop result is not a recent host report that confirms the agent is stopped.` }
if (this.client.status().some(o => o.request.host === current.host && o.request.agent === current.agent && !['completed','failed'].includes(o.state))) throw plain('An operation has no confirmed result. Inspect operations and check operation results before you act again.');
const operation = message(request.action === 'select-config' ? 'save' : request.action as 'start' | 'stop' | 'restart', current.host, current.agent, current.revision, request.action === 'select-config' ? { configurationAction: 'select', name: v.name } : {});
this.client.submit(operation);
result = { state: 'submitted', operationId: operation.id };
this.status = `Operation ${operation.id}: ${plainOperationType(operation)} saved and pending.\nHost: ${current.host}\nAgent: ${current.agent}\nRelay publication does not confirm host acceptance. Inspect operations. Unknown does not mean stopped.`;
} else if (request.action !== 'refresh') throw plain('Not available in this build');
} catch (error) { if (!this.closed) this.status = error instanceof PlainStatus ? error.message : abort.signal.aborted && error === abort.signal.reason ? 'Stopped waiting. Changes may already be saved or submitted. Inspect before you try again.' : error instanceof Error ? plainBackendMessage(error.message) : `Could not complete the operation: ${String(error)}`; }
} catch (error) { result = { state: abort.signal.aborted || generation !== this.generation ? 'cancelled' : 'failed', reason: 'Operation did not complete; inspect status before retrying.' }; if (!this.closed) this.status = error instanceof PlainStatus ? error.message : abort.signal.aborted && error === abort.signal.reason ? 'Stopped waiting. Changes may already be saved or submitted. Inspect before you try again.' : error instanceof Error ? plainBackendMessage(error.message) : `Could not complete the operation: ${String(error)}`; }
finally { delete v.secret; this.active = undefined; this.refresh(); }
return result;
}
}

View File

@@ -23,7 +23,7 @@ input.on('line', line => {
const value = JSON.parse(line);
if (value.quit) controller.close();
else if (value.cancel) controller.cancel();
else void controller.request(value).then(() => send({ complete: value.id }));
else void controller.request(value).then(result => send({ complete: value.id, result }));
} catch { controller.cancel(); }
});
const refresh = setInterval(() => controller.refresh(), 2000);

View File

@@ -10,7 +10,7 @@ export function sectionRows(snapshot: ManagerSnapshot, section: number): Manager
if (section === 1) return snapshot.agents;
if (section === 2) return [
...(snapshot.harnesses ?? []).map(h => ({ id: `harness:${h.id}`, label: `${h.label} · ${h.state}`, detail: `Harness: ${h.label}\nState: ${h.state}\nExecutable: ${h.executable ?? '—'}\nCLI: ${h.cli ?? '—'}` })),
...(settings?.runtimes ?? []).map(r => ({ id: `runtime:${r.id}`, label: r.name, detail: `Runtime: ${r.name}\nHarness: ${r.harness}\nProvider: ${settings?.providers.find(p => p.id === r.providerId)?.name ?? '—'}\nModel: ${r.model}\nEffort: ${r.effort ?? 'Inherit'}` })),
...(settings?.runtimes ?? []).map(r => ({ id: `runtime:${r.id}`, label: r.name, detail: `Runtime: ${r.name}\nHarness: ${r.harness}\nProvider: ${settings?.providers.find(p => p.id === r.providerId)?.name ?? '—'}\nModel: ${r.model}\nEffort: ${r.effort ?? 'Inherit'}\nEnvironment: ${Object.keys(r.environment ?? {}).join(', ') || 'None'}` })),
];
if (section === 3) return (settings?.providers ?? []).map(p => ({ id: p.id, label: p.name, detail: `Provider: ${p.name}\nType: ${p.type}\nEndpoint: ${p.endpoint}` }));
return [];

View File

@@ -4,7 +4,7 @@ import { createCliRenderer } from '@opentui/core';
import { createReadStream, createWriteStream } from 'node:fs';
import { createInterface } from 'node:readline';
import { OpenTuiScreen, type ManagerAction } from './opentui-screen.ts';
import type { ManagerRequest, ManagerSnapshot } from './manager-controller.ts';
import type { ManagerRequest, ManagerSnapshot, ManagerResult } from './manager-controller.ts';
const renderer = await createCliRenderer({ exitOnCtrlC: false });
const screen = new OpenTuiScreen(renderer);
@@ -12,11 +12,11 @@ const output = createWriteStream('', { fd: 4 });
let snapshot: ManagerSnapshot = { local: [], agents: [], status: 'Connecting to Beehive…' };
const navigation = new ManagerNavigation();
let scope = 0, selected = '', sequence = 0, lastStatus = '';
let pending: { id: number; resolve: () => void } | undefined;
let pending: { id: number; resolve: (result: ManagerResult) => void } | undefined;
function request(action: string, values?: Record<string,string>, target?: string, revision?: number) {
if (pending) return Promise.resolve();
if (pending) return Promise.resolve<ManagerResult>({ state: 'ignored', reason: 'An operation is pending.' });
const id = ++sequence;
const completion = new Promise<void>(resolve => { pending = { id, resolve }; });
const completion = new Promise<ManagerResult>(resolve => { pending = { id, resolve }; });
const message: ManagerRequest = { id, action, values, target, revision };
screen.setPending(true); screen.notice(action === 'add-databricks' ? 'Signing in through your browser… Esc cancels the native helper. An OS credential write or browser window may remain; cancelled sign-in will not save a provider.' : 'Working… Esc stops waiting. Remote work and saved changes are not cancelled. Inspect before you try again.');
output.write(JSON.stringify(message) + '\n');
@@ -62,7 +62,8 @@ function render() {
if (!configured) { await routing('configure'); return; }
let secret = await screen.input('Register agent\nAgent nsec private key. Hidden. Saved only in Beehive’s OS credential store.', '', true);
if (secret === undefined) return;
await request('profile-preview', { secret });
const result = await request('profile-preview', { secret });
if (result.state !== 'completed') { secret = ''; return; }
const preview = snapshot.profilePreview;
if (!preview) { secret = ''; return; }
if (await screen.confirm(`Register agent\n${preview.profile?.name ?? 'Name unavailable'}\n${preview.publicKey}\n${preview.profileState === 'unavailable' ? 'Relay unavailable. Register without a profile?' : preview.profileState === 'none' ? 'No profile found.' : preview.profile?.about ?? ''}\nThis does not authorize or start an agent. Save?`)) await request('register-agent',{ secret });
@@ -139,7 +140,7 @@ input.on('line', line => {
try {
const value = JSON.parse(line);
if (value.snapshot) { snapshot = value.snapshot; render(); }
if (value.complete === pending?.id) { const old = pending; pending = undefined; screen.setPending(false); old?.resolve(); }
if (value.complete === pending?.id) { const old = pending; pending = undefined; screen.setPending(false); old?.resolve(value.result ?? { state: 'failed', reason: 'Missing controller result.' }); }
} catch { screen.notice('Beehive could not read the response. Quit and inspect saved operation records before you try again.'); }
});
input.on('close', () => screen.close());

View File

@@ -1,9 +1,10 @@
import { RGBA, BoxRenderable, InputRenderable, ScrollBoxRenderable, SelectRenderable, TextRenderable, TextareaRenderable, type CliRenderer, type KeyEvent } from '@opentui/core';
import type { ManagerResult } from './manager-controller.ts';
import { managerSections } from './manager-navigation.ts';
import { stripVTControlCharacters } from 'node:util';
export type ManagerRow = { id: string; label: string; detail: string; evidence?: string };
export type ManagerAction = { label: string; disabled?: string; run: () => void | Promise<void> };
export type ManagerAction = { label: string; disabled?: string; run: () => void | Promise<void | ManagerResult> };
const fg = RGBA.defaultForeground(), bg = RGBA.defaultBackground();
const clean = (value: string) => stripVTControlCharacters(value).replace(/[\x00-\x08\x0b-\x1f\x7f]/g, '');

View File

@@ -0,0 +1,13 @@
/** Local nonsecret runtime variables. Authority, credentials, loader hooks and
* structured harness fields are never configurable through this map. */
export function runtimeEnvironment(value: unknown): Record<string, string> {
if (!value || typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length > 32) throw Error('Environment requires at most 32 name/value entries');
const result: Record<string, string> = {};
for (const [key, entry] of Object.entries(value)) {
if (!/^[A-Z_][A-Z0-9_]{0,99}$/.test(key) || /^(BUZZ_|BEEHIVE_|GOOSE_|CODEX_|ANTHROPIC_|CLAUDE_|OPENAI_|OPENROUTER_|DATABRICKS_|PI_|NODE_|LD_|DYLD_|XDG_|NPM_|PNPM_|BUN_)/.test(key) || /(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)/.test(key) || ['HOME','PATH','SHELL','ENV','BASH_ENV','ZDOTDIR','USER','LOGNAME','TMPDIR','TMP','TEMP'].includes(key)) throw Error('Environment name is reserved or secret-bearing');
if (typeof entry !== 'string' || Buffer.byteLength(entry) > 2048 || /[\x00-\x1f\x7f-\x9f]/.test(entry)) throw Error('Invalid environment value');
result[key] = entry;
}
if (Buffer.byteLength(JSON.stringify(result)) > 8192) throw Error('Environment exceeds 8192 bytes');
return result;
}

View File

@@ -1,6 +1,7 @@
import { runtimeEnvironment } from './runtime-environment.ts';
import { piEfforts } from './pi.ts';
import { runtimeEfforts } from './runtime-effort.ts';
import type { ManagerSnapshot } from './manager-controller.ts';
import type { ManagerSnapshot, ManagerResult } from './manager-controller.ts';
/** Same short form used by the actual renderer and synthetic key/form fixtures. */
export async function runtimeForm(screen: {
@@ -8,8 +9,8 @@ export async function runtimeForm(screen: {
input(label: string, initial?: string): Promise<string | undefined>;
confirm(label: string): Promise<boolean>;
notice(text: string): void;
}, snapshot: () => ManagerSnapshot, request: (action: string, values?: Record<string,string>) => Promise<void>) {
await request('runtime-form');
}, snapshot: () => ManagerSnapshot, request: (action: string, values?: Record<string,string>) => Promise<ManagerResult>) {
if ((await request('runtime-form')).state !== 'completed') return;
const harnesses = snapshot().harnesses ?? [];
if (!harnesses.length) { screen.notice(snapshot().status); return; }
const labels = harnesses.map(h => `${h.label} · ${h.providers.length ? 'Available' : h.state === 'available' ? 'Provider integration unavailable' : h.state}`);
@@ -20,12 +21,17 @@ export async function runtimeForm(screen: {
if (!providers.length) { screen.notice('Add a supported provider first.'); return; }
const choice = await screen.choose('Provider', providers.map(p => `${p.name} · ${p.id}`)); if (!choice) return;
const provider = providers.find(p => choice === `${p.name} · ${p.id}`)!;
await request('models', { provider: provider.id });
const modelResult = await request('models', { provider: provider.id });
if (modelResult.state === 'cancelled' || modelResult.state === 'ignored') return;
const models = snapshot().models;
const choiceModel = await screen.choose(models ? 'Model' : 'Model listing unavailable · Custom model allowed', [...(models ?? []), 'Custom model']); if (!choiceModel) return;
const model = choiceModel === 'Custom model' ? await screen.input('Exact model ID. Custom does not verify provider access.') : choiceModel; if (!model) return;
const efforts = selected.id === 'buzz-agent' ? runtimeEfforts(provider.type, model) : selected.id === 'pi' ? piEfforts(provider.type, model) : [];
const effort = efforts.length ? await screen.choose('Effort · new runs only', ['Inherit', ...efforts]) : 'Inherit'; if (!effort) return;
const entry = await screen.input('Environment · JSON name/value map · no secrets', '{}'); if (entry === undefined) return;
let environment: string;
try { environment = JSON.stringify(runtimeEnvironment(JSON.parse(entry))); }
catch { screen.notice('Invalid environment. Use nonsecret name/value pairs; identity, provider and loader variables are reserved. Nothing saved.'); return; }
const name = await screen.input('Runtime name', model); if (!name) return;
if (await screen.confirm(`Save ${name}?\n${selected.label} · ${provider.name} · ${model}\n${efforts.length ? `Effort: ${effort}` : 'No supported effort control for this model.'}\nSaved for new runs. Running agents do not change.`)) await request('add-runtime', { name, model, provider: provider.id, harness: selected.id, ...(effort !== 'Inherit' ? { effort } : {}) });
if (await screen.confirm(`Save ${name}?\n${selected.label} · ${provider.name} · ${model}\n${efforts.length ? `Effort: ${effort}` : 'No supported effort control for this model.'}\nEnvironment: ${Object.keys(JSON.parse(environment)).join(', ') || 'None'}\nSaved for new runs. Running agents do not change.`)) await request('add-runtime', { name, model, environment, provider: provider.id, harness: selected.id, ...(effort !== 'Inherit' ? { effort } : {}) });
}

View File

@@ -1,3 +1,4 @@
import { runtimeEnvironment } from './runtime-environment.ts';
import { codexHomeIdentity, codexHomePaths } from './codex-home.ts';
import { publicKey } from './protocol.ts';
import { readSettings, validateSettings, type Settings } from './settings.ts';
@@ -17,6 +18,7 @@ export function runtimeBindings(base: Setup, settings: Settings, agent = base.ag
const paths = managedHome ? codexHomePaths(managedHome) : undefined;
return [`runtime:${runtime.id}`, validateSetup({
host: base.host, ownerPublic: base.ownerPublic, ...(base.ownerSecret ? { ownerSecret: base.ownerSecret } : {}), ...(base.agentSecret ? { agentSecret: base.agentSecret } : {}),
...(runtime.environment ? { environment: runtimeEnvironment(runtime.environment) } : {}),
runner: runtime.executable, args: [], workspace: base.workspace, allowedWorkspaces: base.allowedWorkspaces,
serviceHome: paths?.home ?? base.serviceHome, configDirectory: paths?.config ?? base.configDirectory,
...(base.conversation ? { conversation: base.conversation } : {}), ...(runtime.harness === 'codex' ? { mode: 'codex', codex: { ...(managedHome ? { managedHome } : {}), cli: runtime.cli!, credential: provider.key, models: [runtime.model] } } : { mode: runtime.harness === 'pi' ? 'pi' : 'buzz-agent-api-key', ...(runtime.harness === 'pi' ? { piCli: runtime.cli } : {}),

View File

@@ -1,3 +1,4 @@
import { runtimeEnvironment } from './runtime-environment.ts';
import { piEfforts } from './pi.ts';
import { validateRuntimeEffort } from './runtime-effort.ts';
import { databricksHost } from './databricks.ts';
@@ -12,7 +13,7 @@ export type RegisteredAgent = { publicKey: string; key: CredentialReference; pro
/** Each provider version owns an exact immutable OS credential entry. */
export type ProviderReference = { service: 'beehive'; account: string };
export type SavedProvider = { id: string; name: string; type: 'openai' | 'databricks_v2'; endpoint: string; key: ProviderReference };
export type SavedRuntime = { id: string; name: string; harness: 'buzz-agent' | 'codex' | 'pi'; cli?: string; executable: string; providerId: string; model: string; effort?: string };
export type SavedRuntime = { environment?: Record<string, string>; id: string; name: string; harness: 'buzz-agent' | 'codex' | 'pi'; cli?: string; executable: string; providerId: string; model: string; effort?: string };
export type Settings = { version: 1; revision: number; agents: RegisteredAgent[]; providers: SavedProvider[]; runtimes: SavedRuntime[] };
const path = (directory: string) => join(directory, 'settings.json');
const label = (s: unknown) => typeof s === 'string' && s.length > 0 && s.length <= 128 && !/[\x00-\x1f\x7f]/.test(s);
@@ -29,6 +30,7 @@ export function validateSettings(value: Settings): Settings {
for (const p of value.providers) if (!label(p.name) || !label(p.id) || !['openai','databricks_v2'].includes(p.type) || (p.type === 'openai' ? p.endpoint !== 'https://api.openai.com/v1' : databricksHost(p.endpoint) !== p.endpoint) || p.key?.service !== 'beehive' || !/^provider:[0-9a-f-]{36}$/.test(p.key.account)) throw Error('Invalid saved provider');
for (const r of value.runtimes) if (!label(r.id) || !label(r.name) || !['buzz-agent','codex','pi'].includes(r.harness) || !isAbsolute(r.executable) || !/^[a-zA-Z0-9_.:/-]{1,200}$/.test(r.model) || !value.providers.some(p => p.id === r.providerId)) throw Error('Invalid saved runtime');
for (const r of value.runtimes) {
if (r.environment !== undefined) runtimeEnvironment(r.environment);
if (r.harness === 'codex' ? !r.cli || !isAbsolute(r.cli) || r.effort !== undefined || value.providers.find(p => p.id === r.providerId)?.type !== 'openai' : r.harness === 'pi' ? !r.cli || !isAbsolute(r.cli) || r.effort !== undefined && !piEfforts(value.providers.find(p => p.id === r.providerId)!.type, r.model).includes(r.effort) : r.cli !== undefined) throw Error('Unsupported runtime provider/CLI/effort combination');
}
for (const r of value.runtimes) validateRuntimeEffort(value.providers.find(p => p.id === r.providerId)!.type, r.model, r.effort);
@@ -37,7 +39,7 @@ export function validateSettings(value: Settings): Settings {
fields(value, ['version','revision','agents','providers','runtimes']);
for (const a of value.agents) { fields(a, ['publicKey','key','profile','profileState']); if (a.profile) fields(a.profile, ['relay','name','picture','about']); }
for (const p of value.providers) { fields(p, ['id','name','type','endpoint','key']); fields(p.key, ['service','account']); }
for (const r of value.runtimes) fields(r, ['id','name','harness','executable','providerId','model','effort','cli']);
for (const r of value.runtimes) fields(r, ['id','name','harness','executable','providerId','model','effort','cli','environment']);
return structuredClone(value);
}
/** Absent settings means an empty catalog, never reconstructed credentials. */

View File

@@ -44,7 +44,7 @@ test('external ACP boundary: snapshot, catalog provenance, exact-model same-sess
assert.match(evidence.responseHash, /^[0-9a-f]{64}$/);
assert.equal(evidence.executableHash, s.prepared.executableHash);
}
} finally { await cleanup(s); }
} catch (error) { throw new Error(`ACP fixture mode ${mode} failed`, { cause: error }); } finally { await cleanup(s); }
}
} finally { rmSync(dir, { recursive: true, force: true }); }
});

View File

@@ -95,7 +95,7 @@ for (const providerType of ['openai','databricks_v2','codex-openai'] as const) t
const prior = readSettings(root), id = settingsId();
const model = codex ? 'custom-codex-model' : providerType === 'openai' ? 'gpt-5' : 'databricks-gpt-5-4';
const executable = join(root,'fixture-buzz-agent');
writeFileSync(executable,`#!/bin/sh\n[ \"$BUZZ_AGENT_THINKING_EFFORT\" = high ] || exit 9\nexec '${process.execPath}' '${fileURLToPath(new URL('./acp-fixture.ts',import.meta.url))}' ${providerType === 'openai' ? 'openai' : 'databricks-os'}\n`,{mode:0o700});
writeFileSync(executable,`#!/bin/sh\n[ \"$REVIEW_MODE\" = synthetic ] || exit 10\n[ \"$BUZZ_AGENT_THINKING_EFFORT\" = high ] || exit 9\nexec '${process.execPath}' '${fileURLToPath(new URL('./acp-fixture.ts',import.meta.url))}' ${providerType === 'openai' ? 'openai' : 'databricks-os'}\n`,{mode:0o700});
let requests = 0;
if (codex) {
const server = createServer(async (req,res) => {
@@ -110,12 +110,13 @@ for (const providerType of ['openai','databricks_v2','codex-openai'] as const) t
const endpoint = `http://127.0.0.1:${(server.address() as any).port}/v1/responses`;
writeFileSync(join(root,'mode'),'ok');
writeFileSync(executable,`#!${process.execPath}
if (process.env.REVIEW_MODE !== 'synthetic') throw Error('Missing selected runtime environment');
const response = await fetch(${JSON.stringify(endpoint)}, {method:'POST',headers:{authorization:'Bearer '+process.env.OPENAI_API_KEY},body:JSON.stringify({model:JSON.parse(process.env.CODEX_CONFIG).model})});
if (!response.ok) throw Error('Synthetic provider rejected');
await import(${JSON.stringify(new URL('./conversation-harness-fixture.ts',import.meta.url).href)});
`,{mode:0o700});
}
saveSettings(root,{ ...prior, runtimes: [{ id, name: 'Future', harness: codex ? 'codex' : 'buzz-agent', executable, providerId: prior.providers[0]!.id, model, ...(codex ? {cli:realpathSync(process.execPath)} : {effort:'high'}) }] },prior.revision);
saveSettings(root,{ ...prior, runtimes: [{ id, name: 'Future', environment: {REVIEW_MODE:'synthetic'}, harness: codex ? 'codex' : 'buzz-agent', executable, providerId: prior.providers[0]!.id, model, ...(codex ? {cli:realpathSync(process.execPath)} : {effort:'high'}) }] },prior.revision);
const projected = runtimeBindings(setup,readSettings(root))[`runtime:${id}`]!;
if (codex) { assert.notEqual(projected.serviceHome,root); assert.equal(existsSync(projected.serviceHome!),false); }
await wait(() => running.settingsRevision === 2);

View File

@@ -149,3 +149,26 @@ test('manager presentation preserves raw evidence, unknown states and literal co
assert.match(f.snapshot.agents.find(r => r.id === 'host:host-b')!.detail, /Host configuration: Host name: Host B/);
} finally { f.cleanup(); }
});
test('typed completion separates submitted intent from failures, cancellation and ignored requests', async () => {
const f = fixture();
try {
assert.equal((await f.controller.request({id:1,action:'start'})).state,'failed');
assert.equal((await f.controller.request({id:1,action:'start'})).state,'ignored');
assert.equal((await f.controller.request({id:2,action:'signin',values:{owner,relay:'wss://example.invalid',secret}})).state,'completed');
f.receive(message('inventory','host-a','agent-a',3,{observedAt:Date.now(),phase:'stopped',actualRun:null,assignedHost:'host-a'}));
const result = await f.controller.request({id:3,action:'start',target:JSON.stringify(['host-a','agent-a']),revision:3});
assert.equal(result.state,'submitted');
if (result.state === 'submitted') assert.equal(result.operationId,f.submitted[0]!.id);
assert.equal((await f.controller.request({id:4,action:'start',target:JSON.stringify(['host-a','agent-a']),revision:2})).state,'failed');
} finally {f.cleanup();}
const home = mkdtempSync(join(tmpdir(),'beehive-manager-'));
let finish!: (v:any)=>void;
const c = new ManagerController(home,()=>{},()=>new Promise(resolve=>{finish=resolve;}),undefined,async()=>undefined);
try {
const pending = c.request({id:1,action:'signin',values:{owner,relay:'wss://example.invalid'}});
c.cancel(); finish({ok:true,secret});
assert.equal((await pending).state,'cancelled');
assert.equal(c.snapshot().owner,undefined);
} finally {c.close();rmSync(home,{recursive:true,force:true});}
});

View File

@@ -42,7 +42,7 @@ test('actual runtime form offers Pi both providers, retains custom names, submit
const model=type==='openai'?'gpt-5':'databricks-gpt-5-4';
const snapshot:ManagerSnapshot={local:[],agents:[],status:'fixture',harnesses:[{id:'pi',label:'Pi',executable:'/fake',cli:'/fake-pi',state:'available',providers:['openai','databricks_v2'],reason:'synthetic'}],models:[model],settings:{version:1,revision:1,agents:[],runtimes:[],providers:[{id:'p',name:'Saved',type,endpoint:type==='openai'?'https://api.openai.com/v1':'https://pi-workspace.example',key}]}};
const choices=['Pi · Available','Saved · p',model,'high'];const calls:any[]=[];
await runtimeForm({async choose(_label,options){const value=choices.shift();assert.ok(options.includes(value!));return value;},async input(){return 'My Pi';},async confirm(){return true;},notice(){throw Error('unexpected notice');}},()=>snapshot,async(action,values)=>{calls.push({action,values});});
assert.deepEqual(calls.at(-1),{action:'add-runtime',values:{name:'My Pi',model,provider:'p',harness:'pi',effort:'high'}});
await runtimeForm({async choose(_label,options){const value=choices.shift();assert.ok(options.includes(value!));return value;},async input(label){return label.startsWith('Environment') ? '{}' : 'My Pi';},async confirm(){return true;},notice(){throw Error('unexpected notice');}},()=>snapshot,async(action,values)=>{calls.push({action,values});return {state:"completed"};});
assert.deepEqual(calls.at(-1),{action:'add-runtime',values:{name:'My Pi',model,environment:'{}',provider:'p',harness:'pi',effort:'high'}});
}
});

View File

@@ -0,0 +1,59 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, realpathSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { runtimeEnvironment } from '../src/runtime-environment.ts';
import { AgentSession, prepareAgent } from '../src/acp.ts';
import { runtimeBindings } from '../src/settings-runtime.ts';
import { saveSettings, readSettings, type Settings } from '../src/settings.ts';
import { bindingFingerprint, type Setup } from '../src/host.ts';
import { publicKey } from '../src/protocol.ts';
const key = { service: 'beehive' as const, account: 'provider:00000000-0000-0000-0000-000000000000' };
test('runtime environment validates names, bytes and bounds; catalog Save retains immutable prior definitions', () => {
const root = realpathSync(mkdtempSync(join(tmpdir(), 'beehive-pairing-cli-env-')));
try {
for (const name of ['HOME','PATH','BUZZ_PRIVATE_KEY','BEEHIVE_PI_RUNTIME','NODE_OPTIONS','LD_PRELOAD','OPENAI_API_KEY','PI_ACP_PI_COMMAND','MY_SECRET','DATABRICKS_HOST','BUZZ_AGENT_MODEL','BUZZ_AGENT_THINKING_EFFORT']) assert.throws(() => runtimeEnvironment({[name]:'x'}));
for (const input of [null, [], {REVIEW: 1}, {REVIEW: 'x\n'}, {REVIEW: '界'.repeat(700)}, Object.fromEntries(Array.from({length:33},(_,i)=>[`VAR_${i}`,'x'])), Object.fromEntries(Array.from({length:5},(_,i)=>[`VAR_${i}`,'x'.repeat(2048)]))]) assert.throws(() => runtimeEnvironment(input));
const settings: Settings = {version:1, revision:0, agents:[], providers:[{id:'p',name:'Synthetic',type:'openai',endpoint:'https://api.openai.com/v1',key}], runtimes:[{id:'a',name:'A',harness:'buzz-agent',executable:realpathSync(process.execPath),providerId:'p',model:'gpt-5',environment:{REVIEW_MODE:'first'}}]};
const saved = saveSettings(root,settings,0);
const base: Setup = {host:'fixture',ownerPublic:publicKey('1'.repeat(64)),runner:realpathSync(process.execPath),args:[],workspace:root,serviceHome:root,configDirectory:root,mode:'fixture'};
const first = runtimeBindings(base,saved)['runtime:a']!;
const next = saveSettings(root,{...saved,runtimes:[...saved.runtimes,{...saved.runtimes[0]!,id:'b',name:'B',environment:{REVIEW_MODE:'second'}}]},1);
assert.deepEqual(first.environment,{REVIEW_MODE:'first'});
assert.notEqual(bindingFingerprint(first),bindingFingerprint(runtimeBindings(base,next)['runtime:b']!));
assert.throws(()=>saveSettings(root,{...next,runtimes:[{...next.runtimes[0]!,environment:{REVIEW_MODE:'changed'}},next.runtimes[1]!]},2),/Retained/);
assert.equal(readSettings(root).revision,2);
} finally { rmSync(root,{recursive:true,force:true}); }
});
test('actual owned ACP child receives snapshotted local environment, not ambient identity or credentials', async () => {
const root = realpathSync(mkdtempSync(join(tmpdir(), 'beehive-pairing-cli-env-')));
let session: AgentSession | undefined;
try {
const script = join(root,'fixture.ts'), evidence = join(root,'environment.json');
writeFileSync(script,`import {writeFileSync} from 'node:fs'; writeFileSync(${JSON.stringify(evidence)},JSON.stringify({value:process.env.REVIEW_MODE, identity:process.env.BUZZ_PRIVATE_KEY, token:process.env.DATABRICKS_TOKEN})); await import(${JSON.stringify(new URL('./acp-fixture.ts',import.meta.url).href)});`);
const plan = {executable:realpathSync(process.execPath),args:[script],workspace:root,home:root,configDirectory:root,databricksHost:'https://fixture.invalid',model:'databricks-claude-haiku-4-5',environment:{REVIEW_MODE:'first'}};
const prepared = prepareAgent(plan);
plan.environment.REVIEW_MODE = 'later';
assert.equal(prepared.env.REVIEW_MODE,'first');
assert.equal(prepared.plan.environment?.REVIEW_MODE,'first');
session = new AgentSession(prepared.plan,1500);
await session.catalog(); await session.verify();
assert.deepEqual(JSON.parse(readFileSync(evidence,'utf8')),{value:'first'});
} finally { await session?.owned.stop(); rmSync(root,{recursive:true,force:true}); }
});
test('runtime form stops on failed discovery and cancelled models rather than continuing stale choices', async () => {
const {runtimeForm} = await import('../src/runtime-form.ts');
const snapshot = {local:[],agents:[],status:'synthetic',harnesses:[{id:'buzz-agent',label:'Buzz Agent',executable:'/fixture',state:'available' as const,providers:['openai' as const],reason:'fixture'}],settings:{version:1 as const,revision:1,agents:[],runtimes:[],providers:[{id:'p',name:'Provider',type:'openai' as const,endpoint:'https://api.openai.com/v1',key}]}};
let choices = 0;
const screen = {choose:async (_label:string,options:string[])=>{choices++;return options[0];},input:async()=>{throw Error('must not enter configuration');},confirm:async()=>{throw Error('must not save');},notice:()=>{}};
await runtimeForm(screen,()=>snapshot,async()=>({state:'failed',reason:'fixture'}));
assert.equal(choices,0);
const calls:string[]=[];
await runtimeForm(screen,()=>snapshot,async action=>{calls.push(action);return action === 'models' ? {state:'cancelled',reason:'fixture'} : {state:'completed'};});
assert.equal(choices,2);
assert.deepEqual(calls,['runtime-form','models']);
});

View File

@@ -132,7 +132,7 @@ test('effort is manifest-backed, validates persisted settings and reaches actual
test('short runtime form selects supported model-specific effort; unknown custom omits it and unsupported harness cannot save', async () => {
const snapshot: ManagerSnapshot = { local:[], agents:[], status:'synthetic', models:['gpt-5'], harnesses:[{id:'buzz-agent',label:'Buzz Agent',executable:'/fixture',state:'available',providers:['openai'],reason:'fixture'},{id:'pi',label:'Pi',state:'available',providers:[],reason:'Provider integration unavailable'}], settings:{version:1,revision:1,agents:[],runtimes:[],providers:[{id:'p',name:'OpenAI',type:'openai',endpoint:'https://api.openai.com/v1',key}]} };
const submitted: {action:string;values?:Record<string,string>}[] = [];
const run = async (choices: string[], inputs: string[]) => runtimeForm({choose:async (_label,options) => {const value=choices.shift(); assert.ok(value === undefined || options.includes(value)); return value;},input:async () => inputs.shift(),confirm:async label => {assert.match(label,/Running agents do not change/);return true;},notice:() => {}}, () => snapshot, async (action,values) => {submitted.push({action,values});});
const run = async (choices: string[], inputs: string[]) => runtimeForm({choose:async (_label,options) => {const value=choices.shift(); assert.ok(value === undefined || options.includes(value)); return value;},input:async label => label.startsWith('Environment') ? '{}' : inputs.shift(),confirm:async label => {assert.match(label,/Running agents do not change/);return true;},notice:() => {}}, () => snapshot, async (action,values) => {submitted.push({action,values});return {state:"completed"};});
await run(['Buzz Agent · Available','OpenAI · p','gpt-5','high'],['Reasoning']);
assert.equal(submitted.at(-1)?.values?.effort,'high');
assert.equal(submitted.at(-1)?.values?.model,'gpt-5');

View File

@@ -1,6 +1,27 @@
#!/bin/sh
# Synthetic UI only: fresh file credentials and explicitly blocked external seams.
set -eu
if [ "${1:-}" = --help ]; then
cat <<'HELP'
Synthetic manager walkthrough (100x30 terminal recommended).
Run without arguments; every invocation creates a fresh isolated HOME.
Right/Left: Host / Agents / Harnesses / Providers. a: controls. Tab: list.
Harnesses: Add runtime -> Buzz Agent -> Review OpenAI -> fixture-model ->
Environment JSON (e.g. {"REVIEW_MODE":"synthetic"}) -> Runtime name -> yes.
Escape at Environment cancels without saving. Select the saved runtime to
inspect harness/provider/model/effort/environment names. Save affects new runs.
Providers: Add provider or Models; synthetic keys only. Escape cancels.
Agents: Register opens hidden nsec entry; Escape cancels.
Ctrl-Q: exit even inside a form.
This fixture blocks external credentials, network, browser and service actions.
It does NOT demonstrate registration-first execution or authenticated Move.
Use the committed runtime-environment and host-settings tests for synthetic
owned-process environment and Save/Start/Stop evidence, not this UI fixture.
HELP
exit 0
fi
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
NODE=${BEEHIVE_REVIEW_NODE:-/Users/loganj/Library/Caches/hermit/pkg/node-24.15.0/bin/node}
BUN=${BEEHIVE_REVIEW_BUN:-/Users/loganj/.buzz/artifacts/beehive-opentui-57f9c8fa/runtime/bun-darwin-aarch64/bun}