feat(execution-plane): Execution Contract 与替身执行器(改造第 4 步)

- modules/execution_plane:ExecutionBackend 协议(E1-E4 首版接口面)+
  FakeExecutionBackend 内存替身(零真实副作用)
- 接入:MainTerminal.execution_backend 属性(默认 None=真实链路),
  handle_tool_call 的 run_command 前台/后台、write_file、edit_file 四分支注入
- 替身路径自动跳过浅版本备份(不触真实磁盘)
- 契约文档 docs/execution_contract.md(本地文档目录,不入库):
  E1-E10 接口面归纳、权限分工、Host/Docker 后端映射
- 验收:ExecutionPlaneFakeBackendTest 全绿(替身接收 E1-E4 调用、
  结果结构同构消费、零磁盘写入、默认路径回归)
This commit is contained in:
JOJO 2026-09-07 23:00:31 +08:00
parent d9bd599c1c
commit 27aeed70ca
5 changed files with 251 additions and 14 deletions

View File

@ -142,6 +142,12 @@ class MainTerminal(MainTerminalCommandMixin, MainTerminalContextMixin, MainTermi
self.terminal_ops.attach_terminal_manager(self.terminal_manager)
self._apply_container_session(container_session)
# Execution Plane 后端docs/execution_contract.mdNone = 现有
# Host/Docker 真实链路;注入 ExecutionBackend 实现(如替身执行器)后,
# handle_tool_call 的 E1-E4 分支run_command/后台/write_file/edit_file
# 改走该后端。测试或未来远端执行装配点注入。
self.execution_backend = None
self.todo_manager = TodoManager(self.context_manager)
self.sub_agent_manager = SubAgentManager(
project_path=self.project_path,

View File

@ -1631,6 +1631,12 @@ class MainTerminalToolsExecutionMixin:
append_flag = bool(arguments.get("append", False))
if not path:
result = {"success": False, "error": tr("tools_exec.missing_file_path")}
else:
backend = getattr(self, "execution_backend", None)
if backend is not None:
# Execution Plane 后端(替身/远端):不触真实磁盘,跳过浅备份
mode = "a" if append_flag else "w"
result = backend.write_file(path, content, mode=mode)
else:
# 在写入前先备份当前内容(浅备份)。
self._track_shallow_versioning(path)
@ -1659,6 +1665,11 @@ class MainTerminalToolsExecutionMixin:
result = {"success": False, "error": tr("tools_exec.missing_file_path")}
elif not isinstance(replacements, list) or not replacements:
result = {"success": False, "error": tr("tools_exec.missing_replacements")}
else:
backend = getattr(self, "execution_backend", None)
if backend is not None:
# Execution Plane 后端(替身/远端):不触真实磁盘,跳过浅备份
result = backend.edit_file(path, replacements)
else:
# 在替换前先备份当前内容(浅备份)。
self._track_shallow_versioning(path)
@ -1909,7 +1920,18 @@ class MainTerminalToolsExecutionMixin:
}
else:
bg_manager = getattr(self, "background_command_manager", None)
if not bg_manager:
backend = getattr(self, "execution_backend", None)
if backend is not None:
# Execution Plane 后端(替身/远端):不走真实后台命令线程
result = backend.run_command_background(
arguments["command"],
timeout=float(timeout_value),
conversation_id=getattr(self.context_manager, "current_conversation_id", None),
wait_seconds=5.0,
network_permission=network_permission,
sandbox_write_access=sandbox_write_access,
)
elif not bg_manager:
result = {"success": False, "error": tr("tools_exec.background_manager_unavailable")}
else:
result = bg_manager.create_background_command(
@ -1932,6 +1954,16 @@ class MainTerminalToolsExecutionMixin:
"success": False,
"error": tr("tools_exec.fg_timeout_max")
}
else:
backend = getattr(self, "execution_backend", None)
if backend is not None:
# Execution Plane 后端(替身/远端):不起真实子进程
result = await backend.run_command(
arguments["command"],
timeout=float(timeout_value),
sandbox_write_access=sandbox_write_access,
network_permission=network_permission,
)
else:
result = await self.terminal_ops.run_command(
arguments["command"],

View File

@ -0,0 +1,13 @@
"""Execution PlaneRuntime ↔ 执行环境分层docs/execution_contract.md
- base.ExecutionBackend执行环境后端协议E1-E4 首版接口面
- fake.FakeExecutionBackend内存替身执行器Runtime 独立测试用
接入方式MainTerminal/WebTerminal 实例的 ``execution_backend`` 属性
默认 None = 现有 Host/Docker 真实链路注入实现后工具编排层
handle_tool_call E1-E4 分支改走该后端
"""
from modules.execution_plane.base import ExecutionBackend
from modules.execution_plane.fake import FakeExecutionBackend
__all__ = ["ExecutionBackend", "FakeExecutionBackend"]

View File

@ -0,0 +1,63 @@
"""Execution Plane 契约Runtime ↔ 执行环境分层接口docs/execution_contract.md §3
定位工具编排层core/main_terminal_parts/tools_execution.py通过本接口触达
执行环境现有 Host/Docker 实现仍由 terminal_ops/file_manager 等承载默认路径
本接口是替身执行器 / 未来远端执行后端的统一接入点
首版接口面E1-E4归纳自现状调用点未发明新能力
- E1 run_command前台命令
- E2 run_command_background后台命令登记
- E3 write_file / E4 edit_file文件写
结果结构与现有真实后端保持一致调用方按同一结构消费
- run_command: {success, status, output, return_code, truncated, elapsed_ms}
status {"completed", "timeout", "error", "cancelled"}
- run_command_background: {success, command_id, status}status="running_background"
- write_file/edit_file: {success, path, original_file, new_file}
original_file/new_file 为写前/后全文编辑摘要链路依赖
"""
from __future__ import annotations
from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
@runtime_checkable
class ExecutionBackend(Protocol):
"""执行环境后端协议。实现方FakeExecutionBackend替身、未来 Host/Docker 适配器。
注意本协议只承诺执行语义不承诺安全边界权限裁决permission mode
路径授权_validate_path命令校验_validate_command Runtime 编排层完成
与本接口正交契约 §4
"""
async def run_command(
self,
command: str,
*,
timeout: float,
sandbox_write_access: bool,
network_permission: str,
) -> Dict[str, Any]:
"""E1 前台命令执行。"""
...
def run_command_background(
self,
command: str,
*,
timeout: float,
conversation_id: Optional[str],
wait_seconds: float,
network_permission: str,
sandbox_write_access: bool,
) -> Dict[str, Any]:
"""E2 后台命令登记(立即返回 command_id执行异步进行"""
...
def write_file(self, path: str, content: str, *, mode: str = "w") -> Dict[str, Any]:
"""E3 文件写入mode "w" 覆盖 / "a" 追加)。"""
...
def edit_file(self, path: str, replacements: List[Dict[str, Any]]) -> Dict[str, Any]:
"""E4 文件精确替换replacements 元素含 old_string/new_string/replace_all"""
...

View File

@ -0,0 +1,123 @@
"""FakeExecutionBackend内存替身执行器Execution Plane 的首个非真实实现)。
用途Runtime + 替身执行器的独立测试docs/execution_contract.md §6 验收
不产生任何真实副作用不起子进程不写磁盘不开终端
文件写落内存文件系统命令执行只记录调用并返回固定结构
非目标不模拟命令真实输出语义不模拟审批/权限裁决那是 Runtime 编排层职责
"""
from __future__ import annotations
import time
import uuid
from typing import Any, Dict, List, Optional
class FakeExecutionBackend:
"""内存替身执行器。calls 记录全部调用(供测试断言)。"""
def __init__(self) -> None:
self.calls: List[Dict[str, Any]] = []
self.files: Dict[str, str] = {} # path -> content内存文件系统
self.background_commands: Dict[str, Dict[str, Any]] = {}
async def run_command(
self,
command: str,
*,
timeout: float,
sandbox_write_access: bool,
network_permission: str,
) -> Dict[str, Any]:
self.calls.append({
"op": "run_command",
"command": command,
"timeout": timeout,
"sandbox_write_access": sandbox_write_access,
"network_permission": network_permission,
})
return {
"success": True,
"status": "completed",
"output": f"[fake-exec] {command}",
"return_code": 0,
"truncated": False,
"elapsed_ms": 0,
}
def run_command_background(
self,
command: str,
*,
timeout: float,
conversation_id: Optional[str],
wait_seconds: float,
network_permission: str,
sandbox_write_access: bool,
) -> Dict[str, Any]:
command_id = f"fake_bg_{uuid.uuid4().hex[:8]}"
self.calls.append({
"op": "run_command_background",
"command": command,
"timeout": timeout,
"conversation_id": conversation_id,
"sandbox_write_access": sandbox_write_access,
"network_permission": network_permission,
})
self.background_commands[command_id] = {
"command": command,
"status": "completed",
"output": f"[fake-exec-bg] {command}",
"return_code": 0,
"created_at": time.time(),
}
return {
"success": True,
"command_id": command_id,
"status": "running_background",
}
def write_file(self, path: str, content: str, *, mode: str = "w") -> Dict[str, Any]:
original = self.files.get(path)
if mode == "a" and original is not None:
new_content = original + content
else:
new_content = content
self.files[path] = new_content
self.calls.append({"op": "write_file", "path": path, "mode": mode, "bytes": len(content)})
return {
"success": True,
"path": path,
"original_file": original,
"new_file": new_content,
}
def edit_file(self, path: str, replacements: List[Dict[str, Any]]) -> Dict[str, Any]:
original = self.files.get(path)
if original is None:
self.calls.append({"op": "edit_file", "path": path, "error": "not_found"})
return {"success": False, "error": f"文件不存在: {path}"}
new_content = original
applied = 0
for rep in replacements:
old = rep.get("old_string", "")
new = rep.get("new_string", "")
if not old or old not in new_content:
continue
if rep.get("replace_all"):
applied += new_content.count(old)
new_content = new_content.replace(old, new)
else:
new_content = new_content.replace(old, new, 1)
applied += 1
if applied == 0:
return {"success": False, "error": "未找到任何匹配内容"}
self.files[path] = new_content
self.calls.append({"op": "edit_file", "path": path, "replacements_applied": applied})
return {
"success": True,
"path": path,
"original_file": original,
"new_file": new_content,
"replacements_applied": applied,
}