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

Viewing files at commit 9c0e6fd29feaCommit detailsView latest

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