- devops docs: 8 files updated for Phase 6 state; field-trial.md added (946-line runbook) - developer docs: api-reference (50+ endpoints), quick-start, 5 existing guides updated, 5 new guides added - engineering docs: all 12 files updated (services, architecture, SDK guide, testing, overview) - OpenSpec archives: phase-7-devops-field-trial, developer-docs-phase6-update, engineering-docs-phase6-update - VALIDATOR.md + scripts/start-validator.sh: V&V Architect tooling added - .gitignore: exclude session artifacts, build artifacts, and agent workspaces Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
736 lines
26 KiB
Markdown
736 lines
26 KiB
Markdown
# 11 — SDK Integration Guide
|
|
|
|
AgentIdP ships four official client SDKs — Node.js, Python, Go, and Java. All four expose an identical API surface, handle OAuth 2.0 token acquisition automatically, and throw typed errors. This document covers installation, complete working examples, error handling, and the contribution guide for adding new endpoints.
|
|
|
|
---
|
|
|
|
## 1. SDK Architecture Overview
|
|
|
|
Every SDK composes the same four service clients:
|
|
|
|
| Service client | Node.js | Python | Go | Java |
|
|
|---------------|---------|--------|----|------|
|
|
| Agent Registry | `AgentRegistryClient` | `AgentRegistryClient` | `AgentsClient` | `AgentServiceClient` |
|
|
| Credential Management | `CredentialClient` | `CredentialClient` | `CredentialsClient` | `CredentialServiceClient` |
|
|
| Token Operations | `TokenClient` | `TokenClient` | `TokenServiceClient` | `TokenServiceClient` |
|
|
| Audit Log | `AuditClient` | `AuditClient` | `AuditClient` | `AuditServiceClient` |
|
|
|
|
All four SDKs also implement:
|
|
|
|
- **`AgentIdPClient`** — the top-level client that composes all four service clients and wires them to a shared `TokenManager`.
|
|
- **`TokenManager`** — fetches and caches the OAuth 2.0 access token. Automatically requests a new token when the cached one is within 60 seconds of expiry. Thread-safe / goroutine-safe.
|
|
- **Typed error class** — `AgentIdPError` (Node.js, Python, Go) or `AgentIdPException` (Java) — with `code`, `httpStatus`, and `details` fields.
|
|
|
|
This consistency is a maintained standard. When a new API endpoint is added to the server, it must be added to all four SDKs simultaneously.
|
|
|
|
---
|
|
|
|
## 2. Node.js SDK
|
|
|
|
**Install:**
|
|
|
|
```bash
|
|
npm install @sentryagent/idp-sdk
|
|
```
|
|
|
|
**Requirements:** Node.js 18+ (uses native `fetch`).
|
|
|
|
**Complete example:**
|
|
|
|
```typescript
|
|
import { AgentIdPClient, AgentIdPError } from '@sentryagent/idp-sdk';
|
|
|
|
const client = new AgentIdPClient({
|
|
baseUrl: 'http://localhost:3000',
|
|
clientId: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
|
clientSecret: 'sk_live_...',
|
|
// Optional: restrict scopes. Defaults to all four.
|
|
// scopes: ['agents:read', 'tokens:read'],
|
|
});
|
|
|
|
// Register a new agent
|
|
const agent = await client.agents.registerAgent({
|
|
email: 'classifier-v2@myorg.ai',
|
|
agentType: 'classifier',
|
|
version: '2.0.0',
|
|
capabilities: ['resume:read', 'classify'],
|
|
owner: 'platform-team',
|
|
deploymentEnv: 'production',
|
|
});
|
|
console.log('Registered:', agent.agentId);
|
|
|
|
// List active agents (token acquired automatically)
|
|
const { data: agents } = await client.agents.listAgents({ status: 'active' });
|
|
console.log('Active agents:', agents.length);
|
|
|
|
// Generate credentials for an agent
|
|
const cred = await client.credentials.generateCredential(agent.agentId);
|
|
console.log('Client secret (store this — shown once):', cred.clientSecret);
|
|
|
|
// Rotate credentials
|
|
const newCred = await client.credentials.rotateCredential(agent.agentId, cred.credentialId);
|
|
console.log('New secret:', newCred.clientSecret);
|
|
|
|
// Introspect a token
|
|
const introspection = await client.tokens.introspectToken('eyJ...');
|
|
console.log('Active:', introspection.active);
|
|
|
|
// Error handling
|
|
try {
|
|
await client.agents.getAgent('non-existent-id');
|
|
} catch (err) {
|
|
if (err instanceof AgentIdPError) {
|
|
console.error(err.code); // e.g. AGENT_NOT_FOUND
|
|
console.error(err.httpStatus); // e.g. 404
|
|
console.error(err.details); // optional structured context
|
|
}
|
|
}
|
|
|
|
// Force a fresh token on the next call (e.g. after credential rotation)
|
|
client.clearTokenCache();
|
|
```
|
|
|
|
**Token manager behaviour:** `TokenManager` in `sdk/src/token-manager.ts` caches the token and requests a new one when fewer than 60 seconds remain before expiry.
|
|
|
|
**Service clients are accessible at:**
|
|
- `client.agents` — `AgentRegistryClient` (register, list, get, update, decommission)
|
|
- `client.credentials` — `CredentialClient` (generate, list, rotate, revoke)
|
|
- `client.tokens` — `TokenClient` (introspect, revoke)
|
|
- `client.audit` — `AuditClient` (query, get event)
|
|
|
|
---
|
|
|
|
## 3. Python SDK
|
|
|
|
**Install:**
|
|
|
|
```bash
|
|
pip install sentryagent-idp
|
|
```
|
|
|
|
**Requirements:** Python 3.9+. Synchronous client uses `requests`; asynchronous client uses `httpx`.
|
|
|
|
### Synchronous example
|
|
|
|
```python
|
|
from sentryagent_idp import AgentIdPClient, AgentIdPError, RegisterAgentRequest
|
|
|
|
client = AgentIdPClient(
|
|
base_url="http://localhost:3000",
|
|
client_id="a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
client_secret="sk_live_...",
|
|
# scopes=["agents:read", "tokens:read"], # optional
|
|
)
|
|
|
|
# Register an agent
|
|
agent = client.agents.register_agent(RegisterAgentRequest(
|
|
email="screener@myorg.ai",
|
|
agent_type="screener",
|
|
version="1.0.0",
|
|
capabilities=["resume:read"],
|
|
owner="recruiting-team",
|
|
deployment_env="production",
|
|
))
|
|
print("Registered:", agent.agent_id)
|
|
|
|
# List agents
|
|
result = client.agents.list_agents(status="active", page=1, limit=20)
|
|
for a in result.data:
|
|
print(a.agent_id, a.status)
|
|
|
|
# Generate credentials
|
|
cred = client.credentials.generate_credential(agent.agent_id)
|
|
print("Client secret (shown once):", cred.client_secret)
|
|
|
|
# Error handling
|
|
try:
|
|
client.agents.get_agent("non-existent-id")
|
|
except AgentIdPError as e:
|
|
print(e.code) # e.g. AGENT_NOT_FOUND
|
|
print(e.http_status) # e.g. 404
|
|
print(e.details) # optional dict
|
|
```
|
|
|
|
### Asynchronous example
|
|
|
|
```python
|
|
import asyncio
|
|
from sentryagent_idp import AsyncAgentIdPClient, AgentIdPError
|
|
|
|
async def main() -> None:
|
|
client = AsyncAgentIdPClient(
|
|
base_url="http://localhost:3000",
|
|
client_id="a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
client_secret="sk_live_...",
|
|
)
|
|
|
|
result = await client.agents.list_agents(status="active")
|
|
print(f"Found {result.total} active agents")
|
|
|
|
# Rotate a credential
|
|
new_cred = await client.credentials.rotate_credential(
|
|
"agent-uuid", "credential-uuid"
|
|
)
|
|
print("New secret:", new_cred.client_secret)
|
|
|
|
asyncio.run(main())
|
|
```
|
|
|
|
`AsyncAgentIdPClient` uses an `AsyncTokenManager` backed by `httpx.AsyncClient`. Both sync and async clients are available from the `sentryagent_idp` top-level package.
|
|
|
|
---
|
|
|
|
## 4. Go SDK
|
|
|
|
**Install:**
|
|
|
|
```bash
|
|
go get github.com/sentryagent/idp-sdk-go
|
|
```
|
|
|
|
**Requirements:** Go 1.21+.
|
|
|
|
**Complete example:**
|
|
|
|
```go
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
|
|
agentidp "github.com/sentryagent/idp-sdk-go"
|
|
)
|
|
|
|
func main() {
|
|
ctx := context.Background()
|
|
|
|
client := agentidp.NewAgentIdPClient(agentidp.AgentIdPClientConfig{
|
|
BaseURL: "http://localhost:3000",
|
|
ClientID: "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
ClientSecret: "sk_live_...",
|
|
// Scope: "agents:read agents:write", // optional
|
|
})
|
|
|
|
// Register an agent
|
|
agent, err := client.Agents.RegisterAgent(ctx, agentidp.RegisterAgentRequest{
|
|
Email: "screener@myorg.ai",
|
|
AgentType: "screener",
|
|
Version: "1.0.0",
|
|
Capabilities: []string{"resume:read"},
|
|
Owner: "recruiting-team",
|
|
DeploymentEnv: "production",
|
|
})
|
|
if err != nil {
|
|
// Type-assert for structured error information
|
|
var idpErr *agentidp.AgentIdPError
|
|
if errors.As(err, &idpErr) {
|
|
log.Fatalf("API error: code=%s status=%d", idpErr.Code, idpErr.HTTPStatus)
|
|
}
|
|
log.Fatal(err)
|
|
}
|
|
fmt.Println("Registered:", agent.AgentID)
|
|
|
|
// List agents with filters
|
|
list, err := client.Agents.ListAgents(ctx, &agentidp.ListAgentsParams{
|
|
Status: "active",
|
|
Page: 1,
|
|
Limit: 20,
|
|
})
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
fmt.Printf("Found %d agents\n", list.Total)
|
|
|
|
// Generate credentials
|
|
cred, err := client.Credentials.GenerateCredential(ctx, agent.AgentID, nil)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
fmt.Println("Client secret (shown once):", cred.ClientSecret)
|
|
|
|
// Rotate credentials
|
|
newCred, err := client.Credentials.RotateCredential(ctx, agent.AgentID, cred.CredentialID, nil)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
fmt.Println("New secret:", newCred.ClientSecret)
|
|
}
|
|
```
|
|
|
|
`context.Context` is the first parameter of every method — use `context.Background()` for simple cases or a derived context with deadline/cancellation for production code. The `TokenManager` is goroutine-safe and the client is safe for concurrent use.
|
|
|
|
---
|
|
|
|
## 5. Java SDK
|
|
|
|
**Maven dependency:**
|
|
|
|
```xml
|
|
<dependency>
|
|
<groupId>ai.sentryagent</groupId>
|
|
<artifactId>idp-sdk</artifactId>
|
|
<version>1.0.0</version>
|
|
</dependency>
|
|
```
|
|
|
|
**Gradle:**
|
|
|
|
```groovy
|
|
implementation 'ai.sentryagent:idp-sdk:1.0.0'
|
|
```
|
|
|
|
**Requirements:** Java 17+.
|
|
|
|
### Synchronous example
|
|
|
|
```java
|
|
import ai.sentryagent.idp.AgentIdPClient;
|
|
import ai.sentryagent.idp.AgentIdPException;
|
|
import ai.sentryagent.idp.models.*;
|
|
|
|
// Builder pattern — scope is optional (defaults to all four scopes)
|
|
AgentIdPClient client = new AgentIdPClient(
|
|
"http://localhost:3000",
|
|
"a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
"sk_live_..."
|
|
);
|
|
|
|
// Register an agent
|
|
Agent agent = client.agents().registerAgent(
|
|
RegisterAgentRequest.builder()
|
|
.email("screener@myorg.ai")
|
|
.agentType("screener")
|
|
.version("1.0.0")
|
|
.capabilities(List.of("resume:read"))
|
|
.owner("recruiting-team")
|
|
.deploymentEnv("production")
|
|
.build()
|
|
);
|
|
System.out.println("Registered: " + agent.getAgentId());
|
|
|
|
// List agents
|
|
PaginatedAgents result = client.agents().listAgents(
|
|
ListAgentsParams.builder().status("active").page(1).limit(20).build()
|
|
);
|
|
System.out.println("Total: " + result.getTotal());
|
|
|
|
// Generate credentials
|
|
CredentialWithSecret cred = client.credentials().generateCredential(agent.getAgentId());
|
|
System.out.println("Client secret (shown once): " + cred.getClientSecret());
|
|
|
|
// Rotate credentials
|
|
CredentialWithSecret newCred = client.credentials().rotateCredential(
|
|
agent.getAgentId(), cred.getCredentialId()
|
|
);
|
|
System.out.println("New secret: " + newCred.getClientSecret());
|
|
|
|
// Error handling
|
|
try {
|
|
client.agents().getAgent("non-existent-id");
|
|
} catch (AgentIdPException ex) {
|
|
System.out.printf("code=%s status=%d%n", ex.getCode(), ex.getHttpStatus());
|
|
// e.g. code=AGENT_NOT_FOUND status=404
|
|
}
|
|
```
|
|
|
|
### Async example (CompletableFuture)
|
|
|
|
```java
|
|
import java.util.concurrent.CompletableFuture;
|
|
|
|
// Every sync method has an async counterpart
|
|
CompletableFuture<Agent> future = client.agents().getAgentAsync("agent-uuid");
|
|
future.thenAccept(a -> System.out.println(a.getAgentId()));
|
|
|
|
// Compose multiple async calls
|
|
client.agents().getAgentAsync("agent-uuid")
|
|
.thenCompose(a -> client.credentials().generateCredentialAsync(a.getAgentId()))
|
|
.thenAccept(cred -> System.out.println("New secret: " + cred.getClientSecret()))
|
|
.exceptionally(ex -> {
|
|
if (ex.getCause() instanceof AgentIdPException idpEx) {
|
|
System.err.printf("code=%s%n", idpEx.getCode());
|
|
}
|
|
return null;
|
|
});
|
|
```
|
|
|
|
The `TokenManager` is thread-safe. `AgentIdPClient` is safe for concurrent use from multiple threads.
|
|
|
|
---
|
|
|
|
## 6. Rust SDK
|
|
|
|
The Rust SDK (`sdk-rust/`) is a production-grade, async-first client for the SentryAgent.ai AgentIdP API. It provides full coverage of the 14 API endpoints across agent identity, OAuth 2.0 token management, credential rotation, audit logs, the public marketplace, and agent-to-agent (A2A) delegation.
|
|
|
|
**Requirements:** Rust 1.75+ (stable), `tokio` runtime.
|
|
|
|
---
|
|
|
|
### Installation
|
|
|
|
Add the crate to your `Cargo.toml`:
|
|
|
|
```toml
|
|
[dependencies]
|
|
sentryagent-idp = "1.0"
|
|
tokio = { version = "1.35", features = ["full"] }
|
|
```
|
|
|
|
The crate uses `reqwest` with `rustls-tls` (no OpenSSL dependency) and `serde` for JSON serialisation.
|
|
|
|
---
|
|
|
|
### Authentication
|
|
|
|
The Rust SDK uses the OAuth 2.0 Client Credentials grant, managed transparently by `TokenManager`. You never call `TokenManager` directly — it is embedded in `AgentIdPClient` and invoked automatically before every request.
|
|
|
|
**Token refresh behaviour:**
|
|
- The first API call triggers a `POST /oauth2/token` request with `grant_type=client_credentials`.
|
|
- The returned token is cached behind an async `tokio::sync::Mutex`.
|
|
- Subsequent calls within the token lifetime return the cached token without a network round trip.
|
|
- The cache expires 60 seconds before the server-reported `expires_in`, ensuring tokens never expire mid-flight.
|
|
- The `Mutex` guarantees only one refresh happens even when many `tokio` tasks call `get_token()` concurrently.
|
|
|
|
**Environment variable construction:**
|
|
|
|
```rust
|
|
use sentryagent_idp::AgentIdPClient;
|
|
|
|
// from_env() reads AGENTIDP_API_URL, AGENTIDP_CLIENT_ID, AGENTIDP_CLIENT_SECRET
|
|
let client = AgentIdPClient::from_env()?;
|
|
```
|
|
|
|
**Explicit construction:**
|
|
|
|
```rust
|
|
use sentryagent_idp::AgentIdPClient;
|
|
|
|
let client = AgentIdPClient::new(
|
|
"https://api.sentryagent.ai",
|
|
"a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
|
"sk_live_...",
|
|
);
|
|
```
|
|
|
|
| Environment Variable | Required | Purpose |
|
|
|---|---|---|
|
|
| `AGENTIDP_API_URL` | Yes | Base URL of the AgentIdP API |
|
|
| `AGENTIDP_CLIENT_ID` | Yes | OAuth 2.0 client identifier |
|
|
| `AGENTIDP_CLIENT_SECRET` | Yes | OAuth 2.0 client secret |
|
|
|
|
---
|
|
|
|
### Complete Working Example
|
|
|
|
The following example covers the full agent identity lifecycle: register → generate credentials → issue token → retrieve agent → list audit logs → delete agent.
|
|
|
|
```rust
|
|
use sentryagent_idp::{
|
|
AgentIdPClient, AgentIdPError,
|
|
AuditLogFilters, MarketplaceFilters, RegisterAgentRequest,
|
|
};
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
// Build client from environment variables.
|
|
// Requires: AGENTIDP_API_URL, AGENTIDP_CLIENT_ID, AGENTIDP_CLIENT_SECRET
|
|
let client = AgentIdPClient::from_env()?;
|
|
|
|
// ── Register a new agent ──────────────────────────────────────────────────
|
|
let agent = client.register_agent(RegisterAgentRequest {
|
|
name: "my-screener-agent".to_owned(),
|
|
description: Some("Screens resumes using ML".to_owned()),
|
|
agent_type: "screener".to_owned(),
|
|
capabilities: vec!["resume:read".to_owned(), "classify".to_owned()],
|
|
metadata: None,
|
|
}).await?;
|
|
|
|
println!("Registered: {} (DID: {})", agent.id, agent.did);
|
|
|
|
// ── Generate credentials for the agent ───────────────────────────────────
|
|
let creds = client.generate_credentials(&agent.id).await?;
|
|
println!("Client ID: {}", creds.client_id);
|
|
println!("Client Secret: {} (store this — shown once)", creds.client_secret);
|
|
|
|
// ── Issue a scoped token (TokenManager handles this automatically) ────────
|
|
let token_resp = client.issue_token(&agent.id, &["agents:read", "agents:write"]).await?;
|
|
println!("Token type: {}, expires in {}s", token_resp.token_type, token_resp.expires_in);
|
|
|
|
// ── Retrieve the agent ────────────────────────────────────────────────────
|
|
let fetched = client.get_agent(&agent.id).await?;
|
|
println!("Fetched: {} (public: {})", fetched.name, fetched.is_public);
|
|
|
|
// ── List agents ───────────────────────────────────────────────────────────
|
|
let list = client.list_agents(Some(1), Some(10)).await?;
|
|
println!("Total agents: {}", list.total);
|
|
|
|
// ── Audit logs ────────────────────────────────────────────────────────────
|
|
let logs = client.list_audit_logs(AuditLogFilters {
|
|
agent_id: Some(agent.id.clone()),
|
|
event_type: None,
|
|
from: None,
|
|
to: None,
|
|
page: 1,
|
|
per_page: 10,
|
|
}).await?;
|
|
println!("Audit events: {}", logs.total);
|
|
|
|
// ── Rotate credentials ────────────────────────────────────────────────────
|
|
let new_creds = client.rotate_credentials(&agent.id).await?;
|
|
println!("New secret: {}", new_creds.client_secret);
|
|
|
|
// ── Delete agent ──────────────────────────────────────────────────────────
|
|
client.delete_agent(&agent.id).await?;
|
|
println!("Agent deleted.");
|
|
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
Run the bundled quickstart example directly:
|
|
|
|
```bash
|
|
AGENTIDP_API_URL=http://localhost:3000 \
|
|
AGENTIDP_CLIENT_ID=your-client-id \
|
|
AGENTIDP_CLIENT_SECRET=your-client-secret \
|
|
cargo run --example quickstart
|
|
```
|
|
|
|
---
|
|
|
|
### Client Methods Reference
|
|
|
|
All methods are `async` and return `Result<T, AgentIdPError>`. The client is cheap to clone — the inner `reqwest::Client` and token cache are shared via `Arc`.
|
|
|
|
**Agent Registry** (`sdk-rust/src/agents.rs`):
|
|
|
|
| Method | Signature | Description |
|
|
|--------|-----------|-------------|
|
|
| `register_agent` | `(req: RegisterAgentRequest) -> Result<Agent>` | `POST /agents` — 201 |
|
|
| `get_agent` | `(agent_id: &str) -> Result<Agent>` | `GET /agents/{id}` — 200 |
|
|
| `list_agents` | `(page: Option<u32>, per_page: Option<u32>) -> Result<AgentList>` | `GET /agents` — 200 |
|
|
| `update_agent` | `(agent_id: &str, req: UpdateAgentRequest) -> Result<Agent>` | `PATCH /agents/{id}` — 200 |
|
|
| `delete_agent` | `(agent_id: &str) -> Result<()>` | `DELETE /agents/{id}` — 204 |
|
|
|
|
**Credential Management** (`sdk-rust/src/credentials.rs`):
|
|
|
|
| Method | Signature | Description |
|
|
|--------|-----------|-------------|
|
|
| `generate_credentials` | `(agent_id: &str) -> Result<Credentials>` | `POST /agents/{id}/credentials` — 201. `client_secret` shown once. |
|
|
| `rotate_credentials` | `(agent_id: &str) -> Result<Credentials>` | `POST /agents/{id}/credentials/rotate` — 200. New secret shown once. |
|
|
| `revoke_credentials` | `(agent_id: &str, cred_id: &str) -> Result<()>` | `DELETE /agents/{id}/credentials/{cred_id}` — 204 |
|
|
|
|
**Token Operations** (`sdk-rust/src/oauth2.rs`):
|
|
|
|
| Method | Signature | Description |
|
|
|--------|-----------|-------------|
|
|
| `issue_token` | `(agent_id: &str, scopes: &[&str]) -> Result<TokenResponse>` | Issues a scoped Bearer JWT. Token is cached by `TokenManager` automatically. |
|
|
|
|
**Audit Log** (`sdk-rust/src/audit.rs`):
|
|
|
|
| Method | Signature | Description |
|
|
|--------|-----------|-------------|
|
|
| `list_audit_logs` | `(filters: AuditLogFilters) -> Result<AuditLogList>` | Paginated audit log query with optional agent_id, event_type, from, to filters. |
|
|
|
|
**Marketplace** (`sdk-rust/src/marketplace.rs`):
|
|
|
|
| Method | Signature | Description |
|
|
|--------|-----------|-------------|
|
|
| `list_public_agents` | `(filters: MarketplaceFilters) -> Result<MarketplaceAgentList>` | Lists publicly discoverable agents with optional `q`, `capability`, `publisher` filters. |
|
|
|
|
**A2A Delegation** (`sdk-rust/src/delegation.rs`):
|
|
|
|
| Method | Signature | Description |
|
|
|--------|-----------|-------------|
|
|
| `delegate` | `(req: DelegateRequest) -> Result<DelegationToken>` | Creates a delegation chain and returns the delegation JWT. |
|
|
| `verify_delegation` | `(token: &str) -> Result<DelegationVerification>` | Verifies a delegation token and returns the verified claims. |
|
|
|
|
---
|
|
|
|
### Error Types
|
|
|
|
All SDK operations return `Result<T, AgentIdPError>`. Match on the enum variants for structured error handling:
|
|
|
|
```rust
|
|
use sentryagent_idp::AgentIdPError;
|
|
|
|
match client.get_agent("unknown-id").await {
|
|
Ok(agent) => println!("Found: {}", agent.name),
|
|
Err(AgentIdPError::NotFound(msg)) => {
|
|
eprintln!("Agent not found: {}", msg);
|
|
}
|
|
Err(AgentIdPError::AuthError(msg)) => {
|
|
eprintln!("Auth failed: {}", msg);
|
|
// Token may have been revoked — check credentials
|
|
}
|
|
Err(AgentIdPError::RateLimited { retry_after_secs }) => {
|
|
eprintln!("Rate limited — retry after {}s", retry_after_secs);
|
|
tokio::time::sleep(std::time::Duration::from_secs(retry_after_secs)).await;
|
|
}
|
|
Err(AgentIdPError::ApiError { status, message, code }) => {
|
|
eprintln!("API error {}: {} (code: {:?})", status, message, code);
|
|
}
|
|
Err(AgentIdPError::ConfigError(msg)) => {
|
|
// Missing environment variable — fix before running
|
|
eprintln!("Config error: {}", msg);
|
|
}
|
|
Err(AgentIdPError::HttpError(e)) => {
|
|
// reqwest transport error — network issue
|
|
eprintln!("HTTP transport error: {}", e);
|
|
}
|
|
Err(AgentIdPError::SerdeError(e)) => {
|
|
// JSON parse failure — API response shape mismatch
|
|
eprintln!("Serialization error: {}", e);
|
|
}
|
|
Err(AgentIdPError::DelegationError(msg)) => {
|
|
eprintln!("Delegation chain invalid: {}", msg);
|
|
}
|
|
}
|
|
```
|
|
|
|
| Variant | Trigger | HTTP status |
|
|
|---------|---------|-------------|
|
|
| `HttpError(reqwest::Error)` | Network-level failure (connection refused, timeout) | N/A |
|
|
| `ApiError { status, message, code }` | Non-2xx response not matching a specific variant | Any non-2xx |
|
|
| `AuthError(String)` | 401 or 403 from the API | 401, 403 |
|
|
| `NotFound(String)` | 404 from the API | 404 |
|
|
| `RateLimited { retry_after_secs }` | 429 — parses `Retry-After` header (defaults to 60s) | 429 |
|
|
| `ConfigError(String)` | Missing env var in `from_env()` | N/A |
|
|
| `SerdeError(serde_json::Error)` | JSON deserialisation failure | N/A |
|
|
| `DelegationError(String)` | Invalid delegation chain | N/A |
|
|
|
|
---
|
|
|
|
### Adding a New Endpoint to the Rust SDK
|
|
|
|
When the AgentIdP server adds a new API endpoint, add it to the Rust SDK using this checklist:
|
|
|
|
**File structure** (`sdk-rust/src/`):
|
|
|
|
```
|
|
sdk-rust/src/
|
|
├── lib.rs # Crate root — re-exports and module declarations
|
|
├── client.rs # AgentIdPClient struct and new()/from_env() constructors
|
|
├── token_manager.rs # TokenManager — async token cache
|
|
├── models.rs # All request/response structs (serde Serialize/Deserialize)
|
|
├── error.rs # AgentIdPError enum
|
|
├── agents.rs # Agent registry methods (impl AgentIdPClient)
|
|
├── credentials.rs # Credential management methods
|
|
├── oauth2.rs # Token issuance methods
|
|
├── audit.rs # Audit log methods
|
|
├── marketplace.rs # Marketplace methods
|
|
└── delegation.rs # A2A delegation methods
|
|
```
|
|
|
|
**Checklist:**
|
|
|
|
- [ ] Add request/response structs to `models.rs` with `#[derive(Debug, serde::Serialize, serde::Deserialize)]`
|
|
- [ ] Add the method to the appropriate `impl AgentIdPClient` block in the relevant `<domain>.rs` file. If the endpoint belongs to a new domain, create a new file and declare it as `pub mod <domain>;` in `lib.rs`
|
|
- [ ] Use `self.get_auth_header().await?` for the `Authorization: Bearer` header
|
|
- [ ] Use the shared `parse_response::<T>(resp).await` helper (defined in `agents.rs`) to map HTTP status codes to `AgentIdPError` variants
|
|
- [ ] Add a doc comment (`///`) to the method with: the HTTP method + path, the success response type, and `# Errors` listing which `AgentIdPError` variants it can return
|
|
- [ ] Re-export new public types from `lib.rs` with `pub use models::{NewRequestType, NewResponseType};`
|
|
- [ ] Add a unit test using `mockito::Server` (see `token_manager.rs` tests for the pattern)
|
|
- [ ] Run `cargo test` and verify all tests pass
|
|
- [ ] Run `cargo doc --no-deps --open` and verify the new method appears with correct documentation
|
|
- [ ] Verify `cargo clippy -- -D warnings` exits 0
|
|
|
|
---
|
|
|
|
## 7. SDK Contribution Guide — Adding a New Endpoint
|
|
|
|
When the server adds a new API endpoint, update all four SDKs. The checklist below covers each SDK.
|
|
|
|
### Node.js SDK (`sdk/`)
|
|
|
|
```
|
|
src/
|
|
services/
|
|
agents.ts # AgentRegistryClient
|
|
credentials.ts # CredentialClient
|
|
token.ts # TokenClient
|
|
audit.ts # AuditClient
|
|
types.ts # All request/response type definitions
|
|
token-manager.ts # TokenManager
|
|
client.ts # AgentIdPClient (top-level)
|
|
errors.ts # AgentIdPError
|
|
```
|
|
|
|
Checklist:
|
|
- [ ] Add method to the appropriate service client in `src/services/<client>.ts`
|
|
- [ ] Add TypeScript request/response types in `src/types.ts`
|
|
- [ ] Add JSDoc with `@param`, `@returns`, and `@throws`
|
|
- [ ] Add unit test in `tests/<client>.test.ts`
|
|
- [ ] Verify `npx tsc --strict` exits 0
|
|
|
|
### Python SDK (`sdk-python/`)
|
|
|
|
```
|
|
src/sentryagent_idp/
|
|
services/
|
|
agents.py # AgentRegistryClient + AsyncAgentRegistryClient
|
|
credentials.py # CredentialClient + AsyncCredentialClient
|
|
token.py # TokenClient + AsyncTokenClient
|
|
audit.py # AuditClient + AsyncAuditClient
|
|
client.py # AgentIdPClient + AsyncAgentIdPClient
|
|
token_manager.py # TokenManager (sync)
|
|
async_token_manager.py # AsyncTokenManager
|
|
errors.py # AgentIdPError
|
|
types.py # TypedDict / dataclass definitions
|
|
```
|
|
|
|
Checklist:
|
|
- [ ] Add method to both the sync and async service clients
|
|
- [ ] Add type hints (all parameters and return types)
|
|
- [ ] Verify `mypy --strict` passes
|
|
- [ ] Add unit test in `tests/`
|
|
- [ ] Verify `pytest` passes with >80% coverage
|
|
|
|
### Go SDK (`sdk-go/`)
|
|
|
|
```
|
|
agentidp/
|
|
client.go # AgentIdPClient + AgentIdPClientConfig
|
|
agents.go # AgentsClient
|
|
credentials.go # CredentialsClient
|
|
token_service.go # TokenServiceClient
|
|
audit.go # AuditClient
|
|
token_manager.go # TokenManager (goroutine-safe)
|
|
errors.go # AgentIdPError
|
|
types.go # All request/response struct types
|
|
request.go # Shared HTTP request helper
|
|
```
|
|
|
|
Checklist:
|
|
- [ ] Add method to the appropriate `*Client` type
|
|
- [ ] Use `context.Context` as the first parameter
|
|
- [ ] Add godoc comment above the method
|
|
- [ ] Add request/response struct types in `types.go` if needed
|
|
- [ ] Add unit test in `<file>_test.go`
|
|
- [ ] Verify `go vet ./... && staticcheck ./...` pass
|
|
|
|
### Java SDK (`sdk-java/`)
|
|
|
|
```
|
|
src/main/java/ai/sentryagent/idp/
|
|
AgentIdPClient.java # Top-level client
|
|
services/
|
|
AgentServiceClient.java # Agent Registry
|
|
CredentialServiceClient.java
|
|
TokenServiceClient.java
|
|
AuditServiceClient.java
|
|
models/ # Request/response POJOs (@JsonProperty)
|
|
TokenManager.java # Thread-safe token caching
|
|
AgentIdPException.java # Typed exception
|
|
```
|
|
|
|
Checklist:
|
|
- [ ] Add sync method to the appropriate service client
|
|
- [ ] Add `CompletableFuture<T>` async counterpart with the `Async` suffix
|
|
- [ ] Add request/response POJO in `models/` with `@JsonProperty` annotations
|
|
- [ ] Add Javadoc on the method
|
|
- [ ] Add JUnit 5 test in `src/test/java/`
|
|
- [ ] Verify `mvn verify` passes (compiles, tests, and checks coverage)
|