openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
9,519 lines • 424 kB
JavaScript
import { s as resolveOpenClawStateSqlitePath } from "./openclaw-state-db.paths-BENDWC2V.mjs";
import { c as extractErrorCode, i as isBlockedObjectKey$1, r as isMissingPathError, t as hasErrnoCode } from "./errno-jiy5vtf-.mjs";
import { D as asOptionalRecord, E as asOptionalObjectRecord, _ as normalizeLowercaseStringOrEmpty, a as asPositiveFiniteNumber, b as normalizeOptionalString, g as hasNonEmptyString, i as asNonNegativeFiniteNumber, k as isRecord, m as resolvePositiveTimerTimeoutMs, r as asFiniteNumber, s as asSafeIntegerInRange, w as resolveGlobalSingleton, y as normalizeOptionalLowercaseString } from "./number-coercion-PJEgwXYS.mjs";
import { A as getPluginCacheRetirementSignal, B as materializeErrorStack, D as PluginCacheFactInvalidatedError, E as resolveOpenClawPackageRootSync, F as invalidatePluginCacheMetadata, L as getPluginExecutionFrame, M as getProcessPluginCache, N as getScopedPluginCache, O as bindPluginCacheRoot, P as getScopedPluginCaches, T as resolveCompatibilityHostVersion, Y as isSqliteLockError, j as getPluginCacheRoot, k as getPluginCache } from "./sqlite-worker-identity-CfwbX7eo.mjs";
import { a as normalizeWindowsPathForComparison$1, f as resolveUserPath, i as isPathInside$2, o as normalizeWindowsPathPreservingCase, p as tryProcessCwd, r as isNotFoundPathError, s as resolveConfigDir } from "./utils-Bv29YgC5.mjs";
import { A as resolvePathViaExistingAncestorSync$1, E as parseJsonWithJson5Fallback, F as readFileDescriptorBoundedSync$1, G as isBuiltInDefaultSecretProviderRef, J as resolveSecretRefProviderSourceMismatch, K as isValidSecretProviderAlias, M as matchRootFileOpenFailure, N as openRootFileSync, Q as parseConfigPathArrayIndex, U as ENV_SECRET_REF_ID_RE, W as SINGLE_VALUE_FILE_REF_ID, Y as secretRefKey, _ as resolveIsNixMode, j as resolveRootPathSync, l as registerSecretValueForRedaction, nt as expectDefined, q as normalizeAndGroupSecretRefs, r as redactSensitiveText, v as resolveStateDir } from "./redact-DO6ydQMZ.mjs";
import { $ as string, D as SQLITE_STAGING_TOKEN_FILES, G as array, J as literal, K as boolean, M as isArtifactPreservingStateRead, O as acquireSqliteStagingToken, Q as strictObject, X as number, Z as object, _t as throwSqliteLifecycleErrors, a as SecretStoreValidationError, et as union, i as SECRET_STORE_VALUE_MAX_BYTES, it as isStateDatabaseReadAdmissionInvalidatedError } from "./sqlite-snapshot-staging-owner-fZYx7l2t.mjs";
import { a as executeExistingOpenClawStateRead, c as withExistingOpenClawStateDatabaseReadOnly, i as listAgentIds, o as getActiveOpenClawStateDatabaseReadSnapshot, r as resolveAgentWorkspaceDir, s as withExistingOpenClawStateDatabaseArtifactPreservingReadOnly, u as isDeeplyFrozenPlainData, y as safeParseJson } from "./openclaw-state-db-BxR_sRr1.mjs";
import { a as normalizeSortedUniqueTrimmedStringList, c as normalizeUniqueSingleOrTrimmedStringList, d as sortUniqueStrings, f as uniqueStrings, i as normalizeOptionalTrimmedStringList, n as filterStringEntries, o as normalizeStringEntries, r as normalizeArrayBackedTrimmedStringList, s as normalizeTrimmedStringList, u as normalizeUniqueTrimmedStringList } from "./string-normalization-B48UjRev.mjs";
import { o as isTruthyEnvValue } from "./host-env-security-B3DKP9_D.mjs";
import { n as formatErrorMessageWithCode, t as formatErrorMessage } from "./errors-Qhjprk1B.mjs";
import { F as getNodeSqliteKysely, N as executeSqliteQuerySync, P as executeSqliteQueryTakeFirstSync } from "./openclaw-state-db-schema-migration-required-zCjR9CdD.mjs";
import { n as compareValidSemver } from "./semver-aYpwYdrQ.mjs";
import { o as tableExists } from "./openclaw-state-db-schema-version-B3Gm77Zt.mjs";
import { r as sha256Hex } from "./crypto-digest-DqNCiRzt.mjs";
import { ut as decodeMountInfoPath, xt as compareOpenClawVersions } from "./openclaw-state-db-cache-DaRY_jqO.mjs";
import { i as readSecureFile, n as ok, r as FsSafeError, t as err } from "./result-CY2h4LBI.mjs";
import { _ as MANIFEST_KEY, h as resolveOfficialExternalPluginId, o as getOfficialExternalPluginCatalogManifest } from "./official-external-plugin-catalog-source-CQi5Z-JY.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-Pv5I5Tx3.mjs";
import { t as createSubsystemLogger } from "./subsystem-Caj1-NVj.mjs";
import { r as sanitizeForLog } from "./signal-exit-barrier-CXSq-V21.mjs";
import { t as resolveRuntimeArgs } from "./runtime-worker-url-BbAz91HL.mjs";
import { o as getOrCreatePromise } from "./worker-task-pool-CDWdBchP.mjs";
import { t as runCommandWithTimeout } from "./exec-DUccqZRm.mjs";
import { t as captureOpenClawStateReadWorkerContext } from "./openclaw-state-worker-context-BrKLb-Lm.mjs";
import "./openclaw-state-worker-store-HM_02sPT.mjs";
import "./temp-artifact-cleanup-BBjQL7fB.mjs";
import { i as resolvePluginSourceCapturesDirectory, n as isLegacyPluginSourceCaptureName, r as resolvePluginSourceCaptureFallbackPrefix, t as PLUGIN_SOURCE_CAPTURE_PREFIX } from "./plugin-source-capture-path-piKSCzPY.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, safeRealpathSync as safeRealpathSync$1, safeStatSync } from "@openclaw/fs-safe/path";
import "node:buffer";
import fs from "node:fs/promises";
import { prerelease, satisfies, valid } from "semver";
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/infra/test-runtime-env.ts
/** Detects Vitest/test execution from the env shape used by local and worker processes. */
function isVitestRuntimeEnv(env = process.env) {
const vitest = env.VITEST;
return vitest === "true" || vitest === "1" || env.VITEST_POOL_ID !== void 0 || env.VITEST_WORKER_ID !== void 0 || env.NODE_ENV === "test";
}
//#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) {
materializeErrorStack(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) {
materializeErrorStack(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) materializeErrorStack(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) materializeErrorStack(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) {
materializeErrorStack(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) {
materializeErrorStack(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
/** Selects built plugin artifacts without importing active runtime state. */
/** Canonical packaged runtime replaces staging-only dist-runtime artifacts. */
function resolveCanonicalDistRuntimeSource(source) {
const marker = `${path.sep}dist-runtime${path.sep}extensions${path.sep}`;
const index = source.indexOf(marker);
if (index === -1) return source;
const candidate = `${source.slice(0, index)}${path.sep}dist${path.sep}extensions${path.sep}${source.slice(index + marker.length)}`;
return pluginCacheExistsSync(candidate) ? candidate : source;
}
/** Finalize once at execution; discovery and identity retain the selected artifact. */
function resolvePluginRuntimeExecutionArtifact(selected) {
const source = resolveCanonicalDistRuntimeSource(selected.source);
const rootDir = resolveCanonicalDistRuntimeSource(selected.rootDir);
return {
source,
rootDir: rootDir !== selected.rootDir && !isPathInside$2(pluginCacheRealpathSync(rootDir) ?? rootDir, pluginCacheRealpathSync(source) ?? source) ? selected.rootDir : rootDir
};
}
/** Selects from lifecycle-owned package facts without pinning a runtime registry. */
function resolvePluginRuntimeArtifactSelection(params) {
const { source, rootDir } = resolvePluginRuntimeExecutionArtifact({
source: pluginCacheRealpathSync(params.source) ?? path.resolve(params.source),
rootDir: pluginCacheRealpathSync(params.rootDir) ?? path.resolve(params.rootDir)
});
const artifacts = getPluginCacheRoot(rootDir).runtimeArtifacts;
const key = JSON.stringify([
params.source,
params.entryKind,
params.origin,
params.preferBuiltPluginArtifacts,
params.sourcePreferred,
params.packageManifest?.build?.bundledDist
]);
let resolved = artifacts.get(key);
if (!resolved) {
resolved = resolvePluginRuntimeExecutionArtifact(resolvePreferredBuiltRuntimeArtifact({
...params,
source,
rootDir
}));
artifacts.set(key, resolved);
}
return resolved;
}
/** 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";
}
function resolveBundledArtifactRelativePath(rootDir, relativeSource) {
const file = readPluginCacheFile({
rootDir,
relativePath: "package.json",
rejectHardlinks: false
});
const parsed = file.ok ? parsePluginCacheJson(file) : void 0;
const metadata = parsed?.ok && isRecord(parsed.value) ? getPackageManifestMetadata(parsed.value) : void 0;
const entries = [...metadata?.runtimeExtensions?.length ? metadata.runtimeExtensions : metadata?.extensions ?? [], metadata?.runtimeSetupEntry ?? metadata?.setupEntry].filter((entry) => typeof entry === "string");
const sourceStem = relativeSource.replace(/\.[^.]+$/u, "");
const declared = entries.find((entry) => path.normalize(entry).replace(/\.[^.]+$/u, "") === sourceStem);
if (declared) return /\.[cm]?js$/.test(declared) ? declared : null;
const extensions = new Set(entries.map((entry) => path.extname(entry)));
const extension = extensions.size === 1 ? [...extensions][0] : void 0;
if (!extension || ![
".js",
".mjs",
".cjs"
].includes(extension) || entries.some((entry) => path.normalize(entry).startsWith(`dist${path.sep}`))) return null;
return relativeSource.replace(/\.[^.]+$/u, extension);
}
function resolvePackageLocalDistRuntimeArtifact(params) {
const relativeSource = path.relative(params.rootDir, params.source);
if (!isTypeScriptPackageEntry(relativeSource) || relativeSource === "" || relativeSource.startsWith("..") || path.isAbsolute(relativeSource)) return null;
for (const artifactRelativePath of listBuiltRuntimeEntryCandidates(relativeSource)) {
if (params.origin === "bundled" && !artifactRelativePath.startsWith("./dist/")) continue;
const artifactSource = path.resolve(params.rootDir, artifactRelativePath);
if (pluginCacheExistsSync(artifactSource)) return pluginCacheRealpathSync(artifactSource) ?? path.resolve(artifactSource);
}
return null;
}
function resolvePreferredBundledRootArtifactFromCanonicalPaths(params) {
const { rootDir, source } = params;
const sourceExternal = params.packageManifest?.build?.bundledDist === false;
const extensionsDir = path.dirname(rootDir);
if (path.basename(extensionsDir) !== "extensions") return {
source,
rootDir
};
const packageRoot = path.dirname(extensionsDir);
if (path.basename(packageRoot) === "dist" || path.basename(packageRoot) === "dist-runtime") return {
source,
rootDir
};
const relativeSource = path.relative(rootDir, source);
if (relativeSource === "" || relativeSource.startsWith("..") || path.isAbsolute(relativeSource)) return {
source,
rootDir
};
for (const artifactRootName of sourceExternal ? ["dist"] : ["dist-runtime", "dist"]) {
const artifactRoot = path.join(packageRoot, artifactRootName, "extensions", path.basename(rootDir));
const artifactRelativePath = resolveBundledArtifactRelativePath(artifactRoot, relativeSource);
if (!artifactRelativePath) continue;
const artifactSource = path.join(artifactRoot, artifactRelativePath);
if (pluginCacheExistsSync(artifactSource)) return {
source: pluginCacheRealpathSync(artifactSource) ?? path.resolve(artifactSource),
rootDir: pluginCacheRealpathSync(artifactRoot) ?? path.resolve(artifactRoot)
};
}
return {
source,
rootDir
};
}
/** Applies source, package-local, and root-build preference without runtime memo state. */
function resolvePreferredBuiltRuntimeArtifact(params) {
const { rootDir, source } = params;
if (!params.preferBuiltPluginArtifacts || params.sourcePreferred) return {
source,
rootDir
};
if (params.origin !== "bundled") {
const artifactSource = resolvePackageLocalDistRuntimeArtifact({
...params,
source,
rootDir
});
return artifactSource ? {
source: artifactSource,
rootDir
} : {
source,
rootDir
};
}
const packageLocalArtifactSource = params.packageManifest?.build?.bundledDist === false ? null : resolvePackageLocalDistRuntimeArtifact({
...params,
source,
rootDir
});
if (packageLocalArtifactSource) return {
source: packageLocalArtifactSource,
rootDir
};
return resolvePreferredBundledRootArtifactFromCanonicalPaths({
source,
rootDir,
packageManifest: params.packageManifest
});
}
//#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(rejectedOverride) {
const argvRoot = resolveOpenClawPackageRootSync({ argv1: process.argv[1] });
const safeArgvRoot = argvRoot && rejectedOverride && isPluginInPackageBundledRoots({
rootDir: rejectedOverride,
packageRoot: argvRoot
}) ? null : argvRoot;
const moduleRoot = resolveOpenClawPackageRootSync({ moduleUrl: import.meta.url });
return uniqueStrings([safeArgvRoot, 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 {
for (const packageRoot of resolvePackageRootsForBundledPlugins(rejectedExistingOverride)) {
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/ids.ts
const BUNDLED_CHAT_CHANNEL_ENTRIES = Object.freeze([
{
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: "x",
order: 85,
label: "X (Twitter)"
},
{
channelId: "zalo",
aliases: ["zl"],
order: 80,
label: "Zalo"
},
{
channelId: "zalouser",
aliases: ["zlu"],
order: 85,
label: "Zalo Personal"
}
].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 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/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/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) {
return filterStringEntries(value).map((entry) => 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);
const resolvePolicyId = (id) => normalizePluginPolicyId(normalizePluginId(id));
return {
enabled: config?.enabled ?? true,
allow: normalizePluginConfigList(config?.allow, resolvePolicyId),
deny: normalizePluginConfigList(config?.deny, resolvePolicyId),
loadPaths: normalizePluginConfigList(config?.load?.paths, identityNormalizePluginId),
slots: {
memory: memorySlot.kind === "off" ? null : memorySlot.pluginId,
contextEngine: normalizeSlotValue(config?.slots?.contextEngine)
},
entries: normalizePluginEntries(config?.entries, resolvePolicyId)
};
}
/**
* 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/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
});
const autoEnabledReason = params.autoEnabledReason || (entry?.enabled === true ? "enabled by effective config" : void 0) || (params.origin === "bundled" && resolveChannelConfigEnablement(params.rootConfig, params.id, params.channelIds) === true ? "channel configured" : void 0);
if (params.origin === "bundled" && params.enabledByDefault === true && (!autoEnabledReason || activationSource.plugins.allow.length === 0 || activationSource.plugins.allow.includes(policyId))) return decision("default", {
explicitlyEnabled: false,
reason: "bundled default enablement"
});
if (autoEnabledReason) return decision("auto", {
explicitlyEnabled: false,
reason: autoEnabledReason
});
if (params.origin === "bundled") return decision("disabled", {
explicitlyEnabled: false,
reason: "bundled (disabled by default)"
});
return decision("default");
}
//#endregion
//#region src/plugins/config-state.ts
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 = normalizePluginPolicyId(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
function listAgentWorkspaceDirs(cfg, env = process.env) {
return [...new Set(listAgentIds(cfg).map((agentId) => resolveAgentWorkspaceDir(cfg, agentId, env)))];
}
//#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;
}
/** Editable installs have no reusable identity; npm replacement records invalidate installed code. */
function resolveImmutableInstallIdentity(origin, installOwner, installRecords) {
const install = installOwner ? installRecords[installOwner] : void 0;
if (origin !== "global" || install?.source !== "npm" || !install.integrity) return;
return JSON.stringify([
installOwner,
install.installPath ?? "",
install.resolvedSpec ?? install.spec ?? "",
install.integrity,
install.installedAt ?? ""
]);
}
//#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/plugins/installed-plugin-index-install-owner.ts
function resolveInstalledPluginIndexInstallOwner(record) {
const ownedRecord = record;
return ownedRecord.installOwnerAmbiguous ? void 0 : ownedRecord.installOwner || void 0;
}
//#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 readerCustody = resolveGlobalSingleton(Symbol.for("openclaw.pluginMetadataSnapshotReaderCustody"), () => ({ owners: 0 }));
const readerKeys = [
"adoptCurrentPluginMetadataSnapshotIfAbsent",
"getCurrentPluginMetadataSnapshot",
"resolvePluginMetadataSnapshot",
"loadPluginMetadataSnapshot"
];
const snapshotReaderSlot = resolveGlobalSingleton(Symbol.for("openclaw.pluginMetadataSnapshotReaders"), () => ({}), (readers) => {
if (readerCustody.owners === 0) for (const key of readerKeys) delete readers[key];
}, "close-only");
/** 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-cleanup.ts
/** Missing custody is reclaimable only with complete, current active-use evidence. */
async function reclaimTokenlessPluginSourceCapture(directory, original, legacy, sweep, assertCurrent) {
if (sweep.unknownReason) return;
const { inspectTemporaryDirectoryUsage } = await import("./temp-directory-usage-B1tgpP5_.mjs");
await assertCurrent?.();
const usage = inspectTemporaryDirectoryUsage(directory);
if (usage.kind !== "inactive") {
if (usage.kind === "unknown") sweep.unknownReason = usage.reason;
return;
}
const sameOwnedDirectory = (target) => {
const current = fsSync.lstatSync(target);
return current.dev === original.dev && current.ino === original.ino && current.uid === original.uid && current.isDirectory() && (!process.getuid || current.uid === process.getuid());
};
if (!sameOwnedDirectory(directory)) return;
const retired = path.join(path.dirname(directory), `${legacy ? PLUGIN_SOURCE_CAPTURE_PREFIX : ""}${randomUUID()}`);
await fs.rename(directory, retired);
await assertCurrent?.();
if (!sameOwnedDirectory(retired)) return;
await fs.rm(retired, {
recursive: true,
force: true
});
}
//#endregion
//#region src/plugins/plugin-source-capture-context.ts
function getPluginSourceCaptureStorage() {
return getPluginExecutionFrame()?.sourceCaptureStorage;
}
function resolvePluginSourceCaptureStorage(stateDir, placement) {
const inherited = stateDir === void 0 ? getPluginSourceCaptureStorage() : void 0;
return Object.freeze({
stateDir: path.resolve(stateDir ?? inherited?.stateDir ?? resolveStateDir()),
placement: placement ?? inherited?.placement ?? "state"
});
}
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();
/** 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 nativeMaintenance?.assertCurrent();
if (!unchanged()) return;
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;
await 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;
}
await nativeMaintenance?.assertCurrent();
if (!unchanged()) return;
release(true);
released = true;
if (!retainedNative) {
await nativeMaintenance?.assertCurrent();
if (unchanged()) 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");
}
}
return {
reclaimInstance,
/** 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, reclaimInstance, 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)}`);
}
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, tokenlessSweep = {}) {
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;
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;
await reclaimTokenlessPluginSourceCapture(canonical, stat, legacy, tokenlessSweep, nativeMaintenance?.assertCurrent);
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) {
const tokenlessSweep = {};
let failures = 0;
let firstFailure;
const recordFailure = (error) => {
if (failures++ === 0) firstFailure = error;
};
sweep = reclaimInstances(root, recordFailure, false, void 0, void 0, tokenlessSweep).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, void 0, void 0, tokenlessSweep);
}
} catch (error) {
if (!hasErrnoCode(error, "ENOENT")) recordFailure(error);
}
}).then(() => {
if (tokenlessSweep.unknownReason) warn(`Tokenless temporary roots preserved: ${tokenlessSweep.unknownReason}`);
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();
}
const AGENT_HARNESS_COMPAT_RECORDS = [{
code: "agent-harness-sync-tool-construction",
status: "deprecated",
owner: "sdk",
introduced: "2026-09-08",
deprecated: "2026-10-04",
warningStarts: "2026-10-04",
removalGate: "next-plugin-sdk-major",
replacement: "Await hostCapabilities.createToolSurfaceAsync or createOpenClawCodingToolsAsync with the existing arguments. Released synchronous factories retain their array results and completion timing until the next Plugin SDK major and explicit breaking-release approval.",
docsPath: "/plugins/sdk-migration/compatibility-policy#harness-tool-construction",
surfaces: ["AgentHarnessHostCapabilities.createToolSurface", "createOpenClawCodingTools"],
diagnostics: ["TypeScript @deprecated annotations and migration documentation; no runtime warnings"],
tests: [
"src/plugin-sdk/agent-harness-async-compat.test.ts",
"src/agents/harness/host-capability.test.ts",
"src/plugins/compat/registry.test.ts"
],
releaseNote: "Harness tool construction exposes awaited factories that read fresh exec policy through the existing worker and preserve live host authority. Synchronous SDK factories remain compatible; stored data and update behavior are unchanged."
}, {
code: "agent-harness-sdk-alias",
status: "deprecated",
owner: "agent-runtime",
introduced: "2026-04-24",
deprecated: "2026-04-25",
warningStarts: "2026-04-25",
replacement: "none yet; retain until a harness subpath ships and external migration is proven",
docsPath: "/plugins/sdk-agent-harness",
surfaces: ["openclaw/plugin-sdk/agent-harness", "openclaw/plugin-sdk/agent-harness-runtime"],
diagnostics: ["plugin SDK compatibility warning"],
tests: ["src/plugins/contracts/plugin-sdk-subpaths.test.ts"]
}];
//#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-removed-export-records.ts
const CHANNEL_CLEANUP_REMOVAL = {
commit: "2fec39f57319be5b6e0b20b5305665ed1ff5e685",
removeAfter: "2026-10-04"
};
const REMOVED_EXPORT_SEEDS = [
{
code: "plugin-sdk-allowlist-resolution-entry-mapper",
...CHANNEL_CLEANUP_REMOVAL,
owner: "channel",
subpath: "allow-from",
names: ["mapBasicAllowlistResolutionEntries"],
replacement: "None; project BasicAllowlistResolutionEntry fields in the consuming plugin."
},
{
code: "plugin-sdk-computer-use-validator-compiler",
...CHANNEL_CLEANUP_REMOVAL,
owner: "sdk",
subpath: "computer-use",
names: ["compileComputerUseValidator"],
replacement: "Use Compile(schema).Check from `typebox/compile` with the schemas exported by `openclaw/plugin-sdk/computer-use`."
},
{
code: "plugin-sdk-stoppable-passive-monitor",
...CHANNEL_CLEANUP_REMOVAL,
owner: "channel",
subpath: "extension-shared",
names: ["runStoppablePassiveMonitor"],
replacement: "Use `openclaw/plugin-sdk/channel-outbound.runPassiveAccountLifecycle` with stop: (monitor) => monitor.stop()."
},
{
code: "plugin-sdk-advertised-lan-host",
...CHANNEL_CLEANUP_REMOVAL,
owner: "sdk",
subpath: "gateway-runtime",
names: ["resolveAdvertisedLanHost"],
replacement: "None; advertised LAN host discovery is host-owned and has no public SDK replacement."
},
{
code: "plugin-sdk-json-file-fallback-reader",
...CHANNEL_CLEANUP_REMOVAL,
owner: "sdk",
subpath: "json-store",
names: ["readJsonFileWithFallback"],
replacement: "None; plugins that read JSON artifacts must own parsing, fallback, and file-existence handling."
},
{
code: "plugin-sdk-secret-input-mode-normalizer",
...CHANNEL_CLEANUP_REMOVAL,
owner: "provider",
subpath: "provider-auth",
names: ["normalizeSecretInputModeInput"],
replacement: "None; validate plugin-owned input as the exported SecretInputMode values plaintext or ref."
},
{
code: "plugin-sdk-persistent-dedupe-legacy-json-migration",
commit: "bd64d93ff1ae704d13813f6580c4809ba0e76606",
removeAfter: "2026-10-04",
owner: "sdk",
subpath: "persistent-dedupe",
names: [
"PersistentDedupeLegacyJsonMigrationOptions",
"PersistentDedupeLegacyJsonMigrationResult",
"listPersistentDedupeLegacyJsonFileEntries",
"migratePersistentDedupeLegacyJsonFile"
],
replacement: "None; the legacy JSON migration was retired. Runtime dedupe continues through the SQLite-backed persistent-dedupe APIs."
},
{
code: "plugin-sdk-provider-auth-copilot-helpers",
commit: "f2de06b38de710854aacd19218327358445816c8",
removeAfter: "2026-10-01",
owner: "provider",
subpath: "provider-auth",
names: [
"CachedCopilotToken",
"DEFAULT_COPILOT_API_BASE_URL",
"deriveCopilotApiBaseUrlFromToken",
"resolveCopilotApiToken"
],
replacement: "Provider-local auth APIs for GitHub Copilot; none in the public SDK. Third-party plugins must own their provider-specific token exchange and caching."
}
];
function buildRemovedExportRecord(seed) {
return {
code: seed.code,
status: "removed",
owner: seed.owner,
introduced: "2026-10-07",
removeAfter: seed.removeAfter,
replacement: seed.replacement,
docsPath: "/plugins/sdk-migration/removed-surfaces#retroactively-recorded-shipped-exports",
surfaces: seed.names.map((name) => `openclaw/plugin-sdk/${seed.subpath}.${name}`),
diagnostics: ["plugin SDK shipped-surface guard and migration guide"],
tests: ["src/plugins/compat/registry.test.ts", "test/scripts/plugin-sdk-shipped-surface.test.ts"],
releaseNote: `The ${seed.names.join(", ")} exports from openclaw/plugin-sdk/${seed.subpath} were removed without a compatibility window in ${seed.commit}. This removal was recorded retroactively on 2026-10-07 when the shipped-surface guard was introduced.`
};
}
const PLUGIN_SDK_REMOVED_EXPORT_RECORDS = REMOVED_EXPORT_SEEDS.map(buildRemovedExportRecord);
//#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",
removeAfter: "2026-09-30",
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 in e649be315d751c0f633209f75e9adaa47681727c 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",
removeAfter: "2026-09-30",
owner: "channel",
replacement: "`openclaw/plugin-sdk/channel-outbound`",
releaseNote: "The deprecated `channel-reply-pipeline` Plugin SDK facade was removed in e649be315d751c0f633209f75e9adaa47681727c by the approved September 30 retirement; migrate to `openclaw/plugin-sdk/channel-outbound`."
},
{
code: "plugin-sdk-infra-runtime-subpath",
subpath: "infra-runtime",
status: "removed",
removeAfter: "2026-09-30",
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 in e649be315d751c0f633209f75e9adaa47681727c 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",
removeAfter: "2026-09-30",
owner: "channel",
replacement: "`openclaw/plugin-sdk/channel-outbound`",
releaseNote: "The deprecated `channel-lifecycle` Plugin SDK facade was removed in e649be315d751c0f633209f75e9adaa47681727c by the approved September 30 retirement; migrate to `openclaw/plugin-sdk/channel-outbound`."
},
{
code: "plugin-sdk-channel-message-subpath",
subpath: "channel-message",
status: "removed",
removeAfter: "2026-09-30",
owner: "channel",
replacement: "`openclaw/plugin-sdk/channel-outbound` and `openclaw/plugin-sdk/channel-inbound`",
releaseNote: "The deprecated `channel-message` Plugin SDK facade was removed in e649be315d751c0f633209f75e9adaa47681727c by the approved September 30 retirement; migrate to `openclaw/plugin-sdk/channel-outbound` and `openclaw/plugin-sdk/channel-inbound`."
},
{
code: "plugin-sdk-command-auth-subpath",
subpath: "command-auth",
status: "removed",
removeAfter: "2026-10-01",
owner: "sdk",
replacement: "`openclaw/plugin-sdk/channel-ingress-runtime` for sender authorization; matching named exports from `openclaw/plugin-sdk/command-auth-native`, `openclaw/plugin-sdk/command-status`, `openclaw/plugin-sdk/models-provider-runtime`, and `openclaw/plugin-sdk/channel-inbound`; adapt legacy authorization types to the ingress contract",
releaseNote: "The deprecated `command-auth` Plugin SDK facade was removed in 0d530ea5b0a991735c14e0e6e9711bd26c0c6d7d with SDK-owner approval on October 2, 2026; migrate to focused command and channel contracts as described in the import-path reference."
},
{
code: "plugin-sdk-discord-subpath",
subpath: "discord",
status: "removed",
removeAfter: "2026-10-01",
owner: "channel",
replacement: "matching named exports from `openclaw/plugin-sdk/channel-contract`, `openclaw/plugin-sdk/channel-core`, `openclaw/plugin-sdk/channel-plugin-common`, `openclaw/plugin-sdk/channel-status`, or `openclaw/plugin-sdk/config-contracts`; external plugins use generic channel contracts and the injected runtime for channel-owned behavior",
releaseNote: "The deprecated `discord` Plugin SDK facade was removed in 0d530ea5b0a991735c14e0e6e9711bd26c0c6d7d with SDK-owner approval on October 2, 2026; repository consumers use the Discord plugin's `api.ts` / `runtime-api.ts`, while external plugins use generic channel contracts and the injected runtime."
},
{
code: "plugin-sdk-telegram-account-subpath",
subpath: "telegram-account",
status: "removed",
removeAfter: "2026-10-01",
owner: "channel",
replacement: "generic channel contracts and injected runtime helpers for external plugins; the Telegram plugin's `api.ts` for repository consumers",
releaseNote: "The deprecated `telegram-account` Plugin SDK facade was removed in 0d530ea5b0a991735c14e0e6e9711bd26c0c6d7d with SDK-owner approval on October 2, 2026; migrate account resolution and types to generic channel contracts and injected runtime helpers."
},
{
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,
removeAfter: "removeAfter" in seed ? seed.removeAfter : void 0,
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/reply-tool-authority-record.ts
const REPLY_TOOL_AUTHORITY_COMPAT_RECORD = {
code: "reply-tool-authority-sync-preparation",
status: "deprecated",
owner: "sdk",
introduced: "2026-10-04",
deprecated: "2026-10-04",
warningStarts: "2026-10-04",
removalGate: "next-plugin-sdk-major",
replacement: "Await the Async reply-operation, fingerprint, and V2 queue companions. Question dispatchers await authority.assertCurrentAsync after transport preparation and assert current authority immediately before I/O. Released synchronous contracts remain until the next Plugin SDK major and explicit breaking-release approval.",
docsPath: "/plugins/sdk-migration/how-to-migrate#await-reply-tool-authority",
surfaces: [
"EmbeddedRunAttemptParams.replyOperation",
"EmbeddedRunAttemptParamsV2.replyOperation",
"AgentHarnessAttemptParams.replyOperation",
"AgentHarnessAttemptParamsV2.replyOperation",
"ReplyToolAuthoritySnapshot.fingerprint and project",
"ReplyBackendMessageInjectionV2.queueMessage, claimPendingUserInputAnswer, and cancelPendingUserInput",
"AgentQuestionDispatcher.authority.assertCurrent"
],
diagnostics: ["TypeScript @deprecated annotations and migration documentation; no runtime warnings"],
tests: ["src/plugins/compat/registry.test.ts", "src/plugin-sdk/reply-tool-authority-compat.test.ts"],
releaseNote: "Reply tool authority preparation gains awaited session-reader paths while retaining released synchronous snapshot, operation, and dispatcher contracts. Legacy external V2 dispatchers retain fresh native authority checks; schema, retention, and update behavior are unchanged."
};
//#endregion
//#region src/plugins/compat/session-persistence-records.ts
const DEPRECATED_SESSION_COMPAT = {
status: "deprecated",
owner: "sdk",
removalGate: "next-plugin-sdk-major",
diagnostics: ["TypeScript @deprecated annotations and migration documentation; no runtime warnings"]
};
const SESSION_PERSISTENCE_COMPAT_RECORDS = [
REPLY_TOOL_AUTHORITY_COMPAT_RECORD,
{
code: "codex-transcript-sync-validation",
...DEPRECATED_SESSION_COMPAT,
introduced: "2026-09-08",
deprecated: "2026-10-06",
warningStarts: "2026-10-06",
replacement: "Await readCodexSessionContextProjection for retained worker projection and final validation. Keep the released synchronous validators until the next Plugin SDK major and explicit breaking-release approval. readCodexSessionContext remains a supported synchronous worker reader with its released three-argument generic result contract.",
docsPath: "/plugins/sdk-migration/how-to-migrate#await-session-transcript-persistence",
surfaces: ["openclaw/plugin-sdk/codex-session-transcript-runtime.validateCodexSessionTranscriptReadAdmission", "openclaw/plugin-sdk/codex-session-transcript-runtime.validateCodexSessionTranscriptContextVersion"],
tests: [
"src/plugin-sdk/codex-session-transcript-runtime.compat.test.ts",
"src/config/sessions/session-transcript-context-read.worker.test.ts",
"src/plugins/compat/registry.test.ts"
],
releaseNote: "Codex history validates retained worker projections off the Gateway thread while preserving the released synchronous reader and validator signatures. Storage and update behavior are unchanged."
},
{
code: "deprecated-session-store-beta5-api",
status: "removed",
removeAfter: "2026-09-30",
owner: "sdk",
introduced: "2026-05-21",
deprecated: "2026-07-12",
warningStarts: "2026-07-12",
replacement: "Use `getSessionEntry(...)` and `listSessionEntries(...)` for reads; `patchSessionEntry(...)`, `upsertSessionEntry(...)`, and `deleteSessionEntry(...)` for row mutations; and session identity with `session-transcript-runtime` for active transcripts. The supported-plugin cutoff excludes v2026.7.1-beta.5 and other packages importing the retired bridge.",
docsPath: "/plugins/sdk-migration/removed-surfaces#removed-session-and-transcript-file-apis",
surfaces: [
"openclaw/plugin-sdk/session-store-runtime loadSessionStore",
"openclaw/plugin-sdk/session-store-runtime updateSessionStore",
"openclaw/plugin-sdk/session-store-runtime resolveSessionFilePath",
"openclaw/plugin-sdk/session-store-runtime resolveSessionStoreEntry",
"openclaw/plugin-sdk/session-store-runtime LoadSessionStoreOptions",
"openclaw/plugin-sdk/session-store-runtime UpdateSessionStoreOptions",
"openclaw package root loadSessionStore",
"openclaw package root saveSessionStore"
],
diagnostics: ["plugin compatibility registry and migration guide"],
tests: [
"src/plugin-sdk/session-store-runtime.test.ts",
"src/index.test.ts",
"src/plugins/compat/registry.test.ts"
],
releaseNote: "The September 30, 2026 approved cutoff in 1783c5fd6fc5b03d2b376ba7cbbab4461940207e retired the beta.5 session-store bridge, its option types, and package-root loadSessionStore/saveSessionStore aliases ahead of the former October 12 window. Plugins must use scoped row APIs and identity-backed transcript APIs; the session-store-runtime subpath and resolveStorePath remain available."
},
{
code: "transcript-lock-sync-message-preparation",
...DEPRECATED_SESSION_COMPAT,
introduced: "2026-09-08",
deprecated: "2026-10-05",
warningStarts: "2026-10-08",
replacement: "Use withSessionTranscriptWrite (or withCodexSessionTranscriptMirrorWrite) with preparation.prepareMessage and preparation.source. Legacy lock and opaque callback forms retain durable transaction ordering until the next Plugin SDK major; incognito targets reject them with migration guidance.",
docsPath: "/plugins/sdk-migration/how-to-migrate#await-locked-transcript-preparation",
surfaces: [
"SessionTranscriptWriteLockContext.appendMessage.prepareMessageAfterIdempotencyCheck",
"CodexSessionTranscriptMirrorWriteLockContext.appendMessageWithMessageSequence.prepareMessageAfterIdempotencyCheck",
"SessionTranscriptWriteLockContext.appendMessage.beforeFreshMessageCommit",
"CodexSessionTranscriptMirrorWriteLockContext.appendMessageWithMessageSequence.beforeFreshMessageCommit",
"withSessionTranscriptWriteLock",
"withCodexSessionTranscriptMirrorWriteLock"
],
diagnostics: ["TypeScript @deprecated annotations; one runtime warning per plugin for legacy transcript callbacks/locks; incognito targets receive an actionable replacement error"],
tests: [
"src/plugin-sdk/session-transcript-lock.native.test.ts",
"src/plugin-sdk/session-transcript-preparation-compat.test.ts",
"src/plugins/compat/registry.test.ts"
],
releaseNote: "Transcript writes have an optimistic, actor-compatible preparation contract. Legacy durable callbacks/locks retain transaction ordering; incognito callers migrate to preparation options. Stored data and update behavior are unchanged, and ordinary incognito routing stays host-owned."
},
{
code: "session-upstream-links-sync-persistence",
...DEPRECATED_SESSION_COMPAT,
introduced: "2026-07-14",
deprecated: "2026-10-05",
warningStarts: "2026-10-05",
replacement: "Await upsertSessionUpstreamLinkAsync and deleteSessionUpstreamLinkAsync from session-catalog. Official harnesses await the native initializer's linkAsync method. Retain the synchronous signatures and completion timing shipped in v2026.9.8 until the next Plugin SDK major and explicit breaking-release approval.",
docsPath: "/plugins/sdk-migration/how-to-migrate#await-session-upstream-links",
surfaces: [
"openclaw/plugin-sdk/session-catalog.upsertSessionUpstreamLink",
"openclaw/plugin-sdk/session-catalog.deleteSessionUpstreamLink",
"openclaw/plugin-sdk/agent-harness-session-runtime.createNativeSessionInitializationOwner().prepare().link"
],
tests: [
"src/plugin-sdk/session-catalog-upstream-compat.test.ts",
"src/plugin-sdk/agent-harness-session-compat.test.ts",
"src/sessions/session-upstream-links.test.ts",
"src/plugins/compat/registry.test.ts"
],
releaseNote: "Bundled session adoption and native initialization await upstream-link writes through the shared-state worker. Released synchronous SDK methods remain compatible until the next Plugin SDK major; schemas, stored data, retention, and update behavior are unchanged."
},
{
code: "session-observer-progress-sync-reads",
...DEPRECATED_SESSION_COMPAT,
introduced: "2026-09-08",
deprecated: "2026-10-04",
warningStarts: "2026-10-04",
replacement: "Await sessionObserver.handleEventAsync, getCompanionSnapshotAsync, and disposeAsync. Use reply_dispatch shouldSendToolSummariesAsync/shouldSendFullToolDetailsAsync and onVerboseProgressVisibilityAsync. Retain synchronous contracts until the next Plugin SDK major and explicit breaking-release approval.",
docsPath: "/plugins/sdk-migration/how-to-migrate#await-session-observer-and-progress-visibility",
surfaces: [
"GatewayRequestHandlerOptions.context.sessionObserver.handleEvent",
"GatewayRequestHandlerOptions.context.sessionObserver.getCompanionSnapshot",
"GatewayRequestHandlerOptions.context.sessionObserver.dispose",
"PluginHookReplyDispatchEvent.shouldSendToolSummaries",
"PluginHookReplyDispatchEvent.shouldSendFullToolDetails",
"GetReplyOptions.onVerboseProgressVisibility",
"tryDispatchAcpReplyHook boolean-only event inputs"
],
tests: [
"src/plugins/compat/registry.test.ts",
"src/plugin-sdk/reply-runtime.contract.test.ts",
"src/plugin-sdk/gateway-session-observer-compat.test.ts",
"src/plugin-sdk/acp-runtime.test.ts",
"src/gateway/session-observer.test.ts"
],
releaseNote: "Session observer lifecycle and progress visibility expose awaited worker-backed methods. Released synchronous observer methods, reply-hook booleans, and visibility callbacks remain compatible; schemas, stored data, retention, and update behavior are unchanged."
},
{
code: "channel-inbound-sync-envelope-timestamps",
...DEPRECATED_SESSION_COMPAT,
introduced: "2026-10-05",
deprecated: "2026-10-05",
warningStarts: "2026-10-05",
replacement: "Await readSessionUpdatedAtAsync, createChannelInboundEnvelopeBuilderAsync, or resolveInboundSessionEnvelopeContextAsync at each message's formatting boundary. Resolve routes through resolveAgentRoute and use dispatchInboundDirectDm. Retain the synchronous signatures and callback timing shipped in 2026.9.8 until the next Plugin SDK major and explicit breaking-release approval.",
docsPath: "/plugins/sdk-migration/compatibility-policy#inbound-envelope-timestamps",
surfaces: [
"openclaw/plugin-sdk/session-store-runtime.readSessionUpdatedAt",
"api.runtime.channel.session.readSessionUpdatedAt",
"openclaw/plugin-sdk/channel-inbound.createChannelInboundEnvelopeBuilder",
"openclaw/plugin-sdk/channel-inbound.resolveChannelInboundRouteEnvelope",
"openclaw/plugin-sdk/channel-inbound.resolveInboundSessionEnvelopeContext",
"openclaw/plugin-sdk/channel-inbound.dispatchInboundDirectDmWithRuntime",
"openclaw/plugin-sdk/inbound-envelope.createInboundEnvelopeBuilder",
"openclaw/plugin-sdk/inbound-envelope.resolveInboundRouteEnvelopeBuilder",
"openclaw/plugin-sdk/inbound-envelope.resolveInboundRouteEnvelopeBuilderWithRuntime"
],
tests: [
"src/plugin-sdk/shipped-channel-compat.test.ts",
"src/channels/inbound-event/envelope.test.ts",
"src/channels/inbound-event/envelope.worker.test.ts",
"src/plugin-sdk/direct-dm.test.ts",
"src/plugins/compat/registry.test.ts"
],
releaseNote: "Bundled channels prepare activity timestamps through the existing session reader while released plugins retain their synchronous envelope callbacks. Stored data, schemas, retention, and update behavior are unchanged."
},
{
code: "reply-run-start-unprepared-transcript",
...DEPRECATED_SESSION_COMPAT,
owner: "agent-runtime",
introduced: "2026-10-04",
deprecated: "2026-10-04",
warningStarts: "2026-10-04",
replacement: "Forward every onAgentRunStart argument and its synchronous return value through the current runtime helper. Bundled producers supply prepared transcript facts in the optional fourth argument; retain the released three-argument callback and synchronous transcript-read fallback until the next Plugin SDK major and explicit breaking-release approval.",
docsPath: "/plugins/sdk-migration/compatibility-policy#reply-run-start-transcript-facts",
surfaces: ["openclaw/plugin-sdk/reply-runtime.GetReplyOptions.onAgentRunStart", "PluginHookReplyDispatchContext.onAgentRunStart"],
diagnostics: ["plugin compatibility registry and migration documentation; no runtime warnings"],
tests: [
"src/plugin-sdk/reply-runtime.contract.test.ts",
"src/gateway/server-methods/chat-send-reply-dispatch.test.ts",
"src/plugins/compat/registry.test.ts"
],
releaseNote: "Reply runtimes can pass prepared transcript boundaries without Gateway-thread reads. Released plugin callbacks keep their arguments and synchronous completion acknowledgment; stored data and update behavior are unchanged."
},
{
code: "session-manager-sync-context-read",
...DEPRECATED_SESSION_COMPAT,
introduced: "2026-10-04",
deprecated: "2026-10-04",
warningStarts: "2026-10-04",
replacement: "Await SessionManager.readSessionContextAsync for full-fidelity context consumption. The synchronous reader retains its shipped result until the next Plugin SDK major and explicit breaking-release approval.",
docsPath: "/plugins/sdk-migration/how-to-migrate#await-session-transcript-persistence",
surfaces: ["SessionManager.readSessionContext"],
diagnostics: ["TypeScript @deprecated annotation and one runtime DEP_SESSION_PERSISTENCE warning per method per process"],
tests: [
"src/plugin-sdk/agent-sessions.context-compat.test.ts",
"src/agents/sessions/session-manager-incognito.test.ts",
"src/agents/sessions/session-manager-model-context-snapshot.test.ts",
"src/plugins/compat/registry.test.ts"
],
releaseNote: "Plugins can await full-fidelity SessionManager context reads and asynchronous consumers. Source validation follows consumption; the synchronous reader remains available until the next Plugin SDK major. Storage and update behavior are unchanged."
},
{
code: "session-reset-freshness-sync-read",
...DEPRECATED_SESSION_COMPAT,
introduced: "2026-10-05",
deprecated: "2026-10-05",
warningStarts: "2026-10-05",
replacement: "Await runtime.channel.session.resolveEntryResetFreshnessAsync. The released synchronous resolveEntryResetFreshness method retains its parameters and result until the next Plugin SDK major and explicit breaking-release approval.",
docsPath: "/plugins/sdk-migration#session-reset-freshness",
surfaces: ["api.runtime.channel.session.resolveEntryResetFreshness"],
diagnostics: ["TypeScript @deprecated annotation and migration documentation; no runtime warnings"],
tests: [
"src/plugin-sdk/session-reset-freshness-compat.test.ts",
"src/config/sessions/entry-freshness.test.ts",
"src/plugins/compat/registry.test.ts"
],
releaseNote: "Channel plugins can await session reset freshness through the transcript worker. Existing plugins retain the synchronous method and reset policy; stored data and update behavior are unchanged."
},
{
code: "agent-end-sync-side-effects",
...DEPRECATED_SESSION_COMPAT,
owner: "agent-runtime",
introduced: "2026-05-30",
deprecated: "2026-10-04",
warningStarts: "2026-10-04",
replacement: "Await runAgentEndSideEffectsAsync before releasing the turn lease. The released runAgentEndSideEffects helper retains its synchronous void result and scheduling behavior until the next Plugin SDK major and explicit breaking-release approval.",
docsPath: "/plugins/sdk-agent-harness/attempt-runtime#agent-end-side-effects",
surfaces: ["openclaw/plugin-sdk/agent-harness-runtime.runAgentEndSideEffects"],
diagnostics: ["TypeScript @deprecated annotation and migration documentation; no runtime warnings"],
tests: [
"src/plugin-sdk/agent-harness-runtime.test.ts",
"src/agents/harness/agent-end-side-effects.test.ts",
"src/plugins/compat/registry.test.ts"
],
releaseNote: "Harness plugins can await transcript-anchor preparation before releasing turn authority. The synchronous agent-end helper remains compatible for published plugins; stored data and update behavior are unchanged."
},
{
code: "native-session-generation-sync-authority",
...DEPRECATED_SESSION_COMPAT,
owner: "agent-runtime",
introduced: "2026-09-22",
deprecated: "2026-10-03",
warningStarts: "2026-10-03",
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"
],
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",
...DEPRECATED_SESSION_COMPAT,
introduced: "2026-10-01",
deprecated: "2026-10-01",
warningStarts: "2026-10-01",
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",
...DEPRECATED_SESSION_COMPAT,
introduced: "2026-10-01",
deprecated: "2026-10-01",
warningStarts: "2026-10-01",
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",
...DEPRECATED_SESSION_COMPAT,
owner: "provider",
introduced: "2026-10-01",
deprecated: "2026-10-01",
warningStarts: "2026-10-01",
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."
},
{
code: "agent-execution-preparation-released-signature",
status: "active",
owner: "sdk",
introduced: "2026-10-06",
removalGate: "next-plugin-sdk-major",
replacement: "Keep the execution input shapes shipped in v2026.9.9 and v2026.10.1-beta.1 by openOpenClawAgentSqliteWorkerStore, including the beta capturePreparedGenerationClaim method for parameter-derived types. Private native-adoption capabilities are not plugin requirements. Host schema readmission uses an optional third argument; existing callers and two-argument prepare implementations remain supported without migration or deprecation.",
docsPath: "/plugins/sdk-migration/compatibility-policy#agent-execution-preparation-compatibility",
surfaces: ["openclaw/plugin-sdk/sqlite-runtime.openOpenClawAgentSqliteWorkerStore publicationSource.execution", "openclaw/plugin-sdk/sqlite-runtime.openOpenClawAgentSqliteWorkerStore publicationSource.execution.prepare"],
diagnostics: ["SDK type assertions and compatibility documentation; no runtime warnings"],
tests: [
"src/plugin-sdk/sqlite-runtime.preparation-compat.test.ts",
"src/state/openclaw-agent-execution.creation-witness.test.ts",
"src/plugins/compat/registry.test.ts"
],
releaseNote: "Agent execution reuses completed native preparation while explicit host admission refreshes schema proof. Released execution objects do not require private host capabilities; preparation calls and implementations retain their Promise contract. Schemas, stored data, and update behavior are unchanged."
},
{
code: "acp-session-metadata-released-signatures",
status: "active",
owner: "sdk",
introduced: "2026-10-04",
removalGate: "next-plugin-sdk-major",
replacement: "Keep the released one-argument ACP reader and manager read/write injection signatures. Internal actor bindings are not plugin arguments; actor activation must preserve these callable contracts. The APIs remain supported and are not deprecated.",
docsPath: "/plugins/sdk-migration/compatibility-policy#acp-metadata-binding-compatibility",
surfaces: [
"openclaw/plugin-sdk/acp-runtime.readAcpSessionEntryAsync",
"AcpSessionManagerDeps.loadSessionEntryAsync",
"AcpSessionManagerDeps.upsertSessionMeta"
],
diagnostics: ["SDK type assertions and compatibility documentation; no runtime warnings"],
tests: [
"src/plugin-sdk/acp-runtime.test.ts",
"src/acp/runtime/session-meta-read.test.ts",
"src/plugins/compat/registry.test.ts"
],
releaseNote: "Released ACP readers and manager injection callbacks keep their one-argument contracts while incognito actor composition remains internal and inactive."
},
{
code: "memory-session-released-signatures",
status: "active",
owner: "sdk",
introduced: "2026-10-05",
removalGate: "next-plugin-sdk-major",
replacement: "Keep the released Memory entry, corpus, and reset-recall reader signatures. Internal actor sources are not plugin arguments; actor activation must preserve the existing Promise results and synchronous message observers. These APIs remain supported and are not deprecated.",
docsPath: "/plugins/sdk-migration/compatibility-policy#memory-session-binding-compatibility",
surfaces: [
"openclaw/plugin-sdk/memory-core-host-engine-sessions.buildSessionEntry",
"openclaw/plugin-sdk/memory-core-host-engine-sessions.listSessionTranscriptCorpusEntriesForAgent",
"openclaw/plugin-sdk/memory-core-host-engine-sessions.readSessionResetRecallCutoff"
],
diagnostics: ["SDK type assertions and compatibility documentation; no runtime warnings"],
tests: [
"src/plugin-sdk/memory-core-host-engine-sessions.contract.test.ts",
"src/state/openclaw-agent-execution-incognito.memory.test.ts",
"src/plugins/compat/registry.test.ts"
],
releaseNote: "Released Memory readers retain their argument and return contracts while incognito actor sources remain internal and inactive. Storage and update behavior are unchanged."
},
{
code: "transcript-strict-sync-message-preparation",
...DEPRECATED_SESSION_COMPAT,
introduced: "2026-09-08",
deprecated: "2026-10-05",
warningStarts: "2026-10-08",
replacement: "Use preparation.prepareMessage and preparation.source with appendSessionTranscriptMessageByIdentityStrict or appendSessionTranscriptMessageByIdentity. Legacy opaque callbacks retain durable transaction ordering until the next Plugin SDK major; incognito targets reject them with migration guidance.",
docsPath: "/plugins/sdk-migration/how-to-migrate#await-strict-transcript-message-preparation",
surfaces: [
"appendSessionTranscriptMessageByIdentityStrict.prepareMessageAfterIdempotencyCheck",
"appendSessionTranscriptMessageByIdentityStrict.beforeFreshMessageCommit",
"appendSessionTranscriptMessageByIdentity.prepareMessageAfterIdempotencyCheck",
"appendSessionTranscriptMessageByIdentity.beforeFreshMessageCommit"
],
diagnostics: ["TypeScript @deprecated annotations; one runtime warning per plugin shared with legacy transcript locks; incognito targets receive an actionable replacement error"],
tests: ["src/plugin-sdk/session-transcript-runtime.worker-preparation.test.ts", "src/plugins/compat/registry.test.ts"],
releaseNote: "Strict and ordinary transcript appends accept prepared source authority and awaited message preparation. Legacy durable callbacks remain compatible until the next Plugin SDK major; stored data and update behavior are unchanged."
}
];
//#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"]
};
[
...PLUGIN_SDK_REMOVED_EXPORT_RECORDS,
...AGENT_HARNESS_COMPAT_RECORDS,
...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 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
const MODEL_CATALOG_APIS = [...MODEL_DATA_APIS];
const MODEL_CATALOG_THINKING_FORMATS = [...MODEL_DATA_THINKING_FORMATS];
function isModelCatalogThinkingFormat(value) {
return MODEL_CATALOG_THINKING_FORMATS.includes(value);
}
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 = normalizeCatalogFields(MODEL_CATALOG_THINKING_LEVELS, (level) => {
const mapped = value[level];
return mapped === null ? null : normalizeOptionalString(mapped);
});
return Object.keys(normalized).length > 0 ? normalized : void 0;
}
function normalizeStringMap(value) {
return normalizeCatalogRecords(value, (key, rawValue) => {
const mapValue = normalizeOptionalString(rawValue) ?? "";
return key && !isBlockedObjectKey(key) && mapValue ? mapValue : void 0;
}, (key) => normalizeOptionalString(key) ?? "");
}
function normalizeCatalogFields(fields, normalize, result = {}) {
for (const field of fields) {
const value = normalize(field);
if (value !== void 0) result[field] = value;
}
return result;
}
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 = normalizeCatalogFields([
"input",
"output",
"cacheRead",
"cacheWrite"
], (field) => asNonNegativeFiniteNumber(value[field]));
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 = normalizeCatalogFields([
"prompt",
"completion",
"image",
"audio",
"request"
], (field) => {
const candidate = value[field];
return normalizeOptionalString(candidate) ?? asFiniteNumber(candidate);
});
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 = normalizeCatalogFields([
"p50",
"p75",
"p90",
"p99"
], (field) => asFiniteNumber(value[field]));
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 } : {}
};
normalizeCatalogFields([
"order",
"only",
"ignore",
"quantizations"
], (field) => normalizeOptionalTrimmedStringList(value[field]), routing);
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 = normalizeCatalogFields(["only", "order"], (field) => normalizeOptionalTrimmedStringList(value[field]));
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];
normalizeCatalogFields(["toolSchemaProfile", "toolCallArgumentsEncoding"], (field) => normalizeOptionalString(value[field]), compat);
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) ?? "";
return {
...baseUrl ? { baseUrl } : {},
...api ? { api } : {},
...headers ? { headers } : {},
...defaultModel ? { defaultModel } : {},
...defaultUtilityModel ? { defaultUtilityModel } : {},
models
};
}
function normalizeCatalogRecords(value, normalize, normalizeKey = normalizeProviderId) {
if (!isRecord(value)) return;
const records = {};
for (const [rawKey, rawValue] of Object.entries(value)) {
const key = normalizeKey(rawKey);
const normalized = normalize(key, rawValue);
if (normalized !== void 0) records[key] = normalized;
}
return Object.keys(records).length > 0 ? records : 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;
}
/** 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 = normalizeCatalogRecords(value.providers, (providerId, rawProvider) => providerId && ownedProviders.has(providerId) ? normalizeModelCatalogProvider(rawProvider) : void 0);
const aliases = normalizeCatalogRecords(value.aliases, (alias, rawTarget) => {
if (!alias || !isRecord(rawTarget)) return;
const provider = normalizeProviderId(normalizeOptionalString(rawTarget.provider) ?? "");
if (!provider || !ownedProviders.has(provider)) return;
const api = normalizeModelCatalogApi(rawTarget.api);
const baseUrl = normalizeOptionalString(rawTarget.baseUrl) ?? "";
return {
provider,
...api ? { api } : {},
...baseUrl ? { baseUrl } : {}
};
});
const suppressions = normalizeModelCatalogSuppressions(value.suppressions);
const discovery = normalizeCatalogRecords(value.discovery, (providerId, rawMode) => {
const mode = normalizeOptionalString(rawMode) ?? "";
return providerId && ownedProviders.has(providerId) && MODEL_CATALOG_DISCOVERY_MODES.has(mode) ? mode : void 0;
});
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 pdf = optionalManifestFields({
textExtraction: normalizeOptionalString(pdfRaw.textExtraction),
image: pdfRaw.image === false ? false : normalizeOptionalString(pdfRaw.image)
});
return pdf ? { pdf } : void 0;
}
function normalizeMediaUnderstandingProviderMetadata(value) {
return normalizeNamedMetadataRecord(value, (rawMetadata) => optionalManifestFields({
capabilities: normalizeMediaUnderstandingCapabilities(rawMetadata.capabilities),
defaultModels: normalizeMediaUnderstandingRecord(rawMetadata.defaultModels, normalizeOptionalString),
autoPriority: normalizeMediaUnderstandingRecord(rawMetadata.autoPriority, (priority) => typeof priority === "number" && Number.isFinite(priority) ? priority : void 0),
nativeDocumentInputs: normalizeMediaUnderstandingNativeDocumentInputs(rawMetadata.nativeDocumentInputs),
documentModels: normalizeMediaUnderstandingDocumentModels(rawMetadata.documentModels)
}));
}
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;
return omitUndefinedManifestFields({
provider,
providerBaseUrl: normalizeProviderBaseUrlGuard(rawSignal.providerBaseUrl)
});
});
}
function normalizeCapabilityProviderModeConfigSignal(value) {
if (!isRecord(value)) return;
return optionalManifestFields({
path: normalizeOptionalString(value.path),
default: normalizeOptionalString(value.default),
allowed: normalizeOptionalTrimmedStringList(value.allowed),
disallowed: normalizeOptionalTrimmedStringList(value.disallowed)
});
}
function normalizeCapabilityProviderConfigSignals(value) {
return normalizeManifestObjectList(value, (rawSignal) => {
const rootPath = normalizeOptionalString(rawSignal.rootPath);
if (!rootPath) return;
const required = normalizeOptionalTrimmedStringList(rawSignal.required);
const requiredAny = normalizeOptionalTrimmedStringList(rawSignal.requiredAny);
const mode = normalizeCapabilityProviderModeConfigSignal(rawSignal.mode);
return required || requiredAny || mode ? omitUndefinedManifestFields({
rootPath,
overlayPath: normalizeOptionalString(rawSignal.overlayPath),
overlayMapPath: normalizeOptionalString(rawSignal.overlayMapPath),
required,
requiredAny,
mode
}) : void 0;
});
}
function normalizeCapabilityProviderMetadataEntry(rawMetadata) {
return optionalManifestFields({
aliases: normalizeOptionalTrimmedStringList(rawMetadata.aliases),
authProviders: normalizeOptionalTrimmedStringList(rawMetadata.authProviders),
authSignals: normalizeCapabilityProviderAuthSignals(rawMetadata.authSignals),
configSignals: normalizeCapabilityProviderConfigSignals(rawMetadata.configSignals),
referenceAudioInputs: rawMetadata.referenceAudioInputs === true ? true : 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;
return optionalManifestFields({
featured: typeof value.featured === "boolean" ? value.featured : void 0,
order: typeof value.order === "number" && Number.isFinite(value.order) ? value.order : void 0
});
}
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;
}
/** Only fresh field projections belong here; never mutate the authored manifest. */
function omitUndefinedManifestFields(fields) {
for (const key in fields) if (fields[key] === void 0) delete fields[key];
return fields;
}
function optionalManifestFields(fields) {
omitUndefinedManifestFields(fields);
return Object.keys(fields).length > 0 ? fields : 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;
return optionalManifestFields({
modelPrefixes: normalizeOptionalTrimmedStringList(value.modelPrefixes),
modelPatterns: normalizeOptionalTrimmedStringList(value.modelPatterns)
});
}
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;
}
return optionalManifestFields({
aliases: Object.keys(aliases).length > 0 ? aliases : void 0,
stripPrefixes: normalizeOptionalTrimmedStringList(value.stripPrefixes),
prefixWhenBare: normalizeOptionalString(value.prefixWhenBare),
prefixWhenBareAfterAliasStartsWith: normalizeManifestModelIdPrefixRules(value.prefixWhenBareAfterAliasStartsWith)
});
}
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 = normalizeOptionalTrimmedStringList(rawEndpoint.hosts)?.map((host) => host.toLowerCase());
const hostSuffixes = normalizeOptionalTrimmedStringList(rawEndpoint.hostSuffixes)?.map((host) => host.toLowerCase());
const baseUrls = normalizeOptionalTrimmedStringList(rawEndpoint.baseUrls);
if (!hosts && !hostSuffixes && !baseUrls) return;
return omitUndefinedManifestFields({
endpointClass,
hosts,
hostSuffixes,
baseUrls,
googleVertexRegion: normalizeOptionalString(rawEndpoint.googleVertexRegion),
googleVertexRegionHostSuffix: normalizeOptionalString(rawEndpoint.googleVertexRegionHostSuffix)?.toLowerCase()
});
});
}
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 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 passEnv = normalizeTrimmedStringList(rawIntegration.passEnv).filter((entry) => ENV_SECRET_REF_ID_RE.test(entry)).slice(0, MAX_SECRET_PROVIDER_EXEC_PASS_ENV);
return omitUndefinedManifestFields({
providerAlias: normalizeOptionalString(rawIntegration.providerAlias),
displayName: normalizeOptionalString(rawIntegration.displayName),
description: normalizeOptionalString(rawIntegration.description),
source: "exec",
command,
args: args.length > 0 ? args : void 0,
timeoutMs: normalizeManifestPositiveInteger(rawIntegration.timeoutMs, MAX_SECRET_PROVIDER_EXEC_TIMEOUT_MS),
noOutputTimeoutMs: normalizeManifestPositiveInteger(rawIntegration.noOutputTimeoutMs, MAX_SECRET_PROVIDER_EXEC_TIMEOUT_MS),
maxOutputBytes: normalizeManifestPositiveInteger(rawIntegration.maxOutputBytes, MAX_SECRET_PROVIDER_EXEC_OUTPUT_BYTES),
jsonOnly: typeof rawIntegration.jsonOnly === "boolean" ? rawIntegration.jsonOnly : void 0,
env: normalizeManifestStringRecord(rawIntegration.env),
passEnv: passEnv.length > 0 ? passEnv : void 0
});
});
}
//#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;
return omitUndefinedManifestFields({
id,
authMethods: normalizeOptionalTrimmedStringList(entry.authMethods),
envVars: normalizeOptionalTrimmedStringList(entry.envVars),
authEvidence: normalizeManifestSetupProviderAuthEvidence(entry.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 = normalizeOptionalTrimmedStringList(entry.fallbackPaths);
if (!fileEnvVar && !fallbackPaths) return;
return omitUndefinedManifestFields({
type: "local-file-with-env",
fileEnvVar,
fallbackPaths,
requiresAnyEnv: normalizeOptionalTrimmedStringList(entry.requiresAnyEnv),
requiresAllEnv: normalizeOptionalTrimmedStringList(entry.requiresAllEnv),
credentialMarker,
source: normalizeOptionalString(entry.source)
});
});
}
function normalizeManifestSetup(value) {
if (!isRecord(value)) return;
const nativeSessionCatalog = isRecord(value.nativeSessionCatalog) ? omitUndefinedManifestFields({
label: normalizeOptionalString(value.nativeSessionCatalog.label) ?? "",
legacyDefaultEnabled: value.nativeSessionCatalog.legacyDefaultEnabled === true ? true : void 0,
nodeCommands: normalizeOptionalTrimmedStringList(value.nativeSessionCatalog.nodeCommands),
description: normalizeOptionalString(value.nativeSessionCatalog.description)
}) : void 0;
return optionalManifestFields({
providers: normalizeManifestSetupProviders(value.providers),
cliBackends: normalizeOptionalTrimmedStringList(value.cliBackends),
configMigrations: normalizeOptionalTrimmedStringList(value.configMigrations),
nativeSessionCatalog: nativeSessionCatalog?.label ? nativeSessionCatalog : void 0,
requiresRuntime: typeof value.requiresRuntime === "boolean" ? value.requiresRuntime : void 0
});
}
function normalizeManifestQaRunners(value) {
return normalizeManifestObjectList(value, (entry) => {
const commandName = normalizeOptionalString(entry.commandName) ?? "";
if (!commandName) return;
return omitUndefinedManifestFields({
commandName,
description: normalizeOptionalString(entry.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"
};
const groups = [];
for (const field of ["dataBindings", "actionVerbs"]) {
const entries = value[field];
if (entries !== void 0 && !Array.isArray(entries)) return {
ok: false,
error: `dashboard.${field} must be an array`
};
groups.push({
field,
entries: entries ?? []
});
}
const dashboard = {};
for (const group of groups) {
const { field } = group;
const entries = [];
for (const [index, entry] of group.entries.entries()) {
const normalized = normalizeDashboardCapabilityBase(entry, `dashboard.${field}`, index);
if (typeof normalized === "string") return {
ok: false,
error: normalized
};
const rawParamShape = field === "actionVerbs" && 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`
};
entries.push({
...normalized,
...rawParamShape ? { paramShape: rawParamShape } : {}
});
}
if (entries.length > 0) dashboard[field] = entries;
}
return Object.keys(dashboard).length > 0 ? {
ok: true,
dashboard
} : { ok: true };
}
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 onboardingScopes = normalizeTrimmedStringList(entry.onboardingScopes).filter((scope) => scope === "text-inference" || scope === "image-generation" || scope === "music-generation");
return omitUndefinedManifestFields({
provider,
method,
choiceId,
modelTarget: entry.modelTarget === "utility" ? "utility" : void 0,
platforms: entry.platforms !== void 0 ? normalizeManifestPlatforms(entry.platforms) : void 0,
choiceLabel: normalizeOptionalString(entry.choiceLabel),
choiceHint: normalizeOptionalString(entry.choiceHint),
icon: normalizeSetupPresentationHttpsUrl(entry.icon),
website: normalizeSetupPresentationHttpsUrl(entry.website),
docsUrl: normalizeSetupPresentationHttpsUrl(entry.docsUrl),
assistantPriority: typeof entry.assistantPriority === "number" && Number.isFinite(entry.assistantPriority) ? entry.assistantPriority : void 0,
assistantVisibility: entry.assistantVisibility === "manual-only" || entry.assistantVisibility === "visible" || entry.assistantVisibility === "detected-only" ? entry.assistantVisibility : void 0,
deprecatedChoiceIds: normalizeOptionalTrimmedStringList(entry.deprecatedChoiceIds),
groupId: normalizeOptionalString(entry.groupId),
groupLabel: normalizeOptionalString(entry.groupLabel),
groupHint: normalizeOptionalString(entry.groupHint),
onboardingFeatured: entry.onboardingFeatured === true ? true : void 0,
appGuidedDiscovery: entry.appGuidedDiscovery === true ? true : void 0,
optionKey: normalizeOptionalString(entry.optionKey),
cliFlag: normalizeOptionalString(entry.cliFlag),
cliOption: normalizeOptionalString(entry.cliOption),
cliDescription: normalizeOptionalString(entry.cliDescription),
appGuidedSecret: entry.appGuidedSecret === true ? true : void 0,
personalAccount: entry.personalAccount === true ? true : void 0,
appGuidedActionLabel: normalizeOptionalString(entry.appGuidedActionLabel),
appGuidedAuth: entry.appGuidedAuth === "oauth" || entry.appGuidedAuth === "device-code" ? entry.appGuidedAuth : void 0,
credentialOnly: entry.credentialOnly === true ? true : void 0,
channelLogin: normalizeProviderChannelLogin(entry.channelLogin),
onboardingScopes: onboardingScopes.length > 0 ? onboardingScopes : void 0
});
});
}
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;
return omitUndefinedManifestFields({
schema,
uiHints: normalizeConfigUiHints(rawEntry.uiHints),
runtime: isRecord(rawEntry.runtime) && typeof rawEntry.runtime.safeParse === "function" ? rawEntry.runtime : void 0,
label: normalizeOptionalString(rawEntry.label),
description: normalizeOptionalString(rawEntry.description),
preferOver: normalizeOptionalTrimmedStringList(rawEntry.preferOver),
commands: normalizeManifestChannelCommandDefaults(rawEntry.commands)
});
});
}
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_BRAND_ICON_IDS = ["claw", "mark"];
const THEME_WORKING_INDICATOR_VALUES = [
"claw",
"dots",
"brand",
"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",
mascot: "none",
brandIcon: "mark",
workingIndicator: "dots",
lobsterdex: false,
description: "Luminous green on green-tinted black or pale green, with JetBrains Mono throughout. A classic green terminal atmosphere."
},
{
id: "crt",
name: "CRT",
mascot: "none",
brandIcon: "mark",
workingIndicator: "dots",
lobsterdex: false,
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) => Object.assign(theme, {
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",
"brandName",
"brandIcon",
"workingIndicator",
"lobsterdex",
"communityLinks",
"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.brandName !== void 0) {
const brandName = requireText(record.brandName, "theme.brandName", 80);
if (/[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]/u.test(brandName)) throw new Error("theme.brandName must contain only printable characters");
definition.brandName = brandName;
}
if (record.brandIcon !== void 0) {
const allowedIds = [...THEME_BRAND_ICON_IDS, ...options?.iconIds ?? []];
const brandIcon = allowedIds.find((id) => id === record.brandIcon);
if (!brandIcon) throw new Error(`theme.brandIcon must be one of ${allowedIds.join(", ")}`);
definition.brandIcon = brandIcon;
}
if (record.workingIndicator !== void 0) {
const workingIndicator = THEME_WORKING_INDICATOR_VALUES.find((value) => value === record.workingIndicator);
if (!workingIndicator) throw new Error(`theme.workingIndicator must be one of ${THEME_WORKING_INDICATOR_VALUES.join(", ")}`);
definition.workingIndicator = workingIndicator;
}
for (const key of ["lobsterdex", "communityLinks"]) if (record[key] !== void 0) {
if (typeof record[key] !== "boolean") throw new Error(`theme.${key} must be a boolean`);
definition[key] = record[key];
}
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 !== "icons" && 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(icons, hats, critters, label) {
const paths = (value, kind, catalogIds) => Object.fromEntries(normalizeArtworkEntries(value, `${label}.${kind}`, catalogIds).map(([id, source]) => [id, normalizeArtworkPath(source, `${label}.${kind}.${id}`)]));
return {
...icons !== void 0 ? { icons: paths(icons, "icons", THEME_BRAND_ICON_IDS) } : {},
...hats !== void 0 ? { hats: paths(hats, "hats", THEME_AVATAR_HAT_IDS) } : {},
...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, icons, hats, critters } = entry;
if (Object.keys(entry).some((key) => ![
"id",
"name",
"description",
"source",
"icons",
"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(icons, 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.icons || 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 fail = (error) => cacheResult({
ok: false,
error,
manifestPath
});
const parsed = parsePluginCacheJson(file, { json5: true });
if (!parsed.ok) return fail(`failed to parse plugin manifest: ${String(parsed.error)}`);
const raw = parsed.value;
if (!isRecord(raw)) return fail("plugin manifest must be an object");
const id = normalizeOptionalString(raw.id) ?? "";
if (!id) return fail("plugin manifest requires id");
if (isCoreReservedPluginId(id)) return fail(`plugin manifest id "${id}" is reserved by OpenClaw core`);
const configSchema = isRecord(raw.configSchema) ? raw.configSchema : null;
if (!configSchema) return fail("plugin manifest requires configSchema");
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 fail(`invalid plugin manifest categories: ${categories.error}`);
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 fail(`invalid plugin manifest dashboard: ${dashboardResult.error}`);
const uiCapabilities = validatePluginUiCapabilities(raw.uiCapabilities);
const controlUiResult = normalizeManifestControlUi(raw.controlUi);
if (!controlUiResult.ok) return fail(`invalid plugin manifest controlUi: ${controlUiResult.error}`);
const themesResult = normalizeManifestThemes(raw.themes, id, file.contents.toString("utf8"));
if (!themesResult.ok) return fail(`invalid plugin manifest themes: ${themesResult.error}`);
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 resolveBundleComponentPaths(value, rootDir, defaults) {
const declared = normalizeBundlePathList(value);
return mergeBundlePathLists(defaults.filter((candidate) => pluginCacheExistsSync(path.join(rootDir, candidate))), declared);
}
function resolveCursorComponents(raw, rootDir) {
const commands = resolveBundleComponentPaths(raw.commands, rootDir, [".cursor/commands"]);
const skills = mergeBundlePathLists(resolveBundleComponentPaths(raw.skills, rootDir, ["skills"]), commands);
const agents = resolveBundleComponentPaths(raw.subagents ?? raw.agents, rootDir, [".cursor/agents"]);
const capabilities = [
...skills.length > 0 ? ["skills"] : [],
...commands.length > 0 ? ["commands"] : [],
...agents.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 {
skills,
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") Object.assign(manifest, resolveCursorComponents(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];
if (!rejectHardlinks) {
const builtCandidate = source.replace(/\.[^.]+$/u, ".js");
if (builtCandidate !== source) candidates.push(builtCandidate);
}
const candidate = candidates.find((entry) => pluginCacheExistsSync(entry)) ?? source;
const opened = checkPluginCacheEntry({
rootDir: params.packageDir,
relativePath: path.relative(params.packageDir, candidate),
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;
}
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) {
const type = entry.isSymbolicLink() ? pluginCacheStatSync(fullPath) : entry;
return type?.isFile() ? "file" : type?.isDirectory() ? "directory" : 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 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 packageEntryParams = {
packageDir: dir,
...rootRealPath !== void 0 ? { packageRootRealPath: rootRealPath } : {},
manifest,
pluginIdHint,
origin: params.origin,
requireBuiltRuntimeEntry,
sourceLabel: dir,
diagnostics,
rejectHardlinks
};
const setupSource = resolvePackageSetupSource(packageEntryParams);
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({
...packageEntryParams,
extensions,
manifestId: manifestId ?? normalizeOptionalString(packageMetadata?.plugin?.id)
});
for (const entry of entries) addPackageCandidate(entry.source, entry.idHint, extensions.length > 1 ? entry.idHint : void 0);
return true;
}
const bundleFormat = detectBundleManifestFormat(dir);
if (bundleFormat) {
const bundleManifest = loadBundleManifest({
rootDir: dir,
...rootRealPath !== void 0 ? { rootRealPath } : {},
bundleFormat,
rejectHardlinks
});
if (!bundleManifest.ok) diagnostics.push({
level: "error",
message: bundleManifest.error,
source: bundleManifest.manifestPath
});
else {
addCandidate({
idHint: bundleManifest.manifest.id,
source: dir,
rootDir: dir,
origin: params.origin,
format: "bundle",
bundleFormat,
workspaceDir: params.workspaceDir,
...params.installOwner ? { installOwner: params.installOwner } : {},
...params.installOwnerAmbiguous ? { installOwnerAmbiguous: true } : {},
manifest,
packageDir: dir,
bundledManifestId: bundleManifest.manifest.id,
bundledManifestPath: bundleManifest.manifestPath
});
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 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);
//#endregion
//#region src/config/plugin-install-record-map.ts
const PluginInstallRecordSchema = object({
...InstallRecordShape,
source: union([InstallSourceSchema, literal("marketplace")]),
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()
}).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 (!trimmed.toLowerCase().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({
...params,
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")), {
iconIds: Object.keys(theme.icons ?? {}),
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.icons ? { icons: Object.fromEntries(Object.entries(theme.icons).map(([id, source]) => [id, { svg: readSvg(source) }])) } : {},
...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.icons || 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];
if (!catalogValue) {
merged[key] = value;
continue;
}
const config = {
...catalogValue,
...value,
schema: value.schema ?? catalogValue.schema
};
if (catalogValue.uiHints || value.uiHints) config.uiHints = {
...catalogValue.uiHints,
...value.uiHints
};
for (const field of [
"runtime",
"label",
"description",
"preferOver",
"commands"
]) {
const fallback = value[field] ?? catalogValue[field];
if (fallback) Object.assign(config, { [field]: fallback });
}
merged[key] = config;
}
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 policyId = normalizePluginPolicyId(params.record.id);
const configuredEntry = params.normalized?.entries[policyId];
if (params.normalized?.enabled === false || configuredEntry?.enabled === false || params.normalized?.deny.includes(policyId) || params.normalized?.allow.length && !params.normalized.allow.includes(policyId)) 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(params)) return 2;
if (params.candidate.origin === "bundled") return 3;
if (params.candidate.origin === "workspace") return 4;
return 5;
}
function isIntentionalInstalledBundledDuplicate(params) {
return [[params.left, params.right], [params.right, params.left]].some(([installed, bundled]) => matchesInstalledPluginRecord({
candidate: installed,
env: params.env,
installRecords: params.installRecords
}) && !isStaleForeignBundledPin({
candidate: installed,
env: params.env
}) && bundled.origin === "bundled" && !isBundledPluginInsideDevSourceRoot({
rootDir: bundled.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 normalized = normalizePluginsConfigWithResolverCore((params.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({
candidate,
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,
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({
candidate,
env,
installRecords: getInstallRecords()
});
const existingRank = resolveDuplicatePrecedenceRank({
candidate: existing.candidate,
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({
left: candidate,
right: existing.candidate,
env,
installRecords: getInstallRecords()
})) continue;
if (isSameGlobalPackageDuplicate(candidate, existing.candidate)) continue;
const staleForeignPin = winnerCandidate.origin === "bundled" && isStaleForeignBundledPin({
candidate: overriddenCandidate,
env
}) && matchesInstalledPluginRecord({
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-native-admission-state.ts
const admissions = /* @__PURE__ */ new WeakMap();
/** Publication bounds reusable code to the installed entries and configured workspaces. */
function publishInspectionNativeEsmPolicy(cache, policy) {
const prepared = cache.inspectionNativeEsmPolicy ??= {
...policy,
viewKeys: /* @__PURE__ */ new Set([resolveAdmissionViewKey(false), resolveAdmissionViewKey(true)])
};
for (const state of admissions.get(cache)?.values() ?? []) if (prepared.viewKeys.has(state.viewKey)) state.inspectionNativeEsm ??= { ...prepared };
}
function resolveAdmissionViewKey(readOnly = isArtifactPreservingStateRead(), storage = resolvePluginSourceCaptureStorage(), publicationStateDir = resolveActivePluginInstallRoots().stateDir) {
return JSON.stringify([
readOnly ? "readonly" : "writable",
resolvePluginMetadataEnvFingerprint(),
storage.stateDir,
storage.placement,
publicationStateDir
]);
}
//#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 prepareInspectionNativeEsmPolicy(snapshot, workspaces, options) {
const sources = /* @__PURE__ */ new Map();
const manifests = new Map(snapshot.manifestRegistry.plugins.map((manifest) => [`${manifest.rootDir}\0${manifest.id}`, manifest]));
for (const record of snapshot.index.plugins) {
const identity = resolveImmutableInstallIdentity(record.origin, resolveInstalledPluginIndexInstallOwner(record), snapshot.index.installRecords);
const manifest = manifests.get(`${record.rootDir}\0${record.pluginId}`);
if (!identity || !manifest) continue;
for (const entryKind of ["runtime", "setup"]) {
const source = entryKind === "runtime" ? manifest.source : manifest.setupSource;
if (!source) continue;
for (const preferBuiltPluginArtifacts of [false, true]) {
const artifact = resolvePluginRuntimeExecutionArtifact(resolvePluginRuntimeArtifactSelection({
...manifest,
source,
entryKind,
preferBuiltPluginArtifacts
}));
const canonicalSource = safeRealpathSync$1(artifact.source);
if (canonicalSource) sources.set(canonicalSource, identity);
}
}
}
return {
workspaces: new Set(workspaces),
sources,
devSourceRoot: resolvePluginDiscoveryContext({
config: options.config,
env: options.env,
workspaceDir: options.workspaceDir ?? snapshot.workspaceDir
}).devSourceRoot
};
}
function prepareConfigCompatibility(snapshot, options, fingerprint, trustConfigIdentity = false) {
const compatiblePolicyHashes = options.compatibleConfigs?.map((config) => resolveInstalledPluginIndexPolicyHash(config, options.env));
const compatibleConfigFingerprints = options.compatibleConfigs?.map((config, index) => fingerprint(config, compatiblePolicyHashes?.[index]));
const configIdentities = [...options.compatibleConfigs ?? []];
if (options.config) {
const policyHash = resolveInstalledPluginIndexPolicyHash(options.config, options.env);
if (trustConfigIdentity || policyHash === snapshot.policyHash || compatiblePolicyHashes?.includes(policyHash)) configIdentities.push(options.config);
}
return {
compatiblePolicyHashes,
compatibleConfigFingerprints,
configIdentities
};
}
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, compatibleConfigFingerprints, configIdentities } = prepareConfigCompatibility(snapshot, options, fingerprint);
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 agentWorkspaces = owner === "gateway" && options.config ? listAgentWorkspaceDirs(options.config, options.env) : void 0;
const agentWorkspaceFingerprint = agentWorkspaces && JSON.stringify(agentWorkspaces);
const inspectionPolicy = agentWorkspaces && prepareInspectionNativeEsmPolicy(snapshot, agentWorkspaces, options);
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);
if (inspectionPolicy) publishInspectionNativeEsmPolicy(getProcessPluginCache(), inspectionPolicy);
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. */
/**
* 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";
const fail = (message) => {
if (onMissing === "throw") throw new Error(message);
};
if (!pointer.startsWith("/")) return fail("File-backed secret ids must be absolute JSON pointers (for example: \"/providers/openai/apiKey\").");
let current = root;
for (const rawToken of pointer.slice(1).split("/")) {
const token = rawToken.replace(/~1/g, "/").replace(/~0/g, "~");
if (Array.isArray(current)) {
const index = parseConfigPathArrayIndex(token);
if (index === void 0 || index >= current.length) return fail(`JSON pointer segment "${token}" is out of bounds.`);
current = current[index];
continue;
}
if (!isRecord(current) || !Object.hasOwn(current, token)) return fail(`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 & 2) !== 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
});
}
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
/**
* 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));
}
//#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" || ref.source === "store") return { source: ref.source };
}
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 = resolvePositiveTimerTimeoutMs(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 (!hasNonEmptyString(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(params);
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) {
function invalidResponse(detail) {
throw providerResolutionError({
source: "exec",
provider: params.providerName,
message: `Exec provider "${params.providerName}" ${detail}`
});
}
const trimmed = params.stdout.trim();
if (!trimmed) invalidResponse("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 };
invalidResponse("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 };
invalidResponse("response must be an object.");
}
if (parsed.protocolVersion !== 1) invalidResponse("protocolVersion must be 1.");
const responseValues = parsed.values;
if (!isRecord(responseValues)) invalidResponse("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 = resolvePositiveTimerTimeoutMs(params.providerConfig.timeoutMs, DEFAULT_EXEC_TIMEOUT_MS);
const noOutputTimeoutMs = resolvePositiveTimerTimeoutMs(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
});
let failure;
if (result.termination === "timeout") failure = `Exec provider "${params.providerName}" timed out after ${timeoutMs}ms.`;
else if (result.termination === "no-output-timeout") failure = `Exec provider "${params.providerName}" produced no output for ${noOutputTimeoutMs}ms.`;
else if (result.outputLimitExceeded) failure = `Exec provider output exceeded maxOutputBytes (${maxOutputBytes}).`;
else if (result.code !== 0) failure = `Exec provider "${params.providerName}" exited with code ${String(result.code)}.`;
if (failure !== void 0) throw providerResolutionError({
source: "exec",
provider: params.providerName,
message: failure
});
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 {
const common = {
refs: params.refs,
providerName: params.providerName,
onRefError: params.onRefError
};
if (params.providerConfig.source === "env") return await resolveEnvRefs({
...common,
providerConfig: params.providerConfig,
env: params.options.env ?? process.env
});
if (params.providerConfig.source === "file") return await resolveFileRefs({
...common,
providerConfig: params.providerConfig,
cache: params.options.cache
});
if (params.providerConfig.source === "store") return await resolveStoreRefs({
...common,
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({
...common,
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 (!hasNonEmptyString(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, isVitestRuntimeEnv as s, resolveSecretRefString as t };