mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:00:43 +00:00
fix(plugins): canonicalize install provenance paths
This commit is contained in:
@@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Plugins/externalization: add official npm-first catalogs for externalized channel, provider, and generic plugins, keep unpublished ACPX/Google Chat/LINE bundled, and make missing-plugin repair honor npm-first metadata while ClawHub pack files roll out. Thanks @vincentkoc.
|
||||
- Plugins/update: detect tracked plugin install records whose package directories disappeared during `openclaw update`, reinstall them before normal plugin updates, and fail the update if any install record still points at missing disk payloads.
|
||||
- Plugins/registry: hash manifest and package metadata when validating persisted plugin registries so fast same-size rewrites cannot leave stale plugin metadata trusted.
|
||||
- Plugins/registry: canonicalize install-record provenance paths before trust diagnostics, so npm plugins installed under symlinked temp/state roots no longer warn as untracked local code.
|
||||
- CLI/infer: reject local `codex/*` one-shot model probes before simple-completion dispatch and point operators at the Codex app-server runtime path instead of ending with an empty-output error.
|
||||
- Agents/sessions: preserve terminal lifecycle state when final run metadata persists from a stale in-memory snapshot, preventing `main` sessions from staying stuck as running after completed or timed-out turns.
|
||||
- Gateway/CLI: make `openclaw gateway start` repair stale managed service definitions that point at old OpenClaw versions, missing binaries, or temporary installer paths before starting.
|
||||
|
||||
@@ -3,7 +3,7 @@ import { resolveUserPath } from "../utils.js";
|
||||
import type { PluginCandidate } from "./discovery.js";
|
||||
import { loadInstalledPluginIndexInstallRecordsSync } from "./installed-plugin-index-records.js";
|
||||
import type { PluginManifestRecord } from "./manifest-registry.js";
|
||||
import { isPathInside, safeStatSync } from "./path-safety.js";
|
||||
import { isPathInside, safeRealpathSync, safeStatSync } from "./path-safety.js";
|
||||
import type { PluginRecord, PluginRegistry } from "./registry.js";
|
||||
import type { PluginLogger } from "./types.js";
|
||||
|
||||
@@ -44,15 +44,16 @@ function addPathToMatcher(
|
||||
if (!resolved) {
|
||||
return;
|
||||
}
|
||||
if (matcher.exact.has(resolved) || matcher.dirs.includes(resolved)) {
|
||||
const canonical = safeRealpathSync(resolved) ?? resolved;
|
||||
if (matcher.exact.has(canonical) || matcher.dirs.includes(canonical)) {
|
||||
return;
|
||||
}
|
||||
const stat = safeStatSync(resolved);
|
||||
const stat = safeStatSync(canonical);
|
||||
if (stat?.isDirectory()) {
|
||||
matcher.dirs.push(resolved);
|
||||
matcher.dirs.push(canonical);
|
||||
return;
|
||||
}
|
||||
matcher.exact.add(resolved);
|
||||
matcher.exact.add(canonical);
|
||||
}
|
||||
|
||||
function matchesPathMatcher(matcher: PathMatcher, sourcePath: string): boolean {
|
||||
@@ -101,16 +102,17 @@ function isTrackedByProvenance(params: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): boolean {
|
||||
const sourcePath = resolveUserPath(params.source, params.env);
|
||||
const canonicalSourcePath = safeRealpathSync(sourcePath) ?? sourcePath;
|
||||
const installRule = params.index.installRules.get(params.pluginId);
|
||||
if (installRule) {
|
||||
if (installRule.trackedWithoutPaths) {
|
||||
return true;
|
||||
}
|
||||
if (matchesPathMatcher(installRule.matcher, sourcePath)) {
|
||||
if (matchesPathMatcher(installRule.matcher, canonicalSourcePath)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return matchesPathMatcher(params.index.loadPathMatcher, sourcePath);
|
||||
return matchesPathMatcher(params.index.loadPathMatcher, canonicalSourcePath);
|
||||
}
|
||||
|
||||
function matchesExplicitInstallRule(params: {
|
||||
@@ -120,11 +122,12 @@ function matchesExplicitInstallRule(params: {
|
||||
env: NodeJS.ProcessEnv;
|
||||
}): boolean {
|
||||
const sourcePath = resolveUserPath(params.source, params.env);
|
||||
const canonicalSourcePath = safeRealpathSync(sourcePath) ?? sourcePath;
|
||||
const installRule = params.index.installRules.get(params.pluginId);
|
||||
if (!installRule || installRule.trackedWithoutPaths) {
|
||||
return false;
|
||||
}
|
||||
return matchesPathMatcher(installRule.matcher, sourcePath);
|
||||
return matchesPathMatcher(installRule.matcher, canonicalSourcePath);
|
||||
}
|
||||
|
||||
function resolveCandidateDuplicateRank(params: {
|
||||
|
||||
@@ -6273,6 +6273,74 @@ module.exports = {
|
||||
};
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "does not warn when install paths resolve through a symlinked state root",
|
||||
loadRegistry: () => {
|
||||
useNoBundledPlugins();
|
||||
const stateDir = makeTempDir();
|
||||
const realHome = path.join(stateDir, "real-home");
|
||||
const linkedHome = path.join(stateDir, "linked-home");
|
||||
mkdirSafe(realHome);
|
||||
fs.symlinkSync(realHome, linkedHome, process.platform === "win32" ? "junction" : "dir");
|
||||
|
||||
const pluginDir = path.join(
|
||||
realHome,
|
||||
".openclaw",
|
||||
"npm",
|
||||
"node_modules",
|
||||
"@example",
|
||||
"tracked-symlink-install",
|
||||
);
|
||||
mkdirSafe(pluginDir);
|
||||
const plugin = writePlugin({
|
||||
id: "tracked-symlink-install",
|
||||
body: simplePluginBody("tracked-symlink-install"),
|
||||
dir: pluginDir,
|
||||
filename: "index.cjs",
|
||||
});
|
||||
writePersistedInstalledPluginIndexInstallRecordsSync(
|
||||
{
|
||||
[plugin.id]: {
|
||||
source: "npm",
|
||||
spec: "@example/tracked-symlink-install@1.0.0",
|
||||
installPath: path.join(
|
||||
linkedHome,
|
||||
".openclaw",
|
||||
"npm",
|
||||
"node_modules",
|
||||
"@example",
|
||||
"tracked-symlink-install",
|
||||
),
|
||||
version: "1.0.0",
|
||||
},
|
||||
},
|
||||
{ stateDir },
|
||||
);
|
||||
|
||||
const warnings: string[] = [];
|
||||
const registry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
logger: createWarningLogger(warnings),
|
||||
env: {
|
||||
...process.env,
|
||||
OPENCLAW_STATE_DIR: stateDir,
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins",
|
||||
},
|
||||
config: {
|
||||
plugins: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
registry,
|
||||
warnings,
|
||||
pluginId: plugin.id,
|
||||
expectWarning: false,
|
||||
};
|
||||
},
|
||||
},
|
||||
] as const;
|
||||
|
||||
runScenarioCases(scenarios, (scenario) => {
|
||||
|
||||
Reference in New Issue
Block a user