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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions docs/RULES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions src/agent_rules_kit/governance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
27 changes: 18 additions & 9 deletions tests/test_governance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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:
Expand Down Expand Up @@ -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(
Expand All @@ -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:
Expand Down Expand Up @@ -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(
Expand All @@ -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:
Expand Down