Files
sentryagent-idp/openspec/changes/phase-1-mvp-implementation/specs/credential-management/spec.md
SentryAgent.ai Developer d3530285b9 feat: Phase 1 MVP — complete AgentIdP implementation
Implements all P0 features per OpenSpec change phase-1-mvp-implementation:
- Agent Registry Service (CRUD) — full lifecycle management
- OAuth 2.0 Token Service (Client Credentials flow)
- Credential Management (generate, rotate, revoke)
- Immutable Audit Log Service

Tech: Node.js 18+, TypeScript 5.3+ strict, Express 4.18+, PostgreSQL 14+, Redis 7+
Standards: OpenAPI 3.0 specs, DRY/SOLID, zero `any` types
Quality: 18 unit test suites, 244 tests passing, 97%+ coverage
OpenAPI: 4 complete specs (14 endpoints total)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 09:14:41 +00:00

6.6 KiB

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