mirror of
https://github.com/farcasclaudiu/openclaw.git
synced 2026-06-29 13:02:10 +03: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)
|
- 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.
|
- 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/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.
|
- 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(() => {
|
afterEach(() => {
|
||||||
for (const dir of tempDirs.splice(0)) {
|
while (tempDirs.length > 0) {
|
||||||
|
const dir = tempDirs.pop();
|
||||||
|
if (!dir) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
fs.rmSync(dir, { recursive: true, force: true });
|
fs.rmSync(dir, { recursive: true, force: true });
|
||||||
} catch {
|
} catch {
|
||||||
@@ -135,4 +139,41 @@ describe("loadPluginManifestRegistry", () => {
|
|||||||
);
|
);
|
||||||
expect(duplicateWarnings.length).toBe(0);
|
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 { discoverOpenClawPlugins, type PluginCandidate } from "./discovery.js";
|
||||||
import { loadPluginManifest, type PluginManifest } from "./manifest.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 = {
|
export type PluginManifestRecord = {
|
||||||
id: string;
|
id: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -138,7 +157,7 @@ export function loadPluginManifestRegistry(params: {
|
|||||||
const diagnostics: PluginDiagnostic[] = [...discovery.diagnostics];
|
const diagnostics: PluginDiagnostic[] = [...discovery.diagnostics];
|
||||||
const candidates: PluginCandidate[] = discovery.candidates;
|
const candidates: PluginCandidate[] = discovery.candidates;
|
||||||
const records: PluginManifestRecord[] = [];
|
const records: PluginManifestRecord[] = [];
|
||||||
const seenIds = new Map<string, PluginCandidate>();
|
const seenIds = new Map<string, SeenIdEntry>();
|
||||||
|
|
||||||
for (const candidate of candidates) {
|
for (const candidate of candidates) {
|
||||||
const manifestRes = loadPluginManifest(candidate.rootDir);
|
const manifestRes = loadPluginManifest(candidate.rootDir);
|
||||||
@@ -161,19 +180,37 @@ export function loadPluginManifestRegistry(params: {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingCandidate = seenIds.get(manifest.id);
|
const configSchema = manifest.configSchema;
|
||||||
if (existingCandidate) {
|
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
|
// Check whether both candidates point to the same physical directory
|
||||||
// (e.g. via symlinks or different path representations). If so, this
|
// (e.g. via symlinks or different path representations). If so, this
|
||||||
// is a false-positive duplicate and can be silently skipped.
|
// is a false-positive duplicate and can be silently skipped.
|
||||||
let samePlugin = false;
|
let samePlugin = false;
|
||||||
try {
|
try {
|
||||||
samePlugin =
|
samePlugin =
|
||||||
fs.realpathSync(existingCandidate.rootDir) === fs.realpathSync(candidate.rootDir);
|
fs.realpathSync(existing.candidate.rootDir) === fs.realpathSync(candidate.rootDir);
|
||||||
} catch {
|
} catch {
|
||||||
// If either path is inaccessible, fall through to duplicate warning
|
// If either path is inaccessible, fall through to duplicate warning
|
||||||
}
|
}
|
||||||
if (samePlugin) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
diagnostics.push({
|
diagnostics.push({
|
||||||
@@ -183,15 +220,9 @@ export function loadPluginManifestRegistry(params: {
|
|||||||
message: `duplicate plugin id detected; later plugin may be overridden (${candidate.source})`,
|
message: `duplicate plugin id detected; later plugin may be overridden (${candidate.source})`,
|
||||||
});
|
});
|
||||||
} else {
|
} 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(
|
records.push(
|
||||||
buildRecord({
|
buildRecord({
|
||||||
manifest,
|
manifest,
|
||||||
|
|||||||
Reference in New Issue
Block a user