@@ -9133,10 +9133,95 @@ fn replay_dream_task_response(response_json: &str) -> HandlerOutcome {
91339133 respond(response)
91349134}
91359135
9136+ // SAFETY: Deliberate fault injection for the joint CC rig drive — see the `drive-fault`
9137+ // feature note in Cargo.toml for why this ships in the binary at all.
9138+ //
9139+ // Why it exists: the claude-code-anthropic ("CC") transform leg carries no organic
9140+ // traffic, so its mismatch / "raw-only fence" error paths — the consumer's reaction to a
9141+ // response whose echoed fingerprint or message array does not match what it submitted —
9142+ // can only be exercised by a rig drive. To induce those paths the drive needs OUR
9143+ // transform response to be deliberately malformed; the CC-leg peer correctly refuses to
9144+ // carry fault scaffolding on its own side, so the corruption lives here.
9145+ //
9146+ // Why it is safe: this whole block is compiled ONLY under `--features drive-fault`. A
9147+ // default deploy build has no corruption path at all — that structural absence is the
9148+ // dormancy proof, so there is no runtime-reachable arm a stray env var could trigger.
9149+ // Even under the feature the arm is inert unless MC_DRIVE_FAULT selects it, and it is
9150+ // scoped to transform responses only: respond_transform is the sole transform-response
9151+ // serializer, and facade tools and status/wrapup ops never route through it.
9152+ //
9153+ // Maintenance rule: never add another fault arm without the same absence-proof note in
9154+ // Cargo.toml and the fault-shape tests that pin the existing arms.
9155+ #[cfg(feature = "drive-fault")]
9156+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
9157+ enum DriveFault {
9158+ FingerprintSkew,
9159+ OmitCkMessages,
9160+ }
9161+
9162+ /// Map the raw MC_DRIVE_FAULT value to a fault arm. Pure (no env access) so the
9163+ /// selection logic is unit-testable without mutating process-global env or the
9164+ /// process-wide OnceLock below. Any unrecognized value — or no value — maps to None,
9165+ /// which leaves the response untouched.
9166+ #[cfg(feature = "drive-fault")]
9167+ fn parse_drive_fault(raw: Option<&str>) -> Option<DriveFault> {
9168+ match raw {
9169+ Some("fingerprint_skew") => Some(DriveFault::FingerprintSkew),
9170+ Some("omit_ck_messages") => Some(DriveFault::OmitCkMessages),
9171+ _ => None,
9172+ }
9173+ }
9174+
9175+ /// The active fault arm, read from MC_DRIVE_FAULT once per process (first call wins via
9176+ /// OnceLock — no per-request getenv on the transform hot path).
9177+ #[cfg(feature = "drive-fault")]
9178+ fn drive_fault() -> Option<DriveFault> {
9179+ use std::sync::OnceLock;
9180+ static FAULT: OnceLock<Option<DriveFault>> = OnceLock::new();
9181+ *FAULT.get_or_init(|| parse_drive_fault(std::env::var("MC_DRIVE_FAULT").ok().as_deref()))
9182+ }
9183+
9184+ /// Corrupt a transform response per the selected fault arm and log one loud WARN per
9185+ /// fired fault so a forgotten MC_DRIVE_FAULT is impossible to miss in rig logs.
9186+ #[cfg(feature = "drive-fault")]
9187+ fn apply_drive_fault(response: &mut transform::TransformResponse, fault: DriveFault) {
9188+ match fault {
9189+ DriveFault::FingerprintSkew => {
9190+ // Echo a perturbed fingerprint so it fails any equality check against the
9191+ // submitted value. A response with no fingerprint still gets a non-empty
9192+ // sentinel so the echo cannot match a submitted empty/absent value.
9193+ let perturbed = match response.full_array_fingerprint.take() {
9194+ Some(fingerprint) => format!("{fingerprint}_skew"),
9195+ None => "_skew".to_string(),
9196+ };
9197+ response.full_array_fingerprint = Some(perturbed);
9198+ eprintln!(
9199+ "mc-module: WARN MC_DRIVE_FAULT=fingerprint_skew active — response deliberately corrupted for drive"
9200+ );
9201+ }
9202+ DriveFault::OmitCkMessages => {
9203+ // Drop ck_messages so it serializes ABSENT (the same skip_serializing_if arm
9204+ // need_full_sync uses) while keeping the ok status: a success-shaped response
9205+ // with the array field missing.
9206+ response.ck_messages = None;
9207+ eprintln!(
9208+ "mc-module: WARN MC_DRIVE_FAULT=omit_ck_messages active — response deliberately corrupted for drive"
9209+ );
9210+ }
9211+ }
9212+ }
9213+
91369214fn respond_transform(
91379215 session_id: &str,
91389216 mut response: transform::TransformResponse,
91399217) -> HandlerOutcome {
9218+ // drive-fault: corrupt the response before it is serialized (see the SAFETY note
9219+ // above the fault helpers). No-op unless the feature is compiled in AND MC_DRIVE_FAULT
9220+ // selects an arm; must run before ck_messages is taken for the streaming placeholder.
9221+ #[cfg(feature = "drive-fault")]
9222+ if let Some(fault) = drive_fault() {
9223+ apply_drive_fault(&mut response, fault);
9224+ }
91409225 let response_encode_started_at = Instant::now();
91419226 let pass_timings = response.timings.clone();
91429227 let messages = response.ck_messages.take();
@@ -12662,6 +12747,72 @@ mod tests {
1266212747 assert_eq!(actual, expected);
1266312748 }
1266412749
12750+ // drive-fault fault-shape tests. These only exist under `--features drive-fault`
12751+ // (the same gate as the corruption path itself); a default build has neither the arm
12752+ // nor these tests. They exercise `apply_drive_fault`/`parse_drive_fault` directly
12753+ // rather than setting MC_DRIVE_FAULT, so they never touch process-global env or the
12754+ // process-wide OnceLock and stay deterministic alongside the rest of the suite.
12755+ #[test]
12756+ #[cfg(feature = "drive-fault")]
12757+ fn drive_fault_parse_maps_arms_and_ignores_unknown() {
12758+ assert_eq!(
12759+ parse_drive_fault(Some("fingerprint_skew")),
12760+ Some(DriveFault::FingerprintSkew)
12761+ );
12762+ assert_eq!(
12763+ parse_drive_fault(Some("omit_ck_messages")),
12764+ Some(DriveFault::OmitCkMessages)
12765+ );
12766+ // Unset, empty, and unrecognized values all leave the response untouched.
12767+ assert_eq!(parse_drive_fault(None), None);
12768+ assert_eq!(parse_drive_fault(Some("")), None);
12769+ assert_eq!(parse_drive_fault(Some("anything_else")), None);
12770+ }
12771+
12772+ #[test]
12773+ #[cfg(feature = "drive-fault")]
12774+ fn drive_fault_fingerprint_skew_perturbs_echoed_fingerprint() {
12775+ let submitted = "abc123".to_string();
12776+ let mut response = transform::TransformResponse::passthrough(
12777+ vec![ck("fp-skew", 1, "hello").ck],
12778+ Some(submitted.clone()),
12779+ );
12780+ apply_drive_fault(&mut response, DriveFault::FingerprintSkew);
12781+ // The echoed fingerprint must fail an equality check against the submitted value,
12782+ // while the response otherwise stays success-shaped (ok status, array present).
12783+ let echoed = response
12784+ .full_array_fingerprint
12785+ .clone()
12786+ .expect("fingerprint still echoed after skew");
12787+ assert_ne!(echoed, submitted);
12788+ assert_eq!(response.status, transform::TransformStatus::Ok);
12789+ assert!(response.ck_messages.is_some());
12790+ }
12791+
12792+ #[test]
12793+ #[cfg(feature = "drive-fault")]
12794+ fn drive_fault_omit_ck_messages_absents_field_on_ok_status() {
12795+ let mut response = transform::TransformResponse::passthrough(
12796+ vec![ck("omit-ck", 1, "hello").ck],
12797+ Some("fingerprint".to_string()),
12798+ );
12799+ assert!(
12800+ response.ck_messages.is_some(),
12801+ "passthrough response carries ck_messages before the fault"
12802+ );
12803+ apply_drive_fault(&mut response, DriveFault::OmitCkMessages);
12804+ // Success-shaped (ok status retained) but with the ck_messages field ABSENT on the
12805+ // wire — the same skip_serializing_if arm need_full_sync uses.
12806+ assert_eq!(response.status, transform::TransformStatus::Ok);
12807+ assert!(response.ck_messages.is_none());
12808+ let value = serde_json::to_value(&response).unwrap();
12809+ assert!(
12810+ value.get("ck_messages").is_none(),
12811+ "ck_messages must be absent from the serialized response"
12812+ );
12813+ assert_eq!(value["status"], json!("ok"));
12814+ }
12815+
1266512816 #[tokio::test(flavor = "current_thread")]
1266612817 async fn serve_native_false_is_response_byte_identical_for_all_profiles() {
1266712818 let producer = Arc::new(ProducerState::default());
0 commit comments