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

Viewing files at commit 296bb55574a2Commit detailsView latest

packages/protocol/src/index.ts 521 lines · 16.7 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
  managedBy: z.enum(['agent', 'openclasp']).default('agent'),
114
});
115
116
export const AgentPresenceSchema = z.object({
117
  status: z.enum(['online', 'offline']),
118
  lastSeenAt: z.string().datetime().optional(),
119
  checkedAt: z.string().datetime(),
120
});
121
122
export const PublicAgentCardSchema = z.object({
123
  protocolVersion: z.literal(PROTOCOL_VERSION),
124
  agentId: z.string().min(1),
125
  name: z.string().min(1),
126
  description: z.string(),
127
  framework: z.string().min(1),
128
  agentVersion: z.string().min(1),
129
  capabilities: z.array(z.string()),
130
  limitations: z.array(z.string()),
131
  assurance: z.enum(['oauth_authenticated', 'cryptographically_verified']),
132
  transports: z.array(AgentTransportSchema),
133
  cardUrl: z.string().url(),
134
  a2aAgentCardUrl: z.string().url(),
135
  extensionUri: z.string().url(),
136
  presence: AgentPresenceSchema.optional(),
137
  publishedAt: z.string().datetime(),
138
  updatedAt: z.string().datetime(),
139
});
140
141
export const ContractAcceptanceSchema = z.object({
142
  agentId: z.string().min(1),
143
  method: z.enum(['oauth_installation', 'oauth_account', 'policy_auto_accept', 'ed25519']),
144
  termsHash: z.string().min(1),
145
  acceptedAt: z.string().datetime(),
146
  signature: SignatureSchema.optional(),
147
});
148
149
export const FederatedInteractionStatusSchema = z.enum([
150
  'pending',
151
  'active',
152
  'rejected',
153
  'expired',
154
  'cancelled',
155
  'completed',
156
]);
157
158
export const FederatedInteractionSchema = z.object({
159
  protocolVersion: z.literal(PROTOCOL_VERSION),
160
  interactionId: z.string().uuid(),
161
  initiatorAgentId: z.string().min(1),
162
  responderAgentId: z.string().min(1),
163
  status: FederatedInteractionStatusSchema,
164
  contract: InteractionContractSchema,
165
  termsHash: z.string().min(1),
166
  acceptances: z.record(z.string(), ContractAcceptanceSchema),
167
  initiatorTransport: AgentTransportSchema.optional(),
168
  responderTransport: AgentTransportSchema,
169
  createdAt: z.string().datetime(),
170
  updatedAt: z.string().datetime(),
171
  expiresAt: z.string().datetime(),
172
});
173
174
export const LiveSessionInsightSchema = z.object({
175
  code: z.string().min(1),
176
  severity: z.enum(['info', 'caution', 'high']),
177
  message: z.string().min(1),
178
  evidenceReferences: z.array(z.string()).default([]),
179
});
180
181
export const LiveSessionOfferSchema = z.object({
182
  type: z.literal('openclasp.session.offer'),
183
  version: z.literal('1'),
184
  offerId: z.string().uuid(),
185
  interactionId: z.string().uuid(),
186
  agentId: z.string().min(1),
187
  role: z.enum(['initiator', 'responder']),
188
  counterparty: z.object({
189
    agentId: z.string().min(1),
190
    name: z.string().min(1),
191
    agentVersion: z.string().min(1),
192
    capabilities: z.array(z.string()),
193
  }),
194
  contract: InteractionContractSchema,
195
  contractHash: z.string().min(1),
196
  privateInsights: z.array(LiveSessionInsightSchema).default([]),
197
  issuedAt: z.string().datetime(),
198
  expiresAt: z.string().datetime(),
199
});
200
201
export const LiveSessionAcceptanceSchema = z.object({
202
  type: z.literal('openclasp.session.accepted'),
203
  version: z.literal('1'),
204
  offerId: z.string().uuid(),
205
  interactionId: z.string().uuid(),
206
  agentId: z.string().min(1),
207
  sessionId: z.string().uuid(),
208
  a2aEndpoint: z.string().url(),
209
  expiresAt: z.string().datetime(),
210
});
211
212
export const LiveSessionActivationSchema = z.object({
213
  type: z.literal('openclasp.session.activation'),
214
  version: z.literal('1'),
215
  activationId: z.string().uuid(),
216
  interactionId: z.string().uuid(),
217
  agentId: z.string().min(1),
218
  sessionId: z.string().uuid(),
219
  role: z.enum(['initiator', 'responder']),
220
  peer: z.object({
221
    agentId: z.string().min(1),
222
    sessionId: z.string().uuid(),
223
    endpoint: z.string().url(),
224
    bearerToken: z.string().min(1),
225
    verificationKey: z.string().min(1),
226
  }),
227
  reporting: z.object({
228
    endpoint: z.string().url(),
229
    bearerToken: z.string().min(1),
230
  }),
231
  contractHash: z.string().min(1),
232
  activatedAt: z.string().datetime(),
233
  expiresAt: z.string().datetime(),
234
});
235
236
const SessionDetailKeySchema = z.string().regex(/^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,63}$/);
237
238
export const StructuredSessionDetailsSchema = z
239
  .object({
240
    labels: z.array(SessionDetailKeySchema).max(32).default([]),
241
    metrics: z.record(SessionDetailKeySchema, z.number().finite()).default({}),
242
    flags: z.record(SessionDetailKeySchema, z.boolean()).default({}),
243
  })
244
  .strict()
245
  .default({ labels: [], metrics: {}, flags: {} });
246
247
export const LiveSessionEventSchema = z.object({
248
  eventId: z.string().uuid(),
249
  interactionId: z.string().uuid(),
250
  agentId: z.string().min(1),
251
  sequence: z.number().int().nonnegative(),
252
  type: z.enum([
253
    'session_started',
254
    'message_sent',
255
    'claim',
256
    'evidence',
257
    'correction',
258
    'constraint',
259
    'task_result',
260
    'session_completed',
261
    'session_failed',
262
  ]),
263
  occurredAt: z.string().datetime(),
264
  messageHash: z
265
    .string()
266
    .regex(/^[a-zA-Z0-9_-]{43}$/)
267
    .optional(),
268
  evidenceReferences: z.array(z.string()).default([]),
269
  outcome: z.enum(['success', 'failure', 'partial']).optional(),
270
  details: StructuredSessionDetailsSchema,
271
});
272
273
export const TrustEnvelopeSchema = z.object({
274
  protocolVersion: z.literal(PROTOCOL_VERSION),
275
  interactionId: z.string().uuid(),
276
  requestingAgentId: z.string(),
277
  respondingAgentId: z.string(),
278
  rootControllerId: z.string(),
279
  parentAgentId: z.string().optional(),
280
  agentVersion: z.string(),
281
  delegationId: z.string().uuid().optional(),
282
  requestedCapability: z.string(),
283
  taskCategory: z.string(),
284
  contractHash: z.string(),
285
  dataSharingMode: DataSharingModeSchema,
286
  evidenceRequirements: z.array(z.string()),
287
  timestamp: z.string().datetime(),
288
  expiresAt: z.string().datetime(),
289
  nonce: z.string().min(12),
290
  signature: SignatureSchema.optional(),
291
});
292
293
export const EventTypeSchema = z.enum([
294
  'claim',
295
  'evidence',
296
  'constraint',
297
  'commitment',
298
  'proposal',
299
  'objection',
300
  'policy_warning',
301
  'policy_violation',
302
  'private_suggestion',
303
  'shared_intervention',
304
  'delegation',
305
  'task_result',
306
  'resolution',
307
  'receipt',
308
  'feedback',
309
  'dispute',
310
]);
311
export const InteractionEventSchema = z.object({
312
  protocolVersion: z.literal(PROTOCOL_VERSION),
313
  eventId: z.string().uuid(),
314
  interactionId: z.string().uuid(),
315
  eventType: EventTypeSchema,
316
  agentId: z.string(),
317
  agentVersion: z.string(),
318
  timestamp: z.string().datetime(),
319
  visibility: VisibilitySchema,
320
  provenance: ProvenanceSchema,
321
  payloadHash: z.string(),
322
  payload: z.record(z.string(), z.unknown()),
323
  evidenceRefs: z.array(z.string()).optional(),
324
  signature: SignatureSchema.optional(),
325
});
326
327
export const FeedbackSchema = z.object({
328
  feedbackId: z.string().uuid(),
329
  interactionId: z.string().uuid(),
330
  receiptId: z.string().uuid(),
331
  reviewerAgentId: z.string(),
332
  subjectAgentId: z.string(),
333
  taskCompleted: z.boolean(),
334
  outputAccepted: z.boolean(),
335
  specificationMatched: z.boolean(),
336
  deadlineMet: z.boolean(),
337
  communicationQuality: z.number().min(0).max(1),
338
  evidenceProvided: z.boolean(),
339
  scopeRespected: z.boolean(),
340
  disputeRaised: z.boolean(),
341
  comment: z.string().max(1000).optional(),
342
  submittedAt: z.string().datetime(),
343
  signature: SignatureSchema.optional(),
344
});
345
346
export const ReceiptSchema = z.object({
347
  receiptId: z.string().uuid(),
348
  interactionId: z.string().uuid(),
349
  participants: z.array(z.string()),
350
  agentVersions: z.record(z.string(), z.string()),
351
  contractHash: z.string(),
352
  startedAt: z.string().datetime(),
353
  completedAt: z.string().datetime(),
354
  outcome: z.enum(['success', 'partial', 'failure', 'cancelled']),
355
  commitmentsFulfilled: z.array(z.string()),
356
  commitmentsMissed: z.array(z.string()),
357
  evidenceHashes: z.array(z.string()),
358
  policyWarnings: z.array(z.string()),
359
  policyViolations: z.array(z.string()),
360
  disputeStatus: z.enum(['none', 'open', 'resolved']),
361
  delegationChainHash: z.string(),
362
  unilateral: z.boolean(),
363
  signatures: z.record(z.string(), SignatureSchema).default({}),
364
});
365
366
export const RiskDecisionSchema = z.object({
367
  decision: z.enum(['ALLOW', 'CHALLENGE', 'DENY']),
368
  confidence: z.number().min(0).max(1),
369
  taskCategory: z.string(),
370
  sampleSize: z.number().int().nonnegative(),
371
  dimensions: z.record(z.string(), z.number()),
372
  reasons: z.array(z.string()),
373
  warnings: z.array(z.string()),
374
  dataFreshness: z.record(z.string(), z.string()),
375
  requiredChallenges: z.array(z.string()),
376
});
377
378
export const FactCheckResultSchema = z.object({
379
  claim: z.string(),
380
  claimType: z.enum(['objective', 'subjective', 'prediction']),
381
  status: z.enum([
382
    'verified',
383
    'supported',
384
    'contradicted',
385
    'unverified',
386
    'not_fact_checkable',
387
    'insufficient_permission',
388
  ]),
389
  confidence: z.number().min(0).max(1),
390
  evidenceReferences: z.array(z.string()),
391
  sourceAuthority: z.string(),
392
  sourceFreshness: z.string(),
393
  contradictingEvidence: z.array(z.string()),
394
  suggestedNextAction: z.string(),
395
});
396
397
export type AgentIdentity = z.infer<typeof AgentIdentitySchema>;
398
export type DelegationCredential = z.infer<typeof DelegationCredentialSchema>;
399
export type InteractionContract = z.infer<typeof InteractionContractSchema>;
400
export type TrustEnvelope = z.infer<typeof TrustEnvelopeSchema>;
401
export type InteractionEvent = z.infer<typeof InteractionEventSchema>;
402
export type Feedback = z.infer<typeof FeedbackSchema>;
403
export type Receipt = z.infer<typeof ReceiptSchema>;
404
export type RiskDecision = z.infer<typeof RiskDecisionSchema>;
405
export type FactCheckResult = z.infer<typeof FactCheckResultSchema>;
406
export type ExpectationManifest = z.infer<typeof ExpectationManifestSchema>;
407
export type AgentTransport = z.infer<typeof AgentTransportSchema>;
408
export type AgentPresence = z.infer<typeof AgentPresenceSchema>;
409
export type PublicAgentCard = z.infer<typeof PublicAgentCardSchema>;
410
export type ContractAcceptance = z.infer<typeof ContractAcceptanceSchema>;
411
export type FederatedInteraction = z.infer<typeof FederatedInteractionSchema>;
412
export type LiveSessionInsight = z.infer<typeof LiveSessionInsightSchema>;
413
export type LiveSessionOffer = z.infer<typeof LiveSessionOfferSchema>;
414
export type LiveSessionAcceptance = z.infer<typeof LiveSessionAcceptanceSchema>;
415
export type LiveSessionActivation = z.infer<typeof LiveSessionActivationSchema>;
416
export type LiveSessionEvent = z.infer<typeof LiveSessionEventSchema>;
417
418
export interface KeyPair {
419
  keyId: string;
420
  publicKey: string;
421
  privateKey: string;
422
}
423
424
export function createKeyPair(keyId: string = randomUUID()): KeyPair {
425
  const pair = generateKeyPairSync('ed25519');
426
  return {
427
    keyId,
428
    publicKey: pair.publicKey.export({ type: 'spki', format: 'der' }).toString('base64url'),
429
    privateKey: pair.privateKey.export({ type: 'pkcs8', format: 'der' }).toString('base64url'),
430
  };
431
}
432
433
export function canonicalHash(value: unknown): string {
434
  return createHash('sha256').update(canonicalize(value)).digest('base64url');
435
}
436
437
function unsigned(value: Record<string, unknown>): Record<string, unknown> {
438
  const copy = structuredClone(value);
439
  delete copy.signature;
440
  delete copy.signatures;
441
  return copy;
442
}
443
444
export function signObject<T extends Record<string, unknown>>(
445
  value: T,
446
  key: KeyPair,
447
): T & { signature: z.infer<typeof SignatureSchema> } {
448
  const payload = Buffer.from(canonicalize(unsigned(value)));
449
  const privateKey = {
450
    key: Buffer.from(key.privateKey, 'base64url'),
451
    type: 'pkcs8' as const,
452
    format: 'der' as const,
453
  };
454
  return {
455
    ...value,
456
    signature: {
457
      algorithm: 'Ed25519',
458
      keyId: key.keyId,
459
      value: sign(null, payload, privateKey).toString('base64url'),
460
    },
461
  };
462
}
463
464
export function verifyObject(value: Record<string, unknown>, publicKey: string): boolean {
465
  const parsed = SignatureSchema.safeParse(value.signature);
466
  if (!parsed.success) return false;
467
  const key = {
468
    key: Buffer.from(publicKey, 'base64url'),
469
    type: 'spki' as const,
470
    format: 'der' as const,
471
  };
472
  return verify(
473
    null,
474
    Buffer.from(canonicalize(unsigned(value))),
475
    key,
476
    Buffer.from(parsed.data.value, 'base64url'),
477
  );
478
}
479
480
export function signNamed<T extends Record<string, unknown>>(
481
  value: T,
482
  signerId: string,
483
  key: KeyPair,
484
): T & { signatures: Record<string, z.infer<typeof SignatureSchema>> } {
485
  const payload = Buffer.from(canonicalize(unsigned(value)));
486
  const privateKey = {
487
    key: Buffer.from(key.privateKey, 'base64url'),
488
    type: 'pkcs8' as const,
489
    format: 'der' as const,
490
  };
491
  const signature = {
492
    algorithm: 'Ed25519' as const,
493
    keyId: key.keyId,
494
    value: sign(null, payload, privateKey).toString('base64url'),
495
  };
496
  return {
497
    ...value,
498
    signatures: { ...((value.signatures as object) ?? {}), [signerId]: signature },
499
  };
500
}
501
502
export function verifyNamed(
503
  value: Record<string, unknown>,
504
  signerId: string,
505
  publicKey: string,
506
): boolean {
507
  const signatures = value.signatures as Record<string, unknown> | undefined;
508
  const parsed = SignatureSchema.safeParse(signatures?.[signerId]);
509
  if (!parsed.success) return false;
510
  const key = {
511
    key: Buffer.from(publicKey, 'base64url'),
512
    type: 'spki' as const,
513
    format: 'der' as const,
514
  };
515
  return verify(
516
    null,
517
    Buffer.from(canonicalize(unsigned(value))),
518
    key,
519
    Buffer.from(parsed.data.value, 'base64url'),
520
  );
521
}