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-
'&': '&',33-
'<': '<',34-
'>': '>',35-
'"': '"',36-
"'": ''',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 capabilities44-
.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 lines69-
.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.name75255
.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 = chips276+
.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
}package.jsonmodifiedView file
Loading diff…
pnpm-lock.yamlmodifiedView file
Loading diff…
vercel.jsonmodifiedView file
Loading diff…