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

17 KiB
Raw Permalink Blame History

1. WS1: Rust SDK — Crate Setup

  • 1.1 Create sdk-rust/ directory and Cargo.toml — name: sentryagent-idp, version: 1.0.0, edition: 2021; add dependencies: tokio (features: full), reqwest (features: json, rustls-tls), serde (features: derive), serde_json, uuid (features: v4), thiserror, async-trait; add dev-dependencies: tokio-test, mockito
  • 1.2 Create sdk-rust/src/lib.rs — crate root with #![deny(warnings)]; re-export AgentIdPClient, TokenManager, AgentIdPError, and all model types from submodules; add crate-level //! doc comment describing the SDK
  • 1.3 Create sdk-rust/src/error.rs — define AgentIdPError enum with variants: HttpError(reqwest::Error), ApiError { status: u16, message: String, code: Option<String> }, AuthError(String), NotFound(String), RateLimited { retry_after_secs: u64 }, ConfigError(String), SerdeError(serde_json::Error), DelegationError(String); derive thiserror::Error and Debug; implement std::error::Error
  • 1.4 Create sdk-rust/src/models.rs — define all request structs (RegisterAgentRequest, UpdateAgentRequest, AuditLogFilters, MarketplaceFilters, DelegateRequest) and all response structs (Agent, AgentList, TokenResponse, Credentials, AuditLogEntry, AuditLogList, MarketplaceAgent, MarketplaceAgentList, DelegationToken, DelegationVerification); all structs derive serde::Serialize, serde::Deserialize, Debug, Clone

2. WS1: Rust SDK — Token Manager

  • 2.1 Create sdk-rust/src/token_manager.rs — define TokenCache struct with access_token: Option<String> and expires_at: Option<std::time::Instant>; define TokenManager struct with fields api_url, client_id, client_secret, cache: Arc<Mutex<TokenCache>>
  • 2.2 Implement TokenManager::new(api_url: &str, client_id: &str, client_secret: &str) -> Self — initializes with empty cache
  • 2.3 Implement TokenManager::get_token(&self) -> Result<String, AgentIdPError> — acquires lock, checks expires_at against Instant::now() + 60s, returns cached token if valid, else calls POST /oauth2/token via reqwest, updates cache, releases lock
  • 2.4 Write unit test token_manager_returns_cached_token — mock POST /oauth2/token using mockito, call get_token() twice, verify mock is hit only once
  • 2.5 Write unit test token_manager_refreshes_expired_token — set expires_at to past, verify get_token() triggers a new POST /oauth2/token call
  • 2.6 Write concurrent safety test token_manager_concurrent_calls_no_race — spawn 50 tokio::spawn tasks all calling get_token() simultaneously, verify mock is hit at most once (no thundering herd), verify all 50 tasks receive valid tokens

3. WS1: Rust SDK — Client Methods

  • 3.1 Create sdk-rust/src/client.rs — define AgentIdPClient struct with fields base_url, client_id, client_secret, http: reqwest::Client, token_manager: Arc<Mutex<TokenManager>>; implement new(base_url, client_id, client_secret) -> Self and from_env() -> Result<Self, AgentIdPError> (reads AGENTIDP_API_URL, AGENTIDP_CLIENT_ID, AGENTIDP_CLIENT_SECRET)
  • 3.2 Create sdk-rust/src/agents.rs — implement all agent methods on AgentIdPClient: register_agent, get_agent, list_agents, update_agent, delete_agent — each acquires a bearer token via token_manager.get_token(), makes the correct HTTP call, deserializes response, maps non-2xx responses to AgentIdPError::ApiError
  • 3.3 Create sdk-rust/src/oauth2.rs — implement issue_token(&self, agent_id: &str, scopes: &[&str]) -> Result<TokenResponse, AgentIdPError> — sends POST /oauth2/token with grant_type=client_credentials
  • 3.4 Create sdk-rust/src/credentials.rs — implement generate_credentials, rotate_credentials, revoke_credentials — map 404 response to AgentIdPError::NotFound, map 401 to AgentIdPError::AuthError
  • 3.5 Create sdk-rust/src/audit.rs — implement list_audit_logs(filters: AuditLogFilters) — serialize filters as query parameters; handle empty result set (return empty Vec, not error)
  • 3.6 Create sdk-rust/src/marketplace.rs — implement list_public_agents(filters) and get_public_agent(agent_id) — no auth header required for these endpoints
  • 3.7 Create sdk-rust/src/delegation.rs — implement delegate(req: DelegateRequest) and verify_delegation(token: &str)
  • 3.8 Implement 429 handling across all client methods — parse Retry-After header, return AgentIdPError::RateLimited { retry_after_secs }; verify zero unwrap() calls in all src/ files (run grep -r 'unwrap()' sdk-rust/src/ — must return empty)

4. WS1: Rust SDK — Tests, Examples, Documentation

  • 4.1 Create sdk-rust/examples/quickstart.rs — working example: create AgentIdPClient::from_env(), call register_agent, call issue_token, print token; example must compile with cargo build --example quickstart
  • 4.2 Create sdk-rust/tests/integration_test.rs — integration tests requiring AGENTIDP_API_URL, AGENTIDP_CLIENT_ID, AGENTIDP_CLIENT_SECRET env vars; test: register agent, issue token, get agent, update agent, rotate credentials, delete agent; each test is #[tokio::test] with #[ignore] attribute (run explicitly with cargo test -- --ignored)
  • 4.3 Write sdk-rust/README.md — installation via Cargo.toml, environment variable configuration, quickstart code example, full method reference table with signatures, error handling guide, link to crates.io
  • 4.4 Run cargo doc --no-deps — verify docs generate without errors or warnings; verify all public items have /// doc comments
  • 4.5 Run cargo clippy -- -D warnings — zero warnings; run cargo test (unit tests only, no --ignored) — all pass

5. WS2: A2A Authorization — Database & Types

  • 5.1 Create src/infrastructure/migrations/008_add_delegation_chains.sql — create delegation_chains table with columns: id (UUID PK), tenant_id (UUID FK), delegator_agent_id (UUID FK), delegatee_agent_id (UUID FK), scopes (TEXT[]), delegation_token (TEXT UNIQUE), signature (TEXT), ttl_seconds (INTEGER CHECK 6086400), issued_at (TIMESTAMPTZ), expires_at (TIMESTAMPTZ), revoked_at (TIMESTAMPTZ nullable), created_at (TIMESTAMPTZ DEFAULT NOW); create all four indexes as specified in spec
  • 5.2 Create src/types/delegation.ts — define interfaces: DelegationChain, CreateDelegationRequest (delegateeAgentId, scopes, ttlSeconds), DelegationVerificationResult (valid, chainId, delegatorAgentId, delegateeAgentId, scopes, issuedAt, expiresAt, revokedAt), DelegationTokenPayload

6. WS2: A2A Authorization — Crypto & Service

  • 6.1 Create src/utils/delegationCrypto.ts — implement signDelegationPayload(payload: DelegationTokenPayload, secret: string): string using HMAC-SHA256 (Node.js crypto.createHmac('sha256', secret)); implement verifyDelegationSignature(payload: DelegationTokenPayload, signature: string, secret: string): boolean; implement generateDelegationToken(): string (UUID v4); export only these three functions — no other exports
  • 6.2 Create src/services/DelegationService.ts — implement IDelegationService interface; createDelegation: validate delegateeAgentId exists in same tenant, validate scopes ⊆ delegator's scopes, reject self-delegation, sign payload, insert delegation_chains row, write audit log entry (delegation.created), return DelegationChain
  • 6.3 Implement DelegationService.verifyDelegation(delegationToken) — fetch chain row by delegation_token, if not found throw NotFoundError, verify HMAC signature, check expires_at > NOW() and revoked_at IS NULL, return DelegationVerificationResult with valid: true/false (never throw on expired/revoked — return valid: false); write audit log entry (delegation.verified)
  • 6.4 Implement DelegationService.revokeDelegation(chainId, requestingAgentId) — fetch chain by ID, verify delegator_agent_id === requestingAgentId (else throw ForbiddenError), check not already revoked (else throw ConflictError), update revoked_at = NOW(), write audit log entry (delegation.revoked)

7. WS2: A2A Authorization — Controller, Routes, Tests

  • 7.1 Create src/controllers/DelegationController.ts — implement createDelegation handler (POST /oauth2/token/delegate): extract authenticated agent ID from request context, call DelegationService.createDelegation, return HTTP 201; implement verifyDelegation handler (POST /oauth2/token/verify-delegation): call DelegationService.verifyDelegation, return HTTP 200; implement revokeDelegation handler (DELETE /oauth2/token/delegate/:chainId): call DelegationService.revokeDelegation, return HTTP 204
  • 7.2 Create src/routes/delegation.ts — Express router registering POST /oauth2/token/delegate, POST /oauth2/token/verify-delegation, DELETE /oauth2/token/delegate/:chainId with authentication middleware on all three routes
  • 7.3 Register delegation router in src/routes/index.ts behind A2A_ENABLED feature flag — return HTTP 404 on all delegation routes when A2A_ENABLED=false
  • 7.4 Add delegation Prometheus metrics: agentidp_delegations_created_total, agentidp_delegations_verified_total (labels: result), agentidp_delegations_revoked_total — increment in DelegationController handlers
  • 7.5 Add delegation endpoints to docs/openapi.yaml — include all request/response schemas, error responses, and authentication requirements as defined in spec
  • 7.6 Write unit tests for delegationCrypto.ts — test sign/verify round-trip, test tampered payload fails verification, test different secrets produce different signatures
  • 7.7 Write unit tests for DelegationService — mock DB and audit service; test: create delegation (valid), create delegation (scope escalation rejected), create delegation (self-delegation rejected), create delegation (delegatee in different tenant rejected), verify delegation (valid), verify delegation (expired — returns valid: false not throw), verify delegation (revoked — returns valid: false), revoke delegation (by delegator — succeeds), revoke delegation (by non-delegator — throws ForbiddenError), revoke delegation (already revoked — throws ConflictError)
  • 7.8 Write integration tests for delegation endpoints — test all happy paths and all error cases defined in spec; verify audit log entries are created for each delegation operation

8. WS5: Developer Experience — Scaffold Service

  • 8.1 Install archiver and @types/archiver in API package.json
  • 8.2 Create src/types/scaffold.ts — define ScaffoldLanguage union ('typescript' | 'python' | 'go' | 'java' | 'rust'), ScaffoldOptions interface, ScaffoldTemplate interface
  • 8.3 Create scaffold template files for TypeScript in src/templates/scaffold/typescript/: package.json.tmpl, tsconfig.json.tmpl, src/index.ts.tmpl, .env.example.tmpl, .gitignore.tmpl, README.md.tmpl — each file uses {{AGENT_ID}}, {{AGENT_NAME}}, {{CLIENT_ID}}, {{API_URL}} as template variables; .env.example.tmpl MUST include AGENTIDP_CLIENT_SECRET=<your-client-secret> placeholder (never inject real secret)
  • 8.4 Create scaffold template files for Python in src/templates/scaffold/python/: requirements.txt.tmpl, main.py.tmpl, .env.example.tmpl, .gitignore.tmpl, README.md.tmpl — same template variable convention
  • 8.5 Create scaffold template files for Go in src/templates/scaffold/go/: go.mod.tmpl, main.go.tmpl, .env.example.tmpl, .gitignore.tmpl, README.md.tmpl
  • 8.6 Create scaffold template files for Java in src/templates/scaffold/java/: pom.xml.tmpl, src/main/java/Main.java.tmpl, .env.example.tmpl, .gitignore.tmpl, README.md.tmpl
  • 8.7 Create scaffold template files for Rust in src/templates/scaffold/rust/: Cargo.toml.tmpl, src/main.rs.tmpl, .env.example.tmpl, .gitignore.tmpl, README.md.tmpl
  • 8.8 Create src/services/ScaffoldService.ts — implement IScaffoldService; generateScaffold(agentId, language, apiUrl): load template files for language, inject template variables (replace {{AGENT_ID}}, {{AGENT_NAME}}, {{CLIENT_ID}}, {{API_URL}}), build in-memory ZIP using archiver; return { stream: NodeJS.ReadableStream, filename: string }; emit agentidp_scaffold_generated_total counter and agentidp_scaffold_generation_duration_ms histogram

9. WS5: Developer Experience — Scaffold Controller & Route

  • 9.1 Create src/controllers/ScaffoldController.ts — implement getScaffold handler for GET /sdk/scaffold/:agentId: validate language query param against ScaffoldLanguage union (HTTP 400 on invalid); fetch agent, verify agent belongs to authenticated tenant (HTTP 403 if not); call ScaffoldService.generateScaffold; set Content-Type: application/zip, Content-Disposition: attachment; filename="...", pipe stream to response; write audit log entry (scaffold.generated, metadata: { language })
  • 9.2 Create src/routes/scaffold.ts — Express router for GET /sdk/scaffold/:agentId with authentication middleware; apply scaffold-specific rate limiter (10 req/min per tenant, separate from global rate limiter)
  • 9.3 Register scaffold router in src/routes/index.ts
  • 9.4 Add GET /sdk/scaffold/:agentId to docs/openapi.yaml — document binary response type, query parameters, all error responses
  • 9.5 Write unit tests for ScaffoldService — test: generate TypeScript scaffold (verify ZIP contains all 6 files), generate Python scaffold (verify all 5 files), verify {{CLIENT_ID}} is replaced in .env.example, verify {{AGENTIDP_CLIENT_SECRET}} is placeholder not real secret, verify invalid language throws ValidationError
  • 9.6 Write integration tests for scaffold endpoint — test: TypeScript scaffold returns ZIP with correct Content-Type and Content-Disposition; Python scaffold returns ZIP; HTTP 400 on invalid language; HTTP 403 when agent belongs to different tenant; HTTP 404 when agent does not exist

10. WS5: Developer Experience — Portal & CLI

  • 10.1 Install @stoplight/elements in portal/package.json — remove swagger-ui-react
  • 10.2 Rewrite portal/app/api-explorer/page.tsx — replace SwaggerUI component with @stoplight/elements <API> component; set apiDescriptionUrl, router="hash", layout="sidebar", hideSchemas={false}, tryItCredentialsPolicy="same-origin"; import Elements CSS; remove all Swagger UI imports and CSS
  • 10.3 Run npm run build in portal/ — verify zero TypeScript errors and zero ESLint errors after Elements integration
  • 10.4 Install unzipper and @types/unzipper in cli/package.json
  • 10.5 Create cli/src/commands/scaffold.ts — implement sentryagent scaffold command with Commander options: --agent-id <id> (required), --language <lang> (default: typescript), --out <directory> (default: .); load config, issue Bearer token, call GET /sdk/scaffold/{agentId}?language={language}, pipe response through unzipper.Extract({ path: outDir }), print success message and next steps; handle errors (404, 403, 400) with human-readable messages
  • 10.6 Register scaffold command in cli/src/index.ts — add .addCommand(scaffoldCommand) to Commander program
  • 10.7 Run npm run build in cli/ — zero TypeScript errors; run node dist/index.js scaffold --help — outputs correct usage

11. QA & Release

  • 11.1 Run cargo build and cargo clippy -- -D warnings in sdk-rust/ — zero warnings; run cargo test — all unit tests pass
  • 11.2 Run tsc --noEmit across API, portal, and CLI — zero TypeScript errors
  • 11.3 Run full Jest suite (npm test) — all unit tests pass, coverage >= 80% across all new services: DelegationService, ScaffoldService
  • 11.4 Run npm run build in portal/ with Elements integration — zero errors; verify /api-explorer page renders Elements <API> component
  • 11.5 Run npm run build in cli/ — zero errors; run node dist/index.js scaffold --help — shows correct options; run node dist/index.js --help — shows scaffold command listed
  • 11.6 Apply database migration 008_add_delegation_chains.sql against a test database — verify migration runs without errors and table is created with correct schema
  • 11.7 Run integration tests for all Phase 5 endpoints — delegation (create, verify, revoke), scaffold (all 5 languages)
  • 11.8 Verify feature flag: A2A_ENABLED=false → delegation routes return 404
  • 11.9 Verify scaffold security: GET /sdk/scaffold/:agentId response ZIP never contains a real client_secret value — .env.example placeholder only
  • 11.10 Commit all Phase 5 work on main — one conventional commit per workstream: feat(phase-5): WS1 — Rust SDK, feat(phase-5): WS2 — A2A Authorization, feat(phase-5): WS5 — Developer Experience