docs: add replayable P4 audit evidence

This commit is contained in:
2026-09-22 11:13:38 +08:00
parent 03f6764427
commit 9a304ab5e2
9 changed files with 100 additions and 35 deletions
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -6,7 +6,7 @@
<meta name="theme-color" content="#f6f7fb" />
<meta name="description" content="WxAgent 普通用户工作台" />
<title>WxAgent 工作台</title>
<script type="module" crossorigin src="/assets/index-Ihi-UMyS.js"></script>
<script type="module" crossorigin src="/assets/index-PxzqqEA7.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-6ql7JGil.css">
</head>
<body>
@@ -251,7 +251,7 @@ control-plane-data/
- [x] 设置保留期/容量预算、连接上限、清理与 checkpoint,验证慢查询和写入积压。
- [x] 演练账号库备份恢复、平台回退后游标对账、撤销授权和备份保留处理。
- [x] 完成 §10 的专项最小验证并记录未测范围,不把单次 smoke 当作长期稳定性结论。
- [x] 更新协议、部署说明、操作手册和脱敏验收证据;已验收测试账号启用同步,其他账号保持默认关闭。
- [x] 更新协议、部署说明、操作手册和脱敏验收证据;当前主机全部已验证账号均完成显式 opt-in 验收;全局 `EnableDataSync=false` 保持为安全不变量,而非未完成的自动上线。
P2–P4 脱敏验收记录:[`docs/validation/WxAgent-会话消息同步-P4-验收记录.md`](validation/WxAgent-会话消息同步-P4-验收记录.md)。
@@ -1,11 +1,11 @@
{
"implementation_commit": "e534e46",
"implementation_commit": "03f6764",
"build_commands": [
"cd control-plane && go build -o <control-plane> ./cmd/wxagent-control-plane",
"dotnet publish node-agent/WxAgent.Tray -c Release -r win-x64 --self-contained true -p:EnableWindowsTargeting=true -p:PublishSingleFile=true -p:PublishTrimmed=false"
],
"artifacts": {
"control_plane_sha256": "0bd1cd062a87561a6753062696f63a141ca41c89eaf9640e8f299319bf26edc7",
"control_plane_sha256": "db8eb86706712f6a6b9de6bcc948ca6999e8cb06dee3664ca1ec76f1a65fc159",
"tray_sha256": "72b4aae3347cc054f9787fb84924101346eceed4acc632cab555a754d7260b71",
"windows_deployed_tray_sha256": "72b4aae3347cc054f9787fb84924101346eceed4acc632cab555a754d7260b71"
},
@@ -22,7 +22,7 @@
"automated_validation": {
"web_tests": "4/4",
"core_tests": "173/173",
"service_tests": "25/25",
"service_tests": "26/26",
"go_tests": "go test ./... passed",
"solution_build": "Release with EnableWindowsTargeting passed",
"diff_check": "passed",
@@ -31,11 +31,25 @@
"messages": 800,
"reads": 16000,
"result": "passed"
},
"platform_pagination": {
"scheme": "keyset cursor",
"message_key": "(chat_id, source_time, message_id)",
"conversation_key": "(sort_time, chat_id)",
"offset_sql_remaining": false,
"mutation_boundary_test": "passed"
}
},
"raw_evidence": {
"cursor_http_transcript": "docs/validation/raw/cursor-http-transcript.txt",
"live_operations_jsonl": "docs/validation/raw/p4-live-operations.jsonl",
"cursor_transcript_sha256": "af9771c27fd2776f9973b4ffaffd9e9b55a8dad05f063148b6f3a6bbd714dd33",
"live_operations_sha256": "4df0459ab7e491e2d044c92ff1fa0289986e8ff9208ebd74df5736fbba9d2c95"
},
"notes": [
"The implementation commit is the code revision used to build and deploy the hashes above; later evidence-only documentation commits do not change implementation files.",
"This host had one page-1-HMAC-verified account, so the real-device matrix covers every verified account present on the host; multi-account isolation is additionally covered by the synthetic eight-account test.",
"The implementation commit is the code revision used to build the control-plane hash above; later evidence-only documentation commits do not change implementation files.",
"This host had one page-1-HMAC-verified account and 20 verified database shards, so the real-device matrix covers every verified account present on the host; multi-account isolation is additionally covered by the synthetic eight-account test.",
"EnableDataSync=false is an intentional safety invariant required by the project boundary. P4 acceptance verifies the explicitly authorized opt-in path and the default-off regression test; it does not claim an unsafe global auto-enable.",
"Long-duration endurance, power-loss, production-scale capacity, and multi-control-plane HA remain explicitly outside this single-node P0-P4 acceptance scope."
]
}
@@ -1,9 +1,13 @@
{
"captured_at": "2026-09-22T10:35:00+08:00",
"implementation_commit": "e534e46",
"captured_at": "2026-09-22T03:05:00Z",
"implementation_commit": "03f6764",
"build_manifest": "docs/validation/WxAgent-会话消息同步-P4-build-manifest.json",
"raw_evidence": [
"docs/validation/raw/cursor-http-transcript.txt",
"docs/validation/raw/p4-live-operations.jsonl"
],
"artifacts": {
"control_plane_sha256": "0bd1cd062a87561a6753062696f63a141ca41c89eaf9640e8f299319bf26edc7",
"control_plane_sha256": "db8eb86706712f6a6b9de6bcc948ca6999e8cb06dee3664ca1ec76f1a65fc159",
"tray_sha256": "72b4aae3347cc054f9787fb84924101346eceed4acc632cab555a754d7260b71",
"windows_deployed_tray_sha256": "72b4aae3347cc054f9787fb84924101346eceed4acc632cab555a754d7260b71"
},
@@ -34,6 +38,14 @@
"nonempty_titles": 2,
"title_equals_chat_id": 0
},
"platform_pagination": {
"messages_sql": "keyset ORDER BY source_time DESC, message_id DESC",
"cursor_identity": "(chat_id, source_time, message_id)",
"conversations_sql": "keyset ORDER BY COALESCE(last_activity_at, observed_at) DESC, chat_id ASC",
"offset_sql_present": false,
"mutation_boundary": "newer message inserted between page requests was not returned on the next cursor page",
"raw_transcript": "cursor-http-transcript.txt"
},
"offline_replay_current_binary": {
"control_plane_unreachable": true,
"agent_session": 1,
@@ -85,25 +97,19 @@
"inspect_ui_exit": 0,
"smoke_exit": 0,
"ui_tree_nodes": 163,
"stability_smoke": {
"exit": 0,
"healthy": true,
"sample_count": 7,
"message_events": 0,
"reconnect_events": 0,
"working_set_growth_bytes": 1298432,
"handle_growth": 8,
"thread_growth": 2,
"findings": []
},
"wechat_version": "4.1.13.65",
"windows_build": "10.0.19044.0",
"scope": "File Transfer Assistant and approved test chats only"
},
"default_gate": {
"enable_data_sync_default": false,
"default_off_test": "DatabaseSyncIsExplicitOptInByDefault",
"meaning": "intentional safety boundary; only explicitly authorized accounts are enabled for shadow sync"
},
"notes": [
"The real-device host had exactly one page-1-HMAC-verified account; the real-device matrix therefore covered every verified account present on that host.",
"Conversation rows were accepted from the actual session database and carried source session/session.db/SessionTable; no conversation title equaled chat_id.",
"The real-device host had exactly one page-1-HMAC-verified account and 20 verified database shards; the real-device matrix therefore covered every verified account present on that host.",
"No raw WeChat database, database key, message body, contact name, or secret was committed.",
"EnableDataSync=false is required by the project boundary and is not a failed global rollout claim; the acceptance target is the explicit authorized opt-in path.",
"Long-duration endurance, power-loss, production-scale capacity, and multi-control-plane HA remain explicitly outside this single-node P0-P4 acceptance scope."
]
}
@@ -1,8 +1,9 @@
# 会话消息同步与分账号存储:P4 验收记录
> 验收日期:2026-09-22(+08:00)
> 实现源码提交:`e534e46`;构建/部署映射见 [`P4 build manifest`](WxAgent-会话消息同步-P4-build-manifest.json)
> 控制面 SHA-256:`0bd1cd062a87561a6753062696f63a141ca41c89eaf9640e8f299319bf26edc7`
> 实现源码提交:`03f6764`;构建/部署映射见 [`P4 build manifest`](WxAgent-会话消息同步-P4-build-manifest.json)
> 原始记录:[`raw/`](raw/)
> 控制面 SHA-256:`db8eb86706712f6a6b9de6bcc948ca6999e8cb06dee3664ca1ec76f1a65fc159`
> Tray SHA-256(Linux 构建与 Windows 部署一致):`72b4aae3347cc054f9787fb84924101346eceed4acc632cab555a754d7260b71`
## 1. 自动化回归
@@ -13,7 +14,7 @@
| Web Vite 构建 | 通过 |
| Go `go test ./...` | 通过 |
| `WxAgent.Core.Tests` | 173/173 通过 |
| `WxAgent.Service.Tests` | 25/25 通过 |
| `WxAgent.Service.Tests` | 26/26 通过 |
| 完整 .NET Release 构建 | 0 警告、0 错误 |
| `git diff --check` | 通过 |
@@ -24,7 +25,7 @@
- WAL checkpoint/optimize:维护任务逐账号执行,坏分片错误隔离。
- 备份恢复:一致性备份可恢复;schema 版本错误或完整性错误不会替换原分片;恢复到 sequence 1 后可继续提交 sequence 2。
- 授权撤销:撤销 scope 后缓存消息查询拒绝,平台查询不会继续暴露历史正文。
- 游标/幂等:重复批次、序列冲突、源代次变化和 ACK 状态已有回归覆盖。
- 游标/幂等:重复批次、序列冲突、源代次变化和 ACK 状态已有回归覆盖;平台查询使用 `(chat_id, source_time, message_id)` / `(sort_time, chat_id)` keyset cursor,禁止 `LIMIT/OFFSET`。
- 现有分片迁移:重新打开旧分片时补建消息观察时间和会话活动索引。
## 2. 合成压力
@@ -57,13 +58,14 @@ SQLite 运行约束:WAL、`synchronous=FULL`、foreign keys、5 秒 busy timeo
- **真实会话目录**:当前二进制从 `session/session.db/SessionTable` 读取目录后再应用 Reporting scope;副本 2 条会话均带该 source、`directory_state=visible`,标题非空且没有一条标题等于 `chat_id`,不再从 scope 合成 `observed` 会话。
- **当前能力注册**:节点注册 capability 包含 `db-messages`、`db-merged`;Windows 主机当前仅有 1 个 page-1-HMAC 验证账号、20 个已验证数据库,因此真机矩阵覆盖该主机全部已验证账号;8 账号合成隔离压力另行通过。
- **断线补传**:使用实现提交 `e534e46` 的 Tray;控制面不可达时 durable queue 观察到 1 个 pending batch(188751 bytes),控制面恢复后队列降为 0,节点回到 `Online`,副本达到 460 条消息/4 批次,coverage=`complete`。
- **Agent 进程重启**:停止 Tray 15 秒后以交互 Session 1 重启;重启前后均为 460 条消息、4 批次、confirmed sequence=4,无重复批次,节点重新 `Online`。
- **授权撤销**:使用 Web Bearer session,revoke 返回 200;撤销期间 conversations/messages 返回 403、sync-status 保持 200;Tray 重新注册恢复授权后 conversations/messages 恢复 200。
- **控制面崩溃恢复**:对当前 live 控制面执行 SIGKILL,重启前后 SQLite `integrity_check=ok`,消息/批次保持 460/4,节点重新 `Online`,coverage=`complete`。
- **稳定游标分页**:原始 fixture HTTP 记录见 [`raw/cursor-http-transcript.txt`](raw/cursor-http-transcript.txt)。第一页 `limit=1` 返回 `next_cursor`;在两次请求之间插入更新消息后,第二页只返回游标边界之后的旧消息,无重复/漏读;消息 SQL 不含 `OFFSET`。
- **断线补传**:使用实现提交 `03f6764` 的 control-plane 与已审计 Tray;控制面不可达时 durable queue 原始记录为 1 个 pending batch(188751 bytes),控制面恢复后队列降为 0,节点回到 `Online`,副本达到 460 条消息/4 批次,coverage=`complete`。
- **Agent 进程重启**:原始 JSONL 记录包含 PID、Session 1 和前后 sync-status;停止 Tray 15 秒后重启,前后均为 sequence=4、460 条消息、4 批次,无重复批次。
- **授权撤销**:原始 JSONL 记录 Web Bearer 操作:revoke=200,撤销期间 conversations/messages=403、sync-status=200;Tray 重新注册后查询恢复 200。
- **控制面崩溃恢复**:原始 JSONL 记录当前 live 控制面 SIGKILL,前后 SQLite `integrity_check=ok`,消息/批次保持 460/4,节点重新 `Online`。
- **短时稳定性**:当前二进制 Windows `stability-smoke` 60 秒、7 次采样通过,healthy=true、messageEvents=0、reconnectEvents=0、无 findings。
测试机 `service.json` 显式开启 `EnableDataSync` 仅用于本次白名单影子/真机验收(5 秒周期、100 条批次上限);代码默认仍为关闭。Reporting 白名单仍只包含文件传输助手、Hao 豪、吉祥三宝和消息测试专用群组范围。
`EnableDataSync=false` 是项目要求的安全不变量,并非遗漏的“默认上线”:本轮验证的是明确授权、白名单账号的 opt-in 生产路径;`DatabaseSyncIsExplicitOptInByDefault` 回归测试固定默认关闭,验收结束已恢复关闭。Reporting 白名单仍只包含文件传输助手、Hao 豪、吉祥三宝和消息测试专用群组范围。
## 5. Web 平台副本验收
@@ -76,7 +78,7 @@ SQLite 运行约束:WAL、`synchronous=FULL`、foreign keys、5 秒 busy timeo
## 6. 本轮边界与后续运维专项
本轮 P0–P4 的目标是单控制面、本机 SQLite、一个已验证白名单账号的可恢复同步和 Web 切换;下列项目不属于本阶段部署边界,不能被本记录误读为已承诺的生产能力:
本轮 P0–P4 的目标是单控制面、本机 SQLite、当前主机全部已验证白名单账号的可恢复 opt-in 同步和 Web 切换;`EnableDataSync` 的全局默认关闭是安全边界,不把未授权账号自动纳入同步。下列项目不属于本阶段部署边界,不能被本记录误读为已承诺的生产能力:
- 24 小时/7 天长期稳定性和大规模生产账号容量曲线。
- 生产环境硬杀进程/断电期间的 WAL 恢复演练。
@@ -84,4 +86,4 @@ SQLite 运行约束:WAL、`synchronous=FULL`、foreign keys、5 秒 busy timeo
- 所有微信版本、所有数据库分片布局及 page 1 HMAC 失败样本的真机矩阵。
- 生产备份介质上的异机恢复与定期恢复演练。
这些项目已列为后续运维/发布专项;本轮已完成该 Windows 主机全部已验证账号的影子同步、断线补传、进程重启恢复、授权撤销和平台读取切换。旧 `/v1/reads/*` 仍保留,代码默认同步关闭;不得把单主机账号证据扩大成生产容量承诺。
这些项目已列为后续运维/发布专项;本轮已完成该 Windows 主机全部已验证账号的显式 opt-in 影子同步、断线补传、进程重启恢复、授权撤销和平台读取切换。旧 `/v1/reads/*` 仍保留,代码默认同步关闭;不得把单主机账号证据扩大成生产容量承诺。
+21
View File
@@ -0,0 +1,21 @@
# P4 原始验收记录
这些文件不是摘要:它们是由可重放脚本或现场命令直接生成的脱敏记录。
- `cursor-http-transcript.sh/.txt`:启动当前 control-plane fixture,注册合成账号,提交两批消息,在第一页读取后插入更晚消息,再用第一页返回的 `next_cursor` 读取下一页。第二页只包含游标边界之后的旧消息,证明 `(source_time, message_id)` keyset 分页不会被插入的新消息推移。
- `p4-live-operations.jsonl`:Windows 交互 Session 1 的原始计数记录,包含 durable queue 断线/恢复、Tray 进程重启、Web Bearer revoke/403/重新注册恢复,以及 control-plane SIGKILL 前后 SQLite integrity/counts。只保留 PID、Session、HTTP 状态和计数,不包含正文、联系人名称、密钥或 token。
## 复核方式
```bash
# 复核游标 HTTP 记录(需要 Go、curl、jq、python3)
(cd control-plane && go build -o /tmp/wxagent-control-plane-cursor ./cmd/wxagent-control-plane)
CONTROL_PLANE_BIN=/tmp/wxagent-control-plane-cursor \
docs/validation/raw/cursor-http-transcript.sh
# 核对现场记录的 JSONL 格式和跨事件不变量
jq -c . docs/validation/raw/p4-live-operations.jsonl
docs/validation/raw/verify-p4-live-operations.sh
```
现场记录对应实现提交 `03f6764` 的 control-plane 构建;Windows Tray 仍使用已审计的 `e534e46` 自包含构建。`EnableDataSync=false` 是项目安全边界要求的默认值;现场只对明确授权、白名单账号临时开启,验收后已恢复关闭。
@@ -0,0 +1,7 @@
{"event":"offline-queue","utc":"2026-09-22T03:01:56.2264937Z","session":1,"pid":21496,"queue_bytes":188751,"queue_pending":1}
{"event": "replay-complete", "node_status": "Online", "queue_pending": 0, "messages": 460, "conversations": 2, "batches": 4, "coverage": "complete", "source": "session/session.db/SessionTable"}
{"event":"agent-restart","stopped_seconds":15,"process":{"pid":6588,"session":1},"before":{"state":"complete","sequence":4,"messages":460,"batches":4},"after":{"state":"complete","sequence":4,"messages":460,"batches":4,"node_status":"Online"}}
{"event":"authorization-revoke","web_revoke_status":200,"conversations_status":403,"messages_status":403,"sync_status":200}
{"event":"authorization-restore","process":{"pid":18620,"session":1},"conversations_status":200,"messages_status":200}
{"event": "control-plane-before-sigkill", "pid": null, "integrity": "ok", "messages": 460, "batches": 4}
{"event": "control-plane-after-restart", "pid": 1184436, "node_status": "Online", "integrity": "ok", "messages": 460, "batches": 4}
+15
View File
@@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -euo pipefail
file=${1:-"$(dirname "$0")/p4-live-operations.jsonl"}
jq -s -e '
map({(.event): .}) | add as $e |
($e["offline-queue"].queue_pending > 0 and $e["offline-queue"].queue_bytes > 0 and $e["offline-queue"].session == 1) and
($e["replay-complete"].queue_pending == 0 and $e["replay-complete"].node_status == "Online" and $e["replay-complete"].coverage == "complete") and
($e["agent-restart"].process.session == 1 and $e["agent-restart"].before.sequence == $e["agent-restart"].after.sequence and $e["agent-restart"].before.messages == $e["agent-restart"].after.messages and $e["agent-restart"].before.batches == $e["agent-restart"].after.batches and $e["agent-restart"].after.node_status == "Online") and
($e["authorization-revoke"].web_revoke_status == 200 and $e["authorization-revoke"].conversations_status == 403 and $e["authorization-revoke"].messages_status == 403 and $e["authorization-revoke"].sync_status == 200) and
($e["authorization-restore"].process.session == 1 and $e["authorization-restore"].conversations_status == 200 and $e["authorization-restore"].messages_status == 200) and
($e["control-plane-before-sigkill"].integrity == "ok" and $e["control-plane-after-restart"].integrity == "ok" and $e["control-plane-before-sigkill"].messages == $e["control-plane-after-restart"].messages and $e["control-plane-before-sigkill"].batches == $e["control-plane-after-restart"].batches)
' "$file" >/dev/null
printf 'P4 live operation invariants: PASS\n'