diff --git a/crates/gateway/src/main.rs b/crates/gateway/src/main.rs index ac5f6e3..a88aa38 100644 --- a/crates/gateway/src/main.rs +++ b/crates/gateway/src/main.rs @@ -196,6 +196,7 @@ async fn main() { tools::ToolState { registry: tool_reg, query_fn: tools::QueryExecutor::new(engine.clone()), + truth: std::sync::Arc::new(truth::sql_query_guard_store()), } })) // Phase 38 — Universal API skeleton. Thin OpenAI-compatible diff --git a/crates/gateway/src/tools/mod.rs b/crates/gateway/src/tools/mod.rs index 2f316f6..f96dd93 100644 --- a/crates/gateway/src/tools/mod.rs +++ b/crates/gateway/src/tools/mod.rs @@ -3,12 +3,18 @@ pub mod service; use queryd::context::QueryEngine; use arrow::json::writer::{JsonArray, Writer as JsonWriter}; +use std::sync::Arc; +use truth::TruthStore; /// State for the tool system. #[derive(Clone)] pub struct ToolState { pub registry: registry::ToolRegistry, pub query_fn: QueryExecutor, + /// SQL guard (shared with queryd). Mirrors the queryd /sql truth + /// gate from P42-002 (9cc0ceb) — tools also execute model- + /// originated SQL, need the same destructive-verb block. + pub truth: Arc, } /// Wraps QueryEngine to provide a simple execute interface for tools. diff --git a/crates/gateway/src/tools/service.rs b/crates/gateway/src/tools/service.rs index e5b75d1..ef8e099 100644 --- a/crates/gateway/src/tools/service.rs +++ b/crates/gateway/src/tools/service.rs @@ -92,6 +92,32 @@ async fn call_tool( } }; + // Truth gate — same contract as queryd /sql (P42-002). Rejects + // destructive verbs + empty SQL. Scrum iter 11 CF-1 + CF-2 on this + // file: tools executed model-provided SQL parameters without any + // validation. Close the gap here so the parallel surface has the + // same safety floor as queryd. + let ctx = serde_json::json!({ "sql": sql }); + for outcome in state.truth.evaluate("sql_query", &ctx) { + if outcome.passed { + if let truth::RuleAction::Reject { message } | truth::RuleAction::Block { message } = &outcome.action { + tracing::warn!("tool {name}: SQL blocked by truth gate ({}): {message}", outcome.rule_id); + state.registry.log_invocation(ToolInvocation { + id: format!("inv-{}", chrono::Utc::now().timestamp_millis()), + tool_name: name.clone(), + agent: req.agent.clone(), + params: req.params.clone(), + permission: tool.permission.clone(), + timestamp: chrono::Utc::now(), + success: false, + error: Some(format!("truth gate: {message}")), + rows_returned: None, + }).await; + return Err((StatusCode::FORBIDDEN, message.clone())); + } + } + } + // Execute via query engine let result = state.query_fn.execute(&sql).await;