跳转到内容

Device 反向连接

Device 让本机主动建立 WebSocket,把本地 Shell、文件或 SDK provider 挂到远端树。网关不需要反向访问内网,适合开发机、构建节点、内网服务和 Kubernetes Sidecar。

适合:

  • 能力只存在于内网或本机;
  • 希望用心跳新鲜度反映连接活性:在线、心跳变陈旧、离线三态可分;
  • 结构化任务需要在设备离线时先持久入队,恢复连接后再执行;
  • 希望用独立 Device SK 限制注册位置和调用范围;
  • 容器或 Sidecar 可以稳定运行一个长驻进程。

不适合:

  • 可公开托管的标准 MCP/HTTPS 服务;
  • 需要网关主动扫描网络;
  • 无法接受长连接和本机命令执行风险的环境。

不要把 Admin SK 放进设备。设备 SK 至少需要允许目标路径的注册,并用 registerPaths 收紧位置。具体 tb sk create 参数以当前 CLI 帮助为准;一个典型意图是:

owner: device:build-01
registerPaths: [device/build-01]
scopes:
- device/build-01/**: read,call,register

registerPaths 只是额外收紧,不能替代 register scope。管理员还应决定谁能对设备路径 read 和 call。

可直接照做的设备 SK 签发与验证流程见权限、SK 与可见性。设备 SK 与上游 Secret 的区别见密钥、出站身份与安全边界。

Shell 默认会创建,但 allowlist 缺省为空,所以所有命令均拒绝。只暴露确实需要的命令:

Terminal window
tb connect \
--device-id build-01 \
--path device/build-01 \
--allow uname \
--allow git \
--fs ./shared \
--fs-readonly

如果完全不需要 Shell:

Terminal window
tb connect \
--device-id docs-reader-01 \
--path device/docs-reader-01 \
--no-shell \
--fs ./docs \
--fs-readonly

tb connect 是前台长驻命令,自带心跳和网络闪断重连;进程崩溃后的拉起可以交给 systemd、Docker 或 Kubernetes。--timeout 是 HTTP 单请求参数,不适用于长驻连接,CLI 会拒绝它。

用常驻 daemon 托管连接(仅 Linux)

Section titled “用常驻 daemon 托管连接(仅 Linux)”

在 Linux 上,tb daemon 可以把一次 tb connect 固化成开机自启、崩溃自动重启的后台服务,省去自己写 systemd unit:

Terminal window
tb daemon install \
--device-id build-01 \
--path device/build-01 \
--allow uname \
--fs ./shared \
--fs-readonly
tb daemon status

install 会在当前用户下生成并启用一个 systemd user unit(tool-bridge-device.service),透传 tb connect 的设备 ID、路径与 --allow/--fs/--fs-readonly/--no-shell 等白名单参数;SK 只写入 0600 权限的配置文件,不进入命令行 argv。其余子命令是 tb daemon status / logs / restart / uninstall。

它只支持 Linux + systemd,并且拒绝以 sudo/root 运行;在其他平台会直接报错退出,macOS/Windows 或非 systemd 环境仍用容器 restart policy 或进程管理器托管 tb connect。uninstall 只移除本机服务,不会吊销服务端的 Device SK——需要时另行在网关侧禁用或删除。精确 flag 以 tb daemon install --help 为准。

Shell 与文件入口仍按实时连接调用。对于构建、同步、采集等可异步完成的操作,使用 --command-profile 暴露固定 executable/argv 的结构化命令,并把命令 delivery 声明为:

  • realtime:只允许实时调用,也是缺省值;
  • mailbox:只允许持久化入队;
  • both:允许调用方在实时、直接入队和安全 fallback 之间选择。

CLI/daemon 会在初次连接和每次重连进入 ready 后主动 drain Mailbox;它不会启动固定轮询 timer。完整 profile、tb call --delivery、operation 查询/取消、歧义终态与部署要求见Device 离线投递与 Mailbox。

连接成功会输出确认后的 mountPath。另一个有权限的身份可以检查:

Terminal window
tb device ls
tb tree device/build-01 --depth 3
tb help device/build-01/shell
tb help device/build-01/fs

调用 Shell 前先读取 ~help 的参数 schema,再原样调用其中的完整 cmds[].path。文件系统作为 Context 暴露,使用 tb ctx ls/cat 等命令;只读挂载不应披露写动词。

成功证据不仅是“WebSocket 已连接”:

  1. CLI 收到 ready,mountPath 与预期一致;
  2. tb device ls 显示 online;
  3. 树中只有预期的 shell/fs 节点;
  4. allowlist 内命令成功,未授权命令被拒;
  5. --fs-readonly 下写动词不存在或被拒;
  6. 使用越界 mountPath 的测试连接被 registerPaths 拒绝。

presence 是三态,不是在线/离线开关

Section titled “presence 是三态,不是在线/离线开关”

设备节点的活性由心跳新鲜度派生,而不是一个连接布尔值。当前有三种状态:

presence 含义
online 心跳仍在 TTL 内(约 90s,即 SDK 心跳周期 30s 的 3 倍)
stale 连接位还在,但久无心跳,很可能已不可路由
offline 连接已拆除;实时调用不可路由,Mailbox 命令仍可持久入队

stale 是有意的中间态:连接标记为在线但超过心跳 TTL(或缺少 lastSeenAt)时,presence 会保守降级为 stale,提示这个设备“很可能调不通”,而不是乐观地当作 online。降级只发生在发现投影阶段,不回写权威状态——presence 是实时路由提示,不是设备身份或 Mailbox 能力的可见性条件。

暴露面各不相同,写脚本时要区分:

  • ~tree 的 JSON 里有 presence 字段;tb tree 只对 stale/offline 打标记,online 不额外标注;
  • device 挂载目录的 ~help 会在描述里拼上 (stale);
  • Dashboard 用一个 presence 徽标展示三态,并保留离线设备及其 Mailbox-capable 命令;
  • tb device ls 不是三态:它是裸存储态的两列 ONLINE(yes/no)+ LAST_SEEN,不要把它读成 stale/offline。

具体阈值和字段名以目标实例运行时返回为准,不要在客户端写死心跳窗口。

设备收到调用者来源与权威截止时间

Section titled “设备收到调用者来源与权威截止时间”

网关转发给设备的调用信封会带上一个 context,包含:

  • 调用者来源(provenance):caller.keyId 与 caller.owner,是发起调用身份的非敏感标识,不是 SK 明文(displayName 字段类型存在,但当前网关不填,别依赖它有值);
  • trace id:便于把设备侧日志与网关调用对齐;
  • 权威 deadline expiresAt:以网关时钟表示的绝对截止时间,也就是网关真正取消该调用的时刻(约 60s)。

信封里不含 scope、SK 或授权决定——scope 在网关转发之前就已判完。设备据此做本地审计与限时,但不做授权裁决:设备是执行层,不承担 scope 否决。不要在设备端用 provenance 重新实现一套权限模型,也不要因为拿到了调用者身份就放宽本地白名单。

超时后网关只向设备发送一个协作式取消信号(AbortSignal),能否真正停下取决于设备端实现。因此超时不代表副作用已经撤销:expiresAt 之后,这次调用产生的外部影响应按“未知”处理,由设备或上游自己保证幂等与补偿。

搜索索引对单台设备有节点容量上限(当前约 500 个,以实现为准)。超过时设备照常挂载、照常可调用,只是溢出的工具不会进入全局 ~search。

要注意这个信号的可见范围:溢出只会在网关进程的日志里以 console.warn 出现,tb、Dashboard、HTTP API 和设备 ready 帧里都看不到。它是运维在网关日志中排查的告警,不是终端用户能直接观察到的状态。因此工具很多的设备不要只靠 ~search 发现能力,应结合 ~tree 和节点级 ~help;怀疑有工具“搜不到但能调”时,去查网关日志。

官方 CLI 镜像包含完整 tb 命令。作为长驻设备时,用运行时 Secret 注入 BaseURL/SK,并同时收紧宿主 volume:

Terminal window
docker run -d \
--name tb-device \
--restart unless-stopped \
-e TB_BASE_URL=https://tb.example.com \
-e TB_SK="$TB_DEVICE_SK" \
-v "$PWD/shared:/workspace:ro" \
ghcr.io/tokenrollai/tool-bridge-cli:<version> \
connect \
--device-id build-01 \
--path device/build-01 \
--allow uname \
--fs /workspace \
--fs-readonly

将 <version> 替换为你已经阅读发布说明并验证过的明确版本或 digest,不要改成浮动 tag。

生产环境固定版本或 digest,不使用 edge。Sidecar 只能看到自己的根文件系统以及与业务容器显式共享的 volume;同 Pod 共享网络不等于共享文件。

SDK 的 connect() 默认把本实例通过 registerTool/registerContext 登记的节点和工具表上报:

const connection = tb.connect(
'https://tb.example.com',
process.env.TB_DEVICE_SK!,
{ deviceId: 'my-service-01' },
)
await connection.ready

长驻服务应显式设置稳定 deviceId,避免重启后出现新的身份,也让 presence 在重连后落回同一个节点。Linux 上可以用 tb daemon 托管稳定 deviceId 的常驻连接。SDK 默认 expose 面向自定义 nodes;CLI 才提供现成 Shell 和文件 executor。

现象 处理
ready 前收到权限拒绝 检查 register scope、registerPaths、mountPath 和 deviceId
设备反复 reconnect 检查代理是否保留 WebSocket upgrade、Authorization,以及网络空闲策略
Shell 所有命令都拒绝 缺省 allowlist 为空;显式增加最小 --allow
Sidecar 看不到业务文件 两个容器没有挂同一个 volume,或挂载路径不同
文件能读不能写 使用了 :ro/--fs-readonly,这是预期安全行为
实时调用返回 503 retryable 设备处于 stale/offline 或正在重连;需要异步交付时只对声明 mailbox/both 的命令显式选择 delivery,不要把任意失败自动入队
相同 deviceId 互相替换 每个实例应使用唯一、稳定 ID;旧 generation 不会完成新调用

正常停止 tb connect(或用 tb daemon uninstall 移除常驻服务、终止容器)即可关闭连接。节点会先进入 stale、连接拆除后转 offline,并由宿主按回收策略清理;不要假设断线立即永久删除树记录。

Terminal window
docker stop tb-device
tb device ls

紧急吊销时先禁用或删除该 Device SK,阻止重连;再由管理员通过当前实例的 registry 管理面清理遗留节点。清理命令和权限以 tb help system/registry 为准,不要使用不存在的 tb device rm。

  • 只暴露文件:优先 --no-shell --fs ... --fs-readonly;
  • 能力已有公网 HTTPS/MCP:改用MCP 挂载;
  • 需要嵌入业务进程:阅读嵌入现有应用,并核对目标版本类型定义;
  • 需要设备离线后继续接受结构化任务:阅读Device 离线投递与 Mailbox;
  • 生产 Sidecar 的镜像、架构和 CA 证书要求以当前 tb CLI 容器文档与 tb connect --help 为准;
  • ready、WebSocket 或 stale/offline presence 状态异常:进入故障排查与升级。