Skip to content

Commit bd6035d

Browse files
committed
fix: prefer built plugin artifacts at gateway startup
1 parent 2dd3e40 commit bd6035d

4 files changed

Lines changed: 151 additions & 17 deletions

File tree

src/gateway/server-plugins.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ describe("loadGatewayPlugins", () => {
388388
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
389389
expect.objectContaining({
390390
onlyPluginIds: ["discord", "telegram"],
391+
preferBuiltPluginArtifacts: true,
391392
}),
392393
);
393394
});

src/gateway/server-plugins.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,7 @@ export function loadGatewayPlugins(params: {
602602
allowGatewaySubagentBinding: true,
603603
},
604604
preferSetupRuntimeForChannelPlugins: params.preferSetupRuntimeForChannelPlugins,
605+
preferBuiltPluginArtifacts: true,
605606
...(params.pluginLookUpTable?.manifestRegistry
606607
? { manifestRegistry: params.pluginLookUpTable.manifestRegistry }
607608
: {}),

src/plugins/loader.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5209,6 +5209,64 @@ module.exports = {
52095209
).toBe(true);
52105210
});
52115211

5212+
it("prefers built bundled plugin artifacts over source TS when requested", () => {
5213+
const repoRoot = makeTempDir();
5214+
const sourceDir = path.join(repoRoot, "extensions", "startup-artifact-test");
5215+
const runtimeDir = path.join(repoRoot, "dist-runtime", "extensions", "startup-artifact-test");
5216+
mkdirSafe(sourceDir);
5217+
mkdirSafe(runtimeDir);
5218+
fs.writeFileSync(
5219+
path.join(sourceDir, "openclaw.plugin.json"),
5220+
JSON.stringify(
5221+
{
5222+
id: "startup-artifact-test",
5223+
configSchema: EMPTY_PLUGIN_SCHEMA,
5224+
},
5225+
null,
5226+
2,
5227+
),
5228+
"utf-8",
5229+
);
5230+
fs.writeFileSync(
5231+
path.join(sourceDir, "index.ts"),
5232+
'throw new Error("source TS should not load during gateway startup");\n',
5233+
"utf-8",
5234+
);
5235+
fs.writeFileSync(
5236+
path.join(runtimeDir, "index.js"),
5237+
'module.exports = { id: "startup-artifact-test", register() {} };\n',
5238+
"utf-8",
5239+
);
5240+
5241+
const registry = withEnv(
5242+
{
5243+
OPENCLAW_BUNDLED_PLUGINS_DIR: path.join(repoRoot, "extensions"),
5244+
OPENCLAW_TEST_TRUST_BUNDLED_PLUGINS_DIR: "1",
5245+
OPENCLAW_DISABLE_BUNDLED_PLUGINS: undefined,
5246+
},
5247+
() =>
5248+
loadOpenClawPlugins({
5249+
cache: false,
5250+
preferBuiltPluginArtifacts: true,
5251+
onlyPluginIds: ["startup-artifact-test"],
5252+
config: {
5253+
plugins: {
5254+
allow: ["startup-artifact-test"],
5255+
entries: {
5256+
"startup-artifact-test": {
5257+
enabled: true,
5258+
},
5259+
},
5260+
},
5261+
},
5262+
}),
5263+
);
5264+
5265+
expect(registry.plugins.find((entry) => entry.id === "startup-artifact-test")?.status).toBe(
5266+
"loaded",
5267+
);
5268+
});
5269+
52125270
it("blocks before_prompt_build but preserves legacy model overrides when prompt injection is disabled", async () => {
52135271
useNoBundledPlugins();
52145272
const plugin = writePlugin({

src/plugins/loader.ts

Lines changed: 91 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ import {
112112
getCachedPluginSourceModuleLoader,
113113
type PluginModuleLoaderCache,
114114
} from "./plugin-module-loader-cache.js";
115+
import type { PluginOrigin } from "./plugin-origin.types.js";
115116
import {
116117
createPluginIdScopeSet,
117118
hasExplicitPluginIdScope,
@@ -180,6 +181,11 @@ export type PluginLoadOptions = {
180181
* via package metadata because their setup entry covers the pre-listen startup surface.
181182
*/
182183
preferSetupRuntimeForChannelPlugins?: boolean;
184+
/**
185+
* For hot startup paths, prefer bundled plugin JS artifacts over source TS
186+
* entrypoints when both are present in a source checkout.
187+
*/
188+
preferBuiltPluginArtifacts?: boolean;
183189
toolDiscovery?: boolean;
184190
activate?: boolean;
185191
loadModules?: boolean;
@@ -275,6 +281,7 @@ function createPluginCandidatesFromManifestRegistry(
275281
idHint: record.id,
276282
rootDir: record.rootDir,
277283
source: record.source,
284+
...(record.setupSource !== undefined ? { setupSource: record.setupSource } : {}),
278285
origin: record.origin,
279286
...(record.workspaceDir !== undefined ? { workspaceDir: record.workspaceDir } : {}),
280287
...(record.format !== undefined ? { format: record.format } : {}),
@@ -517,6 +524,52 @@ function resolveCanonicalDistRuntimeSource(source: string): string {
517524
return fs.existsSync(candidate) ? candidate : source;
518525
}
519526

527+
function rewriteBundledRuntimeArtifactRelativePath(relativePath: string): string {
528+
return relativePath.replace(/\.[^.]+$/u, ".js");
529+
}
530+
531+
function resolvePreferredBuiltBundledRuntimeArtifact(params: {
532+
source: string;
533+
rootDir: string;
534+
origin: PluginOrigin;
535+
preferBuiltPluginArtifacts: boolean;
536+
}): { source: string; rootDir: string } {
537+
const rootDir = safeRealpathOrResolve(params.rootDir);
538+
const source = safeRealpathOrResolve(params.source);
539+
if (!params.preferBuiltPluginArtifacts || params.origin !== "bundled") {
540+
return { source, rootDir };
541+
}
542+
const extensionsDir = path.dirname(rootDir);
543+
if (path.basename(extensionsDir) !== "extensions") {
544+
return { source, rootDir };
545+
}
546+
const packageRoot = path.dirname(extensionsDir);
547+
if (path.basename(packageRoot) === "dist" || path.basename(packageRoot) === "dist-runtime") {
548+
return { source, rootDir };
549+
}
550+
const relativeSource = path.relative(rootDir, source);
551+
if (relativeSource === "" || relativeSource.startsWith("..") || path.isAbsolute(relativeSource)) {
552+
return { source, rootDir };
553+
}
554+
const artifactRelativePath = rewriteBundledRuntimeArtifactRelativePath(relativeSource);
555+
for (const artifactRootName of ["dist-runtime", "dist"] as const) {
556+
const artifactRoot = path.join(
557+
packageRoot,
558+
artifactRootName,
559+
"extensions",
560+
path.basename(rootDir),
561+
);
562+
const artifactSource = path.join(artifactRoot, artifactRelativePath);
563+
if (fs.existsSync(artifactSource)) {
564+
return {
565+
source: safeRealpathOrResolve(artifactSource),
566+
rootDir: safeRealpathOrResolve(artifactRoot),
567+
};
568+
}
569+
}
570+
return { source, rootDir };
571+
}
572+
520573
export const __testing = {
521574
buildPluginLoaderJitiOptions,
522575
buildPluginLoaderAliasMap,
@@ -682,6 +735,7 @@ function buildCacheKey(params: {
682735
forceSetupOnlyChannelPlugins?: boolean;
683736
requireSetupEntryForSetupOnlyChannelPlugins?: boolean;
684737
preferSetupRuntimeForChannelPlugins?: boolean;
738+
preferBuiltPluginArtifacts?: boolean;
685739
toolDiscovery?: boolean;
686740
loadModules?: boolean;
687741
runtimeSubagentMode?: "default" | "explicit" | "gateway-bindable";
@@ -722,6 +776,8 @@ function buildCacheKey(params: {
722776
: "allow-full-fallback";
723777
const startupChannelMode =
724778
params.preferSetupRuntimeForChannelPlugins === true ? "prefer-setup" : "full";
779+
const bundledArtifactMode =
780+
params.preferBuiltPluginArtifacts === true ? "prefer-built-artifacts" : "source-default";
725781
const moduleLoadMode = params.loadModules === false ? "manifest-only" : "load-modules";
726782
const discoveryMode = params.toolDiscovery === true ? "tool-discovery" : "default-discovery";
727783
const runtimeSubagentMode = params.runtimeSubagentMode ?? "default";
@@ -734,7 +790,7 @@ function buildCacheKey(params: {
734790
installs,
735791
loadPaths,
736792
activationMetadataKey: params.activationMetadataKey ?? "",
737-
})}::${scopeKey}::${setupOnlyKey}::${setupOnlyModeKey}::${setupOnlyRequirementKey}::${startupChannelMode}::${moduleLoadMode}::${discoveryMode}::${runtimeSubagentMode}::${params.pluginSdkResolution ?? "auto"}::${gatewayMethodsKey}::${activationMode}`;
793+
})}::${scopeKey}::${setupOnlyKey}::${setupOnlyModeKey}::${setupOnlyRequirementKey}::${startupChannelMode}::${bundledArtifactMode}::${moduleLoadMode}::${discoveryMode}::${runtimeSubagentMode}::${params.pluginSdkResolution ?? "auto"}::${gatewayMethodsKey}::${activationMode}`;
738794
}
739795

740796
function matchesScopedPluginRequest(params: {
@@ -812,6 +868,7 @@ function hasExplicitCompatibilityInputs(options: PluginLoadOptions): boolean {
812868
options.forceSetupOnlyChannelPlugins === true ||
813869
options.requireSetupEntryForSetupOnlyChannelPlugins === true ||
814870
options.preferSetupRuntimeForChannelPlugins === true ||
871+
options.preferBuiltPluginArtifacts === true ||
815872
options.loadModules === false
816873
);
817874
}
@@ -1011,6 +1068,7 @@ function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) {
10111068
const requireSetupEntryForSetupOnlyChannelPlugins =
10121069
options.requireSetupEntryForSetupOnlyChannelPlugins === true;
10131070
const preferSetupRuntimeForChannelPlugins = options.preferSetupRuntimeForChannelPlugins === true;
1071+
const preferBuiltPluginArtifacts = options.preferBuiltPluginArtifacts === true;
10141072
const runtimeSubagentMode = resolveRuntimeSubagentMode(options.runtimeOptions);
10151073
const coreGatewayMethodNames = resolveCoreGatewayMethodNames(options);
10161074
const installRecords = {
@@ -1031,6 +1089,7 @@ function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) {
10311089
forceSetupOnlyChannelPlugins,
10321090
requireSetupEntryForSetupOnlyChannelPlugins,
10331091
preferSetupRuntimeForChannelPlugins,
1092+
preferBuiltPluginArtifacts,
10341093
toolDiscovery: options.toolDiscovery,
10351094
loadModules: options.loadModules,
10361095
runtimeSubagentMode,
@@ -1050,6 +1109,7 @@ function resolvePluginLoadCacheContext(options: PluginLoadOptions = {}) {
10501109
forceSetupOnlyChannelPlugins,
10511110
requireSetupEntryForSetupOnlyChannelPlugins,
10521111
preferSetupRuntimeForChannelPlugins,
1112+
preferBuiltPluginArtifacts,
10531113
shouldActivate: options.activate !== false,
10541114
shouldLoadModules: options.loadModules !== false,
10551115
runtimeSubagentMode,
@@ -1375,6 +1435,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
13751435
forceSetupOnlyChannelPlugins,
13761436
requireSetupEntryForSetupOnlyChannelPlugins,
13771437
preferSetupRuntimeForChannelPlugins,
1438+
preferBuiltPluginArtifacts,
13781439
shouldActivate,
13791440
shouldLoadModules,
13801441
cacheKey,
@@ -1697,13 +1758,20 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
16971758
});
16981759
};
16991760
const pluginRoot = safeRealpathOrResolve(candidate.rootDir);
1700-
let runtimePluginRoot = pluginRoot;
1701-
let runtimeCandidateSource =
1702-
candidate.origin === "bundled" ? safeRealpathOrResolve(candidate.source) : candidate.source;
1703-
let runtimeSetupSource =
1704-
candidate.origin === "bundled" && manifestRecord.setupSource
1705-
? safeRealpathOrResolve(manifestRecord.setupSource)
1706-
: manifestRecord.setupSource;
1761+
const runtimeCandidateEntry = resolvePreferredBuiltBundledRuntimeArtifact({
1762+
source: candidate.source,
1763+
rootDir: pluginRoot,
1764+
origin: candidate.origin,
1765+
preferBuiltPluginArtifacts,
1766+
});
1767+
const runtimeSetupEntry = manifestRecord.setupSource
1768+
? resolvePreferredBuiltBundledRuntimeArtifact({
1769+
source: manifestRecord.setupSource,
1770+
rootDir: pluginRoot,
1771+
origin: candidate.origin,
1772+
preferBuiltPluginArtifacts,
1773+
})
1774+
: undefined;
17071775

17081776
const scopedSetupOnlyChannelPluginRequested =
17091777
includeSetupOnlyChannelPlugins &&
@@ -1883,12 +1951,12 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
18831951
continue;
18841952
}
18851953

1886-
const loadSource =
1887-
registrationPlan.loadSetupEntry && runtimeSetupSource
1888-
? runtimeSetupSource
1889-
: runtimeCandidateSource;
1890-
const moduleLoadSource = resolveCanonicalDistRuntimeSource(loadSource);
1891-
const moduleRoot = resolveCanonicalDistRuntimeSource(runtimePluginRoot);
1954+
const loadEntry =
1955+
registrationPlan.loadSetupEntry && runtimeSetupEntry
1956+
? runtimeSetupEntry
1957+
: runtimeCandidateEntry;
1958+
const moduleLoadSource = resolveCanonicalDistRuntimeSource(loadEntry.source);
1959+
const moduleRoot = resolveCanonicalDistRuntimeSource(loadEntry.rootDir);
18921960
const opened = openBoundaryFileSync({
18931961
absolutePath: moduleLoadSource,
18941962
rootPath: moduleRoot,
@@ -1972,11 +2040,17 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
19722040
if (
19732041
registrationPlan.loadSetupRuntimeEntry &&
19742042
setupRegistration.usesBundledSetupContract &&
1975-
runtimeCandidateSource !== safeSource
2043+
resolveCanonicalDistRuntimeSource(runtimeCandidateEntry.source) !== safeSource
19762044
) {
2045+
const runtimeModuleSource = resolveCanonicalDistRuntimeSource(
2046+
runtimeCandidateEntry.source,
2047+
);
2048+
const runtimeModuleRoot = resolveCanonicalDistRuntimeSource(
2049+
runtimeCandidateEntry.rootDir,
2050+
);
19772051
const runtimeOpened = openBoundaryFileSync({
1978-
absolutePath: runtimeCandidateSource,
1979-
rootPath: runtimePluginRoot,
2052+
absolutePath: runtimeModuleSource,
2053+
rootPath: runtimeModuleRoot,
19802054
boundaryLabel: "plugin root",
19812055
rejectHardlinks: candidate.origin !== "bundled",
19822056
skipLexicalRootCheck: true,

0 commit comments

Comments
 (0)