Files
goclaw/internal/tools/delegate_completion_ledger.go
T
Duc Nguyenandntduc bb7712a9ff fix(collaboration): harden delegated task isolation (#1486)
* feat(collaboration): isolate delegated artifacts and child runs

Isolate delegated inputs and outputs behind secure artifact exchange lifecycles. Scope Agent Link tasks by tenant and root agent, and enforce delegation spawn-tree boundaries. Add process-wide child-run admission and preserve logical media paths across native, MCP, and sandbox execution.

* fix(collaboration): harden delegated task isolation

Enforce tenant and root-agent task scope across migrations and stores. Add exactly-once async completion delivery, delegated sandbox boundaries, and confined artifact and media recovery across runtime surfaces.

* fix(collaboration): recover interrupted async tasks

* fix(collaboration): normalize persisted child-run status

---------

Co-authored-by: ntduc <ntduc@cpp.ai.vn>
2026-07-30 14:17:40 +07:00

239 lines
7.1 KiB
Go

package tools
import (
"context"
"encoding/json"
"fmt"
"log/slog"
"time"
"github.com/google/uuid"
"github.com/nextlevelbuilder/goclaw/internal/store"
)
const delegateCompletionTargetKey = "target_agent_key"
const delegateRunningPersistenceTimeout = 250 * time.Millisecond
func (t *DelegateTool) createDelegateCompletion(ctx context.Context, req DelegateRequest) error {
if t.taskStore == nil {
return nil
}
delegationID, err := uuid.Parse(req.DelegationID)
if err != nil {
return fmt.Errorf("invalid delegation ID: %w", err)
}
tenantID := parseUUIDOrNil(req.TenantID)
if tenantID == uuid.Nil || req.FromAgentID == uuid.Nil {
return fmt.Errorf("delegation completion requires tenant and source agent")
}
var sessionKey, originChannel, originChatID, originPeerKind, originUserID *string
if req.SessionKey != "" {
sessionKey = &req.SessionKey
}
if req.Channel != "" {
originChannel = &req.Channel
}
if req.ChatID != "" {
originChatID = &req.ChatID
}
if req.PeerKind != "" {
originPeerKind = &req.PeerKind
}
if req.UserID != "" {
originUserID = &req.UserID
}
data := &store.SubagentTaskData{
BaseModel: store.BaseModel{ID: delegationID},
TenantID: tenantID,
RootAgentID: req.FromAgentID,
ParentAgentKey: req.FromAgentKey,
SessionKey: sessionKey,
Subject: "Delegate to " + req.ToAgentKey,
Description: req.Task,
Status: TaskStatusQueued,
Depth: subagentDepthFromContext(ctx, 0) + 1,
OriginChannel: originChannel,
OriginChatID: originChatID,
OriginPeerKind: originPeerKind,
OriginUserID: originUserID,
Metadata: map[string]any{
asyncCompletionKindKey: asyncCompletionKindDelegate,
delegateCompletionTargetKey: req.ToAgentKey,
asyncCompletionDeliveryKey: asyncCompletionDeliveryPending,
},
}
dbCtx := store.WithTenantID(context.Background(), tenantID)
if err := retryAsyncPersistence(dbCtx, func(attemptCtx context.Context) error {
return t.taskStore.Create(attemptCtx, data)
}); err != nil {
return fmt.Errorf("persist accepted delegation %s: %w", req.DelegationID, err)
}
return nil
}
func (t *DelegateTool) updateDelegateCompletion(
req DelegateRequest,
status string,
result *string,
) error {
if t.taskStore == nil {
return nil
}
delegationID := parseUUIDOrNil(req.DelegationID)
tenantID := parseUUIDOrNil(req.TenantID)
if delegationID == uuid.Nil || tenantID == uuid.Nil || req.FromAgentID == uuid.Nil {
return fmt.Errorf("delegation completion requires valid tenant, source agent, and delegation ID")
}
dbCtx := store.WithTenantID(context.Background(), tenantID)
if err := retryTerminalPersistence(dbCtx, func(attemptCtx context.Context) error {
return t.taskStore.UpdateStatus(
attemptCtx,
req.FromAgentID,
delegationID,
status,
result,
0,
0,
0,
)
}); err != nil {
slog.Error("delegate.async.completion_persist_failed",
"delegation_id", req.DelegationID,
"from_agent_id", req.FromAgentID,
"status", status,
"error", err,
)
return err
}
return nil
}
// updateDelegateRunning is observability-only: execution may proceed even when
// this transition cannot be persisted. Keep it to one short attempt outside
// the admission callback so a database outage cannot monopolize a child-run
// permit.
func (t *DelegateTool) updateDelegateRunning(req DelegateRequest) error {
if t.taskStore == nil {
return nil
}
delegationID := parseUUIDOrNil(req.DelegationID)
tenantID := parseUUIDOrNil(req.TenantID)
if delegationID == uuid.Nil || tenantID == uuid.Nil || req.FromAgentID == uuid.Nil {
return fmt.Errorf("delegation completion requires valid tenant, source agent, and delegation ID")
}
dbCtx, cancel := context.WithTimeout(
store.WithTenantID(context.Background(), tenantID),
delegateRunningPersistenceTimeout,
)
defer cancel()
err := t.taskStore.UpdateStatus(
dbCtx,
req.FromAgentID,
delegationID,
TaskStatusRunning,
nil,
0,
0,
0,
)
if err != nil {
slog.Warn("delegate.async.running_persist_failed",
"delegation_id", req.DelegationID,
"from_agent_id", req.FromAgentID,
"error", err,
)
}
return err
}
func (t *DelegateTool) updateDelegateCompletionMedia(
req DelegateRequest,
media []persistedCompletionMedia,
) error {
if t.taskStore == nil || len(media) == 0 {
return nil
}
delegationID := parseUUIDOrNil(req.DelegationID)
tenantID := parseUUIDOrNil(req.TenantID)
if delegationID == uuid.Nil || tenantID == uuid.Nil || req.FromAgentID == uuid.Nil {
return fmt.Errorf("delegation completion requires valid tenant, source agent, and delegation ID")
}
dbCtx := store.WithTenantID(context.Background(), tenantID)
if err := retryTerminalPersistence(dbCtx, func(attemptCtx context.Context) error {
return t.taskStore.UpdateMetadata(attemptCtx, req.FromAgentID, delegationID, map[string]any{
asyncCompletionMediaKey: media,
})
}); err != nil {
slog.Error("delegate.async.completion_media_persist_failed",
"delegation_id", req.DelegationID,
"from_agent_id", req.FromAgentID,
"error", err,
)
return err
}
return nil
}
func (t *DelegateTool) updateDelegateAnnouncement(req DelegateRequest, delivered bool) {
if t.taskStore == nil {
return
}
delegationID := parseUUIDOrNil(req.DelegationID)
tenantID := parseUUIDOrNil(req.TenantID)
if delegationID == uuid.Nil || tenantID == uuid.Nil || req.FromAgentID == uuid.Nil {
return
}
status := asyncCompletionDeliveryMissed
if delivered {
status = asyncCompletionDeliveryDone
}
dbCtx := store.WithTenantID(context.Background(), tenantID)
if err := retryAsyncPersistence(dbCtx, func(attemptCtx context.Context) error {
return t.taskStore.UpdateMetadata(attemptCtx, req.FromAgentID, delegationID, map[string]any{
asyncCompletionDeliveryKey: status,
})
}); err != nil {
slog.Warn("delegate.async.announcement_status_failed",
"delegation_id", req.DelegationID,
"error", err,
)
}
}
func (t *DelegateTool) executeGetCompletion(ctx context.Context, args map[string]any) *Result {
rawID, _ := args["delegation_id"].(string)
delegationID, err := uuid.Parse(rawID)
if err != nil {
return ErrorResult("delegation_id must be a valid UUID")
}
tenantID := store.TenantIDFromContext(ctx)
fromAgentID := store.AgentIDFromContext(ctx)
if tenantID == uuid.Nil || fromAgentID == uuid.Nil {
return ErrorResult("delegate result lookup requires tenant and agent context")
}
if t.taskStore == nil {
return ErrorResult("durable delegation task tracking is unavailable")
}
dbCtx := store.WithTenantID(context.WithoutCancel(ctx), tenantID)
task, err := t.taskStore.Get(dbCtx, fromAgentID, delegationID)
if err != nil {
return ErrorResult("failed to load delegation result")
}
if task == nil || completionKind(task.Metadata) != asyncCompletionKindDelegate {
return ErrorResult("delegation result not found")
}
payload := persistedCompletionPayload(task)
payload["delegation_id"] = task.ID.String()
delete(payload, "completion_id")
if target, ok := task.Metadata[delegateCompletionTargetKey].(string); ok && target != "" {
payload["agent"] = target
}
encoded, err := json.Marshal(payload)
if err != nil {
return ErrorResult("failed to encode delegation result")
}
return NewResult(string(encoded))
}