bus: tester identity for PM tests (row 110, #1604)

A tester binds with a process proof from the bus's own tester-cli.mjs
when the business var bus.tester is true. It may send [mos test] messages
to a role, receive replies on the tester address and use the read verbs;
every other verb refuses. Messages store from_role tester and write no
human.input event. turnRequest() marks them as never Jason's instruction.
human.mjs is unchanged.

Candidate manifest bdaf7f476abd0b2b5095ab2f7f43e38540e6ba48ee14a02a4f4995a1469b79df
(Darkwing round 2, packet 9f1563ec1), BASE badfa52b9. Approved by Filbert
(comment 28360) and Sage (comment 28361). Sage gate rerun passed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-13 20:43:40 -05:00
co-authored by Claude Opus 5.5
parent 67ac176f2a
commit 179697a8a3
19 changed files with 822 additions and 41 deletions
+40 -5
View File
@@ -8,12 +8,13 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawnSync } from "node:child_process";
import { spawn, spawnSync } from "node:child_process";
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { basename, dirname, join, resolve } from "node:path";
import { Broker } from "../../bus/src/broker.mjs";
import { serve } from "../../bus/src/server.mjs";
import { verifyTester } from "../../bus/src/tester.mjs";
import { Store } from "../../bus/src/store.mjs";
import { SOURCES } from "../../harness/src/bundle.mjs";
import { startTimeOf } from "../src/proc.mjs";
@@ -21,6 +22,8 @@ import { newRun, RUNNER, runnerOf, sessionEnv, spawnSession, writeSessionFile }
import { binds, gitIdentity, prepareRun, REPO, RUNNER_FILES, sandboxArgv, sandboxProblem } from "../src/sandbox.mjs";
const FAKE = new URL("../../harness/tests/fixtures/fake-adapter.mjs", import.meta.url).pathname;
const TESTER_CLI = new URL("../../bus/src/tester-cli.mjs", import.meta.url).pathname;
const HUMAN_MJS = new URL("../../bus/src/human.mjs", import.meta.url).pathname;
const skip = sandboxProblem() ?? false;
function scratch(t) {
@@ -176,8 +179,9 @@ async function wrong(table, sh) {
// A real session for coder, its adapter a shell that runs each message's
// body, or the fake adapter for a TOOL message.
// expose(standIns) names extra read-only binds, for the test that breaks
// the sandbox on purpose.
async function session(t, { expose = () => [] } = {}) {
// the sandbox on purpose. tester (row 110) turns bus.tester on, serves the
// tester proof as the bus runtime does, and binds tester-cli.mjs in.
async function session(t, { expose = () => [], tester = false } = {}) {
const root = scratch(t);
const dataRoot = join(root, "data");
mkdirSync(dataRoot, { mode: 0o700 });
@@ -189,6 +193,7 @@ async function session(t, { expose = () => [] } = {}) {
id: "acme",
human: "jason",
arbiters: { technical: "pm", delivery: "pm" },
...(tester ? { vars: { "bus.tester": true } } : {}),
roles: {
pm: { authority: { withinRole: ["message.send"], crossRole: [] } },
coder: { authority: { withinRole: ["message.send"], crossRole: [] } },
@@ -197,7 +202,9 @@ async function session(t, { expose = () => [] } = {}) {
},
});
const brokerSocket = join(root, "bus.sock");
const server = await serve({ broker, path: brokerSocket });
const launches = [];
const authenticateTester = (proof) => broker.bindTester(verifyTester(proof, { cliPath: TESTER_CLI, launches }));
const server = await serve({ broker, path: brokerSocket, ...(tester ? { authenticateTester } : {}) });
t.after(async () => {
await server.close().catch(() => {});
store.close();
@@ -229,7 +236,7 @@ async function session(t, { expose = () => [] } = {}) {
toolSocket: join(runDir, "tools.sock"), turnMarker: join(runDir, "turn.done"), pollInterval: 50,
});
const paths = { workspace: ws, adapter, harness: "pi", brokerSocket, readOnly: [FAKE, ...expose(s)] };
const paths = { workspace: ws, adapter, harness: "pi", brokerSocket, readOnly: [FAKE, ...(tester ? [TESTER_CLI, HUMAN_MJS] : []), ...expose(s)] };
const child = spawnSession({ runDir, env: sessionEnv({ HOME: process.env.HOME, PATH: process.env.PATH }, run), paths, business: "acme", role: "coder" });
const exited = new Promise((r) => child.once("close", (code, signal) => r({ code, signal })));
t.after(async () => {
@@ -240,6 +247,7 @@ async function session(t, { expose = () => [] } = {}) {
});
await runnerOf(child);
const cap = broker.bindLaunch({ business: "acme", role: "coder", run, harness: "pi", pid: child.pid, startTime: startTimeOf(child.pid) });
launches.push({ pid: child.pid, startTime: startTimeOf(child.pid) });
const pm = broker.bindLaunch({ business: "acme", role: "pm", run: "pm-run", harness: "pi" });
broker.request(pm, { verb: "role.claim" });
child.stdin.end(`${JSON.stringify({ cap })}\n`);
@@ -296,3 +304,30 @@ test("a bind that exposes a stand-in, or a missing workspace bind, fails the mat
assert.deepEqual(await wrong(checks(x), direct(all)), []);
assert.deepEqual(await wrong(checks(x), direct(all.filter((b) => b.src !== x.ws))), ["write and read its workspace"]);
});
// Row 110 (#1604): a role session that runs tester-cli.mjs against its own
// broker socket can't bind as tester. Inside bwrap the shim's pid is the
// session's PID namespace's, so the broker's nonce check on the host fails,
// and the walk would reach the session's launch record. The same shim from
// the test process, outside any run, binds: the refusal is the proof's.
test("a confined session that runs tester-cli.mjs is refused as tester, and the same shim outside the sandbox binds", { skip, timeout: 60000 }, async (t) => {
const x = await session(t, { tester: true });
const request = JSON.stringify({ business: "acme", verb: "agents", args: {} });
const inside = await x.sh(`printf '%s' '${request}' | '${process.execPath}' '${TESTER_CLI}' '${x.brokerSocket}'`);
assert.deepEqual(inside, { rc: 2, out: "tester-required" });
const env = { ...process.env };
delete env.MOSAIC_BUS_TESTER_NONCE;
delete env.MOSAIC_BUS_CLI_NONCE;
const outside = await new Promise((done) => {
const child = spawn(process.execPath, [TESTER_CLI, x.brokerSocket], { env });
let out = "";
let err = "";
child.stdout.on("data", (d) => (out += d));
child.stderr.on("data", (d) => (err += d));
child.on("close", (status) => done({ status, out, err }));
child.stdin.end(request);
});
assert.equal(outside.status, 0, outside.err);
assert.ok(Array.isArray(JSON.parse(outside.out)));
});