package approvalgate import ( "context" "encoding/json" "log/slog" "strings" "github.com/yeomyeonggeori/blueclaw/internal/mcpserver" "github.com/yeomyeonggeori/blueprotocol/agentcontract" "github.com/yeomyeonggeori/blueprotocol/holdrecord" "github.com/yeomyeonggeori/blueprotocol/toolcontract" ) type ApprovalTarget = agentcontract.ApprovalTarget type ApprovalTargetResolution struct { Target ApprovalTarget Choices []holdrecord.Choice Failure toolcontract.ToolResult } type ApprovalTargetRequest struct { ToolName string ToolInput json.RawMessage RequesterPersonID string RequesterEmail string Platform string ConversationID string ReplyTargetID string Transport map[string]any } type ApprovalTargetResolver interface { ResolveApprovalTarget(context.Context, ApprovalTargetRequest) (ApprovalTargetResolution, error) } func (gate *Gate) UseApprovalTargetResolver(approvalTargetResolver ApprovalTargetResolver) { gate.approvalTargetResolver = approvalTargetResolver } func (resolution ApprovalTargetResolution) namesNothingThatExists() bool { return !resolution.Target.IsResolved() && resolution.Failure.Failure != nil } func sendReachesOnlyTheRequester(approvalRequest mcpserver.ApprovalRequest, target ApprovalTarget) bool { if strings.TrimSpace(approvalRequest.SideEffectClass) != toolcontract.ToolSideEffectExternalSend || !target.IsResolved() { return false } for _, recipient := range append([]string{target.ID}, target.IDs...) { if strings.TrimSpace(recipient) != "" && !namesTheRequester(approvalRequest, recipient) { return false } } return true } func namesTheRequester(approvalRequest mcpserver.ApprovalRequest, recipient string) bool { trimmedRecipient := strings.TrimSpace(recipient) requesterEmail := strings.TrimSpace(approvalRequest.RequesterEmail) return trimmedRecipient == strings.TrimSpace(approvalRequest.RequesterPersonID) || (requesterEmail != "" && strings.EqualFold(trimmedRecipient, requesterEmail)) } func (gate *Gate) resolveApprovalTarget(ctx context.Context, approvalRequest mcpserver.ApprovalRequest) ApprovalTargetResolution { if target, isNamed := refusalTarget(ctx); isNamed { return ApprovalTargetResolution{Target: target} } if gate.approvalTargetResolver == nil { return ApprovalTargetResolution{} } transport, failure := CarriedWorkspaceFiles(ctx) if failure != nil { return ApprovalTargetResolution{Failure: *failure} } resolution, errorValue := gate.approvalTargetResolver.ResolveApprovalTarget(ctx, ApprovalTargetRequest{ ToolName: strings.TrimSpace(approvalRequest.ToolName), ToolInput: approvalRequest.ToolInput, RequesterPersonID: approvalRequest.RequesterPersonID, RequesterEmail: approvalRequest.RequesterEmail, Platform: approvalRequest.Platform, ConversationID: approvalRequest.ConversationID, ReplyTargetID: approvalRequest.ReplyTargetID, Transport: transport, }) if errorValue == nil { return resolution } slog.Warn("approvalgate.target_resolution_unreachable", "toolName", strings.TrimSpace(approvalRequest.ToolName), "taskRunID", strings.TrimSpace(approvalRequest.TaskRunID), "reason", errorValue.Error()) return ApprovalTargetResolution{} } func narrowedToolInput(toolInput json.RawMessage, target ApprovalTarget) json.RawMessage { if !target.IsResolved() { return nil } document := map[string]json.RawMessage{} if json.Unmarshal(toolInput, &document) != nil { return nil } identity, errorValue := json.Marshal(narrowedIdentity(target)) if errorValue != nil { return nil } document[strings.TrimSpace(target.InputField)] = identity narrowedInput, errorValue := json.Marshal(document) if errorValue != nil { return nil } return narrowedInput } func narrowedIdentity(target ApprovalTarget) any { if len(target.IDs) > 0 { return target.IDs } return strings.TrimSpace(target.ID) }