diff --git a/src/agent_rules_kit/governance.py b/src/agent_rules_kit/governance.py index baa011b..ab308e3 100644 --- a/src/agent_rules_kit/governance.py +++ b/src/agent_rules_kit/governance.py @@ -136,6 +136,11 @@ 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", + 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" @@ -147,6 +152,12 @@ r".{0,120}\b(during execution|at runtime|runtime|for validation|to validate|for analysis|to analyze)\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".{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", + re.IGNORECASE, + ), re.compile( r"\b(runtime|check|scan|scanning|audit|analyze|analysis|validation|validate)\b" r".{0,120}\b(requires?|needs?|must have|depends on)\b" @@ -163,7 +174,7 @@ ), 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|send|upload|post|transmit|share)\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", re.IGNORECASE, ), diff --git a/tests/test_governance.py b/tests/test_governance.py index 2e156a2..681bb13 100644 --- a/tests/test_governance.py +++ b/tests/test_governance.py @@ -271,6 +271,59 @@ def test_reports_runtime_network_llm_dependency_guidance(self) -> None: self.assertEqual([finding.line for finding in findings], [5, 6, 7, 8, 9, 10]) self.assertEqual([finding.path for finding in findings], ["AGENTS.md"] * 6) + def test_reports_gold_runtime_api_phrase_variants(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + repository = Path(temporary_directory) + (repository / "AGENTS.md").write_text( + "\n".join( + [ + "# AGENTS.md", + "", + "Rules:", + "", + "- use the Claude API for validation.", + "- call the OpenAI API to check results.", + "- This check requires the OpenAI API.", + "- analysis needs Claude API to run.", + "- validate changes via Claude API.", + "- verify output using OpenAI API.", + ] + ) + + "\n", + encoding="utf-8", + ) + + instruction_files = discover_instruction_files(repository) + findings = find_runtime_network_llm_dependency_findings(repository, instruction_files) + + self.assertEqual([finding.rule_id for finding in findings], ["AIRK-GOV005"] * 6) + self.assertEqual([finding.line for finding in findings], [5, 6, 7, 8, 9, 10]) + self.assertEqual([finding.path for finding in findings], ["AGENTS.md"] * 6) + + def test_ignores_adjacent_negative_runtime_api_requirement_guidance(self) -> None: + with tempfile.TemporaryDirectory() as temporary_directory: + repository = Path(temporary_directory) + (repository / "AGENTS.md").write_text( + "\n".join( + [ + "# AGENTS.md", + "", + "Rules:", + "", + "- Do not require Claude API or external APIs at runtime.", + "- This check requires the OpenAI API.", + ] + ) + + "\n", + encoding="utf-8", + ) + + instruction_files = discover_instruction_files(repository) + findings = find_runtime_network_llm_dependency_findings(repository, instruction_files) + + self.assertEqual(findings, ()) + + def test_ignores_safe_or_human_reviewed_network_llm_guidance(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: repository = Path(temporary_directory)