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

Viewing files at commit 479efea741d5Commit detailsView latest

packages/core/src/index.ts 877 lines · 30.7 KB · typescript
1
import { randomUUID } from 'node:crypto';
2
import {
3
  AgentIdentitySchema,
4
  CounterpartyBriefSchema,
5
  canonicalHash,
6
  createKeyPair,
7
  DelegationCredentialSchema,
8
  FeedbackSchema,
9
  FeedbackDimensionSchema,
10
  InteractionContractSchema,
11
  InteractionConclusionSchema,
12
  InteractionEventSchema,
13
  ReceiptSchema,
14
  signNamed,
15
  signObject,
16
  TrustEnvelopeSchema,
17
  verifyNamed,
18
  verifyObject,
19
  type AgentIdentity,
20
  type CounterpartyBrief,
21
  type DelegationCredential,
22
  type FactCheckResult,
23
  type Feedback,
24
  type InteractionContract,
25
  type InteractionConclusion,
26
  type InteractionCompletionReport,
27
  type InteractionFeedback,
28
  type InteractionEvent,
29
  type KeyPair,
30
  type LiveSessionInsight,
31
  type PublicAgentCard,
32
  type Receipt,
33
  type RiskDecision,
34
  type TrustEnvelope,
35
} from '../../protocol/src/index.js';
36
37
const REQUIREMENT_STOP_WORDS = new Set([
38
  'about',
39
  'agent',
40
  'available',
41
  'from',
42
  'have',
43
  'into',
44
  'must',
45
  'provide',
46
  'return',
47
  'should',
48
  'that',
49
  'their',
50
  'this',
51
  'with',
52
]);
53
54
function normalized(value: string): string {
55
  return value
56
    .trim()
57
    .toLowerCase()
58
    .replace(/[^a-z0-9]+/g, ' ')
59
    .trim();
60
}
61
62
function meaningfulTerms(value: string): string[] {
63
  return [
64
    ...new Set(
65
      normalized(value)
66
        .split(' ')
67
        .filter((term) => term.length >= 4 && !REQUIREMENT_STOP_WORDS.has(term)),
68
    ),
69
  ];
70
}
71
72
function assessDeclaration(
73
  requirement: string,
74
  declarations: string[],
75
): { status: 'match' | 'partial' | 'mismatch' | 'unknown'; reason: string; confidence: number } {
76
  const normalizedRequirement = normalized(requirement);
77
  const normalizedDeclarations = declarations.map(normalized);
78
  if (
79
    normalizedDeclarations.some(
80
      (declaration) => declaration === '*' || declaration === normalizedRequirement,
81
    )
82
  )
83
    return {
84
      status: 'match',
85
      reason: 'The counterparty explicitly declares this capability.',
86
      confidence: 0.95,
87
    };
88
89
  const terms = meaningfulTerms(requirement);
90
  const negative = normalizedDeclarations.find((declaration) =>
91
    terms.some(
92
      (term) =>
93
        declaration.includes(`no ${term}`) ||
94
        declaration.includes(`not ${term}`) ||
95
        declaration.includes(`cannot ${term}`),
96
    ),
97
  );
98
  if (negative)
99
    return {
100
      status: 'mismatch',
101
      reason: `The counterparty declares a conflicting limitation: ${negative}`,
102
      confidence: 0.95,
103
    };
104
105
  if (terms.length) {
106
    const declarationText = normalizedDeclarations.join(' ');
107
    const overlap = terms.filter((term) => declarationText.includes(term)).length / terms.length;
108
    if (overlap >= 0.6)
109
      return {
110
        status: 'match',
111
        reason: 'The counterparty declarations strongly match this requirement.',
112
        confidence: 0.8,
113
      };
114
    if (overlap > 0)
115
      return {
116
        status: 'partial',
117
        reason: 'The counterparty declarations only partially cover this requirement.',
118
        confidence: 0.65,
119
      };
120
  }
121
  return {
122
    status: 'unknown',
123
    reason: 'The published agent card does not establish this requirement.',
124
    confidence: 0.35,
125
  };
126
}
127
128
export function buildCounterpartyBrief(input: {
129
  interactionId: string;
130
  contractHash: string;
131
  contract: InteractionContract;
132
  recipientAgentId: string;
133
  subject: PublicAgentCard;
134
  historyInsights: LiveSessionInsight[];
135
  relevantSampleSize: number;
136
  historyConfidence: number;
137
  generatedAt?: string;
138
  expiresAt: string;
139
}): CounterpartyBrief {
140
  const declarations = [
141
    input.subject.description,
142
    ...input.subject.capabilities,
143
    ...input.subject.limitations,
144
  ];
145
  const requirements = [
146
    {
147
      requirementId: 'task-category',
148
      requirement: input.contract.taskCategory,
149
      kind: 'capability' as const,
150
    },
151
    {
152
      requirementId: 'requested-outcome',
153
      requirement: input.contract.requestedOutcome,
154
      kind: 'scope' as const,
155
    },
156
    ...input.contract.allowedActions.map((requirement, index) => ({
157
      requirementId: `allowed-action-${index + 1}`,
158
      requirement,
159
      kind: 'capability' as const,
160
    })),
161
    ...input.contract.successCriteria.map((requirement, index) => ({
162
      requirementId: `success-criterion-${index + 1}`,
163
      requirement,
164
      kind: 'success_criterion' as const,
165
    })),
166
    ...input.contract.evidenceRequirements.map((requirement, index) => ({
167
      requirementId: `evidence-${index + 1}`,
168
      requirement,
169
      kind: 'evidence' as const,
170
    })),
171
  ].map((requirement) => ({
172
    ...requirement,
173
    ...assessDeclaration(requirement.requirement, declarations),
174
    sources: ['contract', 'self_declared'] as const,
175
    evidenceReferences: [input.subject.cardUrl],
176
  }));
177
  const requirementReferences = requirements
178
    .filter((requirement) => requirement.status !== 'match')
179
    .map((requirement) => requirement.requirementId);
180
  const insights = input.historyInsights.map((insight) => ({
181
    ...insight,
182
    requirementReferences:
183
      insight.requirementReferences.length > 0
184
        ? insight.requirementReferences
185
        : requirementReferences,
186
  }));
187
  const needsChallenge =
188
    input.relevantSampleSize === 0 ||
189
    requirements.some((requirement) => requirement.status === 'mismatch') ||
190
    insights.some((insight) => insight.severity === 'high');
191
  return CounterpartyBriefSchema.parse({
192
    briefId: randomUUID(),
193
    interactionId: input.interactionId,
194
    contractHash: input.contractHash,
195
    recipientAgentId: input.recipientAgentId,
196
    subjectAgentId: input.subject.agentId,
197
    taskCategory: input.contract.taskCategory,
198
    decision: needsChallenge ? 'CHALLENGE' : 'ALLOW',
199
    requirements,
200
    insights,
201
    relevantSampleSize: input.relevantSampleSize,
202
    historyConfidence: input.historyConfidence,
203
    subjectAgentVersion: input.subject.agentVersion,
204
    recommendedContractChanges: requirements
205
      .filter((requirement) => requirement.status === 'mismatch')
206
      .map((requirement) => `Resolve mismatch: ${requirement.requirement}`),
207
    generatedAt: input.generatedAt ?? new Date().toISOString(),
208
    expiresAt: input.expiresAt,
209
  });
210
}
211
212
export function buildInteractionConclusion(input: {
213
  interaction: { interactionId: string; termsHash: string; contract: InteractionContract };
214
  reports: InteractionCompletionReport[];
215
  feedback: InteractionFeedback[];
216
  conclusionId?: string;
217
  generatedAt?: string;
218
}): InteractionConclusion {
219
  const outcomes = input.reports.map((report) => report.outcome);
220
  const uniqueOutcomes = new Set(outcomes);
221
  const outcome =
222
    outcomes.length === 0
223
      ? ('cancelled' as const)
224
      : uniqueOutcomes.size === 1
225
        ? outcomes[0]!
226
        : outcomes.includes('failure')
227
          ? ('failure' as const)
228
          : ('partial' as const);
229
  const consensus =
230
    input.reports.length < 2
231
      ? ('unilateral' as const)
232
      : uniqueOutcomes.size === 1
233
        ? ('bilateral_agreement' as const)
234
        : outcomes.includes('partial')
235
          ? ('bilateral_partial_agreement' as const)
236
          : ('conflicting' as const);
237
  const statusRank = { met: 0, partially_met: 1, unknown: 2, missed: 3 } as const;
238
  const criteria = input.interaction.contract.successCriteria.map((criterion) => {
239
    const assessments = input.reports
240
      .flatMap((report) => report.criteria)
241
      .filter((assessment) => assessment.criterion === criterion);
242
    const selected = assessments.sort(
243
      (left, right) => statusRank[right.status] - statusRank[left.status],
244
    )[0];
245
    return {
246
      criterion,
247
      status: selected?.status ?? ('unknown' as const),
248
      ...(selected?.explanation ? { explanation: selected.explanation } : {}),
249
      evidenceReferences: [
250
        ...new Set(assessments.flatMap((assessment) => assessment.evidenceReferences)),
251
      ],
252
    };
253
  });
254
  const averageRatings = Object.fromEntries(
255
    FeedbackDimensionSchema.options.flatMap((dimension) => {
256
      const values = input.feedback
257
        .map((item) => item.ratings[dimension])
258
        .filter((value): value is number => typeof value === 'number');
259
      return values.length
260
        ? [[dimension, values.reduce((total, value) => total + value, 0) / values.length]]
261
        : [];
262
    }),
263
  );
264
  return InteractionConclusionSchema.parse({
265
    conclusionId: input.conclusionId ?? randomUUID(),
266
    interactionId: input.interaction.interactionId,
267
    contractHash: input.interaction.termsHash,
268
    outcome,
269
    consensus,
270
    summary: `${input.reports.length} completion report${input.reports.length === 1 ? '' : 's'} and ${input.feedback.length} feedback response${input.feedback.length === 1 ? '' : 's'} were eligible for this conclusion.`,
271
    criteria,
272
    reportIds: input.reports.map((report) => report.reportId),
273
    feedbackIds: input.feedback.map((item) => item.feedbackId),
274
    averageRatings,
275
    evidenceReferences: [...new Set(input.reports.flatMap((report) => report.evidenceReferences))],
276
    generatedAt: input.generatedAt ?? new Date().toISOString(),
277
  });
278
}
279
280
export interface AuditStore {
281
  append(kind: string, id: string, value: unknown): void;
282
  list(kind: string): unknown[];
283
}
284
285
export class MemoryAuditStore implements AuditStore {
286
  private rows: { kind: string; id: string; value: unknown }[] = [];
287
  append(kind: string, id: string, value: unknown): void {
288
    const old = this.rows.find((row) => row.kind === kind && row.id === id);
289
    if (old && canonicalHash(old.value) !== canonicalHash(value))
290
      throw new Error(`Conflicting duplicate ${kind}:${id}`);
291
    if (!old) this.rows.push({ kind, id, value: structuredClone(value) });
292
  }
293
  list(kind: string): unknown[] {
294
    return this.rows.filter((row) => row.kind === kind).map((row) => structuredClone(row.value));
295
  }
296
}
297
298
export function createIdentity(input: {
299
  agentId: string;
300
  operatorRef: string;
301
  version?: string;
302
  capabilities: string[];
303
  assurance?: AgentIdentity['assurance'];
304
  parentAgentId?: string;
305
  rootControllerId?: string;
306
}): { identity: AgentIdentity; keyPair: KeyPair } {
307
  const keyPair = createKeyPair(`${input.agentId}#1`);
308
  const identity: Record<string, unknown> = {
309
    protocolVersion: '0.1',
310
    agentId: input.agentId,
311
    publicKey: keyPair.publicKey,
312
    keyId: keyPair.keyId,
313
    operatorRef: input.operatorRef,
314
    assurance: input.assurance ?? 'pseudonymous',
315
    agentVersion: input.version ?? '1.0.0',
316
    capabilities: input.capabilities,
317
    supportedProtocols: ['A2A/1.0', 'OpenClasp/0.1'],
318
    ...(input.parentAgentId ? { parentAgentId: input.parentAgentId } : {}),
319
    rootControllerId: input.rootControllerId ?? input.agentId,
320
    revoked: false,
321
    provenance: 'cryptographically_verified',
322
    createdAt: new Date().toISOString(),
323
  };
324
  return { identity: AgentIdentitySchema.parse(signObject(identity, keyPair)), keyPair };
325
}
326
327
export type PolicyContext = {
328
  envelope: TrustEnvelope;
329
  action: string;
330
  dataClasses?: string[];
331
  humanApproved?: boolean;
332
};
333
334
export type Conflict = {
335
  conflictId: string;
336
  interactionId: string;
337
  issue: string;
338
  positions: Record<string, string>;
339
  evidence: string[];
340
  contractClauses: string[];
341
  missingInformation: string[];
342
  possibleResolutions: string[];
343
  permissions: Record<string, boolean>;
344
  status: 'pending_consent' | 'open' | 'resolved';
345
  resolution?: string;
346
};
347
348
type ProfileAggregate = {
349
  agentId: string;
350
  agentVersion: string;
351
  taskCategory: string;
352
  sampleSize: number;
353
  updatedAt: string;
354
  completion: number;
355
  acceptance: number;
356
  specification: number;
357
  deadline: number;
358
  communication: number;
359
  evidence: number;
360
  scope: number;
361
  disputes: number;
362
};
363
364
export class TrustEngine {
365
  readonly agents = new Map<string, AgentIdentity>();
366
  readonly delegations = new Map<string, DelegationCredential>();
367
  readonly contracts = new Map<string, InteractionContract>();
368
  readonly events = new Map<string, InteractionEvent>();
369
  readonly receipts = new Map<string, Receipt>();
370
  readonly conflicts = new Map<string, Conflict>();
371
  readonly feedback = new Map<string, Feedback>();
372
  readonly profiles = new Map<string, ProfileAggregate>();
373
  readonly contributionConsent = new Map<
374
    string,
375
    { enabled: boolean; grantedAt: string; revokedAt?: string }
376
  >();
377
  private readonly nonces = new Set<string>();
378
379
  constructor(readonly store: AuditStore = new MemoryAuditStore()) {
380
    for (const value of store.list('agent') as AgentIdentity[])
381
      this.agents.set(value.agentId, value);
382
    for (const value of store.list('delegation') as DelegationCredential[])
383
      this.delegations.set(value.delegationId, value);
384
    for (const value of store.list('contract') as InteractionContract[])
385
      this.contracts.set(value.interactionId, value);
386
    for (const value of store.list('event') as InteractionEvent[])
387
      this.events.set(value.eventId, value);
388
    for (const value of store.list('receipt') as Receipt[])
389
      this.receipts.set(value.receiptId, value);
390
    for (const value of store.list('feedback') as Feedback[])
391
      this.feedback.set(value.feedbackId, value);
392
    for (const value of store.list('profile') as ProfileAggregate[])
393
      this.profiles.set(`${value.agentId}|${value.agentVersion}|${value.taskCategory}`, value);
394
    for (const value of store.list('conflict') as Conflict[])
395
      this.conflicts.set(value.conflictId, value);
396
    for (const value of store.list('consent') as {
397
      agentId: string;
398
      enabled: boolean;
399
      grantedAt: string;
400
      revokedAt?: string;
401
    }[])
402
      this.contributionConsent.set(value.agentId, value);
403
    for (const value of store.list('revocation') as {
404
      agentId?: string;
405
      delegationId?: string;
406
    }[]) {
407
      if (value.agentId && this.agents.has(value.agentId))
408
        this.agents.set(value.agentId, { ...this.agents.get(value.agentId)!, revoked: true });
409
      if (value.delegationId && this.delegations.has(value.delegationId))
410
        this.delegations.set(value.delegationId, {
411
          ...this.delegations.get(value.delegationId)!,
412
          revoked: true,
413
        });
414
    }
415
  }
416
417
  registerAgent(identity: AgentIdentity): AgentIdentity {
418
    const parsed = AgentIdentitySchema.parse(identity);
419
    if (!verifyObject(parsed as unknown as Record<string, unknown>, parsed.publicKey))
420
      throw new Error('Invalid identity signature');
421
    if (parsed.parentAgentId && parsed.rootControllerId === parsed.agentId)
422
      throw new Error('Child cannot remove root-controller reference');
423
    this.agents.set(parsed.agentId, parsed);
424
    this.store.append('agent', parsed.agentId, parsed);
425
    return parsed;
426
  }
427
428
  revokeAgent(agentId: string): void {
429
    const identity = this.requireAgent(agentId);
430
    const revoked = { ...identity, revoked: true };
431
    this.agents.set(agentId, revoked);
432
    this.store.append('revocation', `agent:${agentId}`, { agentId, at: new Date().toISOString() });
433
  }
434
435
  createDelegation(
436
    parentId: string,
437
    childId: string,
438
    capabilities: string[],
439
    expiresAt: string,
440
    parentKey: KeyPair,
441
  ): DelegationCredential {
442
    const parent = this.requireAgent(parentId);
443
    const child = this.requireAgent(childId);
444
    if (parent.revoked) throw new Error('Parent agent is revoked');
445
    if (child.parentAgentId !== parentId || child.rootControllerId !== parent.rootControllerId)
446
      throw new Error('Invalid child lineage');
447
    if (capabilities.some((capability) => !parent.capabilities.includes(capability)))
448
      throw new Error('Delegation exceeds parent authority');
449
    const raw = {
450
      protocolVersion: '0.1' as const,
451
      delegationId: randomUUID(),
452
      parentAgentId: parentId,
453
      childAgentId: childId,
454
      rootControllerId: parent.rootControllerId,
455
      capabilities,
456
      issuedAt: new Date().toISOString(),
457
      expiresAt,
458
      revoked: false,
459
    };
460
    const credential = DelegationCredentialSchema.parse(signObject(raw, parentKey));
461
    this.delegations.set(credential.delegationId, credential);
462
    this.store.append('delegation', credential.delegationId, credential);
463
    return credential;
464
  }
465
466
  verifyDelegation(delegationId: string, capability?: string, at = new Date()): boolean {
467
    const delegation = this.delegations.get(delegationId);
468
    if (!delegation || delegation.revoked || new Date(delegation.expiresAt) <= at) return false;
469
    const parent = this.agents.get(delegation.parentAgentId);
470
    const child = this.agents.get(delegation.childAgentId);
471
    if (!parent || !child || parent.revoked || child.revoked) return false;
472
    if (
473
      child.rootControllerId !== delegation.rootControllerId ||
474
      child.parentAgentId !== parent.agentId
475
    )
476
      return false;
477
    if (capability && !delegation.capabilities.includes(capability)) return false;
478
    if (delegation.capabilities.some((item) => !parent.capabilities.includes(item))) return false;
479
    return verifyObject(delegation as unknown as Record<string, unknown>, parent.publicKey);
480
  }
481
482
  revokeDelegation(delegationId: string): void {
483
    const item = this.delegations.get(delegationId);
484
    if (!item) throw new Error('Delegation not found');
485
    this.delegations.set(delegationId, { ...item, revoked: true });
486
    this.store.append('revocation', `delegation:${delegationId}`, {
487
      delegationId,
488
      at: new Date().toISOString(),
489
    });
490
  }
491
492
  saveContract(contract: InteractionContract): InteractionContract {
493
    const parsed = InteractionContractSchema.parse(contract);
494
    if (parsed.parties.some((party) => !(party in parsed.signatures)))
495
      throw new Error('Contract requires every party signature');
496
    for (const party of Object.keys(parsed.signatures)) {
497
      const agent = this.requireAgent(party);
498
      if (!verifyNamed(parsed as unknown as Record<string, unknown>, party, agent.publicKey))
499
        throw new Error(`Invalid contract signature: ${party}`);
500
    }
501
    this.contracts.set(parsed.interactionId, parsed);
502
    this.store.append('contract', parsed.interactionId, parsed);
503
    return parsed;
504
  }
505
506
  assess(context: PolicyContext): RiskDecision {
507
    const envelope = TrustEnvelopeSchema.parse(context.envelope);
508
    const responder = this.agents.get(envelope.respondingAgentId);
509
    const requester = this.agents.get(envelope.requestingAgentId);
510
    const contract = this.contracts.get(envelope.interactionId);
511
    const hardFailures: string[] = [];
512
    if (
513
      !requester ||
514
      requester.revoked ||
515
      !verifyObject(envelope as unknown as Record<string, unknown>, requester.publicKey)
516
    )
517
      hardFailures.push('invalid_requester_signature');
518
    if (!responder || responder.revoked) hardFailures.push('invalid_or_revoked_responder');
519
    if (new Date(envelope.expiresAt) <= new Date()) hardFailures.push('expired_envelope');
520
    if (this.nonces.has(envelope.nonce)) hardFailures.push('replay_attempt');
521
    if (
522
      envelope.delegationId &&
523
      !this.verifyDelegation(envelope.delegationId, envelope.requestedCapability)
524
    )
525
      hardFailures.push('invalid_delegation');
526
    if (!contract || canonicalHash({ ...contract, signatures: {} }) !== envelope.contractHash)
527
      hardFailures.push('contract_mismatch');
528
    if (
529
      contract &&
530
      (!contract.allowedActions.includes(context.action) ||
531
        contract.prohibitedActions.includes(context.action))
532
    )
533
      hardFailures.push('action_outside_contract');
534
    if (
535
      contract &&
536
      (context.dataClasses ?? []).some((item) => contract.prohibitedData.includes(item))
537
    )
538
      hardFailures.push('prohibited_data');
539
    if (
540
      contract &&
541
      contract.humanApprovalRequirements.includes(context.action) &&
542
      !context.humanApproved
543
    )
544
      hardFailures.push('missing_human_approval');
545
    if (!hardFailures.includes('replay_attempt')) this.nonces.add(envelope.nonce);
546
    if (hardFailures.length)
547
      return this.decision('DENY', envelope.taskCategory, 1, hardFailures, [], []);
548
549
    const risk = this.getRisk(
550
      envelope.respondingAgentId,
551
      responder?.agentVersion ?? envelope.agentVersion,
552
      envelope.taskCategory,
553
    );
554
    return risk;
555
  }
556
557
  recordEvent(event: InteractionEvent): InteractionEvent {
558
    const parsed = InteractionEventSchema.parse(event);
559
    const agent = this.requireAgent(parsed.agentId);
560
    if (!verifyObject(parsed as unknown as Record<string, unknown>, agent.publicKey))
561
      throw new Error('Invalid event signature');
562
    if (canonicalHash(parsed.payload) !== parsed.payloadHash)
563
      throw new Error('Event payload hash mismatch');
564
    const existing = this.events.get(parsed.eventId);
565
    if (existing && canonicalHash(existing) !== canonicalHash(parsed))
566
      throw new Error('Conflicting duplicate event');
567
    this.events.set(parsed.eventId, parsed);
568
    this.store.append('event', parsed.eventId, parsed);
569
    return parsed;
570
  }
571
572
  createConflict(
573
    input: Omit<Conflict, 'conflictId' | 'status' | 'permissions'> & { participants: string[] },
574
  ): Conflict {
575
    const permissions = Object.fromEntries(input.participants.map((id) => [id, false]));
576
    const conflict: Conflict = {
577
      ...input,
578
      conflictId: randomUUID(),
579
      permissions,
580
      status: 'pending_consent',
581
    };
582
    delete (conflict as any).participants;
583
    this.conflicts.set(conflict.conflictId, conflict);
584
    this.store.append('conflict', `${conflict.conflictId}:created`, conflict);
585
    return conflict;
586
  }
587
588
  permitMediation(conflictId: string, agentId: string): Conflict {
589
    const conflict = this.requireConflict(conflictId);
590
    if (!(agentId in conflict.permissions)) throw new Error('Agent is not a conflict participant');
591
    conflict.permissions[agentId] = true;
592
    if (Object.values(conflict.permissions).every(Boolean)) conflict.status = 'open';
593
    this.store.append('conflict', `${conflict.conflictId}:permit:${agentId}`, conflict);
594
    return conflict;
595
  }
596
597
  resolveConflict(conflictId: string, resolution: string): Conflict {
598
    const conflict = this.requireConflict(conflictId);
599
    if (conflict.status !== 'open') throw new Error('Mutual mediation consent required');
600
    conflict.status = 'resolved';
601
    conflict.resolution = resolution;
602
    this.store.append('conflict', `${conflict.conflictId}:resolved`, conflict);
603
    return conflict;
604
  }
605
606
  submitReceipt(receipt: Receipt): Receipt {
607
    const parsed = this.verifyReceipt(receipt);
608
    this.receipts.set(parsed.receiptId, parsed);
609
    this.store.append('receipt', parsed.receiptId, parsed);
610
    return parsed;
611
  }
612
613
  verifyReceipt(receipt: Receipt): Receipt {
614
    const parsed = ReceiptSchema.parse(receipt);
615
    const required = parsed.unilateral
616
      ? Object.keys(parsed.signatures).slice(0, 1)
617
      : parsed.participants;
618
    if (
619
      !required.length ||
620
      required.some((party) => {
621
        const agent = this.agents.get(party);
622
        return (
623
          !agent ||
624
          !verifyNamed(parsed as unknown as Record<string, unknown>, party, agent.publicKey)
625
        );
626
      })
627
    )
628
      throw new Error('Receipt signature verification failed');
629
    return parsed;
630
  }
631
632
  submitFeedback(item: Feedback): { revealed: boolean } {
633
    const parsed = FeedbackSchema.parse(item);
634
    const receipt = this.receipts.get(parsed.receiptId);
635
    const reviewer = this.agents.get(parsed.reviewerAgentId);
636
    if (
637
      !receipt ||
638
      !reviewer ||
639
      !receipt.participants.includes(parsed.reviewerAgentId) ||
640
      !receipt.participants.includes(parsed.subjectAgentId)
641
    )
642
      throw new Error('Feedback requires a valid participant receipt');
643
    if (!verifyObject(parsed as unknown as Record<string, unknown>, reviewer.publicKey))
644
      throw new Error('Invalid feedback signature');
645
    this.feedback.set(parsed.feedbackId, parsed);
646
    this.store.append('feedback', parsed.feedbackId, parsed);
647
    const related = [...this.feedback.values()].filter(
648
      (value) => value.receiptId === parsed.receiptId,
649
    );
650
    const revealed =
651
      receipt.unilateral ||
652
      new Set(related.map((value) => value.reviewerAgentId)).size >= receipt.participants.length;
653
    if (revealed) for (const feedback of related) this.applyFeedback(feedback, receipt);
654
    return { revealed };
655
  }
656
657
  getRisk(agentId: string, version: string, taskCategory: string): RiskDecision {
658
    const current = this.profiles.get(`${agentId}|${version}|${taskCategory}`);
659
    const otherVersions = [...this.profiles.values()].filter(
660
      (p) => p.agentId === agentId && p.taskCategory === taskCategory,
661
    );
662
    if (!current) {
663
      const continuity = otherVersions.length
664
        ? Math.min(
665
            0.2,
666
            Math.max(
667
              ...otherVersions.map((profile) => profile.sampleSize / (profile.sampleSize + 5)),
668
            ) * 0.25,
669
          )
670
        : 0;
671
      return this.decision(
672
        'CHALLENGE',
673
        taskCategory,
674
        continuity,
675
        ['limited_verified_history'],
676
        ['Agent version has limited task-specific evidence'],
677
        ['request_evidence'],
678
      );
679
    }
680
    const ageDays = Math.max(0, (Date.now() - Date.parse(current.updatedAt)) / 86_400_000);
681
    const freshness = Math.exp(-ageDays / 180);
682
    const quality =
683
      (current.completion +
684
        current.acceptance +
685
        current.specification +
686
        current.deadline +
687
        current.communication +
688
        current.evidence +
689
        current.scope +
690
        (1 - current.disputes)) /
691
      8;
692
    const confidence = Math.min(0.95, (current.sampleSize / (current.sampleSize + 5)) * freshness);
693
    const decision = quality >= 0.7 && confidence >= 0.25 ? 'ALLOW' : 'CHALLENGE';
694
    return {
695
      decision,
696
      confidence,
697
      taskCategory,
698
      sampleSize: current.sampleSize,
699
      dimensions: {
700
        completionReliability: current.completion,
701
        outputAcceptance: current.acceptance,
702
        contractAdherence: (current.specification + current.scope) / 2,
703
        deadlineReliability: current.deadline,
704
        communicationQuality: current.communication,
705
        evidenceQuality: current.evidence,
706
        disputeRate: current.disputes,
707
      },
708
      reasons: [`contextual_quality=${quality.toFixed(2)}`],
709
      warnings: confidence < 0.5 ? ['Limited sample size'] : [],
710
      dataFreshness: { behaviouralProfile: current.updatedAt },
711
      requiredChallenges: decision === 'CHALLENGE' ? ['request_evidence'] : [],
712
    };
713
  }
714
715
  setContributionConsent(agentId: string, enabled: boolean): void {
716
    this.requireAgent(agentId);
717
    const previous = this.contributionConsent.get(agentId);
718
    this.contributionConsent.set(
719
      agentId,
720
      enabled
721
        ? { enabled: true, grantedAt: new Date().toISOString() }
722
        : {
723
            enabled: false,
724
            grantedAt: previous?.grantedAt ?? new Date().toISOString(),
725
            revokedAt: new Date().toISOString(),
726
          },
727
    );
728
    this.store.append('consent', `${agentId}:${Date.now()}`, {
729
      agentId,
730
      ...this.contributionConsent.get(agentId),
731
    });
732
  }
733
734
  networkContribution(event: InteractionEvent): Record<string, unknown> | null {
735
    if (!this.contributionConsent.get(event.agentId)?.enabled || event.visibility === 'local_only')
736
      return null;
737
    return {
738
      eventId: event.eventId,
739
      interactionId: event.interactionId,
740
      eventType: event.eventType,
741
      agentId: event.agentId,
742
      agentVersion: event.agentVersion,
743
      timestamp: event.timestamp,
744
      visibility: event.visibility,
745
      provenance: event.provenance,
746
      payloadHash: event.payloadHash,
747
      evidenceRefs: event.evidenceRefs ?? [],
748
      signature: event.signature,
749
    };
750
  }
751
752
  private applyFeedback(feedback: Feedback, receipt: Receipt): void {
753
    const taskCategory = this.contracts.get(feedback.interactionId)?.taskCategory ?? 'unknown';
754
    const version = receipt.agentVersions[feedback.subjectAgentId] ?? 'unknown';
755
    const key = `${feedback.subjectAgentId}|${version}|${taskCategory}`;
756
    const previous = this.profiles.get(key);
757
    const n = previous?.sampleSize ?? 0;
758
    const next = (old: number | undefined, value: number) => ((old ?? 0) * n + value) / (n + 1);
759
    const profile = {
760
      agentId: feedback.subjectAgentId,
761
      agentVersion: version,
762
      taskCategory,
763
      sampleSize: n + 1,
764
      updatedAt: new Date().toISOString(),
765
      completion: next(previous?.completion, Number(feedback.taskCompleted)),
766
      acceptance: next(previous?.acceptance, Number(feedback.outputAccepted)),
767
      specification: next(previous?.specification, Number(feedback.specificationMatched)),
768
      deadline: next(previous?.deadline, Number(feedback.deadlineMet)),
769
      communication: next(previous?.communication, feedback.communicationQuality),
770
      evidence: next(previous?.evidence, Number(feedback.evidenceProvided)),
771
      scope: next(previous?.scope, Number(feedback.scopeRespected)),
772
      disputes: next(previous?.disputes, Number(feedback.disputeRaised)),
773
    };
774
    this.profiles.set(key, profile);
775
    this.store.append('profile', `${key}:${profile.sampleSize}`, profile);
776
  }
777
778
  private decision(
779
    decision: RiskDecision['decision'],
780
    taskCategory: string,
781
    confidence: number,
782
    reasons: string[],
783
    warnings: string[],
784
    challenges: string[],
785
  ): RiskDecision {
786
    return {
787
      decision,
788
      confidence,
789
      taskCategory,
790
      sampleSize: 0,
791
      dimensions: {},
792
      reasons,
793
      warnings,
794
      dataFreshness: {},
795
      requiredChallenges: challenges,
796
    };
797
  }
798
  private requireAgent(id: string): AgentIdentity {
799
    const value = this.agents.get(id);
800
    if (!value) throw new Error(`Agent not found: ${id}`);
801
    return value;
802
  }
803
  private requireConflict(id: string): Conflict {
804
    const value = this.conflicts.get(id);
805
    if (!value) throw new Error('Conflict not found');
806
    return value;
807
  }
808
}
809
810
export interface FactCheckProvider {
811
  check(claim: string, permission?: boolean): Promise<FactCheckResult>;
812
}
813
814
export class FixtureFactCheckProvider implements FactCheckProvider {
815
  constructor(
816
    private fixtures: Record<
817
      string,
818
      { status: FactCheckResult['status']; evidence: string[] }
819
    > = {},
820
  ) {}
821
  async check(claim: string, permission = true): Promise<FactCheckResult> {
822
    if (!permission)
823
      return result(
824
        claim,
825
        'objective',
826
        'insufficient_permission',
827
        1,
828
        [],
829
        'Grant evidence-source permission',
830
      );
831
    if (/\b(i think|i prefer|best|beautiful)\b/i.test(claim))
832
      return result(claim, 'subjective', 'not_fact_checkable', 1, [], 'Treat as an opinion');
833
    if (/\b(will|might|forecast|predict)\b/i.test(claim))
834
      return result(
835
        claim,
836
        'prediction',
837
        'not_fact_checkable',
838
        0.9,
839
        [],
840
        'Track as a prediction, not a fact',
841
      );
842
    const fixture = this.fixtures[claim];
843
    if (!fixture)
844
      return result(claim, 'objective', 'unverified', 0, [], 'Request authoritative evidence');
845
    return result(
846
      claim,
847
      'objective',
848
      fixture.status,
849
      0.95,
850
      fixture.evidence,
851
      fixture.status === 'contradicted' ? 'Challenge the claim with cited evidence' : 'Continue',
852
    );
853
  }
854
}
855
856
function result(
857
  claim: string,
858
  claimType: FactCheckResult['claimType'],
859
  status: FactCheckResult['status'],
860
  confidence: number,
861
  evidence: string[],
862
  action: string,
863
): FactCheckResult {
864
  return {
865
    claim,
866
    claimType,
867
    status,
868
    confidence,
869
    evidenceReferences: evidence,
870
    sourceAuthority: evidence.length ? 'authoritative_fixture' : 'none',
871
    sourceFreshness: new Date().toISOString(),
872
    contradictingEvidence: status === 'contradicted' ? evidence : [],
873
    suggestedNextAction: action,
874
  };
875
}
876
877
export { signNamed, signObject };