internal/adminapi/task_approval_handler.go
Rama main · réplica de lectura del repositorio original.
package adminapi
import (
"context"
"encoding/json"
"errors"
"net/http"
"strings"
"github.com/yeomyeonggeori/blueclaw/internal/agentruntime"
"github.com/yeomyeonggeori/blueclaw/internal/approvalgate"
"github.com/yeomyeonggeori/blueclaw/internal/identity"
"github.com/yeomyeonggeori/blueclaw/internal/policy"
"github.com/yeomyeonggeori/blueclaw/internal/task"
"github.com/yeomyeonggeori/blueprotocol/agentcontract"
)
type LiveHolds interface {
AnswerAwaitedHold(taskRunID string, signal agentcontract.ApprovalSignal) (bool, error)
}
type TaskApprovalHandler struct {
LiveHolds LiveHolds
TaskLauncher *agentruntime.TaskLauncher
TaskRunService *task.TaskRunService
IdentityService *identity.IdentityService
}
type taskApprovalRequest struct {
TaskRunID string `json:"taskRunID"`
Decision string `json:"decision"`
ApproverEmail string `json:"approverEmail"`
}
var errApproverIsNotTheRequester = errors.New("only the person who asked for this call may approve or reject it")
type taskApprovalResponse struct {
TaskRunID string `json:"taskRunID"`
Status string `json:"status"`
}
func (handler TaskApprovalHandler) HandleApproveTaskRun(responseWriter http.ResponseWriter, request *http.Request) {
if handler.TaskRunService == nil || handler.TaskLauncher == nil || handler.IdentityService == nil {
writeApprovalError(responseWriter, http.StatusServiceUnavailable, "task approval is not configured")
return
}
var approvalRequest taskApprovalRequest
if json.NewDecoder(request.Body).Decode(&approvalRequest) != nil {
writeApprovalError(responseWriter, http.StatusBadRequest, "invalid task approval request")
return
}
taskRun, approvalSignal, errorValue := handler.resolveApproval(approvalRequest)
if errors.Is(errorValue, errApproverIsNotTheRequester) {
writeApprovalError(responseWriter, http.StatusForbidden, errorValue.Error())
return
}
if errorValue != nil {
writeApprovalError(responseWriter, http.StatusBadRequest, errorValue.Error())
return
}
if isAnswered, errorValue := handler.answerLiveWaiter(taskRun, approvalSignal); errorValue != nil {
writeApprovalError(responseWriter, http.StatusConflict, errorValue.Error())
return
} else if isAnswered {
writeApprovalResponse(responseWriter, taskApprovalResponse{TaskRunID: taskRun.TaskRunID, Status: "answered_in_place"})
return
}
approvalgate.SettleSignal(handler.TaskRunService, taskRun.TaskRunID, &approvalSignal, "operator_terminal")
launchResult, errorValue := handler.TaskLauncher.Launch(context.Background(), agentruntime.TaskLaunchRequest{
Source: agentruntime.TaskLaunchSourceAdmin,
SourceReference: "terminal:" + taskRun.TaskRunID,
RequesterPersonID: taskRun.RequesterPersonID,
RequesterEmail: handler.requesterEmail(taskRun.RequesterPersonID),
IsRuntimeRestartResume: true,
ExistingTaskRunID: taskRun.TaskRunID,
ConversationID: taskRun.OriginConversationID,
Prompt: taskRun.Prompt,
PersonAccess: handler.personAccess(taskRun.RequesterPersonID),
})
if errorValue != nil {
writeApprovalError(responseWriter, http.StatusInternalServerError, errorValue.Error())
return
}
writeApprovalResponse(responseWriter, taskApprovalResponse{
TaskRunID: taskRun.TaskRunID,
Status: string(launchResult.TurnResult.TaskRun.Status),
})
}
func (handler TaskApprovalHandler) answerLiveWaiter(taskRun task.TaskRun, approvalSignal agentcontract.ApprovalSignal) (bool, error) {
if handler.LiveHolds == nil {
return false, nil
}
return handler.LiveHolds.AnswerAwaitedHold(taskRun.TaskRunID, approvalSignal)
}
func (handler TaskApprovalHandler) resolveApproval(approvalRequest taskApprovalRequest) (task.TaskRun, agentcontract.ApprovalSignal, error) {
taskRunID := strings.TrimSpace(approvalRequest.TaskRunID)
if taskRunID == "" {
return task.TaskRun{}, "", errors.New("taskRunID is required")
}
taskRun, isFound := handler.TaskRunService.FindTaskRun(taskRunID)
if !isFound {
return task.TaskRun{}, "", errors.New("task run not found")
}
if taskRun.Status != task.TaskStatusWaitingApproval {
return task.TaskRun{}, "", errors.New("task run is not waiting for approval")
}
approvalSignal, errorValue := approvalSignalOf(approvalRequest.Decision)
if errorValue != nil {
return task.TaskRun{}, "", errorValue
}
if !handler.isTheRequester(approvalRequest.ApproverEmail, taskRun) {
return task.TaskRun{}, "", errApproverIsNotTheRequester
}
return taskRun, approvalSignal, nil
}
func (handler TaskApprovalHandler) isTheRequester(approverEmail string, taskRun task.TaskRun) bool {
approverPersonID, isKnown := handler.IdentityService.ResolvePersonIDByEmail(approverEmail)
return isKnown && approverPersonID != "" && approverPersonID == taskRun.RequesterPersonID
}
func approvalSignalOf(decision string) (agentcontract.ApprovalSignal, error) {
switch strings.TrimSpace(decision) {
case "approve":
return agentcontract.ApprovalSignalApprove, nil
case "reject":
return agentcontract.ApprovalSignalReject, nil
default:
return "", errors.New(`decision must be one of "approve", "reject"`)
}
}
func (handler TaskApprovalHandler) personAccess(personID string) policy.PersonAccess {
if handler.IdentityService == nil {
return policy.PersonAccess{PersonID: personID}
}
return handler.IdentityService.ResolvePersonAccess(personID)
}
func (handler TaskApprovalHandler) requesterEmail(personID string) string {
if handler.IdentityService == nil {
return ""
}
return handler.IdentityService.ResolvePersonPrimaryEmail(personID)
}
func marshalApprovalEventBody(value any) string {
document, errorValue := json.Marshal(value)
if errorValue != nil {
return ""
}
return string(document)
}
func writeApprovalError(responseWriter http.ResponseWriter, statusCode int, message string) {
responseWriter.Header().Set("Content-Type", "application/json")
responseWriter.WriteHeader(statusCode)
_ = json.NewEncoder(responseWriter).Encode(map[string]string{"error": message})
}
func writeApprovalResponse(responseWriter http.ResponseWriter, response taskApprovalResponse) {
responseWriter.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(responseWriter).Encode(response)
}