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

Viewing files at commit c2c9d5ba27afCommit detailsView latest

packages/protocol/src/index.ts 409 lines · 13.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.literal('openclasp').default('openclasp'),
114
});
115
116
export const PublicAgentCardSchema = z.object({
117
  protocolVersion: z.literal(PROTOCOL_VERSION),
118
  agentId: z.string().min(1),
119
  name: z.string().min(1),
120
  description: z.string(),
121
  framework: z.string().min(1),
122
  agentVersion: z.string().min(1),
123
  capabilities: z.array(z.string()),
124
  limitations: z.array(z.string()),
125
  assurance: z.enum(['oauth_authenticated', 'cryptographically_verified']),
126
  transports: z.array(AgentTransportSchema),
127
  cardUrl: z.string().url(),
128
  a2aAgentCardUrl: z.string().url(),
129
  extensionUri: z.string().url(),
130
  publishedAt: z.string().datetime(),
131
  updatedAt: z.string().datetime(),
132
});
133
134
export const ContractAcceptanceSchema = z.object({
135
  agentId: z.string().min(1),
136
  method: z.enum(['oauth_installation', 'oauth_account', 'policy_auto_accept', 'ed25519']),
137
  termsHash: z.string().min(1),
138
  acceptedAt: z.string().datetime(),
139
  signature: SignatureSchema.optional(),
140
});
141
142
export const FederatedInteractionStatusSchema = z.enum([
143
  'pending',
144
  'active',
145
  'rejected',
146
  'expired',
147
  'cancelled',
148
  'completed',
149
]);
150
151
export const FederatedInteractionSchema = z.object({
152
  protocolVersion: z.literal(PROTOCOL_VERSION),
153
  interactionId: z.string().uuid(),
154
  initiatorAgentId: z.string().min(1),
155
  responderAgentId: z.string().min(1),
156
  status: FederatedInteractionStatusSchema,
157
  contract: InteractionContractSchema,
158
  termsHash: z.string().min(1),
159
  acceptances: z.record(z.string(), ContractAcceptanceSchema),
160
  initiatorTransport: AgentTransportSchema.optional(),
161
  responderTransport: AgentTransportSchema,
162
  createdAt: z.string().datetime(),
163
  updatedAt: z.string().datetime(),
164
  expiresAt: z.string().datetime(),
165
});
166
167
export const TrustEnvelopeSchema = z.object({
168
  protocolVersion: z.literal(PROTOCOL_VERSION),
169
  interactionId: z.string().uuid(),
170
  requestingAgentId: z.string(),
171
  respondingAgentId: z.string(),
172
  rootControllerId: z.string(),
173
  parentAgentId: z.string().optional(),
174
  agentVersion: z.string(),
175
  delegationId: z.string().uuid().optional(),
176
  requestedCapability: z.string(),
177
  taskCategory: z.string(),
178
  contractHash: z.string(),
179
  dataSharingMode: DataSharingModeSchema,
180
  evidenceRequirements: z.array(z.string()),
181
  timestamp: z.string().datetime(),
182
  expiresAt: z.string().datetime(),
183
  nonce: z.string().min(12),
184
  signature: SignatureSchema.optional(),
185
});
186
187
export const EventTypeSchema = z.enum([
188
  'claim',
189
  'evidence',
190
  'constraint',
191
  'commitment',
192
  'proposal',
193
  'objection',
194
  'policy_warning',
195
  'policy_violation',
196
  'private_suggestion',
197
  'shared_intervention',
198
  'delegation',
199
  'task_result',
200
  'resolution',
201
  'receipt',
202
  'feedback',
203
  'dispute',
204
]);
205
export const InteractionEventSchema = z.object({
206
  protocolVersion: z.literal(PROTOCOL_VERSION),
207
  eventId: z.string().uuid(),
208
  interactionId: z.string().uuid(),
209
  eventType: EventTypeSchema,
210
  agentId: z.string(),
211
  agentVersion: z.string(),
212
  timestamp: z.string().datetime(),
213
  visibility: VisibilitySchema,
214
  provenance: ProvenanceSchema,
215
  payloadHash: z.string(),
216
  payload: z.record(z.string(), z.unknown()),
217
  evidenceRefs: z.array(z.string()).optional(),
218
  signature: SignatureSchema.optional(),
219
});
220
221
export const FeedbackSchema = z.object({
222
  feedbackId: z.string().uuid(),
223
  interactionId: z.string().uuid(),
224
  receiptId: z.string().uuid(),
225
  reviewerAgentId: z.string(),
226
  subjectAgentId: z.string(),
227
  taskCompleted: z.boolean(),
228
  outputAccepted: z.boolean(),
229
  specificationMatched: z.boolean(),
230
  deadlineMet: z.boolean(),
231
  communicationQuality: z.number().min(0).max(1),
232
  evidenceProvided: z.boolean(),
233
  scopeRespected: z.boolean(),
234
  disputeRaised: z.boolean(),
235
  comment: z.string().max(1000).optional(),
236
  submittedAt: z.string().datetime(),
237
  signature: SignatureSchema.optional(),
238
});
239
240
export const ReceiptSchema = z.object({
241
  receiptId: z.string().uuid(),
242
  interactionId: z.string().uuid(),
243
  participants: z.array(z.string()),
244
  agentVersions: z.record(z.string(), z.string()),
245
  contractHash: z.string(),
246
  startedAt: z.string().datetime(),
247
  completedAt: z.string().datetime(),
248
  outcome: z.enum(['success', 'partial', 'failure', 'cancelled']),
249
  commitmentsFulfilled: z.array(z.string()),
250
  commitmentsMissed: z.array(z.string()),
251
  evidenceHashes: z.array(z.string()),
252
  policyWarnings: z.array(z.string()),
253
  policyViolations: z.array(z.string()),
254
  disputeStatus: z.enum(['none', 'open', 'resolved']),
255
  delegationChainHash: z.string(),
256
  unilateral: z.boolean(),
257
  signatures: z.record(z.string(), SignatureSchema).default({}),
258
});
259
260
export const RiskDecisionSchema = z.object({
261
  decision: z.enum(['ALLOW', 'CHALLENGE', 'DENY']),
262
  confidence: z.number().min(0).max(1),
263
  taskCategory: z.string(),
264
  sampleSize: z.number().int().nonnegative(),
265
  dimensions: z.record(z.string(), z.number()),
266
  reasons: z.array(z.string()),
267
  warnings: z.array(z.string()),
268
  dataFreshness: z.record(z.string(), z.string()),
269
  requiredChallenges: z.array(z.string()),
270
});
271
272
export const FactCheckResultSchema = z.object({
273
  claim: z.string(),
274
  claimType: z.enum(['objective', 'subjective', 'prediction']),
275
  status: z.enum([
276
    'verified',
277
    'supported',
278
    'contradicted',
279
    'unverified',
280
    'not_fact_checkable',
281
    'insufficient_permission',
282
  ]),
283
  confidence: z.number().min(0).max(1),
284
  evidenceReferences: z.array(z.string()),
285
  sourceAuthority: z.string(),
286
  sourceFreshness: z.string(),
287
  contradictingEvidence: z.array(z.string()),
288
  suggestedNextAction: z.string(),
289
});
290
291
export type AgentIdentity = z.infer<typeof AgentIdentitySchema>;
292
export type DelegationCredential = z.infer<typeof DelegationCredentialSchema>;
293
export type InteractionContract = z.infer<typeof InteractionContractSchema>;
294
export type TrustEnvelope = z.infer<typeof TrustEnvelopeSchema>;
295
export type InteractionEvent = z.infer<typeof InteractionEventSchema>;
296
export type Feedback = z.infer<typeof FeedbackSchema>;
297
export type Receipt = z.infer<typeof ReceiptSchema>;
298
export type RiskDecision = z.infer<typeof RiskDecisionSchema>;
299
export type FactCheckResult = z.infer<typeof FactCheckResultSchema>;
300
export type ExpectationManifest = z.infer<typeof ExpectationManifestSchema>;
301
export type AgentTransport = z.infer<typeof AgentTransportSchema>;
302
export type PublicAgentCard = z.infer<typeof PublicAgentCardSchema>;
303
export type ContractAcceptance = z.infer<typeof ContractAcceptanceSchema>;
304
export type FederatedInteraction = z.infer<typeof FederatedInteractionSchema>;
305
306
export interface KeyPair {
307
  keyId: string;
308
  publicKey: string;
309
  privateKey: string;
310
}
311
312
export function createKeyPair(keyId: string = randomUUID()): KeyPair {
313
  const pair = generateKeyPairSync('ed25519');
314
  return {
315
    keyId,
316
    publicKey: pair.publicKey.export({ type: 'spki', format: 'der' }).toString('base64url'),
317
    privateKey: pair.privateKey.export({ type: 'pkcs8', format: 'der' }).toString('base64url'),
318
  };
319
}
320
321
export function canonicalHash(value: unknown): string {
322
  return createHash('sha256').update(canonicalize(value)).digest('base64url');
323
}
324
325
function unsigned(value: Record<string, unknown>): Record<string, unknown> {
326
  const copy = structuredClone(value);
327
  delete copy.signature;
328
  delete copy.signatures;
329
  return copy;
330
}
331
332
export function signObject<T extends Record<string, unknown>>(
333
  value: T,
334
  key: KeyPair,
335
): T & { signature: z.infer<typeof SignatureSchema> } {
336
  const payload = Buffer.from(canonicalize(unsigned(value)));
337
  const privateKey = {
338
    key: Buffer.from(key.privateKey, 'base64url'),
339
    type: 'pkcs8' as const,
340
    format: 'der' as const,
341
  };
342
  return {
343
    ...value,
344
    signature: {
345
      algorithm: 'Ed25519',
346
      keyId: key.keyId,
347
      value: sign(null, payload, privateKey).toString('base64url'),
348
    },
349
  };
350
}
351
352
export function verifyObject(value: Record<string, unknown>, publicKey: string): boolean {
353
  const parsed = SignatureSchema.safeParse(value.signature);
354
  if (!parsed.success) return false;
355
  const key = {
356
    key: Buffer.from(publicKey, 'base64url'),
357
    type: 'spki' as const,
358
    format: 'der' as const,
359
  };
360
  return verify(
361
    null,
362
    Buffer.from(canonicalize(unsigned(value))),
363
    key,
364
    Buffer.from(parsed.data.value, 'base64url'),
365
  );
366
}
367
368
export function signNamed<T extends Record<string, unknown>>(
369
  value: T,
370
  signerId: string,
371
  key: KeyPair,
372
): T & { signatures: Record<string, z.infer<typeof SignatureSchema>> } {
373
  const payload = Buffer.from(canonicalize(unsigned(value)));
374
  const privateKey = {
375
    key: Buffer.from(key.privateKey, 'base64url'),
376
    type: 'pkcs8' as const,
377
    format: 'der' as const,
378
  };
379
  const signature = {
380
    algorithm: 'Ed25519' as const,
381
    keyId: key.keyId,
382
    value: sign(null, payload, privateKey).toString('base64url'),
383
  };
384
  return {
385
    ...value,
386
    signatures: { ...((value.signatures as object) ?? {}), [signerId]: signature },
387
  };
388
}
389
390
export function verifyNamed(
391
  value: Record<string, unknown>,
392
  signerId: string,
393
  publicKey: string,
394
): boolean {
395
  const signatures = value.signatures as Record<string, unknown> | undefined;
396
  const parsed = SignatureSchema.safeParse(signatures?.[signerId]);
397
  if (!parsed.success) return false;
398
  const key = {
399
    key: Buffer.from(publicKey, 'base64url'),
400
    type: 'spki' as const,
401
    format: 'der' as const,
402
  };
403
  return verify(
404
    null,
405
    Buffer.from(canonicalize(unsigned(value))),
406
    key,
407
    Buffer.from(parsed.data.value, 'base64url'),
408
  );
409
}