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

Viewing files at commit 479efea741d5Commit detailsView latest

tests/core.test.ts 302 lines · 10.8 KB · typescript
1
import { describe, expect, it } from 'vitest';
2
import {
3
  buildCounterpartyBrief,
4
  buildInteractionConclusion,
5
  createIdentity,
6
  FixtureFactCheckProvider,
7
  TrustEngine,
8
} from '@openclasp/core';
9
import {
10
  canonicalHash,
11
  InteractionCompletionReportSchema,
12
  InteractionFeedbackSchema,
13
  PublicAgentCardSchema,
14
  signNamed,
15
  type InteractionContract,
16
  type Receipt,
17
} from '@openclasp/protocol';
18
import { createSignedEvent } from '@openclasp/sdk';
19
20
describe('general assurance behavior', () => {
21
  it('requires every contract party to sign before the agreement is active', () => {
22
    const engine = new TrustEngine();
23
    const first = createIdentity({
24
      agentId: 'agent:first',
25
      operatorRef: 'operator:a',
26
      capabilities: ['coordinate'],
27
    });
28
    const second = createIdentity({
29
      agentId: 'agent:second',
30
      operatorRef: 'operator:b',
31
      capabilities: ['coordinate'],
32
    });
33
    engine.registerAgent(first.identity);
34
    engine.registerAgent(second.identity);
35
    const unsigned: InteractionContract = {
36
      protocolVersion: '0.1',
37
      interactionId: crypto.randomUUID(),
38
      purpose: 'Coordinate a task',
39
      parties: [first.identity.agentId, second.identity.agentId],
40
      taskCategory: 'coordination',
41
      requestedOutcome: 'Agreed plan',
42
      successCriteria: ['Plan accepted'],
43
      allowedActions: ['coordinate'],
44
      prohibitedActions: [],
45
      allowedData: ['public'],
46
      prohibitedData: ['private'],
47
      evidenceRequirements: [],
48
      delegationRules: [],
49
      humanApprovalRequirements: [],
50
      factCheckingPolicy: 'important_claims',
51
      mediationPolicy: 'mutual_consent',
52
      retentionDays: 30,
53
      completionConditions: ['plan_delivered'],
54
      cancellationConditions: ['either_party'],
55
      signatures: {},
56
    };
57
    const oneSignature = signNamed(
58
      unsigned as unknown as Record<string, unknown>,
59
      first.identity.agentId,
60
      first.keyPair,
61
    ) as unknown as InteractionContract;
62
    expect(() => engine.saveContract(oneSignature)).toThrow('every party');
63
    const complete = signNamed(
64
      oneSignature as unknown as Record<string, unknown>,
65
      second.identity.agentId,
66
      second.keyPair,
67
    ) as unknown as InteractionContract;
68
    expect(engine.saveContract(complete).interactionId).toBe(unsigned.interactionId);
69
  });
70
71
  it('does not fact-check opinions as objective truth', async () => {
72
    const provider = new FixtureFactCheckProvider();
73
    expect((await provider.check('I think green is the best color')).status).toBe(
74
      'not_fact_checkable',
75
    );
76
  });
77
78
  it('verifies a receipt without recording it', () => {
79
    const engine = new TrustEngine();
80
    const agent = createIdentity({
81
      agentId: 'agent:receipt',
82
      operatorRef: 'operator:a',
83
      capabilities: ['work'],
84
    });
85
    engine.registerAgent(agent.identity);
86
    const base: Receipt = {
87
      receiptId: crypto.randomUUID(),
88
      interactionId: crypto.randomUUID(),
89
      participants: [agent.identity.agentId],
90
      agentVersions: { [agent.identity.agentId]: '1.0.0' },
91
      contractHash: 'contract',
92
      startedAt: new Date(Date.now() - 1000).toISOString(),
93
      completedAt: new Date().toISOString(),
94
      outcome: 'success',
95
      commitmentsFulfilled: ['done'],
96
      commitmentsMissed: [],
97
      evidenceHashes: [],
98
      policyWarnings: [],
99
      policyViolations: [],
100
      disputeStatus: 'none',
101
      delegationChainHash: 'none',
102
      unilateral: false,
103
      signatures: {},
104
    };
105
    const signed = signNamed(
106
      base as unknown as Record<string, unknown>,
107
      agent.identity.agentId,
108
      agent.keyPair,
109
    ) as unknown as Receipt;
110
    expect(engine.verifyReceipt(signed).receiptId).toBe(base.receiptId);
111
    expect(engine.receipts.size).toBe(0);
112
  });
113
114
  it('requires opt-in and strips payloads from network contributions', () => {
115
    const engine = new TrustEngine();
116
    const agent = createIdentity({
117
      agentId: 'agent:a',
118
      operatorRef: 'operator:a',
119
      capabilities: ['chat'],
120
    });
121
    engine.registerAgent(agent.identity);
122
    const event = createSignedEvent(
123
      {
124
        protocolVersion: '0.1',
125
        eventId: crypto.randomUUID(),
126
        interactionId: crypto.randomUUID(),
127
        eventType: 'claim',
128
        agentId: agent.identity.agentId,
129
        agentVersion: '1.0.0',
130
        timestamp: new Date().toISOString(),
131
        visibility: 'network_aggregate',
132
        provenance: 'observed',
133
        payload: { rawMessage: 'private conversation', claim: 'secret' },
134
      },
135
      agent.keyPair,
136
    );
137
    expect(engine.networkContribution(event)).toBeNull();
138
    engine.setContributionConsent(agent.identity.agentId, true);
139
    const contribution = engine.networkContribution(event);
140
    expect(contribution).not.toHaveProperty('payload');
141
    expect(JSON.stringify(contribution)).not.toContain('private conversation');
142
  });
143
144
  it('requires mutual mediation consent', () => {
145
    const engine = new TrustEngine();
146
    const conflict = engine.createConflict({
147
      interactionId: crypto.randomUUID(),
148
      issue: 'ambiguous requirement',
149
      participants: ['a', 'b'],
150
      positions: {},
151
      evidence: [],
152
      contractClauses: [],
153
      missingInformation: [],
154
      possibleResolutions: ['clarify'],
155
    });
156
    engine.permitMediation(conflict.conflictId, 'a');
157
    expect(() => engine.resolveConflict(conflict.conflictId, 'clarified')).toThrow('Mutual');
158
    engine.permitMediation(conflict.conflictId, 'b');
159
    expect(engine.resolveConflict(conflict.conflictId, 'clarified').status).toBe('resolved');
160
  });
161
162
  it('builds a private brief against the actual contract requirements', () => {
163
    const interactionId = crypto.randomUUID();
164
    const contract: InteractionContract = {
165
      protocolVersion: '0.1',
166
      interactionId,
167
      purpose: 'Find an engineering role',
168
      parties: ['agent:candidate', 'agent:recruiter'],
169
      taskCategory: 'recruiting',
170
      requestedOutcome: 'Available backend software engineering positions',
171
      successCriteria: ['At least one matching backend role'],
172
      allowedActions: ['recruiting'],
173
      prohibitedActions: [],
174
      allowedData: ['public'],
175
      prohibitedData: ['private'],
176
      evidenceRequirements: ['Job description link'],
177
      delegationRules: [],
178
      humanApprovalRequirements: [],
179
      factCheckingPolicy: 'important_claims',
180
      mediationPolicy: 'mutual_consent',
181
      retentionDays: 30,
182
      completionConditions: ['positions returned'],
183
      cancellationConditions: ['either party'],
184
      signatures: {},
185
    };
186
    const subject = PublicAgentCardSchema.parse({
187
      protocolVersion: '0.1',
188
      agentId: 'agent:recruiter',
189
      name: 'Recruiter',
190
      description: 'Recruiting assistant for marketing roles',
191
      framework: 'Botpress',
192
      agentVersion: '1.0.0',
193
      capabilities: ['recruiting'],
194
      limitations: ['no backend recruitment'],
195
      assurance: 'oauth_authenticated',
196
      transports: [],
197
      cardUrl: 'https://openclasp.example/agents/recruiter/card.json',
198
      a2aAgentCardUrl: 'https://openclasp.example/agents/recruiter/a2a-agent-card.json',
199
      extensionUri: 'https://openclasp.example/extensions/trust/v0.1',
200
      publishedAt: '2026-08-29T00:00:00.000Z',
201
      updatedAt: '2026-08-29T00:00:00.000Z',
202
    });
203
    const brief = buildCounterpartyBrief({
204
      interactionId,
205
      contractHash: canonicalHash(contract),
206
      contract,
207
      recipientAgentId: 'agent:candidate',
208
      subject,
209
      historyInsights: [
210
        {
211
          code: 'limited_verified_history',
212
          severity: 'caution',
213
          message: 'No eligible history exists.',
214
          evidenceReferences: [],
215
          requirementReferences: [],
216
        },
217
      ],
218
      relevantSampleSize: 0,
219
      historyConfidence: 0,
220
      generatedAt: '2026-08-29T00:00:00.000Z',
221
      expiresAt: '2026-08-29T01:00:00.000Z',
222
    });
223
    expect(brief.decision).toBe('CHALLENGE');
224
    expect(brief.requirements).toEqual(
225
      expect.arrayContaining([
226
        expect.objectContaining({ requirementId: 'task-category', status: 'match' }),
227
        expect.objectContaining({ requirementId: 'requested-outcome', status: 'mismatch' }),
228
      ]),
229
    );
230
    expect(brief.insights[0]?.requirementReferences.length).toBeGreaterThan(0);
231
  });
232
233
  it('releases an aggregate conclusion without private feedback comments', () => {
234
    const interactionId = crypto.randomUUID();
235
    const contract: InteractionContract = {
236
      protocolVersion: '0.1',
237
      interactionId,
238
      purpose: 'Coordinate a task',
239
      parties: ['agent:a', 'agent:b'],
240
      taskCategory: 'coordination',
241
      requestedOutcome: 'Deliver a plan',
242
      successCriteria: ['Plan delivered'],
243
      allowedActions: [],
244
      prohibitedActions: [],
245
      allowedData: [],
246
      prohibitedData: [],
247
      evidenceRequirements: [],
248
      delegationRules: [],
249
      humanApprovalRequirements: [],
250
      factCheckingPolicy: 'important_claims',
251
      mediationPolicy: 'mutual_consent',
252
      retentionDays: 30,
253
      completionConditions: ['Plan delivered'],
254
      cancellationConditions: [],
255
      signatures: {},
256
    };
257
    const report = (agentId: string, counterpartyAgentId: string, outcome: 'success' | 'partial') =>
258
      InteractionCompletionReportSchema.parse({
259
        reportId: crypto.randomUUID(),
260
        interactionId,
261
        contractHash: canonicalHash(contract),
262
        reportingAgentId: agentId,
263
        counterpartyAgentId,
264
        agentVersion: '1.0.0',
265
        outcome,
266
        summary: 'Structured summary',
267
        requestedOutcome: contract.requestedOutcome,
268
        criteria: [
269
          {
270
            criterion: 'Plan delivered',
271
            status: outcome === 'success' ? 'met' : 'partially_met',
272
          },
273
        ],
274
        completedAt: '2026-08-29T00:10:00.000Z',
275
        confidence: 0.9,
276
      });
277
    const feedback = (reviewerAgentId: string, subjectAgentId: string, rating: number) =>
278
      InteractionFeedbackSchema.parse({
279
        feedbackId: crypto.randomUUID(),
280
        requestId: crypto.randomUUID(),
281
        interactionId,
282
        reviewerAgentId,
283
        subjectAgentId,
284
        reviewerAgentVersion: '1.0.0',
285
        ratings: { reliability: rating },
286
        wouldWorkAgain: 'yes',
287
        privateComment: `private note from ${reviewerAgentId}`,
288
        confidence: 0.9,
289
        submittedAt: '2026-08-29T00:11:00.000Z',
290
      });
291
    const conclusion = buildInteractionConclusion({
292
      interaction: { interactionId, termsHash: canonicalHash(contract), contract },
293
      reports: [report('agent:a', 'agent:b', 'success'), report('agent:b', 'agent:a', 'partial')],
294
      feedback: [feedback('agent:a', 'agent:b', 0.8), feedback('agent:b', 'agent:a', 0.6)],
295
      conclusionId: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
296
      generatedAt: '2026-08-29T00:12:00.000Z',
297
    });
298
    expect(conclusion.consensus).toBe('bilateral_partial_agreement');
299
    expect(conclusion.averageRatings.reliability).toBeCloseTo(0.7);
300
    expect(JSON.stringify(conclusion)).not.toContain('private note');
301
  });
302
});