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

Viewing files at commit 18afdc33bab8Commit detailsView latest

packages/core/src/index.ts 625 lines · 22.5 KB · typescript
1
import { randomUUID } from 'node:crypto';
2
import {
3
  AgentIdentitySchema,
4
  canonicalHash,
5
  createKeyPair,
6
  DelegationCredentialSchema,
7
  FeedbackSchema,
8
  InteractionContractSchema,
9
  InteractionEventSchema,
10
  ReceiptSchema,
11
  signNamed,
12
  signObject,
13
  TrustEnvelopeSchema,
14
  verifyNamed,
15
  verifyObject,
16
  type AgentIdentity,
17
  type DelegationCredential,
18
  type FactCheckResult,
19
  type Feedback,
20
  type InteractionContract,
21
  type InteractionEvent,
22
  type KeyPair,
23
  type Receipt,
24
  type RiskDecision,
25
  type TrustEnvelope,
26
} from '../../protocol/src/index.js';
27
28
export interface AuditStore {
29
  append(kind: string, id: string, value: unknown): void;
30
  list(kind: string): unknown[];
31
}
32
33
export class MemoryAuditStore implements AuditStore {
34
  private rows: { kind: string; id: string; value: unknown }[] = [];
35
  append(kind: string, id: string, value: unknown): void {
36
    const old = this.rows.find((row) => row.kind === kind && row.id === id);
37
    if (old && canonicalHash(old.value) !== canonicalHash(value))
38
      throw new Error(`Conflicting duplicate ${kind}:${id}`);
39
    if (!old) this.rows.push({ kind, id, value: structuredClone(value) });
40
  }
41
  list(kind: string): unknown[] {
42
    return this.rows.filter((row) => row.kind === kind).map((row) => structuredClone(row.value));
43
  }
44
}
45
46
export function createIdentity(input: {
47
  agentId: string;
48
  operatorRef: string;
49
  version?: string;
50
  capabilities: string[];
51
  assurance?: AgentIdentity['assurance'];
52
  parentAgentId?: string;
53
  rootControllerId?: string;
54
}): { identity: AgentIdentity; keyPair: KeyPair } {
55
  const keyPair = createKeyPair(`${input.agentId}#1`);
56
  const identity: Record<string, unknown> = {
57
    protocolVersion: '0.1',
58
    agentId: input.agentId,
59
    publicKey: keyPair.publicKey,
60
    keyId: keyPair.keyId,
61
    operatorRef: input.operatorRef,
62
    assurance: input.assurance ?? 'pseudonymous',
63
    agentVersion: input.version ?? '1.0.0',
64
    capabilities: input.capabilities,
65
    supportedProtocols: ['A2A/1.0', 'OpenClasp/0.1'],
66
    ...(input.parentAgentId ? { parentAgentId: input.parentAgentId } : {}),
67
    rootControllerId: input.rootControllerId ?? input.agentId,
68
    revoked: false,
69
    provenance: 'cryptographically_verified',
70
    createdAt: new Date().toISOString(),
71
  };
72
  return { identity: AgentIdentitySchema.parse(signObject(identity, keyPair)), keyPair };
73
}
74
75
export type PolicyContext = {
76
  envelope: TrustEnvelope;
77
  action: string;
78
  dataClasses?: string[];
79
  humanApproved?: boolean;
80
};
81
82
export type Conflict = {
83
  conflictId: string;
84
  interactionId: string;
85
  issue: string;
86
  positions: Record<string, string>;
87
  evidence: string[];
88
  contractClauses: string[];
89
  missingInformation: string[];
90
  possibleResolutions: string[];
91
  permissions: Record<string, boolean>;
92
  status: 'pending_consent' | 'open' | 'resolved';
93
  resolution?: string;
94
};
95
96
type ProfileAggregate = {
97
  agentId: string;
98
  agentVersion: string;
99
  taskCategory: string;
100
  sampleSize: number;
101
  updatedAt: string;
102
  completion: number;
103
  acceptance: number;
104
  specification: number;
105
  deadline: number;
106
  communication: number;
107
  evidence: number;
108
  scope: number;
109
  disputes: number;
110
};
111
112
export class TrustEngine {
113
  readonly agents = new Map<string, AgentIdentity>();
114
  readonly delegations = new Map<string, DelegationCredential>();
115
  readonly contracts = new Map<string, InteractionContract>();
116
  readonly events = new Map<string, InteractionEvent>();
117
  readonly receipts = new Map<string, Receipt>();
118
  readonly conflicts = new Map<string, Conflict>();
119
  readonly feedback = new Map<string, Feedback>();
120
  readonly profiles = new Map<string, ProfileAggregate>();
121
  readonly contributionConsent = new Map<
122
    string,
123
    { enabled: boolean; grantedAt: string; revokedAt?: string }
124
  >();
125
  private readonly nonces = new Set<string>();
126
127
  constructor(readonly store: AuditStore = new MemoryAuditStore()) {
128
    for (const value of store.list('agent') as AgentIdentity[])
129
      this.agents.set(value.agentId, value);
130
    for (const value of store.list('delegation') as DelegationCredential[])
131
      this.delegations.set(value.delegationId, value);
132
    for (const value of store.list('contract') as InteractionContract[])
133
      this.contracts.set(value.interactionId, value);
134
    for (const value of store.list('event') as InteractionEvent[])
135
      this.events.set(value.eventId, value);
136
    for (const value of store.list('receipt') as Receipt[])
137
      this.receipts.set(value.receiptId, value);
138
    for (const value of store.list('feedback') as Feedback[])
139
      this.feedback.set(value.feedbackId, value);
140
    for (const value of store.list('profile') as ProfileAggregate[])
141
      this.profiles.set(`${value.agentId}|${value.agentVersion}|${value.taskCategory}`, value);
142
    for (const value of store.list('conflict') as Conflict[])
143
      this.conflicts.set(value.conflictId, value);
144
    for (const value of store.list('consent') as {
145
      agentId: string;
146
      enabled: boolean;
147
      grantedAt: string;
148
      revokedAt?: string;
149
    }[])
150
      this.contributionConsent.set(value.agentId, value);
151
    for (const value of store.list('revocation') as {
152
      agentId?: string;
153
      delegationId?: string;
154
    }[]) {
155
      if (value.agentId && this.agents.has(value.agentId))
156
        this.agents.set(value.agentId, { ...this.agents.get(value.agentId)!, revoked: true });
157
      if (value.delegationId && this.delegations.has(value.delegationId))
158
        this.delegations.set(value.delegationId, {
159
          ...this.delegations.get(value.delegationId)!,
160
          revoked: true,
161
        });
162
    }
163
  }
164
165
  registerAgent(identity: AgentIdentity): AgentIdentity {
166
    const parsed = AgentIdentitySchema.parse(identity);
167
    if (!verifyObject(parsed as unknown as Record<string, unknown>, parsed.publicKey))
168
      throw new Error('Invalid identity signature');
169
    if (parsed.parentAgentId && parsed.rootControllerId === parsed.agentId)
170
      throw new Error('Child cannot remove root-controller reference');
171
    this.agents.set(parsed.agentId, parsed);
172
    this.store.append('agent', parsed.agentId, parsed);
173
    return parsed;
174
  }
175
176
  revokeAgent(agentId: string): void {
177
    const identity = this.requireAgent(agentId);
178
    const revoked = { ...identity, revoked: true };
179
    this.agents.set(agentId, revoked);
180
    this.store.append('revocation', `agent:${agentId}`, { agentId, at: new Date().toISOString() });
181
  }
182
183
  createDelegation(
184
    parentId: string,
185
    childId: string,
186
    capabilities: string[],
187
    expiresAt: string,
188
    parentKey: KeyPair,
189
  ): DelegationCredential {
190
    const parent = this.requireAgent(parentId);
191
    const child = this.requireAgent(childId);
192
    if (parent.revoked) throw new Error('Parent agent is revoked');
193
    if (child.parentAgentId !== parentId || child.rootControllerId !== parent.rootControllerId)
194
      throw new Error('Invalid child lineage');
195
    if (capabilities.some((capability) => !parent.capabilities.includes(capability)))
196
      throw new Error('Delegation exceeds parent authority');
197
    const raw = {
198
      protocolVersion: '0.1' as const,
199
      delegationId: randomUUID(),
200
      parentAgentId: parentId,
201
      childAgentId: childId,
202
      rootControllerId: parent.rootControllerId,
203
      capabilities,
204
      issuedAt: new Date().toISOString(),
205
      expiresAt,
206
      revoked: false,
207
    };
208
    const credential = DelegationCredentialSchema.parse(signObject(raw, parentKey));
209
    this.delegations.set(credential.delegationId, credential);
210
    this.store.append('delegation', credential.delegationId, credential);
211
    return credential;
212
  }
213
214
  verifyDelegation(delegationId: string, capability?: string, at = new Date()): boolean {
215
    const delegation = this.delegations.get(delegationId);
216
    if (!delegation || delegation.revoked || new Date(delegation.expiresAt) <= at) return false;
217
    const parent = this.agents.get(delegation.parentAgentId);
218
    const child = this.agents.get(delegation.childAgentId);
219
    if (!parent || !child || parent.revoked || child.revoked) return false;
220
    if (
221
      child.rootControllerId !== delegation.rootControllerId ||
222
      child.parentAgentId !== parent.agentId
223
    )
224
      return false;
225
    if (capability && !delegation.capabilities.includes(capability)) return false;
226
    if (delegation.capabilities.some((item) => !parent.capabilities.includes(item))) return false;
227
    return verifyObject(delegation as unknown as Record<string, unknown>, parent.publicKey);
228
  }
229
230
  revokeDelegation(delegationId: string): void {
231
    const item = this.delegations.get(delegationId);
232
    if (!item) throw new Error('Delegation not found');
233
    this.delegations.set(delegationId, { ...item, revoked: true });
234
    this.store.append('revocation', `delegation:${delegationId}`, {
235
      delegationId,
236
      at: new Date().toISOString(),
237
    });
238
  }
239
240
  saveContract(contract: InteractionContract): InteractionContract {
241
    const parsed = InteractionContractSchema.parse(contract);
242
    if (parsed.parties.some((party) => !(party in parsed.signatures)))
243
      throw new Error('Contract requires every party signature');
244
    for (const party of Object.keys(parsed.signatures)) {
245
      const agent = this.requireAgent(party);
246
      if (!verifyNamed(parsed as unknown as Record<string, unknown>, party, agent.publicKey))
247
        throw new Error(`Invalid contract signature: ${party}`);
248
    }
249
    this.contracts.set(parsed.interactionId, parsed);
250
    this.store.append('contract', parsed.interactionId, parsed);
251
    return parsed;
252
  }
253
254
  assess(context: PolicyContext): RiskDecision {
255
    const envelope = TrustEnvelopeSchema.parse(context.envelope);
256
    const responder = this.agents.get(envelope.respondingAgentId);
257
    const requester = this.agents.get(envelope.requestingAgentId);
258
    const contract = this.contracts.get(envelope.interactionId);
259
    const hardFailures: string[] = [];
260
    if (
261
      !requester ||
262
      requester.revoked ||
263
      !verifyObject(envelope as unknown as Record<string, unknown>, requester.publicKey)
264
    )
265
      hardFailures.push('invalid_requester_signature');
266
    if (!responder || responder.revoked) hardFailures.push('invalid_or_revoked_responder');
267
    if (new Date(envelope.expiresAt) <= new Date()) hardFailures.push('expired_envelope');
268
    if (this.nonces.has(envelope.nonce)) hardFailures.push('replay_attempt');
269
    if (
270
      envelope.delegationId &&
271
      !this.verifyDelegation(envelope.delegationId, envelope.requestedCapability)
272
    )
273
      hardFailures.push('invalid_delegation');
274
    if (!contract || canonicalHash({ ...contract, signatures: {} }) !== envelope.contractHash)
275
      hardFailures.push('contract_mismatch');
276
    if (
277
      contract &&
278
      (!contract.allowedActions.includes(context.action) ||
279
        contract.prohibitedActions.includes(context.action))
280
    )
281
      hardFailures.push('action_outside_contract');
282
    if (
283
      contract &&
284
      (context.dataClasses ?? []).some((item) => contract.prohibitedData.includes(item))
285
    )
286
      hardFailures.push('prohibited_data');
287
    if (
288
      contract &&
289
      contract.humanApprovalRequirements.includes(context.action) &&
290
      !context.humanApproved
291
    )
292
      hardFailures.push('missing_human_approval');
293
    if (!hardFailures.includes('replay_attempt')) this.nonces.add(envelope.nonce);
294
    if (hardFailures.length)
295
      return this.decision('DENY', envelope.taskCategory, 1, hardFailures, [], []);
296
297
    const risk = this.getRisk(
298
      envelope.respondingAgentId,
299
      responder?.agentVersion ?? envelope.agentVersion,
300
      envelope.taskCategory,
301
    );
302
    return risk;
303
  }
304
305
  recordEvent(event: InteractionEvent): InteractionEvent {
306
    const parsed = InteractionEventSchema.parse(event);
307
    const agent = this.requireAgent(parsed.agentId);
308
    if (!verifyObject(parsed as unknown as Record<string, unknown>, agent.publicKey))
309
      throw new Error('Invalid event signature');
310
    if (canonicalHash(parsed.payload) !== parsed.payloadHash)
311
      throw new Error('Event payload hash mismatch');
312
    const existing = this.events.get(parsed.eventId);
313
    if (existing && canonicalHash(existing) !== canonicalHash(parsed))
314
      throw new Error('Conflicting duplicate event');
315
    this.events.set(parsed.eventId, parsed);
316
    this.store.append('event', parsed.eventId, parsed);
317
    return parsed;
318
  }
319
320
  createConflict(
321
    input: Omit<Conflict, 'conflictId' | 'status' | 'permissions'> & { participants: string[] },
322
  ): Conflict {
323
    const permissions = Object.fromEntries(input.participants.map((id) => [id, false]));
324
    const conflict: Conflict = {
325
      ...input,
326
      conflictId: randomUUID(),
327
      permissions,
328
      status: 'pending_consent',
329
    };
330
    delete (conflict as any).participants;
331
    this.conflicts.set(conflict.conflictId, conflict);
332
    this.store.append('conflict', `${conflict.conflictId}:created`, conflict);
333
    return conflict;
334
  }
335
336
  permitMediation(conflictId: string, agentId: string): Conflict {
337
    const conflict = this.requireConflict(conflictId);
338
    if (!(agentId in conflict.permissions)) throw new Error('Agent is not a conflict participant');
339
    conflict.permissions[agentId] = true;
340
    if (Object.values(conflict.permissions).every(Boolean)) conflict.status = 'open';
341
    this.store.append('conflict', `${conflict.conflictId}:permit:${agentId}`, conflict);
342
    return conflict;
343
  }
344
345
  resolveConflict(conflictId: string, resolution: string): Conflict {
346
    const conflict = this.requireConflict(conflictId);
347
    if (conflict.status !== 'open') throw new Error('Mutual mediation consent required');
348
    conflict.status = 'resolved';
349
    conflict.resolution = resolution;
350
    this.store.append('conflict', `${conflict.conflictId}:resolved`, conflict);
351
    return conflict;
352
  }
353
354
  submitReceipt(receipt: Receipt): Receipt {
355
    const parsed = this.verifyReceipt(receipt);
356
    this.receipts.set(parsed.receiptId, parsed);
357
    this.store.append('receipt', parsed.receiptId, parsed);
358
    return parsed;
359
  }
360
361
  verifyReceipt(receipt: Receipt): Receipt {
362
    const parsed = ReceiptSchema.parse(receipt);
363
    const required = parsed.unilateral
364
      ? Object.keys(parsed.signatures).slice(0, 1)
365
      : parsed.participants;
366
    if (
367
      !required.length ||
368
      required.some((party) => {
369
        const agent = this.agents.get(party);
370
        return (
371
          !agent ||
372
          !verifyNamed(parsed as unknown as Record<string, unknown>, party, agent.publicKey)
373
        );
374
      })
375
    )
376
      throw new Error('Receipt signature verification failed');
377
    return parsed;
378
  }
379
380
  submitFeedback(item: Feedback): { revealed: boolean } {
381
    const parsed = FeedbackSchema.parse(item);
382
    const receipt = this.receipts.get(parsed.receiptId);
383
    const reviewer = this.agents.get(parsed.reviewerAgentId);
384
    if (
385
      !receipt ||
386
      !reviewer ||
387
      !receipt.participants.includes(parsed.reviewerAgentId) ||
388
      !receipt.participants.includes(parsed.subjectAgentId)
389
    )
390
      throw new Error('Feedback requires a valid participant receipt');
391
    if (!verifyObject(parsed as unknown as Record<string, unknown>, reviewer.publicKey))
392
      throw new Error('Invalid feedback signature');
393
    this.feedback.set(parsed.feedbackId, parsed);
394
    this.store.append('feedback', parsed.feedbackId, parsed);
395
    const related = [...this.feedback.values()].filter(
396
      (value) => value.receiptId === parsed.receiptId,
397
    );
398
    const revealed =
399
      receipt.unilateral ||
400
      new Set(related.map((value) => value.reviewerAgentId)).size >= receipt.participants.length;
401
    if (revealed) for (const feedback of related) this.applyFeedback(feedback, receipt);
402
    return { revealed };
403
  }
404
405
  getRisk(agentId: string, version: string, taskCategory: string): RiskDecision {
406
    const current = this.profiles.get(`${agentId}|${version}|${taskCategory}`);
407
    const otherVersions = [...this.profiles.values()].filter(
408
      (p) => p.agentId === agentId && p.taskCategory === taskCategory,
409
    );
410
    if (!current) {
411
      const continuity = otherVersions.length
412
        ? Math.min(
413
            0.2,
414
            Math.max(
415
              ...otherVersions.map((profile) => profile.sampleSize / (profile.sampleSize + 5)),
416
            ) * 0.25,
417
          )
418
        : 0;
419
      return this.decision(
420
        'CHALLENGE',
421
        taskCategory,
422
        continuity,
423
        ['limited_verified_history'],
424
        ['Agent version has limited task-specific evidence'],
425
        ['request_evidence'],
426
      );
427
    }
428
    const ageDays = Math.max(0, (Date.now() - Date.parse(current.updatedAt)) / 86_400_000);
429
    const freshness = Math.exp(-ageDays / 180);
430
    const quality =
431
      (current.completion +
432
        current.acceptance +
433
        current.specification +
434
        current.deadline +
435
        current.communication +
436
        current.evidence +
437
        current.scope +
438
        (1 - current.disputes)) /
439
      8;
440
    const confidence = Math.min(0.95, (current.sampleSize / (current.sampleSize + 5)) * freshness);
441
    const decision = quality >= 0.7 && confidence >= 0.25 ? 'ALLOW' : 'CHALLENGE';
442
    return {
443
      decision,
444
      confidence,
445
      taskCategory,
446
      sampleSize: current.sampleSize,
447
      dimensions: {
448
        completionReliability: current.completion,
449
        outputAcceptance: current.acceptance,
450
        contractAdherence: (current.specification + current.scope) / 2,
451
        deadlineReliability: current.deadline,
452
        communicationQuality: current.communication,
453
        evidenceQuality: current.evidence,
454
        disputeRate: current.disputes,
455
      },
456
      reasons: [`contextual_quality=${quality.toFixed(2)}`],
457
      warnings: confidence < 0.5 ? ['Limited sample size'] : [],
458
      dataFreshness: { behaviouralProfile: current.updatedAt },
459
      requiredChallenges: decision === 'CHALLENGE' ? ['request_evidence'] : [],
460
    };
461
  }
462
463
  setContributionConsent(agentId: string, enabled: boolean): void {
464
    this.requireAgent(agentId);
465
    const previous = this.contributionConsent.get(agentId);
466
    this.contributionConsent.set(
467
      agentId,
468
      enabled
469
        ? { enabled: true, grantedAt: new Date().toISOString() }
470
        : {
471
            enabled: false,
472
            grantedAt: previous?.grantedAt ?? new Date().toISOString(),
473
            revokedAt: new Date().toISOString(),
474
          },
475
    );
476
    this.store.append('consent', `${agentId}:${Date.now()}`, {
477
      agentId,
478
      ...this.contributionConsent.get(agentId),
479
    });
480
  }
481
482
  networkContribution(event: InteractionEvent): Record<string, unknown> | null {
483
    if (!this.contributionConsent.get(event.agentId)?.enabled || event.visibility === 'local_only')
484
      return null;
485
    return {
486
      eventId: event.eventId,
487
      interactionId: event.interactionId,
488
      eventType: event.eventType,
489
      agentId: event.agentId,
490
      agentVersion: event.agentVersion,
491
      timestamp: event.timestamp,
492
      visibility: event.visibility,
493
      provenance: event.provenance,
494
      payloadHash: event.payloadHash,
495
      evidenceRefs: event.evidenceRefs ?? [],
496
      signature: event.signature,
497
    };
498
  }
499
500
  private applyFeedback(feedback: Feedback, receipt: Receipt): void {
501
    const taskCategory = this.contracts.get(feedback.interactionId)?.taskCategory ?? 'unknown';
502
    const version = receipt.agentVersions[feedback.subjectAgentId] ?? 'unknown';
503
    const key = `${feedback.subjectAgentId}|${version}|${taskCategory}`;
504
    const previous = this.profiles.get(key);
505
    const n = previous?.sampleSize ?? 0;
506
    const next = (old: number | undefined, value: number) => ((old ?? 0) * n + value) / (n + 1);
507
    const profile = {
508
      agentId: feedback.subjectAgentId,
509
      agentVersion: version,
510
      taskCategory,
511
      sampleSize: n + 1,
512
      updatedAt: new Date().toISOString(),
513
      completion: next(previous?.completion, Number(feedback.taskCompleted)),
514
      acceptance: next(previous?.acceptance, Number(feedback.outputAccepted)),
515
      specification: next(previous?.specification, Number(feedback.specificationMatched)),
516
      deadline: next(previous?.deadline, Number(feedback.deadlineMet)),
517
      communication: next(previous?.communication, feedback.communicationQuality),
518
      evidence: next(previous?.evidence, Number(feedback.evidenceProvided)),
519
      scope: next(previous?.scope, Number(feedback.scopeRespected)),
520
      disputes: next(previous?.disputes, Number(feedback.disputeRaised)),
521
    };
522
    this.profiles.set(key, profile);
523
    this.store.append('profile', `${key}:${profile.sampleSize}`, profile);
524
  }
525
526
  private decision(
527
    decision: RiskDecision['decision'],
528
    taskCategory: string,
529
    confidence: number,
530
    reasons: string[],
531
    warnings: string[],
532
    challenges: string[],
533
  ): RiskDecision {
534
    return {
535
      decision,
536
      confidence,
537
      taskCategory,
538
      sampleSize: 0,
539
      dimensions: {},
540
      reasons,
541
      warnings,
542
      dataFreshness: {},
543
      requiredChallenges: challenges,
544
    };
545
  }
546
  private requireAgent(id: string): AgentIdentity {
547
    const value = this.agents.get(id);
548
    if (!value) throw new Error(`Agent not found: ${id}`);
549
    return value;
550
  }
551
  private requireConflict(id: string): Conflict {
552
    const value = this.conflicts.get(id);
553
    if (!value) throw new Error('Conflict not found');
554
    return value;
555
  }
556
}
557
558
export interface FactCheckProvider {
559
  check(claim: string, permission?: boolean): Promise<FactCheckResult>;
560
}
561
562
export class FixtureFactCheckProvider implements FactCheckProvider {
563
  constructor(
564
    private fixtures: Record<
565
      string,
566
      { status: FactCheckResult['status']; evidence: string[] }
567
    > = {},
568
  ) {}
569
  async check(claim: string, permission = true): Promise<FactCheckResult> {
570
    if (!permission)
571
      return result(
572
        claim,
573
        'objective',
574
        'insufficient_permission',
575
        1,
576
        [],
577
        'Grant evidence-source permission',
578
      );
579
    if (/\b(i think|i prefer|best|beautiful)\b/i.test(claim))
580
      return result(claim, 'subjective', 'not_fact_checkable', 1, [], 'Treat as an opinion');
581
    if (/\b(will|might|forecast|predict)\b/i.test(claim))
582
      return result(
583
        claim,
584
        'prediction',
585
        'not_fact_checkable',
586
        0.9,
587
        [],
588
        'Track as a prediction, not a fact',
589
      );
590
    const fixture = this.fixtures[claim];
591
    if (!fixture)
592
      return result(claim, 'objective', 'unverified', 0, [], 'Request authoritative evidence');
593
    return result(
594
      claim,
595
      'objective',
596
      fixture.status,
597
      0.95,
598
      fixture.evidence,
599
      fixture.status === 'contradicted' ? 'Challenge the claim with cited evidence' : 'Continue',
600
    );
601
  }
602
}
603
604
function result(
605
  claim: string,
606
  claimType: FactCheckResult['claimType'],
607
  status: FactCheckResult['status'],
608
  confidence: number,
609
  evidence: string[],
610
  action: string,
611
): FactCheckResult {
612
  return {
613
    claim,
614
    claimType,
615
    status,
616
    confidence,
617
    evidenceReferences: evidence,
618
    sourceAuthority: evidence.length ? 'authoritative_fixture' : 'none',
619
    sourceFreshness: new Date().toISOString(),
620
    contradictingEvidence: status === 'contradicted' ? evidence : [],
621
    suggestedNextAction: action,
622
  };
623
}
624
625
export { signNamed, signObject };