mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-20 06:20:55 +00:00
195 lines
6.5 KiB
TypeScript
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";
|