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

Viewing files at commit 2c749578ec22Commit detailsView latest

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