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

Viewing files at commit 18b70a0f11a5Commit detailsView latest

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