mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(security): fail closed on missing npm integrity
This commit is contained in:
@@ -551,7 +551,7 @@ methods. Treat this as feature discovery, not a full enumeration of
|
||||
<Accordion title="Plugin management">
|
||||
- `plugins.list` (`operator.read`) returns the installed plugin inventory plus locally curated official picks, diagnostics, and whether the current install mode allows mutations.
|
||||
- `plugins.search` (`operator.read`) searches installable ClawHub code-plugin and bundle-plugin families. Pass non-empty `query` and optional `limit` from 1 to 100.
|
||||
- `plugins.install` (`operator.admin`) installs either an official catalog entry with `{ source: "official", pluginId, installPolicyWarningAcknowledgement? }` or a ClawHub package with `{ source: "clawhub", packageName, version?, acknowledgeClawHubRisk?, installPolicyWarningAcknowledgement? }`. ClawHub installs preserve Gateway trust, integrity, and install-policy checks. When install policy warns and the Gateway can bind the request to an immutable resolved artifact, the request fails before commit with structured `error.details` containing `installPolicyCode`, target metadata, `reason`, optional `findings`, and a server-issued `acknowledgementToken`. After showing those details, a client may retry with that token as `installPolicyWarningAcknowledgement`. The Gateway consumes the token once and only for the same install request and resolved artifact; policy then re-evaluates the staged source and continues only when the warning is unchanged. A changed or later warning fails that request with its own structured details and, when the artifact remains immutably resolved, a fresh token for another reviewed retry. A block or a warning without immutable resolution metadata is terminal and does not include an acknowledgement token. Successful installs require a Gateway restart.
|
||||
- `plugins.install` (`operator.admin`) installs either an official catalog entry with `{ source: "official", pluginId, installPolicyWarningAcknowledgement? }` or a ClawHub package with `{ source: "clawhub", packageName, version?, acknowledgeClawHubRisk?, installPolicyWarningAcknowledgement? }`. ClawHub installs preserve Gateway trust, integrity, and install-policy checks. When install policy warns and the Gateway can bind the request to an immutable resolved artifact, the request fails before commit with structured `error.details` containing `installPolicyCode: "install_policy_warning_acknowledgement_required"`, target metadata, `reason`, optional `findings`, and a server-issued `acknowledgementToken`. After showing those details, a client may retry with that token as `installPolicyWarningAcknowledgement`. The Gateway consumes the token once and only for the same install request and resolved artifact; policy then re-evaluates the staged source and continues only when the warning is unchanged. Tokens expire after five minutes and are invalidated by a Gateway restart; after either event, retry without the token to receive the current warning and a fresh token. A changed or later warning fails that request with its own structured details and, when the artifact remains immutably resolved, a fresh token for another reviewed retry. A block or a warning without immutable resolution metadata is terminal and does not include an acknowledgement token. Successful installs require a Gateway restart.
|
||||
- `plugins.setEnabled` (`operator.admin`) changes one installed plugin's enabled policy with `{ pluginId, enabled }`. The response includes the updated catalog entry, restart metadata, and any slot-selection warnings.
|
||||
- `plugins.uninstall` (`operator.admin`) removes one externally installed plugin with `{ pluginId }`: config references, the install record, and managed files. Bundled plugins cannot be uninstalled, only disabled. The response lists the removal actions and always requires a Gateway restart.
|
||||
|
||||
|
||||
@@ -57,4 +57,28 @@ describe("resolveNpmIntegrityDrift", () => {
|
||||
"Integrity drift detected for @openclaw/test@1.0.0: expected sha512-old, got sha512-new",
|
||||
);
|
||||
});
|
||||
|
||||
it("fails closed when a pinned resolution omits fresh integrity metadata", async () => {
|
||||
const warn = vi.fn();
|
||||
const onIntegrityDrift = vi.fn(async () => true);
|
||||
|
||||
const result = await resolveNpmIntegrityDriftWithDefaultMessage({
|
||||
spec: "@openclaw/test@1.0.0",
|
||||
expectedIntegrity: "sha512-reviewed",
|
||||
resolution: {
|
||||
resolvedSpec: "@openclaw/test@1.0.0",
|
||||
resolvedAt: "2026-01-01T00:00:00.000Z",
|
||||
},
|
||||
onIntegrityDrift,
|
||||
warn,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
error: "aborted: npm package integrity metadata missing for @openclaw/test@1.0.0",
|
||||
});
|
||||
expect(onIntegrityDrift).not.toHaveBeenCalled();
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
"Integrity metadata missing for @openclaw/test@1.0.0: expected sha512-reviewed",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -86,6 +86,14 @@ type ResolveNpmIntegrityDriftWithDefaultMessageParams = {
|
||||
export async function resolveNpmIntegrityDriftWithDefaultMessage(
|
||||
params: ResolveNpmIntegrityDriftWithDefaultMessageParams,
|
||||
): Promise<{ integrityDrift?: NpmIntegrityDrift; error?: string }> {
|
||||
const expectedIntegrity = normalizeIntegrity(params.expectedIntegrity);
|
||||
const resolvedLabel = params.resolution.resolvedSpec ?? params.spec;
|
||||
if (expectedIntegrity && !normalizeIntegrity(params.resolution.integrity)) {
|
||||
params.warn?.(`Integrity metadata missing for ${resolvedLabel}: expected ${expectedIntegrity}`);
|
||||
return {
|
||||
error: `aborted: npm package integrity metadata missing for ${resolvedLabel}`,
|
||||
};
|
||||
}
|
||||
const driftResult = await resolveNpmIntegrityDrift<NpmIntegrityDriftPayload>({
|
||||
spec: params.spec,
|
||||
expectedIntegrity: params.expectedIntegrity,
|
||||
|
||||
@@ -3612,6 +3612,28 @@ describe("installPluginFromNpmSpec", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed when a pinned npm retry cannot resolve fresh integrity metadata", async () => {
|
||||
mockNpmViewMetadataResult(runCommandWithTimeoutMock, {
|
||||
name: "@openclaw/voice-call",
|
||||
version: "0.0.1",
|
||||
shasum: "reviewedshasum",
|
||||
});
|
||||
|
||||
const onIntegrityDrift = vi.fn(async () => true);
|
||||
const result = await installPluginFromNpmSpec({
|
||||
spec: "@openclaw/voice-call@0.0.1",
|
||||
expectedIntegrity: "sha512-reviewed",
|
||||
onIntegrityDrift,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
ok: false,
|
||||
error: "aborted: npm package integrity metadata missing for @openclaw/voice-call@0.0.1",
|
||||
});
|
||||
expect(onIntegrityDrift).not.toHaveBeenCalled();
|
||||
expect(runCommandWithTimeoutMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("classifies npm package-not-found errors with a stable error code", async () => {
|
||||
runCommandWithTimeoutMock.mockResolvedValue({
|
||||
code: 1,
|
||||
|
||||
Reference in New Issue
Block a user