Repository navigation
fix(sdk/go): re-read edge token on every request #4914
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,7 +10,6 @@ import ( | |
| "os" | ||
| "path/filepath" | ||
| "strings" | ||
| "sync" | ||
| "time" | ||
|
|
||
| "golang.org/x/oauth2" | ||
|
|
@@ -28,32 +27,6 @@ const ( | |
| oidcTokenFile = "oidc_token.json" | ||
| ) | ||
|
|
||
| // edgeTokenLoader provides lazy, thread-safe loading of the edge token | ||
| // from disk. Successful loads are cached; failures are retried so a token | ||
| // created after client construction can be picked up without a restart. | ||
| type edgeTokenLoader struct { | ||
| dir string | ||
| mu sync.Mutex | ||
| token string | ||
| } | ||
|
|
||
| // load returns the edge token string, reading it from disk on the first | ||
| // call. It tries edge_token first, falling back to cf_token for legacy | ||
| // compatibility. Only a successful result is cached. | ||
| func (l *edgeTokenLoader) load() (string, error) { | ||
| l.mu.Lock() | ||
| defer l.mu.Unlock() | ||
| if l.token != "" { | ||
| return l.token, nil | ||
| } | ||
| token, err := readEdgeToken(l.dir) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
| l.token = token | ||
| return token, nil | ||
| } | ||
|
|
||
| // readEdgeToken reads the edge token from the given directory. It tries | ||
| // edge_token first, then cf_token as a legacy fallback. The file content | ||
| // is trimmed of surrounding whitespace. | ||
|
|
@@ -158,14 +131,14 @@ func (d *diskTokenSource) Token() (*oauth2.Token, error) { | |
| // NewClient succeeds even when the token file is missing on disk (FR-007). | ||
| // The error surfaces on the first authentication attempt. | ||
| type lazyEdgeAuth struct { | ||
| loader *edgeTokenLoader | ||
| dir string | ||
| } | ||
|
|
||
| // GetRequestMetadata loads the edge token lazily and returns it as a | ||
| // Bearer authorization header. The first load is cached by the | ||
| // underlying edgeTokenLoader. | ||
| // GetRequestMetadata reads the edge token from disk on every call and | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Notable: The reload now covers gRPC request metadata, but Why this matters: The acceptance criterion of #4515 (subsequent RPCs from an existing client use the new token) is met for Suggested fix: Either add a line to the PR description saying the tunnel is a follow-up (a tracking issue would help it survive the move to ai-openshell/sdk-go), or let Source: goal-alignment agent |
||
| // returns it as a Bearer authorization header. Reading each time picks up | ||
| // tokens rewritten by the CLI without a process restart. | ||
| func (a *lazyEdgeAuth) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) { | ||
| token, err := a.loader.load() | ||
| token, err := readEdgeToken(a.dir) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: Reading Why this matters: The window is narrow and the realistic outcome is one retryable RPC failure, so this is hardening rather than a regression. The OIDC path on main already has the same pattern ( Suggested fix: The cleanest fix is on the writer side: have Source: correctness agent (also flagged by: production agent, gator agent)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: Dropping the cache also changes what happens after a successful load. If Why this matters: Failing after logout is arguably the right outcome and matches Suggested fix: Add a sentence to the PR description that accepts the change and gives the reason (logout now revokes a running client, as the OIDC path already does when Source: goal-alignment agent |
||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| package gateway | ||
|
|
||
| import ( | ||
| "context" | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
|
|
@@ -83,41 +84,66 @@ func TestReadEdgeToken_EmptyFallbackFile(t *testing.T) { | |
| assert.Contains(t, err.Error(), "empty") | ||
| } | ||
|
|
||
| func TestEdgeTokenLoader_Lazy(t *testing.T) { | ||
| func TestLazyEdgeAuth_ReloadsRewrittenToken(t *testing.T) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Minor: The removed Why this matters: That is the exact behavior the compatibility review asks about. Whichever way it gets settled, the contract can change without any test failing. Suggested fix: Add something like Source: test-quality agent |
||
| dir := t.TempDir() | ||
| writeFile(t, dir, edgeTokenFile, "lazy-token") | ||
| writeFile(t, dir, edgeTokenFile, "old-token") | ||
|
|
||
| loader := &edgeTokenLoader{dir: dir} | ||
| auth := &lazyEdgeAuth{dir: dir} | ||
|
|
||
| // First call reads from disk. | ||
| token1, err := loader.load() | ||
| md, err := auth.GetRequestMetadata(context.Background()) | ||
| require.NoError(t, err) | ||
| assert.Equal(t, "lazy-token", token1) | ||
| assert.Equal(t, "Bearer old-token", md["authorization"]) | ||
|
|
||
| // Remove the file. Second call should return cached value. | ||
| require.NoError(t, os.Remove(filepath.Join(dir, edgeTokenFile))) | ||
| writeFile(t, dir, edgeTokenFile, "new-token") | ||
|
|
||
| token2, err := loader.load() | ||
| md, err = auth.GetRequestMetadata(context.Background()) | ||
| require.NoError(t, err) | ||
| assert.Equal(t, "lazy-token", token2, "should return cached token") | ||
| assert.Equal(t, "Bearer new-token", md["authorization"]) | ||
| } | ||
|
|
||
| func TestEdgeTokenLoader_LazyError(t *testing.T) { | ||
| func TestLazyEdgeAuth_PicksUpLateToken(t *testing.T) { | ||
| dir := t.TempDir() | ||
| // No token files exist. | ||
|
|
||
| loader := &edgeTokenLoader{dir: dir} | ||
| auth := &lazyEdgeAuth{dir: dir} | ||
|
|
||
| // First call fails. | ||
| _, err1 := loader.load() | ||
| require.Error(t, err1) | ||
| _, err := auth.GetRequestMetadata(context.Background()) | ||
| require.ErrorIs(t, err, ErrTokenLoad) | ||
|
|
||
| // Write the file now. A transient load failure must not be cached. | ||
| writeFile(t, dir, edgeTokenFile, "late-token") | ||
|
|
||
| token, err2 := loader.load() | ||
| require.NoError(t, err2) | ||
| assert.Equal(t, "late-token", token) | ||
| md, err := auth.GetRequestMetadata(context.Background()) | ||
| require.NoError(t, err) | ||
| assert.Equal(t, "Bearer late-token", md["authorization"]) | ||
| } | ||
|
|
||
| func TestLazyEdgeAuth_TokenRemovedAfterSuccess(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| mutate func(t *testing.T, dir string) | ||
| }{ | ||
| {"removed", func(t *testing.T, dir string) { | ||
| require.NoError(t, os.Remove(filepath.Join(dir, edgeTokenFile))) | ||
| }}, | ||
| {"empty", func(t *testing.T, dir string) { | ||
| writeFile(t, dir, edgeTokenFile, "") | ||
| }}, | ||
| } | ||
| for _, tc := range tests { | ||
| t.Run(tc.name, func(t *testing.T) { | ||
| dir := t.TempDir() | ||
| writeFile(t, dir, edgeTokenFile, "tok") | ||
| auth := &lazyEdgeAuth{dir: dir} | ||
|
|
||
| _, err := auth.GetRequestMetadata(context.Background()) | ||
| require.NoError(t, err) | ||
|
|
||
| tc.mutate(t, dir) | ||
|
|
||
| _, err = auth.GetRequestMetadata(context.Background()) | ||
| require.ErrorIs(t, err, ErrTokenLoad) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| // --- T015: diskTokenSource tests --- | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor: With per-call reads, the package doc in
doc.go:55-56still says token files "are loaded lazily on first authentication attempt". That no longer holds foredge_tokenandcf_token, which are now read on every RPC. (doc.gois outside this diff, hence the comment here.)Why this matters: Go SDK consumers learn the contract from the godoc. Reading it, they would expect a one-time load and would not expect that rotating or deleting the file affects a client that has already authenticated.
Suggested fix: Reword
doc.go:55-56to say token files are read on each authentication attempt, so tokens refreshed by the CLI are picked up. ThelazyEdgeAuthcomment at lines 131-132 is still accurate for the missing-at-construction case. If you touch it anyway, something like "A missing or empty token file surfaces asErrTokenLoadon the RPC that needs it" covers both cases.Source: architecture agent