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

View File

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

View File

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

View File

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

View File

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