diff --git a/crates/spt-daemon/src/broker.rs b/crates/spt-daemon/src/broker.rs index 1b1acc4..be74fa1 100644 --- a/crates/spt-daemon/src/broker.rs +++ b/crates/spt-daemon/src/broker.rs @@ -1466,6 +1466,11 @@ impl OutputLog { // The TOTP gate is the other half of the same seat rule and lives at // the same door, for the same reason the locks do. crate::attach::EngineRoomLock::Pass => { + gate_probe(&format!( + "SEAT_PASS conn={conn} intent={intent:?} by={by:?} code_present={} endpoint={}", + code.is_some(), + self.endpoint + )); return self.bringup_refusal(code, conn, ticket) } crate::attach::EngineRoomLock::NoViewport => { @@ -1517,6 +1522,11 @@ impl OutputLog { ticket: AdmitTicket, ) -> SeatGate { use spt_store::engineroom as er; + let ticket_probe_label = match &ticket { + AdmitTicket::Redeemed(_) => "redeemed", + AdmitTicket::Unredeemed => "unredeemed", + AdmitTicket::None => "none", + }; match ticket { // A bring-up this broker admitted seats its controller without // paying twice for one entry (releases#71). It is not a discount: @@ -1574,6 +1584,12 @@ impl OutputLog { if !er::bringup_required(self.controller.as_ref().map(|c| c.send.id()), conn) { return SeatGate::Proceed(None); } + gate_probe(&format!( + "CALLER=seat conn={conn} code_present={} ticket_was={} seated={:?}", + code.is_some(), + ticket_probe_label, + self.controller.as_ref().map(|c| c.send.id()) + )); match evaluate_bringup(code) { // Admitted. The grant and the briefing both wait for the seat to // actually resolve — see [`SeatGate::Proceed`] and @@ -1716,8 +1732,21 @@ pub(crate) enum BringUpVerdict { // [impl->REQ-ER-BRINGUP-TOTP-GATE] // [impl->REQ-ER-BRINGUP-ATTEMPT-BOUND] // [impl->REQ-ER-BRINGUP-SPAWNS-SESSION] +// RCA PROBE (doyle 2026-08-19, rca-182-gate rig — NOT FOR COMMIT). Appends gate +// events to the file named by SPT_GATE_PROBE so a run's presentations, cred +// verdicts and ledger transitions are observable outside the test's TempDir. +pub(crate) fn gate_probe(msg: &str) { + if let Ok(p) = std::env::var("SPT_GATE_PROBE") { + use std::io::Write; + if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(p) { + let _ = writeln!(f, "{} pid={} {}", crate::brain::now_ms(), std::process::id(), msg); + } + } +} + pub(crate) fn evaluate_bringup(code: Option<&str>) -> BringUpVerdict { use spt_store::engineroom as er; + gate_probe(&format!("EVAL_ENTER code_present={}", code.is_some())); let refuse = |reason: &str, label: &'static str| { eprintln!("ENGINE_ROOM_BRINGUP_REFUSED: {label} — {reason}"); BringUpVerdict::Refused { @@ -1757,6 +1786,12 @@ pub(crate) fn evaluate_bringup(code: Option<&str>) -> BringUpVerdict { // touching the backoff schedule at all. let attempt = er::classify_attempt(&ledger, now, cred.admits()); let verdict = er::classify_bringup(adapter_present, attempt); + gate_probe(&format!( + "EVAL_VERDICT adapter={adapter_present} admits={} ledger_before={{failures:{},last_ms:{}}} attempt={attempt:?}", + cred.admits(), + ledger.failures, + ledger.last_failure_ms + )); if verdict != er::BringUp::NoAdapter { er::apply(&mut ledger, &attempt, now); if let Err(e) = ledger.save() { @@ -3814,6 +3849,11 @@ fn bringup_code_verifies( use spt_net::net::pairing::totp::code_matches_window; let subnets = spt_store::subnet::SubnetStore::load(); let Some(rec) = subnets.find(&room.home_subnet) else { + gate_probe(&format!( + "VERIFY_NO_SUBNET home={} store_len={}", + room.home_subnet, + subnets.subnets.len() + )); return BringUpCred::None; }; let now = spt_net::net::pairing::ntp::ceremony_now_secs(); @@ -3823,6 +3863,21 @@ fn bringup_code_verifies( let admin = rec .admin_seed_bytes() .is_some_and(|seed| code_matches_window(&seed, presented, now)); + gate_probe(&format!( + "VERIFY presented={presented} now={now} step={} member={member} admin={admin} member_seed={} admin_seed={} expected_window={:?}", + spt_net::net::pairing::totp::time_step(now), + rec.seed_bytes().is_some(), + rec.admin_seed_bytes().is_some(), + rec.seed_bytes() + .map(|seed| { + let t = spt_net::net::pairing::totp::TotpSeed::from_bytes(seed); + spt_net::net::pairing::totp::window_steps(spt_net::net::pairing::totp::time_step(now)) + .iter() + .map(|s| format!("{:06}", t.code_at_step(*s, spt_net::net::pairing::totp::DIGITS))) + .collect::>() + }) + .unwrap_or_default() + )); classify_cred(member, admin) } @@ -5540,6 +5595,11 @@ impl Broker { ); return Ok(()); } + gate_probe(&format!( + "CALLER=verb endpoint={} code_present={}", + req.endpoint, + req.code.is_some() + )); let (room, cred) = match evaluate_bringup(req.code.as_deref()) { BringUpVerdict::Refused { reason, .. } => { reply(BRING_UP_REFUSED, reason, 0, None); diff --git a/crates/spt/tests/engine_room_bringup_e2e.rs b/crates/spt/tests/engine_room_bringup_e2e.rs index 0c3554d..b80c706 100644 --- a/crates/spt/tests/engine_room_bringup_e2e.rs +++ b/crates/spt/tests/engine_room_bringup_e2e.rs @@ -125,7 +125,25 @@ fn seed_subnet_and_code(name: &str) -> String { // The SAME clock the verifier reads, so the code we present is the code it // expects — a wall-clock skew here would look like a wrong code. let now = spt_net::net::pairing::ntp::ceremony_now_secs(); - spt_net::net::pairing::totp::TotpSeed::from_bytes(seed).code_at(now) + let code = spt_net::net::pairing::totp::TotpSeed::from_bytes(seed).code_at(now); + // RCA PROBE (doyle 2026-08-19, rig-only): mint-side twin of the broker's + // SPT_GATE_PROBE lines — same file, so mint and verify interleave. + if let Ok(p) = std::env::var("SPT_GATE_PROBE") { + use std::io::Write; + if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(p) { + let _ = writeln!( + f, + "{} pid={} MINT subnet={name} code={code} now={now} step={}", + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or_default(), + std::process::id(), + spt_net::net::pairing::totp::time_step(now) + ); + } + } + code } /// Register the harness adapter the ceremony BINDS. The engine room is an