feat(phase-4): WS4 — Agent Marketplace (public registry, pagination, filters)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
SentryAgent.ai Developer
2026-04-02 10:17:51 +00:00
parent d1e6af25aa
commit 89c99b666d
12 changed files with 417 additions and 1 deletions

View File

@@ -50,6 +50,7 @@ interface AgentWithDIDKeyRow {
key_type: string | null;
curve: string | null;
key_created_at: Date | null;
is_public: boolean | null;
}
/** Result of the internal agent+key lookup. */
@@ -455,6 +456,7 @@ export class DIDService {
updatedAt: row.updated_at,
did: row.did ?? undefined,
didCreatedAt: row.did_created_at ?? undefined,
isPublic: row.is_public ?? false,
};
return {

View File

@@ -0,0 +1,106 @@
/**
* Marketplace Service for SentryAgent.ai AgentIdP.
* Business logic for the public Agent Marketplace.
* Only exposes agents where is_public = true AND status = active.
*/
import { AgentRepository } from '../repositories/AgentRepository.js';
import { AgentNotFoundError } from '../utils/errors.js';
import {
IMarketplaceFilters,
IMarketplaceAgentCard,
IPaginatedMarketplaceResponse,
IMinimalDIDDocument,
IAgent,
} from '../types/index.js';
/**
* Builds a minimal W3C DID Document from agent data when no key material is present.
* Used as a fallback when the agent has a DID assigned but no key record exists yet,
* or when constructing the document purely from agent identity data.
*
* @param agent - The agent record.
* @returns A minimal DID document, or null if the agent has no DID.
*/
function buildMinimalDIDDocument(agent: IAgent): IMinimalDIDDocument | null {
if (!agent.did) {
return null;
}
return {
'@context': [
'https://www.w3.org/ns/did/v1',
'https://w3id.org/security/suites/jws-2020/v1',
],
id: agent.did,
controller: agent.did,
verificationMethod: [],
authentication: [],
};
}
/**
* Maps an IAgent record to the public IMarketplaceAgentCard shape.
* Strips all private fields (email, organizationId, internal status details).
*
* @param agent - The internal agent record.
* @returns The public-facing marketplace agent card.
*/
function mapAgentToCard(agent: IAgent): IMarketplaceAgentCard {
return {
agentId: agent.agentId,
agentType: agent.agentType,
version: agent.version,
capabilities: agent.capabilities,
owner: agent.owner,
deploymentEnv: agent.deploymentEnv,
did: agent.did ?? null,
didDocument: buildMinimalDIDDocument(agent),
publishedAt: agent.updatedAt,
};
}
/**
* Service for the public Agent Marketplace.
* All methods enforce the is_public = true constraint via the repository.
*/
export class MarketplaceService {
/**
* @param agentRepository - The agent data repository.
*/
constructor(private readonly agentRepository: AgentRepository) {}
/**
* Returns a paginated, optionally filtered list of public agents.
* Supports free-text search (?q=), capability filter (?capability=),
* and publisher filter (?publisher=).
*
* @param filters - Marketplace filter and pagination criteria.
* @returns Paginated public agent card listing.
*/
async listPublicAgents(filters: IMarketplaceFilters): Promise<IPaginatedMarketplaceResponse> {
const { agents, total } = await this.agentRepository.findPublicAgents(filters);
return {
data: agents.map(mapAgentToCard),
total,
page: filters.page,
limit: filters.limit,
};
}
/**
* Returns a single public agent with its DID document and agent card.
* Only succeeds when the agent is both public (is_public = true) and active.
*
* @param agentId - The agent UUID to retrieve.
* @returns The public marketplace agent card.
* @throws AgentNotFoundError if the agent does not exist, is private, or is not active.
*/
async getPublicAgent(agentId: string): Promise<IMarketplaceAgentCard> {
const agent = await this.agentRepository.findPublicById(agentId);
if (!agent) {
throw new AgentNotFoundError(agentId);
}
return mapAgentToCard(agent);
}
}