Skip to content

Commit 05df0b4

Browse files
committed
merge: drive-fault compile-time fault arm for CC rig drive (default OFF, structurally absent)
2 parents 92804d7 + 1014d2a commit 05df0b4

2 files changed

Lines changed: 168 additions & 0 deletions

File tree

crates/mc-module/Cargo.toml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,23 @@ tokio = { workspace = true }
3434
sha2 = { workspace = true }
3535
chrono = { version = "0.4", default-features = false, features = ["clock"] }
3636

37+
[features]
38+
# Joint CC rig-drive fault injection. DEFAULT OFF.
39+
#
40+
# A default (deploy) build does not contain the corruption path at all: every line
41+
# of fault code lives under `#[cfg(feature = "drive-fault")]` in src/lib.rs, so its
42+
# ABSENCE from the shipped binary is the dormancy proof — there is no runtime-
43+
# reachable arm that a stray env var could activate (a runtime-reachable corruption
44+
# path was explicitly rejected: a forgotten var in prod would silently raw-fence
45+
# every CC session forever). The drive builds a SEPARATE binary with
46+
# `--features drive-fault`; that binary's only diff vs the deploy SHA is this flag,
47+
# and under it MC_DRIVE_FAULT selects an arm (fingerprint_skew | omit_ck_messages)
48+
# so the drive can flip arms between passes without rebuilding.
49+
#
50+
# Do NOT add drive-fault to a default feature set, and do NOT add new fault arms
51+
# without the matching absence-proof note here and the fault-shape tests in lib.rs.
52+
drive-fault = []
53+
3754
[dev-dependencies]
3855
subc-core = { workspace = true }
3956
# Enable the store's seed helpers for mc-module's compose tests (populate memories +

crates/mc-module/src/lib.rs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
91369214
fn 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

Comments
 (0)