mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-18 00:15:05 +00:00
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:
1 parent
496b7ffce6
commit
bb7712a9ff
186 files changed
+18858
-1233
No files matched your search
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user