4.7 KiB
ACP Zed Pre-Edit Approval Diffs Implementation Plan
For Hermes: Use subagent-driven-development skill to implement this plan task-by-task.
Goal: Gate file mutations in ACP/Zed behind explicit pre-edit approval with a structured diff, similar to Codex/Kimi edit review behavior.
Architecture: Hermes already renders edit diffs after tools run. This PR adds a pre-mutation permission gate for file mutation tools. Intercept write_file, patch, and eventually skill_manage before they mutate disk; compute proposed old/new content; send ACP session/request_permission with kind="edit" and diff content; only execute the mutation after approval. Rejections return a clear tool result and leave files unchanged.
Tech Stack: Python, ACP request_permission, FileEditToolCallContent / acp.tool_diff_content, Hermes file tools, pytest with temp files.
Task 1: Confirm current ACP diff/permission schema
Run:
/home/nour/.hermes/hermes-agent/venv/bin/python - <<'PY'
from acp.schema import RequestPermissionRequest, ToolCallUpdate
import acp, inspect
print(RequestPermissionRequest.model_fields)
print(ToolCallUpdate.model_fields)
print(inspect.signature(acp.tool_diff_content))
PY
Record actual field names. Do not rely on stale examples.
Task 2: Add denied-write test
Objective: A rejected write_file must not mutate disk.
Files:
- Create/modify:
tests/acp/test_edit_approval.py
Test shape:
def test_write_file_rejected_by_acp_permission_does_not_mutate(tmp_path):
path = tmp_path / "demo.txt"
path.write_text("old")
# Install fake ACP edit approval callback returning reject_once.
# Invoke the same interception function that the terminal/tool path will call.
result = maybe_gate_file_edit(
tool_name="write_file",
args={"path": str(path), "content": "new"},
approval_requester=fake_reject,
)
assert path.read_text() == "old"
assert "rejected" in result.lower()
The exact function name will be created in Task 4.
Task 3: Add approved-write test
Objective: Approved writes proceed and include diff content in permission request.
Assert:
- fake requester received tool call
kind == "edit" - content includes diff block for
demo.txt - after approval, file content is changed
Task 4: Implement edit proposal computation
Files:
- Create:
acp_adapter/edit_approval.py
Add pure helpers first:
@dataclass
class EditProposal:
path: str
old_text: str | None
new_text: str
title: str
def proposal_for_write_file(args: dict[str, Any]) -> EditProposal:
path = str(args["path"])
old_text = Path(path).read_text(encoding="utf-8") if Path(path).exists() else None
new_text = str(args.get("content", ""))
return EditProposal(path=path, old_text=old_text, new_text=new_text, title=f"Edit {path}")
For patch, start with replace-mode only. V4A/multi-file patches can be a second task or second PR if too risky.
Task 5: Implement ACP permission requester
Files:
- Modify:
acp_adapter/permissions.pyor newacp_adapter/edit_approval.py
Build request with:
acp.tool_diff_content(path=proposal.path, old_text=proposal.old_text, new_text=proposal.new_text)
Options:
- allow once
- reject once
- optionally allow always/reject always only after policy storage exists
Default deny on exception/cancel/timeout.
Task 6: Intercept file mutation tools before execution
Objective: Ensure mutation cannot happen before approval.
Files:
- Likely modify:
model_tools.pyoracp_adapter/server.pysession-context tool wrapper
Do not bury this inside post-execution acp_adapter/events.py; that is too late.
Preferred design:
- set an ACP session contextvar around
agent.run_conversation(...) - in the central tool execution path, before dispatching
write_file/patch, call the ACP edit approval gate if contextvar exists - if rejected, return a normal tool result string like
{"success": false, "error": "Edit rejected by user"} - if approved, continue to original tool implementation
Task 7: Expand patch coverage
Add tests for:
patchreplace mode approved/rejected- creating a new file via
write_file - missing old string -> should fail before approval or return normal patch error, but must not mutate
- permission requester exception -> deny and no mutation
Task 8: Verification
Run:
scripts/run_tests.sh tests/acp/test_edit_approval.py tests/acp/test_events.py tests/acp/test_tools.py -q
Then run manual Zed verification:
- Ask Hermes ACP to edit a small file.
- Confirm Zed shows a diff before mutation.
- Reject and verify file unchanged.
- Approve and verify file changed.
Do not merge without manual reject-path verification.