实现(08-10-production-employee-smoke-break-glass-reset): 增加员工Smoke受控重置工具
ci-e2e / e2e (push) Canceled after 0s
ci-minimal / check-minimal (push) Canceled after 0s

这个提交包含在:
2026-08-10 21:01:59 +08:00
父节点 b807762c52
当前提交 ec68256588
共修改 10 个文件,包含 1310 行新增和 0 行删除
@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
@@ -0,0 +1,145 @@
# 生产员工 Smoke Break-glass 重置:设计说明
## 1. 边界与分阶段授权
本任务修复专用 employee smoke 的认证能力,不改变员工角色或权限,也不执行合同负向验收。整个任务分三层:本地工具实现、正常候选发布、生产账号 reset。`task.py start` 只进入第一层;发布和账号写入各有独立审批停点。
新脚本必须通过正常发布进入镜像。带外复制虽然改动面看似更小,却绕过 provenance、运行脚本 SHA 和可复现发布合同,不能用于唯一一次生产认证写入。
## 2. 现有 helper 的真实副作用
`repair-oa-only-smoke-accounts.js` 的 dry-run 和 apply 共用 `planForTarget`:
```text
restore_active_employee -> status/employment_status/archived_at
clear_login_lock -> login_failed_* / login_locked_until
add_missing_roles -> INSERT employee_roles, never delete extras
every applied target -> token_version +1 + lifecycle event
password_hash -> never touched
session/challenge -> never revoked/invalidated
```
因此它既不能恢复密码,也不能建立精确角色不变量。本任务仅运行 dry-run 作为“现场仍无需生命周期/角色修复”的独立证明;任何 planned action 都阻塞新脚本 apply,helper apply 永久禁止。
## 3. 新脚本目标模型
新增 `reset-employee-smoke-password.js`,复用现有 helper 导出的 smoke 纯判据,但不调用其 `execute/applyTarget`。目标由数据库状态自行发现,不传 identifier:
```text
status == active && employment_status == active && archived_at == null
&& email/phone identifier ready
&& current login lock is clear
&& must_change_password == false
&& smoke(row.employee_no | row.email | row.name)
&& sorted(roleCodes) == ["employee"]
&& resolvePermissionsForRoles(roleCodes)
excludes ["*", "project.manage", "project.view_all", "system.settings"]
```
查询最多保留两个匹配用于判断唯一性,稳定输出只包含 count/boolean。角色精确断言同时保证没有管理员、经理、财务、人事等额外角色;权限断言独立存在,防止未来 `ROLE_PERMISSIONS.employee` 漂移后角色仍正确但权限已扩大。
## 4. 状态机
```text
PLANNING
-> local implementation/tests/read-only review
-> STOP: deployment approval
-> commit/push/remoteVerified -> normal deploy -> runtime script SHA match
-> production read-only identity/migration/window
-> repair helper dry-run: OK/no actions, apply=0
-> employee script --preflight: PASS/1/exact role/no manage permissions
-> old-hash limited material + temp/final secrets ready
-> STOP: production account-write approval
-> --apply EMPLOYEE_SMOKE_BREAK_GLASS exactly once
-> --verify + independent role/permission snapshot
-> only if post-write negative premise PASS: one login
-> /change-password once -> stop at /portal
-> --verify-password-changed + final role/permission snapshot
-> secret update + sensitive cleanup + read-only reviewer
-> RESET_COMPLETE -> handoff 08-08 readiness
```
任一箭头失败即停止。发布失败不会触及账号;apply 结果不确定时仅 `--verify`,不得重试;写后权限不变量失败时不得登录。
## 5. 命令接口与稳定输出
脚本接口:
- `--preflight`:目标计数、唯一性、smoke/identifier/status/role/permission 布尔。
- `--verify`:上述不变量 + must-change、locks、active session/code、audit 状态。
- `--verify-password-changed`:上述不变量 + must-change=false、一个新 active session、两类 audit。
- `--diagnose-wecom-binding`(可选只读):仅记录绑定/同步/近期认证计数,不输出 external user id。
- `--apply EMPLOYEE_SMOKE_BREAK_GLASS`:唯一写入口,密码从 stdin。
错误只输出 allowlist category,例如 `employee_smoke_target_not_unique`、`employee_smoke_target_changed`、`explicit_confirmation_required`、密码输入/策略错误;未知异常只输出安全类别,不回显数据库消息或目标字段。
## 6. 事务写入合同
apply 在事务外发现唯一目标并取得 token version,生成同源 bcrypt hash;事务内再次读取并核对所有目标/权限不变量。通过后仅执行:
1. 更新目标 `password_hash`、`must_change_password=true`;清三类认证失败/锁定字段与 password-reset request 字段;token version +1。
2. 以 `employee_id=target.id` 撤销 active auth sessions、失效 password reset codes、消耗 email login codes。
3. 写 `SYSTEM_EMPLOYEE_SMOKE_BREAK_GLASS_PASSWORD_RESET` 审计,actor/target 均为目标,extra 只记录 `credentialRecorded=false`。
事务不包含 `employee_roles`、status/employment/archive、identifier、企微或业务表。单测必须对这些缺席路径做断言,而不只验证预期 update 存在。
## 7. 严格一次与不确定结果
执行前记录专用 attempt 哨兵和本次审计基线;一旦发出 apply,`applyExecutionCount` 固定为 1。结果分支:
- 完整 `PASS`:立即 `--verify`。
- 明确 `BLOCKED`/非零:停止,不重试。
- SSH 中断、输出缺失、JSON 不可解析:标记 `APPLY_UNCERTAIN`,只运行 `--verify`。
verify 若显示 must-change、audit/session/code 等状态已落地,则按成功写入处理并禁止第二次 apply;审计计数相对写前恰好 +1 作为独立交叉证明。
## 8. 写后权限再确认
写前诊断不是写后证据。apply 后先由脚本 `--verify` 重算目标、角色和权限,再以独立 `BEGIN READ ONLY` 查询重算同一组不变量:
```text
targetMatches=1
rolesExact=true
hasWildcard=false
hasProjectManage=false
hasProjectViewAll=false
hasSystemSettings=false
```
任何一个值不符都停止在无登录状态。不得通过删除角色、改权限或改验收断言补救;这类生产写入需要新任务。
## 9. 凭据、强制改密与恢复材料
临时/最终密码由受控 CSPRNG 分别生成,均满足应用策略且不相同。临时密码通过 stdin 进入 apply;正式仓库外 employee secret 仅在 verify PASS 后原子更新为正确 identifier + 临时密码,强制改密完成后再替换为最终密码。
旧 hash 只在生产现场 `0700/0600` 暂存,最晚 apply 后 24 小时销毁。它不是完整或部分回滚;即使另行获批写回,也不能恢复 token、会话、challenge、计数和审计。
浏览器使用单 context、workers=1、retries=0:一次登录必须到 `/change-password`,一次提交允许到 `/portal` 后立即关闭;业务 API 发送为 0。随后 `--verify-password-changed` 和最终独立权限快照都 PASS 才清理材料。
## 10. 发布与脚本身份
本地实现和只读 reviewer 通过后形成 clean/published 候选。生产发布沿用现有 identity bootstrap、preflight、migration/window 和 `deploy-existing.sh`,不手工同步单文件。
发布后以 `git show <candidate>:server/src/scripts/reset-employee-smoke-password.js` 为基线,对运行容器同路径做 SHA-256 比较;只记录 `scriptShaMatch=true`。脚本不存在或不一致即停止账号阶段。候选没有 migration 时 pending 必须为空;任何意外 migration 或生产门禁 finding 都停止。
## 11. 失败矩阵
| 阶段 | 失败 | 动作 |
| --- | --- | --- |
| local tests/review | 非 PASS | 修复本地代码,不发布 |
| deploy approval/provenance | 缺失或不一致 | 停止,零生产变更 |
| deployment | 失败或运行 SHA 不符 | 停止,零账号写入 |
| helper dry-run | planned action / unsafe target | PAUSED,helper apply=0 |
| employee preflight | 非唯一/角色或权限不符 | 停止,不选择目标 |
| recovery/secret | 无法安全准备 | 停止,不 apply |
| apply 明确失败 | BLOCKED | 停止,不重试 |
| apply 通道不确定 | 无完整结果 | 仅 `--verify` 定性 |
| post-write role/permission | 任一不变量失败 | 禁止登录,另行决策 |
| login/change-password | 首次失败 | 停止,零 retry,不再 apply |
| cleanup | 失败 | 标记敏感清理未完成,不归档 |
## 12. Reviewer 与交接
本地实现后必须启动只读 reviewer,重点检查目标发现、角色/权限精确断言、事务写集、失败类别和测试反例;reviewer 不改代码。生产执行后再由只读 reviewer 核对候选/脚本 SHA、apply 次数、审计增量、目标限定副作用、写后权限证据和材料清理。
最终只向原 readiness task 交接稳定布尔与计数,不交接凭据。该 task 才负责登录后的合同负向验收和最终 readiness 口径。
@@ -0,0 +1 @@
{"_example": "Fill with {\"file\": \"<path>\", \"reason\": \"<why>\"}. Put spec/research files only — no code paths. Run `python3 .trellis/scripts/get_context.py --mode packages` to list available specs. Delete this line once real entries are added."}
@@ -0,0 +1,102 @@
# 生产员工 Smoke Break-glass 重置:执行计划
> 当前状态:RELEASE APPROVED / PRE-COMMIT。用户已批准提交、推送/provenance、生产只读 preflight、正常候选发布和运行脚本 SHA-256 比对;尚未批准生产账号写入、登录或强制改密。
## 1. 规划事实与决策
- [x] 目标是专用 OA-only smoke employee,不是真人员工;`08-08` 已只读证明唯一资格目标,登录 attempts=0。
- [x] 已完整审阅 `repair-oa-only-smoke-accounts.js`:它不重置密码;apply 可恢复状态、清登录锁、token +1、补缺失角色并写 lifecycle event,但不删除额外角色、不撤销 session/challenge。
- [x] 现有 helper 只作为一次 dry-run 对照,禁止 apply;若 action 非空即停止。
- [x] 不扩展管理员脚本目标面;新增员工 smoke 专用脚本,角色/权限只断言、不写入。
- [x] 新脚本不在生产镜像,必须经正常候选发布;禁止单文件复制、inline code 或手写 SQL。
- [x] 生产写后重新验证 `roles == ['employee']` 和四项管理权限全 false,未通过前禁止登录。
## 2. 启动门禁
- [x] 用户完成本规划只读复核并明确批准 `task.py start`;该批准只覆盖本地实现与测试。
- [x] 读取 `trellis-before-dev`、backend 与 hyb-oa 相关 spec,再编辑 server 脚本/测试。
- [x] 确认工作区仅含本 task 规划及其实现改动;未混入 08-08 或其他任务改动。
## 3. Phase A:先实现最窄工具
- [x] 新增 `server/src/scripts/reset-employee-smoke-password.js`,复用现有 smoke 判据、管理员 stdin 密码校验和应用同源 crypto/password policy,未扩展管理员目标面。
- [x] 实现不接收/不输出 identifier 的唯一目标发现;精确角色与独立权限断言必须同时成立。
- [x] 实现默认/`--dry-run`/`--preflight`、`--verify`、`--verify-password-changed` 和精确 apply 确认串;本地最小范围未加入非必要企微诊断。
- [x] apply 使用 Serializable 事务,先锁 Employee、再锁其角色行并重新核对 id/token/status/smoke/role/permission;只写目标认证字段、目标 session/challenge 和专用 audit。
- [x] 未知异常脱敏;稳定输出不含账号、identifier、id、路径、hash、payload,privacy 口径沿用现有 helper 且更窄。
## 4. Phase B:再补覆盖与只读 review
- [x] 新增 `server/tests/unit/scripts/reset-employee-smoke-password.test.js`,23 条用例覆盖零/多目标、非 smoke、identifier/status/employment/archive/锁/must-change、额外角色、四个禁用权限、事务内 role/token 漂移、确认串、目标限定写入、verify 与最终改密 verify。
- [x] apply 的 Employee update 键集合被精确断言;无 role/status/identifier/企微/业务写入,session/challenge where 精确限定 target id。
- [x] `reset-active-admin-password.test.js` 6/6 PASS,管理员脚本目标面未扩大。
- [x] `repair-oa-only-smoke-accounts.test.js` 5/5 PASS,并新增完整副作用合同:只补角色、不删额外角色、不重置密码、不写 session/challenge。
- [x] node check、focused 34/34、Prisma validate、client typecheck/build/PWA 生成全部 PASS;build 仅有既有 Browserslist 数据陈旧提示。
- [x] 用户只读 reviewer 复核 diff 后结论 PASS:确认唯一 Employee update 仅为认证字段,角色路径纯读,默认 dry-run/精确确认串 fail-closed,两个 verify 均重算精确角色与四项权限;reviewer 未修改代码。
- [x] reviewer 额外确认事务以 Serializable 隔离级别先锁 Employee、再锁角色行,并在写入前重验目标/token/角色/权限;候选到写入之间的前提漂移会中止事务。
## 5. STOP A:发布审批
- [ ] 本地实现、测试和 reviewer PASS 后提交中文 commit,引用 `08-10-production-employee-smoke-break-glass-reset`。
- [x] 已向用户报告精确 diff、测试、脚本完整写集/不写集与发布必要性;用户只读审查通过,当前进入发布审批门,未取得明确发布批准前不 commit/push/deploy。
- [x] 用户已明确批准五步发布范围:commit、push/provenance、发布前只读 preflight、`deploy-existing.sh` 正常发布、发布后运行脚本 SHA-256 比对;批准不包含员工账号 apply、登录或强制改密。
- [ ] 获批后 push 并验证 provenance remoteVerified;再次确认候选无 migration、无意外产品改动。
## 6. Phase C:正常发布与运行脚本身份
- [ ] 运行生产 identity/read-only preflight:身份、65 migration、pending、运行/公网 SHA、rollback window 全部按部署合同通过。
- [ ] 仅在独立发布批准下使用 `deploy-existing.sh` 发布候选;失败立即停止,不进入账号阶段。
- [ ] 发布后复核 ready/public SHA、匿名鉴权和 migration;比较运行镜像新脚本 SHA 与候选基线,要求 `scriptShaMatch=true`。
- [ ] 不执行 `docker cp`、SSH 单文件同步、工作区脚本覆盖或 inline 写入代码。
## 7. Phase D:生产只读前置
- [ ] 取得独立生产账号写入批准;确认批准包含脚本声明的完整认证副作用与一次强制改密,不包含角色或业务写入。
- [ ] rollback window PRE。
- [ ] 运行现有 helper 一次 dry-run:目标 safe、status=OK、actions=[]、passwordReset=false;记录 helperApplyCount=0。
- [ ] 运行新脚本 `--preflight`:targetMatches=1、exactTarget/rolesExact/identifierReady/locksClear=true,四项管理权限 false。
- [ ] 用独立 `BEGIN READ ONLY` 查询复核相同角色/权限基线;读取 break-glass audit 写前计数。
- [ ] 可选写前企微诊断只记录稳定布尔/计数,不修改绑定。
- [ ] 准备 `0700/0600` 旧 hash 有限恢复材料、互不相同的临时/最终密码和 apply attempt 哨兵;任何敏感值不回显。
## 8. Phase E:唯一账号写入
- [ ] 从 stdin 执行且仅执行一次 `--apply EMPLOYEE_SMOKE_BREAK_GLASS`;`applyExecutionCount=1`。
- [ ] 完整 PASS 后立即 `--verify`;若结果不确定,进入 `APPLY_UNCERTAIN`,只允许 `--verify` 定性,禁止第二次 apply。
- [ ] verify 要求 must-change、locks、session/challenge、token/audit 和目标/角色/权限全部 PASS。
- [ ] 独立 `BEGIN READ ONLY` 再次确认 targetMatches=1、`roles == ['employee']`、四项管理权限 false;审计计数较写前恰好 +1。
- [ ] 任一写后权限不变量失败,停止且不登录;不得在本任务修角色或权限。
## 9. Phase F:一次登录与强制改密
- [ ] 仅在 Phase E 全 PASS 后,原子更新仓库外 employee smoke secret 为正确 identifier + 临时密码,权限 `0600`。
- [ ] 单 browser/context、workers=1、retries=0 登录一次,必须进入 `/change-password`。
- [ ] 使用已准备的最终密码提交一次;到 `/portal` 后立即停止,业务 API 实际发送数为 0。
- [ ] `--verify-password-changed` 与最终独立权限快照 PASS;最终 secret 原子更新且 `0600`。
- [ ] 写后企微诊断(若启用)证明绑定状态未变;不得做企微登录/绑定写入。
## 10. Phase G:清理、review 与交接
- [ ] 销毁临时 secret、浏览器状态、attempt 哨兵和旧 hash;记录 `temporaryCredentialDestroyed/browserStateCleared/recoveryMaterialDestroyed=true`。
- [ ] 最终生产 read-only preflight、migration 和 rollback window POST;合同/schema/权限/其他账号写入计数为 0。
- [ ] 只读 reviewer 核对运行脚本 SHA、apply 次数、审计 +1、目标限定副作用、写后精确角色/权限、零业务写入和清理证据。
- [ ] 更新本 task 稳定证据并运行 task/process/governance/sensitive/diff 门禁。
- [ ] 只向 `08-08-production-authenticated-readiness` 交接最终员工凭据可用与负向权限前提未变;回原 task 执行员工合同权限负向。
## 11. 验证命令
```bash
node --check server/src/scripts/reset-employee-smoke-password.js
cd server && npm test -- tests/unit/scripts/reset-employee-smoke-password.test.js tests/unit/scripts/reset-active-admin-password.test.js tests/unit/scripts/repair-oa-only-smoke-accounts.test.js
cd server && npm run prisma:validate
cd client && npm run typecheck && npm run build
python3 ./.trellis/scripts/task.py validate 08-10-production-employee-smoke-break-glass-reset
node scripts/qa/check-process-contract.mjs --baseline --json
node scripts/qa/check-trellis-governance.mjs --json
node scripts/qa/check-sensitive-content.mjs
git diff --check
```
## 12. 当前结论
本地实现、自检与用户只读审查均已完成,结论 PASS。新脚本默认只读 preflight,只有精确确认串进入写入;角色路径只有读取、锁定与精确断言。`--verify` 和 `--verify-password-changed` 均重新输出实际 must-change、`rolesExact` 及四项管理权限布尔。用户已批准五步候选发布,当前进入提交前检查;账号写入、登录和强制改密仍需发布成功后的独立批准。
@@ -0,0 +1,132 @@
# 生产员工 Smoke Break-glass 重置
## Goal
为 `08-08-production-authenticated-readiness` 已只读收敛出的唯一专用 OA-only smoke employee 恢复可控登录凭据,同时保持其角色精确为 `employee`、不获得合同管理权限、不影响真人员工。任务只负责重置生命周期;员工合同权限负向验收仍回到原 readiness task 完成。
本任务含本地安全工具实现、受控发布和一次生产账号写入三个风险层。任务启动只授权本地实现;发布候选与生产账号写入必须分别取得明确批准,任何一步均不得由前一步批准推定。
## Background and Confirmed Facts
- `08-08-production-authenticated-readiness` 已在生产以只读方式从 5 个活跃非管理员候选中收敛出唯一合格目标:identifier ready、未锁定、失败计数为 0、无需首次改密、可进入合同只读范围且不具备合同管理权限;该目标符合运行容器既有 smoke 命名判据,不是真人员工。员工登录 attempts 保持 0。
- 原仓库外员工 smoke 密码对 5 个候选均不匹配,必须恢复凭据后才能完成员工登录态负向验收。
- `repair-oa-only-smoke-accounts.js:357-367,391-446` 的 repair 语义不是密码重置:它可恢复 active/employment/archive 状态、清登录锁、递增 token version、补缺失角色并写 lifecycle event,但不更新 `password_hash`。
- 该 helper 的角色写入使用 `INSERT ... ON CONFLICT DO NOTHING`,只补角色、不删除额外角色,因此不能证明或恢复“角色恰好等于期望列表”;其报告还明确声明 `passwordReset=false`(`:520-523`)。
- helper 的默认 dry-run 使用 `BEGIN READ ONLY`(`:540-588`),且 `isSafeOaOnlySmokeTarget`(`:110-130`)可作为目标安全判据;`--apply` 同时要求 CLI flag 与 `OA_ONLY_SMOKE_REPAIR_APPLY=1`(`:37-41,259-263`)。
- 管理员既有 `reset-active-admin-password.js` 已证明同源 bcrypt、stdin 密码、字面确认串、事务内二次核对、目标限定的 session/challenge 撤销、审计和 `APPLY_UNCERTAIN -> verify` 模式可行;但不得把其目标面扩展到任意员工。
- 新员工脚本当前不在生产镜像。禁止通过 SSH 临时复制、`docker cp`、工作区覆盖、`node -e` 或手写 SQL 注入;必须先作为正常候选发布,再以运行镜像脚本 SHA 与该候选精确一致为执行前提。
- 生产合同终止回滚窗口最近九次均为 `OPEN / terminatedRows=0`。本任务不读写合同;登录和强制改密会产生预期认证写入,但不得进入业务页面。
## Scope
1. 新增员工 smoke 专用 break-glass 密码重置脚本及 focused tests;不改管理员脚本的目标语义,不改现有 repair helper 的 apply 行为。
2. 在本地证明脚本只选择唯一安全目标,角色必须精确为 `['employee']`,权限不得包含 `project.manage`、`project.view_all`、`system.settings` 或 `*`,并在事务内复核这些不变量。
3. 本地实现通过只读 reviewer 后,另行获批提交、推送并走现有生产发布合同,使脚本进入运行镜像;不得带外复制脚本。
4. 另行获批后执行一次生产 reset:repair helper 仅 dry-run 对照,新脚本 preflight、单次 apply、verify、写后角色/权限复核、一次登录与一次强制改密、最终 verify 和 secret/恢复材料清理。
5. 完成后只向 `08-08-production-authenticated-readiness` 交接“员工最终凭据已就绪且负向权限前提仍成立”,不在本任务执行合同 UI/API 负向验收。
## Requirements
### R1 现有 repair helper 只读对照
- 获批进入生产窗口后,先对唯一目标运行一次默认 `--dry-run`,保存脱敏动作集合与 `passwordReset=false` 结论;不得运行其 `--apply`。
- dry-run 若计划 `restore_active_employee`、`clear_login_lock` 或 `add_missing_roles`,说明现场已偏离既有诊断,本任务立即 `PAUSED`,不得让 helper 修复后继续。
- dry-run 必须继续通过 `isSafeOaOnlySmokeTarget`,且不能用其“包含期望角色”结论替代精确角色断言。
### R2 新脚本的唯一目标与权限不变量
- 新增 `server/src/scripts/reset-employee-smoke-password.js`。脚本自行从生产状态发现目标,不从 argv/env 接收完整 identifier,不输出 email、phone、employee_no、name 或目标 id。
- 目标必须同时满足:`status=active`、`employment_status=active`、未 archived、smoke 判据命中、至少一个合法 email/phone 登录 identifier、当前未锁定、`must_change_password=false`、实际角色排序后精确等于 `['employee']`。
- 从实际角色解析出的权限不得包含 `project.manage`、`project.view_all`、`system.settings` 或 `*`。候选数不是恰好 1、角色多/少一项、权限扩大、状态变化或 smoke 判据失败均 `BLOCKED`。
- 脚本只断言角色和权限,禁止 create/delete/update `employee_roles`,禁止激活、归档、修改 employment/status、identifier 或任何业务数据。
- 事务内必须按目标 id、写前 token version、状态、smoke、精确角色和权限再次核对;任一变化导致事务零写入。
### R3 受控密码重置合同
- 提供 `--preflight`、`--verify`、`--verify-password-changed`;可提供只读 `--diagnose-wecom-binding`。唯一写入口必须是 `--apply EMPLOYEE_SMOKE_BREAK_GLASS`,确认串不精确即 `explicit_confirmation_required`。
- 临时密码只从 stdin 读取,使用应用同源 `hashPassword` 与 `validatePassword`;不得进入 argv、环境变量、history、stdout、artifact、task 或对话。
- 单次事务仅更新目标认证状态:新 password hash、`must_change_password=true`、三类失败/锁定域与 password-reset request 状态清零、token version +1;仅撤销目标 session、失效目标 password reset code、消耗目标 email login code,并写专用 break-glass audit。
- 所有 session/challenge 写入必须带 `employee_id=target.id`;审计 actor/target 仅为该目标。不得修改角色、权限、启用/归档状态、企微绑定或其他账号。
- apply 必须严格一次。通道中断、输出缺失或 JSON 不可解析进入 `APPLY_UNCERTAIN`,此时只允许 `--verify`;verify 证明已落地时禁止 retry。
### R4 写后重新验证负向前提
- `--verify` 不只验证 must-change、锁、session/challenge 与 audit,还必须重新发现唯一目标并证明角色精确为 `['employee']`、四项合同管理权限均为 false。
- verify 后再执行一份独立只读角色/权限快照,与写前基线逐项对账;不能沿用 `08-08` 的旧诊断,也不能只断言“包含 employee”。
- 只有 `rolesExact=true`、`contractManagePermissions=false`、目标仍唯一且 reset 副作用全部 PASS,才允许更新临时仓库外 secret 并进行一次登录。
### R5 强制改密、secret 与交接
- 写前生成互不相同的临时密码和最终密码;保存旧 hash 为生产现场 `0700/0600`、最多 24 小时的高敏有限恢复材料。它不能恢复 token version、session、challenge、锁定计数或审计,不得称为回滚。
- 一次登录必须按预期进入 `/change-password`;一次强制改密提交成功后立即停在 `/portal`,不得进入合同或其他业务页,业务 API 实际发送数为 0,自动 retry 为 0。
- `--verify-password-changed` 必须证明 must-change=false、locks cleared、activeSessions=1、password-change audit 与本次 employee-smoke break-glass audit 均存在,且角色/权限不变量仍成立。
- 最终 secret 原子更新且权限为 `0600`;临时凭据、浏览器状态和旧 hash 在验证通过后销毁,只记录清理布尔。
- 回到 `08-08-production-authenticated-readiness` 后再执行员工合同权限负向验收;本任务成功不等于该负向验收已经通过。
### R6 发布、证据与停止条件
- 本地代码必须先通过 focused tests、管理员脚本/repair helper 回归和只读 reviewer;reviewer 不改代码。
- 新脚本必须进入一个 clean/published、remoteVerified 的正常部署候选,经现有 `deploy-existing.sh` 合同另行批准发布;发布前不得生产写入,发布失败立即停止。
- 生产执行前,运行镜像脚本 SHA-256 必须与获批候选中的脚本精确一致;不得复制或替换运行脚本。
- 生产发布与生产账号写入分别需要明确批准。任何审批、SHA、身份、migration、目标、权限、恢复材料或 secret 门禁失败均 fail-closed。
- 任务证据只记录 SHA match、枚举、布尔、计数、apply/login/change-password 次数和清理状态,不记录生产身份、完整路径、identifier、账号、密码、hash、token、cookie 或原始 payload。
## Acceptance Criteria
- [ ] AC1:现有 repair helper 的完整 apply 副作用已由源码和 focused regression 固化;生产仅运行一次 dry-run,输出证明 `passwordReset=false` 且 action 集合为空,helper apply 次数为 0。
- [ ] AC2:新脚本的 preflight 仅在唯一 active-employment/unarchived smoke employee、identifier ready、未锁定、must-change=false、`roles == ['employee']` 且四项管理权限全 false 时 PASS;所有非唯一/额外角色/权限扩大/状态漂移用例均 fail-closed。
- [ ] AC3:新脚本的 apply 只接受精确确认串和 stdin 密码;事务内二次核对目标/token/smoke/状态/精确角色/权限,且没有任何角色、权限、状态、identifier 或业务数据写入路径。
- [ ] AC4:focused tests 证明密码、锁、token、目标限定 session/challenge 与专用 audit 副作用;同时回归管理员 reset 与 repair helper,未扩大既有工具目标面。
- [ ] AC5:本地只读 reviewer 复核脚本与测试后通过;随后只在另行获批的正常发布中把脚本送入生产镜像,运行脚本 SHA 与 clean/published 候选精确一致,带外复制次数为 0。
- [ ] AC6:生产 preflight PASS 后 apply 恰好一次;明确结果或 `APPLY_UNCERTAIN -> verify` 证明写入状态,审计计数写前/写后增量恰好 1,不存在第二次 apply。
- [ ] AC7:apply 后 `--verify` 与独立只读快照均重新证明 `roles == ['employee']`、四项管理权限全 false、目标唯一,且 must-change、locks、session/challenge、token/audit 副作用符合合同;未沿用写前结论。
- [ ] AC8:一次登录按预期进入强制改密,一次提交后 `--verify-password-changed` PASS;非认证业务 API 发送数为 0,最终 secret 为 `0600`,临时凭据、浏览器状态和旧 hash 已销毁。
- [ ] AC9:生产其他账号、角色/权限、企微绑定、合同、schema 与业务数据写入为 0;任务内无敏感值泄露,执行后只读 reviewer 和 task/process/governance/sensitive/diff 门禁通过。
- [ ] AC10:只向 `08-08-production-authenticated-readiness` 交接最终员工凭据可用与负向前提仍成立;员工合同权限负向仍由该 task 执行,本任务不宣称产品已可告知用户测试。
## Out of Scope
- 在本任务内执行合同列表/详情/终止入口或员工合同权限负向验收。
- 修改 employee 角色、权限、active/employment/archive 状态、identifier、企微绑定,创建新 smoke 账号或影响真人员工。
- 执行 `repair-oa-only-smoke-accounts.js --apply`,扩展管理员 break-glass 脚本到任意员工,或使用手写 SQL/Prisma、`node -e`、临时脚本复制完成写入。
- 启用邮件、SMTP、自助忘记密码、生产 feature flag、schema/migration 或合同数据。
- 把旧 hash 写回、删除审计或恢复历史 session/challenge;如确需此类写入必须另行批准。
## Accepted Risks and Deferred Items
- 正常发布会重启/切换应用,即使候选只新增运维脚本也属于生产变更;因此发布审批独立于 task start 和账号写入审批。
- smoke 账号风险低于唯一管理员,但 reset 和强制改密仍会递增 token version、撤销目标会话并写审计,不能视为零成本操作。
- 旧 hash 仅保留一个格式正确的历史值,不能还原其他认证状态;apply 后不存在完整回滚,只能依赖 verify、最终改密或另行批准的 roll-forward。
- 员工负向验收和合同 UI 样本阻塞不由本任务收口;本任务完成后原 readiness task 仍可能保持 PARTIAL/PAUSED。
## Open Questions
- 无。现有 helper 不具备密码重置能力,技术路径已收敛为新专用脚本;发布和生产写入在执行期分别走明确审批停点。
## Test Plan
### 本地实现与复核
- `node --check server/src/scripts/reset-employee-smoke-password.js`
- `cd server && npm test -- tests/unit/scripts/reset-employee-smoke-password.test.js tests/unit/scripts/reset-active-admin-password.test.js tests/unit/scripts/repair-oa-only-smoke-accounts.test.js`
- `cd server && npm run prisma:validate`
- `cd client && npm run typecheck && npm run build`(仅在形成可部署候选前执行)
- `python3 ./.trellis/scripts/task.py validate 08-10-production-employee-smoke-break-glass-reset`
- `node scripts/qa/check-process-contract.mjs --baseline --json`
- `node scripts/qa/check-trellis-governance.mjs --json`
- `node scripts/qa/check-sensitive-content.mjs`
- `git diff --check`
### 另行获批的生产阶段
- clean/published provenance、生产 identity/read-only preflight、65 migration、运行/公网 SHA、rollback window。
- 正常部署新候选;部署完成后运行镜像新脚本 SHA 与候选精确一致。
- repair helper 单次 dry-run(禁止 apply)→ 新脚本 `--preflight` → 独立权限快照。
- 受控旧 hash、临时/最终 secret 就绪后单次 `--apply EMPLOYEE_SMOKE_BREAK_GLASS` → `--verify` → 写后独立角色/权限快照。
- 一次登录/强制改密 → `--verify-password-changed` → secret/hash/browser 清理 → 最终 read-only preflight/window。
- 执行后只读 reviewer 核对脚本 SHA、apply 次数、审计增量、副作用、写后角色/权限与清理证据。
## Evidence Rules
- 允许记录:候选/脚本 SHA、match 布尔、目标/审计/会话/challenge 计数、角色是否精确、权限布尔、attempt 次数、HTTP 状态、清理布尔。
- 禁止记录:identifier、email、phone、employee_no、name、目标 id、临时/最终密码、旧/新 hash、cookie/token、生产路径/身份/连接串和原始 payload。
@@ -0,0 +1,43 @@
{
"id": "production-employee-smoke-break-glass-reset",
"name": "production-employee-smoke-break-glass-reset",
"title": "生产员工 Smoke Break-glass 重置",
"description": "为唯一专用 OA-only smoke employee 新增最窄 break-glass 密码重置工具,经正常候选发布后执行一次受控认证恢复;角色精确为 employee 且合同管理权限写后重新验证,负向业务验收仍归 08-08 task。",
"status": "in_progress",
"dev_type": "operations",
"scope": "server employee-smoke break-glass script/tests, controlled production delivery and credential reset lifecycle; no role/permission or contract writes",
"package": "hyb-oa",
"priority": "P2",
"creator": "shunli",
"assignee": "shunli",
"createdAt": "2026-08-10",
"completedAt": null,
"branch": null,
"base_branch": "main",
"worktree_path": null,
"commit": null,
"pr_url": null,
"subtasks": [],
"children": [],
"parent": null,
"relatedFiles": [
"server/src/scripts/repair-oa-only-smoke-accounts.js",
"server/tests/unit/scripts/repair-oa-only-smoke-accounts.test.js",
"server/src/scripts/reset-active-admin-password.js",
"server/tests/unit/scripts/reset-active-admin-password.test.js",
"server/src/scripts/reset-employee-smoke-password.js",
"server/tests/unit/scripts/reset-employee-smoke-password.test.js",
"server/src/constants/permissions.js",
"server/src/utils/crypto.js",
"server/src/utils/passwordPolicy.js",
"server/src/routes/auth.js",
"server/src/services/contractRegistryFilters.js",
"deploy/trialrun/deploy-existing.sh",
".trellis/tasks/08-08-production-authenticated-readiness",
".trellis/spec/hyb-oa/backend-fastify-prisma.md",
".trellis/spec/hyb-oa/deployment-trialrun.md",
".trellis/spec/hyb-oa/security-privacy.md"
],
"notes": "本地实现与用户只读审查 PASS:新增员工 smoke 专用脚本与 23 条 focused 用例,三组脚本测试 34/34、Prisma、client build 和治理门禁 PASS;reviewer 另确认 Serializable + Employee/role lock 可阻止写入窗口内权限前提漂移。当前停在 STOP A,尚未提交/推送/发布/连接生产;正常生产发布与账号写入仍分别另行批准。现有 repair helper 仅 dry-run,严禁 apply。",
"meta": {}
}