Files
tutti/packages/agent/runtimeprep/codex_directory_windows.go
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

150 lines
5.5 KiB
Go

//go:build windows
package runtimeprep
import (
"fmt"
"log/slog"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
)
// Windows symlink creation requires SeCreateSymbolicLinkPrivilege (or
// Developer Mode). A directory junction does not require that privilege and
// keeps a session-scoped CODEX_HOME connected to the user's live plugin/skill
// state without copying potentially hundreds of megabytes on every turn.
// Copy remains the last-resort fallback for filesystems that do not support
// junctions.
func exposeCodexDirectory(source, target string) error {
started := time.Now()
if err := os.Symlink(source, target); err == nil {
slog.Info("codex runtime directory exposed", "event", "agent.runtime_prep.codex_directory_exposed", "strategy", "symlink", "source", source, "target", target, "elapsedMs", time.Since(started).Milliseconds())
return nil
} else {
symlinkErr := err
if junctionErr := createCodexDirectoryJunction(source, target); junctionErr == nil {
slog.Info("codex runtime directory exposed", "event", "agent.runtime_prep.codex_directory_exposed", "strategy", "junction", "source", source, "target", target, "elapsedMs", time.Since(started).Milliseconds())
return nil
} else {
slog.Debug("codex runtime directory junction unavailable", "event", "agent.runtime_prep.codex_directory_junction_unavailable", "source", source, "target", target, "error", junctionErr)
if copyErr := copyCodexDirectory(source, target); copyErr != nil {
return fmt.Errorf("symlink failed: %v; junction failed: %v; copy failed: %w", symlinkErr, junctionErr, copyErr)
}
slog.Warn("codex runtime directory copied as fallback", "event", "agent.runtime_prep.codex_directory_copied", "source", source, "target", target, "elapsedMs", time.Since(started).Milliseconds())
return nil
}
}
}
// A writable personal Skill root must remain the same filesystem object across
// sessions. Unlike read-mostly plugin projections, this path must never fall
// back to a directory copy because a copy would silently make new Skills
// session-local again.
func exposeCodexSharedDirectory(source, target string) error {
return exposeSharedRuntimeDirectory(source, target)
}
func exposeSharedRuntimeDirectory(source, target string) error {
if err := os.Symlink(source, target); err == nil {
return nil
} else if junctionErr := createCodexDirectoryJunction(source, target); junctionErr != nil {
return fmt.Errorf("symlink failed: %v; junction failed: %w", err, junctionErr)
}
return nil
}
func sameSharedRuntimePath(left, right string) bool {
return strings.EqualFold(filepath.Clean(left), filepath.Clean(right))
}
func createCodexDirectoryJunction(source, target string) error {
// `mklink /J` creates an NTFS directory junction for an ordinary user. Keep
// the paths as separate process arguments; Go quotes paths with spaces for
// cmd.exe, while mklink receives the two exact paths.
command := exec.Command("cmd.exe", "/D", "/S", "/C", "mklink", "/J", target, source)
output, err := command.CombinedOutput()
if err != nil {
message := strings.TrimSpace(string(output))
if message == "" {
return err
}
return fmt.Errorf("%w: %s", err, message)
}
return nil
}
func copyCodexDirectory(source, target string) error {
info, err := os.Stat(source)
if err != nil {
return err
}
if !info.IsDir() {
return fmt.Errorf("source is not a directory: %s", source)
}
if err := os.MkdirAll(target, 0o700); err != nil {
return err
}
resolvedRoot, err := filepath.EvalSymlinks(source)
if err != nil {
return fmt.Errorf("resolve source root %s: %w", source, err)
}
return copyCodexDirectoryTree(source, target, filepath.Clean(resolvedRoot), map[string]bool{})
}
func copyCodexDirectoryTree(source, target, resolvedRoot string, active map[string]bool) error {
resolvedSource, err := filepath.EvalSymlinks(source)
if err != nil {
// Go's Windows resolver does not consistently re-resolve descendants
// below a junction. The subsequent os.Stat call still follows the
// junction, while the lexical path remains fenced below resolvedRoot.
resolvedSource = filepath.Clean(source)
}
resolvedSource = filepath.Clean(resolvedSource)
relative, err := filepath.Rel(resolvedRoot, resolvedSource)
if err != nil || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
return fmt.Errorf("source directory escapes plugin state root: %s", source)
}
key := strings.ToLower(resolvedSource)
if active[key] {
return fmt.Errorf("source directory contains a junction cycle: %s", source)
}
active[key] = true
defer delete(active, key)
if err := os.MkdirAll(target, 0o700); err != nil {
return err
}
entries, err := os.ReadDir(source)
if err != nil {
return fmt.Errorf("read source directory %s: %w", source, err)
}
for _, entry := range entries {
entryPath := filepath.Join(source, entry.Name())
destination := filepath.Join(target, entry.Name())
info, err := os.Stat(entryPath)
if err != nil {
return fmt.Errorf("stat source entry %s: %w", entryPath, err)
}
if info.IsDir() {
if err := copyCodexDirectoryTree(entryPath, destination, resolvedRoot, active); err != nil {
return fmt.Errorf("copy source directory %s: %w", entryPath, err)
}
continue
}
if !info.Mode().IsRegular() {
return fmt.Errorf("source contains unsupported entry: %s", entryPath)
}
content, err := os.ReadFile(entryPath)
if err != nil {
return fmt.Errorf("read source file %s: %w", entryPath, err)
}
if err := os.WriteFile(destination, content, 0o600); err != nil {
return err
}
}
return nil
}