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>
This commit is contained in:
SentryAgent.ai Developer
2026-04-03 02:54:45 +00:00
parent eaabaebf52
commit 8fd6823581
7 changed files with 0 additions and 58 deletions

View File

@@ -0,0 +1,279 @@
## Context
SentryAgent.ai has completed four phases: Phase 1 (MVP — core agent registry, OAuth 2.0, audit log), Phase 2 (Production-Ready — Vault, 4 language SDKs, OPA, React dashboard, Prometheus, Terraform multi-region), Phase 3 (Enterprise — multi-tenancy, W3C DIDs, OIDC, AGNTCY federation, webhooks, SOC 2 controls), and Phase 4 (Developer Growth — production hardening, developer portal, CLI, agent marketplace, GitHub Actions, Stripe billing). The product is technically complete, commercially launched, and has an active developer community.
Phase 5 operates on a stable, proven foundation. Every new workstream is additive — no existing service is refactored, only extended. The architecture constraint that governs Phase 5 is: **any new service MUST follow the existing DI pattern (constructor injection of typed interfaces), MUST emit Prometheus metrics, and MUST be covered by the existing QA gate (>80% coverage, OpenAPI spec-first).**
## Goals / Non-Goals
**Goals:**
- Complete language SDK parity — Rust is the only major language missing a first-party SDK
- Introduce A2A delegation as a first-class authorization primitive aligned with AGNTCY multi-agent workflows
- Give paying tenants visibility into their own usage patterns through analytics
- Expose multi-tier rate limits as a self-service commercial lever
- Eliminate DX friction for new developers — scaffold generation reduces time-to-first-request to under 5 minutes
- Certify AGNTCY compliance formally — this is a competitive moat
**Non-Goals:**
- Real-time WebSocket-based analytics streaming (batch/polling is acceptable for MVP analytics)
- Full marketplace monetization (agent listings with pricing — discovery only, no transactions, out of scope)
- Native mobile SDK (iOS/Android)
- GraphQL API surface
- Webhook delivery for analytics events (Phase 6 if needed)
## Decisions
### ADR-1: Rust SDK is a standalone Cargo crate in `sdk-rust/` with no code generation
**Decision:** The Rust SDK is a hand-authored Cargo crate at `sdk-rust/`, not generated from the OpenAPI spec using `openapi-generator`.
**Rationale:** Code generation produces idiomatically poor Rust — `openapi-generator`'s Rust output does not use `async/await` idiomatically, does not produce proper `thiserror`-based error types, and generates `unwrap()` calls in critical paths. Hand-authored code ensures idiomatic Rust: `async/await` throughout, `Arc<Mutex<TokenCache>>` for thread-safe token caching, `Result<T, AgentIdPError>` for every fallible operation, and zero `unwrap()` in library code. The SDK API surface mirrors the Go SDK pattern (the most recently authored, cleanest SDK) to minimize cognitive load for polyglot teams.
**Alternatives considered:** `openapi-generator --generator rust` — produces non-idiomatic output, requires post-processing, hard to maintain. `progenitor` (Oxide) — excellent output but requires forking Oxide's toolchain and adds a complex build dependency.
---
### ADR-2: A2A delegation chains are stored in PostgreSQL, verified cryptographically at request time
**Decision:** Delegation chains are stored as rows in a `delegation_chains` table. Each row captures: delegator agent ID, delegatee agent ID, granted scopes, expiry, and a cryptographic signature over the delegation payload using the delegator's credential secret. Verification at `POST /oauth2/token/verify-delegation` reconstructs and verifies the chain signature.
**Rationale:** Storing the full delegation chain in the database enables: (1) audit log entries with full chain context, (2) revocation of any link in a chain (invalidating all downstream delegations), and (3) analytics over delegation depth and patterns. Cryptographic signing at issuance means the database is the source of truth but is not trusted blindly — the chain is independently verifiable.
**Alternatives considered:** JWT-encoded delegation claims only (no DB storage) — enables verification without a DB hit but prevents revocation and audit. Blockchain-anchored delegation — extreme overkill for MVP scale, operational complexity exceeds benefit.
---
### ADR-3: Analytics are computed from `usage_events` table using pre-aggregated daily summaries
**Decision:** The analytics endpoints (`GET /analytics/usage-summary`, `GET /analytics/agent-activity`, `GET /analytics/token-trends`) query a new `analytics_daily_aggregates` table that is populated by a nightly aggregation job (pg_cron or a Node.js cron via `node-cron`). Raw `usage_events` rows are not queried at API request time.
**Rationale:** The `usage_events` table is append-only and grows without bound. Scanning it for date-range analytics would produce full-table scans at production scale. Pre-aggregated daily summaries (`tenant_id`, `agent_id`, `date`, `metric_type`, `count`) enable O(days) queries regardless of event volume. The aggregation job runs at 00:05 UTC daily to aggregate the previous day's events.
**Alternatives considered:** Real-time aggregation using PostgreSQL window functions — acceptable at small scale, degrades catastrophically at 10M+ events. TimescaleDB hypertables — excellent solution but adds an infrastructure dependency (separate DB engine) disproportionate to Phase 5 scope.
---
### ADR-4: Multi-tier rate limits are enforced in a new `TierRateLimiter` middleware that reads tier from `tenant_subscriptions`
**Decision:** A new `TierRateLimiter` middleware replaces the flat rate limiter for authenticated routes. It reads the tenant's current tier (`free` | `pro` | `enterprise`) from a Redis-cached lookup of `tenant_subscriptions` and applies the tier-appropriate rate limit from a static tier definition map. The tier definition map is the single source of truth — also returned verbatim by `GET /tiers`.
**Rationale:** The existing `RateLimiterRedis` middleware applies a single flat limit across all tenants. Multi-tier enforcement requires per-tenant limit keys (already supported by `rate-limiter-flexible` via the `keyPrefix` option) and per-tier limit configurations. Centralizing tier definitions in a static config (not a database table) avoids the complexity of dynamic tier management and keeps tier changes as code changes (reviewed, versioned, deployed).
**Alternatives considered:** API gateway (Kong, AWS API Gateway) for rate limiting — correct long-term architecture but adds operational complexity and cost beyond Phase 5 scope. Per-tenant custom limits stored in DB — too flexible, hard to reason about, no self-service model.
---
### ADR-5: Scaffold generator produces a ZIP archive served from `GET /sdk/scaffold/:agentId`
**Decision:** `ScaffoldService` generates an in-memory ZIP archive (using `archiver`) containing language-specific starter files pre-populated with the agent's `clientId` and the API URL. The endpoint streams the ZIP directly from memory — no disk I/O, no S3.
**Rationale:** Scaffold generation is a low-frequency, low-latency-sensitive operation (developers use it once per new project). In-memory generation avoids disk I/O, eliminates cleanup complexity, and produces no persistent artifacts on the server. The `archiver` library supports in-memory streaming to an HTTP response via Node.js streams. Each scaffold is generated on demand and is not cached — the agent's credentials could rotate between requests.
**Alternatives considered:** Pre-built scaffold templates on S3 with client ID injected at runtime — adds AWS dependency, complicates credential injection. GitHub template repositories — developer must authenticate with GitHub, adds friction. Static downloadable templates — not pre-wired with agent credentials, defeats the purpose.
---
### ADR-6: AGNTCY compliance report is generated on demand from live system state, not cached
**Decision:** `GET /agntcy/compliance-report` queries live system state — registered agents, DID documents, OIDC configuration, federation policies, audit log retention settings — and generates a structured compliance report in real time. No pre-computed report cache.
**Rationale:** Compliance reports must reflect current system state. A cached report could misrepresent configuration that has changed since the last cache population. The compliance report endpoint is not on the critical path (it is used by compliance officers, not application code) — latency of 5002000ms is acceptable. The report format is machine-readable JSON (with an optional PDF export hint for human-readable presentation).
**Alternatives considered:** Pre-generated nightly compliance reports stored in S3 — stale by definition, adds S3 dependency. Compliance report built into the monitoring stack (Grafana) — mixing compliance and observability concerns violates single responsibility.
## Component Architecture — How Phase 5 Extends Phase 4
```
┌─────────────────────────────────────────────────────────────────────────────────┐
│ SentryAgent.ai Platform — Phase 5 │
│ │
│ ┌──────────────────────────────┐ ┌──────────────────────────────────────┐ │
│ │ Developer Portal (Next.js) │ │ Web Dashboard (React 18) │ │
│ │ ┌────────────────────────┐ │ │ ┌──────────────────────────────────┐│ │
│ │ │ API Explorer │ │ │ │ Analytics Tab (NEW - WS3) ││ │
│ │ │ (Elements v5 — WS5) │ │ │ │ - Agent Activity Heatmap ││ │
│ │ ├────────────────────────┤ │ │ │ - Token Issuance Trends ││ │
│ │ │ Scaffold Download (WS5)│ │ │ │ - Rotation Frequency ││ │
│ │ └────────────────────────┘ │ │ └──────────────────────────────────┘│ │
│ └──────────────────────────────┘ └──────────────────────────────────────┘ │
│ │ │ │
│ └──────────────┬─────────────────────────┘ │
│ │ HTTPS │
│ ┌─────────────────────────────▼────────────────────────────────────────────┐ │
│ │ Express API (Node.js / TypeScript) │ │
│ │ │ │
│ │ ┌──────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────────┐ │ │
│ │ │ Delegation │ │ Analytics │ │ Tiers & │ │ Scaffold │ │ │
│ │ │ Router (WS2) │ │ Router (WS3)│ │ Upgrade(WS4)│ │ Router (WS5) │ │ │
│ │ └──────┬───────┘ └──────┬──────┘ └──────┬──────┘ └──────┬────────┘ │ │
│ │ │ │ │ │ │ │
│ │ ┌──────▼───────┐ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼────────┐ │ │
│ │ │Delegation │ │Analytics │ │BillingService│ │Scaffold │ │ │
│ │ │Service (WS2) │ │Service (WS3)│ │(extended WS4)│ │Service (WS5) │ │ │
│ │ └──────┬───────┘ └──────┬──────┘ └─────────────┘ └──────┬────────┘ │ │
│ │ │ │ │ │ │
│ │ ┌──────▼───────────────────▼───────────────────────────────────▼────────┐ │ │
│ │ │ TierRateLimiter Middleware (WS4) │ │ │
│ │ │ Reads tenant tier from Redis → applies tier-specific limits │ │ │
│ │ └───────────────────────────────────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌──────────────────┐ ┌────────────────────────────────────────────────┐ │ │
│ │ │ AGNTCY Routes │ │ Existing Phase 14 Routes (unchanged) │ │ │
│ │ │ (WS6) │ │ /agents, /oauth2, /credentials, /audit, │ │ │
│ │ │ /agntcy/ │ │ /marketplace, /billing, /health, /oidc, etc. │ │ │
│ │ │ compliance-report│ └────────────────────────────────────────────────┘ │ │
│ │ │ /agents/:id/ │ │ │
│ │ │ agent-card │ │ │
│ │ └──────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────┬┴──────────────────────┐ │
│ │ │ │ │
│ ┌────────────▼────────┐ ┌───────────▼──────────┐ ┌────────▼──────────────┐ │
│ │ PostgreSQL 14+ │ │ Redis 7+ │ │ External Services │ │
│ │ │ │ │ │ │ │
│ │ delegation_chains │ │ tier_cache:{tenantId} │ │ Stripe (billing) │ │
│ │ (WS2 - new) │ │ delegation_cache:{id} │ │ HashiCorp Vault │ │
│ │ analytics_daily_ │ │ analytics_cache:{k} │ │ OPA Policy Engine │ │
│ │ aggregates (WS3) │ │ │ │ │ │
│ │ tenant_subscriptions │ │ │ │ │ │
│ │ usage_events │ │ │ │ │ │
│ │ (Phase 4 — existing) │ │ │ │ │ │
│ └─────────────────────┘ └────────────────────────┘ └───────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────────────┐ │
│ │ External SDKs & Tooling │ │
│ │ sdk-rust/ (WS1 — new) │ cli/ (extended WS5) │ AGNTCY Test Suite │ │
│ └──────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
```
## System-Level Data Flows
### WS2: A2A Delegation Flow
```
Agent A (Delegator) SentryAgent.ai API Agent B (Delegatee)
│ │ │
│ POST /oauth2/token/delegate │ │
│ { agentId: A, delegateeId: B, │ │
│ scopes: [...], ttl: 3600 } │ │
│──────────────────────────────────>│ │
│ │ 1. Authenticate Agent A │
│ │ 2. Validate B exists │
│ │ 3. Verify scopes ⊆ A's scopes │
│ │ 4. Sign delegation payload │
│ │ with A's credential │
│ │ 5. INSERT delegation_chains │
│ │ 6. Return delegation token │
│ { delegationToken, chainId } │ │
│<──────────────────────────────────│ │
│ │ │
│ (out of band: share delegationToken with Agent B) │
│────────────────────────────────────────────────────────────────> │
│ │ │
│ │ POST /oauth2/token/verify-delegation
│ │ { delegationToken } │
│ │<─────────────────────────────│
│ │ 1. Decode token │
│ │ 2. Fetch chain from DB │
│ │ 3. Verify signature │
│ │ 4. Check expiry & revocation │
│ │ 5. Return chain + scopes │
│ │ { valid, scopes, chainId } │
│ │─────────────────────────────>│
```
### WS3: Analytics Aggregation Flow
```
API Request (any route) Middleware PostgreSQL
│ │ │
│ (every authenticated req) │ │
│──────────────────────────── >│ │
│ │ increment in-memory counter │
│ │ {tenantId, agentId, metric} │
│ │ │
│ │ (every 60s flush — Phase 4) │
│ │──────────────────────────────>│
│ │ INSERT usage_events │
│ │ │
│ │
(00:05 UTC daily) │ │
node-cron job │ │
──────────────────────>│ │
│ aggregate usage_events │
│ for previous day │
│──────────────────────────────>│
│ INSERT analytics_daily_ │
│ aggregates (upsert) │
GET /analytics/agent-activity AnalyticsController AnalyticsService
│ │ │
│──────────────────────────────>│ │
│ │ checkCache(Redis) │
│ │ (miss) → queryAggregates() │
│ │─────────────────────────────>│
│ │ SELECT from │
│ │ analytics_daily_aggregates │
│ │<─────────────────────────────│
│ │ writeCache(Redis, 5min TTL) │
│ { agents: [...heatmap] } │ │
│<──────────────────────────────│ │
```
### WS5: Scaffold Generation Flow
```
Developer (CLI) SentryAgent.ai API ScaffoldService
│ │ │
│ sentryagent scaffold │ │
│ --agent-id abc123 │ │
│ --language typescript │ │
│ │ │
│ GET /sdk/scaffold/abc123 │ │
│ ?language=typescript │ │
│─────────────────────────────────>│ │
│ │ authenticate request │
│ │ fetch agent credentials │
│ │──────────────────────────>│
│ │ generateScaffold( │
│ │ agentId, clientId, │
│ │ language, apiUrl) │
│ │ build ZIP in-memory: │
│ │ - package.json │
│ │ - index.ts │
│ │ - .env.example │
│ │ - README.md │
│ │<──────────────────────────│
│ (ZIP stream, Content- │ │
│ Disposition: attachment) │ │
│<─────────────────────────────────│ │
│ │ │
│ unzip → ready-to-run project │ │
```
## Risks / Trade-offs
- **[Risk] Rust SDK compile times in CI** — Mitigation: Use `sccache` in CI to cache compiled Rust dependencies. The SDK has minimal dependencies — compile time is bounded.
- **[Risk] A2A delegation scope creep** — Mitigation: Delegated scopes are strictly a subset of the delegator's own scopes (enforced at issuance, not just verification). A delegatee cannot escalate privileges beyond what the delegator holds.
- **[Risk] Analytics aggregation job failure leaves stale data** — Mitigation: Aggregation job is idempotent (upsert on `(tenant_id, agent_id, date, metric_type)`). A failed job can be re-run for any date without producing duplicate data.
- **[Risk] Scaffold ZIP includes clientId but not clientSecret** — Mitigation: The scaffold `.env.example` includes `AGENT_CLIENT_ID=<your-client-id>` with a placeholder for `AGENT_CLIENT_SECRET=<your-client-secret>`. The secret is never returned by the scaffold endpoint — developers copy it from the credentials page once.
- **[Risk] Elements (Swagger UI v5) breaking change in portal** — Mitigation: Elements is a drop-in React component. The existing `swagger-ui-react` dependency is replaced, not wrapped. The `/api-explorer` page is isolated — no other portal pages are affected.
- **[Risk] AGNTCY compliance report reflects live state but AGNTCY spec may update** — Mitigation: The report includes the AGNTCY spec version it was evaluated against (`agntcy_spec_version` field). Report consumers can detect when the evaluation is stale relative to a newer AGNTCY spec.
## Migration Plan
1. **WS1 first** (independent, no API changes): Build and publish the Rust SDK. No server-side migrations required.
2. **WS2 second** (requires migration `008_add_delegation_chains.sql`): Apply migration first, then deploy delegation endpoints. No breaking changes to existing endpoints.
3. **WS3 + WS4 in parallel** (WS3 requires migration `009_add_analytics_aggregates.sql`; WS4 requires no migration): Apply WS3 migration, deploy analytics endpoints, schedule nightly aggregation job. WS4 tier rate limiter deploys behind `TIER_RATE_LIMITING_ENABLED` feature flag.
4. **WS5** (extends portal and CLI — independent deployments): Deploy portal with Elements upgrade. Publish updated CLI to npm with `scaffold` command.
5. **WS6 last** (reads live system state — no migrations): Deploy AGNTCY compliance endpoints. Run interoperability test suite in CI on every commit going forward.
**Rollback strategy per workstream:**
- WS1 (Rust SDK): Publish to crates.io is permanent — yanked if critical bug found. No server-side rollback needed.
- WS2 (A2A): Disable delegation routes via `A2A_ENABLED=false` feature flag. `delegation_chains` table is additive — leaving it in place causes no harm.
- WS3 (Analytics): Disable analytics routes via `ANALYTICS_ENABLED=false`. Aggregation job is a cron — disable in deployment config.
- WS4 (Tiers): Revert `TierRateLimiter` middleware to flat `RateLimiterRedis` middleware via `TIER_RATE_LIMITING_ENABLED=false`.
- WS5 (DX): Revert portal deploy to previous version. Publish CLI patch release removing scaffold command.
- WS6 (AGNTCY): Disable AGNTCY routes via `AGNTCY_ENABLED=false` feature flag. No state changes — read-only endpoints.

View File

@@ -0,0 +1,74 @@
## Why
Phase 4 made SentryAgent.ai discoverable and adoptable — developers can now find, register, and use agent identities through the portal, CLI, and marketplace. Phase 5 completes the platform's ambition: to become the definitive global standard for agent identity. This requires three things: (1) language coverage parity (Rust SDK — the final missing language), (2) protocol innovation (A2A delegation — agents authorizing agents), and (3) ecosystem lock-in through compliance certification, advanced analytics, and developer tooling that competitors cannot match.
## What Changes
- **Rust SDK** (`sdk-rust/`): The last major language SDK. Rust developers are disproportionately building high-performance and safety-critical AI agents. Supporting Rust signals platform maturity and seriousness.
- **Agent-to-Agent (A2A) Authorization**: A new authorization primitive — one agent delegates authority to another agent via a verifiable delegation chain. Critical for multi-agent orchestration workflows (e.g., an orchestrator agent issuing sub-tasks to worker agents). New endpoints: `POST /oauth2/token/delegate` and `POST /oauth2/token/verify-delegation`. New DB table: `delegation_chains`.
- **Advanced Analytics Dashboard**: Tenant-facing usage analytics — agent activity heatmaps, token issuance trends, credential rotation frequency, API call pattern breakdowns. New endpoints under `GET /analytics/`. Extends the existing React dashboard (`dashboard/`).
- **Public API Gateway & Rate Limiting SaaS**: Multi-tier rate limits (free/pro/enterprise) enforced at the gateway layer with self-service tier upgrade. New endpoints `GET /tiers` and `POST /billing/upgrade`. Extends the existing Stripe billing and free-tier enforcement from Phase 4.
- **Developer Experience (DX) Improvements**: Swagger UI v5 with `elements` theme in the developer portal, an SDK code generator endpoint (`GET /sdk/scaffold/:agentId`), and a `sentryagent scaffold` CLI command that generates a language-specific starter project with agent auth pre-wired.
- **AGNTCY Compliance Certification Package**: Auto-generated AGNTCY compliance report, agent card export per the AGNTCY spec, a Jest-based interoperability test suite, and a certification guide document. Positions SentryAgent.ai as the reference implementation for AGNTCY.
## Capabilities
### New Capabilities
- `rust-sdk`: Async Rust SDK with `tokio` + `reqwest`, full 14-endpoint coverage, typed error model, thread-safe TokenManager
- `a2a-authorization`: Delegation token issuance and chain verification — agents can authorize other agents with scoped, auditable delegation chains
- `analytics-dashboard`: Tenant analytics — heatmaps, trends, rotation frequency, API patterns — rendered in the existing web dashboard
- `api-gateway-tiers`: Multi-tier rate limits with self-service upgrade; tier definitions returned from `GET /tiers`; upgrade initiated via `POST /billing/upgrade`
- `developer-experience`: Swagger UI v5 with elements theme, SDK scaffold generator endpoint, `sentryagent scaffold` CLI command
- `agntcy-compliance`: Machine-readable compliance report, agent card export, AGNTCY interoperability test suite, certification documentation
### Modified Capabilities
- `web-dashboard` (`dashboard/`): New Analytics tab with charts (recharts) and metric drill-downs
- `cli-tool` (`cli/`): New `sentryagent scaffold` command added to existing CLI package
- `developer-portal` (`portal/`): Swagger UI upgraded from v4 to v5 with elements theme
- `billing-metering`: Extended with `POST /billing/upgrade` self-service tier upgrade; multi-tier rate limit enforcement
- `monitoring`: New Prometheus metrics for delegation chain depth, analytics query latency, scaffold generation count
## Impact
**Code affected:**
- `sdk-rust/` — new Rust crate (entirely new directory)
- `src/services/DelegationService.ts` — new service for A2A delegation
- `src/controllers/DelegationController.ts` — new controller for delegation endpoints
- `src/services/AnalyticsService.ts` — new service for tenant analytics queries
- `src/controllers/AnalyticsController.ts` — new controller for analytics endpoints
- `src/routes/analytics.ts` — new Express router
- `src/routes/delegation.ts` — new Express router
- `src/routes/tiers.ts` — new Express router for tier definitions
- `src/services/BillingService.ts` — extended with `upgradeTier()` method
- `src/controllers/BillingController.ts` — extended with `POST /billing/upgrade` handler
- `src/services/ScaffoldService.ts` — new service generating scaffold ZIP archives
- `src/controllers/ScaffoldController.ts` — new controller for scaffold endpoint
- `src/middleware/tierRateLimiter.ts` — new middleware enforcing per-tier limits
- `src/infrastructure/migrations/008_add_delegation_chains.sql` — new migration
- `src/infrastructure/migrations/009_add_analytics_aggregates.sql` — new migration
- `dashboard/src/pages/Analytics.tsx` — new analytics page added to existing dashboard
- `dashboard/src/components/charts/` — new chart components (heatmap, trends, rotation)
- `portal/app/api-explorer/page.tsx` — upgrade Swagger UI to v5 with elements theme
- `cli/src/commands/scaffold.ts` — new scaffold command in existing CLI
- `docs/agntcy/certification-guide.md` — new AGNTCY certification documentation
- `tests/agntcy/interoperability.test.ts` — new AGNTCY interoperability test suite
**New dependencies (requires CEO approval):**
| Dependency | Workspace | Justification |
|---|---|---|
| `tokio` (Rust) | `sdk-rust/` | Async runtime — standard for async Rust; no viable alternative |
| `reqwest` (Rust) | `sdk-rust/` | HTTP client for Rust — most popular, async-native, TLS-enabled |
| `serde` + `serde_json` (Rust) | `sdk-rust/` | JSON serialization in Rust — de facto standard, zero alternatives |
| `uuid` (Rust crate) | `sdk-rust/` | UUID v4 generation in Rust — standard, lightweight |
| `thiserror` (Rust crate) | `sdk-rust/` | Ergonomic typed error derive macros — DRY error definitions |
| `recharts` | `dashboard/` | React charting library — composable, TypeScript-native, well-maintained |
| `date-fns` | `dashboard/` | Date manipulation for analytics trend queries — lightweight, tree-shakeable |
| `archiver` | `src/` (API) | ZIP archive creation for scaffold generator — battle-tested Node.js archiver |
| `@stoplight/elements` | `portal/` | Swagger UI v5 / Elements theme — modern, interactive, component-based API docs |
**Approved scope (CEO-confirmed 2026-04-02):** WS1 → WS2 → WS5
**Deferred to Phase 6:** WS3 (Advanced Analytics), WS4 (API Gateway Tiers), WS6 (AGNTCY Compliance Certification)

View File

@@ -0,0 +1,254 @@
## WS2: Agent-to-Agent (A2A) Authorization
### Purpose
Enable AI agents to delegate authority to other AI agents via verifiable, auditable, revocable delegation chains. This is a first-class authorization primitive aligned with the AGNTCY multi-agent orchestration model: an orchestrator agent issues sub-tasks to worker agents and must grant those workers scoped authority to act on its behalf.
A delegation chain is: Agent A (delegator) issues a delegation token granting Agent B (delegatee) a subset of A's own scopes for a bounded time period. Agent B presents this token to verify its delegated authority. The chain is stored in PostgreSQL, signed cryptographically, and audited in the existing audit log.
### New Endpoints
#### `POST /oauth2/token/delegate`
**Summary:** Delegate authority from one agent to another.
**Authentication:** Bearer token (the delegating agent's access token).
**Request Body** (`application/json`):
```json
{
"delegateeAgentId": "string",
"scopes": ["string"],
"ttlSeconds": 3600
}
```
| Field | Type | Required | Constraints |
|---|---|---|---|
| `delegateeAgentId` | string | yes | Must be an existing, active agent in the same tenant |
| `scopes` | string[] | yes | Min 1 item. Each scope must be a subset of the delegator's own scopes |
| `ttlSeconds` | integer | yes | Min: 60, Max: 86400 (24 hours) |
**Response 201** (`application/json`):
```json
{
"delegationToken": "string",
"chainId": "string (UUID)",
"delegatorAgentId": "string",
"delegateeAgentId": "string",
"scopes": ["string"],
"expiresAt": "string (ISO 8601)"
}
```
**Error Responses:**
| Status | Code | Description |
|---|---|---|
| 400 | `INVALID_SCOPES` | Requested scopes exceed delegator's own scopes |
| 400 | `INVALID_TTL` | `ttlSeconds` outside allowed range [60, 86400] |
| 401 | `UNAUTHORIZED` | Missing or invalid Bearer token |
| 404 | `AGENT_NOT_FOUND` | `delegateeAgentId` does not exist or is in a different tenant |
| 422 | `SELF_DELEGATION` | Delegator and delegatee are the same agent |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
**Business Rules:**
- Delegated scopes MUST be a strict subset of the delegator's own scopes (no privilege escalation)
- The delegatee must be an active agent in the same tenant as the delegator
- An agent may not delegate to itself
- A delegation entry is written to `delegation_chains` and an audit log entry is created with `event_type: "delegation.created"`
---
#### `POST /oauth2/token/verify-delegation`
**Summary:** Verify a delegation token and return the delegation chain details.
**Authentication:** Bearer token (any authenticated agent in the same tenant, or unauthenticated if `A2A_PUBLIC_VERIFY=true`).
**Request Body** (`application/json`):
```json
{
"delegationToken": "string"
}
```
| Field | Type | Required | Constraints |
|---|---|---|---|
| `delegationToken` | string | yes | The `delegationToken` value returned by `POST /oauth2/token/delegate` |
**Response 200** (`application/json`):
```json
{
"valid": true,
"chainId": "string (UUID)",
"delegatorAgentId": "string",
"delegateeAgentId": "string",
"scopes": ["string"],
"issuedAt": "string (ISO 8601)",
"expiresAt": "string (ISO 8601)",
"revokedAt": null
}
```
**Response when delegation is expired or revoked** (HTTP 200, not 4xx — the token exists but is not valid):
```json
{
"valid": false,
"chainId": "string (UUID)",
"delegatorAgentId": "string",
"delegateeAgentId": "string",
"scopes": ["string"],
"issuedAt": "string (ISO 8601)",
"expiresAt": "string (ISO 8601)",
"revokedAt": "string (ISO 8601) | null"
}
```
**Error Responses:**
| Status | Code | Description |
|---|---|---|
| 400 | `MALFORMED_TOKEN` | Token is not a valid delegation token format |
| 401 | `UNAUTHORIZED` | Missing Bearer token (when `A2A_PUBLIC_VERIFY=false`) |
| 404 | `CHAIN_NOT_FOUND` | No delegation chain found for the given token |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
**Business Rules:**
- Expired delegations return `valid: false` — not an error response
- Revoked delegations return `valid: false` with `revokedAt` populated
- Verification is non-destructive (does not consume or modify the delegation)
- An audit log entry is created with `event_type: "delegation.verified"` on every call
---
#### `DELETE /oauth2/token/delegate/:chainId`
**Summary:** Revoke a delegation chain. Only the delegator agent can revoke.
**Authentication:** Bearer token (must be the delegator agent's token).
**Path Parameter:**
| Parameter | Type | Description |
|---|---|---|
| `chainId` | string (UUID) | The chain ID returned at delegation creation |
**Response 204:** No body.
**Error Responses:**
| Status | Code | Description |
|---|---|---|
| 401 | `UNAUTHORIZED` | Missing or invalid Bearer token |
| 403 | `FORBIDDEN` | Authenticated agent is not the delegator of this chain |
| 404 | `CHAIN_NOT_FOUND` | No delegation chain with this ID |
| 409 | `ALREADY_REVOKED` | Delegation chain has already been revoked |
**Business Rules:**
- Sets `revoked_at` timestamp on the `delegation_chains` row
- Audit log entry created with `event_type: "delegation.revoked"`
- Revoking a parent chain does NOT cascade-revoke child chains — each link must be revoked explicitly
---
### Database Schema Changes
#### Migration: `008_add_delegation_chains.sql`
```sql
CREATE TABLE delegation_chains (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
delegator_agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
delegatee_agent_id UUID NOT NULL REFERENCES agents(id) ON DELETE CASCADE,
scopes TEXT[] NOT NULL,
delegation_token TEXT NOT NULL UNIQUE,
signature TEXT NOT NULL, -- HMAC-SHA256 of delegation payload, keyed by delegator secret
ttl_seconds INTEGER NOT NULL CHECK (ttl_seconds BETWEEN 60 AND 86400),
issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
expires_at TIMESTAMPTZ NOT NULL,
revoked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Index for token lookup (verify-delegation hot path)
CREATE UNIQUE INDEX idx_delegation_chains_token ON delegation_chains(delegation_token);
-- Index for listing delegations by agent
CREATE INDEX idx_delegation_chains_delegator ON delegation_chains(delegator_agent_id, tenant_id);
CREATE INDEX idx_delegation_chains_delegatee ON delegation_chains(delegatee_agent_id, tenant_id);
-- Index for cleanup of expired chains
CREATE INDEX idx_delegation_chains_expires_at ON delegation_chains(expires_at);
```
### New Source Files
| File | Description |
|---|---|
| `src/services/DelegationService.ts` | Business logic: create delegation, verify chain, revoke chain |
| `src/controllers/DelegationController.ts` | HTTP handlers for delegation endpoints |
| `src/routes/delegation.ts` | Express router: `POST /oauth2/token/delegate`, `POST /oauth2/token/verify-delegation`, `DELETE /oauth2/token/delegate/:chainId` |
| `src/types/delegation.ts` | TypeScript interfaces: `DelegationChain`, `CreateDelegationRequest`, `VerifyDelegationRequest`, `DelegationTokenPayload` |
| `src/utils/delegationCrypto.ts` | HMAC-SHA256 signing and verification for delegation payloads — extracted utility, no duplication |
### Modified Source Files
| File | Change |
|---|---|
| `src/routes/index.ts` | Register `delegation` router |
| `src/infrastructure/migrations/` | Add `008_add_delegation_chains.sql` |
| `docs/openapi.yaml` | Add delegation endpoints |
### `DelegationService` Interface
```typescript
interface IDelegationService {
/**
* Create a delegation chain from delegator to delegatee.
* Validates scope subset, signs payload, inserts DB row, writes audit log.
*/
createDelegation(
tenantId: string,
delegatorAgentId: string,
request: CreateDelegationRequest
): Promise<DelegationChain>;
/**
* Verify a delegation token. Returns chain details with valid flag.
* Does not throw on expired/revoked — returns valid: false.
*/
verifyDelegation(delegationToken: string): Promise<DelegationVerificationResult>;
/**
* Revoke a delegation chain. Only the delegator may revoke.
*/
revokeDelegation(chainId: string, requestingAgentId: string): Promise<void>;
}
```
### Prometheus Metrics
| Metric | Type | Labels | Description |
|---|---|---|---|
| `agentidp_delegations_created_total` | Counter | `tenant_id` | Total delegation chains created |
| `agentidp_delegations_verified_total` | Counter | `tenant_id`, `result` (valid/invalid/expired/revoked) | Delegation verification outcomes |
| `agentidp_delegations_revoked_total` | Counter | `tenant_id` | Total delegations revoked |
| `agentidp_delegation_chain_depth` | Histogram | `tenant_id` | Distribution of delegation chain nesting depth |
### Feature Flag
`A2A_ENABLED` environment variable (default: `true`). When `false`, all `/oauth2/token/delegate*` routes return HTTP 404.
### Acceptance Criteria
- `POST /oauth2/token/delegate` creates a delegation chain and returns a delegation token
- Scope subset validation rejects any scope not held by the delegating agent
- `POST /oauth2/token/verify-delegation` returns `valid: true` for active chains
- `POST /oauth2/token/verify-delegation` returns `valid: false` (not 4xx) for expired or revoked chains
- `DELETE /oauth2/token/delegate/:chainId` sets `revoked_at` and subsequent verification returns `valid: false`
- A 403 is returned when a non-delegator agent attempts to revoke a chain
- All delegation events are written to the audit log with correct `event_type`
- Delegation crypto signature uses HMAC-SHA256 — verified at `verify-delegation` time
- Unit test coverage >= 80% on `DelegationService` and `delegationCrypto`
- Integration tests cover: create, verify (valid), verify (expired), verify (revoked), revoke, unauthorized revoke

View File

@@ -0,0 +1,228 @@
## WS5: Developer Experience (DX) Improvements
### Purpose
Reduce time-to-first-successful-agent-call to under 5 minutes for a new developer. Three concrete improvements: (1) upgrade the developer portal's API explorer from Swagger UI v4 to Stoplight Elements — a modern, component-based API documentation experience with better navigation, code samples, and mock server support; (2) add a scaffold generator endpoint that returns a language-specific starter project pre-wired with the developer's agent credentials as a downloadable ZIP; (3) add a `sentryagent scaffold` CLI command that calls the scaffold endpoint and extracts the ZIP into the current directory.
### New Endpoint
#### `GET /sdk/scaffold/:agentId`
**Summary:** Generate and return a language-specific scaffold ZIP for the specified agent.
**Authentication:** Bearer token (tenant-scoped). The authenticated tenant must own the specified agent.
**Path Parameter:**
| Parameter | Type | Description |
|---|---|---|
| `agentId` | string (UUID) | The agent for which to generate the scaffold |
**Query Parameters:**
| Parameter | Type | Required | Default | Constraints |
|---|---|---|---|---|
| `language` | string | no | `typescript` | Enum: `typescript`, `python`, `go`, `java`, `rust` |
**Response 200:**
- Content-Type: `application/zip`
- Content-Disposition: `attachment; filename="sentryagent-scaffold-{agentName}-{language}.zip"`
- Body: Binary ZIP archive stream
**ZIP Archive Contents (TypeScript example):**
```
sentryagent-scaffold-my-agent-typescript/
├── package.json (name: my-agent, version: 0.1.0, deps: sentryagent-idp-sdk)
├── tsconfig.json (strict mode, ES2022 target)
├── .env.example (AGENTIDP_API_URL, AGENTIDP_CLIENT_ID=<pre-filled>, AGENTIDP_CLIENT_SECRET=<placeholder>)
├── .gitignore (.env on first line)
├── src/
│ └── index.ts (imports SDK, creates client from env, issues token, logs success)
└── README.md (step-by-step: cp .env.example .env, fill secret, npm install, npm start)
```
**ZIP Archive Contents (Python example):**
```
sentryagent-scaffold-my-agent-python/
├── requirements.txt (sentryagent-idp)
├── .env.example (AGENTIDP_API_URL, AGENTIDP_CLIENT_ID=<pre-filled>, AGENTIDP_CLIENT_SECRET=<placeholder>)
├── .gitignore (.env on first line)
├── main.py (imports SDK, creates client from env, issues token, prints success)
└── README.md (step-by-step: cp .env.example .env, fill secret, pip install -r requirements.txt, python main.py)
```
**ZIP Archive Contents (Go example):**
```
sentryagent-scaffold-my-agent-go/
├── go.mod (module: my-agent, dep: github.com/sentryagent/sentryagent-idp-go)
├── .env.example (AGENTIDP_API_URL, AGENTIDP_CLIENT_ID=<pre-filled>, AGENTIDP_CLIENT_SECRET=<placeholder>)
├── .gitignore (.env on first line)
├── main.go (imports SDK, creates client from env, issues token, logs success)
└── README.md (step-by-step instructions)
```
**Error Responses:**
| Status | Code | Description |
|---|---|---|
| 400 | `INVALID_LANGUAGE` | `language` query param is not one of the supported values |
| 401 | `UNAUTHORIZED` | Missing or invalid Bearer token |
| 403 | `FORBIDDEN` | Authenticated tenant does not own this agent |
| 404 | `AGENT_NOT_FOUND` | No agent with `agentId` found |
| 429 | `RATE_LIMITED` | Rate limit exceeded |
**Business Rules:**
- `clientId` is pre-filled in `.env.example` — taken from the agent's credentials in the database
- `clientSecret` is always a `<your-client-secret>` placeholder — never returned in scaffold (credentials security policy)
- The ZIP is generated in memory using `archiver` — no disk writes on the server
- Scaffold generation is rate-limited to 10 requests per minute per tenant (separate from the main tier rate limit)
- An audit log entry is created with `event_type: "scaffold.generated"`, `metadata.language`
---
### Developer Portal: Elements API Explorer Upgrade
**File to modify:** `portal/app/api-explorer/page.tsx`
**Current state (Phase 4):** Embeds `swagger-ui-react` (Swagger UI v4) loaded from `NEXT_PUBLIC_API_URL/openapi.json`.
**New state (Phase 5):** Replaces `swagger-ui-react` with `@stoplight/elements` (`<API>` component). Stoplight Elements provides: three-panel layout (navigation, docs, try-it), built-in code samples in multiple languages, mock server support, and better mobile responsiveness.
**Implementation:**
```tsx
// portal/app/api-explorer/page.tsx (complete replacement)
'use client';
import { API } from '@stoplight/elements';
import '@stoplight/elements/styles.min.css';
export default function ApiExplorerPage() {
return (
<main className="h-screen w-full">
<API
apiDescriptionUrl={`${process.env.NEXT_PUBLIC_API_URL}/openapi.json`}
router="hash"
layout="sidebar"
hideSchemas={false}
tryItCredentialsPolicy="same-origin"
/>
</main>
);
}
```
**Files modified:**
- `portal/app/api-explorer/page.tsx` — replace Swagger UI component with Elements `<API>` component
- `portal/package.json` — replace `swagger-ui-react` with `@stoplight/elements`
---
### CLI: `sentryagent scaffold` Command
**File to create:** `cli/src/commands/scaffold.ts`
**Command syntax:**
```
sentryagent scaffold --agent-id <id> [--language typescript|python|go|java|rust] [--out <directory>]
```
**Options:**
| Option | Alias | Default | Description |
|---|---|---|---|
| `--agent-id <id>` | `-a` | (required) | Agent ID to scaffold for |
| `--language <lang>` | `-l` | `typescript` | Target language for scaffold |
| `--out <dir>` | `-o` | `.` (current dir) | Directory to extract scaffold ZIP into |
**Behavior:**
1. Load config from `~/.sentryagent/config.json` — fail with helpful message if not configured
2. Issue an API call: `GET /sdk/scaffold/{agentId}?language={language}` with Bearer token from `POST /oauth2/token`
3. Receive ZIP stream, pipe through `unzipper` to extract into `--out` directory
4. Print success message: `Scaffold generated at ./{agentName}-{language}/`
5. Print next steps:
```
Next steps:
1. cd {agentName}-{language}
2. cp .env.example .env
3. Add your AGENTIDP_CLIENT_SECRET to .env
4. npm install (or equivalent for your language)
5. npm start
```
**Error handling:**
- Agent not found: print `Agent {agentId} not found.`
- Forbidden: print `You do not own agent {agentId}.`
- Invalid language: print `Unsupported language '{lang}'. Choose: typescript, python, go, java, rust`
- Output directory does not exist: create it (with user prompt for confirmation if non-empty)
**New CLI dependencies** (add to `cli/package.json`):
- `unzipper` — streaming ZIP extraction (pure JS, no native deps)
### New Source Files
| File | Description |
|---|---|
| `src/services/ScaffoldService.ts` | Business logic: build ZIP archive in memory using `archiver` |
| `src/controllers/ScaffoldController.ts` | HTTP handler: stream ZIP response |
| `src/routes/scaffold.ts` | Express router: `GET /sdk/scaffold/:agentId` |
| `src/types/scaffold.ts` | TypeScript interfaces: `ScaffoldLanguage`, `ScaffoldOptions`, `ScaffoldTemplate` |
| `src/templates/scaffold/typescript/` | Template files for TypeScript scaffold (package.json, tsconfig.json, index.ts, .env.example, .gitignore, README.md) |
| `src/templates/scaffold/python/` | Template files for Python scaffold (requirements.txt, main.py, .env.example, .gitignore, README.md) |
| `src/templates/scaffold/go/` | Template files for Go scaffold (go.mod, main.go, .env.example, .gitignore, README.md) |
| `src/templates/scaffold/java/` | Template files for Java scaffold (pom.xml, Main.java, .env.example, .gitignore, README.md) |
| `src/templates/scaffold/rust/` | Template files for Rust scaffold (Cargo.toml, src/main.rs, .env.example, .gitignore, README.md) |
| `cli/src/commands/scaffold.ts` | CLI scaffold command implementation |
### Modified Source Files
| File | Change |
|---|---|
| `src/routes/index.ts` | Register `scaffold` router |
| `src/app.ts` | No change needed (routes registered via index) |
| `package.json` (API) | Add `archiver` and `@types/archiver` |
| `portal/app/api-explorer/page.tsx` | Replace Swagger UI with Elements |
| `portal/package.json` | Replace `swagger-ui-react` with `@stoplight/elements` |
| `cli/src/index.ts` | Register `scaffold` command with Commander |
| `cli/package.json` | Add `unzipper` and `@types/unzipper` |
| `docs/openapi.yaml` | Add `GET /sdk/scaffold/:agentId` endpoint |
### `ScaffoldService` Interface
```typescript
interface IScaffoldService {
/**
* Generate an in-memory ZIP archive for the given agent and language.
* Returns a Node.js Readable stream of the ZIP binary.
* Template variables injected: {{AGENT_ID}}, {{AGENT_NAME}}, {{CLIENT_ID}}, {{API_URL}}
*/
generateScaffold(
agentId: string,
language: ScaffoldLanguage,
apiUrl: string
): Promise<{ stream: NodeJS.ReadableStream; filename: string }>;
}
```
### Prometheus Metrics
| Metric | Type | Labels | Description |
|---|---|---|---|
| `agentidp_scaffold_generated_total` | Counter | `language` | Scaffold ZIPs generated by language |
| `agentidp_scaffold_generation_duration_ms` | Histogram | `language` | Time to generate scaffold ZIP |
### Acceptance Criteria
- `GET /sdk/scaffold/:agentId?language=typescript` returns a valid ZIP with all 6 template files
- ZIP contains `.env.example` with `AGENTIDP_CLIENT_ID` pre-filled and `AGENTIDP_CLIENT_SECRET=<your-client-secret>` as placeholder
- ZIP never contains the actual client secret
- `GET /sdk/scaffold/:agentId?language=python` returns Python-specific template files
- All 5 languages (typescript, python, go, java, rust) return valid ZIPs
- HTTP 400 on unknown `language` query param
- HTTP 403 when authenticated tenant does not own the agent
- `sentryagent scaffold --agent-id abc123 --language go` extracts scaffold to current directory
- `sentryagent scaffold --agent-id abc123 --language python --out /tmp/myagent` extracts to `/tmp/myagent`
- Developer portal `/api-explorer` renders Elements v5 with sidebar layout — TypeScript build passes
- Unit tests cover: scaffold generation (each language), forbidden access, invalid language
- Integration tests cover: scaffold endpoint response type, content-disposition header, ZIP validity

View File

@@ -0,0 +1,289 @@
## 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`

View File

@@ -0,0 +1,100 @@
## 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 6086400), `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`