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

```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?;

// 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:

```rust
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/`:

```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)