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

Viewing files at commit 39e235ac59f4Commit detailsView latest

apps/api/src/app.ts 673 lines · 28.4 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
} from '../../../packages/protocol/src/index.js';
15
import {
16
  FixtureFactCheckProvider,
17
  MemoryAuditStore,
18
  TrustEngine,
19
} from '../../../packages/core/src/index.js';
20
import { buildPublicAgentCard } from '../../../packages/persistence/src/index.js';
21
import type { AgentProfile, HostedRepository } from '../../../packages/persistence/src/index.js';
22
import { toA2AAgentCard } from '../../../packages/sidecar/src/index.js';
23
import {
24
  approveAgentSetup,
25
  getOnboardingState,
26
  rejectAgentSetup,
27
} from '../../../packages/persistence/src/onboarding.js';
28
29
type DashboardRepository = Pick<
30
  HostedRepository,
31
  | 'dashboard'
32
  | 'getSettings'
33
  | 'saveSettings'
34
  | 'upsert'
35
  | 'list'
36
  | 'publishAgent'
37
  | 'unpublishAgent'
38
  | 'getPublishedAgent'
39
  | 'searchPublishedAgents'
40
> &
41
  Partial<
42
    Pick<
43
      HostedRepository,
44
      | 'createFederatedInteraction'
45
      | 'listFederatedInteractions'
46
      | 'getFederatedInteraction'
47
      | 'respondToFederatedInteraction'
48
      | 'getLiveSession'
49
      | 'recordLiveSessionEvent'
50
      | 'getRuntimeVerificationKey'
51
      | 'registerAgentRuntime'
52
      | 'disableAgentRuntime'
53
      | 'deleteAgent'
54
      | 'issueAgentAccessToken'
55
      | 'listAgentAccessTokens'
56
      | 'revokeAgentAccessToken'
57
      | 'receiveTemporaryMessage'
58
      | 'sendTemporaryMessage'
59
      | 'listHostedThreads'
60
      | 'getHostedThread'
61
      | 'markHostedThreadRead'
62
      | 'closeHostedThread'
63
    >
64
  >;
65
66
export function buildApi(
67
  engine = new TrustEngine(),
68
  factChecker = new FixtureFactCheckProvider(),
69
  repository?: DashboardRepository,
70
) {
71
  const app = Fastify({
72
    logger: { redact: ['req.headers.authorization', 'req.body.rawMessage', 'req.body.privateKey'] },
73
  });
74
  const engines = new Map<string, Promise<TrustEngine>>();
75
  void app.register(cors, { origin: true });
76
  void app.register(swagger, { openapi: { info: { title: 'OpenClasp API', version: '0.1.0' } } });
77
78
  app.setErrorHandler((error, _request, reply) =>
79
    reply
80
      .status(
81
        error instanceof Error && 'statusCode' in error && typeof error.statusCode === 'number'
82
          ? error.statusCode
83
          : 400,
84
      )
85
      .send({ error: error instanceof Error ? error.message : 'Unknown error' }),
86
  );
87
  void app.register(async (router) => {
88
    const operatorId = (request: { headers: Record<string, unknown> }) => {
89
      const value = request.headers['x-openclasp-operator'];
90
      if (repository && typeof value !== 'string') {
91
        const error = new Error('Authentication required');
92
        Object.assign(error, { statusCode: 401 });
93
        throw error;
94
      }
95
      return typeof value === 'string' ? value : undefined;
96
    };
97
    const scopedEngine = async (request: { headers: Record<string, unknown> }) => {
98
      const owner = operatorId(request);
99
      if (!repository || !owner) return engine;
100
      let pending = engines.get(owner);
101
      if (!pending) {
102
        pending = repository.list(owner).then((rows) => {
103
          const store = new MemoryAuditStore();
104
          for (const row of rows) {
105
            if (row.kind !== 'interaction') store.append(row.kind, row.recordId, row.payload);
106
          }
107
          return new TrustEngine(store);
108
        });
109
        engines.set(owner, pending);
110
      }
111
      return pending;
112
    };
113
    const persist = async (
114
      request: { headers: Record<string, unknown> },
115
      kind: Parameters<HostedRepository['upsert']>[1],
116
      id: string,
117
      value: unknown,
118
    ) => {
119
      const owner = operatorId(request);
120
      if (repository && owner) await repository.upsert(owner, kind, id, value);
121
    };
122
    const publicBaseUrl = (request: { headers: Record<string, unknown> }) => {
123
      const forwardedHost = request.headers['x-forwarded-host'];
124
      const host = typeof forwardedHost === 'string' ? forwardedHost : request.headers.host;
125
      const forwardedProtocol = request.headers['x-forwarded-proto'];
126
      const protocol = typeof forwardedProtocol === 'string' ? forwardedProtocol : 'https';
127
      return process.env.OPENCLASP_PUBLIC_URL ?? `${protocol}://${String(host ?? 'localhost')}`;
128
    };
129
    router.get('/health', async () => ({ status: 'ok' }));
130
    router.get('/ready', async () => ({ status: 'ready' }));
131
    router.get('/openapi.json', async () => app.swagger());
132
    router.get('/.well-known/openclasp-session-key', async () => {
133
      if (!repository?.getRuntimeVerificationKey)
134
        throw new Error('Runtime verification is not configured');
135
      return repository.getRuntimeVerificationKey();
136
    });
137
    router.get('/extensions/trust/v0.1', async () => ({
138
      uri: 'https://openclasp.vercel.app/extensions/trust/v0.1',
139
      name: 'OpenClasp A2A assurance extension',
140
      version: '0.1',
141
      required: false,
142
      transportsMessages: false,
143
      temporaryChatAdapter: true,
144
      documentation: 'https://github.com/aryaamin/OpenClasp/blob/main/docs/A2A_EXTENSION.md',
145
    }));
146
    router.post('/sessions/:id/events', async (request, reply) => {
147
      if (!repository?.recordLiveSessionEvent)
148
        throw new Error('Live-session reporting is not configured');
149
      const authorization = request.headers.authorization;
150
      if (!authorization?.startsWith('Bearer ')) {
151
        reply.status(401);
152
        return { error: 'session_credential_required' };
153
      }
154
      const event = LiveSessionEventSchema.parse(request.body);
155
      if (event.interactionId !== (request.params as { id: string }).id)
156
        throw new Error('Interaction path does not match the event');
157
      return repository.recordLiveSessionEvent(authorization.slice(7), event);
158
    });
159
    router.post('/a2a/temporary/:id', async (request, reply) => {
160
      if (!repository?.receiveTemporaryMessage)
161
        throw new Error('Temporary chat delivery is not configured');
162
      const authorization = request.headers.authorization;
163
      if (!authorization?.startsWith('Bearer '))
164
        return reply.status(401).send({ error: 'session_credential_required' });
165
      const rpc = z
166
        .object({
167
          jsonrpc: z.literal('2.0'),
168
          id: z.union([z.string().min(1).max(200), z.number()]),
169
          method: z.literal('message/send'),
170
          params: z.object({
171
            message: z.object({
172
              parts: z.array(z.record(z.string(), z.unknown())).min(1).max(100),
173
            }),
174
          }),
175
        })
176
        .parse(request.body);
177
      const content = rpc.params.message.parts
178
        .map((part) => (typeof part.text === 'string' ? part.text : ''))
179
        .filter(Boolean)
180
        .join('\n')
181
        .trim();
182
      if (!content) throw new Error('Temporary chat MVP accepts text parts only');
183
      const recipientAgentId = (request.params as { id: string }).id;
184
      const result = await repository.receiveTemporaryMessage(
185
        authorization.slice(7),
186
        recipientAgentId,
187
        String(rpc.id),
188
        content,
189
      );
190
      return {
191
        jsonrpc: '2.0',
192
        id: rpc.id,
193
        result: {
194
          task: { id: result.message.messageId, state: 'submitted' },
195
          privacyMode: 'openclasp_hosted_temporary',
196
          deduplicated: result.deduplicated,
197
        },
198
      };
199
    });
200
    router.get('/agents/:id/card.json', async (request) => {
201
      if (!repository) throw new Error('Hosted persistence is not configured');
202
      const card = await repository.getPublishedAgent((request.params as { id: string }).id);
203
      if (!card) throw new Error('Published agent not found');
204
      return card;
205
    });
206
    router.get('/agents/:id/.well-known/openclasp-agent.json', async (request) => {
207
      if (!repository) throw new Error('Hosted persistence is not configured');
208
      const card = await repository.getPublishedAgent((request.params as { id: string }).id);
209
      if (!card) throw new Error('Published agent not found');
210
      return card;
211
    });
212
    router.get('/agents/:id/a2a-agent-card.json', async (request) => {
213
      if (!repository) throw new Error('Hosted persistence is not configured');
214
      const card = await repository.getPublishedAgent((request.params as { id: string }).id);
215
      if (!card) throw new Error('Published agent not found');
216
      if (!card.transports.length) throw new Error('Agent has not published an A2A endpoint');
217
      return toA2AAgentCard(card);
218
    });
219
    router.get('/v0.1/dashboard', async (request) => {
220
      const owner = operatorId(request);
221
      if (repository && owner) return repository.dashboard(owner);
222
      return {
223
        agents: [...engine.agents.values()],
224
        projects: [],
225
        installations: [],
226
        setupRequests: [],
227
        publications: [],
228
        interactions: [],
229
        federatedInteractions: [],
230
        liveSessions: [],
231
        hostedThreads: [],
232
        events: [...engine.events.values()],
233
        conflicts: [...engine.conflicts.values()],
234
        receipts: [...engine.receipts.values()],
235
        profiles: [...engine.profiles.values()],
236
        runtimes: [],
237
      };
238
    });
239
    router.get('/v0.1/account', async (request) => {
240
      const owner = operatorId(request);
241
      return {
242
        operatorId: owner,
243
        email: decodeURIComponent(String(request.headers['x-openclasp-email'] ?? '')),
244
        name: decodeURIComponent(String(request.headers['x-openclasp-name'] ?? '')),
245
      };
246
    });
247
    router.get('/v0.1/settings', async (request) => {
248
      const owner = operatorId(request);
249
      if (!repository || !owner)
250
        return {
251
          displayName: '',
252
          contributionEnabled: false,
253
          retentionDays: 30,
254
          evidenceSharing: 'ask',
255
          rawConversationsStored: false,
256
        };
257
      return repository.getSettings(owner);
258
    });
259
    router.get('/v0.1/onboarding', async (request) => {
260
      const owner = operatorId(request);
261
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
262
      return getOnboardingState(repository, owner);
263
    });
264
    router.post('/v0.1/onboarding/:id/approve', async (request) => {
265
      const owner = operatorId(request);
266
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
267
      const requestId = (request.params as { id: string }).id;
268
      const state = await getOnboardingState(repository, owner);
269
      const setup = state.setupRequests.find((item) => item.requestId === requestId);
270
      const result = await approveAgentSetup(repository, owner, requestId);
271
      if (result.status === 'connected' && setup?.autoPublish) {
272
        const previous = await repository.getPublishedAgent(result.agent.agentId);
273
        const card = await repository.publishAgent(
274
          owner,
275
          buildPublicAgentCard(result.agent, publicBaseUrl(request), previous),
276
        );
277
        await repository.upsert(owner, 'publication', result.agent.agentId, {
278
          agentId: result.agent.agentId,
279
          published: true,
280
          updatedAt: card.updatedAt,
281
        });
282
      }
283
      return result;
284
    });
285
    router.post('/v0.1/onboarding/:id/reject', async (request) => {
286
      const owner = operatorId(request);
287
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
288
      return rejectAgentSetup(repository, owner, (request.params as { id: string }).id);
289
    });
290
    router.put('/v0.1/settings', async (request) => {
291
      const owner = operatorId(request);
292
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
293
      const value = z
294
        .object({
295
          displayName: z.string().trim().max(100),
296
          contributionEnabled: z.boolean(),
297
          retentionDays: z.number().int().min(0).max(3650),
298
          evidenceSharing: z.enum(['never', 'ask', 'contract_only']),
299
        })
300
        .parse(request.body);
301
      return repository.saveSettings(owner, value);
302
    });
303
    router.post('/v0.1/agents', async (request) => {
304
      const owner = operatorId(request);
305
      const current = await scopedEngine(request);
306
      const identity = AgentIdentitySchema.parse(request.body);
307
      if (owner && identity.operatorRef !== `auth0:${owner}`)
308
        throw new Error('Agent identity is not owned by the authenticated operator');
309
      const registered = current.registerAgent(identity);
310
      await persist(request, 'agent', identity.agentId, registered);
311
      return registered;
312
    });
313
    router.get('/v0.1/agents/:id', async (request) => {
314
      const { id } = request.params as { id: string };
315
      const value = (await scopedEngine(request)).agents.get(id);
316
      if (!value) throw new Error('Agent not found');
317
      return value;
318
    });
319
    router.post('/v0.1/agents/:id/publication', async (request) => {
320
      const owner = operatorId(request);
321
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
322
      const { id } = request.params as { id: string };
323
      const { published } = z.object({ published: z.boolean() }).parse(request.body);
324
      const rows = await repository.list(owner);
325
      const agent = rows.find((row) => row.kind === 'agent_profile' && row.recordId === id)
326
        ?.payload as AgentProfile | undefined;
327
      if (!agent) throw new Error('Owned agent not found');
328
      if (agent.status !== 'active') throw new Error('Revoked agents cannot be published');
329
      if (!published) {
330
        await repository.unpublishAgent(owner, id);
331
        const publication = { agentId: id, published: false, updatedAt: new Date().toISOString() };
332
        await repository.upsert(owner, 'publication', id, publication);
333
        return publication;
334
      }
335
      const previous = await repository.getPublishedAgent(id);
336
      const card = await repository.publishAgent(
337
        owner,
338
        buildPublicAgentCard(agent, publicBaseUrl(request), previous),
339
      );
340
      const publication = { agentId: id, published: true, updatedAt: card.updatedAt };
341
      await repository.upsert(owner, 'publication', id, publication);
342
      return { ...publication, card };
343
    });
344
    router.put('/v0.1/agents/:id/automation', async (request) => {
345
      const owner = operatorId(request);
346
      if (!repository || !owner) throw new Error('Hosted persistence is not configured');
347
      const { id } = request.params as { id: string };
348
      const value = z
349
        .object({
350
          autoPublish: z.boolean(),
351
          autoAcceptPolicy: z.enum(['off', 'safe_matching']),
352
          autoAcceptTaskCategories: z.array(z.string().trim().min(1).max(100)).max(100),
353
        })
354
        .parse(request.body);
355
      const rows = await repository.list(owner);
356
      const current = rows.find((row) => row.kind === 'agent_profile' && row.recordId === id)
357
        ?.payload as AgentProfile | undefined;
358
      if (!current) throw new Error('Owned agent not found');
359
      const agent: AgentProfile = {
360
        ...current,
361
        transport: 'direct_a2a',
362
        description: current.description ?? '',
363
        agentVersion: current.agentVersion ?? '1.0.0',
364
        autoPublish: value.autoPublish,
365
        autoAcceptPolicy: value.autoAcceptPolicy,
366
        autoAcceptTaskCategories: [...new Set(value.autoAcceptTaskCategories)],
367
        updatedAt: new Date().toISOString(),
368
      };
369
      await repository.upsert(owner, 'agent_profile', id, agent);
370
      if (value.autoPublish) {
371
        const previous = await repository.getPublishedAgent(id);
372
        const card = await repository.publishAgent(
373
          owner,
374
          buildPublicAgentCard(agent, publicBaseUrl(request), previous),
375
        );
376
        await repository.upsert(owner, 'publication', id, {
377
          agentId: id,
378
          published: true,
379
          updatedAt: card.updatedAt,
380
        });
381
      } else {
382
        await repository.unpublishAgent(owner, id);
383
        await repository.upsert(owner, 'publication', id, {
384
          agentId: id,
385
          published: false,
386
          updatedAt: new Date().toISOString(),
387
        });
388
      }
389
      return agent;
390
    });
391
    router.put('/v0.1/agents/:id/runtime', async (request) => {
392
      const owner = operatorId(request);
393
      if (!repository?.registerAgentRuntime || !owner)
394
        throw new Error('Hosted runtime delivery is not configured');
395
      const endpoint = z
396
        .object({ endpoint: z.string().url().max(2048) })
397
        .parse(request.body).endpoint;
398
      return repository.registerAgentRuntime(
399
        owner,
400
        (request.params as { id: string }).id,
401
        endpoint,
402
      );
403
    });
404
    router.delete('/v0.1/agents/:id/runtime', async (request) => {
405
      const owner = operatorId(request);
406
      if (!repository?.disableAgentRuntime || !owner)
407
        throw new Error('Hosted runtime delivery is not configured');
408
      return repository.disableAgentRuntime(owner, (request.params as { id: string }).id);
409
    });
410
    router.get('/v0.1/agents/:id/access-tokens', async (request) => {
411
      const owner = operatorId(request);
412
      if (!repository?.listAgentAccessTokens || !owner)
413
        throw new Error('Agent access tokens are not configured');
414
      return repository.listAgentAccessTokens(owner, (request.params as { id: string }).id);
415
    });
416
    router.post('/v0.1/agents/:id/access-tokens', async (request) => {
417
      const owner = operatorId(request);
418
      if (!repository?.issueAgentAccessToken || !owner)
419
        throw new Error('Agent access tokens are not configured');
420
      const value = z
421
        .object({
422
          name: z.string().trim().min(1).max(100),
423
          expiresInDays: z.number().int().min(1).max(365).default(365),
424
        })
425
        .parse(request.body);
426
      return repository.issueAgentAccessToken(owner, (request.params as { id: string }).id, value);
427
    });
428
    router.delete('/v0.1/agents/:id/access-tokens/:tokenId', async (request) => {
429
      const owner = operatorId(request);
430
      if (!repository?.revokeAgentAccessToken || !owner)
431
        throw new Error('Agent access tokens are not configured');
432
      const params = request.params as { id: string; tokenId: string };
433
      return repository.revokeAgentAccessToken(owner, params.id, params.tokenId);
434
    });
435
    router.delete('/v0.1/agents/:id', async (request) => {
436
      const owner = operatorId(request);
437
      if (!repository?.deleteAgent || !owner)
438
        throw new Error('Hosted agent deletion is not configured');
439
      const result = await repository.deleteAgent(owner, (request.params as { id: string }).id);
440
      engines.delete(owner);
441
      return result;
442
    });
443
    router.get('/v0.1/agents/:id/threads', async (request) => {
444
      const owner = operatorId(request);
445
      if (!repository?.listHostedThreads || !owner)
446
        throw new Error('Temporary chat history is not configured');
447
      return repository.listHostedThreads(owner, (request.params as { id: string }).id);
448
    });
449
    router.get('/v0.1/agents/:id/threads/:threadId', async (request) => {
450
      const owner = operatorId(request);
451
      if (!repository?.getHostedThread || !owner)
452
        throw new Error('Temporary chat history is not configured');
453
      const params = request.params as { id: string; threadId: string };
454
      return repository.getHostedThread(owner, params.id, params.threadId);
455
    });
456
    router.post('/v0.1/agents/:id/messages', async (request) => {
457
      const owner = operatorId(request);
458
      if (!repository?.sendTemporaryMessage || !owner)
459
        throw new Error('Temporary chat delivery is not configured');
460
      const value = z
461
        .object({ interactionId: z.string().uuid(), content: z.string().trim().min(1).max(20_000) })
462
        .parse(request.body);
463
      return repository.sendTemporaryMessage(
464
        owner,
465
        (request.params as { id: string }).id,
466
        value.interactionId,
467
        value.content,
468
      );
469
    });
470
    router.post('/v0.1/agents/:id/threads/:threadId/read', async (request) => {
471
      const owner = operatorId(request);
472
      if (!repository?.markHostedThreadRead || !owner)
473
        throw new Error('Temporary chat history is not configured');
474
      const params = request.params as { id: string; threadId: string };
475
      return repository.markHostedThreadRead(owner, params.id, params.threadId);
476
    });
477
    router.post('/v0.1/agents/:id/threads/:threadId/close', async (request) => {
478
      const owner = operatorId(request);
479
      if (!repository?.closeHostedThread || !owner)
480
        throw new Error('Temporary chat history is not configured');
481
      const params = request.params as { id: string; threadId: string };
482
      return repository.closeHostedThread(owner, params.id, params.threadId);
483
    });
484
    router.get('/v0.1/directory', async (request) => {
485
      operatorId(request);
486
      if (!repository) throw new Error('Hosted persistence is not configured');
487
      const query = z
488
        .object({
489
          query: z.string().trim().max(100).optional(),
490
          capability: z.string().trim().max(100).optional(),
491
          limit: z.coerce.number().int().min(1).max(50).optional(),
492
        })
493
        .parse(request.query);
494
      return repository.searchPublishedAgents(query);
495
    });
496
    router.get('/v0.1/directory/:id', async (request) => {
497
      operatorId(request);
498
      if (!repository) throw new Error('Hosted persistence is not configured');
499
      const card = await repository.getPublishedAgent((request.params as { id: string }).id);
500
      if (!card) throw new Error('Published agent not found');
501
      return card;
502
    });
503
    router.get('/v0.1/federated-interactions', async (request) => {
504
      const owner = operatorId(request);
505
      if (!repository?.listFederatedInteractions || !owner)
506
        throw new Error('Federated interactions are not configured');
507
      return repository.listFederatedInteractions(owner);
508
    });
509
    router.get('/v0.1/federated-interactions/:id', async (request) => {
510
      const owner = operatorId(request);
511
      if (!repository?.getFederatedInteraction || !owner)
512
        throw new Error('Federated interactions are not configured');
513
      const interaction = await repository.getFederatedInteraction(
514
        owner,
515
        (request.params as { id: string }).id,
516
      );
517
      if (!interaction) throw new Error('Interaction not found');
518
      return interaction;
519
    });
520
    router.get('/v0.1/federated-interactions/:id/session', async (request) => {
521
      const owner = operatorId(request);
522
      if (!repository?.getLiveSession || !owner)
523
        throw new Error('Live sessions are not configured');
524
      const value = z.object({ agentId: z.string().min(1) }).parse(request.query);
525
      return repository.getLiveSession(owner, (request.params as { id: string }).id, value.agentId);
526
    });
527
    router.post('/v0.1/federated-interactions', async (request) => {
528
      const owner = operatorId(request);
529
      if (!repository?.createFederatedInteraction || !owner)
530
        throw new Error('Federated interactions are not configured');
531
      const interaction = FederatedInteractionSchema.parse(request.body);
532
      const acceptance = interaction.acceptances[interaction.initiatorAgentId];
533
      if (!acceptance) throw new Error('Initiator acceptance is required');
534
      return repository.createFederatedInteraction(owner, {
535
        ...interaction,
536
        acceptances: {
537
          [interaction.initiatorAgentId]: { ...acceptance, method: 'oauth_account' },
538
        },
539
      });
540
    });
541
    router.post('/v0.1/federated-interactions/:id/respond', async (request) => {
542
      const owner = operatorId(request);
543
      if (!repository?.respondToFederatedInteraction || !owner)
544
        throw new Error('Federated interactions are not configured');
545
      const value = z
546
        .object({ agentId: z.string().min(1), decision: z.enum(['accept', 'reject']) })
547
        .parse(request.body);
548
      return repository.respondToFederatedInteraction(
549
        owner,
550
        (request.params as { id: string }).id,
551
        value.agentId,
552
        value.decision,
553
        'oauth_account',
554
      );
555
    });
556
    router.post('/v0.1/delegations', async (request) => {
557
      const value = DelegationCredentialSchema.parse(request.body);
558
      const current = await scopedEngine(request);
559
      current.delegations.set(value.delegationId, value);
560
      await persist(request, 'delegation', value.delegationId, value);
561
      return { valid: current.verifyDelegation(value.delegationId) };
562
    });
563
    router.post('/v0.1/delegations/:id/verify', async (request) => ({
564
      valid: (await scopedEngine(request)).verifyDelegation((request.params as { id: string }).id),
565
    }));
566
    router.post('/v0.1/interactions/contracts', async (request) => {
567
      const contract = (await scopedEngine(request)).saveContract(
568
        InteractionContractSchema.parse(request.body),
569
      );
570
      await persist(request, 'contract', contract.interactionId, contract);
571
      return contract;
572
    });
573
    router.post('/v0.1/events', async (request) => {
574
      const event = (await scopedEngine(request)).recordEvent(
575
        InteractionEventSchema.parse(request.body),
576
      );
577
      await persist(request, 'event', event.eventId, event);
578
      return event;
579
    });
580
    router.post('/v0.1/risk/assess', async (request) =>
581
      (await scopedEngine(request)).assess(request.body as any),
582
    );
583
    router.get('/v0.1/profiles/:id', async (request) => {
584
      const { id } = request.params as { id: string };
585
      const query = request.query as { version?: string; taskCategory?: string };
586
      return (await scopedEngine(request)).getRisk(
587
        id,
588
        query.version ?? '1.0.0',
589
        query.taskCategory ?? 'general',
590
      );
591
    });
592
    router.post('/v0.1/claims/check', async (request) =>
593
      ((value) => factChecker.check(value.claim, value.permission))(
594
        z.object({ claim: z.string(), permission: z.boolean().optional() }).parse(request.body),
595
      ),
596
    );
597
    router.post('/v0.1/receipts', async (request) => {
598
      const receipt = (await scopedEngine(request)).submitReceipt(
599
        ReceiptSchema.parse(request.body),
600
      );
601
      await persist(request, 'receipt', receipt.receiptId, receipt);
602
      return receipt;
603
    });
604
    router.post('/v0.1/receipts/verify', async (request) => {
605
      try {
606
        (await scopedEngine(request)).verifyReceipt(ReceiptSchema.parse(request.body));
607
        return { valid: true };
608
      } catch {
609
        return { valid: false };
610
      }
611
    });
612
    router.post('/v0.1/feedback', async (request) => {
613
      const feedback = FeedbackSchema.parse(request.body);
614
      const current = await scopedEngine(request);
615
      const result = current.submitFeedback(feedback);
616
      await persist(request, 'feedback', feedback.feedbackId, feedback);
617
      for (const profile of current.profiles.values())
618
        await persist(
619
          request,
620
          'profile',
621
          `${profile.agentId}|${profile.agentVersion}|${profile.taskCategory}`,
622
          profile,
623
        );
624
      return result;
625
    });
626
    router.post('/v0.1/conflicts', async (request) => {
627
      const conflict = (await scopedEngine(request)).createConflict(request.body as any);
628
      await persist(request, 'conflict', conflict.conflictId, conflict);
629
      return conflict;
630
    });
631
    router.post('/v0.1/conflicts/:id/permit', async (request) => {
632
      const conflict = (await scopedEngine(request)).permitMediation(
633
        (request.params as { id: string }).id,
634
        (request.body as { agentId: string }).agentId,
635
      );
636
      await persist(request, 'conflict', conflict.conflictId, conflict);
637
      return conflict;
638
    });
639
    router.post('/v0.1/conflicts/:id/resolve', async (request) => {
640
      const conflict = (await scopedEngine(request)).resolveConflict(
641
        (request.params as { id: string }).id,
642
        (request.body as { resolution: string }).resolution,
643
      );
644
      await persist(request, 'conflict', conflict.conflictId, conflict);
645
      return conflict;
646
    });
647
    router.post('/v0.1/revocations/agents/:id', async (request) => {
648
      const id = (request.params as { id: string }).id;
649
      const current = await scopedEngine(request);
650
      current.revokeAgent(id);
651
      await persist(request, 'agent', id, current.agents.get(id));
652
      return { revoked: true };
653
    });
654
    router.post('/v0.1/revocations/delegations/:id', async (request) => {
655
      const id = (request.params as { id: string }).id;
656
      const current = await scopedEngine(request);
657
      current.revokeDelegation(id);
658
      await persist(request, 'delegation', id, current.delegations.get(id));
659
      return { revoked: true };
660
    });
661
    router.post('/v0.1/contributions/consent', async (request) => {
662
      const value = z.object({ agentId: z.string(), enabled: z.boolean() }).parse(request.body);
663
      const current = await scopedEngine(request);
664
      current.setContributionConsent(value.agentId, value.enabled);
665
      await persist(request, 'consent', value.agentId, {
666
        agentId: value.agentId,
667
        ...current.contributionConsent.get(value.agentId),
668
      });
669
      return value;
670
    });
671
  });
672
  return app;
673
}