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