环境认证
环境(environment)自行签发会话,并强制落实这些会话的能力范围。云端身份和中继(relay)凭据属于另一条信任边界,说明见 T3 Connect。中继令牌绝不能当作环境登录。
传输方式变了,授权仍然有效
配对委托出去的是一组作用域。交换引导凭据可以把这份授权收窄,但不能把它放宽。默认授权包含访问和中继管理:terminal:operate 是以服务器用户身份打开的 shell,orchestration:operate 可以启动一个完全访问的智能体,而这两者都能通过 t3 auth session issue 签发包含全部作用域的会话,所以就算默认授权里不给管理权限,也形成不了一道边界。权限更窄的客户端(client)得到的是显式列出的作用域。再创建一条配对链接,必须同时持有 access:write,以及每一个正被委托的作用域。认证处理程序在签发时强制执行这一点;客户端标签和设备元数据不起任何授权作用。
访问读模型(read model)里只有配对元数据,绝不会有能够取回的配对机密。只有创建时的响应才会返回原始凭据。不然,能读到连接列表,就等于有了一条拿到其他客户端授权的途径。
浏览器 Cookie、Bearer 令牌和 DPoP 令牌,适配的是同一套按作用域限定的会话模型。DPoP 把令牌绑定到客户端的证明密钥上;证明无效就必须失败,不能退回去走 Bearer 认证。OAuth 令牌交换那一套词汇,让这些授权的含义为人熟知。
访客链接不是会话
一条访客链接让某一个浏览器可以读取某一个对话线程(thread),并能够提议消息。它的令牌,是用服务器密钥对对话线程和分享 id 做的 HMAC(GuestShareService),因此并没有把机密存下来。对话线程投影(projection)里的那份分享是唯一的状态:替换、关闭或归档它,都会撤销全部令牌;编排器(orchestrator)的串行命令则保证兑换和发送都只发生一次。访客令牌永远不会进入 SessionStore,因此任何 RPC、WebSocket 或 MCP 路径都无法接受这种令牌。访客路由还要求 T3 Connect 仍在为该链接的主机名提供服务;请求里的 Host 头并不做核对,因为局域网客户端可以伪造它。
MCP 客户端是独立的受众
并非由 T3 Code 启动的智能体登录 /mcp,经由一个范围收窄的 OAuth 授权码服务器(McpOAuth)。用户自己机器上的智能体,它接受回环重定向 URI;托管的智能体(ChatGPT、机器人)则接受任意 HTTPS 重定向。HTTPS 重定向意味着:别人发给所有者的一条链接,可以把访问权交到那个人手里,所以批准页会写明访问将去往哪一台主机,批不批准仍由所有者决定。每个客户端都是公开客户端,并靠 PKCE 证明自己;哪怕客户端要求机密,注册时也不会发给它。客户端注册是无状态的,因此未经认证的调用方无法把服务器的状态撑大。批准时,要么消耗一个一次性配对码,要么使用带有 access:write 的浏览器会话;绑定了证明的 T3 Connect 码会被拒绝,并且不会被消耗。
用户授予的是只读访问,或者一个运行时模式(runtime mode)的上限,而不是一份作用域列表:MCP 工具全部属于编排,单凭 orchestration:operate 就足以让智能体以完全访问启动一个对话线程,再借它去行动。得到的是一个普通会话,主体为 mcp-client。只读授权只握有 orchestration:read。在 /mcp 上,它只放行 McpToolAccess 里声明为读取的工具;每个工具都必须声明谁可以调用,代码才能编译。其他任何授权都会再加上 orchestration:operate,以及一个签过名的上限。只有 /mcp 接受这些会话。其余每一条 HTTP 和 WebSocket 路径都会拒绝这个主体,因为 RPC 接口面会让智能体做出超出上限的行为。在 MCP 内部,限制由凭据来定,工具参数只负责点明目标;参见 threadAccess。
签发方 URL 和资源 URL 取自请求的 Host 以及 X-Forwarded-Proto,因此同一台服务器可以经回环、Tailscale Serve 和 T3 Connect 隧道应答。如果代理改写了 Host,或者丢掉了协议头,登录就会失败。
Bearer 客户端和 DPoP 客户端通过已认证的 HTTP 取得短时有效的 WebSocket 票据,这样长期令牌就不会出现在套接字 URL 里。票据会标明它打开的是哪条路由:RPC 套接字,或者媒体路由(设备中心、预览流)。媒体路由在 <img>、EventSource 和若干套接字之间共用同一张票据。DPoP 客户端的 RPC 票据绑定在它的证明密钥上,升级请求把一份新证明放在 dpop 查询参数里,因为 WebSocket 没法设置请求头;在明文 HTTP 的局域网路由上截到的票据,没有这把密钥就毫无用处,而证明里的 jti 会像其他证明一样被消耗掉。媒体票据仍是 Bearer,因为每一次媒体请求都得有自己的证明。早于这套机制的客户端签发的是未绑定票据,两条路由都能打开。浏览器会话可以用自己的 Cookie 来认证这次升级。握手成功并不会多给任何授权:每个 RPC 都声明了自己要求的作用域,WebSocket RPC 组的 RpcScopeAuthorization 中间件会在任何处理程序运行之前先做检查。
作用域有变动时,也不得因此让较旧的客户端连不上。令牌交换会把认得出的请求与配对授权取交集;已退役的名字和未知的名字都会被丢掉。一个请求如果没有任何被授予的作用域,会在消耗这条链接之前就失败。作用域拆开之后,已经存下的凭据绝不会被扩大。
认证响应在原有的线路词汇之内保留 scopes,并用 permissions 给出精确授权。只要带了 permissions,新客户端就用它,哪怕是空的。较旧的服务器会省掉这一项,于是客户端对那些服务器本来就支持的功能,仍用旧式的父级检查。这些发生在客户端的检查,从不改变服务器的授权。权限错误同样保留旧式的 requiredScope,并附上精确的 requiredPermission,这样被拒绝的 RPC 旧客户端也解得开。响应里不认识的权限会被忽略;作为输入提交的授权则保持严格。
桌面应用一旦重启,就会忘掉上一个本地 Bearer 令牌,于是它那份可复用的引导授权,会替换掉同一主体、同一方法下更早的会话。撤销和插入处在同一个 数据库事务 里;替换如果失败,旧凭据必须仍然可用。配对会话和浏览器会话不走这条替换规则。
可复用的开发凭据
Web 开发环境可以在同一个主机名上,跨多个工作树(worktree)和端口,接受同一个 T3CODE_DEV_AUTH_TOKEN。这个令牌,以及含有它的启动 URL,都会授予管理权限。桌面应用和非开发用的服务器会忽略它。设置方法见 开发运行手册。
每个环境都会把这个值做哈希,并在启动时往自己的数据库里写入一条初始记录。环境之间不共享 SQLite 数据、签名密钥、环境 ID、会话记录、配对授权,也不共享撤销状态。本地撤销在重启之后仍然保留,也不会影响到另一个工作树。去掉这个值,或者把它轮换掉,再重启,旧凭据和它的 WebSocket 票据就会失效。
普通凭据的优先级更高。普通凭据一旦被拒绝,绝不会再退回到这份可复用凭据。OAuth 交换创建的是普通的本地 Bearer 子凭据或 DPoP 子凭据,过期和撤销都按正常规则。可复用的 Cookie 在 30 天后过期。
环境是文件系统边界
项目(project)划分的是组织上的边界,并不是文件系统沙箱。filesystem:read 允许读取服务器账户能够读到的文件,包括项目以外的绝对路径。这样,客户端才能把智能体写在临时目录里的产物显示出来。相对路径和写入仍然要遵守 工作区路径规则。
签过名的资源 URL 属于 Bearer 凭据。指向主机上某段媒体的 URL,授予的是对一个规范文件、以及该文件的设备与 inode 身份的访问,并不包括它所在的目录。资源访问在把文件发出去的时候,会重新核对已打开文件的身份,所以原子替换需要一个新 URL,原地修改同一个文件则不需要。这样授权的 HTML 文件不能去加载同级资源;按目录划定范围的工作区预览,是另一项授权。客户端应当分享创作时用的文件引用,免得把临时 URL 里的凭据泄露出去。filesystem:read 在 URL 签发的时候检查,而不是在提供这个 URL 的时候检查:授权被撤销之前签发的 URL 会一直用到过期,而且它并不绑定在签发它的那个会话上。
主机上的视频可能被原地改写。它们的 HTTP 响应 不带缓存验证器,因为对 If-Range 来说,文件元数据证明不了逐字节的同一性。一旦加上弱验证器,原生播放器的跳转就会变成把整个文件下载下来。