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

137 lines
3.7 KiB
Go

package tools
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"io"
"net/http"
"os"
)
type artifactSniffWriter struct {
buf []byte
}
func (w *artifactSniffWriter) Write(p []byte) (int, error) {
if len(w.buf) < 512 {
remaining := min(512-len(w.buf), len(p))
w.buf = append(w.buf, p[:remaining]...)
}
return len(p), nil
}
func copyArtifactFile(
ctx context.Context,
sourceRoot *artifactSecureRoot,
sourcePath string,
destinationRoot *artifactSecureRoot,
destinationPath string,
maxFileBytes int64,
maxRemainingBytes int64,
destinationMode os.FileMode,
) (DelegationArtifact, error) {
if maxRemainingBytes < 0 {
return DelegationArtifact{}, artifactError(
"artifact_total_byte_limit", "copy", destinationPath, ErrArtifactLimitExceeded,
)
}
source, err := sourceRoot.openEntry(sourcePath)
if err != nil {
return DelegationArtifact{}, classifyArtifactOpenError("open_source", sourcePath, err)
}
defer source.close()
if source.kind != artifactEntryRegular {
return DelegationArtifact{}, artifactError(
"artifact_non_regular", "open_source", sourcePath, ErrArtifactNonRegular,
)
}
if source.links != 1 {
return DelegationArtifact{}, artifactError(
"artifact_hardlink", "open_source", sourcePath, ErrArtifactHardlink,
)
}
effectiveLimit := min(maxRemainingBytes, maxFileBytes)
if source.size > effectiveLimit {
return DelegationArtifact{}, artifactError(
"artifact_byte_limit", "copy", destinationPath, ErrArtifactLimitExceeded,
)
}
destination, err := destinationRoot.createFile(destinationPath, 0600)
if err != nil {
return DelegationArtifact{}, wrapArtifactFilesystemError("create_destination", destinationPath, err)
}
closeDestination := true
defer func() {
if closeDestination {
_ = destination.Close()
}
}()
hash := sha256.New()
sniff := &artifactSniffWriter{}
writer := io.MultiWriter(destination, hash, sniff)
reader := io.LimitReader(&contextArtifactReader{ctx: ctx, reader: source.file}, effectiveLimit+1)
written, err := io.Copy(writer, reader)
if err != nil {
return DelegationArtifact{}, artifactError("artifact_copy_failed", "copy", destinationPath, err)
}
if written > effectiveLimit {
return DelegationArtifact{}, artifactError(
"artifact_byte_limit", "copy", destinationPath, ErrArtifactLimitExceeded,
)
}
if err := destination.Sync(); err != nil {
return DelegationArtifact{}, artifactError(
"artifact_sync_failed", "sync_file", destinationPath, err,
)
}
if err := destination.Chmod(destinationMode); err != nil {
return DelegationArtifact{}, artifactError(
"artifact_chmod_failed", "protect_file", destinationPath, err,
)
}
if err := destination.Close(); err != nil {
return DelegationArtifact{}, artifactError(
"artifact_close_failed", "close_file", destinationPath, err,
)
}
closeDestination = false
mediaType := "application/octet-stream"
if len(sniff.buf) > 0 {
mediaType = http.DetectContentType(sniff.buf)
}
return DelegationArtifact{
Path: destinationPath,
SizeBytes: written,
SHA256: hex.EncodeToString(hash.Sum(nil)),
MediaType: mediaType,
}, nil
}
type contextArtifactReader struct {
ctx context.Context
reader io.Reader
}
func (r *contextArtifactReader) Read(p []byte) (int, error) {
if err := checkArtifactContext(r.ctx); err != nil {
return 0, err
}
return r.reader.Read(p)
}
func classifyArtifactOpenError(op, logicalPath string, err error) error {
switch {
case errors.Is(err, errArtifactSymlink):
return artifactError("artifact_symlink", op, logicalPath, ErrArtifactNonRegular)
case errors.Is(err, errArtifactReparsePoint):
return artifactError("artifact_reparse_point", op, logicalPath, ErrArtifactNonRegular)
default:
return wrapArtifactFilesystemError(op, logicalPath, err)
}
}