feat(bus): the bus and the broker core (row 37, S2, rocko)
Rocko's round 2 candidate, approved by Darkwing (#1519 comment 26757).
build.patch 40d7e838, manifest 61519059, 24 files under packages/bus,
schema v3b (179ffe35, lead decision 60). Integration gate in a git
worktree of 942dca9e (S1 in the tree) plus the patch: bus 43/43 and
business 60/60 on Node 24 and 26, every package test and every
scripts/test-*.sh green, test-task 98/98 with the live-provider cases.
Rulings from lead decisions 62 and 63: the human proof is cooperative in
slice 1, and a self-raised cross-role decision routes to the human.
Single-use gated approvals follow in row 43.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { join } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { Store } from './store.mjs';
|
||||
import { Broker, BusError } from './broker.mjs';
|
||||
import { Credentials } from './credentials.mjs';
|
||||
import { serve } from './server.mjs';
|
||||
import { verifyHuman } from './human.mjs';
|
||||
// Trusted host API. S1 supplies resolved definitions, S6 supplies launch records.
|
||||
// Neither a business-file writer nor a socket-accessible configuration endpoint.
|
||||
export async function startBroker({ dataRoot, businesses, launches = [], readers = [], repoRoots = [] }) {
|
||||
let store, credentials, server;
|
||||
try {
|
||||
const references = {};
|
||||
for (const [business, b] of Object.entries(businesses)) {
|
||||
for (const [role, r] of Object.entries(b.roles))
|
||||
if (r.credentials) references[business + '/' + role] = r.credentials;
|
||||
if (b.tracker?.sync?.credentials) references[business + '/@sync'] = b.tracker.sync.credentials;
|
||||
}
|
||||
const protectedRoots = [
|
||||
fileURLToPath(new URL('../../../', import.meta.url)),
|
||||
...repoRoots,
|
||||
...Object.values(businesses).flatMap((b) => Object.values(b.projects ?? {}).map((p) => p.root)),
|
||||
];
|
||||
credentials = new Credentials({ references, repoRoots: protectedRoots, dataRoot });
|
||||
store = new Store(dataRoot);
|
||||
const broker = new Broker({ store, businesses, secretCheck: (value) => credentials.assertClean(value) });
|
||||
// Process identities must be supplied for every managed run when using the human transport.
|
||||
const launchRecords = [];
|
||||
function bindLaunch(r) {
|
||||
if (
|
||||
!Number.isSafeInteger(r.pid) ||
|
||||
r.pid < 2 ||
|
||||
typeof r.startTime !== 'string' ||
|
||||
!/^\d+$/.test(r.startTime)
|
||||
)
|
||||
throw new BusError('invalid-launch-process');
|
||||
const cap = broker.bindLaunch(r);
|
||||
launchRecords.push({ ...r });
|
||||
return { business: r.business, run: r.run, cap };
|
||||
}
|
||||
const bound = launches.map(bindLaunch);
|
||||
const readCaps = readers.map((business) => ({ business, cap: broker.bindReader({ business }) }));
|
||||
const path = join(store.directory, 'broker.sock');
|
||||
server = await serve({
|
||||
broker,
|
||||
path,
|
||||
authenticateHuman: (proof) => {
|
||||
const { business } = verifyHuman(proof, {
|
||||
cliPath: fileURLToPath(new URL('./human-cli.mjs', import.meta.url)),
|
||||
launches: launchRecords,
|
||||
});
|
||||
const human = businesses[business]?.human;
|
||||
if (!human) throw new BusError('unknown-business');
|
||||
return broker.bindHuman({ business, human, via: 'cli', outsideAgent: true });
|
||||
},
|
||||
});
|
||||
return {
|
||||
broker,
|
||||
credentials,
|
||||
path,
|
||||
bindLaunch,
|
||||
launches: bound,
|
||||
readers: readCaps,
|
||||
async close() {
|
||||
await server.close();
|
||||
store.close();
|
||||
credentials.close();
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
await server?.close();
|
||||
store?.close();
|
||||
credentials?.close();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user