| 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 | ReceiptSchema,
|
| 14 | signNamed,
|
| 15 | signObject,
|
| 16 | TrustEnvelopeSchema,
|
| 17 | verifyNamed,
|
| 18 | verifyObject,
|
| 19 | type AgentIdentity,
|
| 20 | type CounterpartyBrief,
|
| 21 | type DelegationCredential,
|
| 22 | type FactCheckResult,
|
| 23 | type Feedback,
|
| 24 | type InteractionContract,
|
| 25 | type InteractionConclusion,
|
| 26 | type InteractionCompletionReport,
|
| 27 | type InteractionFeedback,
|
| 28 | type InteractionEvent,
|
| 29 | type KeyPair,
|
| 30 | type LiveSessionInsight,
|
| 31 | type PublicAgentCard,
|
| 32 | type Receipt,
|
| 33 | type RiskDecision,
|
| 34 | type TrustEnvelope,
|
| 35 | } from '../../protocol/src/index.js';
|
| 36 |
|
| 37 | const REQUIREMENT_STOP_WORDS = new Set([
|
| 38 | 'about',
|
| 39 | 'agent',
|
| 40 | 'available',
|
| 41 | 'from',
|
| 42 | 'have',
|
| 43 | 'into',
|
| 44 | 'must',
|
| 45 | 'provide',
|
| 46 | 'return',
|
| 47 | 'should',
|
| 48 | 'that',
|
| 49 | 'their',
|
| 50 | 'this',
|
| 51 | 'with',
|
| 52 | ]);
|
| 53 |
|
| 54 | function normalized(value: string): string {
|
| 55 | return value
|
| 56 | .trim()
|
| 57 | .toLowerCase()
|
| 58 | .replace(/[^a-z0-9]+/g, ' ')
|
| 59 | .trim();
|
| 60 | }
|
| 61 |
|
| 62 | function meaningfulTerms(value: string): string[] {
|
| 63 | return [
|
| 64 | ...new Set(
|
| 65 | normalized(value)
|
| 66 | .split(' ')
|
| 67 | .filter((term) => term.length >= 4 && !REQUIREMENT_STOP_WORDS.has(term)),
|
| 68 | ),
|
| 69 | ];
|
| 70 | }
|
| 71 |
|
| 72 | function assessDeclaration(
|
| 73 | requirement: string,
|
| 74 | declarations: string[],
|
| 75 | ): { status: 'match' | 'partial' | 'mismatch' | 'unknown'; reason: string; confidence: number } {
|
| 76 | const normalizedRequirement = normalized(requirement);
|
| 77 | const normalizedDeclarations = declarations.map(normalized);
|
| 78 | if (
|
| 79 | normalizedDeclarations.some(
|
| 80 | (declaration) => declaration === '*' || declaration === normalizedRequirement,
|
| 81 | )
|
| 82 | )
|
| 83 | return {
|
| 84 | status: 'match',
|
| 85 | reason: 'The counterparty explicitly declares this capability.',
|
| 86 | confidence: 0.95,
|
| 87 | };
|
| 88 |
|
| 89 | const terms = meaningfulTerms(requirement);
|
| 90 | const negative = normalizedDeclarations.find((declaration) =>
|
| 91 | terms.some(
|
| 92 | (term) =>
|
| 93 | declaration.includes(`no ${term}`) ||
|
| 94 | declaration.includes(`not ${term}`) ||
|
| 95 | declaration.includes(`cannot ${term}`),
|
| 96 | ),
|
| 97 | );
|
| 98 | if (negative)
|
| 99 | return {
|
| 100 | status: 'mismatch',
|
| 101 | reason: `The counterparty declares a conflicting limitation: ${negative}`,
|
| 102 | confidence: 0.95,
|
| 103 | };
|
| 104 |
|
| 105 | if (terms.length) {
|
| 106 | const declarationText = normalizedDeclarations.join(' ');
|
| 107 | const overlap = terms.filter((term) => declarationText.includes(term)).length / terms.length;
|
| 108 | if (overlap >= 0.6)
|
| 109 | return {
|
| 110 | status: 'match',
|
| 111 | reason: 'The counterparty declarations strongly match this requirement.',
|
| 112 | confidence: 0.8,
|
| 113 | };
|
| 114 | if (overlap > 0)
|
| 115 | return {
|
| 116 | status: 'partial',
|
| 117 | reason: 'The counterparty declarations only partially cover this requirement.',
|
| 118 | confidence: 0.65,
|
| 119 | };
|
| 120 | }
|
| 121 | return {
|
| 122 | status: 'unknown',
|
| 123 | reason: 'The published agent card does not establish this requirement.',
|
| 124 | confidence: 0.35,
|
| 125 | };
|
| 126 | }
|
| 127 |
|
| 128 | export function buildCounterpartyBrief(input: {
|
| 129 | interactionId: string;
|
| 130 | contractHash: string;
|
| 131 | contract: InteractionContract;
|
| 132 | recipientAgentId: string;
|
| 133 | subject: PublicAgentCard;
|
| 134 | historyInsights: LiveSessionInsight[];
|
| 135 | relevantSampleSize: number;
|
| 136 | historyConfidence: number;
|
| 137 | generatedAt?: string;
|
| 138 | expiresAt: string;
|
| 139 | }): CounterpartyBrief {
|
| 140 | const declarations = [
|
| 141 | input.subject.description,
|
| 142 | ...input.subject.capabilities,
|
| 143 | ...input.subject.limitations,
|
| 144 | ];
|
| 145 | const requirements = [
|
| 146 | {
|
| 147 | requirementId: 'task-category',
|
| 148 | requirement: input.contract.taskCategory,
|
| 149 | kind: 'capability' as const,
|
| 150 | },
|
| 151 | {
|
| 152 | requirementId: 'requested-outcome',
|
| 153 | requirement: input.contract.requestedOutcome,
|
| 154 | kind: 'scope' as const,
|
| 155 | },
|
| 156 | ...input.contract.allowedActions.map((requirement, index) => ({
|
| 157 | requirementId: `allowed-action-${index + 1}`,
|
| 158 | requirement,
|
| 159 | kind: 'capability' as const,
|
| 160 | })),
|
| 161 | ...input.contract.successCriteria.map((requirement, index) => ({
|
| 162 | requirementId: `success-criterion-${index + 1}`,
|
| 163 | requirement,
|
| 164 | kind: 'success_criterion' as const,
|
| 165 | })),
|
| 166 | ...input.contract.evidenceRequirements.map((requirement, index) => ({
|
| 167 | requirementId: `evidence-${index + 1}`,
|
| 168 | requirement,
|
| 169 | kind: 'evidence' as const,
|
| 170 | })),
|
| 171 | ].map((requirement) => ({
|
| 172 | ...requirement,
|
| 173 | ...assessDeclaration(requirement.requirement, declarations),
|
| 174 | sources: ['contract', 'self_declared'] as const,
|
| 175 | evidenceReferences: [input.subject.cardUrl],
|
| 176 | }));
|
| 177 | const requirementReferences = requirements
|
| 178 | .filter((requirement) => requirement.status !== 'match')
|
| 179 | .map((requirement) => requirement.requirementId);
|
| 180 | const insights = input.historyInsights.map((insight) => ({
|
| 181 | ...insight,
|
| 182 | requirementReferences:
|
| 183 | insight.requirementReferences.length > 0
|
| 184 | ? insight.requirementReferences
|
| 185 | : requirementReferences,
|
| 186 | }));
|
| 187 | const needsChallenge =
|
| 188 | input.relevantSampleSize === 0 ||
|
| 189 | requirements.some((requirement) => requirement.status === 'mismatch') ||
|
| 190 | insights.some((insight) => insight.severity === 'high');
|
| 191 | return CounterpartyBriefSchema.parse({
|
| 192 | briefId: randomUUID(),
|
| 193 | interactionId: input.interactionId,
|
| 194 | contractHash: input.contractHash,
|
| 195 | recipientAgentId: input.recipientAgentId,
|
| 196 | subjectAgentId: input.subject.agentId,
|
| 197 | taskCategory: input.contract.taskCategory,
|
| 198 | decision: needsChallenge ? 'CHALLENGE' : 'ALLOW',
|
| 199 | requirements,
|
| 200 | insights,
|
| 201 | relevantSampleSize: input.relevantSampleSize,
|
| 202 | historyConfidence: input.historyConfidence,
|
| 203 | subjectAgentVersion: input.subject.agentVersion,
|
| 204 | recommendedContractChanges: requirements
|
| 205 | .filter((requirement) => requirement.status === 'mismatch')
|
| 206 | .map((requirement) => `Resolve mismatch: ${requirement.requirement}`),
|
| 207 | generatedAt: input.generatedAt ?? new Date().toISOString(),
|
| 208 | expiresAt: input.expiresAt,
|
| 209 | });
|
| 210 | }
|
| 211 |
|
| 212 | export function buildInteractionConclusion(input: {
|
| 213 | interaction: { interactionId: string; termsHash: string; contract: InteractionContract };
|
| 214 | reports: InteractionCompletionReport[];
|
| 215 | feedback: InteractionFeedback[];
|
| 216 | conclusionId?: string;
|
| 217 | generatedAt?: string;
|
| 218 | }): InteractionConclusion {
|
| 219 | const outcomes = input.reports.map((report) => report.outcome);
|
| 220 | const uniqueOutcomes = new Set(outcomes);
|
| 221 | const outcome =
|
| 222 | outcomes.length === 0
|
| 223 | ? ('cancelled' as const)
|
| 224 | : uniqueOutcomes.size === 1
|
| 225 | ? outcomes[0]!
|
| 226 | : outcomes.includes('failure')
|
| 227 | ? ('failure' as const)
|
| 228 | : ('partial' as const);
|
| 229 | const consensus =
|
| 230 | input.reports.length < 2
|
| 231 | ? ('unilateral' as const)
|
| 232 | : uniqueOutcomes.size === 1
|
| 233 | ? ('bilateral_agreement' as const)
|
| 234 | : outcomes.includes('partial')
|
| 235 | ? ('bilateral_partial_agreement' as const)
|
| 236 | : ('conflicting' as const);
|
| 237 | const statusRank = { met: 0, partially_met: 1, unknown: 2, missed: 3 } as const;
|
| 238 | const criteria = input.interaction.contract.successCriteria.map((criterion) => {
|
| 239 | const assessments = input.reports
|
| 240 | .flatMap((report) => report.criteria)
|
| 241 | .filter((assessment) => assessment.criterion === criterion);
|
| 242 | const selected = assessments.sort(
|
| 243 | (left, right) => statusRank[right.status] - statusRank[left.status],
|
| 244 | )[0];
|
| 245 | return {
|
| 246 | criterion,
|
| 247 | status: selected?.status ?? ('unknown' as const),
|
| 248 | ...(selected?.explanation ? { explanation: selected.explanation } : {}),
|
| 249 | evidenceReferences: [
|
| 250 | ...new Set(assessments.flatMap((assessment) => assessment.evidenceReferences)),
|
| 251 | ],
|
| 252 | };
|
| 253 | });
|
| 254 | const averageRatings = Object.fromEntries(
|
| 255 | FeedbackDimensionSchema.options.flatMap((dimension) => {
|
| 256 | const values = input.feedback
|
| 257 | .map((item) => item.ratings[dimension])
|
| 258 | .filter((value): value is number => typeof value === 'number');
|
| 259 | return values.length
|
| 260 | ? [[dimension, values.reduce((total, value) => total + value, 0) / values.length]]
|
| 261 | : [];
|
| 262 | }),
|
| 263 | );
|
| 264 | return InteractionConclusionSchema.parse({
|
| 265 | conclusionId: input.conclusionId ?? randomUUID(),
|
| 266 | interactionId: input.interaction.interactionId,
|
| 267 | contractHash: input.interaction.termsHash,
|
| 268 | outcome,
|
| 269 | consensus,
|
| 270 | 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.`,
|
| 271 | criteria,
|
| 272 | reportIds: input.reports.map((report) => report.reportId),
|
| 273 | feedbackIds: input.feedback.map((item) => item.feedbackId),
|
| 274 | averageRatings,
|
| 275 | evidenceReferences: [...new Set(input.reports.flatMap((report) => report.evidenceReferences))],
|
| 276 | generatedAt: input.generatedAt ?? new Date().toISOString(),
|
| 277 | });
|
| 278 | }
|
| 279 |
|
| 280 | export interface AuditStore {
|
| 281 | append(kind: string, id: string, value: unknown): void;
|
| 282 | list(kind: string): unknown[];
|
| 283 | }
|
| 284 |
|
| 285 | export class MemoryAuditStore implements AuditStore {
|
| 286 | private rows: { kind: string; id: string; value: unknown }[] = [];
|
| 287 | append(kind: string, id: string, value: unknown): void {
|
| 288 | const old = this.rows.find((row) => row.kind === kind && row.id === id);
|
| 289 | if (old && canonicalHash(old.value) !== canonicalHash(value))
|
| 290 | throw new Error(`Conflicting duplicate ${kind}:${id}`);
|
| 291 | if (!old) this.rows.push({ kind, id, value: structuredClone(value) });
|
| 292 | }
|
| 293 | list(kind: string): unknown[] {
|
| 294 | return this.rows.filter((row) => row.kind === kind).map((row) => structuredClone(row.value));
|
| 295 | }
|
| 296 | }
|
| 297 |
|
| 298 | export function createIdentity(input: {
|
| 299 | agentId: string;
|
| 300 | operatorRef: string;
|
| 301 | version?: string;
|
| 302 | capabilities: string[];
|
| 303 | assurance?: AgentIdentity['assurance'];
|
| 304 | parentAgentId?: string;
|
| 305 | rootControllerId?: string;
|
| 306 | }): { identity: AgentIdentity; keyPair: KeyPair } {
|
| 307 | const keyPair = createKeyPair(`${input.agentId}#1`);
|
| 308 | const identity: Record<string, unknown> = {
|
| 309 | protocolVersion: '0.1',
|
| 310 | agentId: input.agentId,
|
| 311 | publicKey: keyPair.publicKey,
|
| 312 | keyId: keyPair.keyId,
|
| 313 | operatorRef: input.operatorRef,
|
| 314 | assurance: input.assurance ?? 'pseudonymous',
|
| 315 | agentVersion: input.version ?? '1.0.0',
|
| 316 | capabilities: input.capabilities,
|
| 317 | supportedProtocols: ['A2A/1.0', 'OpenClasp/0.1'],
|
| 318 | ...(input.parentAgentId ? { parentAgentId: input.parentAgentId } : {}),
|
| 319 | rootControllerId: input.rootControllerId ?? input.agentId,
|
| 320 | revoked: false,
|
| 321 | provenance: 'cryptographically_verified',
|
| 322 | createdAt: new Date().toISOString(),
|
| 323 | };
|
| 324 | return { identity: AgentIdentitySchema.parse(signObject(identity, keyPair)), keyPair };
|
| 325 | }
|
| 326 |
|
| 327 | export type PolicyContext = {
|
| 328 | envelope: TrustEnvelope;
|
| 329 | action: string;
|
| 330 | dataClasses?: string[];
|
| 331 | humanApproved?: boolean;
|
| 332 | };
|
| 333 |
|
| 334 | export type Conflict = {
|
| 335 | conflictId: string;
|
| 336 | interactionId: string;
|
| 337 | issue: string;
|
| 338 | positions: Record<string, string>;
|
| 339 | evidence: string[];
|
| 340 | contractClauses: string[];
|
| 341 | missingInformation: string[];
|
| 342 | possibleResolutions: string[];
|
| 343 | permissions: Record<string, boolean>;
|
| 344 | status: 'pending_consent' | 'open' | 'resolved';
|
| 345 | resolution?: string;
|
| 346 | };
|
| 347 |
|
| 348 | type ProfileAggregate = {
|
| 349 | agentId: string;
|
| 350 | agentVersion: string;
|
| 351 | taskCategory: string;
|
| 352 | sampleSize: number;
|
| 353 | updatedAt: string;
|
| 354 | completion: number;
|
| 355 | acceptance: number;
|
| 356 | specification: number;
|
| 357 | deadline: number;
|
| 358 | communication: number;
|
| 359 | evidence: number;
|
| 360 | scope: number;
|
| 361 | disputes: number;
|
| 362 | };
|
| 363 |
|
| 364 | export class TrustEngine {
|
| 365 | readonly agents = new Map<string, AgentIdentity>();
|
| 366 | readonly delegations = new Map<string, DelegationCredential>();
|
| 367 | readonly contracts = new Map<string, InteractionContract>();
|
| 368 | readonly events = new Map<string, InteractionEvent>();
|
| 369 | readonly receipts = new Map<string, Receipt>();
|
| 370 | readonly conflicts = new Map<string, Conflict>();
|
| 371 | readonly feedback = new Map<string, Feedback>();
|
| 372 | readonly profiles = new Map<string, ProfileAggregate>();
|
| 373 | readonly contributionConsent = new Map<
|
| 374 | string,
|
| 375 | { enabled: boolean; grantedAt: string; revokedAt?: string }
|
| 376 | >();
|
| 377 | private readonly nonces = new Set<string>();
|
| 378 |
|
| 379 | constructor(readonly store: AuditStore = new MemoryAuditStore()) {
|
| 380 | for (const value of store.list('agent') as AgentIdentity[])
|
| 381 | this.agents.set(value.agentId, value);
|
| 382 | for (const value of store.list('delegation') as DelegationCredential[])
|
| 383 | this.delegations.set(value.delegationId, value);
|
| 384 | for (const value of store.list('contract') as InteractionContract[])
|
| 385 | this.contracts.set(value.interactionId, value);
|
| 386 | for (const value of store.list('event') as InteractionEvent[])
|
| 387 | this.events.set(value.eventId, value);
|
| 388 | for (const value of store.list('receipt') as Receipt[])
|
| 389 | this.receipts.set(value.receiptId, value);
|
| 390 | for (const value of store.list('feedback') as Feedback[])
|
| 391 | this.feedback.set(value.feedbackId, value);
|
| 392 | for (const value of store.list('profile') as ProfileAggregate[])
|
| 393 | this.profiles.set(`${value.agentId}|${value.agentVersion}|${value.taskCategory}`, value);
|
| 394 | for (const value of store.list('conflict') as Conflict[])
|
| 395 | this.conflicts.set(value.conflictId, value);
|
| 396 | for (const value of store.list('consent') as {
|
| 397 | agentId: string;
|
| 398 | enabled: boolean;
|
| 399 | grantedAt: string;
|
| 400 | revokedAt?: string;
|
| 401 | }[])
|
| 402 | this.contributionConsent.set(value.agentId, value);
|
| 403 | for (const value of store.list('revocation') as {
|
| 404 | agentId?: string;
|
| 405 | delegationId?: string;
|
| 406 | }[]) {
|
| 407 | if (value.agentId && this.agents.has(value.agentId))
|
| 408 | this.agents.set(value.agentId, { ...this.agents.get(value.agentId)!, revoked: true });
|
| 409 | if (value.delegationId && this.delegations.has(value.delegationId))
|
| 410 | this.delegations.set(value.delegationId, {
|
| 411 | ...this.delegations.get(value.delegationId)!,
|
| 412 | revoked: true,
|
| 413 | });
|
| 414 | }
|
| 415 | }
|
| 416 |
|
| 417 | registerAgent(identity: AgentIdentity): AgentIdentity {
|
| 418 | const parsed = AgentIdentitySchema.parse(identity);
|
| 419 | if (!verifyObject(parsed as unknown as Record<string, unknown>, parsed.publicKey))
|
| 420 | throw new Error('Invalid identity signature');
|
| 421 | if (parsed.parentAgentId && parsed.rootControllerId === parsed.agentId)
|
| 422 | throw new Error('Child cannot remove root-controller reference');
|
| 423 | this.agents.set(parsed.agentId, parsed);
|
| 424 | this.store.append('agent', parsed.agentId, parsed);
|
| 425 | return parsed;
|
| 426 | }
|
| 427 |
|
| 428 | revokeAgent(agentId: string): void {
|
| 429 | const identity = this.requireAgent(agentId);
|
| 430 | const revoked = { ...identity, revoked: true };
|
| 431 | this.agents.set(agentId, revoked);
|
| 432 | this.store.append('revocation', `agent:${agentId}`, { agentId, at: new Date().toISOString() });
|
| 433 | }
|
| 434 |
|
| 435 | createDelegation(
|
| 436 | parentId: string,
|
| 437 | childId: string,
|
| 438 | capabilities: string[],
|
| 439 | expiresAt: string,
|
| 440 | parentKey: KeyPair,
|
| 441 | ): DelegationCredential {
|
| 442 | const parent = this.requireAgent(parentId);
|
| 443 | const child = this.requireAgent(childId);
|
| 444 | if (parent.revoked) throw new Error('Parent agent is revoked');
|
| 445 | if (child.parentAgentId !== parentId || child.rootControllerId !== parent.rootControllerId)
|
| 446 | throw new Error('Invalid child lineage');
|
| 447 | if (capabilities.some((capability) => !parent.capabilities.includes(capability)))
|
| 448 | throw new Error('Delegation exceeds parent authority');
|
| 449 | const raw = {
|
| 450 | protocolVersion: '0.1' as const,
|
| 451 | delegationId: randomUUID(),
|
| 452 | parentAgentId: parentId,
|
| 453 | childAgentId: childId,
|
| 454 | rootControllerId: parent.rootControllerId,
|
| 455 | capabilities,
|
| 456 | issuedAt: new Date().toISOString(),
|
| 457 | expiresAt,
|
| 458 | revoked: false,
|
| 459 | };
|
| 460 | const credential = DelegationCredentialSchema.parse(signObject(raw, parentKey));
|
| 461 | this.delegations.set(credential.delegationId, credential);
|
| 462 | this.store.append('delegation', credential.delegationId, credential);
|
| 463 | return credential;
|
| 464 | }
|
| 465 |
|
| 466 | verifyDelegation(delegationId: string, capability?: string, at = new Date()): boolean {
|
| 467 | const delegation = this.delegations.get(delegationId);
|
| 468 | if (!delegation || delegation.revoked || new Date(delegation.expiresAt) <= at) return false;
|
| 469 | const parent = this.agents.get(delegation.parentAgentId);
|
| 470 | const child = this.agents.get(delegation.childAgentId);
|
| 471 | if (!parent || !child || parent.revoked || child.revoked) return false;
|
| 472 | if (
|
| 473 | child.rootControllerId !== delegation.rootControllerId ||
|
| 474 | child.parentAgentId !== parent.agentId
|
| 475 | )
|
| 476 | return false;
|
| 477 | if (capability && !delegation.capabilities.includes(capability)) return false;
|
| 478 | if (delegation.capabilities.some((item) => !parent.capabilities.includes(item))) return false;
|
| 479 | return verifyObject(delegation as unknown as Record<string, unknown>, parent.publicKey);
|
| 480 | }
|
| 481 |
|
| 482 | revokeDelegation(delegationId: string): void {
|
| 483 | const item = this.delegations.get(delegationId);
|
| 484 | if (!item) throw new Error('Delegation not found');
|
| 485 | this.delegations.set(delegationId, { ...item, revoked: true });
|
| 486 | this.store.append('revocation', `delegation:${delegationId}`, {
|
| 487 | delegationId,
|
| 488 | at: new Date().toISOString(),
|
| 489 | });
|
| 490 | }
|
| 491 |
|
| 492 | saveContract(contract: InteractionContract): InteractionContract {
|
| 493 | const parsed = InteractionContractSchema.parse(contract);
|
| 494 | if (parsed.parties.some((party) => !(party in parsed.signatures)))
|
| 495 | throw new Error('Contract requires every party signature');
|
| 496 | for (const party of Object.keys(parsed.signatures)) {
|
| 497 | const agent = this.requireAgent(party);
|
| 498 | if (!verifyNamed(parsed as unknown as Record<string, unknown>, party, agent.publicKey))
|
| 499 | throw new Error(`Invalid contract signature: ${party}`);
|
| 500 | }
|
| 501 | this.contracts.set(parsed.interactionId, parsed);
|
| 502 | this.store.append('contract', parsed.interactionId, parsed);
|
| 503 | return parsed;
|
| 504 | }
|
| 505 |
|
| 506 | assess(context: PolicyContext): RiskDecision {
|
| 507 | const envelope = TrustEnvelopeSchema.parse(context.envelope);
|
| 508 | const responder = this.agents.get(envelope.respondingAgentId);
|
| 509 | const requester = this.agents.get(envelope.requestingAgentId);
|
| 510 | const contract = this.contracts.get(envelope.interactionId);
|
| 511 | const hardFailures: string[] = [];
|
| 512 | if (
|
| 513 | !requester ||
|
| 514 | requester.revoked ||
|
| 515 | !verifyObject(envelope as unknown as Record<string, unknown>, requester.publicKey)
|
| 516 | )
|
| 517 | hardFailures.push('invalid_requester_signature');
|
| 518 | if (!responder || responder.revoked) hardFailures.push('invalid_or_revoked_responder');
|
| 519 | if (new Date(envelope.expiresAt) <= new Date()) hardFailures.push('expired_envelope');
|
| 520 | if (this.nonces.has(envelope.nonce)) hardFailures.push('replay_attempt');
|
| 521 | if (
|
| 522 | envelope.delegationId &&
|
| 523 | !this.verifyDelegation(envelope.delegationId, envelope.requestedCapability)
|
| 524 | )
|
| 525 | hardFailures.push('invalid_delegation');
|
| 526 | if (!contract || canonicalHash({ ...contract, signatures: {} }) !== envelope.contractHash)
|
| 527 | hardFailures.push('contract_mismatch');
|
| 528 | if (
|
| 529 | contract &&
|
| 530 | (!contract.allowedActions.includes(context.action) ||
|
| 531 | contract.prohibitedActions.includes(context.action))
|
| 532 | )
|
| 533 | hardFailures.push('action_outside_contract');
|
| 534 | if (
|
| 535 | contract &&
|
| 536 | (context.dataClasses ?? []).some((item) => contract.prohibitedData.includes(item))
|
| 537 | )
|
| 538 | hardFailures.push('prohibited_data');
|
| 539 | if (
|
| 540 | contract &&
|
| 541 | contract.humanApprovalRequirements.includes(context.action) &&
|
| 542 | !context.humanApproved
|
| 543 | )
|
| 544 | hardFailures.push('missing_human_approval');
|
| 545 | if (!hardFailures.includes('replay_attempt')) this.nonces.add(envelope.nonce);
|
| 546 | if (hardFailures.length)
|
| 547 | return this.decision('DENY', envelope.taskCategory, 1, hardFailures, [], []);
|
| 548 |
|
| 549 | const risk = this.getRisk(
|
| 550 | envelope.respondingAgentId,
|
| 551 | responder?.agentVersion ?? envelope.agentVersion,
|
| 552 | envelope.taskCategory,
|
| 553 | );
|
| 554 | return risk;
|
| 555 | }
|
| 556 |
|
| 557 | recordEvent(event: InteractionEvent): InteractionEvent {
|
| 558 | const parsed = InteractionEventSchema.parse(event);
|
| 559 | const agent = this.requireAgent(parsed.agentId);
|
| 560 | if (!verifyObject(parsed as unknown as Record<string, unknown>, agent.publicKey))
|
| 561 | throw new Error('Invalid event signature');
|
| 562 | if (canonicalHash(parsed.payload) !== parsed.payloadHash)
|
| 563 | throw new Error('Event payload hash mismatch');
|
| 564 | const existing = this.events.get(parsed.eventId);
|
| 565 | if (existing && canonicalHash(existing) !== canonicalHash(parsed))
|
| 566 | throw new Error('Conflicting duplicate event');
|
| 567 | this.events.set(parsed.eventId, parsed);
|
| 568 | this.store.append('event', parsed.eventId, parsed);
|
| 569 | return parsed;
|
| 570 | }
|
| 571 |
|
| 572 | createConflict(
|
| 573 | input: Omit<Conflict, 'conflictId' | 'status' | 'permissions'> & { participants: string[] },
|
| 574 | ): Conflict {
|
| 575 | const permissions = Object.fromEntries(input.participants.map((id) => [id, false]));
|
| 576 | const conflict: Conflict = {
|
| 577 | ...input,
|
| 578 | conflictId: randomUUID(),
|
| 579 | permissions,
|
| 580 | status: 'pending_consent',
|
| 581 | };
|
| 582 | delete (conflict as any).participants;
|
| 583 | this.conflicts.set(conflict.conflictId, conflict);
|
| 584 | this.store.append('conflict', `${conflict.conflictId}:created`, conflict);
|
| 585 | return conflict;
|
| 586 | }
|
| 587 |
|
| 588 | permitMediation(conflictId: string, agentId: string): Conflict {
|
| 589 | const conflict = this.requireConflict(conflictId);
|
| 590 | if (!(agentId in conflict.permissions)) throw new Error('Agent is not a conflict participant');
|
| 591 | conflict.permissions[agentId] = true;
|
| 592 | if (Object.values(conflict.permissions).every(Boolean)) conflict.status = 'open';
|
| 593 | this.store.append('conflict', `${conflict.conflictId}:permit:${agentId}`, conflict);
|
| 594 | return conflict;
|
| 595 | }
|
| 596 |
|
| 597 | resolveConflict(conflictId: string, resolution: string): Conflict {
|
| 598 | const conflict = this.requireConflict(conflictId);
|
| 599 | if (conflict.status !== 'open') throw new Error('Mutual mediation consent required');
|
| 600 | conflict.status = 'resolved';
|
| 601 | conflict.resolution = resolution;
|
| 602 | this.store.append('conflict', `${conflict.conflictId}:resolved`, conflict);
|
| 603 | return conflict;
|
| 604 | }
|
| 605 |
|
| 606 | submitReceipt(receipt: Receipt): Receipt {
|
| 607 | const parsed = this.verifyReceipt(receipt);
|
| 608 | this.receipts.set(parsed.receiptId, parsed);
|
| 609 | this.store.append('receipt', parsed.receiptId, parsed);
|
| 610 | return parsed;
|
| 611 | }
|
| 612 |
|
| 613 | verifyReceipt(receipt: Receipt): Receipt {
|
| 614 | const parsed = ReceiptSchema.parse(receipt);
|
| 615 | const required = parsed.unilateral
|
| 616 | ? Object.keys(parsed.signatures).slice(0, 1)
|
| 617 | : parsed.participants;
|
| 618 | if (
|
| 619 | !required.length ||
|
| 620 | required.some((party) => {
|
| 621 | const agent = this.agents.get(party);
|
| 622 | return (
|
| 623 | !agent ||
|
| 624 | !verifyNamed(parsed as unknown as Record<string, unknown>, party, agent.publicKey)
|
| 625 | );
|
| 626 | })
|
| 627 | )
|
| 628 | throw new Error('Receipt signature verification failed');
|
| 629 | return parsed;
|
| 630 | }
|
| 631 |
|
| 632 | submitFeedback(item: Feedback): { revealed: boolean } {
|
| 633 | const parsed = FeedbackSchema.parse(item);
|
| 634 | const receipt = this.receipts.get(parsed.receiptId);
|
| 635 | const reviewer = this.agents.get(parsed.reviewerAgentId);
|
| 636 | if (
|
| 637 | !receipt ||
|
| 638 | !reviewer ||
|
| 639 | !receipt.participants.includes(parsed.reviewerAgentId) ||
|
| 640 | !receipt.participants.includes(parsed.subjectAgentId)
|
| 641 | )
|
| 642 | throw new Error('Feedback requires a valid participant receipt');
|
| 643 | if (!verifyObject(parsed as unknown as Record<string, unknown>, reviewer.publicKey))
|
| 644 | throw new Error('Invalid feedback signature');
|
| 645 | this.feedback.set(parsed.feedbackId, parsed);
|
| 646 | this.store.append('feedback', parsed.feedbackId, parsed);
|
| 647 | const related = [...this.feedback.values()].filter(
|
| 648 | (value) => value.receiptId === parsed.receiptId,
|
| 649 | );
|
| 650 | const revealed =
|
| 651 | receipt.unilateral ||
|
| 652 | new Set(related.map((value) => value.reviewerAgentId)).size >= receipt.participants.length;
|
| 653 | if (revealed) for (const feedback of related) this.applyFeedback(feedback, receipt);
|
| 654 | return { revealed };
|
| 655 | }
|
| 656 |
|
| 657 | getRisk(agentId: string, version: string, taskCategory: string): RiskDecision {
|
| 658 | const current = this.profiles.get(`${agentId}|${version}|${taskCategory}`);
|
| 659 | const otherVersions = [...this.profiles.values()].filter(
|
| 660 | (p) => p.agentId === agentId && p.taskCategory === taskCategory,
|
| 661 | );
|
| 662 | if (!current) {
|
| 663 | const continuity = otherVersions.length
|
| 664 | ? Math.min(
|
| 665 | 0.2,
|
| 666 | Math.max(
|
| 667 | ...otherVersions.map((profile) => profile.sampleSize / (profile.sampleSize + 5)),
|
| 668 | ) * 0.25,
|
| 669 | )
|
| 670 | : 0;
|
| 671 | return this.decision(
|
| 672 | 'CHALLENGE',
|
| 673 | taskCategory,
|
| 674 | continuity,
|
| 675 | ['limited_verified_history'],
|
| 676 | ['Agent version has limited task-specific evidence'],
|
| 677 | ['request_evidence'],
|
| 678 | );
|
| 679 | }
|
| 680 | const ageDays = Math.max(0, (Date.now() - Date.parse(current.updatedAt)) / 86_400_000);
|
| 681 | const freshness = Math.exp(-ageDays / 180);
|
| 682 | const quality =
|
| 683 | (current.completion +
|
| 684 | current.acceptance +
|
| 685 | current.specification +
|
| 686 | current.deadline +
|
| 687 | current.communication +
|
| 688 | current.evidence +
|
| 689 | current.scope +
|
| 690 | (1 - current.disputes)) /
|
| 691 | 8;
|
| 692 | const confidence = Math.min(0.95, (current.sampleSize / (current.sampleSize + 5)) * freshness);
|
| 693 | const decision = quality >= 0.7 && confidence >= 0.25 ? 'ALLOW' : 'CHALLENGE';
|
| 694 | return {
|
| 695 | decision,
|
| 696 | confidence,
|
| 697 | taskCategory,
|
| 698 | sampleSize: current.sampleSize,
|
| 699 | dimensions: {
|
| 700 | completionReliability: current.completion,
|
| 701 | outputAcceptance: current.acceptance,
|
| 702 | contractAdherence: (current.specification + current.scope) / 2,
|
| 703 | deadlineReliability: current.deadline,
|
| 704 | communicationQuality: current.communication,
|
| 705 | evidenceQuality: current.evidence,
|
| 706 | disputeRate: current.disputes,
|
| 707 | },
|
| 708 | reasons: [`contextual_quality=${quality.toFixed(2)}`],
|
| 709 | warnings: confidence < 0.5 ? ['Limited sample size'] : [],
|
| 710 | dataFreshness: { behaviouralProfile: current.updatedAt },
|
| 711 | requiredChallenges: decision === 'CHALLENGE' ? ['request_evidence'] : [],
|
| 712 | };
|
| 713 | }
|
| 714 |
|
| 715 | setContributionConsent(agentId: string, enabled: boolean): void {
|
| 716 | this.requireAgent(agentId);
|
| 717 | const previous = this.contributionConsent.get(agentId);
|
| 718 | this.contributionConsent.set(
|
| 719 | agentId,
|
| 720 | enabled
|
| 721 | ? { enabled: true, grantedAt: new Date().toISOString() }
|
| 722 | : {
|
| 723 | enabled: false,
|
| 724 | grantedAt: previous?.grantedAt ?? new Date().toISOString(),
|
| 725 | revokedAt: new Date().toISOString(),
|
| 726 | },
|
| 727 | );
|
| 728 | this.store.append('consent', `${agentId}:${Date.now()}`, {
|
| 729 | agentId,
|
| 730 | ...this.contributionConsent.get(agentId),
|
| 731 | });
|
| 732 | }
|
| 733 |
|
| 734 | networkContribution(event: InteractionEvent): Record<string, unknown> | null {
|
| 735 | if (!this.contributionConsent.get(event.agentId)?.enabled || event.visibility === 'local_only')
|
| 736 | return null;
|
| 737 | return {
|
| 738 | eventId: event.eventId,
|
| 739 | interactionId: event.interactionId,
|
| 740 | eventType: event.eventType,
|
| 741 | agentId: event.agentId,
|
| 742 | agentVersion: event.agentVersion,
|
| 743 | timestamp: event.timestamp,
|
| 744 | visibility: event.visibility,
|
| 745 | provenance: event.provenance,
|
| 746 | payloadHash: event.payloadHash,
|
| 747 | evidenceRefs: event.evidenceRefs ?? [],
|
| 748 | signature: event.signature,
|
| 749 | };
|
| 750 | }
|
| 751 |
|
| 752 | private applyFeedback(feedback: Feedback, receipt: Receipt): void {
|
| 753 | const taskCategory = this.contracts.get(feedback.interactionId)?.taskCategory ?? 'unknown';
|
| 754 | const version = receipt.agentVersions[feedback.subjectAgentId] ?? 'unknown';
|
| 755 | const key = `${feedback.subjectAgentId}|${version}|${taskCategory}`;
|
| 756 | const previous = this.profiles.get(key);
|
| 757 | const n = previous?.sampleSize ?? 0;
|
| 758 | const next = (old: number | undefined, value: number) => ((old ?? 0) * n + value) / (n + 1);
|
| 759 | const profile = {
|
| 760 | agentId: feedback.subjectAgentId,
|
| 761 | agentVersion: version,
|
| 762 | taskCategory,
|
| 763 | sampleSize: n + 1,
|
| 764 | updatedAt: new Date().toISOString(),
|
| 765 | completion: next(previous?.completion, Number(feedback.taskCompleted)),
|
| 766 | acceptance: next(previous?.acceptance, Number(feedback.outputAccepted)),
|
| 767 | specification: next(previous?.specification, Number(feedback.specificationMatched)),
|
| 768 | deadline: next(previous?.deadline, Number(feedback.deadlineMet)),
|
| 769 | communication: next(previous?.communication, feedback.communicationQuality),
|
| 770 | evidence: next(previous?.evidence, Number(feedback.evidenceProvided)),
|
| 771 | scope: next(previous?.scope, Number(feedback.scopeRespected)),
|
| 772 | disputes: next(previous?.disputes, Number(feedback.disputeRaised)),
|
| 773 | };
|
| 774 | this.profiles.set(key, profile);
|
| 775 | this.store.append('profile', `${key}:${profile.sampleSize}`, profile);
|
| 776 | }
|
| 777 |
|
| 778 | private decision(
|
| 779 | decision: RiskDecision['decision'],
|
| 780 | taskCategory: string,
|
| 781 | confidence: number,
|
| 782 | reasons: string[],
|
| 783 | warnings: string[],
|
| 784 | challenges: string[],
|
| 785 | ): RiskDecision {
|
| 786 | return {
|
| 787 | decision,
|
| 788 | confidence,
|
| 789 | taskCategory,
|
| 790 | sampleSize: 0,
|
| 791 | dimensions: {},
|
| 792 | reasons,
|
| 793 | warnings,
|
| 794 | dataFreshness: {},
|
| 795 | requiredChallenges: challenges,
|
| 796 | };
|
| 797 | }
|
| 798 | private requireAgent(id: string): AgentIdentity {
|
| 799 | const value = this.agents.get(id);
|
| 800 | if (!value) throw new Error(`Agent not found: ${id}`);
|
| 801 | return value;
|
| 802 | }
|
| 803 | private requireConflict(id: string): Conflict {
|
| 804 | const value = this.conflicts.get(id);
|
| 805 | if (!value) throw new Error('Conflict not found');
|
| 806 | return value;
|
| 807 | }
|
| 808 | }
|
| 809 |
|
| 810 | export interface FactCheckProvider {
|
| 811 | check(claim: string, permission?: boolean): Promise<FactCheckResult>;
|
| 812 | }
|
| 813 |
|
| 814 | export class FixtureFactCheckProvider implements FactCheckProvider {
|
| 815 | constructor(
|
| 816 | private fixtures: Record<
|
| 817 | string,
|
| 818 | { status: FactCheckResult['status']; evidence: string[] }
|
| 819 | > = {},
|
| 820 | ) {}
|
| 821 | async check(claim: string, permission = true): Promise<FactCheckResult> {
|
| 822 | if (!permission)
|
| 823 | return result(
|
| 824 | claim,
|
| 825 | 'objective',
|
| 826 | 'insufficient_permission',
|
| 827 | 1,
|
| 828 | [],
|
| 829 | 'Grant evidence-source permission',
|
| 830 | );
|
| 831 | if (/\b(i think|i prefer|best|beautiful)\b/i.test(claim))
|
| 832 | return result(claim, 'subjective', 'not_fact_checkable', 1, [], 'Treat as an opinion');
|
| 833 | if (/\b(will|might|forecast|predict)\b/i.test(claim))
|
| 834 | return result(
|
| 835 | claim,
|
| 836 | 'prediction',
|
| 837 | 'not_fact_checkable',
|
| 838 | 0.9,
|
| 839 | [],
|
| 840 | 'Track as a prediction, not a fact',
|
| 841 | );
|
| 842 | const fixture = this.fixtures[claim];
|
| 843 | if (!fixture)
|
| 844 | return result(claim, 'objective', 'unverified', 0, [], 'Request authoritative evidence');
|
| 845 | return result(
|
| 846 | claim,
|
| 847 | 'objective',
|
| 848 | fixture.status,
|
| 849 | 0.95,
|
| 850 | fixture.evidence,
|
| 851 | fixture.status === 'contradicted' ? 'Challenge the claim with cited evidence' : 'Continue',
|
| 852 | );
|
| 853 | }
|
| 854 | }
|
| 855 |
|
| 856 | function result(
|
| 857 | claim: string,
|
| 858 | claimType: FactCheckResult['claimType'],
|
| 859 | status: FactCheckResult['status'],
|
| 860 | confidence: number,
|
| 861 | evidence: string[],
|
| 862 | action: string,
|
| 863 | ): FactCheckResult {
|
| 864 | return {
|
| 865 | claim,
|
| 866 | claimType,
|
| 867 | status,
|
| 868 | confidence,
|
| 869 | evidenceReferences: evidence,
|
| 870 | sourceAuthority: evidence.length ? 'authoritative_fixture' : 'none',
|
| 871 | sourceFreshness: new Date().toISOString(),
|
| 872 | contradictingEvidence: status === 'contradicted' ? evidence : [],
|
| 873 | suggestedNextAction: action,
|
| 874 | };
|
| 875 | }
|
| 876 |
|
| 877 | export { signNamed, signObject };
|