1115 lines
68 KiB
JavaScript
1115 lines
68 KiB
JavaScript
// Row 195 (#1693): scripts/case1-accept.sh, root's acceptance run on the Case 1 host. Design r7 section 5
|
|
// (agents/darkwing/work/queue-195/DESIGN.md); deviations in agents/darkwing/work/queue-195/BUILD.md.
|
|
// Every case runs the script with --root on a scratch host tree: /etc, /proc, /srv/mosaic, /opt and the
|
|
// host check live under it. Fake systemctl, curl, ss, sudo and stat on PATH record their argv; the fake
|
|
// sudo runs the stack's user's commands from the tree. Nothing here reads or writes the real /etc,
|
|
// /proc or /srv, and the stand-in credential is not a credential.
|
|
import { test } from "node:test";
|
|
import { createHash } from "node:crypto";
|
|
import assert from "node:assert/strict";
|
|
import { spawnSync } from "node:child_process";
|
|
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { tmp } from "./helpers.mjs";
|
|
import { instrumentP10Probe, removeP10Observers, readProbeSeconds, P10_OBSERVERS } from "./case-p10-timing-fixture.mjs";
|
|
import { instrumentInnerTimer } from "./case-inner-timing-fixture.mjs";
|
|
import { CASE_SUDO_SESSION, recordedLivePids, killRecordedPids, fixtureGuard, cleanupFixture, pollFixtureGone, blockingQuerySource, LEGACY_CASES } from "./case-sudo-fixture.mjs";
|
|
|
|
const REPO = fileURLToPath(new URL("../../../", import.meta.url));
|
|
const SCRIPT = join(REPO, "scripts", "case1-accept.sh");
|
|
const COPY = "0123456789abcdef0123456789abcdef01234567";
|
|
const NODE_PIN = "v24.1.0", CLAUDE_PIN = "2.1.0";
|
|
const NODE = `/opt/mosaic-stack/node-${NODE_PIN}/bin/node`;
|
|
const CLAUDE = `/opt/mosaic-stack/claude-${CLAUDE_PIN}/node_modules/@anthropic-ai/claude-code/bin/claude.exe`;
|
|
const HOME = "/srv/mosaic";
|
|
const CLI = `${HOME}/.local/share/mosaic-stack/copies/${COPY}/scripts/mosaic`;
|
|
const PRECHECK = `${HOME}/.local/share/mosaic-stack/copies/${COPY}/scripts/case2-precheck.sh`;
|
|
const CRED = `${HOME}/.config/mosaic-dev/secrets/claude-session.env`;
|
|
const CRED_VALUE = "stand-in-not-a-credential-7f3e2a";
|
|
const AS_MOSAIC = `-u mosaic -H env PATH=/opt/mosaic-stack/node-${NODE_PIN}/bin:/usr/local/bin:/usr/bin:/bin`;
|
|
const ARGS = ["--profile", "factory", "--user", "mosaic", "--home", HOME, "--business", "mosaic-stack", "--copy", COPY,
|
|
"--node", NODE, "--claude", CLAUDE, "--pm", "--", "--port", "732", "--business", "mosaic-stack", "--tester-socket"];
|
|
const OUTER = "bwrap (enforce)", CHILD = "bwrap//&unpriv_bwrap (enforce)";
|
|
const MARKS = ["pass", "FAIL", "closed", "accepted Q182+Q183", "accepted Q183", "accepted mos-1096", "unnamed"];
|
|
const P_PASS = { P1: "closed", P2: "closed", P3: "accepted Q183", P4: "accepted Q182+Q183", P5: "closed", P6: "closed",
|
|
P7: "closed", P8: "closed", P9: "accepted mos-1096", P10: "unnamed", P11: "closed" };
|
|
const PASSWD = "root:x:0:0:root:/root:/bin/bash\nmosaic:x:997:997::/srv/mosaic:/usr/sbin/nologin\nlocaladmin:x:1000:1000::/home/localadmin:/bin/bash\njarvis:x:1001:1001::/home/jarvis:/bin/bash\n";
|
|
const GROUP = "root:x:0:\nsudo:x:27:localadmin\nmosaic:x:997:\nlxd:x:998:\ndocker:x:999:\nlocaladmin:x:1000:\njarvis:x:1001:\n";
|
|
const SS = "LISTEN 0 511 127.0.0.1:732 0.0.0.0:* users:((\"node\",pid=200,fd=21))\n"
|
|
+ "LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:((\"sshd\",pid=900,fd=3))\n";
|
|
const HOSTCHECK_OK = [1, 2, 3, 4, 5, 6, 7].map((n) => `ok ${n} item ${n} holds`).join("\n") + "\nnote bubblewrap 0.9.0 is the install pin\n";
|
|
|
|
function put(file, text, mode) {
|
|
mkdirSync(dirname(file), { recursive: true });
|
|
writeFileSync(file, text);
|
|
if (mode) chmodSync(file, mode);
|
|
}
|
|
const read = file => readFileSync(file,"utf8");
|
|
const edit = (file, f) => writeFileSync(file, f(readFileSync(file, "utf8")));
|
|
const log = (name) => `printf '%s' ${JSON.stringify(name)} >> "$HC/calls.log"; printf ' %s' "$@" >> "$HC/calls.log"; echo >> "$HC/calls.log"`;
|
|
|
|
// Fakes on PATH. Each records its argv and answers from a file in the state directory.
|
|
const FAKES = {
|
|
systemctl: `case "$1" in
|
|
show) [ -f "$HC/show-$2" ] && cat "$HC/show-$2" ;;
|
|
is-enabled) cat "$HC/enabled-$2" 2>/dev/null || { echo disabled; exit 1; } ;;
|
|
esac`,
|
|
curl: `exit "$(cat "$HC/curl-rc")"`,
|
|
ss: `cat "$HC/ss"`,
|
|
od: `if [ "$1" = -An ] && [ "$2" = -tx1 ] && [ "$3" = -N4 ]; then
|
|
[ -f "$HC/elf" ] && cat "$HC/elf" || echo 7f454c46
|
|
else exec /usr/bin/od "$@"; fi`,
|
|
// The credential and the settings file answer from the state directory, as root would see them, and
|
|
// only those calls are recorded; everything else is the real stat, which reads the args file.
|
|
stat: `case "\${@: -1}" in
|
|
*claude-session.env) ${log("stat")}; cat "$HC/stat-cred" ;;
|
|
*console/tester.sock) cat "$HC/stat-socket" ;;
|
|
*console) [ "$2" = '%u %a %F %d %i' ] && cat "$HC/stat-parent" || echo "997 700" ;;
|
|
*managed-settings.json) ${log("stat")}; cat "$HC/stat-settings" ;;
|
|
*) exec /usr/bin/stat "$@" ;;
|
|
esac`,
|
|
// -l -U USER prints the state file; -u jarvis is the bwrap probe; -u mosaic -H env PATH=... CMD runs
|
|
// CMD from the tree, with the PATH entries and each absolute argument that exists there moved under it.
|
|
sudo: `${CASE_SUDO_SESSION}
|
|
/usr/bin/python3 -I -S -c 'import json,os,sys;f=open(os.environ["HC"]+"/sudo-argv.jsonl","a");f.write(json.dumps(sys.argv[1:])+chr(10));f.close()' "$@"
|
|
case "$1" in
|
|
-l) [ -f "$HC/sudo-l-$3" ] && cat "$HC/sudo-l-$3"; exit 0 ;;
|
|
-u) ;;
|
|
*) exit 97 ;;
|
|
esac
|
|
user="$2"; shift 2
|
|
if [ "$user" = jarvis ]; then cat "$HC/jarvis-bwrap" >&2; exit "$(cat "$HC/jarvis-bwrap-rc")"; fi
|
|
[ "$user" = mosaic ] && [ "$1" = -H ] && [ "$2" = env ] || exit 98
|
|
shift 2
|
|
if [ "$1" = -i ]; then
|
|
[ "$2" = HOME=/srv/mosaic ] || exit 98
|
|
shift 2
|
|
fi
|
|
[[ "$1" == PATH=* ]] || exit 98
|
|
p="\${1#PATH=}"; shift
|
|
np=""; IFS=: read -ra parts <<< "$p"; for x in "\${parts[@]}"; do np+="$ROOT_DIR$x:"; done
|
|
cmd="$1"; shift
|
|
args=(); for a in "$@"; do if [[ "$a" == /* ]] && [ -e "$ROOT_DIR$a" ]; then args+=("$ROOT_DIR$a"); else args+=("$a"); fi; done
|
|
export HOME="$ROOT_DIR/srv/mosaic" PATH="$np$PATH"
|
|
[ ! -e "$ROOT_DIR$cmd" ] || cmd="$ROOT_DIR$cmd"
|
|
case_sudo_run "$HC" "$cmd" "\${args[@]}"`,
|
|
};
|
|
|
|
// Executables inside the tree, run by the fake sudo or by the script itself. Node marks a run outside
|
|
// the fake sudo, which sets HOME to the stack's home, so a test can tell whether root ran it.
|
|
const NODE_STUB = `[ "$HOME" = "$ROOT_DIR/srv/mosaic" ] || touch "$HC/node-as-root"
|
|
[ "\${1:-}" = --version ] && { cat "$HC/node-version"; exit 0; }
|
|
exec "$REAL_NODE" "$@"`;
|
|
// The real Python payload runs against a mock socket object, never an OS listener.
|
|
const PYTHON_STUB = `[ -f "$HC/peer-output" ] && exec /usr/bin/python3 -I -S -c 'import os,sys; root=os.environ["HC"]; sys.stdout.buffer.write(open(root+"/peer-output","rb").read()); sys.exit(int(open(root+"/peer-status").read()))'
|
|
[ -f "$HC/peer-hang" ] && {
|
|
here="$BASHPID"; read -r sid < <(/bin/ps -o sid= -p "$here")
|
|
echo "$here" > "$HC/peer-pid"; printf '%s %s\\n' "$here" "$sid" >> "$HC/query-pids"
|
|
trap '' TERM HUP; exec sleep 60
|
|
}
|
|
if [ -f "$HC/peer-change" ]; then cat "$HC/peer-change" > "$HC/stat-socket"; fi
|
|
if [ -f "$HC/peer-pid-change" ]; then cat "$HC/peer-pid-change" > "$HC/show-mosaic-webui.service"; fi
|
|
exec /usr/bin/python3 -I -S -c '
|
|
import os,socket,struct,sys
|
|
class Fake:
|
|
def __init__(self,*a): assert a==(socket.AF_UNIX,socket.SOCK_STREAM)
|
|
def settimeout(self,n): assert n==1
|
|
def connect(self,p): assert p.endswith("/console/tester.sock")
|
|
def getsockopt(self,*a):
|
|
assert a==(socket.SOL_SOCKET,socket.SO_PEERCRED,12)
|
|
return struct.pack("3i",*[int(x) for x in open(os.environ["HC"]+"/peer").read().split()])
|
|
def close(self): pass
|
|
socket.socket=Fake
|
|
payload=sys.argv[4]
|
|
sys.argv=["probe",sys.argv[5]]
|
|
exec(payload)
|
|
' "$@"`;
|
|
const TREE = {
|
|
"/usr/bin/python3": PYTHON_STUB,
|
|
[NODE]: NODE_STUB,
|
|
[CLAUDE]: `[ "\${1:-}" = --version ] && cat "$HC/claude-version"; exit 0`,
|
|
[CLI]: `${log("mosaic")}
|
|
case "$1" in
|
|
talk) case "$(cat "$HC/talk-mode")" in
|
|
reply) echo "sent ab12cd34 to chief"; echo "[chief -> tester] seen" ;;
|
|
noreply) echo "sent ab12cd34 to chief"; echo "no reply from chief within 300 s" >&2; exit 1 ;;
|
|
sentonly) echo "sent ab12cd34 to chief" ;;
|
|
esac ;;
|
|
launches) cat "$HC/launches.json" ;;
|
|
*) exit 2 ;;
|
|
esac`,
|
|
[PRECHECK]: `${log("precheck")}
|
|
echo "precheck: done"; exit "$(cat "$HC/precheck-rc")"`,
|
|
"/usr/local/lib/mosaic-stack/hostcheck.sh": `${log("hostcheck")}
|
|
cat "$HC/hostcheck-out"`,
|
|
};
|
|
|
|
function proc(root, pid, { ppid, uid = 997, exe, argv0 = "x", comm = "x", label }) {
|
|
const d = join(root, "proc", String(pid));
|
|
put(join(d, "status"), `Name:\t${comm}\nState:\tS (sleeping)\nPPid:\t${ppid}\nUid:\t${uid}\t${uid}\t${uid}\t${uid}\n`);
|
|
put(join(d, "cmdline"), `${argv0}\0--x\0`);
|
|
put(join(d, "comm"), `${comm}\n`);
|
|
rmSync(join(d, "exe"), { force: true });
|
|
if (exe) symlinkSync(exe, join(d, "exe"));
|
|
if (label !== undefined) put(join(d, "attr", "current"), label);
|
|
}
|
|
|
|
// A scratch Case 1 host where every F check passes and every P check is closed or accepted.
|
|
function host(t) {
|
|
const dir = tmp(t, "c1accept-");
|
|
const root = join(dir, "root"), hc = join(dir, "hc"), bin = join(dir, "bin");
|
|
const args = join(root, "etc", "mosaic-stack", "dev.args");
|
|
put(args, ARGS.join("\n") + "\n", 0o644);
|
|
put(join(root, "etc", "passwd"), PASSWD);
|
|
put(join(root, "etc", "group"), GROUP);
|
|
put(join(root, "etc", "sudoers.d", "90-localadmin"), "localadmin ALL=(ALL) NOPASSWD: ALL\n");
|
|
put(join(root, "etc", "claude-code", "managed-settings.json"), JSON.stringify({ env: { DISABLE_AUTOUPDATER: "1" } }));
|
|
put(join(root, CRED), `CLAUDE_CODE_OAUTH_TOKEN=${CRED_VALUE}\n`, 0o600);
|
|
put(join(root, HOME, ".config", "mosaic-dev", "businesses", "mosaic-stack.json"), JSON.stringify({ id: "mosaic-stack", launch: { by: "pm" } }));
|
|
put(join(root, "proc", "sys", "net", "ipv4", "ip_unprivileged_port_start"), "1024\n");
|
|
proc(root, 1, { ppid: 0, uid: 0, exe: "/usr/lib/systemd/systemd", comm: "systemd" });
|
|
proc(root, 100, { ppid: 1, exe: NODE, argv0: NODE, comm: "node" });
|
|
proc(root, 200, { ppid: 1, exe: NODE, argv0: NODE, comm: "node" });
|
|
proc(root, 300, { ppid: 100, exe: "/usr/bin/bwrap", argv0: "/usr/bin/bwrap", comm: "bwrap", label: `${OUTER}\n` });
|
|
proc(root, 301, { ppid: 300, exe: NODE, argv0: NODE, comm: "node", label: `${CHILD}\n` });
|
|
proc(root, 302, { ppid: 301, exe: CLAUDE, argv0: CLAUDE, comm: "claude.exe", label: `${CHILD}\n` });
|
|
// A root bwrap with a mislabelled claude child, which F5 must pass over.
|
|
proc(root, 400, { ppid: 1, uid: 0, exe: "/usr/bin/bwrap", argv0: "/usr/bin/bwrap", comm: "bwrap", label: "unconfined\n" });
|
|
proc(root, 401, { ppid: 400, uid: 0, argv0: "claude", comm: "claude", label: "unconfined\n" });
|
|
const state = {
|
|
"[email protected]": "ActiveState=active\nUser=mosaic\nMainPID=100\n",
|
|
"show-mosaic-webui.service": "ActiveState=active\nUser=mosaic\nMainPID=200\n",
|
|
"show-mosaic-hostcheck.service": "[email protected] mosaic-webui.service multi-user.target\n",
|
|
"enabled-mosaic-hostcheck.service": "enabled\n",
|
|
"curl-rc": "0", ss: SS,
|
|
"stat-parent": "997 700 directory 9 10\n", "stat-socket": "997 600 socket 9 11\n", peer: "200 997 997\n",
|
|
"stat-cred": "mosaic 600 regular file\n", "stat-settings": "root:root 644 regular file\n",
|
|
"sudo-l-mosaic": "User mosaic is not allowed to run sudo on dev-53.\n",
|
|
"sudo-l-localadmin": "User localadmin may run the following commands on dev-53:\n (ALL : ALL) ALL\n (ALL) NOPASSWD: ALL\n",
|
|
"jarvis-bwrap": "sudo: unable to execute /usr/bin/bwrap: Permission denied\n", "jarvis-bwrap-rc": "1",
|
|
"node-version": `${NODE_PIN}\n`, "claude-version": `${CLAUDE_PIN} (Claude Code)\n`,
|
|
"talk-mode": "reply", "precheck-rc": "0", "hostcheck-out": HOSTCHECK_OK,
|
|
"launches.json": JSON.stringify([
|
|
{ instance: "pm", live: true, business: "mosaic-stack", run: "r-0002" },
|
|
{ instance: "pm", live: false, business: "mosaic-stack", run: "r-0001" },
|
|
{ instance: "dev-1", live: true, business: "mosaic-stack", run: "r-0003" },
|
|
]),
|
|
};
|
|
for (const [name, text] of Object.entries(state)) put(join(hc, name), text);
|
|
for (const [name, body] of Object.entries(FAKES)) put(join(bin, name), `#!/usr/bin/env bash\n${name === "stat" ? "" : log(name)}\n${body}\n`, 0o755);
|
|
for (const [path, body] of Object.entries(TREE)) put(join(root, path), `#!/usr/bin/env bash\n${body}\n`, 0o755);
|
|
const copyRoot = join(root, HOME, '.local/share/mosaic-stack/copies', COPY);
|
|
put(join(copyRoot, 'packages/cli/src/config.mjs'), `import fs from 'node:fs';export const loadSystem=()=>({dataRoot:fs.readFileSync(${JSON.stringify(join(hc,'data-root'))},'utf8').trim()});`);
|
|
put(join(copyRoot, 'packages/webui/src/cli.mjs'), '// MOSAIC_TESTER_SOCKET_V1\n');
|
|
put(join(hc, 'data-root'), `${HOME}/.mosaic-dev\n`);
|
|
put(join(root, HOME, '.mosaic-dev/console/tester.sock'), 'stand-in socket metadata only');
|
|
chmodSync(join(root, HOME, '.mosaic-dev/console'), 0o700);
|
|
// F9 runs node only when it and each directory above it have no group or other write, whatever the umask.
|
|
for (let d = dirname(NODE); d !== "/"; d = dirname(d)) chmodSync(join(root, d), 0o755);
|
|
chmodSync(root, 0o755);
|
|
return { dir, root, hc, bin, args };
|
|
}
|
|
|
|
function run(h, extra = {}, argv = ["--business", "mosaic-stack", "--args", "/etc/mosaic-stack/dev.args", "--claude-pin", CLAUDE_PIN, "--node-pin", NODE_PIN, "--root", h.root], script = SCRIPT) {
|
|
const r = spawnSync(script, argv, {
|
|
encoding: "utf8",
|
|
env: { ...process.env, PATH: `${h.bin}:${process.env.PATH}`, HC: h.hc, ROOT_DIR: h.root, REAL_NODE: process.execPath, TMPDIR: h.dir, ...extra },
|
|
});
|
|
const lines = r.stdout.split("\n").filter(Boolean);
|
|
// Each check line as { id, path, mark, evidence }.
|
|
const checks = lines.filter((l) => /^[FP][0-9]+ /.test(l)).map((l) => {
|
|
const [id, path, ...rest] = l.split(" ");
|
|
const tail = rest.join(" ");
|
|
const mark = MARKS.find((m) => tail === m || tail.startsWith(`${m} `));
|
|
return { id, path, mark, evidence: tail.slice(mark?.length ?? 0).trim(), line: l };
|
|
});
|
|
return { status: r.status, out: r.stdout, err: r.stderr, lines, checks };
|
|
}
|
|
const calls = (h, name) => (existsSync(join(h.hc, "calls.log")) ? readFileSync(join(h.hc, "calls.log"), "utf8") : "")
|
|
.split("\n").filter((l) => l === name || l.startsWith(`${name} `)).map((l) => l.slice(name.length + 1));
|
|
const failed = (r) => r.checks.filter((c) => c.mark === "FAIL").map((c) => c.id);
|
|
const marksOf = (r, id) => r.checks.filter((c) => c.id === id).map((c) => c.mark);
|
|
const mustRun = (h, extra) => { const r = run(h, extra); assert.ok(r.status === 0 || r.status === 1, r.out + r.err); return r; };
|
|
|
|
test('row253 socket observation uses configured root, metadata and the Console peer without sending bytes', t=>{
|
|
const h=host(t), data='/srv/mosaic/custom-data';
|
|
put(join(h.hc,'data-root'),data+'\n');
|
|
put(join(h.root,data,'console/tester.sock'),'metadata stand-in');
|
|
const r=run(h);
|
|
assert.equal(r.status,0,r.out+r.err);
|
|
assert.match(r.out,/P10 tester-socket-squat unnamed boundary=observed path=\/srv\/mosaic\/custom-data\/console\/tester.sock/);
|
|
assert.match(r.out,/peer_pid=200 peer_uid=997/);
|
|
assert.match(r.out,/hostile same-uid substitution remains/);
|
|
assert.doesNotMatch(r.out,/accepted mos-1101/);
|
|
assert.deepEqual(marksOf(r,'P8'),['closed']);
|
|
});
|
|
|
|
test('row253 P10 refuses unsafe metadata, wrong peers and an unsafe Python without claiming closure',t=>{
|
|
const faults={
|
|
'parent-mode': h=>put(join(h.hc,'stat-parent'),'997 755 directory 9 10\n'),
|
|
'parent-owner': h=>put(join(h.hc,'stat-parent'),'998 700 directory 9 10\n'),
|
|
'socket-mode': h=>put(join(h.hc,'stat-socket'),'997 660 socket 9 11\n'),
|
|
'socket-owner': h=>put(join(h.hc,'stat-socket'),'998 600 socket 9 11\n'),
|
|
'socket-type': h=>put(join(h.hc,'stat-socket'),'997 600 regular file 9 11\n'),
|
|
symlink:h=>{const p=join(h.root,HOME,'.mosaic-dev/console/tester.sock');rmSync(p);symlinkSync('/absent',p);},
|
|
peer:h=>put(join(h.hc,'peer'),'200 998 997\n'),
|
|
'peer-pid':h=>put(join(h.hc,'peer'),'555 997 997\n'),
|
|
'python-mode':h=>chmodSync(join(h.root,'usr/bin/python3'),0o777),
|
|
'python-missing':h=>rmSync(join(h.root,'usr/bin/python3')),
|
|
'python-not-elf':h=>put(join(h.hc,'elf'),'23212f62\n'),
|
|
'peer-malformed':h=>{put(join(h.hc,'peer-output'),'200 997 997 extra\n');put(join(h.hc,'peer-status'),'0');},
|
|
'peer-error':h=>{put(join(h.hc,'peer-output'),'200 997 997\n');put(join(h.hc,'peer-status'),'1');},
|
|
'peer-uid-fields':h=>proc(h.root,200,{ppid:1,uid:998,exe:NODE,comm:'node'}),
|
|
'changed-inode':h=>put(join(h.hc,'peer-change'),'997 600 socket 9 99\n'),
|
|
'changed-pid':h=>put(join(h.hc,'peer-pid-change'),'ActiveState=active\nUser=mosaic\nMainPID=201\n'),
|
|
floor:h=>put(join(h.root,'proc/sys/net/ipv4/ip_unprivileged_port_start'),'700\n'),
|
|
};
|
|
const reasons = {
|
|
"parent-mode": "metadata",
|
|
"parent-owner": "metadata",
|
|
"socket-mode": "metadata",
|
|
"socket-owner": "metadata",
|
|
"socket-type": "metadata",
|
|
"symlink": "metadata",
|
|
"peer": "peer",
|
|
"peer-pid": "peer",
|
|
"python-mode": "interpreter",
|
|
"python-missing": "interpreter",
|
|
"python-not-elf": "interpreter",
|
|
"peer-malformed": "probe",
|
|
"peer-error": "probe-inner-command",
|
|
"peer-uid-fields": "metadata",
|
|
"changed-inode": "changed",
|
|
"changed-pid": "changed",
|
|
"floor": "floor",
|
|
};
|
|
for(const [name,fault] of Object.entries(faults)){
|
|
const h=host(t);fault(h);const r=run(h);
|
|
assert.equal(r.status,name==='peer-uid-fields'?1:0,`${name}: ${r.out}${r.err}`);
|
|
assert.match(r.out,/P10 tester-socket-squat unnamed boundary=failed/,name);
|
|
assert.doesNotMatch(r.out,/boundary=observed/,name);
|
|
assert.match(r.out,new RegExp('^P10 tester-socket-squat unnamed boundary=failed reason='+reasons[name]+' ', 'm'),name);
|
|
}
|
|
});
|
|
|
|
test('row253 acceptance rejects bad config and unsupported copies before talk',t=>{
|
|
const faults={
|
|
outside:h=>put(join(h.hc,'data-root'),'/srv/mosaic2/data\n'),
|
|
dot:h=>put(join(h.hc,'data-root'),'/srv/mosaic/./data\n'),
|
|
absent:h=>rmSync(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs')),
|
|
unmarked:h=>put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/webui/src/cli.mjs'),'// old copy'),
|
|
schema:h=>put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),'console.log(JSON.stringify({dataRoot:"/srv/mosaic/.mosaic-dev",socket:true,extra:1}));process.exit(0);'),
|
|
failed:h=>put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),'console.log(JSON.stringify({dataRoot:"/srv/mosaic/.mosaic-dev",socket:true}));process.exit(1);'),
|
|
};
|
|
for(const [name,fault] of Object.entries(faults)){
|
|
const h=host(t);fault(h);const r=run(h);
|
|
assert.equal(r.status,2,`${name}: ${r.out}${r.err}`);assert.equal(r.out,'');
|
|
assert.match(r.err,/tester (config query|path refuses)|support/);
|
|
assert.deepEqual(calls(h,'mosaic'),[]);
|
|
}
|
|
});
|
|
|
|
test('row253 stalled peer observation ends within its bound without leaving a reader',async t=>{
|
|
const h=host(t);put(join(h.hc,'peer-hang'),'');
|
|
const r=run(h);assert.equal(r.status,0,r.out+r.err);
|
|
assert.match(r.out,/P10 tester-socket-squat unnamed boundary=failed reason=probe/);
|
|
const pid=Number(readFileSync(join(h.hc,'peer-pid'),'utf8'));
|
|
let state='gone';
|
|
for(let n=0;n<50;n++){
|
|
try{state=readFileSync(`/proc/${pid}/stat`,'utf8').split(') ')[1].split(' ')[0];}catch{state='gone';}
|
|
if(state==='gone'||state==='Z')break;
|
|
await new Promise(resolve=>setTimeout(resolve,10));
|
|
}
|
|
assert.ok(state==='gone'||state==='Z',`peer helper ${pid} still ${state}`);
|
|
});
|
|
|
|
test('row253 P8 refuses both retired TCP and board listeners',t=>{
|
|
for(const port of [7333,7331]){
|
|
const h=host(t);put(join(h.hc,'ss'),SS+`LISTEN 0 511 127.0.0.1:${port} 0.0.0.0:* users:(("node",pid=200,fd=22))\n`);
|
|
assert.deepEqual(marksOf(run(h),'P8'),['unnamed']);
|
|
}
|
|
});
|
|
|
|
test('row253 C4 acceptance query inner reason holds under both sudo models',async t=>{
|
|
for(const mode of ['exec','hop']){
|
|
const h=host(t),files=[join(h.hc,'session-pids'),join(h.hc,'query-pids')];
|
|
put(join(h.hc,'session-mode'),mode);
|
|
put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),blockingQuerySource(files[1]));
|
|
const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files);
|
|
const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
|
|
assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query inner-timeout\n');
|
|
assert.ok(seconds<7,`${mode}: ${seconds}s`);assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);
|
|
assert.deepEqual(await pollFixtureGone(files),[]);await stop();
|
|
}
|
|
});
|
|
|
|
test('row253 C4 acceptance query outer reason discards stdout and names residual cleanup',async t=>{
|
|
for(const mode of ['exec','hop']){
|
|
const h=host(t),files=[join(h.hc,'session-pids')];put(join(h.hc,'session-mode'),mode);put(join(h.hc,'stall-config'),'');
|
|
put(join(h.hc,'stall-output'),JSON.stringify({dataRoot:HOME+'/.mosaic-dev',socket:true}));
|
|
const capture=cleanupFixture(t,files);const stop=fixtureGuard(t,files);
|
|
const start=performance.now(),r=run(h),seconds=(performance.now()-start)/1000;capture();
|
|
assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query outer-timeout\n');
|
|
assert.ok(seconds>=7&&seconds<9,`${mode}: ${seconds}s; 2s preflight slack`);
|
|
assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);
|
|
if(mode==='hop')assert.ok(recordedLivePids(files[0]).length>0,'R3 session remains before fixture cleanup');
|
|
killRecordedPids(files[0]);assert.deepEqual(await pollFixtureGone(files),[],'explicit R3 fixture cleanup');await stop();
|
|
}
|
|
});
|
|
|
|
test('row253 C4 acceptance query overflow is output-limit in both models',async t=>{
|
|
for(const mode of ['exec','hop']){
|
|
const h=host(t),files=[join(h.hc,'session-pids')];put(join(h.hc,'session-mode'),mode);const capture=cleanupFixture(t,files);
|
|
put(join(h.root,HOME,'.local/share/mosaic-stack/copies',COPY,'packages/cli/src/config.mjs'),`process.stdout.write('X'.repeat(8192));export const loadSystem=()=>({dataRoot:'${HOME}/.mosaic-dev'});`);
|
|
const r=run(h);capture();assert.equal(r.status,2,r.out+r.err);assert.equal(r.err,'case1-accept: refusing: tester config query output-limit\n');
|
|
assert.equal(r.out,'');assert.deepEqual(calls(h,'mosaic'),[]);assert.deepEqual(await pollFixtureGone(files),[]);
|
|
}
|
|
});
|
|
|
|
test('row299 P10 timing copy preserves shipped bytes and isolates observer variables',()=>{
|
|
const source=read(SCRIPT),copy=instrumentP10Probe(source);
|
|
assert.equal(removeP10Observers(copy),read(SCRIPT),'observer reversal equals a fresh shipped script read');
|
|
for(const block of Object.values(P10_OBSERVERS)){
|
|
const reads=[...block.matchAll(/\bread -r ([A-Za-z_][A-Za-z0-9_ ]*) < \/proc\/uptime/g)];
|
|
assert.equal(reads.length,1,'one direct host clock read per observer');
|
|
const assigned=reads[0][1].trim().split(/\s+/);
|
|
assert.deepEqual(assigned,['row299_up','row299_spare']);
|
|
assert.ok(assigned.every(name=>/^row299_/.test(name)),'read targets cannot clobber capture locals');
|
|
assert.deepEqual([...block.matchAll(/\b([A-Za-z_][A-Za-z0-9_]*)=/g)],[],'no other assignments in observer blocks');
|
|
assert.doesNotMatch(block,/\$ROOT|\$\{ROOT/,'fixture root cannot supply the clock');
|
|
}
|
|
// The fake sudo keys its faults on these argument markers; observers must not add or remove any.
|
|
for(const marker of['SO_PEERCRED','MOSAIC_TESTER_SOCKET_V1','MOSAIC_LEGACY_STATUS'])
|
|
assert.equal(copy.split(marker).length,source.split(marker).length,`observers keep the ${marker} count`);
|
|
const changedTimer=source.replace('tester_capture 2 4 128','tester_capture 1 4 128');
|
|
assert.equal(removeP10Observers(instrumentP10Probe(changedTimer)),changedTimer,'copy keeps current timer mutants');
|
|
});
|
|
|
|
test('row299 P10 observers preserve stdout stderr status and the EXIT trap',t=>{
|
|
const dir=tmp(t,'p10-clock-'),clock=join(dir,'clock'),trapFile=join(dir,'trap');
|
|
put(clock,'',0o600);
|
|
const program=`set -uo pipefail
|
|
capture() (
|
|
maximum=128; rc=87
|
|
trap 'printf trap > "$ROW299_TRAP_FILE"' EXIT
|
|
${P10_OBSERVERS.begin}printf stdout-without-newline
|
|
printf stderr-without-newline >&2
|
|
${P10_OBSERVERS.end}exit "$rc"
|
|
)
|
|
capture`;
|
|
const r=spawnSync('/bin/bash',['-c',program],{encoding:'utf8',env:{...process.env,ROW299_TIMING_FILE:clock,ROW299_TRAP_FILE:trapFile}});
|
|
assert.equal(r.status,87);
|
|
assert.equal(r.stdout,'stdout-without-newline');
|
|
assert.equal(r.stderr,'stderr-without-newline');
|
|
assert.equal(read(trapFile),'trap');
|
|
assert.match(read(clock),/^begin [0-9]+\.[0-9]{2}\nend [0-9]+\.[0-9]{2}\n$/);
|
|
});
|
|
|
|
test('row299 P10 timer observers keep supervisor status and record one P10 pair',t=>{
|
|
const dir=tmp(t,'p10-timer-'),clock=join(dir,'clock'),trapFile=join(dir,'trap');
|
|
put(clock,'',0o600);
|
|
// Same shape as TESTER_SUPERVISOR: positional arguments are the setsid command, finish() has none.
|
|
const program=`set +m
|
|
finish() {
|
|
local rc="$?"
|
|
${P10_OBSERVERS.timerEnd} printf trap > "$ROW299_TRAP_FILE"
|
|
exit "$rc"
|
|
}
|
|
trap finish EXIT
|
|
${P10_OBSERVERS.timerBegin}printf stdout-without-newline
|
|
printf stderr-without-newline >&2
|
|
exit 81`;
|
|
const supervise=(...args)=>spawnSync('/bin/bash',['-c',program,'--',...args],{encoding:'utf8',env:{...process.env,ROW299_TIMER_FILE:clock,ROW299_TRAP_FILE:trapFile}});
|
|
let r=supervise('node','-e','query');
|
|
assert.equal(r.status,81);assert.equal(read(clock),'','a supervisor without the P10 peer records nothing');
|
|
r=supervise('python','-c','getsockopt(SO_PEERCRED)');
|
|
assert.equal(r.status,81);
|
|
assert.equal(r.stdout,'stdout-without-newline');
|
|
assert.equal(r.stderr,'stderr-without-newline');
|
|
assert.equal(read(trapFile),'trap');
|
|
assert.match(read(clock),/^begin [0-9]+\.[0-9]{2}\nend [0-9]+\.[0-9]{2}\n$/);
|
|
const pair=read(clock);
|
|
r=supervise('node','-e','later');
|
|
assert.equal(r.status,81);assert.equal(read(clock),pair,'a later supervisor cannot add a second end');
|
|
});
|
|
|
|
test('row299 P10 observer records are complete and ordered',()=>{
|
|
assert.equal(readProbeSeconds('begin 100.01\nend 102.03\n'),2.02);
|
|
const invalid={
|
|
'missing begin':'end 102.00\n',
|
|
'missing end':'begin 100.00\n',
|
|
'duplicate pair':'begin 100.00\nend 102.00\nbegin 102.00\nend 104.00\n',
|
|
'reversed order':'end 102.00\nbegin 100.00\n',
|
|
'non-numeric':'begin nope\nend 102.00\n',
|
|
'equal readings':'begin 100.00\nend 100.00\n',
|
|
'backward readings':'begin 102.00\nend 100.00\n',
|
|
'missing final newline':'begin 100.00\nend 102.00',
|
|
'extra final newline':'begin 100.00\nend 102.00\n\n',
|
|
};
|
|
for(const [name,text] of Object.entries(invalid))assert.throws(()=>readProbeSeconds(text),/P10 observer/,name);
|
|
});
|
|
|
|
for(const fault of ['inner','outer','overflow'])test(`row253 C4 P10 ${fault} reason holds under both sudo models`,async t=>{
|
|
for(const mode of ['exec','hop']){
|
|
const h=host(t),files=[join(h.hc,'session-pids'),join(h.hc,'query-pids')];put(join(h.hc,'session-mode'),mode);
|
|
// mos-1350(b): measure this host/mode's functional cost before adding a fault.
|
|
let baselineSeconds=0;
|
|
if(fault==='inner'||fault==='outer'){
|
|
const baselineStart=performance.now(),baseline=run(h);
|
|
baselineSeconds=(performance.now()-baselineStart)/1000;
|
|
assert.equal(baseline.status,0,baseline.out+baseline.err);
|
|
assert.deepEqual(await pollFixtureGone(files),[],'baseline leaves no session');
|
|
}
|
|
const capture=cleanupFixture(t,files);
|
|
if(fault==='inner')put(join(h.hc,'peer-hang'),'');
|
|
if(fault==='outer'){put(join(h.hc,'stall-probe'),'');put(join(h.hc,'stall-output'),'200 997 997\n');}
|
|
if(fault==='overflow'){put(join(h.hc,'peer-output'),'200 997 997\n'+'X'.repeat(256));put(join(h.hc,'peer-status'),'0');}
|
|
let observedScript=SCRIPT,timingFile,timerFile;
|
|
if(fault==='inner'||fault==='outer'){
|
|
observedScript=join(h.dir,'case1-p10-observed.sh');timingFile=join(h.hc,'probe-uptime');
|
|
put(timingFile,'',0o600);put(observedScript,instrumentP10Probe(read(SCRIPT)),0o700);
|
|
// The outer fault stalls before the supervisor starts, so only the inner fault has a timer pair.
|
|
if(fault==='inner'){timerFile=join(h.hc,'timer-uptime');put(timerFile,'',0o600);}
|
|
put(join(h.dir,'selector.sh'),read(join(REPO,'scripts','selector.sh')),0o644);
|
|
assert.equal(removeP10Observers(read(observedScript)),read(SCRIPT),'observer reversal equals a fresh shipped script read');
|
|
}
|
|
const stop=fixtureGuard(t,files,7000,timingFile);
|
|
const observe={...(timingFile?{ROW299_TIMING_FILE:timingFile}:{}),...(timerFile?{ROW299_TIMER_FILE:timerFile}:{})};
|
|
const start=performance.now(),r=run(h,observe,undefined,observedScript),seconds=(performance.now()-start)/1000;capture();
|
|
assert.equal(r.status,0,r.out+r.err);
|
|
const reason=fault==='inner'?'inner-timeout':fault==='outer'?'outer-timeout':'output-limit';
|
|
assert.match(r.out,new RegExp('P10 tester-socket-squat unnamed boundary=failed reason=probe-'+reason+' '));
|
|
assert.doesNotMatch(r.out,/boundary=observed/);
|
|
let probeSeconds=seconds,timerSeconds;
|
|
if(timingFile)assert.doesNotThrow(()=>{probeSeconds=readProbeSeconds(read(timingFile));},'P10 observer pair is required');
|
|
if(timerFile)assert.doesNotThrow(()=>{timerSeconds=readProbeSeconds(read(timerFile));},'P10 timer observer pair is required');
|
|
if(fault==='inner'||fault==='outer')t.diagnostic(JSON.stringify({fault,mode,baselineSeconds,faultSeconds:seconds,probeSeconds,timerSeconds}));
|
|
// org-base-reds: the band applies to the inner timer itself. The root pair adds supervisor start-up and
|
|
// clean_session's whole-host /proc scan, which grows with the host's process count, so it must contain
|
|
// the timer pair but has no band of its own; probe-inner-timeout above already places it before the outer bound.
|
|
if(fault==='inner'){
|
|
assert.ok(timerSeconds>=1.25&&timerSeconds<2.75,`${mode}: inner timer ${timerSeconds}s (root probe ${probeSeconds}s); baseline ${baselineSeconds}s is diagnostic only`);
|
|
assert.ok(probeSeconds>=timerSeconds,`${mode}: root probe ${probeSeconds}s must contain the inner timer ${timerSeconds}s`);
|
|
}
|
|
if(fault==='outer'){
|
|
assert.ok(probeSeconds>=3.25&&probeSeconds<4.75,`${mode}: direct probe ${probeSeconds}s; baseline ${baselineSeconds}s is diagnostic only`);
|
|
if(mode==='hop')assert.ok(recordedLivePids(files[0]).length>0,'stalled supervisor is R3 residual');
|
|
for(const file of files)killRecordedPids(file);
|
|
}
|
|
assert.deepEqual(await pollFixtureGone(files),[],fault==='outer'?'explicit R3 cleanup':mode);await stop();
|
|
}
|
|
});
|
|
|
|
// One fault per F check; each makes that check, and only that one, fail.
|
|
const F_FAULTS = {
|
|
F1: (h) => proc(h.root, 100, { ppid: 1, uid: 0, exe: NODE, comm: "node" }),
|
|
F2: (h) => put(join(h.hc, "ss"), SS.replace("127.0.0.1:732 ", "0.0.0.0:732 ")),
|
|
F3: (h) => put(join(h.hc, "talk-mode"), "noreply"),
|
|
F4: (h) => put(join(h.hc, "precheck-rc"), "1"),
|
|
F5: (h) => put(join(h.root, "proc", "302", "attr", "current"), "unconfined\n"),
|
|
F6: (h) => put(join(h.hc, "claude-version"), "2.0.9 (Claude Code)\n"),
|
|
F7: (h) => put(join(h.hc, "launches.json"), JSON.stringify([{ instance: "pm", live: false, business: "mosaic-stack", run: "r-1" }])),
|
|
F8: (h) => put(join(h.hc, "stat-cred"), "mosaic 640 regular file\n"),
|
|
F9: (h) => put(join(h.root, "etc", "claude-code", "managed-settings.json"), JSON.stringify({ env: { DISABLE_AUTOUPDATER: "0" } })),
|
|
F10: (h) => put(join(h.hc, "node-version"), "v24.0.0\n"),
|
|
F11: (h) => put(join(h.root, HOME, ".mosaic-dev", "discord", "binding.json"), "{}"),
|
|
};
|
|
|
|
test("row 195 accept: every check passing exits 0, with each P mark and counts that match the lines", (t) => {
|
|
const h = host(t);
|
|
const r = run(h);
|
|
assert.equal(r.status, 0, r.out + r.err);
|
|
assert.deepEqual(r.checks.map((c) => c.id), ["F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11",
|
|
"P1", "P2", "P3", "P4", "P5", "P6", "P7", "P8", "P9", "P10", "P11"]);
|
|
for (const c of r.checks.filter((x) => x.id.startsWith("F"))) assert.equal(c.mark, "pass", c.line);
|
|
for (const [id, mark] of Object.entries(P_PASS)) assert.deepEqual(marksOf(r, id), [mark], id);
|
|
const counts = r.lines.filter((l) => l.startsWith("count ")).map((l) => l.slice(6));
|
|
assert.deepEqual(counts, MARKS.map((m) => `${m} ${r.checks.filter((c) => c.mark === m).length}`));
|
|
assert.match(r.lines.at(-1), /^case1-accept: every F check passes/);
|
|
assert.match(r.out, /^F5 bwrap-label pass bwrap 300 reads 'bwrap \(enforce\)' and its claude descendant 302 reads/m);
|
|
assert.match(r.out, /^F7 pm-live pass one live launch: pm run r-0002$/m);
|
|
assert.ok(!(r.out + r.err).includes(CRED_VALUE));
|
|
assert.ok(existsSync(join(h.hc, "node-as-root")), "F9 runs the pinned, root-only node as root");
|
|
});
|
|
|
|
test("row 195 accept: exits 1 with each F check failing alone, and its line names it", (t) => {
|
|
for (const [id, fault] of Object.entries(F_FAULTS)) {
|
|
const h = host(t);
|
|
fault(h);
|
|
const r = mustRun(h);
|
|
assert.equal(r.status, 1, `${id}: ${r.out}`);
|
|
assert.deepEqual(failed(r), [id], `${id}: ${r.out}`);
|
|
assert.equal(r.lines.at(-1), "case1-accept: an F check failed, so the row fails");
|
|
}
|
|
});
|
|
|
|
test("row 195 accept: F3 fails on exit 0 with no reply line, and its evidence takes talk's error", (t) => {
|
|
let h = host(t);
|
|
put(join(h.hc, "talk-mode"), "sentonly");
|
|
let r = mustRun(h);
|
|
assert.deepEqual(failed(r), ["F3"]);
|
|
assert.match(r.out, /^F3 tester-reply FAIL talk exited 0 with 1 lines on stdout; last: sent ab12cd34 to chief$/m);
|
|
h = host(t);
|
|
put(join(h.hc, "talk-mode"), "noreply");
|
|
r = mustRun(h);
|
|
assert.match(r.out, /^F3 tester-reply FAIL talk exited 1 with 1 lines on stdout; last: no reply from chief within 300 s$/m);
|
|
});
|
|
|
|
test("row 195 accept: F1 fails on a unit that isn't active or isn't User=mosaic", (t) => {
|
|
for (const show of ["ActiveState=failed\nUser=mosaic\nMainPID=0\n", "ActiveState=active\nUser=root\nMainPID=100\n"]) {
|
|
const h = host(t);
|
|
put(join(h.hc, "show-mosaic-webui.service"), show);
|
|
const r = mustRun(h);
|
|
assert.equal(r.status, 1);
|
|
assert.ok(failed(r).includes("F1"), r.out);
|
|
}
|
|
});
|
|
|
|
test("row 195 accept: F2 fails when healthz doesn't answer and when another pid holds the port", (t) => {
|
|
let h = host(t);
|
|
put(join(h.hc, "curl-rc"), "7");
|
|
let r = mustRun(h);
|
|
assert.deepEqual(failed(r), ["F2"]);
|
|
assert.match(r.out, /^F2 console-loopback FAIL curl http:\/\/127\.0\.0\.1:732\/healthz exited 7$/m);
|
|
h = host(t);
|
|
put(join(h.hc, "ss"), SS.replace("pid=200,fd=21", "pid=555,fd=4"));
|
|
r = mustRun(h);
|
|
assert.deepEqual(failed(r), ["F2"]);
|
|
});
|
|
|
|
test("row 195 accept: F5 fails on unpriv_bwrap (enforce), on complain and on a trailing space", (t) => {
|
|
for (const label of ["unpriv_bwrap (enforce)\n", "bwrap//&unpriv_bwrap (complain)\n", `${CHILD} \n`]) {
|
|
const h = host(t);
|
|
put(join(h.root, "proc", "302", "attr", "current"), label);
|
|
const r = mustRun(h);
|
|
assert.deepEqual(failed(r), ["F5"], `${JSON.stringify(label)}: ${r.out}`);
|
|
}
|
|
const h = host(t);
|
|
put(join(h.root, "proc", "300", "attr", "current"), `${OUTER} \n`);
|
|
assert.deepEqual(failed(mustRun(h)), ["F5"]);
|
|
});
|
|
|
|
test("row 195 accept: F5 fails with no bwrap of mosaic's, and passes on comm alone", (t) => {
|
|
let h = host(t);
|
|
proc(h.root, 300, { ppid: 100, uid: 1001, exe: "/usr/bin/bwrap", comm: "bwrap", label: `${OUTER}\n` });
|
|
let r = mustRun(h);
|
|
assert.deepEqual(failed(r), ["F5"]);
|
|
assert.match(r.out, /^F5 bwrap-label FAIL no \/usr\/bin\/bwrap process of mosaic/m);
|
|
h = host(t);
|
|
proc(h.root, 302, { ppid: 301, argv0: "/proc/self/fd/3", comm: "claude", label: `${CHILD}\n` });
|
|
assert.deepEqual(failed(mustRun(h)), []);
|
|
});
|
|
|
|
test("row 195 accept: F6 fails on a stub that prints nothing, on a path outside claude-<pin> and on a .. segment", (t) => {
|
|
let h = host(t);
|
|
put(join(h.hc, "claude-version"), "");
|
|
let r = mustRun(h);
|
|
assert.deepEqual(failed(r), ["F6"]);
|
|
h = host(t);
|
|
const other = CLAUDE.replace(`claude-${CLAUDE_PIN}`, "claude-2.0.9");
|
|
put(h.args, ARGS.map((a) => (a === CLAUDE ? other : a)).join("\n") + "\n", 0o644);
|
|
put(join(h.root, other), `#!/usr/bin/env bash\necho "${CLAUDE_PIN} (Claude Code)"\n`, 0o755);
|
|
r = mustRun(h);
|
|
assert.deepEqual(failed(r), ["F6"]);
|
|
assert.match(r.out, /^F6 claude-pin FAIL --claude \S+claude-2\.0\.9\S+ is not under \/opt\/mosaic-stack\/claude-2\.1\.0\/$/m);
|
|
// Under claude-<pin>/ by prefix, but the binary it names is 2.0.9's, which answers with the pin.
|
|
h = host(t);
|
|
const dotdot = CLAUDE.replace(`claude-${CLAUDE_PIN}/`, `claude-${CLAUDE_PIN}/../claude-2.0.9/`);
|
|
put(h.args, ARGS.map((a) => (a === CLAUDE ? dotdot : a)).join("\n") + "\n", 0o644);
|
|
put(join(h.root, other), `#!/usr/bin/env bash\necho "${CLAUDE_PIN} (Claude Code)"\n`, 0o755);
|
|
r = mustRun(h);
|
|
assert.deepEqual(failed(r), ["F6"], r.out);
|
|
assert.match(r.out, /^F6 claude-pin FAIL --claude \S+\/claude-2\.1\.0\/\.\.\/claude-2\.0\.9\/\S+ has a \.\. segment$/m);
|
|
});
|
|
|
|
test("row 195 accept: F7 fails on two live PM launches, one for another business, and no launch.by", (t) => {
|
|
const two = [{ instance: "pm", live: true, business: "mosaic-stack", run: "a" }, { instance: "pm", live: true, business: "mosaic-stack", run: "b" }];
|
|
for (const [file, text] of [["launches.json", JSON.stringify(two)], ["launches.json", JSON.stringify([{ instance: "pm", live: true, business: "other", run: "a" }])]]) {
|
|
const h = host(t);
|
|
put(join(h.hc, file), text);
|
|
assert.deepEqual(failed(mustRun(h)), ["F7"]);
|
|
}
|
|
const h = host(t);
|
|
put(join(h.root, HOME, ".config", "mosaic-dev", "businesses", "mosaic-stack.json"), JSON.stringify({ id: "mosaic-stack" }));
|
|
const r = mustRun(h);
|
|
assert.deepEqual(failed(r), ["F7"]);
|
|
assert.match(r.out, /^F7 pm-live FAIL the business file has no launch\.by$/m);
|
|
});
|
|
|
|
test("row 195 accept: F8 fails on 0640, on another owner and on a symlink, and never prints the credential", (t) => {
|
|
for (const fault of [
|
|
(h) => put(join(h.hc, "stat-cred"), "mosaic 640 regular file\n"),
|
|
(h) => put(join(h.hc, "stat-cred"), "root 600 regular file\n"),
|
|
(h) => { const f = join(h.root, CRED); rmSync(f); put(`${f}.real`, `X=${CRED_VALUE}\n`, 0o600); symlinkSync(`${f}.real`, f); },
|
|
]) {
|
|
const h = host(t);
|
|
fault(h);
|
|
const r = mustRun(h);
|
|
assert.deepEqual(failed(r), ["F8"], r.out);
|
|
assert.ok(!(r.out + r.err).includes(CRED_VALUE));
|
|
}
|
|
});
|
|
|
|
test("row 195 accept: F9 fails on another owner, a group-writable mode and a number for the flag", (t) => {
|
|
for (const fault of [
|
|
(h) => put(join(h.hc, "stat-settings"), "root:root 664 regular file\n"),
|
|
(h) => put(join(h.hc, "stat-settings"), "mosaic:root 644 regular file\n"),
|
|
(h) => put(join(h.root, "etc", "claude-code", "managed-settings.json"), JSON.stringify({ env: { DISABLE_AUTOUPDATER: 1 } })),
|
|
]) {
|
|
const h = host(t);
|
|
fault(h);
|
|
assert.deepEqual(failed(mustRun(h)), ["F9"]);
|
|
}
|
|
});
|
|
|
|
test("row 195 accept: F9 won't run --node as root off the pin path or with a writable directory above it", (t) => {
|
|
// Another node that works for the stack's user: F9 refuses to run it, and F10 fails on the path.
|
|
let h = host(t);
|
|
const other = NODE.replace(NODE_PIN, "v24.0.9");
|
|
put(h.args, ARGS.map((a) => (a === NODE ? other : a)).join("\n") + "\n", 0o644);
|
|
put(join(h.root, other), `#!/usr/bin/env bash\n${NODE_STUB}\n`, 0o755);
|
|
let r = mustRun(h);
|
|
assert.deepEqual(failed(r), ["F9", "F10"], r.out);
|
|
assert.match(r.out, /^F9 autoupdater-off FAIL root won't run --node \/opt\/mosaic-stack\/node-v24\.0\.9\/bin\/node: it is not the --node-pin path$/m);
|
|
assert.ok(!existsSync(join(h.hc, "node-as-root")));
|
|
// The pinned path, under a group-writable directory and then a symlinked node: F9 alone fails, and
|
|
// root doesn't run it.
|
|
for (const [fault, bad] of [
|
|
[(x) => chmodSync(join(x.root, dirname(NODE)), 0o775), "/opt/mosaic-stack/node-v24\\.1\\.0/bin \\(uid \\d+, mode 775\\)"],
|
|
[(x) => { const f = join(x.root, NODE); renameSync(f, `${f}.real`); symlinkSync(`${f}.real`, f); }, "/opt/mosaic-stack/node-v24\\.1\\.0/bin/node \\(uid \\d+, mode 777\\)"],
|
|
]) {
|
|
h = host(t);
|
|
fault(h);
|
|
r = mustRun(h);
|
|
assert.deepEqual(failed(r), ["F9"], r.out);
|
|
assert.match(r.out, new RegExp(`^F9 autoupdater-off FAIL root won't run --node \\S+: ${bad} is not root's alone$`, "m"));
|
|
assert.ok(!existsSync(join(h.hc, "node-as-root")));
|
|
}
|
|
});
|
|
|
|
test("row 195 accept: F11 fails on one *.json under discord/ and passes on an empty directory", (t) => {
|
|
let h = host(t);
|
|
put(join(h.root, HOME, ".mosaic-dev", "discord", "chief.json"), "{}");
|
|
let r = mustRun(h);
|
|
assert.deepEqual(failed(r), ["F11"]);
|
|
assert.match(r.out, /^F11 no-discord FAIL \/srv\/mosaic\/\.mosaic-dev\/discord holds bindings: chief\.json$/m);
|
|
h = host(t);
|
|
mkdirSync(join(h.root, HOME, ".mosaic-dev", "discord"), { recursive: true });
|
|
assert.deepEqual(failed(mustRun(h)), []);
|
|
});
|
|
|
|
// Each P check in its unnamed mark, with the line count each fault makes; none changes the exit.
|
|
const P_FAULTS = {
|
|
P1: [1, (h) => put(join(h.hc, "sudo-l-mosaic"), "User mosaic may run the following commands on dev-53:\n")],
|
|
P2: [1, (h) => put(join(h.root, HOME, ".ssh", "authorized_keys"), "ssh-ed25519 AAAA test\n")],
|
|
P5: [1, (h) => edit(join(h.root, "etc", "group"), (s) => s.replace("docker:x:999:", "docker:x:999:jarvis"))],
|
|
P6: [2, (h) => edit(join(h.root, "etc", "group"), (s) => s.replace("sudo:x:27:localadmin", "sudo:x:27:localadmin,bob").replace("lxd:x:998:", "lxd:x:998:carol"))],
|
|
P7: [1, (h) => put(join(h.root, "etc", "sudoers.d", "50-mosaic"), "mosaic ALL=(root) NOPASSWD: /usr/bin/systemctl\n")],
|
|
P8: [1, (h) => put(join(h.hc, "ss"), `${SS}LISTEN 0 511 0.0.0.0:7331 0.0.0.0:* users:(("node",pid=200,fd=23))\n`)],
|
|
P9: [3, (h) => { put(join(h.hc, "hostcheck-out"), HOSTCHECK_OK.replace("ok 2 ", "FAIL 2 ").replace("ok 5 ", "FAIL 5 ")); put(join(h.hc, "jarvis-bwrap-rc"), "0"); }],
|
|
P10: [1, (h) => put(join(h.root, "proc", "sys", "net", "ipv4", "ip_unprivileged_port_start"), "700\n")],
|
|
P11: [1, (h) => put(join(h.hc, "show-mosaic-hostcheck.service"), "[email protected] multi-user.target\n")],
|
|
};
|
|
|
|
test("row 195 accept: each P check reports unnamed on its fault, and the exit stays 0", (t) => {
|
|
for (const [id, [n, fault]] of Object.entries(P_FAULTS)) {
|
|
const h = host(t);
|
|
fault(h);
|
|
const r = run(h);
|
|
assert.equal(r.status, 0, `${id}: ${r.out}`);
|
|
assert.deepEqual(marksOf(r, id), Array(n).fill("unnamed"), `${id}: ${r.out}`);
|
|
}
|
|
});
|
|
|
|
test("row 195 accept: P2 is unnamed with a supplementary group, and P5 names docker", (t) => {
|
|
let h = host(t);
|
|
put(join(h.root, "etc", "group"), GROUP.replace("lxd:x:998:", "lxd:x:998:mosaic"));
|
|
let r = run(h);
|
|
assert.equal(r.status, 0);
|
|
assert.deepEqual(marksOf(r, "P2"), ["unnamed"]);
|
|
assert.match(r.out, /^P2 mosaic-groups-keys unnamed id -G is '997 998'; authorized_keys is absent$/m);
|
|
h = host(t);
|
|
P_FAULTS.P5[1](h);
|
|
r = run(h);
|
|
assert.match(r.out, /^P5 jarvis-reach unnamed jarvis is in docker$/m);
|
|
assert.match(r.out, /^P6 group-docker unnamed jarvis is a member of docker$/m);
|
|
});
|
|
|
|
test("row 195 accept: P5 is closed with no jarvis account, and P2 with an empty authorized_keys", (t) => {
|
|
const h = host(t);
|
|
put(join(h.root, "etc", "passwd"), PASSWD.replace(/^jarvis:.*\n/m, ""));
|
|
put(join(h.root, HOME, ".ssh", "authorized_keys"), "");
|
|
const r = run(h);
|
|
assert.deepEqual(marksOf(r, "P5"), ["closed"]);
|
|
assert.deepEqual(marksOf(r, "P2"), ["closed"]);
|
|
});
|
|
|
|
test("row 195 accept: P9 is unnamed once for each item missing", (t) => {
|
|
const items = {
|
|
hostcheck: (h) => put(join(h.hc, "hostcheck-out"), HOSTCHECK_OK.replace("ok 3 ", "FAIL 3 ")),
|
|
jarvis: (h) => { put(join(h.hc, "jarvis-bwrap-rc"), "0"); put(join(h.hc, "jarvis-bwrap"), ""); },
|
|
member: (h) => put(join(h.root, "etc", "group"), GROUP.replace("mosaic:x:997:", "mosaic:x:997:jarvis")),
|
|
gid: (h) => put(join(h.root, "etc", "passwd"), `${PASSWD}hermes:x:1002:997::/home/hermes:/bin/bash\n`),
|
|
};
|
|
for (const [name, fault] of Object.entries(items)) {
|
|
const h = host(t);
|
|
fault(h);
|
|
const r = run(h);
|
|
assert.equal(r.status, 0);
|
|
assert.deepEqual(marksOf(r, "P9"), ["unnamed"], `${name}: ${r.out}`);
|
|
}
|
|
const h = host(t);
|
|
put(join(h.hc, "hostcheck-out"), "");
|
|
assert.deepEqual(marksOf(run(h), "P9"), Array(6).fill("unnamed"));
|
|
});
|
|
|
|
test("row253 accept: P10 fails its observation with another peer; P11 with the unit disabled", (t) => {
|
|
let h = host(t);
|
|
put(join(h.hc, "peer"), "555 997 997\n");
|
|
let r = run(h);
|
|
assert.equal(r.status, 0);
|
|
assert.deepEqual(marksOf(r, "P10"), ["unnamed"]);
|
|
assert.match(r.out, /P10 tester-socket-squat unnamed boundary=failed reason=peer/);
|
|
h = host(t);
|
|
put(join(h.hc, "enabled-mosaic-hostcheck.service"), "disabled\n");
|
|
r = run(h);
|
|
assert.deepEqual(marksOf(r, "P11"), ["unnamed"]);
|
|
});
|
|
|
|
test("row 195 accept: every P check unnamed together still exits 0", (t) => {
|
|
const h = host(t);
|
|
for (const [, fault] of Object.values(P_FAULTS)) fault(h);
|
|
const r = run(h);
|
|
assert.equal(r.status, 0, r.out + r.err);
|
|
assert.deepEqual(failed(r), []);
|
|
for (const id of ["P1", "P2", "P5", "P6", "P7", "P8", "P9", "P10", "P11"]) assert.ok(marksOf(r, id).every((m) => m === "unnamed"), id);
|
|
assert.match(r.lines.at(-1), /^case1-accept: every F check passes/);
|
|
});
|
|
|
|
test("row 195 accept: a bad args file refuses with exit 2 before any check, each for its own reason", (t) => {
|
|
const bad = {
|
|
"group write": [(h) => chmodSync(h.args, 0o664), /dev\.args must be \S+'s with no group or other write \(it is uid \d+, mode 664\)$/],
|
|
symlink: [(h) => { put(`${h.args}.real`, ARGS.join("\n") + "\n", 0o644); rmSync(h.args); symlinkSync(`${h.args}.real`, h.args); }, /no regular file \/etc\/mosaic-stack\/dev\.args$/],
|
|
"unknown flag": [(h) => put(h.args, ["--send", ...ARGS].join("\n") + "\n", 0o644), /has --send before --, which case2-unit\.sh does not take$/],
|
|
"two copies": [(h) => put(h.args, ["--copy", COPY, ...ARGS].join("\n") + "\n", 0o644), /needs exactly one --copy before --, not 2$/],
|
|
"library profile": [(h) => put(h.args, ARGS.map((a) => (a === "factory" ? "library" : a)).join("\n") + "\n", 0o644), /has --profile 'library', not factory$/],
|
|
"another business": [(h) => put(h.args, ARGS.map((a, i) => (a === "mosaic-stack" && i < 14 ? "acme" : a)).join("\n") + "\n", 0o644), /is for business acme, not mosaic-stack$/],
|
|
"no tester socket": [(h) => put(h.args, ARGS.slice(0, -1).join("\n") + "\n", 0o644), /needs one bare --tester-socket after --$/],
|
|
"retired TCP": [(h) => put(h.args, [...ARGS.slice(0,-1),'--tester-port','7333'].join('\n')+'\n',0o644), /case1-accept.sh retires tester TCP; use --tester-socket$/],
|
|
"socket value": [(h) => put(h.args, [...ARGS,'7333'].join('\n')+'\n',0o644), /unknown serve flag 7333$/],
|
|
"duplicate socket": [(h) => put(h.args, [...ARGS,'--tester-socket'].join('\n')+'\n',0o644), /needs one bare --tester-socket after --$/],
|
|
"short copy": [(h) => put(h.args, ARGS.map((a) => (a === COPY ? "abc123" : a)).join("\n") + "\n", 0o644), /lacks a plain --user, --home, --business, --node, --claude or --copy$/],
|
|
"no file": [(h) => rmSync(h.args), /no regular file \/etc\/mosaic-stack\/dev\.args$/],
|
|
};
|
|
for (const [name, [fault, why]] of Object.entries(bad)) {
|
|
const h = host(t);
|
|
fault(h);
|
|
const r = run(h);
|
|
assert.equal(r.status, 2, `${name}: ${r.out}${r.err}`);
|
|
assert.match(r.err.trimEnd(), /^case1-accept: refusing: /, name);
|
|
assert.match(r.err.trimEnd(), why, name);
|
|
assert.equal(r.out, "", name);
|
|
assert.ok(!existsSync(join(h.hc, "calls.log")), name);
|
|
}
|
|
});
|
|
|
|
test("row 195 accept: usage errors and a run without --root as a user exit 2", (t) => {
|
|
const h = host(t);
|
|
let r = run(h, {}, ["--business", "mosaic-stack", "--args", "/etc/mosaic-stack/dev.args", "--node-pin", NODE_PIN, "--root", h.root]);
|
|
assert.equal(r.status, 2);
|
|
assert.match(r.err, /^usage: /);
|
|
r = run(h, {}, ["--business", "mosaic-stack", "--args", "/etc/mosaic-stack/dev.args", "--claude-pin", CLAUDE_PIN, "--node-pin", "24.1.0", "--root", h.root]);
|
|
assert.equal(r.status, 2);
|
|
r = run(h, {}, ["--business", "mosaic-stack", "--args", "/etc/mosaic-stack/dev.args", "--claude-pin", CLAUDE_PIN, "--node-pin", NODE_PIN]);
|
|
assert.equal(r.status, 2);
|
|
assert.equal(r.err, "case1-accept: refusing: run it as root\n");
|
|
assert.ok(!existsSync(join(h.hc, "calls.log")));
|
|
});
|
|
|
|
// The selector (rename design r2): --org and --business are one option in each of three scopes, the
|
|
// script's own arguments and the args file's unit and serve arguments either side of --. A spelling
|
|
// may appear once per scope, and the other spelling only with the same value; never counted across --.
|
|
const SELECT = { old: ["--business", "mosaic-stack"], new: ["--org", "mosaic-stack"],
|
|
both: ["--business", "mosaic-stack", "--org", "mosaic-stack"], swapped: ["--org", "mosaic-stack", "--business", "mosaic-stack"] };
|
|
// The args file with outer unit selector tokens and serve selector tokens in ARGS's places.
|
|
const argsWith = (outer, serve) => [...ARGS.slice(0, 6), ...outer, ...ARGS.slice(8, 16), "--port", "732", ...serve, "--tester-socket"];
|
|
const invocation = (h, sel) => [...sel, "--args", "/etc/mosaic-stack/dev.args", "--claude-pin", CLAUDE_PIN, "--node-pin", NODE_PIN, "--root", h.root];
|
|
|
|
test("row 195 accept: --org or --business names the business in each of the three scopes, and talk and precheck still get --business", (t) => {
|
|
const runs = [["old", "old", "old"], ["new", "new", "new"], ["both", "swapped", "both"], ["swapped", "both", "swapped"],
|
|
["new", "old", "new"], ["old", "new", "old"], ["both", "old", "swapped"]];
|
|
const seen = [];
|
|
for (const [inv, outer, serve] of runs) {
|
|
const h = host(t), name = `${inv} ${outer} -- ${serve}`;
|
|
put(h.args, argsWith(SELECT[outer], SELECT[serve]).join("\n") + "\n", 0o644);
|
|
const r = run(h, {}, invocation(h, SELECT[inv]));
|
|
assert.equal(r.status, 0, `${name}: ${r.out}${r.err}`);
|
|
assert.deepEqual(failed(r), [], name);
|
|
const talk = calls(h, "mosaic").filter((a) => a.startsWith("talk "));
|
|
assert.equal(talk.length, 1, name);
|
|
assert.match(talk[0], /^talk chief case1-accept-[0-9a-f]{12}: .* --business mosaic-stack --tester --wait 300$/, name);
|
|
assert.deepEqual(calls(h, "precheck"), ["--business mosaic-stack --profile factory"], name);
|
|
seen.push(r.lines.filter((l) => /^[FP][0-9]+ /.test(l)).map((l) => l.split(" ").slice(0, 3).join(" ")));
|
|
}
|
|
for (const marks of seen) assert.deepEqual(marks, seen[0]);
|
|
});
|
|
|
|
test("row 195 accept: a bad selector in any scope, or scopes that name different businesses, refuse with exit 2 before any check", (t) => {
|
|
const usage = "usage: case1-accept.sh --business ID --args FILE --claude-pin PIN --node-pin vN.N.N\n";
|
|
const refusing = (why) => `case1-accept: refusing: ${why}\n`;
|
|
const before = (why) => refusing(`/etc/mosaic-stack/dev.args before --: ${why}`);
|
|
const after = (why) => refusing(`/etc/mosaic-stack/dev.args after --: ${why}`);
|
|
const cases = [
|
|
// The script's own arguments.
|
|
["invocation unequal", ["--business", "mosaic-stack", "--org", "beta"], null, refusing("--business mosaic-stack and --org beta conflict")],
|
|
["invocation unequal, new first", ["--org", "mosaic-stack", "--business", "beta"], null, refusing("--org mosaic-stack and --business beta conflict")],
|
|
["invocation old twice", ["--business", "mosaic-stack", "--business", "mosaic-stack"], null, refusing("--business given twice")],
|
|
["invocation new twice", ["--org", "mosaic-stack", "--org", "mosaic-stack"], null, refusing("--org given twice")],
|
|
["invocation new empty", ["--org", ""], null, usage],
|
|
["invocation new flag-shaped", ["--org", "--args"], null, usage],
|
|
["invocation new invalid", ["--org", "Mosaic"], null, usage],
|
|
["invocation old invalid", ["--business", "a/../b"], null, usage],
|
|
// The args file's unit arguments.
|
|
["unit unequal", ["--org", "mosaic-stack"], argsWith(["--business", "mosaic-stack", "--org", "beta"], SELECT.old), before("--business mosaic-stack and --org beta conflict")],
|
|
["unit unequal, new first", ["--org", "mosaic-stack"], argsWith(["--org", "mosaic-stack", "--business", "beta"], SELECT.old), before("--org mosaic-stack and --business beta conflict")],
|
|
["unit old twice", ["--org", "mosaic-stack"], argsWith(["--business", "mosaic-stack", "--business", "mosaic-stack"], SELECT.old), before("--business given twice")],
|
|
["unit new twice", ["--org", "mosaic-stack"], argsWith(["--org", "mosaic-stack", "--org", "mosaic-stack"], SELECT.new), before("--org given twice")],
|
|
["unit new missing", SELECT.old, [...ARGS.slice(0, 6), ...ARGS.slice(8, 15), "--org"], before("--org needs one value")],
|
|
["unit new flag-shaped", SELECT.old, argsWith(["--org"], SELECT.old), before("--org needs one value, not --copy")],
|
|
["unit new invalid", SELECT.old, argsWith(["--org", "Mosaic"], SELECT.old), before("--org Mosaic is not valid")],
|
|
["unit names another business", SELECT.new, argsWith(["--org", "beta"], SELECT.new), refusing("/etc/mosaic-stack/dev.args is for business beta, not mosaic-stack")],
|
|
// --home takes --org as its value, so --business is the unit's one selector and the home refuses.
|
|
["unit --home value", SELECT.old, argsWith(["--business", "mosaic-stack"], SELECT.old).map((a, i) => (i === 5 ? "--org" : a)),
|
|
refusing("/etc/mosaic-stack/dev.args lacks a plain --user, --home, --business, --node, --claude or --copy")],
|
|
// The args file's serve arguments.
|
|
["serve unequal", SELECT.old, argsWith(SELECT.old, ["--business", "mosaic-stack", "--org", "beta"]), after("--business mosaic-stack and --org beta conflict")],
|
|
["serve unequal, new first", SELECT.old, argsWith(SELECT.old, ["--org", "mosaic-stack", "--business", "beta"]), after("--org mosaic-stack and --business beta conflict")],
|
|
["serve old twice", SELECT.old, argsWith(SELECT.old, ["--business", "mosaic-stack", "--business", "mosaic-stack"]), after("--business given twice")],
|
|
["serve new twice", SELECT.old, argsWith(SELECT.new, ["--org", "mosaic-stack", "--org", "mosaic-stack"]), after("--org given twice")],
|
|
["serve new missing", SELECT.old, [...argsWith(SELECT.old, []), "--org"], after("--org needs one value")],
|
|
["serve new flag-shaped", SELECT.old, argsWith(SELECT.old, ["--org"]), after("--org needs one value, not --tester-socket")],
|
|
["serve new invalid", SELECT.old, argsWith(SELECT.old, ["--org", "Mosaic"]), after("--org Mosaic is not valid")],
|
|
["serve names another business", SELECT.new, argsWith(SELECT.new, ["--org", "beta"]), refusing("serve business differs")],
|
|
["serve has no selector", SELECT.old, argsWith(SELECT.old, []), refusing("/etc/mosaic-stack/dev.args needs --business or --org after --")],
|
|
// --port takes --org as its value; the serve selector is the --business after it.
|
|
["serve --port value", SELECT.old, [...argsWith(SELECT.old, []).slice(0, 17), "--org", "--business", "mosaic-stack", "--tester-socket"],
|
|
refusing("/etc/mosaic-stack/dev.args lacks a privileged numeric --port after --")],
|
|
];
|
|
for (const [name, sel, file, err] of cases) {
|
|
const h = host(t);
|
|
if (file) put(h.args, file.join("\n") + "\n", 0o644);
|
|
const r = run(h, {}, invocation(h, sel));
|
|
assert.equal(r.status, 2, `${name}: ${r.out}${r.err}`);
|
|
assert.equal(r.err, err, name);
|
|
assert.equal(r.out, "", name);
|
|
assert.ok(!existsSync(join(h.hc, "calls.log")), name);
|
|
}
|
|
// A missing value at the end of the script's own arguments is a usage error.
|
|
const h = host(t);
|
|
const r = run(h, {}, ["--args", "/etc/mosaic-stack/dev.args", "--claude-pin", CLAUDE_PIN, "--node-pin", NODE_PIN, "--root", h.root, "--org"]);
|
|
assert.equal(r.status, 2);
|
|
assert.equal(r.err, usage);
|
|
});
|
|
|
|
test("row 195 accept: a copy of the script without selector.sh beside it refuses before parsing", (t) => {
|
|
const h = host(t), lone = join(h.dir, "lone", "case1-accept.sh");
|
|
put(lone, read(SCRIPT), 0o755);
|
|
const r = run(h, {}, undefined, lone);
|
|
assert.equal(r.status, 2, r.out + r.err);
|
|
assert.ok(r.err.endsWith(`case1-accept: refusing: cannot read ${join(h.dir, "lone", "selector.sh")}\n`), r.err);
|
|
assert.equal(r.out, "");
|
|
assert.ok(!existsSync(join(h.hc, "calls.log")));
|
|
});
|
|
|
|
test("row 195 accept: every in-instance call goes through sudo -u mosaic -H env PATH=<node dir>:...", (t) => {
|
|
const h = host(t);
|
|
const r = run(h);
|
|
assert.equal(r.status, 0, r.out + r.err);
|
|
const sudo = calls(h, "sudo");
|
|
const asMosaic = sudo.filter((a) => a.startsWith("-u mosaic"));
|
|
const isolated = asMosaic.filter(a=>a.startsWith('-u mosaic -H env -i '));
|
|
assert.equal(isolated.length, 2);
|
|
for(const a of isolated) assert.ok(a.startsWith(`-u mosaic -H env -i HOME=${HOME} PATH=/opt/mosaic-stack/node-${NODE_PIN}/bin:/usr/local/bin:/usr/bin:/bin `),a);
|
|
for (const a of asMosaic.filter(a=>!isolated.includes(a))) assert.ok(a.startsWith(`${AS_MOSAIC} /`), a);
|
|
const cmds = asMosaic.filter(a=>!isolated.includes(a)).map((a) => a.slice(AS_MOSAIC.length + 1).split(" ")[0]);
|
|
assert.equal(asMosaic.length,7,'two metadata/probe calls and five legacy calls');
|
|
assert.deepEqual([...new Set(cmds)].sort(), ['/usr/bin/bash']);
|
|
// mos-1326: assert every argv element, not flattened/substr command detection.
|
|
const source=read(SCRIPT);
|
|
const supervisor=source.split("<<'MOSAIC_TESTER_SUPERVISOR'")[1].split('\n').slice(1).join('\n').split('\nMOSAIC_TESTER_SUPERVISOR')[0]+'\n';
|
|
assert.equal(createHash('sha256').update(supervisor).digest('hex'),'52509773fa2bb0701402b8d2454816153e18323cfaa75cb72418b46c4dbb2d34');
|
|
const runner=/LEGACY_COMMAND_RUNNER='([\s\S]*?)'\nlegacy_display_rc/.exec(source)[1];
|
|
const f7=/F7_JS='([\s\S]*?)'\ncheck_f7/.exec(source)[1];
|
|
const raw=read(join(h.hc,'sudo-argv.jsonl')).trim().split('\n').map(line=>JSON.parse(line));
|
|
const legacy=raw.filter(a=>a[0]==='-u'&&a[1]==='mosaic'&&a[4].startsWith('PATH='));
|
|
const marker=legacy[0][20].split(':')[0];
|
|
const argv=(tag,inner,command)=>['-u','mosaic','-H','env',`PATH=/opt/mosaic-stack/node-${NODE_PIN}/bin:/usr/local/bin:/usr/bin:/bin`,
|
|
'/usr/bin/bash','-c',supervisor,'--',String(inner),'/usr/bin/setsid','/usr/bin/sleep',NODE,'-e',runner,'--',tag,...command];
|
|
assert.deepEqual(legacy,[
|
|
argv('talk',320,[CLI,'talk','chief',marker+': this is the Case 1 acceptance; reply with one short line.','--business','mosaic-stack','--tester','--wait','300']),
|
|
argv('precheck',200,[PRECHECK,'--business','mosaic-stack','--profile','factory']),
|
|
argv('claude-version',5,[CLAUDE,'--version']),
|
|
argv('pm-list',10,[NODE,'-e',f7,'--',`${HOME}/.config/mosaic-dev/businesses/mosaic-stack.json`,CLI,'mosaic-stack']),
|
|
argv('node-version',5,[NODE,'--version']),
|
|
]);
|
|
assert.deepEqual(sudo.filter((a) => !a.startsWith("-u mosaic")).sort(),
|
|
["-l -U localadmin", "-l -U mosaic", "-u jarvis /usr/bin/bwrap --version"]);
|
|
const talk = calls(h, "mosaic").filter((a) => a.startsWith("talk "));
|
|
assert.equal(talk.length, 1);
|
|
assert.match(talk[0], /^talk chief case1-accept-[0-9a-f]{12}: .* --business mosaic-stack --tester --wait 300$/);
|
|
assert.deepEqual(calls(h, "precheck"), ["--business mosaic-stack --profile factory"]);
|
|
});
|
|
|
|
test("row 195 accept: the host check runs with --check and never --enforce", (t) => {
|
|
const h = host(t);
|
|
run(h);
|
|
assert.deepEqual(calls(h, "hostcheck"), ["--check"]);
|
|
});
|
|
|
|
|
|
// Row276 exercises the real legacy launcher with fixture-only commands, never sudo.
|
|
function legacyBody(source=read(SCRIPT)) {
|
|
const start=source.indexOf('# row253-supervisor-begin'),end=source.indexOf('# row276-legacy-end');
|
|
assert.ok(start>=0&&end>start,'legacy supervisor block exists');
|
|
return source.slice(start,end);
|
|
}
|
|
function legacyCall(h,tag,command,{inner='0.5',outer='2',body=legacyBody()}={}) {
|
|
return spawnSync('/bin/bash',['-c',body+'\nlegacy_run "$@"','--',tag,String(inner),String(outer),...command],{
|
|
encoding:'utf8',env:{...process.env,PATH:`${h.bin}:${process.env.PATH}`,HC:h.hc,ROOT_DIR:h.root,REAL_NODE:process.execPath,TMPDIR:h.dir,
|
|
S_USER:'mosaic',S_HOME:'/srv/mosaic',NODE,WORK:h.dir},
|
|
});
|
|
}
|
|
test('row276 legacy call inventory pins every production bound and the conversation contract',()=>{
|
|
const s=read(SCRIPT);
|
|
assert.equal(s.includes('${AS_USER[@]}'),false);
|
|
for(const x of LEGACY_CASES)assert.ok(s.includes(`legacy_run ${x.tag} ${x.inner} ${x.outer} `),x.tag);
|
|
assert.match(s,/legacy_run talk 320 330 .*--tester --wait 300/);
|
|
assert.match(s,/legacy_display_rc talk/);assert.match(s,/legacy_display_rc precheck/);
|
|
});
|
|
for(const {tag} of LEGACY_CASES)test(`row276 ${tag} legacy inner timeout cleans both sudo models`,async t=>{
|
|
for(const mode of ['exec','hop']){
|
|
const h=host(t),records=join(h.hc,'legacy-pids'),files=[join(h.hc,'session-pids'),records];
|
|
put(join(h.hc,'session-mode'),mode);
|
|
const file=join(h.dir,'blocked.mjs');put(file,blockingQuerySource(records,'signals')+'\nloadSystem();');
|
|
const clock=join(h.dir,'inner-clock');put(clock,'',0o600);
|
|
const body=legacyBody(instrumentInnerTimer(read(SCRIPT),clock));
|
|
const capture=cleanupFixture(t,files),stop=fixtureGuard(t,files,4500,clock);
|
|
// The outer watchdog includes startup and cleanup; only the supervisor pair
|
|
// measures the inner budget. Seven seconds also lets the 4.5s guard expose a missing timer.
|
|
const start=performance.now(),r=legacyCall(h,tag,[NODE,file],{outer:7,body});capture();
|
|
assert.equal(r.status,81,r.stdout+r.stderr);
|
|
assert.equal(r.stderr,`case1-accept: ${tag} inner-timeout\n`);
|
|
const seconds=readProbeSeconds(read(clock)),wall=(performance.now()-start)/1000;
|
|
assert.ok(seconds>=0.25&&seconds<1.25,`${tag}/${mode}: inner timer ${seconds}s (wall ${wall}s)`);
|
|
assert.ok(wall>=seconds,`${mode}: the call contains its timer interval`);
|
|
t.diagnostic(`${tag}/${mode}: inner ${seconds}s; wall ${wall}s`);
|
|
assert.deepEqual(await pollFixtureGone(files),[],mode);await stop();
|
|
}
|
|
});
|
|
test('row276 legacy streams and display status preserve failures without granting success',t=>{
|
|
for(const mode of ['exec','hop'])for(const code of [0,1,7,81,137]){
|
|
const h=host(t);put(join(h.hc,'session-mode'),mode);
|
|
const r=legacyCall(h,'talk',[NODE,'-e',`process.stdout.write('sent retained\\nreply retained\\n');process.stderr.write('original diagnostic\\n');process.exitCode=${code}`],{inner:3,outer:5});
|
|
assert.equal(r.status,code===0?0:87,r.stderr);
|
|
assert.equal(r.stdout,'sent retained\nreply retained\n');assert.equal(r.stderr,'original diagnostic\n');
|
|
assert.equal(read(join(h.dir,'legacy-talk.rc')),String(code===0?0:code)+'\n');
|
|
}
|
|
});
|
|
test('row276 trailer grammar cannot make supervisor failure pass',t=>{
|
|
for(const trailer of ['0','256','999','01','+1','-1','1-no-newline','missing']){
|
|
const h=host(t),body=legacyBody().replace('process.stderr.write("\\nMOSAIC_LEGACY_STATUS="+code+"\\n");',
|
|
trailer==='missing'?'':trailer==='1-no-newline'?'process.stderr.write("\\nMOSAIC_LEGACY_STATUS=1");':`process.stderr.write("\\nMOSAIC_LEGACY_STATUS=${trailer}\\n");`);
|
|
assert.notEqual(body,legacyBody(),'runner trailer seam matched');
|
|
const r=legacyCall(h,'talk',[NODE,'-e','process.exitCode=7'],{body,inner:3,outer:5});
|
|
assert.equal(r.status,87,r.stderr);assert.equal(read(join(h.dir,'legacy-talk.rc')),'87\n');
|
|
assert.ok(r.stderr.endsWith('case1-accept: talk inner-command\n'),r.stderr);
|
|
}
|
|
});
|
|
test('row276 earlier forged trailer and appended zero remain failures',t=>{
|
|
for(const late of [false,true]){
|
|
const h=host(t);let body=legacyBody();
|
|
if(late)body=body.replace('process.stderr.write("\\nMOSAIC_LEGACY_STATUS="+code+"\\n");','process.stderr.write("\\nMOSAIC_LEGACY_STATUS="+code+"\\nMOSAIC_LEGACY_STATUS=0\\n");');
|
|
const r=legacyCall(h,'talk',[NODE,'-e','process.stderr.write("\\nMOSAIC_LEGACY_STATUS=0\\n");process.exitCode=7'],{body,inner:3,outer:5});
|
|
assert.equal(r.status,87,r.stderr);assert.equal(read(join(h.dir,'legacy-talk.rc')),late?'87\n':'7\n');
|
|
}
|
|
});
|
|
test('row276 precheck outer bound discards success status and fixture cleans residual',async t=>{
|
|
for(const mode of ['exec','hop']){
|
|
const h=host(t),files=[join(h.hc,'session-pids')];put(join(h.hc,'session-mode'),mode);put(join(h.hc,'stall-legacy-precheck'),'');
|
|
const capture=cleanupFixture(t,files),stop=fixtureGuard(t,files,4500);
|
|
const start=performance.now(),r=legacyCall(h,'precheck',[NODE,'-e',''],{inner:0.5,outer:1});capture();const seconds=(performance.now()-start)/1000;
|
|
assert.equal(r.status,82,r.stderr);assert.equal(r.stderr,'case1-accept: precheck outer-timeout\n');assert.equal(r.stdout,'');
|
|
assert.ok(seconds>=1&&seconds<3,`${mode}:${seconds}s`);assert.equal(read(join(h.dir,'legacy-precheck.rc')),'82\n');
|
|
killRecordedPids(files[0]);assert.deepEqual(await pollFixtureGone(files),[],'explicit same-uid stalled-supervisor residual cleanup');await stop();
|
|
}
|
|
});
|
|
test('row276 case1 capture and legacy tools refuse unsafe system tools',t=>{
|
|
for(const legacy of [false,true]){
|
|
const h=host(t),statFile=join(h.bin,'stat');edit(statFile,s=>s.replace('case "${@: -1}" in',`case "\${@: -1}" in\n /usr/bin/timeout) echo '0 777';exit 0 ;;`));
|
|
const body=legacyBody();
|
|
const r=legacy?legacyCall(h,'node-version',[NODE,'--version'],{body}):spawnSync('/bin/bash',['-c',body+'\ntester_capture 0.5 2 128 mosaic /srv/mosaic /usr/bin /usr/bin/true'],{encoding:'utf8',env:{...process.env,PATH:`${h.bin}:${process.env.PATH}`,HC:h.hc,ROOT_DIR:h.root,REAL_NODE:process.execPath,TMPDIR:h.dir}});
|
|
assert.equal(r.status,84,r.stdout+r.stderr);if(legacy)assert.equal(r.stderr,'case1-accept: node-version supervisor-tools\n');
|
|
}
|
|
});
|
|
|
|
|
|
test('row276 stderr retention is bounded and keeps the final diagnostic',t=>{
|
|
const h=host(t),r=legacyCall(h,'talk',[NODE,'-e',"process.stderr.write('X'.repeat(70000)+'end diagnostic\\n')"],{inner:3,outer:5});
|
|
assert.equal(r.status,0,r.stderr);assert.ok(Buffer.byteLength(r.stderr)<=65536);assert.ok(r.stderr.endsWith('end diagnostic\n'));
|
|
});
|
|
test('row276 version and PM checks refuse successful-looking stdout on supervisor failure',t=>{
|
|
const source=read(SCRIPT),h=host(t);
|
|
for(const [name,next,output] of [['check_f6','# The PM check',`${CLAUDE_PIN} (Claude Code)`],['check_f7','check_f8()', 'ok pm run r-0002'],['check_f10','check_f11()',NODE_PIN]]){
|
|
const at=source.indexOf(name+'()'),end=source.indexOf(next,at+1);assert.ok(at>=0&&end>at);
|
|
const code=source.slice(at,end)+'\nfpass(){ echo PASS; }; ffail(){ echo FAIL; }; tester_capture_reason(){ echo inner-command; }; legacy_run(){ printf "%s\n" "$OUTPUT"; return 87; }; '+name;
|
|
const r=spawnSync('/bin/bash',['-c',code],{encoding:'utf8',env:{...process.env,CLAUDE,CLAUDE_PIN,NODE,NODE_PIN,WORK:h.dir,OUTPUT:output,BUSINESS_FILE:'/fake',COPY:'/fake',BUSINESS:'mosaic-stack'}});
|
|
assert.equal(r.status,0,r.stderr);assert.equal(r.stdout,'FAIL\n',name);
|
|
}
|
|
});
|
|
|
|
|
|
test('row276 full acceptance keeps all five healthy calls in both sudo models',async t=>{
|
|
for(const mode of ['exec','hop']){
|
|
const h=host(t),files=[join(h.hc,'session-pids')];put(join(h.hc,'session-mode'),mode);
|
|
const capture=cleanupFixture(t,files),r=run(h);capture();
|
|
assert.equal(r.status,0,r.out+r.err);assert.deepEqual(failed(r),[],mode);
|
|
assert.deepEqual(await pollFixtureGone(files),[],mode);
|
|
}
|
|
});
|
|
|
|
// mos-1337 / row253 N5: the read must silence failure opening a vanished PID.
|
|
test('row276 vanished pid stat read returns 3 with empty stderr in both scripts',()=>{
|
|
const missingPid='2147483647';
|
|
assert.equal(existsSync(`/proc/${missingPid}`),false);
|
|
for(const script of [SCRIPT,join(REPO,'scripts/case2-unit.sh')]){
|
|
const fields=/^stat_fields\(\) \{[\s\S]*?^\}/m.exec(read(script));
|
|
assert.ok(fields,'stat_fields exists');
|
|
const r=spawnSync('/bin/bash',['-c',fields[0]+'\nstat_fields "$1"','--',missingPid],{encoding:'utf8'});
|
|
assert.equal(r.status,3,script);
|
|
assert.equal(r.stdout,'',script);
|
|
assert.equal(r.stderr,'',script);
|
|
}
|
|
});
|
|
|
|
// mos-1345: stdout bytes belong to the caller only after bounded supervision.
|
|
test('row276 legacy stdout snapshot preserves exact bytes on success and command failure',async t=>{
|
|
for(const mode of ['exec','hop']){
|
|
for(const code of [0,7]){
|
|
const h=host(t);put(join(h.hc,'session-mode'),mode);
|
|
const r=legacyCall(h,'precheck',[NODE,'-e',`process.stdout.write('exact bytes without newline');process.exitCode=${code};`]);
|
|
assert.equal(r.status,code===0?0:87,r.stderr);
|
|
assert.equal(r.stdout,'exact bytes without newline');
|
|
assert.equal(r.stderr,'');
|
|
}
|
|
const h=host(t),files=[join(h.hc,'session-pids')];
|
|
put(join(h.hc,'session-mode'),mode);put(join(h.hc,'stall-legacy-precheck'),'');
|
|
put(join(h.hc,'stall-output'),'must not be published');
|
|
const capture=cleanupFixture(t,files),stop=fixtureGuard(t,files,4500);
|
|
const r=legacyCall(h,'precheck',[NODE,'-e',''],{inner:0.5,outer:1});capture();
|
|
assert.equal(r.status,82,r.stderr);assert.equal(r.stdout,'');
|
|
assert.equal(r.stderr,'case1-accept: precheck outer-timeout\n');
|
|
killRecordedPids(files[0]);assert.deepEqual(await pollFixtureGone(files),[],'explicit R3 residual cleanup');await stop();
|
|
}
|
|
});
|