第 4 章:服务启动、路由、中间件与认证
4.1 Server 入口是一张依赖图
server/cmd/server/main.go 不只是调用 http.ListenAndServe。它是全系统组合根,决定:
- 哪些组件共享同一个数据库连接池;
- 事件监听器以什么顺序注册;
- Redis 存在和不存在时使用哪套实时拓扑;
- Browser WS 与 Daemon WS 怎样分开;
- 后台 Worker 何时启动、怎样停止;
- Handler 获得哪些可选集成;
- 优雅关闭先停止什么、后停止什么。
理解这个文件,能避免“某个 Service 明明 Publish 了,为什么没有副作用”的问题。
4.2 启动阶段
启动过程可以概括为:
关键步骤如下:
- 初始化结构化日志,并对不安全或缺失配置给出警告。
- 创建 PostgreSQL Pool 与 sqlc Queries。
- 创建同步 Event Bus。
- 创建 Browser Realtime Hub 与独立 Daemon WS Hub。
- 若配置 Redis,按
REALTIME_RELAY_MODE选择sharded、dual或legacy。 - 注册事件到 Subscriber、Activity、Notification、Realtime 的监听器。
- 由
newRouter构造服务、Handler 和路由。 - 启动 Runtime Sweeper、Heartbeat Scheduler、Autopilot Failure Monitor、Webhook Worker、Channel Supervisor 等后台循环。
- 注册数据库支持的 Scheduler Jobs,例如小时用量汇总与 Autopilot Schedule Dispatch。
- 启动 HTTP Server,等待终止信号。
4.3 事件监听顺序为什么重要
events.Bus 的 Publish 是同步的:
- 先依次执行特定 Event Type 的 Handler;
- 再执行
SubscribeAll的全局 Handler; - 每个 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。
否则攻击者可以:
- 带一个普通 PAT;
- 自己添加
X-Actor-Source: member或伪造 Agent/Task ID; - 欺骗下游 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、安装凭据或网络限制。共同原则是:
- 在读取大 Body 前尽量做廉价限流;
- 限制 Body 大小;
- 对原始 Body 验签,不能先反序列化再重编码;
- 对重复投递做幂等;
- 对不支持的事件返回成功但不产生业务副作用,避免平台重试风暴。
4.12 Browser WS 与 Daemon WS 为什么分开
两者身份和流量完全不同:
| 维度 | Browser WS | Daemon WS |
|---|---|---|
| 身份 | User + Membership | Daemon Token / Machine Identity |
| 房间 | User、Workspace、未来细粒度 Scope | Runtime、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() 所有组件。合理顺序大致是:
- 收到 SIGTERM,可按配置先 Hold,让负载均衡停止送新流量。
- 关闭 HTTP 接入并等待在途请求。
- 停止产生新工作的 Scheduler/Worker/Channel 输入。
- Flush/Drain 已接收的 Channel 消息与 Reply。
- 停止 Relay、Hub、Sweepers、Metrics。
- 关闭 Redis Client 与数据库池。
若先关数据库,正在完成的 HTTP/Task 回报会失败;若先断 Channel 而不 Flush,已确认的平台消息可能永远不触发任务。
4.16 本章结论
Server 启动代码揭示了 Multica 的三条核心边界:
- 认证要同时表达“谁”与“以何种身份路径行动”。
- Workspace/Resource Guard 与带范围 SQL 必须叠加,不能互相替代。
- HTTP、Browser WS、Daemon WS、Worker 和 Redis Relay 有不同生命周期,但由同一个组合根明确接线和关闭。
下一章开始进入真正的业务入口:一个 Issue、Comment 或 Chat 消息究竟在什么条件下变成 Agent Task。