mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-13 10:15:48 +00:00
* 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>
391 lines
9.1 KiB
Go
391 lines
9.1 KiB
Go
//go:build linux || darwin
|
|
|
|
package tools
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"strings"
|
|
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
type artifactSecureRoot struct {
|
|
fd int
|
|
}
|
|
|
|
func openArtifactSecureRoot(hostPath string) (*artifactSecureRoot, error) {
|
|
fd, err := unix.Open(hostPath, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC, 0)
|
|
if err != nil {
|
|
return nil, translateArtifactPOSIXError(err)
|
|
}
|
|
return &artifactSecureRoot{fd: fd}, nil
|
|
}
|
|
|
|
func (r *artifactSecureRoot) close() error {
|
|
if r == nil || r.fd < 0 {
|
|
return nil
|
|
}
|
|
fd := r.fd
|
|
r.fd = -1
|
|
return unix.Close(fd)
|
|
}
|
|
|
|
func (r *artifactSecureRoot) mkdirAll(relativePath string, mode fs.FileMode) error {
|
|
components, err := artifactPathComponents(relativePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
current, err := unix.Dup(r.fd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
unix.CloseOnExec(current)
|
|
defer func() {
|
|
_ = unix.Close(current)
|
|
}()
|
|
for _, component := range components {
|
|
if err := unix.Mkdirat(current, component, uint32(mode.Perm())); err != nil && !errors.Is(err, unix.EEXIST) {
|
|
return translateArtifactPOSIXError(err)
|
|
}
|
|
next, err := unix.Openat(
|
|
current,
|
|
component,
|
|
unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC,
|
|
0,
|
|
)
|
|
if err != nil {
|
|
return translateArtifactPOSIXError(err)
|
|
}
|
|
unix.Close(current)
|
|
current = next
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *artifactSecureRoot) createSubroot(relativePath string, mode fs.FileMode) (*artifactSecureRoot, error) {
|
|
parent, name, err := r.openParent(relativePath, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer unix.Close(parent)
|
|
if err := unix.Mkdirat(parent, name, uint32(mode.Perm())); err != nil {
|
|
return nil, translateArtifactPOSIXError(err)
|
|
}
|
|
fd, err := unix.Openat(
|
|
parent,
|
|
name,
|
|
unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC,
|
|
0,
|
|
)
|
|
if err != nil {
|
|
return nil, translateArtifactPOSIXError(err)
|
|
}
|
|
return &artifactSecureRoot{fd: fd}, nil
|
|
}
|
|
|
|
func (r *artifactSecureRoot) openSubroot(relativePath string) (*artifactSecureRoot, error) {
|
|
fd, err := r.openDir(relativePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &artifactSecureRoot{fd: fd}, nil
|
|
}
|
|
|
|
func (r *artifactSecureRoot) createFile(
|
|
relativePath string,
|
|
mode fs.FileMode,
|
|
) (*os.File, error) {
|
|
parent, name, err := r.openParent(relativePath, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer unix.Close(parent)
|
|
fd, err := unix.Openat(
|
|
parent,
|
|
name,
|
|
unix.O_WRONLY|unix.O_CREAT|unix.O_EXCL|unix.O_NOFOLLOW|unix.O_CLOEXEC,
|
|
uint32(mode.Perm()),
|
|
)
|
|
if err != nil {
|
|
return nil, translateArtifactPOSIXError(err)
|
|
}
|
|
return os.NewFile(uintptr(fd), name), nil
|
|
}
|
|
|
|
func (r *artifactSecureRoot) openEntry(relativePath string) (*artifactSecureEntry, error) {
|
|
parent, name, err := r.openParent(relativePath, false)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer unix.Close(parent)
|
|
fd, err := unix.Openat(
|
|
parent,
|
|
name,
|
|
unix.O_RDONLY|unix.O_NONBLOCK|unix.O_NOFOLLOW|unix.O_CLOEXEC,
|
|
0,
|
|
)
|
|
if err != nil {
|
|
return nil, translateArtifactPOSIXError(err)
|
|
}
|
|
var stat unix.Stat_t
|
|
if err := unix.Fstat(fd, &stat); err != nil {
|
|
unix.Close(fd)
|
|
return nil, err
|
|
}
|
|
kind := artifactEntryOther
|
|
switch stat.Mode & unix.S_IFMT {
|
|
case unix.S_IFREG:
|
|
kind = artifactEntryRegular
|
|
case unix.S_IFDIR:
|
|
kind = artifactEntryDirectory
|
|
}
|
|
return &artifactSecureEntry{
|
|
file: os.NewFile(uintptr(fd), name),
|
|
kind: kind,
|
|
size: stat.Size,
|
|
links: uint64(stat.Nlink),
|
|
}, nil
|
|
}
|
|
|
|
func (r *artifactSecureRoot) readDir(relativePath string) ([]string, error) {
|
|
entry, err := r.openEntry(relativePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer entry.close()
|
|
if entry.kind != artifactEntryDirectory {
|
|
return nil, ErrArtifactNonRegular
|
|
}
|
|
|
|
names := make([]string, 0, artifactSecureReadBatchSize)
|
|
for {
|
|
dirEntries, readErr := entry.readDirBatch()
|
|
if readErr != nil && !errors.Is(readErr, io.EOF) {
|
|
return nil, readErr
|
|
}
|
|
if len(dirEntries) > artifactSecureMaxDirectoryEntries-len(names) {
|
|
return nil, ErrArtifactLimitExceeded
|
|
}
|
|
for _, dirEntry := range dirEntries {
|
|
names = append(names, dirEntry.Name())
|
|
}
|
|
if errors.Is(readErr, io.EOF) {
|
|
return names, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func (r *artifactSecureRoot) exists(relativePath string) (bool, error) {
|
|
parent, name, err := r.openParent(relativePath, false)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
defer unix.Close(parent)
|
|
var stat unix.Stat_t
|
|
err = unix.Fstatat(parent, name, &stat, unix.AT_SYMLINK_NOFOLLOW)
|
|
switch {
|
|
case err == nil:
|
|
return true, nil
|
|
case errors.Is(err, unix.ENOENT):
|
|
return false, nil
|
|
default:
|
|
return false, translateArtifactPOSIXError(err)
|
|
}
|
|
}
|
|
|
|
func (r *artifactSecureRoot) syncDir(relativePath string) error {
|
|
fd, err := r.openDir(relativePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer unix.Close(fd)
|
|
err = unix.Fsync(fd)
|
|
if errors.Is(err, unix.EINVAL) || errors.Is(err, unix.ENOTSUP) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (r *artifactSecureRoot) removeTree(relativePath string) error {
|
|
parent, name, err := r.openParent(relativePath, false)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer unix.Close(parent)
|
|
remaining := artifactSecureCleanupEntryBudget
|
|
return removeArtifactTreeAt(parent, name, 0, &remaining)
|
|
}
|
|
|
|
func removeArtifactTreeAt(parent int, name string, depth int, remaining *int) error {
|
|
if depth > artifactSecureMaxDepth {
|
|
return ErrArtifactLimitExceeded
|
|
}
|
|
if *remaining <= 0 {
|
|
return ErrArtifactLimitExceeded
|
|
}
|
|
*remaining--
|
|
var stat unix.Stat_t
|
|
if err := unix.Fstatat(parent, name, &stat, unix.AT_SYMLINK_NOFOLLOW); err != nil {
|
|
if errors.Is(err, unix.ENOENT) {
|
|
return nil
|
|
}
|
|
return translateArtifactPOSIXError(err)
|
|
}
|
|
if stat.Mode&unix.S_IFMT != unix.S_IFDIR {
|
|
return translateArtifactPOSIXError(unix.Unlinkat(parent, name, 0))
|
|
}
|
|
|
|
fd, err := unix.Openat(
|
|
parent,
|
|
name,
|
|
unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC,
|
|
0,
|
|
)
|
|
if err != nil {
|
|
return translateArtifactPOSIXError(err)
|
|
}
|
|
dir := os.NewFile(uintptr(fd), name)
|
|
for {
|
|
entries, readErr := dir.ReadDir(artifactSecureReadBatchSize)
|
|
if readErr != nil && !errors.Is(readErr, io.EOF) {
|
|
_ = dir.Close()
|
|
return readErr
|
|
}
|
|
for _, entry := range entries {
|
|
if err := removeArtifactTreeAt(fd, entry.Name(), depth+1, remaining); err != nil {
|
|
_ = dir.Close()
|
|
return err
|
|
}
|
|
}
|
|
if errors.Is(readErr, io.EOF) {
|
|
break
|
|
}
|
|
}
|
|
if err := dir.Close(); err != nil {
|
|
return err
|
|
}
|
|
return translateArtifactPOSIXError(unix.Unlinkat(parent, name, unix.AT_REMOVEDIR))
|
|
}
|
|
|
|
func (r *artifactSecureRoot) renameReplace(oldName, newName string) error {
|
|
oldPath, err := validateArtifactRelativePath(oldName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newPath, err := validateArtifactRelativePath(newName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if path.Dir(oldPath) != "." || path.Dir(newPath) != "." {
|
|
return ErrArtifactInvalidPath
|
|
}
|
|
return translateArtifactPOSIXError(unix.Renameat(r.fd, oldPath, r.fd, newPath))
|
|
}
|
|
|
|
func (r *artifactSecureRoot) openParent(
|
|
relativePath string,
|
|
create bool,
|
|
) (int, string, error) {
|
|
components, err := artifactPathComponents(relativePath)
|
|
if err != nil {
|
|
return -1, "", err
|
|
}
|
|
if len(components) == 0 {
|
|
return -1, "", ErrArtifactInvalidPath
|
|
}
|
|
parentComponents := components[:len(components)-1]
|
|
current, err := unix.Dup(r.fd)
|
|
if err != nil {
|
|
return -1, "", err
|
|
}
|
|
unix.CloseOnExec(current)
|
|
for _, component := range parentComponents {
|
|
if create {
|
|
if err := unix.Mkdirat(current, component, 0750); err != nil && !errors.Is(err, unix.EEXIST) {
|
|
unix.Close(current)
|
|
return -1, "", translateArtifactPOSIXError(err)
|
|
}
|
|
}
|
|
next, err := unix.Openat(
|
|
current,
|
|
component,
|
|
unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC,
|
|
0,
|
|
)
|
|
if err != nil {
|
|
unix.Close(current)
|
|
return -1, "", translateArtifactPOSIXError(err)
|
|
}
|
|
unix.Close(current)
|
|
current = next
|
|
}
|
|
return current, components[len(components)-1], nil
|
|
}
|
|
|
|
func (r *artifactSecureRoot) openDir(relativePath string) (int, error) {
|
|
if relativePath == "." {
|
|
fd, err := unix.Dup(r.fd)
|
|
if err == nil {
|
|
unix.CloseOnExec(fd)
|
|
}
|
|
return fd, err
|
|
}
|
|
components, err := artifactPathComponents(relativePath)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
current, err := unix.Dup(r.fd)
|
|
if err != nil {
|
|
return -1, err
|
|
}
|
|
unix.CloseOnExec(current)
|
|
for _, component := range components {
|
|
next, err := unix.Openat(
|
|
current,
|
|
component,
|
|
unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC,
|
|
0,
|
|
)
|
|
if err != nil {
|
|
unix.Close(current)
|
|
return -1, translateArtifactPOSIXError(err)
|
|
}
|
|
unix.Close(current)
|
|
current = next
|
|
}
|
|
return current, nil
|
|
}
|
|
|
|
func artifactPathComponents(relativePath string) ([]string, error) {
|
|
if relativePath == "." {
|
|
return nil, nil
|
|
}
|
|
clean, err := validateArtifactRelativePath(relativePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return strings.Split(clean, "/"), nil
|
|
}
|
|
|
|
func translateArtifactPOSIXError(err error) error {
|
|
if errors.Is(err, unix.ELOOP) {
|
|
return fmtArtifactJoined(errArtifactSymlink, err)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func fmtArtifactJoined(classification, cause error) error {
|
|
return errors.Join(classification, cause)
|
|
}
|
|
|
|
func isArtifactAlreadyExists(err error) bool {
|
|
return errors.Is(err, unix.EEXIST) || errors.Is(err, ErrArtifactPublishConflict)
|
|
}
|