mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
fix(browser): auto-start configured browser plugin
This commit is contained in:
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Gateway/models: move local-provider pricing opt-outs, OpenRouter/LiteLLM aliases, and proxy passthrough pricing lookup into plugin manifest metadata so core no longer carries extension-specific pricing tables. Thanks @codex.
|
||||
- Agents/sessions: acquire the session write lock only after cold bootstrap, plugin, and tool setup so fallback runs are not blocked by stalled pre-model startup work. Thanks @codex.
|
||||
- Browser/plugins: auto-start the bundled browser plugin when root `browser` config is present, including restrictive plugin allowlists, and ignore stale persisted plugin registries whose package paths no longer exist. Thanks @codex.
|
||||
- Gateway/models: skip external OpenRouter and LiteLLM pricing refreshes for local/self-hosted model endpoints so startup does not wait on remote pricing catalogs for local-only Ollama, vLLM, and compatible providers. Thanks @codex.
|
||||
- CLI/plugins: stop security-blocked plugin installs from retrying as hook packs, so normal plugin packages report the scanner failure without a misleading "not a valid hook pack" follow-up. Fixes #61175; supersedes #64102. Thanks @KonsultDigital and @ziyincody.
|
||||
- Agents/Anthropic: strip stale trailing assistant prefill turns from outbound replay so context-engine short circuits cannot send unsupported assistant-prefill payloads to provider APIs. Fixes #72556. Thanks @Veda-openclaw.
|
||||
|
||||
@@ -85,8 +85,8 @@ Notes:
|
||||
If `openclaw browser` is an unknown command, check `plugins.allow` in
|
||||
`~/.openclaw/openclaw.json`.
|
||||
|
||||
When `plugins.allow` is present, the bundled browser plugin must be listed
|
||||
explicitly:
|
||||
When `plugins.allow` is present, list the bundled browser plugin explicitly
|
||||
unless the config already has a root `browser` block:
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -96,8 +96,9 @@ explicitly:
|
||||
}
|
||||
```
|
||||
|
||||
`browser.enabled=true` does not restore the CLI subcommand when the plugin
|
||||
allowlist excludes `browser`.
|
||||
An explicit root `browser` block, for example `browser.enabled=true` or
|
||||
`browser.profiles.<name>`, also activates the bundled browser plugin under a
|
||||
restrictive plugin allowlist.
|
||||
|
||||
Related: [Browser tool](/tools/browser#missing-browser-command-or-tool)
|
||||
|
||||
|
||||
@@ -249,6 +249,7 @@ change correctness while legacy manifest ownership fallbacks still exist.
|
||||
"onCommands": ["models"],
|
||||
"onChannels": ["web"],
|
||||
"onRoutes": ["gateway-webhook"],
|
||||
"onConfigPaths": ["browser"],
|
||||
"onCapabilities": ["provider", "tool"]
|
||||
}
|
||||
}
|
||||
@@ -261,6 +262,7 @@ change correctness while legacy manifest ownership fallbacks still exist.
|
||||
| `onCommands` | No | `string[]` | Command ids that should include this plugin in activation/load plans. |
|
||||
| `onChannels` | No | `string[]` | Channel ids that should include this plugin in activation/load plans. |
|
||||
| `onRoutes` | No | `string[]` | Route kinds that should include this plugin in activation/load plans. |
|
||||
| `onConfigPaths` | No | `string[]` | Root-relative config paths that should include this plugin in startup/load plans when the path is present and not explicitly disabled. |
|
||||
| `onCapabilities` | No | `Array<"provider" \| "channel" \| "tool" \| "hook">` | Broad capability hints used by control-plane activation planning. Prefer narrower fields when possible. |
|
||||
|
||||
Current live consumers:
|
||||
@@ -271,6 +273,8 @@ Current live consumers:
|
||||
embedded harnesses and top-level `cliBackends[]` for CLI runtime aliases
|
||||
- channel-triggered setup/channel planning falls back to legacy `channels[]`
|
||||
ownership when explicit channel activation metadata is missing
|
||||
- startup plugin planning uses `activation.onConfigPaths` for non-channel root
|
||||
config surfaces such as the bundled browser plugin's `browser` block
|
||||
- provider-triggered setup/runtime planning falls back to legacy
|
||||
`providers[]` and top-level `cliBackends[]` ownership when explicit provider
|
||||
activation metadata is missing
|
||||
|
||||
@@ -104,7 +104,7 @@ turns do not pay the full token cost.
|
||||
|
||||
## Missing browser command or tool
|
||||
|
||||
If `openclaw browser` is unknown after an upgrade, `browser.request` is missing, or the agent reports the browser tool as unavailable, the usual cause is a `plugins.allow` list that omits `browser`. Add it:
|
||||
If `openclaw browser` is unknown after an upgrade, `browser.request` is missing, or the agent reports the browser tool as unavailable, the usual cause is a `plugins.allow` list that omits `browser` and no root `browser` config block exists. Add it:
|
||||
|
||||
```json5
|
||||
{
|
||||
@@ -114,7 +114,7 @@ If `openclaw browser` is unknown after an upgrade, `browser.request` is missing,
|
||||
}
|
||||
```
|
||||
|
||||
`browser.enabled=true`, `plugins.entries.browser.enabled=true`, and `tools.alsoAllow: ["browser"]` do not substitute for allowlist membership — the allowlist gates plugin loading, and tool policy only runs after load. Removing `plugins.allow` entirely also restores the default.
|
||||
An explicit root `browser` block, for example `browser.enabled=true` or `browser.profiles.<name>`, activates the bundled browser plugin even under a restrictive `plugins.allow`, matching channel config behavior. `plugins.entries.browser.enabled=true` and `tools.alsoAllow: ["browser"]` do not substitute for allowlist membership by themselves. Removing `plugins.allow` entirely also restores the default.
|
||||
|
||||
## Profiles: `openclaw` vs `user`
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{
|
||||
"id": "browser",
|
||||
"enabledByDefault": true,
|
||||
"activation": {
|
||||
"onConfigPaths": ["browser"]
|
||||
},
|
||||
"commandAliases": [{ "name": "browser" }],
|
||||
"skills": ["./skills"],
|
||||
"configSchema": {
|
||||
|
||||
@@ -3,8 +3,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
const applyPluginAutoEnable = vi.hoisted(() =>
|
||||
vi.fn((params: { config: unknown }) => ({
|
||||
config: params.config,
|
||||
changes: [],
|
||||
autoEnabledReasons: {},
|
||||
changes: [] as string[],
|
||||
autoEnabledReasons: {} as Record<string, string[]>,
|
||||
})),
|
||||
);
|
||||
const initSubagentRegistry = vi.hoisted(() => vi.fn());
|
||||
|
||||
@@ -128,21 +128,21 @@ export async function prepareGatewayPluginBootstrap(params: {
|
||||
|
||||
initSubagentRegistry();
|
||||
|
||||
const gatewayPluginConfigAtStart = params.minimalTestGateway
|
||||
const gatewayPluginConfig = params.minimalTestGateway
|
||||
? params.cfgAtStart
|
||||
: applyPluginAutoEnable({
|
||||
config: params.cfgAtStart,
|
||||
env: process.env,
|
||||
}).config;
|
||||
const defaultAgentId = resolveDefaultAgentId(gatewayPluginConfigAtStart);
|
||||
const defaultWorkspaceDir = resolveAgentWorkspaceDir(gatewayPluginConfigAtStart, defaultAgentId);
|
||||
const defaultAgentId = resolveDefaultAgentId(gatewayPluginConfig);
|
||||
const defaultWorkspaceDir = resolveAgentWorkspaceDir(gatewayPluginConfig, defaultAgentId);
|
||||
const pluginLookUpTable = params.minimalTestGateway
|
||||
? undefined
|
||||
: loadPluginLookUpTable({
|
||||
config: gatewayPluginConfigAtStart,
|
||||
activationSourceConfig: params.cfgAtStart,
|
||||
config: gatewayPluginConfig,
|
||||
workspaceDir: defaultWorkspaceDir,
|
||||
env: process.env,
|
||||
activationSourceConfig: params.cfgAtStart,
|
||||
});
|
||||
const deferredConfiguredChannelPluginIds = [
|
||||
...(pluginLookUpTable?.startup.configuredDeferredChannelPluginIds ?? []),
|
||||
@@ -156,12 +156,12 @@ export async function prepareGatewayPluginBootstrap(params: {
|
||||
|
||||
if (!params.minimalTestGateway) {
|
||||
await prestageGatewayBundledRuntimeDeps({
|
||||
cfg: gatewayPluginConfigAtStart,
|
||||
cfg: gatewayPluginConfig,
|
||||
pluginIds: startupPluginIds,
|
||||
log: params.log,
|
||||
});
|
||||
({ pluginRegistry, gatewayMethods: baseGatewayMethods } = loadGatewayStartupPlugins({
|
||||
cfg: gatewayPluginConfigAtStart,
|
||||
cfg: gatewayPluginConfig,
|
||||
activationSourceConfig: params.cfgAtStart,
|
||||
workspaceDir: defaultWorkspaceDir,
|
||||
log: params.log,
|
||||
@@ -178,7 +178,7 @@ export async function prepareGatewayPluginBootstrap(params: {
|
||||
}
|
||||
|
||||
return {
|
||||
gatewayPluginConfigAtStart,
|
||||
gatewayPluginConfigAtStart: gatewayPluginConfig,
|
||||
defaultWorkspaceDir,
|
||||
deferredConfiguredChannelPluginIds,
|
||||
startupPluginIds,
|
||||
|
||||
@@ -83,6 +83,9 @@ function createManifestRegistryFixture() {
|
||||
{
|
||||
id: "browser",
|
||||
channels: [],
|
||||
activation: {
|
||||
onConfigPaths: ["browser"],
|
||||
},
|
||||
origin: "bundled",
|
||||
enabledByDefault: true,
|
||||
providers: [],
|
||||
@@ -485,6 +488,62 @@ describe("resolveGatewayStartupPluginIds", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("starts bundled sidecars selected by root config activation paths", () => {
|
||||
const rawConfig = {
|
||||
browser: {
|
||||
enabled: true,
|
||||
defaultProfile: "docker-cdp",
|
||||
},
|
||||
channels: {},
|
||||
} satisfies OpenClawConfig;
|
||||
const effectiveConfig = {
|
||||
...rawConfig,
|
||||
plugins: {
|
||||
entries: {
|
||||
browser: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies OpenClawConfig;
|
||||
|
||||
expectStartupPluginIdsCase({
|
||||
config: effectiveConfig,
|
||||
activationSourceConfig: rawConfig,
|
||||
expected: ["browser", "memory-core"],
|
||||
});
|
||||
});
|
||||
|
||||
it("lets bundled root config activation paths bypass restrictive allowlists", () => {
|
||||
expectStartupPluginIdsCase({
|
||||
config: {
|
||||
browser: {
|
||||
enabled: true,
|
||||
},
|
||||
channels: {},
|
||||
plugins: {
|
||||
allow: ["telegram"],
|
||||
},
|
||||
},
|
||||
expected: ["browser"],
|
||||
});
|
||||
});
|
||||
|
||||
it("does not bypass restrictive allowlists for disabled root config activation paths", () => {
|
||||
expectStartupPluginIdsCase({
|
||||
config: {
|
||||
browser: {
|
||||
enabled: false,
|
||||
},
|
||||
channels: {},
|
||||
plugins: {
|
||||
allow: ["telegram"],
|
||||
},
|
||||
},
|
||||
expected: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("does not let weak channel presence start untrusted workspace channel owners", () => {
|
||||
loadPluginManifestRegistry
|
||||
.mockReset()
|
||||
|
||||
@@ -171,6 +171,17 @@ export const PLUGIN_COMPAT_RECORDS = [
|
||||
diagnostics: ["activation plan compat reason"],
|
||||
tests: ["src/plugins/activation-planner.test.ts"],
|
||||
},
|
||||
{
|
||||
code: "activation-config-path-hint",
|
||||
status: "active",
|
||||
owner: "plugin-execution",
|
||||
introduced: "2026-04-27",
|
||||
replacement: "manifest contribution ownership for root config surfaces",
|
||||
docsPath: "/plugins/manifest",
|
||||
surfaces: ["activation.onConfigPaths", "startup plugin selection"],
|
||||
diagnostics: ["activation plan compat reason"],
|
||||
tests: ["src/plugins/channel-plugin-ids.test.ts"],
|
||||
},
|
||||
{
|
||||
code: "activation-capability-hint",
|
||||
status: "active",
|
||||
|
||||
@@ -9,10 +9,11 @@ import {
|
||||
} from "../memory-host-sdk/dreaming.js";
|
||||
import { normalizeOptionalLowercaseString } from "../shared/string-coerce.js";
|
||||
import { hasExplicitChannelConfig } from "./channel-presence-policy.js";
|
||||
import { collectPluginConfigContractMatches } from "./config-contracts.js";
|
||||
import { resolveEffectivePluginActivationState } from "./config-state.js";
|
||||
import type { InstalledPluginIndexRecord } from "./installed-plugin-index.js";
|
||||
import { loadPluginManifestRegistryForInstalledIndex } from "./manifest-registry-installed.js";
|
||||
import type { PluginManifestRegistry } from "./manifest-registry.js";
|
||||
import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js";
|
||||
import {
|
||||
createPluginRegistryIdNormalizer,
|
||||
normalizePluginsConfigWithRegistry,
|
||||
@@ -39,6 +40,20 @@ function listDisabledChannelIds(config: OpenClawConfig): Set<string> {
|
||||
);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
||||
}
|
||||
|
||||
function isConfigActivationValueEnabled(value: unknown): boolean {
|
||||
if (value === false) {
|
||||
return false;
|
||||
}
|
||||
if (isRecord(value) && value.enabled === false) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function listPotentialEnabledChannelIds(config: OpenClawConfig, env: NodeJS.ProcessEnv): string[] {
|
||||
const disabled = listDisabledChannelIds(config);
|
||||
return listPotentialConfiguredChannelIds(config, env, { includePersistedAuthState: false })
|
||||
@@ -125,6 +140,60 @@ function listManifestChannelIds(
|
||||
return manifestRegistry.plugins.find((plugin) => plugin.id === pluginId)?.channels ?? [];
|
||||
}
|
||||
|
||||
function findManifestPlugin(
|
||||
manifestRegistry: PluginManifestRegistry,
|
||||
pluginId: string,
|
||||
): PluginManifestRecord | undefined {
|
||||
return manifestRegistry.plugins.find((plugin) => plugin.id === pluginId);
|
||||
}
|
||||
|
||||
function hasConfiguredActivationPath(params: {
|
||||
manifest: PluginManifestRecord | undefined;
|
||||
config: OpenClawConfig;
|
||||
}): boolean {
|
||||
const paths = params.manifest?.activation?.onConfigPaths;
|
||||
if (!paths?.length) {
|
||||
return false;
|
||||
}
|
||||
return paths.some((pathPattern) =>
|
||||
collectPluginConfigContractMatches({
|
||||
root: params.config,
|
||||
pathPattern,
|
||||
}).some((match) => isConfigActivationValueEnabled(match.value)),
|
||||
);
|
||||
}
|
||||
|
||||
function canStartConfiguredRootPlugin(params: {
|
||||
plugin: InstalledPluginIndexRecord;
|
||||
manifest: PluginManifestRecord | undefined;
|
||||
config: OpenClawConfig;
|
||||
pluginsConfig: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
||||
activationSourcePlugins: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
||||
}): boolean {
|
||||
if (params.plugin.origin !== "bundled") {
|
||||
return false;
|
||||
}
|
||||
if (!hasConfiguredActivationPath({ manifest: params.manifest, config: params.config })) {
|
||||
return false;
|
||||
}
|
||||
if (!params.pluginsConfig.enabled || !params.activationSourcePlugins.enabled) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
params.pluginsConfig.deny.includes(params.plugin.pluginId) ||
|
||||
params.activationSourcePlugins.deny.includes(params.plugin.pluginId)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
params.pluginsConfig.entries[params.plugin.pluginId]?.enabled === false ||
|
||||
params.activationSourcePlugins.entries[params.plugin.pluginId]?.enabled === false
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function canStartConfiguredChannelPlugin(params: {
|
||||
plugin: InstalledPluginIndexRecord;
|
||||
config: OpenClawConfig;
|
||||
@@ -301,6 +370,7 @@ export function resolveGatewayStartupPluginIdsFromRegistry(params: {
|
||||
});
|
||||
return params.index.plugins
|
||||
.filter((plugin) => {
|
||||
const manifest = findManifestPlugin(params.manifestRegistry, plugin.pluginId);
|
||||
if (
|
||||
hasConfiguredStartupChannel({
|
||||
plugin,
|
||||
@@ -338,6 +408,17 @@ export function resolveGatewayStartupPluginIdsFromRegistry(params: {
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
canStartConfiguredRootPlugin({
|
||||
plugin,
|
||||
manifest,
|
||||
config: activationSourceConfig,
|
||||
pluginsConfig,
|
||||
activationSourcePlugins,
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
const activationState = resolveEffectivePluginActivationState({
|
||||
id: plugin.pluginId,
|
||||
origin: plugin.origin,
|
||||
|
||||
@@ -86,6 +86,9 @@ function collectCompatCodes(record: PluginManifestRecord): readonly PluginCompat
|
||||
if (record.activation?.onRoutes?.length) {
|
||||
codes.push("activation-route-hint");
|
||||
}
|
||||
if (record.activation?.onConfigPaths?.length) {
|
||||
codes.push("activation-config-path-hint");
|
||||
}
|
||||
if (record.activation?.onCapabilities?.length) {
|
||||
codes.push("activation-capability-hint");
|
||||
}
|
||||
|
||||
@@ -111,6 +111,7 @@ describe("loadPluginManifest JSON5 tolerance", () => {
|
||||
onCommands: ["models", ""],
|
||||
onChannels: ["web", ""],
|
||||
onRoutes: ["gateway-webhook", ""],
|
||||
onConfigPaths: ["browser", ""],
|
||||
onCapabilities: ["provider", "tool", "wat"]
|
||||
},
|
||||
setup: {
|
||||
@@ -133,6 +134,7 @@ describe("loadPluginManifest JSON5 tolerance", () => {
|
||||
onCommands: ["models"],
|
||||
onChannels: ["web"],
|
||||
onRoutes: ["gateway-webhook"],
|
||||
onConfigPaths: ["browser"],
|
||||
onCapabilities: ["provider", "tool"],
|
||||
});
|
||||
expect(result.manifest.setup).toEqual({
|
||||
|
||||
@@ -119,6 +119,8 @@ export type PluginManifestActivation = {
|
||||
onChannels?: string[];
|
||||
/** Route kinds that should include this plugin in activation/load plans. */
|
||||
onRoutes?: string[];
|
||||
/** Root-relative config paths that should include this plugin in startup/load plans. */
|
||||
onConfigPaths?: string[];
|
||||
/** Broad capability hints for activation/load plans. Prefer narrower ownership metadata. */
|
||||
onCapabilities?: PluginManifestActivationCapability[];
|
||||
};
|
||||
@@ -740,6 +742,7 @@ function normalizeManifestActivation(value: unknown): PluginManifestActivation |
|
||||
const onCommands = normalizeTrimmedStringList(value.onCommands);
|
||||
const onChannels = normalizeTrimmedStringList(value.onChannels);
|
||||
const onRoutes = normalizeTrimmedStringList(value.onRoutes);
|
||||
const onConfigPaths = normalizeTrimmedStringList(value.onConfigPaths);
|
||||
const onCapabilities = normalizeTrimmedStringList(value.onCapabilities).filter(
|
||||
(capability): capability is PluginManifestActivationCapability =>
|
||||
capability === "provider" ||
|
||||
@@ -754,6 +757,7 @@ function normalizeManifestActivation(value: unknown): PluginManifestActivation |
|
||||
...(onCommands.length > 0 ? { onCommands } : {}),
|
||||
...(onChannels.length > 0 ? { onChannels } : {}),
|
||||
...(onRoutes.length > 0 ? { onRoutes } : {}),
|
||||
...(onConfigPaths.length > 0 ? { onConfigPaths } : {}),
|
||||
...(onCapabilities.length > 0 ? { onCapabilities } : {}),
|
||||
} satisfies PluginManifestActivation;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import fs from "node:fs";
|
||||
import {
|
||||
inspectPersistedInstalledPluginIndex,
|
||||
readPersistedInstalledPluginIndexSync,
|
||||
@@ -25,7 +26,8 @@ export type PluginRegistrySnapshotSource = "provided" | "persisted" | "derived";
|
||||
export type PluginRegistrySnapshotDiagnosticCode =
|
||||
| "persisted-registry-disabled"
|
||||
| "persisted-registry-missing"
|
||||
| "persisted-registry-stale-policy";
|
||||
| "persisted-registry-stale-policy"
|
||||
| "persisted-registry-stale-source";
|
||||
|
||||
export type PluginRegistrySnapshotDiagnostic = {
|
||||
level: "info" | "warn";
|
||||
@@ -60,6 +62,20 @@ function hasEnvFlag(env: NodeJS.ProcessEnv, name: string): boolean {
|
||||
return Boolean(value && value !== "0" && value !== "false" && value !== "no");
|
||||
}
|
||||
|
||||
function hasMissingPersistedPluginSource(index: InstalledPluginIndex): boolean {
|
||||
return index.plugins.some((plugin) => {
|
||||
if (!plugin.enabled) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
!fs.existsSync(plugin.rootDir) ||
|
||||
!fs.existsSync(plugin.manifestPath) ||
|
||||
(plugin.source ? !fs.existsSync(plugin.source) : false) ||
|
||||
(plugin.setupSource ? !fs.existsSync(plugin.setupSource) : false)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function loadPluginRegistrySnapshotWithMetadata(
|
||||
params: LoadPluginRegistryParams = {},
|
||||
): PluginRegistrySnapshotResult {
|
||||
@@ -91,6 +107,13 @@ export function loadPluginRegistrySnapshotWithMetadata(
|
||||
message:
|
||||
"Persisted plugin registry policy does not match current config; using derived plugin index. Run `openclaw plugins registry --refresh` to update the persisted registry.",
|
||||
});
|
||||
} else if (hasMissingPersistedPluginSource(persistedIndex)) {
|
||||
diagnostics.push({
|
||||
level: "warn",
|
||||
code: "persisted-registry-stale-source",
|
||||
message:
|
||||
"Persisted plugin registry points at missing plugin files; using derived plugin index. Run `openclaw plugins registry --refresh` to update the persisted registry.",
|
||||
});
|
||||
} else {
|
||||
return {
|
||||
snapshot: persistedIndex,
|
||||
|
||||
@@ -109,6 +109,7 @@ function createIndex(
|
||||
pluginId = "demo",
|
||||
overrides: Partial<InstalledPluginIndex> = {},
|
||||
): InstalledPluginIndex {
|
||||
const pluginRoot = overrides.plugins?.[0]?.rootDir ?? `/plugins/${pluginId}`;
|
||||
return {
|
||||
version: 1,
|
||||
hostContractVersion: "2026.4.25",
|
||||
@@ -120,9 +121,9 @@ function createIndex(
|
||||
plugins: [
|
||||
{
|
||||
pluginId,
|
||||
manifestPath: `/plugins/${pluginId}/openclaw.plugin.json`,
|
||||
manifestPath: path.join(pluginRoot, "openclaw.plugin.json"),
|
||||
manifestHash: "manifest-hash",
|
||||
rootDir: `/plugins/${pluginId}`,
|
||||
rootDir: pluginRoot,
|
||||
origin: "global",
|
||||
enabled: true,
|
||||
startup: {
|
||||
@@ -359,6 +360,47 @@ describe("plugin registry facade", () => {
|
||||
});
|
||||
|
||||
it("reads the persisted registry before deriving from discovered candidates", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const rootDir = makeTempDir();
|
||||
const persistedRootDir = makeTempDir();
|
||||
const candidate = createCandidate(rootDir);
|
||||
const config = {} as const;
|
||||
fs.writeFileSync(path.join(persistedRootDir, "index.ts"), "", "utf8");
|
||||
fs.writeFileSync(
|
||||
path.join(persistedRootDir, "openclaw.plugin.json"),
|
||||
JSON.stringify({ id: "persisted", configSchema: { type: "object" } }),
|
||||
"utf8",
|
||||
);
|
||||
await writePersistedInstalledPluginIndex(
|
||||
createIndex("persisted", {
|
||||
policyHash: resolveInstalledPluginIndexPolicyHash(config),
|
||||
plugins: [
|
||||
{
|
||||
...createIndex("persisted").plugins[0],
|
||||
manifestPath: path.join(persistedRootDir, "openclaw.plugin.json"),
|
||||
source: path.join(persistedRootDir, "index.ts"),
|
||||
rootDir: persistedRootDir,
|
||||
},
|
||||
],
|
||||
}),
|
||||
{ stateDir },
|
||||
);
|
||||
|
||||
const result = loadPluginRegistrySnapshotWithMetadata({
|
||||
stateDir,
|
||||
candidates: [candidate],
|
||||
config,
|
||||
env: hermeticEnv(),
|
||||
});
|
||||
|
||||
expect(result.source).toBe("persisted");
|
||||
expect(result.diagnostics).toEqual([]);
|
||||
expect(listPluginRecords({ index: result.snapshot }).map((plugin) => plugin.pluginId)).toEqual([
|
||||
"persisted",
|
||||
]);
|
||||
});
|
||||
|
||||
it("falls back to the derived registry when persisted source paths are missing", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const rootDir = makeTempDir();
|
||||
const candidate = createCandidate(rootDir);
|
||||
@@ -377,10 +419,12 @@ describe("plugin registry facade", () => {
|
||||
env: hermeticEnv(),
|
||||
});
|
||||
|
||||
expect(result.source).toBe("persisted");
|
||||
expect(result.diagnostics).toEqual([]);
|
||||
expect(result.source).toBe("derived");
|
||||
expect(result.diagnostics).toEqual([
|
||||
expect.objectContaining({ code: "persisted-registry-stale-source" }),
|
||||
]);
|
||||
expect(listPluginRecords({ index: result.snapshot }).map((plugin) => plugin.pluginId)).toEqual([
|
||||
"persisted",
|
||||
"demo",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user