use cm_llm::ToolDescriptor; use cm_tools::Effect; use serde_json::{json, Value}; use uuid::Uuid; use super::{Tool, ToolContext}; /// Queues an outbound email — a §15 gated category (outbound message). /// The real effect is an `outbox` row; the delivery transport drains the /// queue in P4. This row must only ever exist after explicit approval. pub struct EmailSend; #[async_trait::async_trait] impl Tool for EmailSend { fn descriptor(&self) -> ToolDescriptor { ToolDescriptor { name: "email.send".into(), description: "Sends an email outside the workspace. Requires \ human approval before it executes." .into(), input_schema: json!({ "type": "object", "properties": { "to": {"type": "string"}, "subject": {"type": "string"}, "body": {"type": "string"}, }, "required": ["to", "subject", "body"], }), } } fn effects(&self) -> &'static [Effect] { &[Effect::SendsExternally] } fn preview(&self, input: &Value) -> Value { json!({ "summary": format!( "Send email to {}", input["to"].as_str().unwrap_or("(missing recipient)") ), "to": input["to"], "subject": input["subject"], "body": input["body"], }) } async fn execute(&self, ctx: &ToolContext, input: Value) -> Result { let to = input["to"].as_str().ok_or("missing 'to'")?; let subject = input["subject"].as_str().ok_or("missing 'subject'")?; let body = input["body"].as_str().ok_or("missing 'body'")?; let id = Uuid::now_v7(); sqlx::query!( "INSERT INTO outbox (id, workspace_id, agent_id, recipient, subject, body) VALUES ($1, $2, $3, $4, $5, $6)", id, ctx.workspace_id.as_uuid(), ctx.agent_id.as_uuid(), to, subject, body, ) .execute(&ctx.pool) .await .map_err(|e| e.to_string())?; Ok(json!({ "queued": true, "outbox_id": id.to_string() })) } }