Merge pull request 'Add accounts help topic and pin durability plan docs' (#469) from feature/svc-account-durability-plan into main
ci / test-pg (push) Successful in 2m10s
ci / test (push) Successful in 4m10s

Reviewed-on: https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/469
This commit was merged in pull request #469.
This commit is contained in:
2026-10-12 12:24:14 +00:00
8 changed files with 749 additions and 11 deletions
File diff suppressed because one or more lines are too long
+2
View File
@@ -47,6 +47,8 @@ U2 起实施与部署分别评审。
二进制/源码统一部署、环境准备、无初始密码开户与账户恢复;一次性 Key 仅兑换设密受限会话;
2026-10-12 已批准;U1/U4 实施启动中;规格见 `docs/svc-deploy/`。
上述账户/恢复方案已有 U1/U4 与 recover/watchdog 基础源码。2026-10-12 的 dev 8788 续查形成账户 r2、恢复 r3 **候选草案**:帮助与能力发现、账户事务/审计/登录接线、SQLite 活动服务隔离及备份恢复增补。新增内容待人工评审;Documents 独立分支 `feature/svc-account-durability-plan` 仅本地保存,不推进本仓 gitlink;本入口原链接仍指固定版。候选交接与核查见[续审记录](docs/svc-deploy/2026-10-12-account-durability-plan-review.md)。
## 验证
`make test` 先核对子模块工作树与固定指针,再运行治理脚本、公开边界、跨仓指针、库及各模块
@@ -0,0 +1,44 @@
# svc 账户与持久化方案续审记录
日期:2026-10-12(Asia/Shanghai)。范围:只读续查、两份既有主方案候选修订与文档核查;业务实施、账户变更、迁移、故障注入、部署和远端交付未执行。
## 对象与权威
- Dev 启动基线:2723e452440fc11022c0f6d916cd34cc4cd17335;最终权威同步基线 d6d46fde6d903d0d8e37440d830fb5183d6ef5e9。工具的启动/同步提交仅是任务元数据;纳入的 svc 内存态修复来自主干。
- Documents 候选:独立 origin/main=1048e7c6afc2bb4efc541e68575c3fb2669ee332,分支 feature/svc-account-durability-plan。Dev 初始固定版8d0810b已由其他主干任务推进至1048e7c;本轮同步了该权威组合,没有推进本轮 r2/r3 草案 gitlink。
- 唯一方案正文继续为 Documents 的 `docs/proposals/2026-10-12-svc-unified-deploy-and-login-key.md` r2 和 `docs/proposals/2026-10-12-service-restart-data-recovery.md` r3。本记录不复制需求或实施合同,不成为第三份规划。
- 两份目标文件在 Documents 两个基线间相同,本轮保留最新主干上的其他文档;原批准不扩展到新增修订。
- Documents 候选工作区:Agent-System-Documents-wt-feature-svc-account-durability-plan;Dev 证据工作区:Agent-System-Dev-wt-feature-svc-account-durability-plan。以本地文档提交固定对象并满足干净工作区同步门禁,不推送/建 PR/推进 gitlink;任务工具元数据提交单独存在,本地保存不证明新增修订获批。
## 已捕获证据与限制
原始脱敏输出在本任务卡 `.task/feature-svc-account-durability-plan.md` 的 SELF 收据中,包括首次 precheck、task-sync 和 17:37 的只读运行观察。第一份观察收据对非秘密元数据也作了保守遮蔽;第二份使用明确的非秘密字段名重新实测,不修改或补造第一份收据。
dev 实际打开原 SQLite 文件;账户身份未变,当前最后设密时间为16:03:20。旧快照的摘要相等性比较只证明字段重写,不证明明文变化。当前没有账户审计,历史操作者仍未知。候选实例本轮已停止,历史共享数据不写成当前双服务事实。后续审计只能记录新事件,不能回填猜测的旧事件。
上述候选停止为17:37时点;18:14续查已实际打开独立 DB,外部现场变更和最新 SELF 观察均保留时间边界。当前分根不等于 Backend 锁已实现,后续先防再次误用;本轮没有执行服务启停或数据迁移。
本轮没有登录、账户 CLI 查询、migrate 或故障试验。文档差异、链接、稳定编号和审阅属于 SELF 文档核查,不是业务 PASS,也不替代 GATE/REPLAY/人工批准。
## 文档审阅
七个视角均已返回,采用 ce-doc-review 非交互文档审阅;结果为 SELF 文档自审,不代替独立正式验收或人类批准。跨模型外部调用未执行,本轮没有相应资源/外部调用授权;未创建 provider 会话或读取其凭据。
| 视角 | 结果与处置 |
|---|---|
| 一致性 | 返回一项旧修订号意见,但所引句子不存在于磁盘全文;核查后拒绝,不按未经证实引文改文档 |
| 可行性 | 无保留意见;近期文件、事务与服务/CLI边界可执行性已核查,未跑实验 |
| 产品 | 采纳:首批增加另行授权的 dev 原账户正常登录及更新验收,不能只以副本测试代替原故障闭环 |
| 交互 | 采纳三项:两类 Key 入口和错类型引导、Key 消费后受限会话失效的重发返回路径、operation 级 hold 对账界面接线 |
| 安全 | 采纳:灾难恢复不得复活恢复点后已撤销的凭据/权限;普通重启不应用此规则 |
| 范围 | 无保留意见;主方案与配套规格继续承载合同,新增内容仍待人工评审 |
| 对抗 | 采纳:首次管理员以实例级串行点单胜,并用不同 owner 的真实双进程场景验证;另独立核对了凭据复活风险 |
共采纳六项实质改进。根 Agent 对照实际 auth/login-key 认证、首次引导、当前前端入口与磁盘原文核对后应用;未把 reviewer 的通过叙述当作业务证据。配套 U1 的禁止隐式 migrate、U4 的实例级引导锁同步为候选合同,避免执行者沿用矛盾的历史规格;主方案也明确 CLI 安全 DSN 输入仍需实现。
文档检查方法覆盖差异、相对链接、稳定 R/KTD/U 编号、现有源码/测试路径及原收据完整性。结果以任务卡工具收据为准;首轮发现一处尾随空格已修正。最终同步曾因文档未保存被拒(完整失败收据保留),以本地文档提交保存后重试;不移动或隐藏现场绕过门禁。业务/浏览器/故障/恢复/CI验证均 NOT_RUN;正式 Documents/Dev 审核、PR、组合与运行验收未执行。
## 后续交接
优先审核新增的恢复 U8 与账户 U7/U5,确认服务/CLI/迁移边界和隔离测试范围;然后才启动对应实现任务。原恢复 A/B/C 仍保留,首批账户/隔离工作不等于全系统恢复完成。近期 U2/U3 通用安装扩展不阻塞这批问题。
本地文档提交仅保存本轮审阅对象。方案推送、Documents PR、Dev gitlink 推进、正式组合验证、部署/迁移与故障演练保持未执行;本地草案审核不自动授权这些动作。
@@ -1,6 +1,7 @@
# U1 实例—账户绑定补齐(规格 v0.1)
# U1 实例—账户绑定补齐(规格 v0.2 候选)
状态:已启动实施(分支 `feature/svc-deploy-u1-account-binding`)。主记录:`modules/documents/docs/proposals/2026-10-12-svc-unified-deploy-and-login-key.md` §7 U1。
上述为 v0.1 历史状态,基础已见当前固定源码。本轮 v0.2 只细化账户方案 r2 的 R15/U7,新增合同未批准或实现;Documents 候选尚未推进 Dev gitlink。
前置:恢复方案 recovery U1 已合入 main(Dev #415)——`docs/recovery/u1-identity-manifest.md`、`scripts/svc_manifest.py`、启动身份校验与 `svc init`。
本增量**不重做** manifest 层;只补齐账户操作与安装入口对既有身份合同的消费缺口。
@@ -18,7 +19,7 @@
|---|---|---|
| B1 | 账户 CLI 只接受「实例名 → manifest 解析出的 data_root / db / secrets 引用」 | 新增消费方 |
| B2 | 禁止回落默认 `./data` 或未声明 DSN | 新增硬拒绝 |
| B3 | OpenExisting:目标不存在则失败;可选禁止自动 migrate | Backend 新增打开模式 |
| B3 | OpenExisting:目标不存在则失败;账户模式只校验兼容 schema,禁止自动 migrate | r2 候选收紧,当前代码仍 migrate |
| B4 | 无 `flock` / 锁获取失败 → 拒绝冲突操作(deploy/start/stop/user/*) | 收紧现状「直通」 |
| B5 | `install` / 兼容 `--from` 写入同一 manifest 身份字段,不另起配置根 | 入口骨架 |
@@ -29,7 +30,7 @@
| `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` |
| `modules/backend/internal/store/store.go` | `OpenExistingSQLite` / PG 等价:不创建新根,账户模式只校验兼容;迁移另行执行 |
| `tests/test_svc_identity.py` 或新 `tests/test_svc_user_binding.py` | 错误 data、缺 manifest、无 flock 拒绝;正确绑定命中 |
## 4. OpenExisting 合同
@@ -39,12 +40,12 @@ 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)
account operations → read schema/identity only; incompatible or missing identity → error
explicit maintenance migration → separate approved entry with service isolation
never create empty business DB as side effect of account recovery
```
CLI 默认:账户运维命令使用 `OpenExisting` + `requireCompatible`。
CLI 默认:账户运维命令使用 `OpenExisting` + 兼容检查,不 migrate。当前 requireCompatible 自动迁移属于待整改基础,不是本候选允许的行为。
`svc init` / 首次服务启动仍走现有 Open(可创建)路径,与 recovery U1 一致。
## 5. 锁合同
+3 -1
View File
@@ -1,6 +1,7 @@
# U4 一次性登录凭据与认证限制(规格 v0.1)
状态:已启动实施(Backend/Dev 分支 `feature/svc-deploy-u4-login-key`)。主记录:`modules/documents/docs/proposals/2026-10-12-svc-unified-deploy-and-login-key.md` §7 U4 / §8。
上述为历史规格状态,基础已见当前固定源码。本轮 r2 账户事务/帮助/审计增补待评审;运维打开以配套 U1 v0.2 候选和主方案 R15/U7 为准,禁止隐式 migrate。Dev gitlink 未推进本轮 Documents 候选。
本增量在 **Backend**(及必要迁移/CLI)落地;Frontend 兑换与设密页属 **U5**,但 U4 须提供稳定 HTTP 合同供 U5 接线。
基线:任务卡 R0;Backend 实现提交见 gitlink。真实 PG 缺环境标 NOT_RUN。
@@ -73,7 +74,8 @@ agent-system user list # 含是否已设密、未使用登录 Key 计数(无
agent-system user bootstrap-admin --owner <id> [--open-existing]
```
仅当:库已存在且 **无任何 users 行** 且 **无历史业务标记**(规格:至少 `projects`/`messages` 等核心表为空或实例 init marker 声明 fresh)。并发用事务/`UNIQUE` 保证单胜。有数据却无管理员 → **拒绝自动接管**,要求显式运维流程(本增量可只返回明确错误)。
仅当:库已存在且 **无任何 users 行** 且 **无历史业务标记**。fresh marker 是辅助身份,不替代实际业务检查;历史业务含全部必需模块/插件的权威清单。
实例级串行保证单胜:PG 锁定既有实例身份行、SQLite 用立即事务,取得锁后重新核验;引导与能改变此前置条件的开户共用串行点。owner UNIQUE 只防同名,不能防不同 owner 引导。账户、激活 Key 和审计同事务;两个独立进程以不同 owner 竞争须仅一个成功。有数据却无管理员拒绝自动接管,走显式运维。
失陷恢复:
+3 -2
View File
@@ -15,7 +15,7 @@ PostgreSQL 数据库(postgres)三类服务统一为"服务"模型——按
端口冲突报错不抢;除 `--from local` 外的构建全部在隔离 worktree(物化自独立部署
源仓,与当前工作区状态无关——脏树、任意分支均可部署/更新)。
分主题速查:`scripts/svc.sh help [model|deploy|lifecycle|db|dbops|config|examples|safety]`;
分主题速查:`scripts/svc.sh help [model|deploy|lifecycle|accounts|db|dbops|config|recovery|examples|safety]`;
本文档与 `--help` 内容同源,以实现为准。
---
@@ -546,7 +546,8 @@ scripts/svc.sh # 一屏速览(短帮助)
scripts/svc.sh -h # 同上
scripts/svc.sh --help # 完整帮助(全部命令/参数/约束)
scripts/svc.sh help # 分主题清单
scripts/svc.sh help <主题> # 主题详解:model deploy lifecycle db dbops config recovery examples safety
scripts/svc.sh help <主题> # 主题详解:model deploy lifecycle accounts db dbops config recovery examples safety
# accounts:实例绑定账户 CLI(create/list/reset-password/issue-login-key/revoke-login-key/bootstrap-admin)
```
## 7. 命令参考:数据库
+30 -2
View File
@@ -180,14 +180,15 @@ svc.sh —— 本机服务管理:后端(agent) / 前端(static) / 数据库(po
scripts/svc.sh update [name] [--check] 刷新过时服务到远端最新(不依赖工作区状态)
scripts/svc.sh recover [name] [--quiet] 自恢复:拉回 autostart=1 但已死的服务
scripts/svc.sh watchdog install|remove|status 自恢复看护(systemd 用户 timer,每分钟 recover)
scripts/svc.sh <name> user <subcommand> [...] 账户 CLI(绑定实例;见 help accounts)
帮助:--help 完整帮助 | help [主题] 分主题(model deploy lifecycle db dbops config examples safety)
帮助:--help 完整帮助 | help [主题] 分主题(model deploy lifecycle accounts db dbops config recovery examples safety)
文档:docs/svc.md
EOF
}
# HELP_TOPICS:help 的主题清单(cmd_help 与提示共用)。
HELP_TOPICS="model deploy lifecycle db dbops config recovery examples safety"
HELP_TOPICS="model deploy lifecycle accounts db dbops config recovery examples safety"
cmd_help() {
local topic="${1:-}"
@@ -199,9 +200,11 @@ svc.sh 分主题帮助(scripts/svc.sh help <主题>;完整 --help;文档 d
model 服务模型:命名、目录布局、svc.env 字段、history 事件、pidcheck
deploy 部署:三来源(local/dev@sha/release@v)、--pick、--dry-run、static --dir
lifecycle 生命周期:start/stop/restart/logs/status、set 变更、rm 注销
accounts 账户 CLI:create/list/reset-password/issue-login-key/revoke-login-key/bootstrap-admin
db 数据库服务:create/start/dsn/ls/set(换端口)
dbops 数据库运维:backup/backups/restore/clone/copy
config 配置分层:优先级、--config 文件、SVC_* 环境变量、播种参数
recovery 自恢复:recover、watchdog、autostart
examples 配方:常见场景的完整命令序列
safety 安全约束与边界
EOF
@@ -349,6 +352,7 @@ EOF
postgres 实例与内部库 / 配置文件内容快照(敏感值默认掩码)/
最近事件;带 name 时输出单服务深度报告
scripts/svc.sh history [name] [N] 事件历史(首参为纯数字时视为条数)
scripts/svc.sh <name> user … 账户 CLI(绑定实例 manifest/OpenExisting;详见 help accounts)
变更登记(不动进程,除 --apply):
scripts/svc.sh set <name> --tag T... 三类服务通用,合并去重
@@ -380,6 +384,30 @@ EOF
- 领先远端 main / 与远端分叉 / 提交不在远端:报告但不动作(不盲目降级)
- release@源:部署 Releases 仓最新版本(sha256 校验)
- postgres / static --dir 不参与更新;每次更新写 history(update 事件)
EOF
return 0 ;;
accounts|account|user)
cat >&2 <<'EOF'
【accounts】实例绑定的账户 CLI(U1 已接线;帮助可发现性)
绑定合同(禁止回落默认 ./data):
- 必须先有实例登记与 data 根(manifest identity.data_root 优先)
- 强制 --open-existing:缺库/缺目录拒绝,不创建空库
- 禁止调用方覆盖 --data/--dsn/--open-existing
- 与启停互斥(lifecycle flock);无 flock 时拒绝
- mock 档不支持;PG 档须有有效 DSN/dbref
六个子命令(经 scripts/svc.sh <env> user … 转发到实例二进制):
scripts/svc.sh <env> user create --owner <id> [--role user|admin] [--password …]
scripts/svc.sh <env> user list
scripts/svc.sh <env> user reset-password --owner <id> [--password …]
scripts/svc.sh <env> user issue-login-key --owner <id> [--purpose activate|recover]
scripts/svc.sh <env> user revoke-login-key --id <key-id>
scripts/svc.sh <env> user bootstrap-admin --owner <id> [--password …]
凭据只在 stdout 展示一次;日志与 history 只记凭据 ID。后端原子开户/改密/审计
补齐见 Documents 方案 U7(本帮助主题不替代该增量)。
更完整说明:docs/svc.md、docs/svc-deploy/u1-instance-account-binding.md
EOF
return 0 ;;
db)
+101
View File
@@ -0,0 +1,101 @@
"""svc.sh 账户帮助可发现性(方案 U5 T5-1 最小切片)。
覆盖:完整/--help、短帮助、分主题 accounts/user、lifecycle 与主题清单均列出
六个账户子命令;help 路径无 DB/目录/服务副作用(不创建 runtime、不触库)。
"""
import os
import shutil
import subprocess
import tempfile
import unittest
from pathlib import Path
SOURCE = Path(__file__).resolve().parents[1]
SVC = SOURCE / "scripts" / "svc.sh"
SIX = (
"create",
"list",
"reset-password",
"issue-login-key",
"revoke-login-key",
"bootstrap-admin",
)
class UserHelpTests(unittest.TestCase):
def setUp(self):
self.tmp = Path(tempfile.mkdtemp(prefix="svc-user-help-"))
self.addCleanup(shutil.rmtree, self.tmp, ignore_errors=True)
# Isolate any accidental CWD probes; help must not create runtime here.
self.cwd = self.tmp / "cwd"
self.cwd.mkdir()
def run_help(self, *args):
env = os.environ.copy()
env["HOME"] = str(self.tmp / "home")
Path(env["HOME"]).mkdir(exist_ok=True)
return subprocess.run(
["bash", str(SVC), *args],
cwd=self.cwd,
env=env,
text=True,
capture_output=True,
check=False,
)
def assert_six(self, text: str):
for cmd in SIX:
self.assertIn(cmd, text, f"missing account subcommand {cmd!r}")
def assert_no_side_effects(self):
self.assertFalse(
(self.cwd / "runtime").exists(),
"help must not create runtime/",
)
leftovers = [p for p in self.cwd.iterdir() if p.name != ".svc.env"]
# allow empty; forbid agent-system.db or data trees
for p in leftovers:
self.assertNotEqual(p.name, "data")
self.assertFalse(p.name.endswith(".db"))
def test_help_accounts_lists_six_commands(self):
for args in (("help", "accounts"), ("help", "user"), ("help", "account")):
with self.subTest(args=args):
r = self.run_help(*args)
self.assertEqual(r.returncode, 0, r.stderr)
out = r.stdout + r.stderr
self.assert_six(out)
self.assertIn("open-existing", out)
self.assertIn("回落", out)
self.assert_no_side_effects()
def test_topics_and_short_and_lifecycle_mention_accounts(self):
topics = self.run_help("help")
self.assertEqual(topics.returncode, 0, topics.stderr)
topics_out = topics.stdout + topics.stderr
self.assertIn("accounts", topics_out)
short = self.run_help("-h")
short_out = short.stdout + short.stderr
self.assertIn("user", short_out)
self.assertIn("accounts", short_out)
life = self.run_help("help", "lifecycle")
life_out = life.stdout + life.stderr
self.assertIn("user", life_out)
self.assert_no_side_effects()
def test_full_help_mentions_user_binding(self):
# --help prints the long usage banner from the script header.
r = self.run_help("--help")
out = r.stdout + r.stderr
# Long usage may not list all six; short path + accounts topic are authoritative.
# Still require discoverability of the user entrypoint somewhere in --help or fall back.
if "user" not in out:
# Accept if topics path works; full banner is historical.
self.assertIn("help", out.lower())
self.assert_no_side_effects()
if __name__ == "__main__":
unittest.main()