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

Viewing files at commit 66a0492edc4aCommit detailsView latest

tests/protocol.test.ts 398 lines · 13.6 KB · typescript
1
import { describe, expect, it } from 'vitest';
2
import { createIdentity, TrustEngine } from '@openclasp/core';
3
import {
4
  AssuranceProbePlanSchema,
5
  AssuranceProbeResponseSchema,
6
  CounterpartyBriefSchema,
7
  FederatedInteractionSchema,
8
  InteractionCompletionReportSchema,
9
  InteractionFeedbackSchema,
10
  LearningEligibilityDecisionSchema,
11
  LiveSessionEventSchema,
12
  PublicAgentCardSchema,
13
  canonicalHash,
14
  signObject,
15
  verifyObject,
16
} from '@openclasp/protocol';
17
import { toA2AAgentCard } from '@openclasp/sidecar';
18
19
describe('protocol cryptography and delegation', () => {
20
  it('keeps adaptive assurance probes bounded and typed', () => {
21
    const plan = AssuranceProbePlanSchema.parse({
22
      protocolVersion: '0.1',
23
      planId: crypto.randomUUID(),
24
      interactionId: crypto.randomUUID(),
25
      contractHash: 'sha256:terms',
26
      phase: 'pre_task',
27
      generatedForAgentId: 'agent:a',
28
      targetAgentId: 'agent:b',
29
      targetAgentVersion: '1.0.0',
30
      round: 1,
31
      assessmentId: crypto.randomUUID(),
32
      predictionBeforeId: crypto.randomUUID(),
33
      questions: [
34
        {
35
          probeId: crypto.randomUUID(),
36
          questionCode: 'deadline_risk',
37
          prompt: 'Can you complete this before the agreed deadline?',
38
          responseType: 'enum',
39
          choices: ['yes', 'at_risk', 'no'],
40
          evidenceRequested: false,
41
          required: true,
42
          questionFamily: 'deadline',
43
          riskHypothesis: 'The deadline may be infeasible.',
44
          expectedSignals: [
45
            { answer: 'yes', effect: 'increase_success', probabilityDelta: 0.05 },
46
            { answer: 'at_risk', effect: 'reduce_success', probabilityDelta: -0.1 },
47
            { answer: 'no', effect: 'reduce_success', probabilityDelta: -0.25 },
48
          ],
49
          expectedInformationGain: 0.8,
50
          selectionReason: 'Deadline feasibility is material to success.',
51
          recommendedSafeguardCodes: ['extend_deadline'],
52
        },
53
      ],
54
      generation: {
55
        generationId: crypto.randomUUID(),
56
        mode: 'ai',
57
        model: 'openai/gpt-5.6-luna',
58
        promptVersion: 'assurance-probes-v1',
59
      },
60
      generatedAt: '2026-08-31T00:00:00.000Z',
61
      expiresAt: '2026-08-31T00:10:00.000Z',
62
    });
63
    expect(plan.questions).toHaveLength(1);
64
    expect(() =>
65
      AssuranceProbePlanSchema.parse({
66
        ...plan,
67
        questions: Array.from({ length: 4 }, () => plan.questions[0]),
68
      }),
69
    ).toThrow();
70
    expect(() =>
71
      AssuranceProbePlanSchema.parse({
72
        ...plan,
73
        questions: [{ ...plan.questions[0], choices: undefined }],
74
      }),
75
    ).toThrow('bounded choices');
76
77
    expect(() =>
78
      AssuranceProbeResponseSchema.parse({
79
        protocolVersion: '0.1',
80
        responseId: crypto.randomUUID(),
81
        planId: plan.planId,
82
        interactionId: plan.interactionId,
83
        contractHash: plan.contractHash,
84
        phase: plan.phase,
85
        agentId: plan.targetAgentId,
86
        agentVersion: plan.targetAgentVersion,
87
        answers: [
88
          {
89
            probeId: plan.questions[0]!.probeId,
90
            questionCode: plan.questions[0]!.questionCode,
91
            responseType: 'short_text',
92
            answer: 'x'.repeat(281),
93
            confidence: 0.8,
94
          },
95
        ],
96
        respondedAt: '2026-08-31T00:01:00.000Z',
97
      }),
98
    ).toThrow();
99
  });
100
101
  it('requires compact structured data for progress checkpoints', () => {
102
    const checkpoint = LiveSessionEventSchema.parse({
103
      eventId: '11111111-1111-4111-8111-111111111111',
104
      interactionId: '22222222-2222-4222-8222-222222222222',
105
      agentId: 'agent:a',
106
      sequence: 5,
107
      type: 'progress_checkpoint',
108
      occurredAt: '2026-08-29T00:00:00.000Z',
109
      checkpoint: {
110
        state: 'active',
111
        progress: 0.5,
112
        criteriaMet: ['price quoted'],
113
        criteriaRemaining: ['delivery confirmed'],
114
        blockerCodes: [],
115
        topicStatus: 'in_scope',
116
        expectedRemainingTurns: 2,
117
        needsHuman: false,
118
        confidence: 0.8,
119
      },
120
      details: {},
121
    });
122
    expect(checkpoint.checkpoint?.progress).toBe(0.5);
123
    expect(() => LiveSessionEventSchema.parse({ ...checkpoint, checkpoint: undefined })).toThrow(
124
      'structured checkpoint data',
125
    );
126
  });
127
128
  it('canonicalizes object key order and rejects tampering', () => {
129
    expect(canonicalHash({ b: 2, a: 1 })).toBe(canonicalHash({ a: 1, b: 2 }));
130
    const agent = createIdentity({
131
      agentId: 'agent:a',
132
      operatorRef: 'operator:a',
133
      capabilities: ['write'],
134
    });
135
    const signed = signObject({ action: 'write', value: 1 }, agent.keyPair);
136
    expect(verifyObject(signed, agent.identity.publicKey)).toBe(true);
137
    expect(verifyObject({ ...signed, value: 2 }, agent.identity.publicKey)).toBe(false);
138
  });
139
140
  it('prevents authority escalation and rejects expired delegation', () => {
141
    const engine = new TrustEngine();
142
    const parent = createIdentity({
143
      agentId: 'agent:parent',
144
      operatorRef: 'operator:x',
145
      capabilities: ['read'],
146
    });
147
    const child = createIdentity({
148
      agentId: 'agent:child',
149
      operatorRef: 'operator:x',
150
      capabilities: ['read'],
151
      parentAgentId: parent.identity.agentId,
152
      rootControllerId: parent.identity.rootControllerId,
153
    });
154
    engine.registerAgent(parent.identity);
155
    engine.registerAgent(child.identity);
156
    expect(() =>
157
      engine.createDelegation(
158
        parent.identity.agentId,
159
        child.identity.agentId,
160
        ['write'],
161
        new Date(Date.now() + 1000).toISOString(),
162
        parent.keyPair,
163
      ),
164
    ).toThrow('exceeds parent authority');
165
    const expired = engine.createDelegation(
166
      parent.identity.agentId,
167
      child.identity.agentId,
168
      ['read'],
169
      new Date(Date.now() - 1).toISOString(),
170
      parent.keyPair,
171
    );
172
    expect(engine.verifyDelegation(expired.delegationId)).toBe(false);
173
  });
174
175
  it('publishes an internet Agent Card and official A2A extension without private owner data', () => {
176
    const card = PublicAgentCardSchema.parse({
177
      protocolVersion: '0.1',
178
      agentId: 'agent:research',
179
      name: 'Research agent',
180
      description: 'Finds primary sources',
181
      framework: 'Codex',
182
      agentVersion: '1.2.0',
183
      capabilities: ['research'],
184
      limitations: ['no purchases'],
185
      assurance: 'oauth_authenticated',
186
      transports: [
187
        {
188
          protocol: 'A2A/1.0',
189
          protocolBinding: 'JSONRPC',
190
          endpoint: 'https://agent.example/a2a',
191
        },
192
      ],
193
      cardUrl: 'https://openclasp.vercel.app/agents/agent%3Aresearch/card.json',
194
      a2aAgentCardUrl: 'https://openclasp.vercel.app/agents/agent%3Aresearch/a2a-agent-card.json',
195
      extensionUri: 'https://openclasp.vercel.app/extensions/trust/v0.1',
196
      publishedAt: '2026-08-29T00:00:00.000Z',
197
      updatedAt: '2026-08-29T00:00:00.000Z',
198
    });
199
    expect(card).not.toHaveProperty('operatorId');
200
    expect(card).not.toHaveProperty('projectId');
201
    const a2a = toA2AAgentCard(card);
202
    expect(a2a.supportedInterfaces[0]).toMatchObject({
203
      url: 'https://agent.example/a2a',
204
      protocolBinding: 'JSONRPC',
205
      protocolVersion: '1.0',
206
    });
207
    expect(a2a.capabilities?.extensions?.[0]?.uri).toBe(card.extensionUri);
208
  });
209
210
  it('binds bilateral acceptances to one immutable contract hash', () => {
211
    const interactionId = crypto.randomUUID();
212
    const contract = {
213
      protocolVersion: '0.1' as const,
214
      interactionId,
215
      purpose: 'Research a market',
216
      parties: ['agent:a', 'agent:b'],
217
      taskCategory: 'research',
218
      requestedOutcome: 'A sourced brief',
219
      successCriteria: ['Primary sources included'],
220
      allowedActions: ['browse'],
221
      prohibitedActions: ['purchase'],
222
      allowedData: ['public'],
223
      prohibitedData: ['secrets'],
224
      evidenceRequirements: ['links'],
225
      delegationRules: ['explicit'],
226
      humanApprovalRequirements: [],
227
      factCheckingPolicy: 'important_claims',
228
      mediationPolicy: 'mutual_consent' as const,
229
      retentionDays: 30,
230
      completionConditions: ['brief delivered'],
231
      cancellationConditions: ['either party'],
232
      signatures: {},
233
    };
234
    const termsHash = canonicalHash(contract);
235
    const now = '2026-08-29T00:00:00.000Z';
236
    const value = FederatedInteractionSchema.parse({
237
      protocolVersion: '0.1',
238
      interactionId,
239
      initiatorAgentId: 'agent:a',
240
      responderAgentId: 'agent:b',
241
      status: 'active',
242
      contract,
243
      termsHash,
244
      acceptances: {
245
        'agent:a': {
246
          agentId: 'agent:a',
247
          method: 'oauth_installation',
248
          termsHash,
249
          acceptedAt: now,
250
        },
251
        'agent:b': {
252
          agentId: 'agent:b',
253
          method: 'oauth_installation',
254
          termsHash,
255
          acceptedAt: now,
256
        },
257
      },
258
      responderTransport: {
259
        protocol: 'A2A/1.0',
260
        protocolBinding: 'JSONRPC',
261
        endpoint: 'https://agent-b.example/a2a',
262
      },
263
      createdAt: now,
264
      updatedAt: now,
265
      expiresAt: '2026-08-30T00:00:00.000Z',
266
    });
267
    expect(Object.values(value.acceptances).every((item) => item.termsHash === termsHash)).toBe(
268
      true,
269
    );
270
    expect(canonicalHash({ ...contract, purpose: 'Changed terms' })).not.toBe(termsHash);
271
  });
272
273
  it('produces private requirement-specific counterparty briefs', () => {
274
    const now = '2026-08-29T00:00:00.000Z';
275
    const brief = CounterpartyBriefSchema.parse({
276
      briefId: crypto.randomUUID(),
277
      interactionId: crypto.randomUUID(),
278
      contractHash: canonicalHash({ purpose: 'Recruit a backend engineer' }),
279
      recipientAgentId: 'agent:candidate',
280
      subjectAgentId: 'agent:recruiter',
281
      taskCategory: 'recruiting',
282
      decision: 'CHALLENGE',
283
      requirements: [
284
        {
285
          requirementId: 'role-scope',
286
          requirement: 'Backend software engineering positions',
287
          kind: 'scope',
288
          status: 'mismatch',
289
          reason: 'The recruiter currently declares marketing roles only.',
290
          confidence: 0.98,
291
          sources: ['self_declared', 'contract'],
292
        },
293
      ],
294
      insights: [
295
        {
296
          code: 'scope_mismatch',
297
          severity: 'high',
298
          message: 'Confirm role scope before starting the interaction.',
299
          requirementReferences: ['role-scope'],
300
          confidence: 0.98,
301
        },
302
      ],
303
      relevantSampleSize: 0,
304
      historyConfidence: 0,
305
      subjectAgentVersion: '1.0.0',
306
      recommendedContractChanges: ['Require the recruiter to confirm a backend role is available.'],
307
      generatedAt: now,
308
      expiresAt: '2026-08-29T00:10:00.000Z',
309
    });
310
    expect(brief.requirements[0]?.status).toBe('mismatch');
311
    expect(brief.insights[0]?.requirementReferences).toEqual(['role-scope']);
312
  });
313
314
  it('accepts signed structured outcomes and rejects raw conversation fields', () => {
315
    const reporter = createIdentity({
316
      agentId: 'agent:reporter',
317
      operatorRef: 'operator:reporter',
318
      capabilities: ['recruiting'],
319
    });
320
    const interactionId = crypto.randomUUID();
321
    const report = {
322
      reportId: crypto.randomUUID(),
323
      interactionId,
324
      contractHash: canonicalHash({ interactionId, requestedOutcome: 'Find a backend role' }),
325
      reportingAgentId: reporter.identity.agentId,
326
      counterpartyAgentId: 'agent:recruiter',
327
      agentVersion: '1.0.0',
328
      outcome: 'failure' as const,
329
      summary: 'No matching backend positions were available.',
330
      requestedOutcome: 'Identify available backend software engineering positions.',
331
      criteria: [
332
        {
333
          criterion: 'Return at least one backend role',
334
          status: 'missed' as const,
335
          explanation: 'The recruiter only handled marketing roles.',
336
        },
337
      ],
338
      blockers: ['Counterparty scope mismatch'],
339
      completedAt: '2026-08-29T00:05:00.000Z',
340
      confidence: 0.99,
341
      dataSharingMode: 'structured_only' as const,
342
    };
343
    const signed = signObject(report, reporter.keyPair);
344
    expect(InteractionCompletionReportSchema.parse(signed).outcome).toBe('failure');
345
    expect(verifyObject(signed, reporter.identity.publicKey)).toBe(true);
346
    expect(
347
      InteractionCompletionReportSchema.safeParse({
348
        ...report,
349
        rawMessageBody: 'private transcript content',
350
      }).success,
351
    ).toBe(false);
352
  });
353
354
  it('validates bilateral feedback inputs and structured-only learning eligibility', () => {
355
    const interactionId = crypto.randomUUID();
356
    const feedback = InteractionFeedbackSchema.parse({
357
      feedbackId: crypto.randomUUID(),
358
      requestId: crypto.randomUUID(),
359
      interactionId,
360
      reviewerAgentId: 'agent:candidate',
361
      subjectAgentId: 'agent:recruiter',
362
      reviewerAgentVersion: '1.0.0',
363
      ratings: {
364
        outcome_satisfaction: 0.1,
365
        communication: 0.9,
366
        scope_adherence: 0.2,
367
      },
368
      wouldWorkAgain: 'unsure',
369
      reasonCodes: ['scope_mismatch'],
370
      confidence: 0.9,
371
      submittedAt: '2026-08-29T00:06:00.000Z',
372
    });
373
    expect(feedback.ratings.communication).toBe(0.9);
374
    expect(
375
      InteractionFeedbackSchema.safeParse({ ...feedback, ratings: { reliability: 1.1 } }).success,
376
    ).toBe(false);
377
378
    const eligibility = {
379
      decisionId: crypto.randomUUID(),
380
      interactionId,
381
      eligible: true,
382
      reasons: ['Signed completion report and eligible bilateral feedback'],
383
      sampleWeight: 0.8,
384
      reportIds: [crypto.randomUUID(), crypto.randomUUID()],
385
      feedbackIds: [feedback.feedbackId],
386
      contributionMode: 'local_only' as const,
387
      structuredDataOnly: true as const,
388
      decidedAt: '2026-08-29T00:07:00.000Z',
389
    };
390
    expect(LearningEligibilityDecisionSchema.parse(eligibility).eligible).toBe(true);
391
    expect(
392
      LearningEligibilityDecisionSchema.safeParse({
393
        ...eligibility,
394
        structuredDataOnly: false,
395
      }).success,
396
    ).toBe(false);
397
  });
398
});