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

Viewing files at commit 310519e1fe0fCommit detailsView latest

apps/api/src/app.ts 1363 lines · 67.9 KB · typescript
1
import Fastify from 'fastify';
2
import cors from '@fastify/cors';
3
import swagger from '@fastify/swagger';
4
import { randomUUID } from 'node:crypto';
5
import { z } from 'zod';
6
import {
7
  AgentIdentitySchema,
8
  AssuranceProbeResponseSchema,
9
  DelegationCredentialSchema,
10
  FeedbackSchema,
11
  InteractionContractSchema,
12
  InteractionEventSchema,
13
  ReceiptSchema,
14
  FederatedInteractionSchema,
15
  LiveSessionEventSchema,
16
  InteractionCompletionReportSchema,
17
  InteractionFeedbackSchema,
18
  ConnectorAgentProfileSchema,
19
  ShieldCaseSchema,
20
  ShieldGuidanceSchema,
21
  ShieldOutcomeSchema,
22
  type PublicAgentCard,
23
} from '../../../packages/protocol/src/index.js';
24
import {
25
  MemoryAuditStore,
26
  TrustEngine,
27
  UnavailableFactCheckProvider,
28
} from '../../../packages/core/src/index.js';
29
import { buildPublicAgentCard } from '../../../packages/persistence/src/index.js';
30
import type { AgentProfile, HostedRepository } from '../../../packages/persistence/src/index.js';
31
import { toA2AAgentCard } from '../../../packages/sidecar/src/index.js';
32
import {
33
  approveAgentSetup,
34
  getOnboardingState,
35
  rejectAgentSetup,
36
} from '../../../packages/persistence/src/onboarding.js';
37
import { FixedWindowRateLimiter } from './security.js';
38
import { renderAgentCardImage } from './agent-card-image.js';
39
import {
40
  createShieldCase,
41
  consultShield,
42
  ShieldCaseInputSchema,
43
  ShieldConsultInputSchema,
44
} from '../../../packages/mcp-server/src/shield-agent.js';
45
46
type DashboardRepository = Pick<
47
  HostedRepository,
48
  | 'getSettings'
49
  | 'saveSettings'
50
  | 'upsert'
51
  | 'list'
52
  | 'publishAgent'
53
  | 'unpublishAgent'
54
  | 'getPublishedAgent'
55
  | 'resolveAgentReference'
56
  | 'searchPublishedAgents'
57
> & {
58
  dashboard(operatorId: string): Promise<Record<string, unknown>>;
59
} & Partial<
60
    Pick<
61
      HostedRepository,
62
      | 'createFederatedInteraction'
63
      | 'listFederatedInteractions'
64
      | 'getFederatedInteraction'
65
      | 'respondToFederatedInteraction'
66
      | 'proposeContractRevision'
67
      | 'respondToContractRevision'
68
      | 'getLiveSession'
69
      | 'getCounterpartyBrief'
70
      | 'submitCompletionReport'
71
      | 'recordSessionCompletionReport'
72
      | 'submitInteractionFeedback'
73
      | 'recordSessionFeedback'
74
      | 'recordSessionAssuranceResponse'
75
      | 'listAssuranceProbePlans'
76
      | 'submitAssuranceProbeResponse'
77
      | 'listAssuranceComparisons'
78
      | 'getAssuranceBrief'
79
      | 'decideAssuranceSafeguard'
80
      | 'listFeedbackRequests'
81
      | 'recordLiveSessionEvent'
82
      | 'touchAgentPresence'
83
      | 'getRuntimeVerificationKey'
84
      | 'registerAgentRuntime'
85
      | 'listAgentRuntimes'
86
      | 'disableAgentRuntime'
87
      | 'deleteAgent'
88
      | 'issueAgentAccessToken'
89
      | 'listAgentAccessTokens'
90
      | 'revokeAgentAccessToken'
91
      | 'createConnectorClaim'
92
      | 'getConnectorClaim'
93
      | 'pollConnectorClaim'
94
      | 'approveConnectorClaim'
95
      | 'rejectConnectorClaim'
96
      | 'createProviderConnection'
97
      | 'getProviderConnection'
98
      | 'completeBotpressConnection'
99
      | 'saveShieldCase'
100
      | 'getShieldCase'
101
      | 'listShieldCases'
102
      | 'saveShieldConsultation'
103
      | 'listShieldConsultations'
104
      | 'saveShieldOutcome'
105
    >
106
  > & {
107
    listContextualIntelligence?: (
108
      operatorId: string,
109
      input?: { agentId?: string; taskCategory?: string },
110
    ) => Promise<any[]>;
111
    searchPersonalizedMarketplace?: (
112
      operatorId: string,
113
      input?: { agentId?: string; taskCategory?: string; query?: string; limit?: number },
114
    ) => Promise<any[]>;
115
  };
116
117
export function buildApi(
118
  engine = new TrustEngine(),
119
  factChecker = new UnavailableFactCheckProvider(),
120
  repository?: DashboardRepository,
121
  security: { internalAuthSecret?: string } = {},
122
) {
123
  const app = Fastify({
124
    bodyLimit: 256 * 1024,
125
    requestTimeout: 30_000,
126
    logger: {
127
      redact: [
128
        'req.headers.authorization',
129
        'req.headers.cookie',
130
        'req.headers.x-openclasp-internal-auth',
131
        'req.headers.x-openclasp-pairing-code',
132
        'req.body.rawMessage',
133
        'req.body.message',
134
        'req.body.situationContext',
135
        'req.body.privateKey',
136
      ],
137
    },
138
  });
139
  const engines = new Map<string, Promise<TrustEngine>>();
140
  const limiter = new FixedWindowRateLimiter();
141
  const allowedOrigin = process.env.OPENCLASP_PUBLIC_URL
142
    ? new URL(process.env.OPENCLASP_PUBLIC_URL).origin
143
    : undefined;
144
  void app.register(cors, {
145
    credentials: Boolean(repository),
146
    origin(origin, callback) {
147
      callback(null, !origin || !allowedOrigin || origin === allowedOrigin);
148
    },
149
  });
150
  void app.register(swagger, { openapi: { info: { title: 'OpenClasp API', version: '0.1.0' } } });
151
152
  app.addHook('onRequest', async (request, reply) => {
153
    const method = request.method.toUpperCase();
154
    if (['GET', 'HEAD', 'OPTIONS'].includes(method)) return;
155
    const operator = request.headers['x-openclasp-operator'];
156
    const bucket = typeof operator === 'string' ? `operator:${operator}` : `ip:${request.ip}`;
157
    const externalSession = request.url.startsWith('/sessions/') || request.url.startsWith('/a2a/');
158
    const result = limiter.consume(bucket, externalSession ? 300 : 120, 60_000);
159
    reply.header('x-ratelimit-remaining', result.remaining);
160
    if (!result.allowed)
161
      return reply
162
        .status(429)
163
        .header('retry-after', result.retryAfterSeconds)
164
        .send({ error: 'rate_limit_exceeded' });
165
  });
166
167
  app.addHook('onSend', async (_request, reply, payload) => {
168
    reply.header('x-content-type-options', 'nosniff');
169
    reply.header('referrer-policy', 'no-referrer');
170
    reply.header('permissions-policy', 'camera=(), microphone=(), geolocation=()');
171
    return payload;
172
  });
173
174
  app.setErrorHandler((error, _request, reply) =>
175
    reply
176
      .status(
177
        error instanceof Error && 'statusCode' in error && typeof error.statusCode === 'number'
178
          ? error.statusCode
179
          : 400,
180
      )
181
      .send({ error: error instanceof Error ? error.message : 'Unknown error' }),
182
  );
183
  void app.register(async (router) => {
184
    const operatorId = (request: { headers: Record<string, unknown> }) => {
185
      const value = request.headers['x-openclasp-operator'];
186
      if (
187
        repository &&
188
        security.internalAuthSecret &&
189
        request.headers['x-openclasp-internal-auth'] !== security.internalAuthSecret
190
      ) {
191
        const error = new Error('Trusted authentication boundary required');
192
        Object.assign(error, { statusCode: 401 });
193
        throw error;
194
      }
195
      if (repository && typeof value !== 'string') {
196
        const error = new Error('Authentication required');
197
        Object.assign(error, { statusCode: 401 });
198
        throw error;
199
      }
200
      return typeof value === 'string' ? value : undefined;
201
    };
202
    const boundAgentId = (request: { headers: Record<string, unknown> }) => {
203
      const value = request.headers['x-openclasp-bound-agent'];
204
      if (typeof value !== 'string' || !value) {
205
        const error = new Error('Agent-bound authentication required');
206
        Object.assign(error, { statusCode: 401 });
207
        throw error;
208
      }
209
      return value;
210
    };
211
    const enforceBoundAgent = (
212
      request: { headers: Record<string, unknown> },
213
      requestedAgentId: string,
214
    ) => {
215
      const value = request.headers['x-openclasp-bound-agent'];
216
      if (typeof value === 'string' && value !== requestedAgentId) {
217
        const error = new Error('Agent credential cannot access another agent');
218
        Object.assign(error, { statusCode: 403 });
219
        throw error;
220
      }
221
    };
222
    const scopedEngine = async (request: { headers: Record<string, unknown> }) => {
223
      const owner = operatorId(request);
224
      if (!repository || !owner) return engine;
225
      let pending = engines.get(owner);
226
      if (!pending) {
227
        pending = repository.list(owner).then((rows) => {
228
          const store = new MemoryAuditStore();
229
          for (const row of rows) {
230
            if (row.kind !== 'interaction') store.append(row.kind, row.recordId, row.payload);
231
          }
232
          return new TrustEngine(store);
233
        });
234
        engines.set(owner, pending);
235
      }
236
      return pending;
237
    };
238
    const persist = async (
239
      request: { headers: Record<string, unknown> },
240
      kind: Parameters<HostedRepository['upsert']>[1],
241
      id: string,
242
      value: unknown,
243
    ) => {
244
      const owner = operatorId(request);
245
      if (repository && owner) await repository.upsert(owner, kind, id, value);
246
    };
247
    const publicBaseUrl = (request: { headers: Record<string, unknown> }) => {
248
      const forwardedHost = request.headers['x-forwarded-host'];
249
      const host = typeof forwardedHost === 'string' ? forwardedHost : request.headers.host;
250
      const forwardedProtocol = request.headers['x-forwarded-proto'];
251
      const protocol = typeof forwardedProtocol === 'string' ? forwardedProtocol : 'https';
252
      return process.env.OPENCLASP_PUBLIC_URL ?? `${protocol}://${String(host ?? 'localhost')}`;
253
    };
254
    const publicPublication = (card: PublicAgentCard) => ({
255
      agentId: card.agentId,
256
      published: true,
257
      profileUrl: card.profileUrl,
258
      cardUrl: card.cardUrl,
259
      a2aAgentCardUrl: card.a2aAgentCardUrl,
260
      verification: card.verification,
261
      updatedAt: card.updatedAt,
262
    });
263
    const escapeHtml = (value: string) =>
264
      value.replace(/[&<>"']/g, (character) => {
265
        const entities: Record<string, string> = {
266
          '&': '&amp;',
267
          '<': '&lt;',
268
          '>': '&gt;',
269
          '"': '&quot;',
270
          "'": '&#39;',
271
        };
272
        return entities[character]!;
273
      });
274
    router.get('/health', async () => ({ status: 'ok' }));
275
    router.get('/ready', async () => ({ status: 'ready' }));
276
    router.get('/openapi.json', async () => app.swagger());
277
    router.get('/.well-known/openclasp-session-key', async () => {
278
      if (!repository?.getRuntimeVerificationKey)
279
        throw new Error('Runtime verification is not configured');
280
      return repository.getRuntimeVerificationKey();
281
    });
282
    router.get('/extensions/trust/v0.1', async () => ({
283
      uri: 'https://openclasp.vercel.app/extensions/trust/v0.1',
284
      name: 'OpenClasp A2A assurance extension',
285
      version: '0.1',
286
      required: false,
287
      transportsMessages: false,
288
      documentation: 'https://github.com/aryaamin/OpenClasp/blob/main/docs/A2A_EXTENSION.md',
289
    }));
290
    router.post('/sessions/:id/events', async (request, reply) => {
291
      if (!repository?.recordLiveSessionEvent)
292
        throw new Error('Live-session reporting is not configured');
293
      const authorization = request.headers.authorization;
294
      if (!authorization?.startsWith('Bearer ')) {
295
        reply.status(401);
296
        return { error: 'session_credential_required' };
297
      }
298
      const event = LiveSessionEventSchema.parse(request.body);
299
      if (event.interactionId !== (request.params as { id: string }).id)
300
        throw new Error('Interaction path does not match the event');
301
      return repository.recordLiveSessionEvent(authorization.slice(7), event);
302
    });
303
    router.post('/sessions/:id/completion-reports', async (request, reply) => {
304
      if (!repository?.recordSessionCompletionReport)
305
        throw new Error('Live-session completion reporting is not configured');
306
      const authorization = request.headers.authorization;
307
      if (!authorization?.startsWith('Bearer ')) {
308
        reply.status(401);
309
        return { error: 'session_credential_required' };
310
      }
311
      const report = InteractionCompletionReportSchema.parse(request.body);
312
      if (report.interactionId !== (request.params as { id: string }).id)
313
        throw new Error('Interaction path does not match the completion report');
314
      return repository.recordSessionCompletionReport(authorization.slice(7), report);
315
    });
316
    router.post('/sessions/:id/feedback', async (request, reply) => {
317
      if (!repository?.recordSessionFeedback)
318
        throw new Error('Live-session feedback is not configured');
319
      const authorization = request.headers.authorization;
320
      if (!authorization?.startsWith('Bearer ')) {
321
        reply.status(401);
322
        return { error: 'session_credential_required' };
323
      }
324
      const feedback = InteractionFeedbackSchema.parse(request.body);
325
      if (feedback.interactionId !== (request.params as { id: string }).id)
326
        throw new Error('Interaction path does not match the feedback');
327
      return repository.recordSessionFeedback(authorization.slice(7), feedback);
328
    });
329
    router.post('/sessions/:id/assurance-responses', async (request, reply) => {
330
      if (!repository?.recordSessionAssuranceResponse)
331
        throw new Error('Assurance response reporting is not configured');
332
      const authorization = request.headers.authorization;
333
      if (!authorization?.startsWith('Bearer ')) {
334
        reply.status(401);
335
        return { error: 'session_credential_required' };
336
      }
337
      const response = AssuranceProbeResponseSchema.parse(request.body);
338
      if (response.interactionId !== (request.params as { id: string }).id)
339
        throw new Error('Interaction path does not match the assurance response');
340
      return repository.recordSessionAssuranceResponse(authorization.slice(7), response);
341
    });
342
    router.get('/agents/:id/card.json', async (request) => {
343
      if (!repository) throw new Error('Hosted persistence is not configured');
344
      const card = await repository.getPublishedAgent((request.params as { id: string }).id);
345
      if (!card) throw new Error('Published agent not found');
346
      return card;
347
    });
348
    router.get('/agents/:id/.well-known/openclasp-agent.json', async (request) => {
349
      if (!repository) throw new Error('Hosted persistence is not configured');
350
      const card = await repository.getPublishedAgent((request.params as { id: string }).id);
351
      if (!card) throw new Error('Published agent not found');
352
      return card;
353
    });
354
    router.get('/agents/:id/a2a-agent-card.json', async (request) => {
355
      if (!repository) throw new Error('Hosted persistence is not configured');
356
      const card = await repository.getPublishedAgent((request.params as { id: string }).id);
357
      if (!card) throw new Error('Published agent not found');
358
      if (!card.transports.length) throw new Error('Agent has not published an A2A endpoint');
359
      return toA2AAgentCard(card);
360
    });
361
    router.get('/agents/:id/og.png', async (request, reply) => {
362
      if (!repository) throw new Error('Hosted persistence is not configured');
363
      const card = await repository.getPublishedAgent((request.params as { id: string }).id);
364
      if (!card) throw new Error('Published agent not found');
365
      return reply
366
        .type('image/png')
367
        .header('Cache-Control', 'public, max-age=0, s-maxage=86400, stale-while-revalidate=604800')
368
        .send(await renderAgentCardImage(card));
369
    });
370
    router.get('/directory/resolve', async (request) => {
371
      if (!repository) throw new Error('Hosted persistence is not configured');
372
      const { reference } = z
373
        .object({ reference: z.string().min(1).max(2048) })
374
        .parse(request.query);
375
      const result = await repository.resolveAgentReference(reference);
376
      if (!result) throw new Error('Published agent not found');
377
      return result;
378
    });
379
    router.get('/directory/search', async (request) => {
380
      if (!repository) throw new Error('Hosted persistence is not configured');
381
      const input = z
382
        .object({
383
          query: z.string().trim().max(100).optional(),
384
          capability: z.string().trim().max(100).optional(),
385
          limit: z.coerce.number().int().min(1).max(50).optional(),
386
        })
387
        .parse(request.query);
388
      return repository.searchPublishedAgents(input);
389
    });
390
    router.get('/a/:reference', async (request, reply) => {
391
      if (!repository) throw new Error('Hosted persistence is not configured');
392
      const result = await repository.resolveAgentReference(
393
        (request.params as { reference: string }).reference,
394
      );
395
      if (!result) throw new Error('Published agent not found');
396
      const { card } = result;
397
      const socialImageUrl = `${publicBaseUrl(request)}/agents/${encodeURIComponent(card.agentId)}/og.png`;
398
      const capabilities = card.capabilities
399
        .map((capability) => `<li>${escapeHtml(capability)}</li>`)
400
        .join('');
401
      const limitations = card.limitations
402
        .map((limitation) => `<li>${escapeHtml(limitation)}</li>`)
403
        .join('');
404
      const runtimeLabel = 'Agent-owned cloud runtime';
405
      const connected = card.verification?.status === 'verified' && card.transports.length > 0;
406
      const presence = connected ? 'connected' : (card.presence?.status ?? 'offline');
407
      const structured = JSON.stringify({
408
        '@context': 'https://schema.org',
409
        '@type': 'SoftwareApplication',
410
        name: card.name,
411
        description: card.description,
412
        url: card.profileUrl,
413
      }).replace(/</g, '\\u003c');
414
      return reply.type('text/html; charset=utf-8').send(`<!doctype html>
415
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
416
<title>${escapeHtml(card.name)} · OpenClasp</title><meta name="description" content="${escapeHtml(card.description)}">
417
<link rel="canonical" href="${escapeHtml(card.profileUrl ?? card.cardUrl)}"><meta property="og:type" content="website"><meta property="og:url" content="${escapeHtml(card.profileUrl ?? card.cardUrl)}">
418
<meta property="og:title" content="${escapeHtml(card.name)} · OpenClasp agent profile"><meta property="og:description" content="${escapeHtml(card.description)}"><meta property="og:image" content="${escapeHtml(socialImageUrl)}"><meta property="og:image:width" content="1200"><meta property="og:image:height" content="630"><meta property="og:image:alt" content="${escapeHtml(`${card.name} Agent Card`)}">
419
<meta name="twitter:card" content="summary_large_image"><meta name="twitter:title" content="${escapeHtml(card.name)} · OpenClasp agent profile"><meta name="twitter:description" content="${escapeHtml(card.description)}"><meta name="twitter:image" content="${escapeHtml(socialImageUrl)}">
420
<meta name="theme-color" content="#100b0a"><meta property="og:site_name" content="OpenClasp">
421
<style>color-scheme:dark;*{box-sizing:border-box}html{background:#0b0909}body{margin:0;color:#f8f3f0;background:radial-gradient(circle at 78% 8%,rgba(240,75,45,.18),transparent 30rem),linear-gradient(145deg,#090808 0%,#120d0c 56%,#1f0e0b 100%);font:15px/1.55 Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;min-height:100vh}a{color:inherit;text-decoration:none}.shell{width:min(1180px,calc(100% - 40px));margin:auto}.topbar{height:72px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid rgba(255,255,255,.09)}.brand{display:flex;align-items:center;gap:11px;font-weight:760;letter-spacing:-.02em}.brand-mark{position:relative;width:26px;height:20px;background:#f04b2d;clip-path:polygon(24% 0,76% 0,100% 50%,76% 100%,24% 100%,0 50%)}.brand-mark:after{content:"";position:absolute;inset:4px;background:#0d0909;clip-path:inherit}.top-cta,.primary{display:inline-flex;align-items:center;justify-content:center;gap:8px;border-radius:999px;background:#f04b2d;color:#fff;font-weight:700;transition:transform .16s ease,background .16s ease}.top-cta{min-height:38px;padding:0 16px;font-size:13px}.top-cta:hover,.primary:hover{background:#ff6245;transform:translateY(-1px)}main{padding:64px 0 48px}.hero{display:grid;grid-template-columns:minmax(0,.86fr) minmax(470px,1.14fr);align-items:center;gap:clamp(42px,7vw,92px);min-height:570px}.eyebrow{margin:0 0 18px;color:#f87358;font:700 11px/1.2 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.17em;text-transform:uppercase}.badges{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:26px}.badge{display:inline-flex;align-items:center;gap:7px;padding:7px 10px;border:1px solid rgba(255,255,255,.13);border-radius:999px;color:#b9aca6;background:rgba(255,255,255,.035);font-size:11px;font-weight:650}.badge.verified{border-color:rgba(68,211,126,.44);color:#5ce090;background:rgba(68,211,126,.07)}.badge.verified:before{content:"✓"}.badge.status:before{width:6px;height:6px;border-radius:50%;background:#766b67;content:""}.badge.status.online{color:#5ce090}.badge.status.online:before{background:#44d37e;box-shadow:0 0 0 4px rgba(68,211,126,.12)}h1{max-width:11ch;margin:0;font-size:clamp(48px,6vw,78px);line-height:.96;letter-spacing:-.065em}h1 span{color:#f87358}.lead{max-width:580px;margin:26px 0 0;color:#b9ada7;font-size:clamp(17px,2vw,21px);line-height:1.55}.facts{display:flex;flex-wrap:wrap;gap:9px 22px;margin:28px 0;color:#887d78;font:600 11px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace;text-transform:uppercase}.facts b{color:#d3c9c4;font-weight:600}.actions{display:flex;flex-wrap:wrap;gap:10px;margin-top:32px}.primary,.secondary{min-height:48px;padding:0 19px}.secondary{display:inline-flex;align-items:center;border:1px solid rgba(255,255,255,.15);border-radius:999px;color:#d8cfca;background:rgba(255,255,255,.035);font-weight:700}.secondary:hover{border-color:rgba(240,75,45,.6);color:#fff}.verification-note{max-width:540px;margin:18px 0 0;color:#766c67;font-size:11px}.share-preview{overflow:hidden;border:1px solid rgba(255,255,255,.14);border-radius:22px;background:rgba(10,8,8,.78);box-shadow:0 34px 100px rgba(0,0,0,.42);transform:rotate(1deg);transition:transform .3s ease,border-color .3s ease}.share-preview:hover{border-color:rgba(240,75,45,.5);transform:rotate(0) translateY(-4px)}.preview-head{height:48px;display:flex;align-items:center;justify-content:space-between;padding:0 16px;border-bottom:1px solid rgba(255,255,255,.09);color:#887d78;font:650 10px/1 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.1em;text-transform:uppercase}.preview-head a{color:#f87358}.share-preview img{display:block;width:100%;height:auto;aspect-ratio:1200/630;object-fit:cover}.preview-foot{display:flex;align-items:center;justify-content:space-between;gap:16px;margin:0;padding:14px 16px;color:#887d78;font-size:11px}.preview-foot strong{color:#d8cfca}.details{display:grid;grid-template-columns:1fr 1fr .9fr;margin:52px 0 0;border:1px solid rgba(255,255,255,.1);border-radius:20px;background:rgba(8,7,7,.38)}.detail{min-height:250px;padding:28px}.detail+.detail{border-left:1px solid rgba(255,255,255,.1)}.detail h2{margin:0 0 20px;color:#8f837e;font:700 10px/1 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.15em;text-transform:uppercase}.chips{display:flex;flex-wrap:wrap;gap:8px;margin:0;padding:0;list-style:none}.chips li{padding:8px 10px;border:1px solid rgba(255,255,255,.11);border-radius:8px;color:#d4cac5;background:rgba(255,255,255,.035);font-size:12px}.limits{display:grid;gap:12px;margin:0;padding:0;list-style:none;color:#aaa09a}.limits li{display:flex;gap:9px}.limits li:before{color:#f87358;content:"—"}.machine{background:linear-gradient(155deg,rgba(240,75,45,.09),transparent 70%)}.machine code{display:block;overflow-wrap:anywhere;color:#887d78;font:11px/1.7 ui-monospace,SFMono-Regular,Menlo,monospace}.machine-links{display:grid;gap:9px;margin-top:21px}.machine-links a{display:flex;justify-content:space-between;padding-top:9px;border-top:1px solid rgba(255,255,255,.08);color:#f87358;font-size:12px;font-weight:700}.join{display:flex;align-items:center;justify-content:space-between;gap:30px;margin:52px 0 0;padding:34px 38px;border:1px solid rgba(240,75,45,.35);border-radius:20px;background:linear-gradient(110deg,rgba(240,75,45,.13),rgba(240,75,45,.03))}.join p{margin:5px 0 0;color:#958984}.join h2{margin:0;font-size:24px;letter-spacing:-.035em}.footer{display:flex;align-items:center;justify-content:space-between;min-height:86px;border-top:1px solid rgba(255,255,255,.09);color:#746a66;font-size:11px}.footer a{color:#a99e99}@media(max-width:900px){.hero{grid-template-columns:1fr;min-height:auto}.hero-copy{max-width:700px}.share-preview{max-width:720px;transform:none}.details{grid-template-columns:1fr 1fr}.detail.machine{grid-column:1/-1;border-left:0;border-top:1px solid rgba(255,255,255,.1)}}@media(max-width:620px){.shell{width:min(100% - 24px,1180px)}.topbar{height:62px}.top-cta{padding:0 12px;font-size:11px}main{padding-top:42px}.hero{gap:34px}h1{font-size:46px}.lead{font-size:16px}.details{grid-template-columns:1fr}.detail{min-height:0}.detail+.detail,.detail.machine{grid-column:auto;border-left:0;border-top:1px solid rgba(255,255,255,.1)}.join{display:grid;padding:26px}.join .primary{width:100%}.footer{align-items:flex-start;flex-direction:column;justify-content:center;gap:5px}.preview-foot{align-items:flex-start;flex-direction:column}}</style>
422
<script type="application/ld+json">${structured}</script></head><body><div class="shell"><header class="topbar"><a class="brand" href="/" aria-label="OpenClasp home"><span class="brand-mark" aria-hidden="true"></span><span>OpenClasp</span></a><a class="top-cta" href="/login">Create your card <span aria-hidden="true">→</span></a></header>
423
<main><section class="hero"><div class="hero-copy"><p class="eyebrow">Public agent profile</p><div class="badges"><span class="badge verified">Publisher verified</span><span class="badge status ${presence === 'online' || presence === 'connected' ? 'online' : ''}">${escapeHtml(presence)}</span><span class="badge">${escapeHtml(runtimeLabel)}</span></div><h1>${escapeHtml(card.name)}<span>.</span></h1><p class="lead">${escapeHtml(card.description || 'Public agent identity on OpenClasp.')}</p><div class="facts"><span>Framework <b>${escapeHtml(card.framework)}</b></span><span>Version <b>${escapeHtml(card.agentVersion)}</b></span><span>Protocol <b>A2A/1.0</b></span></div><div class="actions"><a class="primary" href="/login">Add your agent — free <span aria-hidden="true">→</span></a><a class="secondary" href="${escapeHtml(card.a2aAgentCardUrl)}">Open A2A card</a></div><p class="verification-note">OpenClasp verified control of the publishing account. Capabilities and limitations are declared by the publisher.</p></div>
424
<aside class="share-preview"><div class="preview-head"><span>Share-ready Agent Card</span><a href="${escapeHtml(socialImageUrl)}" download>Download PNG</a></div><img src="${escapeHtml(socialImageUrl)}" width="1200" height="630" alt="Share card for ${escapeHtml(card.name)}"><p class="preview-foot"><strong>Paste this profile anywhere.</strong><span>OpenGraph preview included automatically.</span></p></aside></section>
425
<section class="details"><article class="detail"><h2>What this agent does</h2><ul class="chips">${capabilities || '<li>No capabilities published</li>'}</ul></article><article class="detail"><h2>Declared limitations</h2><ul class="limits">${limitations || '<li>No limitations published</li>'}</ul></article><article class="detail machine"><h2>For agents and developers</h2><code>${escapeHtml(card.agentId)}</code><div class="machine-links"><a href="${escapeHtml(card.cardUrl)}"><span>OpenClasp JSON</span><span aria-hidden="true">↗</span></a><a href="${escapeHtml(card.a2aAgentCardUrl)}"><span>A2A Agent Card</span><span aria-hidden="true">↗</span></a></div></article></section>
426
<section class="join"><div><h2>Give your agent a shareable identity.</h2><p>Publish a verified Agent Card and make it discoverable to other cloud agents.</p></div><a class="primary" href="/login">Create yours — free <span aria-hidden="true">→</span></a></section></main><footer class="footer"><span>OpenClasp · identity and behavioural intelligence for AI agents</span><a href="/">openclasp.dev</a></footer></div></body></html>`);
427
    });
428
    router.get('/v0.1/dashboard', async (request) => {
429
      const owner = operatorId(request);
430
      if (repository && owner) return repository.dashboard(owner);
431
      return {
432
        agents: [...engine.agents.values()],
433
        projects: [],
434
        installations: [],
435
        setupRequests: [],
436
        publications: [],
437
        interactions: [],
438
        federatedInteractions: [],
439
        liveSessions: [],
440
        events: [...engine.events.values()],
441
        conflicts: [...engine.conflicts.values()],
442
        receipts: [...engine.receipts.values()],
443
        profiles: [...engine.profiles.values()],
444
        counterpartyBriefs: [],
445
        completionReports: [],
446
        feedbackRequests: [],
447
        interactionFeedback: [],
448
        interactionConclusions: [],
449
        learningEligibility: [],
450
        profileDeltas: [],
451
        intelligenceSummaries: [],
452
        runtimes: [],
453
        shieldCases: [],
454
        shieldConsultations: [],
455
        shieldOutcomes: [],
456
      };
457
    });
458
    router.get('/v0.1/shield/cases', async (request) => {
459
      const owner = operatorId(request);
460
      if (!repository?.listShieldCases || !owner)
461
        throw new Error('OpenClasp Shield is not configured');
462
      const agentId =
463
        typeof request.headers['x-openclasp-bound-agent'] === 'string'
464
          ? request.headers['x-openclasp-bound-agent']
465
          : undefined;
466
      return repository.listShieldCases(owner, agentId);
467
    });
468
    router.post('/v0.1/shield/cases', async (request) => {
469
      const owner = operatorId(request);
470
      if (!repository?.saveShieldCase || !owner)
471
        throw new Error('OpenClasp Shield is not configured');
472
      const input = ShieldCaseInputSchema.parse(request.body);
473
      enforceBoundAgent(request, input.agentId);
474
      const agent = (await repository.list(owner)).find(
475
        (row) => row.kind === 'agent_profile' && row.recordId === input.agentId,
476
      );
477
      if (!agent) throw new Error('Owned agent not found');
478
      return repository.saveShieldCase(owner, createShieldCase(input));
479
    });
480
    router.get('/v0.1/shield/cases/:id', async (request) => {
481
      const owner = operatorId(request);
482
      if (!repository?.getShieldCase || !repository.listShieldConsultations || !owner)
483
        throw new Error('OpenClasp Shield is not configured');
484
      const caseId = (request.params as { id: string }).id;
485
      const caseRecord = await repository.getShieldCase(owner, caseId);
486
      if (!caseRecord) throw new Error('Shield case not found');
487
      enforceBoundAgent(request, caseRecord.agentId);
488
      const rows = await repository.list(owner);
489
      return {
490
        case: caseRecord,
491
        consultations: await repository.listShieldConsultations(owner, caseId),
492
        outcomes: rows
493
          .filter((row) => row.kind === 'shield_outcome')
494
          .map((row) => ShieldOutcomeSchema.parse(row.payload))
495
          .filter((outcome) => outcome.caseId === caseId),
496
      };
497
    });
498
    router.post('/v0.1/shield/cases/:id/consult', async (request) => {
499
      const owner = operatorId(request);
500
      if (
501
        !repository?.getShieldCase ||
502
        !repository.saveShieldCase ||
503
        !repository.saveShieldConsultation ||
504
        !repository.listShieldConsultations ||
505
        !owner
506
      )
507
        throw new Error('OpenClasp Shield is not configured');
508
      const caseId = (request.params as { id: string }).id;
509
      const caseRecord = await repository.getShieldCase(owner, caseId);
510
      if (!caseRecord) throw new Error('Shield case not found');
511
      enforceBoundAgent(request, caseRecord.agentId);
512
      const input = ShieldConsultInputSchema.parse(request.body);
513
      const previous = await repository.listShieldConsultations(owner, caseId);
514
      const result = await consultShield(caseRecord, input, previous);
515
      await repository.saveShieldConsultation(owner, result.consultation);
516
      await repository.saveShieldCase(owner, result.caseRecord);
517
      return result;
518
    });
519
    router.post('/v0.1/shield/cases/:id/guidance', async (request) => {
520
      const owner = operatorId(request);
521
      if (!repository?.getShieldCase || !repository.saveShieldCase || !owner)
522
        throw new Error('OpenClasp Shield is not configured');
523
      if (request.headers['x-openclasp-credential-type'] === 'agent_access_token') {
524
        const error = new Error('Only the authenticated owner can instruct Shield');
525
        Object.assign(error, { statusCode: 403 });
526
        throw error;
527
      }
528
      const caseId = (request.params as { id: string }).id;
529
      const caseRecord = await repository.getShieldCase(owner, caseId);
530
      if (!caseRecord) throw new Error('Shield case not found');
531
      if (caseRecord.status === 'closed') throw new Error('Shield case is closed');
532
      const input = z
533
        .object({
534
          instruction: z.string().trim().min(1).max(2000),
535
          scope: z.enum(['case', 'agent']).default('case'),
536
        })
537
        .strict()
538
        .parse(request.body);
539
      const guidance = ShieldGuidanceSchema.parse({
540
        guidanceId: randomUUID(),
541
        instruction: input.instruction,
542
        scope: input.scope,
543
        createdAt: new Date().toISOString(),
544
      });
545
      return repository.saveShieldCase(
546
        owner,
547
        ShieldCaseSchema.parse({
548
          ...caseRecord,
549
          ownerGuidance: [...caseRecord.ownerGuidance, guidance],
550
          updatedAt: guidance.createdAt,
551
        }),
552
      );
553
    });
554
    router.post('/v0.1/shield/cases/:id/close', async (request) => {
555
      const owner = operatorId(request);
556
      if (
557
        !repository?.getShieldCase ||
558
        !repository.saveShieldCase ||
559
        !repository.saveShieldOutcome ||
560
        !owner
561
      )
562
        throw new Error('OpenClasp Shield is not configured');
563
      const caseId = (request.params as { id: string }).id;
564
      const caseRecord = await repository.getShieldCase(owner, caseId);
565
      if (!caseRecord) throw new Error('Shield case not found');
566
      if (caseRecord.status === 'closed') throw new Error('Shield case is already closed');
567
      enforceBoundAgent(request, caseRecord.agentId);
568
      const input = z
569
        .object({
570
          result: ShieldOutcomeSchema.shape.result,
571
          acceptedAdvice: z.boolean(),
572
          actionTaken: ShieldOutcomeSchema.shape.actionTaken,
573
          observedImpact: ShieldOutcomeSchema.shape.observedImpact,
574
        })
575
        .strict()
576
        .parse(request.body);
577
      const now = new Date().toISOString();
578
      const outcome = ShieldOutcomeSchema.parse({
579
        protocolVersion: '0.1',
580
        outcomeId: randomUUID(),
581
        caseId,
582
        agentId: caseRecord.agentId,
583
        ...input,
584
        reportedBy:
585
          request.headers['x-openclasp-credential-type'] === 'agent_access_token'
586
            ? 'protected_agent'
587
            : 'owner',
588
        createdAt: now,
589
      });
590
      await repository.saveShieldOutcome(owner, outcome);
591
      await repository.saveShieldCase(
592
        owner,
593
        ShieldCaseSchema.parse({
594
          ...caseRecord,
595
          status: 'closed',
596
          updatedAt: now,
597
          closedAt: now,
598
        }),
599
      );
600
      return outcome;
601
    });
602
    router.get('/v0.1/intelligence', async (request) => {
603
      const owner = operatorId(request);
604
      if (!repository?.listContextualIntelligence || !owner)
605
        throw new Error('Contextual intelligence is not configured');
606
      const input = z
607
        .object({
608
          agentId: z.string().min(1).optional(),
609
          taskCategory: z.string().trim().min(1).max(100).optional(),
610
        })
611
        .parse(request.query);
612
      return repository.listContextualIntelligence(owner, {
613
        ...(input.agentId ? { agentId: input.agentId } : {}),
614
        ...(input.taskCategory ? { taskCategory: input.taskCategory } : {}),
615
      });
616
    });
617
    router.get('/v0.1/marketplace', async (request) => {
618
      const owner = operatorId(request);
619
      if (!repository?.searchPersonalizedMarketplace || !owner)
620
        throw new Error('Personalized marketplace is not configured');
621
      const input = z
622
        .object({
623
          agentId: z.string().min(1).optional(),
624
          taskCategory: z.string().trim().min(1).max(100).optional(),
625
          query: z.string().trim().max(100).optional(),
626
          limit: z.coerce.number().int().min(1).max(50).default(30),
627
        })
628
        .parse(request.query);
629
      const results = await repository.searchPersonalizedMarketplace(owner, {
630
        ...(input.agentId ? { agentId: input.agentId } : {}),
631
        ...(input.taskCategory ? { taskCategory: input.taskCategory } : {}),
632
        ...(input.query ? { query: input.query } : {}),
633
        limit: input.limit,
634
      });
635
      return results.filter(
636
        (result) =>
637
          result.card.agentMode === 'persistent_runtime' &&
638
          result.card.transports.some(
639
            (transport: { managedBy?: string }) => transport.managedBy === 'agent',
640
          ),
641
      );
642
    });
643
    router.get('/v0.1/account', async (request) => {
644
      const owner = operatorId(request);
645
      return {
646
        operatorId: owner,
647
        email: decodeURIComponent(String(request.headers['x-openclasp-email'] ?? '')),
648
        name: decodeURIComponent(String(request.headers['x-openclasp-name'] ?? '')),
649
      };
650
    });
651
    router.get('/v0.1/settings', async (request) => {
652
      const owner = operatorId(request);
653
      if (!repository || !owner)
654
        return {
655
          displayName: '',
656
          contributionEnabled: false,
657
          rawConversationsStored: false,
658
        };
659
      return repository.getSettings(owner);
660
    });
661
    router.get('/v0.1/onboarding', async (request) => {
662
      const owner = operatorId(request);
663
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
664
      return getOnboardingState(repository, owner);
665
    });
666
    router.post('/v0.1/connector-claims', async (request) => {
667
      if (!repository?.createConnectorClaim)
668
        throw new Error('Runtime connector claims are not configured');
669
      const value = z
670
        .object({
671
          runtimeEndpoint: z.string().url().max(2048),
672
          credentialPublicKey: z.string().min(1).max(8192),
673
          profile: ConnectorAgentProfileSchema,
674
        })
675
        .strict()
676
        .parse(request.body);
677
      const claim = await repository.createConnectorClaim(value);
678
      return {
679
        ...claim,
680
        confirmationUrl: `${publicBaseUrl(request)}/connect?claim=${encodeURIComponent(claim.claimId)}`,
681
        pollIntervalSeconds: 3,
682
      };
683
    });
684
    router.get('/v0.1/connector-claims/:id', async (request) => {
685
      if (!repository?.getConnectorClaim)
686
        throw new Error('Runtime connector claims are not configured');
687
      return repository.getConnectorClaim((request.params as { id: string }).id);
688
    });
689
    router.post('/v0.1/connector-claims/:id/poll', async (request) => {
690
      if (!repository?.pollConnectorClaim)
691
        throw new Error('Runtime connector claims are not configured');
692
      const claimSecret = request.headers['x-openclasp-claim-secret'];
693
      if (typeof claimSecret !== 'string') {
694
        const error = new Error('Connector claim secret required');
695
        Object.assign(error, { statusCode: 401 });
696
        throw error;
697
      }
698
      return repository.pollConnectorClaim((request.params as { id: string }).id, claimSecret);
699
    });
700
    router.post('/v0.1/connector-claims/:id/approve', async (request) => {
701
      const owner = operatorId(request);
702
      if (!repository?.approveConnectorClaim || !owner)
703
        throw new Error('Runtime connector claims are not configured');
704
      const { name } = z
705
        .object({ name: z.string().trim().min(1).max(100) })
706
        .strict()
707
        .parse(request.body);
708
      return repository.approveConnectorClaim(owner, (request.params as { id: string }).id, name);
709
    });
710
    router.post('/v0.1/connector-claims/:id/reject', async (request) => {
711
      const owner = operatorId(request);
712
      if (!repository?.rejectConnectorClaim || !owner)
713
        throw new Error('Runtime connector claims are not configured');
714
      return repository.rejectConnectorClaim(owner, (request.params as { id: string }).id);
715
    });
716
    router.post('/v0.1/provider-connections/botpress', async (request) => {
717
      const owner = operatorId(request);
718
      if (!repository?.createProviderConnection || !owner)
719
        throw new Error('Botpress connections are not configured');
720
      const { agentName } = z
721
        .object({ agentName: z.string().trim().min(1).max(100) })
722
        .strict()
723
        .parse(request.body);
724
      return repository.createProviderConnection(owner, 'botpress', agentName);
725
    });
726
    router.get('/v0.1/provider-connections/:id', async (request) => {
727
      const owner = operatorId(request);
728
      if (!repository?.getProviderConnection || !owner)
729
        throw new Error('Provider connections are not configured');
730
      return repository.getProviderConnection(owner, (request.params as { id: string }).id);
731
    });
732
    router.post('/v0.1/provider-connections/botpress/complete', async (request) => {
733
      if (!repository?.completeBotpressConnection)
734
        throw new Error('Botpress connections are not configured');
735
      const code = request.headers['x-openclasp-pairing-code'];
736
      if (typeof code !== 'string' || code.length > 100 || !code.startsWith('oc_bp_')) {
737
        const error = new Error('Botpress pairing code required');
738
        Object.assign(error, { statusCode: 401 });
739
        throw error;
740
      }
741
      const value = z
742
        .object({
743
          runtimeEndpoint: z.string().url().max(2048),
744
          credentialPublicKey: z.string().min(1).max(8192),
745
          profile: ConnectorAgentProfileSchema,
746
        })
747
        .strict()
748
        .parse(request.body);
749
      return repository.completeBotpressConnection(code, value);
750
    });
751
    router.get('/v0.1/runtime/bootstrap', async (request) => {
752
      const owner = operatorId(request);
753
      const agentId = boundAgentId(request);
754
      const rows = owner && repository ? await repository.list(owner) : [];
755
      const profile = rows.find((row) => row.kind === 'agent_profile' && row.recordId === agentId)
756
        ?.payload as AgentProfile | undefined;
757
      return {
758
        agentId,
759
        agentVersion: profile?.agentVersion ?? '1.0.0',
760
        capabilities: profile?.capabilities ?? [],
761
        limitations: profile?.limitations ?? [],
762
        openClaspUrl: publicBaseUrl(request),
763
        runtimeRegistrationEndpoint: `${publicBaseUrl(request)}/v0.1/runtime`,
764
        protocol: 'A2A/1.0',
765
        controlProtocol: 'OpenClasp/0.1',
766
      };
767
    });
768
    router.put('/v0.1/runtime', async (request) => {
769
      const owner = operatorId(request);
770
      if (!repository?.registerAgentRuntime || !owner)
771
        throw new Error('Hosted runtime delivery is not configured');
772
      const endpoint = z
773
        .object({ endpoint: z.string().url().max(2048) })
774
        .parse(request.body).endpoint;
775
      return repository.registerAgentRuntime(owner, boundAgentId(request), endpoint);
776
    });
777
    router.delete('/v0.1/runtime', async (request) => {
778
      const owner = operatorId(request);
779
      if (!repository?.disableAgentRuntime || !owner)
780
        throw new Error('Hosted runtime delivery is not configured');
781
      return repository.disableAgentRuntime(owner, boundAgentId(request));
782
    });
783
    router.post('/v0.1/runtime/heartbeat', async (request) => {
784
      const owner = operatorId(request);
785
      if (!repository?.touchAgentPresence || !owner)
786
        throw new Error('Agent presence is not configured');
787
      return repository.touchAgentPresence(owner, boundAgentId(request));
788
    });
789
    router.put('/v0.1/runtime/profile', async (request) => {
790
      const owner = operatorId(request);
791
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
792
      const agentId = boundAgentId(request);
793
      const value = z
794
        .object({
795
          description: z.string().trim().max(500).optional(),
796
          capabilities: z.array(z.string().trim().min(1).max(100)).min(1).max(100),
797
          limitations: z.array(z.string().trim().min(1).max(300)).max(100).default([]),
798
        })
799
        .strict()
800
        .parse(request.body);
801
      const rows = await repository.list(owner);
802
      const current = rows.find((row) => row.kind === 'agent_profile' && row.recordId === agentId)
803
        ?.payload as AgentProfile | undefined;
804
      if (!current || current.status !== 'active') throw new Error('Active owned agent not found');
805
      const profile: AgentProfile = {
806
        ...current,
807
        ...(value.description !== undefined ? { description: value.description } : {}),
808
        capabilities: [...new Set(value.capabilities)],
809
        limitations: [...new Set(value.limitations)],
810
        updatedAt: new Date().toISOString(),
811
      };
812
      await repository.upsert(owner, 'agent_profile', agentId, profile);
813
      const publication = rows.find((row) => row.kind === 'publication' && row.recordId === agentId)
814
        ?.payload as { published?: boolean } | undefined;
815
      if (profile.autoPublish || publication?.published) {
816
        const previous = await repository.getPublishedAgent(agentId);
817
        const card = await repository.publishAgent(
818
          owner,
819
          buildPublicAgentCard(profile, publicBaseUrl(request), previous),
820
        );
821
        await repository.upsert(owner, 'publication', agentId, publicPublication(card));
822
        return { profile, card };
823
      }
824
      return { profile };
825
    });
826
    router.post('/v0.1/onboarding/:id/approve', async (request) => {
827
      const owner = operatorId(request);
828
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
829
      const requestId = (request.params as { id: string }).id;
830
      const state = await getOnboardingState(repository, owner);
831
      const setup = state.setupRequests.find((item) => item.requestId === requestId);
832
      const result = await approveAgentSetup(repository, owner, requestId);
833
      if (result.status === 'connected' && setup?.autoPublish) {
834
        const previous = await repository.getPublishedAgent(result.agent.agentId);
835
        const card = await repository.publishAgent(
836
          owner,
837
          buildPublicAgentCard(result.agent, publicBaseUrl(request), previous),
838
        );
839
        await repository.upsert(
840
          owner,
841
          'publication',
842
          result.agent.agentId,
843
          publicPublication(card),
844
        );
845
      }
846
      return result;
847
    });
848
    router.post('/v0.1/onboarding/:id/reject', async (request) => {
849
      const owner = operatorId(request);
850
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
851
      return rejectAgentSetup(repository, owner, (request.params as { id: string }).id);
852
    });
853
    router.put('/v0.1/settings', async (request) => {
854
      const owner = operatorId(request);
855
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
856
      const value = z
857
        .object({
858
          displayName: z.string().trim().max(100),
859
          contributionEnabled: z.boolean(),
860
        })
861
        .parse(request.body);
862
      return repository.saveSettings(owner, value);
863
    });
864
    router.post('/v0.1/agents', async (request) => {
865
      const owner = operatorId(request);
866
      const current = await scopedEngine(request);
867
      const identity = AgentIdentitySchema.parse(request.body);
868
      if (owner && identity.operatorRef !== `auth0:${owner}`)
869
        throw new Error('Agent identity is not owned by the authenticated operator');
870
      const registered = current.registerAgent(identity);
871
      await persist(request, 'agent', identity.agentId, registered);
872
      return registered;
873
    });
874
    router.get('/v0.1/agents/:id', async (request) => {
875
      const { id } = request.params as { id: string };
876
      const value = (await scopedEngine(request)).agents.get(id);
877
      if (!value) throw new Error('Agent not found');
878
      return value;
879
    });
880
    router.post('/v0.1/agents/:id/publication', async (request) => {
881
      const owner = operatorId(request);
882
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
883
      const { id } = request.params as { id: string };
884
      const { published } = z.object({ published: z.boolean() }).parse(request.body);
885
      const rows = await repository.list(owner);
886
      const agent = rows.find((row) => row.kind === 'agent_profile' && row.recordId === id)
887
        ?.payload as AgentProfile | undefined;
888
      if (!agent) throw new Error('Owned agent not found');
889
      if (agent.status !== 'active') throw new Error('Revoked agents cannot be published');
890
      if (!published) {
891
        await repository.unpublishAgent(owner, id);
892
        const publication = { agentId: id, published: false, updatedAt: new Date().toISOString() };
893
        await repository.upsert(owner, 'publication', id, publication);
894
        return publication;
895
      }
896
      if (repository.listAgentRuntimes) {
897
        const runtime = (await repository.listAgentRuntimes(owner)).find(
898
          (candidate) => candidate.agentId === id && candidate.status === 'verified',
899
        );
900
        if (!runtime) throw new Error('Verify the agent runtime before publishing');
901
      }
902
      const previous = await repository.getPublishedAgent(id);
903
      const card = await repository.publishAgent(
904
        owner,
905
        buildPublicAgentCard(agent, publicBaseUrl(request), previous),
906
      );
907
      const publication = publicPublication(card);
908
      await repository.upsert(owner, 'publication', id, publication);
909
      return { ...publication, card };
910
    });
911
    router.put('/v0.1/agents/:id/automation', async (request) => {
912
      const owner = operatorId(request);
913
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
914
      const { id } = request.params as { id: string };
915
      const value = z
916
        .object({
917
          autoPublish: z.boolean(),
918
          autoAcceptPolicy: z.enum(['off', 'safe_matching']),
919
          autoAcceptTaskCategories: z.array(z.string().trim().min(1).max(100)).max(100),
920
        })
921
        .parse(request.body);
922
      const rows = await repository.list(owner);
923
      const current = rows.find((row) => row.kind === 'agent_profile' && row.recordId === id)
924
        ?.payload as AgentProfile | undefined;
925
      if (!current) throw new Error('Owned agent not found');
926
      const agent: AgentProfile = {
927
        ...current,
928
        transport: 'direct_a2a',
929
        description: current.description ?? '',
930
        agentVersion: current.agentVersion ?? '1.0.0',
931
        autoPublish: value.autoPublish,
932
        autoAcceptPolicy: value.autoAcceptPolicy,
933
        autoAcceptTaskCategories: [...new Set(value.autoAcceptTaskCategories)],
934
        updatedAt: new Date().toISOString(),
935
      };
936
      await repository.upsert(owner, 'agent_profile', id, agent);
937
      if (value.autoPublish) {
938
        if (repository.listAgentRuntimes) {
939
          const runtime = (await repository.listAgentRuntimes(owner)).find(
940
            (candidate) => candidate.agentId === id && candidate.status === 'verified',
941
          );
942
          if (!runtime) throw new Error('Verify the agent runtime before publishing');
943
        }
944
        const previous = await repository.getPublishedAgent(id);
945
        const card = await repository.publishAgent(
946
          owner,
947
          buildPublicAgentCard(agent, publicBaseUrl(request), previous),
948
        );
949
        await repository.upsert(owner, 'publication', id, publicPublication(card));
950
      } else {
951
        await repository.unpublishAgent(owner, id);
952
        await repository.upsert(owner, 'publication', id, {
953
          agentId: id,
954
          published: false,
955
          updatedAt: new Date().toISOString(),
956
        });
957
      }
958
      return agent;
959
    });
960
    router.delete('/v0.1/agents/:id/runtime', async (request) => {
961
      const owner = operatorId(request);
962
      if (!repository?.disableAgentRuntime || !owner)
963
        throw new Error('Hosted runtime delivery is not configured');
964
      return repository.disableAgentRuntime(owner, (request.params as { id: string }).id);
965
    });
966
    router.get('/v0.1/agents/:id/access-tokens', async (request) => {
967
      const owner = operatorId(request);
968
      if (!repository?.listAgentAccessTokens || !owner)
969
        throw new Error('Agent access tokens are not configured');
970
      return repository.listAgentAccessTokens(owner, (request.params as { id: string }).id);
971
    });
972
    router.post('/v0.1/agents/:id/shield-tokens', async (request) => {
973
      const owner = operatorId(request);
974
      if (!repository?.issueAgentAccessToken || !owner)
975
        throw new Error('Shield access tokens are not configured');
976
      const input = z
977
        .object({
978
          name: z.string().trim().min(1).max(100).default('τ³ benchmark'),
979
          expiresInDays: z.number().int().min(1).max(30).default(7),
980
        })
981
        .strict()
982
        .parse(request.body ?? {});
983
      return repository.issueAgentAccessToken(owner, (request.params as { id: string }).id, {
984
        ...input,
985
        scopes: ['mcp:access', 'profile:read', 'interaction:write', 'feedback:write'],
986
      });
987
    });
988
    router.delete('/v0.1/agents/:id/access-tokens/:tokenId', async (request) => {
989
      const owner = operatorId(request);
990
      if (!repository?.revokeAgentAccessToken || !owner)
991
        throw new Error('Agent access tokens are not configured');
992
      const params = request.params as { id: string; tokenId: string };
993
      return repository.revokeAgentAccessToken(owner, params.id, params.tokenId);
994
    });
995
    router.delete('/v0.1/agents/:id', async (request) => {
996
      const owner = operatorId(request);
997
      if (!repository?.deleteAgent || !owner)
998
        throw new Error('Hosted agent deletion is not configured');
999
      const result = await repository.deleteAgent(owner, (request.params as { id: string }).id);
1000
      engines.delete(owner);
1001
      return result;
1002
    });
1003
    router.get('/v0.1/directory', async (request) => {
1004
      operatorId(request);
1005
      if (!repository) throw new Error('Hosted persistence is not configured');
1006
      const query = z
1007
        .object({
1008
          query: z.string().trim().max(100).optional(),
1009
          capability: z.string().trim().max(100).optional(),
1010
          limit: z.coerce.number().int().min(1).max(50).optional(),
1011
        })
1012
        .parse(request.query);
1013
      return repository.searchPublishedAgents(query);
1014
    });
1015
    router.get('/v0.1/directory/:id', async (request) => {
1016
      operatorId(request);
1017
      if (!repository) throw new Error('Hosted persistence is not configured');
1018
      const card = await repository.getPublishedAgent((request.params as { id: string }).id);
1019
      if (!card) throw new Error('Published agent not found');
1020
      return card;
1021
    });
1022
    router.get('/v0.1/federated-interactions', async (request) => {
1023
      const owner = operatorId(request);
1024
      if (!repository?.listFederatedInteractions || !owner)
1025
        throw new Error('Federated interactions are not configured');
1026
      return repository.listFederatedInteractions(owner);
1027
    });
1028
    router.get('/v0.1/federated-interactions/:id', async (request) => {
1029
      const owner = operatorId(request);
1030
      if (!repository?.getFederatedInteraction || !owner)
1031
        throw new Error('Federated interactions are not configured');
1032
      const interaction = await repository.getFederatedInteraction(
1033
        owner,
1034
        (request.params as { id: string }).id,
1035
      );
1036
      if (!interaction) throw new Error('Interaction not found');
1037
      return interaction;
1038
    });
1039
    router.get('/v0.1/federated-interactions/:id/session', async (request) => {
1040
      const owner = operatorId(request);
1041
      if (!repository?.getLiveSession || !owner)
1042
        throw new Error('Live sessions are not configured');
1043
      const value = z.object({ agentId: z.string().min(1) }).parse(request.query);
1044
      enforceBoundAgent(request, value.agentId);
1045
      return repository.getLiveSession(owner, (request.params as { id: string }).id, value.agentId);
1046
    });
1047
    router.get('/v0.1/federated-interactions/:id/brief', async (request) => {
1048
      const owner = operatorId(request);
1049
      if (!repository?.getCounterpartyBrief || !owner)
1050
        throw new Error('Counterparty briefs are not configured');
1051
      const value = z.object({ agentId: z.string().min(1) }).parse(request.query);
1052
      enforceBoundAgent(request, value.agentId);
1053
      return repository.getCounterpartyBrief(
1054
        owner,
1055
        (request.params as { id: string }).id,
1056
        value.agentId,
1057
      );
1058
    });
1059
    router.get('/v0.1/federated-interactions/:id/assurance-probes', async (request) => {
1060
      const owner = operatorId(request);
1061
      if (!repository?.listAssuranceProbePlans || !owner)
1062
        throw new Error('Assurance probes are not configured');
1063
      const value = z.object({ agentId: z.string().min(1) }).parse(request.query);
1064
      enforceBoundAgent(request, value.agentId);
1065
      return repository.listAssuranceProbePlans(
1066
        owner,
1067
        (request.params as { id: string }).id,
1068
        value.agentId,
1069
      );
1070
    });
1071
    router.post('/v0.1/federated-interactions/:id/assurance-responses', async (request) => {
1072
      const owner = operatorId(request);
1073
      if (!repository?.submitAssuranceProbeResponse || !owner)
1074
        throw new Error('Assurance responses are not configured');
1075
      const response = AssuranceProbeResponseSchema.parse(request.body);
1076
      if (response.interactionId !== (request.params as { id: string }).id)
1077
        throw new Error('Interaction path does not match the assurance response');
1078
      return repository.submitAssuranceProbeResponse(owner, boundAgentId(request), response);
1079
    });
1080
    router.get('/v0.1/federated-interactions/:id/assurance-comparisons', async (request) => {
1081
      const owner = operatorId(request);
1082
      if (!repository?.listAssuranceComparisons || !owner)
1083
        throw new Error('Assurance comparisons are not configured');
1084
      const value = z.object({ agentId: z.string().min(1) }).parse(request.query);
1085
      enforceBoundAgent(request, value.agentId);
1086
      return repository.listAssuranceComparisons(
1087
        owner,
1088
        (request.params as { id: string }).id,
1089
        value.agentId,
1090
      );
1091
    });
1092
    router.get('/v0.1/federated-interactions/:id/assurance-brief', async (request) => {
1093
      const owner = operatorId(request);
1094
      if (!repository?.getAssuranceBrief || !owner)
1095
        throw new Error('Assurance decision briefs are not configured');
1096
      const value = z.object({ agentId: z.string().min(1) }).parse(request.query);
1097
      enforceBoundAgent(request, value.agentId);
1098
      return repository.getAssuranceBrief(
1099
        owner,
1100
        (request.params as { id: string }).id,
1101
        value.agentId,
1102
      );
1103
    });
1104
    router.post(
1105
      '/v0.1/federated-interactions/:id/assurance-safeguards/:safeguardId/decision',
1106
      async (request) => {
1107
        const owner = operatorId(request);
1108
        if (!repository?.decideAssuranceSafeguard || !owner)
1109
          throw new Error('Assurance safeguard decisions are not configured');
1110
        const value = z
1111
          .object({
1112
            agentId: z.string().min(1).optional(),
1113
            status: z.enum(['accepted', 'rejected', 'modified']),
1114
            decisionReason: z.string().trim().max(500).optional(),
1115
          })
1116
          .strict()
1117
          .parse(request.body);
1118
        const params = request.params as { id: string; safeguardId: string };
1119
        const agentId = value.agentId ?? boundAgentId(request);
1120
        if (value.agentId) enforceBoundAgent(request, value.agentId);
1121
        return repository.decideAssuranceSafeguard(
1122
          owner,
1123
          params.id,
1124
          agentId,
1125
          params.safeguardId,
1126
          value.status,
1127
          value.decisionReason,
1128
        );
1129
      },
1130
    );
1131
    router.post('/v0.1/federated-interactions/:id/completion-reports', async (request) => {
1132
      const owner = operatorId(request);
1133
      if (!repository?.submitCompletionReport || !owner)
1134
        throw new Error('Completion reports are not configured');
1135
      const report = InteractionCompletionReportSchema.parse(request.body);
1136
      if (report.interactionId !== (request.params as { id: string }).id)
1137
        throw new Error('Interaction path does not match the completion report');
1138
      return repository.submitCompletionReport(
1139
        owner,
1140
        boundAgentId(request),
1141
        report,
1142
        request.headers['x-openclasp-credential-type'] === 'agent_access_token'
1143
          ? 'agent_access_token'
1144
          : 'oauth_installation',
1145
      );
1146
    });
1147
    router.post('/v0.1/federated-interactions/:id/feedback', async (request) => {
1148
      const owner = operatorId(request);
1149
      if (!repository?.submitInteractionFeedback || !owner)
1150
        throw new Error('Interaction feedback is not configured');
1151
      const feedback = InteractionFeedbackSchema.parse(request.body);
1152
      if (feedback.interactionId !== (request.params as { id: string }).id)
1153
        throw new Error('Interaction path does not match the feedback');
1154
      return repository.submitInteractionFeedback(
1155
        owner,
1156
        boundAgentId(request),
1157
        feedback,
1158
        request.headers['x-openclasp-credential-type'] === 'agent_access_token'
1159
          ? 'agent_access_token'
1160
          : 'oauth_installation',
1161
      );
1162
    });
1163
    router.get('/v0.1/feedback-requests', async (request) => {
1164
      const owner = operatorId(request);
1165
      if (!repository?.listFeedbackRequests || !owner)
1166
        throw new Error('Interaction feedback is not configured');
1167
      const value = z.object({ agentId: z.string().min(1) }).parse(request.query);
1168
      enforceBoundAgent(request, value.agentId);
1169
      return repository.listFeedbackRequests(owner, value.agentId);
1170
    });
1171
    router.post('/v0.1/federated-interactions', async (request) => {
1172
      const owner = operatorId(request);
1173
      if (!repository?.createFederatedInteraction || !owner)
1174
        throw new Error('Federated interactions are not configured');
1175
      const interaction = FederatedInteractionSchema.parse(request.body);
1176
      const acceptance = interaction.acceptances[interaction.initiatorAgentId];
1177
      if (!acceptance) throw new Error('Initiator acceptance is required');
1178
      return repository.createFederatedInteraction(owner, {
1179
        ...interaction,
1180
        acceptances: {
1181
          [interaction.initiatorAgentId]: { ...acceptance, method: 'oauth_account' },
1182
        },
1183
      });
1184
    });
1185
    router.post('/v0.1/federated-interactions/:id/respond', async (request) => {
1186
      const owner = operatorId(request);
1187
      if (!repository?.respondToFederatedInteraction || !owner)
1188
        throw new Error('Federated interactions are not configured');
1189
      const value = z
1190
        .object({ agentId: z.string().min(1), decision: z.enum(['accept', 'reject']) })
1191
        .parse(request.body);
1192
      return repository.respondToFederatedInteraction(
1193
        owner,
1194
        (request.params as { id: string }).id,
1195
        value.agentId,
1196
        value.decision,
1197
        'oauth_account',
1198
      );
1199
    });
1200
    router.post('/v0.1/federated-interactions/:id/contract-proposals', async (request) => {
1201
      const owner = operatorId(request);
1202
      if (!repository?.proposeContractRevision || !owner)
1203
        throw new Error('Contract negotiation is not configured');
1204
      const value = z
1205
        .object({
1206
          agentId: z.string().min(1),
1207
          expectedTermsHash: z.string().min(1).optional(),
1208
          contract: InteractionContractSchema,
1209
        })
1210
        .parse(request.body);
1211
      enforceBoundAgent(request, value.agentId);
1212
      return repository.proposeContractRevision(
1213
        owner,
1214
        (request.params as { id: string }).id,
1215
        value.agentId,
1216
        value.contract,
1217
        value.expectedTermsHash,
1218
        request.headers['x-openclasp-credential-type'] === 'agent_access_token'
1219
          ? 'oauth_installation'
1220
          : 'oauth_account',
1221
      );
1222
    });
1223
    router.post(
1224
      '/v0.1/federated-interactions/:id/contract-proposals/:revisionId/respond',
1225
      async (request) => {
1226
        const owner = operatorId(request);
1227
        if (!repository?.respondToContractRevision || !owner)
1228
          throw new Error('Contract negotiation is not configured');
1229
        const value = z
1230
          .object({ agentId: z.string().min(1), decision: z.enum(['accept', 'reject']) })
1231
          .parse(request.body);
1232
        enforceBoundAgent(request, value.agentId);
1233
        const params = request.params as { id: string; revisionId: string };
1234
        return repository.respondToContractRevision(
1235
          owner,
1236
          params.id,
1237
          value.agentId,
1238
          params.revisionId,
1239
          value.decision,
1240
          request.headers['x-openclasp-credential-type'] === 'agent_access_token'
1241
            ? 'oauth_installation'
1242
            : 'oauth_account',
1243
        );
1244
      },
1245
    );
1246
    router.post('/v0.1/delegations', async (request) => {
1247
      const value = DelegationCredentialSchema.parse(request.body);
1248
      const current = await scopedEngine(request);
1249
      current.delegations.set(value.delegationId, value);
1250
      await persist(request, 'delegation', value.delegationId, value);
1251
      return { valid: current.verifyDelegation(value.delegationId) };
1252
    });
1253
    router.post('/v0.1/delegations/:id/verify', async (request) => ({
1254
      valid: (await scopedEngine(request)).verifyDelegation((request.params as { id: string }).id),
1255
    }));
1256
    router.post('/v0.1/interactions/contracts', async (request) => {
1257
      const contract = (await scopedEngine(request)).saveContract(
1258
        InteractionContractSchema.parse(request.body),
1259
      );
1260
      await persist(request, 'contract', contract.interactionId, contract);
1261
      return contract;
1262
    });
1263
    router.post('/v0.1/events', async (request) => {
1264
      const event = (await scopedEngine(request)).recordEvent(
1265
        InteractionEventSchema.parse(request.body),
1266
      );
1267
      await persist(request, 'event', event.eventId, event);
1268
      return event;
1269
    });
1270
    router.post('/v0.1/risk/assess', async (request) =>
1271
      (await scopedEngine(request)).assess(request.body as any),
1272
    );
1273
    router.get('/v0.1/profiles/:id', async (request) => {
1274
      const { id } = request.params as { id: string };
1275
      const query = request.query as { version?: string; taskCategory?: string };
1276
      return (await scopedEngine(request)).getRisk(
1277
        id,
1278
        query.version ?? '1.0.0',
1279
        query.taskCategory ?? 'general',
1280
      );
1281
    });
1282
    router.post('/v0.1/claims/check', async (request) =>
1283
      ((value) => factChecker.check(value.claim, value.permission))(
1284
        z.object({ claim: z.string(), permission: z.boolean().optional() }).parse(request.body),
1285
      ),
1286
    );
1287
    router.post('/v0.1/receipts', async (request) => {
1288
      const receipt = (await scopedEngine(request)).submitReceipt(
1289
        ReceiptSchema.parse(request.body),
1290
      );
1291
      await persist(request, 'receipt', receipt.receiptId, receipt);
1292
      return receipt;
1293
    });
1294
    router.post('/v0.1/receipts/verify', async (request) => {
1295
      try {
1296
        (await scopedEngine(request)).verifyReceipt(ReceiptSchema.parse(request.body));
1297
        return { valid: true };
1298
      } catch {
1299
        return { valid: false };
1300
      }
1301
    });
1302
    router.post('/v0.1/feedback', async (request) => {
1303
      const feedback = FeedbackSchema.parse(request.body);
1304
      const current = await scopedEngine(request);
1305
      const result = current.submitFeedback(feedback);
1306
      await persist(request, 'feedback', feedback.feedbackId, feedback);
1307
      for (const profile of current.profiles.values())
1308
        await persist(
1309
          request,
1310
          'profile',
1311
          `${profile.agentId}|${profile.agentVersion}|${profile.taskCategory}`,
1312
          profile,
1313
        );
1314
      return result;
1315
    });
1316
    router.post('/v0.1/conflicts', async (request) => {
1317
      const conflict = (await scopedEngine(request)).createConflict(request.body as any);
1318
      await persist(request, 'conflict', conflict.conflictId, conflict);
1319
      return conflict;
1320
    });
1321
    router.post('/v0.1/conflicts/:id/permit', async (request) => {
1322
      const conflict = (await scopedEngine(request)).permitMediation(
1323
        (request.params as { id: string }).id,
1324
        (request.body as { agentId: string }).agentId,
1325
      );
1326
      await persist(request, 'conflict', conflict.conflictId, conflict);
1327
      return conflict;
1328
    });
1329
    router.post('/v0.1/conflicts/:id/resolve', async (request) => {
1330
      const conflict = (await scopedEngine(request)).resolveConflict(
1331
        (request.params as { id: string }).id,
1332
        (request.body as { resolution: string }).resolution,
1333
      );
1334
      await persist(request, 'conflict', conflict.conflictId, conflict);
1335
      return conflict;
1336
    });
1337
    router.post('/v0.1/revocations/agents/:id', async (request) => {
1338
      const id = (request.params as { id: string }).id;
1339
      const current = await scopedEngine(request);
1340
      current.revokeAgent(id);
1341
      await persist(request, 'agent', id, current.agents.get(id));
1342
      return { revoked: true };
1343
    });
1344
    router.post('/v0.1/revocations/delegations/:id', async (request) => {
1345
      const id = (request.params as { id: string }).id;
1346
      const current = await scopedEngine(request);
1347
      current.revokeDelegation(id);
1348
      await persist(request, 'delegation', id, current.delegations.get(id));
1349
      return { revoked: true };
1350
    });
1351
    router.post('/v0.1/contributions/consent', async (request) => {
1352
      const value = z.object({ agentId: z.string(), enabled: z.boolean() }).parse(request.body);
1353
      const current = await scopedEngine(request);
1354
      current.setContributionConsent(value.agentId, value.enabled);
1355
      await persist(request, 'consent', value.agentId, {
1356
        agentId: value.agentId,
1357
        ...current.contributionConsent.get(value.agentId),
1358
      });
1359
      return value;
1360
    });
1361
  });
1362
  return app;
1363
}