Minecart audit test mod¶
tests/mods/minecart-audit/ is an independent, test-side Fabric mod that
records what a machine actually does on the server: the player input attempts,
the server processing the note-block trigger, every tracked chest minecart
leaving its stack, and the ordered inventory captured before the cart is
removed by the void.
It is not the agent's logger, it is not ROM logic, and it places no command blocks. The mod only ever reads the world and appends to its own JSONL file. The agent still has to build its own logger; this mod is the evaluator's independent source of evidence for the same run.
The task it instruments is the Minecart ROM from issue #14; the mod itself is deliberately generic - regions, cart types and identifiers come from a config file, so it can audit any note-block-fed machine and any minecart stack.
Build and install¶
Minecraft Java 26.2 ships unobfuscated classes, so the mod builds with a JDK
and a normal game installation - no Gradle, no remapping. The build script
follows the same pattern as interface-mod/build.py.
python tests/mods/minecart-audit/build.py `
--minecraft-dir "D:\MC\MC_Game\.minecraft" `
--jdk "C:\path\to\jdk-25"
# -> tests/mods/minecart-audit/dist/mc-minecart-audit-0.1.0.jar
--jdk falls back to $MC_AGENT_JAVA, the JDK recorded in a labs/*/lab.json,
$JAVA_HOME, and finally javac on PATH. --minecraft-dir defaults to
D:\MC\MC_Game\.minecraft and can be set with $MC_AGENT_MINECRAFT_DIR.
Provision a lab (or any Fabric server) with the jar and Fabric API:
python tools/lab_server.py provision --name audit --void --fabric-api --carpet `
--mod-jar tests/mods/minecart-audit/dist/mc-minecart-audit-0.1.0.jar
lab_server.copy_if_differentcurrently compares file size only, so a rebuilt jar of the same size would be skipped. Remove the jar fromlabs/<lab>/mods/before re-provisioning until issue #17 lands.
Configuration¶
The mod reads <server run dir>/mc-audit/config.json when the server starts
(-Dmcaudit.config=<path> overrides the location). A missing or malformed
config is loud but harmless: the mod writes mc-audit/status-unconfigured.json,
prints an error on the console, and disables itself. That state is detectable
by the test program, so a run cannot silently pass as "audited".
{
"runId": "rom-2026-09-26-1",
"instanceId": "lab-a",
"dimension": "minecraft:overworld",
"provenance": {
"kind": "source-world | fork | restore",
"reference": "D:/.../Minecart ROM test",
"snapshotId": "...",
"snapshotHash": "..."
},
"outputDir": "mc-audit",
"maxBytes": 67108864,
"sampleIntervalTicks": 1,
"correlationWindowTicks": 2,
"cartTypes": ["minecraft:chest_minecart"],
"agentUuids": ["49e96464-aa17-46b3-b5c6-87574b6b48b4"],
"inputRegions": [
{ "name": "note-block", "from": [0, -59, 0], "to": [0, -59, 0] }
],
"stackRegion": { "from": [2, -60, -1], "to": [3, -59, 1] },
"outputRegion": { "from": [4, -140, -8], "to": [8, -60, 8] }
}
Regions are inclusive block boxes: a point belongs to a region when the block
containing it (floor of each axis) is inside from..to. inputRegions name
the machine inputs; stackRegion is where carts are tracked as fixture
outputs; outputRegion is an optional second out-of-bounds marker. A cart is
tracked when a cartTypes entity loads inside stackRegion in dimension;
it exits the first time it leaves stackRegion or enters outputRegion.
agentUuids is optional: when set, only those players can be classified as
the agent; when empty, any player counts as a player operator (the verifier
still reports the UUID).
Lifecycle and commands¶
/mcaudit (permission level gamemaster; RCON has owner level) is the test
side's control surface. The commands never operate the machine.
| Command | Meaning |
|---|---|
/mcaudit status |
run, instance, phase, sequence, tracked carts, incomplete reasons |
/mcaudit phase init |
fixture initialization (repeats allowed) |
/mcaudit phase restore |
snapshot restore / fork setup |
/mcaudit phase experiment_start |
opens the agent-operation window |
/mcaudit phase experiment_end |
closes it |
/mcaudit mark <label> |
marker for test scripts; cannot be mistaken for an operation |
/mcaudit flush |
force the status file |
/mcaudit end |
writes audit_end and closes the JSONL |
The server also writes audit_ready at startup and audit_end on shutdown. A
server that dies before audit_end leaves an open session; the verifier treats
any open session in the file as incomplete (an explicit
--allow-open-history is required to ignore an earlier crashed session), so
missing evidence is a loud result, not a silent pass. The live smoke includes a
kill/restart negative for exactly this case.
Evidence files¶
mc-audit/
audit-<runId>.jsonl the raw event stream, flushed per event
status-<runId>.json current snapshot (phase, counters, carts, hooks)
latest.json pointer to the current run's files
status-unconfigured.json written when the config is missing/broken
config.json the config the mod consumed
Every event carries seq (strictly increasing in the file, seeded across
server restarts), tick (the real server tick, which a restart resets),
wall, run, inst, session, phase and type. The gate exporter keeps
the real tick and the append sequence, plus the session and wall clock, so an
ordering decision names its clock scope instead of renumbering ticks. A restart appends a new session_start; one runId can
therefore span several server sessions (needed for mod redeploys and snapshot
restores) without losing ordering. maxBytes caps the whole file, not one
session: the byte counter is seeded from the existing file size, so a run
restarted several times cannot reset the budget. Overflow sets truncated and
the verifier treats the run as incomplete.
hook_overhead_ms in the gate manifest is the total session hook time
(sum of every hook's totalNanos divided by 1e6); the per-hook
calls/total/max/avg/errors live in the same manifest under
hook_overhead_by_hook (audit_end.hooks), so the figure cannot be read as
per-call or per-tick. The mixed-trigger regression
(scenario_attack_then_use) punches the note block and then uses it within
the correlation window: the use must be the only input_processed, and a
punch's unscheduled play must never be consumed by a later use callback.
Key event types:
| Type | Meaning |
|---|---|
input_attempt |
player interaction with a note block, with operator UUID, hand, item, result and whether the position is a configured input |
input_request |
NoteBlock.playNote, the actual request; carries the triggering entity (player, entity, or redstone_or_environment) |
input_processed |
The server processing the note; links requestSeq/attemptSeq, records note/instrument and agentOp. path names the vanilla path: triggerEvent when the block event is scheduled, or playNote when the note block's instrument does not work above the block and the block above is not air, so the play itself is the processing (the fixture's harp note block with leaves above uses playNote). |
cart_tracked |
a configured cart loaded inside the stack region, with ordered inventory and origin phase |
cart_sample |
periodic position/motion/inventory sample, deduplicated per cart and tick |
cart_exit |
first time the cart left the stack region / entered the output region (via sample or removal) |
cart_remove |
removal with reason (DISCARDED for the void), position, motion and the ordered inventory captured before the container drops it |
cart_inventory_change |
observed inventory mutation while tracked |
cart_teleport, cart_reload, cart_reappeared |
alternative-behaviour evidence (teleports, chunk reloads, UUID reuse) |
audit_incomplete |
a reason the audit cannot be trusted (missing correlation, hook error, …) |
audit_end |
per-session close with counts, cart summary and hook overhead |
The minecart hooks deliberately inject at Entity.remove and
AbstractMinecartContainer.remove before content destruction, because the
vanilla fast path drops contents first; end-of-tick sampling alone would lose
a fast fall. The note-block hooks distinguish the player attempts
(useItemOn/useWithoutItem, and 26.2's attack punch, recorded with
path: "attack" so a punch cannot poison the chain), the request that carries
the triggering entity (playNote) and the processing (triggerEvent);
right-clicks, sounds and command feedback are never treated as machine
operation.
Only useItemOn/useWithoutItem count as a calibrated machine operation.
26.2's attack punch calls playNote but never changes the note state, so it
cannot trigger the observer/piston machine through the calibrated path; it is
still recorded as an attempt (path: "attack") so the chain stays intact, but
machine_operated ignores it. The live smoke has an attack-only negative that
forces the fake player to survival, punches the note block, and shows the
block still present, the cart unmoved and the verifier fail.
A request or attempt is credited to exactly one processing event, in the same
order the server processes block events: requests are consumed FIFO, and the
attempt is the one whose recorded requestSeq equals the processing request
(not merely a neighbour in the time window). No matching attempt is a
fail-closed gap. The verifier fails a run when one
requestSeq/attemptSeq is referenced by more than one target
input_processed, when an attempt carries a different requestSeq than the
processing it is credited to, or when the attempt/request/processing operator
UUIDs disagree. Only configured input positions feed the correlation maps, and
stale entries are pruned each tick.
All hooks use @Inject only: no redirects, no overwrites, no mutation, no
cancellation. Per-hook call counts, total nanoseconds and max nanoseconds are
reported in the status file and in audit_end so observation cost is
measurable.
Reading the evidence¶
tools/minecart_audit.py check reads one JSONL file and returns a verdict:
python tools/minecart_audit.py check --log labs/audit/mc-audit/audit-<run>.jsonl `
--status labs/audit/mc-audit/status-<run>.json --json
python tools/minecart_audit.py selftest # synthetic positive/negative cases
Exit codes: 0 pass, 1 fail, 2 incomplete/unreadable. The report answers
the acceptance questions directly:
machine_operated- at least one target input was actually processed on the server and correlated to a player request and use attempt, inside the experiment phase;input_chain- every target processing event has a preceding request and, for agent operations, an attempt inside the correlation window; otherwise the run fails with "insufficient ordering evidence";transient_outputs- every cart removed during the experiment has exit evidence and an ordered inventory captured before destruction; duplicate exits and duplicate removals fail;ordering- file order equals occurrence order, exits are strictly increasing, same-tick outputs need distinct sequence numbers;completeness-audit_endsays complete, noaudit_incomplete, no truncation, and the session model matches the status file.
Negative cases are covered both by the verifier selftest (no operation, wrong position, marker-only, answer-only, missing request, environment-only, duplicate output, mixed instance, truncation, missing end) and by the live smoke script below.
Live smoke¶
python tests/mods/minecart-audit/live_smoke.py
The script builds the mod, raises disposable void labs on the issue's ports (27180-27189), and runs:
- a positive run across a server restart (two sessions, one agent operation, one cart captured before the void);
no interaction,wrong position,marker only,answer only,attack onlyandredstone onlynegative runs - all must fail the verifier; the attack run forces the fake player to survival so the punch is recorded without destroying the note block, and shows the block and cart unmoved;- a killed-server run (
stop --force, restart, close the second session) - the verifier must reportincompletefor the abandoned session; - a lab with the mod but no config - must report "not configured";
- a control lab without the mod running the same script, compared on the observable results (the note cycles, the cart is gone) to show the hooks do not change fixture behaviour;
- a read-only copy of the source save loading with the mod installed; the save
is discovered under
--mc-dir's version instances (or--source-save), and a missing save fails the run instead of silently passing subsections; - cross-instance log separation.
Evidence lands in labs/rom18-evidence/ (summary.json, the raw logs, the
verifier reports and the console logs); the directory is git-ignored, so it is
a local artifact, not repository content. Prior attempts are kept as
<evidence>.attempt-<timestamp>/ instead of being deleted.
Combined bridge / interface-mod verification¶
python tests/mods/minecart-audit/bridge_restore_smoke.py
This is the merged-stack run: a clean bridge source (default the
codex/rom13-bridge6 worktree whose head is pinned in the script, i.e. the
guarded restore plus player.view), the clean interface mod 0.6.0
(SHA-256 45f12e16...404f), and the audit mod installed as a required
test mod (--test-mod, so a missing audit mod refuses to start) in both a
source-audit lab and an experiment lab.
The stages, all live on dedicated 26.2 servers:
- a generic three-cart stacked chest-minecart fixture (distinct items) is
forked under the bridge's guarded
fork(frozen, snapshot, world copy); - the experiment lab loads the fork world and the bridge runs the guarded
restorewithexpect_world_dir+replace_existing, thenverifycompares full state (order hash, counts, dimension, positions, velocities, NBT/items) -ok: trueis required before the audit stage continues; - the audit log's
cart_trackedinventories for the restored carts are cross-checked item-by-item against the bridge's post-restore snapshot (same UUIDs, same ordered items); player/player.viewis called on the restored destination and must returnfound, dimension and a normalised eye/direction;- a generic note-block machine in the experiment lab produces the canonical input -> processing -> cart-output chain while the restored copy is still tracked;
- the #17 smoke mod (
examples/smoke-mod, built withtools/build_mod.pyagainst the lab) is deployed alongside the audit and interface mods;mcagent-smoke status/sampleandlab_server.py verify --require-vantagemust all succeed. That smoke mod proves deployment capability and coexistence; it is not the agent's logger and is never treated as one; - both audit sessions are closed with
/mcaudit endand the labs are stopped before the raw logs are copied, so the retained source-child log is complete instead of an open session.
Evidence lands in labs/rom18-b6-evidence/ (summary.json, per-stage JSON
including the recorded bridge calls, the restored-audit crosscheck, the
coexistence output, raw audit logs and console logs).
Stage-one gate adapter¶
tools/stage1_gate.py from #22 consumes a canonical bundle, not the raw
JSONL. The shared lossless adapter tools/stage1_evidence.py (from #28)
projects the raw formats for the full bundle; the module-level
tools/minecart_audit.py export maps the live evidence into the gate's
independent_test_mod artifacts: test-mod-manifest.json,
audit-events.jsonl, negative-cases.jsonl and audit-lifecycle.json. The
mapping keeps the raw event type, server tick and sequence in each row's
detail; the canonical (tick, seq) is a per-identity monotonic counter
because a server restart resets the game tick while the audit sequence keeps
going.
The export binds evidence instead of relabelling it: an event whose own
run/inst/dimension does not match the declared identity is refused, and a
log with an open session or without audit_end is refused. Negative cases
must have verifier verdict exactly fail (an incomplete/crashed log is not a
rejected negative), the gate asserts each row's verifier_verdict == "fail",
and environment-triggered processing rows carry an actor_provenance instead
of a bare null actor. The manifest's fixture_behavior_unchanged and
agent_mod_coexists claims are derived from the live summaries and the
adapter refuses to export when either is false.
python tests/mods/minecart-audit/stage1_evidence.py
reads the two live evidence sets, exports the artifacts, assembles a partial
live bundle with the real run/instance/port/source-world fields, and runs the
gate on it. The historical partial-bundle run reports independent_test_mod: pass with the
assertions input -> processing -> output chain and negative cases rejected
while the overall bundle stays blocked on the other prerequisites' missing
artifacts. That is deliberately not a gate pass: #15's fixture and the other
checks are owned by their own work. The later combined gate passed 8/8 and
the full issue #13 chain is accepted; see the acceptance runbook.
Limitations¶
- This is instrumentation, not an adversarial sandbox. It does not promise to resist same-privilege malicious code; it records evidence for review.
- The actual ROM map and deterministic fixture initialization were delivered by #15; the historical component runs here uses generic void-lab fixtures. The combined bridge/restore run does prove the restored-copy audit coverage, but it is not fixture integration.
- Joining tool calls to
input_*events is #19's audit harness: the mod records operator UUID, tick and sequence precisely so the join is possible, but it does not read the tool trajectory itself. - Same-tick multi-output ordering is enforced and unit-tested in the verifier; the live fixtures produced one cart per run, so it is covered synthetically rather than by a live same-tick pair.