← all tickets

46 — MCTS2: redact private card-choice target keys for the non-acting observer

Status: fixed

Decision (2026-08-02, triage with maintainer): Approved. Ticket 43's "same key in both mcts2 trees" call is superseded — it was made on the incorrect premise that no per-observer keying machinery existed. observed_action_key (rust/netrunner-core/src/ai/mcts2.rs:40) already exists, already redacts InstallCard/DrawCard/DiscardCard per observer, and its documented contract is to mirror server/serialize.py's log redaction. The six per-target keys from ticket 43 currently fall through it unredacted, letting the blind observer's tree condition on an opponent's unobservable card choice.

What to implement

Keep the full per-target action_key values in the acting player's tree. Extend observed_action_key with arms for the blind observer, derived directly from the game's own redaction rules (serialize.py:30–77) and log lines:

| Action | Blind observer | Observed key | |---|---|---| | SprintReturn | Runner | sprint_return | | LongevityTrash | Runner | longevity_trash | | HanseiTrash | Runner | hansei_trash | | AnoeticTrash | Runner | anoetic_trash | | CarnivoreTrash | Corp | carnivore_trash | | ArchivesRecur { source: "archives_fd" } | Runner | archives_recur:archives_fd | | ArchivesRecur { source: "archives_fu" } | Runner | archives_recur:archives_fu:{card_title} |

Design points settled at triage:

public: this codebase's own redaction rule (serialize.py:68–71) deliberately anonymizes Carnivore's grip trashes, and observed_action_key mirrors that rule, not paper rules. If the redaction rule is ever revisited, that is a game-view ticket, and the observer key follows it.

card left; only the identity is hidden.

(faceup Archives is a public zone; the log names the title, engine.rs:2959) so aggregating would discard real information — but the index is neither observed nor stable (each pick shifts the remaining indices). Duplicate titles correctly share an edge: identical copies are indistinguishable and strategically equivalent. legal.rs:204 populates card_title for every faceup option, so the data is reliable.

actor is not the root searcher, its own tree's index keys name different determinised cards per iteration (e.g. Corp sprint_return:{i} edges in a Runner-root search). Redaction cannot fix this — it is inherent to opponent modeling in single-determinisation-per-iteration IS-MCTS, exactly as install_card:{code} edges already alias across determinised hands. Document it; do not restructure the opponent model.

key has two halves — the actor's tree gets the full key; the observer's tree gets what the redacted log reveals. Any future per-target key must decide both.* Also fix ticket 43's overclaimed doc line on action_key: own-zone indices are fixed across determinisations *when the zone owner is the root searcher*; in the opponent tree they are approximate, like all hidden-zone actions.

Definition of done

behavior is unchanged.

(ai/mcts.rs:1110) is replaced with actor-vs-observer assertions: actor gets distinct keys, blind observer gets the aggregated key, covering at least AnoeticTrash, CarnivoreTrash, ArchivesRecur faceup (title key) and facedown (source key).

determinisation claim.

cargo test --workspace pass from rust/.

Comments

2026-08-02: Filed from review of ticket 43's implementation commit c33d6b9. The affected test currently asserts both MCTS2 trees receive the same full key, which encodes the private-information leak rather than the MO-ISMCTS observation contract. Marked needs-triage because public versus private Archives recursion and the exact observer-key design need a maintainer decision before implementation.

2026-08-02: Triaged with the maintainer (grilling session). Approved with the observed-key table above; faceup recursion keyed by title for the observer; Carnivore follows the codebase's redaction rule (type-only for Corp); actor-tree determinisation aliasing accepted as inherent and documented; two-halves key policy recorded so ticket 45's sweep inherits it (comment appended there). Status flipped to ready-for-agent.

2026-08-02: Implemented. observed_action_key (rust/netrunner-core/src/ai/mcts2.rs) gained the seven arms from the table: SprintReturn / LongevityTrash / HanseiTrash / AnoeticTrash collapse to their type-only key for a Runner observer, CarnivoreTrash likewise for a Corp observer, and ArchivesRecur keys as archives_recur:archives_fd (facedown, source kept) or archives_recur:archives_fu:{card_title} (faceup, title kept, index dropped). The acting player's tree is untouched — a Corp observer still gets the full action_key for the HQ choices, as does a Runner observer for Carnivore. The mcts2 module docs now state the two-halves policy (every per-target key must decide the actor half *and* the observer half), and action_key's doc in ai/mcts.rs no longer claims own-zone indices are stable unconditionally — they are stable only when the zone owner is the root searcher, with the opponent-tree aliasing called out as inherent to single-determinisation-per-iteration opponent modeling. The leaky own_zone_target_keys_are_identical_in_both_mcts2_trees test is replaced by six tests covering actor-side preservation, Runner aggregation of the four HQ choices, Corp aggregation of Carnivore, faceup title keying (including two indices of the same title sharing an edge), facedown source-only keying, and the untouched Done* companions. cargo fmt and cargo clippy --workspace -- -D warnings clean; cargo test --workspace 149 passed (144 lib + 5 integration, up from 144 total); pytest 134 passed.

2026-08-02: Superseded in one detail by ticket 45's triage: the maintainer decided the Carnivore log anonymization is cosmetic (the runner heap is serialized ungated with full identity, serialize.py:343) and dropped it. Per this ticket's own provision ("if the redaction rule is ever revisited … the observer key follows it"), CarnivoreTrash becomes a public-outcome code key (carnivore_trash:{card_code}) in both trees and the (Corp, CarnivoreTrash) observer arm shipped here is removed by ticket 45. Everything else in this ticket stands.