Skip to main content

7.13 integrations: connector adapters

// crates/integrations/src/lib.rs
use async_trait::async_trait;
use bo_core::{domain::TenantId, ports::IntegrationConnector, CoreError, Result};
use serde_json::{json, Value};

/// Generic signed webhook. `id`, `url`, `secret` and `actions` come from config/agents/connectors/<id>.yaml.
pub struct WebhookConnector { pub id: String, pub http: reqwest::Client, pub url: String, pub hmac_secret: String, pub actions: Vec<String> }

#[async_trait]
impl IntegrationConnector for WebhookConnector {
fn id(&self) -> &str { &self.id }
fn actions(&self) -> Vec<String> { self.actions.clone() }
async fn invoke(&self, tenant: TenantId, action: &str, payload: Value, key: &str) -> Result<Value> {
if !self.actions.iter().any(|a| a == action) { return Err(CoreError::Integration(format!("unknown action {action}"))); }
let body = json!({ "tenant": tenant.0, "action": action, "payload": payload });
let sig = sign(&self.hmac_secret, &body); // TODO(impl): HMAC-SHA256
use backon::{ExponentialBuilder, Retryable};
let op = || async {
self.http.post(&self.url).header("Idempotency-Key", key).header("X-Signature", &sig)
.json(&body).send().await?.error_for_status()
};
let resp = op.retry(ExponentialBuilder::default().with_max_times(5)).await
.map_err(|e| CoreError::Integration(e.to_string()))?;
Ok(resp.json().await.unwrap_or(json!({ "status": "accepted" })))
}
}
fn sign(_secret: &str, _body: &Value) -> String { "TODO".into() }

/// Stub accounting adapter for development. Replace with QuickBooks, Xero, Odoo or a webhook.
pub struct StubAccounting { pub id: String }
#[async_trait]
impl IntegrationConnector for StubAccounting {
fn id(&self) -> &str { &self.id }
fn actions(&self) -> Vec<String> { vec!["post_bill".into(), "get_vendor".into()] }
async fn invoke(&self, _t: TenantId, action: &str, payload: Value, key: &str) -> Result<Value> {
match action {
"post_bill" => Ok(json!({ "external_id": format!("BILL-{key}"), "echo": payload })),
"get_vendor" => Ok(json!({ "vendors": [] })),
_ => Err(CoreError::Integration(format!("unknown action {action}"))),
}
}
}
// TODO(impl): McpConnector — wraps an MCP server; each MCP tool becomes one action.

The app's engine::adapters::build_connectors maps each connector YAML kind (accounting_stub, webhook, mcp, …) to one of these adapters.