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>
This commit is contained in:
Duc Nguyenandntduc authored and GitHub committed 2026-07-30 14:17:40 +07:00
1 parent 496b7ffce6
commit bb7712a9ff
186 files changed
+18858 -1233

No files matched your search

+51 -4
View File
@@ -5,9 +5,12 @@ import (
"fmt"
"log/slog"
"os"
"path"
"path/filepath"
"strings"
"github.com/nextlevelbuilder/goclaw/internal/providers"
"github.com/nextlevelbuilder/goclaw/internal/tracing"
usagecaps "github.com/nextlevelbuilder/goclaw/internal/usage/caps"
)
@@ -123,6 +126,9 @@ func (t *ReadDocumentTool) Execute(ctx context.Context, args map[string]any) *Re
}
mediaID, _ := args["media_id"].(string)
docPathArg, _ := args["path"].(string)
if strings.TrimSpace(mediaID) != "" && strings.TrimSpace(docPathArg) != "" {
return ErrorResult("Specify either media_id or path, not both.")
}
// Resolve document file path from MediaRefs in context.
docPath, docMime, err := t.resolveDocumentFile(ctx, mediaID, docPathArg)
@@ -130,19 +136,24 @@ func (t *ReadDocumentTool) Execute(ctx context.Context, args map[string]any) *Re
return ErrorResult(err.Error())
}
slog.Info("read_document: resolved file", "path", docPath, "mime", docMime, "media_id", mediaID)
displayPath := logicalDocumentDisplayPath(ctx, docPath, docPathArg)
slog.Info("read_document: resolved file", "path", displayPath, "mime", docMime, "media_id", mediaID)
if isArchiveDocumentPath(docPath) {
return NewResult(fmt.Sprintf(
"Archive file available at %s. read_document does not analyze archive containers directly. Use exec to inspect or extract it, for example: unzip -l %q or unzip -q %q -d <output-dir>, then use list_files/read_file on extracted files.",
docPath, docPath, docPath,
displayPath, displayPath, displayPath,
))
}
// Read document file.
data, err := os.ReadFile(docPath)
if err != nil {
return ErrorResult(fmt.Sprintf("Failed to read document file: %v", err))
slog.Warn("read_document: file read failed",
"path", displayPath,
"error", tracing.RedactText(ctx, err.Error()),
)
return ErrorResult(fmt.Sprintf("Failed to read document file %s.", displayPath))
}
slog.Info("read_document: file loaded", "size_bytes", len(data))
if len(data) > documentMaxBytes {
@@ -169,7 +180,11 @@ func (t *ReadDocumentTool) Execute(ctx context.Context, args map[string]any) *Re
// handing it to a subprocess. A rejection routes to vision rather than
// erroring, preserving today's behavior for MediaRef-derived paths.
if safePath, verr := t.validateExecPath(ctx, docPath); verr != nil {
slog.Warn("security.read_document_local_path_rejected", "path", docPath, "reason", verr.Error())
slog.Warn(
"security.read_document_local_path_rejected",
"path", displayPath,
"reason", tracing.RedactText(ctx, verr.Error()),
)
} else if text, err := t.localParser.Extract(ctx, safePath, docMime); err == nil {
slog.Info("read_document: local extraction hit", "mime", docMime, "bytes", len(text))
return NewResult(text) // no Provider/Model/Usage => no LLM spend
@@ -203,6 +218,35 @@ func (t *ReadDocumentTool) Execute(ctx context.Context, args map[string]any) *Re
return result
}
// logicalDocumentDisplayPath returns only model-safe paths. Resolved host
// paths remain runtime-only even when tracing redaction is not configured.
func logicalDocumentDisplayPath(ctx context.Context, resolvedPath, requestedPath string) string {
requested := path.Clean(strings.ReplaceAll(strings.TrimSpace(requestedPath), "\\", "/"))
if requestedPath != "" && requested != "." && requested != ".." &&
!strings.HasPrefix(requested, "../") && !path.IsAbs(requested) {
return requested
}
if inputRoot := DelegationArtifactInputsFromCtx(ctx); inputRoot != "" {
if relative, ok := artifactRelativeToRoot(inputRoot, resolvedPath); ok &&
relative != "." {
return path.Join("inputs", relative)
}
}
if workspace := ToolWorkspaceFromCtx(ctx); workspace != "" {
if relative, ok := artifactRelativeToRoot(workspace, resolvedPath); ok &&
relative != "." {
return relative
}
}
name := filepath.Base(resolvedPath)
if name == "." || name == string(filepath.Separator) || name == "" {
return "document"
}
return name
}
// validateExecPath confirms a resolved document path is workspace-confined
// before it is passed to a local extractor subprocess. It reuses the same
// allow/deny resolution as the explicit-path argument branch so MediaRef-derived
@@ -217,5 +261,8 @@ func (t *ReadDocumentTool) validateExecPath(ctx context.Context, path string) (s
if err := checkDeniedPath(resolved, workspace, nil); err != nil {
return "", err
}
if err := ValidateRegularFileForRead(resolved); err != nil {
return "", fmt.Errorf("document path is not a safe regular file: %w", err)
}
return resolved, nil
}