The AI trust layer between agents: identity, agreements, safeguards, and outcome intelligence for A2A.

Commit faf8a393

faf8a393d637Merged

aryaamin <[email protected]> · 2 months ago · 4 files · ~4 modified

Description

Make Agent Card images self-contained

Details

Commit
faf8a393d637
Parent
61a3d5a92277

apps/api/src/agent-card-image.tsmodifiedView file

@@ -1,58 +1,29 @@
1+import { readFileSync } from 'node:fs';
12 import { createRequire } from 'node:module';
23 import { dirname, join } from 'node:path';
3-import { mkdirSync, writeFileSync } from 'node:fs';
4+import { createElement, type CSSProperties, type ReactNode } from 'react';
5+import satori from 'satori';
6+import sharp from 'sharp';
47 import type { PublicAgentCard } from '../../../packages/protocol/src/index.js';
5-
6-let sharpModule: Promise<typeof import('sharp')> | undefined;
7-
8-function loadSharp() {
9- if (sharpModule) return sharpModule;
10- const require = createRequire(import.meta.url);
11- const fontDirectory = join(dirname(require.resolve('geist/font/sans')), 'fonts/geist-sans');
12- const configDirectory = '/tmp/openclasp-fontconfig';
13- const cacheDirectory = join(configDirectory, 'cache');
14- const configFile = join(configDirectory, 'fonts.conf');
15- mkdirSync(cacheDirectory, { recursive: true });
16- writeFileSync(
17- configFile,
18- `<?xml version="1.0"?><!DOCTYPE fontconfig SYSTEM "urn:fontconfig:fonts.dtd"><fontconfig><dir>${fontDirectory}</dir><cachedir>${cacheDirectory}</cachedir></fontconfig>`,
19- );
20- process.env.FONTCONFIG_FILE = configFile;
21- process.env.FONTCONFIG_PATH = configDirectory;
22- sharpModule = import('sharp');
23- return sharpModule;
24-}
258
269 const shorten = (value: string, length: number) =>
2710 value.length > length ? `${value.slice(0, length - 1).trimEnd()}…` : value;
2811
29-const escapeXml = (value: string) =>
30- value.replace(/[&<>"']/g, (character) => {
31- const entities: Record<string, string> = {
32- '&': '&amp;',
33- '<': '&lt;',
34- '>': '&gt;',
35- '"': '&quot;',
36- "'": '&apos;',
37- };
38- return entities[character]!;
39- });
12+interface CapabilityChip {
13+ label: string;
14+ width: number;
15+ x: number;
16+}
4017
41-function capabilityChips(capabilities: string[]) {
18+function capabilityChips(capabilities: string[]): CapabilityChip[] {
4219 let x = 64;
43- return capabilities
44- .slice(0, 4)
45- .map((capability) => {
46- const label = shorten(capability, 25);
47- const width = Math.min(250, Math.max(96, label.length * 10 + 32));
48- const chip = `<g transform="translate(${x} 430)">
49- <rect width="${width}" height="42" rx="8" fill="#ffffff0b" stroke="#ffffff28"/>
50- <text x="16" y="27" fill="#ddd4d0" font-size="17">${escapeXml(label)}</text>
51- </g>`;
52- x += width + 12;
53- return chip;
54- })
55- .join('');
20+ return capabilities.slice(0, 4).map((capability) => {
21+ const label = shorten(capability, 25);
22+ const width = Math.min(250, Math.max(96, label.length * 10 + 32));
23+ const chip = { label, width, x };
24+ x += width + 12;
25+ return chip;
26+ });
5627 }
5728
5829 function descriptionLines(value: string) {
@@ -65,11 +36,220 @@
6536 }
6637 if (lines.length > 2) lines.length = 2;
6738 if (lines[1] && lines[1].length > 66) lines[1] = shorten(lines[1], 66);
68- return lines
69- .map((line, index) => `<tspan x="220" dy="${index === 0 ? 0 : 34}">${escapeXml(line)}</tspan>`)
70- .join('');
39+ return lines;
40+}
41+
42+const absolute = (style: CSSProperties): CSSProperties => ({
43+ position: 'absolute',
44+ display: 'flex',
45+ ...style,
46+});
47+
48+let geistFonts: Array<{
49+ name: string;
50+ data: Buffer;
51+ weight: 400 | 700;
52+ style: 'normal';
53+}> | null = null;
54+
55+function loadGeistFonts() {
56+ if (geistFonts) return geistFonts;
57+ const require = createRequire(import.meta.url);
58+ const fontDirectory = join(dirname(require.resolve('geist/font/sans')), 'fonts/geist-sans');
59+ geistFonts = [
60+ {
61+ name: 'Geist',
62+ data: readFileSync(join(fontDirectory, 'Geist-Regular.ttf')),
63+ weight: 400,
64+ style: 'normal',
65+ },
66+ {
67+ name: 'Geist',
68+ data: readFileSync(join(fontDirectory, 'Geist-Bold.ttf')),
69+ weight: 700,
70+ style: 'normal',
71+ },
72+ ];
73+ return geistFonts;
7174 }
7275
76+async function renderTextLayer({
77+ card,
78+ chips,
79+ description,
80+ initials,
81+ name,
82+ nameSize,
83+ publicReference,
84+}: {
85+ card: PublicAgentCard;
86+ chips: CapabilityChip[];
87+ description: string[];
88+ initials: string;
89+ name: string;
90+ nameSize: number;
91+ publicReference: string;
92+}) {
93+ const children: ReactNode[] = [
94+ createElement(
95+ 'div',
96+ {
97+ key: 'brand',
98+ style: absolute({
99+ left: 108,
100+ top: 58,
101+ color: '#f04b2d',
102+ fontSize: 27,
103+ fontWeight: 700,
104+ letterSpacing: -1,
105+ lineHeight: 1,
106+ }),
107+ },
108+ 'OpenClasp',
109+ ),
110+ createElement(
111+ 'div',
112+ {
113+ key: 'verified',
114+ style: absolute({
115+ left: 978,
116+ top: 69,
117+ color: '#44d37e',
118+ fontSize: 16,
119+ lineHeight: 1,
120+ }),
121+ },
122+ 'Publisher verified',
123+ ),
124+ createElement(
125+ 'div',
126+ {
127+ key: 'initials',
128+ style: absolute({
129+ left: 64,
130+ top: 165,
131+ width: 122,
132+ height: 122,
133+ alignItems: 'center',
134+ justifyContent: 'center',
135+ color: '#ff8068',
136+ fontSize: 42,
137+ fontWeight: 700,
138+ }),
139+ },
140+ initials || 'AI',
141+ ),
142+ createElement(
143+ 'div',
144+ {
145+ key: 'framework',
146+ style: absolute({
147+ left: 220,
148+ top: 160,
149+ color: '#a99e99',
150+ fontSize: 18,
151+ letterSpacing: 2,
152+ lineHeight: 1,
153+ }),
154+ },
155+ `${shorten(card.framework, 44).toUpperCase()} · AGENT CARD`,
156+ ),
157+ createElement(
158+ 'div',
159+ {
160+ key: 'name',
161+ style: absolute({
162+ left: 220,
163+ top: 196,
164+ color: '#f7f2ef',
165+ fontSize: nameSize,
166+ fontWeight: 700,
167+ letterSpacing: -2,
168+ lineHeight: 1,
169+ }),
170+ },
171+ name,
172+ ),
173+ ...description.map((line, index) =>
174+ createElement(
175+ 'div',
176+ {
177+ key: `description-${index}`,
178+ style: absolute({
179+ left: 220,
180+ top: 287 + index * 34,
181+ color: '#c7bdb8',
182+ fontSize: 24,
183+ lineHeight: 1,
184+ }),
185+ },
186+ line,
187+ ),
188+ ),
189+ ...chips.map((chip) =>
190+ createElement(
191+ 'div',
192+ {
193+ key: `chip-${chip.x}`,
194+ style: absolute({
195+ left: chip.x + 16,
196+ top: 442,
197+ color: '#ddd4d0',
198+ fontSize: 17,
199+ lineHeight: 1,
200+ }),
201+ },
202+ chip.label,
203+ ),
204+ ),
205+ createElement(
206+ 'div',
207+ {
208+ key: 'reference',
209+ style: absolute({
210+ left: 64,
211+ top: 568,
212+ color: '#8f8580',
213+ fontSize: 16,
214+ lineHeight: 1,
215+ }),
216+ },
217+ publicReference,
218+ ),
219+ createElement(
220+ 'div',
221+ {
222+ key: 'disclaimer',
223+ style: absolute({
224+ right: 64,
225+ top: 568,
226+ color: '#8f8580',
227+ fontSize: 16,
228+ lineHeight: 1,
229+ }),
230+ },
231+ 'Capabilities are self-declared',
232+ ),
233+ ];
234+
235+ return satori(
236+ createElement(
237+ 'div',
238+ {
239+ style: {
240+ position: 'relative',
241+ display: 'flex',
242+ width: 1200,
243+ height: 630,
244+ fontFamily: 'Geist',
245+ },
246+ },
247+ ...children,
248+ ),
249+ { width: 1200, height: 630, fonts: loadGeistFonts() },
250+ );
251+}
252+
73253 export async function renderAgentCardImage(card: PublicAgentCard) {
74254 const initials = card.name
75255 .split(/\s+/)
@@ -80,7 +260,24 @@
80260 const publicReference = shorten(card.profileUrl ?? card.cardUrl, 88);
81261 const name = shorten(card.name, 42);
82262 const nameSize = name.length > 30 ? 50 : 62;
83- const description = card.description || 'Public agent identity on OpenClasp';
263+ const description = descriptionLines(card.description || 'Public agent identity on OpenClasp');
264+ const chips = capabilityChips(card.capabilities);
265+ const textLayer = await renderTextLayer({
266+ card,
267+ chips,
268+ description,
269+ initials,
270+ name,
271+ nameSize,
272+ publicReference,
273+ });
274+ const textContent = textLayer.slice(textLayer.indexOf('>') + 1, textLayer.lastIndexOf('</svg>'));
275+ const chipBackgrounds = chips
276+ .map(
277+ ({ width, x }) =>
278+ `<rect x="${x}" y="430" width="${width}" height="42" rx="8" fill="#ffffff0b" stroke="#ffffff28"/>`,
279+ )
280+ .join('');
84281 const svg = `<svg width="1200" height="630" viewBox="0 0 1200 630" xmlns="http://www.w3.org/2000/svg">
85282 <defs>
86283 <linearGradient id="background" x1="0" y1="0" x2="1" y2="1">
@@ -96,25 +293,13 @@
96293 <rect width="1200" height="630" fill="url(#background)"/>
97294 <rect width="1200" height="630" fill="url(#glow)"/>
98295 <path d="M64 66h18l10 10-10 10H64l-10-10 10-10Z" fill="none" stroke="#f04b2d" stroke-width="4"/>
99- <text x="108" y="84" fill="#f04b2d" font-family="Geist" font-size="27" font-weight="700" letter-spacing="-1">OpenClasp</text>
100- <g transform="translate(936 58)">
101- <rect width="200" height="42" rx="21" fill="#44d37e0d" stroke="#44d37e88"/>
102- <path d="M18 21l5 5 9-11" fill="none" stroke="#44d37e" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
103- <text x="42" y="27" fill="#44d37e" font-family="Geist" font-size="16">Publisher verified</text>
104- </g>
105- <g transform="translate(64 165)">
106- <rect width="122" height="122" rx="28" fill="#f04b2d1a" stroke="#f04b2d80"/>
107- <text x="61" y="78" text-anchor="middle" fill="#ff8068" font-family="Geist" font-size="42" font-weight="700">${escapeXml(initials || 'AI')}</text>
108- </g>
109- <text x="220" y="176" fill="#a99e99" font-family="Geist" font-size="18" letter-spacing="2">${escapeXml(shorten(card.framework, 44).toUpperCase())} · AGENT CARD</text>
110- <text x="220" y="246" fill="#f7f2ef" font-family="Geist" font-size="${nameSize}" font-weight="700" letter-spacing="-2">${escapeXml(name)}</text>
111- <text x="220" y="308" fill="#c7bdb8" font-family="Geist" font-size="24">${descriptionLines(description)}</text>
112- <g font-family="Geist">${capabilityChips(card.capabilities)}</g>
296+ <rect x="936" y="58" width="200" height="42" rx="21" fill="#44d37e0d" stroke="#44d37e88"/>
297+ <path d="M954 79l5 5 9-11" fill="none" stroke="#44d37e" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
298+ <rect x="64" y="165" width="122" height="122" rx="28" fill="#f04b2d1a" stroke="#f04b2d80"/>
299+ ${chipBackgrounds}
113300 <line x1="64" y1="540" x2="1136" y2="540" stroke="#ffffff21"/>
114- <text x="64" y="582" fill="#8f8580" font-family="Geist" font-size="16">${escapeXml(publicReference)}</text>
115- <text x="1136" y="582" text-anchor="end" fill="#8f8580" font-family="Geist" font-size="16">Capabilities are self-declared</text>
301+ ${textContent}
116302 </svg>`;
117303
118- const { default: sharp } = await loadSharp();
119304 return sharp(Buffer.from(svg)).png().toBuffer();
120305 }