Skip to content

Commit 89b1950

Browse files
committed
feat: improve desktop agent lifecycle UX
1 parent 5ca3acd commit 89b1950

25 files changed

Lines changed: 496 additions & 163 deletions

core/internal/apply/codex_install.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,16 @@ func ResolveCodexExecutable() (string, bool) {
6161
return p, true
6262
}
6363
home, _ := os.UserHomeDir()
64-
for _, dir := range CodexSearchDirs(home) {
64+
seen := map[string]struct{}{}
65+
for _, dir := range append(CodexSearchDirs(home), executableSearchDirs()...) {
66+
dir = strings.TrimSpace(dir)
67+
if dir == "" {
68+
continue
69+
}
70+
if _, ok := seen[dir]; ok {
71+
continue
72+
}
73+
seen[dir] = struct{}{}
6574
for _, name := range codexCommandNames() {
6675
candidate := filepath.Join(dir, name)
6776
if codexExecutableFile(candidate) && !codexClientExecutablePath(candidate) {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package apply
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"runtime"
8+
"strings"
9+
)
10+
11+
func resolveExecutableBySearch(name string) (string, error) {
12+
for _, candidate := range executableSearchCandidates(name) {
13+
if regularFile(candidate) {
14+
return candidate, nil
15+
}
16+
}
17+
return "", fmt.Errorf("executable not found: %s", name)
18+
}
19+
20+
func executableSearchCandidates(name string) []string {
21+
name = strings.TrimSpace(name)
22+
if name == "" {
23+
return nil
24+
}
25+
var out []string
26+
seen := map[string]struct{}{}
27+
add := func(path string) {
28+
path = strings.TrimSpace(path)
29+
if path == "" {
30+
return
31+
}
32+
cleaned := filepath.Clean(path)
33+
if _, ok := seen[cleaned]; ok {
34+
return
35+
}
36+
seen[cleaned] = struct{}{}
37+
out = append(out, cleaned)
38+
}
39+
for _, dir := range executableSearchDirs() {
40+
for _, file := range executableCommandNames(name) {
41+
add(filepath.Join(dir, file))
42+
}
43+
}
44+
return out
45+
}
46+
47+
func executableCommandNames(name string) []string {
48+
name = strings.TrimSpace(name)
49+
if name == "" {
50+
return nil
51+
}
52+
if runtime.GOOS != "windows" {
53+
return []string{name}
54+
}
55+
lower := strings.ToLower(name)
56+
out := []string{name}
57+
for _, ext := range []string{".exe", ".cmd", ".ps1", ".bat"} {
58+
if strings.HasSuffix(lower, ext) {
59+
continue
60+
}
61+
out = append(out, name+ext)
62+
}
63+
return out
64+
}
65+
66+
func executableSearchDirs() []string {
67+
var dirs []string
68+
seen := map[string]struct{}{}
69+
add := func(dir string) {
70+
dir = strings.TrimSpace(dir)
71+
if dir == "" {
72+
return
73+
}
74+
cleaned := filepath.Clean(dir)
75+
if _, ok := seen[cleaned]; ok {
76+
return
77+
}
78+
seen[cleaned] = struct{}{}
79+
dirs = append(dirs, cleaned)
80+
}
81+
for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
82+
add(dir)
83+
}
84+
home, _ := os.UserHomeDir()
85+
if strings.TrimSpace(home) != "" {
86+
home = filepath.Clean(home)
87+
add(filepath.Join(home, "bin"))
88+
add(filepath.Join(home, ".local", "bin"))
89+
add(filepath.Join(home, ".npm-global", "bin"))
90+
add(filepath.Join(home, ".volta", "bin"))
91+
add(filepath.Join(home, ".local", "share", "fnm", "current", "bin"))
92+
add(filepath.Join(home, ".fnm", "current", "bin"))
93+
add(filepath.Join(home, "Library", "pnpm"))
94+
add(filepath.Join(home, ".opencode", "bin"))
95+
}
96+
for _, dir := range platformExecutableSearchDirs(home) {
97+
add(dir)
98+
}
99+
for _, dir := range loginShellExecutableSearchDirs() {
100+
add(dir)
101+
}
102+
if runtime.GOOS != "windows" {
103+
add("/opt/homebrew/bin")
104+
add("/opt/homebrew/sbin")
105+
add("/usr/local/bin")
106+
add("/usr/local/sbin")
107+
add("/usr/bin")
108+
add("/bin")
109+
}
110+
return dirs
111+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
//go:build darwin
2+
3+
package apply
4+
5+
import (
6+
"os"
7+
"os/exec"
8+
"path/filepath"
9+
"strings"
10+
"sync"
11+
)
12+
13+
var (
14+
darwinLoginShellDirsOnce sync.Once
15+
darwinLoginShellDirs []string
16+
)
17+
18+
func loginShellExecutableSearchDirs() []string {
19+
darwinLoginShellDirsOnce.Do(func() {
20+
home, err := os.UserHomeDir()
21+
if err != nil || strings.TrimSpace(home) == "" {
22+
return
23+
}
24+
user := strings.TrimSpace(os.Getenv("USER"))
25+
if user == "" {
26+
user = strings.TrimSpace(os.Getenv("LOGNAME"))
27+
}
28+
shell := strings.TrimSpace(os.Getenv("SHELL"))
29+
if shell == "" {
30+
shell = "/bin/zsh"
31+
}
32+
cmd := exec.Command(shell, "-ilc", `printf %s "$PATH"`)
33+
cmd.Env = []string{
34+
"HOME=" + home,
35+
"USER=" + user,
36+
"LOGNAME=" + user,
37+
"SHELL=" + shell,
38+
}
39+
out, err := cmd.Output()
40+
if err != nil {
41+
return
42+
}
43+
seen := map[string]struct{}{}
44+
for _, dir := range filepath.SplitList(strings.TrimSpace(string(out))) {
45+
dir = strings.TrimSpace(dir)
46+
if dir == "" {
47+
continue
48+
}
49+
if _, ok := seen[dir]; ok {
50+
continue
51+
}
52+
seen[dir] = struct{}{}
53+
darwinLoginShellDirs = append(darwinLoginShellDirs, dir)
54+
}
55+
})
56+
return darwinLoginShellDirs
57+
}
58+
59+
func platformExecutableSearchDirs(home string) []string {
60+
if strings.TrimSpace(home) == "" {
61+
return nil
62+
}
63+
home = filepath.Clean(home)
64+
return []string{
65+
filepath.Join(home, "bin"),
66+
filepath.Join(home, ".npm-global", "bin"),
67+
filepath.Join(home, "Library", "pnpm"),
68+
filepath.Join(home, ".volta", "bin"),
69+
filepath.Join(home, ".local", "share", "fnm", "current", "bin"),
70+
filepath.Join(home, ".fnm", "current", "bin"),
71+
}
72+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
//go:build !darwin
2+
3+
package apply
4+
5+
func loginShellExecutableSearchDirs() []string {
6+
return nil
7+
}
8+
9+
func platformExecutableSearchDirs(home string) []string {
10+
return nil
11+
}

core/internal/apply/lifecycle.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -546,14 +546,20 @@ func resolveExecutable(name string) (string, error) {
546546
if err != nil && runtime.GOOS == "windows" {
547547
path, err = exec.LookPath(name)
548548
}
549-
if err != nil && runtime.GOOS == "windows" && strings.EqualFold(name, "npm") {
549+
if err == nil {
550+
return path, nil
551+
}
552+
if p, searchErr := resolveExecutableBySearch(name); searchErr == nil {
553+
return p, nil
554+
}
555+
if runtime.GOOS == "windows" && strings.EqualFold(name, "npm") {
550556
for _, candidate := range windowsNPMCandidates() {
551557
if regularFile(candidate) {
552558
return candidate, nil
553559
}
554560
}
555561
}
556-
if err != nil && runtime.GOOS == "windows" && strings.EqualFold(name, "python") {
562+
if runtime.GOOS == "windows" && strings.EqualFold(name, "python") {
557563
if py, pyErr := exec.LookPath("py.exe"); pyErr == nil {
558564
return py, nil
559565
}

core/internal/buildinfo/buildinfo.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import "strings"
44

55
// Set at link time via -ldflags (see .goreleaser.yaml).
66
var (
7-
Version = "dev0.1.93"
7+
Version = "dev0.1.98"
88
Commit = "none"
99
Date = "unknown"
1010
)

core/internal/proxy/server.go

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"errors"
99
"fmt"
1010
"io"
11-
"net"
1211
"net/http"
1312
"strconv"
1413
"strings"
@@ -73,38 +72,16 @@ func NewServer(cfg profile.ProxyConfig) *Server {
7372
}
7473
mux := http.NewServeMux()
7574
mux.HandleFunc("/health", s.handleHealth)
76-
mux.HandleFunc("/__debug/call-log", s.localOnly(s.handleDebugCallLog))
77-
mux.HandleFunc("/__debug/system-log", s.localOnly(s.handleDebugSystemLog))
78-
mux.HandleFunc("/__debug/transform-request", s.localOnly(s.handleDebugTransform))
79-
mux.HandleFunc("/__debug/resolve-route", s.localOnly(s.handleDebugResolveRoute))
80-
mux.HandleFunc("/__debug/shutdown", s.localOnly(s.handleDebugShutdown))
75+
mux.HandleFunc("/__debug/call-log", s.handleDebugCallLog)
76+
mux.HandleFunc("/__debug/system-log", s.handleDebugSystemLog)
77+
mux.HandleFunc("/__debug/transform-request", s.handleDebugTransform)
78+
mux.HandleFunc("/__debug/resolve-route", s.handleDebugResolveRoute)
79+
mux.HandleFunc("/__debug/shutdown", s.handleDebugShutdown)
8180
mux.HandleFunc("/", s.handleProxy)
8281
s.Server = &http.Server{Addr: cfg.Host + ":" + strconv.Itoa(cfg.Port), Handler: mux}
8382
return s
8483
}
8584

86-
func (s *Server) localOnly(next http.HandlerFunc) http.HandlerFunc {
87-
return func(w http.ResponseWriter, r *http.Request) {
88-
if s.Config.DebugLocalOnly && !isLocalRequest(r) {
89-
writeJSON(w, http.StatusForbidden, map[string]string{"error": "debug endpoints are only available from loopback clients"})
90-
return
91-
}
92-
next(w, r)
93-
}
94-
}
95-
96-
func isLocalRequest(r *http.Request) bool {
97-
if r == nil {
98-
return false
99-
}
100-
host, _, err := net.SplitHostPort(strings.TrimSpace(r.RemoteAddr))
101-
if err != nil {
102-
host = strings.TrimSpace(r.RemoteAddr)
103-
}
104-
ip := net.ParseIP(host)
105-
return ip != nil && ip.IsLoopback()
106-
}
107-
10885
func (s *Server) loadStore() (*profile.Store, error) {
10986
loader := s.ProfileLoader
11087
if loader == nil {

core/internal/proxy/server_test.go

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ func TestDebugSystemLogPaginatesDefaultLimit(t *testing.T) {
136136
syslog.Write("system", "entry-"+strconv.Itoa(i))
137137
}
138138
s := NewServer(profile.ProxyConfig{Host: "127.0.0.1", Port: 27483})
139+
t.Cleanup(func() { _ = s.CallLogs.Close() })
139140
ts := httptest.NewServer(s.Server.Handler)
140141
defer ts.Close()
141142

@@ -173,24 +174,11 @@ func TestDebugRoutesAllowNonLoopbackClientsByDefault(t *testing.T) {
173174
}
174175
}
175176

176-
func TestDebugRoutesRejectNonLoopbackClientsWhenLocalOnly(t *testing.T) {
177-
s := NewServer(profile.ProxyConfig{Host: "0.0.0.0", Port: 27483, DebugLocalOnly: true})
178-
req := httptest.NewRequest(http.MethodGet, "http://example.com/__debug/call-log", nil)
179-
req.RemoteAddr = "203.0.113.10:45678"
180-
rec := httptest.NewRecorder()
181-
182-
s.Server.Handler.ServeHTTP(rec, req)
183-
184-
if rec.Code != http.StatusForbidden {
185-
t.Fatalf("status = %d, want %d", rec.Code, http.StatusForbidden)
186-
}
187-
}
188-
189-
func TestDebugRoutesAllowLoopbackClientsWhenLocalOnly(t *testing.T) {
177+
func TestDebugRoutesIgnoreLocalOnlyCompatibilityFlag(t *testing.T) {
190178
s := NewServer(profile.ProxyConfig{Host: "0.0.0.0", Port: 27483, DebugLocalOnly: true})
191179
s.CallLogs = openTestCallLogStore(t)
192180
req := httptest.NewRequest(http.MethodGet, "http://example.com/__debug/call-log", nil)
193-
req.RemoteAddr = "127.0.0.1:45678"
181+
req.RemoteAddr = "203.0.113.10:45678"
194182
rec := httptest.NewRecorder()
195183

196184
s.Server.Handler.ServeHTTP(rec, req)

core/internal/subscriptionauth/oauth.go

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,6 @@ const (
4848
// claudeTokenURL is a var (not const) so tests can point it at a local server.
4949
var claudeTokenURL = "https://platform.claude.com/v1/oauth/token"
5050

51-
// oauthHTTPClient deliberately ignores HTTP_PROXY/HTTPS_PROXY/ALL_PROXY.
52-
// OAuth callback/token/profile traffic should not be routed through user API proxies.
53-
var oauthHTTPClient = &http.Client{Transport: oauthDirectTransport()}
54-
55-
func oauthDirectTransport() *http.Transport {
56-
transport := http.DefaultTransport.(*http.Transport).Clone()
57-
transport.Proxy = nil
58-
return transport
59-
}
60-
6151
type LoginResult struct {
6252
OK bool `json:"ok"`
6353
LoggedIn bool `json:"loggedIn,omitempty"`
@@ -160,7 +150,7 @@ func loginCodex(ctx context.Context, openBrowser bool) (string, error) {
160150
if err != nil {
161151
return "", err
162152
}
163-
redirectURI := fmt.Sprintf("http://localhost:%d%s", codexCallbackPort, codexCallbackPath)
153+
redirectURI := codexRedirectURI()
164154
server, err := startCallbackServer(ctx, callbackOptions{
165155
Port: codexCallbackPort,
166156
Path: codexCallbackPath,
@@ -239,6 +229,9 @@ func buildClaudeAuthorizeURL(pkce pkcePair, redirectURI string) string {
239229
return claudeAuthorizeURL + "?" + q.Encode()
240230
}
241231

232+
func codexRedirectURI() string {
233+
return fmt.Sprintf("http://127.0.0.1:%d%s", codexCallbackPort, codexCallbackPath)
234+
}
242235
func buildCodexAuthorizeURL(pkce pkcePair, state, redirectURI string) string {
243236
q := url.Values{}
244237
q.Set("response_type", "code")
@@ -398,7 +391,7 @@ func enrichClaudeProfile(ctx context.Context, creds *tokenCredentials) error {
398391
}
399392
req.Header.Set("Authorization", "Bearer "+creds.Access)
400393
req.Header.Set("Accept", "application/json")
401-
resp, err := oauthHTTPClient.Do(req)
394+
resp, err := http.DefaultClient.Do(req)
402395
if err != nil {
403396
return err
404397
}
@@ -462,7 +455,7 @@ func postForm(ctx context.Context, rawURL string, form url.Values, dest any) err
462455
}
463456

464457
func doJSON(req *http.Request, dest any, label string) error {
465-
resp, err := oauthHTTPClient.Do(req)
458+
resp, err := http.DefaultClient.Do(req)
466459
if err != nil {
467460
return err
468461
}

0 commit comments

Comments
 (0)