diff --git a/.gitignore b/.gitignore index a90e648..af5b286 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ artifacts/* !artifacts/forc10/ !artifacts/forc10/** artifacts/forc10/out/ +crates/*/artifacts/ +proptest-regressions/ diff --git a/crates/evidenceos-daemon/src/http_preflight.rs b/crates/evidenceos-daemon/src/http_preflight.rs index 786afe6..073692b 100644 --- a/crates/evidenceos-daemon/src/http_preflight.rs +++ b/crates/evidenceos-daemon/src/http_preflight.rs @@ -55,7 +55,6 @@ pub struct PreflightToolCallResponse { pub paramsHash: Option, } - #[derive(Debug, Clone, Deserialize)] #[allow(non_snake_case)] pub struct PostflightToolCallRequest { @@ -172,7 +171,6 @@ async fn preflight_tool_call( } } - async fn postflight_tool_call( State(state): State, headers: HeaderMap, @@ -322,7 +320,10 @@ pub async fn preflight_tool_call_impl( validate_hex_64(client_hash) .map_err(|_| HttpErr::invalid_argument("invalid paramsHash", "invalid_params_hash"))?; if client_hash != params_hash { - return Err(HttpErr::invalid_argument("paramsHash mismatch", "params_hash_mismatch")); + return Err(HttpErr::invalid_argument( + "paramsHash mismatch", + "params_hash_mismatch", + )); } } @@ -411,7 +412,6 @@ pub async fn preflight_tool_call_impl( Ok(response) } - #[derive(Debug)] pub struct PostflightHttpErr { pub(crate) status: StatusCode, @@ -443,38 +443,57 @@ pub async fn postflight_tool_call_impl( ) -> Result { validate_authorization(headers, &state.cfg) .map_err(|_| PostflightHttpErr::invalid("unauthorized"))?; - let _ = validate_request_id(headers).map_err(|_| PostflightHttpErr::invalid("missing_request_id"))?; - let req: PostflightToolCallRequest = serde_json::from_slice(body) - .map_err(|_| PostflightHttpErr::invalid("invalid_json"))?; + let _ = validate_request_id(headers) + .map_err(|_| PostflightHttpErr::invalid("missing_request_id"))?; + let req: PostflightToolCallRequest = + serde_json::from_slice(body).map_err(|_| PostflightHttpErr::invalid("invalid_json"))?; validate_ascii_printable_len(&req.toolName, 1, 128, "toolName") .map_err(|_| PostflightHttpErr::invalid("invalid_tool_name"))?; - validate_hex_64(&req.paramsHash).map_err(|_| PostflightHttpErr::invalid("invalid_params_hash"))?; + validate_hex_64(&req.paramsHash) + .map_err(|_| PostflightHttpErr::invalid("invalid_params_hash"))?; if let Some(h) = req.outputHash.as_deref() { validate_hex_64(h).map_err(|_| PostflightHttpErr::invalid("invalid_output_hash"))?; } if let Some(h) = req.preflightReceiptHash.as_deref() { - validate_hex_64(h).map_err(|_| PostflightHttpErr::invalid("invalid_preflight_receipt_hash"))?; + validate_hex_64(h) + .map_err(|_| PostflightHttpErr::invalid("invalid_preflight_receipt_hash"))?; } if let Some(session) = req.sessionId.as_deref() { validate_ascii_printable_len(session, 0, 128, "sessionId") .map_err(|_| PostflightHttpErr::invalid("invalid_session_id"))?; } if state.high_risk_tools.contains(&req.toolName) - && req.sessionId.as_deref().map(|s| s.is_empty()).unwrap_or(true) + && req + .sessionId + .as_deref() + .map(|s| s.is_empty()) + .unwrap_or(true) { return Err(PostflightHttpErr::invalid("session_required")); } - let output_json = req.output.as_ref().map(|v| serde_json::to_vec(v).unwrap_or_default()); - let output_len = output_json.as_ref().map(|v| v.len() as u64).or(req.outputBytes).unwrap_or(0); + let output_json = req + .output + .as_ref() + .map(|v| serde_json::to_vec(v).unwrap_or_default()); + let output_len = output_json + .as_ref() + .map(|v| v.len() as u64) + .or(req.outputBytes) + .unwrap_or(0); let output_hash = req .outputHash .clone() .or_else(|| output_json.as_ref().map(|v| hex::encode(sha256_bytes(v)))); - let operation = req.sessionId.clone().unwrap_or_else(|| "no-session".to_string()); + let operation = req + .sessionId + .clone() + .unwrap_or_else(|| "no-session".to_string()); let principal = principal_id_from_auth(headers); - let semantic_hash = output_hash.clone().unwrap_or_else(|| req.paramsHash.clone()); + let semantic_hash = output_hash + .clone() + .unwrap_or_else(|| req.paramsHash.clone()); let now_ms = state.clock.now_ms(); let (probe_verdict, snapshot) = { let mut guard = state.probe.lock(); @@ -505,7 +524,10 @@ pub async fn postflight_tool_call_impl( if output_len > max_bytes { decision = "REDACT".to_string(); let preview = serde_json::to_string(raw).unwrap_or_default(); - let preview: String = preview.chars().take(state.cfg.postflight_preview_chars).collect(); + let preview: String = preview + .chars() + .take(state.cfg.postflight_preview_chars) + .collect(); output_rewrite = Some(json!({ "truncated": true, "len": output_len, @@ -519,7 +541,8 @@ pub async fn postflight_tool_call_impl( let budget_delta = 1.0; let budget_remaining = state .hard_freeze_ops - .saturating_sub(snapshot.unique_semantic_hashes_operation) as f64; + .saturating_sub(snapshot.unique_semantic_hashes_operation) + as f64; let mut response = PostflightToolCallResponse { decision, @@ -540,7 +563,8 @@ pub async fn postflight_tool_call_impl( }, "response": response.clone(), }); - let canonical = canonical_json(&receipt_payload).map_err(|_| PostflightHttpErr::invalid("receipt_canonical"))?; + let canonical = canonical_json(&receipt_payload) + .map_err(|_| PostflightHttpErr::invalid("receipt_canonical"))?; response.receiptHash = hex::encode(sha256_bytes(&canonical)); let etl_record = json!({ @@ -564,8 +588,10 @@ fn append_postflight_etl(path: &PathBuf, record: &Value) -> Result<(), std::io:: } let mut file = OpenOptions::new().create(true).append(true).open(path)?; serde_json::to_writer(&mut file, record)?; - file.write_all(b" -")?; + file.write_all( + b" +", + )?; Ok(()) } @@ -910,6 +936,7 @@ mod tests { clock, rate_state: Arc::new(Mutex::new(RateLimitState::default())), high_risk_tools: Arc::new(HashSet::from(["shell.exec".to_string()])), + postflight_etl_path: PathBuf::from("/dev/null"), } } diff --git a/crates/evidenceos-daemon/src/http_preflight_tests.rs b/crates/evidenceos-daemon/src/http_preflight_tests.rs index 2693971..a632167 100644 --- a/crates/evidenceos-daemon/src/http_preflight_tests.rs +++ b/crates/evidenceos-daemon/src/http_preflight_tests.rs @@ -8,7 +8,8 @@ use serde_json::{json, Map, Value}; use crate::config::DaemonConfig; use crate::http_preflight::{ - postflight_tool_call_impl, preflight_tool_call_impl, stable_params_hash, HttpPreflightState, RateLimitState, + postflight_tool_call_impl, preflight_tool_call_impl, stable_params_hash, HttpPreflightState, + RateLimitState, }; use crate::probe::{ProbeClock, ProbeConfig, ProbeDetector}; use crate::telemetry::Telemetry; @@ -231,10 +232,12 @@ async fn invalid_inputs_use_constant_public_error_shape() { assert_eq!(lengths.len(), 1); } - #[tokio::test] async fn postflight_redacts_large_output_deterministically() { - let st = state(DaemonConfig { postflight_default_max_output_bytes: 8, ..DaemonConfig::default() }); + let st = state(DaemonConfig { + postflight_default_max_output_bytes: 8, + ..DaemonConfig::default() + }); let headers = request_headers("req-post-1"); let body = json!({ "toolName":"exec", @@ -242,9 +245,14 @@ async fn postflight_redacts_large_output_deterministically() { "paramsHash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "status":"ok", "output": {"x":"abcdefghijklmnopqrstuvwxyz"} - }).to_string(); - let r1 = postflight_tool_call_impl(&st, &headers, body.as_bytes()).await.expect("ok"); - let r2 = postflight_tool_call_impl(&st, &headers, body.as_bytes()).await.expect("ok"); + }) + .to_string(); + let r1 = postflight_tool_call_impl(&st, &headers, body.as_bytes()) + .await + .expect("ok"); + let r2 = postflight_tool_call_impl(&st, &headers, body.as_bytes()) + .await + .expect("ok"); assert_eq!(r1.outputRewrite, r2.outputRewrite); assert_eq!(r1.decision, "REDACT"); } @@ -253,15 +261,22 @@ async fn postflight_redacts_large_output_deterministically() { async fn postflight_budget_decreases_monotonically_per_operation_for_unique_outputs() { let st = state(DaemonConfig::default()); let headers = request_headers("req-post-2"); - let mk = |idx| json!({ - "toolName":"tool.a", - "sessionId":"op1", - "paramsHash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", - "status":"ok", - "output": {"n": idx} - }).to_string(); - let r1 = postflight_tool_call_impl(&st, &headers, mk(1).as_bytes()).await.expect("r1"); - let r2 = postflight_tool_call_impl(&st, &headers, mk(2).as_bytes()).await.expect("r2"); + let mk = |idx| { + json!({ + "toolName":"tool.a", + "sessionId":"op1", + "paramsHash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "status":"ok", + "output": {"n": idx} + }) + .to_string() + }; + let r1 = postflight_tool_call_impl(&st, &headers, mk(1).as_bytes()) + .await + .expect("r1"); + let r2 = postflight_tool_call_impl(&st, &headers, mk(2).as_bytes()) + .await + .expect("r2"); assert!(r2.budgetRemainingBits.unwrap_or(0.0) <= r1.budgetRemainingBits.unwrap_or(0.0)); } @@ -274,7 +289,10 @@ async fn postflight_requires_session_id_for_high_risk_tools() { "paramsHash": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", "status":"ok", "output": {"ok":true} - }).to_string(); - let err = postflight_tool_call_impl(&st, &headers, body.as_bytes()).await.expect_err("must fail"); + }) + .to_string(); + let err = postflight_tool_call_impl(&st, &headers, body.as_bytes()) + .await + .expect_err("must fail"); assert_eq!(err.status, axum::http::StatusCode::BAD_REQUEST); }