chaitin/OctoBus
GitHub: chaitin/OctoBus
OctoBus 是一个本地运行的安全网关,用于管理可插拔的 Node.js 服务包,并通过 capset 机制向 AI agent 精确暴露经过审批的 gRPC 能力。
Stars: 178 | Forks: 83
[中文版 README](README.zh-CN.md)
OctoBus 是一个本地运行的单二进制网关,用于管理可插拔的 Node.js 服务包,并按 capset 将这些包中的 gRPC 能力暴露给客户端或 agent。
当前的实现提供了一个用 Go 构建的 `octobus` 二进制文件,主要负责:
- daemon:启动本地控制面和公共数据面,并根据每个服务的 runtime 模式管理 Node.js 子进程
- CLI:通过本地 admin API 管理服务、实例和 capset
- gateway:将选定的方法作为 gRPC 暴露,并将 unary 方法作为 Connect RPC 和 MCP streamable HTTP 暴露
- storage:使用 SQLite 记录服务、实例、capset、方法绑定、描述符和运行时状态
- runtime management:导入服务包,准备 runtime 目录,并管理长期运行或按需调用的 Node.js 实例
## 项目概述
OctoBus 基于以下核心模型构建:
- **service**:可导入的 Node.js 包内的服务根。它包含 `service.json`、proto 文件和一个 gRPC 实现。单个分发包可以通过 `//service-dir` 暴露多个服务根。
- **instance**:服务的一个 runtime 实例,具有独立的配置和工作目录。长期运行的实例还具有日志和本地监听端口。
- **capset**:面向 agent 或特定用例的确定性能力集合,由 `capset -> service -> instance -> method` 绑定组成。
- **method binding**:在 capset 中实际选择并暴露的 gRPC 方法。Unary 方法可以通过 gRPC、Connect RPC 和 MCP 调用。Streaming 方法仅支持长期运行服务的 gRPC 调用,不可通过 Connect RPC、MCP 或按需调用路径使用。
默认情况下,daemon 监听 `127.0.0.1:9000` 端口。admin API、gRPC、Connect RPC、MCP 和 reflection 都通过该端口进行分发。你可以使用 `--addr` 显式绑定到另一个地址,例如 `0.0.0.0:9000`;在远程暴露 OctoBus 时,你需要自行负责网络访问控制。CLI 默认通过 admin API 执行管理操作,并不直接写入 SQLite。
服务包默认使用 `long-running` runtime 模式:在创建或启动实例后,OctoBus 会启动一个常驻的 Node.js gRPC 子进程。包也可以在 `service.json` 中声明 `"runtime":{"mode":"on-demand"}`:此类实例不会被预先启动,也不存储 PID 或监听地址。对于每个传入的请求,OctoBus 会启动一个短暂的 `invoke` 子进程。
## 启动 Daemon
### 从 npm 安装
OctoBus 已发布为 `@chaitin-ai/octobus` npm 包。主包会安装一个小型的 Node.js 启动器,并通过特定平台的可选依赖项(例如 `@chaitin-ai/octobus-linux-x64`)拉取匹配的原生 Go 二进制文件。
```
npm install -g @chaitin-ai/octobus
octobus serve
```
你也可以在不进行全局安装的情况下运行它:
```
npx @chaitin-ai/octobus serve
```
npm 包仅安装 `octobus` 二进制文件。正常的服务导入和 runtime 流程仍需依赖 `node`、`npm`、`protoc` 和 `git`,具体说明见下文。
### 使用 Docker 运行
Docker 镜像包含 `octobus` 二进制文件以及正常服务导入和实例启动流程所需的 runtime 依赖。
```
docker run --rm \
-p 9000:9000 \
-v octobus-data:/var/lib/octobus \
ghcr.io/chaitin/octobus:latest
```
容器默认监听 `0.0.0.0:9000`,并将 daemon 状态存储在 `/var/lib/octobus` 下。
### 从源码构建
首次检出代码后,构建二进制文件:
```
task build
```
使用默认配置启动:
```
./bin/octobus serve
```
常用选项:
```
./bin/octobus serve \
--data-dir .octobus \
--addr 127.0.0.1:9000
```
你也可以通过环境变量覆盖默认值:
```
export OCTOBUS_DATA_DIR="./.octobus"
export OCTOBUS_ADDR="127.0.0.1:9000"
```
数据目录存储 SQLite 数据库、服务 artifacts 和 runtime、实例配置以及日志。默认的数据目录是启动 daemon 命令时当前目录下的 `.octobus`。
### 依赖项
要在本地运行 daemon 并执行正常的服务导入/启动流程,请安装以下命令行工具并确保它们在 `PATH` 中可用:
- `node`:运行导入的 Node.js 服务包;其版本必须满足包自身的要求
- `npm`:在服务导入期间获取 npm 包,并在 runtime 目录中安装生产环境依赖
- `protoc`:在服务导入期间编译 proto 描述符
- `git`:获取并打包通过 HTTPS Git 源导入的包
如果在下载 Go 模块时执行 `go build` 或 `task build` 因超时而失败(例如来自 `proxy.golang.org` 的 `dial tcp ... i/o timeout`),你可能需要配置 Go 模块代理:
```
go env -w GOPROXY=https://goproxy.cn,direct
```
## 基本工作流
以下示例使用内置的 calculator 服务运行一个完整的工作流。在开始之前,请构建二进制文件,按上述方式启动 daemon,并验证 CLI 能否连接:
```
./bin/octobus status
```
如果 daemon 未在默认地址运行,请通过全局选项或环境变量指定。本地 daemon 默认使用 HTTP/h2c,地址可以是纯 `host:port` 或 `http://host:port`:
```
./bin/octobus --addr 127.0.0.1:19001 status
OCTOBUS_ADDR=http://127.0.0.1:19001 ./bin/octobus service list
```
仅当 OctoBus 被远程暴露且 TLS 由外部代理提供时,才使用 `https://host:port` 格式。
你还可以运行纯净检出环境的冒烟测试脚本,测试本地 calculator 的正常流程。该任务会清理生成的 artifacts,重新构建二进制文件和本地 SDK,安装 calculator 示例依赖,启动一个临时 daemon,导入服务,创建实例和 capset,并调用 Connect RPC 以断言响应结果为 `result: 42`:
```
task example:clean-checkout-smoke
```
导入示例服务包:
```
./bin/octobus service import calculator ./examples/calculator-js
```
第一个位置参数 `calculator` 是本地 OctoBus 服务 ID,为必填项。`--name` 是可选的,用于覆盖显示名称。当省略 `--name` 时,首次导入会使用 `service.json` 中的 `displayName`,如果没有 `displayName` 则使用 `name`。在不带 `--name` 的情况下重新导入相同的服务 ID,会保留现有的显示名称。
当 `SOURCE` 路径即使存在于 CLI 机器上,也应该由 daemon 文件系统解析时,请使用 `--source-mode remote`。使用 `--source-mode upload` 可强制客户端上传,如果源不是现有的受支持的本地源,则会在发出 admin 请求之前失败。
创建并启动实例:
```
./bin/octobus instance create \
calculator-test \
--service calculator \
--config-json '{"label":"primary"}' \
--secret-json '{"apiToken":"dev-token"}'
```
创建 capset 并暴露实例中的方法:
```
./bin/octobus capset create dev --name DevAgent
./bin/octobus capset add-instance \
dev \
calculator-test
```
查看 capset 目录并确认该方法已暴露:
```
./bin/octobus catalog dev --all --json
```
通过 Connect RPC 调用 calculator:
```
curl -X POST \
http://127.0.0.1:9000/capsets/dev/connect/calculator-test/calculator.v1.CalculatorService/Add \
-H 'Content-Type: application/json' \
-d '{"left":20,"right":22}'
```
补充说明:
- 除了客户端本地目录外,`service import` 还支持客户端本地和远程的 HTTP(S) `.tgz` / `.tar.gz` / `.zip` 归档文件、`npm:` 源以及 HTTPS Git 源。除远程 HTTP(S) 归档 URL 外,其他包源均可在后面追加 `//service-dir` 以选择分发包内的服务根,例如 `npm:@scope/tentacle@1.0.0//Hanqing_Ticket` 或 `https://github.com/acme/tentacle.git//Hanqing_Ticket@v1.0.0`。远程归档 URL 将包根目录作为服务根;如需导入多服务归档,请使用递归导入。有关源传输模式、离线导入、强制重装依赖等选项,请参见 `./bin/octobus service import --help`。
- 使用 `service import --recursive SOURCE` 可导入在多服务分发包中发现的所有服务根,例如 `./bin/octobus service import --recursive npm:@chaitin-ai/octobus-tentacles`。在递归模式下,`SOURCE//some-dir` 会将发现范围限制在该扫描根目录下,但依然会使用各自 `service.json.name` 中的 ID 导入发现的每个服务。
- `instance` 支持 `list/get/update/delete/update-config/update-secret/start/stop/restart`。对于 `long-running` 服务,`create` 默认会启动实例。配置可来自 `--config`、`--config-json` 或 stdin;secrets 可来自 `--secret`、`--secret-json` 或 stdin。
- `on-demand` 实例会保持逻辑上的 `enabled/running` 状态,但 `start/stop/restart` 以及带 `--restart` 的配置更新会返回错误,因为该 runtime 模式不支持持久的 runtime 控制。
- `capset` 支持 `list/get/update/delete/add-instance/remove-instance`。你还可以使用 `select-method` / `unselect-method` 进行精确的方法暴露控制。`add-token/list-tokens/remove-token` 用于管理访问 token。
- `capset add-instance` 接受两个位置参数:capset ID 和实例 ID。系统会从实例记录中查找服务。默认情况下,此命令会选择所有方法,并在执行时静态展开当前所有的服务方法。使用 `--no-all-methods` 可稍后使用 `select-method` 选择方法。gRPC 目录包含选定的 unary 和 streaming 方法;Connect RPC、MCP 和 OpenAPI 仅包含 unary 方法。后续服务更新添加的方法不会自动暴露给现有的 capset。
有关更多调用方式,请参见下一节。各命令的详细说明可通过每个子命令的 `--help` 查看。
## 调用暴露的能力
获取 capset 目录:
```
curl 'http://127.0.0.1:9000/admin/v1/catalog/dev?all=true'
```
目录会按协议返回每个方法的信息,包括 runtime 模式、后端状态、gRPC metadata、Connect RPC endpoint、MCP tool 名称、描述符哈希/版本以及请求/响应消息名称。默认情况下,仅返回 gRPC 目录。使用 `grpc=true`、`connect=true`、`mcp=true` 或 `all=true` 查询参数可选择协议,或运行 `./bin/octobus catalog --help` 查看 CLI 选项。
默认情况下,capset 不需要访问 token。当未添加任何 token 时,capset 下的 Connect RPC、MCP、gRPC、reflection 和公共 OpenAPI endpoint 将保持公开访问。添加一个或多个 token 后,这些公共资源将需要有效的凭据:HTTP/Connect/MCP/OpenAPI 使用 `Authorization: Bearer
`,而 gRPC 和 reflection 使用同名 metadata。Token 密钥仅在创建时提交。OctoBus 仅持久化验证哈希,不存储明文 token。
```
printf '%s' 'dev-secret' | ./bin/octobus capset add-token dev local --token-stdin
./bin/octobus capset list-tokens dev
./bin/octobus capset remove-token dev local
```
### gRPC
gRPC 调用会保留原始的方法路径,并通过 metadata 指定路由目标:
```
grpcurl -plaintext \
-H 'x-octobus-capset: dev' \
-H 'x-octobus-instance: gitlab-test' \
-d '{"projectId":"p1"}' \
127.0.0.1:9000 \
gitlab.MergeRequestService/List
```
### Connect RPC
Connect RPC endpoint 为:
```
POST /capsets/{capset_id}/connect/{instance_id}/{full_service}/{method}
```
示例:
```
curl -X POST \
http://127.0.0.1:9000/capsets/dev/connect/gitlab-test/gitlab.MergeRequestService/List \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer dev-secret' \
-H 'x-octobus-ext-business-request-id: req-1' \
-d '{"projectId":"p1"}'
```
Connect RPC 使用 protobuf JSON 映射,拒绝未知字段,并默认在响应中忽略零值。字段级别的 schema 可通过 capset OpenAPI endpoint 获取:
```
curl http://127.0.0.1:9000/capsets/dev/openapi.json
curl http://127.0.0.1:9000/capsets/dev/openapi.yaml
curl http://127.0.0.1:9000/admin/v1/catalog/dev/openapi.json
curl http://127.0.0.1:9000/admin/v1/catalog/dev/openapi.yaml
```
### MCP
MCP streamable HTTP endpoint 为:
```
POST /capsets/{capset_id}/mcp
```
列出工具:
```
curl -X POST http://127.0.0.1:9000/capsets/dev/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
```
调用工具:
```
curl -X POST http://127.0.0.1:9000/capsets/dev/mcp \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"gitlab__gitlab-test__list","arguments":{"projectId":"p1"}}}'
```
默认的工具名称由 `{service}__{instance}__{method}` 生成。如果存在冲突,请在运行 `capset select-method` 时使用 `--mcp-tool` 显式指定。
### gRPC Reflection
OctoBus 直接使用导入期间归档的描述符提供 gRPC reflection,而不是将 reflection 代理到 Node 实例。Reflection 请求必须包含 `x-octobus-capset`,且响应仅限于该 capset 中暴露的方法所需的描述符闭包。
```
grpcurl -plaintext \
-H 'x-octobus-capset: dev' \
127.0.0.1:9000 \
list
```
## 查看访问日志
capset 的公共协议访问会被写入数据目录下的 `access.log` 文件中。该文件为 NDJSON 格式,权限为 `0600`。它会记录协议、capset、服务、实例、方法/工具、路由、状态码、持续时间、远程地址和 User Agent。它不会记录请求体、响应体、Authorization、token、secret 或业务元数据。
通过 CLI 查看日志:
```
./bin/octobus logs
./bin/octobus logs --capset dev --instance calculator-test
./bin/octobus logs --service calculator --limit 1000
./bin/octobus logs --capset dev --tail 0 --follow
```
## 开发服务包
一个服务包至少必须包含:
```
my-service/
package.json
service.json
proto/
service.proto
dist/
index.js
```
单个 npm 分发包也可以包含多个服务根。在这种情况下,根目录的 `package.json` 是依赖安装、发布和 runtime entry 的唯一事实来源,而每个服务根子目录提供各自的 `service.json`、proto 和 schema。在导入单个服务期间,在源地址后追加 `//service-dir` 可选择目标服务根。如果没有该后缀,则根目录本身即为服务根。使用 `octobus service import --recursive SOURCE` 可在一条命令中发现并导入所有服务根;在递归模式下,`SOURCE//some-dir` 是用于发现的扫描根目录。
`service.json` 示例:
```
{
"schema": "chaitin.octobus.service.v1",
"name": "gitlab-wrapper",
"displayName": "GitLab Wrapper",
"description": "GitLab API wrapper service",
"runtime": {
"mode": "long-running"
},
"proto": {
"roots": ["proto"],
"files": ["proto/gitlab.proto"]
},
"configSchema": "config.schema.json",
"secretSchema": "secret.schema.json"
}
```
必填字段:
- `schema`
- `name`
- `proto.roots`
- `proto.files`
`name` 是在包内声明的名称,而不是 OctoBus 服务 ID。`service.json` 不得声明顶级的 `id` 或 `entry` 字段。Runtime entry 必须由分发包根目录的 `package.json bin` 提供:单入口包可以使用字符串或单入口对象,而多服务包必须使 `service.json.name` 与根 `bin` 对象中的某个键匹配。`runtime.mode` 是可选的,支持 `long-running 和 `on-demand`;如果省略,则等同于 `long-running`。如果提供了 `configSchema`,则在创建或更新实例配置时会执行 JSON Schema 验证。如果提供了 `secretSchema`,则在创建或更新实例 secrets 时会执行 JSON Schema 验证。
当 `long-running` 实例启动时,OctoBus 会从 runtime 目录执行解析出的 `node_entry`,并传递固定的参数:
```
--runtime serve --host 127.0.0.1 --port --config --secret --workdir --service --instance
```
服务进程必须启动 gRPC server 并实现标准的 gRPC health check。
```
--runtime invoke --method --config --secret --metadata --workdir --service --instance
```
在本地运行服务 entry 时,请使用 `OCTOBUS_SERVICE_CONTEXT` 将默认的 config/secret 注入到业务 CLI 中,并加上 `--runtime dev`:
```
OCTOBUS_SERVICE_CONTEXT='{"config":{"baseUrl":"https://example.com"},"secret":{"token":"dev-token"}}' \
node bin/service.js call --data-json '{"id":"123"}'
```
## 开发说明
### 架构
```
Client / Agent
-> OctoBus Go binary
-> public HTTP/2 h2c server
-> gRPC gateway
-> Connect RPC adapter
-> MCP adapter
-> reflection server
-> localhost admin API
-> SQLite store
-> descriptor loader
-> Node supervisor
-> Node.js gRPC instance processes
-> on-demand invoke subprocesses
```
主要代码目录:
- `cmd/octobus`:程序入口点、根命令、`serve` 命令以及 daemon 装配
- `internal/cli`:Cobra CLI;所有管理命令均调用本地 admin API
- `internal/admin`:本地 admin HTTP API
- `internal/packageimport`:服务包获取、解压、runtime 准备以及描述符编译
- `internal/supervisor`:实例配置写入、Node 子进程的启动/停止/恢复、健康检查以及日志
- `internal/store`:SQLite schema、迁移以及领域对象的读取/写入
- `internal/protocol`:gRPC 代理、Connect RPC、MCP、目录、OpenAPI 以及 reflection
- `internal/descriptors`:proto 描述符编译、加载以及方法 metadata 解析
- `sdk`:`@chaitin-ai/octobus-sdk` 的 TypeScript 源码、测试和构建 artifacts
- `examples/calculator-js`:long-running JavaScript calculator 服务示例
- `examples/calculator-on-demand-js`:on-demand JavaScript calculator 服务示例
- `tests/e2e`:端到端测试
- `docs/design`:设计文档和目标
Runtime 数据的布局大致如下:
```
{data_dir}/
octobus.db
artifacts/services/{service_id}/
.tgz or package.zip
package/
runtime/
descriptor.protoset
instances/{instance_id}/
config.json
secret.json
stdout.log
stderr.log
tmp/
```
当 daemon 重启时,它会从 SQLite 中恢复 `enabled=true` 且 `runtime_mode=long-running` 的实例,并重新启动相应的 Node.js 子进程。`on-demand` 实例不会被预先启动;后续请求会通过 `invoke` 调用它们。
### 环境要求
- Go:项目的 `go.mod` 声明了 `go 1.26.1`
- Task:`Taskfile.yml` 用于管理构建、检查和测试入口
- Node.js / npm:导入和运行 Node.js 服务包时必需
- `protoc`:在服务导入期间编译 proto 描述符以及运行 e2e 测试时必需
- `git`:从 HTTPS Git 源导入服务时以及部分测试中必需
### 构建与测试
本项目使用 `Taskfile.yml` 来管理 lint、测试和构建阶段。运行所有阶段:
```
task all
```
你也可以运行各个单独的阶段:
```
task # list available tasks
task lint
task test
task build
```
`task test` 会首先构建本地 SDK,并为 long-running 和 on-demand calculator 示例安装依赖,然后运行具有跨包覆盖率的 Go 测试,包括 `tests/e2e`。`task build` 会生成 `bin/octobus` 并为 `version` 子命令注入构建 metadata。如果当前提交刚好位于与 `v[0-9]*` 匹配的 OctoBus 发布 tag 上,则该 tag 将用作显示的版本号。否则,版本号将来自最近可达的匹配 tag 加上提交距离和简短提交哈希,例如 `v1.2.0-12-gabc1234`;如果没有可达的匹配 tag,则回退到简短的 Git 提交哈希。没有 Git metadata 的构建环境可以使用 `OCTOBUS_VERSION`、`OCTOBUS_COMMIT` 和 `OCTOBUS_BUILD_DATE` 覆盖注入的值。你可以使用以下命令检查结果:
```
./bin/octobus version
```
端到端测试也可以单独运行:
```
go test ./tests/e2e -count=1
```
端到端测试会构建真实的 `octobus` 二进制文件,启动真实的 daemon,通过 CLI 调用 admin API,然后验证 gRPC、Connect RPC、MCP、OpenAPI 和 reflection endpoint。
默认的 GitHub Actions CI 是一个轻量级的验证流程:它会检查公共追踪记录、Go 格式和 vet,运行 `go test ./cmd/... ./internal/...`,构建二进制文件,检查 OctoBus 的 npm 二进制包,并在 `sdk` 下运行 npm test/build/pack 空运行。完整的 `task test` 和 e2e 依然是本地的质量门禁。OctoBus 二进制包的发布仅由 `v` tag 推送构建触发,且 tag 版本必须与 `npm/octobus/package.json.version` 匹配。SDK 的发布仅由 `sdk-v` tag 推送构建触发,且 tag 版本必须与 `sdk/package.json.version` 匹配。这两条 npm 发布流程都需要仓库密钥 `NPM_TOKEN`。
## 安全与合规
如果你认为此仓库包含安全漏洞、侵权内容或其他合规风险,请私下将其报告至 `octobus-opensource@chaitin.com`。
有关完整的报告流程和所需的详细信息,请参阅 [SECURITY.md](SECURITY.md)。标签:API网关, GNU通用公共许可证, Go, MITM代理, Node.js, Python工具, Ruby工具, 人工智能, 工具调用, 日志审计, 暗色界面, 本地网关, 用户模式Hook绕过, 自定义脚本, 请求拦截