diff --git a/.github/workflows/publish-agent.yml b/.github/workflows/publish-agent.yml new file mode 100644 index 0000000..3ac6b3e --- /dev/null +++ b/.github/workflows/publish-agent.yml @@ -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 diff --git a/README.md b/README.md index 20848b5..db1bd6a 100644 --- a/README.md +++ b/README.md @@ -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//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 @@ -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) | 包 | 职责 | diff --git a/llmdoc/architecture/modules-and-boundaries.md b/llmdoc/architecture/modules-and-boundaries.md index e160f2e..d4d50fa 100644 --- a/llmdoc/architecture/modules-and-boundaries.md +++ b/llmdoc/architecture/modules-and-boundaries.md @@ -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) | ## 依赖方向要点 diff --git a/llmdoc/guides/k8s-device-sidecar.md b/llmdoc/guides/k8s-device-sidecar.md new file mode 100644 index 0000000..eacd1c2 --- /dev/null +++ b/llmdoc/guides/k8s-device-sidecar.md @@ -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/`,进程活着 = 节点在树上,进程退出 = 网关回收(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 -c tb-agent` 应有 `connected -> device/`。 +- **401/403** → SK 被网关拒或 scope 不含 `register`;用 `tb sk create ... --register-path device/` 重签。 +- **节点残留** → device-id 用了固定值且旧 pod 未优雅退出;改用 `$(POD_NAME)` 或切原生 sidecar。 diff --git a/packages/cli/Dockerfile b/packages/cli/Dockerfile new file mode 100644 index 0000000..3645b2e --- /dev/null +++ b/packages/cli/Dockerfile @@ -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"] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73417ce..7da84d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: devDependencies: - '@biomejs/biome': - specifier: ^2.5.2 - version: 2.5.2 '@eslint/js': specifier: ^9.39.5 version: 9.39.5 @@ -435,63 +432,6 @@ packages: resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} engines: {node: '>=6.9.0'} - '@biomejs/biome@2.5.2': - resolution: {integrity: sha512-VQ3RCqr7JmDIX+w6stWYl+g/3bYofN3q2wDBHUKKc/c7i5QWrFKFBZYCYPWTE6agsUPMIZZe6/CMmVUfUAhkKA==} - engines: {node: '>=14.21.3'} - hasBin: true - - '@biomejs/cli-darwin-arm64@2.5.2': - resolution: {integrity: sha512-e7P3P7EkwFc/KiX2AHw4YDLIBOMfG9CPCAwy52k5Bp0dfhkozx9hf6wCmIr2QeXy2XeccJ3V/Sg+hDmzYEqxSg==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [darwin] - - '@biomejs/cli-darwin-x64@2.5.2': - resolution: {integrity: sha512-ymzMvjC1Jg0b9K0D26ZdARqFQXs7MocfLC5FOCGfkC0Ss+ACUJkX5364ZM5nT4NLZanHRZNVrZEy+Ibwcvux/g==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [darwin] - - '@biomejs/cli-linux-arm64-musl@2.5.2': - resolution: {integrity: sha512-w+ANG0ZvTu9IeEg9QnstoOnk6L0fpwJifW6aHR18+cb5Z39bkANItYjAfMrnvce5tmMK+IQ6nPX7/kQFdam5iw==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@biomejs/cli-linux-arm64@2.5.2': - resolution: {integrity: sha512-t7sseOmqND57uUWTwlawU6BYj+J06T/9EkydzBhkrgw/FK3QVhjU2wsJR0frljrKZ0/I8A/rYw7284QgqjQfIQ==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@biomejs/cli-linux-x64-musl@2.5.2': - resolution: {integrity: sha512-VArNLAzND063tF+XY0yPyM+DyahpzOMzOAvb7qs259nhjJWRjvjZdssuA+Rfl+l07+NOesKZ0Xu2yFrXyBMtzw==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - libc: [musl] - - '@biomejs/cli-linux-x64@2.5.2': - resolution: {integrity: sha512-M/lOZrewzTCRDINbjhQ1gYYru37KlD3kJBQwwKCG0ckz5E9IZwIoJ3X0wBwRXA+yBDIwWUuPBHS67HzJY4dTfA==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@biomejs/cli-win32-arm64@2.5.2': - resolution: {integrity: sha512-kbjFFKyZlzYnAuw7sRy5qDoFG6zrP40UK08oPQsWK0ct3NMnGSt+Bs1iviEEyEIP57N5MrykGXdO/wRiaR4lww==} - engines: {node: '>=14.21.3'} - cpu: [arm64] - os: [win32] - - '@biomejs/cli-win32-x64@2.5.2': - resolution: {integrity: sha512-4InchVpdVmdkkkgjQqKpgvyu+VPnoF/7RPSw5YATgEVpt2j72wcCAeV5TwaE9ZGJUZWZn7v2CwSAj6CrMJEx8A==} - engines: {node: '>=14.21.3'} - cpu: [x64] - os: [win32] - '@cfworker/json-schema@4.1.1': resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} @@ -5335,41 +5275,6 @@ snapshots: '@babel/helper-string-parser': 7.29.7 '@babel/helper-validator-identifier': 7.29.7 - '@biomejs/biome@2.5.2': - optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.5.2 - '@biomejs/cli-darwin-x64': 2.5.2 - '@biomejs/cli-linux-arm64': 2.5.2 - '@biomejs/cli-linux-arm64-musl': 2.5.2 - '@biomejs/cli-linux-x64': 2.5.2 - '@biomejs/cli-linux-x64-musl': 2.5.2 - '@biomejs/cli-win32-arm64': 2.5.2 - '@biomejs/cli-win32-x64': 2.5.2 - - '@biomejs/cli-darwin-arm64@2.5.2': - optional: true - - '@biomejs/cli-darwin-x64@2.5.2': - optional: true - - '@biomejs/cli-linux-arm64-musl@2.5.2': - optional: true - - '@biomejs/cli-linux-arm64@2.5.2': - optional: true - - '@biomejs/cli-linux-x64-musl@2.5.2': - optional: true - - '@biomejs/cli-linux-x64@2.5.2': - optional: true - - '@biomejs/cli-win32-arm64@2.5.2': - optional: true - - '@biomejs/cli-win32-x64@2.5.2': - optional: true - '@cfworker/json-schema@4.1.1': {} '@cloudflare/kv-asset-handler@0.5.0': {}