# WS3 — Rust SDK Documentation

**Target file:** `docs/engineering/11-sdk-guide.md`

**Operation:** Append the following complete section to the end of `docs/engineering/11-sdk-guide.md`, after the final line of `## 6. SDK Contribution Guide — Adding a New Endpoint`.

---

## Instructions to Developer

Append the following Markdown verbatim to the end of `docs/engineering/11-sdk-guide.md`. Do not modify any existing content. The new section is `## 6. Rust SDK` (the current section 6 becomes section 7 — renumber it as part of this change).

**Renaming instruction:** Change the existing heading `## 6. SDK Contribution Guide — Adding a New Endpoint` to `## 7. SDK Contribution Guide — Adding a New Endpoint` before appending. The new Rust SDK section takes the `## 6` slot.

---

## Content to Insert (before the existing Section 6, which becomes Section 7)

Insert the following after the Java SDK section (`## 5. Java SDK`) and before the existing contribution guide (which becomes `## 7`):

```markdown
---

## 6. Rust SDK

The Rust SDK (`sdk-rust/`) is a production-grade, async-first client for the SentryAgent.ai AgentIdP API. It provides full coverage of the 14 API endpoints across agent identity, OAuth 2.0 token management, credential rotation, audit logs, the public marketplace, and agent-to-agent (A2A) delegation.

**Requirements:** Rust 1.75+ (stable), `tokio` runtime.

---

### Installation

Add the crate to your `Cargo.toml`:

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

The crate uses `reqwest` with `rustls-tls` (no OpenSSL dependency) and `serde` for JSON serialisation.

---

### Authentication

The Rust SDK uses the OAuth 2.0 Client Credentials grant, managed transparently by `TokenManager`. You never call `TokenManager` directly — it is embedded in `AgentIdPClient` and invoked automatically before every request.

**Token refresh behaviour:**
- The first API call triggers a `POST /oauth2/token` request with `grant_type=client_credentials`.
- The returned token is cached behind an async `tokio::sync::Mutex`.
- Subsequent calls within the token lifetime return the cached token without a network round trip.
- The cache expires 60 seconds before the server-reported `expires_in`, ensuring tokens never expire mid-flight.
- The `Mutex` guarantees only one refresh happens even when many `tokio` tasks call `get_token()` concurrently.

**Environment variable construction:**

```rust
use sentryagent_idp::AgentIdPClient;

// from_env() reads AGENTIDP_API_URL, AGENTIDP_CLIENT_ID, AGENTIDP_CLIENT_SECRET
let client = AgentIdPClient::from_env()?;
```

**Explicit construction:**

```rust
use sentryagent_idp::AgentIdPClient;

let client = AgentIdPClient::new(
    "https://api.sentryagent.ai",
    "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
    "sk_live_...",
);
```

| Environment Variable | Required | Purpose |
|---|---|---|
| `AGENTIDP_API_URL` | Yes | Base URL of the AgentIdP API |
| `AGENTIDP_CLIENT_ID` | Yes | OAuth 2.0 client identifier |
| `AGENTIDP_CLIENT_SECRET` | Yes | OAuth 2.0 client secret |

---

### Complete Working Example

The following example covers the full agent identity lifecycle: register → generate credentials → issue token → retrieve agent → list audit logs → delete agent.

```rust
use sentryagent_idp::{
    AgentIdPClient, AgentIdPError,
    AuditLogFilters, MarketplaceFilters, RegisterAgentRequest,
};

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

    // ── Register a new agent ──────────────────────────────────────────────────
    let agent = client.register_agent(RegisterAgentRequest {
        name: "my-screener-agent".to_owned(),
        description: Some("Screens resumes using ML".to_owned()),
        agent_type: "screener".to_owned(),
        capabilities: vec!["resume:read".to_owned(), "classify".to_owned()],
        metadata: None,
    }).await?;

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

    // ── Generate credentials for the agent ───────────────────────────────────
    let creds = client.generate_credentials(&agent.id).await?;
    println!("Client ID:     {}", creds.client_id);
    println!("Client Secret: {} (store this — shown once)", creds.client_secret);

    // ── Issue a scoped token (TokenManager handles this automatically) ────────
    let token_resp = client.issue_token(&agent.id, &["agents:read", "agents:write"]).await?;
    println!("Token type: {}, expires in {}s", token_resp.token_type, token_resp.expires_in);

    // ── Retrieve the agent ────────────────────────────────────────────────────
    let fetched = client.get_agent(&agent.id).await?;
    println!("Fetched: {} (public: {})", fetched.name, fetched.is_public);

    // ── List agents ───────────────────────────────────────────────────────────
    let list = client.list_agents(Some(1), Some(10)).await?;
    println!("Total agents: {}", list.total);

    // ── Audit logs ────────────────────────────────────────────────────────────
    let logs = client.list_audit_logs(AuditLogFilters {
        agent_id: Some(agent.id.clone()),
        event_type: None,
        from: None,
        to: None,
        page: 1,
        per_page: 10,
    }).await?;
    println!("Audit events: {}", logs.total);

    // ── Rotate credentials ────────────────────────────────────────────────────
    let new_creds = client.rotate_credentials(&agent.id).await?;
    println!("New secret: {}", new_creds.client_secret);

    // ── Delete agent ──────────────────────────────────────────────────────────
    client.delete_agent(&agent.id).await?;
    println!("Agent deleted.");

    Ok(())
}
```

Run the bundled quickstart example directly:

```bash
AGENTIDP_API_URL=http://localhost:3000 \
AGENTIDP_CLIENT_ID=your-client-id \
AGENTIDP_CLIENT_SECRET=your-client-secret \
cargo run --example quickstart
```

---

### Client Methods Reference

All methods are `async` and return `Result<T, AgentIdPError>`. The client is cheap to clone — the inner `reqwest::Client` and token cache are shared via `Arc`.

**Agent Registry** (`sdk-rust/src/agents.rs`):

| Method | Signature | Description |
|--------|-----------|-------------|
| `register_agent` | `(req: RegisterAgentRequest) -> Result<Agent>` | `POST /agents` — 201 |
| `get_agent` | `(agent_id: &str) -> Result<Agent>` | `GET /agents/{id}` — 200 |
| `list_agents` | `(page: Option<u32>, per_page: Option<u32>) -> Result<AgentList>` | `GET /agents` — 200 |
| `update_agent` | `(agent_id: &str, req: UpdateAgentRequest) -> Result<Agent>` | `PATCH /agents/{id}` — 200 |
| `delete_agent` | `(agent_id: &str) -> Result<()>` | `DELETE /agents/{id}` — 204 |

**Credential Management** (`sdk-rust/src/credentials.rs`):

| Method | Signature | Description |
|--------|-----------|-------------|
| `generate_credentials` | `(agent_id: &str) -> Result<Credentials>` | `POST /agents/{id}/credentials` — 201. `client_secret` shown once. |
| `rotate_credentials` | `(agent_id: &str) -> Result<Credentials>` | `POST /agents/{id}/credentials/rotate` — 200. New secret shown once. |
| `revoke_credentials` | `(agent_id: &str, cred_id: &str) -> Result<()>` | `DELETE /agents/{id}/credentials/{cred_id}` — 204 |

**Token Operations** (`sdk-rust/src/oauth2.rs`):

| Method | Signature | Description |
|--------|-----------|-------------|
| `issue_token` | `(agent_id: &str, scopes: &[&str]) -> Result<TokenResponse>` | Issues a scoped Bearer JWT. Token is cached by `TokenManager` automatically. |

**Audit Log** (`sdk-rust/src/audit.rs`):

| Method | Signature | Description |
|--------|-----------|-------------|
| `list_audit_logs` | `(filters: AuditLogFilters) -> Result<AuditLogList>` | Paginated audit log query with optional agent_id, event_type, from, to filters. |

**Marketplace** (`sdk-rust/src/marketplace.rs`):

| Method | Signature | Description |
|--------|-----------|-------------|
| `list_public_agents` | `(filters: MarketplaceFilters) -> Result<MarketplaceAgentList>` | Lists publicly discoverable agents with optional `q`, `capability`, `publisher` filters. |

**A2A Delegation** (`sdk-rust/src/delegation.rs`):

| Method | Signature | Description |
|--------|-----------|-------------|
| `delegate` | `(req: DelegateRequest) -> Result<DelegationToken>` | Creates a delegation chain and returns the delegation JWT. |
| `verify_delegation` | `(token: &str) -> Result<DelegationVerification>` | Verifies a delegation token and returns the verified claims. |

---

### Error Types

All SDK operations return `Result<T, AgentIdPError>`. Match on the enum variants for structured error handling:

```rust
use sentryagent_idp::AgentIdPError;

match client.get_agent("unknown-id").await {
    Ok(agent) => println!("Found: {}", agent.name),
    Err(AgentIdPError::NotFound(msg)) => {
        eprintln!("Agent not found: {}", msg);
    }
    Err(AgentIdPError::AuthError(msg)) => {
        eprintln!("Auth failed: {}", msg);
        // Token may have been revoked — check credentials
    }
    Err(AgentIdPError::RateLimited { retry_after_secs }) => {
        eprintln!("Rate limited — retry after {}s", retry_after_secs);
        tokio::time::sleep(std::time::Duration::from_secs(retry_after_secs)).await;
    }
    Err(AgentIdPError::ApiError { status, message, code }) => {
        eprintln!("API error {}: {} (code: {:?})", status, message, code);
    }
    Err(AgentIdPError::ConfigError(msg)) => {
        // Missing environment variable — fix before running
        eprintln!("Config error: {}", msg);
    }
    Err(AgentIdPError::HttpError(e)) => {
        // reqwest transport error — network issue
        eprintln!("HTTP transport error: {}", e);
    }
    Err(AgentIdPError::SerdeError(e)) => {
        // JSON parse failure — API response shape mismatch
        eprintln!("Serialization error: {}", e);
    }
    Err(AgentIdPError::DelegationError(msg)) => {
        eprintln!("Delegation chain invalid: {}", msg);
    }
}
```

| Variant | Trigger | HTTP status |
|---------|---------|-------------|
| `HttpError(reqwest::Error)` | Network-level failure (connection refused, timeout) | N/A |
| `ApiError { status, message, code }` | Non-2xx response not matching a specific variant | Any non-2xx |
| `AuthError(String)` | 401 or 403 from the API | 401, 403 |
| `NotFound(String)` | 404 from the API | 404 |
| `RateLimited { retry_after_secs }` | 429 — parses `Retry-After` header (defaults to 60s) | 429 |
| `ConfigError(String)` | Missing env var in `from_env()` | N/A |
| `SerdeError(serde_json::Error)` | JSON deserialisation failure | N/A |
| `DelegationError(String)` | Invalid delegation chain | N/A |

---

### Adding a New Endpoint to the Rust SDK

When the AgentIdP server adds a new API endpoint, add it to the Rust SDK using this checklist:

**File structure** (`sdk-rust/src/`):

```
sdk-rust/src/
├── lib.rs           # Crate root — re-exports and module declarations
├── client.rs        # AgentIdPClient struct and new()/from_env() constructors
├── token_manager.rs # TokenManager — async token cache
├── models.rs        # All request/response structs (serde Serialize/Deserialize)
├── error.rs         # AgentIdPError enum
├── agents.rs        # Agent registry methods (impl AgentIdPClient)
├── credentials.rs   # Credential management methods
├── oauth2.rs        # Token issuance methods
├── audit.rs         # Audit log methods
├── marketplace.rs   # Marketplace methods
└── delegation.rs    # A2A delegation methods
```

**Checklist:**

- [ ] Add request/response structs to `models.rs` with `#[derive(Debug, serde::Serialize, serde::Deserialize)]`
- [ ] Add the method to the appropriate `impl AgentIdPClient` block in the relevant `<domain>.rs` file. If the endpoint belongs to a new domain, create a new file and declare it as `pub mod <domain>;` in `lib.rs`
- [ ] Use `self.get_auth_header().await?` for the `Authorization: Bearer` header
- [ ] Use the shared `parse_response::<T>(resp).await` helper (defined in `agents.rs`) to map HTTP status codes to `AgentIdPError` variants
- [ ] Add a doc comment (`///`) to the method with: the HTTP method + path, the success response type, and `# Errors` listing which `AgentIdPError` variants it can return
- [ ] Re-export new public types from `lib.rs` with `pub use models::{NewRequestType, NewResponseType};`
- [ ] Add a unit test using `mockito::Server` (see `token_manager.rs` tests for the pattern)
- [ ] Run `cargo test` and verify all tests pass
- [ ] Run `cargo doc --no-deps --open` and verify the new method appears with correct documentation
- [ ] Verify `cargo clippy -- -D warnings` exits 0
```
