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

Viewing files at commit 922a6b480fb8Commit detailsView latest

packages/core/src/index.ts 1358 lines · 48.5 KB · typescript
1
import { randomUUID } from 'node:crypto';
2
import {
3
  AgentIdentitySchema,
4
  CounterpartyBriefSchema,
5
  ContextualReliabilitySummarySchema,
6
  canonicalHash,
7
  createKeyPair,
8
  DelegationCredentialSchema,
9
  FeedbackSchema,
10
  FeedbackDimensionSchema,
11
  InteractionContractSchema,
12
  InteractionConclusionSchema,
13
  InteractionEventSchema,
14
  LearningEligibilityDecisionSchema,
15
  ReceiptSchema,
16
  signNamed,
17
  signObject,
18
  TrustEnvelopeSchema,
19
  verifyNamed,
20
  verifyObject,
21
  type AgentIdentity,
22
  type CounterpartyBrief,
23
  type ContextualReliabilitySummary,
24
  type DelegationCredential,
25
  type FactCheckResult,
26
  type Feedback,
27
  type InteractionContract,
28
  type InteractionConclusion,
29
  type InteractionCompletionReport,
30
  type InteractionFeedback,
31
  type InteractionEvent,
32
  type KeyPair,
33
  type LiveSessionInsight,
34
  type LearningEligibilityDecision,
35
  type PublicAgentCard,
36
  type Receipt,
37
  type RiskDecision,
38
  type TrustEnvelope,
39
} from '../../protocol/src/index.js';
40
41
const REQUIREMENT_STOP_WORDS = new Set([
42
  'about',
43
  'agent',
44
  'available',
45
  'from',
46
  'have',
47
  'into',
48
  'must',
49
  'provide',
50
  'return',
51
  'should',
52
  'that',
53
  'their',
54
  'this',
55
  'with',
56
]);
57
58
function normalized(value: string): string {
59
  return value
60
    .trim()
61
    .toLowerCase()
62
    .replace(/[^a-z0-9]+/g, ' ')
63
    .trim();
64
}
65
66
function meaningfulTerms(value: string): string[] {
67
  return [
68
    ...new Set(
69
      normalized(value)
70
        .split(' ')
71
        .filter((term) => term.length >= 4 && !REQUIREMENT_STOP_WORDS.has(term)),
72
    ),
73
  ];
74
}
75
76
function assessDeclaration(
77
  requirement: string,
78
  declarations: string[],
79
): { status: 'match' | 'partial' | 'mismatch' | 'unknown'; reason: string; confidence: number } {
80
  const normalizedRequirement = normalized(requirement);
81
  const normalizedDeclarations = declarations.map(normalized);
82
  if (
83
    normalizedDeclarations.some(
84
      (declaration) => declaration === '*' || declaration === normalizedRequirement,
85
    )
86
  )
87
    return {
88
      status: 'match',
89
      reason: 'The counterparty explicitly declares this capability.',
90
      confidence: 0.95,
91
    };
92
93
  const terms = meaningfulTerms(requirement);
94
  const negative = normalizedDeclarations.find((declaration) =>
95
    terms.some(
96
      (term) =>
97
        declaration.includes(`no ${term}`) ||
98
        declaration.includes(`not ${term}`) ||
99
        declaration.includes(`cannot ${term}`),
100
    ),
101
  );
102
  if (negative)
103
    return {
104
      status: 'mismatch',
105
      reason: `The counterparty declares a conflicting limitation: ${negative}`,
106
      confidence: 0.95,
107
    };
108
109
  if (terms.length) {
110
    const declarationText = normalizedDeclarations.join(' ');
111
    const overlap = terms.filter((term) => declarationText.includes(term)).length / terms.length;
112
    if (overlap >= 0.6)
113
      return {
114
        status: 'match',
115
        reason: 'The counterparty declarations strongly match this requirement.',
116
        confidence: 0.8,
117
      };
118
    if (overlap > 0)
119
      return {
120
        status: 'partial',
121
        reason: 'The counterparty declarations only partially cover this requirement.',
122
        confidence: 0.65,
123
      };
124
  }
125
  return {
126
    status: 'unknown',
127
    reason: 'The published agent card does not establish this requirement.',
128
    confidence: 0.35,
129
  };
130
}
131
132
export function buildCounterpartyBrief(input: {
133
  interactionId: string;
134
  contractHash: string;
135
  contract: InteractionContract;
136
  recipientAgentId: string;
137
  subject: PublicAgentCard;
138
  historyInsights: LiveSessionInsight[];
139
  relevantSampleSize: number;
140
  historyConfidence: number;
141
  generatedAt?: string;
142
  expiresAt: string;
143
}): CounterpartyBrief {
144
  const declarations = [
145
    input.subject.description,
146
    ...input.subject.capabilities,
147
    ...input.subject.limitations,
148
  ];
149
  const requirements = [
150
    {
151
      requirementId: 'task-category',
152
      requirement: input.contract.taskCategory,
153
      kind: 'capability' as const,
154
    },
155
    {
156
      requirementId: 'requested-outcome',
157
      requirement: input.contract.requestedOutcome,
158
      kind: 'scope' as const,
159
    },
160
    ...input.contract.allowedActions.map((requirement, index) => ({
161
      requirementId: `allowed-action-${index + 1}`,
162
      requirement,
163
      kind: 'capability' as const,
164
    })),
165
    ...input.contract.successCriteria.map((requirement, index) => ({
166
      requirementId: `success-criterion-${index + 1}`,
167
      requirement,
168
      kind: 'success_criterion' as const,
169
    })),
170
    ...input.contract.evidenceRequirements.map((requirement, index) => ({
171
      requirementId: `evidence-${index + 1}`,
172
      requirement,
173
      kind: 'evidence' as const,
174
    })),
175
  ].map((requirement) => ({
176
    ...requirement,
177
    ...assessDeclaration(requirement.requirement, declarations),
178
    sources: ['contract', 'self_declared'] as const,
179
    evidenceReferences: [input.subject.cardUrl],
180
  }));
181
  const requirementReferences = requirements
182
    .filter((requirement) => requirement.status !== 'match')
183
    .map((requirement) => requirement.requirementId);
184
  const insights = input.historyInsights.map((insight) => ({
185
    ...insight,
186
    requirementReferences:
187
      insight.requirementReferences.length > 0
188
        ? insight.requirementReferences
189
        : requirementReferences,
190
  }));
191
  const needsChallenge =
192
    input.relevantSampleSize === 0 ||
193
    requirements.some((requirement) => requirement.status === 'mismatch') ||
194
    insights.some((insight) => insight.severity === 'high');
195
  return CounterpartyBriefSchema.parse({
196
    briefId: randomUUID(),
197
    interactionId: input.interactionId,
198
    contractHash: input.contractHash,
199
    recipientAgentId: input.recipientAgentId,
200
    subjectAgentId: input.subject.agentId,
201
    taskCategory: input.contract.taskCategory,
202
    decision: needsChallenge ? 'CHALLENGE' : 'ALLOW',
203
    requirements,
204
    insights,
205
    relevantSampleSize: input.relevantSampleSize,
206
    historyConfidence: input.historyConfidence,
207
    subjectAgentVersion: input.subject.agentVersion,
208
    recommendedContractChanges: requirements
209
      .filter((requirement) => requirement.status === 'mismatch')
210
      .map((requirement) => `Resolve mismatch: ${requirement.requirement}`),
211
    generatedAt: input.generatedAt ?? new Date().toISOString(),
212
    expiresAt: input.expiresAt,
213
  });
214
}
215
216
export function buildInteractionConclusion(input: {
217
  interaction: { interactionId: string; termsHash: string; contract: InteractionContract };
218
  reports: InteractionCompletionReport[];
219
  feedback: InteractionFeedback[];
220
  conclusionId?: string;
221
  generatedAt?: string;
222
  lifecycle?: 'provisional' | 'final';
223
  pendingFeedbackAgentIds?: string[];
224
  peerReportStatus?: InteractionConclusion['peerReportStatus'];
225
}): InteractionConclusion {
226
  const outcomes = input.reports.map((report) => report.outcome);
227
  const uniqueOutcomes = new Set(outcomes);
228
  const outcome =
229
    outcomes.length === 0
230
      ? ('cancelled' as const)
231
      : uniqueOutcomes.size === 1
232
        ? outcomes[0]!
233
        : outcomes.includes('failure')
234
          ? ('failure' as const)
235
          : ('partial' as const);
236
  const consensus =
237
    input.reports.length < 2
238
      ? ('unilateral' as const)
239
      : uniqueOutcomes.size === 1
240
        ? ('bilateral_agreement' as const)
241
        : outcomes.includes('partial')
242
          ? ('bilateral_partial_agreement' as const)
243
          : ('conflicting' as const);
244
  const statusRank = { met: 0, partially_met: 1, unknown: 2, missed: 3 } as const;
245
  const criteria = input.interaction.contract.successCriteria.map((criterion) => {
246
    const assessments = input.reports
247
      .flatMap((report) => report.criteria)
248
      .filter((assessment) => assessment.criterion === criterion);
249
    const selected = assessments.sort(
250
      (left, right) => statusRank[right.status] - statusRank[left.status],
251
    )[0];
252
    return {
253
      criterion,
254
      status: selected?.status ?? ('unknown' as const),
255
      ...(selected?.explanation ? { explanation: selected.explanation } : {}),
256
      evidenceReferences: [
257
        ...new Set(assessments.flatMap((assessment) => assessment.evidenceReferences)),
258
      ],
259
    };
260
  });
261
  const averageRatings = Object.fromEntries(
262
    FeedbackDimensionSchema.options.flatMap((dimension) => {
263
      const values = input.feedback
264
        .map((item) => item.ratings[dimension])
265
        .filter((value): value is number => typeof value === 'number');
266
      return values.length
267
        ? [[dimension, values.reduce((total, value) => total + value, 0) / values.length]]
268
        : [];
269
    }),
270
  );
271
  const reportedAgentIds = new Set(input.reports.map((report) => report.reportingAgentId));
272
  const missingReportAgentIds = input.interaction.contract.parties.filter(
273
    (agentId) => !reportedAgentIds.has(agentId),
274
  );
275
  const lifecycle = input.lifecycle ?? (missingReportAgentIds.length ? 'provisional' : 'final');
276
  const reportConfidence = input.reports.length
277
    ? input.reports.reduce((total, report) => total + report.confidence, 0) / input.reports.length
278
    : 0;
279
  const confidence =
280
    input.reports.length >= 2
281
      ? Math.min(0.95, reportConfidence * 0.9 + (uniqueOutcomes.size === 1 ? 0.05 : 0))
282
      : Math.min(0.55, reportConfidence * 0.6);
283
  const reportSummary = input.reports[0]?.summary;
284
  const summary =
285
    lifecycle === 'provisional'
286
      ? `Provisional one-sided outcome: ${reportSummary ?? 'one participant supplied a terminal report'}. The peer has not supplied an independent completion report.`
287
      : input.reports.length >= 2
288
        ? `${input.reports.length} independent completion reports were reconciled${input.feedback.length ? ` with ${input.feedback.length} eligible feedback response${input.feedback.length === 1 ? '' : 's'}` : ''}.`
289
        : `Final unilateral outcome: ${reportSummary ?? 'one participant supplied the available terminal report'}. The peer response window closed without an independent completion report.`;
290
  return InteractionConclusionSchema.parse({
291
    conclusionId: input.conclusionId ?? randomUUID(),
292
    interactionId: input.interaction.interactionId,
293
    contractHash: input.interaction.termsHash,
294
    outcome,
295
    consensus,
296
    lifecycle,
297
    confidence,
298
    missingReportAgentIds,
299
    pendingFeedbackAgentIds: input.pendingFeedbackAgentIds ?? [],
300
    peerReportStatus:
301
      input.reports.length >= 2 ? 'received' : (input.peerReportStatus ?? 'awaiting'),
302
    summary: summary.slice(0, 2000),
303
    criteria,
304
    reportIds: input.reports.map((report) => report.reportId),
305
    feedbackIds: input.feedback.map((item) => item.feedbackId),
306
    averageRatings,
307
    evidenceReferences: [...new Set(input.reports.flatMap((report) => report.evidenceReferences))],
308
    generatedAt: input.generatedAt ?? new Date().toISOString(),
309
  });
310
}
311
312
function extremeFeedbackPenalty(feedback: InteractionFeedback): number {
313
  const ratings = Object.values(feedback.ratings).filter(
314
    (value): value is number => typeof value === 'number',
315
  );
316
  if (!ratings.length) return 0.5;
317
  const extremeShare =
318
    ratings.filter((value) => value === 0 || value === 1).length / ratings.length;
319
  return extremeShare === 1 && feedback.evidenceReferences.length === 0 ? 0.6 : 1;
320
}
321
322
export function evaluateLearningEligibility(input: {
323
  interactionId: string;
324
  reports: InteractionCompletionReport[];
325
  feedback: InteractionFeedback[];
326
  consensus: InteractionConclusion['consensus'];
327
  contributionMode: 'local_only' | 'network_aggregate';
328
  reviewerCredibility?: Record<string, number>;
329
  manipulationSignals?: string[];
330
  decisionId?: string;
331
  decidedAt?: string;
332
}): LearningEligibilityDecision {
333
  const attestedReports = input.reports.filter((report) => report.platformAttestation);
334
  const attestedFeedback = input.feedback.filter((item) => item.platformAttestation);
335
  const bilateralReports =
336
    new Set(attestedReports.map((report) => report.reportingAgentId)).size >= 2;
337
  const evidenceReferences = [
338
    ...new Set([
339
      ...attestedReports.flatMap((report) => report.evidenceReferences),
340
      ...attestedFeedback.flatMap((item) => item.evidenceReferences),
341
    ]),
342
  ];
343
  const evidenceBacked = bilateralReports || evidenceReferences.length > 0;
344
  const eligible = attestedReports.length > 0;
345
  const extremeWithoutEvidence = attestedFeedback.filter(
346
    (item) => extremeFeedbackPenalty(item) < 1,
347
  );
348
  const mirroredFeedback =
349
    attestedFeedback.length === 2 &&
350
    canonicalHash(attestedFeedback[0]!.ratings) === canonicalHash(attestedFeedback[1]!.ratings) &&
351
    evidenceReferences.length === 0;
352
  const manipulationSignals = [
353
    ...new Set([
354
      ...(input.manipulationSignals ?? []),
355
      ...(extremeWithoutEvidence.length ? ['extreme_feedback_without_evidence'] : []),
356
      ...(mirroredFeedback ? ['reciprocal_rating_mirroring'] : []),
357
    ]),
358
  ];
359
  const contributionMode =
360
    input.contributionMode === 'network_aggregate' &&
361
    (!bilateralReports || manipulationSignals.length > 0)
362
      ? ('local_only' as const)
363
      : input.contributionMode;
364
  const reviewerWeight = attestedFeedback.length
365
    ? attestedFeedback.reduce((total, item) => {
366
        const credibility = Math.max(
367
          0.1,
368
          Math.min(1, input.reviewerCredibility?.[item.reviewerAgentId] ?? 0.5),
369
        );
370
        return total + item.confidence * credibility * extremeFeedbackPenalty(item);
371
      }, 0) / attestedFeedback.length
372
    : 0;
373
  const sampleWeight = eligible
374
    ? Math.max(
375
        0.1,
376
        Math.min(
377
          1,
378
          0.1 +
379
            (bilateralReports ? 0.35 : 0) +
380
            Math.min(0.2, reviewerWeight * 0.2) +
381
            (evidenceReferences.length ? 0.1 : 0) -
382
            (input.consensus === 'conflicting' ? 0.2 : 0) -
383
            Math.min(0.25, manipulationSignals.length * 0.12),
384
        ),
385
      )
386
    : 0;
387
  const reasons = [
388
    attestedReports.length
389
      ? `${attestedReports.length} platform-attested completion report(s)`
390
      : 'No attested completion report',
391
    bilateralReports
392
      ? 'Bilateral reports provide outcome corroboration'
393
      : 'Outcome is not bilaterally corroborated',
394
    attestedFeedback.length
395
      ? `${attestedFeedback.length} eligible feedback response(s)`
396
      : 'No eligible feedback response',
397
    evidenceReferences.length
398
      ? `${evidenceReferences.length} permitted evidence reference(s)`
399
      : 'No permitted external evidence reference',
400
    !bilateralReports && eligible
401
      ? 'One-sided structured evidence is retained locally at reduced weight'
402
      : evidenceBacked
403
        ? 'Evidence threshold supports the configured contribution mode'
404
        : 'Evidence is insufficient for shared-network contribution',
405
    input.consensus === 'conflicting'
406
      ? 'Conflicting reports reduce sample weight'
407
      : 'No report-conflict penalty',
408
    ...(manipulationSignals.length
409
      ? [`Manipulation checks reduced weight: ${manipulationSignals.join(', ')}`]
410
      : ['No feedback-manipulation signal detected']),
411
  ];
412
  return LearningEligibilityDecisionSchema.parse({
413
    decisionId: input.decisionId ?? randomUUID(),
414
    interactionId: input.interactionId,
415
    eligible,
416
    reasons,
417
    sampleWeight,
418
    reportIds: attestedReports.map((report) => report.reportId),
419
    feedbackIds: attestedFeedback.map((item) => item.feedbackId),
420
    evidenceReferences,
421
    manipulationSignals,
422
    contributionMode,
423
    structuredDataOnly: true,
424
    decidedAt: input.decidedAt ?? new Date().toISOString(),
425
  });
426
}
427
428
export type BehaviouralObservations = Partial<
429
  Record<
430
    | 'completion'
431
    | 'acceptance'
432
    | 'specification'
433
    | 'deadline'
434
    | 'communication'
435
    | 'evidence'
436
    | 'scope'
437
    | 'correction'
438
    | 'limitations'
439
    | 'disputes',
440
    number
441
  >
442
>;
443
444
export const BEHAVIOURAL_DIMENSIONS = [
445
  'completion',
446
  'acceptance',
447
  'specification',
448
  'deadline',
449
  'communication',
450
  'evidence',
451
  'scope',
452
  'correction',
453
  'limitations',
454
  'disputes',
455
] as const;
456
457
export type BehaviouralDimension = (typeof BEHAVIOURAL_DIMENSIONS)[number];
458
459
const RELIABILITY_WEIGHTS: Record<BehaviouralDimension, number> = {
460
  completion: 0.16,
461
  acceptance: 0.12,
462
  specification: 0.12,
463
  deadline: 0.1,
464
  communication: 0.08,
465
  evidence: 0.12,
466
  scope: 0.1,
467
  correction: 0.08,
468
  limitations: 0.08,
469
  disputes: 0.04,
470
};
471
472
const DIMENSION_RISK_LABELS: Record<BehaviouralDimension, string> = {
473
  completion: 'Outcomes are often incomplete.',
474
  acceptance: 'Delivered outcomes are not consistently accepted.',
475
  specification: 'Requirements are not consistently satisfied.',
476
  deadline: 'Delivery timing is unreliable.',
477
  communication: 'Communication quality is inconsistent.',
478
  evidence: 'Claims are not consistently supported by evidence.',
479
  scope: 'The agent has a pattern of leaving agreed scope.',
480
  correction: 'The agent does not consistently correct mistakes well.',
481
  limitations: 'The agent does not consistently disclose limitations early.',
482
  disputes: 'The interaction history contains elevated disagreement.',
483
};
484
485
export function summarizeContextualReliability(input: {
486
  profile: ContextualBehaviouralProfile & {
487
    agentId: string;
488
    agentVersion: string;
489
    taskCategory: string;
490
  };
491
  deltas?: Array<{ dimensionDeltas: Record<string, number>; appliedAt: string }>;
492
  currentAgentVersion?: string;
493
  now?: string;
494
}): ContextualReliabilitySummary {
495
  const currentAgentVersion = input.currentAgentVersion ?? input.profile.agentVersion;
496
  const versionChanged = currentAgentVersion !== input.profile.agentVersion;
497
  const ageDays = Math.max(
498
    0,
499
    (Date.parse(input.now ?? new Date().toISOString()) - Date.parse(input.profile.updatedAt)) /
500
      86_400_000,
501
  );
502
  const freshness = Math.exp(-ageDays / 180);
503
  const effectiveSampleSize = Math.max(
504
    0,
505
    input.profile.effectiveSampleSize ?? input.profile.sampleSize,
506
  );
507
  const confidenceValue = Math.min(
508
    0.98,
509
    (effectiveSampleSize / (effectiveSampleSize + 4)) * freshness * (versionChanged ? 0.35 : 1),
510
  );
511
  const confidenceLevel =
512
    confidenceValue >= 0.67 ? 'high' : confidenceValue >= 0.34 ? 'medium' : 'low';
513
  const legacyDimensions = new Set<BehaviouralDimension>([
514
    'completion',
515
    'acceptance',
516
    'specification',
517
    'deadline',
518
    'communication',
519
    'evidence',
520
    'scope',
521
    'disputes',
522
  ]);
523
  const measuredDimensions = new Set(
524
    BEHAVIOURAL_DIMENSIONS.filter((dimension) => {
525
      const explicit = input.profile.dimensionSampleSizes?.[dimension];
526
      return typeof explicit === 'number' ? explicit > 0 : legacyDimensions.has(dimension);
527
    }),
528
  );
529
  const dimensionValues = Object.fromEntries(
530
    BEHAVIOURAL_DIMENSIONS.map((dimension) => [
531
      dimension,
532
      dimension === 'disputes'
533
        ? 1 - Math.max(0, Math.min(1, Number(input.profile.disputes ?? 0.5)))
534
        : Math.max(0, Math.min(1, Number(input.profile[dimension] ?? 0.5))),
535
    ]),
536
  ) as Record<BehaviouralDimension, number>;
537
  const measuredWeight = BEHAVIOURAL_DIMENSIONS.reduce(
538
    (total, dimension) =>
539
      total + (measuredDimensions.has(dimension) ? RELIABILITY_WEIGHTS[dimension] : 0),
540
    0,
541
  );
542
  const score =
543
    BEHAVIOURAL_DIMENSIONS.reduce(
544
      (total, dimension) =>
545
        total +
546
        (measuredDimensions.has(dimension)
547
          ? dimensionValues[dimension] * RELIABILITY_WEIGHTS[dimension]
548
          : 0),
549
      0,
550
    ) / Math.max(Number.EPSILON, measuredWeight);
551
  const recentDeltas = [...(input.deltas ?? [])]
552
    .sort((left, right) => Date.parse(right.appliedAt) - Date.parse(left.appliedAt))
553
    .slice(0, 5);
554
  const trendDelta = recentDeltas.length
555
    ? recentDeltas.reduce((total, delta) => {
556
        const weighted = BEHAVIOURAL_DIMENSIONS.reduce((sum, dimension) => {
557
          const raw = Number(delta.dimensionDeltas[dimension] ?? 0);
558
          return sum + (dimension === 'disputes' ? -raw : raw) * RELIABILITY_WEIGHTS[dimension];
559
        }, 0);
560
        return total + weighted;
561
      }, 0) / recentDeltas.length
562
    : 0;
563
  const ordered = BEHAVIOURAL_DIMENSIONS.filter((dimension) =>
564
    measuredDimensions.has(dimension),
565
  ).map((dimension) => ({ dimension, score: dimensionValues[dimension] }));
566
  const strengths = ordered
567
    .filter((item) => item.score >= 0.65)
568
    .sort((left, right) => right.score - left.score)
569
    .slice(0, 3);
570
  const risks = ordered
571
    .filter((item) => item.score < 0.6)
572
    .sort((left, right) => left.score - right.score)
573
    .slice(0, 3)
574
    .map((item) => ({ ...item, reason: DIMENSION_RISK_LABELS[item.dimension] }));
575
  return ContextualReliabilitySummarySchema.parse({
576
    agentId: input.profile.agentId,
577
    agentVersion: input.profile.agentVersion,
578
    taskCategory: input.profile.taskCategory,
579
    score,
580
    confidence: {
581
      level: confidenceLevel,
582
      value: confidenceValue,
583
      evidenceCount: Math.max(0, Math.floor(input.profile.sampleSize)),
584
      effectiveSampleSize,
585
    },
586
    trend: {
587
      direction: trendDelta > 0.015 ? 'improving' : trendDelta < -0.015 ? 'declining' : 'stable',
588
      delta: Math.max(-1, Math.min(1, trendDelta)),
589
    },
590
    strengths,
591
    risks,
592
    versionStatus: {
593
      currentVersion: currentAgentVersion,
594
      evidenceVersion: input.profile.agentVersion,
595
      status: versionChanged ? 'reduced_confidence' : 'current',
596
    },
597
    source: 'private_verified_history',
598
    updatedAt: input.profile.updatedAt,
599
  });
600
}
601
602
export type ContextualBehaviouralProfile = Record<BehaviouralDimension, number> & {
603
  sampleSize: number;
604
  effectiveSampleSize: number;
605
  dimensionSampleSizes?: Partial<Record<BehaviouralDimension, number>>;
606
  updatedAt: string;
607
};
608
609
export function updateContextualBehaviouralProfile(input: {
610
  current?: Partial<ContextualBehaviouralProfile>;
611
  observations: BehaviouralObservations;
612
  sampleWeight: number;
613
  appliedAt?: string;
614
  decayDays?: number;
615
}): { profile: ContextualBehaviouralProfile; dimensionDeltas: Record<string, number> } {
616
  const appliedAt = input.appliedAt ?? new Date().toISOString();
617
  const decayDays = input.decayDays ?? 180;
618
  const priorAgeDays = input.current?.updatedAt
619
    ? Math.max(0, (Date.parse(appliedAt) - Date.parse(input.current.updatedAt)) / 86_400_000)
620
    : 0;
621
  const priorEffectiveSampleSize = Math.max(
622
    0,
623
    input.current?.effectiveSampleSize ?? input.current?.sampleSize ?? 0,
624
  );
625
  const decayedPriorWeight =
626
    priorEffectiveSampleSize * Math.exp(-priorAgeDays / Math.max(1, decayDays));
627
  const sampleWeight = Math.max(0, Math.min(1, input.sampleWeight));
628
  const hasObservation = BEHAVIOURAL_DIMENSIONS.some(
629
    (dimension) => typeof input.observations[dimension] === 'number',
630
  );
631
  const nextEffectiveSampleSize = decayedPriorWeight + (hasObservation ? sampleWeight : 0);
632
  const dimensionDeltas: Record<string, number> = {};
633
  const dimensions = Object.fromEntries(
634
    BEHAVIOURAL_DIMENSIONS.map((dimension) => {
635
      const previous = Math.max(0, Math.min(1, input.current?.[dimension] ?? 0.5));
636
      const observed = input.observations[dimension];
637
      if (typeof observed !== 'number' || sampleWeight === 0) return [dimension, previous];
638
      const next =
639
        (previous * decayedPriorWeight + Math.max(0, Math.min(1, observed)) * sampleWeight) /
640
        Math.max(Number.EPSILON, nextEffectiveSampleSize);
641
      dimensionDeltas[dimension] = next - previous;
642
      return [dimension, next];
643
    }),
644
  ) as Record<BehaviouralDimension, number>;
645
  const legacyDimensions = new Set<BehaviouralDimension>([
646
    'completion',
647
    'acceptance',
648
    'specification',
649
    'deadline',
650
    'communication',
651
    'evidence',
652
    'scope',
653
    'disputes',
654
  ]);
655
  const dimensionSampleSizes = Object.fromEntries(
656
    BEHAVIOURAL_DIMENSIONS.map((dimension) => {
657
      const prior =
658
        input.current?.dimensionSampleSizes?.[dimension] ??
659
        (legacyDimensions.has(dimension) ? priorEffectiveSampleSize : 0);
660
      const decayed = prior * Math.exp(-priorAgeDays / Math.max(1, decayDays));
661
      return [
662
        dimension,
663
        decayed + (typeof input.observations[dimension] === 'number' ? sampleWeight : 0),
664
      ];
665
    }),
666
  ) as Record<BehaviouralDimension, number>;
667
  return {
668
    profile: {
669
      ...dimensions,
670
      sampleSize:
671
        Math.max(0, Math.floor(input.current?.sampleSize ?? 0)) +
672
        (hasObservation && sampleWeight > 0 ? 1 : 0),
673
      effectiveSampleSize: nextEffectiveSampleSize,
674
      dimensionSampleSizes,
675
      updatedAt: appliedAt,
676
    },
677
    dimensionDeltas,
678
  };
679
}
680
681
export function deriveBehaviouralObservations(input: {
682
  subjectAgentId: string;
683
  reviewerAgentId?: string;
684
  reports: InteractionCompletionReport[];
685
  reviewerFeedback?: InteractionFeedback;
686
  conclusion: InteractionConclusion;
687
}): BehaviouralObservations {
688
  const subjectReport = input.reviewerAgentId
689
    ? input.reports.find(
690
        (report) =>
691
          report.reportingAgentId === input.reviewerAgentId &&
692
          report.counterpartyAgentId === input.subjectAgentId,
693
      )
694
    : (input.reports.find((report) => report.counterpartyAgentId === input.subjectAgentId) ??
695
      input.reports.find((report) => report.reportingAgentId === input.subjectAgentId));
696
  const criteria =
697
    subjectReport?.criteria.filter((criterion) => criterion.status !== 'unknown') ?? [];
698
  const criterionValue = (status: (typeof criteria)[number]['status']) =>
699
    status === 'met' ? 1 : status === 'partially_met' ? 0.5 : 0;
700
  const ratings = input.reviewerFeedback?.ratings;
701
  const observation: BehaviouralObservations = {
702
    ...(subjectReport
703
      ? {
704
          completion:
705
            subjectReport.outcome === 'success' ? 1 : subjectReport.outcome === 'partial' ? 0.5 : 0,
706
        }
707
      : {}),
708
    ...(criteria.length
709
      ? {
710
          specification:
711
            criteria.reduce((total, criterion) => total + criterionValue(criterion.status), 0) /
712
            criteria.length,
713
        }
714
      : {}),
715
    ...(typeof ratings?.outcome_satisfaction === 'number'
716
      ? { acceptance: ratings.outcome_satisfaction }
717
      : {}),
718
    ...(typeof ratings?.timeliness === 'number' ? { deadline: ratings.timeliness } : {}),
719
    ...(typeof ratings?.communication === 'number' ? { communication: ratings.communication } : {}),
720
    ...(typeof ratings?.evidence_quality === 'number'
721
      ? { evidence: ratings.evidence_quality }
722
      : {}),
723
    ...(typeof ratings?.scope_adherence === 'number' ? { scope: ratings.scope_adherence } : {}),
724
    ...(typeof ratings?.correction_handling === 'number'
725
      ? { correction: ratings.correction_handling }
726
      : {}),
727
    ...(typeof ratings?.limitation_disclosure === 'number'
728
      ? { limitations: ratings.limitation_disclosure }
729
      : {}),
730
    ...(subjectReport || input.reviewerFeedback
731
      ? { disputes: input.conclusion.consensus === 'conflicting' ? 1 : 0 }
732
      : {}),
733
  };
734
  return Object.fromEntries(
735
    Object.entries(observation).map(([key, value]) => [key, Math.max(0, Math.min(1, value))]),
736
  );
737
}
738
739
export interface AuditStore {
740
  append(kind: string, id: string, value: unknown): void;
741
  list(kind: string): unknown[];
742
}
743
744
export class MemoryAuditStore implements AuditStore {
745
  private rows: { kind: string; id: string; value: unknown }[] = [];
746
  append(kind: string, id: string, value: unknown): void {
747
    const old = this.rows.find((row) => row.kind === kind && row.id === id);
748
    if (old && canonicalHash(old.value) !== canonicalHash(value))
749
      throw new Error(`Conflicting duplicate ${kind}:${id}`);
750
    if (!old) this.rows.push({ kind, id, value: structuredClone(value) });
751
  }
752
  list(kind: string): unknown[] {
753
    return this.rows.filter((row) => row.kind === kind).map((row) => structuredClone(row.value));
754
  }
755
}
756
757
export function createIdentity(input: {
758
  agentId: string;
759
  operatorRef: string;
760
  version?: string;
761
  capabilities: string[];
762
  assurance?: AgentIdentity['assurance'];
763
  parentAgentId?: string;
764
  rootControllerId?: string;
765
}): { identity: AgentIdentity; keyPair: KeyPair } {
766
  const keyPair = createKeyPair(`${input.agentId}#1`);
767
  const identity: Record<string, unknown> = {
768
    protocolVersion: '0.1',
769
    agentId: input.agentId,
770
    publicKey: keyPair.publicKey,
771
    keyId: keyPair.keyId,
772
    operatorRef: input.operatorRef,
773
    assurance: input.assurance ?? 'pseudonymous',
774
    agentVersion: input.version ?? '1.0.0',
775
    capabilities: input.capabilities,
776
    supportedProtocols: ['A2A/1.0', 'OpenClasp/0.1'],
777
    ...(input.parentAgentId ? { parentAgentId: input.parentAgentId } : {}),
778
    rootControllerId: input.rootControllerId ?? input.agentId,
779
    revoked: false,
780
    provenance: 'cryptographically_verified',
781
    createdAt: new Date().toISOString(),
782
  };
783
  return { identity: AgentIdentitySchema.parse(signObject(identity, keyPair)), keyPair };
784
}
785
786
export type PolicyContext = {
787
  envelope: TrustEnvelope;
788
  action: string;
789
  dataClasses?: string[];
790
  humanApproved?: boolean;
791
};
792
793
export type Conflict = {
794
  conflictId: string;
795
  interactionId: string;
796
  issue: string;
797
  positions: Record<string, string>;
798
  evidence: string[];
799
  contractClauses: string[];
800
  missingInformation: string[];
801
  possibleResolutions: string[];
802
  permissions: Record<string, boolean>;
803
  status: 'pending_consent' | 'open' | 'resolved';
804
  resolution?: string;
805
};
806
807
type ProfileAggregate = {
808
  agentId: string;
809
  agentVersion: string;
810
  taskCategory: string;
811
  sampleSize: number;
812
  updatedAt: string;
813
  completion: number;
814
  acceptance: number;
815
  specification: number;
816
  deadline: number;
817
  communication: number;
818
  evidence: number;
819
  scope: number;
820
  disputes: number;
821
};
822
823
export class TrustEngine {
824
  readonly agents = new Map<string, AgentIdentity>();
825
  readonly delegations = new Map<string, DelegationCredential>();
826
  readonly contracts = new Map<string, InteractionContract>();
827
  readonly events = new Map<string, InteractionEvent>();
828
  readonly receipts = new Map<string, Receipt>();
829
  readonly conflicts = new Map<string, Conflict>();
830
  readonly feedback = new Map<string, Feedback>();
831
  readonly profiles = new Map<string, ProfileAggregate>();
832
  readonly contributionConsent = new Map<
833
    string,
834
    { enabled: boolean; grantedAt: string; revokedAt?: string }
835
  >();
836
  private readonly nonces = new Set<string>();
837
838
  constructor(readonly store: AuditStore = new MemoryAuditStore()) {
839
    for (const value of store.list('agent') as AgentIdentity[])
840
      this.agents.set(value.agentId, value);
841
    for (const value of store.list('delegation') as DelegationCredential[])
842
      this.delegations.set(value.delegationId, value);
843
    for (const value of store.list('contract') as InteractionContract[])
844
      this.contracts.set(value.interactionId, value);
845
    for (const value of store.list('event') as InteractionEvent[])
846
      this.events.set(value.eventId, value);
847
    for (const value of store.list('receipt') as Receipt[])
848
      this.receipts.set(value.receiptId, value);
849
    for (const value of store.list('feedback') as Feedback[])
850
      this.feedback.set(value.feedbackId, value);
851
    for (const value of store.list('profile') as ProfileAggregate[])
852
      this.profiles.set(`${value.agentId}|${value.agentVersion}|${value.taskCategory}`, value);
853
    for (const value of store.list('conflict') as Conflict[])
854
      this.conflicts.set(value.conflictId, value);
855
    for (const value of store.list('consent') as {
856
      agentId: string;
857
      enabled: boolean;
858
      grantedAt: string;
859
      revokedAt?: string;
860
    }[])
861
      this.contributionConsent.set(value.agentId, value);
862
    for (const value of store.list('revocation') as {
863
      agentId?: string;
864
      delegationId?: string;
865
    }[]) {
866
      if (value.agentId && this.agents.has(value.agentId))
867
        this.agents.set(value.agentId, { ...this.agents.get(value.agentId)!, revoked: true });
868
      if (value.delegationId && this.delegations.has(value.delegationId))
869
        this.delegations.set(value.delegationId, {
870
          ...this.delegations.get(value.delegationId)!,
871
          revoked: true,
872
        });
873
    }
874
  }
875
876
  registerAgent(identity: AgentIdentity): AgentIdentity {
877
    const parsed = AgentIdentitySchema.parse(identity);
878
    if (!verifyObject(parsed as unknown as Record<string, unknown>, parsed.publicKey))
879
      throw new Error('Invalid identity signature');
880
    if (parsed.parentAgentId && parsed.rootControllerId === parsed.agentId)
881
      throw new Error('Child cannot remove root-controller reference');
882
    this.agents.set(parsed.agentId, parsed);
883
    this.store.append('agent', parsed.agentId, parsed);
884
    return parsed;
885
  }
886
887
  revokeAgent(agentId: string): void {
888
    const identity = this.requireAgent(agentId);
889
    const revoked = { ...identity, revoked: true };
890
    this.agents.set(agentId, revoked);
891
    this.store.append('revocation', `agent:${agentId}`, { agentId, at: new Date().toISOString() });
892
  }
893
894
  createDelegation(
895
    parentId: string,
896
    childId: string,
897
    capabilities: string[],
898
    expiresAt: string,
899
    parentKey: KeyPair,
900
  ): DelegationCredential {
901
    const parent = this.requireAgent(parentId);
902
    const child = this.requireAgent(childId);
903
    if (parent.revoked) throw new Error('Parent agent is revoked');
904
    if (child.parentAgentId !== parentId || child.rootControllerId !== parent.rootControllerId)
905
      throw new Error('Invalid child lineage');
906
    if (capabilities.some((capability) => !parent.capabilities.includes(capability)))
907
      throw new Error('Delegation exceeds parent authority');
908
    const raw = {
909
      protocolVersion: '0.1' as const,
910
      delegationId: randomUUID(),
911
      parentAgentId: parentId,
912
      childAgentId: childId,
913
      rootControllerId: parent.rootControllerId,
914
      capabilities,
915
      issuedAt: new Date().toISOString(),
916
      expiresAt,
917
      revoked: false,
918
    };
919
    const credential = DelegationCredentialSchema.parse(signObject(raw, parentKey));
920
    this.delegations.set(credential.delegationId, credential);
921
    this.store.append('delegation', credential.delegationId, credential);
922
    return credential;
923
  }
924
925
  verifyDelegation(delegationId: string, capability?: string, at = new Date()): boolean {
926
    const delegation = this.delegations.get(delegationId);
927
    if (!delegation || delegation.revoked || new Date(delegation.expiresAt) <= at) return false;
928
    const parent = this.agents.get(delegation.parentAgentId);
929
    const child = this.agents.get(delegation.childAgentId);
930
    if (!parent || !child || parent.revoked || child.revoked) return false;
931
    if (
932
      child.rootControllerId !== delegation.rootControllerId ||
933
      child.parentAgentId !== parent.agentId
934
    )
935
      return false;
936
    if (capability && !delegation.capabilities.includes(capability)) return false;
937
    if (delegation.capabilities.some((item) => !parent.capabilities.includes(item))) return false;
938
    return verifyObject(delegation as unknown as Record<string, unknown>, parent.publicKey);
939
  }
940
941
  revokeDelegation(delegationId: string): void {
942
    const item = this.delegations.get(delegationId);
943
    if (!item) throw new Error('Delegation not found');
944
    this.delegations.set(delegationId, { ...item, revoked: true });
945
    this.store.append('revocation', `delegation:${delegationId}`, {
946
      delegationId,
947
      at: new Date().toISOString(),
948
    });
949
  }
950
951
  saveContract(contract: InteractionContract): InteractionContract {
952
    const parsed = InteractionContractSchema.parse(contract);
953
    if (parsed.parties.some((party) => !(party in parsed.signatures)))
954
      throw new Error('Contract requires every party signature');
955
    for (const party of Object.keys(parsed.signatures)) {
956
      const agent = this.requireAgent(party);
957
      if (!verifyNamed(parsed as unknown as Record<string, unknown>, party, agent.publicKey))
958
        throw new Error(`Invalid contract signature: ${party}`);
959
    }
960
    this.contracts.set(parsed.interactionId, parsed);
961
    this.store.append('contract', parsed.interactionId, parsed);
962
    return parsed;
963
  }
964
965
  assess(context: PolicyContext): RiskDecision {
966
    const envelope = TrustEnvelopeSchema.parse(context.envelope);
967
    const responder = this.agents.get(envelope.respondingAgentId);
968
    const requester = this.agents.get(envelope.requestingAgentId);
969
    const contract = this.contracts.get(envelope.interactionId);
970
    const hardFailures: string[] = [];
971
    if (
972
      !requester ||
973
      requester.revoked ||
974
      !verifyObject(envelope as unknown as Record<string, unknown>, requester.publicKey)
975
    )
976
      hardFailures.push('invalid_requester_signature');
977
    if (!responder || responder.revoked) hardFailures.push('invalid_or_revoked_responder');
978
    if (new Date(envelope.expiresAt) <= new Date()) hardFailures.push('expired_envelope');
979
    if (this.nonces.has(envelope.nonce)) hardFailures.push('replay_attempt');
980
    if (
981
      envelope.delegationId &&
982
      !this.verifyDelegation(envelope.delegationId, envelope.requestedCapability)
983
    )
984
      hardFailures.push('invalid_delegation');
985
    if (!contract || canonicalHash({ ...contract, signatures: {} }) !== envelope.contractHash)
986
      hardFailures.push('contract_mismatch');
987
    if (
988
      contract &&
989
      (!contract.allowedActions.includes(context.action) ||
990
        contract.prohibitedActions.includes(context.action))
991
    )
992
      hardFailures.push('action_outside_contract');
993
    if (
994
      contract &&
995
      (context.dataClasses ?? []).some((item) => contract.prohibitedData.includes(item))
996
    )
997
      hardFailures.push('prohibited_data');
998
    if (
999
      contract &&
1000
      contract.humanApprovalRequirements.includes(context.action) &&
1001
      !context.humanApproved
1002
    )
1003
      hardFailures.push('missing_human_approval');
1004
    if (!hardFailures.includes('replay_attempt')) this.nonces.add(envelope.nonce);
1005
    if (hardFailures.length)
1006
      return this.decision('DENY', envelope.taskCategory, 1, hardFailures, [], []);
1007
1008
    const risk = this.getRisk(
1009
      envelope.respondingAgentId,
1010
      responder?.agentVersion ?? envelope.agentVersion,
1011
      envelope.taskCategory,
1012
    );
1013
    return risk;
1014
  }
1015
1016
  recordEvent(event: InteractionEvent): InteractionEvent {
1017
    const parsed = InteractionEventSchema.parse(event);
1018
    const agent = this.requireAgent(parsed.agentId);
1019
    if (!verifyObject(parsed as unknown as Record<string, unknown>, agent.publicKey))
1020
      throw new Error('Invalid event signature');
1021
    if (canonicalHash(parsed.payload) !== parsed.payloadHash)
1022
      throw new Error('Event payload hash mismatch');
1023
    const existing = this.events.get(parsed.eventId);
1024
    if (existing && canonicalHash(existing) !== canonicalHash(parsed))
1025
      throw new Error('Conflicting duplicate event');
1026
    this.events.set(parsed.eventId, parsed);
1027
    this.store.append('event', parsed.eventId, parsed);
1028
    return parsed;
1029
  }
1030
1031
  createConflict(
1032
    input: Omit<Conflict, 'conflictId' | 'status' | 'permissions'> & { participants: string[] },
1033
  ): Conflict {
1034
    const permissions = Object.fromEntries(input.participants.map((id) => [id, false]));
1035
    const conflict: Conflict = {
1036
      ...input,
1037
      conflictId: randomUUID(),
1038
      permissions,
1039
      status: 'pending_consent',
1040
    };
1041
    delete (conflict as any).participants;
1042
    this.conflicts.set(conflict.conflictId, conflict);
1043
    this.store.append('conflict', `${conflict.conflictId}:created`, conflict);
1044
    return conflict;
1045
  }
1046
1047
  permitMediation(conflictId: string, agentId: string): Conflict {
1048
    const conflict = this.requireConflict(conflictId);
1049
    if (!(agentId in conflict.permissions)) throw new Error('Agent is not a conflict participant');
1050
    conflict.permissions[agentId] = true;
1051
    if (Object.values(conflict.permissions).every(Boolean)) conflict.status = 'open';
1052
    this.store.append('conflict', `${conflict.conflictId}:permit:${agentId}`, conflict);
1053
    return conflict;
1054
  }
1055
1056
  resolveConflict(conflictId: string, resolution: string): Conflict {
1057
    const conflict = this.requireConflict(conflictId);
1058
    if (conflict.status !== 'open') throw new Error('Mutual mediation consent required');
1059
    conflict.status = 'resolved';
1060
    conflict.resolution = resolution;
1061
    this.store.append('conflict', `${conflict.conflictId}:resolved`, conflict);
1062
    return conflict;
1063
  }
1064
1065
  submitReceipt(receipt: Receipt): Receipt {
1066
    const parsed = this.verifyReceipt(receipt);
1067
    this.receipts.set(parsed.receiptId, parsed);
1068
    this.store.append('receipt', parsed.receiptId, parsed);
1069
    return parsed;
1070
  }
1071
1072
  verifyReceipt(receipt: Receipt): Receipt {
1073
    const parsed = ReceiptSchema.parse(receipt);
1074
    const required = parsed.unilateral
1075
      ? Object.keys(parsed.signatures).slice(0, 1)
1076
      : parsed.participants;
1077
    if (
1078
      !required.length ||
1079
      required.some((party) => {
1080
        const agent = this.agents.get(party);
1081
        return (
1082
          !agent ||
1083
          !verifyNamed(parsed as unknown as Record<string, unknown>, party, agent.publicKey)
1084
        );
1085
      })
1086
    )
1087
      throw new Error('Receipt signature verification failed');
1088
    return parsed;
1089
  }
1090
1091
  submitFeedback(item: Feedback): { revealed: boolean } {
1092
    const parsed = FeedbackSchema.parse(item);
1093
    const receipt = this.receipts.get(parsed.receiptId);
1094
    const reviewer = this.agents.get(parsed.reviewerAgentId);
1095
    if (
1096
      !receipt ||
1097
      !reviewer ||
1098
      !receipt.participants.includes(parsed.reviewerAgentId) ||
1099
      !receipt.participants.includes(parsed.subjectAgentId)
1100
    )
1101
      throw new Error('Feedback requires a valid participant receipt');
1102
    if (!verifyObject(parsed as unknown as Record<string, unknown>, reviewer.publicKey))
1103
      throw new Error('Invalid feedback signature');
1104
    this.feedback.set(parsed.feedbackId, parsed);
1105
    this.store.append('feedback', parsed.feedbackId, parsed);
1106
    const related = [...this.feedback.values()].filter(
1107
      (value) => value.receiptId === parsed.receiptId,
1108
    );
1109
    const revealed =
1110
      receipt.unilateral ||
1111
      new Set(related.map((value) => value.reviewerAgentId)).size >= receipt.participants.length;
1112
    if (revealed) for (const feedback of related) this.applyFeedback(feedback, receipt);
1113
    return { revealed };
1114
  }
1115
1116
  getRisk(agentId: string, version: string, taskCategory: string): RiskDecision {
1117
    const current = this.profiles.get(`${agentId}|${version}|${taskCategory}`);
1118
    const otherVersions = [...this.profiles.values()].filter(
1119
      (p) => p.agentId === agentId && p.taskCategory === taskCategory,
1120
    );
1121
    if (!current) {
1122
      const continuity = otherVersions.length
1123
        ? Math.min(
1124
            0.2,
1125
            Math.max(
1126
              ...otherVersions.map((profile) => profile.sampleSize / (profile.sampleSize + 5)),
1127
            ) * 0.25,
1128
          )
1129
        : 0;
1130
      return this.decision(
1131
        'CHALLENGE',
1132
        taskCategory,
1133
        continuity,
1134
        ['limited_verified_history'],
1135
        ['Agent version has limited task-specific evidence'],
1136
        ['request_evidence'],
1137
      );
1138
    }
1139
    const ageDays = Math.max(0, (Date.now() - Date.parse(current.updatedAt)) / 86_400_000);
1140
    const freshness = Math.exp(-ageDays / 180);
1141
    const quality =
1142
      (current.completion +
1143
        current.acceptance +
1144
        current.specification +
1145
        current.deadline +
1146
        current.communication +
1147
        current.evidence +
1148
        current.scope +
1149
        (1 - current.disputes)) /
1150
      8;
1151
    const confidence = Math.min(0.95, (current.sampleSize / (current.sampleSize + 5)) * freshness);
1152
    const decision = quality >= 0.7 && confidence >= 0.25 ? 'ALLOW' : 'CHALLENGE';
1153
    return {
1154
      decision,
1155
      confidence,
1156
      taskCategory,
1157
      sampleSize: current.sampleSize,
1158
      dimensions: {
1159
        completionReliability: current.completion,
1160
        outputAcceptance: current.acceptance,
1161
        contractAdherence: (current.specification + current.scope) / 2,
1162
        deadlineReliability: current.deadline,
1163
        communicationQuality: current.communication,
1164
        evidenceQuality: current.evidence,
1165
        disputeRate: current.disputes,
1166
      },
1167
      reasons: [`contextual_quality=${quality.toFixed(2)}`],
1168
      warnings: confidence < 0.5 ? ['Limited sample size'] : [],
1169
      dataFreshness: { behaviouralProfile: current.updatedAt },
1170
      requiredChallenges: decision === 'CHALLENGE' ? ['request_evidence'] : [],
1171
    };
1172
  }
1173
1174
  setContributionConsent(agentId: string, enabled: boolean): void {
1175
    this.requireAgent(agentId);
1176
    const previous = this.contributionConsent.get(agentId);
1177
    this.contributionConsent.set(
1178
      agentId,
1179
      enabled
1180
        ? { enabled: true, grantedAt: new Date().toISOString() }
1181
        : {
1182
            enabled: false,
1183
            grantedAt: previous?.grantedAt ?? new Date().toISOString(),
1184
            revokedAt: new Date().toISOString(),
1185
          },
1186
    );
1187
    this.store.append('consent', `${agentId}:${Date.now()}`, {
1188
      agentId,
1189
      ...this.contributionConsent.get(agentId),
1190
    });
1191
  }
1192
1193
  networkContribution(event: InteractionEvent): Record<string, unknown> | null {
1194
    if (!this.contributionConsent.get(event.agentId)?.enabled || event.visibility === 'local_only')
1195
      return null;
1196
    return {
1197
      eventId: event.eventId,
1198
      interactionId: event.interactionId,
1199
      eventType: event.eventType,
1200
      agentId: event.agentId,
1201
      agentVersion: event.agentVersion,
1202
      timestamp: event.timestamp,
1203
      visibility: event.visibility,
1204
      provenance: event.provenance,
1205
      payloadHash: event.payloadHash,
1206
      evidenceRefs: event.evidenceRefs ?? [],
1207
      signature: event.signature,
1208
    };
1209
  }
1210
1211
  private applyFeedback(feedback: Feedback, receipt: Receipt): void {
1212
    const taskCategory = this.contracts.get(feedback.interactionId)?.taskCategory ?? 'unknown';
1213
    const version = receipt.agentVersions[feedback.subjectAgentId] ?? 'unknown';
1214
    const key = `${feedback.subjectAgentId}|${version}|${taskCategory}`;
1215
    const previous = this.profiles.get(key);
1216
    const n = previous?.sampleSize ?? 0;
1217
    const next = (old: number | undefined, value: number) => ((old ?? 0) * n + value) / (n + 1);
1218
    const profile = {
1219
      agentId: feedback.subjectAgentId,
1220
      agentVersion: version,
1221
      taskCategory,
1222
      sampleSize: n + 1,
1223
      updatedAt: new Date().toISOString(),
1224
      completion: next(previous?.completion, Number(feedback.taskCompleted)),
1225
      acceptance: next(previous?.acceptance, Number(feedback.outputAccepted)),
1226
      specification: next(previous?.specification, Number(feedback.specificationMatched)),
1227
      deadline: next(previous?.deadline, Number(feedback.deadlineMet)),
1228
      communication: next(previous?.communication, feedback.communicationQuality),
1229
      evidence: next(previous?.evidence, Number(feedback.evidenceProvided)),
1230
      scope: next(previous?.scope, Number(feedback.scopeRespected)),
1231
      disputes: next(previous?.disputes, Number(feedback.disputeRaised)),
1232
    };
1233
    this.profiles.set(key, profile);
1234
    this.store.append('profile', `${key}:${profile.sampleSize}`, profile);
1235
  }
1236
1237
  private decision(
1238
    decision: RiskDecision['decision'],
1239
    taskCategory: string,
1240
    confidence: number,
1241
    reasons: string[],
1242
    warnings: string[],
1243
    challenges: string[],
1244
  ): RiskDecision {
1245
    return {
1246
      decision,
1247
      confidence,
1248
      taskCategory,
1249
      sampleSize: 0,
1250
      dimensions: {},
1251
      reasons,
1252
      warnings,
1253
      dataFreshness: {},
1254
      requiredChallenges: challenges,
1255
    };
1256
  }
1257
  private requireAgent(id: string): AgentIdentity {
1258
    const value = this.agents.get(id);
1259
    if (!value) throw new Error(`Agent not found: ${id}`);
1260
    return value;
1261
  }
1262
  private requireConflict(id: string): Conflict {
1263
    const value = this.conflicts.get(id);
1264
    if (!value) throw new Error('Conflict not found');
1265
    return value;
1266
  }
1267
}
1268
1269
export interface FactCheckProvider {
1270
  check(claim: string, permission?: boolean): Promise<FactCheckResult>;
1271
}
1272
1273
export class UnavailableFactCheckProvider implements FactCheckProvider {
1274
  async check(claim: string, permission = true): Promise<FactCheckResult> {
1275
    if (!permission)
1276
      return result(
1277
        claim,
1278
        'objective',
1279
        'insufficient_permission',
1280
        1,
1281
        [],
1282
        'Grant evidence-source permission',
1283
      );
1284
    return result(
1285
      claim,
1286
      'objective',
1287
      'unverified',
1288
      0,
1289
      [],
1290
      'No production evidence provider is configured; request authoritative evidence',
1291
    );
1292
  }
1293
}
1294
1295
export class FixtureFactCheckProvider implements FactCheckProvider {
1296
  constructor(
1297
    private fixtures: Record<
1298
      string,
1299
      { status: FactCheckResult['status']; evidence: string[] }
1300
    > = {},
1301
  ) {}
1302
  async check(claim: string, permission = true): Promise<FactCheckResult> {
1303
    if (!permission)
1304
      return result(
1305
        claim,
1306
        'objective',
1307
        'insufficient_permission',
1308
        1,
1309
        [],
1310
        'Grant evidence-source permission',
1311
      );
1312
    if (/\b(i think|i prefer|best|beautiful)\b/i.test(claim))
1313
      return result(claim, 'subjective', 'not_fact_checkable', 1, [], 'Treat as an opinion');
1314
    if (/\b(will|might|forecast|predict)\b/i.test(claim))
1315
      return result(
1316
        claim,
1317
        'prediction',
1318
        'not_fact_checkable',
1319
        0.9,
1320
        [],
1321
        'Track as a prediction, not a fact',
1322
      );
1323
    const fixture = this.fixtures[claim];
1324
    if (!fixture)
1325
      return result(claim, 'objective', 'unverified', 0, [], 'Request authoritative evidence');
1326
    return result(
1327
      claim,
1328
      'objective',
1329
      fixture.status,
1330
      0.95,
1331
      fixture.evidence,
1332
      fixture.status === 'contradicted' ? 'Challenge the claim with cited evidence' : 'Continue',
1333
    );
1334
  }
1335
}
1336
1337
function result(
1338
  claim: string,
1339
  claimType: FactCheckResult['claimType'],
1340
  status: FactCheckResult['status'],
1341
  confidence: number,
1342
  evidence: string[],
1343
  action: string,
1344
): FactCheckResult {
1345
  return {
1346
    claim,
1347
    claimType,
1348
    status,
1349
    confidence,
1350
    evidenceReferences: evidence,
1351
    sourceAuthority: evidence.length ? 'authoritative_fixture' : 'none',
1352
    sourceFreshness: new Date().toISOString(),
1353
    contradictingEvidence: status === 'contradicted' ? evidence : [],
1354
    suggestedNextAction: action,
1355
  };
1356
}
1357
1358
export { signNamed, signObject };