# STORAGE LABOS / FlashOps — AI 协作与系统操作指南

> 文档版本：2.0
>
> 核对日期：2026-07-27
>
> 适用对象：Codex、Claude Code、Cursor、GitHub Copilot Agent，以及参与项目的人类开发者
>
> 目标：只把本文件交给 AI，也能让它安全地理解、运行、修改、验证和交付本系统

本文件是可以单独分发给 AI 的完整系统说明书，不依赖仓库中的任何其他文档。它已经内嵌产品边界、
架构、数据模型、Run/Step 状态机、恢复阶梯、API、Agent 协议、开发流程、生产运维、Git 和安全规则。
即使 AI 只能看到这一份文件，也应能够正确查询线上系统、运行本地模拟、判断允许执行的动作，并在
获得仓库或服务器权限后安全开发和运维。

如果 AI 同时能看到源码，以实际代码和测试结果为准；如果生产状态与本文不一致，先只读核验并报告
差异，不要凭本文直接覆盖服务器。

---

## 0. 直接交给 AI 的任务开场词

把下面内容和具体任务一起发给 AI：

~~~text
你正在维护 STORAGE LABOS / FlashOps。

你收到的这份文档已经包含操作本系统所需的产品、架构、状态机、API、Agent、生产运维、
安全和 Git 规则。请完整阅读并只以本文作为任务上下文，不要要求用户再提供其他项目文档。

如果任务同时提供仓库，先检查状态并保护现有未提交改动；如果只有本文，先使用公开 GET 接口进行
只读核验。默认只操作本地开发环境；除非任务明确要求，不得修改生产服务器、生产数据、DNS、证书、
Gitea 设置或调用生产环境写接口。任何真实硬件、固件刷写、Format、Sanitize、Reset、断电或 OOB
动作都必须停下并取得明确授权。

任务：<在这里写具体目标>

完成后请报告：改动文件、关键决策、测试结果、未完成项、风险、提交号和远程分支。
~~~

---

## 1. AI 必须先记住的系统事实

| 项目 | 当前值 |
|---|---|
| 产品 | STORAGE LABOS，后端代号 FlashOps |
| 用途 | 存储固件回归测试的无人值守控制平面 |
| Git 网页 | <https://git.imagebrewing.com/yuanshuai/storage-labos> |
| 克隆地址 | <https://git.imagebrewing.com/yuanshuai/storage-labos.git> |
| 默认分支 | main |
| 公网预览 | <https://flashops.imagebrewing.com/> |
| 公网 API 文档 | <https://flashops.imagebrewing.com/docs> |
| 本地控制台 | <http://localhost:8000/console/Dashboard.dc.html> |
| 本地 API 文档 | <http://localhost:8000/docs> |
| 后端 | Python 3.9+、FastAPI、SQLAlchemy |
| 当前数据库 | SQLite，必须保持单 worker |
| 仓库代码能力 | 模拟 Run 与 Agent dry-run；不代表真实硬件动作已交付 |
| 当前线上能力 | API 版本 0.1.0，仅模拟 Run；Agent 路由尚未发布 |
| 当前测试基线 | 25 项 |
| 生产认证边界 | 预览站匿名开放，尚无应用级 RBAC |
| Gitea | 公开仓库匿名可读；注册账号需管理员批准 |
| CI | 生产服务器没有 Runner，测试结果必须由提交者本地提供 |

重要：公网预览虽然没有登录限制，但这不等于 AI 获得了生产写权限。当前部分 POST 接口在技术上
也能匿名调用，这是尚未补齐 RBAC 的原型边界，不应被当作授权。

---

## 2. 权限与行为边界

AI 应按以下顺序判断是否可以行动：

| 行为 | 默认是否允许 | 条件 |
|---|---:|---|
| 阅读仓库、文档、Git 历史 | 是 | 保持只读 |
| 查看公网页面、调用健康检查和 GET 查询 | 是 | 不产生业务状态变化 |
| 在本地新分支修改代码和文档 | 用户要求开发时允许 | 保护已有改动 |
| 本地运行测试和模拟演示 | 是 | 不连接真实设备 |
| 创建本地模拟 Run | 是 | 仅使用本地数据库 |
| 提交 Git | 用户要求开发或交付时允许 | 检查范围、测试和秘密后提交 |
| 推送功能分支 | 用户要求共享或交付时允许 | 不强推 main |
| 合并 Pull Request | 否 | 需要仓库负责人明确授权或正常评审 |
| 调用生产 POST、PATCH、PUT、DELETE | 否 | 必须得到针对该动作的明确授权 |
| 修改 DNS、Nginx、systemd、证书、Gitea 设置 | 否 | 必须明确要求部署或运维 |
| 操作真实 DUT、NVMe、固件或 OOB | 否 | 必须明确目标、批准范围和回滚方案 |
| Format、Sanitize、刷写、Reset、断电 | 绝不默认执行 | 需要明确授权、可牺牲 DUT、审批和证据链 |

遇到权限不清时，继续完成安全的本地分析、测试或方案设计，把真正需要授权的动作单独列出。

---

## 3. 决策依据与证据优先级

AI 不应只根据文件名或旧说明猜测当前能力，按下面顺序取证：

1. 当前用户的明确任务、授权与限制；
2. 本文件写明的安全边界和不可执行动作；
3. 线上健康检查、API 返回、服务状态和日志等实际证据；
4. 如果 AI 获得仓库：当前工作树、Git diff、源码和自动化测试；
5. 如果 AI 获得服务器：生效配置、运行进程、数据路径和备份；
6. 历史提交或历史部署记录；
7. 推测。

如果本文、代码、测试和生产状态互相矛盾，必须在交付说明中指出。不得把“规划”“草案”“静态
页面”描述成已经可用于真实硬件的能力，也不得因为拥有技术访问能力就推断拥有写入授权。

---

## 4. 第一次接手：安全启动流程

### 4.1 全新克隆

~~~bash
git clone https://git.imagebrewing.com/yuanshuai/storage-labos.git
cd storage-labos
git status -sb

cd flashops
make setup
make test
make demo
~~~

make setup 会初始化本地虚拟环境，并重建 flashops/var 下的本地 SQLite 数据。它只适合全新克隆
或确认可以重置的本地开发环境；如果工作目录里已有需要保留的本地演示数据，不要直接运行。

### 4.2 已存在的工作目录

~~~bash
git status -sb
git diff
git diff --cached
git log -5 --oneline
~~~

先识别用户已有的未提交改动。不得使用 reset --hard、checkout 丢弃文件、clean -fd 或其他会
抹掉用户工作的命令。不得把不相关改动混进自己的提交。

### 4.3 只有本文时

只有本文、没有仓库时，AI 仍可以：

- 使用第 6 节列出的公开 GET 接口查询健康、指标、Run、事件和 Host；
- 根据第 5 节解释状态、恢复、检查点和系统边界；
- 根据第 11 节指导获授权的人完成服务器检查、发布、备份和故障排查；
- 根据第 2 节拒绝未授权生产写入与危险硬件动作；
- 输出诊断结论、变更方案、验证清单和交接记录。

只有本文时，AI 不得声称已经修改代码、提交 Git、连接服务器或执行生产动作。只有在真正获得相应
工具和授权后，才能执行这些操作。

---

## 5. 系统结构与当前边界

~~~text
STORAGE LABOS/
├── CONTRIBUTING.md                 本文：完整 AI 系统说明书
├── .gitea/                         PR 模板与未来 CI 工作流
├── flashops/
│   ├── services/control-plane/     FastAPI 控制平面
│   ├── services/host-agent/        拉模式 Host Agent
│   ├── deploy/                     FlashOps 与 Gitea 部署模板
│   ├── var/                        本地数据库和证据，禁止提交
│   └── Makefile                    开发和验证入口
└── 前端UI八页面完成/               FastAPI 同源挂载的静态控制台
~~~

当前已经具备：

- FastAPI 控制平面、14 张持久化表和 SQLite 数据库；
- Run 事件溯源、状态投影、状态机和安全门禁；
- 模拟回归、故障注入、L1–L3 恢复演示和 Evidence Bundle；
- 仓库代码已具备 Agent 注册、token、心跳、步骤租约、ACK、续租、事件、完成、超时回收和幂等链路，
  但该增量尚未发布到当前公网环境；
- 公开预览控制台及部分真实 API 接入页面。

当前不应声称已经具备：

- 真实 nvme-cli 固件刷写、Format 或 Sanitize；
- 真实 JetKVM、PDU 或 AC 断电控制；
- 完整 SSO、RBAC、审批与审计授权；
- 多工位生产调度、PostgreSQL 高可用；
- 已验收的失败聚类自动决策。

### 5.1 五层架构与依赖方向

~~~text
浏览器控制台 ──HTTP──▶ FastAPI 控制平面 ◀──HTTP 长轮询── Host Agent
                              │                         │
                              ▼                         ├──▶ 工具适配器
                         SQLite / 证据                  └──▶ 独立 OOB 控制器
~~~

五个逻辑层：

1. 产品交互层：控制台页面和 HTTP API；
2. 控制平面：工作流物化、状态机、安全门禁、资源锁、事件和恢复决策；
3. 数据面：客户测试主机上的拉模式 Agent，只执行控制平面批准的步骤；
4. 带外平面：独立于测试主机的 OOB 观测与电源控制，主机死亡后仍应可用；
5. 证据与分析层：事件时间线、Evidence Bundle、失败签名和审计。

依赖必须单向：

- Agent 不依赖控制平面的 Python 模块，只通过 HTTP 契约通信；
- 控制平面不主动反连 Agent，Agent 必须主动注册、心跳和领取步骤；
- 状态机与恢复决策保持纯函数，不做数据库、网络或时钟 I/O；
- API 路由只校验请求并调用 service，不承载核心业务判断；
- 所有危险动作在下发 Agent 前先经过安全门禁；
- 带外通道不能依赖被测主机的 OS、网络栈或 Agent 进程。

### 5.2 核心数据模型

当前 SQLite 中的核心对象如下，AI 讨论数据时应使用这些名称：

| 对象 | 作用 |
|---|---|
| TestHost | 测试主机、Agent 身份、心跳和工具链能力 |
| OobController | 独立带外控制器、电源和观测能力 |
| Dut | 被测盘及序列号、系统盘保护、状态 |
| FirmwareArtifact | 固件版本、型号适配和校验信息 |
| Workflow | 版本化 SOP 与步骤定义 |
| Run | 一次工作流执行及当前状态投影 |
| RunStep | Run 物化后的顺序步骤、attempt、租约和结果 |
| RunEvent | append-only 统一事件时间线 |
| RecoveryAction | 恢复级别、触发原因、结果和证据 |
| ResourceLock | DUT、Host 等独占资源锁 |
| FailureSignature | 归一化失败签名 |
| RunFailure | Run 与失败签名的关联 |
| EvidenceBundle | 证据包 URI、完整率、大小和哈希 |
| AuditLog | 人工或系统管理动作的审计记录 |

RunEvent 是状态变化的权威事实；Run 和 RunStep 上的 state 是事件施加后的当前投影。修改投影时必须
与事件处于同一数据库事务，不能先改状态后补事件。

### 5.3 Run 状态机

~~~text
QUEUED ──资源锁成功──▶ PREFLIGHT ──六项门禁通过──▶ RUNNING
   │                      │                                │
   └──紧急停止──▶ ABORTED ├──门禁拒绝──▶ REJECTED         ├──完成──▶ COMPLETED
                          └──紧急停止──▶ ABORTED            ├──人工暂停──▶ PAUSED
                                                           │                │
                                                           │                └──继续──▶ RUNNING
                                                           ├──故障──▶ RECOVERING
                                                           │             ├──恢复成功──▶ RUNNING
                                                           │             └──耗尽/不安全──▶ FROZEN
                                                           ├──冻结──▶ FROZEN
                                                           └──紧急停止──▶ ABORTED
~~~

状态含义：

| 状态 | 含义 |
|---|---|
| QUEUED | 等待 DUT、Host 等独占资源 |
| PREFLIGHT | 执行安全门禁并冻结环境指纹 |
| RUNNING | 正在执行步骤或循环 |
| RECOVERING | 正在执行恢复阶梯 |
| PAUSED | 在安全检查点人工暂停 |
| COMPLETED | 正常结束，verdict 为 PASS 或 FAIL |
| FROZEN | 冻结现场，禁止覆盖性动作 |
| ABORTED | 紧急停止 |
| REJECTED | 安全门禁拒绝启动 |

终态为 COMPLETED、FROZEN、ABORTED、REJECTED，终态不可继续转移。任何非法转移都应被视为代码
缺陷，不能吞掉异常继续运行。任意非终态都可以因紧急停止进入 ABORTED；终态不能再次停止、暂停或继续。

### 5.4 Step 状态机与租约

~~~text
PENDING ──租给 Agent──▶ DISPATCHED ──ACK──▶ RUNNING
   ▲                                           ├──▶ SUCCEEDED
   │                                           ├──▶ FAILED
   └──仅幂等且可重试时 attempt+1 ◀─────────────├──▶ TIMED_OUT
                                               ├──▶ CANCELLED
                                               └──▶ SKIPPED
~~~

- PENDING：等待调度；
- DISPATCHED：已分配 Agent 并创建 60 秒租约，等待 ACK；
- RUNNING：Agent 已确认接手，执行期间必须续租；
- SUCCEEDED、FAILED、TIMED_OUT、CANCELLED、SKIPPED：步骤终态；
- 每次派发增加 attempt，旧 attempt 的 ACK、续租、事件和完成上报必须返回 409；
- DISPATCHED 未 ACK 而租约到期时退回 PENDING；
- RUNNING 租约到期时，只有幂等且仍有重试额度的步骤能退回 PENDING；
- 非幂等步骤或重试耗尽时进入 TIMED_OUT，Run verdict 变为 INCONCLUSIVE 并冻结；
- retry 默认是 0；刷写、Format、Sanitize 等危险步骤默认非幂等且不得自动重试。

### 5.5 五级恢复阶梯

恢复动作根据可用通道逐级升级，不允许跳过安全判断：

| 级别 | 动作 | 使用前提 | 典型触发 |
|---|---|---|---|
| L1_AGENT_SOFT | Agent 优雅停止或软重启进程 | Agent 仍在线 | 脚本卡死、进程僵死 |
| L2_OS_REBOOT | 通过 OS 通道重启主机 | 主机网络仍在线 | Agent 崩溃、驱动异常 |
| L3_OOB_RESET | OOB 触发主板 Reset | OOB 在线 | 心跳丢失、蓝屏 |
| L4_OOB_ATX_POWER | OOB 模拟 ATX 长按关机再开机 | OOB 在线 | Reset 无效 |
| L5_OOB_AC_CYCLE | AC 断电、安全等待、重新上电 | OOB 在线 | ATX 无效、DUT 掉盘最后手段 |
| FREEZE | 冻结现场并停止自动恢复 | 无 | 数据完整性失败或阶梯耗尽 |

不可更改的恢复规则：

1. 数据完整性不一致直接 FREEZE，不能先重启或重跑；
2. OOB 离线时 L3–L5 不可用，应标记 INFRA_FAILURE 并冻结，不能误判 DUT_DEFECT；
3. 每次恢复必须记录 trigger、level、outcome、时间和 evidence_uri；
4. 恢复成功只表示系统重新可执行，不表示原步骤成功；
5. 每循环最多恢复 3 次，每 Run 最多恢复 20 次；
6. L5 断电后默认安全等待 10 秒再上电；
7. 真实 L3–L5 当前没有交付，现有演示只证明状态和证据链路。

失败分类必须区分：

- DUT_DEFECT：证据支持被测盘缺陷；
- INFRA_FAILURE：供电、网络、Host、OOB 等基础设施问题；
- SCRIPT_FAILURE：测试脚本或工具问题；
- DATA_INTEGRITY：数据校验失败，立即冻结；
- UNKNOWN：证据不足，不能强行归类。

### 5.6 检查点与断点续跑

检查点只在安全的步骤边界创建，不在无法证明幂等的步骤中间创建。

| 中断位置 | 恢复后的行为 |
|---|---|
| 步骤已 SUCCEEDED 且检查点已保存 | 从下一步继续 |
| RUNNING 步骤幂等 | 可重跑当前步骤 |
| RUNNING 步骤非幂等 | 不自动重跑，INCONCLUSIVE 并等待人工确认 |
| 循环体中的步骤 | 从当前 loop_index 的循环起点重跑整轮 |

检查点至少要能确定 loop_index、下一步骤、当前固件/环境指纹和最后事件序号。保存检查点必须先于
推进下一步骤；恢复时必须验证数据库投影、Agent attempt 和 DUT 当前状态一致。

### 5.7 心跳与双通道观测

~~~text
Agent 正常心跳：每 5 秒
15 秒未收到：Host → DEGRADED，只记事件，不立刻做危险动作
30 秒未收到：Host → OFFLINE，并结合 OOB 判断
  ├── OOB 在线且主机有电：可进入 L3
  ├── OOB 在线但主机无电：INFRA_FAILURE
  └── OOB 也离线：INFRA_FAILURE + FREEZE
心跳恢复：核对 Run、Step、attempt、租约和 DUT 枚举后再决定续跑
~~~

单一心跳、网络 ping 或 UI 状态都不能独立证明 DUT 故障。涉及恢复和缺陷归因时必须组合 Agent、
Host 网络、OOB 电源/画面和 DUT 枚举至少两个通道的证据。

### 5.8 六项安全门禁

创建 Run 前至少检查：

1. discovered_serial 与登记 DUT 序列号完全一致；
2. 目标不是系统盘，且没有系统分区或根文件系统；
3. 破坏性动作已有明确授权且授权尚未过期；
4. 固件型号、目标设备和校验值匹配；
5. Host 在线、未被其他 Run 占用且环境指纹满足要求；
6. 需要 OOB 的工作流必须确认 OOB 在线且能力匹配。

任一门禁失败都进入 REJECTED，不得通过 UI、API 参数或直接数据库写入绕过。真实危险命令还必须
增加签名命令模板、参数 schema、allowlist、审批人、审批时间、可牺牲 DUT 和短期授权。

### 5.9 Evidence Bundle

最小证据包包含 manifest.json 与按 seq 排序的 events.ndjson，并记录：

- Run、Workflow、DUT、Firmware、Host 和环境指纹；
- 完整事件时间线和人工介入；
- 步骤结果、退出码、日志 URI；
- 恢复动作与恢复前后证据；
- 检查点、verdict、完整率、大小和哈希。

大日志和二进制产物存对象目录，数据库只保存索引、摘要和 URI。事件按控制平面分配的 seq 排序，
不能依赖不同主机的本地时间戳。任何人工暂停、继续、急停、审批或覆盖默认策略的动作都必须留下事件。

---

## 6. AI 如何安全使用正在运行的系统

### 6.0 控制台页面

| 页面 | URL | 当前能力 |
|---|---|---|
| 总览 | /console/Dashboard.dc.html | 产品看板，部分指标仍为静态展示 |
| 任务中心 | /console/Tasks.dc.html | 已接安全预检和创建 Run |
| 实时运行 | /console/LiveRun.dc.html | 已接 Run、事件、演示、暂停、继续和急停 |
| 证据浏览器 | /console/Evidence.dc.html | 主要为静态样例，真实下载待接 |
| 失败中心 | /console/Failures.dc.html | 静态原型，失败聚类执行待实现 |
| 资产中心 | /console/Assets.dc.html | 静态原型，资产 CRUD 待实现 |
| 版本比较 | /console/Compare.dc.html | 静态原型，真实 A/B 查询待实现 |
| 工作流模板 | /console/Workflows.dc.html | 静态原型，编辑与发布 API 待实现 |

控制台和 API 同源，动态请求使用 /api/v1 相对路径。页面展示不是后端能力证据；AI 必须通过 API
返回或测试确认功能是否真实接入。事件轮询使用 after_seq 增量读取。

### 6.1 公网只读检查

以下请求只读取公开信息，可用于健康诊断：

~~~bash
curl -fsS https://flashops.imagebrewing.com/api/v1/health
curl -fsS https://flashops.imagebrewing.com/api/v1/dashboard/summary
curl -fsS 'https://flashops.imagebrewing.com/api/v1/runs?limit=10'
~~~

不要在诊断任务中为了“试一下”调用生产 POST 接口。健康检查成功不代表业务能力或真实硬件已经验收。
当前公网 OpenAPI 版本为 0.1.0，Agent 路径尚未发布，访问 /api/v1/agent/hosts 返回 404 是已知部署
边界，不应误判成 Gitea、Nginx 或 Host 故障。

Run 查询的关键字段：

| 字段 | 含义 |
|---|---|
| id、name | Run 标识和名称 |
| state、verdict | 当前状态与 PASS / FAIL / INCONCLUSIVE |
| execution_mode | simulated 或 agent_dry_run |
| workflow_key、workflow_version | 固定的工作流版本 |
| dut_id、test_host_id | 绑定资产 |
| loop_target、loop_done、progress | 循环目标与完成进度 |
| checkpoint | 最近安全恢复点 |
| environment | 创建时冻结的环境指纹 |
| recovery_count | 已执行恢复次数 |
| unattended_completion | 是否无人干预完成 |
| human_touches | 人工介入次数 |
| freeze_reason | 冻结原因 |
| evidence | 详情接口返回的证据包索引 |

事件字段为 id、seq、ts、source、kind、severity、step_id、loop_index、message、payload。必须按 seq
消费；after_seq 传客户端最后确认的 seq。source 可能是 control_plane、agent、oob 或 human。

### 6.2 本地启动

~~~bash
cd flashops
make dev
~~~

另开终端检查：

~~~bash
curl -fsS http://127.0.0.1:8000/api/v1/health
curl -fsS http://127.0.0.1:8000/api/v1/dashboard/summary
~~~

### 6.3 本地创建安全的模拟 Run

只在 localhost 使用下面示例：

~~~bash
curl -fsS -X POST http://127.0.0.1:8000/api/v1/runs \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "AI local verification",
    "loops": 2,
    "inject_failure": true,
    "execution_mode": "simulated",
    "created_by": "ai-local"
  }'
~~~

返回 Run ID 后：

~~~bash
curl -fsS http://127.0.0.1:8000/api/v1/runs/RUN_ID
curl -fsS 'http://127.0.0.1:8000/api/v1/runs/RUN_ID/events?after_seq=0'
~~~

也可以直接执行 make demo；它会创建独立的本地模拟链路并输出 Evidence Bundle 路径。

### 6.4 API 快速索引

下表中的非 Agent 路径已在当前公网部署；Agent 路径属于仓库代码基线，当前只适合本地开发和测试。

| 方法与路径 | 含义 | 默认安全级别 |
|---|---|---|
| GET /api/v1/health | 服务健康 | 只读 |
| GET /api/v1/dashboard/summary | 汇总指标 | 只读 |
| GET /api/v1/runs | Run 列表 | 只读 |
| GET /api/v1/runs/{run_id} | Run 详情与证据 | 只读 |
| GET /api/v1/runs/{run_id}/events | 增量事件 | 只读 |
| GET /api/v1/agent/hosts | Agent Host 状态 | 只读 |
| POST /api/v1/preflight | 安全预检 | 计算型；生产调用仍需明确目的 |
| POST /api/v1/runs | 创建 Run | 写操作；生产默认禁止 |
| POST /api/v1/runs/demo | 创建演示 Run | 写操作；生产默认禁止 |
| POST /api/v1/runs/{id}/pause | 暂停 | 写操作 |
| POST /api/v1/runs/{id}/resume | 继续 | 写操作 |
| POST /api/v1/runs/{id}/emergency-stop | 紧急停止 | 高影响写操作 |
| POST /api/v1/agent/* | Agent 注册与执行协议 | 凭据操作；不得随意调用 |

核心请求体已经内嵌如下；运行时 OpenAPI 只用于核验部署版本是否发生变化，不是阅读本文的前置条件。

Preflight 请求：

~~~json
{
  "dut_id": "可选 DUT ID",
  "host_id": "可选 Host ID",
  "firmware_id": "可选固件 ID",
  "discovered_serial": "探针实际发现的序列号"
}
~~~

Create Run 请求：

~~~json
{
  "name": "固件 A/B 无人值守回归",
  "workflow_id": null,
  "dut_id": null,
  "host_id": null,
  "firmware_a_id": null,
  "firmware_b_id": null,
  "loops": 8,
  "inject_failure": true,
  "execution_mode": "simulated",
  "created_by": "console-demo"
}
~~~

- loops 范围 1–500；
- execution_mode 只能是 simulated 或 agent_dry_run；
- simulated 会由控制平面启动内置模拟执行器；
- agent_dry_run 会物化步骤并等待独立 Agent 领取，但不允许真实主机命令。

暂停、继续和紧急停止使用：

~~~json
{
  "actor": "操作者或 AI 任务标识",
  "reason": "可审计的具体原因"
}
~~~

常见状态码：

| 状态码 | 含义 |
|---:|---|
| 200 | 查询或动作成功 |
| 201 | Run、注册等资源创建成功 |
| 204 | Agent 当前没有可领取步骤 |
| 400 | 业务参数或资产关系无效 |
| 401 | Agent 注册口令或 bearer token 无效 |
| 404 | Run、Host、DUT 等对象不存在 |
| 409 | 非法状态、资源冲突、旧 attempt 或幂等键冲突 |
| 422 | 请求字段或 Idempotency-Key 校验失败 |
| 503 | 生产环境缺少 Agent 注册口令 |

### 6.5 Agent 使用规则

- 本节描述仓库已经实现但尚未发布到公网的 Agent 协议；不要向当前生产域名调用这些端点；
- Agent 是拉模式，控制平面永远不主动连接测试主机；
- 生产注册需要 X-FlashOps-Enrollment-Token；
- 注册成功返回的原始 bearer token 只出现一次，只能保存到权限 0600 的本机状态文件；
- 后续 Agent 请求使用 Authorization: Bearer；
- ACK、续租、事件和完成接口要求 UUID 格式的 Idempotency-Key；
- 不得把 enrollment token、bearer token 或 agent-state.json 输出到聊天、日志或 Git；
- 未经授权不得代表真实 Host 注册、轮换 token 或领取生产步骤；
- 日常开发优先使用 make dev-agent、make agent-demo 和测试夹具。

Agent 端点和顺序：

| 顺序 | 方法与路径 | 行为 |
|---:|---|---|
| 1 | POST /api/v1/agent/register | 注册或轮换 token，上报 Host 指纹 |
| 2 | POST /api/v1/agent/{agent_id}/heartbeat | 上报健康、工具链和设备探针 |
| 3 | POST /api/v1/agent/{agent_id}/lease | 领取步骤，最长等待 20 秒 |
| 4 | POST /api/v1/agent/{agent_id}/steps/{step_id}/ack | 确认当前 attempt |
| 5 | POST /api/v1/agent/{agent_id}/steps/{step_id}/renew | 续租，不超过步骤 timeout |
| 6 | POST /api/v1/agent/{agent_id}/steps/{step_id}/events | 批量上报 1–100 条事件 |
| 7 | POST /api/v1/agent/{agent_id}/steps/{step_id}/complete | 上报 SUCCEEDED 或 FAILED |

注册体至少需要 agent_version，可选 host_id 或 host_name，并可携带 os_family、os_version、kernel、
cpu、motherboard、bios_version、ip_address、toolchain 和 labels。心跳体可携带 healthy、
agent_version、ip_address、toolchain 和 device_probe。

租约请求为：

~~~json
{"wait_timeout_s": 0}
~~~

wait_timeout_s 范围 0–20。ACK 与 renew 请求为 {"attempt": 1}。事件请求示例：

~~~json
{
  "attempt": 1,
  "events": [
    {
      "message": "step started",
      "severity": "INFO",
      "payload": {}
    }
  ]
}
~~~

完成请求示例：

~~~json
{
  "attempt": 1,
  "status": "SUCCEEDED",
  "exit_code": 0,
  "error_class": null,
  "result": {},
  "log_uri": null
}
~~~

ACK、renew、events、complete 都必须带新的 UUID 格式 Idempotency-Key。相同操作重放返回当前投影
并标记 replayed=true；同一个 key 用于其他步骤或其他动作时返回 409。重新注册会轮换 token，使旧
token 立即失效。控制平面只保存 token 的 SHA-256 摘要。

---

## 7. 三条不可破坏的架构红线

### 7.1 控制面是状态唯一真相

Agent 和测试主机只持有执行缓存。Run、Step、锁、检查点和证据索引的权威状态在控制平面。
不得把 Agent 本地状态反向当成数据库真相。

### 7.2 状态变化必须追加事件

所有 Run 状态变化必须经过 events/recorder.py 的 append_event。不得绕过事件记录直接 UPDATE
Run 状态。新增状态或事件时必须同步修改枚举、转移表、投影逻辑、测试和文档。

### 7.3 安全默认拒绝

序列号、破坏性授权、系统盘、固件型号、Host 和 OOB 任一条件不满足都必须拒绝下发。危险步骤
默认不可自动重试。数据完整性失败要冻结现场，不能为了完成率自动重跑并覆盖证据。

---

## 8. AI 修改代码的标准流程

### 8.1 开始前

1. 确认用户目标属于解释、诊断、开发还是部署；
2. 检查 Git 状态、当前分支和已有改动；
3. 阅读与任务直接相关的源码、测试和文档；
4. 找到当前行为的证据，不能只根据 UI 文案猜测；
5. 若涉及高影响选择，先说明方案和边界。

### 8.2 实现时

- 修改现有项目，不随意建立第二套平行架构；
- 优先小而完整的改动，不混入无关格式化；
- 修复缺陷时增加能复现问题的测试；
- API、状态、事件、环境变量或部署变化必须同步文档；
- 保持 Python 3.9 兼容，不在未修改运行基线前使用更高版本独占语法；
- SQLite 环境保持单 worker；
- 不绕过 service、event recorder、state machine 或 safety gate；
- 保留用户未提交的改动，不擅自覆盖或清理。

### 8.3 完成前

~~~bash
cd flashops
make test
make demo
~~~

回到仓库根目录：

~~~bash
git status -sb
git diff --check
git diff --stat
~~~

只暂存本次任务相关文件：

~~~bash
git add <明确的文件或目录>
git diff --cached --stat
git diff --cached --check
~~~

AI 还必须确认暂存区没有：

- .env、密码、Token、私钥或证书私钥；
- SQLite、日志、Evidence Bundle、Agent state；
- 虚拟环境、缓存、构建输出；
- 未脱敏的客户数据或真实设备标识；
- 无关的用户改动。

---

## 9. 测试与验收门禁

| 变更类型 | 最低验证 |
|---|---|
| 纯文档 | 链接和命令核对、git diff --check |
| Python 逻辑 | make test |
| 状态机、恢复、安全 | 对应单测 + make test + make demo |
| API | 正常、错误、幂等或冲突路径测试 |
| Agent | 控制面与 host-agent 测试，凭据权限检查 |
| 前端 | 真实浏览器检查受影响页面，确认控制台与 API 一致 |
| Nginx | nginx -t 后才能 reload |
| systemd | systemd-analyze verify 或目标机安全验证 |
| 生产部署 | 本地测试、备份、健康检查、HTTPS、日志和回滚检查 |

测试失败时不得把失败包装成完成。先判断是本次回归、环境差异还是已有问题，并保留原始证据。

---

## 10. Git 与 Gitea 交付规范

### 10.1 分支

- main：稳定、可测试、可发布，禁止强推；
- feat/<short-name>：功能；
- fix/<short-name>：缺陷；
- docs/<short-name>：文档；
- chore/<short-name>：工具、依赖和部署。

~~~bash
git switch main
git pull --ff-only
git switch -c docs/ai-operating-guide
~~~

### 10.2 提交

使用 Conventional Commits：

| 类型 | 示例 |
|---|---|
| feat | feat(agent): add signed command registry |
| fix | fix(events): preserve sequence under concurrent writes |
| docs | docs(ai): add system operating guide |
| test | test(safety): reject system disk target |
| refactor | refactor(runs): isolate checkpoint projection |
| perf | perf(api): reduce event timeline query cost |
| chore | chore(deploy): pin gitea image |

一次提交只表达一个完整意图。提交前检查暂存区，不能用一条模糊提交吞掉多项无关工作。

### 10.3 推送与 Pull Request

~~~bash
git commit -m 'docs(ai): add system operating guide'
git push -u origin docs/ai-operating-guide
~~~

在 Gitea 创建 Pull Request，说明：

- 改了什么、为什么；
- 影响的系统边界；
- 实际运行的测试及结果；
- 安全影响；
- 风险、回滚方法和未完成项。

至少一名团队成员评审后合并。优先 Squash merge，PR 标题使用 Conventional Commit。
生产服务器没有 Gitea Actions Runner；仓库中的工作流是未来隔离 Runner 的模板，不能把“工作流存在”
误报成“CI 已运行”。

### 10.4 凭据

公开仓库允许匿名读取和克隆。写操作需要经过批准的 Gitea 账号。管理员密码和访问令牌只允许存在于
系统钥匙串、密码管理器或被 .gitignore 覆盖的本机文件中；不得写进 remote URL、文档、脚本或提交。

---

## 11. 生产部署上下文

| 资源 | 当前值 |
|---|---|
| 云服务器 | 腾讯云 CVM，62.234.39.66 |
| 操作系统 | Ubuntu 24.04 |
| DNS | DNSPod A 记录，TTL 600 |
| FlashOps 域名 | flashops.imagebrewing.com |
| FlashOps 服务 | flashops.service |
| 当前公网 API | 0.1.0，仅模拟 Run，未发布 Agent 路由 |
| 应用用户 | ubuntu |
| 应用目录 | /data/wangzhan/app/storage-labos |
| 内部端口 | 127.0.0.1:18080 |
| 应用数据 | /data/wangzhan/app/storage-labos/flashops/var |
| 私有环境文件 | /etc/flashops/flashops.env |
| FlashOps Nginx | /www/server/panel/vhost/nginx/flashops.imagebrewing.com.conf |
| FlashOps 证书 | /etc/letsencrypt/live/flashops.imagebrewing.com/ |
| Gitea 域名 | git.imagebrewing.com |
| Gitea 容器 | gitea，镜像版本固定为 1.27.0 |
| Gitea 内部端口 | 127.0.0.1:13000 |
| Gitea 实时数据 | /data/gitea/data |
| Gitea Nginx | /www/server/panel/vhost/nginx/git.imagebrewing.com.conf |
| Gitea 证书 | /etc/letsencrypt/live/git.imagebrewing.com/ |
| COS 挂载 | /chucun |
| Gitea 备份 | /chucun/wangzhan-production/backups/gitea/ |

生产操作规则：

1. 只有用户明确要求部署或运维时才连接服务器；
2. 先核对主机、域名、当前服务、端口和目标文件；
3. 部署前运行本地测试并备份当前版本和数据；
4. 不直接在服务器编辑项目源码，应由 Git 提交或可核验发布包交付；
5. Nginx 修改必须先测试配置再 reload；
6. SQLite 继续使用单 worker，扩展并发前先迁移 PostgreSQL；
7. Gitea 实时仓库和 SQLite 不得放到 COSFS；
8. COS 只作备份目标，备份需要 ZIP 与 SHA-256 sidecar；
9. 不在输出中展示 SSH、Gitea、Agent、DNS 或云平台凭据；
10. 部署后验证内部 health、外部 HTTPS、最终跳转、日志和回滚点。

FlashOps 当前是匿名预览环境。接入真实硬件或非公开数据前，必须先完成应用级 SSO / RBAC、
审批、审计和命令模板签名；不能只依赖 Nginx Basic Auth。

### 11.1 FlashOps 服务管理与健康检查

只有获得服务器运维授权后才执行：

~~~bash
sudo systemctl status flashops --no-pager -l
sudo systemctl is-enabled flashops
curl -fsS http://127.0.0.1:18080/api/v1/health
curl -fsS https://flashops.imagebrewing.com/api/v1/health
~~~

需要重启时先确认没有不可中断的 Run，再执行：

~~~bash
sudo systemctl restart flashops
sudo systemctl status flashops --no-pager -l
curl -fsS http://127.0.0.1:18080/api/v1/health
~~~

服务必须只绑定 127.0.0.1:18080，不能把 Uvicorn 直接暴露公网。当前 SQLite 只允许运行一个
worker；增加 worker 前必须先迁移 PostgreSQL，并处理跨进程事件总线与锁。

关键环境变量：

| 变量 | 作用 |
|---|---|
| FLASHOPS_ENV | production 或 development |
| FLASHOPS_DATABASE_URL | 数据库 DSN |
| FLASHOPS_OBJECT_STORE_URL | 证据对象目录或对象存储 |
| FLASHOPS_BUS_URL | 外部事件总线，当前未启用 |
| FLASHOPS_AGENT_ENROLLMENT_TOKEN | Agent 首次注册口令 |
| FLASHOPS_API_HOST | API 监听地址 |
| FLASHOPS_API_PORT | API 端口 |
| FLASHOPS_CORS_ORIGINS | 允许的前端来源 |
| FLASHOPS_ENGINE_TICK_S | 引擎 tick |
| FLASHOPS_TIME_SCALE | 模拟时间倍率 |

生产秘密只放在 root-only、权限 0600 的 /etc/flashops/flashops.env。不得把值写进 systemd unit、
Nginx 注释、命令历史、聊天或 Git。

### 11.2 日志

~~~bash
sudo journalctl -u flashops -n 200 --no-pager
sudo journalctl -u flashops --since '30 minutes ago' --no-pager
sudo tail -n 100 /www/wwwlogs/flashops.imagebrewing.com.log
sudo tail -n 100 /www/wwwlogs/flashops.imagebrewing.com.error.log
~~~

持续跟踪日志只能用于短时诊断，结束后退出，避免长期占用会话。输出日志前先检查是否包含 token、
客户数据、设备序列号或其他敏感信息。

### 11.3 Nginx、DNS 与证书

任何配置修改都必须先测试，成功后才能 reload：

~~~bash
sudo nginx -t
sudo systemctl reload nginx
sudo certbot certificates
getent ahostsv4 flashops.imagebrewing.com
getent ahostsv4 git.imagebrewing.com
~~~

证书模拟续期：

~~~bash
sudo certbot renew --dry-run --non-interactive --no-random-sleep-on-renew
~~~

两个域名应指向 62.234.39.66；HTTP 应 301 到 HTTPS；证书 SAN 必须匹配访问域名。ACME challenge
路径必须保持可达。证书续期后由 deploy hook reload Nginx。

FlashOps 当前配置不应包含 auth_basic、allow 或 deny 访问限制。若匿名访问出现 401/403，先检查
浏览器旧认证缓存、Nginx 生效配置和上游安全产品；Agent API 因 token 无效返回 401 是另一回事。

### 11.4 标准发布流程

1. 确认目标 Git 提交、工作树和本次发布范围；
2. 在本地执行 make test 与 make demo；
3. 检查暂存和发布内容不含 .venv、var、缓存、凭据和运行产物；
4. 使用 SQLite 在线备份 API 备份当前生产数据库；
5. 保存当前可运行发布包、SHA-256 和回滚说明；
6. 将新版本上传到独立 incoming 目录，不覆盖正在运行的目录；
7. 校验上传包 SHA-256，安装或复用服务器虚拟环境；
8. 保留生产 flashops/var，绝不能用本地空 var 覆盖；
9. 校验 systemd unit 和私有环境文件权限；
10. 切换版本并重启 flashops；
11. 轮询 127.0.0.1:18080/api/v1/health；
12. 执行 nginx -t，必要时 reload；
13. 从服务器内部和外部各验证一次 HTTPS、最终跳转、health 和 dashboard JSON；
14. 检查最近日志无新 error，并记录提交号、时间、备份和回滚点。

不要在生产目录直接手工改源码。发布物必须能映射到 Git 提交。发布失败时优先切回已经验证的旧发布
目录，并保留失败目录和日志用于排查，不能边修边覆盖证据。

### 11.5 SQLite 备份与恢复边界

运行中的 SQLite 应使用在线备份 API，不要直接复制可能正在写入的数据库：

~~~bash
sudo /data/wangzhan/app/storage-labos/flashops/.venv/bin/python - <<'PY'
import datetime
import sqlite3
from pathlib import Path

source = Path('/data/wangzhan/app/storage-labos/flashops/var/flashops.db')
stamp = datetime.datetime.now().strftime('%Y%m%d-%H%M%S')
target = Path('/data/wangzhan/backups') / ('flashops-db-' + stamp + '.db')
target.parent.mkdir(parents=True, exist_ok=True)
with sqlite3.connect(source) as src, sqlite3.connect(target) as dst:
    src.backup(dst)
print(target)
PY
~~~

备份后记录文件大小和 SHA-256。数据库恢复是高影响动作，本文不授权 AI 自动执行。获明确授权后也要：

1. 停止 FlashOps；
2. 核对恢复目标、备份时间和校验值；
3. 再备份当前数据库；
4. 替换后修复 ubuntu:ubuntu 所有权和文件权限；
5. 启动服务；
6. 验证 health、表数量、最新 Run 和事件 seq 连续性；
7. 保留恢复前数据库，直到验收完成。

### 11.6 Gitea 运维与 COS 备份

Gitea 公开仓库允许匿名查看和 HTTPS 克隆；账号注册后需要管理员人工批准。当前不开放独立 Git SSH
端口，也没有生产 Actions Runner。

获授权后的只读检查：

~~~bash
sudo docker ps --filter name=^gitea$
sudo docker logs --tail 100 gitea
curl -fsS http://127.0.0.1:13000/api/healthz
sudo systemctl status flashops-gitea-backup.timer --no-pager
sudo systemctl list-timers flashops-gitea-backup.timer --no-pager
~~~

手工触发一次可恢复的 Gitea dump：

~~~bash
sudo systemctl start flashops-gitea-backup.service
sudo systemctl status flashops-gitea-backup.service --no-pager
find /chucun/wangzhan-production/backups/gitea -maxdepth 1 -type f \
  -name 'gitea-*.zip' -ls
~~~

备份脚本使用 Gitea 自身 dump，生成 ZIP 和 SHA-256 sidecar，成功复制到 COS 后清理本机临时包。
定时器每日约 03:20 运行并带随机延迟。实时仓库、Git 对象和 Gitea SQLite 必须留在
/data/gitea/data 本地文件系统；COSFS 不提供数据库依赖的完整锁与原子语义，只能用于备份。

Gitea 恢复或升级是高影响操作：必须先验证备份、阅读目标版本迁移说明、安排维护窗口，并保留旧容器
镜像和 /data/gitea/data 快照。镜像必须固定版本，不能使用 latest。

### 11.7 故障定位

公网 502：

~~~bash
sudo systemctl status flashops --no-pager -l
sudo journalctl -u flashops -n 200 --no-pager
curl -v http://127.0.0.1:18080/api/v1/health
sudo nginx -t
sudo tail -n 100 /www/wwwlogs/flashops.imagebrewing.com.error.log
~~~

常见原因是服务未启动、Python 依赖错误、18080 端口不一致、var 不可写或 systemd 安全策略阻止路径。

SQLite locked：

- 确认只有一个 Uvicorn worker；
- 检查是否有脚本绕过 session 直接写数据库；
- 所有状态写入应走同一事务和事件入口；
- 需要多进程时迁移 PostgreSQL，不能继续增加 SQLite 锁重试掩盖问题。

Gitea 页面不可访问：

~~~bash
sudo docker ps --filter name=^gitea$
sudo docker logs --tail 100 gitea
curl -fsS http://127.0.0.1:13000/api/healthz
getent ahostsv4 git.imagebrewing.com
sudo nginx -t
~~~

HTTPS 证书异常时核对 DNS、证书路径、证书域名、有效期和 Nginx 是否已 reload。不要在原因未确认时
重复签发证书或删除现有证书目录。

### 11.8 生产验收清单

- [ ] flashops.service 为 active；
- [ ] Gitea 容器为 running，loopback health 为 pass；
- [ ] Nginx active 且 nginx -t 成功；
- [ ] 两个域名解析到 62.234.39.66；
- [ ] HTTP 301 到 HTTPS；
- [ ] 两张证书域名正确、未过期、自动续期可用；
- [ ] 未登录访问 FlashOps 控制台最终返回 200；
- [ ] FlashOps health 返回 status=ok；
- [ ] dashboard summary 返回合法 JSON；
- [ ] 未登录可以查看并克隆 public Git 仓库；
- [ ] 生产 var 数据可写，但应用源码目录不被随意写入；
- [ ] 最新日志没有新增 error；
- [ ] FlashOps 数据库备份有 SHA-256；
- [ ] Gitea COS dump 有 ZIP 和有效 SHA-256 sidecar；
- [ ] 发布提交号、时间和回滚点已经记录。

---

## 12. 常见任务剧本

### 12.1 “看看项目并继续完善”

1. 以本文的能力边界、状态机和安全规则建立基线；
2. 若获得仓库，检查工作树、源码和测试；只有本文时先做线上只读核验；
3. 运行可用的测试与模拟演示；
4. 区分已实现、部分实现和待实现；
5. 优先选择一个能独立验收的纵向切片；
6. 实现、补测试、同步本文；
7. 报告实际证据，不把规划写成完成。

### 12.2 “排查网站打不开”

1. 从公网检查 DNS、TLS、重定向和最终状态码；
2. 检查 Nginx 生效配置和日志；
3. 检查本机内部端口和服务状态；
4. 区分 401、403、404、502 和浏览器缓存；
5. 诊断任务只报告原因，除非用户同时要求修复。

### 12.3 “修改 API”

1. 只有获得仓库后才能实施修改；先核对路由、schema、service 和测试；
2. 保持业务逻辑不堆在 route；
3. 状态写入走事件记录；
4. 为正常与错误路径增加测试；
5. 检查 OpenAPI；
6. 更新本文中的接口、状态码、权限和部署说明。

### 12.4 “增加真实硬件能力”

先停止直接实现危险命令。必须先确认：

- 使用可牺牲 DUT，不是系统盘；
- 有明确序列号和固件型号匹配；
- 已实现签名命令模板与 allowlist；
- 有审批、短期授权、审计和 Evidence Bundle；
- Agent 与控制面身份可信；
- 有恢复、急停和人工接管方案；
- 用户明确批准真实动作。

条件不完整时，只允许实现接口、模拟器、只读探针和测试。

### 12.5 “发布一个版本”

1. 确认工作树和目标提交；
2. 运行完整测试与演示；
3. 检查秘密和发布范围；
4. 备份服务器应用数据；
5. 部署可追溯提交；
6. 重启并验证内部与公网健康；
7. 检查日志、证书和备份；
8. 记录提交号、时间、验证结果和回滚点。

---

## 13. AI 交付时的固定报告格式

AI 最终回复至少包含：

~~~text
结果：
- 本次目标是否完成

改动：
- 文件与关键行为

验证：
- 实际运行的命令
- 通过/失败数量
- 浏览器或生产验收结果

Git：
- 分支
- 提交号
- Pull Request 或远程地址

风险与边界：
- 尚未实现或未验证的内容
- 是否涉及生产、真实设备或凭据

下一步：
- 仅列真正需要用户决定或后续开发的事项
~~~

不得只说“应该可以”“理论上完成”。必须区分已执行验证、静态检查和推断。

---

## 14. 完成定义

一项 AI 开发任务只有同时满足以下条件才算完成：

- 用户要求的行为已经实现，而不是只写方案；
- 原有未提交工作得到保护；
- 相关测试通过，或失败原因被明确记录；
- 安全门禁、事件写路径和状态机未被绕过；
- 文档与实际代码、API 和部署保持一致；
- 没有提交凭据、生产数据或运行产物；
- Git 提交范围清晰并可回滚；
- 如果要求部署，公网、内部健康、日志和备份均已验证；
- 最终报告包含证据、提交号、风险和边界。

当本文件中的域名、端口、路径、API、安全策略、测试数量或部署结构发生变化时，修改代码的 AI
必须在同一个 Pull Request 中同步更新本文件。
