import express from "express"; import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto"; import { readdir, readFile, stat } from "node:fs/promises"; import { createServer } from "node:http"; import pathModule from "node:path"; import { fileURLToPath } from "node:url"; import { gzipSync } from "node:zlib"; import { Pool } from "pg"; import { handleAssistantCallerRequest } from "../../ontology-core/src/assistant-action-caller.mjs"; import { loadCatalog } from "../../ontology-core/src/catalog.mjs"; const SUPPORTED_SURFACES = new Set(["engine", "ops", "global"]); const SUPPORTED_EXECUTOR_TYPES = new Set(["codex-remote", "ndc-agent-core"]); const SUPPORTED_CONNECTION_MODES = new Set(["hub", "direct"]); const SUPPORTED_EXECUTOR_STATUSES = new Set(["unknown", "online", "offline", "checking", "error"]); const SUPPORTED_THREAD_STATES = new Set(["active", "archived"]); const SUPPORTED_MESSAGE_ROLES = new Set(["user", "assistant", "system", "tool"]); const SUPPORTED_TOOL_PACKS = new Set(["engine", "ops", "ndc-agent-core", "deploy", "docs"]); const SUPPORTED_RUN_STATUSES = new Set(["running", "completed", "failed", "timeout"]); const AI_WORKSPACE_BRIDGE_PACKAGE_NAME = "@nodedc/ai-workspace-bridge"; const AI_WORKSPACE_BRIDGE_PACKAGE_BIN = "ai-workspace-bridge"; const AI_WORKSPACE_BRIDGE_PACKAGE_PUBLIC_PATH = "/api/ai-workspace/bridge-package.tgz"; const AI_WORKSPACE_SETUP_CODE_PREFIX = "ndcaws"; const TAR_BLOCK_SIZE = 512; let cachedBridgePackageTarball = null; const ASSISTANT_ACTION_TOOL_PROFILE_BASE = { schemaVersion: "ai-workspace.assistant-actions.v1", endpoint: "/api/ai-workspace/assistant/v1/actions", modelFlow: "interpret_user_intent_then_call_structured_action", phases: ["preview", "execute"], safety: { read: "execute_after_structured_action_selection", write: "preview_then_explicit_user_confirmation_then_execute", destructive: "forbidden", }, }; const ACCESS_DENIED_TEXT = "Доступ к модулю ограничен, обратитесь к администратору системы."; const RUN_APP_ID_ALIASES = new Map([ ["hub", "launcher"], ["nodedc-hub", "launcher"], ["nodedc_launcher", "launcher"], ["nodedc-launcher", "launcher"], ]); const APP_ROUTING_CATALOG = [ { appId: "launcher", appTitle: "NODE.DC Launcher", surface: "launcher", skillId: "launcher-context", whenToUse: [ "users", "roles", "admin scopes", "application access", "company/workspace contours", "invites", "entitlements", ], appAliases: ["hub"], actionNamespaces: ["hub.*", "launcher.*", "access.*"], actionIdPrefixes: ["hub.", "launcher.", "access."], mcpServerNames: [], requiredScopes: ["launcher:access:read"], deniedText: ACCESS_DENIED_TEXT, }, { appId: "engine", appTitle: "NODE.DC Engine / InJoin", surface: "engine", skillId: "engine-context", whenToUse: [ "workflow graph", "code", "runtime", "agent node", "NDC Agent Core", "engineering automation", ], actionNamespaces: ["engine.*", "ndc-agent-core.*"], mcpServerNames: ["nodedc-engine", "nodedc-agent-core"], requiredScopes: ["engine:workspace:read"], deniedText: ACCESS_DENIED_TEXT, }, { appId: "ops", appTitle: "NODE.DC Ops / Tasker", surface: "ops", skillId: "ops-context", whenToUse: [ "projects", "cards", "comments", "structured blocks", "checkers", "labels", "operational reports", ], actionNamespaces: ["ops.*", "tasker_*"], mcpServerNames: ["nodedc-ops-agent", "nodedc_ops_agent"], requiredScopes: ["ops:project:read"], deniedText: ACCESS_DENIED_TEXT, }, ]; const ASSISTANT_ACTION_CACHE_TTL_MS = Number(process.env.AI_WORKSPACE_ASSISTANT_ACTION_CACHE_TTL_MS || 30_000); const ASSISTANT_ACTION_RELAY_POLL_TIMEOUT_MS = Number(process.env.AI_WORKSPACE_ASSISTANT_RELAY_POLL_TIMEOUT_MS || 25_000); const ASSISTANT_ACTION_RELAY_POLL_IDLE_MS = Number(process.env.AI_WORKSPACE_ASSISTANT_RELAY_POLL_IDLE_MS || 500); const ASSISTANT_ACTION_RELAY_ERROR_BACKOFF_MS = Number(process.env.AI_WORKSPACE_ASSISTANT_RELAY_ERROR_BACKOFF_MS || 5_000); const BRIDGE_RUN_MIRROR_POLL_MS = 1200; const BRIDGE_RUN_MIRROR_MAX_MS = 12 * 60 * 60 * 1000; let assistantActionIdsCache = { loadedAt: 0, actionIds: [] }; let assistantActionRelayStopping = false; const config = readConfig(); const pool = new Pool({ connectionString: config.databaseUrl, max: config.databasePoolSize }); const app = express(); const httpServer = createServer(app); const activeBridgeRunMirrors = new Map(); app.disable("x-powered-by"); app.use(express.json({ limit: "2mb" })); app.get("/healthz", asyncRoute(async (_req, res) => { await pool.query("select 1"); res.json({ ok: true, service: "nodedc-ai-workspace-assistant", database: "ready", internalApiConfigured: config.internalAccessTokens.length > 0, }); })); app.get("/api/ai-workspace/assistant/v1/settings", requireInternalApi, asyncRoute(async (req, res) => { const owner = getRequestOwner(req); const settings = await getOwnerSettings(owner); res.json({ ok: true, owner: publicOwner(owner), settings: publicOwnerSettings(settings) }); })); app.patch("/api/ai-workspace/assistant/v1/settings", requireInternalApi, asyncRoute(async (req, res) => { const owner = getRequestOwner(req); const command = sanitizeOwnerSettingsCommand(req.body); const settings = await updateOwnerSettings(owner, command); res.json({ ok: true, owner: publicOwner(owner), settings: publicOwnerSettings(settings) }); })); app.post("/api/ai-workspace/assistant/v1/run-profile", requireInternalApi, asyncRoute(async (req, res) => { const owner = getRequestOwner(req); const command = sanitizeRunProfileCommand(req.body); const ownerSettings = await getOwnerSettings(owner); const executorId = command.selectedExecutorId || ownerSettings.selectedExecutorId; if (!executorId) { res.status(400).json({ ok: false, error: "ai_workspace_executor_required" }); return; } const executor = await getExecutor(owner, executorId); if (!executor) { res.status(404).json({ ok: false, error: "ai_workspace_executor_not_found" }); return; } const thread = { id: command.threadId || randomUUID(), title: command.threadTitle || "AI Workspace Bridge", originSurface: command.originSurface || optionalString(command.context.surface) || "engine", activeContext: {}, enabledToolPacks: command.enabledToolPacks, }; const bridgePayload = { context: command.context, enabledToolPacks: command.enabledToolPacks, workspacePath: command.workspacePath || executor.workspacePath || "", client: command.client, }; const runProfile = await buildRunProfile({ owner, thread, executor, ownerSettings, bridgePayload }); res.json({ ok: true, owner: publicOwner(owner), runProfile }); })); app.post("/api/ai-workspace/assistant/v1/executors/:executorId/dispatch", requireInternalApi, asyncRoute(async (req, res) => { const owner = getRequestOwner(req); const executorId = sanitizeUuid(req.params.executorId, "executorId"); const executor = await getExecutor(owner, executorId); if (!executor) { res.status(404).json({ ok: false, error: "ai_workspace_executor_not_found" }); return; } const command = sanitizeBridgeDispatchCommand({ ...(isPlainObject(req.body) ? req.body : {}), selectedExecutorId: executorId }); if (!command.message) { res.status(400).json({ ok: false, error: "ai_workspace_dispatch_requires_user_message" }); return; } const ownerSettings = await getOwnerSettings(owner); const thread = { id: command.threadId || randomUUID(), title: command.threadTitle || "AI Workspace Bridge", originSurface: command.originSurface || optionalString(command.context.surface) || "engine", activeContext: {}, enabledToolPacks: command.enabledToolPacks, }; const bridgePayload = { threadId: thread.id, threadTitle: thread.title, workspacePath: command.workspacePath || executor.workspacePath || "", message: command.message, displayMessage: command.displayMessage || command.message, publicUserMessage: command.publicUserMessage || command.message, resume: command.resume, history: command.history, context: command.context, enabledToolPacks: command.enabledToolPacks, client: command.client, }; bridgePayload.runProfile = await buildRunProfile({ owner, thread, executor, ownerSettings, bridgePayload }); const bridge = await dispatchExecutorMessage(executor, bridgePayload); res.json({ ok: true, owner: publicOwner(owner), executorId: executor.id, bridge, runProfile: redactRunProfile(bridgePayload.runProfile), }); })); app.post("/api/ai-workspace/assistant/v1/executors/:executorId/stop", requireInternalApi, asyncRoute(async (req, res) => { const owner = getRequestOwner(req); const executorId = sanitizeUuid(req.params.executorId, "executorId"); const executor = await getExecutor(owner, executorId); if (!executor) { res.status(404).json({ ok: false, error: "ai_workspace_executor_not_found" }); return; } const command = sanitizeBridgeStopCommand(req.body); if (!command.requestId && !command.threadId) { res.status(400).json({ ok: false, error: "ai_workspace_stop_target_empty" }); return; } const bridge = await dispatchExecutorStop(executor, command); res.json({ ok: true, owner: publicOwner(owner), executorId: executor.id, bridge, }); })); app.post("/api/ai-workspace/assistant/v1/actions", requireInternalApi, asyncRoute(async (req, res) => { const owner = getRequestOwner(req); res.json(await executeAssistantActionRequest(req.body, owner)); })); app.get("/api/ai-workspace/assistant/v1/executors", requireInternalApi, asyncRoute(async (req, res) => { const owner = getRequestOwner(req); const [executors, settings] = await Promise.all([ listExecutors(owner), getOwnerSettings(owner), ]); res.json({ ok: true, owner: publicOwner(owner), selectedExecutorId: settings.selectedExecutorId, settings: publicOwnerSettings(settings), executors }); })); app.post("/api/ai-workspace/assistant/v1/executors", requireInternalApi, asyncRoute(async (req, res) => { const owner = getRequestOwner(req); const command = sanitizeExecutorCommand(req.body, { partial: false }); const executor = await createExecutor(owner, command); if (req.body?.select === true) { await selectExecutor(owner, executor.id); } const settings = await getOwnerSettings(owner); res.status(201).json({ ok: true, owner: publicOwner(owner), selectedExecutorId: settings.selectedExecutorId, executor }); })); app.patch("/api/ai-workspace/assistant/v1/executors/:executorId", requireInternalApi, asyncRoute(async (req, res) => { const owner = getRequestOwner(req); const executorId = sanitizeUuid(req.params.executorId, "executorId"); const command = sanitizeExecutorCommand(req.body, { partial: true }); const executor = await updateExecutor(owner, executorId, command); if (!executor) { res.status(404).json({ ok: false, error: "ai_workspace_executor_not_found" }); return; } res.json({ ok: true, owner: publicOwner(owner), executor }); })); app.delete("/api/ai-workspace/assistant/v1/executors/:executorId", requireInternalApi, asyncRoute(async (req, res) => { const owner = getRequestOwner(req); const executorId = sanitizeUuid(req.params.executorId, "executorId"); const deleted = await deleteExecutor(owner, executorId); if (!deleted) { res.status(404).json({ ok: false, error: "ai_workspace_executor_not_found" }); return; } res.json({ ok: true, owner: publicOwner(owner), deleted: true }); })); app.post("/api/ai-workspace/assistant/v1/executors/:executorId/select", requireInternalApi, asyncRoute(async (req, res) => { const owner = getRequestOwner(req); const executorId = sanitizeUuid(req.params.executorId, "executorId"); const executor = await selectExecutor(owner, executorId); if (!executor) { res.status(404).json({ ok: false, error: "ai_workspace_executor_not_found" }); return; } res.json({ ok: true, owner: publicOwner(owner), selectedExecutorId: executor.id, executor }); })); app.post("/api/ai-workspace/assistant/v1/executors/:executorId/check", requireInternalApi, asyncRoute(async (req, res) => { const owner = getRequestOwner(req); const executorId = sanitizeUuid(req.params.executorId, "executorId"); const executor = await getExecutor(owner, executorId); if (!executor) { res.status(404).json({ ok: false, error: "ai_workspace_executor_not_found" }); return; } const check = await checkExecutor(executor); const updated = await updateExecutor(owner, executor.id, { status: check.status, statusDetail: check.statusDetail, lastSeenAt: check.lastSeenAt, metadata: { ...(executor.metadata || {}), lastCheck: check.details, }, }); res.json({ ok: true, owner: publicOwner(owner), executor: updated || executor, check: check.details }); })); app.get("/api/ai-workspace/assistant/v1/executors/:executorId/events", requireInternalApi, asyncRoute(async (req, res) => { const owner = getRequestOwner(req); const executorId = sanitizeUuid(req.params.executorId, "executorId"); const executor = await getExecutor(owner, executorId); if (!executor) { res.status(404).json({ ok: false, error: "ai_workspace_executor_not_found" }); return; } const latestOnly = String(req.query.since || "") === "latest"; const payload = await readExecutorEvents(executor, { since: latestOnly ? 0 : Number(req.query.since || 0), limit: sanitizeLimit(req.query.limit, 100, 500), }); res.json({ ok: true, owner: publicOwner(owner), executorId: executor.id, online: payload.online, latestEventId: payload.latestEventId, events: latestOnly ? [] : payload.events, }); })); app.get("/api/ai-workspace/assistant/v1/executors/:executorId/agent/windows.ps1", requireInternalApi, asyncRoute(async (req, res) => { const owner = getRequestOwner(req); const executorId = sanitizeUuid(req.params.executorId, "executorId"); const executor = await getExecutor(owner, executorId); if (!executor) { res.status(404).send("executor_not_found"); return; } const installerPort = installerPortFromQuery(req.query.port); if (!installerPort) { res.status(400).send("installer_port_invalid"); return; } const ownerSettings = await getOwnerSettings(owner); const appMcpServers = installerMcpServersFromSettings(ownerSettings, req.query); const opsMcpServer = appMcpServers.find((server) => server.appId === "ops") || null; const opsMcpUrl = optionalString(opsMcpServer?.url) || ""; const opsMcpToken = bearerTokenFromHeaders(opsMcpServer?.httpHeaders) || ""; const opsMcpServerName = optionalString(opsMcpServer?.serverName) || "nodedc_tasker"; const appMcpServersJson = appMcpServers.length ? JSON.stringify(appMcpServers) : ""; const templateUrl = new URL("./templates/install-windows.ps1", import.meta.url); let source = await readFile(templateUrl, "utf8"); source = source .replace('[string]$HubUrl = "",', `[string]$HubUrl = ${psSingle(config.hubWebSocketUrl)},`) .replace('[string]$HubFallbackUrls = "",', `[string]$HubFallbackUrls = ${psSingle(config.hubFallbackWebSocketUrls.join(","))},`) .replace('[string]$PairingCode = "",', `[string]$PairingCode = ${psSingle(executor.pairingCode || "")},`) .replace('[string]$MachineName = "",', `[string]$MachineName = ${psSingle(executor.name)},`) .replace('[string]$OpsMcpUrl = "",', `[string]$OpsMcpUrl = ${psSingle(opsMcpUrl)},`) .replace('[string]$OpsMcpToken = "",', `[string]$OpsMcpToken = ${psSingle(opsMcpToken)},`) .replace('[string]$OpsMcpServerName = "nodedc_tasker",', `[string]$OpsMcpServerName = ${psSingle(opsMcpServerName)},`) .replace('[string]$AppMcpServersJson = "",', `[string]$AppMcpServersJson = ${psSingle(appMcpServersJson)},`) .replace('[int]$Port = 8787,', `[int]$Port = ${installerPort},`) .replace('[string]$Workspace = "",', `[string]$Workspace = ${psSingle(executor.workspacePath || "")},`); const installerHash = createHash("sha256").update(source).digest("hex"); const safeName = optionalString(executor.name)?.replace(/[^A-Za-z0-9._-]+/g, "_") || "NDC"; res.setHeader("Content-Type", "application/octet-stream"); res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate"); res.setHeader("Pragma", "no-cache"); res.setHeader("Expires", "0"); res.setHeader("Surrogate-Control", "no-store"); res.setHeader("X-AI-Workspace-Installer-SHA256", installerHash); res.setHeader("Content-Disposition", `attachment; filename="${safeName}_BRIDGE-agent-install.ps1"`); res.send(source); })); app.post("/api/ai-workspace/assistant/v1/executors/:executorId/agent/setup-command", requireInternalApi, asyncRoute(async (req, res) => { const owner = getRequestOwner(req); const executorId = sanitizeUuid(req.params.executorId, "executorId"); const executor = await getExecutor(owner, executorId); if (!executor) { res.status(404).json({ ok: false, error: "ai_workspace_executor_not_found" }); return; } if (!cleanPairingCode(executor.pairingCode || "")) { res.status(400).json({ ok: false, error: "pairing_code_required" }); return; } const installerPort = installerPortFromQuery(req.body?.port || req.query?.port); if (!installerPort) { res.status(400).json({ ok: false, error: "installer_port_invalid" }); return; } const ownerSettings = await getOwnerSettings(owner); const appMcpServers = installerMcpServersFromSettings(ownerSettings, isPlainObject(req.body) ? req.body : {}); const setupCode = await createAiWorkspaceSetupCode(owner, executor, { port: installerPort, appMcpServers, }); const command = buildBridgeSetupCommand(setupCode.code); res.status(201).json({ ok: true, owner: publicOwner(owner), executorId: executor.id, install: { command, packageName: config.bridgePackageName, packageSpec: config.bridgePackageSpec, gatewayUrl: config.setupGatewayUrl, expiresAt: setupCode.expiresAt, }, setupCode: { suffix: setupCode.suffix, expiresAt: setupCode.expiresAt, }, }); })); app.get("/api/ai-workspace/assistant/v1/bridge-package.tgz", requireInternalApi, asyncRoute(async (_req, res) => { const bridgePackage = await getBridgePackageTarball(); res.setHeader("Content-Type", "application/octet-stream"); res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate, proxy-revalidate"); res.setHeader("Pragma", "no-cache"); res.setHeader("Expires", "0"); res.setHeader("Surrogate-Control", "no-store"); res.setHeader("X-AI-Workspace-Bridge-Package-SHA256", bridgePackage.sha256); res.setHeader("Content-Disposition", `attachment; filename="${bridgePackage.filename}"`); res.send(bridgePackage.buffer); })); app.post("/api/ai-workspace/assistant/v1/setup-codes/redeem", requireInternalApi, asyncRoute(async (req, res) => { const setupCode = optionalString(req.body?.setupCode || req.body?.setup_code || req.query?.setupCode || req.query?.setup_code); if (!setupCode) { res.status(400).json({ ok: false, error: "setup_code_required" }); return; } const setup = await redeemAiWorkspaceSetupCode(setupCode); res.json({ ok: true, setup }); })); app.get("/api/ai-workspace/assistant/v1/threads", requireInternalApi, asyncRoute(async (req, res) => { const owner = getRequestOwner(req); const surface = req.query.surface ? sanitizeSurface(req.query.surface) : null; const kind = sanitizeThreadKind(req.query.kind || req.query.threadKind || req.query.thread_kind || "shared"); const includeArchived = isTruthy(req.query.includeArchived || req.query.include_archived); const limit = sanitizeLimit(req.query.limit, 100, 200); const threads = await listThreads(owner, { surface, kind, limit, includeArchived }); res.json({ ok: true, owner: publicOwner(owner), threads }); })); app.post("/api/ai-workspace/assistant/v1/threads", requireInternalApi, asyncRoute(async (req, res) => { const owner = getRequestOwner(req); const command = sanitizeThreadCommand(req.body, { partial: false }); const thread = await createThread(owner, command); res.status(201).json({ ok: true, owner: publicOwner(owner), thread }); })); app.get("/api/ai-workspace/assistant/v1/threads/:threadId", requireInternalApi, asyncRoute(async (req, res) => { const owner = getRequestOwner(req); const threadId = sanitizeUuid(req.params.threadId, "threadId"); const thread = await getThread(owner, threadId); if (!thread) { res.status(404).json({ ok: false, error: "ai_workspace_thread_not_found" }); return; } res.json({ ok: true, owner: publicOwner(owner), thread }); })); app.patch("/api/ai-workspace/assistant/v1/threads/:threadId", requireInternalApi, asyncRoute(async (req, res) => { const owner = getRequestOwner(req); const threadId = sanitizeUuid(req.params.threadId, "threadId"); const command = sanitizeThreadCommand(req.body, { partial: true }); const thread = await updateThread(owner, threadId, command); if (!thread) { res.status(404).json({ ok: false, error: "ai_workspace_thread_not_found" }); return; } res.json({ ok: true, owner: publicOwner(owner), thread }); })); app.get("/api/ai-workspace/assistant/v1/threads/:threadId/messages", requireInternalApi, asyncRoute(async (req, res) => { const owner = getRequestOwner(req); const threadId = sanitizeUuid(req.params.threadId, "threadId"); const limit = sanitizeLimit(req.query.limit, 200, 1000); const cursor = optionalString(req.query.before); const thread = await getThread(owner, threadId); if (!thread) { res.status(404).json({ ok: false, error: "ai_workspace_thread_not_found" }); return; } const messages = await listThreadMessages(owner, threadId, { limit, before: cursor }); res.json({ ok: true, owner: publicOwner(owner), threadId, messages }); })); app.post("/api/ai-workspace/assistant/v1/threads/:threadId/messages", requireInternalApi, asyncRoute(async (req, res) => { const owner = getRequestOwner(req); const threadId = sanitizeUuid(req.params.threadId, "threadId"); const command = sanitizeMessageCommand(req.body); const message = await createThreadMessage(owner, threadId, command); if (!message) { res.status(404).json({ ok: false, error: "ai_workspace_thread_not_found" }); return; } res.status(201).json({ ok: true, owner: publicOwner(owner), message }); })); app.post("/api/ai-workspace/assistant/v1/threads/:threadId/dispatch", requireInternalApi, asyncRoute(async (req, res) => { const owner = getRequestOwner(req); const threadId = sanitizeUuid(req.params.threadId, "threadId"); const command = sanitizeDispatchCommand(req.body); const thread = await getThread(owner, threadId); if (!thread) { res.status(404).json({ ok: false, error: "ai_workspace_thread_not_found" }); return; } const message = await getThreadMessage(owner, threadId, command.messageId); if (!message) { res.status(404).json({ ok: false, error: "ai_workspace_thread_message_not_found" }); return; } if (message.role !== "user") { res.status(400).json({ ok: false, error: "ai_workspace_dispatch_requires_user_message" }); return; } const ownerSettings = await getOwnerSettings(owner); const executorId = command.selectedExecutorId || thread.selectedExecutorId || ownerSettings.selectedExecutorId; if (!executorId) { res.status(400).json({ ok: false, error: "ai_workspace_executor_required" }); return; } const executor = await getExecutor(owner, executorId); if (!executor) { res.status(404).json({ ok: false, error: "ai_workspace_executor_not_found" }); return; } const history = await listThreadMessages(owner, threadId, { limit: 40, before: null }); const bridgePayload = buildBridgeMessagePayload({ thread, message, history, executor, command, ownerSettings }); const missingContext = missingNdcAgentCoreContext(bridgePayload.context); const missingOpsContext = missingOpsContextFields(bridgePayload.context); if (missingContext.length > 0) { const guardInstruction = ndcMissingContextInstruction(bridgePayload.context, missingContext); bridgePayload.context = { ...bridgePayload.context, contextReady: false, missingContext, accessMode: "chat-readonly", guardInstruction, }; } else if (normalizeKey(bridgePayload.context?.modeId) === "ndc-agent-core") { bridgePayload.context = { ...bridgePayload.context, contextReady: true, missingContext: [], accessMode: "agent-write", }; } else if (missingOpsContext.length > 0) { bridgePayload.context = { ...bridgePayload.context, contextReady: false, missingContext: missingOpsContext, accessMode: "chat-readonly", guardInstruction: opsMissingContextInstruction(bridgePayload.context, missingOpsContext), }; } else if (normalizeKey(bridgePayload.context?.modeId) === "ops") { bridgePayload.context = { ...bridgePayload.context, contextReady: true, missingContext: [], accessMode: "ops-write", }; } const runProfile = await buildRunProfile({ owner, thread, executor, ownerSettings, bridgePayload }); bridgePayload.runProfile = runProfile; let bridge = null; try { bridge = await dispatchExecutorMessage(executor, bridgePayload); } catch (error) { const failureText = bridgeFailureText({ message: errorMessage(error) }); const assistantMessage = await createThreadMessage(owner, threadId, { role: "assistant", content: failureText, payload: { surface: optionalString(bridgePayload.context?.surface) || thread.originSurface || "global", kind: "bridge_failure", executorId: executor.id, modeId: bridgePayload.context?.modeId, }, }); await updateExecutor(owner, executor.id, { status: "offline", statusDetail: sanitizeBridgeErrorText(errorMessage(error)), lastSeenAt: executor.lastSeenAt, metadata: { ...(executor.metadata || {}), lastDispatchError: { at: new Date().toISOString(), message: sanitizeBridgeErrorText(errorMessage(error)), }, }, }).catch(() => null); res.json({ ok: true, owner: publicOwner(owner), threadId, executor, message, assistantMessage, bridge: { ok: false, accepted: false, mode: executor.connectionMode === "direct" ? "direct" : "hub", error: sanitizeBridgeErrorText(errorMessage(error)), }, }); return; } const run = await createBridgeRun({ owner, thread, executor, bridge, payload: bridgePayload }); startBridgeRunMirror({ owner, thread, executor, bridge, run }); res.json({ ok: true, owner: publicOwner(owner), threadId, executor, message, assistantMessage: null, runProfile: redactRunProfile(runProfile), bridge, }); })); app.use((error, _req, res, _next) => { const status = Number(error?.status || 500); const message = error instanceof Error ? error.message : "internal_error"; res.status(status >= 400 && status < 600 ? status : 500).json({ ok: false, error: message }); }); await migrate(); await recoverBridgeRunMirrors(); httpServer.listen(config.port, "0.0.0.0", () => { console.log(`NODE.DC AI Workspace Assistant listening on http://0.0.0.0:${config.port}`); startAssistantActionRelayLoop(); }); process.on("SIGTERM", shutdown); process.on("SIGINT", shutdown); async function listExecutors(owner) { const result = await pool.query( `select * from ai_workspace_executors where owner_key = $1 order by updated_at desc, created_at desc`, [owner.key] ); return Promise.all(result.rows.map((row) => decorateExecutorLiveStatus(toExecutor(row)))); } async function decorateExecutorLiveStatus(executor) { if (!(executor?.connectionMode === "hub" || executor?.pairingCode)) return executor; const pairingCode = cleanPairingCode(executor.pairingCode); if (!pairingCode) { return { ...executor, status: "offline", statusDetail: "pairing_code_required" }; } if (!config.hubInternalHttpUrl || !config.hubInternalAccessToken) return executor; try { const payload = await hubRequestJson( `/api/ai-workspace/hub/v1/agents/${encodeURIComponent(pairingCode)}`, { method: "GET" }, 1800 ); const agent = isPlainObject(payload.agent) ? payload.agent : null; if (agent?.online && agent.runtimeStatus !== "error") { return { ...executor, status: "online", statusDetail: "", lastSeenAt: optionalString(agent.lastSeenAt) || executor.lastSeenAt, }; } return { ...executor, status: "offline", statusDetail: agent?.runtimeStatus === "error" ? optionalString(agent.runtimeError) || "bridge_runtime_error" : "bridge_agent_offline", lastSeenAt: optionalString(agent?.lastSeenAt) || executor.lastSeenAt, }; } catch (error) { return { ...executor, status: "offline", statusDetail: errorMessage(error) || "bridge_agent_offline", }; } } async function createExecutor(owner, command) { if (command.connectionMode === "hub" && !command.pairingCode) { command.pairingCode = makePairingCode(); } const result = await pool.query( `insert into ai_workspace_executors ( id, owner_key, owner_user_id, owner_email, name, type, connection_mode, endpoint, workspace_path, agent_port, pairing_code, model, account_label, capabilities, status, status_detail, last_seen_at, metadata ) values ( $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14::jsonb, $15, $16, $17, $18::jsonb ) returning *`, [ command.id || randomUUID(), owner.key, owner.userId, owner.email, command.name, command.type, command.connectionMode, command.endpoint, command.workspacePath, command.agentPort, command.pairingCode, command.model, command.accountLabel, JSON.stringify(command.capabilities), command.status, command.statusDetail, command.lastSeenAt, JSON.stringify(command.metadata), ] ); return toExecutor(result.rows[0]); } async function updateExecutor(owner, executorId, command) { const fields = []; const values = [owner.key, executorId]; const mappings = { name: "name", type: "type", connectionMode: "connection_mode", endpoint: "endpoint", workspacePath: "workspace_path", agentPort: "agent_port", pairingCode: "pairing_code", model: "model", accountLabel: "account_label", capabilities: "capabilities", status: "status", statusDetail: "status_detail", lastSeenAt: "last_seen_at", metadata: "metadata", }; for (const [key, column] of Object.entries(mappings)) { if (!Object.hasOwn(command, key)) continue; const value = key === "capabilities" || key === "metadata" ? JSON.stringify(command[key]) : command[key]; values.push(value); fields.push(`${column} = $${values.length}${key === "capabilities" || key === "metadata" ? "::jsonb" : ""}`); } if (fields.length === 0) { return getExecutor(owner, executorId); } values.push(new Date()); const result = await pool.query( `update ai_workspace_executors set ${fields.join(", ")}, updated_at = $${values.length} where owner_key = $1 and id = $2 returning *`, values ); return result.rows[0] ? toExecutor(result.rows[0]) : null; } async function deleteExecutor(owner, executorId) { const client = await pool.connect(); try { await client.query("begin"); await client.query( `update ai_workspace_owner_settings set selected_executor_id = null, updated_at = now() where owner_key = $1 and selected_executor_id = $2`, [owner.key, executorId] ); const result = await client.query( "delete from ai_workspace_executors where owner_key = $1 and id = $2", [owner.key, executorId] ); await client.query("commit"); return result.rowCount > 0; } catch (error) { await client.query("rollback"); throw error; } finally { client.release(); } } async function getExecutor(owner, executorId) { const result = await pool.query( "select * from ai_workspace_executors where owner_key = $1 and id = $2", [owner.key, executorId] ); return result.rows[0] ? toExecutor(result.rows[0]) : null; } async function createAiWorkspaceSetupCode(owner, executor, options = {}) { const code = makeAiWorkspaceSetupCode(); const expiresAt = new Date(Date.now() + config.setupCodeTtlSeconds * 1000); const metadata = { packageName: config.bridgePackageName, setupGatewayUrl: config.setupGatewayUrl, bridge: bridgeSetupPayload(executor, options), }; const client = await pool.connect(); try { await client.query("begin"); await client.query( `update ai_workspace_setup_codes set status = 'revoked' where owner_key = $1 and executor_id = $2 and status = 'active'`, [owner.key, executor.id] ); await client.query( `insert into ai_workspace_setup_codes ( id, owner_key, owner_user_id, owner_email, executor_id, code_hash, code_suffix, expires_at, metadata ) values ($1, $2, $3, $4, $5, $6, $7, $8, $9::jsonb)`, [ randomUUID(), owner.key, owner.userId, owner.email, executor.id, hashSetupCode(code), setupCodeSuffix(code), expiresAt, JSON.stringify(metadata), ] ); await client.query("commit"); } catch (error) { await client.query("rollback"); throw error; } finally { client.release(); } return { code, suffix: setupCodeSuffix(code), expiresAt: expiresAt.toISOString(), }; } async function redeemAiWorkspaceSetupCode(code) { const client = await pool.connect(); let committed = false; try { await client.query("begin"); const result = await client.query( `select c.*, e.name as executor_name from ai_workspace_setup_codes c left join ai_workspace_executors e on e.owner_key = c.owner_key and e.id = c.executor_id where c.code_hash = $1 for update of c`, [hashSetupCode(code)] ); const row = result.rows[0]; if (!row) throw badRequest("setup_code_invalid"); if (row.status !== "active") throw httpError("setup_code_not_active", 410); if (new Date(row.expires_at).getTime() <= Date.now()) { await client.query( "update ai_workspace_setup_codes set status = 'expired' where id = $1", [row.id] ); await client.query("commit"); committed = true; throw httpError("setup_code_expired", 410); } await client.query( "update ai_workspace_setup_codes set status = 'used', used_at = now() where id = $1", [row.id] ); await client.query("commit"); committed = true; const metadata = isPlainObject(row.metadata) ? row.metadata : {}; const bridge = isPlainObject(metadata.bridge) ? metadata.bridge : null; if (!bridge?.pairingCode || !Array.isArray(bridge?.hubUrls) || !bridge.hubUrls.length) { throw httpError("setup_payload_invalid", 500); } return { service: "NDC AI Workspace Bridge", packageName: optionalString(metadata.packageName) || config.bridgePackageName, executor: { id: row.executor_id, name: optionalString(row.executor_name) || optionalString(bridge.machineName) || "Codex worker", }, expiresAt: toIso(row.expires_at), bridge, }; } catch (error) { if (!committed) { try { await client.query("rollback"); } catch {} } throw error; } finally { client.release(); } } function bridgeSetupPayload(executor, options = {}) { const hubUrls = uniqueStrings([config.hubWebSocketUrl, ...(config.hubFallbackWebSocketUrls || [])]); return { protocol: "ai-workspace-bridge/v1", hubUrl: hubUrls[0] || "", hubUrls, pairingCode: cleanPairingCode(executor.pairingCode || ""), machineName: optionalString(executor.name) || "Codex worker", workspace: optionalString(executor.workspacePath) || "", port: sanitizeInteger(options.port, 8787, 1, 65535), appMcpServers: Array.isArray(options.appMcpServers) ? options.appMcpServers : [], }; } function buildBridgeSetupCommand(code) { const parts = [ "npx", "--yes", "--package", config.bridgePackageSpec, AI_WORKSPACE_BRIDGE_PACKAGE_BIN, "setup", code, "--gateway", config.setupGatewayUrl, ]; return parts.join(" "); } async function getBridgePackageTarball() { if (cachedBridgePackageTarball) return cachedBridgePackageTarball; const packageRoot = fileURLToPath(new URL("./assets/ai-workspace-npm/", import.meta.url)); const packageJson = JSON.parse(await readFile(pathModule.join(packageRoot, "package.json"), "utf8")); const files = await collectBridgePackageFiles(packageRoot); const tarBuffer = await buildBridgePackageTarBuffer(packageRoot, files); const buffer = gzipSync(tarBuffer, { level: 9 }); cachedBridgePackageTarball = { buffer, sha256: createHash("sha256").update(buffer).digest("hex"), filename: bridgePackageTarballFilename(packageJson.name, packageJson.version), }; return cachedBridgePackageTarball; } async function collectBridgePackageFiles(root, relDir = "") { const currentDir = pathModule.join(root, relDir); const entries = await readdir(currentDir, { withFileTypes: true }); const files = []; for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) { if (shouldSkipBridgePackageEntry(entry.name)) continue; const relPath = relDir ? `${relDir}/${entry.name}` : entry.name; if (entry.isDirectory()) { files.push(...await collectBridgePackageFiles(root, relPath)); continue; } if (entry.isFile()) files.push(relPath); } return files; } function shouldSkipBridgePackageEntry(name) { return ( name === ".DS_Store" || name === "__MACOSX" || name === "node_modules" || name.endsWith(".tgz") ); } async function buildBridgePackageTarBuffer(root, files) { const chunks = []; for (const relPath of files) { const fullPath = pathModule.join(root, ...relPath.split("/")); const fileStat = await stat(fullPath); if (!fileStat.isFile()) continue; const body = await readFile(fullPath); const mode = relPath.startsWith("bin/") ? 0o755 : 0o644; chunks.push(buildTarHeader({ name: `package/${relPath}`, mode, size: body.length, type: "0", })); chunks.push(body); const padding = (TAR_BLOCK_SIZE - (body.length % TAR_BLOCK_SIZE)) % TAR_BLOCK_SIZE; if (padding) chunks.push(Buffer.alloc(padding)); } chunks.push(Buffer.alloc(TAR_BLOCK_SIZE * 2)); return Buffer.concat(chunks); } function buildTarHeader({ name, mode, size, type }) { const header = Buffer.alloc(TAR_BLOCK_SIZE); const splitName = splitTarName(name); writeTarString(header, 0, 100, splitName.name); writeTarOctal(header, 100, 8, mode); writeTarOctal(header, 108, 8, 0); writeTarOctal(header, 116, 8, 0); writeTarOctal(header, 124, 12, size); writeTarOctal(header, 136, 12, 0); header.fill(0x20, 148, 156); writeTarString(header, 156, 1, type || "0"); writeTarString(header, 257, 6, "ustar"); writeTarString(header, 263, 2, "00"); writeTarString(header, 265, 32, "nodedc"); writeTarString(header, 297, 32, "nodedc"); if (splitName.prefix) writeTarString(header, 345, 155, splitName.prefix); writeTarChecksum(header, header.reduce((sum, value) => sum + value, 0)); return header; } function splitTarName(name) { if (Buffer.byteLength(name) <= 100) return { name, prefix: "" }; let slashIndex = name.lastIndexOf("/"); while (slashIndex > 0) { const prefix = name.slice(0, slashIndex); const entryName = name.slice(slashIndex + 1); if (Buffer.byteLength(prefix) <= 155 && Buffer.byteLength(entryName) <= 100) { return { name: entryName, prefix }; } slashIndex = name.lastIndexOf("/", slashIndex - 1); } throw new Error(`bridge_package_path_too_long:${name}`); } function writeTarString(buffer, offset, length, value) { buffer.write(String(value || ""), offset, length, "ascii"); } function writeTarOctal(buffer, offset, length, value) { const text = Math.trunc(Number(value) || 0).toString(8).padStart(length - 1, "0").slice(-(length - 1)); buffer.write(text, offset, length - 1, "ascii"); buffer[offset + length - 1] = 0; } function writeTarChecksum(buffer, checksum) { const text = Math.trunc(Number(checksum) || 0).toString(8).padStart(6, "0").slice(-6); buffer.write(text, 148, 6, "ascii"); buffer[154] = 0; buffer[155] = 0x20; } function bridgePackageTarballFilename(packageName, version) { const rawName = String(packageName || AI_WORKSPACE_BRIDGE_PACKAGE_NAME).replace(/^@[^/]+\//, ""); const safeName = rawName.replace(/[^A-Za-z0-9._-]+/g, "-") || "ai-workspace-bridge"; const safeVersion = String(version || "0.0.0").replace(/[^A-Za-z0-9._-]+/g, "-"); return `${safeName}-${safeVersion}.tgz`; } function makeAiWorkspaceSetupCode() { return `${AI_WORKSPACE_SETUP_CODE_PREFIX}_${randomBytes(18).toString("base64url")}`; } function hashSetupCode(code) { return createHash("sha256").update(String(code || ""), "utf8").digest("hex"); } function setupCodeSuffix(code) { return String(code || "").replace(/[^A-Za-z0-9]/g, "").slice(-6).toUpperCase(); } async function selectExecutor(owner, executorId) { const executor = await getExecutor(owner, executorId); if (!executor) return null; await pool.query( `insert into ai_workspace_owner_settings ( owner_key, owner_user_id, owner_email, selected_executor_id ) values ($1, $2, $3, $4) on conflict (owner_key) do update set owner_user_id = excluded.owner_user_id, owner_email = excluded.owner_email, selected_executor_id = excluded.selected_executor_id, updated_at = now()`, [owner.key, owner.userId, owner.email, executorId] ); return executor; } async function updateOwnerSettings(owner, command) { const current = await getOwnerSettings(owner); const selectedExecutorId = Object.hasOwn(command, "selectedExecutorId") ? command.selectedExecutorId : current.selectedExecutorId; if (selectedExecutorId && !(await getExecutor(owner, selectedExecutorId))) { const error = new Error("ai_workspace_executor_not_found"); error.status = 404; throw error; } const activeContext = Object.hasOwn(command, "activeContext") ? mergeOwnerActiveContext(current.activeContext || {}, command.activeContext) : current.activeContext || {}; const enabledToolPacks = Object.hasOwn(command, "enabledToolPacks") ? mergeToolPacks(current.enabledToolPacks, command.enabledToolPacks) : current.enabledToolPacks || []; const metadata = Object.hasOwn(command, "metadata") ? mergeOwnerMetadata(current.metadata || {}, command.metadata) : current.metadata || {}; const result = await pool.query( `insert into ai_workspace_owner_settings ( owner_key, owner_user_id, owner_email, selected_executor_id, active_context, enabled_tool_packs, metadata ) values ($1, $2, $3, $4, $5::jsonb, $6::text[], $7::jsonb) on conflict (owner_key) do update set owner_user_id = excluded.owner_user_id, owner_email = excluded.owner_email, selected_executor_id = excluded.selected_executor_id, active_context = excluded.active_context, enabled_tool_packs = excluded.enabled_tool_packs, metadata = excluded.metadata, updated_at = now() returning *`, [ owner.key, owner.userId, owner.email, selectedExecutorId, JSON.stringify(activeContext), enabledToolPacks, JSON.stringify(metadata), ] ); return toOwnerSettings(result.rows[0]); } function mergeOwnerActiveContext(current, incoming) { const currentContext = isPlainObject(current) ? current : {}; const incomingContext = isPlainObject(incoming) ? incoming : {}; const currentContexts = isPlainObject(currentContext.contexts) ? currentContext.contexts : {}; const incomingContexts = isPlainObject(incomingContext.contexts) ? incomingContext.contexts : {}; const surface = normalizeKey(incomingContext.surface); const nextContexts = { ...currentContexts }; for (const [key, value] of Object.entries(incomingContexts)) { const normalizedKey = normalizeKey(key); if (!normalizedKey || !isPlainObject(value)) continue; const { contexts: _nestedContexts, ...surfaceContext } = value; nextContexts[normalizedKey] = { ...(isPlainObject(nextContexts[normalizedKey]) ? nextContexts[normalizedKey] : {}), ...surfaceContext, }; } if ((surface === "engine" || surface === "ops") && !isPlainObject(incomingContexts[surface])) { const { contexts: _contexts, ...surfaceContext } = incomingContext; nextContexts[surface] = { ...(isPlainObject(nextContexts[surface]) ? nextContexts[surface] : {}), ...surfaceContext, }; } return { ...currentContext, ...incomingContext, contexts: nextContexts, }; } function mergeOwnerMetadata(current, incoming) { const currentMetadata = isPlainObject(current) ? current : {}; const incomingMetadata = isPlainObject(incoming) ? incoming : {}; const currentAppGrants = isPlainObject(currentMetadata.appGrants) ? currentMetadata.appGrants : {}; const incomingAppGrants = isPlainObject(incomingMetadata.appGrants) ? incomingMetadata.appGrants : {}; const nextAppGrants = { ...currentAppGrants }; for (const [key, value] of Object.entries(incomingAppGrants)) { const appId = normalizeKey(key); if (!appId) continue; if (value === null || value === false) { delete nextAppGrants[appId]; continue; } if (!isPlainObject(value)) continue; nextAppGrants[appId] = { ...(isPlainObject(nextAppGrants[appId]) ? nextAppGrants[appId] : {}), ...value, appId: optionalString(value.appId) || appId, }; } return { ...currentMetadata, ...incomingMetadata, appGrants: nextAppGrants, }; } function installerMcpServersFromSettings(settings, query = {}) { const servers = []; const metadata = isPlainObject(settings?.metadata) ? settings.metadata : {}; collectInstallerMcpServers(servers, metadata.mcpServers, {}); const appGrants = isPlainObject(metadata.appGrants) ? metadata.appGrants : {}; for (const [appId, grant] of Object.entries(appGrants)) { if (!isPlainObject(grant)) continue; collectInstallerMcpServers(servers, grant.mcpServers, { appId: optionalString(grant.appId) || normalizeKey(appId), appTitle: optionalString(grant.appTitle || grant.title), }); } const legacyOpsUrl = optionalString(query.opsMcpUrl || query.ops_mcp_url); const legacyOpsToken = optionalString(query.opsMcpToken || query.ops_mcp_token); if (legacyOpsUrl && legacyOpsToken) { servers.push({ appId: "ops", appTitle: "NODE.DC Ops", serverName: optionalString(query.opsMcpServerName || query.ops_mcp_server_name) || "nodedc_tasker", url: legacyOpsUrl, enabled: true, required: false, startupTimeoutSec: 20, toolTimeoutSec: 60, httpHeaders: { Authorization: `Bearer ${legacyOpsToken}`, Accept: "application/json", "MCP-Protocol-Version": "2025-06-18", }, }); } const byServerName = new Map(); for (const server of servers.map(sanitizeInstallerMcpServer).filter(Boolean)) { byServerName.set(server.serverName, server); } return Array.from(byServerName.values()); } function collectInstallerMcpServers(target, value, defaults = {}) { const items = Array.isArray(value) ? value : isPlainObject(value) ? Object.values(value) : []; for (const item of items) { if (!isPlainObject(item)) continue; target.push({ ...defaults, ...item }); } } function sanitizeInstallerMcpServer(raw) { if (!isPlainObject(raw)) return null; const serverName = safeMcpServerName(raw.serverName || raw.server_name || raw.name); const url = optionalString(raw.url); if (!serverName || !url) return null; const httpHeaders = sanitizeInstallerMcpHeaders(raw.httpHeaders || raw.http_headers || raw.headers); return { appId: normalizeKey(raw.appId || raw.app_id) || "global", appTitle: optionalString(raw.appTitle || raw.app_title || raw.title), serverName, url, enabled: raw.enabled !== false, required: raw.required === true, startupTimeoutSec: sanitizeInteger(raw.startupTimeoutSec || raw.startup_timeout_sec, 20, 1, 600), toolTimeoutSec: sanitizeInteger(raw.toolTimeoutSec || raw.tool_timeout_sec, 60, 1, 3600), httpHeaders, }; } function sanitizeInstallerMcpHeaders(value) { if (!isPlainObject(value)) return {}; const headers = {}; for (const [key, rawValue] of Object.entries(value)) { const headerName = optionalString(key); const headerValue = optionalString(rawValue); if (!headerName || !headerValue || headerName.length > 120 || headerValue.length > 4000) continue; headers[headerName] = headerValue; } return headers; } function bearerTokenFromHeaders(headers) { if (!isPlainObject(headers)) return ""; const authorization = optionalString(headers.Authorization || headers.authorization); const match = authorization?.match(/^Bearer\s+(.+)$/i); return match ? match[1].trim() : ""; } function safeMcpServerName(value) { const text = optionalString(value); if (!text) return ""; return text.replace(/[^A-Za-z0-9_-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 80); } function sanitizeInteger(value, fallback, min, max) { const number = Number(value || fallback); if (!Number.isFinite(number)) return fallback; return Math.min(Math.max(Math.trunc(number), min), max); } async function checkExecutor(executor) { const checkedAt = new Date().toISOString(); if (executor.connectionMode === "direct") { return checkDirectExecutor(executor, checkedAt); } return checkHubExecutor(executor, checkedAt); } async function checkHubExecutor(executor, checkedAt) { const pairingCode = cleanPairingCode(executor.pairingCode); if (!pairingCode) { return executorCheckResult("error", "pairing_code_required", checkedAt, { checkedAt, mode: "hub" }); } if (!config.hubInternalHttpUrl) { return executorCheckResult("unknown", "ai_workspace_hub_url_not_configured", checkedAt, { checkedAt, mode: "hub", pairingCode }); } if (!config.hubInternalAccessToken) { return executorCheckResult("unknown", "ai_workspace_hub_token_not_configured", checkedAt, { checkedAt, mode: "hub", pairingCode }); } try { const agentPayload = await hubRequestJson( `/api/ai-workspace/hub/v1/agents/${encodeURIComponent(pairingCode)}`, { method: "GET" }, 5000 ); const agent = isPlainObject(agentPayload.agent) ? agentPayload.agent : null; if (!agent?.online) { return executorCheckResult("offline", "bridge_agent_offline", optionalString(agent?.lastSeenAt) || optionalString(executor.lastSeenAt) || checkedAt, { checkedAt, mode: "hub", pairingCode, agent, }); } try { const healthPayload = await hubRequestJson( `/api/ai-workspace/hub/v1/agents/${encodeURIComponent(pairingCode)}/request`, { method: "POST", body: { command: "health", payload: {}, timeoutMs: 7000, quiet: true, }, }, 9000 ); const health = isPlainObject(healthPayload.payload) ? healthPayload.payload : {}; const compatibility = bridgeCompatibilityStatus(health, agent); if (!compatibility.ok) { return executorCheckResult( "error", "agent_update_required", optionalString(health.runtimeCheckedAt) || agent.lastSeenAt || checkedAt, { checkedAt, mode: "hub", pairingCode, agent, health: summarizeBridgeHealth(health), bridgeCompatibility: compatibility, } ); } const runtimeOk = health.ok !== false && health.runtimeStatus !== "error"; return executorCheckResult( runtimeOk ? "online" : "error", runtimeOk ? "bridge_health_ok" : optionalString(health.runtimeError || health.error) || "bridge_health_failed", optionalString(health.runtimeCheckedAt) || agent.lastSeenAt || checkedAt, { checkedAt, mode: "hub", pairingCode, agent, health: summarizeBridgeHealth(health), } ); } catch (error) { const status = Number(error?.status || 0); const statusDetail = errorMessage(error); return executorCheckResult( status === 503 ? "offline" : executorStatusForHubError(statusDetail), statusDetail, agent.lastSeenAt || checkedAt, { checkedAt, mode: "hub", pairingCode, agent, error: statusDetail, status, } ); } } catch (error) { const statusDetail = errorMessage(error); return executorCheckResult(executorStatusForHubError(statusDetail), statusDetail, checkedAt, { checkedAt, mode: "hub", pairingCode, error: statusDetail, status: Number(error?.status || 0) || null, }); } } async function checkDirectExecutor(executor, checkedAt) { const url = bridgeHealthUrl(executor.endpoint); if (!url) { return executorCheckResult("error", "bridge_endpoint_required", checkedAt, { checkedAt, mode: "direct" }); } try { const health = await fetchJson(url, { method: "GET", headers: { Accept: "application/json" } }, 7000); const compatibility = bridgeCompatibilityStatus(health, {}); if (!compatibility.ok) { return executorCheckResult("error", "agent_update_required", optionalString(health.runtimeCheckedAt) || checkedAt, { checkedAt, mode: "direct", endpoint: executor.endpoint, health: summarizeBridgeHealth(health), bridgeCompatibility: compatibility, }); } const runtimeOk = health.ok !== false && health.runtimeStatus !== "error"; return executorCheckResult( runtimeOk ? "online" : "error", runtimeOk ? "bridge_health_ok" : optionalString(health.runtimeError || health.error) || "bridge_health_failed", optionalString(health.runtimeCheckedAt) || checkedAt, { checkedAt, mode: "direct", endpoint: executor.endpoint, health: summarizeBridgeHealth(health), } ); } catch (error) { const statusDetail = errorMessage(error); return executorCheckResult("offline", statusDetail || "bridge_direct_unavailable", checkedAt, { checkedAt, mode: "direct", endpoint: executor.endpoint, error: statusDetail, status: Number(error?.status || 0) || null, }); } } async function readExecutorEvents(executor, options = {}) { if (executor.connectionMode !== "hub" && !executor.pairingCode) { return { events: [], latestEventId: 0, online: false }; } const pairingCode = cleanPairingCode(executor.pairingCode); if (!pairingCode) { return { events: [], latestEventId: 0, online: false }; } const params = new URLSearchParams(); params.set("since", String(Number(options.since || 0))); params.set("limit", String(sanitizeLimit(options.limit, 100, 500))); const payload = await hubRequestJson( `/api/ai-workspace/hub/v1/agents/${encodeURIComponent(pairingCode)}/events?${params.toString()}`, { method: "GET" }, 10000 ); return { events: Array.isArray(payload.events) ? payload.events.map(cleanBridgeEventForUi) : [], latestEventId: Number(payload.latestEventId || 0), online: payload.online === true, }; } async function dispatchExecutorMessage(executor, payload) { if (executor.connectionMode === "hub" || executor.pairingCode) { const pairingCode = cleanPairingCode(executor.pairingCode); if (!pairingCode) { const error = new Error("bridge_pairing_code_empty"); error.status = 400; throw error; } const response = await hubRequestJson( `/api/ai-workspace/hub/v1/agents/${encodeURIComponent(pairingCode)}/dispatch`, { method: "POST", body: { command: "message", payload, timeoutMs: 12 * 60 * 60 * 1000, quiet: false, }, }, 10000 ); return { ok: true, accepted: true, requestId: optionalString(response.requestId), threadId: payload.threadId, mode: "hub", }; } const url = bridgeCommandUrl(executor.endpoint, "message"); if (!url) { const error = new Error("bridge_endpoint_required"); error.status = 400; throw error; } const response = await fetchJson( url, { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json" }, body: JSON.stringify(payload), }, 120000 ); return { ok: response?.ok !== false, accepted: false, response, threadId: payload.threadId, mode: "direct", }; } async function dispatchExecutorStop(executor, payload) { if (executor.connectionMode === "hub" || executor.pairingCode) { const pairingCode = cleanPairingCode(executor.pairingCode); if (!pairingCode) { const error = new Error("bridge_pairing_code_empty"); error.status = 400; throw error; } const response = await hubRequestJson( `/api/ai-workspace/hub/v1/agents/${encodeURIComponent(pairingCode)}/dispatch`, { method: "POST", body: { command: "stop", payload, timeoutMs: 30000, quiet: false, }, }, 10000 ); return { ok: true, accepted: true, requestId: optionalString(response.requestId), targetRequestId: payload.requestId, threadId: payload.threadId, mode: "hub", }; } const url = bridgeCommandUrl(executor.endpoint, "stop"); if (!url) { const error = new Error("bridge_endpoint_required"); error.status = 400; throw error; } const response = await fetchJson( url, { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json" }, body: JSON.stringify(payload), }, 10000 ); return { ok: response?.ok !== false, accepted: false, response, targetRequestId: payload.requestId, threadId: payload.threadId, mode: "direct", }; } async function createBridgeRun({ owner, thread, executor, bridge, payload }) { const requestId = optionalString(bridge?.requestId); if (!requestId || bridge?.accepted !== true || bridge?.mode !== "hub") return null; const context = isPlainObject(payload?.context) ? payload.context : {}; const runProfile = isPlainObject(payload?.runProfile) ? payload.runProfile : null; const metadata = { modeId: optionalString(context.modeId), modeTitle: optionalString(context.modeTitle), workspacePath: optionalString(payload?.workspacePath), threadTitle: optionalString(payload?.threadTitle) || thread.title, executorName: executor.name, executorType: executor.type, runProfile: runProfile ? redactRunProfile(runProfile) : null, }; const result = await pool.query( `insert into ai_workspace_runs ( id, owner_key, owner_user_id, owner_email, thread_id, executor_id, request_id, origin_surface, status, latest_event_id, metadata ) values ($1, $2, $3, $4, $5, $6, $7, $8, 'running', 0, $9::jsonb) on conflict (owner_key, thread_id, request_id) do update set owner_user_id = excluded.owner_user_id, owner_email = excluded.owner_email, executor_id = excluded.executor_id, origin_surface = excluded.origin_surface, status = 'running', completed_at = null, metadata = ai_workspace_runs.metadata || excluded.metadata, updated_at = now() returning *`, [ randomUUID(), owner.key, owner.userId, owner.email, thread.id, executor.id, requestId, thread.originSurface || "global", JSON.stringify(metadata), ] ); return toBridgeRun(result.rows[0]); } async function updateBridgeRunState(run, patch) { if (!run?.id) return null; const fields = []; const values = [run.id]; if (Object.hasOwn(patch, "status")) { values.push(sanitizeEnum(patch.status, SUPPORTED_RUN_STATUSES, "unsupported_run_status")); fields.push(`status = $${values.length}`); } if (Object.hasOwn(patch, "latestEventId")) { const rawLatestEventId = Number(patch.latestEventId || 0); const latestEventId = Number.isFinite(rawLatestEventId) ? Math.max(0, Math.trunc(rawLatestEventId)) : 0; values.push(latestEventId); fields.push(`latest_event_id = greatest(latest_event_id, $${values.length})`); } if (Object.hasOwn(patch, "completedAt")) { values.push(patch.completedAt ? new Date(patch.completedAt).toISOString() : null); fields.push(`completed_at = $${values.length}::timestamptz`); } if (Object.hasOwn(patch, "metadata")) { values.push(JSON.stringify(isPlainObject(patch.metadata) ? patch.metadata : {})); fields.push(`metadata = metadata || $${values.length}::jsonb`); } if (fields.length === 0) return run; const result = await pool.query( `update ai_workspace_runs set ${fields.join(", ")}, updated_at = now() where id = $1 returning *`, values ); return result.rows[0] ? toBridgeRun(result.rows[0]) : null; } async function persistBridgeRunEvents(run, events) { if (!run?.id || !Array.isArray(events) || events.length === 0) return; for (const event of events) { const hubEventId = bridgeEventId(event); const occurredAt = eventOccurredAt(event) || new Date().toISOString(); await pool.query( `insert into ai_workspace_run_events ( id, run_id, owner_key, thread_id, request_id, hub_event_id, kind, payload, occurred_at ) values ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9::timestamptz) on conflict do nothing`, [ randomUUID(), run.id, run.ownerKey, run.threadId, run.requestId, hubEventId, optionalString(event?.kind) || "event", JSON.stringify(isPlainObject(event) ? event : { value: event }), occurredAt, ] ); } } async function listBridgeRunEvents(run, limit = 1000) { if (!run?.id) return []; const result = await pool.query( `select payload from ( select payload, occurred_at, created_at from ai_workspace_run_events where run_id = $1 order by occurred_at desc, created_at desc limit $2 ) e order by occurred_at asc, created_at asc`, [run.id, sanitizeLimit(limit, 1000, 5000)] ); return result.rows.map((row) => row.payload).filter(Boolean); } async function recoverBridgeRunMirrors() { if (!config.hubInternalHttpUrl || !config.hubInternalAccessToken) return; const result = await pool.query( `select r.*, to_jsonb(e) as executor_row from ai_workspace_runs r join ai_workspace_executors e on e.id = r.executor_id and e.owner_key = r.owner_key where r.status = 'running' and r.started_at > now() - interval '12 hours' order by r.started_at asc limit 200` ); for (const row of result.rows) { const run = toBridgeRun(row); const executor = row.executor_row ? toExecutor(row.executor_row) : null; if (!executor) continue; const owner = { key: run.ownerKey, userId: run.ownerUserId, email: run.ownerEmail }; const thread = { id: run.threadId, originSurface: run.originSurface }; const bridge = { accepted: true, mode: "hub", requestId: run.requestId }; const events = await listBridgeRunEvents(run); startBridgeRunMirror({ owner, thread, executor, bridge, run, events }); } } function startBridgeRunMirror({ owner, thread, executor, bridge, run = null, events = [] }) { const requestId = optionalString(bridge?.requestId || run?.requestId); if (!requestId || bridge?.accepted !== true || bridge?.mode !== "hub") return; const threadId = thread?.id || run?.threadId; if (!owner?.key || !threadId || !executor) return; const key = `${owner.key}:${threadId}:${requestId}`; if (activeBridgeRunMirrors.has(key)) return; const restoredStartedAt = run?.startedAt ? new Date(run.startedAt).getTime() : 0; const state = { key, owner, threadId, originSurface: thread?.originSurface || run?.originSurface || "global", executorId: executor.id, executor, requestId, run, latestEventId: Math.max(0, Number(run?.latestEventId || 0)), events: Array.isArray(events) ? events.slice(-1000) : [], startedAt: Number.isFinite(restoredStartedAt) && restoredStartedAt > 0 ? restoredStartedAt : Date.now(), }; activeBridgeRunMirrors.set(key, state); void mirrorBridgeRun(state); } async function mirrorBridgeRun(state) { try { const done = await mirrorBridgeRunOnce(state); if (done) { activeBridgeRunMirrors.delete(state.key); return; } if (Date.now() - state.startedAt > BRIDGE_RUN_MIRROR_MAX_MS) { await createThreadMessage(state.owner, state.threadId, { role: "assistant", content: "Bridge request timed out before a final assistant response was persisted.", payload: { surface: state.originSurface, kind: "bridge_failure", requestId: state.requestId, executorId: state.executorId, }, }); if (state.run) { state.run = await updateBridgeRunState(state.run, { status: "timeout", latestEventId: state.latestEventId, completedAt: new Date().toISOString(), }); } activeBridgeRunMirrors.delete(state.key); return; } } catch { // Transient Hub/DB failures are retried while the request remains within the mirror window. } setTimeout(() => void mirrorBridgeRun(state), BRIDGE_RUN_MIRROR_POLL_MS); } async function mirrorBridgeRunOnce(state) { const previousLatestEventId = state.latestEventId; const payload = await readExecutorEvents(state.executor, { since: state.latestEventId, limit: 500, }); state.latestEventId = Math.max(state.latestEventId, Number(payload.latestEventId || 0)); const nextEvents = (Array.isArray(payload.events) ? payload.events : []) .filter((event) => optionalString(event?.requestId) === state.requestId); if (nextEvents.length) { if (state.run) await persistBridgeRunEvents(state.run, nextEvents); state.events = [...state.events, ...nextEvents].slice(-1000); } if (state.run && state.latestEventId !== previousLatestEventId) { state.run = await updateBridgeRunState(state.run, { latestEventId: state.latestEventId }); } const failure = latestBridgeFailureEvent(state.events); if (failure) { await createThreadMessage(state.owner, state.threadId, { role: "assistant", content: bridgeFailureText(failure), payload: { surface: state.originSurface, kind: "bridge_failure", requestId: state.requestId, executorId: state.executorId, }, }); if (state.run) { state.run = await updateBridgeRunState(state.run, { status: bridgeRunStatusFromFailure(failure), latestEventId: state.latestEventId, completedAt: new Date().toISOString(), }); } return true; } const hasDone = state.events.some((event) => optionalString(event?.kind) === "done"); if (!hasDone) return false; const finalText = latestBridgeAssistantText(state.events); if (!finalText) return false; await createThreadMessage(state.owner, state.threadId, { role: "assistant", content: finalText, payload: { surface: state.originSurface, kind: "bridge_final", requestId: state.requestId, executorId: state.executorId, }, }); if (state.run) { state.run = await updateBridgeRunState(state.run, { status: "completed", latestEventId: state.latestEventId, completedAt: new Date().toISOString(), }); } return true; } function latestBridgeFailureEvent(events) { return [...(Array.isArray(events) ? events : [])] .reverse() .find((event) => { const kind = optionalString(event?.kind); return kind === "error" || kind === "timeout"; }) || null; } function bridgeFailureText(event) { const text = sanitizeBridgeErrorText(event?.text || event?.message || "bridge_agent_failed"); return text.startsWith("Bridge request failed:") ? text : `Bridge request failed: ${text}`; } function latestBridgeAssistantText(events) { const event = [...(Array.isArray(events) ? events : [])] .reverse() .find((item) => optionalString(item?.kind) === "codex_message" && optionalString(item?.text)); if (!event) return ""; const text = optionalString(event.text); return text.replace(/^Codex emitted assistant output:\s*/i, "").trim(); } function bridgeRunStatusFromFailure(event) { return optionalString(event?.kind) === "timeout" ? "timeout" : "failed"; } function bridgeEventId(event) { const value = Number(event?.id || event?.eventId || event?.event_id || 0); return Number.isFinite(value) && value > 0 ? Math.trunc(value) : null; } function eventOccurredAt(event) { const text = optionalString(event?.at || event?.createdAt || event?.created_at || event?.time); if (!text) return null; const date = new Date(text); return Number.isNaN(date.getTime()) ? null : date.toISOString(); } function buildBridgeMessagePayload({ thread, message, history, executor, command, ownerSettings }) { const ownerContext = isPlainObject(ownerSettings?.activeContext) ? ownerSettings.activeContext : {}; const threadContext = isPlainObject(thread.activeContext) ? thread.activeContext : {}; const commandContext = isPlainObject(command.context) ? command.context : {}; const surfaceContexts = mergeSurfaceContexts(threadContext, ownerContext, commandContext); const engineContext = surfaceContexts.engine || {}; const opsContext = surfaceContexts.ops || {}; const commandModeId = optionalString(command.modeId); const commandModeKey = normalizeKey(commandModeId); const ownerContextModeKey = normalizeKey(ownerContext.modeId); const commandContextModeKey = normalizeKey(commandContext.modeId); const requestFromOpsMode = commandModeKey === "ops" || commandContextModeKey === "ops" || normalizeKey(commandContext.surface) === "ops"; const useUnifiedAgentCore = requestFromOpsMode && isNdcAgentCoreContextReady(engineContext); const commandProvidesNdcContext = commandModeKey === "ndc-agent-core" || commandContextModeKey === "ndc-agent-core"; const preferOwnerContext = ownerContextModeKey === "ndc-agent-core" && !commandProvidesNdcContext && !commandModeId; const effectiveThreadContext = preferOwnerContext ? {} : threadContext; const ownerContextReady = isNdcAgentCoreContextReady(ownerContext); const requestContextReady = isNdcAgentCoreContextReady(effectiveThreadContext) || isNdcAgentCoreContextReady(commandContext); const promoteOwnerContext = !preferOwnerContext && ownerContextReady && !requestContextReady && !commandModeId; const enabledToolPacks = mergeToolPacks( ownerSettings?.enabledToolPacks, thread.enabledToolPacks, (preferOwnerContext || promoteOwnerContext || useUnifiedAgentCore) ? ["ndc-agent-core", "engine", "ops"] : [] ); const mergedContext = { ...(preferOwnerContext || promoteOwnerContext ? ownerContext : {}), ...effectiveThreadContext, ...commandContext, ...(useUnifiedAgentCore ? engineContext : {}), }; const mergedContextModeId = optionalString(mergedContext.modeId); const preferContextMode = requestContextReady && (!commandModeId || commandModeKey === "ops") && mergedContextModeId; const modeId = preferOwnerContext ? optionalString(ownerContext.modeId) || "ndc-agent-core" : promoteOwnerContext ? optionalString(ownerContext.modeId) || "ndc-agent-core" : useUnifiedAgentCore ? optionalString(engineContext.modeId) || "ndc-agent-core" : preferContextMode ? mergedContextModeId : commandModeId || mergedContextModeId || "ops"; const commandModeTitle = optionalString(command.modeTitle); const mergedContextModeTitle = optionalString(mergedContext.modeTitle); const modeTitle = preferOwnerContext ? optionalString(ownerContext.modeTitle) || "NDC Agent Core" : promoteOwnerContext ? optionalString(ownerContext.modeTitle) || "NDC Agent Core" : useUnifiedAgentCore ? optionalString(engineContext.modeTitle) || "NDC Agent Core" : preferContextMode ? mergedContextModeTitle || mergedContextModeId : commandModeTitle || mergedContextModeTitle || "Ops"; const context = { ...mergedContext, surface: optionalString(commandContext.surface) || optionalString(threadContext.surface) || thread.originSurface || "ops", contexts: surfaceContexts, engineContext, opsContext, enabledToolPacks, remoteControlMode: optionalString(command.remoteControlMode || mergedContext.remoteControlMode) || "remote", modeId, modeTitle, ...(useUnifiedAgentCore ? { sourceSurface: optionalString(commandContext.surface) || optionalString(threadContext.surface) || thread.originSurface || "ops" } : {}), }; return { threadId: thread.id, threadTitle: thread.title, workspacePath: optionalString(command.workspacePath) || optionalString(executor.workspacePath) || "", message: message.content, displayMessage: message.content, publicUserMessage: message.content, resume: command.resume === true, enabledToolPacks, history: Array.isArray(history) ? history.slice(-40).map((item) => ({ role: optionalString(item.role), text: optionalString(item.content), })).filter((item) => item.role && item.text) : [], context, client: { surface: optionalString(context.surface) || thread.originSurface || "ops", protocol: "ai-workspace-bridge/v1", sentAt: new Date().toISOString(), }, }; } async function buildRunProfile({ owner, thread, executor, ownerSettings, bridgePayload }) { const context = isPlainObject(bridgePayload?.context) ? bridgePayload.context : {}; const sourceSurface = optionalString(context.sourceSurface) || optionalString(context.surface) || thread.originSurface || "global"; const targetContexts = isPlainObject(context.contexts) ? context.contexts : {}; const enabledToolPacks = mergeToolPacks( ownerSettings?.enabledToolPacks, thread.enabledToolPacks, bridgePayload?.enabledToolPacks ); const grantResolution = await resolveRunAppGrants({ owner, context, ownerSettings }); const appGrants = summarizeRunAppGrants({ appGrants: grantResolution.appGrants }); const mcpServers = runProfileMcpServersFromAppGrants(ownerSettings, grantResolution.appGrants); const mcpServerNames = mcpServers.map((server) => server.serverName).filter(Boolean); const assistantActions = await assistantActionToolProfileForRun(); const appCatalog = buildRunProfileAppCatalog({ appGrants: grantResolution.appGrants, mcpServers, assistantActions, }); const appAccess = summarizeRunAppAccess(appCatalog); const requiredMcpServerNames = mcpServers .filter((server) => server.required === true) .map((server) => server.serverName) .filter(Boolean); const diagnostics = { schemaVersion: "ai-workspace.run-profile.diagnostics.v1", dynamicProfile: true, contextReady: context.contextReady !== false, accessMode: optionalString(context.accessMode) || "chat", sourceSurface, modeId: optionalString(context.modeId) || "ops", targetSurfaces: Object.keys(targetContexts).map(normalizeKey).filter(Boolean).sort(), enabledToolPacks, appGrantIds: Object.keys(appGrants).sort(), appCatalogIds: appCatalog.map((app) => app.appId).sort(), grantedAppIds: appAccess.grantedAppIds, deniedAppIds: appAccess.deniedAppIds, notGrantedAppIds: appAccess.notGrantedAppIds, entitlementAdapters: grantResolution.diagnostics, mcpServerNames, requiredMcpServerNames, assistantActionIds: assistantActions.actionIds, assistantActionGatewayConfigured: Boolean(assistantActions.gatewayUrl && assistantActions.gatewayToken), missingContext: Array.isArray(context.missingContext) ? context.missingContext.map(optionalString).filter(Boolean) : [], }; const runProfile = { schemaVersion: "ai-workspace.run-profile.v1", runId: randomUUID(), createdAt: new Date().toISOString(), owner: publicOwner(owner), executor: { id: executor.id, type: executor.type, connectionMode: executor.connectionMode, }, sourceSurface, modeId: optionalString(context.modeId) || "ops", modeTitle: optionalString(context.modeTitle) || "", targetContexts: redactForPublicDiagnostics(targetContexts), enabledToolPacks, appGrants, appCatalog, appAccess, toolProfile: { schemaVersion: "ai-workspace.tool-profile.v1", enabledToolPacks, mcpServers, mcpServerNames, requiredMcpServerNames, assistantActions, }, policyPrompt: buildRunProfilePolicyPrompt({ context, diagnostics, assistantActions }), diagnostics, }; runProfile.diagnostics.profileHash = runProfileHash(runProfile); return runProfile; } async function assistantActionToolProfileForRun() { const gatewayUrl = assistantActionGatewayUrlForRun(); const gatewayToken = optionalString( process.env.NDC_AI_WORKSPACE_ASSISTANT_ACTION_GATEWAY_TOKEN || config.hubInternalAccessToken || config.internalAccessTokens[0] || "" ); const actionIds = await assistantActionIdsForRun(); return { ...ASSISTANT_ACTION_TOOL_PROFILE_BASE, endpoint: actionGatewayEndpointPath(gatewayUrl) || ASSISTANT_ACTION_TOOL_PROFILE_BASE.endpoint, actionIds, ...(gatewayUrl ? { gatewayUrl } : {}), ...(gatewayToken ? { gatewayToken } : {}), }; } function assistantActionGatewayUrlForRun() { const explicit = cleanHttpEndpoint(process.env.NDC_AI_WORKSPACE_ASSISTANT_ACTION_GATEWAY_URL); if (explicit) return explicit; if (config.assistantActionRelayEnabled && config.assistantActionRelayId && config.hubInternalHttpUrl) { return cleanHttpEndpoint( `${config.hubInternalHttpUrl.replace(/\/+$/, "")}/api/ai-workspace/hub/v1/assistant-relays/${encodeURIComponent(config.assistantActionRelayId)}/actions` ); } return cleanHttpEndpoint(config.hubInternalHttpUrl || httpUrlFromWebSocketUrl(config.hubWebSocketUrl)); } function actionGatewayEndpointPath(gatewayUrl) { try { const url = new URL(gatewayUrl); return `${url.pathname}${url.search || ""}`; } catch { return ""; } } async function assistantActionIdsForRun() { const now = Date.now(); if ( assistantActionIdsCache.actionIds.length && now - assistantActionIdsCache.loadedAt < ASSISTANT_ACTION_CACHE_TTL_MS ) { return assistantActionIdsCache.actionIds; } try { const catalog = await loadCatalog(); const actionIds = (Array.isArray(catalog?.assistantActions?.actions) ? catalog.assistantActions.actions : []) .filter((action) => ( isPlainObject(action) && optionalString(action.id) && action.adapterStatus === "implemented" && action.confirmationMode !== "forbidden" )) .map((action) => optionalString(action.id)) .filter(Boolean) .sort(); assistantActionIdsCache = { loadedAt: now, actionIds }; return actionIds; } catch { assistantActionIdsCache = { loadedAt: now, actionIds: [] }; return []; } } function startAssistantActionRelayLoop() { if (!config.assistantActionRelayEnabled) return; if (!config.assistantActionRelayId || !config.hubInternalHttpUrl || !config.hubInternalAccessToken) { console.warn("Assistant action relay disabled: relay id, hub URL, or hub token is missing."); return; } void assistantActionRelayLoop(); } async function assistantActionRelayLoop() { while (!assistantActionRelayStopping) { try { const payload = await hubRequestJson( `/api/ai-workspace/hub/v1/assistant-relays/${encodeURIComponent(config.assistantActionRelayId)}/poll`, { method: "POST", body: { limit: 5, timeoutMs: ASSISTANT_ACTION_RELAY_POLL_TIMEOUT_MS, }, }, ASSISTANT_ACTION_RELAY_POLL_TIMEOUT_MS + 5000 ); const calls = Array.isArray(payload?.calls) ? payload.calls : []; for (const call of calls) { void handleAssistantActionRelayCall(call).catch((error) => { console.error("Assistant action relay call failed:", errorMessage(error)); }); } await delay(ASSISTANT_ACTION_RELAY_POLL_IDLE_MS); } catch (error) { if (!assistantActionRelayStopping) { console.error("Assistant action relay poll failed:", errorMessage(error)); await delay(ASSISTANT_ACTION_RELAY_ERROR_BACKOFF_MS); } } } } async function handleAssistantActionRelayCall(call) { const callId = optionalString(call?.callId); if (!callId) return; let status = 200; let body = null; try { const owner = getRequestOwner({ headers: relayCallHeaders(call?.headers), query: {} }); body = await executeAssistantActionRequest(isPlainObject(call?.payload) ? call.payload : {}, owner); } catch (error) { status = Number(error?.status || 500); body = { ok: false, error: errorMessage(error) }; } await hubRequestJson( `/api/ai-workspace/hub/v1/assistant-relays/${encodeURIComponent(config.assistantActionRelayId)}/results/${encodeURIComponent(callId)}`, { method: "POST", body: { status, body, }, }, 10000 ); } function relayCallHeaders(value) { const headers = {}; if (!isPlainObject(value)) return headers; for (const [key, rawValue] of Object.entries(value)) { const name = optionalString(key)?.toLowerCase(); const text = optionalString(rawValue); if (!name || !text) continue; headers[name] = text; } return headers; } function delay(ms) { return new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms || 0)))); } async function resolveRunAppGrants({ owner, context, ownerSettings }) { const metadata = isPlainObject(ownerSettings?.metadata) ? ownerSettings.metadata : {}; const staticAppGrants = isPlainObject(metadata.appGrants) ? metadata.appGrants : {}; const appGrants = normalizeRunAppGrantsObject(staticAppGrants); const adapterDiagnostics = []; const adapters = Array.isArray(config.entitlementAdapters) ? config.entitlementAdapters : []; if (!adapters.length) { return { appGrants, diagnostics: { source: "settings", adapters: [], }, }; } for (const adapter of adapters) { const result = await fetchRunEntitlementAdapter({ adapter, owner, context, ownerSettings }).catch((error) => ({ ok: false, error: errorMessage(error), })); if (!result.ok) { adapterDiagnostics.push({ appId: adapter.appId, status: "error", required: adapter.required === true, error: sanitizeBridgeErrorText(result.error || "entitlement_adapter_failed"), }); if (adapter.required === true) { const error = new Error(`entitlement_adapter_failed:${adapter.appId}`); error.status = 502; throw error; } continue; } const adapterAppGrants = normalizeEntitlementAdapterAppGrants(result.payload, adapter); for (const [rawAppId, grant] of Object.entries(adapterAppGrants)) { if (!isPlainObject(grant)) continue; const appId = canonicalRunAppId(grant.appId || rawAppId); if (!appId) continue; const existing = isPlainObject(appGrants[appId]) ? appGrants[appId] : {}; const merged = { ...existing, ...grant, appId, }; merged.mcpServers = isRunAppGrantDenied(merged) ? [] : Object.hasOwn(grant, "mcpServers") ? grant.mcpServers : existing.mcpServers; appGrants[appId] = merged; } adapterDiagnostics.push({ appId: adapter.appId, status: "ok", required: adapter.required === true, grantIds: Object.keys(adapterAppGrants).sort(), mcpServerNames: Object.values(adapterAppGrants) .flatMap((grant) => { const servers = Array.isArray(grant?.mcpServers) ? grant.mcpServers : isPlainObject(grant?.mcpServers) ? Object.values(grant.mcpServers) : []; return servers.map((server) => safeMcpServerName(server?.serverName || server?.server_name || server?.name)); }) .filter(Boolean), }); } return { appGrants, diagnostics: { source: adapterDiagnostics.some((item) => item.status === "ok") ? "adapters+settings" : "settings", adapters: adapterDiagnostics, }, }; } function normalizeRunAppGrantsObject(value) { const out = {}; if (!isPlainObject(value)) return out; for (const [key, rawGrant] of Object.entries(value)) { if (!isPlainObject(rawGrant)) continue; const appId = canonicalRunAppId(rawGrant.appId || rawGrant.app_id || key); if (!appId) continue; const existing = isPlainObject(out[appId]) ? out[appId] : {}; const grant = { ...existing, ...rawGrant, appId, }; grant.mcpServers = isRunAppGrantDenied(grant) ? [] : Object.hasOwn(rawGrant, "mcpServers") ? rawGrant.mcpServers : existing.mcpServers; out[appId] = grant; } return out; } async function fetchRunEntitlementAdapter({ adapter, owner, context, ownerSettings }) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), adapter.timeoutMs); const body = JSON.stringify({ schemaVersion: "ai-workspace.entitlement-request.v1", appId: adapter.appId, owner: publicOwner(owner), activeContext: redactForPublicDiagnostics(isPlainObject(ownerSettings?.activeContext) ? ownerSettings.activeContext : {}), runContext: redactForPublicDiagnostics(context), requestedAt: new Date().toISOString(), }); try { const response = await fetch(adapter.url, { method: "POST", headers: { Accept: "application/json", "Content-Type": "application/json", ...(adapter.authorization ? { Authorization: adapter.authorization } : {}), ...adapter.headers, }, body, signal: controller.signal, }); const text = await response.text(); let payload = {}; try { payload = text ? JSON.parse(text) : {}; } catch { payload = { ok: false, error: "invalid_json_response" }; } if (!response.ok || payload?.ok === false) { throw new Error(optionalString(payload?.error || payload?.message) || `http_${response.status}`); } return { ok: true, payload }; } catch (error) { if (error?.name === "AbortError") throw new Error("entitlement_adapter_timeout"); throw error; } finally { clearTimeout(timeout); } } function normalizeEntitlementAdapterAppGrants(payload, adapter) { const source = isPlainObject(payload?.appGrants) ? payload.appGrants : Array.isArray(payload?.appGrants) ? payload.appGrants : payload?.grant || payload?.appGrant || payload?.entitlement || payload?.entitlements || payload?.grants || payload; const out = {}; if (Array.isArray(source)) { for (const item of source) { const grant = normalizeEntitlementAdapterGrant(item, adapter); if (grant?.appId) out[grant.appId] = grant; } return out; } if (!isPlainObject(source)) return out; if (source.mcpServers || source.scopes || source.appId || source.app_id || source.surface) { const grant = normalizeEntitlementAdapterGrant(source, adapter); if (grant?.appId) out[grant.appId] = grant; return out; } for (const [key, value] of Object.entries(source)) { const grant = normalizeEntitlementAdapterGrant(value, { ...adapter, appId: normalizeKey(key) || adapter.appId }); if (grant?.appId) out[grant.appId] = grant; } return out; } function normalizeEntitlementAdapterGrant(value, adapter) { if (!isPlainObject(value)) return null; const appId = canonicalRunAppId(value.appId || value.app_id || adapter.appId); if (!appId) return null; return { ...value, appId, appTitle: optionalString(value.appTitle || value.app_title || value.title || adapter.title), surface: optionalString(value.surface) || appId, source: "entitlement-adapter", adapterId: adapter.id, }; } function canonicalRunAppId(value) { const appId = normalizeKey(value); if (!appId) return ""; return RUN_APP_ID_ALIASES.get(appId) || appId; } function runProfileMcpServersFromSettings(settings) { const metadata = isPlainObject(settings?.metadata) ? settings.metadata : {}; const appGrants = normalizeRunAppGrantsObject(metadata.appGrants); return runProfileMcpServersFromAppGrants(settings, appGrants); } function runProfileMcpServersFromAppGrants(settings, appGrantsInput = {}) { const servers = []; const metadata = isPlainObject(settings?.metadata) ? settings.metadata : {}; const hasEntitlementAdapters = Array.isArray(config.entitlementAdapters) && config.entitlementAdapters.length > 0; if (!hasEntitlementAdapters) { collectInstallerMcpServers(servers, metadata.mcpServers, {}); } const appGrants = normalizeRunAppGrantsObject(appGrantsInput); for (const [appId, grant] of Object.entries(appGrants)) { if (!isPlainObject(grant)) continue; if (isRunAppGrantDenied(grant)) continue; collectInstallerMcpServers(servers, grant.mcpServers, { appId: optionalString(grant.appId) || normalizeKey(appId), appTitle: optionalString(grant.appTitle || grant.title), }); } const byServerName = new Map(); for (const server of servers.map(sanitizeInstallerMcpServer).filter(Boolean)) { if (server.enabled === false) continue; byServerName.set(server.serverName, server); } return Array.from(byServerName.values()); } function summarizeRunAppGrants(metadata) { const appGrants = normalizeRunAppGrantsObject(metadata?.appGrants); const out = {}; for (const [key, value] of Object.entries(appGrants)) { if (!isPlainObject(value)) continue; const appId = canonicalRunAppId(value.appId || key); if (!appId) continue; const denied = isRunAppGrantDenied(value); const mcpServers = Array.isArray(value.mcpServers) ? value.mcpServers : isPlainObject(value.mcpServers) ? Object.values(value.mcpServers) : []; out[appId] = { appId, appTitle: optionalString(value.appTitle || value.title), surface: optionalString(value.surface) || appId, status: denied ? "denied" : "granted", granted: !denied, denied, deniedReason: denied ? optionalString(value.reason || value.deniedReason || value.denied_reason || value.status) : null, deniedText: denied ? optionalString(value.deniedText || value.denied_text) || ACCESS_DENIED_TEXT : null, updatedAt: optionalString(value.updatedAt || value.updated_at), context: redactForPublicDiagnostics(isPlainObject(value.context) ? value.context : {}), scopes: uniqueStrings(Array.isArray(value.scopes) ? value.scopes : []), hasMcpServers: !denied && mcpServers.length > 0, mcpServerNames: mcpServers .map((server) => safeMcpServerName(server?.serverName || server?.server_name || server?.name)) .filter(Boolean), }; } return out; } function buildRunProfileAppCatalog({ appGrants, mcpServers, assistantActions }) { const grants = normalizeRunAppGrantsObject(appGrants); const dynamicAppIds = Object.keys(grants) .map(canonicalRunAppId) .filter(Boolean) .filter((appId) => !APP_ROUTING_CATALOG.some((entry) => entry.appId === appId)); const catalog = [ ...APP_ROUTING_CATALOG, ...dynamicAppIds.map((appId) => ({ appId, appTitle: appId, surface: appId, skillId: `${appId}-context`, whenToUse: [], actionNamespaces: [`${appId}.*`], actionIdPrefixes: [`${appId}.`], mcpServerNames: [], requiredScopes: [], deniedText: ACCESS_DENIED_TEXT, })), ]; const mcpByAppId = new Map(); for (const server of Array.isArray(mcpServers) ? mcpServers : []) { const appId = normalizeKey(server?.appId); if (!appId) continue; const list = mcpByAppId.get(appId) || []; if (server?.serverName && !list.includes(server.serverName)) list.push(server.serverName); mcpByAppId.set(appId, list); } const actionIds = Array.isArray(assistantActions?.actionIds) ? assistantActions.actionIds : []; const allMcpServerNames = uniqueStrings((Array.isArray(mcpServers) ? mcpServers : []) .map((server) => server?.serverName) .filter(Boolean)); return catalog.map((entry) => { const grant = isPlainObject(grants[entry.appId]) ? grants[entry.appId] : null; const grantedByLegacyMcp = !grant && entry.mcpServerNames.some((name) => allMcpServerNames.includes(name)); const denied = grant ? isRunAppGrantDenied(grant) : !grantedByLegacyMcp; const status = grant ? (denied ? "denied" : "granted") : grantedByLegacyMcp ? "granted" : "not-granted"; const grantMcpServers = grant ? Array.isArray(grant.mcpServers) ? grant.mcpServers : isPlainObject(grant.mcpServers) ? Object.values(grant.mcpServers) : [] : []; const grantMcpServerNames = grantMcpServers .map((server) => safeMcpServerName(server?.serverName || server?.server_name || server?.name)) .filter(Boolean); const advertisedMcpServerNames = uniqueStrings([ ...entry.mcpServerNames, ...grantMcpServerNames, ...(mcpByAppId.get(entry.appId) || []), ]); const actionIdPrefixes = uniqueStrings([ ...(Array.isArray(entry.actionIdPrefixes) ? entry.actionIdPrefixes : []), `${entry.appId}.`, ...(Array.isArray(entry.appAliases) ? entry.appAliases.map((alias) => `${normalizeKey(alias)}.`) : []), ].filter(Boolean)); return { schemaVersion: "ai-workspace.app-route.v1", appId: entry.appId, appTitle: entry.appTitle, surface: entry.surface, skillId: entry.skillId, status, granted: status === "granted", denied: status === "denied", notGranted: status === "not-granted", deniedReason: status === "denied" ? optionalString(grant?.reason || grant?.deniedReason || grant?.denied_reason || grant?.status) || status : null, deniedText: status === "denied" ? optionalString(grant?.deniedText || grant?.denied_text) || entry.deniedText || ACCESS_DENIED_TEXT : null, whenToUse: uniqueStrings(entry.whenToUse), actionNamespaces: uniqueStrings(entry.actionNamespaces), actionIds: actionIds.filter((actionId) => actionIdPrefixes.some((prefix) => String(actionId || "").startsWith(prefix))), mcpServerNames: status === "granted" ? advertisedMcpServerNames : [], requiredScopes: uniqueStrings(grant?.requiredScopes || grant?.required_scopes || entry.requiredScopes), scopes: uniqueStrings(Array.isArray(grant?.scopes) ? grant.scopes : []), }; }); } function summarizeRunAppAccess(appCatalog) { const apps = Array.isArray(appCatalog) ? appCatalog : []; return { schemaVersion: "ai-workspace.app-access.v1", grantedAppIds: apps.filter((app) => app?.granted === true).map((app) => app.appId).sort(), deniedAppIds: apps.filter((app) => app?.status === "denied" || app?.denied === true).map((app) => app.appId).sort(), notGrantedAppIds: apps.filter((app) => app?.status === "not-granted").map((app) => app.appId).sort(), availableSkillIds: apps.filter((app) => app?.granted === true).map((app) => app.skillId).filter(Boolean).sort(), }; } function isRunAppGrantDenied(grant) { if (!isPlainObject(grant)) return false; const status = normalizeKey(grant.status || grant.state || grant.accessStatus || grant.access_status); return grant.enabled === false || grant.allowed === false || grant.granted === false || grant.denied === true || ["denied", "disabled", "blocked", "revoked", "not-granted", "not_granted", "forbidden"].includes(status); } function buildRunProfilePolicyPrompt({ context, diagnostics, assistantActions }) { const lines = [ "AI Workspace dynamic run profile:", `- source surface: ${diagnostics.sourceSurface}`, `- mode: ${diagnostics.modeId}`, `- access mode: ${diagnostics.accessMode}`, `- context ready: ${diagnostics.contextReady ? "yes" : "no"}`, `- entitlement source: ${diagnostics.entitlementAdapters?.source || "settings"}`, `- enabled tool packs: ${diagnostics.enabledToolPacks.length ? diagnostics.enabledToolPacks.join(", ") : "none"}`, `- app routes granted: ${diagnostics.grantedAppIds.length ? diagnostics.grantedAppIds.join(", ") : "none"}`, `- MCP servers available in this run: ${diagnostics.mcpServerNames.length ? diagnostics.mcpServerNames.join(", ") : "none"}`, `- assistant action ids available: ${Array.isArray(assistantActions?.actionIds) ? assistantActions.actionIds.join(", ") : "none"}`, "- Interpret the user's natural-language request first; call assistant actions only after selecting a structured action id.", "- Read assistant actions may execute after structured action selection. Privileged/write assistant actions require preview, explicit user confirmation, then execute.", "- Ops card actions advertised in this run are valid assistant actions: use ops.card.list_recent for reading cards, ops.card.create for creating cards, and ops.card.add_comment for comments instead of refusing because direct Ops MCP tools are absent.", "- Destructive assistant actions are forbidden; offer safe alternatives such as block/disable instead of delete.", "- MCP tokens and headers are runtime secrets and must never be printed in public answers.", ]; if (diagnostics.deniedAppIds.length) { lines.push( `- App routes explicitly denied by access control: ${diagnostics.deniedAppIds.join(", ")}.`, `- If the user requests one of those explicitly denied app capabilities, do not bypass it. Say exactly: ${ACCESS_DENIED_TEXT}`, ); } else { lines.push("- No app route is explicitly denied in this run; do not claim module access is restricted."); } const opsContext = isPlainObject(context?.contexts?.ops) ? context.contexts.ops : {}; if (opsContext.opsWorkspaceSlug || opsContext.opsProjectId) { lines.push( `- Ops target workspace: ${opsContext.opsWorkspaceSlug || opsContext.opsWorkspaceId || "unknown"}`, `- Ops target project: ${opsContext.opsProjectId || "unknown"}` ); } const engineContext = isPlainObject(context?.contexts?.engine) ? context.contexts.engine : {}; if (engineContext.workflowId || engineContext.agentNodeId) { lines.push( `- Engine target workflow: ${engineContext.workflowId || "unknown"}`, `- Engine target agent node: ${engineContext.agentNodeId || "unknown"}` ); } return lines.join("\n"); } function runProfileHash(runProfile) { const publicProfile = redactRunProfile(runProfile); const stableProfile = { ...publicProfile, runId: undefined, createdAt: undefined, diagnostics: { ...(isPlainObject(publicProfile?.diagnostics) ? publicProfile.diagnostics : {}), profileHash: undefined, }, }; return createHash("sha256").update(JSON.stringify(stableProfile)).digest("hex").slice(0, 16); } function redactRunProfile(runProfile) { if (!isPlainObject(runProfile)) return null; const toolProfile = isPlainObject(runProfile.toolProfile) ? runProfile.toolProfile : {}; return { ...runProfile, targetContexts: redactForPublicDiagnostics(runProfile.targetContexts), appGrants: redactForPublicDiagnostics(runProfile.appGrants), toolProfile: { ...toolProfile, assistantActions: redactAssistantActions(toolProfile.assistantActions), mcpServers: Array.isArray(toolProfile.mcpServers) ? toolProfile.mcpServers.map(redactMcpServer) : [], }, }; } function redactAssistantActions(value) { if (!isPlainObject(value)) return value; return { ...value, ...(value.gatewayToken ? { gatewayToken: "" } : {}), }; } function redactMcpServer(server) { if (!isPlainObject(server)) return {}; const headers = isPlainObject(server.httpHeaders) ? server.httpHeaders : {}; return { ...server, httpHeaders: Object.fromEntries(Object.keys(headers).map((key) => [key, ""])), headers: undefined, }; } function redactForPublicDiagnostics(value, depth = 0) { if (depth > 6) return "[max-depth]"; if (Array.isArray(value)) return value.map((item) => redactForPublicDiagnostics(item, depth + 1)); if (!isPlainObject(value)) return value; const out = {}; for (const [key, item] of Object.entries(value)) { if (/token|secret|password|authorization|cookie|api[_-]?key/i.test(key)) { out[key] = ""; continue; } out[key] = redactForPublicDiagnostics(item, depth + 1); } return out; } function mergeSurfaceContexts(...contexts) { const out = {}; for (const context of contexts) { if (!isPlainObject(context)) continue; const bank = isPlainObject(context.contexts) ? context.contexts : {}; for (const [key, value] of Object.entries(bank)) { const normalizedKey = normalizeKey(key); if (!normalizedKey || !isPlainObject(value)) continue; out[normalizedKey] = mergeBridgeContextObject(out[normalizedKey], value); } const surface = normalizeKey(context.surface); const mode = normalizeKey(context.modeId); const inferredSurface = surface === "engine" || surface === "ops" ? surface : mode === "ndc-agent-core" ? "engine" : mode === "ops" ? "ops" : ""; if (inferredSurface) { const { contexts: _contexts, engineContext: _engineContext, opsContext: _opsContext, ...plain } = context; out[inferredSurface] = mergeBridgeContextObject(out[inferredSurface], plain); } } return out; } function mergeBridgeContextObject(current, incoming) { const out = isPlainObject(current) ? { ...current } : {}; if (!isPlainObject(incoming)) return out; for (const [key, value] of Object.entries(incoming)) { if (key === "contexts") continue; if (isBridgeEmptyContextValue(value) && !isBridgeEmptyContextValue(out[key])) continue; if (isPlainObject(value) && isPlainObject(out[key])) { out[key] = mergeBridgeContextObject(out[key], value); } else { out[key] = value; } } return out; } function isBridgeEmptyContextValue(value) { if (value === null || value === undefined) return true; if (typeof value === "string") return value.trim() === ""; if (Array.isArray(value)) return value.length === 0; if (isPlainObject(value)) return Object.keys(value).length === 0; return false; } function isNdcAgentCoreContextReady(context) { if (!isPlainObject(context)) return false; return Boolean(optionalString(context.workflowId) && optionalString(context.agentNodeId)); } function missingNdcAgentCoreContext(context) { if (!isPlainObject(context) || normalizeKey(context.modeId) !== "ndc-agent-core") return []; const missing = []; if (!optionalString(context.workflowId)) missing.push("workflow"); if (!optionalString(context.agentNodeId)) missing.push("agent node"); if (!optionalString(context.roleId)) missing.push("роль"); return missing; } function missingOpsContextFields(context) { if (!isPlainObject(context) || normalizeKey(context.modeId) !== "ops") return []; const opsContext = isPlainObject(context.opsContext) ? context.opsContext : context; const missing = []; if (!optionalString(opsContext.opsWorkspaceSlug || opsContext.opsWorkspaceId)) missing.push("Ops workspace"); if (!optionalString(opsContext.opsProjectId || opsContext.opsProjectIdentifier || opsContext.opsProjectSlug)) missing.push("Ops project"); return missing; } function ndcMissingContextInstruction(context, missingContext) { const missing = Array.isArray(missingContext) ? missingContext.join(", ") : "context"; const workflow = optionalString(context?.workflowTitle || context?.workflowId) || "не выбран"; const role = optionalString(context?.roleTitle || context?.roleId) || "не выбрана"; return [ "System context guard:", "The current NODE.DC Engine context is incomplete.", `Missing: ${missing}.`, `Selected workflow: ${workflow}.`, `Selected role: ${role}.`, "You still have full chat access: answer the user's question normally, in the user's language, and describe the current connection if asked.", "Do not claim that an agent node is selected when it is missing.", "Do not create, edit, patch, deploy, or write workflow graph changes until the missing agent node is selected.", "If the user asks for development or graph edits, explain briefly that selecting an agent node is required for that specific write action.", ].join("\n"); } function opsMissingContextInstruction(context, missingContext) { const missing = Array.isArray(missingContext) ? missingContext.join(", ") : "Ops context"; const opsContext = isPlainObject(context?.opsContext) ? context.opsContext : {}; const workspace = optionalString(opsContext.opsWorkspaceTitle || opsContext.opsWorkspaceSlug || opsContext.opsWorkspaceId) || "не выбран"; const project = optionalString(opsContext.opsProjectTitle || opsContext.opsProjectIdentifier || opsContext.opsProjectId) || "не выбран"; return [ "Ops context guard:", "The current NODE.DC Ops context is incomplete.", `Missing: ${missing}.`, `Selected workspace: ${workspace}.`, `Selected project: ${project}.`, "You still have full chat access: answer the user's question normally in the user's language.", "Do not create, edit, move, or delete Ops tasks until workspace and project are selected.", "If the user asks for Ops task changes, explain briefly that selecting Ops workspace and project is required for that write action.", "Do not claim that an Engine agent node is required for Ops task writes.", ].join("\n"); } function mergeToolPacks(...values) { const out = []; for (const value of values) { const items = Array.isArray(value) ? value : []; for (const item of items) { const normalized = optionalString(item); if (!SUPPORTED_TOOL_PACKS.has(normalized) || out.includes(normalized)) continue; out.push(normalized); } } return out; } function executorCheckResult(status, statusDetail, lastSeenAt, details = {}) { return { status: SUPPORTED_EXECUTOR_STATUSES.has(status) ? status : "error", statusDetail: optionalString(statusDetail) || "bridge_check_failed", lastSeenAt: lastSeenAt || new Date().toISOString(), details, }; } async function hubRequestJson(path, options = {}, timeoutMs = 5000) { const target = `${config.hubInternalHttpUrl.replace(/\/+$/, "")}${path}`; const body = options.body === undefined ? undefined : JSON.stringify(options.body); return fetchJson( target, { method: options.method || "GET", headers: { Accept: "application/json", Authorization: `Bearer ${config.hubInternalAccessToken}`, ...(body ? { "Content-Type": "application/json" } : {}), }, body, }, timeoutMs ); } async function fetchJson(url, init = {}, timeoutMs = 5000) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { ...init, redirect: "manual", signal: controller.signal }); const text = await response.text(); let data = null; try { data = text ? JSON.parse(text) : {}; } catch { data = { ok: false, error: "invalid_json_response", body: text.slice(0, 500) }; } if (!response.ok) { const error = new Error(optionalString(data?.error || data?.message) || `http_${response.status}`); error.status = response.status; error.payload = data; throw error; } return data; } catch (error) { if (error?.name === "AbortError") { const timeoutError = new Error("bridge_check_timeout"); timeoutError.status = 504; throw timeoutError; } throw error; } finally { clearTimeout(timer); } } function bridgeHealthUrl(endpoint) { const text = optionalString(endpoint); if (!text) return ""; try { const url = new URL(text); const path = url.pathname.replace(/\/+$/, ""); if (path.endsWith("/api/ai-workspace/bridge/v1/health")) { return url.toString(); } if (path.endsWith("/api/ai-workspace/bridge/v1")) { url.pathname = `${path}/health`; return url.toString(); } url.pathname = `${path}/api/ai-workspace/bridge/v1/health`.replace(/\/+/g, "/"); return url.toString(); } catch { return ""; } } function bridgeCommandUrl(endpoint, command) { const text = optionalString(endpoint); if (!text) return ""; try { const url = new URL(text); const path = url.pathname.replace(/\/+$/, ""); const suffix = String(command || "").replace(/^\/+/, ""); if (path.endsWith(`/api/ai-workspace/bridge/v1/${suffix}`)) { return url.toString(); } if (path.endsWith("/api/ai-workspace/bridge/v1")) { url.pathname = `${path}/${suffix}`; return url.toString(); } url.pathname = `${path}/api/ai-workspace/bridge/v1/${suffix}`.replace(/\/+/g, "/"); return url.toString(); } catch { return ""; } } function summarizeBridgeHealth(health) { if (!isPlainObject(health)) return {}; return { ok: health.ok !== false, service: optionalString(health.service), agentVersion: optionalString(health.agentVersion), protocolVersion: optionalString(health.protocolVersion), capabilities: Array.isArray(health.capabilities) ? uniqueStrings(health.capabilities).slice(0, 80) : [], host: optionalString(health.host), machineName: optionalString(health.machineName), runtimeStatus: optionalString(health.runtimeStatus), runtimeError: optionalString(health.runtimeError), runtimeCheckedAt: optionalString(health.runtimeCheckedAt), hubConnected: health.hubConnected === true, hubMode: optionalString(health.hubMode), }; } function bridgeCompatibilityStatus(health, agent) { const agentVersion = optionalString(health?.agentVersion || agent?.agentVersion); const protocolVersion = optionalString(health?.protocolVersion || agent?.protocolVersion); const capabilities = uniqueStrings([ ...(Array.isArray(agent?.capabilities) ? agent.capabilities : []), ...(Array.isArray(health?.capabilities) ? health.capabilities : []), ]); const requiredCapabilities = Array.isArray(config.requiredBridgeCapabilities) ? config.requiredBridgeCapabilities : []; const missingCapabilities = requiredCapabilities.filter((item) => !capabilities.includes(item)); const minAgentVersion = optionalString(config.minimumBridgeAgentVersion); const requiredProtocolVersion = optionalString(config.requiredBridgeProtocolVersion); if (requiredProtocolVersion && protocolVersion !== requiredProtocolVersion) { return { ok: false, reason: "protocol_version_required", agentVersion, protocolVersion, requiredProtocolVersion, minimumBridgeAgentVersion: minAgentVersion, missingCapabilities, }; } if (minAgentVersion && compareSemanticVersion(agentVersion, minAgentVersion) < 0) { return { ok: false, reason: "agent_version_too_old", agentVersion, protocolVersion, requiredProtocolVersion, minimumBridgeAgentVersion: minAgentVersion, missingCapabilities, }; } if (missingCapabilities.length) { return { ok: false, reason: "capability_required", agentVersion, protocolVersion, requiredProtocolVersion, minimumBridgeAgentVersion: minAgentVersion, missingCapabilities, }; } return { ok: true, agentVersion, protocolVersion, requiredProtocolVersion, minimumBridgeAgentVersion: minAgentVersion, missingCapabilities: [], }; } function cleanBridgeEventForUi(event) { if (!isPlainObject(event)) return event; const kind = optionalString(event.kind) || ""; const text = optionalString(event.text || event.message) || ""; if (kind === "error") { return { ...event, text: sanitizeBridgeErrorText(text), message: "", }; } if (kind !== "stdout" && kind !== "stderr") return event; const cleaned = summarizeLegacyCodexEventText(text); if (!cleaned || cleaned === text) return event; return { ...event, kind: cleaned.startsWith("Codex emitted assistant output:") ? "codex_message" : "codex_event", text: cleaned, message: "", }; } function summarizeLegacyCodexEventText(value) { const text = optionalString(value) || ""; if (!text) return ""; if (/^codex\s*\n/i.test(text)) { const answer = text.replace(/^codex\s*\n/i, "").trim(); return answer ? `Codex emitted assistant output: ${answer.slice(0, 1200)}` : "Codex emitted assistant output."; } if (/^tokens used/im.test(text)) return "Tokens reported."; return text.length > 1200 ? `${text.slice(0, 1200).trim()}...` : text; } function sanitizeBridgeErrorText(value) { const text = optionalString(value) || "bridge_agent_failed"; return text.length > 1200 ? `${text.slice(0, 1200).trim()}...` : text; } async function getOwnerSettings(owner) { const result = await pool.query( "select * from ai_workspace_owner_settings where owner_key = $1", [owner.key] ); return result.rows[0] ? toOwnerSettings(result.rows[0]) : { ownerKey: owner.key, selectedExecutorId: null, activeContext: {}, enabledToolPacks: [], metadata: {} }; } async function listThreads(owner, { surface, kind = "all", limit, includeArchived = false }) { const values = [owner.key, limit]; const clauses = ["owner_key = $1"]; if (!includeArchived) clauses.push("lifecycle_state = 'active'"); if (surface) { values.push(surface); clauses.push(`origin_surface = $${values.length}`); } if (kind === "shared") { clauses.push("coalesce(active_context->>'modeId', '') <> 'codex-remote'"); } else if (kind === "remote") { clauses.push("active_context->>'modeId' = 'codex-remote'"); } const result = await pool.query( `select * from ai_workspace_threads where ${clauses.join("\n and ")} order by updated_at desc, created_at desc limit $2`, values ); return result.rows.map(toThread); } async function createThread(owner, command) { const result = await pool.query( `insert into ai_workspace_threads ( id, owner_key, owner_user_id, owner_email, title, origin_surface, active_context, enabled_tool_packs, linked_artifacts, selected_executor_id, lifecycle_state, metadata ) values ( $1, $2, $3, $4, $5, $6, $7::jsonb, $8::text[], $9::jsonb, $10, $11, $12::jsonb ) returning *`, [ randomUUID(), owner.key, owner.userId, owner.email, command.title, command.originSurface, JSON.stringify(command.activeContext), command.enabledToolPacks, JSON.stringify(command.linkedArtifacts), command.selectedExecutorId, command.lifecycleState, JSON.stringify(command.metadata), ] ); return toThread(result.rows[0]); } async function getThread(owner, threadId) { const result = await pool.query( "select * from ai_workspace_threads where owner_key = $1 and id = $2", [owner.key, threadId] ); return result.rows[0] ? toThread(result.rows[0]) : null; } async function updateThread(owner, threadId, command) { const fields = []; const values = [owner.key, threadId]; const mappings = { title: "title", originSurface: "origin_surface", activeContext: "active_context", enabledToolPacks: "enabled_tool_packs", linkedArtifacts: "linked_artifacts", selectedExecutorId: "selected_executor_id", lifecycleState: "lifecycle_state", metadata: "metadata", }; for (const [key, column] of Object.entries(mappings)) { if (!Object.hasOwn(command, key)) continue; const isJson = key === "activeContext" || key === "linkedArtifacts" || key === "metadata"; const isArray = key === "enabledToolPacks"; const value = isJson ? JSON.stringify(command[key]) : command[key]; values.push(value); fields.push(`${column} = $${values.length}${isJson ? "::jsonb" : isArray ? "::text[]" : ""}`); } if (fields.length === 0) { return getThread(owner, threadId); } values.push(new Date()); const result = await pool.query( `update ai_workspace_threads set ${fields.join(", ")}, updated_at = $${values.length} where owner_key = $1 and id = $2 returning *`, values ); return result.rows[0] ? toThread(result.rows[0]) : null; } async function listThreadMessages(owner, threadId, { limit, before }) { const values = [owner.key, threadId, limit]; const beforeSql = before ? "and m.created_at < $4::timestamptz" : ""; if (before) values.push(before); const result = await pool.query( `select m.* from ai_workspace_thread_messages m join ai_workspace_threads t on t.id = m.thread_id where t.owner_key = $1 and m.thread_id = $2 ${beforeSql} order by m.created_at desc, m.sequence desc limit $3`, values ); return result.rows.reverse().map(toThreadMessage); } async function getThreadMessage(owner, threadId, messageId) { const result = await pool.query( `select m.* from ai_workspace_thread_messages m join ai_workspace_threads t on t.id = m.thread_id where t.owner_key = $1 and m.thread_id = $2 and m.id = $3`, [owner.key, threadId, messageId] ); return result.rows[0] ? toThreadMessage(result.rows[0]) : null; } async function createThreadMessage(owner, threadId, command) { const client = await pool.connect(); try { await client.query("begin"); const thread = await client.query( "select id from ai_workspace_threads where owner_key = $1 and id = $2 for update", [owner.key, threadId] ); if (thread.rowCount === 0) { await client.query("rollback"); return null; } const bridgeRequestId = bridgeResultRequestId(command); if (bridgeRequestId) { const existing = await client.query( `select * from ai_workspace_thread_messages where thread_id = $1 and role = $2 and payload->>'kind' in ('bridge_final', 'bridge_failure') and payload->>'requestId' = $3 order by sequence asc limit 1`, [threadId, command.role, bridgeRequestId] ); if (existing.rowCount > 0) { await client.query("commit"); return toThreadMessage(existing.rows[0]); } } const sequenceResult = await client.query( "select coalesce(max(sequence), 0) + 1 as next_sequence from ai_workspace_thread_messages where thread_id = $1", [threadId] ); const sequence = Number(sequenceResult.rows[0]?.next_sequence || 1); const inserted = await client.query( `insert into ai_workspace_thread_messages ( id, thread_id, sequence, role, content, payload ) values ($1, $2, $3, $4, $5, $6::jsonb) returning *`, [randomUUID(), threadId, sequence, command.role, command.content, JSON.stringify(command.payload)] ); await client.query("update ai_workspace_threads set updated_at = now() where id = $1", [threadId]); await client.query("commit"); return toThreadMessage(inserted.rows[0]); } catch (error) { await client.query("rollback"); throw error; } finally { client.release(); } } function bridgeResultRequestId(command) { if (!command || command.role !== "assistant" || !isPlainObject(command.payload)) return ""; const kind = optionalString(command.payload.kind); if (kind !== "bridge_final" && kind !== "bridge_failure") return ""; return optionalString(command.payload.requestId || command.payload.request_id) || ""; } async function migrate() { await pool.query(` create table if not exists ai_workspace_executors ( id uuid primary key, owner_key text not null, owner_user_id text, owner_email text, name text not null, type text not null default 'codex-remote', connection_mode text not null default 'hub', endpoint text, workspace_path text, agent_port integer, pairing_code text, model text, account_label text, capabilities jsonb not null default '[]'::jsonb, status text not null default 'unknown', status_detail text, last_seen_at timestamptz, metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint ai_workspace_executors_owner_check check (owner_user_id is not null or owner_email is not null), constraint ai_workspace_executors_type_check check (type in ('codex-remote', 'ndc-agent-core')), constraint ai_workspace_executors_connection_mode_check check (connection_mode in ('hub', 'direct')), constraint ai_workspace_executors_status_check check (status in ('unknown', 'online', 'offline', 'checking', 'error')) ) `); await pool.query(` create table if not exists ai_workspace_owner_settings ( owner_key text primary key, owner_user_id text, owner_email text, selected_executor_id uuid references ai_workspace_executors(id) on delete set null, active_context jsonb not null default '{}'::jsonb, enabled_tool_packs text[] not null default '{}'::text[], metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint ai_workspace_owner_settings_owner_check check (owner_user_id is not null or owner_email is not null) ) `); await pool.query(` create table if not exists ai_workspace_threads ( id uuid primary key, owner_key text not null, owner_user_id text, owner_email text, title text not null, origin_surface text not null default 'global', active_context jsonb not null default '{}'::jsonb, enabled_tool_packs text[] not null default '{}'::text[], linked_artifacts jsonb not null default '[]'::jsonb, selected_executor_id uuid references ai_workspace_executors(id) on delete set null, lifecycle_state text not null default 'active', metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint ai_workspace_threads_owner_check check (owner_user_id is not null or owner_email is not null), constraint ai_workspace_threads_surface_check check (origin_surface in ('engine', 'ops', 'global')), constraint ai_workspace_threads_lifecycle_check check (lifecycle_state in ('active', 'archived')) ) `); await pool.query(` alter table ai_workspace_owner_settings add column if not exists active_context jsonb not null default '{}'::jsonb, add column if not exists enabled_tool_packs text[] not null default '{}'::text[] `); await pool.query(` create table if not exists ai_workspace_thread_messages ( id uuid primary key, thread_id uuid not null references ai_workspace_threads(id) on delete cascade, sequence integer not null, role text not null, content text not null default '', payload jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), constraint ai_workspace_thread_messages_role_check check (role in ('user', 'assistant', 'system', 'tool')), unique (thread_id, sequence) ) `); await pool.query(` create table if not exists ai_workspace_runs ( id uuid primary key, owner_key text not null, owner_user_id text, owner_email text, thread_id uuid not null references ai_workspace_threads(id) on delete cascade, executor_id uuid references ai_workspace_executors(id) on delete set null, request_id text not null, origin_surface text not null default 'global', status text not null default 'running', latest_event_id bigint not null default 0, metadata jsonb not null default '{}'::jsonb, started_at timestamptz not null default now(), completed_at timestamptz, created_at timestamptz not null default now(), updated_at timestamptz not null default now(), constraint ai_workspace_runs_owner_check check (owner_user_id is not null or owner_email is not null), constraint ai_workspace_runs_surface_check check (origin_surface in ('engine', 'ops', 'global')), constraint ai_workspace_runs_status_check check (status in ('running', 'completed', 'failed', 'timeout')), unique (owner_key, thread_id, request_id) ) `); await pool.query(` create table if not exists ai_workspace_run_events ( id uuid primary key, run_id uuid not null references ai_workspace_runs(id) on delete cascade, owner_key text not null, thread_id uuid not null references ai_workspace_threads(id) on delete cascade, request_id text not null, hub_event_id bigint, kind text not null default 'event', payload jsonb not null default '{}'::jsonb, occurred_at timestamptz not null default now(), created_at timestamptz not null default now() ) `); await pool.query(` create table if not exists ai_workspace_setup_codes ( id uuid primary key, owner_key text not null, owner_user_id text, owner_email text, executor_id uuid not null references ai_workspace_executors(id) on delete cascade, code_hash text not null unique, code_suffix text not null, status text not null default 'active', expires_at timestamptz not null, used_at timestamptz, metadata jsonb not null default '{}'::jsonb, created_at timestamptz not null default now(), constraint ai_workspace_setup_codes_owner_check check (owner_user_id is not null or owner_email is not null), constraint ai_workspace_setup_codes_status_check check (status in ('active', 'used', 'expired', 'revoked')) ) `); await pool.query("create index if not exists ai_workspace_executors_owner_idx on ai_workspace_executors(owner_key, updated_at desc)"); await pool.query("create index if not exists ai_workspace_executors_pairing_code_idx on ai_workspace_executors(pairing_code) where pairing_code is not null"); await pool.query("create index if not exists ai_workspace_threads_owner_surface_idx on ai_workspace_threads(owner_key, origin_surface, updated_at desc)"); await pool.query("create index if not exists ai_workspace_thread_messages_thread_idx on ai_workspace_thread_messages(thread_id, sequence)"); await pool.query("create index if not exists ai_workspace_runs_owner_thread_idx on ai_workspace_runs(owner_key, thread_id, updated_at desc)"); await pool.query("create index if not exists ai_workspace_runs_request_idx on ai_workspace_runs(owner_key, request_id)"); await pool.query("create index if not exists ai_workspace_run_events_run_idx on ai_workspace_run_events(run_id, occurred_at asc, created_at asc)"); await pool.query("create unique index if not exists ai_workspace_run_events_hub_event_idx on ai_workspace_run_events(run_id, hub_event_id) where hub_event_id is not null"); await pool.query("create index if not exists ai_workspace_setup_codes_owner_executor_idx on ai_workspace_setup_codes(owner_key, executor_id, created_at desc)"); await pool.query("create index if not exists ai_workspace_setup_codes_expires_idx on ai_workspace_setup_codes(status, expires_at)"); } function sanitizeExecutorCommand(payload, { partial }) { const source = isPlainObject(payload) ? payload : {}; const command = {}; if (Object.hasOwn(source, "id")) { command.id = sanitizeUuid(source.id, "id"); } if (!partial || Object.hasOwn(source, "name")) { command.name = requireNonEmptyString(source.name, "name"); } if (!partial || Object.hasOwn(source, "type")) { command.type = sanitizeEnum(source.type || "codex-remote", SUPPORTED_EXECUTOR_TYPES, "unsupported_executor_type"); } if (!partial || Object.hasOwn(source, "connectionMode") || Object.hasOwn(source, "connection_mode")) { command.connectionMode = sanitizeEnum(source.connectionMode || source.connection_mode || "hub", SUPPORTED_CONNECTION_MODES, "unsupported_connection_mode"); } copyOptionalString(command, "endpoint", source.endpoint); copyOptionalString(command, "workspacePath", source.workspacePath || source.workspace_path); copyOptionalInteger(command, "agentPort", source.agentPort || source.agent_port, 1, 65535); copyOptionalString(command, "pairingCode", source.pairingCode || source.pairing_code); copyOptionalString(command, "model", source.model); copyOptionalString(command, "accountLabel", source.accountLabel || source.account_label); if (!partial || Object.hasOwn(source, "capabilities")) { command.capabilities = sanitizeCapabilities(source.capabilities); } if (!partial || Object.hasOwn(source, "status")) { command.status = sanitizeEnum(source.status || "unknown", SUPPORTED_EXECUTOR_STATUSES, "unsupported_executor_status"); } copyOptionalString(command, "statusDetail", source.statusDetail || source.status_detail); if (Object.hasOwn(source, "lastSeenAt") || Object.hasOwn(source, "last_seen_at")) { command.lastSeenAt = sanitizeOptionalIso(source.lastSeenAt || source.last_seen_at, "lastSeenAt"); } if (!partial || Object.hasOwn(source, "metadata")) { command.metadata = isPlainObject(source.metadata) ? source.metadata : {}; } return command; } function sanitizeThreadCommand(payload, { partial }) { const source = isPlainObject(payload) ? payload : {}; const command = {}; if (!partial || Object.hasOwn(source, "title")) { command.title = requireNonEmptyString(source.title || "AI Workspace Thread", "title"); } if (!partial || Object.hasOwn(source, "originSurface") || Object.hasOwn(source, "origin_surface")) { command.originSurface = sanitizeSurface(source.originSurface || source.origin_surface || "global"); } if (!partial || Object.hasOwn(source, "activeContext") || Object.hasOwn(source, "active_context")) { const value = source.activeContext || source.active_context; command.activeContext = isPlainObject(value) ? value : {}; } if (!partial || Object.hasOwn(source, "enabledToolPacks") || Object.hasOwn(source, "enabled_tool_packs")) { command.enabledToolPacks = sanitizeToolPacks(source.enabledToolPacks || source.enabled_tool_packs || []); } if (!partial || Object.hasOwn(source, "linkedArtifacts") || Object.hasOwn(source, "linked_artifacts")) { const value = source.linkedArtifacts || source.linked_artifacts; command.linkedArtifacts = Array.isArray(value) ? value.filter(isPlainObject) : []; } if (Object.hasOwn(source, "selectedExecutorId") || Object.hasOwn(source, "selected_executor_id")) { const selectedExecutorId = source.selectedExecutorId || source.selected_executor_id; command.selectedExecutorId = selectedExecutorId ? sanitizeUuid(selectedExecutorId, "selectedExecutorId") : null; } if (!partial || Object.hasOwn(source, "lifecycleState") || Object.hasOwn(source, "lifecycle_state")) { command.lifecycleState = sanitizeEnum(source.lifecycleState || source.lifecycle_state || "active", SUPPORTED_THREAD_STATES, "unsupported_thread_state"); } if (!partial || Object.hasOwn(source, "metadata")) { command.metadata = isPlainObject(source.metadata) ? source.metadata : {}; } return command; } function sanitizeOwnerSettingsCommand(payload) { const source = isPlainObject(payload) ? payload : {}; const command = {}; if (Object.hasOwn(source, "selectedExecutorId") || Object.hasOwn(source, "selected_executor_id")) { const selectedExecutorId = source.selectedExecutorId || source.selected_executor_id; command.selectedExecutorId = selectedExecutorId ? sanitizeUuid(selectedExecutorId, "selectedExecutorId") : null; } if (Object.hasOwn(source, "activeContext") || Object.hasOwn(source, "active_context")) { const activeContext = source.activeContext || source.active_context; command.activeContext = isPlainObject(activeContext) ? activeContext : {}; } if (Object.hasOwn(source, "enabledToolPacks") || Object.hasOwn(source, "enabled_tool_packs")) { command.enabledToolPacks = sanitizeToolPacks(source.enabledToolPacks || source.enabled_tool_packs || []); } if (Object.hasOwn(source, "metadata")) { command.metadata = isPlainObject(source.metadata) ? source.metadata : {}; } return command; } function sanitizeRunProfileCommand(payload) { const source = isPlainObject(payload) ? payload : {}; const context = isPlainObject(source.context) ? source.context : {}; const client = isPlainObject(source.client) ? source.client : {}; const selectedExecutorId = source.selectedExecutorId || source.selected_executor_id || null; const toolPacks = mergeToolPacks( source.enabledToolPacks, source.enabled_tool_packs, context.enabledToolPacks, ); return { selectedExecutorId: selectedExecutorId ? sanitizeUuid(selectedExecutorId, "selectedExecutorId") : null, threadId: optionalString(source.threadId || source.thread_id), threadTitle: optionalString(source.threadTitle || source.thread_title), workspacePath: optionalString(source.workspacePath || source.workspace_path), originSurface: sanitizeSurface(source.originSurface || source.origin_surface || context.sourceSurface || context.surface || client.surface || "engine"), context, client, enabledToolPacks: toolPacks, }; } function sanitizeBridgeDispatchCommand(payload) { const source = isPlainObject(payload) ? payload : {}; const command = sanitizeRunProfileCommand(source); return { ...command, message: optionalString(source.message || source.content), displayMessage: optionalString(source.displayMessage || source.display_message), publicUserMessage: optionalString(source.publicUserMessage || source.public_user_message), resume: source.resume === true, history: Array.isArray(source.history) ? source.history.slice(-40).map((item) => ({ role: optionalString(item?.role), text: optionalString(item?.text || item?.content), })).filter((item) => item.role && item.text) : [], }; } function sanitizeThreadKind(value) { const text = normalizeKey(value || "all"); if (text === "shared" || text === "remote" || text === "all") return text; return "all"; } function sanitizeBridgeStopCommand(payload) { const source = isPlainObject(payload) ? payload : {}; return { requestId: optionalString(source.requestId || source.request_id), threadId: optionalString(source.threadId || source.thread_id), reason: optionalString(source.reason) || "user_stop", client: isPlainObject(source.client) ? source.client : {}, }; } function sanitizeMessageCommand(payload) { const source = isPlainObject(payload) ? payload : {}; return { role: sanitizeEnum(source.role || "user", SUPPORTED_MESSAGE_ROLES, "unsupported_message_role"), content: optionalString(source.content) || "", payload: isPlainObject(source.payload) ? source.payload : {}, }; } function sanitizeDispatchCommand(payload) { const source = isPlainObject(payload) ? payload : {}; return { messageId: sanitizeUuid(source.messageId || source.message_id, "messageId"), selectedExecutorId: source.selectedExecutorId || source.selected_executor_id ? sanitizeUuid(source.selectedExecutorId || source.selected_executor_id, "selectedExecutorId") : null, context: isPlainObject(source.context) ? source.context : {}, workspacePath: optionalString(source.workspacePath || source.workspace_path), remoteControlMode: optionalString(source.remoteControlMode || source.remote_control_mode), modeId: optionalString(source.modeId || source.mode_id), modeTitle: optionalString(source.modeTitle || source.mode_title), resume: source.resume === true, }; } function getRequestOwner(req) { const userId = optionalString(req.headers["x-nodedc-user-id"] || req.query.userId); const email = normalizeEmail(req.headers["x-nodedc-user-email"] || req.query.email); const role = normalizeKey(req.headers["x-nodedc-user-role"] || req.query.role); const groups = uniqueStrings( String(req.headers["x-nodedc-user-groups"] || req.query.groups || "") .split(/[\n,;]+/) .map((item) => item.trim()) .filter(Boolean) ); if (!userId && !email) { throw badRequest("ai_workspace_owner_required"); } return { key: email ? `email:${email}` : `user:${userId}`, userId, email, role: role || "", groups, }; } function sanitizeAssistantActionCommand(body, owner) { const source = isPlainObject(body) ? body : {}; const phase = normalizeKey(source.phase || source.mode || "preview"); if (!["preview", "dry-run", "execute"].includes(phase)) { throw badRequest("assistant_action_phase_invalid"); } const sourceInput = isPlainObject(source.input) ? source.input : source; const input = { ...sourceInput }; delete input.phase; delete input.mode; delete input.input; const ownerContext = assistantOwnerContext(owner, sourceInput); input.actorUserId = owner.userId || null; input.actorEmail = owner.email || null; input.actorSubject = owner.key; input.groups = owner.groups; input.launcherGlobalRole = ownerContext.launcherGlobalRole; input.membershipRole = ownerContext.membershipRole; input.membershipStatus = ownerContext.membershipStatus; input.launcherUserStatus = ownerContext.launcherUserStatus; input.assistantRole = ownerContext.assistantRole; const confirmationToken = optionalString(source.confirmationToken || sourceInput.confirmationToken || source.confirmation?.token); if (confirmationToken) input.confirmationToken = confirmationToken; return { phase, input }; } async function executeAssistantActionRequest(body, owner) { const command = sanitizeAssistantActionCommand(body, owner); let opsGatewayToken = ""; if (command.phase === "execute") { const ownerSettings = await getOwnerSettings(owner); const grantResolution = await resolveRunAppGrants({ owner, context: isPlainObject(ownerSettings.activeContext) ? ownerSettings.activeContext : {}, ownerSettings, }); opsGatewayToken = opsGatewayTokenFromAppGrants(ownerSettings, grantResolution.appGrants, config.opsGatewayBaseUrl); } const action = await handleAssistantCallerRequest(command, { baseUrl: config.ontologyLauncherBaseUrl, launcherInternalToken: config.launcherInternalAccessToken, opsGatewayBaseUrl: config.opsGatewayBaseUrl, opsGatewayToken, opsEntitlementUrl: config.opsEntitlementUrl, opsEntitlementAuthorization: config.opsEntitlementAuthorization, defaultOpsWorkspaceSlug: config.defaultOpsWorkspaceSlug, defaultOpsProjectId: config.defaultOpsProjectId, }); return { ok: true, owner: publicOwner(owner), action: publicAssistantCallerResult(action) }; } function urlOrigin(value) { try { return new URL(value).origin; } catch { return ""; } } function opsGatewayTokenFromAppGrants(ownerSettings, appGrants, opsGatewayBaseUrl) { const servers = runProfileMcpServersFromAppGrants(ownerSettings, appGrants); const targetOrigin = urlOrigin(opsGatewayBaseUrl); const preferred = servers.find((server) => ( server.appId === "ops" && (!targetOrigin || urlOrigin(server.url) === targetOrigin) )) || servers.find((server) => server.appId === "ops"); return bearerTokenFromHeaders(preferred?.httpHeaders); } function assistantOwnerContext(owner, sourceInput = {}) { const groups = new Set(owner.groups || []); const ownerRole = normalizeKey(owner.role); const isRoot = ownerRole === "root-admin" || ownerRole === "root_admin" || groups.has("nodedc:superadmin") || groups.has("nodedc:launcher:admin"); const membershipRole = launcherMembershipRoleFromOwner(ownerRole, sourceInput); const assistantRole = assistantRoleFromOwner({ isRoot, sourceInput }); return { assistantRole, launcherGlobalRole: isRoot ? "root_admin" : ownerRole.replace(/-/g, "_"), membershipRole, membershipStatus: optionalString(sourceInput.membershipStatus) || "active", launcherUserStatus: optionalString(sourceInput.launcherUserStatus || sourceInput.globalStatus) || "active", }; } function assistantRoleFromOwner({ isRoot, sourceInput }) { if (isRoot) return "admin"; const value = normalizeKey(sourceInput.assistantRole || sourceInput.coreAssistantRole); if (value === "assistant-admin" || value === "admin") return "admin"; if (value === "assistant-blocked" || value === "blocked") return "blocked"; return "member"; } function launcherMembershipRoleFromOwner(ownerRole, sourceInput = {}) { const role = ownerRole.replace(/-/g, "_"); if (["client_owner", "client_admin", "member"].includes(role)) return role; const sourceRole = normalizeKey(sourceInput.membershipRole).replace(/-/g, "_"); if (["client_owner", "client_admin", "member"].includes(sourceRole)) return sourceRole; return "member"; } function publicOwner(owner) { return { ownerKey: owner.key, userId: owner.userId, email: owner.email, role: owner.role || "", groups: Array.isArray(owner.groups) ? owner.groups : [], }; } function publicAssistantCallerResult(result) { if (!isPlainObject(result)) return result; const { raw: _raw, ...publicResult } = result; return publicResult; } function publicOwnerSettings(settings) { if (!settings || !isPlainObject(settings)) return settings; return { ...settings, metadata: redactOwnerSettingsMetadata(settings.metadata), }; } function redactOwnerSettingsMetadata(metadata) { if (!isPlainObject(metadata)) return {}; const next = { ...metadata }; if (isPlainObject(next.appGrants)) { const appGrants = {}; for (const [appId, grant] of Object.entries(next.appGrants)) { if (!isPlainObject(grant)) continue; appGrants[appId] = { ...grant, mcpServers: redactMcpServers(grant.mcpServers), }; } next.appGrants = appGrants; } if (Object.hasOwn(next, "mcpServers")) { next.mcpServers = redactMcpServers(next.mcpServers); } return next; } function redactMcpServers(value) { const items = Array.isArray(value) ? value : isPlainObject(value) ? Object.values(value) : []; return items.map((item) => { if (!isPlainObject(item)) return null; const redacted = { ...item }; if (isPlainObject(redacted.httpHeaders) || isPlainObject(redacted.http_headers) || isPlainObject(redacted.headers)) { redacted.httpHeaders = { configured: true }; delete redacted.http_headers; delete redacted.headers; } return redacted; }).filter(Boolean); } function toExecutor(row) { return { id: row.id, ownerKey: row.owner_key, ownerUserId: row.owner_user_id, ownerEmail: row.owner_email, name: row.name, type: row.type, connectionMode: row.connection_mode, endpoint: row.endpoint, workspacePath: row.workspace_path, agentPort: row.agent_port, pairingCode: row.pairing_code, model: row.model, accountLabel: row.account_label, capabilities: sanitizeCapabilities(row.capabilities), status: row.status, statusDetail: row.status_detail, lastSeenAt: toIso(row.last_seen_at), metadata: row.metadata || {}, createdAt: toIso(row.created_at), updatedAt: toIso(row.updated_at), }; } function toOwnerSettings(row) { return { ownerKey: row.owner_key, ownerUserId: row.owner_user_id, ownerEmail: row.owner_email, selectedExecutorId: row.selected_executor_id, activeContext: row.active_context || {}, enabledToolPacks: row.enabled_tool_packs || [], metadata: row.metadata || {}, createdAt: toIso(row.created_at), updatedAt: toIso(row.updated_at), }; } function toThread(row) { return { id: row.id, ownerKey: row.owner_key, ownerUserId: row.owner_user_id, ownerEmail: row.owner_email, title: row.title, originSurface: row.origin_surface, activeContext: row.active_context || {}, enabledToolPacks: row.enabled_tool_packs || [], linkedArtifacts: row.linked_artifacts || [], selectedExecutorId: row.selected_executor_id, lifecycleState: row.lifecycle_state, metadata: row.metadata || {}, createdAt: toIso(row.created_at), updatedAt: toIso(row.updated_at), }; } function toThreadMessage(row) { return { id: row.id, threadId: row.thread_id, sequence: row.sequence, role: row.role, content: row.content, payload: row.payload || {}, createdAt: toIso(row.created_at), }; } function toBridgeRun(row) { return { id: row.id, ownerKey: row.owner_key, ownerUserId: row.owner_user_id, ownerEmail: row.owner_email, threadId: row.thread_id, executorId: row.executor_id, requestId: row.request_id, originSurface: row.origin_surface, status: row.status, latestEventId: Number(row.latest_event_id || 0), metadata: row.metadata || {}, startedAt: toIso(row.started_at), completedAt: toIso(row.completed_at), createdAt: toIso(row.created_at), updatedAt: toIso(row.updated_at), }; } function requireInternalApi(req, res, next) { if (!config.internalAccessTokens.length) { res.status(503).json({ ok: false, error: "ai_workspace_assistant_token_not_configured" }); return; } const authorization = typeof req.headers.authorization === "string" ? req.headers.authorization : ""; const bearerToken = authorization.match(/^Bearer\s+(.+)$/i)?.[1] || ""; const headerToken = typeof req.headers["x-nodedc-internal-token"] === "string" ? req.headers["x-nodedc-internal-token"] : ""; const requestToken = bearerToken || headerToken; if (!config.internalAccessTokens.some((token) => safeTokenEquals(requestToken, token))) { res.status(401).json({ ok: false, error: "ai_workspace_assistant_unauthorized" }); return; } next(); } function safeTokenEquals(actual, expected) { if (!actual || !expected) return false; const actualBuffer = Buffer.from(actual); const expectedBuffer = Buffer.from(expected); if (actualBuffer.length !== expectedBuffer.length) return false; return timingSafeEqual(actualBuffer, expectedBuffer); } function sanitizeSurface(value) { return sanitizeEnum(value || "global", SUPPORTED_SURFACES, "unsupported_surface"); } function sanitizeToolPacks(value) { if (!Array.isArray(value)) return []; return Array.from(new Set(value.map(normalizeKey).filter(Boolean))).filter((toolPack) => { if (!SUPPORTED_TOOL_PACKS.has(toolPack)) throw badRequest("unsupported_tool_pack"); return true; }); } function sanitizeCapabilities(value) { const items = Array.isArray(value) ? value : Array.isArray(value?.items) ? value.items : isPlainObject(value) ? Object.entries(value).filter(([, enabled]) => enabled === true).map(([key]) => key) : []; return Array.from(new Set(items.map(normalizeKey).filter(Boolean))); } function sanitizeEnum(value, supported, errorMessage) { const key = normalizeKey(value); if (!supported.has(key)) { throw badRequest(errorMessage); } return key; } function sanitizeLimit(value, fallback, max) { const limit = Number(value || fallback); if (!Number.isFinite(limit)) return fallback; return Math.min(Math.max(Math.trunc(limit), 1), max); } function normalizePairingCode(value) { return String(value || "").replace(/[^A-Za-z0-9]/g, "").toUpperCase().slice(0, 32); } function cleanPairingCode(value) { const normalized = normalizePairingCode(value); if (!normalized) return ""; return normalized.replace(/(.{4})(?=.)/g, "$1-"); } function makePairingCode() { return cleanPairingCode(randomUUID().replace(/-/g, "").slice(0, 12)); } function installerPortFromQuery(raw) { const value = optionalString(raw); if (!value) return 8787; if (!/^\d{1,5}$/.test(value)) return null; const port = Number(value); return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : null; } function psSingle(value) { return `'${String(value || "").replace(/'/g, "''")}'`; } function sanitizeUuid(value, name) { const text = optionalString(value); if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(text || "")) { throw badRequest(`${name}_invalid`); } return text; } function sanitizeOptionalIso(value, name) { const text = optionalString(value); if (!text) return null; const date = new Date(text); if (Number.isNaN(date.getTime())) { throw badRequest(`${name}_invalid`); } return date.toISOString(); } function copyOptionalString(target, key, value) { if (value === undefined) return; target[key] = key === "pairingCode" ? cleanPairingCode(value) : optionalString(value); } function copyOptionalInteger(target, key, value, min, max) { if (value === undefined) return; if (value === null || value === "") { target[key] = null; return; } const number = Number(value); if (!Number.isInteger(number) || number < min || number > max) { throw badRequest(`${key}_invalid`); } target[key] = number; } function requireNonEmptyString(value, name) { const text = optionalString(value); if (!text) throw badRequest(`${name}_required`); return text; } function optionalString(value) { if (typeof value !== "string") return null; const text = value.trim(); return text ? text : null; } function uniqueStrings(values) { const result = []; const seen = new Set(); for (const value of values || []) { const text = optionalString(value); if (!text || seen.has(text)) continue; seen.add(text); result.push(text); } return result; } function isTruthy(value) { const text = optionalString(value)?.toLowerCase() || ""; return text === "1" || text === "true" || text === "yes" || text === "on"; } function isFalsy(value) { const text = optionalString(value)?.toLowerCase() || ""; return text === "0" || text === "false" || text === "no" || text === "off"; } function normalizeEmail(value) { const text = optionalString(value); return text ? text.toLowerCase() : null; } function normalizeKey(value) { return String(value || "").trim().toLowerCase().replace(/_/g, "-"); } function compareSemanticVersion(actual, minimum) { const actualText = optionalString(actual); const minimumText = optionalString(minimum); if (!minimumText) return 0; if (!actualText) return -1; const parse = (value) => String(value || "") .split(/[.-]/) .slice(0, 4) .map((part) => { const match = part.match(/^\d+/); return match ? Number(match[0]) : 0; }); const left = parse(actualText); const right = parse(minimumText); const length = Math.max(left.length, right.length, 3); for (let i = 0; i < length; i += 1) { const delta = Number(left[i] || 0) - Number(right[i] || 0); if (delta !== 0) return delta > 0 ? 1 : -1; } return 0; } function isPlainObject(value) { return Boolean(value && typeof value === "object" && !Array.isArray(value)); } function toIso(value) { if (!value) return null; return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); } function badRequest(message) { return httpError(message, 400); } function httpError(message, status = 500) { const error = new Error(message); error.status = status; return error; } function errorMessage(error) { return String(error?.message || error?.error || error || "bridge_check_failed"); } function executorStatusForHubError(message) { const text = String(message || ""); if ( text === "ai_workspace_hub_token_not_configured" || text === "ai_workspace_hub_unauthorized" || text === "ai_workspace_hub_url_not_configured" ) { return "unknown"; } return "error"; } function asyncRoute(handler) { return (req, res, next) => { Promise.resolve(handler(req, res, next)).catch(next); }; } function httpUrlFromWebSocketUrl(value) { const text = optionalString(value); if (!text) return ""; try { const url = new URL(text); if (url.protocol === "wss:") url.protocol = "https:"; else if (url.protocol === "ws:") url.protocol = "http:"; else if (url.protocol !== "http:" && url.protocol !== "https:") return ""; url.search = ""; url.hash = ""; url.pathname = url.pathname.replace(/\/api\/ai-workspace\/hub\/?$/i, "") || "/"; return url.toString().replace(/\/+$/, ""); } catch { return ""; } } function isDeployedPublicHubUrl(value) { try { return new URL(value).hostname === "ai-hub.nodedc.ru"; } catch { return false; } } function parseEntitlementAdapters() { const adapters = []; collectEntitlementAdapters(adapters, parseJsonEnv( process.env.AI_WORKSPACE_ENTITLEMENT_ADAPTERS_JSON || process.env.NDC_AI_WORKSPACE_ENTITLEMENT_ADAPTERS_JSON || "" )); collectEntitlementAdapter(adapters, "ops", { url: process.env.AI_WORKSPACE_OPS_ENTITLEMENT_URL || process.env.NDC_AI_WORKSPACE_OPS_ENTITLEMENT_URL, token: process.env.AI_WORKSPACE_OPS_ENTITLEMENT_TOKEN || process.env.NDC_AI_WORKSPACE_OPS_ENTITLEMENT_TOKEN, required: process.env.AI_WORKSPACE_OPS_ENTITLEMENT_REQUIRED || process.env.NDC_AI_WORKSPACE_OPS_ENTITLEMENT_REQUIRED, }); collectEntitlementAdapter(adapters, "engine", { url: process.env.AI_WORKSPACE_ENGINE_ENTITLEMENT_URL || process.env.NDC_AI_WORKSPACE_ENGINE_ENTITLEMENT_URL, token: process.env.AI_WORKSPACE_ENGINE_ENTITLEMENT_TOKEN || process.env.NDC_AI_WORKSPACE_ENGINE_ENTITLEMENT_TOKEN, required: process.env.AI_WORKSPACE_ENGINE_ENTITLEMENT_REQUIRED || process.env.NDC_AI_WORKSPACE_ENGINE_ENTITLEMENT_REQUIRED, }); collectEntitlementAdapter(adapters, "launcher", { url: process.env.AI_WORKSPACE_LAUNCHER_ENTITLEMENT_URL || process.env.NDC_AI_WORKSPACE_LAUNCHER_ENTITLEMENT_URL || process.env.AI_WORKSPACE_HUB_ENTITLEMENT_URL || process.env.NDC_AI_WORKSPACE_HUB_ENTITLEMENT_URL, token: process.env.AI_WORKSPACE_LAUNCHER_ENTITLEMENT_TOKEN || process.env.NDC_AI_WORKSPACE_LAUNCHER_ENTITLEMENT_TOKEN || process.env.AI_WORKSPACE_HUB_ENTITLEMENT_TOKEN || process.env.NDC_AI_WORKSPACE_HUB_ENTITLEMENT_TOKEN, required: process.env.AI_WORKSPACE_LAUNCHER_ENTITLEMENT_REQUIRED || process.env.NDC_AI_WORKSPACE_LAUNCHER_ENTITLEMENT_REQUIRED || process.env.AI_WORKSPACE_HUB_ENTITLEMENT_REQUIRED || process.env.NDC_AI_WORKSPACE_HUB_ENTITLEMENT_REQUIRED, }); const byAppId = new Map(); for (const adapter of adapters) { byAppId.set(adapter.appId, adapter); } const order = new Map([ ["ops", 10], ["engine", 20], ["launcher", 90], ]); return Array.from(byAppId.values()) .sort((left, right) => (order.get(left.appId) || 50) - (order.get(right.appId) || 50)); } function parseJsonEnv(value) { const text = optionalString(value); if (!text) return null; try { return JSON.parse(text); } catch { return null; } } function collectEntitlementAdapters(target, value) { if (Array.isArray(value)) { for (const item of value) collectEntitlementAdapter(target, "", item); return; } if (!isPlainObject(value)) return; for (const [appId, adapter] of Object.entries(value)) { collectEntitlementAdapter(target, appId, adapter); } } function collectEntitlementAdapter(target, defaultAppId, value) { const adapter = sanitizeEntitlementAdapter(value, defaultAppId); if (adapter) target.push(adapter); } function sanitizeEntitlementAdapter(value, defaultAppId = "") { if (!isPlainObject(value)) return null; const appId = canonicalRunAppId(value.appId || value.app_id || defaultAppId); const url = cleanHttpEndpoint(value.url || value.endpoint); if (!appId || !url) return null; const tokenEnv = optionalString(value.tokenEnv || value.token_env); const rawToken = optionalString(value.token || value.bearerToken || value.bearer_token) || (tokenEnv ? optionalString(process.env[tokenEnv]) : null) || optionalString(process.env.NODEDC_INTERNAL_ACCESS_TOKEN) || optionalString(process.env.NODEDC_PLATFORM_SERVICE_TOKEN) || ""; const authorization = optionalString(value.authorization || value.Authorization) || (rawToken ? rawToken.match(/^Bearer\s+/i) ? rawToken : `Bearer ${rawToken}` : ""); return { id: optionalString(value.id) || appId, appId, title: optionalString(value.title || value.appTitle || value.app_title), url, authorization, headers: sanitizeAdapterHeaders(value.headers), required: value.required === true || isTruthy(value.required), timeoutMs: sanitizeInteger(value.timeoutMs || value.timeout_ms, 5000, 500, 30000), }; } function sanitizeAdapterHeaders(value) { if (!isPlainObject(value)) return {}; const headers = {}; for (const [key, rawValue] of Object.entries(value)) { const headerName = optionalString(key); const headerValue = optionalString(rawValue); if (!headerName || !headerValue || /[\r\n\0]/.test(headerName) || /[\r\n\0]/.test(headerValue)) continue; if (/^authorization$/i.test(headerName)) continue; headers[headerName] = headerValue; } return headers; } function cleanHttpEndpoint(value) { const text = optionalString(value); if (!text) return ""; try { const url = new URL(text); if (url.protocol !== "http:" && url.protocol !== "https:") return ""; url.username = ""; url.password = ""; return url.toString().replace(/\/+$/, ""); } catch { return ""; } } function readConfig() { const nodedcEnv = normalizeKey(process.env.NODEDC_ENV || process.env.NDC_ENV || process.env.NODE_ENV || ""); const databaseUrl = process.env.DATABASE_URL || process.env.AI_WORKSPACE_ASSISTANT_DATABASE_URL || "postgres://nodedc_ai_workspace:nodedc_ai_workspace@localhost:5432/nodedc_ai_workspace"; const hubWebSocketUrl = optionalString(process.env.AI_WORKSPACE_HUB_PUBLIC_URL) || optionalString(process.env.NDC_AI_WORKSPACE_HUB_URL) || optionalString(process.env.AI_WORKSPACE_HUB_URL) || "wss://ai-hub.nodedc.ru/api/ai-workspace/hub"; const hubInternalHttpUrl = optionalString(process.env.AI_WORKSPACE_HUB_INTERNAL_URL) || optionalString(process.env.NDC_AI_WORKSPACE_HUB_HTTP_URL) || optionalString(process.env.AI_WORKSPACE_HUB_HTTP_URL) || httpUrlFromWebSocketUrl(hubWebSocketUrl); const setupGatewayUrl = optionalString(process.env.AI_WORKSPACE_SETUP_GATEWAY_URL) || optionalString(process.env.NDC_AI_WORKSPACE_SETUP_GATEWAY_URL) || httpUrlFromWebSocketUrl(hubWebSocketUrl); const explicitHubAccessToken = optionalString(process.env.AI_WORKSPACE_HUB_TOKEN) || optionalString(process.env.NDC_AI_WORKSPACE_HUB_TOKEN) || ""; const sharedInternalAccessToken = optionalString(process.env.NODEDC_INTERNAL_ACCESS_TOKEN) || ""; const defaultOntologyLauncherBaseUrl = process.env.NODE_ENV === 'production' ? 'http://launcher:5173' : 'http://launcher.local.nodedc' const ontologyLauncherBaseUrl = optionalString(process.env.NDC_ONTOLOGY_LAUNCHER_BASE_URL) || optionalString(process.env.NDC_LAUNCHER_BASE_URL) || optionalString(process.env.NDC_LAUNCHER_INTERNAL_URL) || optionalString(process.env.LAUNCHER_INTERNAL_URL) || optionalString(process.env.LAUNCHER_BASE_URL) || defaultOntologyLauncherBaseUrl; const launcherInternalAccessToken = optionalString(process.env.NDC_LAUNCHER_INTERNAL_ACCESS_TOKEN) || optionalString(process.env.LAUNCHER_INTERNAL_TOKEN) || sharedInternalAccessToken; const assistantActionRelayId = optionalString(process.env.AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ID) || optionalString(process.env.NDC_AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ID) || (nodedcEnv === "local" || nodedcEnv === "tunnel-local-e2e" ? "local-dev" : ""); const assistantActionRelayEnabledRaw = optionalString( process.env.AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ENABLED || process.env.NDC_AI_WORKSPACE_ASSISTANT_ACTION_RELAY_ENABLED ); const assistantActionRelayDefaultEnabled = Boolean( assistantActionRelayId && (nodedcEnv === "local" || nodedcEnv === "tunnel-local-e2e") && isDeployedPublicHubUrl(hubInternalHttpUrl) ); const assistantActionRelayEnabled = assistantActionRelayEnabledRaw ? isTruthy(assistantActionRelayEnabledRaw) && !isFalsy(assistantActionRelayEnabledRaw) : assistantActionRelayDefaultEnabled; const entitlementAdapters = parseEntitlementAdapters(); const opsEntitlementAdapter = entitlementAdapters.find((adapter) => adapter.appId === "ops") || null; const opsGatewayBaseUrl = optionalString(process.env.AI_WORKSPACE_OPS_GATEWAY_BASE_URL) || optionalString(process.env.NDC_AI_WORKSPACE_OPS_GATEWAY_BASE_URL) || ""; const normalizedSetupGatewayUrl = setupGatewayUrl.replace(/\/+$/, ""); const bridgePackageSpec = optionalString(process.env.AI_WORKSPACE_BRIDGE_PACKAGE_SPEC) || optionalString(process.env.NDC_AI_WORKSPACE_BRIDGE_PACKAGE_SPEC) || `${normalizedSetupGatewayUrl}${AI_WORKSPACE_BRIDGE_PACKAGE_PUBLIC_PATH}`; return { nodedcEnv, port: Number(process.env.PORT || process.env.AI_WORKSPACE_ASSISTANT_PORT || "18082"), databaseUrl, databasePoolSize: Number(process.env.AI_WORKSPACE_ASSISTANT_DATABASE_POOL_SIZE || "10"), hubWebSocketUrl, hubInternalHttpUrl: hubInternalHttpUrl.replace(/\/+$/, ""), setupGatewayUrl: normalizedSetupGatewayUrl, bridgePackageSpec, bridgePackageName: optionalString(process.env.AI_WORKSPACE_BRIDGE_PACKAGE_NAME) || optionalString(process.env.NDC_AI_WORKSPACE_BRIDGE_PACKAGE_NAME) || AI_WORKSPACE_BRIDGE_PACKAGE_NAME, minimumBridgeAgentVersion: optionalString(process.env.AI_WORKSPACE_MIN_BRIDGE_AGENT_VERSION) || optionalString(process.env.NDC_AI_WORKSPACE_MIN_BRIDGE_AGENT_VERSION) || "", requiredBridgeProtocolVersion: optionalString(process.env.AI_WORKSPACE_REQUIRED_BRIDGE_PROTOCOL_VERSION) || optionalString(process.env.NDC_AI_WORKSPACE_REQUIRED_BRIDGE_PROTOCOL_VERSION) || "", requiredBridgeCapabilities: uniqueStrings(String( process.env.AI_WORKSPACE_REQUIRED_BRIDGE_CAPABILITIES || process.env.NDC_AI_WORKSPACE_REQUIRED_BRIDGE_CAPABILITIES || "" ).split(/[\n,;]+/)), setupCodeTtlSeconds: sanitizeInteger( process.env.AI_WORKSPACE_SETUP_CODE_TTL_SECONDS || process.env.NDC_AI_WORKSPACE_SETUP_CODE_TTL_SECONDS, 15 * 60, 60, 24 * 60 * 60 ), hubInternalAccessToken: explicitHubAccessToken || (isDeployedPublicHubUrl(hubInternalHttpUrl) ? "" : sharedInternalAccessToken), ontologyLauncherBaseUrl: ontologyLauncherBaseUrl.replace(/\/+$/, ""), launcherInternalAccessToken, opsGatewayBaseUrl: opsGatewayBaseUrl.replace(/\/+$/, ""), opsEntitlementUrl: opsEntitlementAdapter?.url || "", opsEntitlementAuthorization: opsEntitlementAdapter?.authorization || "", defaultOpsWorkspaceSlug: optionalString(process.env.AI_WORKSPACE_OPS_DEFAULT_WORKSPACE_SLUG) || optionalString(process.env.NDC_AI_WORKSPACE_OPS_DEFAULT_WORKSPACE_SLUG) || "", defaultOpsProjectId: optionalString(process.env.AI_WORKSPACE_OPS_DEFAULT_PROJECT_ID) || optionalString(process.env.NDC_AI_WORKSPACE_OPS_DEFAULT_PROJECT_ID) || "", assistantActionRelayEnabled, assistantActionRelayId, hubFallbackWebSocketUrls: String( process.env.AI_WORKSPACE_HUB_FALLBACK_URLS || process.env.NDC_AI_WORKSPACE_HUB_FALLBACK_URLS || "" ) .split(/[\n,;]+/) .map((item) => item.trim()) .filter(Boolean), internalAccessTokens: uniqueStrings([ process.env.AI_WORKSPACE_ASSISTANT_TOKEN, process.env.NODEDC_INTERNAL_ACCESS_TOKEN, process.env.NODEDC_PLATFORM_SERVICE_TOKEN, ]), entitlementAdapters, }; } async function shutdown() { assistantActionRelayStopping = true; try { httpServer.close(); await pool.end(); } finally { process.exit(0); } }