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