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/taskstate" ) type Gate struct { taskRunService taskstate.TaskRunStore questionWorder holdrecord.QuestionWorder approvalTargetResolver ApprovalTargetResolver permissionAsker PermissionAsker schedules Schedules } func New(taskRunService taskstate.TaskRunStore) *Gate { return &Gate{taskRunService: taskRunService} } func (gate *Gate) AwaitApproval(ctx context.Context, approvalRequest mcpserver.ApprovalRequest) (mcpserver.ApprovalOutcome, error) { taskRunID := strings.TrimSpace(approvalRequest.TaskRunID) if _, isApproved := gate.approvedHold(taskRunID, approvalRequest); isApproved { return gate.approvedOutcome(taskRunID, approvalRequest), nil } resolution, confirmation, isQuestion := gate.questionToAsk(approvalRequest) if !isQuestion { resolution = gate.resolveApprovalTarget(ctx, approvalRequest) if resolution.namesNothingThatExists() { return mcpserver.ApprovalOutcome{Decision: mcpserver.ApprovalDecisionUnresolvedTarget, Failure: resolution.Failure}, nil } if sendReachesOnlyTheRequester(approvalRequest, resolution.Target) { return mcpserver.ApprovalOutcome{Decision: mcpserver.ApprovalDecisionApproved}, nil } if gate.isAnsweredByItsSchedule(approvalRequest) { return gate.approvedByItsSchedule(taskRunID, approvalRequest, resolution), nil } confirmation = gate.confirmationWording(ctx, approvalRequest, resolution) } return gate.askedOutcome(ctx, taskRunID, approvalRequest, confirmation, resolution), nil } func (gate *Gate) holdCall(taskRunID string, approvalRequest mcpserver.ApprovalRequest, confirmation string, resolution ApprovalTargetResolution) (holdrecord.Hold, bool) { if _, errorValue := gate.taskRunService.PauseTaskRun(taskRunID, agentcontract.TaskStatusWaitingApproval, confirmation); errorValue != nil { slog.Warn("approvalgate.call_is_unanswerable", "taskRunID", taskRunID, "toolName", strings.TrimSpace(approvalRequest.ToolName), "reason", errorValue.Error()) return holdrecord.Hold{}, false } return gate.recordHeldCall(taskRunID, approvalRequest, confirmation, resolution), true } func (gate *Gate) approvedOutcome(taskRunID string, approvalRequest mcpserver.ApprovalRequest) mcpserver.ApprovalOutcome { holdID := RecordApprovalSpent(gate.taskRunService, taskRunID, approvalRequest.ToolName, approvalRequest.ToolInput) return mcpserver.ApprovalOutcome{Decision: mcpserver.ApprovalDecisionApproved, HoldID: holdID} } func (gate *Gate) approvedHold(taskRunID string, approvalRequest mcpserver.ApprovalRequest) (holdrecord.Hold, bool) { if taskRunID == "" { return holdrecord.Hold{}, false } return holdrecord.ApprovedHoldForCall(holdrecord.Holds(gate.taskRunService.ListTaskEvent(taskRunID)), approvalRequest.ToolName, approvalRequest.ToolInput) } func (gate *Gate) recordHeldCall(taskRunID string, approvalRequest mcpserver.ApprovalRequest, confirmation string, resolution ApprovalTargetResolution) holdrecord.Hold { hold := holdrecord.Open(gate.taskRunService, taskRunID, agentcontract.HeldCall{ ToolName: approvalRequest.ToolName, ToolInput: approvalRequest.ToolInput, ApprovedToolInput: narrowedToolInput(approvalRequest.ToolInput, resolution.Target), Confirmation: confirmation, HarnessSession: approvalRequest.HarnessSession, HandoffID: resolution.Target.HandoffID, }, resolution.Choices) gate.taskRunService.AppendTaskEvent(taskRunID, agentcontract.TaskEventConfirmationRequested, marshalEventBody(map[string]string{ "userFacingMessage": confirmation, "message": confirmation, "reasonCode": approvalReasonCode(approvalRequest), "reasonDetail": "approval gate for " + approvalRequest.ToolName, "responseLanguage": approvalRequest.ResponseLanguage, "source": "tool_catalog", })) gate.taskRunService.AppendTaskEvent(taskRunID, agentcontract.TaskEventAskRequested, marshalEventBody(askRecord(approvalRequest, confirmation))) return hold } func approvalReasonCode(approvalRequest mcpserver.ApprovalRequest) string { if sideEffectClass := strings.TrimSpace(approvalRequest.SideEffectClass); sideEffectClass != "" { return sideEffectClass } return "approval_required" } func askRecord(approvalRequest mcpserver.ApprovalRequest, confirmation string) map[string]any { return map[string]any{ "kind": "ask_confirm", "message": confirmation, "reasonCode": approvalReasonCode(approvalRequest), "reasonDetail": "approval gate for " + approvalRequest.ToolName, "responseLanguage": approvalRequest.ResponseLanguage, } } func marshalEventBody(value any) string { document, errorValue := json.Marshal(value) if errorValue != nil { return "" } return string(document) }