fix(skills): preserve review marks across contexts
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
import json
|
||||
from contextlib import contextmanager
|
||||
from contextvars import copy_context
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -914,6 +915,61 @@ class TestCuratorConsolidationDeleteGuard:
|
||||
# Skill must remain active on disk — fail closed, no archive.
|
||||
assert (skills_root / "active-skill").exists()
|
||||
|
||||
def test_background_review_read_survives_copied_tool_contexts(
|
||||
self, tmp_path, monkeypatch
|
||||
):
|
||||
"""A view in one tool worker authorizes a patch in the next worker."""
|
||||
from tools.skills_tool import skill_view
|
||||
from tools.skill_manager_tool import _reset_background_review_read_marks
|
||||
|
||||
_reset_background_review_read_marks()
|
||||
with _curator_pass(tmp_path, monkeypatch=monkeypatch):
|
||||
_create_curator_skill("reviewed", _skill_content("reviewed"))
|
||||
|
||||
viewed = copy_context().run(skill_view, "reviewed")
|
||||
assert json.loads(viewed)["success"] is True
|
||||
|
||||
patched = copy_context().run(
|
||||
skill_manage,
|
||||
action="patch",
|
||||
name="reviewed",
|
||||
old_string="Step 1: Do the thing.",
|
||||
new_string="Step 1: Do the thing safely.",
|
||||
)
|
||||
assert json.loads(patched)["success"] is True
|
||||
|
||||
_reset_background_review_read_marks()
|
||||
|
||||
def test_background_review_read_marks_stay_isolated_between_reviews(
|
||||
self, tmp_path, monkeypatch
|
||||
):
|
||||
"""Copied tool contexts share only their own review's read marks."""
|
||||
from tools.skills_tool import skill_view
|
||||
from tools.skill_manager_tool import _reset_background_review_read_marks
|
||||
|
||||
_reset_background_review_read_marks()
|
||||
with _curator_pass(tmp_path, monkeypatch=monkeypatch):
|
||||
_create_curator_skill("reviewed", _skill_content("reviewed"))
|
||||
|
||||
first_review = copy_context()
|
||||
_reset_background_review_read_marks()
|
||||
second_review = copy_context()
|
||||
|
||||
viewed = first_review.run(skill_view, "reviewed")
|
||||
assert json.loads(viewed)["success"] is True
|
||||
|
||||
blocked = second_review.run(
|
||||
skill_manage,
|
||||
action="patch",
|
||||
name="reviewed",
|
||||
old_string="Step 1: Do the thing.",
|
||||
new_string="Step 1: Do the thing safely.",
|
||||
)
|
||||
result = json.loads(blocked)
|
||||
assert result["success"] is False
|
||||
assert result.get("_read_before_write_required") is True
|
||||
|
||||
_reset_background_review_read_marks()
|
||||
|
||||
def test_background_review_support_file_overwrite_requires_that_file_read(self, tmp_path, monkeypatch):
|
||||
from tools.skills_tool import skill_view
|
||||
|
||||
@@ -36,6 +36,7 @@ import json
|
||||
import logging
|
||||
import re
|
||||
import shutil
|
||||
import threading
|
||||
import contextvars as _ctxvars
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
@@ -52,9 +53,25 @@ from agent.skill_utils import (
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_background_review_read_paths: "_ctxvars.ContextVar[frozenset[str]]" = _ctxvars.ContextVar(
|
||||
"background_review_read_paths", default=frozenset()
|
||||
)
|
||||
class _BackgroundReviewReadMarks:
|
||||
"""Read marks shared by copied tool contexts within one review run."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._lock = threading.Lock()
|
||||
self._paths: set[str] = set()
|
||||
|
||||
def add(self, path: str) -> None:
|
||||
with self._lock:
|
||||
self._paths.add(path)
|
||||
|
||||
def contains(self, path: str) -> bool:
|
||||
with self._lock:
|
||||
return path in self._paths
|
||||
|
||||
|
||||
_background_review_read_paths: (
|
||||
"_ctxvars.ContextVar[Optional[_BackgroundReviewReadMarks]]"
|
||||
) = _ctxvars.ContextVar("background_review_read_paths", default=None)
|
||||
|
||||
|
||||
def mark_background_review_skill_read(path: Path) -> None:
|
||||
@@ -77,9 +94,11 @@ def mark_background_review_skill_read(path: Path) -> None:
|
||||
resolved = str(path.resolve())
|
||||
except Exception:
|
||||
resolved = str(path)
|
||||
current = set(_background_review_read_paths.get())
|
||||
current.add(resolved)
|
||||
_background_review_read_paths.set(frozenset(current))
|
||||
marks = _background_review_read_paths.get()
|
||||
if marks is None:
|
||||
marks = _BackgroundReviewReadMarks()
|
||||
_background_review_read_paths.set(marks)
|
||||
marks.add(resolved)
|
||||
|
||||
|
||||
def _background_review_has_read(path: Path) -> bool:
|
||||
@@ -87,12 +106,13 @@ def _background_review_has_read(path: Path) -> bool:
|
||||
resolved = str(path.resolve())
|
||||
except Exception:
|
||||
resolved = str(path)
|
||||
return resolved in _background_review_read_paths.get()
|
||||
marks = _background_review_read_paths.get()
|
||||
return marks is not None and marks.contains(resolved)
|
||||
|
||||
|
||||
def _reset_background_review_read_marks() -> None:
|
||||
"""Test helper: clear read-before-write marks for the current context."""
|
||||
_background_review_read_paths.set(frozenset())
|
||||
"""Start a fresh, isolated read set for the current review context."""
|
||||
_background_review_read_paths.set(_BackgroundReviewReadMarks())
|
||||
|
||||
# Import security scanner — external hub installs always get scanned;
|
||||
# agent-created skills only get scanned when skills.guard_agent_created is on.
|
||||
|
||||
Reference in New Issue
Block a user