远程架构
每条连接都通过 HTTP 和 WebSocket,把一个客户端(client)接到一个环境(environment)。环境拥有提供方(provider)、执行、文件和持久状态。直接访问、Tailscale、SSH 和 T3 Connect 改变的是客户端如何到达那台服务器;它们并不引入另一种执行模型。安装方法见远程访问。
身份独立于路由
一个环境(environment)的 ID 在服务器重启和端点变更之后保持不变。已保存的连接属于客户端(client)配置档的本地数据;服务器的身份和状态则不是。仓库身份可以把不同环境里的克隆关联起来,但绝不会在环境之间路由工作。一个项目(project)及其对话线程(thread)只属于一个环境;智能体要触及另一个环境的对话线程,只能通过对等链接。
规范键会跟随已存在的 upstream 远程,因此拉取请求(pull request)功能指向分叉所跟踪的那个仓库。分叉还会报告自己的 origin,客户端据此分组和标注,因此一个分叉绝不会被折叠成对其上游的一次检出。
环境 ID 初始化必须以原子方式发布一份完整的 ID。修复空的 ID 文件时会保留一份恢复文件,这样并发的或延迟到达的初始化程序会选出同一个胜出者。如果把这份恢复状态当作普通的临时文件清理掉,就可能在一台已经运行的服务器底下把身份改掉。
公布的端点只是可达性提示。只有正在连接的设备才能证明某条路由确实可用。尤其是,主机的环回地址一旦被另一台设备打开,指向的就是另一台机器。当可共享的端点不可用时,端点选择不得静默退回到环回地址。
已保存的环境持有一份有序的路由列表,驱动(driver)会连接第一条能连通的路由。每一条直连路由都会先用公开描述符做检查,因此一条已保存的局域网地址如果由另一台机器在另一个网络上应答,就不会收到凭据。这项检查并不能证明路由可用:当每一条路由都保持沉默时,仍会逐条尝试。连接失败的路由会继续下一条,包括已被阻止的路由,例如已登出的 T3 Connect;只有不兼容的服务器才会让遍历停下来,因为它在每一条路由上都是同一台服务器。当当前连接走的是较靠后的路由时,监督器会预检更靠前的路由,并在其中一条能够连上时替换当前会话。预检包含授权,因此一条能够应答、却拒绝这个客户端的路由,绝不会赔上一条仍在工作的会话;此后仍然失败的路由会被搁置一段冷却时间,以免不稳定的网络把连接来回切换。
已连接的服务器会报告它所绑定的局域网和 tailnet 地址,客户端把它们保存为学到的路由。学到的路由复用它被学到时所经那条路由的凭据:T3 Connect 访问令牌(它并不绑定到某个源,因为每个 DPoP 证明都会写明自己所签名的 URL),或者已配对的 bearer 令牌。已配对的令牌只有在自己是通过明文 HTTP 完成配对时,才会在 tailnet 之外学到明文 HTTP 地址,因此通过 HTTPS 配对的令牌绝不会开始以未加密方式传送。服务器不再报告的学到的路由会被丢弃,局域网地址变更时,旧地址就是这样被替换的;用户保存的路由绝不会被改动。报告的地址和任何公布的端点一样只是提示,因此学到的路由在投入使用之前,仍必须以这个环境的身份作出应答。
没有配置主机的后台服务,正是无头机器能够这样工作的原因。它在每个 IPv4 接口上监听,并关闭每一条连接,除非直接对端的两个地址都是环回、私有或 tailnet 的 IPv4。这样,VPS 的公网地址保持关闭,包括公网地址被 NAT 到私有接口上的云虚拟机。它并不是防火墙:T3 Connect 和 Tailscale Serve 按设计通过环回到达服务器,而重写源地址的代理看起来像是私有的。如果每个地址单独一个监听器,就会错过 DHCP 的变化;通配监听则让所报告的地址保持最新。
拉取请求路由的信任覆盖整份路由列表。添加或更改一条路由会撤销这份信任;重新排序则不会,因为同样的地址仍然受信任。
对等链接
对等链接让一个环境(environment)的智能体在另一个环境里工作。发起链接的环境使用对方给出的配对码,以外部 MCP 客户端的身份登录对方的 /mcp,并把该会话的令牌保存在自己的密钥存储里。对方环境不需要任何新的东西:它会多看到一个 OAuth 客户端,在共享与访问里列出它,也可以在那里撤销。在发起链接的一侧取消链接,只会忘掉该令牌。
- 已链接环境测试版(
linkedEnvironmentsEnabled)同时作为两侧的开关。 在发起链接的一侧关闭时,PeerLinks.link、链接请求以及PeerForwarding.call/links会拒绝;readThread、waitForThread和interruptThread则不会,因此一个已经在运行的远程任务仍会完成,也仍可以被取消。在接收一侧关闭时,链接注册会被拒绝,链接会话得到 403。发起链接的一侧把这报告为可重试,于是该任务的跟随者会继续等待,而不是让任务失败。 - 当工具的参数把
OrchestratorMcpEnvironmentTarget取作environmentId时,该工具在已链接的环境中运行。McpToolAccess.toLayer会在工具的访问声明检查过调用方之后,把这样的调用发到那里,因此只读或已经结束的调用方会在任何内容发出之前被拒绝。调用所点名的对话线程(thread)或项目(project),在其实际所在之处接受检查。以调用方对话线程自身身份行事的工具,不能带上已链接的环境。 - 每次转发的调用都携带调用方智能体的模式,写在
T3-Mode-Limit里。接收方的/mcp用它时,只会收窄客户端(client)已经批准的访问。因此,计划模式的智能体无法通过一条完全访问的链接去启动完全访问的工作。 - 描述符中的环境 id 用于过滤地址;它并不能证明身份。 令牌只会发给报告该已链接 id 的地址,因此一个现在报告的是另一台机器的局域网地址,绝不会收到这个令牌。不过任何东西都可以报告任意 id,所以只有来自用户客户端的地址才算某台机器的地址;环回地址永远不算,因为它指的是打开它的那一台机器。智能体给出的地址会单独记录,
linkRequestTarget只有在客户端已经恰好能在该地址上到达一台机器时,才会为它指定这台机器。单凭一个地址,绝不会把尚未过期的链接移到另一个地址。 - 一侧的所有调用方共享该链接在另一侧的唯一会话,因此幂等键在离开之前会与调用方的命名空间一起做哈希。否则,两个复用同一个键的智能体会在远端发生碰撞。
- 转发的等待会被拆成每次至多 50 秒的调用,因为接收方的
/mcp在等待保持打开期间什么都不发送,而 T3 Connect 的边缘大约会在 100 秒后丢弃空闲请求。
一条在低于完全访问的级别完成配对的链接所启动的工作,会始终留在该链接之内。 接收环境知道某个会话是对等链接,是因为发起链接的一侧用 t3code-peer-link 这一软件 id 注册自己的 OAuth 客户端,并且该标记被签入会话。该会话启动的对话线程会带上它,连同配对时的访问权限,作为不可变的 linkOrigin;它们派生出的子智能体、分叉和 create_threads 也是如此。规则位于 mcp/linkOrigin.ts,并在共享的访问声明中运行,而不是按工具逐个运行:
- 已链接的工作只能更改来源相同的对话线程,不能更改用户自己的对话线程,也不能更改另一条链接的对话线程;
- 它不能更改项目、设置或计划任务,也不能使用接收环境自己的链接;
- 它的项目设置脚本会被跳过(
ProjectSetupScriptRunner会检查每一条运行这些脚本的路径)。
读取不受围栏限制,而且客户端自己的 thread.create 绝不可能携带 linkOrigin。
委派任务可以在已链接的环境中运行。 此时它的子项在那边就是一条普通的对话线程,因此这边的父项记录该任务时并没有自己的子对话线程(用的是 remoteChild,而不是 childThreadId)。RemoteDelegation 中的跟随者会等待那条对话线程,并用 delegated_task.remote.complete 完成该任务。这条内部命令复用了本地收尾里属于父项的那一半,因此父项会以同样的方式被唤醒。启动时会再次跟随仍处于打开状态的远程任务,这也避免重启恢复把它们当成已被放弃的提供方工作。
链接所做的是路由,而不是隔离:链接启动的智能体,以接收环境的用户身份运行。因此,完全访问的链接不受围栏限制:它的智能体本来就可以做该用户能做的任何事情,围栏只会挡住 T3 自己的工具。它的工作仍会携带 linkOrigin,于是客户端可以显示它来自何处。没有带上访问权限的印记(产生于访问权限开始被记录之前)会受到围栏限制。
托管 Web 是客户端
托管的 Web 应用把连接目录存在浏览器里,并直接连接到每一个环境(environment)。它不代理流量,也不在服务端持有配对状态。因此,把界面以 HTTPS 托管,并不能让一个明文 HTTP 的局域网后端从该浏览器上下文中被访问。
托管配对 URL 在查询参数里标识后端,并把配对密钥放在片段里。片段不会进入发往托管源的请求。浏览器与环境交换该密钥,并把它从历史记录中去掉。把令牌放进查询参数,就会把它泄露给错误的源。
访问与进程所有权是两回事
Tailscale 为普通配对提供一个端点,因此不需要单独的环境(environment)类型。对每一条路由,身份验证仍然由环境负责。参见环境身份验证和 T3 Connect 信任边界。
SSH 既可以启动服务器,也可以转发端口。桌面端的主进程拥有这一生命周期,因为它能拉起 SSH 并处理身份验证提示。渲染进程通过共享的连接运行时使用转发后的端点。SSH 清理 只有在启动器拥有这台远程服务器时才会停止它;启动器发现时已经在运行的服务器,必须在客户端(client)断开之后继续存活。重新连接会在打开应用传输之前先恢复转发。
远程服务器可以比多个客户端版本存在得更久。客户端必须使用公布的能力,并处理这些能力缺失的情况,而不能假定自己的版本就描述了服务器。进程替换属于启动器的更新协议;连接运行时处理由此产生的断开。
没有本地环境的桌面
桌面应用通常会启动自己的主服务器,但桌面设置 localEnvironmentEnabled(apps/desktop/src/settings/DesktopAppSettings.ts)会关掉这一行为。更改它会重新启动应用;任何本地状态都不会被删除。下次启动时,主进程会跳过端口选择、服务器暴露,以及主后端和 WSL 后端,并立刻打开窗口。渲染进程通过 desktopBridge.getLocalEnvironmentEnabled() 看到这一点:readPrimaryEnvironmentTarget 返回 null,于是会跳过主身份验证和由平台管理的发现,只连接已保存的环境(配对、中继(relay)、SSH)。这一点之所以可行,是因为桌面渲染进程并不是由后端提供的:t3code:// 方案从磁盘提供打包的客户端(开发时是 Vite),API 流量始终发往该环境自己的 URL。