Files
openclaw/src/config/config.pruning-defaults.test.ts
T
Jason O'Neal 7fffbf60b0 fix: harden package URL downloads (#85578)
* fix: harden package URL downloads

Guard package acceptance URL downloads with HTTPS-only validation, no embedded credentials, private/special-use DNS and IP rejection, manual redirect checks, bounded timeout/size limits, pinned lookup, and atomic temp-file writes. Add tooling tests for unsafe URLs, redirect validation, size limits, and successful writes.

* fix: cancel redirect response bodies before closing dispatcher

ClawSweeper P2: the redirect branch in openPackageDownloadResponse cleared
the timeout and awaited dispatcher.close() without first cancelling
response.body. Undici's close() is graceful — it waits for in-flight
requests to complete — so a malicious redirect with a slow/never-ending
body could hang the hardened downloader.

Fix: call response.body?.cancel() before dispatcher.close() to abort the
redirect body immediately.

Test: add a regression test that uses a ReadableStream with an indefinite
interval to simulate a hanging body, and asserts cancel() was called.

Refs: clawsweeper review on PR #85512

* test: harden redirect body cancellation race in regression test

Guard the ReadableStream controller.enqueue() call with a cancelled
flag and try/catch to prevent ERR_INVALID_STATE when the interval
fires after cancel() closes the controller.

* fix: cancel final response body before closing dispatcher in downloadUrl

ClawSweeper P2: the HTTP-error and declared-oversize early-exit paths
in downloadUrl threw before consuming or canceling response.body. The
finally block then cleared the timeout and awaited graceful
dispatcher.close() with the body still open, allowing a slow/never-ending
response to hang release tooling.

Fix: add response.body?.cancel() in the finally block before
dispatcher.close().

Tests: add two regressions:
- HTTP 500 with slow body: asserts cancel() called before dispatcher close
- Declared content-length oversize with slow body: same assertion

* fix: add trusted package URL source policy

* fix: keep package URL resolver dependency-free

* test: cover encoded IPv6 package URL bypasses

* docs: sync package acceptance source overview

* docs: restore release doc formatting

* docs: sync package acceptance trusted-url source

* test: cover dotted IPv4 embedded IPv6 package URLs

* fix: parse dotted IPv4 embedded in IPv6 package URLs

* test: isolate anthropic pruning defaults

* test: move anthropic dated model coverage

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-05-23 17:28:29 +01:00

153 lines
4.7 KiB
TypeScript

import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { setBundledPluginsDirOverrideForTest } from "../plugins/bundled-dir.js";
import { resetBundledPluginPublicArtifactLoaderForTest } from "../plugins/public-surface-loader.js";
import type { OpenClawConfig } from "./config.js";
import { applyProviderConfigDefaultsForConfig } from "./provider-policy.js";
function expectAnthropicPruningDefaults(cfg: OpenClawConfig, heartbeatEvery = "30m") {
expect(cfg.agents?.defaults?.contextPruning?.mode).toBe("cache-ttl");
expect(cfg.agents?.defaults?.contextPruning?.ttl).toBe("1h");
expect(cfg.agents?.defaults?.heartbeat?.every).toBe(heartbeatEvery);
}
function applyAnthropicDefaultsForTest(config: OpenClawConfig) {
return applyProviderConfigDefaultsForConfig({ provider: "anthropic", config, env: {} });
}
describe("config pruning defaults", () => {
beforeEach(() => {
setBundledPluginsDirOverrideForTest(path.resolve(import.meta.dirname, "../../extensions"));
resetBundledPluginPublicArtifactLoaderForTest();
vi.stubEnv(
"OPENCLAW_BUNDLED_PLUGINS_DIR",
path.resolve(import.meta.dirname, "../../extensions"),
);
});
afterEach(() => {
setBundledPluginsDirOverrideForTest(undefined);
resetBundledPluginPublicArtifactLoaderForTest();
vi.unstubAllEnvs();
});
it("does not enable contextPruning by default", () => {
const cfg = applyAnthropicDefaultsForTest({ agents: { defaults: {} } });
expect(cfg.agents?.defaults?.contextPruning?.mode).toBeUndefined();
});
it("enables cache-ttl pruning + 1h heartbeat for Anthropic OAuth", () => {
const cfg = applyAnthropicDefaultsForTest({
auth: {
profiles: {
"anthropic:me": { provider: "anthropic", mode: "oauth", email: "me@example.com" },
},
},
agents: { defaults: {} },
});
expectAnthropicPruningDefaults(cfg, "1h");
});
it("backfills raw and canonical Claude CLI policies for selected Anthropic CLI auth", () => {
const cfg = applyAnthropicDefaultsForTest({
auth: {
order: { anthropic: ["anthropic:claude-cli"] },
profiles: {
"anthropic:claude-cli": { provider: "claude-cli", mode: "oauth" },
},
},
agents: {
defaults: {
model: { primary: "anthropic/opus-4.7" },
models: {
"anthropic/opus-4.7": { params: { maxTokens: 1200 } },
},
},
},
});
expect(cfg.agents?.defaults?.models?.["anthropic/opus-4.7"]).toEqual({
params: { maxTokens: 1200 },
agentRuntime: { id: "claude-cli" },
});
expect(cfg.agents?.defaults?.models?.["anthropic/claude-opus-4-7"]).toEqual({
agentRuntime: { id: "claude-cli" },
});
});
it("enables cache-ttl pruning + 1h cache TTL for Anthropic API keys", () => {
const cfg = applyAnthropicDefaultsForTest({
auth: {
profiles: {
"anthropic:api": { provider: "anthropic", mode: "api_key" },
},
},
agents: {
defaults: {
model: { primary: "anthropic/claude-opus-4-6" },
},
},
});
expectAnthropicPruningDefaults(cfg);
expect(
cfg.agents?.defaults?.models?.["anthropic/claude-opus-4-6"]?.params?.cacheRetention,
).toBe("short");
});
it("adds default cacheRetention for Anthropic Claude models on Bedrock", () => {
const cfg = applyAnthropicDefaultsForTest({
auth: {
profiles: {
"anthropic:api": { provider: "anthropic", mode: "api_key" },
},
},
agents: {
defaults: {
model: { primary: "amazon-bedrock/us.anthropic.claude-opus-4-6-v1" },
},
},
});
expect(
cfg.agents?.defaults?.models?.["amazon-bedrock/us.anthropic.claude-opus-4-6-v1"]?.params
?.cacheRetention,
).toBe("short");
});
it("does not add default cacheRetention for non-Anthropic Bedrock models", () => {
const cfg = applyAnthropicDefaultsForTest({
auth: {
profiles: {
"anthropic:api": { provider: "anthropic", mode: "api_key" },
},
},
agents: {
defaults: {
model: { primary: "amazon-bedrock/amazon.nova-micro-v1:0" },
},
},
});
expect(
cfg.agents?.defaults?.models?.["amazon-bedrock/amazon.nova-micro-v1:0"]?.params
?.cacheRetention,
).toBeUndefined();
});
it("does not override explicit contextPruning mode", () => {
const cfg = applyAnthropicDefaultsForTest({
auth: {
profiles: {
"anthropic:api": { provider: "anthropic", mode: "api_key" },
},
},
agents: { defaults: { contextPruning: { mode: "off" } } },
});
expect(cfg.agents?.defaults?.contextPruning?.mode).toBe("off");
});
});