Auditable cold-start runs¶
A cold-start run is one attempt by a real agent, with a fresh context, to research and operate a Minecraft world through the Toolkit. The goal of this page is narrower than the science: define the environment contract, the run entry, the trajectory sink and the judgement, so that a later review can tell what actually happened without trusting the agent's final message.
This is the plan tracked by mc-agent#19. It adds no model client and no session backend to mc-agent: the harness owns the model, the run directory owns the evidence, and these tools only check and record facts.
No pre-written solution
The task input for the Minecart ROM cold start is
examples/coldstart/task-minecart-rom.md.
It states the task and the requirement to really run the machine; it does
not contain an answer, a solution script or a prior trajectory. Do not add
one before the cold start.
1. Fix the harness first¶
The first cold start uses a user-launched harness with ordinary development
tools (a terminal, file read/write, source and dependency fetching, a JDK to
build, and the bridge JSON CLI or MCP). The restricted Hermes webhook route is
not that harness: its per-route toolsets deliberately expose the Toolkit MCP
server only, with no terminal or file tools, so an agent on that route cannot
write, compile and install its own logger. Webhook delivery is a later,
optional path; see Unattended Hermes.
A harness configuration is a small JSON file; the reference one is
examples/coldstart/harness-pi.json.
It names the harness command, how to read its version, the model and provider
strings for the record, the shell probe, the source endpoints to fetch, the
minimum JDK, the bridge command and the reserved ports. Machine-specific
values (JDK path, bridge executable) can be supplied without editing the file:
python tools/harness_preflight.py run \
--config examples/coldstart/harness-pi.json \
--out labs/coldstart/preflight \
--run-dir labs/coldstart/run-01 \
--set "build.java=C:\path\to\jdk-25\bin\java.exe" \
--set "bridge.command=C:\path\to\.venv\Scripts\mc-bridge.exe"
The configuration is fixed before the agent starts. Changing it afterwards invalidates the run: the preflight report records the exact values it checked.
2. The environment contract: harness_preflight.py¶
tools/harness_preflight.py answers one question: can this harness reach
everything the agent will need, before a model is spent on discovering it?
Each check writes its command, exit code, transcript and hash under
<out>/probes/; the summary is <out>/preflight.json and preflight.md.
| Check | Proves | Notes |
|---|---|---|
harness_identity |
the configured harness command answers its version probe | model/provider strings are recorded, not secrets |
terminal |
the harness can run a shell command | shell from the config |
filesystem |
write, read-back and hash a nonce file | exact bytes preserved |
ports |
the reserved API/mod ports are free and inside the range | default range 27190–27199 |
source_fetch |
every configured source/dependency endpoint answers (Fabric meta, Modrinth) | optional build.pip_packages adds a pip probe; one failing endpoint fails the check |
build_install |
a JDK >= build.min_java compiles and packages a probe jar, and the installed bytes hash-match |
Java 25 is required for 26.2; a PATH JDK 21 fails this check on purpose |
bridge_cli |
mc-bridge --help runs |
the JSON CLI is the required call surface |
bridge_mcp |
mc-bridge mcp --help runs |
optional; skipped when the mcp extra is absent, because the JSON CLI is an accepted alternative |
bridge_smoke |
tools/smoke_offline.py completes with the echo backend |
real bridge + loop processes against a fake mod on the reserved ports |
lab_management |
lab_server.py list works; with a lab configured and start enabled, a real headless Fabric server is provisioned, pinned to the reserved ports, started, answers RCON list, and stops |
records lab.json and the console hash; a disabled start is SKIP, so the audit's preflight cross-check rejects it |
--checks/--skip select a subset; SKIP never counts as PASS. Exit code 0
means overall PASS, 1 means at least one selected check failed. A probe that
crashes is a failed check, not a crashed preflight.
lab_management starts a real server, so it is the slow check. The reference
config runs it. Run the fast subset during development:
python tools/harness_preflight.py list
python tools/harness_preflight.py run --config <config> --out <dir> \
--checks harness_identity,terminal,filesystem,ports
When --run-dir is given, the report is also appended to the run trajectory as
a mark record, so the audit can see which environment the run started in.
3. Open a run: run_trace.py init¶
python tools/run_trace.py init \
--run-dir labs/coldstart/run-01 \
--run-id run-01 \
--task-file examples/coldstart/task-minecart-rom.md \
--harness pi --model <model> --provider <provider> --harness-version <version> \
--note "cold start; fresh session"
init writes:
labs/coldstart/run-01/
run.json run manifest: harness, model, task hash, repo commit, imports, artifacts
task.md the exact task bytes handed to the agent (never the answer)
visibility.json every document/skill visible to the agent, with path + sha256
visibility/files/ byte copies of those files (small files)
trajectory.jsonl append-only canonical records
evidence.json declarations of external evidence (filled in before the audit)
preflight/ optional harness_preflight.py output
audit/ run_audit.py output
Clean-context rules. The session must not carry the design discussion of
this repository, a standard answer, a previously successful script or a
solution trajectory. The task is the task file above; the visible documents and
skills are recorded by visibility.json; nothing else is supplied. The world's
full memory data is not hidden: the protocol records what is visible and
never filters inventory, NBT or tick order. The evidence snapshot is an
inventory, not a sandbox.
Import the harness's own trajectory. A pi run records tool calls and
results in its session JSONL ($PI_SESSION_FILE). Copy it into the run and
translate it into canonical records when the session ends:
python tools/run_trace.py import-pi --run-dir labs/coldstart/run-01 \
--session "$PI_SESSION_FILE" --phase agent
The raw session is copied to imports/ and hashed. Other harnesses append the
same records directly through record/call/result (see below) or get their
own importer; the canonical schema does not depend on pi.
Open the run before the session starts
init records the run creation time. A session imported later must have
started after it, otherwise validate reports the timestamps out of order.
Create the run first, then launch the fresh harness session.
4. The trajectory sink¶
trajectory.jsonl is append-only, one JSON object per line, flushed to disk on
every append. seq is assigned by the recorder; at is RFC 3339 UTC.
| Record | Required fields | Meaning |
|---|---|---|
header |
schema, run_id |
written by init |
call |
call_id, tool, arguments |
one tool invocation: file, terminal, bridge, build, script, MCP call |
result |
call_id, status (ok/error/open) |
the invocation's return or error; duration_ms optional |
phase |
phase, actor |
a lifecycle transition |
mark |
name |
an annotating fact (preflight, artifact, fixture, …) |
Phases are prepare (test-side setup), restore (copy restoration), agent
(the agent's own work), audit (post-run review). Actors are operator,
test, agent, restore, harness, reviewer. This is how "test
preparation", "agent operation" and "copy restoration" stay distinguishable in
one file.
Convenience commands:
python tools/run_trace.py call --run-dir <run> --call-id c1 --tool bash \
--arguments '{"command": "javac LoggerMod.java"}' --phase agent --actor agent
python tools/run_trace.py result --run-dir <run> --call-id c1 --status ok --result '{"text": "compiled"}'
python tools/run_trace.py phase --run-dir <run> --phase restore --actor restore --instance lab-a
python tools/run_trace.py artifact --run-dir <run> --path build/logger.jar --label "agent logger jar"
python tools/run_trace.py review --run-dir <run> --id machine_operated.causality \
--status resolved --by "reviewer name" --note "watched the replay" \
--evidence trajectory:c2-noteblock --evidence testmod:seq3
python tools/run_trace.py validate --run-dir <run>
artifact copies self-written code and build outputs into artifacts/ and
records their hashes in run.json. review appends an explicit review
resolution (section 5). validate checks the structure: unique call ids, one
result per call, monotonic seq and timestamps, known phases,
task/visibility hashes, import and artifact hashes, declared evidence hashes,
and well-formed review records. Missing a result is an error, not a warning.
5. Evidence declarations¶
External artifacts stay where they were produced (the agent's logger output,
the test mod's file), and evidence.json declares them with a path relative to
the run and, when available, a sha256 that the audit re-checks:
{
"agent_logger": {
"path": "agent/logger.jsonl", // raw output written by the agent's own logger
"sha256": "...", // re-checked; a change is INFRA_ERROR
"normalized": "agent/logger.norm.jsonl" // optional canonical view
},
"test_mod": { "path": "test/testmod.jsonl", "sha256": "..." },
"answer": { "path": "answer.json", "sha256": "..." },
"oracle": { "path": "oracle.json", "sha256": "..." },
"fixture": { "path": "fixture/ready.json", "sha256": "..." },
"restore": { "path": "restore/verify.json", "sha256": "..." },
"preflight":{ "path": "preflight/preflight.json", "sha256": "..." },
"review": { "path": "reviews.jsonl", "sha256": "..." } // default location if omitted
}
Test-side events (the independent oracle side)¶
The independent test mod (mc-agent#18) writes a JSONL file. The audit consumes this projection of it; the mod may use its own field names for the aliases in brackets:
| Event | Fields | Meaning |
|---|---|---|
instance_ready |
instance, dimension, tick |
fixture initialised |
restore_started / restore_finished |
instance, tick |
copy restoration bookkeeping |
input_attempt |
actor, tick |
something was tried (right-click callback, marker, command ack) |
input_processed |
actor, tick |
the server actually processed the machine input |
cart_ejected |
uuid, tick |
a cart left the stack |
cart_removed |
uuid, tick, reason |
a cart was removed (for example the void) |
end |
tick |
experiment finished |
An input_attempt is never accepted as operation; machine_operated requires
input_processed. #18 owns the real format; if its names differ, map them in
an adapter. The raw file is retained and hashed either way.
Every event may carry run_id ([alias] runId); when present it must match the
run manifest. An event from another run is INFRA_ERROR, not evidence.
Agent logger events (the agent side)¶
The agent chooses how to log. For the audit, the output (or a verified normalized view of it) contains:
| Event | Fields | Meaning |
|---|---|---|
logger_armed |
at, tick, instance |
the agent's logger is running, before the machine is actuated |
cart_observed |
uuid, at, tick, position, items |
one ejected cart captured before removal |
logger_flushed / logger_error |
at |
output durability / failure |
If a normalized view is declared, the audit checks the raw file against the
declaration's sha256, loads the normalized file against its own
normalized_sha256 (never the raw hash), and requires every normalized
record to carry source_sha256 equal to the raw logger's hash; a missing or
stale source_sha256 is INFRA_ERROR. The raw output is never replaced by the
normalized view.
Explicit review resolutions¶
Some facts are semantic: they cannot be decided by parsing alone, and the
protocol refuses to guess. The audit lists such facts as required review
entries, and the run must carry an explicit resolution before the flag can be
PASS. Resolutions live in reviews.jsonl (append-only, latest entry per id
wins) and are written with run_trace.py review:
{
"id": "machine_operated.causality", // the required-review id from audit.json
"status": "resolved", // resolved | rejected | pending
"by": "reviewer name", // resolved entries must carry by + at
"at": "2026-09-26T00:00:00Z",
"note": "watched the replay; the traced command is the one the test mod processed",
"evidence": ["trajectory:c2-noteblock", "testmod:seq3"]
}
- no entry or
pendingkeeps the flag PENDING; resolvedmust carryby, a parseableatand a non-empty evidence list; every ref must resolve against the run (atrajectory:<call_id>that exists, atestmod:/logger:seqNthat exists, or afixture:/restore:/preflight:field or scalar value present in the declared payload). A malformed entry (missing reviewer, invalid timestamp, empty/blank evidence) isINFRA_ERROR; a well-formed entry whose refs do not resolve keeps the flag PENDING and is reported underreview_problems. When all refs resolve, the flag's mechanical outcome is restored - a mechanically failed flag is never flipped to PASS;rejectedfails the flag (AGENT_FAIL), because the reviewer judged the evidence insufficient.
The required-review ids in this revision are machine_operated.causality,
machine_operated.ordering, logger_armed_before_activation.running,
logger_armed_before_activation.ordering, transient_outputs_captured.window,
transient_outputs_captured.inventory, agent_read_log.linkage and
answer_correct.oracle_independence. A run whose mechanical checks pass but
whose required reviews are unresolved is PENDING, never PASS.
Correlation and provenance¶
Before any flag is computed, the audit correlates the run's identities and fails closed on mismatches:
- every record may carry
run_id; a mismatch with the run manifest isINFRA_ERROR, in the test-mod/agent-logger files and in the trajectory; imports/andartifacts/files are re-hashed againstrun.json, so a tampered raw session or build output isINFRA_ERROR;- test-mod events must not mix instances or dimensions, and agent logger events
must name the same experiment instance/dimension as the test mod - the record
fields are aliases (
instance/instance_id,dimension/dim); - a declared preflight must match the run manifest: harness name/model/provider,
the repo commit, a passing
portscheck and a passing livelab_managementcheck that actually started a server; a SKIP or a different environment isINFRA_ERROR; - oracle
evidence_refsmust resolve against declared test-mod/fixture/restore evidence: atestmod:seqNref must name an existing record, and afixture:/restore:/preflight:suffix must name a field or scalar value present in the declared payload. Agent-side refs (trajectory:,logger:,answer) never count as independent, and an unresolved ref is PENDING while no independent ref fails the flag. The answer must cover exactly the oracle's carts: an omitted cart isAGENT_FAIL.
6. What the audit decides: run_audit.py¶
python tools/run_audit.py run --run-dir labs/coldstart/run-01
It computes five flags, each with evidence references into the records above,
and writes audit/audit.json plus audit/audit.md. Exit codes: 0 PASS,
1 FAIL, 2 PENDING.
| Flag | PASS requires | Required review (id) | Evidence |
|---|---|---|---|
machine_operated |
>= 1 input_processed event; >= 1 agent call in the agent phase ordered before the first processing event by time/tick/sequence |
machine_operated.causality; machine_operated.ordering when no shared ordering layer exists |
testmod:seq…, trajectory:<call_id> |
logger_armed_before_activation |
a logger_armed record ordered before the first input_processed event (time, then tick, then same-tick sequence; same instance/dimension); an agent call showing the logger being written/built/started |
logger_armed_before_activation.running; .ordering when no shared ordering layer exists |
logger:seq…, testmod:seq…, trajectory:<call_id> |
transient_outputs_captured |
every cart_ejected uuid has a cart_observed record ordered after ejection and before removal (same instance/dimension); each capture carries an items list |
.window when there is no removal evidence or no shared ordering layer; .inventory when a capture has no items list |
testmod:seq…, logger:seq… |
agent_read_log |
an agent call after the last capture that both names the logger and returns captured observation content (a cart uuid plus its items, or the full ordered item list), with a plausible read/exec call | agent_read_log.linkage when the linkage is ambiguous (filename echoed, uuid alone, content without a logger reference, or unorderable) |
trajectory:<call_id>, logger:seq… |
answer_correct |
the answer matches a verified oracle cart by cart, covers every oracle cart, and the oracle's refs resolve to independent game-side evidence | answer_correct.oracle_independence |
oracle:<ref>, oracle |
The audit is deliberately split into mechanical and semantic parts. Presence,
ordering, hashes and item comparison are mechanical. Causality ("this traced
call is what the test mod processed"), whether the armed logger was really
loaded, whether a capture window is proven, the linkage of a reported read and
the oracle's independence are required reviews: the audit reports them with
stable ids, and apply_reviews keeps the flag PENDING until reviews.jsonl
contains an explicit resolved entry (or fails it when the entry is
rejected). A needs_review note without a review id is informational only;
it never decides PASS. The first run is allowed to need human semantic review;
no second model is required. audit.md lists every required review and whether
it is resolved.
Mechanical ordering never guesses. Times decide first, then ticks. The
intra-tick seq tie-break is only used inside one stream: a trajectory seq
is the recorder counter while a test-mod/logger seq is per-file, so
cross-stream records that share a tick are unknown and become a required
review unless a common clock/sequence provenance orders them. A capture that
sorts before the cart was ejected (for example a pre-activation inventory
read) fails outright, and an input_attempt alone never proves operation.
Missing critical records fail closed. With no test_mod file, no
input_processed, no logger_armed, no capture, no read or no oracle, the
corresponding flag fails or stays pending - there is no path to PASS.
The oracle rule. answer_correct requires an oracle whose status is
verified, whose evidence_refs resolve against declared test-mod / fixture /
restore evidence, and that carries at least one independent game-side ref. An
oracle copied from the agent's answer fails; an unresolved ref is PENDING. The
answer must cover exactly the oracle's carts - a partial answer that omits a
cart is AGENT_FAIL even when the remaining carts match. When the fixture is
new or unverified, the oracle stays pending; the audit reports PENDING and
never claims correctness early.
7. Failure classification¶
| Class | Meaning | Examples |
|---|---|---|
AGENT_FAIL |
evidence exists and the agent did not operate, capture, read or answer correctly | only input_attempt, late logger, missed cart, answer mismatch |
FIXTURE_INVALID |
initialisation or restore was not faithful | fixture report invalid, restore status not ok |
INFRA_ERROR |
the tools or the audit itself are unavailable or inconsistent | broken trajectory, missing/hashed-mismatched evidence, failed preflight |
Precedence is INFRA_ERROR, then FIXTURE_INVALID, then AGENT_FAIL. PENDING is not a failure: it means the run cannot be judged yet (pending oracle, the fixture/restore evidence has not been declared, or a required review is unresolved). A run can be PENDING at stage 19 and become decidable when the prerequisite work lands.
8. Relationship to the prerequisites¶
This page defines the contract; the live cold start happens after the stage-one gate is fully green. The other workstreams own their ends:
| Piece | Owner | What the audit reads from it |
|---|---|---|
| deterministic fixture + ready snapshot | mc-agent#15 | fixture evidence, instance_ready |
| fake-player identity and server-vantage context | mc-agent#16 | the actor identity on test-mod events |
| mod deployment and lab instance identity | mc-agent#17 | restore/reload evidence, lab ports and dirs |
| independent test mod | mc-agent#18 | test_mod events |
| faithful snapshot restore | mc-agent-bridge#6 | restore evidence |
Integration acceptance (a real cold start judged PASS) must wait for all of
them. This document and its tooling can be developed and tested in parallel,
which is what the synthetic fixtures under tools/tests/ do. The parent-level
stage handoff and current status are in the
Minecart ROM acceptance runbook.
9. Verify the tooling without Minecraft¶
python -m unittest discover -s tools/tests -v
python tools/harness_preflight.py run --config <config> --out <dir> --checks terminal,filesystem
python tools/run_trace.py validate --run-dir labs/coldstart/run-01
python tools/run_audit.py run --run-dir labs/coldstart/run-01 --json
The unit tests build valid and deliberately broken run directories (attempts
without processing, late logger, capture after removal, test-side-only
observation, pending oracle, invalid fixture, tampered hashes) and assert the
flag and classification outcomes. They never launch Minecraft and never touch
the network; the live proofs are the preflight probes and the separate
stage-one smoke checks. The same suite runs in CI
(.github/workflows/tools.yml) on every change under tools/.