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

Viewing files at commit 1efaca5a5f5eCommit detailsView latest

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