beehive: keep public assignment and auth guidance credential-neutral

Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
Logan Johnson
2026-09-10 14:34:16 -04:00
parent 3dbf9aaaab
commit 1315af75cc
2 changed files with 11 additions and 4 deletions

View File

@@ -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; }

View File

@@ -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);