| 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 };
|