baa-conductor


baa-conductor / docs / auth
im_wower  ·  2026-04-02

README.md

  1# Auth Model
  2
  3`T-012` 的目标不是直接把鉴权做成生产实现,而是先把 Control API 周围的角色、Token 形式和授权边界压成一套可复用的模型,供后续 `T-003` 接入。
  4
  5当前仓库的临时单节点运行模式已经简化为:
  6
  7- `mini` 单节点
  8- Firefox 插件默认直连 `https://conductor.makefile.so`
  9- `CONTROL_API_AUTH_REQUIRED=false`
 10
 11也就是说,这份文档现在更偏“鉴权模型预案”,不是当前 live 临时部署的硬要求。
 12
 13## 设计原则
 14
 15- 所有 Control API 请求统一走 `Authorization: Bearer <token>`
 16- 先做最小可落地模型:机器侧 token 与人类/运维会话 token 分开
 17- 写接口不仅校验角色,还校验资源归属
 18- `browser_admin``ops_admin` 必须分权,不能共用同一组维护能力
 19- `readonly` 必须只能看状态,不能借读权限侧写成写权限
 20
 21## 角色边界
 22
 23| Role | 代表对象 | 推荐 Token | 明确允许 | 明确禁止 |
 24| --- | --- | --- | --- | --- |
 25| `controller` | 当前单节点 conductor 服务 | `service_hmac`,后续可升级为 `service_signed` | `controllers.heartbeat`、`leader.acquire`、`tasks.plan`、`tasks.claim` | `system.pause`、`system.resume`、`system.drain`、任何维护操作、任何 step 执行回写 |
 26| `worker` | 运行 step 的 Codex worker | `service_hmac`,后续可升级为 `service_signed` | `steps.heartbeat`、`steps.checkpoint`、`steps.complete`、`steps.fail` | claim task、lease、task 创建、系统级操作、维护操作 |
 27| `browser_admin` | 可见 Claude `control` 会话 / 浏览器管理面板 | `browser_session` | `tasks.create`、`system.pause`、`system.resume`、`system.drain`、状态/日志查看 | lease、claim、step 执行回写、主备切换 |
 28| `ops_admin` | 运维维护入口 | `ops_session` | `maintenance.promote`、`maintenance.demote`、状态/日志查看 | task 创建、普通调度、step 执行回写、浏览器控制按钮 |
 29| `readonly` | 状态面板、观察者、只读审计入口 | `browser_session` | `system.state.read`、`tasks.read`、`tasks.logs.read`、`runs.read` | 任何写接口 |
 30
 31其中 `tasks.create -> browser_admin` 的划分来自设计文档的 task 创建流程:可见 `control` 会话通过 Control API 发起 task。
 32
 33## Token 模型
 34
 35当前代码把 Token 形式整理成四类:
 36
 37| Token Kind | 状态 | 角色 | 用途 |
 38| --- | --- | --- | --- |
 39| `service_hmac` | 当前可用 | `controller`、`worker` | 当前最低要求。使用共享密钥或 HMAC 签名材料做机器间鉴权 |
 40| `service_signed` | 未来演进 | `controller`、`worker` | 后续把 role、scope、节点身份变成可签名 claim |
 41| `browser_session` | 当前可用 | `browser_admin`、`readonly` | 浏览器侧 bearer token,承载控制面板和只读面板 |
 42| `ops_session` | 当前可用 | `ops_admin` | 与浏览器控制面板分离的运维 token |
 43
 44## 当前已落地的 UI session MVP
 45
 46`T-S071` 已经把浏览器工作台的最小 session 闭环接进 `conductor-daemon` 47
 48- `POST /v1/ui/session/login`
 49- `POST /v1/ui/session/logout`
 50- `GET /v1/ui/session/me`
 51
 52当前实现边界:
 53
 54- 介质:`HttpOnly` cookie
 55- cookie 属性:`SameSite=Lax`、`Path=/`,在 `https` 下自动加 `Secure`
 56- session 存储:当前进程内内存表,重启后失效
 57- 角色:`browser_admin`、`readonly`
 58
 59当前环境变量:
 60
 61- `BAA_UI_BROWSER_ADMIN_PASSWORD`
 62- `BAA_UI_READONLY_PASSWORD`
 63- `BAA_UI_SESSION_TTL_SEC`
 64
 65当前 `login` 接口是单用户最小模型:
 66
 67- 请求体:`{ "role": "browser_admin" | "readonly", "password": "..." }`
 68- 不向前端暴露长期 bearer token
 69- `me` 始终返回当前会话状态和 `available_roles`,供 `/app/login` 与前端路由守卫使用
 70
 71当前未覆盖的范围:
 72
 73- session 不持久化到数据库
 74- 现有大部分 `/v1/*` 业务接口仍保留原有访问边界,后续需要把 `browser_session` 逐步接到真正的读写授权中间层
 75
 76统一 claim 形状建议包含:
 77
 78- `subject`
 79- `role`
 80- `tokenKind`
 81- `audience = "control_api"`
 82- `tokenId`
 83- `issuedAt`
 84- `expiresAt`
 85- `scope`
 86- `controllerId`
 87- `workerId`
 88- `nodeId`
 89- `sessionId`
 90
 91MVP 阶段并不要求四类 token 都立即有完整签发器;`packages/auth` 先把这些字段和验证接口占好位。
 92
 93## 资源归属规则
 94
 95对写接口,角色校验之外还要做归属校验:
 96
 97- `controller` 请求必须带 `controllerId`
 98- `worker` 请求必须带 `workerId`
 99- 如果目标资源上已经有 `controllerId``workerId`,则必须与 token 中的身份一致
100- `browser_admin`、`ops_admin`、`readonly` 当前阶段不做资源级身份绑定,只按角色与 token 种类分权
101
102这意味着:
103
104- `controller` 不能代替其他 `controller` 续租或持久化对方拥有的对象
105- `worker` 不能替其他 `worker` 回写 step 心跳、checkpoint、complete/fail
106
107## Control API 权限矩阵
108
109| Route | Action | Allowed Roles | Binding |
110| --- | --- | --- | --- |
111| `POST /v1/controllers/heartbeat` | `controllers.heartbeat` | `controller` | `controller` |
112| `POST /v1/leader/acquire` | `leader.acquire` | `controller` | `controller` |
113| `POST /v1/tasks` | `tasks.create` | `browser_admin` | none |
114| `POST /v1/tasks/claim` | `tasks.claim` | `controller` | `controller` |
115| `POST /v1/tasks/:task_id/plan` | `tasks.plan` | `controller` | `controller` |
116| `POST /v1/steps/:step_id/heartbeat` | `steps.heartbeat` | `worker` | `worker` |
117| `POST /v1/steps/:step_id/checkpoint` | `steps.checkpoint` | `worker` | `worker` |
118| `POST /v1/steps/:step_id/complete` | `steps.complete` | `worker` | `worker` |
119| `POST /v1/steps/:step_id/fail` | `steps.fail` | `worker` | `worker` |
120| `POST /v1/system/pause` | `system.pause` | `browser_admin` | none |
121| `POST /v1/system/resume` | `system.resume` | `browser_admin` | none |
122| `POST /v1/system/drain` | `system.drain` | `browser_admin` | none |
123| `POST /v1/system/promote` | `maintenance.promote` | `ops_admin` | none |
124| `POST /v1/system/demote` | `maintenance.demote` | `ops_admin` | none |
125| `GET /v1/system/state` | `system.state.read` | `browser_admin`、`ops_admin`、`readonly` | none |
126| `GET /v1/tasks/:task_id` | `tasks.read` | `browser_admin`、`ops_admin`、`readonly` | none |
127| `GET /v1/tasks/:task_id/logs` | `tasks.logs.read` | `browser_admin`、`ops_admin`、`readonly` | none |
128| `GET /v1/runs/:run_id` | `runs.read` | `browser_admin`、`ops_admin`、`readonly` | none |
129
130`promote` / `demote` 还没有落到当前主控制面代码里,但鉴权模型先把它们单独列出来,避免后续把维护入口塞进 `browser_admin`131
132## `packages/auth` 提供的骨架
133
134`packages/auth/src` 现在拆成四层:
135
136- `actions.ts`
137  - 定义统一 action 名称与读写/资源绑定属性
138- `model.ts`
139  - 定义角色、principal、token 形状、bearer 解析和 token verifier 接口
140- `policy.ts`
141  - 定义角色边界、权限矩阵以及 `authorize(...)`
142- `control-api.ts`
143  - 定义 route 到 action 的映射,以及 `authorizeControlApiRoute(...)`
144
145这让后续接入方可以先做三段式流程:
146
1471.`Authorization` 头提取 bearer token
1482. 用自己的 verifier 还原 `AuthPrincipal`
1493. 按路由和资源归属调用 `authorizeControlApiRoute(...)`
150
151示意代码:
152
153```ts
154import {
155  authorizeControlApiRoute,
156  extractBearerToken,
157  type AuthPrincipal
158} from "@baa-conductor/auth";
159
160async function guardControlApiRequest(
161  method: "GET" | "POST",
162  path: string,
163  authorizationHeader: string | undefined,
164  principal: AuthPrincipal,
165  resource?: { controllerId?: string; workerId?: string }
166) {
167  const tokenResult = extractBearerToken(authorizationHeader);
168
169  if (!tokenResult.ok) {
170    return { ok: false, statusCode: 401, reason: tokenResult.reason };
171  }
172
173  return authorizeControlApiRoute({
174    method,
175    path,
176    principal,
177    resource
178  });
179}
180```
181
182## 对 `T-003` 的直接交接点
183
184- 直接复用 `CONTROL_API_AUTH_RULES`,避免在 Worker 里再写一份路由权限表
185- 先把 verifier 作为注入接口实现,不要把签名校验和业务 handler 混在一起
186- 对 step 相关写接口,鉴权之后再结合 DB 中的 `assigned_worker_id` 做最终归属比对
187- 对 controller 相关写接口,鉴权之后再结合 leader term / owner 字段做冲突校验
188
189## 当前风险
190
191- `service_hmac` 只是模型约定,密钥轮换策略还未实现
192- 浏览器侧和运维侧 token 的签发、撤销、持久化方式还没落地
193- 资源归属检查目前只到 `controllerId` / `workerId` 这一层,后续可能还要细化到 `run_id``step_id`