8000 feat(remote): live remote-browser web UI at /remote/ by adeelahmad · Pull Request #11 · adeelahmad/Tether · GitHub
[go: up one dir, main page]

Skip to content

feat(remote): live remote-browser web UI at /remote/ - #11

Merged
adeelahmad merged 20 commits into
mainfrom
claude/remote-ui-295adi
Oct 11, 2026
Merged

adeelahmad merged 20 commits into
mainfrom
claude/remote-ui-295adi

Conversation

@adeelahmad
Copy link
Copy Markdown
Owner

Requested by Adeel · project thread

What

Before: /remote answered 501 not_implemented.

After: opening http://127.0.0.1:8787/remote/ (or just the server address in a browser) shows a web app embedded in the binary. You can pick any Chrome tab, watch it live, and drive it with mouse, wheel, touch, keyboard (including IME and mobile keyboards) and the clipboard. It also handles alert/confirm/prompt dialogs, has an address bar with back/forward/reload, lets you open and close tabs, has a watch-only mode, and works on phones in light and dark themes. You can take over the exact tab an agent or Selenium script is driving.

How: frames come from CDP Page.startScreencast and go out as binary websocket messages. Quiet or background tabs get topped up with Page.captureScreenshot keyframes, at 4/s for a few seconds after any input. Each tab runs one screencast hub shared by every viewer, and each viewer keeps only the newest frame. Input goes through a new optional cdp.Screen capability (Input.dispatchMouseEvent, insertText, dialogs, bringToFront) and skips the per-tab lock, so a person can always step in. Address-bar navigation goes through Core.Navigate, so --allow-domains applies. New flags: --no-remote and --remote-quality. The protocol, the reasoning behind using a websocket rather than WebRTC, and the limitations are in docs/REMOTE.md.

Why

Adeel asked for every stubbed face to ship in the first release, not just the stubs.

How tested

  • make check equivalent: gofmt, go vet, golangci-lint (0 issues), go test -race ./.... internal/remote 88% and internal/server 89% coverage.
  • make test-integration against real headless Chromium. The new TestRemoteUI streams JPEG frames, clicks a form field at its on-screen position, types, pastes, submits with Enter, navigates and goes back, answers a prompt(), and closes the tab.
  • Driven manually in a second Chromium through Playwright on desktop and phone viewports. Canvas clicks map correctly, typing and Shift+Arrow work, and sign-in and dialogs work. This caught a login bug, now fixed and covered by a test: Referrer-Policy: no-referrer made the form's Origin null.
  • make cross and govulncheck pass.
  • Docs updated: README, docs/REMOTE.md (new), ARCHITECTURE, SECURITY, CONTRIBUTING, CHANGELOG.

Security

  • No new way to reach the browser without auth, and nothing new is logged that could hold a cookie or token.
    • With --token, /remote/login trades the token for an HttpOnly, SameSite=Strict cookie scoped to /remote. The cookie is an HMAC of the token and is accepted only under /remote/, never by the proxy, API or WebDriver. Login is checked with Sec-Fetch-Site/Origin, and failed attempts are slowed down.
    • The UI is served with CSP script-src 'self' and frame-ancestors 'none', plus X-Frame-Options: DENY. The websocket rejects foreign Origins and allows only X-Forwarded-Host for tailscale serve.
    • Without a token, the existing loopback Host/Origin guard still applies.

Does not touch internal/proxy. It adds small, additive changes to cdp.KeyEvent (Commands), the cdptest fake, server.Options (UIPrefix) and config.

🤖 Generated with Claude Code

https://claude.ai/code/session_01UzmmijqffKE848kSpCqq72


Generated by Claude Code

8000

Extend the single global --allow-domains list into layered policies:

- internal/policy: domain rules (allow/deny, *.sub, *), principals and a
  JSON --policy-file defining named tokens with their own rules, an admin
  flag and a scripts permission. --deny-domains adds a global denylist.
- Auth attaches the token's principal to the request context; --token is
  the admin "default" principal.
- Tab ownership: non-admin tokens only see their own tabs, get their own
  per-site default tabs, and cannot adopt existing tabs.
- core.RulesPolicy checks global ∩ requester ∩ owner ∩ session rules,
  re-checks the final URL, and guards restricted tabs in the browser via
  the DevTools Fetch domain so redirects, clicks and page script obey the
  same rules.
- API: session rules on tab create, GET/PUT /tabs/{id}/policy, /whoami.
  WebDriver: tether:options.policy, ownership on every command, and
  Execute Script gated by the scripts permission.
- Docs: docs/POLICIES.md, SECURITY.md, ARCHITECTURE.md, README, example
  policy file. Integration test drives a real Chrome as a restricted token.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UfsJJgC4Jp8VqckdcChH4W
Replace the /mcp stub with a real Model Context Protocol server on the
shared session core:

- Streamable HTTP transport (MCP 2025-06-18, 2025-03-26, 2024-11-05):
  Mcp-Session-Id sessions, protocol-version checks, batches, 202 for
  notifications, DELETE to end a session. Inherits token auth, the
  loopback guard and --allow-domains from the server.
- 16 browser tools: tabs/new/select/close, navigate, history, snapshot
  (interactive elements with stable backend-node refs plus page text),
  click, type, press_key, select_option, scroll, wait_for, eval,
  screenshot and in-tab fetch with the user's cookies. Tool annotations,
  isError results with stable codes, policy checked against the page's
  current URL, password values and Set-Cookie never returned, tool
  arguments never logged.
- Each MCP session tracks a current tab and the tabs it opened; ending
  the session (or idling past --idle-timeout) closes only those.
- `tether mcp`: stdio bridge for Claude Desktop and other stdio clients,
  local or across a tailnet, with ordered forwarding.
- --mcp flag (default on) to turn the face off.

Also fix screenshots of idle background tabs hanging forever (Chrome
does not composite hidden tabs): probe briefly, then bring the tab to
the front and retry, as ChromeDriver does. Affects WebDriver too.

Docs: docs/MCP.md, README, ARCHITECTURE, SECURITY threat model,
CHANGELOG. Tests: unit tests with the cdp fakes and a real headless
Chrome integration test driving every major tool.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HpbHKDrhpTtF3U8jAhD61N
Replace the /remote stub with a real interactive face, embedded in the
binary and built on the shared session core:

- Live video of any Chrome tab via CDP Page.startScreencast, with
  screenshot keyframes for quiet and background tabs (faster for a few
  seconds after input). One screencast per tab is shared by every viewer;
  each viewer keeps only the newest frame.
- Mouse, wheel, touch, keyboard (IME and mobile keyboards), paste, copy
  and JavaScript dialog relay over a small documented websocket protocol.
  Input bypasses the session lock so a person can always intervene.
- Address bar (through Core.Navigate and the domain policy),
  back/forward/reload, bring to front, open/close/adopt tabs, watch-only
  mode, phone layout, light and dark themes. Plain HTML/CSS/JS, no build.
- Token-protected servers get a sign-in page that trades the token for an
  HttpOnly, SameSite=Strict, path-scoped HMAC cookie accepted only under
  /remote/. Strict CSP, no framing, Origin-checked websocket.
- New cdp.Screen capability (screencast, mouse, insertText, dialogs,
  bringToFront), cdptest fakes, --no-remote and --remote-quality flags.
- Unit tests, a real-Chrome integration test, and docs/REMOTE.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzmmijqffKE848kSpCqq72
@adeelahmad adeelahmad self-assigned this Oct 11, 2026
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HpbHKDrhpTtF3U8jAhD61N
Comment thread internal/server/uilogin.go Fixed
claude added 16 commits October 11, 2026 03:39
…rint

Close the remaining WebDriver gaps on top of new cdp.Tab primitives:

- Switch To Frame / Parent Frame for same- and cross-site (out-of-process)
  frames at any depth, via cdp.Tab.InFrame and iframe auto-attach.
- Get Element Shadow Root and Find From Shadow Root, including closed roots;
  shadow roots round-trip through scripts.
- Alert commands and unhandledPromptBehavior; calls blocked by a dialog
  fail fast instead of hanging.
- Multiple windows: New Window, popups joined automatically, switching,
  closing; the session ends with its last window.
- Window rect, maximise, minimise and fullscreen, applied only to windows
  that hold nothing but Tether tabs.
- File inputs via Element Send Keys plus Selenium's /se/file upload.
- Print Page.
- Focus emulation on attached tabs so input reaches background tabs with
  cross-site iframes promptly.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AhaxD4mv92X8w5zV8j34MD
Apply the same HTTPS detection to the logout cookie as to the login one,
and test both. Plain-HTTP loopback/tailnet use keeps non-Secure cookies,
which browsers would otherwise refuse to store.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzmmijqffKE848kSpCqq72
POST/DELETE /session/{id}/actions dispatch key, mouse, pen, touch and
wheel input as real CDP events, tick by tick, with element/pointer/viewport
origins, animated moves and scrolls, click counting, US-layout Shift, and
input state that persists until Release Actions.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EnJnokVdV3oFBMpLc45zcJ
…t click

Chrome routes input into frames asynchronously, and on some versions a
real click into a cross-site frame of a background tab never arrives.
Arm a one-shot listener before clicking inside a frame, wait briefly for
it, and click from script if the real click did not land.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AhaxD4mv92X8w5zV8j34MD
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AhaxD4mv92X8w5zV8j34MD
Page.startScreencast fails with "Not attached to an active page" while a
navigation commits, which ended the stream for a viewer that connected to
a freshly opened tab (seen in CI). Retry the start on every keyframe tick
instead of giving up, and re-issue it after each main-frame navigation,
which can swap the page's renderer and silently end the stream.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UzmmijqffKE848kSpCqq72
Attaching to a tab that is closing can fail after Target.getTargetInfo
succeeds (Page.enable: session not found). Map that to ErrNoSuchTab so
faces answer 410/no such window instead of 500.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AhaxD4mv92X8w5zV8j34MD
Resolves conflicts with #7 (console logs): Tab gets both Guard and Logs,
the API keeps the policy routes and adds /tabs/{id}/logs, and docs list
both features. Also makes TestConsoleLogs tolerate a late favicon 404
between the two WebDriver log reads (it asserted the second read was
empty; it should assert no entry repeats).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X9CEjRcHsJmgWdtz6ZTKsk
Tab gains Mouse/Touch alongside Guard and Logs; the WebDriver session
keeps the log cursor and the Actions input state; docs list everything.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X9CEjRcHsJmgWdtz6ZTKsk
# Conflicts:
#	README.md
#	internal/cdp/cdptest/cdptest.go
Conflict resolution plus one fix the combination needs: popups and new
windows now belong to the session that opened them. A popup is attached
as the session's own tab (same owner, same session rules, guarded), so a
non-admin token can switch to its popups, and a restricted session can
no longer leave its allowlist through window.open or New Window. A popup
that loaded a forbidden page before Tether attached is closed and the
switch fails.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X9CEjRcHsJmgWdtz6ZTKsk
# Conflicts:
#	CHANGELOG.md
#	docs/WEBDRIVER.md
#	internal/cdp/cdptest/cdptest.go
#	internal/webdriver/errors.go
#	internal/webdriver/webdriver.go
#	test/integration/integration_test.go
Conflict resolution: the fake tab serves MCP's OnCall/Props and the
WebDriver wrappers; app wiring keeps per-token credentials and the
optional MCP face; docs describe MCP alongside per-token policies.
TestNavigateHonoursPolicy now simulates drift with SetURL, since the
browser-side guard (from policies) refuses the navigation it used.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X9CEjRcHsJmgWdtz6ZTKsk
# Conflicts:
#	README.md
#	internal/cdp/cdptest/cdptest.go
#	test/integration/integration_test.go
The UI login cookie is now per token and carries that token's principal,
so the remote UI is held to tab ownership and rules like every face.
Non-admin tokens list only their own tabs, and viewed tabs get the
browser-side navigation guard. Screen drops Mouse and HandleDialog in
favour of the Tab methods.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X9CEjRcHsJmgWdtz6ZTKsk
# Conflicts:
#	CHANGELOG.md
#	CONTRIBUTING.md
#	README.md
#	SECURITY.md
#	cmd/tether/main.go
#	docs/ARCHITECTURE.md
#	internal/app/app.go
#	internal/cdp/cdptest/cdptest.go
#	internal/config/config.go
#	internal/core/core.go
#	internal/server/server.go
#	internal/session/session.go
@adeelahmad
adeelahmad merged commit ebcab00 into main Oct 11, 2026
13 checks passed
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.

3 participants

0