阶段一集成门禁¶
Issue #14 是矿车 ROM 前置需求(#15、bridge#6、#16、#17、#18、#19)与阶段二冷启动之间的就绪门禁。某个仓库的单测通过、或者某个工具返回 issued,都不是整条实机链路可用的证据。本门禁消费这些前置项产出的实机证据,尽可能自行重算,并在任何缺项上拒绝通过。
门禁由 tools/stage1_gate.py 实现(纯标准库)。它不启动游戏、也不制造证据:它读取一个门禁包(包含 bundle.json 与原始证据的目录),并严格失败关闭:
缺少 bundle / check / 证据,或 origin 为 scaffold -> blocked
证据存在但无效、不一致、不完整 -> fail
每个 check 都通过 -> pass
退出码:0 通过、1 失败、3 阻塞、2 用法错误。脚本无法把"前置未就绪"误当成成功。
已验收状态(2026-09-26)¶
组合实机门禁已通过 8/8 检查,经独立审查后在 PR #30 合并,#14 已完成。 后续自主冷启动和全新实例回归也已验收;#13 总运行手册 汇总三个阶段的审查与证据,门禁执行记录见实机集成运行。
开发基线(历史记录)¶
- 门禁工具、证据 schema 与离线自测已实现;
selftest通过 152 项检查(不涉及游戏与实机证据)。 - 初始工具开发阶段尚无完整实机前置证据,部分 bundle 为
blocked;该历史状态已由上方验收通过的完整门禁取代。新的运行仍会在缺少证据时阻塞。 - 开发期间记录了独立基线:源存档(26.2-Fabric 实例下的
Minecart ROM test)的只读 tree 哈希为8cd54c86af9fa8d6b9ea33441fb21dac295cd2b5ddaa60327f5fb3a30255324a(40 个文件、11 556 310 字节,使用下方规范排除列表)。没有写入任何文件,源存档只被以只读方式打开。 - 集成运行预留端口:27240-27249,通过
run.allowed_port_ranges显式配置(编排便利,不是产品硬编码要求——见下)。
命令¶
python tools/stage1_gate.py scaffold labs/stage1-evidence # 生成规范的空目录结构
python tools/stage1_gate.py list # 列出 check 与所需证据
python tools/stage1_gate.py list --json # 机器可读目录
python tools/stage1_gate.py check labs/stage1-evidence \
--source-world "<只读源存档路径>" \
--report stage1-report.json
python tools/stage1_gate.py hash-tree "<世界目录>" # 确定性只读目录哈希
python tools/stage1_gate.py selftest # 门禁逻辑自测,不需要游戏
check 选项:
| 选项 | 含义 |
|---|---|
--report PATH |
写出完整 JSON 报告 |
--json |
打印 JSON 报告而非文本 |
--verbose |
打印每条断言,而不只是问题 |
--source-world PATH |
只读重算该目录的哈希,与 run.source_world 对比 |
--skip-source-rehash |
跳过独立哈希;整个运行会变为 blocked,永远不会是 pass |
--port-range LOW-HIGH |
收窄 run.allowed_port_ranges;超出已声明区间的值会失败 |
包结构¶
stage1-evidence/
bundle.json
artifacts/
fixture_map/ fixture-manifest.json, init-runs.jsonl,
player-identity.json, command-block-scan.json,
cleanup-rebuild.json, raw/...
restore_fidelity/ snapshot-before/(协议快照目录),
snapshot-after/, restore-record.json,
source-unchanged.json, failure-cases.jsonl
player_context/ identity-records.jsonl, entry-contract.json,
version-pins.json
agent_dev_capability/ tool-environment.json, jar-update.json,
smoke-mod.json, instance-isolation.jsonl
independent_test_mod/ test-mod-manifest.json, audit-events.jsonl,
negative-cases.jsonl, audit-lifecycle.json
trace_persistence/ tool-trace.jsonl, trace-join.json,
missing-log-detection.jsonl
smoke_fixture_validity/ smoke-report.json, fixture-validity.json,
version-lock.json, evidence-index.json
bundle.json 是清单。checks.<id>.evidence.<kind> 指定每个证据相对包根目录的路径;路径使用 POSIX(/)、必须留在包内、不能含 ..。
{
"schema_version": 1,
"kind": "mc-agent.stage1.gate-bundle",
"origin": "live",
"generated_at": "2026-09-26T12:00:00Z",
"run": {
"run_id": "rom13-stage1-20260926",
"issue": "guajun/mc-agent#14",
"allowed_port_ranges": ["27240-27249"],
"tool_category_map": { "terminal": ["bash"], "file": ["write"], "source": ["git"], "mcp": ["mcp"] },
"child_runs": [
{ "run_id": "init-child-1", "instance_id": "exp-1" },
{ "run_id": "init-child-2", "instance_id": "exp-1" },
{ "run_id": "init-child-3", "instance_id": "exp-1" }
],
"source_world": {
"label": "Minecart ROM test",
"path": "<只读源存档路径>",
"before_tree_sha256": "<64 位十六进制>",
"after_tree_sha256": "<64 位十六进制>"
},
"instances": [
{ "instance_id": "src-audit", "role": "source_audit", "dimension": "minecraft:overworld",
"world_dir": "labs/rom13-src/world", "rcon_port": 27240, "bridge_port": 27241 },
{ "instance_id": "exp-1", "role": "experiment", "dimension": "minecraft:overworld",
"world_dir": "labs/rom13-exp/world", "rcon_port": 27242, "bridge_port": 27243 }
]
},
"checks": {
"fixture_map": {
"evidence": {
"fixture_manifest": "artifacts/fixture_map/fixture-manifest.json",
"init_runs": "artifacts/fixture_map/init-runs.jsonl",
"player_identity": "artifacts/fixture_map/player-identity.json",
"command_block_scan": "artifacts/fixture_map/command-block-scan.json",
"cleanup_rebuild": "artifacts/fixture_map/cleanup-rebuild.json"
}
}
}
}
只有 origin 为 live 才可能通过。scaffold(以及其它任何值)无论其余内容多完整都是 blocked。child_runs 声明各次独立初始化;每个初始化样本引用其中之一,而不必强行共用同一个父 run id。
Check 与证据 schema¶
python tools/stage1_gate.py list 会连同规范路径打印同一份目录。类型约定:s 字符串、h64 64 位小写十六进制、h40 40 位小写十六进制(commit)、h16 16 位小写十六进制(协议 orderHash)、b 布尔、i 整数、n 有限数、v3 [x, y, z]、t ISO-8601 时间戳。
1. fixture_map —— 地图 fixture 固定且初始化确定(#15)¶
fixture-manifest.json
| 字段 | 类型 | 要求 |
|---|---|---|
map.url |
s | 不可变/带版本的下载入口 |
map.sha256 |
h64 | 地图文件哈希(与 version_lock.fixture-map 交叉核对) |
map.bytes |
i | > 0 |
map.mc_version |
s | 如 26.2(与 version_lock.minecraft 交叉核对) |
map.immutable |
b | true |
map.download_verified |
b | 冷缓存下载已验证 |
map.bad_hash_rejected |
b | 错误哈希被拒绝而非接受 |
mods[] |
list | >= 1 项,每项 {name, version, sha256(h64)};carpet 项必需并与 version_lock.carpet 交叉核对 |
world.directory |
s | 副本名,绝不是源存档本身 |
world.tree_sha256 |
h64 | 准备好的世界副本的 tree 哈希 |
world.files |
i | > 0 |
init-runs.jsonl —— 每次独立初始化一行,至少 3 行:
| 字段 | 类型 | 要求 |
|---|---|---|
init_id |
s | 每次运行唯一 |
run_id |
s | 必须在 run.child_runs 中声明;必须使用 >= 3 个不同的 child run |
instance_id |
s | 必须等于该 child run 声明的实例 |
state_hash |
h64 | 所有初始化一致 |
order_hash |
h16 | 所有初始化一致 |
entity_count |
i | > 0,各次一致 |
inventory_total |
i | > 0,各次一致 |
early_output |
b | false |
ready |
b | true |
tick |
i | >= 0 |
player-identity.json:uuid、name、dimension、pos(v3)、yaw、pitch(n)、source(s,如 carpet)、facing_target(b true)、server_vantage_uuid(s,必须等于 uuid)。该 uuid 是绑定的任务玩家:identity-records.jsonl 中除 unknown_identity 外的每一行都必须使用它,每个 input_attempt/input_processed 审计事件也必须携带它(或按下方规则显式声明缺失)。
command-block-scan.json:method(s)、world_dirs(非空字符串列表)、command_blocks(i,必须为 0)、scanned(b true)、placed_by_init(b false)。world_dirs 必须覆盖 fixture world.directory 以及每个已声明的 run.instances[].world_dir,扫描无关目录不能代替已声明世界。
cleanup-rebuild.json:steps(非空字符串列表)、source_world_untouched(b true)、rebuild_reproducible(b true)。
2. restore_fidelity —— 恢复经过验证,而不只是 issued(bridge#6)¶
| 证据 | 内容 |
|---|---|
snapshot-before/ |
协议快照目录(meta.json + entities.jsonl),格式与 tools/fork_verify.py 读取的一致 |
snapshot-after/ |
受控恢复之后、继续推进 tick 之前拍下的快照 |
restore-record.json |
端点/命令结果记录 |
source-unchanged.json |
源存档恢复前后的 tree 哈希 |
failure-cases.jsonl |
每个注入失败场景一行 |
门禁自行用 fork_verify 加载并校验两份快照,然后比较:orderHash、按类型计数、按 UUID 顺序的完整 nbt 字符串,以及双方都提供时的便利字段 pos/vel。两侧每个实体都必须有非空 NBT 字符串——null、缺失或空 NBT 一律失败(restore_nbt_missing),因此完整库存比较不可能变成空谈。双方都缺 vel 是允许的(它是 NBT 的便利副本,见 docs/fork-verify.md 规则 11);只有一侧有该字段则失败。
restore-record.json:endpoint.source 与 endpoint.target 必须各自能按 instance_id 或唯一 role 解析到 run.instances 中声明的实例;source 必须是 source_audit 实例、target 必须是 experiment 实例,两者必须不同。dimension 必须等于 target 实例声明的 dimension。另外要求:endpoint.target_resolved(true)、endpoint.wrong_target_rejected(true)、chunks_loaded(true)、tick_controlled(true)、duplicates_pre_existing(= 0)、commands_issued(i >= 1)、commands_failed(= 0)、partial_failure(false)、pause_state_preserved(true)、issued_is_not_success(true —— 记录本身必须声明 issued != 已恢复)。
source-unchanged.json:before_tree_sha256(h64)、after_tree_sha256(h64,相等)、unchanged(true)、hash_tool(s)、exclusions(列表)。
failure-cases.jsonl 必须覆盖六个场景——summon_refusal、duplicate_pre_existing、wrong_endpoint、inventory_mutation_order_hash、corrupt_metadata、partial_failure——每行 {case, injected, expected, observed, passed: true}。
3. player_context —— 真实假人身份与服务端 vantage(#16)¶
identity-records.jsonl 必须覆盖 task_bind、hit、miss、two_players、unknown_identity。公共字段:case、uuid、viewed_uuid、dimension、pos(v3)、yaw、pitch、task_entry、channel(mcp 或 cli —— 实际使用的服务端 vantage 通路)、accepted(b)。task_bind、hit、miss、two_players 都必须看到任务 uuid(不得串人);two_players 还需 other_uuid;unknown_identity 必须 accepted: false 且有非空 rejected_reason。
entry-contract.json:mode(external_task 或 manual_external)、fields(非空字符串列表)、native_chat_verified(b —— 允许 false 并如实记录)、unsupported_entries(字符串列表)。
version-pins.json:interface_mod 与 bridge 对象,含 repo、commit(h40)与 tested: true;commit 会与 version_lock.mc-agent-interface-mod 和 version_lock.mc-agent-bridge 交叉核对。
4. agent_dev_capability —— 在可定位实例上构建并安装(#17)¶
tool-environment.json:harness、model(s)、docs_visible(非空字符串列表),以及 tools 中 terminal、file、filesystem_write、source_access、build、install、mcp_or_cli、lab_manage 全为 true。
jar-update.json:case: same_size_different_content、old_sha256、new_sha256(h64,不同)、bytes(i,大小相同)、loaded_sha256(h64,必须等于 new_sha256)、runtime_evidence(s)。
smoke-mod.json:mod_id、version(s)、built_sha256 与 deployed_sha256(h64,相等)、server_log_ref、sample_output_ref(s)、build_errors_detected、load_failure_detected、missing_dependency_detected、memory_state_rebuilt_after_restart(true)、restart_evidence_ref(s)、no_rom_logic(true —— 这里的每个布尔都必须为 true)。
instance-isolation.jsonl:>= 2 行,含 instance_id、role(source_audit/experiment)、world_dir、rcon_port、bridge_port、restarted(true)、resolves_correct_world(true)、conflicting_instance(false)。每行都必须在 id/role/world/端口上与 run.instances 声明一致;每个声明实例都必须被覆盖;端口必须唯一且落在已声明/生效区间内。
5. independent_test_mod —— 可审计的机器输入与瞬态输出(#18)¶
test-mod-manifest.json:mod_id、version(s)、sha256(h64,与 version_lock.test-mod 交叉核对)、read_only(true)、hook_overhead_ms(n >= 0,整场会话所有 hook 的总时长,不是每次调用/每 tick)、fixture_behavior_unchanged(true)、loaded_in(非空字符串列表,含 source_audit 与 experiment)、agent_mod_coexists(true)、no_command_blocks(true)。
audit-events.jsonl —— 规范的服务端事件 schema:
| 字段 | 类型 | 要求 |
|---|---|---|
event_id |
s | 全文件唯一;重复即失败 |
run_id |
s | 父 run.run_id,或 init/restore 阶段所属的 run.child_runs id |
instance_id |
s | 在 run.instances 中声明;child run 则为其声明的实例 |
dimension |
s | 必须等于该 instance_id 声明的 dimension |
tick |
i | >= 0;真实服务器 tick,重启后允许重置 |
seq |
i | >= 0;按 run_id/instance_id/dimension 严格递增(追加顺序即排序键) |
event |
s | 见下 |
phase |
s | init、agent 或 restore;agent 事件必须属于父 run |
actor_uuid |
s/null | input_attempt/input_processed 上必需:绑定的任务玩家 UUID,或 null 加非空 actor_provenance 说明缺失原因 |
cart_uuid |
s | cart_emitted 与 cart_removed 上必需 |
pos |
v3 | 视事件而定 |
captured_before_removal |
b | cart_emitted 上必须为 true |
removal_reason |
s | cart_removed 上必须存在(如 void) |
任何关联之前先强制来源校验:即使把改动后的字节正确刷新进证据索引,来自未声明 run、实例或维度的事件也会让门禁失败;非 null 的 actor_uuid 必须等于 fixture 假人 UUID(audit_actor_mismatch),键缺失失败(audit_actor_missing),null 且无 actor_provenance 失败(audit_actor_provenance)。必需事件:input_attempt、input_processed、cart_emitted、cart_removed。attempt -> processing -> emission -> removal 链按完整 run_id/instance_id/dimension 身份关联(移除另加 cart_uuid),input_processed 的 actor 必须与其 input_attempt 一致,每个 cart_emitted 都必须有同一身份上更早的处理输入,每个弹出的矿车都必须有对应的 cart_removed,每个弹出的矿车都在移除前被持久化,每次移除都带原因。init/restore 事件永远不算 Agent 操作。
negative-cases.jsonl 必须覆盖 no_interaction、wrong_position、marker_only、answer_only,每行含 attempted(b)、processed(= 0)与 evidence_ref(s)。明确归入负例的事件绝不能是 input_processed。
audit-lifecycle.json:states(非空字符串列表)包含 ready、init、experiment_start、experiment_end、flush;missing_log_status 与 overflow_status 均为 error;per_instance_files(true);ring_buffer_reliance(false)。
6. trace_persistence —— 工具与游戏事件按 run/实例关联(#19)¶
tool-trace.jsonl —— 每次工具调用一行:call_id(唯一)、run_id(必须等于 run.run_id)、instance_id(必须在 run.instances 中声明)、tool、args(对象)、result(任意,必须存在)、error(字符串或 null,必须存在)、started_at/ended_at(t,有序)。trace 必须覆盖 terminal、file、source、mcp 四类。run.tool_category_map 可以按类别覆盖名称匹配,但它只会在默认类别之上合并(部分或空 map 不能删掉必需类别),空/全空白模式会失败。
trace-join.json:joins[] 含 call_id(必须存在于 trace)、audit_ref(run_id、instance_id、dimension、event_id、tick)与 verified: true。每个 audit_ref 都会在 audit-events.jsonl 中解析:事件必须恰好存在一次、在完整身份与 tick 上匹配,且 phase: agent。unmatched_agent_events 必须为 0,并等于没有 join 的 agent 阶段审计事件计算值(每个 agent 侧游戏事件都要有工具调用);unmatched_tool_calls 必须等于没有 join 的工具调用计算值(trace 可以包含构建、写文件等非游戏调用)。
missing-log-detection.jsonl:场景 trace_missing 与 audit_missing,每行 {case, detected: true, exit_nonzero: true, message_ref}。缺日志必须明确报错,绝不能被静默忽略。
7. smoke_fixture_validity —— 通用 smoke、校准与锁定版本(#14)¶
smoke-report.json:suites[] 覆盖 smoke_offline、lab_boot、fake_player_mcp、snapshot_restore、test_mod_load,每项 {name, command, status: "pass", checks >= 1, log_ref}。
fixture-validity.json:input_semantics(s)、stack_positions(非空 [x, y, z] 列表)、output_boundary(非空对象)、void_window_ticks(i >= 1)、end_condition(s)、timeout_s(i >= 1)、hook_overhead_ms(n >= 0,整场会话所有 hook 的总时长,不是每次调用/每 tick)、with_mod_without_mod_consistent(true)。
version-lock.json:components[] 必须包含以下名称,每项带其必需固定值(version 字符串在它本身就是固定值时必需,否则可选):
| 组件 | 必需固定 | 交叉核对对象 |
|---|---|---|
mc-agent |
commit(h40) |
- |
mc-agent-interface-mod |
commit(h40) |
version-pins.json |
mc-agent-bridge |
commit(h40) |
version-pins.json |
minecraft |
version(s) |
fixture-manifest.map.mc_version |
fabric-loader |
version(s) |
- |
jdk |
version(s) |
- |
carpet |
sha256(h64) |
fixture-manifest.mods[carpet] |
test-mod |
sha256(h64) |
test-mod-manifest.sha256 |
fixture-map |
sha256(h64) |
fixture-manifest.map.sha256 |
固定值与其指名的证据不一致即失败(version_lock_conflict)。
evidence-index.json:tool(s)与 entries[],每项对文件为 {path, sha256, bytes}、对目录为 {path, tree_sha256, files, bytes}。它必须固定除自身以外的所有已声明证据;哈希不匹配、重复、文件缺失或漏固定都会让门禁失败。额外的原始证据也可以列入。
8. evidence_integrity —— 包结构、哈希、端口与源存档(工具自算)¶
无证据文件。门禁自行校验 schema 版本与 kind、origin、run.issue、run.child_runs、运行实例(id/端口唯一、两种角色齐全、每个实例声明 dimension)、已声明证据类型与路径、证据索引与重算哈希、审计事件的 run/instance/dimension 来源、以及 run.source_world 前后相等。
端口区间是显式配置而非产品硬编码要求:必须声明 run.allowed_port_ranges(缺失即失败),每个实例与 instance-isolation 端口都必须落在其中,--port-range 只能收窄(超出已声明区间会失败 port_range_conflict)。
带 --source-world(或清单中的路径)时,门禁只读重算源存档哈希并比较。--skip-source-rehash 跳过这项独立检查,因此整个运行会变为 blocked —— 不存在未重算却报 pass 的退出码。
目录树哈希¶
python tools/stage1_gate.py hash-tree <dir> 输出确定性摘要:按相对 POSIX 路径排序,每项含大小与单文件 SHA-256;排除 session.lock、logs、*.log、.DS_Store、Thumbs.db、__pycache__。门禁与 version_lock/fixture-manifest 的世界哈希使用同一算法与排除列表;不要混用不同哈希工具。
集成运行手册(前置项合并之后)¶
父级当前状态、阶段一/阶段二交接命令与循环依赖防护见 Minecart ROM 验收 runbook。
- 从 #15、bridge#6、#16-#19 收集原始证据到一个包里(
scaffold会打印规范结构)。保留原始日志,并一并列入索引。 - 填写
bundle.json:origin: live、run id、child run id、源存档路径与前后 tree 哈希、声明的端口区间,以及落在这些端口上的实例。 - 对所有文件与目录树生成
evidence-index.json。 - 带
--source-world与--report运行check;把报告附到 issue #14。任何blocked或fail都阻止进入阶段二。 - 协调者审阅报告并抽查原始证据后才宣布门禁通过。门禁不能替代这次审阅。
限制¶
- 门禁校验协调者提供的证据,无法证明证据没有被伪造。原始日志应留在仓库/附件链路中并接受抽查。
- "hook 只读"之类的布尔事实与规范事件日志交叉核对,但本工具不会实机重测。
- 各前置项的原始格式必须无损映射到上述 schema;该映射属于协调者审阅范围。
- 门禁通过是阶段二的必要条件而非充分条件:issue 的其它验收点(人工审阅、正确答案证据)仍然必须满足。
- 快照
meta.json无法证明它来自哪个实机实例(instance只是server/client,worldDir可为 null),因此前后快照对只证明状态相等,不证明端点身份;端点证据由绑定的restore-record.json加审计/轨迹来源提供。