合并远端AI游戏创作分支更新
Project CI / Frontend tests (push) Failing after 20s
Project CI / Repository checks (push) Successful in 2m21s
Project CI / Backend tests (push) Successful in 5m16s
Project CI / Native shell tests (push) Failing after 9m35s

保留 Agent Runtime、Runner、项目与测试的模块化拆分
吸收跨平台启动与本地开发栈稳定性修复
补齐 Gitea CI 门禁与异步测试稳定性改进
统一前端 Runtime 水合、策略读取与状态重置语义
This commit is contained in:
AIGameCreator App
2026-07-23 12:40:28 +08:00
58 changed files with 6490 additions and 565 deletions
@@ -459,6 +459,16 @@ DDD 边界检查:
npm run check:server-rs-ddd
```
## Gitea CI 与 PR 检查
- 仓库 CI 入口是 `.gitea/workflows/project-ci.yml`,向 `master`、`codex/ai-game-creator-app` 推送和所有 PR 创建、更新时必须运行,也允许手工触发。
- CI 固定拆分为 `Repository checks`、`Frontend tests`、`Backend tests`、`Native shell tests` 四个 required job;对应 PR context 完整名称是 `Project CI / Repository checks (pull_request)`、`Project CI / Frontend tests (pull_request)`、`Project CI / Backend tests (pull_request)`、`Project CI / Native shell tests (pull_request)`,首次运行后仍须从 Gitea 最近一周 context 表复核。测试使用独立 job,不能只藏在综合检查 step 中;原生壳验收单独运行以便定位重型构建失败。
- 四个 job 共同覆盖 `npm run check`,并追加 `npm run check:server-rs-ddd`、`cargo test --locked --workspace --no-fail-fast --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --all-targets --manifest-path server-rs/Cargo.toml` 和 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`。后端 runner 安装 `ffmpeg`,避免视频抽帧测试因工具缺失提前返回。`codex/ai-game-creator-app` 分支必须用独立 lockfile 安装 AI 游戏创作壳依赖,其原生壳入口还必须覆盖 `npm run ai-game-creator-shell:check`、release build smoke,并检查 AI Tauri `Cargo.lock` 不漂移。原生壳 job 还要通过 Google 官方签名 APT 源安装 `google-chrome-stable`,用于 headless preview 的真实 DOM / canvas smoke;同时安装 `ripgrep`,把 `actions/setup-node` 的完整 Node.js 22 发行目录与 root-owned rustup proxy 映射到 `/usr/local`,供只接受受信任系统命令目录的 `command.exec` 沙箱测试使用,不能放宽生产命令目录白名单。Tauri 的 1132 项级别 suite 固定 `--test-threads=1`,避免共享 Agent Runtime 后台锁与异步终态在 libtest 并行调度下互相干扰。
- checkout 必须使用完整历史。PR 将 base SHA 写入 `SPACETIME_SCHEMA_BASE_REF`,直接推送 `master` 使用 before SHA;事件基线不可解析时直接失败。Gitea 检查的是 PR head 而非预合并 commit,workflow 必须拒绝不包含最新 base commit 的过期 PR,分支保护同时保持“PR 过期禁止合并”。
- 普通 PR job 不读取业务 secret,不运行真实 API/SpacetimeDB/OSS/支付/生成/live smoke,也不执行会修改外部状态的维护、迁移、发布或备份命令。
- Gitea 至少升级到 `1.26.4` 后才能注册执行 PR job 的 runner;`ubuntu-latest` 标签只映射到固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像,不使用浮动镜像 tag,不映射 host,不向 job 暴露 Docker socket、业务 secret 或不必要内网。runner 能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm、Rust 分发、crates.io 和 Google Chrome 的 `dl.google.com` 官方签名 APT 源;workflow 的官方 action 固定完整 commit,若内网禁用 GitHub,先在当前 Gitea 镜像对应 commit 并改用绝对 URL。受控镜像优先预装 rustup。Gitea 1.26 的任务超时由 runner 全局配置控制;首次运行成功后,`master` 分支保护必须要求上述四个 job 全部成功。
- `genarrative-station` 当前使用 Gitea `1.26.4` + 基于 Runner `2.0.0-dind-rootless` 的固定 digest 修补镜像:只修复 `systempaths=unconfined` 的空 slice 被 `mergo` 丢失,真实 job 必须保持 `MaskedPaths=[]`、`ReadonlyPaths=[]`;外层仍非 privileged、无 `CAP_SYS_ADMIN`,内部 Docker 只监听 Unix socket,`docker_host: "-"` 阻止 socket 进入 job。job 只在 `gitea-actions` internal network,通过 `/git` reverse gateway 访问 Gitea,通过拒绝私网、保留地址和 metadata 的 80/443 proxy 访问公共依赖;直连公网和 Gitea 数据网必须失败。内层 bwrap 所需 namespace/proc 选项只能用于该 rootless DinD,不能放宽宿主 rootful runner。系统依赖步骤在 root job 中不调用 sudo,非 root 时用 `sudo -E` 保留受控 proxy;Cargo 关闭 HTTP multiplexing 并设置 10 次网络重试,rustup bootstrap/toolchain 安装也按有界次数重试。AI 原生壳 job 把 Node 发行目录与 rustup proxy 安装到 `/usr/local` 的受信任只读路径,并在测试前执行完整 bwrap canary。宿主 compose helper 必须把 `/opt/gitea-stack` 挂到同名绝对路径,避免相对 volume 错误落到 `/stack` 空目录。
## 后端相关默认验证
后端修改后,按 DDD 文档中的验收命令执行。涉及 API smoke 时:
+51 -8
View File
@@ -32,12 +32,28 @@
## 异步 Runtime 测试不能把 child idle 当成终态结果已发布
- 现象:isolated child 已显示 idle,单次 all-join reconcile 却偶发返回空列表,完整 Rust suite 里出现低概率失败,单独重跑通常通过。
- 原因:child Runtime 释放执行 lane 与持久化终态 result、发布 join readiness 不是同一个原子观测点;测试只等待 idle,会在终态 result 发布前抢先 reconcile。
- 处理:产品协议仍以 durable terminal result 和 join readiness 为准。测试在有界时限内重复调用幂等 reconcile,直到取得唯一 join 或超时;不得靠固定长 sleep,也不能因为第一次为空就把协议改成吞掉未完成 child。
- 验证:`isolated_agents_with_same_template_run_independently_and_join_once` 最多执行 100 次、每次间隔 20ms 的 reconcile,并继续断言只有一个 all-join 和一次父唤醒。
- 现象:isolated child 已显示 idle,单次 all-join reconcile 却偶发返回空列表;或者 Runtime 已显示 completed / failed,Goal、conversation、Agent DB 审计和 per-Agent lock 仍未完成,完整 Rust suite 里出现低概率失败,单独重跑通常通过。
- 原因:Runtime state、Goal sidecar、终态 result、conversation、审计记录、handoff 清理和执行 lane 释放不是同一个原子观测点;测试只等待 idle / failed 会在同一后台 drain 的 durable 收尾前抢先断言。
- 处理:产品协议仍以 durable terminal result 和 join readiness 为准。测试在有界时限内等待业务目标终态;需要断言同一 drain 的后续副作用时,同时以 per-Agent runtime task lock 释放为 fence,命中后重新读取投影。join 场景继续重复调用幂等 reconcile,直到取得唯一 join 或超时;不得靠固定长 sleep,也不能因为第一次为空就把协议改成吞掉未完成 child。
- 验证:`isolated_agents_with_same_template_run_independently_and_join_once` 最多执行 100 次、每次间隔 20ms 的 reconcile,并继续断言只有一个 all-join 和一次父唤醒;Goal、loop-budget、finalization 与 Supervisor reconciliation 测试必须在目标 status / phase 与 Agent lane 同时收束后再读取最终副作用。`background_agent_runtime_marks_response_plan_step_failed_when_final_reply_fails` 和 `background_agent_runtime_tasks_can_run_in_parallel_and_persist_replies` 同样必须经过该 fence 后再断言 `turn.failed` 或 `agent.runtime.completed` 审计。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/tests.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent.rs`。
## CI root 环境不能用文件只读权限注入写失败
- 现象:本地测试把 conversation 文件设为 readonly 后能稳定得到写入失败,Gitea Actions 中同一断言却发现写入成功并继续执行任务。
- 原因:隔离 job 内测试进程可能以 root 运行;root 不受普通 owner write bit 的同等限制,`set_readonly(true)` 不是跨 runner 身份的确定性故障注入。
- 处理:需要覆盖写失败恢复时使用仅在 `cfg(test)` 生效、一次性消费并限定写入阶段的 marker;生产路径仍走真实持久化函数。测试同时断言 marker 已消费、失败前数据未落盘和恢复后 exactly-once,不依赖 chmod、固定 sleep 或 runner 用户身份。
- 验证:在普通本地用户和 root 容器中分别运行用户消息、assistant 最终回复持久化失败测试,均应进入相同 durable phase 并通过恢复断言。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/project.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent.rs`、`apps/ai-game-creator-shell/src-tauri/src/tests.rs`。
## Ubuntu 容器不能把 chromium-browser 的 Snap 占位包当成 CI 浏览器
- 现象:AI 游戏创作壳的 1132 条 Rust 测试全部通过,尾部 `agent-run:smoke` 却以 `spawn google-chrome ENOENT` 失败;直接给 Ubuntu 24.04 job 安装 `chromium-browser` 仍拿不到可执行浏览器。
- 原因:Ubuntu 24.04 仓库里的 `chromium-browser` 是 Snap 过渡包,普通 Docker job 没有 snapd 宿主能力;固定 job image 也不预装 Google Chrome。脚本回退到命令名 `google-chrome` 后只能在本机通过,在干净 Runner 中必然 ENOENT。
- 处理:Native job 通过 Google 官方签名 APT 源安装 `google-chrome-stable`,安装后先执行 `google-chrome --version`;smoke 继续真实启动 headless 浏览器验证 DOM / canvas,不允许因 CI 缺浏览器而跳过或降级为静态 HTTP 检查。
- 验证:固定 Ubuntu 24.04 job image 内先确认 `apt-cache policy chromium-browser` 仅为 Snap 占位,再安装官方签名包并运行 `google-chrome --version`;Gitea Native job 最终必须在 1132 passed / 5 ignored 后继续通过 `agent-run:smoke`。
- 关联:`.gitea/workflows/project-ci.yml`、`apps/ai-game-creator-shell/scripts/smoke-agent-run-local-provider.mjs`。
## PTY 测试不能假设输入回显与后续输出必然分行
- 现象:PTY 环境隔离用例偶发得到 `你好BRIDGE_ENV:`,而不是独立的 `你好` 与 `BRIDGE_ENV:` 两行;真实私有环境变量并未泄漏,但整行相等断言失败。
@@ -3251,8 +3267,8 @@
- 现象:target 注册了 SIGTERM 清理逻辑,但 `command.terminate` 只偶尔出现 stopped marker;耗时 300-500ms 的清理经常被提前截断。
- 原因:如果先向 wrapper/bwrap/trampoline/target 共用的外层进程组发送 SIGTERM,wrapper 会先退出,bwrap 的 die-with-parent 随即收走 namespace;名义上的 800ms 宽限并没有真正留给 target。
- 处理:process-session target 在 child pre-exec 内暂时屏蔽 SIGTTOU,完成 setpgid + PTY slave tcsetpgrp 并恢复信号掩码后才 exec;不能先 spawn 到后台组再由 parent 设前台,否则 target 可能已经因 immediate read 收到 SIGTTIN。Runtime 通过两级私有控制通道请求 trampoline 只向 target group 发 SIGTERM。direct leader 退出后 trampoline 继续检查同组后代,外层 wrapper/bwrap 在最多 800ms 宽限期保持存活,超时才强杀 containment group。
- 验证:使用直接 bash target 启动同组后台子进程;leader 在输出 READY 后自然退出,仍存活的子进程收到 TERM 后由 trap 延迟 400ms 写 marker 并退出,terminate 返回前 marker 必须存在。另跑 immediate stdin/EOF、Runner owner SIGKILL 和后代隔离用例,确认前台切组没有破坏交互或 fail-closed 回收。
- 处理:process-session target 在 child pre-exec 内暂时屏蔽 SIGTTOU,完成 setpgid + PTY slave tcsetpgrp 并恢复信号掩码后才 exec;不能先 spawn 到后台组再由 parent 设前台,否则 target 可能已经因 immediate read 收到 SIGTTIN。Runtime 通过两级私有控制通道请求 trampoline 只向 target group 发 SIGTERM。direct leader 退出后 trampoline 继续检查同组后代,外层 wrapper/bwrap 在最多 800ms 宽限期保持存活,超时才强杀 containment group。reader 发现未换行输出超过上限时必须先原子投影 `output-limit-exceeded` 并唤醒 poll,再异步发送终止控制,不能让高负载下的 supervisor 调度延迟把已越界进程继续暴露为 `running`。
- 验证:使用直接 bash target 启动同组后台子进程;leader 在输出 READY 后自然退出,仍存活的子进程收到 TERM 后由 trap 延迟 400ms 写 marker 并退出,terminate 返回前 marker 必须存在。正式 `command.exec` 测试夹具仍必须走允许的 `npm run` 等程序,不能为了构造 stdin race 绕过白名单直接解析 `bash -lc`。另跑 immediate stdin/EOF、Runner owner SIGKILL 和后代隔离用例,确认前台切组没有破坏交互或 fail-closed 回收;测试互斥锁在前序 panic 后应恢复 guard 继续报告后续独立结果,不能用 `PoisonError` 掩盖真实失败范围。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/process_session.rs`、`process_session_bridge.rs`、`command_sandbox_trampoline.rs`。
## 启动记录必须封闭状态组合,child 不能自行猜 durable commit 超时
@@ -3295,14 +3311,14 @@
- 处理:正式主聊天只路由到 `project-supervisor` active Session,活跃期输入继续 same-run steer;同一父 run 最多同时保留 3 个 `dispatched / ready` 静态专业委派,已预留的同 action delivery 恢复复用原 target Session/run,不另占名额。同一工具计划完成委派后,Runtime 在下一次 Provider planning 前直接持久化 `waiting-for-delegate-receipts` 并释放 lane,不让模型轮询等待。delivery 单向推进 `dispatched -> ready -> claimed-by-parent / suppressed`,claim 单向推进 `Prepared -> Committed -> Observed`;先持有 claim 锁,再对 delegationId 排序去重并按序取齐 delivery 锁,任一锁不可得时零状态推进。delivery / claim journal 与 pending observation 是事实源;Agent DB append 只能 best-effort,失败不得推翻已持久化结果。入队在 Session lane 内完成,Runner 通知在 lane 外发送;`agent.run_status` 保留 claim 身份校验但不绑定全局 project revision/fingerprint。
- 恢复门禁:只有 `project-supervisor` 的 executing `agent.delegate / agent.run_status` 可在项目锁内重验 durable pending、Session/run/action fingerprint、delivery/claim/child 身份和当前 policy 后补交;只有 delivery 预留且无 child 时,拒绝动作必须把该预留 CAS 为 suppressed。其他 executing 动作或副作用身份不明必须进入 `needs-reconciliation`。parent-wake 以 project/Agent/run 做 coalescing singleflight,新信号不能在已有 worker 退出窗口丢失;有界重试接受 lane 竞争、暂时连接、连接中止、broken pipe、unexpected EOF、资源暂不可用和超时类错误。损坏 journal、身份冲突及重启扫描中的损坏 barrier 直接投影 reconciliation。External Runner wake 用项目根、method、Agent、runId 和 loop iteration 派生稳定 requestId,目标未观察到、仍 waiting 或 lane 忙时返回不缓存的可重试错误。
- 身份与收束:子终态发布前同时核对 parent Agent/Session/run/action、delegationId 派生、target Agent/Session/run、child source 和 child 反向 parent/delegation 链接。错配 child 保持原 delivery 不变并记录冲突;父任务先进入 completed / failed / cancelled / budget-exhausted 时,终态写入路径 suppress 尚未认领的匹配 delivery,合法迟到 child 不能重新写 ready。父 run 在 waiting、ready-unclaimed 或 unobserved claim 任一非零时都不得 final;全部清零后仍由原 Supervisor Session/run 的 finalization journal 幂等写入唯一 assistant,不创建新 receipt run。
- 验证:Rust 定向回归使用 `project_supervisor_` 前缀,覆盖 delivery/claim 状态机、同 action 幂等、第 4 个新委派拒绝与已预留委派复用/拒绝 suppression、后续 delivery 锁忙时零部分认领、Agent DB 故障后回执仍可重放、未 Observed 阻断 final、Provider planning 前 durable 等待、parent-wake coalescing/结构性错误、重启损坏 barrier、错配和迟到 child、executing `run_status` 续接与 delegate policy 重验;`agent_background_enqueue_notifies_only_after_session_lane_release` 覆盖入队锁序,Runner 内部测试覆盖定向 wake 与不缓存重试。真实 Provider 必须同时证明专业 Agent 时间区间重叠、父 run 仅一次 waiting、同一 Observed claim 认领全部回执、唯一 assistant、第二轮历史引用不新增委派和项目范围密钥扫描为 0。
- 验证:Rust 定向回归使用 `project_supervisor_` 前缀,覆盖 delivery/claim 状态机、同 action 幂等、第 4 个新委派拒绝与已预留委派复用/拒绝 suppression、后续 delivery 锁忙时零部分认领、Agent DB 故障后回执仍可重放、未 Observed 阻断 final、Provider planning 前 durable 等待、parent-wake coalescing/结构性错误、重启损坏 barrier、错配和迟到 child、executing `run_status` 续接与 delegate policy 重验;本地 mock Provider 长套件应允许一次短间隔 connectivity 重试,并在断言前同时等待终态投影和 Agent lane 释放,避免端口瞬时波动或后台收尾窗口制造假失败。`agent_background_enqueue_notifies_only_after_session_lane_release` 覆盖入队锁序,Runner 内部测试覆盖定向 wake 与不缓存重试。真实 Provider 必须同时证明专业 Agent 时间区间重叠、父 run 仅一次 waiting、同一 Observed claim 认领全部回执、唯一 assistant、第二轮历史引用不新增委派和项目范围密钥扫描为 0。
- 关联:`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`、`apps/ai-game-creator-shell/src-tauri/src/delegation.rs`、`agent.rs`、`runner.rs`、`tests.rs`。
## 父 run 协作策略不能在绑定后继续按全局 live policy 重验
- 现象:同一 Supervisor 父 run 已经持久化合法 collaboration batch,管理员随后修改或损坏 `.agent/collaboration-policy.json`,后续 spawn、claim、mutation、MCP 或 finalization 却突然改用新策略、进入 reconciliation;或者 snapshot 被删除后,Runtime 又按 live policy 把已有 run 当成未绑定 run。另一类症状是 contractless/v1 batch 被跳过、两个不安全 run ID 经字符替换落到同一 snapshot/锁 key,或旧 `Prepared / Committed` claim 因 snapshot/binding 不可读而不能重放 observation。
- 原因:把项目级 policy 当成每个动作的 live 执行事实,没有为父 run 设置明确线性化点、不可变策略快照和独立“曾绑定”记录;或者在 v2 batch 完整验真前就用 `contract.policy` 播种 snapshot。只对 run ID 做 lossy 规范化、让锁复用该路径片段,或用通用原子 replace 代替同一身份锁内 CAS,也会制造路径碰撞、并发覆盖和伪合同漂移。
- 处理:V1.38 固定顺序为 `v2 batch -> snapshot -> binding sidecar -> action side effects`。snapshot 位于 `.agent/runtime/collaboration-policy-snapshots/<agentKey>/<runKey>.json`,其完整字段必须统一为 `schemaVersion / projectId / parentAgentId / parentRunId / boundFrom / policy / policyFingerprint / snapshotFingerprint / boundAt`;snapshot fingerprint 覆盖除 `snapshotFingerprint / boundAt` 外的全部稳定字段。独立 binding 位于 `.agent/runtime/collaboration-policy-snapshot-bindings/<agentKey>/<runKey>.json`,固定包含 `schemaVersion / projectId / parentAgentId / parentRunId / boundFrom / policyFingerprint / snapshotFingerprint / boundAt`,与 snapshot 逐字段交叉验证并持久证明“该 run 曾绑定”。
- 处理:V1.38 固定顺序为 `v2 batch -> snapshot -> binding sidecar -> action side effects`。snapshot 位于 `.agent/runtime/collaboration-policy-snapshots/<agentKey>/<runKey>.json`,其完整字段必须统一为 `schemaVersion / projectId / parentAgentId / parentRunId / boundFrom / policy / policyFingerprint / snapshotFingerprint / boundAt`;snapshot fingerprint 覆盖除 `snapshotFingerprint / boundAt` 外的全部稳定字段。独立 binding 位于 `.agent/runtime/collaboration-policy-snapshot-bindings/<agentKey>/<runKey>.json`,固定包含 `schemaVersion / projectId / parentAgentId / parentRunId / boundFrom / policyFingerprint / snapshotFingerprint / boundAt`,与 snapshot 逐字段交叉验证并持久证明“该 run 曾绑定”。同一 run 并发绑定时,无论 loser 是读取到不同快照还是在 winner 持锁期间耗尽有界等待,都必须返回稳定的“并发绑定冲突”错误分类。Unix 同进程首次并发初始化安全锁路径时,需要短暂串行化 `mkdirat/openat` 打开阶段,规避 macOS loser 在最终 `O_CREAT` 前观察到瞬时 `ENOENT`;返回后的 `flock` 仍承担跨线程、跨进程互斥。
- 路径与恢复:不安全或规范化后变化的 Agent/run ID 使用有界安全前缀加原始 ID 稳定 SHA-256,锁 key 对完整 `parentAgentId + NUL + parentRunId` 计算稳定 SHA-256,不能只做字符替换。恢复顺序为 existing valid snapshot > 完整验真的 v2 batch contract > 符合严格状态门禁的 legacy 当前有效 policy;snapshot 缺 binding 可从 snapshot 补写,binding 存在但 snapshot 丢失只能按可信 v2 contract 和首次绑定身份恢复,无可信 v2 时禁止 live policy 重绑。contractless/v1 collaboration batch 必须先失败关闭。`legacy-current-project-policy` 只允许无 snapshot/binding、无可信 v2 contract,且不存在上述旧 batch,并由可信身份和状态明确证明属于 `pending / running / waiting-for-confirmation / waiting-for-user-input` 的旧父 run;terminal、`needs-reconciliation` 或身份/状态未知 run 的状态读取不得新建 snapshot。
- 漂移与 Claim:绑定后 global policy 的 `matched / drifted / unreadable` 只报告状态,不能改变后续动作或完成门禁;新 policy 只用于后续新父 run。旧 durable claim、未观察 claim 和 legacy claimed delivery 先按原 action/group 身份恢复且不得取得新 delivery;新的 claim 必须先成功解析 effective snapshot 并核对 binding,再执行 V1.35-V1.37 的全锁、预算、完整 observation 和 group 数量门禁。
- 真实 E2E 现场:正在运行的正式客户端可能在验收期间启动或重启正式 Runner,导致 source endpoint 身份真实变化。不得关闭 `sourceRunnerEndpointUnchanged` 门禁,也不得杀掉不属于验收器的进程;应把同一配置内容复制到仓库外的大容量磁盘私有目录,目录/文件权限分别为 `0700/0600`,不复制 endpoint、锁、会话或数据库,验收后删除。功能完整但 source endpoint 被外部改变的报告与后续干净清理报告不得拼接。
@@ -3560,3 +3576,30 @@
- 恢复:给既有持久 batch 增加新职责时必须升级 schema;新 v3 按新合同失败关闭,旧 v2 collaboration batch 与 v1 contractless batch 继续按原 fingerprint 和创建时语义恢复,不能在反序列化时用新规则误杀升级中的无人干预轮次。
- 验证:正向只读与否定式可修改分别做定向回归,并额外让只读 quality Agent 尝试 `file.write`,证明目标文件和 revision 均不变且下一请求只广告 `respond_to_user`;同内容 v3 batch 必须失败关闭,v2 必须可恢复。真实外部 E2E 必须证明首批两份委派建立、程序 Agent 实际推进 `game/index.html` revision、质量 Agent mutation 为零、父 run 最终 settled,并保持人工输入和终局残留为零。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/autonomous_policy.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_tool_plan.rs`、`apps/ai-game-creator-shell/src-tauri/src/agent/runtime_actions/provider_action_batch.rs`。
## Gitea PR runner 不能把宿主 Docker socket 当普通 volume
- 现象:`act_runner` 的 `container.docker_host` 留空时,job inspect 会出现 `/var/run/docker.sock:/var/run/docker.sock`;PR 脚本即使 `valid_volumes: []`,仍可直接调用 Docker API读取其它容器、挂宿主目录或取得 runner 配置。另一类迁移故障是 rootless daemon可以启动,但 bwrap 在 job 内报 `No permissions to create new namespace`、`Failed to make / slave` 或 `Mount too revealing`。
- 原因:空 `docker_host` 会自动发现并把控制 socket 传播到 job;rootful Docker 的 seccomp、AppArmor 与 system-path masks 又不支持完整 nested bwrap。Runner 2.0.0 还有一处独立合并缺陷:`parseSystemPaths` 把 `systempaths=unconfined` 转成显式空 slice 后,`mergo.WithOverride` 不覆盖 empty value,真实 job 又恢复 Docker 默认 masks,表现为配置文件写了 unconfined、手工 `docker run` canary 也成功,但 Actions job 仍在 `--proc /proc` 返回 EPERM。直接使用 `--privileged`、外层 `CAP_SYS_ADMIN` 或 host executor 会把测试跑绿建立在破坏 PR 隔离的前提上。宿主启用 Clash fake-IP 时,简单按 DNS 的 `198.18.0.0/15` 判断公网还会误拒所有公共依赖。
- 处理:先把 Gitea 升到至少 `1.26.4`,再用固定 digest 的 Runner 2.0.0 rootless DinD;外层保持非 privileged、无 `CAP_SYS_ADMIN`,内部 Docker 仅 Unix socket,runner 固定 `docker_host: "-"`。若版本仍有上述 empty-slice 缺陷,只做 merge 后保留 `MaskedPaths=[]` / `ReadonlyPaths=[]` 的最小补丁并固定自有镜像 digest,不对整个 HostConfig 启用 overwrite-empty。job 使用独立 internal network,Gitea 经 reverse gateway,公共 80/443 经使用公共 DoH 验证真实 IP、拒绝私网/保留地址/metadata 的 proxy;DoH 要缓存并合并同域并发,长下载 timeout 不能只有 60 秒。rootless job 内的 bwrap namespace/proc 选项不能复制到宿主 rootful runner。apt 步骤在 root 时直接执行,非 root 时用 `sudo -E`,否则 sudo `env_reset` 会让 apt 丢失 proxy。
- 运维陷阱:从容器内运行 Compose 时,宿主 `/opt/gitea-stack` 必须挂到容器同名绝对路径;挂成 `/stack` 会让相对 bind source 被 daemon解析为宿主 `/stack/...`,表现为 Gitea进入空安装页、gateway 脚本“缺失”。发现后不要迁移空库,立即用同路径 mount 重建并核对原数据大小、installed 日志、仓库数和 API。切换前保留冷数据 tar、pg_dumpall 和原 compose/env/runner 配置,备份与 token 不提交 Git。
- 验证:同时检查 Gitea 版本、runner declare、外层 `Privileged=false`/无 CapAdd/无宿主 socket、inner job `Binds=[]`、`MaskedPaths=[]`、`ReadonlyPaths=[]`、固定 image digest、`/var/run/docker.sock` 不存在、公共 proxy 可用、直连公网/Postgres/metadata 失败,以及完整 bwrap canary。AI 原生壳的共享 Agent Runtime 后台锁 suite 固定单线程执行;并行全量出现锁或异步终态失败、逐项单线程全部通过时,修正 suite 调度口径,不放宽断言。最后重跑四个 CI job;checkout 成功但 apt/rustup/npm 同时失败时,先排 proxy/env,而不是改测试。
- 关联:`.gitea/workflows/project-ci.yml`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`、`docs/project-memory/shared-memory/development-workflow.md`。
## Unix 文件身份复核不能假定 Linux 的 `dev_t` 类型
- 现象:AI 游戏创作 Tauri 壳在 Linux CI 编译通过,但 macOS 上会在 Agent DB、External Runner owner 和 tool-plan handoff 的 `fstatat` 身份复核中报 `i32 == u64` 类型错误;Tauri 失败后配套后端收束,终端还可能短暂出现 SpacetimeDB 订阅连接失败的连锁日志。
- 原因:`libc::stat.st_dev` 跟随平台 `dev_t`,macOS 为有符号整数,而 `std::os::unix::fs::MetadataExt::dev()` 统一返回 `u64`;直接比较会把 Linux 的类型偶合误当成 Unix 通用契约。
- 处理:与 Rust 标准库的 Unix `MetadataExt` 实现保持一致,先把 `st_dev / st_ino` 规范为 `u64`,再与 `metadata.dev() / metadata.ino()` 比较;设备号、inode 和文件类型三重检查均必须保留。
- macOS 测试夹具:`std::env::temp_dir()` 可能返回 `/var/folders/...`,而 `/var` 是系统兼容符号链接。需要真实项目根的 Runtime 测试应先 canonicalize 已存在的临时根目录,再创建唯一子目录;不得为了让夹具通过而放宽生产 Runtime 的项目根及祖先符号链接拒绝规则。
- 验证:macOS 本机运行 `cargo check --locked --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml`,并复跑 Agent DB、project owner 和 tool-plan handoff 的 Unix 相对句柄替换检测;Linux CI 继续覆盖原有安全回归。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/project.rs`、`runner.rs`、`tool_plan_handoff.rs`。
## AI 游戏创作壳不能用全局或匿名身份发布本地模块
- 现象:`npm run agc` 在发布模块时先访问 `auth.spacetimedb.com` 并以 401 失败;改成 `--anonymous` 后首次可能成功,但再次启动会因匿名 identity 变化而 403。若把 403 当成可忽略警告继续启动,api-server 会连接旧 schema,随后持续输出 `external_generation_job`、`profile_recharge_order_expiration_timer` 等缺表订阅失败,Tauri 也可能在后端就绪前退出或迟迟不弹窗。
- 原因:本地 publish 默认继承开发者全局 SpacetimeDB 云端登录,离线时 standalone 无法校验 issuer;`--anonymous` 不是可跨进程持久复用的 owner identity;AI 游戏创作壳若再复用主站历史数据目录,还会继承旧数据库归属和旧 schema。
- 处理:AI 游戏创作壳固定使用 gitignored 的独立数据目录;standalone 就绪后先从 `/v1/identity` 获取并按 data dir 而非监听端口持久化同一 API identity,再用数据目录内权限为 `0600` 的独立 `cli.toml` 执行 `spacetime login --token` 和 publish。旧端口作用域记录在同一 data dir 下身份唯一时迁移,存在多个不同身份时失败关闭,不能猜 owner。远程 server 继续使用正常登录配置;本地 publish 403 必须阻断 API/Vite,不得带旧 schema 降级启动。`.app/dev-stack.json` 记录规范化 data dir,独立壳复用后端时必须同时匹配数据库名、专用目录和健康状态;缺少目录字段的旧状态不得复用。POSIX 启动器在 `spawn` 后立即监听 `error / exit`、保存 detached leader 的 PGID、向外层登记句柄并用独立进程组收束 npm、Node、Cargo 和子进程;direct leader 先退出后仍向负 PGID 发信号清理后代,ready 前中断、超时或 ENOENT 也走统一清理,退出后确认 3080、8082、3101 均释放。
- macOS 日志:api-server 进程指标当前只实现 Windows API 和 Linux `/proc`,macOS 必须跳过 observable callback 注册;不能每轮采集为每个指标重复打印“不支持平台”。Rust/Tauri 既有 `dead_code` warning 与一次性配置缺失提示不属于长驻重试日志。非 Linux `project.verify` 校验 `npm run` 参数时必须越过 `--silent`、`--ignore-scripts` 等前置选项定位真实脚本名,不能固定读取 `run` 后第一个参数,否则会在 macOS 将合法验证误报为“缺少脚本名”并引发 Runtime 测试级联失败。
- 验证:定向测试覆盖同一 data dir 跨端口复用 identity、不同 data dir 隔离、旧 state/data dir 不匹配拒绝复用、spawn ENOENT 受控失败、direct leader 以 42 退出后同组 descendant 仍收到 TERM,以及后端 ready 前句柄已登记且超时清理。连续运行两次 `npm run agc`,两次都必须真实完成 module publish、`/v1/ping`、`/healthz`、Vite 3080 和 Tauri `Running`;稳定观察期间不得出现缺表订阅失败或进程指标平台告警,Ctrl-C 后三个端口和主 Tauri 进程均应释放。
- 关联:`scripts/dev.mjs`、`apps/ai-game-creator-shell/scripts/start-dev-stack.mjs`、`server-rs/crates/api-server/src/process_metrics.rs`。
@@ -472,6 +472,7 @@ game-project/
- `apps/ai-game-creator-shell` 是独立 Tauri App,不复用 `apps/desktop-shell`。
- 独立客户端启动时先进入平台登录检查;未登录页默认展示手机号验证码登录,并保留密码登录切换。验证码登录调用平台后端 `/api/auth/phone/send-code` 与 `/api/auth/phone/login`,密码登录继续调用 `/api/auth/entry`;Tauri dev 下 `/api` 走固定 3080 Vite 代理,发布版静态窗口下登录请求默认直连本机配套 `http://127.0.0.1:8082` API,网络层失败时展示登录服务不可达提示,不裸露 WebView 的 `Load failed`。
- `npm run agc` 的本地 SpacetimeDB owner identity 以独立 `spacetimeDataDir` 为作用域,不绑定可能漂移的监听端口;旧端口作用域记录仅在同一 data dir 下身份唯一时自动迁移,出现多个不同旧身份时失败关闭。`.app/dev-stack.json` 必须记录规范化 `spacetimeDataDir`,独立壳只复用数据库名和该目录同时匹配且健康的后端,旧 schema 状态或共享目录状态缺少此字段时不得复用。POSIX 子进程在 `spawn` 返回时立即登记 `error / exit` 生命周期、保存 detached leader 的 PGID 并把句柄交给外层;即使 direct leader 已先退出,也必须继续向负 PGID 发信号清理同组后代。后端 ready 前的 SIGINT、SIGTERM、超时或 ENOENT 都必须走同一进程组清理链路,不能遗留 npm、Cargo 或 SpacetimeDB。非 Linux Runtime 执行 `project.verify` 时,`npm run` 参数校验必须允许受控的 `--silent`、`--ignore-scripts` 位于脚本名前,并继续拒绝缺少真实脚本名的调用。
- Tauri Rust 入口保持薄壳:`src-tauri/src/main.rs` 只保留共享类型 / 常量、模块声明、CLI preflight、`tauri::Builder`、运行时配置初始化和 `invoke_handler` 清单;命令行入口放在 `cli.rs`,Tauri command 包装放在 `commands.rs`,运行时配置与 LLM 配置检查放在 `config.rs`,Agent loop 与生成编排放在 `agent.rs`,上传 / 画板 / 平台美术生成接入放在 `assets.rs`,本地项目文件、记忆、对话、权限、checkpoint、manifest 和通用路径工具放在 `project.rs`,本地 HTTP 预览与 preview 命令放在 `preview.rs`,旧窗口兼容命令放在 `windows.rs`,Rust 单测放在 `tests.rs`。后续继续拆分时保持 Tauri command 名、JSON 字段、`.agent/*` 路径和错误语义不变。
- 本地项目初始化会创建 `game/`、`assets/`、`memory/`、`memory/agents/`、`exports/`、`.agent/logs/`,写入 `.agent/manifest.json`,生成 append-only JSONL 本地项目索引 `.agent/agent.db`,并生成默认 `game/index.html`。
- v1 conversation 记录使用 append-only JSONL,每行带 `schemaVersion`、`role`、`content`、`agentId` 和 `updatedAt`,作为聊天历史和单 agent 对话历史的事实源;目录在首次写入时创建。
@@ -497,6 +498,9 @@ game-project/
- 终端可用 `npm run ai-game-creator-shell:agent-run -- /绝对项目路径 "游戏创作需求"` 跑一次真实 LLM 生成、落盘、`game.static_smoke` 和本地 HTTP 预览;发布 App 读取 Tauri 应用配置目录中的 `game-creator.config.json`,开发 CLI 无 AppHandle 时才读取仓库旁边的配置模板和 gitignored 本机覆盖文件,不把 API Key 写入仓库或项目文件。自动验证可加 `--no-wait`,例如 `npm run ai-game-creator-shell:agent-run -- --no-wait /tmp/genarrative-ai-game-test "像素风反弹弹幕厨房"`,生成预览 trace 后立即停止本地预览,避免终端卡在回车等待。默认 API kind 为 `openai_responses`;旧 Chat Completions 兼容网关设置 `llm.apiKind` 为 `openai_chat`,Anthropic Messages 网关设置 `llm.apiKind` 为 `anthropic`。真实 OpenAI-compatible 网关建议设置 `llm.stream` 为 `true` 跑 Planner 和 Generator,避免长请求非流式空闲断连。
- 终端可用 `npm run ai-game-creator-shell:agent-run:smoke` 跑一次无密钥本地端到端 smoke:脚本启动本机 OpenAI-compatible SSE 流式测试 provider,预置一个本地上传图片和一个本地上传音频,复用真实 `--agent-run`、Planner / Orchestrator / 角色 agent / Generator / Evaluator loop、本地落盘、`game.static_smoke` 和本地 HTTP 预览,并断言每次 provider 请求都使用 `stream: true`、Planner 与 Generator 分别命中自己的 `agentLlm` provider 配置、provider prompt 收到图片与音频资产上下文以及最近对话上下文、生成 HTML 引用这些资产、预览服务能用 `GET` 读取 `/assets/...`、用 `HEAD` 返回真实资源长度和对应 MIME、headless Chrome 打开预览后至少执行一帧游戏 JS,且通过确定性亮色探针采样证明 canvas 不是空白画布、`.agent/run.latest.json` 的 step group 覆盖 design / balance / art / audio / code / publishing 六组、第二轮会重跑 Evaluator 命中任务及其下游影响任务,未受影响角色 carry-over;随后脚本自动给 CLI 发送回车停止预览。该脚本只用于开发验证,不进入产品生成路径。
- `npm run ai-game-creator-shell:dev` 的 Tauri `devUrl` 固定为 `http://127.0.0.1:3080/`,Vite 必须 `strictPort` 对齐;`beforeDevCommand` 先复用已经跑在 3080 且页面标题为 `AI 游戏创作` 的本 app Vite server,否则才启动新的 Vite,若端口被其它服务占用则直接失败并提示释放端口。
- AI 游戏创作 App 的本地后端使用 gitignored 的 `server-rs/.spacetimedb/ai-game-creator/data`,不复用主站旧 standalone 数据目录。启动器从本地 `/v1/identity` 获取并持久化 API identity,再通过数据目录内 `0600` 的独立 `dev-cli/cli.toml` 发布模块;不得读取或覆盖开发者全局 SpacetimeDB 登录,也不得回退到每次变化的 `--anonymous` 身份。发布失败时 API 和 Vite 不得继续启动旧 schema,避免 `external_generation_job` 等缺表订阅进入持续重试。
- `start-dev-stack.mjs` 在 POSIX 下以独立进程组托管后端和 Vite,关闭 Tauri 或任一子进程失败时必须收束整组;macOS 不注册仅支持 Windows/Linux 的 api-server 进程指标 observable callback,避免每轮指标采集重复输出平台不支持告警。
- Unix 下 Agent DB、External Runner owner 和 tool-plan handoff 的相对句柄复核必须同时比较设备号、inode 和文件类型;`libc::stat` 的 `st_dev / st_ino` 先按 Rust `MetadataExt` 的 Unix 口径规范为 `u64` 再比较,保持 Linux 和 macOS 的同一安全语义,不得为了通过 macOS 编译而删除路径替换检测。
- AI 游戏创作 App 的 Vite root 保持在 `apps/ai-game-creator-shell`,但开发服务器必须通过 `server.fs.allow: [repoRoot]` 允许加载 `packages/shared/src` 共享契约;配置自检同时守住该规则,避免 typecheck 通过后真实 Tauri WebView 因共享源码 403 变成白屏。Tauri 事件 capability 只向 `client`、`developer`、`main`、`launcher` 窗口开放 `core:event:allow-listen` 和 `core:event:allow-unlisten`;Runtime 事件仍由 Rust 发出,前端不获得 `emit` 权限。
- `.agent/manifest.json` 会保存 6 个专业组下 16 个组内角色任务状态,当前覆盖 `Director`、`Gameplay`、`Difficulty`、`Asset`、`Polish`、`SFX`、`Code`、`Review`、`Preview`、`Playtest`、`Publish`;程序组内显式包含 `quality-review` 质量评审 gate,由 Evaluator trace 标记完成;开发窗口的专业组面板读取 manifest,而不是前端硬编码。
- 主窗口的 agent 状态列表以 manifest 角色任务为底表,再合并最近 run trace 中 `taskGraph.tasks` 的任务状态、同 taskId / group / role 的最新 step 状态、输入输出路径、错误摘要、lifecycleStatus 和 `activeTaskIds` / `carriedTaskIds` / `readyTaskIds` 编排标记;如果 trace 缺失或过期,只展示 manifest 的静态任务状态和“暂无最近运行证据”。
@@ -198,6 +198,29 @@ npm run check
`npm run build` 由 `scripts/build-gate.mjs` 串行构建主站和后台;该门禁会把 Vite warning 当成失败处理。若看到 `Build gate failed because warnings were emitted`,先看 warning 原文,例如 chunk 体积超过 `vite.config.ts` / `apps/admin-web/vite.config.ts` 的 `chunkSizeWarningLimit`,不要先按 Rust 编译失败排查。
### Gitea Actions PR 门禁
仓库级 Gitea Actions 工作流固定为 `.gitea/workflows/project-ci.yml`,在向 `master` 或 `codex/ai-game-creator-app` 推送、创建或更新 PR,以及手工触发时运行。工作流拆成四个必须通过的 job:
- `Repository checks`:执行 `npm run lint`、主站与后台生产构建、内容数据检查和提交差异空白检查。
- `Frontend tests`:先按根 lockfile 与 `apps/ai-game-creator-shell/package-lock.json` 分别执行 `npm ci`,再独立执行根 `npm run test`,让 Vitest 文件数和测试数在 Gitea job 列表中明确可见。
- `Backend tests`:执行 `npm run check:server-rs-ddd`、`cargo test --locked --workspace --no-fail-fast`、`api-server --all-targets` 编译和 `spacetime-module` 编译;runner 安装 `ffmpeg`,避免视频抽帧测试因工具缺失提前返回。依赖真实服务或密钥的测试必须显式 `ignored`,不能让普通 PR job访问现场环境。
- `Native shell tests`:按根 lockfile 与 AI 游戏创作壳独立 lockfile 安装依赖后执行 `npm run check:native-shells`,覆盖微信壳、Expo 和 Tauri 的完整验收,并确认桌面壳与 AI 游戏创作壳的 `Cargo.lock` 都没有被构建过程改写,避免把重型原生壳或依赖锁漂移隐藏在基础检查末尾。`codex/ai-game-creator-app` 分支的同名脚本还会执行 `npm run ai-game-creator-shell:check` 和 AI 游戏创作壳 release build smoke。该 job 会通过 Google 官方签名 APT 源安装 `google-chrome-stable`,用于 headless preview 的真实 DOM / canvas smoke;同时把 `actions/setup-node` 安装的 Node.js 22 与 npm 映射到 `/usr/local/bin`,满足 `command.exec` 仅信任系统命令目录的安全边界,不允许为适配 CI 放宽生产白名单。
四个 job 合起来覆盖根 `npm run check`,并补齐根检查没有包含的 server-rs DDD、正式 workspace Rust 测试与现役后端编译门禁。普通 PR CI 不注入业务密钥,不启动真实 API、SpacetimeDB、OSS、支付、图片生成或生产 live smoke;需要现场环境、可变外部状态、Docker 编排或发布凭据的 `check:*` 继续按对应专题和 Jenkins 发布流程执行,不能遍历所有同名前缀脚本冒充 PR 门禁。
PR checkout 必须保留完整 Git 历史,并把 PR base SHA 传给 `SPACETIME_SCHEMA_BASE_REF`。`check:spacetime-schema` 依赖该基线识别已有表字段删除、改名、重排和改类型;事件给出的基线缺失或本地不可解析时必须直接失败,不能退化为空差异检查。Gitea 的 PR checkout 是 PR head,不是与目标分支的预合并 commit,因此 workflow 还会验证 PR head 包含事件中的最新 base commit;分支保护必须继续开启“PR 过期禁止合并”,过期分支先更新再重跑。向 `master` 直接推送时使用 push before SHA,手工触发时回退到 `origin/master`。
启用或注册执行 PR job 的 runner 前,Gitea 服务端必须至少升级到 `1.26.4`;不得在 `1.26.2` 上执行不受信任 PR 代码。runner 必须提供 `ubuntu-latest` 标签,并将其映射到经验证且固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像;禁止使用浮动镜像 tag,禁止将该标签映射到 host 执行器,禁止向 job 暴露 Docker socket、业务环境变量、业务密钥或不必要的内网。workflow 会安装 Node 22、仓库 `rust-toolchain.toml` 固定的 Rust 1.96.0,以及 clang/lld、Tauri Linux 依赖和 headless preview 使用的 Google Chrome;受控 runner 镜像应预装 rustup,fallback 下载只用于首次引导。runner 仍需能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm registry、Rust 分发、crates.io,以及 Google Chrome 的 `dl.google.com` 官方签名 APT 源。workflow 中的 `actions/checkout` / `actions/setup-node` 固定到完整 commit;内网 runner 不允许访问 GitHub 时,先把对应 commit 镜像到当前 Gitea 并把 workflow 改为绝对 action URL。首版不使用 Actions cache,避免未配置 runner cache 网络时把缓存恢复错误变成 PR 失败。当前 Runner 2.0.0 已支持 job 级 `timeout-minutes`,但 runner 全局 `3h` 仍是所有任务的硬上限;若 workflow 以后新增更短 timeout,不能删除全局兜底。
当前 `genarrative-station` 已于 2026-07-21 升级到 Gitea `1.26.4`,并使用基于 Gitea Runner `2.0.0-dind-rootless` 的固定 digest 修补镜像与固定 digest 的 Ubuntu 24.04 job image。Runner 2.0.0 会先把 `systempaths=unconfined` 解析为空 `MaskedPaths` / `ReadonlyPaths`,再被 `mergo.WithOverride` 当成 empty value 丢失;站点修补只在 merge 后保留这两个显式空 slice,不改其它 runner 行为。真实 job inspect 必须看到 `MaskedPaths=[]`、`ReadonlyPaths=[]`、`SecurityOpt=[seccomp=unconfined]`、`Privileged=false`、无 CapAdd 且 `Binds=[]`。外层 runner 以 `rootless` 用户运行,`privileged=false`、不增加 `CAP_SYS_ADMIN`,只映射 `/dev/net/tun`,内部 Docker 只监听私有 Unix socket;runner 配置必须保持 `docker_host: "-"`、`valid_volumes: []` 和 `bind_workdir: false`,防止内部 Docker socket 或宿主 bind mount 进入 job。job 只连接 `gitea-actions` internal network:`genarrative-station` 由只转发 `/git` 到 Gitea 的内部 gateway 解析,公网依赖只经拒绝私网、保留地址和 metadata 的 80/443 egress proxy;绕过 proxy 的公网和 Postgres/Redis 数据网都必须不可达。完整 bwrap canary 需要 rootless DinD 外层的 rootlesskit AppArmor/userns 边界,以及内层 job 的 namespace/proc 挂载支持;相关 `seccomp/systempaths` 放宽只允许存在于这个无宿主 socket 的 rootless DinD 内层,禁止复制回控制宿主 rootful Docker 的 runner。
workflow 的系统依赖步骤在 UID 0 时不再调用 sudo;非 root runner 必须使用 `sudo -E` 保留受控 proxy 环境,避免 `sudo` 的 `env_reset` 让 apt 绕过 gateway。Cargo 通过 proxy 下载 sparse index/crate 时固定关闭 HTTP multiplexing 并设置 `CARGO_NET_RETRY=10`;rustup bootstrap 与 toolchain 安装也执行有界重试,降低代理链瞬时 TLS EOF 对后端测试的影响,但不能用重试掩盖持续不可达。AI 原生壳 job 不能把 `$HOME/.cargo/bin` 或 setup-node toolcache 直接加入 `command.exec` 的受信任 PATH;应安装 `ripgrep`,把完整 Node 发行目录复制到 `/usr/local/lib`,把 root-owned rustup proxy 安装到 `/usr/local/bin`,并通过 `RUSTUP_HOME` 只读挂载工具链。测试前先运行与应用一致的完整 bwrap canary,失败时停止测试,不允许跳过 sandbox 用例。Tauri 的 1132 项级别测试 suite 共享 Agent Runtime 后台锁与异步终态,必须固定 `--test-threads=1`;并行 suite 偶发失败后逐个重跑通过,不能反过来证明并行全量门禁稳定。
站点 stack 仍由宿主 `/opt/gitea-stack` 管理,`.env`、runner 注册文件和数据库凭据不进入仓库。Compose 必须在 helper/container 内把该目录挂到同一个绝对路径再执行;若挂成 `/stack`,相对 bind source 会被 Docker daemon 误解析为宿主 `/stack/...` 并启动空数据目录。升级或 runner 迁移前先停止 Gitea 写入并保留 `data/gitea` 冷快照、`pg_dumpall`、compose/.env 与 runner config/.runner;本次可回滚快照位于 `backups/gitea-ci-migration-20260721-211018`。备份文件、绝对宿主配置和注册 token 不得提交 Git。
workflow 首次成功运行后,在 Gitea `master` 分支保护中把 `Project CI / Repository checks (pull_request)`、`Project CI / Frontend tests (pull_request)`、`Project CI / Backend tests (pull_request)`、`Project CI / Native shell tests (pull_request)` 四个完整 context 都设为合并必需检查,并从最近一周已上报 context 表复核名称后再保存。不能只填裸 job 名,否则无法匹配 Gitea 实际上报的 `<workflow> / <job> (<event>)`。只提交 workflow 文件不会自动创建 runner,也不会自动修改分支保护;如果 Actions 长时间停留在等待状态,先到仓库或组织的 Actions runner 页面确认存在在线、带 `ubuntu-latest` 标签的 runner。
视觉小说负向扫描与验收门禁:
```bash