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

Viewing files at commit 1da1b7ef4016Commit detailsView latest

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