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

Viewing files at commit d596f9595c49Commit detailsView latest

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