源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
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

This commit is contained in:
agent-radar
2026-09-01 20:17:27 +08:00
commit 2ed3415fb7
8018 changed files with 1936476 additions and 0 deletions
@@ -0,0 +1,14 @@
//go:build !windows
package runtimecmd
import "os"
func executableNameCandidates(command string, _ []string) []string {
return []string{command}
}
func isExecutableFile(path string) bool {
stat, err := os.Stat(path)
return err == nil && !stat.IsDir() && stat.Mode().Perm()&0o111 != 0
}
@@ -0,0 +1,64 @@
//go:build windows
package runtimecmd
import (
"os"
"path/filepath"
"strings"
)
const defaultWindowsPathExt = ".COM;.EXE;.BAT;.CMD;.PS1"
func executableNameCandidates(command string, env []string) []string {
if filepath.Ext(command) != "" {
return []string{command}
}
pathExt := strings.TrimSpace(envValue(env, "PATHEXT"))
if pathExt == "" {
pathExt = defaultWindowsPathExt
}
// Prefer Windows PATHEXT launchers before an extensionless file. npm writes
// both a POSIX `opencode` shim and a Windows `opencode.cmd` shim into the
// same prefix; choosing the POSIX shim makes ACP probes hang or fail on
// Windows. Keep the extensionless candidate as a final fallback for managed
// PE launchers such as the Tutti agent binary.
result := make([]string, 0, 6)
seen := map[string]struct{}{}
extensions := strings.Split(pathExt, ";")
hasPowerShell := false
for _, extension := range extensions {
if strings.EqualFold(strings.TrimSpace(extension), ".PS1") {
hasPowerShell = true
break
}
}
if !hasPowerShell {
extensions = append(extensions, ".PS1")
}
for _, extension := range extensions {
extension = strings.TrimSpace(extension)
if extension == "" {
continue
}
if !strings.HasPrefix(extension, ".") {
extension = "." + extension
}
candidate := command + extension
key := strings.ToLower(candidate)
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
result = append(result, candidate)
}
if _, ok := seen[strings.ToLower(command)]; !ok {
result = append(result, command)
}
return result
}
func isExecutableFile(path string) bool {
stat, err := os.Stat(path)
return err == nil && !stat.IsDir()
}
@@ -0,0 +1,28 @@
package runtimecmd
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestResolverPrefersWindowsNPMShimOverPOSIXShim(t *testing.T) {
dir := t.TempDir()
posixShim := filepath.Join(dir, "opencode")
windowsShim := filepath.Join(dir, "opencode.cmd")
if err := os.WriteFile(posixShim, []byte("#!/bin/sh\n"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(windowsShim, []byte("@echo off\r\n"), 0o644); err != nil {
t.Fatal(err)
}
resolver := Resolver{
Environ: func() []string { return []string{"PATH=" + dir} },
HomeDir: func() (string, error) { return t.TempDir(), nil },
}
if got := resolver.Resolve("opencode", resolver.Env(nil)); !strings.EqualFold(got, windowsShim) {
t.Fatalf("Resolve() = %q, want Windows shim %q", got, windowsShim)
}
}
@@ -0,0 +1,42 @@
package runtimecmd
import (
"path/filepath"
"runtime"
)
// NPMGlobalLayout describes npm's platform-specific global prefix layout.
type NPMGlobalLayout struct {
PrefixDir string
BinDir string
}
// ResolveNPMGlobalLayout keeps npm install and executable discovery on the
// same platform contract. installBinDir is the directory that must contain
// the user-facing npm launcher.
func ResolveNPMGlobalLayout(installBinDir string) NPMGlobalLayout {
if runtime.GOOS == "windows" {
// Windows npm writes global command shims directly into its prefix (not
// <prefix>/bin). The requested directory is therefore both the prefix
// and the executable directory so a managed install produces, for
// example, %USERPROFILE%\\.local\\bin\\codex.cmd.
return NPMGlobalLayout{PrefixDir: installBinDir, BinDir: installBinDir}
}
prefixDir := filepath.Dir(installBinDir)
return NPMGlobalLayout{PrefixDir: prefixDir, BinDir: installBinDir}
}
// UserManagedNPMExecutableDirs returns Tutti's current user-level npm launcher
// directory followed by any backward-compatible directory that older Tutti
// releases used. Callers use this list for discovery only; fresh installs still
// target the first entry.
func UserManagedNPMExecutableDirs(home string) []string {
installBinDir := filepath.Join(home, ".local", "bin")
dirs := []string{ResolveNPMGlobalLayout(installBinDir).BinDir}
if runtime.GOOS == "windows" {
// Older Windows releases passed %USERPROFILE%\.local as npm's prefix,
// so npm placed codex.cmd/tutti-agent.cmd directly in that directory.
dirs = append(dirs, filepath.Join(home, ".local"))
}
return dirs
}
@@ -0,0 +1,84 @@
//go:build windows
package runtimecmd
import (
"os"
"path/filepath"
"testing"
)
func TestResolveNPMGlobalLayoutUsesRequestedBinAsWindowsPrefix(t *testing.T) {
home := t.TempDir()
requestedBin := filepath.Join(home, ".local", "bin")
layout := ResolveNPMGlobalLayout(requestedBin)
want := requestedBin
if layout.PrefixDir != want || layout.BinDir != want {
t.Fatalf("ResolveNPMGlobalLayout() = %#v, want prefix and bin %q", layout, want)
}
}
func TestUserManagedNPMExecutableDirsIncludesLegacyWindowsPrefix(t *testing.T) {
home := t.TempDir()
dirs := UserManagedNPMExecutableDirs(home)
want := []string{
filepath.Join(home, ".local", "bin"),
filepath.Join(home, ".local"),
}
if len(dirs) != len(want) {
t.Fatalf("UserManagedNPMExecutableDirs() = %#v, want %#v", dirs, want)
}
for index := range want {
if dirs[index] != want[index] {
t.Fatalf("UserManagedNPMExecutableDirs()[%d] = %q, want %q", index, dirs[index], want[index])
}
}
}
func TestResolverFindsWindowsNPMGlobalShim(t *testing.T) {
home := t.TempDir()
shimDir := filepath.Join(home, ".local", "bin")
if err := os.MkdirAll(shimDir, 0o755); err != nil {
t.Fatal(err)
}
shim := filepath.Join(shimDir, "codex.cmd")
if err := os.WriteFile(shim, []byte("@echo off\r\n"), 0o755); err != nil {
t.Fatal(err)
}
resolver := Resolver{
Environ: func() []string { return []string{"PATH=C:\\Windows\\System32"} },
HomeDir: func() (string, error) { return home, nil },
LookPath: func(string) (string, error) { return "", os.ErrNotExist },
IsExecutableFile: func(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
},
}
if got := resolver.ResolveBinary([]string{"codex.exe", "codex.cmd"}, nil); got != shim {
t.Fatalf("ResolveBinary() = %q, want %q", got, shim)
}
}
func TestResolverFindsLegacyWindowsNPMGlobalShimOutsidePath(t *testing.T) {
home := t.TempDir()
legacyPrefix := filepath.Join(home, ".local")
if err := os.MkdirAll(legacyPrefix, 0o755); err != nil {
t.Fatal(err)
}
shim := filepath.Join(legacyPrefix, "codex.cmd")
if err := os.WriteFile(shim, []byte("@echo off\r\n"), 0o755); err != nil {
t.Fatal(err)
}
resolver := Resolver{
Environ: func() []string { return []string{"PATH=C:\\Windows\\System32"} },
HomeDir: func() (string, error) { return home, nil },
LookPath: func(string) (string, error) { return "", os.ErrNotExist },
IsExecutableFile: func(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
},
}
if got := resolver.ResolveBinary([]string{"codex.exe", "codex.cmd"}, nil); got != shim {
t.Fatalf("ResolveBinary() = %q, want legacy shim %q", got, shim)
}
}
+347
View File
@@ -0,0 +1,347 @@
package runtimecmd
import (
"net"
"net/http"
"net/url"
"os"
"strings"
"sync"
"time"
"golang.org/x/net/http/httpproxy"
)
// System proxy injection.
//
// Some standalone agent processes only honor the
// HTTP(S)_PROXY environment variables — they do NOT read the OS system proxy.
// Desktop runtimes commonly work around this by resolving the OS proxy via
// Electron's session.resolveProxy() (Chromium/SystemConfiguration) and injecting
// HTTPS_PROXY/HTTP_PROXY into the spawned process. Without it, a child agent
// connects directly and, from a restricted region, gets `403 Request not
// allowed` from the upstream API while the app keeps working.
//
// We mirror that behavior here through a narrow platform adapter and inject
// equivalent env vars. To stay faithful to the
// app we: skip SOCKS entries (downstream HTTP agents don't speak SOCKS), use the
// same default NO_PROXY, and never override a proxy the user/session already set.
//
// Effective precedence, everywhere (spawned agents and in-process clients):
//
// session-explicit env > process env (incl. the user's shell env, when the
// desktop forwards it) > OS system proxy > direct
//
// Out of scope, deliberately: SOCKS proxies and PAC resolution (scutil only
// exposes only a PAC URL; the desktop's Chromium stack handles PAC natively for
// its own requests). Windows support intentionally covers the static per-user
// proxy only. Setting TUTTI_DISABLE_PROXY_AUTODETECT=1 disables system-proxy
// detection and injection entirely; explicit env proxies keep working.
// noProxyDefault matches the value the Claude desktop app injects.
const noProxyDefault = "localhost,127.0.0.1,::1,.local"
// disableProxyAutodetectEnvKey is the escape hatch for machines where the
// detected system proxy is broken but direct connections work.
const disableProxyAutodetectEnvKey = "TUTTI_DISABLE_PROXY_AUTODETECT"
func proxyAutodetectDisabled() bool {
value := strings.TrimSpace(os.Getenv(disableProxyAutodetectEnvKey))
return value == "1" || strings.EqualFold(value, "true")
}
// injectSystemProxyEnv appends HTTPS_PROXY/HTTP_PROXY/NO_PROXY derived from the
// OS system proxy, but only for keys not already present (case-insensitive)
// in env, so explicit user/session settings always win.
func (r Resolver) injectSystemProxyEnv(env []string) []string {
if proxyAutodetectDisabled() {
return env
}
proxyEnv := r.systemProxyEnv()
if len(proxyEnv) == 0 {
return env
}
for _, key := range []string{"HTTPS_PROXY", "HTTP_PROXY", "NO_PROXY"} {
value, ok := proxyEnv[key]
if !ok || strings.TrimSpace(value) == "" {
continue
}
if _, exists := envValueFrom(env, key); exists {
continue
}
env = append(env, key+"="+value)
}
return env
}
// InjectSystemProxyEnv appends the OS system proxy variables
// (HTTPS_PROXY/HTTP_PROXY/NO_PROXY) to env for any key not already present,
// reading the same platform system-proxy source as Resolver.Env(). It exists so
// the spawn paths that build their own environment instead of going through
// Resolver.Env() — managed-runtime installs, agent logins, workspace terminals —
// get identical proxy treatment. No-op on unsupported platforms or when no system proxy is
// configured; never overrides a user/session-set value.
func InjectSystemProxyEnv(env []string) []string {
return Resolver{}.injectSystemProxyEnv(env)
}
// DynamicProxyFunc returns an http.Transport.Proxy function that resolves the
// proxy per request: explicit proxies from the process environment win, the
// OS system proxy fills in the blanks, and NO_PROXY (from env, the system
// exceptions, or noProxyDefault) plus loopback targets always bypass. It is the
// in-process counterpart of InjectSystemProxyEnv, so daemon HTTP clients and
// spawned agents make the same routing decision. The system proxy is re-read
// (through a short-lived cache) on every call, so toggling the proxy panel
// takes effect without restarting the daemon.
func DynamicProxyFunc() func(*http.Request) (*url.URL, error) {
return func(req *http.Request) (*url.URL, error) {
cfg := httpproxy.FromEnvironment()
if !proxyAutodetectDisabled() {
mergeSystemProxy(cfg, Resolver{}.systemProxyEnv())
}
return cfg.ProxyFunc()(req.URL)
}
}
// EffectiveProxySummary reports which source currently supplies the outbound
// proxy — "env" (explicit environment variables), "system" (OS system
// proxy), or "none" — plus the proxy address as host:port with any credentials
// stripped. Startup/diagnostic logging only; it answers "did requests on this
// machine go through a proxy" without another capture round-trip.
func EffectiveProxySummary() (source string, host string) {
fromEnv := httpproxy.FromEnvironment()
if raw := firstNonEmptyString(fromEnv.HTTPSProxy, fromEnv.HTTPProxy); raw != "" {
return "env", proxyHostForLog(raw)
}
if proxyAutodetectDisabled() {
return "none", ""
}
sys := Resolver{}.systemProxyEnv()
if raw := firstNonEmptyString(sys["HTTPS_PROXY"], sys["HTTP_PROXY"]); raw != "" {
return "system", proxyHostForLog(raw)
}
return "none", ""
}
func firstNonEmptyString(values ...string) string {
for _, value := range values {
if trimmed := strings.TrimSpace(value); trimmed != "" {
return trimmed
}
}
return ""
}
// proxyHostForLog reduces a proxy URL to host:port, never exposing userinfo.
func proxyHostForLog(raw string) string {
parsed, err := url.Parse(raw)
if err != nil || parsed == nil || parsed.Host == "" {
// Scheme-less values ("host:port") parse without a Host; retry with an
// explicit scheme rather than logging the raw (possibly credentialed)
// string.
parsed, err = url.Parse("http://" + raw)
if err != nil || parsed == nil {
return ""
}
}
return parsed.Host
}
// SystemProxyEnv exposes the cached OS system proxy as env-style keys
// (HTTPS_PROXY/HTTP_PROXY/NO_PROXY) for observability call sites that want to
// log the effective proxy source. Returns nil when autodetect is disabled or no
// system proxy is configured.
func SystemProxyEnv() map[string]string {
if proxyAutodetectDisabled() {
return nil
}
return Resolver{}.systemProxyEnv()
}
// mergeSystemProxy fills empty fields of an env-derived proxy config with the
// system proxy values, preserving env-first precedence. When any proxy ends up
// configured without a NO_PROXY, noProxyDefault applies so local services are
// never routed through the proxy (httpproxy additionally hard-bypasses
// loopback hosts).
func mergeSystemProxy(cfg *httpproxy.Config, systemEnv map[string]string) {
if cfg.HTTPSProxy == "" {
cfg.HTTPSProxy = systemEnv["HTTPS_PROXY"]
}
if cfg.HTTPProxy == "" {
cfg.HTTPProxy = systemEnv["HTTP_PROXY"]
}
if cfg.NoProxy == "" {
cfg.NoProxy = systemEnv["NO_PROXY"]
}
if cfg.NoProxy == "" && (cfg.HTTPSProxy != "" || cfg.HTTPProxy != "") {
cfg.NoProxy = noProxyDefault
}
}
// systemProxyCache memoizes platform proxy reads so hot paths (per-spawn env
// injection, per-request proxy resolution) avoid native I/O each time, while
// still noticing proxy-panel toggles within the TTL.
var systemProxyCache = struct {
sync.Mutex
env map[string]string
expiresAt time.Time
}{}
const systemProxyCacheTTL = 60 * time.Second
func (r Resolver) systemProxyEnv() map[string]string {
if r.ScutilProxy != nil {
// Injected (test) source: bypass the cache so cases stay independent.
output, ok := r.ScutilProxy()
if !ok {
return nil
}
return parseScutilProxy(output)
}
systemProxyCache.Lock()
defer systemProxyCache.Unlock()
if time.Now().Before(systemProxyCache.expiresAt) {
return systemProxyCache.env
}
env := readSystemProxyEnv()
systemProxyCache.env = env
systemProxyCache.expiresAt = time.Now().Add(systemProxyCacheTTL)
return env
}
// parseScutilProxy turns `scutil --proxy` output into proxy env vars.
//
// Example input:
//
// <dictionary> {
// HTTPSEnable : 1
// HTTPSProxy : 127.0.0.1
// HTTPSPort : 7890
// ...
// }
//
// SOCKS entries are intentionally ignored, and PAC (ProxyAutoConfig) is not
// resolved — scutil only exposes the PAC URL, not the per-URL result, so we
// leave those to the user's explicit env. Returns nil when no usable proxy is
// configured (direct connection).
func parseScutilProxy(output string) map[string]string {
fields := map[string]string{}
for _, line := range strings.Split(output, "\n") {
key, value, ok := strings.Cut(line, ":")
if !ok {
continue
}
fields[strings.TrimSpace(key)] = strings.TrimSpace(value)
}
proxyURL := func(enableKey, hostKey, portKey string) string {
if fields[enableKey] != "1" {
return ""
}
host := fields[hostKey]
port := fields[portKey]
if host == "" || port == "" {
return ""
}
return "http://" + net.JoinHostPort(host, port)
}
// Prefer the HTTPS entry (the upstream API is HTTPS); fall back to HTTP.
// Both env vars point at the same proxy, mirroring the Claude app.
url := proxyURL("HTTPSEnable", "HTTPSProxy", "HTTPSPort")
if url == "" {
url = proxyURL("HTTPEnable", "HTTPProxy", "HTTPPort")
}
if url == "" {
return nil
}
return map[string]string{
"HTTPS_PROXY": url,
"HTTP_PROXY": url,
"NO_PROXY": noProxyDefault,
}
}
// parseWindowsProxyServer converts the WinINet ProxyServer registry value into
// env-style HTTP proxy settings. Windows accepts either one proxy for all
// protocols ("host:port") or a protocol map ("http=...;https=...;socks=...").
// SOCKS entries are deliberately ignored because downstream HTTP agents do not
// consistently support SOCKS proxy URLs.
func parseWindowsProxyServer(proxyServer, proxyOverride string) map[string]string {
proxyServer = strings.TrimSpace(proxyServer)
if proxyServer == "" {
return nil
}
values := map[string]string{}
if strings.Contains(proxyServer, "=") {
for _, part := range strings.Split(proxyServer, ";") {
key, value, ok := strings.Cut(part, "=")
if !ok {
continue
}
key = strings.ToLower(strings.TrimSpace(key))
value = normalizeHTTPProxyURL(value)
if value != "" && (key == "http" || key == "https") {
values[key] = value
}
}
} else if value := normalizeHTTPProxyURL(proxyServer); value != "" {
values["http"] = value
values["https"] = value
}
if len(values) == 0 {
return nil
}
if values["https"] == "" {
values["https"] = values["http"]
}
if values["http"] == "" {
values["http"] = values["https"]
}
result := map[string]string{
"HTTPS_PROXY": values["https"],
"HTTP_PROXY": values["http"],
"NO_PROXY": windowsNoProxy(proxyOverride),
}
return result
}
func normalizeHTTPProxyURL(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return ""
}
if !strings.Contains(value, "://") {
value = "http://" + value
}
parsed, err := url.Parse(value)
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
return ""
}
return parsed.String()
}
func windowsNoProxy(proxyOverride string) string {
items := strings.Split(noProxyDefault, ",")
seen := map[string]struct{}{}
for _, value := range items {
seen[strings.ToLower(value)] = struct{}{}
}
for _, raw := range strings.Split(proxyOverride, ";") {
value := strings.TrimSpace(raw)
if value == "" || strings.EqualFold(value, "<local>") {
continue
}
if strings.HasPrefix(value, "*.") {
value = value[1:]
}
key := strings.ToLower(value)
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
items = append(items, value)
}
return strings.Join(items, ",")
}
@@ -0,0 +1,19 @@
//go:build darwin
package runtimecmd
import (
"context"
"os/exec"
"time"
)
func readSystemProxyEnv() map[string]string {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
out, err := exec.CommandContext(ctx, "scutil", "--proxy").Output()
if err != nil {
return nil
}
return parseScutilProxy(string(out))
}
@@ -0,0 +1,5 @@
//go:build !darwin && !windows
package runtimecmd
func readSystemProxyEnv() map[string]string { return nil }
@@ -0,0 +1,25 @@
//go:build windows
package runtimecmd
import "golang.org/x/sys/windows/registry"
const windowsInternetSettingsKey = `Software\Microsoft\Windows\CurrentVersion\Internet Settings`
func readSystemProxyEnv() map[string]string {
key, err := registry.OpenKey(registry.CURRENT_USER, windowsInternetSettingsKey, registry.QUERY_VALUE)
if err != nil {
return nil
}
defer key.Close()
enabled, _, err := key.GetIntegerValue("ProxyEnable")
if err != nil || enabled == 0 {
return nil
}
server, _, err := key.GetStringValue("ProxyServer")
if err != nil {
return nil
}
override, _, _ := key.GetStringValue("ProxyOverride")
return parseWindowsProxyServer(server, override)
}
@@ -0,0 +1,300 @@
package runtimecmd
import (
"net/http"
"net/url"
"runtime"
"strings"
"testing"
"time"
"golang.org/x/net/http/httpproxy"
)
// Real-world `scutil --proxy` output with Clash Verge system proxy enabled
// (HTTP/HTTPS/SOCKS all pointing at 127.0.0.1:7890).
const scutilProxyEnabled = `<dictionary> {
ExceptionsList : <array> {
0 : 127.0.0.1
1 : localhost
2 : *.local
}
HTTPEnable : 1
HTTPPort : 7890
HTTPProxy : 127.0.0.1
HTTPSEnable : 1
HTTPSPort : 7890
HTTPSProxy : 127.0.0.1
ProxyAutoConfigEnable : 0
SOCKSEnable : 1
SOCKSPort : 7890
SOCKSProxy : 127.0.0.1
}`
const scutilProxyDisabled = `<dictionary> {
ExceptionsList : <array> {
0 : 127.0.0.1
1 : localhost
}
HTTPEnable : 0
HTTPSEnable : 0
ProxyAutoConfigEnable : 0
SOCKSEnable : 0
}`
func TestParseScutilProxyEnabled(t *testing.T) {
got := parseScutilProxy(scutilProxyEnabled)
want := map[string]string{
"HTTPS_PROXY": "http://127.0.0.1:7890",
"HTTP_PROXY": "http://127.0.0.1:7890",
"NO_PROXY": noProxyDefault,
}
for k, v := range want {
if got[k] != v {
t.Fatalf("parseScutilProxy()[%q] = %q, want %q", k, got[k], v)
}
}
}
func TestParseScutilProxyDisabledReturnsNil(t *testing.T) {
if got := parseScutilProxy(scutilProxyDisabled); got != nil {
t.Fatalf("parseScutilProxy() = %v, want nil", got)
}
}
func TestParseScutilProxyHTTPOnly(t *testing.T) {
out := `<dictionary> {
HTTPEnable : 1
HTTPProxy : 10.0.0.2
HTTPPort : 3128
HTTPSEnable : 0
SOCKSEnable : 0
}`
got := parseScutilProxy(out)
if got["HTTP_PROXY"] != "http://10.0.0.2:3128" || got["HTTPS_PROXY"] != "http://10.0.0.2:3128" {
t.Fatalf("parseScutilProxy() = %v, want HTTP(S)_PROXY=http://10.0.0.2:3128", got)
}
}
func TestParseScutilProxySOCKSOnlyIgnored(t *testing.T) {
out := `<dictionary> {
HTTPEnable : 0
HTTPSEnable : 0
SOCKSEnable : 1
SOCKSProxy : 127.0.0.1
SOCKSPort : 7890
}`
if got := parseScutilProxy(out); got != nil {
t.Fatalf("parseScutilProxy() with SOCKS only = %v, want nil (SOCKS skipped)", got)
}
}
func TestParseWindowsProxyServerSingleProxy(t *testing.T) {
got := parseWindowsProxyServer("127.0.0.1:7890", "<local>;*.corp.example;localhost")
if got["HTTPS_PROXY"] != "http://127.0.0.1:7890" || got["HTTP_PROXY"] != "http://127.0.0.1:7890" {
t.Fatalf("parseWindowsProxyServer() proxy = %v", got)
}
if got["NO_PROXY"] != "localhost,127.0.0.1,::1,.local,.corp.example" {
t.Fatalf("parseWindowsProxyServer() NO_PROXY = %q", got["NO_PROXY"])
}
}
func TestParseWindowsProxyServerProtocolMap(t *testing.T) {
got := parseWindowsProxyServer("http=proxy.internal:8080;https=secure.internal:8443;socks=127.0.0.1:1080", "")
if got["HTTP_PROXY"] != "http://proxy.internal:8080" || got["HTTPS_PROXY"] != "http://secure.internal:8443" {
t.Fatalf("parseWindowsProxyServer() = %v", got)
}
}
func TestParseWindowsProxyServerIgnoresSocksOnly(t *testing.T) {
if got := parseWindowsProxyServer("socks=127.0.0.1:1080", ""); got != nil {
t.Fatalf("parseWindowsProxyServer() = %v, want nil", got)
}
}
func TestEnvInjectsSystemProxy(t *testing.T) {
resolver := Resolver{
Environ: func() []string { return []string{"PATH=/usr/bin:/bin"} },
HomeDir: func() (string, error) { return t.TempDir(), nil },
ScutilProxy: func() (string, bool) { return scutilProxyEnabled, true },
}
env := resolver.Env(nil)
if got := envValue(env, "HTTPS_PROXY"); got != "http://127.0.0.1:7890" {
t.Fatalf("HTTPS_PROXY = %q, want http://127.0.0.1:7890", got)
}
if got := envValue(env, "HTTP_PROXY"); got != "http://127.0.0.1:7890" {
t.Fatalf("HTTP_PROXY = %q, want http://127.0.0.1:7890", got)
}
if got := envValue(env, "NO_PROXY"); got != noProxyDefault {
t.Fatalf("NO_PROXY = %q, want %q", got, noProxyDefault)
}
}
func TestEnvDoesNotOverrideExplicitProxy(t *testing.T) {
resolver := Resolver{
// User already exported a (lowercase) proxy — must be preserved.
Environ: func() []string {
return []string{"PATH=/usr/bin:/bin", "https_proxy=http://user-set:1080"}
},
HomeDir: func() (string, error) { return t.TempDir(), nil },
ScutilProxy: func() (string, bool) { return scutilProxyEnabled, true },
}
env := resolver.Env(nil)
if got := envValue(env, "https_proxy"); got != "http://user-set:1080" {
t.Fatalf("https_proxy = %q, want it preserved as http://user-set:1080", got)
}
// And we must not have appended a conflicting upper-case HTTPS_PROXY.
count := 0
for _, item := range env {
if k, _, _ := strings.Cut(item, "="); strings.EqualFold(k, "HTTPS_PROXY") {
count++
}
}
if count != 1 {
t.Fatalf("found %d HTTPS_PROXY entries, want exactly 1 (no override)", count)
}
}
func TestEnvNoProxyWhenScutilUnavailable(t *testing.T) {
resolver := Resolver{
Environ: func() []string { return []string{"PATH=/usr/bin:/bin"} },
HomeDir: func() (string, error) { return t.TempDir(), nil },
ScutilProxy: func() (string, bool) { return "", false },
}
env := resolver.Env(nil)
if got := envValue(env, "HTTPS_PROXY"); got != "" {
t.Fatalf("HTTPS_PROXY = %q, want empty when scutil unavailable", got)
}
}
func proxyFor(t *testing.T, cfg *httpproxy.Config, rawURL string) *url.URL {
t.Helper()
target, err := url.Parse(rawURL)
if err != nil {
t.Fatalf("url.Parse(%q): %v", rawURL, err)
}
proxy, err := cfg.ProxyFunc()(target)
if err != nil {
t.Fatalf("ProxyFunc(%q): %v", rawURL, err)
}
return proxy
}
func TestMergeSystemProxyEnvWins(t *testing.T) {
cfg := &httpproxy.Config{HTTPSProxy: "http://env-proxy:8080"}
mergeSystemProxy(cfg, map[string]string{
"HTTPS_PROXY": "http://system-proxy:7890",
"HTTP_PROXY": "http://system-proxy:7890",
"NO_PROXY": noProxyDefault,
})
if cfg.HTTPSProxy != "http://env-proxy:8080" {
t.Fatalf("HTTPSProxy = %q, want env value preserved", cfg.HTTPSProxy)
}
if cfg.HTTPProxy != "http://system-proxy:7890" {
t.Fatalf("HTTPProxy = %q, want system value filling the blank", cfg.HTTPProxy)
}
if got := proxyFor(t, cfg, "https://api.anthropic.com/v1"); got == nil || got.Host != "env-proxy:8080" {
t.Fatalf("proxy = %v, want env proxy env-proxy:8080", got)
}
}
func TestMergeSystemProxyFallsBackToSystem(t *testing.T) {
cfg := &httpproxy.Config{}
mergeSystemProxy(cfg, map[string]string{
"HTTPS_PROXY": "http://system-proxy:7890",
"HTTP_PROXY": "http://system-proxy:7890",
"NO_PROXY": noProxyDefault,
})
if got := proxyFor(t, cfg, "https://api.anthropic.com/v1"); got == nil || got.Host != "system-proxy:7890" {
t.Fatalf("proxy = %v, want system proxy when env has none", got)
}
}
func TestMergeSystemProxyDirectWhenNoneConfigured(t *testing.T) {
cfg := &httpproxy.Config{}
mergeSystemProxy(cfg, nil)
if got := proxyFor(t, cfg, "https://api.anthropic.com/v1"); got != nil {
t.Fatalf("proxy = %v, want nil (direct) when nothing configured", got)
}
}
func TestMergeSystemProxyBypassesLoopbackAndNoProxy(t *testing.T) {
cfg := &httpproxy.Config{}
mergeSystemProxy(cfg, map[string]string{
"HTTPS_PROXY": "http://system-proxy:7890",
"HTTP_PROXY": "http://system-proxy:7890",
"NO_PROXY": noProxyDefault,
})
for _, target := range []string{
"http://127.0.0.1:4545/v1/health",
"http://localhost:4545/v1/health",
"https://printer.local/status",
} {
if got := proxyFor(t, cfg, target); got != nil {
t.Fatalf("proxy for %q = %v, want nil (local bypass)", target, got)
}
}
if got := proxyFor(t, cfg, "https://api.anthropic.com/v1"); got == nil {
t.Fatalf("proxy for external target = nil, want system proxy")
}
}
func TestMergeSystemProxyDefaultsNoProxyForEnvOnlyProxy(t *testing.T) {
// Env sets a proxy but no NO_PROXY, and no system proxy exists: the
// default exclusions still apply so .local names never hit the proxy.
cfg := &httpproxy.Config{HTTPSProxy: "http://env-proxy:8080"}
mergeSystemProxy(cfg, nil)
if cfg.NoProxy != noProxyDefault {
t.Fatalf("NoProxy = %q, want default %q", cfg.NoProxy, noProxyDefault)
}
}
func TestProxyAutodetectDisabledSkipsInjection(t *testing.T) {
t.Setenv(disableProxyAutodetectEnvKey, "1")
resolver := Resolver{
Environ: func() []string { return []string{"PATH=/usr/bin:/bin"} },
HomeDir: func() (string, error) { return t.TempDir(), nil },
ScutilProxy: func() (string, bool) { return scutilProxyEnabled, true },
}
env := resolver.Env(nil)
if got := envValue(env, "HTTPS_PROXY"); got != "" {
t.Fatalf("HTTPS_PROXY = %q, want empty when autodetect disabled", got)
}
}
func TestProxyAutodetectDisabledKeepsEnvProxy(t *testing.T) {
t.Setenv(disableProxyAutodetectEnvKey, "true")
t.Setenv("HTTPS_PROXY", "http://env-proxy:8080")
fn := DynamicProxyFunc()
request, err := http.NewRequest(http.MethodGet, "https://api.anthropic.com/v1", nil)
if err != nil {
t.Fatalf("NewRequest: %v", err)
}
got, err := fn(request)
if err != nil {
t.Fatalf("proxy func err = %v", err)
}
if got == nil || got.Host != "env-proxy:8080" {
t.Fatalf("proxy = %v, want explicit env proxy to keep working", got)
}
}
func TestSystemProxyEnvCachesScutil(t *testing.T) {
if runtime.GOOS != "darwin" {
t.Skip("scutil cache path is darwin-only")
}
systemProxyCache.Lock()
systemProxyCache.env = map[string]string{"HTTPS_PROXY": "http://cached-proxy:7890"}
systemProxyCache.expiresAt = time.Now().Add(time.Minute)
systemProxyCache.Unlock()
t.Cleanup(func() {
systemProxyCache.Lock()
systemProxyCache.env = nil
systemProxyCache.expiresAt = time.Time{}
systemProxyCache.Unlock()
})
got := Resolver{}.systemProxyEnv()
if got["HTTPS_PROXY"] != "http://cached-proxy:7890" {
t.Fatalf("systemProxyEnv() = %v, want cached value within TTL", got)
}
}
@@ -0,0 +1,447 @@
package runtimecmd
import (
"os"
"os/exec"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
)
type Resolver struct {
Environ func() []string
HomeDir func() (string, error)
IsExecutableFile func(string) bool
LookPath func(string) (string, error)
// ScutilProxy returns the raw output of `scutil --proxy` (and whether it is
// available). It is injectable for legacy macOS parser tests; production
// reads the platform system proxy through the build-tagged adapter.
ScutilProxy func() (string, bool)
}
// nestingGuardEnvKeys are the environment variables a parent Claude Code
// session exports to detect (and refuse) nested launches. When tuttid itself
// runs inside a Claude Code session these leak into spawned agent subprocesses, causing
// a child `claude` to abort with "cannot be launched inside another Claude Code
// session". They are stripped from the base environment so each spawned agent
// starts as a fresh session; they are CLAUDE-specific and harmless to other runtimes.
var nestingGuardEnvKeys = []string{
"CLAUDECODE",
"CLAUDE_CODE_ENTRYPOINT",
"CLAUDE_CODE_SESSION_ID",
"CLAUDE_CODE_CHILD_SESSION",
}
func (r Resolver) Env(overrides []string) []string {
baseEnv := stripEnvKeys(r.environ(), nestingGuardEnvKeys)
env := append(baseEnv, overrides...)
pathKey := pathEnvKey(env)
pathGroups := [][]string{}
if overridePathGroups := pathGroupsFromEnv(overrides, pathKey, envValue(baseEnv, pathKey)); len(overridePathGroups) > 0 {
pathGroups = append(pathGroups, overridePathGroups...)
pathGroups = append(pathGroups, preferredExecutableDirs(env))
pathGroups = append(pathGroups, r.fallbackExecutableDirs())
} else {
pathGroups = append(pathGroups, preferredExecutableDirs(env))
pathGroups = append(pathGroups, splitPathList(envValue(baseEnv, pathKey)))
pathGroups = append(pathGroups, r.fallbackExecutableDirs())
}
pathDirs := mergePathDirs(pathGroups...)
if len(pathDirs) > 0 {
env = setEnvValue(env, pathKey, strings.Join(pathDirs, string(os.PathListSeparator)))
}
return r.injectSystemProxyEnv(env)
}
func (r Resolver) Resolve(command string, env []string) string {
command = strings.TrimSpace(command)
if command == "" || strings.ContainsAny(command, `/\`) {
return command
}
if candidates := r.ResolveAll(command, env); len(candidates) > 0 {
return candidates[0]
}
return command
}
// ResolveAll returns every executable match for command in the effective PATH,
// preserving directory order and removing duplicate paths. It deliberately
// does not call LookPath: callers that need an OS-specific fallback can append
// that result explicitly, while discovery callers get a complete, stable view
// of the resolver's own search plan.
func (r Resolver) ResolveAll(command string, env []string) []string {
command = strings.TrimSpace(command)
if command == "" || strings.ContainsAny(command, "/\\") {
return nil
}
return r.ResolveAllNames([]string{command}, env)
}
// ResolveAllNames returns every executable match for a set of command names.
// Directory order is primary and name order is secondary, which lets callers
// enumerate platform launcher variants without changing PATH precedence.
func (r Resolver) ResolveAllNames(commandNames []string, env []string) []string {
result := []string{}
seen := map[string]struct{}{}
for _, dir := range splitPathList(envValue(env, pathEnvKey(env))) {
for _, command := range commandNames {
command = strings.TrimSpace(command)
if command == "" || strings.ContainsAny(command, `/\\`) {
continue
}
for _, executableName := range executableNameCandidates(command, env) {
candidate := filepath.Join(dir, executableName)
if !r.isExecutableFile(candidate) {
continue
}
key := executablePathKey(candidate)
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
result = append(result, candidate)
}
}
}
return result
}
func (r Resolver) ResolveBinary(binaryNames []string, overrides []string) string {
env := r.Env(overrides)
for _, binaryName := range binaryNames {
binaryName = strings.TrimSpace(binaryName)
if binaryName == "" {
continue
}
path := r.Resolve(binaryName, env)
if path != binaryName {
return path
}
if path := r.lookPath(binaryName); path != "" {
return path
}
}
return ""
}
func (r Resolver) UserBinInstallDirs(overrides []string) []string {
baseEnv := r.environ()
env := append([]string{}, baseEnv...)
env = append(env, overrides...)
pathValue := envValue(baseEnv, pathEnvKey(baseEnv))
if overridePath, ok := envValueFrom(overrides, pathEnvKey(env)); ok {
pathValue = overridePath
}
candidates := [][]string{
splitPathList(pathValue),
}
home, err := r.homeDir()
if err == nil && strings.TrimSpace(home) != "" {
managedNPMDirs := UserManagedNPMExecutableDirs(home)
candidates = append(candidates, managedNPMDirs, []string{filepath.Join(home, "bin")})
}
return mergePathDirs(candidates...)
}
func preferredExecutableDirs(env []string) []string {
dirs := []string{}
if nPrefix := strings.TrimSpace(envValue(env, "N_PREFIX")); nPrefix != "" {
dirs = append([]string{filepath.Join(nPrefix, "bin")}, dirs...)
}
if pnpmHome := strings.TrimSpace(envValue(env, "PNPM_HOME")); pnpmHome != "" {
dirs = append([]string{pnpmHome}, dirs...)
}
if voltaHome := strings.TrimSpace(envValue(env, "VOLTA_HOME")); voltaHome != "" {
dirs = append([]string{filepath.Join(voltaHome, "bin")}, dirs...)
}
if asdfDataDir := strings.TrimSpace(envValue(env, "ASDF_DATA_DIR")); asdfDataDir != "" {
dirs = append([]string{filepath.Join(asdfDataDir, "shims")}, dirs...)
}
if miseDataDir := strings.TrimSpace(envValue(env, "MISE_DATA_DIR")); miseDataDir != "" {
dirs = append([]string{filepath.Join(miseDataDir, "shims")}, dirs...)
}
if fnmDir := strings.TrimSpace(envValue(env, "FNM_DIR")); fnmDir != "" {
dirs = append(fnmNodeBinDirs(fnmDir), dirs...)
}
return dirs
}
func (r Resolver) fallbackExecutableDirs() []string {
dirs := []string{
"/opt/homebrew/bin",
"/usr/local/bin",
"/usr/bin",
"/bin",
}
homeDirs := []string{}
home, err := r.homeDir()
if err == nil && strings.TrimSpace(home) != "" {
homeDirs = []string{
filepath.Join(home, ".tutti", "bin"),
filepath.Join(home, ".opencode", "bin"),
}
homeDirs = append(homeDirs, UserManagedNPMExecutableDirs(home)...)
homeDirs = append(homeDirs,
filepath.Join(home, "bin"),
filepath.Join(home, ".npm-global", "bin"),
filepath.Join(home, ".n", "bin"),
filepath.Join(home, "n", "bin"),
filepath.Join(home, ".volta", "bin"),
filepath.Join(home, ".asdf", "shims"),
filepath.Join(home, ".mise", "shims"),
filepath.Join(home, ".bun", "bin"),
filepath.Join(home, "Library", "pnpm"),
)
homeDirs = append(homeDirs, nvmNodeBinDirs(home)...)
homeDirs = append(homeDirs, fnmNodeBinDirs(filepath.Join(home, ".fnm"))...)
// Cursor's native Windows installer places agent.ps1 and its versioned
// payload under %LOCALAPPDATA%\\cursor-agent rather than on PATH.
homeDirs = append(homeDirs, filepath.Join(home, "AppData", "Local", "cursor-agent"))
}
return append(homeDirs, dirs...)
}
func (r Resolver) environ() []string {
if r.Environ != nil {
return r.Environ()
}
return os.Environ()
}
func (r Resolver) homeDir() (string, error) {
if r.HomeDir != nil {
return r.HomeDir()
}
return os.UserHomeDir()
}
func (r Resolver) isExecutableFile(path string) bool {
if r.IsExecutableFile != nil {
return r.IsExecutableFile(path)
}
return isExecutableFile(path)
}
func (r Resolver) lookPath(binaryName string) string {
lookPath := r.LookPath
if lookPath == nil {
lookPath = exec.LookPath
}
path, err := lookPath(binaryName)
if err == nil && strings.TrimSpace(path) != "" {
return strings.TrimSpace(path)
}
return ""
}
func nvmNodeBinDirs(home string) []string {
matches, err := filepath.Glob(filepath.Join(home, ".nvm", "versions", "node", "*", "bin"))
if err != nil {
return nil
}
sort.SliceStable(matches, func(i, j int) bool {
return compareNVMNodeBinDirs(matches[i], matches[j]) > 0
})
return matches
}
func compareNVMNodeBinDirs(left string, right string) int {
leftVersion := nvmNodeVersion(left)
rightVersion := nvmNodeVersion(right)
for i := 0; i < max(len(leftVersion), len(rightVersion)); i++ {
leftPart := versionPart(leftVersion, i)
rightPart := versionPart(rightVersion, i)
if leftPart > rightPart {
return 1
}
if leftPart < rightPart {
return -1
}
}
return strings.Compare(left, right)
}
func nvmNodeVersion(binDir string) []int {
versionDir := filepath.Base(filepath.Dir(binDir))
version := strings.TrimPrefix(versionDir, "v")
parts := strings.Split(version, ".")
result := make([]int, 0, len(parts))
for _, part := range parts {
value, err := strconv.Atoi(part)
if err != nil {
return nil
}
result = append(result, value)
}
return result
}
func versionPart(version []int, index int) int {
if index < 0 || index >= len(version) {
return 0
}
return version[index]
}
func fnmNodeBinDirs(fnmDir string) []string {
matches, err := filepath.Glob(filepath.Join(fnmDir, "node-versions", "*", "installation", "bin"))
if err != nil {
return nil
}
return matches
}
func mergePathDirs(groups ...[]string) []string {
result := []string{}
seen := map[string]bool{}
for _, group := range groups {
for _, dir := range group {
normalized := strings.TrimSpace(dir)
if normalized == "" {
continue
}
key := pathDirKey(normalized)
if seen[key] {
continue
}
seen[key] = true
result = append(result, normalized)
}
}
return result
}
func pathDirKey(dir string) string {
key := filepath.Clean(strings.TrimSpace(dir))
if runtime.GOOS == "windows" {
key = strings.ToLower(key)
}
return key
}
func executablePathKey(path string) string {
return pathDirKey(path)
}
func pathEnvKey(env []string) string {
for i := len(env) - 1; i >= 0; i-- {
key, _, ok := strings.Cut(env[i], "=")
if ok && strings.EqualFold(key, "PATH") {
return key
}
}
return "PATH"
}
func envValue(env []string, key string) string {
value, _ := envValueFrom(env, key)
return value
}
func envValueFrom(env []string, key string) (string, bool) {
for i := len(env) - 1; i >= 0; i-- {
candidateKey, value, ok := strings.Cut(env[i], "=")
if ok && strings.EqualFold(candidateKey, key) {
return value, true
}
}
return "", false
}
func pathGroupsFromEnv(env []string, key string, basePath string) [][]string {
groups := [][]string{}
baseDirs := splitPathList(basePath)
inheritedBaseDirs := []string{}
for i := len(env) - 1; i >= 0; i-- {
candidateKey, value, ok := strings.Cut(env[i], "=")
if ok && strings.EqualFold(candidateKey, key) {
dirs := splitPathList(value)
if prefix, inherited, ok := splitInheritedPath(dirs, baseDirs); ok {
groups = append(groups, prefix)
if len(inheritedBaseDirs) == 0 {
inheritedBaseDirs = inherited
}
continue
}
groups = append(groups, dirs)
}
}
if len(inheritedBaseDirs) > 0 {
groups = append(groups, inheritedBaseDirs)
}
return groups
}
// splitPathList follows the host separator but also accepts the colon form
// emitted by MSYS/Unix-like test and launcher environments on Windows. A
// native Windows path containing a drive letter or backslashes remains on the
// normal semicolon path-list path.
func splitPathList(value string) []string {
if runtime.GOOS == "windows" && !strings.Contains(value, ";") &&
strings.Contains(value, ":") && !strings.Contains(value, `\`) {
return strings.Split(value, ":")
}
return filepath.SplitList(value)
}
func splitInheritedPath(dirs []string, baseDirs []string) ([]string, []string, bool) {
if len(baseDirs) == 0 || len(dirs) < len(baseDirs) {
return nil, nil, false
}
for i := 0; i+len(baseDirs) <= len(dirs); i++ {
if pathDirSlicesEqual(dirs[i:i+len(baseDirs)], baseDirs) {
return dirs[:i], dirs[i:], true
}
}
return nil, nil, false
}
func pathDirSlicesEqual(a []string, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if pathDirKey(a[i]) != pathDirKey(b[i]) {
return false
}
}
return true
}
func stripEnvKeys(env []string, keys []string) []string {
if len(keys) == 0 {
return env
}
next := make([]string, 0, len(env))
for _, item := range env {
candidateKey, _, ok := strings.Cut(item, "=")
if ok {
drop := false
for _, key := range keys {
if strings.EqualFold(candidateKey, key) {
drop = true
break
}
}
if drop {
continue
}
}
next = append(next, item)
}
return next
}
func setEnvValue(env []string, key string, value string) []string {
next := make([]string, 0, len(env)+1)
for _, item := range env {
candidateKey, _, ok := strings.Cut(item, "=")
if ok && strings.EqualFold(candidateKey, key) {
continue
}
next = append(next, item)
}
return append(next, key+"="+value)
}
@@ -0,0 +1,457 @@
package runtimecmd
import (
"os"
"path/filepath"
"reflect"
"strings"
"testing"
)
func TestResolverFindsKnownNodeGlobalBin(t *testing.T) {
home := t.TempDir()
binDir := filepath.Join(home, ".nvm", "versions", "node", "v24.12.0", "bin")
if err := os.MkdirAll(binDir, 0o755); err != nil {
t.Fatalf("mkdir bin dir: %v", err)
}
binaryPath := filepath.Join(binDir, "codex-acp")
writeExecutable(t, binaryPath)
resolver := Resolver{
Environ: func() []string {
return []string{"PATH=" + strings.Join([]string{filepath.FromSlash("/usr/bin"), filepath.FromSlash("/bin")}, string(os.PathListSeparator))}
},
HomeDir: func() (string, error) {
return home, nil
},
LookPath: func(string) (string, error) {
return "", os.ErrNotExist
},
}
env := resolver.Env(nil)
if got := resolver.Resolve("codex-acp", env); got != binaryPath {
t.Fatalf("Resolve() = %q, want %q", got, binaryPath)
}
if got := resolver.ResolveBinary([]string{"codex-acp"}, nil); got != binaryPath {
t.Fatalf("ResolveBinary() = %q, want %q", got, binaryPath)
}
}
func TestResolverPrefersNewestNVMNodeBin(t *testing.T) {
home := t.TempDir()
oldBinDir := filepath.Join(home, ".nvm", "versions", "node", "v20.19.4", "bin")
newBinDir := filepath.Join(home, ".nvm", "versions", "node", "v24.16.0", "bin")
for _, dir := range []string{oldBinDir, newBinDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("mkdir bin dir: %v", err)
}
writeExecutable(t, filepath.Join(dir, "codex"))
}
resolver := Resolver{
Environ: func() []string {
return []string{"PATH=/usr/bin:/bin"}
},
HomeDir: func() (string, error) {
return home, nil
},
LookPath: func(string) (string, error) {
return "", os.ErrNotExist
},
}
env := resolver.Env(nil)
want := filepath.Join(newBinDir, "codex")
if got := resolver.Resolve("codex", env); got != want {
t.Fatalf("Resolve() = %q, want newest NVM codex %q", got, want)
}
}
func TestResolverPrefersExistingPathOverScannedNVMFallback(t *testing.T) {
home := t.TempDir()
activeBinDir := filepath.Join(home, ".nvm", "versions", "node", "v20.19.4", "bin")
newerBinDir := filepath.Join(home, ".nvm", "versions", "node", "v24.16.0", "bin")
for _, dir := range []string{activeBinDir, newerBinDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("mkdir bin dir: %v", err)
}
writeExecutable(t, filepath.Join(dir, "codex"))
}
resolver := Resolver{
Environ: func() []string {
return []string{
"PATH=" + activeBinDir + string(os.PathListSeparator) + "/usr/bin:/bin",
}
},
HomeDir: func() (string, error) {
return home, nil
},
LookPath: func(string) (string, error) {
return "", os.ErrNotExist
},
}
env := resolver.Env(nil)
want := filepath.Join(activeBinDir, "codex")
if got := resolver.Resolve("codex", env); got != want {
t.Fatalf("Resolve() = %q, want active PATH codex %q", got, want)
}
}
func TestResolverResolveAllReturnsEveryExecutableMatchInPathOrder(t *testing.T) {
home := t.TempDir()
firstDir := filepath.Join(home, "first")
secondDir := filepath.Join(home, "second")
for _, dir := range []string{firstDir, secondDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("mkdir %s: %v", dir, err)
}
writeExecutable(t, filepath.Join(dir, "codex"))
}
resolver := Resolver{IsExecutableFile: func(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}}
env := []string{"PATH=" + strings.Join([]string{firstDir, secondDir, firstDir}, string(os.PathListSeparator))}
got := resolver.ResolveAll("codex", env)
want := []string{filepath.Join(firstDir, "codex"), filepath.Join(secondDir, "codex")}
if !reflect.DeepEqual(got, want) {
t.Fatalf("ResolveAll() = %#v, want %#v", got, want)
}
if selected := resolver.Resolve("codex", env); selected != want[0] {
t.Fatalf("Resolve() = %q, want first candidate %q", selected, want[0])
}
}
func TestResolverResolveAllNamesKeepsDirectoryOrderBeforeLauncherVariant(t *testing.T) {
home := t.TempDir()
firstDir := filepath.Join(home, "first")
secondDir := filepath.Join(home, "second")
for _, path := range []string{
filepath.Join(firstDir, "codex.cmd"),
filepath.Join(secondDir, "codex.exe"),
} {
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("mkdir %s: %v", filepath.Dir(path), err)
}
writeExecutable(t, path)
}
resolver := Resolver{IsExecutableFile: func(path string) bool {
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}}
env := []string{"PATH=" + strings.Join([]string{firstDir, secondDir}, string(os.PathListSeparator))}
got := resolver.ResolveAllNames([]string{"codex.exe", "codex.cmd"}, env)
want := []string{filepath.Join(firstDir, "codex.cmd"), filepath.Join(secondDir, "codex.exe")}
if !reflect.DeepEqual(got, want) {
t.Fatalf("ResolveAllNames() = %#v, want %#v", got, want)
}
}
func TestResolverFindsTuttiBinFallback(t *testing.T) {
home := t.TempDir()
tuttiBinDir := filepath.Join(home, ".tutti", "bin")
if err := os.MkdirAll(tuttiBinDir, 0o755); err != nil {
t.Fatalf("mkdir tutti bin dir: %v", err)
}
tuttiPath := filepath.Join(tuttiBinDir, "tutti")
writeExecutable(t, tuttiPath)
resolver := Resolver{
Environ: func() []string {
return []string{"PATH=/usr/bin:/bin"}
},
HomeDir: func() (string, error) {
return home, nil
},
LookPath: func(string) (string, error) {
return "", os.ErrNotExist
},
}
env := resolver.Env(nil)
if got := resolver.Resolve("tutti", env); got != tuttiPath {
t.Fatalf("Resolve() = %q, want %q", got, tuttiPath)
}
if got := resolver.ResolveBinary([]string{"tutti"}, nil); got != tuttiPath {
t.Fatalf("ResolveBinary() = %q, want %q", got, tuttiPath)
}
}
func TestResolverFindsOpenCodeBinFallback(t *testing.T) {
home := t.TempDir()
opencodeBinDir := filepath.Join(home, ".opencode", "bin")
if err := os.MkdirAll(opencodeBinDir, 0o755); err != nil {
t.Fatalf("mkdir opencode bin dir: %v", err)
}
opencodePath := filepath.Join(opencodeBinDir, "opencode")
writeExecutable(t, opencodePath)
resolver := Resolver{
Environ: func() []string {
return []string{"PATH=/usr/bin:/bin"}
},
HomeDir: func() (string, error) {
return home, nil
},
LookPath: func(string) (string, error) {
return "", os.ErrNotExist
},
}
env := resolver.Env(nil)
if got := resolver.Resolve("opencode", env); got != opencodePath {
t.Fatalf("Resolve() = %q, want %q", got, opencodePath)
}
if got := resolver.ResolveBinary([]string{"opencode"}, nil); got != opencodePath {
t.Fatalf("ResolveBinary() = %q, want %q", got, opencodePath)
}
}
func TestResolverFindsFnmNodeBin(t *testing.T) {
home := t.TempDir()
fnmDir := filepath.Join(home, "custom-fnm")
binDir := filepath.Join(fnmDir, "node-versions", "v24.12.0", "installation", "bin")
if err := os.MkdirAll(binDir, 0o755); err != nil {
t.Fatalf("mkdir bin dir: %v", err)
}
nodePath := filepath.Join(binDir, "node")
writeExecutable(t, nodePath)
resolver := Resolver{
Environ: func() []string {
return []string{
"PATH=/usr/bin:/bin",
"FNM_DIR=" + fnmDir,
}
},
HomeDir: func() (string, error) {
return home, nil
},
LookPath: func(string) (string, error) {
return "", os.ErrNotExist
},
}
env := resolver.Env(nil)
if got := resolver.Resolve("node", env); got != nodePath {
t.Fatalf("Resolve() = %q, want %q", got, nodePath)
}
}
func TestResolverPrefersFnmNodeBinOverExistingPathNode(t *testing.T) {
home := t.TempDir()
fnmDir := filepath.Join(home, "custom-fnm")
fnmBinDir := filepath.Join(fnmDir, "node-versions", "v24.12.0", "installation", "bin")
existingBinDir := filepath.Join(home, "existing", "bin")
for _, dir := range []string{fnmBinDir, existingBinDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("mkdir bin dir: %v", err)
}
}
fnmNodePath := filepath.Join(fnmBinDir, "node")
writeExecutable(t, fnmNodePath)
writeExecutable(t, filepath.Join(existingBinDir, "node"))
resolver := Resolver{
Environ: func() []string {
return []string{
"PATH=" + existingBinDir + string(os.PathListSeparator) + "/usr/bin:/bin",
"FNM_DIR=" + fnmDir,
}
},
HomeDir: func() (string, error) {
return home, nil
},
LookPath: func(string) (string, error) {
return "", os.ErrNotExist
},
}
env := resolver.Env(nil)
if got := resolver.Resolve("node", env); got != fnmNodePath {
t.Fatalf("Resolve() = %q, want %q", got, fnmNodePath)
}
}
func TestResolverPrefersInheritedPathOverBunInstallBin(t *testing.T) {
home := t.TempDir()
bunInstall := filepath.Join(home, "custom-bun")
bunBinDir := filepath.Join(bunInstall, "bin")
pathBinDir := filepath.Join(home, "path-bin")
for _, dir := range []string{bunBinDir, pathBinDir} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("mkdir bin dir: %v", err)
}
}
writeExecutable(t, filepath.Join(bunBinDir, "claude"))
pathClaude := filepath.Join(pathBinDir, "claude")
writeExecutable(t, pathClaude)
resolver := Resolver{
Environ: func() []string {
return []string{
"PATH=" + pathBinDir + string(os.PathListSeparator) + "/usr/bin:/bin",
"BUN_INSTALL=" + bunInstall,
}
},
HomeDir: func() (string, error) {
return home, nil
},
LookPath: func(string) (string, error) {
return "", os.ErrNotExist
},
}
env := resolver.Env(nil)
if got := resolver.Resolve("claude", env); got != pathClaude {
t.Fatalf("Resolve() = %q, want inherited PATH claude %q", got, pathClaude)
}
}
func TestResolverReplacesPathEnv(t *testing.T) {
resolver := Resolver{
Environ: func() []string {
return []string{"PATH=/first", "OTHER=value"}
},
HomeDir: func() (string, error) {
return "", os.ErrNotExist
},
}
env := resolver.Env([]string{"PATH=/override:/first"})
pathCount := 0
pathValue := ""
for _, item := range env {
key, value, ok := strings.Cut(item, "=")
if ok && key == "PATH" {
pathCount++
pathValue = value
}
}
if pathCount != 1 {
t.Fatalf("PATH entry count = %d, want 1 in %#v", pathCount, env)
}
if !strings.HasPrefix(pathValue, "/override") {
t.Fatalf("PATH = %q, want override prefix", pathValue)
}
}
func TestResolverMergesMultiplePathOverrides(t *testing.T) {
resolver := Resolver{
Environ: func() []string {
return []string{"PATH=/usr/bin:/bin"}
},
HomeDir: func() (string, error) {
return "", os.ErrNotExist
},
}
pathValueForTest := func(paths ...string) string {
converted := make([]string, 0, len(paths))
for _, path := range paths {
converted = append(converted, filepath.FromSlash(path))
}
return strings.Join(converted, string(os.PathListSeparator))
}
env := resolver.Env([]string{
"PATH=" + pathValueForTest("/state/bin", "/usr/bin", "/bin"),
"PATH=" + pathValueForTest("/managed/node/bin", "/usr/bin", "/bin"),
})
pathCount := 0
pathValue := ""
for _, item := range env {
key, value, ok := strings.Cut(item, "=")
if ok && key == "PATH" {
pathCount++
pathValue = value
}
}
if pathCount != 1 {
t.Fatalf("PATH entry count = %d, want 1 in %#v", pathCount, env)
}
pathDirs := filepath.SplitList(pathValue)
if len(pathDirs) < 4 ||
pathDirs[0] != filepath.FromSlash("/managed/node/bin") ||
pathDirs[1] != filepath.FromSlash("/state/bin") ||
pathDirs[2] != filepath.FromSlash("/usr/bin") ||
pathDirs[3] != filepath.FromSlash("/bin") {
t.Fatalf("PATH = %q, want override prefixes before inherited base path", pathValue)
}
}
func TestResolverEnvStripsClaudeCodeNestingGuards(t *testing.T) {
resolver := Resolver{
Environ: func() []string {
return []string{
"PATH=/usr/bin",
"CLAUDECODE=1",
"CLAUDE_CODE_ENTRYPOINT=claude-desktop",
"CLAUDE_CODE_SESSION_ID=abc",
"CLAUDE_CODE_CHILD_SESSION=1",
"CLAUDE_CODE_OAUTH_SCOPES=keep-me",
"OTHER=value",
}
},
HomeDir: func() (string, error) {
return "", os.ErrNotExist
},
}
env := resolver.Env(nil)
for _, item := range env {
key, _, _ := strings.Cut(item, "=")
switch key {
case "CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT", "CLAUDE_CODE_SESSION_ID", "CLAUDE_CODE_CHILD_SESSION":
t.Fatalf("nesting guard %q should be stripped, got %#v", key, env)
}
}
if value, ok := envValueFrom(env, "CLAUDE_CODE_OAUTH_SCOPES"); !ok || value != "keep-me" {
t.Fatalf("unrelated CLAUDE_CODE_* var was dropped: %#v", env)
}
if value, ok := envValueFrom(env, "OTHER"); !ok || value != "value" {
t.Fatalf("unrelated var was dropped: %#v", env)
}
}
func TestResolverUserBinInstallDirsPrefersPathEntriesThenFallbacks(t *testing.T) {
home := t.TempDir()
pathDir := filepath.Join(home, "custom-bin")
resolver := Resolver{
Environ: func() []string {
return []string{"PATH=" + pathDir + string(os.PathListSeparator) + "/usr/bin"}
},
HomeDir: func() (string, error) {
return home, nil
},
}
dirs := resolver.UserBinInstallDirs(nil)
if len(dirs) < 4 {
t.Fatalf("len(dirs) = %d, want at least 4; dirs=%#v", len(dirs), dirs)
}
if dirs[0] != pathDir {
t.Fatalf("dirs[0] = %q, want %q", dirs[0], pathDir)
}
if dirs[1] != "/usr/bin" {
t.Fatalf("dirs[1] = %q, want /usr/bin", dirs[1])
}
if dirs[2] != filepath.Join(home, ".local", "bin") {
t.Fatalf("dirs[2] = %q, want fallback ~/.local/bin", dirs[2])
}
if dirs[3] != filepath.Join(home, "bin") {
t.Fatalf("dirs[3] = %q, want fallback ~/bin", dirs[3])
}
}
func writeExecutable(t *testing.T, path string) {
t.Helper()
if err := os.WriteFile(path, []byte("#!/bin/sh\nexit 0\n"), 0o755); err != nil {
t.Fatalf("write executable %s: %v", path, err)
}
}