// agent_home.go 实现 agent CLI 受管 home 目录的物化与重定向 env 注入。 // // 每个 AgentKind 一个受管 home://。spawn 前把 // DB 中的配置文件解密落盘(全量覆写受管文件,不清空目录 —— agent 运行期写入的 // 缓存 / 凭据得以保留),再按 client_type 注入配置目录重定向变量。受管 home 位于 // DataDir 下,容器场景随持久卷存活。 package acp import ( "context" "fmt" "os" "path/filepath" "runtime" "strings" ) // materializeAgentHome 创建受管 home 并物化该 AgentKind 的全部配置文件。 // 返回 home 绝对路径。配置文件为空时仍创建目录(agent 需要可写 home)。 func (s *Supervisor) materializeAgentHome(ctx context.Context, kind *AgentKind) (string, error) { home, err := filepath.Abs(filepath.Join(s.cfg.AgentHomesDir, kind.ID.String())) if err != nil { return "", fmt.Errorf("resolve agent home: %w", err) } if err := os.MkdirAll(home, 0o700); err != nil { return "", fmt.Errorf("create agent home: %w", err) } // client 专属配置目录预创建:CODEX_HOME 指向的目录必须已存在(codex 硬性要求), // 其余预创建无害且省去 agent 首次启动的目录初始化分支。 if sub := clientConfigSubdir(kind.ClientType); sub != "" { if err := os.MkdirAll(filepath.Join(home, sub), 0o700); err != nil { return "", fmt.Errorf("create client config dir: %w", err) } } files, err := s.repo.ListConfigFiles(ctx, kind.ID) if err != nil { return "", err } sep := string(filepath.Separator) for _, f := range files { plain, derr := s.crypto.Decrypt(f.EncryptedContent) if derr != nil { return "", fmt.Errorf("decrypt config file %s: %w", f.RelPath, derr) } dst := filepath.Clean(filepath.Join(home, filepath.FromSlash(f.RelPath))) // rel_path 写入时已校验;此处兜底防 DB 被直接篡改后逃逸受管目录。 if !strings.HasPrefix(dst, home+sep) { return "", fmt.Errorf("config file %s escapes agent home", f.RelPath) } if err := os.MkdirAll(filepath.Dir(dst), 0o700); err != nil { return "", fmt.Errorf("create config file dir %s: %w", f.RelPath, err) } if err := os.WriteFile(dst, plain, 0o600); err != nil { return "", fmt.Errorf("write config file %s: %w", f.RelPath, err) } } return home, nil } // agentHomeEnv 返回受管 home 对应的重定向 env 默认值。优先级最低:AgentKind env // 与系统 extraEnv 可覆盖同名 key。 func agentHomeEnv(home string, ct ClientType) map[string]string { env := map[string]string{"HOME": home} if runtime.GOOS == "windows" { // Windows 上 Node/Rust 解析 home 走 USERPROFILE(开发环境)。 env["USERPROFILE"] = home } switch ct { case ClientClaudeCode: env["CLAUDE_CONFIG_DIR"] = filepath.Join(home, ".claude") case ClientCodex: env["CODEX_HOME"] = filepath.Join(home, ".codex") case ClientGemini: // GEMINI_CLI_HOME 替换的是 home 本身,.gemini 段由 CLI 拼接。 env["GEMINI_CLI_HOME"] = home } return env } // clientConfigSubdir 返回 client 专属配置子目录(相对受管 home),无则空串。 func clientConfigSubdir(ct ClientType) string { switch ct { case ClientClaudeCode: return ".claude" case ClientCodex: return ".codex" case ClientGemini: return ".gemini" } return "" }