import { Inject, Injectable } from '@nestjs/common'; import { InteractionCoordinationClient, type CoordinationObservation, type CoordinationResult, type CoordinationScope, type InteractionCoordinationIdentity, type InteractionCoordinationPort, type HandoffReceipt, } from '@mosaicstack/coord'; import type { RuntimeProviderRequestContext } from '../agent/runtime-provider-registry.service.js'; import type { CreateHandoffDto } from './interaction-coordination.dto.js'; export const COORDINATION_PORT = Symbol('COORDINATION_PORT'); export const COORDINATION_CONFIG = Symbol('COORDINATION_CONFIG'); const HANDOFF_TRACKING_TTL_MS = 60 * 60 * 1_000; const MAX_TRACKED_HANDOFFS = 1_000; const MAX_IDEMPOTENCY_KEY_LENGTH = 128; const MAX_SUMMARY_LENGTH = 2_048; const MAX_CONTEXT_LENGTH = 8_192; const MAX_MISSION_ID_LENGTH = 128; export interface InteractionCoordinationConfig { interactionAgentId?: string; orchestrationAgentId?: string; } interface HandoffOwner { actorId: string; tenantId: string; requesterAgentId: string; correlationId: string; expiresAt: number; } interface NormalizedHandoffRequest { idempotencyKey: string; summary: string; context?: string; missionId?: string; } interface TrackedHandoff { request: NormalizedHandoffRequest; receipt: Promise; expiresAt: number; } /** * Gateway authority boundary for the interaction agent. It derives requester, * actor, and tenant from trusted server configuration and authentication; no * channel request can name a target or gain orchestrator-owned orchestration verbs. */ @Injectable() export class InteractionCoordinationService { private readonly owners = new Map(); private readonly handoffsByIdempotencyKey = new Map(); constructor( @Inject(COORDINATION_PORT) private readonly port: InteractionCoordinationPort, @Inject(COORDINATION_CONFIG) private readonly config: InteractionCoordinationConfig, private readonly handoffIdFactory: () => string = (): string => crypto.randomUUID(), ) {} async handoff( request: CreateHandoffDto, context: RuntimeProviderRequestContext, ): Promise { this.pruneExpiredTracking(); const normalized = this.normalizeRequest(request); const scope = this.scope(context); const idempotencyKey = this.idempotencyKey(normalized.idempotencyKey, scope); const existing = this.handoffsByIdempotencyKey.get(idempotencyKey); if (existing !== undefined) { if (!sameRequest(existing.request, normalized)) { throw new InteractionCoordinationGatewayError( 'handoff_conflict', 'Handoff idempotency key is already bound to different immutable input', ); } return existing.receipt; } const pending = this.deliverHandoff(this.handoffIdFactory(), normalized, scope); const tracked: TrackedHandoff = { request: normalized, receipt: pending, expiresAt: this.expiresAt(), }; this.handoffsByIdempotencyKey.set(idempotencyKey, tracked); this.enforceTrackingLimit(this.handoffsByIdempotencyKey); try { return await pending; } catch (error: unknown) { if (this.handoffsByIdempotencyKey.get(idempotencyKey) === tracked) { this.handoffsByIdempotencyKey.delete(idempotencyKey); } throw error; } } async observe( handoffId: string, context: RuntimeProviderRequestContext, ): Promise { this.pruneExpiredTracking(); const scope = this.scope(context); const owner = this.ownerFor(handoffId, scope); return this.client().observe(handoffId, { ...scope, correlationId: owner.correlationId }); } async result( handoffId: string, context: RuntimeProviderRequestContext, ): Promise { this.pruneExpiredTracking(); const scope = this.scope(context); const owner = this.ownerFor(handoffId, scope); return this.client().result(handoffId, { ...scope, correlationId: owner.correlationId }); } private async deliverHandoff( handoffId: string, request: NormalizedHandoffRequest, scope: CoordinationScope, ): Promise { const receipt = await this.client((): string => handoffId).handoff(request, scope); const owner: HandoffOwner = { actorId: scope.actorId, tenantId: scope.tenantId, requesterAgentId: scope.requesterAgentId, correlationId: scope.correlationId, expiresAt: this.expiresAt(), }; const existing = this.owners.get(receipt.handoffId); if (existing !== undefined && !sameOwner(existing, owner)) { throw new InteractionCoordinationGatewayError( 'handoff_conflict', 'Handoff ID is already bound to a different authenticated scope', ); } this.owners.set(receipt.handoffId, owner); this.enforceTrackingLimit(this.owners); return receipt; } private client(handoffIdFactory?: () => string): InteractionCoordinationClient { return new InteractionCoordinationClient(this.identity(), this.port, handoffIdFactory); } private identity(): InteractionCoordinationIdentity { const interactionAgentId = this.config.interactionAgentId?.trim(); const orchestrationAgentId = this.config.orchestrationAgentId?.trim(); if (!interactionAgentId) { throw new InteractionCoordinationGatewayError( 'unconfigured_requester', 'Interaction agent identity is not configured', ); } if (!orchestrationAgentId) { throw new InteractionCoordinationGatewayError( 'unconfigured_target', 'Orchestration agent identity is not configured', ); } return { interactionAgentId, orchestrationAgentId }; } private scope(context: RuntimeProviderRequestContext): CoordinationScope { const identity = this.identity(); return Object.freeze({ actorId: context.actorScope.userId, tenantId: context.actorScope.tenantId, correlationId: context.correlationId, requesterAgentId: identity.interactionAgentId, }); } private normalizeRequest(request: CreateHandoffDto): NormalizedHandoffRequest { if (typeof request !== 'object' || request === null) { throw new InteractionCoordinationGatewayError( 'invalid_request', 'Handoff request is invalid', ); } const idempotencyKey = this.requiredString( request.idempotencyKey, 'idempotency key', MAX_IDEMPOTENCY_KEY_LENGTH, ); const summary = this.requiredString(request.summary, 'summary', MAX_SUMMARY_LENGTH); const context = this.optionalString(request.context, 'context', MAX_CONTEXT_LENGTH); const missionId = this.optionalString(request.missionId, 'mission ID', MAX_MISSION_ID_LENGTH); return Object.freeze({ idempotencyKey, summary, ...(context === undefined ? {} : { context }), ...(missionId === undefined ? {} : { missionId }), }); } private idempotencyKey(requestKey: string, scope: CoordinationScope): string { return `${scope.tenantId}\u0000${scope.actorId}\u0000${scope.requesterAgentId}\u0000${requestKey}`; } private requiredString(value: unknown, field: string, maximumLength: number): string { if (typeof value !== 'string') { throw new InteractionCoordinationGatewayError( 'invalid_request', `Handoff ${field} must be a string`, ); } const normalized = value.trim(); if (normalized.length === 0 || normalized.length > maximumLength) { throw new InteractionCoordinationGatewayError( 'invalid_request', `Handoff ${field} is invalid`, ); } return normalized; } private optionalString(value: unknown, field: string, maximumLength: number): string | undefined { if (value === undefined) return undefined; return this.requiredString(value, field, maximumLength); } private expiresAt(): number { return Date.now() + HANDOFF_TRACKING_TTL_MS; } private pruneExpiredTracking(): void { const now = Date.now(); for (const [key, tracked] of this.handoffsByIdempotencyKey) { if (tracked.expiresAt <= now) this.handoffsByIdempotencyKey.delete(key); } for (const [key, owner] of this.owners) { if (owner.expiresAt <= now) this.owners.delete(key); } } private enforceTrackingLimit(entries: Map): void { while (entries.size > MAX_TRACKED_HANDOFFS) { const oldest = entries.keys().next().value; if (typeof oldest !== 'string') return; entries.delete(oldest); } } private ownerFor(handoffId: string, scope: CoordinationScope): HandoffOwner { const owner = this.owners.get(handoffId); if (owner === undefined) { throw new InteractionCoordinationGatewayError('not_found', 'Handoff was not found'); } if ( owner.tenantId !== scope.tenantId || owner.actorId !== scope.actorId || owner.requesterAgentId !== scope.requesterAgentId ) { throw new InteractionCoordinationGatewayError( 'cross_tenant_forbidden', 'Handoff is outside the authenticated scope', ); } return owner; } } export type InteractionCoordinationGatewayErrorCode = | 'cross_tenant_forbidden' | 'handoff_conflict' | 'invalid_request' | 'not_found' | 'unconfigured_requester' | 'unconfigured_target'; function sameOwner(left: HandoffOwner, right: HandoffOwner): boolean { return ( left.actorId === right.actorId && left.tenantId === right.tenantId && left.requesterAgentId === right.requesterAgentId ); } function sameRequest(left: NormalizedHandoffRequest, right: NormalizedHandoffRequest): boolean { return ( left.idempotencyKey === right.idempotencyKey && left.summary === right.summary && left.context === right.context && left.missionId === right.missionId ); } export class InteractionCoordinationGatewayError extends Error { constructor( readonly code: InteractionCoordinationGatewayErrorCode, message: string, ) { super(message); this.name = InteractionCoordinationGatewayError.name; } }