## WS1: Rust SDK

### Purpose

Deliver a production-grade, idiomatic Rust SDK for SentryAgent.ai AgentIdP. The SDK covers all 14 API endpoints, provides a thread-safe `TokenManager` with automatic token refresh, uses `async/await` throughout via `tokio`, and models all errors as a typed `AgentIdPError` enum. Rust developers building high-performance or safety-critical AI agents can integrate with SentryAgent.ai without writing HTTP boilerplate.

The SDK is published to crates.io as `sentryagent-idp`. It mirrors the API surface of the Go SDK (the most recently authored and cleanest SDK) to reduce cognitive load for polyglot teams.

### New Files to Create

| File | Description |
|---|---|
| `sdk-rust/Cargo.toml` | Crate manifest — name: `sentryagent-idp`, edition: 2021 |
| `sdk-rust/src/lib.rs` | Crate root — re-exports `AgentIdPClient`, `TokenManager`, `AgentIdPError`, all model types |
| `sdk-rust/src/client.rs` | `AgentIdPClient` struct — wraps `reqwest::Client`, holds base URL + credentials |
| `sdk-rust/src/token_manager.rs` | `TokenManager` struct — `Arc<Mutex<TokenCache>>`, auto-refresh logic |
| `sdk-rust/src/error.rs` | `AgentIdPError` enum — all typed error variants, implements `std::error::Error` |
| `sdk-rust/src/models.rs` | All request/response model structs — serde Serialize/Deserialize |
| `sdk-rust/src/agents.rs` | Agent CRUD methods on `AgentIdPClient` |
| `sdk-rust/src/oauth2.rs` | Token issuance and refresh methods |
| `sdk-rust/src/credentials.rs` | Credential management methods |
| `sdk-rust/src/audit.rs` | Audit log query methods |
| `sdk-rust/src/marketplace.rs` | Marketplace listing and detail methods |
| `sdk-rust/src/delegation.rs` | A2A delegation methods (WS2 integration) |
| `sdk-rust/examples/quickstart.rs` | Working quickstart example — register agent, issue token, make authenticated call |
| `sdk-rust/README.md` | Installation, configuration, quickstart, all methods with examples |
| `sdk-rust/tests/integration_test.rs` | Integration tests against a real API instance (reads `AGENTIDP_API_URL` env var) |

### Cargo.toml Dependencies

```toml
[dependencies]
tokio = { version = "1.35", features = ["full"] }
reqwest = { version = "0.11", features = ["json", "rustls-tls"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = { version = "1.6", features = ["v4"] }
thiserror = "1.0"
async-trait = "0.1"

[dev-dependencies]
tokio-test = "0.4"
mockito = "1.2"
```

### Public API Surface

#### `AgentIdPClient`

```rust
pub struct AgentIdPClient {
    base_url: String,
    client_id: String,
    client_secret: String,
    http: reqwest::Client,
    token_manager: Arc<Mutex<TokenManager>>,
}

impl AgentIdPClient {
    /// Create a new client. Does not make any network calls at construction time.
    pub fn new(base_url: &str, client_id: &str, client_secret: &str) -> Self;

    /// Create a client from environment variables:
    /// AGENTIDP_API_URL, AGENTIDP_CLIENT_ID, AGENTIDP_CLIENT_SECRET
    pub fn from_env() -> Result<Self, AgentIdPError>;

    // Agent methods
    pub async fn register_agent(&self, req: RegisterAgentRequest) -> Result<Agent, AgentIdPError>;
    pub async fn get_agent(&self, agent_id: &str) -> Result<Agent, AgentIdPError>;
    pub async fn list_agents(&self, page: u32, per_page: u32) -> Result<AgentList, AgentIdPError>;
    pub async fn update_agent(&self, agent_id: &str, req: UpdateAgentRequest) -> Result<Agent, AgentIdPError>;
    pub async fn delete_agent(&self, agent_id: &str) -> Result<(), AgentIdPError>;

    // OAuth2 token methods
    pub async fn issue_token(&self, agent_id: &str, scopes: &[&str]) -> Result<TokenResponse, AgentIdPError>;

    // Credential methods
    pub async fn generate_credentials(&self, agent_id: &str) -> Result<Credentials, AgentIdPError>;
    pub async fn rotate_credentials(&self, agent_id: &str) -> Result<Credentials, AgentIdPError>;
    pub async fn revoke_credentials(&self, agent_id: &str) -> Result<(), AgentIdPError>;

    // Audit log methods
    pub async fn list_audit_logs(&self, filters: AuditLogFilters) -> Result<AuditLogList, AgentIdPError>;

    // Marketplace methods
    pub async fn list_public_agents(&self, filters: MarketplaceFilters) -> Result<MarketplaceAgentList, AgentIdPError>;
    pub async fn get_public_agent(&self, agent_id: &str) -> Result<MarketplaceAgent, AgentIdPError>;

    // Delegation methods (WS2)
    pub async fn delegate(&self, req: DelegateRequest) -> Result<DelegationToken, AgentIdPError>;
    pub async fn verify_delegation(&self, token: &str) -> Result<DelegationVerification, AgentIdPError>;
}
```

#### `TokenManager`

```rust
/// Thread-safe token cache with automatic refresh.
/// Holds the current access token and its expiry.
/// Re-issues a token when it is within 60 seconds of expiry.
pub struct TokenManager {
    client_id: String,
    client_secret: String,
    api_url: String,
    cache: Arc<Mutex<TokenCache>>,
}

struct TokenCache {
    access_token: Option<String>,
    expires_at: Option<std::time::Instant>,
}

impl TokenManager {
    pub fn new(api_url: &str, client_id: &str, client_secret: &str) -> Self;

    /// Returns a valid access token. Refreshes automatically if expired or within 60s of expiry.
    pub async fn get_token(&self) -> Result<String, AgentIdPError>;
}
```

#### `AgentIdPError`

```rust
#[derive(Debug, thiserror::Error)]
pub enum AgentIdPError {
    #[error("HTTP request failed: {0}")]
    HttpError(#[from] reqwest::Error),

    #[error("API error {status}: {message}")]
    ApiError { status: u16, message: String, code: Option<String> },

    #[error("Authentication failed: {0}")]
    AuthError(String),

    #[error("Agent not found: {0}")]
    NotFound(String),

    #[error("Rate limit exceeded. Retry after {retry_after_secs}s")]
    RateLimited { retry_after_secs: u64 },

    #[error("Invalid configuration: {0}")]
    ConfigError(String),

    #[error("Serialization error: {0}")]
    SerdeError(#[from] serde_json::Error),

    #[error("Delegation chain invalid: {0}")]
    DelegationError(String),
}
```

### Model Structs (complete — no placeholders)

```rust
// Request types
pub struct RegisterAgentRequest {
    pub name: String,
    pub description: Option<String>,
    pub capabilities: Vec<String>,
    pub metadata: Option<serde_json::Value>,
}

pub struct UpdateAgentRequest {
    pub name: Option<String>,
    pub description: Option<String>,
    pub capabilities: Option<Vec<String>>,
    pub is_public: Option<bool>,
    pub metadata: Option<serde_json::Value>,
}

pub struct AuditLogFilters {
    pub agent_id: Option<String>,
    pub event_type: Option<String>,
    pub from: Option<String>,   // ISO 8601
    pub to: Option<String>,     // ISO 8601
    pub page: u32,
    pub per_page: u32,
}

pub struct MarketplaceFilters {
    pub q: Option<String>,
    pub capability: Option<String>,
    pub publisher: Option<String>,
    pub page: u32,
    pub per_page: u32,
}

pub struct DelegateRequest {
    pub delegatee_agent_id: String,
    pub scopes: Vec<String>,
    pub ttl_seconds: u64,
}

// Response types
pub struct Agent {
    pub id: String,
    pub name: String,
    pub description: Option<String>,
    pub capabilities: Vec<String>,
    pub did: String,
    pub is_public: bool,
    pub created_at: String,
    pub updated_at: String,
}

pub struct AgentList {
    pub agents: Vec<Agent>,
    pub total: u64,
    pub page: u32,
    pub per_page: u32,
}

pub struct TokenResponse {
    pub access_token: String,
    pub token_type: String,
    pub expires_in: u64,
    pub scope: String,
}

pub struct Credentials {
    pub client_id: String,
    pub client_secret: String,  // Only present on generate/rotate — never on read
    pub created_at: String,
}

pub struct AuditLogEntry {
    pub id: String,
    pub agent_id: String,
    pub event_type: String,
    pub actor: String,
    pub metadata: serde_json::Value,
    pub timestamp: String,
}

pub struct AuditLogList {
    pub entries: Vec<AuditLogEntry>,
    pub total: u64,
    pub page: u32,
    pub per_page: u32,
}

pub struct MarketplaceAgent {
    pub id: String,
    pub name: String,
    pub description: Option<String>,
    pub capabilities: Vec<String>,
    pub did_document: serde_json::Value,
    pub publisher: String,
    pub created_at: String,
}

pub struct MarketplaceAgentList {
    pub agents: Vec<MarketplaceAgent>,
    pub total: u64,
    pub page: u32,
    pub per_page: u32,
}

pub struct DelegationToken {
    pub delegation_token: String,
    pub chain_id: String,
    pub expires_at: String,
}

pub struct DelegationVerification {
    pub valid: bool,
    pub chain_id: String,
    pub delegator_agent_id: String,
    pub delegatee_agent_id: String,
    pub scopes: Vec<String>,
    pub expires_at: String,
}
```

### Database Schema Changes

None. The Rust SDK is a client library — it makes HTTP calls to the existing API. No database changes are required for WS1.

### Acceptance Criteria

- `cargo build` passes with zero warnings (deny warnings enforced via `#![deny(warnings)]` in `lib.rs`)
- `cargo clippy` passes with zero warnings
- `cargo test` runs all unit tests — all pass
- Integration tests pass against a live API instance when `AGENTIDP_API_URL`, `AGENTIDP_CLIENT_ID`, `AGENTIDP_CLIENT_SECRET` are set
- `TokenManager::get_token()` is thread-safe: concurrent calls from multiple `tokio` tasks do not produce race conditions (verified by a concurrent-call test with 50 parallel futures)
- Zero `unwrap()` calls in `src/` (only in `examples/` and `tests/` where panicking is acceptable)
- All public items have `///` doc comments
- `cargo doc --no-deps` generates docs without errors
- Published to crates.io as `sentryagent-idp` version `1.0.0`
