Skip to content

Enforce browser control ownership#119

Merged
glittercowboy merged 3 commits intomainfrom
codex/unified-web-surface-daemon
Apr 30, 2026
Merged

Enforce browser control ownership#119
glittercowboy merged 3 commits intomainfrom
codex/unified-web-surface-daemon

Conversation

@glittercowboy
Copy link
Copy Markdown
Contributor

@glittercowboy glittercowboy commented Apr 30, 2026

Summary

  • bumps protocol-go to v0.31.0
  • enforces browser control ownership and stale input rejection
  • forwards frame viewport metadata and input acknowledgement fields

Dependency / merge order

  • Depends on protocol-go v0.31.0.
  • Merge before gsd-cloud unified web surface PR.

Verification

  • GOCACHE=/private/tmp/gsd-daemon-unified-web-surface-gocache GOMODCACHE=/private/tmp/gsd-daemon-unified-web-surface-gomodcache go test ./...
  • GOCACHE=/private/tmp/gsd-daemon-unified-web-surface-gocache GOMODCACHE=/private/tmp/gsd-daemon-unified-web-surface-gomodcache go build -o gsd-cloud .

Post-merge

  • Daemon runtime behavior changed; release tag required after merge.

Summary by CodeRabbit

  • New Features

    • Improved session management with per-session concurrency control and versioning to ensure consistent state.
    • Frame rendering now captures viewport dimensions and device pixel ratio for enhanced visual accuracy and consistency.
  • Bug Fixes

    • Strengthened user input handling with owner validation, freshness checks, and explicit acknowledgment responses.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 30, 2026

📝 Walkthrough

Walkthrough

Extends browser session management with per-session control versioning and ownership tracking. Adds richer frame metadata (viewport dimensions, device pixel ratio) to session messages. Enhances Open with initialization parameters (InitialURL, BridgeMode, PreviewID). Implements optimistic concurrency control in Claim/Release/UserInput operations with version validation and acknowledgment protocol.

Changes

Cohort / File(s) Summary
Session State & Control Flow
internal/browser/manager.go
Adds controlVersion and lastFrameSeq to session state; implements owner validation and optimistic concurrency control using msg.ControlVersion in Claim/Release; gates UserInput on ownership, control-version freshness, and frame-sequence freshness; sends BrowserUserInputAck protocol messages on both acceptance and rejection.
Manager Tests & Fakes
internal/browser/manager_test.go
Protects fake state with mutexes; adds snapshot helpers for fakeService and recordingSender; expands fake Frame response with viewport and device pixel ratio; introduces tests for zero-value parameter preservation, invalid owner rejection, and stale control-version rejection.
Frame & Service Metadata
internal/browser/service.go
Decodes additional cloud_frame fields (viewportWidth, viewportHeight, devicePixelRatio); adds new helper to conditionally construct augmented cloud_user_input parameters based on protocol.BrowserUserInput metadata presence.
Type Definitions
internal/browser/types.go
Extends OpenRequest with InitialURL, BridgeMode, PreviewID; extends Frame struct with ViewportWidth, ViewportHeight, DevicePixelRatio.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Browser Client
    participant Manager as Session Manager
    participant Transport as Protocol Transport
    participant Service as Backend Service

    Client->>Manager: UserInput (controlVersion, frameSeq, ...)
    
    alt Control Version is Stale
        Manager->>Transport: Send BrowserUserInputAck<br/>(ControlVersion, Reason: Stale)
        Transport-->>Client: UserInputAck (rejected)
    else Control Version is Fresh
        Manager->>Service: Invoke UserInput RPC<br/>(augmented params)
        Service-->>Manager: Response
        Manager->>Transport: Send BrowserUserInputAck<br/>(ControlVersion, Timestamp)
        Transport-->>Client: UserInputAck (accepted)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 Hopping through versions and claims so new,
Frame metadata blooms in viewport hue,
Control flows steady with ownership tight,
Stale versions rejected, fresh inputs take flight! 🎪

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Enforce browser control ownership' clearly and concisely summarizes the main change in the PR—implementing ownership enforcement for browser control with version-based concurrency and stale input rejection, which is the central feature across all modified files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/unified-web-surface-daemon

Review rate limit: 2/10 reviews remaining, refill in 42 minutes and 34 seconds.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/browser/manager_test.go`:
- Around line 283-336: The test reads sent.msgs and svc.calls while the
manager's frameLoop may append to them, causing races; protect access by adding
a mutex to recordingSender and fakeService (guarding recordingSender.msgs and
fakeService.calls) and update any helpers (e.g., hasCall) and the test's
inspection code to use locked accessor methods (e.g.,
recordingSender.Messages(), fakeService.HasCall(name) or similar) so the slices
are read under the mutex; alternatively stop the session/frame loop before
iterating by calling the manager shutdown method if one exists, but the minimal
change is to serialize access to sent.msgs and svc.calls via mutex-protected
getters used in TestUserInputRejectsStaleControlVersion.

In `@internal/browser/manager.go`:
- Around line 78-80: The OpenRequest fields InitialURL, BridgeMode, and
PreviewID are populated in manager.go but not propagated into the LocalService
implementation; update the LocalService methods that create/start sessions (the
Open/OpenSession/Start methods in the LocalService implementation in
internal/browser/service.go) to accept and forward these fields from OpenRequest
into the browser startup/open call so the new protocol fields are used when
launching sessions; locate the OpenRequest type and the LocalService method
implementations and ensure InitialURL, BridgeMode, and PreviewID are read from
the OpenRequest and passed into the underlying browser start/open logic.
- Around line 253-265: The Claim and Release flows mutate state.owner and
state.controlVersion before calling m.sender.Send, so if Send fails the local
state remains changed; fix by capturing the previous owner and controlVersion,
perform the state mutation, then attempt m.sender.Send, and if Send returns an
error revert state.owner and state.controlVersion back to the captured values
while holding m.mu (apply same pattern in both Claim and Release around
m.sender.Send), ensuring the revert occurs only on send failure so the local
state matches what the peer actually observed.
- Around line 175-194: The code advances current.lastFrameSeq regardless of
whether m.sender.Send succeeded; change it to check the error returned by
m.sender.Send(ctx, &protocol.BrowserFrame{...}) and only update
m.byID[browserID].lastFrameSeq when send returns nil. Specifically, capture the
error from m.sender.Send, handle/log it as appropriate, and move the
m.mu.Lock()/unlock and the conditional that updates current.lastFrameSeq (using
current == state && frame.Sequence > current.lastFrameSeq) so it runs only when
the send error is nil; keep references to m.sender.Send, protocol.BrowserFrame,
m.byID[browserID], lastFrameSeq, and frame.Sequence to locate the change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c35a2357-bb15-44b6-8590-ffc79b82c46f

📥 Commits

Reviewing files that changed from the base of the PR and between 156be4a and a483ab4.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (5)
  • go.mod
  • internal/browser/manager.go
  • internal/browser/manager_test.go
  • internal/browser/service.go
  • internal/browser/types.go

Comment thread internal/browser/manager_test.go
Comment on lines +78 to +80
InitialURL: msg.InitialURL,
BridgeMode: msg.BridgeMode,
PreviewID: msg.PreviewID,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Wire these new open-session fields through LocalService.

InitialURL, BridgeMode, and PreviewID are copied into OpenRequest here, but the only shown service implementation in internal/browser/service.go:41-70 never uses them when starting/opening the browser. As written, sessions opened through this manager still ignore the new protocol fields.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/browser/manager.go` around lines 78 - 80, The OpenRequest fields
InitialURL, BridgeMode, and PreviewID are populated in manager.go but not
propagated into the LocalService implementation; update the LocalService methods
that create/start sessions (the Open/OpenSession/Start methods in the
LocalService implementation in internal/browser/service.go) to accept and
forward these fields from OpenRequest into the browser startup/open call so the
new protocol fields are used when launching sessions; locate the OpenRequest
type and the LocalService method implementations and ensure InitialURL,
BridgeMode, and PreviewID are read from the OpenRequest and passed into the
underlying browser start/open logic.

Comment on lines 175 to +194
_ = m.sender.Send(ctx, &protocol.BrowserFrame{
Type: protocol.MsgTypeBrowserFrame,
BrowserID: browserID,
SessionID: req.SessionID,
ChannelID: req.ChannelID,
Seq: frame.Sequence,
ContentType: frame.ContentType,
DataBase64: frame.DataBase64,
Width: frame.Width,
Height: frame.Height,
CapturedAt: frame.CapturedAt,
Type: protocol.MsgTypeBrowserFrame,
BrowserID: browserID,
SessionID: req.SessionID,
ChannelID: req.ChannelID,
Seq: frame.Sequence,
ContentType: frame.ContentType,
DataBase64: frame.DataBase64,
Width: frame.Width,
Height: frame.Height,
ViewportWidth: frame.ViewportWidth,
ViewportHeight: frame.ViewportHeight,
DevicePixelRatio: frame.DevicePixelRatio,
CapturedAt: frame.CapturedAt,
})
m.mu.Lock()
if current := m.byID[browserID]; current == state && frame.Sequence > current.lastFrameSeq {
current.lastFrameSeq = frame.Sequence
}
m.mu.Unlock()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only advance lastFrameSeq after a successful frame send.

lastFrameSeq is updated even when m.sender.Send drops the BrowserFrame, because the send error is ignored. That can make later input look stale relative to a frame the client never received.

Suggested guard
- _ = m.sender.Send(ctx, &protocol.BrowserFrame{
+ if err := m.sender.Send(ctx, &protocol.BrowserFrame{
      Type:             protocol.MsgTypeBrowserFrame,
      BrowserID:        browserID,
      SessionID:        req.SessionID,
      ChannelID:        req.ChannelID,
      Seq:              frame.Sequence,
      ContentType:      frame.ContentType,
      DataBase64:       frame.DataBase64,
      Width:            frame.Width,
      Height:           frame.Height,
      ViewportWidth:    frame.ViewportWidth,
      ViewportHeight:   frame.ViewportHeight,
      DevicePixelRatio: frame.DevicePixelRatio,
      CapturedAt:       frame.CapturedAt,
- })
- m.mu.Lock()
- if current := m.byID[browserID]; current == state && frame.Sequence > current.lastFrameSeq {
-     current.lastFrameSeq = frame.Sequence
+ }); err == nil {
+     m.mu.Lock()
+     if current := m.byID[browserID]; current == state && frame.Sequence > current.lastFrameSeq {
+         current.lastFrameSeq = frame.Sequence
+     }
+     m.mu.Unlock()
  }
- m.mu.Unlock()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/browser/manager.go` around lines 175 - 194, The code advances
current.lastFrameSeq regardless of whether m.sender.Send succeeded; change it to
check the error returned by m.sender.Send(ctx, &protocol.BrowserFrame{...}) and
only update m.byID[browserID].lastFrameSeq when send returns nil. Specifically,
capture the error from m.sender.Send, handle/log it as appropriate, and move the
m.mu.Lock()/unlock and the conditional that updates current.lastFrameSeq (using
current == state && frame.Sequence > current.lastFrameSeq) so it runs only when
the send error is nil; keep references to m.sender.Send, protocol.BrowserFrame,
m.byID[browserID], lastFrameSeq, and frame.Sequence to locate the change.

Comment on lines +253 to +265
state.owner = owner
state.controlVersion++
out := &protocol.BrowserControlClaim{
Type: protocol.MsgTypeBrowserControlClaim,
BrowserID: msg.BrowserID,
SessionID: state.openRequest.SessionID,
ChannelID: state.openRequest.ChannelID,
Owner: string(owner),
Reason: msg.Reason,
ControlVersion: state.controlVersion,
}
m.mu.Unlock()
return m.sender.Send(ctx, out)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Rollback local control state if the outbound event send fails.

Both Claim and Release update state.owner / state.controlVersion before calling m.sender.Send. If that send fails, the method returns an error but keeps the mutated state, so later ownership and stale-version checks run against a version the peer never saw.

Suggested rollback pattern
+ prevOwner := state.owner
+ prevVersion := state.controlVersion
  state.owner = owner
  state.controlVersion++
  out := &protocol.BrowserControlClaim{
      Type:           protocol.MsgTypeBrowserControlClaim,
      BrowserID:      msg.BrowserID,
      SessionID:      state.openRequest.SessionID,
      ChannelID:      state.openRequest.ChannelID,
      Owner:          string(owner),
      Reason:         msg.Reason,
      ControlVersion: state.controlVersion,
  }
  m.mu.Unlock()
- return m.sender.Send(ctx, out)
+ if err := m.sender.Send(ctx, out); err != nil {
+     m.mu.Lock()
+     if current := m.byID[msg.BrowserID]; current == state {
+         current.owner = prevOwner
+         current.controlVersion = prevVersion
+     }
+     m.mu.Unlock()
+     return err
+ }
+ return nil

Apply the same rollback pattern in Release.

Also applies to: 287-297

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/browser/manager.go` around lines 253 - 265, The Claim and Release
flows mutate state.owner and state.controlVersion before calling m.sender.Send,
so if Send fails the local state remains changed; fix by capturing the previous
owner and controlVersion, perform the state mutation, then attempt
m.sender.Send, and if Send returns an error revert state.owner and
state.controlVersion back to the captured values while holding m.mu (apply same
pattern in both Claim and Release around m.sender.Send), ensuring the revert
occurs only on send failure so the local state matches what the peer actually
observed.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
internal/browser/manager_test.go (1)

371-380: ⚡ Quick win

Consider asserting ControlVersion in the stale-input ack as well.

You already verify reject reason/accepted state; adding one assertion here would cover the newly-forwarded ack metadata too.

Suggested test tightening
 	for _, msg := range sentMessages {
 		ack, ok := msg.(*protocol.BrowserUserInputAck)
 		if ok && ack.InputID == "input-1" {
 			foundAck = true
+			if ack.ControlVersion == 0 {
+				t.Fatalf("ack controlVersion not populated: %+v", ack)
+			}
 			if ack.Accepted || ack.Reason != protocol.BrowserInputRejectStaleFrame {
 				t.Fatalf("ack = %+v", ack)
 			}
 		}
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/browser/manager_test.go` around lines 371 - 380, The test currently
checks that a BrowserUserInputAck for InputID "input-1" is present and that
ack.Accepted is false and ack.Reason equals
protocol.BrowserInputRejectStaleFrame; add an assertion that ack.ControlVersion
equals the expected forwarded control version (e.g., the version value used when
sending the stale input) to validate the forwarded ack metadata. Locate the loop
that examines sent.snapshot() and the ack variable of type
*protocol.BrowserUserInputAck and add a check against the expected
ControlVersion for that stale-input ack.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/browser/manager_test.go`:
- Around line 371-380: The test currently checks that a BrowserUserInputAck for
InputID "input-1" is present and that ack.Accepted is false and ack.Reason
equals protocol.BrowserInputRejectStaleFrame; add an assertion that
ack.ControlVersion equals the expected forwarded control version (e.g., the
version value used when sending the stale input) to validate the forwarded ack
metadata. Locate the loop that examines sent.snapshot() and the ack variable of
type *protocol.BrowserUserInputAck and add a check against the expected
ControlVersion for that stale-input ack.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 6041f93a-7e0c-45e6-93d8-e02059c9db35

📥 Commits

Reviewing files that changed from the base of the PR and between 14a4f0c and ce36791.

📒 Files selected for processing (1)
  • internal/browser/manager_test.go

@glittercowboy glittercowboy force-pushed the codex/unified-web-surface-daemon branch from ce36791 to 6129557 Compare April 30, 2026 17:36
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
internal/browser/manager.go (2)

253-265: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Rollback control state if the outbound claim/release event fails.

Both paths mutate state.owner / state.controlVersion before the send. If m.sender.Send fails, local ownership/version diverges from what the peer observed, and later stale-version checks can reject otherwise valid requests.

Suggested pattern
+	prevOwner := state.owner
+	prevVersion := state.controlVersion
 	state.owner = owner
 	state.controlVersion++
 	out := &protocol.BrowserControlClaim{
 		Type:           protocol.MsgTypeBrowserControlClaim,
 		BrowserID:      msg.BrowserID,
 		SessionID:      state.openRequest.SessionID,
 		ChannelID:      state.openRequest.ChannelID,
 		Owner:          string(owner),
 		Reason:         msg.Reason,
 		ControlVersion: state.controlVersion,
 	}
 	m.mu.Unlock()
-	return m.sender.Send(ctx, out)
+	if err := m.sender.Send(ctx, out); err != nil {
+		m.mu.Lock()
+		if current := m.byID[msg.BrowserID]; current == state {
+			current.owner = prevOwner
+			current.controlVersion = prevVersion
+		}
+		m.mu.Unlock()
+		return err
+	}
+	return nil

Apply the same rollback pattern in Release when that method changes the owner/version.

Also applies to: 283-297

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/browser/manager.go` around lines 253 - 265, The code updates
state.owner and state.controlVersion before calling m.sender.Send (creating a
protocol.BrowserControlClaim) which can fail; capture the previous owner and
version (e.g., oldOwner, oldVersion) before mutating, call m.sender.Send, and if
it returns an error revert state.owner and state.controlVersion back under m.mu
to the old values so local state stays consistent with the peer; apply the same
rollback pattern to the Release path that mutates
state.owner/state.controlVersion (referencing the Release method and the same
m.sender.Send usage).

175-194: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Only advance lastFrameSeq after a successful frame send.

Lines 175-194 still move lastFrameSeq forward even when m.sender.Send drops the BrowserFrame. That makes later input look stale relative to a frame the client never received.

Suggested fix
-	_ = m.sender.Send(ctx, &protocol.BrowserFrame{
+	if err := m.sender.Send(ctx, &protocol.BrowserFrame{
 		Type:             protocol.MsgTypeBrowserFrame,
 		BrowserID:        browserID,
 		SessionID:        req.SessionID,
 		ChannelID:        req.ChannelID,
 		Seq:              frame.Sequence,
 		ContentType:      frame.ContentType,
 		DataBase64:       frame.DataBase64,
 		Width:            frame.Width,
 		Height:           frame.Height,
 		ViewportWidth:    frame.ViewportWidth,
 		ViewportHeight:   frame.ViewportHeight,
 		DevicePixelRatio: frame.DevicePixelRatio,
 		CapturedAt:       frame.CapturedAt,
-	})
-	m.mu.Lock()
-	if current := m.byID[browserID]; current == state && frame.Sequence > current.lastFrameSeq {
-		current.lastFrameSeq = frame.Sequence
+	}); err == nil {
+		m.mu.Lock()
+		if current := m.byID[browserID]; current == state && frame.Sequence > current.lastFrameSeq {
+			current.lastFrameSeq = frame.Sequence
+		}
+		m.mu.Unlock()
 	}
-	m.mu.Unlock()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/browser/manager.go` around lines 175 - 194, The code advances
current.lastFrameSeq regardless of whether m.sender.Send succeeded; change
m.sender.Send(ctx, &protocol.BrowserFrame{...}) to capture its returned error
and only update lastFrameSeq when send returns nil—i.e., call err :=
m.sender.Send(...), then if err == nil acquire m.mu.Lock(), lookup current :=
m.byID[browserID], confirm current == state and frame.Sequence >
current.lastFrameSeq, set current.lastFrameSeq = frame.Sequence, and finally
m.mu.Unlock(); do not advance lastFrameSeq on send failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/browser/manager.go`:
- Around line 314-320: The code reads state.owner after releasing m.mu, risking
a data race; capture the owner under lock and use that captured value in the
error/reporting calls. Specifically, inside the block that references
parseControlOwner, OwnerLex, state.openRequest and state.controlVersion, add a
local variable (e.g., capturedOwner := state.owner) before calling
m.mu.Unlock(), then use capturedOwner in the fmt.Errorf message (and any related
logging or error reporting such as the value sent to
m.sendInputAck/BrowserInputRejectOwnerMismatch) instead of reading state.owner
after unlocking.

---

Duplicate comments:
In `@internal/browser/manager.go`:
- Around line 253-265: The code updates state.owner and state.controlVersion
before calling m.sender.Send (creating a protocol.BrowserControlClaim) which can
fail; capture the previous owner and version (e.g., oldOwner, oldVersion) before
mutating, call m.sender.Send, and if it returns an error revert state.owner and
state.controlVersion back under m.mu to the old values so local state stays
consistent with the peer; apply the same rollback pattern to the Release path
that mutates state.owner/state.controlVersion (referencing the Release method
and the same m.sender.Send usage).
- Around line 175-194: The code advances current.lastFrameSeq regardless of
whether m.sender.Send succeeded; change m.sender.Send(ctx,
&protocol.BrowserFrame{...}) to capture its returned error and only update
lastFrameSeq when send returns nil—i.e., call err := m.sender.Send(...), then if
err == nil acquire m.mu.Lock(), lookup current := m.byID[browserID], confirm
current == state and frame.Sequence > current.lastFrameSeq, set
current.lastFrameSeq = frame.Sequence, and finally m.mu.Unlock(); do not advance
lastFrameSeq on send failure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 87a19d3f-5f0f-469e-a8fb-ace0bb0a517f

📥 Commits

Reviewing files that changed from the base of the PR and between ce36791 and 6129557.

📒 Files selected for processing (4)
  • internal/browser/manager.go
  • internal/browser/manager_test.go
  • internal/browser/service.go
  • internal/browser/types.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/browser/types.go
  • internal/browser/service.go
  • internal/browser/manager_test.go

Comment on lines +314 to 320
inputOwner, ownerOK := parseControlOwner(msg.Owner)
if !ownerOK || inputOwner != OwnerLex || state.owner != OwnerLex {
req := state.openRequest
version := state.controlVersion
m.mu.Unlock()
_ = m.sendInputAck(ctx, req, msg, false, protocol.BrowserInputRejectOwnerMismatch, version)
return fmt.Errorf("browser control belongs to %s", state.owner)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Show the owner-mismatch branch and confirm `state.owner` is read after unlocking.
nl -ba internal/browser/manager.go | sed -n '314,320p'

Repository: gsd-build/daemon

Length of output: 101


🏁 Script executed:

sed -n '314,330p' internal/browser/manager.go | cat -n

Repository: gsd-build/daemon

Length of output: 957


🏁 Script executed:

sed -n '300,330p' internal/browser/manager.go | cat -n

Repository: gsd-build/daemon

Length of output: 1529


🏁 Script executed:

# Find the state struct definition to confirm state.owner is a field
rg -A 20 "type.*state\s*struct" internal/browser/manager.go

Repository: gsd-build/daemon

Length of output: 42


🏁 Script executed:

# Search for Claim/Release functions that might modify owner
rg "func.*Claim|func.*Release" internal/browser/manager.go

Repository: gsd-build/daemon

Length of output: 240


🏁 Script executed:

# Look for other similar patterns where state fields are accessed after unlock
rg -B 5 -A 5 "m.mu.Unlock()" internal/browser/manager.go | head -80

Repository: gsd-build/daemon

Length of output: 2118


🏁 Script executed:

# Search for "type" and "state" together more broadly
rg "type\s+\w+\s*struct" internal/browser/manager.go

Repository: gsd-build/daemon

Length of output: 158


🏁 Script executed:

# Look at the Claim function implementation to see if it modifies owner
rg -A 30 "func (m \*Manager) Claim" internal/browser/manager.go

Repository: gsd-build/daemon

Length of output: 42


🏁 Script executed:

# Look at the Release function implementation
rg -A 30 "func (m \*Manager) Release" internal/browser/manager.go

Repository: gsd-build/daemon

Length of output: 42


🏁 Script executed:

# Find the sessionState struct definition with its fields
rg -A 20 "type sessionState struct" internal/browser/manager.go

Repository: gsd-build/daemon

Length of output: 568


🏁 Script executed:

# Search for where owner is assigned or modified
rg "\.owner\s*=" internal/browser/manager.go

Repository: gsd-build/daemon

Length of output: 108


🏁 Script executed:

# Look for Claim and Release more broadly
rg "Claim|Release" internal/browser/manager.go | head -20

Repository: gsd-build/daemon

Length of output: 432


🏁 Script executed:

# Show context for owner assignments
rg -B 10 "state\.owner = " internal/browser/manager.go

Repository: gsd-build/daemon

Length of output: 675


Capture state.owner before unlocking to prevent data race.

Line 21 reads state.owner after m.mu.Unlock() on line 19. Since Claim and Release operations modify this field under lock protection, the error message can report a stale or incorrect owner value due to concurrent modification.

Suggested fix
 	inputOwner, ownerOK := parseControlOwner(msg.Owner)
 	if !ownerOK || inputOwner != OwnerLex || state.owner != OwnerLex {
 		req := state.openRequest
 		version := state.controlVersion
+		currentOwner := state.owner
 		m.mu.Unlock()
 		_ = m.sendInputAck(ctx, req, msg, false, protocol.BrowserInputRejectOwnerMismatch, version)
-		return fmt.Errorf("browser control belongs to %s", state.owner)
+		return fmt.Errorf("browser control belongs to %s", currentOwner)
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
inputOwner, ownerOK := parseControlOwner(msg.Owner)
if !ownerOK || inputOwner != OwnerLex || state.owner != OwnerLex {
req := state.openRequest
version := state.controlVersion
m.mu.Unlock()
_ = m.sendInputAck(ctx, req, msg, false, protocol.BrowserInputRejectOwnerMismatch, version)
return fmt.Errorf("browser control belongs to %s", state.owner)
inputOwner, ownerOK := parseControlOwner(msg.Owner)
if !ownerOK || inputOwner != OwnerLex || state.owner != OwnerLex {
req := state.openRequest
version := state.controlVersion
currentOwner := state.owner
m.mu.Unlock()
_ = m.sendInputAck(ctx, req, msg, false, protocol.BrowserInputRejectOwnerMismatch, version)
return fmt.Errorf("browser control belongs to %s", currentOwner)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/browser/manager.go` around lines 314 - 320, The code reads
state.owner after releasing m.mu, risking a data race; capture the owner under
lock and use that captured value in the error/reporting calls. Specifically,
inside the block that references parseControlOwner, OwnerLex, state.openRequest
and state.controlVersion, add a local variable (e.g., capturedOwner :=
state.owner) before calling m.mu.Unlock(), then use capturedOwner in the
fmt.Errorf message (and any related logging or error reporting such as the value
sent to m.sendInputAck/BrowserInputRejectOwnerMismatch) instead of reading
state.owner after unlocking.

@glittercowboy glittercowboy merged commit 2e76e7f into main Apr 30, 2026
2 checks passed
@glittercowboy glittercowboy deleted the codex/unified-web-surface-daemon branch April 30, 2026 18:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant