Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 60 additions & 16 deletions src/agent_rules_kit/governance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
),
Expand All @@ -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], ...] = (
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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(
Expand Down
53 changes: 34 additions & 19 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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,
)

Expand Down Expand Up @@ -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,
)

Expand All @@ -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,
)

Expand Down Expand Up @@ -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,
)

Expand All @@ -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,
)

Expand Down Expand Up @@ -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,
)

Expand Down
7 changes: 4 additions & 3 deletions tests/test_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__":
Expand Down
22 changes: 15 additions & 7 deletions tests/test_governance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.",
Expand Down Expand Up @@ -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.",
]
)
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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}",
]
)
Expand All @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
7 changes: 4 additions & 3 deletions tests/test_init_plan.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__":
Expand Down