action_key sweep; drop cosmetic heap-trash log redactionStatus: fixed
Decision (2026-08-02, triage with maintainer): The audit's section A is approved in full (including the low-value tail), section B is resolved here rather than deferred, and the keying policy is refined from ticket 46's two-halves rule into three cases:
1. Public outcome → code key, same in both trees. When the chosen card's
identity is revealed by resolving the action (played operations, runner
installs, cards landing in the public heap, revealed search picks), key by
card_code. No observer arm needed — matches the existing
play_event/install_program precedent. Index keys here would be strictly
worse: different cards at one index share an edge, one card at different
indices splits.
2. Hidden outcome → index key for the actor + redacted observer arm (the
ticket-43/46 pattern; the four Corp HQ choices and facedown Archives stay
exactly as shipped).
3. Public position → slot/id key, same in both trees (the
advance_card/rez_card precedent; a Runner-root determinisation may swap
which card sits in an unrezzed slot — accepted, consistent, not novel).
| Action | Key | Notes |
|---|---|---|
| PlayOperation | play_operation:{card_code} | Schema: add card_code: String with #[serde(default)], populate in legal.rs — the audit's open question, settled by case 1 |
| PantographInstall | pantograph_install:{card_code} | field exists |
| IlluminationInstall | illumination_install:{card_code} | field exists |
| MethuselahTrash | methuselah_trash:{card_code} | field exists; log names the card and the heap is public — see redaction section |
| CarnivoreTrash | carnivore_trash:{card_code} | Flips ticket 43's index key and removes ticket 46's Corp observer arm — see redaction section |
| ChooseSearchCard | choose_search_card:{card_code} | Schema: add card_code (serde default), populate in legal.rs from the decision's candidate list — resolves audit section B, see below |
aircheck_run:{target} (the priority row — the search currently cannot
distinguish which server Aircheck runs at), above_the_law:{index},
retribution:{slot}:{index}, seamless_target:{server_index}:{slot_index},
free_rez:{server_key}:{slot_index},
use_anoetic_void:{server_index}:{slot_index}, knickknack_trash:{slot}:{index},
sipa_swap:{target_ice_id}, tao_first_ice:{ice_id}, tao_swap:{ice_id},
use_botulus:{program_index}:{sub_index}, use_resource:{slot}:{resource_index},
use_leech:{program_index}, use_carnivore:{hardware_index},
pump_breaker:{breaker_index}.
Same key both trees, no observer arms. The low-value tail (use_resource …
pump_breaker) is deliberately included: same one-line fix, and it leaves the
action_key fallback holding only payloadless actions — an invariant worth
having, rather than a curated exception list.
The runner heap is serialized ungated with full card identity for both sides
(serialize.py:343, via card_view) — the heap is a public zone. So the two
corp-side log anonymizations for cards that land there hide nothing from an
attentive observer and are dropped:
serialize.py "Carnivore: Runner trashes … from the
grip") — with it gone, Carnivore's outcome is public and its key follows
case 1 in both trees; remove observed_action_key's
(Corp, CarnivoreTrash) arm from ticket 46 (that ticket's text explicitly
anticipated the observer key following any redaction-rule change; a
superseding comment is appended there).
observed_action_key already gives a Corp observer the full discard key, so
the log simply catches up to the keys.
The genuinely-hidden rules stay: Corp installs/draws/advances, Corp discards (facedown Archives), the HQ-trash and Sprint anonymizations, and unstolen R&D accesses.
ChooseSearchCard)
The audit's three objections to index keys all dissolve under a code key:
opponent-searcher determinisation fan-out is the accepted
install_card:{code} aliasing; a code key carries no positional information,
so there is no deck-order leak (the actor legitimately knows the candidates —
the search reveals them, pending_stack_search is shown to the decider); and
variable list length is irrelevant without positions. Observer half: the
chosen card is publicly revealed on resolution (engine.rs:2677) while
unchosen candidates stay hidden (serialize.py:282 blanks them for the
non-decider) — and the key only ever names the chosen card, so both trees
share it. InstallFoundCard/GripFoundCard need nothing: unit variants whose
only defect was the aggregated parent edge.
AccessCard/TrashCard (access-queue indices into determinisation-shuffled
zones) and every payloadless action stay on the type-only fallback.
action_key implements both tables; the two schema additions land with
#[serde(default)] so the wire format stays backward-compatible;
legal.rs populates the new codes.
observed_action_key loses the Carnivore arm; no new observer arms (thissweep adds none by construction).
serialize.py loses the two cosmetic rules listed above and no others.action_key /
observed_action_key, replacing the two-halves phrasing, with the
fallback-is-payloadless-only invariant stated.
including: same-code duplicates sharing an edge for one code-keyed action,
and an aircheck_run two-server case.
this ticket's comment records the flip once implemented.
cargo fmt, cargo clippy --workspace -- -D warnings,
cargo test --workspace (from rust/), and uv run pytest -q green.
2026-08-02: Filed from ticket 43's "audit, don't fix" clause. Marked
needs-triage: section A is mostly mechanical but PlayOperation has an
open schema question (index vs. card code) and the Corp board-slot rows want
a maintainer's confirmation; section B is an unresolved soundness question,
not a task.
2026-08-02: Ticket 46 (triaged, ready-for-agent) established a policy that
binds this sweep: every per-target key has two halves — the actor's tree
gets the full action_key; the blind observer's tree gets what the redacted
log reveals, via mcts2::observed_action_key.
2026-08-02: Triaged with the maintainer (grilling session). Section A
approved in full including the low-value tail; the two-halves policy refined
to the three-case policy above; PlayOperation schema question settled
(add card_code); section B resolved with code keys instead of a separate
design ticket; maintainer additionally decided the heap-trash log
anonymizations are cosmetic (public heap) and should be dropped, which flips
Carnivore to a public-outcome code key in both trees, superseding ticket 46's
Carnivore observer arm. Rewritten as a single implementation ticket; status
flipped to ready-for-agent.
2026-08-02: Implemented as specified. Both tables land in
ai::mcts::action_key: six code keys (case 1) and fifteen slot/id keys
(case 3), with PlayOperation.card_code and ChooseSearchCard.card_code
added behind #[serde(default)] and populated in legal.rs (the latter from
the StackSearch decision's candidate list, so stack_search_options now
takes the cards rather than just their count). CarnivoreTrash flipped from
ticket 43's index key to carnivore_trash:{card_code} and ticket 46's
(Corp, CarnivoreTrash) arm is gone from observed_action_key; no observer
arms were added, as the policy predicts. serialize.py lost exactly the two
cosmetic corp-side heap rules ("Runner discards …" and the Carnivore grip
trash) and nothing else.
AccessCard/TrashCard got explicit type-only arms rather than being left
to fall through — that is the only way to make the "fallback holds only
payloadless actions" invariant literally true, since those two carry an index
they deliberately ignore. Both action_key and observed_action_key now
document the three-case policy (and the mcts2 module header's two-halves
bullet was rewritten to match), with the invariant stated on action_key.
Tests: golden cases for every row of both tables plus a shared-with-observer
sweep over all 21, same-code search picks sharing an edge, the aircheck_run
two-server case, Carnivore's flip to a code key in both trees, and the
access-queue aggregation. The stale use_leech/seamless_target type-only
rows left action_key_golden_cases, and
blind_corp_aggregates_carnivore_grip_target_keys was replaced. cargo fmt
and cargo clippy --workspace -- -D warnings clean; cargo test --workspace
155 passed (150 lib, up from 144, + 5 integration); uv run pytest -q 134
passed after make rust-py.