docs: document gateway utility helpers

This commit is contained in:
Peter Steinberger
2026-06-03 18:48:23 -04:00
parent 0771a8ab6f
commit 59366ca420
10 changed files with 46 additions and 0 deletions

View File

@@ -1,9 +1,11 @@
// Gateway connection detail builder for CLI/user-facing target diagnostics.
import { redactSensitiveUrlLikeString } from "@openclaw/net-policy/redact-sensitive-url";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { resolveConfigPath, resolveGatewayPort } from "../config/paths.js";
import type { OpenClawConfig } from "../config/types.js";
import { isSecureWebSocketUrl } from "./net.js";
/** Resolved gateway target plus redacted display text for diagnostics. */
export type GatewayConnectionDetails = {
url: string;
urlSource: string;
@@ -18,6 +20,7 @@ type GatewayConnectionDetailResolvers = {
resolveGatewayPort?: (cfg?: OpenClawConfig, env?: NodeJS.ProcessEnv) => number;
};
/** Build gateway target details and reject unsafe remote plaintext websocket URLs. */
export function buildGatewayConnectionDetailsWithResolvers(
options: {
config?: OpenClawConfig;

View File

@@ -1,3 +1,4 @@
// Process-local MCP loopback runtime state for owner/non-owner HTTP access.
type McpLoopbackRuntime = {
port: number;
ownerToken: string;
@@ -6,14 +7,17 @@ type McpLoopbackRuntime = {
let activeRuntime: McpLoopbackRuntime | undefined;
/** Return a copy of the active loopback runtime, if one has been installed. */
export function getActiveMcpLoopbackRuntime(): McpLoopbackRuntime | undefined {
return activeRuntime ? { ...activeRuntime } : undefined;
}
/** Install the active loopback runtime used by in-process MCP callers. */
export function setActiveMcpLoopbackRuntime(runtime: McpLoopbackRuntime): void {
activeRuntime = { ...runtime };
}
/** Choose the bearer token matching owner/non-owner caller identity. */
export function resolveMcpLoopbackBearerToken(
runtime: McpLoopbackRuntime,
senderIsOwner: boolean,
@@ -21,12 +25,14 @@ export function resolveMcpLoopbackBearerToken(
return senderIsOwner ? runtime.ownerToken : runtime.nonOwnerToken;
}
/** Clear loopback runtime only when the owning token matches the active runtime. */
export function clearActiveMcpLoopbackRuntimeByOwnerToken(ownerToken: string): void {
if (activeRuntime?.ownerToken === ownerToken) {
activeRuntime = undefined;
}
}
/** Build the MCP server config injected into agents for loopback tool access. */
export function createMcpLoopbackServerConfig(port: number) {
return {
mcpServers: {

View File

@@ -1,3 +1,4 @@
// OpenAI-compatible `/v1/models` HTTP route backed by configured OpenClaw agents.
import type { IncomingMessage, ServerResponse } from "node:http";
import { listAgentIds, resolveDefaultAgentId } from "../agents/agent-scope.js";
import { getRuntimeConfig } from "../config/io.js";
@@ -74,6 +75,7 @@ function resolveRequestPath(req: IncomingMessage): string {
return new URL(req.url ?? "/", "http://localhost").pathname;
}
/** Handle OpenAI-compatible model list/detail requests, returning false for unrelated paths. */
export async function handleOpenAiModelsHttpRequest(
req: IncomingMessage,
res: ServerResponse,

View File

@@ -1,3 +1,4 @@
// Capability-token helpers for plugin-hosted node surfaces.
import { randomBytes } from "node:crypto";
import {
asDateTimestampMs,
@@ -7,22 +8,27 @@ import {
} from "@openclaw/normalization-core/number-coercion";
import { safeEqualSecret } from "../security/secret-equal.js";
/** Path marker used to scope plugin-hosted node URLs with one-time capabilities. */
export const PLUGIN_NODE_CAPABILITY_PATH_PREFIX = "/__openclaw__/cap";
const PLUGIN_NODE_CAPABILITY_QUERY_PARAM = "oc_cap";
/** Default lifetime for plugin-node capability tokens. */
export const DEFAULT_PLUGIN_NODE_CAPABILITY_TTL_MS = 10 * 60_000;
/** Declared plugin surface that may receive scoped node capabilities. */
export type PluginNodeCapabilitySurface = {
surface: string;
ttlMs?: number;
scopeKey?: string;
};
/** Client-side storage for surface URLs and minted plugin-node capabilities. */
export type PluginNodeCapabilityClient = {
pluginSurfaceUrls?: Record<string, string>;
pluginNodeCapabilitySurfaces?: Record<string, PluginNodeCapabilitySurface>;
pluginNodeCapabilities?: Record<string, { capability: string; expiresAtMs: number }>;
};
/** Index surfaces by normalized surface id, keeping the strictest TTL per surface. */
export function indexPluginNodeCapabilitySurfaces(
surfaces: readonly PluginNodeCapabilitySurface[],
): Record<string, PluginNodeCapabilitySurface> {
@@ -44,6 +50,7 @@ export function indexPluginNodeCapabilitySurfaces(
return indexed;
}
/** Parsed URL details after extracting path/query capability tokens. */
export type NormalizedPluginNodeCapabilityUrl = {
pathname: string;
capability?: string;
@@ -71,10 +78,12 @@ function resolvePluginNodeCapabilityStorageKey(surface: PluginNodeCapabilitySurf
return scopeKey ? `${normalizedSurface}\0${scopeKey}` : normalizedSurface;
}
/** Resolve a positive TTL for a plugin-node capability surface. */
export function resolvePluginNodeCapabilityTtlMs(surface: PluginNodeCapabilitySurface) {
return asPositiveSafeInteger(surface.ttlMs) ?? DEFAULT_PLUGIN_NODE_CAPABILITY_TTL_MS;
}
/** Resolve the expiration timestamp for a capability minted against a surface. */
export function resolvePluginNodeCapabilityExpiresAtMs(
surface: PluginNodeCapabilitySurface,
nowMs: number = Date.now(),
@@ -82,10 +91,12 @@ export function resolvePluginNodeCapabilityExpiresAtMs(
return resolveExpiresAtMsFromDurationMs(resolvePluginNodeCapabilityTtlMs(surface), { nowMs });
}
/** Mint an opaque capability token for plugin-node surface access. */
export function mintPluginNodeCapabilityToken(): string {
return randomBytes(18).toString("base64url");
}
/** Append a capability path segment to a plugin host URL. */
export function buildPluginNodeCapabilityScopedHostUrl(
baseUrl: string,
capability: string,
@@ -107,6 +118,7 @@ export function buildPluginNodeCapabilityScopedHostUrl(
}
}
/** Replace the capability segment in an already scoped host URL. */
export function replacePluginNodeCapabilityInScopedHostUrl(
scopedUrl: string,
capability: string,
@@ -140,6 +152,7 @@ export function replacePluginNodeCapabilityInScopedHostUrl(
}
}
/** Parse and rewrite scoped capability URLs into canonical paths plus query tokens. */
export function normalizePluginNodeCapabilityScopedUrl(
rawUrl: string,
): NormalizedPluginNodeCapabilityUrl {
@@ -199,6 +212,7 @@ export function normalizePluginNodeCapabilityScopedUrl(
};
}
/** Store a minted capability on a client under the surface/scope storage key. */
export function setClientPluginNodeCapability(params: {
client: PluginNodeCapabilityClient;
surface: PluginNodeCapabilitySurface;

View File

@@ -1,3 +1,4 @@
// Gateway event subscription wiring for agent, heartbeat, transcript, and lifecycle broadcasts.
import { clearAgentRunContext, onAgentEvent } from "../infra/agent-events.js";
import { onHeartbeatEvent } from "../infra/heartbeat-events.js";
import { onSessionLifecycleEvent } from "../sessions/session-lifecycle-events.js";
@@ -10,6 +11,7 @@ import type {
ToolEventRecipientRegistry,
} from "./server-chat-state.js";
/** Register gateway runtime event subscriptions and return unsubscribe handles. */
export function startGatewayEventSubscriptions(params: {
broadcast: (event: string, payload: unknown, opts?: { dropIfSlow?: boolean }) => void;
broadcastToConnIds: (
@@ -30,6 +32,7 @@ export function startGatewayEventSubscriptions(params: {
ReturnType<typeof import("./server-chat.js").createAgentEventHandler>
> | null = null;
const getAgentEventHandler = () => {
// Lazy-load heavy chat modules only after the first agent event reaches the gateway.
agentEventHandlerPromise ??= Promise.all([
import("./server-chat.js"),
import("./server-session-key.js"),

View File

@@ -1,3 +1,4 @@
// Gateway HTTP server listen helper with retry and lock-aware errors.
import type { Server as HttpServer } from "node:http";
import { GatewayLockError } from "../../infra/gateway-lock.js";
import { sleep } from "../../utils.js";
@@ -15,6 +16,7 @@ async function closeServerQuietly(httpServer: HttpServer): Promise<void> {
});
}
/** Listen on the configured gateway host/port, retrying transient EADDRINUSE windows. */
export async function listenGatewayHttpServer(params: {
httpServer: HttpServer;
bindHost: string;

View File

@@ -1,3 +1,4 @@
// Gateway readiness checker for channel health and startup sidecar state.
import type { ChannelAccountSnapshot } from "../../channels/plugins/types.public.js";
import {
DEFAULT_CHANNEL_CONNECT_GRACE_MS,
@@ -9,6 +10,7 @@ import {
import type { ChannelManager } from "../server-channels.js";
import type { GatewayEventLoopHealth } from "./event-loop-health.js";
/** Snapshot returned by the gateway readiness probe. */
export type ReadinessResult = {
ready: boolean;
failing: string[];
@@ -16,6 +18,7 @@ export type ReadinessResult = {
eventLoop?: GatewayEventLoopHealth;
};
/** Function form used by HTTP readiness endpoints and tests. */
export type ReadinessChecker = () => ReadinessResult;
const DEFAULT_READINESS_CACHE_TTL_MS = 1_000;
@@ -33,6 +36,7 @@ function shouldIgnoreReadinessFailure(
return health.reason === "not-running" && accountSnapshot.restartPending === true;
}
/** Create a cached readiness checker over channel runtime health. */
export function createReadinessChecker(deps: {
channelManager: ChannelManager;
startedAt: number;

View File

@@ -1,3 +1,4 @@
// Session-store key canonicalization across default agents, main aliases, and legacy keys.
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
@@ -17,6 +18,7 @@ import {
} from "../routing/session-key.js";
import { normalizeSessionKeyPreservingOpaquePeerIds } from "../sessions/session-key-utils.js";
/** Canonicalize an opaque session key into the agent-scoped store namespace. */
export function canonicalizeSessionKeyForAgent(agentId: string, key: string): string {
const lowered = normalizeLowercaseStringOrEmpty(key);
if (lowered === "global" || lowered === "unknown") {
@@ -68,6 +70,7 @@ function resolveParsedSessionStoreKey(
return { agentId, sessionKey: `agent:${agentId}:${rest}` };
}
/** Resolve any incoming session key into the canonical key used in persisted session stores. */
export function resolveSessionStoreKey(params: {
cfg: OpenClawConfig;
sessionKey: string;
@@ -107,6 +110,7 @@ export function resolveSessionStoreKey(params: {
return canonicalizeSessionKeyForAgent(agentId, raw);
}
/** Resolve the agent that owns a canonical session-store key. */
export function resolveSessionStoreAgentId(cfg: OpenClawConfig, canonicalKey: string): string {
if (canonicalKey === "global" || canonicalKey === "unknown") {
return resolveDefaultStoreAgentId(cfg);
@@ -118,6 +122,7 @@ export function resolveSessionStoreAgentId(cfg: OpenClawConfig, canonicalKey: st
return resolveDefaultStoreAgentId(cfg);
}
/** Resolve a session key for lookup inside a specific agent's store. */
export function resolveStoredSessionKeyForAgentStore(params: {
cfg: OpenClawConfig;
agentId: string;
@@ -139,6 +144,7 @@ export function resolveStoredSessionKeyForAgentStore(params: {
});
}
/** Resolve the owner agent for a stored session key, returning null for global/unknown keys. */
export function resolveStoredSessionOwnerAgentId(params: {
cfg: OpenClawConfig;
agentId: string;
@@ -151,6 +157,7 @@ export function resolveStoredSessionOwnerAgentId(params: {
return resolveSessionStoreAgentId(params.cfg, canonicalKey);
}
/** Canonicalize spawned-by parent references while preserving main-session aliases. */
export function canonicalizeSpawnedByForAgent(
cfg: OpenClawConfig,
agentId: string,

View File

@@ -1,3 +1,4 @@
// Session patch applier for gateway session metadata and model/runtime overrides.
import { randomUUID } from "node:crypto";
import {
normalizeOptionalLowercaseString,
@@ -129,6 +130,7 @@ function normalizeSubagentControlScope(raw: string): "children" | "none" | undef
return undefined;
}
/** Apply a validated gateway session patch to an in-memory session store entry. */
export async function applySessionsPatchToStore(params: {
cfg: OpenClawConfig;
store: Record<string, SessionEntry>;
@@ -161,6 +163,7 @@ export async function applySessionsPatchToStore(params: {
};
const existing = store[storeKey];
// Existing entries without session ids are placeholder aliases; assigning an id makes them real.
const next: SessionEntry = existing?.sessionId
? {
...existing,

View File

@@ -1,3 +1,4 @@
// HTTP endpoint adapter for invoking gateway tools from OpenAI-compatible clients.
import type { IncomingMessage, ServerResponse } from "node:http";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { normalizeMessageChannel } from "../utils/message-channel.js";
@@ -14,6 +15,7 @@ import { invokeGatewayTool, type ToolsInvokeInput } from "./tools-invoke-shared.
const DEFAULT_BODY_BYTES = 2 * 1024 * 1024;
/** Handle `/tools/invoke` requests and return false when another HTTP route should handle them. */
export async function handleToolsInvokeHttpRequest(
req: IncomingMessage,
res: ServerResponse,