Files
openclaw/extensions/anthropic/provider-policy-api.test.ts
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

162 lines
4.4 KiB
TypeScript

import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-types";
import { describe, expect, it } from "vitest";
import {
applyConfigDefaults,
normalizeConfig,
resolveThinkingProfile,
} from "./provider-policy-api.js";
function createModel(id: string, name: string): ModelDefinitionConfig {
return {
id,
name,
reasoning: false,
input: ["text"],
cost: {
input: 0,
output: 0,
cacheRead: 0,
cacheWrite: 0,
},
contextWindow: 128_000,
maxTokens: 8_192,
};
}
function collectLegacyExtendedLevelIds(levels: readonly { id: string }[] | undefined): string[] {
const ids: string[] = [];
for (const level of levels ?? []) {
if (level.id === "xhigh" || level.id === "max") {
ids.push(level.id);
}
}
return ids;
}
function levelIds(levels: readonly { id: string }[] | undefined): string[] {
return (levels ?? []).map((level) => level.id);
}
describe("anthropic provider policy public artifact", () => {
it("normalizes Anthropic provider config", () => {
const normalized = normalizeConfig({
provider: "anthropic",
providerConfig: {
baseUrl: "https://api.anthropic.com",
models: [createModel("claude-sonnet-4-6", "Claude Sonnet 4.6")],
},
});
expect(normalized.api).toBe("anthropic-messages");
expect(normalized.baseUrl).toBe("https://api.anthropic.com");
});
it("normalizes Claude CLI provider config", () => {
const normalized = normalizeConfig({
provider: "claude-cli",
providerConfig: {
baseUrl: "https://api.anthropic.com",
models: [createModel("claude-sonnet-4-6", "Claude Sonnet 4.6")],
},
});
expect(normalized.api).toBe("anthropic-messages");
});
it("does not normalize non-Anthropic provider config", () => {
const providerConfig = {
baseUrl: "https://chatgpt.com/backend-api/codex",
models: [createModel("gpt-5.4", "GPT-5.4")],
};
expect(
normalizeConfig({
provider: "openai-codex",
providerConfig,
}),
).toBe(providerConfig);
});
it("applies Anthropic API-key defaults without loading the full provider plugin", () => {
const nextConfig = applyConfigDefaults({
config: {
auth: {
profiles: {
"anthropic:default": {
provider: "anthropic",
mode: "api_key",
},
},
order: { anthropic: ["anthropic:default"] },
},
agents: {
defaults: {},
},
},
env: {},
});
expect(nextConfig.agents?.defaults?.contextPruning?.mode).toBe("cache-ttl");
expect(nextConfig.agents?.defaults?.contextPruning?.ttl).toBe("1h");
});
it("adds cacheRetention defaults for dated Anthropic primary model refs", () => {
const nextConfig = applyConfigDefaults({
config: {
auth: {
profiles: {
"anthropic:default": {
provider: "anthropic",
mode: "api_key",
},
},
},
agents: {
defaults: {
model: { primary: "anthropic/claude-sonnet-4-20250514" },
},
},
},
env: {},
});
expect(
nextConfig.agents?.defaults?.models?.["anthropic/claude-sonnet-4-20250514"]?.params
?.cacheRetention,
).toBe("short");
});
it("exposes Claude Opus 4.7 thinking levels without loading the full provider plugin", () => {
const profile = resolveThinkingProfile({
provider: "anthropic",
modelId: "claude-opus-4-7",
});
const ids = levelIds(profile?.levels);
expect(ids).toContain("xhigh");
expect(ids).toContain("adaptive");
expect(ids).toContain("max");
expect(profile?.defaultLevel).toBe("off");
});
it("keeps adaptive-only Claude profiles aligned with the runtime provider", () => {
const profile = resolveThinkingProfile({
provider: "anthropic",
modelId: "claude-opus-4-6",
});
if (!profile) {
throw new Error("Expected Anthropic policy profile");
}
expect(levelIds(profile.levels)).toContain("adaptive");
expect(profile.defaultLevel).toBe("adaptive");
expect(collectLegacyExtendedLevelIds(profile.levels)).toStrictEqual([]);
});
it("does not expose Anthropic thinking profiles for unrelated providers", () => {
expect(
resolveThinkingProfile({
provider: "openai",
modelId: "claude-opus-4-7",
}),
).toBeNull();
});
});