Files
goclaw/internal/tools/async_completion_media.go
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

81 lines
2.0 KiB
Go

package tools
import (
"encoding/json"
"path/filepath"
"strings"
"github.com/nextlevelbuilder/goclaw/internal/bus"
)
const asyncCompletionMediaKey = "completion_media"
type persistedCompletionMedia struct {
Path string `json:"path"`
MimeType string `json:"mime_type,omitempty"`
Filename string `json:"filename,omitempty"`
Caption string `json:"caption,omitempty"`
}
// completionMediaDescriptors converts runtime host paths into workspace-safe
// logical paths. Outside-workspace media is intentionally omitted.
func completionMediaDescriptors(
media []bus.MediaFile,
workspace, logicalPrefix string,
) []persistedCompletionMedia {
if len(media) == 0 || workspace == "" {
return nil
}
descriptors := make([]persistedCompletionMedia, 0, len(media))
for _, item := range media {
rawPath := item.Path
if !filepath.IsAbs(rawPath) {
rawPath = filepath.Join(workspace, rawPath)
}
absolutePath, err := filepath.Abs(rawPath)
if err != nil {
continue
}
relativePath, err := filepath.Rel(workspace, absolutePath)
if err != nil {
continue
}
logicalPath := filepath.ToSlash(relativePath)
if logicalPrefix != "" {
logicalPath = strings.TrimSuffix(logicalPrefix, "/") + "/" + logicalPath
}
normalized, err := validateArtifactRelativePath(logicalPath)
if err != nil {
continue
}
descriptors = append(descriptors, persistedCompletionMedia{
Path: normalized,
MimeType: item.MimeType,
Filename: item.Filename,
Caption: item.Caption,
})
}
return descriptors
}
func persistedCompletionMediaPayload(raw any) []persistedCompletionMedia {
encoded, err := json.Marshal(raw)
if err != nil {
return nil
}
var decoded []persistedCompletionMedia
if err := json.Unmarshal(encoded, &decoded); err != nil {
return nil
}
safe := decoded[:0]
for _, item := range decoded {
normalized, err := validateArtifactRelativePath(item.Path)
if err != nil {
continue
}
item.Path = normalized
safe = append(safe, item)
}
return safe
}