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, packet9f1563ec1), BASEbadfa52b9. 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:
@@ -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)));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user