From ad922303d7947eb57dcef279a0f898a5af065294 Mon Sep 17 00:00:00 2001 From: stone Date: Thu, 30 Jul 2026 23:39:49 +0800 Subject: [PATCH 1/7] fix: guard compiler acceptance CLI output privacy --- applications/compiler_runner.py | 39 ++++++-- .../test_compiler_runner_acceptance_report.py | 52 ++++++++++ .../unit/test_compiler_runner_cli_privacy.py | 97 +++++++++++++++++++ 3 files changed, 181 insertions(+), 7 deletions(-) create mode 100644 tests/unit/test_compiler_runner_cli_privacy.py diff --git a/applications/compiler_runner.py b/applications/compiler_runner.py index 7fa35652..80154424 100644 --- a/applications/compiler_runner.py +++ b/applications/compiler_runner.py @@ -237,11 +237,17 @@ def _parser() -> argparse.ArgumentParser: def _safe_markdown_files(root: Path) -> tuple[Path, ...]: - if not root.is_dir(): - raise ValueError("acceptance root must be a directory") + if root.is_symlink() or not root.is_dir(): + raise ValueError("acceptance root must be a non-symlink directory") return tuple( sorted( - (path for path in root.rglob("*.md") if path.is_file()), + ( + path + for path in root.rglob("*", recurse_symlinks=False) + if path.suffix.casefold() == ".md" + and not path.is_symlink() + and path.is_file() + ), key=lambda path: PurePath(*path.relative_to(root).parts).as_posix(), ) ) @@ -330,7 +336,19 @@ def _write_acceptance_report( *, acceptance_context: _AcceptanceContext, ) -> None: - if ".context-engine" not in output.parts: + try: + state_index = len(output.parts) - 1 - output.parts[::-1].index( + ".context-engine" + ) + except ValueError: + raise ValueError( + "acceptance reports must be written under .context-engine" + ) from None + state_directory = Path(*output.parts[: state_index + 1]).resolve() + resolved_output = output.resolve() + if resolved_output == state_directory or not resolved_output.is_relative_to( + state_directory + ): raise ValueError("acceptance reports must be written under .context-engine") report = _acceptance_report( root, @@ -338,8 +356,8 @@ def _write_acceptance_report( acceptance_context=acceptance_context, ) serialized = json.dumps(report, sort_keys=True, separators=(",", ":")) + "\n" - output.parent.mkdir(parents=True, exist_ok=True) - output.write_text(serialized, encoding="utf-8") + resolved_output.parent.mkdir(parents=True, exist_ok=True) + resolved_output.write_text(serialized, encoding="utf-8") sys.stdout.write(serialized) @@ -368,5 +386,12 @@ def main() -> None: raise SystemExit("one runner operation is required") +def _privacy_safe_main() -> None: + try: + main() + except Exception: + raise SystemExit("compiler runner operation failed") from None + + if __name__ == "__main__": - main() + _privacy_safe_main() diff --git a/tests/unit/test_compiler_runner_acceptance_report.py b/tests/unit/test_compiler_runner_acceptance_report.py index d7010849..c075737e 100644 --- a/tests/unit/test_compiler_runner_acceptance_report.py +++ b/tests/unit/test_compiler_runner_acceptance_report.py @@ -231,6 +231,58 @@ def test_acceptance_report_is_count_only_deterministic_and_written_under_ignore( assert first.stdout == second.stdout +def test_acceptance_corpus_matches_file_provider_markdown_directory_rules( + tmp_path: Path, +) -> None: + corpus = tmp_path / "corpus" + corpus.mkdir() + included = corpus / "included.MD" + included.write_text("# Included\n", encoding="utf-8") + ordinary = corpus / "ordinary.md" + ordinary.write_text("# Ordinary\n", encoding="utf-8") + target = corpus / "target.md" + target.write_text("# Target\n", encoding="utf-8") + (corpus / "linked.md").symlink_to(target) + external = tmp_path / "external" + external.mkdir() + (external / "outside.md").write_text("# Outside\n", encoding="utf-8") + (corpus / "linked-directory").symlink_to(external, target_is_directory=True) + + discovered = compiler_runner._safe_markdown_files(corpus) + + assert discovered == (included, ordinary, target) + + +def test_acceptance_corpus_root_must_not_be_a_symlink(tmp_path: Path) -> None: + corpus = tmp_path / "corpus" + corpus.mkdir() + linked_root = tmp_path / "linked-root" + linked_root.symlink_to(corpus, target_is_directory=True) + + with pytest.raises(ValueError, match="non-symlink directory"): + compiler_runner._safe_markdown_files(linked_root) + + +def test_acceptance_output_must_resolve_beneath_its_state_directory( + tmp_path: Path, +) -> None: + corpus = tmp_path / "corpus" + corpus.mkdir() + state_directory = tmp_path / ".context-engine" + state_directory.mkdir() + escaped_output = state_directory / ".." / "report.json" + + with pytest.raises(ValueError, match="under .context-engine"): + compiler_runner._write_acceptance_report( + corpus, + escaped_output, + 2048, + acceptance_context=compiler_runner.acceptance_context(), + ) + + assert not (tmp_path / "report.json").exists() + + @pytest.mark.parametrize( "failure_kind", ("io", "permission", "vanished", "directory"), diff --git a/tests/unit/test_compiler_runner_cli_privacy.py b/tests/unit/test_compiler_runner_cli_privacy.py new file mode 100644 index 00000000..66af307d --- /dev/null +++ b/tests/unit/test_compiler_runner_cli_privacy.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +import re +import subprocess +import sys +from pathlib import Path + +REPOSITORY_ROOT = Path(__file__).parents[2] +_MACHINE_ABSOLUTE_PATH_PATTERNS = ( + re.compile(r"(? subprocess.CompletedProcess[str]: + return subprocess.run( + [sys.executable, "-m", "applications.compiler_runner", *arguments], + cwd=REPOSITORY_ROOT, + check=False, + capture_output=True, + text=True, + timeout=30, + ) + + +def _assert_process_output_is_private( + completed: subprocess.CompletedProcess[str], +) -> None: + captured = completed.stdout + completed.stderr + + assert str(REPOSITORY_ROOT) not in captured + assert all( + pattern.search(captured) is None + for pattern in _MACHINE_ABSOLUTE_PATH_PATTERNS + ) + + +def test_machine_absolute_path_guard_recognizes_platform_path_shapes() -> None: + canaries = ( + "/private-machine/repository/app.py", + "C:\\Users\\person\\repository\\app.py", + "\\\\server\\share\\repository\\app.py", + ) + + assert all( + any( + pattern.search(canary) is not None + for pattern in _MACHINE_ABSOLUTE_PATH_PATTERNS + ) + for canary in canaries + ) + + +def test_acceptance_cli_success_output_has_no_machine_local_absolute_path( + tmp_path: Path, +) -> None: + corpus = tmp_path / "corpus" + corpus.mkdir() + (corpus / "note.md").write_text("# Note\n\nBody.\n", encoding="utf-8") + output = tmp_path / ".context-engine/report.json" + + completed = _run_acceptance_cli( + "--acceptance-report", + "--root", + str(corpus), + "--output", + str(output), + ) + + assert completed.returncode == 0 + _assert_process_output_is_private(completed) + + +def test_five_acceptance_cli_operator_errors_and_uncaught_exceptions_are_private( + tmp_path: Path, +) -> None: + corpus = tmp_path / "corpus" + corpus.mkdir() + (corpus / "note.md").write_text("# Note\n\nBody.\n", encoding="utf-8") + root_file = tmp_path / "root-file.md" + root_file.write_text("# Not a directory\n", encoding="utf-8") + state_directory = tmp_path / ".context-engine" + state_directory.mkdir() + operator_errors = ( + ("--root", str(tmp_path / "missing")), + ("--root", str(root_file)), + ("--root", str(corpus), "--output", str(tmp_path / "report.json")), + ("--root", str(corpus), "--token-ceiling", "0"), + ("--root", str(corpus), "--output", str(state_directory)), + ) + + for arguments in operator_errors: + completed = _run_acceptance_cli("--acceptance-report", *arguments) + + assert completed.returncode != 0 + _assert_process_output_is_private(completed) From 4929ec8ae5a8b83283136ee727f6a4dbcd07fc7e Mon Sep 17 00:00:00 2001 From: stone Date: Thu, 30 Jul 2026 23:40:13 +0800 Subject: [PATCH 2/7] docs: document test target isolation --- CONTRIBUTING.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5b7dd81c..fd26e555 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -85,6 +85,14 @@ make integration # real-PostgreSQL integration/security harness make security-gate # M0 security veto gate ``` +Run `make typecheck` and `make test` sequentially, never concurrently. +`tests/unit/test_bot_delivery_model_egress_contract.py` invokes +`npm run build`, which writes the same TypeScript build tree that +`make typecheck` reads through `tsc`; overlap can therefore produce a spurious +test failure. A single non-reproducible `test_egress_grant` failure is +watch-only: do not treat it as a known defect without a fresh reproduction, +and split a recurrence into its own issue with that evidence. + When you are done, stop the harness: ```bash From a3ee39801b6dd35a26973b44ae4a24391444a770 Mon Sep 17 00:00:00 2001 From: stone Date: Thu, 30 Jul 2026 23:44:41 +0800 Subject: [PATCH 3/7] fix: redact compiler argument errors --- applications/compiler_runner.py | 7 ++++++- tests/unit/test_compiler_runner_cli_privacy.py | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/applications/compiler_runner.py b/applications/compiler_runner.py index 80154424..fa75af96 100644 --- a/applications/compiler_runner.py +++ b/applications/compiler_runner.py @@ -45,6 +45,11 @@ def __call__( ) -> CompilationOutcome: ... +class _PrivacySafeArgumentParser(argparse.ArgumentParser): + def error(self, message: str) -> None: + raise SystemExit("compiler runner arguments are invalid") + + def _boundary_failure() -> CompilationFailure: return CompilationFailure( code=CompilationFailureCode.UNSUPPORTED_DOCUMENT_SHAPE, @@ -202,7 +207,7 @@ def _emit( def _parser() -> argparse.ArgumentParser: - parser = argparse.ArgumentParser(description=__doc__) + parser = _PrivacySafeArgumentParser(description=__doc__) parser.add_argument("--compile", action="store_true") parser.add_argument("--config", default="markdown-config-v3") parser.add_argument( diff --git a/tests/unit/test_compiler_runner_cli_privacy.py b/tests/unit/test_compiler_runner_cli_privacy.py index 66af307d..7974725f 100644 --- a/tests/unit/test_compiler_runner_cli_privacy.py +++ b/tests/unit/test_compiler_runner_cli_privacy.py @@ -72,6 +72,16 @@ def test_acceptance_cli_success_output_has_no_machine_local_absolute_path( _assert_process_output_is_private(completed) +def test_acceptance_cli_parser_error_does_not_echo_the_invalid_argument() -> None: + invalid_argument = "/private-machine/repository/not-an-integer" + + completed = _run_acceptance_cli("--token-ceiling", invalid_argument) + + assert completed.returncode != 0 + assert invalid_argument not in completed.stdout + completed.stderr + _assert_process_output_is_private(completed) + + def test_five_acceptance_cli_operator_errors_and_uncaught_exceptions_are_private( tmp_path: Path, ) -> None: From 51964fb22c3801be848177eb24e74eb7129a74bf Mon Sep 17 00:00:00 2001 From: stone Date: Thu, 30 Jul 2026 23:54:01 +0800 Subject: [PATCH 4/7] fix: type compiler privacy boundaries --- applications/compiler_runner.py | 4 ++-- tests/unit/test_compiler_runner_acceptance_report.py | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/applications/compiler_runner.py b/applications/compiler_runner.py index fa75af96..46ca3820 100644 --- a/applications/compiler_runner.py +++ b/applications/compiler_runner.py @@ -10,7 +10,7 @@ import subprocess import sys from pathlib import Path, PurePath -from typing import Final, Protocol, cast +from typing import Final, Never, Protocol, cast from adapters.parsers.ragflow_markdown import compile_rich_markdown, rich_token_count from engine.supply import ( @@ -46,7 +46,7 @@ def __call__( class _PrivacySafeArgumentParser(argparse.ArgumentParser): - def error(self, message: str) -> None: + def error(self, message: str) -> Never: raise SystemExit("compiler runner arguments are invalid") diff --git a/tests/unit/test_compiler_runner_acceptance_report.py b/tests/unit/test_compiler_runner_acceptance_report.py index c075737e..9426f5a2 100644 --- a/tests/unit/test_compiler_runner_acceptance_report.py +++ b/tests/unit/test_compiler_runner_acceptance_report.py @@ -10,6 +10,7 @@ import pytest import applications.compiler_runner as compiler_runner +from eval._compiler_acceptance import acceptance_context from eval.embedding_benchmark import ( BenchmarkUnavailable, validate_json_schema_document, @@ -277,7 +278,7 @@ def test_acceptance_output_must_resolve_beneath_its_state_directory( corpus, escaped_output, 2048, - acceptance_context=compiler_runner.acceptance_context(), + acceptance_context=acceptance_context(), ) assert not (tmp_path / "report.json").exists() From 5f8ddb0cdc83472db085048a6d28b09b32504d9e Mon Sep 17 00:00:00 2001 From: stone Date: Fri, 31 Jul 2026 01:35:29 +0800 Subject: [PATCH 5/7] fix: align compiler acceptance privacy rules --- applications/compiler_runner.py | 10 +++-- .../test_compiler_runner_acceptance_report.py | 20 +++++++-- .../unit/test_compiler_runner_cli_privacy.py | 45 ++++++++++++++++--- 3 files changed, 62 insertions(+), 13 deletions(-) diff --git a/applications/compiler_runner.py b/applications/compiler_runner.py index 46ca3820..9917feb9 100644 --- a/applications/compiler_runner.py +++ b/applications/compiler_runner.py @@ -243,13 +243,13 @@ def _parser() -> argparse.ArgumentParser: def _safe_markdown_files(root: Path) -> tuple[Path, ...]: if root.is_symlink() or not root.is_dir(): - raise ValueError("acceptance root must be a non-symlink directory") + raise SystemExit("acceptance root must be a non-symlink directory") return tuple( sorted( ( path for path in root.rglob("*", recurse_symlinks=False) - if path.suffix.casefold() == ".md" + if path.name.casefold().endswith(".md") and not path.is_symlink() and path.is_file() ), @@ -346,7 +346,7 @@ def _write_acceptance_report( ".context-engine" ) except ValueError: - raise ValueError( + raise SystemExit( "acceptance reports must be written under .context-engine" ) from None state_directory = Path(*output.parts[: state_index + 1]).resolve() @@ -354,7 +354,7 @@ def _write_acceptance_report( if resolved_output == state_directory or not resolved_output.is_relative_to( state_directory ): - raise ValueError("acceptance reports must be written under .context-engine") + raise SystemExit("acceptance reports must be written under .context-engine") report = _acceptance_report( root, token_ceiling, @@ -381,6 +381,8 @@ def main() -> None: if args.acceptance_report: if args.root is None: raise SystemExit("--acceptance-report requires --root") + if cast(int, args.token_ceiling) < 1: + raise SystemExit("rich Markdown token ceiling must be positive") _write_acceptance_report( cast(Path, args.root), cast(Path, args.output), diff --git a/tests/unit/test_compiler_runner_acceptance_report.py b/tests/unit/test_compiler_runner_acceptance_report.py index 9426f5a2..a986b6d0 100644 --- a/tests/unit/test_compiler_runner_acceptance_report.py +++ b/tests/unit/test_compiler_runner_acceptance_report.py @@ -237,6 +237,14 @@ def test_acceptance_corpus_matches_file_provider_markdown_directory_rules( ) -> None: corpus = tmp_path / "corpus" corpus.mkdir() + lowercase_directory = corpus / "bare-lowercase" + lowercase_directory.mkdir() + bare_lowercase = lowercase_directory / ".md" + bare_lowercase.write_text("# Bare lowercase\n", encoding="utf-8") + uppercase_directory = corpus / "bare-uppercase" + uppercase_directory.mkdir() + bare_uppercase = uppercase_directory / ".MD" + bare_uppercase.write_text("# Bare uppercase\n", encoding="utf-8") included = corpus / "included.MD" included.write_text("# Included\n", encoding="utf-8") ordinary = corpus / "ordinary.md" @@ -251,7 +259,13 @@ def test_acceptance_corpus_matches_file_provider_markdown_directory_rules( discovered = compiler_runner._safe_markdown_files(corpus) - assert discovered == (included, ordinary, target) + assert discovered == ( + bare_lowercase, + bare_uppercase, + included, + ordinary, + target, + ) def test_acceptance_corpus_root_must_not_be_a_symlink(tmp_path: Path) -> None: @@ -260,7 +274,7 @@ def test_acceptance_corpus_root_must_not_be_a_symlink(tmp_path: Path) -> None: linked_root = tmp_path / "linked-root" linked_root.symlink_to(corpus, target_is_directory=True) - with pytest.raises(ValueError, match="non-symlink directory"): + with pytest.raises(SystemExit, match="non-symlink directory"): compiler_runner._safe_markdown_files(linked_root) @@ -273,7 +287,7 @@ def test_acceptance_output_must_resolve_beneath_its_state_directory( state_directory.mkdir() escaped_output = state_directory / ".." / "report.json" - with pytest.raises(ValueError, match="under .context-engine"): + with pytest.raises(SystemExit, match="under .context-engine"): compiler_runner._write_acceptance_report( corpus, escaped_output, diff --git a/tests/unit/test_compiler_runner_cli_privacy.py b/tests/unit/test_compiler_runner_cli_privacy.py index 7974725f..aef37537 100644 --- a/tests/unit/test_compiler_runner_cli_privacy.py +++ b/tests/unit/test_compiler_runner_cli_privacy.py @@ -5,9 +5,12 @@ import sys from pathlib import Path +import pytest + REPOSITORY_ROOT = Path(__file__).parents[2] _MACHINE_ABSOLUTE_PATH_PATTERNS = ( re.compile(r"(? None: canaries = ( "/private-machine/repository/app.py", + "C:/" + "Users" + "/person/repository/app.py", "C:\\Users\\person\\repository\\app.py", "\\\\server\\share\\repository\\app.py", ) @@ -52,6 +56,18 @@ def test_machine_absolute_path_guard_recognizes_platform_path_shapes() -> None: ) +def test_process_output_privacy_assertion_rejects_a_planted_leak() -> None: + completed = subprocess.CompletedProcess( + args=(), + returncode=1, + stdout="C:/" + "Users" + "/person/repository/app.py\n", + stderr="", + ) + + with pytest.raises(AssertionError): + _assert_process_output_is_private(completed) + + def test_acceptance_cli_success_output_has_no_machine_local_absolute_path( tmp_path: Path, ) -> None: @@ -93,15 +109,32 @@ def test_five_acceptance_cli_operator_errors_and_uncaught_exceptions_are_private state_directory = tmp_path / ".context-engine" state_directory.mkdir() operator_errors = ( - ("--root", str(tmp_path / "missing")), - ("--root", str(root_file)), - ("--root", str(corpus), "--output", str(tmp_path / "report.json")), - ("--root", str(corpus), "--token-ceiling", "0"), - ("--root", str(corpus), "--output", str(state_directory)), + ( + ("--root", str(tmp_path / "missing")), + "acceptance root must be a non-symlink directory", + ), + ( + ("--root", str(root_file)), + "acceptance root must be a non-symlink directory", + ), + ( + ("--root", str(corpus), "--output", str(tmp_path / "report.json")), + "acceptance reports must be written under .context-engine", + ), + ( + ("--root", str(corpus), "--token-ceiling", "0"), + "rich Markdown token ceiling must be positive", + ), + ( + ("--root", str(corpus), "--output", str(state_directory)), + "acceptance reports must be written under .context-engine", + ), ) - for arguments in operator_errors: + for arguments, expected_message in operator_errors: completed = _run_acceptance_cli("--acceptance-report", *arguments) assert completed.returncode != 0 + assert completed.stdout == "" + assert completed.stderr == f"{expected_message}\n" _assert_process_output_is_private(completed) From 87518076a53dd1b300b18f168c0d14135238366e Mon Sep 17 00:00:00 2001 From: stone Date: Fri, 31 Jul 2026 01:35:35 +0800 Subject: [PATCH 6/7] docs: separate test collision guidance --- CONTRIBUTING.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fd26e555..a81404c9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -86,12 +86,16 @@ make security-gate # M0 security veto gate ``` Run `make typecheck` and `make test` sequentially, never concurrently. -`tests/unit/test_bot_delivery_model_egress_contract.py` invokes -`npm run build`, which writes the same TypeScript build tree that -`make typecheck` reads through `tsc`; overlap can therefore produce a spurious -test failure. A single non-reproducible `test_egress_grant` failure is -watch-only: do not treat it as a known defect without a fresh reproduction, -and split a recurrence into its own issue with that evidence. +`make test` depends on `bot-build`, and +`tests/unit/test_bot_delivery_model_egress_contract.py` also invokes +`npm run build`. Both can write TypeScript build trees while `make typecheck` +reads them through `tsc`; overlap can therefore produce a spurious test +failure. + +Separately, a single non-reproducible `test_egress_grant` failure remains an +undiagnosed watch item and is not attributed to the build-tree collision above. +Do not treat it as a known defect without a fresh reproduction; split a +recurrence into its own issue with that evidence. When you are done, stop the harness: From 0e66d751fa171bbece574e82fa35865d5dadc6a8 Mon Sep 17 00:00:00 2001 From: stone Date: Fri, 31 Jul 2026 01:38:49 +0800 Subject: [PATCH 7/7] test: prove unexpected compiler failure privacy --- .../unit/test_compiler_runner_cli_privacy.py | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_compiler_runner_cli_privacy.py b/tests/unit/test_compiler_runner_cli_privacy.py index aef37537..8dadf9a2 100644 --- a/tests/unit/test_compiler_runner_cli_privacy.py +++ b/tests/unit/test_compiler_runner_cli_privacy.py @@ -98,7 +98,7 @@ def test_acceptance_cli_parser_error_does_not_echo_the_invalid_argument() -> Non _assert_process_output_is_private(completed) -def test_five_acceptance_cli_operator_errors_and_uncaught_exceptions_are_private( +def test_five_acceptance_cli_operator_errors_emit_their_specific_safe_messages( tmp_path: Path, ) -> None: corpus = tmp_path / "corpus" @@ -138,3 +138,28 @@ def test_five_acceptance_cli_operator_errors_and_uncaught_exceptions_are_private assert completed.stdout == "" assert completed.stderr == f"{expected_message}\n" _assert_process_output_is_private(completed) + + +def test_acceptance_cli_unexpected_failure_emits_only_the_generic_safe_message( + tmp_path: Path, +) -> None: + corpus = tmp_path / "corpus" + corpus.mkdir() + (corpus / "note.md").write_text("# Note\n\nBody.\n", encoding="utf-8") + state_directory = tmp_path / ".context-engine" + state_directory.mkdir() + blocked_parent = state_directory / "blocked-parent" + blocked_parent.write_text("not a directory\n", encoding="utf-8") + + completed = _run_acceptance_cli( + "--acceptance-report", + "--root", + str(corpus), + "--output", + str(blocked_parent / "report.json"), + ) + + assert completed.returncode != 0 + assert completed.stdout == "" + assert completed.stderr == "compiler runner operation failed\n" + _assert_process_output_is_private(completed)