Skip to content

Commit 913eef9

Browse files
Merge pull request #91 from 73ai/fix-scope-waiter-race
Fix race condition in ScopeWaiter consent flow
2 parents fa97c54 + 3292d3c commit 913eef9

File tree

2 files changed

+27
-6
lines changed

2 files changed

+27
-6
lines changed

agent/tools/gws_execute.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,14 +210,17 @@ func (g *GWSExecuteTool) requestConsent(ctx context.Context, scopes []string) er
210210
if err != nil {
211211
return fmt.Errorf("generate auth URL: %w", err)
212212
}
213+
// Register before notifying so Signal can't race ahead of Await.
214+
g.scopeWaiter.Register(state, scopes, g.account)
215+
213216
if err := g.interactor.Notify("I need additional Google access to complete this request."); err != nil {
214217
return fmt.Errorf("notify: %w", err)
215218
}
216219
if err := g.interactor.NotifyLink("Tap to grant access", url); err != nil {
217220
return fmt.Errorf("notify link: %w", err)
218221
}
219222

220-
if err := g.scopeWaiter.Wait(state, g.authTimeout, scopes, g.account); err != nil {
223+
if err := g.scopeWaiter.Await(state, g.authTimeout); err != nil {
221224
return fmt.Errorf("auth: %w", err)
222225
}
223226

oauth/google/waiter.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,28 @@ func NewScopeWaiter() *ScopeWaiter {
2626
return &ScopeWaiter{pending: make(map[string]*pendingAuth)}
2727
}
2828

29-
// Wait blocks until Signal is called for the given state or the timeout expires.
30-
// The scopes and account are stored so the callback can retrieve them via Lookup.
31-
func (w *ScopeWaiter) Wait(state string, timeout time.Duration, scopes []string, account string) error {
29+
// Register adds a pending auth entry so that Signal can resolve it
30+
// even before Await is called — eliminating the race where Signal
31+
// fires between NotifyLink and Await.
32+
func (w *ScopeWaiter) Register(state string, scopes []string, account string) {
3233
w.mu.Lock()
33-
p := &pendingAuth{
34+
w.pending[state] = &pendingAuth{
3435
ch: make(chan error, 1),
3536
scopes: scopes,
3637
account: account,
3738
}
38-
w.pending[state] = p
3939
w.mu.Unlock()
40+
}
41+
42+
// Await blocks until Signal is called for a previously registered state
43+
// or the timeout expires.
44+
func (w *ScopeWaiter) Await(state string, timeout time.Duration) error {
45+
w.mu.Lock()
46+
p, ok := w.pending[state]
47+
w.mu.Unlock()
48+
if !ok {
49+
return errors.New("state not registered")
50+
}
4051

4152
defer func() {
4253
w.mu.Lock()
@@ -52,6 +63,13 @@ func (w *ScopeWaiter) Wait(state string, timeout time.Duration, scopes []string,
5263
}
5364
}
5465

66+
// Wait registers a state and blocks until Signal is called or timeout.
67+
// For race-free usage prefer Register + Await.
68+
func (w *ScopeWaiter) Wait(state string, timeout time.Duration, scopes []string, account string) error {
69+
w.Register(state, scopes, account)
70+
return w.Await(state, timeout)
71+
}
72+
5573
// Lookup returns the scopes and account associated with a pending state.
5674
// Returns false if no pending auth exists for that state.
5775
func (w *ScopeWaiter) Lookup(state string) (scopes []string, account string, ok bool) {

0 commit comments

Comments
 (0)