docs: bedroom developer documentation — complete docs/developers/ set

Adds the full bedroom-developer-docs OpenSpec change implementation:

- docs/developers/README.md — index page
- docs/developers/quick-start.md — bootstrap to working token in 7 steps
- docs/developers/concepts.md — AgentIdP, AGNTCY, lifecycle, OAuth 2.0, free tier
- docs/developers/guides/README.md — guide index
- docs/developers/guides/register-an-agent.md — all fields, validation, common errors
- docs/developers/guides/manage-credentials.md — generate, list, rotate, revoke
- docs/developers/guides/issue-and-revoke-tokens.md — OAuth 2.0 flow, introspect, revoke
- docs/developers/guides/query-audit-logs.md — filters, pagination, 90-day retention
- docs/developers/api-reference.md — all 14 endpoints, all error codes, curl examples

Also commits deferred OpenSpec housekeeping from previous session:
- Archives phase-1-mvp-implementation change to openspec/changes/archive/
- Adds bedroom-developer-docs change artifacts (30/30 tasks complete)
- Syncs 4 delta specs to openspec/specs/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
SentryAgent.ai Developer
2026-03-28 14:13:03 +00:00
parent d3530285b9
commit 61ea975c79
29 changed files with 2397 additions and 0 deletions

42
docs/developers/README.md Normal file
View File

@@ -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 |

View File

@@ -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 <access_token>
```
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, 1128 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 .
```

128
docs/developers/concepts.md Normal file
View File

@@ -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 <token>` 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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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 <token>` 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"
}
}
```

View File

@@ -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 <token>` 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. 1128 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.

View File

@@ -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="<paste token here>"
```
> 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 <token>` 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