























@@ -1427,6 +1427,243 @@ describe("loadPluginManifestRegistry", () => {
14271427);
14281428});
142914291430+it("ignores legacy provider discovery entries outside the plugin root", () => {
1431+const root = makeTempDir();
1432+const pluginDir = path.join(root, "plugin");
1433+const outsideDir = path.join(root, "outside");
1434+mkdirSafe(pluginDir);
1435+mkdirSafe(outsideDir);
1436+writeManifest(pluginDir, {
1437+id: "outside-provider",
1438+providers: ["outside-provider"],
1439+providerDiscoveryEntry: "../outside/provider-discovery.js",
1440+configSchema: { type: "object" },
1441+});
1442+fs.writeFileSync(
1443+path.join(outsideDir, "provider-discovery.js"),
1444+"export default {};\n",
1445+"utf8",
1446+);
1447+1448+const registry = loadSingleCandidateRegistry({
1449+idHint: "outside-provider",
1450+rootDir: pluginDir,
1451+origin: "bundled",
1452+});
1453+1454+expect(registry.plugins[0]?.providerDiscoverySource).toBeUndefined();
1455+expectDiagnosticFields(registry, {
1456+level: "warn",
1457+pluginId: "outside-provider",
1458+source: path.join(pluginDir, "openclaw.plugin.json"),
1459+messageIncludes: "providerDiscoveryEntry must resolve inside the plugin root",
1460+});
1461+});
1462+1463+it("ignores absolute provider discovery entries", () => {
1464+const dir = makeTempDir();
1465+const outsideDir = makeTempDir();
1466+const outsideEntry = path.join(outsideDir, "provider-discovery.js");
1467+fs.writeFileSync(outsideEntry, "export default {};\n", "utf8");
1468+writeManifest(dir, {
1469+id: "absolute-provider",
1470+providers: ["absolute-provider"],
1471+providerDiscoveryEntry: outsideEntry,
1472+configSchema: { type: "object" },
1473+});
1474+1475+const registry = loadSingleCandidateRegistry({
1476+idHint: "absolute-provider",
1477+rootDir: dir,
1478+origin: "bundled",
1479+});
1480+1481+expect(registry.plugins[0]?.providerDiscoverySource).toBeUndefined();
1482+expectDiagnosticFields(registry, {
1483+level: "warn",
1484+pluginId: "absolute-provider",
1485+source: path.join(dir, "openclaw.plugin.json"),
1486+messageIncludes: "providerDiscoveryEntry must resolve inside the plugin root",
1487+});
1488+});
1489+1490+it("ignores provider catalog entries that resolve outside the plugin root", () => {
1491+const dir = makeTempDir();
1492+const outsideDir = makeTempDir();
1493+const outsideEntry = path.join(outsideDir, "provider-catalog.js");
1494+fs.writeFileSync(outsideEntry, "export default {};\n", "utf8");
1495+writeManifest(dir, {
1496+id: "absolute-catalog",
1497+providers: ["absolute-catalog"],
1498+providerCatalogEntry: outsideEntry,
1499+configSchema: { type: "object" },
1500+});
1501+1502+const registry = loadSingleCandidateRegistry({
1503+idHint: "absolute-catalog",
1504+rootDir: dir,
1505+origin: "bundled",
1506+});
1507+1508+expect(registry.plugins[0]?.providerDiscoverySource).toBeUndefined();
1509+expectDiagnosticFields(registry, {
1510+level: "warn",
1511+pluginId: "absolute-catalog",
1512+source: path.join(dir, "openclaw.plugin.json"),
1513+messageIncludes: "providerCatalogEntry must resolve inside the plugin root",
1514+});
1515+});
1516+1517+it("ignores provider discovery entries that resolve through a symlink outside the plugin root", () => {
1518+if (process.platform === "win32") {
1519+return;
1520+}
1521+const dir = makeTempDir();
1522+const outsideDir = makeTempDir();
1523+const outsideEntry = path.join(outsideDir, "provider-discovery.js");
1524+const linkedEntry = path.join(dir, "provider-discovery.js");
1525+fs.writeFileSync(outsideEntry, "export default {};\n", "utf8");
1526+try {
1527+fs.symlinkSync(outsideEntry, linkedEntry);
1528+} catch {
1529+return;
1530+}
1531+writeManifest(dir, {
1532+id: "symlink-provider",
1533+providers: ["symlink-provider"],
1534+providerDiscoveryEntry: "./provider-discovery.js",
1535+configSchema: { type: "object" },
1536+});
1537+1538+const registry = loadSingleCandidateRegistry({
1539+idHint: "symlink-provider",
1540+rootDir: dir,
1541+origin: "bundled",
1542+});
1543+1544+expect(registry.plugins[0]?.providerDiscoverySource).toBeUndefined();
1545+expectDiagnosticFields(registry, {
1546+level: "warn",
1547+pluginId: "symlink-provider",
1548+source: path.join(dir, "openclaw.plugin.json"),
1549+messageIncludes: "providerDiscoveryEntry must resolve inside the plugin root",
1550+});
1551+});
1552+1553+it("ignores provider discovery .js fallbacks that resolve outside the plugin root", () => {
1554+if (process.platform === "win32") {
1555+return;
1556+}
1557+const dir = makeTempDir();
1558+const outsideDir = makeTempDir();
1559+const outsideEntry = path.join(outsideDir, "provider-discovery.js");
1560+const linkedEntry = path.join(dir, "provider-discovery.js");
1561+fs.writeFileSync(outsideEntry, "export default {};\n", "utf8");
1562+try {
1563+fs.symlinkSync(outsideEntry, linkedEntry);
1564+} catch {
1565+return;
1566+}
1567+writeManifest(dir, {
1568+id: "fallback-symlink-provider",
1569+providers: ["fallback-symlink-provider"],
1570+providerDiscoveryEntry: "./provider-discovery.ts",
1571+configSchema: { type: "object" },
1572+});
1573+1574+const registry = loadSingleCandidateRegistry({
1575+idHint: "fallback-symlink-provider",
1576+rootDir: dir,
1577+origin: "bundled",
1578+});
1579+1580+expect(registry.plugins[0]?.providerDiscoverySource).toBeUndefined();
1581+expectDiagnosticFields(registry, {
1582+level: "warn",
1583+pluginId: "fallback-symlink-provider",
1584+source: path.join(dir, "openclaw.plugin.json"),
1585+messageIncludes: "providerDiscoveryEntry must resolve inside the plugin root",
1586+});
1587+});
1588+1589+it("ignores non-bundled provider discovery entries that are hardlinked", () => {
1590+if (process.platform === "win32") {
1591+return;
1592+}
1593+const dir = makeTempDir();
1594+const outsideDir = makeTempDir();
1595+const outsideEntry = path.join(outsideDir, "provider-discovery.js");
1596+const linkedEntry = path.join(dir, "provider-discovery.js");
1597+fs.writeFileSync(outsideEntry, "export default {};\n", "utf8");
1598+try {
1599+fs.linkSync(outsideEntry, linkedEntry);
1600+} catch (err) {
1601+if ((err as NodeJS.ErrnoException).code === "EXDEV") {
1602+return;
1603+}
1604+throw err;
1605+}
1606+writeManifest(dir, {
1607+id: "hardlink-provider",
1608+providers: ["hardlink-provider"],
1609+providerDiscoveryEntry: "./provider-discovery.js",
1610+configSchema: { type: "object" },
1611+});
1612+1613+const registry = loadSingleCandidateRegistry({
1614+idHint: "hardlink-provider",
1615+rootDir: dir,
1616+origin: "config",
1617+});
1618+1619+expect(registry.plugins[0]?.providerDiscoverySource).toBeUndefined();
1620+expectDiagnosticFields(registry, {
1621+level: "warn",
1622+pluginId: "hardlink-provider",
1623+source: path.join(dir, "openclaw.plugin.json"),
1624+messageIncludes: "providerDiscoveryEntry must resolve inside the plugin root",
1625+});
1626+});
1627+1628+it("ignores non-bundled provider discovery .js fallbacks that are hardlinked", () => {
1629+if (process.platform === "win32") {
1630+return;
1631+}
1632+const dir = makeTempDir();
1633+const outsideDir = makeTempDir();
1634+const outsideEntry = path.join(outsideDir, "provider-discovery.js");
1635+const linkedEntry = path.join(dir, "provider-discovery.js");
1636+fs.writeFileSync(outsideEntry, "export default {};\n", "utf8");
1637+try {
1638+fs.linkSync(outsideEntry, linkedEntry);
1639+} catch (err) {
1640+if ((err as NodeJS.ErrnoException).code === "EXDEV") {
1641+return;
1642+}
1643+throw err;
1644+}
1645+writeManifest(dir, {
1646+id: "fallback-hardlink-provider",
1647+providers: ["fallback-hardlink-provider"],
1648+providerDiscoveryEntry: "./provider-discovery.ts",
1649+configSchema: { type: "object" },
1650+});
1651+1652+const registry = loadSingleCandidateRegistry({
1653+idHint: "fallback-hardlink-provider",
1654+rootDir: dir,
1655+origin: "config",
1656+});
1657+1658+expect(registry.plugins[0]?.providerDiscoverySource).toBeUndefined();
1659+expectDiagnosticFields(registry, {
1660+level: "warn",
1661+pluginId: "fallback-hardlink-provider",
1662+source: path.join(dir, "openclaw.plugin.json"),
1663+messageIncludes: "providerDiscoveryEntry must resolve inside the plugin root",
1664+});
1665+});
1666+14301667it("preserves activation and setup descriptors from plugin manifests", () => {
14311668const dir = makeTempDir();
14321669writeManifest(dir, {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。