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

Viewing files at commit d24aacea3d6aCommit detailsView latest

packages/core/src/index.ts 1091 lines · 38.5 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
}): InteractionConclusion {
221
  const outcomes = input.reports.map((report) => report.outcome);
222
  const uniqueOutcomes = new Set(outcomes);
223
  const outcome =
224
    outcomes.length === 0
225
      ? ('cancelled' as const)
226
      : uniqueOutcomes.size === 1
227
        ? outcomes[0]!
228
        : outcomes.includes('failure')
229
          ? ('failure' as const)
230
          : ('partial' as const);
231
  const consensus =
232
    input.reports.length < 2
233
      ? ('unilateral' as const)
234
      : uniqueOutcomes.size === 1
235
        ? ('bilateral_agreement' as const)
236
        : outcomes.includes('partial')
237
          ? ('bilateral_partial_agreement' as const)
238
          : ('conflicting' as const);
239
  const statusRank = { met: 0, partially_met: 1, unknown: 2, missed: 3 } as const;
240
  const criteria = input.interaction.contract.successCriteria.map((criterion) => {
241
    const assessments = input.reports
242
      .flatMap((report) => report.criteria)
243
      .filter((assessment) => assessment.criterion === criterion);
244
    const selected = assessments.sort(
245
      (left, right) => statusRank[right.status] - statusRank[left.status],
246
    )[0];
247
    return {
248
      criterion,
249
      status: selected?.status ?? ('unknown' as const),
250
      ...(selected?.explanation ? { explanation: selected.explanation } : {}),
251
      evidenceReferences: [
252
        ...new Set(assessments.flatMap((assessment) => assessment.evidenceReferences)),
253
      ],
254
    };
255
  });
256
  const averageRatings = Object.fromEntries(
257
    FeedbackDimensionSchema.options.flatMap((dimension) => {
258
      const values = input.feedback
259
        .map((item) => item.ratings[dimension])
260
        .filter((value): value is number => typeof value === 'number');
261
      return values.length
262
        ? [[dimension, values.reduce((total, value) => total + value, 0) / values.length]]
263
        : [];
264
    }),
265
  );
266
  return InteractionConclusionSchema.parse({
267
    conclusionId: input.conclusionId ?? randomUUID(),
268
    interactionId: input.interaction.interactionId,
269
    contractHash: input.interaction.termsHash,
270
    outcome,
271
    consensus,
272
    summary: `${input.reports.length} completion report${input.reports.length === 1 ? '' : 's'} and ${input.feedback.length} feedback response${input.feedback.length === 1 ? '' : 's'} were eligible for this conclusion.`,
273
    criteria,
274
    reportIds: input.reports.map((report) => report.reportId),
275
    feedbackIds: input.feedback.map((item) => item.feedbackId),
276
    averageRatings,
277
    evidenceReferences: [...new Set(input.reports.flatMap((report) => report.evidenceReferences))],
278
    generatedAt: input.generatedAt ?? new Date().toISOString(),
279
  });
280
}
281
282
function extremeFeedbackPenalty(feedback: InteractionFeedback): number {
283
  const ratings = Object.values(feedback.ratings).filter(
284
    (value): value is number => typeof value === 'number',
285
  );
286
  if (!ratings.length) return 0.5;
287
  const extremeShare =
288
    ratings.filter((value) => value === 0 || value === 1).length / ratings.length;
289
  return extremeShare === 1 && feedback.evidenceReferences.length === 0 ? 0.6 : 1;
290
}
291
292
export function evaluateLearningEligibility(input: {
293
  interactionId: string;
294
  reports: InteractionCompletionReport[];
295
  feedback: InteractionFeedback[];
296
  consensus: InteractionConclusion['consensus'];
297
  contributionMode: 'local_only' | 'network_aggregate';
298
  reviewerCredibility?: Record<string, number>;
299
  decisionId?: string;
300
  decidedAt?: string;
301
}): LearningEligibilityDecision {
302
  const attestedReports = input.reports.filter((report) => report.platformAttestation);
303
  const attestedFeedback = input.feedback.filter((item) => item.platformAttestation);
304
  const bilateralReports =
305
    new Set(attestedReports.map((report) => report.reportingAgentId)).size >= 2;
306
  const evidenceReferences = [
307
    ...new Set([
308
      ...attestedReports.flatMap((report) => report.evidenceReferences),
309
      ...attestedFeedback.flatMap((item) => item.evidenceReferences),
310
    ]),
311
  ];
312
  const evidenceBacked = bilateralReports || evidenceReferences.length > 0;
313
  const eligible = attestedReports.length > 0 && evidenceBacked;
314
  const reviewerWeight = attestedFeedback.length
315
    ? attestedFeedback.reduce((total, item) => {
316
        const credibility = Math.max(
317
          0.1,
318
          Math.min(1, input.reviewerCredibility?.[item.reviewerAgentId] ?? 0.5),
319
        );
320
        return total + item.confidence * credibility * extremeFeedbackPenalty(item);
321
      }, 0) / attestedFeedback.length
322
    : 0;
323
  const sampleWeight = eligible
324
    ? Math.max(
325
        0.1,
326
        Math.min(
327
          1,
328
          0.25 +
329
            (bilateralReports ? 0.35 : 0) +
330
            Math.min(0.2, reviewerWeight * 0.2) +
331
            (evidenceReferences.length ? 0.1 : 0) -
332
            (input.consensus === 'conflicting' ? 0.2 : 0),
333
        ),
334
      )
335
    : 0;
336
  const reasons = [
337
    attestedReports.length
338
      ? `${attestedReports.length} platform-attested completion report(s)`
339
      : 'No attested completion report',
340
    bilateralReports
341
      ? 'Bilateral reports provide outcome corroboration'
342
      : 'Outcome is not bilaterally corroborated',
343
    attestedFeedback.length
344
      ? `${attestedFeedback.length} eligible feedback response(s)`
345
      : 'No eligible feedback response',
346
    evidenceReferences.length
347
      ? `${evidenceReferences.length} permitted evidence reference(s)`
348
      : 'No permitted external evidence reference',
349
    input.consensus === 'conflicting'
350
      ? 'Conflicting reports reduce sample weight'
351
      : 'No report-conflict penalty',
352
  ];
353
  return LearningEligibilityDecisionSchema.parse({
354
    decisionId: input.decisionId ?? randomUUID(),
355
    interactionId: input.interactionId,
356
    eligible,
357
    reasons,
358
    sampleWeight,
359
    reportIds: attestedReports.map((report) => report.reportId),
360
    feedbackIds: attestedFeedback.map((item) => item.feedbackId),
361
    evidenceReferences,
362
    contributionMode: input.contributionMode,
363
    structuredDataOnly: true,
364
    decidedAt: input.decidedAt ?? new Date().toISOString(),
365
  });
366
}
367
368
export type BehaviouralObservations = Partial<
369
  Record<
370
    | 'completion'
371
    | 'acceptance'
372
    | 'specification'
373
    | 'deadline'
374
    | 'communication'
375
    | 'evidence'
376
    | 'scope'
377
    | 'disputes',
378
    number
379
  >
380
>;
381
382
export const BEHAVIOURAL_DIMENSIONS = [
383
  'completion',
384
  'acceptance',
385
  'specification',
386
  'deadline',
387
  'communication',
388
  'evidence',
389
  'scope',
390
  'disputes',
391
] as const;
392
393
export type BehaviouralDimension = (typeof BEHAVIOURAL_DIMENSIONS)[number];
394
395
export type ContextualBehaviouralProfile = Record<BehaviouralDimension, number> & {
396
  sampleSize: number;
397
  effectiveSampleSize: number;
398
  updatedAt: string;
399
};
400
401
export function updateContextualBehaviouralProfile(input: {
402
  current?: Partial<ContextualBehaviouralProfile>;
403
  observations: BehaviouralObservations;
404
  sampleWeight: number;
405
  appliedAt?: string;
406
  decayDays?: number;
407
}): { profile: ContextualBehaviouralProfile; dimensionDeltas: Record<string, number> } {
408
  const appliedAt = input.appliedAt ?? new Date().toISOString();
409
  const decayDays = input.decayDays ?? 180;
410
  const priorAgeDays = input.current?.updatedAt
411
    ? Math.max(0, (Date.parse(appliedAt) - Date.parse(input.current.updatedAt)) / 86_400_000)
412
    : 0;
413
  const priorEffectiveSampleSize = Math.max(
414
    0,
415
    input.current?.effectiveSampleSize ?? input.current?.sampleSize ?? 0,
416
  );
417
  const decayedPriorWeight =
418
    priorEffectiveSampleSize * Math.exp(-priorAgeDays / Math.max(1, decayDays));
419
  const sampleWeight = Math.max(0, Math.min(1, input.sampleWeight));
420
  const hasObservation = BEHAVIOURAL_DIMENSIONS.some(
421
    (dimension) => typeof input.observations[dimension] === 'number',
422
  );
423
  const nextEffectiveSampleSize = decayedPriorWeight + (hasObservation ? sampleWeight : 0);
424
  const dimensionDeltas: Record<string, number> = {};
425
  const dimensions = Object.fromEntries(
426
    BEHAVIOURAL_DIMENSIONS.map((dimension) => {
427
      const previous = Math.max(0, Math.min(1, input.current?.[dimension] ?? 0.5));
428
      const observed = input.observations[dimension];
429
      if (typeof observed !== 'number' || sampleWeight === 0) return [dimension, previous];
430
      const next =
431
        (previous * decayedPriorWeight + Math.max(0, Math.min(1, observed)) * sampleWeight) /
432
        Math.max(Number.EPSILON, nextEffectiveSampleSize);
433
      dimensionDeltas[dimension] = next - previous;
434
      return [dimension, next];
435
    }),
436
  ) as Record<BehaviouralDimension, number>;
437
  return {
438
    profile: {
439
      ...dimensions,
440
      sampleSize:
441
        Math.max(0, Math.floor(input.current?.sampleSize ?? 0)) +
442
        (hasObservation && sampleWeight > 0 ? 1 : 0),
443
      effectiveSampleSize: nextEffectiveSampleSize,
444
      updatedAt: appliedAt,
445
    },
446
    dimensionDeltas,
447
  };
448
}
449
450
export function deriveBehaviouralObservations(input: {
451
  subjectAgentId: string;
452
  reports: InteractionCompletionReport[];
453
  reviewerFeedback?: InteractionFeedback;
454
  conclusion: InteractionConclusion;
455
}): BehaviouralObservations {
456
  const subjectReport = input.reports.find(
457
    (report) => report.reportingAgentId === input.subjectAgentId,
458
  );
459
  const criteria =
460
    subjectReport?.criteria.filter((criterion) => criterion.status !== 'unknown') ?? [];
461
  const criterionValue = (status: (typeof criteria)[number]['status']) =>
462
    status === 'met' ? 1 : status === 'partially_met' ? 0.5 : 0;
463
  const ratings = input.reviewerFeedback?.ratings;
464
  const observation: BehaviouralObservations = {
465
    ...(subjectReport
466
      ? {
467
          completion:
468
            subjectReport.outcome === 'success' ? 1 : subjectReport.outcome === 'partial' ? 0.5 : 0,
469
        }
470
      : {}),
471
    ...(criteria.length
472
      ? {
473
          specification:
474
            criteria.reduce((total, criterion) => total + criterionValue(criterion.status), 0) /
475
            criteria.length,
476
        }
477
      : {}),
478
    ...(typeof ratings?.outcome_satisfaction === 'number'
479
      ? { acceptance: ratings.outcome_satisfaction }
480
      : {}),
481
    ...(typeof ratings?.timeliness === 'number' ? { deadline: ratings.timeliness } : {}),
482
    ...(typeof ratings?.communication === 'number' ? { communication: ratings.communication } : {}),
483
    ...(typeof ratings?.evidence_quality === 'number'
484
      ? { evidence: ratings.evidence_quality }
485
      : {}),
486
    ...(typeof ratings?.scope_adherence === 'number' ? { scope: ratings.scope_adherence } : {}),
487
    disputes: input.conclusion.consensus === 'conflicting' ? 1 : 0,
488
  };
489
  return Object.fromEntries(
490
    Object.entries(observation).map(([key, value]) => [key, Math.max(0, Math.min(1, value))]),
491
  );
492
}
493
494
export interface AuditStore {
495
  append(kind: string, id: string, value: unknown): void;
496
  list(kind: string): unknown[];
497
}
498
499
export class MemoryAuditStore implements AuditStore {
500
  private rows: { kind: string; id: string; value: unknown }[] = [];
501
  append(kind: string, id: string, value: unknown): void {
502
    const old = this.rows.find((row) => row.kind === kind && row.id === id);
503
    if (old && canonicalHash(old.value) !== canonicalHash(value))
504
      throw new Error(`Conflicting duplicate ${kind}:${id}`);
505
    if (!old) this.rows.push({ kind, id, value: structuredClone(value) });
506
  }
507
  list(kind: string): unknown[] {
508
    return this.rows.filter((row) => row.kind === kind).map((row) => structuredClone(row.value));
509
  }
510
}
511
512
export function createIdentity(input: {
513
  agentId: string;
514
  operatorRef: string;
515
  version?: string;
516
  capabilities: string[];
517
  assurance?: AgentIdentity['assurance'];
518
  parentAgentId?: string;
519
  rootControllerId?: string;
520
}): { identity: AgentIdentity; keyPair: KeyPair } {
521
  const keyPair = createKeyPair(`${input.agentId}#1`);
522
  const identity: Record<string, unknown> = {
523
    protocolVersion: '0.1',
524
    agentId: input.agentId,
525
    publicKey: keyPair.publicKey,
526
    keyId: keyPair.keyId,
527
    operatorRef: input.operatorRef,
528
    assurance: input.assurance ?? 'pseudonymous',
529
    agentVersion: input.version ?? '1.0.0',
530
    capabilities: input.capabilities,
531
    supportedProtocols: ['A2A/1.0', 'OpenClasp/0.1'],
532
    ...(input.parentAgentId ? { parentAgentId: input.parentAgentId } : {}),
533
    rootControllerId: input.rootControllerId ?? input.agentId,
534
    revoked: false,
535
    provenance: 'cryptographically_verified',
536
    createdAt: new Date().toISOString(),
537
  };
538
  return { identity: AgentIdentitySchema.parse(signObject(identity, keyPair)), keyPair };
539
}
540
541
export type PolicyContext = {
542
  envelope: TrustEnvelope;
543
  action: string;
544
  dataClasses?: string[];
545
  humanApproved?: boolean;
546
};
547
548
export type Conflict = {
549
  conflictId: string;
550
  interactionId: string;
551
  issue: string;
552
  positions: Record<string, string>;
553
  evidence: string[];
554
  contractClauses: string[];
555
  missingInformation: string[];
556
  possibleResolutions: string[];
557
  permissions: Record<string, boolean>;
558
  status: 'pending_consent' | 'open' | 'resolved';
559
  resolution?: string;
560
};
561
562
type ProfileAggregate = {
563
  agentId: string;
564
  agentVersion: string;
565
  taskCategory: string;
566
  sampleSize: number;
567
  updatedAt: string;
568
  completion: number;
569
  acceptance: number;
570
  specification: number;
571
  deadline: number;
572
  communication: number;
573
  evidence: number;
574
  scope: number;
575
  disputes: number;
576
};
577
578
export class TrustEngine {
579
  readonly agents = new Map<string, AgentIdentity>();
580
  readonly delegations = new Map<string, DelegationCredential>();
581
  readonly contracts = new Map<string, InteractionContract>();
582
  readonly events = new Map<string, InteractionEvent>();
583
  readonly receipts = new Map<string, Receipt>();
584
  readonly conflicts = new Map<string, Conflict>();
585
  readonly feedback = new Map<string, Feedback>();
586
  readonly profiles = new Map<string, ProfileAggregate>();
587
  readonly contributionConsent = new Map<
588
    string,
589
    { enabled: boolean; grantedAt: string; revokedAt?: string }
590
  >();
591
  private readonly nonces = new Set<string>();
592
593
  constructor(readonly store: AuditStore = new MemoryAuditStore()) {
594
    for (const value of store.list('agent') as AgentIdentity[])
595
      this.agents.set(value.agentId, value);
596
    for (const value of store.list('delegation') as DelegationCredential[])
597
      this.delegations.set(value.delegationId, value);
598
    for (const value of store.list('contract') as InteractionContract[])
599
      this.contracts.set(value.interactionId, value);
600
    for (const value of store.list('event') as InteractionEvent[])
601
      this.events.set(value.eventId, value);
602
    for (const value of store.list('receipt') as Receipt[])
603
      this.receipts.set(value.receiptId, value);
604
    for (const value of store.list('feedback') as Feedback[])
605
      this.feedback.set(value.feedbackId, value);
606
    for (const value of store.list('profile') as ProfileAggregate[])
607
      this.profiles.set(`${value.agentId}|${value.agentVersion}|${value.taskCategory}`, value);
608
    for (const value of store.list('conflict') as Conflict[])
609
      this.conflicts.set(value.conflictId, value);
610
    for (const value of store.list('consent') as {
611
      agentId: string;
612
      enabled: boolean;
613
      grantedAt: string;
614
      revokedAt?: string;
615
    }[])
616
      this.contributionConsent.set(value.agentId, value);
617
    for (const value of store.list('revocation') as {
618
      agentId?: string;
619
      delegationId?: string;
620
    }[]) {
621
      if (value.agentId && this.agents.has(value.agentId))
622
        this.agents.set(value.agentId, { ...this.agents.get(value.agentId)!, revoked: true });
623
      if (value.delegationId && this.delegations.has(value.delegationId))
624
        this.delegations.set(value.delegationId, {
625
          ...this.delegations.get(value.delegationId)!,
626
          revoked: true,
627
        });
628
    }
629
  }
630
631
  registerAgent(identity: AgentIdentity): AgentIdentity {
632
    const parsed = AgentIdentitySchema.parse(identity);
633
    if (!verifyObject(parsed as unknown as Record<string, unknown>, parsed.publicKey))
634
      throw new Error('Invalid identity signature');
635
    if (parsed.parentAgentId && parsed.rootControllerId === parsed.agentId)
636
      throw new Error('Child cannot remove root-controller reference');
637
    this.agents.set(parsed.agentId, parsed);
638
    this.store.append('agent', parsed.agentId, parsed);
639
    return parsed;
640
  }
641
642
  revokeAgent(agentId: string): void {
643
    const identity = this.requireAgent(agentId);
644
    const revoked = { ...identity, revoked: true };
645
    this.agents.set(agentId, revoked);
646
    this.store.append('revocation', `agent:${agentId}`, { agentId, at: new Date().toISOString() });
647
  }
648
649
  createDelegation(
650
    parentId: string,
651
    childId: string,
652
    capabilities: string[],
653
    expiresAt: string,
654
    parentKey: KeyPair,
655
  ): DelegationCredential {
656
    const parent = this.requireAgent(parentId);
657
    const child = this.requireAgent(childId);
658
    if (parent.revoked) throw new Error('Parent agent is revoked');
659
    if (child.parentAgentId !== parentId || child.rootControllerId !== parent.rootControllerId)
660
      throw new Error('Invalid child lineage');
661
    if (capabilities.some((capability) => !parent.capabilities.includes(capability)))
662
      throw new Error('Delegation exceeds parent authority');
663
    const raw = {
664
      protocolVersion: '0.1' as const,
665
      delegationId: randomUUID(),
666
      parentAgentId: parentId,
667
      childAgentId: childId,
668
      rootControllerId: parent.rootControllerId,
669
      capabilities,
670
      issuedAt: new Date().toISOString(),
671
      expiresAt,
672
      revoked: false,
673
    };
674
    const credential = DelegationCredentialSchema.parse(signObject(raw, parentKey));
675
    this.delegations.set(credential.delegationId, credential);
676
    this.store.append('delegation', credential.delegationId, credential);
677
    return credential;
678
  }
679
680
  verifyDelegation(delegationId: string, capability?: string, at = new Date()): boolean {
681
    const delegation = this.delegations.get(delegationId);
682
    if (!delegation || delegation.revoked || new Date(delegation.expiresAt) <= at) return false;
683
    const parent = this.agents.get(delegation.parentAgentId);
684
    const child = this.agents.get(delegation.childAgentId);
685
    if (!parent || !child || parent.revoked || child.revoked) return false;
686
    if (
687
      child.rootControllerId !== delegation.rootControllerId ||
688
      child.parentAgentId !== parent.agentId
689
    )
690
      return false;
691
    if (capability && !delegation.capabilities.includes(capability)) return false;
692
    if (delegation.capabilities.some((item) => !parent.capabilities.includes(item))) return false;
693
    return verifyObject(delegation as unknown as Record<string, unknown>, parent.publicKey);
694
  }
695
696
  revokeDelegation(delegationId: string): void {
697
    const item = this.delegations.get(delegationId);
698
    if (!item) throw new Error('Delegation not found');
699
    this.delegations.set(delegationId, { ...item, revoked: true });
700
    this.store.append('revocation', `delegation:${delegationId}`, {
701
      delegationId,
702
      at: new Date().toISOString(),
703
    });
704
  }
705
706
  saveContract(contract: InteractionContract): InteractionContract {
707
    const parsed = InteractionContractSchema.parse(contract);
708
    if (parsed.parties.some((party) => !(party in parsed.signatures)))
709
      throw new Error('Contract requires every party signature');
710
    for (const party of Object.keys(parsed.signatures)) {
711
      const agent = this.requireAgent(party);
712
      if (!verifyNamed(parsed as unknown as Record<string, unknown>, party, agent.publicKey))
713
        throw new Error(`Invalid contract signature: ${party}`);
714
    }
715
    this.contracts.set(parsed.interactionId, parsed);
716
    this.store.append('contract', parsed.interactionId, parsed);
717
    return parsed;
718
  }
719
720
  assess(context: PolicyContext): RiskDecision {
721
    const envelope = TrustEnvelopeSchema.parse(context.envelope);
722
    const responder = this.agents.get(envelope.respondingAgentId);
723
    const requester = this.agents.get(envelope.requestingAgentId);
724
    const contract = this.contracts.get(envelope.interactionId);
725
    const hardFailures: string[] = [];
726
    if (
727
      !requester ||
728
      requester.revoked ||
729
      !verifyObject(envelope as unknown as Record<string, unknown>, requester.publicKey)
730
    )
731
      hardFailures.push('invalid_requester_signature');
732
    if (!responder || responder.revoked) hardFailures.push('invalid_or_revoked_responder');
733
    if (new Date(envelope.expiresAt) <= new Date()) hardFailures.push('expired_envelope');
734
    if (this.nonces.has(envelope.nonce)) hardFailures.push('replay_attempt');
735
    if (
736
      envelope.delegationId &&
737
      !this.verifyDelegation(envelope.delegationId, envelope.requestedCapability)
738
    )
739
      hardFailures.push('invalid_delegation');
740
    if (!contract || canonicalHash({ ...contract, signatures: {} }) !== envelope.contractHash)
741
      hardFailures.push('contract_mismatch');
742
    if (
743
      contract &&
744
      (!contract.allowedActions.includes(context.action) ||
745
        contract.prohibitedActions.includes(context.action))
746
    )
747
      hardFailures.push('action_outside_contract');
748
    if (
749
      contract &&
750
      (context.dataClasses ?? []).some((item) => contract.prohibitedData.includes(item))
751
    )
752
      hardFailures.push('prohibited_data');
753
    if (
754
      contract &&
755
      contract.humanApprovalRequirements.includes(context.action) &&
756
      !context.humanApproved
757
    )
758
      hardFailures.push('missing_human_approval');
759
    if (!hardFailures.includes('replay_attempt')) this.nonces.add(envelope.nonce);
760
    if (hardFailures.length)
761
      return this.decision('DENY', envelope.taskCategory, 1, hardFailures, [], []);
762
763
    const risk = this.getRisk(
764
      envelope.respondingAgentId,
765
      responder?.agentVersion ?? envelope.agentVersion,
766
      envelope.taskCategory,
767
    );
768
    return risk;
769
  }
770
771
  recordEvent(event: InteractionEvent): InteractionEvent {
772
    const parsed = InteractionEventSchema.parse(event);
773
    const agent = this.requireAgent(parsed.agentId);
774
    if (!verifyObject(parsed as unknown as Record<string, unknown>, agent.publicKey))
775
      throw new Error('Invalid event signature');
776
    if (canonicalHash(parsed.payload) !== parsed.payloadHash)
777
      throw new Error('Event payload hash mismatch');
778
    const existing = this.events.get(parsed.eventId);
779
    if (existing && canonicalHash(existing) !== canonicalHash(parsed))
780
      throw new Error('Conflicting duplicate event');
781
    this.events.set(parsed.eventId, parsed);
782
    this.store.append('event', parsed.eventId, parsed);
783
    return parsed;
784
  }
785
786
  createConflict(
787
    input: Omit<Conflict, 'conflictId' | 'status' | 'permissions'> & { participants: string[] },
788
  ): Conflict {
789
    const permissions = Object.fromEntries(input.participants.map((id) => [id, false]));
790
    const conflict: Conflict = {
791
      ...input,
792
      conflictId: randomUUID(),
793
      permissions,
794
      status: 'pending_consent',
795
    };
796
    delete (conflict as any).participants;
797
    this.conflicts.set(conflict.conflictId, conflict);
798
    this.store.append('conflict', `${conflict.conflictId}:created`, conflict);
799
    return conflict;
800
  }
801
802
  permitMediation(conflictId: string, agentId: string): Conflict {
803
    const conflict = this.requireConflict(conflictId);
804
    if (!(agentId in conflict.permissions)) throw new Error('Agent is not a conflict participant');
805
    conflict.permissions[agentId] = true;
806
    if (Object.values(conflict.permissions).every(Boolean)) conflict.status = 'open';
807
    this.store.append('conflict', `${conflict.conflictId}:permit:${agentId}`, conflict);
808
    return conflict;
809
  }
810
811
  resolveConflict(conflictId: string, resolution: string): Conflict {
812
    const conflict = this.requireConflict(conflictId);
813
    if (conflict.status !== 'open') throw new Error('Mutual mediation consent required');
814
    conflict.status = 'resolved';
815
    conflict.resolution = resolution;
816
    this.store.append('conflict', `${conflict.conflictId}:resolved`, conflict);
817
    return conflict;
818
  }
819
820
  submitReceipt(receipt: Receipt): Receipt {
821
    const parsed = this.verifyReceipt(receipt);
822
    this.receipts.set(parsed.receiptId, parsed);
823
    this.store.append('receipt', parsed.receiptId, parsed);
824
    return parsed;
825
  }
826
827
  verifyReceipt(receipt: Receipt): Receipt {
828
    const parsed = ReceiptSchema.parse(receipt);
829
    const required = parsed.unilateral
830
      ? Object.keys(parsed.signatures).slice(0, 1)
831
      : parsed.participants;
832
    if (
833
      !required.length ||
834
      required.some((party) => {
835
        const agent = this.agents.get(party);
836
        return (
837
          !agent ||
838
          !verifyNamed(parsed as unknown as Record<string, unknown>, party, agent.publicKey)
839
        );
840
      })
841
    )
842
      throw new Error('Receipt signature verification failed');
843
    return parsed;
844
  }
845
846
  submitFeedback(item: Feedback): { revealed: boolean } {
847
    const parsed = FeedbackSchema.parse(item);
848
    const receipt = this.receipts.get(parsed.receiptId);
849
    const reviewer = this.agents.get(parsed.reviewerAgentId);
850
    if (
851
      !receipt ||
852
      !reviewer ||
853
      !receipt.participants.includes(parsed.reviewerAgentId) ||
854
      !receipt.participants.includes(parsed.subjectAgentId)
855
    )
856
      throw new Error('Feedback requires a valid participant receipt');
857
    if (!verifyObject(parsed as unknown as Record<string, unknown>, reviewer.publicKey))
858
      throw new Error('Invalid feedback signature');
859
    this.feedback.set(parsed.feedbackId, parsed);
860
    this.store.append('feedback', parsed.feedbackId, parsed);
861
    const related = [...this.feedback.values()].filter(
862
      (value) => value.receiptId === parsed.receiptId,
863
    );
864
    const revealed =
865
      receipt.unilateral ||
866
      new Set(related.map((value) => value.reviewerAgentId)).size >= receipt.participants.length;
867
    if (revealed) for (const feedback of related) this.applyFeedback(feedback, receipt);
868
    return { revealed };
869
  }
870
871
  getRisk(agentId: string, version: string, taskCategory: string): RiskDecision {
872
    const current = this.profiles.get(`${agentId}|${version}|${taskCategory}`);
873
    const otherVersions = [...this.profiles.values()].filter(
874
      (p) => p.agentId === agentId && p.taskCategory === taskCategory,
875
    );
876
    if (!current) {
877
      const continuity = otherVersions.length
878
        ? Math.min(
879
            0.2,
880
            Math.max(
881
              ...otherVersions.map((profile) => profile.sampleSize / (profile.sampleSize + 5)),
882
            ) * 0.25,
883
          )
884
        : 0;
885
      return this.decision(
886
        'CHALLENGE',
887
        taskCategory,
888
        continuity,
889
        ['limited_verified_history'],
890
        ['Agent version has limited task-specific evidence'],
891
        ['request_evidence'],
892
      );
893
    }
894
    const ageDays = Math.max(0, (Date.now() - Date.parse(current.updatedAt)) / 86_400_000);
895
    const freshness = Math.exp(-ageDays / 180);
896
    const quality =
897
      (current.completion +
898
        current.acceptance +
899
        current.specification +
900
        current.deadline +
901
        current.communication +
902
        current.evidence +
903
        current.scope +
904
        (1 - current.disputes)) /
905
      8;
906
    const confidence = Math.min(0.95, (current.sampleSize / (current.sampleSize + 5)) * freshness);
907
    const decision = quality >= 0.7 && confidence >= 0.25 ? 'ALLOW' : 'CHALLENGE';
908
    return {
909
      decision,
910
      confidence,
911
      taskCategory,
912
      sampleSize: current.sampleSize,
913
      dimensions: {
914
        completionReliability: current.completion,
915
        outputAcceptance: current.acceptance,
916
        contractAdherence: (current.specification + current.scope) / 2,
917
        deadlineReliability: current.deadline,
918
        communicationQuality: current.communication,
919
        evidenceQuality: current.evidence,
920
        disputeRate: current.disputes,
921
      },
922
      reasons: [`contextual_quality=${quality.toFixed(2)}`],
923
      warnings: confidence < 0.5 ? ['Limited sample size'] : [],
924
      dataFreshness: { behaviouralProfile: current.updatedAt },
925
      requiredChallenges: decision === 'CHALLENGE' ? ['request_evidence'] : [],
926
    };
927
  }
928
929
  setContributionConsent(agentId: string, enabled: boolean): void {
930
    this.requireAgent(agentId);
931
    const previous = this.contributionConsent.get(agentId);
932
    this.contributionConsent.set(
933
      agentId,
934
      enabled
935
        ? { enabled: true, grantedAt: new Date().toISOString() }
936
        : {
937
            enabled: false,
938
            grantedAt: previous?.grantedAt ?? new Date().toISOString(),
939
            revokedAt: new Date().toISOString(),
940
          },
941
    );
942
    this.store.append('consent', `${agentId}:${Date.now()}`, {
943
      agentId,
944
      ...this.contributionConsent.get(agentId),
945
    });
946
  }
947
948
  networkContribution(event: InteractionEvent): Record<string, unknown> | null {
949
    if (!this.contributionConsent.get(event.agentId)?.enabled || event.visibility === 'local_only')
950
      return null;
951
    return {
952
      eventId: event.eventId,
953
      interactionId: event.interactionId,
954
      eventType: event.eventType,
955
      agentId: event.agentId,
956
      agentVersion: event.agentVersion,
957
      timestamp: event.timestamp,
958
      visibility: event.visibility,
959
      provenance: event.provenance,
960
      payloadHash: event.payloadHash,
961
      evidenceRefs: event.evidenceRefs ?? [],
962
      signature: event.signature,
963
    };
964
  }
965
966
  private applyFeedback(feedback: Feedback, receipt: Receipt): void {
967
    const taskCategory = this.contracts.get(feedback.interactionId)?.taskCategory ?? 'unknown';
968
    const version = receipt.agentVersions[feedback.subjectAgentId] ?? 'unknown';
969
    const key = `${feedback.subjectAgentId}|${version}|${taskCategory}`;
970
    const previous = this.profiles.get(key);
971
    const n = previous?.sampleSize ?? 0;
972
    const next = (old: number | undefined, value: number) => ((old ?? 0) * n + value) / (n + 1);
973
    const profile = {
974
      agentId: feedback.subjectAgentId,
975
      agentVersion: version,
976
      taskCategory,
977
      sampleSize: n + 1,
978
      updatedAt: new Date().toISOString(),
979
      completion: next(previous?.completion, Number(feedback.taskCompleted)),
980
      acceptance: next(previous?.acceptance, Number(feedback.outputAccepted)),
981
      specification: next(previous?.specification, Number(feedback.specificationMatched)),
982
      deadline: next(previous?.deadline, Number(feedback.deadlineMet)),
983
      communication: next(previous?.communication, feedback.communicationQuality),
984
      evidence: next(previous?.evidence, Number(feedback.evidenceProvided)),
985
      scope: next(previous?.scope, Number(feedback.scopeRespected)),
986
      disputes: next(previous?.disputes, Number(feedback.disputeRaised)),
987
    };
988
    this.profiles.set(key, profile);
989
    this.store.append('profile', `${key}:${profile.sampleSize}`, profile);
990
  }
991
992
  private decision(
993
    decision: RiskDecision['decision'],
994
    taskCategory: string,
995
    confidence: number,
996
    reasons: string[],
997
    warnings: string[],
998
    challenges: string[],
999
  ): RiskDecision {
1000
    return {
1001
      decision,
1002
      confidence,
1003
      taskCategory,
1004
      sampleSize: 0,
1005
      dimensions: {},
1006
      reasons,
1007
      warnings,
1008
      dataFreshness: {},
1009
      requiredChallenges: challenges,
1010
    };
1011
  }
1012
  private requireAgent(id: string): AgentIdentity {
1013
    const value = this.agents.get(id);
1014
    if (!value) throw new Error(`Agent not found: ${id}`);
1015
    return value;
1016
  }
1017
  private requireConflict(id: string): Conflict {
1018
    const value = this.conflicts.get(id);
1019
    if (!value) throw new Error('Conflict not found');
1020
    return value;
1021
  }
1022
}
1023
1024
export interface FactCheckProvider {
1025
  check(claim: string, permission?: boolean): Promise<FactCheckResult>;
1026
}
1027
1028
export class FixtureFactCheckProvider implements FactCheckProvider {
1029
  constructor(
1030
    private fixtures: Record<
1031
      string,
1032
      { status: FactCheckResult['status']; evidence: string[] }
1033
    > = {},
1034
  ) {}
1035
  async check(claim: string, permission = true): Promise<FactCheckResult> {
1036
    if (!permission)
1037
      return result(
1038
        claim,
1039
        'objective',
1040
        'insufficient_permission',
1041
        1,
1042
        [],
1043
        'Grant evidence-source permission',
1044
      );
1045
    if (/\b(i think|i prefer|best|beautiful)\b/i.test(claim))
1046
      return result(claim, 'subjective', 'not_fact_checkable', 1, [], 'Treat as an opinion');
1047
    if (/\b(will|might|forecast|predict)\b/i.test(claim))
1048
      return result(
1049
        claim,
1050
        'prediction',
1051
        'not_fact_checkable',
1052
        0.9,
1053
        [],
1054
        'Track as a prediction, not a fact',
1055
      );
1056
    const fixture = this.fixtures[claim];
1057
    if (!fixture)
1058
      return result(claim, 'objective', 'unverified', 0, [], 'Request authoritative evidence');
1059
    return result(
1060
      claim,
1061
      'objective',
1062
      fixture.status,
1063
      0.95,
1064
      fixture.evidence,
1065
      fixture.status === 'contradicted' ? 'Challenge the claim with cited evidence' : 'Continue',
1066
    );
1067
  }
1068
}
1069
1070
function result(
1071
  claim: string,
1072
  claimType: FactCheckResult['claimType'],
1073
  status: FactCheckResult['status'],
1074
  confidence: number,
1075
  evidence: string[],
1076
  action: string,
1077
): FactCheckResult {
1078
  return {
1079
    claim,
1080
    claimType,
1081
    status,
1082
    confidence,
1083
    evidenceReferences: evidence,
1084
    sourceAuthority: evidence.length ? 'authoritative_fixture' : 'none',
1085
    sourceFreshness: new Date().toISOString(),
1086
    contradictingEvidence: status === 'contradicted' ? evidence : [],
1087
    suggestedNextAction: action,
1088
  };
1089
}
1090
1091
export { signNamed, signObject };