跳到主要内容

第 4 章:服务启动、路由、中间件与认证

4.1 Server 入口是一张依赖图

server/cmd/server/main.go 不只是调用 http.ListenAndServe。它是全系统组合根,决定:

  • 哪些组件共享同一个数据库连接池;
  • 事件监听器以什么顺序注册;
  • Redis 存在和不存在时使用哪套实时拓扑;
  • Browser WS 与 Daemon WS 怎样分开;
  • 后台 Worker 何时启动、怎样停止;
  • Handler 获得哪些可选集成;
  • 优雅关闭先停止什么、后停止什么。

理解这个文件,能避免“某个 Service 明明 Publish 了,为什么没有副作用”的问题。

4.2 启动阶段

启动过程可以概括为:

关键步骤如下:

  1. 初始化结构化日志,并对不安全或缺失配置给出警告。
  2. 创建 PostgreSQL Pool 与 sqlc Queries。
  3. 创建同步 Event Bus。
  4. 创建 Browser Realtime Hub 与独立 Daemon WS Hub。
  5. 若配置 Redis,按 REALTIME_RELAY_MODE 选择 shardedduallegacy
  6. 注册事件到 Subscriber、Activity、Notification、Realtime 的监听器。
  7. newRouter 构造服务、Handler 和路由。
  8. 启动 Runtime Sweeper、Heartbeat Scheduler、Autopilot Failure Monitor、Webhook Worker、Channel Supervisor 等后台循环。
  9. 注册数据库支持的 Scheduler Jobs,例如小时用量汇总与 Autopilot Schedule Dispatch。
  10. 启动 HTTP Server,等待终止信号。

4.3 事件监听顺序为什么重要

events.BusPublish 是同步的:

  1. 先依次执行特定 Event Type 的 Handler;
  2. 再执行 SubscribeAll 的全局 Handler;
  3. 每个 Handler 的 Panic 被单独恢复,不阻止后续监听器。

这使业务副作用可以在广播前完成。例如 Subscriber、Activity、Notification 等特定监听器先写派生数据,全局 Realtime Broadcaster 后把事件推给客户端。

需要注意:同步不等于事务。数据库事务通常已经提交后才 Publish;监听器失败不会自动回滚原业务写入。因此监听器必须可降级、可重算,不能偷偷承载唯一持久事实。

4.4 Router 是边界目录,不是业务事实源

router.go 超过普通小项目的路由文件规模,因为它同时负责:

  • 构造共享 Service;
  • 注入 Storage、Analytics、Feature Flags、Cloud 代理;
  • 装配 Lark、Slack、Composio、GitHub、VCS;
  • 给不同路由组挂不同认证与 Workspace Middleware;
  • 暴露公开 Webhook、Auth Callback、Health、Metrics 等特殊入口;
  • 连接 Daemon RPC Handler 与 WS Hub。

它应该被当作“系统接线图”阅读,而不是把里面每个闭包都当成业务算法。

4.5 四类主要令牌

人类会话 JWT

浏览器登录后可使用 HttpOnly multica_auth Cookie。状态改变请求还必须通过 CSRF 校验。

JWT 也可经 Authorization: Bearer 传入。Middleware 验证 HMAC 签名与 sub,再把可信用户 ID 写入 Server-side Header/Context。

Cookie 工具在 internal/auth/cookie.go,通用认证中间件在 internal/middleware/auth.go

mul_ Personal Access Token

CLI 和 API 用户可使用 mul_ PAT:

  • 数据库存 Hash,不存明文;
  • 校验有效期与撤销状态;
  • Redis 可缓存短时间认证结果;
  • last_used_at 只在缓存 Miss 时更新,避免每请求写数据库;
  • Cache TTL 会被令牌剩余有效期截断。

mdt_ Daemon Token

Daemon 注册后使用 mdt_ 令牌证明机器身份。它绑定可访问的 Workspace/Daemon,而不是某个正在执行的 Agent。

DaemonAuth 支持现代 Daemon Token,并为旧客户端保留 PAT/JWT 兼容路径。认证结果写入 Context:

  • Daemon ID;
  • Daemon Workspace 范围;
  • 认证路径标签,用于权限与慢日志分析。

mat_ Agent Task Token

这是最重要的任务内能力令牌:

  • 在 Server Claim Task 时生成;
  • 绑定 user_id + agent_id + task_id + workspace_id
  • 注入 Agent 子进程;
  • Agent 调用 multica issue ...multica attachment ... 等命令时使用;
  • Middleware 用数据库绑定覆盖所有客户端自带的 X-Agent-ID/X-Task-ID
  • 设置可信 X-Actor-Source: task_token
  • Owner-only、账户级动作必须拒绝该认证路径。

它的设计目标是:Agent 只能以“这一次任务”被授权的身份行动,不能继承 Daemon Owner 的全部长期权限。

4.6 可选的 mcn_ Cloud Node PAT

当 Multica Cloud Fleet 配置存在时,mcn_ Token 由外部 Fleet 验证:

  • Fleet 是 Token 状态与 Owner ID 的权威;
  • 本地仍校验 Owner ID 对应真实 User,防“幽灵身份”;
  • 配置缺失时直接 Fail Closed,不回落到普通 JWT/PAT;
  • Fleet 明确拒绝返回 401;Fleet 不可用返回 503,提醒调用方重试而不是丢弃有效凭据;
  • 标记 X-Actor-Source: cloud_pat,账户级人类操作可拒绝机器身份。

这一分支显示认证不只是“Token 是否有效”,还要告诉下游“它属于哪种行动者”。

4.7 为什么要先删除客户端传来的 Actor Header

通用 Auth 和 Daemon Auth 都先删除 X-Actor-Source。随后只有服务端可信分支能重新写入 task_tokencloud_pat

否则攻击者可以:

  1. 带一个普通 PAT;
  2. 自己添加 X-Actor-Source: member 或伪造 Agent/Task ID;
  3. 欺骗下游 Actor Resolver,把机器操作当成人类操作。

“先清空,再由认证分支盖章”是一个可复用的边界模式。

4.8 Workspace Middleware 的两层职责

典型业务路由既要认证 User,又要确认 Workspace Membership:

  • Auth 证明用户身份;
  • Workspace Middleware 从路径/Header 解析 Workspace,并验证 Membership;
  • 资源级 Guard 继续判断角色、Owner、Agent Permission Mode、Collaborator 等;
  • 查询本身仍应带 Workspace ID,形成纵深防御。

Redis Membership Cache 可以加速正向结果,但成员删除时必须失效缓存。

4.9 Daemon 访问不是“登录用户的另一种形式”

Daemon Handler 会继续检查:

  • Token 是否允许目标 Workspace;
  • Runtime 是否属于该 Daemon/Workspace;
  • Task 是否属于允许的 Runtime/Workspace;
  • Claim Payload 中的 Runtime 列表是否全部授权;
  • WS 连接的 Identity 是否允许 RPC Body 中的目标。

所以 mdt_ 只是第一道身份门,不是“拿到后可操作整个 API”。

相关 Guard 集中在 handler/daemon.gorequireDaemon*Accessdaemonws.ClientIdentity

4.10 其他中间件

Client Metadata

middleware/client.go 解析 X-Client-Platform / Version / OS,用于日志、兼容性判断和统计。

Daemon、Web、Desktop、Mobile 的升级节奏不同,没有版本维度就很难解释“只有某些客户端失败”。

CORS 与 CSP

Web API 需要限制 Origin;Browser WS 也维护独立 Allowed Origins。CSP 则降低脚本注入风险,尤其是 Cookie Auth 与富文本界面并存时。

Rate Limit 与 Trusted Proxy

middleware/ratelimit.go 只有在 Remote IP 来自可信代理网段时才信任转发 Header,避免客户端伪造 X-Forwarded-For 绕过限流。

Autopilot Webhook 还按 Token、来源 IP 和绝对 IP 设置不同预算,多实例时可用 Redis Lua 保持原子性。

Request Logger 与 Metrics

Request Logger 记录方法、路由、状态、耗时和客户端信息;Prometheus Middleware 使用路由模板而不是原始 URL,避免 UUID 造成高基数。

健康检查和预期的 Runtime/Task 404 会降低日志等级,避免正常轮询淹没错误信号。

4.11 公开入口为何要独立审视

以下入口不能简单套“登录用户”路由组:

  • 登录/验证码与 OAuth Callback;
  • GitHub/VCS Webhook;
  • Autopilot Token Webhook;
  • Slack/Feishu 安装或绑定回调;
  • Health/Readiness;
  • Browser/Daemon WebSocket Upgrade;
  • Cloud Billing Webhook。

它们分别依赖签名、一次性 State、Webhook Token、安装凭据或网络限制。共同原则是:

  1. 在读取大 Body 前尽量做廉价限流;
  2. 限制 Body 大小;
  3. 对原始 Body 验签,不能先反序列化再重编码;
  4. 对重复投递做幂等;
  5. 对不支持的事件返回成功但不产生业务副作用,避免平台重试风暴。

4.12 Browser WS 与 Daemon WS 为什么分开

两者身份和流量完全不同:

维度Browser WSDaemon WS
身份User + MembershipDaemon Token / Machine Identity
房间User、Workspace、未来细粒度 ScopeRuntime、Workspace、User 控制范围
主要流量业务事件、缓存失效心跳、Task Wakeup、RPC Claim、配置变化
客户端行为断线重连、重新查询WS-first 控制,必要时 HTTP Fallback
风险越权订阅、慢客户端重复 Claim、并发写帧、失活 Runtime

将两者塞进一个 Hub 会让授权、背压和协议演进互相污染。

4.13 Redis 实时拓扑

无 Redis 时,Hub 只服务当前进程连接,适合单节点。

有 Redis 时:

  • sharded:按 Scope Hash 到固定数量的 Redis Streams,节点读取各 Shard;
  • legacy:按活跃 Scope 建消费者;
  • dual:迁移期同时写两套路径;
  • Event ID 用于节点/客户端去重;
  • Relay 还可把 Daemon Runtime Wakeup 送到持有连接的节点。

默认 Sharded 模式避免 Scope 数增长时创建过多 Redis Stream/Consumer。

4.14 后台 Worker 的生命周期

Server 启动的不只是 HTTP:

  • Runtime Sweeper:把过期心跳的 Runtime 标成 Offline;
  • Batched Heartbeat/状态协调;
  • Task Stale/Queued/Prepare Lease 处理;
  • Autopilot Failure Monitor;
  • Webhook Delivery Worker;
  • Channel Supervisor;
  • DB-backed Scheduler;
  • DB/Realtime/DaemonWS Metrics Collector。

这些循环必须接受 Context,支持停止,并尽量使用数据库 Claim/Lease 保证多实例安全。

4.15 优雅关闭顺序

关闭不是并行 cancel() 所有组件。合理顺序大致是:

  1. 收到 SIGTERM,可按配置先 Hold,让负载均衡停止送新流量。
  2. 关闭 HTTP 接入并等待在途请求。
  3. 停止产生新工作的 Scheduler/Worker/Channel 输入。
  4. Flush/Drain 已接收的 Channel 消息与 Reply。
  5. 停止 Relay、Hub、Sweepers、Metrics。
  6. 关闭 Redis Client 与数据库池。

若先关数据库,正在完成的 HTTP/Task 回报会失败;若先断 Channel 而不 Flush,已确认的平台消息可能永远不触发任务。

4.16 本章结论

Server 启动代码揭示了 Multica 的三条核心边界:

  1. 认证要同时表达“谁”与“以何种身份路径行动”。
  2. Workspace/Resource Guard 与带范围 SQL 必须叠加,不能互相替代。
  3. HTTP、Browser WS、Daemon WS、Worker 和 Redis Relay 有不同生命周期,但由同一个组合根明确接线和关闭。

下一章开始进入真正的业务入口:一个 Issue、Comment 或 Chat 消息究竟在什么条件下变成 Agent Task。


上一章:领域模型、PostgreSQL 与 sqlc
下一章:Issue、Comment 与工作触发