import { WORKFLOW_ENGINE, normalizeRunSpec, } from './contracts.mjs'; const EXECUTION_REQUEST_VERSION = 'workflow-execution-request-v1'; const EXECUTION_DECISION_VERSION = 'workflow-execution-decision-v1'; const MAX_IDEMPOTENCY_KEY_CHARACTERS = 200; function clampTimeoutMs(value, fallback = 30_000) { const number = Number(value); if (!Number.isFinite(number)) return fallback; return Math.min(60_000, Math.max(500, Math.floor(number))); } function normalizeIdempotencyKey(value) { return String(value ?? '').trim().slice(0, MAX_IDEMPOTENCY_KEY_CHARACTERS); } export function normalizeWorkflowExecutionRequest(input = {}) { const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {}; const spec = normalizeRunSpec(source.spec ?? source.runSpec ?? source); return { version: EXECUTION_REQUEST_VERSION, spec, idempotencyKey: normalizeIdempotencyKey(source.idempotencyKey), authorization: { executionAllowed: source.authorization?.executionAllowed === true, actorId: String(source.authorization?.actorId ?? '').trim() || null, }, controls: { timeoutMs: clampTimeoutMs(source.controls?.timeoutMs), fallbackAllowed: source.controls?.fallbackAllowed !== false, cancellationAllowed: source.controls?.cancellationAllowed !== false, }, }; } function disabledHandoffError(decision) { const error = new Error( `Workflow execution handoff is disabled; ${decision.effectiveEngine} remains the executor`, ); error.code = 'WORKFLOW_EXECUTION_HANDOFF_DISABLED'; error.status = 409; error.decision = decision; return error; } /** * Phase 3 boundary adapter. * * It turns control-plane routing into a framework-neutral, auditable decision. * It intentionally has no remote dispatch path yet: Native remains owned by the * existing Portal Agent Run and non-Native candidates are dry-run only. */ export function createWorkflowExecutionAdapter({ configService, engineRegistry, } = {}) { if (!configService?.selectEngine) { throw new Error('Workflow execution adapter requires orchestrator config service'); } if (!engineRegistry?.has) { throw new Error('Workflow execution adapter requires workflow engine registry'); } async function plan(input) { const request = normalizeWorkflowExecutionRequest(input); const selection = await configService.selectEngine({ runId: request.spec.runId, requestId: request.spec.requestId, userId: request.spec.subject.userId, workflowName: request.spec.workflow.name, }); const candidateEngine = selection.candidateEngine ?? selection.engine ?? WORKFLOW_ENGINE.NATIVE; const nonNativeCandidate = candidateEngine !== WORKFLOW_ENGINE.NATIVE; const gates = { implementation: false, routingSelected: nonNativeCandidate, engineRegistered: engineRegistry.has(candidateEngine), idempotencyKeyPresent: Boolean(request.idempotencyKey), executionAuthorized: request.authorization.executionAllowed, killSwitchOpen: selection.reason !== 'kill_switch', }; return { version: EXECUTION_DECISION_VERSION, runId: request.spec.runId, requestId: request.spec.requestId, mode: selection.mode, candidateEngine, effectiveEngine: WORKFLOW_ENGINE.NATIVE, fallbackEngine: selection.fallbackEngine ?? WORKFLOW_ENGINE.NATIVE, dryRun: nonNativeCandidate, handoffAllowed: false, reason: nonNativeCandidate ? 'execution_gate_disabled' : selection.reason ?? 'native_selected', candidateReason: selection.candidateReason ?? null, configVersion: selection.configVersion, gates, controls: request.controls, }; } return { plan, async start(input) { const decision = await plan(input); if (decision.candidateEngine === WORKFLOW_ENGINE.NATIVE) { return { dispatched: false, reason: 'native_execution_owned_by_portal', decision, }; } throw disabledHandoffError(decision); }, async resume() { throw disabledHandoffError({ effectiveEngine: WORKFLOW_ENGINE.NATIVE, reason: 'execution_gate_disabled', }); }, async cancel() { throw disabledHandoffError({ effectiveEngine: WORKFLOW_ENGINE.NATIVE, reason: 'execution_gate_disabled', }); }, }; } export const workflowExecutionAdapterInternals = { EXECUTION_DECISION_VERSION, EXECUTION_REQUEST_VERSION, MAX_IDEMPOTENCY_KEY_CHARACTERS, clampTimeoutMs, disabledHandoffError, normalizeIdempotencyKey, };