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

Viewing files at commit c345cf789a03Commit detailsView latest

tests/core.test.ts 509 lines · 17.5 KB · typescript
1
import { describe, expect, it } from 'vitest';
2
import {
3
  buildCounterpartyBrief,
4
  buildInteractionConclusion,
5
  createIdentity,
6
  deriveBehaviouralObservations,
7
  evaluateLearningEligibility,
8
  FixtureFactCheckProvider,
9
  summarizeContextualReliability,
10
  TrustEngine,
11
  updateContextualBehaviouralProfile,
12
} from '@openclasp/core';
13
import {
14
  canonicalHash,
15
  InteractionCompletionReportSchema,
16
  InteractionFeedbackSchema,
17
  PublicAgentCardSchema,
18
  signNamed,
19
  type InteractionContract,
20
  type InteractionConclusion,
21
  type InteractionCompletionReport,
22
  type InteractionFeedback,
23
  type Receipt,
24
} from '@openclasp/protocol';
25
import { createSignedEvent } from '@openclasp/sdk';
26
27
describe('general assurance behavior', () => {
28
  it('requires every contract party to sign before the agreement is active', () => {
29
    const engine = new TrustEngine();
30
    const first = createIdentity({
31
      agentId: 'agent:first',
32
      operatorRef: 'operator:a',
33
      capabilities: ['coordinate'],
34
    });
35
    const second = createIdentity({
36
      agentId: 'agent:second',
37
      operatorRef: 'operator:b',
38
      capabilities: ['coordinate'],
39
    });
40
    engine.registerAgent(first.identity);
41
    engine.registerAgent(second.identity);
42
    const unsigned: InteractionContract = {
43
      protocolVersion: '0.1',
44
      interactionId: crypto.randomUUID(),
45
      purpose: 'Coordinate a task',
46
      parties: [first.identity.agentId, second.identity.agentId],
47
      taskCategory: 'coordination',
48
      requestedOutcome: 'Agreed plan',
49
      successCriteria: ['Plan accepted'],
50
      allowedActions: ['coordinate'],
51
      prohibitedActions: [],
52
      allowedData: ['public'],
53
      prohibitedData: ['private'],
54
      evidenceRequirements: [],
55
      delegationRules: [],
56
      humanApprovalRequirements: [],
57
      factCheckingPolicy: 'important_claims',
58
      mediationPolicy: 'mutual_consent',
59
      retentionDays: 30,
60
      completionConditions: ['plan_delivered'],
61
      cancellationConditions: ['either_party'],
62
      signatures: {},
63
    };
64
    const oneSignature = signNamed(
65
      unsigned as unknown as Record<string, unknown>,
66
      first.identity.agentId,
67
      first.keyPair,
68
    ) as unknown as InteractionContract;
69
    expect(() => engine.saveContract(oneSignature)).toThrow('every party');
70
    const complete = signNamed(
71
      oneSignature as unknown as Record<string, unknown>,
72
      second.identity.agentId,
73
      second.keyPair,
74
    ) as unknown as InteractionContract;
75
    expect(engine.saveContract(complete).interactionId).toBe(unsigned.interactionId);
76
  });
77
78
  it('does not fact-check opinions as objective truth', async () => {
79
    const provider = new FixtureFactCheckProvider();
80
    expect((await provider.check('I think green is the best color')).status).toBe(
81
      'not_fact_checkable',
82
    );
83
  });
84
85
  it('verifies a receipt without recording it', () => {
86
    const engine = new TrustEngine();
87
    const agent = createIdentity({
88
      agentId: 'agent:receipt',
89
      operatorRef: 'operator:a',
90
      capabilities: ['work'],
91
    });
92
    engine.registerAgent(agent.identity);
93
    const base: Receipt = {
94
      receiptId: crypto.randomUUID(),
95
      interactionId: crypto.randomUUID(),
96
      participants: [agent.identity.agentId],
97
      agentVersions: { [agent.identity.agentId]: '1.0.0' },
98
      contractHash: 'contract',
99
      startedAt: new Date(Date.now() - 1000).toISOString(),
100
      completedAt: new Date().toISOString(),
101
      outcome: 'success',
102
      commitmentsFulfilled: ['done'],
103
      commitmentsMissed: [],
104
      evidenceHashes: [],
105
      policyWarnings: [],
106
      policyViolations: [],
107
      disputeStatus: 'none',
108
      delegationChainHash: 'none',
109
      unilateral: false,
110
      signatures: {},
111
    };
112
    const signed = signNamed(
113
      base as unknown as Record<string, unknown>,
114
      agent.identity.agentId,
115
      agent.keyPair,
116
    ) as unknown as Receipt;
117
    expect(engine.verifyReceipt(signed).receiptId).toBe(base.receiptId);
118
    expect(engine.receipts.size).toBe(0);
119
  });
120
121
  it('requires opt-in and strips payloads from network contributions', () => {
122
    const engine = new TrustEngine();
123
    const agent = createIdentity({
124
      agentId: 'agent:a',
125
      operatorRef: 'operator:a',
126
      capabilities: ['chat'],
127
    });
128
    engine.registerAgent(agent.identity);
129
    const event = createSignedEvent(
130
      {
131
        protocolVersion: '0.1',
132
        eventId: crypto.randomUUID(),
133
        interactionId: crypto.randomUUID(),
134
        eventType: 'claim',
135
        agentId: agent.identity.agentId,
136
        agentVersion: '1.0.0',
137
        timestamp: new Date().toISOString(),
138
        visibility: 'network_aggregate',
139
        provenance: 'observed',
140
        payload: { rawMessage: 'private conversation', claim: 'secret' },
141
      },
142
      agent.keyPair,
143
    );
144
    expect(engine.networkContribution(event)).toBeNull();
145
    engine.setContributionConsent(agent.identity.agentId, true);
146
    const contribution = engine.networkContribution(event);
147
    expect(contribution).not.toHaveProperty('payload');
148
    expect(JSON.stringify(contribution)).not.toContain('private conversation');
149
  });
150
151
  it('requires mutual mediation consent', () => {
152
    const engine = new TrustEngine();
153
    const conflict = engine.createConflict({
154
      interactionId: crypto.randomUUID(),
155
      issue: 'ambiguous requirement',
156
      participants: ['a', 'b'],
157
      positions: {},
158
      evidence: [],
159
      contractClauses: [],
160
      missingInformation: [],
161
      possibleResolutions: ['clarify'],
162
    });
163
    engine.permitMediation(conflict.conflictId, 'a');
164
    expect(() => engine.resolveConflict(conflict.conflictId, 'clarified')).toThrow('Mutual');
165
    engine.permitMediation(conflict.conflictId, 'b');
166
    expect(engine.resolveConflict(conflict.conflictId, 'clarified').status).toBe('resolved');
167
  });
168
169
  it('builds a private brief against the actual contract requirements', () => {
170
    const interactionId = crypto.randomUUID();
171
    const contract: InteractionContract = {
172
      protocolVersion: '0.1',
173
      interactionId,
174
      purpose: 'Find an engineering role',
175
      parties: ['agent:candidate', 'agent:recruiter'],
176
      taskCategory: 'recruiting',
177
      requestedOutcome: 'Available backend software engineering positions',
178
      successCriteria: ['At least one matching backend role'],
179
      allowedActions: ['recruiting'],
180
      prohibitedActions: [],
181
      allowedData: ['public'],
182
      prohibitedData: ['private'],
183
      evidenceRequirements: ['Job description link'],
184
      delegationRules: [],
185
      humanApprovalRequirements: [],
186
      factCheckingPolicy: 'important_claims',
187
      mediationPolicy: 'mutual_consent',
188
      retentionDays: 30,
189
      completionConditions: ['positions returned'],
190
      cancellationConditions: ['either party'],
191
      signatures: {},
192
    };
193
    const subject = PublicAgentCardSchema.parse({
194
      protocolVersion: '0.1',
195
      agentId: 'agent:recruiter',
196
      name: 'Recruiter',
197
      description: 'Recruiting assistant for marketing roles',
198
      framework: 'Botpress',
199
      agentVersion: '1.0.0',
200
      capabilities: ['recruiting'],
201
      limitations: ['no backend recruitment'],
202
      assurance: 'oauth_authenticated',
203
      transports: [],
204
      cardUrl: 'https://openclasp.example/agents/recruiter/card.json',
205
      a2aAgentCardUrl: 'https://openclasp.example/agents/recruiter/a2a-agent-card.json',
206
      extensionUri: 'https://openclasp.example/extensions/trust/v0.1',
207
      publishedAt: '2026-08-29T00:00:00.000Z',
208
      updatedAt: '2026-08-29T00:00:00.000Z',
209
    });
210
    const brief = buildCounterpartyBrief({
211
      interactionId,
212
      contractHash: canonicalHash(contract),
213
      contract,
214
      recipientAgentId: 'agent:candidate',
215
      subject,
216
      historyInsights: [
217
        {
218
          code: 'limited_verified_history',
219
          severity: 'caution',
220
          message: 'No eligible history exists.',
221
          evidenceReferences: [],
222
          requirementReferences: [],
223
        },
224
      ],
225
      relevantSampleSize: 0,
226
      historyConfidence: 0,
227
      generatedAt: '2026-08-29T00:00:00.000Z',
228
      expiresAt: '2026-08-29T01:00:00.000Z',
229
    });
230
    expect(brief.decision).toBe('CHALLENGE');
231
    expect(brief.requirements).toEqual(
232
      expect.arrayContaining([
233
        expect.objectContaining({ requirementId: 'task-category', status: 'match' }),
234
        expect.objectContaining({ requirementId: 'requested-outcome', status: 'mismatch' }),
235
      ]),
236
    );
237
    expect(brief.insights[0]?.requirementReferences.length).toBeGreaterThan(0);
238
  });
239
240
  it('releases an aggregate conclusion without private feedback comments', () => {
241
    const interactionId = crypto.randomUUID();
242
    const contract: InteractionContract = {
243
      protocolVersion: '0.1',
244
      interactionId,
245
      purpose: 'Coordinate a task',
246
      parties: ['agent:a', 'agent:b'],
247
      taskCategory: 'coordination',
248
      requestedOutcome: 'Deliver a plan',
249
      successCriteria: ['Plan delivered'],
250
      allowedActions: [],
251
      prohibitedActions: [],
252
      allowedData: [],
253
      prohibitedData: [],
254
      evidenceRequirements: [],
255
      delegationRules: [],
256
      humanApprovalRequirements: [],
257
      factCheckingPolicy: 'important_claims',
258
      mediationPolicy: 'mutual_consent',
259
      retentionDays: 30,
260
      completionConditions: ['Plan delivered'],
261
      cancellationConditions: [],
262
      signatures: {},
263
    };
264
    const report = (agentId: string, counterpartyAgentId: string, outcome: 'success' | 'partial') =>
265
      InteractionCompletionReportSchema.parse({
266
        reportId: crypto.randomUUID(),
267
        interactionId,
268
        contractHash: canonicalHash(contract),
269
        reportingAgentId: agentId,
270
        counterpartyAgentId,
271
        agentVersion: '1.0.0',
272
        outcome,
273
        summary: 'Structured summary',
274
        requestedOutcome: contract.requestedOutcome,
275
        criteria: [
276
          {
277
            criterion: 'Plan delivered',
278
            status: outcome === 'success' ? 'met' : 'partially_met',
279
          },
280
        ],
281
        completedAt: '2026-08-29T00:10:00.000Z',
282
        confidence: 0.9,
283
      });
284
    const feedback = (reviewerAgentId: string, subjectAgentId: string, rating: number) =>
285
      InteractionFeedbackSchema.parse({
286
        feedbackId: crypto.randomUUID(),
287
        requestId: crypto.randomUUID(),
288
        interactionId,
289
        reviewerAgentId,
290
        subjectAgentId,
291
        reviewerAgentVersion: '1.0.0',
292
        ratings: { reliability: rating },
293
        wouldWorkAgain: 'yes',
294
        privateComment: `private note from ${reviewerAgentId}`,
295
        confidence: 0.9,
296
        submittedAt: '2026-08-29T00:11:00.000Z',
297
      });
298
    const conclusion = buildInteractionConclusion({
299
      interaction: { interactionId, termsHash: canonicalHash(contract), contract },
300
      reports: [report('agent:a', 'agent:b', 'success'), report('agent:b', 'agent:a', 'partial')],
301
      feedback: [feedback('agent:a', 'agent:b', 0.8), feedback('agent:b', 'agent:a', 0.6)],
302
      conclusionId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
303
      generatedAt: '2026-08-29T00:12:00.000Z',
304
    });
305
    expect(conclusion.consensus).toBe('bilateral_partial_agreement');
306
    expect(conclusion.averageRatings.reliability).toBeCloseTo(0.7);
307
    expect(JSON.stringify(conclusion)).not.toContain('private note');
308
309
    const provisional = buildInteractionConclusion({
310
      interaction: { interactionId, termsHash: canonicalHash(contract), contract },
311
      reports: [report('agent:a', 'agent:b', 'partial')],
312
      feedback: [],
313
      pendingFeedbackAgentIds: ['agent:a', 'agent:b'],
314
      peerReportStatus: 'unreachable',
315
    });
316
    expect(provisional).toMatchObject({
317
      lifecycle: 'provisional',
318
      consensus: 'unilateral',
319
      missingReportAgentIds: ['agent:b'],
320
      pendingFeedbackAgentIds: ['agent:a', 'agent:b'],
321
      peerReportStatus: 'unreachable',
322
    });
323
    expect(provisional.confidence).toBeLessThanOrEqual(0.55);
324
  });
325
});
326
327
describe('behavioural learning', () => {
328
  const attestation = {
329
    algorithm: 'Ed25519' as const,
330
    keyId: 'openclasp:test',
331
    value: 'signed-test-record',
332
    digest: 'a'.repeat(43),
333
  };
334
  const interactionId = 'dddddddd-dddd-4ddd-8ddd-dddddddddddd';
335
  const report = (
336
    reportingAgentId: string,
337
    counterpartyAgentId: string,
338
    outcome: 'success' | 'partial' = 'success',
339
  ): InteractionCompletionReport =>
340
    InteractionCompletionReportSchema.parse({
341
      reportId: crypto.randomUUID(),
342
      interactionId,
343
      contractHash: 'contract-hash',
344
      reportingAgentId,
345
      counterpartyAgentId,
346
      agentVersion: '1.0.0',
347
      outcome,
348
      summary: 'Structured outcome only',
349
      requestedOutcome: 'Complete the task',
350
      criteria: [
351
        {
352
          criterion: 'Task completed',
353
          status: outcome === 'success' ? 'met' : 'partially_met',
354
        },
355
      ],
356
      confidence: 0.9,
357
      completedAt: '2026-08-29T00:10:00.000Z',
358
      platformAttestation: attestation,
359
    });
360
  const feedback = (
361
    reviewerAgentId: string,
362
    subjectAgentId: string,
363
    rating: number,
364
    evidenceReferences: string[] = [],
365
  ): InteractionFeedback =>
366
    InteractionFeedbackSchema.parse({
367
      feedbackId: crypto.randomUUID(),
368
      requestId: crypto.randomUUID(),
369
      interactionId,
370
      reviewerAgentId,
371
      subjectAgentId,
372
      reviewerAgentVersion: '1.0.0',
373
      ratings: {
374
        outcome_satisfaction: rating,
375
        communication: rating,
376
        timeliness: rating,
377
        evidence_quality: rating,
378
        scope_adherence: rating,
379
        correction_handling: rating,
380
        limitation_disclosure: rating,
381
      },
382
      wouldWorkAgain: rating >= 0.5 ? 'yes' : 'no',
383
      evidenceReferences,
384
      confidence: 1,
385
      submittedAt: '2026-08-29T00:11:00.000Z',
386
      submissionMethod: 'runtime_session',
387
      platformAttestation: attestation,
388
    });
389
  const conclusion = (consensus: InteractionConclusion['consensus']): InteractionConclusion => ({
390
    conclusionId: 'eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee',
391
    interactionId,
392
    contractHash: 'contract-hash',
393
    outcome: 'partial',
394
    consensus,
395
    summary: 'Structured conclusion',
396
    criteria: [],
397
    reportIds: [],
398
    feedbackIds: [],
399
    averageRatings: {},
400
    evidenceReferences: [],
401
    generatedAt: '2026-08-29T00:12:00.000Z',
402
  });
403
404
  it('keeps unilateral evidence local at low weight and penalizes manipulation signals', () => {
405
    const reports = [report('agent:a', 'agent:b'), report('agent:b', 'agent:a')];
406
    const normal = evaluateLearningEligibility({
407
      interactionId,
408
      reports,
409
      feedback: [feedback('agent:a', 'agent:b', 0.8)],
410
      consensus: 'bilateral_agreement',
411
      contributionMode: 'local_only',
412
      reviewerCredibility: { 'agent:a': 1 },
413
    });
414
    const extreme = evaluateLearningEligibility({
415
      interactionId,
416
      reports,
417
      feedback: [feedback('agent:a', 'agent:b', 1)],
418
      consensus: 'bilateral_agreement',
419
      contributionMode: 'local_only',
420
      reviewerCredibility: { 'agent:a': 1 },
421
    });
422
    const unsupported = evaluateLearningEligibility({
423
      interactionId,
424
      reports: [report('agent:a', 'agent:b')],
425
      feedback: [],
426
      consensus: 'unilateral',
427
      contributionMode: 'network_aggregate',
428
    });
429
    expect(normal.eligible).toBe(true);
430
    expect(extreme.sampleWeight).toBeLessThan(normal.sampleWeight);
431
    expect(extreme.manipulationSignals).toContain('extreme_feedback_without_evidence');
432
    expect(unsupported).toMatchObject({
433
      eligible: true,
434
      sampleWeight: 0.1,
435
      contributionMode: 'local_only',
436
      structuredDataOnly: true,
437
    });
438
  });
439
440
  it('derives bounded observations and applies weighted history decay', () => {
441
    const observations = deriveBehaviouralObservations({
442
      subjectAgentId: 'agent:b',
443
      reports: [report('agent:b', 'agent:a', 'partial')],
444
      reviewerFeedback: feedback('agent:a', 'agent:b', 0.8, ['evidence:artifact:1']),
445
      conclusion: conclusion('conflicting'),
446
    });
447
    expect(observations).toMatchObject({
448
      completion: 0.5,
449
      specification: 0.5,
450
      acceptance: 0.8,
451
      communication: 0.8,
452
      correction: 0.8,
453
      limitations: 0.8,
454
      disputes: 1,
455
    });
456
    const updated = updateContextualBehaviouralProfile({
457
      current: {
458
        completion: 1,
459
        communication: 1,
460
        sampleSize: 10,
461
        effectiveSampleSize: 10,
462
        updatedAt: '2026-03-02T00:12:00.000Z',
463
      },
464
      observations,
465
      sampleWeight: 0.8,
466
      appliedAt: '2026-08-29T00:12:00.000Z',
467
    });
468
    expect(updated.profile.sampleSize).toBe(11);
469
    expect(updated.profile.effectiveSampleSize).toBeLessThan(5);
470
    expect(updated.profile.completion).toBeLessThan(1);
471
    expect(updated.dimensionDeltas.completion).toBeLessThan(0);
472
  });
473
474
  it('summarizes reliability with explicit confidence, trend, and version reduction', () => {
475
    const summary = summarizeContextualReliability({
476
      profile: {
477
        agentId: 'agent:b',
478
        agentVersion: '1.0.0',
479
        taskCategory: 'procurement',
480
        completion: 0.9,
481
        acceptance: 0.85,
482
        specification: 0.8,
483
        deadline: 0.75,
484
        communication: 0.9,
485
        evidence: 0.4,
486
        scope: 0.8,
487
        correction: 0.7,
488
        limitations: 0.65,
489
        disputes: 0.1,
490
        sampleSize: 12,
491
        effectiveSampleSize: 8,
492
        updatedAt: '2026-08-29T00:12:00.000Z',
493
      },
494
      deltas: [
495
        {
496
          dimensionDeltas: { completion: 0.1, evidence: 0.08 },
497
          appliedAt: '2026-08-29T00:12:00.000Z',
498
        },
499
      ],
500
      currentAgentVersion: '2.0.0',
501
      now: '2026-08-30T00:12:00.000Z',
502
    });
503
    expect(summary.taskCategory).toBe('procurement');
504
    expect(summary.confidence.level).toBe('low');
505
    expect(summary.trend.direction).toBe('improving');
506
    expect(summary.risks[0]?.dimension).toBe('evidence');
507
    expect(summary.versionStatus.status).toBe('reduced_confidence');
508
  });
509
});