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

Viewing files at commit 5b7999fcbb0cCommit detailsView latest

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