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 = z
875- .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 = z
950- .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.getLiveSession
977- ? 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 = z
1021- .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);