PINK DITAv2: kernel-level finiteness guard (no more null-string crash on inf/NaN)

The aborted hard cutover crash-looped with "Rust kernel returned null string" from
process_intent on the first live trading step. Root cause (reproduced): a non-finite
(inf/NaN) numeric field reaching the kernel — Python json.dumps emits the Infinity/NaN
token, serde_json rejects it at parse, and the FFI returned null. Magnitude is fine;
only finiteness was the problem.

Defense in depth, kernel catches it:
- Rust FFI (lib.rs): dita_kernel_process_intent_json / _on_venue_event_json now return
  a clean INVALID_INTENT KernelResult on parse failure (incl. Infinity/NaN tokens) AND
  on serialize failure (a non-finite produced internally) — never a null string.
- Python bridge (rust_backend.py): ExecutionKernel.process_intent validates intent
  finiteness/bounds (target_size, reference_price, limit_price, leverage, exit_leg_ratios;
  size>=0) BEFORE the FFI and rejects INVALID_INTENT, naming the offending field+value.
- contracts.py: add KernelDiagnosticCode.INVALID_INTENT.
- pink_direct.py: on INVALID_INTENT, log full upstream provenance (snapshot.price,
  capital, leverage, sizes) so the numerical SOURCE can be located on the next live run.
- on_venue_event bridge tolerates the fallback's null slot (uses the live slot).

Verified: kernel recompiled; offline 65 + 7 new guard tests green (no regression);
direct-FFI inf payload -> INVALID_INTENT (no null crash). NOTE: this turns the cutover
crash into a clean rejection — the upstream source of the non-finite (the live run's
inf) still needs locating, now aided by the provenance log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-05-31 09:10:13 +02:00
parent 0c15a7698e
commit 9168cf0759
5 changed files with 193 additions and 7 deletions

View File

@@ -1477,6 +1477,31 @@ fn into_c_string(value: &str) -> *mut c_char {
CString::new(value).unwrap().into_raw()
}
/// Build a well-formed INVALID_INTENT KernelResult JSON string. Used when the
/// kernel cannot safely process a request — a payload that fails to parse
/// (e.g. non-finite Infinity/NaN tokens serde rejects) or a result that fails
/// to serialize (a non-finite value produced internally). This keeps the kernel
/// from ever returning a null string on non-finite input/output: it rejects
/// cleanly with a diagnostic instead of panicking.
fn invalid_intent_cstring(reason: &str, detail: &str) -> *mut c_char {
let fallback = serde_json::json!({
"outcome": {
"accepted": false,
"slot_id": 0,
"trade_id": "",
"state": "IDLE",
"diagnostic_code": "INVALID_INTENT",
"severity": "WARNING",
"transitions": [],
"emitted_events": [],
"details": {"reason": reason, "detail": detail}
},
"slot": serde_json::Value::Null,
"snapshot": serde_json::Value::Null
});
into_c_string(&fallback.to_string())
}
fn with_handle_mut<F, R>(handle: *mut KernelHandle, f: F) -> Result<R, String>
where
F: FnOnce(&mut KernelCore) -> Result<R, String>,
@@ -1551,11 +1576,21 @@ pub extern "C" fn dita_kernel_process_intent_json(
};
let control_mode = cstr_to_string(control_mode).unwrap_or_else(|_| "NORMAL".to_string());
let control_verbosity = cstr_to_string(control_verbosity).unwrap_or_else(|_| "QUIET".to_string());
// Reject non-parseable payloads (incl. non-finite Infinity/NaN tokens serde
// refuses) with a clean INVALID_INTENT, never a null string.
let intent: KernelIntent = match serde_json::from_str(&payload) {
Ok(value) => value,
Err(err) => return invalid_intent_cstring("INVALID_INTENT_PARSE", &err.to_string()),
};
match with_handle_mut(handle, |core| {
let intent: KernelIntent = serde_json::from_str(&payload).map_err(|err| err.to_string())?;
Ok(core.process_intent(intent, &control_mode, &control_verbosity))
Ok::<_, String>(core.process_intent(intent, &control_mode, &control_verbosity))
}) {
Ok(result) => serde_json::to_string(&result).ok().map(|s| into_c_string(&s)).unwrap_or(ptr::null_mut()),
// A serialize failure means a non-finite value reached the output; reject
// cleanly rather than returning null.
Ok(result) => match serde_json::to_string(&result) {
Ok(s) => into_c_string(&s),
Err(err) => invalid_intent_cstring("INVALID_INTENT_SERIALIZE", &err.to_string()),
},
Err(_) => ptr::null_mut(),
}
}
@@ -1573,11 +1608,17 @@ pub extern "C" fn dita_kernel_on_venue_event_json(
};
let control_mode = cstr_to_string(control_mode).unwrap_or_else(|_| "NORMAL".to_string());
let control_verbosity = cstr_to_string(control_verbosity).unwrap_or_else(|_| "QUIET".to_string());
let event: VenueEvent = match serde_json::from_str(&payload) {
Ok(value) => value,
Err(err) => return invalid_intent_cstring("INVALID_EVENT_PARSE", &err.to_string()),
};
match with_handle_mut(handle, |core| {
let event: VenueEvent = serde_json::from_str(&payload).map_err(|err| err.to_string())?;
Ok(core.on_venue_event(event, &control_mode, &control_verbosity))
Ok::<_, String>(core.on_venue_event(event, &control_mode, &control_verbosity))
}) {
Ok(result) => serde_json::to_string(&result).ok().map(|s| into_c_string(&s)).unwrap_or(ptr::null_mut()),
Ok(result) => match serde_json::to_string(&result) {
Ok(s) => into_c_string(&s),
Err(err) => invalid_intent_cstring("INVALID_EVENT_SERIALIZE", &err.to_string()),
},
Err(_) => ptr::null_mut(),
}
}