feat(openspec): propose phase-5-scale-ecosystem change
6 workstreams, 119 tasks — Scale & Ecosystem: - WS1: Rust SDK - WS2: Agent-to-Agent (A2A) Authorization - WS3: Advanced Analytics Dashboard - WS4: Public API Gateway & Rate Limiting SaaS - WS5: Developer Experience (DX) improvements - WS6: AGNTCY Compliance Certification Package Awaiting CEO approval to begin implementation. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
58
openspec/changes/phase-5-scale-ecosystem/.openspec.yaml
Normal file
58
openspec/changes/phase-5-scale-ecosystem/.openspec.yaml
Normal file
@@ -0,0 +1,58 @@
|
||||
schema: spec-driven
|
||||
id: phase-5-scale-ecosystem
|
||||
title: "Phase 5 — Scale & Ecosystem"
|
||||
status: proposed
|
||||
created: "2026-04-02"
|
||||
author: Virtual Architect
|
||||
phase: 5
|
||||
theme: "Scale & Ecosystem — making SentryAgent.ai the definitive standard for agent identity globally"
|
||||
|
||||
workstreams:
|
||||
- id: ws1
|
||||
name: Rust SDK
|
||||
directory: specs/rust-sdk
|
||||
- id: ws2
|
||||
name: Agent-to-Agent (A2A) Authorization
|
||||
directory: specs/a2a-authorization
|
||||
- id: ws3
|
||||
name: Advanced Analytics Dashboard
|
||||
directory: specs/analytics-dashboard
|
||||
- id: ws4
|
||||
name: Public API Gateway & Rate Limiting SaaS
|
||||
directory: specs/api-gateway-tiers
|
||||
- id: ws5
|
||||
name: Developer Experience (DX) Improvements
|
||||
directory: specs/developer-experience
|
||||
- id: ws6
|
||||
name: AGNTCY Compliance Certification Package
|
||||
directory: specs/agntcy-compliance
|
||||
|
||||
artifacts:
|
||||
- proposal.md
|
||||
- design.md
|
||||
- specs/rust-sdk/spec.md
|
||||
- specs/a2a-authorization/spec.md
|
||||
- specs/analytics-dashboard/spec.md
|
||||
- specs/api-gateway-tiers/spec.md
|
||||
- specs/developer-experience/spec.md
|
||||
- specs/agntcy-compliance/spec.md
|
||||
- tasks.md
|
||||
|
||||
dependencies:
|
||||
new:
|
||||
- tokio
|
||||
- reqwest
|
||||
- serde
|
||||
- serde_json
|
||||
- swagger-ui-dist
|
||||
- elements-api
|
||||
- archiver
|
||||
- recharts
|
||||
- date-fns
|
||||
existing:
|
||||
- express
|
||||
- postgresql
|
||||
- redis
|
||||
- stripe
|
||||
- next
|
||||
- commander
|
||||
279
openspec/changes/phase-5-scale-ecosystem/design.md
Normal file
279
openspec/changes/phase-5-scale-ecosystem/design.md
Normal 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 500–2000ms 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 1–4 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.
|
||||
72
openspec/changes/phase-5-scale-ecosystem/proposal.md
Normal file
72
openspec/changes/phase-5-scale-ecosystem/proposal.md
Normal file
@@ -0,0 +1,72 @@
|
||||
## 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 |
|
||||
|
||||
**Delivery sequence:** WS1 → WS2 → WS3 + WS4 (parallel) → WS5 → WS6
|
||||
@@ -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
|
||||
@@ -0,0 +1,320 @@
|
||||
## WS6: AGNTCY Compliance Certification Package
|
||||
|
||||
### Purpose
|
||||
|
||||
Position SentryAgent.ai as the reference implementation for the AGNTCY standard. Deliver four artifacts: (1) an auto-generated machine-readable AGNTCY compliance report endpoint; (2) an agent card export endpoint per the AGNTCY Agent Card specification; (3) a Jest-based interoperability test suite verifying AGNTCY alignment on every CI run; (4) a human-readable certification guide documenting how SentryAgent.ai satisfies each AGNTCY requirement.
|
||||
|
||||
This workstream produces no user-facing UI changes. It is infrastructure for compliance, certification, and ecosystem trust.
|
||||
|
||||
### New Endpoints
|
||||
|
||||
#### `GET /agntcy/compliance-report`
|
||||
|
||||
**Summary:** Generate and return a real-time AGNTCY compliance report for the authenticated tenant's environment.
|
||||
|
||||
**Authentication:** Bearer token (tenant-scoped). The tenant's subscription tier must be `pro` or `enterprise`.
|
||||
|
||||
**Response 200** (`application/json`):
|
||||
```json
|
||||
{
|
||||
"reportId": "string (UUID)",
|
||||
"generatedAt": "string (ISO 8601)",
|
||||
"agntcySpecVersion": "1.0.0",
|
||||
"tenantId": "string (UUID)",
|
||||
"overallStatus": "compliant",
|
||||
"sections": [
|
||||
{
|
||||
"id": "agent-identity",
|
||||
"name": "Agent Identity",
|
||||
"status": "compliant",
|
||||
"requirements": [
|
||||
{
|
||||
"id": "AI-001",
|
||||
"description": "Each agent MUST have a globally unique, persistent identifier",
|
||||
"status": "compliant",
|
||||
"evidence": "All agents are assigned a UUID v4 at registration, stored immutably in agents.id",
|
||||
"verifiedAt": "string (ISO 8601)"
|
||||
},
|
||||
{
|
||||
"id": "AI-002",
|
||||
"description": "Each agent MUST have a W3C DID document",
|
||||
"status": "compliant",
|
||||
"evidence": "DID documents are auto-generated as did:web identifiers at agent registration",
|
||||
"verifiedAt": "string (ISO 8601)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "authentication",
|
||||
"name": "Authentication",
|
||||
"status": "compliant",
|
||||
"requirements": [
|
||||
{
|
||||
"id": "AUTH-001",
|
||||
"description": "Agent authentication MUST use OAuth 2.0 or OIDC",
|
||||
"status": "compliant",
|
||||
"evidence": "OAuth 2.0 Client Credentials flow implemented at POST /oauth2/token",
|
||||
"verifiedAt": "string (ISO 8601)"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "authorization",
|
||||
"name": "Authorization",
|
||||
"status": "compliant",
|
||||
"requirements": []
|
||||
},
|
||||
{
|
||||
"id": "audit-and-governance",
|
||||
"name": "Audit & Governance",
|
||||
"status": "compliant",
|
||||
"requirements": []
|
||||
},
|
||||
{
|
||||
"id": "interoperability",
|
||||
"name": "Interoperability",
|
||||
"status": "compliant",
|
||||
"requirements": []
|
||||
},
|
||||
{
|
||||
"id": "delegation",
|
||||
"name": "Agent-to-Agent Delegation",
|
||||
"status": "compliant",
|
||||
"requirements": []
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"totalRequirements": 24,
|
||||
"compliant": 24,
|
||||
"nonCompliant": 0,
|
||||
"notApplicable": 0
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`overallStatus`** values: `"compliant"` | `"partial"` | `"non-compliant"`
|
||||
|
||||
**Error Responses:**
|
||||
|
||||
| Status | Code | Description |
|
||||
|---|---|---|
|
||||
| 401 | `UNAUTHORIZED` | Missing or invalid Bearer token |
|
||||
| 403 | `TIER_REQUIRED` | Compliance report requires Pro or Enterprise tier |
|
||||
| 429 | `RATE_LIMITED` | Rate limit exceeded |
|
||||
|
||||
**Business Rules:**
|
||||
- Report is generated on demand from live system state — no cache
|
||||
- Each requirement's `status` is computed by querying current system configuration (e.g., verify DID documents exist by checking `agents` table, verify audit log is enabled by checking config)
|
||||
- `agntcySpecVersion` is hardcoded to the AGNTCY spec version the system was last validated against
|
||||
- An audit log entry is created with `event_type: "compliance.report_generated"`
|
||||
|
||||
---
|
||||
|
||||
#### `GET /agents/:id/agent-card`
|
||||
|
||||
**Summary:** Return the AGNTCY-compliant Agent Card for a specific agent. Agent Cards are publicly accessible for public agents and require authentication for private agents.
|
||||
|
||||
**Authentication:** Optional. Required only if the agent's `is_public` is `false`.
|
||||
|
||||
**Path Parameter:**
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|---|---|---|
|
||||
| `id` | string (UUID) | Agent ID |
|
||||
|
||||
**Response 200** (`application/json`):
|
||||
|
||||
Per the AGNTCY Agent Card specification:
|
||||
```json
|
||||
{
|
||||
"agntcyVersion": "1.0",
|
||||
"type": "agent-card",
|
||||
"agent": {
|
||||
"id": "string (UUID)",
|
||||
"name": "string",
|
||||
"description": "string | null",
|
||||
"did": "did:web:sentryagent.ai:agents:abc123",
|
||||
"capabilities": ["string"],
|
||||
"version": "string",
|
||||
"publisher": {
|
||||
"tenantId": "string (UUID)",
|
||||
"name": "string"
|
||||
},
|
||||
"endpoints": {
|
||||
"tokenEndpoint": "https://api.sentryagent.ai/oauth2/token",
|
||||
"delegationEndpoint": "https://api.sentryagent.ai/oauth2/token/delegate"
|
||||
},
|
||||
"authentication": {
|
||||
"schemes": ["oauth2_client_credentials"],
|
||||
"tokenEndpoint": "https://api.sentryagent.ai/oauth2/token"
|
||||
},
|
||||
"governance": {
|
||||
"auditLogEnabled": true,
|
||||
"credentialRotationPolicy": "manual",
|
||||
"complianceStandards": ["AGNTCY-1.0", "OAuth2-RFC6749", "W3C-DID"]
|
||||
},
|
||||
"metadata": {}
|
||||
},
|
||||
"issuedAt": "string (ISO 8601)",
|
||||
"expiresAt": "string (ISO 8601)"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
|
||||
| Status | Code | Description |
|
||||
|---|---|---|
|
||||
| 401 | `UNAUTHORIZED` | Agent is private and no Bearer token provided |
|
||||
| 403 | `FORBIDDEN` | Agent is private and authenticated tenant does not own it |
|
||||
| 404 | `AGENT_NOT_FOUND` | No agent with the given ID |
|
||||
| 429 | `RATE_LIMITED` | Rate limit exceeded |
|
||||
|
||||
**Business Rules:**
|
||||
- Public agents (`is_public: true`) return agent card without authentication
|
||||
- Private agents require the owning tenant's Bearer token
|
||||
- Agent card `expiresAt` is `issuedAt + 24 hours` (cards are short-lived — consumers should re-fetch daily)
|
||||
- `complianceStandards` array is sourced from system config, not per-agent configuration
|
||||
|
||||
---
|
||||
|
||||
### AGNTCY Interoperability Test Suite
|
||||
|
||||
**File:** `tests/agntcy/interoperability.test.ts`
|
||||
|
||||
A Jest test suite that verifies AGNTCY alignment on every CI run. Tests run against a live API instance (reads `AGENTIDP_API_URL` from environment).
|
||||
|
||||
**Test categories and cases:**
|
||||
|
||||
```typescript
|
||||
// AGNTCY-AI-001: Agent identity uniqueness
|
||||
test('each registered agent receives a unique UUID', ...)
|
||||
test('agent UUID is immutable after registration', ...)
|
||||
|
||||
// AGNTCY-AI-002: W3C DID documents
|
||||
test('registered agent has a valid did:web DID', ...)
|
||||
test('DID document resolves via GET /agents/:id', ...)
|
||||
|
||||
// AGNTCY-AUTH-001: OAuth 2.0 token issuance
|
||||
test('POST /oauth2/token returns access_token and token_type: bearer', ...)
|
||||
test('access token is a valid JWT with correct claims', ...)
|
||||
test('expired token is rejected with 401', ...)
|
||||
|
||||
// AGNTCY-AUTH-002: OIDC compliance
|
||||
test('GET /.well-known/openid-configuration returns valid OIDC discovery document', ...)
|
||||
test('JWKS endpoint returns valid JWK Set', ...)
|
||||
|
||||
// AGNTCY-AUTHZ-001: Scope-based access control
|
||||
test('token with agent:read scope cannot call agent:write operations', ...)
|
||||
test('scopes are included in JWT payload', ...)
|
||||
|
||||
// AGNTCY-DEL-001: Agent-to-Agent delegation
|
||||
test('POST /oauth2/token/delegate creates a valid delegation chain', ...)
|
||||
test('delegated scopes cannot exceed delegator scopes', ...)
|
||||
test('POST /oauth2/token/verify-delegation returns valid: true for active chain', ...)
|
||||
test('POST /oauth2/token/verify-delegation returns valid: false for expired chain', ...)
|
||||
|
||||
// AGNTCY-AUDIT-001: Immutable audit logs
|
||||
test('every token issuance creates an audit log entry', ...)
|
||||
test('audit log entries cannot be deleted via API', ...)
|
||||
|
||||
// AGNTCY-GOV-001: Agent lifecycle governance
|
||||
test('credential rotation is logged in audit log', ...)
|
||||
test('agent deletion logs deletion event in audit log', ...)
|
||||
|
||||
// AGNTCY-INTER-001: Agent Card export
|
||||
test('GET /agents/:id/agent-card returns valid AGNTCY Agent Card', ...)
|
||||
test('Agent Card contains required agntcyVersion, did, capabilities fields', ...)
|
||||
|
||||
// AGNTCY-COMP-001: Compliance report
|
||||
test('GET /agntcy/compliance-report returns compliant status', ...)
|
||||
test('compliance report covers all 6 AGNTCY sections', ...)
|
||||
test('compliance report totalRequirements >= 24', ...)
|
||||
```
|
||||
|
||||
**Running the suite:**
|
||||
```bash
|
||||
# In CI (requires live API):
|
||||
AGENTIDP_API_URL=http://localhost:3000 npm run test:agntcy
|
||||
|
||||
# Added to package.json:
|
||||
"test:agntcy": "jest --testPathPattern=tests/agntcy --forceExit"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### AGNTCY Certification Guide
|
||||
|
||||
**File:** `docs/agntcy/certification-guide.md`
|
||||
|
||||
A markdown document structured as follows:
|
||||
1. **Overview** — What AGNTCY certification means and how SentryAgent.ai achieves it
|
||||
2. **Requirement Mapping** — Table mapping each AGNTCY requirement ID to the SentryAgent.ai implementation (endpoint, service, or config)
|
||||
3. **Running the Compliance Report** — Step-by-step guide to generating and interpreting the compliance report
|
||||
4. **Agent Card Usage** — How to retrieve, cache, and use Agent Cards in multi-agent workflows
|
||||
5. **Self-Certification Checklist** — Checklist for operators deploying self-hosted SentryAgent.ai to verify their instance's compliance
|
||||
6. **Submitting for Official AGNTCY Certification** — Links and instructions for the Linux Foundation AGNTCY certification program
|
||||
|
||||
---
|
||||
|
||||
### New Source Files
|
||||
|
||||
| File | Description |
|
||||
|---|---|
|
||||
| `src/services/ComplianceService.ts` | Business logic: query system state, evaluate each AGNTCY requirement, build report |
|
||||
| `src/controllers/ComplianceController.ts` | HTTP handlers for compliance report and agent card endpoints |
|
||||
| `src/routes/agntcy.ts` | Express router: `GET /agntcy/compliance-report`, `GET /agents/:id/agent-card` |
|
||||
| `src/types/compliance.ts` | TypeScript interfaces: `ComplianceReport`, `ComplianceSection`, `ComplianceRequirement`, `AgentCard` |
|
||||
| `src/config/agntcyRequirements.ts` | Static array of AGNTCY requirement definitions (id, description, evaluator function reference) |
|
||||
| `tests/agntcy/interoperability.test.ts` | Jest interoperability test suite |
|
||||
| `docs/agntcy/certification-guide.md` | Human-readable certification guide |
|
||||
|
||||
### Modified Source Files
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/routes/index.ts` | Register `agntcy` router |
|
||||
| `src/routes/agents.ts` | Add `GET /agents/:id/agent-card` route (or register via agntcy router — agent-card is agent-scoped) |
|
||||
| `package.json` (API) | Add `"test:agntcy"` script |
|
||||
| `docs/openapi.yaml` | Add `GET /agntcy/compliance-report` and `GET /agents/:id/agent-card` endpoints |
|
||||
|
||||
### `ComplianceService` Interface
|
||||
|
||||
```typescript
|
||||
interface IComplianceService {
|
||||
/**
|
||||
* Generate a live AGNTCY compliance report for the given tenant.
|
||||
* Evaluates all registered AGNTCY requirements against current system state.
|
||||
*/
|
||||
generateComplianceReport(tenantId: string): Promise<ComplianceReport>;
|
||||
|
||||
/**
|
||||
* Generate an AGNTCY Agent Card for a specific agent.
|
||||
*/
|
||||
generateAgentCard(agentId: string): Promise<AgentCard>;
|
||||
}
|
||||
```
|
||||
|
||||
### Prometheus Metrics
|
||||
|
||||
| Metric | Type | Labels | Description |
|
||||
|---|---|---|---|
|
||||
| `agentidp_compliance_reports_generated_total` | Counter | `tenant_id` | Total compliance reports generated |
|
||||
| `agentidp_compliance_report_duration_ms` | Histogram | — | Time to generate compliance report |
|
||||
| `agentidp_agent_cards_served_total` | Counter | `visibility` (public/private) | Agent cards served by visibility |
|
||||
|
||||
### Feature Flag
|
||||
|
||||
`AGNTCY_ENABLED` (default: `true`). When `false`, all `/agntcy/` routes and `GET /agents/:id/agent-card` return HTTP 404.
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- `GET /agntcy/compliance-report` returns a report with `overallStatus: "compliant"` on a correctly configured instance
|
||||
- Report contains all 6 sections: agent-identity, authentication, authorization, audit-and-governance, interoperability, delegation
|
||||
- Report `totalRequirements >= 24`
|
||||
- `GET /agents/:id/agent-card` returns a valid AGNTCY Agent Card with all required fields
|
||||
- Agent Card is accessible without auth for public agents
|
||||
- Agent Card requires owning tenant's auth for private agents
|
||||
- All 25+ interoperability test cases pass against a live API instance
|
||||
- `npm run test:agntcy` exits 0 on a correctly configured instance
|
||||
- `docs/agntcy/certification-guide.md` is complete — no TODOs, no placeholders
|
||||
- Unit tests cover: compliance report generation (compliant system, partially compliant), agent card generation (public agent, private agent)
|
||||
@@ -0,0 +1,279 @@
|
||||
## WS3: Advanced Analytics Dashboard
|
||||
|
||||
### Purpose
|
||||
|
||||
Give paying tenants actionable visibility into their agent usage patterns. Analytics surface four dimensions: agent activity over time (heatmap), token issuance frequency and volume (trends), credential rotation frequency (rotation frequency table), and per-endpoint API call patterns (call patterns breakdown). Data is pre-aggregated nightly from the existing `usage_events` table into a new `analytics_daily_aggregates` table. Analytics are rendered in a new Analytics tab in the existing React web dashboard.
|
||||
|
||||
### New Endpoints
|
||||
|
||||
#### `GET /analytics/usage-summary`
|
||||
|
||||
**Summary:** Return a high-level usage summary for the authenticated tenant over a date range.
|
||||
|
||||
**Authentication:** Bearer token (tenant-scoped).
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Default | Constraints |
|
||||
|---|---|---|---|---|
|
||||
| `from` | string (YYYY-MM-DD) | no | 30 days ago | Must be <= `to` |
|
||||
| `to` | string (YYYY-MM-DD) | no | today | Must be <= today |
|
||||
|
||||
**Response 200** (`application/json`):
|
||||
```json
|
||||
{
|
||||
"tenantId": "string (UUID)",
|
||||
"period": {
|
||||
"from": "string (YYYY-MM-DD)",
|
||||
"to": "string (YYYY-MM-DD)"
|
||||
},
|
||||
"summary": {
|
||||
"totalApiCalls": 84320,
|
||||
"totalTokenIssuances": 12400,
|
||||
"totalCredentialRotations": 48,
|
||||
"activeAgentCount": 23,
|
||||
"averageDailyApiCalls": 2810,
|
||||
"peakDailyApiCalls": 5102,
|
||||
"peakDate": "2026-03-28"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
|
||||
| Status | Code | Description |
|
||||
|---|---|---|
|
||||
| 400 | `INVALID_DATE_RANGE` | `from` > `to`, or date range exceeds 365 days |
|
||||
| 401 | `UNAUTHORIZED` | Missing or invalid Bearer token |
|
||||
| 403 | `ANALYTICS_NOT_AVAILABLE` | Tenant is on free tier — analytics require Pro or Enterprise |
|
||||
| 429 | `RATE_LIMITED` | Rate limit exceeded |
|
||||
|
||||
---
|
||||
|
||||
#### `GET /analytics/agent-activity`
|
||||
|
||||
**Summary:** Return per-agent daily activity counts for heatmap rendering.
|
||||
|
||||
**Authentication:** Bearer token (tenant-scoped).
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Default | Constraints |
|
||||
|---|---|---|---|---|
|
||||
| `from` | string (YYYY-MM-DD) | no | 30 days ago | Must be <= `to` |
|
||||
| `to` | string (YYYY-MM-DD) | no | today | Max range: 90 days |
|
||||
| `agentId` | string (UUID) | no | (all agents) | Filter to a single agent |
|
||||
|
||||
**Response 200** (`application/json`):
|
||||
```json
|
||||
{
|
||||
"tenantId": "string (UUID)",
|
||||
"period": {
|
||||
"from": "string (YYYY-MM-DD)",
|
||||
"to": "string (YYYY-MM-DD)"
|
||||
},
|
||||
"agents": [
|
||||
{
|
||||
"agentId": "string (UUID)",
|
||||
"agentName": "string",
|
||||
"dailyActivity": [
|
||||
{
|
||||
"date": "2026-03-01",
|
||||
"apiCalls": 342,
|
||||
"tokenIssuances": 12,
|
||||
"credentialRotations": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
|
||||
| Status | Code | Description |
|
||||
|---|---|---|
|
||||
| 400 | `INVALID_DATE_RANGE` | `from` > `to`, or date range exceeds 90 days |
|
||||
| 401 | `UNAUTHORIZED` | Missing or invalid Bearer token |
|
||||
| 403 | `ANALYTICS_NOT_AVAILABLE` | Free tier — requires Pro or Enterprise |
|
||||
| 404 | `AGENT_NOT_FOUND` | `agentId` filter specified but agent does not belong to tenant |
|
||||
| 429 | `RATE_LIMITED` | Rate limit exceeded |
|
||||
|
||||
---
|
||||
|
||||
#### `GET /analytics/token-trends`
|
||||
|
||||
**Summary:** Return daily token issuance counts and success/failure breakdown for trend charts.
|
||||
|
||||
**Authentication:** Bearer token (tenant-scoped).
|
||||
|
||||
**Query Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Default | Constraints |
|
||||
|---|---|---|---|---|
|
||||
| `from` | string (YYYY-MM-DD) | no | 30 days ago | Must be <= `to` |
|
||||
| `to` | string (YYYY-MM-DD) | no | today | Max range: 365 days |
|
||||
| `granularity` | string | no | `day` | Enum: `day`, `week` |
|
||||
|
||||
**Response 200** (`application/json`):
|
||||
```json
|
||||
{
|
||||
"tenantId": "string (UUID)",
|
||||
"period": {
|
||||
"from": "string (YYYY-MM-DD)",
|
||||
"to": "string (YYYY-MM-DD)"
|
||||
},
|
||||
"granularity": "day",
|
||||
"dataPoints": [
|
||||
{
|
||||
"date": "2026-03-01",
|
||||
"totalIssuances": 420,
|
||||
"successfulIssuances": 415,
|
||||
"failedIssuances": 5,
|
||||
"uniqueAgents": 8
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
|
||||
| Status | Code | Description |
|
||||
|---|---|---|
|
||||
| 400 | `INVALID_DATE_RANGE` | `from` > `to`, or date range exceeds 365 days |
|
||||
| 400 | `INVALID_GRANULARITY` | `granularity` is not `day` or `week` |
|
||||
| 401 | `UNAUTHORIZED` | Missing or invalid Bearer token |
|
||||
| 403 | `ANALYTICS_NOT_AVAILABLE` | Free tier — requires Pro or Enterprise |
|
||||
| 429 | `RATE_LIMITED` | Rate limit exceeded |
|
||||
|
||||
---
|
||||
|
||||
### Database Schema Changes
|
||||
|
||||
#### Migration: `009_add_analytics_aggregates.sql`
|
||||
|
||||
```sql
|
||||
CREATE TABLE analytics_daily_aggregates (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
||||
agent_id UUID REFERENCES agents(id) ON DELETE SET NULL, -- NULL = tenant-wide aggregate
|
||||
date DATE NOT NULL,
|
||||
metric_type VARCHAR(64) NOT NULL, -- 'api_calls' | 'token_issuances' | 'credential_rotations' | 'token_failures'
|
||||
count BIGINT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
CONSTRAINT uq_daily_aggregate UNIQUE (tenant_id, agent_id, date, metric_type)
|
||||
);
|
||||
|
||||
-- Index for analytics queries (tenant + date range)
|
||||
CREATE INDEX idx_analytics_tenant_date ON analytics_daily_aggregates(tenant_id, date);
|
||||
CREATE INDEX idx_analytics_agent_date ON analytics_daily_aggregates(agent_id, date) WHERE agent_id IS NOT NULL;
|
||||
```
|
||||
|
||||
#### Nightly Aggregation Job
|
||||
|
||||
A `node-cron` job runs at `00:05 UTC` daily inside the Express API process. It executes an upsert query aggregating the previous day's `usage_events` rows into `analytics_daily_aggregates`. The job is idempotent — running it twice for the same date produces no duplicates (upsert on the unique constraint).
|
||||
|
||||
Job logic (pseudocode):
|
||||
```
|
||||
1. Compute target_date = yesterday (UTC)
|
||||
2. SELECT tenant_id, agent_id, metric_type, SUM(count)
|
||||
FROM usage_events
|
||||
WHERE date = target_date
|
||||
GROUP BY tenant_id, agent_id, metric_type
|
||||
3. UPSERT INTO analytics_daily_aggregates
|
||||
ON CONFLICT (tenant_id, agent_id, date, metric_type)
|
||||
DO UPDATE SET count = EXCLUDED.count, updated_at = NOW()
|
||||
```
|
||||
|
||||
### New Source Files
|
||||
|
||||
| File | Description |
|
||||
|---|---|
|
||||
| `src/services/AnalyticsService.ts` | Business logic: query aggregates, build response shapes, Redis caching |
|
||||
| `src/controllers/AnalyticsController.ts` | HTTP handlers for analytics endpoints |
|
||||
| `src/routes/analytics.ts` | Express router for `/analytics/` prefix |
|
||||
| `src/jobs/analyticsAggregation.ts` | `node-cron` job that aggregates usage_events nightly |
|
||||
| `src/types/analytics.ts` | TypeScript interfaces: `UsageSummary`, `AgentActivityResponse`, `TokenTrendsResponse`, `DailyAggregate` |
|
||||
| `dashboard/src/pages/Analytics.tsx` | New Analytics tab in existing React dashboard |
|
||||
| `dashboard/src/components/charts/AgentHeatmap.tsx` | Heatmap component using `recharts` `ResponsiveContainer` + custom cells |
|
||||
| `dashboard/src/components/charts/TokenTrendsChart.tsx` | Line chart of token issuance over time using `recharts` `LineChart` |
|
||||
| `dashboard/src/components/charts/RotationFrequencyTable.tsx` | Sortable table of credential rotation counts per agent |
|
||||
| `dashboard/src/api/analyticsApi.ts` | Typed fetch functions for analytics endpoints |
|
||||
|
||||
### Modified Source Files
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/app.ts` | Register `analytics` router; start nightly aggregation cron job |
|
||||
| `src/infrastructure/migrations/` | Add `009_add_analytics_aggregates.sql` |
|
||||
| `dashboard/src/App.tsx` | Add Analytics route and nav link |
|
||||
| `package.json` (API) | Add `node-cron` dependency |
|
||||
| `package.json` (dashboard) | Add `recharts`, `date-fns` dependencies |
|
||||
| `docs/openapi.yaml` | Add analytics endpoints |
|
||||
|
||||
### Redis Caching
|
||||
|
||||
Analytics responses are cached in Redis with `analytics:{tenantId}:{endpoint}:{queryHash}` keys. TTL: 5 minutes for agent-activity and token-trends; 60 seconds for usage-summary. Cache is invalidated on the next request after TTL expiry (no explicit invalidation).
|
||||
|
||||
### `AnalyticsService` Interface
|
||||
|
||||
```typescript
|
||||
interface IAnalyticsService {
|
||||
/**
|
||||
* Return a high-level usage summary for a tenant over a date range.
|
||||
*/
|
||||
getUsageSummary(tenantId: string, from: Date, to: Date): Promise<UsageSummary>;
|
||||
|
||||
/**
|
||||
* Return per-agent daily activity data for heatmap rendering.
|
||||
*/
|
||||
getAgentActivity(
|
||||
tenantId: string,
|
||||
from: Date,
|
||||
to: Date,
|
||||
agentId?: string
|
||||
): Promise<AgentActivityResponse>;
|
||||
|
||||
/**
|
||||
* Return daily token issuance trends with success/failure breakdown.
|
||||
*/
|
||||
getTokenTrends(
|
||||
tenantId: string,
|
||||
from: Date,
|
||||
to: Date,
|
||||
granularity: 'day' | 'week'
|
||||
): Promise<TokenTrendsResponse>;
|
||||
}
|
||||
```
|
||||
|
||||
### Prometheus Metrics
|
||||
|
||||
| Metric | Type | Labels | Description |
|
||||
|---|---|---|---|
|
||||
| `agentidp_analytics_query_duration_ms` | Histogram | `endpoint` | Analytics query latency (before cache) |
|
||||
| `agentidp_analytics_cache_hits_total` | Counter | `endpoint` | Analytics Redis cache hits |
|
||||
| `agentidp_analytics_cache_misses_total` | Counter | `endpoint` | Analytics Redis cache misses |
|
||||
| `agentidp_analytics_aggregation_job_duration_ms` | Gauge | — | Nightly aggregation job runtime |
|
||||
| `agentidp_analytics_aggregation_job_last_run` | Gauge | — | Unix timestamp of last successful aggregation job run |
|
||||
|
||||
### Feature Flags
|
||||
|
||||
| Variable | Default | Description |
|
||||
|---|---|---|
|
||||
| `ANALYTICS_ENABLED` | `true` | When `false`, all `/analytics/` routes return HTTP 404 |
|
||||
| `ANALYTICS_FREE_TIER` | `false` | When `true`, free tier tenants can access analytics (for beta/testing) |
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- `GET /analytics/usage-summary` returns correct aggregate counts for a date range
|
||||
- `GET /analytics/agent-activity` returns per-agent daily rows matching `analytics_daily_aggregates`
|
||||
- `GET /analytics/token-trends` returns daily and weekly granularity correctly
|
||||
- All three endpoints return HTTP 403 for free-tier tenants (when `ANALYTICS_FREE_TIER=false`)
|
||||
- Date range validation rejects `from > to` with HTTP 400
|
||||
- Nightly aggregation job runs idempotently — running twice for same date produces no duplicates
|
||||
- Analytics responses are cached in Redis — a second identical request does not hit the DB
|
||||
- Dashboard Analytics tab renders heatmap, trend chart, and rotation table with mock data in Storybook
|
||||
- Unit test coverage >= 80% on `AnalyticsService`
|
||||
- Integration tests cover: summary, activity, trends (daily), trends (weekly), free-tier rejection, invalid date range
|
||||
@@ -0,0 +1,276 @@
|
||||
## WS4: Public API Gateway & Rate Limiting SaaS
|
||||
|
||||
### Purpose
|
||||
|
||||
Replace the single flat rate limit (Phase 4) with a multi-tier enforcement model where each tenant's rate limits are determined by their subscription tier (`free` | `pro` | `enterprise`). Expose the tier definitions publicly via `GET /tiers` so developers can understand limits before registering. Add `POST /billing/upgrade` so tenants can self-service upgrade their tier without contacting support.
|
||||
|
||||
This workstream closes the gap between Phase 4's flat rate limiter and a proper commercial SaaS gateway model.
|
||||
|
||||
### New Endpoints
|
||||
|
||||
#### `GET /tiers`
|
||||
|
||||
**Summary:** Return the current tier definitions including rate limits, feature flags, and pricing.
|
||||
|
||||
**Authentication:** None (public endpoint).
|
||||
|
||||
**Response 200** (`application/json`):
|
||||
```json
|
||||
{
|
||||
"tiers": [
|
||||
{
|
||||
"id": "free",
|
||||
"name": "Free",
|
||||
"price": {
|
||||
"monthly": 0,
|
||||
"currency": "USD"
|
||||
},
|
||||
"limits": {
|
||||
"registeredAgents": 10,
|
||||
"apiCallsPerDay": 1000,
|
||||
"tokenIssuancesPerDay": 200,
|
||||
"rateLimitPerMinute": 60,
|
||||
"rateLimitBurst": 10,
|
||||
"auditLogRetentionDays": 30
|
||||
},
|
||||
"features": {
|
||||
"marketplace": true,
|
||||
"githubActions": true,
|
||||
"analytics": false,
|
||||
"webhooks": false,
|
||||
"sso": false,
|
||||
"sla": false,
|
||||
"customDomain": false,
|
||||
"prioritySupport": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "pro",
|
||||
"name": "Pro",
|
||||
"price": {
|
||||
"monthly": 49,
|
||||
"currency": "USD"
|
||||
},
|
||||
"limits": {
|
||||
"registeredAgents": 100,
|
||||
"apiCallsPerDay": 50000,
|
||||
"tokenIssuancesPerDay": 10000,
|
||||
"rateLimitPerMinute": 600,
|
||||
"rateLimitBurst": 100,
|
||||
"auditLogRetentionDays": 90
|
||||
},
|
||||
"features": {
|
||||
"marketplace": true,
|
||||
"githubActions": true,
|
||||
"analytics": true,
|
||||
"webhooks": true,
|
||||
"sso": false,
|
||||
"sla": false,
|
||||
"customDomain": false,
|
||||
"prioritySupport": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "enterprise",
|
||||
"name": "Enterprise",
|
||||
"price": {
|
||||
"monthly": null,
|
||||
"currency": "USD",
|
||||
"note": "Contact sales"
|
||||
},
|
||||
"limits": {
|
||||
"registeredAgents": null,
|
||||
"apiCallsPerDay": null,
|
||||
"tokenIssuancesPerDay": null,
|
||||
"rateLimitPerMinute": 6000,
|
||||
"rateLimitBurst": 1000,
|
||||
"auditLogRetentionDays": 365
|
||||
},
|
||||
"features": {
|
||||
"marketplace": true,
|
||||
"githubActions": true,
|
||||
"analytics": true,
|
||||
"webhooks": true,
|
||||
"sso": true,
|
||||
"sla": true,
|
||||
"customDomain": true,
|
||||
"prioritySupport": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
|
||||
| Status | Code | Description |
|
||||
|---|---|---|
|
||||
| 429 | `RATE_LIMITED` | Rate limit exceeded (even unauthenticated endpoints have a global IP-based limit) |
|
||||
|
||||
**Notes:**
|
||||
- `null` limits mean unlimited
|
||||
- Tier definitions are sourced from a static configuration object in the codebase, not a database table
|
||||
- The response is cached at the HTTP layer with `Cache-Control: public, max-age=3600`
|
||||
|
||||
---
|
||||
|
||||
#### `POST /billing/upgrade`
|
||||
|
||||
**Summary:** Initiate a self-service tier upgrade for the authenticated tenant. Creates a Stripe Checkout session for the target tier.
|
||||
|
||||
**Authentication:** Bearer token (tenant-scoped).
|
||||
|
||||
**Request Body** (`application/json`):
|
||||
```json
|
||||
{
|
||||
"targetTier": "pro"
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | Required | Constraints |
|
||||
|---|---|---|---|
|
||||
| `targetTier` | string | yes | Enum: `pro`, `enterprise` — cannot downgrade via this endpoint |
|
||||
|
||||
**Response 200** (`application/json`):
|
||||
```json
|
||||
{
|
||||
"checkoutUrl": "https://checkout.stripe.com/pay/cs_...",
|
||||
"sessionId": "cs_...",
|
||||
"targetTier": "pro",
|
||||
"expiresAt": "string (ISO 8601)"
|
||||
}
|
||||
```
|
||||
|
||||
**Error Responses:**
|
||||
|
||||
| Status | Code | Description |
|
||||
|---|---|---|
|
||||
| 400 | `ALREADY_ON_TIER` | Tenant is already subscribed to `targetTier` |
|
||||
| 400 | `INVALID_TARGET_TIER` | `targetTier` is not a valid upgradeable tier |
|
||||
| 400 | `DOWNGRADE_NOT_SUPPORTED` | `targetTier` is lower than the tenant's current tier |
|
||||
| 401 | `UNAUTHORIZED` | Missing or invalid Bearer token |
|
||||
| 422 | `STRIPE_ERROR` | Stripe API returned an error creating the Checkout session |
|
||||
| 429 | `RATE_LIMITED` | Rate limit exceeded |
|
||||
|
||||
**Business Rules:**
|
||||
- This endpoint extends the existing `BillingService` — a new `upgradeTier(tenantId, targetTier)` method creates a Stripe Checkout session with the correct Stripe Price ID for the target tier
|
||||
- The Stripe Price IDs per tier are configured via environment variables: `STRIPE_PRICE_ID_PRO`, `STRIPE_PRICE_ID_ENTERPRISE`
|
||||
- After payment, Stripe sends `customer.subscription.created` webhook → existing webhook handler updates `tenant_subscriptions`
|
||||
- The `TierRateLimiter` reads the updated tier from `tenant_subscriptions` within 60 seconds (Redis cache TTL for tier lookup)
|
||||
- Downgrade is handled via the existing Stripe customer portal — not exposed as an API endpoint
|
||||
|
||||
---
|
||||
|
||||
### `TierRateLimiter` Middleware
|
||||
|
||||
This replaces the single `RateLimiterRedis` middleware for all authenticated routes. It reads the tenant's current tier, looks up the tier rate limit configuration, and enforces it using per-tenant Redis keys via `rate-limiter-flexible`.
|
||||
|
||||
**Middleware behavior:**
|
||||
1. Extract `tenantId` from the authenticated request context
|
||||
2. Look up tier from Redis cache key `tier:{tenantId}` (TTL: 60 seconds)
|
||||
3. On cache miss: query `tenant_subscriptions` for `tenantId`, cache result for 60s
|
||||
4. Look up rate limit configuration for the tier from the static tier config
|
||||
5. Apply `rate-limiter-flexible` with key `rl:{tier}:{tenantId}` and tier-specific limits
|
||||
6. On rate limit exceeded: return HTTP 429 with headers:
|
||||
- `X-RateLimit-Limit: <limit>`
|
||||
- `X-RateLimit-Remaining: <remaining>`
|
||||
- `X-RateLimit-Reset: <unix timestamp>`
|
||||
- `Retry-After: <seconds>`
|
||||
7. Increment `agentidp_rate_limit_hits_total` counter (labels: `tier`, `tenant_id`, `endpoint`)
|
||||
|
||||
**Unauthenticated routes:** Continue to use the existing flat `RateLimiterRedis` with IP-based keys (unchanged from Phase 4).
|
||||
|
||||
### Tier Configuration Object
|
||||
|
||||
Centralized in `src/config/tiers.ts` — this is the single source of truth for all tier limits and features. Both `GET /tiers` and `TierRateLimiter` read from this same object.
|
||||
|
||||
```typescript
|
||||
export const TIER_CONFIG: Record<TierName, TierDefinition> = {
|
||||
free: {
|
||||
id: 'free',
|
||||
limits: {
|
||||
registeredAgents: 10,
|
||||
apiCallsPerDay: 1000,
|
||||
tokenIssuancesPerDay: 200,
|
||||
rateLimitPerMinute: 60,
|
||||
rateLimitBurst: 10,
|
||||
auditLogRetentionDays: 30,
|
||||
},
|
||||
features: { analytics: false, webhooks: false, sso: false, sla: false },
|
||||
stripeProductId: null,
|
||||
},
|
||||
pro: {
|
||||
id: 'pro',
|
||||
limits: {
|
||||
registeredAgents: 100,
|
||||
apiCallsPerDay: 50000,
|
||||
tokenIssuancesPerDay: 10000,
|
||||
rateLimitPerMinute: 600,
|
||||
rateLimitBurst: 100,
|
||||
auditLogRetentionDays: 90,
|
||||
},
|
||||
features: { analytics: true, webhooks: true, sso: false, sla: false },
|
||||
stripeProductId: process.env.STRIPE_PRICE_ID_PRO ?? '',
|
||||
},
|
||||
enterprise: {
|
||||
id: 'enterprise',
|
||||
limits: {
|
||||
registeredAgents: null,
|
||||
apiCallsPerDay: null,
|
||||
tokenIssuancesPerDay: null,
|
||||
rateLimitPerMinute: 6000,
|
||||
rateLimitBurst: 1000,
|
||||
auditLogRetentionDays: 365,
|
||||
},
|
||||
features: { analytics: true, webhooks: true, sso: true, sla: true },
|
||||
stripeProductId: process.env.STRIPE_PRICE_ID_ENTERPRISE ?? '',
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### New Source Files
|
||||
|
||||
| File | Description |
|
||||
|---|---|
|
||||
| `src/config/tiers.ts` | Static tier configuration — single source of truth for limits and features |
|
||||
| `src/middleware/tierRateLimiter.ts` | `TierRateLimiter` middleware — reads tenant tier, enforces tier-specific limits |
|
||||
| `src/routes/tiers.ts` | Express router for `GET /tiers` |
|
||||
| `src/types/tiers.ts` | TypeScript interfaces: `TierDefinition`, `TierName`, `TierLimits`, `TierFeatures` |
|
||||
|
||||
### Modified Source Files
|
||||
|
||||
| File | Change |
|
||||
|---|---|
|
||||
| `src/middleware/rateLimiter.ts` | Retain for unauthenticated routes; authenticated routes switch to `tierRateLimiter` |
|
||||
| `src/services/BillingService.ts` | Add `upgradeTier(tenantId, targetTier)` method |
|
||||
| `src/controllers/BillingController.ts` | Add handler for `POST /billing/upgrade` |
|
||||
| `src/routes/billing.ts` | Register `POST /billing/upgrade` route |
|
||||
| `src/routes/index.ts` | Register `tiers` router |
|
||||
| `.env.example` | Add `STRIPE_PRICE_ID_PRO`, `STRIPE_PRICE_ID_ENTERPRISE`, `TIER_RATE_LIMITING_ENABLED` |
|
||||
| `docs/openapi.yaml` | Add `GET /tiers` and `POST /billing/upgrade` endpoints |
|
||||
|
||||
### Prometheus Metrics
|
||||
|
||||
| Metric | Type | Labels | Description |
|
||||
|---|---|---|---|
|
||||
| `agentidp_rate_limit_hits_total` | Counter | `tier`, `tenant_id`, `endpoint` | Rate limit rejections per tier (replaces old flat counter) |
|
||||
| `agentidp_tier_cache_hits_total` | Counter | — | Tier Redis cache hits |
|
||||
| `agentidp_tier_cache_misses_total` | Counter | — | Tier Redis cache misses |
|
||||
| `agentidp_billing_upgrades_total` | Counter | `from_tier`, `to_tier` | Self-service upgrade checkout sessions created |
|
||||
|
||||
### Feature Flag
|
||||
|
||||
`TIER_RATE_LIMITING_ENABLED` (default: `true`). When `false`, the system uses the old flat `RateLimiterRedis` middleware — this is the rollback mechanism.
|
||||
|
||||
### Acceptance Criteria
|
||||
|
||||
- `GET /tiers` returns all three tier definitions matching `TIER_CONFIG` exactly — no database query, cached `Cache-Control: max-age=3600`
|
||||
- `POST /billing/upgrade` creates a Stripe Checkout session and returns `checkoutUrl`
|
||||
- `POST /billing/upgrade` returns HTTP 400 `ALREADY_ON_TIER` when tenant is already on the target tier
|
||||
- `POST /billing/upgrade` returns HTTP 400 `DOWNGRADE_NOT_SUPPORTED` when target tier is lower than current
|
||||
- `TierRateLimiter` enforces free tier limits (60 req/min) for free tenants
|
||||
- `TierRateLimiter` enforces pro tier limits (600 req/min) for pro tenants
|
||||
- Tier lookup is cached in Redis — second request does not query `tenant_subscriptions`
|
||||
- Rate limit response includes `X-RateLimit-*` headers and `Retry-After`
|
||||
- After a Stripe webhook updates `tenant_subscriptions` to `pro`, `TierRateLimiter` applies pro limits within 60 seconds (next cache refresh)
|
||||
- Unit tests cover: tier lookup (cached), tier lookup (miss), free limit enforcement, pro limit enforcement, upgrade (success), upgrade (already on tier), upgrade (downgrade rejected)
|
||||
@@ -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
|
||||
289
openspec/changes/phase-5-scale-ecosystem/specs/rust-sdk/spec.md
Normal file
289
openspec/changes/phase-5-scale-ecosystem/specs/rust-sdk/spec.md
Normal 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`
|
||||
175
openspec/changes/phase-5-scale-ecosystem/tasks.md
Normal file
175
openspec/changes/phase-5-scale-ecosystem/tasks.md
Normal file
@@ -0,0 +1,175 @@
|
||||
## 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 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
|
||||
- [ ] 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. WS3: Analytics — Database, Aggregation Job
|
||||
|
||||
- [ ] 8.1 Create `src/infrastructure/migrations/009_add_analytics_aggregates.sql` — create `analytics_daily_aggregates` table with columns: `id` (UUID PK), `tenant_id` (UUID FK), `agent_id` (UUID nullable FK), `date` (DATE), `metric_type` (VARCHAR 64), `count` (BIGINT), `created_at`, `updated_at`; add unique constraint on `(tenant_id, agent_id, date, metric_type)`; create indexes on `(tenant_id, date)` and `(agent_id, date) WHERE agent_id IS NOT NULL`
|
||||
- [ ] 8.2 Install `node-cron` npm package — add to `package.json`
|
||||
- [ ] 8.3 Create `src/jobs/analyticsAggregation.ts` — implement `runAnalyticsAggregation(targetDate: Date): Promise<void>`: execute upsert query aggregating previous day's `usage_events` rows into `analytics_daily_aggregates`; query is idempotent (upsert on unique constraint); update `agentidp_analytics_aggregation_job_duration_ms` gauge and `agentidp_analytics_aggregation_job_last_run` gauge on completion
|
||||
- [ ] 8.4 Register cron job in `src/app.ts` — schedule `runAnalyticsAggregation` at `00:05 UTC` daily using `node-cron`; log job start, completion, and any errors; do not crash the process on job failure — log error and continue
|
||||
|
||||
## 9. WS3: Analytics — Service, Controller, Routes
|
||||
|
||||
- [ ] 9.1 Create `src/types/analytics.ts` — define interfaces: `UsageSummary`, `AgentActivityResponse`, `TokenTrendsResponse`, `DailyAggregate`, `AnalyticsDateRange`
|
||||
- [ ] 9.2 Create `src/services/AnalyticsService.ts` — implement `IAnalyticsService`; `getUsageSummary`: validate date range (from <= to, max 365 days), check Redis cache (`analytics:{tenantId}:summary:{hash}`, TTL 60s), on miss query `analytics_daily_aggregates`, compute totals, write to cache, return `UsageSummary`
|
||||
- [ ] 9.3 Implement `AnalyticsService.getAgentActivity(tenantId, from, to, agentId?)` — validate date range (max 90 days), check Redis cache (TTL 5 min), on miss query `analytics_daily_aggregates` grouped by `agent_id` and `date`, join agent names from `agents` table, write to cache, return `AgentActivityResponse`
|
||||
- [ ] 9.4 Implement `AnalyticsService.getTokenTrends(tenantId, from, to, granularity)` — support `day` and `week` granularity (weekly: `date_trunc('week', date)`), check Redis cache (TTL 5 min), return `TokenTrendsResponse` with `successfulIssuances`, `failedIssuances`, `uniqueAgents` per period
|
||||
- [ ] 9.5 Create `src/controllers/AnalyticsController.ts` — handlers for `getUsageSummary`, `getAgentActivity`, `getTokenTrends`; parse and validate query parameters; return HTTP 403 for free-tier tenants (check `ANALYTICS_FREE_TIER` env and tenant subscription); emit `agentidp_analytics_query_duration_ms` histogram and cache hit/miss counters
|
||||
- [ ] 9.6 Create `src/routes/analytics.ts` — Express router for `/analytics/usage-summary`, `/analytics/agent-activity`, `/analytics/token-trends`; all routes require authentication middleware
|
||||
- [ ] 9.7 Register analytics router in `src/routes/index.ts` behind `ANALYTICS_ENABLED` feature flag
|
||||
- [ ] 9.8 Add analytics endpoints to `docs/openapi.yaml` — all query parameters, response schemas, and error codes as defined in spec
|
||||
- [ ] 9.9 Write unit tests for `AnalyticsService` — test: usage-summary (cache hit), usage-summary (cache miss → DB query), agent-activity (with agentId filter), agent-activity (no filter — all agents), token-trends (daily), token-trends (weekly), date range validation (from > to rejected), date range validation (> max days rejected), free-tier rejection
|
||||
- [ ] 9.10 Write integration tests for analytics endpoints — test all three endpoints with valid date ranges, verify free-tier rejection, verify invalid date range errors
|
||||
|
||||
## 10. WS3: Analytics — Dashboard UI
|
||||
|
||||
- [ ] 10.1 Install `recharts` and `date-fns` in `dashboard/package.json`
|
||||
- [ ] 10.2 Create `dashboard/src/api/analyticsApi.ts` — typed fetch functions for all three analytics endpoints: `fetchUsageSummary(token, from, to)`, `fetchAgentActivity(token, from, to, agentId?)`, `fetchTokenTrends(token, from, to, granularity)`; all functions return typed response objects; handle 403 response with a typed `AnalyticsNotAvailableError`
|
||||
- [ ] 10.3 Create `dashboard/src/components/charts/AgentHeatmap.tsx` — renders a grid heatmap (agents × dates) using `recharts` or a custom CSS grid; color intensity represents `apiCalls` count; hover tooltip shows agent name, date, apiCalls, tokenIssuances, credentialRotations; accepts `agents` prop from `AgentActivityResponse`
|
||||
- [ ] 10.4 Create `dashboard/src/components/charts/TokenTrendsChart.tsx` — renders a `recharts` `ComposedChart` with a `Line` for `successfulIssuances` and a `Bar` for `failedIssuances`; X-axis is dates; tooltip shows all three metrics per period; accepts `dataPoints` prop from `TokenTrendsResponse`
|
||||
- [ ] 10.5 Create `dashboard/src/components/charts/RotationFrequencyTable.tsx` — renders a sortable table of credential rotation counts per agent; columns: Agent Name, Rotations (period), Last Rotation Date; sortable by any column; accepts `agents` prop derived from `AgentActivityResponse` filtering `credentialRotations`
|
||||
- [ ] 10.6 Create `dashboard/src/pages/Analytics.tsx` — analytics tab page; renders date range picker (from/to), calls all three analytics APIs, renders `AgentHeatmap`, `TokenTrendsChart`, `RotationFrequencyTable`; shows a `UpgradeRequired` component when API returns 403
|
||||
- [ ] 10.7 Add Analytics route to `dashboard/src/App.tsx` — add `/analytics` route; add "Analytics" link to dashboard navigation
|
||||
- [ ] 10.8 Run `npm run build` in `dashboard/` — zero TypeScript errors, zero ESLint errors
|
||||
|
||||
## 11. WS4: API Gateway Tiers — Configuration & Middleware
|
||||
|
||||
- [ ] 11.1 Create `src/types/tiers.ts` — define interfaces: `TierName` (union: `'free' | 'pro' | 'enterprise'`), `TierLimits`, `TierFeatures`, `TierDefinition` (includes `id`, `limits`, `features`, `stripeProductId`)
|
||||
- [ ] 11.2 Create `src/config/tiers.ts` — define `TIER_CONFIG: Record<TierName, TierDefinition>` with complete limit and feature definitions for `free`, `pro`, and `enterprise` tiers as specified in spec; export `getTierConfig(tier: TierName): TierDefinition` helper
|
||||
- [ ] 11.3 Create `src/middleware/tierRateLimiter.ts` — implement `TierRateLimiter` middleware: extract `tenantId` from authenticated request context; check Redis key `tier:{tenantId}` (TTL 60s) for cached tier; on miss query `tenant_subscriptions` for tenant's current tier, cache for 60s; look up rate limit config from `TIER_CONFIG`; apply `RateLimiterRedis` with key `rl:{tier}:{tenantId}`; on rejection return HTTP 429 with `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`, `Retry-After` headers; increment `agentidp_rate_limit_hits_total` counter with `tier` and `tenant_id` labels
|
||||
- [ ] 11.4 Replace `RateLimiterRedis` middleware on all authenticated routes in `src/routes/index.ts` with `TierRateLimiter`; keep the flat IP-based `RateLimiterRedis` on unauthenticated routes unchanged; wrap replacement in `TIER_RATE_LIMITING_ENABLED` feature flag (fall back to old middleware when `false`)
|
||||
|
||||
## 12. WS4: API Gateway Tiers — Endpoints
|
||||
|
||||
- [ ] 12.1 Create `src/routes/tiers.ts` — Express router for `GET /tiers`; handler reads `TIER_CONFIG`, formats response as specified in spec, sets `Cache-Control: public, max-age=3600` header; no database query; no authentication required
|
||||
- [ ] 12.2 Register `tiers` router in `src/routes/index.ts`
|
||||
- [ ] 12.3 Implement `BillingService.upgradeTier(tenantId: string, targetTier: 'pro' | 'enterprise'): Promise<{ checkoutUrl: string; sessionId: string; expiresAt: string }>` — fetch current tier from `tenant_subscriptions`, validate no self-upgrade or downgrade, create Stripe Checkout session with `STRIPE_PRICE_ID_PRO` or `STRIPE_PRICE_ID_ENTERPRISE`, return checkout URL
|
||||
- [ ] 12.4 Add `upgradeTier` handler to `src/controllers/BillingController.ts` — validate `targetTier` enum, call `BillingService.upgradeTier`, return HTTP 200 with `checkoutUrl`, `sessionId`, `targetTier`, `expiresAt`
|
||||
- [ ] 12.5 Register `POST /billing/upgrade` route in `src/routes/billing.ts` with authentication middleware
|
||||
- [ ] 12.6 Add `STRIPE_PRICE_ID_PRO`, `STRIPE_PRICE_ID_ENTERPRISE`, `TIER_RATE_LIMITING_ENABLED` to `.env.example` with documentation comments
|
||||
- [ ] 12.7 Add `GET /tiers` and `POST /billing/upgrade` to `docs/openapi.yaml`
|
||||
- [ ] 12.8 Write unit tests for `TierRateLimiter` — test: free tier limit enforced (60 req/min), pro tier limit enforced (600 req/min), tier looked up from Redis cache (DB not called), tier fetched from DB on cache miss, rollback path (`TIER_RATE_LIMITING_ENABLED=false` uses old flat limiter)
|
||||
- [ ] 12.9 Write unit tests for `BillingService.upgradeTier` — test: upgrade free → pro (creates Stripe session), upgrade free → enterprise (creates Stripe session), already on pro (returns ALREADY_ON_TIER error), downgrade attempt (returns DOWNGRADE_NOT_SUPPORTED error)
|
||||
- [ ] 12.10 Write integration tests for `GET /tiers` — verify response structure, verify `Cache-Control` header, verify no auth required; write integration tests for `POST /billing/upgrade` — mock Stripe, verify checkout URL returned
|
||||
|
||||
## 13. WS5: Developer Experience — Scaffold Service
|
||||
|
||||
- [ ] 13.1 Install `archiver` and `@types/archiver` in API `package.json`
|
||||
- [ ] 13.2 Create `src/types/scaffold.ts` — define `ScaffoldLanguage` union (`'typescript' | 'python' | 'go' | 'java' | 'rust'`), `ScaffoldOptions` interface, `ScaffoldTemplate` interface
|
||||
- [ ] 13.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)
|
||||
- [ ] 13.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
|
||||
- [ ] 13.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`
|
||||
- [ ] 13.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`
|
||||
- [ ] 13.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`
|
||||
- [ ] 13.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
|
||||
|
||||
## 14. WS5: Developer Experience — Scaffold Controller & Route
|
||||
|
||||
- [ ] 14.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 }`)
|
||||
- [ ] 14.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 `TierRateLimiter`)
|
||||
- [ ] 14.3 Register `scaffold` router in `src/routes/index.ts`
|
||||
- [ ] 14.4 Add `GET /sdk/scaffold/:agentId` to `docs/openapi.yaml` — document binary response type, query parameters, all error responses
|
||||
- [ ] 14.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`
|
||||
- [ ] 14.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
|
||||
|
||||
## 15. WS5: Developer Experience — Portal & CLI
|
||||
|
||||
- [ ] 15.1 Install `@stoplight/elements` in `portal/package.json` — remove `swagger-ui-react`
|
||||
- [ ] 15.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
|
||||
- [ ] 15.3 Run `npm run build` in `portal/` — verify zero TypeScript errors and zero ESLint errors after Elements integration
|
||||
- [ ] 15.4 Install `unzipper` and `@types/unzipper` in `cli/package.json`
|
||||
- [ ] 15.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
|
||||
- [ ] 15.6 Register `scaffold` command in `cli/src/index.ts` — add `.addCommand(scaffoldCommand)` to Commander program
|
||||
- [ ] 15.7 Run `npm run build` in `cli/` — zero TypeScript errors; run `node dist/index.js scaffold --help` — outputs correct usage
|
||||
|
||||
## 16. WS6: AGNTCY Compliance — Compliance Service
|
||||
|
||||
- [ ] 16.1 Create `src/types/compliance.ts` — define interfaces: `ComplianceRequirement` (id, description, status, evidence, verifiedAt), `ComplianceSection` (id, name, status, requirements), `ComplianceReport` (reportId, generatedAt, agntcySpecVersion, tenantId, overallStatus, sections, summary), `AgentCard` (agntcyVersion, type, agent, issuedAt, expiresAt)
|
||||
- [ ] 16.2 Create `src/config/agntcyRequirements.ts` — define the complete array of AGNTCY requirement objects (minimum 24 requirements), each with: `id` (e.g., `AI-001`), `description` (from AGNTCY spec), `section` (e.g., `agent-identity`), and `evaluate(tenantId: string, db: Pool): Promise<RequirementEvaluation>` function — each evaluator queries the live system and returns `{ status, evidence }`
|
||||
- [ ] 16.3 Create `src/services/ComplianceService.ts` — implement `IComplianceService`; `generateComplianceReport(tenantId)`: run all requirement evaluators from `agntcyRequirements.ts` in parallel, group results by section, compute overall status (`compliant` if all pass, `partial` if any non-compliant, `non-compliant` if >20% fail), build `ComplianceReport`, write audit log entry (`compliance.report_generated`), emit `agentidp_compliance_reports_generated_total` counter and `agentidp_compliance_report_duration_ms` histogram
|
||||
- [ ] 16.4 Implement `ComplianceService.generateAgentCard(agentId)` — fetch agent from DB, build `AgentCard` per AGNTCY spec format, set `expiresAt = issuedAt + 24 hours`, set `complianceStandards` from system config, emit `agentidp_agent_cards_served_total` counter with `visibility` label
|
||||
|
||||
## 17. WS6: AGNTCY Compliance — Controller, Routes
|
||||
|
||||
- [ ] 17.1 Create `src/controllers/ComplianceController.ts` — implement `getComplianceReport` handler: check tenant tier is pro or enterprise (HTTP 403 `TIER_REQUIRED` for free tier), call `ComplianceService.generateComplianceReport`, return HTTP 200; implement `getAgentCard` handler: check agent visibility (HTTP 401 if private and unauthenticated, HTTP 403 if private and wrong tenant), call `ComplianceService.generateAgentCard`, return HTTP 200
|
||||
- [ ] 17.2 Create `src/routes/agntcy.ts` — Express router for `GET /agntcy/compliance-report` (requires auth) and `GET /agents/:id/agent-card` (auth optional); register behind `AGNTCY_ENABLED` feature flag
|
||||
- [ ] 17.3 Register `agntcy` router in `src/routes/index.ts`
|
||||
- [ ] 17.4 Add `GET /agntcy/compliance-report` and `GET /agents/:id/agent-card` to `docs/openapi.yaml`
|
||||
- [ ] 17.5 Write unit tests for `ComplianceService` — test: `generateComplianceReport` (all 24 requirements pass → `compliant`), `generateComplianceReport` (one evaluator fails → `partial`), `generateAgentCard` (public agent), `generateAgentCard` (private agent — verify agent data is included), `generateAgentCard` (non-existent agent → throws NotFoundError)
|
||||
- [ ] 17.6 Write integration tests for compliance endpoints — test: compliance report for pro tenant (HTTP 200, overallStatus), compliance report for free tenant (HTTP 403), agent card for public agent (no auth required), agent card for private agent (auth required, correct tenant succeeds, wrong tenant HTTP 403)
|
||||
|
||||
## 18. WS6: AGNTCY Compliance — Interoperability Tests & Docs
|
||||
|
||||
- [ ] 18.1 Create `tests/agntcy/interoperability.test.ts` — implement all 25+ AGNTCY interoperability test cases as defined in spec: AI-001 (agent UUID uniqueness), AI-002 (W3C DID document), AUTH-001 (OAuth 2.0 token issuance), AUTH-002 (OIDC discovery), AUTHZ-001 (scope enforcement), DEL-001 through DEL-004 (delegation chain), AUDIT-001 through AUDIT-002 (immutable audit log), GOV-001 through GOV-002 (lifecycle governance), INTER-001 (agent card), COMP-001 (compliance report)
|
||||
- [ ] 18.2 Add `"test:agntcy": "jest --testPathPattern=tests/agntcy --forceExit"` script to `package.json`
|
||||
- [ ] 18.3 Write `docs/agntcy/certification-guide.md` — complete document with all 6 sections: Overview, Requirement Mapping table, Running the Compliance Report (step-by-step), Agent Card Usage, Self-Certification Checklist, Submitting for Official AGNTCY Certification; no placeholders, no TODOs
|
||||
|
||||
## 19. QA & Release
|
||||
|
||||
- [ ] 19.1 Run `cargo build` and `cargo clippy -- -D warnings` in `sdk-rust/` — zero warnings; run `cargo test` — all unit tests pass
|
||||
- [ ] 19.2 Run `tsc --noEmit` across API, dashboard, portal, and CLI — zero TypeScript errors
|
||||
- [ ] 19.3 Run full Jest suite (`npm test`) — all unit tests pass, coverage >= 80% across all new services: `DelegationService`, `AnalyticsService`, `ScaffoldService`, `ComplianceService`, `TierRateLimiter`
|
||||
- [ ] 19.4 Run `npm run build` in `portal/` with Elements integration — zero errors; verify `/api-explorer` page renders Elements `<API>` component
|
||||
- [ ] 19.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
|
||||
- [ ] 19.6 Apply database migrations `008_add_delegation_chains.sql` and `009_add_analytics_aggregates.sql` against a test database — verify migrations run without errors and tables are created with correct schemas
|
||||
- [ ] 19.7 Run integration tests for all Phase 5 endpoints — delegation (create, verify, revoke), analytics (usage-summary, agent-activity, token-trends), tiers (GET /tiers, POST /billing/upgrade), scaffold (all 5 languages), AGNTCY (compliance-report, agent-card)
|
||||
- [ ] 19.8 Run `npm run test:agntcy` — all 25+ interoperability test cases pass
|
||||
- [ ] 19.9 Verify feature flags: `A2A_ENABLED=false` → delegation routes return 404; `ANALYTICS_ENABLED=false` → analytics routes return 404; `TIER_RATE_LIMITING_ENABLED=false` → flat rate limiter used; `AGNTCY_ENABLED=false` → AGNTCY routes return 404
|
||||
- [ ] 19.10 Verify tier rate limiting: free tenant receives 429 at 61st request/minute; pro tenant allows 600 requests/minute; tier cache refresh within 60s after Stripe webhook updates subscription
|
||||
- [ ] 19.11 Verify scaffold security: `GET /sdk/scaffold/:agentId` response ZIP never contains a real `client_secret` value — `.env.example` placeholder only
|
||||
- [ ] 19.12 Commit all Phase 5 work on `main` — one conventional commit per workstream (e.g., `feat(phase-5): WS1 — Rust SDK`, `feat(phase-5): WS2 — A2A Authorization`, etc.)
|
||||
Reference in New Issue
Block a user