Files
SentryAgent.ai Developer 8fd6823581 chore(openspec): archive phase-5-scale-ecosystem — 68/68 tasks complete
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>
2026-04-03 02:54:45 +00:00

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 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