WS1 (Rust SDK), WS2 (A2A Authorization), WS5 (Developer Experience) all delivered, QA gates passed, committed to main. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
9.5 KiB
9.5 KiB
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
[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
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
/// 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
#[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)
// 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 buildpasses with zero warnings (deny warnings enforced via#![deny(warnings)]inlib.rs)cargo clippypasses with zero warningscargo testruns all unit tests — all pass- Integration tests pass against a live API instance when
AGENTIDP_API_URL,AGENTIDP_CLIENT_ID,AGENTIDP_CLIENT_SECRETare set TokenManager::get_token()is thread-safe: concurrent calls from multipletokiotasks do not produce race conditions (verified by a concurrent-call test with 50 parallel futures)- Zero
unwrap()calls insrc/(only inexamples/andtests/where panicking is acceptable) - All public items have
///doc comments cargo doc --no-depsgenerates docs without errors- Published to crates.io as
sentryagent-idpversion1.0.0