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

Viewing files at commit 9a8fdf2387ffCommit detailsView latest

packages/protocol/src/index.ts 408 lines · 13.0 KB · typescript
1
import { createHash, generateKeyPairSync, randomUUID, sign, verify } from 'node:crypto';
2
import { canonicalize } from 'json-canonicalize';
3
import { z } from 'zod';
4
5
export const PROTOCOL_VERSION = '0.1' as const;
6
export const DEFAULT_EXTENSION_URI = 'https://openclasp.vercel.app/extensions/trust/v0.1';
7
8
export const ProvenanceSchema = z.enum([
9
  'self_declared',
10
  'operator_attested',
11
  'cryptographically_verified',
12
  'domain_verified',
13
  'third_party_verified',
14
  'observed',
15
  'disputed',
16
]);
17
export const VisibilitySchema = z.enum([
18
  'local_only',
19
  'private_requester',
20
  'private_responder',
21
  'shared_participants',
22
  'network_aggregate',
23
]);
24
export const DataSharingModeSchema = z.enum([
25
  'local_only',
26
  'structured_only',
27
  'permitted_evidence',
28
]);
29
export const SignatureSchema = z.object({
30
  algorithm: z.literal('Ed25519'),
31
  keyId: z.string().min(1),
32
  value: z.string().min(1),
33
});
34
35
export const ExpectationManifestSchema = z.object({
36
  requiredInputs: z.array(z.string()).default([]),
37
  supportedTaskCategories: z.array(z.string()).min(1),
38
  responseTimeMs: z.number().int().positive().optional(),
39
  dataRequirements: z.array(z.string()).default([]),
40
  prohibitedData: z.array(z.string()).default([]),
41
  humanApprovalThresholds: z.array(z.string()).default([]),
42
  delegationPolicy: z.string().default('explicit'),
43
  evidenceRequirements: z.array(z.string()).default([]),
44
  factCheckingPreference: z
45
    .enum(['none', 'important_claims', 'all_objective_claims'])
46
    .default('important_claims'),
47
  mediationPreference: z.enum(['never', 'mutual_consent']).default('mutual_consent'),
48
  retentionDays: z.number().int().nonnegative().default(30),
49
  communicationFormats: z.array(z.string()).default(['text/plain']),
50
  cancellationPolicy: z.string().default('either_party'),
51
  provenance: ProvenanceSchema,
52
});
53
54
export const AgentIdentitySchema = z.object({
55
  protocolVersion: z.literal(PROTOCOL_VERSION),
56
  agentId: z.string().min(1),
57
  publicKey: z.string().min(1),
58
  keyId: z.string().min(1),
59
  operatorRef: z.string().min(1),
60
  assurance: z.enum(['pseudonymous', 'domain_associated', 'organization_associated']),
61
  agentVersion: z.string().min(1),
62
  capabilities: z.array(z.string()),
63
  supportedProtocols: z.array(z.string()),
64
  parentAgentId: z.string().optional(),
65
  rootControllerId: z.string().min(1),
66
  revoked: z.boolean().default(false),
67
  provenance: ProvenanceSchema,
68
  createdAt: z.string().datetime(),
69
  signature: SignatureSchema.optional(),
70
});
71
72
export const DelegationCredentialSchema = z.object({
73
  protocolVersion: z.literal(PROTOCOL_VERSION),
74
  delegationId: z.string().uuid(),
75
  parentAgentId: z.string(),
76
  childAgentId: z.string(),
77
  rootControllerId: z.string(),
78
  capabilities: z.array(z.string()).min(1),
79
  issuedAt: z.string().datetime(),
80
  expiresAt: z.string().datetime(),
81
  revoked: z.boolean().default(false),
82
  signature: SignatureSchema.optional(),
83
});
84
85
export const InteractionContractSchema = z.object({
86
  protocolVersion: z.literal(PROTOCOL_VERSION),
87
  interactionId: z.string().uuid(),
88
  purpose: z.string(),
89
  parties: z.array(z.string()).min(1),
90
  taskCategory: z.string(),
91
  requestedOutcome: z.string(),
92
  successCriteria: z.array(z.string()),
93
  allowedActions: z.array(z.string()),
94
  prohibitedActions: z.array(z.string()),
95
  allowedData: z.array(z.string()),
96
  prohibitedData: z.array(z.string()),
97
  deadline: z.string().datetime().optional(),
98
  evidenceRequirements: z.array(z.string()),
99
  delegationRules: z.array(z.string()),
100
  humanApprovalRequirements: z.array(z.string()),
101
  factCheckingPolicy: z.string(),
102
  mediationPolicy: z.enum(['none', 'mutual_consent']),
103
  retentionDays: z.number().int().nonnegative(),
104
  completionConditions: z.array(z.string()),
105
  cancellationConditions: z.array(z.string()),
106
  signatures: z.record(z.string(), SignatureSchema).default({}),
107
});
108
109
export const AgentTransportSchema = z.object({
110
  protocol: z.literal('A2A/1.0'),
111
  protocolBinding: z.string().min(1).default('JSONRPC'),
112
  endpoint: z.string().url(),
113
});
114
115
export const PublicAgentCardSchema = z.object({
116
  protocolVersion: z.literal(PROTOCOL_VERSION),
117
  agentId: z.string().min(1),
118
  name: z.string().min(1),
119
  description: z.string(),
120
  framework: z.string().min(1),
121
  agentVersion: z.string().min(1),
122
  capabilities: z.array(z.string()),
123
  limitations: z.array(z.string()),
124
  assurance: z.enum(['oauth_authenticated', 'cryptographically_verified']),
125
  transports: z.array(AgentTransportSchema),
126
  cardUrl: z.string().url(),
127
  a2aAgentCardUrl: z.string().url(),
128
  extensionUri: z.string().url(),
129
  publishedAt: z.string().datetime(),
130
  updatedAt: z.string().datetime(),
131
});
132
133
export const ContractAcceptanceSchema = z.object({
134
  agentId: z.string().min(1),
135
  method: z.enum(['oauth_installation', 'oauth_account', 'policy_auto_accept', 'ed25519']),
136
  termsHash: z.string().min(1),
137
  acceptedAt: z.string().datetime(),
138
  signature: SignatureSchema.optional(),
139
});
140
141
export const FederatedInteractionStatusSchema = z.enum([
142
  'pending',
143
  'active',
144
  'rejected',
145
  'expired',
146
  'cancelled',
147
  'completed',
148
]);
149
150
export const FederatedInteractionSchema = z.object({
151
  protocolVersion: z.literal(PROTOCOL_VERSION),
152
  interactionId: z.string().uuid(),
153
  initiatorAgentId: z.string().min(1),
154
  responderAgentId: z.string().min(1),
155
  status: FederatedInteractionStatusSchema,
156
  contract: InteractionContractSchema,
157
  termsHash: z.string().min(1),
158
  acceptances: z.record(z.string(), ContractAcceptanceSchema),
159
  initiatorTransport: AgentTransportSchema.optional(),
160
  responderTransport: AgentTransportSchema,
161
  createdAt: z.string().datetime(),
162
  updatedAt: z.string().datetime(),
163
  expiresAt: z.string().datetime(),
164
});
165
166
export const TrustEnvelopeSchema = z.object({
167
  protocolVersion: z.literal(PROTOCOL_VERSION),
168
  interactionId: z.string().uuid(),
169
  requestingAgentId: z.string(),
170
  respondingAgentId: z.string(),
171
  rootControllerId: z.string(),
172
  parentAgentId: z.string().optional(),
173
  agentVersion: z.string(),
174
  delegationId: z.string().uuid().optional(),
175
  requestedCapability: z.string(),
176
  taskCategory: z.string(),
177
  contractHash: z.string(),
178
  dataSharingMode: DataSharingModeSchema,
179
  evidenceRequirements: z.array(z.string()),
180
  timestamp: z.string().datetime(),
181
  expiresAt: z.string().datetime(),
182
  nonce: z.string().min(12),
183
  signature: SignatureSchema.optional(),
184
});
185
186
export const EventTypeSchema = z.enum([
187
  'claim',
188
  'evidence',
189
  'constraint',
190
  'commitment',
191
  'proposal',
192
  'objection',
193
  'policy_warning',
194
  'policy_violation',
195
  'private_suggestion',
196
  'shared_intervention',
197
  'delegation',
198
  'task_result',
199
  'resolution',
200
  'receipt',
201
  'feedback',
202
  'dispute',
203
]);
204
export const InteractionEventSchema = z.object({
205
  protocolVersion: z.literal(PROTOCOL_VERSION),
206
  eventId: z.string().uuid(),
207
  interactionId: z.string().uuid(),
208
  eventType: EventTypeSchema,
209
  agentId: z.string(),
210
  agentVersion: z.string(),
211
  timestamp: z.string().datetime(),
212
  visibility: VisibilitySchema,
213
  provenance: ProvenanceSchema,
214
  payloadHash: z.string(),
215
  payload: z.record(z.string(), z.unknown()),
216
  evidenceRefs: z.array(z.string()).optional(),
217
  signature: SignatureSchema.optional(),
218
});
219
220
export const FeedbackSchema = z.object({
221
  feedbackId: z.string().uuid(),
222
  interactionId: z.string().uuid(),
223
  receiptId: z.string().uuid(),
224
  reviewerAgentId: z.string(),
225
  subjectAgentId: z.string(),
226
  taskCompleted: z.boolean(),
227
  outputAccepted: z.boolean(),
228
  specificationMatched: z.boolean(),
229
  deadlineMet: z.boolean(),
230
  communicationQuality: z.number().min(0).max(1),
231
  evidenceProvided: z.boolean(),
232
  scopeRespected: z.boolean(),
233
  disputeRaised: z.boolean(),
234
  comment: z.string().max(1000).optional(),
235
  submittedAt: z.string().datetime(),
236
  signature: SignatureSchema.optional(),
237
});
238
239
export const ReceiptSchema = z.object({
240
  receiptId: z.string().uuid(),
241
  interactionId: z.string().uuid(),
242
  participants: z.array(z.string()),
243
  agentVersions: z.record(z.string(), z.string()),
244
  contractHash: z.string(),
245
  startedAt: z.string().datetime(),
246
  completedAt: z.string().datetime(),
247
  outcome: z.enum(['success', 'partial', 'failure', 'cancelled']),
248
  commitmentsFulfilled: z.array(z.string()),
249
  commitmentsMissed: z.array(z.string()),
250
  evidenceHashes: z.array(z.string()),
251
  policyWarnings: z.array(z.string()),
252
  policyViolations: z.array(z.string()),
253
  disputeStatus: z.enum(['none', 'open', 'resolved']),
254
  delegationChainHash: z.string(),
255
  unilateral: z.boolean(),
256
  signatures: z.record(z.string(), SignatureSchema).default({}),
257
});
258
259
export const RiskDecisionSchema = z.object({
260
  decision: z.enum(['ALLOW', 'CHALLENGE', 'DENY']),
261
  confidence: z.number().min(0).max(1),
262
  taskCategory: z.string(),
263
  sampleSize: z.number().int().nonnegative(),
264
  dimensions: z.record(z.string(), z.number()),
265
  reasons: z.array(z.string()),
266
  warnings: z.array(z.string()),
267
  dataFreshness: z.record(z.string(), z.string()),
268
  requiredChallenges: z.array(z.string()),
269
});
270
271
export const FactCheckResultSchema = z.object({
272
  claim: z.string(),
273
  claimType: z.enum(['objective', 'subjective', 'prediction']),
274
  status: z.enum([
275
    'verified',
276
    'supported',
277
    'contradicted',
278
    'unverified',
279
    'not_fact_checkable',
280
    'insufficient_permission',
281
  ]),
282
  confidence: z.number().min(0).max(1),
283
  evidenceReferences: z.array(z.string()),
284
  sourceAuthority: z.string(),
285
  sourceFreshness: z.string(),
286
  contradictingEvidence: z.array(z.string()),
287
  suggestedNextAction: z.string(),
288
});
289
290
export type AgentIdentity = z.infer<typeof AgentIdentitySchema>;
291
export type DelegationCredential = z.infer<typeof DelegationCredentialSchema>;
292
export type InteractionContract = z.infer<typeof InteractionContractSchema>;
293
export type TrustEnvelope = z.infer<typeof TrustEnvelopeSchema>;
294
export type InteractionEvent = z.infer<typeof InteractionEventSchema>;
295
export type Feedback = z.infer<typeof FeedbackSchema>;
296
export type Receipt = z.infer<typeof ReceiptSchema>;
297
export type RiskDecision = z.infer<typeof RiskDecisionSchema>;
298
export type FactCheckResult = z.infer<typeof FactCheckResultSchema>;
299
export type ExpectationManifest = z.infer<typeof ExpectationManifestSchema>;
300
export type AgentTransport = z.infer<typeof AgentTransportSchema>;
301
export type PublicAgentCard = z.infer<typeof PublicAgentCardSchema>;
302
export type ContractAcceptance = z.infer<typeof ContractAcceptanceSchema>;
303
export type FederatedInteraction = z.infer<typeof FederatedInteractionSchema>;
304
305
export interface KeyPair {
306
  keyId: string;
307
  publicKey: string;
308
  privateKey: string;
309
}
310
311
export function createKeyPair(keyId: string = randomUUID()): KeyPair {
312
  const pair = generateKeyPairSync('ed25519');
313
  return {
314
    keyId,
315
    publicKey: pair.publicKey.export({ type: 'spki', format: 'der' }).toString('base64url'),
316
    privateKey: pair.privateKey.export({ type: 'pkcs8', format: 'der' }).toString('base64url'),
317
  };
318
}
319
320
export function canonicalHash(value: unknown): string {
321
  return createHash('sha256').update(canonicalize(value)).digest('base64url');
322
}
323
324
function unsigned(value: Record<string, unknown>): Record<string, unknown> {
325
  const copy = structuredClone(value);
326
  delete copy.signature;
327
  delete copy.signatures;
328
  return copy;
329
}
330
331
export function signObject<T extends Record<string, unknown>>(
332
  value: T,
333
  key: KeyPair,
334
): T & { signature: z.infer<typeof SignatureSchema> } {
335
  const payload = Buffer.from(canonicalize(unsigned(value)));
336
  const privateKey = {
337
    key: Buffer.from(key.privateKey, 'base64url'),
338
    type: 'pkcs8' as const,
339
    format: 'der' as const,
340
  };
341
  return {
342
    ...value,
343
    signature: {
344
      algorithm: 'Ed25519',
345
      keyId: key.keyId,
346
      value: sign(null, payload, privateKey).toString('base64url'),
347
    },
348
  };
349
}
350
351
export function verifyObject(value: Record<string, unknown>, publicKey: string): boolean {
352
  const parsed = SignatureSchema.safeParse(value.signature);
353
  if (!parsed.success) return false;
354
  const key = {
355
    key: Buffer.from(publicKey, 'base64url'),
356
    type: 'spki' as const,
357
    format: 'der' as const,
358
  };
359
  return verify(
360
    null,
361
    Buffer.from(canonicalize(unsigned(value))),
362
    key,
363
    Buffer.from(parsed.data.value, 'base64url'),
364
  );
365
}
366
367
export function signNamed<T extends Record<string, unknown>>(
368
  value: T,
369
  signerId: string,
370
  key: KeyPair,
371
): T & { signatures: Record<string, z.infer<typeof SignatureSchema>> } {
372
  const payload = Buffer.from(canonicalize(unsigned(value)));
373
  const privateKey = {
374
    key: Buffer.from(key.privateKey, 'base64url'),
375
    type: 'pkcs8' as const,
376
    format: 'der' as const,
377
  };
378
  const signature = {
379
    algorithm: 'Ed25519' as const,
380
    keyId: key.keyId,
381
    value: sign(null, payload, privateKey).toString('base64url'),
382
  };
383
  return {
384
    ...value,
385
    signatures: { ...((value.signatures as object) ?? {}), [signerId]: signature },
386
  };
387
}
388
389
export function verifyNamed(
390
  value: Record<string, unknown>,
391
  signerId: string,
392
  publicKey: string,
393
): boolean {
394
  const signatures = value.signatures as Record<string, unknown> | undefined;
395
  const parsed = SignatureSchema.safeParse(signatures?.[signerId]);
396
  if (!parsed.success) return false;
397
  const key = {
398
    key: Buffer.from(publicKey, 'base64url'),
399
    type: 'spki' as const,
400
    format: 'der' as const,
401
  };
402
  return verify(
403
    null,
404
    Buffer.from(canonicalize(unsigned(value))),
405
    key,
406
    Buffer.from(parsed.data.value, 'base64url'),
407
  );
408
}