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

Viewing files at commit d2f1913e8c83Commit detailsView latest

apps/api/src/app.ts 1150 lines · 58.8 KB · typescript
1
import Fastify from 'fastify';
2
import cors from '@fastify/cors';
3
import swagger from '@fastify/swagger';
4
import { z } from 'zod';
5
import {
6
  AgentIdentitySchema,
7
  DelegationCredentialSchema,
8
  FeedbackSchema,
9
  InteractionContractSchema,
10
  InteractionEventSchema,
11
  ReceiptSchema,
12
  FederatedInteractionSchema,
13
  LiveSessionEventSchema,
14
  InteractionCompletionReportSchema,
15
  InteractionFeedbackSchema,
16
  type PublicAgentCard,
17
} from '../../../packages/protocol/src/index.js';
18
import {
19
  MemoryAuditStore,
20
  TrustEngine,
21
  UnavailableFactCheckProvider,
22
} from '../../../packages/core/src/index.js';
23
import { buildPublicAgentCard } from '../../../packages/persistence/src/index.js';
24
import type { AgentProfile, HostedRepository } from '../../../packages/persistence/src/index.js';
25
import { toA2AAgentCard } from '../../../packages/sidecar/src/index.js';
26
import {
27
  approveAgentSetup,
28
  createDashboardAgent,
29
  createHostedProviderAgent,
30
  getOnboardingState,
31
  rejectAgentSetup,
32
} from '../../../packages/persistence/src/onboarding.js';
33
import { FixedWindowRateLimiter } from './security.js';
34
import { renderAgentCardImage } from './agent-card-image.js';
35
36
type DashboardRepository = Pick<
37
  HostedRepository,
38
  | 'getSettings'
39
  | 'saveSettings'
40
  | 'upsert'
41
  | 'list'
42
  | 'publishAgent'
43
  | 'unpublishAgent'
44
  | 'getPublishedAgent'
45
  | 'resolveAgentReference'
46
  | 'searchPublishedAgents'
47
> & {
48
  dashboard(operatorId: string): Promise<Record<string, unknown>>;
49
} & Partial<
50
    Pick<
51
      HostedRepository,
52
      | 'createFederatedInteraction'
53
      | 'listFederatedInteractions'
54
      | 'getFederatedInteraction'
55
      | 'respondToFederatedInteraction'
56
      | 'proposeContractRevision'
57
      | 'respondToContractRevision'
58
      | 'getLiveSession'
59
      | 'getCounterpartyBrief'
60
      | 'submitCompletionReport'
61
      | 'recordSessionCompletionReport'
62
      | 'submitInteractionFeedback'
63
      | 'recordSessionFeedback'
64
      | 'listFeedbackRequests'
65
      | 'recordLiveSessionEvent'
66
      | 'touchAgentPresence'
67
      | 'getRuntimeVerificationKey'
68
      | 'registerAgentRuntime'
69
      | 'disableAgentRuntime'
70
      | 'deleteAgent'
71
      | 'issueAgentAccessToken'
72
      | 'listAgentAccessTokens'
73
      | 'revokeAgentAccessToken'
74
      | 'receiveTemporaryMessage'
75
      | 'sendTemporaryMessage'
76
      | 'listHostedThreads'
77
      | 'getHostedThread'
78
      | 'markHostedThreadRead'
79
      | 'closeHostedThread'
80
    >
81
  > & {
82
    listContextualIntelligence?: (
83
      operatorId: string,
84
      input?: { agentId?: string; taskCategory?: string },
85
    ) => Promise<any[]>;
86
    searchPersonalizedMarketplace?: (
87
      operatorId: string,
88
      input?: { agentId?: string; taskCategory?: string; query?: string; limit?: number },
89
    ) => Promise<any[]>;
90
  };
91
92
export function buildApi(
93
  engine = new TrustEngine(),
94
  factChecker = new UnavailableFactCheckProvider(),
95
  repository?: DashboardRepository,
96
  security: { internalAuthSecret?: string } = {},
97
) {
98
  const app = Fastify({
99
    bodyLimit: 256 * 1024,
100
    requestTimeout: 30_000,
101
    logger: {
102
      redact: [
103
        'req.headers.authorization',
104
        'req.headers.cookie',
105
        'req.headers.x-openclasp-internal-auth',
106
        'req.body.rawMessage',
107
        'req.body.privateKey',
108
      ],
109
    },
110
  });
111
  const engines = new Map<string, Promise<TrustEngine>>();
112
  const limiter = new FixedWindowRateLimiter();
113
  const allowedOrigin = process.env.OPENCLASP_PUBLIC_URL
114
    ? new URL(process.env.OPENCLASP_PUBLIC_URL).origin
115
    : undefined;
116
  void app.register(cors, {
117
    credentials: Boolean(repository),
118
    origin(origin, callback) {
119
      callback(null, !origin || !allowedOrigin || origin === allowedOrigin);
120
    },
121
  });
122
  void app.register(swagger, { openapi: { info: { title: 'OpenClasp API', version: '0.1.0' } } });
123
124
  app.addHook('onRequest', async (request, reply) => {
125
    const method = request.method.toUpperCase();
126
    if (['GET', 'HEAD', 'OPTIONS'].includes(method)) return;
127
    const operator = request.headers['x-openclasp-operator'];
128
    const bucket = typeof operator === 'string' ? `operator:${operator}` : `ip:${request.ip}`;
129
    const externalSession = request.url.startsWith('/sessions/') || request.url.startsWith('/a2a/');
130
    const result = limiter.consume(bucket, externalSession ? 300 : 120, 60_000);
131
    reply.header('x-ratelimit-remaining', result.remaining);
132
    if (!result.allowed)
133
      return reply
134
        .status(429)
135
        .header('retry-after', result.retryAfterSeconds)
136
        .send({ error: 'rate_limit_exceeded' });
137
  });
138
139
  app.addHook('onSend', async (_request, reply, payload) => {
140
    reply.header('x-content-type-options', 'nosniff');
141
    reply.header('referrer-policy', 'no-referrer');
142
    reply.header('permissions-policy', 'camera=(), microphone=(), geolocation=()');
143
    return payload;
144
  });
145
146
  app.setErrorHandler((error, _request, reply) =>
147
    reply
148
      .status(
149
        error instanceof Error && 'statusCode' in error && typeof error.statusCode === 'number'
150
          ? error.statusCode
151
          : 400,
152
      )
153
      .send({ error: error instanceof Error ? error.message : 'Unknown error' }),
154
  );
155
  void app.register(async (router) => {
156
    const operatorId = (request: { headers: Record<string, unknown> }) => {
157
      const value = request.headers['x-openclasp-operator'];
158
      if (
159
        repository &&
160
        security.internalAuthSecret &&
161
        request.headers['x-openclasp-internal-auth'] !== security.internalAuthSecret
162
      ) {
163
        const error = new Error('Trusted authentication boundary required');
164
        Object.assign(error, { statusCode: 401 });
165
        throw error;
166
      }
167
      if (repository && typeof value !== 'string') {
168
        const error = new Error('Authentication required');
169
        Object.assign(error, { statusCode: 401 });
170
        throw error;
171
      }
172
      return typeof value === 'string' ? value : undefined;
173
    };
174
    const boundAgentId = (request: { headers: Record<string, unknown> }) => {
175
      const value = request.headers['x-openclasp-bound-agent'];
176
      if (typeof value !== 'string' || !value) {
177
        const error = new Error('Agent-bound authentication required');
178
        Object.assign(error, { statusCode: 401 });
179
        throw error;
180
      }
181
      return value;
182
    };
183
    const enforceBoundAgent = (
184
      request: { headers: Record<string, unknown> },
185
      requestedAgentId: string,
186
    ) => {
187
      const value = request.headers['x-openclasp-bound-agent'];
188
      if (typeof value === 'string' && value !== requestedAgentId) {
189
        const error = new Error('Agent credential cannot access another agent');
190
        Object.assign(error, { statusCode: 403 });
191
        throw error;
192
      }
193
    };
194
    const scopedEngine = async (request: { headers: Record<string, unknown> }) => {
195
      const owner = operatorId(request);
196
      if (!repository || !owner) return engine;
197
      let pending = engines.get(owner);
198
      if (!pending) {
199
        pending = repository.list(owner).then((rows) => {
200
          const store = new MemoryAuditStore();
201
          for (const row of rows) {
202
            if (row.kind !== 'interaction') store.append(row.kind, row.recordId, row.payload);
203
          }
204
          return new TrustEngine(store);
205
        });
206
        engines.set(owner, pending);
207
      }
208
      return pending;
209
    };
210
    const persist = async (
211
      request: { headers: Record<string, unknown> },
212
      kind: Parameters<HostedRepository['upsert']>[1],
213
      id: string,
214
      value: unknown,
215
    ) => {
216
      const owner = operatorId(request);
217
      if (repository && owner) await repository.upsert(owner, kind, id, value);
218
    };
219
    const publicBaseUrl = (request: { headers: Record<string, unknown> }) => {
220
      const forwardedHost = request.headers['x-forwarded-host'];
221
      const host = typeof forwardedHost === 'string' ? forwardedHost : request.headers.host;
222
      const forwardedProtocol = request.headers['x-forwarded-proto'];
223
      const protocol = typeof forwardedProtocol === 'string' ? forwardedProtocol : 'https';
224
      return process.env.OPENCLASP_PUBLIC_URL ?? `${protocol}://${String(host ?? 'localhost')}`;
225
    };
226
    const publicPublication = (card: PublicAgentCard) => ({
227
      agentId: card.agentId,
228
      published: true,
229
      profileUrl: card.profileUrl,
230
      cardUrl: card.cardUrl,
231
      a2aAgentCardUrl: card.a2aAgentCardUrl,
232
      verification: card.verification,
233
      updatedAt: card.updatedAt,
234
    });
235
    const escapeHtml = (value: string) =>
236
      value.replace(/[&<>"']/g, (character) => {
237
        const entities: Record<string, string> = {
238
          '&': '&amp;',
239
          '<': '&lt;',
240
          '>': '&gt;',
241
          '"': '&quot;',
242
          "'": '&#39;',
243
        };
244
        return entities[character]!;
245
      });
246
    router.get('/health', async () => ({ status: 'ok' }));
247
    router.get('/ready', async () => ({ status: 'ready' }));
248
    router.get('/openapi.json', async () => app.swagger());
249
    router.get('/.well-known/openclasp-session-key', async () => {
250
      if (!repository?.getRuntimeVerificationKey)
251
        throw new Error('Runtime verification is not configured');
252
      return repository.getRuntimeVerificationKey();
253
    });
254
    router.get('/extensions/trust/v0.1', async () => ({
255
      uri: 'https://openclasp.vercel.app/extensions/trust/v0.1',
256
      name: 'OpenClasp A2A assurance extension',
257
      version: '0.1',
258
      required: false,
259
      transportsMessages: false,
260
      temporaryChatAdapter: true,
261
      documentation: 'https://github.com/aryaamin/OpenClasp/blob/main/docs/A2A_EXTENSION.md',
262
    }));
263
    router.post('/sessions/:id/events', async (request, reply) => {
264
      if (!repository?.recordLiveSessionEvent)
265
        throw new Error('Live-session reporting is not configured');
266
      const authorization = request.headers.authorization;
267
      if (!authorization?.startsWith('Bearer ')) {
268
        reply.status(401);
269
        return { error: 'session_credential_required' };
270
      }
271
      const event = LiveSessionEventSchema.parse(request.body);
272
      if (event.interactionId !== (request.params as { id: string }).id)
273
        throw new Error('Interaction path does not match the event');
274
      return repository.recordLiveSessionEvent(authorization.slice(7), event);
275
    });
276
    router.post('/sessions/:id/completion-reports', async (request, reply) => {
277
      if (!repository?.recordSessionCompletionReport)
278
        throw new Error('Live-session completion reporting is not configured');
279
      const authorization = request.headers.authorization;
280
      if (!authorization?.startsWith('Bearer ')) {
281
        reply.status(401);
282
        return { error: 'session_credential_required' };
283
      }
284
      const report = InteractionCompletionReportSchema.parse(request.body);
285
      if (report.interactionId !== (request.params as { id: string }).id)
286
        throw new Error('Interaction path does not match the completion report');
287
      return repository.recordSessionCompletionReport(authorization.slice(7), report);
288
    });
289
    router.post('/sessions/:id/feedback', async (request, reply) => {
290
      if (!repository?.recordSessionFeedback)
291
        throw new Error('Live-session feedback is not configured');
292
      const authorization = request.headers.authorization;
293
      if (!authorization?.startsWith('Bearer ')) {
294
        reply.status(401);
295
        return { error: 'session_credential_required' };
296
      }
297
      const feedback = InteractionFeedbackSchema.parse(request.body);
298
      if (feedback.interactionId !== (request.params as { id: string }).id)
299
        throw new Error('Interaction path does not match the feedback');
300
      return repository.recordSessionFeedback(authorization.slice(7), feedback);
301
    });
302
    router.post('/a2a/temporary/:id', async (request, reply) => {
303
      if (!repository?.receiveTemporaryMessage)
304
        throw new Error('Temporary chat delivery is not configured');
305
      const authorization = request.headers.authorization;
306
      if (!authorization?.startsWith('Bearer '))
307
        return reply.status(401).send({ error: 'session_credential_required' });
308
      const rpc = z
309
        .object({
310
          jsonrpc: z.literal('2.0'),
311
          id: z.union([z.string().min(1).max(200), z.number()]),
312
          method: z.literal('message/send'),
313
          params: z.object({
314
            message: z.object({
315
              parts: z.array(z.record(z.string(), z.unknown())).min(1).max(100),
316
            }),
317
          }),
318
        })
319
        .parse(request.body);
320
      const content = rpc.params.message.parts
321
        .map((part) => (typeof part.text === 'string' ? part.text : ''))
322
        .filter(Boolean)
323
        .join('\n')
324
        .trim();
325
      if (!content) throw new Error('Temporary chat MVP accepts text parts only');
326
      const recipientAgentId = (request.params as { id: string }).id;
327
      const result = await repository.receiveTemporaryMessage(
328
        authorization.slice(7),
329
        recipientAgentId,
330
        String(rpc.id),
331
        content,
332
      );
333
      return {
334
        jsonrpc: '2.0',
335
        id: rpc.id,
336
        result: {
337
          task: { id: result.message.messageId, state: 'submitted' },
338
          privacyMode: 'openclasp_hosted_temporary',
339
          deduplicated: result.deduplicated,
340
        },
341
      };
342
    });
343
    router.get('/agents/:id/card.json', async (request) => {
344
      if (!repository) throw new Error('Hosted persistence is not configured');
345
      const card = await repository.getPublishedAgent((request.params as { id: string }).id);
346
      if (!card) throw new Error('Published agent not found');
347
      return card;
348
    });
349
    router.get('/agents/:id/.well-known/openclasp-agent.json', async (request) => {
350
      if (!repository) throw new Error('Hosted persistence is not configured');
351
      const card = await repository.getPublishedAgent((request.params as { id: string }).id);
352
      if (!card) throw new Error('Published agent not found');
353
      return card;
354
    });
355
    router.get('/agents/:id/a2a-agent-card.json', async (request) => {
356
      if (!repository) throw new Error('Hosted persistence is not configured');
357
      const card = await repository.getPublishedAgent((request.params as { id: string }).id);
358
      if (!card) throw new Error('Published agent not found');
359
      if (!card.transports.length) throw new Error('Agent has not published an A2A endpoint');
360
      return toA2AAgentCard(card);
361
    });
362
    router.get('/agents/:id/og.png', async (request, reply) => {
363
      if (!repository) throw new Error('Hosted persistence is not configured');
364
      const card = await repository.getPublishedAgent((request.params as { id: string }).id);
365
      if (!card) throw new Error('Published agent not found');
366
      return reply
367
        .type('image/png')
368
        .header('Cache-Control', 'public, max-age=0, s-maxage=86400, stale-while-revalidate=604800')
369
        .send(await renderAgentCardImage(card));
370
    });
371
    router.get('/directory/resolve', async (request) => {
372
      if (!repository) throw new Error('Hosted persistence is not configured');
373
      const { reference } = z
374
        .object({ reference: z.string().min(1).max(2048) })
375
        .parse(request.query);
376
      const result = await repository.resolveAgentReference(reference);
377
      if (!result) throw new Error('Published agent not found');
378
      return result;
379
    });
380
    router.get('/directory/search', async (request) => {
381
      if (!repository) throw new Error('Hosted persistence is not configured');
382
      const input = z
383
        .object({
384
          query: z.string().trim().max(100).optional(),
385
          capability: z.string().trim().max(100).optional(),
386
          limit: z.coerce.number().int().min(1).max(50).optional(),
387
        })
388
        .parse(request.query);
389
      return repository.searchPublishedAgents(input);
390
    });
391
    router.get('/a/:reference', async (request, reply) => {
392
      if (!repository) throw new Error('Hosted persistence is not configured');
393
      const result = await repository.resolveAgentReference(
394
        (request.params as { reference: string }).reference,
395
      );
396
      if (!result) throw new Error('Published agent not found');
397
      const { card } = result;
398
      const socialImageUrl = `${publicBaseUrl(request)}/agents/${encodeURIComponent(card.agentId)}/og.png`;
399
      const capabilities = card.capabilities
400
        .map((capability) => `<li>${escapeHtml(capability)}</li>`)
401
        .join('');
402
      const limitations = card.limitations
403
        .map((limitation) => `<li>${escapeHtml(limitation)}</li>`)
404
        .join('');
405
      const cloudAgent =
406
        card.agentMode === 'persistent_runtime' &&
407
        card.transports.some((transport) => transport.managedBy === 'agent');
408
      const runtimeLabel = cloudAgent ? 'Cloud runtime' : 'Temporary identity';
409
      const presence = card.presence?.status ?? 'offline';
410
      const structured = JSON.stringify({
411
        '@context': 'https://schema.org',
412
        '@type': 'SoftwareApplication',
413
        name: card.name,
414
        description: card.description,
415
        url: card.profileUrl,
416
      }).replace(/</g, '\\u003c');
417
      return reply.type('text/html; charset=utf-8').send(`<!doctype html>
418
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
419
<title>${escapeHtml(card.name)} · OpenClasp</title><meta name="description" content="${escapeHtml(card.description)}">
420
<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)}">
421
<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`)}">
422
<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)}">
423
<meta name="theme-color" content="#100b0a"><meta property="og:site_name" content="OpenClasp">
424
<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>
425
<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>
426
<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' ? '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>
427
<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>
428
<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>
429
<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>`);
430
    });
431
    router.get('/v0.1/dashboard', async (request) => {
432
      const owner = operatorId(request);
433
      if (repository && owner) return repository.dashboard(owner);
434
      return {
435
        agents: [...engine.agents.values()],
436
        projects: [],
437
        installations: [],
438
        setupRequests: [],
439
        publications: [],
440
        interactions: [],
441
        federatedInteractions: [],
442
        liveSessions: [],
443
        hostedThreads: [],
444
        events: [...engine.events.values()],
445
        conflicts: [...engine.conflicts.values()],
446
        receipts: [...engine.receipts.values()],
447
        profiles: [...engine.profiles.values()],
448
        counterpartyBriefs: [],
449
        completionReports: [],
450
        feedbackRequests: [],
451
        interactionFeedback: [],
452
        interactionConclusions: [],
453
        learningEligibility: [],
454
        profileDeltas: [],
455
        intelligenceSummaries: [],
456
        runtimes: [],
457
      };
458
    });
459
    router.get('/v0.1/intelligence', async (request) => {
460
      const owner = operatorId(request);
461
      if (!repository?.listContextualIntelligence || !owner)
462
        throw new Error('Contextual intelligence is not configured');
463
      const input = z
464
        .object({
465
          agentId: z.string().min(1).optional(),
466
          taskCategory: z.string().trim().min(1).max(100).optional(),
467
        })
468
        .parse(request.query);
469
      return repository.listContextualIntelligence(owner, {
470
        ...(input.agentId ? { agentId: input.agentId } : {}),
471
        ...(input.taskCategory ? { taskCategory: input.taskCategory } : {}),
472
      });
473
    });
474
    router.get('/v0.1/marketplace', async (request) => {
475
      const owner = operatorId(request);
476
      if (!repository?.searchPersonalizedMarketplace || !owner)
477
        throw new Error('Personalized marketplace is not configured');
478
      const input = z
479
        .object({
480
          agentId: z.string().min(1).optional(),
481
          taskCategory: z.string().trim().min(1).max(100).optional(),
482
          query: z.string().trim().max(100).optional(),
483
          limit: z.coerce.number().int().min(1).max(50).default(30),
484
        })
485
        .parse(request.query);
486
      const results = await repository.searchPersonalizedMarketplace(owner, {
487
        ...(input.agentId ? { agentId: input.agentId } : {}),
488
        ...(input.taskCategory ? { taskCategory: input.taskCategory } : {}),
489
        ...(input.query ? { query: input.query } : {}),
490
        limit: input.limit,
491
      });
492
      return results.filter(
493
        (result) =>
494
          result.card.agentMode === 'persistent_runtime' &&
495
          result.card.transports.some(
496
            (transport: { managedBy?: string }) => transport.managedBy === 'agent',
497
          ),
498
      );
499
    });
500
    router.get('/v0.1/account', async (request) => {
501
      const owner = operatorId(request);
502
      return {
503
        operatorId: owner,
504
        email: decodeURIComponent(String(request.headers['x-openclasp-email'] ?? '')),
505
        name: decodeURIComponent(String(request.headers['x-openclasp-name'] ?? '')),
506
      };
507
    });
508
    router.get('/v0.1/settings', async (request) => {
509
      const owner = operatorId(request);
510
      if (!repository || !owner)
511
        return {
512
          displayName: '',
513
          contributionEnabled: false,
514
          retentionDays: 30,
515
          evidenceSharing: 'ask',
516
          rawConversationsStored: false,
517
        };
518
      return repository.getSettings(owner);
519
    });
520
    router.get('/v0.1/onboarding', async (request) => {
521
      const owner = operatorId(request);
522
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
523
      return getOnboardingState(repository, owner);
524
    });
525
    router.get('/v0.1/runtime/bootstrap', async (request) => {
526
      const owner = operatorId(request);
527
      const agentId = boundAgentId(request);
528
      const rows = owner && repository ? await repository.list(owner) : [];
529
      const profile = rows.find((row) => row.kind === 'agent_profile' && row.recordId === agentId)
530
        ?.payload as AgentProfile | undefined;
531
      return {
532
        agentId,
533
        agentVersion: profile?.agentVersion ?? '1.0.0',
534
        capabilities: profile?.capabilities ?? [],
535
        limitations: profile?.limitations ?? [],
536
        openClaspUrl: publicBaseUrl(request),
537
        runtimeRegistrationEndpoint: `${publicBaseUrl(request)}/v0.1/runtime`,
538
        protocol: 'A2A/1.0',
539
        controlProtocol: 'OpenClasp/0.1',
540
      };
541
    });
542
    router.put('/v0.1/runtime', async (request) => {
543
      const owner = operatorId(request);
544
      if (!repository?.registerAgentRuntime || !owner)
545
        throw new Error('Hosted runtime delivery is not configured');
546
      const endpoint = z
547
        .object({ endpoint: z.string().url().max(2048) })
548
        .parse(request.body).endpoint;
549
      return repository.registerAgentRuntime(owner, boundAgentId(request), endpoint);
550
    });
551
    router.delete('/v0.1/runtime', async (request) => {
552
      const owner = operatorId(request);
553
      if (!repository?.disableAgentRuntime || !owner)
554
        throw new Error('Hosted runtime delivery is not configured');
555
      return repository.disableAgentRuntime(owner, boundAgentId(request));
556
    });
557
    router.post('/v0.1/runtime/heartbeat', async (request) => {
558
      const owner = operatorId(request);
559
      if (!repository?.touchAgentPresence || !owner)
560
        throw new Error('Agent presence is not configured');
561
      return repository.touchAgentPresence(owner, boundAgentId(request));
562
    });
563
    router.put('/v0.1/runtime/profile', async (request) => {
564
      const owner = operatorId(request);
565
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
566
      const agentId = boundAgentId(request);
567
      const value = z
568
        .object({
569
          description: z.string().trim().max(500).optional(),
570
          capabilities: z.array(z.string().trim().min(1).max(100)).min(1).max(100),
571
          limitations: z.array(z.string().trim().min(1).max(300)).max(100).default([]),
572
        })
573
        .strict()
574
        .parse(request.body);
575
      const rows = await repository.list(owner);
576
      const current = rows.find((row) => row.kind === 'agent_profile' && row.recordId === agentId)
577
        ?.payload as AgentProfile | undefined;
578
      if (!current || current.status !== 'active') throw new Error('Active owned agent not found');
579
      const profile: AgentProfile = {
580
        ...current,
581
        ...(value.description !== undefined ? { description: value.description } : {}),
582
        capabilities: [...new Set(value.capabilities)],
583
        limitations: [...new Set(value.limitations)],
584
        updatedAt: new Date().toISOString(),
585
      };
586
      await repository.upsert(owner, 'agent_profile', agentId, profile);
587
      const publication = rows.find((row) => row.kind === 'publication' && row.recordId === agentId)
588
        ?.payload as { published?: boolean } | undefined;
589
      if (profile.autoPublish || publication?.published) {
590
        const previous = await repository.getPublishedAgent(agentId);
591
        const card = await repository.publishAgent(
592
          owner,
593
          buildPublicAgentCard(profile, publicBaseUrl(request), previous),
594
        );
595
        await repository.upsert(owner, 'publication', agentId, publicPublication(card));
596
        return { profile, card };
597
      }
598
      return { profile };
599
    });
600
    router.post('/v0.1/onboarding/:id/approve', async (request) => {
601
      const owner = operatorId(request);
602
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
603
      const requestId = (request.params as { id: string }).id;
604
      const state = await getOnboardingState(repository, owner);
605
      const setup = state.setupRequests.find((item) => item.requestId === requestId);
606
      const result = await approveAgentSetup(repository, owner, requestId);
607
      if (result.status === 'connected' && setup?.autoPublish) {
608
        const previous = await repository.getPublishedAgent(result.agent.agentId);
609
        const card = await repository.publishAgent(
610
          owner,
611
          buildPublicAgentCard(result.agent, publicBaseUrl(request), previous),
612
        );
613
        await repository.upsert(
614
          owner,
615
          'publication',
616
          result.agent.agentId,
617
          publicPublication(card),
618
        );
619
      }
620
      return result;
621
    });
622
    router.post('/v0.1/onboarding/:id/reject', async (request) => {
623
      const owner = operatorId(request);
624
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
625
      return rejectAgentSetup(repository, owner, (request.params as { id: string }).id);
626
    });
627
    router.put('/v0.1/settings', async (request) => {
628
      const owner = operatorId(request);
629
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
630
      const value = z
631
        .object({
632
          displayName: z.string().trim().max(100),
633
          contributionEnabled: z.boolean(),
634
          retentionDays: z.number().int().min(0).max(3650),
635
          evidenceSharing: z.enum(['never', 'ask', 'contract_only']),
636
        })
637
        .parse(request.body);
638
      return repository.saveSettings(owner, value);
639
    });
640
    router.post('/v0.1/provider-connections', async (request) => {
641
      const owner = operatorId(request);
642
      if (!repository?.issueAgentAccessToken || !owner)
643
        throw new Error('Hosted provider connections are not configured');
644
      const value = z
645
        .object({
646
          provider: z.enum(['botpress', 'custom']),
647
          agentName: z.string().trim().min(1).max(100),
648
          projectName: z.string().trim().min(1).max(100),
649
          description: z.string().trim().min(1).max(500),
650
          capabilities: z.array(z.string().trim().min(1).max(100)).min(1).max(100),
651
          limitations: z.array(z.string().trim().min(1).max(300)).max(100).default([]),
652
          expiresInDays: z.number().int().min(1).max(365).default(365),
653
        })
654
        .parse(request.body);
655
      const created = await createHostedProviderAgent(repository, owner, value);
656
      try {
657
        const accessToken = await repository.issueAgentAccessToken(owner, created.agent.agentId, {
658
          name: 'Botpress',
659
          expiresInDays: value.expiresInDays,
660
        });
661
        return { ...created, provider: value.provider, accessToken };
662
      } catch (error) {
663
        if (repository.deleteAgent)
664
          await repository.deleteAgent(owner, created.agent.agentId).catch(() => undefined);
665
        throw error;
666
      }
667
    });
668
    router.post('/v0.1/quickstart/agent', async (request) => {
669
      const owner = operatorId(request);
670
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
671
      const value = z
672
        .object({
673
          agentName: z.string().trim().min(1).max(100),
674
          projectName: z.string().trim().min(1).max(100).default('My agents'),
675
          description: z.string().trim().min(1).max(500),
676
          framework: z.string().trim().min(1).max(100).optional(),
677
          capabilities: z.array(z.string().trim().min(1).max(100)).min(1).max(20),
678
          limitations: z.array(z.string().trim().min(1).max(300)).max(20).default([]),
679
        })
680
        .strict()
681
        .parse(request.body);
682
      const created = await createDashboardAgent(repository, owner, value);
683
      return created;
684
    });
685
    router.post('/v0.1/agents', async (request) => {
686
      const owner = operatorId(request);
687
      const current = await scopedEngine(request);
688
      const identity = AgentIdentitySchema.parse(request.body);
689
      if (owner && identity.operatorRef !== `auth0:${owner}`)
690
        throw new Error('Agent identity is not owned by the authenticated operator');
691
      const registered = current.registerAgent(identity);
692
      await persist(request, 'agent', identity.agentId, registered);
693
      return registered;
694
    });
695
    router.get('/v0.1/agents/:id', async (request) => {
696
      const { id } = request.params as { id: string };
697
      const value = (await scopedEngine(request)).agents.get(id);
698
      if (!value) throw new Error('Agent not found');
699
      return value;
700
    });
701
    router.post('/v0.1/agents/:id/publication', async (request) => {
702
      const owner = operatorId(request);
703
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
704
      const { id } = request.params as { id: string };
705
      const { published } = z.object({ published: z.boolean() }).parse(request.body);
706
      const rows = await repository.list(owner);
707
      const agent = rows.find((row) => row.kind === 'agent_profile' && row.recordId === id)
708
        ?.payload as AgentProfile | undefined;
709
      if (!agent) throw new Error('Owned agent not found');
710
      if (agent.status !== 'active') throw new Error('Revoked agents cannot be published');
711
      if (!published) {
712
        await repository.unpublishAgent(owner, id);
713
        const publication = { agentId: id, published: false, updatedAt: new Date().toISOString() };
714
        await repository.upsert(owner, 'publication', id, publication);
715
        return publication;
716
      }
717
      const previous = await repository.getPublishedAgent(id);
718
      const card = await repository.publishAgent(
719
        owner,
720
        buildPublicAgentCard(agent, publicBaseUrl(request), previous),
721
      );
722
      const publication = publicPublication(card);
723
      await repository.upsert(owner, 'publication', id, publication);
724
      return { ...publication, card };
725
    });
726
    router.put('/v0.1/agents/:id/automation', async (request) => {
727
      const owner = operatorId(request);
728
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
729
      const { id } = request.params as { id: string };
730
      const value = z
731
        .object({
732
          autoPublish: z.boolean(),
733
          autoAcceptPolicy: z.enum(['off', 'safe_matching']),
734
          autoAcceptTaskCategories: z.array(z.string().trim().min(1).max(100)).max(100),
735
        })
736
        .parse(request.body);
737
      const rows = await repository.list(owner);
738
      const current = rows.find((row) => row.kind === 'agent_profile' && row.recordId === id)
739
        ?.payload as AgentProfile | undefined;
740
      if (!current) throw new Error('Owned agent not found');
741
      const agent: AgentProfile = {
742
        ...current,
743
        transport: 'direct_a2a',
744
        description: current.description ?? '',
745
        agentVersion: current.agentVersion ?? '1.0.0',
746
        autoPublish: value.autoPublish,
747
        autoAcceptPolicy: value.autoAcceptPolicy,
748
        autoAcceptTaskCategories: [...new Set(value.autoAcceptTaskCategories)],
749
        updatedAt: new Date().toISOString(),
750
      };
751
      await repository.upsert(owner, 'agent_profile', id, agent);
752
      if (value.autoPublish) {
753
        const previous = await repository.getPublishedAgent(id);
754
        const card = await repository.publishAgent(
755
          owner,
756
          buildPublicAgentCard(agent, publicBaseUrl(request), previous),
757
        );
758
        await repository.upsert(owner, 'publication', id, publicPublication(card));
759
      } else {
760
        await repository.unpublishAgent(owner, id);
761
        await repository.upsert(owner, 'publication', id, {
762
          agentId: id,
763
          published: false,
764
          updatedAt: new Date().toISOString(),
765
        });
766
      }
767
      return agent;
768
    });
769
    router.put('/v0.1/agents/:id/runtime', async (request) => {
770
      const owner = operatorId(request);
771
      if (!repository?.registerAgentRuntime || !owner)
772
        throw new Error('Hosted runtime delivery is not configured');
773
      const endpoint = z
774
        .object({ endpoint: z.string().url().max(2048) })
775
        .parse(request.body).endpoint;
776
      return repository.registerAgentRuntime(
777
        owner,
778
        (request.params as { id: string }).id,
779
        endpoint,
780
      );
781
    });
782
    router.delete('/v0.1/agents/:id/runtime', async (request) => {
783
      const owner = operatorId(request);
784
      if (!repository?.disableAgentRuntime || !owner)
785
        throw new Error('Hosted runtime delivery is not configured');
786
      return repository.disableAgentRuntime(owner, (request.params as { id: string }).id);
787
    });
788
    router.get('/v0.1/agents/:id/access-tokens', async (request) => {
789
      const owner = operatorId(request);
790
      if (!repository?.listAgentAccessTokens || !owner)
791
        throw new Error('Agent access tokens are not configured');
792
      return repository.listAgentAccessTokens(owner, (request.params as { id: string }).id);
793
    });
794
    router.post('/v0.1/agents/:id/access-tokens', async (request) => {
795
      const owner = operatorId(request);
796
      if (!repository?.issueAgentAccessToken || !owner)
797
        throw new Error('Agent access tokens are not configured');
798
      const value = z
799
        .object({
800
          name: z.string().trim().min(1).max(100),
801
          expiresInDays: z.number().int().min(1).max(365).default(365),
802
        })
803
        .parse(request.body);
804
      return repository.issueAgentAccessToken(owner, (request.params as { id: string }).id, value);
805
    });
806
    router.delete('/v0.1/agents/:id/access-tokens/:tokenId', async (request) => {
807
      const owner = operatorId(request);
808
      if (!repository?.revokeAgentAccessToken || !owner)
809
        throw new Error('Agent access tokens are not configured');
810
      const params = request.params as { id: string; tokenId: string };
811
      return repository.revokeAgentAccessToken(owner, params.id, params.tokenId);
812
    });
813
    router.delete('/v0.1/agents/:id', async (request) => {
814
      const owner = operatorId(request);
815
      if (!repository?.deleteAgent || !owner)
816
        throw new Error('Hosted agent deletion is not configured');
817
      const result = await repository.deleteAgent(owner, (request.params as { id: string }).id);
818
      engines.delete(owner);
819
      return result;
820
    });
821
    router.get('/v0.1/agents/:id/threads', async (request) => {
822
      const owner = operatorId(request);
823
      if (!repository?.listHostedThreads || !owner)
824
        throw new Error('Temporary chat history is not configured');
825
      return repository.listHostedThreads(owner, (request.params as { id: string }).id);
826
    });
827
    router.get('/v0.1/agents/:id/threads/:threadId', async (request) => {
828
      const owner = operatorId(request);
829
      if (!repository?.getHostedThread || !owner)
830
        throw new Error('Temporary chat history is not configured');
831
      const params = request.params as { id: string; threadId: string };
832
      return repository.getHostedThread(owner, params.id, params.threadId);
833
    });
834
    router.post('/v0.1/agents/:id/messages', async (request) => {
835
      const owner = operatorId(request);
836
      if (!repository?.sendTemporaryMessage || !owner)
837
        throw new Error('Temporary chat delivery is not configured');
838
      const value = z
839
        .object({ interactionId: z.string().uuid(), content: z.string().trim().min(1).max(20_000) })
840
        .parse(request.body);
841
      return repository.sendTemporaryMessage(
842
        owner,
843
        (request.params as { id: string }).id,
844
        value.interactionId,
845
        value.content,
846
      );
847
    });
848
    router.post('/v0.1/agents/:id/threads/:threadId/read', async (request) => {
849
      const owner = operatorId(request);
850
      if (!repository?.markHostedThreadRead || !owner)
851
        throw new Error('Temporary chat history is not configured');
852
      const params = request.params as { id: string; threadId: string };
853
      return repository.markHostedThreadRead(owner, params.id, params.threadId);
854
    });
855
    router.post('/v0.1/agents/:id/threads/:threadId/close', async (request) => {
856
      const owner = operatorId(request);
857
      if (!repository?.closeHostedThread || !owner)
858
        throw new Error('Temporary chat history is not configured');
859
      const params = request.params as { id: string; threadId: string };
860
      return repository.closeHostedThread(owner, params.id, params.threadId);
861
    });
862
    router.get('/v0.1/directory', async (request) => {
863
      operatorId(request);
864
      if (!repository) throw new Error('Hosted persistence is not configured');
865
      const query = z
866
        .object({
867
          query: z.string().trim().max(100).optional(),
868
          capability: z.string().trim().max(100).optional(),
869
          limit: z.coerce.number().int().min(1).max(50).optional(),
870
        })
871
        .parse(request.query);
872
      return repository.searchPublishedAgents(query);
873
    });
874
    router.get('/v0.1/directory/:id', async (request) => {
875
      operatorId(request);
876
      if (!repository) throw new Error('Hosted persistence is not configured');
877
      const card = await repository.getPublishedAgent((request.params as { id: string }).id);
878
      if (!card) throw new Error('Published agent not found');
879
      return card;
880
    });
881
    router.get('/v0.1/federated-interactions', async (request) => {
882
      const owner = operatorId(request);
883
      if (!repository?.listFederatedInteractions || !owner)
884
        throw new Error('Federated interactions are not configured');
885
      return repository.listFederatedInteractions(owner);
886
    });
887
    router.get('/v0.1/federated-interactions/:id', async (request) => {
888
      const owner = operatorId(request);
889
      if (!repository?.getFederatedInteraction || !owner)
890
        throw new Error('Federated interactions are not configured');
891
      const interaction = await repository.getFederatedInteraction(
892
        owner,
893
        (request.params as { id: string }).id,
894
      );
895
      if (!interaction) throw new Error('Interaction not found');
896
      return interaction;
897
    });
898
    router.get('/v0.1/federated-interactions/:id/session', async (request) => {
899
      const owner = operatorId(request);
900
      if (!repository?.getLiveSession || !owner)
901
        throw new Error('Live sessions are not configured');
902
      const value = z.object({ agentId: z.string().min(1) }).parse(request.query);
903
      enforceBoundAgent(request, value.agentId);
904
      return repository.getLiveSession(owner, (request.params as { id: string }).id, value.agentId);
905
    });
906
    router.get('/v0.1/federated-interactions/:id/brief', async (request) => {
907
      const owner = operatorId(request);
908
      if (!repository?.getCounterpartyBrief || !owner)
909
        throw new Error('Counterparty briefs are not configured');
910
      const value = z.object({ agentId: z.string().min(1) }).parse(request.query);
911
      enforceBoundAgent(request, value.agentId);
912
      return repository.getCounterpartyBrief(
913
        owner,
914
        (request.params as { id: string }).id,
915
        value.agentId,
916
      );
917
    });
918
    router.post('/v0.1/federated-interactions/:id/completion-reports', async (request) => {
919
      const owner = operatorId(request);
920
      if (!repository?.submitCompletionReport || !owner)
921
        throw new Error('Completion reports are not configured');
922
      const report = InteractionCompletionReportSchema.parse(request.body);
923
      if (report.interactionId !== (request.params as { id: string }).id)
924
        throw new Error('Interaction path does not match the completion report');
925
      return repository.submitCompletionReport(
926
        owner,
927
        boundAgentId(request),
928
        report,
929
        request.headers['x-openclasp-credential-type'] === 'agent_access_token'
930
          ? 'agent_access_token'
931
          : 'oauth_installation',
932
      );
933
    });
934
    router.post('/v0.1/federated-interactions/:id/feedback', async (request) => {
935
      const owner = operatorId(request);
936
      if (!repository?.submitInteractionFeedback || !owner)
937
        throw new Error('Interaction feedback is not configured');
938
      const feedback = InteractionFeedbackSchema.parse(request.body);
939
      if (feedback.interactionId !== (request.params as { id: string }).id)
940
        throw new Error('Interaction path does not match the feedback');
941
      return repository.submitInteractionFeedback(
942
        owner,
943
        boundAgentId(request),
944
        feedback,
945
        request.headers['x-openclasp-credential-type'] === 'agent_access_token'
946
          ? 'agent_access_token'
947
          : 'oauth_installation',
948
      );
949
    });
950
    router.get('/v0.1/feedback-requests', async (request) => {
951
      const owner = operatorId(request);
952
      if (!repository?.listFeedbackRequests || !owner)
953
        throw new Error('Interaction feedback is not configured');
954
      const value = z.object({ agentId: z.string().min(1) }).parse(request.query);
955
      enforceBoundAgent(request, value.agentId);
956
      return repository.listFeedbackRequests(owner, value.agentId);
957
    });
958
    router.post('/v0.1/federated-interactions', async (request) => {
959
      const owner = operatorId(request);
960
      if (!repository?.createFederatedInteraction || !owner)
961
        throw new Error('Federated interactions are not configured');
962
      const interaction = FederatedInteractionSchema.parse(request.body);
963
      const acceptance = interaction.acceptances[interaction.initiatorAgentId];
964
      if (!acceptance) throw new Error('Initiator acceptance is required');
965
      return repository.createFederatedInteraction(owner, {
966
        ...interaction,
967
        acceptances: {
968
          [interaction.initiatorAgentId]: { ...acceptance, method: 'oauth_account' },
969
        },
970
      });
971
    });
972
    router.post('/v0.1/federated-interactions/:id/respond', async (request) => {
973
      const owner = operatorId(request);
974
      if (!repository?.respondToFederatedInteraction || !owner)
975
        throw new Error('Federated interactions are not configured');
976
      const value = z
977
        .object({ agentId: z.string().min(1), decision: z.enum(['accept', 'reject']) })
978
        .parse(request.body);
979
      return repository.respondToFederatedInteraction(
980
        owner,
981
        (request.params as { id: string }).id,
982
        value.agentId,
983
        value.decision,
984
        'oauth_account',
985
      );
986
    });
987
    router.post('/v0.1/federated-interactions/:id/contract-proposals', async (request) => {
988
      const owner = operatorId(request);
989
      if (!repository?.proposeContractRevision || !owner)
990
        throw new Error('Contract negotiation is not configured');
991
      const value = z
992
        .object({
993
          agentId: z.string().min(1),
994
          expectedTermsHash: z.string().min(1).optional(),
995
          contract: InteractionContractSchema,
996
        })
997
        .parse(request.body);
998
      enforceBoundAgent(request, value.agentId);
999
      return repository.proposeContractRevision(
1000
        owner,
1001
        (request.params as { id: string }).id,
1002
        value.agentId,
1003
        value.contract,
1004
        value.expectedTermsHash,
1005
        request.headers['x-openclasp-credential-type'] === 'agent_access_token'
1006
          ? 'oauth_installation'
1007
          : 'oauth_account',
1008
      );
1009
    });
1010
    router.post(
1011
      '/v0.1/federated-interactions/:id/contract-proposals/:revisionId/respond',
1012
      async (request) => {
1013
        const owner = operatorId(request);
1014
        if (!repository?.respondToContractRevision || !owner)
1015
          throw new Error('Contract negotiation is not configured');
1016
        const value = z
1017
          .object({ agentId: z.string().min(1), decision: z.enum(['accept', 'reject']) })
1018
          .parse(request.body);
1019
        enforceBoundAgent(request, value.agentId);
1020
        const params = request.params as { id: string; revisionId: string };
1021
        return repository.respondToContractRevision(
1022
          owner,
1023
          params.id,
1024
          value.agentId,
1025
          params.revisionId,
1026
          value.decision,
1027
          request.headers['x-openclasp-credential-type'] === 'agent_access_token'
1028
            ? 'oauth_installation'
1029
            : 'oauth_account',
1030
        );
1031
      },
1032
    );
1033
    router.post('/v0.1/delegations', async (request) => {
1034
      const value = DelegationCredentialSchema.parse(request.body);
1035
      const current = await scopedEngine(request);
1036
      current.delegations.set(value.delegationId, value);
1037
      await persist(request, 'delegation', value.delegationId, value);
1038
      return { valid: current.verifyDelegation(value.delegationId) };
1039
    });
1040
    router.post('/v0.1/delegations/:id/verify', async (request) => ({
1041
      valid: (await scopedEngine(request)).verifyDelegation((request.params as { id: string }).id),
1042
    }));
1043
    router.post('/v0.1/interactions/contracts', async (request) => {
1044
      const contract = (await scopedEngine(request)).saveContract(
1045
        InteractionContractSchema.parse(request.body),
1046
      );
1047
      await persist(request, 'contract', contract.interactionId, contract);
1048
      return contract;
1049
    });
1050
    router.post('/v0.1/events', async (request) => {
1051
      const event = (await scopedEngine(request)).recordEvent(
1052
        InteractionEventSchema.parse(request.body),
1053
      );
1054
      await persist(request, 'event', event.eventId, event);
1055
      return event;
1056
    });
1057
    router.post('/v0.1/risk/assess', async (request) =>
1058
      (await scopedEngine(request)).assess(request.body as any),
1059
    );
1060
    router.get('/v0.1/profiles/:id', async (request) => {
1061
      const { id } = request.params as { id: string };
1062
      const query = request.query as { version?: string; taskCategory?: string };
1063
      return (await scopedEngine(request)).getRisk(
1064
        id,
1065
        query.version ?? '1.0.0',
1066
        query.taskCategory ?? 'general',
1067
      );
1068
    });
1069
    router.post('/v0.1/claims/check', async (request) =>
1070
      ((value) => factChecker.check(value.claim, value.permission))(
1071
        z.object({ claim: z.string(), permission: z.boolean().optional() }).parse(request.body),
1072
      ),
1073
    );
1074
    router.post('/v0.1/receipts', async (request) => {
1075
      const receipt = (await scopedEngine(request)).submitReceipt(
1076
        ReceiptSchema.parse(request.body),
1077
      );
1078
      await persist(request, 'receipt', receipt.receiptId, receipt);
1079
      return receipt;
1080
    });
1081
    router.post('/v0.1/receipts/verify', async (request) => {
1082
      try {
1083
        (await scopedEngine(request)).verifyReceipt(ReceiptSchema.parse(request.body));
1084
        return { valid: true };
1085
      } catch {
1086
        return { valid: false };
1087
      }
1088
    });
1089
    router.post('/v0.1/feedback', async (request) => {
1090
      const feedback = FeedbackSchema.parse(request.body);
1091
      const current = await scopedEngine(request);
1092
      const result = current.submitFeedback(feedback);
1093
      await persist(request, 'feedback', feedback.feedbackId, feedback);
1094
      for (const profile of current.profiles.values())
1095
        await persist(
1096
          request,
1097
          'profile',
1098
          `${profile.agentId}|${profile.agentVersion}|${profile.taskCategory}`,
1099
          profile,
1100
        );
1101
      return result;
1102
    });
1103
    router.post('/v0.1/conflicts', async (request) => {
1104
      const conflict = (await scopedEngine(request)).createConflict(request.body as any);
1105
      await persist(request, 'conflict', conflict.conflictId, conflict);
1106
      return conflict;
1107
    });
1108
    router.post('/v0.1/conflicts/:id/permit', async (request) => {
1109
      const conflict = (await scopedEngine(request)).permitMediation(
1110
        (request.params as { id: string }).id,
1111
        (request.body as { agentId: string }).agentId,
1112
      );
1113
      await persist(request, 'conflict', conflict.conflictId, conflict);
1114
      return conflict;
1115
    });
1116
    router.post('/v0.1/conflicts/:id/resolve', async (request) => {
1117
      const conflict = (await scopedEngine(request)).resolveConflict(
1118
        (request.params as { id: string }).id,
1119
        (request.body as { resolution: string }).resolution,
1120
      );
1121
      await persist(request, 'conflict', conflict.conflictId, conflict);
1122
      return conflict;
1123
    });
1124
    router.post('/v0.1/revocations/agents/:id', async (request) => {
1125
      const id = (request.params as { id: string }).id;
1126
      const current = await scopedEngine(request);
1127
      current.revokeAgent(id);
1128
      await persist(request, 'agent', id, current.agents.get(id));
1129
      return { revoked: true };
1130
    });
1131
    router.post('/v0.1/revocations/delegations/:id', async (request) => {
1132
      const id = (request.params as { id: string }).id;
1133
      const current = await scopedEngine(request);
1134
      current.revokeDelegation(id);
1135
      await persist(request, 'delegation', id, current.delegations.get(id));
1136
      return { revoked: true };
1137
    });
1138
    router.post('/v0.1/contributions/consent', async (request) => {
1139
      const value = z.object({ agentId: z.string(), enabled: z.boolean() }).parse(request.body);
1140
      const current = await scopedEngine(request);
1141
      current.setContributionConsent(value.agentId, value.enabled);
1142
      await persist(request, 'consent', value.agentId, {
1143
        agentId: value.agentId,
1144
        ...current.contributionConsent.get(value.agentId),
1145
      });
1146
      return value;
1147
    });
1148
  });
1149
  return app;
1150
}