mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
fix(plugins): require provenance for official npm trust
Require OpenClaw-owned install provenance before granting official npm plugin scanner trust. Direct npm package names now scan normally; catalog, onboarding, and doctor paths pass explicit provenance.\n\nValidation:\n- pnpm test:serial src/plugins/install.npm-spec.test.ts src/cli/plugins-cli.install.test.ts src/commands/onboarding-plugin-install.test.ts src/commands/doctor/shared/missing-configured-plugin-install.test.ts src/channels/plugins/contracts/channel-catalog.contract.test.ts src/commands/auth-choice.apply.plugin-provider.test.ts\n- pnpm test:serial src/plugins/install.test.ts src/plugins/provider-auth-choices.test.ts src/plugins/provider-install-catalog.test.ts src/commands/channel-setup/plugin-install.test.ts\n- pnpm exec oxfmt --check --threads=1 ...\n- node scripts/run-oxlint.mjs ...\n- Crabbox cbx_6157440c9bbe / run_cbd813956eed: pnpm check:changed passed\n\nThanks @fede-kamel and @vincentkoc.
This commit is contained in:
@@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Channels/WhatsApp: attach native outbound mention metadata for group text and media captions by resolving `@+<digits>` and `@<digits>` tokens against WhatsApp participant data, including LID groups. Fixes #39879; carries forward #56863. Thanks @kengi1437, @joe2643, and @fridayck.
|
||||
- Plugins/uninstall: remove empty managed git install parent directories after deleting cloned plugin repos and cover npm/git uninstall residue in Docker plugin lifecycle tests. Thanks @vincentkoc.
|
||||
- Plugins/install: resolve bare official external plugin IDs such as `brave` through the official catalog when no bundled source is available, so packaged installs fetch the intended scoped npm package instead of an unrelated unscoped package. Fixes #76373. Thanks @bek91 and @vincentkoc.
|
||||
- Plugins/install: require OpenClaw-owned install provenance before granting official npm plugin scanner trust, so direct npm package names no longer bypass launch-code scanning while catalog, onboarding, and doctor installs stay trusted. Thanks @fede-kamel and @vincentkoc.
|
||||
- Gateway/sessions: keep async `sessions.list` title and preview hydration bounded to transcript head/tail reads so Control UI polling cannot full-scan large session transcripts every refresh. Thanks @vincentkoc.
|
||||
- Gateway/sessions: keep agent runtime metadata on lightweight `sessions.list` rows so model-only session patches do not make Control UI lose runtime identity. Thanks @vincentkoc.
|
||||
- Gateway/sessions: keep bulk `sessions.list` rows lightweight by skipping per-row transcript usage fallback, display model inference, and plugin projection, avoiding event-loop stalls in large session stores. Thanks @Marvinthebored and @vincentkoc.
|
||||
|
||||
@@ -39,6 +39,7 @@ export type ChannelPluginCatalogEntry = {
|
||||
id: string;
|
||||
pluginId?: string;
|
||||
origin?: PluginOrigin;
|
||||
trustedSourceLinkedOfficialInstall?: boolean;
|
||||
meta: ChannelMeta;
|
||||
install: ChannelPluginCatalogInstall;
|
||||
installSource?: PluginInstallSourceInfo;
|
||||
@@ -203,7 +204,7 @@ function loadOfficialCatalogEntries(options: CatalogOptions): ChannelPluginCatal
|
||||
? loadCatalogEntriesFromPaths(officialPaths)
|
||||
: loadOfficialCatalogEntriesFromPaths(officialPaths);
|
||||
return [...builtInEntries, ...fileEntries]
|
||||
.map((entry) => buildExternalCatalogEntry(entry))
|
||||
.map((entry) => buildExternalCatalogEntry(entry, { trustedSourceLinkedOfficialInstall: true }))
|
||||
.filter((entry): entry is ChannelPluginCatalogEntry => Boolean(entry));
|
||||
}
|
||||
|
||||
@@ -297,6 +298,7 @@ function buildCatalogEntryFromManifest(params: {
|
||||
packageName?: string;
|
||||
packageDir?: string;
|
||||
origin?: PluginOrigin;
|
||||
trustedSourceLinkedOfficialInstall?: boolean;
|
||||
workspaceDir?: string;
|
||||
channel?: PluginPackageChannel;
|
||||
install?: PluginPackageInstall;
|
||||
@@ -326,6 +328,9 @@ function buildCatalogEntryFromManifest(params: {
|
||||
id,
|
||||
...(pluginId ? { pluginId } : {}),
|
||||
...(params.origin ? { origin: params.origin } : {}),
|
||||
...(params.trustedSourceLinkedOfficialInstall
|
||||
? { trustedSourceLinkedOfficialInstall: true }
|
||||
: {}),
|
||||
meta,
|
||||
install,
|
||||
installSource: describePluginInstallSource(install, {
|
||||
@@ -334,10 +339,16 @@ function buildCatalogEntryFromManifest(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function buildExternalCatalogEntry(entry: ExternalCatalogEntry): ChannelPluginCatalogEntry | null {
|
||||
function buildExternalCatalogEntry(
|
||||
entry: ExternalCatalogEntry,
|
||||
options?: {
|
||||
trustedSourceLinkedOfficialInstall?: boolean;
|
||||
},
|
||||
): ChannelPluginCatalogEntry | null {
|
||||
const manifest = entry[MANIFEST_KEY];
|
||||
return buildCatalogEntryFromManifest({
|
||||
packageName: entry.name,
|
||||
trustedSourceLinkedOfficialInstall: options?.trustedSourceLinkedOfficialInstall,
|
||||
channel: manifest?.channel,
|
||||
install: manifest?.install,
|
||||
});
|
||||
|
||||
@@ -141,6 +141,7 @@ export function describeOfficialFallbackChannelCatalogContract(params: {
|
||||
|
||||
expect(entry?.install.npmSpec).toBe(params.npmSpec);
|
||||
expect(entry?.pluginId).toBeUndefined();
|
||||
expect(entry?.trustedSourceLinkedOfficialInstall).toBe(true);
|
||||
});
|
||||
|
||||
it("lets external catalogs override shipped fallback channel metadata", () => {
|
||||
@@ -217,6 +218,7 @@ export function describeOfficialFallbackChannelCatalogContract(params: {
|
||||
expect(entry?.install.npmSpec).toBe(params.externalNpmSpec);
|
||||
expect(entry?.meta.label).toBe(params.externalLabel);
|
||||
expect(entry?.pluginId).toBeUndefined();
|
||||
expect(entry?.trustedSourceLinkedOfficialInstall).toBeUndefined();
|
||||
});
|
||||
|
||||
it("surfaces package-name drift in external channel catalog install metadata", () => {
|
||||
|
||||
@@ -675,6 +675,7 @@ describe("plugins cli install", () => {
|
||||
expect.objectContaining({
|
||||
spec: "@openclaw/brave-plugin",
|
||||
expectedPluginId: "brave",
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
}),
|
||||
);
|
||||
expect(writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith({
|
||||
@@ -708,6 +709,7 @@ describe("plugins cli install", () => {
|
||||
expectedPluginId: "wecom",
|
||||
expectedIntegrity:
|
||||
"sha512-bnzfdIEEu1/LFvcdyjaTkyxt27w6c7dqhkPezU62OWaqmcdFsUGR3T55USK/O9pIKsNcnL1Tnu1pqKYCWHFgWQ==",
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -728,6 +730,11 @@ describe("plugins cli install", () => {
|
||||
|
||||
await expect(runPluginsCommand(["plugins", "install", "wecom"])).rejects.toThrow("__exit__:1");
|
||||
|
||||
expect(installPluginFromNpmSpec).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
}),
|
||||
);
|
||||
expect(installHooksFromNpmSpec).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "@wecom/wecom-openclaw-plugin@2026.4.23",
|
||||
@@ -845,6 +852,11 @@ describe("plugins cli install", () => {
|
||||
expectedPluginId: "brave",
|
||||
}),
|
||||
);
|
||||
expect(installPluginFromNpmSpec).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
}),
|
||||
);
|
||||
expect(installPluginFromClawHub).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -279,6 +279,7 @@ async function tryInstallPluginOrHookPackFromNpmSpec(params: {
|
||||
extensionsDir: string;
|
||||
expectedPluginId?: string;
|
||||
expectedIntegrity?: string;
|
||||
trustedSourceLinkedOfficialInstall?: boolean;
|
||||
runtime?: RuntimeEnv;
|
||||
}): Promise<{ ok: true } | { ok: false }> {
|
||||
const result = await installPluginFromNpmSpec({
|
||||
@@ -287,6 +288,9 @@ async function tryInstallPluginOrHookPackFromNpmSpec(params: {
|
||||
spec: params.spec,
|
||||
...(params.expectedPluginId ? { expectedPluginId: params.expectedPluginId } : {}),
|
||||
...(params.expectedIntegrity ? { expectedIntegrity: params.expectedIntegrity } : {}),
|
||||
...(params.trustedSourceLinkedOfficialInstall
|
||||
? { trustedSourceLinkedOfficialInstall: true }
|
||||
: {}),
|
||||
extensionsDir: params.extensionsDir,
|
||||
logger: createPluginInstallLogger(params.runtime),
|
||||
});
|
||||
@@ -787,6 +791,7 @@ export async function runPluginInstallCommand(params: {
|
||||
extensionsDir,
|
||||
expectedPluginId: officialExternalPlan.pluginId,
|
||||
expectedIntegrity: officialExternalPlan.expectedIntegrity,
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
runtime,
|
||||
});
|
||||
if (!npmResult.ok) {
|
||||
|
||||
@@ -33,6 +33,9 @@ function toOnboardingPluginInstallEntry(
|
||||
pluginId: entry.pluginId ?? entry.id,
|
||||
label: entry.meta.label,
|
||||
install: entry.install,
|
||||
...(entry.trustedSourceLinkedOfficialInstall
|
||||
? { trustedSourceLinkedOfficialInstall: true }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -125,6 +125,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
npmSpec: "@openclaw/plugin-matrix@1.2.3",
|
||||
expectedIntegrity: "sha512-test",
|
||||
},
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -146,6 +147,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
extensionsDir: "/tmp/openclaw-plugins",
|
||||
expectedPluginId: "matrix",
|
||||
expectedIntegrity: "sha512-test",
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
}),
|
||||
);
|
||||
expect(mocks.writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith(
|
||||
@@ -224,6 +226,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
install: {
|
||||
npmSpec: "@openclaw/plugin-matrix@1.2.3",
|
||||
},
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -240,6 +243,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
spec: "@openclaw/plugin-matrix@1.2.3",
|
||||
extensionsDir: "/tmp/openclaw-plugins",
|
||||
expectedPluginId: "matrix",
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
}),
|
||||
);
|
||||
expect(mocks.writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith(
|
||||
@@ -273,6 +277,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
clawhubSpec: "clawhub:@openclaw/plugin-matrix@stable",
|
||||
npmSpec: "@openclaw/plugin-matrix@1.2.3",
|
||||
},
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -289,6 +294,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
expect.objectContaining({
|
||||
spec: "@openclaw/plugin-matrix@1.2.3",
|
||||
expectedPluginId: "matrix",
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
}),
|
||||
);
|
||||
expect(result.changes).toEqual([
|
||||
@@ -321,6 +327,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
npmSpec: "@openclaw/twitch",
|
||||
defaultChoice: "npm",
|
||||
},
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
},
|
||||
]);
|
||||
|
||||
@@ -338,6 +345,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
expect.objectContaining({
|
||||
spec: "@openclaw/twitch",
|
||||
expectedPluginId: "twitch",
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
}),
|
||||
);
|
||||
expect(result.changes).toEqual([
|
||||
@@ -813,6 +821,11 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
expectedPluginId: "wecom",
|
||||
}),
|
||||
);
|
||||
expect(mocks.installPluginFromNpmSpec).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
}),
|
||||
);
|
||||
expect(result.changes).toEqual([
|
||||
'Installed missing configured plugin "wecom" from @wecom/wecom-openclaw-plugin@2026.4.23.',
|
||||
]);
|
||||
@@ -863,6 +876,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
expect.objectContaining({
|
||||
spec: "@openclaw/codex",
|
||||
expectedPluginId: "codex",
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
}),
|
||||
);
|
||||
expect(mocks.writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith(
|
||||
@@ -940,6 +954,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
expect.objectContaining({
|
||||
spec: "@openclaw/codex",
|
||||
expectedPluginId: "codex",
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
}),
|
||||
);
|
||||
expect(mocks.writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith(
|
||||
@@ -1073,6 +1088,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
install: {
|
||||
npmSpec: "@openclaw/discord",
|
||||
},
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
},
|
||||
]);
|
||||
mocks.installPluginFromNpmSpec.mockResolvedValueOnce({
|
||||
@@ -1132,6 +1148,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
expect.objectContaining({
|
||||
spec: "@openclaw/discord",
|
||||
expectedPluginId: "discord",
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
}),
|
||||
);
|
||||
expect(mocks.writePersistedInstalledPluginIndexInstallRecords).toHaveBeenCalledWith(
|
||||
@@ -1476,6 +1493,7 @@ describe("repairMissingConfiguredPluginInstalls", () => {
|
||||
expect.objectContaining({
|
||||
spec: "@openclaw/brave-plugin",
|
||||
expectedPluginId: "brave",
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
}),
|
||||
);
|
||||
expect(result.changes).toEqual([
|
||||
|
||||
@@ -36,6 +36,7 @@ type DownloadableInstallCandidate = {
|
||||
npmSpec?: string;
|
||||
clawhubSpec?: string;
|
||||
expectedIntegrity?: string;
|
||||
trustedSourceLinkedOfficialInstall?: boolean;
|
||||
defaultChoice?: PluginPackageInstall["defaultChoice"];
|
||||
};
|
||||
|
||||
@@ -44,12 +45,14 @@ const RUNTIME_PLUGIN_INSTALL_CANDIDATES: readonly DownloadableInstallCandidate[]
|
||||
pluginId: "acpx",
|
||||
label: "ACPX Runtime",
|
||||
npmSpec: "@openclaw/acpx",
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
},
|
||||
// Runtime-only configs do not have a provider/channel integration catalog entry.
|
||||
{
|
||||
pluginId: "codex",
|
||||
label: "Codex",
|
||||
npmSpec: "@openclaw/codex",
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -201,6 +204,9 @@ function collectDownloadableInstallCandidates(params: {
|
||||
...(entry.install.expectedIntegrity
|
||||
? { expectedIntegrity: entry.install.expectedIntegrity }
|
||||
: {}),
|
||||
...(entry.trustedSourceLinkedOfficialInstall
|
||||
? { trustedSourceLinkedOfficialInstall: true }
|
||||
: {}),
|
||||
...(entry.install.defaultChoice ? { defaultChoice: entry.install.defaultChoice } : {}),
|
||||
});
|
||||
}
|
||||
@@ -229,6 +235,7 @@ function collectDownloadableInstallCandidates(params: {
|
||||
...(entry.install.expectedIntegrity
|
||||
? { expectedIntegrity: entry.install.expectedIntegrity }
|
||||
: {}),
|
||||
...(entry.origin === "bundled" ? { trustedSourceLinkedOfficialInstall: true } : {}),
|
||||
...(entry.install.defaultChoice ? { defaultChoice: entry.install.defaultChoice } : {}),
|
||||
});
|
||||
}
|
||||
@@ -256,6 +263,7 @@ function collectDownloadableInstallCandidates(params: {
|
||||
...(npmSpec ? { npmSpec } : {}),
|
||||
...(clawhubSpec ? { clawhubSpec } : {}),
|
||||
...(install.expectedIntegrity ? { expectedIntegrity: install.expectedIntegrity } : {}),
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
...(install.defaultChoice ? { defaultChoice: install.defaultChoice } : {}),
|
||||
});
|
||||
}
|
||||
@@ -419,6 +427,9 @@ async function installCandidate(params: {
|
||||
extensionsDir,
|
||||
expectedPluginId: candidate.pluginId,
|
||||
expectedIntegrity: candidate.expectedIntegrity,
|
||||
...(candidate.trustedSourceLinkedOfficialInstall
|
||||
? { trustedSourceLinkedOfficialInstall: true }
|
||||
: {}),
|
||||
mode: "install",
|
||||
});
|
||||
if (!result.ok) {
|
||||
|
||||
@@ -200,6 +200,7 @@ describe("ensureOnboardingPluginInstalled", () => {
|
||||
npmSpec: "@wecom/wecom-openclaw-plugin@1.2.3",
|
||||
expectedIntegrity: "sha512-wecom",
|
||||
},
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
},
|
||||
prompter: {
|
||||
select: vi.fn(async () => "npm"),
|
||||
@@ -211,7 +212,9 @@ describe("ensureOnboardingPluginInstalled", () => {
|
||||
expect(installPluginFromNpmSpec).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: "@wecom/wecom-openclaw-plugin@1.2.3",
|
||||
expectedPluginId: "demo-plugin",
|
||||
expectedIntegrity: "sha512-wecom",
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
timeoutMs: 300_000,
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -30,6 +30,7 @@ export type OnboardingPluginInstallEntry = {
|
||||
pluginId: string;
|
||||
label: string;
|
||||
install: PluginPackageInstall;
|
||||
trustedSourceLinkedOfficialInstall?: boolean;
|
||||
};
|
||||
|
||||
export type OnboardingPluginInstallStatus = "installed" | "skipped" | "failed" | "timed_out";
|
||||
@@ -585,7 +586,11 @@ async function installPluginFromNpmSpecWithProgress(params: {
|
||||
installPluginFromNpmSpec({
|
||||
spec: params.npmSpec,
|
||||
timeoutMs: ONBOARDING_PLUGIN_INSTALL_TIMEOUT_MS,
|
||||
expectedPluginId: params.entry.pluginId,
|
||||
expectedIntegrity: params.entry.install.expectedIntegrity,
|
||||
...(params.entry.trustedSourceLinkedOfficialInstall
|
||||
? { trustedSourceLinkedOfficialInstall: true }
|
||||
: {}),
|
||||
extensionsDir: resolveDefaultPluginExtensionsDir(),
|
||||
logger: {
|
||||
info: updateProgress,
|
||||
|
||||
@@ -342,7 +342,7 @@ describe("installPluginFromNpmSpec", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
const officialLaunchPluginCases = [
|
||||
{
|
||||
spec: "@openclaw/acpx",
|
||||
pluginId: "acpx",
|
||||
@@ -363,8 +363,10 @@ describe("installPluginFromNpmSpec", () => {
|
||||
pluginId: "voice-call",
|
||||
indexJs: `import { spawn } from "node:child_process";\nspawn("ngrok", ["http", "3000"]);`,
|
||||
},
|
||||
])(
|
||||
"allows official npm plugin $spec with reviewed launch code",
|
||||
];
|
||||
|
||||
it.each(officialLaunchPluginCases)(
|
||||
"blocks direct official npm plugin $spec with launch code without source provenance",
|
||||
async ({ spec, pluginId, indexJs }) => {
|
||||
const npmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
|
||||
const warnings: string[] = [];
|
||||
@@ -386,6 +388,45 @@ describe("installPluginFromNpmSpec", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) {
|
||||
return;
|
||||
}
|
||||
expect(result.code).toBe(PLUGIN_INSTALL_ERROR_CODE.SECURITY_SCAN_BLOCKED);
|
||||
expect(fs.existsSync(path.join(npmRoot, "node_modules", spec))).toBe(false);
|
||||
expect(
|
||||
warnings.some((warning) =>
|
||||
warning.includes("allowed because it is an official OpenClaw package"),
|
||||
),
|
||||
).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(officialLaunchPluginCases)(
|
||||
"allows source-linked official npm plugin $spec with reviewed launch code",
|
||||
async ({ spec, pluginId, indexJs }) => {
|
||||
const npmRoot = path.join(suiteTempRootTracker.makeTempDir(), "npm");
|
||||
const warnings: string[] = [];
|
||||
mockNpmViewAndInstall({
|
||||
spec,
|
||||
packageName: spec,
|
||||
version: "2026.5.2",
|
||||
pluginId,
|
||||
npmRoot,
|
||||
indexJs,
|
||||
});
|
||||
|
||||
const result = await installPluginFromNpmSpec({
|
||||
spec,
|
||||
npmDir: npmRoot,
|
||||
expectedPluginId: pluginId,
|
||||
trustedSourceLinkedOfficialInstall: true,
|
||||
logger: {
|
||||
info: () => {},
|
||||
warn: (msg: string) => warnings.push(msg),
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
if (!result.ok) {
|
||||
return;
|
||||
|
||||
@@ -115,12 +115,6 @@ type PluginInstallPolicyRequest = {
|
||||
};
|
||||
|
||||
const defaultLogger: PluginInstallLogger = {};
|
||||
const TRUSTED_OFFICIAL_NPM_PLUGIN_PACKAGES = new Map([
|
||||
["@openclaw/acpx", "acpx"],
|
||||
["@openclaw/codex", "codex"],
|
||||
["@openclaw/google-meet", "google-meet"],
|
||||
["@openclaw/voice-call", "voice-call"],
|
||||
]);
|
||||
|
||||
function ensureOpenClawExtensions(params: { manifest: PackageManifest }):
|
||||
| {
|
||||
@@ -196,26 +190,6 @@ function hasPackageRuntimeDependencies(manifest: PackageManifest): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function isTrustedOfficialNpmPluginInstall(params: {
|
||||
installPolicyRequest?: PluginInstallPolicyRequest;
|
||||
packageName: string;
|
||||
pluginId: string;
|
||||
}): boolean {
|
||||
if (params.installPolicyRequest?.kind !== "plugin-npm") {
|
||||
return false;
|
||||
}
|
||||
const requested = parseRegistryNpmSpec(params.installPolicyRequest.requestedSpecifier ?? "");
|
||||
if (!requested) {
|
||||
return false;
|
||||
}
|
||||
const expectedPluginId = TRUSTED_OFFICIAL_NPM_PLUGIN_PACKAGES.get(requested.name);
|
||||
return (
|
||||
expectedPluginId !== undefined &&
|
||||
params.packageName === requested.name &&
|
||||
params.pluginId === expectedPluginId
|
||||
);
|
||||
}
|
||||
|
||||
function buildBlockedInstallResult(params: {
|
||||
blocked: NonNullable<NonNullable<InstallSecurityScanResult>["blocked"]>;
|
||||
}): Extract<InstallPluginResult, { ok: false }> {
|
||||
@@ -777,19 +751,12 @@ async function validatePackagePluginInstallSource(params: {
|
||||
const scanMode = params.resolveEffectiveMode
|
||||
? await params.resolveEffectiveMode(pluginId)
|
||||
: params.mode;
|
||||
const trustedOfficialInstall =
|
||||
params.trustedSourceLinkedOfficialInstall ||
|
||||
isTrustedOfficialNpmPluginInstall({
|
||||
installPolicyRequest: params.installPolicyRequest,
|
||||
packageName: pkgName,
|
||||
pluginId,
|
||||
});
|
||||
const scanResult = await runInstallSourceScan({
|
||||
subject: `Plugin "${pluginId}"`,
|
||||
scan: async () =>
|
||||
await params.runtime.scanPackageInstallSource({
|
||||
dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall,
|
||||
trustedSourceLinkedOfficialInstall: trustedOfficialInstall,
|
||||
trustedSourceLinkedOfficialInstall: params.trustedSourceLinkedOfficialInstall,
|
||||
packageDir: params.packageDir,
|
||||
pluginId,
|
||||
logger: params.logger,
|
||||
@@ -1279,6 +1246,7 @@ export async function installPluginFromNpmSpec(
|
||||
dependencyScanRootDir: npmRoot,
|
||||
logger,
|
||||
expectedPluginId,
|
||||
trustedSourceLinkedOfficialInstall: params.trustedSourceLinkedOfficialInstall,
|
||||
mode: effectiveMode,
|
||||
installPolicyRequest: {
|
||||
kind: "plugin-npm",
|
||||
|
||||
@@ -378,6 +378,9 @@ export async function applyAuthChoiceLoadedPluginProvider(
|
||||
pluginId: installCatalogEntry.pluginId,
|
||||
label: installCatalogEntry.label,
|
||||
install: installCatalogEntry.install,
|
||||
...(installCatalogEntry.origin === "bundled"
|
||||
? { trustedSourceLinkedOfficialInstall: true }
|
||||
: {}),
|
||||
},
|
||||
prompter: params.prompter,
|
||||
runtime: params.runtime,
|
||||
|
||||
Reference in New Issue
Block a user