Block stale repair memory reads
Authored by
mfwolffe <wolffemf@dukes.jmu.edu>
- SHA
cce89ea9ab7aaa303b8d5ac906bb75fdc9bbe084- Parents
-
9eed110 - Tree
56e87db
cce89ea
cce89ea9ab7aaa303b8d5ac906bb75fdc9bbe0849eed110
56e87db| Status | File | + | - |
|---|---|---|---|
| M |
src/loader/runtime/hooks.py
|
19 | 1 |
| M |
tests/test_permissions.py
|
58 | 0 |
src/loader/runtime/hooks.pymodified@@ -451,7 +451,8 @@ class RelativePathContextHook(BaseToolHook): | ||
| 451 | 451 | return str(candidate.parent) |
| 452 | 452 | |
| 453 | 453 | |
| 454 | -_OBSERVATION_TOOLS = frozenset({"read", "glob", "grep", "bash"}) | |
| 454 | +_DURABLE_MEMORY_READ_TOOLS = frozenset({"notepad_read", "project_memory_read"}) | |
| 455 | +_OBSERVATION_TOOLS = frozenset({"read", "glob", "grep", "bash"}) | _DURABLE_MEMORY_READ_TOOLS | |
| 455 | 456 | _MUTATION_TOOLS = frozenset({"write", "edit", "patch", "bash"}) |
| 456 | 457 | _READ_ONLY_BASH_PREFIXES = frozenset( |
| 457 | 458 | {"ls", "pwd", "find", "stat", "cat", "head", "tail", "rg", "grep"} |
@@ -729,6 +730,23 @@ class ActiveRepairScopeHook(BaseToolHook): | ||
| 729 | 730 | if repair is None: |
| 730 | 731 | return HookResult() |
| 731 | 732 | |
| 733 | + if context.tool_call.name in _DURABLE_MEMORY_READ_TOOLS: | |
| 734 | + allowed_preview = ", ".join(f"`{path}`" for path in repair.allowed_paths[:3]) | |
| 735 | + if len(repair.allowed_paths) > 3: | |
| 736 | + allowed_preview += ", ..." | |
| 737 | + target_preview = allowed_preview or f"`{repair.artifact_path}`" | |
| 738 | + return HookResult( | |
| 739 | + decision=HookDecision.DENY, | |
| 740 | + message=( | |
| 741 | + "[Blocked - active repair scope: verification already identified " | |
| 742 | + "concrete repair targets, and durable memory may be stale.] " | |
| 743 | + "Suggestion: trust the active verifier/DoD over notepad or project " | |
| 744 | + f"memory while repairing. Continue with {target_preview}; do not use " | |
| 745 | + "durable notes as completion evidence until verification passes." | |
| 746 | + ), | |
| 747 | + terminal_state="blocked", | |
| 748 | + ) | |
| 749 | + | |
| 732 | 750 | observed_paths = _extract_observation_paths(context.tool_call) |
| 733 | 751 | if not observed_paths: |
| 734 | 752 | return HookResult() |
tests/test_permissions.pymodified@@ -678,6 +678,64 @@ async def test_active_repair_scope_hook_blocks_reference_reads_while_fixing( | ||
| 678 | 678 | assert str(repair_target) in result.message |
| 679 | 679 | |
| 680 | 680 | |
| 681 | +@pytest.mark.asyncio | |
| 682 | +async def test_active_repair_scope_hook_blocks_stale_memory_reads_while_fixing( | |
| 683 | + temp_dir: Path, | |
| 684 | +) -> None: | |
| 685 | + registry = create_default_registry(temp_dir) | |
| 686 | + policy = build_permission_policy( | |
| 687 | + active_mode=PermissionMode.WORKSPACE_WRITE, | |
| 688 | + workspace_root=temp_dir, | |
| 689 | + tool_requirements=registry.get_tool_requirements(), | |
| 690 | + ) | |
| 691 | + dod_store = DefinitionOfDoneStore(temp_dir) | |
| 692 | + dod = create_definition_of_done("Repair the active artifact set") | |
| 693 | + dod.status = "fixing" | |
| 694 | + dod_path = dod_store.save(dod) | |
| 695 | + repair_target = temp_dir / "guide" / "chapters" / "05-load-balancing.html" | |
| 696 | + session = FakeSession( | |
| 697 | + active_dod_path=str(dod_path), | |
| 698 | + messages=[ | |
| 699 | + Message( | |
| 700 | + role=Role.USER, | |
| 701 | + content=( | |
| 702 | + "[DEFINITION OF DONE CHECK STILL FAILING]\n" | |
| 703 | + "HTML guide content quality issues:\n" | |
| 704 | + "Repair focus:\n" | |
| 705 | + f"- {repair_target}: thin content (1500 text chars, expected at least 1758)\n" | |
| 706 | + f"- Immediate next step: edit `{repair_target}`.\n" | |
| 707 | + ), | |
| 708 | + ) | |
| 709 | + ], | |
| 710 | + ) | |
| 711 | + hook = ActiveRepairScopeHook( | |
| 712 | + dod_store=dod_store, | |
| 713 | + project_root=temp_dir, | |
| 714 | + session=session, | |
| 715 | + ) | |
| 716 | + | |
| 717 | + result = await hook.pre_tool_use( | |
| 718 | + HookContext( | |
| 719 | + tool_call=ToolCall( | |
| 720 | + id="memory-1", | |
| 721 | + name="notepad_read", | |
| 722 | + arguments={}, | |
| 723 | + ), | |
| 724 | + tool=registry.get("notepad_read"), | |
| 725 | + registry=registry, | |
| 726 | + permission_policy=policy, | |
| 727 | + source="native", | |
| 728 | + ) | |
| 729 | + ) | |
| 730 | + | |
| 731 | + assert result.decision == HookDecision.DENY | |
| 732 | + assert result.terminal_state == "blocked" | |
| 733 | + assert result.message is not None | |
| 734 | + assert "durable memory may be stale" in result.message | |
| 735 | + assert "trust the active verifier/DoD" in result.message | |
| 736 | + assert str(repair_target) in result.message | |
| 737 | + | |
| 738 | + | |
| 681 | 739 | @pytest.mark.asyncio |
| 682 | 740 | async def test_active_repair_scope_hook_allows_reads_inside_active_artifact_set( |
| 683 | 741 | temp_dir: Path, |