Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/workflows/publish-agent.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# GHCR 发布 tb-agent 边车镜像:与 publish-cli.yml 同 tag 触发(版本单一真源 = CLI package.json)。
# 用户直接 `docker run ghcr.io/tokenrollai/tool-bridge/tb-agent connect ...`,或作 k8s sidecar。
# 一次性前置:首发后在 GitHub Packages 设置里把该镜像可见性设为 public。

name: publish-agent

on:
push:
tags:
- 'cli-v*' # 打 tag 即发布:git tag cli-v0.7.0 && git push origin cli-v0.7.0
workflow_dispatch:

permissions:
contents: read
packages: write # 推 GHCR(GITHUB_TOKEN)

jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

# tag 版本与 package.json 一致性校验(与 publish-cli.yml 同规则)
- name: Check tag matches package version
if: github.ref_type == 'tag'
run: |
TAG_VERSION="${GITHUB_REF_NAME#cli-v}"
PKG_VERSION="$(node -p "require('./packages/cli/package.json').version")"
if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
echo "tag cli-v$TAG_VERSION != package.json $PKG_VERSION" >&2
exit 1
fi

- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3

- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}/tb-agent
tags: |
type=match,pattern=cli-v(.*),group=1
type=raw,value=latest,enable=${{ github.ref_type == 'tag' }}

- uses: docker/build-push-action@v6
with:
context: .
file: packages/cli/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ tb ctx search ctx/docs hi
tb connect --allow 'echo' --allow 'uname' --fs ~/shared # 长驻;shell 白名单 + fs 暴露
tb device ls # 另一终端:看设备在线状态
tb call device/<id>/shell --tool exec --args '{"command":"echo hi"}'
# k8s pod 部署即注册:用 tb-agent 边车镜像,见 llmdoc/guides/k8s-device-sidecar.md

# ── 联邦另一个 HTBP 服务 ─────────────────────────────
tb server add fed/team-b --base-url https://tb.team-b.example.com --sk-ref team-b-sk
Expand Down Expand Up @@ -216,6 +217,10 @@ tb login && tb status --json

同一套核心经 Node 宿主(SQLite + 本地 FS)以单容器运行、`/data` 卷持久化——宿主中立装配面(SDK 同款)已就绪,镜像在路线图中。也可以现在就用 SDK + `@hono/node-server` 自行拉起一个 Node 实例(见上文 SDK 一节)。

### k8s 设备边车

要让 ACK / k8s 里的 pod **部署即被发现**,用 tb-agent 边车镜像(`ghcr.io/tokenrollai/tool-bridge/tb-agent`,与 CLI 同源同版本)常驻 `tb connect`,把 pod 反向挂到 `device/` 下。完整原理、受限 SK 签发与可 apply 的清单见 [k8s-device-sidecar 指南](llmdoc/guides/k8s-device-sidecar.md)。

## 仓库结构(pnpm monorepo)

| 包 | 职责 |
Expand Down
1 change: 1 addition & 0 deletions llmdoc/architecture/modules-and-boundaries.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
| Plugin System | 自定义 Provider 注册与生命周期(探活/契约校验/信封传输) | `plugin/` | gateway `providers/pluginClient|pluginTool|pluginContext` + builtin `system/plugin`;首个 in-repo plugin 参考实现:`packages/plugin-feishu`(CF Worker,飞书 TAT 自动换发) |
| Dashboard | `~help` 通用渲染器 + 管理表单,**无专用后端** | — | `packages/dashboard`(React SPA)经 gateway Static Assets 挂 `/ui` |
| 部署 | CF 与 Docker 两条路径产出同一棵树 | — | CF:`scripts/provision.mjs` + wrangler;Docker/Node:`packages/server`(SQLite/FS/ws DeviceHub)+ 根 Dockerfile,见 [../guides/docker-host.md](../guides/docker-host.md) |
| 设备边车 | pod 部署即反向注册到 `device/`:常驻 `tb connect` 的 tb-agent 镜像(k8s sidecar) | — | `packages/cli/Dockerfile` → `ghcr.io/tokenrollai/tool-bridge/tb-agent`(与 CLI 同源同版本),见 [../guides/k8s-device-sidecar.md](../guides/k8s-device-sidecar.md) |

## 依赖方向要点

Expand Down
92 changes: 92 additions & 0 deletions llmdoc/guides/k8s-device-sidecar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Guide:k8s 设备边车(k8s-device-sidecar)

> 用途:把阿里云 ACK / 任意 k8s 里的 pod,在部署时反向注册到 HTBP 树的 `device/` 下,部署即被 tool-bridge 发现。镜像见 [`packages/cli/Dockerfile`](../../packages/cli/Dockerfile),发布见 [`.github/workflows/publish-agent.yml`](../../.github/workflows/publish-agent.yml)。设备协议本身见 [docker-host.md](./docker-host.md) 的「设备断线回收」段。

## 一句话原理

`tb login` 只把凭证写进本地 profile,**不注册任何设备**;`tb connect` 才开一条常驻 WebSocket(`/system/device/ws`)把本机 shell/fs 挂到 `device/<device-id>`,进程活着 = 节点在树上,进程退出 = 网关回收(DO alarm / `sweepOrphans`)。所以让 pod「部署即被发现」= 在 pod 里常驻一个 `tb connect`。

k8s 里的干净做法:**tb-agent 边车容器**,与业务容器同 pod、独立进程跑 `tb connect`,业务容器零改动。

## 镜像

`ghcr.io/tokenrollai/tool-bridge/tb-agent`(多阶段从 monorepo 构建,CLI 版本 == 网关版本)。

```bash
docker build -f packages/cli/Dockerfile -t tb-agent .
docker run --rm -e TB_BASE_URL=https://tool-bridge.example.com -e TB_SK=tbk_... \
tb-agent connect --device-id demo --allow echo
```

`ENTRYPOINT=["tini","--","tb"]`、`CMD=["--help"]`:裸跑打印帮助;实际用 `args`/命令行覆盖成 `connect ...`。tini 作 PID1 转发 SIGTERM(触发 WS 优雅关闭,节点即时下线)并回收 shell 子进程僵尸。

## 三条铁律(否则踩坑)

1. **别把 admin SK 塞进 pod。** 签一个只能注册到某前缀的受限 SK,泄漏也越不了界(`registerPaths`):
```bash
tb sk create --owner device:myapp \
--scope 'device/myapp/**:read,call' \
--register-path device/myapp
```
2. **device-id 用 Downward API 注入 pod 名。** `tb connect` 默认拿 `os.hostname()` 当 id;多副本时 pod 名唯一且随部署变化——pod 死 → WS 断 → 网关自动回收孤儿节点,不留僵尸。要固定路径(单实例)就显式 `--path device/myapp`。
3. **凭证走 env**,`connect` 读 `TB_BASE_URL`/`TB_SK`,无需 `tb login`。

## 可直接 apply 的清单

```yaml
apiVersion: v1
kind: Secret
metadata:
name: tool-bridge
type: Opaque
stringData:
TB_BASE_URL: "https://tool-bridge.example.com"
TB_SK: "tbk_..." # 上面 tb sk create 生成的受限 SK
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
replicas: 2
selector: { matchLabels: { app: myapp } }
template:
metadata: { labels: { app: myapp } }
spec:
containers:
# ── 业务容器,原样不动 ──
- name: app
image: your-app:latest

# ── 原生 sidecar(k8s 1.29+):先于业务就绪、晚于业务终止,优雅下线 ──
- name: tb-agent
image: ghcr.io/tokenrollai/tool-bridge/tb-agent:latest
args:
- "connect"
- "--device-id"
- "$(POD_NAME)" # 每 pod 唯一,自动回收
- "--no-shell" # 纯 fs 暴露示例;要 shell 见下
- "--fs"
- "/data"
- "--fs-readonly"
# 暴露 shell:删掉 --no-shell,换白名单(默认拒绝一切):
# - "--allow"
# - "git"
env:
- name: POD_NAME
valueFrom: { fieldRef: { fieldPath: metadata.name } }
envFrom:
- secretRef: { name: tool-bridge }
volumeMounts:
- { name: data, mountPath: /data }
volumes:
- { name: data, emptyDir: {} }
```

> **原生 sidecar(推荐,k8s ≥1.29)**:把 `tb-agent` 移到 `initContainers` 并加 `restartPolicy: Always`,它会先于业务容器就绪、晚于业务容器终止,SIGTERM 时 WS 正常关闭、节点即时下线,而非等网关 alarm 回收。放在 `containers` 下(如上)则是普通边车,行为也可用,只是终止顺序不保证。

## 排障

- **UI 里看不到节点** → 确认边车在跑 `connect` 而非只 `login`;`kubectl logs <pod> -c tb-agent` 应有 `connected <id> -> device/<id>`。
- **401/403** → SK 被网关拒或 scope 不含 `register`;用 `tb sk create ... --register-path device/<prefix>` 重签。
- **节点残留** → device-id 用了固定值且旧 pod 未优雅退出;改用 `$(POD_NAME)` 或切原生 sidecar。
38 changes: 38 additions & 0 deletions packages/cli/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# tb-agent:常驻 `tb connect` 的边车(sidecar)镜像 —— 把内网/集群里的一台机器或一个 pod
# 反向挂到 HTBP 树的 device/ 下,部署即被发现。与根 Dockerfile 同源(从 monorepo 构建),
# 保证 agent 的 CLI 版本 == 网关版本,不依赖 npm 发布时序。
#
# 单文件 ESM bin:tsup 已把 @tool-bridge/core 内联,仅 commander/partysocket/ws/marked*
# 5 个运行时依赖留 external,故用 `pnpm deploy --prod` 隔出最小 node_modules 进 slim。
# `tb connect` 自身处理 SIGTERM/SIGINT 优雅收尾(见 deviceRuntime.ts),配 tini 做 PID1
# 转发信号 + 回收 shell 子进程僵尸。
#
# 用法:
# docker build -f packages/cli/Dockerfile -t tb-agent .
# docker run --rm -e TB_BASE_URL=https://tool-bridge.example.com -e TB_SK=tbk_... \
# tb-agent connect --device-id demo --allow echo
# k8s sidecar 清单见 llmdoc/guides/k8s-device-sidecar.md。

FROM node:22-bookworm AS build
WORKDIR /repo
RUN corepack enable
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml tsconfig.base.json ./
COPY packages ./packages
RUN pnpm install --frozen-lockfile
RUN pnpm --filter @tool-bridge/cli build
# 隔离 prod 部署:CLI 包(files=["dist"])+ 生产依赖 → /out(--legacy:workspace 依赖按 pack 规则复制)
RUN pnpm --filter @tool-bridge/cli --prod deploy --legacy /out

FROM node:22-bookworm-slim
ENV NODE_ENV=production
RUN apt-get update \
&& apt-get install -y --no-install-recommends tini \
&& rm -rf /var/lib/apt/lists/*
COPY --from=build /out /app
# 把 bin 暴露到 PATH:dist/index.js 带 shebang 且已可执行
RUN ln -s /app/dist/index.js /usr/local/bin/tb
USER node
# tini 作 PID1:转发 SIGTERM 给 tb(触发 WS 优雅关闭 → 节点即时下线)、回收 shell 子进程
ENTRYPOINT ["/usr/bin/tini", "--", "tb"]
# 裸跑打印帮助;k8s/compose 用 args 覆盖为 `connect --device-id ... --allow ...`
CMD ["--help"]
95 changes: 0 additions & 95 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.