diff --git a/src/agent_rules_kit/governance.py b/src/agent_rules_kit/governance.py index ab308e3..561c091 100644 --- a/src/agent_rules_kit/governance.py +++ b/src/agent_rules_kit/governance.py @@ -17,12 +17,14 @@ COMMAND_CONFIRMATION_RULE_ID = "AIRK-GOV004" COMMAND_CONFIRMATION_MESSAGE = ( - "Instruction file appears to encourage unsafe command execution without an explicit confirmation boundary." + "Instruction file appears to encourage unsafe command execution without an explicit " + "confirmation boundary." ) RUNTIME_NETWORK_LLM_RULE_ID = "AIRK-GOV005" RUNTIME_NETWORK_LLM_MESSAGE = ( - "Instruction file appears to encourage runtime network, LLM, or external API use that conflicts with local-first boundaries." + "Instruction file appears to encourage runtime network, LLM, or external API use that " + "conflicts with local-first boundaries." ) AUTHORITY_SCOPE_RULE_ID = "AIRK-GOV001" @@ -58,7 +60,8 @@ re.compile( r"\b(do not|don't|must not|should not|never|avoid|forbid|forbidden|no)\b" r".{0,120}\b(" - r"bypass(?:ing)?|skip(?:ping)?|ignore|commit(?:ting)?|push(?:ing|es)?|direct push(?:es)?|force[- ]push|merge" + r"bypass(?:ing)?|skip(?:ping)?|ignore|commit(?:ting)?|push(?:ing|es)?|" + r"direct push(?:es)?|force[- ]push|merge" r")\b", re.IGNORECASE, ), @@ -82,8 +85,17 @@ re.compile(r"\bchmod\s+-R\s+(777|[0-7]{3,4})\b", re.IGNORECASE), re.compile(r"\bchown\s+-R\b", re.IGNORECASE), re.compile(r"\b(curl|wget)\b.{0,120}\|\s*(sh|bash)\b", re.IGNORECASE), - re.compile(r"\b(install|uninstall)\b.{0,100}\b(without asking|without confirmation|automatically|always)\b", re.IGNORECASE), - re.compile(r"\b(run|execute)\b.{0,80}\b(repository|repo)\s+scripts?\b.{0,80}\b(automatically|without asking|as trusted instructions)\b", re.IGNORECASE), + re.compile( + r"\b(install|uninstall)\b.{0,100}\b(" + r"without asking|without confirmation|automatically|always" + r")\b", + re.IGNORECASE, + ), + re.compile( + r"\b(run|execute)\b.{0,80}\b(repository|repo)\s+scripts?\b" + r".{0,80}\b(automatically|without asking|as trusted instructions)\b", + re.IGNORECASE, + ), ) NEGATED_COMMAND_CONFIRMATION_CONTEXT_PATTERNS: tuple[Pattern[str], ...] = ( @@ -121,7 +133,10 @@ re.compile( r"\b(send|upload|post|transmit|share)\b" r".{0,100}\b(repository|repo|source code|codebase|workspace|context|files?)\b" - r".{0,140}\b(OpenAI|Anthropic|Claude|Gemini|ChatGPT|LLM|external API|external service|remote service)\b", + r".{0,140}\b(" + r"OpenAI|Anthropic|Claude|Gemini|ChatGPT|LLM|external API|" + r"external service|remote service" + r")\b", re.IGNORECASE, ), re.compile( @@ -132,30 +147,50 @@ ), re.compile( r"\b(call|query|use)\b" - r".{0,100}\b(remote\s+)?(LLM|OpenAI|Anthropic|Claude|Gemini|ChatGPT|external API|remote API)\b" + r".{0,100}\b(remote\s+)?(" + r"LLM|OpenAI|Anthropic|Claude|Gemini|ChatGPT|external API|remote API" + r")\b" r".{0,100}\b(check|runtime|scan|scanning|audit|analyze|analysis|validation|validate)\b", re.IGNORECASE, ), re.compile( r"\b(use|call|invoke|query)\b" - r".{0,80}\b(Claude API|Anthropic API|OpenAI API|Gemini API|ChatGPT API|LLM API|remote LLM|external LLM)\b", + r".{0,80}\b(" + r"Claude API|Anthropic API|OpenAI API|Gemini API|ChatGPT API|LLM API|" + r"remote LLM|external LLM" + r")\b", re.IGNORECASE, ), re.compile( r"\b(validator|linter|tool|CLI|command|check|runtime|execution)\b" r".{0,120}\b(depends on|requires?|needs?|uses?|using|must use|must call)\b" - r".{0,120}\b(OpenAI API|Anthropic API|Claude API|Gemini API|ChatGPT API|LLM API|external API|remote API)\b", + r".{0,120}\b(" + r"OpenAI API|Anthropic API|Claude API|Gemini API|ChatGPT API|LLM API|" + r"external API|remote API" + r")\b", re.IGNORECASE, ), re.compile( - r"\b(OpenAI API|Anthropic API|Claude API|Gemini API|ChatGPT API|LLM API|external API|remote API)\b" - r".{0,120}\b(during execution|at runtime|runtime|for validation|to validate|for analysis|to analyze)\b", + r"\b(" + r"OpenAI API|Anthropic API|Claude API|Gemini API|ChatGPT API|LLM API|" + r"external API|remote API" + r")\b" + r".{0,120}\b(" + r"during execution|at runtime|runtime|for validation|to validate|for analysis|" + r"to analyze" + r")\b", re.IGNORECASE, ), re.compile( - r"\b(validat(?:e|es|ing|ion)|verif(?:y|ies|ying|ication)|check(?:s|ing)?|analyz(?:e|es|ing|sis))\b" + r"\b(" + r"validat(?:e|es|ing|ion)|verif(?:y|ies|ying|ication)|check(?:s|ing)?|" + r"analyz(?:e|es|ing|sis)" + r")\b" r".{0,80}\b(via|using|through|with|by calling|by querying)\b" - r".{0,80}\b(Claude(?:\s+API)?|Anthropic(?:\s+API)?|OpenAI(?:\s+API)?|Gemini(?:\s+API)?|ChatGPT|LLM API|remote API|external API)\b", + r".{0,80}\b(" + r"Claude(?:\s+API)?|Anthropic(?:\s+API)?|OpenAI(?:\s+API)?|" + r"Gemini(?:\s+API)?|ChatGPT|LLM API|remote API|external API" + r")\b", re.IGNORECASE, ), re.compile( @@ -169,13 +204,22 @@ NEGATED_RUNTIME_NETWORK_LLM_CONTEXT_PATTERNS: tuple[Pattern[str], ...] = ( re.compile( r"\b(do not|don't|must not|should not|never|avoid|avoids|forbid|forbidden|no|without)\b" - r".{0,180}\b(network|internet|online|LLMs?|OpenAI|Anthropic|Claude|Gemini|ChatGPT|external APIs?|remote services?|API calls?)\b", + r".{0,180}\b(" + r"network|internet|online|LLMs?|OpenAI|Anthropic|Claude|Gemini|ChatGPT|" + r"external APIs?|remote services?|API calls?" + r")\b", re.IGNORECASE, ), re.compile( r"\b(does not|do not|don't|must not|should not|never|avoid|avoids|no)\b" - r".{0,140}\b(call|use|depend|requires?|needs?|rely|relies|send|upload|post|transmit|share)\b" - r".{0,140}\b(network|LLMs?|OpenAI|Anthropic|Claude|Gemini|ChatGPT|external APIs?|remote services?)\b", + r".{0,140}\b(" + r"call|use|depend|requires?|needs?|rely|relies|send|upload|post|" + r"transmit|share" + r")\b" + r".{0,140}\b(" + r"network|LLMs?|OpenAI|Anthropic|Claude|Gemini|ChatGPT|external APIs?|" + r"remote services?" + r")\b", re.IGNORECASE, ), re.compile( diff --git a/tests/test_cli.py b/tests/test_cli.py index bfd9ff5..5c43e08 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -11,6 +11,19 @@ FIXTURE_ROOT = Path(__file__).parent / "fixtures" / "repositories" +REVIEW_CI_BYPASS_MESSAGE = ( + "Instruction file appears to encourage bypassing review, CI, or safe integration " + "boundaries." +) +COMMAND_CONFIRMATION_MESSAGE = ( + "Instruction file appears to encourage unsafe command execution without an explicit " + "confirmation boundary." +) +RUNTIME_NETWORK_LLM_MESSAGE = ( + "Instruction file appears to encourage runtime network, LLM, or external API use that " + "conflicts with local-first boundaries." +) + class CliTests(unittest.TestCase): def test_version_flag_prints_version(self) -> None: @@ -314,9 +327,10 @@ def test_init_dry_run_plans_backup_before_replace_without_writing(self) -> None: def test_init_requires_explicit_mode(self) -> None: output = io.StringIO() - with tempfile.TemporaryDirectory() as temporary_directory: - with redirect_stderr(output): - exit_code = main(["init", temporary_directory]) + with tempfile.TemporaryDirectory() as temporary_directory, redirect_stderr( + output + ): + exit_code = main(["init", temporary_directory]) self.assertEqual(exit_code, 2) self.assertIn( @@ -355,16 +369,17 @@ def test_init_dry_run_redacts_secret_like_repository_values(self) -> None: def test_init_rejects_dry_run_and_write_together(self) -> None: output = io.StringIO() - with tempfile.TemporaryDirectory() as temporary_directory: - with redirect_stderr(output): - exit_code = main( - [ - "init", - temporary_directory, - "--dry-run", - "--write", - ] - ) + with tempfile.TemporaryDirectory() as temporary_directory, redirect_stderr( + output + ): + exit_code = main( + [ + "init", + temporary_directory, + "--dry-run", + "--write", + ] + ) self.assertEqual(exit_code, 2) self.assertIn( @@ -617,7 +632,7 @@ def test_check_console_reports_review_ci_bypass_findings(self) -> None: self.assertIn("AIRK-GOV003 [warning] AGENTS.md:8", text) self.assertIn("AIRK-GOV003 [warning] AGENTS.md:10", text) self.assertIn( - "Instruction file appears to encourage bypassing review, CI, or safe integration boundaries.", + REVIEW_CI_BYPASS_MESSAGE, text, ) @@ -709,7 +724,7 @@ def test_check_markdown_reports_review_ci_bypass_findings(self) -> None: self.assertIn("| AIRK-GOV003 | warning | AGENTS.md:8 |", text) self.assertIn("| AIRK-GOV003 | warning | AGENTS.md:10 |", text) self.assertIn( - "Instruction file appears to encourage bypassing review, CI, or safe integration boundaries.", + REVIEW_CI_BYPASS_MESSAGE, text, ) @@ -727,7 +742,7 @@ def test_check_console_reports_unsafe_command_execution_findings(self) -> None: self.assertIn("Findings:", text) self.assertIn("AIRK-GOV004 [warning] AGENTS.md:9", text) self.assertIn( - "Instruction file appears to encourage unsafe command execution without an explicit confirmation boundary.", + COMMAND_CONFIRMATION_MESSAGE, text, ) @@ -773,7 +788,7 @@ def test_check_markdown_reports_unsafe_command_execution_findings(self) -> None: self.assertIn("## Findings", text) self.assertIn("| AIRK-GOV004 | warning | AGENTS.md:9 |", text) self.assertIn( - "Instruction file appears to encourage unsafe command execution without an explicit confirmation boundary.", + COMMAND_CONFIRMATION_MESSAGE, text, ) @@ -790,7 +805,7 @@ def test_check_console_reports_runtime_network_llm_findings(self) -> None: self.assertIn("Findings:", text) self.assertIn("AIRK-GOV005 [warning] AGENTS.md:9", text) self.assertIn( - "Instruction file appears to encourage runtime network, LLM, or external API use that conflicts with local-first boundaries.", + RUNTIME_NETWORK_LLM_MESSAGE, text, ) @@ -836,7 +851,7 @@ def test_check_markdown_reports_runtime_network_llm_findings(self) -> None: self.assertIn("## Findings", text) self.assertIn("| AIRK-GOV005 | warning | AGENTS.md:9 |", text) self.assertIn( - "Instruction file appears to encourage runtime network, LLM, or external API use that conflicts with local-first boundaries.", + RUNTIME_NETWORK_LLM_MESSAGE, text, ) diff --git a/tests/test_discovery.py b/tests/test_discovery.py index bf3e8ac..7009cc8 100644 --- a/tests/test_discovery.py +++ b/tests/test_discovery.py @@ -63,9 +63,10 @@ def test_discovery_rejects_missing_root(self) -> None: discover_instruction_files(FIXTURE_ROOT / "missing-repo") def test_discovery_rejects_file_root(self) -> None: - with tempfile.NamedTemporaryFile() as temporary_file: - with self.assertRaises(ValueError): - discover_instruction_files(temporary_file.name) + with tempfile.NamedTemporaryFile() as temporary_file, self.assertRaises( + ValueError + ): + discover_instruction_files(temporary_file.name) if __name__ == "__main__": diff --git a/tests/test_governance.py b/tests/test_governance.py index 681bb13..0b4ed0c 100644 --- a/tests/test_governance.py +++ b/tests/test_governance.py @@ -10,11 +10,18 @@ find_missing_authority_scope_findings, find_missing_secret_boundary_findings, find_review_ci_bypass_findings, - find_unsafe_command_execution_findings, find_runtime_network_llm_dependency_findings, + find_unsafe_command_execution_findings, find_unsupported_claim_findings, ) +SECRET_BOUNDARY_GUIDANCE = ( + "- Do not commit secrets, tokens, credentials, private URLs, or customer data." +) +HUMAN_LLM_PLANNING_GUIDANCE = ( + "- A human may use ChatGPT or Claude for planning, with no secrets and human review." +) + class GovernanceFindingTests(unittest.TestCase): def test_reports_unsupported_security_and_maturity_claims(self) -> None: @@ -215,7 +222,8 @@ def test_ignores_safe_or_confirmed_command_guidance(self) -> None: "Rules:", "", "- Do not run destructive commands without explicit human confirmation.", - "- Ask the maintainer before using sudo, rm -rf, chmod -R, or downloaded scripts.", + "- Ask the maintainer before using sudo, rm -rf, chmod -R, " + "or downloaded scripts.", "- Run pytest -q.", "- Run ruff check .", "- Run git diff --check.", @@ -339,7 +347,7 @@ def test_ignores_safe_or_human_reviewed_network_llm_guidance(self) -> None: "- The CLI does not use the OpenAI API during execution.", "- Do not commit OPENAI_API_KEY or other API keys.", "- Supported instruction files include CLAUDE.md and GEMINI.md.", - "- A human may use ChatGPT or Claude for planning, with no secrets and human review.", + HUMAN_LLM_PLANNING_GUIDANCE, "- Use pull requests and GitHub CI before merge.", ] ) @@ -411,7 +419,7 @@ def test_ignores_files_with_secret_handling_boundary(self) -> None: "", "Rules:", "", - "- Do not commit secrets, tokens, credentials, private URLs, or customer data.", + SECRET_BOUNDARY_GUIDANCE, ] ) + "\n", @@ -443,7 +451,7 @@ def test_ignores_files_with_authority_scope_variant_boundaries(self) -> None: "", "Rules:", "", - "- Do not commit secrets, tokens, credentials, private URLs, or customer data.", + SECRET_BOUNDARY_GUIDANCE, f"- {guidance}", ] ) @@ -468,7 +476,7 @@ def test_reports_missing_authority_scope_boundary(self) -> None: "", "- Read relevant files before editing.", "- Run local checks before committing.", - "- Do not commit secrets, tokens, credentials, private URLs, or customer data.", + SECRET_BOUNDARY_GUIDANCE, ] ) + "\n", @@ -497,7 +505,7 @@ def test_ignores_files_with_authority_scope_boundary(self) -> None: "", "- Read relevant files before editing.", "- Run local checks before committing.", - "- Do not commit secrets, tokens, credentials, private URLs, or customer data.", + SECRET_BOUNDARY_GUIDANCE, ] ) + "\n", diff --git a/tests/test_init_plan.py b/tests/test_init_plan.py index 2eecdb4..2d88e8a 100644 --- a/tests/test_init_plan.py +++ b/tests/test_init_plan.py @@ -42,9 +42,10 @@ def test_build_init_plan_rejects_missing_root(self) -> None: build_init_plan(missing_root) def test_build_init_plan_rejects_file_root(self) -> None: - with tempfile.NamedTemporaryFile() as temporary_file: - with self.assertRaises(ValueError): - build_init_plan(temporary_file.name) + with tempfile.NamedTemporaryFile() as temporary_file, self.assertRaises( + ValueError + ): + build_init_plan(temporary_file.name) if __name__ == "__main__":