跳到主要内容

第 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 选择 sharded、dual 或 legacy。
  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.Bus 的 Publish 是同步的:

  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_token 或 cloud_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.go 的 requireDaemon*Access 与 daemonws.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 与工作触发。