mirror of
https://github.com/block/buzz.git
synced 2026-11-08 18:31:56 +00:00
feat(beehive): execute Pi with saved providers and owned refresh
Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
@@ -162,3 +162,30 @@ merely sending BUZZ_AGENT_PROVIDER to an unrelated process.
|
||||
|
||||
Evidence for this continuation, final suite and remaining work:
|
||||
`/Users/loganj/.buzz/artifacts/beehive-adapter-integration-9b0f8958/INTEGRATION_RESULT.md`.
|
||||
|
||||
## Pi runtime continuation (supersedes Pi-disabled statements above)
|
||||
|
||||
Pi now uses the Desktop `buzz-pi-acp` fork plus a separately resolved `pi` CLI
|
||||
with saved OpenAI and Databricks providers. Desktop imposes no Pi semver floor:
|
||||
execution requires fork identity, ACP protocol 1, and positive exact model and
|
||||
thinking config-option evidence before prompts. Both probe and conversation broker
|
||||
use `_meta.systemPrompt` and immutable spawn-time provider/model/effort settings.
|
||||
|
||||
A run-owned wrapper creates an isolated Pi directory. Its public `models.json`
|
||||
contains `$BEEHIVE_PI_KEY`, never a credential. OpenAI uses native Pi Responses;
|
||||
Databricks uses shared-manifest Responses/Anthropic/MLflow routing, preserving raw
|
||||
model/FQN and workspace. Its adapter receives only a private loopback capability;
|
||||
every provider request enters the existing native workspace-bound token source.
|
||||
No Desktop credentials, persistent Pi auth files, native OAuth copy or plugin
|
||||
framework. Unknown/custom models remain exact and do not gain guessed effort.
|
||||
Supported Pi thinking levels are the intersection of model capabilities and Pi's
|
||||
native vocabulary. No selection is explicitly `off`, not retained user settings.
|
||||
|
||||
Save/registration remain inert; existing runs keep their snapshots. Explicit
|
||||
Start/Stop own wrapper, adapter, Pi descendants, proxy and temporary directory.
|
||||
No native binary change; exports still require `src/*.ts`, `src/*.json` and the
|
||||
existing `bin/beehive-databricks`. Tests use explicit synthetic module/process
|
||||
seams (`pi-isolation-loader.mjs`), fake external ACP executables and loopback
|
||||
provider servers, not installed harnesses or real OS stores. Targeted cases live
|
||||
in `pi-host.test.ts`, `pi-conversation.test.ts`, and `pi.test.ts`. This slice does
|
||||
not claim completion of Codex/Claude or erase prior combined-suite failures.
|
||||
|
||||
@@ -686,3 +686,15 @@ The fixture reuses the private NIP42/59 relay via an optional HTTP handler; it d
|
||||
not change that relay's existing default admission behavior. No production relay,
|
||||
OS credential or installed-native public metadata evidence is claimed.
|
||||
See PUBLIC_METADATA.md for the editing and explicit publication path and limits.
|
||||
|
||||
### Pi saved-provider runtime slice
|
||||
|
||||
From package CWD, direct pinned Node: `node --test test/pi.test.ts
|
||||
test/pi-host.test.ts test/pi-conversation.test.ts test/runtime-integration.test.ts`.
|
||||
Pi host/conversation tests import `pi-isolation-loader.mjs` before dynamically
|
||||
loading product modules. The hook explicitly injects itself into the owned Pi
|
||||
wrapper and replaces only the native Databricks invocation with workspace/reference
|
||||
checked synthetic refresh. HTTPS destinations are denied except the exact fixture
|
||||
route, rewritten to a test-created loopback provider. Do not replace this with
|
||||
HOME isolation or run these fixtures against an installed harness. The negative
|
||||
Pi boundary tests use only saved synthetic OpenAI keys and make no provider call.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PI_ADAPTER, piEnvironment, piConfigEvidence } from './pi.ts';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { buzzProviderEnvironment, type BuzzProvider } from './buzz-provider.ts';
|
||||
import { validateCustom, type CustomAcp } from './custom-acp.ts';
|
||||
@@ -10,7 +11,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'; codex?: CodexSetup; claude?: ClaudeSetup; provider?: string; model: string; instructions?: string }>;
|
||||
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 }>;
|
||||
/** 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. */
|
||||
@@ -36,6 +37,10 @@ export function prepareAgent(input: AgentLaunch) {
|
||||
const d = validateCustom(plan.custom);
|
||||
if (plan.harness !== 'goose' || d.contract !== 'goose-native' || d.executable !== plan.executable || JSON.stringify(d.args) !== JSON.stringify(plan.args)) throw Error('Unsupported custom launch contract');
|
||||
}
|
||||
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)) });
|
||||
}
|
||||
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');
|
||||
@@ -89,6 +94,7 @@ export class AgentSession {
|
||||
private stoppingProbe = false;
|
||||
private session = '';
|
||||
private codexModelVerified = false;
|
||||
private piModelVerified = false;
|
||||
private response = '';
|
||||
private prompting = false;
|
||||
private timeoutMs: number;
|
||||
@@ -162,10 +168,12 @@ export class AgentSession {
|
||||
// Only observations belonging to this owned session can invalidate its proof.
|
||||
// Model acknowledgement is not permanent: keep this guard active after completion.
|
||||
if (p.sessionId !== this.session) return;
|
||||
if (update.sessionUpdate === 'current_model_update' && update.currentModelId !== this.prepared.plan.model) throw Error('ACP model changed');
|
||||
if (this.prepared.plan.harness === 'pi' && update.sessionUpdate === 'current_mode_update' && update.currentModeId !== (this.prepared.plan.buzzProvider?.effort ?? 'off')) throw Error('Pi effort changed');
|
||||
if (update.sessionUpdate === 'current_model_update' && update.currentModelId !== (this.prepared.plan.harness === 'pi' ? `beehive/${this.prepared.plan.model}` : this.prepared.plan.model)) throw Error('ACP model changed');
|
||||
if (update.sessionUpdate === 'config_option_update') {
|
||||
if (this.prepared.plan.harness === 'pi') piConfigEvidence(update, this.prepared.plan.model, this.prepared.plan.buzzProvider?.effort);
|
||||
if (!Array.isArray(update.configOptions)) throw Error('Invalid ACP model update');
|
||||
for (const option of update.configOptions) if (record(option).category === 'model' && record(option).currentValue !== this.prepared.plan.model) throw Error('ACP model changed');
|
||||
for (const option of update.configOptions) if (record(option).category === 'model' && record(option).currentValue !== (this.prepared.plan.harness === 'pi' ? `beehive/${this.prepared.plan.model}` : this.prepared.plan.model)) throw Error('ACP model changed');
|
||||
}
|
||||
if (p.sessionId !== this.session || !this.prompting) return;
|
||||
if (update.sessionUpdate === 'agent_message_chunk') {
|
||||
@@ -202,9 +210,13 @@ export class AgentSession {
|
||||
const nativeBuzz = !this.prepared.plan.harness;
|
||||
const protocol = this.prepared.plan.harness === 'codex' || nativeBuzz ? 2 : 1;
|
||||
const init = record(await this.request('initialize', { protocolVersion: protocol, clientCapabilities: {}, clientInfo: { name: 'beehive', version: '0.0.1' } }));
|
||||
if (init.protocolVersion !== protocol || record(init.agentInfo).name !== (this.prepared.plan.harness === 'codex' ? CODEX_ADAPTER : this.prepared.plan.harness === 'claude' ? CLAUDE_ADAPTER : this.prepared.plan.harness === 'goose' ? 'goose' : 'buzz-agent')) throw Error('Unsupported harness capabilities');
|
||||
const created = record(await this.request('session/new', { cwd: this.prepared.plan.workspace, mcpServers: [], ...((this.prepared.plan.harness === 'codex' || nativeBuzz) && this.prepared.plan.instructions !== undefined ? { systemPrompt: this.prepared.plan.instructions } : {}), ...(this.prepared.plan.harness === 'claude' && this.prepared.plan.instructions !== undefined ? { _meta: { systemPrompt: { append: this.prepared.plan.instructions } } } : {}) }));
|
||||
if (init.protocolVersion !== protocol || record(init.agentInfo).name !== (this.prepared.plan.harness === 'pi' ? PI_ADAPTER : this.prepared.plan.harness === 'codex' ? CODEX_ADAPTER : this.prepared.plan.harness === 'claude' ? CLAUDE_ADAPTER : this.prepared.plan.harness === 'goose' ? 'goose' : 'buzz-agent')) throw Error('Unsupported harness capabilities');
|
||||
const created = record(await this.request('session/new', { cwd: this.prepared.plan.workspace, mcpServers: [], ...(this.prepared.plan.harness === 'pi' && this.prepared.plan.instructions !== undefined ? { _meta: { systemPrompt: this.prepared.plan.instructions } } : {}), ...((this.prepared.plan.harness === 'codex' || nativeBuzz) && this.prepared.plan.instructions !== undefined ? { systemPrompt: this.prepared.plan.instructions } : {}), ...(this.prepared.plan.harness === 'claude' && this.prepared.plan.instructions !== undefined ? { _meta: { systemPrompt: { append: this.prepared.plan.instructions } } } : {}) }));
|
||||
this.session = identifier(created.sessionId);
|
||||
if (this.prepared.plan.harness === 'pi') {
|
||||
const models = piConfigEvidence(created, this.prepared.plan.model, this.prepared.plan.buzzProvider?.effort);
|
||||
this.piModelVerified = true; return { state: 'reported', models, authentication: 'unverified' };
|
||||
}
|
||||
if (this.prepared.plan.harness === 'codex') {
|
||||
const models = codexModels(created, this.prepared.plan.model);
|
||||
this.codexModelVerified = true;
|
||||
@@ -221,6 +233,7 @@ export class AgentSession {
|
||||
/** Require explicit exact-model acknowledgement, then a response in the SAME session/process. */
|
||||
async verify(): Promise<Evidence> {
|
||||
if (!this.session) throw Error('Create ACP session first');
|
||||
if (this.prepared.plan.harness === 'pi' && !this.piModelVerified) throw Error('Pi exact model/effort evidence required');
|
||||
if (this.prepared.plan.harness === 'codex' && !this.codexModelVerified) throw Error('Codex exact fresh model evidence required before prompts');
|
||||
const model = this.prepared.plan.model;
|
||||
if (!this.prepared.plan.harness) {
|
||||
@@ -245,6 +258,7 @@ export class AgentSession {
|
||||
export function spawnAgent(prepared: ReturnType<typeof prepareAgent>, extra: Record<string, string> = {}) {
|
||||
const { plan } = prepared;
|
||||
const env = { ...prepared.env, ...extra };
|
||||
if (plan.harness === 'pi') return spawnOwned(process.execPath, [fileURLToPath(new URL('./pi-runtime-child.ts', import.meta.url)), plan.executable], plan.workspace, env);
|
||||
if (plan.buzzProvider?.provider === 'databricks_v2' && plan.buzzProvider.credential) {
|
||||
delete env.DATABRICKS_TOKEN;
|
||||
env.BEEHIVE_DATABRICKS_RUNTIME = JSON.stringify({ host: plan.buzzProvider.baseUrl, key: plan.buzzProvider.credential, model: plan.model });
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { PI_ADAPTER, piConfigEvidence } from './pi.ts';
|
||||
import { CODEX_ADAPTER, codexModels } from './codex.ts';
|
||||
import { CLAUDE_ADAPTER, claudeModels } from './claude.ts';
|
||||
import { gooseModels } from './acp.ts';
|
||||
@@ -158,6 +159,7 @@ export class ConversationSession {
|
||||
const pending = new Map<string | number, Pending>();
|
||||
const sessions = new Set<string>();
|
||||
const customProfiles = new Set<string>();
|
||||
let piNative = false;
|
||||
const modelConfigs = new Set<string>(['model']);
|
||||
const prompts = new Map<string, { id: string | number; hash: ReturnType<typeof createHash>; text: boolean; cancelled: boolean }>();
|
||||
this.cancellations.push(() => {
|
||||
@@ -194,6 +196,15 @@ export class ConversationSession {
|
||||
if (p?.mode !== 'set' || p.key !== 'buzz' || typeof p.text !== 'string' || !p.text.includes(this.prepared.plan.instructions)) throw Error('Custom exact native profile missing');
|
||||
}
|
||||
}
|
||||
if (this.prepared.plan.harness === 'pi') {
|
||||
if (msg.method === 'initialize') p.protocolVersion = 1;
|
||||
if (['session/load','session/resume','session/set_model','session/set_mode','session/set_config_option'].includes(msg.method)) throw Error('Pi requires fresh session/Restart');
|
||||
if (msg.method === 'session/new') {
|
||||
if (!piNative) throw Error('Pi initialize required');
|
||||
delete p.systemPrompt;
|
||||
if (this.prepared.plan.instructions !== undefined) p._meta = { ...p._meta, systemPrompt: this.prepared.plan.instructions };
|
||||
}
|
||||
}
|
||||
if (this.prepared.plan.harness === 'codex') {
|
||||
if (msg.method === 'initialize') p.protocolVersion = 2;
|
||||
if (['session/load', 'session/resume', 'session/set_model', 'session/set_config_option'].includes(msg.method)) throw Error('Codex requires fresh session/Restart');
|
||||
@@ -260,6 +271,10 @@ export class ConversationSession {
|
||||
if (request.method === 'initialize' && this.prepared.plan.custom) {
|
||||
if (msg.result?.protocolVersion !== 1 || msg.result?.agentInfo?.name !== 'goose') throw Error('Custom Goose-native contract unavailable');
|
||||
}
|
||||
if (request.method === 'initialize' && this.prepared.plan.harness === 'pi') {
|
||||
if (msg.result?.protocolVersion !== 1 || msg.result?.agentInfo?.name !== PI_ADAPTER) throw Error('Pi fork capability missing');
|
||||
piNative = true;
|
||||
}
|
||||
if (request.method === 'initialize' && this.prepared.plan.harness === 'codex') {
|
||||
if (msg.result?.protocolVersion !== 2 || msg.result?.agentInfo?.name !== CODEX_ADAPTER) throw Error('Codex native profile capability unavailable');
|
||||
codexNative = true;
|
||||
@@ -282,6 +297,10 @@ export class ConversationSession {
|
||||
}
|
||||
if (modelConfigs.size > 128) throw Error();
|
||||
sessions.delete(session);
|
||||
if (this.prepared.plan.harness === 'pi') {
|
||||
piConfigEvidence(msg.result, this.prepared.plan.model, this.prepared.plan.buzzProvider?.effort);
|
||||
sessions.add(session); send(socket, msg); return;
|
||||
}
|
||||
if (this.prepared.plan.harness === 'codex') {
|
||||
codexModels(msg.result, this.prepared.plan.model);
|
||||
sessions.add(session); send(socket, msg); return;
|
||||
@@ -313,9 +332,13 @@ export class ConversationSession {
|
||||
}
|
||||
if (fromHarness && msg.method === 'session/update') {
|
||||
const p = msg.params; const u = p?.update;
|
||||
if (u?.sessionUpdate === 'current_model_update' && u.currentModelId !== this.prepared.plan.model) throw Error();
|
||||
if (this.prepared.plan.harness === 'pi') {
|
||||
if (u?.sessionUpdate === 'config_option_update') piConfigEvidence(u, this.prepared.plan.model, this.prepared.plan.buzzProvider?.effort);
|
||||
if (u?.sessionUpdate === 'current_mode_update' && u.currentModeId !== (this.prepared.plan.buzzProvider?.effort ?? 'off')) throw Error('Pi effort changed');
|
||||
}
|
||||
if (u?.sessionUpdate === 'current_model_update' && u.currentModelId !== (this.prepared.plan.harness === 'pi' ? `beehive/${this.prepared.plan.model}` : this.prepared.plan.model)) throw Error();
|
||||
if (u?.sessionUpdate === 'config_option_update' && Array.isArray(u.configOptions)) {
|
||||
for (const option of u.configOptions) if (option.category === 'model' && option.currentValue !== this.prepared.plan.model) throw Error();
|
||||
for (const option of u.configOptions) if (option.category === 'model' && option.currentValue !== (this.prepared.plan.harness === 'pi' ? `beehive/${this.prepared.plan.model}` : this.prepared.plan.model)) throw Error();
|
||||
}
|
||||
const prompt = prompts.get(p?.sessionId);
|
||||
if (prompt && u?.sessionUpdate === 'agent_message_chunk' && u.content?.type === 'text' && typeof u.content.text === 'string') {
|
||||
|
||||
@@ -36,6 +36,7 @@ export function prepareConversationBinding(input: ConversationSetup, agent: Agen
|
||||
if (prepared.plan.args.some(a => a.includes('\0'))) throw Error('ACP arguments cannot contain NUL');
|
||||
if (input.authTag !== undefined && (typeof input.authTag !== 'string' || input.authTag.length > 16384 || !input.authTag.length)) throw Error('Invalid local owner attestation');
|
||||
const providerEnv = { ...prepared.env };
|
||||
if (agent.harness === 'pi') { delete providerEnv.BEEHIVE_PI_KEY; delete providerEnv.BEEHIVE_PI_RUNTIME; }
|
||||
// The relay transport/shim never needs the OS-backed Databricks bearer.
|
||||
// The actual model consumer obtains a run-local capability in spawnAgent.
|
||||
if (agent.buzzProvider?.provider === 'databricks_v2' && agent.buzzProvider.credential) delete providerEnv.DATABRICKS_TOKEN;
|
||||
@@ -53,7 +54,7 @@ export function prepareConversationBinding(input: ConversationSetup, agent: Agen
|
||||
BUZZ_RELAY_URL: url.href,
|
||||
BUZZ_ACP_AGENT_OWNER: owner,
|
||||
...(input.authTag === undefined ? {} : { BUZZ_AUTH_TAG: input.authTag }),
|
||||
...((prepared.plan.harness === 'claude' || prepared.plan.harness === 'codex') ? {} : { BUZZ_ACP_MODEL: prepared.plan.model }),
|
||||
...((prepared.plan.harness === 'claude' || prepared.plan.harness === 'codex' || prepared.plan.harness === 'pi') ? {} : { BUZZ_ACP_MODEL: prepared.plan.model }),
|
||||
...(prepared.plan.instructions === undefined ? {} : { BUZZ_ACP_SYSTEM_PROMPT: prepared.plan.instructions }),
|
||||
BUZZ_ACP_MCP_COMMAND: '',
|
||||
BUZZ_ACP_AGENTS: '1',
|
||||
|
||||
@@ -45,6 +45,7 @@ export async function databricksRuntime(input: { host: string; key: ProviderRefe
|
||||
controller.signal.throwIfAborted();
|
||||
if (!token.secret || /\s/.test(token.secret)) throw Error();
|
||||
const headers = new Headers({ authorization: `Bearer ${token.secret}`, 'content-type': 'application/json' });
|
||||
if (url.pathname === '/ai-gateway/anthropic/v1/messages') headers.set('anthropic-version', '2023-06-01');
|
||||
token.secret = undefined;
|
||||
const upstream = await fetcher(host + url.pathname + url.search, { method: req.method, headers, ...(body ? { body: body.toString() } : {}), signal: controller.signal, redirect: 'error' });
|
||||
if (upstream.status === 401 || upstream.status === 403) { denied = true; await upstream.body?.cancel(); error(res, 401); return; }
|
||||
@@ -56,7 +57,7 @@ export async function databricksRuntime(input: { host: string; key: ProviderRefe
|
||||
finally { await reader.cancel(); reader.releaseLock(); }
|
||||
controller.signal.throwIfAborted();
|
||||
if (stopped || res.destroyed) return;
|
||||
res.writeHead(200, { 'content-type': 'application/json' }); res.end(Buffer.concat(chunks));
|
||||
res.writeHead(200, { 'content-type': upstream.headers.get('content-type')?.startsWith('text/event-stream') ? 'text/event-stream' : 'application/json' }); res.end(Buffer.concat(chunks));
|
||||
} catch { error(res, 503); }
|
||||
finally { clearTimeout(timer); req.removeListener('aborted', abort); res.removeListener('close', abort); active.delete(controller); }
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ export async function discoverHarnesses(signal: AbortSignal, options: { home?: s
|
||||
let state: DetectedHarness['state'] = !binary ? 'not-installed' : row.underlyingCli && !cli ? 'cli-missing' : 'available';
|
||||
if (state === 'available' && row.id === 'codex' && !compatibleCodex(await probe(binary!, ['--version'], [...managed, path].join(delimiter), home, signal) ?? '')) state = 'incompatible';
|
||||
signal.throwIfAborted();
|
||||
const providers = state === 'available' && row.id === 'buzz-agent' ? ['openai', 'databricks_v2'] : state === 'available' && row.id === 'codex' ? ['openai'] : [];
|
||||
const providers = state === 'available' && ['buzz-agent','pi'].includes(row.id) ? ['openai', 'databricks_v2'] : state === 'available' && row.id === 'codex' ? ['openai'] : [];
|
||||
const reason = state === 'not-installed' ? 'Not installed' : state === 'cli-missing' ? `Underlying ${row.underlyingCli} CLI not installed` : state === 'incompatible' ? 'Codex ACP 1.10.0 or newer required; version unknown/outdated' : providers.length ? 'Detected; provider/model access unverified' : 'Detected; this catalog’s OS-backed providers are not supported by this adapter. Use existing local setup for native authentication.';
|
||||
result.push({ id: row.id, label: row.label!, executable: binary, cli, state, providers, reason });
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { validatePi } from './pi.ts';
|
||||
import { readSettings } from './settings.ts';
|
||||
import { runtimeBindings } from './settings-runtime.ts';
|
||||
import { managerCredential } from './manager-credential.ts';
|
||||
@@ -25,7 +26,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'; codex?: CodexSetup; claude?: ClaudeSetup; gooseProvider?: string; gooseModels?: string[]; databricksHost?: string; serviceHome?: string; configDirectory?: string; conversation?: ConversationSetup };
|
||||
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 };
|
||||
/** 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;
|
||||
@@ -45,16 +46,18 @@ export function setupOwner(setup: Pick<Setup, 'ownerPublic' | 'ownerSecret'>): s
|
||||
return owner;
|
||||
}
|
||||
/** Operator-approved compatible models, not an authenticated provider catalog. */
|
||||
export function setupModels(s: Setup): string[] { return s.mode === 'buzz-agent-api-key' ? [...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 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);
|
||||
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);
|
||||
if (s.agentSecret !== undefined) publicKey(String(s.agentSecret));
|
||||
if (!isAbsolute(String(s.runner)) || !isAbsolute(String(s.workspace)) || !Array.isArray(s.args) || s.args.some(a => typeof a !== 'string') || !['buzz-agent-api-key','diagnostic-acp','fixture','buzz-agent-databricks-v2','goose','claude','codex'].includes(String(s.mode))) throw Error('Invalid local setup');
|
||||
if (s.buzzProvider !== undefined && s.mode !== 'buzz-agent-api-key') throw Error('Buzz provider belongs only to Buzz Agent API-key bindings');
|
||||
if (s.mode === 'buzz-agent-api-key') {
|
||||
if (!isAbsolute(String(s.runner)) || !isAbsolute(String(s.workspace)) || !Array.isArray(s.args) || s.args.some(a => typeof a !== 'string') || !['buzz-agent-api-key','diagnostic-acp','fixture','buzz-agent-databricks-v2','goose','claude','codex','pi'].includes(String(s.mode))) throw Error('Invalid local setup');
|
||||
if (s.piCli !== undefined && s.mode !== 'pi') throw Error('Pi CLI belongs only to Pi bindings');
|
||||
if (s.buzzProvider !== undefined && s.mode !== 'buzz-agent-api-key' && s.mode !== 'pi') throw Error('Buzz provider belongs only to Buzz Agent API-key bindings');
|
||||
if (s.mode === 'buzz-agent-api-key' || s.mode === 'pi') {
|
||||
if (s.mode === 'pi') validatePi(String(s.piCli), s.buzzProvider as BuzzProvider);
|
||||
validateBuzzProvider(s.buzzProvider as BuzzProvider);
|
||||
if (s.args.length || typeof s.serviceHome !== 'string' || !isAbsolute(s.serviceHome) || typeof s.configDirectory !== 'string' || !isAbsolute(s.configDirectory) || s.databricksHost !== undefined || s.gooseProvider !== undefined || s.claude !== undefined || s.codex !== undefined || s.custom !== undefined) throw Error('Buzz Agent requires dedicated local context and zero args, not another harness auth contract');
|
||||
}
|
||||
@@ -251,7 +254,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 === 'goose' || setup.mode === 'claude' || setup.mode === 'codex' ? '' : text(setup.databricksHost), ...(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 : { ...(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
|
||||
|
||||
@@ -276,8 +276,8 @@ A Stop result is not a recent host report that confirms the agent is stopped.` }
|
||||
const selected = this.harnesses?.find(h => h.id === (v.harness ?? 'buzz-agent'));
|
||||
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'].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', executable: selected.executable, ...(selected.id === 'codex' ? { cli: selected.cli } : {}), providerId: v.provider ?? '', model: v.model ?? '', ...(v.effort ? { effort: v.effort } : {}) }] },previous.revision);
|
||||
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);
|
||||
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.';
|
||||
|
||||
30
beehive/src/pi-runtime-child.ts
Normal file
30
beehive/src/pi-runtime-child.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { databricksRuntime } from './databricks-runtime.ts';
|
||||
import { piModelConfig } from './pi.ts';
|
||||
|
||||
// Entire wrapper, adapter, Pi children and native helpers are in the existing
|
||||
// supervisor group. No provider secret is persisted, including on forced exit.
|
||||
let stopping = false;
|
||||
let bridge: Awaited<ReturnType<typeof databricksRuntime>> | undefined;
|
||||
let child: ReturnType<typeof spawn> | undefined;
|
||||
let directory: string | undefined;
|
||||
const stop = async () => { stopping = true; child?.kill('SIGTERM'); await bridge?.stop(); };
|
||||
process.on('SIGTERM', () => { void stop(); });
|
||||
process.on('SIGINT', () => { void stop(); });
|
||||
try {
|
||||
const { provider, model } = JSON.parse(process.env.BEEHIVE_PI_RUNTIME ?? '');
|
||||
delete process.env.BEEHIVE_PI_RUNTIME;
|
||||
if (provider.provider === 'databricks_v2') bridge = await databricksRuntime({ host: provider.baseUrl, key: provider.credential, model });
|
||||
if (!stopping) {
|
||||
directory = mkdtempSync(join(tmpdir(), 'beehive-pi-'));
|
||||
const agent = join(directory, 'agent'); mkdirSync(agent, { mode: 0o700 });
|
||||
writeFileSync(join(agent, 'models.json'), JSON.stringify(piModelConfig(provider, model, bridge?.endpoint)), { mode: 0o600 });
|
||||
const executable = process.argv[2]; if (!executable) throw Error();
|
||||
child = spawn(executable, ['--', '--provider', 'beehive', '--model', model, '--thinking', provider.effort ?? 'off', '--no-extensions', '--no-skills', '--no-prompt-templates', '--no-themes'], { stdio: ['inherit','inherit','ignore'], env: { ...process.env, HOME: directory, PI_CODING_AGENT_DIR: agent, BEEHIVE_PI_KEY: bridge?.capability ?? process.env.BEEHIVE_PI_KEY! } });
|
||||
await new Promise<void>((resolve,reject) => { child!.once('error',reject); child!.once('close', code => { process.exitCode = stopping ? 0 : code ?? 1; resolve(); }); });
|
||||
}
|
||||
} catch { process.exitCode = 1; }
|
||||
finally { await stop(); if (directory) rmSync(directory, { recursive: true, force: true }); }
|
||||
45
beehive/src/pi.ts
Normal file
45
beehive/src/pi.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { accessSync, constants, realpathSync } from 'node:fs';
|
||||
import { dirname, isAbsolute } from 'node:path';
|
||||
import { validateBuzzProvider, type BuzzProvider } from './buzz-provider.ts';
|
||||
import { runtimeEfforts, runtimeCapabilities } from './runtime-effort.ts';
|
||||
|
||||
/** Desktop 78618804: fork identity (not upstream pi-acp) gates native metadata.
|
||||
* The fork has no Desktop semver floor; positive session config evidence is required. */
|
||||
export const PI_ADAPTER = 'buzz-pi-acp';
|
||||
export function piEfforts(provider: string, model: string) {
|
||||
return runtimeEfforts(provider, model).filter(e => ['minimal','low','medium','high','xhigh'].includes(e));
|
||||
}
|
||||
export function validatePi(cli: string, provider: BuzzProvider) {
|
||||
validateBuzzProvider(provider);
|
||||
if (!isAbsolute(cli) || !provider.credential || !['openai-compat','databricks_v2'].includes(provider.provider) || provider.apiKeyFile !== undefined) throw Error('Pi requires a saved OS provider and explicit CLI');
|
||||
if (provider.effort !== undefined && provider.models.some(m => !piEfforts(provider.provider, m).includes(provider.effort!))) throw Error('Unsupported Pi effort');
|
||||
}
|
||||
export function piEnvironment(cli: string, provider: BuzzProvider, home: string, model: string, secret?: string): Record<string,string> {
|
||||
validatePi(cli, provider);
|
||||
if (realpathSync(cli) !== cli || !provider.models.includes(model)) throw Error('Pi CLI/model changed');
|
||||
accessSync(cli, constants.X_OK);
|
||||
if (!secret || secret.length > 16384 || /[\s\0]/.test(secret)) throw Error('Pi OS provider admission required');
|
||||
return { PATH: `${dirname(cli)}:${dirname(process.execPath)}:/usr/bin:/bin`, HOME: home, PI_ACP_PI_COMMAND: cli,
|
||||
BEEHIVE_PI_KEY: provider.provider === 'databricks_v2' ? 'native-refresh-owned' : secret,
|
||||
BEEHIVE_PI_RUNTIME: JSON.stringify({ provider, model }) };
|
||||
}
|
||||
/** Public-only models.json: apiKey names an environment variable, never a token.
|
||||
* Provider name is private to this run; raw model/FQN remains unchanged on wire. */
|
||||
export function piModelConfig(provider: BuzzProvider, model: string, endpoint = provider.baseUrl) {
|
||||
const db = provider.provider === 'databricks_v2';
|
||||
const route = runtimeCapabilities(provider.provider,model)?.databricks_v2_wire_route;
|
||||
const anthropic = db && route === 'anthropic-messages';
|
||||
const responses = db && route === 'openai-responses';
|
||||
const api = anthropic ? 'anthropic-messages' : responses || !db ? 'openai-responses' : 'openai-completions';
|
||||
const baseUrl = endpoint + (db ? anthropic ? '/ai-gateway/anthropic' : responses ? '/ai-gateway/openai/v1' : '/ai-gateway/mlflow/v1' : '');
|
||||
return { providers: { beehive: { baseUrl, api, apiKey: '$BEEHIVE_PI_KEY', authHeader: true, models: [{ id: model, name: model, reasoning: piEfforts(provider.provider,model).length > 0, input: ['text'] }] } } };
|
||||
}
|
||||
export function piConfigEvidence(value: any, model: string, effort?: string): string[] {
|
||||
const options = value?.configOptions;
|
||||
if (!Array.isArray(options) || options.length > 128) throw Error('Pi config capability missing');
|
||||
for (const [category, expected] of [['model', `beehive/${model}`], ['thought_level', effort ?? 'off']]) {
|
||||
const matches = options.filter((o: any) => o.category === category);
|
||||
if (matches.length !== 1 || matches[0].currentValue !== expected || !Array.isArray(matches[0].options) || !matches[0].options.some((o: any) => o.value === expected)) throw Error('Pi exact model/effort evidence mismatch');
|
||||
}
|
||||
return [model];
|
||||
}
|
||||
@@ -3,12 +3,25 @@ import manifest from './model-capabilities.json' with { type: 'json' };
|
||||
/** Packaged verbatim from scripts/model-capabilities.json. Interpretation mirrors
|
||||
* Desktop modelCapabilities.ts exact/family precedence. No fallback effort picker:
|
||||
* unknown custom names and UC FQNs deliberately omit tuning rather than guessing. */
|
||||
export function runtimeEfforts(provider: string, model: string): readonly string[] {
|
||||
export function runtimeCapabilities(provider: string, model: string) {
|
||||
const canon = provider === 'openai-compat' ? 'openai' : provider;
|
||||
if (canon === 'databricks_v2' && model.split('.').length === 3 && model.split('.').every(v => v.length > 0 && !/[\s/]/.test(v))) return [];
|
||||
if (canon === 'databricks_v2' && model.split('.').length === 3 && model.split('.').every(v => v.length > 0 && !/[\s/]/.test(v))) {
|
||||
const service = model.split('.')[2]!.toLowerCase();
|
||||
const tokens = manifest.family_tokens.flatMap(token => {
|
||||
let from = 0;
|
||||
while (true) {
|
||||
const i = service.indexOf(token, from); if (i < 0) return [];
|
||||
if (i === 0 || !/[a-z0-9]/.test(service[i-1]!)) return [i];
|
||||
from = i + 1;
|
||||
}
|
||||
});
|
||||
const stripped = service.slice(tokens.length ? Math.min(...tokens) : 0);
|
||||
const version = /^gpt-(\d+)(?=[^a-z0-9]|$)/.exec(stripped);
|
||||
return { ...manifest.provider_fallbacks.databricks_v2.concrete_unknown, supported_efforts: [] as string[], databricks_v2_wire_route: version && Number(version[1]) >= 5 ? 'openai-responses' : 'mlflow-chat' };
|
||||
}
|
||||
const lower = model.toLowerCase();
|
||||
const exact = manifest.exact_records.find(r => r.provider === canon && r.raw_model_id.toLowerCase() === lower);
|
||||
if (exact) return exact.thinking_mode === 'omit-fields' ? [] : exact.supported_efforts;
|
||||
if (exact) return exact;
|
||||
let start = lower.length;
|
||||
for (const token of manifest.family_tokens) {
|
||||
let from = 0;
|
||||
@@ -26,7 +39,14 @@ export function runtimeEfforts(provider: string, model: string): readonly string
|
||||
return lengths.length ? [{ rule, length: Math.max(...lengths) }] : [];
|
||||
}).sort((a, b) => b.length - a.length || (a.rule.id < b.rule.id ? -1 : 1));
|
||||
const rule = matches[0]?.rule;
|
||||
return !rule || rule.thinking_mode === 'omit-fields' ? [] : rule.supported_efforts;
|
||||
if (rule) return rule;
|
||||
const fallback = manifest.provider_fallbacks[canon as keyof typeof manifest.provider_fallbacks]?.concrete_unknown;
|
||||
return fallback ? { ...fallback, supported_efforts: [] as string[] } : undefined;
|
||||
}
|
||||
|
||||
export function runtimeEfforts(provider: string, model: string): readonly string[] {
|
||||
const result = runtimeCapabilities(provider, model);
|
||||
return !result || result.thinking_mode === 'omit-fields' ? [] : result.supported_efforts;
|
||||
}
|
||||
|
||||
/** Recheck at save AND actual launch; no caller can inject a universal level. */
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { piEfforts } from './pi.ts';
|
||||
import { runtimeEfforts } from './runtime-effort.ts';
|
||||
import type { ManagerSnapshot } from './manager-controller.ts';
|
||||
|
||||
@@ -23,7 +24,7 @@ export async function runtimeForm(screen: {
|
||||
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) : [];
|
||||
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 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 } : {}) });
|
||||
|
||||
@@ -14,7 +14,7 @@ export function runtimeBindings(base: Setup, settings: Settings): Record<string,
|
||||
host: base.host, ownerPublic: base.ownerPublic, ...(base.ownerSecret ? { ownerSecret: base.ownerSecret } : {}), ...(base.agentSecret ? { agentSecret: base.agentSecret } : {}),
|
||||
runner: runtime.executable, args: [], workspace: base.workspace, allowedWorkspaces: base.allowedWorkspaces,
|
||||
serviceHome: base.serviceHome, configDirectory: base.configDirectory,
|
||||
...(base.conversation ? { conversation: base.conversation } : {}), ...(runtime.harness === 'codex' ? { mode: 'codex', codex: { cli: runtime.cli!, credential: provider.key, models: [runtime.model] } } : { mode: 'buzz-agent-api-key',
|
||||
...(base.conversation ? { conversation: base.conversation } : {}), ...(runtime.harness === 'codex' ? { mode: 'codex', codex: { cli: runtime.cli!, credential: provider.key, models: [runtime.model] } } : { mode: runtime.harness === 'pi' ? 'pi' : 'buzz-agent-api-key', ...(runtime.harness === 'pi' ? { piCli: runtime.cli } : {}),
|
||||
buzzProvider: provider.type === 'databricks_v2' ? { provider:'databricks_v2',auth:'token',baseUrl:provider.endpoint,credential:provider.key,models:[runtime.model],...(runtime.effort ? { effort:runtime.effort } : {}) } : { provider: 'openai-compat', baseUrl: provider.endpoint, credential: provider.key, wire: 'auto', models: [runtime.model], ...(runtime.effort ? { effort:runtime.effort } : {}) } }),
|
||||
})];
|
||||
}));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { piEfforts } from './pi.ts';
|
||||
import { validateRuntimeEffort } from './runtime-effort.ts';
|
||||
import { databricksHost } from './databricks.ts';
|
||||
import { existsSync, mkdirSync, rmdirSync, lstatSync } from 'node:fs';
|
||||
@@ -11,7 +12,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'; cli?: string; executable: string; providerId: string; model: string; effort?: string };
|
||||
export type SavedRuntime = { 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);
|
||||
@@ -26,9 +27,9 @@ export function validateSettings(value: Settings): Settings {
|
||||
if (a.profile && (typeof a.profile.relay !== 'string' || Object.values(a.profile).some(v => typeof v !== 'string' || v.length > 2048 || /[\x00-\x1f\x7f]/.test(v)))) throw Error('Invalid public profile');
|
||||
}
|
||||
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'].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 (!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.harness === 'codex' ? !r.cli || !isAbsolute(r.cli) || r.effort !== undefined || value.providers.find(p => p.id === r.providerId)?.type !== 'openai' : r.cli !== undefined) throw Error('Unsupported runtime provider/CLI/effort combination');
|
||||
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);
|
||||
// Reject unknown fields, including accidental secret-bearing input.
|
||||
|
||||
44
beehive/test/pi-acp-fixture.ts
Normal file
44
beehive/test/pi-acp-fixture.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { createInterface } from 'node:readline';
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import assert from 'node:assert/strict';
|
||||
const config = JSON.parse(readFileSync(join(process.cwd(),'pi-fixture.json'),'utf8'));
|
||||
const agent = process.env.PI_CODING_AGENT_DIR!;
|
||||
const document = readFileSync(join(agent,'models.json'),'utf8');
|
||||
const provider = JSON.parse(document).providers.beehive;
|
||||
assert.equal(provider.apiKey,'$BEEHIVE_PI_KEY');
|
||||
assert.equal(provider.models[0].id,config.model);
|
||||
assert.equal(provider.models[0].name,config.model);
|
||||
assert.equal(document.includes('synthetic'),false);
|
||||
const args = process.argv.slice(2);
|
||||
assert.equal(args[args.indexOf('--model')+1],config.model);
|
||||
assert.equal(args[args.indexOf('--thinking')+1],config.effort ?? 'off');
|
||||
assert.equal(process.env.PI_ACP_PI_COMMAND,process.execPath);
|
||||
assert.equal(process.env.OPENAI_API_KEY,undefined);
|
||||
assert.equal(process.env.DATABRICKS_TOKEN,undefined);
|
||||
assert.equal(process.env.BEEHIVE_PI_RUNTIME,undefined);
|
||||
const descendant = spawn(process.execPath,['-e','setInterval(()=>{},1000)'],{stdio:'ignore'});
|
||||
writeFileSync(join(process.cwd(),'pi-owned.json'),JSON.stringify({adapter:process.pid,descendant:descendant.pid,directory:process.env.HOME,endpoint:provider.baseUrl}));
|
||||
const sessionId = 'pi-fixture-session';
|
||||
const configOptions = [{id:'model',category:'model',currentValue:`beehive/${config.model}`,options:[{value:`beehive/${config.model}`} ]},{id:'thought_level',category:'thought_level',currentValue:config.effort ?? 'off',options:[{value:config.effort ?? 'off'}]}];
|
||||
const send = (m: unknown) => console.log(JSON.stringify({jsonrpc:'2.0',...m as object}));
|
||||
createInterface({input:process.stdin}).on('line',async line => {
|
||||
const m=JSON.parse(line); let result;
|
||||
if(m.method==='initialize') result={protocolVersion:config.mode==='wrong-protocol'?2:1,agentInfo:{name:config.mode==='wrong-fork'?'pi-acp':'buzz-pi-acp',version:'0.0.33'}};
|
||||
else if(m.method==='session/new') { if(config.instructions) assert.equal(m.params._meta?.systemPrompt,config.instructions); if(config.mode==='wrong-model') configOptions[0].currentValue='beehive/wrong'; if(config.mode==='wrong-effort') configOptions[1].currentValue='low'; result={sessionId,configOptions}; }
|
||||
else if(m.method==='session/prompt') {
|
||||
assert.equal(m.params.sessionId,sessionId);
|
||||
if(config.mode==='drift-effort' || config.mode==='drift-model') { send({method:'session/update',params:{sessionId,update:config.mode==='drift-effort'?{sessionUpdate:'current_mode_update',currentModeId:'low'}:{sessionUpdate:'current_model_update',currentModelId:'beehive/wrong'}}});return; }
|
||||
for(let n=0;n<(config.db?2:1);n++) {
|
||||
const endpoint=config.db?provider.baseUrl+'/responses':config.endpoint+'/v1/responses';
|
||||
if(!config.db) assert.equal(provider.baseUrl,'https://api.openai.com/v1');
|
||||
const response=await fetch(endpoint,{method:'POST',headers:{authorization:'Bearer '+process.env[provider.apiKey.slice(1)]},body:JSON.stringify({model:provider.models[0].id, ...(config.effort?{reasoning:{effort:args[args.indexOf('--thinking')+1]}}:{}),input:'synthetic'})});
|
||||
assert.equal(response.status,200); await response.text();
|
||||
if(config.db) await new Promise(r=>setTimeout(r,75));
|
||||
}
|
||||
send({method:'session/update',params:{sessionId,update:{sessionUpdate:'agent_message_chunk',content:{type:'text',text:'Synthetic Pi complete'}}}}); result={stopReason:'end_turn'};
|
||||
} else if(m.method==='session/cancel') return;
|
||||
else throw Error('Unexpected Pi RPC '+m.method);
|
||||
send({id:m.id,result});
|
||||
});
|
||||
51
beehive/test/pi-conversation.test.ts
Normal file
51
beehive/test/pi-conversation.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import './pi-isolation-loader.mjs';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { createServer } from 'node:http';
|
||||
import { once } from 'node:events';
|
||||
import { mkdtempSync, writeFileSync, readFileSync, realpathSync, rmSync, existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
const {ConversationSession}=await import('../src/broker.ts');
|
||||
const {newKey,publicKey}=await import('../src/protocol.ts');
|
||||
for(const variant of ['openai','databricks','cancel']) test(`Pi ${variant} conversation broker proves model/effort, native metadata and owned Stop`,async t=>{
|
||||
const db=variant!=='openai',cancel=variant==='cancel';
|
||||
const root=realpathSync(mkdtempSync(join(tmpdir(),'beehive-pi-conversation-')));t.after(()=>rmSync(root,{recursive:true,force:true}));
|
||||
const model=db?'databricks-gpt-5-4':'gpt-5',effort='high',instructions='Pi exact native standing instructions';
|
||||
const key={service:'beehive' as const,account:'provider:00000000-0000-0000-0000-000000000000'};
|
||||
const observed:any[]=[];let requested!:()=>void;const requestStarted=new Promise<void>(resolve=>{requested=resolve;});
|
||||
const server=createServer(async(req,res)=>{let body='';for await(const c of req)body+=c;observed.push({path:req.url,auth:req.headers.authorization,body:JSON.parse(body)});requested();if(!cancel)res.end('{}');});
|
||||
server.listen(0,'127.0.0.1');await once(server,'listening');t.after(()=>new Promise<void>(r=>server.close(()=>r())));
|
||||
const endpoint=`http://127.0.0.1:${(server.address() as any).port}`,route=db?'/ai-gateway/openai/v1/responses':'/v1/responses';
|
||||
writeFileSync(join(root,'pi-fixture.json'),JSON.stringify({model,effort,db,endpoint,route,key,instructions}));
|
||||
const executable=join(root,'buzz-pi-acp'),runtime=join(root,'fake-relay-runtime');
|
||||
writeFileSync(executable,`#!${process.execPath}\nawait import(${JSON.stringify(new URL('./pi-acp-fixture.ts',import.meta.url).href)});\n`,{mode:0o700});
|
||||
writeFileSync(runtime,`#!${process.execPath}
|
||||
import {spawn} from 'node:child_process';import {createInterface} from 'node:readline';import {writeFileSync} from 'node:fs';
|
||||
if(process.env.BEEHIVE_PI_KEY || process.env.BEEHIVE_PI_RUNTIME || process.env.BUZZ_ACP_MODEL) throw Error('Relay runtime received provider control');
|
||||
const child=spawn(process.env.BUZZ_ACP_AGENT_COMMAND,process.env.BUZZ_ACP_AGENT_ARGS.split(','),{detached:true,stdio:['pipe','pipe','ignore'],env:{PATH:'/usr/bin:/bin'}});
|
||||
writeFileSync('pi-transport-pids',JSON.stringify([process.pid,child.pid]));
|
||||
const pending=new Map();let seq=0;
|
||||
createInterface({input:child.stdout}).on('line',line=>{const m=JSON.parse(line);pending.get(m.id)?.(m);});
|
||||
const request=(method,params)=>new Promise(resolve=>{const id=++seq;pending.set(id,resolve);child.stdin.write(JSON.stringify({jsonrpc:'2.0',id,method,params})+'\\n');});
|
||||
await request('initialize',{protocolVersion:2,clientCapabilities:{}});
|
||||
const created=await request('session/new',{cwd:process.cwd(),mcpServers:[]});
|
||||
await request('session/prompt',{sessionId:created.result.sessionId,prompt:[{type:'text',text:'Synthetic conversation'}]});
|
||||
`,{mode:0o700});
|
||||
const session=new ConversationSession({executable:runtime,relay:'ws://127.0.0.1:1'},{executable,args:[],workspace:root,home:root,configDirectory:root,databricksHost:'',harness:'pi',piCli:realpathSync(process.execPath),model,instructions,resolvedProviderKey:'synthetic-key',buzzProvider:{provider:db?'databricks_v2':'openai-compat',baseUrl:db?'https://pi-workspace.example':'https://api.openai.com/v1',credential:key,models:[model],effort,...(db?{auth:'token' as const}:{wire:'auto' as const})}},newKey(),publicKey(newKey()));
|
||||
t.after(()=>session.stop());
|
||||
const proof=session.verify();
|
||||
if(cancel) {
|
||||
const rejected=assert.rejects(proof);await Promise.race([requestStarted,proof]);session.cancel();await rejected;
|
||||
} else {
|
||||
const evidence=await proof;assert.equal(evidence.model,model);assert.equal(evidence.source,'external-buzz-conversation');
|
||||
assert.equal(observed.length,db?2:1);
|
||||
for(const [i,row]of observed.entries()){assert.equal(row.path,route);assert.equal(row.body.model,model);assert.equal(row.body.reasoning.effort,effort);assert.equal(row.auth,db?`Bearer synthetic-pi-generation-${i+1}`:'Bearer synthetic-key');}
|
||||
}
|
||||
await session.stop();
|
||||
const owned=JSON.parse(readFileSync(join(root,'pi-owned.json'),'utf8'));
|
||||
const transport=JSON.parse(readFileSync(join(root,'pi-transport-pids'),'utf8'));
|
||||
for(const pid of [...transport,owned.adapter,owned.descendant])assert.throws(()=>process.kill(pid,0),(e:any)=>e.code==='ESRCH');
|
||||
assert.equal(existsSync(owned.directory),false);
|
||||
if(db)await assert.rejects(fetch(owned.endpoint+'/responses'));
|
||||
});
|
||||
98
beehive/test/pi-host.test.ts
Normal file
98
beehive/test/pi-host.test.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import './pi-isolation-loader.mjs';
|
||||
import type { Setup } from '../src/host.ts';
|
||||
import type { Message } from '../src/protocol.ts';
|
||||
import { createServer } from 'node:http';
|
||||
import { once } from 'node:events';
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, rmSync, existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
// Load product modules only AFTER the explicit subprocess isolation hooks.
|
||||
const { addDatabricks } = await import('../src/databricks.ts');
|
||||
const { addOpenAI } = await import('../src/settings-credentials.ts');
|
||||
const { readSettings, saveSettings, settingsId } = await import('../src/settings.ts');
|
||||
const { newKey, publicKey, message } = await import('../src/protocol.ts');
|
||||
const { host, provision } = await import('../src/host.ts');
|
||||
const { createGenesis } = await import('../src/assignment.ts');
|
||||
const { writePrivate } = await import('../src/storage.ts');
|
||||
|
||||
function fixture(t: test.TestContext) { const root = realpathSync(mkdtempSync(join(tmpdir(),'beehive-pairing-cli-settings-'))); t.after(() => rmSync(root,{ recursive: true, force: true })); return root; }
|
||||
const sleep = (ms: number) => new Promise(r => setTimeout(r,ms));
|
||||
async function wait(fn: () => boolean) { for(let n=0;n<150;n++) { if(fn()) return; await sleep(20); } throw Error('Fixture wait expired'); }
|
||||
|
||||
for (const providerType of ['pi-openai','pi-databricks','pi-custom','pi-databricks-custom'] as const) test(`${providerType} saved runtime reaches host Save/new Start while the prior active run remains immutable`, async t => {
|
||||
const root = fixture(t), owner = newKey(), agent = newKey(), hostKey = publicKey(newKey());
|
||||
const db = providerType.startsWith('pi-databricks');
|
||||
const home = root, config = root;
|
||||
const setup: Setup = { host: hostKey, ownerSecret: owner, agentSecret: agent, runner: realpathSync(process.execPath), args: ['-e','setInterval(()=>{},1000)'], workspace: root, serviceHome: home, configDirectory: config, mode: 'fixture' };
|
||||
provision(root,setup,createGenesis(publicKey(owner),publicKey(agent),hostKey));
|
||||
let receive: (m: Message) => void = () => {}; const reports: Message[] = [];
|
||||
const transport = { binding: { host: hostKey, owner: publicKey(owner) }, validate() {}, connect(_url: string,_secret: string,handle: (m: Message) => void) { receive = handle; return { ready: Promise.resolve(), send(m: Message) { reports.push(m); }, close() {} }; } };
|
||||
let providerReads = 0;
|
||||
const running = await host(root,'ws://127.0.0.1',undefined,transport,undefined,async (input,signal) => { signal.throwIfAborted(); assert.equal((input as any).provider,!db ? 'openai-compat' : 'databricks_v2'); if (db) assert.equal((input as any).host,'https://pi-workspace.example'); providerReads++; return {ok:true,secret:'synthetic-provider-key'}; });
|
||||
t.after(() => running.close());
|
||||
const start = message('start',hostKey,publicKey(agent),0); receive(start);
|
||||
await wait(() => reports.some(m => m.type === 'inventory' && m.body.phase === 'running'));
|
||||
const active = reports.filter(m => m.type === 'inventory').at(-1)!.body.actualRun;
|
||||
let stored: string | null = null;
|
||||
if (!db) addOpenAI(root,'Fixture','synthetic',{ read: () => stored, create(_r,v) { stored = v; } });
|
||||
else await addDatabricks(root,'Workspace','https://pi-workspace.example',new AbortController().signal,async input => { assert.equal(input.action,'login'); return {ok:true}; });
|
||||
const prior = readSettings(root), id = settingsId();
|
||||
const model = providerType === 'pi-databricks-custom' ? 'tenant.schema.gpt-6-service' : providerType === 'pi-custom' ? 'retained-custom-model' : db ? 'databricks-gpt-5-4' : 'gpt-5';
|
||||
const effort = providerType.endsWith('custom') ? undefined : 'high';
|
||||
const executable = join(root,'buzz-pi-acp');
|
||||
const observations: any[] = [];
|
||||
const server = createServer(async (req,res) => {
|
||||
let body=''; for await(const chunk of req) body+=chunk;
|
||||
observations.push({path:req.url,authorization:req.headers.authorization,body:JSON.parse(body)});
|
||||
res.setHeader('content-type','text/event-stream');res.end('data: {"type":"response.completed"}\n\n');
|
||||
});
|
||||
server.listen(0,'127.0.0.1');await once(server,'listening');
|
||||
t.after(() => new Promise<void>(resolve=>server.close(()=>resolve())));
|
||||
const endpoint=`http://127.0.0.1:${(server.address() as any).port}`;
|
||||
const route=db?'/ai-gateway/openai/v1/responses':'/v1/responses';
|
||||
writeFileSync(join(root,'pi-fixture.json'),JSON.stringify({model,effort,db,endpoint,route,key:prior.providers[0]!.key}));
|
||||
writeFileSync(executable,`#!${process.execPath}\nawait import(${JSON.stringify(new URL('./pi-acp-fixture.ts',import.meta.url).href)});\n`,{mode:0o700});
|
||||
saveSettings(root,{ ...prior, runtimes: [{ id, name: 'Pi custom name', harness: 'pi', executable, providerId: prior.providers[0]!.id, model, cli:realpathSync(process.execPath), ...(effort?{effort}:{}) }] },prior.revision);
|
||||
assert.equal(readSettings(root).runtimes[0]!.name,'Pi custom name');
|
||||
assert.equal(providerReads,0,'Save does not accept secrets or start Pi');
|
||||
await wait(() => running.settingsRevision === 2);
|
||||
await wait(() => reports.some(m => m.type === 'inventory' && (m.body.harnessSetups as any[]).some(r => r.id === `runtime:${id}`)));
|
||||
const inventory = reports.filter(m => m.type === 'inventory').at(-1)!;
|
||||
assert.deepEqual(inventory.body.actualRun,active);
|
||||
const binding = (inventory.body.harnessSetups as any[]).find(r => r.id === `runtime:${id}`);
|
||||
const save = message('save',hostKey,publicKey(agent),inventory.revision,{ model, workspace: root, profile: 'default', harnessSetup: { id: binding.id, fingerprint: binding.fingerprint } });
|
||||
receive(save); await wait(() => reports.some(m => m.body.operation === save.id));
|
||||
assert.equal(reports.find(m => m.body.operation === save.id)?.body.result,'saved; running configuration unchanged');
|
||||
assert.deepEqual(reports.filter(m => m.type === 'inventory').at(-1)!.body.actualRun,active);
|
||||
const current = reports.filter(m => m.type === 'inventory').at(-1)!;
|
||||
const stop = message('stop',hostKey,publicKey(agent),current.revision); receive(stop); await wait(() => reports.some(m => m.body.operation === stop.id));
|
||||
const stopped = reports.filter(m => m.type === 'inventory').at(-1)!;
|
||||
assert.equal(stopped.body.phase,'stopped');
|
||||
const nextStart = message('start',hostKey,publicKey(agent),stopped.revision); receive(nextStart); await wait(() => reports.some(m => m.body.operation === nextStart.id));
|
||||
assert.equal(reports.find(m => m.body.operation === nextStart.id)?.body.result,'accepted');
|
||||
assert.equal((reports.filter(m => m.type === 'inventory').at(-1)!.body.actualRun as any).selection.model,model);
|
||||
assert.equal(providerReads,1);
|
||||
assert.equal(observations.length,db?2:1);
|
||||
for(const [i,observed] of observations.entries()) {
|
||||
assert.equal(observed.path,route);
|
||||
assert.equal(observed.body.model,model);
|
||||
assert.equal(observed.body.reasoning?.effort,effort);
|
||||
assert.equal(observed.authorization,db?`Bearer synthetic-pi-generation-${i+1}`:'Bearer synthetic-provider-key');
|
||||
}
|
||||
const owned=JSON.parse(readFileSync(join(root,'pi-owned.json'),'utf8'));
|
||||
const beforeStop=reports.filter(m=>m.type==='inventory').at(-1)!;
|
||||
const piStop=message('stop',hostKey,publicKey(agent),beforeStop.revision);receive(piStop);
|
||||
await wait(()=>reports.some(m=>m.body.operation===piStop.id));
|
||||
assert.equal(reports.filter(m=>m.type==='inventory').at(-1)!.body.phase,'stopped');
|
||||
for(const pid of [owned.adapter,owned.descendant]) assert.throws(()=>process.kill(pid,0),(e:any)=>e.code==='ESRCH');
|
||||
assert.equal(existsSync(owned.directory),false);
|
||||
if(db) { await assert.rejects(fetch(owned.endpoint+'/responses')); const native=readFileSync(join(root,'native-calls'),'utf8').trim().split('\n').map(v=>JSON.parse(v)); assert.equal(native.length,2); assert.deepEqual(native.map(v=>v.input.key),[prior.providers[0]!.key,prior.providers[0]!.key]); }
|
||||
assert.ok(!JSON.stringify(reports).includes('synthetic-provider-key'));
|
||||
assert.ok(!readFileSync(join(root,'journal.json'),'utf8').includes('synthetic-provider-key'));
|
||||
const invalid = { ...readSettings(root), revision: 3, runtimes: [] }; writePrivate(join(root,'settings.json'),invalid);
|
||||
await sleep(2100); assert.equal(running.settingsRevision,2,'invalid replacement retains effective catalog');
|
||||
await running.close();
|
||||
});
|
||||
|
||||
33
beehive/test/pi-isolation-loader.mjs
Normal file
33
beehive/test/pi-isolation-loader.mjs
Normal file
@@ -0,0 +1,33 @@
|
||||
// Explicit test-only subprocess seam: never installed or imported by production.
|
||||
import { registerHooks } from 'node:module';
|
||||
import { readFileSync, appendFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
const loader = import.meta.url;
|
||||
registerHooks({ load(url,context,next) {
|
||||
const result = next(url,context);
|
||||
if (url.endsWith('/src/acp.ts')) return { ...result, source: String(result.source).replace("[fileURLToPath(new URL('./pi-runtime-child.ts', import.meta.url)),", `[ '--import', ${JSON.stringify(loader)}, fileURLToPath(new URL('./pi-runtime-child.ts', import.meta.url)),`) };
|
||||
if (url.endsWith('/src/databricks.ts')) {
|
||||
const source = String(result.source);
|
||||
const start = source.indexOf('export async function databricksNative(');
|
||||
const end = source.indexOf('/** Native grant',start);
|
||||
if(start<0 || end<0) throw Error('Native isolation seam changed');
|
||||
return {...result,source:source.slice(0,start)+`export async function databricksNative(input,signal) { return globalThis.piFixtureNative(input,signal); }\n`+source.slice(end)};
|
||||
}
|
||||
return result;
|
||||
}});
|
||||
let expires = 0, generation = 0;
|
||||
globalThis.piFixtureNative = async (input,signal) => {
|
||||
signal.throwIfAborted();
|
||||
const fixture = JSON.parse(readFileSync(join(process.cwd(),'pi-fixture.json'),'utf8'));
|
||||
if (input.action !== 'token' || input.host !== 'https://pi-workspace.example' || JSON.stringify(input.key) !== JSON.stringify(fixture.key)) throw Error('Fixture native boundary mismatch');
|
||||
if (Date.now() >= expires) { generation++; expires=Date.now()+50; }
|
||||
appendFileSync(join(process.cwd(),'native-calls'),JSON.stringify({input,generation})+'\n');
|
||||
return {ok:true,secret:`synthetic-pi-generation-${generation}`};
|
||||
};
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async (url,options) => {
|
||||
if (!String(url).startsWith('https://')) return originalFetch(url,options);
|
||||
const fixture = JSON.parse(readFileSync(join(process.cwd(),'pi-fixture.json'),'utf8'));
|
||||
if (url !== 'https://pi-workspace.example'+fixture.route) throw Error('Nonfixture network forbidden');
|
||||
return originalFetch(fixture.endpoint+fixture.route,options);
|
||||
};
|
||||
48
beehive/test/pi.test.ts
Normal file
48
beehive/test/pi.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { mkdtempSync, writeFileSync, realpathSync, rmSync, readFileSync, existsSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { AgentSession, prepareAgent, type AgentLaunch } from '../src/acp.ts';
|
||||
import { piEfforts, piModelConfig } from '../src/pi.ts';
|
||||
import { runtimeForm } from '../src/runtime-form.ts';
|
||||
import type { ManagerSnapshot } from '../src/manager-controller.ts';
|
||||
import { runtimeCapabilities } from '../src/runtime-effort.ts';
|
||||
const key={service:'beehive' as const,account:'provider:00000000-0000-0000-0000-000000000000'};
|
||||
const provider={provider:'openai-compat' as const,baseUrl:'https://api.openai.com/v1',wire:'auto' as const,credential:key,models:['gpt-5'],effort:'high'};
|
||||
for(const mode of ['wrong-model','wrong-effort','wrong-fork','wrong-protocol','drift-effort','drift-model']) test(`Pi rejects ${mode} before any provider request, Stop owns children`,async t=>{
|
||||
const root=realpathSync(mkdtempSync(join(tmpdir(),'beehive-pi-negative-')));t.after(()=>rmSync(root,{recursive:true,force:true}));
|
||||
const executable=join(root,'buzz-pi-acp');
|
||||
writeFileSync(executable,`#!${process.execPath}\nawait import(${JSON.stringify(new URL('./pi-acp-fixture.ts',import.meta.url).href)});\n`,{mode:0o700});
|
||||
writeFileSync(join(root,'pi-fixture.json'),JSON.stringify({mode,model:'gpt-5',effort:'high',db:false,endpoint:'http://127.0.0.1:1'}));
|
||||
const launch:AgentLaunch={executable,args:[],workspace:root,home:root,configDirectory:root,databricksHost:'',harness:'pi',piCli:realpathSync(process.execPath),buzzProvider:provider,model:'gpt-5',resolvedProviderKey:'synthetic-secret'};
|
||||
assert.throws(()=>prepareAgent({...launch,resolvedProviderKey:undefined}),/admission/);
|
||||
const session=new AgentSession(launch);let stopped=false;t.after(()=>stopped?undefined:session.owned.stop());
|
||||
if(mode.startsWith('drift-')) await session.catalog(); else await assert.rejects(session.catalog());
|
||||
await assert.rejects(session.verify());
|
||||
await session.owned.stop();stopped=true;
|
||||
const owned=JSON.parse(readFileSync(join(root,'pi-owned.json'),'utf8'));
|
||||
for(const pid of [owned.adapter,owned.descendant]) assert.throws(()=>process.kill(pid,0),(e:any)=>e.code==='ESRCH');
|
||||
assert.equal(existsSync(owned.directory),false);
|
||||
});
|
||||
|
||||
test('Pi model routes use the shared capability owner; UC tenant components never choose protocol or effort',()=>{
|
||||
const db={...provider,provider:'databricks_v2' as const,wire:undefined,auth:'token' as const,baseUrl:'https://pi-workspace.example'};
|
||||
for(const [model,api,suffix] of [['databricks-gpt-5-4','openai-responses','/ai-gateway/openai/v1'],['databricks-claude-sonnet-4-5','anthropic-messages','/ai-gateway/anthropic'],['gpt-5.schema.unknown','openai-completions','/ai-gateway/mlflow/v1'],['catalog.schema.gpt-6-service','openai-responses','/ai-gateway/openai/v1'],['catalog.schema.agpt-5-gpt-6','openai-responses','/ai-gateway/openai/v1'],['catalog.schema.claude-gpt-5','openai-completions','/ai-gateway/mlflow/v1']]) {
|
||||
const p=piModelConfig(db,model).providers.beehive;
|
||||
assert.equal(p.api,api);assert.equal(p.baseUrl,db.baseUrl+suffix);assert.equal(p.models[0].id,model);
|
||||
}
|
||||
assert.deepEqual(piEfforts('databricks_v2','catalog.schema.gpt-6-service'),[]);
|
||||
assert.deepEqual(piEfforts('openai','unknown-custom'),[]);
|
||||
assert.equal(runtimeCapabilities('databricks_v2','gpt-5.schema.unknown')!.supported_efforts.length,0);
|
||||
});
|
||||
|
||||
test('actual runtime form offers Pi both providers, retains custom names, submits only supported model effort',async()=>{
|
||||
for(const type of ['openai','databricks_v2'] as const) {
|
||||
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'}});
|
||||
}
|
||||
});
|
||||
@@ -101,10 +101,11 @@ test('Desktop contract detects fake adapter/CLI independently, strict Codex vers
|
||||
const rows = await discoverHarnesses(new AbortController().signal, {home:root,path:root,bundled:[],common:[],loginShells:[]});
|
||||
assert.equal(rows.find(r => r.id === 'pi')?.executable,join(root,'buzz-pi-acp'));
|
||||
assert.equal(rows.find(r => r.id === 'pi')?.state,'available');
|
||||
assert.deepEqual(rows.find(r => r.id === 'pi')?.providers,['openai','databricks_v2']);
|
||||
assert.equal(rows.find(r => r.id === 'codex')?.state,'available');
|
||||
assert.equal(rows.find(r => r.id === 'claude')?.state,'cli-missing');
|
||||
assert.equal(rows.find(r => r.id === 'goose')?.state,'not-installed');
|
||||
assert.deepEqual(rows.filter(r => r.providers.length).map(r => r.id),['codex','buzz-agent']);
|
||||
assert.deepEqual(rows.filter(r => r.providers.length).map(r => r.id),['codex','buzz-agent','pi']);
|
||||
for (const value of ['1.9.99','1.10','1.10.0-rc1','not-version']) assert.equal(compatibleCodex(value),false);
|
||||
for (const value of ['1.10.0','pkg 2.0.0']) assert.equal(compatibleCodex(value),true);
|
||||
const slow = join(root,'slow'); writeFileSync(slow,'#!/bin/sh\nexec /bin/sleep 60\n',{mode:0o700});
|
||||
|
||||
Reference in New Issue
Block a user