# 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
```bash
cargo add cots
```
Or add it to your `Cargo.toml` directly:
```toml
[dependencies]
cots = "0.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
```
## Configure
```rust
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):
```rust
let agent = AgentClient::new(AgentConfig::from_env()?);
```
## Use it — `guard()` is the one method most agents need
```rust
use cots::agents::NormalizedAction;
let result = agent
.guard(NormalizedAction::new("Slack", "send_slack_message"), || async {
// send the real Slack message here
"sent"
})
.await?;
```
`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:
```rust
let reg = agent.control_plane.register_tenant("Srotas Space Pvt Ltd").await?;
let created = agent.control_plane.register_agent(®.tenant.tenant_id, "Ops Agent").await?;
agent.control_plane
.create_action_surface(®.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/`:
```bash
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](./LICENSE). © 2026 Cots.ai.
---
Made with ❤️ by [Srotas Space](https://open-source.srotas.space)