chaitin/OctoBus

GitHub: chaitin/OctoBus

OctoBus 是一个本地运行的安全网关,用于管理可插拔的 Node.js 服务包,并通过 capset 机制向 AI agent 精确暴露经过审批的 gRPC 能力。

Stars: 178 | Forks: 83

OctoBus

ci contributing security

[中文版 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绕过, 自定义脚本, 请求拦截