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

Viewing files at commit faf8a393d637Commit detailsView latest

apps/api/src/app.ts 1136 lines · 50.9 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 structured = JSON.stringify({
406
        '@context': 'https://schema.org',
407
        '@type': 'SoftwareApplication',
408
        name: card.name,
409
        description: card.description,
410
        url: card.profileUrl,
411
      }).replace(/</g, '\\u003c');
412
      return reply.type('text/html; charset=utf-8').send(`<!doctype html>
413
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
414
<title>${escapeHtml(card.name)} · OpenClasp</title><meta name="description" content="${escapeHtml(card.description)}">
415
<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)}">
416
<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`)}">
417
<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)}">
418
<style>color-scheme:dark;*{box-sizing:border-box}body{margin:0;background:#0c0a0a;color:#f6f1ee;font:15px/1.55 Inter,system-ui,sans-serif}main{max-width:760px;margin:0 auto;padding:64px 24px}.brand{color:#f04b2d;font-weight:700}.verified{display:inline-block;margin:24px 0 8px;padding:5px 9px;border:1px solid #2fbf71;color:#2fbf71;border-radius:999px;font-size:12px}.verification-note{margin:0 0 24px;font-size:12px}h1{font-size:42px;line-height:1.05;margin:8px 0 12px}p{color:#a39a94}.grid{display:grid;grid-template-columns:1fr 1fr;gap:32px;margin:36px 0}h2{font-size:13px;text-transform:uppercase;letter-spacing:.08em;color:#a39a94}a{color:#f04b2d;overflow-wrap:anywhere}.links{padding-top:24px;border-top:1px solid #2b2523}.status{color:${card.presence?.status === 'online' ? '#2fbf71' : '#a39a94'}}@media(max-width:600px){.grid{grid-template-columns:1fr}h1{font-size:34px}}</style>
419
<script type="application/ld+json">${structured}</script></head><body><main><div class="brand">OpenClasp</div><div class="verified">✓ Publisher verified</div><p class="verification-note">OpenClasp verified control of the publishing account. Capabilities are self-declared.</p>
420
<h1>${escapeHtml(card.name)}</h1><p>${escapeHtml(card.description || 'No description provided.')}</p><p class="status">${escapeHtml(card.presence?.status ?? 'offline')} · ${escapeHtml(card.agentMode.replace('_', ' '))}</p>
421
<div class="grid"><section><h2>Capabilities</h2><ul>${capabilities || '<li>None published</li>'}</ul></section><section><h2>Limitations</h2><ul>${limitations || '<li>None published</li>'}</ul></section></div>
422
<div class="links"><p><strong>Agent ID</strong><br>${escapeHtml(card.agentId)}</p><p><a href="${escapeHtml(card.cardUrl)}">Machine-readable card</a> · <a href="${escapeHtml(card.a2aAgentCardUrl)}">A2A Agent Card</a> · <a href="${escapeHtml(socialImageUrl)}">Social preview</a></p></div></main></body></html>`);
423
    });
424
    router.get('/v0.1/dashboard', async (request) => {
425
      const owner = operatorId(request);
426
      if (repository && owner) return repository.dashboard(owner);
427
      return {
428
        agents: [...engine.agents.values()],
429
        projects: [],
430
        installations: [],
431
        setupRequests: [],
432
        publications: [],
433
        interactions: [],
434
        federatedInteractions: [],
435
        liveSessions: [],
436
        hostedThreads: [],
437
        events: [...engine.events.values()],
438
        conflicts: [...engine.conflicts.values()],
439
        receipts: [...engine.receipts.values()],
440
        profiles: [...engine.profiles.values()],
441
        counterpartyBriefs: [],
442
        completionReports: [],
443
        feedbackRequests: [],
444
        interactionFeedback: [],
445
        interactionConclusions: [],
446
        learningEligibility: [],
447
        profileDeltas: [],
448
        intelligenceSummaries: [],
449
        runtimes: [],
450
      };
451
    });
452
    router.get('/v0.1/intelligence', async (request) => {
453
      const owner = operatorId(request);
454
      if (!repository?.listContextualIntelligence || !owner)
455
        throw new Error('Contextual intelligence 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
        })
461
        .parse(request.query);
462
      return repository.listContextualIntelligence(owner, {
463
        ...(input.agentId ? { agentId: input.agentId } : {}),
464
        ...(input.taskCategory ? { taskCategory: input.taskCategory } : {}),
465
      });
466
    });
467
    router.get('/v0.1/marketplace', async (request) => {
468
      const owner = operatorId(request);
469
      if (!repository?.searchPersonalizedMarketplace || !owner)
470
        throw new Error('Personalized marketplace is not configured');
471
      const input = z
472
        .object({
473
          agentId: z.string().min(1).optional(),
474
          taskCategory: z.string().trim().min(1).max(100).optional(),
475
          query: z.string().trim().max(100).optional(),
476
          limit: z.coerce.number().int().min(1).max(50).default(30),
477
        })
478
        .parse(request.query);
479
      return repository.searchPersonalizedMarketplace(owner, {
480
        ...(input.agentId ? { agentId: input.agentId } : {}),
481
        ...(input.taskCategory ? { taskCategory: input.taskCategory } : {}),
482
        ...(input.query ? { query: input.query } : {}),
483
        limit: input.limit,
484
      });
485
    });
486
    router.get('/v0.1/account', async (request) => {
487
      const owner = operatorId(request);
488
      return {
489
        operatorId: owner,
490
        email: decodeURIComponent(String(request.headers['x-openclasp-email'] ?? '')),
491
        name: decodeURIComponent(String(request.headers['x-openclasp-name'] ?? '')),
492
      };
493
    });
494
    router.get('/v0.1/settings', async (request) => {
495
      const owner = operatorId(request);
496
      if (!repository || !owner)
497
        return {
498
          displayName: '',
499
          contributionEnabled: false,
500
          retentionDays: 30,
501
          evidenceSharing: 'ask',
502
          rawConversationsStored: false,
503
        };
504
      return repository.getSettings(owner);
505
    });
506
    router.get('/v0.1/onboarding', async (request) => {
507
      const owner = operatorId(request);
508
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
509
      return getOnboardingState(repository, owner);
510
    });
511
    router.get('/v0.1/runtime/bootstrap', async (request) => {
512
      const owner = operatorId(request);
513
      const agentId = boundAgentId(request);
514
      const rows = owner && repository ? await repository.list(owner) : [];
515
      const profile = rows.find((row) => row.kind === 'agent_profile' && row.recordId === agentId)
516
        ?.payload as AgentProfile | undefined;
517
      return {
518
        agentId,
519
        agentVersion: profile?.agentVersion ?? '1.0.0',
520
        capabilities: profile?.capabilities ?? [],
521
        limitations: profile?.limitations ?? [],
522
        openClaspUrl: publicBaseUrl(request),
523
        runtimeRegistrationEndpoint: `${publicBaseUrl(request)}/v0.1/runtime`,
524
        protocol: 'A2A/1.0',
525
        controlProtocol: 'OpenClasp/0.1',
526
      };
527
    });
528
    router.put('/v0.1/runtime', async (request) => {
529
      const owner = operatorId(request);
530
      if (!repository?.registerAgentRuntime || !owner)
531
        throw new Error('Hosted runtime delivery is not configured');
532
      const endpoint = z
533
        .object({ endpoint: z.string().url().max(2048) })
534
        .parse(request.body).endpoint;
535
      return repository.registerAgentRuntime(owner, boundAgentId(request), endpoint);
536
    });
537
    router.delete('/v0.1/runtime', async (request) => {
538
      const owner = operatorId(request);
539
      if (!repository?.disableAgentRuntime || !owner)
540
        throw new Error('Hosted runtime delivery is not configured');
541
      return repository.disableAgentRuntime(owner, boundAgentId(request));
542
    });
543
    router.post('/v0.1/runtime/heartbeat', async (request) => {
544
      const owner = operatorId(request);
545
      if (!repository?.touchAgentPresence || !owner)
546
        throw new Error('Agent presence is not configured');
547
      return repository.touchAgentPresence(owner, boundAgentId(request));
548
    });
549
    router.put('/v0.1/runtime/profile', async (request) => {
550
      const owner = operatorId(request);
551
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
552
      const agentId = boundAgentId(request);
553
      const value = z
554
        .object({
555
          description: z.string().trim().max(500).optional(),
556
          capabilities: z.array(z.string().trim().min(1).max(100)).min(1).max(100),
557
          limitations: z.array(z.string().trim().min(1).max(300)).max(100).default([]),
558
        })
559
        .strict()
560
        .parse(request.body);
561
      const rows = await repository.list(owner);
562
      const current = rows.find((row) => row.kind === 'agent_profile' && row.recordId === agentId)
563
        ?.payload as AgentProfile | undefined;
564
      if (!current || current.status !== 'active') throw new Error('Active owned agent not found');
565
      const profile: AgentProfile = {
566
        ...current,
567
        ...(value.description !== undefined ? { description: value.description } : {}),
568
        capabilities: [...new Set(value.capabilities)],
569
        limitations: [...new Set(value.limitations)],
570
        updatedAt: new Date().toISOString(),
571
      };
572
      await repository.upsert(owner, 'agent_profile', agentId, profile);
573
      const publication = rows.find((row) => row.kind === 'publication' && row.recordId === agentId)
574
        ?.payload as { published?: boolean } | undefined;
575
      if (profile.autoPublish || publication?.published) {
576
        const previous = await repository.getPublishedAgent(agentId);
577
        const card = await repository.publishAgent(
578
          owner,
579
          buildPublicAgentCard(profile, publicBaseUrl(request), previous),
580
        );
581
        await repository.upsert(owner, 'publication', agentId, publicPublication(card));
582
        return { profile, card };
583
      }
584
      return { profile };
585
    });
586
    router.post('/v0.1/onboarding/:id/approve', async (request) => {
587
      const owner = operatorId(request);
588
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
589
      const requestId = (request.params as { id: string }).id;
590
      const state = await getOnboardingState(repository, owner);
591
      const setup = state.setupRequests.find((item) => item.requestId === requestId);
592
      const result = await approveAgentSetup(repository, owner, requestId);
593
      if (result.status === 'connected' && setup?.autoPublish) {
594
        const previous = await repository.getPublishedAgent(result.agent.agentId);
595
        const card = await repository.publishAgent(
596
          owner,
597
          buildPublicAgentCard(result.agent, publicBaseUrl(request), previous),
598
        );
599
        await repository.upsert(
600
          owner,
601
          'publication',
602
          result.agent.agentId,
603
          publicPublication(card),
604
        );
605
      }
606
      return result;
607
    });
608
    router.post('/v0.1/onboarding/:id/reject', async (request) => {
609
      const owner = operatorId(request);
610
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
611
      return rejectAgentSetup(repository, owner, (request.params as { id: string }).id);
612
    });
613
    router.put('/v0.1/settings', async (request) => {
614
      const owner = operatorId(request);
615
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
616
      const value = z
617
        .object({
618
          displayName: z.string().trim().max(100),
619
          contributionEnabled: z.boolean(),
620
          retentionDays: z.number().int().min(0).max(3650),
621
          evidenceSharing: z.enum(['never', 'ask', 'contract_only']),
622
        })
623
        .parse(request.body);
624
      return repository.saveSettings(owner, value);
625
    });
626
    router.post('/v0.1/provider-connections', async (request) => {
627
      const owner = operatorId(request);
628
      if (!repository?.issueAgentAccessToken || !owner)
629
        throw new Error('Hosted provider connections are not configured');
630
      const value = z
631
        .object({
632
          provider: z.enum(['botpress', 'custom']),
633
          agentName: z.string().trim().min(1).max(100),
634
          projectName: z.string().trim().min(1).max(100),
635
          description: z.string().trim().min(1).max(500),
636
          capabilities: z.array(z.string().trim().min(1).max(100)).min(1).max(100),
637
          limitations: z.array(z.string().trim().min(1).max(300)).max(100).default([]),
638
          expiresInDays: z.number().int().min(1).max(365).default(365),
639
        })
640
        .parse(request.body);
641
      const created = await createHostedProviderAgent(repository, owner, value);
642
      try {
643
        const accessToken = await repository.issueAgentAccessToken(owner, created.agent.agentId, {
644
          name: 'Botpress',
645
          expiresInDays: value.expiresInDays,
646
        });
647
        return { ...created, provider: value.provider, accessToken };
648
      } catch (error) {
649
        if (repository.deleteAgent)
650
          await repository.deleteAgent(owner, created.agent.agentId).catch(() => undefined);
651
        throw error;
652
      }
653
    });
654
    router.post('/v0.1/quickstart/agent', async (request) => {
655
      const owner = operatorId(request);
656
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
657
      const value = z
658
        .object({
659
          agentName: z.string().trim().min(1).max(100),
660
          projectName: z.string().trim().min(1).max(100).default('My agents'),
661
          description: z.string().trim().min(1).max(500),
662
          framework: z.string().trim().min(1).max(100).optional(),
663
          capabilities: z.array(z.string().trim().min(1).max(100)).min(1).max(20),
664
          limitations: z.array(z.string().trim().min(1).max(300)).max(20).default([]),
665
        })
666
        .strict()
667
        .parse(request.body);
668
      const created = await createDashboardAgent(repository, owner, value);
669
      return created;
670
    });
671
    router.post('/v0.1/agents', async (request) => {
672
      const owner = operatorId(request);
673
      const current = await scopedEngine(request);
674
      const identity = AgentIdentitySchema.parse(request.body);
675
      if (owner && identity.operatorRef !== `auth0:${owner}`)
676
        throw new Error('Agent identity is not owned by the authenticated operator');
677
      const registered = current.registerAgent(identity);
678
      await persist(request, 'agent', identity.agentId, registered);
679
      return registered;
680
    });
681
    router.get('/v0.1/agents/:id', async (request) => {
682
      const { id } = request.params as { id: string };
683
      const value = (await scopedEngine(request)).agents.get(id);
684
      if (!value) throw new Error('Agent not found');
685
      return value;
686
    });
687
    router.post('/v0.1/agents/:id/publication', async (request) => {
688
      const owner = operatorId(request);
689
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
690
      const { id } = request.params as { id: string };
691
      const { published } = z.object({ published: z.boolean() }).parse(request.body);
692
      const rows = await repository.list(owner);
693
      const agent = rows.find((row) => row.kind === 'agent_profile' && row.recordId === id)
694
        ?.payload as AgentProfile | undefined;
695
      if (!agent) throw new Error('Owned agent not found');
696
      if (agent.status !== 'active') throw new Error('Revoked agents cannot be published');
697
      if (!published) {
698
        await repository.unpublishAgent(owner, id);
699
        const publication = { agentId: id, published: false, updatedAt: new Date().toISOString() };
700
        await repository.upsert(owner, 'publication', id, publication);
701
        return publication;
702
      }
703
      const previous = await repository.getPublishedAgent(id);
704
      const card = await repository.publishAgent(
705
        owner,
706
        buildPublicAgentCard(agent, publicBaseUrl(request), previous),
707
      );
708
      const publication = publicPublication(card);
709
      await repository.upsert(owner, 'publication', id, publication);
710
      return { ...publication, card };
711
    });
712
    router.put('/v0.1/agents/:id/automation', async (request) => {
713
      const owner = operatorId(request);
714
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
715
      const { id } = request.params as { id: string };
716
      const value = z
717
        .object({
718
          autoPublish: z.boolean(),
719
          autoAcceptPolicy: z.enum(['off', 'safe_matching']),
720
          autoAcceptTaskCategories: z.array(z.string().trim().min(1).max(100)).max(100),
721
        })
722
        .parse(request.body);
723
      const rows = await repository.list(owner);
724
      const current = rows.find((row) => row.kind === 'agent_profile' && row.recordId === id)
725
        ?.payload as AgentProfile | undefined;
726
      if (!current) throw new Error('Owned agent not found');
727
      const agent: AgentProfile = {
728
        ...current,
729
        transport: 'direct_a2a',
730
        description: current.description ?? '',
731
        agentVersion: current.agentVersion ?? '1.0.0',
732
        autoPublish: value.autoPublish,
733
        autoAcceptPolicy: value.autoAcceptPolicy,
734
        autoAcceptTaskCategories: [...new Set(value.autoAcceptTaskCategories)],
735
        updatedAt: new Date().toISOString(),
736
      };
737
      await repository.upsert(owner, 'agent_profile', id, agent);
738
      if (value.autoPublish) {
739
        const previous = await repository.getPublishedAgent(id);
740
        const card = await repository.publishAgent(
741
          owner,
742
          buildPublicAgentCard(agent, publicBaseUrl(request), previous),
743
        );
744
        await repository.upsert(owner, 'publication', id, publicPublication(card));
745
      } else {
746
        await repository.unpublishAgent(owner, id);
747
        await repository.upsert(owner, 'publication', id, {
748
          agentId: id,
749
          published: false,
750
          updatedAt: new Date().toISOString(),
751
        });
752
      }
753
      return agent;
754
    });
755
    router.put('/v0.1/agents/:id/runtime', async (request) => {
756
      const owner = operatorId(request);
757
      if (!repository?.registerAgentRuntime || !owner)
758
        throw new Error('Hosted runtime delivery is not configured');
759
      const endpoint = z
760
        .object({ endpoint: z.string().url().max(2048) })
761
        .parse(request.body).endpoint;
762
      return repository.registerAgentRuntime(
763
        owner,
764
        (request.params as { id: string }).id,
765
        endpoint,
766
      );
767
    });
768
    router.delete('/v0.1/agents/:id/runtime', async (request) => {
769
      const owner = operatorId(request);
770
      if (!repository?.disableAgentRuntime || !owner)
771
        throw new Error('Hosted runtime delivery is not configured');
772
      return repository.disableAgentRuntime(owner, (request.params as { id: string }).id);
773
    });
774
    router.get('/v0.1/agents/:id/access-tokens', async (request) => {
775
      const owner = operatorId(request);
776
      if (!repository?.listAgentAccessTokens || !owner)
777
        throw new Error('Agent access tokens are not configured');
778
      return repository.listAgentAccessTokens(owner, (request.params as { id: string }).id);
779
    });
780
    router.post('/v0.1/agents/:id/access-tokens', async (request) => {
781
      const owner = operatorId(request);
782
      if (!repository?.issueAgentAccessToken || !owner)
783
        throw new Error('Agent access tokens are not configured');
784
      const value = z
785
        .object({
786
          name: z.string().trim().min(1).max(100),
787
          expiresInDays: z.number().int().min(1).max(365).default(365),
788
        })
789
        .parse(request.body);
790
      return repository.issueAgentAccessToken(owner, (request.params as { id: string }).id, value);
791
    });
792
    router.delete('/v0.1/agents/:id/access-tokens/:tokenId', async (request) => {
793
      const owner = operatorId(request);
794
      if (!repository?.revokeAgentAccessToken || !owner)
795
        throw new Error('Agent access tokens are not configured');
796
      const params = request.params as { id: string; tokenId: string };
797
      return repository.revokeAgentAccessToken(owner, params.id, params.tokenId);
798
    });
799
    router.delete('/v0.1/agents/:id', async (request) => {
800
      const owner = operatorId(request);
801
      if (!repository?.deleteAgent || !owner)
802
        throw new Error('Hosted agent deletion is not configured');
803
      const result = await repository.deleteAgent(owner, (request.params as { id: string }).id);
804
      engines.delete(owner);
805
      return result;
806
    });
807
    router.get('/v0.1/agents/:id/threads', async (request) => {
808
      const owner = operatorId(request);
809
      if (!repository?.listHostedThreads || !owner)
810
        throw new Error('Temporary chat history is not configured');
811
      return repository.listHostedThreads(owner, (request.params as { id: string }).id);
812
    });
813
    router.get('/v0.1/agents/:id/threads/:threadId', async (request) => {
814
      const owner = operatorId(request);
815
      if (!repository?.getHostedThread || !owner)
816
        throw new Error('Temporary chat history is not configured');
817
      const params = request.params as { id: string; threadId: string };
818
      return repository.getHostedThread(owner, params.id, params.threadId);
819
    });
820
    router.post('/v0.1/agents/:id/messages', async (request) => {
821
      const owner = operatorId(request);
822
      if (!repository?.sendTemporaryMessage || !owner)
823
        throw new Error('Temporary chat delivery is not configured');
824
      const value = z
825
        .object({ interactionId: z.string().uuid(), content: z.string().trim().min(1).max(20_000) })
826
        .parse(request.body);
827
      return repository.sendTemporaryMessage(
828
        owner,
829
        (request.params as { id: string }).id,
830
        value.interactionId,
831
        value.content,
832
      );
833
    });
834
    router.post('/v0.1/agents/:id/threads/:threadId/read', async (request) => {
835
      const owner = operatorId(request);
836
      if (!repository?.markHostedThreadRead || !owner)
837
        throw new Error('Temporary chat history is not configured');
838
      const params = request.params as { id: string; threadId: string };
839
      return repository.markHostedThreadRead(owner, params.id, params.threadId);
840
    });
841
    router.post('/v0.1/agents/:id/threads/:threadId/close', async (request) => {
842
      const owner = operatorId(request);
843
      if (!repository?.closeHostedThread || !owner)
844
        throw new Error('Temporary chat history is not configured');
845
      const params = request.params as { id: string; threadId: string };
846
      return repository.closeHostedThread(owner, params.id, params.threadId);
847
    });
848
    router.get('/v0.1/directory', async (request) => {
849
      operatorId(request);
850
      if (!repository) throw new Error('Hosted persistence is not configured');
851
      const query = z
852
        .object({
853
          query: z.string().trim().max(100).optional(),
854
          capability: z.string().trim().max(100).optional(),
855
          limit: z.coerce.number().int().min(1).max(50).optional(),
856
        })
857
        .parse(request.query);
858
      return repository.searchPublishedAgents(query);
859
    });
860
    router.get('/v0.1/directory/:id', async (request) => {
861
      operatorId(request);
862
      if (!repository) throw new Error('Hosted persistence is not configured');
863
      const card = await repository.getPublishedAgent((request.params as { id: string }).id);
864
      if (!card) throw new Error('Published agent not found');
865
      return card;
866
    });
867
    router.get('/v0.1/federated-interactions', async (request) => {
868
      const owner = operatorId(request);
869
      if (!repository?.listFederatedInteractions || !owner)
870
        throw new Error('Federated interactions are not configured');
871
      return repository.listFederatedInteractions(owner);
872
    });
873
    router.get('/v0.1/federated-interactions/:id', async (request) => {
874
      const owner = operatorId(request);
875
      if (!repository?.getFederatedInteraction || !owner)
876
        throw new Error('Federated interactions are not configured');
877
      const interaction = await repository.getFederatedInteraction(
878
        owner,
879
        (request.params as { id: string }).id,
880
      );
881
      if (!interaction) throw new Error('Interaction not found');
882
      return interaction;
883
    });
884
    router.get('/v0.1/federated-interactions/:id/session', async (request) => {
885
      const owner = operatorId(request);
886
      if (!repository?.getLiveSession || !owner)
887
        throw new Error('Live sessions are not configured');
888
      const value = z.object({ agentId: z.string().min(1) }).parse(request.query);
889
      enforceBoundAgent(request, value.agentId);
890
      return repository.getLiveSession(owner, (request.params as { id: string }).id, value.agentId);
891
    });
892
    router.get('/v0.1/federated-interactions/:id/brief', async (request) => {
893
      const owner = operatorId(request);
894
      if (!repository?.getCounterpartyBrief || !owner)
895
        throw new Error('Counterparty briefs are not configured');
896
      const value = z.object({ agentId: z.string().min(1) }).parse(request.query);
897
      enforceBoundAgent(request, value.agentId);
898
      return repository.getCounterpartyBrief(
899
        owner,
900
        (request.params as { id: string }).id,
901
        value.agentId,
902
      );
903
    });
904
    router.post('/v0.1/federated-interactions/:id/completion-reports', async (request) => {
905
      const owner = operatorId(request);
906
      if (!repository?.submitCompletionReport || !owner)
907
        throw new Error('Completion reports are not configured');
908
      const report = InteractionCompletionReportSchema.parse(request.body);
909
      if (report.interactionId !== (request.params as { id: string }).id)
910
        throw new Error('Interaction path does not match the completion report');
911
      return repository.submitCompletionReport(
912
        owner,
913
        boundAgentId(request),
914
        report,
915
        request.headers['x-openclasp-credential-type'] === 'agent_access_token'
916
          ? 'agent_access_token'
917
          : 'oauth_installation',
918
      );
919
    });
920
    router.post('/v0.1/federated-interactions/:id/feedback', async (request) => {
921
      const owner = operatorId(request);
922
      if (!repository?.submitInteractionFeedback || !owner)
923
        throw new Error('Interaction feedback is not configured');
924
      const feedback = InteractionFeedbackSchema.parse(request.body);
925
      if (feedback.interactionId !== (request.params as { id: string }).id)
926
        throw new Error('Interaction path does not match the feedback');
927
      return repository.submitInteractionFeedback(
928
        owner,
929
        boundAgentId(request),
930
        feedback,
931
        request.headers['x-openclasp-credential-type'] === 'agent_access_token'
932
          ? 'agent_access_token'
933
          : 'oauth_installation',
934
      );
935
    });
936
    router.get('/v0.1/feedback-requests', async (request) => {
937
      const owner = operatorId(request);
938
      if (!repository?.listFeedbackRequests || !owner)
939
        throw new Error('Interaction feedback is not configured');
940
      const value = z.object({ agentId: z.string().min(1) }).parse(request.query);
941
      enforceBoundAgent(request, value.agentId);
942
      return repository.listFeedbackRequests(owner, value.agentId);
943
    });
944
    router.post('/v0.1/federated-interactions', async (request) => {
945
      const owner = operatorId(request);
946
      if (!repository?.createFederatedInteraction || !owner)
947
        throw new Error('Federated interactions are not configured');
948
      const interaction = FederatedInteractionSchema.parse(request.body);
949
      const acceptance = interaction.acceptances[interaction.initiatorAgentId];
950
      if (!acceptance) throw new Error('Initiator acceptance is required');
951
      return repository.createFederatedInteraction(owner, {
952
        ...interaction,
953
        acceptances: {
954
          [interaction.initiatorAgentId]: { ...acceptance, method: 'oauth_account' },
955
        },
956
      });
957
    });
958
    router.post('/v0.1/federated-interactions/:id/respond', async (request) => {
959
      const owner = operatorId(request);
960
      if (!repository?.respondToFederatedInteraction || !owner)
961
        throw new Error('Federated interactions are not configured');
962
      const value = z
963
        .object({ agentId: z.string().min(1), decision: z.enum(['accept', 'reject']) })
964
        .parse(request.body);
965
      return repository.respondToFederatedInteraction(
966
        owner,
967
        (request.params as { id: string }).id,
968
        value.agentId,
969
        value.decision,
970
        'oauth_account',
971
      );
972
    });
973
    router.post('/v0.1/federated-interactions/:id/contract-proposals', async (request) => {
974
      const owner = operatorId(request);
975
      if (!repository?.proposeContractRevision || !owner)
976
        throw new Error('Contract negotiation is not configured');
977
      const value = z
978
        .object({
979
          agentId: z.string().min(1),
980
          expectedTermsHash: z.string().min(1).optional(),
981
          contract: InteractionContractSchema,
982
        })
983
        .parse(request.body);
984
      enforceBoundAgent(request, value.agentId);
985
      return repository.proposeContractRevision(
986
        owner,
987
        (request.params as { id: string }).id,
988
        value.agentId,
989
        value.contract,
990
        value.expectedTermsHash,
991
        request.headers['x-openclasp-credential-type'] === 'agent_access_token'
992
          ? 'oauth_installation'
993
          : 'oauth_account',
994
      );
995
    });
996
    router.post(
997
      '/v0.1/federated-interactions/:id/contract-proposals/:revisionId/respond',
998
      async (request) => {
999
        const owner = operatorId(request);
1000
        if (!repository?.respondToContractRevision || !owner)
1001
          throw new Error('Contract negotiation is not configured');
1002
        const value = z
1003
          .object({ agentId: z.string().min(1), decision: z.enum(['accept', 'reject']) })
1004
          .parse(request.body);
1005
        enforceBoundAgent(request, value.agentId);
1006
        const params = request.params as { id: string; revisionId: string };
1007
        return repository.respondToContractRevision(
1008
          owner,
1009
          params.id,
1010
          value.agentId,
1011
          params.revisionId,
1012
          value.decision,
1013
          request.headers['x-openclasp-credential-type'] === 'agent_access_token'
1014
            ? 'oauth_installation'
1015
            : 'oauth_account',
1016
        );
1017
      },
1018
    );
1019
    router.post('/v0.1/delegations', async (request) => {
1020
      const value = DelegationCredentialSchema.parse(request.body);
1021
      const current = await scopedEngine(request);
1022
      current.delegations.set(value.delegationId, value);
1023
      await persist(request, 'delegation', value.delegationId, value);
1024
      return { valid: current.verifyDelegation(value.delegationId) };
1025
    });
1026
    router.post('/v0.1/delegations/:id/verify', async (request) => ({
1027
      valid: (await scopedEngine(request)).verifyDelegation((request.params as { id: string }).id),
1028
    }));
1029
    router.post('/v0.1/interactions/contracts', async (request) => {
1030
      const contract = (await scopedEngine(request)).saveContract(
1031
        InteractionContractSchema.parse(request.body),
1032
      );
1033
      await persist(request, 'contract', contract.interactionId, contract);
1034
      return contract;
1035
    });
1036
    router.post('/v0.1/events', async (request) => {
1037
      const event = (await scopedEngine(request)).recordEvent(
1038
        InteractionEventSchema.parse(request.body),
1039
      );
1040
      await persist(request, 'event', event.eventId, event);
1041
      return event;
1042
    });
1043
    router.post('/v0.1/risk/assess', async (request) =>
1044
      (await scopedEngine(request)).assess(request.body as any),
1045
    );
1046
    router.get('/v0.1/profiles/:id', async (request) => {
1047
      const { id } = request.params as { id: string };
1048
      const query = request.query as { version?: string; taskCategory?: string };
1049
      return (await scopedEngine(request)).getRisk(
1050
        id,
1051
        query.version ?? '1.0.0',
1052
        query.taskCategory ?? 'general',
1053
      );
1054
    });
1055
    router.post('/v0.1/claims/check', async (request) =>
1056
      ((value) => factChecker.check(value.claim, value.permission))(
1057
        z.object({ claim: z.string(), permission: z.boolean().optional() }).parse(request.body),
1058
      ),
1059
    );
1060
    router.post('/v0.1/receipts', async (request) => {
1061
      const receipt = (await scopedEngine(request)).submitReceipt(
1062
        ReceiptSchema.parse(request.body),
1063
      );
1064
      await persist(request, 'receipt', receipt.receiptId, receipt);
1065
      return receipt;
1066
    });
1067
    router.post('/v0.1/receipts/verify', async (request) => {
1068
      try {
1069
        (await scopedEngine(request)).verifyReceipt(ReceiptSchema.parse(request.body));
1070
        return { valid: true };
1071
      } catch {
1072
        return { valid: false };
1073
      }
1074
    });
1075
    router.post('/v0.1/feedback', async (request) => {
1076
      const feedback = FeedbackSchema.parse(request.body);
1077
      const current = await scopedEngine(request);
1078
      const result = current.submitFeedback(feedback);
1079
      await persist(request, 'feedback', feedback.feedbackId, feedback);
1080
      for (const profile of current.profiles.values())
1081
        await persist(
1082
          request,
1083
          'profile',
1084
          `${profile.agentId}|${profile.agentVersion}|${profile.taskCategory}`,
1085
          profile,
1086
        );
1087
      return result;
1088
    });
1089
    router.post('/v0.1/conflicts', async (request) => {
1090
      const conflict = (await scopedEngine(request)).createConflict(request.body as any);
1091
      await persist(request, 'conflict', conflict.conflictId, conflict);
1092
      return conflict;
1093
    });
1094
    router.post('/v0.1/conflicts/:id/permit', async (request) => {
1095
      const conflict = (await scopedEngine(request)).permitMediation(
1096
        (request.params as { id: string }).id,
1097
        (request.body as { agentId: string }).agentId,
1098
      );
1099
      await persist(request, 'conflict', conflict.conflictId, conflict);
1100
      return conflict;
1101
    });
1102
    router.post('/v0.1/conflicts/:id/resolve', async (request) => {
1103
      const conflict = (await scopedEngine(request)).resolveConflict(
1104
        (request.params as { id: string }).id,
1105
        (request.body as { resolution: string }).resolution,
1106
      );
1107
      await persist(request, 'conflict', conflict.conflictId, conflict);
1108
      return conflict;
1109
    });
1110
    router.post('/v0.1/revocations/agents/:id', async (request) => {
1111
      const id = (request.params as { id: string }).id;
1112
      const current = await scopedEngine(request);
1113
      current.revokeAgent(id);
1114
      await persist(request, 'agent', id, current.agents.get(id));
1115
      return { revoked: true };
1116
    });
1117
    router.post('/v0.1/revocations/delegations/:id', async (request) => {
1118
      const id = (request.params as { id: string }).id;
1119
      const current = await scopedEngine(request);
1120
      current.revokeDelegation(id);
1121
      await persist(request, 'delegation', id, current.delegations.get(id));
1122
      return { revoked: true };
1123
    });
1124
    router.post('/v0.1/contributions/consent', async (request) => {
1125
      const value = z.object({ agentId: z.string(), enabled: z.boolean() }).parse(request.body);
1126
      const current = await scopedEngine(request);
1127
      current.setContributionConsent(value.agentId, value.enabled);
1128
      await persist(request, 'consent', value.agentId, {
1129
        agentId: value.agentId,
1130
        ...current.contributionConsent.get(value.agentId),
1131
      });
1132
      return value;
1133
    });
1134
  });
1135
  return app;
1136
}