fix(skills): preserve review marks across contexts

This commit is contained in:
Jony
2026-08-24 23:33:51 -07:00
committed by Teknium
parent 34041faea8
commit 335c60ecdd
2 changed files with 85 additions and 9 deletions
+56
View File
@@ -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
+29 -9
View File
@@ -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.