docs(review): row 36 S1 round 2, approve (filbert)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-04 23:10:26 -05:00
co-authored by Claude Opus 5.5
parent 473155d463
commit 17481148e2
8 changed files with 556 additions and 0 deletions
@@ -0,0 +1,48 @@
#!/bin/bash
# Round 2: round 1 mutants M1-M20 plus N1-N6, against the round 2 candidate.
# name|file|perl substitution
cd ~/filbert-scratch/r36b/repo
S=packages/business/src
L=~/filbert-scratch/r36b/logs
: > $L/mut-summary.txt
while IFS='|' read -r name file expr; do
[ -z "$name" ] && continue
rm -rf $S && cp -a ~/filbert-scratch/r36b/src-orig $S
perl -0pi -e "$expr" $S/$file
if cmp -s $S/$file ~/filbert-scratch/r36b/src-orig/$file; then echo "$name NOAPPLY" >> $L/mut-summary.txt; continue; fi
node --test packages/business/tests/ > $L/mut-$name.txt 2>&1
f=$(grep -E '^ℹ fail' $L/mut-$name.txt | awk '{print $3}')
extra=""
if [ "$f" = 0 ]; then bash scripts/test-task.sh > $L/mut-$name-task.txt 2>&1; extra=" task:$(grep -E 'passed' $L/mut-$name-task.txt | tail -1)"; fi
echo "$name business_fail=$f$extra" >> $L/mut-summary.txt
done <<'LIST'
M1-vars-narrower-wider|vars.mjs|s/NETWORKS\.indexOf\(a\) <= NETWORKS\.indexOf\(b\)/NETWORKS.indexOf(a) >= NETWORKS.indexOf(b)/
M2-resolve-network-wider|resolve.mjs|s/NETWORKS\.indexOf\(a\) <= NETWORKS\.indexOf\(b\)/NETWORKS.indexOf(a) >= NETWORKS.indexOf(b)/
M3-cross-not-narrowed|resolve.mjs|s/crossRole = crossRole\.filter\(\(a\) => vars\["limits\.authority"\]\.includes\(a\)\);//
M4-layer-check-off|vars.mjs|s/if \(!entry\.layers\.includes\(layer\)\)/if (false)/
M5-no-realpath-file|credentials.mjs|s/real = realpathSync\(ref\.file\);/real = ref.file;/
M6-gated-only-allowed|role.mjs|s/if \(GATED_ONLY\.includes\(action\)\) refuse/if (false) refuse/
M7-launcher-in-instances|business.mjs|s/if \(name === launch\.by\) refuse/if (false) refuse/
M8-sync-bot-shared|business.mjs|s/role\.tracker && \(role\.tracker\.bot === tracker\.sync\.bot/false && (role.tracker.bot === tracker.sync.bot/
M9-within-cross-overlap|role.mjs|s/if \(both\.length > 0\) refuse/if (false) refuse/
M10-project-id-mismatch|resolve.mjs|s/\|\| declared\[0\] !== project\.id//
M11-token-empty-ok|credentials.mjs|s/if \(stat\.size === 0\) problems/if (false) problems/
M12-token-uid-off|credentials.mjs|s/if \(stat\.uid !== uid\) problems/if (false) problems/
M13-contract-symlink-ok|role.mjs|s/!stat\.isFile\(\) \|\| stat\.isSymbolicLink\(\) \|\| stat\.size === 0/!stat.isFile() || stat.size === 0/
M14-vikunja-expired-ok|credentials.mjs|s/problems\.push\(`\$\{label\}: expired/warnings.push(`\${label}: expired/
M15-credmap-not-exact|business.mjs|s/refuse\(`\$\{where\} must reference exactly/if (false) refuse(`\${where} must reference exactly/
M16-tracker-without-vikunja|business.mjs|s/refuse\(`\$\{at\} has "tracker" but/if (false) refuse(`\${at} has "tracker" but/
M17-max-ceiling-off|business.mjs|s/ \|\| count > ceiling//
M18-project-undeclared-instance|resolve.mjs|s/if \(!Object\.hasOwn\(business\.roles, name\)\) refuse\(`project/if (false) refuse(`project/
M19-unknown-var-ok|vars.mjs|s/if \(!entry\) refuse\(`\$\{where\}: unknown variable/if (!entry) continue; if (false) refuse(`\${where}: unknown variable/
M20-launch-gate-cross-only|resolve.mjs|s/withinRole = withinRole\.filter\(\(a\) => a !== "role\.launch"\);//
N1-launch-by-cross-ok|business.mjs|s/if \(!launcher\.authority\.withinRole\.includes\("role\.launch"\)\)/if (![...launcher.authority.withinRole, ...launcher.authority.crossRole].includes("role.launch"))/
N2-launch-checks-cross|resolve.mjs|s/&& withinRole\.includes\("role\.launch"\) \?/\&\& (withinRole.includes("role.launch") || crossRole.includes("role.launch")) ?/
N3-enoent-not-distinct|util.mjs|s/if \(error\.code === "ENOENT"\) refuse/if (false) refuse/
N4-uid-check-off|business.mjs|s/if \(stat\.uid !== process\.getuid\(\)\) refuse/if (false) refuse/
N5-mode-mask-group-only|business.mjs|s/\(stat\.mode & 0o022\)/(stat.mode \& 0o020)/
N6-launch-any-instance|resolve.mjs|s/business\.launch && business\.launch\.by === instance && withinRole/business.launch \&\& withinRole/
LIST
rm -rf $S && cp -a ~/filbert-scratch/r36b/src-orig $S
diff -r $S ~/filbert-scratch/r36b/src-orig && echo RESTORED
cat $L/mut-summary.txt
@@ -0,0 +1,267 @@
// Row 36 round 2 probes (Filbert). Round 1 probe.mjs plus C10-C16 and E1-E8. PROBE_REPO picks the candidate clone.
import { mkdirSync, mkdtempSync, writeFileSync, readFileSync, chmodSync, symlinkSync, renameSync, rmSync } from "node:fs";
import { join } from "node:path";
const R = process.env.PROBE_REPO ?? `${process.env.HOME}/filbert-scratch/r36b/repo`;
const B = await import(`${R}/packages/business/src/index.mjs`);
const H = await import(`${R}/packages/business/tests/helpers.mjs`);
const base = process.env.PROBE_WORK ?? `${process.env.HOME}/filbert-scratch/r36b/probe/work`;
mkdirSync(base, { recursive: true });
let n = 0;
const out = (name, ok, detail = "") => console.log(`${ok ? "PASS" : "FAIL"} ${name}${detail ? ` :: ${detail}` : ""}`);
const tryRun = (f) => { try { return { ok: true, v: f() }; } catch (e) { return { ok: false, e: e.message, code: e.exitCode }; } };
function setup(mut = (d) => d, project = null, rolesDir = null) {
const root = mkdtempSync(join(base, "b-"));
H.systemConfig(root);
const env = { MOSAIC_CONFIG: join(root, "config", "config.json") };
const doc = mut(H.businessDoc(root));
const dir = join(root, "config");
H.writeJson(join(dir, "businesses", "acme.json"), doc);
if (project) H.writeJson(join(root, "project", ".mosaic", "project.json"), project);
rolesDir ??= join(R, "roles");
return { root, env, dir, rolesDir };
}
function load(s) { return B.loadBusiness("acme", { rolesDir: s.rolesDir, dir: s.dir, env: s.env }); }
// A. example refuses as shipped, and each placeholder refuses alone.
{
const ex = JSON.parse(readFileSync(`${R}/packages/business/examples/mosaic-stack.example.json`, "utf8"));
const r = tryRun(() => B.validateBusinessDocument(ex, "mosaic-stack.json", { rolesDir: `${R}/roles` }));
out("A1 example refuses as shipped", !r.ok, r.e);
const fixBots = structuredClone(ex); let id = 10;
fixBots.tracker.sync.botId = id++; for (const v of Object.values(fixBots.roles)) v.tracker.botId = id++;
const r2 = tryRun(() => B.validateBusinessDocument(fixBots, "mosaic-stack.json", { rolesDir: `${R}/roles` }));
out("A2 bot ids fixed, dates still placeholders: refuses", !r2.ok, r2.e);
const fixDates = JSON.parse(JSON.stringify(ex).replaceAll("YYYY-MM-DD", "2099-01-01"));
const r3 = tryRun(() => B.validateBusinessDocument(fixDates, "mosaic-stack.json", { rolesDir: `${R}/roles` }));
out("A3 dates fixed, bot ids 0: refuses", !r3.ok, r3.e);
const both = JSON.parse(JSON.stringify(fixBots).replaceAll("YYYY-MM-DD", "2099-01-01"));
const r4 = tryRun(() => B.validateBusinessDocument(both, "mosaic-stack.json", { rolesDir: `${R}/roles` }));
out("A4 both fixed: document validates (shape only; token files are a CLI check)", r4.ok, r4.e);
}
// B. resolver precedence, intersect, refusals.
const sys = (root) => H.systemFor(root);
{
const s = setup((d) => { d.vars["limits.tools"] = ["read", "grep", "bash", "write"]; d.vars["limits.network"] = "open";
d.roles.coder.vars = { harness: "pi", "limits.tools": ["read", "bash", "edit", "write"], "limits.network": "open" }; return d; },
{ projectVersion: 1, id: "stack", vars: { "tracker.pollSeconds": 45, "limits.tools": ["read", "bash", "grep", "write"] },
roles: { coder: { vars: { "tracker.pollSeconds": 20 } } } });
const b = load(s);
const p = B.loadProject(join(s.root, "project"));
const r = B.resolveInstance({ system: sys(s.root), business: b, project: p, instance: "coder" });
out("B1 replace: project roles.<i> beats project beats business", r.vars["tracker.pollSeconds"] === 20, `${r.vars["tracker.pollSeconds"]} from ${r.provenance["tracker.pollSeconds"]}`);
out("B2 limits.tools intersects business ∩ project ∩ agent ∩ ceiling", JSON.stringify(r.limits.tools) === JSON.stringify(["read", "write", "bash"]), JSON.stringify(r.limits.tools));
out("B3 limits.network open can't widen role api-only", r.limits.network === "api-only", r.limits.network);
const r0 = B.resolveInstance({ system: sys(s.root), business: b, instance: "coder" });
out("B4 no project: business value", r0.vars["tracker.pollSeconds"] === 60, String(r0.vars["tracker.pollSeconds"]));
out("B5 default kept when unset", r0.vars["tracker.reconcileMinutes"] === 60 && r0.provenance["tracker.reconcileMinutes"] === "default");
}
for (const [name, mut] of [
["B6 unknown key at business refuses", (d) => { d.vars["tracker.pollseconds"] = 30; return d; }],
["B7 agent-only key (harness) at business refuses", (d) => { d.vars.harness = "pi"; return d; }],
["B8 business-only key at agent refuses", (d) => { d.roles.coder.vars = { "tracker.baseUrl": "http://x" }; return d; }],
["B9 system key at business refuses", (d) => { d.vars.dataRoot = "/x"; return d; }],
["B10 __proto__ key refuses", (d) => JSON.parse(JSON.stringify(d).replace('"vars":{', '"vars":{"__proto__":{"a":1},')) ],
["B11 limits.network outside vocabulary refuses", (d) => { d.vars["limits.network"] = "lan"; return d; }],
["B12 limits.authority unknown action refuses", (d) => { d.vars["limits.authority"] = ["task.delete"]; return d; }],
]) {
const s = setup(mut);
const r = tryRun(() => load(s));
out(name, !r.ok && r.code === 2, r.e);
}
for (const [name, project] of [
["B13 project file: agent key (model) at project vars refuses", { projectVersion: 1, id: "stack", vars: { model: "x" }, roles: {} }],
["B14 project roles.<i>: agent key refuses (project layer)", { projectVersion: 1, id: "stack", vars: {}, roles: { coder: { vars: { harness: "pi" } } } }],
["B15 project roles names undeclared instance refuses", { projectVersion: 1, id: "stack", vars: {}, roles: { ghost: { vars: {} } } }],
["B16 project roles.constructor refuses", { projectVersion: 1, id: "stack", vars: {}, roles: { constructor: { vars: {} } } }],
]) {
const s = setup((d) => d, project);
const b = load(s);
const r = tryRun(() => B.resolveInstance({ system: sys(s.root), business: b, project: B.loadProject(join(s.root, "project")), instance: "coder" }));
out(name, !r.ok, r.e);
}
{
const s = setup();
const b = load(s);
for (const inst of ["constructor", "toString", "__proto__"]) {
const r = tryRun(() => B.resolveInstance({ system: sys(s.root), business: b, instance: inst }));
out(`B17 instance ${inst} refuses`, !r.ok, r.e);
}
}
// C. authority: limits.authority allowlist, role.launch.
{
const s = setup((d) => { d.vars["limits.authority"] = ["task.create", "task.assign", "role.launch", "message.send", "task.scope.change", "review.verdict"];
d.roles.pm.vars = { ...d.roles.pm.vars, "limits.authority": ["task.create", "role.launch", "task.scope.change", "task.close"] }; return d; });
const b = load(s);
const pm = B.resolveInstance({ system: sys(s.root), business: b, instance: "pm" });
out("C1 pm within = definition ∩ business ∩ agent", JSON.stringify(pm.limits.authority.withinRole) === JSON.stringify(["task.create", "role.launch"]), JSON.stringify(pm.limits.authority));
out("C2 pm cross keeps scope.change, drops priority.change", JSON.stringify(pm.limits.authority.crossRole) === JSON.stringify(["task.scope.change"]));
out("C3 classify: dropped action is gated", B.classify(pm, "task.assign") === "gated" && B.classify(pm, "task.priority.change") === "gated");
out("C4 classify: gated-only action is gated", B.classify(pm, "deploy") === "gated");
out("C5 limits can't grant an action outside the definition (review.verdict for pm)", B.classify(pm, "review.verdict") === "gated");
const rv = B.resolveInstance({ system: sys(s.root), business: b, instance: "reviewer" });
out("C6 reviewer keeps only listed actions", JSON.stringify(rv.limits.authority.withinRole) === JSON.stringify(["review.verdict", "message.send"]), JSON.stringify(rv.limits.authority.withinRole));
const e = tryRun(() => B.classify(pm, "task.delete"));
out("C7 classify unknown action refuses", !e.ok, e.e);
}
{
const s = setup();
const b = load(s);
const pm = B.resolveInstance({ system: sys(s.root), business: b, instance: "pm" });
out("C8 pm with launch.by=pm: role.launch within", B.classify(pm, "role.launch") === "within" && pm.launch !== null);
const s2 = setup((d) => { delete d.launch; return d; });
const r2 = tryRun(() => load(s2));
if (r2.ok) { const pm2 = B.resolveInstance({ system: sys(s2.root), business: r2.v, instance: "pm" });
out("C9 no launch block: pm role.launch gated", B.classify(pm2, "role.launch") === "gated" && pm2.launch === null); }
else out("C9 no launch block: business refuses (launch required)", true, r2.e);
const s3 = setup((d) => { d.launch.by = "cto"; d.launch.instances = ["coder", "reviewer"]; return d; });
const r3 = tryRun(() => load(s3));
out("C10 launch.by=cto (no role.launch in cto definition) refuses, exit 2", !r3.ok && r3.code === 2, r3.e);
const s4 = setup((d) => { d.roles.pm.vars = { ...d.roles.pm.vars, "limits.authority": ["task.create"] }; return d; });
const b4 = load(s4);
const pm4 = B.resolveInstance({ system: sys(s4.root), business: b4, instance: "pm" });
out("C11 agent limits.authority drops role.launch for launch.by: launch null, gated", pm4.launch === null && B.classify(pm4, "role.launch") === "gated", `launch=${pm4.launch ? "set" : "null"} classify=${B.classify(pm4, "role.launch")}`);
}
const sweep = [];
{
const s = setup((d) => { d.vars["limits.authority"] = ["task.create", "task.assign", "message.send", "review.verdict", "task.update.assigned"]; return d; });
const b = load(s);
const pm = B.resolveInstance({ system: sys(s.root), business: b, instance: "pm" });
out("C12 business limits.authority drops role.launch: pm launch null, gated", pm.launch === null && B.classify(pm, "role.launch") === "gated");
sweep.push(["business-layer drop", s, b]);
}
for (const [name, where, expectLoad] of [
["C13 a definition with role.launch only cross-role can't be launch.by", "crossRole", false],
["C14 a definition with role.launch within-role can be launch.by", "withinRole", true],
]) {
const s = setup((d) => { d.launch.by = "cto"; d.launch.instances = ["coder", "reviewer"]; return d; });
const roles = H.rolesCopy(s.root);
const ctoFile = join(roles, "cto.json");
const cto = JSON.parse(readFileSync(ctoFile, "utf8"));
cto.authority[where].push("role.launch");
writeFileSync(ctoFile, JSON.stringify(cto, null, 2));
s.rolesDir = roles;
const r = tryRun(() => load(s));
if (!expectLoad) { out(name, !r.ok, r.e); continue; }
if (!r.ok) { out(name, false, r.e); continue; }
const c = B.resolveInstance({ system: sys(s.root), business: r.v, instance: "cto" });
const p = B.resolveInstance({ system: sys(s.root), business: r.v, instance: "pm" });
out(name, c.launch?.by === "cto" && B.classify(c, "role.launch") === "within" && p.launch === null && B.classify(p, "role.launch") === "gated",
`cto=${B.classify(c, "role.launch")} pm=${B.classify(p, "role.launch")}`);
sweep.push(["cto launcher", s, r.v]);
}
{
const two = setup((d) => { d.roles.pm2 = { ...d.roles.pm, tracker: { bot: "bot-acme-pm2", botId: 77 } }; delete d.roles.pm2.holder; d.launch.by = "pm2"; return d; });
const r = tryRun(() => load(two));
if (r.ok) {
const pm = B.resolveInstance({ system: sys(two.root), business: r.v, instance: "pm" });
const pm2 = B.resolveInstance({ system: sys(two.root), business: r.v, instance: "pm2" });
out("C15 two pm instances: only launch.by keeps role.launch", pm.launch === null && B.classify(pm, "role.launch") === "gated" && pm2.launch?.by === "pm2" && B.classify(pm2, "role.launch") === "within");
sweep.push(["pm2 launcher", two, r.v]);
} else out("C15 two pm instances", false, r.e);
const def = setup(); sweep.push(["default", def, load(def)]);
const nol = setup((d) => { delete d.launch; return d; }); sweep.push(["no launch", nol, load(nol)]);
const agentDrop = setup((d) => { d.roles.pm.vars = { ...d.roles.pm.vars, "limits.authority": ["task.create"] }; return d; }); sweep.push(["agent-layer drop", agentDrop, load(agentDrop)]);
const projDrop = setup((d) => d, { projectVersion: 1, id: "stack", vars: {}, roles: { pm: { vars: { "limits.authority": ["task.create", "message.send"] } } } });
sweep.push(["project-layer drop", projDrop, load(projDrop), B.loadProject(join(projDrop.root, "project"))]);
let checked = 0; const bad = [];
for (const [label, s, b, p] of sweep) {
for (const instance of Object.keys(b.roles)) {
const rec = B.resolveInstance({ system: sys(s.root), business: b, project: p, instance });
checked++;
if ((rec.launch !== null) !== (B.classify(rec, "role.launch") === "within")) bad.push(`${label}/${instance}`);
}
}
out("C16 sweep: launch !== null exactly when classify(role.launch) is within", bad.length === 0 && checked > 0, `checked=${checked} configs=${sweep.length} bad=${JSON.stringify(bad)}`);
}
// D. credentials: never opened (fs hook counts opens of token paths).
{
const s = setup();
const b = load(s);
const refs = [...Object.values(b.tracker.sync.credentials), ...Object.values(b.roles).flatMap((r) => Object.values(r.credentials))];
const opened = globalThis.__opened;
opened.length = 0;
const problems = refs.flatMap((ref) => B.checkCredentialRef(ref, { forbiddenRoots: [R] }).problems);
const tokenOpens = opened.filter((p) => refs.some((ref) => ref.file && String(p).startsWith(ref.file)));
out("D1 checkCredentialRef on 9 token files: 0 opens", tokenOpens.length === 0 && problems.length === 0, `refs=${refs.length} opens=${JSON.stringify(tokenOpens)} problems=${problems.length}`);
// symlinked parent pointing into a forbidden root
const inRepo = join(s.root, "fakerepo"); mkdirSync(join(inRepo, "sec"), { recursive: true });
const tok = join(inRepo, "sec", "t.token"); writeFileSync(tok, "x"); chmodSync(tok, 0o600);
const link = join(s.root, "outside"); symlinkSync(join(inRepo, "sec"), link);
const ref = B.parseCredentialRef({ file: join(link, "t.token"), rotateBy: "2099-01-01" }, "gitea", "p");
const r = B.checkCredentialRef(ref, { forbiddenRoots: [inRepo] });
out("D2 token reached through a symlinked parent inside a forbidden root refuses", r.problems.some((p) => p.includes("inside")), JSON.stringify(r.problems));
chmodSync(tok, 0o640);
const r2 = B.checkCredentialRef(B.parseCredentialRef({ file: tok, expires: "2099-01-01" }, "vikunja", "p"), {});
out("D3 mode 640 refuses", r2.problems.some((p) => p.includes("mode")), JSON.stringify(r2.problems));
const r3 = B.checkCredentialRef(B.parseCredentialRef({ file: tok, expires: "2000-01-01" }, "vikunja", "p"), {});
out("D4 expired vikunja refuses", r3.problems.some((p) => p.includes("expired")));
const r4 = B.checkCredentialRef(B.parseCredentialRef({ env: "GITEA_TOKEN", rotateBy: "2000-01-01" }, "gitea", "p"), { env: {} });
out("D5 gitea rotateBy past only warns; env unset only warns", r4.problems.length === 0 && r4.warnings.length === 2, JSON.stringify(r4.warnings));
}
{ // hook sanity: the business file read is seen
const s = setup(); globalThis.__opened.length = 0; load(s);
out("D0 hook sees the business file read", globalThis.__opened.some((p) => p.endsWith("acme.json")), String(globalThis.__opened.length));
}
// E. the one-descriptor reader (n1).
{
const s = setup();
const f = join(s.dir, "businesses", "acme.json");
globalThis.__opened.length = 0; load(s);
const paths = globalThis.__opened.filter((p) => p === f);
const fdReads = globalThis.__opened.filter((p) => p.startsWith("fd:"));
out("E1 business load: one open of the path, reads by descriptor", paths.length === 1 && fdReads.length >= 1, JSON.stringify(globalThis.__opened.map((p) => p.replace(s.root, "<root>").replace(R, "<repo>"))));
}
{
const s = setup();
const f = join(s.dir, "businesses", "acme.json");
const real = join(s.root, "real.json"); writeFileSync(real, readFileSync(f)); chmodSync(real, 0o600);
rmSync(f); symlinkSync(real, f);
const r = tryRun(() => load(s));
out("E2 business file is a symlink: exit 4", !r.ok && r.code === 4, r.e);
}
{
const s = setup();
const f = join(s.dir, "businesses", "acme.json");
chmodSync(f, 0o000);
const r = tryRun(() => load(s));
out("E3 business file mode 000 (EACCES at open): refuses", !r.ok, `exit=${r.code} ${r.e}`);
chmodSync(f, 0o600);
}
{
const s = setup();
const d = join(s.dir, "businesses");
const moved = join(s.root, "elsewhere"); renameSync(d, moved); symlinkSync(moved, d);
const r = tryRun(() => load(s));
out("E4 businesses/ itself a symlink (parent, not the file): loads, as in round 1", r.ok, r.ok ? "" : r.e);
}
{
const s = setup();
const roles = H.rolesCopy(s.root);
const pm = join(roles, "pm.json"); const real = join(s.root, "pm-real.json");
renameSync(pm, real); symlinkSync(real, pm);
s.rolesDir = roles;
const r = tryRun(() => load(s));
out("E5 role definition is a symlink: exit 4", !r.ok && r.code === 4, r.e);
}
{
const s = setup((d) => d, { projectVersion: 1, id: "stack", vars: {}, roles: {} });
const pf = join(s.root, "project", ".mosaic", "project.json"); const real = join(s.root, "project-real.json");
renameSync(pf, real); symlinkSync(real, pf);
const r = tryRun(() => B.loadProject(join(s.root, "project")));
out("E6 project file is a symlink: exit 4", !r.ok && r.code === 4, r.e);
}
{
const s = setup();
const f = join(s.dir, "businesses", "acme.json");
chmodSync(f, 0o604);
const r = tryRun(() => load(s));
out("E7 other-readable but not writable (604) loads", r.ok, r.ok ? "" : r.e);
chmodSync(f, 0o602);
const r2 = tryRun(() => load(s));
out("E8 other-writable (602) refuses, exit 2", !r2.ok && r2.code === 2, r2.e);
}
@@ -0,0 +1,26 @@
M1-vars-narrower-wider business_fail=2
M2-resolve-network-wider business_fail=2
M3-cross-not-narrowed business_fail=1
M4-layer-check-off business_fail=5
M5-no-realpath-file business_fail=1
M6-gated-only-allowed business_fail=2
M7-launcher-in-instances business_fail=1
M8-sync-bot-shared business_fail=1
M9-within-cross-overlap business_fail=1
M10-project-id-mismatch business_fail=1
M11-token-empty-ok business_fail=1
M12-token-uid-off business_fail=1
M13-contract-symlink-ok business_fail=0 task:selftest: 98 passed, 0 failed
M14-vikunja-expired-ok business_fail=2
M15-credmap-not-exact business_fail=2
M16-tracker-without-vikunja business_fail=1
M17-max-ceiling-off business_fail=1
M18-project-undeclared-instance business_fail=2
M19-unknown-var-ok business_fail=2
M20-launch-gate-cross-only business_fail=1
N1-launch-by-cross-ok business_fail=0 task:selftest: 98 passed, 0 failed
N2-launch-checks-cross business_fail=0 task:selftest: 98 passed, 0 failed
N3-enoent-not-distinct business_fail=6
N4-uid-check-off business_fail=0 task:selftest: 98 passed, 0 failed
N5-mode-mask-group-only business_fail=1
N6-launch-any-instance business_fail=1
@@ -0,0 +1,10 @@
v24.21.0
✔ merge doesn't change its inputs (0.251118ms)
ℹ tests 60
ℹ suites 0
ℹ pass 60
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 2734.347022
@@ -0,0 +1,53 @@
PASS A1 example refuses as shipped :: mosaic-stack.json roles.pm.tracker.botId must be a positive integer (got 0)
PASS A2 bot ids fixed, dates still placeholders: refuses :: mosaic-stack.json roles.pm.credentials.gitea.rotateBy must be a date YYYY-MM-DD (got "YYYY-MM-DD")
PASS A3 dates fixed, bot ids 0: refuses :: mosaic-stack.json roles.pm.tracker.botId must be a positive integer (got 0)
PASS A4 both fixed: document validates (shape only; token files are a CLI check)
PASS B1 replace: project roles.<i> beats project beats business :: 20 from project:stack:roles.coder
PASS B2 limits.tools intersects business ∩ project ∩ agent ∩ ceiling :: ["read","write","bash"]
PASS B3 limits.network open can't widen role api-only :: api-only
PASS B4 no project: business value :: 60
PASS B5 default kept when unset
PASS B6 unknown key at business refuses :: ~/filbert-scratch/r36b/probe/work/b-r1Zh8W/config/businesses/acme.json vars: unknown variable "tracker.pollseconds"
PASS B7 agent-only key (harness) at business refuses :: ~/filbert-scratch/r36b/probe/work/b-iKw1uQ/config/businesses/acme.json vars: variable harness can't be set at the business layer (allowed: agent)
PASS B8 business-only key at agent refuses :: ~/filbert-scratch/r36b/probe/work/b-HcYwsQ/config/businesses/acme.json roles.coder.vars: variable tracker.baseUrl can't be set at the agent layer (allowed: business)
PASS B9 system key at business refuses :: ~/filbert-scratch/r36b/probe/work/b-juFiiI/config/businesses/acme.json vars: variable dataRoot can't be set at the business layer (allowed: system)
PASS B10 __proto__ key refuses :: ~/filbert-scratch/r36b/probe/work/b-DKVQBq/config/businesses/acme.json vars: unknown variable "__proto__"
PASS B11 limits.network outside vocabulary refuses :: variable limits.network must be one of: none, api-only, open (got "lan")
PASS B12 limits.authority unknown action refuses :: variable limits.authority entry names an unknown action: "task.delete"
PASS B13 project file: agent key (model) at project vars refuses :: ~/filbert-scratch/r36b/probe/work/b-eL0D1t/project/.mosaic/project.json vars: variable model can't be set at the project layer (allowed: agent)
PASS B14 project roles.<i>: agent key refuses (project layer) :: ~/filbert-scratch/r36b/probe/work/b-Fn7lVd/project/.mosaic/project.json roles.coder.vars: variable harness can't be set at the project layer (allowed: agent)
PASS B15 project roles names undeclared instance refuses :: project stack sets vars for role instance ghost, which business acme doesn't declare
PASS B16 project roles.constructor refuses :: project stack sets vars for role instance constructor, which business acme doesn't declare
PASS B17 instance constructor refuses :: business acme declares no role instance "constructor"
PASS B17 instance toString refuses :: business acme declares no role instance "toString"
PASS B17 instance __proto__ refuses :: business acme declares no role instance "__proto__"
PASS C1 pm within = definition ∩ business ∩ agent :: {"withinRole":["task.create","role.launch"],"crossRole":["task.scope.change"]}
PASS C2 pm cross keeps scope.change, drops priority.change
PASS C3 classify: dropped action is gated
PASS C4 classify: gated-only action is gated
PASS C5 limits can't grant an action outside the definition (review.verdict for pm)
PASS C6 reviewer keeps only listed actions :: ["review.verdict","message.send"]
PASS C7 classify unknown action refuses :: unknown action: "task.delete"
PASS C8 pm with launch.by=pm: role.launch within
PASS C9 no launch block: pm role.launch gated
PASS C10 launch.by=cto (no role.launch in cto definition) refuses, exit 2 :: ~/filbert-scratch/r36b/probe/work/b-I8HG87/config/businesses/acme.json launch.by names cto, whose role cto doesn't hold role.launch within-role
PASS C11 agent limits.authority drops role.launch for launch.by: launch null, gated :: launch=null classify=gated
PASS C12 business limits.authority drops role.launch: pm launch null, gated
PASS C13 a definition with role.launch only cross-role can't be launch.by :: ~/filbert-scratch/r36b/probe/work/b-dit59X/config/businesses/acme.json launch.by names cto, whose role cto doesn't hold role.launch within-role
PASS C14 a definition with role.launch within-role can be launch.by :: cto=within pm=gated
PASS C15 two pm instances: only launch.by keeps role.launch
PASS C16 sweep: launch !== null exactly when classify(role.launch) is within :: checked=29 configs=7 bad=[]
PASS D1 checkCredentialRef on 9 token files: 0 opens :: refs=9 opens=[] problems=0
PASS D2 token reached through a symlinked parent inside a forbidden root refuses :: ["gitea file ~/filbert-scratch/r36b/probe/work/b-WKi0wx/outside/t.token: inside ~/filbert-scratch/r36b/probe/work/b-WKi0wx/fakerepo; token files live outside the repository and dataRoot"]
PASS D3 mode 640 refuses :: ["vikunja file ~/filbert-scratch/r36b/probe/work/b-WKi0wx/fakerepo/sec/t.token: mode 640 gives group or other access; use 600"]
PASS D4 expired vikunja refuses
PASS D5 gitea rotateBy past only warns; env unset only warns :: ["gitea env GITEA_TOKEN: not set in this environment; the launcher must provide it","gitea env GITEA_TOKEN: rotation was due on 2000-01-01"]
PASS D0 hook sees the business file read :: 10
PASS E1 business load: one open of the path, reads by descriptor :: ["<root>/config/businesses/acme.json","fd:21","<repo>/roles/pm.json","fd:21","<repo>/roles/cto.json","fd:21","<repo>/roles/coder.json","fd:21","<repo>/roles/reviewer.json","fd:21"]
PASS E2 business file is a symlink: exit 4 :: business file must be a regular, non-symbolic-link file: ~/filbert-scratch/r36b/probe/work/b-l791eV/config/businesses/acme.json
PASS E3 business file mode 000 (EACCES at open): refuses :: exit=4 business file must be a regular, non-symbolic-link file: ~/filbert-scratch/r36b/probe/work/b-NERwev/config/businesses/acme.json
PASS E4 businesses/ itself a symlink (parent, not the file): loads, as in round 1
PASS E5 role definition is a symlink: exit 4 :: role file must be a regular, non-symbolic-link file: ~/filbert-scratch/r36b/probe/work/b-Yw9PhA/roles/pm.json
PASS E6 project file is a symlink: exit 4 :: project file must be a regular, non-symbolic-link file: ~/filbert-scratch/r36b/probe/work/b-oDByB5/project/.mosaic/project.json
PASS E7 other-readable but not writable (604) loads
PASS E8 other-writable (602) refuses, exit 2 :: business file must not be writable by group or other (mode 602): ~/filbert-scratch/r36b/probe/work/b-cZe15L/config/businesses/acme.json
@@ -0,0 +1,6 @@
v26.8.1
business exit 0 ℹ pass 60 ℹ fail 0
test-config exit 0 selftest: 24 passed, 0 failed
test-task exit 0 selftest: 98 passed, 0 failed
test-conductor exit 0 selftest: 17 passed, 0 failed
test-queue exit 0 queue suite: 27 passed, 0 failed
@@ -0,0 +1,17 @@
SAME a.json r1=0 r2=0 MOSAIC_ROLE_TOOLS=read,bash MOSAIC_ROLE_NETWORK=none
SAME b.json r1=0 r2=0 MOSAIC_ROLE_TOOLS=read MOSAIC_ROLE_NETWORK=open
SAME c.json r1=2 r2=2
SAME d.json r1=2 r2=2
SAME e.json r1=2 r2=2
SAME f.json r1=2 r2=2
SAME g.json r1=2 r2=2
SAME h.json r1=2 r2=2
SAME i.json r1=2 r2=2
SAME j.json r1=2 r2=2
SAME k.json r1=2 r2=2
SAME l.json r1=2 r2=2
SAME M.json r1=2 r2=2
SAME n.json r1=2 r2=2
SAME o.json r1=2 r2=2
SAME researcher.json r1=0 r2=0 MOSAIC_ROLE_TOOLS=read,grep,find,ls,bash MOSAIC_ROLE_NETWORK=none
SAME missing.json r1=4 r2=4
@@ -0,0 +1,129 @@
# Slice 1 S1, row 36, round 2 review (Filbert)
Issue #1518, request comment 26728, queue revs 108 to 110 (`768243cc`).
Packet: `agents/darkwing/work/slice1-s1/build-r2.md` (`d9568cec`).
Candidate: manifest `build-r2-manifest.sha256`, sha256
`869168c702c272b051bc957a4a4314df962553ac432a5c2c5210f5ba1291afc7`, 34
files, over `fef4b362` with `build-r2.patch` (sha256
`a27890d5c70c5dd38d6b98ce4badfff6334c1cc0c485dcee7c90b1e6438e8a15`).
Round 1 record: `review-r1.md`.
Verdict: **approve.** B1 and B2 are fixed. The n1 fix holds, and three
notes below don't block.
## Method
- New scratch clone `~/filbert-scratch/r36b/repo` at `fef4b362`, push URL
`DISABLED`. `git apply build-r2.patch`, then `sha256sum -c`: 34 OK. The
patch touches the same 34 paths as round 1. Comparing the two applied
trees file by file, exactly the six files in `r1-to-r2.diff` differ.
- `probe-r2.mjs` is round 1's `probe.mjs` with C10 and C11 turned into
assertions, plus C12 to C16 and E1 to E8. It runs under `hook.mjs` as
before.
- `mutants-r2.sh` runs round 1's 20 mutants on the round 2 source, plus
six new ones (N1 to N6).
- I reran v1 `resolve-role` on the 17 role files, round 1 candidate
against round 2 candidate.
- Teed output is in `r2-*.txt` in this directory.
## Suites
| Suite | Result |
|---|---|
| `node --test packages/business/tests/` (Node 26.8.1) | 60 pass, 0 fail |
| `node --test "packages/business/tests/*.test.mjs"` (Node 24.21.0, `node:24`, no network, clone read-only, host uid) | 60 pass, 0 fail |
| test-config, test-task | 24 and 98 passed, 0 failed |
| test-conductor, test-queue | 17 and 27 passed, 0 failed |
## B1, `launch` and `role.launch`: fixed
- C10: `launch.by: "cto"` refuses with exit 2, "launch.by names cto, whose
role cto doesn't hold role.launch within-role".
- `launch` is null and `role.launch` is gated when `limits.authority`
drops `role.launch` for pm:
- C11, at the agent layer;
- C12, at the business layer.
Darkwing's tests cover the project layer.
- C13 and C14 use a copied roles directory with `role.launch` added to
cto:
- Cross-role only: `launch.by: "cto"` still refuses.
- Within-role: cto may launch (`launch` set, `within`), and pm is gated
with `launch` null.
- C15: with two pm-definition instances, only the one named in
`launch.by` keeps `role.launch`.
- C16 sweeps 7 configurations, which resolve to 29 instance records. In
every one, `launch !== null` exactly when `classify(record,
"role.launch")` is `within`. That is the README's new sentence, checked.
## B2, cross-role narrowing: fixed
The new test leaves `task.reassign` out of coder's allowlist and checks
that it classifies `gated`. My round 1 mutant M3 now fails 1 business
test.
## n1, the one-descriptor reader: fixed
- E1: with the fs hook on, a business load opens the business file path
once, then reads by descriptor. Each role file is opened the same way.
No second path lookup follows the open, so a swap between the check
and the read has nothing to hit.
- A symlink refuses with exit 4 in each of these places: the business file
(E2), a role definition (E5) and the project file (E6).
- Mode 604 loads (E7). Mode 602 refuses with exit 2 (E8), as does 660 in
Darkwing's test.
- Darkwing's tests cover a directory and a FIFO. Both pass on Node 24
too, and the FIFO case returns rather than hanging.
## Round 1 probes and v1
- A1 to A4, B1 to B17, C1 to C9 and D0 to D5 give the same results as
round 1. D1 still records no open of any of the nine token paths.
- v1 `resolve-role`: all 17 files give the same stdout and exit code as
round 1's candidate, which matched the base.
## Mutants
22 of 26 killed.
| Mutant | Result |
|---|---|
| M1 to M12, M14 to M20 (round 1 set) | killed |
| M3 crossRole not narrowed by `limits.authority` | killed (1), was B2 |
| M13 contract symlink check off | survived, equivalent (round 1 n2) |
| N1 `launch.by` check accepts `role.launch` cross-role | **survived** (note 2) |
| N2 `resolveInstance` keeps `launch` if `role.launch` is cross-role | survived, equivalent while `checkLaunch` holds |
| N3 open errors no longer tell ENOENT apart | killed (6) |
| N4 business file uid check off | survived, untestable without a second uid |
| N5 mode mask checks group only | killed (1) |
| N6 `launch` given to any instance holding the verb | killed (1) |
## Notes (not blocking)
1. **Open errors other than ENOENT all say "must be a regular,
non-symbolic-link file".** The intended case is ELOOP. EACCES gets the
same text and exit 4: a business file at mode 000 refuses as "must be
a regular, non-symbolic-link file" (E3). It still fails closed. Round 1
reported the same file as "not valid JSON", which was no better, so
this isn't a regression. Adding `error.code` to the message for
anything other than ELOOP would make it accurate.
2. **No test for a launcher whose definition holds `role.launch` only
cross-role.** N1 survives because the shipped roles never put the verb
in `crossRole`. My C13 probe shows the code refuses that case. A test
with a copied roles directory, like C13, would pin it.
3. **N4: the uid check.** A test would need a file owned by a second user,
which node:test can't create without root. Leave it untested.
4. **Parent directory links.** `O_NOFOLLOW` covers only the last path
component, so a symlinked `businesses/` directory still loads (E4).
Round 1 behaved the same way, and the directory is the user's own.
This is not a finding, only a record of the boundary.
## Files
- `probe-r2.mjs`, `mutants-r2.sh`
- Output:
- `r2-probe.txt`
- `r2-mut-summary.txt`
- `r2-suites.txt`
- `r2-node24.txt`
- `r2-v1-compat.txt`