| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -12,6 +12,7 @@ | |||
| 12 | 12 | from pathlib import Path | |
| 13 | 13 | ||
| 14 | 14 | from lib import config, repo_layout | |
| 15 | + from lib.core.domain import FileChange, Target | ||
| 15 | 16 | ||
| 16 | 17 | ||
| 17 | 18 | class PolicyContext: | |
@@ -32,6 +33,17 @@ def __init__(self, cwd: str, anchor_path: str = "", session_id: str = ""): | |||
| 32 | 33 | def cwd(self) -> str: | |
| 33 | 34 | return self._cwd | |
| 34 | 35 | ||
| 36 | + def for_target(self, target: Target) -> PolicyContext: | ||
| 37 | + """Return the policy view anchored to the target being evaluated. | ||
| 38 | + | ||
| 39 | + A single ``apply_patch`` can contain files below an aggregate workspace or even span | ||
| 40 | + repositories. Repo-scoped rules must therefore resolve ownership from each file target, | ||
| 41 | + not from the session cwd or one call-level anchor. | ||
| 42 | + """ | ||
| 43 | + if isinstance(target, FileChange): | ||
| 44 | + return PolicyContext(self._cwd, anchor_path=target.path, session_id=self.session_id) | ||
| 45 | + return self | ||
| 46 | + | ||
| 35 | 47 | @property | |
| 36 | 48 | def anchor_abspath(self) -> str: | |
| 37 | 49 | """被编辑文件的绝对路径(edit 族规则做 gitignore / 路径判断用)。""" | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -86,18 +86,19 @@ def evaluate(change: Change, ctx: PolicyContext, rules: list[Rule]) -> Decision: | |||
| 86 | 86 | findings: list[Finding] = [] | |
| 87 | 87 | ||
| 88 | 88 | for target in change.targets: | |
| 89 | + target_ctx = ctx.for_target(target) if hasattr(ctx, "for_target") else ctx | ||
| 89 | 90 | kind = getattr(target, "kind", None) | |
| 90 | - applicable = [r for r in rules if r.target_kind == kind and _safe_applies(r, target, ctx)] | ||
| 91 | + applicable = [r for r in rules if r.target_kind == kind and _safe_applies(r, target, target_ctx)] | ||
| 91 | 92 | if not applicable: | |
| 92 | 93 | continue | |
| 93 | 94 | # content-aware 规则命中 → 惰性解析(读盘+套 edit 得"改后全文"再解析 imports/decls) | |
| 94 | 95 | if isinstance(target, FileChange) and any(r.needs_content for r in applicable): | |
| 95 | 96 | try: | |
| 96 | - enrich(target, ctx) | ||
| 97 | + enrich(target, target_ctx) | ||
| 97 | 98 | except Exception: | |
| 98 | 99 | pass # 解析失败 → 不产 content findings(fail-open) | |
| 99 | 100 | for r in applicable: | |
| 100 | - findings.extend(_safe_check(r, target, ctx)) | ||
| 101 | + findings.extend(_safe_check(r, target, target_ctx)) | ||
| 101 | 102 | ||
| 102 | 103 | # mutation 级规则:不看具体 target,直接吃 Change | |
| 103 | 104 | for r in rules: | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -1,5 +1,5 @@ | |||
| 1 | 1 | #!/usr/bin/env python3 | |
| 2 | - """PreToolUse (Edit/Write/MultiEdit/NotebookEdit): 编辑侧策略引擎入口。 | ||
| 2 | + """PreToolUse (Edit/Write/MultiEdit/NotebookEdit/apply_patch): 编辑侧策略引擎入口。 | ||
| 3 | 3 | ||
| 4 | 4 | 把这次文件改动投影成 `FileChange`,跑 FILE_CHANGE 规则(checkout 占有、分支失活、 | |
| 5 | 5 | requirements.txt、层级依赖 lint),deny 则在落盘前拦下。 | |
@@ -16,14 +16,11 @@ | |||
| 16 | 16 | from lib.core import engine # noqa: E402 | |
| 17 | 17 | from lib.core.context import PolicyContext # noqa: E402 | |
| 18 | 18 | ||
| 19 | - _FILE_TOOLS = ("Edit", "Write", "MultiEdit", "NotebookEdit") | ||
| 20 | - | ||
| 21 | - | ||
| 22 | 19 | def decide(inp: hook_io.HookInput) -> str | None: | |
| 23 | - if not inp.is_tool(*_FILE_TOOLS): | ||
| 24 | - return None | ||
| 25 | 20 | change = engine.project(inp) | |
| 26 | - ctx = PolicyContext(inp.cwd, anchor_path=inp.file_path, session_id=inp.session_id) | ||
| 21 | + if not change.targets: | ||
| 22 | + return None | ||
| 23 | + ctx = PolicyContext(inp.cwd, session_id=inp.session_id) | ||
| 27 | 24 | decision = engine.evaluate(change, ctx, rules.REGISTRY) | |
| 28 | 25 | if not decision.blocked: | |
| 29 | 26 | return None | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -217,6 +217,34 @@ def test_edit_owner_guard(): | |||
| 217 | 217 | outside = _hook_input("Edit", {"session_id": "sess-B", "cwd": R, "tool_input": {"file_path": f"{R}/x.py"}}) | |
| 218 | 218 | assert guard.decide(outside) is None | |
| 219 | 219 | ||
| 220 | + def test_apply_patch_owner_guard_uses_target_path(): | ||
| 221 | + """Codex ``apply_patch`` must enter the edit policy and anchor owner lookup to the patched | ||
| 222 | + file. Its session cwd commonly remains at the aggregate workspace root, which is not a repo. | ||
| 223 | + """ | ||
| 224 | + guard = _load_hook("pretool_policy_edit") | ||
| 225 | + from lib.context import session as session_lock | ||
| 226 | + R = "/tmp/dlut_patch_owner" | ||
| 227 | + repo = f"{R}/repo" | ||
| 228 | + shutil.rmtree(R, ignore_errors=True); os.makedirs(repo, exist_ok=True) | ||
| 229 | + _git(repo, "init", "-q") | ||
| 230 | + fp = f"{repo}/a.py" | ||
| 231 | + Path(fp).write_text("old\n") | ||
| 232 | + patch = f"*** Begin Patch\n*** Update File: {fp}\n@@\n-old\n+new\n*** End Patch\n" | ||
| 233 | + | ||
| 234 | + # The hook's freeform-tool normalization stores the patch under ``input``. | ||
| 235 | + inp_a = _hook_input("apply_patch", { | ||
| 236 | + "session_id": "sess-A", "cwd": R, "tool_input": {"input": patch}, | ||
| 237 | + }) | ||
| 238 | + assert guard.decide(inp_a) is None | ||
| 239 | + assert session_lock.read(repo)["session_id"] == "sess-A" | ||
| 240 | + | ||
| 241 | + session_lock.acquire(repo, "sess-A", "feat/x", pid=os.getpid()) | ||
| 242 | + inp_b = _hook_input("apply_patch", { | ||
| 243 | + "session_id": "sess-B", "cwd": R, "tool_input": {"input": patch}, | ||
| 244 | + }) | ||
| 245 | + reason = guard.decide(inp_b) | ||
| 246 | + assert reason and "worktree" in reason and "owner.lock" in reason | ||
| 247 | + | ||
| 220 | 248 | def test_branch_merged_guard_uses_file_path(): | |
| 221 | 249 | """INACTIVE 分支编辑拦截按 file_path 解析 repo——session cwd 在 workspace 根时 | |
| 222 | 250 | cwd-based 查找为 None,guard 此前静默失效。Also exercises the gate's SHA validation: the | |
| Back | FazBrowse Home | New Git URL |
0 commit comments