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
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9fe35d7
feat: per-token and per-session domain policies
claude Oct 11, 2026
e6c3b99
feat(mcp): MCP server face for AI agents, plus a stdio bridge
claude Oct 11, 2026
2e56b6e
feat(remote): live remote-browser web UI at /remote/
claude Oct 11, 2026
4a55ca2
test: make the missing token-file assertion OS-neutral
claude Oct 11, 2026
dbe6b3d
feat(webdriver): frames, shadow roots, alerts, windows, uploads and p…
claude Oct 11, 2026
cdd2904
fix(server): mark UI cookies Secure whenever served over HTTPS
claude Oct 11, 2026
14e00a0
feat(webdriver): W3C Actions API (key, pointer, wheel)
claude Oct 11, 2026
cc13d1f
fix(webdriver): confirm clicks inside frames, falling back to a scrip…
claude Oct 11, 2026
cd5851d
fix(webdriver): make the upload zip-slip guard explicit
claude Oct 11, 2026
3472ef1
fix(cdp): keep the screencast alive across navigations
claude Oct 11, 2026
f5a9b84
fix(cdp): report a tab that closes mid-attach as gone
claude Oct 11, 2026
af8e4b2
Merge main into policies: combine with console log capture
claude Oct 11, 2026
701acb4
Merge policies + console logs into Actions API
claude Oct 11, 2026
147435b
Merge remote-tracking branch 'origin/main' into actions
claude Oct 11, 2026
ad49a54
Merge Actions, policies and console logs into WebDriver gaps
claude Oct 11, 2026
fb6bcae
Merge remote-tracking branch 'origin/main' into gaps
claude Oct 11, 2026
dea7e6a
Merge policies, console logs, Actions and WebDriver gaps into MCP
claude Oct 11, 2026
6088a29
Merge remote-tracking branch 'origin/main' into mcp
claude Oct 11, 2026
1e46f5f
Merge MCP and policies into remote UI
claude Oct 11, 2026
b94458d
Merge remote-tracking branch 'origin/main' into remote
claude Oct 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
faces; `--mcp=false` turns it off. See `docs/MCP.md`.
- `tether mcp`: a stdio bridge for MCP clients that don't speak HTTP
(Claude Desktop, IDEs), local or across a tailnet.
- Remote-browser UI face as a documented stub on the shared core.
- Screenshots of idle background tabs no longer hang: Tether brings the tab
to the front and retries, as ChromeDriver does (WebDriver and MCP).
- Remote-browser web UI at `/remote/`, embedded in the binary: live video of
any Chrome tab (CDP screencast, with screenshot keyframes for quiet and
background tabs), mouse, wheel, touch, keyboard (IME and mobile keyboards
included) and clipboard forwarding, JavaScript dialog relay, address bar,
back/forward/reload, open/close tabs, watch-only mode and a phone layout.
One screencast per tab is shared by all viewers. With `--token`, a sign-in
page trades the token for an `HttpOnly`, `SameSite=Strict`, UI-only cookie.
Each token signs in to its own UI session under its own rules and tabs.
New flags `--no-remote` and `--remote-quality`. See `docs/REMOTE.md`.
- Sprint Zero: Makefile, golangci-lint, CI (lint → test + coverage →
build matrix → govulncheck), CodeQL, Dependabot, GoReleaser (archives,
checksums, multi-arch Docker image, Homebrew cask), community docs.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Read [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) first. In short:
| `internal/proxy` | HTTP/HTTPS forward-proxy face |
| `internal/webdriver` | W3C WebDriver face |
| `internal/mcp` | MCP face and stdio bridge |
| `internal/remote` | Planned face (stub) |
| `internal/remote` | Remote-browser UI face; its web app lives in `internal/remote/assets` (plain HTML/CSS/JS, no build step) |
| `internal/server`, `internal/api` | Listener, auth, guard, control API |

Faces stay thin. If a change needs browser logic, it almost certainly
Expand Down
24 changes: 20 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ Chrome already speaks, and turns each tab into something any client can use:
- 🧠 **An MCP server for agents.** Claude, Cursor or your own agent get
tools to read, click, type, screenshot and fetch in your real browser
([docs/MCP.md](docs/MCP.md)).
- 🧩 **One core, four faces.** Proxy, WebDriver and MCP ship today on the
same tab sessions; a live remote-browser UI is designed in and stubbed
([architecture](docs/ARCHITECTURE.md)).
- 🖥️ **A live remote-browser UI.** Open `/remote/` from your phone or another
laptop, watch any tab in real time, and take over with mouse, keyboard and
clipboard — even the tab your agent is driving ([docs](docs/REMOTE.md)).
- 🧩 **One core, four faces.** Proxy, WebDriver, MCP and the remote UI all ship
today on the same tab sessions ([architecture](docs/ARCHITECTURE.md)).
- 🔒 **Safe by default.** Loopback-only unless you set a token. Built to sit
on a private [Tailscale](https://tailscale.com) tailnet, not the internet.

Expand Down Expand Up @@ -155,6 +157,16 @@ d.quit()

Covered commands and behaviour: [docs/WEBDRIVER.md](docs/WEBDRIVER.md).

### Remote browser UI

Open **http://127.0.0.1:8787/remote/** (or just the server address) in any
browser. Pick a tab, see it live, click, scroll, type, paste; answer its
`alert`/`confirm` dialogs; open, navigate and close tabs. Toggle *watch only*
to look over an agent's shoulder without disturbing it. With `--token`, the UI
asks for the token once and keeps a strict, UI-only cookie.
[docs/REMOTE.md](docs/REMOTE.md) covers the controls, Tailscale setup and the
websocket protocol.

### Control API

```sh
Expand All @@ -179,6 +191,7 @@ export TETHER_TOKEN="$(openssl rand -hex 32)"
tether serve --launch --addr "$(tailscale ip -4):8787"
# elsewhere on the tailnet:
curl -x "http://tether:$TETHER_TOKEN@my-laptop:8787" --cacert tether-ca.pem https://github.com/
# or watch and drive it from your phone: http://my-laptop:8787/remote/
```

Tether refuses to listen beyond loopback without a token. Read
Expand Down Expand Up @@ -219,6 +232,8 @@ Every flag has a `TETHER_*` environment variable; flags win.
| `--data-dir` | user config dir | Local CA and launched profile |
| `--no-mitm` | `false` | Refuse HTTPS `CONNECT` instead of intercepting |
| `--mcp` | `true` | Serve the MCP face at `/mcp` |
| `--no-remote` | `false` | Don't serve the remote-browser UI at `/remote/` |
| `--remote-quality` | `70` | JPEG quality of the remote UI video (1–100) |
| `--log-level`, `--log-format` | `info`, `text` | `json` for machines; secrets are always redacted |

Docker (headless Chromium inside the image):
Expand All @@ -241,6 +256,7 @@ local CA, and it only drives Chromium-family browsers.
| MCP for agents | ✅ | Often | Often | Via wrappers |
| Runs entirely on your machine | ✅ | ✅ | ❌ | ✅ |
| Remote access | Tailscale + token | Usually local only | ✅ | DIY |
| Watch & take over a tab live | ✅ built-in web UI | ❌ | ✅ | ❌ |
| Single static binary | ✅ | ❌ | n/a | ❌ (driver + runtime) |
| Browsers | Chrome / Chromium / Edge | Varies | Varies | Many |

Expand All @@ -250,7 +266,7 @@ local CA, and it only drives Chromium-family browsers.
- [x] WebDriver Actions: hover, drag and drop, key chords, wheel, multi-touch
- [x] Safe-by-default exposure, token auth, domain allowlist seam
- [x] MCP server face: 16 browser tools over the same core, plus a stdio bridge
- [ ] Remote-browser UI with live screencast and input — stubbed at `/remote`
- [x] Remote-browser UI: live screencast, mouse/keyboard/clipboard, dialogs, tab management
- [x] Per-token and per-session domain policies, enforced in the browser ([docs/POLICIES.md](docs/POLICIES.md))
- [x] Per-tab console and browser log capture (control API + WebDriver `get_log`)

Expand Down
4 changes: 4 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ within 72 hours and to ship a fix or mitigation for confirmed issues within
| Session cookie never forwarded | `tether_tab` is stripped before any request reaches the page or the site. Site `Set-Cookie` headers are not relayed to clients; the browser keeps its cookies. |
| Background tabs; adopted tabs never closed | Tether doesn't steal focus, and only closes tabs it opened. |
| Launched Chrome binds DevTools to `127.0.0.1` | The DevTools port itself is never exposed by Tether. |
| Remote UI login cookie is `HttpOnly`, `SameSite=Strict`, path-scoped and UI-only | Browsers can't send bearer tokens, so `/remote/login` trades the token for an HMAC-of-token cookie that scripts can't read, other sites can't trigger, and the proxy/API/WebDriver never accept. Rotating the token signs every browser out. |
| Remote UI pages are locked down | Strict CSP (`script-src 'self'`, `frame-ancestors 'none'`), `X-Frame-Options: DENY`, `Referrer-Policy: no-referrer`; the websocket rejects foreign `Origin`s. `--no-remote` turns the UI off entirely. |

## Recommended exposure: Tailscale

Expand Down Expand Up @@ -71,6 +73,8 @@ a sandbox against a determined attacker who also shares the browser profile
| One agent reads or steers another agent's tab | Per-token ownership: non-admin tokens only resolve their own tabs, including via cookie, header and WebDriver session ID | Admin tokens see everything by design. |
| Tether used to read a page the user has open | Only tabs Tether opened, or tabs explicitly addressed by ID, are driven; adopting requires an admin token | Any admin token can address any tab by ID. |
| Prompt injection: a web page steers an AI agent connected over MCP | Server instructions tell the model page content is untrusted; tool annotations mark destructive tools; `--allow-domains` bounds where it can go; password values and `Set-Cookie` are never returned | An agent with access to your browser can be talked into acting as you. Use an allowlist and a client that confirms destructive tool calls. See [docs/MCP.md](docs/MCP.md#security). |
| A site embeds or scripts the remote UI (clickjacking, cross-site websocket) | `frame-ancestors 'none'`, `X-Frame-Options: DENY`, Origin check on the websocket, `SameSite=Strict` cookie, Origin/`Sec-Fetch-Site` check on login | — |
| Remote UI token guessing | Login requires the full token (≥16 chars) and each failure is delayed | Use a long random token (`openssl rand -hex 32`). |
| Chrome DevTools port exposed | `--launch` binds it to loopback; Tether never re-exposes it | If you start Chrome yourself with `--remote-debugging-address=0.0.0.0`, that is outside Tether's control. |
| A client attaches local files to a page (WebDriver file inputs) | Clients are fully trusted (see above); remote uploads land in a per-session temp dir deleted when the session ends | A token holder can make a page upload any file the Tether user can read. |
| Automation rearranges the person's screen | Window resize/maximise/minimise/fullscreen only act on windows holding nothing but Tether tabs | — |
Expand Down
5 changes: 4 additions & 1 deletion cmd/tether/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ func serve(args []string, stdout, stderr io.Writer) int {
"version", version.Version,
"addr", a.Addr.String(),
"browser", a.Browser,
"auth", cfg.Token != "",
"auth", cfg.HasAuth(),
"ca", a.CAPath,
)
fmt.Fprintf(stdout, "\n Tether %s is serving %s on http://%s\n", version.Version, a.Browser, a.Addr)
Expand All @@ -96,6 +96,9 @@ func serve(args []string, stdout, stderr io.Writer) int {
if cfg.MCP {
fmt.Fprintf(stdout, " MCP: http://%s/mcp (claude mcp add --transport http tether http://%s/mcp)\n", a.Addr, a.Addr)
}
if !cfg.NoRemote {
fmt.Fprintf(stdout, " Remote UI: http://%s/remote/ (watch and drive tabs from any browser)\n", a.Addr)
}
fmt.Fprintln(stdout)

if err := a.Serve(ctx); err != nil {
Expand Down
42 changes: 38 additions & 4 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ flowchart LR
P[Proxy face<br/>HTTP + CONNECT/MITM]
W[WebDriver face<br/>W3C common set]
M[MCP face<br/>Streamable HTTP tools]
R[Remote UI face<br/><i>stub</i>]
R[Remote UI face<br/>screencast + input]
A[Control API<br/>/tether/v1]
subgraph core[core]
S[session.Manager<br/>tab ⇄ session]
Expand All @@ -36,7 +36,7 @@ flowchart LR
C1 --> G --> P
C2 --> G --> W
C3 --> G --> M
C4 -.-> G -.-> R
C4 --> G --> R
G --> A
P & W & M & R & A --> E
E --> S
Expand All @@ -61,7 +61,7 @@ flowchart LR
| `internal/api` | Control plane: health, whoami, tabs, tab policies, eval, CA download. |
| `internal/server` | Listener, routing, per-token auth, loopback guard, graceful shutdown, exposure checks. |
| `internal/mcp` | MCP face: Streamable HTTP transport, per-agent sessions, browser tools, and the stdio bridge (see [MCP.md](MCP.md)). |
| `internal/remote` | Planned face, mounted and answering `501` today. |
| `internal/remote` | Remote-browser face: the embedded web app, a per-tab screencast hub, and the websocket input protocol (see [REMOTE.md](REMOTE.md)). |
| `internal/certs`, `internal/logx`, `internal/config`, `internal/version`, `internal/httpx` | Supporting pieces. |

## The session model
Expand Down Expand Up @@ -150,11 +150,45 @@ proxy auth and Tether control headers never reach the page.
| HTTP proxy | **Shipping** | `Sessions.Resolve`, `Core.Do` |
| WebDriver | **Shipping** | `Sessions.Create/Get/Close`, `Core.Navigate`, `cdp.Tab` primitives |
| MCP server | **Shipping** | `Sessions.Create/Get/Close`, `Core.Navigate`, `Core.Eval`, `Core.Do` (fetch mode), `core.Policy`, `cdp.Tab` primitives. Streamable HTTP on the same listener, so it inherits auth and Tailscale exposure. Each MCP session tracks one current tab and the tabs it opened; `core.Policy` is checked against the page's current URL before every page tool. Each agent authenticates with its own token, so per-token policies and tab ownership apply unchanged. |
| Remote UI | Stub (`/remote` → 501) | Will stream a tab with `Page.startScreencast` (WebRTC later) and forward input through `cdp.Tab.Click`/`Key`, letting a person watch or take over exactly the tab an agent is driving. |
| Remote UI | **Shipping** | `Sessions.Get/Create/Close`, `Core.Navigate` (address bar, under policy), `cdp.Tab` (`Key`, `Info`, history) and the optional `cdp.Screen` (screencast, mouse, text, dialogs). A person can watch or take over exactly the tab an agent is driving. |

Every face implements `core.Face` (`Name()` + `http.Handler`) and is mounted
by the server; adding one is a new package and one line in `internal/app`.

## The remote UI

```mermaid
sequenceDiagram
participant B as Person's browser (/remote/)
participant R as Remote face
participant H as Screencast hub
participant Ch as Chrome tab
B->>R: GET /remote/ws/{tab} (token or UI cookie, same Origin)
R->>H: join(tab)
H->>Ch: Page.startScreencast (first viewer only)
loop every paint
Ch-->>H: Page.screencastFrame (JPEG)
H-->>B: binary frame, newest wins per viewer
H->>Ch: screencastFrameAck
end
Note over H,Ch: quiet or background tab: Page.captureScreenshot keyframes
B->>R: {"t":"mouse"|"key"|"text"|"wheel"}
R->>Ch: Input.dispatch* (no session lock: input never waits on a navigation)
B->>R: {"t":"nav","url":…}
R->>Ch: Core.Navigate under the session lock and Policy
```

Screencasting is an optional capability, `cdp.Screen`, that the Chrome tab
implements; faces that never need video don't depend on it. One hub runs at
most one screencast per tab and fans frames out to every viewer, each with a
one-frame buffer, so a slow viewer drops frames instead of slowing the others.
Input bypasses the per-tab lock on purpose: a person must be able to click
"Cancel" while a script's navigation hangs. Watching a tab touches its
session, so it isn't reaped as idle. The server's auth layer, not the face,
owns the UI login cookie, and it is only honoured under `/remote/`. Each
token gets its own cookie, so a UI session acts as that token: same tab
ownership, same rules.

## Policy and identity

`internal/policy` is a leaf package: domain `Rules`, the `Principal` a request
Expand Down
2 changes: 1 addition & 1 deletion docs/POLICIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Tether hands out a logged-in browser, so "who may take it where" matters.
Policies answer that in three layers, all enforced in the core — every face
(proxy, WebDriver, control API, and the MCP and remote-UI faces as they land)
(proxy, WebDriver, control API, MCP and the remote UI)
is held to them.

| Layer | Set by | Scope |
Expand Down
Loading
Loading
0