From dba8508807118a98de338f842ef6caf2176e2355 Mon Sep 17 00:00:00 2001 From: weichen Date: Mon, 12 Oct 2026 03:44:26 +0000 Subject: [PATCH] Pin Backend U4 login-key implementation and record specs Why: wire Dev to the Backend feature that delivers OpenExisting and one-time login keys for the approved deploy proposal. Evidence: SELF Backend tests; gitlink 1ba7225. Impact: Backend gitlink + docs; runtime via new binary. Refs: .task/feature-svc-deploy-u4-login-key.md Co-authored-by: Cursor --- .task/feature-svc-deploy-u4-login-key.md | 6 +- .../svc-deploy/u1-instance-account-binding.md | 78 +++++++++ docs/svc-deploy/u1-task-card-draft.md | 20 +++ docs/svc-deploy/u4-one-time-login-key.md | 160 ++++++++++++++++++ docs/svc-deploy/u4-task-card-draft.md | 21 +++ modules/backend | 2 +- 6 files changed, 283 insertions(+), 4 deletions(-) create mode 100644 docs/svc-deploy/u1-instance-account-binding.md create mode 100644 docs/svc-deploy/u1-task-card-draft.md create mode 100644 docs/svc-deploy/u4-one-time-login-key.md create mode 100644 docs/svc-deploy/u4-task-card-draft.md diff --git a/.task/feature-svc-deploy-u4-login-key.md b/.task/feature-svc-deploy-u4-login-key.md index a99f4ea6..112f3084 100644 --- a/.task/feature-svc-deploy-u4-login-key.md +++ b/.task/feature-svc-deploy-u4-login-key.md @@ -7,15 +7,15 @@ - 权威仓:ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= 9a9665b458916179df6d94066dfb9aa657a13d60 - 工作区:/home/weichen/projects/Agent-System-Dev-wt-feature-svc-deploy-u4-login-key(worktree);分支:feature/svc-deploy-u4-login-key -- 授权与交付终点:<本次允许的动作与终点;未授权事项写明> +- 授权与交付终点:2026-10-12 用户批准提案 r1 并授权 U4 实施;允许 Backend/Dev 推送与 PR;不合入;非目标 U5 页面 - 交付模式:manual(逐阶段人工评审);明确授权后可记为 authorized-continuous - 自动推进授权:无;授权范围:无;授权来源:无;授权终点:无;停止条件:无(未授权) -- 当前阶段:需求拆解;当前增量:I1(待定义) +- 当前阶段:实施与自检;当前增量:U4 OpenExisting + login keys - 流程依据:仓库根目录 policy/delivery.md(归档后仍按仓库根目录定位) ## 目标与非目标 -<预期结果、受影响入口/模块/契约;明确不做什么。> +目标:一次性登录 Key、受限会话、OpenExisting;非目标:svc 接线(U1)、登录页(U5) ## 关联方向与规划 diff --git a/docs/svc-deploy/u1-instance-account-binding.md b/docs/svc-deploy/u1-instance-account-binding.md new file mode 100644 index 00000000..56c7810e --- /dev/null +++ b/docs/svc-deploy/u1-instance-account-binding.md @@ -0,0 +1,78 @@ +# U1 实例—账户绑定补齐(规格 v0.1) + +状态:设计候选,随主提案 r1 评审。主记录:`modules/documents/docs/proposals/2026-10-12-svc-unified-deploy-and-login-key.md` §7 U1。 +前置:恢复方案 recovery U1 已合入 main(Dev #415)——`docs/recovery/u1-identity-manifest.md`、`scripts/svc_manifest.py`、启动身份校验与 `svc init`。 +本增量**不重做** manifest 层;只补齐账户操作与安装入口对既有身份合同的消费缺口。 + +基线(写作时 SELF):Dev `af9c2de`(含 recovery U1)。实施前以任务 `task-sync` 后 SHA 为准。业务实施 **NOT_RUN**。 + +## 1. 目标与非目标 + +**目标:** 同一实例的启停、升级、开户、签发/撤销登录 Key 始终命中同一存储与数据根;缺失、不符或锁不可用时拒绝;为后续 `install` / doctor 提供稳定的实例解析入口。 + +非目标:不实现二进制下载(U2)、不实现 doctor/env prepare 全量(U3)、不实现一次性 Key 语义(U4)、不改登录页(U5)。 + +## 2. 不变量(相对 recovery U1) + +| ID | 不变量 | 相对 #415 | +|---|---|---| +| B1 | 账户 CLI 只接受「实例名 → manifest 解析出的 data_root / db / secrets 引用」 | 新增消费方 | +| B2 | 禁止回落默认 `./data` 或未声明 DSN | 新增硬拒绝 | +| B3 | OpenExisting:目标不存在则失败;可选禁止自动 migrate | Backend 新增打开模式 | +| B4 | 无 `flock` / 锁获取失败 → 拒绝冲突操作(deploy/start/stop/user/*) | 收紧现状「直通」 | +| B5 | `install` / 兼容 `--from` 写入同一 manifest 身份字段,不另起配置根 | 入口骨架 | + +## 3. 触点与文件 + +| 文件 | 变更 | +|---|---| +| `scripts/svc.sh` | `user` 子命令骨架:解析 `ENV_NAME` → 读 manifest/`svc_manifest.py` → 组装 `--data`/`AGENT_SYSTEM_DSN`/`--open-existing` 调 `$ENV_BIN`;锁检查收紧;`install` 可先做 dry-run 计划打印(完整 binary/source 属 U2) | +| `scripts/svc_manifest.py` | 如需:导出「账户操作所需最小字段」查询(data_root、db mode/ref、auth);不改 schema=1 语义 | +| `modules/backend/cmd/agent-system/main.go` | `user` 子命令增加 `--open-existing`(或全局 store 打开标志);缺库/缺目录失败 | +| `modules/backend/internal/store/store.go` | `OpenExistingSQLite` / PG 等价:不 `MkdirAll` 创建新数据根;可选 `MigrateMode=requireCompatible\|forbid` | +| `tests/test_svc_identity.py` 或新 `tests/test_svc_user_binding.py` | 错误 data、缺 manifest、无 flock 拒绝;正确绑定命中 | + +## 4. OpenExisting 合同 + +``` +OpenExisting(path|dsn, opts): + if storage path/parent missing → error (do not create) + if SQLite file missing → error + open DB + if opts.migrate == forbid → read schema version only; mismatch → error + if opts.migrate == requireCompatible → existing migrate(); reject unknown newer schema (keep current migrate gate) + never create empty business DB as side effect of account recovery +``` + +CLI 默认:账户运维命令使用 `OpenExisting` + `requireCompatible`。 +`svc init` / 首次服务启动仍走现有 Open(可创建)路径,与 recovery U1 一致。 + +## 5. 锁合同 + +- 复用 `runtime//.start.lock`。 +- `flock` 不可用或超时:退出非 0,写 hist_event,**不得**执行 deploy/start/stop/user。 +- 账户操作与生命周期互斥(同一锁)。 + +## 6. 验收用例(U1) + +| # | 场景 | 判据 | +|---|---|---| +| T1 | 实例有合法 manifest;`svc user list` | 使用 manifest data_root/DSN;列表与该库一致 | +| T2 | 故意传错误 `--data` 或未绑定实例 | 拒绝;不创建目录;不写默认 `./data` | +| T3 | 删除 data 根后再 user create | OpenExisting 失败;无新空库 | +| T4 | 隐藏 `flock`(PATH 去掉)后 deploy/user | 拒绝;旧服务若在跑则仍在 | +| T5 | recovery U1 既有 T1–T6 回归 | 全绿(不破坏 identity) | + +真实 PG:绑定 dbref 与 marker 一致才允许 user 操作;缺环境标 NOT_RUN。 + +## 7. 完成门槛与依赖 + +- 完成条件见主提案 §7 U1 行。 +- 依赖:main 已含 recovery U1(#415)。 +- 被依赖:U4 CLI 签发、U5 `svc user` 接线、U2 `install` 写身份。 + +## 8. 任务卡建议(待授权后 `task-start`) + +- 分支名:`feature/svc-deploy-u1-account-binding` +- R0:与本提案合入后的 `origin/main`(或与 U4 兄弟共享批准时的同一 R0) +- 交付模式:manual;实施需主提案阶段批准后再授权 diff --git a/docs/svc-deploy/u1-task-card-draft.md b/docs/svc-deploy/u1-task-card-draft.md new file mode 100644 index 00000000..79491594 --- /dev/null +++ b/docs/svc-deploy/u1-task-card-draft.md @@ -0,0 +1,20 @@ +# 草稿:feature/svc-deploy-u1-account-binding 任务卡 + +状态:**待授权**。主提案批准并获实施授权后,在共享主检出执行: + +```bash +RECEIPT=/tmp/precheck-svc-deploy-u1.json CMD="make precheck" make evidence +make task-start NAME=feature/svc-deploy-u1-account-binding PRECHECK_RECEIPT=/tmp/precheck-svc-deploy-u1.json +``` + +将本草稿字段拷入新卡并删除本文件或改为「已启动」链接。 + +## 建议填写 + +- 主记录:`modules/documents/docs/proposals/2026-10-12-svc-unified-deploy-and-login-key.md` r1 §7 U1 +- 规格:`docs/svc-deploy/u1-instance-account-binding.md` v0.1 +- 目标:账户 CLI 绑定实例 manifest;OpenExisting;无 flock 拒绝冲突操作;不破坏 recovery U1 +- 非目标:U2 下载、U3 doctor、U4 Key 语义、U5 页面 +- 兄弟任务:可与 `feature/svc-deploy-u4-login-key` 共用同一 R0 +- 验收:规格 §6 T1–T5;治理回归;真实 PG 缺环境 NOT_RUN +- 启动授权:未授权(须主提案阶段批准 + 明确实施授权) diff --git a/docs/svc-deploy/u4-one-time-login-key.md b/docs/svc-deploy/u4-one-time-login-key.md new file mode 100644 index 00000000..80d37f43 --- /dev/null +++ b/docs/svc-deploy/u4-one-time-login-key.md @@ -0,0 +1,160 @@ +# U4 一次性登录凭据与认证限制(规格 v0.1) + +状态:设计候选,随主提案 r1 评审。主记录:`modules/documents/docs/proposals/2026-10-12-svc-unified-deploy-and-login-key.md` §7 U4 / §8。 +本增量在 **Backend**(及必要迁移/CLI)落地;Frontend 兑换与设密页属 **U5**,但 U4 须提供稳定 HTTP 合同供 U5 接线。 + +基线(写作时 SELF):Backend 随 Dev `af9c2de` gitlink;关键现状见主提案 §2。业务实施 **NOT_RUN**。 + +## 1. 目标与非目标 + +**目标:** 无初始密码开户与已有账户恢复均通过一次性登录 Key;Key 只能经专用接口原子兑换为「仅可设密」的短期会话;服务端拒绝受限会话与待改密会话访问业务 API;设密在同一事务内完成且成功后强制重新密码登录。 + +非目标:不改长期 `api_keys` 的集成语义(除失陷显式撤销);不实现 svc 接线与登录页(U5);不做 doctor/部署(U1–U3)。 + +## 2. 现状锚点(必须改掉的行为) + +| 现状 | 出处 | U4 动作 | +|---|---|---| +| CLI 省略密码 → `RandomPassword()` + 明文打印 | `app.CreateUserAccount` | 默认无密码 + 签发 activate Key;随机密码可保留为显式 `--password` / `--generate-password` | +| `api_keys` 可重复使用、无 TTL | `store/auth.go` | **不改**长期 Key;新增独立表 | +| Middleware 不看 `must_change` | `server/auth.go` | 统一拦截 | +| `changePassword` 强制 `current` | `server/accounts.go` | 分型:受限会话走 set-initial / set-recovered | +| `SetUserPassword` 总置 `must_change=1` | `store/auth.go` | 设密成功路径一次写清,避免半完成 | +| Open 必 MkdirAll + migrate | `store/store.go` | 账户运维走 OpenExisting(与 U1 合同一致;可本增量先做 Backend API) | + +## 3. 数据模型 + +### 3.1 `login_keys`(名称可微调,勿混入 `api_keys`) + +| 列 | 说明 | +|---|---| +| `id` | 稳定凭据编号(对外审计/撤销用,非明文) | +| `owner` | 目标用户 | +| `purpose` | `activate` \| `recover` | +| `key_hash` | SHA-256(或更强);明文前缀建议 `alk_`(activation/login key) | +| `status` | `unused` \| `consumed` \| `revoked` \| `expired` | +| `expires_at` | 默认 now+30m;签发时可配置(夹紧 5–120m) | +| `consumed_at` | 可空 | +| `created_at` / `created_by` | 审计(CLI 操作者或 `system`) | +| `instance_id` | 可选;与部署身份关联时填写 | + +唯一性:同一明文只能对应一行;未使用 Key 可按 (owner, purpose) 允许多条,但签发新 Key 时建议撤销同 purpose 的未使用旧 Key(规格默认:**签发即撤销同 owner+purpose 的 unused**)。 + +### 3.2 `users` 变更 + +- `password_hash`:允许 NULL / 空,表示未设置。 +- 保留 `must_change_password`:用于「已有随机密码首登」迁移路径;无密码账户用「无 hash」+ activate 流,不依赖假密码。 +- 角色:首期仍映射 `is_admin`;API 可收 `role=admin|user`,未知角色 **拒绝**(不静默降级)。 + +### 3.3 `sessions` 变更 + +增加 `scope`(或等价):`full` \| `set_password_only`。 +受限会话 TTL 默认 10 分钟。 +`CreateSession(owner, scope, ttl)`。 + +## 4. CLI 合同 + +``` +agent-system user create --owner --role user|admin [--open-existing] + → 无 --password:password 未设置 + 同事务签发 activate Key;stdout 明文一次 + → --password / --generate-password:兼容旧路径(must_change=1) + +agent-system user issue-login-key --owner --purpose activate|recover [--ttl 30m] [--open-existing] + → 精确命中已有用户;不改 password_hash;不吊销 full 会话;不改业务数据 + → recover 不删除重建 + +agent-system user revoke-login-key --id [--open-existing] +agent-system user list # 含是否已设密、未使用登录 Key 计数(无明文) +``` + +首次管理员: + +``` +agent-system user bootstrap-admin --owner [--open-existing] +``` + +仅当:库已存在且 **无任何 users 行** 且 **无历史业务标记**(规格:至少 `projects`/`messages` 等核心表为空或实例 init marker 声明 fresh)。并发用事务/`UNIQUE` 保证单胜。有数据却无管理员 → **拒绝自动接管**,要求显式运维流程(本增量可只返回明确错误)。 + +失陷恢复: + +``` +... issue-login-key --purpose recover --revoke-all-credentials +``` + +默认 **不**撤销长期 `api_keys`;加旗标才撤销全部 api_keys + 可选吊销 full 会话。 + +## 5. HTTP 合同 + +| 方法 | 路径 | 认证 | 行为 | +|---|---|---|---| +| POST | `/api/auth/login-key/redeem` | 无(body: `{ "key": "alk_..." }`) | 事务:校验 unused+未过期 → 条件更新 consumed → 建 `set_password_only` 会话 → 设 cookie → 提交 | +| POST | `/api/auth/password` | session | `scope=full`:要求 `current`(现行为);`scope=set_password_only`:忽略 current,要求 `next`+确认由客户端做,服务端校验强度 | +| POST | `/api/auth/logout` | session | 允许受限会话 | +| GET | `/api/me` | session | 允许受限会话;响应含 `sessionScope`、`mustChangePassword`、`passwordSet` | +| 其他 `/api/*` | — | — | `set_password_only` **或** (`must_change` 且非上述白名单) → **403** | + +禁止:用 Bearer / `?key=` 提交一次性登录 Key;redeem 外任何路径接受 `alk_`。 + +兑换并发:`UPDATE ... WHERE id=? AND status='unused' AND expires_at>now` 影响行数必须为 1;否则失败。消费后建会话失败 → **整事务回滚**(Key 仍为 unused)。 + +设密成功事务(受限或兼容 must_change): + +1. 写 password_hash +2. 清 must_change;标记已设密 +3. 作废该 owner 全部 unused/本 purpose 登录 Key +4. 删除该 owner 全部会话(含当前受限会话) +5. 清 cookie / 不发新 full 会话 +6. 响应指示 `reloginRequired: true` + +半完成态禁止:密码已变但旧 Key 仍 unused;或 Key 已 consumed 但无会话且密码未变(兑换回滚已覆盖后者)。 + +## 6. 中间件伪代码 + +``` +after AuthOwner/SessionOwner: + if session.scope == set_password_only: + if path not in {GET /api/me, POST /api/auth/password, POST /api/auth/logout}: + 403 + else if user.must_change_password or not user.password_set: + if path not in same allowlist: + 403 + // API key (long-lived): if must_change or not password_set → 403 on business routes + // (key cannot complete password change) +``` + +## 7. 测试计划(Backend) + +| # | 场景 | 判据 | +|---|---|---| +| T1 | create 无密码 | DB hash 空;一条 unused activate Key;stdout 有明文一次 | +| T2 | 同名 create | 冲突;原账户与 Key 不变 | +| T3 | redeem 成功 | Key consumed;cookie 为 set_password_only;/api/me 200;/api/projects 403 | +| T4 | 同 Key 并发 100 次 redeem | 恰好 1 成功 | +| T5 | 过期 / 撤销 / 重放 | 均失败 | +| T6 | 设密成功 | hash 在;Key 作废;会话全删;再带旧 cookie → 401;密码登录 success 且 scope=full | +| T7 | 设密中途 DB 失败(注入) | 无「已改密但仍 unused Key」 | +| T8 | redeem 后 CreateSession 失败 | Key 仍 unused | +| T9 | recover 签发 | 不改 hash;不删业务行;可选保留 api_keys | +| T10 | must_change 旧路径 + 中间件 | 业务 API 403;设密后可访问 | +| T11 | OpenExisting 缺库 | CLI 失败不建目录 | + +SQLite 与真实 PG 双跑;缺 PG 环境标 NOT_RUN。 + +## 8. 安全与交付 + +- 明文只在签发时交付;日志/evidence 只记 `id`/purpose/结果。 +- 不把 Key 放 URL、命令行参数名以外的 argv 常驻、或 localStorage(U5 约束)。 +- 对齐 OWASP Forgot Password / Session Management;保留现有 CSRF。 + +## 9. 完成门槛与依赖 + +- 完成条件见主提案 §7 U4 行。 +- 依赖:U1 OpenExisting 可并行由 Backend 先实现最小集,再与 svc 接线合并。 +- 被依赖:U5 登录页兑换与设密分型。 + +## 10. 任务卡建议(待授权后 `task-start`) + +- 分支名:`feature/svc-deploy-u4-login-key` +- 组件:Backend feature 分支 + Dev gitlink;Frontend 不在本增量强制改动(契约测试可用 httptest) +- R0:与 U1 兄弟共享批准时的同一 `origin/main` +- 交付模式:manual diff --git a/docs/svc-deploy/u4-task-card-draft.md b/docs/svc-deploy/u4-task-card-draft.md new file mode 100644 index 00000000..587370eb --- /dev/null +++ b/docs/svc-deploy/u4-task-card-draft.md @@ -0,0 +1,21 @@ +# 草稿:feature/svc-deploy-u4-login-key 任务卡 + +状态:**待授权**。主提案批准并获实施授权后,在共享主检出执行: + +```bash +RECEIPT=/tmp/precheck-svc-deploy-u4.json CMD="make precheck" make evidence +make task-start NAME=feature/svc-deploy-u4-login-key PRECHECK_RECEIPT=/tmp/precheck-svc-deploy-u4.json +``` + +将本草稿字段拷入新卡并删除本文件或改为「已启动」链接。 + +## 建议填写 + +- 主记录:`modules/documents/docs/proposals/2026-10-12-svc-unified-deploy-and-login-key.md` r1 §7 U4 +- 规格:`docs/svc-deploy/u4-one-time-login-key.md` v0.1 +- 目标:独立 login_keys;原子兑换;受限会话中间件;设密事务;无密码开户/恢复签发;OpenExisting 最小集 +- 非目标:svc 账户接线与登录页(U5);部署来源(U2/U3) +- 组件:Backend 为主;Dev 仅 gitlink / 文档引用;Frontend 不强制本增量改动 +- 兄弟任务:可与 `feature/svc-deploy-u1-account-binding` 共用同一 R0 +- 验收:规格 §7 T1–T11;SQLite+PG;并发兑换;治理/Backend 测试 +- 启动授权:未授权(须主提案阶段批准 + 明确实施授权) diff --git a/modules/backend b/modules/backend index d1b5a94d..1ba7225e 160000 --- a/modules/backend +++ b/modules/backend @@ -1 +1 @@ -Subproject commit d1b5a94d150fca28115c926dc2d69b643b12f7b5 +Subproject commit 1ba7225e838178d05c1d71415b844b60381b3640