Files
wizardchen 6d0f09d75f fix(agent): 加固 MCP 工具人审批的跨实例与并发语义
- 跨实例 Resolve 通过 Redis reply channel 接收 owning 实例的 ack
  (ok / not_found / tenant_mismatch / user_mismatch / already_resolved),
  HTTP 不再静默 200,超时退化为 NotFound。
- PendingRequest 增加 UserID,Resolve 校验会话所有者,避免同租户
  其他用户越权批准/篡改参数。
- 新增 ErrAlreadyResolved,timer/ctx 命中后再次 Resolve 返回 400 而
  不是静默成功;waiter.deliver 暴露胜出语义。
- NeedsApproval 默认 fail-close(DB 故障要求审批),可经
  WEKNORA_AGENT_TOOL_APPROVAL_FAIL_OPEN=true 回退。
- Pub/Sub 增加 OriginID 自过滤、退避重连、namespace 后缀,避免自回
  环噪声与多部署串扰。
- 工具执行超时常量统一通过 ToolExecContext.ExecTimeout 注入,
  mcp_tool.go 不再硬编码 60s。
- handler 修正 path 二次解码、错误用 errors.Is、modified_args=null 的
  静默清空、ListMCPToolApprovals 的 404/500 误判。
- repository 用 clause.OnConflict 做原子 upsert,消除并发 500。
- 增加 NotFound / TenantMismatch / UserMismatch / AlreadyResolved /
  Race 等用例。
2026-05-10 22:57:12 +08:00

48 lines
1.6 KiB
Go

package tools
import (
"context"
"time"
"github.com/Tencent/WeKnora/internal/event"
)
type execCtxKey struct{}
// ToolExecContext is attached to context during agent tool execution (per tool call).
type ToolExecContext struct {
SessionID string
AssistantMessageID string
RequestID string
ToolCallID string
UserID string // owner of the originating session; used by HITL gates for authorization (issue #1173)
EventBus *event.EventBus
// ApprovalCtx is the parent ctx WITHOUT defaultToolExecTimeout; used when the tool
// must wait for human approval that may exceed normal tool exec timeout (issue #1173).
// Falls back to the per-tool execCtx when nil.
ApprovalCtx context.Context
// ExecTimeout mirrors the per-tool exec timeout the engine applied to the
// outer ctx. Tools that legitimately consume that ctx (e.g. MCP human
// approval) can re-derive a fresh timeout from ApprovalCtx using this
// value instead of hard-coding a duration. Zero means "fallback to 60s".
ExecTimeout time.Duration
}
// WithToolExecContext returns ctx that carries ToolExecContext for MCP approval and similar features.
func WithToolExecContext(ctx context.Context, meta *ToolExecContext) context.Context {
if meta == nil {
return ctx
}
return context.WithValue(ctx, execCtxKey{}, meta)
}
// ToolExecFromContext returns metadata attached by the agent engine, if any.
func ToolExecFromContext(ctx context.Context) (*ToolExecContext, bool) {
v := ctx.Value(execCtxKey{})
if v == nil {
return nil, false
}
meta, ok := v.(*ToolExecContext)
return meta, ok && meta != nil
}