## 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>`, 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>, } 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; // Agent methods pub async fn register_agent(&self, req: RegisterAgentRequest) -> Result; pub async fn get_agent(&self, agent_id: &str) -> Result; pub async fn list_agents(&self, page: u32, per_page: u32) -> Result; pub async fn update_agent(&self, agent_id: &str, req: UpdateAgentRequest) -> Result; 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; // Credential methods pub async fn generate_credentials(&self, agent_id: &str) -> Result; pub async fn rotate_credentials(&self, agent_id: &str) -> Result; pub async fn revoke_credentials(&self, agent_id: &str) -> Result<(), AgentIdPError>; // Audit log methods pub async fn list_audit_logs(&self, filters: AuditLogFilters) -> Result; // Marketplace methods pub async fn list_public_agents(&self, filters: MarketplaceFilters) -> Result; pub async fn get_public_agent(&self, agent_id: &str) -> Result; // Delegation methods (WS2) pub async fn delegate(&self, req: DelegateRequest) -> Result; pub async fn verify_delegation(&self, token: &str) -> Result; } ``` #### `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>, } struct TokenCache { access_token: Option, expires_at: Option, } 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; } ``` #### `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 }, #[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, pub capabilities: Vec, pub metadata: Option, } pub struct UpdateAgentRequest { pub name: Option, pub description: Option, pub capabilities: Option>, pub is_public: Option, pub metadata: Option, } pub struct AuditLogFilters { pub agent_id: Option, pub event_type: Option, pub from: Option, // ISO 8601 pub to: Option, // ISO 8601 pub page: u32, pub per_page: u32, } pub struct MarketplaceFilters { pub q: Option, pub capability: Option, pub publisher: Option, pub page: u32, pub per_page: u32, } pub struct DelegateRequest { pub delegatee_agent_id: String, pub scopes: Vec, pub ttl_seconds: u64, } // Response types pub struct Agent { pub id: String, pub name: String, pub description: Option, pub capabilities: Vec, pub did: String, pub is_public: bool, pub created_at: String, pub updated_at: String, } pub struct AgentList { pub agents: Vec, 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, pub total: u64, pub page: u32, pub per_page: u32, } pub struct MarketplaceAgent { pub id: String, pub name: String, pub description: Option, pub capabilities: Vec, pub did_document: serde_json::Value, pub publisher: String, pub created_at: String, } pub struct MarketplaceAgentList { pub agents: Vec, 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, 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`