mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 20:17:28 +00:00
This commit is contained in:
committed by
GitHub
parent
4c7838e3cf
commit
4f043991e0
@@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
- Security: fix Chutes manual OAuth login state validation (thanks @aether-ai-agent). (#16058)
|
||||
- macOS: hard-limit unkeyed `openclaw://agent` deep links and ignore `deliver` / `to` / `channel` unless a valid unattended key is provided. Thanks @Cillian-Collins.
|
||||
- Plugins: suppress false duplicate plugin id warnings when the same extension is discovered via multiple paths (config/workspace/global vs bundled), while still warning on genuine duplicates. (#16222) Thanks @shadril238.
|
||||
- Security/Google Chat: deprecate `users/<email>` allowlists (treat `users/...` as immutable user id only); keep raw email allowlists for usability. Thanks @vincentkoc.
|
||||
- Security/Archive: enforce archive extraction entry/size limits to prevent resource exhaustion from high-expansion ZIP/TAR archives. Thanks @vincentkoc.
|
||||
|
||||
|
||||
@@ -20,7 +20,11 @@ function writeManifest(dir: string, manifest: Record<string, unknown>) {
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
while (tempDirs.length > 0) {
|
||||
const dir = tempDirs.pop();
|
||||
if (!dir) {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
} catch {
|
||||
@@ -135,4 +139,41 @@ describe("loadPluginManifestRegistry", () => {
|
||||
);
|
||||
expect(duplicateWarnings.length).toBe(0);
|
||||
});
|
||||
|
||||
it("prefers higher-precedence origins for the same physical directory (config > workspace > global > bundled)", () => {
|
||||
const dir = makeTempDir();
|
||||
fs.mkdirSync(path.join(dir, "sub"), { recursive: true });
|
||||
const manifest = { id: "precedence-plugin", configSchema: { type: "object" } };
|
||||
writeManifest(dir, manifest);
|
||||
|
||||
// Use a different-but-equivalent path representation without requiring symlinks.
|
||||
const altDir = path.join(dir, "sub", "..");
|
||||
|
||||
const candidates: PluginCandidate[] = [
|
||||
{
|
||||
idHint: "precedence-plugin",
|
||||
source: path.join(dir, "index.ts"),
|
||||
rootDir: dir,
|
||||
origin: "bundled",
|
||||
},
|
||||
{
|
||||
idHint: "precedence-plugin",
|
||||
source: path.join(altDir, "index.ts"),
|
||||
rootDir: altDir,
|
||||
origin: "config",
|
||||
},
|
||||
];
|
||||
|
||||
const registry = loadPluginManifestRegistry({
|
||||
candidates,
|
||||
cache: false,
|
||||
});
|
||||
|
||||
const duplicateWarnings = registry.diagnostics.filter(
|
||||
(d) => d.level === "warn" && d.message?.includes("duplicate plugin id"),
|
||||
);
|
||||
expect(duplicateWarnings.length).toBe(0);
|
||||
expect(registry.plugins.length).toBe(1);
|
||||
expect(registry.plugins[0]?.origin).toBe("config");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,6 +6,25 @@ import { normalizePluginsConfig, type NormalizedPluginsConfig } from "./config-s
|
||||
import { discoverOpenClawPlugins, type PluginCandidate } from "./discovery.js";
|
||||
import { loadPluginManifest, type PluginManifest } from "./manifest.js";
|
||||
|
||||
type SeenIdEntry = {
|
||||
candidate: PluginCandidate;
|
||||
recordIndex: number;
|
||||
};
|
||||
|
||||
function pluginOriginRank(origin: PluginOrigin): number {
|
||||
// Precedence: config > workspace > global > bundled
|
||||
switch (origin) {
|
||||
case "config":
|
||||
return 0;
|
||||
case "workspace":
|
||||
return 1;
|
||||
case "global":
|
||||
return 2;
|
||||
case "bundled":
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
export type PluginManifestRecord = {
|
||||
id: string;
|
||||
name?: string;
|
||||
@@ -138,7 +157,7 @@ export function loadPluginManifestRegistry(params: {
|
||||
const diagnostics: PluginDiagnostic[] = [...discovery.diagnostics];
|
||||
const candidates: PluginCandidate[] = discovery.candidates;
|
||||
const records: PluginManifestRecord[] = [];
|
||||
const seenIds = new Map<string, PluginCandidate>();
|
||||
const seenIds = new Map<string, SeenIdEntry>();
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const manifestRes = loadPluginManifest(candidate.rootDir);
|
||||
@@ -161,19 +180,37 @@ export function loadPluginManifestRegistry(params: {
|
||||
});
|
||||
}
|
||||
|
||||
const existingCandidate = seenIds.get(manifest.id);
|
||||
if (existingCandidate) {
|
||||
const configSchema = manifest.configSchema;
|
||||
const manifestMtime = safeStatMtimeMs(manifestRes.manifestPath);
|
||||
const schemaCacheKey = manifestMtime
|
||||
? `${manifestRes.manifestPath}:${manifestMtime}`
|
||||
: manifestRes.manifestPath;
|
||||
|
||||
const existing = seenIds.get(manifest.id);
|
||||
if (existing) {
|
||||
// Check whether both candidates point to the same physical directory
|
||||
// (e.g. via symlinks or different path representations). If so, this
|
||||
// is a false-positive duplicate and can be silently skipped.
|
||||
let samePlugin = false;
|
||||
try {
|
||||
samePlugin =
|
||||
fs.realpathSync(existingCandidate.rootDir) === fs.realpathSync(candidate.rootDir);
|
||||
fs.realpathSync(existing.candidate.rootDir) === fs.realpathSync(candidate.rootDir);
|
||||
} catch {
|
||||
// If either path is inaccessible, fall through to duplicate warning
|
||||
}
|
||||
if (samePlugin) {
|
||||
// Prefer higher-precedence origins even if candidates are passed in
|
||||
// an unexpected order (config > workspace > global > bundled).
|
||||
if (pluginOriginRank(candidate.origin) < pluginOriginRank(existing.candidate.origin)) {
|
||||
records[existing.recordIndex] = buildRecord({
|
||||
manifest,
|
||||
candidate,
|
||||
manifestPath: manifestRes.manifestPath,
|
||||
schemaCacheKey,
|
||||
configSchema,
|
||||
});
|
||||
seenIds.set(manifest.id, { candidate, recordIndex: existing.recordIndex });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
diagnostics.push({
|
||||
@@ -183,15 +220,9 @@ export function loadPluginManifestRegistry(params: {
|
||||
message: `duplicate plugin id detected; later plugin may be overridden (${candidate.source})`,
|
||||
});
|
||||
} else {
|
||||
seenIds.set(manifest.id, candidate);
|
||||
seenIds.set(manifest.id, { candidate, recordIndex: records.length });
|
||||
}
|
||||
|
||||
const configSchema = manifest.configSchema;
|
||||
const manifestMtime = safeStatMtimeMs(manifestRes.manifestPath);
|
||||
const schemaCacheKey = manifestMtime
|
||||
? `${manifestRes.manifestPath}:${manifestMtime}`
|
||||
: manifestRes.manifestPath;
|
||||
|
||||
records.push(
|
||||
buildRecord({
|
||||
manifest,
|
||||
|
||||
Reference in New Issue
Block a user