openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
9,029 lines • 392 kB
JavaScript
import { i as extractErrorCode } from "./error-coercion-DKQY4wyF.mjs";
import { D as isRecord, S as resolveGlobalSingleton, T as asOptionalRecord, a as asPositiveFiniteNumber, g as normalizeLowercaseStringOrEmpty, i as asNonNegativeFiniteNumber, m as resolvePositiveTimerTimeoutMs, r as asFiniteNumber, s as asSafeIntegerInRange, v as normalizeOptionalLowercaseString, w as asOptionalObjectRecord, y as normalizeOptionalString } from "./number-coercion-CmpLVzZ6.mjs";
import { C as getProcessPluginCache, D as materializePluginCacheError, E as invalidatePluginCacheMetadata, R as isSqliteLockError, S as getPluginCacheRoot, T as getScopedPluginCaches, _ as resolveOpenClawPackageRootSync, b as getPluginCache, g as resolveCompatibilityHostVersion, k as getPluginExecutionFrame, v as PluginCacheFactInvalidatedError, w as getScopedPluginCache, x as getPluginCacheRetirementSignal, y as bindPluginCacheRoot } from "./sqlite-user-version-C7PVO8Wf.mjs";
import { a as normalizeWindowsPathPreservingCase, d as resolveUserPath, f as tryProcessCwd, i as normalizeWindowsPathForComparison$1, n as isNotFoundPathError, o as resolveConfigDir, r as isPathInside$2 } from "./utils-BfTq4rUg.mjs";
import { $ as secretRefKey, A as hasErrnoCode, D as parseJsonWithJson5Fallback, F as resolveRootPathSync, I as matchRootFileOpenFailure, J as SINGLE_VALUE_FILE_REF_ID, L as openRootFileSync, M as isMissingPathError, P as resolvePathViaExistingAncestorSync$1, Q as resolveSecretRefProviderSourceMismatch, X as isValidSecretProviderAlias, Y as isBuiltInDefaultSecretProviderRef, Z as normalizeAndGroupSecretRefs, _ as resolveIsNixMode, l as registerSecretValueForRedaction, nt as parseConfigPathArrayIndex, q as ENV_SECRET_REF_ID_RE, r as redactSensitiveText, rt as isBlockedObjectKey$1, st as expectDefined, v as resolveStateDir, y as isVitestRuntimeEnv, z as readFileDescriptorBoundedSync$1 } from "./redact-TLu2QTmL.mjs";
import { a as executeExistingOpenClawStateRead, c as withExistingOpenClawStateDatabaseArtifactPreservingReadOnly, f as isDeeplyFrozenPlainData, i as listAgentIds, l as withExistingOpenClawStateDatabaseReadOnly, o as getActiveOpenClawStateDatabaseReadSnapshot, r as resolveAgentWorkspaceDir, s as isArtifactPreservingStateRead, x as safeParseJson } from "./openclaw-state-db-Bj56_1ny.mjs";
import { a as normalizeTrimmedStringList, c as normalizeUniqueTrimmedStringList, i as normalizeStringEntries, l as sortUniqueStrings, n as normalizeOptionalTrimmedStringList, o as normalizeUniqueSingleOrTrimmedStringList, r as normalizeSortedUniqueTrimmedStringList, t as normalizeArrayBackedTrimmedStringList, u as uniqueStrings } from "./string-normalization-C9v1UMOm.mjs";
import { n as formatErrorMessageWithCode, t as formatErrorMessage } from "./errors-BwnhmTMH.mjs";
import { s as resolveOpenClawStateSqlitePath } from "./openclaw-state-db.paths-D1QtF7Ci.mjs";
import { L as executeSqliteQuerySync, R as executeSqliteQueryTakeFirstSync, z as getNodeSqliteKysely } from "./openclaw-state-db-schema-migration-required-DtltEO1E.mjs";
import { n as compareValidSemver } from "./semver-aYpwYdrQ.mjs";
import { s as tableExists } from "./openclaw-state-db-schema-version-CNeUiDjx.mjs";
import { r as sha256Hex } from "./crypto-digest-DFe1GlHl.mjs";
import { r as getOrCreatePromise } from "./lazy-promise-DLFIcwoO.mjs";
import { o as isTruthyEnvValue } from "./host-env-security-Dgi-NhWK.mjs";
import { et as compareOpenClawVersions, z as decodeMountInfoPath } from "./openclaw-state-db-cache-BjVpQwyn.mjs";
import { n as readSecureFile, t as FsSafeError } from "./fs-safe-JcIGlJ4o.mjs";
import { _ as MANIFEST_KEY, h as resolveOfficialExternalPluginId, o as getOfficialExternalPluginCatalogManifest } from "./official-external-plugin-catalog-source-BYyeyJUb.mjs";
import { a as isPrereleaseResolutionAllowed, i as resolveOfficialExternalPluginLegacyIds, o as parseRegistryNpmSpec, r as resolveOfficialExternalPluginInstall, s as BUNDLED_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_ENTRIES, t as getOfficialExternalPluginCatalogEntryForPackage } from "./official-external-plugin-catalog-BvaPRPMo.mjs";
import { B as number, F as array, H as strictObject, I as boolean, J as isStateDatabaseReadAdmissionInvalidatedError, R as literal, U as string, V as object, W as union, _ as acquireSqliteStagingToken, g as SQLITE_STAGING_TOKEN_FILES, st as throwSqliteLifecycleErrors } from "./sqlite-readonly-location-cleanup-CmRNKiuM.mjs";
import { o as sanitizeForLog, t as createSubsystemLogger } from "./subsystem-M2sQt_up.mjs";
import { t as resolveRuntimeArgs } from "./runtime-worker-url-QXcu2VO1.mjs";
import { c as SecretStoreValidationError, s as SECRET_STORE_VALUE_MAX_BYTES } from "./sqlite-worker-transfer-DphMZ7LJ.mjs";
import { n as ok, t as err } from "./result-BQGgYouL.mjs";
import "./openclaw-state-worker-store-DJL0WCzh.mjs";
import { t as captureOpenClawStateReadWorkerContext } from "./openclaw-state-worker-context-DzNKG4ET.mjs";
import "./temp-artifact-cleanup-ClhKIDFh.mjs";
import { i as resolvePluginSourceCapturesDirectory, n as isLegacyPluginSourceCaptureName, r as resolvePluginSourceCaptureFallbackPrefix, t as PLUGIN_SOURCE_CAPTURE_PREFIX } from "./plugin-source-capture-path-DrCyVB7i.mjs";
import { t as runCommandWithTimeout } from "./exec-B7W00oZE.mjs";
import { createRequire } from "node:module";
import fsSync, { lstatSync, realpathSync } from "node:fs";
import path from "node:path";
import os, { tmpdir } from "node:os";
import { fileURLToPath } from "node:url";
import crypto, { randomBytes, randomUUID } from "node:crypto";
import { formatPosixMode, readRegularFileSync, safePathSegmentHashed } from "@openclaw/fs-safe/advanced";
import { AsyncLocalStorage } from "node:async_hooks";
import { isPathInside, safeStatSync } from "@openclaw/fs-safe/path";
import fs from "node:fs/promises";
import { prerelease, satisfies, valid } from "semver";
import "node:buffer";
import { tryReadJsonSync } from "@openclaw/fs-safe/json";
import "commander";
import "@openclaw/fs-safe/atomic";
import pMap from "p-map";
import { inspectPathPermissions, safeStat } from "@openclaw/fs-safe/permissions";
//#region packages/normalization-core/src/stable-stringify.ts
const preserveString = (value) => value;
/** Deterministically stringifies values, optionally normalizing strings before key ordering. */
function stableStringify(value, normalizeString = preserveString) {
return stringifyStableValue(value, /* @__PURE__ */ new WeakSet(), normalizeString);
}
function stringifyStableValue(value, stack, normalizeString, write) {
if (value === null || value === void 0) return String(value);
if (typeof value === "number" && !Number.isFinite(value)) return JSON.stringify(String(value));
if (typeof value === "bigint") return JSON.stringify(value.toString());
if (typeof value === "string") return JSON.stringify(normalizeString(value));
if (typeof value !== "object") return JSON.stringify(value) ?? "null";
if (stack.has(value)) return JSON.stringify("[Circular]");
stack.add(value);
try {
return stringifyObjectValue(value, stack, normalizeString, write);
} finally {
stack.delete(value);
}
}
function stringifyObjectValue(value, stack, normalizeString, write) {
if (value instanceof Error) return stringifyStableValue({
name: value.name,
message: value.message,
stack: value.stack
}, stack, normalizeString, write);
if (value instanceof Uint8Array) return stringifyStableValue({
type: "Uint8Array",
data: encodeBase64(value)
}, stack, normalizeString, write);
if (Array.isArray(value)) {
if (write) {
write("[");
let separator = "";
for (const entry of value) {
write(separator);
write(stringifyStableValue(entry, stack, normalizeString, write));
separator = ",";
}
write("]");
return "";
}
const serializedEntries = [];
for (const entry of value) serializedEntries.push(stringifyStableValue(entry, stack, normalizeString));
return `[${serializedEntries.join(",")}]`;
}
const record = value;
const keys = Object.keys(record);
const fields = normalizeString === preserveString ? keys.sort() : keys.map((key) => ({
key,
normalizedKey: normalizeString(key)
})).sort((left, right) => {
return compareStableStrings(left.normalizedKey, right.normalizedKey) || compareStableStrings(left.key, right.key);
});
write?.("{");
const serializedFields = normalizeString === preserveString ? keys : [];
let separator = "";
let fieldIndex = 0;
for (const field of fields) {
const key = typeof field === "string" ? field : field.key;
const prefix = `${JSON.stringify(typeof field === "string" ? field : field.normalizedKey)}:`;
if (write) {
write(`${separator}${prefix}`);
write(stringifyStableValue(record[key], stack, normalizeString, write));
separator = ",";
} else serializedFields[fieldIndex++] = `${prefix}${stringifyStableValue(record[key], stack, normalizeString)}`;
}
if (write) {
write("}");
return "";
}
return `{${serializedFields.join(",")}}`;
}
function encodeBase64(value) {
let binary = "";
for (const byte of value) binary += String.fromCharCode(byte);
return btoa(binary);
}
function compareStableStrings(left, right) {
return left < right ? -1 : left > right ? 1 : 0;
}
//#endregion
//#region src/plugins/path-safety.ts
/** Plugin-local re-export of shared path safety helpers for plugin install/runtime code. */
function canProbeWindowsPhysicalAlias(rootPath, targetPath) {
const driveRoot = /^(?:[\\/]{2}[?.][\\/])?[A-Za-z]:[\\/]/.exec(targetPath)?.[0];
if (driveRoot) return isPathInside(driveRoot, targetPath);
const normalizedRoot = normalizeWindowsPathPreservingCase(rootPath);
const probeRoot = isPathInside(rootPath, normalizedRoot) && isPathInside(normalizedRoot, rootPath) ? path.win32.parse(normalizedRoot).root || normalizedRoot : rootPath;
return isPathInside(probeRoot, targetPath);
}
/** Resolves matching physical spellings when Windows presents one tree through different aliases. */
function resolvePhysicalPathInsideRootSync(rootPath, targetPath) {
if (process.platform !== "win32") return;
try {
if (!canProbeWindowsPhysicalAlias(rootPath, targetPath) && !canProbeWindowsPhysicalAlias(fsSync.realpathSync(rootPath), targetPath)) return;
const root = fsSync.statSync(rootPath, { bigint: true });
if (!root.isDirectory() || root.ino === 0n) return;
let current = path.resolve(targetPath);
while (true) {
const candidate = fsSync.statSync(current, { bigint: true });
if (candidate.dev === root.dev && candidate.ino === root.ino) {
const physicalRoot = fsSync.realpathSync(current);
return {
rootPath: physicalRoot,
targetPath: path.resolve(physicalRoot, path.relative(current, targetPath)),
rootIdentity: Object.freeze({
dev: root.dev,
ino: root.ino
})
};
}
const parent = path.dirname(current);
if (parent === current) return;
current = parent;
}
} catch {
return;
}
}
function isPathInside$1(rootPath, targetPath) {
return isPathInside(rootPath, targetPath) || resolvePhysicalPathInsideRootSync(rootPath, targetPath) !== void 0;
}
function createIdentityBoundRootFileFs(rootPath, expected) {
const lstatSync = ((...args) => {
const stat = Reflect.apply(fsSync.lstatSync, fsSync, args);
if (args[0] === rootPath) {
const dev = typeof stat.dev === "bigint" ? stat.dev : BigInt(stat.dev);
const ino = typeof stat.ino === "bigint" ? stat.ino : BigInt(stat.ino);
if (!stat.isDirectory() || dev !== expected.dev || ino !== expected.ino) throw new FsSafeError("path-mismatch", "plugin root identity changed during alias reconciliation");
}
return stat;
});
return {
closeSync: fsSync.closeSync,
constants: fsSync.constants,
fstatSync: fsSync.fstatSync,
lstatSync,
openSync: fsSync.openSync,
readFileSync: fsSync.readFileSync,
realpathSync: fsSync.realpathSync
};
}
/** Opens a plugin artifact after reconciling Windows root aliases. */
function openPluginRootFileSync(params) {
const admittedRoot = params.rootRealPath ?? params.rootPath;
const physical = isPathInside(admittedRoot, params.filePath) ? void 0 : resolvePhysicalPathInsideRootSync(admittedRoot, params.filePath);
return openRootFileSync({
absolutePath: physical?.targetPath ?? params.filePath,
rootPath: physical?.rootPath ?? params.rootPath,
rootRealPath: physical?.rootPath ?? params.rootRealPath,
boundaryLabel: params.boundaryLabel ?? "plugin root",
rejectHardlinks: params.rejectHardlinks,
maxBytes: params.maxBytes,
skipLexicalRootCheck: physical ? true : void 0,
ioFs: physical ? createIdentityBoundRootFileFs(physical.rootPath, physical.rootIdentity) : void 0
});
}
//#endregion
//#region src/plugins/plugin-cache-files.ts
const DEFAULT_PLUGIN_METADATA_MAX_BYTES = 16777216;
function capturedPluginFile(filePath, rootRealPath, contents, stat) {
return {
ok: true,
path: filePath,
rootRealPath,
contents,
hash: crypto.createHash("sha256").update(contents).digest("hex"),
signature: {
size: stat.size,
mtimeMs: stat.mtimeMs,
ctimeMs: stat.ctimeMs
}
};
}
function entryKey(relativePath, rejectHardlinks) {
return JSON.stringify([path.normalize(relativePath), rejectHardlinks]);
}
function enforceFileSize(entry, maxBytes, reason = "validation", filePath) {
if (entry.ok && maxBytes !== void 0 && Math.max(entry.signature.size, entry.contents.length) > maxBytes) return {
ok: false,
failure: {
ok: false,
reason,
error: new FsSafeError("too-large", `File exceeds ${maxBytes} bytes: ${filePath ?? entry.path}`)
}
};
return entry;
}
function pathFacts(targetPath) {
const absolute = path.resolve(targetPath);
const root = getPluginCacheRoot(path.dirname(absolute));
const key = path.basename(absolute);
let facts = root.paths.get(key);
if (!facts) {
facts = {};
root.paths.set(key, facts);
}
return facts;
}
function pluginCacheExistsSync(targetPath) {
const facts = pathFacts(targetPath);
return facts.exists ??= fsSync.existsSync(targetPath);
}
function resolveRealpath(targetPath) {
if (path.resolve(targetPath) === targetPath && pluginCacheRealpathSync(targetPath, true) === targetPath) return targetPath;
return fsSync.realpathSync(targetPath);
}
function pluginCacheRealpathSync(targetPath, native = false) {
const facts = pathFacts(targetPath);
const key = native ? "nativeRealpath" : "realpath";
if (facts[key] === void 0) try {
facts[key] = native ? fsSync.realpathSync.native(targetPath) : resolveRealpath(targetPath);
pathFacts(facts[key])[key] = facts[key];
} catch {
facts[key] = null;
}
return facts[key];
}
/** A trusted owner that changes permissions replaces its own observed stat. */
function refreshPluginCacheStat(targetPath) {
pathFacts(targetPath).stat = void 0;
return pluginCacheStatSync(targetPath);
}
function pluginCacheStatSync(targetPath, throwOnError = false) {
const facts = pathFacts(targetPath);
if (facts.stat === void 0) {
facts.statError = void 0;
try {
facts.stat = fsSync.statSync(targetPath);
facts.exists = true;
} catch (error) {
materializePluginCacheError(error);
facts.statError = error;
facts.stat = null;
}
}
if (facts.stat === null && throwOnError) throw facts.statError;
return facts.stat;
}
/** Final symlink checks must retain lstat facts separately from followed target stats. */
function pluginCacheLstatSync(targetPath) {
const facts = pathFacts(targetPath);
if (facts.lstat === void 0) try {
facts.lstat = fsSync.lstatSync(targetPath);
} catch {
facts.lstat = null;
}
return facts.lstat;
}
function readPluginCacheDirectory(targetPath) {
const root = getPluginCacheRoot(targetPath);
if (!root.directory) try {
root.directory = {
ok: true,
entries: fsSync.readdirSync(targetPath, { withFileTypes: true })
};
} catch (error) {
materializePluginCacheError(error);
root.directory = {
ok: false,
error
};
}
if (!root.directory.ok) throw root.directory.error;
return root.directory.entries;
}
/** Discovery validation is immutable metadata; actual imports validate their own file identity. */
function checkPluginCacheEntry(params) {
let root = getPluginCacheRoot(params.rootDir);
const key = entryKey(params.relativePath, params.rejectHardlinks);
const cached = root.checkedEntries.get(key);
if (cached) return cached;
const absolutePath = path.resolve(params.rootDir, params.relativePath);
let checked;
if (!pluginCacheExistsSync(absolutePath)) try {
const resolved = resolveRootPathSync({
absolutePath,
rootPath: params.rootDir,
rootCanonicalPath: params.rootRealPath,
boundaryLabel: "plugin package directory"
});
root = bindPluginCacheRoot(params.rootDir, resolved.rootCanonicalPath);
checked = {
ok: true,
path: resolved.canonicalPath,
rootRealPath: resolved.rootCanonicalPath,
exists: false
};
} catch (error) {
checked = {
ok: false,
reason: "validation",
error
};
}
else {
const opened = openPluginRootFileSync({
filePath: absolutePath,
rootPath: params.rootDir,
rootRealPath: params.rootRealPath,
boundaryLabel: "plugin package directory",
rejectHardlinks: params.rejectHardlinks
});
if (!opened.ok) checked = opened;
else {
fsSync.closeSync(opened.fd);
root = bindPluginCacheRoot(params.rootDir, opened.rootRealPath);
Object.assign(pathFacts(opened.path), {
exists: true,
stat: opened.stat
});
checked = {
ok: true,
path: opened.path,
rootRealPath: opened.rootRealPath,
exists: true
};
}
}
if (!checked.ok) materializePluginCacheError(checked.error);
root.checkedEntries.set(key, checked);
return checked;
}
/** Reads metadata once under its original boundary policy, including absence and invalid files. */
function readPluginCacheFile(params) {
const lexicalRoot = path.resolve(params.rootDir);
let root = getPluginCacheRoot(lexicalRoot);
const maxBytes = params.maxBytes === null ? void 0 : params.maxBytes ?? DEFAULT_PLUGIN_METADATA_MAX_BYTES;
const key = entryKey(params.relativePath, params.rejectHardlinks);
const limitKey = JSON.stringify([key, maxBytes]);
const strict = params.rejectHardlinks ? void 0 : root.files.get(entryKey(params.relativePath, true));
const cached = root.files.get(key) ?? root.files.get(limitKey) ?? (strict?.ok ? strict : void 0);
if (cached) return enforceFileSize(cached, maxBytes);
const requestedPath = path.resolve(lexicalRoot, params.relativePath);
const checked = root.checkedEntries.get(key);
if (pathFacts(requestedPath).exists === false || checked && (!checked.ok || !checked.exists)) {
const entry = {
ok: false,
failure: checked && !checked.ok ? checked : {
ok: false,
reason: "path"
}
};
root.files.set(key, entry);
return entry;
}
const canonicalRoot = params.rootRealPath ?? pluginCacheRealpathSync(lexicalRoot) ?? lexicalRoot;
const canonicalCached = getPluginCacheRoot(canonicalRoot).files.get(key);
if (canonicalCached?.ok) {
bindPluginCacheRoot(lexicalRoot, canonicalRoot);
return enforceFileSize(canonicalCached, maxBytes);
}
const absolutePath = path.resolve(canonicalRoot, params.relativePath);
const opened = openPluginRootFileSync({
filePath: absolutePath,
rootRealPath: canonicalRoot,
rootPath: canonicalRoot,
boundaryLabel: "plugin root",
rejectHardlinks: params.rejectHardlinks,
maxBytes
});
let entry;
if (!opened.ok) {
entry = {
ok: false,
failure: opened
};
if (opened.reason === "path" && opened.error && typeof opened.error === "object" && "code" in opened.error && opened.error.code === "ENOENT") {
pathFacts(requestedPath).exists = false;
pathFacts(absolutePath).exists = false;
}
} else {
root = bindPluginCacheRoot(lexicalRoot, opened.rootRealPath);
try {
const contents = maxBytes === void 0 ? fsSync.readFileSync(opened.fd) : readFileDescriptorBoundedSync$1(opened.fd, maxBytes);
entry = capturedPluginFile(opened.path, opened.rootRealPath, contents, opened.stat);
Object.assign(pathFacts(absolutePath), {
exists: true,
stat: opened.stat
});
root.checkedEntries.set(key, {
ok: true,
path: opened.path,
rootRealPath: opened.rootRealPath,
exists: true
});
} catch (error) {
entry = {
ok: false,
failure: {
ok: false,
reason: "io",
error
},
failurePhase: "read"
};
} finally {
fsSync.closeSync(opened.fd);
}
}
if (!entry.ok) materializePluginCacheError(entry.failure.error);
root.files.set(entry.ok ? key : limitKey, entry);
return entry;
}
/** Catalog files retain the regular-file policy, which rejects final symlinks but allows hardlinks. */
function readPluginCacheRegularFile(params) {
const absolutePath = path.resolve(params.filePath);
const lexicalRoot = path.dirname(absolutePath);
const canonicalRoot = pluginCacheRealpathSync(lexicalRoot) ?? lexicalRoot;
const root = bindPluginCacheRoot(lexicalRoot, canonicalRoot);
const filename = path.basename(absolutePath);
const key = JSON.stringify(["regular", filename]);
const limitKey = JSON.stringify([
"regular",
filename,
params.maxBytes
]);
let entry = root.files.get(key) ?? root.files.get(limitKey);
if (!entry && pathFacts(absolutePath).exists === false) {
entry = {
ok: false,
failure: {
ok: false,
reason: "path"
}
};
root.files.set(key, entry);
}
if (!entry) try {
const { buffer: contents, stat } = readRegularFileSync({
filePath: absolutePath,
maxBytes: params.maxBytes
});
entry = capturedPluginFile(path.join(canonicalRoot, filename), canonicalRoot, contents, stat);
Object.assign(pathFacts(absolutePath), {
exists: true,
stat
});
root.files.set(key, entry);
} catch (error) {
materializePluginCacheError(error);
entry = {
ok: false,
failure: {
ok: false,
reason: "io",
error
}
};
root.files.set(error instanceof FsSafeError && error.code === "too-large" ? limitKey : key, entry);
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") pathFacts(absolutePath).exists = false;
}
return enforceFileSize(entry, params.maxBytes, "io", absolutePath);
}
function readPluginCacheJsonFile(filePath, options = {}) {
const file = readPluginCacheRegularFile({
filePath,
...options
});
return file.ok ? parsePluginCacheJson(file) : {
ok: false,
error: file.failure.error
};
}
function parsePluginCacheJson(file, options = {}) {
const key = options.json5 ? "json5" : "json";
if (!file[key]) try {
const source = file.contents.toString("utf8");
file[key] = {
ok: true,
value: options.json5 ? parseJsonWithJson5Fallback(source) : JSON.parse(source)
};
} catch (error) {
materializePluginCacheError(error);
file[key] = {
ok: false,
error
};
}
return file[key];
}
//#endregion
//#region src/plugins/package-entrypoints.ts
const PUBLIC_SURFACE_SOURCE_EXTENSIONS = [
".ts",
".mts",
".js",
".mjs",
".cts",
".cjs"
];
/** True when a package entrypoint needs built JavaScript candidates. */
function isTypeScriptPackageEntry(entryPath) {
return [
".ts",
".tsx",
".mts",
".cts"
].includes(path.extname(entryPath).toLowerCase());
}
/** Lists built runtime entry candidates for a TypeScript package entrypoint. */
function listBuiltRuntimeEntryCandidates(entryPath) {
if (!isTypeScriptPackageEntry(entryPath)) return [];
const normalized = entryPath.replace(/\\/g, "/");
const withoutExtension = normalized.replace(/\.[^.]+$/u, "");
const normalizedRelative = withoutExtension.replace(/^\.\//u, "");
const distWithoutExtension = normalizedRelative.startsWith("src/") ? `./dist/${normalizedRelative.slice(4)}` : `./dist/${normalizedRelative}`;
const sourceExtension = path.extname(normalized).toLowerCase();
const outputExtensions = sourceExtension === ".mts" ? [
".mjs",
".js",
".cjs"
] : sourceExtension === ".cts" ? [
".cjs",
".js",
".mjs"
] : [
".js",
".mjs",
".cjs"
];
return [
distWithoutExtension,
...normalizedRelative.startsWith("src/") ? [`./dist/${normalizedRelative}`] : [],
withoutExtension
].flatMap((basePath) => outputExtensions.map((extension) => `${basePath}${extension}`));
}
//#endregion
//#region src/plugins/package-manifest.ts
const DEFAULT_PLUGIN_ENTRY_CANDIDATES = [
"index.ts",
"index.js",
"index.mjs",
"index.cjs"
];
function getPackageManifestMetadata(manifest) {
return manifest?.[MANIFEST_KEY];
}
function resolvePackageExtensionEntries(manifest) {
const rawOpenClaw = manifest?.[MANIFEST_KEY];
if (rawOpenClaw === void 0 || rawOpenClaw === null) return {
status: "missing",
entries: []
};
if (!isRecord(rawOpenClaw)) return {
status: "invalid",
entries: [],
error: "package.json openclaw must be an object"
};
const raw = rawOpenClaw.extensions;
if (raw === void 0 || raw === null) return {
status: "missing",
entries: []
};
if (!Array.isArray(raw)) return {
status: "invalid",
entries: [],
error: "package.json openclaw.extensions must be an array"
};
const entries = [];
for (const [index, entry] of raw.entries()) {
const normalized = normalizeOptionalString(entry);
if (!normalized) return {
status: "invalid",
entries: [],
error: `package.json openclaw.extensions[${index}] must be a non-empty string`
};
entries.push(normalized);
}
if (entries.length === 0) return {
status: "empty",
entries: []
};
return {
status: "ok",
entries
};
}
//#endregion
//#region src/plugins/plugin-runtime-artifact-selection.ts
/** Built hosts default only checkout plugins to compiled execution, not installed packages. */
function resolvePluginRuntimeArtifactPreference(preferBuiltPluginArtifacts) {
if (preferBuiltPluginArtifacts !== void 0) return preferBuiltPluginArtifacts ? "all" : "source";
return /\.[cm]?js$/.test(new URL(import.meta.url).pathname) ? "bundled" : "source";
}
//#endregion
//#region src/plugins/bundled-dir.ts
/** Resolves the bundled plugin directory for source checkouts, dist builds, and tests. */
const DISABLED_BUNDLED_PLUGINS_DIR = path.join(os.tmpdir(), "openclaw-empty-bundled-plugins");
const TEST_TRUST_BUNDLED_PLUGINS_DIR_ENV = "OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR";
/** Returns true when env disables bundled plugin discovery. */
function areBundledPluginsDisabled(env = process.env) {
const raw = normalizeOptionalLowercaseString(env.OPENCLAW_DISABLE_BUNDLED_PLUGINS);
return raw === "1" || raw === "true";
}
function resolveDisabledBundledPluginsDir() {
if (!pluginCacheExistsSync(DISABLED_BUNDLED_PLUGINS_DIR)) {
fsSync.mkdirSync(DISABLED_BUNDLED_PLUGINS_DIR, { recursive: true });
refreshPluginCacheStat(DISABLED_BUNDLED_PLUGINS_DIR);
}
return DISABLED_BUNDLED_PLUGINS_DIR;
}
function isSourceCheckoutRoot(packageRoot) {
return pluginCacheExistsSync(path.join(packageRoot, "pnpm-workspace.yaml")) && pluginCacheExistsSync(path.join(packageRoot, "src")) && pluginCacheExistsSync(path.join(packageRoot, "extensions"));
}
function shouldTrustTestBundledPluginsDirOverride(env) {
const separateEnv = env !== process.env;
if (!isTruthyEnvValue(env[TEST_TRUST_BUNDLED_PLUGINS_DIR_ENV]) && !(separateEnv && isTruthyEnvValue(process.env[TEST_TRUST_BUNDLED_PLUGINS_DIR_ENV]))) return false;
return isVitestRuntimeEnv(env) || separateEnv && isVitestRuntimeEnv(process.env);
}
function hasUsableBundledPluginTree(pluginsDir) {
if (!pluginCacheExistsSync(pluginsDir)) return false;
try {
return readPluginCacheDirectory(pluginsDir).some((entry) => {
if (!entry.isDirectory()) return false;
const pluginDir = path.join(pluginsDir, entry.name);
return pluginCacheExistsSync(path.join(pluginDir, "package.json")) || pluginCacheExistsSync(path.join(pluginDir, "openclaw.plugin.json"));
});
} catch {
return false;
}
}
function safeRealpathSync(targetPath) {
return pluginCacheRealpathSync(targetPath, true);
}
function trustedBundledPluginRootsForPackageRoot(packageRoot) {
const roots = [path.join(packageRoot, "dist", "extensions"), path.join(packageRoot, "dist-runtime", "extensions")];
if (isSourceCheckoutRoot(packageRoot)) roots.push(path.join(packageRoot, "extensions"));
return roots;
}
function resolvePackageRootsForBundledPlugins() {
const argvRoot = resolveOpenClawPackageRootSync({ argv1: process.argv[1] });
const moduleRoot = resolveOpenClawPackageRootSync({ moduleUrl: import.meta.url });
return uniqueStrings([argvRoot, moduleRoot].filter((entry) => Boolean(entry)));
}
function resolveSourceCheckoutDependencyDiagnostic(env = process.env) {
if (areBundledPluginsDisabled(env)) return null;
for (const packageRoot of resolvePackageRootsForBundledPlugins()) {
if (!isSourceCheckoutRoot(packageRoot)) continue;
const extensionsDir = path.join(packageRoot, "extensions");
if (!isPluginInPackageBundledRoots({
rootDir: extensionsDir,
packageRoot
}) || !hasUsableBundledPluginTree(extensionsDir)) continue;
if (pluginCacheExistsSync(path.join(packageRoot, "node_modules", ".pnpm"))) continue;
return {
source: packageRoot,
message: "OpenClaw source checkout detected without pnpm workspace dependencies; run `pnpm install` from the repo root so bundled plugins can load package-local dependencies."
};
}
return null;
}
function resolveTrustedExistingOverride(resolvedOverride) {
const realOverride = safeRealpathSync(resolvedOverride);
if (!realOverride) return null;
const modulePackageRoot = resolveOpenClawPackageRootSync({ moduleUrl: import.meta.url });
if (!modulePackageRoot || !isPluginInPackageBundledRoots({
rootDir: realOverride,
packageRoot: modulePackageRoot
})) return null;
if (!hasUsableBundledPluginTree(realOverride)) return null;
return realOverride;
}
/** Checks physical containment in a package's source or compiled plugin trees. */
function isPluginInPackageBundledRoots(params) {
const realPluginRoot = safeRealpathSync(params.rootDir);
const realPackageRoot = safeRealpathSync(params.packageRoot);
if (!realPluginRoot || !realPackageRoot) return false;
return trustedBundledPluginRootsForPackageRoot(params.packageRoot).map((trustedRoot) => safeRealpathSync(trustedRoot)).some((trustedRoot) => trustedRoot !== null && isPathInside$2(realPackageRoot, trustedRoot) && isPathInside$2(trustedRoot, realPluginRoot));
}
/** Recognizes compiled bundle ownership only; this never confers plugin trust. */
function isForeignBundledPluginRoot(rootDir, env = process.env) {
const realRootDir = safeRealpathSync(rootDir);
if (!realRootDir) return false;
const extensionsDir = path.dirname(realRootDir);
const compiledDir = path.dirname(extensionsDir);
if (path.basename(extensionsDir) !== "extensions" || !["dist", "dist-runtime"].includes(path.basename(compiledDir))) return false;
const packageRoot = path.dirname(compiledDir);
if (resolveOpenClawPackageRootSync({ cwd: packageRoot }) !== packageRoot || !isPluginInPackageBundledRoots({
rootDir: realRootDir,
packageRoot
})) return false;
const activeBundledDir = resolveBundledPluginsDir(env);
const realActiveDir = activeBundledDir ? safeRealpathSync(activeBundledDir) : null;
if (realActiveDir && isPathInside$2(realActiveDir, realRootDir)) return false;
const runningRoots = resolvePackageRootsForBundledPlugins();
return runningRoots.length > 0 && !runningRoots.some((runningRoot) => isPluginInPackageBundledRoots({
rootDir: realRootDir,
packageRoot: runningRoot
}));
}
function resolveBundledDirFromPackageRoot(packageRoot, preference = "bundled") {
const builtExtensionsDir = path.join(packageRoot, "dist", "extensions");
const runtimeExtensionsDir = path.join(packageRoot, "dist-runtime", "extensions");
if (isSourceCheckoutRoot(packageRoot)) {
const sourceExtensionsDir = path.join(packageRoot, "extensions");
return (preference === "source" ? [sourceExtensionsDir] : [
builtExtensionsDir,
runtimeExtensionsDir,
sourceExtensionsDir
]).find((rootDir) => isPluginInPackageBundledRoots({
rootDir,
packageRoot
}) && hasUsableBundledPluginTree(rootDir));
}
return pluginCacheExistsSync(builtExtensionsDir) ? [runtimeExtensionsDir, builtExtensionsDir].find((rootDir) => isPluginInPackageBundledRoots({
rootDir,
packageRoot
})) : void 0;
}
function resolveBundledPluginsDirUncached(env) {
if (areBundledPluginsDisabled(env)) return resolveDisabledBundledPluginsDir();
const override = env.OPENCLAW_BUNDLED_PLUGINS_DIR?.trim();
let rejectedExistingOverride = null;
if (override) {
const resolvedOverride = resolveUserPath(override, env);
if (pluginCacheExistsSync(resolvedOverride)) {
if (shouldTrustTestBundledPluginsDirOverride(env)) return path.resolve(resolvedOverride);
const trustedOverride = resolveTrustedExistingOverride(resolvedOverride);
if (trustedOverride) return trustedOverride;
rejectedExistingOverride = resolvedOverride;
}
}
try {
const argvRoot = resolveOpenClawPackageRootSync({ argv1: process.argv[1] });
const safeArgvRoot = Boolean(argvRoot && rejectedExistingOverride && isPluginInPackageBundledRoots({
rootDir: rejectedExistingOverride,
packageRoot: argvRoot
})) ? null : argvRoot;
const moduleRoot = resolveOpenClawPackageRootSync({ moduleUrl: import.meta.url });
const packageRoots = uniqueStrings([safeArgvRoot, moduleRoot].filter((entry) => Boolean(entry)));
for (const packageRoot of packageRoots) {
const bundledDir = resolveBundledDirFromPackageRoot(packageRoot, resolvePluginRuntimeArtifactPreference());
if (bundledDir) return bundledDir;
}
} catch {}
try {
const execDir = path.dirname(process.execPath);
const siblingBuilt = path.join(execDir, "dist", "extensions");
if (pluginCacheExistsSync(siblingBuilt)) return siblingBuilt;
const sibling = path.join(execDir, "extensions");
if (pluginCacheExistsSync(sibling)) return sibling;
} catch {}
try {
let cursor = path.dirname(fileURLToPath(import.meta.url));
for (let i = 0; i < 6; i += 1) {
const candidate = path.join(cursor, "extensions");
if (pluginCacheExistsSync(candidate)) return candidate;
const parent = path.dirname(cursor);
if (parent === cursor) break;
cursor = parent;
}
} catch {}
}
function resolveBundledPluginsDir(env = process.env) {
const disabled = areBundledPluginsDisabled(env);
const override = disabled ? void 0 : env.OPENCLAW_BUNDLED_PLUGINS_DIR?.trim();
const resolvedOverride = override ? resolveUserPath(override, env) : void 0;
const trustOverride = shouldTrustTestBundledPluginsDirOverride(env);
const argv1 = process.argv[1];
const execPath = process.execPath;
const cwd = tryProcessCwd();
const metadata = getPluginCache().metadata;
const cached = metadata.bundledPluginsDir;
if (cached && cached.moduleUrl === import.meta.url && cached.disabled === disabled && cached.resolvedOverride === resolvedOverride && cached.trustOverride === trustOverride && cached.argv1 === argv1 && cached.execPath === execPath && cached.cwd === cwd) return cached.value;
const value = resolveBundledPluginsDirUncached(env);
metadata.bundledPluginsDir = {
moduleUrl: import.meta.url,
disabled,
resolvedOverride,
trustOverride,
argv1,
execPath,
cwd,
value
};
return value;
}
//#endregion
//#region src/channels/bundled-channel-catalog-read.ts
const OFFICIAL_CHANNEL_CATALOG_RELATIVE_PATH = path.join("dist", "channel-catalog.json");
function listPackageRoots() {
return uniqueStrings([resolveOpenClawPackageRootSync({ cwd: process.cwd() }), resolveOpenClawPackageRootSync({ moduleUrl: import.meta.url })].filter((entry) => Boolean(entry)));
}
function readBundledExtensionCatalogEntriesSync(pluginsDir) {
if (!pluginsDir) return [];
try {
return readPluginCacheDirectory(pluginsDir).filter((entry) => entry.isDirectory()).flatMap((entry) => {
const parsed = readPluginCacheJsonFile(path.join(pluginsDir, entry.name, "package.json"));
return parsed.ok && isRecord(parsed.value) ? [parsed.value] : [];
});
} catch {
return [];
}
}
function readOfficialCatalogFileSync() {
const bundledExternalEntries = BUNDLED_OFFICIAL_EXTERNAL_PLUGIN_CATALOG_ENTRIES.filter((entry) => typeof entry === "object" && entry !== null);
for (const packageRoot of listPackageRoots()) {
const payload = readPluginCacheJsonFile(path.join(packageRoot, OFFICIAL_CHANNEL_CATALOG_RELATIVE_PATH));
if (payload.ok && isRecord(payload.value)) {
const entries = Array.isArray(payload.value.entries) ? payload.value.entries.filter((entry) => isRecord(entry)) : [];
return [...bundledExternalEntries, ...entries];
}
}
return bundledExternalEntries;
}
function isChannelCatalogEntryLike(entry) {
return "openclaw" in entry;
}
function toBundledChannelEntry(entry) {
const channel = isChannelCatalogEntryLike(entry) ? entry.openclaw?.channel : entry;
const id = normalizeOptionalLowercaseString(channel?.id);
if (!id || !channel) return null;
return {
id,
channel,
aliases: Array.isArray(channel.aliases) ? channel.aliases.map((alias) => normalizeOptionalLowercaseString(alias)).filter((alias) => Boolean(alias)) : [],
order: typeof channel.order === "number" && Number.isFinite(channel.order) ? channel.order : Number.MAX_SAFE_INTEGER
};
}
function listBundledChannelCatalogEntries() {
const pluginsDir = resolveBundledPluginsDir();
const catalogs = getPluginCache().metadata.bundledChannelCatalogs;
const key = JSON.stringify([process.cwd(), pluginsDir]);
const cached = catalogs.get(key);
if (cached) return cached;
const entries = /* @__PURE__ */ new Map();
for (const entry of readBundledExtensionCatalogEntriesSync(pluginsDir)) {
const channelEntry = toBundledChannelEntry(entry);
if (channelEntry) entries.set(channelEntry.id, channelEntry);
}
for (const entry of readOfficialCatalogFileSync()) {
const channelEntry = toBundledChannelEntry(entry);
if (channelEntry) entries.set(channelEntry.id, entries.get(channelEntry.id) ?? channelEntry);
}
const catalog = Array.from(entries.values()).toSorted((left, right) => left.order - right.order || left.id.localeCompare(right.id));
catalogs.set(key, catalog);
return catalog;
}
//#endregion
//#region src/channels/bundled-channel-ids.generated.ts
const GENERATED_BUNDLED_CHANNEL_IDS = [
{
channelId: "a2a",
order: 75,
label: "A2A"
},
{
channelId: "buzz",
order: 56,
label: "Buzz"
},
{
channelId: "clickclack",
order: 85,
label: "ClickClack"
},
{
channelId: "discord",
label: "Discord"
},
{
channelId: "feishu",
aliases: ["lark"],
order: 35,
label: "Feishu"
},
{
channelId: "googlechat",
aliases: ["gchat", "google-chat"],
order: 55,
label: "Google Chat"
},
{
channelId: "imessage",
aliases: ["imsg"],
label: "iMessage"
},
{
channelId: "irc",
aliases: ["internet-relay-chat"],
label: "IRC"
},
{
channelId: "line",
order: 75,
label: "LINE"
},
{
channelId: "matrix",
order: 70,
label: "Matrix"
},
{
channelId: "mattermost",
order: 65,
label: "Mattermost"
},
{
channelId: "msteams",
aliases: ["teams"],
order: 60,
label: "Microsoft Teams"
},
{
channelId: "nextcloud-talk",
aliases: ["nc", "nc-talk"],
order: 65,
label: "Nextcloud Talk"
},
{
channelId: "nostr",
order: 55,
label: "Nostr"
},
{
channelId: "qa-channel",
order: 999,
configurable: false,
label: "QA Channel"
},
{
channelId: "raft",
order: 72,
label: "Raft"
},
{
channelId: "reef",
label: "Reef"
},
{
channelId: "signal",
label: "Signal"
},
{
channelId: "slack",
label: "Slack"
},
{
channelId: "sms",
order: 88,
label: "SMS"
},
{
channelId: "synology-chat",
order: 90,
label: "Synology Chat"
},
{
channelId: "telegram",
label: "Telegram"
},
{
channelId: "tlon",
order: 90,
label: "Tlon"
},
{
channelId: "twitch",
aliases: ["twitch-chat"],
label: "Twitch"
},
{
channelId: "whatsapp",
label: "WhatsApp"
},
{
channelId: "zalo",
aliases: ["zl"],
order: 80,
label: "Zalo"
},
{
channelId: "zalouser",
aliases: ["zlu"],
order: 85,
label: "Zalo Personal"
}
];
//#endregion
//#region src/channels/ids.ts
function listBundledChatChannelEntries() {
return GENERATED_BUNDLED_CHANNEL_IDS.filter((entry) => entry.configurable !== false).map((entry) => ({
id: normalizeOptionalLowercaseString(entry.channelId) ?? entry.channelId,
aliases: entry.aliases ?? [],
label: entry.label?.trim() || void 0,
order: entry.order ?? Number.MAX_SAFE_INTEGER
})).toSorted((left, right) => left.order - right.order || left.id.localeCompare(right.id, "en", { sensitivity: "base" }));
}
const BUNDLED_CHAT_CHANNEL_ENTRIES = Object.freeze(listBundledChatChannelEntries());
const CHAT_CHANNEL_ID_SET = new Set(BUNDLED_CHAT_CHANNEL_ENTRIES.map((entry) => entry.id));
Object.freeze(BUNDLED_CHAT_CHANNEL_ENTRIES.map((entry) => entry.id));
const CHAT_CHANNEL_ALIASES = Object.freeze(Object.fromEntries(BUNDLED_CHAT_CHANNEL_ENTRIES.flatMap((entry) => entry.aliases.map((alias) => [alias, entry.id]))));
function normalizeRuntimeBundledChatChannelId(normalized) {
return listBundledChannelCatalogEntries().find((entry) => entry.id === normalized || entry.aliases.includes(normalized))?.id ?? null;
}
function normalizeChatChannelId(raw) {
const normalized = normalizeOptionalLowercaseString(raw);
if (!normalized) return null;
const resolved = CHAT_CHANNEL_ALIASES[normalized] ?? normalized;
return CHAT_CHANNEL_ID_SET.has(resolved) ? resolved : normalizeRuntimeBundledChatChannelId(normalized);
}
//#endregion
//#region src/plugins/default-enablement.ts
/** True when a plugin should be enabled by default for a platform. */
function isPluginEnabledByDefaultForPlatform(plugin, platform = process.platform) {
if (plugin.enabledByDefault === true) return true;
return plugin.enabledByDefaultOnPlatforms?.includes(platform) === true;
}
//#endregion
//#region src/plugins/slots.ts
/** Applies mutually exclusive plugin slot selection for memory and context-engine plugins. */
const DEFAULT_SLOT_BY_KEY = {
memory: "memory-core",
contextEngine: "legacy"
};
Object.keys(DEFAULT_SLOT_BY_KEY);
/** Returns the implicit plugin id that owns a slot before config overrides it. */
function defaultSlotIdForKey(slotKey) {
return DEFAULT_SLOT_BY_KEY[slotKey];
}
/** Raw `plugins.slots[key]`: `none` turns the slot off, blank leaves it unset. */
function normalizeSlotValue(value) {
const trimmed = normalizeOptionalString(value);
if (!trimmed) return;
if (normalizeOptionalLowercaseString(trimmed) === "none") return null;
return trimmed;
}
function resolveSlotSelection(slotKey, value) {
const normalized = normalizeSlotValue(value);
if (normalized === void 0) return {
kind: "default",
pluginId: defaultSlotIdForKey(slotKey)
};
return normalized === null ? { kind: "off" } : {
kind: "pinned",
pluginId: normalized
};
}
//#endregion
//#region src/plugins/config-normalization-shared.ts
/** Default plugin id normalizer for already-canonical ids. */
const identityNormalizePluginId = (id) => id.trim();
function normalizePluginConfigList(value, normalizePluginId) {
if (!Array.isArray(value)) return [];
return value.map((entry) => typeof entry === "string" ? normalizePluginId(entry) : "").filter(Boolean);
}
function normalizeHookTimeoutMs(value) {
return asSafeIntegerInRange(value, {
min: 1,
max: 6e5
});
}
function normalizeHookTimeouts(value) {
if (!isRecord(value)) return;
const normalized = {};
for (const [hookName, timeoutMs] of Object.entries(value)) {
const normalizedTimeoutMs = normalizeHookTimeoutMs(timeoutMs);
if (normalizedTimeoutMs !== void 0) normalized[hookName] = normalizedTimeoutMs;
}
return Object.keys(normalized).length > 0 ? normalized : void 0;
}
function normalizePluginHooks(value) {
if (!isRecord(value)) return;
const hooks = {};
for (const key of ["allowPromptInjection", "allowConversationAccess"]) if (typeof value[key] === "boolean") hooks[key] = value[key];
const timeoutMs = normalizeHookTimeoutMs(value.timeoutMs);
const timeouts = normalizeHookTimeouts(value.timeouts);
if (timeoutMs !== void 0) hooks.timeoutMs = timeoutMs;
if (timeouts !== void 0) hooks.timeouts = timeouts;
return Object.keys(hooks).length > 0 ? hooks : void 0;
}
function normalizePluginModelConfig(value, kind) {
if (!isRecord(value)) return;
const config = {};
if (typeof value.allowModelOverride === "boolean") config.allowModelOverride = value.allowModelOverride;
for (const [key, configuredKey] of [["allowedModels", "hasAllowedModelsConfig"], ["allowedCompletionModels", "hasAllowedCompletionModelsConfig"]]) {
if (key === "allowedCompletionModels" && kind !== "llm") continue;
const models = normalizeArrayBackedTrimmedStringList(value[key]);
if (models) {
config[configuredKey] = true;
if (models.length > 0) config[key] = models;
}
}
if (kind === "llm") {
for (const key of ["allowAuthProfileOverride", "allowAgentIdOverride"]) if (typeof value[key] === "boolean") config[key] = value[key];
}
return Object.keys(config).length > 0 ? config : void 0;
}
function normalizePluginEntries(entries, normalizePluginId) {
if (!isRecord(entries)) return {};
const normalized = {};
for (const [key, value] of Object.entries(entries)) {
const normalizedKey = normalizePluginId(key);
if (!normalizedKey) continue;
if (!isRecord(value)) {
normalized[normalizedKey] = {};
continue;
}
const entry = value;
normalized[normalizedKey] = {
...normalized[normalizedKey],
enabled: typeof entry.enabled === "boolean" ? entry.enabled : normalized[normalizedKey]?.enabled,
hooks: normalizePluginHooks(entry.hooks) ?? normalized[normalizedKey]?.hooks,
subagent: normalizePluginModelConfig(entry.subagent, "subagent") ?? normalized[normalizedKey]?.subagent,
llm: normalizePluginModelConfig(entry.llm, "llm") ?? normalized[normalizedKey]?.llm,
config: "config" in entry ? entry.config : normalized[normalizedKey]?.config
};
}
return normalized;
}
/** Normalizes plugin config while allowing callers to resolve aliases first. */
function normalizePluginsConfigWithResolverCore(config, normalizePluginId = identityNormalizePluginId) {
const memorySlot = resolveSlotSelection("memory", config?.slots?.memory);
return {
enabled: config?.enabled ?? true,
allow: normalizePluginConfigList(config?.allow, normalizePluginId),
deny: normalizePluginConfigList(config?.deny, normalizePluginId),
loadPaths: normalizePluginConfigList(config?.load?.paths, identityNormalizePluginId),
slots: {
memory: memorySlot.kind === "off" ? null : memorySlot.pluginId,
contextEngine: normalizeSlotValue(config?.slots?.contextEngine)
},
entries: normalizePluginEntries(config?.entries, normalizePluginId)
};
}
/**
* Enables an owner for any enabled channel; disables it only when all channels are off.
* Unspecified channels leave the plugin's own activation policy in control.
*/
function resolveChannelConfigEnablement(cfg, pluginId, channelIds = []) {
const channels = cfg?.channels;
if (!channels) return;
const enablement = (channelIds.length ? channelIds.map((channelId) => normalizeChatChannelId(channelId) ?? channelId) : [normalizeChatChannelId(pluginId)]).map((channelId) => {
const entry = channelId ? channels[channelId] : void 0;
return isRecord(entry) ? entry.enabled : void 0;
});
if (enablement.includes(true)) return true;
return enablement.every((enabled) => enabled === false) ? false : void 0;
}
//#endregion
//#region src/plugins/plugin-policy-id.ts
/**
* Canonicalizes a plugin id for comparison against `plugins.allow`, `plugins.deny`, and
* `plugins.entries`, which are lowercase-normalized when config is normalized. A manifest declares
* its id in whatever case its author chose, so policy must compare this derived key rather than the
* declared id. The declared id itself stays untouched: the loader matches it against the plugin's
* runtime export id, and rewriting it would break plugins whose export matches a mixed-case manifest.
*/
function normalizePluginPolicyId(id) {
return normalizeOptionalLowercaseString(id) ?? "";
}
//#endregion
//#region src/plugins/config-activation-shared.ts
function resolveExplicitPluginSelectionShared(params) {
const policyId = normalizePluginPolicyId(params.id);
if (params.config.entries[policyId]?.enabled === true) return "enabled in config";
if (params.origin === "bundled" && resolveChannelConfigEnablement(params.rootConfig, params.id, params.channelIds) === true) return "channel enabled in config";
if (params.config.slots.memory === params.id) return "selected memory slot";
if (params.config.slots.contextEngine === params.id) return "selected context engine slot";
if (params.origin !== "bundled" && params.config.allow.includes(policyId)) return "selected in allowlist";
}
function resolvePluginActivationStateShared(params) {
const activationSource = params.activationSource ?? {
plugins: params.config,
rootConfig: params.rootConfig
};
const explicitReason = resolveExplicitPluginSelectionShared({
id: params.id,
origin: params.origin,
config: activationSource.plugins,
rootConfig: activationSource.rootConfig,
channelIds: params.channelIds
});
const decision = (source, details = {}) => ({
enabled: source !== "disabled",
activated: source !== "disabled",
explicitlyEnabled: explicitReason !== void 0,
source,
reason: void 0,
...details
});
if (!params.config.enabled) return decision("disabled", { reason: "plugins disabled" });
const policyId = normalizePluginPolicyId(params.id);
if (params.config.deny.includes(policyId)) return decision("disabled", { reason: "blocked by denylist" });
const entry = params.config.entries[policyId];
if (entry?.enabled === false) return decision("disabled", { reason: "disabled in config" });
if (resolveChannelConfigEnablement(activationSource.rootConfig ?? params.rootConfig, params.id, params.channelIds) === false) return decision("disabled", { reason: "channel disabled in config" });
const explicitlyAllowed = params.config.allow.includes(policyId);
if (params.origin === "workspace" && !explicitlyAllowed && entry?.enabled !== true && explicitReason !== "selected context engine slot") return decision("disabled", { reason: "workspace plugin (disabled by default)" });
if (params.config.slots.memory === params.id) return decision("explicit", {
explicitlyEnabled: true,
reason: "selected memory slot"
});
if (params.config.slots.contextEngine === params.id) return decision("explicit", {
explicitlyEnabled: true,
reason: "selected context engine slot"
});
if (params.allowBundledChannelExplicitBypassesAllowlist === true && explicitReason === "channel enabled in config") return decision("explicit", {
explicitlyEnabled: true,
reason: explicitReason
});
if (params.config.allow.length > 0 && !explicitlyAllowed) return decision("disabled", { reason: "not in allowlist" });
if (explicitReason) return decision("explicit", {
explicitlyEnabled: true,
reason: explicitReason
});
if (params.autoEnabledReason) return decision("auto", {
explicitlyEnabled: false,
reason: params.autoEnabledReason
});
if (entry?.enabled === true) return decision("auto", {
explicitlyEnabled: false,
reason: "enabled by effective config"
});
if (params.origin === "bundled" && resolveChannelConfigEnablement(params.rootConfig, params.id, params.channelIds) === true) return decision("auto", {
explicitlyEnabled: false,
reason: "channel configured"
});
if (params.origin === "bundled" && params.enabledByDefault === true) return decision("default", {
explicitlyEnabled: false,
reason: "bundled default enablement"
});
if (params.origin === "bundled") return decision("disabled", {
explicitlyEnabled: false,
reason: "bundled (disabled by default)"
});
return decision("default");
}
//#endregion
//#region src/plugins/config-state.ts
/** Normalizes plugin config and resolves effective enablement, slots, and activation sources. */
const BUILT_IN_PLUGIN_ALIAS_LOOKUP = /* @__PURE__ */ new Map([
["google-gemini-cli", "google"],
["minimax-portal", "minimax"],
["minimax-portal-auth", "minimax"]
]);
/** Normalizes user/config plugin ids into the canonical lowercase key form. */
function normalizePluginId(id) {
const normalized = normalizeOptionalLowercaseString(id) ?? "";
return BUILT_IN_PLUGIN_ALIAS_LOOKUP.get(normalized) ?? normalized;
}
/** Builds caller-owned policy without exposing the host's prepared objects. */
const createNormalizedPluginsConfig = (config) => normalizePluginsConfigWithResolverCore(config, normalizePluginId);
const normalizePluginsConfig = (config) => {
if (preparedRuntimePluginsConfig && preparedRuntimePluginsConfig.source === config) return preparedRuntimePluginsConfig.value;
return createNormalizedPluginsConfig(config);
};
let preparedRuntimePluginsConfig;
function resolveEffectivePluginActivationState(params) {
return resolvePluginActivationStateShared({
...params,
allowBundledChannelExplicitBypassesAllowlist: true
});
}
//#endregion
//#region packages/model-catalog-core/src/provider-id.ts
function normalizeProviderId(provider) {
return normalizeLowercaseStringOrEmpty(provider);
}
//#endregion
//#region src/secrets/resolve-errors.ts
/** Error for failures that affect an entire configured secret provider. */
var SecretProviderResolutionError = class extends Error {
constructor(params) {
super(params.message, params.cause !== void 0 ? { cause: params.cause } : void 0);
this.scope = "provider";
this.name = "SecretProviderResolutionError";
this.code = params.code;
this.source = params.source;
this.provider = params.provider;
}
};
/** Error for failures limited to one SecretRef id under a provider. */
var SecretRefResolutionError = class extends Error {
constructor(params) {
super(params.message, params.cause !== void 0 ? { cause: params.cause } : void 0);
this.scope = "ref";
this.name = "SecretRefResolutionError";
this.code = params.code;
this.source = params.source;
this.provider = params.provider;
this.refId = params.refId;
}
};
function isSecretResolutionError(value) {
return value instanceof SecretProviderResolutionError || value instanceof SecretRefResolutionError;
}
/** Sanitized provider detail suitable for operator-facing diagnostics. */
function describeSecretResolutionOperatorDiagnostic(value) {
if (value instanceof SecretRefResolutionError && value.code === "SECRET_REF_REDACTED_VALUE") return `Secret reference "${value.source}:${value.provider}:${value.refId}" resolves to a redaction placeholder`;
if (value instanceof SecretProviderResolutionError && value.code === "SECRET_PROVIDER_PATH_SECURITY_UNVERIFIABLE") return "Windows path security could not be verified";
}
/** Sanitized recovery action suitable for operator-facing diagnostics. */
function describeSecretResolutionOperatorRecovery(value) {
if (value instanceof SecretRefResolutionError && value.code === "SECRET_REF_REDACTED_VALUE") return "Run openclaw doctor --fix to repair a store-backed Gateway token; supply a real credential for other secrets, then restart the Gateway and reconnect or re-pair clients";
if (!(value instanceof SecretProviderResolutionError) || value.code !== "SECRET_PROVIDER_PATH_SECURITY_UNVERIFIABLE") return;
return value.source === "exec" ? "Restore Windows path security verification, or use an existing provider command whose owner and ACLs OpenClaw can verify" : "Restore Windows path security verification, or use an existing secret file whose owner and ACLs OpenClaw can verify";
}
function providerResolutionError(params) {
return new SecretProviderResolutionError({
...params,
code: params.code ?? "SECRET_PROVIDER_UNAVAILABLE"
});
}
function refResolutionError(params) {
return new SecretRefResolutionError(params);
}
const REDACTED_SECRET_VALUES = /* @__PURE__ */ new Set([
"__OPENCLAW_REDACTED__",
"REDACTED",
"xoxb-REDACTED",
"xapp-REDACTED",
"***",
"[redacted]",
"[REDACTED]",
"<redacted>",
"[REDACTED_PRIVATE_KEY]",
"[REDACTED CREDENTIAL]"
]);
function isRedactedSecretValue(value) {
return typeof value === "string" && REDACTED_SECRET_VALUES.has(value.trim());
}
//#endregion
//#region src/agents/workspace-dirs.ts
/** Lists unique workspace directories for configured agents and the default agent. */
function listAgentWorkspaceDirs(cfg, env = process.env) {
const dirs = /* @__PURE__ */ new Set();
for (const agentId of listAgentIds(cfg)) dirs.add(resolveAgentWorkspaceDir(cfg, agentId, env));
return [...dirs];
}
//#endregion
//#region src/plugins/current-plugin-metadata-state.ts
/** Owns config identity reuse for the current immutable metadata snapshot. */
const currentPluginMetadataConfigIdentityCache = {
add(config) {
getProcessPluginCache().metadata.current.configIdentities.add(config);
},
clear() {
getProcessPluginCache().metadata.current.configIdentities = /* @__PURE__ */ new WeakSet();
},
has(config) {
return getProcessPluginCache().metadata.current.configIdentities.has(config);
}
};
/** Stores the process-current plugin metadata snapshot and compatible config fingerprints. */
function setCurrentPluginMetadataSnapshotState(snapshot, configFingerprint, compatiblePolicyHashes, compatibleConfigFingerprints, modelIdNormalizationPolicies, owner = "operation", envFingerprint, defaultDiscoveryCompatible = false, agentWorkspaceFingerprint) {
const state = getProcessPluginCache().metadata.current;
state.snapshot = snapshot;
state.owner = owner;
state.configFingerprint = snapshot ? configFingerprint : void 0;
state.agentWorkspaceFingerprint = snapshot ? agentWorkspaceFingerprint : void 0;
state.envFingerprint = snapshot ? envFingerprint : void 0;
state.defaultDiscoveryCompatible = Boolean(snapshot && defaultDiscoveryCompatible);
state.compatiblePolicyHashes = snapshot ? compatiblePolicyHashes : void 0;
state.compatibleConfigFingerprints = snapshot ? compatibleConfigFingerprints : void 0;
state.revision = Symbol("plugin-metadata-snapshot");
}
/** Reads the boot inventory without importing discovery into lightweight consumers. */
function getGatewayPluginMetadataSnapshot() {
const cache = getPluginCache();
if (cache.kind === "process" && cache.metadata.current.owner === "gateway") return cache.metadata.current.snapshot;
}
/** Captures the current snapshot and the policies eligible for process-wide publication. */
function getCurrentPluginMetadataSnapshotState() {
const state = getProcessPluginCache().metadata.current;
return {
snapshot: state.snapshot,
owner: state.owner,
configFingerprint: state.configFingerprint,
agentWorkspaceFingerprint: state.agentWorkspaceFingerprint,
envFingerprint: state.envFingerprint,
defaultDiscoveryCompatible: state.defaultDiscoveryCompatible,
compatiblePolicyHashes: state.compatiblePolicyHashes,
compatibleConfigFingerprints: state.compatibleConfigFingerprints,
revision: state.revision
};
}
//#endregion
//#region src/state/config-machine-state.ts
function normalizeConfigMachineStateKey(key) {
const normalized = key.trim();
if (!normalized) throw new Error("config machine state key must not be empty");
return normalized;
}
function readConfigMachineStateRowInDatabase(database, key) {
if (!tableExists(database, "config_machine_state")) return;
const db = getNodeSqliteKysely(database);
return executeSqliteQueryTakeFirstSync(database, db.selectFrom("config_machine_state").select(["value_json", "updated_at_ms"]).where("state_key", "=", normalizeConfigMachineStateKey(key)));
}
function readConfigMachineStateWithMetadata(key, options = {}, behavior = {}) {
const read = ({ db: database }) => {
const row = readConfigMachineStateRowInDatabase(database, key);
return row ? {
value: JSON.parse(row.value_json),
updatedAtMs: row.updated_at_ms
} : void 0;
};
return behavior.artifactPreservingReadOnly ? withExistingOpenClawStateDatabaseArtifactPreservingReadOnly(read, options) : withExistingOpenClawStateDatabaseReadOnly(read, options);
}
function readConfigMachineState(key, options = {}, behavior = {}) {
return readConfigMachineStateWithMetadata(key, options, behavior)?.value;
}
//#endregion
//#region src/plugins/install-root-context.ts
const pluginInstallRootContext = resolveGlobalSingleton(Symbol.for("openclaw.pluginInstallRootContext"), () => new AsyncLocalStorage());
/** Resolve the ordinary operator-owned plugin roots before a run redirects state. */
function resolvePluginInstallRoots(env = process.env, homedir = os.homedir) {
const configDir = resolveConfigDir(env, homedir);
return Object.freeze({
extensionsDir: path.join(configDir, "extensions"),
gitDir: path.join(configDir, "git"),
npmDir: path.join(configDir, "npm"),
stateDir: resolveStateDir(env, homedir)
});
}
/** Return run-pinned install roots, or resolve the caller's ordinary roots. */
function resolveActivePluginInstallRoots(env = process.env, homedir = os.homedir) {
return pluginInstallRootContext.getStore() ?? resolvePluginInstallRoots(env, homedir);
}
/** Artifact paths do not need the state directory's legacy-location filesystem probes. */
function resolveActivePluginInstallDir(kind, env = process.env, homedir = os.homedir) {
const roots = pluginInstallRootContext.getStore();
return roots ? roots[`${kind}Dir`] : path.join(resolveConfigDir(env, homedir), kind);
}
/** Return whether the current run pinned operator-owned plugin install roots. */
function hasActivePluginInstallRoots() {
return pluginInstallRootContext.getStore() !== void 0;
}
//#endregion
//#region src/plugins/plugin-metadata-snapshot-readers.ts
const snapshotReaderSlot = resolveGlobalSingleton(Symbol.for("openclaw.pluginMetadataSnapshotReaders"), () => ({}));
resolveGlobalSingleton(Symbol.for("openclaw.pluginMetadataSnapshotReaderCustody"), () => ({ owners: 0 }));
/** Called at module evaluation; a retained Gateway keeps its registered readers. */
function registerPluginMetadataSnapshotReaders(readers) {
Object.assign(snapshotReaderSlot, readers);
}
//#endregion
//#region src/plugins/plugin-source-capture-context.ts
const runInPluginSourceCaptureContext = resolveGlobalSingleton(Symbol.for("openclaw.pluginSourceCaptureContext"), () => AsyncLocalStorage.snapshot());
resolveGlobalSingleton(Symbol.for("openclaw.pluginSourceCaptureMaintenance"), () => new AsyncLocalStorage());
//#endregion
//#region src/plugins/plugin-source-capture-native-loads.ts
/** The process-wide capture owner installs this observer once. */
function observePluginNativeLoads() {
const paths = /* @__PURE__ */ new Set();
const loadAddon = process.dlopen.bind(process);
process.dlopen = (...args) => {
try {
const file = fsSync.realpathSync.native(args[1]);
paths.add(process.platform === "win32" ? normalizeWindowsPathPreservingCase(file) : file);
} catch {}
return loadAddon(...args);
};
return paths;
}
function createPluginNativeCaptureCustody(ownedRoots) {
const nativeLoadPaths = observePluginNativeLoads();
const nativeReferences = /* @__PURE__ */ new Map();
const retiringNativeRoots = /* @__PURE__ */ new Set();
const retainedRoots = /* @__PURE__ */ new Set();
return {
nativeReferences,
retiringNativeRoots,
/** Independent inventories and loaded modules can retain the same native namespace. */
isPluginSourceCaptureRetained(directory) {
return [...ownedRoots, ...retainedRoots].some((root) => isPathInside$2(root, directory) || isPathInside$2(directory, root)) || [...nativeReferences.keys(), ...nativeLoadPaths].some((file) => isPathInside$2(directory, file));
},
/** Physical module lifetime outlives registration and CommonJS cache eviction. */
retainLoadedPluginSourceCapture(directory) {
if (![...nativeLoadPaths].some((file) => isPathInside$2(directory, file))) return false;
const retained = [...ownedRoots].find((root) => isPathInside$2(root, directory)) ?? directory;
if (![...retainedRoots].some((root) => isPathInside$2(root, retained) || isPathInside$2(retained, root))) process.emitWarning(`Plugin source capture cleanup: retained-by-loaded-module: ${retained}; cleanup deferred until the next startup`);
retainedRoots.add(retained);
return true;
},
/** Warm generations retain superseded snapshots until their local cache retires. */
retainPluginNativeCapturePath(capturedPath) {
const file = path.resolve(capturedPath);
if ([...retiringNativeRoots].some((root) => file.startsWith(root + path.sep))) throw new Error("Plugin native capture is being reclaimed");
nativeReferences.set(file, (nativeReferences.get(file) ?? 0) + 1);
let released = false;
return () => {
if (released) return;
released = true;
const references = nativeReferences.get(file);
if (references === 1) nativeReferences.delete(file);
else nativeReferences.set(file, references - 1);
};
}
};
}
//#endregion
//#region src/plugins/plugin-source-capture-directory.ts
const CAPTURE_GRACE_MS = 36e5;
const { instances, ownedRoots, nativeReferences, retiringNativeRoots, isPluginSourceCaptureRetained, retainLoadedPluginSourceCapture, retainPluginNativeCapturePath, sweeps, warningBackoff } = resolveGlobalSingleton(Symbol.for("openclaw.pluginSourceCaptureInstances"), () => {
const instanceRoots = /* @__PURE__ */ new Set();
const nativeCustody = createPluginNativeCaptureCustody(instanceRoots);
process.once("exit", () => {
for (const [key, instance] of instances) try {
const root = retireInstance(key, instance);
if (root) removeInstanceSync(root, instance.pendingNative);
} catch (error) {
process.stderr.write(`Plugin source capture exit cleanup failed: ${String(error)}\n`);
}
});
return {
instances: /* @__PURE__ */ new Map(),
ownedRoots: instanceRoots,
...nativeCustody,
sweeps: /* @__PURE__ */ new Map(),
warningBackoff: /* @__PURE__ */ new Map()
};
});
function retireInstance(key, instance) {
if (instance.root && retainLoadedPluginSourceCapture(instance.root)) {
instance.references.clear();
scheduleCaptureCleanup(key, instance);
return;
}
instance.closing = true;
let removalRoot = instance.root;
try {
instance.token?.(true);
} catch (error) {
if (!hasErrnoCode(error, "ENOENT")) throw error;
instance.token?.();
removalRoot = void 0;
}
if (instance.root) ownedRoots.delete(instance.root);
instance.references.clear();
instances.delete(key);
scheduleCaptureCleanup(key, instance);
return removalRoot;
}
function warn(error) {
process.emitWarning(`Plugin source capture cleanup: ${String(error)}`);
}
/** Reclamation owns an existing native token until its captured payload is gone. */
async function reclaimInstance(directory, originalDirectory, nativeMaintenance) {
const ownerPath = path.join(directory, SQLITE_STAGING_TOKEN_FILES[0]);
const family = SQLITE_STAGING_TOKEN_FILES.map((file) => fsSync.lstatSync(path.join(directory, file), { throwIfNoEntry: false }));
const originalOwner = family[0];
const captures = path.join(directory, "captures");
const captured = fsSync.lstatSync(captures, { throwIfNoEntry: false });
if (process.getuid && originalDirectory.uid !== process.getuid() || !originalOwner || family.some((file) => file && (!file.isFile() || file.nlink !== 1 || process.getuid && file.uid !== process.getuid())) || captured && !captured.isDirectory()) return;
const unchanged = () => {
const currentDirectory = fsSync.lstatSync(directory);
const currentOwner = fsSync.lstatSync(ownerPath);
return currentDirectory.dev === originalDirectory.dev && currentDirectory.ino === originalDirectory.ino && currentDirectory.isDirectory() && currentOwner.isFile() && currentOwner.nlink === 1 && currentOwner.dev === originalOwner.dev && currentOwner.ino === originalOwner.ino;
};
const release = acquireSqliteStagingToken(directory, "reclaim");
let released = false;
const errors = [];
ownedRoots.add(directory);
try {
if (!unchanged()) return;
const native = path.join(directory, "native");
const nativeStat = fsSync.lstatSync(native, { throwIfNoEntry: false });
await fs.rm(captures, {
recursive: true,
force: true
});
let retainedNative = Boolean(nativeStat);
if (nativeStat?.isDirectory() && nativeMaintenance) {
for (const nativeEntry of await fs.readdir(native, { withFileTypes: true })) {
const nativeDirectory = path.join(native, nativeEntry.name);
if (!nativeEntry.isDirectory()) continue;
nativeMaintenance.assertCurrent();
if (!unchanged()) return;
const contained = (file) => file.startsWith(nativeDirectory + path.sep);
if ([...nativeMaintenance.retainedPaths].some(contained) || [...nativeReferences.keys()].some(contained)) continue;
retiringNativeRoots.add(nativeDirectory);
try {
await fs.rm(nativeDirectory, {
recursive: true,
force: true
});
nativeMaintenance.removed.push(nativeDirectory);
} finally {
retiringNativeRoots.delete(nativeDirectory);
}
}
retainedNative = (await fs.readdir(native)).length > 0;
}
release(true);
released = true;
if (!retainedNative && unchanged()) {
nativeMaintenance?.assertCurrent();
await fs.rm(directory, {
recursive: true,
force: true
});
}
} catch (error) {
errors.push(error);
} finally {
try {
if (!released) release();
} catch (error) {
errors.push(error);
} finally {
ownedRoots.delete(directory);
}
throwSqliteLifecycleErrors(errors, "Plugin source reclamation and cleanup failed");
}
}
function removeInstanceSync(root, pendingNative = []) {
if (retainLoadedPluginSourceCapture(root)) return;
fsSync.rmSync(path.join(root, "captures"), {
recursive: true,
force: true
});
for (const directory of pendingNative) fsSync.rmSync(directory, {
recursive: true,
force: true
});
const native = path.join(root, "native");
if (!fsSync.existsSync(native) || fsSync.readdirSync(native).length === 0) fsSync.rmSync(root, {
recursive: true,
force: true
});
}
async function reclaimInstances(root, recordFailure, legacy = false, nativeMaintenance, fallbackPrefix) {
let entries;
try {
entries = await fs.readdir(root, { withFileTypes: true });
} catch (error) {
if (!hasErrnoCode(error, "ENOENT")) throw error;
return;
}
if (entries.length === 0) return;
const cutoff = Date.now() - CAPTURE_GRACE_MS;
let legacyAllowed;
const lstatIfPresent = (file) => fs.lstat(file).catch((error) => {
if (!hasErrnoCode(error, "ENOENT")) throw error;
});
for (const entry of entries) {
if (!entry.isDirectory() || legacy && !isLegacyPluginSourceCaptureName(entry.name) || fallbackPrefix && !entry.name.startsWith(fallbackPrefix)) continue;
const directory = path.join(root, entry.name);
try {
const stat = await fs.lstat(directory);
const changed = legacy ? Math.max(stat.mtimeMs, stat.ctimeMs, stat.birthtimeMs) : stat.mtimeMs;
if (!stat.isDirectory() || changed > cutoff && !nativeMaintenance?.startup) continue;
const canonical = await fs.realpath(directory);
if (ownedRoots.has(canonical) || retainLoadedPluginSourceCapture(canonical)) continue;
const tokenPath = path.join(canonical, SQLITE_STAGING_TOKEN_FILES[0]);
const nativeStat = await lstatIfPresent(path.join(canonical, "native"));
const tokenStat = await lstatIfPresent(tokenPath);
if (legacy && tokenStat) continue;
if (!tokenStat) {
if (fallbackPrefix || changed > cutoff) continue;
if (nativeStat) continue;
if (legacy) {
if (legacyAllowed === void 0) {
const { inspectOtherOpenClawProcesses } = await import("./openclaw-process-census-Cl9j7zvg.mjs");
const census = inspectOtherOpenClawProcesses();
legacyAllowed = "error" in census || census.pids.length === 0;
}
if (!legacyAllowed) continue;
if (process.getuid && (await fs.lstat(canonical)).uid !== process.getuid()) continue;
}
const retired = path.join(root, `${legacy ? PLUGIN_SOURCE_CAPTURE_PREFIX : ""}${randomUUID()}`);
await fs.rename(canonical, retired);
await fs.rm(retired, {
recursive: true,
force: true
});
continue;
}
await reclaimInstance(canonical, stat, nativeMaintenance);
} catch (error) {
if (!hasErrnoCode(error, "ENOENT") && !isSqliteLockError(error)) recordFailure(error);
}
}
}
/** Coalesce active scans, but throttle diagnostics independently of cleanup retries. */
function sweepPluginSourceCaptureDirectories(stateDir) {
const root = path.resolve(resolvePluginSourceCapturesDirectory(stateDir));
let sweep = sweeps.get(root);
if (!sweep) {
let failures = 0;
let firstFailure;
const recordFailure = (error) => {
if (failures++ === 0) firstFailure = error;
};
sweep = reclaimInstances(root, recordFailure).catch(recordFailure).then(() => reclaimInstances(tmpdir(), recordFailure, false, void 0, resolvePluginSourceCaptureFallbackPrefix(stateDir))).catch(recordFailure).then(async () => {
const visited = /* @__PURE__ */ new Set();
for (const candidate of [path.join(stateDir, "tmp"), tmpdir()]) try {
const directory = await fs.realpath(candidate);
if (!visited.has(directory)) {
visited.add(directory);
await reclaimInstances(directory, recordFailure, true);
}
} catch (error) {
if (!hasErrnoCode(error, "ENOENT")) recordFailure(error);
}
}).then(() => {
if (failures === 0) {
warningBackoff.delete(root);
return;
}
const now = Date.now();
const previous = warningBackoff.get(root);
if (previous && now < previous.next) return;
const delay = Math.min((previous?.delay ?? CAPTURE_GRACE_MS / 2) * 2, 24 * CAPTURE_GRACE_MS);
if (!previous && warningBackoff.size >= 32) {
const oldest = warningBackoff.keys().next().value;
if (oldest !== void 0) warningBackoff.delete(oldest);
}
warningBackoff.set(root, {
next: now + delay,
delay
});
warn(`${failures} cleanup failure(s) in ${root}; will retry. First: ${formatErrorMessage(firstFailure)}`);
}).finally(() => sweeps.delete(root));
sweeps.set(root, sweep);
}
return sweep;
}
function scheduleCaptureCleanup(key, instance) {
const scheduler = [...instance.references].findLast((reference) => reference.scheduler && !reference.scheduler.signal.aborted)?.scheduler ?? void 0;
if (instance.scheduler === scheduler) return;
instance.scheduler = scheduler;
instance.cleanupScope?.beginClose();
instance.cleanupScope = void 0;
if (!scheduler || instance.storage.placement === "temporary") return;
const scope = scheduler.scope();
instance.cleanupScope = scope;
scope.signal.addEventListener("abort", () => scheduleCaptureCleanup(key, instance), { once: true });
runInPluginSourceCaptureContext(() => scope.schedule({
id: `plugin-source-captures:${key}`,
delayMs: CAPTURE_GRACE_MS,
everyMs: CAPTURE_GRACE_MS,
run: () => sweepPluginSourceCaptureDirectories(instance.storage.stateDir)
}));
}
resolveGlobalSingleton(Symbol.for("openclaw.pluginRuntimeCloseRetainedError"), () => class RetainedRuntimeError extends Error {
constructor(cause) {
super("Plugin runtime still owns resources; inspect cleanup failures before retrying Gateway close.", { cause });
this.name = "PluginRuntimeCloseRetainedError";
}
});
resolveGlobalSingleton(Symbol.for("openclaw.pluginRuntimeCloseCompletedError"), () => class CompletedRuntimeError extends AggregateError {});
//#endregion
//#region src/plugins/plugin-metadata-lifecycle.ts
const pluginMetadataProcessMemoClears = /* @__PURE__ */ new Map();
resolveGlobalSingleton(Symbol.for("openclaw.gatewayPluginMetadataOwners"), () => /* @__PURE__ */ new Set());
/** Registers a metadata clear hook with the lifetime of its facts. */
function registerPluginMetadataProcessMemoLifecycleClear(clearProcessMemo, options = { owner: "process" }) {
pluginMetadataProcessMemoClears.set(clearProcessMemo, options.owner);
}
//#endregion
//#region src/plugins/installed-plugin-index-row.ts
const INSTALLED_PLUGIN_INDEX_STATE_KEY = "plugins.installedIndex";
/** Shared inspection commands use the same existing-only, artifact-preserving reader. */
function readPluginMetadataStateRowSync(selector, databaseOptions, artifactPreservingReadOnly = false) {
const row = readPluginMetadataStateRowsSync([selector === "installed-index" ? INSTALLED_PLUGIN_INDEX_STATE_KEY : "plugins.bundledDiscovery"], databaseOptions, artifactPreservingReadOnly)[0];
return row ? { value_json: row.value_json } : void 0;
}
/** Acquire related metadata facts from the same prepared database bytes. */
function readPluginMetadataStateRowsSync(stateKeys, databaseOptions, artifactPreservingReadOnly = false) {
const read = ({ db }) => {
if (!tableExists(db, "config_machine_state")) return [];
return executeSqliteQuerySync(db, getNodeSqliteKysely(db).selectFrom("config_machine_state").select(["state_key", "value_json"]).where("state_key", "in", stateKeys)).rows;
};
return (artifactPreservingReadOnly ? withExistingOpenClawStateDatabaseArtifactPreservingReadOnly(read, databaseOptions) : withExistingOpenClawStateDatabaseReadOnly(read, databaseOptions)) ?? [];
}
//#endregion
//#region src/plugins/bundled-discovery-state.ts
function parseBundledDiscoveryMode(value) {
return value === "compat" || value === "allowlist" ? value : void 0;
}
function readBundledDiscoveryFact(read) {
try {
return read();
} catch (error) {
if (isStateDatabaseReadAdmissionInvalidatedError(error)) throw new PluginCacheFactInvalidatedError("Plugin discovery read admission changed during preparation; retry the operation.", { cause: error });
throw error;
}
}
function resolveBundledDiscoveryOptions(options) {
return options.path || options.database || !hasActivePluginInstallRoots() ? options : {
...options,
env: {
...options.env ?? process.env,
OPENCLAW_STATE_DIR: resolveActivePluginInstallRoots(options.env).stateDir
}
};
}
function readBundledDiscoveryMode(options = {}, behavior = {}) {
const resolvedOptions = resolveBundledDiscoveryOptions(options);
return parseBundledDiscoveryMode(readBundledDiscoveryFact(() => readConfigMachineState("plugins.bundledDiscovery", resolvedOptions, behavior)));
}
const discoveryState = resolveGlobalSingleton(Symbol.for("openclaw.bundledDiscoveryMode"), () => ({
generation: {},
snapshotModes: /* @__PURE__ */ new WeakMap()
}));
registerPluginMetadataProcessMemoLifecycleClear(() => {
clearBundledDiscoveryModeMemo();
});
function resolveBundledDiscoveryMemoKey(env) {
const scopedEnv = hasActivePluginInstallRoots() ? {
...env,
OPENCLAW_STATE_DIR: resolveActivePluginInstallRoots(env).stateDir
} : env;
return resolveOpenClawStateSqlitePath(scopedEnv);
}
/**
* Callers loading a registry with an explicit env pass it so the mode comes
* from that env's state root; omitting it reads the process root. Pinned
* install roots win over both, matching readBundledDiscoveryMode.
*/
function readBundledDiscoveryModeMemoized(env = process.env, behavior = {}, readPreparedValue) {
const options = env === process.env ? {} : { env };
const snapshot = behavior.artifactPreservingReadOnly || isArtifactPreservingStateRead() ? readBundledDiscoveryFact(() => getActiveOpenClawStateDatabaseReadSnapshot(resolveBundledDiscoveryOptions(options))) : void 0;
if (snapshot || behavior.artifactPreservingReadOnly) {
const prepared = snapshot && discoveryState.snapshotModes.get(snapshot);
if (prepared) return prepared.value;
const value = readBundledDiscoveryMode(options, behavior);
if (snapshot) discoveryState.snapshotModes.set(snapshot, { value });
return value;
}
const key = resolveBundledDiscoveryMemoKey(env);
if (discoveryState.memoized?.key !== key) {
const owner = getPluginCache();
const prepared = owner.preparedBundledDiscoveryModes.get(key);
if (prepared && "value" in prepared && prepared.value.generation === discoveryState.generation) {
getPluginCacheRetirementSignal(owner).throwIfAborted();
discoveryState.memoized = {
key,
value: prepared.value.value
};
} else discoveryState.memoized = {
key,
value: readPreparedValue ? parseBundledDiscoveryMode(readBundledDiscoveryFact(() => readPreparedValue(key))) : readBundledDiscoveryMode(env === process.env ? {} : { env })
};
}
return discoveryState.memoized.value;
}
/**
* Clears the memo after a machine-state write so same-process readers observe
* the new mode. Without this, doctor's migration could cache the pre-migration
* absent mode and rebuild plugin indexes against stale strict-gate decisions.
*/
function clearBundledDiscoveryModeMemo() {
discoveryState.memoized = void 0;
discoveryState.snapshotModes = /* @__PURE__ */ new WeakMap();
discoveryState.generation = {};
for (const cache of /* @__PURE__ */ new Set([getPluginCache(), getProcessPluginCache()])) cache.preparedBundledDiscoveryModes.clear();
}
//#endregion
//#region src/plugins/compat/agent-list-runtime-projection-records.ts
const AGENT_LIST_RUNTIME_PROJECTION_COMPAT_RECORDS = [{
code: "agent-list-runtime-projection",
status: "deprecated",
owner: "sdk",
introduced: "2026-07-27",
deprecated: "2026-10-02",
warningStarts: "2026-10-02",
removeAfter: "2027-01-02",
replacement: "Canonical `cfg.agents.entries`, or `listAgentIds` / `resolveAgentConfig` from `openclaw/plugin-sdk/agent-runtime`",
docsPath: "/plugins/sdk-migration/how-to-migrate#agent-roster-config",
surfaces: ["non-enumerable runtime OpenClawConfig.agents.list projection"],
diagnostics: ["plugin compatibility registry and migration documentation; no runtime warnings"],
tests: ["src/config/runtime-overrides.test.ts", "src/plugins/compat/registry.test.ts"],
releaseNote: "The runtime agents.list projection introduced in #113146 remains untyped compatibility for plugins built against stable SDK releases through 2026.9.x; authored configs use agents.entries without default markers."
}];
//#endregion
//#region src/plugins/compat/deprecation-marking.ts
const MARKING_DATE = "2026-07-25";
const DEPRECATION_MARKING = {
status: "removal-pending",
introduced: MARKING_DATE,
deprecated: MARKING_DATE,
warningStarts: MARKING_DATE,
removeAfter: "2026-10-01"
};
/** The elapsed window does not replace each family's required reader-migration proof. */
const DEPRECATION_MARKING_COMPAT_RECORDS = [
{
code: "plugin-sdk-channel-setup-input-fields",
...DEPRECATION_MARKING,
owner: "channel",
replacement: "plugin-local setup input intersections that declare each owning channel field; retain each field until a new published-plugin artifact sweep finds no reader",
docsPath: "/plugins/sdk-migration#published-channel-setup-compatibility",
surfaces: [
"ChannelSetupInput.privateKey",
"ChannelSetupInput.secret",
"ChannelSetupInput.botToken",
"ChannelSetupInput.appToken",
"ChannelSetupInput.signingSecret",
"ChannelSetupInput.mode",
"ChannelSetupInput.cliPath",
"ChannelSetupInput.authDir",
"ChannelSetupInput.httpUrl",
"ChannelSetupInput.httpPort",
"ChannelSetupInput.webhookPath",
"ChannelSetupInput.webhookUrl",
"ChannelSetupInput.userId",
"ChannelSetupInput.accessToken",
"ChannelSetupInput.password",
"ChannelSetupInput.deviceName",
"ChannelSetupInput.url",
"ChannelSetupInput.baseUrl",
"ChannelSetupInput.code",
"ChannelSetupInput.groupChannels",
"ChannelSetupInput.dmAllowlist",
"ChannelSetupInput.autoDiscoverChannels"
],
diagnostics: ["TypeScript @deprecated annotations on the reader-backed ChannelSetupInput compatibility tier", "published-plugin artifact reader sweep required before field removal"],
tests: ["src/plugin-sdk/channel-setup.test.ts", "src/plugins/compat/registry.test.ts"],
releaseNote: "ChannelSetupInput keeps its reader-backed channel fields through the dated migration window while plugins move them into plugin-local input types."
},
{
code: "plugin-sdk-broad-runtime-barrels",
...DEPRECATION_MARKING,
owner: "sdk",
replacement: "focused plugin SDK subpaths for each runtime capability; retain until bundled and published plugins no longer import any of the seven broad barrels",
docsPath: "/plugins/sdk-migration#compatibility-policy",
surfaces: [
"openclaw/plugin-sdk/agent-runtime",
"openclaw/plugin-sdk/agent-runtime loadModelCatalog params.useCache",
"openclaw/plugin-sdk/agent-runtime loadModelCatalog params.cacheOnly",
"openclaw/plugin-sdk/agent-runtime loadModelCatalog params.metadataSnapshot",
"openclaw/plugin-sdk/agent-runtime loadModelCatalog",
"openclaw/plugin-sdk/cli-runtime",
"openclaw/plugin-sdk/conversation-runtime",
"openclaw/plugin-sdk/hook-runtime",
"openclaw/plugin-sdk/media-runtime",
"openclaw/plugin-sdk/media-runtime buildAgentMediaPayload",
"openclaw/plugin-sdk/plugin-runtime",
"openclaw/plugin-sdk/security-runtime"
],
diagnostics: ["TypeScript @deprecated annotations on broad plugin SDK barrels", "plugin boundary report compatibility inventory"],
tests: ["src/plugins/contracts/plugin-sdk-subpaths.test.ts", "src/plugins/compat/registry.test.ts"],
releaseNote: "Broad agent, CLI, conversation, hook, media, plugin, and security runtime barrels remain available while bundled and external plugins migrate to focused subpaths."
},
{
code: "plugin-sdk-provider-owned-helper-shims",
...DEPRECATION_MARKING,
owner: "provider",
replacement: "provider-local auth, model, replay, OAuth, and stream helper APIs; retain until every helper is migrated in official providers and absent from published plugins",
docsPath: "/plugins/sdk-migration#compatibility-policy",
surfaces: [
"openclaw/plugin-sdk/provider-stream MOONSHOT_THINKING_STREAM_HOOKS",
"openclaw/plugin-sdk/provider-stream-shared defaultToolStreamExtraParams",
"openclaw/plugin-sdk/provider-stream-shared stripTrailingAnthropicAssistantPrefillWhenThinking",
"openclaw/plugin-sdk/provider-stream-shared createAnthropicThinkingPrefillPayloadWrapper",
"openclaw/plugin-sdk/provider-stream-shared OpenAICompatibleThinkingLevel",
"openclaw/plugin-sdk/provider-stream-shared isOpenAICompatibleThinkingEnabled",
"openclaw/plugin-sdk/provider-stream-shared DeepSeekV4ThinkingLevel",
"openclaw/plugin-sdk/provider-stream-shared DeepSeekV4ReasoningEffort",
"openclaw/plugin-sdk/provider-stream-shared createDeepSeekV4OpenAICompatibleThinkingWrapper",
"openclaw/plugin-sdk/provider-stream-shared createThinkingOnlyFinalTextWrapper",
"openclaw/plugin-sdk/provider-stream-shared createGoogleThinkingPayloadWrapper",
"openclaw/plugin-sdk/provider-stream-shared createGoogleThinkingStreamWrapper",
"openclaw/plugin-sdk/provider-model-shared isProxyReasoningUnsupportedModelHint",
"openclaw/plugin-sdk/provider-model-shared NATIVE_ANTHROPIC_REPLAY_HOOKS",
"openclaw/plugin-sdk/provider-model-shared PASSTHROUGH_GEMINI_REPLAY_HOOKS",
"openclaw/plugin-sdk/provider-auth toFormUrlEncoded",
"openclaw/plugin-sdk/provider-auth generatePkceVerifierChallenge",
"openclaw/plugin-sdk/provider-oauth-runtime OAuthProvider",
"openclaw/plugin-sdk/provider-oauth-runtime OAuthProviderInfo"
],
diagnostics: ["TypeScript @deprecated annotations naming provider-local replacements", "plugin boundary report compatibility inventory"],
tests: ["src/plugins/contracts/plugin-sdk-subpaths.test.ts", "src/plugins/compat/registry.test.ts"],
releaseNote: "Provider-specific auth, model, replay, OAuth, and stream shortcuts remain as deprecated SDK shims while providers move to their local APIs."
},
{
code: "message-presentation-legacy-bridges",
...DEPRECATION_MARKING,
owner: "channel",
replacement: "MessagePresentation values and channel presentation renderers; retain until reply producers and official channel packages no longer emit or read legacy interactive replies",
docsPath: "/plugins/sdk-migration#compatibility-policy",
surfaces: [
"InteractiveReplyButton.value",
"InteractiveReplyButton.url",
"InteractiveReplyButton.webApp",
"InteractiveReplyButton.web_app",
"InteractiveReplyOption.value",
"InteractiveReplyButton",
"InteractiveReplyOption",
"InteractiveReplyBlock",
"InteractiveReply",
"normalizeInteractiveReply",
"hasInteractiveReplyBlocks",
"presentationToInteractiveReply",
"presentationToInteractiveControlsReply",
"interactiveReplyToPresentation",
"resolveInteractiveTextFallback",
"src/auto-reply ReplyPayload.interactive",
"openclaw/plugin-sdk/reply-payload ReplyPayload.interactive",
"reduceInteractiveReply",
"@openclaw/discord buildDiscordInteractiveComponents",
"@openclaw/slack buildSlackInteractiveBlocks"
],
diagnostics: ["TypeScript @deprecated annotations naming MessagePresentation replacements", "plugin boundary report compatibility inventory"],
tests: [
"src/interactive/payload.test.ts",
"src/plugin-sdk/reply-payload.test.ts",
"src/plugins/compat/registry.test.ts"
],
releaseNote: "Legacy interactive reply values and channel-specific rendering bridges remain available while producers migrate to MessagePresentation."
},
{
code: "plugin-sdk-focused-compat-aliases",
...DEPRECATION_MARKING,
owner: "sdk",
replacement: "the focused replacement named by each TypeScript @deprecated annotation; retain until every enumerated alias has zero bundled and published readers",
docsPath: "/plugins/sdk-migration#compatibility-policy",
surfaces: [
"openclaw/plugin-sdk/acp-runtime __testing",
"openclaw/plugin-sdk/channel-inbound BuildChannelTurnContextParams",
"openclaw/plugin-sdk/channel-inbound BuiltChannelTurnContext",
"openclaw/plugin-sdk/channel-inbound buildChannelTurnContext",
"openclaw/plugin-sdk/channel-inbound finalizeChannelInboundContext",
"openclaw/plugin-sdk/channel-inbound filterChannelTurnSupplementalContext",
"openclaw/plugin-sdk/channel-send-result ChannelSendRawResult",
"openclaw/plugin-sdk/keyed-async-queue KeyedAsyncQueue.getTailMapForTesting",
"openclaw/plugin-sdk/persistent-dedupe PersistentDedupeLegacyPathOptions.lockOptions",
"openclaw/plugin-sdk/retry-runtime createTelegramRetryRunner",
"openclaw/plugin-sdk/ssrf-policy SsrfPolicyOptions.allowPrivateNetwork",
"openclaw/plugin-sdk/ssrf-policy ssrfPolicyFromAllowPrivateNetwork",
"openclaw/plugin-sdk/tts-runtime TtsSynthesisStreamResult",
"openclaw/plugin-sdk/tts-runtime TtsRuntimeFacade._test"
],
diagnostics: ["TypeScript @deprecated annotations naming focused replacements", "plugin boundary report compatibility inventory"],
tests: [
"src/plugin-sdk/channel-inbound.test.ts",
"src/plugin-sdk/ssrf-policy.test.ts",
"src/plugins/compat/registry.test.ts"
],
releaseNote: "Focused SDK compatibility aliases remain available through a dated window while callers adopt their annotated replacements."
},
{
code: "agent-harness-terminal-result-aliases",
...DEPRECATION_MARKING,
owner: "agent-runtime",
replacement: "AgentHarnessAttemptResult.terminal and AgentHarnessDeliveryDefaults.visibleReplies; retain until harness migration verifies that legacy terminal fields and sourceVisibleReplies are unread",
docsPath: "/plugins/sdk-agent-harness",
surfaces: [
"AgentHarnessAttemptResult.aborted",
"AgentHarnessAttemptResult.externalAbort",
"AgentHarnessAttemptResult.timedOut",
"AgentHarnessAttemptResult.idleTimedOut",
"AgentHarnessAttemptResult.timedOutDuringCompaction",
"AgentHarnessAttemptResult.timedOutDuringToolExecution",
"AgentHarnessAttemptResult.timedOutByRunBudget",
"AgentHarnessAttemptResult.promptError",
"AgentHarnessAttemptResult.promptErrorSource",
"AgentHarnessDeliveryDefaults.sourceVisibleReplies"
],
diagnostics: ["TypeScript @deprecated annotations on agent harness result and delivery defaults", "plugin boundary report compatibility inventory"],
tests: ["src/agents/harness/settled-turn-finalization-result.test.ts", "src/plugins/compat/registry.test.ts"],
releaseNote: "Agent harness result booleans and sourceVisibleReplies remain available while harnesses migrate to terminal outcomes and visibleReplies."
},
{
code: "official-plugin-export-aliases",
...DEPRECATION_MARKING,
owner: "channel",
replacement: "MessagePresentation renderers and host-owned timeout/runtime behavior; retain until minimum supported official plugin packages no longer import these aliases",
docsPath: "/plugins/compatibility#current-compatibility-areas",
surfaces: [
"@openclaw/discord buildDiscordInteractiveComponents",
"@openclaw/discord normalizeDiscordListenerTimeoutMs",
"@openclaw/discord normalizeDiscordInboundWorkerTimeoutMs",
"@openclaw/discord isAbortError",
"@openclaw/discord runDiscordTaskWithTimeout",
"@openclaw/slack buildSlackInteractiveBlocks"
],
diagnostics: ["TypeScript @deprecated annotations on published official-plugin exports", "plugin boundary report compatibility inventory"],
tests: ["src/plugins/compat/registry.test.ts"],
releaseNote: "Published channel presentation and Discord timeout aliases remain available while consumers move to their canonical exports and host-owned behavior."
},
{
code: "memory-host-compatibility-aliases",
...DEPRECATION_MARKING,
owner: "sdk",
replacement: "canonical memory cache/FTS tables; retain until supported memory integrations are verified to use canonical tables without overrides and legacy table data remains preserved",
docsPath: "/plugins/sdk-migration#compatibility-policy",
surfaces: ["@openclaw/memory-host-sdk ensureMemoryIndexSchema.embeddingCacheTable", "@openclaw/memory-host-sdk ensureMemoryIndexSchema.ftsTable"],
diagnostics: ["TypeScript @deprecated annotations on memory-host SDK compatibility fields", "plugin boundary report memory-host SDK summary"],
tests: ["packages/memory-host-sdk/src/host/memory-schema.test.ts", "src/plugins/compat/registry.test.ts"],
releaseNote: "Memory-host table overrides remain available until supported artifact usage and legacy data preservation are verified."
},
{
code: "plugin-runtime-api-compat-aliases",
...DEPRECATION_MARKING,
owner: "plugin-execution",
replacement: "the namespaced plugin API and focused runtime methods named per surface; retain until all enumerated flat API and runtime aliases have no readers",
docsPath: "/plugins/sdk-migration#compatibility-policy",
surfaces: [
"OpenClawPluginApi.registerSessionExtension",
"OpenClawPluginApi.enqueueNextTurnInjection",
"OpenClawPluginApi.registerControlUiDescriptor",
"OpenClawPluginApi.registerRuntimeLifecycle",
"OpenClawPluginApi.registerAgentEventSubscription",
"OpenClawPluginApi.emitAgentEvent",
"OpenClawPluginApi.setRunContext",
"OpenClawPluginApi.getRunContext",
"OpenClawPluginApi.clearRunContext",
"OpenClawPluginApi.registerSessionSchedulerJob",
"OpenClawPluginApi.registerSessionAction",
"OpenClawPluginApi.sendSessionAttachment",
"OpenClawPluginApi.scheduleSessionTurn",
"OpenClawPluginApi.unscheduleSessionTurnsByTag",
"PluginHookContext.senderExternalId",
"PluginAttachmentChannelHints.telegram",
"PluginAttachmentChannelHints.slack",
"AgentPromptSurfaceKind pi_main",
"PluginRuntime.channel.reply.createReplyDispatcherWithTyping",
"PluginRuntime.channel.reply.resolveHumanDelayConfig",
"PluginRuntime.channel.reply.dispatchReplyFromConfig",
"PluginRuntime.channel.reply.finalizeInboundContext",
"PluginRuntime.channel.media.fetchRemoteMedia",
"PluginRuntime.channel.session.resolveStorePath",
"PluginRuntime.channel.session.recordInboundSession",
"PluginRuntime.channel.inbound.runPreparedReply",
"PluginRuntime.channel.turn",
"PluginRuntime.system.requestHeartbeatNow"
],
diagnostics: ["TypeScript @deprecated annotations on plugin API and runtime aliases", "plugin boundary report compatibility inventory"],
tests: [
"src/plugins/captured-registration.test.ts",
"src/plugins/runtime/index.test.ts",
"src/plugins/compat/registry.test.ts"
],
releaseNote: "Flat plugin registration and broad runtime aliases remain available while plugins migrate to namespaced APIs and focused runtime methods."
},
{
code: "plugin-provider-manifest-compat-aliases",
...DEPRECATION_MARKING,
owner: "provider",
replacement: "manifest-owned plugin kind/setup metadata and model catalog registration; retain until providers no longer publish runtime kind or legacy catalog hooks",
docsPath: "/plugins/sdk-migration#compatibility-policy",
surfaces: [
"DefinePluginEntryOptions.kind",
"SingleProviderPluginOptions.kind",
"OpenClawPluginDefinition.kind",
"PluginPackageChannel.cliAddOptions",
"ProviderPlugin.catalog",
"ProviderPlugin.staticCatalog",
"ProviderPlugin.suppressBuiltInModel",
"ProviderPlugin.augmentModelCatalog",
"ProviderBuiltInModelSuppressionContext"
],
diagnostics: ["TypeScript @deprecated annotations on plugin manifest and provider catalog aliases", "plugin boundary report compatibility inventory"],
tests: [
"src/plugins/contracts/package-manifest.contract.test.ts",
"src/plugins/contracts/provider-catalog-deprecation.contract.test.ts",
"src/plugins/compat/registry.test.ts"
],
releaseNote: "Runtime plugin kind/setup metadata and provider catalog hooks remain available while plugins migrate ownership into manifests and catalog registrations."
}
];
//#endregion
//#region src/plugins/compat/plugin-sdk-subpath-records.ts
const PLUGIN_SDK_SUBPATH_SEEDS = [
{
code: "plugin-sdk-channel-streaming-subpath",
subpath: "channel-streaming",
status: "removed",
owner: "channel",
replacement: "`openclaw/plugin-sdk/channel-outbound`",
releaseNote: "The deprecated `channel-streaming` Plugin SDK subpath was removed; plugins now import channel streaming helpers from `channel-outbound`."
},
{
code: "plugin-sdk-config-runtime-subpath",
subpath: "config-runtime",
status: "removed",
owner: "config",
replacement: "`api.pluginConfig`, `openclaw/plugin-sdk/config-mutation`, `openclaw/plugin-sdk/runtime-config-snapshot`, and `openclaw/plugin-sdk/config-contracts`",
releaseNote: "The deprecated `config-runtime` Plugin SDK facade was removed by the approved September 30 retirement; migrate to `api.pluginConfig`, `openclaw/plugin-sdk/config-mutation`, `openclaw/plugin-sdk/runtime-config-snapshot`, and `openclaw/plugin-sdk/config-contracts`."
},
{
code: "plugin-sdk-inbound-reply-dispatch-subpath",
subpath: "inbound-reply-dispatch",
owner: "channel",
removalGate: "next-plugin-sdk-major",
replacement: "`openclaw/plugin-sdk/channel-inbound` and `openclaw/plugin-sdk/channel-outbound`"
},
{
code: "plugin-sdk-channel-reply-pipeline-subpath",
subpath: "channel-reply-pipeline",
status: "removed",
owner: "channel",
replacement: "`openclaw/plugin-sdk/channel-outbound`",
releaseNote: "The deprecated `channel-reply-pipeline` Plugin SDK facade was removed by the approved September 30 retirement; migrate to `openclaw/plugin-sdk/channel-outbound`."
},
{
code: "plugin-sdk-infra-runtime-subpath",
subpath: "infra-runtime",
status: "removed",
owner: "sdk",
replacement: "focused typed subpaths including `openclaw/plugin-sdk/system-event-runtime`, `openclaw/plugin-sdk/error-runtime`, `openclaw/plugin-sdk/approval-runtime`, and `openclaw/plugin-sdk/ssrf-runtime`; use injected runtime APIs for host operations without a public subpath",
releaseNote: "The deprecated `infra-runtime` Plugin SDK facade was removed by the approved September 30 retirement; migrate to focused typed subpaths including `openclaw/plugin-sdk/system-event-runtime`, `openclaw/plugin-sdk/error-runtime`, `openclaw/plugin-sdk/approval-runtime`, and `openclaw/plugin-sdk/ssrf-runtime`; use injected runtime APIs for host operations without a public subpath."
},
{
code: "plugin-sdk-text-runtime-subpath",
subpath: "text-runtime",
status: "removed",
owner: "sdk",
replacement: "`openclaw/plugin-sdk/logging-core`, `openclaw/plugin-sdk/text-chunking`, `openclaw/plugin-sdk/text-utility-runtime`, and `openclaw/plugin-sdk/string-coerce-runtime`",
releaseNote: "The deprecated `text-runtime` Plugin SDK facade was removed; plugins now import logging, chunking, text utility, and string coercion helpers from their focused subpaths."
},
{
code: "plugin-sdk-channel-secret-runtime-subpath",
subpath: "channel-secret-runtime",
status: "removed",
owner: "channel",
replacement: "`openclaw/plugin-sdk/channel-secret-basic-runtime` and `openclaw/plugin-sdk/channel-secret-tts-runtime`",
releaseNote: "The deprecated `channel-secret-runtime` Plugin SDK subpath was removed; plugins now use the focused basic and TTS secret-runtime subpaths."
},
{
code: "plugin-sdk-agent-config-primitives-subpath",
subpath: "agent-config-primitives",
status: "removed",
owner: "config",
replacement: "`openclaw/plugin-sdk/channel-config-schema`",
releaseNote: "The deprecated `agent-config-primitives` Plugin SDK subpath was removed; plugins now use maintained config-schema primitives."
},
{
code: "plugin-sdk-matrix-subpath",
subpath: "matrix",
status: "removed",
owner: "channel",
replacement: "`openclaw/plugin-sdk/run-command`",
releaseNote: "The deprecated `matrix` Plugin SDK facade was removed; command execution now uses the generic `run-command` subpath."
},
{
code: "plugin-sdk-channel-logging-subpath",
subpath: "channel-logging",
status: "removed",
owner: "channel",
replacement: "`openclaw/plugin-sdk/channel-inbound` and `openclaw/plugin-sdk/channel-outbound`",
releaseNote: "The deprecated `channel-logging` Plugin SDK subpath was removed; channel logging helpers now come from the inbound and outbound channel surfaces."
},
{
code: "plugin-sdk-channel-lifecycle-subpath",
subpath: "channel-lifecycle",
status: "removed",
owner: "channel",
replacement: "`openclaw/plugin-sdk/channel-outbound`",
releaseNote: "The deprecated `channel-lifecycle` Plugin SDK facade was removed by the approved September 30 retirement; migrate to `openclaw/plugin-sdk/channel-outbound`."
},
{
code: "plugin-sdk-channel-message-subpath",
subpath: "channel-message",
status: "removed",
owner: "channel",
replacement: "`openclaw/plugin-sdk/channel-outbound` and `openclaw/plugin-sdk/channel-inbound`",
releaseNote: "The deprecated `channel-message` Plugin SDK facade was removed by the approved September 30 retirement; migrate to `openclaw/plugin-sdk/channel-outbound` and `openclaw/plugin-sdk/channel-inbound`."
},
{
code: "plugin-sdk-group-access-subpath",
subpath: "group-access",
status: "removed",
owner: "channel",
replacement: "`openclaw/plugin-sdk/channel-ingress-runtime`",
releaseNote: "The deprecated `group-access` Plugin SDK subpath was removed; plugins now resolve message admission through `channel-ingress-runtime`."
},
{
code: "plugin-sdk-zod-subpath",
subpath: "zod",
status: "removed",
owner: "sdk",
replacement: "the direct `zod` package import",
releaseNote: "The deprecated `zod` Plugin SDK re-export was removed; plugins now import `zod` directly."
}
];
function buildPluginSdkSubpathRecord(seed) {
const shared = {
code: seed.code,
owner: seed.owner,
introduced: "2026-07-06",
replacement: seed.replacement,
docsPath: "/plugins/sdk-migration",
surfaces: [`openclaw/plugin-sdk/${seed.subpath}`],
tests: ["src/plugins/compat/registry.test.ts"]
};
if ("status" in seed && seed.status === "removed") return {
...shared,
status: seed.status,
diagnostics: ["plugin SDK compatibility registry and migration guide"],
releaseNote: seed.releaseNote
};
return {
...shared,
status: "status" in seed ? seed.status : "deprecated",
deprecated: "2026-07-06",
warningStarts: "2026-07-06",
removalGate: "removalGate" in seed ? seed.removalGate : void 0,
diagnostics: ["repository deprecated API usage guard for core and bundled plugins; no external runtime import warning"]
};
}
const PLUGIN_SDK_SUBPATH_RECORDS = PLUGIN_SDK_SUBPATH_SEEDS.map(buildPluginSdkSubpathRecord);
const BUNDLED_ONLY_PUBLIC_PLUGIN_SDK_SUBPATH_SEEDS = [
{
subpath: "media-understanding",
status: "removal-pending",
removeAfter: "2026-09-30",
replacement: "`api.registerMediaUnderstandingProvider(...)` with provider-owned request helpers and types from `openclaw/plugin-sdk/plugin-entry`; retain the public subpath through the 2026-09-30 window while official plugin consumers migrate",
docsPath: "/plugins/architecture"
},
{
subpath: "memory-host-core",
status: "removal-pending",
removeAfter: "2026-09-30",
replacement: "host-prepared memory prompts via `openclaw/plugin-sdk/core` and memory capability registration through the injected plugin API; retain the facade through the 2026-09-30 window and until a focused public-artifact read seam exists",
docsPath: "/plugins/architecture-internals#context-engine-plugins"
},
{
subpath: "plugin-config-runtime",
status: "removal-pending",
removeAfter: "2026-12-01",
replacement: "`api.pluginConfig`, runtime tool context config, and focused `config-contracts`, `runtime-config-snapshot`, or `config-mutation` subpaths; retain the public subpath through the 2026-12-01 window while official plugin consumers migrate",
docsPath: "/plugins/sdk-runtime"
},
{
subpath: "tool-plugin",
status: "deprecated",
replacement: "retain the public subpath until plugin authoring has a nonexecuting static metadata replacement for `defineToolPlugin`; `getToolPluginMetadata` currently reads metadata only from an already-executed entry",
docsPath: "/plugins/tool-plugins"
}
];
function buildPublicSdkSubpathRecord({ subpath, ...compat }) {
return {
code: `plugin-sdk-${subpath}-public-demotion`,
owner: "sdk",
introduced: "2026-07-15",
deprecated: "2026-07-15",
warningStarts: "2026-07-15",
...compat,
surfaces: [`openclaw/plugin-sdk/${subpath}`],
diagnostics: ["registry-backed public SDK demotion window; no external runtime import warning"],
tests: ["src/plugins/compat/registry.test.ts"]
};
}
const BUNDLED_ONLY_PUBLIC_PLUGIN_SDK_SUBPATH_RECORDS = BUNDLED_ONLY_PUBLIC_PLUGIN_SDK_SUBPATH_SEEDS.map(buildPublicSdkSubpathRecord);
//#endregion
//#region src/plugins/compat/session-persistence-records.ts
const SESSION_PERSISTENCE_COMPAT_RECORDS = [
{
code: "native-session-generation-sync-authority",
status: "deprecated",
owner: "agent-runtime",
introduced: "2026-09-22",
deprecated: "2026-10-03",
warningStarts: "2026-10-03",
removalGate: "next-plugin-sdk-major",
replacement: "Await prepareNativeSessionGenerationAuthority, resolveNativeSessionBindingWithAuthority, and reclaimNativeSessionGenerationWithAuthority with NativeSessionGenerationOperationsV2. Retain released synchronous capture and two-argument mutation callbacks until published official harness readers migrate and a breaking release is explicitly approved.",
docsPath: "/plugins/sdk-migration/compatibility-policy#native-session-generation-authority",
surfaces: [
"captureNativeSessionGenerationAuthority",
"resolveNativeSessionBinding",
"reclaimNativeSessionGeneration",
"NativeSessionGenerationOperations"
],
diagnostics: ["TypeScript @deprecated annotations and migration documentation; no runtime warnings"],
tests: [
"src/plugin-sdk/agent-harness-session-compat.test.ts",
"src/agents/harness/native-session/binding-generation.test.ts",
"src/agents/harness/native-session/binding-generation-authority.test.ts",
"src/plugins/compat/registry.test.ts"
],
releaseNote: "Native harnesses can admit binding operations through worker-backed session authority. Released official harness plugins retain synchronous authority capture and lineage-checking mutation callbacks across host upgrades."
},
{
code: "session-manager-sync-persistence",
status: "deprecated",
owner: "sdk",
introduced: "2026-10-01",
deprecated: "2026-10-01",
warningStarts: "2026-10-01",
removalGate: "next-plugin-sdk-major",
replacement: "Await the matching Async-suffixed SessionManager method, including the returned rewrite commit. Retain synchronous adapters only for shipped third-party contracts until the next Plugin SDK major and explicit breaking-release approval.",
docsPath: "/plugins/sdk-migration/how-to-migrate#await-session-transcript-persistence",
surfaces: [
"SessionManager.appendMessage",
"SessionManager.appendMessageWithTranscriptAnchor",
"SessionManager.appendCompaction",
"SessionManager.appendResetBoundary",
"SessionManager.appendCustomEntry",
"SessionManager.appendSessionInfo",
"SessionManager.appendCustomMessageEntry",
"SessionManager.appendLeafControl",
"SessionManager.appendLabelChange",
"SessionManager.branch",
"SessionManager.branchWithSummary",
"SessionManager.removeTrailingEntries",
"SessionManager.persist",
"SessionManager.prepareTranscriptRewrite",
"SessionManager.appendMessageToTranscript",
"SessionManager.open",
"SessionManager.openBounded",
"SessionManager.openDetachedBounded",
"SessionManager.openModelContext",
"SessionManager.setSessionTarget",
"SessionManager.reloadPersistedTranscript"
],
diagnostics: ["TypeScript @deprecated annotations naming awaited twins", "one runtime DEP_SESSION_PERSISTENCE warning per method per process"],
tests: [
"src/plugins/compat/registry.test.ts",
"src/agents/sessions/session-manager-async-entries.test.ts",
"src/agents/sessions/session-manager-async-message.test.ts",
"src/agents/sessions/session-manager-maintenance-async.test.ts"
],
releaseNote: "Plugins can await SessionManager transcript mutations through the existing SQLite worker. Synchronous methods retain their shipped return values as deprecated third-party adapters until the next Plugin SDK major; storage formats and update behavior are unchanged."
},
{
code: "extension-session-sync-persistence",
status: "deprecated",
owner: "sdk",
introduced: "2026-10-01",
deprecated: "2026-10-01",
warningStarts: "2026-10-01",
removalGate: "next-plugin-sdk-major",
replacement: "Await ExtensionAPI.appendEntryAsync, setSessionNameAsync, and setLabelAsync, and AgentSession.setSessionNameAsync. Existing synchronous third-party methods retain their void return contract until the next Plugin SDK major and explicit breaking-release approval.",
docsPath: "/plugins/sdk-migration/how-to-migrate#await-extension-session-changes",
surfaces: [
"ExtensionAPI.appendEntry",
"ExtensionAPI.setSessionName",
"ExtensionAPI.setLabel",
"AgentSession.setSessionName"
],
diagnostics: ["TypeScript @deprecated annotations, migration guide, and once-per-method DEP_SESSION_PERSISTENCE warning"],
tests: ["src/plugins/compat/registry.test.ts", "src/agents/sessions/sdk.metadata-admission.test.ts"],
releaseNote: "Extensions can await transcript entries, session names, and labels; the shipped synchronous methods remain third-party compatibility adapters through the next Plugin SDK major."
},
{
code: "provider-replay-sync-persistence",
status: "deprecated",
owner: "provider",
introduced: "2026-10-01",
deprecated: "2026-10-01",
warningStarts: "2026-10-01",
removalGate: "next-plugin-sdk-major",
replacement: "Use ProviderPlugin.sanitizeReplayHistoryAsync with ProviderSanitizeReplayHistoryContextV2 and await ProviderReplaySessionStateV2.appendCustomEntryAsync; use sanitizeGoogleGeminiReplayHistoryAsync for the shared Gemini implementation. Retain legacy third-party contexts and hooks until the next Plugin SDK major and explicit breaking-release approval.",
docsPath: "/plugins/sdk-migration/how-to-migrate#await-provider-replay-metadata",
surfaces: [
"ProviderPlugin.sanitizeReplayHistory",
"ProviderReplaySessionState.appendCustomEntry",
"sanitizeGoogleGeminiReplayHistory"
],
diagnostics: ["TypeScript @deprecated annotations, versioned migration guide, and once-per-method DEP_SESSION_PERSISTENCE warning"],
tests: [
"src/plugins/compat/registry.test.ts",
"src/plugins/provider-replay-helpers.test.ts",
"src/plugins/provider-runtime.test.ts",
"src/plugin-sdk/provider-model-shared.test.ts"
],
releaseNote: "Provider replay hooks can await committed transcript metadata through additive V2 context types. Legacy hooks, context types, and the synchronous Gemini helper remain available for third-party migration through the next Plugin SDK major."
}
];
//#endregion
//#region src/plugins/compat/registry-records.ts
const ACTIVATION_HINT_METADATA = {
status: "active",
owner: "plugin-execution",
introduced: "2026-04-24",
docsPath: "/plugins/manifest",
diagnostics: ["activation plan compat reason"],
tests: ["src/plugins/activation-planner.test.ts"]
};
[
...SESSION_PERSISTENCE_COMPAT_RECORDS,
...AGENT_LIST_RUNTIME_PROJECTION_COMPAT_RECORDS,
...PLUGIN_SDK_SUBPATH_RECORDS,
...BUNDLED_ONLY_PUBLIC_PLUGIN_SDK_SUBPATH_RECORDS,
...DEPRECATION_MARKING_COMPAT_RECORDS,
{ ...ACTIVATION_HINT_METADATA },
{ ...ACTIVATION_HINT_METADATA },
{ ...ACTIVATION_HINT_METADATA },
{ ...ACTIVATION_HINT_METADATA },
{ ...ACTIVATION_HINT_METADATA },
{ ...ACTIVATION_HINT_METADATA },
{ ...ACTIVATION_HINT_METADATA }
];
//#endregion
//#region src/plugins/installed-plugin-index-hash.ts
/** Hashes JSON-serializable data with SHA-256. */
function hashJson(value) {
return sha256Hex(JSON.stringify(value));
}
/** Hashes JSON-like data independently of object property insertion order. */
function hashStableJson(value) {
return sha256Hex(stableStringify(value));
}
//#endregion
//#region src/plugins/installed-plugin-index-policy.ts
/** Hashes config policy inputs that can change installed plugin activation. */
function resolveInstalledPluginIndexPolicyHash(config, env, behavior = {}) {
const normalized = normalizePluginsConfig(config?.plugins);
const channelPolicy = {};
const channels = config?.channels;
if (channels && typeof channels === "object" && !Array.isArray(channels)) {
for (const [channelId, value] of Object.entries(channels)) if (value && typeof value === "object" && !Array.isArray(value)) {
const enabled = value.enabled;
if (typeof enabled === "boolean") channelPolicy[channelId] = enabled;
}
}
return hashJson({
plugins: {
enabled: normalized.enabled,
allow: normalized.allow,
deny: normalized.deny,
bundledDiscovery: readBundledDiscoveryModeMemoized(env, behavior) ?? null,
slots: normalized.slots,
entries: Object.fromEntries(Object.entries(normalized.entries).flatMap(([pluginId, entry]) => typeof entry.enabled === "boolean" ? [[pluginId, entry.enabled]] : []).toSorted(([left], [right]) => left.localeCompare(right)))
},
channels: Object.fromEntries(Object.entries(channelPolicy).toSorted(([left], [right]) => left.localeCompare(right)))
});
}
//#endregion
//#region src/plugins/bundled-load-path-aliases.ts
const PACKAGED_BUNDLED_ROOTS = [path.join("dist", "extensions"), path.join("dist-runtime", "extensions")];
/** Normalizes bundled lookup paths without preserving trailing separators. */
function normalizeBundledLookupPath(targetPath) {
const normalized = path.normalize(targetPath);
const root = path.parse(normalized).root;
let trimmed = normalized;
while (trimmed.length > root.length && (trimmed.endsWith(path.sep) || trimmed.endsWith("/"))) trimmed = trimmed.slice(0, -1);
return trimmed;
}
function findPackagedBundledRoot(localPath) {
const normalized = normalizeBundledLookupPath(localPath);
for (const packagedRoot of PACKAGED_BUNDLED_ROOTS) {
const marker = `${path.sep}${packagedRoot}`;
const markerIndex = normalized.lastIndexOf(marker);
if (markerIndex === -1) continue;
const markerEnd = markerIndex + marker.length;
if (normalized.length !== markerEnd && normalized[markerEnd] !== path.sep) continue;
return {
packageRoot: normalized.slice(0, markerIndex),
bundledRoot: normalized.slice(0, markerEnd)
};
}
return null;
}
/** Builds the legacy extensions root for a packaged bundled plugin root. */
function buildLegacyBundledRootPath(localPath) {
const packaged = findPackagedBundledRoot(localPath);
return packaged ? path.join(packaged.packageRoot, "extensions") : null;
}
//#endregion
//#region src/plugins/bundled-source-overlays.ts
/** Parses Linux mountinfo content into absolute mount points. */
function parseLinuxMountInfoMountPoints(mountInfo) {
const mountPoints = /* @__PURE__ */ new Set();
for (const line of mountInfo.split(/\r?\n/u)) {
const trimmed = line.trim();
if (!trimmed) continue;
const mountPoint = trimmed.split(" ")[4];
if (!mountPoint) continue;
mountPoints.add(path.resolve(decodeMountInfoPath(mountPoint)));
}
return mountPoints;
}
function readLinuxMountPoints() {
const metadata = getPluginCache().metadata;
if (!metadata.discoveryMountPoints) try {
metadata.discoveryMountPoints = parseLinuxMountInfoMountPoints(fsSync.readFileSync("/proc/self/mountinfo", "utf8"));
} catch {
metadata.discoveryMountPoints = /* @__PURE__ */ new Set();
}
return metadata.discoveryMountPoints;
}
function isFilesystemMountPoint(targetPath) {
const target = pluginCacheStatSync(targetPath);
const parent = pluginCacheStatSync(path.dirname(targetPath));
return Boolean(target && parent && (target.dev !== parent.dev || target.ino === parent.ino));
}
function sourceOverlaysDisabled(env) {
const raw = normalizeOptionalLowercaseString(env.OPENCLAW_DISABLE_BUNDLED_SOURCE_OVERLAYS);
return raw === "1" || raw === "true";
}
/** True when a path appears to be a mounted bundled source overlay. */
function isBundledSourceOverlayPath(params) {
const resolved = path.resolve(params.sourcePath);
return (params.mountPoints ?? readLinuxMountPoints()).has(resolved) || isFilesystemMountPoint(resolved);
}
/** Lists source overlay directories that shadow packaged bundled plugin dirs. */
function listBundledSourceOverlayDirs(params) {
if (sourceOverlaysDisabled(params.env ?? process.env) || !params.bundledRoot) return [];
const legacyRoot = buildLegacyBundledRootPath(params.bundledRoot);
if (!legacyRoot || !pluginCacheExistsSync(legacyRoot)) return [];
let entries;
try {
entries = readPluginCacheDirectory(legacyRoot);
} catch {
return [];
}
const mountPoints = params.mountPoints ?? readLinuxMountPoints();
const legacyRootMounted = isBundledSourceOverlayPath({
sourcePath: legacyRoot,
mountPoints
});
const overlayDirs = [];
for (const entry of entries) {
if (!entry.isDirectory()) continue;
const sourceDir = path.join(legacyRoot, entry.name);
if (!pluginCacheExistsSync(path.join(params.bundledRoot, entry.name))) continue;
if (!legacyRootMounted && !isBundledSourceOverlayPath({
sourcePath: sourceDir,
mountPoints
})) continue;
overlayDirs.push(sourceDir);
}
return overlayDirs.toSorted((left, right) => left.localeCompare(right));
}
//#endregion
//#region src/plugins/candidate-install-owner.ts
const PLUGIN_CANDIDATE_INSTALL_OWNER = Symbol.for("openclaw.pluginCandidateInstallOwner");
function recordPluginCandidateInstallOwner(candidate, installOwner, ambiguous = false) {
if (!installOwner && !ambiguous) return candidate;
Object.defineProperty(candidate, PLUGIN_CANDIDATE_INSTALL_OWNER, {
configurable: true,
enumerable: true,
value: ambiguous ? { ambiguous: true } : { installOwner }
});
return candidate;
}
function readPluginCandidateInstallOwner(candidate) {
return candidate[PLUGIN_CANDIDATE_INSTALL_OWNER];
}
function resolvePluginCandidateInstallOwner(candidate) {
return readPluginCandidateInstallOwner(candidate)?.installOwner;
}
function isPluginCandidateInstallOwnerAmbiguous(candidate) {
return readPluginCandidateInstallOwner(candidate)?.ambiguous === true;
}
//#endregion
//#region packages/llm-core/src/model-data.ts
const MODEL_DATA_APIS = [
"openai-completions",
"openai-responses",
"openai-chatgpt-responses",
"anthropic-messages",
"google-generative-ai",
"google-interactions",
"google-vertex",
"github-copilot",
"bedrock-converse-stream",
"ollama",
"pi-messages",
"azure-openai-responses"
];
const MODEL_DATA_THINKING_FORMATS = [
"openai",
"openrouter",
"deepseek",
"together",
"qwen",
"qwen-chat-template",
"zai"
];
const MODEL_DATA_THINKING_LEVELS = [
"off",
"minimal",
"low",
"medium",
"high",
"xhigh",
"max"
];
//#endregion
//#region packages/model-catalog-core/src/model-catalog-types.ts
/** Supported API protocols for model catalog entries. */
const MODEL_CATALOG_APIS = [...MODEL_DATA_APIS];
/** Supported model thinking/reasoning wire formats. */
const MODEL_CATALOG_THINKING_FORMATS = [...MODEL_DATA_THINKING_FORMATS];
/** Narrow a string to a supported model catalog thinking format. */
function isModelCatalogThinkingFormat(value) {
return MODEL_CATALOG_THINKING_FORMATS.includes(value);
}
/** Model-level thinking settings carried by provider catalog metadata. */
const MODEL_CATALOG_THINKING_LEVELS = [...MODEL_DATA_THINKING_LEVELS];
//#endregion
//#region packages/model-catalog-core/src/model-catalog-context-windows.ts
function normalizePositiveInteger$1(value) {
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
}
function normalizeModelCatalogContextWindows(value) {
if (!Array.isArray(value)) return;
const seen = /* @__PURE__ */ new Set();
const contextWindows = value.slice(0, 16).flatMap((entry) => {
if (!isRecord(entry)) return [];
const id = normalizeOptionalString(entry.id) ?? "";
const label = normalizeOptionalString(entry.label) ?? "";
const contextWindow = normalizePositiveInteger$1(entry.contextWindow);
if (!id || !label || contextWindow === void 0 || seen.has(id)) return [];
seen.add(id);
return [{
id,
label,
contextWindow
}];
});
return contextWindows.length > 0 ? contextWindows.toSorted((left, right) => left.contextWindow - right.contextWindow || left.id.localeCompare(right.id)) : void 0;
}
function normalizeModelCatalogContextWindowSelection(value) {
const contextWindows = normalizeModelCatalogContextWindows(value.contextWindows);
const contextWindowDefault = normalizeOptionalString(value.contextWindowDefault);
return contextWindowDefault && contextWindows?.some((option) => option.id === contextWindowDefault) ? {
contextWindows,
contextWindowDefault
} : {};
}
//#endregion
//#region packages/model-catalog-core/src/model-catalog-normalize.ts
const MODEL_CATALOG_INPUTS = /* @__PURE__ */ new Set([
"text",
"image",
"document"
]);
const MODEL_CATALOG_DISCOVERY_MODES = /* @__PURE__ */ new Set([
"static",
"refreshable",
"runtime"
]);
const MODEL_CATALOG_STATUSES = /* @__PURE__ */ new Set([
"available",
"preview",
"deprecated",
"disabled"
]);
const MODEL_CATALOG_API_SET = new Set(MODEL_CATALOG_APIS);
/** Reject object keys that can mutate prototypes when copied into records. */
function isBlockedObjectKey(key) {
return key === "__proto__" || key === "prototype" || key === "constructor";
}
function normalizeModelCatalogThinkingLevelMap(value) {
if (!isRecord(value)) return;
const normalized = {};
for (const level of MODEL_CATALOG_THINKING_LEVELS) {
const mapped = value[level];
if (mapped === null) {
normalized[level] = null;
continue;
}
const normalizedValue = normalizeOptionalString(mapped);
if (normalizedValue !== void 0) normalized[level] = normalizedValue;
}
return Object.keys(normalized).length > 0 ? normalized : void 0;
}
function normalizeStringMap(value) {
if (!isRecord(value)) return;
const normalized = {};
for (const [rawKey, rawValue] of Object.entries(value)) {
const key = normalizeOptionalString(rawKey);
const mapValue = normalizeOptionalString(rawValue) ?? "";
if (key && !isBlockedObjectKey(key) && mapValue) normalized[key] = mapValue;
}
return Object.keys(normalized).length > 0 ? normalized : void 0;
}
function normalizeModelCatalogApi(value) {
const api = normalizeOptionalString(value) ?? "";
return MODEL_CATALOG_API_SET.has(api) ? api : void 0;
}
function normalizeModelCatalogInputs(value) {
const inputs = normalizeTrimmedStringList(value).filter((input) => MODEL_CATALOG_INPUTS.has(input));
return inputs.length > 0 ? inputs : void 0;
}
function normalizePositiveInteger(value) {
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : void 0;
}
function normalizeModelCatalogTieredCost(value) {
if (!Array.isArray(value)) return;
const normalized = [];
for (const entry of value) {
if (!isRecord(entry) || !Array.isArray(entry.range)) continue;
const input = asNonNegativeFiniteNumber(entry.input);
const output = asNonNegativeFiniteNumber(entry.output);
const cacheRead = asNonNegativeFiniteNumber(entry.cacheRead);
const cacheWrite = asNonNegativeFiniteNumber(entry.cacheWrite);
if (input === void 0 || output === void 0 || cacheRead === void 0 || cacheWrite === void 0 || entry.range.length < 1 || entry.range.length > 2) continue;
const rangeValues = entry.range.map((rangeValue) => asNonNegativeFiniteNumber(rangeValue));
if (rangeValues.some((rangeValue) => rangeValue === void 0)) continue;
normalized.push({
input,
output,
cacheRead,
cacheWrite,
range: rangeValues.length === 1 ? [rangeValues[0]] : [rangeValues[0], rangeValues[1]]
});
}
return normalized.length > 0 ? normalized : void 0;
}
function normalizeModelCatalogCost(value) {
if (!isRecord(value)) return;
const cost = {};
for (const field of [
"input",
"output",
"cacheRead",
"cacheWrite"
]) {
const normalized = asNonNegativeFiniteNumber(value[field]);
if (normalized !== void 0) cost[field] = normalized;
}
const tieredPricing = normalizeModelCatalogTieredCost(value.tieredPricing);
if (tieredPricing) cost.tieredPricing = tieredPricing;
return Object.keys(cost).length > 0 ? cost : void 0;
}
function normalizeOpenRouterPrice(value) {
if (!isRecord(value)) return;
const maxPrice = {};
for (const field of [
"prompt",
"completion",
"image",
"audio",
"request"
]) {
const candidate = value[field];
const normalized = normalizeOptionalString(candidate) ?? asFiniteNumber(candidate);
if (normalized !== void 0) maxPrice[field] = normalized;
}
return Object.keys(maxPrice).length > 0 ? maxPrice : void 0;
}
function normalizeOpenRouterMetricPreference(value) {
const numeric = asFiniteNumber(value);
if (numeric !== void 0) return numeric;
if (!isRecord(value)) return;
const normalized = {};
for (const field of [
"p50",
"p75",
"p90",
"p99"
]) {
const cutoff = asFiniteNumber(value[field]);
if (cutoff !== void 0) normalized[field] = cutoff;
}
return Object.keys(normalized).length > 0 ? normalized : void 0;
}
function normalizeOpenRouterSort(value) {
const sort = normalizeOptionalString(value);
if (sort) return sort;
if (!isRecord(value)) return;
const by = normalizeOptionalString(value.by);
const partition = value.partition === null ? null : normalizeOptionalString(value.partition) ?? void 0;
const normalized = {
...by ? { by } : {},
...partition !== void 0 ? { partition } : {}
};
return Object.keys(normalized).length > 0 ? normalized : void 0;
}
function normalizeOpenRouterRouting(value) {
if (!isRecord(value)) return;
const routing = {
...typeof value.allow_fallbacks === "boolean" ? { allow_fallbacks: value.allow_fallbacks } : {},
...typeof value.require_parameters === "boolean" ? { require_parameters: value.require_parameters } : {},
...value.data_collection === "deny" || value.data_collection === "allow" ? { data_collection: value.data_collection } : {},
...typeof value.zdr === "boolean" ? { zdr: value.zdr } : {},
...typeof value.enforce_distillable_text === "boolean" ? { enforce_distillable_text: value.enforce_distillable_text } : {}
};
for (const field of [
"order",
"only",
"ignore",
"quantizations"
]) {
const normalized = normalizeOptionalTrimmedStringList(value[field]);
if (normalized) routing[field] = normalized;
}
const sort = normalizeOpenRouterSort(value.sort);
if (sort) routing.sort = sort;
const maxPrice = normalizeOpenRouterPrice(value.max_price);
if (maxPrice) routing.max_price = maxPrice;
for (const field of ["preferred_min_throughput", "preferred_max_latency"]) {
const normalized = normalizeOpenRouterMetricPreference(value[field]);
if (normalized !== void 0) routing[field] = normalized;
}
return Object.keys(routing).length > 0 ? routing : void 0;
}
function normalizeVercelGatewayRouting(value) {
if (!isRecord(value)) return;
const routing = {};
for (const field of ["only", "order"]) {
const normalized = normalizeOptionalTrimmedStringList(value[field]);
if (normalized) routing[field] = normalized;
}
return Object.keys(routing).length > 0 ? routing : void 0;
}
function normalizeModelCatalogCompat(value) {
if (!isRecord(value)) return;
const compat = {};
for (const field of [
"supportsStore",
"supportsPromptCacheKey",
"supportsDeveloperRole",
"supportsReasoningEffort",
"supportsTemperature",
"supportsInstructions",
"supportsUsageInStreaming",
"supportsTools",
"supportsStrictMode",
"supportsJsonSchemaResponseFormat",
"requiresStringContent",
"strictMessageKeys",
"requiresToolResultName",
"requiresAssistantAfterToolResult",
"requiresThinkingAsText",
"requiresReasoningContentOnAssistantMessages",
"zaiToolStream",
"sendSessionAffinityHeaders",
"sendSessionIdHeader",
"supportsEagerToolInputStreaming",
"supportsLongCacheRetention",
"supportsResponsesContinuation",
"requiresOpenAiAnthropicToolPayload"
]) if (typeof value[field] === "boolean") compat[field] = value[field];
for (const field of ["toolSchemaProfile", "toolCallArgumentsEncoding"]) {
const normalized = normalizeOptionalString(value[field]) ?? "";
if (normalized) compat[field] = normalized;
}
for (const field of [
"visibleReasoningDetailTypes",
"supportedReasoningEfforts",
"unsupportedToolSchemaKeywords"
]) {
const normalized = normalizeTrimmedStringList(value[field]);
if (normalized.length > 0 || field === "supportedReasoningEfforts" && Array.isArray(value[field])) compat[field] = normalized;
}
if (isRecord(value.reasoningEffortMap)) {
const reasoningEffortMap = Object.fromEntries(Object.entries(value.reasoningEffortMap).flatMap(([rawKey, rawMapped]) => {
const key = rawKey.trim();
const mapped = typeof rawMapped === "string" ? rawMapped.trim() : "";
return key && mapped ? [[key, mapped]] : [];
}));
if (Object.keys(reasoningEffortMap).length > 0) compat.reasoningEffortMap = reasoningEffortMap;
}
const codeMode = normalizeOptionalString(value.codeMode) ?? "";
if (codeMode === "preferred" || codeMode === "capable") compat.codeMode = codeMode;
const maxTokensField = normalizeOptionalString(value.maxTokensField) ?? "";
if (maxTokensField === "max_completion_tokens" || maxTokensField === "max_tokens") compat.maxTokensField = maxTokensField;
const thinkingFormat = normalizeOptionalString(value.thinkingFormat) ?? "";
if (isModelCatalogThinkingFormat(thinkingFormat)) compat.thinkingFormat = thinkingFormat;
if (value.cacheControlFormat === "anthropic") compat.cacheControlFormat = "anthropic";
const openRouterRouting = normalizeOpenRouterRouting(value.openRouterRouting);
if (openRouterRouting) compat.openRouterRouting = openRouterRouting;
const vercelGatewayRouting = normalizeVercelGatewayRouting(value.vercelGatewayRouting);
if (vercelGatewayRouting) compat.vercelGatewayRouting = vercelGatewayRouting;
return Object.keys(compat).length > 0 ? compat : void 0;
}
function normalizeModelCatalogStatus(value) {
const status = normalizeOptionalString(value) ?? "";
return MODEL_CATALOG_STATUSES.has(status) ? status : void 0;
}
function normalizeModelCatalogImageTokenMode(value) {
const tokenMode = normalizeOptionalString(value) ?? "";
if (tokenMode === "tile" || tokenMode === "detail" || tokenMode === "provider") return tokenMode;
}
function normalizeModelCatalogMediaInput(value) {
if (!isRecord(value) || !isRecord(value.image)) return;
const normalizedImage = {};
for (const field of [
"maxBytes",
"maxPixels",
"maxSidePx",
"preferredSidePx"
]) {
const normalized = normalizePositiveInteger(value.image[field]);
if (normalized !== void 0) normalizedImage[field] = normalized;
}
const tokenMode = normalizeModelCatalogImageTokenMode(value.image.tokenMode);
if (tokenMode) normalizedImage.tokenMode = tokenMode;
return Object.keys(normalizedImage).length > 0 ? { image: normalizedImage } : void 0;
}
function normalizeModelCatalogModel(value) {
if (!isRecord(value)) return;
const id = normalizeOptionalString(value.id) ?? "";
if (!id) return;
const name = normalizeOptionalString(value.name) ?? "";
const api = normalizeModelCatalogApi(value.api);
const baseUrl = normalizeOptionalString(value.baseUrl) ?? "";
const headers = normalizeStringMap(value.headers);
const input = normalizeModelCatalogInputs(value.input);
const reasoning = typeof value.reasoning === "boolean" ? value.reasoning : void 0;
const contextWindow = asPositiveFiniteNumber(value.contextWindow);
const contextWindowSelection = normalizeModelCatalogContextWindowSelection(value);
const contextTokens = normalizePositiveInteger(value.contextTokens);
const maxTokens = asPositiveFiniteNumber(value.maxTokens);
const thinkingLevelMap = normalizeModelCatalogThinkingLevelMap(value.thinkingLevelMap);
const cost = normalizeModelCatalogCost(value.cost);
const compat = normalizeModelCatalogCompat(value.compat);
const mediaInput = normalizeModelCatalogMediaInput(value.mediaInput);
const status = normalizeModelCatalogStatus(value.status);
const statusReason = normalizeOptionalString(value.statusReason) ?? "";
const replaces = normalizeTrimmedStringList(value.replaces);
const replacedBy = normalizeOptionalString(value.replacedBy) ?? "";
const tags = normalizeTrimmedStringList(value.tags);
return {
id,
...name ? { name } : {},
...api ? { api } : {},
...baseUrl ? { baseUrl } : {},
...headers ? { headers } : {},
...input ? { input } : {},
...reasoning !== void 0 ? { reasoning } : {},
...contextWindow !== void 0 ? { contextWindow } : {},
...contextWindowSelection,
...contextTokens !== void 0 ? { contextTokens } : {},
...maxTokens !== void 0 ? { maxTokens } : {},
...thinkingLevelMap ? { thinkingLevelMap } : {},
...cost ? { cost } : {},
...compat ? { compat } : {},
...mediaInput ? { mediaInput } : {},
...status ? { status } : {},
...statusReason ? { statusReason } : {},
...replaces.length > 0 ? { replaces } : {},
...replacedBy ? { replacedBy } : {},
...tags.length > 0 ? { tags } : {}
};
}
function normalizeModelCatalogProvider(value) {
if (!isRecord(value)) return;
const models = Array.isArray(value.models) ? value.models.map((entry) => normalizeModelCatalogModel(entry)).filter((entry) => Boolean(entry)) : [];
if (models.length === 0) return;
const baseUrl = normalizeOptionalString(value.baseUrl) ?? "";
const api = normalizeModelCatalogApi(value.api);
const headers = normalizeStringMap(value.headers);
const defaultModel = normalizeOptionalString(value.defaultModel) ?? "";
const defaultUtilityModel = normalizeOptionalString(value.defaultUtilityModel) ?? "";
const recommended = Array.isArray(value.recommendedModels) ? value.recommendedModels.map(normalizeOptionalString) : [];
const recommendedModels = recommended.length > 0 && new Set(recommended).size === recommended.length && recommended.every((id) => Boolean(id) && models.some((model) => model.id === id)) ? recommended : [];
return {
...baseUrl ? { baseUrl } : {},
...api ? { api } : {},
...headers ? { headers } : {},
...defaultModel ? { defaultModel } : {},
...defaultUtilityModel ? { defaultUtilityModel } : {},
...recommendedModels.length > 0 ? { recommendedModels } : {},
models
};
}
function normalizeModelCatalogProviders(value, ownedProviders) {
if (!isRecord(value)) return;
const providers = {};
for (const [rawProviderId, rawProvider] of Object.entries(value)) {
const providerId = normalizeProviderId(rawProviderId);
if (!providerId || !ownedProviders.has(providerId)) continue;
const provider = normalizeModelCatalogProvider(rawProvider);
if (provider) providers[providerId] = provider;
}
return Object.keys(providers).length > 0 ? providers : void 0;
}
function normalizeModelCatalogAliases(value, ownedProviders) {
if (!isRecord(value)) return;
const aliases = {};
for (const [rawAlias, rawTarget] of Object.entries(value)) {
const alias = normalizeProviderId(rawAlias);
if (!alias || !isRecord(rawTarget)) continue;
const provider = normalizeProviderId(normalizeOptionalString(rawTarget.provider) ?? "");
if (!provider || !ownedProviders.has(provider)) continue;
const api = normalizeModelCatalogApi(rawTarget.api);
const baseUrl = normalizeOptionalString(rawTarget.baseUrl) ?? "";
aliases[alias] = {
provider,
...api ? { api } : {},
...baseUrl ? { baseUrl } : {}
};
}
return Object.keys(aliases).length > 0 ? aliases : void 0;
}
function normalizeModelCatalogSuppressions(value) {
if (!Array.isArray(value)) return;
const suppressions = [];
for (const entry of value) {
if (!isRecord(entry)) continue;
const provider = normalizeProviderId(normalizeOptionalString(entry.provider) ?? "");
const model = normalizeOptionalString(entry.model) ?? "";
if (!provider || !model) continue;
const reason = normalizeOptionalString(entry.reason) ?? "";
const replacedBy = isRecord(entry.retirement) ? normalizeOptionalString(entry.retirement.replacedBy) : void 0;
const retirement = isRecord(entry.retirement) && (entry.retirement.replacedBy === void 0 || replacedBy) ? replacedBy ? { replacedBy } : {} : void 0;
const rawWhen = isRecord(entry.when) ? entry.when : void 0;
const baseUrlHosts = normalizeTrimmedStringList(rawWhen?.baseUrlHosts).map((host) => host.toLowerCase());
const providerConfigApiIn = normalizeTrimmedStringList(rawWhen?.providerConfigApiIn).map((api) => api.toLowerCase());
const when = baseUrlHosts.length > 0 || providerConfigApiIn.length > 0 ? {
...baseUrlHosts.length > 0 ? { baseUrlHosts } : {},
...providerConfigApiIn.length > 0 ? { providerConfigApiIn } : {}
} : void 0;
if (retirement && entry.when !== void 0 && (!rawWhen || !when || rawWhen.baseUrlHosts !== void 0 && baseUrlHosts.length === 0 || rawWhen.providerConfigApiIn !== void 0 && providerConfigApiIn.length === 0)) continue;
suppressions.push({
provider,
model,
...reason ? { reason } : {},
...retirement ? { retirement } : {},
...when ? { when } : {}
});
}
return suppressions.length > 0 ? suppressions : void 0;
}
function normalizeModelCatalogDiscovery(value, ownedProviders) {
if (!isRecord(value)) return;
const discovery = {};
for (const [rawProviderId, rawMode] of Object.entries(value)) {
const providerId = normalizeProviderId(rawProviderId);
const mode = normalizeOptionalString(rawMode) ?? "";
if (providerId && ownedProviders.has(providerId) && MODEL_CATALOG_DISCOVERY_MODES.has(mode)) discovery[providerId] = mode;
}
return Object.keys(discovery).length > 0 ? discovery : void 0;
}
/** Normalize a raw model catalog object for the set of providers owned by a plugin/manifest. */
function normalizeModelCatalog(value, params) {
if (!isRecord(value)) return;
const ownedProviders = new Set([...params.ownedProviders].map(normalizeProviderId).filter(Boolean));
const modelsDev = Object.fromEntries(Object.entries(normalizeStringMap(value.modelsDev) ?? {}).flatMap(([rawProviderId, sourceId]) => {
const providerId = normalizeProviderId(rawProviderId);
return ownedProviders.has(providerId) && !isBlockedObjectKey(providerId) ? [[providerId, sourceId]] : [];
}));
const providers = normalizeModelCatalogProviders(value.providers, ownedProviders);
const aliases = normalizeModelCatalogAliases(value.aliases, ownedProviders);
const suppressions = normalizeModelCatalogSuppressions(value.suppressions);
const discovery = normalizeModelCatalogDiscovery(value.discovery, ownedProviders);
const runtimeAugment = value.runtimeAugment === true;
const catalog = {
...Object.keys(modelsDev).length > 0 ? { modelsDev } : {},
...providers ? { providers } : {},
...aliases ? { aliases } : {},
...suppressions ? { suppressions } : {},
...discovery ? { discovery } : {},
...runtimeAugment ? { runtimeAugment } : {}
};
return Object.keys(catalog).length > 0 ? catalog : void 0;
}
//#endregion
//#region packages/gateway-protocol/src/plugin-ui-capabilities.ts
/** Static UI contribution kinds, in canonical presentation order. */
const PLUGIN_UI_CAPABILITIES = [
"page",
"navigation",
"panel",
"action",
"accessory",
"widget",
"replacement",
"link-reader"
];
/** Omission is unspecified; an empty declaration explicitly advertises no UI. */
function validatePluginUiCapabilities(value) {
if (value === void 0) return { ok: true };
if (!Array.isArray(value)) return {
ok: false,
error: "must be an array"
};
for (const entry of value) if (!PLUGIN_UI_CAPABILITIES.some((candidate) => candidate === entry)) return {
ok: false,
error: `contains unknown UI capability ${JSON.stringify(entry)}`
};
return {
ok: true,
capabilities: PLUGIN_UI_CAPABILITIES.filter((capability) => value.includes(capability))
};
}
//#endregion
//#region packages/plugin-package-contract/src/categories.ts
/** Active browse taxonomy, ordered with the core configuration surfaces first. */
const PLUGIN_CATEGORY_SLUGS = [
"channels",
"models",
"agent-runtimes",
"memory",
"context",
"voice",
"web",
"computer-use",
"media",
"security",
"integrations",
"developer-tools",
"infrastructure",
"documents-files",
"inbox-collaboration",
"productivity",
"scheduling",
"finance-payments",
"sales-marketing",
"data-analytics",
"agent-orchestration",
"research",
"other"
];
/** Published declarations remain readable while authors adopt the active taxonomy. */
const LEGACY_PLUGIN_CATEGORY_SLUGS = [
"tools",
"runtime",
"gateway"
];
const ACCEPTED_PLUGIN_CATEGORY_SLUGS = [...PLUGIN_CATEGORY_SLUGS, ...LEGACY_PLUGIN_CATEGORY_SLUGS];
/** Validate optional ordered package-owned plugin categories. */
function validatePluginCategories(value) {
if (value === void 0) return { ok: true };
if (!Array.isArray(value)) return {
ok: false,
error: "must be an array"
};
if (value.length < 1 || value.length > 3) return {
ok: false,
error: "must contain between 1 and 3 entries"
};
const categories = [];
for (const entry of value) {
const category = ACCEPTED_PLUGIN_CATEGORY_SLUGS.find((candidate) => candidate === entry);
if (!category) return {
ok: false,
error: `contains unknown category ${JSON.stringify(entry)}`
};
if (categories.includes(category)) return {
ok: false,
error: "must not contain duplicates"
};
categories.push(category);
}
return {
ok: true,
categories
};
}
//#endregion
//#region src/plugins/doctor-session-route-state-owner-types.ts
function coerceDoctorSessionRouteStateOwners(value) {
if (!Array.isArray(value)) return [];
return value.flatMap((entry) => {
const candidate = asOptionalObjectRecord(entry);
if (typeof candidate?.id !== "string" || typeof candidate.label !== "string") return [];
const id = candidate.id.trim();
const label = candidate.label.trim();
if (!id || !label) return [];
const owner = {
id,
label,
providerIds: normalizeTrimmedStringList(candidate.providerIds),
runtimeIds: normalizeTrimmedStringList(candidate.runtimeIds),
cliSessionKeys: normalizeTrimmedStringList(candidate.cliSessionKeys),
authProfilePrefixes: normalizeTrimmedStringList(candidate.authProfilePrefixes)
};
return [
"providerIds",
"runtimeIds",
"cliSessionKeys",
"authProfilePrefixes"
].every((key) => candidate[key] === void 0 || owner[key].length > 0) ? [owner] : [];
});
}
//#endregion
//#region src/plugins/manifest-contract-keys.ts
const PLUGIN_MANIFEST_CONTRACT_KEYS = [
"codeModeExecutors",
"embeddedExtensionFactories",
"agentToolResultMiddleware",
"trustedToolPolicies",
"externalAuthProviders",
"decisionProviders",
"embeddingProviders",
"speechProviders",
"realtimeTranscriptionProviders",
"realtimeVoiceProviders",
"mediaUnderstandingProviders",
"transcriptSourceProviders",
"documentExtractors",
"imageGenerationProviders",
"videoGenerationProviders",
"musicGenerationProviders",
"webContentExtractors",
"webFetchProviders",
"webSearchProviders",
"workerProviders",
"storageProviders",
"usageProviders",
"migrationProviders",
"gatewayMethodDispatch",
"tools"
];
//#endregion
//#region src/plugins/manifest-capability-normalizers.ts
function normalizeDecisionCapabilities(value) {
if (!isRecord(value) || !Array.isArray(value.questionTypes) || value.questionTypes.length === 0 || value.questionTypes.length > 3 || !value.questionTypes.every((kind) => kind === "boolean" || kind === "choice" || kind === "score")) return;
const capabilities = { questionTypes: [...new Set(value.questionTypes)] };
for (const key of [
"maxQuestions",
"maxChoiceAlternatives",
"maxScoreLevels",
"maxInputTokens"
]) {
const limit = value[key];
if (typeof limit === "number" && Number.isSafeInteger(limit) && limit > 0) capabilities[key] = limit;
}
if (value.inputTokenScope === "encoded-question" || value.inputTokenScope === "state-plus-each-criterion") capabilities.inputTokenScope = value.inputTokenScope;
if (typeof value.requiresBooleanCriteria === "boolean") capabilities.requiresBooleanCriteria = value.requiresBooleanCriteria;
if (value.confidence === "provider-specific" || value.confidence === "none") capabilities.confidence = value.confidence;
return capabilities;
}
/** Normalize provider-owned model descriptors without importing provider code. */
function normalizeManifestDecisionModels(value, providers) {
const seen = /* @__PURE__ */ new Set();
return normalizeManifestObjectList(value, (entry) => {
const provider = normalizeOptionalString(entry.provider);
const id = normalizeOptionalString(entry.id);
const name = normalizeOptionalString(entry.name);
if (!provider || !id || !name || !providers?.includes(provider)) return;
const ref = `${provider}/${id}`;
if (seen.has(ref)) return;
const capabilities = normalizeDecisionCapabilities(entry.capabilities);
seen.add(ref);
return {
provider,
id,
name,
...capabilities ? { capabilities } : {}
};
});
}
/** Endpoint restrictions constrain a provider alias without changing stored credential identity. */
function normalizeManifestProviderAuthAliases(value) {
return normalizeManifestRecord(value, (entry) => {
if (typeof entry === "string") return normalizeOptionalString(entry);
if (isRecord(entry)) {
const provider = normalizeOptionalString(entry.provider);
const baseUrls = normalizeTrimmedStringList(entry.baseUrls);
if (provider && baseUrls.length > 0) return {
provider,
baseUrls
};
}
});
}
function isPluginToolProfile(profile) {
return profile === "minimal" || profile === "coding" || profile === "messaging" || profile === "full";
}
function normalizeStringListRecord(value) {
return normalizeManifestRecord(value, normalizeOptionalTrimmedStringList);
}
function normalizeManifestStringRecord(value) {
return normalizeManifestRecord(value, normalizeOptionalString);
}
function normalizeManifestMcpServers(value) {
return normalizeNamedMetadataRecord(value, (server) => ({ ...server }));
}
function normalizeManifestRecord(value, normalizeEntry) {
if (!isRecord(value)) return;
const normalized = Object.create(null);
for (const [rawId, rawEntry] of Object.entries(value)) {
const id = normalizeOptionalString(rawId) ?? "";
const entry = !id || isBlockedObjectKey$1(id) ? void 0 : normalizeEntry(rawEntry, id);
if (entry) normalized[id] = entry;
}
return Object.keys(normalized).length > 0 ? normalized : void 0;
}
function normalizeNamedMetadataRecord(value, normalizeEntry) {
return normalizeManifestRecord(value, (entry, id) => isRecord(entry) ? normalizeEntry(entry, id) : void 0);
}
function normalizeManifestObjectList(value, normalizeEntry) {
if (!Array.isArray(value)) return;
const normalized = [];
for (const entry of value) {
const result = isRecord(entry) ? normalizeEntry(entry) : void 0;
if (result !== void 0) normalized.push(result);
}
return normalized.length > 0 ? normalized : void 0;
}
function normalizeManifestTranscriptSources(value, ownedProviders = []) {
const locatorKeys = [
"accountId",
"guildId",
"channelId",
"meetingUrl"
];
return normalizeNamedMetadataRecord(value, (entry, id) => {
if (!ownedProviders.includes(id)) return;
const name = normalizeOptionalString(entry.name);
const raw = entry.autoStart;
const autoStart = isRecord(raw) && Object.entries(raw).every(([key, mode]) => locatorKeys.some((locator) => locator === key) && (mode === "optional" || mode === "required")) ? Object.fromEntries(Object.entries(raw)) : void 0;
return name || autoStart ? {
...name ? { name } : {},
...autoStart ? { autoStart } : {}
} : void 0;
});
}
function isMediaUnderstandingCapability(value) {
return value === "image" || value === "audio" || value === "video";
}
function normalizeMediaUnderstandingRecord(value, normalizeValue) {
if (!isRecord(value)) return;
const normalized = {};
for (const [rawKey, rawValue] of Object.entries(value)) {
if (!isMediaUnderstandingCapability(rawKey)) continue;
const entry = normalizeValue(rawValue);
if (entry !== void 0) normalized[rawKey] = entry;
}
return Object.keys(normalized).length > 0 ? normalized : void 0;
}
function normalizeMediaUnderstandingCapabilities(value) {
const values = normalizeTrimmedStringList(value).filter(isMediaUnderstandingCapability);
return values.length > 0 ? values : void 0;
}
function normalizeMediaUnderstandingNativeDocumentInputs(value) {
const values = normalizeTrimmedStringList(value).filter((entry) => entry === "pdf");
return values.length > 0 ? values : void 0;
}
function normalizeMediaUnderstandingDocumentModels(value) {
if (!isRecord(value)) return;
const pdfRaw = value.pdf;
if (!isRecord(pdfRaw)) return;
const textExtraction = normalizeOptionalString(pdfRaw.textExtraction);
const image = pdfRaw.image === false ? false : normalizeOptionalString(pdfRaw.image);
const pdf = {
...textExtraction ? { textExtraction } : {},
...image !== void 0 ? { image } : {}
};
return Object.keys(pdf).length > 0 ? { pdf } : void 0;
}
function normalizeMediaUnderstandingProviderMetadata(value) {
return normalizeNamedMetadataRecord(value, (rawMetadata) => {
const capabilities = normalizeMediaUnderstandingCapabilities(rawMetadata.capabilities);
const defaultModels = normalizeMediaUnderstandingRecord(rawMetadata.defaultModels, normalizeOptionalString);
const autoPriority = normalizeMediaUnderstandingRecord(rawMetadata.autoPriority, (priority) => typeof priority === "number" && Number.isFinite(priority) ? priority : void 0);
const nativeDocumentInputs = normalizeMediaUnderstandingNativeDocumentInputs(rawMetadata.nativeDocumentInputs);
const documentModels = normalizeMediaUnderstandingDocumentModels(rawMetadata.documentModels);
const metadata = {
...capabilities ? { capabilities } : {},
...defaultModels ? { defaultModels } : {},
...autoPriority ? { autoPriority } : {},
...nativeDocumentInputs ? { nativeDocumentInputs } : {},
...documentModels ? { documentModels } : {}
};
return Object.keys(metadata).length > 0 ? metadata : void 0;
});
}
function normalizeProviderBaseUrlGuard(value) {
if (!isRecord(value)) return;
const provider = normalizeOptionalString(value.provider);
const allowedBaseUrls = normalizeTrimmedStringList(value.allowedBaseUrls);
if (!provider || allowedBaseUrls.length === 0) return;
const defaultBaseUrl = normalizeOptionalString(value.defaultBaseUrl);
return {
provider,
...defaultBaseUrl ? { defaultBaseUrl } : {},
allowedBaseUrls
};
}
function normalizeCapabilityProviderAuthSignals(value) {
return normalizeManifestObjectList(value, (rawSignal) => {
const provider = normalizeOptionalString(rawSignal.provider);
if (!provider) return;
const providerBaseUrl = normalizeProviderBaseUrlGuard(rawSignal.providerBaseUrl);
return {
provider,
...providerBaseUrl ? { providerBaseUrl } : {}
};
});
}
function normalizeCapabilityProviderModeConfigSignal(value) {
if (!isRecord(value)) return;
const pathResult = normalizeOptionalString(value.path);
const defaultValue = normalizeOptionalString(value.default);
const allowed = normalizeTrimmedStringList(value.allowed);
const disallowed = normalizeTrimmedStringList(value.disallowed);
const signal = {
...pathResult ? { path: pathResult } : {},
...defaultValue ? { default: defaultValue } : {},
...allowed.length > 0 ? { allowed } : {},
...disallowed.length > 0 ? { disallowed } : {}
};
return Object.keys(signal).length > 0 ? signal : void 0;
}
function normalizeCapabilityProviderConfigSignals(value) {
return normalizeManifestObjectList(value, (rawSignal) => {
const rootPath = normalizeOptionalString(rawSignal.rootPath);
if (!rootPath) return;
const overlayPath = normalizeOptionalString(rawSignal.overlayPath);
const overlayMapPath = normalizeOptionalString(rawSignal.overlayMapPath);
const required = normalizeTrimmedStringList(rawSignal.required);
const requiredAny = normalizeTrimmedStringList(rawSignal.requiredAny);
const mode = normalizeCapabilityProviderModeConfigSignal(rawSignal.mode);
const signal = {
rootPath,
...overlayPath ? { overlayPath } : {},
...overlayMapPath ? { overlayMapPath } : {},
...required.length > 0 ? { required } : {},
...requiredAny.length > 0 ? { requiredAny } : {},
...mode ? { mode } : {}
};
return required.length > 0 || requiredAny.length > 0 || mode ? signal : void 0;
});
}
function normalizeCapabilityProviderMetadataEntry(rawMetadata) {
const aliases = normalizeTrimmedStringList(rawMetadata.aliases);
const authProviders = normalizeTrimmedStringList(rawMetadata.authProviders);
const authSignals = normalizeCapabilityProviderAuthSignals(rawMetadata.authSignals);
const configSignals = normalizeCapabilityProviderConfigSignals(rawMetadata.configSignals);
const referenceAudioInputs = rawMetadata.referenceAudioInputs === true ? true : void 0;
const metadata = {
...aliases.length > 0 ? { aliases } : {},
...authProviders.length > 0 ? { authProviders } : {},
...authSignals ? { authSignals } : {},
...configSignals ? { configSignals } : {},
...referenceAudioInputs ? { referenceAudioInputs } : {}
};
return Object.keys(metadata).length > 0 ? metadata : void 0;
}
function normalizeCapabilityProviderMetadata(value) {
return normalizeNamedMetadataRecord(value, normalizeCapabilityProviderMetadataEntry);
}
function normalizePluginToolMetadata(value) {
return normalizeNamedMetadataRecord(value, (rawMetadata) => {
const providerMetadata = normalizeCapabilityProviderMetadataEntry(rawMetadata);
const profiles = normalizeTrimmedStringList(rawMetadata.profiles).filter(isPluginToolProfile);
const metadata = {
...providerMetadata,
...rawMetadata.optional === true ? { optional: true } : {},
...profiles.length > 0 ? { profiles } : {},
...rawMetadata.replaySafe === true ? { replaySafe: true } : {},
...rawMetadata.sideEffecting === true ? { sideEffecting: true } : {}
};
return Object.keys(metadata).length > 0 ? metadata : void 0;
});
}
function normalizeManifestCatalog(value) {
if (!isRecord(value)) return;
const featured = typeof value.featured === "boolean" ? value.featured : void 0;
const order = typeof value.order === "number" && Number.isFinite(value.order) ? value.order : void 0;
if (featured === void 0 && order === void 0) return;
return {
...featured !== void 0 ? { featured } : {},
...order !== void 0 ? { order } : {}
};
}
function normalizeManifestContracts(value) {
if (!isRecord(value)) return;
const contracts = {};
for (const key of PLUGIN_MANIFEST_CONTRACT_KEYS) {
const entries = normalizeTrimmedStringList(value[key]);
if (entries.length > 0) contracts[key] = entries;
}
return Object.keys(contracts).length > 0 ? contracts : void 0;
}
function isManifestConfigLiteral(value) {
return value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean";
}
function normalizeManifestDangerousConfigFlags(value) {
return normalizeManifestObjectList(value, (entry) => {
const pathValue = normalizeOptionalString(entry.path) ?? "";
return pathValue && isManifestConfigLiteral(entry.equals) ? {
path: pathValue,
equals: entry.equals
} : void 0;
});
}
function normalizeManifestSecretInputPaths(value) {
return normalizeManifestObjectList(value, (entry) => {
const pathLocal = normalizeOptionalString(entry.path) ?? "";
if (!pathLocal) return;
const expected = entry.expected === "string" ? entry.expected : void 0;
const ownerKind = entry.ownerKind === "capability" || entry.ownerKind === "route" ? entry.ownerKind : void 0;
return {
path: pathLocal,
...expected ? { expected } : {},
...ownerKind ? { ownerKind } : {}
};
});
}
function normalizeManifestConfigContracts(value) {
if (!isRecord(value)) return;
const compatibilityMigrationPaths = normalizeTrimmedStringList(value.compatibilityMigrationPaths);
const compatibilityRuntimePaths = normalizeTrimmedStringList(value.compatibilityRuntimePaths);
const rawSecretInputs = isRecord(value.secretInputs) ? value.secretInputs : void 0;
const dangerousFlags = normalizeManifestDangerousConfigFlags(value.dangerousFlags);
const secretInputPaths = rawSecretInputs ? normalizeManifestSecretInputPaths(rawSecretInputs.paths) : void 0;
const secretInputs = secretInputPaths ? {
...typeof rawSecretInputs?.bundledDefaultEnabled === "boolean" ? { bundledDefaultEnabled: rawSecretInputs.bundledDefaultEnabled } : {},
paths: secretInputPaths
} : void 0;
const configContracts = {
...compatibilityMigrationPaths.length > 0 ? { compatibilityMigrationPaths } : {},
...compatibilityRuntimePaths.length > 0 ? { compatibilityRuntimePaths } : {},
...dangerousFlags ? { dangerousFlags } : {},
...secretInputs ? { secretInputs } : {}
};
return Object.keys(configContracts).length > 0 ? configContracts : void 0;
}
//#endregion
//#region src/plugins/manifest-command-aliases.ts
/** Normalizes manifest-declared CLI command aliases. */
/** Normalizes manifest command alias records, omitting invalid entries. */
function normalizeManifestCommandAliases(value) {
if (!Array.isArray(value)) return;
const normalized = [];
for (const entry of value) {
if (typeof entry === "string") {
const name = normalizeOptionalString(entry) ?? "";
if (name) normalized.push({ name });
continue;
}
if (!isRecord(entry)) continue;
const name = normalizeOptionalString(entry.name) ?? "";
if (!name) continue;
const kind = entry.kind === "runtime-slash" ? entry.kind : void 0;
const cliCommand = normalizeOptionalString(entry.cliCommand) ?? "";
normalized.push({
name,
...kind ? { kind } : {},
...cliCommand ? { cliCommand } : {}
});
}
return normalized.length > 0 ? normalized : void 0;
}
//#endregion
//#region src/plugins/manifest-config-groups.ts
/** Invalid presentation metadata leaves the plugin's complete flat settings form available. */
function normalizeConfigGroups(raw, schema) {
if (!Array.isArray(raw) || raw.length === 0 || !isRecord(schema.properties)) return;
const ids = /* @__PURE__ */ new Set();
const assigned = /* @__PURE__ */ new Set();
const groups = [];
for (const entry of raw) {
if (!isRecord(entry)) return;
const id = typeof entry.id === "string" ? entry.id.trim() : "";
const title = typeof entry.title === "string" ? entry.title.trim() : "";
if (!id || !title || ids.has(id) || entry.order !== void 0 && !Number.isInteger(entry.order) || !Array.isArray(entry.properties) || entry.properties.length === 0) return;
const properties = [];
for (const property of entry.properties) {
if (typeof property !== "string" || !property || !Object.hasOwn(schema.properties, property) || assigned.has(property)) return;
assigned.add(property);
properties.push(property);
}
ids.add(id);
groups.push({
id,
title,
...typeof entry.order === "number" ? { order: entry.order } : {},
properties
});
}
return groups;
}
//#endregion
//#region packages/model-catalog-core/src/model-catalog-pricing.ts
const OPENROUTER_MODELS_URL = "https://openrouter.ai/api/v1/models";
const MODELS_DEV_CATALOG_URL = "https://models.opencode.ai/api.json";
const MODEL_PRICING_SOURCES = [
{
id: "openCode",
label: "OpenCode",
url: MODELS_DEV_CATALOG_URL,
authoritative: true
},
{
id: "venice",
label: "Venice",
url: "https://api.venice.ai/api/v1/models",
authoritative: true
},
{
id: "chutes",
label: "Chutes",
url: "https://llm.chutes.ai/v1/models",
authoritative: true
},
{
id: "cerebras",
label: "Cerebras",
url: "https://api.cerebras.ai/public/v1/models",
authoritative: true
},
{
id: "deepinfra",
label: "DeepInfra",
url: "https://api.deepinfra.com/models/list",
authoritative: true
},
{
id: "modelsDev",
label: "models.dev",
url: MODELS_DEV_CATALOG_URL,
authoritative: false
},
{
id: "openRouter",
label: "OpenRouter",
url: OPENROUTER_MODELS_URL,
authoritative: false
},
{
id: "liteLLM",
label: "LiteLLM",
url: "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json",
authoritative: false
}
];
/** Normalize source policy without deciding which plugin owns the provider. */
function normalizeModelPricingProvider(value) {
const record = asOptionalRecord(value);
if (!record) return;
const policy = typeof record.external === "boolean" ? { external: record.external } : {};
for (const { id: sourceId } of MODEL_PRICING_SOURCES) {
const raw = record[sourceId];
if (raw === false) {
policy[sourceId] = false;
continue;
}
const row = asOptionalRecord(raw);
const provider = normalizeProviderId(normalizeOptionalString(row?.provider) ?? "");
const modelIdTransforms = normalizeTrimmedStringList(row?.modelIdTransforms).filter((entry) => entry === "version-dots");
const source = {
...provider ? { provider } : {},
...row?.passthroughProviderModel === true ? { passthroughProviderModel: true } : {},
...modelIdTransforms.length > 0 ? { modelIdTransforms } : {}
};
if (Object.keys(source).length > 0) policy[sourceId] = source;
}
return Object.keys(policy).length > 0 ? policy : void 0;
}
//#endregion
//#region src/plugins/plugin-provider-request-policy.ts
/** Manifest parsing and prepared metadata use the same provider request policy. */
function normalizeManifestProviderRequestProvider(value) {
if (!isRecord(value)) return;
const family = normalizeOptionalString(value.family);
const compatibilityFamily = normalizeOptionalString(value.compatibilityFamily) === "moonshot" ? "moonshot" : void 0;
const supportsStreamingUsage = isRecord(value.openAICompletions) ? value.openAICompletions.supportsStreamingUsage : void 0;
const openAICompletions = typeof supportsStreamingUsage === "boolean" ? { supportsStreamingUsage } : void 0;
const providerRequest = {
...family ? { family } : {},
...compatibilityFamily ? { compatibilityFamily } : {},
...openAICompletions ? { openAICompletions } : {}
};
return Object.keys(providerRequest).length > 0 ? providerRequest : void 0;
}
//#endregion
//#region src/plugins/manifest-model-provider-normalizers.ts
const MAX_SECRET_PROVIDER_EXEC_ARGS = 128;
const MAX_SECRET_PROVIDER_EXEC_ARG_BYTES = 1024;
const MAX_SECRET_PROVIDER_EXEC_TIMEOUT_MS = 12e4;
const MAX_SECRET_PROVIDER_EXEC_OUTPUT_BYTES = 20971520;
const MAX_SECRET_PROVIDER_EXEC_PASS_ENV = 128;
const SECRET_PROVIDER_NODE_COMMAND_PLACEHOLDER = "${node}";
function normalizeManifestModelSupport(value) {
if (!isRecord(value)) return;
const modelPrefixes = normalizeTrimmedStringList(value.modelPrefixes);
const modelPatterns = normalizeTrimmedStringList(value.modelPatterns);
const modelSupport = {
...modelPrefixes.length > 0 ? { modelPrefixes } : {},
...modelPatterns.length > 0 ? { modelPatterns } : {}
};
return Object.keys(modelSupport).length > 0 ? modelSupport : void 0;
}
function normalizeOwnedProviderMap(value, ownedProvidersRaw, normalizePolicy) {
if (!isRecord(value) || !isRecord(value.providers)) return;
const ownedProviders = new Set([...ownedProvidersRaw].map((provider) => normalizeProviderId(provider)).filter(Boolean));
const providers = {};
for (const [rawProviderId, rawPolicy] of Object.entries(value.providers)) {
const providerId = normalizeProviderId(rawProviderId);
const policy = providerId && ownedProviders.has(providerId) ? normalizePolicy(rawPolicy) : null;
if (providerId && policy) providers[providerId] = policy;
}
return Object.keys(providers).length > 0 ? providers : void 0;
}
function normalizeManifestModelPricing(value, params) {
const providers = normalizeOwnedProviderMap(value, params.ownedProviders, normalizeModelPricingProvider);
return providers ? { providers } : void 0;
}
function normalizeManifestModelIdPrefixRules(value) {
return normalizeManifestObjectList(value, (rawRule) => {
const modelPrefix = normalizeOptionalString(rawRule.modelPrefix);
const prefix = normalizeOptionalString(rawRule.prefix);
return modelPrefix && prefix ? {
modelPrefix,
prefix
} : void 0;
});
}
function normalizeManifestModelIdNormalizationProvider(value) {
if (!isRecord(value)) return;
const aliases = {};
if (isRecord(value.aliases)) for (const [rawAlias, rawCanonical] of Object.entries(value.aliases)) {
const alias = normalizeProviderId(rawAlias);
const canonical = normalizeOptionalString(rawCanonical);
if (alias && canonical) aliases[alias] = canonical;
}
const stripPrefixes = normalizeTrimmedStringList(value.stripPrefixes);
const prefixWhenBare = normalizeOptionalString(value.prefixWhenBare);
const prefixWhenBareAfterAliasStartsWith = normalizeManifestModelIdPrefixRules(value.prefixWhenBareAfterAliasStartsWith);
const normalization = {
...Object.keys(aliases).length > 0 ? { aliases } : {},
...stripPrefixes.length > 0 ? { stripPrefixes } : {},
...prefixWhenBare ? { prefixWhenBare } : {},
...prefixWhenBareAfterAliasStartsWith ? { prefixWhenBareAfterAliasStartsWith } : {}
};
return Object.keys(normalization).length > 0 ? normalization : void 0;
}
function normalizeManifestModelIdNormalization(value, params) {
const providers = normalizeOwnedProviderMap(value, params.ownedProviders, normalizeManifestModelIdNormalizationProvider);
return providers ? { providers } : void 0;
}
function normalizeManifestProviderEndpoints(value) {
return normalizeManifestObjectList(value, (rawEndpoint) => {
const endpointClass = normalizeOptionalString(rawEndpoint.endpointClass);
if (!endpointClass) return;
const hosts = normalizeTrimmedStringList(rawEndpoint.hosts).map((host) => host.toLowerCase());
const hostSuffixes = normalizeTrimmedStringList(rawEndpoint.hostSuffixes).map((host) => host.toLowerCase());
const baseUrls = normalizeTrimmedStringList(rawEndpoint.baseUrls);
const googleVertexRegion = normalizeOptionalString(rawEndpoint.googleVertexRegion);
const googleVertexRegionHostSuffix = normalizeOptionalString(rawEndpoint.googleVertexRegionHostSuffix)?.toLowerCase();
if (hosts.length === 0 && hostSuffixes.length === 0 && baseUrls.length === 0) return;
return {
endpointClass,
...hosts.length > 0 ? { hosts } : {},
...hostSuffixes.length > 0 ? { hostSuffixes } : {},
...baseUrls.length > 0 ? { baseUrls } : {},
...googleVertexRegion ? { googleVertexRegion } : {},
...googleVertexRegionHostSuffix ? { googleVertexRegionHostSuffix } : {}
};
});
}
function normalizeManifestProviderRequest(value, params) {
const providers = normalizeOwnedProviderMap(value, params.ownedProviders, normalizeManifestProviderRequestProvider);
return providers ? { providers } : void 0;
}
function normalizeManifestPositiveInteger(value, max) {
return typeof value === "number" && Number.isInteger(value) && value > 0 && value <= max ? value : void 0;
}
function normalizeManifestSecretProviderIntegrations(value) {
return normalizeNamedMetadataRecord(value, (rawIntegration) => {
const command = normalizeOptionalString(rawIntegration.command);
if (rawIntegration.source !== "exec" || command !== SECRET_PROVIDER_NODE_COMMAND_PLACEHOLDER) return;
const providerAlias = normalizeOptionalString(rawIntegration.providerAlias);
const displayName = normalizeOptionalString(rawIntegration.displayName);
const description = normalizeOptionalString(rawIntegration.description);
const args = [];
if (Array.isArray(rawIntegration.args)) for (const entry of rawIntegration.args) {
if (typeof entry !== "string" || entry.length > MAX_SECRET_PROVIDER_EXEC_ARG_BYTES) continue;
args.push(entry);
if (args.length >= MAX_SECRET_PROVIDER_EXEC_ARGS) break;
}
const timeoutMs = normalizeManifestPositiveInteger(rawIntegration.timeoutMs, MAX_SECRET_PROVIDER_EXEC_TIMEOUT_MS);
const noOutputTimeoutMs = normalizeManifestPositiveInteger(rawIntegration.noOutputTimeoutMs, MAX_SECRET_PROVIDER_EXEC_TIMEOUT_MS);
const maxOutputBytes = normalizeManifestPositiveInteger(rawIntegration.maxOutputBytes, MAX_SECRET_PROVIDER_EXEC_OUTPUT_BYTES);
const env = normalizeManifestStringRecord(rawIntegration.env);
const passEnv = normalizeTrimmedStringList(rawIntegration.passEnv).filter((entry) => ENV_SECRET_REF_ID_RE.test(entry)).slice(0, MAX_SECRET_PROVIDER_EXEC_PASS_ENV);
return {
...providerAlias ? { providerAlias } : {},
...displayName ? { displayName } : {},
...description ? { description } : {},
source: "exec",
command,
...args.length > 0 ? { args } : {},
...timeoutMs !== void 0 ? { timeoutMs } : {},
...noOutputTimeoutMs !== void 0 ? { noOutputTimeoutMs } : {},
...maxOutputBytes !== void 0 ? { maxOutputBytes } : {},
...typeof rawIntegration.jsonOnly === "boolean" ? { jsonOnly: rawIntegration.jsonOnly } : {},
...env ? { env } : {},
...passEnv.length > 0 ? { passEnv } : {}
};
});
}
//#endregion
//#region src/plugins/manifest-platforms.ts
const MANIFEST_PLATFORMS = /* @__PURE__ */ new Set([
"aix",
"android",
"darwin",
"freebsd",
"haiku",
"linux",
"openbsd",
"sunos",
"win32",
"cygwin",
"netbsd"
]);
function normalizeManifestPlatforms(value) {
return normalizeTrimmedStringList(value).filter((platform) => MANIFEST_PLATFORMS.has(platform));
}
//#endregion
//#region src/cli/program/command-descriptor-utils.ts
const SAFE_COMMAND_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
/** Normalize and validate a command descriptor name for safe Commander registration. */
function normalizeCommandDescriptorName(name) {
const normalized = name.trim();
return SAFE_COMMAND_NAME_PATTERN.test(normalized) ? normalized : null;
}
/** Strip unsafe terminal content from descriptor descriptions. */
function sanitizeCommandDescriptorDescription(description) {
return sanitizeForLog(description).trim();
}
//#endregion
//#region src/plugins/setup-presentation-url.ts
function normalizeSetupPresentationHttpsUrl(value) {
const normalized = normalizeOptionalString(value);
const url = normalized ? URL.parse(normalized) : null;
if (!url) return;
const canonical = url.toString();
return url.protocol === "https:" && url.hostname && !url.username && !url.password && canonical.length <= 2048 ? canonical : void 0;
}
//#endregion
//#region src/plugins/manifest-setup-normalizers.ts
function normalizeManifestActivation(value) {
if (!isRecord(value)) return;
const activation = {};
if (typeof value.onStartup === "boolean") activation.onStartup = value.onStartup;
for (const key of [
"onProviders",
"onAgentHarnesses",
"onCommands",
"onChannels",
"onRoutes",
"onConfigPaths"
]) {
const entries = normalizeTrimmedStringList(value[key]);
if (entries.length > 0) activation[key] = entries;
}
const onCapabilities = normalizeTrimmedStringList(value.onCapabilities).filter((capability) => capability === "provider" || capability === "channel" || capability === "tool" || capability === "hook");
if (onCapabilities.length > 0) activation.onCapabilities = onCapabilities;
return Object.keys(activation).length > 0 ? activation : void 0;
}
function normalizeChannelAccountKeyPolicies(value, channels) {
if (!isRecord(value)) return;
const policies = Object.create(null);
for (const channel of channels) {
if (isBlockedObjectKey$1(channel) || !Object.hasOwn(value, channel)) continue;
const entry = value[channel];
const field = isRecord(entry) ? normalizeOptionalString(entry.canonicalAliasesRequireOwnField) : void 0;
if (field && !isBlockedObjectKey$1(field)) policies[channel] = { canonicalAliasesRequireOwnField: field };
}
return Object.keys(policies).length ? policies : void 0;
}
function normalizeManifestCliCommands(value) {
if (!Array.isArray(value)) return;
const seen = /* @__PURE__ */ new Set();
return normalizeManifestObjectList(value, (entry) => {
if (typeof entry.name !== "string" || typeof entry.description !== "string") return;
const name = normalizeCommandDescriptorName(entry.name);
const description = sanitizeCommandDescriptorDescription(entry.description);
if (!name || !description || typeof entry.hasSubcommands !== "boolean" || seen.has(name)) return;
seen.add(name);
return {
name,
description,
hasSubcommands: entry.hasSubcommands
};
}) ?? [];
}
function normalizeManifestSetupProviders(value) {
return normalizeManifestObjectList(value, (entry) => {
const id = normalizeOptionalString(entry.id) ?? "";
if (!id) return;
const authMethods = normalizeTrimmedStringList(entry.authMethods);
const envVars = normalizeTrimmedStringList(entry.envVars);
const authEvidence = normalizeManifestSetupProviderAuthEvidence(entry.authEvidence);
return {
id,
...authMethods.length > 0 ? { authMethods } : {},
...envVars.length > 0 ? { envVars } : {},
...authEvidence ? { authEvidence } : {}
};
});
}
function normalizeManifestSetupProviderAuthEvidence(value) {
return normalizeManifestObjectList(value, (entry) => {
if (entry.type !== "local-file-with-env") return;
const credentialMarker = normalizeOptionalString(entry.credentialMarker);
if (!credentialMarker) return;
const fileEnvVar = normalizeOptionalString(entry.fileEnvVar);
const fallbackPaths = normalizeTrimmedStringList(entry.fallbackPaths);
if (!fileEnvVar && fallbackPaths.length === 0) return;
const requiresAnyEnv = normalizeTrimmedStringList(entry.requiresAnyEnv);
const requiresAllEnv = normalizeTrimmedStringList(entry.requiresAllEnv);
const source = normalizeOptionalString(entry.source);
return {
type: "local-file-with-env",
...fileEnvVar ? { fileEnvVar } : {},
...fallbackPaths.length > 0 ? { fallbackPaths } : {},
...requiresAnyEnv.length > 0 ? { requiresAnyEnv } : {},
...requiresAllEnv.length > 0 ? { requiresAllEnv } : {},
credentialMarker,
...source ? { source } : {}
};
});
}
function normalizeManifestSetup(value) {
if (!isRecord(value)) return;
const providers = normalizeManifestSetupProviders(value.providers);
const cliBackends = normalizeTrimmedStringList(value.cliBackends);
const configMigrations = normalizeTrimmedStringList(value.configMigrations);
const nativeSessionCatalog = isRecord(value.nativeSessionCatalog) ? {
label: normalizeOptionalString(value.nativeSessionCatalog.label) ?? "",
description: normalizeOptionalString(value.nativeSessionCatalog.description),
nodeCommands: normalizeTrimmedStringList(value.nativeSessionCatalog.nodeCommands),
legacyDefaultEnabled: value.nativeSessionCatalog.legacyDefaultEnabled === true
} : void 0;
const requiresRuntime = typeof value.requiresRuntime === "boolean" ? value.requiresRuntime : void 0;
const setup = {
...providers ? { providers } : {},
...cliBackends.length > 0 ? { cliBackends } : {},
...configMigrations.length > 0 ? { configMigrations } : {},
...nativeSessionCatalog?.label ? { nativeSessionCatalog: {
label: nativeSessionCatalog.label,
...nativeSessionCatalog.legacyDefaultEnabled ? { legacyDefaultEnabled: true } : {},
...nativeSessionCatalog.nodeCommands.length > 0 ? { nodeCommands: nativeSessionCatalog.nodeCommands } : {},
...nativeSessionCatalog.description ? { description: nativeSessionCatalog.description } : {}
} } : {},
...requiresRuntime !== void 0 ? { requiresRuntime } : {}
};
return Object.keys(setup).length > 0 ? setup : void 0;
}
function normalizeManifestQaRunners(value) {
return normalizeManifestObjectList(value, (entry) => {
const commandName = normalizeOptionalString(entry.commandName) ?? "";
if (!commandName) return;
const description = normalizeOptionalString(entry.description) ?? "";
return {
commandName,
...description ? { description } : {}
};
});
}
function normalizeDashboardCapabilityBase(value, field, index) {
if (!isRecord(value)) return `${field}[${index}] must be an object`;
const id = normalizeOptionalString(value.id);
const method = normalizeOptionalString(value.method);
const description = normalizeOptionalString(value.description);
if (!id || !/^[a-z0-9][a-z0-9._-]*$/u.test(id)) return `${field}[${index}].id must be a lowercase capability id`;
if (!method) return `${field}[${index}].method must be a non-empty string`;
if (!description) return `${field}[${index}].description must be a non-empty string`;
return {
id,
method,
description
};
}
function normalizeManifestDashboard(value) {
if (value === void 0) return { ok: true };
if (!isRecord(value)) return {
ok: false,
error: "dashboard must be an object"
};
if (value.dataBindings !== void 0 && !Array.isArray(value.dataBindings)) return {
ok: false,
error: "dashboard.dataBindings must be an array"
};
if (value.actionVerbs !== void 0 && !Array.isArray(value.actionVerbs)) return {
ok: false,
error: "dashboard.actionVerbs must be an array"
};
const dataBindings = [];
for (const [index, entry] of (value.dataBindings ?? []).entries()) {
const normalized = normalizeDashboardCapabilityBase(entry, "dashboard.dataBindings", index);
if (typeof normalized === "string") return {
ok: false,
error: normalized
};
dataBindings.push(normalized);
}
const actionVerbs = [];
for (const [index, entry] of (value.actionVerbs ?? []).entries()) {
const normalized = normalizeDashboardCapabilityBase(entry, "dashboard.actionVerbs", index);
if (typeof normalized === "string") return {
ok: false,
error: normalized
};
const rawParamShape = isRecord(entry) ? entry.paramShape : void 0;
if (rawParamShape !== void 0 && !isRecord(rawParamShape)) return {
ok: false,
error: `dashboard.actionVerbs[${index}].paramShape must be a JSON Schema object`
};
actionVerbs.push({
...normalized,
...rawParamShape ? { paramShape: rawParamShape } : {}
});
}
if (dataBindings.length === 0 && actionVerbs.length === 0) return { ok: true };
return {
ok: true,
dashboard: {
...dataBindings.length > 0 ? { dataBindings } : {},
...actionVerbs.length > 0 ? { actionVerbs } : {}
}
};
}
function normalizeManifestControlUi(value) {
if (value === void 0) return ok(void 0);
if (!isRecord(value) || Object.keys(value).some((key) => key !== "entry" && key !== "styles")) return err("controlUi must contain only entry and optional styles");
const entry = typeof value.entry === "string" ? value.entry.replace(/^\.\//u, "") : "";
if (entry.length > 512 || !/^dist\/(?:[\w-][\w.-]*\/)+[\w-][\w.-]*\.m?js$/u.test(entry)) return err("controlUi.entry must be a JavaScript file in a dedicated dist subdirectory");
if (value.styles !== void 0 && (!Array.isArray(value.styles) || value.styles.length > 16)) return err("controlUi.styles must be an array of at most 16 stylesheets");
const assetPrefix = entry.slice(0, entry.lastIndexOf("/") + 1);
const styles = [];
for (const rawStyle of value.styles ?? []) {
const style = typeof rawStyle === "string" ? rawStyle.replace(/^\.\//u, "") : "";
if (style.length > 512 || !style.startsWith(assetPrefix) || !/^(?:[\w-][\w.-]*\/)+[\w-][\w.-]*\.css$/u.test(style)) return err("controlUi.styles must contain CSS files under the entry's asset directory");
if (!styles.includes(style)) styles.push(style);
}
return ok({
entry,
...styles.length > 0 ? { styles } : {}
});
}
function normalizeProviderChannelLogin(value) {
if (!isRecord(value) || Object.keys(value).some((key) => key !== "aliases")) return;
if (value.aliases !== void 0 && (!Array.isArray(value.aliases) || value.aliases.some((alias) => typeof alias !== "string" || !alias.trim()))) return;
const aliases = normalizeUniqueTrimmedStringList(value.aliases);
return aliases.length > 0 ? { aliases } : {};
}
function normalizeProviderAuthChoices(value) {
return normalizeManifestObjectList(value, (entry) => {
const provider = normalizeOptionalString(entry.provider) ?? "";
const method = normalizeOptionalString(entry.method) ?? "";
const choiceId = normalizeOptionalString(entry.choiceId) ?? "";
if (!provider || !method || !choiceId) return;
const choiceLabel = normalizeOptionalString(entry.choiceLabel) ?? "";
const choiceHint = normalizeOptionalString(entry.choiceHint) ?? "";
const icon = normalizeSetupPresentationHttpsUrl(entry.icon);
const website = normalizeSetupPresentationHttpsUrl(entry.website);
const docsUrl = normalizeSetupPresentationHttpsUrl(entry.docsUrl);
const assistantPriority = typeof entry.assistantPriority === "number" && Number.isFinite(entry.assistantPriority) ? entry.assistantPriority : void 0;
const assistantVisibility = entry.assistantVisibility === "manual-only" || entry.assistantVisibility === "visible" || entry.assistantVisibility === "detected-only" ? entry.assistantVisibility : void 0;
const deprecatedChoiceIds = normalizeTrimmedStringList(entry.deprecatedChoiceIds);
const groupId = normalizeOptionalString(entry.groupId) ?? "";
const groupLabel = normalizeOptionalString(entry.groupLabel) ?? "";
const groupHint = normalizeOptionalString(entry.groupHint) ?? "";
const optionKey = normalizeOptionalString(entry.optionKey) ?? "";
const cliFlag = normalizeOptionalString(entry.cliFlag) ?? "";
const cliOption = normalizeOptionalString(entry.cliOption) ?? "";
const cliDescription = normalizeOptionalString(entry.cliDescription) ?? "";
const appGuidedActionLabel = normalizeOptionalString(entry.appGuidedActionLabel) ?? "";
const appGuidedAuth = entry.appGuidedAuth === "oauth" || entry.appGuidedAuth === "device-code" ? entry.appGuidedAuth : void 0;
const onboardingScopes = normalizeTrimmedStringList(entry.onboardingScopes).filter((scope) => scope === "text-inference" || scope === "image-generation" || scope === "music-generation");
const channelLogin = normalizeProviderChannelLogin(entry.channelLogin);
return {
provider,
method,
choiceId,
...entry.modelTarget === "utility" ? { modelTarget: "utility" } : {},
...entry.platforms !== void 0 ? { platforms: normalizeManifestPlatforms(entry.platforms) } : {},
...choiceLabel ? { choiceLabel } : {},
...choiceHint ? { choiceHint } : {},
...icon ? { icon } : {},
...website ? { website } : {},
...docsUrl ? { docsUrl } : {},
...assistantPriority !== void 0 ? { assistantPriority } : {},
...assistantVisibility ? { assistantVisibility } : {},
...deprecatedChoiceIds.length > 0 ? { deprecatedChoiceIds } : {},
...groupId ? { groupId } : {},
...groupLabel ? { groupLabel } : {},
...groupHint ? { groupHint } : {},
...entry.onboardingFeatured === true ? { onboardingFeatured: true } : {},
...entry.appGuidedDiscovery === true ? { appGuidedDiscovery: true } : {},
...optionKey ? { optionKey } : {},
...cliFlag ? { cliFlag } : {},
...cliOption ? { cliOption } : {},
...cliDescription ? { cliDescription } : {},
...entry.appGuidedSecret === true ? { appGuidedSecret: true } : {},
...entry.personalAccount === true ? { personalAccount: true } : {},
...appGuidedActionLabel ? { appGuidedActionLabel } : {},
...appGuidedAuth ? { appGuidedAuth } : {},
...entry.credentialOnly === true ? { credentialOnly: true } : {},
...channelLogin ? { channelLogin } : {},
...onboardingScopes.length > 0 ? { onboardingScopes } : {}
};
});
}
function normalizeConfigUiHints(value) {
if (!isRecord(value)) return;
const normalized = Object.create(null);
for (const [hintPath, rawHint] of Object.entries(value)) {
if (!isRecord(rawHint)) continue;
const hint = { ...rawHint };
if ("presentation" in hint && hint.presentation !== "phone-number") delete hint.presentation;
normalized[hintPath] = hint;
}
return Object.keys(normalized).length > 0 ? normalized : void 0;
}
function normalizeChannelConfigs(value) {
return normalizeNamedMetadataRecord(value, (rawEntry) => {
const schema = isRecord(rawEntry.schema) ? rawEntry.schema : null;
if (!schema) return;
const uiHints = normalizeConfigUiHints(rawEntry.uiHints);
const runtime = isRecord(rawEntry.runtime) && typeof rawEntry.runtime.safeParse === "function" ? rawEntry.runtime : void 0;
const label = normalizeOptionalString(rawEntry.label) ?? "";
const description = normalizeOptionalString(rawEntry.description) ?? "";
const preferOver = normalizeTrimmedStringList(rawEntry.preferOver);
const commandDefaults = normalizeManifestChannelCommandDefaults(rawEntry.commands);
return {
schema,
...uiHints ? { uiHints } : {},
...runtime ? { runtime } : {},
...label ? { label } : {},
...description ? { description } : {},
...preferOver.length > 0 ? { preferOver } : {},
...commandDefaults ? { commands: commandDefaults } : {}
};
});
}
function normalizeManifestChannelCommandDefaults(value) {
if (!isRecord(value)) return;
const commands = {};
for (const key of ["nativeCommandsAutoEnabled", "nativeSkillsAutoEnabled"]) if (typeof value[key] === "boolean") commands[key] = value[key];
return Object.keys(commands).length > 0 ? commands : void 0;
}
//#endregion
//#region packages/gateway-protocol/src/theme-ids.ts
/** Lightweight theme identifiers used while browser preferences boot. */
const BUILTIN_THEME_IDS = [
"claw",
"knot",
"dash",
"absolutely",
"tide",
"beacon",
"phosphor",
"crt",
"manuscript",
"rose",
"miami"
];
const THEME_LOCAL_ID_PATTERN = new RegExp(`^[a-z0-9][a-z0-9_-]{0,63}$`);
function isBuiltinThemeId(value) {
return BUILTIN_THEME_IDS.some((id) => id === value);
}
function isThemeId(value) {
if (isBuiltinThemeId(value)) return true;
if (typeof value !== "string" || value.length > 256) return false;
const separator = value.lastIndexOf("/");
const owner = value.slice(0, separator);
return separator > 0 && !owner.startsWith("user/") && /^@?[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*)*$/i.test(owner) && THEME_LOCAL_ID_PATTERN.test(value.slice(separator + 1));
}
//#endregion
//#region packages/gateway-protocol/src/theme.ts
const THEME_COLOR_KEYS = [
"background",
"foreground",
"card",
"card-foreground",
"popover",
"popover-foreground",
"primary",
"primary-foreground",
"secondary",
"secondary-foreground",
"muted",
"muted-foreground",
"accent",
"accent-foreground",
"destructive",
"destructive-foreground",
"border",
"input",
"ring"
];
const THEME_FONT_KEYS = ["font-sans", "font-mono"];
const THEME_MASCOT_VALUES = ["claw", "none"];
const THEME_CRITTER_IDS = ["penguin", "fedora"];
const THEME_AVATAR_HAT_IDS = [
"fedora",
"crown",
"santa",
"party",
"pumpkin"
];
const THEME_ARTWORK_ID_PATTERN = /^[a-z0-9][a-z0-9_-]{0,31}$/;
const MAX_THEME_DEFINITION_BYTES = 4096;
[
{
id: "claw",
name: "Claw",
description: "Signature coral red and teal on charcoal or pale surfaces, with Instrument Sans throughout. A balanced everyday workspace."
},
{
id: "knot",
name: "Knot",
description: "Crimson accents on true black or clean white, with Geist typography. Sharp, minimal, and high contrast."
},
{
id: "dash",
name: "Dash",
description: "Toasted caramel on deep cocoa or warm cream, with DM Sans controls and Fraunces serif chat. Warm and bookish."
},
{
id: "absolutely",
name: "Absolutely",
description: "Terracotta clay on warm graphite or soft cream, with Space Grotesk controls and Lora serif chat. Quiet editorial character."
},
{
id: "tide",
name: "Tide",
description: "Steel cyan on cool slate or pale blue-gray, with IBM Plex Sans throughout. Calm, technical, and understated."
},
{
id: "beacon",
name: "Beacon",
description: "High-visibility amber on black or white, bold focus rings, and Atkinson Hyperlegible typography. Designed for maximum readability."
},
{
id: "phosphor",
name: "Phosphor",
description: "Luminous green on green-tinted black or pale green, with JetBrains Mono throughout. A classic green terminal atmosphere."
},
{
id: "crt",
name: "CRT",
description: "White phosphor and amber on tube black, with a light counterpart, JetBrains Mono, and nearly square corners. A retro computer console."
},
{
id: "manuscript",
name: "Manuscript",
description: "Lapis blue and gold on aged paper, with a dark reading-room variant and Lora serif throughout. A quiet illuminated manuscript."
},
{
id: "rose",
name: "Rosé",
description: "Dried rose and gold on deep plum-gray or soft rose-tinted cream, with DM Sans. Gentle, muted, and warm."
},
{
id: "miami",
name: "Miami",
description: "Hot magenta and cyan on violet-black or pale lavender, with Space Grotesk. Bright neon energy and a synthwave character."
}
].map((theme) => ({
id: theme.id,
name: theme.name,
description: theme.description,
source: "builtin",
modes: ["light", "dark"]
}));
function requireRecord(value, label) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
return value;
}
function requireKeys(value, allowed, label) {
const unknown = Object.keys(value).find((key) => !allowed.includes(key));
if (unknown) throw new Error(`${label} has unsupported field ${unknown}`);
}
function requireText(value, label, maxLength) {
if (typeof value !== "string" || !value.trim() || value.trim().length > maxLength || Array.from(value).some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127)) throw new Error(`${label} must be nonempty text of at most ${maxLength} characters`);
return value.trim();
}
const NUMBER = "[+-]?(?:\\d+(?:\\.\\d+)?|\\.\\d+)(?:e[+-]?\\d+)?";
const COMPONENT = `${NUMBER}%?`;
const HUE = `${NUMBER}(?:deg|grad|rad|turn)?`;
const LEGACY_COLOR_FUNCTION = new RegExp(`^(?:(?:rgb|rgba)\\( *(?:${NUMBER} *, *${NUMBER} *, *${NUMBER}|${NUMBER}% *, *${NUMBER}% *, *${NUMBER}%)|(?:hsl|hsla)\\( *${HUE} *, *${NUMBER}% *, *${NUMBER}%)(?: *, *${COMPONENT})? *\\)$`, "i");
const COLOR_FUNCTION = new RegExp(`^(?:(?:rgb|rgba|oklab|lab)\\( *${COMPONENT} +${COMPONENT} +${COMPONENT}|(?:hsl|hsla)\\( *${HUE} +${COMPONENT} +${COMPONENT}|(?:oklch|lch)\\( *${COMPONENT} +${COMPONENT} +${HUE})(?: */ *${COMPONENT})? *\\)$`, "i");
const COLOR_SPACE_FUNCTION = new RegExp(`^color\\( *(?:srgb|srgb-linear|display-p3|a98-rgb|prophoto-rgb|rec2020|xyz|xyz-d50|xyz-d65) +${COMPONENT} +${COMPONENT} +${COMPONENT}(?: */ *${COMPONENT})? *\\)$`, "i");
const FONT_IDENTIFIER = "(?:--[a-z0-9_-]*|-?[a-z_][a-z0-9_-]*)";
const FONT_FAMILY = `(?:"[a-z0-9 ,'._-]*"|'[a-z0-9 ,"._-]*'|${FONT_IDENTIFIER}(?: +${FONT_IDENTIFIER})*)`;
const FONT_FAMILY_LIST = new RegExp(`^${FONT_FAMILY}(?: *, *${FONT_FAMILY})*$`, "i");
const CSS_WIDE_KEYWORDS = /* @__PURE__ */ new Set([
"inherit",
"initial",
"unset",
"revert",
"revert-layer"
]);
const GENERIC_FONT_FAMILIES = /* @__PURE__ */ new Set([
"serif",
"sans-serif",
"monospace",
"cursive",
"fantasy",
"system-ui",
"ui-serif",
"ui-sans-serif",
"ui-monospace",
"ui-rounded",
"emoji",
"math",
"fangsong",
"-webkit-body"
]);
function isFontFamilyList(value) {
if (!FONT_FAMILY_LIST.test(value)) return false;
return value.replace(/"[^"]*"|'[^']*'/g, "").split(",").every((family) => {
const words = family.trim().toLowerCase().split(/ +/);
return words.every((word) => !CSS_WIDE_KEYWORDS.has(word) && word !== "default" && (words.length === 1 || !GENERIC_FONT_FAMILIES.has(word)));
});
}
function requireColor(value, label) {
const color = requireText(value, label, 120);
if (!/^#(?:[0-9a-f]{3,4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(color) && !/^(?:transparent|black|white)$/i.test(color) && !LEGACY_COLOR_FUNCTION.test(color) && !COLOR_FUNCTION.test(color) && !COLOR_SPACE_FUNCTION.test(color)) throw new Error(`${label} must be a hex, rgb, hsl, lab, lch, oklab, oklch, or color() color`);
return color;
}
function normalizePalette(value, mode) {
const palette = requireRecord(value, `theme.${mode}`);
requireKeys(palette, [...THEME_COLOR_KEYS, ...THEME_FONT_KEYS], `theme.${mode}`);
const entries = THEME_COLOR_KEYS.map((key) => [key, requireColor(palette[key], `theme.${mode}.${key}`)]);
const result = { ...Object.fromEntries(entries) };
for (const key of THEME_FONT_KEYS) {
if (palette[key] === void 0) continue;
const font = requireText(palette[key], `theme.${mode}.${key}`, 120);
if (!isFontFamilyList(font)) throw new Error(`theme.${mode}.${key} must contain only font family names`);
result[key] = font;
}
return result;
}
/** Rejects executable CSS and incomplete palettes before they reach storage or a stylesheet. */
function normalizeThemeDefinition(input, options) {
const record = requireRecord(input, "theme");
requireKeys(record, [
"name",
"description",
"mascot",
"workingPhrases",
"critters",
"avatarHat",
"light",
"dark"
], "theme");
const definition = {
name: requireText(record.name, "theme.name", 80),
description: requireText(record.description, "theme.description", 320),
...record.light !== void 0 ? { light: normalizePalette(record.light, "light") } : {},
...record.dark !== void 0 ? { dark: normalizePalette(record.dark, "dark") } : {}
};
if (record.mascot !== void 0) {
const mascot = THEME_MASCOT_VALUES.find((candidate) => candidate === record.mascot);
if (!mascot) throw new Error(`theme.mascot must be one of ${THEME_MASCOT_VALUES.join(", ")}`);
definition.mascot = mascot;
}
if (record.workingPhrases !== void 0) {
if (!Array.isArray(record.workingPhrases) || record.workingPhrases.length > 24) throw new Error(`theme.workingPhrases must be an array of at most 24 entries`);
const phrases = Array.from(record.workingPhrases, (phrase, index) => requireText(phrase, `theme.workingPhrases[${index}]`, 24));
if (new Set(phrases).size !== phrases.length) throw new Error("theme.workingPhrases must not contain duplicate entries after trimming");
definition.workingPhrases = phrases;
}
if (record.critters !== void 0) {
if (!Array.isArray(record.critters) || record.critters.length > 8) throw new Error("theme.critters must be an array of at most 8 entries");
const allowedIds = [...THEME_CRITTER_IDS, ...options?.critterIds ?? []];
const critters = Array.from(record.critters, (entry, index) => {
const critter = allowedIds.find((id) => id === entry);
if (!critter) throw new Error(`theme.critters[${index}] must be one of ${allowedIds.join(", ")}`);
return critter;
});
if (new Set(critters).size !== critters.length) throw new Error("theme.critters must not contain duplicate entries");
definition.critters = critters;
}
if (record.avatarHat !== void 0) {
const allowedIds = [...THEME_AVATAR_HAT_IDS, ...options?.hatIds ?? []];
const avatarHat = allowedIds.find((id) => id === record.avatarHat);
if (!avatarHat) throw new Error(`theme.avatarHat must be one of ${allowedIds.join(", ")}`);
definition.avatarHat = avatarHat;
}
if (!definition.light && !definition.dark) throw new Error("theme must provide at least one light or dark palette");
if (new TextEncoder().encode(JSON.stringify(definition)).length > 4096) throw new Error(`theme definition exceeds ${MAX_THEME_DEFINITION_BYTES} bytes`);
return definition;
}
//#endregion
//#region src/plugins/manifest-themes.ts
const MAX_PLUGIN_THEMES = 32;
const MAX_THEME_ARTWORK_ENTRIES = 8;
function objectProperties(value) {
return value.type === "ObjectExpression" ? value.properties.filter((property) => property.type === "Property") : [];
}
function propertyName(property) {
return property.key.type === "Identifier" ? property.key.name : property.key.type === "Literal" ? String(property.key.value) : "";
}
function validateArtworkDuplicates(source) {
const { parseExpressionAt } = createRequire(import.meta.url)("acorn");
const root = parseExpressionAt(`(${source}\n)`, 0, { ecmaVersion: "latest" });
for (const property of objectProperties(root)) {
if (propertyName(property) !== "themes" || property.value.type !== "ArrayExpression") continue;
for (const theme of property.value.elements) {
if (!theme || theme.type !== "ObjectExpression") continue;
for (const artwork of objectProperties(theme)) {
const kind = propertyName(artwork);
if (kind !== "hats" && kind !== "critters") continue;
const ids = /* @__PURE__ */ new Set();
for (const entry of objectProperties(artwork.value)) {
const id = propertyName(entry);
if (ids.has(id)) throw new Error(`themes.${kind} has duplicate artwork ID ${id}`);
ids.add(id);
}
}
}
}
}
function normalizeArtworkEntries(value, label, catalogIds) {
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be a map with at most ${MAX_THEME_ARTWORK_ENTRIES} entries`);
const entries = Object.entries(value);
if (entries.length > MAX_THEME_ARTWORK_ENTRIES) throw new Error(`${label} must be a map with at most ${MAX_THEME_ARTWORK_ENTRIES} entries`);
for (const [id] of entries) if (!THEME_ARTWORK_ID_PATTERN.test(id) || catalogIds.includes(id)) throw new Error(`${label}.${id} must use a safe local ID outside the built-in catalog`);
return entries;
}
function normalizeArtworkPath(value, label) {
const relativePath = typeof value === "string" ? value.replace(/^\.\//, "") : "";
if (!/^(?:[a-z0-9_-][a-z0-9._-]*\/)*[a-z0-9_-][a-z0-9._-]*\.svg$/i.test(relativePath)) throw new Error(`${label} must be an SVG file inside the plugin root`);
return relativePath;
}
function normalizeArtwork(hats, critters, label) {
return {
...hats !== void 0 ? { hats: Object.fromEntries(normalizeArtworkEntries(hats, `${label}.hats`, THEME_AVATAR_HAT_IDS).map(([id, source]) => [id, normalizeArtworkPath(source, `${label}.hats.${id}`)])) } : {},
...critters !== void 0 ? { critters: Object.fromEntries(normalizeArtworkEntries(critters, `${label}.critters`, THEME_CRITTER_IDS).map(([id, metadata]) => {
const entryLabel = `${label}.critters.${id}`;
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) throw new Error(`${entryLabel} must be an object with an SVG source`);
const { source, title, crossMs } = metadata;
if (Object.keys(metadata).some((key) => ![
"source",
"title",
"crossMs"
].includes(key))) throw new Error(`${entryLabel} has unsupported fields`);
if (title !== void 0 && (typeof title !== "string" || title.length > 60 || /[\p{Cc}\p{Cf}\p{Cs}]/u.test(title))) throw new Error(`${entryLabel}.title must be at most 60 printable characters`);
if (crossMs !== void 0 && (typeof crossMs !== "number" || !Number.isInteger(crossMs) || crossMs < 5e3 || crossMs > 9e4)) throw new Error(`${entryLabel}.crossMs must be an integer from 5000 to 90000`);
return [id, {
source: normalizeArtworkPath(source, `${entryLabel}.source`),
...title !== void 0 ? { title } : {},
...crossMs !== void 0 ? { crossMs } : {}
}];
})) } : {}
};
}
function normalizeManifestThemes(value, pluginId, manifestSource) {
if (value === void 0) return { ok: true };
if (!Array.isArray(value) || value.length > MAX_PLUGIN_THEMES) return {
ok: false,
error: `themes must be an array with at most ${MAX_PLUGIN_THEMES} entries`
};
if (value.length && (pluginId === "user" || !isThemeId(`${pluginId}/x`))) return {
ok: false,
error: "themes require a portable plugin ID outside the reserved user namespace"
};
const themes = [];
const ids = /* @__PURE__ */ new Set();
for (const [index, entry] of value.entries()) {
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return {
ok: false,
error: `themes[${index}] must be an object`
};
const { id, name, description, source, hats, critters } = entry;
if (Object.keys(entry).some((key) => ![
"id",
"name",
"description",
"source",
"hats",
"critters"
].includes(key)) || typeof id !== "string" || !THEME_LOCAL_ID_PATTERN.test(id) || ids.has(id) || typeof name !== "string" || !name.trim() || name.trim().length > 80 || typeof description !== "string" || !description.trim() || description.trim().length > 320 || Array.from(name + description).some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127) || typeof source !== "string") return {
ok: false,
error: `themes[${index}] requires a unique safe id, name, description, and JSON source`
};
const relativePath = source.replace(/^\.\//, "");
if (!/^(?:[a-z0-9_-][a-z0-9._-]*\/)*[a-z0-9_-][a-z0-9._-]*\.json$/i.test(relativePath)) return {
ok: false,
error: `themes[${index}].source must be a JSON file inside the plugin root`
};
try {
themes.push({
id,
name: name.trim(),
description: description.trim(),
source: relativePath,
...normalizeArtwork(hats, critters, `themes[${index}]`)
});
} catch (error) {
return {
ok: false,
error: error instanceof Error ? error.message : "invalid theme artwork"
};
}
ids.add(id);
}
if (manifestSource && themes.some((theme) => theme.hats || theme.critters)) try {
validateArtworkDuplicates(manifestSource);
} catch (error) {
return {
ok: false,
error: error instanceof Error ? error.message : "invalid theme artwork"
};
}
return {
ok: true,
themes
};
}
//#endregion
//#region src/plugins/manifest.ts
/** Loads and normalizes OpenClaw plugin manifests, including contracts and config schemas. */
/** Canonical plugin manifest filename inside plugin roots. */
const PLUGIN_MANIFEST_FILENAME = "openclaw.plugin.json";
const MAX_PLUGIN_MANIFEST_BYTES = 262144;
const CORE_RESERVED_PLUGIN_IDS = /* @__PURE__ */ new Set(["node-mcp"]);
const VALID_PLUGIN_KINDS = /* @__PURE__ */ new Set(["memory", "context-engine"]);
function isCoreReservedPluginId(id) {
return CORE_RESERVED_PLUGIN_IDS.has(normalizePluginPolicyId(id));
}
function parsePluginKind(raw) {
const values = typeof raw === "string" ? [raw] : Array.isArray(raw) ? raw : [];
const kinds = [];
for (const value of values) {
if (typeof value !== "string" || !VALID_PLUGIN_KINDS.has(value)) continue;
const kind = value;
if (!kinds.includes(kind)) kinds.push(kind);
}
return kinds.length === 0 ? void 0 : kinds.length === 1 ? kinds[0] : kinds;
}
function parseDoctorStateMigrationDescriptors(raw) {
if (typeof raw === "boolean") return raw;
if (!Array.isArray(raw)) return;
const seen = /* @__PURE__ */ new Set();
return raw.flatMap((value) => {
if (!isRecord(value)) return [];
const id = normalizeOptionalString(value.id);
if (!id || seen.has(id)) return [];
seen.add(id);
return [{
id,
...value.doctorOnly === true ? { doctorOnly: true } : {},
...value.phase === "after-session-repair" ? { phase: "after-session-repair" } : {}
}];
});
}
function parseManifestBackupResources(raw) {
if (raw === void 0) return { ok: true };
if (!Array.isArray(raw)) return {
ok: false,
error: "backupResources must be an array"
};
const resources = /* @__PURE__ */ new Map();
for (const [index, entry] of raw.entries()) {
if (!isRecord(entry) || Object.keys(entry).length !== 3 || !("disposition" in entry) || !("scope" in entry) || !("relativePath" in entry)) return {
ok: false,
error: `backupResources[${index}] must contain only disposition, scope, and relativePath`
};
const { disposition, scope, relativePath } = entry;
if (disposition !== "include" && disposition !== "regenerable") return {
ok: false,
error: `backupResources[${index}].disposition is invalid`
};
if (scope !== "state" && scope !== "agent") return {
ok: false,
error: `backupResources[${index}].scope is invalid`
};
if (typeof relativePath !== "string" || !relativePath || relativePath.includes("\\") || relativePath.includes("\0") || path.posix.isAbsolute(relativePath) || path.win32.isAbsolute(relativePath) || /^[A-Za-z][A-Za-z\d+.-]*:/.test(relativePath) || relativePath.split("/").some((segment) => !segment || segment === "." || segment === "..")) return {
ok: false,
error: `backupResources[${index}].relativePath must be a strict relative POSIX path`
};
const resource = {
disposition,
scope,
relativePath
};
resources.set(`${scope}\0${relativePath}\0${disposition}`, resource);
}
return {
ok: true,
resources: [...resources.entries()].toSorted(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([, resource]) => resource)
};
}
function loadPluginManifest(rootDir, rejectHardlinks = true, rootRealPath) {
const file = readPluginCacheFile({
rootDir,
relativePath: PLUGIN_MANIFEST_FILENAME,
...rootRealPath !== void 0 ? { rootRealPath } : {},
maxBytes: MAX_PLUGIN_MANIFEST_BYTES,
rejectHardlinks
});
const manifestPath = file.ok ? file.path : path.join(rootDir, PLUGIN_MANIFEST_FILENAME);
if (!file.ok) return matchRootFileOpenFailure(file.failure, {
path: () => ({
ok: false,
error: `plugin manifest not found: ${manifestPath}`,
manifestPath
}),
fallback: (failure) => ({
ok: false,
error: `unsafe plugin manifest path: ${manifestPath} (${failure.reason})`,
manifestPath
})
});
if (file.manifest) return file.manifest;
const cacheResult = (result) => {
return file.manifest = result;
};
const parsed = parsePluginCacheJson(file, { json5: true });
if (!parsed.ok) return cacheResult({
ok: false,
error: `failed to parse plugin manifest: ${String(parsed.error)}`,
manifestPath
});
const raw = parsed.value;
if (!isRecord(raw)) return cacheResult({
ok: false,
error: "plugin manifest must be an object",
manifestPath
});
const id = normalizeOptionalString(raw.id) ?? "";
if (!id) return cacheResult({
ok: false,
error: "plugin manifest requires id",
manifestPath
});
if (isCoreReservedPluginId(id)) return cacheResult({
ok: false,
error: `plugin manifest id "${id}" is reserved by OpenClaw core`,
manifestPath
});
const configSchema = isRecord(raw.configSchema) ? raw.configSchema : null;
if (!configSchema) return cacheResult({
ok: false,
error: "plugin manifest requires configSchema",
manifestPath
});
const backupResources = parseManifestBackupResources(raw.backupResources);
if (!backupResources.ok) return cacheResult({
ok: false,
error: `invalid plugin manifest backupResources: ${backupResources.error}`,
manifestPath,
diagnosticCode: "backup-resource-declaration-invalid"
});
const categories = validatePluginCategories(raw.categories);
if (!categories.ok) return cacheResult({
ok: false,
error: `invalid plugin manifest categories: ${categories.error}`,
manifestPath
});
const requiresPlugins = normalizeTrimmedStringList(raw.requiresPlugins);
const enabledByDefaultOnPlatforms = normalizeManifestPlatforms(raw.enabledByDefaultOnPlatforms);
const legacyPluginIds = normalizeTrimmedStringList(raw.legacyPluginIds);
const autoEnableWhenConfiguredProviders = normalizeTrimmedStringList(raw.autoEnableWhenConfiguredProviders);
const providers = normalizeTrimmedStringList(raw.providers);
const channels = normalizeTrimmedStringList(raw.channels);
const contracts = normalizeManifestContracts(raw.contracts);
const cliBackends = normalizeTrimmedStringList(raw.cliBackends);
const rawDoctorContract = isRecord(raw.doctorContract) ? raw.doctorContract : void 0;
const stateMigrations = parseDoctorStateMigrationDescriptors(rawDoctorContract?.stateMigrations);
const doctorContract = rawDoctorContract ? {
...Object.fromEntries([
"configRepair",
"resolveSessionStoreAgentIds",
"sessionRouteStateOwners"
].flatMap((key) => typeof rawDoctorContract[key] === "boolean" ? [[key, rawDoctorContract[key]]] : [])),
...stateMigrations !== void 0 ? { stateMigrations } : {}
} : void 0;
const manifestBeforeDashboard = {
id,
configSchema,
..."categories" in categories ? { categories: categories.categories } : {},
...backupResources.resources !== void 0 ? { backupResources: backupResources.resources } : {},
...requiresPlugins.length > 0 ? { requiresPlugins } : {},
...raw.enabledByDefault === true ? { enabledByDefault: true } : {},
...enabledByDefaultOnPlatforms.length > 0 ? { enabledByDefaultOnPlatforms } : {},
...legacyPluginIds.length > 0 ? { legacyPluginIds } : {},
...autoEnableWhenConfiguredProviders.length > 0 ? { autoEnableWhenConfiguredProviders } : {},
kind: parsePluginKind(raw.kind),
channels,
channelAccountKeyPolicies: normalizeChannelAccountKeyPolicies(raw.channelAccountKeyPolicies, channels),
providers,
providerCatalogEntry: normalizeOptionalString(raw.providerCatalogEntry),
capabilityCatalogEntry: raw.capabilityCatalogEntry === void 0 ? void 0 : normalizeOptionalString(raw.capabilityCatalogEntry) ?? "",
modelSupport: normalizeManifestModelSupport(raw.modelSupport),
modelCatalog: normalizeModelCatalog(raw.modelCatalog, { ownedProviders: /* @__PURE__ */ new Set([...providers, ...cliBackends]) }),
modelPricing: normalizeManifestModelPricing(raw.modelPricing, { ownedProviders: new Set(providers) }),
modelIdNormalization: normalizeManifestModelIdNormalization(raw.modelIdNormalization, { ownedProviders: new Set(providers) }),
providerEndpoints: normalizeManifestProviderEndpoints(raw.providerEndpoints),
providerRequest: normalizeManifestProviderRequest(raw.providerRequest, { ownedProviders: new Set(providers) }),
secretProviderIntegrations: normalizeManifestSecretProviderIntegrations(raw.secretProviderIntegrations),
cliBackends,
syntheticAuthRefs: normalizeTrimmedStringList(raw.syntheticAuthRefs),
nonSecretAuthMarkers: normalizeTrimmedStringList(raw.nonSecretAuthMarkers),
commandAliases: normalizeManifestCommandAliases(raw.commandAliases),
cliCommands: normalizeManifestCliCommands(raw.cliCommands),
providerUsageAuthEnvVars: normalizeStringListRecord(raw.providerUsageAuthEnvVars),
providerAuthAliases: normalizeManifestProviderAuthAliases(raw.providerAuthAliases),
providerAuthChoices: normalizeProviderAuthChoices(raw.providerAuthChoices),
activation: normalizeManifestActivation(raw.activation),
setup: normalizeManifestSetup(raw.setup),
doctorContract,
doctorHealthChecks: raw.doctorHealthChecks === true ? true : void 0,
sessionRouteStateOwners: raw.sessionRouteStateOwners === void 0 ? void 0 : coerceDoctorSessionRouteStateOwners(raw.sessionRouteStateOwners),
qaRunners: normalizeManifestQaRunners(raw.qaRunners)
};
const dashboardResult = normalizeManifestDashboard(raw.dashboard);
if (!dashboardResult.ok) return cacheResult({
ok: false,
error: `invalid plugin manifest dashboard: ${dashboardResult.error}`,
manifestPath
});
const uiCapabilities = validatePluginUiCapabilities(raw.uiCapabilities);
const controlUiResult = normalizeManifestControlUi(raw.controlUi);
if (!controlUiResult.ok) return cacheResult({
ok: false,
error: `invalid plugin manifest controlUi: ${controlUiResult.error}`,
manifestPath
});
const themesResult = normalizeManifestThemes(raw.themes, id, file.contents.toString("utf8"));
if (!themesResult.ok) return cacheResult({
ok: false,
error: `invalid plugin manifest themes: ${themesResult.error}`,
manifestPath
});
return cacheResult({
ok: true,
...!uiCapabilities.ok ? { warnings: [`ignoring invalid plugin manifest uiCapabilities: ${uiCapabilities.error}`] } : {},
manifest: {
...manifestBeforeDashboard,
dashboard: dashboardResult.dashboard,
controlUi: controlUiResult.value,
...uiCapabilities.ok && uiCapabilities.capabilities !== void 0 ? { uiCapabilities: uiCapabilities.capabilities } : {},
themes: themesResult.themes,
mcpServers: normalizeManifestMcpServers(raw.mcpServers),
skills: normalizeTrimmedStringList(raw.skills),
name: normalizeOptionalString(raw.name),
description: normalizeOptionalString(raw.description),
catalog: normalizeManifestCatalog(raw.catalog),
version: normalizeOptionalString(raw.version),
uiHints: normalizeConfigUiHints(raw.uiHints),
configGroups: normalizeConfigGroups(raw.configGroups, configSchema),
contracts,
decisionModels: normalizeManifestDecisionModels(raw.decisionModels, contracts?.decisionProviders),
transcriptSources: normalizeManifestTranscriptSources(raw.transcriptSources, contracts?.transcriptSourceProviders),
mediaUnderstandingProviderMetadata: normalizeMediaUnderstandingProviderMetadata(raw.mediaUnderstandingProviderMetadata),
imageGenerationProviderMetadata: normalizeCapabilityProviderMetadata(raw.imageGenerationProviderMetadata),
videoGenerationProviderMetadata: normalizeCapabilityProviderMetadata(raw.videoGenerationProviderMetadata),
musicGenerationProviderMetadata: normalizeCapabilityProviderMetadata(raw.musicGenerationProviderMetadata),
toolMetadata: normalizePluginToolMetadata(raw.toolMetadata),
configContracts: normalizeManifestConfigContracts(raw.configContracts),
channelConfigs: normalizeChannelConfigs(raw.channelConfigs)
},
manifestPath
});
}
//#endregion
//#region src/plugins/bundle-manifest.ts
/** Reads Agent/Codex/Claude/Cursor bundle manifests into OpenClaw plugin manifest metadata. */
/** Relative manifest path for Codex-style plugin bundles. */
const CODEX_BUNDLE_MANIFEST_RELATIVE_PATH = ".codex-plugin/plugin.json";
const CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH = ".claude-plugin/plugin.json";
const CURSOR_BUNDLE_MANIFEST_RELATIVE_PATH = ".cursor-plugin/plugin.json";
const AGENT_BUNDLE_MANIFEST_RELATIVE_PATH = "plugin.json";
const AGENT_BUNDLE_EXTENSION_NAMESPACE = "ai.openclaw";
const AGENT_BUNDLE_MANIFEST_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
const MAX_AGENT_BUNDLE_MANIFEST_BYTES = 262144;
const log$1 = createSubsystemLogger("plugins/bundle-manifest");
/** Normalizes string-or-list path fields from bundle manifests. */
function normalizeBundlePathList(value) {
return normalizeUniqueSingleOrTrimmedStringList(value);
}
function mergeBundlePathLists(...groups) {
return [...new Set(groups.flat())];
}
function hasInlineCapabilityValue(value) {
if (typeof value === "string") return value.trim().length > 0;
if (Array.isArray(value)) return value.length > 0;
if (isRecord(value)) return Object.keys(value).length > 0;
return value === true;
}
function slugifyPluginId(raw, rootDir) {
const fallback = path.basename(rootDir);
return (normalizeLowercaseStringOrEmpty(raw) || normalizeLowercaseStringOrEmpty(fallback)).replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "bundle-plugin";
}
function loadBundleManifestFile(params) {
const manifestPath = path.join(params.rootDir, params.manifestRelativePath);
const file = readPluginCacheFile({
rootDir: params.rootDir,
rootRealPath: params.rootRealPath,
relativePath: params.manifestRelativePath,
rejectHardlinks: params.rejectHardlinks,
maxBytes: MAX_AGENT_BUNDLE_MANIFEST_BYTES
});
if (!file.ok) return matchRootFileOpenFailure(file.failure, {
path: () => {
if (params.allowMissing) return {
ok: true,
raw: {},
manifestPath
};
return {
ok: false,
error: `plugin manifest not found: ${manifestPath}`,
manifestPath
};
},
fallback: (failure) => ({
ok: false,
error: `unsafe plugin manifest path: ${manifestPath} (${failure.reason})`,
manifestPath
})
});
const result = parsePluginCacheJson(file, { json5: !params.strictJson });
if (!result.ok) return {
ok: false,
error: `failed to parse plugin manifest: ${formatErrorMessage(result.error)}`,
manifestPath
};
if (!isRecord(result.value)) return {
ok: false,
error: "plugin manifest must be an object",
manifestPath
};
return {
ok: true,
raw: result.value,
manifestPath
};
}
function resolveCodexComponentDirs(raw, rootDir, component) {
const declared = normalizeBundlePathList(raw[component]);
if (declared.length > 0) return declared;
return pluginCacheExistsSync(path.join(rootDir, component)) ? [component] : [];
}
function resolveCursorCommandRootDirs(raw, rootDir) {
return resolveBundleComponentPaths(raw.commands, rootDir, [".cursor/commands"]);
}
function resolveCursorSkillDirs(raw, rootDir) {
return mergeBundlePathLists(resolveBundleComponentPaths(raw.skills, rootDir, ["skills"]), resolveCursorCommandRootDirs(raw, rootDir));
}
function resolveCursorAgentDirs(raw, rootDir) {
return resolveBundleComponentPaths(raw.subagents ?? raw.agents, rootDir, [".cursor/agents"]);
}
function resolveBundleComponentPaths(value, rootDir, defaults) {
const declared = normalizeBundlePathList(value);
return mergeBundlePathLists(defaults.filter((candidate) => pluginCacheExistsSync(path.join(rootDir, candidate))), declared);
}
function buildCursorCapabilities(raw, rootDir) {
const capabilities = [
...resolveCursorSkillDirs(raw, rootDir).length > 0 ? ["skills"] : [],
...resolveCursorCommandRootDirs(raw, rootDir).length > 0 ? ["commands"] : [],
...resolveCursorAgentDirs(raw, rootDir).length > 0 ? ["agents"] : []
];
for (const [capability, defaultPath] of [
["hooks", ".cursor/hooks.json"],
["rules", ".cursor/rules"],
["mcpServers", ".mcp.json"]
]) if (hasInlineCapabilityValue(raw[capability]) || pluginCacheExistsSync(path.join(rootDir, defaultPath))) capabilities.push(capability);
return capabilities;
}
function resolveAgentSkillDirs(rootDir) {
try {
return pluginCacheStatSync(path.join(rootDir, "skills"))?.isDirectory() ? ["skills"] : [];
} catch {
return [];
}
}
function resolveAgentActivation(raw, manifestPath) {
if (raw.extensions === void 0) return;
if (!isRecord(raw.extensions)) {
log$1.warn(`ignoring Agent Plugins extensions in ${manifestPath}: expected an object`);
return;
}
const openclawExtension = raw.extensions[AGENT_BUNDLE_EXTENSION_NAMESPACE];
if (openclawExtension === void 0) return;
if (!isRecord(openclawExtension)) {
log$1.warn(`ignoring Agent Plugins ${AGENT_BUNDLE_EXTENSION_NAMESPACE} extension in ${manifestPath}: expected an object`);
return;
}
return normalizeManifestActivation(openclawExtension.activation);
}
function loadBundleManifest(params) {
const rejectHardlinks = params.rejectHardlinks ?? true;
const manifestRelativePath = params.bundleFormat === "codex" ? CODEX_BUNDLE_MANIFEST_RELATIVE_PATH : params.bundleFormat === "cursor" ? CURSOR_BUNDLE_MANIFEST_RELATIVE_PATH : params.bundleFormat === "agent" ? AGENT_BUNDLE_MANIFEST_RELATIVE_PATH : CLAUDE_BUNDLE_MANIFEST_RELATIVE_PATH;
const loaded = loadBundleManifestFile({
rootDir: params.rootDir,
...params.rootRealPath !== void 0 ? { rootRealPath: params.rootRealPath } : {},
manifestRelativePath,
rejectHardlinks,
allowMissing: params.bundleFormat === "claude",
strictJson: params.bundleFormat === "agent"
});
if (!loaded.ok) return loaded;
const raw = loaded.raw;
const interfaceRecord = isRecord(raw.interface) ? raw.interface : void 0;
const name = normalizeOptionalString(raw.name);
const description = normalizeOptionalString(raw.description) ?? normalizeOptionalString(raw.shortDescription) ?? normalizeOptionalString(interfaceRecord?.shortDescription);
const version = normalizeOptionalString(raw.version);
const manifest = {
id: slugifyPluginId(name, params.rootDir),
name,
description,
version,
skills: [],
settingsFiles: [],
hooks: [],
bundleFormat: params.bundleFormat,
activation: void 0,
capabilities: []
};
if (params.bundleFormat === "agent") {
if (raw.$schema !== AGENT_BUNDLE_MANIFEST_SCHEMA) return {
ok: false,
error: `root plugin.json is not an Agent Plugins manifest; expected $schema ${AGENT_BUNDLE_MANIFEST_SCHEMA}`,
manifestPath: loaded.manifestPath
};
if (!name) return {
ok: false,
error: "agent plugin manifest name must be a non-empty string",
manifestPath: loaded.manifestPath
};
manifest.skills = resolveAgentSkillDirs(params.rootDir);
manifest.activation = resolveAgentActivation(raw, loaded.manifestPath);
manifest.capabilities = [...manifest.skills.length > 0 ? ["skills"] : [], ...pluginCacheExistsSync(path.join(params.rootDir, "mcp.json")) ? ["mcpServers"] : []];
} else {
manifest.activation = normalizeManifestActivation(raw.activation);
if (params.bundleFormat === "codex") {
manifest.skills = resolveCodexComponentDirs(raw, params.rootDir, "skills");
manifest.hooks = resolveCodexComponentDirs(raw, params.rootDir, "hooks");
manifest.capabilities = [...manifest.skills.length > 0 ? ["skills"] : [], ...manifest.hooks.length > 0 ? ["hooks"] : []];
for (const [capability, defaultPath] of [["mcpServers", ".mcp.json"], ["apps", ".app.json"]]) if (hasInlineCapabilityValue(raw[capability]) || pluginCacheExistsSync(path.join(params.rootDir, defaultPath))) manifest.capabilities.push(capability);
} else if (params.bundleFormat === "cursor") {
manifest.skills = resolveCursorSkillDirs(raw, params.rootDir);
manifest.capabilities = buildCursorCapabilities(raw, params.rootDir);
} else Object.assign(manifest, resolveClaudeComponents(raw, params.rootDir));
}
const extensions = isRecord(raw.extensions) ? raw.extensions : void 0;
const openai = isRecord(extensions?.["com.openai"]) ? extensions["com.openai"] : void 0;
const onboardingSkill = normalizeOptionalString(openai?.onboardingSkill);
if (onboardingSkill) {
const relative = path.normalize(onboardingSkill);
if (!path.isAbsolute(relative) && relative !== ".." && !relative.startsWith(`..${path.sep}`) && path.basename(relative) === "SKILL.md" && manifest.skills.some((root) => {
const fromRoot = path.relative(path.resolve(params.rootDir, root), path.resolve(params.rootDir, relative));
return fromRoot !== ".." && !fromRoot.startsWith(`..${path.sep}`) && !path.isAbsolute(fromRoot);
})) manifest.onboardingSkill = relative;
}
return {
ok: true,
manifest,
manifestPath: loaded.manifestPath
};
}
function resolveClaudeComponents(raw, rootDir) {
const skillRoots = resolveBundleComponentPaths(raw.skills, rootDir, ["skills"]);
const commands = resolveBundleComponentPaths(raw.commands, rootDir, ["commands"]);
const agents = resolveBundleComponentPaths(raw.agents, rootDir, ["agents"]);
const outputStyles = resolveBundleComponentPaths(raw.outputStyles, rootDir, ["output-styles"]);
const skills = mergeBundlePathLists(skillRoots, commands, agents, outputStyles);
const settingsFiles = pluginCacheExistsSync(path.join(rootDir, "settings.json")) ? ["settings.json"] : [];
const hooks = resolveBundleComponentPaths(raw.hooks, rootDir, ["hooks/hooks.json"]);
return {
skills,
settingsFiles,
hooks,
capabilities: [
...skills.length > 0 ? ["skills"] : [],
...commands.length > 0 ? ["commands"] : [],
...agents.length > 0 ? ["agents"] : [],
...hasInlineCapabilityValue(raw.hooks) || hooks.length > 0 ? ["hooks"] : [],
...hasInlineCapabilityValue(raw.mcpServers) || resolveBundleComponentPaths(raw.mcpServers, rootDir, [".mcp.json"]).length > 0 ? ["mcpServers"] : [],
...hasInlineCapabilityValue(raw.lspServers) || resolveBundleComponentPaths(raw.lspServers, rootDir, [".lsp.json"]).length > 0 ? ["lspServers"] : [],
...hasInlineCapabilityValue(raw.outputStyles) || outputStyles.length > 0 ? ["outputStyles"] : [],
...settingsFiles.length > 0 ? ["settings"] : []
]
};
}
function detectBundleManifestFormat(rootDir, hasPackageExtensions = false) {
if (hasPackageExtensions && pluginCacheExistsSync(path.join(rootDir, "openclaw.plugin.json"))) return null;
if (pluginCacheExistsSync(path.join(rootDir, ".codex-plugin/plugin.json"))) return "codex";
if (pluginCacheExistsSync(path.join(rootDir, ".cursor-plugin/plugin.json"))) return "cursor";
if (pluginCacheExistsSync(path.join(rootDir, ".claude-plugin/plugin.json"))) return "claude";
if (pluginCacheExistsSync(path.join(rootDir, "openclaw.plugin.json"))) return null;
if (pluginCacheExistsSync(path.join(rootDir, "plugin.json"))) {
const agentManifest = loadBundleManifestFile({
rootDir,
manifestRelativePath: AGENT_BUNDLE_MANIFEST_RELATIVE_PATH,
rejectHardlinks: false,
strictJson: true
});
if (agentManifest.ok && agentManifest.raw.$schema === AGENT_BUNDLE_MANIFEST_SCHEMA) return "agent";
}
if (DEFAULT_PLUGIN_ENTRY_CANDIDATES.some((candidate) => pluginCacheExistsSync(path.join(rootDir, candidate)))) return null;
if ([
path.join(rootDir, "skills"),
path.join(rootDir, "commands"),
path.join(rootDir, "agents"),
path.join(rootDir, "hooks", "hooks.json"),
path.join(rootDir, ".mcp.json"),
path.join(rootDir, ".lsp.json"),
path.join(rootDir, "settings.json")
].some((candidate) => pluginCacheExistsSync(candidate))) return "claude";
return null;
}
//#endregion
//#region src/plugins/package-compat.ts
function normalizePartialComparableVersion(version) {
const trimmed = version.trim();
return /^[vV]?[0-9]+\.[0-9]+$/.test(trimmed) ? {
version: `${trimmed}.0`,
isPartial: true
} : {
version: trimmed,
isPartial: false
};
}
function normalizePluginApiVersionForComparator(version, target) {
const normalizedCorrection = OPENCLAW_NUMERIC_CORRECTION_PATTERN.exec(version.trim())?.[1];
if (normalizedCorrection) return normalizedCorrection;
return prerelease(normalizePartialComparableVersion(target).version) ? version : OPENCLAW_RELEASE_SUFFIX_PATTERN.exec(version.trim())?.[1] ?? version;
}
function satisfiesComparator(version, token) {
const trimmed = token.trim();
if (!trimmed) return true;
const match = /^(>=|<=|>|<|=|\^|~)?\s*(.+)$/.exec(trimmed);
if (!match) return false;
const operator = match[1] ?? "";
const target = match[2]?.trim();
if (!target || /^[<>=^~]/.test(target)) return false;
const comparableVersion = normalizePluginApiVersionForComparator(version, target);
const normalizedTarget = normalizePartialComparableVersion(target);
const comparator = normalizedTarget.isPartial && !operator ? `>=${normalizedTarget.version}` : `${operator}${normalizedTarget.version}`;
return satisfies(comparableVersion, comparator, { includePrerelease: true });
}
function satisfiesSemverRange(version, range) {
if (range.includes("||")) return false;
const tokens = normalizeStringEntries(range.trim().split(/\s+/));
if (tokens.length === 0) return false;
return tokens.every((token) => satisfiesComparator(version, token));
}
const OPENCLAW_RELEASE_SUFFIX_PATTERN = /^[vV]?(\d{4}\.[1-9]\d?\.[1-9]\d*)(?:-\d+|-(?:alpha|beta|rc)\.\d+)$/i;
const OPENCLAW_NUMERIC_CORRECTION_PATTERN = /^[vV]?(\d{4}\.[1-9]\d?\.[1-9]\d*)-\d+$/;
/** Resolves the plugin API compatibility range declared by package metadata. */
function resolvePackagePluginApiRange(packageMetadata) {
if (!isRecord(packageMetadata) || !("compat" in packageMetadata)) return { ok: true };
const compat = packageMetadata.compat;
if (compat === void 0 || compat === null) return { ok: true };
if (!isRecord(compat)) return {
ok: false,
error: "package.json openclaw.compat must be an object"
};
if (!("pluginApi" in compat)) return { ok: true };
const pluginApi = compat.pluginApi;
if (typeof pluginApi !== "string") return {
ok: false,
error: "package.json openclaw.compat.pluginApi must be a string"
};
const range = pluginApi.trim();
if (!range) return {
ok: false,
error: "package.json openclaw.compat.pluginApi must not be empty"
};
return {
ok: true,
range
};
}
/** Checks whether a host plugin API version satisfies a package plugin API range. */
function satisfiesPluginApiRange(pluginApiVersion, pluginApiRange) {
if (!pluginApiRange) return true;
return satisfiesSemverRange(pluginApiVersion, pluginApiRange);
}
//#endregion
//#region src/plugins/runtime-degraded-state.ts
/** Boot-stable quarantine state for configured plugins whose payload failed verification. */
const PLUGIN_AVAILABILITY_POLICY = {
state: "configured-unavailable",
severity: "warning",
repairCommand: "openclaw doctor --fix"
};
//#endregion
//#region src/plugins/discovery-availability.ts
const CONFIGURED_PLUGIN_PATH_UNAVAILABLE = "configured-plugin-path-unavailable";
const CONFIGURED_PLUGIN_PATH_INSPECTION_FAILED = "configured-plugin-path-inspection-failed";
function shouldSkipIncompatiblePackagePluginApi(params) {
if (params.origin === "bundled") return false;
const packagePluginApiRangeCheck = resolvePackagePluginApiRange(params.packageManifest);
if (!packagePluginApiRangeCheck.ok) {
params.diagnostics.push({
level: "warn",
configDisposition: "preserve",
source: path.join(params.packageDir, "package.json"),
message: `invalid package plugin API metadata: ${packagePluginApiRangeCheck.error}; skipping discovery (check package.json openclaw.compat.pluginApi)`,
pluginId: params.pluginId
});
return true;
}
const packagePluginApiRange = packagePluginApiRangeCheck.range;
if (!packagePluginApiRange) return false;
const compatibilityHostVersion = resolveCompatibilityHostVersion(params.env);
if (satisfiesPluginApiRange(compatibilityHostVersion, packagePluginApiRange)) return false;
params.diagnostics.push({
level: "warn",
configDisposition: "preserve",
source: path.join(params.packageDir, "package.json"),
message: `plugin requires plugin API ${packagePluginApiRange}, but this host is ${compatibilityHostVersion}; skipping discovery (check "openclaw --version", OPENCLAW_COMPATIBILITY_HOST_VERSION, or run "openclaw doctor")`,
pluginId: params.pluginId
});
return true;
}
function pluginPathFailureDiagnostic(source, origin, error) {
if (origin === "config" && !isMissingPathError(error)) {
const errorCode = extractErrorCode(error) ?? "UNKNOWN";
const fixHint = `${errorCode === "EACCES" || errorCode === "EPERM" ? `Fix permissions on ${source}` : errorCode === "ELOOP" ? `Fix the symbolic link loop at ${source}` : `Resolve the filesystem inspection error on ${source}`}, then run \`${PLUGIN_AVAILABILITY_POLICY.repairCommand}\`.`;
return {
level: "warn",
code: CONFIGURED_PLUGIN_PATH_INSPECTION_FAILED,
configDisposition: "preserve",
errorCode,
source,
fixHint,
message: `Configured plugin load path inspection failed: ${source} (${formatErrorMessageWithCode(error)}). Uninspected plugin configuration is preserved. ${fixHint}`
};
}
return origin === "config" ? {
level: "warn",
code: CONFIGURED_PLUGIN_PATH_UNAVAILABLE,
configDisposition: "preserve",
source,
message: `Configured plugin load path is unavailable: ${source}. Uninspected plugin configuration is preserved. Restore access to the path, then run \`${PLUGIN_AVAILABILITY_POLICY.repairCommand}\`.`
} : {
level: "error",
source,
message: `plugin path not found: ${source}`
};
}
function inspectPluginLoadPath(source, origin, diagnostics) {
try {
const stat = pluginCacheStatSync(source, origin === "config");
if (!stat && !pluginCacheExistsSync(source)) diagnostics.push(pluginPathFailureDiagnostic(source, origin, void 0));
if (origin === "config" && stat) {
if (stat.isDirectory()) readPluginCacheDirectory(source);
else if (!stat.isFile()) throw Object.assign(/* @__PURE__ */ new Error("Plugin load path is not a regular file or directory"), { code: "ERR_INVALID_FILE_TYPE" });
}
return stat;
} catch (error) {
diagnostics.push(pluginPathFailureDiagnostic(source, origin, error));
return null;
}
}
//#endregion
//#region src/plugins/hardlink-policy.ts
/** Enforces plugin root hardlink policy with bundled and immutable Nix-store exceptions. */
const NIX_STORE_ROOT = "/nix/store";
/** Returns true when a plugin root resolves inside the immutable Nix store. */
function isNixStorePluginRoot(rootDir) {
const rootRealPath = pluginCacheRealpathSync(rootDir) ?? path.resolve(rootDir);
return rootRealPath === NIX_STORE_ROOT || rootRealPath.startsWith(`${NIX_STORE_ROOT}/`);
}
/** Decides whether plugin file hardlinks should fail boundary validation for one root. */
function shouldRejectHardlinkedPluginFiles(params) {
if (params.origin === "bundled") return false;
if (resolveIsNixMode(params.env) && isNixStorePluginRoot(params.rootDir)) return false;
return true;
}
//#endregion
//#region src/plugins/discovery-required-plugins.ts
function addMissingRequiredPluginDiagnostics(result, params) {
const candidatePolicyIds = new Set(result.candidates.map((candidate) => normalizePluginPolicyId(candidate.idHint)));
const seen = /* @__PURE__ */ new Set();
let configuredFileManifestPolicyIds;
for (const candidate of result.candidates) for (const requiredPluginId of candidate.requiredPluginIds ?? []) {
const requiredPolicyId = normalizePluginPolicyId(requiredPluginId);
if (candidatePolicyIds.has(requiredPolicyId)) continue;
if (!configuredFileManifestPolicyIds) {
configuredFileManifestPolicyIds = /* @__PURE__ */ new Set();
for (const configuredCandidate of result.candidates) {
if (configuredCandidate.origin !== "config" || configuredCandidate.packageDir) continue;
const rejectHardlinks = shouldRejectHardlinkedPluginFiles({
origin: configuredCandidate.origin,
rootDir: configuredCandidate.rootDir,
env: params.env
});
const manifest = loadPluginManifest(configuredCandidate.rootDir, rejectHardlinks);
if (manifest.ok) configuredFileManifestPolicyIds.add(normalizePluginPolicyId(manifest.manifest.id));
}
}
if (configuredFileManifestPolicyIds.has(requiredPolicyId)) continue;
const key = `${normalizePluginPolicyId(candidate.idHint)}\0${requiredPolicyId}`;
if (seen.has(key)) continue;
seen.add(key);
result.diagnostics.push({
level: "warn",
pluginId: candidate.idHint,
source: candidate.requiredPluginSource ?? candidate.source,
message: `plugin "${candidate.idHint}" requires plugin "${requiredPluginId}"; install "${requiredPluginId}" to use it`
});
}
}
//#endregion
//#region src/plugins/package-entry-resolution.ts
function reportPackageEntryDiagnostic(params, level, message) {
params.diagnostics.push({
level,
...params.pluginIdHint ? { pluginId: params.pluginIdHint } : {},
message,
source: params.sourceLabel
});
return null;
}
function resolvePackageRuntimeExtensionEntries(params) {
const declared = getPackageManifestMetadata(params.manifest ?? void 0)?.runtimeExtensions;
const runtimeExtensions = [];
if (!Array.isArray(declared)) return {
ok: true,
runtimeExtensions
};
for (const [index, entry] of declared.entries()) {
const normalized = normalizeOptionalString(entry);
if (!normalized) return {
ok: false,
error: `package.json openclaw.runtimeExtensions[${index}] must be a non-empty string`
};
runtimeExtensions.push(normalized);
}
if (runtimeExtensions.length > 0 && runtimeExtensions.length !== params.extensions.length) return {
ok: false,
error: `package.json openclaw.runtimeExtensions length (${runtimeExtensions.length}) must match openclaw.extensions length (${params.extensions.length})`
};
return {
ok: true,
runtimeExtensions
};
}
function missingCompiledRuntimeEntryMessage(params) {
const label = params.context === "install" ? "package install" : "installed plugin package";
const recovery = params.context === "install" ? "retry installation after the publisher ships compiled JavaScript" : "update or reinstall the plugin after the publisher ships compiled JavaScript, or disable/uninstall the plugin until then";
return `${label} requires compiled runtime output for TypeScript entry ${params.entry}: expected ${params.candidates.join(", ")}. This is a plugin packaging issue, not a local config problem; ${recovery}. TypeScript source fallback is only supported for source checkouts and local development paths.`;
}
function resolvePackageEntrySource(params) {
const source = path.resolve(params.packageDir, params.entryPath);
const rejectHardlinks = params.rejectHardlinks ?? true;
const candidates = [source];
const openCandidate = (absolutePath) => {
const opened = checkPluginCacheEntry({
rootDir: params.packageDir,
relativePath: path.relative(params.packageDir, absolutePath),
rootRealPath: params.packageRootRealPath,
rejectHardlinks
});
if (!opened.ok) return matchRootFileOpenFailure(opened, {
path: () => null,
io: () => reportPackageEntryDiagnostic(params, "warn", `extension entry unreadable (I/O error): ${params.entryPath}`),
fallback: () => reportPackageEntryDiagnostic(params, "error", `extension entry escapes package directory: ${params.entryPath}`)
});
return opened.exists ? opened.path : null;
};
if (!rejectHardlinks) {
const builtCandidate = source.replace(/\.[^.]+$/u, ".js");
if (builtCandidate !== source) candidates.push(builtCandidate);
}
for (const candidate of candidates) {
if (!pluginCacheExistsSync(candidate)) continue;
return openCandidate(candidate);
}
return openCandidate(source);
}
function resolveSafePackageEntry(params) {
const absolutePath = path.resolve(params.packageDir, params.entryPath);
if (pluginCacheExistsSync(absolutePath)) {
const existingSource = resolvePackageEntrySource(params);
if (!existingSource) return null;
return {
relativePath: path.relative(params.packageDir, absolutePath).replace(/\\/g, "/"),
existingSource
};
}
if (!checkPluginCacheEntry({
rootDir: params.packageDir,
relativePath: params.entryPath,
rootRealPath: params.packageRootRealPath,
rejectHardlinks: params.rejectHardlinks ?? true
}).ok) return reportPackageEntryDiagnostic(params, "error", `extension entry escapes package directory: ${params.entryPath}`);
return { relativePath: path.relative(params.packageDir, absolutePath).replace(/\\/g, "/") };
}
function resolvePackageRuntimeEntrySource(params) {
const safeEntry = resolveSafePackageEntry(params);
if (!safeEntry) return null;
if (params.runtimeEntryPath) {
const runtimeSource = resolvePackageEntrySource({
...params,
entryPath: params.runtimeEntryPath
});
if (runtimeSource) return runtimeSource;
return reportPackageEntryDiagnostic(params, "error", `${params.runtimeEntryLabel ?? "runtime entry"} not found: ${params.runtimeEntryPath}`);
}
if (params.origin === "config" || params.origin === "global") {
const builtEntryCandidates = listBuiltRuntimeEntryCandidates(safeEntry.relativePath);
for (const candidate of builtEntryCandidates) {
if (!pluginCacheExistsSync(path.resolve(params.packageDir, candidate))) continue;
return resolvePackageEntrySource({
...params,
entryPath: candidate
});
}
if ((params.requireBuiltRuntimeEntry ?? params.origin === "global") && isTypeScriptPackageEntry(safeEntry.relativePath)) return reportPackageEntryDiagnostic(params, "warn", missingCompiledRuntimeEntryMessage({
context: "installed",
entry: safeEntry.relativePath,
candidates: builtEntryCandidates
}));
}
if (safeEntry.existingSource) return safeEntry.existingSource;
if (params.rejectHardlinks === false) {
const trustedFallbackSource = resolvePackageEntrySource(params);
if (trustedFallbackSource) return trustedFallbackSource;
}
return reportPackageEntryDiagnostic(params, "error", `${params.sourceEntryLabel ?? "extension entry"} not found: ${safeEntry.relativePath}`);
}
/** Resolves the runtime setup source for a plugin package manifest. */
function resolvePackageSetupSource(params) {
const packageManifest = getPackageManifestMetadata(params.manifest ?? void 0);
const setupEntryPath = normalizeOptionalString(packageManifest?.setupEntry);
if (!setupEntryPath) return null;
return resolvePackageRuntimeEntrySource({
...params,
entryPath: setupEntryPath,
sourceEntryLabel: "setup entry",
runtimeEntryPath: normalizeOptionalString(packageManifest?.runtimeSetupEntry),
runtimeEntryLabel: "runtime setup entry",
pluginIdHint: params.pluginIdHint ?? normalizeOptionalString(packageManifest?.plugin?.id) ?? normalizeOptionalString(packageManifest?.channel?.id)
});
}
/** Keeps declarations paired with their runtime sources when earlier entries cannot resolve. */
function resolvePackageRuntimeExtensions(params) {
const runtimeResolution = resolvePackageRuntimeExtensionEntries(params);
if (!runtimeResolution.ok) {
reportPackageEntryDiagnostic(params, "error", runtimeResolution.error);
return [];
}
return params.extensions.flatMap((entryPath, index) => {
const source = resolvePackageRuntimeEntrySource({
...params,
entryPath,
sourceEntryLabel: "extension entry",
runtimeEntryPath: runtimeResolution.runtimeExtensions[index],
runtimeEntryLabel: "runtime extension entry"
});
return source ? [{
entryPath,
source
}] : [];
});
}
//#endregion
//#region src/plugins/plugin-lifecycle-trace.ts
/** Checks the opt-in plugin lifecycle tracing environment flag. */
function isPluginLifecycleTraceEnabled() {
const raw = process.env.OPENCLAW_PLUGIN_LIFECYCLE_TRACE?.trim().toLowerCase();
return raw === "1" || raw === "true" || raw === "yes";
}
function emitPluginLifecycleTrace(params) {
const elapsedMs = Number(process.hrtime.bigint() - params.start) / 1e6;
const detailText = Object.entries(params.details ?? {}).filter((entry) => entry[1] !== void 0).map(([key, value]) => `${key}=${typeof value === "string" ? JSON.stringify(value) : value}`).join(" ");
const suffix = detailText ? ` ${detailText}` : "";
console.error(`[plugins:lifecycle] phase=${JSON.stringify(params.phase)} ms=${elapsedMs.toFixed(2)} status=${params.status}${suffix}`);
}
/** Traces a synchronous plugin lifecycle phase when tracing is enabled. */
function tracePluginLifecyclePhase(phase, fn, details) {
if (!isPluginLifecycleTraceEnabled()) return fn();
const start = process.hrtime.bigint();
let status = "error";
try {
const result = fn();
status = "ok";
return result;
} finally {
emitPluginLifecycleTrace({
phase,
start,
status,
details
});
}
}
//#endregion
//#region src/plugins/record-groups.ts
function groupPluginRecords(records, keyOf) {
const groups = /* @__PURE__ */ new Map();
for (const record of records) {
const key = keyOf(record);
const group = groups.get(key);
if (group) group.push(record);
else groups.set(key, [record]);
}
return groups;
}
//#endregion
//#region src/plugins/dev-source-root.ts
/** Env var that points bundled-plugin lookup at an OpenClaw source checkout. */
const OPENCLAW_DEV_SOURCE_ROOT_ENV = "OPENCLAW_DEV_SOURCE_ROOT";
function readPackageName(packageJsonPath) {
try {
const parsed = JSON.parse(fsSync.readFileSync(packageJsonPath, "utf-8"));
return typeof parsed.name === "string" ? parsed.name : null;
} catch {
return null;
}
}
/** Resolves and validates the configured OpenClaw development source root. */
function resolveOpenClawDevSourceRoot(env = process.env) {
const rawRoot = env[OPENCLAW_DEV_SOURCE_ROOT_ENV]?.trim();
if (!rawRoot) return null;
const resolvedRoot = resolveUserPath(rawRoot, env);
const roots = getPluginCache().sdk.devSourceRoots;
if (roots.has(resolvedRoot)) return roots.get(resolvedRoot) ?? null;
const realRoot = pluginCacheRealpathSync(resolvedRoot);
const result = realRoot && readPackageName(path.join(realRoot, "package.json")) === "openclaw" && pluginCacheExistsSync(path.join(realRoot, "src")) && pluginCacheExistsSync(path.join(realRoot, "extensions")) ? realRoot : null;
roots.set(resolvedRoot, result);
return result;
}
/** Source builds own their bundled SDK consumers even without an explicit development selector. */
function resolveBundledPluginSourceRoot(env = process.env) {
const selected = resolveOpenClawDevSourceRoot(env);
if (selected) return selected;
const hostRoot = resolveOpenClawPackageRootSync({ moduleUrl: import.meta.url });
return hostRoot && isSourceCheckoutRoot(hostRoot) ? hostRoot : null;
}
/** Prioritizes already-bundled candidates; the selector itself never grants provenance. */
function isBundledPluginInsideDevSourceRoot(params) {
const devSourceRoot = resolveBundledPluginSourceRoot(params.env);
if (!devSourceRoot) return false;
if (!isPluginInPackageBundledRoots({
packageRoot: devSourceRoot,
rootDir: resolveUserPath(params.rootDir, params.env)
})) return false;
if (resolveOpenClawDevSourceRoot(params.env)) return true;
const hostRoot = pluginCacheRealpathSync(devSourceRoot);
const pluginRoot = pluginCacheRealpathSync(resolveUserPath(params.rootDir, params.env));
return Boolean(hostRoot && pluginRoot && ["dist", "dist-runtime"].some((tree) => isPathInside$2(path.join(hostRoot, tree, "extensions"), pluginRoot)));
}
//#endregion
//#region src/plugins/install-paths.ts
/** Validates a plugin id for install path safety. */
function validatePluginId(pluginId) {
const trimmed = pluginId.trim();
if (!trimmed) return "invalid plugin name: missing";
if (trimmed.includes("\\")) return "invalid plugin name: path separators not allowed";
const segments = trimmed.split("/");
if (segments.some((segment) => !segment)) return "invalid plugin name: malformed scope";
if (segments.some((segment) => segment === "." || segment === "..")) return "invalid plugin name: reserved path segment";
if (segments.length === 1) {
if (trimmed.startsWith("@")) return "invalid plugin name: scoped ids must use @scope/name format";
return null;
}
if (segments.length !== 2) return "invalid plugin name: path separators not allowed";
if (!segments[0]?.startsWith("@") || segments[0].length < 2) return "invalid plugin name: scoped ids must use @scope/name format";
return null;
}
/** Resolves the default directory for path-installed plugin extensions. */
function resolveDefaultPluginExtensionsDir(env = process.env, homedir) {
return resolveActivePluginInstallDir("extensions", env, homedir);
}
/** Resolves the default directory for managed npm plugin installs. */
function resolveDefaultPluginNpmDir(env = process.env, homedir) {
return resolveActivePluginInstallDir("npm", env, homedir);
}
/** Encodes an npm package name into a managed npm project directory name. */
function encodePluginNpmProjectDirName(packageName) {
const trimmed = packageName.trim();
if (!trimmed) throw new Error("invalid npm package name: missing");
return safePathSegmentHashed(trimmed);
}
/** Resolves the directory containing managed npm plugin projects. */
function resolvePluginNpmProjectsDir(npmDir) {
const npmBase = npmDir ? resolveUserPath(npmDir) : resolveDefaultPluginNpmDir();
return path.join(npmBase, "projects");
}
/** Resolves the managed npm project directory for a package name. */
function resolvePluginNpmProjectDir(params) {
return path.join(resolvePluginNpmProjectsDir(params.npmDir), encodePluginNpmProjectDirName(params.packageName));
}
const PLUGIN_NPM_GENERATION_PROJECT_SEPARATOR = "__openclaw-generation__";
const PLUGIN_NPM_GENERATION_KEY_DIR_NAME_PATTERN = new RegExp(`^g-[a-f0-9]{16}$`, "u");
/** Resolves the managed npm artifact-generation project directory prefix for a package. */
function resolvePluginNpmGenerationProjectDirPrefix(packageName) {
return `${encodePluginNpmProjectDirName(packageName)}${PLUGIN_NPM_GENERATION_PROJECT_SEPARATOR}`;
}
/** Checks that a managed path preserves its npm-root-relative real path. */
function isPluginNpmManagedPath(params) {
const npmDir = path.resolve(params.npmDir ?? resolveDefaultPluginNpmDir());
const managedPath = path.resolve(params.managedPath);
try {
return lstatSync(npmDir).isDirectory() && path.relative(npmDir, managedPath) === path.relative(realpathSync(npmDir), params.allowMissing ? resolvePathViaExistingAncestorSync$1(managedPath) : realpathSync(managedPath));
} catch {
return false;
}
}
/** Checks whether a project directory has the exact managed shape owned by a package. */
function isPluginNpmProjectDir(params) {
const npmDir = path.resolve(params.npmDir ?? resolveDefaultPluginNpmDir());
const projectDir = path.resolve(params.projectDir);
if (path.dirname(projectDir) !== path.resolve(resolvePluginNpmProjectsDir(npmDir))) return false;
if (!isPluginNpmManagedPath({
managedPath: projectDir,
npmDir
})) return false;
if (!isPluginNpmManagedPath({
managedPath: path.join(projectDir, "node_modules", ...params.packageName.split("/")),
npmDir,
allowMissing: true
})) return false;
if (projectDir === path.resolve(resolvePluginNpmProjectDir(params))) return true;
const projectName = path.basename(projectDir);
const generationPrefix = resolvePluginNpmGenerationProjectDirPrefix(params.packageName);
return projectName.startsWith(generationPrefix) && PLUGIN_NPM_GENERATION_KEY_DIR_NAME_PATTERN.test(projectName.slice(generationPrefix.length));
}
//#endregion
//#region src/plugins/roots.ts
function resolvePluginSourceRoots(params) {
const env = params.env ?? process.env;
const workspaceRoot = params.workspaceDir ? resolveUserPath(params.workspaceDir, env) : void 0;
return {
stock: resolveBundledPluginsDir(env),
global: resolveDefaultPluginExtensionsDir(env),
workspace: workspaceRoot ? path.join(workspaceRoot, ".openclaw", "extensions") : void 0
};
}
function resolvePluginCacheInputs(params) {
const env = params.env ?? process.env;
return {
roots: resolvePluginSourceRoots({
workspaceDir: params.workspaceDir,
env
}),
loadPaths: normalizeStringEntries((params.loadPaths ?? []).filter((entry) => typeof entry === "string")).map((entry) => resolveUserPath(entry, env)),
devSourceRoot: resolveOpenClawDevSourceRoot(env)
};
}
//#endregion
//#region src/plugins/status-dependencies-core.ts
function normalizeDependencyMap(raw) {
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return {};
const normalized = {};
for (const [name, spec] of Object.entries(raw)) {
const normalizedName = name.trim();
if (!normalizedName || typeof spec !== "string" || !spec.trim()) continue;
normalized[normalizedName] = spec.trim();
}
return normalized;
}
/** Normalizes raw package dependency maps into sorted plugin dependency specs. */
function normalizePluginDependencySpecs(params) {
const dependencies = normalizeDependencyMap(params.dependencies);
const optionalDependencies = normalizeDependencyMap(params.optionalDependencies);
for (const name of Object.keys(optionalDependencies)) delete dependencies[name];
return {
dependencies,
optionalDependencies
};
}
//#endregion
//#region src/plugins/discovery.ts
/** Discovers plugin candidates from bundled, workspace, global, package, and bundle roots. */
const SCANNED_DIRECTORY_IGNORE_NAMES = /* @__PURE__ */ new Set([
".git",
".hg",
".svn",
".turbo",
".yarn",
".yarn-cache",
"build",
"coverage",
"dist",
"node_modules"
]);
function currentUid(overrideUid) {
if (overrideUid !== void 0) return overrideUid;
if (process.platform === "win32") return null;
if (typeof process.getuid !== "function") return null;
return process.getuid();
}
function checkSourceEscapesRoot(params) {
const sourceRealPath = pluginCacheRealpathSync(params.source);
const rootRealPath = pluginCacheRealpathSync(params.rootDir);
if (!sourceRealPath || !rootRealPath) return null;
if (isPathInside$1(rootRealPath, sourceRealPath)) return null;
return {
source: params.source,
message: `blocked plugin candidate: source escapes plugin root (${params.source} -> ${sourceRealPath}; root=${rootRealPath})`
};
}
function checkPathStatAndPermissions(params) {
if (process.platform === "win32") return null;
const pathsToCheck = [params.rootDir, params.source];
const seen = /* @__PURE__ */ new Set();
for (const targetPath of pathsToCheck) {
const normalized = path.resolve(targetPath);
if (seen.has(normalized)) continue;
seen.add(normalized);
let stat = pluginCacheStatSync(targetPath);
if (!stat) return {
source: targetPath,
message: `blocked plugin candidate: cannot stat path (${targetPath})`
};
let modeBits = stat.mode & 511;
if ((modeBits & 2) !== 0 && params.origin === "bundled") try {
fsSync.chmodSync(targetPath, modeBits & -19);
const repairedStat = refreshPluginCacheStat(targetPath);
if (!repairedStat) return {
source: targetPath,
message: `blocked plugin candidate: cannot stat path (${targetPath})`
};
stat = repairedStat;
modeBits = repairedStat.mode & 511;
} catch {}
if ((modeBits & 2) !== 0) return {
source: targetPath,
message: `blocked plugin candidate: world-writable path (${targetPath}, mode=${formatPosixMode(modeBits)})`
};
if (params.origin !== "bundled" && params.uid !== null && typeof stat.uid === "number" && stat.uid !== params.uid && stat.uid !== 0) return {
source: targetPath,
message: `blocked plugin candidate: suspicious ownership (${targetPath}, uid=${stat.uid}, expected uid=${params.uid} or root)`
};
}
return null;
}
function isExtensionFile(filePath) {
const ext = path.extname(filePath);
if (!PUBLIC_SURFACE_SOURCE_EXTENSIONS.some((extension) => extension === ext)) return false;
if (/\.d\.[cm]?ts$/.test(filePath)) return false;
return !normalizeLowercaseStringOrEmpty(path.basename(filePath)).includes(".test.");
}
function shouldIgnoreScannedDirectory(dirName) {
const normalized = normalizeLowercaseStringOrEmpty(dirName);
return !normalized || SCANNED_DIRECTORY_IGNORE_NAMES.has(normalized) || normalized.endsWith(".bak") || normalized.includes(".backup-") || normalized.includes(".disabled");
}
function resolveScannedEntryType(entry, fullPath) {
if (entry.isFile()) return "file";
if (entry.isDirectory()) return "directory";
if (!entry.isSymbolicLink()) return null;
const stat = pluginCacheStatSync(fullPath);
if (!stat) return null;
if (stat.isFile()) return "file";
if (stat.isDirectory()) return "directory";
return null;
}
function resolvesToSameDirectory(left, right) {
if (!left || !right) return false;
const leftRealPath = pluginCacheRealpathSync(left);
const rightRealPath = pluginCacheRealpathSync(right);
if (leftRealPath && rightRealPath) return leftRealPath === rightRealPath;
return path.resolve(left) === path.resolve(right);
}
function mergeCandidateInstallOwner(existing, candidateOwner, candidateOwnerAmbiguous) {
const existingOwner = resolvePluginCandidateInstallOwner(existing);
const ownerConflict = existingOwner && candidateOwner && existingOwner !== candidateOwner;
if (isPluginCandidateInstallOwnerAmbiguous(existing) || candidateOwnerAmbiguous || ownerConflict) recordPluginCandidateInstallOwner(existing, void 0, true);
else if (candidateOwner) recordPluginCandidateInstallOwner(existing, candidateOwner);
}
function prepareInstalledPluginPaths(installRecords, env, diagnostics) {
const byPath = /* @__PURE__ */ new Map();
const installedPluginDirKeys = /* @__PURE__ */ new Set();
const managedPluginDirs = /* @__PURE__ */ new Set();
const resolveRecordPath = (rawPath) => typeof rawPath === "string" && rawPath.trim() ? resolveUserPath(rawPath, env) : void 0;
for (const [installOwner, record] of Object.entries(installRecords ?? {})) {
const installPath = resolveRecordPath(record.installPath);
const sourcePath = resolveRecordPath(record.sourcePath);
for (const recordedPath of [installPath, sourcePath]) if (recordedPath && pluginCacheExistsSync(recordedPath)) {
const key = resolveManagedPluginDirKey(recordedPath);
if (key) managedPluginDirs.add(key);
}
const resolved = installPath ?? sourcePath;
if (!resolved || !pluginCacheExistsSync(resolved)) continue;
const pathKey = pluginCacheRealpathSync(resolved) ?? path.resolve(resolved);
const requireBuiltRuntimeEntry = !(record.source === "path" && installPath && sourcePath && resolvesToSameDirectory(installPath, sourcePath));
const existing = byPath.get(pathKey);
if (existing) {
existing.requireBuiltRuntimeEntry ||= requireBuiltRuntimeEntry;
if (existing.installOwner !== installOwner) {
delete existing.installOwner;
existing.installOwnerAmbiguous = true;
diagnostics.push({
level: "error",
source: resolved,
message: "multiple plugin install records claim the same package path; refresh or reinstall the package before using managed lifecycle actions"
});
}
} else {
byPath.set(pathKey, {
path: resolved,
requireBuiltRuntimeEntry,
installOwner
});
const dirKey = resolveManagedPluginDirKey(resolved);
if (dirKey) installedPluginDirKeys.add(dirKey);
}
}
return {
installedPaths: [...byPath.values()],
installedPluginDirKeys,
managedPluginDirs
};
}
function resolveManagedPluginDirKey(installedPath) {
const stat = pluginCacheStatSync(installedPath);
if (!stat) return null;
const pluginDir = stat.isFile() ? path.dirname(installedPath) : installedPath;
return pluginCacheRealpathSync(pluginDir) ?? path.resolve(pluginDir);
}
function isManagedPluginDir(params) {
if (!params.managedPluginDirs || params.managedPluginDirs.size === 0) return false;
const key = params.realpath ?? pluginCacheRealpathSync(params.dir) ?? path.resolve(params.dir);
return params.managedPluginDirs.has(key);
}
function readPackageManifest(dir, rejectHardlinks = true, rootRealPath) {
const file = readPluginCacheFile({
rootDir: dir,
...rootRealPath !== void 0 ? { rootRealPath } : {},
relativePath: "package.json",
rejectHardlinks
});
if (!file.ok) return null;
const parsed = parsePluginCacheJson(file);
return parsed.ok && isRecord(parsed.value) ? parsed.value : null;
}
function deriveIdHint(params) {
const base = path.basename(params.filePath, path.extname(params.filePath));
const pluginId = normalizeOptionalString(params.manifestId) ?? derivePackagePluginIdHint(params.packageName) ?? params.fallbackId;
return params.hasMultipleExtensions ? `${pluginId}/${base}` : pluginId;
}
function derivePackagePluginIdHint(packageName) {
const rawPackageName = normalizeOptionalString(packageName);
if (!rawPackageName) return;
const unscoped = rawPackageName.slice(rawPackageName.lastIndexOf("/") + 1);
for (const suffix of ["-provider", "-plugin"]) if (unscoped.endsWith(suffix) && unscoped.length > suffix.length) return unscoped.slice(0, -suffix.length);
return normalizeOptionalString(unscoped);
}
function resolvePluginPackageEntries(params) {
const entryIdSources = groupPluginRecords(resolvePackageRuntimeExtensions(params), (entry) => deriveIdHint({
filePath: entry.source,
manifestId: params.manifestId,
packageName: params.manifest?.name,
fallbackId: path.basename(params.packageDir),
hasMultipleExtensions: params.extensions.length > 1
}));
const entries = [];
for (const [idHint, sources] of entryIdSources) {
if (params.extensions.length > 1 && sources.length > 1) {
params.diagnostics.push({
level: "error",
pluginId: idHint,
source: params.sourceLabel,
message: `plugin package entries collide on derived id "${idHint}" (${sources.map((entry) => path.relative(params.packageDir, entry.source)).join(", ")}); rename the entry files to unique basenames`
});
continue;
}
for (const entry of sources) entries.push({
...entry,
idHint
});
}
return entries;
}
function pushInvalidPackageExtensionDiagnostic(params) {
if (params.resolution.status !== "invalid" && params.resolution.status !== "empty") return false;
params.diagnostics.push({
level: "error",
source: params.source,
message: params.resolution.status === "invalid" ? params.resolution.error : "package.json openclaw.extensions is empty",
...params.pluginId ? { pluginId: params.pluginId } : {}
});
return true;
}
function isSourceCheckoutExtensionsDir(extensionsDir) {
const packageRoot = path.dirname(extensionsDir);
return pluginCacheExistsSync(path.join(packageRoot, ".git")) && pluginCacheExistsSync(path.join(packageRoot, "pnpm-workspace.yaml")) && pluginCacheExistsSync(path.join(packageRoot, "src")) && hasUsableBundledPluginTree(extensionsDir);
}
function resolveBundledSourceCheckoutExtensionsDir(bundledRoot) {
if (!bundledRoot) return;
const legacyRoot = buildLegacyBundledRootPath(bundledRoot);
if (!legacyRoot || !isPluginInPackageBundledRoots({
rootDir: legacyRoot,
packageRoot: path.dirname(legacyRoot)
}) || !isSourceCheckoutExtensionsDir(legacyRoot)) return;
return legacyRoot;
}
function isHostBundledPluginRoot(dir, env) {
const bundledRoot = resolveBundledPluginsDir(env);
const realDir = pluginCacheRealpathSync(dir) ?? path.resolve(dir);
return [bundledRoot, resolveBundledSourceCheckoutExtensionsDir(bundledRoot)].some((root) => root && isPathInside$1(pluginCacheRealpathSync(root) ?? path.resolve(root), realDir));
}
function readChildDirectoryNames(dir) {
if (!dir || !pluginCacheExistsSync(dir)) return /* @__PURE__ */ new Set();
try {
return new Set(sortUniqueStrings(readPluginCacheDirectory(dir).filter((entry) => entry.isDirectory()).map((entry) => entry.name)));
} catch {
return /* @__PURE__ */ new Set();
}
}
function readBundledDistOptOutDirectoryNames(sourceExtensionsDir) {
const names = /* @__PURE__ */ new Set();
if (!sourceExtensionsDir) return names;
for (const name of readChildDirectoryNames(sourceExtensionsDir)) if (getPackageManifestMetadata(readPackageManifest(path.join(sourceExtensionsDir, name), false) ?? void 0)?.build?.bundledDist === false) names.add(name);
return names;
}
function createPluginScanner(env, ownershipUid) {
const result = {
candidates: [],
diagnostics: []
};
const { candidates, diagnostics } = result;
const attemptedSources = /* @__PURE__ */ new Map();
function addCandidate(params) {
const resolved = path.resolve(params.source);
if (attemptedSources.has(resolved)) {
const existing = attemptedSources.get(resolved);
if (existing) mergeCandidateInstallOwner(existing, params.installOwner, params.installOwnerAmbiguous === true);
return;
}
const resolvedRoot = pluginCacheRealpathSync(params.rootDir) ?? path.resolve(params.rootDir);
const issue = checkSourceEscapesRoot({
source: resolved,
rootDir: resolvedRoot
}) ?? checkPathStatAndPermissions({
source: resolved,
rootDir: resolvedRoot,
origin: params.origin,
uid: currentUid(ownershipUid)
});
if (issue) {
diagnostics.push({
level: "warn",
...params.idHint ? { pluginId: params.idHint } : {},
...issue
});
attemptedSources.set(resolved, void 0);
return;
}
const manifest = params.manifest ?? null;
const packageManifest = getPackageManifestMetadata(manifest ?? void 0);
const packageDependencies = normalizePluginDependencySpecs({
dependencies: manifest?.dependencies,
optionalDependencies: manifest?.optionalDependencies
});
const candidate = {
idHint: params.idHint,
...params.effectivePluginId ? { effectivePluginId: params.effectivePluginId } : {},
...params.diagnosticIdHint && params.diagnosticIdHint !== params.idHint ? { diagnosticIdHint: params.diagnosticIdHint } : {},
source: resolved,
setupSource: params.setupSource,
rootDir: resolvedRoot,
origin: params.origin,
format: params.format ?? "openclaw",
bundleFormat: params.bundleFormat,
workspaceDir: params.workspaceDir,
packageName: normalizeOptionalString(manifest?.name),
packageVersion: normalizeOptionalString(manifest?.version),
packageDescription: normalizeOptionalString(manifest?.description),
packageDir: params.packageDir,
packageManifest,
packageDependencies: packageDependencies.dependencies,
packageOptionalDependencies: packageDependencies.optionalDependencies,
rawPackageManifest: manifest ?? void 0,
bundledManifestId: params.bundledManifestId,
bundledManifest: params.bundledManifest,
bundledManifestPath: params.bundledManifestPath,
...params.requiredPluginIds && params.requiredPluginIds.length > 0 ? { requiredPluginIds: params.requiredPluginIds } : {},
...params.requiredPluginSource ? { requiredPluginSource: params.requiredPluginSource } : {}
};
recordPluginCandidateInstallOwner(candidate, params.installOwner, params.installOwnerAmbiguous === true);
candidates.push(candidate);
attemptedSources.set(resolved, candidate);
}
function discoverBundleInRoot(params) {
const bundleFormat = detectBundleManifestFormat(params.rootDir);
if (!bundleFormat) return false;
const rootRealPath = pluginCacheRealpathSync(params.rootDir) ?? void 0;
const rejectHardlinks = shouldRejectHardlinkedPluginFiles({
origin: params.origin,
rootDir: params.rootDir,
env
});
const bundleManifest = loadBundleManifest({
rootDir: params.rootDir,
...rootRealPath !== void 0 ? { rootRealPath } : {},
bundleFormat,
rejectHardlinks
});
if (!bundleManifest.ok) {
diagnostics.push({
level: "error",
message: bundleManifest.error,
source: bundleManifest.manifestPath
});
return false;
}
addCandidate({
idHint: bundleManifest.manifest.id,
source: params.rootDir,
rootDir: params.rootDir,
origin: params.origin,
format: "bundle",
bundleFormat,
workspaceDir: params.workspaceDir,
...params.installOwner ? { installOwner: params.installOwner } : {},
...params.installOwnerAmbiguous ? { installOwnerAmbiguous: true } : {},
manifest: params.manifest,
packageDir: params.rootDir,
bundledManifestId: bundleManifest.manifest.id,
bundledManifestPath: bundleManifest.manifestPath
});
return true;
}
function discoverPluginDirectory(params) {
const { dir, rootRealPath } = params;
const requireBuiltRuntimeEntry = params.requireBuiltRuntimeEntry ?? isManagedPluginDir({
dir,
realpath: rootRealPath,
managedPluginDirs: params.managedPluginDirs
});
const rejectHardlinks = shouldRejectHardlinkedPluginFiles({
origin: params.origin,
rootDir: dir,
env
});
const manifest = readPackageManifest(dir, rejectHardlinks, rootRealPath);
const packageMetadata = getPackageManifestMetadata(manifest ?? void 0);
const manifestResult = loadPluginManifest(dir, rejectHardlinks, rootRealPath);
const candidateManifest = manifestResult.ok ? manifestResult : void 0;
const manifestId = candidateManifest?.manifest.id;
const pluginIdHint = normalizeOptionalString(manifestId) ?? normalizeOptionalString(packageMetadata?.plugin?.id) ?? normalizeOptionalString(packageMetadata?.channel?.id) ?? derivePackagePluginIdHint(manifest?.name) ?? path.basename(dir);
if (shouldSkipIncompatiblePackagePluginApi({
origin: params.origin,
packageManifest: packageMetadata,
pluginId: pluginIdHint,
packageDir: dir,
env,
diagnostics
})) return true;
const extensionResolution = resolvePackageExtensionEntries(manifest ?? void 0);
if (pushInvalidPackageExtensionDiagnostic({
resolution: extensionResolution,
source: dir,
pluginId: pluginIdHint,
diagnostics
})) return true;
const extensions = extensionResolution.status === "ok" ? extensionResolution.entries : [];
const setupSource = resolvePackageSetupSource({
packageDir: dir,
...rootRealPath !== void 0 ? { packageRootRealPath: rootRealPath } : {},
manifest,
pluginIdHint,
origin: params.origin,
requireBuiltRuntimeEntry,
sourceLabel: dir,
diagnostics,
rejectHardlinks
});
const addPackageCandidate = (source, idHint, effectivePluginId) => {
addCandidate({
idHint,
...effectivePluginId ? { effectivePluginId } : {},
diagnosticIdHint: pluginIdHint,
source,
...setupSource ? { setupSource } : {},
rootDir: dir,
origin: params.origin,
workspaceDir: params.workspaceDir,
...params.installOwner ? { installOwner: params.installOwner } : {},
...params.installOwnerAmbiguous ? { installOwnerAmbiguous: true } : {},
manifest,
packageDir: dir,
requiredPluginIds: candidateManifest?.manifest.requiresPlugins,
requiredPluginSource: candidateManifest?.manifestPath
});
};
if (extensions.length > 0) {
const entries = resolvePluginPackageEntries({
packageDir: dir,
...rootRealPath !== void 0 ? { packageRootRealPath: rootRealPath } : {},
manifest,
extensions,
manifestId: manifestId ?? normalizeOptionalString(packageMetadata?.plugin?.id),
origin: params.origin,
pluginIdHint,
requireBuiltRuntimeEntry,
sourceLabel: dir,
diagnostics,
rejectHardlinks
});
for (const entry of entries) addPackageCandidate(entry.source, entry.idHint, extensions.length > 1 ? entry.idHint : void 0);
return true;
}
if (discoverBundleInRoot({
rootDir: dir,
origin: params.origin,
workspaceDir: params.workspaceDir,
...params.installOwner ? { installOwner: params.installOwner } : {},
...params.installOwnerAmbiguous ? { installOwnerAmbiguous: true } : {},
manifest
})) return true;
const indexFile = [...DEFAULT_PLUGIN_ENTRY_CANDIDATES].map((candidate) => path.join(dir, candidate)).find((candidate) => pluginCacheExistsSync(candidate));
if (indexFile && isExtensionFile(indexFile)) {
addPackageCandidate(indexFile, manifestId ?? path.basename(dir));
return true;
}
return false;
}
function discoverInDirectory(params) {
if (!pluginCacheExistsSync(params.dir)) return;
let entries;
try {
entries = readPluginCacheDirectory(params.dir).toSorted((left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0);
} catch (err) {
diagnostics.push(params.origin === "config" ? pluginPathFailureDiagnostic(params.dir, params.origin, err) : {
level: "warn",
message: `failed to read extensions dir: ${params.dir} (${String(err)})`,
source: params.dir
});
return;
}
for (const entry of entries) {
const fullPath = path.join(params.dir, entry.name);
const entryType = resolveScannedEntryType(entry, fullPath);
if (entryType === "file") {
if (!(params.scanFiles ?? params.origin === "bundled") || !isExtensionFile(fullPath)) continue;
addCandidate({
idHint: path.basename(entry.name, path.extname(entry.name)),
source: fullPath,
rootDir: path.dirname(fullPath),
origin: params.origin,
workspaceDir: params.workspaceDir,
...params.installOwner ? { installOwner: params.installOwner } : {},
...params.installOwnerAmbiguous ? { installOwnerAmbiguous: true } : {}
});
continue;
}
if (entryType !== "directory") continue;
if (params.skipDirectories?.has(entry.name)) continue;
if (shouldIgnoreScannedDirectory(entry.name)) continue;
const fullPathRealPath = pluginCacheRealpathSync(fullPath) ?? void 0;
const fullPathDirKey = fullPathRealPath ?? path.resolve(fullPath);
if (params.origin === "bundled" && !isPathInside$1(pluginCacheRealpathSync(params.dir) ?? path.resolve(params.dir), fullPathDirKey)) {
diagnostics.push({
level: "error",
source: fullPath,
message: `blocked plugin candidate: package directory escapes bundled root (${fullPath})`
});
continue;
}
if (params.skipRootDirKeys?.has(fullPathDirKey)) continue;
discoverPluginDirectory({
...params,
dir: fullPath,
rootRealPath: fullPathRealPath
});
}
}
function discoverFromPath(params) {
const resolved = resolveUserPath(params.rawPath, env);
const stat = inspectPluginLoadPath(resolved, params.origin, diagnostics);
if (!stat) return;
const origin = (params.origin === "config" || params.origin === "global") && isHostBundledPluginRoot(stat.isFile() ? path.dirname(resolved) : resolved, env) ? "bundled" : params.origin;
if (stat.isFile()) {
if (!isExtensionFile(resolved)) {
diagnostics.push({
level: "error",
message: `plugin path is not a supported file: ${resolved}`,
source: resolved
});
return;
}
addCandidate({
idHint: path.basename(resolved, path.extname(resolved)),
source: resolved,
rootDir: path.dirname(resolved),
origin,
workspaceDir: params.workspaceDir,
...params.installOwner ? { installOwner: params.installOwner } : {},
...params.installOwnerAmbiguous ? { installOwnerAmbiguous: true } : {}
});
return;
}
if (stat.isDirectory()) {
if (discoverPluginDirectory({
...params,
origin,
dir: resolved,
rootRealPath: pluginCacheRealpathSync(resolved) ?? void 0
})) return;
discoverInDirectory({
dir: resolved,
origin,
workspaceDir: params.workspaceDir,
...params.scanFiles !== void 0 || params.origin === "config" ? { scanFiles: params.scanFiles ?? true } : {},
...params.requireBuiltRuntimeEntry !== void 0 ? { requireBuiltRuntimeEntry: params.requireBuiltRuntimeEntry } : {},
...params.managedPluginDirs ? { managedPluginDirs: params.managedPluginDirs } : {},
...params.skipRootDirKeys ? { skipRootDirKeys: params.skipRootDirKeys } : {}
});
}
}
function discoverConfiguredPaths(loadPaths, workspaceDir) {
const firstConfigured = candidates.length;
for (const loadPath of loadPaths) if (typeof loadPath === "string" && loadPath.trim()) discoverFromPath({
rawPath: loadPath.trim(),
origin: "config",
workspaceDir
});
for (const candidate of candidates.slice(firstConfigured)) candidate.configSelected = true;
}
function finish() {
const candidatesBySource = /* @__PURE__ */ new Map();
const seenDiagnostics = /* @__PURE__ */ new Set();
const uniqueDiagnostics = [];
for (const candidate of candidates) {
const key = pluginCacheRealpathSync(candidate.source) ?? path.resolve(candidate.source);
const existing = candidatesBySource.get(key);
if (existing) {
const retained = candidate.origin === "bundled" ? candidate : existing;
const duplicate = retained === candidate ? existing : candidate;
if (duplicate.origin === "config" || duplicate.configSelected) retained.configSelected = true;
if (duplicate.sourcePreferred) retained.sourcePreferred = true;
mergeCandidateInstallOwner(retained, resolvePluginCandidateInstallOwner(duplicate), isPluginCandidateInstallOwnerAmbiguous(duplicate));
candidatesBySource.set(key, retained);
continue;
}
candidatesBySource.set(key, candidate);
}
for (const diagnostic of diagnostics) {
const key = [
diagnostic.level,
diagnostic.pluginId ?? "",
diagnostic.source ?? "",
diagnostic.message
].join("\0");
if (seenDiagnostics.has(key)) continue;
seenDiagnostics.add(key);
uniqueDiagnostics.push(diagnostic);
}
result.candidates = [...candidatesBySource.values()];
result.diagnostics = uniqueDiagnostics;
return result;
}
return {
result,
discoverConfiguredPaths,
discoverFromPath,
discoverInDirectory,
finish
};
}
function discoveryPolicy(env, ownershipUid, bundledRoot) {
return {
ownershipUid: currentUid(ownershipUid),
compatibilityHostVersion: resolveCompatibilityHostVersion(env),
bundledRoot: bundledRoot ?? resolveBundledPluginsDir(env) ?? "",
nix: resolveIsNixMode(env),
sourceOverlaysDisabled: env.OPENCLAW_DISABLE_BUNDLED_SOURCE_OVERLAYS ?? "",
home: env.OPENCLAW_HOME ?? "",
userHome: env.HOME ?? "",
userProfile: env.USERPROFILE ?? "",
cwd: process.cwd()
};
}
function discoverSharedPluginRoots(params) {
const { roots, env } = params;
const cache = getPluginCache().metadata.sharedDiscovery;
const key = hashStableJson({
roots,
installRecords: Object.entries(params.installRecords ?? {}),
rootScope: params.rootScope ?? "all",
policy: discoveryPolicy(env, params.ownershipUid, roots.stock)
});
const cached = cache.get(key);
if (cached) return cached;
const scanner = createPluginScanner(env, params.ownershipUid);
const { result, discoverInDirectory } = scanner;
const workspaceCandidates = /* @__PURE__ */ new Set();
const discoverWorkspacePath = (options) => {
const first = result.candidates.length;
scanner.discoverFromPath(options);
for (const candidate of result.candidates.slice(first)) workspaceCandidates.add(candidate);
};
tracePluginLifecyclePhase("discovery scan", () => {
for (const sourceOverlayDir of listBundledSourceOverlayDirs({
bundledRoot: roots.stock,
env
})) {
const firstOverlay = result.candidates.length;
discoverWorkspacePath({
rawPath: sourceOverlayDir,
origin: "bundled"
});
for (const candidate of result.candidates.slice(firstOverlay)) candidate.sourcePreferred = true;
result.diagnostics.push({
level: "warn",
source: sourceOverlayDir,
message: "using bind-mounted bundled plugin source overlay; this source overrides the packaged dist bundle for the same plugin id"
});
}
const sourceCheckoutDependencyDiagnostic = resolveSourceCheckoutDependencyDiagnostic(env);
if (sourceCheckoutDependencyDiagnostic) result.diagnostics.push({
level: "warn",
source: sourceCheckoutDependencyDiagnostic.source,
message: sourceCheckoutDependencyDiagnostic.message
});
const sourceCheckoutExtensionsDir = resolveBundledSourceCheckoutExtensionsDir(roots.stock);
const bundledDistOptOutDirectories = readBundledDistOptOutDirectoryNames(sourceCheckoutExtensionsDir);
if (sourceCheckoutExtensionsDir) for (const dirName of bundledDistOptOutDirectories) discoverWorkspacePath({
rawPath: path.join(sourceCheckoutExtensionsDir, dirName),
origin: "bundled"
});
if (roots.stock) discoverInDirectory({
dir: roots.stock,
origin: "bundled",
skipDirectories: bundledDistOptOutDirectories
});
const sourceCheckoutMatchesBundledRoot = resolvesToSameDirectory(sourceCheckoutExtensionsDir, roots.stock);
if (sourceCheckoutExtensionsDir && !sourceCheckoutMatchesBundledRoot) discoverInDirectory({
dir: sourceCheckoutExtensionsDir,
origin: "bundled",
skipDirectories: readChildDirectoryNames(roots.stock)
});
if (params.rootScope !== "bundled") {
const { installedPaths, installedPluginDirKeys, managedPluginDirs } = prepareInstalledPluginPaths(params.installRecords, env, result.diagnostics);
for (const installedPath of installedPaths) discoverWorkspacePath({
rawPath: installedPath.path,
origin: "global",
...installedPath.installOwner ? { installOwner: installedPath.installOwner } : {},
...installedPath.installOwnerAmbiguous ? { installOwnerAmbiguous: true } : {},
requireBuiltRuntimeEntry: installedPath.requireBuiltRuntimeEntry,
managedPluginDirs,
scanFiles: true
});
discoverInDirectory({
dir: roots.global,
origin: "global",
managedPluginDirs,
skipRootDirKeys: installedPluginDirKeys
});
}
}, { scope: "shared" });
const shared = {
candidates: result.candidates.map((candidate) => ({
candidate,
usesWorkspace: workspaceCandidates.has(candidate)
})),
diagnostics: result.diagnostics
};
cache.set(key, shared);
return shared;
}
function discoverOpenClawPlugins(params) {
const env = params.env ?? process.env;
const workspaceDir = normalizeOptionalString(params.workspaceDir);
const workspaceRoot = workspaceDir ? resolveUserPath(workspaceDir, env) : void 0;
const defaultRoots = resolvePluginSourceRoots({
workspaceDir: workspaceRoot,
env
});
const roots = params.bundledRoot ? {
...defaultRoots,
stock: path.resolve(params.bundledRoot)
} : defaultRoots;
const cache = getPluginCache().metadata.discovery;
const key = hashStableJson({
phase: "all",
roots,
workspaceDir,
loadPaths: params.extraPaths ?? [],
installRecords: Object.entries(params.installRecords ?? {}),
rootScope: params.rootScope ?? "all",
policy: discoveryPolicy(env, params.ownershipUid, roots.stock)
});
const cached = cache.get(key);
if (cached) return cached;
const scanner = createPluginScanner(env, params.ownershipUid);
const { result, discoverInDirectory } = scanner;
if (params.rootScope !== "bundled") tracePluginLifecyclePhase("discovery scan", () => {
scanner.discoverConfiguredPaths(params.extraPaths ?? [], workspaceDir);
const workspaceMatchesBundledRoot = resolvesToSameDirectory(workspaceRoot, roots.stock);
if (roots.workspace && workspaceRoot && !workspaceMatchesBundledRoot) discoverInDirectory({
dir: roots.workspace,
origin: "workspace",
workspaceDir: workspaceRoot
});
}, {
scope: "scoped",
extraPathCount: params.extraPaths?.length ?? 0
});
const shared = discoverSharedPluginRoots({
roots: {
stock: roots.stock,
global: roots.global
},
installRecords: params.installRecords,
ownershipUid: params.ownershipUid,
env,
rootScope: params.rootScope
});
for (const { candidate, usesWorkspace } of shared.candidates) result.candidates.push({
...candidate,
...usesWorkspace ? { workspaceDir } : {}
});
result.diagnostics.push(...shared.diagnostics);
scanner.finish();
addMissingRequiredPluginDiagnostics(result, { env });
cache.set(key, result);
return result;
}
//#endregion
//#region src/plugins/installed-plugin-index-facts.ts
/** Package facts share the existing generation; mutable management inputs stay uncached. */
function getInstalledPluginIndexFacts(index) {
const entries = getPluginCache().metadata.indexFacts;
const existing = entries.get(index);
if (existing) return existing;
if (!isDeeplyFrozenPlainData(index)) return;
const facts = {};
entries.set(index, facts);
return facts;
}
//#endregion
//#region src/config/zod-schema.installs.ts
const InstallSourceSchema = union([
literal("npm"),
literal("archive"),
literal("path"),
literal("clawhub"),
literal("git")
]);
const PluginInstallSourceSchema = union([InstallSourceSchema, literal("marketplace")]);
/** Zod object shape for persisted generic install records. */
const InstallRecordShape = {
source: InstallSourceSchema,
spec: string().optional(),
sourcePath: string().optional(),
installPath: string().optional(),
version: string().optional(),
resolvedName: string().optional(),
resolvedVersion: string().optional(),
resolvedSpec: string().optional(),
integrity: string().optional(),
shasum: string().optional(),
resolvedAt: string().optional(),
installedAt: string().optional(),
clawhubUrl: string().optional(),
clawhubPackage: string().optional(),
clawhubFamily: union([literal("code-plugin"), literal("bundle-plugin")]).optional(),
clawhubChannel: union([
literal("official"),
literal("community"),
literal("private")
]).optional(),
clawhubTrustDisposition: union([
literal("clean"),
literal("review-recommended"),
literal("review-required"),
literal("blocked")
]).optional(),
clawhubTrustScanStatus: string().optional(),
clawhubTrustModerationState: string().optional(),
clawhubTrustReasons: array(string()).optional(),
clawhubTrustPending: boolean().optional(),
clawhubTrustStale: boolean().optional(),
clawhubTrustCheckedAt: string().optional(),
clawhubTrustAcknowledgedAt: string().optional(),
artifactKind: union([literal("legacy-zip"), literal("npm-pack")]).optional(),
artifactFormat: union([literal("zip"), literal("tgz")]).optional(),
npmIntegrity: string().optional(),
npmShasum: string().optional(),
npmTarballName: string().optional(),
clawpackSha256: string().optional(),
clawpackSpecVersion: number().int().nonnegative().optional(),
clawpackManifestSha256: string().optional(),
clawpackSize: number().int().nonnegative().optional(),
gitUrl: string().optional(),
gitRef: string().optional(),
gitCommit: string().optional()
};
object(InstallRecordShape);
const PluginInstallRecordShape = {
...InstallRecordShape,
source: PluginInstallSourceSchema,
marketplaceName: string().optional(),
marketplaceSource: string().optional(),
marketplacePlugin: string().optional(),
acceptedSurface: strictObject({
channels: array(string().min(1)),
providers: array(string().min(1)),
tools: array(string().min(1)),
contracts: array(string().min(1)),
hooks: array(string().min(1)),
mcpServers: array(string().min(1)),
cliCommands: array(string().min(1)),
cliBackends: array(string().min(1)),
skills: array(string().min(1)),
dangerousConfigFlags: array(string().min(1))
}).optional(),
acceptedSurfaceHash: string().optional(),
acceptedSurfaceAt: string().optional(),
acceptedSurfaceIntegrity: string().optional()
};
//#endregion
//#region src/config/plugin-install-record-map.ts
const PluginInstallRecordSchema = object(PluginInstallRecordShape).passthrough();
const NORMALIZED_STRING_FIELDS = [
"spec",
"sourcePath",
"installPath",
"version",
"resolvedName",
"resolvedVersion",
"resolvedSpec",
"integrity",
"shasum",
"resolvedAt",
"installedAt",
"clawhubUrl",
"clawhubPackage",
"clawhubTrustScanStatus",
"clawhubTrustModerationState",
"clawhubTrustCheckedAt",
"clawhubTrustAcknowledgedAt",
"npmIntegrity",
"npmShasum",
"npmTarballName",
"clawpackSha256",
"clawpackManifestSha256",
"gitUrl",
"gitRef",
"gitCommit",
"marketplaceName",
"marketplaceSource",
"marketplacePlugin",
"acceptedSurfaceHash",
"acceptedSurfaceAt",
"acceptedSurfaceIntegrity"
];
function createPluginInstallRecordMap() {
return Object.create(null);
}
function setPluginInstallRecordMapEntry(records, pluginId, record) {
Object.defineProperty(records, pluginId, {
configurable: true,
enumerable: true,
value: record,
writable: true
});
}
function getPluginInstallRecordMapEntry(records, pluginId) {
return records && Object.hasOwn(records, pluginId) ? records[pluginId] : void 0;
}
function copyPluginInstallRecordMap(records) {
const copied = createPluginInstallRecordMap();
for (const [pluginId, record] of Object.entries(records ?? {})) setPluginInstallRecordMapEntry(copied, pluginId, record);
return copied;
}
function parsePluginInstallRecord(value) {
const parsed = PluginInstallRecordSchema.safeParse(value);
if (!parsed.success) return null;
const record = parsed.data;
for (const field of NORMALIZED_STRING_FIELDS) {
const fieldValue = record[field];
if (fieldValue === void 0) continue;
const normalized = fieldValue.trim();
if (normalized) record[field] = normalized;
else delete record[field];
}
if (record.clawhubTrustReasons) {
const reasons = record.clawhubTrustReasons.map((entry) => entry.trim()).filter(Boolean);
if (reasons.length > 0) record.clawhubTrustReasons = reasons;
else delete record.clawhubTrustReasons;
}
return record;
}
function parsePluginInstallRecordMap(value) {
if (!isRecord(value)) return null;
const records = createPluginInstallRecordMap();
for (const [pluginId, rawRecord] of Object.entries(value)) {
const record = parsePluginInstallRecord(rawRecord);
if (!record) return null;
setPluginInstallRecordMapEntry(records, pluginId, record);
}
return records;
}
function inspectPluginInstallRecordMap(value) {
if (value === void 0) return { status: "missing" };
const records = parsePluginInstallRecordMap(value);
return records ? {
status: "valid",
records
} : { status: "invalid" };
}
//#endregion
//#region src/plugins/doctor-contract-artifact.ts
/** Resolves the Doctor artifact shared by loading, index hashing, and freshness. */
const CONTRACT_API_EXTENSIONS = [
".js",
".mjs",
".cjs",
".ts",
".mts",
".cts"
];
const CURRENT_MODULE_PATH = fileURLToPath(import.meta.url);
const ORDERED_EXTENSIONS = CURRENT_MODULE_PATH.includes(`${path.sep}dist${path.sep}`) || CURRENT_MODULE_PATH.includes(`${path.sep}dist-runtime${path.sep}`) ? CONTRACT_API_EXTENSIONS : [...CONTRACT_API_EXTENSIONS.slice(3), ...CONTRACT_API_EXTENSIONS.slice(0, 3)];
["doctor-contract-api", "contract-api"].map((basename) => {
const rootPaths = ORDERED_EXTENSIONS.map((extension) => `${basename}${extension}`);
return {
rootPaths,
paths: rootPaths.flatMap((filename) => [filename, path.join("dist", filename)])
};
});
//#endregion
//#region src/infra/clawhub-spec.ts
/** Parses explicit `clawhub:<name>[@version]` package specs for ClawHub installs. */
function parseClawHubPluginSpec(raw) {
const trimmed = raw.trim();
if (!normalizeLowercaseStringOrEmpty(trimmed).startsWith("clawhub:")) return null;
const spec = trimmed.slice(8).trim();
if (!spec) return null;
const atIndex = spec.lastIndexOf("@");
if (atIndex <= 0) return { name: spec };
if (atIndex >= spec.length - 1) return null;
const name = spec.slice(0, atIndex).trim();
const version = spec.slice(atIndex + 1).trim();
if (!name || !version) return null;
return {
name,
version
};
}
//#endregion
//#region src/plugins/manifest-install-owner.ts
const PLUGIN_MANIFEST_INSTALL_OWNER = Symbol.for("openclaw.pluginManifestInstallOwner");
function recordPluginManifestInstallOwner(record, installOwner, ambiguous = false) {
if (!installOwner && !ambiguous) return record;
Object.defineProperty(record, PLUGIN_MANIFEST_INSTALL_OWNER, {
configurable: false,
enumerable: true,
value: ambiguous ? { ambiguous: true } : { installOwner }
});
return record;
}
path.join("plugins", "installs.json");
function resolveStoreEnv(options) {
const env = options.env ?? process.env;
if (options.stateDir) return {
...env,
OPENCLAW_STATE_DIR: options.stateDir
};
if (hasActivePluginInstallRoots()) return {
...env,
OPENCLAW_STATE_DIR: resolveActivePluginInstallRoots(env).stateDir
};
return env;
}
/** Resolves the canonical SQLite-backed installed plugin index path. */
function resolveInstalledPluginIndexStorePath(options = {}) {
if (options.filePath) return options.filePath;
return resolveOpenClawStateSqlitePath(resolveStoreEnv(options));
}
/** Resolves state database options for the installed plugin index store. */
function resolveInstalledPluginIndexStateDatabaseOptions(options = {}) {
if (options.filePath) return {
...options.env ? { env: options.env } : {},
path: options.filePath
};
return { env: resolveStoreEnv(options) };
}
//#endregion
//#region src/plugins/installed-plugin-index-record-state.ts
/** Read failures must escape before either projection can authorize recovery or rebuilding. */
function readPersistedInstalledPluginIndexRowSync(options) {
if (options.filePath?.endsWith(".json")) return;
return readPluginMetadataStateRowSync("installed-index", resolveInstalledPluginIndexStateDatabaseOptions(options), options.artifactPreservingReadOnly);
}
/** Share the SQLite row while validating install records independently from index metadata. */
function getPersistedInstalledPluginIndexCacheEntry(options, readRow = () => readPersistedInstalledPluginIndexRowSync(options)) {
const cache = getPluginCache().persistedInstalledIndex;
const key = path.resolve(resolveInstalledPluginIndexStorePath(options));
const current = cache.get(key);
if (current && "value" in current) return current.value;
const row = readRow();
const entry = { state: row ? {
status: "present",
value: safeParseJson(row.value_json)
} : { status: "missing" } };
cache.set(key, { value: entry });
return entry;
}
function inspectPersistedInstalledPluginIndexInstallRecords(entry) {
if (!entry.records) {
const state = entry.state;
const records = (state.status === "present" ? state.value : void 0)?.index?.installRecords;
entry.records = state.status === "missing" ? { status: "missing" } : records === void 0 ? { status: "invalid" } : inspectPluginInstallRecordMap(records);
}
return entry.records;
}
function inspectPersistedInstalledPluginIndexInstallRecordsSync(options = {}) {
return inspectPersistedInstalledPluginIndexInstallRecords(getPersistedInstalledPluginIndexCacheEntry(options));
}
//#endregion
//#region src/plugins/npm-project-roots.ts
function sortPaths(paths) {
return paths.toSorted((left, right) => left.localeCompare(right));
}
function readManagedProjects(projectDirs, npmDir) {
return sortPaths(projectDirs).flatMap((projectRoot) => {
const manifest = tryReadJsonSync(path.join(projectRoot, "package.json"));
if (!isRecord(manifest) || !isRecord(manifest.dependencies)) return [];
return Object.keys(manifest.dependencies).some((packageName) => validatePluginId(packageName) === null && isPluginNpmProjectDir({
packageName,
projectDir: projectRoot,
npmDir
})) ? [{
projectRoot,
manifest
}] : [];
});
}
/** Lists directory candidates; each consumer establishes its own package ownership. */
function listPluginNpmProjectCandidatesSync(npmRoot) {
const projectsDir = resolvePluginNpmProjectsDir(npmRoot);
try {
return sortPaths(fsSync.readdirSync(projectsDir, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => path.join(projectsDir, entry.name)));
} catch (error) {
if (isNotFoundPathError(error)) return [];
throw error;
}
}
/** Lists admitted projects with the manifest that established their managed ownership. */
function listManagedPluginNpmProjectsSync(npmRoot) {
return readManagedProjects(listPluginNpmProjectCandidatesSync(npmRoot), npmRoot);
}
//#endregion
//#region src/plugins/managed-npm-retention.ts
const RETAINED_MANAGED_NPM_INSTALL_MARKER_DIR = ".openclaw-retained-npm-installs";
function resolveRetainedManagedNpmInstallPackageInfo(packageDir) {
const resolvedPackageDir = path.resolve(packageDir);
const packageBase = path.basename(resolvedPackageDir);
const parentDir = path.dirname(resolvedPackageDir);
const parentBase = path.basename(parentDir);
const scopedPackage = parentBase.startsWith("@");
const nodeModulesRoot = scopedPackage ? path.dirname(parentDir) : parentDir;
if (path.basename(nodeModulesRoot) !== "node_modules") return null;
const packageName = scopedPackage ? `${parentBase}/${packageBase}` : packageBase;
if (!packageBase || packageBase === "." || !packageName.trim()) return null;
const projectRoot = path.dirname(nodeModulesRoot);
return {
packageName,
projectRoot,
markerPath: path.join(projectRoot, RETAINED_MANAGED_NPM_INSTALL_MARKER_DIR, `${safePathSegmentHashed(packageName)}.json`)
};
}
function hasRetainedManagedNpmInstallMarker(packageDir) {
const info = resolveRetainedManagedNpmInstallPackageInfo(packageDir);
return info ? fsSync.existsSync(info.markerPath) : false;
}
//#endregion
//#region src/plugins/installed-plugin-index-record-reader.ts
/** Reads installed-index records back into manifest registry records. */
function readJsonObjectFileSync(filePath) {
const parsed = tryReadJsonSync(filePath);
return isRecord(parsed) ? parsed : null;
}
function readStringRecord(value) {
if (!isRecord(value)) return {};
const record = {};
for (const [key, raw] of Object.entries(value).toSorted(([left], [right]) => left.localeCompare(right))) {
if (isBlockedObjectKey$1(key)) continue;
if (typeof raw === "string" && raw.trim()) record[key] = raw.trim();
}
return record;
}
function hasPackagePluginMetadata(manifest) {
const openclaw = manifest.openclaw;
if (!isRecord(openclaw)) return false;
const extensions = openclaw.extensions;
return Array.isArray(extensions) && extensions.some((entry) => typeof entry === "string");
}
function readManifestPluginId(packageDir) {
const manifest = readJsonObjectFileSync(path.join(packageDir, "openclaw.plugin.json"));
return (typeof manifest?.id === "string" ? manifest.id.trim() : "") || void 0;
}
function resolveRecoveredManagedNpmRoot(options = {}) {
return path.resolve(options.stateDir ? path.join(options.stateDir, "npm") : resolveDefaultPluginNpmDir(options.env));
}
function resolveRecoveredManagedNpmPluginId(params) {
const { packageManifest } = params;
if (!packageManifest || !hasPackagePluginMetadata(packageManifest)) return;
const packageName = typeof packageManifest.name === "string" && packageManifest.name.trim() ? packageManifest.name.trim() : params.packageName;
const pluginId = readManifestPluginId(params.packageDir) ?? packageName;
return validatePluginId(pluginId) ? void 0 : pluginId;
}
function readManagedNpmInstallTimestampMs(params) {
const timestampPaths = params.sharedLegacyRoot ? [params.packageDir] : [path.join(params.projectRoot, "package.json"), params.projectRoot];
for (const filePath of timestampPaths) try {
return fsSync.statSync(filePath).mtimeMs;
} catch {}
return 0;
}
function buildRecoveredManagedNpmInstallCandidatesForRoot(params) {
const dependencies = readStringRecord(params.rootManifest?.dependencies);
const candidates = [];
for (const [packageName, dependencySpec] of Object.entries(dependencies)) {
const packageDir = path.join(params.projectRoot, "node_modules", ...packageName.split("/"));
if (!safeStatSync(packageDir)?.isDirectory()) continue;
if (hasRetainedManagedNpmInstallMarker(packageDir)) continue;
const packageManifest = readJsonObjectFileSync(path.join(packageDir, "package.json"));
const pluginId = resolveRecoveredManagedNpmPluginId({
packageName,
packageDir,
packageManifest
});
if (!pluginId) continue;
const version = typeof packageManifest?.version === "string" && packageManifest.version.trim() ? packageManifest.version.trim() : void 0;
candidates.push({
pluginId,
installTimestampMs: readManagedNpmInstallTimestampMs({
packageDir,
projectRoot: params.projectRoot,
sharedLegacyRoot: params.sharedLegacyRoot
}),
installRecord: {
source: "npm",
spec: `${packageName}@${dependencySpec}`,
installPath: packageDir,
...version ? {
version,
resolvedName: packageName,
resolvedVersion: version
} : {},
...version ? { resolvedSpec: `${packageName}@${version}` } : {}
}
});
}
return candidates;
}
/** Lists recoverable managed npm installs without assigning active precedence. */
function listRecoveredManagedNpmInstallCandidates(options = {}) {
const npmRoot = resolveRecoveredManagedNpmRoot(options);
return [...buildRecoveredManagedNpmInstallCandidatesForRoot({
projectRoot: npmRoot,
rootManifest: readJsonObjectFileSync(path.join(npmRoot, "package.json")),
sharedLegacyRoot: true
}), ...listManagedPluginNpmProjectsSync(npmRoot).flatMap(({ projectRoot, manifest }) => buildRecoveredManagedNpmInstallCandidatesForRoot({
projectRoot,
rootManifest: manifest,
sharedLegacyRoot: false
}))];
}
function recordsShareInstallPath(left, right) {
if (!left?.installPath || !right.installPath) return false;
return normalizeInstallPathForComparison(left.installPath) === normalizeInstallPathForComparison(right.installPath);
}
function normalizeInstallPathForComparison(filePath) {
const resolved = path.resolve(filePath);
return process.platform === "win32" ? normalizeWindowsPathForComparison$1(resolved) : resolved;
}
function pickMostRecentRecoveredManagedNpmCandidate(candidates) {
return candidates.toSorted((left, right) => {
const byTimestamp = right.installTimestampMs - left.installTimestampMs;
if (byTimestamp !== 0) return byTimestamp;
return (right.installRecord.installPath ?? "").localeCompare(left.installRecord.installPath ?? "");
})[0];
}
function emitManagedNpmRecoveryFallbackWarning(params) {
process.emitWarning(`Managed npm recovery found ${params.candidates.length} installs for plugin "${params.pluginId}" without an authoritative active path; selected the most recently installed candidate. Run \`openclaw doctor --fix\` to persist and retire stale generations.`, {
code: "OPENCLAW_PLUGIN_INSTALL_RECOVERY_FALLBACK",
type: "OpenClawPluginRecoveryWarning",
detail: JSON.stringify({
pluginId: params.pluginId,
selectedInstallPath: params.selected.installRecord.installPath,
candidates: params.candidates.map((candidate) => ({
installPath: candidate.installRecord.installPath,
installTimestampMs: candidate.installTimestampMs
}))
})
});
}
function buildRecoveredManagedNpmInstallRecords(persisted, options = {}) {
const npmRoot = resolveRecoveredManagedNpmRoot(options);
const records = createPluginInstallRecordMap();
const candidatesByPluginId = groupPluginRecords(listRecoveredManagedNpmInstallCandidates(options), (candidate) => candidate.pluginId);
for (const [pluginId, candidates] of candidatesByPluginId) {
const persistedRecord = getPluginInstallRecordMapEntry(persisted ?? void 0, pluginId);
const authoritative = candidates.find((candidate) => recordsShareInstallPath(persistedRecord, candidate.installRecord));
const selected = authoritative ?? pickMostRecentRecoveredManagedNpmCandidate(candidates);
setPluginInstallRecordMapEntry(records, pluginId, selected.installRecord);
const recoversUnavailableManagedPath = isUnavailableManagedNpmInstallRecord({
npmRoot,
persisted: persistedRecord,
recovered: selected.installRecord
});
if (!authoritative && candidates.length > 1 && (!persistedRecord || recoversUnavailableManagedPath)) emitManagedNpmRecoveryFallbackWarning({
pluginId,
selected,
candidates
});
}
return records;
}
function readInstallRecordVersion(record) {
return record?.resolvedVersion ?? record?.version;
}
function isUnavailableManagedNpmInstallRecord(params) {
const installPath = params.persisted?.installPath;
if (params.persisted?.source !== "npm" || !installPath) return false;
try {
if (fsSync.statSync(installPath).isDirectory()) return false;
} catch (error) {
if (!isNotFoundPathError(error)) return false;
}
const packageInfo = resolveRetainedManagedNpmInstallPackageInfo(installPath);
if (!packageInfo || packageInfo.packageName !== params.recovered.resolvedName) return false;
const npmRoot = normalizeInstallPathForComparison(params.npmRoot);
return normalizeInstallPathForComparison(packageInfo.projectRoot) === npmRoot || normalizeInstallPathForComparison(path.dirname(packageInfo.projectRoot)) === normalizeInstallPathForComparison(resolvePluginNpmProjectsDir(params.npmRoot));
}
function mergeRecoveredManagedNpmMetadata(persisted, recovered, options = {}) {
const next = {
...persisted,
...recovered
};
if (options.preservePersistedSpec) {
const persistedSpec = persisted.spec ? parseRegistryNpmSpec(persisted.spec) : null;
const selectorIsCompatible = persistedSpec !== null && isPrereleaseResolutionAllowed({
spec: persistedSpec,
resolvedVersion: recovered.resolvedVersion
}) && (persistedSpec.selectorKind !== "exact-version" || persistedSpec.selector !== void 0 && recovered.resolvedVersion !== void 0 && compareValidSemver(persistedSpec.selector, recovered.resolvedVersion) === 0);
if (persistedSpec?.name === recovered.resolvedName && selectorIsCompatible) next.spec = persisted.spec;
}
delete next.integrity;
delete next.shasum;
delete next.resolvedAt;
delete next.installedAt;
return next;
}
function mergeRecoveredManagedNpmRecord(params) {
if (params.persisted && isUnavailableManagedNpmInstallRecord(params)) return mergeRecoveredManagedNpmMetadata(params.persisted, params.recovered, { preservePersistedSpec: true });
const persistedVersion = readInstallRecordVersion(params.persisted);
const recoveredVersion = readInstallRecordVersion(params.recovered);
if (params.persisted?.source === "npm" && recordsShareInstallPath(params.persisted, params.recovered) && recoveredVersion && persistedVersion !== recoveredVersion) return mergeRecoveredManagedNpmMetadata(params.persisted, params.recovered);
return params.persisted ?? params.recovered;
}
/** Merges persisted records with managed npm installs recovered from the current root. */
function mergeRecoveredManagedNpmInstallRecords(persisted, options) {
const npmRoot = resolveRecoveredManagedNpmRoot(options);
const recovered = buildRecoveredManagedNpmInstallRecords(persisted, options);
const merged = copyPluginInstallRecordMap(persisted ?? void 0);
for (const [pluginId, record] of Object.entries(recovered)) setPluginInstallRecordMapEntry(merged, pluginId, mergeRecoveredManagedNpmRecord({
npmRoot,
persisted: getPluginInstallRecordMapEntry(merged, pluginId),
recovered: record
}));
return merged;
}
function requireLoadablePluginInstallRecordState(state) {
if (state.status === "invalid") throw new Error("Persisted plugin install records are invalid. Run openclaw doctor to inspect and repair plugin installation state.");
return state.status === "valid" ? state.records : null;
}
function resolveInstallRecordsCacheKey(options) {
return [path.resolve(resolveInstalledPluginIndexStorePath(options)), resolveRecoveredManagedNpmRoot(options)].join("\0");
}
/** Synchronously loads installed plugin records, recovering managed npm installs and caching them. */
function loadInstalledPluginIndexInstallRecordsSync(params = {}) {
const cacheKey = resolveInstallRecordsCacheKey(params);
const cache = getPluginCache().installRecords;
const cached = cache.get(cacheKey);
if (cached) return copyPluginInstallRecordMap(cached);
const records = mergeRecoveredManagedNpmInstallRecords(requireLoadablePluginInstallRecordState(inspectPersistedInstalledPluginIndexInstallRecordsSync(params)), params);
cache.set(cacheKey, records);
return copyPluginInstallRecordMap(records);
}
//#endregion
//#region src/plugins/official-external-install-records.ts
function resolveNpmSpecPackageName(spec) {
return spec ? parseRegistryNpmSpec(spec)?.name : void 0;
}
function resolveClawHubSpecPackageName(spec) {
return spec ? parseClawHubPluginSpec(spec)?.name : void 0;
}
function resolveExactNpmPackageName(value) {
const packageName = resolveNpmSpecPackageName(value);
return packageName && value.trim() === packageName ? packageName : void 0;
}
function resolveRecordedPackageNames(fields) {
const packageNames = [];
for (const [value, resolvePackageName] of fields) {
if (value === void 0) continue;
const packageName = resolvePackageName(value);
if (!packageName) return;
packageNames.push(packageName);
}
return packageNames;
}
function resolveUnanimousRecordedNpmPackageName(record) {
if (record.source !== "npm") return;
const packageNames = resolveRecordedPackageNames([
[record.spec, resolveNpmSpecPackageName],
[record.resolvedName, resolveExactNpmPackageName],
[record.resolvedSpec, resolveNpmSpecPackageName]
]);
return packageNames && new Set(packageNames).size === 1 ? packageNames[0] : void 0;
}
function hasTrustedOfficialNpmProvenance(record) {
return record.source === "npm" && record.artifactKind === void 0 && record.sourcePath === void 0;
}
function resolveRecordedClawHubPackageNames(record) {
return resolveRecordedPackageNames([
[record.clawhubPackage, resolveExactNpmPackageName],
[record.spec, resolveClawHubSpecPackageName],
[record.resolvedSpec, (spec) => resolveClawHubSpecPackageName(spec) ?? resolveNpmSpecPackageName(spec)],
[record.resolvedName, resolveExactNpmPackageName]
]);
}
function isOfficialClawHubInstallRecord(record) {
if (record.source !== "clawhub" || record.clawhubChannel !== "official") return false;
return (record.clawhubUrl ?? "").trim().replace(/\/+$/, "") === "https://clawhub.ai";
}
/** Resolves one package identity from a current trusted official ClawHub install record. */
function resolveTrustedOfficialClawHubPackageName(record) {
if (!isOfficialClawHubInstallRecord(record)) return;
const packageNames = resolveRecordedClawHubPackageNames(record);
return packageNames && new Set(packageNames).size === 1 ? packageNames[0] : void 0;
}
/** Binds official trust to the actual package and consistent recorded source identity. */
function isTrustedOfficialPluginInstallRecord(params) {
const packageName = params.packageName?.trim();
const entry = packageName ? getOfficialExternalPluginCatalogEntryForPackage(packageName) : void 0;
if (!entry || resolveOfficialExternalPluginId(entry) !== params.pluginId && !resolveOfficialExternalPluginLegacyIds(entry).includes(params.pluginId)) return false;
const install = resolveOfficialExternalPluginInstall(entry);
const record = params.record;
if (record.source === "npm") return hasTrustedOfficialNpmProvenance(record) && resolveNpmSpecPackageName(install?.npmSpec) === packageName && resolveUnanimousRecordedNpmPackageName(record) === packageName && (record.clawhubPackage === void 0 || resolveExactNpmPackageName(record.clawhubPackage) === packageName);
return Boolean(install?.clawhubSpec || install?.npmSpec) && (record.clawhubPackage !== void 0 || record.spec !== void 0) && resolveTrustedOfficialClawHubPackageName(record) === packageName;
}
//#endregion
//#region src/plugins/installed-plugin-record-match.ts
function resolveCandidateInstallOwner(params) {
if (isPluginCandidateInstallOwnerAmbiguous(params.candidate)) return;
const installOwner = resolvePluginCandidateInstallOwner(params.candidate);
if (installOwner) return Object.hasOwn(params.installRecords, installOwner) ? installOwner : void 0;
}
function matchesInstalledPluginRecord(params) {
if (params.candidate.origin !== "global" && params.candidate.origin !== "config") return false;
const installOwner = resolveCandidateInstallOwner(params);
const record = installOwner ? params.installRecords[installOwner] : void 0;
if (!record) return false;
const candidatePaths = [
params.candidate.rootDir,
params.candidate.packageDir,
params.candidate.source,
params.candidate.setupSource
].filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => {
const resolved = resolveUserPath(entry, params.env);
return pluginCacheRealpathSync(resolved) ?? resolved;
});
const trackedPaths = (params.installPathOnly ? [record.installPath] : [record.installPath, record.sourcePath]).filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => {
const resolved = resolveUserPath(entry, params.env);
return pluginCacheRealpathSync(resolved) ?? resolved;
});
if (candidatePaths.length === 0 || trackedPaths.length === 0) return false;
return trackedPaths.some((trackedPath) => candidatePaths.some((candidatePath) => candidatePath === trackedPath || isPathInside$1(trackedPath, candidatePath) || isPathInside$1(candidatePath, trackedPath)));
}
function resolvePluginTrust(params) {
const installOwner = resolveCandidateInstallOwner(params);
const record = installOwner ? params.installRecords[installOwner] : void 0;
const origin = params.candidate.origin;
let reason;
if (origin === "bundled") reason = "bundled";
else if (isPluginCandidateInstallOwnerAmbiguous(params.candidate)) reason = "owner-ambiguous";
else if (origin === "workspace" || record?.source === "path" || record?.source === "npm" && (record.artifactKind !== void 0 || record.sourcePath !== void 0)) reason = "origin-path";
else if (!record || !installOwner) reason = "record-missing";
else if (!matchesInstalledPluginRecord({
pluginId: params.pluginId,
candidate: params.candidate,
env: params.env,
installRecords: params.installRecords,
installPathOnly: true
})) reason = "install-path-mismatch";
else if (isTrustedOfficialPluginInstallRecord({
pluginId: installOwner,
packageName: params.candidate.packageName,
record
})) reason = "trusted-official";
else if (record.source === "npm" && record.spec === void 0 && record.resolvedName === void 0 && record.resolvedSpec === void 0 || record.source === "clawhub" && record.clawhubUrl === void 0 && record.clawhubChannel === void 0) reason = "provenance-missing";
else reason = "provenance-invalid";
return {
reason,
registryPath: params.registryPath,
origin,
installSource: record?.source,
installSpec: record?.spec === void 0 ? void 0 : redactSensitiveText(record.spec, { mode: "tools" })
};
}
//#endregion
//#region packages/gateway-protocol/src/svg-image.ts
const SVG_PROLOGUE_WHITESPACE_RE = /\s*/y;
function skipSvgPrologueWhitespace(text, index) {
SVG_PROLOGUE_WHITESPACE_RE.lastIndex = index;
SVG_PROLOGUE_WHITESPACE_RE.exec(text);
return SVG_PROLOGUE_WHITESPACE_RE.lastIndex;
}
function startsWithToken(text, index, token) {
return text.slice(index, index + token.length).toLowerCase() === token;
}
/**
* Recognizes an SVG root element after an optional XML declaration and comments.
*
* An index scan rather than a regex on purpose: the equivalent
* `(?:<!--[\s\S]*?-->\s*)*<svg` backtracks exponentially on comment-like bytes that
* never reach a root element, and these bytes arrive from remote icon and
* link-favicon responses on the Gateway's single event loop, so one crafted
* response would stall every session. A comment ends at its first `-->`, so text
* between a closed comment and the root element is rejected, not absorbed.
*/
function startsWithSvgRootElement(text) {
let index = skipSvgPrologueWhitespace(text, 0);
if (startsWithToken(text, index, "<?xml")) {
const declarationEnd = text.indexOf(">", index);
if (declarationEnd < 0) return false;
index = skipSvgPrologueWhitespace(text, declarationEnd + 1);
}
while (startsWithToken(text, index, "<!--")) {
const commentEnd = text.indexOf("-->", index + 4);
if (commentEnd < 0) return false;
index = skipSvgPrologueWhitespace(text, commentEnd + 3);
}
if (!startsWithToken(text, index, "<svg")) return false;
const delimiter = text[index + 4];
return delimiter === ">" || delimiter === "/" && text[index + 5] === ">" || delimiter !== void 0 && /\s/u.test(delimiter);
}
/**
* SVG images stay self-contained: no script, document expansion, embedded
* documents, or outbound fetches can reach the browser through an image route.
*/
function isSelfContainedSvg(text) {
return !text.includes("\0") && !/<!doctype|<!entity/iu.test(text) && !/<\s*(?:script|foreignObject|image|use|iframe)\b/iu.test(text) && !/\b(?:href|xlink:href|src)\s*=/iu.test(text) && startsWithSvgRootElement(text);
}
//#endregion
//#region src/plugins/portable-icon-paths.ts
/** Package-local artwork contracts shared by discovery and package builders. */
const PORTABLE_PLUGIN_ICON_PATH = "assets/icon.png";
const PLUGIN_ACTIVITY_ICON_PATH = "assets/activity.svg";
const PLUGIN_TOOL_ACTIVITY_ICON_DIR = "assets/activity";
const PLUGIN_ACTIVITY_ICON_MAX_BYTES = 32768;
function isPluginActivityToolName(value) {
return /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/u.test(value);
}
//#endregion
//#region src/plugins/manifest-theme-definitions.ts
/** Capture palettes and artwork so a published generation never reads changed files. */
function loadManifestThemeDefinitions(params) {
if (!params.themes?.length) return;
return params.themes.flatMap((theme) => {
try {
if (params.pluginId === "user" || !isThemeId(`${params.pluginId}/${theme.id}`)) throw new Error("qualified theme ID must be portable, outside user/, and at most 256 characters");
const file = readPluginCacheFile({
rootDir: params.rootDir,
relativePath: theme.source,
rejectHardlinks: params.rejectHardlinks,
maxBytes: MAX_THEME_DEFINITION_BYTES * 4
});
if (!file.ok) throw new Error("source must be a readable JSON file inside the plugin root");
const definition = normalizeThemeDefinition(JSON.parse(file.contents.toString("utf8")), {
hatIds: Object.keys(theme.hats ?? {}),
critterIds: Object.keys(theme.critters ?? {})
});
if (definition.name !== theme.name || definition.description !== theme.description) throw new Error("name and description must match the manifest declaration");
const readSvg = (source) => {
const svgFile = readPluginCacheFile({
rootDir: params.rootDir,
relativePath: source,
rejectHardlinks: params.rejectHardlinks,
maxBytes: PLUGIN_ACTIVITY_ICON_MAX_BYTES
});
if (!svgFile.ok) throw new Error(`artwork ${source} must be a readable SVG inside the plugin root, at most ${PLUGIN_ACTIVITY_ICON_MAX_BYTES} bytes`);
const svg = svgFile.contents.toString("utf8");
if (!isSelfContainedSvg(svg)) throw new Error(`artwork ${source} must be a self-contained SVG`);
return svg;
};
const artwork = {
...theme.hats ? { hats: Object.fromEntries(Object.entries(theme.hats).map(([id, source]) => [id, { svg: readSvg(source) }])) } : {},
...theme.critters ? { critters: Object.fromEntries(Object.entries(theme.critters).map(([id, { source, ...metadata }]) => [id, {
svg: readSvg(source),
...metadata
}])) } : {}
};
return [{
id: theme.id,
definition,
...theme.hats || theme.critters ? { artwork } : {}
}];
} catch (error) {
params.diagnostics.push({
level: "warn",
pluginId: params.pluginId,
message: `theme ${theme.id} is unavailable: ${error instanceof Error ? error.message : "invalid definition"}`
});
return [];
}
});
}
//#endregion
//#region src/plugins/manifest-record.ts
function resolvePluginSourcePath(sourcePath) {
if (pluginCacheExistsSync(sourcePath)) return sourcePath;
if (sourcePath.endsWith(".ts")) {
const jsPath = sourcePath.slice(0, -3) + ".js";
if (pluginCacheExistsSync(jsPath)) return jsPath;
}
return sourcePath;
}
function isPluginRootPath(params) {
const resolvedTargetPath = path.resolve(params.targetPath);
if (!isPathInside$1(path.resolve(params.rootPath), resolvedTargetPath)) return false;
const targetRealPath = pluginCacheRealpathSync(resolvedTargetPath);
if (!targetRealPath) return params.targetMustExist !== true;
if (!isPathInside$1(params.rootRealPath, targetRealPath)) return false;
if (params.rejectHardlinks === true) {
const targetStat = pluginCacheStatSync(resolvedTargetPath);
if (!targetStat || targetStat.nlink > 1) return false;
}
return true;
}
function resolveManifestPluginSourcePath(params) {
const pushDiagnostic = () => {
params.diagnostics.push({
level: "warn",
pluginId: sanitizeForLog(params.pluginId),
source: sanitizeForLog(params.manifestPath),
message: `plugin manifest ${params.entryName} must resolve inside the plugin root; ignoring entry`
});
};
if (!params.entry || path.isAbsolute(params.entry)) {
pushDiagnostic();
return;
}
const rootPath = path.resolve(params.rootDir);
const rootRealPath = pluginCacheRealpathSync(rootPath) ?? rootPath;
const sourcePath = path.resolve(rootPath, params.entry);
if (!isPluginRootPath({
rootPath,
targetPath: sourcePath,
rootRealPath,
rejectHardlinks: params.rejectHardlinks,
targetMustExist: pluginCacheExistsSync(sourcePath)
})) {
pushDiagnostic();
return;
}
const resolvedSourcePath = resolvePluginSourcePath(sourcePath);
if (!isPluginRootPath({
rootPath,
targetPath: resolvedSourcePath,
rootRealPath,
rejectHardlinks: params.rejectHardlinks,
targetMustExist: pluginCacheExistsSync(resolvedSourcePath)
})) {
pushDiagnostic();
return;
}
return resolvedSourcePath;
}
function resolvePortablePluginIcons(params) {
let root;
const resolveIcon = (relativePath) => {
const iconPath = path.resolve(params.rootDir, relativePath);
const iconStat = pluginCacheLstatSync(iconPath);
if (!iconStat?.isFile() || params.rejectHardlinks && iconStat.nlink > 1) return;
if (!root) {
const rootPath = path.resolve(params.rootDir);
root = {
path: rootPath,
realPath: pluginCacheRealpathSync(rootPath) ?? rootPath
};
}
return isPluginRootPath({
rootPath: root.path,
targetPath: iconPath,
rootRealPath: root.realPath,
rejectHardlinks: params.rejectHardlinks,
targetMustExist: true
}) ? iconPath : void 0;
};
const iconPath = resolveIcon(PORTABLE_PLUGIN_ICON_PATH);
const activityIconPath = resolveIcon(PLUGIN_ACTIVITY_ICON_PATH);
const directory = path.resolve(params.rootDir, PLUGIN_TOOL_ACTIVITY_ICON_DIR);
if (!isPluginRootPath({
rootPath: params.rootDir,
rootRealPath: params.rootDir === root?.path ? root.realPath : pluginCacheRealpathSync(params.rootDir) ?? params.rootDir,
targetPath: directory,
targetMustExist: true
})) return {
iconPath,
activityIconPath
};
let entries;
try {
entries = readPluginCacheDirectory(directory);
} catch {
return {
iconPath,
activityIconPath
};
}
if (entries.length > 128) return {
iconPath,
activityIconPath
};
const paths = [];
for (const name of entries.map((entry) => entry.name).toSorted()) {
const toolName = name.endsWith(".svg") ? name.slice(0, -4) : "";
if (!isPluginActivityToolName(toolName)) continue;
const toolIconPath = resolveIcon(`${PLUGIN_TOOL_ACTIVITY_ICON_DIR}/${name}`);
if (toolIconPath) paths.push([toolName, toolIconPath]);
}
return {
iconPath,
activityIconPath,
...paths.length ? { toolActivityIconPaths: Object.fromEntries(paths) } : {}
};
}
function mergePackageChannelMetaIntoChannelConfigs(params) {
const channelId = params.packageChannel?.id?.trim();
if (!channelId || isBlockedObjectKey$1(channelId) || !params.channelConfigs || !Object.hasOwn(params.channelConfigs, channelId)) return params.channelConfigs;
const existing = params.channelConfigs[channelId];
if (!existing) return params.channelConfigs;
const label = existing.label ?? normalizeOptionalString(params.packageChannel?.label) ?? "";
const description = existing.description ?? normalizeOptionalString(params.packageChannel?.blurb) ?? "";
const preferOver = existing.preferOver ?? normalizeOptionalTrimmedStringList(params.packageChannel?.preferOver);
const commands = existing.commands ?? normalizeManifestChannelCommandDefaults(params.packageChannel?.commands);
const merged = Object.create(null);
for (const [key, value] of Object.entries(params.channelConfigs)) if (!isBlockedObjectKey$1(key)) merged[key] = value;
merged[channelId] = {
...existing,
...label ? { label } : {},
...description ? { description } : {},
...preferOver?.length ? { preferOver } : {},
...commands ? { commands } : {}
};
return merged;
}
function mergeManifestContracts(manifestContracts, catalogContracts) {
if (!catalogContracts) return manifestContracts;
const contracts = {};
for (const key of PLUGIN_MANIFEST_CONTRACT_KEYS) {
const merged = normalizeUniqueTrimmedStringList([...manifestContracts?.[key] ?? [], ...catalogContracts[key] ?? []]);
if (merged.length > 0) contracts[key] = merged;
}
return Object.keys(contracts).length > 0 ? contracts : void 0;
}
function mergeCatalogChannelConfigs(params) {
if (!params.catalogChannelConfigs) return params.manifestChannelConfigs;
const merged = Object.create(null);
for (const [key, value] of Object.entries(params.catalogChannelConfigs)) if (!isBlockedObjectKey$1(key)) merged[key] = value;
for (const [key, value] of Object.entries(params.manifestChannelConfigs ?? {})) if (!isBlockedObjectKey$1(key)) {
const catalogValue = merged[key];
merged[key] = catalogValue ? {
...catalogValue,
...value,
schema: value.schema ?? catalogValue.schema,
...catalogValue.uiHints || value.uiHints ? { uiHints: {
...catalogValue.uiHints,
...value.uiHints
} } : {},
...value.runtime ?? catalogValue.runtime ? { runtime: value.runtime ?? catalogValue.runtime } : {},
...value.label ?? catalogValue.label ? { label: value.label ?? catalogValue.label } : {},
...value.description ?? catalogValue.description ? { description: value.description ?? catalogValue.description } : {},
...value.preferOver ?? catalogValue.preferOver ? { preferOver: value.preferOver ?? catalogValue.preferOver } : {},
...value.commands ?? catalogValue.commands ? { commands: value.commands ?? catalogValue.commands } : {}
} : value;
}
return Object.keys(merged).length > 0 ? merged : void 0;
}
function buildPluginManifestRecord(params) {
const pluginId = params.candidate.effectivePluginId ?? params.manifest.id;
const resolveCatalogEntry = (entryName) => {
const entry = params.manifest[entryName];
return entry === void 0 ? void 0 : resolveManifestPluginSourcePath({
rootDir: params.candidate.rootDir,
manifestPath: params.manifestPath,
pluginId,
entryName,
entry,
rejectHardlinks: params.rejectHardlinks,
diagnostics: params.diagnostics
});
};
const manifestChannelConfigs = params.candidate.origin === "bundled" && params.bundledChannelConfigCollector ? params.bundledChannelConfigCollector({
pluginDir: params.candidate.packageDir ?? params.candidate.rootDir,
manifest: params.manifest,
packageManifest: params.candidate.packageManifest
}) : params.manifest.channelConfigs;
const officialCatalogManifest = params.candidate.origin !== "bundled" ? getOfficialExternalPluginCatalogManifest(getOfficialExternalPluginCatalogEntryForPackage(params.candidate.packageName) ?? {}) : void 0;
const channelConfigs = mergePackageChannelMetaIntoChannelConfigs({
channelConfigs: mergeCatalogChannelConfigs({
manifestChannelConfigs,
catalogChannelConfigs: officialCatalogManifest?.channelConfigs
}),
packageChannel: params.candidate.packageManifest?.channel
});
const packageChannelCommands = normalizeManifestChannelCommandDefaults(params.candidate.packageManifest?.channel?.commands);
return {
id: pluginId,
categories: params.manifest.categories,
backupResources: params.manifest.backupResources,
doctorContract: params.manifest.doctorContract,
doctorHealthChecks: params.manifest.doctorHealthChecks,
sessionRouteStateOwners: params.manifest.sessionRouteStateOwners,
name: normalizeOptionalString(params.manifest.name) ?? params.candidate.packageName,
description: normalizeOptionalString(params.manifest.description) ?? params.candidate.packageDescription,
catalog: normalizeManifestCatalog({
featured: params.manifest.catalog?.featured ?? officialCatalogManifest?.catalog?.featured,
order: params.manifest.catalog?.order ?? officialCatalogManifest?.catalog?.order
}),
...resolvePortablePluginIcons({
rootDir: params.candidate.rootDir,
rejectHardlinks: params.rejectHardlinks
}),
version: normalizeOptionalString(params.manifest.version) ?? params.candidate.packageVersion,
packageName: params.candidate.packageName,
packageVersion: params.candidate.packageVersion,
packageDescription: params.candidate.packageDescription,
enabledByDefault: params.manifest.enabledByDefault === true ? true : void 0,
enabledByDefaultOnPlatforms: params.manifest.enabledByDefaultOnPlatforms,
autoEnableWhenConfiguredProviders: params.manifest.autoEnableWhenConfiguredProviders,
legacyPluginIds: params.manifest.legacyPluginIds,
format: params.candidate.format ?? "openclaw",
bundleFormat: params.candidate.bundleFormat,
kind: params.manifest.kind,
channels: params.manifest.channels ?? [],
channelAccountKeyPolicies: params.manifest.channelAccountKeyPolicies,
providers: params.manifest.providers ?? [],
providerDiscoverySource: resolveCatalogEntry("providerCatalogEntry"),
capabilityCatalogSource: params.manifest.capabilityCatalogEntry === void 0 ? void 0 : resolveCatalogEntry("capabilityCatalogEntry") ?? null,
modelSupport: params.manifest.modelSupport,
modelCatalog: params.manifest.modelCatalog,
modelPricing: params.manifest.modelPricing,
modelIdNormalization: params.manifest.modelIdNormalization,
providerEndpoints: params.manifest.providerEndpoints,
providerRequest: params.manifest.providerRequest,
secretProviderIntegrations: params.manifest.secretProviderIntegrations,
cliBackends: params.manifest.cliBackends ?? [],
syntheticAuthRefs: params.manifest.syntheticAuthRefs ?? [],
nonSecretAuthMarkers: params.manifest.nonSecretAuthMarkers ?? [],
commandAliases: params.manifest.commandAliases,
cliCommands: params.manifest.cliCommands,
providerUsageAuthEnvVars: params.manifest.providerUsageAuthEnvVars,
providerAuthAliases: params.manifest.providerAuthAliases,
providerAuthChoices: params.manifest.providerAuthChoices,
activation: params.manifest.activation,
setup: params.manifest.setup,
packageManifest: params.candidate.packageManifest,
packageDependencies: params.candidate.packageDependencies,
packageOptionalDependencies: params.candidate.packageOptionalDependencies,
packageChannel: params.candidate.packageManifest?.channel,
packageInstall: params.candidate.packageManifest?.install,
trustedOfficialInstall: params.trust.reason === "trusted-official" ? true : void 0,
trust: params.trust,
qaRunners: params.manifest.qaRunners,
dashboard: params.manifest.dashboard,
controlUi: params.manifest.controlUi,
uiCapabilities: params.manifest.uiCapabilities,
themes: params.manifest.themes,
themeDefinitions: loadManifestThemeDefinitions({
pluginId,
rootDir: params.candidate.rootDir,
themes: params.manifest.themes,
rejectHardlinks: params.rejectHardlinks,
diagnostics: params.diagnostics
}),
mcpServers: params.manifest.mcpServers,
skills: params.manifest.skills ?? [],
settingsFiles: [],
hooks: [],
origin: params.candidate.origin,
workspaceDir: params.candidate.workspaceDir,
rootDir: params.candidate.rootDir,
source: params.candidate.source,
setupSource: params.candidate.setupSource,
manifestPath: params.manifestPath,
schemaCacheKey: params.schemaCacheKey,
configSchema: params.configSchema,
configUiHints: params.manifest.uiHints,
configGroups: params.manifest.configGroups,
contracts: mergeManifestContracts(params.manifest.contracts, officialCatalogManifest?.contracts),
transcriptSources: params.manifest.transcriptSources,
decisionModels: params.manifest.decisionModels,
mediaUnderstandingProviderMetadata: params.manifest.mediaUnderstandingProviderMetadata,
imageGenerationProviderMetadata: params.manifest.imageGenerationProviderMetadata,
videoGenerationProviderMetadata: params.manifest.videoGenerationProviderMetadata,
musicGenerationProviderMetadata: params.manifest.musicGenerationProviderMetadata,
toolMetadata: params.manifest.toolMetadata,
configContracts: params.manifest.configContracts,
channelConfigs,
...params.candidate.packageManifest?.channel?.id ? { channelCatalogMeta: {
id: params.candidate.packageManifest.channel.id,
...typeof params.candidate.packageManifest.channel.label === "string" ? { label: params.candidate.packageManifest.channel.label } : {},
...typeof params.candidate.packageManifest.channel.blurb === "string" ? { blurb: params.candidate.packageManifest.channel.blurb } : {},
...params.candidate.packageManifest.channel.preferOver ? { preferOver: params.candidate.packageManifest.channel.preferOver } : {},
...packageChannelCommands ? { commands: packageChannelCommands } : {}
} } : {}
};
}
function buildBundleManifestRecord(params) {
return {
id: params.manifest.id,
name: normalizeOptionalString(params.manifest.name) ?? params.candidate.idHint,
description: normalizeOptionalString(params.manifest.description),
...resolvePortablePluginIcons({
rootDir: params.candidate.rootDir,
rejectHardlinks: params.rejectHardlinks
}),
version: normalizeOptionalString(params.manifest.version),
packageName: params.candidate.packageName,
packageVersion: params.candidate.packageVersion,
packageDescription: params.candidate.packageDescription,
packageManifest: params.candidate.packageManifest,
packageDependencies: params.candidate.packageDependencies,
packageOptionalDependencies: params.candidate.packageOptionalDependencies,
packageChannel: params.candidate.packageManifest?.channel,
packageInstall: params.candidate.packageManifest?.install,
format: "bundle",
bundleFormat: params.candidate.bundleFormat,
bundleCapabilities: params.manifest.capabilities,
...params.manifest.onboardingSkill ? { onboardingSkill: params.manifest.onboardingSkill } : {},
activation: params.manifest.activation,
channels: [],
providers: [],
cliBackends: [],
syntheticAuthRefs: [],
nonSecretAuthMarkers: [],
skills: params.manifest.skills ?? [],
settingsFiles: params.manifest.settingsFiles ?? [],
hooks: params.manifest.hooks ?? [],
origin: params.candidate.origin,
workspaceDir: params.candidate.workspaceDir,
rootDir: params.candidate.rootDir,
source: params.candidate.source,
manifestPath: params.manifestPath,
schemaCacheKey: void 0,
configSchema: void 0,
configUiHints: void 0,
configContracts: void 0,
channelConfigs: void 0
};
}
//#endregion
//#region src/plugins/min-host-version.ts
/** Validation message for plugin minHostVersion manifest fields. */
const MIN_HOST_VERSION_FORMAT = "openclaw.install.minHostVersion must use a semver floor in the form \">=x.y.z[-prerelease][+build]\"";
const SEMVER_LABEL_RE = String.raw`\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?`;
const MIN_HOST_VERSION_RE = new RegExp(`^>=(${SEMVER_LABEL_RE})$`);
const LEGACY_MIN_HOST_VERSION_RE = new RegExp(`^(${SEMVER_LABEL_RE})$`);
/** Parses a plugin minHostVersion manifest field. */
function parseMinHostVersionRequirement(raw, options = {}) {
const trimmed = normalizeOptionalString(raw);
if (!trimmed) return null;
const match = trimmed.match(MIN_HOST_VERSION_RE) ?? (options.allowLegacyBareSemver ? trimmed.match(LEGACY_MIN_HOST_VERSION_RE) : null);
if (!match) return null;
const minimumLabel = match[1] ?? "";
if (!valid(minimumLabel)) return null;
return {
raw: trimmed,
minimumLabel
};
}
/** Checks whether the current host satisfies a plugin minHostVersion requirement. */
function checkMinHostVersion(params) {
if (params.minHostVersion === void 0) return {
ok: true,
requirement: null
};
const requirement = parseMinHostVersionRequirement(params.minHostVersion, { allowLegacyBareSemver: params.allowLegacyBareSemver });
if (!requirement) return {
ok: false,
kind: "invalid",
error: MIN_HOST_VERSION_FORMAT
};
const currentVersion = normalizeOptionalString(params.currentVersion) || "unknown";
const comparison = compareOpenClawVersions(currentVersion, requirement.minimumLabel);
if (comparison === null) return {
ok: false,
kind: "unknown_host_version",
requirement
};
if (comparison < 0) return {
ok: false,
kind: "incompatible",
requirement,
currentVersion
};
return {
ok: true,
requirement
};
}
//#endregion
//#region src/plugins/manifest-registry-build.ts
const PLUGIN_ORIGIN_RANK = {
config: 0,
workspace: 1,
global: 2,
bundled: 3
};
function rejectCaseFoldedIdCollisions(records, diagnostics) {
const rejected = /* @__PURE__ */ new Set();
for (const [policyId, matches] of groupPluginRecords(records, (record) => normalizePluginPolicyId(record.id))) {
const declaredIds = [...new Set(matches.map((record) => record.id))].toSorted();
if (declaredIds.length < 2) continue;
const message = `plugin ids ${declaredIds.map((id) => JSON.stringify(id)).join(", ")} collide as normalized id ${JSON.stringify(policyId)}; refusing all colliding plugins`;
for (const record of matches) {
rejected.add(record);
diagnostics.push({
level: "error",
pluginId: record.id,
source: record.source,
message
});
}
}
return records.filter((record) => !rejected.has(record));
}
function pushNonBundledChannelConfigDescriptorDiagnostic(params) {
if (params.record.origin === "bundled" || params.record.format === "bundle") return;
const configuredEntry = params.normalized?.entries[params.record.id];
if (params.normalized?.enabled === false || configuredEntry?.enabled === false || params.normalized?.deny.includes(params.record.id) || params.normalized?.allow.length && !params.normalized.allow.includes(params.record.id)) return;
const declaredChannels = params.record.channels.map((channelId) => channelId.trim()).filter((channelId) => channelId.length > 0);
if (declaredChannels.length === 0) return;
const channelConfigs = params.record.channelConfigs ?? {};
const missingChannels = declaredChannels.filter((channelId) => !Object.hasOwn(channelConfigs, channelId));
if (missingChannels.length === 0) return;
const safeMissingChannels = missingChannels.map(sanitizeForLog);
params.diagnostics.push({
level: "warn",
pluginId: sanitizeForLog(params.record.id),
source: sanitizeForLog(params.record.manifestPath),
message: `channel plugin manifest declares ${safeMissingChannels.join(", ")} without channelConfigs metadata; add openclaw.plugin.json#channelConfigs so config schema and setup surfaces work before runtime loads. Channels without channelConfigs still appear in channel listings, but setup UI may be limited.`
});
}
function dedupePluginDiagnostics(diagnostics, discoveryDiagnostics) {
const seen = /* @__PURE__ */ new Set();
const deduped = [];
for (const diagnostic of diagnostics) {
const key = JSON.stringify([
diagnostic.level,
diagnostic.pluginId ?? "",
diagnostic.message,
diagnostic.level === "error" || discoveryDiagnostics.has(diagnostic) ? diagnostic.source ?? "" : ""
]);
if (seen.has(key)) continue;
seen.add(key);
deduped.push(diagnostic);
}
return deduped;
}
function isStaleForeignBundledPin(params) {
return isForeignBundledPluginRoot(params.candidate.rootDir, params.env) && !isBundledPluginInsideDevSourceRoot({
rootDir: params.candidate.rootDir,
env: params.env
});
}
function resolveDuplicatePrecedenceRank(params) {
if (params.candidate.origin === "config" || params.candidate.configSelected) return 0;
if (params.candidate.origin === "bundled" && isBundledPluginInsideDevSourceRoot({
rootDir: params.candidate.rootDir,
env: params.env
})) return 1;
if (params.candidate.origin === "global" && !isStaleForeignBundledPin({
candidate: params.candidate,
env: params.env
}) && matchesInstalledPluginRecord({
pluginId: params.pluginId,
candidate: params.candidate,
config: params.config,
env: params.env,
installRecords: params.installRecords
})) return 2;
if (params.candidate.origin === "bundled") return 3;
if (params.candidate.origin === "workspace") return 4;
return 5;
}
function isIntentionalInstalledBundledDuplicate(params) {
const leftIsInstalled = matchesInstalledPluginRecord({
pluginId: params.pluginId,
candidate: params.left,
config: params.config,
env: params.env,
installRecords: params.installRecords
});
const rightIsInstalled = matchesInstalledPluginRecord({
pluginId: params.pluginId,
candidate: params.right,
config: params.config,
env: params.env,
installRecords: params.installRecords
});
return leftIsInstalled && !isStaleForeignBundledPin({
candidate: params.left,
env: params.env
}) && params.right.origin === "bundled" && !isBundledPluginInsideDevSourceRoot({
rootDir: params.right.rootDir,
env: params.env
}) || rightIsInstalled && !isStaleForeignBundledPin({
candidate: params.right,
env: params.env
}) && params.left.origin === "bundled" && !isBundledPluginInsideDevSourceRoot({
rootDir: params.left.rootDir,
env: params.env
});
}
function isSameGlobalPackageDuplicate(left, right) {
if (left.origin !== "global" || right.origin !== "global") return false;
const leftPackageName = normalizeOptionalString(left.packageName);
const rightPackageName = normalizeOptionalString(right.packageName);
if (!leftPackageName || leftPackageName !== rightPackageName) return false;
const leftPackageVersion = normalizeOptionalString(left.packageVersion);
const rightPackageVersion = normalizeOptionalString(right.packageVersion);
return Boolean(leftPackageVersion && rightPackageVersion && leftPackageVersion === rightPackageVersion);
}
function buildPluginManifestRegistry(params) {
const config = params.config ?? {};
const normalized = normalizePluginsConfigWithResolverCore(config.plugins);
const env = params.env ?? process.env;
const registryPath = params.registryPath ?? resolveInstalledPluginIndexStorePath({ env });
const { getInstallRecords } = params;
const discovery = params.candidates ? {
candidates: params.candidates,
diagnostics: params.diagnostics ?? []
} : params.discovery ?? discoverOpenClawPlugins({
workspaceDir: params.workspaceDir,
extraPaths: normalized.loadPaths,
env,
installRecords: getInstallRecords()
});
const discovered = new Set(discovery.diagnostics);
const diagnostics = [...discovered];
const candidates = discovery.candidates.toSorted((left, right) => Number(right.origin === "config" || right.configSelected === true) - Number(left.origin === "config" || left.configSelected === true));
const seenIds = /* @__PURE__ */ new Map();
const manifestWarnings = /* @__PURE__ */ new Map();
const reportedExplicitOverrides = /* @__PURE__ */ new Set();
const currentHostVersion = resolveCompatibilityHostVersion(env);
const explicitConfiguredFileSources = new Set(normalized.loadPaths.map((loadPath) => resolveUserPath(loadPath, env)).filter((loadPath) => pluginCacheStatSync(loadPath)?.isFile() === true).map((loadPath) => path.resolve(loadPath)));
for (const candidate of candidates) {
const rejectHardlinks = shouldRejectHardlinkedPluginFiles({
origin: candidate.origin,
rootDir: candidate.rootDir,
env
});
const isBundleRecord = (candidate.format ?? "openclaw") === "bundle";
const isManifestlessConfiguredFile = candidate.origin === "config" && explicitConfiguredFileSources.has(path.resolve(candidate.source)) && !pluginCacheExistsSync(path.join(candidate.rootDir, "openclaw.plugin.json"));
if (isManifestlessConfiguredFile && isCoreReservedPluginId(candidate.idHint)) {
diagnostics.push({
level: "error",
pluginId: candidate.idHint,
source: candidate.source,
message: `plugin manifest id "${candidate.idHint}" is reserved by OpenClaw core`
});
continue;
}
const manifestRes = candidate.origin === "bundled" && candidate.bundledManifest && candidate.bundledManifestPath ? {
ok: true,
manifest: candidate.bundledManifest,
manifestPath: candidate.bundledManifestPath
} : isBundleRecord && candidate.bundleFormat ? loadBundleManifest({
rootDir: candidate.rootDir,
bundleFormat: candidate.bundleFormat,
rejectHardlinks
}) : isManifestlessConfiguredFile ? {
ok: true,
manifest: {
id: candidate.idHint,
configSchema: {
type: "object",
additionalProperties: false
},
activation: { onStartup: true }
},
manifestPath: candidate.source
} : loadPluginManifest(candidate.rootDir, rejectHardlinks);
if (!manifestRes.ok) {
diagnostics.push({
level: "error",
pluginId: candidate.diagnosticIdHint ?? candidate.idHint,
message: manifestRes.error,
source: manifestRes.manifestPath,
..."diagnosticCode" in manifestRes && manifestRes.diagnosticCode ? { code: manifestRes.diagnosticCode } : {}
});
continue;
}
if ("warnings" in manifestRes && manifestRes.warnings) manifestWarnings.set(manifestRes.manifestPath, manifestRes.warnings);
const manifest = manifestRes.manifest;
const effectivePluginId = candidate.effectivePluginId ?? manifest.id;
if (candidate.origin !== "bundled") {
const packageManifestSource = path.join(candidate.packageDir ?? candidate.rootDir, "package.json");
const allowLegacyBareMinHostVersion = candidate.origin === "global" && matchesInstalledPluginRecord({
pluginId: effectivePluginId,
candidate,
config,
env,
installRecords: getInstallRecords()
});
const minHostVersionCheck = checkMinHostVersion({
currentVersion: currentHostVersion,
minHostVersion: candidate.packageManifest?.install?.minHostVersion,
allowLegacyBareSemver: allowLegacyBareMinHostVersion
});
if (!minHostVersionCheck.ok) {
diagnostics.push({
level: minHostVersionCheck.kind === "invalid" ? "error" : "warn",
pluginId: effectivePluginId,
source: packageManifestSource,
message: minHostVersionCheck.kind === "invalid" ? `plugin manifest invalid | ${minHostVersionCheck.error}` : minHostVersionCheck.kind === "unknown_host_version" ? `plugin requires OpenClaw >=${minHostVersionCheck.requirement.minimumLabel}, but this host version could not be determined; skipping load` : `plugin requires OpenClaw >=${minHostVersionCheck.requirement.minimumLabel}, but this host is ${minHostVersionCheck.currentVersion}; skipping load`
});
continue;
}
const packagePluginApiRangeCheck = resolvePackagePluginApiRange(candidate.packageManifest);
if (!packagePluginApiRangeCheck.ok) {
diagnostics.push({
level: "error",
pluginId: effectivePluginId,
source: packageManifestSource,
message: `plugin manifest invalid | ${packagePluginApiRangeCheck.error}`
});
continue;
}
const packagePluginApiRange = packagePluginApiRangeCheck.range;
if (packagePluginApiRange && !satisfiesPluginApiRange(currentHostVersion, packagePluginApiRange)) {
diagnostics.push({
level: "warn",
configDisposition: "preserve",
pluginId: effectivePluginId,
source: packageManifestSource,
message: `plugin requires plugin API ${packagePluginApiRange}, but this host is ${currentHostVersion}; skipping load (check "openclaw --version", OPENCLAW_COMPATIBILITY_HOST_VERSION, or run "openclaw doctor")`
});
continue;
}
}
const configSchema = "configSchema" in manifest ? manifest.configSchema : void 0;
const schemaCacheKey = (() => {
if (!configSchema || isManifestlessConfiguredFile) return;
const file = readPluginCacheFile({
rootDir: candidate.rootDir,
relativePath: path.relative(candidate.rootDir, manifestRes.manifestPath),
rejectHardlinks,
maxBytes: 262144
});
return file.ok ? `${manifestRes.manifestPath}:${file.hash}` : manifestRes.manifestPath;
})();
const record = isBundleRecord ? buildBundleManifestRecord({
manifest,
candidate,
manifestPath: manifestRes.manifestPath,
rejectHardlinks
}) : buildPluginManifestRecord({
manifest,
candidate,
manifestPath: manifestRes.manifestPath,
diagnostics,
rejectHardlinks,
schemaCacheKey,
configSchema,
trust: resolvePluginTrust({
registryPath,
pluginId: effectivePluginId,
candidate,
env,
installRecords: getInstallRecords()
}),
...params.bundledChannelConfigCollector ? { bundledChannelConfigCollector: params.bundledChannelConfigCollector } : {}
});
if (candidate.sourcePreferred || candidate.origin === "bundled" && candidate.configSelected) record.sourcePreferred = true;
recordPluginManifestInstallOwner(record, resolvePluginCandidateInstallOwner(candidate), isPluginCandidateInstallOwnerAmbiguous(candidate));
const existing = seenIds.get(effectivePluginId);
if (existing) {
const samePath = existing.candidate.rootDir === candidate.rootDir;
if ((() => {
if (samePath) return true;
const existingReal = pluginCacheRealpathSync(existing.candidate.rootDir);
const candidateReal = pluginCacheRealpathSync(candidate.rootDir);
return Boolean(existingReal && candidateReal && existingReal === candidateReal);
})()) {
if (record.sourcePreferred || existing.record.sourcePreferred) {
record.sourcePreferred = true;
existing.record.sourcePreferred = true;
}
if (PLUGIN_ORIGIN_RANK[candidate.origin] < PLUGIN_ORIGIN_RANK[existing.candidate.origin]) seenIds.set(effectivePluginId, {
candidate,
record
});
continue;
}
const candidateRank = resolveDuplicatePrecedenceRank({
pluginId: effectivePluginId,
candidate,
config,
env,
installRecords: getInstallRecords()
});
const existingRank = resolveDuplicatePrecedenceRank({
pluginId: effectivePluginId,
candidate: existing.candidate,
config,
env,
installRecords: getInstallRecords()
});
const candidateWins = candidateRank < existingRank;
const winnerCandidate = candidateWins ? candidate : existing.candidate;
const overriddenCandidate = candidateWins ? existing.candidate : candidate;
if (candidateWins) seenIds.set(effectivePluginId, {
candidate,
record
});
if (isIntentionalInstalledBundledDuplicate({
pluginId: effectivePluginId,
left: candidate,
right: existing.candidate,
config,
env,
installRecords: getInstallRecords()
})) continue;
if (isSameGlobalPackageDuplicate(candidate, existing.candidate)) continue;
const staleForeignPin = winnerCandidate.origin === "bundled" && isStaleForeignBundledPin({
candidate: overriddenCandidate,
env
}) && matchesInstalledPluginRecord({
pluginId: effectivePluginId,
candidate: overriddenCandidate,
env,
installRecords: getInstallRecords()
});
const explicitOverride = !staleForeignPin && Math.min(candidateRank, existingRank) === 0 && candidateRank !== existingRank;
if (explicitOverride) {
if (reportedExplicitOverrides.has(effectivePluginId)) continue;
reportedExplicitOverrides.add(effectivePluginId);
}
diagnostics.push({
level: explicitOverride ? "info" : "warn",
...explicitOverride ? { code: "explicit-config-plugin-selection" } : {},
pluginId: effectivePluginId,
source: overriddenCandidate.source,
message: staleForeignPin ? `stale plugin install record: "${effectivePluginId}" is pinned to ${overriddenCandidate.rootDir}, which belongs to a different OpenClaw installation. This installation's bundled plugin is being used instead. No uninstall is needed to use the bundled plugin. Uninstalling, even with \`--keep-files\`, removes plugin configuration; re-enabling does not restore it.` : explicitOverride ? `duplicate plugin id resolved by explicit config-selected plugin; ${overriddenCandidate.origin} plugin will be overridden by config plugin (${winnerCandidate.source})` : `duplicate plugin id detected; ${overriddenCandidate.origin} plugin will be overridden by ${winnerCandidate.origin} plugin (${winnerCandidate.source})`
});
continue;
}
seenIds.set(effectivePluginId, {
candidate,
record
});
}
const plugins = rejectCaseFoldedIdCollisions([...seenIds.values()].map(({ record }) => record), diagnostics);
for (const record of plugins) {
for (const warning of manifestWarnings.get(record.manifestPath) ?? []) diagnostics.push({
level: "warn",
pluginId: sanitizeForLog(record.id),
source: sanitizeForLog(record.manifestPath),
message: sanitizeForLog(warning)
});
pushNonBundledChannelConfigDescriptorDiagnostic({
record,
diagnostics,
normalized
});
}
return {
plugins,
diagnostics: dedupePluginDiagnostics(diagnostics, discovered)
};
}
//#endregion
//#region src/plugins/manifest-registry.ts
function loadPluginManifestRegistryCore(params = {}) {
if (!params.candidates && !params.discovery && !params.installRecords) {
const gatewaySnapshot = getGatewayPluginMetadataSnapshot();
if (gatewaySnapshot) return gatewaySnapshot.manifestRegistry;
}
const env = params.env ?? process.env;
let installRecords = params.installRecords;
return buildPluginManifestRegistry({
...params,
env,
getInstallRecords: () => installRecords ??= loadInstalledPluginIndexInstallRecordsSync({ env })
});
}
//#endregion
//#region src/plugins/manifest-registry-installed.ts
function resolveInstalledManifestRegistryIndexFingerprint(index) {
const facts = getInstalledPluginIndexFacts(index);
const cached = facts?.fingerprint;
if (cached) return cached;
const fingerprint = hashStableJson({
version: index.version,
hostContractVersion: index.hostContractVersion,
compatRegistryVersion: index.compatRegistryVersion,
migrationVersion: index.migrationVersion,
policyHash: index.policyHash,
installRecords: index.installRecords,
diagnostics: index.diagnostics,
plugins: index.plugins.map(({ doctorContractFile: _doctorContractFile, sourceAdmissions: _sourceAdmissions, packageBuild, ...plugin }) => ({
...plugin,
...packageBuild?.bundledDist === void 0 ? {} : { packageBuild: { bundledDist: packageBuild.bundledDist } }
}))
});
if (facts) facts.fingerprint = fingerprint;
return fingerprint;
}
//#endregion
//#region src/plugins/plugin-control-plane-context.ts
function resolvePluginDiscoveryContext(params = {}) {
const paths = params.config?.plugins?.load?.paths;
return resolvePluginCacheInputs({
env: params.env ?? process.env,
workspaceDir: params.workspaceDir,
loadPaths: params.loadPaths ?? (Array.isArray(paths) ? paths : void 0)
});
}
/** Resolves a stable fingerprint for plugin control-plane activation state. */
function resolvePluginControlPlaneFingerprint(params = {}) {
const inventoryFingerprint = params.inventoryFingerprint ?? (params.index ? resolveInstalledManifestRegistryIndexFingerprint(params.index) : void 0);
return hashJson({
discovery: resolvePluginDiscoveryContext(params),
policyFingerprint: params.policyHash ?? resolveInstalledPluginIndexPolicyHash(params.config, params.env),
...inventoryFingerprint ? { inventoryFingerprint } : {},
...params.activationFingerprint ? { activationFingerprint: params.activationFingerprint } : {}
});
}
//#endregion
//#region src/plugins/plugin-metadata-env.ts
const PLUGIN_METADATA_ENV_KEYS = [
"ANDROID_DATA",
"APPDATA",
"HOME",
"OPENCLAW_BUNDLED_PLUGINS_DIR",
"OPENCLAW_COMPATIBILITY_HOST_VERSION",
"OPENCLAW_CONFIG_PATH",
"OPENCLAW_DEV_SOURCE_ROOT",
"OPENCLAW_DISABLE_BUNDLED_PLUGINS",
"OPENCLAW_DISABLE_BUNDLED_SOURCE_OVERLAYS",
"OPENCLAW_HOME",
"OPENCLAW_NIX_MODE",
"OPENCLAW_STATE_DIR",
"PREFIX",
"USERPROFILE",
"XDG_CONFIG_HOME"
];
/** Compares discovery namespaces without resolving or probing filesystem roots. */
function resolvePluginMetadataEnvFingerprint(env = process.env) {
return hashJson({
env: Object.fromEntries(PLUGIN_METADATA_ENV_KEYS.flatMap((key) => {
const value = env[key];
return value === void 0 ? [] : [[key, value]];
})),
installRoots: hasActivePluginInstallRoots() ? resolveActivePluginInstallRoots() : void 0,
trustBundledPluginsDirOverride: shouldTrustTestBundledPluginsDirOverride(env),
cwd: tryProcessCwd()
});
}
//#endregion
//#region src/plugins/plugin-scope.ts
/** Normalizes plugin id scope input into a sorted unique string list. */
function normalizePluginIdScope(ids) {
return ids === void 0 ? void 0 : normalizeSortedUniqueTrimmedStringList(ids);
}
/** Serializes plugin scope for cache keys. */
function serializePluginIdScope(ids) {
return ids === void 0 ? "__unscoped__" : JSON.stringify(ids);
}
//#endregion
//#region src/plugins/current-plugin-metadata-snapshot.ts
function resolveAgentWorkspaceFingerprint(config, env) {
return JSON.stringify(listAgentWorkspaceDirs(config, env));
}
function prepareCurrentPluginMetadataSnapshotPublication(snapshot, options, owner = "operation") {
const fingerprint = (config, policyHash) => resolvePluginControlPlaneFingerprint({
config,
env: options.env,
index: snapshot.index,
policyHash,
workspaceDir: options.workspaceDir ?? snapshot.workspaceDir
});
const compatiblePolicyHashes = options.compatibleConfigs?.map((config) => resolveInstalledPluginIndexPolicyHash(config, options.env));
const compatibleConfigFingerprints = options.compatibleConfigs?.map((config, index) => fingerprint(config, compatiblePolicyHashes?.[index]));
const configFingerprint = fingerprint(options.config, snapshot.policyHash);
const defaultDiscoveryConfigFingerprint = fingerprint({}, snapshot.policyHash);
const defaultDiscoveryCompatible = configFingerprint === defaultDiscoveryConfigFingerprint || snapshot.configFingerprint === defaultDiscoveryConfigFingerprint || Boolean(compatibleConfigFingerprints?.includes(defaultDiscoveryConfigFingerprint));
const envFingerprint = resolvePluginMetadataEnvFingerprint(options.env);
const agentWorkspaceFingerprint = owner === "gateway" && options.config ? resolveAgentWorkspaceFingerprint(options.config, options.env) : void 0;
const configIdentities = [...options.compatibleConfigs ?? []];
if (options.config) {
const policyHash = resolveInstalledPluginIndexPolicyHash(options.config, options.env);
if (policyHash === snapshot.policyHash || Boolean(compatiblePolicyHashes?.includes(policyHash))) configIdentities.push(options.config);
}
return () => {
if (getCurrentPluginMetadataSnapshotState().owner === "gateway" && owner !== "gateway") throw new Error("Gateway plugin metadata can only be replaced after shutdown");
currentPluginMetadataConfigIdentityCache.clear();
setCurrentPluginMetadataSnapshotState(snapshot, configFingerprint, compatiblePolicyHashes, compatibleConfigFingerprints, owner === "gateway" || defaultDiscoveryCompatible ? snapshot.owners.modelIdNormalizationPolicies : void 0, owner, envFingerprint, defaultDiscoveryCompatible, agentWorkspaceFingerprint);
for (const config of configIdentities) currentPluginMetadataConfigIdentityCache.add(config);
};
}
/** Publishes a prepared CLI snapshot without displacing a lifecycle owner. */
function adoptCurrentPluginMetadataSnapshotIfAbsent(snapshot, options = {}) {
if (getScopedPluginCache()?.kind === "operation" || getCurrentPluginMetadataSnapshotState().snapshot !== void 0) return;
prepareCurrentPluginMetadataSnapshotPublication(snapshot, options)();
}
/** Installation revokes operation facts even when it runs between metadata scopes. */
function revokeCurrentPluginMetadataSnapshotScopes() {
const caches = new Set(getScopedPluginCaches());
const runtimeCaches = /* @__PURE__ */ new Set();
for (let scoped = getPluginExecutionFrame()?.metadataScope; scoped; scoped = scoped.parent) if (scoped.immutableRuntimeGeneration) runtimeCaches.add(scoped.cache);
else caches.add(scoped.cache);
for (const cache of caches) if (cache.kind === "operation" && !runtimeCaches.has(cache)) invalidatePluginCacheMetadata(cache);
}
function isScopedSnapshotInCurrentCache(scoped) {
if (!scoped.immutableRuntimeGeneration && scoped.metadata !== scoped.cache.metadata) return false;
const cache = getScopedPluginCache();
return cache?.kind !== "operation" || scoped.cache === cache;
}
const NEEDS_PREPARED_POLICY = Symbol("needs-prepared-policy");
function resolveCompatiblePluginMetadataSnapshot(candidate, params, options = {}) {
const snapshot = candidate.snapshot;
if (!snapshot) return;
if (candidate.immutableRuntimeGeneration) return snapshot;
const env = params.env ?? process.env;
if (candidate.envFingerprint !== resolvePluginMetadataEnvFingerprint(env)) return;
const requestedPluginIds = normalizePluginIdScope(params.pluginIds ?? params.pluginIdScope?.resolve({ index: snapshot.index }));
const snapshotPluginIds = normalizePluginIdScope(snapshot.pluginIds);
if (requestedPluginIds !== void 0 && serializePluginIdScope(snapshotPluginIds) !== serializePluginIdScope(requestedPluginIds)) return;
if (snapshotPluginIds !== void 0 && requestedPluginIds === void 0 && params.allowScopedSnapshot !== true) return;
const requestedWorkspaceDir = params.workspaceDir ?? (params.allowWorkspaceScopedSnapshot === true || options.scopedOwnerContext === true ? snapshot.workspaceDir : void 0);
if (snapshot.workspaceDir !== void 0 && requestedWorkspaceDir === void 0) return;
if (requestedWorkspaceDir !== void 0 && (snapshot.workspaceDir ?? "") !== (requestedWorkspaceDir ?? "")) return;
const canReuseCachedConfig = Boolean(params.config && candidate.hasConfigIdentity?.(params.config));
if (canReuseCachedConfig && params.requireDefaultDiscoveryContext !== true) return snapshot;
if (params.config && !canReuseCachedConfig && params.allowSynchronousPolicyRead === false) return NEEDS_PREPARED_POLICY;
const requestedPolicyHash = params.config && !canReuseCachedConfig ? resolveInstalledPluginIndexPolicyHash(params.config, params.env) : void 0;
if (requestedPolicyHash && snapshot.policyHash !== requestedPolicyHash) {
if (!candidate.compatiblePolicyHashes?.includes(requestedPolicyHash)) return;
}
if (params.config && !canReuseCachedConfig) {
const requestedConfigFingerprint = resolvePluginControlPlaneFingerprint({
config: params.config,
env,
index: snapshot.index,
policyHash: requestedPolicyHash,
workspaceDir: requestedWorkspaceDir
});
if (!(candidate.configFingerprint === requestedConfigFingerprint || snapshot.configFingerprint === requestedConfigFingerprint || Boolean(candidate.compatibleConfigFingerprints?.includes(requestedConfigFingerprint)))) return;
}
if (params.requireDefaultDiscoveryContext === true && options.scopedOwnerContext !== true && candidate.defaultDiscoveryCompatible !== true) return;
return snapshot;
}
function getCurrentPluginMetadataSnapshot(params = {}) {
for (let scoped = getPluginExecutionFrame()?.metadataScope; scoped; scoped = scoped.parent) {
if (!isScopedSnapshotInCurrentCache(scoped)) continue;
const compatibleScoped = resolveCompatiblePluginMetadataSnapshot(scoped, params, { scopedOwnerContext: true });
if (compatibleScoped === NEEDS_PREPARED_POLICY) return;
if (compatibleScoped) return compatibleScoped;
}
const scopedCache = getScopedPluginCache();
if (scopedCache && scopedCache !== getProcessPluginCache()) return;
const state = getCurrentPluginMetadataSnapshotState();
const compatible = resolveCompatiblePluginMetadataSnapshot({
...state,
hasConfigIdentity: (config) => currentPluginMetadataConfigIdentityCache.has(config),
immutableRuntimeGeneration: state.owner === "gateway"
}, params);
return compatible === NEEDS_PREPARED_POLICY ? void 0 : compatible;
}
registerPluginMetadataSnapshotReaders({
adoptCurrentPluginMetadataSnapshotIfAbsent,
getCurrentPluginMetadataSnapshot
});
registerPluginMetadataProcessMemoLifecycleClear(revokeCurrentPluginMetadataSnapshotScopes, { owner: "operation" });
//#endregion
//#region src/utils/run-with-concurrency.ts
/** Runs async tasks with bounded concurrency while preserving result indexes. */
async function runTasksWithConcurrency(params) {
const { tasks, limit, onTaskError, throwOnError = false } = params;
const errorMode = params.errorMode ?? "continue";
if (tasks.length === 0) return {
results: [],
firstError: void 0,
hasError: false
};
const resolvedLimit = Number.isFinite(limit) ? Math.max(1, Math.min(Math.floor(limit), tasks.length)) : tasks.length;
const results = Array.from({ length: tasks.length });
let firstError = void 0;
let hasError = false;
return new Promise((resolve, reject) => {
const runOne = async (task, index) => {
if (errorMode === "stop" && hasError) return;
try {
results[index] = await task();
} catch (error) {
if (!hasError) {
firstError = error;
hasError = true;
}
let rejectionError = error;
try {
onTaskError?.(error, index);
} catch (callbackError) {
rejectionError = callbackError;
}
if (throwOnError) reject(rejectionError);
}
};
const mapper = AsyncLocalStorage.bind((task, index) => Promise.resolve().then(() => runOne(task, index)));
pMap(tasks.slice(), mapper, { concurrency: resolvedLimit }).then(() => resolve({
results,
firstError,
hasError
}), reject);
});
}
//#endregion
//#region src/secrets/exec-provider-path-validation.ts
/** Checks the same command-path trust boundary before validation, writes, and execution. */
async function assertSecureExecCommandPath(params) {
const commandPath = resolveUserPath(params.command);
if (!commandPath) throw new Error(`${params.label} must be an absolute path.`);
const stat = await safeStat(commandPath);
if (!stat.ok) throw new Error(`${params.label} is not readable: ${commandPath}`);
if (stat.isDir) throw new Error(`${params.label} must be a file: ${commandPath}`);
if (stat.isSymlink) throw new Error(`${params.label} must not be a symlink: ${commandPath}`);
if (params.trustedDirs && params.trustedDirs.length > 0) {
if (!params.trustedDirs.map((entry) => resolveUserPath(entry)).some((dir) => isPathInside(dir, commandPath))) throw new Error(`${params.label} is outside trustedDirs: ${commandPath}`);
}
const perms = await inspectPathPermissions(commandPath);
if (!perms.ok) throw new Error(`${params.label} permissions could not be verified: ${commandPath}`);
if (perms.worldWritable || perms.groupWritable) throw new Error(`${params.label} permissions are too open: ${commandPath}`);
if (process.platform === "win32" && perms.source === "unknown") throw new FsSafeError("permission-unverified", `${params.label} ACL verification unavailable on Windows for ${commandPath}. Move the command to a path whose ACLs OpenClaw can verify; there is no provider-level bypass.`);
if (process.platform !== "win32" && typeof process.getuid === "function" && stat.uid != null) {
const uid = process.getuid();
if (stat.uid !== uid) throw new Error(`${params.label} must be owned by the current user (uid=${uid}): ${commandPath}`);
}
return commandPath;
}
//#endregion
//#region src/secrets/json-pointer.ts
/** JSON Pointer token helpers for file-backed secret refs. */
function failOrUndefined(params) {
if (params.onMissing === "throw") throw new Error(params.message);
}
function decodeJsonPointerToken(token) {
return token.replace(/~1/g, "/").replace(/~0/g, "~");
}
/**
* Reads a value from a JSON-like document using an absolute JSON Pointer.
* Missing segments throw by default; `onMissing: "undefined"` is for optional probes.
*/
function readJsonPointer(root, pointer, options = {}) {
const onMissing = options.onMissing ?? "throw";
if (!pointer.startsWith("/")) return failOrUndefined({
onMissing,
message: "File-backed secret ids must be absolute JSON pointers (for example: \"/providers/openai/apiKey\")."
});
const tokens = pointer.slice(1).split("/").map((token) => decodeJsonPointerToken(token));
let current = root;
for (const token of tokens) {
if (Array.isArray(current)) {
const index = parseConfigPathArrayIndex(token);
if (index === void 0 || index >= current.length) return failOrUndefined({
onMissing,
message: `JSON pointer segment "${token}" is out of bounds.`
});
current = current[index];
continue;
}
if (!isRecord(current) || !Object.hasOwn(current, token)) return failOrUndefined({
onMissing,
message: `JSON pointer segment "${token}" does not exist.`
});
current = current[token];
}
return current;
}
//#endregion
//#region src/plugins/manifest-owner-policy.ts
/** Resolves whether a manifest owner plugin is effectively activated. */
function isActivatedManifestOwner(params) {
return resolveEffectivePluginActivationState({
id: params.plugin.id,
origin: params.plugin.origin,
channelIds: params.plugin.channels,
config: params.normalizedConfig,
rootConfig: params.rootConfig,
enabledByDefault: isPluginEnabledByDefaultForPlatform(params.plugin)
}).activated;
}
//#endregion
//#region src/secrets/provider-integrations.ts
/** Materializes trusted plugin secret-provider integrations into exec provider configs. */
const NODE_COMMAND_PLACEHOLDER = "${node}";
function resolvePluginRelativePath(value, pluginRoot) {
const resolved = path.resolve(pluginRoot, value);
return isPathInside$2(pluginRoot, resolved) ? resolved : void 0;
}
function resolveArg(arg, pluginRoot) {
if (!arg.startsWith("./") && !arg.startsWith("../")) return arg;
return resolvePluginRelativePath(arg, pluginRoot);
}
function isSecurePosixPathStat(stat) {
if (process.platform === "win32") return true;
if ((stat.mode & 18) !== 0) return false;
if (typeof process.getuid !== "function" || typeof stat.uid !== "number") return true;
const uid = process.getuid();
return stat.uid === uid || stat.uid === 0;
}
function resolveNodeEntrypointArg(params) {
const entrypoint = params.integration.args?.[0];
if (!entrypoint?.startsWith("./")) return;
const resolved = resolvePluginRelativePath(entrypoint, params.pluginRoot);
if (!resolved) return;
try {
const opened = openRootFileSync({
absolutePath: resolved,
rootPath: params.pluginRoot,
boundaryLabel: "plugin root",
rejectHardlinks: params.rejectHardlinks,
symlinks: process.platform === "win32" ? "follow-parents-within-root" : "reject"
});
if (!opened.ok) return;
try {
if (!isSecurePosixPathStat(opened.stat)) return;
if (process.platform !== "win32") for (let directory = path.dirname(opened.path);; directory = path.dirname(directory)) {
const stat = fsSync.lstatSync(directory);
if (!stat.isDirectory() || !isSecurePosixPathStat(stat)) return;
if (directory === opened.rootRealPath) break;
}
return opened.path;
} finally {
fsSync.closeSync(opened.fd);
}
} catch {
return;
}
}
function materializeExecProviderConfig(integration, record, env) {
const pluginRoot = record.rootDir;
if (integration.command !== NODE_COMMAND_PLACEHOLDER) return;
const nodeEntrypoint = resolveNodeEntrypointArg({
integration,
pluginRoot,
rejectHardlinks: shouldRejectHardlinkedPluginFiles({
origin: record.origin,
rootDir: pluginRoot,
env
})
});
if (!nodeEntrypoint) return;
const args = integration.args?.map((arg, index) => index === 0 ? nodeEntrypoint : resolveArg(arg, pluginRoot)).filter((arg) => arg !== void 0);
if (integration.args && args?.length !== integration.args.length) return;
return {
source: "exec",
command: process.execPath,
...args ? { args: [...resolveRuntimeArgs(), ...args] } : {},
...integration.timeoutMs !== void 0 ? { timeoutMs: integration.timeoutMs } : {},
...integration.noOutputTimeoutMs !== void 0 ? { noOutputTimeoutMs: integration.noOutputTimeoutMs } : {},
...integration.maxOutputBytes !== void 0 ? { maxOutputBytes: integration.maxOutputBytes } : {},
...integration.jsonOnly === false ? { jsonOnly: false } : {},
...integration.env ? { env: integration.env } : {},
...integration.passEnv ? { passEnv: integration.passEnv } : {},
trustedDirs: [.../* @__PURE__ */ new Set([path.dirname(process.execPath), pluginRoot])]
};
}
function canExposeSecretProviderIntegrations(params) {
if (params.record.origin !== "bundled" && params.record.origin !== "global") return false;
return isActivatedManifestOwner({
plugin: params.record,
normalizedConfig: params.normalizedConfig,
rootConfig: params.config
});
}
/** Narrows a secret provider config to the plugin-integration exec shape. */
function isPluginIntegrationSecretProviderConfig(value) {
return typeof value === "object" && value !== null && "source" in value && value.source === "exec" && "pluginIntegration" in value && typeof value.pluginIntegration === "object" && value.pluginIntegration !== null && "pluginId" in value.pluginIntegration && typeof value.pluginIntegration.pluginId === "string" && value.pluginIntegration.pluginId.trim().length > 0 && "integrationId" in value.pluginIntegration && typeof value.pluginIntegration.integrationId === "string" && value.pluginIntegration.integrationId.trim().length > 0;
}
/** Materializes an active trusted plugin secret-provider integration into an exec provider. */
function resolveSecretProviderIntegrationConfig(params) {
const config = params.config ?? {};
const normalizedConfig = normalizePluginsConfig(config.plugins);
const env = params.env ?? process.env;
const { pluginId, integrationId } = params.providerConfig.pluginIntegration;
if (!isValidSecretProviderAlias(params.providerAlias)) return {
ok: false,
reason: `provider alias "${params.providerAlias}" is invalid`
};
const record = params.manifestRegistry.plugins.find((candidate) => candidate.id === pluginId);
if (!record) return {
ok: false,
reason: `plugin "${pluginId}" is not installed`
};
if (!canExposeSecretProviderIntegrations({
record,
normalizedConfig,
config
})) return {
ok: false,
reason: `plugin "${pluginId}" is not active or is not from a trusted install origin`
};
const integration = record.secretProviderIntegrations?.[integrationId];
if (!integration) return {
ok: false,
reason: `plugin "${record.id}" does not declare secret provider integration "${integrationId}"`
};
const materialized = materializeExecProviderConfig(integration, record, env);
if (!materialized) return {
ok: false,
reason: `plugin "${record.id}" integration "${integrationId}" could not be materialized`
};
return {
ok: true,
providerConfig: materialized
};
}
//#endregion
//#region src/secrets/sentinel.ts
const SECRET_SENTINEL_SOURCE = "oc-sent-v2\\.[A-Za-z0-9_-]+\\.end";
11 + Math.ceil(262288 / 3) + 4;
new RegExp(SECRET_SENTINEL_SOURCE, "g");
const secretSentinelKeys = resolveGlobalSingleton(Symbol.for("openclaw.secretSentinel.keys"), () => ({ keys: randomBytes(64) })).keys;
secretSentinelKeys.subarray(0, 32);
secretSentinelKeys.subarray(32);
//#endregion
//#region src/secrets/store/secret-store-validation.ts
function assertSecretStoreEnvName(name) {
if (!ENV_SECRET_REF_ID_RE.test(name)) throw new SecretStoreValidationError("SECRET_STORE_INVALID_NAME", `Secret store name must match ${String(ENV_SECRET_REF_ID_RE)}.`);
}
createSubsystemLogger("secrets/store");
async function readSecretStoreValue(params) {
try {
const name = params.name;
assertSecretStoreEnvName(name);
const context = params.context ?? captureOpenClawStateReadWorkerContext(params.database);
params.assertCurrent?.();
const reply = await executeExistingOpenClawStateRead({
path: context.admission.databasePath,
env: context.environment
}, {
type: "secrets.value",
input: { name }
}, {
context,
preferIndependentWarmRead: true
});
if (reply && (!reply.ok || reply.type !== "secrets.value")) throw new Error("Unexpected secret store value result.");
const row = reply?.row;
if (row?.kind === "secret") registerSecretValueForRedaction(row.value);
context.admission.assertCurrent();
params.assertCurrent?.();
if (!row) return err({
code: "SECRET_STORE_NOT_FOUND",
message: `Secret store entry "${name}" was not found.`
});
return ok(row.value);
} catch (error) {
if (error instanceof SecretStoreValidationError) return err({
code: "SECRET_STORE_INVALID_NAME",
message: error.message
});
return err({
code: "SECRET_STORE_UNAVAILABLE",
message: "Secret store database is unavailable.",
cause: error
});
}
}
//#endregion
//#region src/secrets/resolve-store.ts
const STORE_SECRET_REF_BATCH_MAX_BYTES = 512 * SECRET_STORE_VALUE_MAX_BYTES;
async function resolveStoreRefs(params) {
const context = captureOpenClawStateReadWorkerContext(params.database);
const resolved = /* @__PURE__ */ new Map();
let resolvedBytes = 0;
for (const ref of params.refs) {
const result = await readSecretStoreValue({
scope: { kind: "team" },
name: ref.id,
context
});
context.admission.assertCurrent();
if (!result.ok) {
if (result.error.code === "SECRET_STORE_NOT_FOUND" || result.error.code === "SECRET_STORE_INVALID_NAME") {
params.onRefError(refResolutionError({
code: result.error.code === "SECRET_STORE_NOT_FOUND" ? "SECRET_REF_NOT_FOUND" : "SECRET_REF_INVALID",
source: "store",
provider: params.providerName,
refId: ref.id,
message: result.error.message
}));
continue;
}
throw providerResolutionError({
code: "SECRET_PROVIDER_UNAVAILABLE",
source: "store",
provider: params.providerName,
message: result.error.message,
cause: result.error.cause
});
}
resolvedBytes += Buffer.byteLength(result.value, "utf8");
if (resolvedBytes > STORE_SECRET_REF_BATCH_MAX_BYTES) throw providerResolutionError({
code: "SECRET_PROVIDER_INVALID",
source: "store",
provider: params.providerName,
message: `Store provider "${params.providerName}" exceeded its ${STORE_SECRET_REF_BATCH_MAX_BYTES}-byte batch limit.`
});
resolved.set(ref.id, result.value);
}
return resolved;
}
//#endregion
//#region src/secrets/shared.ts
/** Shared parsing helpers for secrets migration/runtime code. */
/**
* Narrows to strings that contain non-whitespace content.
*/
function isNonEmptyString(value) {
return typeof value === "string" && value.trim().length > 0;
}
/**
* Normalizes numeric config to a positive integer, falling back when the input is not finite.
*/
function normalizePositiveInt(value, fallback) {
if (typeof value === "number" && Number.isFinite(value)) return Math.max(1, Math.floor(value));
return Math.max(1, Math.floor(fallback));
}
/**
* Normalizes timer values with the shared timeout coercion rules used by secret providers.
*/
function normalizePositiveTimerMs(value, fallback) {
return resolvePositiveTimerTimeoutMs(value, fallback);
}
//#endregion
//#region src/secrets/resolve.ts
/** Resolves SecretRef values from env, file, exec, and store secret providers. */
const DEFAULT_PROVIDER_CONCURRENCY = 4;
const DEFAULT_MAX_REFS_PER_PROVIDER = 512;
const DEFAULT_MAX_BATCH_BYTES = 262144;
const DEFAULT_FILE_MAX_BYTES = 1048576;
const DEFAULT_FILE_TIMEOUT_MS = 5e3;
const DEFAULT_EXEC_TIMEOUT_MS = 5e3;
const DEFAULT_EXEC_MAX_OUTPUT_BYTES = 1048576;
const SAFE_EXEC_ERROR_CODES = /* @__PURE__ */ new Set(["AMBIGUOUS_DUPLICATE_KEY", "NOT_FOUND"]);
function throwUnknownProviderResolutionError(params) {
if (isSecretResolutionError(params.err)) throw params.err;
throw providerResolutionError({
...process.platform === "win32" && (params.source === "file" || params.source === "exec") && params.err instanceof FsSafeError && params.err.code === "permission-unverified" ? { code: "SECRET_PROVIDER_PATH_SECURITY_UNVERIFIABLE" } : {},
source: params.source,
provider: params.provider,
message: formatErrorMessage(params.err),
cause: params.err
});
}
function resolveConfiguredProvider(params) {
const { ref, config } = params;
const providerConfig = config.secrets?.providers?.[ref.provider];
if (isBuiltInDefaultSecretProviderRef(config, ref)) {
if (ref.source === "env") return { source: "env" };
if (ref.source === "store") return { source: "store" };
}
if (!providerConfig) throw providerResolutionError({
code: "SECRET_PROVIDER_NOT_CONFIGURED",
source: ref.source,
provider: ref.provider,
message: `Secret provider "${ref.provider}" is not configured (ref: ${ref.source}:${ref.provider}:${ref.id}).`
});
const configuredSource = resolveSecretRefProviderSourceMismatch(config, ref);
if (configuredSource) throw providerResolutionError({
code: "SECRET_PROVIDER_INVALID",
source: ref.source,
provider: ref.provider,
message: `Secret provider "${ref.provider}" has source "${configuredSource}" but ref requests "${ref.source}".`
});
if (isPluginIntegrationSecretProviderConfig(providerConfig)) {
const resolved = resolveSecretProviderIntegrationConfig({
manifestRegistry: params.manifestRegistry ?? getCurrentPluginMetadataSnapshot({
config,
env: params.env,
allowWorkspaceScopedSnapshot: true
})?.manifestRegistry ?? loadPluginManifestRegistryCore({
config,
env: params.env
}),
providerAlias: ref.provider,
providerConfig,
config,
env: params.env
});
if (!resolved.ok) throw providerResolutionError({
source: ref.source,
provider: ref.provider,
message: `Secret provider "${ref.provider}" plugin integration is unavailable: ${resolved.reason}.`
});
return resolved.providerConfig;
}
return providerConfig;
}
async function readFileProviderPayload(params) {
const cacheKey = params.providerName;
const cache = params.cache;
const read = async () => {
const filePath = resolveUserPath(params.providerConfig.path);
const timeoutMs = normalizePositiveTimerMs(params.providerConfig.timeoutMs, DEFAULT_FILE_TIMEOUT_MS);
const maxBytes = normalizePositiveInt(params.providerConfig.maxBytes, DEFAULT_FILE_MAX_BYTES);
try {
const { buffer: payload } = await readSecureFile({
filePath,
label: `secrets.providers.${params.providerName}.path`,
io: {
maxBytes,
timeoutMs
},
permissions: { allowInsecure: false }
});
const text = payload.toString("utf8").replace(/^\uFEFF/, "");
if (params.providerConfig.mode === "singleValue") return text.replace(/\r?\n$/, "");
const parsed = JSON.parse(text);
if (!isRecord(parsed)) throw new Error(`File provider "${params.providerName}" payload is not a JSON object.`);
return parsed;
} catch (error) {
if (error instanceof FsSafeError && error.code === "timeout") throw new Error(`File provider "${params.providerName}" timed out after ${timeoutMs}ms.`, { cause: error });
throw error;
}
};
if (!cache) return await read();
cache.filePayloadByProvider ??= /* @__PURE__ */ new Map();
return await getOrCreatePromise(cache.filePayloadByProvider, cacheKey, read);
}
async function resolveEnvRefs(params) {
const resolved = /* @__PURE__ */ new Map();
const allowlist = params.providerConfig.allowlist ? new Set(params.providerConfig.allowlist) : null;
for (const ref of params.refs) {
if (allowlist && !allowlist.has(ref.id)) {
params.onRefError(refResolutionError({
code: "SECRET_REF_POLICY_DENIED",
source: "env",
provider: params.providerName,
refId: ref.id,
message: `Environment variable "${ref.id}" is not allowlisted in secrets.providers.${params.providerName}.allowlist.`
}));
continue;
}
const envValue = params.env[ref.id];
if (!isNonEmptyString(envValue)) {
params.onRefError(refResolutionError({
code: "SECRET_REF_NOT_FOUND",
source: "env",
provider: params.providerName,
refId: ref.id,
message: `Environment variable "${ref.id}" is missing or empty.`
}));
continue;
}
resolved.set(ref.id, envValue);
}
return resolved;
}
async function resolveFileRefs(params) {
const payload = await readFileProviderPayload({
providerName: params.providerName,
providerConfig: params.providerConfig,
cache: params.cache
});
const mode = params.providerConfig.mode ?? "json";
const resolved = /* @__PURE__ */ new Map();
if (mode === "singleValue") {
for (const ref of params.refs) {
if (ref.id !== "value") {
params.onRefError(refResolutionError({
code: "SECRET_REF_INVALID",
source: "file",
provider: params.providerName,
refId: ref.id,
message: `singleValue file provider "${params.providerName}" expects ref id "${SINGLE_VALUE_FILE_REF_ID}".`
}));
continue;
}
resolved.set(ref.id, payload);
}
return resolved;
}
for (const ref of params.refs) try {
resolved.set(ref.id, readJsonPointer(payload, ref.id, { onMissing: "throw" }));
} catch (err) {
params.onRefError(refResolutionError({
code: "SECRET_REF_NOT_FOUND",
source: "file",
provider: params.providerName,
refId: ref.id,
message: formatErrorMessage(err),
cause: err
}));
}
return resolved;
}
function parseExecValues(params) {
const trimmed = params.stdout.trim();
if (!trimmed) throw providerResolutionError({
source: "exec",
provider: params.providerName,
message: `Exec provider "${params.providerName}" returned empty stdout.`
});
let parsed;
try {
parsed = JSON.parse(trimmed);
} catch {
if (!params.jsonOnly && params.ids.length === 1) return { [expectDefined(params.ids[0], "ids entry at 0")]: trimmed };
throw providerResolutionError({
source: "exec",
provider: params.providerName,
message: `Exec provider "${params.providerName}" returned invalid JSON.`
});
}
if (!isRecord(parsed)) {
if (!params.jsonOnly && params.ids.length === 1 && typeof parsed === "string") return { [expectDefined(params.ids[0], "ids entry at 0")]: parsed };
throw providerResolutionError({
source: "exec",
provider: params.providerName,
message: `Exec provider "${params.providerName}" response must be an object.`
});
}
if (parsed.protocolVersion !== 1) throw providerResolutionError({
source: "exec",
provider: params.providerName,
message: `Exec provider "${params.providerName}" protocolVersion must be 1.`
});
const responseValues = parsed.values;
if (!isRecord(responseValues)) throw providerResolutionError({
source: "exec",
provider: params.providerName,
message: `Exec provider "${params.providerName}" response missing "values".`
});
const responseErrors = isRecord(parsed.errors) ? parsed.errors : null;
const out = {};
for (const id of params.ids) {
if (responseErrors && Object.hasOwn(responseErrors, id)) {
const entry = responseErrors[id];
const code = isRecord(entry) && typeof entry.code === "string" ? entry.code : null;
const safeCode = code && SAFE_EXEC_ERROR_CODES.has(code) ? code : null;
params.onRefError(refResolutionError({
code: safeCode === "NOT_FOUND" ? "SECRET_REF_NOT_FOUND" : "SECRET_REF_PROVIDER_ERROR",
source: "exec",
provider: params.providerName,
refId: id,
message: `Exec provider "${params.providerName}" failed for id "${id}"${safeCode ? ` (${safeCode})` : ""}.`
}));
continue;
}
if (!Object.hasOwn(responseValues, id)) {
params.onRefError(refResolutionError({
code: "SECRET_REF_NOT_FOUND",
source: "exec",
provider: params.providerName,
refId: id,
message: `Exec provider "${params.providerName}" response missing id "${id}".`
}));
continue;
}
out[id] = responseValues[id];
}
return out;
}
async function resolveExecRefs(params) {
const ids = uniqueStrings(params.refs.map((ref) => ref.id));
const secureCommandPath = await assertSecureExecCommandPath({
command: params.providerConfig.command,
label: `secrets.providers.${params.providerName}.command`,
trustedDirs: params.providerConfig.trustedDirs
});
const input = JSON.stringify({
protocolVersion: 1,
provider: params.providerName,
ids
});
if (Buffer.byteLength(input, "utf8") > DEFAULT_MAX_BATCH_BYTES) throw providerResolutionError({
code: "SECRET_PROVIDER_INVALID",
source: "exec",
provider: params.providerName,
message: `Exec provider "${params.providerName}" request exceeded maxBatchBytes (${DEFAULT_MAX_BATCH_BYTES}).`
});
const childEnv = {};
for (const key of params.providerConfig.passEnv ?? []) {
const value = params.env[key];
if (value !== void 0) childEnv[key] = value;
}
for (const [key, value] of Object.entries(params.providerConfig.env ?? {})) childEnv[key] = value;
const timeoutMs = normalizePositiveTimerMs(params.providerConfig.timeoutMs, DEFAULT_EXEC_TIMEOUT_MS);
const noOutputTimeoutMs = normalizePositiveTimerMs(params.providerConfig.noOutputTimeoutMs, timeoutMs);
const maxOutputBytes = normalizePositiveInt(params.providerConfig.maxOutputBytes, DEFAULT_EXEC_MAX_OUTPUT_BYTES);
const jsonOnly = params.providerConfig.jsonOnly ?? true;
const result = await runCommandWithTimeout([secureCommandPath, ...params.providerConfig.args ?? []], {
baseEnv: {},
cwd: path.dirname(secureCommandPath),
env: childEnv,
input,
killProcessTree: true,
maxCombinedOutputBytes: maxOutputBytes,
maxOutputBytes,
noOutputTimeoutMs,
outputCapture: "head",
terminateOnOutputLimit: true,
timeoutMs
});
if (result.termination === "timeout") throw providerResolutionError({
source: "exec",
provider: params.providerName,
message: `Exec provider "${params.providerName}" timed out after ${timeoutMs}ms.`
});
if (result.termination === "no-output-timeout") throw providerResolutionError({
source: "exec",
provider: params.providerName,
message: `Exec provider "${params.providerName}" produced no output for ${noOutputTimeoutMs}ms.`
});
if (result.outputLimitExceeded) throw providerResolutionError({
source: "exec",
provider: params.providerName,
message: `Exec provider output exceeded maxOutputBytes (${maxOutputBytes}).`
});
if (result.code !== 0) throw providerResolutionError({
source: "exec",
provider: params.providerName,
message: `Exec provider "${params.providerName}" exited with code ${String(result.code)}.`
});
const values = parseExecValues({
providerName: params.providerName,
ids,
stdout: result.stdout,
jsonOnly,
onRefError: params.onRefError
});
return new Map(Object.entries(values));
}
async function resolveProviderRefs(params) {
try {
if (params.providerConfig.source === "env") return await resolveEnvRefs({
refs: params.refs,
providerName: params.providerName,
onRefError: params.onRefError,
providerConfig: params.providerConfig,
env: params.options.env ?? process.env
});
if (params.providerConfig.source === "file") return await resolveFileRefs({
refs: params.refs,
providerName: params.providerName,
onRefError: params.onRefError,
providerConfig: params.providerConfig,
cache: params.options.cache
});
if (params.providerConfig.source === "store") return await resolveStoreRefs({
refs: params.refs,
providerName: params.providerName,
onRefError: params.onRefError,
database: { env: params.options.env ?? process.env }
});
if (params.providerConfig.source === "exec") {
if (isPluginIntegrationSecretProviderConfig(params.providerConfig)) throw providerResolutionError({
source: params.source,
provider: params.providerName,
message: `Secret provider "${params.providerName}" plugin integration was not materialized before exec resolution.`
});
return await resolveExecRefs({
refs: params.refs,
providerName: params.providerName,
onRefError: params.onRefError,
providerConfig: params.providerConfig,
env: params.options.env ?? process.env
});
}
throw providerResolutionError({
source: params.source,
provider: params.providerName,
message: `Unsupported secret provider source "${String(params.providerConfig.source)}".`
});
} catch (err) {
return throwUnknownProviderResolutionError({
source: params.source,
provider: params.providerName,
err
});
}
}
function createProviderResolutionTasks(params) {
return params.groups.map((group) => async () => {
const refFailures = /* @__PURE__ */ new Map();
const onRefError = (error) => {
if (params.errorMode === "stop") throw error;
refFailures.set(error.refId, error);
};
if (group.refs.length > DEFAULT_MAX_REFS_PER_PROVIDER) throw providerResolutionError({
code: "SECRET_PROVIDER_INVALID",
source: group.source,
provider: group.providerName,
message: `Secret provider "${group.providerName}" exceeded maxRefsPerProvider (${DEFAULT_MAX_REFS_PER_PROVIDER}).`
});
const providerConfig = resolveConfiguredProvider({
ref: expectDefined(group.refs[0], "refs entry at 0"),
config: params.options.config,
env: params.options.env ?? process.env,
manifestRegistry: params.options.manifestRegistry
});
const values = await resolveProviderRefs({
refs: group.refs,
source: group.source,
providerName: group.providerName,
providerConfig,
options: params.options,
onRefError
});
for (const ref of group.refs) {
if (refFailures.has(ref.id)) continue;
if (!values.has(ref.id)) {
onRefError(refResolutionError({
code: "SECRET_REF_PROVIDER_CONTRACT",
source: group.source,
provider: group.providerName,
refId: ref.id,
message: `Secret provider "${group.providerName}" did not return id "${ref.id}".`
}));
continue;
}
if (isRedactedSecretValue(values.get(ref.id))) onRefError(refResolutionError({
code: "SECRET_REF_REDACTED_VALUE",
source: group.source,
provider: group.providerName,
refId: ref.id,
message: `Secret reference "${group.source}:${group.providerName}:${ref.id}" resolves to a redaction placeholder. Run openclaw doctor --fix to repair a store-backed Gateway token; supply a real credential for other secrets.`
}));
}
return {
group,
values,
refFailures
};
});
}
async function resolveSecretRefProviderGroups(params) {
const groups = normalizeAndGroupSecretRefs(params.refs);
const errorsByIndex = /* @__PURE__ */ new Map();
const taskResults = await runTasksWithConcurrency({
tasks: createProviderResolutionTasks({
groups,
options: params.options,
errorMode: params.errorMode
}),
limit: DEFAULT_PROVIDER_CONCURRENCY,
errorMode: params.errorMode,
onTaskError: (error, index) => {
errorsByIndex.set(index, error);
}
});
const resolved = /* @__PURE__ */ new Map();
const failures = [];
for (const result of taskResults.results) {
if (!result) continue;
for (const ref of result.group.refs) {
const error = result.refFailures.get(ref.id);
if (error) failures.push({
group: result.group,
error
});
else resolved.set(secretRefKey(ref), result.values.get(ref.id));
}
}
for (const [index, group] of groups.entries()) if (errorsByIndex.has(index)) failures.push({
group,
error: errorsByIndex.get(index)
});
return {
resolved,
failures,
hasError: taskResults.hasError,
firstError: taskResults.firstError
};
}
/** Resolves a batch of SecretRefs, grouped by provider for bounded provider concurrency. */
async function resolveSecretRefValues(refs, options) {
const result = await resolveSecretRefProviderGroups({
refs,
options,
errorMode: "stop"
});
if (result.hasError) throw result.firstError;
return result.resolved;
}
/** Resolves one SecretRef, using the optional shared runtime cache. */
async function resolveSecretRefValue(ref, options) {
const cache = options.cache;
const key = secretRefKey(ref);
const resolve = async () => {
const resolved = await resolveSecretRefValues([ref], options);
if (!resolved.has(key)) throw refResolutionError({
code: "SECRET_REF_PROVIDER_CONTRACT",
source: ref.source,
provider: ref.provider,
refId: ref.id,
message: `Secret reference "${key}" resolved to no value.`
});
return resolved.get(key);
};
if (!cache) return await resolve();
cache.resolvedByRefKey ??= /* @__PURE__ */ new Map();
return await getOrCreatePromise(cache.resolvedByRefKey, key, resolve);
}
/** Resolves one SecretRef and requires a non-empty string result. */
async function resolveSecretRefString(ref, options) {
const resolved = await resolveSecretRefValue(ref, options);
if (!isNonEmptyString(resolved)) throw new Error(`Secret reference "${ref.source}:${ref.provider}:${ref.id}" resolved to a non-string or empty value.`);
return resolved;
}
//#endregion
export { describeSecretResolutionOperatorRecovery as a, describeSecretResolutionOperatorDiagnostic as i, resolveSecretRefValue as n, isSecretResolutionError as o, resolveSecretRefValues as r, resolveSecretRefString as t };