1# Environment authentication
2
3The environment issues its own sessions and enforces their capabilities. Cloud
4identity and relay credentials belong to a separate trust boundary, described in
5[T3 Connect](./t3-connect.md). A relay token is never an environment login.
6
7## Authority survives transport changes
8
9Pairing delegates a set of scopes. Exchanging a bootstrap credential can narrow
10that grant but cannot widen it. Ordinary pairing does not grant access-management
11or relay-management authority. Creating another pairing link requires both
12`access:write` and every scope being delegated. The
13[auth handlers](../../apps/server/src/auth/http.ts) enforce this at issuance;
14client labels and device metadata have no authorization role.
15
16The access read model contains pairing metadata, never recoverable pairing
17secrets. Only the creation response returns the raw credential. Otherwise read
18access to the connections list would become a way to acquire another client's
19authority.
20
21Browser cookies, bearer tokens, and DPoP tokens adapt the same scoped session
22model. DPoP binds a token to a client's proof key; an invalid proof must fail
23rather than fall back to bearer authentication. The OAuth token-exchange
24vocabulary gives these grants a familiar meaning.
25
26### MCP clients are a separate audience
27
28Agents T3 Code did not launch sign in to `/mcp` through a narrow OAuth
29authorization-code server ([McpOAuth](../../apps/server/src/auth/McpOAuth.ts)).
30It accepts loopback redirect URIs for agents on the user's machine and any
31HTTPS redirect for hosted agents (ChatGPT, bots). An HTTPS redirect means a
32link someone else sends the owner can deliver access to that someone, so the
33approval page names the host access goes to and approving stays the owner's
34call. Every client is public and proves itself with PKCE; a client that asks
35for a secret is registered without one. Client registration is stateless, so an unauthenticated caller cannot grow server
36state. Approval spends a one-time pairing code, or uses a browser session with
37`access:write`; proof-bound T3 Connect codes are refused without being spent.
38
39The user grants either read-only access or a runtime-mode ceiling, not a
40scope list: MCP tools are all orchestration, and `orchestration:operate`
41alone would let an agent start a thread in full access and act through it.
42The result is an ordinary session with subject `mcp-client`. A read-only
43grant holds `orchestration:read` alone. On `/mcp` it passes only tools
44declared as reads in [McpToolAccess](../../apps/server/src/mcp/McpToolAccess.ts),
45where every tool must declare who may call it to compile. Any other grant adds
46`orchestration:operate` and a signed ceiling. Only `/mcp` accepts these sessions. Every other HTTP and WebSocket
47path rejects that subject, because the RPC surface would let the agent act
48above its ceiling. Inside MCP the credential sets the limits and tool
49parameters only pick targets; see
50[threadAccess](../../apps/server/src/mcp/threadAccess.ts).
51
52Issuer and resource URLs come from the request's Host and
53`X-Forwarded-Proto`, so one server answers over loopback, Tailscale Serve and a
54T3 Connect tunnel. A proxy that rewrites Host or drops the protocol header
55breaks sign-in.
56
57Bearer and DPoP clients obtain short-lived WebSocket tickets through authenticated
58HTTP so long-lived tokens stay out of socket URLs. Browser sessions can
59authenticate the upgrade with their cookie. A successful handshake grants no
60extra authority: [every RPC declares a required
61scope](../../apps/server/src/auth/RpcAuthorization.ts), and the WebSocket RPC
62group's `RpcScopeAuthorization` middleware checks it before any handler runs.
63
64Scope changes must not prevent older clients from connecting. Token exchange
65intersects recognized requests with the pairing grant; retired and unknown names
66are dropped. A request with no granted scopes fails before consuming the link.
67Stored credentials are never expanded when scopes split.
68
69Auth responses keep `scopes` within the original wire vocabulary and include
70`permissions` for the exact grant. New clients use `permissions` when present,
71even if empty. Older servers omit it, so clients use legacy parent checks for
72features those servers already support. These client checks never change server
73authorization. Permission errors likewise retain a legacy `requiredScope` and
74add the exact `requiredPermission`, so a denied RPC stays decodable by old clients.
75Unknown response permissions are ignored; grant inputs stay strict.
76
77Desktop restarts forget the previous local bearer token, so its reusable
78bootstrap grant replaces earlier sessions for the same subject and method.
79Revocation and insertion share a [database
80transaction](../../apps/server/src/persistence/AuthSessions.ts); a failed
81replacement must leave the old credential usable. Pairing and browser sessions
82do not follow this replacement rule.
83
84### Reusable dev credential
85
86Web development environments can accept one `T3CODE_DEV_AUTH_TOKEN` across
87worktrees and ports on one hostname. The token and startup URLs that contain it
88grant administrative access. Desktop and non-development servers ignore it. See
89the [development runbook](../operations/development.md#reusable-dev-credential)
90for setup.
91
92Each environment hashes the value and seeds its own database record at startup.
93Environments do not share SQLite data, signing keys, environment IDs, session
94records, pairing grants, or revocation state. Local revocation persists after
95restart and does not affect another worktree. Removing or rotating the value
96and restarting invalidates the old credential and its WebSocket tickets.
97
98Normal credentials keep precedence. A rejected normal credential never falls
99back to the reusable credential. OAuth exchanges create ordinary local bearer
100or DPoP children with normal expiry and revocation. The reusable cookie expires
101after 30 days.
102
103## The environment is the filesystem boundary
104
105Projects are organizational boundaries, not filesystem sandboxes.
106`filesystem:read` permits reading files the server account can read, including
107absolute paths outside a project. This lets clients display artifacts that an
108agent writes in a temporary directory. Relative paths and writes still follow
109the [workspace path rules](../../apps/server/src/workspace/WorkspaceFileSystem.ts).
110
111Signed asset URLs are bearer credentials. A URL for media on the host grants
112access to one canonical file and its device/inode identity, not its containing directory.
113[Asset access](../../apps/server/src/assets/AssetAccess.ts) rechecks the opened
114file's identity when serving it, so atomic replacement requires a new URL while
115editing the same file in place does not. An HTML file authorized this way cannot
116load sibling assets; directory-scoped workspace previews are a separate grant.
117Clients should share the authored file reference so they do not disclose the
118temporary URL's credential. `filesystem:read` is checked when the URL is minted,
119not when it is served: a URL issued before the grant was revoked keeps working
120until it expires, and it is not bound to the session that minted it.
121
122Host videos can change in place. Their [HTTP
123responses](../../apps/server/src/http.ts) omit cache validators because file
124metadata cannot prove byte-for-byte identity for `If-Range`. Adding weak
125validators would turn native-player seeks into full downloads.