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

Viewing files at commit 3dc3c2c6da63Commit detailsView latest

packages/core/src/index.ts 618 lines · 22.3 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
    for (const party of Object.keys(parsed.signatures)) {
243
      const agent = this.requireAgent(party);
244
      if (!verifyNamed(parsed as unknown as Record<string, unknown>, party, agent.publicKey))
245
        throw new Error(`Invalid contract signature: ${party}`);
246
    }
247
    this.contracts.set(parsed.interactionId, parsed);
248
    this.store.append('contract', parsed.interactionId, parsed);
249
    return parsed;
250
  }
251
252
  assess(context: PolicyContext): RiskDecision {
253
    const envelope = TrustEnvelopeSchema.parse(context.envelope);
254
    const responder = this.agents.get(envelope.respondingAgentId);
255
    const requester = this.agents.get(envelope.requestingAgentId);
256
    const contract = this.contracts.get(envelope.interactionId);
257
    const hardFailures: string[] = [];
258
    if (
259
      !requester ||
260
      requester.revoked ||
261
      !verifyObject(envelope as unknown as Record<string, unknown>, requester.publicKey)
262
    )
263
      hardFailures.push('invalid_requester_signature');
264
    if (!responder || responder.revoked) hardFailures.push('invalid_or_revoked_responder');
265
    if (new Date(envelope.expiresAt) <= new Date()) hardFailures.push('expired_envelope');
266
    if (this.nonces.has(envelope.nonce)) hardFailures.push('replay_attempt');
267
    if (
268
      envelope.delegationId &&
269
      !this.verifyDelegation(envelope.delegationId, envelope.requestedCapability)
270
    )
271
      hardFailures.push('invalid_delegation');
272
    if (!contract || canonicalHash({ ...contract, signatures: {} }) !== envelope.contractHash)
273
      hardFailures.push('contract_mismatch');
274
    if (
275
      contract &&
276
      (!contract.allowedActions.includes(context.action) ||
277
        contract.prohibitedActions.includes(context.action))
278
    )
279
      hardFailures.push('action_outside_contract');
280
    if (
281
      contract &&
282
      (context.dataClasses ?? []).some((item) => contract.prohibitedData.includes(item))
283
    )
284
      hardFailures.push('prohibited_data');
285
    if (
286
      contract &&
287
      contract.humanApprovalRequirements.includes(context.action) &&
288
      !context.humanApproved
289
    )
290
      hardFailures.push('missing_human_approval');
291
    if (!hardFailures.includes('replay_attempt')) this.nonces.add(envelope.nonce);
292
    if (hardFailures.length)
293
      return this.decision('DENY', envelope.taskCategory, 1, hardFailures, [], []);
294
295
    const risk = this.getRisk(
296
      envelope.respondingAgentId,
297
      responder?.agentVersion ?? envelope.agentVersion,
298
      envelope.taskCategory,
299
    );
300
    return risk;
301
  }
302
303
  recordEvent(event: InteractionEvent): InteractionEvent {
304
    const parsed = InteractionEventSchema.parse(event);
305
    const agent = this.requireAgent(parsed.agentId);
306
    if (!verifyObject(parsed as unknown as Record<string, unknown>, agent.publicKey))
307
      throw new Error('Invalid event signature');
308
    if (canonicalHash(parsed.payload) !== parsed.payloadHash)
309
      throw new Error('Event payload hash mismatch');
310
    const existing = this.events.get(parsed.eventId);
311
    if (existing && canonicalHash(existing) !== canonicalHash(parsed))
312
      throw new Error('Conflicting duplicate event');
313
    this.events.set(parsed.eventId, parsed);
314
    this.store.append('event', parsed.eventId, parsed);
315
    return parsed;
316
  }
317
318
  createConflict(
319
    input: Omit<Conflict, 'conflictId' | 'status' | 'permissions'> & { participants: string[] },
320
  ): Conflict {
321
    const permissions = Object.fromEntries(input.participants.map((id) => [id, false]));
322
    const conflict: Conflict = {
323
      ...input,
324
      conflictId: randomUUID(),
325
      permissions,
326
      status: 'pending_consent',
327
    };
328
    delete (conflict as any).participants;
329
    this.conflicts.set(conflict.conflictId, conflict);
330
    this.store.append('conflict', `${conflict.conflictId}:created`, conflict);
331
    return conflict;
332
  }
333
334
  permitMediation(conflictId: string, agentId: string): Conflict {
335
    const conflict = this.requireConflict(conflictId);
336
    if (!(agentId in conflict.permissions)) throw new Error('Agent is not a conflict participant');
337
    conflict.permissions[agentId] = true;
338
    if (Object.values(conflict.permissions).every(Boolean)) conflict.status = 'open';
339
    this.store.append('conflict', `${conflict.conflictId}:permit:${agentId}`, conflict);
340
    return conflict;
341
  }
342
343
  resolveConflict(conflictId: string, resolution: string): Conflict {
344
    const conflict = this.requireConflict(conflictId);
345
    if (conflict.status !== 'open') throw new Error('Mutual mediation consent required');
346
    conflict.status = 'resolved';
347
    conflict.resolution = resolution;
348
    this.store.append('conflict', `${conflict.conflictId}:resolved`, conflict);
349
    return conflict;
350
  }
351
352
  submitReceipt(receipt: Receipt): Receipt {
353
    const parsed = ReceiptSchema.parse(receipt);
354
    const required = parsed.unilateral
355
      ? Object.keys(parsed.signatures).slice(0, 1)
356
      : parsed.participants;
357
    if (
358
      !required.length ||
359
      required.some((party) => {
360
        const agent = this.agents.get(party);
361
        return (
362
          !agent ||
363
          !verifyNamed(parsed as unknown as Record<string, unknown>, party, agent.publicKey)
364
        );
365
      })
366
    )
367
      throw new Error('Receipt signature verification failed');
368
    this.receipts.set(parsed.receiptId, parsed);
369
    this.store.append('receipt', parsed.receiptId, parsed);
370
    return parsed;
371
  }
372
373
  submitFeedback(item: Feedback): { revealed: boolean } {
374
    const parsed = FeedbackSchema.parse(item);
375
    const receipt = this.receipts.get(parsed.receiptId);
376
    const reviewer = this.agents.get(parsed.reviewerAgentId);
377
    if (
378
      !receipt ||
379
      !reviewer ||
380
      !receipt.participants.includes(parsed.reviewerAgentId) ||
381
      !receipt.participants.includes(parsed.subjectAgentId)
382
    )
383
      throw new Error('Feedback requires a valid participant receipt');
384
    if (!verifyObject(parsed as unknown as Record<string, unknown>, reviewer.publicKey))
385
      throw new Error('Invalid feedback signature');
386
    this.feedback.set(parsed.feedbackId, parsed);
387
    this.store.append('feedback', parsed.feedbackId, parsed);
388
    const related = [...this.feedback.values()].filter(
389
      (value) => value.receiptId === parsed.receiptId,
390
    );
391
    const revealed =
392
      receipt.unilateral ||
393
      new Set(related.map((value) => value.reviewerAgentId)).size >= receipt.participants.length;
394
    if (revealed) for (const feedback of related) this.applyFeedback(feedback, receipt);
395
    return { revealed };
396
  }
397
398
  getRisk(agentId: string, version: string, taskCategory: string): RiskDecision {
399
    const current = this.profiles.get(`${agentId}|${version}|${taskCategory}`);
400
    const otherVersions = [...this.profiles.values()].filter(
401
      (p) => p.agentId === agentId && p.taskCategory === taskCategory,
402
    );
403
    if (!current) {
404
      const continuity = otherVersions.length
405
        ? Math.min(
406
            0.2,
407
            Math.max(
408
              ...otherVersions.map((profile) => profile.sampleSize / (profile.sampleSize + 5)),
409
            ) * 0.25,
410
          )
411
        : 0;
412
      return this.decision(
413
        'CHALLENGE',
414
        taskCategory,
415
        continuity,
416
        ['limited_verified_history'],
417
        ['Agent version has limited task-specific evidence'],
418
        ['request_evidence'],
419
      );
420
    }
421
    const ageDays = Math.max(0, (Date.now() - Date.parse(current.updatedAt)) / 86_400_000);
422
    const freshness = Math.exp(-ageDays / 180);
423
    const quality =
424
      (current.completion +
425
        current.acceptance +
426
        current.specification +
427
        current.deadline +
428
        current.communication +
429
        current.evidence +
430
        current.scope +
431
        (1 - current.disputes)) /
432
      8;
433
    const confidence = Math.min(0.95, (current.sampleSize / (current.sampleSize + 5)) * freshness);
434
    const decision = quality >= 0.7 && confidence >= 0.25 ? 'ALLOW' : 'CHALLENGE';
435
    return {
436
      decision,
437
      confidence,
438
      taskCategory,
439
      sampleSize: current.sampleSize,
440
      dimensions: {
441
        completionReliability: current.completion,
442
        outputAcceptance: current.acceptance,
443
        contractAdherence: (current.specification + current.scope) / 2,
444
        deadlineReliability: current.deadline,
445
        communicationQuality: current.communication,
446
        evidenceQuality: current.evidence,
447
        disputeRate: current.disputes,
448
      },
449
      reasons: [`contextual_quality=${quality.toFixed(2)}`],
450
      warnings: confidence < 0.5 ? ['Limited sample size'] : [],
451
      dataFreshness: { behaviouralProfile: current.updatedAt },
452
      requiredChallenges: decision === 'CHALLENGE' ? ['request_evidence'] : [],
453
    };
454
  }
455
456
  setContributionConsent(agentId: string, enabled: boolean): void {
457
    this.requireAgent(agentId);
458
    const previous = this.contributionConsent.get(agentId);
459
    this.contributionConsent.set(
460
      agentId,
461
      enabled
462
        ? { enabled: true, grantedAt: new Date().toISOString() }
463
        : {
464
            enabled: false,
465
            grantedAt: previous?.grantedAt ?? new Date().toISOString(),
466
            revokedAt: new Date().toISOString(),
467
          },
468
    );
469
    this.store.append('consent', `${agentId}:${Date.now()}`, {
470
      agentId,
471
      ...this.contributionConsent.get(agentId),
472
    });
473
  }
474
475
  networkContribution(event: InteractionEvent): Record<string, unknown> | null {
476
    if (!this.contributionConsent.get(event.agentId)?.enabled || event.visibility === 'local_only')
477
      return null;
478
    return {
479
      eventId: event.eventId,
480
      interactionId: event.interactionId,
481
      eventType: event.eventType,
482
      agentId: event.agentId,
483
      agentVersion: event.agentVersion,
484
      timestamp: event.timestamp,
485
      visibility: event.visibility,
486
      provenance: event.provenance,
487
      payloadHash: event.payloadHash,
488
      evidenceRefs: event.evidenceRefs ?? [],
489
      signature: event.signature,
490
    };
491
  }
492
493
  private applyFeedback(feedback: Feedback, receipt: Receipt): void {
494
    const taskCategory = this.contracts.get(feedback.interactionId)?.taskCategory ?? 'unknown';
495
    const version = receipt.agentVersions[feedback.subjectAgentId] ?? 'unknown';
496
    const key = `${feedback.subjectAgentId}|${version}|${taskCategory}`;
497
    const previous = this.profiles.get(key);
498
    const n = previous?.sampleSize ?? 0;
499
    const next = (old: number | undefined, value: number) => ((old ?? 0) * n + value) / (n + 1);
500
    const profile = {
501
      agentId: feedback.subjectAgentId,
502
      agentVersion: version,
503
      taskCategory,
504
      sampleSize: n + 1,
505
      updatedAt: new Date().toISOString(),
506
      completion: next(previous?.completion, Number(feedback.taskCompleted)),
507
      acceptance: next(previous?.acceptance, Number(feedback.outputAccepted)),
508
      specification: next(previous?.specification, Number(feedback.specificationMatched)),
509
      deadline: next(previous?.deadline, Number(feedback.deadlineMet)),
510
      communication: next(previous?.communication, feedback.communicationQuality),
511
      evidence: next(previous?.evidence, Number(feedback.evidenceProvided)),
512
      scope: next(previous?.scope, Number(feedback.scopeRespected)),
513
      disputes: next(previous?.disputes, Number(feedback.disputeRaised)),
514
    };
515
    this.profiles.set(key, profile);
516
    this.store.append('profile', `${key}:${profile.sampleSize}`, profile);
517
  }
518
519
  private decision(
520
    decision: RiskDecision['decision'],
521
    taskCategory: string,
522
    confidence: number,
523
    reasons: string[],
524
    warnings: string[],
525
    challenges: string[],
526
  ): RiskDecision {
527
    return {
528
      decision,
529
      confidence,
530
      taskCategory,
531
      sampleSize: 0,
532
      dimensions: {},
533
      reasons,
534
      warnings,
535
      dataFreshness: {},
536
      requiredChallenges: challenges,
537
    };
538
  }
539
  private requireAgent(id: string): AgentIdentity {
540
    const value = this.agents.get(id);
541
    if (!value) throw new Error(`Agent not found: ${id}`);
542
    return value;
543
  }
544
  private requireConflict(id: string): Conflict {
545
    const value = this.conflicts.get(id);
546
    if (!value) throw new Error('Conflict not found');
547
    return value;
548
  }
549
}
550
551
export interface FactCheckProvider {
552
  check(claim: string, permission?: boolean): Promise<FactCheckResult>;
553
}
554
555
export class FixtureFactCheckProvider implements FactCheckProvider {
556
  constructor(
557
    private fixtures: Record<
558
      string,
559
      { status: FactCheckResult['status']; evidence: string[] }
560
    > = {},
561
  ) {}
562
  async check(claim: string, permission = true): Promise<FactCheckResult> {
563
    if (!permission)
564
      return result(
565
        claim,
566
        'objective',
567
        'insufficient_permission',
568
        1,
569
        [],
570
        'Grant evidence-source permission',
571
      );
572
    if (/\b(i think|i prefer|best|beautiful)\b/i.test(claim))
573
      return result(claim, 'subjective', 'not_fact_checkable', 1, [], 'Treat as an opinion');
574
    if (/\b(will|might|forecast|predict)\b/i.test(claim))
575
      return result(
576
        claim,
577
        'prediction',
578
        'not_fact_checkable',
579
        0.9,
580
        [],
581
        'Track as a prediction, not a fact',
582
      );
583
    const fixture = this.fixtures[claim];
584
    if (!fixture)
585
      return result(claim, 'objective', 'unverified', 0, [], 'Request authoritative evidence');
586
    return result(
587
      claim,
588
      'objective',
589
      fixture.status,
590
      0.95,
591
      fixture.evidence,
592
      fixture.status === 'contradicted' ? 'Challenge the claim with cited evidence' : 'Continue',
593
    );
594
  }
595
}
596
597
function result(
598
  claim: string,
599
  claimType: FactCheckResult['claimType'],
600
  status: FactCheckResult['status'],
601
  confidence: number,
602
  evidence: string[],
603
  action: string,
604
): FactCheckResult {
605
  return {
606
    claim,
607
    claimType,
608
    status,
609
    confidence,
610
    evidenceReferences: evidence,
611
    sourceAuthority: evidence.length ? 'authoritative_fixture' : 'none',
612
    sourceFreshness: new Date().toISOString(),
613
    contradictingEvidence: status === 'contradicted' ? evidence : [],
614
    suggestedNextAction: action,
615
  };
616
}
617
618
export { signNamed, signObject };