Repository navigation
feat: snail007/goproxy front door; relicense to GPL-3.0 - #8
Merged
Merged
Conversation
…ense to GPL-3.0 New internal/frontdoor accepts every connection and classifies it with goproxy's HTTP request decoder (CONNECT, absolute-form proxy request, or a local request) before net/http serves it. Auth, the loopback guard and routing are unchanged. goproxy issues contained in frontdoor: - its decoder reads the head in one fixed-size Read: we read the whole head (up to 1 MiB) first and hand it over in one piece; - it logs request lines, query tokens included, to the global logger: the global logger is discarded (Tether logs only through slog); - it panics on short malformed heads: recovered into an error; - ServerChannel.ListenTCP races on its error result: we run our own accept loop. goproxy is GPLv3, so Tether is relicensed from MIT to GPL-3.0-or-later. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X9CEjRcHsJmgWdtz6ZTKsk
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
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HpbHKDrhpTtF3U8jAhD61N
…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
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X9CEjRcHsJmgWdtz6ZTKsk
# Conflicts: # docs/ARCHITECTURE.md # go.sum
adeelahmad
marked this pull request as ready for review
October 11, 2026 04:35
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Before: connections went straight from a
net.Listenerintonet/http, and Tether was MIT-licensed.After: every connection passes through a new
internal/frontdoor. It uses snail007/goproxy's HTTP request decoder to classify the connection as a CONNECT tunnel, an absolute-form proxy request, or a request for Tether's own endpoints, then hands it tonet/http. Auth, the loopback guard and routing are unchanged. Because goproxy is GPLv3, Tether is relicensed to GPL-3.0-or-later. That covers LICENSE, the README, CONTRIBUTING, the GoReleaser image label and the Homebrew cask license.How
frontdoorcontains four goproxy problems found while integrating it:Read. The front door reads the whole head first (up to 1 MiB, with a 30s deadline) and passes it over in one piece, so large cookies and slow clients still work.string(buf)[:50]). The panic is recovered and the connection r 10BC8 ejected.ServerChannel.ListenTCPhas a data race on its error result (fails under-race). Tether runs its own accept loop instead.Verified
-race, including new frontdoor tests: head over 4 KB, HTTP/1.0 without Host, malformed input, IPv6 and Close.🤖 Generated with Claude Code
https://claude.ai/code/session_01X9CEjRcHsJmgWdtz6ZTKsk
Generated by Claude Code