Files
openclaw/src/plugins/runtime/index.ts
2026-03-16 22:58:55 -07:00

195 lines
6.5 KiB
TypeScript

import { createRequire } from "node:module";
import {
getApiKeyForModel as getApiKeyForModelRaw,
resolveApiKeyForProvider as resolveApiKeyForProviderRaw,
} from "../../agents/model-auth.js";
import { resolveStateDir } from "../../config/paths.js";
import {
generateImage,
listRuntimeImageGenerationProviders,
} from "../../image-generation/runtime.js";
import {
describeImageFile,
describeImageFileWithModel,
describeVideoFile,
runMediaUnderstandingFile,
transcribeAudioFile,
} from "../../media-understanding/runtime.js";
import { listSpeechVoices, textToSpeech, textToSpeechTelephony } from "../../tts/runtime.js";
import { listWebSearchProviders, runWebSearch } from "../../web-search/runtime.js";
import { createRuntimeAgent } from "./runtime-agent.js";
import { createRuntimeChannel } from "./runtime-channel.js";
import { createRuntimeConfig } from "./runtime-config.js";
import { createRuntimeEvents } from "./runtime-events.js";
import { createRuntimeLogging } from "./runtime-logging.js";
import { createRuntimeMedia } from "./runtime-media.js";
import { createRuntimeSystem } from "./runtime-system.js";
import { createRuntimeTools } from "./runtime-tools.js";
import type { PluginRuntime } from "./types.js";
let cachedVersion: string | null = null;
function resolveVersion(): string {
if (cachedVersion) {
return cachedVersion;
}
try {
const require = createRequire(import.meta.url);
const pkg = require("../../../package.json") as { version?: string };
cachedVersion = pkg.version ?? "unknown";
return cachedVersion;
} catch {
cachedVersion = "unknown";
return cachedVersion;
}
}
function createUnavailableSubagentRuntime(): PluginRuntime["subagent"] {
const unavailable = () => {
throw new Error("Plugin runtime subagent methods are only available during a gateway request.");
};
return {
run: unavailable,
waitForRun: unavailable,
getSessionMessages: unavailable,
getSession: unavailable,
deleteSession: unavailable,
};
}
// ── Process-global gateway subagent runtime ─────────────────────────
// The gateway creates a real subagent runtime during startup, but gateway-owned
// plugin registries may be loaded (and cached) before the gateway path runs.
// A process-global holder lets explicitly gateway-bindable runtimes resolve the
// active gateway subagent dynamically without changing the default behavior for
// ordinary plugin runtimes.
const GATEWAY_SUBAGENT_SYMBOL: unique symbol = Symbol.for(
"openclaw.plugin.gatewaySubagentRuntime",
) as unknown as typeof GATEWAY_SUBAGENT_SYMBOL;
type GatewaySubagentState = {
subagent: PluginRuntime["subagent"] | undefined;
};
const gatewaySubagentState: GatewaySubagentState = (() => {
const g = globalThis as typeof globalThis & {
[GATEWAY_SUBAGENT_SYMBOL]?: GatewaySubagentState;
};
const existing = g[GATEWAY_SUBAGENT_SYMBOL];
if (existing) {
return existing;
}
const created: GatewaySubagentState = { subagent: undefined };
g[GATEWAY_SUBAGENT_SYMBOL] = created;
return created;
})();
/**
* Set the process-global gateway subagent runtime.
* Called during gateway startup so that gateway-bindable plugin runtimes can
* resolve subagent methods dynamically even when their registry was cached
* before the gateway finished loading plugins.
*/
export function setGatewaySubagentRuntime(subagent: PluginRuntime["subagent"]): void {
gatewaySubagentState.subagent = subagent;
}
/**
* Reset the process-global gateway subagent runtime.
* Used by tests to avoid leaking gateway state across module reloads.
*/
export function clearGatewaySubagentRuntime(): void {
gatewaySubagentState.subagent = undefined;
}
/**
* Create a late-binding subagent that resolves to:
* 1. An explicitly provided subagent (from runtimeOptions), OR
* 2. The process-global gateway subagent when the caller explicitly opts in, OR
* 3. The unavailable fallback (throws with a clear error message).
*/
function createLateBindingSubagent(
explicit?: PluginRuntime["subagent"],
allowGatewaySubagentBinding = false,
): PluginRuntime["subagent"] {
if (explicit) {
return explicit;
}
const unavailable = createUnavailableSubagentRuntime();
if (!allowGatewaySubagentBinding) {
return unavailable;
}
return new Proxy(unavailable, {
get(_target, prop, _receiver) {
const resolved = gatewaySubagentState.subagent ?? unavailable;
return Reflect.get(resolved, prop, resolved);
},
});
}
export type CreatePluginRuntimeOptions = {
subagent?: PluginRuntime["subagent"];
allowGatewaySubagentBinding?: boolean;
};
export function createPluginRuntime(_options: CreatePluginRuntimeOptions = {}): PluginRuntime {
const runtime = {
version: resolveVersion(),
config: createRuntimeConfig(),
agent: createRuntimeAgent(),
subagent: createLateBindingSubagent(
_options.subagent,
_options.allowGatewaySubagentBinding === true,
),
system: createRuntimeSystem(),
media: createRuntimeMedia(),
tts: { textToSpeech, textToSpeechTelephony, listVoices: listSpeechVoices },
mediaUnderstanding: {
runFile: runMediaUnderstandingFile,
describeImageFile,
describeImageFileWithModel,
describeVideoFile,
transcribeAudioFile,
},
imageGeneration: {
generate: generateImage,
listProviders: listRuntimeImageGenerationProviders,
},
webSearch: {
listProviders: listWebSearchProviders,
search: runWebSearch,
},
stt: { transcribeAudioFile },
tools: createRuntimeTools(),
channel: createRuntimeChannel(),
events: createRuntimeEvents(),
logging: createRuntimeLogging(),
state: { resolveStateDir },
modelAuth: {
// Wrap model-auth helpers so plugins cannot steer credential lookups:
// - agentDir / store: stripped (prevents reading other agents' stores)
// - profileId / preferredProfile: stripped (prevents cross-provider
// credential access via profile steering)
// Plugins only specify provider/model; the core auth pipeline picks
// the appropriate credential automatically.
getApiKeyForModel: (params) =>
getApiKeyForModelRaw({
model: params.model,
cfg: params.cfg,
}),
resolveApiKeyForProvider: (params) =>
resolveApiKeyForProviderRaw({
provider: params.provider,
cfg: params.cfg,
}),
},
} satisfies PluginRuntime;
return runtime;
}
export type { PluginRuntime } from "./types.js";