Skip to content

Commit d1b334b

Browse files
committed
fix(daemon): skip codex sync when auth is unavailable
1 parent 77cd44b commit d1b334b

4 files changed

Lines changed: 219 additions & 14 deletions

File tree

cmd/daemon/main.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -148,12 +148,23 @@ func main() {
148148
}
149149
}
150150

151-
codexUsageSyncService := daemon.NewCodexUsageSyncService(cfg)
152-
if err := codexUsageSyncService.Start(ctx); err != nil {
153-
slog.Error("Failed to start Codex usage sync service", slog.Any("err", err))
151+
codexInstalled, err := daemon.CodexInstallationStatus()
152+
if err != nil {
153+
if reason, ok := daemon.CodexSyncSkipReason(err); ok {
154+
slog.Info("Skipping Codex usage sync service startup", slog.String("reason", reason))
155+
} else {
156+
slog.Error("Failed to check Codex installation status", slog.Any("err", err))
157+
}
158+
} else if !codexInstalled {
159+
slog.Info("Skipping Codex usage sync service startup", slog.String("reason", "codex_not_configured"))
154160
} else {
155-
slog.Info("Codex usage sync service started")
156-
defer codexUsageSyncService.Stop()
161+
codexUsageSyncService := daemon.NewCodexUsageSyncService(cfg)
162+
if err := codexUsageSyncService.Start(ctx); err != nil {
163+
slog.Error("Failed to start Codex usage sync service", slog.Any("err", err))
164+
} else {
165+
slog.Info("Codex usage sync service started")
166+
defer codexUsageSyncService.Stop()
167+
}
157168
}
158169

159170
// Create processor instance

daemon/codex_ratelimit.go

Lines changed: 95 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package daemon
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"net/http"
89
"os"
@@ -16,6 +17,14 @@ const codexUsageCacheTTL = 10 * time.Minute
1617
var (
1718
loadCodexAuthFunc = loadCodexAuth
1819
fetchCodexUsageFunc = fetchCodexUsage
20+
codexPathExistsFunc = codexPathExists
21+
)
22+
23+
var (
24+
errCodexDirMissing = errors.New("codex directory missing")
25+
errCodexAuthFileMissing = errors.New("codex auth file missing")
26+
errCodexAuthInvalid = errors.New("codex auth invalid")
27+
errCodexTokenInvalid = errors.New("codex token invalid")
1928
)
2029

2130
// CodexRateLimitData holds the parsed rate limit data from the Codex API
@@ -63,15 +72,79 @@ type codexIDTokenClaims struct {
6372
AccountID string `json:"accountId"`
6473
}
6574

75+
func codexConfigDirPath() (string, error) {
76+
homeDir, err := os.UserHomeDir()
77+
if err != nil {
78+
return "", fmt.Errorf("failed to get home directory: %w", err)
79+
}
80+
81+
return filepath.Join(homeDir, ".codex"), nil
82+
}
83+
84+
func codexAuthFilePath() (string, error) {
85+
dir, err := codexConfigDirPath()
86+
if err != nil {
87+
return "", err
88+
}
89+
90+
return filepath.Join(dir, "auth.json"), nil
91+
}
92+
93+
func codexPathExists(path string) (bool, error) {
94+
_, err := os.Stat(path)
95+
if err == nil {
96+
return true, nil
97+
}
98+
if errors.Is(err, os.ErrNotExist) {
99+
return false, nil
100+
}
101+
return false, err
102+
}
103+
104+
func codexInstallationStatus() (bool, error) {
105+
dirPath, err := codexConfigDirPath()
106+
if err != nil {
107+
return false, err
108+
}
109+
exists, err := codexPathExistsFunc(dirPath)
110+
if err != nil {
111+
return false, err
112+
}
113+
if !exists {
114+
return false, errCodexDirMissing
115+
}
116+
117+
authPath, err := codexAuthFilePath()
118+
if err != nil {
119+
return false, err
120+
}
121+
exists, err = codexPathExistsFunc(authPath)
122+
if err != nil {
123+
return false, err
124+
}
125+
if !exists {
126+
return false, errCodexAuthFileMissing
127+
}
128+
129+
return true, nil
130+
}
131+
132+
func CodexInstallationStatus() (bool, error) {
133+
return codexInstallationStatus()
134+
}
135+
66136
// loadCodexAuth reads the Codex authentication data from ~/.codex/auth.json.
67137
func loadCodexAuth() (*codexAuthData, error) {
68-
homeDir, err := os.UserHomeDir()
138+
authPath, err := codexAuthFilePath()
69139
if err != nil {
70-
return nil, fmt.Errorf("failed to get home directory: %w", err)
140+
return nil, err
71141
}
72142

73-
data, err := os.ReadFile(filepath.Join(homeDir, ".codex", "auth.json"))
143+
data, err := os.ReadFile(authPath)
74144
if err != nil {
145+
if errors.Is(err, os.ErrNotExist) {
146+
return nil, errCodexAuthFileMissing
147+
}
75148
return nil, fmt.Errorf("codex auth file read failed: %w", err)
76149
}
77150

@@ -81,7 +154,7 @@ func loadCodexAuth() (*codexAuthData, error) {
81154
}
82155

83156
if auth.TokenData == nil || auth.TokenData.AccessToken == "" {
84-
return nil, fmt.Errorf("no access token found in codex auth")
157+
return nil, errCodexAuthInvalid
85158
}
86159

87160
accountID := ""
@@ -133,6 +206,9 @@ func fetchCodexUsage(ctx context.Context, auth *codexAuthData) (*CodexRateLimitD
133206
defer resp.Body.Close()
134207

135208
if resp.StatusCode != http.StatusOK {
209+
if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden {
210+
return nil, errCodexTokenInvalid
211+
}
136212
return nil, fmt.Errorf("codex usage API returned status %d", resp.StatusCode)
137213
}
138214

@@ -172,3 +248,18 @@ func shortenCodexAPIError(err error) string {
172248

173249
return "network"
174250
}
251+
252+
func CodexSyncSkipReason(err error) (string, bool) {
253+
switch {
254+
case errors.Is(err, errCodexDirMissing):
255+
return "missing_codex_dir", true
256+
case errors.Is(err, errCodexAuthFileMissing):
257+
return "missing_auth_file", true
258+
case errors.Is(err, errCodexAuthInvalid):
259+
return "invalid_auth", true
260+
case errors.Is(err, errCodexTokenInvalid):
261+
return "invalid_auth_token", true
262+
default:
263+
return "", false
264+
}
265+
}

daemon/codex_usage_sync.go

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package daemon
22

33
import (
44
"context"
5-
"fmt"
65
"log/slog"
76
"sync"
87
"time"
@@ -70,6 +69,10 @@ func (s *CodexUsageSyncService) sync() {
7069
}
7170

7271
if err := syncCodexUsage(context.Background(), s.config); err != nil {
72+
if reason, ok := CodexSyncSkipReason(err); ok {
73+
slog.Info("Skipping codex usage sync", slog.String("reason", reason))
74+
return
75+
}
7376
slog.Warn("Failed to sync codex usage", slog.Any("err", err))
7477
}
7578
}
@@ -83,12 +86,12 @@ func syncCodexUsage(ctx context.Context, config model.ShellTimeConfig) error {
8386
defer cancel()
8487

8588
auth, err := loadCodexAuthFunc()
86-
if err != nil || auth == nil {
87-
if err == nil && auth == nil {
88-
err = fmt.Errorf("codex auth unavailable")
89-
}
89+
if err != nil {
9090
return err
9191
}
92+
if auth == nil {
93+
return errCodexAuthInvalid
94+
}
9295

9396
usage, err := fetchCodexUsageFunc(runCtx, auth)
9497
if err != nil {

daemon/codex_usage_sync_test.go

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package daemon
33
import (
44
"context"
55
"encoding/json"
6+
"errors"
67
"net/http"
78
"net/http/httptest"
89
"sync/atomic"
@@ -99,6 +100,105 @@ func TestSyncCodexUsage_AuthError(t *testing.T) {
99100
assert.ErrorIs(t, err, assert.AnError)
100101
}
101102

103+
func TestSyncCodexUsage_InvalidTokenSkips(t *testing.T) {
104+
t.Helper()
105+
106+
originalLoad := loadCodexAuthFunc
107+
originalFetch := fetchCodexUsageFunc
108+
defer func() {
109+
loadCodexAuthFunc = originalLoad
110+
fetchCodexUsageFunc = originalFetch
111+
}()
112+
113+
loadCodexAuthFunc = func() (*codexAuthData, error) {
114+
return &codexAuthData{AccessToken: "test-token"}, nil
115+
}
116+
fetchCodexUsageFunc = func(ctx context.Context, auth *codexAuthData) (*CodexRateLimitData, error) {
117+
return nil, errCodexTokenInvalid
118+
}
119+
120+
cfg := model.ShellTimeConfig{Token: "shelltime-token"}
121+
err := syncCodexUsage(context.Background(), cfg)
122+
require.Error(t, err)
123+
124+
reason, ok := CodexSyncSkipReason(err)
125+
require.True(t, ok)
126+
assert.Equal(t, "invalid_auth_token", reason)
127+
}
128+
129+
func TestCodexInstallationStatus_MissingCodexDir(t *testing.T) {
130+
originalExists := codexPathExistsFunc
131+
defer func() {
132+
codexPathExistsFunc = originalExists
133+
}()
134+
135+
codexPathExistsFunc = func(path string) (bool, error) {
136+
return false, nil
137+
}
138+
139+
ok, err := CodexInstallationStatus()
140+
assert.False(t, ok)
141+
assert.ErrorIs(t, err, errCodexDirMissing)
142+
}
143+
144+
func TestCodexInstallationStatus_MissingAuthFile(t *testing.T) {
145+
originalExists := codexPathExistsFunc
146+
defer func() {
147+
codexPathExistsFunc = originalExists
148+
}()
149+
150+
callCount := 0
151+
codexPathExistsFunc = func(path string) (bool, error) {
152+
callCount++
153+
if callCount == 1 {
154+
return true, nil
155+
}
156+
return false, nil
157+
}
158+
159+
ok, err := CodexInstallationStatus()
160+
assert.False(t, ok)
161+
assert.ErrorIs(t, err, errCodexAuthFileMissing)
162+
}
163+
164+
func TestCodexInstallationStatus_Ready(t *testing.T) {
165+
originalExists := codexPathExistsFunc
166+
defer func() {
167+
codexPathExistsFunc = originalExists
168+
}()
169+
170+
codexPathExistsFunc = func(path string) (bool, error) {
171+
return true, nil
172+
}
173+
174+
ok, err := CodexInstallationStatus()
175+
require.NoError(t, err)
176+
assert.True(t, ok)
177+
}
178+
179+
func TestCodexSyncSkipReason(t *testing.T) {
180+
testCases := []struct {
181+
name string
182+
err error
183+
expected string
184+
ok bool
185+
}{
186+
{name: "missing dir", err: errCodexDirMissing, expected: "missing_codex_dir", ok: true},
187+
{name: "missing auth file", err: errCodexAuthFileMissing, expected: "missing_auth_file", ok: true},
188+
{name: "invalid auth", err: errCodexAuthInvalid, expected: "invalid_auth", ok: true},
189+
{name: "invalid token", err: errCodexTokenInvalid, expected: "invalid_auth_token", ok: true},
190+
{name: "other error", err: errors.New("boom"), expected: "", ok: false},
191+
}
192+
193+
for _, tc := range testCases {
194+
t.Run(tc.name, func(t *testing.T) {
195+
reason, ok := CodexSyncSkipReason(tc.err)
196+
assert.Equal(t, tc.ok, ok)
197+
assert.Equal(t, tc.expected, reason)
198+
})
199+
}
200+
}
201+
102202
func TestCodexUsageSyncService_StartRunsImmediatelyAndOnTicker(t *testing.T) {
103203
t.Helper()
104204

0 commit comments

Comments
 (0)