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

Viewing files at commit d596f9595c49Commit detailsView latest

tests/protocol.test.ts 190 lines · 6.4 KB · typescript
1
import { describe, expect, it } from 'vitest';
2
import { createIdentity, TrustEngine } from '@openclasp/core';
3
import {
4
  FederatedInteractionSchema,
5
  HostedMessageSchema,
6
  HostedThreadSchema,
7
  PublicAgentCardSchema,
8
  canonicalHash,
9
  signObject,
10
  verifyObject,
11
} from '@openclasp/protocol';
12
import { toA2AAgentCard } from '@openclasp/sidecar';
13
14
describe('protocol cryptography and delegation', () => {
15
  it('canonicalizes object key order and rejects tampering', () => {
16
    expect(canonicalHash({ b: 2, a: 1 })).toBe(canonicalHash({ a: 1, b: 2 }));
17
    const agent = createIdentity({
18
      agentId: 'agent:a',
19
      operatorRef: 'operator:a',
20
      capabilities: ['write'],
21
    });
22
    const signed = signObject({ action: 'write', value: 1 }, agent.keyPair);
23
    expect(verifyObject(signed, agent.identity.publicKey)).toBe(true);
24
    expect(verifyObject({ ...signed, value: 2 }, agent.identity.publicKey)).toBe(false);
25
  });
26
27
  it('prevents authority escalation and rejects expired delegation', () => {
28
    const engine = new TrustEngine();
29
    const parent = createIdentity({
30
      agentId: 'agent:parent',
31
      operatorRef: 'operator:x',
32
      capabilities: ['read'],
33
    });
34
    const child = createIdentity({
35
      agentId: 'agent:child',
36
      operatorRef: 'operator:x',
37
      capabilities: ['read'],
38
      parentAgentId: parent.identity.agentId,
39
      rootControllerId: parent.identity.rootControllerId,
40
    });
41
    engine.registerAgent(parent.identity);
42
    engine.registerAgent(child.identity);
43
    expect(() =>
44
      engine.createDelegation(
45
        parent.identity.agentId,
46
        child.identity.agentId,
47
        ['write'],
48
        new Date(Date.now() + 1000).toISOString(),
49
        parent.keyPair,
50
      ),
51
    ).toThrow('exceeds parent authority');
52
    const expired = engine.createDelegation(
53
      parent.identity.agentId,
54
      child.identity.agentId,
55
      ['read'],
56
      new Date(Date.now() - 1).toISOString(),
57
      parent.keyPair,
58
    );
59
    expect(engine.verifyDelegation(expired.delegationId)).toBe(false);
60
  });
61
62
  it('publishes an internet Agent Card and official A2A extension without private owner data', () => {
63
    const card = PublicAgentCardSchema.parse({
64
      protocolVersion: '0.1',
65
      agentId: 'agent:research',
66
      name: 'Research agent',
67
      description: 'Finds primary sources',
68
      framework: 'Codex',
69
      agentVersion: '1.2.0',
70
      capabilities: ['research'],
71
      limitations: ['no purchases'],
72
      assurance: 'oauth_authenticated',
73
      transports: [
74
        {
75
          protocol: 'A2A/1.0',
76
          protocolBinding: 'JSONRPC',
77
          endpoint: 'https://agent.example/a2a',
78
        },
79
      ],
80
      cardUrl: 'https://openclasp.vercel.app/agents/agent%3Aresearch/card.json',
81
      a2aAgentCardUrl: 'https://openclasp.vercel.app/agents/agent%3Aresearch/a2a-agent-card.json',
82
      extensionUri: 'https://openclasp.vercel.app/extensions/trust/v0.1',
83
      publishedAt: '2026-08-29T00:00:00.000Z',
84
      updatedAt: '2026-08-29T00:00:00.000Z',
85
    });
86
    expect(card).not.toHaveProperty('operatorId');
87
    expect(card).not.toHaveProperty('projectId');
88
    const a2a = toA2AAgentCard(card);
89
    expect(a2a.supportedInterfaces[0]).toMatchObject({
90
      url: 'https://agent.example/a2a',
91
      protocolBinding: 'JSONRPC',
92
      protocolVersion: '1.0',
93
    });
94
    expect(a2a.capabilities?.extensions?.[0]?.uri).toBe(card.extensionUri);
95
  });
96
97
  it('binds bilateral acceptances to one immutable contract hash', () => {
98
    const interactionId = crypto.randomUUID();
99
    const contract = {
100
      protocolVersion: '0.1' as const,
101
      interactionId,
102
      purpose: 'Research a market',
103
      parties: ['agent:a', 'agent:b'],
104
      taskCategory: 'research',
105
      requestedOutcome: 'A sourced brief',
106
      successCriteria: ['Primary sources included'],
107
      allowedActions: ['browse'],
108
      prohibitedActions: ['purchase'],
109
      allowedData: ['public'],
110
      prohibitedData: ['secrets'],
111
      evidenceRequirements: ['links'],
112
      delegationRules: ['explicit'],
113
      humanApprovalRequirements: [],
114
      factCheckingPolicy: 'important_claims',
115
      mediationPolicy: 'mutual_consent' as const,
116
      retentionDays: 30,
117
      completionConditions: ['brief delivered'],
118
      cancellationConditions: ['either party'],
119
      signatures: {},
120
    };
121
    const termsHash = canonicalHash(contract);
122
    const now = '2026-08-29T00:00:00.000Z';
123
    const value = FederatedInteractionSchema.parse({
124
      protocolVersion: '0.1',
125
      interactionId,
126
      initiatorAgentId: 'agent:a',
127
      responderAgentId: 'agent:b',
128
      status: 'active',
129
      contract,
130
      termsHash,
131
      acceptances: {
132
        'agent:a': {
133
          agentId: 'agent:a',
134
          method: 'oauth_installation',
135
          termsHash,
136
          acceptedAt: now,
137
        },
138
        'agent:b': {
139
          agentId: 'agent:b',
140
          method: 'oauth_installation',
141
          termsHash,
142
          acceptedAt: now,
143
        },
144
      },
145
      responderTransport: {
146
        protocol: 'A2A/1.0',
147
        protocolBinding: 'JSONRPC',
148
        endpoint: 'https://agent-b.example/a2a',
149
      },
150
      createdAt: now,
151
      updatedAt: now,
152
      expiresAt: '2026-08-30T00:00:00.000Z',
153
    });
154
    expect(Object.values(value.acceptances).every((item) => item.termsHash === termsHash)).toBe(
155
      true,
156
    );
157
    expect(canonicalHash({ ...contract, purpose: 'Changed terms' })).not.toBe(termsHash);
158
  });
159
160
  it('validates bounded hosted temporary-chat history', () => {
161
    const threadId = crypto.randomUUID();
162
    const now = new Date().toISOString();
163
    const thread = HostedThreadSchema.parse({
164
      threadId,
165
      interactionId: threadId,
166
      participantAgentIds: ['agent:temporary', 'agent:persistent'],
167
      status: 'open',
168
      privacyMode: 'openclasp_hosted_temporary',
169
      unreadCount: 1,
170
      createdAt: now,
171
      updatedAt: now,
172
      expiresAt: new Date(Date.now() + 1000).toISOString(),
173
    });
174
    const message = HostedMessageSchema.parse({
175
      messageId: crypto.randomUUID(),
176
      threadId,
177
      interactionId: threadId,
178
      senderAgentId: 'agent:persistent',
179
      recipientAgentId: 'agent:temporary',
180
      contentType: 'text/plain',
181
      content: 'Interview request',
182
      contentHash: canonicalHash('Interview request'),
183
      delivery: 'delivered',
184
      createdAt: now,
185
    });
186
    expect(thread.privacyMode).toBe('openclasp_hosted_temporary');
187
    expect(message.contentHash).toBe(canonicalHash(message.content));
188
    expect(() => HostedMessageSchema.parse({ ...message, content: 'x'.repeat(20_001) })).toThrow();
189
  });
190
});