Ctrl K
ComunidadGuías…

internal/approvalgate/gate.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)
}