- 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>
26 KiB
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 sharedTokenManager.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) orAgentIdPException(Java) — withcode,httpStatus, anddetailsfields.
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:
npm install @sentryagent/idp-sdk
Requirements: Node.js 18+ (uses native fetch).
Complete example:
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:
pip install sentryagent-idp
Requirements: Python 3.9+. Synchronous client uses requests; asynchronous client uses httpx.
Synchronous example
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
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:
go get github.com/sentryagent/idp-sdk-go
Requirements: Go 1.21+.
Complete example:
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:
<dependency>
<groupId>ai.sentryagent</groupId>
<artifactId>idp-sdk</artifactId>
<version>1.0.0</version>
</dependency>
Gradle:
implementation 'ai.sentryagent:idp-sdk:1.0.0'
Requirements: Java 17+.
Synchronous example
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)
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:
[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/tokenrequest withgrant_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
Mutexguarantees only one refresh happens even when manytokiotasks callget_token()concurrently.
Environment variable construction:
use sentryagent_idp::AgentIdPClient;
// from_env() reads AGENTIDP_API_URL, AGENTIDP_CLIENT_ID, AGENTIDP_CLIENT_SECRET
let client = AgentIdPClient::from_env()?;
Explicit construction:
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.
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:
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:
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.rswith#[derive(Debug, serde::Serialize, serde::Deserialize)] - Add the method to the appropriate
impl AgentIdPClientblock in the relevant<domain>.rsfile. If the endpoint belongs to a new domain, create a new file and declare it aspub mod <domain>;inlib.rs - Use
self.get_auth_header().await?for theAuthorization: Bearerheader - Use the shared
parse_response::<T>(resp).awaithelper (defined inagents.rs) to map HTTP status codes toAgentIdPErrorvariants - Add a doc comment (
///) to the method with: the HTTP method + path, the success response type, and# Errorslisting whichAgentIdPErrorvariants it can return - Re-export new public types from
lib.rswithpub use models::{NewRequestType, NewResponseType}; - Add a unit test using
mockito::Server(seetoken_manager.rstests for the pattern) - Run
cargo testand verify all tests pass - Run
cargo doc --no-deps --openand verify the new method appears with correct documentation - Verify
cargo clippy -- -D warningsexits 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 --strictexits 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 --strictpasses - Add unit test in
tests/ - Verify
pytestpasses 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
*Clienttype - Use
context.Contextas the first parameter - Add godoc comment above the method
- Add request/response struct types in
types.goif 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 theAsyncsuffix - Add request/response POJO in
models/with@JsonPropertyannotations - Add Javadoc on the method
- Add JUnit 5 test in
src/test/java/ - Verify
mvn verifypasses (compiles, tests, and checks coverage)