# sentryagent-idp — Rust SDK

Production-grade Rust client for the [SentryAgent.ai](https://sentryagent.ai) AgentIdP API. Covers all 14 API endpoints across agent identity, OAuth 2.0 token management, credential rotation, audit logs, the public marketplace, and A2A delegation.

## Features

- Async-first — every API call is `async` and backed by `tokio`
- Thread-safe token cache — `TokenManager` refreshes tokens automatically before expiry
- Typed errors — every failure maps to a variant of `AgentIdPError`
- Zero `unwrap()` in library code — all errors propagated with `?`
- Full `//!` and `///` doc coverage — `cargo doc --no-deps` generates clean docs
- `#![deny(warnings)]` enforced — zero clippy warnings

## Installation

Add to your `Cargo.toml`:

```toml
[dependencies]
sentryagent-idp = "1.0"
tokio = { version = "1", features = ["full"] }
```

## Environment Variables

| Variable | Purpose |
|---|---|
| `AGENTIDP_API_URL` | Base URL of the AgentIdP API (e.g. `https://api.sentryagent.ai`) |
| `AGENTIDP_CLIENT_ID` | OAuth 2.0 client identifier |
| `AGENTIDP_CLIENT_SECRET` | OAuth 2.0 client secret |

## Quickstart

```rust
use sentryagent_idp::{AgentIdPClient, RegisterAgentRequest};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Build from environment variables.
    let client = AgentIdPClient::from_env()?;

    // Register a new agent.
    let agent = client.register_agent(RegisterAgentRequest {
        name: "my-agent".to_owned(),
        description: Some("Does useful things".to_owned()),
        agent_type: "worker".to_owned(),
        capabilities: vec!["read:data".to_owned()],
        metadata: None,
    }).await?;

    println!("Agent registered: {} (DID: {})", agent.id, agent.did);

    // Issue a scoped access token.
    let token = client.issue_token(&agent.id, &["agents:read"]).await?;
    println!("Token issued, expires in {}s", token.expires_in);

    // Clean up.
    client.delete_agent(&agent.id).await?;
    Ok(())
}
```

## Method Reference

### Agent Registry

| Method | Endpoint | Description |
|---|---|---|
| `register_agent(req)` | `POST /agents` | Register a new agent identity |
| `get_agent(id)` | `GET /agents/{id}` | Retrieve an agent by ID |
| `list_agents(page, per_page)` | `GET /agents` | List all agents (paginated) |
| `update_agent(id, req)` | `PATCH /agents/{id}` | Partially update an agent |
| `delete_agent(id)` | `DELETE /agents/{id}` | Permanently delete an agent |

### OAuth 2.0

| Method | Endpoint | Description |
|---|---|---|
| `issue_token(agent_id, scopes)` | `POST /oauth2/token` | Issue a scoped access token |

### Credentials

| Method | Endpoint | Description |
|---|---|---|
| `generate_credentials(agent_id)` | `POST /agents/{id}/credentials` | Generate credentials (returns secret once) |
| `rotate_credentials(agent_id)` | `POST /agents/{id}/credentials/rotate` | Rotate credentials (invalidates previous) |
| `revoke_credentials(agent_id, cred_id)` | `DELETE /agents/{id}/credentials/{cred_id}` | Revoke a specific credential set |

### Audit Logs

| Method | Endpoint | Description |
|---|---|---|
| `list_audit_logs(filters)` | `GET /audit-logs` | Query audit events with optional filters |

### Marketplace (unauthenticated)

| Method | Endpoint | Description |
|---|---|---|
| `list_public_agents(filters)` | `GET /marketplace/agents` | Browse public marketplace agents |
| `get_public_agent(id)` | `GET /marketplace/agents/{id}` | Retrieve a single marketplace agent |

### Delegation

| Method | Endpoint | Description |
|---|---|---|
| `delegate(req)` | `POST /delegation` | Create an A2A delegation token |
| `verify_delegation(token)` | `POST /delegation/verify` | Verify and decode a delegation token |

## Error Handling

All methods return `Result<T, AgentIdPError>`. Match on variants for fine-grained handling:

```rust
use sentryagent_idp::AgentIdPError;

match client.get_agent("unknown-id").await {
    Err(AgentIdPError::NotFound(msg)) => {
        eprintln!("Agent not found: {}", msg);
    }
    Err(AgentIdPError::RateLimited { retry_after_secs }) => {
        eprintln!("Rate limited — retry after {}s", retry_after_secs);
    }
    Err(AgentIdPError::AuthError(msg)) => {
        eprintln!("Authentication failed: {}", msg);
    }
    Err(AgentIdPError::ApiError { status, message, code }) => {
        eprintln!("API error {}: {} (code: {:?})", status, message, code);
    }
    Err(e) => eprintln!("Unexpected error: {}", e),
    Ok(agent) => println!("Found: {}", agent.name),
}
```

### Error Variants

| Variant | Cause |
|---|---|
| `HttpError(reqwest::Error)` | Network-level transport failure |
| `ApiError { status, message, code }` | Non-2xx HTTP response with error body |
| `AuthError(String)` | 401 or 403 — invalid credentials or insufficient scope |
| `NotFound(String)` | 404 — resource does not exist |
| `RateLimited { retry_after_secs }` | 429 — too many requests |
| `ConfigError(String)` | Missing environment variable on `from_env()` |
| `SerdeError(serde_json::Error)` | JSON parsing failure |
| `DelegationError(String)` | Invalid or revoked delegation chain |

## Running Integration Tests

Integration tests are ignored by default. Set the three environment variables and run:

```bash
AGENTIDP_API_URL=https://api.sentryagent.ai \
AGENTIDP_CLIENT_ID=your-client-id \
AGENTIDP_CLIENT_SECRET=your-client-secret \
cargo test -- --ignored
```

## Publishing to crates.io

This crate is published as `sentryagent-idp` version `1.0.0`. To publish a new version:

```bash
# Update version in Cargo.toml, then:
cargo publish --registry crates-io
```

Ensure `CARGO_REGISTRY_TOKEN` is set to a valid crates.io API token before publishing.

## License

MIT — see LICENSE for details.
