Skip to content

Feat/pet tool approval#264

Open
our-past wants to merge 3 commits into1024XEngineer:devfrom
our-past:feat/pet-tool-approval
Open

Feat/pet tool approval#264
our-past wants to merge 3 commits into1024XEngineer:devfrom
our-past:feat/pet-tool-approval

Conversation

@our-past
Copy link
Copy Markdown
Contributor

@our-past our-past commented May 9, 2026

PR: Pet 通道工具审批机制

Related Issue: #255

背景

当前 pet 通道的 LLM 可以调用工具箱中的高风险工具(如 execwrite_filesubagent 等),虽然有 deny 黑名单和 workspace 限制,但没有用户显式确认机制。用户无法在工具执行前进行审批,存在安全隐患。

目标

实现 ToolApprover hook,让 pet 通道的客户端可以审批/拒绝高风险工具的执行。

实现方案

1. 新增类型和常量 (pkg/pet/types.go)

  • PushTypeToolApproval = "tool_approval" — 推送类型
  • ActionToolApprovalResponse = "tool_approval_response" — 客户端响应 Action
  • ToolApprovalPush — 推送给客户端的审批请求
  • ToolApprovalResponse — 客户端返回的审批结果

2. PetHook 实现 ToolApprover (pkg/pet/hooks.go)

  • 新增 pendingApprovals 映射,存储等待审批的请求
  • ApproveTool():推送审批请求给客户端 → 等待用户响应(60s 超时)→ 返回审批结果
  • ResolveApproval():由 PetService 在收到客户端响应时调用,结束等待

3. PetService 新增审批支持 (pkg/pet/service.go)

  • sessionPush 回调:定向推送给指定 session 的客户端
  • resolveApproval 回调:将客户端审批结果转发给 PetHook
  • handleToolApprovalResponse():处理 tool_approval_response 请求

4. PetChannel 新增定向推送 (pkg/channels/pet/channel.go)

  • sendPushToSession():只推送给匹配 sessionID 的 WebSocket 连接

5. 审批推送流程

LLM 调用 exec("ipconfig")
    ↓
AgentLoop → PetHook.ApproveTool()
    ↓
发送 tool_approval push(只发给当前会话)
    ↓
客户端弹出确认框
    ↓
用户选择 → 发回 tool_approval_response
    ↓
PetService.handleToolApprovalResponse()
    ↓
PetHook.ResolveApproval() → 返回审批结果
    ↓
AgentLoop 执行或跳过工具

6. 客户端交互格式

服务端推送

{
  "type": "push",
  "push_type": "tool_approval",
  "data": {
    "request_id": "tool_approval_1714896000123456789",
    "tool": "exec",
    "arguments": { "command": "ipconfig" }
  },
  "timestamp": 1714896000
}

客户端回复(同一条 WebSocket 连接):

{
  "action": "tool_approval_response",
  "data": {
    "request_id": "tool_approval_1714896000123456789",
    "approved": true
  }
}

7. 后续待改进

  • 配置化:支持自定义哪些工具需要审批(tool_approval.require_approval 配置项)
  • 记忆化:同一会话内同命令可记住用户选择,减少重复审批
  • session 校验:回复审批时校验 sessionID 是否匹配发起方

文件变更

修改文件

文件 变更说明
pkg/pet/types.go 新增审批相关类型和常量
pkg/pet/hooks.go PetHook 实现 ToolApprover 接口
pkg/pet/service.go 新增会话推送和审批响应处理
pkg/channels/pet/channel.go 新增定向推送方法
pkg/gateway/gateway.go 注册审批回调
docs/PET_CHANNEL_API.md 新增 tool_approval API 文档

提交记录

53b21d22 docs(pet): add tool approval push documentation to API docs
76843076 feat(pet): PetHook implements ToolApprover interface for user approval
83d30fd4 feat(pet): add tool approval push types and action constants

our-past added 3 commits May 10, 2026 01:05
- PushTypeToolApproval: new push type for tool approval requests
- ActionToolApprovalResponse: client response action for approval
- ToolApprovalPush: push data with request_id, tool, arguments
- ToolApprovalResponse: response data with request_id, approved
- Add pendingApprovals map to track approval requests in-flight
- Add approvalMu mutex for concurrent access
- Implement ApproveTool(): send push to client, wait for response
- Implement ResolveApproval(): resolve pending approval with user decision
- Timeout 60 seconds, auto-deny on timeout
- Push only to the session that initiated the tool call via sessionPush
- Add section 3.9 tool_approval with push format, field reference
- Add client response format and frontend handling example
- Update push_type quick reference table with tool_approval
- Update version to v2.11
@our-past our-past requested review from Melon80 and hazy-cloudy May 9, 2026 17:21
Copy link
Copy Markdown

@fennoai fennoai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found 3 issues in the new approval flow: one integration regression that affects non-pet channels, one race in approval registration, and one missing trust-boundary check when resolving approvals.

Comment thread pkg/gateway/gateway.go

petHook := pet.NewPetHook(svc.CharManager(), svc.ActionManager(), svc, svc.MemoryStore(), svc.ConversationStore(), svc.UserProfileManager())
svc.SetApprovalResolver(petHook.ResolveApproval)
agentLoop.MountHook(agent.NamedHook("pet", petHook))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High: this hook is mounted globally (对所有 channel 生效), but PetHook.ApproveTool now waits for a pet-channel WebSocket approval keyed by req.ChatID. On non-pet channels, req.ChatID is a Telegram/Discord/etc chat id, so sessionPush will never reach a pet client and every tool call will block for 60s and then auto-deny. The approval path needs to be scoped to pet-originated turns, or it will break tool execution outside the pet channel.

Comment thread pkg/pet/hooks.go
Comment on lines +999 to +1007
if h.petService.sessionPush != nil {
h.petService.sessionPush(req.ChatID, pushMsg)
} else if h.petService.pushHandler != nil {
h.petService.pushHandler(pushMsg)
}

ch := make(chan bool, 1)
h.approvalMu.Lock()
h.pendingApprovals[requestID] = ch
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

High: the approval request is sent before pendingApprovals[requestID] is registered. If the client responds quickly, handleToolApprovalResponse can call ResolveApproval before the map entry exists, which drops the decision and leaves this call waiting until the 60s timeout. Register the channel first, then publish the push.

Comment thread pkg/pet/service.go
if err := json.Unmarshal(req.Data, &data); err != nil {
return s.sendError(sessionID, req.Action, "invalid tool approval response data")
}
if s.resolveApproval != nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Medium: this resolves any request_id without checking that the response came from the same session/connection that received the approval prompt. A different connected client that learns the request_id can approve or deny someone else's tool call, and it also contradicts the docs that say the reply must come back on the same WebSocket connection.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant