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]>
84 lines
3.2 KiB
JavaScript
84 lines
3.2 KiB
JavaScript
// Started with fork() by the trusted host; boot refs/capabilities travel over IPC,
|
|
// never command arguments, stdout or service-token-bearing environment variables.
|
|
import { startBroker } from './runtime.mjs';
|
|
import { BusError } from './broker.mjs';
|
|
const LAUNCH_OPS = new Set(['identity', 'authorizeLaunch', 'refuse', 'endLaunch', 'credentialStatus']);
|
|
function launchOp(m) {
|
|
if (m.op === 'identity') return runtime.identity(m.cap);
|
|
if (m.op === 'authorizeLaunch') return runtime.authorizeLaunch(m.cap, m.instance);
|
|
if (m.op === 'refuse') return runtime.refuse(m.cap, m.code);
|
|
if (m.op === 'endLaunch') return runtime.endLaunch(m.record);
|
|
return runtime.credentialStatus(m.business, m.role);
|
|
}
|
|
// The host's request id comes back on the reply, so a late reply can't
|
|
// answer a later request (host.mjs). A message without one gets none.
|
|
const tag = (message, reply) => (Number.isSafeInteger(message?.id) ? { ...reply, id: message.id } : reply);
|
|
let runtime,
|
|
booted = false,
|
|
closing = false;
|
|
async function close(code) {
|
|
if (closing) return;
|
|
closing = true;
|
|
try {
|
|
await runtime?.close();
|
|
} catch {
|
|
code = 2;
|
|
}
|
|
process.exitCode = code;
|
|
if (process.connected) process.disconnect();
|
|
}
|
|
if (!process.send) {
|
|
process.stderr.write('trusted-host-required\n');
|
|
process.exitCode = 2;
|
|
} else {
|
|
const timer = setTimeout(() => close(2), 10000);
|
|
process.on('message', async (message) => {
|
|
try {
|
|
if (message?.op === 'bindLaunch' && runtime) {
|
|
try {
|
|
process.send(tag(message, { ok: true, launch: runtime.bindLaunch(message.record) }));
|
|
} catch (e) {
|
|
process.send(tag(message, { ok: false, error: e instanceof BusError ? e.code : 'bind-refused' }));
|
|
}
|
|
return;
|
|
}
|
|
// S6 launcher ops, one reply each, like bindLaunch.
|
|
if (LAUNCH_OPS.has(message?.op) && runtime) {
|
|
try {
|
|
process.send(tag(message, { ok: true, result: launchOp(message) }));
|
|
} catch (e) {
|
|
process.send(tag(message, { ok: false, error: e instanceof BusError ? e.code : 'launch-op-refused' }));
|
|
}
|
|
return;
|
|
}
|
|
if (message?.op === 'close') {
|
|
clearTimeout(timer);
|
|
await close(0);
|
|
return;
|
|
}
|
|
if (message?.op !== 'boot' || booted) throw new BusError('invalid-host-request');
|
|
booted = true;
|
|
clearTimeout(timer);
|
|
// `trackers` is S3's plain-data boot config; the adapter is loaded only when a business has one.
|
|
const { trackers, ...config } = message.config ?? {};
|
|
if (trackers) {
|
|
const { tasksAdapter } = await import('../../tasks/src/adapter.mjs');
|
|
config.tasks = tasksAdapter({ trackers });
|
|
}
|
|
runtime = await startBroker(config);
|
|
if (closing) {
|
|
await runtime.close();
|
|
return;
|
|
}
|
|
process.send({ ok: true, path: runtime.path, launches: runtime.launches, readers: runtime.readers });
|
|
} catch (e) {
|
|
process.send?.(tag(message, { ok: false, error: e instanceof BusError ? e.code : 'startup-refused' }), () =>
|
|
close(2),
|
|
);
|
|
}
|
|
});
|
|
process.on('disconnect', () => close(2));
|
|
process.on('SIGTERM', () => close(0));
|
|
process.on('SIGINT', () => close(0));
|
|
}
|