Repository navigation
feat: per-token and per-session domain policies - #9
Merged
Merged
Conversation
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
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
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.
Requested by Adeel · project thread
What
Before: one global
--allow-domainslist and one shared token. Every client could see and drive every tab, and the allowlist only checked the URL a client asked for, so a redirect, a click or page script could still leave it.After: policies come in layers, and every face enforces them.
--allow-domains, plus the new--deny-domains, plus the policy file's top level.--policy-filedefines named tokens. Each has its own allow/deny rules, anadminflag and ascriptspermission.--tokenis still accepted and becomes the admin tokendefault.POST /tether/v1/tabs {"policy":…},GET/PUT /tether/v1/tabs/{id}/policy, or WebDrivertether:options.policy.A URL must pass global ∩ requesting token ∩ owning token ∩ session rules, and deny always wins. Restricted tabs also get a guard inside the browser, built on the DevTools
Fetchdomain. It pauses every top-level document request and fails the ones the policy forbids withERR_BLOCKED_BY_CLIENT, so redirects, link clicks andlocation =are covered too. As a second line of defence, the core re-checks the URL each request finally landed on.Tab ownership: a non-admin token only sees the tabs it opened. It also gets its own default tab per site and cannot adopt existing tabs. Anything it doesn't own looks like
410 tab_gone.GET /tether/v1/whoamireports the calling token and its permissions.Why
Per-session and per-agent policies were on the v1 roadmap and are needed for the first release (the MCP face will rely on per-agent tokens).
How tested
make checkequivalents: gofmt,go vet(incl. integration tag),golangci-lint(0 issues),go test -race ./...make test-integrationagainst real headless Chromium. The newTestPoliciesdrives a restricted token and checks that script navigation, link clicks and redirects are blocked, that another token gets 410 on the agent's tab through the API and the proxy, that session deny rules work, and that admin browsing is unaffected. I confirmed the test fails when the browser-side guard is disabled.docs/POLICIES.md, README, SECURITY.md (defaults and threat model), ARCHITECTURE.md, WEBDRIVER.md, CHANGELOG, andexamples/policy.json(which a test keeps valid)Security
Known limits (documented): the rules govern top-level navigation. Subresources and in-page
fetch()are not restricted, so"scripts": falseis recommended for agents.🤖 Generated with Claude Code
https://claude.ai/code/session_01UfsJJgC4Jp8VqckdcChH4W
Generated by Claude Code