openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
384 lines (383 loc) • 18.5 kB
JavaScript
import { u as normalizeSortedUniqueTrimmedStringList } from "./string-normalization-DsCfAx8q.js";
import { c as WRITE_SCOPE } from "./operator-scopes-Dw7Gu2cA.js";
import { t as ErrorCodes } from "./gateway-error-details-w0nAGBBp.js";
import { Ft as validateEnvironmentsStatusParams, Mt as validateEnvironmentsCreateParams, Nt as validateEnvironmentsDestroyParams, Pt as validateEnvironmentsListParams, _s as validateWorkerDesktopObserveParams, bt as validateDesktopLaunchParams, hs as validateWorkerDesktopLaunchParams, xt as validateDesktopObserveParams } from "./src-BiL5aQto.js";
import { r as authorizeOperatorScopesForRequiredScope } from "./method-scopes-K6J_UQGL.js";
import { d as errorShape } from "./error-codes-Bo8q2D1o.js";
import { n as NODE_DESKTOP_STREAM_COMMAND } from "./node-desktop-stream-BZM2AiRA.js";
import { d as resolveRequiredNodeCommandAuthority, l as resolveNodeCommandAllowlist, o as isNodeCommandAllowed } from "./node-command-policy-H4FpS0aA.js";
import { t as resolveWorkerPlacementCapabilities } from "./placement-capabilities-1KvP5S1_.js";
import { c as listDevicePairing } from "./device-pairing-DFIQp3ZY.js";
import { a as listNodePairing } from "./device-pairing-node-DiTk_P80.js";
import { t as collectNodeCatalogRuntimeState } from "./node-registry-private-CPqGMumT.js";
import { n as readNodeSessionWithheldCommands } from "./node-registry-DspP5BRY.js";
import { t as formatForLog } from "./ws-log-Dkg9wKNU.js";
import { r as respondUnavailableOnThrow } from "./nodes.helpers-7uACMTmw.js";
import { t as assertValidParams } from "./validation-pzrlzFvo.js";
import { r as isDesktopCredentialsRequiredError } from "./host-source-errors-46uOYNUn.js";
import { o as projectPairedDeviceNodeBindings } from "./device-pairing-node-state-DxNt2jjv.js";
import { n as getNodeDesktopService } from "./node-source-context-Csxf7qYw.js";
import { r as listKnownNodes, t as createKnownNodeCatalog } from "./node-catalog-BbldLLoP.js";
//#region src/gateway/server-methods/environments.ts
const GATEWAY_ENVIRONMENT = {
id: "gateway",
type: "local",
label: "Gateway local",
status: "available",
platform: process.platform,
sessionHost: true,
trust: "persistent",
capabilities: [
"agent.run",
"sessions",
"tools",
"workspace"
]
};
const WORKER_STATUS = {
requested: "starting",
provisioning: "starting",
bootstrapping: "starting",
ready: "available",
attached: "available",
idle: "available",
draining: "stopping",
destroying: "stopping",
destroyed: "unavailable",
failed: "error",
orphaned: "error"
};
function uniqueSortedStrings(...items) {
return normalizeSortedUniqueTrimmedStringList(items.flatMap((item) => item ?? []));
}
function summarizeNodeEnvironment(node, config, requiredCommands, liveNode) {
const capabilities = uniqueSortedStrings(node.caps, node.commands);
const platform = node.platform?.trim();
const allowlist = node.connected === true ? resolveNodeCommandAllowlist(config, {
platform: node.platform,
deviceFamily: node.deviceFamily,
commands: node.commands,
approvedCommands: node.commands
}) : void 0;
const invocableCommands = allowlist ? uniqueSortedStrings(node.commands).filter((command) => command.length <= 128 && isNodeCommandAllowed({
command,
declaredCommands: node.commands,
allowlist
}).ok).slice(0, 128) : [];
const desktop = invocableCommands.includes(NODE_DESKTOP_STREAM_COMMAND);
const requiredNodeCommand = allowlist && liveNode ? resolveRequiredNodeCommandAuthority({
requiredCommands,
declaredCommands: liveNode.declaredCommands,
effectiveCommands: liveNode.commands,
withheldCommands: readNodeSessionWithheldCommands(liveNode),
allowlist
}) : void 0;
return {
id: `node:${node.nodeId}`,
type: "node",
label: node.displayName ?? node.nodeId,
status: node.connected ? "available" : "unavailable",
...platform ? { platform } : {},
sessionHost: node.sessionHost === true,
...node.workerSlots ? { workerSlots: { ...node.workerSlots } } : {},
...node.workerBundle ? { workerBundle: structuredClone(node.workerBundle) } : {},
...node.lastConnectedAtMs !== void 0 ? { lastConnectedAtMs: node.lastConnectedAtMs } : {},
...node.lastDisconnectedAtMs !== void 0 ? { lastDisconnectedAtMs: node.lastDisconnectedAtMs } : {},
...node.lastSeenAtMs !== void 0 ? { lastSeenAtMs: node.lastSeenAtMs } : {},
...node.lastSeenReason ? { lastSeenReason: node.lastSeenReason } : {},
trust: "persistent",
...desktop ? { desktop: true } : {},
...capabilities.length > 0 ? { capabilities } : {},
...invocableCommands.length > 0 ? { invocableCommands } : {},
...requiredNodeCommand ? { requiredNodeCommand } : {},
...node.issues?.length ? { issues: [...node.issues] } : {}
};
}
/** Projects a durable worker row without exposing its SSH credential reference. */
function summarizeWorkerEnvironment(record, now = Date.now()) {
return {
id: record.environmentId,
type: "worker",
status: WORKER_STATUS[record.state],
...record.sharedHost === null ? {} : { trust: record.sharedHost ? "persistent" : "disposable" },
...record.desktopAvailable ? { desktop: true } : {},
worker: {
providerId: record.providerId,
...record.leaseId ? { leaseId: record.leaseId } : {},
state: record.state,
ageMs: Math.max(0, Math.trunc(now - record.createdAtMs)),
...record.state === "idle" && record.idleSinceAtMs !== null ? { idleMs: Math.max(0, Math.trunc(now - record.idleSinceAtMs)) } : {},
attachedSessionIds: uniqueSortedStrings(record.attachedSessionIds),
tunnelStatus: record.tunnelStatus,
...(record.state === "failed" || record.state === "orphaned") && record.error ? { error: record.error } : {},
...record.desktopAvailable ? { desktop: true } : {},
...record.desktopApps.length > 0 ? { desktopApps: [...record.desktopApps] } : {}
}
};
}
async function listGatewayEnvironments(context, workers = listWorkerEnvironments(context), runtimeId) {
const [devices, nodes] = await Promise.all([listDevicePairing(), listNodePairing()]);
const managedCloudNodeIds = new Set(workers.flatMap((environment) => environment.providerId !== "device" && environment.nodeDeviceId && environment.state !== "destroyed" ? [environment.nodeDeviceId] : []));
const visibleDevices = devices.paired.filter((device) => !managedCloudNodeIds.has(device.deviceId));
const connectedNodes = context.nodeRegistry.listConnectedForPairingStates(projectPairedDeviceNodeBindings(visibleDevices));
const runtimeState = collectNodeCatalogRuntimeState(context.nodeRegistry, connectedNodes);
const connectedNodesById = new Map(connectedNodes.map((node) => [node.nodeId, node]));
const requiredCommands = runtimeId ? resolveWorkerPlacementCapabilities(runtimeId).devicePlacement?.requiredNodeCommands ?? [] : [];
const catalog = createKnownNodeCatalog({
pairedDevices: visibleDevices,
pairedNodes: nodes.paired.filter((node) => !managedCloudNodeIds.has(node.nodeId)),
connectedNodes: connectedNodes.filter((node) => !managedCloudNodeIds.has(node.nodeId)),
...runtimeState
});
const config = context.getRuntimeConfig();
return [config.desktop?.host?.enabled === true ? {
...GATEWAY_ENVIRONMENT,
desktop: true
} : GATEWAY_ENVIRONMENT, ...listKnownNodes(catalog).map((node) => summarizeNodeEnvironment(node, config, requiredCommands, connectedNodesById.get(node.nodeId)))];
}
function listWorkerEnvironments(context) {
try {
return context.workerEnvironmentService?.list() ?? [];
} catch {
throw new Error("environment inventory unavailable");
}
}
function listWorkerProfiles(context) {
if (!context.workerEnvironmentService || !context.workerPlacementDispatchService) return [];
const profiles = context.getRuntimeConfig().cloudWorkers?.profiles ?? {};
return Object.entries(profiles).flatMap(([id, profile]) => {
const providerId = typeof profile.provider === "string" ? profile.provider.trim() : "";
return id.trim() && providerId ? [{
id: id.trim(),
providerId
}] : [];
}).toSorted((left, right) => left.id.localeCompare(right.id));
}
async function listWorkerProfilesWithMachines(context) {
const summaries = listWorkerProfiles(context);
return await Promise.all(summaries.map(async (summary) => {
const executionModes = ["worker-turn", "remote-exec"].filter((mode) => context.workerEnvironmentService?.supportsExecutionMode(summary.id, mode) === true);
const executionMode = executionModes[0];
const resolvedSummary = Object.assign(summary, executionMode ? {
executionMode,
executionModes
} : {});
try {
const machines = await context.workerEnvironmentService?.listMachineOptions?.(summary.id) ?? [];
return machines.length > 0 ? Object.assign(resolvedSummary, { machines }) : resolvedSummary;
} catch (error) {
context.logGateway.warn(`worker machine catalog unavailable (${summary.id}): ${formatForLog(error)}`);
return resolvedSummary;
}
}));
}
async function respondWorkerMutation(respond, run, invalidCodes, unavailableMessage) {
try {
respond(true, summarizeWorkerEnvironment(await run()), void 0);
} catch (error) {
const code = error && typeof error === "object" && "code" in error ? error.code : void 0;
const invalid = typeof code === "string" && invalidCodes.includes(code);
const message = invalid && error instanceof Error ? error.message : unavailableMessage;
respond(false, void 0, errorShape(invalid ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE, message));
}
}
async function respondDesktopObserve(params) {
if (params.request.source.kind === "host") {
if (params.context.getRuntimeConfig().desktop?.host?.enabled !== true) {
params.respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "gateway host desktop is disabled; enable the Desktop lab (config: desktop.host.enabled=true), then restart the gateway"));
return;
}
if (!params.context.hostDesktopService) {
params.respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "gateway host desktop is not active; desktop.host.enabled changes require a gateway restart"));
return;
}
try {
params.respond(true, await params.context.hostDesktopService.observe({
control: params.request.control ?? false,
..."credentials" in params.request && params.request.credentials ? { credentials: params.request.credentials } : {}
}), void 0);
} catch (error) {
if (isDesktopCredentialsRequiredError(error)) {
params.respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, error.message, { details: {
code: error.detailCode,
auth: error.auth
} }));
return;
}
params.respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, error instanceof Error ? error.message : "gateway host desktop observe unavailable; verify the VNC server and retry"));
}
return;
}
if (params.request.source.kind === "node") {
const service = getNodeDesktopService(params.context);
if (!service) {
params.respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "node desktop is disabled; explicitly allow desktop.stream, then restart the gateway"));
return;
}
try {
params.respond(true, await service.observe({
nodeId: params.request.source.nodeId,
control: params.request.control ?? false,
..."credentials" in params.request && params.request.credentials ? { credentials: params.request.credentials } : {}
}), void 0);
} catch (error) {
if (isDesktopCredentialsRequiredError(error)) {
params.respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, error.message, { details: {
code: error.detailCode,
auth: error.auth
} }));
return;
}
params.respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, error instanceof Error ? error.message : "node desktop observe unavailable"));
}
return;
}
const service = params.context.workerEnvironmentService;
if (!service) {
params.respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "unknown environmentId"));
return;
}
try {
const result = await service.observeDesktop({
environmentId: params.request.source.environmentId,
control: params.request.control ?? false
});
params.respond(true, result, void 0);
} catch (error) {
const code = error && typeof error === "object" && "code" in error ? error.code : void 0;
const invalid = code === "environment_not_found" || code === "invalid_state";
params.respond(false, void 0, errorShape(invalid ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE, invalid && error instanceof Error ? error.message : "worker desktop observe unavailable"));
}
}
async function respondDesktopLaunch(params) {
const service = params.context.workerEnvironmentService;
if (!service) {
params.respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "unknown environmentId"));
return;
}
try {
params.respond(true, await service.launchDesktopApp({
environmentId: params.environmentId,
app: params.app
}), void 0);
} catch (error) {
const code = error && typeof error === "object" && "code" in error ? error.code : void 0;
const invalid = code === "environment_not_found" || code === "invalid_state" || code === "desktop_app_not_found" || code === "unsupported_platform";
const actionable = invalid || code === "launcher_failure";
params.respond(false, void 0, errorShape(invalid ? ErrorCodes.INVALID_REQUEST : ErrorCodes.UNAVAILABLE, actionable && error instanceof Error ? error.message : "worker desktop app launch unavailable; try again"));
}
}
const environmentsHandlers = {
"environments.list": async ({ params, respond, client, context }) => {
if (!assertValidParams(params, validateEnvironmentsListParams, "environments.list", respond)) return;
if (params.runtimeId) {
const scopes = Array.isArray(client?.connect.scopes) ? client.connect.scopes : [];
const access = authorizeOperatorScopesForRequiredScope(WRITE_SCOPE, scopes);
if (!access.allowed) {
respond(false, void 0, errorShape(ErrorCodes.FORBIDDEN, `missing scope: ${access.missingScope}`));
return;
}
}
await respondUnavailableOnThrow(respond, async () => {
const workers = listWorkerEnvironments(context);
const environments = await listGatewayEnvironments(context, workers, params.runtimeId);
const summarizedAtMs = Date.now();
environments.push(...workers.map((record) => summarizeWorkerEnvironment(record, summarizedAtMs)));
const profiles = await listWorkerProfilesWithMachines(context);
respond(true, {
environments,
...profiles.length > 0 ? { profiles } : {}
}, void 0);
});
},
"environments.status": async ({ params, respond, context }) => {
if (!assertValidParams(params, validateEnvironmentsStatusParams, "environments.status", respond)) return;
await respondUnavailableOnThrow(respond, async () => {
const environment = (await listGatewayEnvironments(context)).find((entry) => entry.id === params.environmentId);
if (environment) {
respond(true, environment, void 0);
return;
}
let worker;
try {
worker = context.workerEnvironmentService?.get(params.environmentId);
} catch {
respond(false, void 0, errorShape(ErrorCodes.UNAVAILABLE, "environment status unavailable"));
return;
}
respond(Boolean(worker), worker ? summarizeWorkerEnvironment(worker) : void 0, worker ? void 0 : errorShape(ErrorCodes.INVALID_REQUEST, "unknown environmentId"));
});
},
"environments.create": async ({ params, respond, context }) => {
if (!assertValidParams(params, validateEnvironmentsCreateParams, "environments.create", respond)) return;
const service = context.workerEnvironmentService;
if (!service) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "cloud worker environments are not configured"));
return;
}
await respondWorkerMutation(respond, () => service.create(params.profileId, params.idempotencyKey), ["profile_not_found", "invalid_profile"], "worker environment creation failed");
},
"environments.destroy": async ({ params, respond, context }) => {
if (!assertValidParams(params, validateEnvironmentsDestroyParams, "environments.destroy", respond)) return;
const service = context.workerEnvironmentService;
if (!service) {
respond(false, void 0, errorShape(ErrorCodes.INVALID_REQUEST, "unknown environmentId"));
return;
}
await respondWorkerMutation(respond, async () => {
const placementService = context.workerPlacementDispatchService;
if (params.force && !placementService?.forceDestroyEnvironment) throw new Error("cloud worker placement control is unavailable");
const destroyed = params.force ? await placementService.forceDestroyEnvironment(params.environmentId, (error) => {
context.logGateway.warn(`worker environment forced teardown cleanup failed: ${formatForLog(error)}`);
}) : await service.destroyUnattached(params.environmentId);
try {
await context.workerPlacementDispatchService?.reconcileActive?.(params.environmentId);
} catch (error) {
context.logGateway.warn(`worker placement reconciliation after destroy failed: ${formatForLog(error)}`);
}
return destroyed;
}, ["environment_not_found", "invalid_state"], "worker environment destruction failed");
},
"worker.desktop.observe": async ({ params, respond, context }) => {
if (!assertValidParams(params, validateWorkerDesktopObserveParams, "worker.desktop.observe", respond)) return;
await respondDesktopObserve({
request: {
source: {
kind: "environment",
environmentId: params.environmentId
},
...params.control === void 0 ? {} : { control: params.control }
},
respond,
context
});
},
"worker.desktop.launch": async ({ params, respond, context }) => {
if (!assertValidParams(params, validateWorkerDesktopLaunchParams, "worker.desktop.launch", respond)) return;
await respondDesktopLaunch({
environmentId: params.environmentId,
app: params.app,
respond,
context
});
},
"desktop.observe": async ({ params, respond, context }) => {
if (!assertValidParams(params, validateDesktopObserveParams, "desktop.observe", respond)) return;
await respondDesktopObserve({
request: params,
respond,
context
});
},
"desktop.launch": async ({ params, respond, context }) => {
if (!assertValidParams(params, validateDesktopLaunchParams, "desktop.launch", respond)) return;
await respondDesktopLaunch({
environmentId: params.source.environmentId,
app: params.app,
respond,
context
});
}
};
//#endregion
export { summarizeWorkerEnvironment as i, listGatewayEnvironments as n, listWorkerProfiles as r, environmentsHandlers as t };