cots 0.2.0

Cots.ai SDK for Rust. cots::agents::AgentClient — configure once with tenant_id/agent_id, then guard() every governed action through the PEP/PDP before executing it.
Documentation

cots

The Cots.ai SDK for Rust. cots::agents is the interceptor client for agents written in Rust — mirrors @cots/sample (the Node package) field for field: same config shape, same guard() pattern, same wire format the Rust PEP/PDP expects. Future modules (cots::policy, cots::audit, ...) would live alongside agents in this same crate as it grows.

Add it

cargo add cots

Or add it to your Cargo.toml directly:

[dependencies]
cots = "0.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

Configure

use cots::agents::{AgentClient, AgentConfig};

let agent = AgentClient::new(
    AgentConfig::new("ten_xxxxxxxxxx", "agt_xxxxxxxxxx")
        .with_data_plane_url("http://localhost:9090")
        .with_control_plane_url("http://localhost:8080/api"),
);

Or load COTS_TENANT_ID / COTS_AGENT_ID / COTS_DATA_PLANE_URL / COTS_CONTROL_PLANE_URL / COTS_APPROVAL_TIMEOUT_MS / COTS_APPROVAL_POLL_MS from the environment (same convention as the Node SDK):

let agent = AgentClient::new(AgentConfig::from_env()?);

Use it — guard() is the one method most agents need

use cots::agents::NormalizedAction;

let result = agent
    .guard(NormalizedAction::new("Slack", "send_slack_message"), || async {
        // send the real Slack message here
        "sent"
    })
    .await?;

// result.outcome is Executed | Blocked | Timeout
// result.decision is "allowed" | "blocked" | "require_approval"

guard() calls the interceptor first. If allowed, your closure runs immediately. If blocked, it never runs. If require_approval, it holds and polls the control plane until a human decides (or approval_timeout_ms elapses), then runs your closure only if approved.

Need lower-level control? agent.intercept(action) and agent.wait_for_approval(action_event_id) are the two calls guard() composes.

Onboarding (one-time, per organization/agent)

agent.control_plane exposes the same onboarding surface the admin panel's wizard uses:

let reg = agent.control_plane.register_tenant("Srotas Space Pvt Ltd").await?;
let created = agent.control_plane.register_agent(&reg.tenant.tenant_id, "Ops Agent").await?;
agent.control_plane
    .create_action_surface(&reg.tenant.tenant_id, &created.agent_id, "Slack", &["send_slack_message"])
    .await?;
let pep_config = agent.control_plane.activate_agent(&created.agent_id).await?;
// pep_config.pep_endpoint is a real-deployment placeholder — locally, keep using data_plane_url above.

Framework-agnostic — works with actix-web, axum, or nothing at all

cots::agents has no dependency on any web framework — just reqwest + tokio. It works inside whatever server your agent already runs (or no server at all, e.g. a CLI or worker). Two runnable proofs are in examples/:

cargo run --example with_actix   # actix-web service on :8090
cargo run --example with_axum    # axum service on :8091
curl -X POST localhost:8090/notify
curl -X POST localhost:8091/notify

Both call cots::agents::AgentClient::guard(...) from inside a request handler and return the real interceptor decision — same crate, same API, either framework.

What this crate does not do

  • No real Slack/SMS/email/payments integrations — your execute closure does that however you like.
  • No retry beyond the approval poll loop — intercept() is a single request; on failure it returns Err.
  • No dependency on any particular agent framework — call guard() wherever your agent decides to act.

License

Licensed under the MIT License. © 2026 Cots.ai.


Made with ❤️ by Srotas Space