# ADR-0028:Project Identity、版本化 RBAC 与默认拒绝 Policy Core - 状态:Proposed - 日期:2026-07-19 - 关联:QL-RFC-0001、ADR-0025、ADR-0027 ## 上下文 QingLong 2.x 只有面板单用户登录和 API App route scope。`logs/crons/envs` 等 scope 只决定某类 `/open/*` 路由能否进入,既没有稳定 ActorRef,也不绑定 Project、资源 identity 或审批策略。把 `logs` 直接解释为 3.0 `artifact.read` 会让一个旧 token 获得所有 Project 日志;把当前用户名直接作为 owner 又会把可修改的显示值误当成持久安全主体。 ADR-0027 已要求 Artifact 在任何文件访问前执行 `artifact.read`,但只定义了 authorizer port。3.0 还需要一个可在 edge 上低成本运行、在 cluster 中可替换存储 adapter、能够表达 User/API App/MCP Client/Agent/System/Worker 的基础 Policy Core。 ## 决策 ### 1. ActorRef 使用统一、精确且有界的主体 主体固定为: ```ts interface PolicySubject { type: 'user' | 'api_app' | 'mcp_client' | 'agent' | 'system' | 'worker'; id: string; } ``` ID 为 1~255 字符,不允许控制字符。所有 subject、Project、RoleBinding、snapshot 和 policy request 都拒绝未知字段,避免调用方提交一个看似生效但实际被忽略的 `permissions/context`。Artifact read 已复用同一 subject 类型,不再维护第二套 identity 枚举。 Authentication adapter 负责把 session/token/mTLS credential 映射为稳定 subject;PolicyEngine 不读取 HTTP header、cookie、用户名或 legacy App scope。身份认证与授权仍是两个边界。 ### 2. Project 是权限和归档状态的事实源 `0017-project-policy` 新增 `Projects`: - `id/name/slug/status/version`; - `created_at_ms/updated_at_ms`; - slug 唯一,status 为 `active|archived`; - migration 创建 RFC 要求的 `default` Project,baseline 时间为 0,表示从 legacy 升级继承而非伪造创建时间。 baseline Project 故意不创建 owner binding。生产升级必须通过一次性、可审计且认证后的 bootstrap claim 建立首个 owner;禁止根据当前 username、默认密码、system App 或任意已有 token 静默授予。ADR-0029 已孵化默认不可达的 bootstrap core,但本机控制台/authentication adapter 与产品入口尚未实现,因此 migration 注册仍不代表 3.0 API 可用。 ### 3. RoleBinding append-only、按主体版本化 `ProjectRoleBindings` 的 identity 是 `(projectId,subjectType,subjectId,version)`。每次赋权、变更角色或撤销都追加一行: - state 为 `active|revoked`;active 必须有 role,revoked 不得携带 role; - role 为 `owner|admin|operator|viewer`; - `mutationId` 在 Project 内唯一,同请求重放返回 existing,内容漂移返回稳定 mutation conflict; - 调用方提供 expected current version,新版本必须严格 `+1`;SQLite adapter 使用短 `IMMEDIATE` transaction,使并发赋权只有一个版本获胜; - 每行保存 `changedBy` ActorRef 和时间,不覆盖历史授权事实。 撤销是一条新版本,不删除旧行。当前读取使用 `(project,subject,version DESC)` 点查;另有 subject→Project 索引供未来列出可见 Project。edge 不把所有 membership 加载到内存,也不常驻缓存;cluster-control 必须实现 PostgreSQL CAS/locking adapter,不能共享 SQLite。 ### 4. 首版角色权限矩阵固定在领域代码 静态权限包括 RFC 原有权限,并补充 `project.manage`、`policy.manage`、`approval.decide`。Tool 权限只接受精确 `tool.call:{toolName}`;外部请求不接受 `tool.call:*`。 | Role | 允许范围 | | --- | --- | | owner | Project 内全部已声明权限和精确 Tool call | | admin | 除 `project.manage` 外的全部已声明权限和精确 Tool call | | operator | 读、Task create/update、Run start/stop/retry、Secret use、精确 Tool call | | viewer | `project/task/run/artifact` 只读 | 首版不引入用户自定义 role、外部策略语言或 allow/deny JSON。添加权限必须修改 canonical permission registry、矩阵和测试,不能依赖数据库自由字符串。接口保持独立,后续可接 OPA 或 Project custom policy,但新实现必须保持默认拒绝和相同 decision contract。 ### 5. Policy decision 默认拒绝,Agent 写操作要求审批 稳定 decision effect 为 `allow|deny|require_approval`,reason 使用低敏机器码: - Project 不存在:`deny/project_not_found`; - 无 binding 或已撤销:`deny/subject_unbound`; - role 不包含权限:`deny/permission_missing`; - archived Project 对非只读操作:`deny/project_archived`; - role 允许:`allow/role_grant`; - Agent 的写操作、Secret use/manage、管理权限和 Tool call:`require_approval/agent_action_requires_approval`。 Policy storage 损坏、snapshot identity 漂移或读取失败不返回 deny 原因,而返回稳定 unavailable 并 fail closed。因为当前 ApprovalRequest 状态机尚未实现,`require_approval` 不能被视为 allow;调用端不得先执行再补审批。 archived Project 只允许 `project.read/task.read/run.read/artifact.read`,方便历史审计;不允许启动 Run、使用 Secret 或管理 policy。 ### 6. 旧身份不自动继承 3.0 权限 - 2.x 面板登录只证明 legacy session;ADR-0030 已提供默认不可达的稳定 `user/usr_legacy_primary` 映射 core,但生产 middleware 尚未装配; - legacy API App 的 `logs` scope 不等于 `artifact.read`,`crons` 不等于 `run.start`; - system App、Worker token、MCP session 和 Agent identity 都必须经各自 authentication adapter 产生 subject,并显式绑定 Project role; - migration 不复制 client secret、token、username 或 password 到 RoleBinding。 未来兼容向导可以展示建议映射,但必须由已认证 owner 明确确认并写入审计 mutation。 ### 7. 当前保持 production unreachable 本切片已实现 domain registry、`ProjectPolicyEngine`、SQLite append/resolve repository、`0017` migration、schema ownership 和 `ProjectPolicyArtifactReadAuthorizer`;ADR-0029 另已实现默认不可达的首 owner bootstrap core。但仍没有: - 首 owner bootstrap 的本机控制台 issuer、认证 adapter、产品入口与恢复码; - session/token/mTLS → subject authentication adapter; - role 管理 API/UI、审计事件和 rate limit; - ApprovalRequest 持久化与一次性消费; - PostgreSQL repository、跨副本 cache invalidation 或 OPA adapter; - typedi/Express/MCP/Worker startup 装配。 因此 default Project 在现有生产入口中仍是 ownerless,所有真实调用默认拒绝。不得为“让 API 能用”而在 loader 中隐式授予 owner。 ## 影响 正面影响: - Artifact、Secret、Run、Tool 可以共享一套 Project/Actor permission vocabulary; - 权限变更有不可覆盖历史、幂等 mutation 和 CAS 并发语义; - edge 每次决策只需有界点查,不需要策略 sidecar 或全量 membership cache; - legacy token 不会因升级静默扩大权限; - Agent 写行为在 Approval 状态机完成前无法被误当成 allow。 代价与风险: - 初次升级必须完成显式 owner bootstrap,否则 3.0 API 保持不可用; - 每次无缓存决策至少一次数据库读取,未来缓存必须以 binding/project version 正确失效; - 固定角色不支持复杂 ABAC、资源标签和 Tool 风险等级; - 当前所有 Agent Tool call 都要求审批,未来只能由可信 Tool manifest 风险分类放宽,不能由 Agent 自报只读; - RoleBinding 历史持续增长,需要与 Project 生命周期一致的审计 retention,而不能直接覆盖或清空。 ## 未选择的方案 1. **把 legacy scope 直接映射为 Permission**:没有 Project/资源边界且会扩大旧 token 权限,拒绝。 2. **默认用户名自动成为 owner**:显示名可变且没有一次性 bootstrap 审计,拒绝。 3. **RoleBinding 单行覆盖**:丢失授权历史和并发裁决证据,拒绝。 4. **把 permissions JSON 存进 binding**:无法维护 canonical 语义,损坏/拼写会形成隐式权限,首版拒绝。 5. **未绑定主体默认 viewer**:跨 Project 数据泄露,拒绝。 6. **Agent role 允许即直接执行写操作**:绕过 RFC 默认审批原则,拒绝。 7. **edge 引入 OPA sidecar**:资源与部署复杂度不适合基础能力,首版拒绝。 ## 验证要求 - migration 在 legacy fixture 上创建 ownerless default Project,重复执行和 ownership audit 通过; - subject、permission、Project、binding、snapshot、request 非 canonical 输入在 repository/policy 副作用前拒绝; - role matrix、archived Project、unbound/revoked/default-deny 和 Agent approval 全覆盖; - append replay、mutation drift、stale expected version 和两个 SQLite 连接并发赋权可重复验证; - current row 损坏、active-null-role、identity 漂移和非 SQLite adapter fail closed; - Artifact authorizer 只委托 `artifact.read` 且错误 action 拒绝; - Node 22/24 全量测试、类型检查、schema audit 和 GitNexus reachability 通过; - app/loaders/api/services 不得导入或装配本切片。