Lucky.ai AI Dev Factory — 规格 v3.1

状态:草案,待 Owner 确认第 15 节的待决事项
取代:LuckyAI_AI_Dev_Factory_Spec_v3.md(v3);v3 已取代 v1、v2 两个 HTML 版本
依据:v3 评审(2026-10-03);A0 兼容性审计 luckyai-autodev-a0/AUTODEV_A0_COMPATIBILITY_REPORT.md(2026-10-02)
读者:Owner;以及实现本规格的 Claude / Codex


v3.1 变更摘要

# 变更 位置
1 新增"前置条件":主机没有 sudo、开发身份 = 生产身份、已有 d1 与发布脚本。阶段 B 之前必须完成身份拆分 §0.1、§13
2 不再使用 sudo。Runner 通过 systemd 模板单元 + polkit 启停各身份的进程(按 cgroup 整体终止,并带超时、沙箱和出站限制) §1.2、附录 C
3 Runner 从不在 Agent 拥有的仓库中运行 git。下行走本地只读镜像,上行走 git bundle 交接 §1.1、§6.1
4 门禁配置(package.json#scripts、pytest.ini、tsconfig、vite 配置等)列为禁止修改,否则"检查清单从 main 读取"形同虚设 §7.4
5 联网的 setup 阶段不执行任何安装脚本(npm ci --ignore-scripts、pip --only-binary),检查示例改为与真实仓库一致(pytest + 前端 build) §7.1、§7.2
6 提示词改为通过 stdin 传入,不再放在 argv 中(单个参数上限 128 KiB,且 ps 可见) §6
7 main 与发布标签只有 Owner 能写;生产健康检查通过后才推进 main §3、§11.2
8 服务端 hook 按"推送身份 → 允许的 from/type"整表校验;approve、check 增加 subject/check_id §4.3、§4.4
9 状态页使用受限身份 owner-web,不能发布 §1、§9.2
10 Staging 运行身份 stagingapp 纳入隔离与自检 §11.1、§9.4
11 急停覆盖 WSL 中正在运行的 Codex;读不到急停标志时按急停处理 §9.3
12 Gitolite 规则改正(不用 C/RWC,main 加 $,refs/agent/ 只允许 chat) §3
13 rebase 改为新建分支版本 agent/<JOB>/v<N+1>,所有 ref 始终只快进 §3、§5
14 特权脚本内联在留言中,不再提交到任务分支(避免随发布进入 main) §10
15 补齐 v3 中缺失的 §7.6 引用、REBASE_NEEDED 状态、deploy-staging 角色、check_id 字段等 各处

0. 目标与非目标

目标:日常软件开发由 Claude(作者)与 Codex(审查者)协作完成,Owner 只需要做四件事:

  1. 提需求(必要时花 2 分钟确认方案);
  2. 看状态,收通知;
  3. 审批特权操作(root、生产数据库迁移、密钥、生产发布);
  4. 处理升级事件(第 8 节列出的情况)。

非目标:

三条核心原则:

原则 含义
结论绑定 SHA 任何审查结论、测试结果、审批都绑定 40 位 commit SHA(或脚本的 sha256)。代码一变,旧结论全部失效
两道门禁 测试门禁(确定性,看退出码)和审查门禁(Codex 的语义审查)独立判定,两者都通过才算 VERIFIED
边界靠权限,不靠提示词 对 Agent 的所有限制都由 Unix 用户、文件权限、systemd 沙箱、Git 服务端规则来强制执行。提示词只用来说明意图,不作为安全措施

0.1 前置条件(主机现状,来自 A0 审计)

现状 对本规格的影响
主机上没有 sudo(A0 F7),root 操作需要单独的 root 登录 本规格不使用 sudo。跨身份执行统一使用 systemd 模板单元 + polkit(§1.2)
只有 claude 与 root 两个用户;开发身份 = 生产身份(luckyai.service 以 claude 运行,claude 可写生产代码、luckyai.env、数据目录)(A0 B3) 用户隔离是阶段 B 的硬前置。必须先完成 A0 W3 的身份拆分(deploy/d1-root-setup.sh identity-split,需 root 登录)。完成前只允许阶段 A,且只在不含案件数据的测试仓库上进行
A0 已记录新开发身份名为 luckydev 本规格中的 claude-dev 即该身份,实施时统一命名(D11)
已有 devcontrol/d1(SQLite dev_control),以及已安装的发布入口 luckyai-root-release + 发布收件箱(Owner 输入哈希) 两者与本规格的关系见 D9、D10;在 Owner 决定前,本规格不修改它们
生产代码与 d1 不在同一谱系(A0 B2) 本规格的 main 以哪条线为基线,由 D9 决定

1. 角色与权限

角色 运行身份 能做 不能做
Owner 你本人(owner 用户 + 自己的 Gitolite 密钥) 新建任务、确认方案、裁决争议、审批特权请求、发布(推送 main 与发布标签)、急停 —
Claude(作者) claude-dev(= A0 的 luckydev) 在自己的克隆里读写代码、跑测试、本地 commit;从本地只读镜像拉取 持有任何 Git 密钥;读取案件数据、生产配置、密钥;绕过代理直接访问外网
Codex(审查者) WSL 独立 distro 中的 codex 用户 在一次性检出的只读副本上审查 写代码;推送;网络(审查时的沙箱);访问 Windows 文件系统
Runner(调度) VPS 上的 agentrun 用户,systemd 服务 启停 Agent 单元;维护本地镜像;从 bundle 导入 Claude 的 commit 并推送任务分支;写对话;触发测试和 staging 部署 写 main 和发布标签;执行特权操作;在 Agent 拥有的仓库中运行 git
CI(测试执行) VPS 上的 agentci 用户 在一次性检出中运行固定的检查命令(setup 经代理联网且不执行脚本,checks 断网) 持有任何凭证(从本地镜像克隆,不需要密钥)
Staging 部署 deploy-staging 执行 agent-factory 中固定的 deploy/staging.sh 访问生产环境
Staging 运行 stagingapp 运行 staging 实例(即 Claude 写的代码) 读取案件数据、生产配置与密钥;访问白名单以外的网络
出站代理 agentproxy 按域名白名单转发 HTTPS(Anthropic API、npm registry、PyPI) —
Codex 包装器 WSL 中的 codexw 用户 拉取任务;以 codex 身份调用 Codex;校验输出后发送留言 写代码分支
状态页 owner-web(独立 Gitolite 身份) 代 Owner 写受限的决定:approve_plan、reject_plan、answer、approve_request 发布、裁决、推送任何代码 ref
Executor(特权执行器) root 拥有的 systemd 服务 只执行 Owner 按哈希批准过的请求,以及 agent-factory 中固定的部署/回滚脚本 接受任何自由文本指令;对不可信仓库运行会读取工作区配置的 git 命令
Git 服务 git 用户(Gitolite) 按身份强制执行 ref 权限,并校验留言格式 —

1.1 关键修正:不共享 .git,也不进入对方的仓库

v1 画的是"一个仓库 + 多个 worktree"。同一仓库的 worktree 共享 .git(包括 hooks 和 config),Claude 只要能写自己的 worktree,就能植入 hook,等 Runner 运行 git 命令时以 Runner 身份执行。v3 改为每个身份一个独立克隆,但仍让 Runner "从 Claude 的克隆 fetch"并检查工作区是否干净。这不够:

v3.1 规则:Runner 从不在 Agent 拥有的目录中运行 git。 数据双向流动方式如下:

1.2 调用机制:systemd 模板单元(替代 sudo)

单元 User 用途
agent-prep@.service claude-dev 从镜像同步克隆,准备工作分支
agent-claude@.service claude-dev 运行 Claude
agent-handoff@.service claude-dev 检查工作区,生成 bundle
agent-ci-setup@.service agentci 安装依赖(经代理,不执行脚本)
agent-ci@.service agentci 运行检查(PrivateNetwork=yes)
agent-staging-deploy@.service deploy-staging 部署 staging

2. 总体架构

                ┌──────────────────────────── VPS ──────────────────────────────┐
 Owner ─agentctl─▶ Gitolite(git 用户)                                             │
 (ssh / 状态页)  │   lucky-ai.git: main · agent/<JOB>/v<N> · agent/<JOB>/chat · stop  │
                │   agent-factory.git(Runner/hooks/Executor 代码,仅 Owner 可写)     │
                │      ▲ 推送任务分支/对话 (agentrun)          ▲ 推送对话 (codexw)      │
                │  ┌───┴─────────────┐  更新   ┌────────────────────────┐            │
                │  │ Runner          │───────▶│ 本地只读镜像              │            │
                │  │ (agentrun)      │        └───┬────────────────┬───┘            │
                │  └─┬──────────▲────┘   file://  │                │ file://         │
                │    │启停单元   │读 bundle        ▼                ▼                 │
                │    │     ┌────┴───────┐   claude-dev 克隆      agentci 一次性检出    │
                │    └────▶│ handoff 目录│◀── agent-claude@      agent-ci@(断网)     │
                │          └────────────┘                                          │
                │  agentproxy(域名白名单)◀── claude-dev / agent-ci-setup / staging   │
                │  Executor (root) ◀── Gitolite 中 Owner 按哈希批准的请求与发布决定     │
                │  staging(stagingapp,合成数据) / production                       │
                └───────────────────────────────────────────────┬─────────────────┘
                                                                │ 仅出站
                ┌──────────── WSL(独立 distro)─────────────────┴───┐
                │ codexw:轮询 → 一次性检出 → 以 codex 身份 codex exec  │
                │        (只读沙箱,提示词走 stdin)→ 校验 → 发留言     │
                └────────────────────────────────────────────────────┘

3. 仓库与 ref 布局

ref 写入者 规则
refs/heads/main 仅 owner(agentctl release 在生产健康检查通过后推送) 只允许快进;禁止强推和删除
refs/heads/agent/<JOB>/v<N> agentrun 内容来自 Claude 的 bundle;只允许快进。rebase 时新建 v<N+1>,旧分支保留用于审计(也防止旧 SHA 被 GC)
refs/agent/<JOB>/chat agentrun、codexw、owner、owner-web 只允许追加(快进);服务端 hook 校验每条留言(§4.4)
refs/agent-control/stop owner 存在即急停(§9.3)
refs/tags/release/<JOB> 仅 owner 不可删除、不可移动

refs/agent/ 下除 refs/agent/<JOB>/chat 以外的任何 ref 都会被 hook 拒绝。

Gitolite 规则示意(实现时以 Gitolite 官方文档核对):

repo lucky-ai
    RW   refs/heads/main$                              = owner
    -    refs/heads/main$                              = @all
    RW   refs/tags/release/                            = owner
    -    refs/tags/release/                            = @all
    RW+                                                = owner
    RW   refs/heads/agent/[A-Z]+-[0-9]+/v[0-9]+$       = agentrun
    RW   refs/agent/[A-Z]+-[0-9]+/chat$                = agentrun codexw owner-web
    R                                                  = agentrun codexw
    -                                                  = @all

repo agent-factory
    RW+                                                = owner
    R                                                  = agentrun codexw

说明:


4. 留言协议

4.1 存储方式

每条留言是 refs/agent/<JOB>/chat 上的一个 commit(空树),commit message 格式如下:

[<type>] <JOB> #<id> <from>

<一行 JSON 对象>

为什么用空树:对话不会混进代码历史,也不占用工作区。首行中的 type、JOB、id、from 必须与 JSON 一致,hook 会核对。

4.2 公共字段

{
  "schema_version": 1,
  "job": "DEV-184",
  "id": 7,
  "from": "codex",
  "type": "risk",
  "reply_to": 5,
  "commit": "<40 位 SHA,或 null>",
  "created_at": "2026-10-03T10:00:00Z",
  "body": "Markdown 文本,≤ 8 KB"
}

4.3 留言类型

type 发送方 用途 专有字段
task owner 需求 title、acceptance_criteria[](必须可验证)、base(main 的 SHA)、plan_review: required/skip
plan claude 一页方案 approach、files[]、risks[]、test_plan、questions[]、privileged_needs[]
review claude 请求审查某个 SHA commit、branch、base、tests_run[]、uncertainties[]、cost_usd
risk codex 审查发现问题 subject、findings[]、prior_findings[]
done claude 修复后的回应 commit(新 SHA)、branch、previous_commit、resolutions[]、cost_usd
approve codex 审查对象已无阻塞问题 subject:code 或 privileged:<REQ>;commit(subject=code 时)或 request_msg_id(特权请求时);closed[]、residual_low[]
check runner 确定性检查结果 check_id:ci / staging_smoke / prod_health / privileged:<REQ>;commit;checks_from(仅 ci,main 的 SHA);results[];verdict: pass/fail
escalate runner 需要 Owner 处理 reason_code(§8)、summary、options[]
privileged_request runner(代 Claude 提交) 申请特权操作 见 §10
owner owner、owner-web Owner 的决定 decision:approve_plan / reject_plan / answer / rule / release / abort / retry / shelve / approve_request;release 另带 commit、with_migration
state runner 状态变更记录(便于回放) from_state、to_state

4.4 服务端校验(Gitolite VREF 或 update hook)

推送到 refs/agent/ 下时,以下任何一条不满足都会被拒绝:

  1. ref 名必须是 refs/agent/<JOB>/chat;

  2. 只允许快进;每次推送只追加 1 条留言;该 commit 只有一个父提交(第一条除外),树为空树;

  3. JSON 能解析,通过对应 type 的 schema 校验,并与 commit message 首行一致;

  4. id 连续,reply_to 合法;

  5. 推送身份决定允许的 from 和 type:

    推送身份 允许的 from 允许的 type
    owner owner task、owner(全部 decision)
    owner-web owner owner,且 decision ∈ approve_plan/reject_plan/answer/approve_request
    agentrun claude plan、review、done
    agentrun runner check、escalate、privileged_request、state
    codexw codex risk、approve

    因此 Agent 无法冒充 Owner,Runner 也无法替 Codex 发 approve;

  6. commit、previous_commit 若不为空,必须可从某个 refs/heads/agent/<JOB>/v* 到达;branch 必须是包含该 commit 的那个分支;

  7. owner 的 approve_request 中的 script_sha256 必须等于它所引用的 privileged_request 中的值。

4.5 发现项(finding)结构

{
  "fid": "F3",
  "severity": "high",
  "title": "缓存 key 未包含 user_id,跨用户泄露",
  "location": "src/product/cache.ts:42",
  "evidence": "两个用户先后请求 /products?mine=1 会命中同一个 key",
  "suggestion": "key 加上 user_id,或只缓存公共字段"
}

4.6 留言是数据,不是指令


5. 任务状态机

状态不另外存储:Runner 通过重放 chat 日志推导出当前状态。每次状态变更额外写一条 state 留言,方便人工回放。

NEW(排队)
 └─▶ PLANNING ──(plan 中有 questions 或 plan_review=required)──▶ PLAN_PENDING_OWNER
        │                                                          │ approve_plan / answer
        └──────────────(plan_review=skip 且无问题)──────┬───────────┘
                                                        ▼
                                                 BUILDING(分支 v<N>)
                                                        │ Claude 输出 review
                                                        ▼
                               ┌──────────────── IN_REVIEW(SHA) ────────────────┐
                               │   并行:CHECKING(SHA)  +  REVIEWING(SHA)        │
                               └────────────────────────┬───────────────────────┘
          check(ci)=fail 或 risk 中有阻塞项 ┌────────────┴──────────┐ check(ci)=pass 且 approve(code)
                                            ▼                       ▼
                                  FIXING(round+1)              VERIFIED(SHA)
                                            │ done(新 SHA)          │ 自动部署 staging
                                            └──▶ IN_REVIEW          ▼
                                                              STAGING_READY ──通知 Owner
                                                                    │ owner: release
                                                                    ▼
                                  main 不能快进到 SHA ──是──▶ REBASE_NEEDED ──▶ BUILDING(v<N+1>,rebase)
                                                                    │ 否
                                                                    ▼
                                                               RELEASING(Executor 部署生产)
                                         prod_health=fail ◀────────┤
                                                ▼                   │ prod_health=pass
                                     ROLLED_BACK → E12              ▼
                                                           RELEASE_FINALIZING(Owner 推送 main 与标签)
                                                                    ▼
                                                                RELEASED
 BUILDING / FIXING ──▶ PRIVILEGED_REVIEW(Codex 审脚本)──▶ AWAITING_APPROVAL ── 批准并执行 ──▶ 回到原状态
 任意状态 ──▶ ESCALATED(§8)── Owner 决定 ──▶ 回到相应状态
 任意状态 ──▶ PAUSED(急停) / ABORTED(Owner)
 STAGING_READY ── owner: shelve ──▶ 回到队首(重新进入时先重新部署 staging)

VERIFIED(X) 的充分必要条件(全部针对同一个 SHA X):

  1. Runner 的 check 留言:check_id=ci、commit=X、verdict=pass;
  2. Codex 的 approve 留言:subject=code、commit=X;
  3. 所有 high/medium 发现项的状态为 closed、withdrawn,或经 Owner rule 裁定为 accept_risk;
  4. base..X 的 diff 没有触碰 forbidden 或 gate_config(§7.4)。

任何新 commit 都会让任务回到 IN_REVIEW,旧的 check 和 approve 自动失效。

rebase:发布时 main 已前进,Runner 让 Claude 在新分支 v<N+1> 上 rebase 到新的 main。rebase 不计入修复轮次,但必须重新走 IN_REVIEW → staging 的完整流程。冲突的解决同样由 Codex 审查。

并发:第一版同一时间只运行一个活动任务,其他任务排队。从 PLANNING 起直到 RELEASED/ABORTED 都算活动。STAGING_READY 也算活动,因为只有一个 staging 实例;Owner 暂时不想验收时可以 shelve,让出 staging。等运行稳定后再考虑并行。


6. Agent 调用

A0 核对项:以下 CLI 参数实现前必须用当前版本逐项核对。2026-10-03 已知:claude 2.1.288 的 --help 列出了 --max-budget-usd、--output-format、--permission-mode、--allowedTools,没有列出 --max-turns,需确认该参数是否仍然可用;Claude 是否遵循 HTTPS_PROXY;codex exec 能否从 stdin(-)读取提示词,以及 -C、--sandbox、--output-schema、-o 是否可用。

6.1 Claude(作者)

每次调用 Claude 分三步,全部以 claude-dev 身份在各自的单元中运行:

① 准备(agent-prep@):从本地镜像 fetch;BUILDING 时从 base 建立工作分支,FIXING 时从上一个 commit 继续,rebase 时同时取最新的 main。

② 运行(agent-claude@):

# 片段,完整示例见附录 C
[Service]
User=claude-dev
WorkingDirectory=/home/claude-dev/work/lucky-ai
EnvironmentFile=/run/agentrun/%i/env          # 只含 MAX_TURNS、BUDGET_USD,由 Runner 生成
StandardInput=file:/run/agentrun/%i/prompt.md # 提示词走 stdin:不受 128 KiB argv 限制,ps 也看不到
StandardOutput=file:/var/lib/agent-factory/out/%i.json
ExecStart=/usr/local/bin/claude -p --output-format json \
  --max-turns ${MAX_TURNS} --max-budget-usd ${BUDGET_USD} \
  --permission-mode acceptEdits \
  --allowedTools "Read,Edit,Write,Glob,Grep,Bash(python -m pytest:*),Bash(npm run build:*),Bash(npm install:*),Bash(git status:*),Bash(git diff:*),Bash(git log:*),Bash(git show:*),Bash(git add:*),Bash(git commit:*),Bash(git rebase:*)"

③ 交接(agent-handoff@):

# 以 claude-dev 身份运行:这是 Claude 自己的仓库,不存在提权问题
test -z "$(git status --porcelain)"                 # 工作区必须干净
test "$(git rev-parse HEAD)" = "$CLAIMED_COMMIT"    # 与 Claude 输出的 commit 一致
git bundle create "$HANDOFF/$INST/work.bundle" "$BASE..HEAD"

④ 导入(Runner,在 agentrun 自己的仓库中):

交接目录 /var/lib/agent-factory/handoff/ 属主 claude-dev、组 agentrun,权限 2750。

6.2 Codex(审查者):两遍审查,保证独立性

# 在 WSL 中由 codexw 执行
dir=$(mktemp -d /var/tmp/review.XXXXXXXX)     # 不使用可预测的 /tmp 路径
# $dir/src:该 SHA 的一次性检出(codex 只读);$dir/out:属主 codex 的输出目录
sudo -u codex -- codex exec -C "$dir/src" \
  --sandbox read-only \
  --output-schema /opt/agent/schemas/findings.json \
  -o "$dir/out/result.json" \
  - < "$dir/prompt.md"                        # 提示词走 stdin

6.3 超时、重试与限流

步骤 超时(默认,可配置;由单元的 RuntimeMaxSec 强制)
Claude 方案 15 分钟
Claude 开发 / 修复 / rebase 60 分钟
Codex 审查(两遍合计) 20 分钟
确定性检查(setup + checks) 30 分钟

重试 ≠ 修复:


7. 确定性检查(测试门禁)

7.1 检查清单从 main 读取

检查命令定义在 .agent/checks.yaml 中,Runner 始终从 main 读取这个文件,绝不从任务分支读取。

但只做到这一点还不够:检查命令会运行任务分支中的 package.json 脚本、pytest.ini、vite/tsconfig 配置。只要 Claude 改了这些文件(例如把 pytest.ini 的 addopts 改成跳过全部测试),检查就会照常"通过"。因此这些文件列为门禁配置(gate_config),任务不得修改(§7.4)。

示例(与当前仓库一致:后端 Python + pytest,前端只有 build 脚本):

# .agent/checks.yaml(受保护文件,见 §7.4)
setup:                        # 经代理联网,但不执行任何安装脚本
  - id: py-deps
    cwd: backend
    argv: ["sh", "-c", "python3 -m venv .venv && .venv/bin/pip install --no-cache-dir --only-binary=:all: -r requirements.lock.txt"]
    network: proxy            # --only-binary:拒绝 sdist,联网时不运行任何 setup.py
  - id: js-deps
    cwd: frontend
    argv: ["npm", "ci", "--ignore-scripts"]
    network: proxy
  - id: js-deps-scripts
    cwd: frontend
    argv: ["npm", "rebuild"]
    network: none             # 依赖的安装脚本在断网后才执行
checks:                       # 全部断网
  - id: backend-tests
    cwd: backend
    argv: [".venv/bin/python", "-m", "pytest", "-q"]
  - id: frontend-build
    cwd: frontend
    argv: ["npm", "run", "build"]     # tsc -b && vite build
timeout_s: 1800

7.2 执行方式

7.3 测试完整性报告

Runner 自动比对 base..SHA,列出以下变化,交给 Codex 第 1 遍审查,同时显示在状态页:

上述变化如果没有 Codex 明确认可的理由 → 作为 high 级发现项处理;仍无法收敛 → E9。

7.4 受保护路径

配置文件为 .agent/protected.yaml,从 main 读取:

级别 路径示例 处理方式
forbidden .agent/<strong>、deploy/</strong>、infra/<strong>、.github/</strong> 等 CI 配置、.env*、scripts/backup*(由 cron 执行,见 A0 F6) 任务的 diff 一旦触碰 → 立即 E4,不进入审查
gate_config frontend/package.json 的 scripts 字段(按 JSON 语义比较,不是整个文件);backend/pytest.ini、setup.cfg/pyproject.toml 中的 pytest 配置;frontend/tsconfig*.json、frontend/vite.config.*;.npmrc、pip.conf;以后新增的 lint/test 配置 视同 forbidden → E4。需要修改时由 Owner 亲自修改
sensitive backend/alembic/<strong>(迁移)、</strong>/auth/<strong>、</strong>/payment/**、依赖清单(requirements*.txt、package.json 中除 scripts 以外的部分、lockfile)、**/conftest.py 允许修改;Codex 必须重点审查;发布页面上高亮提示

本系统自身的代码(Runner、hooks、Executor)在独立的 agent-factory 仓库中,Agent 对它没有写权限。


8. 升级条件(何时打扰 Owner)

代码 触发条件 Owner 的选项
E1 ROUND_LIMIT 修复 2 轮后仍有阻塞项,或检查仍失败 再给 N 轮 / 亲自指导 / 放弃
E2 DISPUTE Claude 标为 disputed,Codex 复核后仍坚持(maintained) 对每个 fid 裁定 fix 或 accept_risk
E3 CHECK_UNRELIABLE 测试不稳定,或 base 上已经失败 修好测试 / 临时豁免该检查
E4 FORBIDDEN_PATH diff 触碰了 forbidden 或 gate_config 拒绝 / 由 Owner 自己修改
E5 BUDGET 单任务的耗时或费用超出上限,或当日总额超限 追加预算 / 终止
E6 PLAN_QUESTIONS 方案中带有问题,或 plan_review=required(即 PLAN_PENDING_OWNER) 回答 / 确认方案
E7 INFRA 重试次数用尽(限流不算,见 §6.3) 排查环境
E8 STAGING_FAIL_AFTER_FIX staging 部署或冒烟测试在修复后仍然失败 同 E1
E9 TEST_INTEGRITY 测试被删除或跳过,且无法达成一致 裁定
E10 AGENT_MISBEHAVIOR 输出连续不合 schema、越权尝试被拒绝 查看日志
E11 SELF_CHECK_FAILED 启动自检失败(§9.4) 修复隔离配置;修好之前系统不运行
E12 PROD_ROLLBACK 生产健康检查失败,已自动回滚;main 未推进 查看日志 / 回到 FIXING / 放弃;涉及迁移时决定是否恢复备份

特权请求(§10)不算升级,走单独的审批通知。

通知原则:只在 E1–E12、待审批、staging 可验收、发布结果这几种情况推送;外加每日一条摘要。通知内容只包含任务号、状态、原因和一行摘要,不含代码、日志和密钥。


9. Owner 界面

9.1 agentctl(阶段 B,命令行)

在 VPS 上以 owner 身份运行。命令会写入 from=owner 的留言,服务端按身份校验。

agentctl new "商品列表接口加缓存" --accept "命中率…" --accept "更新后 1s 内失效" [--no-plan-review]
agentctl status                 # 所有任务:状态 / SHA / 轮次 / 用时 / 费用 / 待办
agentctl status DEV-184         # 单个任务详情 + 未关闭的发现项
agentctl log DEV-184            # 回放完整对话
agentctl approve-plan DEV-184   |  agentctl reject-plan DEV-184 "意见"
agentctl answer DEV-184 "问题的答复"
agentctl rule DEV-184 F3 accept-risk "理由"     # 或 fix
agentctl release DEV-184        # 见 §11.2;等待生产健康检查,通过后推送 main 与标签
agentctl finalize DEV-184       # release 中途断开时,补做推送 main 与标签
agentctl shelve DEV-184         # 让出 staging,任务回到队首
agentctl approve REQ-7          # 先完整显示脚本、理由、影响、回滚方式和 Codex 结论,再要求输入 sha256 前 12 位
agentctl retry DEV-184 | abort DEV-184
agentctl stop | resume          # 急停

9.2 状态页(阶段 C)

9.3 急停

9.4 启动自检(失败即停)

Runner 启动时、每天一次、以及每个任务开始前,执行以下检查。任何一项不符合预期 → E11,系统不运行。

VPS 上,以 claude-dev、agentci、stagingapp、deploy-staging 身份(每个身份一个自检单元 agent-selfcheck-<身份>.service,已加入 polkit 规则):

VPS 上的配置:

WSL 上(由 codexw 自检后上报):


10. 特权操作:审批与执行器

适用范围:root 或系统配置、防火墙、系统级软件包安装、新增出站网络(包括代理白名单)、生产数据库迁移、密钥变更,以及所有不经 §11.2 发布流程的生产操作。(生产发布和发布失败时的自动回滚走 §11.2,不在此列。)

流程:

  1. Claude 在输出的 privileged_needs[] 中内联要执行的脚本内容。脚本里不能有交互,也不能从外部下载并执行内容(系统包管理器安装软件包除外,须在 category=system_package 中声明)。脚本不提交到任务分支,因此不会随发布进入 main;

  2. Runner 发出 privileged_request 留言,任务进入 PRIVILEGED_REVIEW:

    {
      "req_id": "REQ-7",
      "category": "system_package",
      "script": "#!/bin/bash\n…(≤ 16 KB)",
      "script_sha256": "…",
      "reason": "…",
      "impact": "…",
      "rollback": "…",
      "expires_at": "<发出后 24 小时>"
    }
    
  3. Codex 先审查这个脚本,发出 approve(subject=privileged:REQ-7)后,请求才会推送给 Owner(AWAITING_APPROVAL);

  4. Owner 执行 agentctl approve REQ-7(先显示完整脚本,再输入哈希前缀),或在状态页上二次认证后批准。批准对象就是这个 sha256;

  5. Executor(root)轮询 Gitolite 的裸仓库,只使用 cat-file、rev-parse、for-each-ref 这类只读命令,执行前依次确认:

    • 存在 from=runner 的请求、from=codex 的 approve(subject=privileged:REQ-7)、from=owner 的 approve_request,且三者的 sha256 一致;
    • 从请求留言中取出脚本,写入 root 拥有的目录,重新计算哈希并比对;
    • 请求未过期,批准未被使用过(已使用记录保存在 root 拥有的 /var/lib/agent-factory/executor/used/);
    • 以 bash -euo pipefail 执行,完整记录日志;
  6. Executor 把结果写入 /var/lib/agent-factory/executor/results/REQ-7.json(Runner 可读),Runner 以 check 留言回写(check_id=privileged:REQ-7)。Executor 自身没有 Gitolite 身份。

硬规则:

与现有发布入口的关系:luckyai-root-release 已采用"暂存副本 + Owner 输入哈希"的做法,Executor 沿用同样的思路。是替换还是包装它,由 D10 决定。


11. Staging 与发布

11.1 Staging(自动)

11.2 发布(Owner 审批)

  1. Owner 执行 agentctl release DEV-184。确认页面列出:SHA X、staging 地址、sensitive 路径的 diff、遗留的 low 项、被 Owner 裁定接受的风险。
  2. 如果 main 不能快进到 X → REBASE_NEEDED(§5),本次发布结束。
  3. 如果 diff 触碰了迁移目录,必须加 --with-migration 再确认一次;没有加则拒绝。
  4. agentctl 写入 owner 留言(decision=release、commit=X、with_migration),任务进入 RELEASING。
  5. Executor 看到发布决定后,核对 X 已有 check(ci)=pass、approve(code)、check(staging_smoke)=pass,然后:
    • 涉及迁移时先备份数据库,再执行迁移;
    • 执行 /opt/agent-factory/deploy/prod.sh X 和健康检查;结果由 Runner 回写为 check(prod_health)。
  6. 健康检查通过 → agentctl 以 Owner 身份把 main 快进到 X,并打上 release/DEV-184 标签 → RELEASED。如果 agentctl 中途断开,用 agentctl finalize DEV-184 补做。
  7. 健康检查失败 → Executor 执行 deploy/rollback.sh <上一个 release 标签> → ROLLED_BACK → E12。main 不推进,下一个任务仍然基于健康的代码。

迁移与回滚:迁移必须与上一个发布版本的代码兼容(先扩展、后收缩),这样回滚代码后旧代码仍能在新表结构上运行。Codex 审查 backend/alembic/** 时必须检查这一点。确实做不到兼容时,在 release 确认页中明确提示,回滚后由 Owner 决定是否恢复备份。


12. 隔离与安全基线


13. 分阶段实施

阶段 0:前置条件(root 操作,Owner 亲自执行)

阶段 A:协议与手动触发(1–2 天,不需要 root)

范围限制:只在不含案件数据的测试仓库上进行,在 Owner 启动的会话中完成,不涉及生产,也不需要阶段 0。

交付物(放在 agent-factory 仓库):

验收:

阶段 B:调度与强制边界(3–5 天,需要阶段 0)

交付物:

验收(全部必须自动化测试通过):

阶段 C:审批与发布(3–5 天)

交付物:staging 部署与冒烟测试(含 stagingapp 沙箱)、Executor 与特权请求流程、发布与回滚、状态页。

验收:

由谁来实现

时间约束

v2 中有一条规则:"2026-10-10 结束前,Claude 只能由 Owner 启动,不允许无人值守";A0 报告也记录了"庭审周期间不再请求 root 操作"。如果这两条仍然有效:


14. 取舍记录

14.1 与 v1 / v2

来源 内容 处理方式
v1 五个组件的分工 保留,Coordinator 收窄为 Runner + Gitolite + Executor
v1 单仓库 + 多个 worktree 改为每个身份独立克隆,且 Runner 不进入 Agent 的仓库(§1.1)
v1 worktree-codex → feature/codex(Codex 也写代码) 删除,Codex 只读
v1 Codex 给出 "审查 + 测试 → PASS" 拆为两道门禁;测试由 Runner 亲自运行
v1 修复循环全自动、没有上限 加上轮次上限、升级条件、争议裁决
v1 / v2 SHA 绑定、生产审批、特权操作禁止无人值守、"留言不得变成 shell" 保留,并落实为具体机制
v2 SQLite、租约、心跳、Task/Run 分表 删除,改为重放对话日志推导状态,用 flock 保证单实例
v2 重试 ≠ 修复、不稳定测试处理、留言视为不可信、急停、开发与案件隔离、WSL 加固、IP 隐私 保留
v2 "Codex 只做诊断" 否定,Codex 的语义审查本身就是一道门禁
v2 庭审前规则 作为有条件的时间约束保留(§13)

14.2 v3 → v3.1

v3 v3.1
sudo -u claude-dev(主机没有 sudo) systemd 模板单元 + polkit
Runner 从 Claude 的克隆 fetch,并检查工作区 本地镜像下行,bundle 上行;Runner 不进入 Agent 的仓库
agentci 有 Gitolite 读权限,却又"不持有凭证" 从本地镜像克隆,确实不需要凭证
提示词放在 argv 走 stdin
检查清单从 main 读取,但脚本和测试配置来自任务分支 增加 gate_config 级别
npm ci 联网时执行生命周期脚本 --ignore-scripts / --only-binary,断网后再执行脚本
示例检查为 npm test / npm run lint(仓库中不存在) pytest + 前端 build
agentrun 可随时推进 main 只有 Owner 能写 main 和发布标签
先推进 main,再部署生产 生产健康检查通过后才推进 main
hook 只校验 from 与推送者 按身份校验 from 和 type
状态页身份未定义 owner-web,不能发布
急停用进程组信号;WSL 只在拉取前检查 按 cgroup 终止;WSL 运行中轮询,读不到即停
引用不存在的 §7.6;rebase 需要强推 新建 v<N+1> 分支,所有 ref 只快进
特权脚本提交在任务分支 内联在留言中
staging 运行身份不在隔离范围内 stagingapp 纳入沙箱与自检

14.3 与主机上已有资产

资产 处理方式
devcontrol/d1 的 SQLite dev_control(dev_tasks/dev_events 等) 不作为本系统的状态存储;是否把对话日志同步到其中做展示,见 D9
luckyai-root-release + 发布收件箱 Executor 沿用其"暂存副本 + Owner 输入哈希"的做法;替换还是包装,见 D10
WSL_WORKER_DEVELOPMENT_PLAN.md 中的案件 Worker 与 Codex 的 distro 严格分开;通道选择见 D14

15. 待 Owner 决定

# 问题 建议
D1 开发流水线是否迁到独立的开发 VPS? 长期建议迁移;短期先用同一台 VPS + 用户隔离 + 单元沙箱 + 自检
D2 Git 服务用 Gitolite 还是 Forgejo/Gitea? Gitolite:轻量,按 ref 授权正好满足需要;需要网页界面时再考虑 Forgejo
D3 通知渠道(Telegram / 邮件 / 企业微信) 选一个你每天都会看的
D4 新增依赖是否需要 Owner 审批? 默认作为 sensitive 处理并在发布页高亮,不单独审批
D5 预算:单任务耗时和费用上限、每日总额上限、限流等待多久后通知 先定保守值,跑两周后再调整
D6 修复轮次上限 2
D7 plan_review 默认值 新功能 required,bugfix 任务由你在新建时加 --no-plan-review
D8 v2 的庭审前规则、A0 的"庭审周不做 root 操作"是否仍然有效? 决定阶段 0 和阶段 B 何时开始
D9 main 的基线:devcontrol/d1(61ae661)还是已部署的谱系(aaaee3d,case-discovery/mvp)?d1 的 dev_control 如何处置?(A0 W2) 以已部署的谱系为基线,避免"能发布的代码"与"生产代码"不一致;d1 中需要的部分以任务的形式移植
D10 Executor 替换还是包装现有的 luckyai-root-release? 阶段 C 中替换;过渡期保留旧入口,两者不同时使用
D11 开发身份命名:luckydev(A0 已记录)还是 claude-dev 沿用 luckydev,本规格中的 claude-dev 统一替换
D12 出站代理的实现,以及 staging 需要访问哪些外部服务 用 tinyproxy 或 squid 做域名白名单;staging 默认不允许外部访问
D13 STAGING_READY 是否阻塞队列 阻塞;需要时用 shelve 让出
D14 codexw 到 VPS 的通道:直接 SSH,还是经现有的 Cloudflare Access/Tunnel 如果 VPS 已经对外开放 SSH(仅密钥登录),直接用 SSH;否则用 Cloudflare Access 的 SSH 代理,不新开端口

附录 A:agent send 的实现要点

# 追加一条留言:空树 commit;非快进的推送会被拒绝,相当于自带并发保护
ref="refs/agent/$JOB/chat"
git fetch origin "+$ref:$ref" 2>/dev/null || true
parent=$(git rev-parse -q --verify "$ref" || true)
id=$(( $(git rev-list --count "$ref" 2>/dev/null || echo 0) + 1 ))
json=$(build_and_validate_json "$id" ...)          # 按 schema 校验后再组装
empty=$(git hash-object -w -t tree /dev/null)       # -w:确保空树对象写入对象库
new=$(printf '[%s] %s #%s %s\n\n%s\n' "$type" "$JOB" "$id" "$from" "$json" \
      | git commit-tree "$empty" ${parent:+-p "$parent"})
git update-ref "$ref" "$new" ${parent:-""}
git push origin "$ref"                              # 被拒绝时:重新 fetch,重算 id,重试(最多 3 次)

# 读取对话
git log --reverse --format='%B' "refs/agent/$JOB/chat"

自定义 ref 默认不会被拉取,每个克隆(包括本地镜像)都需要配置:

git config --add remote.origin.fetch '+refs/agent/*:refs/agent/*'

从本地镜像克隆时使用 file:// URL(或 --no-local),避免硬链接对象目录。

附录 B:角色文件要点

.agent/CLAUDE_ROLE.md:

.agent/CODEX_ROLE.md:

附录 C:systemd 单元与 polkit 示例

# /etc/systemd/system/agent-claude@.service(由 Owner 从 agent-factory 安装,root 拥有)
[Unit]
Description=Claude author step %i

[Service]
Type=exec
User=claude-dev
Group=claude-dev
WorkingDirectory=/home/claude-dev/work/lucky-ai
EnvironmentFile=/run/agentrun/%i/env
Environment=HTTPS_PROXY=http://127.0.0.1:3128 HTTP_PROXY=http://127.0.0.1:3128
StandardInput=file:/run/agentrun/%i/prompt.md
StandardOutput=file:/var/lib/agent-factory/out/%i.json
StandardError=journal
ExecStart=/usr/local/bin/claude -p --output-format json --max-turns ${MAX_TURNS} --max-budget-usd ${BUDGET_USD} --permission-mode acceptEdits --allowedTools "…(同 §6.1)"

NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=tmpfs
BindPaths=/home/claude-dev
ReadWritePaths=/home/claude-dev
PrivateTmp=yes
# 前缀 "-":路径不存在时不报错。实际路径以身份拆分后的布局为准
InaccessiblePaths=-<CASE_DATA_ROOT> -<PROD_ROOT> -<PROD_CONFIG_DIR> -/var/lib/luckyai
IPAddressDeny=any
IPAddressAllow=localhost
RuntimeMaxSec=3600
KillMode=control-group
TimeoutStopSec=30
MemoryMax=4G
// /etc/polkit-1/rules.d/50-agent-factory.rules
polkit.addRule(function (action, subject) {
  if (action.id !== "org.freedesktop.systemd1.manage-units") return;
  var unit = action.lookup("unit") || "";
  var verb = action.lookup("verb") || "";
  var agentUnit = /^agent-((prep|claude|handoff|ci-setup|ci|staging-deploy)@[A-Za-z0-9-]+|selfcheck-[a-z-]+)\.service$/;
  if (subject.user === "agentrun" && agentUnit.test(unit) && (verb === "start" || verb === "stop")) {
    return polkit.Result.YES;
  }
  if (subject.user === "owner" && verb === "stop" &&
      (agentUnit.test(unit) || unit === "agent-runner.service")) {
    return polkit.Result.YES;
  }
});

实现时核对:当前 systemd/polkit 版本是否向规则传递 unit 与 verb;ProtectHome=tmpfs 与 BindPaths 的组合在当前 systemd 版本下的行为;StandardInput=file: 的文件权限要求。