Scaffolds the phase-3-enterprise OpenSpec change (proposal only — awaiting CEO approval before implementation). 6 workstreams, 95 implementation tasks: WS1: Multi-Tenancy (21 tasks) — org model, RLS, admin API WS2: W3C DIDs (12 tasks) — DID:WEB, agent DID documents, AGNTCY cards WS3: OIDC (12 tasks) — oidc-provider, ID tokens, JWKS, discovery WS4: Federation (11 tasks) — cross-instance trust, JWT assertions WS5: Webhooks (17 tasks) — subscriptions, Bull queue, HMAC, retry WS6: SOC2 (22 tasks) — encryption at rest, Merkle audit chain, controls Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
354 lines
11 KiB
Markdown
354 lines
11 KiB
Markdown
# W3C Decentralized Identifiers (DIDs) — Specification
|
|
|
|
**Workstream**: 2 of 6
|
|
**Phase**: 3 — Enterprise
|
|
**Author**: Virtual Architect
|
|
**Date**: 2026-03-29
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
Issue a W3C `did:web` identifier for every registered agent and serve DID Documents over HTTPS. The AgentIdP instance itself has a root DID Document at `/.well-known/did.json`. Each agent has an individual DID Document at `/agents/:id/did`. A DID resolution endpoint wraps the standard resolution workflow. Agent cards in AGNTCY format are derivable from DID Documents.
|
|
|
|
The `did:web` method resolves to `https://<host>/.well-known/did.json` (instance) and `https://<host>/agents/<agentId>/did` (per-agent). All DID Documents are W3C DID Core 1.0 compliant.
|
|
|
|
---
|
|
|
|
## API Endpoints
|
|
|
|
### GET /.well-known/did.json
|
|
|
|
Root DID Document for the AgentIdP instance. No authentication required — this is a public discovery endpoint.
|
|
|
|
```yaml
|
|
GET /.well-known/did.json
|
|
No authentication required
|
|
|
|
Responses:
|
|
200 OK:
|
|
Content-Type: application/json
|
|
schema:
|
|
type: object
|
|
description: W3C DID Core 1.0 compliant DID Document
|
|
required: [id, "@context", verificationMethod, authentication]
|
|
properties:
|
|
"@context":
|
|
type: array
|
|
items:
|
|
type: string
|
|
example:
|
|
- "https://www.w3.org/ns/did/v1"
|
|
- "https://w3id.org/security/suites/jws-2020/v1"
|
|
id:
|
|
type: string
|
|
description: DID for this AgentIdP instance
|
|
example: "did:web:idp.sentryagent.ai"
|
|
controller:
|
|
type: string
|
|
example: "did:web:idp.sentryagent.ai"
|
|
verificationMethod:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/VerificationMethod'
|
|
authentication:
|
|
type: array
|
|
items:
|
|
type: string
|
|
description: References to verification methods for authentication
|
|
assertionMethod:
|
|
type: array
|
|
items:
|
|
type: string
|
|
service:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/DIDService'
|
|
example:
|
|
"@context":
|
|
- "https://www.w3.org/ns/did/v1"
|
|
id: "did:web:idp.sentryagent.ai"
|
|
controller: "did:web:idp.sentryagent.ai"
|
|
verificationMethod:
|
|
- id: "did:web:idp.sentryagent.ai#key-1"
|
|
type: "JsonWebKey2020"
|
|
controller: "did:web:idp.sentryagent.ai"
|
|
publicKeyJwk:
|
|
kty: "EC"
|
|
crv: "P-256"
|
|
x: "f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU"
|
|
y: "x_FEzRu9m36HLN_tue659LNpXW6pCyStikYjKIWI5a0"
|
|
authentication:
|
|
- "did:web:idp.sentryagent.ai#key-1"
|
|
service:
|
|
- id: "did:web:idp.sentryagent.ai#agent-registry"
|
|
type: "AgentIdentityProvider"
|
|
serviceEndpoint: "https://idp.sentryagent.ai"
|
|
500 Internal Server Error:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
```
|
|
|
|
---
|
|
|
|
### GET /agents/:id/did
|
|
|
|
Per-agent DID Document. No authentication required — DID Documents are public.
|
|
|
|
```yaml
|
|
GET /agents/{agentId}/did
|
|
No authentication required
|
|
|
|
Path Parameters:
|
|
agentId:
|
|
type: string
|
|
description: Agent ID
|
|
|
|
Responses:
|
|
200 OK:
|
|
Content-Type: application/json
|
|
schema:
|
|
type: object
|
|
description: W3C DID Core 1.0 compliant per-agent DID Document
|
|
example:
|
|
"@context":
|
|
- "https://www.w3.org/ns/did/v1"
|
|
- "https://w3id.org/agntcy/v1"
|
|
id: "did:web:idp.sentryagent.ai:agents:agt_01HXK7Z9P3FKWABCDEF67890"
|
|
controller: "did:web:idp.sentryagent.ai"
|
|
verificationMethod:
|
|
- id: "did:web:idp.sentryagent.ai:agents:agt_01HXK7Z9P3FKWABCDEF67890#key-1"
|
|
type: "JsonWebKey2020"
|
|
controller: "did:web:idp.sentryagent.ai:agents:agt_01HXK7Z9P3FKWABCDEF67890"
|
|
publicKeyJwk:
|
|
kty: "EC"
|
|
crv: "P-256"
|
|
x: "abc123"
|
|
y: "def456"
|
|
authentication:
|
|
- "did:web:idp.sentryagent.ai:agents:agt_01HXK7Z9P3FKWABCDEF67890#key-1"
|
|
service:
|
|
- id: "did:web:idp.sentryagent.ai:agents:agt_01HXK7Z9P3FKWABCDEF67890#agent-card"
|
|
type: "AgentCard"
|
|
serviceEndpoint: "https://idp.sentryagent.ai/agents/agt_01HXK7Z9P3FKWABCDEF67890/did/card"
|
|
agntcy:
|
|
agentId: "agt_01HXK7Z9P3FKWABCDEF67890"
|
|
agentType: "orchestrator"
|
|
capabilities:
|
|
- "task-planning"
|
|
- "tool-use"
|
|
deploymentEnv: "production"
|
|
owner: "acme-ai"
|
|
version: "1.2.0"
|
|
404 Not Found:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
example:
|
|
code: "AGENT_NOT_FOUND"
|
|
message: "Agent not found"
|
|
410 Gone:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
example:
|
|
code: "AGENT_DECOMMISSIONED"
|
|
message: "Agent has been decommissioned — DID Document is no longer active"
|
|
```
|
|
|
|
---
|
|
|
|
### GET /agents/:id/did/resolve
|
|
|
|
DID resolution endpoint: resolves any `did:web` DID and returns the DID resolution result in W3C DID Resolution format. This enables external systems to use AgentIdP as a resolver for agent DIDs. Authentication required (`agents:read` scope).
|
|
|
|
```yaml
|
|
GET /agents/{agentId}/did/resolve
|
|
Authorization: Bearer <token with agents:read scope>
|
|
|
|
Path Parameters:
|
|
agentId:
|
|
type: string
|
|
|
|
Responses:
|
|
200 OK:
|
|
Content-Type: application/ld+json;profile="https://w3id.org/did-resolution"
|
|
schema:
|
|
type: object
|
|
required: [didDocument, didDocumentMetadata, didResolutionMetadata]
|
|
properties:
|
|
didDocument:
|
|
type: object
|
|
description: The resolved DID Document
|
|
didDocumentMetadata:
|
|
type: object
|
|
properties:
|
|
created:
|
|
type: string
|
|
format: date-time
|
|
updated:
|
|
type: string
|
|
format: date-time
|
|
deactivated:
|
|
type: boolean
|
|
didResolutionMetadata:
|
|
type: object
|
|
properties:
|
|
contentType:
|
|
type: string
|
|
example: "application/did+ld+json"
|
|
retrieved:
|
|
type: string
|
|
format: date-time
|
|
example:
|
|
didDocument:
|
|
"@context": ["https://www.w3.org/ns/did/v1"]
|
|
id: "did:web:idp.sentryagent.ai:agents:agt_01HXK7Z9P3FKWABCDEF67890"
|
|
didDocumentMetadata:
|
|
created: "2026-03-29T12:00:00Z"
|
|
updated: "2026-03-29T12:00:00Z"
|
|
deactivated: false
|
|
didResolutionMetadata:
|
|
contentType: "application/did+ld+json"
|
|
retrieved: "2026-03-29T14:00:00Z"
|
|
401 Unauthorized:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
404 Not Found:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
```
|
|
|
|
---
|
|
|
|
### GET /agents/:id/did/card
|
|
|
|
AGNTCY-format agent card derived from DID Document. Returns a JSON object representing the agent's identity and capabilities in the AGNTCY agent card format. No authentication required.
|
|
|
|
```yaml
|
|
GET /agents/{agentId}/did/card
|
|
No authentication required
|
|
|
|
Responses:
|
|
200 OK:
|
|
Content-Type: application/json
|
|
schema:
|
|
type: object
|
|
description: AGNTCY-format agent card
|
|
properties:
|
|
did:
|
|
type: string
|
|
name:
|
|
type: string
|
|
agentType:
|
|
type: string
|
|
capabilities:
|
|
type: array
|
|
items:
|
|
type: string
|
|
owner:
|
|
type: string
|
|
version:
|
|
type: string
|
|
deploymentEnv:
|
|
type: string
|
|
identityProvider:
|
|
type: string
|
|
description: DID of the issuing AgentIdP instance
|
|
issuedAt:
|
|
type: string
|
|
format: date-time
|
|
example:
|
|
did: "did:web:idp.sentryagent.ai:agents:agt_01HXK7Z9P3FKWABCDEF67890"
|
|
name: "acme-orchestrator"
|
|
agentType: "orchestrator"
|
|
capabilities: ["task-planning", "tool-use"]
|
|
owner: "acme-ai"
|
|
version: "1.2.0"
|
|
deploymentEnv: "production"
|
|
identityProvider: "did:web:idp.sentryagent.ai"
|
|
issuedAt: "2026-03-29T12:00:00Z"
|
|
404 Not Found:
|
|
schema:
|
|
$ref: '#/components/schemas/ErrorResponse'
|
|
```
|
|
|
|
---
|
|
|
|
## Database Schema Changes
|
|
|
|
### New Table: agent_did_keys
|
|
|
|
Stores the public/private key pair used to sign each agent's DID Document. The private key is stored in Vault; only the public key JWK is stored in PostgreSQL.
|
|
|
|
```sql
|
|
CREATE TABLE agent_did_keys (
|
|
key_id VARCHAR(40) PRIMARY KEY,
|
|
agent_id VARCHAR(40) NOT NULL UNIQUE REFERENCES agents(agent_id),
|
|
organization_id VARCHAR(40) NOT NULL REFERENCES organizations(organization_id),
|
|
public_key_jwk JSONB NOT NULL,
|
|
vault_key_path VARCHAR(255) NOT NULL, -- Vault path where private key is stored
|
|
key_type VARCHAR(20) NOT NULL DEFAULT 'EC',
|
|
curve VARCHAR(10) NOT NULL DEFAULT 'P-256',
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
rotated_at TIMESTAMPTZ,
|
|
CONSTRAINT agent_did_keys_key_type_check CHECK (key_type IN ('EC', 'RSA'))
|
|
);
|
|
|
|
CREATE INDEX idx_agent_did_keys_agent_id ON agent_did_keys(agent_id);
|
|
CREATE INDEX idx_agent_did_keys_org_id ON agent_did_keys(organization_id);
|
|
```
|
|
|
|
### New Column: agents.did
|
|
|
|
```sql
|
|
ALTER TABLE agents
|
|
ADD COLUMN did VARCHAR(255),
|
|
ADD COLUMN did_created_at TIMESTAMPTZ;
|
|
|
|
-- Populated automatically on agent creation
|
|
-- Example value: "did:web:idp.sentryagent.ai:agents:agt_01HXK7Z9P3FKWABCDEF67890"
|
|
```
|
|
|
|
---
|
|
|
|
## Configuration
|
|
|
|
| Environment Variable | Description | Default |
|
|
|---------------------|-------------|---------|
|
|
| `DID_WEB_DOMAIN` | Domain name for `did:web` construction | Required — derived from `HOST` if not set |
|
|
| `DID_KEY_TYPE` | Cryptographic key type for DID keys | `EC` |
|
|
| `DID_KEY_CURVE` | Elliptic curve for EC keys | `P-256` |
|
|
| `DID_DOCUMENT_CACHE_TTL_SECONDS` | How long to cache DID Documents in Redis | `300` |
|
|
|
|
---
|
|
|
|
## Dependencies
|
|
|
|
| Package | Version | Purpose |
|
|
|---------|---------|---------|
|
|
| `did-resolver` | `^4.1.0` | W3C DID resolution interface |
|
|
| `web-did-resolver` | `^2.0.27` | DID:WEB method resolver |
|
|
|
|
---
|
|
|
|
## Security Considerations
|
|
|
|
- DID Documents are public endpoints — no authentication, no rate-limit-sensitive data exposed
|
|
- Private keys for DID signing are stored in Vault; never written to PostgreSQL
|
|
- DID Document cache in Redis has a TTL — stale documents are evicted automatically
|
|
- Decommissioned agents return HTTP 410 Gone with `deactivated: true` in DID Document metadata
|
|
- DID rotation: when a credential is rotated, the DID Document key can optionally be rotated; the old key is retained in history
|
|
- `GET /agents/:id/did/card` exposes only data already present in the agent registration — no new sensitive fields
|
|
|
|
---
|
|
|
|
## Acceptance Criteria
|
|
|
|
- [ ] Every new agent registration automatically generates a `did:web` DID and key pair
|
|
- [ ] Root DID Document at `/.well-known/did.json` is W3C DID Core 1.0 compliant (validated by `did-resolver`)
|
|
- [ ] Per-agent DID Document returns correct `did:web` identifier and public key JWK
|
|
- [ ] DID resolution endpoint returns W3C DID Resolution format
|
|
- [ ] Decommissioned agent DID Document returns 410 Gone with `deactivated: true`
|
|
- [ ] Agent card at `/agents/:id/did/card` matches AGNTCY agent card format
|
|
- [ ] Private keys never appear in any API response or log
|
|
- [ ] TypeScript strict, zero `any`, >80% test coverage on DIDService
|