Files
stack/packages/bus/src/process.mjs
T
jason.woltjeandClaude Opus 5.5 f1ee9a9895 bus, business: agents land approved merges into the business working repository (row 137, #1631)
Row 137 round 4, Darkwing's candidate (agents/darkwing/work/queue-137/r4/,
build.patch 0cabe175, manifest 548ee477, 31 paths, base 63c82f1c4).
Filbert approved round 4 (comment 28809) and Dewey approved it (28816)
after round 3 changes (28801, 28805). Darkwing's round 4 ran 9/9 mutant
kills, bus 317, seat 150, land-work 86.

Round 5 is a rebase only (agents/sage/work/queue-137-r5/, build.patch
d6cbaaa8, manifest f73598fe). Row 150 changed docs/TOOLS.md, broker.mjs
and cli/README.md under it. The round 4 patch applied unchanged onto
435d1c616, and with index and @@ lines removed the two patches are
byte-identical. Filbert checked the interdiff (comment 28818).

A landing moves refs/heads/main in <dataRoot>/working/<business>/
<project>.git, never the project root or canonical refactor. The seed now
fetches the root as refs/heads/root and fast-forwards main only when it
is an ancestor, so a landed main survives the next push. Only a
cross-role merge decision lands. Its squash names the real channel:
"via the CLI" for the human, and "as its arbiter, via the broker" for an
arbiter's decision.resolve. Every other class refuses not-authorized
before the human check.

This carries row 139's two items. REQ-LAND-1: a gated merge decision
never lands. REQ-DEC-2: the host land op accepts an arbiter-resolved
merge. land-work's read.mjs stays human-only, and land-work.sh still
accepts only a human CLI resolution.

Sage's landing gate ran on 435d1c616 plus the candidate: 25 steps,
all rc 0 (15 packages, 10 script suites), 20:51:00Z to 20:55:34Z,
network sage-r137_default removed. Cherry-picked onto 5d98fb194, which
added only a docs/SESSIONS.md line.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-16 15:56:00 -05:00

111 lines
4.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',
'checkPush',
'recordPush',
'reviewFetch',
'checkLand',
'recordLand',
'landRefused',
'landHeld',
'threadsState',
'threadIdle',
]);
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, m.subject);
if (m.op === 'endLaunch') return runtime.endLaunch(m.record);
// Row 93 (#1587): the git socket's ops.
if (m.op === 'checkPush') return runtime.checkPush(m.cap, m.task_ref);
if (m.op === 'recordPush') return runtime.recordPush(m.cap, m.push);
if (m.op === 'reviewFetch') return runtime.reviewFetch(m.cap, m.args);
// Row 137 (#1631): the git socket's land op, and the launcher's question whether to bind it.
if (m.op === 'checkLand') return runtime.checkLand(m.cap, m.args);
if (m.op === 'recordLand') return runtime.recordLand(m.ticket, m.land);
if (m.op === 'landRefused') return runtime.landRefused(m.ticket, m.args);
if (m.op === 'landHeld') return runtime.landHeld(m.record);
// Row 121 (#1615): the launcher's view of the business's threads, and its idle release.
if (m.op === 'threadsState') return runtime.threadsState(m.business);
if (m.op === 'threadIdle') return runtime.threadIdle(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));
}