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>
101 lines
17 KiB
Markdown
101 lines
17 KiB
Markdown
## 1. WS1: Rust SDK — Crate Setup
|
||
|
||
- [x] 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`
|
||
- [x] 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
|
||
- [x] 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`
|
||
- [x] 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
|
||
|
||
- [x] 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>>`
|
||
- [x] 2.2 Implement `TokenManager::new(api_url: &str, client_id: &str, client_secret: &str) -> Self` — initializes with empty cache
|
||
- [x] 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
|
||
- [x] 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
|
||
- [x] 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
|
||
- [x] 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
|
||
|
||
- [x] 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`)
|
||
- [x] 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`
|
||
- [x] 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`
|
||
- [x] 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`
|
||
- [x] 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)
|
||
- [x] 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
|
||
- [x] 3.7 Create `sdk-rust/src/delegation.rs` — implement `delegate(req: DelegateRequest)` and `verify_delegation(token: &str)`
|
||
- [x] 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
|
||
|
||
- [x] 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`
|
||
- [x] 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`)
|
||
- [x] 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
|
||
- [x] 4.4 Run `cargo doc --no-deps` — verify docs generate without errors or warnings; verify all public items have `///` doc comments
|
||
- [x] 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
|
||
|
||
- [x] 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 60–86400), `issued_at` (TIMESTAMPTZ), `expires_at` (TIMESTAMPTZ), `revoked_at` (TIMESTAMPTZ nullable), `created_at` (TIMESTAMPTZ DEFAULT NOW); create all four indexes as specified in spec
|
||
- [x] 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
|
||
|
||
- [x] 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
|
||
- [x] 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`
|
||
- [x] 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`)
|
||
- [x] 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
|
||
|
||
- [x] 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
|
||
- [x] 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
|
||
- [x] 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`
|
||
- [x] 7.4 Add delegation Prometheus metrics: `agentidp_delegations_created_total`, `agentidp_delegations_verified_total` (labels: result), `agentidp_delegations_revoked_total` — increment in `DelegationController` handlers
|
||
- [x] 7.5 Add delegation endpoints to `docs/openapi.yaml` — include all request/response schemas, error responses, and authentication requirements as defined in spec
|
||
- [x] 7.6 Write unit tests for `delegationCrypto.ts` — test sign/verify round-trip, test tampered payload fails verification, test different secrets produce different signatures
|
||
- [x] 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)
|
||
- [x] 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
|
||
|
||
- [x] 8.1 Install `archiver` and `@types/archiver` in API `package.json`
|
||
- [x] 8.2 Create `src/types/scaffold.ts` — define `ScaffoldLanguage` union (`'typescript' | 'python' | 'go' | 'java' | 'rust'`), `ScaffoldOptions` interface, `ScaffoldTemplate` interface
|
||
- [x] 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)
|
||
- [x] 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
|
||
- [x] 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`
|
||
- [x] 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`
|
||
- [x] 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`
|
||
- [x] 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
|
||
|
||
- [x] 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 }`)
|
||
- [x] 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)
|
||
- [x] 9.3 Register `scaffold` router in `src/routes/index.ts`
|
||
- [x] 9.4 Add `GET /sdk/scaffold/:agentId` to `docs/openapi.yaml` — document binary response type, query parameters, all error responses
|
||
- [x] 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`
|
||
- [x] 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
|
||
|
||
- [x] 10.1 Install `@stoplight/elements` in `portal/package.json` — remove `swagger-ui-react`
|
||
- [x] 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
|
||
- [x] 10.3 Run `npm run build` in `portal/` — verify zero TypeScript errors and zero ESLint errors after Elements integration
|
||
- [x] 10.4 Install `unzipper` and `@types/unzipper` in `cli/package.json`
|
||
- [x] 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
|
||
- [x] 10.6 Register `scaffold` command in `cli/src/index.ts` — add `.addCommand(scaffoldCommand)` to Commander program
|
||
- [x] 10.7 Run `npm run build` in `cli/` — zero TypeScript errors; run `node dist/index.js scaffold --help` — outputs correct usage
|
||
|
||
## 11. QA & Release
|
||
|
||
- [x] 11.1 Run `cargo build` and `cargo clippy -- -D warnings` in `sdk-rust/` — zero warnings; run `cargo test` — all unit tests pass
|
||
- [x] 11.2 Run `tsc --noEmit` across API, portal, and CLI — zero TypeScript errors
|
||
- [x] 11.3 Run full Jest suite (`npm test`) — all unit tests pass, coverage >= 80% across all new services: `DelegationService`, `ScaffoldService`
|
||
- [x] 11.4 Run `npm run build` in `portal/` with Elements integration — zero errors; verify `/api-explorer` page renders Elements `<API>` component
|
||
- [x] 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
|
||
- [x] 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
|
||
- [x] 11.7 Run integration tests for all Phase 5 endpoints — delegation (create, verify, revoke), scaffold (all 5 languages)
|
||
- [x] 11.8 Verify feature flag: `A2A_ENABLED=false` → delegation routes return 404
|
||
- [x] 11.9 Verify scaffold security: `GET /sdk/scaffold/:agentId` response ZIP never contains a real `client_secret` value — `.env.example` placeholder only
|
||
- [x] 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`
|