fix(beehive): bound projected settings inventory before adoption

Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
Logan Johnson
2026-09-12 11:19:57 -04:00
parent 78f0c3738f
commit 4eada242e3
7 changed files with 146 additions and 17 deletions

View File

@@ -39,3 +39,42 @@ External evidence: `/Users/loganj/.buzz/artifacts/beehive-host-settings-build-fb
## Publication/package boundary
Do not claim installed/delivered/live credentials verified. Only task files should be committed on `feat/beehive-opentui-57f9c8fa`; no push/PR/merge/default branch or process mutation. Continue missing functionality and narrow credential/process review before delivery. Export exact accepted commit including all new `beehive/src/*.ts`; use the existing package-local lock/runtime recipe. Each eventual occupied `opentui-current` is replaced with one current export, preserving settings/evidence/processes and no retained rollback copy. Installed synthetic smoke must use the extended loader, not fresh HOME alone.
## Continuation 0394e318 (still incomplete; do not publish)
Recovered 78f0c373 without restarting its implementation. This continuation adds:
- Shared `protocol.ts::serializeManagement` / `MANAGEMENT_WIRE_BYTES` for both encrypted transports. The wire ceiling remains exactly 32768 UTF-8 plaintext bytes; diagnostic envelope and NIP-59 consumers use that same constant.
- `host.ts` validates each **actual projected slot inventory** before changing any binding map or loaded revision, at startup and heartbeat adoption. The inventory includes retained assignment/configurations/history/actual run and repeated model/workspace fields. Adoption reserves 8192 bytes (the existing configuration budget) below the wire ceiling. Oversized saved catalogs remain public saved revisions, not loaded revisions; prior reports continue. Startup with an oversized retained catalog refuses and releases its own lock after successful teardown.
- Real long-workspace/200-character-model regression: a fitting catalog round-trips through both codecs, a 19-runtime catalog is saved but not adopted, prior inventory remains unchanged and publishable, and reopen refuses it. Exact-limit/multibyte-overflow tests exercise both producers.
- Synthetic authenticated service fixture: unauthenticated Stop does not invoke teardown; concurrent verified Stops share one failed teardown promise; subsequent Stop/status remain Unknown; reservations/host lock remain; duplicate Start refuses. No process/key deletion is authorized by failure.
- Broker assertion now reports mode plus underlying verification error, without changing its checks or 2000 ms deadline.
### Seven-point delivery status
1. **Registration/UI:** preserved prior implementation and evidence; unchanged here.
2. **Databricks v2:** NOT completed. No native helper, OS-token custody/refresh or model bridge was built. Bounded source check confirmed `crates/buzz-agent/src/auth.rs::PkceOAuthTokenSource::new_with_http_timeout` immediately derives/reads its file cache and `save` writes it. A Beehive-only custody seam at that owner is necessary; passing a fresh directory is not OS token storage. Do not run the existing helper as a substitute. No helper packaging recipe can be claimed until a working helper exists. Existing normal build owner is `cargo build --release -p buzz-agent` with pinned Rust 1.95.0; future bridge must use an isolated target output and include the built helper in export.
3. **Harness discovery:** still Buzz Agent PATH-only; Desktop-equivalent adapter/CLI/version rules remain required. Prior-art plan has exact source owners and current Pi correction. No discovery rewrite made.
4. **Models/effort:** existing OpenAI listing/custom model retained; Databricks models and supported harness-specific effort selection/launch propagation remain required. No new end-to-end Databricks/effort evidence.
5. **Relay:** URL/actual connection state retained; optional metadata name capture still missing. No network fetch added.
6. **Wire:** pre-adoption actual representation gate and shared producer/consumer ceiling added and tested. Residual: 8 KiB reserve is conservative headroom, NOT proof that arbitrarily growing assignment/run history fits forever. It also applies to legacy startup inventories; unusually large existing inventories can now refuse startup even if below 32 KiB. Assess this compatibility boundary before release. Existing send failure handling does not constitute a future-lifecycle fit proof.
7. **Lifecycle/failures:** new service teardown/unauthorized-stop coverage passes. Broader credential helper late-completion/cancellation/failure matrix remains incomplete. Existing active-run immutability fixture still passes.
### Validation and failure assessment
Artifacts: `/Users/loganj/.buzz/artifacts/beehive-host-settings-continuation-0394e318/`.
- Node 24.15.0 direct TypeScript `--noEmit`: passed (`typecheck-final.log`).
- Focused host settings: 6/6 (`settings-fixed.log`). Original test failure retained in `settings.log`: JSON round-trip omits an undefined `move` property; fixture now compares serialized representation, not an in-memory undefined key.
- Service: 3/3 (`service-failures.log`).
- Broker diagnostic run: 1/1, all existing modes (`broker-diagnostic.log`).
- One complete final touched-package suite: **159 passed, 2 failed, 19 skipped**, 180 tests, 32.063s (`full-suite.log`). NOT green. No full-suite rerun, deadline inflation, serialization or assertion weakening.
- Broker now identifies failing `ok` stimulus: `Conversation response timed out; check relay admission and local sign-in`. Actual delta inspection from 4bdb5f09 to 78f0c373 found no `broker.ts` diff; `acp.ts` adds resolvedProviderKey only under `if (plan.buzzProvider)`. This fixture has no buzzProvider and takes the legacy Databricks environment path. Thus this stimulus does not exercise the new provider-key launch branch. It still exercises shared supervisor/broker ownership; a targeted pass is not a root-cause diagnosis or release waiver.
- Additional final-suite failure: `conversation.test.ts:52`, first Start at line 75 received no management receipt within the existing polling bound. It uses the same synthetic legacy Databricks/conversation path, no buzzProvider. Causality is unestablished; no contention explanation is asserted and no flake-repair campaign was started.
- Reuse prior unchanged Bun renderer and synthetic PTY evidence; no new UI smoke claimed.
### Narrow review authority owners / remaining risk
Credential authority: `credential-store.ts`, `native-credentials.ts`, `credential-helper.ts`, `manager-credential.ts`, `settings-credentials.ts`, `manager-credential-child.ts`; native OAuth authority will be `crates/buzz-agent/src/auth.rs`, not Desktop cache access. Process authority: `host-service.ts` exact capability/nonce/HMAC, `host.ts` loaded revision/Stop fences, `owned.ts`, `broker.ts`, `acp.ts`. Wire authority: `protocol.ts`, `nostr-codec.ts`, `host.ts::applySettings` and slot `validateBindings`.
This is a bounded continuation handoff, not a genuine external/toolchain blocker and not completion of BUILD IT ALL. Native auth/discovery/effort/relay work remains unexhausted. No push/install/package publication, native build, real credential access, real relay/provider request, browser login, installed harness execution or existing host control. Both installed packages remain at the previously reported 4bdb5f09; they were not inspected or mutated here.

View File

@@ -18,7 +18,7 @@ import { hash, semanticHash, selection, sameSelection, sameLaunchSelection, move
import { installationSlotsAsync } from './slots.ts';
import { validateGenesis, type Genesis } from './assignment.ts';
import { connect, validateRelayURL } from './client.ts';
import { digest, fields, message, object, publicKey, text, type Message } from './protocol.ts';
import { digest, fields, serializeManagement, message, object, publicKey, text, type Message } from './protocol.ts';
import { readPrivate, writePrivate } from './storage.ts';
import { ConversationSession } from './broker.ts';
import { spawnOwned, type OwnedProcess } from './owned.ts';
@@ -140,12 +140,12 @@ function slot(setup: Setup, path: string, agent: string, currentSetup: (id: stri
let owned: OwnedProcess | ConversationSession | undefined;
let acp: AgentSession | ConversationSession | undefined;
let catalog: Catalog | { state: 'not-probed' | 'failed'; authentication: 'unverified' } = { state: 'not-probed', authentication: 'unverified' };
function inventory() {
const candidate = bindings[state.selected.harnessSetup?.id ?? setupId];
function inventory(projectedBindings = bindings) {
const candidate = projectedBindings[state.selected.harnessSetup?.id ?? setupId];
const nextSetup = !retiredBindings[state.selected.harnessSetup?.id ?? setupId] && candidate && (!state.selected.harnessSetup || state.selected.harnessSetup.fingerprint === bindingFingerprint(candidate)) ? candidate : undefined;
return message('inventory',setup.host,agent,state.revision, {
defaultHarnessSetup: setupId,
harnessSetups: Object.entries(bindings).map(([id, value]) => ({ id, availability: retiredBindings[id] ? 'retired' : value.mode === 'diagnostic-acp' ? 'diagnostic-only' : 'available', ...(value.custom ? { label: value.custom.label, contract: value.custom.contract, authentication: 'unverified', ...(value.mode === 'diagnostic-acp' ? { reason: diagnosticReason } : {}) } : {}), fingerprint: bindingFingerprint(value), kind: value.mode, models: setupModels(value), workspaces: value.allowedWorkspaces ?? [value.workspace] })),
harnessSetups: Object.entries(projectedBindings).map(([id, value]) => ({ id, availability: retiredBindings[id] ? 'retired' : value.mode === 'diagnostic-acp' ? 'diagnostic-only' : 'available', ...(value.custom ? { label: value.custom.label, contract: value.custom.contract, authentication: 'unverified', ...(value.mode === 'diagnostic-acp' ? { reason: diagnosticReason } : {}) } : {}), fingerprint: bindingFingerprint(value), kind: value.mode, models: setupModels(value), workspaces: value.allowedWorkspaces ?? [value.workspace] })),
configurations: configurations(state.configurations, state.selected),
runHistory: Object.values(state.runs ?? {}).slice(-8).map(run => ({ run: run.run, configuration: run.selection.configuration, harnessSetup: run.harnessSetup, model: run.selection.model, workspace: run.selection.workspace, appliedInstructions: run.appliedInstructions, preparedInputHash: run.preparedInputHash })),
move: state.move ? 'destination preflight pending; source still assigned' : undefined, assignmentChain: state.assignment.chain ?? [], assignedHost: state.assignment.assignedHost, genesis: state.assignment.genesis, executionAuthority: state.assignment.assignedHost === setup.host, phase: state.phase, selectedNext: state.selected, actualRun: state.actual,
@@ -575,7 +575,13 @@ function slot(setup: Setup, path: string, agent: string, currentSetup: (id: stri
Object.defineProperty(state.operations,m.id,{ value: { fingerprint, reply }, enumerable: true, configurable: true, writable: true });
state.outbox.push(reply); save(); publish(reply); publish(inventory());
}
return { agent, receive, replay, heartbeat() {
return { agent, receive, replay, validateBindings(projected: Record<string, Setup>) {
// Check the real inventory, including retained state and duplicated selected
// models/workspaces, not a row-count estimate. Reserve 8 KiB (the existing
// configuration budget) for subsequent lifecycle/selection growth. Both
// transports still enforce the exact 32 KiB ceiling at every publication.
serializeManagement(inventory(projected), 8192);
}, heartbeat() {
if (owned && (owned.exited || acp?.healthy === false) && state.phase === 'running') { state.phase = 'quarantined'; save(); }
publish(inventory());
}, async close() {
@@ -624,10 +630,11 @@ export async function host(directory: string, url: string, signal?: AbortSignal,
if (candidate.revision < effectiveSettings.revision) throw Error('Settings revision regressed');
for (const collection of ['agents','providers','runtimes'] as const) for (const row of effectiveSettings[collection]) if (!candidate[collection].some(n => JSON.stringify(n) === JSON.stringify(row))) throw Error('Retained settings changed');
const additions = entries.map(entry => runtimeBindings(entry.setup,candidate));
// All slots must fit before mutating ANY effective binding map/revision.
for (const [index, entry] of entries.entries()) slots.get(entry.agent)!.validateBindings({ ...entry.bindings, ...additions[index] });
for (const [index, entry] of entries.entries()) Object.assign(entry.bindings,additions[index]);
effectiveSettings = candidate;
};
applySettings(effectiveSettings);
reloadSettings = () => {
try { const candidate = readSettings(directory); if (candidate.revision !== effectiveSettings.revision) applySettings(candidate); }
catch { /* Saved invalid settings remain on disk for repair; prior effective catalog is retained. */ }
@@ -667,6 +674,7 @@ export async function host(directory: string, url: string, signal?: AbortSignal,
function hostKeyForMetadata() { return transport?.binding.host ?? entry.setup.host; }
}, url, providerRead));
}
applySettings(effectiveSettings);
const setup = entries[0]?.setup;
const hostKey = transport?.binding.host ?? setup!.host;
// Every slot is hydrated before dialing. Initial WS history may arrive in the

View File

@@ -1,6 +1,6 @@
import { finalizeEvent, generateSecretKey, getPublicKey, getEventHash, verifyEvent, type Event } from 'nostr-tools/pure';
import { v2 } from 'nostr-tools/nip44';
import { fields, object, parseMessage, type Message } from './protocol.ts';
import { fields, object, parseMessage, serializeManagement, MANAGEMENT_WIRE_BYTES, type Message } from './protocol.ts';
const domain = 'beehive-management-v1';
/** Authenticated inner message; never infer the sender from the ephemeral outer key. */
@@ -10,8 +10,7 @@ export type AuthenticatedMessage = { sender: string; message: Message };
* operation identity remains Message.id, NOT the rumor, seal, or wrap event id.
*/
export function wrapManagement(message: Message, secret: Uint8Array, recipient: string, now = Math.floor(Date.now() / 1000)): Event {
const content = JSON.stringify(parseMessage(message));
if (Buffer.byteLength(content) > 32768) throw Error('Management message exceeds bounded wire size');
const content = serializeManagement(message);
const rumor = { kind: 14, pubkey: getPublicKey(secret), created_at: now, tags: [['p', recipient], ['subject', domain]], content };
const seal = finalizeEvent({ kind: 13, created_at: now, tags: [], content: v2.encrypt(JSON.stringify({ ...rumor, id: getEventHash(rumor) }), v2.utils.getConversationKey(secret, recipient)) }, secret);
const ephemeral = generateSecretKey();
@@ -32,7 +31,7 @@ export function unwrapManagement(value: unknown, secret: Uint8Array): Authentica
if (seal.tags.length) throw Error('Unexpected seal tags');
const rumor = object(JSON.parse(v2.decrypt(seal.content, v2.utils.getConversationKey(secret, seal.pubkey))));
fields(rumor, ['id', 'pubkey', 'created_at', 'kind', 'tags', 'content']);
if (rumor.pubkey !== seal.pubkey || rumor.kind !== 14 || typeof rumor.content !== 'string' || Buffer.byteLength(rumor.content) > 32768 || JSON.stringify(rumor.tags) !== JSON.stringify([['p', recipient], ['subject', domain]]) || rumor.id !== getEventHash(rumor as unknown as Event)) throw Error('Invalid management rumor');
if (rumor.pubkey !== seal.pubkey || rumor.kind !== 14 || typeof rumor.content !== 'string' || Buffer.byteLength(rumor.content) > MANAGEMENT_WIRE_BYTES || JSON.stringify(rumor.tags) !== JSON.stringify([['p', recipient], ['subject', domain]]) || rumor.id !== getEventHash(rumor as unknown as Event)) throw Error('Invalid management rumor');
return { sender: seal.pubkey, message: parseMessage(JSON.parse(rumor.content)) };
}
/** Tracer authority boundary. Host-to-host Move traffic is intentionally denied until

View File

@@ -28,6 +28,14 @@ export function parseMessage(value: unknown): Message {
object(m.body);
return m as Message;
}
/** Plaintext ceiling shared by the diagnostic and NIP-59 transports. */
export const MANAGEMENT_WIRE_BYTES = 32768;
/** Serialize exactly what transports carry; optional reserve is for adoption headroom. */
export function serializeManagement(value: Message, reserveBytes = 0): string {
const content = JSON.stringify(parseMessage(value));
if (!Number.isSafeInteger(reserveBytes) || reserveBytes < 0 || Buffer.byteLength(content) + reserveBytes > MANAGEMENT_WIRE_BYTES) throw Error('Management message exceeds bounded wire size');
return content;
}
export function digest(value: string): Buffer { return createHash('sha256').update(value).digest(); }
export function publicKey(secret: string): string { return Buffer.from(schnorr.getPublicKey(secret)).toString('hex'); }
export function newKey(): string { return Buffer.from(schnorr.utils.randomPrivateKey()).toString('hex'); }
@@ -35,12 +43,11 @@ function bytes(e: Omit<Envelope, 'signature'>): string { return JSON.stringify([
function encryptionKey(secret: string): Buffer { return digest(`beehive-private-v1:${secret}`); }
/** Encrypt before relay publication; the relay only receives the owner's public key. */
export function seal(message: Message, secret: string): Envelope {
parseMessage(message);
if (Buffer.byteLength(JSON.stringify(message)) > 32768) throw Error('Management message exceeds bounded wire size');
const content = serializeManagement(message);
const nonce = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', encryptionKey(secret), nonce);
cipher.setAAD(Buffer.from('beehive-private-v1'));
const ciphertext = Buffer.concat([cipher.update(JSON.stringify(message)), cipher.final()]).toString('hex');
const ciphertext = Buffer.concat([cipher.update(content), cipher.final()]).toString('hex');
const e = { v: 1 as const, owner: publicKey(secret), nonce: nonce.toString('hex'), ciphertext, tag: cipher.getAuthTag().toString('hex') };
return { ...e, signature: Buffer.from(schnorr.sign(digest(bytes(e)), secret)).toString('hex') };
}
@@ -51,7 +58,7 @@ export function verifyEnvelope(value: unknown, owner: string): Envelope {
for (const [key, length] of [['nonce',24],['tag',32],['signature',128]] as const) {
if (typeof e[key] !== 'string' || e[key].length !== length || !/^[0-9a-f]+$/.test(e[key])) throw Error('Invalid envelope');
}
if (typeof e.ciphertext !== 'string' || e.ciphertext.length > 65536 || !/^(?:[0-9a-f]{2})+$/.test(e.ciphertext)) throw Error('Invalid ciphertext');
if (typeof e.ciphertext !== 'string' || e.ciphertext.length > MANAGEMENT_WIRE_BYTES * 2 || !/^(?:[0-9a-f]{2})+$/.test(e.ciphertext)) throw Error('Invalid ciphertext');
const envelope = e as Envelope;
if (!schnorr.verify(envelope.signature, digest(bytes(envelope)), owner)) throw Error('Invalid signature');
return envelope;

View File

@@ -28,7 +28,7 @@ test('broker enforces model, completion health, cancellation and owned teardown
const r = await result;
if (['ok', 'optional-ok', 'optional-reject'].includes(mode)) {
if (mode === 'optional-reject') assert.equal(readFileSync(join(dir, 'config-error-forwarded'), 'utf8'), 'yes');
assert.ok('value' in r); assert.equal(r.value.session, 'conversation-session');
assert.ok('value' in r, `${mode}: ${'error' in r ? String(r.error) : 'missing evidence'}`); assert.equal(r.value.session, 'conversation-session');
for (let i = 0; i < 40 && !existsSync(join(dir, 'reverse-rpc')); i++) await delay(20);
assert.equal(readFileSync(join(dir, 'reverse-rpc'), 'utf8'), 'ok');
} else assert.ok('error' in r, `${mode} must not commit evidence`);

View File

@@ -1,10 +1,12 @@
import { connect } from 'node:net';
import { writePrivate } from '../src/storage.ts';
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, realpathSync, writeFileSync, mkdirSync, existsSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { pathToFileURL } from 'node:url';
import { startService, stopService, serviceStatus } from '../src/host-service.ts';
import { startService, stopService, serviceStatus, serveHost } from '../src/host-service.ts';
test('detached actual host survives client return; verified duplicate/reopen/stop; stale locks never adopted', async t => {
const root = realpathSync(mkdtempSync('/tmp/beehive-pairing-cli-service-'));
@@ -42,3 +44,29 @@ test('startup failure does not report running or remove an unowned endpoint', as
await assert.rejects(startService(root,pathToFileURL(entry)),/unknown/);
assert.equal(existsSync(join(root,'service.sock')),true);
});
test('failed owned teardown remains Unknown, retains reservations and never retries or accepts an unauthenticated Stop', async t => {
const root = realpathSync(mkdtempSync('/tmp/beehive-pairing-cli-teardown-'));
const instance = 'a'.repeat(32), token = 'b'.repeat(64);
mkdirSync(join(root,'service.lock'),{mode:0o700});
mkdirSync(join(root,'host.lock'),{mode:0o700});
writePrivate(join(root,'service.json'),{version:1,instance,token,socket:join(root,'service.sock')});
let closes = 0;
const server = await serveHost(root,instance,async () => ({status: () => ({agents: 1, relay: 'disconnected'}), async close() { closes++; throw Error('synthetic containment failure'); }}));
t.after(async () => { await new Promise<void>(resolve => server.close(() => resolve())); rmSync(root,{recursive:true,force:true}); });
await new Promise<void>((resolve,reject) => {
const socket = connect(join(root,'service.sock'));
socket.on('error',reject); socket.on('close',() => resolve());
socket.on('connect',() => socket.end(JSON.stringify({request:{action:'stop',instance,nonce:'c'.repeat(32)},mac:'0'.repeat(64)})+'\n'));
});
assert.equal(closes,0);
assert.equal((await serviceStatus(root)).state,'running');
const results = await Promise.all([stopService(root,instance),stopService(root,instance)]);
assert.ok(results.every(r => r.state === 'unknown'));
assert.equal(closes,1,'one retained teardown promise, not repeated process control');
assert.equal((await serviceStatus(root)).state,'unknown');
assert.equal((await stopService(root,instance)).state,'unknown');
assert.equal(closes,1);
await assert.rejects(startService(root),/unknown/);
for (const path of ['service.json','service.lock','host.lock','service.sock']) assert.ok(existsSync(join(root,path)),path);
});

View File

@@ -1,3 +1,4 @@
import { wrapManagement, unwrapManagement } from '../src/nostr-codec.ts';
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync, existsSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
@@ -10,7 +11,7 @@ import { registerAgent, agentNsec, addOpenAI } from '../src/settings-credentials
import { readSettings, saveSettings, settingsId } from '../src/settings.ts';
import { profileFromEvents } from '../src/agent-profile.ts';
import { isolatedFileCredentials } from './isolated-file-credentials.ts';
import { newKey, publicKey, message, type Message } from '../src/protocol.ts';
import { newKey, publicKey, message, serializeManagement, seal, open, type Message } from '../src/protocol.ts';
import { openAIModels } from '../src/settings-models.ts';
import { host, provision, type Setup, bindingFingerprint } from '../src/host.ts';
import { createGenesis } from '../src/assignment.ts';
@@ -110,3 +111,50 @@ test('saved runtime reaches host Save/new Start while the prior active run remai
await sleep(2100); assert.equal(running.settingsRevision,2,'invalid replacement retains effective catalog');
await running.close();
});
test('oversized projected inventory retains loaded catalog and serializable reports, including on reopen', async t => {
const root = fixture(t), owner = newKey(), agent = newKey(), hostKey = publicKey(newKey());
// Long real paths repeat in each wire row; 100 short public records are not a wire budget.
const setup: Setup = { host: hostKey, ownerSecret: owner, agentSecret: agent, runner: realpathSync(process.execPath), args: [], workspace: root, allowedWorkspaces: [root, ...Array.from({length: 12}, (_, i) => join(root, `workspace-${i}-` + 'x'.repeat(120)))], serviceHome: root, configDirectory: root, mode: 'fixture' };
provision(root,setup,createGenesis(publicKey(owner),publicKey(agent),hostKey));
const reports: Message[] = [];
const transport = { binding: {host: hostKey, owner: publicKey(owner)}, validate() {}, connect() { return { ready: Promise.resolve(), send(m: Message) { serializeManagement(m); reports.push(m); }, close() {} }; } };
const running = await host(root,'ws://127.0.0.1',undefined,transport);
t.after(() => running.close());
let stored: string | null = null;
addOpenAI(root,'Fixture','synthetic',{read: () => stored, create(_r,v) { stored = v; }});
const prior = readSettings(root);
const row = {id: settingsId(), name: 'Fitting', harness: 'buzz-agent' as const, executable: realpathSync(process.execPath), providerId: prior.providers[0]!.id, model: 'm'.repeat(200)};
saveSettings(root,{...prior, runtimes: [row]},prior.revision);
await wait(() => running.settingsRevision === 2);
const fitting = reports.filter(m => m.type === 'inventory').at(-1)!;
assert.equal((fitting.body.harnessSetups as unknown[]).length,2);
// Both production codecs accept the actual fitting inventory.
assert.deepEqual(open(seal(fitting,owner),owner),JSON.parse(serializeManagement(fitting)));
assert.deepEqual(unwrapManagement(wrapManagement(fitting,Buffer.from(owner,'hex'),publicKey(agent)),Buffer.from(agent,'hex')).message,JSON.parse(serializeManagement(fitting)));
const saved = readSettings(root);
saveSettings(root,{...saved,runtimes: [...saved.runtimes,...Array.from({length: 18}, () => ({...row,id:settingsId()}))]},saved.revision);
assert.equal(readSettings(root).revision,3,'public save is retained, not falsely reported loaded');
const before = reports.length;
await sleep(2100);
assert.equal(running.settingsRevision,2);
assert.ok(reports.length > before,'prior inventory continues to publish');
assert.deepEqual(reports.filter(m => m.type === 'inventory').at(-1)!.body.harnessSetups,fitting.body.harnessSetups);
await running.close();
await assert.rejects(host(root,'ws://127.0.0.1',undefined,transport),/bounded wire size/);
assert.equal(existsSync(join(root,'host.lock')),false,'failed startup releases only its own lock');
});
test('shared management serializer preserves the exact UTF-8 wire limit for both producers', () => {
const secret = newKey(), recipient = newKey();
const m = message('inventory',publicKey(secret),publicKey(recipient),0,{ padding: '' });
const overhead = Buffer.byteLength(JSON.stringify(m));
m.body.padding = 'a'.repeat(32768-overhead);
assert.equal(Buffer.byteLength(serializeManagement(m)),32768);
assert.deepEqual(open(seal(m,secret),secret),m);
assert.deepEqual(unwrapManagement(wrapManagement(m,Buffer.from(secret,'hex'),publicKey(recipient)),Buffer.from(recipient,'hex')).message,m);
m.body.padding += 'é';
assert.throws(() => serializeManagement(m),/bounded wire size/);
assert.throws(() => seal(m,secret),/bounded wire size/);
assert.throws(() => wrapManagement(m,Buffer.from(secret,'hex'),publicKey(recipient)),/bounded wire size/);
});