mirror of
https://github.com/block/buzz.git
synced 2026-10-28 21:11:48 +00:00
feat(beehive): align saved providers and model choices with Desktop
Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
@@ -131,3 +131,93 @@ Independent UX acceptance is arranged by Larry after this functional handback.
|
||||
Current-Desktop provider/harness parity is a separately supplied bounded read-only
|
||||
delta, not a parity claim by this implementation. No packaged harness-contract pin,
|
||||
Pi/Codex policy, Desktop/native source or installed artifact changed here.
|
||||
|
||||
## Current Desktop parity continuation — b5cd2aa0
|
||||
|
||||
Local source continuation of `86921025`, compared with Desktop
|
||||
`6d1f488d2273dd314184fc2ec805f8982327dc51` at `refs/parity/a8c4d530-main`.
|
||||
The parity paragraph above is historical; this bounded delta is now implemented.
|
||||
The enrollment/Move evidence above remains reusable and unchanged in its original
|
||||
artifact directory. Installed `176497874` exports, native code, relay protocols,
|
||||
harness discovery tiers and Databricks v2 auth are unchanged.
|
||||
|
||||
- Providers independently adds OpenAI, Anthropic, OpenAI-compatible and OpenRouter,
|
||||
alongside existing Databricks v2. Compatible endpoints require HTTPS without
|
||||
URL credentials/query/fragment and select auto/chat/responses. Anthropic and
|
||||
OpenRouter use their canonical endpoints and key contracts. Each API-key save
|
||||
validates the entire candidate before retaining an exact recovery reference,
|
||||
writes/read-backs the OS entry, then commits public settings by CAS. Secrets
|
||||
stay in hidden input/private helper IPC/closed runtime environment, not argv or
|
||||
public settings. No plaintext fallback. Failed persistence retains references.
|
||||
- Buzz Agent accepts all five providers. Pi remains OpenAI/Databricks v2 only;
|
||||
Codex remains OpenAI only with its own CLI/effort restrictions. Catalog validation
|
||||
repeats these restrictions, independent of picker filtering. The stale Pi
|
||||
diagnostic row now uses buzz-pi-acp and salman1993/buzz-pi-acp#86b201e.
|
||||
- OpenAI models use Desktop text prefixes/exclusions, creation-descending ordering,
|
||||
ID tie-break, dated alias suppression and display names. Compatible lists are
|
||||
unfiltered. Anthropic uses x-api-key/anthropic-version, display_name and after_id;
|
||||
OpenRouter selects tools-capable entries. Fetches forbid redirects, bound each
|
||||
response to 1 MiB, total rows to 10,000, returned rows to 1,000 and pagination to
|
||||
20 pages with repeated/missing-cursor refusal. UI labels never replace exact IDs.
|
||||
Custom model entry remains available after listing failure.
|
||||
- Unknown/custom models and UC FQNs retain authored provider fallback efforts.
|
||||
Known model constraints and Anthropic omit-fields remain; Pi projects only its
|
||||
supported effort vocabulary. Save and actual launch both validate. Saving a
|
||||
runtime/selected-next still does not alter an active run.
|
||||
|
||||
### New evidence
|
||||
|
||||
`/Users/loganj/.buzz/artifacts/beehive-parity-b5cd2aa0/`
|
||||
|
||||
- `DESKTOP_PARITY_DELTA.md`: durable reconciliation of public source report
|
||||
`62399bb5…89a24`, with canonical consumer pointers and resolved scope.
|
||||
- `focused-candidate.log`: **63 passed / 0 failed**. Provider-specific model APIs,
|
||||
bounded pagination, labels, compatibility and effort validation; actual host
|
||||
Save preserves the prior run, then Stop/Start produces real owned synthetic
|
||||
requests for Anthropic/compatible/OpenRouter. Unknown compatible/router models
|
||||
deliver high effort; Anthropic preserves omitted effort. Existing Pi/Codex,
|
||||
registration-first continuation, controller and discovery checks remain green.
|
||||
These external wrappers assert the production provider environment then call a
|
||||
loopback fixture; they are not actual vendor adapters or live API attestations.
|
||||
- `typecheck-final.log`: strict Node 24.15.0 TypeScript passed.
|
||||
- `renderer-candidate.log`: Bun 1.4.2 renderer **4/4**.
|
||||
- `full-candidate.log`: one final default-concurrent package run, **244 passed /
|
||||
2 failed / 19 skipped**, 265 tests, natural 45.719s. **Not green.** Broker `ok`
|
||||
conversation timeout recurred. Admission wrong-authority timing was 3210ms >
|
||||
2500ms; retained records show wrong-authority rejection plus accepted Start and
|
||||
subsequent authorized Stop, with storage-phase and receive-order trace. Those
|
||||
records establish a timing assertion failure, not a wrong-authority execution
|
||||
failure; underlying latency cause is not established. No timeout/assertion
|
||||
relaxation, serial masking or retry-to-green. Native 700-test history reused.
|
||||
- `pty-candidate.log`, `walkthrough.json`, numbered `.ansi` and `.txt`: actual
|
||||
isolated 100×30 PTY adds all three providers with hidden synthetic keys, opens
|
||||
selected-provider Models, saves OpenRouter/custom/high/environment runtime,
|
||||
registers the initially unassigned agent with that runtime, observes actual
|
||||
unknown-custom-model running, awaits Stop and exits 0. Synthetic API key absent
|
||||
from terminal bytes. Frames reconstruct PTY output; not screenshots.
|
||||
- Development failures retained: first provider fixture incorrectly assigned high
|
||||
effort to an Anthropic omit-fields model and reused OpenAI-only ACP validation;
|
||||
corrected fixture now omits effort and checks the generic saved-provider seam.
|
||||
First unit constraint assumed gpt-4.1 lacked fallback high (manifest says otherwise);
|
||||
replaced with explicit known gpt-5 xhigh rejection. `provider-first.log` and
|
||||
`anthropic-first.log` reached outer execution timeouts after failures; they are
|
||||
not successful runs. First typechecks retain headers-union and duplicate-test-
|
||||
ternary diagnostics. `renderer.log` is an invocation/filter error, superseded by
|
||||
the explicit-path renderer run. First PTY retained a noncanonical /tmp executable
|
||||
alias; production correctly refused Start. Loader now resolves the synthetic
|
||||
executable canonically. `pty-failed/` retains those frames; no production path
|
||||
check was weakened.
|
||||
|
||||
### Reviewer delta
|
||||
|
||||
`sh beehive/tools/ux-walkthrough.sh --help` documents the new provider/runtime steps.
|
||||
Run without arguments for a fresh explicit installed-loader and synthetic-credential
|
||||
HOME. Use only `synthetic-provider-key` and the printed synthetic nsec. Host service
|
||||
buttons remain fenced. Provider API tests exercise bounded fetches separately; the
|
||||
PTY uses synthetic model responses. `pty-walkthrough.py` in the new artifact folder
|
||||
reproduces the final provider/custom-runtime Start/Stop delta. Prior independent-
|
||||
root Move PTY is reused, not relabeled as a new provider Move test.
|
||||
|
||||
No production OS-store/provider/browser/relay/harness/service operations, native
|
||||
rebuild, repository-wide CI, push, export or install were performed. Independent UX
|
||||
review remains Larry's acceptance step.
|
||||
|
||||
@@ -17,7 +17,7 @@ export function validateBuzzProvider(value: BuzzProvider): void {
|
||||
for (const model of value.models) validateRuntimeEffort(value.provider, model, value.effort);
|
||||
if (value.provider === 'databricks_v2' ? !['token', 'external-oauth'].includes(value.auth ?? '') : value.auth !== undefined) throw Error('Unsupported provider authentication contract');
|
||||
if (value.credential) {
|
||||
if (!['openai-compat','databricks_v2'].includes(value.provider) || value.apiKeyFile !== undefined || (value.provider === 'databricks_v2' ? value.auth !== 'token' : value.auth !== undefined) || value.credential.service !== 'beehive' || !/^provider:[0-9a-f-]{36}$/.test(value.credential.account)) throw Error('Invalid OS provider binding');
|
||||
if (value.apiKeyFile !== undefined || (value.provider === 'databricks_v2' ? value.auth !== 'token' : value.auth !== undefined) || value.credential.service !== 'beehive' || !/^provider:[0-9a-f-]{36}$/.test(value.credential.account)) throw Error('Invalid OS provider binding');
|
||||
} else if (value.auth === 'external-oauth' ? value.apiKeyFile !== undefined : typeof value.apiKeyFile !== 'string' || !isAbsolute(value.apiKeyFile)) throw Error('Provider requires owner-only key file; external OAuth forbids token fallback');
|
||||
let url: URL;
|
||||
try { url = new URL(value.baseUrl); } catch { throw Error('Invalid local provider endpoint'); }
|
||||
|
||||
@@ -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' && ['buzz-agent','pi'].includes(row.id) ? ['openai', 'databricks_v2'] : state === 'available' && row.id === 'codex' ? ['openai'] : [];
|
||||
const providers = state === 'available' && ['buzz-agent','pi'].includes(row.id) ? row.id === 'buzz-agent' ? ['openai', 'anthropic', 'openai-compat', 'openrouter', 'databricks_v2'] : ['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 });
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export type ManagerResult =
|
||||
export type ManagerRequest = { id: number; action: string; values?: Record<string, string>; target?: string; revision?: number };
|
||||
export type ManagerItem = { startTarget?: string; startRevision?: number; target?: string; id: string; label: string; detail: string; evidence?: string; disabled?: Record<string, string> };
|
||||
export type MoveDestination = { target: string; revision: number; label: string; reason?: string };
|
||||
export type ManagerSnapshot = { managementRelay?: 'connected' | 'disconnected'; diagnostics?: ManagerItem[]; local: ManagerItem[]; agents: (ManagerItem & { revision: number; configurations: string[]; destinations?: MoveDestination[] })[]; 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 };
|
||||
export type ManagerSnapshot = { managementRelay?: 'connected' | 'disconnected'; diagnostics?: ManagerItem[]; local: ManagerItem[]; agents: (ManagerItem & { revision: number; configurations: string[]; destinations?: MoveDestination[] })[]; routing?: { owner: string; relay: string }; owner?: string; status: string; settings?: Settings; service?: ServiceStatus; hostRelay?: string; relayName?: string; profilePreview?: RegisteredAgent; models?: string[]; modelLabels?: Record<string,string>; runtimeExecutable?: string; harnesses?: DetectedHarness[]; databricksHost?: string };
|
||||
const short = (s: string) => s.length > 22 ? `${s.slice(0,8)}…${s.slice(-6)}` : s;
|
||||
|
||||
/** Final plain display text thrown by this boundary; never rewrapped or retranslated. */
|
||||
@@ -154,6 +154,7 @@ export class ManagerController {
|
||||
private probing = false;
|
||||
private profilePreview?: RegisteredAgent;
|
||||
private models?: string[];
|
||||
private modelLabels?: Record<string,string>;
|
||||
private runtimeExecutable?: string;
|
||||
private harnesses?: DetectedHarness[];
|
||||
private databricksHost?: string;
|
||||
@@ -236,7 +237,7 @@ A Stop result is not a recent host report that confirms the agent is stopped.` }
|
||||
try { settings = readSettings(this.hostDirectory); if (existsSync(join(this.hostDirectory,'host-identity.json'))) hostRelay = readHostIdentityPublic(this.hostDirectory).pairing.relay; } catch { /* Existing error row remains actionable; never reset settings. */ }
|
||||
const footerRelay = hostRelay ?? routing?.relay;
|
||||
this.observeRelayName(footerRelay);
|
||||
return { managementRelay:this.owner ? this.client?.connected ? 'connected' : 'disconnected' : undefined, settings, service: this.service, hostRelay, relayName: this.relayName && this.relayName.relay === footerRelay ? this.relayName.name : undefined, profilePreview: this.profilePreview, models: this.models, runtimeExecutable: this.runtimeExecutable, harnesses: this.harnesses, databricksHost: this.databricksHost, local, agents, diagnostics, routing: routing ? { owner: routing.owner, relay: routing.relay } : undefined, owner: this.owner, status: this.status + (this.owner ? `\n${this.directoryState}` : '') };
|
||||
return { managementRelay:this.owner ? this.client?.connected ? 'connected' : 'disconnected' : undefined, settings, service: this.service, hostRelay, relayName: this.relayName && this.relayName.relay === footerRelay ? this.relayName.name : undefined, profilePreview: this.profilePreview, models: this.models, modelLabels:this.modelLabels, runtimeExecutable: this.runtimeExecutable, harnesses: this.harnesses, databricksHost: this.databricksHost, local, agents, diagnostics, routing: routing ? { owner: routing.owner, relay: routing.relay } : undefined, owner: this.owner, status: this.status + (this.owner ? `\n${this.directoryState}` : '') };
|
||||
}
|
||||
|
||||
private moveDestinations(source: Message): MoveDestination[] {
|
||||
@@ -403,17 +404,17 @@ A Stop result is not a recent host report that confirms the agent is stopped.` }
|
||||
this.databricksHost = process.env.DATABRICKS_HOST ?? ''; this.status = 'Provider credentials stay in Beehive’s OS store.';
|
||||
} else if (request.action === 'add-databricks') {
|
||||
await addDatabricks(this.hostDirectory,v.name,v.endpoint,abort.signal); check(); this.status = 'Databricks signed in and saved. No agent was started.';
|
||||
} else if (request.action === 'add-openai') {
|
||||
await this.credential({ action: 'add-openai', directory: this.hostDirectory, name: v.name, secret: v.secret },abort.signal); check(); this.status = 'Provider saved. No agent was started.';
|
||||
} else if (request.action === 'add-openai' || request.action === 'add-provider') {
|
||||
await this.credential({ action: request.action, directory: this.hostDirectory, name: v.name, secret: v.secret, type:v.type, endpoint:v.endpoint, wire:v.wire },abort.signal); check(); this.status = 'Provider saved. No agent was started.';
|
||||
} else if (request.action === 'runtime-form') {
|
||||
this.runtimeExecutable = undefined; this.harnesses = undefined; this.models = undefined;
|
||||
this.runtimeExecutable = undefined; this.harnesses = undefined; this.models = undefined; this.modelLabels = undefined;
|
||||
const harnesses = await discoverHarnesses(abort.signal); check(); this.harnesses = harnesses;
|
||||
this.runtimeExecutable = this.harnesses.find(h => h.id === 'buzz-agent' && h.providers.length)?.executable; this.models = undefined;
|
||||
this.runtimeExecutable = this.harnesses.find(h => h.id === 'buzz-agent' && h.providers.length)?.executable; this.models = undefined; this.modelLabels = undefined;
|
||||
this.status = this.harnesses.map(h => `${h.label}: ${h.reason}`).join(' · ');
|
||||
} else if (request.action === 'models') {
|
||||
this.models = undefined;
|
||||
this.models = undefined; this.modelLabels = undefined;
|
||||
const provider = readSettings(this.hostDirectory).providers.find(p => p.id === v.provider);
|
||||
const result = provider?.type === 'databricks_v2' ? await databricksNative({ action: 'models',host:provider.endpoint,key:provider.key },abort.signal) : await this.credential({ action: 'models', directory: this.hostDirectory, provider: v.provider },abort.signal); check(); this.models = result.models; this.status = 'Model list loaded. Custom model is also available.';
|
||||
const result = provider?.type === 'databricks_v2' ? await databricksNative({ action: 'models',host:provider.endpoint,key:provider.key },abort.signal) : await this.credential({ action: 'models', directory: this.hostDirectory, provider: v.provider },abort.signal); check(); this.models = result.models; this.modelLabels = result.modelLabels; this.status = 'Model list loaded. Custom model is also available.';
|
||||
} else if (request.action === 'add-runtime') {
|
||||
const selected = this.harnesses?.find(h => h.id === (v.harness ?? 'buzz-agent'));
|
||||
const previous = readSettings(this.hostDirectory);
|
||||
|
||||
@@ -6,9 +6,9 @@ import { object } from './protocol.ts';
|
||||
import { readHostIdentity, bootstrapHostIdentity } from './host-identity.ts';
|
||||
import { createCredential, credentialReference, readCredential, systemCredentials } from './credential-store.ts';
|
||||
import { publicKey } from './protocol.ts';
|
||||
import { registerAgent, agentNsec, addOpenAI, providerCredentials } from './settings-credentials.ts';
|
||||
import { registerAgent, agentNsec, addOpenAI, addProvider, providerCredentials } from './settings-credentials.ts';
|
||||
import { readSettings } from './settings.ts';
|
||||
import { openAIModels } from './settings-models.ts';
|
||||
import { providerModelOptions } from './settings-models.ts';
|
||||
|
||||
// Node-only, bounded by the controller. Secrets use private IPC, never argv/output.
|
||||
process.once('message', async (input: any) => {
|
||||
@@ -30,6 +30,9 @@ process.once('message', async (input: any) => {
|
||||
} else if (input.action === 'add-openai') {
|
||||
addOpenAI(input.directory,input.name,input.secret,providerCredentials());
|
||||
process.send?.({ ok: true });
|
||||
} else if (input.action === 'add-provider') {
|
||||
addProvider(input.directory,input.name,input.secret,providerCredentials(),input.type,input.endpoint,input.wire);
|
||||
process.send?.({ok:true});
|
||||
} else if (input.action === 'provider-read') {
|
||||
const secret = providerCredentials().read(input.key);
|
||||
if (!secret) throw Error('Missing provider credential');
|
||||
@@ -39,8 +42,8 @@ process.once('message', async (input: any) => {
|
||||
if (!provider) throw Error('Missing provider');
|
||||
const secret = providerCredentials().read(provider.key);
|
||||
if (!secret) throw Error('Missing provider credential');
|
||||
const models = await openAIModels(secret, AbortSignal.timeout(7000));
|
||||
process.send?.({ ok: true, models });
|
||||
const options = await providerModelOptions(provider, secret, AbortSignal.timeout(7000));
|
||||
process.send?.({ ok: true, models:options.map(v=>v.id), modelLabels:Object.fromEntries(options.map(v=>[v.id,v.name])) });
|
||||
} else if (input.action === 'signin') {
|
||||
const ref = credentialReference('owner', input.owner);
|
||||
if (input.secret !== undefined) {
|
||||
|
||||
@@ -83,16 +83,19 @@ function render() {
|
||||
let actions: ManagerAction[] = scope !== 1 ? [
|
||||
{ label: 'Add provider', run: async () => {
|
||||
await request('provider-form');
|
||||
const type = await screen.choose('Add provider',['OpenAI','Databricks v2']); if (!type) return;
|
||||
const type = await screen.choose('Add provider',['OpenAI','Anthropic','OpenAI-compatible','OpenRouter','Databricks v2']); if (!type) return;
|
||||
if (type === 'Databricks v2') {
|
||||
const endpoint = await screen.input('Databricks workspace URL',snapshot.databricksHost ?? ''); if (!endpoint) return;
|
||||
const name = await screen.input('Provider name','Databricks'); if (!name) return;
|
||||
if (await screen.confirm(`Sign in to ${name} in your browser? Tokens stay in Beehive’s OS store. No agent will start.`)) await request('add-databricks',{ name,endpoint });
|
||||
return;
|
||||
}
|
||||
const name = await screen.input('Provider name','OpenAI'); if (!name) return;
|
||||
let secret = await screen.input('OpenAI API key. Hidden. Saved in Beehive’s OS credential store.','',true); if (!secret) return;
|
||||
if (await screen.confirm(`Save provider ${name}? No agent will start.`)) await request('add-openai',{ name, secret });
|
||||
const providerType = type === 'Anthropic' ? 'anthropic' : type === 'OpenRouter' ? 'openrouter' : type === 'OpenAI-compatible' ? 'openai-compat' : 'openai';
|
||||
const endpoint = providerType === 'openai-compat' ? await screen.input('HTTPS endpoint') : providerType === 'anthropic' ? 'https://api.anthropic.com' : providerType === 'openrouter' ? 'https://openrouter.ai/api/v1' : 'https://api.openai.com/v1'; if (!endpoint) return;
|
||||
const wire = providerType === 'openai-compat' ? await screen.choose('API',['auto','chat','responses']) : undefined; if (providerType === 'openai-compat' && !wire) return;
|
||||
const name = await screen.input('Provider name',type); if (!name) return;
|
||||
let secret = await screen.input(`${type} API key · Hidden · OS credential store`,'',true); if (!secret) return;
|
||||
if (await screen.confirm(`Save provider ${name}? No agent will start.`)) await request('add-provider',{ name, secret, type:providerType, endpoint, ...(wire ? {wire} : {}) });
|
||||
secret = '';
|
||||
} },
|
||||
{ label: 'Add runtime', run: () => runtimeForm(screen, () => snapshot, request) },
|
||||
@@ -157,7 +160,7 @@ The source stops before destination launch. Workspace, session and credentials s
|
||||
{ label: 'Models', disabled: rows.some(r => r.id === selected) ? undefined : 'Select provider', run: async () => {
|
||||
const provider = selected;
|
||||
await request('models', { provider });
|
||||
if (snapshot.models) await screen.choose('Models', snapshot.models);
|
||||
if (snapshot.models) await screen.choose('Models', snapshot.models.map(id=>snapshot.modelLabels?.[id] && snapshot.modelLabels[id] !== id ? `${snapshot.modelLabels[id]} · ${id}` : id));
|
||||
} },
|
||||
];
|
||||
if (scope === 1 && !snapshot.owner) actions.push({ label: 'New private instructions draft…', run: async () => { const name = await screen.input('Private instructions · 1 of 2\nDraft name', '', false, false, v => v.trim() ? '' : 'Enter a draft name.'); if (name === undefined) return; const instructions = await screen.input(`Private instructions · 2 of 2\nDraft: ${name}\nSaved only on this computer. Not published or used by an agent. Do not enter passwords or keys.`, '', false, true, v => v.trim() ? '' : 'Enter instructions.'); if (instructions !== undefined) await request('draft', { name, instructions }); } });
|
||||
|
||||
@@ -7,11 +7,11 @@ export const presets = [
|
||||
{
|
||||
"id": "pi",
|
||||
"label": "Pi",
|
||||
"command": "pi-acp",
|
||||
"command": "buzz-pi-acp",
|
||||
"args": [],
|
||||
"env": {},
|
||||
"installInstructionsUrl": "https://github.com/svkozak/pi-acp",
|
||||
"installHint": "Install Pi with npm install -g --ignore-scripts @earendil-works/pi-coding-agent. Install the Pi ACP adapter with npm install -g pi-acp.",
|
||||
"installInstructionsUrl": "https://github.com/salman1993/buzz-pi-acp",
|
||||
"installHint": "Install Pi with npm install -g --ignore-scripts @earendil-works/pi-coding-agent. Requires Node.js 22 or newer. Install the Pi ACP adapter with npm install -g --install-links=true git+https://github.com/salman1993/buzz-pi-acp.git#86b201e. Make sure buzz-pi-acp is on PATH, then restart Buzz.",
|
||||
"underlyingCli": "pi",
|
||||
"underlyingCliInstallInstructionsUrl": "https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent"
|
||||
},
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
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. */
|
||||
* Desktop modelCapabilities.ts exact/family precedence. Provider fallbacks are authored capabilities, including custom names and UC FQNs. */
|
||||
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))) {
|
||||
@@ -17,7 +16,7 @@ export function runtimeCapabilities(provider: string, model: string) {
|
||||
});
|
||||
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' };
|
||||
return { ...manifest.provider_fallbacks.databricks_v2.concrete_unknown, 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);
|
||||
@@ -40,8 +39,8 @@ export function runtimeCapabilities(provider: string, model: string) {
|
||||
}).sort((a, b) => b.length - a.length || (a.rule.id < b.rule.id ? -1 : 1));
|
||||
const rule = matches[0]?.rule;
|
||||
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;
|
||||
const pair = new Map(Object.entries(manifest.provider_fallbacks)).get(canon) ?? manifest.provider_fallbacks._default;
|
||||
return model.trim() ? pair.concrete_unknown : pair.blank;
|
||||
}
|
||||
|
||||
export function runtimeEfforts(provider: string, model: string): readonly string[] {
|
||||
|
||||
@@ -24,8 +24,10 @@ export async function runtimeForm(screen: {
|
||||
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 labelsById = snapshot().modelLabels ?? {};
|
||||
const modelLabels = (models ?? []).map(id=>labelsById[id] && labelsById[id] !== id ? `${labelsById[id]} · ${id}` : id);
|
||||
const choiceModel = await screen.choose(models ? 'Model' : 'Model listing unavailable · Custom model allowed', [...modelLabels, 'Custom model']); if (!choiceModel) return;
|
||||
const model = choiceModel === 'Custom model' ? await screen.input('Exact model ID. Custom does not verify provider access.') : models?.[modelLabels.indexOf(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;
|
||||
|
||||
@@ -6,7 +6,7 @@ import { loadNativeEntry, type NativeEntry } from './native-credentials.ts';
|
||||
import { type CredentialBackend, credentialReference, createCredential, readCredential } from './credential-store.ts';
|
||||
import { nip19 } from 'nostr-tools';
|
||||
import { publicKey } from './protocol.ts';
|
||||
import { readSettings, saveSettings, settingsId, settingsLock, type ProviderReference, type RegisteredAgent } from './settings.ts';
|
||||
import { readSettings, saveSettings, settingsId, settingsLock, type ProviderReference, type RegisteredAgent, type SavedProvider, validateSettings } from './settings.ts';
|
||||
|
||||
/** Decode hidden nsec input on Node, never in the renderer's display state. */
|
||||
export function agentNsec(input: string): string {
|
||||
@@ -46,13 +46,18 @@ export function providerCredentials(load = loadNativeEntry): ProviderCredentials
|
||||
}
|
||||
/** Read-back precedes public persistence. Failed commit retains the exact key entry. */
|
||||
export function addOpenAI(directory: string, name: string, secret: string, backend: ProviderCredentials) {
|
||||
return addProvider(directory,name,secret,backend,'openai','https://api.openai.com/v1');
|
||||
}
|
||||
/** Verified immutable OS entry for each supported API-key provider. */
|
||||
export function addProvider(directory: string, name: string, secret: string, backend: ProviderCredentials, type: SavedProvider['type'], endpoint: string, wire?: SavedProvider['wire']) {
|
||||
if (!name || name.length > 128 || /[\x00-\x1f\x7f]/.test(name)) throw Error('Enter a provider name');
|
||||
if (!secret || secret.length > 16384 || /\s/.test(secret)) throw Error('Enter an API key');
|
||||
const previous = readSettings(directory), id = settingsId();
|
||||
const key: ProviderReference = { service: 'beehive', account: `provider:${id}` };
|
||||
const candidate = validateSettings({ ...previous, providers: [...previous.providers, { id, name, type, endpoint, key, ...(wire ? {wire} : {}) }] });
|
||||
settingsLock(directory, () => { retainCredentialAttempt(directory,key); backend.create(key, secret); });
|
||||
if (backend.read(key) !== secret) throw Error('Provider credential verification failed');
|
||||
return saveSettings(directory, { ...previous, providers: [...previous.providers, { id, name, type: 'openai', endpoint: 'https://api.openai.com/v1', key }] }, previous.revision);
|
||||
return saveSettings(directory, candidate, previous.revision);
|
||||
}
|
||||
|
||||
/** Public recovery references precede OS writes. Cancellation may leave a stored
|
||||
|
||||
@@ -9,18 +9,51 @@ export function detectBuzzAgent(path = process.env.PATH ?? ''): string | undefin
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
/** Authenticated OpenAI model list. Empty and error stay distinct; custom is independent. */
|
||||
export async function openAIModels(secret: string, signal: AbortSignal, fetcher: typeof fetch = fetch): Promise<string[]> {
|
||||
const response = await fetcher('https://api.openai.com/v1/models',{ headers: { Authorization: `Bearer ${secret}` }, signal, redirect: 'error' });
|
||||
if (!response.ok || !response.body) throw Error('OpenAI model list unavailable');
|
||||
const reader = response.body.getReader(); let bytes = 0; const chunks: Uint8Array[] = [];
|
||||
try { while (true) { const next = await reader.read(); if (next.done) break; bytes += next.value.length; if (bytes > 1048576) throw Error('OpenAI model list exceeds limit'); chunks.push(next.value); } }
|
||||
finally { await reader.cancel(); reader.releaseLock(); }
|
||||
try {
|
||||
const json = JSON.parse(Buffer.concat(chunks).toString());
|
||||
if (!Array.isArray(json.data) || json.data.length > 10000) throw Error();
|
||||
const ids: string[] = json.data.map((v: any) => v.id).filter((id: unknown): id is string => typeof id === 'string' && /^[a-zA-Z0-9_.:/-]{1,200}$/.test(id) && !/(embedding|whisper|tts|dall-e|image|audio|realtime|moderation)/i.test(id));
|
||||
const all = new Set(ids);
|
||||
return [...all].filter(id => { const base = id.replace(/-\d{4}-\d{2}-\d{2}$/,''); return base === id || !all.has(base); }).sort().slice(0,1000);
|
||||
} catch { throw Error('OpenAI model list is invalid'); }
|
||||
/** A provider label never substitutes for the exact launch ID. */
|
||||
export type ProviderModel = { id: string; name: string };
|
||||
const validId = (id: unknown): id is string => typeof id === 'string' && /^[a-zA-Z0-9_.:/-]{1,200}$/.test(id);
|
||||
function openAILabel(id: string) {
|
||||
const canonical = id.replace(/-\d{4}-\d{2}-\d{2}$/,'');
|
||||
const suffix = (s: string) => s.split('-').map(v => /^pro$/i.test(v) ? 'Pro' : /^(mini|nano)$/i.test(v) ? v.toLowerCase() : v).join(' ');
|
||||
return canonical.startsWith('chatgpt-') ? `ChatGPT ${suffix(canonical.slice(8))}` : canonical.startsWith('gpt-') ? `GPT-${suffix(canonical.slice(4))}` : canonical;
|
||||
}
|
||||
async function page(url: string, headers: Record<string,string>, signal: AbortSignal, fetcher: typeof fetch): Promise<any> {
|
||||
const response = await fetcher(url,{headers,signal,redirect:'error'});
|
||||
if (!response.ok || !response.body) throw Error('Provider model list unavailable');
|
||||
const reader = response.body.getReader(); let bytes = 0; const chunks: Uint8Array[] = [];
|
||||
try { while (true) { const next = await reader.read(); if (next.done) break; bytes += next.value.length; if (bytes > 1048576) throw Error('Provider model list exceeds limit'); chunks.push(next.value); } }
|
||||
finally { await reader.cancel(); reader.releaseLock(); }
|
||||
try { const json = JSON.parse(Buffer.concat(chunks).toString()); if (!Array.isArray(json.data) || json.data.length > 10000) throw Error(); return json; }
|
||||
catch { throw Error('Provider model list is invalid'); }
|
||||
}
|
||||
/** Desktop provider-specific discovery, bounded by response bytes, rows and pages.
|
||||
* Errors never include response bodies, endpoints or credentials. */
|
||||
export async function providerModelOptions(provider: {type:string;endpoint:string}, secret: string, signal: AbortSignal, fetcher: typeof fetch = fetch): Promise<ProviderModel[]> {
|
||||
try {
|
||||
if (!['openai','openai-compat','anthropic','openrouter'].includes(provider.type)) throw Error();
|
||||
const base = provider.endpoint.replace(/\/$/,'');
|
||||
const url = `${base}${provider.type === 'anthropic' && !base.endsWith('/v1') ? '/v1' : ''}/models`;
|
||||
const headers: Record<string,string> = provider.type === 'anthropic' ? {'x-api-key':secret,'anthropic-version':'2023-06-01'} : {Authorization:`Bearer ${secret}`};
|
||||
const rows: any[] = []; const cursors = new Set<string>(); let cursor: string | undefined;
|
||||
for (let i=0;i<20;i++) {
|
||||
const json = await page(url + (cursor ? `?after_id=${encodeURIComponent(cursor)}` : ''),headers,signal,fetcher);
|
||||
rows.push(...json.data); if (rows.length > 10000) throw Error();
|
||||
if (provider.type !== 'anthropic' || !json.has_more) break;
|
||||
if (!validId(json.last_id) || cursors.has(json.last_id) || i === 19) throw Error();
|
||||
cursor=json.last_id; cursors.add(cursor!);
|
||||
}
|
||||
const all = new Set(rows.map(v=>v.id)); const seen = new Set<string>();
|
||||
if (['openai','openai-compat'].includes(provider.type)) rows.sort((a,b)=>(Number.isFinite(b.created) ? b.created : -Infinity)-(Number.isFinite(a.created) ? a.created : -Infinity) || (String(a.id) < String(b.id) ? -1 : String(a.id) > String(b.id) ? 1 : 0));
|
||||
return rows.filter(v=>validId(v.id)).filter(v=>provider.type !== 'openai' || /^(gpt-|o|chatgpt-)/i.test(v.id) && !/(audio|dall-e|embedding|image|moderation|realtime|speech|transcribe|tts|whisper)/i.test(v.id) && (v.id.replace(/-\d{4}-\d{2}-\d{2}$/,'') === v.id || !all.has(v.id.replace(/-\d{4}-\d{2}-\d{2}$/,''))))
|
||||
.filter(v=>provider.type !== 'openrouter' || Array.isArray(v.supported_parameters) && v.supported_parameters.includes('tools'))
|
||||
.filter(v=>!seen.has(v.id) && !!seen.add(v.id)).slice(0,1000)
|
||||
.map(v=>({id:v.id,name:provider.type === 'anthropic' && typeof v.display_name === 'string' && v.display_name.length <= 200 && !/[\x00-\x1f\x7f]/.test(v.display_name) ? v.display_name : ['openai','openai-compat'].includes(provider.type) ? openAILabel(v.id) : v.id}));
|
||||
} catch { throw Error('Provider model list unavailable or invalid'); }
|
||||
}
|
||||
/** Compatibility ID-only API; UI receives labels separately. */
|
||||
export async function providerModels(provider: {type:string;endpoint:string}, secret: string, signal: AbortSignal, fetcher: typeof fetch = fetch) {
|
||||
return (await providerModelOptions(provider,secret,signal,fetcher)).map(v=>v.id);
|
||||
}
|
||||
export async function openAIModels(secret: string, signal: AbortSignal, fetcher: typeof fetch = fetch) {
|
||||
return providerModels({type:'openai',endpoint:'https://api.openai.com/v1'},secret,signal,fetcher);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ export function runtimeBindings(base: Setup, settings: Settings, agent = base.ag
|
||||
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 } : {}),
|
||||
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 } : {}) } }),
|
||||
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: provider.type === 'openai' ? 'openai-compat' : provider.type, baseUrl: provider.endpoint, credential: provider.key, ...(['openai','openai-compat'].includes(provider.type) ? {wire: provider.wire ?? 'auto'} : {}), models: [runtime.model], ...(runtime.effort ? { effort:runtime.effort } : {}) } }),
|
||||
})];
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { validateBuzzProvider } from './buzz-provider.ts';
|
||||
import { runtimeEnvironment } from './runtime-environment.ts';
|
||||
import { piEfforts } from './pi.ts';
|
||||
import { validateRuntimeEffort } from './runtime-effort.ts';
|
||||
@@ -14,7 +15,7 @@ import { credentialReference, type CredentialReference } from './credential-stor
|
||||
export type RegisteredAgent = { runtimeId?: string; publicKey: string; key: CredentialReference; profile?: { relay: string; name?: string; about?: string; picture?: string }; profileState: 'found' | 'none' | 'unavailable' };
|
||||
/** 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 SavedProvider = { id: string; name: string; type: 'openai' | 'anthropic' | 'openai-compat' | 'openrouter' | 'databricks_v2'; endpoint: string; wire?: 'auto' | 'chat' | 'responses'; key: ProviderReference };
|
||||
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');
|
||||
@@ -30,18 +31,21 @@ export function validateSettings(value: Settings): Settings {
|
||||
if (a.runtimeId !== undefined && !value.runtimes.some(r => r.id === a.runtimeId)) throw Error('Registered runtime missing');
|
||||
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 p of value.providers) {
|
||||
if (!label(p.name) || !label(p.id) || !['openai','anthropic','openai-compat','openrouter','databricks_v2'].includes(p.type) || (p.type === 'openai' && p.endpoint !== 'https://api.openai.com/v1') || (p.type === 'databricks_v2' && databricksHost(p.endpoint) !== p.endpoint) || (p.type !== 'openai-compat' && p.wire !== undefined)) throw Error('Invalid saved provider');
|
||||
validateBuzzProvider({provider:p.type === 'openai' ? 'openai-compat' : p.type, baseUrl:p.endpoint, credential:p.key, models:['validation-custom'], ...(p.type === 'databricks_v2' ? {auth:'token'} : {}), ...(['openai','openai-compat'].includes(p.type) ? {wire:p.wire ?? 'auto'} : {})});
|
||||
}
|
||||
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');
|
||||
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' ? !['openai','databricks_v2'].includes(value.providers.find(p => p.id === r.providerId)!.type) || !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.
|
||||
const fields = (o: object, allowed: string[]) => { if (Object.keys(o).some(k => !allowed.includes(k))) throw Error('Unexpected settings field'); };
|
||||
fields(value, ['version','revision','agents','providers','runtimes']);
|
||||
for (const a of value.agents) { fields(a, ['publicKey','key','profile','profileState','runtimeId']); 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 p of value.providers) { fields(p, ['id','name','type','endpoint','key','wire']); fields(p.key, ['service','account']); }
|
||||
for (const r of value.runtimes) fields(r, ['id','name','harness','executable','providerId','model','effort','cli','environment']);
|
||||
return structuredClone(value);
|
||||
}
|
||||
|
||||
@@ -14,13 +14,13 @@ for await (const line of createInterface({ input: process.stdin })) {
|
||||
if (mode === 'flood') { console.log('x'.repeat(1024 * 1024 + 1)); continue; }
|
||||
let result: unknown;
|
||||
if (m.method === 'initialize') result = { protocolVersion: m.params.protocolVersion, agentInfo: { name: 'buzz-agent' } };
|
||||
else if (m.method === 'session/new') result = { sessionId, models: { currentModelId: 'ignored-default', availableModels: mode === 'empty' ? [] : [{ modelId: mode === 'filtered' ? 'other-model' : ['openai','databricks-os'].includes(mode ?? '') ? process.env.BUZZ_AGENT_MODEL : 'databricks-claude-haiku-4-5', name: 'DO-NOT-RELAY-SECRET' }] } };
|
||||
else if (m.method === 'session/new') result = { sessionId, models: { currentModelId: 'ignored-default', availableModels: mode === 'empty' ? [] : [{ modelId: mode === 'filtered' ? 'other-model' : ['openai','databricks-os','saved-provider'].includes(mode ?? '') ? process.env.BUZZ_AGENT_MODEL : 'databricks-claude-haiku-4-5', name: 'DO-NOT-RELAY-SECRET' }] } };
|
||||
else if (m.method === 'session/set_model') {
|
||||
selected = m.params.modelId;
|
||||
if (mode === 'reject') { send({ jsonrpc: '2.0', id: m.id, error: { code: -1, message: 'DO-NOT-RELAY-SECRET' } }); continue; }
|
||||
result = { sessionId, modelId: mode === 'wrong-model' ? 'other-model' : selected };
|
||||
} else if (m.method === 'session/prompt') {
|
||||
if (selected !== process.env.BUZZ_AGENT_MODEL || m.params.sessionId !== sessionId || (mode !== 'databricks-os' && process.env.DATABRICKS_TOKEN) || process.env.BUZZ_PRIVATE_KEY || process.env.BUZZ_AGENT_PROVIDER !== (mode === 'openai' ? 'openai-compat' : 'databricks_v2')) process.exit(8);
|
||||
if (selected !== process.env.BUZZ_AGENT_MODEL || m.params.sessionId !== sessionId || (mode !== 'databricks-os' && process.env.DATABRICKS_TOKEN) || process.env.BUZZ_PRIVATE_KEY || mode !== 'saved-provider' && process.env.BUZZ_AGENT_PROVIDER !== (mode === 'openai' ? 'openai-compat' : 'databricks_v2')) process.exit(8);
|
||||
if (mode === 'databricks-os' && (!/^http:\/\/127\.0\.0\.1:\d+$/.test(process.env.DATABRICKS_HOST ?? '') || !/^[0-9a-f]{64}$/.test(process.env.DATABRICKS_TOKEN ?? '') || process.env.BEEHIVE_DATABRICKS_RUNTIME !== undefined)) process.exit(8);
|
||||
if (mode === 'openai' && process.env.OPENAI_COMPAT_API_KEY !== 'synthetic-provider-key') process.exit(8);
|
||||
if (mode === 'delayed') { writeFileSync('prompt-started', '1'); await delay(4000); }
|
||||
|
||||
@@ -22,14 +22,14 @@ test('ten pinned presets distinguish executable/CLI/adapter absence without laun
|
||||
const dir = realpathSync(mkdtempSync(join(tmpdir(), 'bh-presets-')));
|
||||
try {
|
||||
assert.deepEqual(presets.map(p => [p.id, p.command, [...p.args]]), [
|
||||
['pi','pi-acp',[]], ['devin','devin',['acp']], ['cursor','cursor-agent',['acp']], ['omp','omp',['acp']], ['grok','grok',['agent','--always-approve','stdio']], ['opencode','opencode',['acp']], ['kimi','kimi',['acp']], ['amp','amp-acp',[]], ['hermes','hermes-acp',[]], ['openclaw','openclaw',['acp']],
|
||||
['pi','buzz-pi-acp',[]], ['devin','devin',['acp']], ['cursor','cursor-agent',['acp']], ['omp','omp',['acp']], ['grok','grok',['agent','--always-approve','stdio']], ['opencode','opencode',['acp']], ['kimi','kimi',['acp']], ['amp','amp-acp',[]], ['hermes','hermes-acp',[]], ['openclaw','openclaw',['acp']],
|
||||
]);
|
||||
assert.deepEqual(presets.filter(p => 'underlyingCli' in p).map(p => p.id), ['pi', 'amp']);
|
||||
const pi = () => discoverPresets(dir).find(p => p.id === 'pi')!;
|
||||
assert.equal(pi().availability, 'not-installed');
|
||||
writeFileSync(join(dir, 'pi'), '#!/bin/sh\nexit 99\n', { mode: 0o700 });
|
||||
assert.equal(pi().availability, 'adapter-missing');
|
||||
writeFileSync(join(dir, 'pi-acp'), `#!/bin/sh\ntouch '${join(dir, 'unexpected-execution')}'\n`, { mode: 0o700 });
|
||||
writeFileSync(join(dir, 'buzz-pi-acp'), `#!/bin/sh\ntouch '${join(dir, 'unexpected-execution')}'\n`, { mode: 0o700 });
|
||||
assert.equal(pi().availability, 'available');
|
||||
rmSync(join(dir, 'pi')); assert.equal(pi().availability, 'cli-missing');
|
||||
for (const p of discoverPresets(dir)) { assert.equal(p.authentication, 'unverified'); assert.deepEqual(p.env, {}); assert.match(p.reason, /no exact actual-session model/); }
|
||||
|
||||
@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { finalizeEvent } from 'nostr-tools/pure';
|
||||
import { nsecEncode } from 'nostr-tools/nip19';
|
||||
import { registerAgent, agentNsec, addOpenAI } from '../src/settings-credentials.ts';
|
||||
import { registerAgent, agentNsec, addOpenAI, addProvider } from '../src/settings-credentials.ts';
|
||||
import { readSettings, saveSettings, settingsId } from '../src/settings.ts';
|
||||
import { profileFromEvents } from '../src/agent-profile.ts';
|
||||
import { isolatedFileCredentials } from './isolated-file-credentials.ts';
|
||||
@@ -75,28 +75,49 @@ test('OpenAI verifies store before public commit; listing is bounded and custom
|
||||
assert.throws(() => saveSettings(root,catalog,catalog.revision),/changed/);
|
||||
});
|
||||
|
||||
for (const providerType of ['openai','databricks_v2','codex-openai'] as const) test(`${providerType} saved runtime reaches host Save/new Start while the prior active run remains immutable`, async t => {
|
||||
for (const providerType of ['openai','databricks_v2','codex-openai','anthropic','openai-compat','openrouter'] 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 codex = providerType === 'codex-openai';
|
||||
const added = ['anthropic','openai-compat','openrouter'].includes(providerType);
|
||||
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,providerType !== 'databricks_v2' ? 'openai-compat' : 'databricks_v2'); if (providerType === 'databricks_v2') assert.equal((input as any).host,'https://fixture.example'); providerReads++; return {ok:true,secret:codex ? 'fixture-codex-private-key' : 'synthetic-provider-key'}; });
|
||||
const running = await host(root,'ws://127.0.0.1',undefined,transport,undefined,async (input,signal) => { signal.throwIfAborted(); assert.equal((input as any).provider,providerType === 'openai' || codex ? 'openai-compat' : providerType); if (providerType === 'databricks_v2') assert.equal((input as any).host,'https://fixture.example'); providerReads++; return {ok:true,secret:codex ? 'fixture-codex-private-key' : '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 (providerType !== 'databricks_v2') addOpenAI(root,'Fixture','synthetic',{ read: () => stored, create(_r,v) { stored = v; } });
|
||||
if (added) addProvider(root,'Fixture','synthetic',{read:()=>stored,create:(_r,v)=>{stored=v;}},providerType as 'anthropic'|'openai-compat'|'openrouter','https://fixture.example/v1',providerType === 'openai-compat' ? 'responses' : undefined);
|
||||
else if (providerType !== 'databricks_v2') addOpenAI(root,'Fixture','synthetic',{ read: () => stored, create(_r,v) { stored = v; } });
|
||||
else await addDatabricks(root,'Fixture','https://fixture.example',new AbortController().signal,async () => ({ok:true}));
|
||||
const prior = readSettings(root), id = settingsId();
|
||||
const model = codex ? 'custom-codex-model' : providerType === 'openai' ? 'gpt-5' : 'databricks-gpt-5-4';
|
||||
const model = codex ? 'custom-codex-model' : added ? (providerType === 'anthropic' ? 'claude-sonnet-4-5' : 'unknown-custom-model') : providerType === 'openai' ? 'gpt-5' : 'databricks-gpt-5-4';
|
||||
const executable = join(root,'fixture-buzz-agent');
|
||||
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 (added) {
|
||||
const keyName = providerType === 'anthropic' ? 'ANTHROPIC_API_KEY' : providerType === 'openrouter' ? 'OPENROUTER_API_KEY' : 'OPENAI_COMPAT_API_KEY';
|
||||
const server = createServer(async (req,res) => {
|
||||
assert.equal(req.method,'POST'); assert.equal(req.url, providerType === 'anthropic' ? '/v1/messages' : providerType === 'openrouter' ? '/v1/chat/completions' : '/v1/responses');
|
||||
assert.equal(req.headers.authorization,'Bearer synthetic-provider-key');
|
||||
let body=''; for await (const chunk of req) body+=chunk;
|
||||
assert.deepEqual(JSON.parse(body),{model,...(providerType === 'anthropic' ? {} : {effort:'high'})}); requests++; res.end('{}');
|
||||
});
|
||||
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}` + (providerType === 'anthropic' ? '/v1/messages' : providerType === 'openrouter' ? '/v1/chat/completions' : '/v1/responses');
|
||||
writeFileSync(executable,`#!${process.execPath}
|
||||
if (process.env.BUZZ_AGENT_PROVIDER !== ${JSON.stringify(providerType)} || process.env.REVIEW_MODE !== 'synthetic') throw Error('Wrong binding');
|
||||
const response = await fetch(${JSON.stringify(endpoint)},{method:'POST',headers:{authorization:'Bearer '+process.env[${JSON.stringify(keyName)}]},body:JSON.stringify({model:process.env.BUZZ_AGENT_MODEL,effort:process.env.BUZZ_AGENT_THINKING_EFFORT})});
|
||||
if (!response.ok) throw Error('Synthetic provider refused');
|
||||
process.argv[2]='saved-provider';
|
||||
await import(${JSON.stringify(new URL('./acp-fixture.ts',import.meta.url).href)});
|
||||
`,{mode:0o700});
|
||||
}
|
||||
if (codex) {
|
||||
const server = createServer(async (req,res) => {
|
||||
assert.equal(req.method,'POST'); assert.equal(req.url,'/v1/responses');
|
||||
@@ -116,7 +137,7 @@ 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', 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);
|
||||
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)} : providerType === 'anthropic' ? {} : {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);
|
||||
@@ -137,6 +158,7 @@ await import(${JSON.stringify(new URL('./conversation-harness-fixture.ts',import
|
||||
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);
|
||||
if (added) { assert.equal(requests,1); assert.ok(!readFileSync(join(root,'journal.json'),'utf8').includes('synthetic-provider-key')); }
|
||||
if (codex) { assert.equal(requests,1); assert.ok(!readFileSync(join(root,'journal.json'),'utf8').includes('fixture-codex-private-key')); }
|
||||
assert.ok(!JSON.stringify(reports).includes('synthetic-provider-key'));
|
||||
assert.ok(!readFileSync(join(root,'journal.json'),'utf8').includes('synthetic-provider-key'));
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { registerAgent, agentNsec, addOpenAI } from '../src/settings-credentials.ts';
|
||||
import { registerAgent, agentNsec, addOpenAI, addProvider } from '../src/settings-credentials.ts';
|
||||
import type { CredentialReference } from '../src/credential-store.ts';
|
||||
import { bootstrapHostIdentity } from '../src/host-identity.ts';
|
||||
import { isolatedFileCredentials } from './isolated-file-credentials.ts';
|
||||
@@ -16,6 +16,7 @@ export async function fixtureCredential(input: any, signal: AbortSignal) {
|
||||
}
|
||||
if (input.action === 'register-agent') { registerAgent(input.directory,agentNsec(input.secret),backend,input.profile,input.runtimeId,input.expectedRevision); return { ok: true }; }
|
||||
if (input.action === 'add-openai') { addOpenAI(input.directory,input.name,input.secret,{ read: r => backend.read(r as unknown as CredentialReference), create: (r,s) => backend.create(r as unknown as CredentialReference,s) }); return { ok: true }; }
|
||||
if (input.action === 'add-provider') { addProvider(input.directory,input.name,input.secret,{read:r=>backend.read(r as unknown as CredentialReference),create:(r,s)=>backend.create(r as unknown as CredentialReference,s)},input.type,input.endpoint,input.wire); return {ok:true}; }
|
||||
if (input.action === 'models') return { ok: true, models: ['fixture-model'] };
|
||||
if (input.action !== 'signin') throw Error('Unsupported installed fixture action');
|
||||
if (input.secret) {
|
||||
|
||||
@@ -6,13 +6,13 @@ const fixture = fileURLToPath(new URL('./manager-installed-fixture.ts', import.m
|
||||
// Explicit test-only seam. No production environment switch or OS-store fallback.
|
||||
registerHooks({ load(url, context, next) {
|
||||
const result = next(url, context);
|
||||
if (url.endsWith('/src/harness-discovery.ts')) return { ...result, source: "export async function discoverHarnesses() { return [{id:'buzz-agent',label:'Buzz Agent',executable:process.execPath,state:'available',providers:['openai','databricks_v2'],reason:'Synthetic only'}]; }" };
|
||||
if (url.endsWith('/src/harness-discovery.ts')) return { ...result, source: "import {realpathSync} from 'node:fs'; export async function discoverHarnesses() { return [{id:'buzz-agent',label:'Buzz Agent',executable:process.env.BEEHIVE_TEST_REGISTRATION_WALKTHROUGH === '1' ? realpathSync(process.env.HOME + '/synthetic-acp') : process.execPath,state:'available',providers:['openai','anthropic','openai-compat','openrouter','databricks_v2'],reason:'Synthetic only'}]; }" };
|
||||
if (url.endsWith('/src/databricks-runtime.ts') || url.endsWith('/src/databricks-runtime-child.ts')) return { ...result, source: "throw Error('Runtime auth disabled by installed fixture');" };
|
||||
if (url.endsWith('/src/databricks.ts')) return { ...result, source: "export function databricksHost(v) { return new URL(v).origin; } export async function databricksNative() { throw Error('Native OAuth disabled by installed fixture'); } export async function addDatabricks() { throw Error('Native OAuth disabled by installed fixture'); }" };
|
||||
if (!walkthrough && url.endsWith('/src/relay-directory.ts')) return { ...result, source: "export async function fetchRelayDirectory() { throw Error('Directory disabled by installed fixture'); }" };
|
||||
if (url.endsWith('/src/agent-profile.ts')) return { ...result, source: "export async function fetchAgentProfile() { return {profileState:'none'}; }" };
|
||||
if (url.endsWith('/src/host-service.ts')) return { ...result, source: "export async function serviceStatus() { return {state:'stopped',relay:'disconnected'}; } export async function startService() { throw Error('Service start disabled by installed fixture'); } export async function stopService() { throw Error('Service stop disabled by installed fixture'); }" };
|
||||
if (url.endsWith('/src/settings-models.ts')) return { ...result, source: "export function detectBuzzAgent() { return process.execPath; } export async function openAIModels() { throw Error('Direct provider access disabled by installed fixture'); }" };
|
||||
if (url.endsWith('/src/settings-models.ts')) return { ...result, source: "export function detectBuzzAgent() { return process.execPath; } export async function providerModels() { throw Error('Direct provider access disabled by installed fixture'); } export async function providerModelOptions() { throw Error('Direct provider access disabled by installed fixture'); } export async function openAIModels() { throw Error('Direct provider access disabled by installed fixture'); }" };
|
||||
if (url.endsWith('/src/relay-name.ts')) return { ...result, source: "export function relayNameFromDocument() { return undefined; } export async function fetchRelayName() { return undefined; }" };
|
||||
if (!url.endsWith('/src/manager-entry.ts')) return result;
|
||||
const source = String(result.source);
|
||||
|
||||
@@ -32,9 +32,9 @@ test('Pi model routes use the shared capability owner; UC tenant components neve
|
||||
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);
|
||||
assert.ok(piEfforts('databricks_v2','catalog.schema.gpt-6-service').includes('high'));
|
||||
assert.ok(piEfforts('openai','unknown-custom').includes('high'));
|
||||
assert.ok(runtimeCapabilities('databricks_v2','gpt-5.schema.unknown')!.supported_efforts.includes('high'));
|
||||
});
|
||||
|
||||
test('actual runtime form offers Pi both providers, retains custom names, submits only supported model effort',async()=>{
|
||||
|
||||
40
beehive/test/provider-parity.test.ts
Normal file
40
beehive/test/provider-parity.test.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { providerModelOptions } from '../src/settings-models.ts';
|
||||
import { runtimeEfforts } from '../src/runtime-effort.ts';
|
||||
import { validateSettings, type Settings } from '../src/settings.ts';
|
||||
import { presets } from '../src/presets.ts';
|
||||
const key = {service:'beehive' as const,account:'provider:11111111-1111-1111-1111-111111111111'};
|
||||
const signal = () => AbortSignal.timeout(1000);
|
||||
test('Desktop OpenAI prefix/exclusion/creation ordering/alias labels differ from compatible endpoints',async()=>{
|
||||
const data=[{id:'gpt-5-2026-01-01',created:10},{id:'gpt-5',created:1},{id:'o3',created:9},{id:'other-model',created:7},{id:'gpt-transcribe',created:12},{id:'chatgpt-4o',created:2},{id:'custom',created:20},{id:'gpt-4.1-2025-04-14',created:3}];
|
||||
const fetcher: typeof fetch = async(url,options)=>{assert.equal(url,'https://fixture.example/v1/models');assert.equal((options?.headers as any).Authorization,'Bearer synthetic');assert.equal(options?.redirect,'error');return new Response(JSON.stringify({data}));};
|
||||
assert.deepEqual(await providerModelOptions({type:'openai',endpoint:'https://fixture.example/v1'},'synthetic',signal(),fetcher),[{id:'o3',name:'o3'},{id:'other-model',name:'other-model'},{id:'gpt-4.1-2025-04-14',name:'GPT-4.1'},{id:'chatgpt-4o',name:'ChatGPT 4o'},{id:'gpt-5',name:'GPT-5'}]);
|
||||
const compatible=await providerModelOptions({type:'openai-compat',endpoint:'https://fixture.example/v1'},'synthetic',signal(),fetcher);
|
||||
assert.equal(compatible[0]?.id,'custom');assert.ok(compatible.some(v=>v.id==='gpt-5-2026-01-01'));assert.ok(compatible.some(v=>v.id==='gpt-transcribe'));
|
||||
});
|
||||
test('Anthropic headers, display names, bounded pagination; OpenRouter tools-only',async()=>{
|
||||
let calls=0;
|
||||
const rows=await providerModelOptions({type:'anthropic',endpoint:'https://fixture.example'},'synthetic',signal(),async(url,options)=>{
|
||||
assert.equal((options?.headers as any)['x-api-key'],'synthetic');assert.equal((options?.headers as any)['anthropic-version'],'2023-06-01');
|
||||
assert.equal(url,'https://fixture.example/v1/models'+(calls?'?after_id=a':''));
|
||||
return new Response(JSON.stringify(calls++?{data:[{id:'b',display_name:'Beta'}],has_more:false}:{data:[{id:'a',display_name:'Alpha'}],has_more:true,last_id:'a'}));
|
||||
});assert.deepEqual(rows,[{id:'a',name:'Alpha'},{id:'b',name:'Beta'}]);assert.equal(calls,2);
|
||||
await assert.rejects(providerModelOptions({type:'anthropic',endpoint:'https://fixture.example'},'synthetic',signal(),async()=>new Response(JSON.stringify({data:[],has_more:true,last_id:'repeat'}))),/unavailable/);
|
||||
const router=await providerModelOptions({type:'openrouter',endpoint:'https://fixture.example/api/v1'},'synthetic',signal(),async()=>new Response(JSON.stringify({data:[{id:'vendor/tools',supported_parameters:['tools']},{id:'vendor/text',supported_parameters:[]}]})));
|
||||
assert.deepEqual(router,[{id:'vendor/tools',name:'vendor/tools'}]);
|
||||
await assert.rejects(providerModelOptions({type:'openai',endpoint:'https://fixture.example'},'synthetic',signal(),async()=>new Response('x'.repeat(1048577))),/unavailable/);
|
||||
});
|
||||
test('OS-backed provider validation preserves harness matrix and custom effort rules',()=>{
|
||||
for (const type of ['anthropic','openai-compat','openrouter'] as const) {
|
||||
const settings:Settings={version:1,revision:0,agents:[],providers:[{id:'p',name:'P',type,endpoint:'https://fixture.example/v1',key,...(type==='openai-compat'?{wire:'responses' as const}:{})}],runtimes:[{id:'r',name:'R',harness:'buzz-agent',executable:process.execPath,providerId:'p',model:'custom'}]};
|
||||
validateSettings(settings);
|
||||
for (const harness of ['codex','pi'] as const) assert.throws(()=>validateSettings({...settings,runtimes:[{...settings.runtimes[0]!,harness,cli:process.execPath}]}),/combination/);
|
||||
assert.throws(()=>validateSettings({...settings,providers:[{...settings.providers[0]!,endpoint:'https://secret@fixture.example'}]}),/endpoint/);
|
||||
if(type !== 'anthropic') validateSettings({...settings,runtimes:[{...settings.runtimes[0]!,effort:'high'}]});
|
||||
}
|
||||
assert.deepEqual(runtimeEfforts('anthropic','custom'),[]);
|
||||
assert.ok(runtimeEfforts('openai','custom').includes('high'));
|
||||
assert.ok(!runtimeEfforts('openai','gpt-5').includes('xhigh'));
|
||||
const pi=presets.find(p=>p.id==='pi')!;assert.equal(pi.command,'buzz-pi-acp');assert.match(pi.installHint,/salman1993\/buzz-pi-acp.git#86b201e/);
|
||||
});
|
||||
@@ -15,7 +15,7 @@ import { createControllerConfig } from '../src/controller-config.ts';
|
||||
import { provisionCredentialSlot } from '../src/credential-slots.ts';
|
||||
import { createGenesis } from '../src/assignment.ts';
|
||||
import { createCredential } from '../src/credential-store.ts';
|
||||
import { registerAgent, agentNsec, addOpenAI } from '../src/settings-credentials.ts';
|
||||
import { registerAgent, agentNsec, addOpenAI, addProvider } from '../src/settings-credentials.ts';
|
||||
import { readSettings, saveSettings } from '../src/settings.ts';
|
||||
import { host } from '../src/host.ts';
|
||||
import { isolatedFileCredentials } from './isolated-file-credentials.ts';
|
||||
@@ -61,7 +61,7 @@ export async function registrationFixture(home: string, changed: (s: ManagerSnap
|
||||
let providerKey: string | null = null;
|
||||
addOpenAI(directory,'Review OpenAI','synthetic-provider-key',{read:()=>providerKey,create:(_r,v)=>{providerKey=v;}});
|
||||
const executable = join(home,'synthetic-acp');
|
||||
writeFileSync(executable,`#!/bin/sh\n[ "$REVIEW_MODE" = synthetic ] || exit 9\nexec '${process.execPath}' '${fileURLToPath(new URL('./acp-fixture.ts',import.meta.url))}' openai\n`,{mode:0o700});
|
||||
writeFileSync(executable,`#!/bin/sh\n[ "$REVIEW_MODE" = synthetic ] || exit 9\nexec '${process.execPath}' '${fileURLToPath(new URL('./acp-fixture.ts',import.meta.url))}' saved-provider\n`,{mode:0o700});
|
||||
const settings = readSettings(directory), runtimeId = 'review-runtime';
|
||||
saveSettings(directory,{...settings,runtimes:[{id:runtimeId,name:'Review runtime',harness:'buzz-agent',executable,providerId:settings.providers[0]!.id,model:'gpt-5',effort:'high',environment:{REVIEW_MODE:'synthetic'}}]},settings.revision);
|
||||
let providerFailure = false;
|
||||
@@ -120,7 +120,8 @@ export async function registrationFixture(home: string, changed: (s: ManagerSnap
|
||||
const credential = async (input: any, signal: AbortSignal) => {
|
||||
signal.throwIfAborted();
|
||||
if (input.action === 'signin') return {ok:true,secret:ownerSecret};
|
||||
if (input.action === 'models') return {ok:true,models:['gpt-5']};
|
||||
if (input.action === 'add-provider') { addProvider(directory,input.name,input.secret,{read:()=>providerKey,create:(_r,v)=>{providerKey=v;}},input.type,input.endpoint,input.wire); return {ok:true}; }
|
||||
if (input.action === 'models') { const p=readSettings(directory).providers.find(p=>p.id===input.provider); const model=p?.type === 'anthropic' ? 'claude-sonnet-4-5' : p?.type === 'openrouter' ? 'vendor/custom-model' : 'gpt-5'; return {ok:true,models:[model],modelLabels:{[model]:p?.type === 'anthropic' ? 'Claude Sonnet 4.5' : model}}; }
|
||||
if (input.action !== 'register-agent') throw Error('Fixture denies external credential operation');
|
||||
await beforeCredential?.(); signal.throwIfAborted();
|
||||
if (credentialFailure) throw Error('Synthetic credential failure');
|
||||
|
||||
@@ -115,8 +115,8 @@ test('Desktop contract detects fake adapter/CLI independently, strict Codex vers
|
||||
test('effort is manifest-backed, validates persisted settings and reaches actual ACP harness environment', async t => {
|
||||
assert.equal(readFileSync(new URL('../src/model-capabilities.json',import.meta.url),'utf8'),readFileSync(new URL('../../scripts/model-capabilities.json',import.meta.url),'utf8'));
|
||||
assert.ok(runtimeEfforts('openai','gpt-5').includes('high'));
|
||||
assert.deepEqual(runtimeEfforts('openai','unknown-custom'),[]);
|
||||
assert.deepEqual(runtimeEfforts('databricks_v2','catalog.schema.gpt-5'),[]);
|
||||
assert.ok(runtimeEfforts('openai','unknown-custom').includes('high'));
|
||||
assert.ok(runtimeEfforts('databricks_v2','catalog.schema.gpt-5').includes('high'));
|
||||
assert.ok(!runtimeEfforts('databricks_v2','databricks-claude-sonnet-4-5').includes('none'));
|
||||
const root = fixture(t), runner=join(root,'agent');
|
||||
writeFileSync(runner, `#!/bin/sh\n[ "$BUZZ_AGENT_THINKING_EFFORT" = high ] || exit 9\nexec '${process.execPath}' '${fileURLToPath(new URL('./acp-fixture.ts',import.meta.url))}' openai\n`, {mode:0o700});
|
||||
@@ -129,7 +129,7 @@ test('effort is manifest-backed, validates persisted settings and reaches actual
|
||||
await session.catalog(); await session.verify();
|
||||
});
|
||||
|
||||
test('short runtime form selects supported model-specific effort; unknown custom omits it and unsupported harness cannot save', async () => {
|
||||
test('short runtime form selects supported model-specific effort; unknown custom inherits provider capabilities 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 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"};});
|
||||
@@ -137,7 +137,7 @@ test('short runtime form selects supported model-specific effort; unknown custom
|
||||
assert.equal(submitted.at(-1)?.values?.effort,'high');
|
||||
assert.equal(submitted.at(-1)?.values?.model,'gpt-5');
|
||||
submitted.length=0;
|
||||
await run(['Buzz Agent · Available','OpenAI · p','Custom model'],['unrecognized-model','Custom']);
|
||||
await run(['Buzz Agent · Available','OpenAI · p','Custom model','Inherit'],['unrecognized-model','Custom']);
|
||||
assert.equal(submitted.at(-1)?.action,'add-runtime'); assert.equal(submitted.at(-1)?.values?.effort,undefined);
|
||||
submitted.length=0;
|
||||
await run(['Pi · Provider integration unavailable'],[]);
|
||||
|
||||
@@ -18,6 +18,13 @@ Agents: Sign in with saved owner key -> select Review agent -> Start -> yes ->
|
||||
changing the current run or requiring identity registration again.
|
||||
Synthetic agent nsec (never use real credentials in this fixture):
|
||||
nsec1yg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3qxh9tww
|
||||
Providers: Add provider -> Anthropic / OpenAI-compatible / OpenRouter.
|
||||
Use synthetic-provider-key (hidden); compatible endpoint https://fixture.example/v1.
|
||||
Compatible API: auto / chat / responses. Select provider -> Models.
|
||||
Harnesses: Add runtime -> Buzz Agent -> saved provider -> model or Custom model ->
|
||||
effort (when offered) -> environment {"REVIEW_MODE":"synthetic"} -> name -> Save.
|
||||
Agents: Choose runtime then explicit Start/Restart applies the selected runtime.
|
||||
Synthetic model access is not real provider authentication.
|
||||
Ctrl-Q: exit even inside a form; this test closes its owned relay/host.
|
||||
|
||||
Review host begins WITHOUT an agent slot or genesis. Explicit local Start
|
||||
|
||||
Reference in New Issue
Block a user