Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

271 lines
8.0 KiB
Go

package runtime
import (
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
market "github.com/tutti-os/tutti/packages/connector/daemon/core"
)
const preparedReceiptFilename = ".tutti-connector-receipt.json"
// ExecutionSnapshotter copies a verified prepared tree into an immutable,
// route-scoped snapshot so updates cannot mutate a running connector.
type ExecutionSnapshotter struct{ root string }
func NewExecutionSnapshotter(root string) (*ExecutionSnapshotter, error) {
if !filepath.IsAbs(strings.TrimSpace(root)) {
return nil, errors.New("connector execution snapshot root must be absolute")
}
return &ExecutionSnapshotter{root: filepath.Clean(root)}, nil
}
// CleanupOrphans removes execution snapshots left by a previous host process.
// It must run before runtime routes are restored because active routes own
// their snapshots in memory and remove them during normal retirement.
func (snapshotter *ExecutionSnapshotter) CleanupOrphans() error {
if snapshotter == nil {
return errors.New("connector execution snapshotter is unavailable")
}
if err := os.MkdirAll(snapshotter.root, 0o700); err != nil {
return err
}
stateRoot, err := filepath.EvalSymlinks(snapshotter.root)
if err != nil {
return err
}
parent := filepath.Join(stateRoot, "execution-snapshots")
if err := os.MkdirAll(parent, 0o700); err != nil {
return err
}
entries, err := os.ReadDir(parent)
if err != nil {
return err
}
var cleanupErrors []error
for _, entry := range entries {
name := entry.Name()
if !strings.HasPrefix(name, ".staging-") && !strings.HasSuffix(name, ".ready") {
continue
}
if err := snapshotter.remove(filepath.Join(parent, name), true); err != nil {
cleanupErrors = append(cleanupErrors, err)
}
}
return errors.Join(cleanupErrors...)
}
func (snapshotter *ExecutionSnapshotter) Create(prepared market.PreparedArtifactReceipt, executableEntries ...string) (string, error) {
if snapshotter == nil || strings.TrimSpace(prepared.InventoryDigest) == "" {
return "", errors.New("prepared connector inventory digest is missing")
}
if err := os.MkdirAll(snapshotter.root, 0o700); err != nil {
return "", err
}
stateRoot, err := filepath.EvalSymlinks(snapshotter.root)
if err != nil {
return "", err
}
parent := filepath.Join(stateRoot, "execution-snapshots")
if err := os.MkdirAll(parent, 0o700); err != nil {
return "", err
}
if err := os.Chmod(parent, 0o700); err != nil {
return "", err
}
staging, err := os.MkdirTemp(parent, ".staging-")
if err != nil {
return "", err
}
cleanup := true
defer func() {
if cleanup {
_ = snapshotter.remove(staging, true)
}
}()
if err := copyExecutionTree(prepared.PreparedPath, staging); err != nil {
return "", err
}
digest, err := ExecutionInventoryDigest(staging)
if err != nil {
return "", err
}
if digest != prepared.InventoryDigest {
return "", errors.New("connector execution snapshot does not match verified inventory")
}
for _, relative := range executableEntries {
executable, entryErr := PreparedEntrypoint(staging, relative)
if entryErr != nil {
return "", fmt.Errorf("prepare connector artifact-native entrypoint: %w", entryErr)
}
if err := os.Chmod(executable, 0o500); err != nil {
return "", err
}
}
target := staging + ".ready"
if err := os.Rename(staging, target); err != nil {
return "", err
}
staging = target
readyExecutablePaths := make(map[string]struct{}, len(executableEntries))
for _, relative := range executableEntries {
readyExecutablePaths[filepath.Join(target, filepath.FromSlash(relative))] = struct{}{}
}
if err := makeExecutionTreeReadOnly(target, readyExecutablePaths); err != nil {
return "", err
}
cleanup = false
return target, nil
}
func (snapshotter *ExecutionSnapshotter) Remove(root string) error {
return snapshotter.remove(root, false)
}
func (snapshotter *ExecutionSnapshotter) remove(root string, allowStaging bool) error {
if snapshotter == nil {
return errors.New("connector execution snapshotter is unavailable")
}
if strings.TrimSpace(root) == "" {
return nil
}
stateRoot, err := filepath.EvalSymlinks(snapshotter.root)
if err != nil {
return err
}
parent := filepath.Join(stateRoot, "execution-snapshots")
parentReal, err := filepath.EvalSymlinks(parent)
if err != nil {
return err
}
clean := filepath.Clean(root)
base := filepath.Base(clean)
validName := strings.HasSuffix(base, ".ready") || (allowStaging && strings.HasPrefix(base, ".staging-"))
info, statErr := os.Lstat(clean)
if !validName || filepath.Dir(clean) != parentReal || statErr != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
return errors.New("connector execution snapshot removal target is invalid")
}
_ = filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error {
if walkErr == nil && entry.IsDir() {
_ = os.Chmod(path, 0o700)
}
return nil
})
return os.RemoveAll(root)
}
func copyExecutionTree(source, destination string) error {
return filepath.WalkDir(source, func(path string, _ os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
relative, err := filepath.Rel(source, path)
if err != nil || relative == "." {
return err
}
target := filepath.Join(destination, relative)
info, err := os.Lstat(path)
if err != nil {
return err
}
if info.Mode()&os.ModeSymlink != 0 {
return errors.New("prepared connector snapshot contains a symlink")
}
if info.IsDir() {
return os.Mkdir(target, 0o700)
}
if !info.Mode().IsRegular() {
return errors.New("prepared connector snapshot contains an unsupported file type")
}
sourceFile, err := os.Open(path)
if err != nil {
return err
}
openedInfo, statErr := sourceFile.Stat()
if statErr != nil || !openedInfo.Mode().IsRegular() {
_ = sourceFile.Close()
return errors.New("prepared connector file changed during snapshot")
}
targetFile, err := os.OpenFile(target, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
if err != nil {
_ = sourceFile.Close()
return err
}
_, copyErr := io.Copy(targetFile, sourceFile)
syncErr := targetFile.Sync()
return errors.Join(copyErr, syncErr, targetFile.Close(), sourceFile.Close())
})
}
// ExecutionInventoryDigest computes the same verified tree identity used by
// artifact preparation and immutable execution snapshots.
func ExecutionInventoryDigest(root string) (string, error) {
hash := sha256.New()
err := filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
relative, err := filepath.Rel(root, path)
if err != nil || relative == "." || relative == preparedReceiptFilename {
return err
}
info, err := entry.Info()
if err != nil {
return err
}
if entry.Type()&os.ModeSymlink != 0 || (!entry.IsDir() && !info.Mode().IsRegular()) {
return errors.New("connector execution snapshot contains an unsupported file type")
}
_, _ = io.WriteString(hash, filepath.ToSlash(relative))
_, _ = hash.Write([]byte{0})
if entry.IsDir() {
_, _ = hash.Write([]byte("dir\x00"))
return nil
}
_, _ = io.WriteString(hash, fmt.Sprintf("file\x00%d\x00", info.Size()))
file, err := os.Open(path)
if err != nil {
return err
}
_, copyErr := io.Copy(hash, file)
return errors.Join(copyErr, file.Close())
})
if err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}
func makeExecutionTreeReadOnly(root string, executablePaths map[string]struct{}) error {
var directories []string
err := filepath.WalkDir(root, func(path string, entry os.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if entry.IsDir() {
directories = append(directories, path)
return nil
}
mode := os.FileMode(0o400)
if _, executable := executablePaths[filepath.Clean(path)]; executable {
mode = 0o500
}
return os.Chmod(path, mode)
})
if err != nil {
return err
}
for index := len(directories) - 1; index >= 0; index-- {
if err := os.Chmod(directories[index], 0o500); err != nil {
return err
}
}
return nil
}