diff --git a/docs/scratchpads/829-mutator-gate.md b/docs/scratchpads/829-mutator-gate.md index 98eedbd..b5af096 100644 --- a/docs/scratchpads/829-mutator-gate.md +++ b/docs/scratchpads/829-mutator-gate.md @@ -108,3 +108,5 @@ Carried authority chain also verified/read for the locked T-B gate contract: SPE - Reproduced RIDER E exactly at head `1792b7934dda7eff64a207b8b0edb9c460d4164b`: a temporary production file containing `exec claude --dangerously-skip-permissions "terra-r3" # launch-runtime.py` made the guard exit 0 and report `1 gated/1 total`. - Root cause: classification searched the unparsed physical line, and the broad gated regex treated any `launch-runtime.py` substring—including comments and inert arguments—as an invocation before the direct-launch finding was evaluated. - Round-4 plan: add permanent RED cases for the exact comment evasion plus string-argument, echo, and unrelated-variable marker evasions; tokenize/strip comments by launcher syntax; recognize only command-position wrapper invocations with `--runtime` and the gated command separator; retain 14/14 real inventory; rerun guard coverage and all gates fresh. +- Mos Rider A/B decision: adopt **both** defenses. Command-position parsing remains necessary because a normal `claude -p` launch is consequential even without the dangerous flag. The primitive-location invariant is more mechanically robust for dangerous mode because it does not need to recognize a wrapper marker at all. Move the sole raw `--dangerously-skip-permissions` literal into `launch-runtime.py`; any occurrence in another production file is independently RED. +- Rider-A RED matrix adds heredoc body, backslash continuation, non-first `;`/`&&`/pipe commands, command substitution, `eval`, and variable-execution indirection in addition to the six marker/comment evasions. Before the augmented implementation, primitive ownership, command substitution, `eval`, variable execution, and the preserved 14-site inventory all fail. diff --git a/packages/mosaic/src/mutator-gate/runtime_launch_guard_unittest.py b/packages/mosaic/src/mutator-gate/runtime_launch_guard_unittest.py index 880134d..6c63d03 100644 --- a/packages/mosaic/src/mutator-gate/runtime_launch_guard_unittest.py +++ b/packages/mosaic/src/mutator-gate/runtime_launch_guard_unittest.py @@ -48,6 +48,14 @@ class RuntimeLaunchGuardTest(unittest.TestCase): "marker-argument.sh": 'exec claude --dangerously-skip-permissions "launch-runtime.py"\n', "marker-echo.sh": 'exec claude --dangerously-skip-permissions "prompt"; echo launch-runtime.py\n', "marker-variable.sh": 'marker=launch-runtime.py; exec claude --dangerously-skip-permissions "prompt"\n', + "heredoc.sh": "cat <<'EOF'\nexec claude --dangerously-skip-permissions prompt # launch-runtime.py\nEOF\n", + "continued.sh": "exec \\\n claude --dangerously-skip-permissions prompt # launch-runtime.py\n", + "chain-semicolon.sh": "true; claude -p prompt\n", + "chain-and.sh": "true && claude -p prompt\n", + "chain-pipe.sh": "printf input | claude -p prompt\n", + "command-substitution.sh": "output=$(claude -p prompt)\n", + "eval.sh": "launcher='claude -p prompt'\neval \"$launcher\"\n", + "variable-exec.sh": "launcher=claude\n\"$launcher\" -p prompt\n", } for filename, source in cases.items(): with self.subTest(filename=filename): @@ -65,6 +73,14 @@ class RuntimeLaunchGuardTest(unittest.TestCase): with self.subTest(filename=filename): self.assertEqual(GUARD.scan_text(Path(filename), source), []) + def test_dangerous_primitive_is_owned_only_by_the_choke_point(self) -> None: + primitive = "--dangerously-skip-permissions" + self.assertNotEqual(GUARD.scan_text(Path("caller.ts"), f"args = ['{primitive}'];\n"), []) + self.assertEqual( + GUARD.scan_text(Path("framework/tools/lease-broker/launch-runtime.py"), f'FLAG = "{primitive}"\n'), + [], + ) + def test_repository_has_no_ungated_consequential_runtime_launch(self) -> None: violations = GUARD.scan_repository(REPO_ROOT) self.assertEqual( @@ -72,6 +88,9 @@ class RuntimeLaunchGuardTest(unittest.TestCase): [], "\n".join(GUARD.format_violation(violation) for violation in violations), ) + inventory = GUARD.inventory_repository(REPO_ROOT) + self.assertEqual(len(inventory), 14) + self.assertTrue(all(site.classification == "gated" for site in inventory)) def test_repository_walk_skips_tests_build_outputs_and_reports_unscannable_source(self) -> None: with tempfile.TemporaryDirectory() as directory: