diff --git a/CHANGELOG.md b/CHANGELOG.md index 83490a1..d551cf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ This project has a published GitHub Release line, but no stable support or API g ### Fixed +- Scoped governance finding suppression to same-line negation or approval cues so adjacent safe guidance no longer hides unrelated risky instructions. - Reject symlinked supported instruction files and harden `init --write` temporary and backup paths against symlink escapes. - Report non-UTF-8 supported instruction files as `AIRK-SYS001` findings instead of silently skipping governance analysis. - Updated generated `AGENTS.md` baseline content so `init --write` no longer creates instructions that fail the current governance scope or authority check. diff --git a/docs/RULES.md b/docs/RULES.md index 1f03ed2..8187717 100644 --- a/docs/RULES.md +++ b/docs/RULES.md @@ -37,6 +37,8 @@ Future rule-order changes must remain deterministic, documented, fixture-backed, ## Rule reference +Line-based suppression is intentionally same-line scoped. Negative or approval language can suppress a trigger on the same line, but nearby guidance does not suppress a separate risky instruction line. This favors visible findings over hiding independent risky instructions. + ### AIRK-SYS001 — Unreadable instruction file Flags supported instruction files that cannot be analyzed as UTF-8. diff --git a/src/agent_rules_kit/governance.py b/src/agent_rules_kit/governance.py index 8a621f0..306e783 100644 --- a/src/agent_rules_kit/governance.py +++ b/src/agent_rules_kit/governance.py @@ -303,9 +303,9 @@ def make_context_aware_predicate( trigger_patterns: tuple[Pattern[str], ...], negation_patterns: tuple[Pattern[str], ...], *, - context_window: int = 2, + context_window: int = 0, ) -> ContextPredicate: - """Return a predicate that evaluates triggers on one line and negations nearby.""" + """Return a predicate that evaluates triggers with same-line negation scope.""" def predicate(lines: Sequence[str], index: int) -> bool: if index < 0 or index >= len(lines): diff --git a/tests/test_governance.py b/tests/test_governance.py index de25e01..a3ceb36 100644 --- a/tests/test_governance.py +++ b/tests/test_governance.py @@ -182,7 +182,7 @@ def test_ignores_negative_guidance_about_review_ci_bypass(self) -> None: self.assertEqual(findings, ()) - def test_ignores_adjacent_negative_guidance_about_review_ci_bypass(self) -> None: + def test_reports_adjacent_review_ci_bypass_despite_negative_guidance(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: repository = Path(temporary_directory) (repository / "AGENTS.md").write_text( @@ -203,8 +203,9 @@ def test_ignores_adjacent_negative_guidance_about_review_ci_bypass(self) -> None instruction_files = discover_instruction_files(repository) findings = find_review_ci_bypass_findings(repository, instruction_files) - self.assertEqual(findings, ()) - + self.assertEqual([finding.rule_id for finding in findings], ["AIRK-GOV003"]) + self.assertEqual([finding.line for finding in findings], [6]) + self.assertEqual([finding.evidence for finding in findings], ["- Commit directly to main."]) def test_reports_unsafe_command_execution_guidance(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: @@ -342,7 +343,7 @@ def test_reports_gold_runtime_api_phrase_variants(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_ignores_adjacent_negative_runtime_api_requirement_guidance(self) -> None: + def test_reports_adjacent_runtime_api_requirement_despite_negative_guidance(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: repository = Path(temporary_directory) (repository / "AGENTS.md").write_text( @@ -363,8 +364,12 @@ def test_ignores_adjacent_negative_runtime_api_requirement_guidance(self) -> Non instruction_files = discover_instruction_files(repository) findings = find_runtime_network_llm_dependency_findings(repository, instruction_files) - self.assertEqual(findings, ()) - + self.assertEqual([finding.rule_id for finding in findings], ["AIRK-GOV005"]) + self.assertEqual([finding.line for finding in findings], [6]) + self.assertEqual( + [finding.evidence for finding in findings], + ["- This check requires the OpenAI API."], + ) def test_ignores_safe_or_human_reviewed_network_llm_guidance(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: @@ -394,7 +399,7 @@ def test_ignores_safe_or_human_reviewed_network_llm_guidance(self) -> None: self.assertEqual(findings, ()) - def test_ignores_adjacent_negative_guidance_about_runtime_network_llm(self) -> None: + def test_reports_adjacent_runtime_network_llm_despite_negative_guidance(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: repository = Path(temporary_directory) (repository / "AGENTS.md").write_text( @@ -415,8 +420,12 @@ def test_ignores_adjacent_negative_guidance_about_runtime_network_llm(self) -> N instruction_files = discover_instruction_files(repository) findings = find_runtime_network_llm_dependency_findings(repository, instruction_files) - self.assertEqual(findings, ()) - + self.assertEqual([finding.rule_id for finding in findings], ["AIRK-GOV005"]) + self.assertEqual([finding.line for finding in findings], [6]) + self.assertEqual( + [finding.evidence for finding in findings], + ["- The check command must call an LLM API to audit the repository."], + ) def test_reports_missing_secret_handling_boundary(self) -> None: with tempfile.TemporaryDirectory() as temporary_directory: