Files
stack/packages/seat/tests/sandbox.test.mjs
jason.woltje 87db40e0cd fix(cli,seat,discord,organization,control-board,harness,plugins,webui): review round: launcher reasons, session, discord reads, mosaic-config and keys by label (#1873)
Review "changes 8" fix round for org-redact6.

- launcher: a harness module's throw names its system code or class,
  never its message; preflight, loader, bundle, checkout, sandbox and
  route text is relayed with host paths replaced (relay()).
- claude-code and pi preflights name MOSAIC_CLAUDE_BIN and the
  repository's node_modules, never a path; persona and skill bundle
  errors and sandboxProblem name their file or the sandbox by label.
- seat session: thread store, legacy store, overlap, busy lock and
  newRun refusals name the place under the data root.
- discord: unknown keys, binding names and turn ids are described when
  not plain; approvals, journal and repository read errors name the file
  and code; git drops stderr lines that carry a host path and a spawn
  failure names its code only.
- seat, control-board: registration fields and seen keys are described
  when not plain; invalid seat name and layout state the constraint.
- organization: the registry names the config directory and registry
  file by label; --project is quoted only when it is a plain name;
  systemConfigProblem moves to its own module.
- scripts/mosaic-config.mjs: the config by where it came from, the data
  root and its marker by role, keys and arguments only when plain, and
  error codes in place of raw messages. Exit codes unchanged.
- webui sso-config: an unknown console key is named only when plain.

Sentinel tests in each package's path-redaction-labels file fail on
BASE; pins that quoted a path now assert the label and its absence.
2026-11-11 08:53:31 -06:00

430 lines
25 KiB
JavaScript

// The sandbox a role session runs in (src/sandbox.mjs, #1586). A real
// session, the runner in its bwrap sandbox against an in-process broker,
// answers each message by running the body in /bin/sh; the tests read the
// answers. Stand-ins for the founder's files sit in the test's own
// directory, plus a file in the host's /dev/shm and a SysV shared memory
// segment, each checked to exist on the host first, so a refusal means the
// sandbox hid it.
import { test } from "node:test";
import assert from "node:assert/strict";
import { spawn, spawnSync } from "node:child_process";
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, statSync, symlinkSync, 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 { createLoader } from "../../plugins/src/loader.mjs";
import { startTimeOf } from "../src/proc.mjs";
import { newRun, RUNNER, runnerOf, sessionEnv, spawnSession, writeSessionFile } from "../src/session.mjs";
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;
async function harnessModule(name) {
const loader = createLoader({ log: () => {} });
const { refused } = await loader.load({ harness: [name] });
if (refused.length) throw new Error(`harness ${name} refused: ${JSON.stringify(refused)}`);
return loader.modules("harness")[0];
}
// The confined sessions below run pi, so they get the pi module's mounts.
const PI_MOUNTS = (await harnessModule("pi")).mounts({ env: {} });
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) {
const root = realpathSync(mkdtempSync(join(tmpdir(), "mosaic-sandbox-")));
t.after(() => rmSync(root, { recursive: true, force: true }));
return root;
}
test("RUNNER_FILES is exactly what the runner and the harness sources it starts import", () => {
const seen = new Set();
const walk = (file) => {
if (seen.has(file)) return;
seen.add(file);
for (const [, rel] of readFileSync(file, "utf8").matchAll(/(?:from|import\()\s*["'](\.\.?\/[^"']+)["']/g)) walk(resolve(dirname(file), rel));
};
for (const file of [RUNNER, ...Object.values(SOURCES)]) walk(file);
assert.deepEqual([...seen].sort(), [...RUNNER_FILES].sort());
});
test("row 331 mounts only the parsing validator and fixed marker code, never proxy state", (t) => {
const root = scratch(t);
const list = binds({ runDir: join(root, "run"), workspace: join(root, "ws"), adapter: join(root, "a.sh"), brokerSocket: join(root, "bus.sock") });
for (const relative of ["packages/model-proxy/src/validation.mjs", "packages/harness/src/first-output.mjs"]) {
const src = join(REPO, relative);
assert.deepEqual(list.filter(b => b.src === src), [{ kind: "ro", src, why: "a repository file the runner loads" }]);
}
assert.deepEqual(list.filter(b => b.src?.startsWith(join(REPO, "packages/model-proxy"))).map(b => b.src), [join(REPO, "packages/model-proxy/src/validation.mjs")]);
assert.equal(list.some(b => b.src === REPO || b.src === join(REPO, "packages/harness/src")), false);
});
test("sandboxProblem names a missing bwrap and a bwrap that fails", (t) => {
const root = scratch(t);
// #1873: the sandbox by name, never the binary's host path.
assert.equal(sandboxProblem({ bwrap: join(root, "nope") }), "the sandbox (bwrap) is missing");
const bad = join(root, "bwrap");
writeFileSync(bad, "#!/bin/sh\necho 'bwrap: No permissions to create new namespace' >&2\nexit 1\n");
chmodSync(bad, 0o755);
assert.equal(sandboxProblem({ bwrap: bad }), "the sandbox (bwrap) failed: bwrap: No permissions to create new namespace");
if (!skip) assert.equal(sandboxProblem(), null);
});
test("every bind says why; the bundle and session file stay read-only over the run directory", (t) => {
const root = scratch(t);
const runDir = join(root, "run");
const list = binds({ runDir, workspace: join(root, "ws"), adapter: join(root, "a.sh"), brokerSocket: join(root, "bus.sock") });
assert.ok(list.every((b) => b.why));
const at = (src) => list.findIndex((b) => b.src === src);
assert.equal(list[at(runDir)].kind, "rw");
for (const f of ["bundle", "session.json"]) {
assert.equal(list[at(join(runDir, f))].kind, "ro");
assert.ok(at(join(runDir, f)) > at(runDir), `${f} is mounted over the run directory`);
}
assert.deepEqual(list.at(-1), { kind: "rw", src: join(runDir, "tmp"), dest: "/tmp", why: list.at(-1).why });
assert.equal(at(join(root, "launch.sock")), -1, "no launch socket for a role that may not launch");
assert.equal(at(join(root, "git.sock")), -1, "no git socket for a role that neither pushes nor reviews");
assert.ok(!list.some((b) => b.src === realpathSync(join(REPO, "node_modules"))), "no harness mounts unless the module names them");
});
// #1873 A3: a checkout the launch may not see is covered by an empty tmpfs after the workspace bind,
// at the path the session sees; a path outside the workspace is refused, never mounted.
test("A3 a hidden checkout is a tmpfs over the workspace bind; a path outside the workspace, a symlink or a file refuses", (t) => {
const root = scratch(t);
const workspace = join(root, "ws");
mkdirSync(join(workspace, "web"), { recursive: true });
mkdirSync(join(workspace, "stack"));
const args = { runDir: join(root, "run"), workspace, adapter: join(root, "a.sh"), brokerSocket: join(root, "bus.sock") };
const list = binds({ ...args, hidden: [join(workspace, "web")] });
const ws = list.findIndex((b) => b.src === workspace);
const hide = list.findIndex((b) => b.kind === "tmpfs");
assert.ok(ws >= 0 && hide > ws, "the tmpfs comes after the workspace bind");
assert.equal(list[hide].dest, join(workspace, "web"));
assert.ok(list[hide].why);
const moved = binds({ ...args, workspaceDest: "/work", hidden: [join(workspace, "web")] });
assert.equal(moved.find((b) => b.kind === "tmpfs").dest, "/work/web");
assert.equal(binds(args).some((b) => b.kind === "tmpfs"), false);
for (const bad of [workspace, join(root, "other"), join(workspace, "..", "ws2")]) {
assert.throws(() => binds({ ...args, hidden: [bad] }), /is not inside the workspace/, bad);
}
// A tmpfs at a symlink would mount over its target, so only a real directory is covered.
symlinkSync(join(workspace, "stack"), join(workspace, "linked"));
writeFileSync(join(workspace, "file"), "x\n");
for (const bad of [join(workspace, "linked"), join(workspace, "file"), join(workspace, "missing")]) {
assert.throws(() => binds({ ...args, hidden: [bad] }), /is not a directory/, bad);
}
});
// Row 65: the harness module names its own mounts; binds() mounts each read-only, after the
// adapter and before the broker socket, and names no harness itself.
test("row 65 a harness module's mounts bind read-only after the adapter, each with the module's reason", async (t) => {
const root = scratch(t);
const args = { runDir: join(root, "run"), workspace: join(root, "ws"), adapter: join(root, "a.sh"), brokerSocket: join(root, "bus.sock") };
const pi = await harnessModule("pi");
const list = binds({ ...args, harnessMounts: pi.mounts({ env: {} }) });
const at = (src) => list.findIndex((b) => b.src === src);
const modules = list[at(realpathSync(join(REPO, "node_modules")))];
assert.deepEqual(modules, { kind: "ro", src: realpathSync(join(REPO, "node_modules")), dest: join(REPO, "node_modules"), why: "pi and its packages, on the session's PATH" });
assert.ok(at(join(root, "a.sh")) < at(modules.src) && at(modules.src) < at(join(root, "bus.sock")));
const fixture = binds({ ...args, harnessMounts: [{ src: join(root, "tool"), why: "a fixture harness's tool" }] });
assert.deepEqual(fixture.find((b) => b.src === join(root, "tool")), { kind: "ro", src: join(root, "tool"), why: "a fixture harness's tool" });
// Row 66 (#1559): pi also mounts the unslop extension's three files, each read-only at its own path.
for (const f of ["extension.ts", "unslop-check.js", "lists.json"]) {
const src = join(REPO, "tools", "unslop-hook", f);
assert.deepEqual(list.find((b) => b.src === src), { kind: "ro", src, why: "the unslop check the session's second extension runs" }, f);
}
const stream = join(REPO, "packages", "plugins", "harness", "pi", "model-stream.mjs");
assert.deepEqual(list.find((b) => b.src === stream), { kind: "ro", src: stream, why: "the managed model event stream" });
assert.equal(binds(args).length, list.length - 5);
});
// Row 121 (#1615): a thread run gets only its own thread's two directories,
// writable, and a home/.claude/projects mount point made before the sandbox starts.
test("REQ-THREAD-2 a thread store binds its session directory and Claude transcripts, and nothing more", (t) => {
const root = scratch(t);
const runDir = join(root, "run");
const store = { dir: join(root, "threads", "acme", "th"), session: join(root, "threads", "acme", "th", "session"), projects: join(root, "threads", "acme", "th", "claude-projects") };
const args = { runDir, workspace: join(root, "ws"), adapter: join(root, "a.sh"), brokerSocket: join(root, "bus.sock") };
const list = binds({ ...args, threadStore: store });
const plain = binds(args);
assert.deepEqual(list.slice(0, plain.length), plain, "the other binds are unchanged");
assert.deepEqual(list.slice(plain.length).map(({ kind, src, dest }) => ({ kind, src, dest })), [
{ kind: "rw", src: store.session, dest: undefined },
{ kind: "rw", src: store.projects, dest: join(runDir, "home", ".claude", "projects") },
]);
assert.ok(list.every((b) => b.why));
assert.equal(list.findIndex((b) => b.src === store.dir || b.src === join(root, "threads")), -1, "not the store's parent, not other threads");
prepareRun(runDir);
assert.equal(existsSync(join(runDir, "home", ".claude")), false);
prepareRun(runDir, { threadStore: store });
assert.equal(statSync(join(runDir, "home", ".claude", "projects")).mode & 0o777, 0o700);
});
// Row 93 (#1587): the git socket is bound read-only, before the workspace, when the launcher passes one.
test("the host's git socket is bound only when given, read-only and before the writable binds", (t) => {
const root = scratch(t);
const args = { runDir: join(root, "run"), workspace: join(root, "ws"), adapter: join(root, "a.sh"), brokerSocket: join(root, "bus.sock") };
const list = binds({ ...args, gitSocket: join(root, "git.sock") });
const at = (src) => list.findIndex((b) => b.src === src);
assert.equal(list[at(join(root, "git.sock"))].kind, "ro");
assert.match(list[at(join(root, "git.sock"))].why, /push_working and fetch_review/);
assert.ok(at(join(root, "git.sock")) < at(join(root, "ws")));
assert.equal(binds(args).findIndex((b) => b.src === join(root, "git.sock")), -1);
assert.ok(RUNNER_FILES.includes(join(REPO, "packages/harness/src/git-tools.mjs")));
});
// Row 104 (#1598): the git identity comes from the business and the role
// instance, both checked as ids, and sandboxArgv sets it for every session.
test("the git identity is the instance's, at <instance>@<business>.mosaic.invalid, and sandboxArgv refuses without it", (t) => {
const email = "[email protected]";
assert.deepEqual(gitIdentity({ business: "acme", role: "coder" }), { GIT_AUTHOR_NAME: "coder", GIT_AUTHOR_EMAIL: email, GIT_COMMITTER_NAME: "coder", GIT_COMMITTER_EMAIL: email });
for (const [business, role] of [[undefined, "coder"], ["acme", undefined], ["acme", ""], ["Acme", "coder"], ["acme", "coder\nx"], ["acme", "coder>"], ["acme", 7]]) {
assert.throws(() => gitIdentity({ business, role }), /^Error: git identity: (business|role) .* isn't an id$/, `${business}/${role}`);
}
const root = scratch(t);
const runDir = join(root, "run");
const list = binds({ runDir, workspace: join(root, "ws"), adapter: join(root, "a.sh"), brokerSocket: join(root, "bus.sock") });
const argv = sandboxArgv(list, runDir, ["/bin/true"], { business: "acme", role: "coder" });
const set = {};
argv.slice(0, argv.indexOf("--")).forEach((a, i, all) => {
if (a === "--setenv") set[all[i + 1]] = all[i + 2];
});
assert.deepEqual(set, { HOME: join(runDir, "home"), TMPDIR: "/tmp", ...gitIdentity({ business: "acme", role: "coder" }) });
assert.throws(() => sandboxArgv(list, runDir, ["/bin/true"], {}), /git identity: business undefined isn't an id/);
});
// The founder's files and the other sessions' state, as stand-ins. The
// /dev/shm file and the SysV segment are the host's, made for this test and
// removed after it.
function standIns(t, root, dataRoot) {
const s = {
secret: join(root, "home", ".config", "mosaic-dev", "secrets", "stand-in.env"),
mosaic: join(root, "home", ".mosaic", "fleet", "token"),
otherWs: join(root, "ws-other"),
shm: join("/dev/shm", basename(root)),
};
writeFileSync(s.shm, "stand-in\n", { mode: 0o600 });
t.after(() => rmSync(s.shm, { force: true }));
const ipc = spawnSync("/usr/bin/ipcmk", ["-M", "4096"], { encoding: "utf8" });
assert.equal(ipc.status, 0, ipc.stderr);
s.segment = Number(ipc.stdout.trim().split(" ").at(-1));
t.after(() => spawnSync("/usr/bin/ipcrm", ["-m", String(s.segment)]));
for (const f of [s.secret, s.mosaic]) {
mkdirSync(dirname(f), { recursive: true, mode: 0o700 });
writeFileSync(f, "stand-in\n", { mode: 0o600 });
}
mkdirSync(s.otherWs);
writeFileSync(join(s.otherWs, "notes.md"), "another instance's work\n");
const other = newRun(dataRoot, "acme");
writeFileSync(join(other.runDir, "session.json"), "{}\n", { mode: 0o600 });
s.otherRun = other.runDir;
return s;
}
// [what, shell command, may the session do it, the output it must give].
function checks({ s, ws, runDir, run, dataRoot, store }) {
return [
["HOME is the run's home/, empty", `printf %s "$HOME:"; ls -A "$HOME"`, true, `${join(runDir, "home")}:`],
["TMPDIR is the private /tmp", `printf %s "$TMPDIR"`, true, "/tmp"],
["read a stand-in ~/.config/mosaic-dev/secrets", `cat '${s.secret}'`, false],
["read a stand-in ~/.mosaic", `cat '${s.mosaic}'`, false],
["read another instance's workspace", `cat '${s.otherWs}/notes.md'`, false],
["read another run directory", `cat '${s.otherRun}/session.json'`, false],
["read the data root's store", `cat '${store}' >/dev/null`, false],
["the data root holds only this run", `ls -A '${dataRoot}'; ls -A '${dataRoot}/launches/acme'`, true, `launches\n${run}`],
["read the checkout outside the runner's files", `cat '${join(REPO, "AGENTS.md")}' >/dev/null`, false],
["read the seat's own source", `cat '${join(REPO, "packages", "seat", "src", "session.mjs")}' >/dev/null`, false],
["read a stand-in in the host's /dev/shm", `cat '${s.shm}'`, false],
["see the test's own pid in /proc", `test -e /proc/${process.pid}`, false],
["read its own IPC namespace's segment list", "test -r /proc/sysvipc/shm", true],
["see a SysV segment the host made", `awk '$2 == ${s.segment} { f = 1 } END { exit !f }' /proc/sysvipc/shm`, false],
["write to /", "touch /probe", false],
["write beside the workspace", `touch '${dirname(ws)}/probe'`, false],
["write in the data root", `touch '${dataRoot}/probe'`, false],
["write a runner file", `test -w '${RUNNER}'`, false],
["write the run's bundle", `test -w '${runDir}/bundle/prompt.md' || touch '${runDir}/bundle/probe'`, false],
["write the session file", `test -w '${runDir}/session.json'`, false],
["write and read its workspace", `echo ws > '${ws}/probe' && cat '${ws}/probe'`, true, "ws"],
["write and read its run directory", `echo run > '${runDir}/probe' && cat '${runDir}/probe'`, true, "run"],
["write and read /tmp", "echo tmp > /tmp/probe && cat /tmp/probe", true, "tmp"],
["run node", `'${process.execPath}' -e 'console.log(6 * 7)'`, true, "42"],
["run pi, the session's harness", `'${join(REPO, "node_modules", ".bin", "pi")}' --version >/dev/null`, true],
// Row 104 (#1598): a commit in a clone with no user config carries the
// instance's identity from the environment, and the clone's config
// gains no user key.
["commit in a fresh clone as the instance", `d=$(mktemp -d) && cd "$d" && git init -q o && git -C o commit --allow-empty -q -m base && git clone -q o c && cd c && echo x > f && git add f && git commit -q -m m && ! git config --file .git/config --get-regexp '^user\\.' && git log -1 --format='%an <%ae>|%cn <%ce>'`, true, "coder <[email protected]>|coder <[email protected]>"],
];
}
// The checks whose answer isn't the expected one, by name.
async function wrong(table, sh) {
const out = [];
for (const [what, cmd, may, expect] of table) {
const r = await sh(cmd);
if ((r.rc === 0) !== may || (may && expect !== undefined && r.out !== expect)) out.push(what);
}
return out;
}
// 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. 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 });
const store = new Store(dataRoot);
const broker = new Broker({
store,
businesses: {
acme: {
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: [] } },
},
},
},
});
const brokerSocket = join(root, "bus.sock");
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();
});
const { run, runDir } = newRun(dataRoot, "acme");
const s = standIns(t, root, dataRoot);
const bundle = join(runDir, "bundle");
const ws = join(root, "ws");
mkdirSync(bundle, { mode: 0o700 });
mkdirSync(ws);
writeFileSync(join(bundle, "prompt.md"), "# test\n");
writeFileSync(join(bundle, "policy.json"), JSON.stringify({ harness: "pi", workspace: ws, tools: [], typed: [], network: "none", needs: [], credentials: [] }));
writeFileSync(join(bundle, "tools.json"), "[]");
const adapter = join(root, "adapter.sh");
writeFileSync(adapter, [
// Row 109 (#1603): a state block opens the request, so the body is what follows the "Message ...:" header and its blank line.
`body=$(printf '%s\\n' "$MOSAIC_REQUEST" | sed '1,/^Message .*:$/d' | sed '1d')`,
`case $body in TOOL\\ *) exec '${process.execPath}' '${FAKE}' ;; esac`,
`out=$(cd "$MOSAIC_WORKSPACE" && /bin/sh -c "$body" 2>&1); rc=$?`,
`printf 'rc=%s\\n%s' "$rc" "$out"`,
`echo done > "$MOSAIC_TURN_MARKER"`,
"",
].join("\n"), { mode: 0o644 });
writeSessionFile(runDir, {
business: "acme", instance: "coder", run, launchedBy: "pm", harness: "pi", provider: "fake", model: "fake-model",
brokerSocket, launchSocket: join(root, "launch.sock"), workspace: ws, sessionDir: join(runDir, "session"), adapter,
bundle: { prompt: join(bundle, "prompt.md"), policy: join(bundle, "policy.json"), tools: join(bundle, "tools.json") },
toolSocket: join(runDir, "tools.sock"), turnMarker: join(runDir, "turn.done"), turnEnd: "marker", harnessEnv: {}, pollInterval: 50,
});
const paths = { workspace: ws, adapter, harnessMounts: PI_MOUNTS, 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 () => {
try {
process.kill(child.pid, "SIGTERM");
} catch {}
await exited;
});
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`);
const ask = async (body) => {
const sent = broker.request(pm, { verb: "message.send", args: { to: "coder", body, class: "REQUEST" } });
const end = Date.now() + 15000;
while (Date.now() < end) {
const reply = broker.request(pm, { verb: "message.receive" }).find((m) => m.in_reply_to === sent.id);
if (reply) return reply.body;
await new Promise((r) => setTimeout(r, 25));
}
throw new Error(`no answer to ${body}`);
};
const sh = async (cmd) => {
const m = (await ask(cmd)).match(/^rc=(\d+)(?:\n([\s\S]*))?$/);
assert.ok(m, `the session's shell answered ${cmd}`);
return { rc: Number(m[1]), out: m[2] ?? "" };
};
const facts = { s, ws, runDir, run, dataRoot, store: store.path, root, adapter, brokerSocket };
for (const f of [s.secret, s.mosaic, s.shm, join(s.otherWs, "notes.md"), join(s.otherRun, "session.json"), store.path, join(REPO, "AGENTS.md"), `/proc/${process.pid}`]) {
assert.ok(existsSync(f), `${f} exists on the host`);
}
const listed = spawnSync("awk", [`$2 == ${s.segment} { f = 1 } END { exit !f }`, "/proc/sysvipc/shm"]);
assert.equal(listed.status, 0, `segment ${s.segment} is listed on the host`);
return { ...facts, ask, sh };
}
test("a confined session's shell reaches its own workspace and run directory and nothing of the founder's or another session's", { skip, timeout: 60000 }, async (t) => {
const x = await session(t);
assert.deepEqual(await wrong(checks(x), x.sh), []);
// The private /tmp is the run's tmp/ on the host, not the host's /tmp.
assert.equal(readFileSync(join(x.runDir, "tmp", "probe"), "utf8"), "tmp\n");
assert.equal(readFileSync(join(x.ws, "probe"), "utf8"), "ws\n");
// The harness reaches its tool socket: the refusal is the tool server's,
// not a failed connect.
assert.equal((await x.ask("TOOL no_such_tool {}")).split("\n")[0], "refused: unknown-tool");
});
test("a bind that exposes a stand-in, or a missing workspace bind, fails the matching check", { skip, timeout: 60000 }, async (t) => {
const x = await session(t, { expose: (s) => [s.secret, s.mosaic] });
assert.deepEqual(await wrong(checks(x), x.sh), ["read a stand-in ~/.config/mosaic-dev/secrets", "read a stand-in ~/.mosaic"]);
// The same checks in a sandbox started directly from binds(), without the
// session: all pass with every bind, and the workspace check fails
// without the workspace's.
const all = binds({ runDir: x.runDir, workspace: x.ws, adapter: x.adapter, harnessMounts: PI_MOUNTS, brokerSocket: x.brokerSocket });
const direct = (list) => async (cmd) => {
const argv = sandboxArgv(list, x.runDir, ["/bin/sh", "-c", cmd], { business: "acme", role: "coder" });
const r = spawnSync(argv[0], argv.slice(1), { encoding: "utf8", timeout: 10000 });
return { rc: r.status, out: (r.stdout + r.stderr).replace(/\n$/, "") };
};
prepareRun(x.runDir);
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)));
});