diff --git a/docs/developers/README.md b/docs/developers/README.md new file mode 100644 index 0000000..6b17308 --- /dev/null +++ b/docs/developers/README.md @@ -0,0 +1,42 @@ +# SentryAgent.ai AgentIdP — Developer Documentation + +The complete documentation for bedroom developers building with SentryAgent.ai AgentIdP. + +## What is this? + +SentryAgent.ai AgentIdP is a free, open-source Identity Provider built specifically for AI agents. Your agent gets a unique ID, OAuth 2.0 credentials, and a full audit trail — for free. + +## Documents + +| Document | What it covers | +|----------|----------------| +| [Quick Start](quick-start.md) | Register your first agent and issue a token in under 5 minutes | +| [Core Concepts](concepts.md) | What AgentIdP is, how it works, and why you need it | +| [Guides](guides/README.md) | Step-by-step walkthroughs for each workflow | +| [API Reference](api-reference.md) | Every endpoint, field, error code, and example | + +## Guides + +| Guide | What it covers | +|-------|----------------| +| [Register an Agent](guides/register-an-agent.md) | All fields, validation rules, common errors | +| [Manage Credentials](guides/manage-credentials.md) | Generate, list, rotate, revoke credentials | +| [Issue and Revoke Tokens](guides/issue-and-revoke-tokens.md) | OAuth 2.0 client credentials flow, introspect, revoke | +| [Query Audit Logs](guides/query-audit-logs.md) | Filters, pagination, event structure, retention | + +## Base URL + +``` +http://localhost:3000/api/v1 # local development +``` + +All endpoints require a Bearer token in the `Authorization` header unless noted otherwise. + +## Free Tier Limits + +| Resource | Limit | +|----------|-------| +| Registered agents | 100 | +| Token requests/month | 10,000 | +| API rate limit | 100 req/min | +| Audit log retention | 90 days | diff --git a/docs/developers/api-reference.md b/docs/developers/api-reference.md new file mode 100644 index 0000000..50e2cbc --- /dev/null +++ b/docs/developers/api-reference.md @@ -0,0 +1,583 @@ +# API Reference + +Complete reference for all 14 endpoints across the four SentryAgent.ai AgentIdP services. + +## Base URL + +``` +http://localhost:3000/api/v1 +``` + +The port is configured via the `PORT` environment variable (default: `3000`). + +All endpoints are currently unversioned within the path prefix `/api/v1`. API versioning will be introduced in Phase 2. + +## Authentication + +All endpoints require a JWT Bearer token in the `Authorization` header: + +``` +Authorization: Bearer +``` + +Obtain a token via `POST /token` using your agent's `client_id` and `client_secret`. + +## Table of Contents + +- [Errors](#errors) +- [Agent Registry](#agent-registry) — 5 endpoints +- [OAuth 2.0 Tokens](#oauth-20-tokens) — 3 endpoints +- [Credential Management](#credential-management) — 4 endpoints +- [Audit Log](#audit-log) — 2 endpoints + +--- + +## Errors + +All error responses use this envelope: + +```json +{ + "code": "ERROR_CODE", + "message": "Human-readable description.", + "details": {} +} +``` + +The `details` field is optional and provides additional context (e.g. which field failed validation). + +### Error codes + +| Code | HTTP Status | Description | +|------|-------------|-------------| +| `VALIDATION_ERROR` | 400 | Request body or query parameter failed validation | +| `UNAUTHORIZED` | 401 | Missing, expired, or invalid Bearer token | +| `FORBIDDEN` | 403 | Valid token but insufficient scope | +| `AGENT_NOT_FOUND` | 404 | Agent with the given `agentId` does not exist | +| `CREDENTIAL_NOT_FOUND` | 404 | Credential with the given `credentialId` does not exist | +| `AUDIT_EVENT_NOT_FOUND` | 404 | Audit event with the given `eventId` does not exist (or outside retention window) | +| `AGENT_ALREADY_EXISTS` | 409 | An agent with this email is already registered | +| `AGENT_ALREADY_DECOMMISSIONED` | 409 | Agent has already been decommissioned | +| `CREDENTIAL_ALREADY_REVOKED` | 409 | Credential has already been revoked | +| `RATE_LIMIT_EXCEEDED` | 429 | 100 req/min limit exceeded | +| `FREE_TIER_LIMIT_EXCEEDED` | 403 | Free tier resource limit reached | +| `INSUFFICIENT_SCOPE` | 403 | Token is missing a required scope | +| `IMMUTABLE_FIELD` | 400 | Attempt to modify a field that cannot be changed | +| `AGENT_NOT_ACTIVE` | 403 | Operation requires agent to be in `active` status | +| `AGENT_DECOMMISSIONED` | 403 | Cannot modify a decommissioned agent | +| `RETENTION_WINDOW_EXCEEDED` | 400 | Requested audit date is outside the 90-day retention window | +| `INTERNAL_SERVER_ERROR` | 500 | Unexpected server error | + +### Rate limit headers + +Every response includes rate limit headers: + +| Header | Description | +|--------|-------------| +| `X-RateLimit-Limit` | Maximum requests per minute (100) | +| `X-RateLimit-Remaining` | Requests remaining in current window | +| `X-RateLimit-Reset` | Unix timestamp when the window resets | + +On `429` responses, wait until `X-RateLimit-Reset` before retrying. + +--- + +## Agent Registry + +### POST /agents — Register a new agent + +Creates a new AI agent identity. The `agentId` is system-assigned. + +**Auth**: Bearer token with `agents:write` scope. + +**Request body** (`application/json`): + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `email` | string | Yes | Unique email-format identifier | +| `agentType` | enum | Yes | `screener` \| `classifier` \| `orchestrator` \| `extractor` \| `summarizer` \| `router` \| `monitor` \| `custom` | +| `version` | string | Yes | Semantic version (e.g. `1.0.0`) | +| `capabilities` | string[] | Yes | `resource:action` strings, min 1 | +| `owner` | string | Yes | Owning team/org, 1–128 chars | +| `deploymentEnv` | enum | Yes | `development` \| `staging` \| `production` | + +**Response codes**: + +| Code | Meaning | +|------|---------| +| `201` | Agent registered successfully | +| `400` | Validation error | +| `401` | Invalid token | +| `403` | Insufficient scope or free tier limit reached | +| `409` | Email already registered | +| `429` | Rate limit exceeded | + +**Example**: + +```bash +curl -s -X POST http://localhost:3000/api/v1/agents \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "email": "screener-001@talent.ai", + "agentType": "screener", + "version": "1.0.0", + "capabilities": ["resume:read", "email:send"], + "owner": "talent-team", + "deploymentEnv": "production" + }' | jq . +``` + +--- + +### GET /agents — List agents + +Returns a paginated list of registered agents. + +**Auth**: Bearer token with `agents:read` scope. + +**Query parameters**: + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `page` | integer | 1 | Page number (1-based) | +| `limit` | integer | 20 | Results per page (max 100) | +| `owner` | string | — | Filter by owner (exact match) | +| `agentType` | enum | — | Filter by agent type | +| `status` | enum | — | Filter by status | + +**Response codes**: + +| Code | Meaning | +|------|---------| +| `200` | List returned | +| `400` | Invalid query parameters | +| `401` | Invalid token | +| `403` | Insufficient scope | +| `429` | Rate limit exceeded | + +**Example**: + +```bash +curl -s "http://localhost:3000/api/v1/agents?page=1&limit=20&status=active" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +--- + +### GET /agents/{agentId} — Get agent by ID + +Returns the full identity record for a single agent. + +**Auth**: Bearer token with `agents:read` scope. + +**Path parameters**: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `agentId` | UUID | The agent's immutable identifier | + +**Response codes**: + +| Code | Meaning | +|------|---------| +| `200` | Agent record returned | +| `401` | Invalid token | +| `403` | Insufficient scope | +| `404` | Agent not found | +| `429` | Rate limit exceeded | + +**Example**: + +```bash +curl -s "http://localhost:3000/api/v1/agents/$AGENT_ID" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +--- + +### PATCH /agents/{agentId} — Update agent metadata + +Partially updates agent metadata. Only provided fields are changed. Immutable fields (`agentId`, `email`, `createdAt`) cannot be updated. + +**Auth**: Bearer token with `agents:write` scope. + +**Request body** (`application/json`) — all fields optional: + +| Field | Type | Description | +|-------|------|-------------| +| `agentType` | enum | Updated agent type | +| `version` | string | Updated semantic version | +| `capabilities` | string[] | Updated capabilities (replaces the full list) | +| `owner` | string | Updated owner | +| `deploymentEnv` | enum | Updated deployment environment | +| `status` | enum | Updated status (`active` \| `suspended` \| `decommissioned`) | + +> Setting `status` to `decommissioned` is **irreversible**. The agent cannot be reactivated. + +**Response codes**: + +| Code | Meaning | +|------|---------| +| `200` | Agent updated, full record returned | +| `400` | Validation error or attempt to modify immutable field | +| `401` | Invalid token | +| `403` | Insufficient scope or agent is decommissioned | +| `404` | Agent not found | +| `429` | Rate limit exceeded | + +**Example**: + +```bash +curl -s -X PATCH "http://localhost:3000/api/v1/agents/$AGENT_ID" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ "version": "1.5.0", "status": "suspended" }' | jq . +``` + +--- + +### DELETE /agents/{agentId} — Decommission an agent + +Permanently decommissions an agent (soft delete). All active credentials are immediately revoked. This operation is **irreversible**. + +**Auth**: Bearer token with `agents:write` scope. + +**Response codes**: + +| Code | Meaning | +|------|---------| +| `204` | Agent decommissioned (no body) | +| `401` | Invalid token | +| `403` | Insufficient scope | +| `404` | Agent not found | +| `409` | Agent already decommissioned | +| `429` | Rate limit exceeded | + +**Example**: + +```bash +curl -s -X DELETE "http://localhost:3000/api/v1/agents/$AGENT_ID" \ + -H "Authorization: Bearer $TOKEN" \ + -o /dev/null -w "%{http_code}\n" +``` + +--- + +## OAuth 2.0 Tokens + +### POST /token — Issue an access token + +Issues a signed RS256 JWT via the OAuth 2.0 Client Credentials grant. + +**Auth**: Client credentials in the request body (no Bearer token required for this endpoint). + +> **Content-Type**: This endpoint uses `application/x-www-form-urlencoded`, not JSON. + +**Request fields** (form-encoded): + +| Field | Required | Description | +|-------|----------|-------------| +| `grant_type` | Yes | Must be `client_credentials` | +| `client_id` | Yes | Your agent's `agentId` (UUID) | +| `client_secret` | Yes | The credential secret | +| `scope` | No | Space-separated scopes. If omitted, all scopes are granted. | + +**Response codes**: + +| Code | Meaning | +|------|---------| +| `200` | Token issued | +| `400` | Malformed request, invalid scope, or unsupported grant type | +| `401` | Invalid `client_id` or `client_secret` | +| `403` | Agent suspended or monthly token limit reached | +| `429` | Rate limit exceeded | + +**Note on 429**: The `X-RateLimit-*` headers are returned on all responses, including `429`. + +**Example**: + +```bash +curl -s -X POST http://localhost:3000/api/v1/token \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id=$CLIENT_ID" \ + -d "client_secret=$CLIENT_SECRET" \ + -d "scope=agents:read agents:write" | jq . +``` + +--- + +### POST /token/introspect — Introspect a token + +Checks whether a token is active. Returns `{ "active": false }` for expired or revoked tokens — always `200 OK`. + +**Auth**: Bearer token with `tokens:read` scope. + +> **Content-Type**: `application/x-www-form-urlencoded` + +**Request fields**: + +| Field | Required | Description | +|-------|----------|-------------| +| `token` | Yes | The JWT to introspect | +| `token_type_hint` | No | Optional hint — `access_token` | + +**Response codes**: + +| Code | Meaning | +|------|---------| +| `200` | Result returned (check `active` field) | +| `400` | Missing `token` parameter | +| `401` | Caller's Bearer token is invalid | +| `403` | Caller's token lacks `tokens:read` scope | +| `429` | Rate limit exceeded | + +**Example**: + +```bash +curl -s -X POST http://localhost:3000/api/v1/token/introspect \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "token=$TOKEN_TO_CHECK" | jq . +``` + +--- + +### POST /token/revoke — Revoke a token + +Immediately invalidates a token. Idempotent — revoking an already-revoked token returns `200`. + +**Auth**: Bearer token (agent can revoke its own tokens). + +> **Content-Type**: `application/x-www-form-urlencoded` + +**Request fields**: + +| Field | Required | Description | +|-------|----------|-------------| +| `token` | Yes | The JWT to revoke | +| `token_type_hint` | No | Optional hint — `access_token` | + +**Response codes**: + +| Code | Meaning | +|------|---------| +| `200` | Token revoked (or was already inactive) | +| `400` | Missing `token` parameter | +| `401` | Caller's Bearer token is invalid | +| `403` | Insufficient permissions to revoke this token | +| `429` | Rate limit exceeded | + +**Example**: + +```bash +curl -s -X POST http://localhost:3000/api/v1/token/revoke \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "token=$TOKEN_TO_REVOKE" | jq . +``` + +--- + +## Credential Management + +### POST /agents/{agentId}/credentials — Generate credentials + +Creates a new `client_id` + `client_secret` pair. The `clientSecret` is returned **once only**. + +**Auth**: Bearer token with `agents:write` scope. + +**Request body** (`application/json`) — optional: + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `expiresAt` | ISO 8601 | No | Optional expiry date. Must be a future date. If omitted, credential does not expire. | + +**Response codes**: + +| Code | Meaning | +|------|---------| +| `201` | Credential created — save `clientSecret` now | +| `400` | Invalid `expiresAt` | +| `401` | Invalid token | +| `403` | Insufficient scope or agent not active | +| `404` | Agent not found | +| `429` | Rate limit exceeded | + +**Example**: + +```bash +curl -s -X POST "http://localhost:3000/api/v1/agents/$AGENT_ID/credentials" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ "expiresAt": "2027-01-01T00:00:00.000Z" }' | jq . +``` + +--- + +### GET /agents/{agentId}/credentials — List credentials + +Returns all credentials (active and revoked). The `clientSecret` is never returned. + +**Auth**: Bearer token with `agents:read` scope. + +**Query parameters**: + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `page` | integer | 1 | Page number | +| `limit` | integer | 20 | Results per page (max 100) | +| `status` | enum | — | Filter by `active` or `revoked` | + +**Response codes**: + +| Code | Meaning | +|------|---------| +| `200` | List returned | +| `400` | Invalid query parameters | +| `401` | Invalid token | +| `403` | Insufficient scope | +| `404` | Agent not found | +| `429` | Rate limit exceeded | + +**Example**: + +```bash +curl -s "http://localhost:3000/api/v1/agents/$AGENT_ID/credentials?status=active" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +--- + +### POST /agents/{agentId}/credentials/{credentialId}/rotate — Rotate a credential + +Replaces the `clientSecret` for the same `credentialId`. The old secret is immediately invalidated. + +**Auth**: Bearer token with `agents:write` scope. + +**Request body** (`application/json`) — optional: + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `expiresAt` | ISO 8601 | No | New expiry for the rotated credential | + +**Response codes**: + +| Code | Meaning | +|------|---------| +| `200` | Credential rotated — save new `clientSecret` now | +| `400` | Invalid `expiresAt` | +| `401` | Invalid token | +| `403` | Insufficient scope | +| `404` | Agent or credential not found | +| `409` | Credential is already revoked | +| `429` | Rate limit exceeded | + +**Example**: + +```bash +curl -s -X POST \ + "http://localhost:3000/api/v1/agents/$AGENT_ID/credentials/$CREDENTIAL_ID/rotate" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{}' | jq . +``` + +--- + +### DELETE /agents/{agentId}/credentials/{credentialId} — Revoke a credential + +Permanently revokes a credential. The credential can no longer obtain tokens. Irreversible. + +**Auth**: Bearer token with `agents:write` scope. + +**Response codes**: + +| Code | Meaning | +|------|---------| +| `204` | Credential revoked (no body) | +| `401` | Invalid token | +| `403` | Insufficient scope | +| `404` | Agent or credential not found | +| `409` | Credential already revoked | +| `429` | Rate limit exceeded | + +**Example**: + +```bash +curl -s -X DELETE \ + "http://localhost:3000/api/v1/agents/$AGENT_ID/credentials/$CREDENTIAL_ID" \ + -H "Authorization: Bearer $TOKEN" \ + -o /dev/null -w "%{http_code}\n" +``` + +--- + +## Audit Log + +### GET /audit — Query audit log + +Returns a paginated, filtered list of audit events (most recent first). + +**Auth**: Bearer token with `audit:read` scope. + +**Query parameters**: + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `page` | integer | 1 | Page number | +| `limit` | integer | 50 | Results per page (max 200) | +| `agentId` | UUID | — | Filter by agent | +| `action` | enum | — | Filter by action type (see [Audit Log guide](guides/query-audit-logs.md)) | +| `outcome` | enum | — | `success` or `failure` | +| `fromDate` | ISO 8601 | — | Events at or after this timestamp (max 90 days ago) | +| `toDate` | ISO 8601 | — | Events at or before this timestamp | + +**Response codes**: + +| Code | Meaning | +|------|---------| +| `200` | Events returned | +| `400` | Invalid parameters or date outside retention window | +| `401` | Invalid token | +| `403` | Token lacks `audit:read` scope | +| `429` | Rate limit exceeded | + +**Example**: + +```bash +curl -s "http://localhost:3000/api/v1/audit?agentId=$AGENT_ID&action=token.issued&limit=50" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +--- + +### GET /audit/{eventId} — Get audit event by ID + +Returns a single audit event by its immutable `eventId`. + +**Auth**: Bearer token with `audit:read` scope. + +**Path parameters**: + +| Parameter | Type | Description | +|-----------|------|-------------| +| `eventId` | UUID | The audit event's identifier | + +**Response codes**: + +| Code | Meaning | +|------|---------| +| `200` | Audit event returned | +| `401` | Invalid token | +| `403` | Token lacks `audit:read` scope | +| `404` | Event not found or outside 90-day retention window | +| `429` | Rate limit exceeded | + +**Example**: + +```bash +curl -s "http://localhost:3000/api/v1/audit/$EVENT_ID" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` diff --git a/docs/developers/concepts.md b/docs/developers/concepts.md new file mode 100644 index 0000000..5fe5690 --- /dev/null +++ b/docs/developers/concepts.md @@ -0,0 +1,128 @@ +# Core Concepts + +Everything you need to understand how SentryAgent.ai AgentIdP works — without needing to read an RFC. + +--- + +## What is AgentIdP? + +SentryAgent.ai AgentIdP is a free, open-source Identity Provider (IdP) built specifically for AI agents. It answers three questions that today's auth systems don't handle for agents: + +1. **Who is this agent?** — a unique, immutable identity registered in the AgentIdP registry +2. **Is it who it claims to be?** — verified via OAuth 2.0 credentials +3. **Is it allowed to do this?** — enforced via scope-based access control + +Think of it as the difference between a human logging in with a password and a service account authenticating with client credentials. Humans use passwords and MFA. Agents use `client_id` + `client_secret` — and AgentIdP manages that for them. + +--- + +## What is an AI Agent Identity? + +A human identity has a username, a password, and a profile. An AI agent identity has the equivalent: + +| Human | AI Agent | +|-------|----------| +| Username | `email` (unique identifier, e.g. `screener-001@myproject.ai`) | +| Immutable ID | `agentId` (UUID, assigned at registration, never changes) | +| Profile | `agentType`, `version`, `capabilities`, `owner`, `deploymentEnv` | +| Password | `clientSecret` (generated, stored as bcrypt hash) | +| Login session | JWT access token (1 hour, RS256 signed) | +| Account status | `status` (active / suspended / decommissioned) | + +The key difference from human identities: an agent's `agentId` is **immutable**. Once assigned, it never changes — even if other metadata is updated. This makes it safe to use as a stable reference across systems. + +Agents also carry **capabilities** — a list of `resource:action` strings (e.g. `resume:read`, `email:send`) that describe what the agent is permitted to do. These are informational in Phase 1 and will be enforced in the authorization layer in Phase 2. + +--- + +## AGNTCY Alignment + +AGNTCY is an open standard from the Linux Foundation that defines how AI agents should be identified, authenticated, and governed across different systems and platforms. + +The key principle: **agents are first-class identities**, not service accounts bolted onto human auth systems. + +What this means for you as a developer: + +- Your agent gets its own permanent ID that travels with it across systems +- Other AGNTCY-compliant systems can verify your agent's identity without trusting your word +- Your agent's full lifecycle — registration, credential rotation, decommission — follows a defined, interoperable model + +SentryAgent.ai implements AGNTCY's non-human identity model. When you register an agent here, you're registering it in a way that aligns with where the industry is heading, not a proprietary silo. + +--- + +## Agent Lifecycle + +Every agent moves through a defined set of states. Understanding these states matters because they affect whether your agent can authenticate. + +### States + +| State | What it means | Can get tokens? | Can be updated? | +|-------|---------------|-----------------|-----------------| +| `active` | Agent is operational | Yes | Yes | +| `suspended` | Temporarily disabled | No — credentials rejected | Yes — can be reactivated | +| `decommissioned` | Permanently retired | No — credentials revoked | No | + +### Transitions + +``` +registration + | + v + [active] <-----> [suspended] + | + v (irreversible) +[decommissioned] +``` + +**Suspending** an agent prevents it from obtaining new tokens. Existing unexpired tokens continue to work until they expire. Use suspension when you need to temporarily disable an agent (e.g. investigation, maintenance). + +**Decommissioning** an agent permanently retires it. All active credentials are immediately revoked. The agent record is retained in the database for audit purposes but the agent can never be reactivated. This operation is **irreversible** — use it only when you intend to permanently retire the agent. + +--- + +## OAuth 2.0 Client Credentials + +OAuth 2.0 is the auth standard used everywhere — GitHub, Google, Stripe. AgentIdP uses one specific flow from OAuth 2.0: the **Client Credentials grant**. + +Here is what actually happens when your agent authenticates: + +1. Your agent has a `client_id` (its `agentId`) and a `client_secret` (generated by AgentIdP) +2. Your agent sends both to `POST /token` along with the scopes it needs +3. AgentIdP verifies the secret, checks the agent is active, and issues a **JWT access token** +4. Your agent attaches that token as `Authorization: Bearer ` on all subsequent API calls +5. The token expires after 1 hour — your agent requests a new one + +There are no redirects, no browser windows, no user consent screens. It is a direct machine-to-machine exchange — exactly right for agents that run unattended. + +### Scopes + +Scopes limit what a token is permitted to do. Request only the scopes your agent actually needs. + +| Scope | What it allows | +|-------|----------------| +| `agents:read` | Read agent identity records | +| `agents:write` | Create, update, and decommission agent records | +| `tokens:read` | Introspect tokens (check if active/expired) | +| `audit:read` | Query the audit log | + +Example: an agent that only reads audit logs should request only `audit:read`. If it doesn't have `agents:write`, it cannot accidentally modify agent records. + +### The secret is shown once + +When you generate credentials (`POST /agents/{agentId}/credentials`), the `clientSecret` is returned in the response **one time only**. AgentIdP stores a bcrypt hash — the plaintext is gone. If you lose the secret, you rotate the credential to get a new one. + +--- + +## Free Tier Limits + +AgentIdP is free. These are the limits on the free tier: + +| Resource | Limit | What happens when exceeded | +|----------|-------|---------------------------| +| Registered agents | 100 | `POST /agents` returns `403 FREE_TIER_LIMIT_EXCEEDED` | +| Token requests/month | 10,000 | `POST /token` returns `403 unauthorized_client` | +| API rate limit | 100 req/min | All endpoints return `429 RATE_LIMIT_EXCEEDED` with `X-RateLimit-*` headers | +| Audit log retention | 90 days | Events older than 90 days are automatically purged; queries return empty results | + +The monthly token counter resets on the first day of each calendar month. The rate limit window resets every 60 seconds; the reset timestamp is in the `X-RateLimit-Reset` response header. diff --git a/docs/developers/guides/README.md b/docs/developers/guides/README.md new file mode 100644 index 0000000..ed44dcc --- /dev/null +++ b/docs/developers/guides/README.md @@ -0,0 +1,12 @@ +# Guides + +Step-by-step walkthroughs for each AgentIdP workflow. + +| Guide | What it covers | +|-------|----------------| +| [Register an Agent](register-an-agent.md) | All registration fields, validation rules, common errors and fixes | +| [Manage Credentials](manage-credentials.md) | Generate, list, rotate, and revoke credentials | +| [Issue and Revoke Tokens](issue-and-revoke-tokens.md) | OAuth 2.0 Client Credentials flow, JWT structure, introspect, revoke | +| [Query Audit Logs](query-audit-logs.md) | Filters, pagination, event structure, 90-day retention | + +All guides assume you have a running local server and a valid Bearer token. See the [Quick Start](../quick-start.md) if you haven't done that yet. diff --git a/docs/developers/guides/issue-and-revoke-tokens.md b/docs/developers/guides/issue-and-revoke-tokens.md new file mode 100644 index 0000000..8fbb535 --- /dev/null +++ b/docs/developers/guides/issue-and-revoke-tokens.md @@ -0,0 +1,203 @@ +# Issue and Revoke Tokens + +This guide covers the complete token lifecycle: issuing, using, inspecting, and revoking JWT access tokens. + +--- + +## Issue a token + +`POST /api/v1/token` + +This is the OAuth 2.0 Client Credentials grant. Your agent exchanges its `client_id` and `client_secret` for a signed JWT access token. + +> **Important**: This endpoint uses `application/x-www-form-urlencoded` encoding, not JSON. + +```bash +curl -s -X POST http://localhost:3000/api/v1/token \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id=$CLIENT_ID" \ + -d "client_secret=$CLIENT_SECRET" \ + -d "scope=agents:read agents:write" | jq . +``` + +Response (`200 OK`): + +```json +{ + "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJjbGllbnRfaWQiOiJhMWIyYzNkNC1lNWY2LTc4OTAtYWJjZC1lZjEyMzQ1Njc4OTAiLCJzY29wZSI6ImFnZW50czpyZWFkIGFnZW50czp3cml0ZSIsImp0aSI6InV1aWQtaGVyZSIsImlhdCI6MTc0MzE1MTIwMCwiZXhwIjoxNzQzMTU0ODAwfQ.signature", + "token_type": "Bearer", + "expires_in": 3600, + "scope": "agents:read agents:write" +} +``` + +The token expires in `3600` seconds (1 hour). Request a new one before it expires. + +### Request fields + +| Field | Required | Description | +|-------|----------|-------------| +| `grant_type` | Yes | Must be `client_credentials` | +| `client_id` | Yes | Your agent's `agentId` (UUID) | +| `client_secret` | Yes | The secret from credential generation | +| `scope` | No | Space-separated list of requested scopes. If omitted, all scopes are granted. | + +### Available scopes + +| Scope | What it allows | +|-------|----------------| +| `agents:read` | Read agent records | +| `agents:write` | Create, update, decommission agents | +| `tokens:read` | Introspect tokens | +| `audit:read` | Query audit logs | + +Request only the scopes your agent needs. + +--- + +## What's inside the JWT + +A JWT has three base64-encoded parts separated by dots: header, payload, and signature. The payload contains your agent's identity claims. + +Decode the payload to inspect it (for development only — never trust an unverified token in production): + +```bash +# Extract the middle part (payload) of your token and decode it +TOKEN_PAYLOAD=$(echo "$TOKEN" | cut -d. -f2) +echo "$TOKEN_PAYLOAD" | base64 --decode 2>/dev/null | jq . +``` + +Claims in the payload: + +| Claim | Description | +|-------|-------------| +| `sub` | Subject — your agent's `agentId` | +| `client_id` | The `agentId` that authenticated | +| `scope` | Scopes granted by this token | +| `jti` | JWT ID — unique identifier for this token (used for revocation) | +| `iat` | Issued at (Unix timestamp in seconds) | +| `exp` | Expires at (Unix timestamp in seconds) | + +--- + +## Use the token + +Include the token in the `Authorization` header of every API request: + +```bash +curl -s http://localhost:3000/api/v1/agents \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +--- + +## Introspect a token + +`POST /api/v1/token/introspect` + +Check whether a token is currently active (valid, not expired, not revoked). Requires a Bearer token with `tokens:read` scope. + +```bash +curl -s -X POST http://localhost:3000/api/v1/token/introspect \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "token=$TOKEN_TO_CHECK" | jq . +``` + +Response for an active token: + +```json +{ + "active": true, + "sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "client_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "scope": "agents:read agents:write", + "token_type": "Bearer", + "iat": 1743151200, + "exp": 1743154800 +} +``` + +Response for an inactive (expired or revoked) token: + +```json +{ + "active": false +} +``` + +> The introspect endpoint always returns `200 OK` — even for inactive tokens. You must check the `active` field to determine token validity. + +--- + +## Revoke a token + +`POST /api/v1/token/revoke` + +Immediately invalidates a token, preventing it from being used for any subsequent requests. Requires a Bearer token. + +```bash +curl -s -X POST http://localhost:3000/api/v1/token/revoke \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "token=$TOKEN_TO_REVOKE" | jq . +``` + +Response (`200 OK`): + +```json +{} +``` + +**Notes on revocation**: +- Revocation is immediate — the token is rejected on the next request +- Revoking an already-revoked or expired token is not an error (idempotent per RFC 7009) +- An agent can revoke its own tokens; revoking another agent's token requires an admin-scoped token +- Revoking a token does not affect the credential that issued it — new tokens can still be obtained using the same credentials + +--- + +## Token errors + +### `401 invalid_client` — wrong credentials + +```json +{ + "error": "invalid_client", + "error_description": "Client authentication failed. Invalid client_id or client_secret." +} +``` + +Check that `client_id` matches the agent's `agentId` and `client_secret` is the current active secret. + +### `403 unauthorized_client` — agent suspended or monthly limit reached + +```json +{ + "error": "unauthorized_client", + "error_description": "Agent is currently suspended and cannot obtain tokens." +} +``` + +Or: + +```json +{ + "error": "unauthorized_client", + "error_description": "Free tier monthly token limit of 10,000 requests has been reached." +} +``` + +For suspension: reactivate the agent first. For the monthly limit: the counter resets on the first day of the next calendar month. + +### `400 unsupported_grant_type` + +```json +{ + "error": "unsupported_grant_type", + "error_description": "Only 'client_credentials' grant type is supported." +} +``` + +Only `client_credentials` is supported. Do not use `authorization_code`, `password`, or other grant types. diff --git a/docs/developers/guides/manage-credentials.md b/docs/developers/guides/manage-credentials.md new file mode 100644 index 0000000..d4a0f45 --- /dev/null +++ b/docs/developers/guides/manage-credentials.md @@ -0,0 +1,167 @@ +# Manage Credentials + +A credential is a `client_id` + `client_secret` pair that your agent uses to get access tokens. This guide covers all four credential operations. + +All credential endpoints are under `/api/v1/agents/{agentId}/credentials` and require a Bearer token with `agents:write` scope. + +--- + +## Generate credentials + +`POST /api/v1/agents/{agentId}/credentials` + +Creates a new credential for the agent. The `clientSecret` is returned **once only**. + +```bash +curl -s -X POST "http://localhost:3000/api/v1/agents/$AGENT_ID/credentials" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{}' | jq . +``` + +To set an expiry date (optional): + +```bash +curl -s -X POST "http://localhost:3000/api/v1/agents/$AGENT_ID/credentials" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ "expiresAt": "2027-03-28T00:00:00.000Z" }' | jq . +``` + +Response (`201 Created`): + +```json +{ + "credentialId": "c9d8e7f6-a5b4-3210-fedc-ba9876543210", + "clientId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "clientSecret": "sk_live_7f3a2b1c9d8e4f0a6b5c3d2e1f0a9b8c", + "status": "active", + "createdAt": "2026-03-28T09:00:00.000Z", + "expiresAt": "2027-03-28T00:00:00.000Z", + "revokedAt": null +} +``` + +> **Save the `clientSecret` immediately.** It is shown once. The server stores a bcrypt hash and cannot recover the plaintext. If you lose it, rotate the credential to get a new one. + +An agent can hold **multiple active credentials** at the same time. This supports zero-downtime rotation: generate a new credential, update all consumers to use it, then revoke the old one. + +**Restrictions**: +- The agent must be in `active` status. Suspended and decommissioned agents cannot generate credentials. + +--- + +## List credentials + +`GET /api/v1/agents/{agentId}/credentials` + +Returns all credentials for the agent (both active and revoked). The `clientSecret` is **never** returned in list responses. + +```bash +curl -s "http://localhost:3000/api/v1/agents/$AGENT_ID/credentials" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +Response: + +```json +{ + "data": [ + { + "credentialId": "c9d8e7f6-a5b4-3210-fedc-ba9876543210", + "clientId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "status": "active", + "createdAt": "2026-03-28T09:00:00.000Z", + "expiresAt": "2027-03-28T00:00:00.000Z", + "revokedAt": null + } + ], + "total": 1, + "page": 1, + "limit": 20 +} +``` + +### Pagination + +```bash +curl -s "http://localhost:3000/api/v1/agents/$AGENT_ID/credentials?page=1&limit=50" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +### Filter by status + +```bash +# Active credentials only +curl -s "http://localhost:3000/api/v1/agents/$AGENT_ID/credentials?status=active" \ + -H "Authorization: Bearer $TOKEN" | jq . + +# Revoked credentials only +curl -s "http://localhost:3000/api/v1/agents/$AGENT_ID/credentials?status=revoked" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +--- + +## Rotate a credential + +`POST /api/v1/agents/{agentId}/credentials/{credentialId}/rotate` + +Rotation immediately invalidates the current `clientSecret` and generates a new one — the `credentialId` stays the same. Use this for periodic secret rotation or emergency rotation if a secret is compromised. + +```bash +curl -s -X POST \ + "http://localhost:3000/api/v1/agents/$AGENT_ID/credentials/$CREDENTIAL_ID/rotate" \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{}' | jq . +``` + +Response (`200 OK`): + +```json +{ + "credentialId": "c9d8e7f6-a5b4-3210-fedc-ba9876543210", + "clientId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "clientSecret": "sk_live_9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d", + "status": "active", + "createdAt": "2026-03-28T09:00:00.000Z", + "expiresAt": null, + "revokedAt": null +} +``` + +**What changes after rotation**: +- The `clientSecret` is a new value — the old secret is immediately invalid +- The `credentialId` is the same — no changes needed to references by ID +- Any tokens issued using the old secret remain valid until they expire naturally (tokens are not revoked by credential rotation) + +**What cannot be rotated**: A `revoked` credential cannot be rotated. Generate a new credential instead. + +--- + +## Revoke a credential + +`DELETE /api/v1/agents/{agentId}/credentials/{credentialId}` + +Permanently revokes a credential. The credential can no longer be used to obtain new tokens. + +```bash +curl -s -X DELETE \ + "http://localhost:3000/api/v1/agents/$AGENT_ID/credentials/$CREDENTIAL_ID" \ + -H "Authorization: Bearer $TOKEN" \ + -o /dev/null -w "%{http_code}\n" +``` + +Successful response: `204 No Content` (empty body). + +**Effects of revocation**: +- The credential status is set to `revoked` +- The credential cannot be used to call `POST /token` +- Any tokens that were issued using this credential remain valid until they expire — to immediately invalidate tokens, revoke them explicitly using `POST /token/revoke` +- The credential record is retained for audit purposes +- Revocation is **irreversible** — a revoked credential cannot be re-activated + +**Revocation vs decommission**: +- Revoking a credential affects that credential only; the agent stays active +- Decommissioning an agent (`DELETE /api/v1/agents/{agentId}`) revokes all credentials simultaneously and permanently retires the agent diff --git a/docs/developers/guides/query-audit-logs.md b/docs/developers/guides/query-audit-logs.md new file mode 100644 index 0000000..e865452 --- /dev/null +++ b/docs/developers/guides/query-audit-logs.md @@ -0,0 +1,183 @@ +# Query Audit Logs + +The audit log is an immutable, append-only record of every significant action on the AgentIdP platform. This guide covers how to query it, what filters are available, and how retention works. + +Requires: `Authorization: Bearer ` with `audit:read` scope. + +--- + +## What gets logged + +Every action below is automatically recorded. You cannot create, modify, or delete audit events — the log is read-only via the API. + +| Action | Triggered by | +|--------|-------------| +| `agent.created` | Successful `POST /agents` | +| `agent.updated` | Successful `PATCH /agents/{agentId}` | +| `agent.decommissioned` | Successful `DELETE /agents/{agentId}` | +| `agent.suspended` | Status changed to `suspended` | +| `agent.reactivated` | Status changed from `suspended` to `active` | +| `token.issued` | Successful `POST /token` | +| `token.revoked` | Successful `POST /token/revoke` | +| `token.introspected` | Successful `POST /token/introspect` | +| `credential.generated` | Successful `POST /agents/{agentId}/credentials` | +| `credential.rotated` | Successful `POST /agents/{agentId}/credentials/{credentialId}/rotate` | +| `credential.revoked` | Successful `DELETE /agents/{agentId}/credentials/{credentialId}` | +| `auth.failed` | Failed authentication attempt on `POST /token` | + +--- + +## Query the audit log + +`GET /api/v1/audit` + +Returns a paginated list of audit events, most recent first. + +```bash +curl -s "http://localhost:3000/api/v1/audit" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +Response: + +```json +{ + "data": [ + { + "eventId": "f1e2d3c4-b5a6-7890-cdef-123456789012", + "agentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "action": "token.issued", + "outcome": "success", + "ipAddress": "127.0.0.1", + "userAgent": "curl/7.88.1", + "metadata": { + "scope": "agents:read agents:write", + "expiresAt": "2026-03-28T10:00:00.000Z" + }, + "timestamp": "2026-03-28T09:00:00.000Z" + } + ], + "total": 47, + "page": 1, + "limit": 50 +} +``` + +--- + +## Audit event structure + +| Field | Type | Description | +|-------|------|-------------| +| `eventId` | UUID | Immutable unique ID for this event | +| `agentId` | UUID | The agent that triggered the event | +| `action` | string | What happened (see table above) | +| `outcome` | string | `success` or `failure` | +| `ipAddress` | string | Client IP (IPv4 or IPv6) | +| `userAgent` | string | HTTP User-Agent from the request | +| `metadata` | object | Action-specific details (varies by action) | +| `timestamp` | ISO 8601 | When the event occurred | + +### `metadata` by action + +| Action | Metadata fields | +|--------|----------------| +| `token.issued` | `scope`, `expiresAt` | +| `credential.generated` | `credentialId` | +| `credential.rotated` | `credentialId` | +| `agent.created` | `agentType`, `owner` | +| `auth.failed` | `reason`, `clientId` | + +--- + +## Filters + +All filter parameters are optional and can be combined (logical AND). + +### Filter by agent + +```bash +curl -s "http://localhost:3000/api/v1/audit?agentId=$AGENT_ID" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +### Filter by action + +```bash +curl -s "http://localhost:3000/api/v1/audit?action=token.issued" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +### Filter by outcome + +```bash +# Failed authentication attempts only +curl -s "http://localhost:3000/api/v1/audit?outcome=failure" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +### Filter by date range + +```bash +curl -s "http://localhost:3000/api/v1/audit?fromDate=2026-03-01T00:00:00.000Z&toDate=2026-03-28T23:59:59.999Z" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +### Combine filters + +```bash +# All failed token requests for a specific agent today +curl -s "http://localhost:3000/api/v1/audit?agentId=$AGENT_ID&action=auth.failed&outcome=failure&fromDate=2026-03-28T00:00:00.000Z" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +--- + +## Pagination + +Default page size is 50, maximum is 200. + +```bash +curl -s "http://localhost:3000/api/v1/audit?page=2&limit=100" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +Use `total`, `page`, and `limit` from the response to calculate the number of pages: + +``` +total_pages = ceil(total / limit) +``` + +--- + +## Get a single event + +`GET /api/v1/audit/{eventId}` + +```bash +curl -s "http://localhost:3000/api/v1/audit/$EVENT_ID" \ + -H "Authorization: Bearer $TOKEN" | jq . +``` + +--- + +## Retention — 90 days + +On the free tier, audit events are retained for 90 days. Events older than 90 days are automatically purged. + +- Querying for dates outside the 90-day window returns an empty result set — not an error +- Requesting a specific `eventId` for a purged event returns `404 Not Found` +- The `fromDate` filter cannot be set to a date older than 90 days; doing so returns `400 RETENTION_WINDOW_EXCEEDED` + +To check the earliest available date: + +```json +{ + "code": "RETENTION_WINDOW_EXCEEDED", + "message": "Free tier audit log retention is 90 days. Requested date is outside the retention window.", + "details": { + "retentionDays": 90, + "earliestAvailable": "2025-12-28T00:00:00.000Z" + } +} +``` diff --git a/docs/developers/guides/register-an-agent.md b/docs/developers/guides/register-an-agent.md new file mode 100644 index 0000000..20b7eb1 --- /dev/null +++ b/docs/developers/guides/register-an-agent.md @@ -0,0 +1,172 @@ +# Register an Agent + +This guide covers everything about registering a new agent identity, including all fields, validation rules, and how to fix common errors. + +--- + +## The registration request + +`POST /api/v1/agents` + +Requires: `Authorization: Bearer ` with `agents:write` scope. + +### Request fields + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `email` | string (email) | Yes | Unique identifier for this agent. Must be a valid email format and unique across all registered agents. | +| `agentType` | string (enum) | Yes | Functional classification of the agent. See values below. | +| `version` | string (semver) | Yes | Semantic version of the agent software (e.g. `1.0.0`, `2.3.1-beta`). | +| `capabilities` | string[] | Yes | One or more capability strings in `resource:action` format. Minimum 1. | +| `owner` | string | Yes | Team or organisation that owns this agent. 1–128 characters. | +| `deploymentEnv` | string (enum) | Yes | Target deployment environment. See values below. | + +### `agentType` values + +| Value | Description | +|-------|-------------| +| `screener` | Screens or filters content | +| `classifier` | Classifies or categorises inputs | +| `orchestrator` | Coordinates other agents or workflows | +| `extractor` | Extracts structured data | +| `summarizer` | Produces summaries | +| `router` | Routes requests to other agents | +| `monitor` | Monitors systems or outputs | +| `custom` | Any type not covered above | + +### `deploymentEnv` values + +| Value | Description | +|-------|-------------| +| `development` | Local or dev environment | +| `staging` | Pre-production testing | +| `production` | Live production workloads | + +### `capabilities` format + +Each capability is a string matching `resource:action`. Examples: + +``` +resume:read +email:send +candidate:score +document:classify +data:* +``` + +The `*` wildcard in the action position means all actions on that resource. Capabilities are informational in Phase 1. + +--- + +## Example — register a screener agent + +```bash +curl -s -X POST http://localhost:3000/api/v1/agents \ + -H "Authorization: Bearer $TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "email": "screener-001@talent.ai", + "agentType": "screener", + "version": "1.0.0", + "capabilities": ["resume:read", "email:send", "candidate:score"], + "owner": "talent-acquisition-team", + "deploymentEnv": "production" + }' | jq . +``` + +Successful response (`201 Created`): + +```json +{ + "agentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "email": "screener-001@talent.ai", + "agentType": "screener", + "version": "1.0.0", + "capabilities": ["resume:read", "email:send", "candidate:score"], + "owner": "talent-acquisition-team", + "deploymentEnv": "production", + "status": "active", + "createdAt": "2026-03-28T09:00:00.000Z", + "updatedAt": "2026-03-28T09:00:00.000Z" +} +``` + +The `agentId` is assigned by the system — it is immutable and never changes. + +--- + +## Immutable fields + +After registration, the following fields **cannot be changed**: + +- `agentId` — system-assigned, permanent +- `email` — the agent's stable identity +- `createdAt` — registration timestamp + +To update any other field, use `PATCH /api/v1/agents/{agentId}`. + +--- + +## Common errors and fixes + +### `400 VALIDATION_ERROR` — invalid email format + +```json +{ + "code": "VALIDATION_ERROR", + "message": "Request validation failed.", + "details": { "field": "email", "reason": "Must be a valid email address." } +} +``` + +**Fix**: Use a valid email format, e.g. `my-agent@myproject.ai`. + +--- + +### `400 VALIDATION_ERROR` — invalid version format + +```json +{ + "code": "VALIDATION_ERROR", + "message": "Request validation failed.", + "details": { "field": "version", "reason": "Must be a valid semantic version string." } +} +``` + +**Fix**: Use semantic versioning — `1.0.0`, `2.1.3`, `1.0.0-beta.1`. The format is `MAJOR.MINOR.PATCH`. + +--- + +### `400 VALIDATION_ERROR` — invalid capability format + +Capabilities must match `resource:action` — lowercase letters, numbers, hyphens, and underscores only. + +**Fix**: Use `resume:read` not `Resume:Read` or `read-resume`. + +--- + +### `409 AGENT_ALREADY_EXISTS` — duplicate email + +```json +{ + "code": "AGENT_ALREADY_EXISTS", + "message": "An agent with this email address is already registered.", + "details": { "email": "screener-001@talent.ai" } +} +``` + +**Fix**: Choose a different email address. Each agent must have a unique email. + +--- + +### `403 FREE_TIER_LIMIT_EXCEEDED` — 100 agent limit reached + +```json +{ + "code": "FREE_TIER_LIMIT_EXCEEDED", + "message": "Free tier limit of 100 registered agents has been reached.", + "details": { "limit": 100, "current": 100 } +} +``` + +**Fix**: Decommission agents you no longer need before registering new ones. diff --git a/docs/developers/quick-start.md b/docs/developers/quick-start.md new file mode 100644 index 0000000..ad5f84c --- /dev/null +++ b/docs/developers/quick-start.md @@ -0,0 +1,247 @@ +# Quick Start — Register Your First Agent + +This guide gets you from zero to a working agent identity with a valid OAuth 2.0 access token. It takes under 5 minutes. + +## Prerequisites + +You need two tools installed: + +- **Docker** (includes `docker-compose`) — to run PostgreSQL and Redis +- **Node.js 18+** (includes `npm`) — to run the server +- **curl** — to call the API + +Nothing else. No accounts, no sign-ups. + +--- + +## Step 1 — Clone and configure + +```bash +git clone https://git.sentryagent.ai/vijay_admin/sentryagent-idp.git +cd sentryagent-idp +npm install +``` + +Generate an RSA keypair for signing tokens (required): + +```bash +# Generate private key +openssl genrsa -out private.pem 2048 + +# Extract public key +openssl rsa -in private.pem -pubout -out public.pem +``` + +Create your `.env` file: + +```bash +cat > .env << 'EOF' +DATABASE_URL=postgresql://sentryagent:sentryagent@localhost:5432/sentryagent_idp +REDIS_URL=redis://localhost:6379 +PORT=3000 +JWT_PRIVATE_KEY="$(cat private.pem)" +JWT_PUBLIC_KEY="$(cat public.pem)" +EOF +``` + +> **Note**: The `.env` file stores your private key. Do not commit it to version control. + +--- + +## Step 2 — Start infrastructure + +Start PostgreSQL and Redis using Docker Compose (infrastructure services only): + +```bash +docker-compose up -d postgres redis +``` + +Expected output: + +``` +[+] Running 2/2 + ✔ Container sentryagent-idp-postgres-1 Healthy + ✔ Container sentryagent-idp-redis-1 Healthy +``` + +Services are ready when both show `Healthy`. Run migrations: + +```bash +npm run db:migrate +``` + +Expected output: + +``` +Running database migrations... + ✓ Applied: 001_create_agents.sql + ✓ Applied: 002_create_credentials.sql + ✓ Applied: 003_create_tokens.sql + ✓ Applied: 004_create_audit_log.sql + +Migrations complete. 4 migration(s) applied. +``` + +--- + +## Step 3 — Start the AgentIdP server + +```bash +npm run dev +``` + +Expected output: + +``` +SentryAgent.ai AgentIdP listening on port 3000 +Database pool connected +Redis client connected +``` + +The API is now live at `http://localhost:3000/api/v1`. + +--- + +## Step 4 — Generate a bootstrap token + +All API endpoints require a Bearer token. For first-time setup, generate a bootstrap token using your RSA private key: + +```bash +node -e " +const jwt = require('jsonwebtoken'); +const fs = require('fs'); +const { v4: uuidv4 } = require('uuid'); +const key = fs.readFileSync('private.pem', 'utf8'); +const now = Math.floor(Date.now() / 1000); +const token = jwt.sign({ + sub: 'bootstrap', + client_id: 'bootstrap', + scope: 'agents:read agents:write tokens:read audit:read', + jti: uuidv4(), + iat: now, + exp: now + 3600 +}, key, { algorithm: 'RS256' }); +console.log(token); +" +``` + +Copy the token output and export it: + +```bash +export BOOTSTRAP_TOKEN="" +``` + +> This bootstrap token is a one-time tool for registering your first agent. Once you have an agent with credentials, use `POST /token` for all subsequent authentication. + +--- + +## Step 5 — Register an agent + +```bash +curl -s -X POST http://localhost:3000/api/v1/agents \ + -H "Authorization: Bearer $BOOTSTRAP_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "email": "my-first-agent@myproject.ai", + "agentType": "custom", + "version": "1.0.0", + "capabilities": ["data:read"], + "owner": "my-team", + "deploymentEnv": "development" + }' | jq . +``` + +Example response (`201 Created`): + +```json +{ + "agentId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "email": "my-first-agent@myproject.ai", + "agentType": "custom", + "version": "1.0.0", + "capabilities": ["data:read"], + "owner": "my-team", + "deploymentEnv": "development", + "status": "active", + "createdAt": "2026-03-28T09:00:00.000Z", + "updatedAt": "2026-03-28T09:00:00.000Z" +} +``` + +Save the `agentId`: + +```bash +export AGENT_ID="a1b2c3d4-e5f6-7890-abcd-ef1234567890" +``` + +--- + +## Step 6 — Generate a credential + +```bash +curl -s -X POST "http://localhost:3000/api/v1/agents/$AGENT_ID/credentials" \ + -H "Authorization: Bearer $BOOTSTRAP_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{}' | jq . +``` + +Example response (`201 Created`): + +```json +{ + "credentialId": "c9d8e7f6-a5b4-3210-fedc-ba9876543210", + "clientId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "clientSecret": "sk_live_7f3a2b1c9d8e4f0a6b5c3d2e1f0a9b8c", + "status": "active", + "createdAt": "2026-03-28T09:00:00.000Z", + "expiresAt": null, + "revokedAt": null +} +``` + +> **Save the `clientSecret` now.** It is shown once and never retrievable again. The server stores only a bcrypt hash. + +```bash +export CLIENT_ID="a1b2c3d4-e5f6-7890-abcd-ef1234567890" # same as AGENT_ID +export CLIENT_SECRET="sk_live_7f3a2b1c9d8e4f0a6b5c3d2e1f0a9b8c" +``` + +--- + +## Step 7 — Issue an access token + +Use the OAuth 2.0 Client Credentials flow. Note that the `/token` endpoint uses **form-encoded** body, not JSON: + +```bash +curl -s -X POST http://localhost:3000/api/v1/token \ + -H "Content-Type: application/x-www-form-urlencoded" \ + -d "grant_type=client_credentials" \ + -d "client_id=$CLIENT_ID" \ + -d "client_secret=$CLIENT_SECRET" \ + -d "scope=agents:read agents:write" | jq . +``` + +Example response (`200 OK`): + +```json +{ + "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", + "token_type": "Bearer", + "expires_in": 3600, + "scope": "agents:read agents:write" +} +``` + +```bash +export TOKEN="eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." +``` + +Your agent now has a valid JWT. Use it in the `Authorization: Bearer ` header for all API calls. + +--- + +## What's next + +- [Core Concepts](concepts.md) — understand AgentIdP, AGNTCY, and the agent identity model +- [Guides](guides/README.md) — step-by-step walkthroughs for credentials, tokens, and audit logs +- [API Reference](api-reference.md) — every endpoint documented with curl examples diff --git a/openspec/changes/phase-1-mvp-implementation/.openspec.yaml b/openspec/changes/archive/2026-03-28-phase-1-mvp-implementation/.openspec.yaml similarity index 100% rename from openspec/changes/phase-1-mvp-implementation/.openspec.yaml rename to openspec/changes/archive/2026-03-28-phase-1-mvp-implementation/.openspec.yaml diff --git a/openspec/changes/phase-1-mvp-implementation/design.md b/openspec/changes/archive/2026-03-28-phase-1-mvp-implementation/design.md similarity index 100% rename from openspec/changes/phase-1-mvp-implementation/design.md rename to openspec/changes/archive/2026-03-28-phase-1-mvp-implementation/design.md diff --git a/openspec/changes/phase-1-mvp-implementation/proposal.md b/openspec/changes/archive/2026-03-28-phase-1-mvp-implementation/proposal.md similarity index 100% rename from openspec/changes/phase-1-mvp-implementation/proposal.md rename to openspec/changes/archive/2026-03-28-phase-1-mvp-implementation/proposal.md diff --git a/openspec/changes/phase-1-mvp-implementation/specs/agent-registry/spec.md b/openspec/changes/archive/2026-03-28-phase-1-mvp-implementation/specs/agent-registry/spec.md similarity index 100% rename from openspec/changes/phase-1-mvp-implementation/specs/agent-registry/spec.md rename to openspec/changes/archive/2026-03-28-phase-1-mvp-implementation/specs/agent-registry/spec.md diff --git a/openspec/changes/phase-1-mvp-implementation/specs/audit-log/spec.md b/openspec/changes/archive/2026-03-28-phase-1-mvp-implementation/specs/audit-log/spec.md similarity index 100% rename from openspec/changes/phase-1-mvp-implementation/specs/audit-log/spec.md rename to openspec/changes/archive/2026-03-28-phase-1-mvp-implementation/specs/audit-log/spec.md diff --git a/openspec/changes/phase-1-mvp-implementation/specs/credential-management/spec.md b/openspec/changes/archive/2026-03-28-phase-1-mvp-implementation/specs/credential-management/spec.md similarity index 100% rename from openspec/changes/phase-1-mvp-implementation/specs/credential-management/spec.md rename to openspec/changes/archive/2026-03-28-phase-1-mvp-implementation/specs/credential-management/spec.md diff --git a/openspec/changes/phase-1-mvp-implementation/specs/oauth2-token/spec.md b/openspec/changes/archive/2026-03-28-phase-1-mvp-implementation/specs/oauth2-token/spec.md similarity index 100% rename from openspec/changes/phase-1-mvp-implementation/specs/oauth2-token/spec.md rename to openspec/changes/archive/2026-03-28-phase-1-mvp-implementation/specs/oauth2-token/spec.md diff --git a/openspec/changes/phase-1-mvp-implementation/tasks.md b/openspec/changes/archive/2026-03-28-phase-1-mvp-implementation/tasks.md similarity index 100% rename from openspec/changes/phase-1-mvp-implementation/tasks.md rename to openspec/changes/archive/2026-03-28-phase-1-mvp-implementation/tasks.md diff --git a/openspec/changes/bedroom-developer-docs/.openspec.yaml b/openspec/changes/bedroom-developer-docs/.openspec.yaml new file mode 100644 index 0000000..65bf7c9 --- /dev/null +++ b/openspec/changes/bedroom-developer-docs/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-03-28 diff --git a/openspec/changes/bedroom-developer-docs/design.md b/openspec/changes/bedroom-developer-docs/design.md new file mode 100644 index 0000000..08a541f --- /dev/null +++ b/openspec/changes/bedroom-developer-docs/design.md @@ -0,0 +1,63 @@ +## Context + +Phase 1 MVP is complete: 46 source files, 14 API endpoints across 4 OpenAPI 3.0 specs, 244 passing tests. The implementation is production-grade and live on `git.sentryagent.ai`. However, the developer experience stops at the code. There is no entry point for a bedroom developer who has never heard of AgentIdP, AGNTCY, or client credentials OAuth 2.0. + +The documentation must be written, owned, and maintained as a first-class deliverable — not an afterthought. It is produced by a Virtual Technical Writer subagent with full access to the codebase and OpenAPI specs. + +**Constraints:** +- Audience: bedroom developers — assume competence with HTTP and basic programming, assume no prior knowledge of AgentIdP or AGNTCY +- Format: Markdown only — renders on GitHub, no external tooling required +- No build step — docs are static `.md` files in `docs/developers/` +- All code examples must be real, runnable, and copy-pasteable +- Tone: direct, practical, no enterprise jargon + +## Goals / Non-Goals + +**Goals:** +- Bedroom developer can register their first agent and issue a token in under 5 minutes using only the quick-start guide +- Every API endpoint is documented in plain English with at least one working curl example +- Core concepts are explained without assuming prior knowledge of OAuth 2.0 or AGNTCY +- All four P0 workflows (register, credential, token, audit) have step-by-step guides +- FAQ covers the most likely failure points and free-tier limits + +**Non-Goals:** +- No web-rendered documentation site (Phase 2 — out of scope) +- No SDK documentation (Node.js SDK not yet built — Phase 1 P1 remaining) +- No video tutorials or interactive demos +- No multi-language code examples (Node.js + curl only for now) +- No enterprise deployment documentation (separate from bedroom developer focus) + +## Decisions + +**Decision 1: Single flat folder vs nested structure** +Chosen: flat `docs/developers/` with a `tutorials/` subfolder only for multi-step guides. +Alternative considered: deep nesting by category. Rejected — adds navigation friction for a small doc set. + +**Decision 2: Raw OpenAPI YAML as API reference vs human-written reference** +Chosen: human-written `api-reference.md` alongside the existing OpenAPI specs. +Alternative considered: link to raw YAML only. Rejected — YAML is not readable for bedroom developers; the whole point is accessibility. + +**Decision 3: Standalone docs vs inline code comments** +Chosen: standalone Markdown files in `docs/developers/`. +Alternative considered: JSDoc-generated docs. Rejected — JSDoc is for library consumers, not REST API users. + +**Decision 4: Who writes the docs** +Chosen: Virtual Technical Writer subagent — spawned by CTO with full codebase + OpenAPI spec context. +Alternative considered: Virtual Principal Developer writes docs. Rejected — developer time should stay on code; writing accessible prose for non-technical audiences is a distinct skill warranting a dedicated role. + +**Decision 5: Versioning** +Chosen: docs live in the same repo as code, versioned together via git. No separate docs versioning scheme in Phase 1. + +## Risks / Trade-offs + +- **[Risk] Docs drift from implementation** → Mitigation: Virtual QA Engineer verifies API reference examples against actual endpoints before sign-off; curl examples are tested against a running instance +- **[Risk] Tone inconsistency across docs** → Mitigation: Technical Writer receives a unified style brief in the subagent prompt (plain English, second person, imperative voice, no jargon) +- **[Risk] Quick-start prerequisites unclear** → Mitigation: Quick-start lists exact prerequisites (Docker, curl, nothing else) and links to docker-compose.yml + +## Migration Plan + +Documentation only — no migration required. Files are added to `docs/developers/` and committed to `develop`. No rollback needed. + +## Open Questions + +*(none — scope is fully defined)* diff --git a/openspec/changes/bedroom-developer-docs/proposal.md b/openspec/changes/bedroom-developer-docs/proposal.md new file mode 100644 index 0000000..d2833e9 --- /dev/null +++ b/openspec/changes/bedroom-developer-docs/proposal.md @@ -0,0 +1,34 @@ +## Why + +SentryAgent.ai AgentIdP Phase 1 MVP is fully implemented, tested, and live — but there is zero human-readable documentation for the developers we are building this for. A bedroom developer landing on this repo today cannot register their first agent without reading raw OpenAPI YAML or diving into source code. We fix that now. + +## What Changes + +- New `docs/developers/` folder containing a complete, self-contained documentation set for bedroom developers +- Quick-start guide: first agent registered and authenticated in under 5 minutes +- Core concepts doc: plain-English explanation of AgentIdP, AGNTCY alignment, and the agent identity model +- Step-by-step guides: agent registration, credential management, token issuance, audit log queries +- Human-friendly API reference: every endpoint documented with real curl examples and response samples +- FAQ: common errors, gotchas, and free-tier limits explained +- All docs written for a bedroom developer audience — no enterprise jargon, no assumed knowledge + +## Capabilities + +### New Capabilities + +- `quick-start`: 5-minute guide from zero to first authenticated agent request — install, register, credential, token, done +- `core-concepts`: Plain-English explanation of what AgentIdP is, how it relates to AGNTCY, the agent identity lifecycle, and why it matters +- `developer-guides`: Step-by-step tutorials for the four core workflows: registering an agent, managing credentials, issuing and revoking tokens, querying the audit log +- `api-reference`: Human-friendly API reference covering all 14 endpoints with real examples, field descriptions, error codes, and rate limit notes + +### Modified Capabilities + +*(none — this change introduces documentation only; no existing API specs are modified)* + +## Impact + +- New folder: `docs/developers/` (7 markdown files) +- No code changes — documentation only +- No new dependencies +- No API changes +- Existing `docs/openapi/` specs are reference material for the Technical Writer but are not modified diff --git a/openspec/changes/bedroom-developer-docs/specs/api-reference/spec.md b/openspec/changes/bedroom-developer-docs/specs/api-reference/spec.md new file mode 100644 index 0000000..2e7f80a --- /dev/null +++ b/openspec/changes/bedroom-developer-docs/specs/api-reference/spec.md @@ -0,0 +1,50 @@ +## ADDED Requirements + +### Requirement: API reference exists at docs/developers/api-reference.md +The system SHALL provide a human-readable API reference at `docs/developers/api-reference.md` covering all 14 endpoints across the four services: Agent Registry, OAuth 2.0 Token, Credential Management, and Audit Log. + +#### Scenario: Developer finds any endpoint within 10 seconds +- **WHEN** the developer opens the API reference +- **THEN** they SHALL find a table of contents at the top linking to each of the four service sections + +### Requirement: Every endpoint is documented with method, path, description, and auth requirements +For each of the 14 endpoints, the reference SHALL document: HTTP method, path, one-sentence description, and whether Bearer token auth is required. + +#### Scenario: Developer knows which endpoints require authentication +- **WHEN** the developer scans the reference +- **THEN** they SHALL clearly see which endpoints require a Bearer token (all except POST /token) and which do not + +### Requirement: Every endpoint includes a complete curl example +For each endpoint, the reference SHALL include at least one complete, runnable curl example with real placeholder values. + +#### Scenario: Developer copies a curl example and runs it +- **WHEN** the developer copies a curl example from the reference +- **THEN** the command SHALL be complete — no ellipses, no `...`, no missing flags — requiring only substitution of their own agentId, token, and base URL + +### Requirement: Every endpoint documents all request parameters and body fields +For each endpoint that accepts a request body or query parameters, the reference SHALL list every field with: name, type, required/optional, description, and validation constraints. + +#### Scenario: Developer knows what fields are required for POST /agents +- **WHEN** the developer reads the POST /agents section +- **THEN** they SHALL see a table listing every field, its type, whether it is required, and any constraints (e.g. email format, max length) + +### Requirement: Every endpoint documents all response codes and response body schemas +For each endpoint, the reference SHALL document every possible HTTP response code (2xx and 4xx/5xx) with a description and example response body. + +#### Scenario: Developer understands a 429 response +- **WHEN** the developer reads the rate limit error documentation +- **THEN** they SHALL understand what triggered it, what the X-RateLimit-* headers mean, and when they can retry + +### Requirement: API reference includes a base URL and versioning section +The reference SHALL include a section at the top explaining the base URL convention, port configuration, and that all endpoints are unversioned in Phase 1. + +#### Scenario: Developer knows where to send requests +- **WHEN** the developer reads the base URL section +- **THEN** they SHALL see the default base URL (http://localhost:3000), how to change the port via environment variable, and a note that versioning will be introduced in Phase 2 + +### Requirement: API reference includes an errors section +The reference SHALL include a dedicated errors section listing all standard error response shapes, all custom error codes, and their HTTP status code mappings. + +#### Scenario: Developer handles an AgentNotFoundError +- **WHEN** the developer reads the errors section +- **THEN** they SHALL see the exact JSON shape of the error response, the error code string, and the HTTP status (404) diff --git a/openspec/changes/bedroom-developer-docs/specs/core-concepts/spec.md b/openspec/changes/bedroom-developer-docs/specs/core-concepts/spec.md new file mode 100644 index 0000000..e56eb82 --- /dev/null +++ b/openspec/changes/bedroom-developer-docs/specs/core-concepts/spec.md @@ -0,0 +1,43 @@ +## ADDED Requirements + +### Requirement: Core concepts guide exists at docs/developers/concepts.md +The system SHALL provide a concepts guide at `docs/developers/concepts.md` that explains the AgentIdP model in plain English with no assumed prior knowledge of AGNTCY or OAuth 2.0. + +#### Scenario: Developer understands what AgentIdP is +- **WHEN** a developer reads the concepts guide +- **THEN** they SHALL be able to explain in one sentence what SentryAgent.ai AgentIdP does and why they need it + +### Requirement: Concepts guide explains what an AI agent identity is +The guide SHALL explain in plain English what it means to give an AI agent an identity — how it differs from a human user account and why agents need their own identity model. + +#### Scenario: Agent identity vs human identity distinction is clear +- **WHEN** the developer reads the agent identity section +- **THEN** they SHALL understand that agents are non-human, machine-operated identities that need persistent, auditable credentials — not session-based logins + +### Requirement: Concepts guide explains the AGNTCY alignment +The guide SHALL explain what AGNTCY is (Linux Foundation standard), why SentryAgent.ai aligns to it, and what benefit that gives the developer — without requiring the developer to read the AGNTCY specification. + +#### Scenario: Developer understands AGNTCY without external reading +- **WHEN** the developer reads the AGNTCY section +- **THEN** they SHALL understand that AGNTCY-aligned agent IDs are interoperable across the AI agent ecosystem, and that SentryAgent.ai implements this for free + +### Requirement: Concepts guide explains the agent lifecycle +The guide SHALL explain the four lifecycle states of an agent (active, suspended, decommissioned) and what each state means for credential and token behaviour. + +#### Scenario: Developer understands what happens when an agent is decommissioned +- **WHEN** the developer reads the lifecycle section +- **THEN** they SHALL understand that decommissioning is irreversible, all credentials are revoked, and no new tokens can be issued + +### Requirement: Concepts guide explains OAuth 2.0 Client Credentials in plain English +The guide SHALL explain the Client Credentials grant in plain English — no RFC references, no formal OAuth jargon — focused on how agents use it to authenticate. + +#### Scenario: Developer understands client_id and client_secret without prior OAuth knowledge +- **WHEN** the developer reads the OAuth section +- **THEN** they SHALL understand that client_id identifies the agent and client_secret proves it — analogous to a username and password for machines + +### Requirement: Concepts guide explains the free-tier limits +The guide SHALL document all free-tier limits (100 agents, 10,000 tokens/month, 100 req/min, 90-day audit retention) in a clear table. + +#### Scenario: Developer knows the limits before hitting them +- **WHEN** the developer reads the free-tier section +- **THEN** they SHALL see a table with all four limits and a note on what happens when each is exceeded diff --git a/openspec/changes/bedroom-developer-docs/specs/developer-guides/spec.md b/openspec/changes/bedroom-developer-docs/specs/developer-guides/spec.md new file mode 100644 index 0000000..facacec --- /dev/null +++ b/openspec/changes/bedroom-developer-docs/specs/developer-guides/spec.md @@ -0,0 +1,56 @@ +## ADDED Requirements + +### Requirement: Developer guides index exists at docs/developers/guides/README.md +The system SHALL provide a guides index at `docs/developers/guides/README.md` listing all available guides with one-line descriptions and links. + +#### Scenario: Developer finds the right guide quickly +- **WHEN** the developer opens the guides folder +- **THEN** they SHALL see a list of all guides with descriptions so they can choose the one relevant to their task + +### Requirement: Agent registration guide exists at docs/developers/guides/register-an-agent.md +The system SHALL provide a step-by-step guide for registering an agent, including all required and optional fields, validation rules, and how to handle the response. + +#### Scenario: Developer registers their first agent +- **WHEN** the developer follows the registration guide +- **THEN** they SHALL successfully create an agent and understand what `agentId`, `clientId`, and `status` mean in the response + +#### Scenario: Developer understands registration validation errors +- **WHEN** the guide covers validation +- **THEN** it SHALL show examples of common validation errors (missing required fields, invalid email format) and how to fix them + +### Requirement: Credential management guide exists at docs/developers/guides/manage-credentials.md +The system SHALL provide a guide covering all four credential operations: generate, list, rotate, and revoke — with curl examples and explanation of when to use each. + +#### Scenario: Developer rotates a compromised credential +- **WHEN** the developer follows the rotation section +- **THEN** they SHALL understand that rotation replaces the secret while keeping the same `credentialId`, and the old secret is immediately invalid + +#### Scenario: Developer understands credential revocation vs agent decommission +- **WHEN** the developer reads the guide +- **THEN** they SHALL understand the difference: revoking a credential leaves the agent active with other credentials; decommissioning the agent revokes everything permanently + +### Requirement: Token guide exists at docs/developers/guides/issue-and-revoke-tokens.md +The system SHALL provide a guide covering token issuance, introspection, and revocation — explaining the JWT structure, expiry, and how to use the Bearer token in API requests. + +#### Scenario: Developer uses a token to authenticate a request +- **WHEN** the developer follows the token guide +- **THEN** they SHALL see an example of using the issued token as a Bearer token in an Authorization header on a subsequent API call + +#### Scenario: Developer introspects a token to check validity +- **WHEN** the developer reads the introspection section +- **THEN** they SHALL understand what `active: true/false` means and what fields are returned + +#### Scenario: Developer revokes a token +- **WHEN** the developer follows the revocation section +- **THEN** they SHALL understand that revoked tokens are immediately invalid even if not yet expired + +### Requirement: Audit log guide exists at docs/developers/guides/query-audit-logs.md +The system SHALL provide a guide for querying the audit log — covering available filters (agentId, action, outcome, date range), pagination, and how to interpret audit events. + +#### Scenario: Developer queries audit events for a specific agent +- **WHEN** the developer follows the audit guide +- **THEN** they SHALL see a curl example filtering by `agentId` and understand the structure of each audit event + +#### Scenario: Developer understands audit log retention +- **WHEN** the developer reads the guide +- **THEN** they SHALL understand that free-tier audit logs are retained for 90 days and what happens after that window diff --git a/openspec/changes/bedroom-developer-docs/specs/quick-start/spec.md b/openspec/changes/bedroom-developer-docs/specs/quick-start/spec.md new file mode 100644 index 0000000..613cb45 --- /dev/null +++ b/openspec/changes/bedroom-developer-docs/specs/quick-start/spec.md @@ -0,0 +1,45 @@ +## ADDED Requirements + +### Requirement: Quick-start guide exists at docs/developers/quick-start.md +The system SHALL provide a quick-start guide at `docs/developers/quick-start.md` that enables a bedroom developer to register their first agent and issue an OAuth 2.0 access token in under 5 minutes. + +#### Scenario: Developer completes quick-start from zero +- **WHEN** a developer with no prior AgentIdP knowledge follows the quick-start guide +- **THEN** they SHALL have a registered agent, a valid credential, and a working access token by the end + +### Requirement: Quick-start lists exact prerequisites +The quick-start guide SHALL list all prerequisites at the top before any steps, so the developer knows what they need before starting. + +#### Scenario: Prerequisites are minimal and explicit +- **WHEN** the developer reads the prerequisites section +- **THEN** they SHALL see exactly: Docker (for running PostgreSQL and Redis) and curl (for API calls) — nothing else required + +### Requirement: Quick-start provides a working docker-compose startup command +The quick-start guide SHALL include a single command to start the required infrastructure (PostgreSQL + Redis) using the project's `docker-compose.yml`. + +#### Scenario: Developer starts infrastructure +- **WHEN** the developer runs the provided docker-compose command +- **THEN** the guide SHALL confirm what services are started and what ports they run on + +### Requirement: Quick-start covers the full 4-step workflow +The quick-start guide SHALL cover exactly these four steps in order, each with a working curl command and the expected response: + +1. Start the AgentIdP server +2. Register an agent (`POST /agents`) +3. Generate a credential (`POST /agents/{agentId}/credentials`) +4. Issue an access token (`POST /token`) + +#### Scenario: Each step has a copy-pasteable curl command +- **WHEN** the developer reads any step +- **THEN** they SHALL find a complete curl command with real placeholder values they can substitute + +#### Scenario: Each step shows the expected JSON response +- **WHEN** the developer runs a curl command from the guide +- **THEN** the guide SHALL show them what a successful response looks like so they can verify their output + +### Requirement: Quick-start ends with a next-steps section +The quick-start guide SHALL end with a "What's Next" section linking to: core-concepts.md, developer-guides.md, and api-reference.md. + +#### Scenario: Developer knows where to go after quick-start +- **WHEN** the developer reaches the end of the quick-start +- **THEN** they SHALL see at least 3 links to deeper documentation diff --git a/openspec/changes/bedroom-developer-docs/tasks.md b/openspec/changes/bedroom-developer-docs/tasks.md new file mode 100644 index 0000000..3b4da72 --- /dev/null +++ b/openspec/changes/bedroom-developer-docs/tasks.md @@ -0,0 +1,50 @@ +## 1. Folder Structure & Setup + +- [x] 1.1 Create `docs/developers/` directory +- [x] 1.2 Create `docs/developers/guides/` subdirectory +- [x] 1.3 Create `docs/developers/README.md` — index page listing all docs with one-line descriptions and links + +## 2. Quick-Start Guide + +- [x] 2.1 Create `docs/developers/quick-start.md` — prerequisites section (Docker + curl only) +- [x] 2.2 Write Step 1: start infrastructure with docker-compose command + confirmation of services and ports +- [x] 2.3 Write Step 2: start AgentIdP server with npm command + expected startup output +- [x] 2.4 Write Step 3: register an agent — complete curl for `POST /agents` with example request body and expected JSON response +- [x] 2.5 Write Step 4: generate a credential — complete curl for `POST /agents/{agentId}/credentials` with example response showing `clientId` and `clientSecret` +- [x] 2.6 Write Step 5: issue an access token — complete curl for `POST /token` with form-encoded body and example JWT response +- [x] 2.7 Write "What's Next" section linking to concepts.md, guides/README.md, and api-reference.md + +## 3. Core Concepts Guide + +- [x] 3.1 Create `docs/developers/concepts.md` — intro section: what is AgentIdP in one paragraph +- [x] 3.2 Write "What is an AI Agent Identity" section — plain-English explanation of agent identities vs human identities +- [x] 3.3 Write "AGNTCY Alignment" section — what AGNTCY is, why it matters, benefit to the developer (no external reading required) +- [x] 3.4 Write "Agent Lifecycle" section — four states (active, suspended, decommissioned) and what each means for credentials and tokens, including irreversibility of decommission +- [x] 3.5 Write "OAuth 2.0 Client Credentials" section — plain-English explanation of client_id, client_secret, and how agents use them; no RFC jargon +- [x] 3.6 Write "Free Tier Limits" section — table of all four limits (100 agents, 10k tokens/month, 100 req/min, 90-day audit) with notes on what happens when each is exceeded + +## 4. Developer Guides + +- [x] 4.1 Create `docs/developers/guides/README.md` — index listing all four guides with descriptions and links +- [x] 4.2 Create `docs/developers/guides/register-an-agent.md` — step-by-step registration guide with all required/optional fields, validation rules, and example success + error responses (including common validation errors and fixes) +- [x] 4.3 Create `docs/developers/guides/manage-credentials.md` — guide covering all four credential operations: generate (with secret handling note), list (with pagination), rotate (explaining same credentialId, old secret immediately invalid), revoke (with comparison to agent decommission) +- [x] 4.4 Create `docs/developers/guides/issue-and-revoke-tokens.md` — token guide covering: issuance with form-encoded body, JWT structure explanation, using token as Bearer in subsequent requests, introspection (`active` field), revocation and immediate invalidation +- [x] 4.5 Create `docs/developers/guides/query-audit-logs.md` — audit log guide covering: available filters (agentId, action, outcome, date range), pagination params, audit event structure, 90-day retention behaviour + +## 5. API Reference + +- [x] 5.1 Create `docs/developers/api-reference.md` — top section: base URL, port config via env var, versioning note (Phase 1 unversioned) +- [x] 5.2 Write table of contents linking to all four service sections +- [x] 5.3 Write errors reference section: all error response shapes, all custom error codes (ValidationError, AgentNotFoundError, AgentAlreadyExistsError, CredentialError, AuthenticationError, AuthorizationError, RateLimitError, FreeTierLimitError), HTTP status mappings +- [x] 5.4 Document Agent Registry endpoints (5): `POST /agents`, `GET /agents`, `GET /agents/{agentId}`, `PATCH /agents/{agentId}`, `DELETE /agents/{agentId}` — each with method, path, auth requirement, request fields table, response codes table, and complete curl example +- [x] 5.5 Document OAuth 2.0 Token endpoints (3): `POST /token`, `POST /token/introspect`, `POST /token/revoke` — each with method, path, auth requirement, request fields table (noting form-encoded for /token), response codes table, curl example, and X-RateLimit header documentation for 429s +- [x] 5.6 Document Credential Management endpoints (4): `POST /agents/{agentId}/credentials`, `GET /agents/{agentId}/credentials`, `POST /agents/{agentId}/credentials/{credentialId}/rotate`, `DELETE /agents/{agentId}/credentials/{credentialId}` — each with method, path, auth requirement, request fields table, response codes table, and complete curl example +- [x] 5.7 Document Audit Log endpoints (2): `GET /audit`, `GET /audit/{eventId}` — each with method, path, auth requirement, query parameter table (including all filter options), response codes table, and complete curl example + +## 6. QA & Review + +- [x] 6.1 Verify all curl examples are syntactically correct and complete (no ellipses, no missing flags) +- [x] 6.2 Verify all 14 endpoints from the OpenAPI specs are covered in api-reference.md +- [x] 6.3 Verify all internal links (cross-references between docs) resolve correctly +- [x] 6.4 Verify free-tier limits in concepts.md match README.md Section 3.3 +- [x] 6.5 Verify quick-start guide is self-contained — a developer can complete it using only that file diff --git a/openspec/specs/agent-registry/spec.md b/openspec/specs/agent-registry/spec.md new file mode 100644 index 0000000..2b89091 --- /dev/null +++ b/openspec/specs/agent-registry/spec.md @@ -0,0 +1,86 @@ +## ADDED Requirements + +### Requirement: Register a new AI agent +The system SHALL create a new agent identity record with a system-assigned immutable UUID (`agentId`) when a valid `CreateAgentRequest` is received. The `email` field SHALL be unique across all agents. The agent SHALL be created with `status: active`. The system SHALL enforce a free-tier limit of 100 registered agents per account. + +#### Scenario: Successful agent registration +- **WHEN** a POST request to `/agents` is received with a valid `CreateAgentRequest` body and a valid Bearer token +- **THEN** the system creates the agent, assigns a UUID `agentId`, sets `status` to `active`, sets `createdAt` and `updatedAt` to the current timestamp, and returns `201` with the full `Agent` object + +#### Scenario: Duplicate email rejected +- **WHEN** a POST request to `/agents` is received with an `email` that is already registered +- **THEN** the system returns `409 Conflict` with `code: AGENT_ALREADY_EXISTS` + +#### Scenario: Free tier limit enforced +- **WHEN** a POST request to `/agents` is received and the account already has 100 registered agents +- **THEN** the system returns `403 Forbidden` with `code: FREE_TIER_LIMIT_EXCEEDED` and `details.limit: 100` + +#### Scenario: Invalid request body rejected +- **WHEN** a POST request to `/agents` is received with a missing required field or invalid field value (e.g. invalid semver, invalid email, invalid capability pattern) +- **THEN** the system returns `400 Bad Request` with `code: VALIDATION_ERROR` and `details` identifying the failing field + +### Requirement: Retrieve a single agent by ID +The system SHALL return the full `Agent` record for a given `agentId`. + +#### Scenario: Agent found +- **WHEN** a GET request to `/agents/{agentId}` is received with a valid Bearer token and a UUID that exists in the registry +- **THEN** the system returns `200 OK` with the full `Agent` object + +#### Scenario: Agent not found +- **WHEN** a GET request to `/agents/{agentId}` is received with a UUID that does not exist +- **THEN** the system returns `404 Not Found` with `code: AGENT_NOT_FOUND` + +### Requirement: List agents with pagination and filtering +The system SHALL return a paginated list of agents, orderd by `createdAt` descending, optionally filtered by `owner`, `agentType`, and/or `status`. + +#### Scenario: Successful paginated list +- **WHEN** a GET request to `/agents` is received with optional `page`, `limit`, `owner`, `agentType`, `status` query parameters and a valid Bearer token +- **THEN** the system returns `200 OK` with a `PaginatedAgentsResponse` containing `data`, `total`, `page`, and `limit` + +#### Scenario: Invalid pagination parameters rejected +- **WHEN** a GET request to `/agents` is received with `limit` greater than 100 or `page` less than 1 +- **THEN** the system returns `400 Bad Request` with `code: VALIDATION_ERROR` + +### Requirement: Update agent metadata +The system SHALL partially update a mutable agent record. `agentId`, `email`, and `createdAt` SHALL be immutable. Setting `status` to `decommissioned` SHALL be a one-way irreversible operation. + +#### Scenario: Successful partial update +- **WHEN** a PATCH request to `/agents/{agentId}` is received with a valid partial `UpdateAgentRequest` body and a valid Bearer token +- **THEN** the system updates only the provided fields, sets `updatedAt` to the current timestamp, and returns `200 OK` with the full updated `Agent` object + +#### Scenario: Attempt to modify immutable field rejected +- **WHEN** a PATCH request to `/agents/{agentId}` contains the `email` field +- **THEN** the system returns `400 Bad Request` with `code: IMMUTABLE_FIELD` and `details.field: email` + +#### Scenario: Decommissioned agent cannot be updated +- **WHEN** a PATCH request to `/agents/{agentId}` targets an agent with `status: decommissioned` +- **THEN** the system returns `403 Forbidden` with `code: AGENT_DECOMMISSIONED` + +### Requirement: Decommission (soft-delete) an agent +The system SHALL set an agent's `status` to `decommissioned` and revoke all of its active credentials. The agent record SHALL be retained for audit purposes. This operation SHALL be irreversible. + +#### Scenario: Successful decommission +- **WHEN** a DELETE request to `/agents/{agentId}` is received with a valid Bearer token and the agent exists and is not already decommissioned +- **THEN** the system sets `status` to `decommissioned`, revokes all active credentials for this agent, and returns `204 No Content` + +#### Scenario: Already decommissioned agent rejected +- **WHEN** a DELETE request to `/agents/{agentId}` is received for an agent that is already `decommissioned` +- **THEN** the system returns `409 Conflict` with `code: AGENT_ALREADY_DECOMMISSIONED` + +### Requirement: Authentication required on all agent endpoints +All agent endpoints SHALL require a valid Bearer JWT in the `Authorization` header. + +#### Scenario: Missing token rejected +- **WHEN** any request to `/agents` or `/agents/{agentId}` is received without an `Authorization: Bearer` header +- **THEN** the system returns `401 Unauthorized` with `code: UNAUTHORIZED` + +#### Scenario: Invalid token rejected +- **WHEN** any request to `/agents` or `/agents/{agentId}` is received with an expired, malformed, or revoked Bearer token +- **THEN** the system returns `401 Unauthorized` with `code: UNAUTHORIZED` + +### Requirement: Rate limiting on all agent endpoints +The system SHALL enforce a rate limit of 100 requests per minute per authenticated client. Rate limit state SHALL be tracked in Redis. + +#### Scenario: Rate limit exceeded +- **WHEN** a client sends more than 100 requests to any agent endpoint within a 60-second window +- **THEN** the system returns `429 Too Many Requests` with `X-RateLimit-Limit`, `X-RateLimit-Remaining: 0`, and `X-RateLimit-Reset` headers diff --git a/openspec/specs/audit-log/spec.md b/openspec/specs/audit-log/spec.md new file mode 100644 index 0000000..00ced41 --- /dev/null +++ b/openspec/specs/audit-log/spec.md @@ -0,0 +1,72 @@ +## ADDED Requirements + +### Requirement: Audit events are written internally for all significant actions +The system SHALL automatically create an immutable `AuditEvent` record for each of the following actions: `agent.created`, `agent.updated`, `agent.decommissioned`, `agent.suspended`, `agent.reactivated`, `token.issued`, `token.revoked`, `token.introspected`, `credential.generated`, `credential.rotated`, `credential.revoked`, `auth.failed`. No API endpoint SHALL allow external creation, modification, or deletion of audit records. + +#### Scenario: Audit event created on agent registration +- **WHEN** a new agent is successfully registered via `POST /agents` +- **THEN** an `AuditEvent` with `action: agent.created`, `outcome: success`, and `metadata` containing `agentType` and `owner` is persisted + +#### Scenario: Audit event created on failed authentication +- **WHEN** a `POST /token` request fails due to invalid credentials +- **THEN** an `AuditEvent` with `action: auth.failed`, `outcome: failure`, and `metadata` containing `reason` and `clientId` is persisted + +#### Scenario: Audit event created on token issuance +- **WHEN** a token is successfully issued via `POST /token` +- **THEN** an `AuditEvent` with `action: token.issued`, `outcome: success`, and `metadata` containing `scope` and `expiresAt` is persisted + +### Requirement: Query the audit log with pagination and filtering +The system SHALL return a paginated list of audit events ordered by `timestamp` descending. The caller SHALL hold a valid Bearer token with `audit:read` scope. Filtering SHALL support `agentId`, `action`, `outcome`, `fromDate`, and `toDate` — all optional, combined with logical AND. + +#### Scenario: Successful audit log query +- **WHEN** a GET request to `/audit` is received with a valid Bearer token with `audit:read` scope +- **THEN** the system returns `200 OK` with a `PaginatedAuditEventsResponse` containing `data`, `total`, `page`, and `limit` + +#### Scenario: Filter by agentId +- **WHEN** a GET request to `/audit?agentId={uuid}` is received +- **THEN** only events where `agentId` equals the provided UUID are returned + +#### Scenario: Filter by action +- **WHEN** a GET request to `/audit?action=token.issued` is received +- **THEN** only events with `action: token.issued` are returned + +#### Scenario: Filter by date range +- **WHEN** a GET request to `/audit?fromDate=2026-03-01T00:00:00.000Z&toDate=2026-03-28T23:59:59.999Z` is received +- **THEN** only events with `timestamp` within the specified range are returned + +#### Scenario: fromDate after toDate rejected +- **WHEN** a GET request to `/audit` is received with `fromDate` that is chronologically after `toDate` +- **THEN** the system returns `400 Bad Request` with `code: VALIDATION_ERROR` and `details.reason` explaining the invalid date range + +#### Scenario: Insufficient scope rejected +- **WHEN** a GET request to `/audit` is received with a valid Bearer token that does not have `audit:read` scope +- **THEN** the system returns `403 Forbidden` with `code: INSUFFICIENT_SCOPE` + +### Requirement: Retrieve a single audit event by ID +The system SHALL return a single immutable `AuditEvent` by its `eventId`. The caller SHALL hold a valid Bearer token with `audit:read` scope. + +#### Scenario: Audit event found +- **WHEN** a GET request to `/audit/{eventId}` is received with a valid Bearer token with `audit:read` scope and a UUID that exists in the audit log +- **THEN** the system returns `200 OK` with the full `AuditEvent` object + +#### Scenario: Audit event not found +- **WHEN** a GET request to `/audit/{eventId}` is received with a UUID that does not exist in the audit log +- **THEN** the system returns `404 Not Found` with `code: AUDIT_EVENT_NOT_FOUND` + +### Requirement: Free-tier 90-day audit log retention +On the free tier, the system SHALL only return audit events from the last 90 days. Events older than 90 days SHALL be treated as not accessible (return empty results for queries, `404` for direct lookups). The system SHALL return a `400` error with `code: RETENTION_WINDOW_EXCEEDED` if a `fromDate` query parameter falls outside the 90-day retention window. + +#### Scenario: Query outside retention window rejected +- **WHEN** a GET request to `/audit` is received with `fromDate` more than 90 days before today +- **THEN** the system returns `400 Bad Request` with `code: RETENTION_WINDOW_EXCEEDED` and `details.retentionDays: 90` + +#### Scenario: Direct lookup of expired event returns 404 +- **WHEN** a GET request to `/audit/{eventId}` is received for an event with a `timestamp` older than 90 days +- **THEN** the system returns `404 Not Found` with `code: AUDIT_EVENT_NOT_FOUND` + +### Requirement: Rate limiting on audit endpoints +The system SHALL enforce a rate limit of 100 requests per minute per authenticated client on all audit endpoints. + +#### Scenario: Rate limit exceeded on audit endpoint +- **WHEN** a client sends more than 100 requests to any audit endpoint within a 60-second window +- **THEN** the system returns `429 Too Many Requests` with `X-RateLimit-Limit`, `X-RateLimit-Remaining: 0`, and `X-RateLimit-Reset` headers diff --git a/openspec/specs/credential-management/spec.md b/openspec/specs/credential-management/spec.md new file mode 100644 index 0000000..1dc0cad --- /dev/null +++ b/openspec/specs/credential-management/spec.md @@ -0,0 +1,83 @@ +## ADDED Requirements + +### Requirement: Generate new credentials for an agent +The system SHALL generate a new `client_id`/`client_secret` pair for a specified agent. The `client_id` SHALL equal the agent's `agentId`. The `client_secret` SHALL be a cryptographically random string with the prefix `sk_live_` followed by 64 hex characters (256 bits of entropy). The plain-text secret SHALL be returned in the response exactly once and SHALL never be stored in plain text — only a bcrypt hash (10 rounds) SHALL be persisted. The agent MUST be in `active` status to generate credentials. + +#### Scenario: Successful credential generation +- **WHEN** a POST request to `/agents/{agentId}/credentials` is received with a valid Bearer token and the agent exists with `status: active` +- **THEN** the system generates a new credential, persists the bcrypt hash of the secret, and returns `201 Created` with a `CredentialWithSecret` response including the plain-text `clientSecret` + +#### Scenario: clientSecret not returned after creation +- **WHEN** a GET request to `/agents/{agentId}/credentials` is made after credential creation +- **THEN** the `clientSecret` field is NOT present in any `Credential` object in the response + +#### Scenario: Suspended agent cannot generate credentials +- **WHEN** a POST request to `/agents/{agentId}/credentials` is received for an agent with `status: suspended` +- **THEN** the system returns `403 Forbidden` with `code: AGENT_NOT_ACTIVE` + +#### Scenario: Decommissioned agent cannot generate credentials +- **WHEN** a POST request to `/agents/{agentId}/credentials` is received for an agent with `status: decommissioned` +- **THEN** the system returns `403 Forbidden` with `code: AGENT_NOT_ACTIVE` + +#### Scenario: Optional expiry respected +- **WHEN** a POST request to `/agents/{agentId}/credentials` is received with an `expiresAt` value that is a future date-time +- **THEN** the credential is created with the specified `expiresAt` value + +#### Scenario: Past expiry rejected +- **WHEN** a POST request to `/agents/{agentId}/credentials` is received with an `expiresAt` value that is in the past +- **THEN** the system returns `400 Bad Request` with `code: VALIDATION_ERROR` and `details.field: expiresAt` + +#### Scenario: Agent not found +- **WHEN** a POST request to `/agents/{agentId}/credentials` is received for a `agentId` that does not exist +- **THEN** the system returns `404 Not Found` with `code: AGENT_NOT_FOUND` + +### Requirement: List credentials for an agent +The system SHALL return a paginated list of all credentials (both `active` and `revoked`) for an agent, ordered by `createdAt` descending. The `clientSecret` SHALL never be included in list responses. + +#### Scenario: Successful credential list +- **WHEN** a GET request to `/agents/{agentId}/credentials` is received with optional `page`, `limit`, `status` query parameters and a valid Bearer token +- **THEN** the system returns `200 OK` with a `PaginatedCredentialsResponse` containing `data`, `total`, `page`, and `limit`, with no `clientSecret` fields + +#### Scenario: Filter by status +- **WHEN** a GET request to `/agents/{agentId}/credentials?status=active` is received +- **THEN** only credentials with `status: active` are returned + +### Requirement: Rotate a credential +The system SHALL rotate an existing active credential by generating a new `clientSecret` for the same `credentialId`. The previous secret SHALL be immediately invalidated. The new plain-text secret SHALL be returned once and never persisted. Only `active` credentials can be rotated. + +#### Scenario: Successful rotation +- **WHEN** a POST request to `/agents/{agentId}/credentials/{credentialId}/rotate` is received with a valid Bearer token and the credential exists with `status: active` +- **THEN** the system generates a new secret, replaces the stored bcrypt hash, and returns `200 OK` with a `CredentialWithSecret` response including the new plain-text `clientSecret`. The `credentialId` remains unchanged. + +#### Scenario: Revoked credential cannot be rotated +- **WHEN** a POST request to `/agents/{agentId}/credentials/{credentialId}/rotate` is received for a credential with `status: revoked` +- **THEN** the system returns `409 Conflict` with `code: CREDENTIAL_ALREADY_REVOKED` + +#### Scenario: Credential not found +- **WHEN** a POST request to `/agents/{agentId}/credentials/{credentialId}/rotate` is received with a `credentialId` that does not exist for the given agent +- **THEN** the system returns `404 Not Found` with `code: CREDENTIAL_NOT_FOUND` + +### Requirement: Revoke a credential +The system SHALL permanently revoke a credential by setting its `status` to `revoked` and recording a `revokedAt` timestamp. The credential record SHALL be retained for audit purposes. Revocation SHALL be irreversible. Tokens previously issued with this credential SHALL remain valid until their natural expiry (token revocation is handled separately via `POST /token/revoke`). Revoking an already-revoked credential SHALL return `409 Conflict`. + +#### Scenario: Successful revocation +- **WHEN** a DELETE request to `/agents/{agentId}/credentials/{credentialId}` is received with a valid Bearer token and the credential exists with `status: active` +- **THEN** the system sets `status` to `revoked`, sets `revokedAt` to the current timestamp, and returns `204 No Content` + +#### Scenario: Already-revoked credential rejected +- **WHEN** a DELETE request to `/agents/{agentId}/credentials/{credentialId}` is received for a credential that is already `revoked` +- **THEN** the system returns `409 Conflict` with `code: CREDENTIAL_ALREADY_REVOKED` + +### Requirement: Agent decommission cascades to credential revocation +When an agent is decommissioned via `DELETE /agents/{agentId}`, the system SHALL revoke all active credentials for that agent as part of the same operation. + +#### Scenario: All credentials revoked on agent decommission +- **WHEN** an agent is successfully decommissioned via `DELETE /agents/{agentId}` +- **THEN** all credentials for that agent with `status: active` are set to `status: revoked` with `revokedAt` = current timestamp + +### Requirement: Authentication required on all credential endpoints +All credential endpoints SHALL require a valid Bearer JWT. An agent MAY manage its own credentials using a self-issued token. Managing another agent's credentials SHALL return `403 Forbidden` unless the caller holds an admin-scoped token (admin scope is not implemented in Phase 1 — return `403` for all cross-agent requests). + +#### Scenario: Unauthenticated request rejected +- **WHEN** any request to `/agents/{agentId}/credentials` is received without a valid Bearer token +- **THEN** the system returns `401 Unauthorized` with `code: UNAUTHORIZED` diff --git a/openspec/specs/oauth2-token/spec.md b/openspec/specs/oauth2-token/spec.md new file mode 100644 index 0000000..331d15b --- /dev/null +++ b/openspec/specs/oauth2-token/spec.md @@ -0,0 +1,76 @@ +## ADDED Requirements + +### Requirement: Issue access token via Client Credentials grant +The system SHALL issue a signed RS256 JWT access token when an agent authenticates with a valid `client_id` (agentId) and `client_secret` using the OAuth 2.0 Client Credentials grant (RFC 6749 §4.4). The request body SHALL use `application/x-www-form-urlencoded` encoding. The response SHALL include `Cache-Control: no-store` and `Pragma: no-cache` headers. The system SHALL enforce a free-tier limit of 10,000 token requests per calendar month per client. + +#### Scenario: Successful token issuance +- **WHEN** a POST request to `/token` is received with `grant_type=client_credentials`, a valid `client_id`, and a valid `client_secret` for an `active` agent +- **THEN** the system verifies the credential, issues a signed JWT with `sub` = `agentId`, `scope` = requested (or default) scope, `exp` = now + 3600s, and returns `200 OK` with `TokenResponse` + +#### Scenario: Invalid client credentials rejected +- **WHEN** a POST request to `/token` is received with a `client_id` that does not exist or a `client_secret` that does not match +- **THEN** the system returns `401 Unauthorized` with `error: invalid_client` + +#### Scenario: Suspended agent cannot obtain tokens +- **WHEN** a POST request to `/token` is received for an agent with `status: suspended` +- **THEN** the system returns `403 Forbidden` with `error: unauthorized_client` and a description indicating the agent is suspended + +#### Scenario: Decommissioned agent cannot obtain tokens +- **WHEN** a POST request to `/token` is received for an agent with `status: decommissioned` +- **THEN** the system returns `403 Forbidden` with `error: unauthorized_client` + +#### Scenario: Unsupported grant type rejected +- **WHEN** a POST request to `/token` is received with a `grant_type` other than `client_credentials` +- **THEN** the system returns `400 Bad Request` with `error: unsupported_grant_type` + +#### Scenario: Invalid scope rejected +- **WHEN** a POST request to `/token` is received with a `scope` value that contains an unrecognised scope identifier +- **THEN** the system returns `400 Bad Request` with `error: invalid_scope` + +#### Scenario: Free tier monthly token limit enforced +- **WHEN** a POST request to `/token` is received and the agent has already made 10,000 token requests in the current calendar month +- **THEN** the system returns `403 Forbidden` with `error: unauthorized_client` and a description indicating the monthly free-tier limit is reached + +### Requirement: Token introspection (RFC 7662) +The system SHALL determine whether a given access token is currently active (valid, not expired, not revoked). The endpoint SHALL return `200 OK` for both active and inactive tokens — the `active` field in the response SHALL indicate validity. The caller SHALL hold a valid Bearer token with `tokens:read` scope. + +#### Scenario: Active token introspection +- **WHEN** a POST request to `/token/introspect` is received with a valid, non-expired, non-revoked token and the caller has `tokens:read` scope +- **THEN** the system returns `200 OK` with `active: true` and the token's claims (`sub`, `client_id`, `scope`, `token_type`, `iat`, `exp`) + +#### Scenario: Expired or revoked token introspection +- **WHEN** a POST request to `/token/introspect` is received with a token that is expired or has been revoked +- **THEN** the system returns `200 OK` with `active: false` and no other claims + +#### Scenario: Insufficient scope for introspection +- **WHEN** a POST request to `/token/introspect` is received with a valid Bearer token that does not have `tokens:read` scope +- **THEN** the system returns `403 Forbidden` with `code: INSUFFICIENT_SCOPE` + +### Requirement: Token revocation (RFC 7009) +The system SHALL invalidate a given access token immediately. Revoking an already-revoked or expired token SHALL be a successful, idempotent operation (RFC 7009 §2.1). Revoked token JTIs SHALL be stored in Redis with TTL equal to the token's remaining lifetime. + +#### Scenario: Successful token revocation +- **WHEN** a POST request to `/token/revoke` is received with a valid Bearer token and a `token` parameter containing a valid JWT +- **THEN** the system adds the token's JTI to the Redis revocation list, and returns `200 OK` with an empty body + +#### Scenario: Revocation of already-revoked token is idempotent +- **WHEN** a POST request to `/token/revoke` is received with a token that is already in the Redis revocation list +- **THEN** the system returns `200 OK` with an empty body (no error) + +#### Scenario: Missing token parameter rejected +- **WHEN** a POST request to `/token/revoke` is received with no `token` field in the body +- **THEN** the system returns `400 Bad Request` with `code: VALIDATION_ERROR` + +### Requirement: JWT claims structure +All issued JWTs SHALL contain the following claims: `sub` (agentId), `client_id` (agentId), `scope` (space-separated granted scopes), `jti` (UUID, unique per token), `iat` (issued-at Unix timestamp), `exp` (expiry Unix timestamp). Tokens SHALL be signed with RS256. + +#### Scenario: JWT contains required claims +- **WHEN** a token is issued via `POST /token` +- **THEN** the decoded JWT payload contains `sub`, `client_id`, `scope`, `jti`, `iat`, and `exp` fields + +### Requirement: Rate limiting on token endpoints +The system SHALL enforce a rate limit of 100 requests per minute per `client_id` on all token endpoints. + +#### Scenario: Rate limit exceeded on token endpoint +- **WHEN** a client sends more than 100 requests to any token endpoint within a 60-second window +- **THEN** the system returns `429 Too Many Requests` with `X-RateLimit-Limit`, `X-RateLimit-Remaining: 0`, and `X-RateLimit-Reset` headers