Files
sentryagent-idp/openspec/changes/phase-5-scale-ecosystem/specs/rust-sdk/spec.md
SentryAgent.ai Developer 389a764e8d feat(openspec): propose phase-5-scale-ecosystem change
6 workstreams, 119 tasks — Scale & Ecosystem:
- WS1: Rust SDK
- WS2: Agent-to-Agent (A2A) Authorization
- WS3: Advanced Analytics Dashboard
- WS4: Public API Gateway & Rate Limiting SaaS
- WS5: Developer Experience (DX) improvements
- WS6: AGNTCY Compliance Certification Package

Awaiting CEO approval to begin implementation.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 15:33:08 +00:00

290 lines
9.5 KiB
Markdown

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