Files
jason.woltjeandClaude Sonnet 5.5 2209ac4c17 docs: add Researcher row 123 packet (chief of staff ends runs and changes the organization file)
Candidate on 0bdd573ff + row 49 r1 (manifest 87ef1321...), 21 mutants killed,
targeted gate green. Nothing landed or pushed.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
2026-10-15 11:39:36 -05:00

114 lines
7.4 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# Row 123 (#1617): the chief of staff ends runs and changes the organization file.
# Each check must fail on a mutant. For each mutant this runs one named test file against the
# real source (control) and against the source with one exact-once text edit, in the given worktree,
# one test file at a time, and restores every source file on exit.
# Usage: mutants.sh <worktree> <outdir> (MUTANT_DRY=1 only proves every edit matches exactly once)
set -u
WT=$(cd "$1" && pwd); OUT=$(mkdir -p "$2" && cd "$2" && pwd)
cd "$WT" || exit 2
BAK=$(mktemp -d "$OUT/.bak.XXXXXX")
declare -A SAVED
save() { [ -n "${SAVED[$1]:-}" ] || { mkdir -p "$BAK/$(dirname "$1")"; cp "$1" "$BAK/$1"; SAVED[$1]=1; }; }
restore() { for f in "${!SAVED[@]}"; do cp "$BAK/$f" "$f"; done; rm -rf "$BAK"; }
trap restore EXIT
edit() { # file old new: exact-once replace
save "$1"
node -e '
const fs = require("fs"); const [f, a, b] = process.argv.slice(1);
const s = fs.readFileSync(f, "utf8"); const n = s.split(a).length - 1;
if (n !== 1) { console.error("expected one match, found " + n); process.exit(3); }
fs.writeFileSync(f, s.replace(a, () => b));
' "$1" "$2" "$3"
}
run() { # name testfile
if [ -n "${MUTANT_DRY:-}" ]; then echo "$1: edit ok (dry)"; return; fi
setsid -w timeout -k 10 400 node --test "$2" > "$OUT/$1.out" 2>&1; echo "$?" > "$OUT/$1.rc"
echo "$1: exit $(cat "$OUT/$1.rc"); $(grep -E '^ℹ (tests|pass|fail)' "$OUT/$1.out" | tr '\n' ' ')"
grep -E '^✖' "$OUT/$1.out" | sort -u | sed 's/ ([0-9.]*ms)$//' | sed "s/^/ failed: /"
}
mutant() { # name testfile file old new
edit "$3" "$4" "$5" || { echo "$1: edit failed"; return; }
diff -u "$BAK/$3" "$3" > "$OUT/$1.patch"
run "$1" "$2"
cp "$BAK/$3" "$3"
}
CS=packages/bus/tests/chief-of-staff.test.mjs; BCS=packages/business/tests/chief-of-staff.test.mjs
B=packages/bus/src/broker.mjs; V=packages/business/src/vocabulary.mjs; C=roles/chief-of-staff.json
for t in "$CS" "$BCS"; do run "control-$(basename "$(dirname "$(dirname "$t")")")" "$t"; done
# role.end (REQ-ROLE-2, REQ-COS-1)
# m1: the reason form skips the authority check, so any caller may end a run with a reason
mutant m1-role-end-unchecked "$CS" "$B" "this.#consumeAuthority(s, 'role.end', { target: a.role }, { reason: a.reason });" "void 0;"
# m2: a reason and a decision together are accepted
mutant m2-reason-and-decision-accepted "$CS" "$B" "if ('decision' in a) fail('invalid-request');" "void 0;"
# m3: the reason has no length bound
mutant m3-reason-unbounded "$CS" "$B" "string(a.reason, 500);" "string(a.reason, 100000);"
# m4: the chief may end its own run
mutant m4-self-end-allowed "$CS" "$B" "if (a.role === s.role || END_PROTECTED" "if (END_PROTECTED"
# m5: the chief may end the assistant's run
mutant m5-assistant-end-allowed "$CS" "$B" "END_PROTECTED.includes(b.roles[a.role]?.definition)" "false"
# m6: the reason is not written to role_claims.reason
mutant m6-reason-not-stored "$CS" "$B" "this.#claimEnd(s, h, 'revoke', a.decision ?? null, a.reason ?? null);" "this.#claimEnd(s, h, 'revoke', a.decision ?? null, null);"
# m7: the reason is not in the action.allowed body
mutant m7-reason-not-logged "$CS" "$B" "{ target: a.role }, { reason: a.reason });" "{ target: a.role }, {});"
# m8: the decision path checks role.end instead of role.revoke, so another role's decision no longer ends a run
mutant m8-decision-path-wrong-action "$CS" "$B" "this.#consumeAuthority(s, 'role.revoke', { decision: a.decision, target: a.role });" "this.#consumeAuthority(s, 'role.end', { decision: a.decision, target: a.role });"
# organization.set (REQ-LIMIT-1, REQ-ROLE-3, REQ-COS-1)
# m9: organization.set skips the authority check, so any agent may change the file
mutant m9-organization-set-unchecked "$CS" "$B" 'this.#store.transaction(() => this.#consumeAuthority(s, "organization.change"));' "void 0;"
# m10: organization.set accepts a key beyond the write path (launch)
mutant m10-key-list-widened "$CS" "$B" "const ORGANIZATION_KEYS = ['revision', 'limits', 'models', 'instances'];" "const ORGANIZATION_KEYS = ['revision', 'limits', 'models', 'instances', 'launch'];"
# m11: the revision is optional
mutant m11-revision-optional "$CS" "$B" 'else keys(args, ORGANIZATION_KEYS, ["revision"]);' "else keys(args, ORGANIZATION_KEYS, []);"
# m12: the writer is told the wrong actor
mutant m12-wrong-actor "$CS" "$B" 'actor = this.#business(s.business).roles[s.role].definition + "." + s.role;' "actor = s.role;"
# m13: limits and limits.set stop being human-only
mutant m13-limits-open-to-agents "$CS" "$B" 'if (!role && !s.human) fail("human-required");' "void 0;"
# m14: a validation failure from the shared writer no longer maps to invalid-request
mutant m14-invalid-change-not-refused "$CS" "$B" 'if (e.name === "BusinessError" || e.code === "invalid-request"' 'if (e.name === "NoSuchError" || e.code === "invalid-request"'
# vocabulary and the shipped role file (REQ-COS-1: role.revoke stays gated for every role)
# m15: role.end leaves the action vocabulary
mutant m15-vocabulary-drops-role-end "$BCS" "$V" ' "role.end",
' ''
# m16: the shipped chief file drops its role.end grant
mutant m16-chief-file-drops-role-end "$CS" "$C" '"role.launch", "role.end", "organization.change"' '"role.launch", "organization.change"'
# m17: the shipped chief file drops its organization.change grant
mutant m17-chief-file-drops-organization-change "$CS" "$C" '"role.end", "organization.change"' '"role.end"'
# m18: the shipped chief file lists role.revoke as within-role
mutant m18-chief-file-grants-role-revoke "$BCS" "$C" '"role.launch", "role.end"' '"role.launch", "role.revoke", "role.end"'
# schema (REQ-ROLE-2): the 3c→3d trigger. A schema edit must keep the pinned digest honest, so each of these
# edits schema.sql and the 3c-3d step the same way and re-seals SCHEMA_SHA256 from the edited schema.sql.
SQ=packages/bus/schema.sql; ST=packages/bus/src/store.mjs; MG=packages/bus/src/migrations/3c-3d.sql
reseal() {
save "$ST"
local sha; sha=$(sha256sum "$SQ" | cut -d' ' -f1)
node -e '
const fs = require("fs"); const [f, sha] = process.argv.slice(1);
const s = fs.readFileSync(f, "utf8");
fs.writeFileSync(f, s.replace(/export const SCHEMA_SHA256 = \x27[a-f0-9]+\x27;/, "export const SCHEMA_SHA256 = \x27" + sha + "\x27;"));
' "$ST" "$sha"
}
schema_mutant() { # name testfile old new
edit "$SQ" "$3" "$4" && edit "$MG" "$3" "$4" || { echo "$1: edit failed"; return; }
reseal
{ diff -u "$BAK/$SQ" "$SQ"; diff -u "$BAK/$MG" "$MG"; diff -u "$BAK/$ST" "$ST"; } > "$OUT/$1.patch"
run "$1" "$2"
for f in "$SQ" "$MG" "$ST"; do cp "$BAK/$f" "$f"; done
}
# m19: the trigger goes back to the 3c rule (a revoke needs a decision), so a reason alone is refused by storage
schema_mutant m19-trigger-decision-only "$CS" "WHEN NEW.op = 'revoke' AND NEW.decision IS NULL AND NEW.reason IS NULL" "WHEN NEW.op = 'revoke' AND NEW.decision IS NULL"
# m20: the trigger never fires, so a revoke with neither a decision nor a reason is accepted
schema_mutant m20-trigger-never-fires "$CS" "WHEN NEW.op = 'revoke' AND NEW.decision IS NULL AND NEW.reason IS NULL" "WHEN 0"
# m21: the 3c→3d step is a no-op on the trigger: the shipped schema has it, a migrated 3c database keeps the old rule.
# (Run against the migration test only; the step file keeps its text but the DROP/CREATE are removed, so the engine
# refuses at import with migration-target-mismatch. That refusal is the check: a step that does not reach the
# pinned schema cannot ship.)
save "$MG"; : > "$MG"
{ diff -u "$BAK/$MG" "$MG"; } > "$OUT/m21-step-empty.patch"
run m21-step-empty "$CS"
cp "$BAK/$MG" "$MG"