use async_trait::async_trait;
use car_engine::ToolExecutor;
use serde_json::{json, Value};
const M365_TOOL_TIER: &str = "full_access";
pub struct M365Tools;
impl M365Tools {
pub fn new() -> Self {
Self
}
fn available(&self) -> bool {
car_auth::access_token().is_some()
}
pub fn tool_defs(&self) -> Vec<Value> {
if !self.available() {
return Vec::new();
}
m365_tool_defs()
}
}
impl Default for M365Tools {
fn default() -> Self {
Self::new()
}
}
fn m365_tool_defs() -> Vec<Value> {
vec![json!({
"name": "m365_task",
"description": "Delegate a task to the user's Microsoft 365 assistant (their Parslee AI \
Employee) — email, calendar, contacts, CRM (HubSpot), meetings, and business research \
across their connected M365 and tools. Give a plain-language instruction like \
'summarize my unread email from today', 'what's on my calendar tomorrow', 'draft a \
follow-up to the Acme thread', or 'find our latest deal with Contoso in HubSpot'. \
Returns the assistant's reply (and a conversation_id you can pass back to continue the \
same thread). IMPORTANT: actions that SEND or CHANGE things (send an email, create a \
calendar event) are drafted and returned for approval, not executed silently — relay \
the reply and let the user confirm. Mailbox / calendar access needs a connected \
Microsoft account; if it isn't connected the reply says so.",
"parameters": {
"type": "object",
"properties": {
"task": {
"type": "string",
"description": "The plain-language M365 task or question."
},
"conversation_id": {
"type": "string",
"description": "Optional id from a previous m365_task reply, to continue the same thread."
}
},
"required": ["task"]
},
"mutating": true,
"tier": M365_TOOL_TIER
})]
}
#[async_trait]
impl ToolExecutor for M365Tools {
async fn execute(&self, tool: &str, params: &Value) -> Result<Value, String> {
match tool {
"m365_task" => car_parslee::parslee_m365::chat_task(params).await,
other => Err(format!("unknown tool: '{other}'")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn m365_task_requires_full_access_tier() {
let defs = m365_tool_defs();
assert_eq!(defs.len(), 1);
assert_eq!(defs[0]["name"], "m365_task");
assert_eq!(
defs[0]["tier"], M365_TOOL_TIER,
"m365_task must require full-access approval because it delegates to an external \
agent that can act on the user's mailbox / calendar"
);
assert_eq!(defs[0]["mutating"], true);
}
#[tokio::test]
async fn unknown_tool_falls_through() {
let tools = M365Tools::new();
let err = tools.execute("nope", &json!({})).await.unwrap_err();
assert!(err.starts_with("unknown tool"), "{err}");
}
}