feat(runtime): 审批/提问纳入公共入口,①②③链路收口
- RuntimeService 新增 list_pending_approvals(三类分桶)与 resolve_approval (kind 路由 tool/plan/question,透传 manager 单次裁决/越权/参数校验语义) - 协议能力 run.*/approval.*/session.* 全部有 RuntimeService 公共方法 - 现状文档记录本轮收口决策:①②③贯通为终点,③↔④全量贯通后置(§7.5-补四件事) - 全量回归:失败恰为 4 项存量,与改造无关
This commit is contained in:
parent
4378eb6a46
commit
d7cb854f0c
@ -246,16 +246,18 @@ v1 原文把「内存态」直接判为「状态唯一 Owner 的障碍」,混
|
|||||||
|
|
||||||
> 总原则:协议草案与接口适配可迭代推进;**每一步保留现有门闸、保存和恢复保障**;四层不要求四进程(G13);Remote Worker 不是本轮完成条件。
|
> 总原则:协议草案与接口适配可迭代推进;**每一步保留现有门闸、保存和恢复保障**;四层不要求四进程(G13);Remote Worker 不是本轮完成条件。
|
||||||
|
|
||||||
> **实施状态速览(2026-09-07,commit d9bd599c / 27aeed70)**:
|
> **实施状态速览(2026-09-07,commit d9bd599c / 27aeed70 / 4378eb6a 起)**:
|
||||||
> | 步骤 | 状态 | 证据 |
|
> | 步骤 | 状态 | 证据 |
|
||||||
> |---|---|---|
|
> |---|---|---|
|
||||||
> | 第 0 步 | ✅ 代码侧完成(N3 人工验证待用户) | §6.1 状态注记 |
|
> | 第 0 步 | ✅ 代码侧完成(N3 人工验证待用户) | §6.1 状态注记 |
|
||||||
> | 第 1 步 | ✅ | `StandaloneRuntimeLifecycleTest`;`server/tasks` 拆解;emit_event/run_background 安全包装 |
|
> | 第 1 步 | ✅ | `StandaloneRuntimeLifecycleTest`;`server/tasks` 拆解;emit_event/run_background 安全包装 |
|
||||||
> | 第 2 步 | ✅ | `docs/runtime_protocol.md`;`ProtocolSmokeChainTest` 全链路(含服务层补建对话) |
|
> | 第 2 步 | ✅ | `docs/runtime_protocol.md`;`ProtocolSmokeChainTest` 全链路(含服务层补建对话、审批公共入口路由) |
|
||||||
> | 第 3 步 | ✅ | 协议 §5.2-5.4(序号作用域/window_start 缺口检测/重启三场景/多端共享);`get_task_events` meta 水位 |
|
> | 第 3 步 | ✅ | 协议 §5.2-5.4(序号作用域/window_start 缺口检测/重启三场景/多端共享);`get_task_events` meta 水位 |
|
||||||
> | 第 4 步 | ✅ 首版(E1-E4 面) | `modules/execution_plane` + `docs/execution_contract.md` + `ExecutionPlaneFakeBackendTest`;E5-E10 与 Host/Docker 同契约接入为后续项 |
|
> | 第 4 步 | ✅ 首版(E1-E4 面) | `modules/execution_plane` + `docs/execution_contract.md` + `ExecutionPlaneFakeBackendTest`;E5-E10 与 Host/Docker 同契约接入为后续项(§7.5-补) |
|
||||||
> | 第 5 步 | ✅ 收窄完成(代码就绪度) | 公共入口覆盖清单:run.*/approval.*/session.* 全绿(协议 §3/§4);正式客户端开发不在本轮 |
|
> | 第 5 步 | ✅ 收窄完成(代码就绪度) | 公共入口覆盖:run.*/approval.*/session.* 全部进入 RuntimeService(协议 §3/§4);正式客户端开发不在本轮 |
|
||||||
> | 遗留 | ⬜ | N3 真实环境人工验证、N4/N5 疑点、E5-E10 纳入、定时任务(§7.6 后置) |
|
> | 遗留 | ⬜ | N3 真实环境人工验证、N4/N5 疑点、③↔④ 全量贯通(§7.5-补)、定时任务(§7.6 后置) |
|
||||||
|
>
|
||||||
|
> **本轮收口决策(2026-09-07 用户拍板)**:①②③ 链路(Client ↔ Gateway ↔ Runtime)贯通即为本轮终点;③↔④ 全量贯通(Host/Docker 迁入 ExecutionBackend 契约、E5-E10 纳入)后置为独立工作,期间默认路径 `execution_backend=None`(现有真实链路不变)。
|
||||||
|
|
||||||
### 7.1 第 0 步:闭环疑点与小修复(不依赖架构决策)
|
### 7.1 第 0 步:闭环疑点与小修复(不依赖架构决策)
|
||||||
|
|
||||||
@ -289,6 +291,18 @@ v1 原文把「内存态」直接判为「状态唯一 Owner 的障碍」,混
|
|||||||
- 定义 Execution Contract:相同 Runtime 可接测试执行器(替身)及现有 Host/Docker 实现;执行链无需了解 Web 会话与客户端连接。
|
- 定义 Execution Contract:相同 Runtime 可接测试执行器(替身)及现有 Host/Docker 实现;执行链无需了解 Web 会话与客户端连接。
|
||||||
- 验收:Runtime + 替身执行器可独立测试;Host/Docker 后端经同一契约接入。
|
- 验收:Runtime + 替身执行器可独立测试;Host/Docker 后端经同一契约接入。
|
||||||
|
|
||||||
|
#### 7.5-补 ③↔④ 全量贯通工作分解(2026-09-07 定,后置独立工作)
|
||||||
|
|
||||||
|
> 首版已完成:契约文档 + ExecutionBackend 协议(E1-E4)+ 替身执行器 + 接入点
|
||||||
|
> (`MainTerminal.execution_backend`,默认 None)。以下为「Host/Docker 迁入契约」的四件事:
|
||||||
|
|
||||||
|
1. **接口面补全 E5-E10**:文件读三模式 / 文件 CRUD 族 / 持久终端 6 操作 / path_validate / 执行环境快照 / 命令校验。照搬现状语义(返回结构、截断、错误形态),不发明新能力。主改 `modules/execution_plane/base.py`。
|
||||||
|
2. **真实适配器 HostDockerBackend**:新实现类内部委托现有 terminal_ops / file_manager / terminal_manager / background_command_manager。**决策点**:薄适配(纯转发,快、风险小,4 处后端选择复制的结构债留着)vs 厚适配(顺手收敛 terminal_ops/run.py、background_command_manager.py、persistent_terminal/start.py、container_file_proxy.py 的后端选择逻辑,根治但动四个核心执行文件)。**既定方向:先薄适配**。
|
||||||
|
3. **编排层全面切换**:tools_execution.py 给 E5-E10 分支插 backend 注入(约 6-10 处);默认装配由 None 改为 HostDockerBackend 实例。**风险点是行为等价**:权限裁决/先读后写/浅备份/编辑摘要/字符截断留在编排层,执行细节归后端,边界必须切干净。
|
||||||
|
4. **验收**:替身测试保持绿 + 全量回归 + N3 真实环境人工验证一并做。
|
||||||
|
|
||||||
|
完成后收益:Runtime 对执行环境零直接依赖;换后端(Remote Worker、子智能体独立沙箱)只需换实现,不碰智能体循环。
|
||||||
|
|
||||||
### 7.6 第 5 步:正式客户端接入 + 定时任务后置接入
|
### 7.6 第 5 步:正式客户端接入 + 定时任务后置接入
|
||||||
|
|
||||||
- Web 与正式 CLI/GUI/IDE 接入同一业务服务与协议。
|
- Web 与正式 CLI/GUI/IDE 接入同一业务服务与协议。
|
||||||
|
|||||||
@ -147,6 +147,66 @@ class RuntimeService:
|
|||||||
|
|
||||||
return task_manager.promote_runtime_pending_to_guidance(username, task_id, message_id)
|
return task_manager.promote_runtime_pending_to_guidance(username, task_id, message_id)
|
||||||
|
|
||||||
|
# ---- 审批/提问(公共入口;三类 manager 的薄路由,裁决语义由 manager 保证)----
|
||||||
|
|
||||||
|
_APPROVAL_KINDS = ("tool", "plan", "question")
|
||||||
|
|
||||||
|
def list_pending_approvals(
|
||||||
|
self, username: str, conversation_id: Optional[str] = None, *, kind: Optional[str] = None
|
||||||
|
) -> Dict[str, List[Dict[str, Any]]]:
|
||||||
|
"""待决审批/提问合并查询(approval.list)。kind=None 返回三类全量(按键分桶)。"""
|
||||||
|
from server.state import plan_approval_manager, tool_approval_manager, user_question_manager
|
||||||
|
|
||||||
|
kinds = (kind,) if kind else self._APPROVAL_KINDS
|
||||||
|
if any(k not in self._APPROVAL_KINDS for k in kinds):
|
||||||
|
raise ValueError(f"runtime_context: 未知审批类型 {kind}")
|
||||||
|
result: Dict[str, List[Dict[str, Any]]] = {}
|
||||||
|
if "tool" in kinds:
|
||||||
|
result["tool"] = tool_approval_manager.list_pending(username, conversation_id)
|
||||||
|
if "plan" in kinds:
|
||||||
|
result["plan"] = plan_approval_manager.list_pending(username, conversation_id)
|
||||||
|
if "question" in kinds:
|
||||||
|
result["question"] = user_question_manager.list_pending(username, conversation_id)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def resolve_approval(
|
||||||
|
self,
|
||||||
|
kind: str,
|
||||||
|
*,
|
||||||
|
username: str,
|
||||||
|
item_id: str,
|
||||||
|
decision: Optional[str] = None,
|
||||||
|
approved: Optional[bool] = None,
|
||||||
|
comment: Optional[str] = None,
|
||||||
|
selected_option_id: Optional[str] = None,
|
||||||
|
text: Optional[str] = None,
|
||||||
|
dismissed: bool = False,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""统一裁决入口(approval.resolve)。kind 路由:
|
||||||
|
- tool: decision ∈ {"approved","rejected"}(锁内单次裁决,重复回答返回现状)
|
||||||
|
- plan: approved bool + 可选 comment
|
||||||
|
- question: selected_option_id / text / dismissed 三选一语义
|
||||||
|
错误语义与 manager 一致:越权 PermissionError、不存在 KeyError、参数非法 ValueError。
|
||||||
|
"""
|
||||||
|
from server.state import plan_approval_manager, tool_approval_manager, user_question_manager
|
||||||
|
|
||||||
|
if kind == "tool":
|
||||||
|
if decision is None:
|
||||||
|
raise ValueError("runtime_context: tool 审批需 decision")
|
||||||
|
return tool_approval_manager.decide(item_id, username, str(decision), reason=comment)
|
||||||
|
if kind == "plan":
|
||||||
|
if approved is None:
|
||||||
|
raise ValueError("runtime_context: plan 审批需 approved")
|
||||||
|
return plan_approval_manager.answer(
|
||||||
|
approval_id=item_id, username=username, approved=bool(approved), comment=comment
|
||||||
|
)
|
||||||
|
if kind == "question":
|
||||||
|
return user_question_manager.answer(
|
||||||
|
question_id=item_id, username=username,
|
||||||
|
selected_option_id=selected_option_id, text=text, dismissed=dismissed,
|
||||||
|
)
|
||||||
|
raise ValueError(f"runtime_context: 未知审批类型 {kind}")
|
||||||
|
|
||||||
# ---- 会话查询(公共入口;CLI/定时器等非 Web 调用方不依赖 Web 路由)----
|
# ---- 会话查询(公共入口;CLI/定时器等非 Web 调用方不依赖 Web 路由)----
|
||||||
|
|
||||||
def list_sessions(
|
def list_sessions(
|
||||||
|
|||||||
@ -214,7 +214,8 @@ def check_chain():
|
|||||||
else:
|
else:
|
||||||
raise AssertionError("principal 与查询目标不一致必须抛 PermissionError")
|
raise AssertionError("principal 与查询目标不一致必须抛 PermissionError")
|
||||||
|
|
||||||
# 6. 审批语义:create → list_pending → decide → 重复 decide 返回现状(单次裁决)
|
# 6. 审批语义:公共入口 list_pending_approvals / resolve_approval
|
||||||
|
# (manager 层语义已有覆盖,这里验收公共入口路由与错误语义透传)
|
||||||
from server.state import tool_approval_manager
|
from server.state import tool_approval_manager
|
||||||
|
|
||||||
item = tool_approval_manager.create_request(
|
item = tool_approval_manager.create_request(
|
||||||
@ -222,11 +223,16 @@ def check_chain():
|
|||||||
tool_call_id="tc_smoke", tool_name="run_command",
|
tool_call_id="tc_smoke", tool_name="run_command",
|
||||||
arguments={"command": "echo hi"}, preview={},
|
arguments={"command": "echo hi"}, preview={},
|
||||||
)
|
)
|
||||||
pending = tool_approval_manager.list_pending("gw_smoke_user", conv_id)
|
pending = runtime_service.list_pending_approvals("gw_smoke_user", conv_id)
|
||||||
assert any(p.get("approval_id") == item["approval_id"] for p in pending), "审批应入 pending 列表"
|
assert any(p.get("approval_id") == item["approval_id"] for p in pending.get("tool", [])), \
|
||||||
first = tool_approval_manager.decide(item["approval_id"], "gw_smoke_user", "approved")
|
"公共入口审批列表应含该项"
|
||||||
|
first = runtime_service.resolve_approval(
|
||||||
|
"tool", username="gw_smoke_user", item_id=item["approval_id"], decision="approved"
|
||||||
|
)
|
||||||
assert first.get("status") == "approved"
|
assert first.get("status") == "approved"
|
||||||
second = tool_approval_manager.decide(item["approval_id"], "gw_smoke_user", "rejected")
|
second = runtime_service.resolve_approval(
|
||||||
|
"tool", username="gw_smoke_user", item_id=item["approval_id"], decision="rejected"
|
||||||
|
)
|
||||||
assert second.get("status") == "approved", "重复裁决必须返回现状(单次裁决)"
|
assert second.get("status") == "approved", "重复裁决必须返回现状(单次裁决)"
|
||||||
# 越权防护:他人裁决应拒绝
|
# 越权防护:他人裁决应拒绝
|
||||||
item2 = tool_approval_manager.create_request(
|
item2 = tool_approval_manager.create_request(
|
||||||
@ -234,11 +240,22 @@ def check_chain():
|
|||||||
tool_call_id="tc_smoke2", tool_name="write_file", arguments={}, preview={},
|
tool_call_id="tc_smoke2", tool_name="write_file", arguments={}, preview={},
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
tool_approval_manager.decide(item2["approval_id"], "other_user", "approved")
|
runtime_service.resolve_approval(
|
||||||
|
"tool", username="other_user", item_id=item2["approval_id"], decision="approved"
|
||||||
|
)
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise AssertionError("越权裁决必须抛 PermissionError")
|
raise AssertionError("越权裁决必须抛 PermissionError")
|
||||||
|
# 未知审批类型应拒绝
|
||||||
|
try:
|
||||||
|
runtime_service.resolve_approval(
|
||||||
|
"bogus", username="gw_smoke_user", item_id="x", decision="approved"
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise AssertionError("未知审批类型必须抛 ValueError")
|
||||||
|
|
||||||
|
|
||||||
def check_fake_exec():
|
def check_fake_exec():
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user