# ADR-0002: 业务 = REST/SSE; gRPC = 观测面 - **Status**: Accepted (方案 A 修正版落地) - **Date**: 2026-04-26 - **Deciders**: PM (产品经理) + Flyto Agent core team - **Related code**: `platform/common/internal/server/`, `platform/common/cmd/common/main.go`, `platform/common/internal/api/grpc/proto/{health,safetychain}.proto`, `deploy/docker-compose.yml`, `deploy/Caddyfile` - **Related TODO**: `core/TODO.md` L692 (登记 + 完成同日, 2026-04-26); 衍生 tracked debt L693 / L694 / L695 - **Commit chain**: `01f08e7` (C1) → `8189d05` (C2) → `694bd07` (C3) → `e1db327` (C4) → `c35e761` (C5) → C6 (本 ADR) - **Cross-reference**: ADR-0001 (反向思维 gate); memory `feedback_architecture_principle_over_rule_of_two.md` (gRPC 优 HTTP 是 SafetyChain 通路铁律) --- ## 1. 背景 / Context `platform/common/internal/server/server.go` 早期写好了 1263 行业务 REST/SSE 实现 (SSE 流式 / CORS / Bearer auth / IP 限速 / 15s 心跳 / 会话 CRUD / panic recovery / request-id 中间件), 但 cmd/common/main.go 没启动它. 注释明说 "REST/SSE 仍由 internal/server 管理, 目前和本二进制正交; 未来 REST 侧需要同 一套 auth/tenant 接线时, 再并入这里." -- 即计划好了等连, 还没并入. PM 在 L569 反事实工作流缩水后提新方向: **消费者语言无关, 引擎自身无需为特殊 语言频繁优化**. 论据是: logistics platform 是 C#, 但未来可能加 Java/Python 等其他行业 platform. 如果业务通道走 gRPC, 每加一种语言就要 protoc gen + 维护 stub, 每个新行业要复制业务 RPC. 走 REST 则任何 HTTP 客户端通用. 但 9 天前 (2026-04-17) PM 拍板的 SafetyChain 方向是 "C# 走 gRPC 优于 HTTP" (memory `feedback_architecture_principle_over_rule_of_two.md` 记载). 这个 决定基于: SafetyChain 是 platform/common 内部观测面, 给 C# logistics 拉 verdict / breaker 状态, **是后台到后台的强类型 RPC**, gRPC 适配. 两个方向看似矛盾. 本 ADR 的核心工作就是阐明: 它们不冲突, 是 **bifurcation** (分层) 不是 **U-turn** (反悔). --- ## 2. 决策 / Decision **采纳方案 A 修正版** (激活 server.go + 改造接 OIDC/tenant/safetychain wire + 不加 grpc-gateway + 立 ADR), **拒绝原方案 A 完整版** (含 grpc-gateway 给观测 RPC 加 REST 镜像). ### 2.1 业务通道 = REST/SSE (`/api/v1/*` → common:8080) - 引擎消费层 (`engine.Run` / `engine.Session`) 唯一业务通道走 HTTP/1.1 + SSE - 端点形态 (server.go:286+ 已实现): `GET /api/v1/health` / `POST /api/v1/agent/run` (SSE 流式) / `POST /api/v1/sessions` / `GET|DELETE /api/v1/sessions/{id}` / `POST /api/v1/sessions/{id}/messages` / `POST /api/v1/sessions/{id}/permissions/{request_id}` - 鉴权: OIDC bearer token via `auth.HTTPMiddleware`, 与 admin HTTP 共用同一 套 Verifier (commit 2 把 raw shared-secret BearerToken 删除, 不再两套) - streaming: SSE `text/event-stream` (`text_delta` / `tool_use` / `done` 等 named events), W3C 标准, 浏览器 EventSource 原生支持 - 任何语言任何 HTTP 客户端都能消费, 不需要预先 codegen stub - C# logistics platform 业务调用走 HttpClient (Refit / RestSharp 强类型 绑定), 不需要写业务 gRPC stub ### 2.2 观测通道 = gRPC (`:9090`) - SafetyChainService (ListVerdicts / ListBreakerStates) + HealthService 仅 gRPC. 这两个是 9 天前 (2026-04-17) PM 拍板的内部强类型观测 RPC, 沿用 不变 - 不再加任何业务 gRPC RPC. C# logistics 后续业务调用走 REST/SSE - 观测的 HTTP 镜像由 admin/server.go 单独实现 (`/admin/safetychain/verdicts` + `/admin/safetychain/breakers`), 不引 grpc-gateway 重复一份 ### 2.3 Tool 级安全链装饰是行业 platform 责任 - cmd/common 起 REST listener 后**不**自动调 `safetychain.Assemble` 装饰 Tool. 一刀切 AlwaysApprove + DefaultExtractor 会把所有 Tool 锁同一组合 或 fan out 到不匹配 Tool 语义的 wrap. - common 保持纯 transport. verdictStore + breakerRegistry 接线就绪, 由行业 驱动代码 (logistics 等) 在 Tool 注册时调 `safetychain.Assemble` 装饰并 写数据. v0.3 阶段 verdictStore 仍然空 (C# logistics 的业务 Tool wire 还没起步). ### 2.4 端口拓扑 ``` cmd/common 进程 ├ :9090 gRPC — Health + SafetyChain (compose 内部, 不映射 host) ├ :8081 admin HTTP — /admin/health, /admin/version, /admin/tenant, │ /admin/safetychain/{verdicts,breakers} (opt-in) └ :8080 business REST — /api/v1/agent/run (SSE), /api/v1/sessions/*, /api/v1/health (新增, 由 --rest-addr 启用) ``` Caddy 边界: `hub.flytoex.net/api/v1/* → common:8080` (flush_interval -1 + response_header_timeout 0); `/admin/* → common:8081`; 其他 → logistics:8080. --- ## 3. 评估流程 / Evaluation Process 3-agent 并行 review (memory `feedback_agent_teams_review_mode.md` 模式), 三方 独立产出报告后主线程 reconcile, PM 二次拍板. ### 3.1 调研 agent (业界证据) 调研 11 个主流 LLM Agent / SaaS 框架的 transport 选型: | 框架 | 业务 transport | streaming | 双通道? | |---|---|---|---| | Anthropic Messages | REST | SSE (named events) | 无 gRPC | | OpenAI Chat / Assistants | REST | SSE | 无 gRPC | | AWS Bedrock | REST | binary `application/vnd.amazon.eventstream` | 无 gRPC | | Google Vertex AI | gRPC + REST mirror | gRPC streaming | 同源 proto transcoding | | Cohere / Mistral / Replicate / Together / Fireworks | REST | SSE | 无 gRPC | | Ollama | REST | NDJSON | N/A 单进程 | | LM Studio | REST (OpenAI 兼容) | SSE | N/A | | LangServe | REST (FastAPI auto) | SSE / chunked HTTP | 无 gRPC | **业界共识**: 10 / 11 公开 REST/SSE, 仅 Vertex 用 gRPC. Vertex 是 Google 内部全栈 gRPC 的延伸, 不仿照. **streaming 共识**: SSE 是 LLM 子领域共识 (Ollama / Bedrock 是少数派但仍是 HTTP/1.1 chunked, 不是 gRPC). **双通道 (业务 REST + 观测 gRPC) 在 LLM 圈零先例**, 但**与 LLM 主流 (REST 业务) 对齐**. 唯一合法 prior art 是 GCP transcoding (一份 proto 同时长出 gRPC + REST), 不是 etcd/K8s 的反向 (gRPC 主, REST 副). **grpc-gateway 调研**: v2.29.0 (2026-04-16) 健康活跃, 19,846 stars, etcd / Istio 生产案例. 给我们场景 (SafetyChainService 加 REST 镜像) 价值窄: admin / server.go 已实现 `handleSafetychainVerdicts` + `handleSafetychainBreakers` REST handler, 重复. **gRPC-Web 现状**: Google 官方 grpc-web 仍要 Envoy. ConnectRPC 是 2024-2025 转向的现代替代 (Buf 自家替换 grpc-web 为 connect-web), 不需要 Envoy. 但 Flyto 没有浏览器前端需求, 这一项不影响本决策. ### 3.2 质疑 agent (6 道硬质疑, 4 个 blocker) | Q | 严重程度 | 关键发现 | |---|---|---| | Q1: SSE streaming/性能坑 | 严重 | Caddy proxy buffering / Chrome 6-conn / 1000 单/s 带宽 / mobile NAT — Q1.1 上线 100% 踩坑须 Caddyfile flush_interval -1 | | Q2: auth 双份维护 | **致命** | server.go 用 raw `BearerToken` ConstantTimeCompare, 不走 OIDC. 接进 cmd/common 后跑两套不兼容 auth. **必须 commit 2 单独修** | | Q3: 端口/mux 整合 | 严重 | 同端口分路径 vs 双端口, 没拍板必返工. 推荐双端口 | | Q4: grpc-gateway 价值 | **致命** | admin/server.go 已有 `/admin/safetychain/{verdicts,breakers}` REST handler, 加 grpc-gateway 是把同一表面再做一次. 砍 | | Q5: ADR-0002 立硬规则 | 严重 | 与 9 天前 "gRPC 优 HTTP" 看似对冲. 实为分层. ADR 须显式 cross-reference | | Q6: rollback / migration | 严重 | server.go in-memory sessions 单实例假设, k8s 多副本立刻挂. ADR 须显式标注限制 | ### 3.3 设计 agent (4 备选 + 推荐方案 A 修正版) | 方案 | 范围 | commit 数 | 优点 | 代价 | |---|---|---|---|---| | A 完整版 | 激活 + grpc-gateway 镜像 + ADR | 8-10 | 观测 RPC 双通道 | 与 Q4 冲突, admin 已有 REST handler 重复 | | **A 修正版** | **激活 + 不加 gateway + ADR** | **6** | **3-agent 共识** | **server.go 改造工作量 5 处替换非 trivial** | | B 只激活不加 gateway 不立 ADR | 同 A 修正版减 ADR | 5 | 最快 | 失去架构原则记录, 下次再有人想加业务 RPC 又要重新讨论 | | C 重写 server.go | 不复用 1263 行 | 10+ | 干净起步 | 浪费已踩坑过的 SSE/permission 桥接/限速实现 | | D 全 gRPC 化业务 | 与 PM 方向冲突 | 12+ | C# 强类型 stub | LLM 圈零先例, sanity check 通过即砍 | 设计 agent 自行砍掉 grpc-gateway (与 Q4 共识), 推荐 A 修正版. ### 3.4 PM 拍板与 reconcile 主线程 reconcile 3 份报告, 关键结论: 1. **grpc-gateway 砍** (3 方共识, admin 已有 REST handler) 2. **ADR-0002 立, 但定性 bifurcation 不是 U-turn** — 9 天前 SafetyChain 走 gRPC 是观测面方向, 现在仍成立; PM 新拍 "业务用 REST" 是补另一面 3. **server.go 改造工作量 = 5 处替换** (auth OIDC / 删 provider 写死 / 加 tenant / 接 safetychain wire / 拆 signal), 6 commit 节奏对 4. **双端口拓扑** (业务 :8080 + admin :8081 + gRPC :9090) 5. **Caddyfile flush_interval -1** 必须显式 (Q1.1 缓解) 6. **4 tracked debt 登记**: in-memory sessions / cross-transport request-id / SSE 带宽 / Swagger 与 L407 关联 PM 拍板 A 修正版后开干. 6 commit 节奏完整落地. --- ## 4. 后果 / Consequences ### 正面 - 与 LLM 业界 10/11 主流 (REST/SSE 业务通道) 对齐, 任何 HTTP 客户端通用 - 浏览器 EventSource 原生支持, 未来加 Web dashboard 零代理依赖 - C# logistics 不需要为业务调用写 gRPC stub, HttpClient (Refit / RestSharp) 即用; 后续加新行业 (Java / Python) 同样不需要 codegen - OIDC 鉴权跨 gRPC + admin HTTP + 业务 REST 同一套 Verifier, dev 模式 Verifier=nil 跳过与 admin 一致 - SafetyChain / Health gRPC 观测面保留, 沿 9 天前方向不破坏 - common 保持纯 transport, Tool 级 SafetyChain 装饰由行业 platform 自决 + 自填 Validator + extractor, 避免 cmd/common 一刀切锁死单一组合 ### 负面 - 失去 gRPC 强类型契约, REST 协议靠 OpenAPI/Swagger 文档维护 (L407 直接 子项) - SSE framing 比 gRPC binary 多 5-10x 带宽 overhead (Q1.3); 1000 单/s 上量后跨 region 流量成本要量化 (L695 monitoring) - in-memory sessions 单实例假设, k8s 多副本要换 SessionStore (L693) - gRPC + REST cross-transport request-id / trace 串通要 OpenTelemetry 投入 (L694) ### 中性 - ADR-0002 是 "当前阶段默认 + 决策上下文", 不是硬规则. 触发 § 6 重评估 条件可改方向 --- ## 5. 替代方案保留 / Alternatives Preserved (CLAUDE.md 原则 5) ### 方案 A 完整版 (含 grpc-gateway) 如果未来 SafetyChain 观测 RPC 加到 5+ 个且 admin/server.go 手写镜像维护 负担显现, 可以从 A 修正版升级到 A 完整版. 升级路径: 1. SafetyChainService.proto 加 `option (google.api.http) = { ... }` 注解 2. Makefile 加 protoc-gen-grpc-gateway / protoc-gen-openapiv2 step 3. cmd/common 加 grpc-gateway runtime.ServeMux, 路径 `/api/observe/*` 4. admin/server.go 现有 REST handler 视情况 deprecate 或保留作为 fallback 预期升级成本: ~300 行 Go (含 generated code) + 1 个新依赖 (grpc-ecosystem/grpc-gateway/v2). 触发条件: 见 § 6. ### 方案 D 全 gRPC 化业务 + grpc-gateway 反向产 REST 与 PM 拍板的方向冲突 (REST 业务). 列出来作 sanity check, 已 PM 二次确认 不走. 触发重评估: 见 § 6. ### 不立 ADR (方案 B) 放弃记录架构原则的代价是: 下次有人想加业务 RPC 时又要重新讨论. 已选立 ADR. ### 重写 server.go (方案 C) 放弃 1263 行已踩坑过的 SSE / permission 桥接 / 限速实现, 浪费. 已 PM 拍板复用. --- ## 6. 触发重新评估的条件 未来出现以下任一情况, 应重新评估 ADR-0002 的决定: 1. **SSE 性能瓶颈实测**: logistics platform 上量后, SSE 跨 region 带宽 成本超过 gRPC streaming 5x (Q1.3 数学预测), 且 L695 monitoring 数据 支持 2. **第二个非 C# 行业 platform 出现 gRPC 强需求**: 如银行业 ERP 要求"高频 RPC 必须 gRPC + mTLS 过合规审计", rule of two 满足后 重评估业务 transport 3. **业界共识改变**: Anthropic / OpenAI / Bedrock 任一在自家公开 API 引入 gRPC 业务通道并稳定运行 6 个月以上 4. **观测 RPC 数量爆炸**: SafetyChain 加到 5+ RPC 且 admin/server.go 手写镜像维护负担显现, 触发 § 5 方案 A 完整版升级 5. **grpc-gateway 生态激进变化**: ConnectRPC 全面取代 grpc-gateway 后, 升级 路径要重写 (但当前 ConnectRPC 与 Flyto 无浏览器前端需求无关, 不影响) 满足任一即可重新走 3-agent review 评估升级 / 切方向. --- ## 7. 参考链接 ### 业界 LLM API 文档 - [Anthropic Messages API streaming](https://docs.anthropic.com/en/docs/build-with-claude/streaming) - [OpenAI Chat Completions streaming](https://platform.openai.com/docs/api-reference/chat-streaming) - [AWS Bedrock InvokeModelWithResponseStream](https://docs.aws.amazon.com/bedrock/latest/APIReference/API_runtime_InvokeModelWithResponseStream.html) - [Google Vertex AI gRPC predict](https://cloud.google.com/distributed-cloud/hosted/docs/latest/gdch/apis/vertex-ai/prediction/grpc/overview) - [Google AIP-127 HTTP/gRPC transcoding](https://google.aip.dev/127) - [Cohere streaming](https://docs.cohere.com/v2/docs/streaming) - [Mistral La Plateforme](https://mistral.ai/news/la-plateforme/) - [Ollama API docs](https://github.com/ollama/ollama/blob/main/docs/api.md) - [LM Studio OpenAI Compatibility](https://lmstudio.ai/docs/developer/openai-compat) - [LangServe](https://blog.langchain.com/introducing-langserve/) - [Stainless customer page (OpenAI / Anthropic / Meta SDK auto-gen)](https://www.stainless.com/customers/openai) ### gRPC / 替代方案 - [grpc-gateway repo](https://github.com/grpc-ecosystem/grpc-gateway) - [etcd grpc-gateway docs](https://etcd.io/docs/v3.5/dev-guide/api_grpc_gateway/) - [APISIX K8s skip grpc-gateway analysis](https://api7.ai/blog/apisix-migrate-etcd-operation-from-http-to-grpc) - [grpc-web repo](https://github.com/grpc/grpc-web) - [State of gRPC in browser](https://grpc.io/blog/state-of-grpc-web/) - [ConnectRPC connect-es](https://github.com/connectrpc/connect-es) - [Buf Connect-Web announcement](https://buf.build/blog/connect-web-protobuf-grpc-in-the-browser) ### 项目内部参考 - `core/docs/adr/0001-reverse-thinking-gate.md` — ADR 体例参考 - `~/.claude/projects/-home-admin-ccm/memory/feedback_architecture_principle_over_rule_of_two.md` — gRPC 优 HTTP 是 SafetyChain 通路铁律 (本 ADR 的 cross-reference 论据) - `platform/common/internal/server/server.go` — 业务 REST/SSE 实现 (1263 行) - `platform/common/cmd/common/main.go` — 三 listener wire (gRPC + admin HTTP + 业务 REST) - `platform/common/internal/admin/server.go` — admin 观测面 (含 /admin/safetychain/* REST handler, 复用论据见 § 2.2) - `platform/common/internal/auth/middleware.go` — auth.HTTPMiddleware, 跨 admin + 业务 REST 共用 - `deploy/docker-compose.yml` + `deploy/Caddyfile` — 端口拓扑 + Caddy 路由 --- ## 8. 修订记录 | 日期 | 版本 | 变更 | |---|---|---| | 2026-04-26 | v1.0 | 初稿. 方案 A 修正版落地 6 commit, ADR 起草 |