From 63789efec2fc6fad1ef887ba512519dbd5b76352 Mon Sep 17 00:00:00 2001 From: pt-act <211776491+pt-act@users.noreply.github.com> Date: Tue, 1 Sep 2026 09:48:34 +0100 Subject: [PATCH 1/2] fix(cli): restore unreachable corpus verify command and parsel verify alias The corpus integrity CLI (wallbreaker corpus verify [--update], aliased as 'wallbreaker parsel verify') was added in 53c9ca2 (roadmap-implementation TG3) but its dispatch was lost when the PR #21 line merged with the PR #24 line in da21689: the Daedalus side of cli.py won the merge, leaving _run_corpus_verify() defined but unreachable from the CLI. - add 'corpus' back to SUBCOMMANDS - restore the corpus subparser (--update / --lock) in build_sub_parser() - restore the dispatch: 'corpus' -> _run_corpus_verify(), plus the 'parsel verify' alias (choices + getattr routing) - re-add 'import subprocess' / 'from pathlib import Path' used by _run_corpus_verify / _resolve_lock_path (also lost in the merge) The load-time pin check (load_corpus_with_pin_check) was always intact; this only restores the interactive pin/verify surface its own error messages point users at ('run: wallbreaker corpus verify --update'). --- wallbreaker/cli.py | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/wallbreaker/cli.py b/wallbreaker/cli.py index f67191e..e2fd24b 100644 --- a/wallbreaker/cli.py +++ b/wallbreaker/cli.py @@ -2,7 +2,9 @@ import argparse import asyncio +import subprocess import sys +from pathlib import Path from dotenv import load_dotenv @@ -168,7 +170,7 @@ def _add_endpoint_flags(parser: argparse.ArgumentParser) -> None: parser.add_argument("--api-key", help="API key literal (prefer --api-key-env)") -SUBCOMMANDS = ("lib", "parsel", "eni", "transform", "findings", "report", "export", "check", "regrade", "baseline", "dashboard") +SUBCOMMANDS = ("lib", "parsel", "eni", "transform", "findings", "report", "export", "check", "regrade", "baseline", "dashboard", "corpus") def build_main_parser() -> argparse.ArgumentParser: @@ -244,7 +246,7 @@ def build_sub_parser() -> argparse.ArgumentParser: parsel = sub.add_parser( "parsel", help="Manage the P4RS3LT0NGV3 transform library (MCP server backend)" ) - parsel.add_argument("parsel_action", choices=["update", "list", "path"]) + parsel.add_argument("parsel_action", choices=["update", "list", "path", "verify"]) eni = sub.add_parser("eni", help="Browse the ENI persona-jailbreak collection") eni.add_argument("eni_action", choices=["list", "update", "path"]) @@ -305,6 +307,19 @@ def build_sub_parser() -> argparse.ArgumentParser: help="Acknowledge the risk of exposing this unauthenticated single-operator dashboard", ) + corpus = sub.add_parser("corpus", help="Manage corpus integrity pins (library.lock.toml)") + corpus_sub = corpus.add_subparsers(dest="corpus_action", required=True) + cv = corpus_sub.add_parser( + "verify", + help="Check corpus SHAs against library.lock.toml; non-zero exit on UNRESOLVED or DRIFT", + ) + cv.add_argument( + "--update", + action="store_true", + help="Attempt to resolve actual HEAD SHAs via git ls-remote and update the lock file", + ) + cv.add_argument("--lock", default=None, help="Path to library.lock.toml (default: repo root)") + return parser @@ -412,7 +427,11 @@ def main(argv: list[str] | None = None) -> int: from .tools.eni import run_eni_cli return run_eni_cli(args) + if args.command == "corpus": + return _run_corpus_verify(args) if args.command == "parsel": + if getattr(args, "parsel_action", None) == "verify": + return _run_corpus_verify(args) from .tools.parsel_lib import run_parsel_cli return run_parsel_cli(args) From bd30f995ba6fc7cb2eeed6eae8677493f7bc3ebc Mon Sep 17 00:00:00 2001 From: pt-act <211776491+pt-act@users.noreply.github.com> Date: Tue, 1 Sep 2026 12:11:40 +0100 Subject: [PATCH 2/2] test(cli): pin corpus verify routing + changelog entry Regression tests for the dispatch restored in the previous commit: parser-level parsing (corpus verify, --update/--lock, parsel verify alias), main() routing via monkeypatched _run_corpus_verify with return-code propagation, and a guard that the alias does not swallow the real parsel actions. Prevents a future merge from silently stranding the command again. CHANGELOG notes the fix under Unreleased. --- CHANGELOG.md | 5 +++ tests/test_corpus_cli_routing.py | 77 ++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 tests/test_corpus_cli_routing.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d353b3..d216345 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased — WebUI V2 unified operator surface +- Fixed `wallbreaker corpus verify [--update]` (and the `wallbreaker parsel verify` + alias) becoming unreachable when the PR #21 and PR #24 lines merged: the dispatch + and subparser were dropped from `cli.py` while `_run_corpus_verify` survived as + dead code. Load-time pin checks were unaffected; the interactive command works + again. Routing is now regression-tested (`tests/test_corpus_cli_routing.py`). - Added a shared typed capability catalog so TUI behavior is the canonical contract and every registered operation remains discoverable from V2. - Added server-owned queued executions with pause, resume, steering, attacker switching, diff --git a/tests/test_corpus_cli_routing.py b/tests/test_corpus_cli_routing.py new file mode 100644 index 0000000..25a2a49 --- /dev/null +++ b/tests/test_corpus_cli_routing.py @@ -0,0 +1,77 @@ +"""Regression tests: the `corpus verify` CLI routing must stay reachable. + +The dispatch was born in 53c9ca2 (roadmap-implementation TG3) and silently +lost when the PR #21 and PR #24 lines merged in da21689 — _run_corpus_verify +survived as dead code while nothing failed. These tests pin the routing so a +future merge cannot strand the command again. +""" +from __future__ import annotations + +from pathlib import Path + +import pytest + +import wallbreaker.cli as cli +from wallbreaker.cli import SUBCOMMANDS, _run_corpus_verify, build_sub_parser + + +def test_corpus_in_subcommands(): + assert "corpus" in SUBCOMMANDS + + +def test_corpus_verify_parses(): + args = build_sub_parser().parse_args(["corpus", "verify"]) + assert args.command == "corpus" + assert args.corpus_action == "verify" + assert args.update is False + + +def test_corpus_verify_parses_update_and_lock(): + args = build_sub_parser().parse_args( + ["corpus", "verify", "--update", "--lock", "/tmp/other.lock.toml"] + ) + assert args.update is True + assert args.lock == "/tmp/other.lock.toml" + + +def test_parsel_verify_alias_parses(): + args = build_sub_parser().parse_args(["parsel", "verify"]) + assert args.command == "parsel" + assert args.parsel_action == "verify" + + +def test_main_routes_corpus_verify(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + lock = tmp_path / "library.lock.toml" + lock.write_text("", encoding="utf-8") + called = {} + + def fake_verify(args): + called["lock"] = args.lock + return 7 + + monkeypatch.setattr(cli, "_run_corpus_verify", fake_verify) + rc = cli.main(["corpus", "verify", "--lock", str(lock)]) + assert rc == 7 + assert called["lock"] == str(lock) + + +def test_main_routes_parsel_verify_alias(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + called = {} + + def fake_verify(args): + called["via"] = "parsel" + return 0 + + monkeypatch.setattr(cli, "_run_corpus_verify", fake_verify) + rc = cli.main(["parsel", "verify"]) + assert rc == 0 + assert called["via"] == "parsel" + + +def test_main_routes_parsel_update_to_parsel_lib(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + """The verify alias must not swallow the real parsel actions.""" + monkeypatch.setattr( + "wallbreaker.tools.parsel_lib.run_parsel_cli", lambda args: 0 + ) + rc = cli.main(["parsel", "list"]) + assert rc == 0