feat(ai-workspace): add local relay profiles

This commit is contained in:
Codex
2026-06-20 12:54:19 +03:00
parent 2d5fef3948
commit 3526351b1b
28 changed files with 2959 additions and 77 deletions
+578 -6
View File
@@ -3,6 +3,8 @@ import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
import { readFile } from "node:fs/promises";
import { createServer } from "node:http";
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"]);
@@ -12,8 +14,25 @@ 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 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 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 });
@@ -47,6 +66,115 @@ app.patch("/api/ai-workspace/assistant/v1/settings", requireInternalApi, asyncRo
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([
@@ -397,6 +525,7 @@ 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);
@@ -1017,6 +1146,62 @@ async function dispatchExecutorMessage(executor, payload) {
};
}
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;
@@ -1447,6 +1632,7 @@ async function buildRunProfile({ owner, thread, executor, ownerSettings, bridgeP
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 requiredMcpServerNames = mcpServers
.filter((server) => server.required === true)
.map((server) => server.serverName)
@@ -1464,6 +1650,8 @@ async function buildRunProfile({ owner, thread, executor, ownerSettings, bridgeP
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)
: [],
@@ -1490,14 +1678,164 @@ async function buildRunProfile({ owner, thread, executor, ownerSettings, bridgeP
mcpServers,
mcpServerNames,
requiredMcpServerNames,
assistantActions,
},
policyPrompt: buildRunProfilePolicyPrompt({ context, diagnostics }),
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 : {};
@@ -1709,7 +2047,7 @@ function summarizeRunAppGrants(metadata) {
return out;
}
function buildRunProfilePolicyPrompt({ context, diagnostics }) {
function buildRunProfilePolicyPrompt({ context, diagnostics, assistantActions }) {
const lines = [
"AI Workspace dynamic run profile:",
`- source surface: ${diagnostics.sourceSurface}`,
@@ -1719,6 +2057,11 @@ function buildRunProfilePolicyPrompt({ context, diagnostics }) {
`- entitlement source: ${diagnostics.entitlementAdapters?.source || "settings"}`,
`- enabled tool packs: ${diagnostics.enabledToolPacks.length ? diagnostics.enabledToolPacks.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.",
];
const opsContext = isPlainObject(context?.contexts?.ops) ? context.contexts.ops : {};
@@ -1754,19 +2097,29 @@ function runProfileHash(runProfile) {
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: {
...(isPlainObject(runProfile.toolProfile) ? runProfile.toolProfile : {}),
mcpServers: Array.isArray(runProfile.toolProfile?.mcpServers)
? runProfile.toolProfile.mcpServers.map(redactMcpServer)
...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: "<redacted>" } : {}),
};
}
function redactMcpServer(server) {
if (!isPlainObject(server)) return {};
const headers = isPlainObject(server.httpHeaders) ? server.httpHeaders : {};
@@ -2520,12 +2873,62 @@ function sanitizeOwnerSettingsCommand(payload) {
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 {
@@ -2573,6 +2976,111 @@ function getRequestOwner(req) {
};
}
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,
@@ -2583,6 +3091,12 @@ function publicOwner(owner) {
};
}
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 {
@@ -2874,6 +3388,11 @@ function isTruthy(value) {
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;
@@ -3051,6 +3570,7 @@ function cleanHttpEndpoint(value) {
}
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 ||
@@ -3070,8 +3590,44 @@ function readConfig() {
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) ||
"";
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"),
@@ -3079,6 +3635,21 @@ function readConfig() {
hubInternalHttpUrl: hubInternalHttpUrl.replace(/\/+$/, ""),
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 ||
@@ -3092,11 +3663,12 @@ function readConfig() {
process.env.NODEDC_INTERNAL_ACCESS_TOKEN,
process.env.NODEDC_PLATFORM_SERVICE_TOKEN,
]),
entitlementAdapters: parseEntitlementAdapters(),
entitlementAdapters,
};
}
async function shutdown() {
assistantActionRelayStopping = true;
try {
httpServer.close();
await pool.end();