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

736 lines
24 KiB
Go

package runtimeprep
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"os"
"path"
"path/filepath"
"regexp"
"slices"
"sort"
"strings"
"sync"
)
var extensionRuntimeEnvName = regexp.MustCompile(`^[A-Z_][A-Z0-9_]*$`)
var extensionRuntimeSharedDirsMu sync.Mutex
type ExtensionRuntimePreparer struct{}
func (ExtensionRuntimePreparer) Provider() string {
return ""
}
func (ExtensionRuntimePreparer) Prepare(ctx context.Context, input ProviderPrepareInput) (ProviderPrepareResult, error) {
if input.ExtensionRuntimePrep == nil {
return InstructionFilePreparer{}.Prepare(ctx, input)
}
if err := ValidateExtensionRuntimePrep(*input.ExtensionRuntimePrep); err != nil {
return ProviderPrepareResult{}, err
}
if err := writeExtensionRuntimeInstructions(input); err != nil {
return ProviderPrepareResult{}, err
}
if input.ExtensionRuntimePrep.Home == nil {
skillRoots, err := cwdExtensionSkillRoots(input.ExtensionSkillRoots)
if err != nil {
return ProviderPrepareResult{}, err
}
if err := materializeExtensionRuntimeSkills(input, skillRoots, true); err != nil {
return ProviderPrepareResult{}, err
}
return ProviderPrepareResult{Cwd: input.Cwd}, nil
}
env, err := prepareExtensionRuntimeHome(input, *input.ExtensionRuntimePrep.Home)
if err != nil {
return ProviderPrepareResult{}, err
}
return ProviderPrepareResult{
Cwd: input.Cwd,
Env: []string{env},
}, nil
}
func writeExtensionRuntimeInstructions(input ProviderPrepareInput) error {
fileName := strings.TrimSpace(input.ExtensionRuntimePrep.InstructionsFile)
if fileName == "" {
fileName = "AGENTS.md"
}
path := filepath.Join(input.Cwd, fileName)
policy, err := tuttiCLIPolicy(input.PrepareInput)
if err != nil {
return err
}
writeResult, err := input.Store.WriteManagedBlock(path, policy)
if err != nil {
return err
}
if input.Manifest != nil {
input.Manifest.RecordManagedFile(path, "provider-instructions", writeResult.Created)
}
return nil
}
func prepareExtensionRuntimeHome(input ProviderPrepareInput, home ExtensionRuntimeHome) (string, error) {
sessionHome := filepath.Join(input.RuntimeRoot, filepath.FromSlash(strings.TrimSpace(home.DirName)))
if err := os.MkdirAll(sessionHome, 0o700); err != nil {
return "", fmt.Errorf("create extension runtime home: %w", err)
}
sourceHome := resolveExtensionRuntimeSourceHome(home)
if err := exposeExtensionRuntimeSharedDirs(input, sourceHome, sessionHome, home); err != nil {
return "", err
}
userConfig, err := copyExtensionRuntimeHomeFiles(sourceHome, sessionHome, home)
if err != nil {
return "", err
}
externalDirs, err := extensionRuntimeExternalDirs(input, sourceHome, home)
if err != nil {
return "", err
}
if err := writeExtensionRuntimeConfig(filepath.Join(sessionHome, filepath.FromSlash(home.ConfigFile)), userConfig, externalDirs, home); err != nil {
return "", err
}
if err := prepareExtensionRTKIntegration(input, sessionHome, home); err != nil {
return "", err
}
if input.Manifest != nil {
input.Manifest.RecordManagedFile(sessionHome, "provider-extension-home", true)
}
return strings.TrimSpace(home.EnvVar) + "=" + sessionHome, nil
}
// exposeExtensionRuntimeSharedDirs keeps explicitly declared mutable runtime
// directories stable across otherwise isolated session homes. This is useful
// for provider-owned caches or verified helper binaries that would otherwise
// be fetched again for every fresh session. The signed profile selects only
// relative paths beneath its ordinary source home; runtimeprep remains
// provider-neutral.
func exposeExtensionRuntimeSharedDirs(input ProviderPrepareInput, sourceHome, sessionHome string, home ExtensionRuntimeHome) error {
if len(home.SharedDirs) == 0 {
return nil
}
if strings.TrimSpace(sourceHome) == "" {
return nil
}
// Preparation can run concurrently for multiple sessions in one daemon.
// Serialize adoption and projection so two first sessions cannot race while
// creating the same stable provider-owned directory.
extensionRuntimeSharedDirsMu.Lock()
defer extensionRuntimeSharedDirsMu.Unlock()
for _, declared := range home.SharedDirs {
rel := filepath.Clean(filepath.FromSlash(strings.TrimSpace(declared)))
source := filepath.Join(sourceHome, rel)
target := filepath.Join(sessionHome, rel)
seedExtensionRuntimeSharedDirFromLegacySessions(input, home, rel, source)
if err := exposeExtensionRuntimeSharedDir(source, target); err != nil {
return fmt.Errorf("expose extension runtime shared dir %s: %w", declared, err)
}
}
return nil
}
type extensionRuntimeLegacySharedDir struct {
agentSessionID string
path string
updatedAt int64
}
// seedExtensionRuntimeSharedDirFromLegacySessions is an upgrade bridge for
// sessions created before sharedDirs existed. Those sessions may already hold
// an expensive provider-owned helper or cache while the newly created stable
// source directory is still empty. Only same-provider runtime roots with a
// manifest-owned extension home are eligible; symlink projections created by
// the new scheme are ignored.
func seedExtensionRuntimeSharedDirFromLegacySessions(
input ProviderPrepareInput,
home ExtensionRuntimeHome,
sharedRel string,
stableDir string,
) {
if extensionRuntimeSharedDirHasEntries(stableDir) {
return
}
candidates := extensionRuntimeLegacySharedDirCandidates(input, home, sharedRel)
for _, candidate := range candidates {
if err := mergeExtensionRuntimeSharedDir(candidate.path, stableDir); err != nil {
slog.Warn("extension runtime legacy shared directory migration skipped",
"event", "agent.runtime_prepare.extension_shared_dir.legacy_migration_skipped",
"provider", strings.TrimSpace(input.Provider),
"agent_session_id", strings.TrimSpace(input.AgentSessionID),
"legacy_agent_session_id", candidate.agentSessionID,
"shared_dir", filepath.ToSlash(sharedRel),
"error", err,
)
continue
}
if !extensionRuntimeSharedDirHasEntries(stableDir) {
continue
}
slog.Info("extension runtime legacy shared directory migrated",
"event", "agent.runtime_prepare.extension_shared_dir.legacy_migrated",
"provider", strings.TrimSpace(input.Provider),
"agent_session_id", strings.TrimSpace(input.AgentSessionID),
"legacy_agent_session_id", candidate.agentSessionID,
"shared_dir", filepath.ToSlash(sharedRel),
)
return
}
}
func extensionRuntimeSharedDirHasEntries(dir string) bool {
entries, err := os.ReadDir(dir)
return err == nil && len(entries) > 0
}
func extensionRuntimeLegacySharedDirCandidates(
input ProviderPrepareInput,
home ExtensionRuntimeHome,
sharedRel string,
) []extensionRuntimeLegacySharedDir {
currentRoot := filepath.Clean(strings.TrimSpace(input.RuntimeRoot))
runsRoot := filepath.Dir(currentRoot)
entries, err := os.ReadDir(runsRoot)
if err != nil {
return nil
}
provider := strings.TrimSpace(input.Provider)
homeRel := filepath.Clean(filepath.FromSlash(strings.TrimSpace(home.DirName)))
result := make([]extensionRuntimeLegacySharedDir, 0)
for _, entry := range entries {
if !entry.IsDir() || entry.Type()&os.ModeSymlink != 0 {
continue
}
candidateRoot := filepath.Join(runsRoot, entry.Name())
if sameSharedRuntimePath(candidateRoot, currentRoot) {
continue
}
content, err := os.ReadFile(filepath.Join(candidateRoot, SidecarManifestFileName))
if err != nil {
continue
}
var manifest Manifest
if json.Unmarshal(content, &manifest) != nil || strings.TrimSpace(manifest.Provider) != provider {
continue
}
if !sameSharedRuntimePath(strings.TrimSpace(manifest.RuntimeRoot), candidateRoot) {
continue
}
candidateHome := filepath.Join(candidateRoot, homeRel)
if !extensionRuntimeManifestOwnsHome(manifest, candidateHome) {
continue
}
candidatePath := filepath.Join(candidateHome, sharedRel)
info, err := os.Lstat(candidatePath)
if err != nil || info.Mode()&os.ModeSymlink != 0 || !info.IsDir() {
continue
}
result = append(result, extensionRuntimeLegacySharedDir{
agentSessionID: strings.TrimSpace(manifest.AgentSessionID),
path: candidatePath,
updatedAt: manifest.UpdatedAtUnixMS,
})
}
sort.SliceStable(result, func(i, j int) bool {
return result[i].updatedAt > result[j].updatedAt
})
return result
}
func extensionRuntimeManifestOwnsHome(manifest Manifest, homePath string) bool {
for _, managed := range manifest.ManagedFiles {
if strings.TrimSpace(managed.Kind) == "provider-extension-home" &&
sameSharedRuntimePath(strings.TrimSpace(managed.Path), homePath) {
return true
}
}
return false
}
func exposeExtensionRuntimeSharedDir(source, target string) error {
if same, err := sameResolvedPath(source, target); err == nil && same {
return nil
}
targetInfo, targetErr := os.Lstat(target)
if targetErr != nil && !os.IsNotExist(targetErr) {
return fmt.Errorf("inspect session directory: %w", targetErr)
}
if targetErr == nil && targetInfo.Mode()&os.ModeSymlink != 0 {
return errors.New("session directory points at an unexpected location")
}
if targetErr == nil && !targetInfo.IsDir() {
return errors.New("session path is not a directory")
}
sourceInfo, sourceErr := os.Stat(source)
if sourceErr != nil && !os.IsNotExist(sourceErr) {
return fmt.Errorf("inspect stable directory: %w", sourceErr)
}
if sourceErr == nil && !sourceInfo.IsDir() {
return errors.New("stable path is not a directory")
}
if targetErr == nil && os.IsNotExist(sourceErr) {
if err := os.MkdirAll(filepath.Dir(source), 0o700); err != nil {
return fmt.Errorf("create stable directory parent: %w", err)
}
if err := os.Rename(target, source); err == nil {
targetErr = os.ErrNotExist
sourceErr = nil
} else if err := mergeExtensionRuntimeSharedDir(target, source); err != nil {
return err
}
}
if os.IsNotExist(sourceErr) {
if err := os.MkdirAll(source, 0o700); err != nil {
return fmt.Errorf("create stable directory: %w", err)
}
}
if targetErr == nil {
if err := mergeExtensionRuntimeSharedDir(target, source); err != nil {
return err
}
if err := os.RemoveAll(target); err != nil {
return fmt.Errorf("remove adopted session directory: %w", err)
}
}
if err := os.MkdirAll(filepath.Dir(target), 0o700); err != nil {
return fmt.Errorf("create session directory parent: %w", err)
}
return exposeSharedRuntimeDirectory(source, target)
}
func sameResolvedPath(left, right string) (bool, error) {
resolvedLeft, err := filepath.EvalSymlinks(left)
if err != nil {
return false, err
}
resolvedRight, err := filepath.EvalSymlinks(right)
if err != nil {
return false, err
}
return sameSharedRuntimePath(resolvedLeft, resolvedRight), nil
}
func mergeExtensionRuntimeSharedDir(source, target string) error {
info, err := os.Stat(source)
if err != nil {
return fmt.Errorf("inspect session shared directory: %w", err)
}
if !info.IsDir() {
return errors.New("session shared path is not a directory")
}
if err := os.MkdirAll(target, 0o700); err != nil {
return fmt.Errorf("create stable shared directory: %w", err)
}
entries, err := os.ReadDir(source)
if err != nil {
return fmt.Errorf("read session shared directory: %w", err)
}
for _, entry := range entries {
src := filepath.Join(source, entry.Name())
dst := filepath.Join(target, entry.Name())
entryInfo, err := os.Lstat(src)
if err != nil {
return fmt.Errorf("inspect session shared entry: %w", err)
}
if entryInfo.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("session shared directory contains a symlink: %s", src)
}
if entryInfo.IsDir() {
if err := mergeExtensionRuntimeSharedDir(src, dst); err != nil {
return err
}
continue
}
if !entryInfo.Mode().IsRegular() {
return fmt.Errorf("session shared directory contains an unsupported entry: %s", src)
}
if _, err := os.Stat(dst); err == nil {
continue
} else if !os.IsNotExist(err) {
return fmt.Errorf("inspect stable shared entry: %w", err)
}
content, err := os.ReadFile(src)
if err != nil {
return fmt.Errorf("read session shared entry: %w", err)
}
if err := os.WriteFile(dst, content, entryInfo.Mode().Perm()); err != nil {
return fmt.Errorf("write stable shared entry: %w", err)
}
}
return nil
}
const hermesRTKPluginPython = `"""Session-scoped RTK command rewriting for Hermes."""
import shutil
import subprocess
import sys
def register(ctx):
if shutil.which("rtk") is None:
print("rtk: hermes plugin disabled; executable not found", file=sys.stderr)
return
ctx.register_hook("pre_tool_call", _pre_tool_call)
def _pre_tool_call(tool_name=None, args=None, **_kwargs):
if tool_name != "terminal" or not isinstance(args, dict):
return
command = args.get("command")
if not isinstance(command, str) or not command.strip():
return
try:
result = subprocess.run(
["rtk", "rewrite", command],
shell=False,
timeout=2,
capture_output=True,
text=True,
)
except Exception as exc:
print(f"rtk: hermes rewrite failed: {exc}", file=sys.stderr)
return
if result.returncode not in {0, 3}:
return
rewritten = result.stdout.strip()
if rewritten and rewritten != command:
args["command"] = rewritten
`
const hermesRTKPluginManifest = `name: rtk-rewrite
version: "0.1.0"
description: Rewrite Hermes terminal commands through session-scoped RTK.
author: Tutti
hooks:
- pre_tool_call
provides_hooks:
- pre_tool_call
`
func prepareExtensionRTKIntegration(input ProviderPrepareInput, sessionHome string, home ExtensionRuntimeHome) error {
if !input.RTKSaverMode || !strings.EqualFold(strings.TrimSpace(input.Provider), "acp:hermes") {
return nil
}
pluginDir := filepath.Join(sessionHome, "plugins", "rtk-rewrite")
if err := os.MkdirAll(pluginDir, 0o700); err != nil {
return fmt.Errorf("create Hermes RTK plugin directory: %w", err)
}
if err := os.WriteFile(filepath.Join(pluginDir, "__init__.py"), []byte(hermesRTKPluginPython), 0o600); err != nil {
return fmt.Errorf("write Hermes RTK plugin: %w", err)
}
if err := os.WriteFile(filepath.Join(pluginDir, "plugin.yaml"), []byte(hermesRTKPluginManifest), 0o600); err != nil {
return fmt.Errorf("write Hermes RTK plugin manifest: %w", err)
}
configFile := strings.TrimSpace(home.ConfigFile)
if configFile == "" {
return errors.New("hermes RTK integration requires a session config file")
}
configPath := filepath.Join(sessionHome, filepath.FromSlash(configFile))
config, err := os.ReadFile(configPath)
if err != nil && !os.IsNotExist(err) {
return fmt.Errorf("read Hermes session config for RTK plugin: %w", err)
}
merged, err := mergeYAMLStringList(string(config), []string{"plugins", "enabled"}, []string{"rtk-rewrite"})
if err != nil {
return fmt.Errorf("enable Hermes RTK plugin: %w", err)
}
if err := os.WriteFile(configPath, []byte(merged), 0o600); err != nil {
return fmt.Errorf("write Hermes session config with RTK plugin: %w", err)
}
return nil
}
func resolveExtensionRuntimeSourceHome(home ExtensionRuntimeHome) string {
if sourceEnv := strings.TrimSpace(home.SourceEnvVar); sourceEnv != "" {
if v := strings.TrimSpace(os.Getenv(sourceEnv)); v != "" {
return v
}
}
rel := strings.TrimSpace(home.SourceDefaultRel)
if rel == "" {
return ""
}
candidates := []string{}
if platformHome := extensionRuntimePlatformSourceHome(rel); platformHome != "" {
candidates = appendUniquePath(candidates, platformHome)
}
if userHome, err := os.UserHomeDir(); err == nil && userHome != "" {
candidates = appendUniquePath(candidates, filepath.Join(userHome, filepath.FromSlash(rel)))
}
for _, candidate := range candidates {
info, err := os.Stat(candidate)
if err == nil && info.IsDir() {
return candidate
}
if err != nil && !os.IsNotExist(err) {
return candidate
}
}
if len(candidates) > 0 {
// Preserve the original default even when it has not been created yet.
return candidates[0]
}
return ""
}
func copyExtensionRuntimeHomeFiles(sourceHome string, sessionHome string, home ExtensionRuntimeHome) ([]byte, error) {
var config []byte
if sourceHome == "" {
return nil, nil
}
configFile := filepath.Clean(filepath.FromSlash(strings.TrimSpace(home.ConfigFile)))
if configFile != "." && configFile != "" {
data, err := os.ReadFile(filepath.Join(sourceHome, configFile))
if err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("read extension runtime %s: %w", configFile, err)
}
if err == nil {
config = data
}
}
for _, file := range home.CopyFiles {
name := filepath.Clean(filepath.FromSlash(strings.TrimSpace(file)))
if name == configFile {
continue
}
src := filepath.Join(sourceHome, name)
if err := copyExtensionRuntimeHomeFile(src, filepath.Join(sessionHome, name)); err != nil {
return nil, err
}
}
return config, nil
}
func copyExtensionRuntimeHomeFile(src string, dst string) error {
data, err := os.ReadFile(src)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return fmt.Errorf("read extension runtime %s: %w", filepath.Base(src), err)
}
if err := os.MkdirAll(filepath.Dir(dst), 0o700); err != nil {
return fmt.Errorf("create extension runtime home subdir: %w", err)
}
return os.WriteFile(dst, data, 0o600)
}
func extensionRuntimeExternalDirs(input ProviderPrepareInput, sourceHome string, home ExtensionRuntimeHome) ([]string, error) {
externalDirs := []string{}
if home.IncludeSkillRoots {
skillRoots, err := extensionRuntimeSkillRoots(input, input.ExtensionSkillRoots)
if err != nil {
return nil, err
}
if err := materializeExtensionRuntimeSkills(input, skillRoots, false); err != nil {
return nil, err
}
for _, root := range skillRoots {
externalDirs = appendUniquePath(externalDirs, root)
}
}
if home.IncludeUserHomeDir && sourceHome != "" {
userSkillDir := strings.TrimSpace(home.UserHomeSkillDir)
if userSkillDir == "" {
userSkillDir = "skills"
}
globalSkills := filepath.Join(sourceHome, filepath.FromSlash(userSkillDir))
if info, err := os.Stat(globalSkills); err == nil && info.IsDir() {
externalDirs = appendUniquePath(externalDirs, globalSkills)
} else if err != nil && !os.IsNotExist(err) {
return nil, fmt.Errorf("inspect extension runtime user skill dir: %w", err)
}
}
return externalDirs, nil
}
func extensionRuntimeSkillRoots(input ProviderPrepareInput, declaredRoots []string) ([]string, error) {
roots := make([]string, 0, len(declaredRoots))
for _, root := range declaredRoots {
root = strings.TrimSpace(root)
if root == "" {
continue
}
if err := validateExtensionRuntimeRelPath(root, "extension runtime skill root"); err != nil {
return nil, err
}
root = filepath.Clean(filepath.FromSlash(root))
roots = appendUniquePath(roots, filepath.Join(input.RuntimeRoot, "extension-skills", root))
}
return roots, nil
}
func materializeExtensionRuntimeSkills(input ProviderPrepareInput, roots []string, fallback bool) error {
skillRoots := append([]string(nil), roots...)
if len(skillRoots) == 0 && fallback {
if root := providerSkillRoot(input.Cwd, input.Provider); root != "" {
skillRoots = []string{root}
}
}
for _, skillRoot := range skillRoots {
if !filepath.IsAbs(skillRoot) {
skillRoot = filepath.Join(input.Cwd, skillRoot)
}
skillPaths, err := installProviderNativeSkillsStable(skillRoot, input.PrepareInput)
if err != nil {
return err
}
if input.Manifest != nil {
for _, skillPath := range skillPaths {
input.Manifest.RecordManagedFile(skillPath, "provider-skill", true)
}
}
}
return nil
}
func writeExtensionRuntimeConfig(path string, userConfig []byte, externalDirs []string, home ExtensionRuntimeHome) error {
if strings.TrimSpace(home.ConfigFile) == "" {
return nil
}
config := string(userConfig)
if len(home.ExternalDirsKey) > 0 {
var err error
config, err = mergeYAMLStringList(config, home.ExternalDirsKey, externalDirs)
if err != nil {
return err
}
}
if strings.TrimSpace(config) == "" {
return nil
}
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return fmt.Errorf("create extension runtime config dir: %w", err)
}
if err := os.WriteFile(path, []byte(config), 0o600); err != nil {
return fmt.Errorf("write extension runtime config: %w", err)
}
return nil
}
func ValidateExtensionRuntimePrep(prep ExtensionRuntimePrep) error {
if file := strings.TrimSpace(prep.InstructionsFile); file != "" {
if err := validateExtensionRuntimeRelPath(file, "extension runtime instructions file"); err != nil {
return err
}
}
if prep.Home == nil {
return nil
}
home := *prep.Home
if !extensionRuntimeEnvName.MatchString(strings.TrimSpace(home.EnvVar)) {
return errors.New("extension runtime home env is unsupported")
}
if err := validateExtensionRuntimeRelPath(home.DirName, "extension runtime home dir"); err != nil {
return err
}
if sourceEnv := strings.TrimSpace(home.SourceEnvVar); sourceEnv != "" && !extensionRuntimeEnvName.MatchString(sourceEnv) {
return errors.New("extension runtime source env is unsupported")
}
if sourceRel := strings.TrimSpace(home.SourceDefaultRel); sourceRel != "" {
if err := validateExtensionRuntimeRelPath(sourceRel, "extension runtime source default path"); err != nil {
return err
}
}
if strings.TrimSpace(home.ConfigFormat) != "" && strings.TrimSpace(home.ConfigFormat) != "yaml" {
return errors.New("extension runtime config format is unsupported")
}
for _, file := range home.CopyFiles {
if err := validateExtensionRuntimeRelPath(file, "extension runtime copy file"); err != nil {
return err
}
}
for _, dir := range home.SharedDirs {
if err := validateExtensionRuntimeRelPath(dir, "extension runtime shared dir"); err != nil {
return err
}
}
if configFile := strings.TrimSpace(home.ConfigFile); configFile != "" {
if err := validateExtensionRuntimeRelPath(configFile, "extension runtime config file"); err != nil {
return err
}
}
if len(home.ExternalDirsKey) > 0 && !slices.Equal(home.ExternalDirsKey, []string{"skills", "external_dirs"}) {
return errors.New("extension runtime external dirs key is unsupported")
}
if userSkillDir := strings.TrimSpace(home.UserHomeSkillDir); userSkillDir != "" {
if err := validateExtensionRuntimeRelPath(userSkillDir, "extension runtime user skill dir"); err != nil {
return err
}
}
return validateExtensionRuntimeHomePathConflicts(home)
}
func validateExtensionRuntimeHomePathConflicts(home ExtensionRuntimeHome) error {
files := append([]string(nil), home.CopyFiles...)
if configFile := strings.TrimSpace(home.ConfigFile); configFile != "" {
files = append(files, configFile)
}
seen := map[string]bool{}
for _, sharedDir := range home.SharedDirs {
sharedDir = filepath.Clean(filepath.FromSlash(strings.TrimSpace(sharedDir)))
for existing := range seen {
if extensionRuntimePathsOverlap(existing, sharedDir) {
return fmt.Errorf("extension runtime shared dirs overlap: %s and %s", existing, sharedDir)
}
}
seen[sharedDir] = true
for _, file := range files {
file = filepath.Clean(filepath.FromSlash(strings.TrimSpace(file)))
if extensionRuntimePathsOverlap(sharedDir, file) {
return fmt.Errorf("extension runtime shared dir %s conflicts with copied file %s", sharedDir, file)
}
}
}
return nil
}
func extensionRuntimePathsOverlap(left, right string) bool {
return left == right || strings.HasPrefix(left, right+string(filepath.Separator)) || strings.HasPrefix(right, left+string(filepath.Separator))
}
func validateExtensionRuntimeRelPath(value string, label string) error {
trimmed := strings.TrimSpace(value)
portable := strings.ReplaceAll(trimmed, `\`, "/")
cleaned := filepath.Clean(filepath.FromSlash(trimmed))
// Runtime descriptors use slash-separated paths, so reject absolute
// spellings from both the descriptor syntax and the host OS.
if cleaned == "." || cleaned == "" || isPortableAbsolutePath(trimmed, portable) || cleaned == ".." || strings.HasPrefix(cleaned, ".."+string(filepath.Separator)) {
return fmt.Errorf("%s must be a safe relative path", label)
}
return nil
}
func isPortableAbsolutePath(trimmed, portable string) bool {
return filepath.IsAbs(filepath.FromSlash(trimmed)) || path.IsAbs(portable) || portableDrivePath(portable)
}
func portableDrivePath(value string) bool {
return len(value) >= 2 && value[1] == ':' &&
((value[0] >= 'a' && value[0] <= 'z') || (value[0] >= 'A' && value[0] <= 'Z'))
}
func appendUniquePath(paths []string, path string) []string {
path = filepath.Clean(strings.TrimSpace(path))
if path == "." || path == "" {
return paths
}
if slices.Contains(paths, path) {
return paths
}
return append(paths, path)
}