| 1 | import { randomUUID } from 'node:crypto';
|
| 2 | import {
|
| 3 | AgentIdentitySchema,
|
| 4 | CounterpartyBriefSchema,
|
| 5 | ContextualReliabilitySummarySchema,
|
| 6 | canonicalHash,
|
| 7 | createKeyPair,
|
| 8 | DelegationCredentialSchema,
|
| 9 | FeedbackSchema,
|
| 10 | FeedbackDimensionSchema,
|
| 11 | InteractionContractSchema,
|
| 12 | InteractionConclusionSchema,
|
| 13 | InteractionEventSchema,
|
| 14 | LearningEligibilityDecisionSchema,
|
| 15 | ReceiptSchema,
|
| 16 | signNamed,
|
| 17 | signObject,
|
| 18 | TrustEnvelopeSchema,
|
| 19 | verifyNamed,
|
| 20 | verifyObject,
|
| 21 | type AgentIdentity,
|
| 22 | type CounterpartyBrief,
|
| 23 | type ContextualReliabilitySummary,
|
| 24 | type DelegationCredential,
|
| 25 | type FactCheckResult,
|
| 26 | type Feedback,
|
| 27 | type InteractionContract,
|
| 28 | type InteractionConclusion,
|
| 29 | type InteractionCompletionReport,
|
| 30 | type InteractionFeedback,
|
| 31 | type InteractionEvent,
|
| 32 | type KeyPair,
|
| 33 | type LiveSessionInsight,
|
| 34 | type LearningEligibilityDecision,
|
| 35 | type PublicAgentCard,
|
| 36 | type Receipt,
|
| 37 | type RiskDecision,
|
| 38 | type TrustEnvelope,
|
| 39 | } from '../../protocol/src/index.js';
|
| 40 |
|
| 41 | const REQUIREMENT_STOP_WORDS = new Set([
|
| 42 | 'about',
|
| 43 | 'agent',
|
| 44 | 'available',
|
| 45 | 'from',
|
| 46 | 'have',
|
| 47 | 'into',
|
| 48 | 'must',
|
| 49 | 'provide',
|
| 50 | 'return',
|
| 51 | 'should',
|
| 52 | 'that',
|
| 53 | 'their',
|
| 54 | 'this',
|
| 55 | 'with',
|
| 56 | ]);
|
| 57 |
|
| 58 | function normalized(value: string): string {
|
| 59 | return value
|
| 60 | .trim()
|
| 61 | .toLowerCase()
|
| 62 | .replace(/[^a-z0-9]+/g, ' ')
|
| 63 | .trim();
|
| 64 | }
|
| 65 |
|
| 66 | function meaningfulTerms(value: string): string[] {
|
| 67 | return [
|
| 68 | ...new Set(
|
| 69 | normalized(value)
|
| 70 | .split(' ')
|
| 71 | .filter((term) => term.length >= 4 && !REQUIREMENT_STOP_WORDS.has(term)),
|
| 72 | ),
|
| 73 | ];
|
| 74 | }
|
| 75 |
|
| 76 | function assessDeclaration(
|
| 77 | requirement: string,
|
| 78 | declarations: string[],
|
| 79 | ): { status: 'match' | 'partial' | 'mismatch' | 'unknown'; reason: string; confidence: number } {
|
| 80 | const normalizedRequirement = normalized(requirement);
|
| 81 | const normalizedDeclarations = declarations.map(normalized);
|
| 82 | if (
|
| 83 | normalizedDeclarations.some(
|
| 84 | (declaration) => declaration === '*' || declaration === normalizedRequirement,
|
| 85 | )
|
| 86 | )
|
| 87 | return {
|
| 88 | status: 'match',
|
| 89 | reason: 'The counterparty explicitly declares this capability.',
|
| 90 | confidence: 0.95,
|
| 91 | };
|
| 92 |
|
| 93 | const terms = meaningfulTerms(requirement);
|
| 94 | const negative = normalizedDeclarations.find((declaration) =>
|
| 95 | terms.some(
|
| 96 | (term) =>
|
| 97 | declaration.includes(`no ${term}`) ||
|
| 98 | declaration.includes(`not ${term}`) ||
|
| 99 | declaration.includes(`cannot ${term}`),
|
| 100 | ),
|
| 101 | );
|
| 102 | if (negative)
|
| 103 | return {
|
| 104 | status: 'mismatch',
|
| 105 | reason: `The counterparty declares a conflicting limitation: ${negative}`,
|
| 106 | confidence: 0.95,
|
| 107 | };
|
| 108 |
|
| 109 | if (terms.length) {
|
| 110 | const declarationText = normalizedDeclarations.join(' ');
|
| 111 | const overlap = terms.filter((term) => declarationText.includes(term)).length / terms.length;
|
| 112 | if (overlap >= 0.6)
|
| 113 | return {
|
| 114 | status: 'match',
|
| 115 | reason: 'The counterparty declarations strongly match this requirement.',
|
| 116 | confidence: 0.8,
|
| 117 | };
|
| 118 | if (overlap > 0)
|
| 119 | return {
|
| 120 | status: 'partial',
|
| 121 | reason: 'The counterparty declarations only partially cover this requirement.',
|
| 122 | confidence: 0.65,
|
| 123 | };
|
| 124 | }
|
| 125 | return {
|
| 126 | status: 'unknown',
|
| 127 | reason: 'The published agent card does not establish this requirement.',
|
| 128 | confidence: 0.35,
|
| 129 | };
|
| 130 | }
|
| 131 |
|
| 132 | export function buildCounterpartyBrief(input: {
|
| 133 | interactionId: string;
|
| 134 | contractHash: string;
|
| 135 | contract: InteractionContract;
|
| 136 | recipientAgentId: string;
|
| 137 | subject: PublicAgentCard;
|
| 138 | historyInsights: LiveSessionInsight[];
|
| 139 | relevantSampleSize: number;
|
| 140 | historyConfidence: number;
|
| 141 | generatedAt?: string;
|
| 142 | expiresAt: string;
|
| 143 | }): CounterpartyBrief {
|
| 144 | const declarations = [
|
| 145 | input.subject.description,
|
| 146 | ...input.subject.capabilities,
|
| 147 | ...input.subject.limitations,
|
| 148 | ];
|
| 149 | const requirements = [
|
| 150 | {
|
| 151 | requirementId: 'task-category',
|
| 152 | requirement: input.contract.taskCategory,
|
| 153 | kind: 'capability' as const,
|
| 154 | },
|
| 155 | {
|
| 156 | requirementId: 'requested-outcome',
|
| 157 | requirement: input.contract.requestedOutcome,
|
| 158 | kind: 'scope' as const,
|
| 159 | },
|
| 160 | ...input.contract.allowedActions.map((requirement, index) => ({
|
| 161 | requirementId: `allowed-action-${index + 1}`,
|
| 162 | requirement,
|
| 163 | kind: 'capability' as const,
|
| 164 | })),
|
| 165 | ...input.contract.successCriteria.map((requirement, index) => ({
|
| 166 | requirementId: `success-criterion-${index + 1}`,
|
| 167 | requirement,
|
| 168 | kind: 'success_criterion' as const,
|
| 169 | })),
|
| 170 | ...input.contract.evidenceRequirements.map((requirement, index) => ({
|
| 171 | requirementId: `evidence-${index + 1}`,
|
| 172 | requirement,
|
| 173 | kind: 'evidence' as const,
|
| 174 | })),
|
| 175 | ].map((requirement) => ({
|
| 176 | ...requirement,
|
| 177 | ...assessDeclaration(requirement.requirement, declarations),
|
| 178 | sources: ['contract', 'self_declared'] as const,
|
| 179 | evidenceReferences: [input.subject.cardUrl],
|
| 180 | }));
|
| 181 | const requirementReferences = requirements
|
| 182 | .filter((requirement) => requirement.status !== 'match')
|
| 183 | .map((requirement) => requirement.requirementId);
|
| 184 | const insights = input.historyInsights.map((insight) => ({
|
| 185 | ...insight,
|
| 186 | requirementReferences:
|
| 187 | insight.requirementReferences.length > 0
|
| 188 | ? insight.requirementReferences
|
| 189 | : requirementReferences,
|
| 190 | }));
|
| 191 | const needsChallenge =
|
| 192 | input.relevantSampleSize === 0 ||
|
| 193 | requirements.some((requirement) => requirement.status === 'mismatch') ||
|
| 194 | insights.some((insight) => insight.severity === 'high');
|
| 195 | return CounterpartyBriefSchema.parse({
|
| 196 | briefId: randomUUID(),
|
| 197 | interactionId: input.interactionId,
|
| 198 | contractHash: input.contractHash,
|
| 199 | recipientAgentId: input.recipientAgentId,
|
| 200 | subjectAgentId: input.subject.agentId,
|
| 201 | taskCategory: input.contract.taskCategory,
|
| 202 | decision: needsChallenge ? 'CHALLENGE' : 'ALLOW',
|
| 203 | requirements,
|
| 204 | insights,
|
| 205 | relevantSampleSize: input.relevantSampleSize,
|
| 206 | historyConfidence: input.historyConfidence,
|
| 207 | subjectAgentVersion: input.subject.agentVersion,
|
| 208 | recommendedContractChanges: requirements
|
| 209 | .filter((requirement) => requirement.status === 'mismatch')
|
| 210 | .map((requirement) => `Resolve mismatch: ${requirement.requirement}`),
|
| 211 | generatedAt: input.generatedAt ?? new Date().toISOString(),
|
| 212 | expiresAt: input.expiresAt,
|
| 213 | });
|
| 214 | }
|
| 215 |
|
| 216 | export function buildInteractionConclusion(input: {
|
| 217 | interaction: { interactionId: string; termsHash: string; contract: InteractionContract };
|
| 218 | reports: InteractionCompletionReport[];
|
| 219 | feedback: InteractionFeedback[];
|
| 220 | conclusionId?: string;
|
| 221 | generatedAt?: string;
|
| 222 | lifecycle?: 'provisional' | 'final';
|
| 223 | pendingFeedbackAgentIds?: string[];
|
| 224 | peerReportStatus?: InteractionConclusion['peerReportStatus'];
|
| 225 | }): InteractionConclusion {
|
| 226 | const outcomes = input.reports.map((report) => report.outcome);
|
| 227 | const uniqueOutcomes = new Set(outcomes);
|
| 228 | const outcome =
|
| 229 | outcomes.length === 0
|
| 230 | ? ('cancelled' as const)
|
| 231 | : uniqueOutcomes.size === 1
|
| 232 | ? outcomes[0]!
|
| 233 | : outcomes.includes('failure')
|
| 234 | ? ('failure' as const)
|
| 235 | : ('partial' as const);
|
| 236 | const consensus =
|
| 237 | input.reports.length < 2
|
| 238 | ? ('unilateral' as const)
|
| 239 | : uniqueOutcomes.size === 1
|
| 240 | ? ('bilateral_agreement' as const)
|
| 241 | : outcomes.includes('partial')
|
| 242 | ? ('bilateral_partial_agreement' as const)
|
| 243 | : ('conflicting' as const);
|
| 244 | const statusRank = { met: 0, partially_met: 1, unknown: 2, missed: 3 } as const;
|
| 245 | const criteria = input.interaction.contract.successCriteria.map((criterion) => {
|
| 246 | const assessments = input.reports
|
| 247 | .flatMap((report) => report.criteria)
|
| 248 | .filter((assessment) => assessment.criterion === criterion);
|
| 249 | const selected = assessments.sort(
|
| 250 | (left, right) => statusRank[right.status] - statusRank[left.status],
|
| 251 | )[0];
|
| 252 | return {
|
| 253 | criterion,
|
| 254 | status: selected?.status ?? ('unknown' as const),
|
| 255 | ...(selected?.explanation ? { explanation: selected.explanation } : {}),
|
| 256 | evidenceReferences: [
|
| 257 | ...new Set(assessments.flatMap((assessment) => assessment.evidenceReferences)),
|
| 258 | ],
|
| 259 | };
|
| 260 | });
|
| 261 | const averageRatings = Object.fromEntries(
|
| 262 | FeedbackDimensionSchema.options.flatMap((dimension) => {
|
| 263 | const values = input.feedback
|
| 264 | .map((item) => item.ratings[dimension])
|
| 265 | .filter((value): value is number => typeof value === 'number');
|
| 266 | return values.length
|
| 267 | ? [[dimension, values.reduce((total, value) => total + value, 0) / values.length]]
|
| 268 | : [];
|
| 269 | }),
|
| 270 | );
|
| 271 | const reportedAgentIds = new Set(input.reports.map((report) => report.reportingAgentId));
|
| 272 | const missingReportAgentIds = input.interaction.contract.parties.filter(
|
| 273 | (agentId) => !reportedAgentIds.has(agentId),
|
| 274 | );
|
| 275 | const lifecycle = input.lifecycle ?? (missingReportAgentIds.length ? 'provisional' : 'final');
|
| 276 | const reportConfidence = input.reports.length
|
| 277 | ? input.reports.reduce((total, report) => total + report.confidence, 0) / input.reports.length
|
| 278 | : 0;
|
| 279 | const confidence =
|
| 280 | input.reports.length >= 2
|
| 281 | ? Math.min(0.95, reportConfidence * 0.9 + (uniqueOutcomes.size === 1 ? 0.05 : 0))
|
| 282 | : Math.min(0.55, reportConfidence * 0.6);
|
| 283 | const reportSummary = input.reports[0]?.summary;
|
| 284 | const summary =
|
| 285 | lifecycle === 'provisional'
|
| 286 | ? `Provisional one-sided outcome: ${reportSummary ?? 'one participant supplied a terminal report'}. The peer has not supplied an independent completion report.`
|
| 287 | : input.reports.length >= 2
|
| 288 | ? `${input.reports.length} independent completion reports were reconciled${input.feedback.length ? ` with ${input.feedback.length} eligible feedback response${input.feedback.length === 1 ? '' : 's'}` : ''}.`
|
| 289 | : `Final unilateral outcome: ${reportSummary ?? 'one participant supplied the available terminal report'}. The peer response window closed without an independent completion report.`;
|
| 290 | return InteractionConclusionSchema.parse({
|
| 291 | conclusionId: input.conclusionId ?? randomUUID(),
|
| 292 | interactionId: input.interaction.interactionId,
|
| 293 | contractHash: input.interaction.termsHash,
|
| 294 | outcome,
|
| 295 | consensus,
|
| 296 | lifecycle,
|
| 297 | confidence,
|
| 298 | missingReportAgentIds,
|
| 299 | pendingFeedbackAgentIds: input.pendingFeedbackAgentIds ?? [],
|
| 300 | peerReportStatus:
|
| 301 | input.reports.length >= 2 ? 'received' : (input.peerReportStatus ?? 'awaiting'),
|
| 302 | summary: summary.slice(0, 2000),
|
| 303 | criteria,
|
| 304 | reportIds: input.reports.map((report) => report.reportId),
|
| 305 | feedbackIds: input.feedback.map((item) => item.feedbackId),
|
| 306 | averageRatings,
|
| 307 | evidenceReferences: [...new Set(input.reports.flatMap((report) => report.evidenceReferences))],
|
| 308 | generatedAt: input.generatedAt ?? new Date().toISOString(),
|
| 309 | });
|
| 310 | }
|
| 311 |
|
| 312 | function extremeFeedbackPenalty(feedback: InteractionFeedback): number {
|
| 313 | const ratings = Object.values(feedback.ratings).filter(
|
| 314 | (value): value is number => typeof value === 'number',
|
| 315 | );
|
| 316 | if (!ratings.length) return 0.5;
|
| 317 | const extremeShare =
|
| 318 | ratings.filter((value) => value === 0 || value === 1).length / ratings.length;
|
| 319 | return extremeShare === 1 && feedback.evidenceReferences.length === 0 ? 0.6 : 1;
|
| 320 | }
|
| 321 |
|
| 322 | export function evaluateLearningEligibility(input: {
|
| 323 | interactionId: string;
|
| 324 | reports: InteractionCompletionReport[];
|
| 325 | feedback: InteractionFeedback[];
|
| 326 | consensus: InteractionConclusion['consensus'];
|
| 327 | contributionMode: 'local_only' | 'network_aggregate';
|
| 328 | reviewerCredibility?: Record<string, number>;
|
| 329 | manipulationSignals?: string[];
|
| 330 | decisionId?: string;
|
| 331 | decidedAt?: string;
|
| 332 | }): LearningEligibilityDecision {
|
| 333 | const attestedReports = input.reports.filter((report) => report.platformAttestation);
|
| 334 | const attestedFeedback = input.feedback.filter((item) => item.platformAttestation);
|
| 335 | const bilateralReports =
|
| 336 | new Set(attestedReports.map((report) => report.reportingAgentId)).size >= 2;
|
| 337 | const evidenceReferences = [
|
| 338 | ...new Set([
|
| 339 | ...attestedReports.flatMap((report) => report.evidenceReferences),
|
| 340 | ...attestedFeedback.flatMap((item) => item.evidenceReferences),
|
| 341 | ]),
|
| 342 | ];
|
| 343 | const evidenceBacked = bilateralReports || evidenceReferences.length > 0;
|
| 344 | const eligible = attestedReports.length > 0;
|
| 345 | const extremeWithoutEvidence = attestedFeedback.filter(
|
| 346 | (item) => extremeFeedbackPenalty(item) < 1,
|
| 347 | );
|
| 348 | const mirroredFeedback =
|
| 349 | attestedFeedback.length === 2 &&
|
| 350 | canonicalHash(attestedFeedback[0]!.ratings) === canonicalHash(attestedFeedback[1]!.ratings) &&
|
| 351 | evidenceReferences.length === 0;
|
| 352 | const manipulationSignals = [
|
| 353 | ...new Set([
|
| 354 | ...(input.manipulationSignals ?? []),
|
| 355 | ...(extremeWithoutEvidence.length ? ['extreme_feedback_without_evidence'] : []),
|
| 356 | ...(mirroredFeedback ? ['reciprocal_rating_mirroring'] : []),
|
| 357 | ]),
|
| 358 | ];
|
| 359 | const contributionMode =
|
| 360 | input.contributionMode === 'network_aggregate' &&
|
| 361 | (!bilateralReports || manipulationSignals.length > 0)
|
| 362 | ? ('local_only' as const)
|
| 363 | : input.contributionMode;
|
| 364 | const reviewerWeight = attestedFeedback.length
|
| 365 | ? attestedFeedback.reduce((total, item) => {
|
| 366 | const credibility = Math.max(
|
| 367 | 0.1,
|
| 368 | Math.min(1, input.reviewerCredibility?.[item.reviewerAgentId] ?? 0.5),
|
| 369 | );
|
| 370 | return total + item.confidence * credibility * extremeFeedbackPenalty(item);
|
| 371 | }, 0) / attestedFeedback.length
|
| 372 | : 0;
|
| 373 | const sampleWeight = eligible
|
| 374 | ? Math.max(
|
| 375 | 0.1,
|
| 376 | Math.min(
|
| 377 | 1,
|
| 378 | 0.1 +
|
| 379 | (bilateralReports ? 0.35 : 0) +
|
| 380 | Math.min(0.2, reviewerWeight * 0.2) +
|
| 381 | (evidenceReferences.length ? 0.1 : 0) -
|
| 382 | (input.consensus === 'conflicting' ? 0.2 : 0) -
|
| 383 | Math.min(0.25, manipulationSignals.length * 0.12),
|
| 384 | ),
|
| 385 | )
|
| 386 | : 0;
|
| 387 | const reasons = [
|
| 388 | attestedReports.length
|
| 389 | ? `${attestedReports.length} platform-attested completion report(s)`
|
| 390 | : 'No attested completion report',
|
| 391 | bilateralReports
|
| 392 | ? 'Bilateral reports provide outcome corroboration'
|
| 393 | : 'Outcome is not bilaterally corroborated',
|
| 394 | attestedFeedback.length
|
| 395 | ? `${attestedFeedback.length} eligible feedback response(s)`
|
| 396 | : 'No eligible feedback response',
|
| 397 | evidenceReferences.length
|
| 398 | ? `${evidenceReferences.length} permitted evidence reference(s)`
|
| 399 | : 'No permitted external evidence reference',
|
| 400 | !bilateralReports && eligible
|
| 401 | ? 'One-sided structured evidence is retained locally at reduced weight'
|
| 402 | : evidenceBacked
|
| 403 | ? 'Evidence threshold supports the configured contribution mode'
|
| 404 | : 'Evidence is insufficient for shared-network contribution',
|
| 405 | input.consensus === 'conflicting'
|
| 406 | ? 'Conflicting reports reduce sample weight'
|
| 407 | : 'No report-conflict penalty',
|
| 408 | ...(manipulationSignals.length
|
| 409 | ? [`Manipulation checks reduced weight: ${manipulationSignals.join(', ')}`]
|
| 410 | : ['No feedback-manipulation signal detected']),
|
| 411 | ];
|
| 412 | return LearningEligibilityDecisionSchema.parse({
|
| 413 | decisionId: input.decisionId ?? randomUUID(),
|
| 414 | interactionId: input.interactionId,
|
| 415 | eligible,
|
| 416 | reasons,
|
| 417 | sampleWeight,
|
| 418 | reportIds: attestedReports.map((report) => report.reportId),
|
| 419 | feedbackIds: attestedFeedback.map((item) => item.feedbackId),
|
| 420 | evidenceReferences,
|
| 421 | manipulationSignals,
|
| 422 | contributionMode,
|
| 423 | structuredDataOnly: true,
|
| 424 | decidedAt: input.decidedAt ?? new Date().toISOString(),
|
| 425 | });
|
| 426 | }
|
| 427 |
|
| 428 | export type BehaviouralObservations = Partial<
|
| 429 | Record<
|
| 430 | | 'completion'
|
| 431 | | 'acceptance'
|
| 432 | | 'specification'
|
| 433 | | 'deadline'
|
| 434 | | 'communication'
|
| 435 | | 'evidence'
|
| 436 | | 'scope'
|
| 437 | | 'correction'
|
| 438 | | 'limitations'
|
| 439 | | 'disputes',
|
| 440 | number
|
| 441 | >
|
| 442 | >;
|
| 443 |
|
| 444 | export const BEHAVIOURAL_DIMENSIONS = [
|
| 445 | 'completion',
|
| 446 | 'acceptance',
|
| 447 | 'specification',
|
| 448 | 'deadline',
|
| 449 | 'communication',
|
| 450 | 'evidence',
|
| 451 | 'scope',
|
| 452 | 'correction',
|
| 453 | 'limitations',
|
| 454 | 'disputes',
|
| 455 | ] as const;
|
| 456 |
|
| 457 | export type BehaviouralDimension = (typeof BEHAVIOURAL_DIMENSIONS)[number];
|
| 458 |
|
| 459 | const RELIABILITY_WEIGHTS: Record<BehaviouralDimension, number> = {
|
| 460 | completion: 0.16,
|
| 461 | acceptance: 0.12,
|
| 462 | specification: 0.12,
|
| 463 | deadline: 0.1,
|
| 464 | communication: 0.08,
|
| 465 | evidence: 0.12,
|
| 466 | scope: 0.1,
|
| 467 | correction: 0.08,
|
| 468 | limitations: 0.08,
|
| 469 | disputes: 0.04,
|
| 470 | };
|
| 471 |
|
| 472 | const DIMENSION_RISK_LABELS: Record<BehaviouralDimension, string> = {
|
| 473 | completion: 'Outcomes are often incomplete.',
|
| 474 | acceptance: 'Delivered outcomes are not consistently accepted.',
|
| 475 | specification: 'Requirements are not consistently satisfied.',
|
| 476 | deadline: 'Delivery timing is unreliable.',
|
| 477 | communication: 'Communication quality is inconsistent.',
|
| 478 | evidence: 'Claims are not consistently supported by evidence.',
|
| 479 | scope: 'The agent has a pattern of leaving agreed scope.',
|
| 480 | correction: 'The agent does not consistently correct mistakes well.',
|
| 481 | limitations: 'The agent does not consistently disclose limitations early.',
|
| 482 | disputes: 'The interaction history contains elevated disagreement.',
|
| 483 | };
|
| 484 |
|
| 485 | export function summarizeContextualReliability(input: {
|
| 486 | profile: ContextualBehaviouralProfile & {
|
| 487 | agentId: string;
|
| 488 | agentVersion: string;
|
| 489 | taskCategory: string;
|
| 490 | };
|
| 491 | deltas?: Array<{ dimensionDeltas: Record<string, number>; appliedAt: string }>;
|
| 492 | currentAgentVersion?: string;
|
| 493 | now?: string;
|
| 494 | }): ContextualReliabilitySummary {
|
| 495 | const currentAgentVersion = input.currentAgentVersion ?? input.profile.agentVersion;
|
| 496 | const versionChanged = currentAgentVersion !== input.profile.agentVersion;
|
| 497 | const ageDays = Math.max(
|
| 498 | 0,
|
| 499 | (Date.parse(input.now ?? new Date().toISOString()) - Date.parse(input.profile.updatedAt)) /
|
| 500 | 86_400_000,
|
| 501 | );
|
| 502 | const freshness = Math.exp(-ageDays / 180);
|
| 503 | const effectiveSampleSize = Math.max(
|
| 504 | 0,
|
| 505 | input.profile.effectiveSampleSize ?? input.profile.sampleSize,
|
| 506 | );
|
| 507 | const confidenceValue = Math.min(
|
| 508 | 0.98,
|
| 509 | (effectiveSampleSize / (effectiveSampleSize + 4)) * freshness * (versionChanged ? 0.35 : 1),
|
| 510 | );
|
| 511 | const confidenceLevel =
|
| 512 | confidenceValue >= 0.67 ? 'high' : confidenceValue >= 0.34 ? 'medium' : 'low';
|
| 513 | const legacyDimensions = new Set<BehaviouralDimension>([
|
| 514 | 'completion',
|
| 515 | 'acceptance',
|
| 516 | 'specification',
|
| 517 | 'deadline',
|
| 518 | 'communication',
|
| 519 | 'evidence',
|
| 520 | 'scope',
|
| 521 | 'disputes',
|
| 522 | ]);
|
| 523 | const measuredDimensions = new Set(
|
| 524 | BEHAVIOURAL_DIMENSIONS.filter((dimension) => {
|
| 525 | const explicit = input.profile.dimensionSampleSizes?.[dimension];
|
| 526 | return typeof explicit === 'number' ? explicit > 0 : legacyDimensions.has(dimension);
|
| 527 | }),
|
| 528 | );
|
| 529 | const dimensionValues = Object.fromEntries(
|
| 530 | BEHAVIOURAL_DIMENSIONS.map((dimension) => [
|
| 531 | dimension,
|
| 532 | dimension === 'disputes'
|
| 533 | ? 1 - Math.max(0, Math.min(1, Number(input.profile.disputes ?? 0.5)))
|
| 534 | : Math.max(0, Math.min(1, Number(input.profile[dimension] ?? 0.5))),
|
| 535 | ]),
|
| 536 | ) as Record<BehaviouralDimension, number>;
|
| 537 | const measuredWeight = BEHAVIOURAL_DIMENSIONS.reduce(
|
| 538 | (total, dimension) =>
|
| 539 | total + (measuredDimensions.has(dimension) ? RELIABILITY_WEIGHTS[dimension] : 0),
|
| 540 | 0,
|
| 541 | );
|
| 542 | const score =
|
| 543 | BEHAVIOURAL_DIMENSIONS.reduce(
|
| 544 | (total, dimension) =>
|
| 545 | total +
|
| 546 | (measuredDimensions.has(dimension)
|
| 547 | ? dimensionValues[dimension] * RELIABILITY_WEIGHTS[dimension]
|
| 548 | : 0),
|
| 549 | 0,
|
| 550 | ) / Math.max(Number.EPSILON, measuredWeight);
|
| 551 | const recentDeltas = [...(input.deltas ?? [])]
|
| 552 | .sort((left, right) => Date.parse(right.appliedAt) - Date.parse(left.appliedAt))
|
| 553 | .slice(0, 5);
|
| 554 | const trendDelta = recentDeltas.length
|
| 555 | ? recentDeltas.reduce((total, delta) => {
|
| 556 | const weighted = BEHAVIOURAL_DIMENSIONS.reduce((sum, dimension) => {
|
| 557 | const raw = Number(delta.dimensionDeltas[dimension] ?? 0);
|
| 558 | return sum + (dimension === 'disputes' ? -raw : raw) * RELIABILITY_WEIGHTS[dimension];
|
| 559 | }, 0);
|
| 560 | return total + weighted;
|
| 561 | }, 0) / recentDeltas.length
|
| 562 | : 0;
|
| 563 | const ordered = BEHAVIOURAL_DIMENSIONS.filter((dimension) =>
|
| 564 | measuredDimensions.has(dimension),
|
| 565 | ).map((dimension) => ({ dimension, score: dimensionValues[dimension] }));
|
| 566 | const strengths = ordered
|
| 567 | .filter((item) => item.score >= 0.65)
|
| 568 | .sort((left, right) => right.score - left.score)
|
| 569 | .slice(0, 3);
|
| 570 | const risks = ordered
|
| 571 | .filter((item) => item.score < 0.6)
|
| 572 | .sort((left, right) => left.score - right.score)
|
| 573 | .slice(0, 3)
|
| 574 | .map((item) => ({ ...item, reason: DIMENSION_RISK_LABELS[item.dimension] }));
|
| 575 | return ContextualReliabilitySummarySchema.parse({
|
| 576 | agentId: input.profile.agentId,
|
| 577 | agentVersion: input.profile.agentVersion,
|
| 578 | taskCategory: input.profile.taskCategory,
|
| 579 | score,
|
| 580 | confidence: {
|
| 581 | level: confidenceLevel,
|
| 582 | value: confidenceValue,
|
| 583 | evidenceCount: Math.max(0, Math.floor(input.profile.sampleSize)),
|
| 584 | effectiveSampleSize,
|
| 585 | },
|
| 586 | trend: {
|
| 587 | direction: trendDelta > 0.015 ? 'improving' : trendDelta < -0.015 ? 'declining' : 'stable',
|
| 588 | delta: Math.max(-1, Math.min(1, trendDelta)),
|
| 589 | },
|
| 590 | strengths,
|
| 591 | risks,
|
| 592 | versionStatus: {
|
| 593 | currentVersion: currentAgentVersion,
|
| 594 | evidenceVersion: input.profile.agentVersion,
|
| 595 | status: versionChanged ? 'reduced_confidence' : 'current',
|
| 596 | },
|
| 597 | source: 'private_verified_history',
|
| 598 | updatedAt: input.profile.updatedAt,
|
| 599 | });
|
| 600 | }
|
| 601 |
|
| 602 | export type ContextualBehaviouralProfile = Record<BehaviouralDimension, number> & {
|
| 603 | sampleSize: number;
|
| 604 | effectiveSampleSize: number;
|
| 605 | dimensionSampleSizes?: Partial<Record<BehaviouralDimension, number>>;
|
| 606 | updatedAt: string;
|
| 607 | };
|
| 608 |
|
| 609 | export function updateContextualBehaviouralProfile(input: {
|
| 610 | current?: Partial<ContextualBehaviouralProfile>;
|
| 611 | observations: BehaviouralObservations;
|
| 612 | sampleWeight: number;
|
| 613 | appliedAt?: string;
|
| 614 | decayDays?: number;
|
| 615 | }): { profile: ContextualBehaviouralProfile; dimensionDeltas: Record<string, number> } {
|
| 616 | const appliedAt = input.appliedAt ?? new Date().toISOString();
|
| 617 | const decayDays = input.decayDays ?? 180;
|
| 618 | const priorAgeDays = input.current?.updatedAt
|
| 619 | ? Math.max(0, (Date.parse(appliedAt) - Date.parse(input.current.updatedAt)) / 86_400_000)
|
| 620 | : 0;
|
| 621 | const priorEffectiveSampleSize = Math.max(
|
| 622 | 0,
|
| 623 | input.current?.effectiveSampleSize ?? input.current?.sampleSize ?? 0,
|
| 624 | );
|
| 625 | const decayedPriorWeight =
|
| 626 | priorEffectiveSampleSize * Math.exp(-priorAgeDays / Math.max(1, decayDays));
|
| 627 | const sampleWeight = Math.max(0, Math.min(1, input.sampleWeight));
|
| 628 | const hasObservation = BEHAVIOURAL_DIMENSIONS.some(
|
| 629 | (dimension) => typeof input.observations[dimension] === 'number',
|
| 630 | );
|
| 631 | const nextEffectiveSampleSize = decayedPriorWeight + (hasObservation ? sampleWeight : 0);
|
| 632 | const dimensionDeltas: Record<string, number> = {};
|
| 633 | const dimensions = Object.fromEntries(
|
| 634 | BEHAVIOURAL_DIMENSIONS.map((dimension) => {
|
| 635 | const previous = Math.max(0, Math.min(1, input.current?.[dimension] ?? 0.5));
|
| 636 | const observed = input.observations[dimension];
|
| 637 | if (typeof observed !== 'number' || sampleWeight === 0) return [dimension, previous];
|
| 638 | const next =
|
| 639 | (previous * decayedPriorWeight + Math.max(0, Math.min(1, observed)) * sampleWeight) /
|
| 640 | Math.max(Number.EPSILON, nextEffectiveSampleSize);
|
| 641 | dimensionDeltas[dimension] = next - previous;
|
| 642 | return [dimension, next];
|
| 643 | }),
|
| 644 | ) as Record<BehaviouralDimension, number>;
|
| 645 | const legacyDimensions = new Set<BehaviouralDimension>([
|
| 646 | 'completion',
|
| 647 | 'acceptance',
|
| 648 | 'specification',
|
| 649 | 'deadline',
|
| 650 | 'communication',
|
| 651 | 'evidence',
|
| 652 | 'scope',
|
| 653 | 'disputes',
|
| 654 | ]);
|
| 655 | const dimensionSampleSizes = Object.fromEntries(
|
| 656 | BEHAVIOURAL_DIMENSIONS.map((dimension) => {
|
| 657 | const prior =
|
| 658 | input.current?.dimensionSampleSizes?.[dimension] ??
|
| 659 | (legacyDimensions.has(dimension) ? priorEffectiveSampleSize : 0);
|
| 660 | const decayed = prior * Math.exp(-priorAgeDays / Math.max(1, decayDays));
|
| 661 | return [
|
| 662 | dimension,
|
| 663 | decayed + (typeof input.observations[dimension] === 'number' ? sampleWeight : 0),
|
| 664 | ];
|
| 665 | }),
|
| 666 | ) as Record<BehaviouralDimension, number>;
|
| 667 | return {
|
| 668 | profile: {
|
| 669 | ...dimensions,
|
| 670 | sampleSize:
|
| 671 | Math.max(0, Math.floor(input.current?.sampleSize ?? 0)) +
|
| 672 | (hasObservation && sampleWeight > 0 ? 1 : 0),
|
| 673 | effectiveSampleSize: nextEffectiveSampleSize,
|
| 674 | dimensionSampleSizes,
|
| 675 | updatedAt: appliedAt,
|
| 676 | },
|
| 677 | dimensionDeltas,
|
| 678 | };
|
| 679 | }
|
| 680 |
|
| 681 | export function deriveBehaviouralObservations(input: {
|
| 682 | subjectAgentId: string;
|
| 683 | reviewerAgentId?: string;
|
| 684 | reports: InteractionCompletionReport[];
|
| 685 | reviewerFeedback?: InteractionFeedback;
|
| 686 | conclusion: InteractionConclusion;
|
| 687 | }): BehaviouralObservations {
|
| 688 | const subjectReport = input.reviewerAgentId
|
| 689 | ? input.reports.find(
|
| 690 | (report) =>
|
| 691 | report.reportingAgentId === input.reviewerAgentId &&
|
| 692 | report.counterpartyAgentId === input.subjectAgentId,
|
| 693 | )
|
| 694 | : (input.reports.find((report) => report.counterpartyAgentId === input.subjectAgentId) ??
|
| 695 | input.reports.find((report) => report.reportingAgentId === input.subjectAgentId));
|
| 696 | const criteria =
|
| 697 | subjectReport?.criteria.filter((criterion) => criterion.status !== 'unknown') ?? [];
|
| 698 | const criterionValue = (status: (typeof criteria)[number]['status']) =>
|
| 699 | status === 'met' ? 1 : status === 'partially_met' ? 0.5 : 0;
|
| 700 | const ratings = input.reviewerFeedback?.ratings;
|
| 701 | const observation: BehaviouralObservations = {
|
| 702 | ...(subjectReport
|
| 703 | ? {
|
| 704 | completion:
|
| 705 | subjectReport.outcome === 'success' ? 1 : subjectReport.outcome === 'partial' ? 0.5 : 0,
|
| 706 | }
|
| 707 | : {}),
|
| 708 | ...(criteria.length
|
| 709 | ? {
|
| 710 | specification:
|
| 711 | criteria.reduce((total, criterion) => total + criterionValue(criterion.status), 0) /
|
| 712 | criteria.length,
|
| 713 | }
|
| 714 | : {}),
|
| 715 | ...(typeof ratings?.outcome_satisfaction === 'number'
|
| 716 | ? { acceptance: ratings.outcome_satisfaction }
|
| 717 | : {}),
|
| 718 | ...(typeof ratings?.timeliness === 'number' ? { deadline: ratings.timeliness } : {}),
|
| 719 | ...(typeof ratings?.communication === 'number' ? { communication: ratings.communication } : {}),
|
| 720 | ...(typeof ratings?.evidence_quality === 'number'
|
| 721 | ? { evidence: ratings.evidence_quality }
|
| 722 | : {}),
|
| 723 | ...(typeof ratings?.scope_adherence === 'number' ? { scope: ratings.scope_adherence } : {}),
|
| 724 | ...(typeof ratings?.correction_handling === 'number'
|
| 725 | ? { correction: ratings.correction_handling }
|
| 726 | : {}),
|
| 727 | ...(typeof ratings?.limitation_disclosure === 'number'
|
| 728 | ? { limitations: ratings.limitation_disclosure }
|
| 729 | : {}),
|
| 730 | ...(subjectReport || input.reviewerFeedback
|
| 731 | ? { disputes: input.conclusion.consensus === 'conflicting' ? 1 : 0 }
|
| 732 | : {}),
|
| 733 | };
|
| 734 | return Object.fromEntries(
|
| 735 | Object.entries(observation).map(([key, value]) => [key, Math.max(0, Math.min(1, value))]),
|
| 736 | );
|
| 737 | }
|
| 738 |
|
| 739 | export interface AuditStore {
|
| 740 | append(kind: string, id: string, value: unknown): void;
|
| 741 | list(kind: string): unknown[];
|
| 742 | }
|
| 743 |
|
| 744 | export class MemoryAuditStore implements AuditStore {
|
| 745 | private rows: { kind: string; id: string; value: unknown }[] = [];
|
| 746 | append(kind: string, id: string, value: unknown): void {
|
| 747 | const old = this.rows.find((row) => row.kind === kind && row.id === id);
|
| 748 | if (old && canonicalHash(old.value) !== canonicalHash(value))
|
| 749 | throw new Error(`Conflicting duplicate ${kind}:${id}`);
|
| 750 | if (!old) this.rows.push({ kind, id, value: structuredClone(value) });
|
| 751 | }
|
| 752 | list(kind: string): unknown[] {
|
| 753 | return this.rows.filter((row) => row.kind === kind).map((row) => structuredClone(row.value));
|
| 754 | }
|
| 755 | }
|
| 756 |
|
| 757 | export function createIdentity(input: {
|
| 758 | agentId: string;
|
| 759 | operatorRef: string;
|
| 760 | version?: string;
|
| 761 | capabilities: string[];
|
| 762 | assurance?: AgentIdentity['assurance'];
|
| 763 | parentAgentId?: string;
|
| 764 | rootControllerId?: string;
|
| 765 | }): { identity: AgentIdentity; keyPair: KeyPair } {
|
| 766 | const keyPair = createKeyPair(`${input.agentId}#1`);
|
| 767 | const identity: Record<string, unknown> = {
|
| 768 | protocolVersion: '0.1',
|
| 769 | agentId: input.agentId,
|
| 770 | publicKey: keyPair.publicKey,
|
| 771 | keyId: keyPair.keyId,
|
| 772 | operatorRef: input.operatorRef,
|
| 773 | assurance: input.assurance ?? 'pseudonymous',
|
| 774 | agentVersion: input.version ?? '1.0.0',
|
| 775 | capabilities: input.capabilities,
|
| 776 | supportedProtocols: ['A2A/1.0', 'OpenClasp/0.1'],
|
| 777 | ...(input.parentAgentId ? { parentAgentId: input.parentAgentId } : {}),
|
| 778 | rootControllerId: input.rootControllerId ?? input.agentId,
|
| 779 | revoked: false,
|
| 780 | provenance: 'cryptographically_verified',
|
| 781 | createdAt: new Date().toISOString(),
|
| 782 | };
|
| 783 | return { identity: AgentIdentitySchema.parse(signObject(identity, keyPair)), keyPair };
|
| 784 | }
|
| 785 |
|
| 786 | export type PolicyContext = {
|
| 787 | envelope: TrustEnvelope;
|
| 788 | action: string;
|
| 789 | dataClasses?: string[];
|
| 790 | humanApproved?: boolean;
|
| 791 | };
|
| 792 |
|
| 793 | export type Conflict = {
|
| 794 | conflictId: string;
|
| 795 | interactionId: string;
|
| 796 | issue: string;
|
| 797 | positions: Record<string, string>;
|
| 798 | evidence: string[];
|
| 799 | contractClauses: string[];
|
| 800 | missingInformation: string[];
|
| 801 | possibleResolutions: string[];
|
| 802 | permissions: Record<string, boolean>;
|
| 803 | status: 'pending_consent' | 'open' | 'resolved';
|
| 804 | resolution?: string;
|
| 805 | };
|
| 806 |
|
| 807 | type ProfileAggregate = {
|
| 808 | agentId: string;
|
| 809 | agentVersion: string;
|
| 810 | taskCategory: string;
|
| 811 | sampleSize: number;
|
| 812 | updatedAt: string;
|
| 813 | completion: number;
|
| 814 | acceptance: number;
|
| 815 | specification: number;
|
| 816 | deadline: number;
|
| 817 | communication: number;
|
| 818 | evidence: number;
|
| 819 | scope: number;
|
| 820 | disputes: number;
|
| 821 | };
|
| 822 |
|
| 823 | export class TrustEngine {
|
| 824 | readonly agents = new Map<string, AgentIdentity>();
|
| 825 | readonly delegations = new Map<string, DelegationCredential>();
|
| 826 | readonly contracts = new Map<string, InteractionContract>();
|
| 827 | readonly events = new Map<string, InteractionEvent>();
|
| 828 | readonly receipts = new Map<string, Receipt>();
|
| 829 | readonly conflicts = new Map<string, Conflict>();
|
| 830 | readonly feedback = new Map<string, Feedback>();
|
| 831 | readonly profiles = new Map<string, ProfileAggregate>();
|
| 832 | readonly contributionConsent = new Map<
|
| 833 | string,
|
| 834 | { enabled: boolean; grantedAt: string; revokedAt?: string }
|
| 835 | >();
|
| 836 | private readonly nonces = new Set<string>();
|
| 837 |
|
| 838 | constructor(readonly store: AuditStore = new MemoryAuditStore()) {
|
| 839 | for (const value of store.list('agent') as AgentIdentity[])
|
| 840 | this.agents.set(value.agentId, value);
|
| 841 | for (const value of store.list('delegation') as DelegationCredential[])
|
| 842 | this.delegations.set(value.delegationId, value);
|
| 843 | for (const value of store.list('contract') as InteractionContract[])
|
| 844 | this.contracts.set(value.interactionId, value);
|
| 845 | for (const value of store.list('event') as InteractionEvent[])
|
| 846 | this.events.set(value.eventId, value);
|
| 847 | for (const value of store.list('receipt') as Receipt[])
|
| 848 | this.receipts.set(value.receiptId, value);
|
| 849 | for (const value of store.list('feedback') as Feedback[])
|
| 850 | this.feedback.set(value.feedbackId, value);
|
| 851 | for (const value of store.list('profile') as ProfileAggregate[])
|
| 852 | this.profiles.set(`${value.agentId}|${value.agentVersion}|${value.taskCategory}`, value);
|
| 853 | for (const value of store.list('conflict') as Conflict[])
|
| 854 | this.conflicts.set(value.conflictId, value);
|
| 855 | for (const value of store.list('consent') as {
|
| 856 | agentId: string;
|
| 857 | enabled: boolean;
|
| 858 | grantedAt: string;
|
| 859 | revokedAt?: string;
|
| 860 | }[])
|
| 861 | this.contributionConsent.set(value.agentId, value);
|
| 862 | for (const value of store.list('revocation') as {
|
| 863 | agentId?: string;
|
| 864 | delegationId?: string;
|
| 865 | }[]) {
|
| 866 | if (value.agentId && this.agents.has(value.agentId))
|
| 867 | this.agents.set(value.agentId, { ...this.agents.get(value.agentId)!, revoked: true });
|
| 868 | if (value.delegationId && this.delegations.has(value.delegationId))
|
| 869 | this.delegations.set(value.delegationId, {
|
| 870 | ...this.delegations.get(value.delegationId)!,
|
| 871 | revoked: true,
|
| 872 | });
|
| 873 | }
|
| 874 | }
|
| 875 |
|
| 876 | registerAgent(identity: AgentIdentity): AgentIdentity {
|
| 877 | const parsed = AgentIdentitySchema.parse(identity);
|
| 878 | if (!verifyObject(parsed as unknown as Record<string, unknown>, parsed.publicKey))
|
| 879 | throw new Error('Invalid identity signature');
|
| 880 | if (parsed.parentAgentId && parsed.rootControllerId === parsed.agentId)
|
| 881 | throw new Error('Child cannot remove root-controller reference');
|
| 882 | this.agents.set(parsed.agentId, parsed);
|
| 883 | this.store.append('agent', parsed.agentId, parsed);
|
| 884 | return parsed;
|
| 885 | }
|
| 886 |
|
| 887 | revokeAgent(agentId: string): void {
|
| 888 | const identity = this.requireAgent(agentId);
|
| 889 | const revoked = { ...identity, revoked: true };
|
| 890 | this.agents.set(agentId, revoked);
|
| 891 | this.store.append('revocation', `agent:${agentId}`, { agentId, at: new Date().toISOString() });
|
| 892 | }
|
| 893 |
|
| 894 | createDelegation(
|
| 895 | parentId: string,
|
| 896 | childId: string,
|
| 897 | capabilities: string[],
|
| 898 | expiresAt: string,
|
| 899 | parentKey: KeyPair,
|
| 900 | ): DelegationCredential {
|
| 901 | const parent = this.requireAgent(parentId);
|
| 902 | const child = this.requireAgent(childId);
|
| 903 | if (parent.revoked) throw new Error('Parent agent is revoked');
|
| 904 | if (child.parentAgentId !== parentId || child.rootControllerId !== parent.rootControllerId)
|
| 905 | throw new Error('Invalid child lineage');
|
| 906 | if (capabilities.some((capability) => !parent.capabilities.includes(capability)))
|
| 907 | throw new Error('Delegation exceeds parent authority');
|
| 908 | const raw = {
|
| 909 | protocolVersion: '0.1' as const,
|
| 910 | delegationId: randomUUID(),
|
| 911 | parentAgentId: parentId,
|
| 912 | childAgentId: childId,
|
| 913 | rootControllerId: parent.rootControllerId,
|
| 914 | capabilities,
|
| 915 | issuedAt: new Date().toISOString(),
|
| 916 | expiresAt,
|
| 917 | revoked: false,
|
| 918 | };
|
| 919 | const credential = DelegationCredentialSchema.parse(signObject(raw, parentKey));
|
| 920 | this.delegations.set(credential.delegationId, credential);
|
| 921 | this.store.append('delegation', credential.delegationId, credential);
|
| 922 | return credential;
|
| 923 | }
|
| 924 |
|
| 925 | verifyDelegation(delegationId: string, capability?: string, at = new Date()): boolean {
|
| 926 | const delegation = this.delegations.get(delegationId);
|
| 927 | if (!delegation || delegation.revoked || new Date(delegation.expiresAt) <= at) return false;
|
| 928 | const parent = this.agents.get(delegation.parentAgentId);
|
| 929 | const child = this.agents.get(delegation.childAgentId);
|
| 930 | if (!parent || !child || parent.revoked || child.revoked) return false;
|
| 931 | if (
|
| 932 | child.rootControllerId !== delegation.rootControllerId ||
|
| 933 | child.parentAgentId !== parent.agentId
|
| 934 | )
|
| 935 | return false;
|
| 936 | if (capability && !delegation.capabilities.includes(capability)) return false;
|
| 937 | if (delegation.capabilities.some((item) => !parent.capabilities.includes(item))) return false;
|
| 938 | return verifyObject(delegation as unknown as Record<string, unknown>, parent.publicKey);
|
| 939 | }
|
| 940 |
|
| 941 | revokeDelegation(delegationId: string): void {
|
| 942 | const item = this.delegations.get(delegationId);
|
| 943 | if (!item) throw new Error('Delegation not found');
|
| 944 | this.delegations.set(delegationId, { ...item, revoked: true });
|
| 945 | this.store.append('revocation', `delegation:${delegationId}`, {
|
| 946 | delegationId,
|
| 947 | at: new Date().toISOString(),
|
| 948 | });
|
| 949 | }
|
| 950 |
|
| 951 | saveContract(contract: InteractionContract): InteractionContract {
|
| 952 | const parsed = InteractionContractSchema.parse(contract);
|
| 953 | if (parsed.parties.some((party) => !(party in parsed.signatures)))
|
| 954 | throw new Error('Contract requires every party signature');
|
| 955 | for (const party of Object.keys(parsed.signatures)) {
|
| 956 | const agent = this.requireAgent(party);
|
| 957 | if (!verifyNamed(parsed as unknown as Record<string, unknown>, party, agent.publicKey))
|
| 958 | throw new Error(`Invalid contract signature: ${party}`);
|
| 959 | }
|
| 960 | this.contracts.set(parsed.interactionId, parsed);
|
| 961 | this.store.append('contract', parsed.interactionId, parsed);
|
| 962 | return parsed;
|
| 963 | }
|
| 964 |
|
| 965 | assess(context: PolicyContext): RiskDecision {
|
| 966 | const envelope = TrustEnvelopeSchema.parse(context.envelope);
|
| 967 | const responder = this.agents.get(envelope.respondingAgentId);
|
| 968 | const requester = this.agents.get(envelope.requestingAgentId);
|
| 969 | const contract = this.contracts.get(envelope.interactionId);
|
| 970 | const hardFailures: string[] = [];
|
| 971 | if (
|
| 972 | !requester ||
|
| 973 | requester.revoked ||
|
| 974 | !verifyObject(envelope as unknown as Record<string, unknown>, requester.publicKey)
|
| 975 | )
|
| 976 | hardFailures.push('invalid_requester_signature');
|
| 977 | if (!responder || responder.revoked) hardFailures.push('invalid_or_revoked_responder');
|
| 978 | if (new Date(envelope.expiresAt) <= new Date()) hardFailures.push('expired_envelope');
|
| 979 | if (this.nonces.has(envelope.nonce)) hardFailures.push('replay_attempt');
|
| 980 | if (
|
| 981 | envelope.delegationId &&
|
| 982 | !this.verifyDelegation(envelope.delegationId, envelope.requestedCapability)
|
| 983 | )
|
| 984 | hardFailures.push('invalid_delegation');
|
| 985 | if (!contract || canonicalHash({ ...contract, signatures: {} }) !== envelope.contractHash)
|
| 986 | hardFailures.push('contract_mismatch');
|
| 987 | if (
|
| 988 | contract &&
|
| 989 | (!contract.allowedActions.includes(context.action) ||
|
| 990 | contract.prohibitedActions.includes(context.action))
|
| 991 | )
|
| 992 | hardFailures.push('action_outside_contract');
|
| 993 | if (
|
| 994 | contract &&
|
| 995 | (context.dataClasses ?? []).some((item) => contract.prohibitedData.includes(item))
|
| 996 | )
|
| 997 | hardFailures.push('prohibited_data');
|
| 998 | if (
|
| 999 | contract &&
|
| 1000 | contract.humanApprovalRequirements.includes(context.action) &&
|
| 1001 | !context.humanApproved
|
| 1002 | )
|
| 1003 | hardFailures.push('missing_human_approval');
|
| 1004 | if (!hardFailures.includes('replay_attempt')) this.nonces.add(envelope.nonce);
|
| 1005 | if (hardFailures.length)
|
| 1006 | return this.decision('DENY', envelope.taskCategory, 1, hardFailures, [], []);
|
| 1007 |
|
| 1008 | const risk = this.getRisk(
|
| 1009 | envelope.respondingAgentId,
|
| 1010 | responder?.agentVersion ?? envelope.agentVersion,
|
| 1011 | envelope.taskCategory,
|
| 1012 | );
|
| 1013 | return risk;
|
| 1014 | }
|
| 1015 |
|
| 1016 | recordEvent(event: InteractionEvent): InteractionEvent {
|
| 1017 | const parsed = InteractionEventSchema.parse(event);
|
| 1018 | const agent = this.requireAgent(parsed.agentId);
|
| 1019 | if (!verifyObject(parsed as unknown as Record<string, unknown>, agent.publicKey))
|
| 1020 | throw new Error('Invalid event signature');
|
| 1021 | if (canonicalHash(parsed.payload) !== parsed.payloadHash)
|
| 1022 | throw new Error('Event payload hash mismatch');
|
| 1023 | const existing = this.events.get(parsed.eventId);
|
| 1024 | if (existing && canonicalHash(existing) !== canonicalHash(parsed))
|
| 1025 | throw new Error('Conflicting duplicate event');
|
| 1026 | this.events.set(parsed.eventId, parsed);
|
| 1027 | this.store.append('event', parsed.eventId, parsed);
|
| 1028 | return parsed;
|
| 1029 | }
|
| 1030 |
|
| 1031 | createConflict(
|
| 1032 | input: Omit<Conflict, 'conflictId' | 'status' | 'permissions'> & { participants: string[] },
|
| 1033 | ): Conflict {
|
| 1034 | const permissions = Object.fromEntries(input.participants.map((id) => [id, false]));
|
| 1035 | const conflict: Conflict = {
|
| 1036 | ...input,
|
| 1037 | conflictId: randomUUID(),
|
| 1038 | permissions,
|
| 1039 | status: 'pending_consent',
|
| 1040 | };
|
| 1041 | delete (conflict as any).participants;
|
| 1042 | this.conflicts.set(conflict.conflictId, conflict);
|
| 1043 | this.store.append('conflict', `${conflict.conflictId}:created`, conflict);
|
| 1044 | return conflict;
|
| 1045 | }
|
| 1046 |
|
| 1047 | permitMediation(conflictId: string, agentId: string): Conflict {
|
| 1048 | const conflict = this.requireConflict(conflictId);
|
| 1049 | if (!(agentId in conflict.permissions)) throw new Error('Agent is not a conflict participant');
|
| 1050 | conflict.permissions[agentId] = true;
|
| 1051 | if (Object.values(conflict.permissions).every(Boolean)) conflict.status = 'open';
|
| 1052 | this.store.append('conflict', `${conflict.conflictId}:permit:${agentId}`, conflict);
|
| 1053 | return conflict;
|
| 1054 | }
|
| 1055 |
|
| 1056 | resolveConflict(conflictId: string, resolution: string): Conflict {
|
| 1057 | const conflict = this.requireConflict(conflictId);
|
| 1058 | if (conflict.status !== 'open') throw new Error('Mutual mediation consent required');
|
| 1059 | conflict.status = 'resolved';
|
| 1060 | conflict.resolution = resolution;
|
| 1061 | this.store.append('conflict', `${conflict.conflictId}:resolved`, conflict);
|
| 1062 | return conflict;
|
| 1063 | }
|
| 1064 |
|
| 1065 | submitReceipt(receipt: Receipt): Receipt {
|
| 1066 | const parsed = this.verifyReceipt(receipt);
|
| 1067 | this.receipts.set(parsed.receiptId, parsed);
|
| 1068 | this.store.append('receipt', parsed.receiptId, parsed);
|
| 1069 | return parsed;
|
| 1070 | }
|
| 1071 |
|
| 1072 | verifyReceipt(receipt: Receipt): Receipt {
|
| 1073 | const parsed = ReceiptSchema.parse(receipt);
|
| 1074 | const required = parsed.unilateral
|
| 1075 | ? Object.keys(parsed.signatures).slice(0, 1)
|
| 1076 | : parsed.participants;
|
| 1077 | if (
|
| 1078 | !required.length ||
|
| 1079 | required.some((party) => {
|
| 1080 | const agent = this.agents.get(party);
|
| 1081 | return (
|
| 1082 | !agent ||
|
| 1083 | !verifyNamed(parsed as unknown as Record<string, unknown>, party, agent.publicKey)
|
| 1084 | );
|
| 1085 | })
|
| 1086 | )
|
| 1087 | throw new Error('Receipt signature verification failed');
|
| 1088 | return parsed;
|
| 1089 | }
|
| 1090 |
|
| 1091 | submitFeedback(item: Feedback): { revealed: boolean } {
|
| 1092 | const parsed = FeedbackSchema.parse(item);
|
| 1093 | const receipt = this.receipts.get(parsed.receiptId);
|
| 1094 | const reviewer = this.agents.get(parsed.reviewerAgentId);
|
| 1095 | if (
|
| 1096 | !receipt ||
|
| 1097 | !reviewer ||
|
| 1098 | !receipt.participants.includes(parsed.reviewerAgentId) ||
|
| 1099 | !receipt.participants.includes(parsed.subjectAgentId)
|
| 1100 | )
|
| 1101 | throw new Error('Feedback requires a valid participant receipt');
|
| 1102 | if (!verifyObject(parsed as unknown as Record<string, unknown>, reviewer.publicKey))
|
| 1103 | throw new Error('Invalid feedback signature');
|
| 1104 | this.feedback.set(parsed.feedbackId, parsed);
|
| 1105 | this.store.append('feedback', parsed.feedbackId, parsed);
|
| 1106 | const related = [...this.feedback.values()].filter(
|
| 1107 | (value) => value.receiptId === parsed.receiptId,
|
| 1108 | );
|
| 1109 | const revealed =
|
| 1110 | receipt.unilateral ||
|
| 1111 | new Set(related.map((value) => value.reviewerAgentId)).size >= receipt.participants.length;
|
| 1112 | if (revealed) for (const feedback of related) this.applyFeedback(feedback, receipt);
|
| 1113 | return { revealed };
|
| 1114 | }
|
| 1115 |
|
| 1116 | getRisk(agentId: string, version: string, taskCategory: string): RiskDecision {
|
| 1117 | const current = this.profiles.get(`${agentId}|${version}|${taskCategory}`);
|
| 1118 | const otherVersions = [...this.profiles.values()].filter(
|
| 1119 | (p) => p.agentId === agentId && p.taskCategory === taskCategory,
|
| 1120 | );
|
| 1121 | if (!current) {
|
| 1122 | const continuity = otherVersions.length
|
| 1123 | ? Math.min(
|
| 1124 | 0.2,
|
| 1125 | Math.max(
|
| 1126 | ...otherVersions.map((profile) => profile.sampleSize / (profile.sampleSize + 5)),
|
| 1127 | ) * 0.25,
|
| 1128 | )
|
| 1129 | : 0;
|
| 1130 | return this.decision(
|
| 1131 | 'CHALLENGE',
|
| 1132 | taskCategory,
|
| 1133 | continuity,
|
| 1134 | ['limited_verified_history'],
|
| 1135 | ['Agent version has limited task-specific evidence'],
|
| 1136 | ['request_evidence'],
|
| 1137 | );
|
| 1138 | }
|
| 1139 | const ageDays = Math.max(0, (Date.now() - Date.parse(current.updatedAt)) / 86_400_000);
|
| 1140 | const freshness = Math.exp(-ageDays / 180);
|
| 1141 | const quality =
|
| 1142 | (current.completion +
|
| 1143 | current.acceptance +
|
| 1144 | current.specification +
|
| 1145 | current.deadline +
|
| 1146 | current.communication +
|
| 1147 | current.evidence +
|
| 1148 | current.scope +
|
| 1149 | (1 - current.disputes)) /
|
| 1150 | 8;
|
| 1151 | const confidence = Math.min(0.95, (current.sampleSize / (current.sampleSize + 5)) * freshness);
|
| 1152 | const decision = quality >= 0.7 && confidence >= 0.25 ? 'ALLOW' : 'CHALLENGE';
|
| 1153 | return {
|
| 1154 | decision,
|
| 1155 | confidence,
|
| 1156 | taskCategory,
|
| 1157 | sampleSize: current.sampleSize,
|
| 1158 | dimensions: {
|
| 1159 | completionReliability: current.completion,
|
| 1160 | outputAcceptance: current.acceptance,
|
| 1161 | contractAdherence: (current.specification + current.scope) / 2,
|
| 1162 | deadlineReliability: current.deadline,
|
| 1163 | communicationQuality: current.communication,
|
| 1164 | evidenceQuality: current.evidence,
|
| 1165 | disputeRate: current.disputes,
|
| 1166 | },
|
| 1167 | reasons: [`contextual_quality=${quality.toFixed(2)}`],
|
| 1168 | warnings: confidence < 0.5 ? ['Limited sample size'] : [],
|
| 1169 | dataFreshness: { behaviouralProfile: current.updatedAt },
|
| 1170 | requiredChallenges: decision === 'CHALLENGE' ? ['request_evidence'] : [],
|
| 1171 | };
|
| 1172 | }
|
| 1173 |
|
| 1174 | setContributionConsent(agentId: string, enabled: boolean): void {
|
| 1175 | this.requireAgent(agentId);
|
| 1176 | const previous = this.contributionConsent.get(agentId);
|
| 1177 | this.contributionConsent.set(
|
| 1178 | agentId,
|
| 1179 | enabled
|
| 1180 | ? { enabled: true, grantedAt: new Date().toISOString() }
|
| 1181 | : {
|
| 1182 | enabled: false,
|
| 1183 | grantedAt: previous?.grantedAt ?? new Date().toISOString(),
|
| 1184 | revokedAt: new Date().toISOString(),
|
| 1185 | },
|
| 1186 | );
|
| 1187 | this.store.append('consent', `${agentId}:${Date.now()}`, {
|
| 1188 | agentId,
|
| 1189 | ...this.contributionConsent.get(agentId),
|
| 1190 | });
|
| 1191 | }
|
| 1192 |
|
| 1193 | networkContribution(event: InteractionEvent): Record<string, unknown> | null {
|
| 1194 | if (!this.contributionConsent.get(event.agentId)?.enabled || event.visibility === 'local_only')
|
| 1195 | return null;
|
| 1196 | return {
|
| 1197 | eventId: event.eventId,
|
| 1198 | interactionId: event.interactionId,
|
| 1199 | eventType: event.eventType,
|
| 1200 | agentId: event.agentId,
|
| 1201 | agentVersion: event.agentVersion,
|
| 1202 | timestamp: event.timestamp,
|
| 1203 | visibility: event.visibility,
|
| 1204 | provenance: event.provenance,
|
| 1205 | payloadHash: event.payloadHash,
|
| 1206 | evidenceRefs: event.evidenceRefs ?? [],
|
| 1207 | signature: event.signature,
|
| 1208 | };
|
| 1209 | }
|
| 1210 |
|
| 1211 | private applyFeedback(feedback: Feedback, receipt: Receipt): void {
|
| 1212 | const taskCategory = this.contracts.get(feedback.interactionId)?.taskCategory ?? 'unknown';
|
| 1213 | const version = receipt.agentVersions[feedback.subjectAgentId] ?? 'unknown';
|
| 1214 | const key = `${feedback.subjectAgentId}|${version}|${taskCategory}`;
|
| 1215 | const previous = this.profiles.get(key);
|
| 1216 | const n = previous?.sampleSize ?? 0;
|
| 1217 | const next = (old: number | undefined, value: number) => ((old ?? 0) * n + value) / (n + 1);
|
| 1218 | const profile = {
|
| 1219 | agentId: feedback.subjectAgentId,
|
| 1220 | agentVersion: version,
|
| 1221 | taskCategory,
|
| 1222 | sampleSize: n + 1,
|
| 1223 | updatedAt: new Date().toISOString(),
|
| 1224 | completion: next(previous?.completion, Number(feedback.taskCompleted)),
|
| 1225 | acceptance: next(previous?.acceptance, Number(feedback.outputAccepted)),
|
| 1226 | specification: next(previous?.specification, Number(feedback.specificationMatched)),
|
| 1227 | deadline: next(previous?.deadline, Number(feedback.deadlineMet)),
|
| 1228 | communication: next(previous?.communication, feedback.communicationQuality),
|
| 1229 | evidence: next(previous?.evidence, Number(feedback.evidenceProvided)),
|
| 1230 | scope: next(previous?.scope, Number(feedback.scopeRespected)),
|
| 1231 | disputes: next(previous?.disputes, Number(feedback.disputeRaised)),
|
| 1232 | };
|
| 1233 | this.profiles.set(key, profile);
|
| 1234 | this.store.append('profile', `${key}:${profile.sampleSize}`, profile);
|
| 1235 | }
|
| 1236 |
|
| 1237 | private decision(
|
| 1238 | decision: RiskDecision['decision'],
|
| 1239 | taskCategory: string,
|
| 1240 | confidence: number,
|
| 1241 | reasons: string[],
|
| 1242 | warnings: string[],
|
| 1243 | challenges: string[],
|
| 1244 | ): RiskDecision {
|
| 1245 | return {
|
| 1246 | decision,
|
| 1247 | confidence,
|
| 1248 | taskCategory,
|
| 1249 | sampleSize: 0,
|
| 1250 | dimensions: {},
|
| 1251 | reasons,
|
| 1252 | warnings,
|
| 1253 | dataFreshness: {},
|
| 1254 | requiredChallenges: challenges,
|
| 1255 | };
|
| 1256 | }
|
| 1257 | private requireAgent(id: string): AgentIdentity {
|
| 1258 | const value = this.agents.get(id);
|
| 1259 | if (!value) throw new Error(`Agent not found: ${id}`);
|
| 1260 | return value;
|
| 1261 | }
|
| 1262 | private requireConflict(id: string): Conflict {
|
| 1263 | const value = this.conflicts.get(id);
|
| 1264 | if (!value) throw new Error('Conflict not found');
|
| 1265 | return value;
|
| 1266 | }
|
| 1267 | }
|
| 1268 |
|
| 1269 | export interface FactCheckProvider {
|
| 1270 | check(claim: string, permission?: boolean): Promise<FactCheckResult>;
|
| 1271 | }
|
| 1272 |
|
| 1273 | export class FixtureFactCheckProvider implements FactCheckProvider {
|
| 1274 | constructor(
|
| 1275 | private fixtures: Record<
|
| 1276 | string,
|
| 1277 | { status: FactCheckResult['status']; evidence: string[] }
|
| 1278 | > = {},
|
| 1279 | ) {}
|
| 1280 | async check(claim: string, permission = true): Promise<FactCheckResult> {
|
| 1281 | if (!permission)
|
| 1282 | return result(
|
| 1283 | claim,
|
| 1284 | 'objective',
|
| 1285 | 'insufficient_permission',
|
| 1286 | 1,
|
| 1287 | [],
|
| 1288 | 'Grant evidence-source permission',
|
| 1289 | );
|
| 1290 | if (/\b(i think|i prefer|best|beautiful)\b/i.test(claim))
|
| 1291 | return result(claim, 'subjective', 'not_fact_checkable', 1, [], 'Treat as an opinion');
|
| 1292 | if (/\b(will|might|forecast|predict)\b/i.test(claim))
|
| 1293 | return result(
|
| 1294 | claim,
|
| 1295 | 'prediction',
|
| 1296 | 'not_fact_checkable',
|
| 1297 | 0.9,
|
| 1298 | [],
|
| 1299 | 'Track as a prediction, not a fact',
|
| 1300 | );
|
| 1301 | const fixture = this.fixtures[claim];
|
| 1302 | if (!fixture)
|
| 1303 | return result(claim, 'objective', 'unverified', 0, [], 'Request authoritative evidence');
|
| 1304 | return result(
|
| 1305 | claim,
|
| 1306 | 'objective',
|
| 1307 | fixture.status,
|
| 1308 | 0.95,
|
| 1309 | fixture.evidence,
|
| 1310 | fixture.status === 'contradicted' ? 'Challenge the claim with cited evidence' : 'Continue',
|
| 1311 | );
|
| 1312 | }
|
| 1313 | }
|
| 1314 |
|
| 1315 | function result(
|
| 1316 | claim: string,
|
| 1317 | claimType: FactCheckResult['claimType'],
|
| 1318 | status: FactCheckResult['status'],
|
| 1319 | confidence: number,
|
| 1320 | evidence: string[],
|
| 1321 | action: string,
|
| 1322 | ): FactCheckResult {
|
| 1323 | return {
|
| 1324 | claim,
|
| 1325 | claimType,
|
| 1326 | status,
|
| 1327 | confidence,
|
| 1328 | evidenceReferences: evidence,
|
| 1329 | sourceAuthority: evidence.length ? 'authoritative_fixture' : 'none',
|
| 1330 | sourceFreshness: new Date().toISOString(),
|
| 1331 | contradictingEvidence: status === 'contradicted' ? evidence : [],
|
| 1332 | suggestedNextAction: action,
|
| 1333 | };
|
| 1334 | }
|
| 1335 |
|
| 1336 | export { signNamed, signObject };
|