import crypto from 'node:crypto'; import { normalizeWorkflowEngineId } from './contracts.mjs'; const EXECUTOR_JOB_REQUEST_VERSION = 'executor-job-request-v1'; const EXECUTOR_JOB_STATE_VERSION = 'executor-job-state-v1'; const EXECUTOR_JOB_EVENT_VERSION = 'executor-job-event-v1'; const EXECUTOR_DISPATCH_IMPLEMENTED = true; const MAX_INSTRUCTION_CHARACTERS = 32_000; const MAX_EVENT_PAGE_SIZE = 500; const TERMINAL_JOB_STATUSES = new Set([ 'succeeded', 'failed', 'cancelled', 'timed_out', 'blocked', ]); const PHASE3_EXECUTOR_DESCRIPTORS = Object.freeze([ Object.freeze({ id: 'goosed', label: 'Goosed', kind: 'remote', capabilities: Object.freeze(['session-execution', 'streaming', 'tools']), }), Object.freeze({ id: 'aider', label: 'Aider', kind: 'worker', capabilities: Object.freeze(['code-edit', 'multi-file']), }), Object.freeze({ id: 'openhands', label: 'OpenHands', kind: 'worker', capabilities: Object.freeze(['repo-task', 'code-edit', 'command-execution']), }), ]); function clone(value) { return typeof structuredClone === 'function' ? structuredClone(value) : JSON.parse(JSON.stringify(value)); } function clampInteger(value, fallback, min, max) { const number = Number(value); if (!Number.isFinite(number)) return fallback; return Math.min(max, Math.max(min, Math.floor(number))); } function normalizeReference(value, fallbackKind) { const source = value && typeof value === 'object' && !Array.isArray(value) ? value : {}; const kind = String(source.kind ?? fallbackKind ?? '').trim().slice(0, 64); const id = String(source.id ?? '').trim().slice(0, 256); return kind && id ? { kind, id } : null; } function stableValue(value) { if (Array.isArray(value)) return value.map(stableValue); if (!value || typeof value !== 'object') return value; return Object.fromEntries( Object.keys(value) .sort() .map((key) => [key, stableValue(value[key])]), ); } function fingerprint(value) { return crypto .createHash('sha256') .update(JSON.stringify(stableValue(value))) .digest('hex'); } function gatewayError(code, message, status = 409) { const error = new Error(message); error.code = code; error.status = status; return error; } export function buildExecutorJobEvent({ eventId = crypto.randomUUID(), jobId, sequence = 0, type, timestamp = Date.now(), data = null, } = {}) { const normalizedJobId = String(jobId ?? '').trim(); const normalizedType = String(type ?? '').trim(); if (!normalizedJobId) { throw gatewayError('EXECUTOR_EVENT_JOB_ID_REQUIRED', 'Executor event requires jobId', 422); } if (!normalizedType) { throw gatewayError('EXECUTOR_EVENT_TYPE_REQUIRED', 'Executor event requires type', 422); } return { version: EXECUTOR_JOB_EVENT_VERSION, eventId: String(eventId), jobId: normalizedJobId, sequence: Math.max(0, Number(sequence) || 0), type: normalizedType, timestamp: Number(timestamp) || Date.now(), data: data == null ? null : clone(data), }; } export function normalizeExecutorJobRequest(input = {}) { const source = input && typeof input === 'object' && !Array.isArray(input) ? input : {}; const jobId = String(source.jobId ?? crypto.randomUUID()).trim(); const idempotencyKey = String(source.idempotencyKey ?? '').trim().slice(0, 200); const executor = normalizeWorkflowEngineId(source.executor, ''); const instruction = String(source.task?.instruction ?? source.instruction ?? '') .trim() .slice(0, MAX_INSTRUCTION_CHARACTERS); if (!jobId) throw gatewayError('EXECUTOR_JOB_ID_REQUIRED', 'Executor job requires jobId', 422); if (!/^[a-zA-Z0-9:_-]{1,128}$/.test(jobId)) { throw gatewayError( 'EXECUTOR_JOB_ID_INVALID', 'Executor jobId must be a safe identifier no longer than 128 characters', 422, ); } if (!idempotencyKey) { throw gatewayError( 'EXECUTOR_IDEMPOTENCY_KEY_REQUIRED', 'Executor job requires idempotencyKey', 422, ); } if (!executor) { throw gatewayError('EXECUTOR_ID_REQUIRED', 'Executor job requires executor', 422); } if (!instruction) { throw gatewayError('EXECUTOR_INSTRUCTION_REQUIRED', 'Executor job requires instruction', 422); } return { version: EXECUTOR_JOB_REQUEST_VERSION, jobId, idempotencyKey, executor, task: { type: String(source.task?.type ?? 'code_task').trim().slice(0, 128) || 'code_task', instruction, workspaceRef: normalizeReference(source.task?.workspaceRef, 'workspace'), inputRefs: Array.isArray(source.task?.inputRefs) ? source.task.inputRefs .map((item) => normalizeReference(item, 'artifact')) .filter(Boolean) .slice(0, 100) : [], }, subject: { tenantId: String(source.subject?.tenantId ?? '').trim() || null, userId: String(source.subject?.userId ?? '').trim() || null, }, authorization: { executionAllowed: source.authorization?.executionAllowed === true, actorId: String(source.authorization?.actorId ?? '').trim() || null, }, policy: { sideEffectsAllowed: source.policy?.sideEffectsAllowed === true, networkAllowed: source.policy?.networkAllowed === true, }, controls: { timeoutMs: clampInteger(source.controls?.timeoutMs, 15 * 60_000, 500, 60 * 60_000), cancellationAllowed: source.controls?.cancellationAllowed !== false, fallbackExecutor: normalizeWorkflowEngineId(source.controls?.fallbackExecutor, ''), maxAttempts: clampInteger(source.controls?.maxAttempts, 3, 1, 20), }, metadata: source.metadata && typeof source.metadata === 'object' ? clone(source.metadata) : {}, }; } function validateExecutorAdapter(adapter) { if (!adapter || typeof adapter !== 'object') { throw new Error('Executor adapter must be an object'); } const id = normalizeWorkflowEngineId(adapter.id, ''); if (!id) throw new Error('Executor adapter requires a valid id'); for (const method of ['submit', 'cancel', 'getState', 'streamEvents']) { if (typeof adapter[method] !== 'function') { throw new Error(`Executor adapter ${id} missing method: ${method}`); } } return id; } export function createExecutorAdapterRegistry(initialAdapters = []) { const adapters = new Map(); function register(adapter) { const id = validateExecutorAdapter(adapter); if (adapters.has(id)) throw new Error(`Executor adapter already registered: ${id}`); adapters.set(id, adapter); return adapter; } for (const adapter of initialAdapters) register(adapter); return { register, has(id) { return adapters.has(normalizeWorkflowEngineId(id, '')); }, get(id) { return adapters.get(normalizeWorkflowEngineId(id, '')) ?? null; }, list() { return [...adapters.values()].map((adapter) => ({ id: adapter.id, label: adapter.label ?? adapter.id, kind: adapter.kind ?? 'plugin', enabled: adapter.enabled === true, dispatchImplemented: adapter.dispatchImplemented === true, status: adapter.status ?? 'unknown', capabilities: Array.isArray(adapter.capabilities) ? [...adapter.capabilities] : [], })); }, }; } export function createDisabledExecutorAdapter(descriptor) { const id = normalizeWorkflowEngineId(descriptor?.id, ''); if (!id) throw new Error('Disabled executor adapter requires a valid id'); const reject = () => { throw gatewayError( 'EXECUTOR_ADAPTER_DISABLED', `Executor adapter ${id} is contract-only and cannot dispatch`, ); }; return { id, label: descriptor.label ?? id, kind: descriptor.kind ?? 'plugin', enabled: false, dispatchImplemented: false, status: 'contract-only', capabilities: Array.isArray(descriptor.capabilities) ? [...descriptor.capabilities] : [], submit: reject, cancel: reject, getState: reject, async *streamEvents() { reject(); }, }; } export function createWorkerQueueExecutorAdapter(descriptor, { enabled = false } = {}) { const id = normalizeWorkflowEngineId(descriptor?.id, ''); if (!id) throw new Error('Worker queue executor adapter requires a valid id'); const reject = () => { throw gatewayError( 'EXECUTOR_WORKER_CLAIM_REQUIRED', `Executor adapter ${id} is dispatched through the worker claim protocol`, ); }; return { id, label: descriptor.label ?? id, kind: descriptor.kind ?? 'worker', enabled, dispatchImplemented: true, status: enabled ? 'worker-queue' : 'disabled', capabilities: Array.isArray(descriptor.capabilities) ? [...descriptor.capabilities, 'lease-fencing'] : ['lease-fencing'], submit: reject, cancel: reject, getState: reject, async *streamEvents() { reject(); }, }; } export function createPhase3ExecutorAdapterRegistry() { return createExecutorAdapterRegistry( PHASE3_EXECUTOR_DESCRIPTORS.map(createDisabledExecutorAdapter), ); } export function listPhase3ExecutorAdapters() { return createPhase3ExecutorAdapterRegistry().list(); } export function createPhase5ExecutorAdapterRegistry({ enabledExecutors = [] } = {}) { const enabled = new Set( (Array.isArray(enabledExecutors) ? enabledExecutors : []) .map((id) => normalizeWorkflowEngineId(id, '')) .filter(Boolean), ); return createExecutorAdapterRegistry( PHASE3_EXECUTOR_DESCRIPTORS.map((descriptor) => ( createWorkerQueueExecutorAdapter(descriptor, { enabled: enabled.has(descriptor.id) }) )), ); } export function listPhase5ExecutorAdapters(options = {}) { return createPhase5ExecutorAdapterRegistry(options).list(); } export function projectExecutorJobForRead(record) { if (!record) return null; const { request: _request, requestFingerprint: _requestFingerprint, lease, ...safe } = clone(record); return { ...safe, lease: lease ? { workerId: lease.workerId ?? null, acquiredAt: lease.acquiredAt ?? null, heartbeatAt: lease.heartbeatAt ?? null, expiresAt: lease.expiresAt ?? null, } : null, }; } export function createInMemoryExecutorJobStore() { const byId = new Map(); const byIdempotencyKey = new Map(); const eventsByJobId = new Map(); const workersById = new Map(); function appendEvent(jobId, event) { if (!event) return; const events = eventsByJobId.get(jobId) ?? []; events.push({ ...clone(event), jobId, sequence: events.length + 1, }); eventsByJobId.set(jobId, events); } function transitionResult(applied, record, reason = null) { return { applied, record: record ? clone(record) : null, reason, }; } return { kind: 'memory', durable: false, async getById(jobId) { const value = byId.get(String(jobId ?? '').trim()); return value ? clone(value) : null; }, async getByIdempotencyKey(key) { const jobId = byIdempotencyKey.get(String(key ?? '').trim()); const value = jobId ? byId.get(jobId) : null; return value ? clone(value) : null; }, async createIfAbsent(record, { initialEvent = null } = {}) { const existingId = byIdempotencyKey.get(record.idempotencyKey); if (existingId) return { created: false, record: clone(byId.get(existingId)) }; if (byId.has(record.jobId)) { throw gatewayError('EXECUTOR_JOB_ID_CONFLICT', `Executor job id already exists: ${record.jobId}`); } byId.set(record.jobId, clone(record)); byIdempotencyKey.set(record.idempotencyKey, record.jobId); appendEvent(record.jobId, initialEvent); return { created: true, record: clone(record) }; }, async update(record, { event = null } = {}) { if (!byId.has(record.jobId)) return null; byId.set(record.jobId, clone(record)); appendEvent(record.jobId, event); return clone(record); }, async claimNext({ workerId, executors, now, leaseToken, leaseDurationMs, eventFactory, }) { const allowedExecutors = new Set(executors); const candidate = [...byId.values()] .filter((record) => ( allowedExecutors.has(record.executor) && ( record.status === 'queued' || ( record.status === 'retryable' && Number(record.nextAttemptAt ?? 0) <= now ) ) )) .sort((left, right) => ( Number(left.createdAt ?? 0) - Number(right.createdAt ?? 0) || String(left.jobId).localeCompare(String(right.jobId)) ))[0]; if (!candidate) return null; const updated = { ...clone(candidate), status: 'leased', reason: null, attempts: Number(candidate.attempts ?? 0) + 1, nextAttemptAt: null, lease: { token: leaseToken, workerId, acquiredAt: now, heartbeatAt: now, expiresAt: now + leaseDurationMs, }, updatedAt: now, completedAt: null, }; byId.set(updated.jobId, clone(updated)); appendEvent( updated.jobId, typeof eventFactory === 'function' ? eventFactory(updated, candidate) : null, ); return clone(updated); }, async transition(jobId, { expectedStatuses = [], leaseToken = null, updater, eventFactory = null, } = {}) { const normalizedJobId = String(jobId ?? '').trim(); const current = byId.get(normalizedJobId); if (!current) return transitionResult(false, null, 'not_found'); if (expectedStatuses.length && !expectedStatuses.includes(current.status)) { return transitionResult(false, current, 'status_mismatch'); } if (leaseToken != null && current.lease?.token !== leaseToken) { return transitionResult(false, current, 'lease_mismatch'); } const updated = updater(clone(current)); if (!updated || updated.jobId !== current.jobId) { throw new Error('Executor job transition must preserve job identity'); } byId.set(normalizedJobId, clone(updated)); appendEvent( normalizedJobId, typeof eventFactory === 'function' ? eventFactory(updated, current) : null, ); return transitionResult(true, updated); }, async listExpiredLeases({ now, limit = 100 } = {}) { return [...byId.values()] .filter((record) => ( ['leased', 'running'].includes(record.status) && Number(record.lease?.expiresAt ?? 0) <= now )) .sort((left, right) => ( Number(left.lease?.expiresAt ?? 0) - Number(right.lease?.expiresAt ?? 0) )) .slice(0, clampInteger(limit, 100, 1, 500)) .map(clone); }, async getQueueStats({ now = Date.now() } = {}) { const records = [...byId.values()]; const counts = {}; for (const record of records) { counts[record.status] = Number(counts[record.status] ?? 0) + 1; } return { total: records.length, counts, claimable: records.filter((record) => ( record.status === 'queued' || ( record.status === 'retryable' && Number(record.nextAttemptAt ?? 0) <= now ) )).length, expiredLeases: records.filter((record) => ( ['leased', 'running'].includes(record.status) && Number(record.lease?.expiresAt ?? 0) <= now )).length, }; }, async getAdmissionStats({ tenantId = null, userId = null, now = Date.now(), windowMs = 60_000, } = {}) { const activeStatuses = new Set(['queued', 'retryable', 'leased', 'running']); const records = [...byId.values()]; const matchesTenant = (record) => ( tenantId != null && record.request?.subject?.tenantId === tenantId ); const matchesUser = (record) => ( userId != null && record.request?.subject?.userId === userId ); return { globalActive: records.filter((record) => activeStatuses.has(record.status)).length, tenantActive: tenantId == null ? 0 : records.filter((record) => activeStatuses.has(record.status) && matchesTenant(record)).length, userActive: userId == null ? 0 : records.filter((record) => activeStatuses.has(record.status) && matchesUser(record)).length, tenantRecent: tenantId == null ? 0 : records.filter((record) => ( matchesTenant(record) && Number(record.createdAt ?? 0) >= now - windowMs )).length, userRecent: userId == null ? 0 : records.filter((record) => ( matchesUser(record) && Number(record.createdAt ?? 0) >= now - windowMs )).length, }; }, async recordWorkerHeartbeat(record) { workersById.set(record.workerId, clone(record)); return clone(record); }, async listWorkers({ now = Date.now(), staleAfterMs = 60_000 } = {}) { return [...workersById.values()] .sort((left, right) => Number(right.lastSeenAt) - Number(left.lastSeenAt)) .map((worker) => ({ ...clone(worker), stale: Number(worker.lastSeenAt ?? 0) < now - staleAfterMs, })); }, async listEvents(jobId, { after = 0, limit = MAX_EVENT_PAGE_SIZE } = {}) { const cursor = Math.max(0, Number(after) || 0); const pageSize = clampInteger(limit, MAX_EVENT_PAGE_SIZE, 1, MAX_EVENT_PAGE_SIZE); return (eventsByJobId.get(String(jobId ?? '').trim()) ?? []) .filter((event) => event.sequence > cursor) .slice(0, pageSize) .map(clone); }, }; } function validateJobStore(store) { for (const method of [ 'getById', 'getByIdempotencyKey', 'createIfAbsent', 'update', 'listEvents', ]) { if (typeof store?.[method] !== 'function') { throw new Error(`Executor job store missing method: ${method}`); } } } export function createExecutorGateway({ registry = createPhase3ExecutorAdapterRegistry(), store = createInMemoryExecutorJobStore(), nowMs = Date.now, executionEnabled = false, admissionPolicy = null, } = {}) { if (!registry?.has || !registry?.list) { throw new Error('Executor Gateway requires an adapter registry'); } validateJobStore(store); function previewNormalized(request) { const adapter = registry.get(request.executor); const fallbackAdapter = request.controls.fallbackExecutor ? registry.get(request.controls.fallbackExecutor) : null; const fallbackRegistered = Boolean(fallbackAdapter); const fallbackAvailable = fallbackAdapter?.enabled === true && fallbackAdapter?.dispatchImplemented === true; const gates = { implementation: EXECUTOR_DISPATCH_IMPLEMENTED, serviceEnabled: executionEnabled === true, adapterRegistered: Boolean(adapter), adapterEnabled: adapter?.enabled === true, executionAuthorized: request.authorization.executionAllowed, sideEffectsAllowed: request.policy.sideEffectsAllowed, idempotencyKeyPresent: Boolean(request.idempotencyKey), workspaceReferencePresent: Boolean(request.task.workspaceRef), }; return { version: 'executor-dispatch-decision-v1', jobId: request.jobId, executor: request.executor, fallbackExecutor: request.controls.fallbackExecutor || null, fallbackRegistered, fallbackAvailable, dispatchAllowed: Object.values(gates).every(Boolean), reason: !EXECUTOR_DISPATCH_IMPLEMENTED ? 'executor_dispatch_not_implemented' : executionEnabled !== true ? 'executor_execution_disabled' : !adapter ? 'executor_adapter_unregistered' : adapter.enabled !== true ? 'executor_adapter_disabled' : !request.authorization.executionAllowed ? 'executor_execution_unauthorized' : !request.policy.sideEffectsAllowed ? 'executor_side_effects_not_allowed' : !request.task.workspaceRef ? 'executor_workspace_required' : 'queued_for_worker', gates, }; } async function preview(input) { return previewNormalized(normalizeExecutorJobRequest(input)); } async function createJob(input) { const request = normalizeExecutorJobRequest(input); const { jobId: _jobId, idempotencyKey: _idempotencyKey, ...semanticRequest } = request; const requestFingerprint = fingerprint(semanticRequest); const existing = await store.getByIdempotencyKey(request.idempotencyKey); if (existing) { if (existing.requestFingerprint !== requestFingerprint) { throw gatewayError( 'EXECUTOR_IDEMPOTENCY_CONFLICT', `Idempotency key already belongs to executor job ${existing.jobId}`, ); } return { created: false, job: existing }; } let decision = previewNormalized(request); if (decision.dispatchAllowed && admissionPolicy?.evaluate) { const admission = await admissionPolicy.evaluate(request); if (!admission.allowed) { decision = { ...decision, dispatchAllowed: false, reason: admission.reason, admission, }; } else { decision = { ...decision, admission }; } } const now = nowMs(); const dispatchAllowed = decision.dispatchAllowed; const record = { version: EXECUTOR_JOB_STATE_VERSION, jobId: request.jobId, idempotencyKey: request.idempotencyKey, requestFingerprint, executor: request.executor, status: dispatchAllowed ? 'queued' : 'blocked', reason: decision.reason, attempts: 0, maxAttempts: request.controls.maxAttempts, dispatchAllowed, cancellationAllowed: request.controls.cancellationAllowed, timeoutMs: request.controls.timeoutMs, fallbackExecutor: decision.fallbackExecutor, fallbackRegistered: decision.fallbackRegistered, fallbackAvailable: decision.fallbackAvailable, decision, request: dispatchAllowed ? request : null, createdAt: now, updatedAt: now, startedAt: null, completedAt: dispatchAllowed ? null : now, }; const result = await store.createIfAbsent(record, { initialEvent: buildExecutorJobEvent({ jobId: request.jobId, sequence: 1, type: dispatchAllowed ? 'executor_job_queued' : 'executor_job_blocked', timestamp: now, data: { executor: request.executor, status: record.status, reason: record.reason, dispatchAllowed, }, }), }); if ( !result.created && result.record.requestFingerprint !== requestFingerprint ) { throw gatewayError( 'EXECUTOR_IDEMPOTENCY_CONFLICT', `Idempotency key already belongs to executor job ${result.record.jobId}`, ); } return { created: result.created, job: result.record }; } async function cancel(jobId, { reason = 'cancelled_by_request' } = {}) { const current = await store.getById(jobId); if (!current) return null; if (current.status === 'cancelled' || !current.cancellationAllowed) return current; if (TERMINAL_JOB_STATUSES.has(current.status) && current.status !== 'blocked') return current; const now = nowMs(); const applyCancellation = (record) => ({ ...record, status: 'cancelled', reason: String(reason).slice(0, 256), lease: null, updatedAt: now, completedAt: now, }); const eventForCancellation = (updated) => buildExecutorJobEvent({ jobId: updated.jobId, type: 'executor_job_cancelled', timestamp: now, data: { status: updated.status, reason: updated.reason, }, }); if (typeof store.transition === 'function') { const result = await store.transition(current.jobId, { expectedStatuses: ['blocked', 'queued', 'retryable', 'leased', 'running'], updater: applyCancellation, eventFactory: eventForCancellation, }); return result.applied ? result.record : result.record; } const updated = applyCancellation(current); return store.update(updated, { event: eventForCancellation(updated) }); } return { preview, createJob, cancel, async getJob(jobId, { includeRequest = false } = {}) { const record = await store.getById(jobId); return includeRequest ? record : projectExecutorJobForRead(record); }, async listEvents(jobId, { after = 0, limit = MAX_EVENT_PAGE_SIZE } = {}) { const normalizedJobId = String(jobId ?? '').trim(); if (!normalizedJobId) return null; const job = await store.getById(normalizedJobId); if (!job) return null; const cursor = Math.max(0, Number(after) || 0); const events = await store.listEvents(normalizedJobId, { after: cursor, limit: clampInteger(limit, MAX_EVENT_PAGE_SIZE, 1, MAX_EVENT_PAGE_SIZE), }); return { jobId: normalizedJobId, events, nextCursor: events.at(-1)?.sequence ?? cursor, }; }, listAdapters() { return registry.list(); }, status() { return { version: 'executor-gateway-status-v1', dispatchImplemented: EXECUTOR_DISPATCH_IMPLEMENTED, executionEnabled: executionEnabled === true, store: { kind: String(store.kind ?? (store.durable ? 'durable' : 'memory')), durable: store.durable === true, }, adapters: registry.list(), }; }, }; } export const executorGatewayInternals = { EXECUTOR_DISPATCH_IMPLEMENTED, EXECUTOR_JOB_EVENT_VERSION, EXECUTOR_JOB_REQUEST_VERSION, EXECUTOR_JOB_STATE_VERSION, MAX_EVENT_PAGE_SIZE, MAX_INSTRUCTION_CHARACTERS, PHASE3_EXECUTOR_DESCRIPTORS, TERMINAL_JOB_STATUSES, fingerprint, projectExecutorJobForRead, stableValue, };