diff --git a/beehive/src/cli.ts b/beehive/src/cli.ts index 5bb3b0f5b9..638402f4bf 100644 --- a/beehive/src/cli.ts +++ b/beehive/src/cli.ts @@ -1,3 +1,4 @@ +import { installationPublicSlots } from './slots.ts'; import { provisionCredentialSlot, reconcileCredentialProvision, orphanCredentialSlots } from './credential-slots.ts'; import { readHostIdentity } from './host-identity.ts'; import { verifyHostRegistration } from './host-registration.ts'; @@ -247,7 +248,7 @@ async function main() { importSlotKey(dir, agent, secret); console.log(`Local key restored for ${agent}; retained assignment and history unchanged. Start still requires this host's assignment. No keys or sessions transferred.`); } else if (command === 'assignment-export') { - const entries = installationSlots(resolve(text(args[0]))); + const entries = installationPublicSlots(resolve(text(args[0]))); const entry = args[2] ? entries.find(e => e.agent === args[2]) : entries.length === 1 ? entries[0] : undefined; if (!entry) throw Error('Specify agent public key after export filename for multi-slot host'); const state = object(readPrivate(entry.path)); @@ -266,6 +267,7 @@ async function main() { } finally { ui.close(); } } else if (command === 'conversation-setup') { const dir = resolve(text(args[0])); + if (object(readPrivate(join(dir, 'setup.json'))).version === 3) throw Error('Immutable public bindings cannot be rewritten: use local-setup action normal to create a NEW reference; identity credentials are not required'); const lock = join(dir, 'host.lock'); mkdirSync(lock, { mode: 0o700 }); // Same atomic exclusion as host startup; never remove a competing lock. try { @@ -277,7 +279,7 @@ async function main() { throw Error('Immutable bindings cannot be rewritten: use migrate-slots first for a legacy installation, then local-setup action normal to convert the selected ACP binding to a NEW reference'); } finally { rmdirSync(lock); } } else if (command === 'auth-info') { - const entries = installationSlots(resolve(text(args[0]))); + const entries = installationPublicSlots(resolve(text(args[0]))); const setup = args[1] ? entries[0]!.bindings[text(args[1])] : entries[0]!.setup; if (!setup) throw Error('Unknown local binding'); if (setup.mode === 'diagnostic-acp') { console.log('Setup/diagnostic only: authentication unverified, no model/native profile contract. Configure locally as the service user; no login/probe was run.'); return; } diff --git a/beehive/test/v3-bindings.test.ts b/beehive/test/v3-bindings.test.ts index a182b4a1be..8f41959d21 100644 --- a/beehive/test/v3-bindings.test.ts +++ b/beehive/test/v3-bindings.test.ts @@ -23,8 +23,8 @@ test('v3 actual local wizard: immutable replacement, neutral normal conversion, const directory = join(root, 'host'), file = join(root, 'credentials.json'), credentials = isolatedFileCredentials(file); const owner = newKey(), secret = newKey(), agent = publicKey(secret); const setup = validateSetup({ host: 'v3-local', ownerPublic: publicKey(owner), mode: 'goose', runner: realpathSync(process.execPath), args: ['acp'], workspace: root, serviceHome: root, configDirectory: root, gooseProvider: 'fixture', gooseModels: ['a'] }); - async function cli(responses: [string, string, (() => void)?][], success = true) { - const child = 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: file }, stdio: ['pipe', 'pipe', 'pipe'] }); + async function cli(responses: [string, string, (() => void)?][], success = true, command = ['local-setup', directory]) { + const child = spawn(process.execPath, ['--import', resolve('test/isolated-credentials-loader.ts'), 'src/cli.ts', ...command], { env: { PATH: '/usr/bin:/bin', HOME: root, BEEHIVE_TEST_CREDENTIAL_FILE: file }, stdio: ['pipe', 'pipe', 'pipe'] }); let output = '', cursor = 0; child.stdout.on('data', b => output += b); child.stderr.on('data', b => output += b); const exited = new Promise(resolve => child.on('close', resolve)); try { @@ -48,6 +48,11 @@ test('v3 actual local wizard: immutable replacement, neutral normal conversion, // A malformed synthetic credential file makes ANY read fail. Neutral wizard paths // must still work; the fixture loader prevents accidental native-store access. writeFileSync(file, 'invalid JSON'); + const genesisFile = join(root, 'exported-genesis.json'); + await cli([], true, ['assignment-export', directory, genesisFile, agent]); + assert.deepEqual(readPrivate(genesisFile), (readPrivate(journal) as any).assignment.genesis); + assert.match(await cli([], true, ['auth-info', directory]), /Goose/); + assert.match(await cli([], false, ['conversation-setup', directory]), /NEW reference/); await cli(normal('yes')); const entries = installationPublicSlots(directory), fingerprint = bindingFingerprint(entries[0]!.bindings.default!); assert.equal(entries[0]!.bindings.default!.conversation, undefined);