mirror of
https://github.com/block/buzz.git
synced 2026-10-29 21:41:33 +00:00
242 lines
19 KiB
TypeScript
242 lines
19 KiB
TypeScript
import { isolatedFileCredentials } from './isolated-file-credentials.ts';
|
|
import { credentialReference } from '../src/credential-store.ts';
|
|
import { importSlotKey, installationSlots } from '../src/slots.ts';
|
|
import { conversationRelayFixture } from './conversation-relay-fixture.ts';
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { spawn } from 'node:child_process';
|
|
import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync, realpathSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join, resolve } from 'node:path';
|
|
import { setTimeout as delay } from 'node:timers/promises';
|
|
import { newKey, publicKey, message } from '../src/protocol.ts';
|
|
import { hostPairing, registerHost } from '../src/host-registration.ts';
|
|
import { verifyHostCatalog, catalogResponse, HostInventory } from '../src/host-catalog.ts';
|
|
import { writePrivate } from '../src/storage.ts';
|
|
import { provisionCredentialSlot } from '../src/credential-slots.ts';
|
|
import { createGenesis } from '../src/assignment.ts';
|
|
import { nostrFixture } from './nostr-fixture.ts';
|
|
import { connectNostr } from '../src/nostr-client.ts';
|
|
|
|
function registration(owner: string, key: string, relay: string, label = 'same label') {
|
|
return registerHost(hostPairing({ version: 1, purpose: 'beehive-host-registration', host: publicKey(key), owner: publicKey(owner), label, relay, nonce: newKey() }), owner, Math.floor(Date.now() / 1000) + 600);
|
|
}
|
|
|
|
test('catalog verifies owner/signature/relay/expiry; labels cannot spoof identity and missing is not stopped', () => {
|
|
const owner = newKey(), a = newKey(), b = newKey(), relay = 'wss://example.invalid';
|
|
const one = registration(owner, a, relay), two = registration(owner, b, relay);
|
|
const value = { version: 1, owner: publicKey(owner), relay, registrations: [one, two] };
|
|
const catalog = verifyHostCatalog(value, value.owner, relay);
|
|
assert.throws(() => verifyHostCatalog(value, publicKey(newKey()), relay));
|
|
assert.throws(() => verifyHostCatalog(value, value.owner, 'wss://wrong.invalid'));
|
|
assert.throws(() => verifyHostCatalog({ ...value, registrations: [{ ...one, signature: '0'.repeat(128) }] }, value.owner, relay));
|
|
assert.throws(() => verifyHostCatalog(value, value.owner, relay, one.expires));
|
|
const inventory = message('inventory', publicKey(a), 'agent', 0, { observedAt: Date.now(), phase: 'stopped' });
|
|
assert.throws(() => catalogResponse(catalog, { sender: publicKey(b), message: inventory }));
|
|
assert.throws(() => catalogResponse(catalog, { sender: publicKey(a), message: { ...inventory, host: 'same label' } }));
|
|
const view = new HostInventory(catalog);
|
|
assert.deepEqual(view.rows().map(r => r.status), ['unreachable/unknown', 'unreachable/unknown']);
|
|
view.receive({ sender: publicKey(a), message: inventory });
|
|
assert.equal(view.rows()[0]!.status, 'recent-host-report');
|
|
assert.equal(view.rows()[0]!.report!.body.phase, 'stopped');
|
|
assert.equal(view.rows()[1]!.status, 'unreachable/unknown');
|
|
assert.equal(view.rows(Date.now() + 7000)[0]!.status, 'unreachable/unknown');
|
|
assert.equal(view.rows(one.expires * 1000)[0]!.report, undefined);
|
|
});
|
|
|
|
for (const scenario of ['external', 'configured', 'installed', 'v3-conversion']) {
|
|
const installed = scenario === 'installed' || scenario === 'v3-conversion', converting = scenario === 'v3-conversion';
|
|
const profiled = scenario === 'installed';
|
|
const profileInstructions = 'PRIVATE_C66_EXACT: keep this behavior private and answer carefully.';
|
|
test(`actual owner TUI owner-public credential slots Start/Stop via private transport (${scenario})`, { skip: installed && !process.env.BEEHIVE_REAL_BUZZ_ACP }, async () => {
|
|
const root = realpathSync(mkdtempSync(join(tmpdir(), 'beehive-pairing-cli-catalog-tui-')));
|
|
const owner = newKey(), a = newKey(), b = newKey(), agent = newKey();
|
|
const members = new Set([owner, a, b].map(publicKey));
|
|
const relay = await nostrFixture(publicKey(owner), members, { open: scenario === 'installed' });
|
|
const one = registration(owner, a, relay.url), two = registration(owner, b, relay.url);
|
|
const catalog = verifyHostCatalog({ version: 1, owner: publicKey(owner), relay: relay.url, registrations: (scenario === 'configured' || scenario === 'installed') ? [two] : [one, two] }, publicKey(owner), relay.url);
|
|
const directory = join(root, 'host'); mkdirSync(directory);
|
|
const credentialFile = join(root, 'credentials.json');
|
|
const credentials = isolatedFileCredentials(credentialFile);
|
|
credentials.create(credentialReference('host', publicKey(a)), a);
|
|
writePrivate(join(directory, 'host-identity.json'), { version: 3, pairing: one.request, key: credentialReference('host', publicKey(a)), registration: (scenario === 'configured' || scenario === 'installed') ? null : one });
|
|
const conversation = installed ? await conversationRelayFixture(root, owner, publicKey(agent)) : undefined;
|
|
provisionCredentialSlot(directory, { host: publicKey(a), ownerPublic: publicKey(owner), runner: realpathSync(process.execPath), args: [resolve(installed ? 'test/conversation-harness-fixture.ts' : 'test/runner.ts')], workspace: root, mode: installed ? 'buzz-agent-databricks-v2' : 'fixture', ...(conversation ? { serviceHome: root, configDirectory: root, databricksHost: 'https://fixture.invalid', ...(!converting ? { conversation: { executable: realpathSync(process.env.BEEHIVE_REAL_BUZZ_ACP!), relay: conversation.url, replyTool: { executable: realpathSync(join(process.env.BEEHIVE_REAL_BUZZ_ACP!, '..', 'buzz')) } } } : {}) } : {}) }, agent, createGenesis(publicKey(owner), publicKey(agent), publicKey(a)), credentials);
|
|
const journalPath = join(directory, 'agents', publicKey(agent), 'journal.json');
|
|
if (converting) {
|
|
const original = readFileSync(journalPath);
|
|
const wizard = spawn(process.execPath, ['--import', resolve('test/isolated-credentials-loader.ts'), 'src/cli.ts', 'local-setup', directory], { env: { PATH: '/usr/bin:/bin', HOME: root, BEEHIVE_TEST_CREDENTIAL_FILE: join(root, 'unused-credentials.json') }, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
let output = ''; wizard.stdout.on('data', b => output += b); wizard.stderr.on('data', b => output += b);
|
|
const exited = new Promise(resolve => wizard.on('close', resolve));
|
|
try {
|
|
for (const [prompt, answer] of [['Local action [', 'normal'], ['Existing agent public key:', publicKey(agent)], ['NEW immutable normal binding ID:', 'normal'], ['Absolute installed buzz-acp executable:', realpathSync(process.env.BEEHIVE_REAL_BUZZ_ACP!)], ['Buzz CONVERSATION relay URL', conversation!.url], ['Absolute installed buzz CLI executable:', realpathSync(join(process.env.BEEHIVE_REAL_BUZZ_ACP!, '..', 'buzz'))], ['Save NEW normal binding under installation lock', 'yes']]) {
|
|
for (let n = 0; n < 400 && !output.includes(prompt!); n++) { if (wizard.exitCode !== null) break; await delay(20); }
|
|
assert.ok(output.includes(prompt!), output); wizard.stdin.write(`${answer}\n`);
|
|
}
|
|
assert.equal(await exited, 0, output);
|
|
} finally { if (wizard.exitCode === null && wizard.signalCode === null) wizard.kill('SIGKILL'); await exited; }
|
|
assert.deepEqual(readFileSync(journalPath), original);
|
|
assert.equal(installationSlots(directory, credentials)[0]!.setup.conversation, undefined);
|
|
assert.ok(installationSlots(directory, credentials)[0]!.bindings.normal!.conversation);
|
|
}
|
|
const manifest = readFileSync(join(directory, 'setup.json'), 'utf8');
|
|
assert.ok(!manifest.includes(owner) && !manifest.includes(agent));
|
|
const helper = join(root, 'credential-child.mjs');
|
|
writeFileSync(helper, `import { readFileSync } from 'node:fs'; let input=''; for await (const b of process.stdin) input+=b; const secret=JSON.parse(readFileSync(${JSON.stringify(credentialFile)},'utf8'))[JSON.stringify(JSON.parse(input))]; process.stdout.write(JSON.stringify(secret ? {status:'present',secret} : {status:'missing'}));`);
|
|
const loader = join(root, 'credential-helper-loader.mjs');
|
|
writeFileSync(loader, `import { registerHooks } from 'node:module'; registerHooks({load(url,ctx,next){const r=next(url,ctx); if(!url.endsWith('/src/credential-helper.ts'))return r;return {...r,source:String(r.source).replace("new URL('./credential-helper-child.ts', import.meta.url)", ${JSON.stringify(`new URL(${JSON.stringify('file://' + helper)})`)})};}});`);
|
|
// Actual normal setup resumes both approved compatibility and unapproved
|
|
// installed reply/Stop fixtures, even when an agent manifest already exists.
|
|
const beforeSetup = readFileSync(join(directory, 'host-identity.json'));
|
|
const setupChild = spawn(process.execPath, ['--import', resolve('test/isolated-credentials-loader.ts'), '--import', loader, 'src/cli.ts', 'setup', directory], { env: { PATH: '/usr/bin:/bin', HOME: root, BEEHIVE_TEST_CREDENTIAL_FILE: credentialFile }, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
let setupOutput = ''; setupChild.stdout.on('data', b => setupOutput += b); setupChild.stderr.on('data', b => setupOutput += b);
|
|
const setupExit = new Promise<number | null>(done => setupChild.on('close', done));
|
|
try {
|
|
assert.equal(await setupExit, 0, setupOutput);
|
|
assert.match(setupOutput, /Host configured, NOT serving/);
|
|
assert.ok(!setupOutput.includes('Check/join'));
|
|
assert.deepEqual(readFileSync(join(directory, 'host-identity.json')), beforeSetup);
|
|
assert.equal(readFileSync(join(directory, 'setup.json'), 'utf8'), manifest);
|
|
} catch (error) {
|
|
await conversation?.close(); await relay.close(); rmSync(root, { recursive: true, force: true });
|
|
throw error;
|
|
} finally { if (setupChild.exitCode === null && setupChild.signalCode === null) setupChild.kill('SIGKILL'); await setupExit; }
|
|
const hostChild = spawn(process.execPath, ['--import', resolve('test/isolated-credentials-loader.ts'), '--import', loader, 'src/cli.ts', 'host', directory, ...(installed ? [relay.url] : []), '--owner-present'], { env: { PATH: '/usr/bin:/bin', HOME: root, BEEHIVE_TEST_CREDENTIAL_FILE: credentialFile }, stdio: ['ignore','pipe','pipe'] });
|
|
let hostOutput = ''; hostChild.stdout.on('data', b => hostOutput += b); hostChild.stderr.on('data', b => hostOutput += b);
|
|
const hostExit = new Promise<number | null>(resolve => hostChild.on('close', resolve));
|
|
const running = { async close() { if (hostChild.exitCode === null && hostChild.signalCode === null) hostChild.kill('SIGTERM'); assert.equal(await hostExit, 0, hostOutput); } };
|
|
for (let n=0; n<400 && !hostOutput.includes('Host online'); n++) { if (hostChild.exitCode !== null) break; await delay(20); }
|
|
assert.match(hostOutput, /Host online/);
|
|
const foreignMessages: unknown[] = [];
|
|
const foreign = connectNostr(relay.url, Buffer.from(b, 'hex'), m => foreignMessages.push(m), () => {});
|
|
await foreign.ready;
|
|
const path = join(root, 'catalog.json'); writePrivate(path, catalog);
|
|
const env: NodeJS.ProcessEnv = { PATH: '/usr/bin:/bin', HOME: root, BEEHIVE_TEST_CREDENTIAL_FILE: credentialFile };
|
|
const child = spawn(process.execPath, ['--import', resolve('test/isolated-credentials-loader.ts'), 'src/cli.ts', 'tui', path, ...(installed ? [relay.url] : [])], { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
let output = ''; child.stdout.on('data', b => output += b.toString()); child.stderr.on('data', b => output += b.toString());
|
|
const exit = new Promise<number | null>((done, reject) => { child.on('exit', done); child.on('error', reject); });
|
|
async function wait(predicate: () => boolean) { for (let n = 0; n < (installed ? 3000 : 400); n++) { if (predicate()) return; if (child.exitCode !== null) throw Error(output); await delay(20); } throw Error(`TUI observation timeout: ${output}`); }
|
|
async function command(line: string) { const start = output.length; child.stdin.write(`${line}\n`); await wait(() => output.slice(start).includes('beehive> ')); return output.slice(start); }
|
|
try {
|
|
await wait(() => output.includes('Owner private key'));
|
|
child.stdin.write(`${owner}\n`);
|
|
await wait(() => output.includes('beehive> '));
|
|
let rows = await command('hosts');
|
|
assert.match(rows, /UNREACHABLE \/ UNKNOWN/);
|
|
assert.ok(rows.includes(publicKey(a))); assert.ok(rows.includes(publicKey(b)));
|
|
assert.match(rows, /recent host report/);
|
|
await command(`select ${publicKey(a)}`);
|
|
if (converting) {
|
|
await command('binding normal');
|
|
let selected = '';
|
|
for (let n = 0; n < 80; n++) { selected = await command('show'); if (selected.includes('"revision": 1,\n "body"')) break; await delay(25); }
|
|
assert.ok(selected.includes('"revision": 1,\n "body"'), selected);
|
|
const saved = JSON.parse(readFileSync(journalPath, 'utf8'));
|
|
assert.equal(saved.selected.harnessSetup.id, 'normal'); assert.equal(saved.actual, null);
|
|
}
|
|
if (profiled) {
|
|
let at = output.length; child.stdin.write('profile-new\n');
|
|
await wait(() => output.slice(at).includes('Profile name:')); child.stdin.write('Private careful\n');
|
|
await wait(() => output.slice(at).includes('Nonsecret behavior instructions')); child.stdin.write(`${profileInstructions}\n`);
|
|
await wait(() => output.slice(at).includes('Publish immutable revision?')); child.stdin.write('no\n');
|
|
await wait(() => output.slice(at).includes('beehive> '));
|
|
const drafts = await command('drafts');
|
|
const id = drafts.match(/([a-f0-9-]{36}) \| saved draft/)![1];
|
|
assert.match(drafts, /not submitted/);
|
|
at = output.length; child.stdin.write(`profile-resume ${id}\n`);
|
|
await wait(() => output.slice(at).includes('Resume [edit/publish/cancel]')); child.stdin.write('publish\n');
|
|
await wait(() => output.slice(at).includes('Publish immutable revision?')); child.stdin.write('yes\n');
|
|
await wait(() => output.slice(at).includes('beehive> '));
|
|
let published = '';
|
|
for (let n = 0; n < 80; n++) { published = await command('profiles'); if (published.includes(profileInstructions)) break; await delay(25); }
|
|
assert.ok(published.includes(profileInstructions), published);
|
|
await command('apply 1');
|
|
let selected = '';
|
|
for (let n = 0; n < 80; n++) { selected = await command('show'); if (selected.includes('"revision": 1,\n "body"')) break; await delay(25); }
|
|
assert.ok(selected.includes('"revision": 1,\n "body"'), selected);
|
|
assert.equal(JSON.parse(readFileSync(journalPath, 'utf8')).actual, null);
|
|
}
|
|
await command('start');
|
|
await wait(() => output.includes('completed | accepted'));
|
|
let shown = '';
|
|
for (let n = 0; n < 80; n++) { shown = await command('show'); if (shown.includes('"phase": "running"')) break; await delay(25); }
|
|
assert.match(shown, /"phase": "running"/);
|
|
assert.ok(shown.includes(publicKey(agent)));
|
|
if (conversation) {
|
|
await wait(() => conversation.replies.length > 0);
|
|
const actual = JSON.parse(readFileSync(journalPath, 'utf8')).actual;
|
|
assert.equal(actual.evidence.agentPublicKey, publicKey(agent));
|
|
assert.equal(actual.evidence.session, 'conversation-session');
|
|
assert.equal(actual.selection.model, 'databricks-claude-haiku-4-5');
|
|
if (profiled) {
|
|
assert.equal(actual.selection.behavior.instructions, profileInstructions);
|
|
assert.equal(readFileSync(join(root, 'received-system-instructions'), 'utf8'), profileInstructions);
|
|
const nativeInputs = readFileSync(join(root, 'native-session-inputs.jsonl'), 'utf8').trim().split('\n').map(line => JSON.parse(line));
|
|
assert.ok(nativeInputs.length > 0);
|
|
assert.ok(nativeInputs.every(input => input.systemPrompt === profileInstructions));
|
|
}
|
|
console.log('New private management transport installed signed agent reply:', conversation.replies[0]!.id);
|
|
}
|
|
const stopAt = output.length;
|
|
await command('stop');
|
|
await wait(() => output.slice(stopAt).includes('accepted'));
|
|
for (let n = 0; n < 80; n++) { shown = await command('show'); if (shown.includes('"phase": "stopped"')) break; await delay(25); }
|
|
assert.match(shown, /"phase": "stopped"/);
|
|
if (conversation) for (const file of ['harness-pid', 'descendant-pid', 'tool-shim-pid']) {
|
|
const pid = Number(readFileSync(join(root, file), 'utf8'));
|
|
assert.ok(Number.isSafeInteger(pid) && pid > 0);
|
|
assert.throws(() => process.kill(pid, 0), (error: NodeJS.ErrnoException) => error.code === 'ESRCH', `${file} survives accepted Stop`);
|
|
}
|
|
const beforeReplay = readFileSync(journalPath, 'utf8');
|
|
await foreign.publish(message('start', publicKey(a), publicKey(agent), 2), publicKey(a));
|
|
await foreign.publish(message('inventory', publicKey(a), publicKey(agent), 999, { phase: 'FORGED', observedAt: Date.now() + 500 }), publicKey(owner));
|
|
const ownerWire = connectNostr(relay.url, Buffer.from(owner, 'hex'), () => {}, () => {});
|
|
await ownerWire.ready;
|
|
try {
|
|
const journal = JSON.parse(beforeReplay);
|
|
// Rewrap the exact original durable Start after Stop. Relay ACK is not another execution.
|
|
const intentDirectory = join(root, 'management-intents');
|
|
const { readdirSync } = await import('node:fs');
|
|
const { open } = await import('../src/protocol.ts');
|
|
const scope = join(intentDirectory, readdirSync(intentDirectory)[0]!);
|
|
const intent = readdirSync(scope).filter(n => n.endsWith('.intent')).map(n => JSON.parse(readFileSync(join(scope, n), 'utf8'))).map(v => open(v.envelope, owner)).find(m => m.type === 'start')!;
|
|
await ownerWire.publish(intent, publicKey(a));
|
|
await command('reconcile');
|
|
await delay(100);
|
|
shown = await command('show');
|
|
assert.ok(!shown.includes('FORGED'));
|
|
assert.match(shown, /"phase": "stopped"/);
|
|
assert.equal(readFileSync(journalPath, 'utf8'), beforeReplay);
|
|
assert.equal(Object.keys(journal.operations).length, converting || profiled ? 3 : 2);
|
|
} finally { ownerWire.close(); }
|
|
assert.equal(foreignMessages.length, 0, 'another admitted host cannot read owner inventory or commands');
|
|
assert.equal(relay.httpRequests.length, 0);
|
|
assert.ok(relay.history.every(e => e.kind === 1059 && e.tags.length === 1));
|
|
assert.ok(!output.includes(owner)); assert.ok(!output.includes(a)); assert.ok(!output.includes(agent));
|
|
const state = JSON.parse(readFileSync(journalPath, 'utf8'));
|
|
assert.equal(state.phase, 'stopped'); assert.equal(state.revision, converting || profiled ? 3 : 2);
|
|
// Simulate deliberate OS deletion while the host still has its old hydrated
|
|
// setup in memory: the production pre-spawn re-read must refuse that cache.
|
|
const reference = JSON.parse(manifest).agents[publicKey(agent)].key;
|
|
credentials.remove(reference);
|
|
const runs = JSON.stringify(state.runs);
|
|
await command('start');
|
|
await wait(() => Object.keys(JSON.parse(readFileSync(journalPath, 'utf8')).operations).length === (converting || profiled ? 4 : 3));
|
|
const refused = JSON.parse(readFileSync(journalPath, 'utf8'));
|
|
assert.equal(refused.phase, 'stopped'); assert.equal(refused.actual, null);
|
|
assert.equal(JSON.stringify(refused.runs), runs);
|
|
assert.equal(installationSlots(directory, credentials)[0]!.keyPresent, false);
|
|
await running.close();
|
|
importSlotKey(directory, publicKey(agent), agent, credentials);
|
|
assert.equal(installationSlots(directory, credentials)[0]!.setup.agentSecret, agent);
|
|
assert.equal(readFileSync(join(directory, 'setup.json'), 'utf8'), manifest);
|
|
child.stdin.write('quit\n'); assert.equal(await exit, 0, output);
|
|
|
|
} finally {
|
|
if (child.exitCode === null && child.signalCode === null) { child.kill('SIGTERM'); await exit; }
|
|
foreign.close(); await running.close(); await conversation?.close(); await relay.close(); rmSync(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
}
|