Skip to content

feat(agent): Linux remote desktop (X11 mirror, pure-Go) + Phase 0 bugfixes#2602

Merged
ToddHebebrand merged 24 commits into
mainfrom
ToddHebebrand/ubuntu-breeze-remote
Jul 17, 2026
Merged

feat(agent): Linux remote desktop (X11 mirror, pure-Go) + Phase 0 bugfixes#2602
ToddHebebrand merged 24 commits into
mainfrom
ToddHebebrand/ubuntu-breeze-remote

Conversation

@ToddHebebrand

Copy link
Copy Markdown
Collaborator

Summary

Makes Linux remote desktop actually work. It has never functioned in any released build — release agents are CGO_ENABLED=0, but the only Linux capturer was behind //go:build linux && cgo, so every shipped agent contained the ErrNotSupported stub (confirmed via ldd on a deployed v0.96.0 agent — no libX11 linkage). This branch delivers a working Linux X11 mirror plus three independent correctness bugfixes.

Full design + implementation plan: docs/superpowers/specs/2026-07-16-linux-remote-desktop-design.md, docs/superpowers/plans/2026-07-16-linux-remote-desktop-phase0-1.md.

Phase 0 — correctness bugfixes (all platforms)

  • Stop the Breeze Assist migrate/uninstall thrash loop. With Assist disabled + not installed, every heartbeat re-ran migrateToSessions() (recreating sessions/, pkill-ing helpers, rewriting autostart) then uninstallLocked() deleted it again — an endless per-tick loop fleet-wide, on every platform. Gated the migration on settings.Enabled || wasInstalled (snapshot taken before migrateFromLegacyName can delete the binary).
  • Don't run the macOS launchctl/plist helper-spawn path on Linux (spawnHelperForDesktop gated on != "windows" → now == "darwin", with a typed ErrLinuxDesktopHelperUnsupported fast-fail).
  • Interim honest error for Linux start_desktop (superseded by real capture in Phase 1).

Phase 1 — Linux X11 mirror (pure-Go, no CGO)

Speaks the X11 wire protocol via github.com/jezek/xgb v1.3.1 — no C libraries, so it ships in the CGO_ENABLED=0 release build. (Chosen over purego/Xlib because Xlib calls exit(1) when the X connection dies — exactly the xrdp-logout case this targets — which is inexpressible to escape from Go; xgb turns it into a plain read error.)

  • New internal/remote/desktop/x11 package: Xauthority cookie parser, display/session resolver (finds the live X display via /proc, resolves its owner + Xauthority), per-instance MIT-SHM capture, XTEST input (xdotool removed), XFixes cursor, RandR monitors. All X state is per-capturer-instance (the old C-global g_ctx is gone), which is what makes concurrent capturers safe.
  • SHM segment is created 0600 and chowned to the X-server owner UID — the agent runs as root but Xorg runs as the session user, so a naive root-owned 0600 would fail to attach; this keeps the framebuffer off other local users while still letting the non-root X server attach.
  • Heartbeat routes Linux start_desktop/screenshot/computer-action to the direct capturer path; stop routing is state-based; OnSessionStopped registered on Linux; static-screen watchdog fixed so an idle desktop isn't killed.
  • Honest desktopAccess capability report (real connect probe, not just resolve): no_display_session / wayland_unsupported / x11_connect_failed / x11_auth_failed, surfaced as localized "Connect Desktop" tooltips in all five locales.

Verification

Built with per-task spec + quality review (each task reviewed, the capturer's close-race and the SHM world-readable finding both caught and fixed in review). Builds green on linux/darwin/windows; all unit suites pass.

Rig-verified on real hardware (Ubuntu 22.04, non-root X server + root agent, the faithful xrdp config):

  • Resolver, cookie auth, SHM capture, full capturer, XTEST input, RandR monitors — all pass against a live X server.
  • X-server-death survival: killed the X server mid-capture → process exited 0 (no SIGSEGV) and the agent stayed alive (the exact scenario Xlib would crash on).
  • SHM security confirmed live (ipcs): capture segment = owner=<sessionuser> perms=600 8.3MB nattch=2 — zero-copy, not world-readable, chowned to the X-server owner.
  • Full WebRTC Connect Desktop end-to-end confirmed working through the viewer against production.

Not in this PR (follow-ups)

  • Clipboard copy/paste — the clipboard_linux.go code exists (text/RTF/images) but needs display-context plumbing; coming as a fast follow-up (text-first, pure-Go xgb selections).
  • Wayland/PipeWire, the Linux desktop-helper binary, and the legacyBinaryPath()==defaultBinaryPath() Linux guard — Phase 2 (separate plan).
  • Minor: DeviceInfoTab desktopAccess Section is macOS-gated (button tooltip is the live Linux surface); per-AI-call input-handler connection (finalizer-mitigated).

Rollout note

Linux desktop mirrors an existing X session — it can't create one. On a headless/xrdp box a user must log in (RDP/console) first so a session exists; with no session the Connect Desktop button is greyed with a "no active graphical session" tooltip.

🤖 Generated with Claude Code

Todd Hebebrand and others added 20 commits July 16, 2026 23:42
Field investigation of an Ubuntu 22.04 xrdp VM + full code map showed Linux
remote desktop has never worked in released builds (CGO_ENABLED=0 strips the
X11 capturer) plus six compounding defects. Spec covers Phase 0 correctness
fixes, Phase 1 X11 mirror via purego runtime loading, Phase 2 Linux
desktop-helper + Wayland portal/PipeWire, Phase 3 deferred session creation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Multi-agent research + adversarial verification corrected five load-bearing
assumptions in the draft:
- X11 mechanism: jezek/xgb pure-Go wire protocol, NOT purego/Xlib (Xlib exit()s
  the whole agent when a mirrored X session dies — the xrdp case this targets)
- per-capturer-instance X state (delete C-global g_ctx) — the single-session
  invariant holds only inside SessionManager; WS/screenshot/monitor-switch/cursor
  all run concurrently
- desktopAccess mode must be 'user_session' not 'available' (zod mode enum has no
  .catch — 'available' silently drops the whole object on deployed servers)
- dynamic isHeadless needs state-based stop routing + unconditional OnSessionStopped
  + atomic (plain-bool per-tick write races pool workers) + static-screen watchdog
- Phase 0 thrash fix: snapshot wasInstalled before migrateFromLegacyName; flag the
  Linux legacyBinaryPath==defaultBinaryPath collision for Phase 2

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
TDD task breakdown (15 tasks) grounded in multi-agent research + adversarial
verification of the spec. Phase 0 = 3 independent bugfix PRs (Assist thrash,
darwin-spawn-on-Linux, honest error). Phase 1 = X11 mirror via jezek/xgb pure-Go
wire protocol: Xauthority cookie parser, display/session resolver, per-instance
SHM capture, XTEST input, XFixes cursor, RandR monitors, dynamic-headless routing
with state-based stop + static-screen watchdog fix, desktopAccess reporting, API
+ shared reason enum, 5-locale web reasons, and rig verification.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… heartbeat

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…interim)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
initShm() created the MIT-SHM segment with IPC_CREAT|0o777, making the
captured framebuffer world-readable/writable to any local user. Create
the segment as 0600 instead, and chown it to the session owner's uid
(DisplayTarget.OwnerUID) so the non-root X server on xrdp/GDM can still
attach while other local users cannot. Best-effort: if the chown fails
or the owner uid is unknown, shm.AttachChecked fails cleanly and
initShm already falls back to core-protocol capture.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…[correctness]

Capture() and CaptureRegion() released c.mu before calling
conn.CaptureBGRX()/CaptureRegionBGRX() and copying the pixel buffer,
so an in-flight capture was not mutually excluded from Close() (which
unmaps the SHM segment and nils the connection under the same lock).
On the WS stop path, Stop() closes the capturer without joining the
~30fps capture-loop goroutine, so this could SIGSEGV (read of unmapped
SHM) or nil-pointer panic. Hold c.mu for the full body of both methods,
matching the pattern already used by GetScreenBounds()/Close().

Also harden mapResolveErr's Wayland-unsupported check with errors.Is
instead of == for robustness against future error wrapping.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, disconnect notify, static-screen watchdog

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…M cleanup

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jul 17, 2026

Copy link
Copy Markdown

Deploying breeze with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7241a40
Status: ✅  Deploy successful!
Preview URL: https://1bb81eb8.breeze-9te.pages.dev
Branch Preview URL: https://toddhebebrand-ubuntu-breeze.breeze-9te.pages.dev

View logs

Comment thread agent/internal/remote/desktop/x11/conn_linux.go Fixed
Todd Hebebrand and others added 4 commits July 17, 2026 16:58
… helper

Fixes the Lint Agent (Go) govet finding on the Task 11 test helper.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
golangci-lint runs on Linux CI and lints the //go:build linux files that a
darwin dev machine skips; add _ = to the cleanup Close()/error returns flagged
by errcheck across the x11 package + monitor/capture linux files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eQL)

The SHM segment owner UID traces from strconv.Atoi (loginctl parse) to a
uint32 conversion; add an explicit <= math.MaxUint32 guard so an out-of-range
value skips the chown (falling back to core-protocol capture) instead of
truncating. Resolves the CodeQL go/incorrect-integer-conversion high alert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ToddHebebrand
ToddHebebrand merged commit 1db89d6 into main Jul 17, 2026
50 checks passed
@ToddHebebrand
ToddHebebrand deleted the ToddHebebrand/ubuntu-breeze-remote branch July 17, 2026 23:48
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.

2 participants