mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-20 13:41:30 +00:00
refactor(plugins): tighten web fetch provider boundary (#59646)
* refactor(plugins): tighten web fetch provider boundary * fix(config): sync fetch secret parity and baseline * fix(ci): enforce web fetch boundary guard
This commit is contained in:
@@ -67,19 +67,6 @@ type WebFetchConfig = NonNullable<OpenClawConfig["tools"]>["web"] extends infer
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
export type FetchFirecrawlContentParams = {
|
||||
url: string;
|
||||
extractMode: ExtractMode;
|
||||
apiKey: string;
|
||||
baseUrl: string;
|
||||
onlyMainContent: boolean;
|
||||
maxAgeMs: number;
|
||||
proxy: "auto" | "basic" | "stealth";
|
||||
storeInCache: boolean;
|
||||
timeoutSeconds: number;
|
||||
maxChars?: number;
|
||||
};
|
||||
|
||||
function resolveFetchConfig(cfg?: OpenClawConfig): WebFetchConfig {
|
||||
const fetch = cfg?.tools?.web?.fetch;
|
||||
if (!fetch || typeof fetch !== "object") {
|
||||
@@ -247,65 +234,6 @@ function normalizeContentType(value: string | null | undefined): string | undefi
|
||||
return trimmed || undefined;
|
||||
}
|
||||
|
||||
export async function fetchFirecrawlContent(params: FetchFirecrawlContentParams): Promise<{
|
||||
text: string;
|
||||
title?: string;
|
||||
finalUrl?: string;
|
||||
status?: number;
|
||||
warning?: string;
|
||||
}> {
|
||||
const config: OpenClawConfig = {
|
||||
tools: {
|
||||
web: {
|
||||
fetch: {
|
||||
provider: "firecrawl",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
firecrawl: {
|
||||
enabled: true,
|
||||
config: {
|
||||
webFetch: {
|
||||
apiKey: params.apiKey,
|
||||
baseUrl: params.baseUrl,
|
||||
onlyMainContent: params.onlyMainContent,
|
||||
maxAgeMs: params.maxAgeMs,
|
||||
timeoutSeconds: params.timeoutSeconds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const resolved = resolveWebFetchDefinition({
|
||||
config,
|
||||
preferRuntimeProviders: false,
|
||||
providerId: "firecrawl",
|
||||
});
|
||||
if (!resolved) {
|
||||
throw new Error("Firecrawl web fetch provider is unavailable.");
|
||||
}
|
||||
|
||||
const payload = await resolved.definition.execute({
|
||||
url: params.url,
|
||||
extractMode: params.extractMode,
|
||||
maxChars: params.maxChars ?? DEFAULT_FETCH_MAX_CHARS,
|
||||
proxy: params.proxy,
|
||||
storeInCache: params.storeInCache,
|
||||
});
|
||||
|
||||
return {
|
||||
text: typeof payload.text === "string" ? payload.text : "",
|
||||
title: typeof payload.title === "string" ? payload.title : undefined,
|
||||
finalUrl: typeof payload.finalUrl === "string" ? payload.finalUrl : undefined,
|
||||
status: typeof payload.status === "number" ? payload.status : undefined,
|
||||
warning: typeof payload.warning === "string" ? payload.warning : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
type WebFetchRuntimeParams = {
|
||||
url: string;
|
||||
extractMode: ExtractMode;
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
export { createWebFetchTool, extractReadableContent, fetchFirecrawlContent } from "./web-fetch.js";
|
||||
export { createWebFetchTool, extractReadableContent } from "./web-fetch.js";
|
||||
export { createWebSearchTool } from "./web-search.js";
|
||||
|
||||
@@ -544,6 +544,51 @@ describe("normalizeCompatibilityConfigValues", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps explicit plugin-owned web fetch config while filling missing legacy fields", () => {
|
||||
const res = normalizeCompatibilityConfigValues({
|
||||
tools: {
|
||||
web: {
|
||||
fetch: {
|
||||
provider: "firecrawl",
|
||||
firecrawl: {
|
||||
apiKey: "legacy-firecrawl-key",
|
||||
baseUrl: "https://api.firecrawl.dev",
|
||||
onlyMainContent: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
firecrawl: {
|
||||
enabled: true,
|
||||
config: {
|
||||
webFetch: {
|
||||
apiKey: "explicit-firecrawl-key",
|
||||
timeoutSeconds: 30,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig);
|
||||
|
||||
expect(res.config.plugins?.entries?.firecrawl).toEqual({
|
||||
enabled: true,
|
||||
config: {
|
||||
webFetch: {
|
||||
apiKey: "explicit-firecrawl-key",
|
||||
timeoutSeconds: 30,
|
||||
baseUrl: "https://api.firecrawl.dev",
|
||||
onlyMainContent: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(res.changes).toEqual([
|
||||
"Merged tools.web.fetch.firecrawl → plugins.entries.firecrawl.config.webFetch (filled missing fields from legacy; kept explicit plugin config values).",
|
||||
]);
|
||||
});
|
||||
|
||||
it("migrates legacy talk flat fields to provider/providers", () => {
|
||||
const res = normalizeCompatibilityConfigValues({
|
||||
talk: {
|
||||
|
||||
@@ -136,6 +136,7 @@ export function installWebSearchProviderContractSuite(params: {
|
||||
export function installWebFetchProviderContractSuite(params: {
|
||||
provider: Lazy<WebFetchProviderPlugin>;
|
||||
credentialValue: Lazy<unknown>;
|
||||
pluginId?: string;
|
||||
}) {
|
||||
it("satisfies the base web fetch provider contract", () => {
|
||||
const provider = resolveLazy(params.provider);
|
||||
@@ -152,11 +153,28 @@ export function installWebFetchProviderContractSuite(params: {
|
||||
|
||||
expect(provider.envVars).toEqual([...new Set(provider.envVars)]);
|
||||
expect(provider.envVars.every((entry) => entry.trim().length > 0)).toBe(true);
|
||||
expect(provider.credentialPath.trim()).not.toBe("");
|
||||
if (provider.inactiveSecretPaths) {
|
||||
expect(provider.inactiveSecretPaths).toEqual([...new Set(provider.inactiveSecretPaths)]);
|
||||
// Runtime inactive-path classification uses inactiveSecretPaths as the complete list.
|
||||
expect(provider.inactiveSecretPaths).toContain(provider.credentialPath);
|
||||
}
|
||||
|
||||
const fetchConfigTarget: Record<string, unknown> = {};
|
||||
provider.setCredentialValue(fetchConfigTarget, credentialValue);
|
||||
expect(provider.getCredentialValue(fetchConfigTarget)).toEqual(credentialValue);
|
||||
|
||||
if (provider.setConfiguredCredentialValue && provider.getConfiguredCredentialValue) {
|
||||
const configTarget = {} as OpenClawConfig;
|
||||
provider.setConfiguredCredentialValue(configTarget, credentialValue);
|
||||
expect(provider.getConfiguredCredentialValue(configTarget)).toEqual(credentialValue);
|
||||
}
|
||||
|
||||
if (provider.applySelectionConfig && params.pluginId) {
|
||||
const applied = provider.applySelectionConfig({} as OpenClawConfig);
|
||||
expect(applied.plugins?.entries?.[params.pluginId]?.enabled).toBe(true);
|
||||
}
|
||||
|
||||
const config = {
|
||||
tools: {
|
||||
web: {
|
||||
|
||||
@@ -1421,12 +1421,19 @@ export type WebFetchProviderPlugin = {
|
||||
signupUrl: string;
|
||||
docsUrl?: string;
|
||||
autoDetectOrder?: number;
|
||||
/** Canonical plugin-owned config path for this provider's primary fetch credential. */
|
||||
credentialPath: string;
|
||||
/**
|
||||
* Legacy or inactive credential paths that should warn but not activate this provider.
|
||||
* Include credentialPath here when overriding the list, because runtime classification
|
||||
* treats inactiveSecretPaths as the full inactive surface for this provider.
|
||||
*/
|
||||
inactiveSecretPaths?: string[];
|
||||
getCredentialValue: (fetchConfig?: Record<string, unknown>) => unknown;
|
||||
setCredentialValue: (fetchConfigTarget: Record<string, unknown>, value: unknown) => void;
|
||||
getConfiguredCredentialValue?: (config?: OpenClawConfig) => unknown;
|
||||
setConfiguredCredentialValue?: (configTarget: OpenClawConfig, value: unknown) => void;
|
||||
/** Apply the minimal config needed to select this provider without scattering plugin config writes in core. */
|
||||
applySelectionConfig?: (config: OpenClawConfig) => OpenClawConfig;
|
||||
resolveRuntimeMetadata?: (
|
||||
ctx: WebFetchRuntimeMetadataContext,
|
||||
|
||||
@@ -108,6 +108,9 @@ describe("exec SecretRef id parity", () => {
|
||||
if (id.startsWith("plugins.entries.") && id.includes(".config.webSearch.apiKey")) {
|
||||
return "tools.web.search";
|
||||
}
|
||||
if (id.startsWith("plugins.entries.") && id.includes(".config.webFetch.apiKey")) {
|
||||
return "tools.web.fetch";
|
||||
}
|
||||
if (id.startsWith("tools.web.search.")) {
|
||||
return "tools.web.search";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user