Files
stack/packages/cli/src/transport.mjs
T
jason.woltjeandClaude Opus 5.5 2f5303c1c7 feat(cli): the mosaic CLI, broker host and decision notifier (row 39, S4, rocko)
packages/cli adds mosaic inbox, decide, tasks, agents and trail over the
human-cli transport, and mosaic bus start, stop and status as the trusted
host (unit mosaic-bus@<business>, scripts/bus-service.sh). The host boots
packages/bus/src/process.mjs, passes config.trackers from the tracker.*
variables (lead decision 70), and runs a notifier child. The notifier DMs
each open blocking decision once and sends an 08:00 America/Chicago
digest, journaled in notify/<business>/sent.jsonl at 0600 with no Discord
ids. A torn journal tail is copied aside and truncated; a malformed line,
a directory looser than 0700 or a symlinked journal refuses (lead
decision 71). packages/discord gains dmRecipient, createDm and notify.mjs.

Candidate agents/rocko/work/slice1-s4, base b9b6cf00, build.patch
b52f7d68, manifest e858504e (29 files). Darkwing approved round 2 on
#1521 (comment 26855), Filbert approved round 2 (comment 26856). The
packet's mutant table lists M28 as killed; it survived, and BUILD-LOG
records the correction.

Integration gate in a worktree on 2557e29d with the patch applied:
bus 67, business 60, cli 49, control-board 124, discord 178, ledger 78,
mosaic 69, queue 148, seat 19, tasks 51 and webui 14, all with no
failures. Conversation is 149/3, the same K1, K3 and K10 cases that fail
on the base; S4 doesn't touch the package. Every scripts/test-*.sh is
green, with test-release 14/14 and test-task 98/98 on the existing gate2
compose network. A scratch test, not in this commit, booted the real
host with trackers against S3's fake Vikunja: the adapter went ready and
a task.close on a missing task answered task-not-found after a Vikunja
read.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 07:49:38 -05:00

76 lines
2.6 KiB
JavaScript

// The human transport (lead decision 70): run
// `packages/bus/src/human-cli.mjs <socket>` as a child, write
// `{business, verb, args}` on its stdin, read the JSON reply on its stdout,
// or one bus error code on its stderr. The bus does the proof: the child
// re-executes itself with a nonce and the broker checks the process and its
// ancestry for agent markers. Nothing here carries a capability.
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
import { CliError } from "./errors.mjs";
export const HUMAN_CLI = fileURLToPath(new URL("../../bus/src/human-cli.mjs", import.meta.url));
// The broker's markers (packages/bus/src/human.mjs). The broker checks the
// whole ancestry; this check is only the clear early message for the case
// it would refuse anyway.
export const AGENT_MARKERS = Object.freeze([
"MOSAIC_BUS_CAP",
"MOSAIC_RUN_ID",
"MOSAIC_AGENT_RUN",
"CLAUDECODE",
"CLAUDE_CODE_ENTRYPOINT",
"CODEX_THREAD_ID",
"PI_AGENT_DIR",
]);
export function refuseInsideAgent(env = process.env) {
const found = AGENT_MARKERS.filter((k) => env[k]);
if (found.length > 0) {
throw new CliError(`refused: this runs only from a human shell, outside any agent run (found ${found.join(", ")} in the environment)`, 3);
}
}
// Exit codes by bus error code; anything else is invalid input (2).
const EXIT = {
"human-required": 3,
unauthenticated: 3,
"unknown-business": 3,
"read-only": 3,
"outcome-unknown": 1,
"response-too-large": 1,
"invalid-response": 1,
};
export function busExit(code) {
return EXIT[code] ?? 2;
}
// Returns a call(verb, args) for one business. `cli` and `spawn` exist for
// the tests; the command line never sets them.
export function humanTransport({ socket, business, env = process.env, cli = HUMAN_CLI, spawn = spawnSync, timeoutMs = 30000 }) {
return async function call(verb, args = {}) {
const proc = spawn(process.execPath, [cli, socket], {
input: `${JSON.stringify({ business, verb, args })}\n`,
encoding: "utf8",
env,
timeout: timeoutMs,
maxBuffer: 8 * 1024 * 1024,
});
if (proc.error || proc.status === null) throw new CliError("outcome-unknown: the bus transport did not finish", 1);
if (proc.status !== 0) {
const code = String(proc.stderr ?? "").trim().split("\n").reverse().find((l) => /^[a-z-]{1,64}$/.test(l)) ?? "invalid-response";
const error = new CliError(code, busExit(code));
error.code = code;
throw error;
}
try {
return JSON.parse(proc.stdout);
} catch {
const error = new CliError("invalid-response", 1);
error.code = "invalid-response";
throw error;
}
};
}