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

Viewing files at commit c2393f876b9dCommit detailsView latest

apps/demo/src/scenario.ts 502 lines · 17.5 KB · typescript
1
import { randomUUID } from 'node:crypto';
2
import {
3
  canonicalHash,
4
  InteractionCompletionReportSchema,
5
  InteractionFeedbackSchema,
6
  PublicAgentCardSchema,
7
  signNamed,
8
  signObject,
9
  type Feedback,
10
  type InteractionContract,
11
  type Receipt,
12
} from '../../../packages/protocol/src/index.js';
13
import {
14
  buildCounterpartyBrief,
15
  buildInteractionConclusion,
16
  createIdentity,
17
  deriveBehaviouralObservations,
18
  evaluateLearningEligibility,
19
  FixtureFactCheckProvider,
20
  TrustEngine,
21
  updateContextualBehaviouralProfile,
22
} from '../../../packages/core/src/index.js';
23
import { attestSessionRecord } from '../../../packages/persistence/src/relay.js';
24
import { createSignedEnvelope, createSignedEvent } from '../../../packages/sdk/src/index.js';
25
import { OpenClaspSidecar, openClaspA2AExtension } from '../../../packages/sidecar/src/index.js';
26
27
export async function runDemo(log: (line: string) => void = () => undefined) {
28
  const engine = new TrustEngine();
29
  const requester = createIdentity({
30
    agentId: 'agent:requester',
31
    operatorRef: 'operator:acme',
32
    capabilities: ['research.request'],
33
  });
34
  const provider = createIdentity({
35
    agentId: 'agent:provider',
36
    operatorRef: 'operator:labs',
37
    capabilities: ['research.answer', 'research.delegate'],
38
  });
39
  const subagent = createIdentity({
40
    agentId: 'agent:provider:subagent',
41
    operatorRef: 'operator:labs',
42
    capabilities: ['research.answer'],
43
    parentAgentId: provider.identity.agentId,
44
    rootControllerId: provider.identity.rootControllerId,
45
  });
46
  for (const agent of [requester, provider, subagent]) engine.registerAgent(agent.identity);
47
  log('1. Created and verified requester, provider, and subagent identities');
48
49
  const delegation = engine.createDelegation(
50
    provider.identity.agentId,
51
    subagent.identity.agentId,
52
    ['research.answer'],
53
    new Date(Date.now() + 60_000).toISOString(),
54
    provider.keyPair,
55
  );
56
  if (!engine.verifyDelegation(delegation.delegationId, 'research.answer'))
57
    throw new Error('Delegation verification failed');
58
  log('2. Verified scoped parent/subagent delegation');
59
60
  const interactionId = randomUUID();
61
  const baseContract: InteractionContract = {
62
    protocolVersion: '0.1',
63
    interactionId,
64
    purpose: 'Summarize a public research fixture',
65
    parties: [requester.identity.agentId, provider.identity.agentId],
66
    taskCategory: 'research.summary',
67
    requestedOutcome: 'Evidence-backed summary',
68
    successCriteria: ['Citations included'],
69
    allowedActions: ['research.answer'],
70
    prohibitedActions: ['publish.private_data'],
71
    allowedData: ['public'],
72
    prohibitedData: ['private'],
73
    evidenceRequirements: ['authoritative_source'],
74
    delegationRules: ['research.answer'],
75
    humanApprovalRequirements: ['publish'],
76
    factCheckingPolicy: 'important_claims',
77
    mediationPolicy: 'mutual_consent',
78
    retentionDays: 30,
79
    completionConditions: ['summary_delivered'],
80
    cancellationConditions: ['either_party'],
81
    signatures: {},
82
  };
83
  const requesterSigned = signNamed(
84
    baseContract as unknown as Record<string, unknown>,
85
    requester.identity.agentId,
86
    requester.keyPair,
87
  ) as unknown as InteractionContract;
88
  const contract = signNamed(
89
    requesterSigned as unknown as Record<string, unknown>,
90
    provider.identity.agentId,
91
    provider.keyPair,
92
  ) as unknown as InteractionContract;
93
  engine.saveContract(contract);
94
  log('3. Exchanged expectations and signed a minimal interaction contract');
95
96
  const providerCard = PublicAgentCardSchema.parse({
97
    protocolVersion: '0.1',
98
    agentId: provider.identity.agentId,
99
    name: 'Research Provider',
100
    description: 'Produces evidence-backed research summaries',
101
    framework: 'Demo runtime',
102
    agentVersion: provider.identity.agentVersion,
103
    capabilities: ['research.answer', 'research.summary', 'authoritative_source'],
104
    limitations: ['public data only'],
105
    assurance: 'cryptographically_verified',
106
    transports: [
107
      {
108
        protocol: 'A2A/1.0',
109
        protocolBinding: 'JSONRPC',
110
        endpoint: 'https://provider.example/a2a',
111
      },
112
    ],
113
    cardUrl: 'https://openclasp.example/agents/provider/card.json',
114
    a2aAgentCardUrl: 'https://openclasp.example/agents/provider/a2a-agent-card.json',
115
    extensionUri: 'https://openclasp.example/extensions/trust/v0.1',
116
    publishedAt: new Date().toISOString(),
117
    updatedAt: new Date().toISOString(),
118
  });
119
  const initialBrief = buildCounterpartyBrief({
120
    interactionId,
121
    contractHash: canonicalHash({ ...contract, signatures: {} }),
122
    contract,
123
    recipientAgentId: requester.identity.agentId,
124
    subject: providerCard,
125
    historyInsights: [
126
      {
127
        code: 'limited_verified_history',
128
        severity: 'caution',
129
        message: 'Ask for task-relevant evidence because this version has limited history.',
130
        evidenceReferences: [],
131
        requirementReferences: [],
132
      },
133
    ],
134
    relevantSampleSize: 0,
135
    historyConfidence: 0,
136
    expiresAt: new Date(Date.now() + 60_000).toISOString(),
137
  });
138
  if (
139
    initialBrief.recipientAgentId !== requester.identity.agentId ||
140
    initialBrief.subjectAgentId !== provider.identity.agentId ||
141
    initialBrief.decision !== 'CHALLENGE'
142
  )
143
    throw new Error('Private counterparty brief was not recipient-bound');
144
  log('3a. Delivered a recipient-bound counterparty brief before the A2A session');
145
146
  engine.setContributionConsent(provider.identity.agentId, true);
147
  const contractHash = canonicalHash({ ...contract, signatures: {} });
148
  const envelope = createSignedEnvelope(
149
    {
150
      protocolVersion: '0.1',
151
      interactionId,
152
      requestingAgentId: requester.identity.agentId,
153
      respondingAgentId: provider.identity.agentId,
154
      rootControllerId: requester.identity.rootControllerId,
155
      agentVersion: requester.identity.agentVersion,
156
      requestedCapability: 'research.answer',
157
      taskCategory: 'research.summary',
158
      contractHash,
159
      dataSharingMode: 'structured_only',
160
      evidenceRequirements: ['authoritative_source'],
161
      timestamp: new Date().toISOString(),
162
      expiresAt: new Date(Date.now() + 60_000).toISOString(),
163
      nonce: randomUUID(),
164
    },
165
    requester.keyPair,
166
  );
167
  const claim = 'OpenClasp uses a blockchain';
168
  const claimEvent = createSignedEvent(
169
    {
170
      protocolVersion: '0.1',
171
      eventId: randomUUID(),
172
      interactionId,
173
      eventType: 'claim',
174
      agentId: provider.identity.agentId,
175
      agentVersion: provider.identity.agentVersion,
176
      timestamp: new Date().toISOString(),
177
      visibility: 'network_aggregate',
178
      provenance: 'observed',
179
      payload: { claim },
180
      evidenceRefs: [],
181
    },
182
    provider.keyPair,
183
  );
184
  const sidecar = new OpenClaspSidecar(engine);
185
  const extension = openClaspA2AExtension();
186
  const forwarding = await sidecar.forward({
187
    message: {
188
      role: 'ROLE_USER',
189
      parts: [{ text: 'Provide the summary' }],
190
      extensions: [extension.uri ?? ''],
191
      metadata: { [extension.uri ?? '']: envelope, rawMessage: 'must stay local' },
192
    },
193
    envelope,
194
    policy: { action: 'research.answer', dataClasses: ['public'] },
195
    structuredEvents: [claimEvent],
196
    send: async () => ({ kind: 'message', text: claim }),
197
  });
198
  if (
199
    !forwarding.forwarded ||
200
    JSON.stringify(forwarding.networkPayloads).includes('rawMessage') ||
201
    JSON.stringify(forwarding.networkPayloads).includes(claim)
202
  )
203
    throw new Error('Structured-only privacy boundary failed');
204
  log('4. Forwarded an A2A message with trust metadata; raw content stayed local');
205
206
  const factChecker = new FixtureFactCheckProvider({
207
    [claim]: { status: 'contradicted', evidence: ['docs:data-boundary#no-blockchain'] },
208
  });
209
  const check = await factChecker.check(claim);
210
  if (check.status !== 'contradicted') throw new Error('Fact check did not contradict fixture');
211
  log('5. Issued a private evidence-backed contradiction warning');
212
213
  const blockedEnvelope = createSignedEnvelope(
214
    { ...envelope, nonce: randomUUID(), timestamp: new Date().toISOString() },
215
    requester.keyPair,
216
  );
217
  const blocked = engine.assess({
218
    envelope: blockedEnvelope,
219
    action: 'publish.private_data',
220
    dataClasses: ['private'],
221
  });
222
  if (blocked.decision !== 'DENY') throw new Error('Scope violation was not blocked');
223
  log('6. Blocked deterministic out-of-scope and prohibited-data action');
224
225
  const conflict = engine.createConflict({
226
    interactionId,
227
    issue: 'Unsupported factual claim',
228
    participants: [requester.identity.agentId, provider.identity.agentId],
229
    positions: {
230
      [requester.identity.agentId]: 'Claim is contradicted',
231
      [provider.identity.agentId]: 'Claim should be corrected',
232
    },
233
    evidence: check.evidenceReferences,
234
    contractClauses: ['evidenceRequirements'],
235
    missingInformation: [],
236
    possibleResolutions: ['Correct the claim and cite the architecture'],
237
  });
238
  engine.permitMediation(conflict.conflictId, requester.identity.agentId);
239
  engine.permitMediation(conflict.conflictId, provider.identity.agentId);
240
  engine.resolveConflict(conflict.conflictId, 'Provider corrected the claim');
241
  log('7. Opened and resolved a mutually consented conflict');
242
243
  const baseReceipt: Receipt = {
244
    receiptId: randomUUID(),
245
    interactionId,
246
    participants: [requester.identity.agentId, provider.identity.agentId],
247
    agentVersions: { [requester.identity.agentId]: '1.0.0', [provider.identity.agentId]: '1.0.0' },
248
    contractHash,
249
    startedAt: contract.signatures[requester.identity.agentId]
250
      ? new Date(Date.now() - 5000).toISOString()
251
      : new Date().toISOString(),
252
    completedAt: new Date().toISOString(),
253
    outcome: 'success',
254
    commitmentsFulfilled: ['summary_delivered'],
255
    commitmentsMissed: [],
256
    evidenceHashes: [canonicalHash(check.evidenceReferences)],
257
    policyWarnings: ['contradicted_claim'],
258
    policyViolations: ['blocked_scope_attempt'],
259
    disputeStatus: 'resolved',
260
    delegationChainHash: canonicalHash(delegation),
261
    unilateral: false,
262
    signatures: {},
263
  };
264
  const receiptOne = signNamed(
265
    baseReceipt as unknown as Record<string, unknown>,
266
    requester.identity.agentId,
267
    requester.keyPair,
268
  ) as unknown as Receipt;
269
  const receipt = signNamed(
270
    receiptOne as unknown as Record<string, unknown>,
271
    provider.identity.agentId,
272
    provider.keyPair,
273
  ) as unknown as Receipt;
274
  engine.submitReceipt(receipt);
275
  log('8. Independently verified a bilateral completion receipt');
276
277
  const feedbackBase = (
278
    reviewer: typeof requester,
279
    subject: typeof provider,
280
  ): Omit<Feedback, 'signature'> => ({
281
    feedbackId: randomUUID(),
282
    interactionId,
283
    receiptId: receipt.receiptId,
284
    reviewerAgentId: reviewer.identity.agentId,
285
    subjectAgentId: subject.identity.agentId,
286
    taskCompleted: true,
287
    outputAccepted: true,
288
    specificationMatched: true,
289
    deadlineMet: true,
290
    communicationQuality: 0.9,
291
    evidenceProvided: true,
292
    scopeRespected: true,
293
    disputeRaised: false,
294
    submittedAt: new Date().toISOString(),
295
  });
296
  const firstFeedback = signObject(
297
    feedbackBase(requester, provider),
298
    requester.keyPair,
299
  ) as Feedback;
300
  const secondFeedback = signObject(
301
    feedbackBase(provider, requester),
302
    provider.keyPair,
303
  ) as Feedback;
304
  if (engine.submitFeedback(firstFeedback).revealed) throw new Error('Feedback revealed too early');
305
  if (!engine.submitFeedback(secondFeedback).revealed)
306
    throw new Error('Bilateral feedback was not revealed');
307
  const learnedRisk = engine.getRisk(provider.identity.agentId, '1.0.0', 'research.summary');
308
  const newVersionRisk = engine.getRisk(provider.identity.agentId, '2.0.0', 'research.summary');
309
  if (learnedRisk.sampleSize !== 1 || newVersionRisk.confidence >= learnedRisk.confidence)
310
    throw new Error('Behavioural learning/version reduction failed');
311
  log('9. Updated contextual history; new versions inherit reduced confidence');
312
313
  const platformSecret = 'demo-platform-attestation-secret-at-least-32-bytes';
314
  const completionReport = (
315
    reportingAgentId: string,
316
    counterpartyAgentId: string,
317
    summary: string,
318
  ) => {
319
    const base = InteractionCompletionReportSchema.parse({
320
      reportId: randomUUID(),
321
      interactionId,
322
      contractHash,
323
      reportingAgentId,
324
      counterpartyAgentId,
325
      agentVersion: '1.0.0',
326
      outcome: 'success',
327
      summary,
328
      requestedOutcome: contract.requestedOutcome,
329
      criteria: [{ criterion: 'Citations included', status: 'met' }],
330
      evidenceReferences: ['evidence:demo:authoritative-source'],
331
      completedAt: new Date().toISOString(),
332
      confidence: 0.9,
333
      dataSharingMode: 'structured_only',
334
      submissionMethod: 'runtime_session',
335
    });
336
    return InteractionCompletionReportSchema.parse({
337
      ...base,
338
      platformAttestation: attestSessionRecord(platformSecret, base),
339
    });
340
  };
341
  const interactionReports = [
342
    completionReport(
343
      requester.identity.agentId,
344
      provider.identity.agentId,
345
      'Requested summary was received.',
346
    ),
347
    completionReport(
348
      provider.identity.agentId,
349
      requester.identity.agentId,
350
      'Evidence-backed summary was delivered.',
351
    ),
352
  ];
353
  const interactionFeedback = (
354
    reviewerAgentId: string,
355
    subjectAgentId: string,
356
    privateComment: string,
357
  ) => {
358
    const base = InteractionFeedbackSchema.parse({
359
      feedbackId: randomUUID(),
360
      requestId: randomUUID(),
361
      interactionId,
362
      reviewerAgentId,
363
      subjectAgentId,
364
      reviewerAgentVersion: '1.0.0',
365
      ratings: {
366
        overall_satisfaction: 0.9,
367
        outcome_satisfaction: 0.9,
368
        communication: 0.85,
369
        timeliness: 0.9,
370
        scope_adherence: 1,
371
        evidence_quality: 0.9,
372
        correction_handling: 0.9,
373
        reliability: 0.9,
374
      },
375
      wouldWorkAgain: 'yes',
376
      privateComment,
377
      evidenceReferences: ['evidence:demo:authoritative-source'],
378
      confidence: 0.9,
379
      submittedAt: new Date().toISOString(),
380
      submissionMethod: 'runtime_session',
381
    });
382
    return InteractionFeedbackSchema.parse({
383
      ...base,
384
      platformAttestation: attestSessionRecord(platformSecret, base),
385
    });
386
  };
387
  const interactionFeedbackItems = [
388
    interactionFeedback(
389
      requester.identity.agentId,
390
      provider.identity.agentId,
391
      'requester-private-note',
392
    ),
393
    interactionFeedback(
394
      provider.identity.agentId,
395
      requester.identity.agentId,
396
      'provider-private-note',
397
    ),
398
  ];
399
  const structuredConclusion = buildInteractionConclusion({
400
    interaction: { interactionId, termsHash: contractHash, contract },
401
    reports: interactionReports,
402
    feedback: interactionFeedbackItems,
403
  });
404
  const contributionMode = () =>
405
    [requester.identity.agentId, provider.identity.agentId].every(
406
      (agentId) => engine.contributionConsent.get(agentId)?.enabled,
407
    )
408
      ? ('network_aggregate' as const)
409
      : ('local_only' as const);
410
  const localEligibility = evaluateLearningEligibility({
411
    interactionId,
412
    reports: interactionReports,
413
    feedback: interactionFeedbackItems,
414
    consensus: structuredConclusion.consensus,
415
    contributionMode: contributionMode(),
416
  });
417
  engine.setContributionConsent(requester.identity.agentId, true);
418
  const networkEligibility = evaluateLearningEligibility({
419
    interactionId,
420
    reports: interactionReports,
421
    feedback: interactionFeedbackItems,
422
    consensus: structuredConclusion.consensus,
423
    contributionMode: contributionMode(),
424
  });
425
  const observations = deriveBehaviouralObservations({
426
    subjectAgentId: provider.identity.agentId,
427
    reports: interactionReports,
428
    reviewerFeedback: interactionFeedbackItems[0]!,
429
    conclusion: structuredConclusion,
430
  });
431
  const weightedProfile = updateContextualBehaviouralProfile({
432
    observations,
433
    sampleWeight: localEligibility.sampleWeight,
434
  });
435
  if (
436
    !localEligibility.eligible ||
437
    localEligibility.contributionMode !== 'local_only' ||
438
    networkEligibility.contributionMode !== 'network_aggregate' ||
439
    weightedProfile.profile.sampleSize !== 1 ||
440
    JSON.stringify(structuredConclusion).includes('private-note')
441
  )
442
    throw new Error('Structured completion, feedback, or eligibility lifecycle failed');
443
  log('9a. Attested bilateral outcomes, concealed comments, and applied weighted local learning');
444
445
  const tampered = { ...receipt, outcome: 'failure' as const };
446
  let tamperRejected = false;
447
  try {
448
    engine.submitReceipt(tampered);
449
  } catch {
450
    tamperRejected = true;
451
  }
452
  const expired = engine.createDelegation(
453
    provider.identity.agentId,
454
    subagent.identity.agentId,
455
    ['research.answer'],
456
    new Date(Date.now() - 1000).toISOString(),
457
    provider.keyPair,
458
  );
459
  const replay = engine.assess({ envelope, action: 'research.answer' });
460
  const invalidIdentity = { ...requester.identity, operatorRef: 'operator:attacker' };
461
  let invalidRejected = false;
462
  try {
463
    engine.registerAgent(invalidIdentity);
464
  } catch {
465
    invalidRejected = true;
466
  }
467
  if (
468
    !tamperRejected ||
469
    engine.verifyDelegation(expired.delegationId) ||
470
    replay.decision !== 'DENY' ||
471
    !invalidRejected
472
  )
473
    throw new Error('Adversarial checks failed');
474
  log('10. Rejected tampered receipt, expired delegation, replay, and invalid signature');
475
476
  const unilateralBase = {
477
    ...baseReceipt,
478
    receiptId: randomUUID(),
479
    unilateral: true,
480
    signatures: {},
481
  };
482
  const unilateral = signNamed(
483
    unilateralBase as unknown as Record<string, unknown>,
484
    requester.identity.agentId,
485
    requester.keyPair,
486
  ) as unknown as Receipt;
487
  engine.submitReceipt(unilateral);
488
  log('11. Produced a clearly labelled unilateral receipt');
489
  return {
490
    engine,
491
    learnedRisk,
492
    newVersionRisk,
493
    forwarding,
494
    check,
495
    receipt,
496
    initialBrief,
497
    structuredConclusion,
498
    localEligibility,
499
    networkEligibility,
500
    weightedProfile,
501
  };
502
}