The AI trust layer between agents: identity, agreements, safeguards, and outcome intelligence for A2A.
Commit 3f140fdd
3f140fdd2001Merged
aryaamin <[email protected]> · 2 months ago · 13 files · +1 added · ~9 modified · -3 deleted
Description
Simplify Agent Card onboarding
Details
- Commit
- 3f140fdd2001
- Parent
- 8a15fe87b720
apps/api/src/app.tsmodifiedView file
@@ -13,6 +13,7 @@
1313
LiveSessionEventSchema,1414
InteractionCompletionReportSchema,1515
InteractionFeedbackSchema,16+
type PublicAgentCard,1617
} from '../../../packages/protocol/src/index.js';1718
import {1819
MemoryAuditStore,@@ -30,11 +31,6 @@
3031
rejectAgentSetup,3132
} from '../../../packages/persistence/src/onboarding.js';3233
import { FixedWindowRateLimiter } from './security.js';33-
import {34-
buildOwnerCompletionReport,35-
buildOwnerFeedback,36-
buildQuickstartInteraction,37-
} from './quickstart.js';3834
3935
type DashboardRepository = Pick<4036
HostedRepository,@@ -226,6 +222,15 @@
226222
const protocol = typeof forwardedProtocol === 'string' ? forwardedProtocol : 'https';227223
return process.env.OPENCLASP_PUBLIC_URL ?? `${protocol}://${String(host ?? 'localhost')}`;228224
};225+
const publicPublication = (card: PublicAgentCard) => ({226+
agentId: card.agentId,227+
published: true,228+
profileUrl: card.profileUrl,229+
cardUrl: card.cardUrl,230+
a2aAgentCardUrl: card.a2aAgentCardUrl,231+
verification: card.verification,232+
updatedAt: card.updatedAt,233+
});229234
const escapeHtml = (value: string) =>230235
value.replace(/[&<>"']/g, (character) => {231236
const entities: Record<string, string> = {@@ -396,9 +401,9 @@
396401
return reply.type('text/html; charset=utf-8').send(`<!doctype html>397402
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">398403
<title>${escapeHtml(card.name)} · OpenClasp</title><meta name="description" content="${escapeHtml(card.description)}">399-
<meta property="og:title" content="${escapeHtml(card.name)} · OpenClasp verified agent"><meta property="og:description" content="${escapeHtml(card.description)}">400-
<style>color-scheme:dark;*{box-sizing:border-box}body{margin:0;background:#0c0a0a;color:#f6f1ee;font:15px/1.55 Inter,system-ui,sans-serif}main{max-width:760px;margin:0 auto;padding:64px 24px}.brand{color:#f04b2d;font-weight:700}.verified{display:inline-block;margin:24px 0 8px;padding:5px 9px;border:1px solid #2fbf71;color:#2fbf71;border-radius:999px;font-size:12px}h1{font-size:42px;line-height:1.05;margin:8px 0 12px}p{color:#a39a94}.grid{display:grid;grid-template-columns:1fr 1fr;gap:32px;margin:36px 0}h2{font-size:13px;text-transform:uppercase;letter-spacing:.08em;color:#a39a94}a{color:#f04b2d;overflow-wrap:anywhere}.links{padding-top:24px;border-top:1px solid #2b2523}.status{color:${card.presence?.status === 'online' ? '#2fbf71' : '#a39a94'}}@media(max-width:600px){.grid{grid-template-columns:1fr}h1{font-size:34px}}</style>401-
<script type="application/ld+json">${structured}</script></head><body><main><div class="brand">OpenClasp</div><div class="verified">✓ Account and agent ownership verified</div>404+
<meta property="og:title" content="${escapeHtml(card.name)} · OpenClasp agent profile"><meta property="og:description" content="${escapeHtml(card.description)}">405+
<style>color-scheme:dark;*{box-sizing:border-box}body{margin:0;background:#0c0a0a;color:#f6f1ee;font:15px/1.55 Inter,system-ui,sans-serif}main{max-width:760px;margin:0 auto;padding:64px 24px}.brand{color:#f04b2d;font-weight:700}.verified{display:inline-block;margin:24px 0 8px;padding:5px 9px;border:1px solid #2fbf71;color:#2fbf71;border-radius:999px;font-size:12px}.verification-note{margin:0 0 24px;font-size:12px}h1{font-size:42px;line-height:1.05;margin:8px 0 12px}p{color:#a39a94}.grid{display:grid;grid-template-columns:1fr 1fr;gap:32px;margin:36px 0}h2{font-size:13px;text-transform:uppercase;letter-spacing:.08em;color:#a39a94}a{color:#f04b2d;overflow-wrap:anywhere}.links{padding-top:24px;border-top:1px solid #2b2523}.status{color:${card.presence?.status === 'online' ? '#2fbf71' : '#a39a94'}}@media(max-width:600px){.grid{grid-template-columns:1fr}h1{font-size:34px}}</style>406+
<script type="application/ld+json">${structured}</script></head><body><main><div class="brand">OpenClasp</div><div class="verified">✓ Publisher verified</div><p class="verification-note">OpenClasp verified control of the publishing account. Capabilities are self-declared.</p>402407
<h1>${escapeHtml(card.name)}</h1><p>${escapeHtml(card.description || 'No description provided.')}</p><p class="status">${escapeHtml(card.presence?.status ?? 'offline')} · ${escapeHtml(card.agentMode.replace('_', ' '))}</p>403408
<div class="grid"><section><h2>Capabilities</h2><ul>${capabilities || '<li>None published</li>'}</ul></section><section><h2>Limitations</h2><ul>${limitations || '<li>None published</li>'}</ul></section></div>404409
<div class="links"><p><strong>Agent ID</strong><br>${escapeHtml(card.agentId)}</p><p><a href="${escapeHtml(card.cardUrl)}">OpenClasp card</a> · <a href="${escapeHtml(card.a2aAgentCardUrl)}">A2A Agent Card</a></p></div></main></body></html>`);@@ -560,11 +565,7 @@
560565
owner,561566
buildPublicAgentCard(profile, publicBaseUrl(request), previous),562567
);563-
await repository.upsert(owner, 'publication', agentId, {564-
agentId,565-
published: true,566-
updatedAt: card.updatedAt,567-
});568+
await repository.upsert(owner, 'publication', agentId, publicPublication(card));568569
return { profile, card };569570
}570571
return { profile };@@ -582,11 +583,12 @@
582583
owner,583584
buildPublicAgentCard(result.agent, publicBaseUrl(request), previous),584585
);585-
await repository.upsert(owner, 'publication', result.agent.agentId, {586-
agentId: result.agent.agentId,587-
published: true,588-
updatedAt: card.updatedAt,589-
});586+
await repository.upsert(587+
owner,588+
'publication',589+
result.agent.agentId,590+
publicPublication(card),591+
);590592
}591593
return result;592594
});@@ -644,22 +646,14 @@
644646
agentName: z.string().trim().min(1).max(100),645647
projectName: z.string().trim().min(1).max(100).default('My agents'),646648
description: z.string().trim().min(1).max(500),649+
framework: z.string().trim().min(1).max(100).optional(),647650
capabilities: z.array(z.string().trim().min(1).max(100)).min(1).max(20),648651
limitations: z.array(z.string().trim().min(1).max(300)).max(20).default([]),649652
})650653
.strict()651654
.parse(request.body);652655
const created = await createDashboardAgent(repository, owner, value);653-
const card = await repository.publishAgent(654-
owner,655-
buildPublicAgentCard(created.agent, publicBaseUrl(request)),656-
);657-
await repository.upsert(owner, 'publication', created.agent.agentId, {658-
agentId: created.agent.agentId,659-
published: true,660-
updatedAt: card.updatedAt,661-
});662-
return { ...created, card };656+
return created;663657
});664658
router.post('/v0.1/agents', async (request) => {665659
const owner = operatorId(request);@@ -698,7 +692,7 @@
698692
owner,699693
buildPublicAgentCard(agent, publicBaseUrl(request), previous),700694
);701-
const publication = { agentId: id, published: true, updatedAt: card.updatedAt };695+
const publication = publicPublication(card);702696
await repository.upsert(owner, 'publication', id, publication);703697
return { ...publication, card };704698
});@@ -734,11 +728,7 @@
734728
owner,735729
buildPublicAgentCard(agent, publicBaseUrl(request), previous),736730
);737-
await repository.upsert(owner, 'publication', id, {738-
agentId: id,739-
published: true,740-
updatedAt: card.updatedAt,741-
});731+
await repository.upsert(owner, 'publication', id, publicPublication(card));742732
} else {743733
await repository.unpublishAgent(owner, id);744734
await repository.upsert(owner, 'publication', id, {@@ -867,49 +857,6 @@
867857
throw new Error('Federated interactions are not configured');868858
return repository.listFederatedInteractions(owner);869859
});870-
router.post('/v0.1/federated-interactions/start', async (request) => {871-
const owner = operatorId(request);872-
if (!repository?.createFederatedInteraction || !owner)873-
throw new Error('Federated interactions are not configured');874-
const value = z875-
.object({876-
initiatorAgentId: z.string().min(1),877-
responderAgentId: z.string().min(1),878-
task: z.string().trim().min(10).max(2000),879-
requestedOutcome: z.string().trim().min(3).max(1000),880-
successCriterion: z.string().trim().min(3).max(1000),881-
taskCategory: z.string().trim().min(1).max(100).optional(),882-
deadline: z.string().datetime().optional(),883-
})884-
.strict()885-
.refine((input) => !input.deadline || Date.parse(input.deadline) > Date.now(), {886-
message: 'Deadline must be in the future',887-
path: ['deadline'],888-
})889-
.parse(request.body);890-
const [initiator, responder] = await Promise.all([891-
repository.getPublishedAgent(value.initiatorAgentId),892-
repository.getPublishedAgent(value.responderAgentId),893-
]);894-
if (!initiator) throw new Error('Publish the initiating agent before starting an agreement');895-
if (!responder) throw new Error('The selected counterparty is no longer available');896-
if (repository.listFederatedInteractions) {897-
const existing = (await repository.listFederatedInteractions(owner)).find(898-
(candidate) =>899-
['pending', 'active'].includes(candidate.status) &&900-
candidate.initiatorAgentId === value.initiatorAgentId &&901-
candidate.responderAgentId === value.responderAgentId &&902-
candidate.contract.purpose === value.task.slice(0, 500) &&903-
candidate.contract.requestedOutcome === value.requestedOutcome &&904-
candidate.contract.successCriteria[0] === value.successCriterion,905-
);906-
if (existing) return existing;907-
}908-
return repository.createFederatedInteraction(909-
owner,910-
buildQuickstartInteraction(initiator, responder, value),911-
);912-
});913860
router.get('/v0.1/federated-interactions/:id', async (request) => {914861
const owner = operatorId(request);915862
if (!repository?.getFederatedInteraction || !owner)@@ -941,46 +888,6 @@
941888
value.agentId,942889
);943890
});944-
router.post('/v0.1/federated-interactions/:id/complete', async (request) => {945-
const owner = operatorId(request);946-
if (!repository?.getFederatedInteraction || !repository.submitCompletionReport || !owner)947-
throw new Error('Interaction completion is not configured');948-
const interactionId = (request.params as { id: string }).id;949-
const value = z950-
.object({951-
agentId: z.string().min(1),952-
outcome: z.enum(['success', 'partial', 'failure', 'cancelled']),953-
summary: z.string().trim().min(3).max(2000),954-
evidenceReferences: z.array(z.string().url().max(2048)).max(20).default([]),955-
})956-
.strict()957-
.parse(request.body);958-
const [interaction, rows] = await Promise.all([959-
repository.getFederatedInteraction(owner, interactionId),960-
repository.list(owner),961-
]);962-
if (!interaction) throw new Error('Interaction not found');963-
if (!['active', 'completed'].includes(interaction.status))964-
throw new Error('The agreement must be active before reporting its outcome');965-
const duplicate = rows.find(966-
(row) =>967-
row.kind === 'completion_report' &&968-
row.payload?.interactionId === interactionId &&969-
row.payload?.reportingAgentId === value.agentId,970-
);971-
if (duplicate) return { report: duplicate.payload, deduplicated: true };972-
const agent = rows.find(973-
(row) => row.kind === 'agent_profile' && row.recordId === value.agentId,974-
)?.payload as AgentProfile | undefined;975-
if (!agent || agent.status !== 'active') throw new Error('Active owned agent not found');976-
const session = repository.getLiveSession977-
? await repository.getLiveSession(owner, interactionId, value.agentId)978-
: undefined;979-
const report = buildOwnerCompletionReport(interaction, agent, value, {980-
...(session?.activatedAt ? { startedAt: session.activatedAt } : {}),981-
});982-
return repository.submitCompletionReport(owner, value.agentId, report, 'oauth_account');983-
});984891
router.post('/v0.1/federated-interactions/:id/completion-reports', async (request) => {985892
const owner = operatorId(request);986893
if (!repository?.submitCompletionReport || !owner)@@ -1012,42 +919,6 @@
1012919
? 'agent_access_token'1013920
: 'oauth_installation',1014921
);1015-
});1016-
router.post('/v0.1/feedback-requests/:id/respond', async (request) => {1017-
const owner = operatorId(request);1018-
if (!repository?.listFeedbackRequests || !repository.submitInteractionFeedback || !owner)1019-
throw new Error('Interaction feedback is not configured');1020-
const value = z1021-
.object({1022-
agentId: z.string().min(1),1023-
rating: z.number().int().min(1).max(5),1024-
wouldWorkAgain: z.enum(['yes', 'no', 'unsure']),1025-
privateComment: z.string().trim().max(1000).optional(),1026-
})1027-
.strict()1028-
.parse(request.body);1029-
const [requests, rows] = await Promise.all([1030-
repository.listFeedbackRequests(owner, value.agentId),1031-
repository.list(owner),1032-
]);1033-
const duplicate = rows.find(1034-
(row) =>1035-
row.kind === 'interaction_feedback' &&1036-
row.payload?.requestId === (request.params as { id: string }).id &&1037-
row.payload?.reviewerAgentId === value.agentId,1038-
);1039-
if (duplicate) return { feedback: duplicate.payload, deduplicated: true };1040-
const feedbackRequest = requests.find(1041-
(candidate) => candidate.requestId === (request.params as { id: string }).id,1042-
);1043-
if (!feedbackRequest || feedbackRequest.status !== 'pending')1044-
throw new Error('Pending feedback request not found');1045-
const agent = rows.find(1046-
(row) => row.kind === 'agent_profile' && row.recordId === value.agentId,1047-
)?.payload as AgentProfile | undefined;1048-
if (!agent || agent.status !== 'active') throw new Error('Active owned agent not found');1049-
const feedback = buildOwnerFeedback(feedbackRequest, agent, value);1050-
return repository.submitInteractionFeedback(owner, value.agentId, feedback, 'oauth_account');1051922
});1052923
router.get('/v0.1/feedback-requests', async (request) => {1053924
const owner = operatorId(request);apps/api/src/quickstart.tsdeleted
Loading diff…
apps/dashboard/src/components/first-run-guide.tsxmodifiedView file
Loading diff…
apps/dashboard/src/main.tsxmodifiedView file
Loading diff…
apps/dashboard/src/styles.cssmodifiedView file
Loading diff…
docs/GO_LIVE_PLAN.mdmodifiedView file
Loading diff…
docs/PRODUCTION_VERIFICATION.mdmodifiedView file
Loading diff…
packages/persistence/src/onboarding.tsmodifiedView file
Loading diff…
tests/agent-card-onboarding.test.tsaddedView file
Loading diff…
tests/api.test.tsmodifiedView file
Loading diff…
tests/onboarding.test.tsmodifiedView file
Loading diff…
tests/quickstart-api.test.tsdeleted
Loading diff…
tests/quickstart.test.tsdeleted
Loading diff…