feat(openspec): Phase 4 Developer Growth & Go-to-Market Readiness

OpenSpec change: phase-4-developer-growth (spec-driven, 4/4 artifacts)

6 workstreams, 90 implementation tasks, delivery sequence:
WS1 → WS2 + WS3 (parallel) → WS4 → WS5 → WS6

Workstreams:
1. Production Hardening — ioredis rate limiting, DB pool tuning, /health/detailed, k6 load tests
2. Developer Portal — Next.js 14, Swagger UI explorer, onboarding wizard, pricing/SDK pages
3. CLI Tool — sentryagent npm CLI, 5 commands, shell completion
4. Agent Marketplace — public searchable registry powered by existing agent/DID infrastructure
5. GitHub Actions — register-agent + issue-token Actions via OIDC (no stored secrets)
6. Billing & Usage Metering — Stripe Checkout, webhook-driven state, free tier enforcement

New capabilities (8 specs): production-hardening, developer-portal, cli-tool,
agent-marketplace, github-actions, billing-metering (+delta: web-dashboard, monitoring)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
SentryAgent.ai Developer
2026-04-02 04:00:34 +00:00
parent f1fbe0e29a
commit b0f70b7ac4
12 changed files with 630 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
## ADDED Requirements
### Requirement: Marketplace listing endpoint returns public agent registry
The system SHALL expose `GET /marketplace/agents` returning a paginated list of publicly visible agents. Each listing SHALL include: `agentId`, `name`, `description`, `capabilities` (array of strings), `publisherName`, `did`, `createdAt`. The endpoint SHALL be unauthenticated (public access). Agents are included in the marketplace when their `isPublic` flag is `true`.
#### Scenario: Unauthenticated user lists marketplace agents
- **WHEN** an unauthenticated client calls `GET /marketplace/agents`
- **THEN** the response is HTTP 200 with a paginated list of public agents
#### Scenario: Pagination works correctly
- **WHEN** a client calls `GET /marketplace/agents?page=2&limit=20`
- **THEN** the response returns the correct page of results with `totalCount`, `page`, and `totalPages` in the response envelope
### Requirement: Marketplace search filters agents by capability, publisher, or name
The system SHALL support `GET /marketplace/agents?q=<search>` performing a case-insensitive search across agent name, description, and capabilities. The system SHALL also support `GET /marketplace/agents?capability=<cap>` and `GET /marketplace/agents?publisher=<name>` for structured filtering.
#### Scenario: Full-text search returns relevant agents
- **WHEN** a client calls `GET /marketplace/agents?q=translation`
- **THEN** agents whose name, description, or capabilities contain "translation" are returned
#### Scenario: Capability filter returns matching agents
- **WHEN** a client calls `GET /marketplace/agents?capability=nlp`
- **THEN** only agents with "nlp" in their capabilities array are returned
### Requirement: Marketplace detail endpoint returns agent with DID document
The system SHALL expose `GET /marketplace/agents/:agentId` returning the full public agent profile including the W3C DID document and AGNTCY agent card. The endpoint SHALL be unauthenticated.
#### Scenario: Agent detail includes DID document
- **WHEN** a client calls `GET /marketplace/agents/:agentId` for a public agent
- **THEN** the response includes `agentId`, `name`, `description`, `capabilities`, `did`, `didDocument`, `agentCard`, and `publisherName`
#### Scenario: Private agent returns 404 on marketplace
- **WHEN** a client calls `GET /marketplace/agents/:agentId` for an agent with `isPublic: false`
- **THEN** the response is HTTP 404
### Requirement: Agents can be published to or withdrawn from the marketplace
The system SHALL allow authenticated tenant users to set `isPublic: true` on an agent via `PATCH /agents/:agentId` (`{ "isPublic": true }`), making it appear in the marketplace. Setting `isPublic: false` removes it from marketplace listings without deleting the agent.
#### Scenario: Agent published to marketplace
- **WHEN** an authenticated user calls `PATCH /agents/:agentId` with `{ "isPublic": true }`
- **THEN** the agent appears in `GET /marketplace/agents` results
#### Scenario: Agent withdrawn from marketplace
- **WHEN** an authenticated user calls `PATCH /agents/:agentId` with `{ "isPublic": false }`
- **THEN** the agent no longer appears in `GET /marketplace/agents` results

View File

@@ -0,0 +1,60 @@
## ADDED Requirements
### Requirement: Per-tenant usage is tracked for API calls, active agents, and token issuances
The system SHALL track the following usage metrics per tenant per day: `api_calls` (every authenticated API request), `token_issuances` (every successful `POST /oauth2/token`), `active_agents` (count of non-revoked agents at end of day). Usage SHALL be aggregated in memory and flushed to a `usage_events` PostgreSQL table every 60 seconds.
#### Scenario: API call increments usage counter
- **WHEN** an authenticated tenant makes any API request
- **THEN** the tenant's `api_calls` counter for the current day is incremented
#### Scenario: Usage is persisted to database on flush interval
- **WHEN** 60 seconds elapse since the last flush
- **THEN** all in-memory counters are written to the `usage_events` table and reset to zero
### Requirement: Free tier limits are enforced per tenant
The system SHALL enforce free tier limits: 10 active agents maximum, 1,000 API calls per day. When a limit is exceeded, the offending request SHALL be rejected with HTTP 429 and a response body indicating which limit was reached and how to upgrade. Limit summaries SHALL be cached in Redis with a 60-second TTL.
#### Scenario: Agent registration blocked at free tier limit
- **WHEN** a free-tier tenant with 10 active agents calls `POST /agents`
- **THEN** the response is HTTP 429 with `{ "error": "free_tier_limit", "limit": "agents", "max": 10, "upgradeUrl": "..." }`
#### Scenario: API call blocked after daily limit
- **WHEN** a free-tier tenant has made 1,000 API calls today and makes another request
- **THEN** the response is HTTP 429 with `{ "error": "free_tier_limit", "limit": "api_calls", "max": 1000, "upgradeUrl": "..." }`
#### Scenario: Paid tenant is not rate limited by usage tiers
- **WHEN** a paid-tier tenant exceeds free tier thresholds
- **THEN** the request is processed normally with no usage-based rejection
### Requirement: Stripe Checkout initiates paid tier subscription
The system SHALL expose `POST /billing/checkout` (authenticated) that creates a Stripe Checkout session for the paid tier plan and returns a `checkoutUrl`. The tenant is redirected to Stripe Checkout to complete payment. On success, Stripe sends a `customer.subscription.created` webhook event.
#### Scenario: Checkout session created
- **WHEN** an authenticated tenant calls `POST /billing/checkout`
- **THEN** the response is HTTP 200 with `{ "checkoutUrl": "https://checkout.stripe.com/..." }`
#### Scenario: Duplicate subscription prevented
- **WHEN** a tenant with an active paid subscription calls `POST /billing/checkout`
- **THEN** the response is HTTP 409 with `{ "error": "already_subscribed" }`
### Requirement: Stripe webhooks update tenant subscription state
The system SHALL expose `POST /billing/webhook` (Stripe webhook endpoint) that verifies the `stripe-signature` header using `stripe.webhooks.constructEvent()` and processes: `customer.subscription.created` (set tenant to paid), `invoice.payment_succeeded` (extend subscription period), `customer.subscription.deleted` (revert tenant to free tier). All events without valid signatures SHALL be rejected with HTTP 400.
#### Scenario: Webhook without valid signature is rejected
- **WHEN** `POST /billing/webhook` is called with an invalid or missing `stripe-signature` header
- **THEN** the response is HTTP 400 and no state is changed
#### Scenario: Subscription created webhook activates paid tier
- **WHEN** Stripe sends a valid `customer.subscription.created` event for a tenant
- **THEN** the tenant's `subscriptionStatus` is updated to `active` and free tier limits no longer apply
#### Scenario: Subscription deleted webhook reverts to free tier
- **WHEN** Stripe sends a valid `customer.subscription.deleted` event
- **THEN** the tenant's `subscriptionStatus` is updated to `cancelled` and free tier limits are re-enforced
### Requirement: Billing is feature-flag gated
All billing enforcement and Stripe integration SHALL be gated behind the `BILLING_ENABLED` environment variable. When `BILLING_ENABLED=false`, free tier limits are not enforced, all tenants have paid-tier access, and Stripe webhook endpoint returns HTTP 200 without processing. Usage metering continues regardless of this flag.
#### Scenario: Billing disabled — no limits enforced
- **WHEN** `BILLING_ENABLED=false` and a free-tier tenant has 11 active agents
- **THEN** agent registration succeeds without HTTP 429

View File

@@ -0,0 +1,65 @@
## ADDED Requirements
### Requirement: sentryagent CLI is an installable npm package
The system SHALL provide a `sentryagent` CLI at `cli/` with its own `package.json`, built with `commander` and `chalk`, and published to npm as `sentryagent`. The CLI SHALL be installable globally via `npm install -g sentryagent`. The CLI binary SHALL be named `sentryagent`.
#### Scenario: CLI installs and shows help
- **WHEN** a user runs `npm install -g sentryagent` and then `sentryagent --help`
- **THEN** the command displays available subcommands and global options without errors
#### Scenario: CLI version flag works
- **WHEN** a user runs `sentryagent --version`
- **THEN** the CLI outputs its version number matching `package.json`
### Requirement: CLI persists configuration in ~/.sentryagent/config.json
The CLI SHALL store API endpoint (`apiUrl`) and credentials (`clientId`, `clientSecret`) in `~/.sentryagent/config.json`. The `sentryagent configure` command SHALL prompt for these values interactively and write them to the config file. All other commands SHALL read from this config file automatically.
#### Scenario: Configure command saves credentials
- **WHEN** a user runs `sentryagent configure` and enters an API URL, client ID, and client secret
- **THEN** `~/.sentryagent/config.json` is created or updated with the entered values
#### Scenario: Command fails gracefully when not configured
- **WHEN** a user runs any command before running `sentryagent configure`
- **THEN** the CLI outputs a human-readable error: "Not configured. Run `sentryagent configure` first."
### Requirement: register-agent command registers a new agent
The CLI SHALL provide `sentryagent register-agent --name <name> [--description <desc>]` that calls `POST /agents` and outputs the created agent's ID and name.
#### Scenario: Agent registered successfully
- **WHEN** a user runs `sentryagent register-agent --name "my-agent"`
- **THEN** the CLI outputs the new agent ID and confirms registration
### Requirement: list-agents command lists all agents
The CLI SHALL provide `sentryagent list-agents` that calls `GET /agents` and outputs a formatted table of agent ID, name, status, and creation date.
#### Scenario: Agents listed in table format
- **WHEN** a user runs `sentryagent list-agents`
- **THEN** the CLI outputs a formatted table with all agents for the authenticated tenant
### Requirement: issue-token command issues an OAuth2 token
The CLI SHALL provide `sentryagent issue-token --agent-id <id>` that calls `POST /oauth2/token` and outputs the access token and expiry.
#### Scenario: Token issued successfully
- **WHEN** a user runs `sentryagent issue-token --agent-id <id>`
- **THEN** the CLI outputs the access token and its expiry timestamp
### Requirement: rotate-credentials command rotates agent credentials
The CLI SHALL provide `sentryagent rotate-credentials --agent-id <id>` that calls `POST /agents/:id/credentials/rotate` and outputs the new client secret.
#### Scenario: Credentials rotated with confirmation prompt
- **WHEN** a user runs `sentryagent rotate-credentials --agent-id <id>`
- **THEN** the CLI prompts for confirmation ("This will invalidate the current secret. Continue? [y/N]") before rotating
### Requirement: tail-audit-log command polls and streams audit events
The CLI SHALL provide `sentryagent tail-audit-log [--agent-id <id>]` that polls `GET /audit/logs` every 5 seconds and streams new events to stdout in a human-readable format. The command SHALL run until the user presses Ctrl+C.
#### Scenario: Audit log events stream to stdout
- **WHEN** a user runs `sentryagent tail-audit-log`
- **THEN** new audit events appear in the terminal as they are created, one per line
### Requirement: CLI supports bash and zsh shell completion
The CLI SHALL provide `sentryagent completion bash` and `sentryagent completion zsh` commands that output shell completion scripts. Installation instructions SHALL be included in the CLI README.
#### Scenario: Bash completion script is generated
- **WHEN** a user runs `sentryagent completion bash`
- **THEN** a valid bash completion script is output to stdout

View File

@@ -0,0 +1,48 @@
## ADDED Requirements
### Requirement: Public developer portal is a standalone Next.js 14 application
The system SHALL provide a public developer portal at `portal/` — a standalone Next.js 14 application with Tailwind CSS. The portal SHALL be deployable independently of the API (to Vercel, Cloudflare Pages, or any static host). The portal SHALL communicate with the API exclusively via the public REST API at `NEXT_PUBLIC_API_URL`. No server-side API secrets SHALL be stored in the portal.
#### Scenario: Portal builds and exports successfully
- **WHEN** `npm run build` is executed in `portal/`
- **THEN** the build completes without errors and produces a deployable `out/` or `.next/` directory
#### Scenario: API URL is configurable via environment variable
- **WHEN** `NEXT_PUBLIC_API_URL=https://api.sentryagent.ai` is set and the portal is built
- **THEN** all API calls in the portal use that base URL
### Requirement: Interactive API explorer is embedded in the portal
The portal SHALL embed a Swagger UI (`swagger-ui-react`) loaded from the existing OpenAPI spec at `/openapi.json` (served by the Express API). The explorer SHALL allow unauthenticated browsing of all endpoints and authenticated execution using a Bearer token entered by the user.
#### Scenario: API explorer loads the OpenAPI spec
- **WHEN** a user visits `/api-explorer`
- **THEN** Swagger UI loads and renders all endpoints from the OpenAPI spec without errors
#### Scenario: User executes authenticated request in explorer
- **WHEN** a user enters a Bearer token in the Swagger UI Authorize dialog and executes `GET /agents`
- **THEN** the request is sent with `Authorization: Bearer <token>` and the response is displayed
### Requirement: Agent registration onboarding wizard guides new users
The portal SHALL provide a guided wizard at `/get-started` covering: (1) sign up / log in, (2) create first agent, (3) generate credentials, (4) copy code snippet for their preferred SDK (Node.js, Python, Go, Java). The wizard SHALL complete in ≤ 4 steps.
#### Scenario: Wizard completes agent registration
- **WHEN** a new user completes all wizard steps
- **THEN** an agent is registered via the API, credentials are generated, and a ready-to-run code snippet is displayed
#### Scenario: SDK code snippet matches selected language
- **WHEN** a user selects "Python" as their preferred SDK in step 4
- **THEN** the displayed code snippet uses the Python SDK (`sentryagent-idp`) syntax
### Requirement: Free tier documentation page explains limits and upgrade path
The portal SHALL include a `/pricing` page documenting free tier limits (10 agents, 1,000 API calls/day), the paid tier capabilities (unlimited agents, higher rate limits, SLA), and a clear call-to-action to upgrade via Stripe Checkout. The page SHALL not require authentication to view.
#### Scenario: Pricing page is publicly accessible
- **WHEN** an unauthenticated user visits `/pricing`
- **THEN** the page renders with free tier limits and upgrade CTA without requiring login
### Requirement: Portal links to all four SDKs
The portal SHALL include an `/sdks` page listing all four officially supported SDKs (Node.js, Python, Go, Java) with: package name, installation command, a minimal working example, and a link to the SDK repository.
#### Scenario: SDK page displays all four SDKs
- **WHEN** a user visits `/sdks`
- **THEN** all four SDKs are listed with installation commands and code examples

View File

@@ -0,0 +1,41 @@
## ADDED Requirements
### Requirement: register-agent Action registers an agent in CI using OIDC
The system SHALL provide a GitHub Action at `.github/actions/register-agent/action.yml` (`sentryagent/register-agent@v1`) that registers a new agent via the SentryAgent.ai API using GitHub OIDC token exchange. The Action SHALL accept inputs: `api-url` (required), `agent-name` (required), `agent-description` (optional). The Action SHALL output: `agent-id`. No long-lived API credentials SHALL be required.
#### Scenario: Agent registered in CI workflow
- **WHEN** a GitHub Actions workflow includes `uses: sentryagent/register-agent@v1` with valid `api-url` and `agent-name` inputs
- **THEN** the step completes successfully, an agent is registered in SentryAgent.ai, and `steps.<id>.outputs.agent-id` is populated
#### Scenario: OIDC exchange fails — action fails with clear message
- **WHEN** the GitHub OIDC token cannot be exchanged (e.g., trust policy not configured)
- **THEN** the action fails with an error message explaining how to configure the OIDC trust policy
### Requirement: issue-token Action issues an OAuth2 token in CI using OIDC
The system SHALL provide a GitHub Action at `.github/actions/issue-token/action.yml` (`sentryagent/issue-token@v1`) that issues an OAuth2 access token for an agent via OIDC exchange. The Action SHALL accept inputs: `api-url` (required), `agent-id` (required). The Action SHALL output: `access-token`, `expires-at`. The access token SHALL be masked in GitHub Actions logs.
#### Scenario: Token issued in CI workflow
- **WHEN** a GitHub Actions workflow includes `uses: sentryagent/issue-token@v1` with `api-url` and `agent-id`
- **THEN** the step completes and `steps.<id>.outputs.access-token` contains a valid Bearer token
#### Scenario: Access token is masked in logs
- **WHEN** the action issues a token
- **THEN** the token value is registered with `core.setSecret()` and does not appear in plaintext in the workflow log
### Requirement: GitHub OIDC trust policy is configurable via API
The system SHALL allow tenants to register a GitHub OIDC trust policy via `POST /oidc/trust-policies` specifying: `provider: "github"`, `repository` (e.g., `org/repo`), `branch` (optional), and `agentId`. Only workflows matching the trust policy SHALL be permitted to exchange GitHub OIDC tokens for SentryAgent.ai agent tokens.
#### Scenario: Trust policy restricts token exchange to specified repo
- **WHEN** a trust policy is registered for `org/repo-a` and a GitHub OIDC token from `org/repo-b` is presented
- **THEN** the token exchange is rejected with HTTP 403
#### Scenario: Trust policy permits token exchange for matching repo
- **WHEN** a trust policy is registered for `org/repo-a` and a valid GitHub OIDC token from `org/repo-a` is presented
- **THEN** the token exchange succeeds and an agent access token is returned
### Requirement: Both Actions include README with setup instructions
Each Action directory SHALL include a `README.md` with: purpose, prerequisites (OIDC trust policy setup), inputs table, outputs table, a minimal workflow example, and a link to full documentation on the developer portal.
#### Scenario: README is present and complete
- **WHEN** a developer reads `register-agent/README.md`
- **THEN** they can configure the OIDC trust policy and add the action to their workflow without external documentation

View File

@@ -0,0 +1,29 @@
## ADDED Requirements
### Requirement: Rate limiter hit counter is exposed as Prometheus metric
The system SHALL expose a `agentidp_rate_limit_hits_total` counter (labels: `endpoint`, `tenant_id`) incremented each time a request is rejected by the Redis-backed rate limiter (HTTP 429). This metric SHALL be available at `GET /metrics` alongside existing metrics.
#### Scenario: Rate limit rejection increments counter
- **WHEN** a client is rejected by the rate limiter on `POST /oauth2/token`
- **THEN** `agentidp_rate_limit_hits_total{endpoint="/oauth2/token"}` is incremented by 1
### Requirement: Database connection pool saturation is exposed as Prometheus metric
The system SHALL expose `agentidp_db_pool_active_connections` (gauge) and `agentidp_db_pool_waiting_requests` (gauge) reflecting the current number of active database connections and queued requests waiting for a free connection.
#### Scenario: Pool metrics reflect current state
- **WHEN** 15 of 20 pool connections are in use and 2 requests are queued
- **THEN** `agentidp_db_pool_active_connections` reads 15 and `agentidp_db_pool_waiting_requests` reads 2
### Requirement: Per-tenant API call rate is exposed as Prometheus metric
The system SHALL expose `agentidp_tenant_api_calls_total` counter (label: `tenant_id`) incremented on every authenticated API request. This enables per-tenant traffic analysis in Grafana.
#### Scenario: Per-tenant counter increments on authenticated request
- **WHEN** tenant `org-abc` makes an authenticated API call
- **THEN** `agentidp_tenant_api_calls_total{tenant_id="org-abc"}` is incremented by 1
### Requirement: Usage tier enforcement rejections are tracked as Prometheus metric
The system SHALL expose `agentidp_billing_limit_rejections_total` counter (labels: `tenant_id`, `limit_type`) incremented each time a request is rejected due to a free tier limit (`agents` or `api_calls`).
#### Scenario: Agent limit rejection increments counter
- **WHEN** a free-tier tenant is rejected from creating an agent due to the 10-agent limit
- **THEN** `agentidp_billing_limit_rejections_total{limit_type="agents"}` is incremented by 1

View File

@@ -0,0 +1,53 @@
## ADDED Requirements
### Requirement: Redis-backed distributed rate limiting replaces in-memory limiter
The system SHALL use `ioredis` + `rate-limiter-flexible` to enforce rate limits across all Express instances using a Redis sliding window algorithm. The in-memory `express-rate-limit` store SHALL be removed. Rate limit configuration SHALL be injectable via environment variables (`RATE_LIMIT_WINDOW_MS`, `RATE_LIMIT_MAX_REQUESTS`). When `REDIS_RATE_LIMIT_ENABLED=false`, the system SHALL fall back to an in-memory limiter for local development.
#### Scenario: Rate limit enforced across multiple instances
- **WHEN** two Express instances are running behind a load balancer and a client sends requests alternating between instances
- **THEN** the rate limit counter is shared across both instances via Redis and the client is rejected after the combined limit is reached
#### Scenario: Redis unavailable — graceful fallback
- **WHEN** Redis is unreachable and `REDIS_RATE_LIMIT_ENABLED=true`
- **THEN** the system SHALL log a warning and fall back to in-memory limiting rather than rejecting all requests
#### Scenario: Rate limit exceeded
- **WHEN** a client exceeds the configured request limit within the window
- **THEN** the system SHALL respond with HTTP 429 and a `Retry-After` header indicating when the window resets
### Requirement: Database connection pool is explicitly configured
The system SHALL configure `pg` connection pool with explicit `max`, `min`, `idleTimeoutMillis`, and `connectionTimeoutMillis` parameters via environment variables (`DB_POOL_MAX`, `DB_POOL_MIN`, `DB_POOL_IDLE_TIMEOUT_MS`, `DB_POOL_CONNECTION_TIMEOUT_MS`). Defaults SHALL be: max=20, min=2, idleTimeout=30000ms, connectionTimeout=5000ms.
#### Scenario: Pool exhaustion under load
- **WHEN** all pool connections are in use and a new query is requested
- **THEN** the system SHALL queue the request and resolve it within `DB_POOL_CONNECTION_TIMEOUT_MS`, or reject with a 503 if timeout is exceeded
#### Scenario: Idle connections are reaped
- **WHEN** a connection has been idle for longer than `DB_POOL_IDLE_TIMEOUT_MS`
- **THEN** the pool SHALL close the connection and reduce active pool size toward `min`
### Requirement: Detailed health endpoint reports per-service status
The system SHALL expose `GET /health/detailed` returning a JSON object with individual status for each dependency: `database`, `redis`, `vault` (if configured), `opa` (if configured). Each service SHALL report `status` (`healthy` | `degraded` | `unreachable`), `latencyMs`, and an optional `message`. The overall response status SHALL be HTTP 200 if all services are healthy, HTTP 207 if any are degraded, and HTTP 503 if any are unreachable.
#### Scenario: All services healthy
- **WHEN** all dependencies respond within acceptable latency
- **THEN** `GET /health/detailed` returns HTTP 200 with all services reporting `status: "healthy"`
#### Scenario: Redis unreachable
- **WHEN** Redis does not respond within 2000ms
- **THEN** `GET /health/detailed` returns HTTP 503 with `redis.status: "unreachable"` and overall `status: "unhealthy"`
#### Scenario: Vault degraded
- **WHEN** Vault responds but with latency exceeding 1000ms
- **THEN** `GET /health/detailed` returns HTTP 207 with `vault.status: "degraded"` and a latency measurement
### Requirement: k6 load test suite validates production readiness
The system SHALL include a k6 load test suite at `tests/load/` covering: agent registration under load (100 virtual users, 60s), token issuance under load (1000 virtual users, 60s), and credential rotation under load (50 virtual users, 60s). Each scenario SHALL define pass/fail thresholds: p95 response time < 500ms, error rate < 1%.
#### Scenario: Token issuance load test passes thresholds
- **WHEN** the k6 load test `token-issuance.js` runs with 1000 virtual users for 60 seconds
- **THEN** p95 response time SHALL be below 500ms and error rate SHALL be below 1%
#### Scenario: Load test threshold failure surfaces clearly
- **WHEN** a k6 threshold is breached during the load test run
- **THEN** the k6 process SHALL exit with a non-zero exit code, making CI failure explicit

View File

@@ -0,0 +1,23 @@
## ADDED Requirements
### Requirement: Usage dashboard tab displays per-tenant metering data
The web dashboard SHALL include a "Usage" tab in the main navigation displaying the current billing period's usage: API calls used / daily limit, active agents count / agent limit, token issuances this period. Usage data SHALL be fetched from `GET /billing/usage` (new authenticated endpoint). The tab SHALL update on page load and on a 60-second polling interval.
#### Scenario: Usage tab shows current metrics
- **WHEN** an authenticated user navigates to the Usage tab
- **THEN** the dashboard displays current API call count, agent count, and token issuance count for the current billing period
#### Scenario: Free tier warning shown when approaching limit
- **WHEN** a free-tier tenant has used ≥ 80% of their daily API call limit
- **THEN** a warning banner is displayed with a link to the upgrade/pricing page
### Requirement: Billing status panel shows subscription tier and upgrade CTA
The web dashboard Usage tab SHALL include a billing status panel showing: current tier (Free / Paid), subscription status (active / cancelled / trial), and — for free-tier tenants — an "Upgrade" button linking to `POST /billing/checkout` flow.
#### Scenario: Free tier tenant sees upgrade CTA
- **WHEN** a free-tier tenant views the Usage tab
- **THEN** an "Upgrade to Paid" button is visible and initiates Stripe Checkout when clicked
#### Scenario: Paid tier tenant sees subscription status
- **WHEN** a paid-tier tenant views the Usage tab
- **THEN** the panel shows "Paid" tier with subscription status and next renewal date, with no upgrade CTA