Slice 1 S6. packages/harness runs a role session under the pi or claude
adapter behind a tool gate, and packages/seat holds the session and its
process. The bus host gains a launch socket: `mosaic bus start
<business> --pm <harness>:<model>` runs the PM without a window, the PM
launches role sessions through the broker within the business file's
`launch.max`, and every launch, refusal and end is a launch-log line and
a broker event. `mosaic talk`, `mosaic launches off|on|list` and
`mosaic stop <run>` are new CLI verbs. packages/bus gains the IPC ops
identity, authorize, refuse, endLaunch and credentialStatus, plus
Broker.endLaunch (decision 77); no socket verb, event kind or schema
changed. The session environment is an allowlist, and the runner exits
before claiming if a founder credential reaches it.
Filbert built it (agents/filbert/work/s6). Darkwing reviewed on #1523:
round 1 changes (26991/26995), round 2 changes (27004/27010), round 3
changes (27012/27032), round 4 approve (27033/27034) on manifest
08a78972, 42 paths.
Sage's gate on 64b0f9f1 (12:32Z), sequential and teed: node suites
business 60, bus 74, cli 83, control-board 124, conversation 182,
discord 178, harness 56, ledger 78, mosaic 69, queue 149, runs 41,
seat 27, tasks 51, webui 39, every one 0 failed; test-auth 15,
test-conductor 17, test-config 24, test-discord 66,
test-extension-package 18, test-foundation 44, test-queue 27,
test-release 14, test-task 98 and 26 without Docker, all 0 failed.
64b0f9f1..ee7f909b touches none of the 42 paths. It landed after the
Q14 hold ended at 2026-10-11T17:19:23.857Z. The recorded PM-to-coder
run on a scratch data root is still owed before row 41 moves to done.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
116 lines
4.3 KiB
JavaScript
116 lines
4.3 KiB
JavaScript
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 and ends them.
|
|
// Neither a business-file writer nor a socket-accessible configuration endpoint.
|
|
// `tasks` is S3's adapter factory: ({broker, credentials, businesses}) => {handle, timeout, close}.
|
|
export async function startBroker({
|
|
dataRoot,
|
|
businesses,
|
|
launches = [],
|
|
readers = [],
|
|
repoRoots = [],
|
|
tasks = null,
|
|
}) {
|
|
let store, credentials, server, adapter;
|
|
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 }) }));
|
|
if (tasks) {
|
|
adapter = await tasks({ broker, credentials, businesses });
|
|
if (typeof adapter?.handle !== 'function' || !Number.isSafeInteger(adapter.timeout) || adapter.timeout < 1)
|
|
throw new BusError('invalid-adapter');
|
|
}
|
|
const path = join(store.directory, 'broker.sock');
|
|
server = await serve({
|
|
tasks: adapter ? { handle: adapter.handle, timeout: adapter.timeout } : null,
|
|
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 });
|
|
},
|
|
});
|
|
// S6 launcher operations. The host decides capacity and the instance list; the broker
|
|
// authorizes role.launch and keeps the evidence, refusals included.
|
|
function authorizeLaunch(cap, instance) {
|
|
try {
|
|
return broker.authorize(cap, 'role.launch', { target: instance });
|
|
} catch (e) {
|
|
if (!(e instanceof BusError)) throw e;
|
|
broker.refuse(cap, e.code);
|
|
throw e;
|
|
}
|
|
}
|
|
// Metadata only (service, date, state); the token stays in Credentials.
|
|
function credentialStatus(business, role) {
|
|
return credentials
|
|
.status()
|
|
.filter((c) => c.instance === business + '/' + role)
|
|
.map(({ service, date, state }) => ({ service, date, state }));
|
|
}
|
|
return {
|
|
broker,
|
|
credentials,
|
|
path,
|
|
bindLaunch,
|
|
endLaunch: (r) => broker.endLaunch(r),
|
|
identity: (cap) => broker.identity(cap),
|
|
authorizeLaunch,
|
|
refuse: (cap, code) => broker.refuse(cap, code),
|
|
credentialStatus,
|
|
launches: bound,
|
|
readers: readCaps,
|
|
async close() {
|
|
await server.close();
|
|
await adapter?.close?.();
|
|
store.close();
|
|
credentials.close();
|
|
},
|
|
};
|
|
} catch (e) {
|
|
await server?.close();
|
|
await adapter?.close?.();
|
|
store?.close();
|
|
credentials?.close();
|
|
throw e;
|
|
}
|
|
}
|