FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

Merge pull request #97 from qiankunli/fix/codex-apply-patch-owner-lock · compforge/devloop@af6ca1e · GitHub

Commit af6ca1e

Browse files
authored
Merge pull request #97 from qiankunli/fix/codex-apply-patch-owner-lock
fix(codex): restore apply_patch owner guard
2 parents 59a666a + ec8edf5 commit af6ca1e

4 files changed

Lines changed: 48 additions & 10 deletions

File tree

‎devloop/hooks/lib/core/context.py‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from pathlib import Path
1313

1414
from lib import config, repo_layout
15+
from lib.core.domain import FileChange, Target
1516

1617

1718
class PolicyContext:
@@ -32,6 +33,17 @@ def __init__(self, cwd: str, anchor_path: str = "", session_id: str = ""):
3233
def cwd(self) -> str:
3334
return self._cwd
3435

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+
3547
@property
3648
def anchor_abspath(self) -> str:
3749
"""被编辑文件的绝对路径(edit 族规则做 gitignore / 路径判断用)。"""

‎devloop/hooks/lib/core/engine.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,18 +86,19 @@ def evaluate(change: Change, ctx: PolicyContext, rules: list[Rule]) -> Decision:
8686
findings: list[Finding] = []
8787

8888
for target in change.targets:
89+
target_ctx = ctx.for_target(target) if hasattr(ctx, "for_target") else ctx
8990
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)]
9192
if not applicable:
9293
continue
9394
# content-aware 规则命中 → 惰性解析(读盘+套 edit 得"改后全文"再解析 imports/decls)
9495
if isinstance(target, FileChange) and any(r.needs_content for r in applicable):
9596
try:
96-
enrich(target, ctx)
97+
enrich(target, target_ctx)
9798
except Exception:
9899
pass # 解析失败 → 不产 content findings(fail-open)
99100
for r in applicable:
100-
findings.extend(_safe_check(r, target, ctx))
101+
findings.extend(_safe_check(r, target, target_ctx))
101102

102103
# mutation 级规则:不看具体 target,直接吃 Change
103104
for r in rules:

‎devloop/hooks/pretool_policy_edit.py‎

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python3
2-
"""PreToolUse (Edit/Write/MultiEdit/NotebookEdit): 编辑侧策略引擎入口。
2+
"""PreToolUse (Edit/Write/MultiEdit/NotebookEdit/apply_patch): 编辑侧策略引擎入口。
33
44
把这次文件改动投影成 `FileChange`,跑 FILE_CHANGE 规则(checkout 占有、分支失活、
55
requirements.txt、层级依赖 lint),deny 则在落盘前拦下。
@@ -16,14 +16,11 @@
1616
from lib.core import engine # noqa: E402
1717
from lib.core.context import PolicyContext # noqa: E402
1818

19-
_FILE_TOOLS = ("Edit", "Write", "MultiEdit", "NotebookEdit")
20-
21-
2219
def decide(inp: hook_io.HookInput) -> str | None:
23-
if not inp.is_tool(*_FILE_TOOLS):
24-
return None
2520
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)
2724
decision = engine.evaluate(change, ctx, rules.REGISTRY)
2825
if not decision.blocked:
2926
return None

‎devloop/tests/test_guards.py‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,34 @@ def test_edit_owner_guard():
217217
outside = _hook_input("Edit", {"session_id": "sess-B", "cwd": R, "tool_input": {"file_path": f"{R}/x.py"}})
218218
assert guard.decide(outside) is None
219219

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+
220248
def test_branch_merged_guard_uses_file_path():
221249
"""INACTIVE 分支编辑拦截按 file_path 解析 repo——session cwd 在 workspace 根时
222250
cwd-based 查找为 None,guard 此前静默失效。Also exercises the gate's SHA validation: the

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL