mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 20:35:39 -06:00
feat(firecrawl): add keyless scrape support (#94551)
Merged under maintainer approval while exact-head CI was queue-bound. Co-authored-by: Developers Digest <124798203+developersdigest@users.noreply.github.com> Co-authored-by: Vincent Koc <vincentkoc@users.noreply.github.com>
This commit is contained in:
@@ -340,7 +340,7 @@ session establishment, not on every turn; use `/new`, `/reset`, or a gateway
|
||||
restart after changing native plugin config.
|
||||
|
||||
- `plugins.entries.firecrawl.config.webFetch`: Firecrawl web-fetch provider settings.
|
||||
- `apiKey`: Firecrawl API key (accepts SecretRef). Falls back to `plugins.entries.firecrawl.config.webSearch.apiKey`, legacy `tools.web.fetch.firecrawl.apiKey`, or `FIRECRAWL_API_KEY` env var.
|
||||
- `apiKey`: Optional Firecrawl API key for higher limits (accepts SecretRef). Falls back to `plugins.entries.firecrawl.config.webSearch.apiKey`, legacy `tools.web.fetch.firecrawl.apiKey`, or `FIRECRAWL_API_KEY` env var.
|
||||
- `baseUrl`: Firecrawl API base URL (default: `https://api.firecrawl.dev`; self-hosted overrides must target private/internal endpoints).
|
||||
- `onlyMainContent`: extract only the main content from pages (default: `true`).
|
||||
- `maxAgeMs`: maximum cache age in milliseconds (default: `172800000` / 2 days).
|
||||
|
||||
+1
-1
@@ -857,7 +857,7 @@ lives on the [First-run FAQ](/help/faq-first-run).
|
||||
|
||||
- If you use allowlists, add `web_search`/`web_fetch`/`x_search` or `group:web`.
|
||||
- `web_fetch` is enabled by default (unless explicitly disabled).
|
||||
- If `tools.web.fetch.provider` is omitted, OpenClaw auto-detects the first ready fetch fallback provider from available credentials. Today the bundled provider is Firecrawl.
|
||||
- If `tools.web.fetch.provider` is omitted, OpenClaw auto-detects the first ready fetch fallback provider from configured credentials. Today the bundled provider is Firecrawl.
|
||||
- Daemons read env vars from `~/.openclaw/.env` (or the service environment).
|
||||
|
||||
Docs: [Web tools](/tools/web).
|
||||
|
||||
@@ -154,7 +154,8 @@ See [Web tools](/tools/web).
|
||||
|
||||
### 5) Web fetch tool (Firecrawl)
|
||||
|
||||
`web_fetch` can call **Firecrawl** when an API key is present:
|
||||
`web_fetch` can call **Firecrawl** with keyless starter access. Add an API key
|
||||
for higher limits:
|
||||
|
||||
- `FIRECRAWL_API_KEY` or `plugins.entries.firecrawl.config.webFetch.apiKey`
|
||||
|
||||
|
||||
+20
-9
@@ -2,7 +2,8 @@
|
||||
summary: "Firecrawl search, scrape, and web_fetch fallback"
|
||||
read_when:
|
||||
- You want Firecrawl-backed web extraction
|
||||
- You need a Firecrawl API key
|
||||
- You want keyless Firecrawl web_fetch
|
||||
- You need a Firecrawl API key for search or higher limits
|
||||
- You want Firecrawl as a web_search provider
|
||||
- You want anti-bot extraction for web_fetch
|
||||
title: "Firecrawl"
|
||||
@@ -17,10 +18,12 @@ OpenClaw can use **Firecrawl** in three ways:
|
||||
It is a hosted extraction/search service that supports bot circumvention and caching,
|
||||
which helps with JS-heavy sites or pages that block plain HTTP fetches.
|
||||
|
||||
## Get an API key
|
||||
## Keyless web_fetch and API keys
|
||||
|
||||
1. Create a Firecrawl account and generate an API key.
|
||||
2. Store it in config or set `FIRECRAWL_API_KEY` in the gateway environment.
|
||||
The explicitly selected hosted Firecrawl `web_fetch` fallback supports starter
|
||||
access without an API key. Add `FIRECRAWL_API_KEY` in the gateway environment
|
||||
or configure it when you need higher limits. Firecrawl `web_search` and
|
||||
`firecrawl_scrape` require an API key.
|
||||
|
||||
## Configure Firecrawl search
|
||||
|
||||
@@ -57,17 +60,23 @@ Notes:
|
||||
- `baseUrl` defaults to hosted Firecrawl at `https://api.firecrawl.dev`. Self-hosted overrides are allowed only for private/internal endpoints; HTTP is accepted only for those private targets.
|
||||
- `FIRECRAWL_BASE_URL` is the shared env fallback for Firecrawl search and scrape base URLs.
|
||||
|
||||
## Configure Firecrawl scrape + web_fetch fallback
|
||||
## Configure Firecrawl web_fetch fallback
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
web: {
|
||||
fetch: {
|
||||
provider: "firecrawl", // explicit selection enables keyless fallback
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
firecrawl: {
|
||||
enabled: true,
|
||||
config: {
|
||||
webFetch: {
|
||||
apiKey: "FIRECRAWL_API_KEY_HERE",
|
||||
baseUrl: "https://api.firecrawl.dev",
|
||||
onlyMainContent: true,
|
||||
maxAgeMs: 172800000,
|
||||
@@ -82,13 +91,15 @@ Notes:
|
||||
|
||||
Notes:
|
||||
|
||||
- Firecrawl fallback attempts run only when an API key is available (`plugins.entries.firecrawl.config.webFetch.apiKey` or `FIRECRAWL_API_KEY`).
|
||||
- The explicitly selected Firecrawl `web_fetch` fallback works without an API key. When configured, OpenClaw sends `plugins.entries.firecrawl.config.webFetch.apiKey` or `FIRECRAWL_API_KEY` for higher limits.
|
||||
- Choosing Firecrawl during onboarding or `openclaw configure --section web` enables the plugin and selects Firecrawl for `web_fetch` unless another fetch provider is already configured.
|
||||
- `firecrawl_scrape` requires an API key.
|
||||
- `maxAgeMs` controls how old cached results can be (ms). Default is 2 days.
|
||||
- Legacy `tools.web.fetch.firecrawl.*` config is auto-migrated by `openclaw doctor --fix`.
|
||||
- Firecrawl scrape/base URL overrides follow the same hosted/private rule as search: public hosted traffic uses `https://api.firecrawl.dev`; self-hosted overrides must resolve to private/internal endpoints.
|
||||
- `firecrawl_scrape` rejects obvious private, loopback, metadata, and non-HTTP(S) target URLs before forwarding them to Firecrawl, matching the `web_fetch` target-safety contract for explicit Firecrawl scrape calls.
|
||||
|
||||
`firecrawl_scrape` reuses the same `plugins.entries.firecrawl.config.webFetch.*` settings and env vars.
|
||||
`firecrawl_scrape` reuses the same `plugins.entries.firecrawl.config.webFetch.*` settings and env vars, including its required API key.
|
||||
|
||||
### Self-hosted Firecrawl
|
||||
|
||||
@@ -141,7 +152,7 @@ than basic-only scraping.
|
||||
`web_fetch` extraction order:
|
||||
|
||||
1. Readability (local)
|
||||
2. Firecrawl (if selected or auto-detected as the active web-fetch fallback)
|
||||
2. Firecrawl (when selected, or auto-detected from configured credentials)
|
||||
3. Basic HTML cleanup (last fallback)
|
||||
|
||||
The selection knob is `tools.web.fetch.provider`. If you omit it, OpenClaw
|
||||
|
||||
@@ -48,7 +48,7 @@ Truncate output to this many characters.
|
||||
Runs Readability (main-content extraction) on the HTML response.
|
||||
</Step>
|
||||
<Step title="Fallback (optional)">
|
||||
If Readability fails and Firecrawl is configured, retries through the
|
||||
If Readability fails and Firecrawl is selected, retries through the
|
||||
Firecrawl API with bot-circumvention mode.
|
||||
</Step>
|
||||
<Step title="Cache">
|
||||
@@ -120,7 +120,7 @@ If Readability extraction fails, `web_fetch` can fall back to
|
||||
enabled: true,
|
||||
config: {
|
||||
webFetch: {
|
||||
apiKey: "fc-...", // optional if FIRECRAWL_API_KEY is set
|
||||
// apiKey: "fc-...", // optional; omit for keyless starter access
|
||||
baseUrl: "https://api.firecrawl.dev",
|
||||
onlyMainContent: true,
|
||||
maxAgeMs: 86400000, // cache duration (1 day)
|
||||
@@ -133,11 +133,11 @@ If Readability extraction fails, `web_fetch` can fall back to
|
||||
}
|
||||
```
|
||||
|
||||
`plugins.entries.firecrawl.config.webFetch.apiKey` supports SecretRef objects.
|
||||
`plugins.entries.firecrawl.config.webFetch.apiKey` is optional and supports SecretRef objects.
|
||||
Legacy `tools.web.fetch.firecrawl.*` config is auto-migrated by `openclaw doctor --fix`.
|
||||
|
||||
<Note>
|
||||
If Firecrawl is enabled and its SecretRef is unresolved with no
|
||||
If you configure a Firecrawl API-key SecretRef and it is unresolved with no
|
||||
`FIRECRAWL_API_KEY` env fallback, gateway startup fails fast.
|
||||
</Note>
|
||||
|
||||
@@ -151,7 +151,7 @@ Current runtime behavior:
|
||||
|
||||
- `tools.web.fetch.provider` selects the fetch fallback provider explicitly.
|
||||
- If `provider` is omitted, OpenClaw auto-detects the first ready web-fetch
|
||||
provider from available credentials. Non-sandboxed `web_fetch` can use
|
||||
provider from configured credentials. Non-sandboxed `web_fetch` can use
|
||||
installed plugins that declare `contracts.webFetchProviders` and register a
|
||||
matching provider at runtime. Today the bundled provider is Firecrawl.
|
||||
- Sandboxed `web_fetch` calls stay limited to bundled providers.
|
||||
|
||||
+1
-1
@@ -307,7 +307,7 @@ plugin or run `openclaw doctor --fix` to clean up the stale config.
|
||||
|
||||
- choose it with `tools.web.fetch.provider`
|
||||
- or omit that field and let OpenClaw auto-detect the first ready web-fetch
|
||||
provider from available credentials
|
||||
provider from configured credentials
|
||||
- non-sandboxed `web_fetch` can use installed plugin providers that declare
|
||||
`contracts.webFetchProviders`; sandboxed fetches stay bundled-only
|
||||
- today the bundled web-fetch provider is Firecrawl, configured under
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*",
|
||||
"undici": "8.3.0"
|
||||
"undici": "8.5.0"
|
||||
},
|
||||
"openclaw": {
|
||||
"extensions": [
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"discord-api-types": "0.38.48",
|
||||
"libopus-wasm": "0.2.0",
|
||||
"typebox": "1.1.39",
|
||||
"undici": "8.3.0",
|
||||
"undici": "8.5.0",
|
||||
"ws": "8.21.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
},
|
||||
"webFetch.apiKey": {
|
||||
"label": "Firecrawl Fetch API Key",
|
||||
"help": "Firecrawl API key for web fetch fallback (fallback: FIRECRAWL_API_KEY env var).",
|
||||
"help": "Optional for hosted keyless scraping; add a key for higher limits (fallback: FIRECRAWL_API_KEY env var).",
|
||||
"sensitive": true,
|
||||
"placeholder": "fc-..."
|
||||
},
|
||||
|
||||
@@ -87,6 +87,7 @@ export type FirecrawlScrapeParams = {
|
||||
cfg?: OpenClawConfig;
|
||||
url: string;
|
||||
extractMode: "markdown" | "text";
|
||||
access?: "credential" | "keyless";
|
||||
maxChars?: number;
|
||||
onlyMainContent?: boolean;
|
||||
maxAgeMs?: number;
|
||||
@@ -184,7 +185,7 @@ async function postFirecrawlJson<T>(
|
||||
url: string;
|
||||
mode?: FirecrawlEndpointMode;
|
||||
timeoutSeconds: number;
|
||||
apiKey: string;
|
||||
apiKey?: string;
|
||||
body: Record<string, unknown>;
|
||||
errorLabel: string;
|
||||
},
|
||||
@@ -201,8 +202,10 @@ async function postFirecrawlJson<T>(
|
||||
init: {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
"Content-Type": "application/json",
|
||||
// Hosted Firecrawl accepts starter scrape requests without a token.
|
||||
// Send one only when configured so higher-limit accounts still apply.
|
||||
...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}),
|
||||
},
|
||||
body: JSON.stringify(params.body),
|
||||
},
|
||||
@@ -522,7 +525,9 @@ export async function runFirecrawlScrape(
|
||||
assertFirecrawlScrapeTargetAllowed(params.url);
|
||||
|
||||
const apiKey = resolveFirecrawlApiKey(params.cfg);
|
||||
if (!apiKey) {
|
||||
// Hosted v2/scrape accepts starter requests without a bearer token.
|
||||
// Only the selected web_fetch provider opts into that access mode.
|
||||
if (!apiKey && params.access !== "keyless") {
|
||||
throw new Error(
|
||||
"firecrawl_scrape needs a Firecrawl API key. Set FIRECRAWL_API_KEY in the Gateway environment, or configure plugins.entries.firecrawl.config.webFetch.apiKey.",
|
||||
);
|
||||
|
||||
@@ -14,7 +14,9 @@ function ensureRecord(target: Record<string, unknown>, key: string): Record<stri
|
||||
export const FIRECRAWL_WEB_FETCH_PROVIDER_SHARED = {
|
||||
id: "firecrawl",
|
||||
label: "Firecrawl",
|
||||
hint: "Fetch pages with Firecrawl for JS-heavy or bot-protected sites.",
|
||||
hint: "Fetch pages with keyless starter access; add a key for higher limits.",
|
||||
requiresCredential: false,
|
||||
credentialLabel: "Firecrawl API key (optional)",
|
||||
envVars: ["FIRECRAWL_API_KEY"],
|
||||
placeholder: "fc-...",
|
||||
signupUrl: "https://www.firecrawl.dev/",
|
||||
|
||||
@@ -25,6 +25,7 @@ export function createFirecrawlWebFetchProvider(): WebFetchProviderPlugin {
|
||||
cfg: config,
|
||||
url,
|
||||
extractMode,
|
||||
access: "keyless",
|
||||
maxChars,
|
||||
...(proxy ? { proxy } : {}),
|
||||
...(storeInCache !== undefined ? { storeInCache } : {}),
|
||||
|
||||
@@ -110,6 +110,18 @@ describe("firecrawl tools", () => {
|
||||
throw new Error("expected Firecrawl plugin entry");
|
||||
}
|
||||
expect(pluginEntry.enabled).toBe(true);
|
||||
expect(applied.tools?.web?.fetch?.provider).toBe("firecrawl");
|
||||
|
||||
const preservedFetchProvider = provider.applySelectionConfig({
|
||||
tools: {
|
||||
web: {
|
||||
fetch: {
|
||||
provider: "other",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig);
|
||||
expect(preservedFetchProvider.tools?.web?.fetch?.provider).toBe("other");
|
||||
});
|
||||
|
||||
it("parses scrape payloads into wrapped external-content results", () => {
|
||||
@@ -241,6 +253,72 @@ describe("firecrawl tools", () => {
|
||||
expect(authHeader).toBe("Bearer firecrawl-test-key");
|
||||
});
|
||||
|
||||
it("omits Firecrawl authorization for keyless scrape requests", async () => {
|
||||
let capturedInit: RequestInit | undefined;
|
||||
global.fetch = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
capturedInit = init;
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
success: true,
|
||||
data: {
|
||||
markdown: "# Keyless",
|
||||
metadata: {
|
||||
sourceURL: "https://example.com/keyless-firecrawl",
|
||||
statusCode: 200,
|
||||
},
|
||||
},
|
||||
}),
|
||||
{
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
},
|
||||
);
|
||||
}) as typeof fetch;
|
||||
|
||||
await runActualFirecrawlScrape({
|
||||
cfg: {
|
||||
plugins: {
|
||||
entries: {
|
||||
firecrawl: {
|
||||
config: {
|
||||
webFetch: {
|
||||
baseUrl: "https://api.firecrawl.dev",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
url: "https://example.com/keyless-firecrawl",
|
||||
extractMode: "markdown",
|
||||
access: "keyless",
|
||||
});
|
||||
|
||||
expect(new Headers(capturedInit?.headers).has("Authorization")).toBe(false);
|
||||
});
|
||||
|
||||
it("requires credentials for direct scrape requests", async () => {
|
||||
await expect(
|
||||
runActualFirecrawlScrape({
|
||||
cfg: {
|
||||
plugins: {
|
||||
entries: {
|
||||
firecrawl: {
|
||||
config: {
|
||||
webFetch: {
|
||||
baseUrl: "https://api.firecrawl.dev",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
url: "https://example.com/direct-scrape",
|
||||
extractMode: "markdown",
|
||||
}),
|
||||
).rejects.toThrow("firecrawl_scrape needs a Firecrawl API key");
|
||||
});
|
||||
|
||||
it("blocks private and non-http scrape targets before Firecrawl requests", () => {
|
||||
expect(
|
||||
firecrawlClientTesting.assertFirecrawlScrapeTargetAllowed("https://example.com/page"),
|
||||
@@ -402,6 +480,7 @@ describe("firecrawl tools", () => {
|
||||
|
||||
expect(provider.id).toBe("firecrawl");
|
||||
expect(provider.credentialPath).toBe("plugins.entries.firecrawl.config.webFetch.apiKey");
|
||||
expect(provider.requiresCredential).toBe(false);
|
||||
const pluginEntry = applied.plugins?.entries?.firecrawl;
|
||||
if (!pluginEntry) {
|
||||
throw new Error("expected Firecrawl fetch plugin entry");
|
||||
@@ -430,6 +509,7 @@ describe("firecrawl tools", () => {
|
||||
cfg: { test: true },
|
||||
url: "https://docs.openclaw.ai",
|
||||
extractMode: "markdown",
|
||||
access: "keyless",
|
||||
maxChars: 1500,
|
||||
proxy: "stealth",
|
||||
storeInCache: false,
|
||||
@@ -454,6 +534,7 @@ describe("firecrawl tools", () => {
|
||||
cfg: { test: true },
|
||||
url: "https://docs.openclaw.ai",
|
||||
extractMode: "markdown",
|
||||
access: "keyless",
|
||||
maxChars: 1500,
|
||||
});
|
||||
await expect(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Firecrawl plugin module implements web search shared behavior.
|
||||
import {
|
||||
createWebSearchProviderContractFields,
|
||||
enablePluginInConfig,
|
||||
type WebSearchProviderPlugin,
|
||||
} from "openclaw/plugin-sdk/provider-web-search-contract";
|
||||
|
||||
@@ -22,6 +23,12 @@ export function getConfiguredFirecrawlFetchCredentialFallback(config?: {
|
||||
}
|
||||
|
||||
export function buildFirecrawlWebSearchProviderBase(): Omit<WebSearchProviderPlugin, "createTool"> {
|
||||
const contractFields = createWebSearchProviderContractFields({
|
||||
credentialPath: FIRECRAWL_CREDENTIAL_PATH,
|
||||
searchCredential: { type: "scoped", scopeId: "firecrawl" },
|
||||
configuredCredential: { pluginId: "firecrawl" },
|
||||
});
|
||||
|
||||
return {
|
||||
id: "firecrawl",
|
||||
label: "Firecrawl Search",
|
||||
@@ -34,12 +41,26 @@ export function buildFirecrawlWebSearchProviderBase(): Omit<WebSearchProviderPlu
|
||||
docsUrl: "https://docs.openclaw.ai/tools/firecrawl",
|
||||
autoDetectOrder: 60,
|
||||
credentialPath: FIRECRAWL_CREDENTIAL_PATH,
|
||||
...createWebSearchProviderContractFields({
|
||||
credentialPath: FIRECRAWL_CREDENTIAL_PATH,
|
||||
searchCredential: { type: "scoped", scopeId: "firecrawl" },
|
||||
configuredCredential: { pluginId: "firecrawl" },
|
||||
selectionPluginId: "firecrawl",
|
||||
}),
|
||||
...contractFields,
|
||||
applySelectionConfig: (config) => {
|
||||
const enabled = enablePluginInConfig(config, "firecrawl");
|
||||
if (!enabled.enabled || enabled.config.tools?.web?.fetch?.provider) {
|
||||
return enabled.config;
|
||||
}
|
||||
return {
|
||||
...enabled.config,
|
||||
tools: {
|
||||
...enabled.config.tools,
|
||||
web: {
|
||||
...enabled.config.tools?.web,
|
||||
fetch: {
|
||||
...enabled.config.tools?.web?.fetch,
|
||||
provider: "firecrawl",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
getConfiguredCredentialFallback: getConfiguredFirecrawlFetchCredentialFallback,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"description": "OpenClaw Matrix QA runner plugin",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"undici": "8.3.0"
|
||||
"undici": "8.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/matrix": "workspace:*",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"@grammyjs/transformer-throttler": "1.2.1",
|
||||
"grammy": "1.43.0",
|
||||
"typebox": "1.1.39",
|
||||
"undici": "8.3.0"
|
||||
"undici": "8.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@openclaw/plugin-sdk": "workspace:*"
|
||||
|
||||
+1
-1
@@ -1949,7 +1949,7 @@
|
||||
"tslog": "4.10.2",
|
||||
"typebox": "1.1.39",
|
||||
"typescript": "6.0.3",
|
||||
"undici": "8.3.0",
|
||||
"undici": "8.5.0",
|
||||
"web-push": "3.6.7",
|
||||
"web-tree-sitter": "0.26.9",
|
||||
"ws": "8.21.0",
|
||||
|
||||
Generated
+22
-22
@@ -87,7 +87,7 @@ importers:
|
||||
version: 0.3.0
|
||||
'@openclaw/proxyline':
|
||||
specifier: 0.3.3
|
||||
version: 0.3.3(undici@8.3.0)
|
||||
version: 0.3.3(undici@8.5.0)
|
||||
chalk:
|
||||
specifier: 5.6.2
|
||||
version: 5.6.2
|
||||
@@ -188,8 +188,8 @@ importers:
|
||||
specifier: 6.0.3
|
||||
version: 6.0.3
|
||||
undici:
|
||||
specifier: 8.3.0
|
||||
version: 8.3.0
|
||||
specifier: 8.5.0
|
||||
version: 8.5.0
|
||||
web-push:
|
||||
specifier: 3.6.7
|
||||
version: 3.6.7
|
||||
@@ -442,8 +442,8 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/plugin-sdk
|
||||
undici:
|
||||
specifier: 8.3.0
|
||||
version: 8.3.0
|
||||
specifier: 8.5.0
|
||||
version: 8.5.0
|
||||
|
||||
extensions/byteplus:
|
||||
devDependencies:
|
||||
@@ -689,8 +689,8 @@ importers:
|
||||
specifier: 1.1.39
|
||||
version: 1.1.39
|
||||
undici:
|
||||
specifier: 8.3.0
|
||||
version: 8.3.0
|
||||
specifier: 8.5.0
|
||||
version: 8.5.0
|
||||
ws:
|
||||
specifier: 8.21.0
|
||||
version: 8.21.0
|
||||
@@ -1364,8 +1364,8 @@ importers:
|
||||
extensions/qa-matrix:
|
||||
dependencies:
|
||||
undici:
|
||||
specifier: 8.3.0
|
||||
version: 8.3.0
|
||||
specifier: 8.5.0
|
||||
version: 8.5.0
|
||||
devDependencies:
|
||||
'@openclaw/matrix':
|
||||
specifier: workspace:*
|
||||
@@ -1533,8 +1533,8 @@ importers:
|
||||
specifier: 1.1.39
|
||||
version: 1.1.39
|
||||
undici:
|
||||
specifier: 8.3.0
|
||||
version: 8.3.0
|
||||
specifier: 8.5.0
|
||||
version: 8.5.0
|
||||
devDependencies:
|
||||
'@openclaw/plugin-sdk':
|
||||
specifier: workspace:*
|
||||
@@ -7375,12 +7375,12 @@ packages:
|
||||
undici-types@7.24.6:
|
||||
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
|
||||
|
||||
undici@7.27.2:
|
||||
resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==}
|
||||
undici@7.28.0:
|
||||
resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
|
||||
undici@8.3.0:
|
||||
resolution: {integrity: sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==}
|
||||
undici@8.5.0:
|
||||
resolution: {integrity: sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==}
|
||||
engines: {node: '>=22.19.0'}
|
||||
|
||||
unhomoglyph@1.0.6:
|
||||
@@ -9223,9 +9223,9 @@ snapshots:
|
||||
jszip: 3.10.1
|
||||
tar: 7.5.16
|
||||
|
||||
'@openclaw/proxyline@0.3.3(undici@8.3.0)':
|
||||
'@openclaw/proxyline@0.3.3(undici@8.5.0)':
|
||||
dependencies:
|
||||
undici: 8.3.0
|
||||
undici: 8.5.0
|
||||
|
||||
'@opentelemetry/api-logs@0.219.0':
|
||||
dependencies:
|
||||
@@ -11895,7 +11895,7 @@ snapshots:
|
||||
saxes: 6.0.0
|
||||
symbol-tree: 3.2.4
|
||||
tough-cookie: 4.1.3
|
||||
undici: 7.27.2
|
||||
undici: 7.28.0
|
||||
w3c-xmlserializer: 5.0.0
|
||||
webidl-conversions: 8.0.1
|
||||
whatwg-mimetype: 5.0.0
|
||||
@@ -12763,7 +12763,7 @@ snapshots:
|
||||
'@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3)
|
||||
'@mozilla/readability': 0.6.0
|
||||
'@openclaw/fs-safe': 0.3.0
|
||||
'@openclaw/proxyline': 0.3.3(undici@8.3.0)
|
||||
'@openclaw/proxyline': 0.3.3(undici@8.5.0)
|
||||
'@silvia-odwyer/photon-node': 0.3.4
|
||||
chalk: 5.6.2
|
||||
chokidar: 5.0.0
|
||||
@@ -12801,7 +12801,7 @@ snapshots:
|
||||
tslog: 4.10.2
|
||||
typebox: 1.1.39
|
||||
typescript: 6.0.3
|
||||
undici: 8.3.0
|
||||
undici: 8.5.0
|
||||
web-push: 3.6.7
|
||||
web-tree-sitter: 0.26.9
|
||||
ws: 8.21.0
|
||||
@@ -13873,9 +13873,9 @@ snapshots:
|
||||
|
||||
undici-types@7.24.6: {}
|
||||
|
||||
undici@7.27.2: {}
|
||||
undici@7.28.0: {}
|
||||
|
||||
undici@8.3.0: {}
|
||||
undici@8.5.0: {}
|
||||
|
||||
unhomoglyph@1.0.6: {}
|
||||
|
||||
|
||||
@@ -420,15 +420,25 @@ describe("runConfigureWizard", () => {
|
||||
|
||||
it("persists provider-owned web search config changes returned by setupSearch", async () => {
|
||||
setupBaseWizardState();
|
||||
mocks.setupSearch.mockImplementation(async (cfg: OpenClawConfig) =>
|
||||
createEnabledWebSearchConfig("firecrawl", {
|
||||
mocks.setupSearch.mockImplementation(async (cfg: OpenClawConfig) => {
|
||||
const configured = createEnabledWebSearchConfig("firecrawl", {
|
||||
enabled: true,
|
||||
config: { webSearch: { apiKey: "fc-entered-key" } },
|
||||
})(cfg),
|
||||
);
|
||||
})(cfg);
|
||||
return {
|
||||
...configured,
|
||||
tools: {
|
||||
...configured.tools,
|
||||
web: {
|
||||
...configured.tools?.web,
|
||||
fetch: { provider: "firecrawl" },
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
queueWizardPrompts({
|
||||
select: [],
|
||||
confirm: [true, false],
|
||||
confirm: [true, true],
|
||||
});
|
||||
|
||||
await runWebConfigureWizard();
|
||||
@@ -442,6 +452,12 @@ describe("runConfigureWizard", () => {
|
||||
const search = getWebSearch(written);
|
||||
expect(search.provider).toBe("firecrawl");
|
||||
expect(search.enabled).toBe(true);
|
||||
const tools = requireRecord(written.tools, "tools config");
|
||||
const web = requireRecord(tools.web, "web config");
|
||||
expect(requireRecord(web.fetch, "web fetch config")).toEqual({
|
||||
enabled: true,
|
||||
provider: "firecrawl",
|
||||
});
|
||||
const firecrawl = getPluginEntry(written, "firecrawl");
|
||||
expect(firecrawl.enabled).toBe(true);
|
||||
const firecrawlConfig = requireRecord(firecrawl.config, "firecrawl config");
|
||||
@@ -449,6 +465,45 @@ describe("runConfigureWizard", () => {
|
||||
"fc-entered-key",
|
||||
);
|
||||
expect(mocks.setupSearch).toHaveBeenCalledOnce();
|
||||
expect(mocks.setupSearch).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
{ preserveDisabledSearchState: false },
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps web_search disabled when provider setup has no credential", async () => {
|
||||
setupBaseWizardState();
|
||||
mocks.setupSearch.mockImplementation(async (cfg: OpenClawConfig) => ({
|
||||
...cfg,
|
||||
tools: {
|
||||
...cfg.tools,
|
||||
web: {
|
||||
...cfg.tools?.web,
|
||||
fetch: { provider: "firecrawl" },
|
||||
search: { enabled: false, provider: "firecrawl" },
|
||||
},
|
||||
},
|
||||
}));
|
||||
queueWizardPrompts({
|
||||
select: [],
|
||||
confirm: [true, true],
|
||||
});
|
||||
|
||||
await runWebConfigureWizard();
|
||||
|
||||
const written = requireWriteConfig();
|
||||
expect(getWebSearch(written)).toMatchObject({
|
||||
enabled: false,
|
||||
provider: "firecrawl",
|
||||
});
|
||||
const tools = requireRecord(written.tools, "tools config");
|
||||
const web = requireRecord(tools.web, "web config");
|
||||
expect(requireRecord(web.fetch, "web fetch config")).toEqual({
|
||||
enabled: true,
|
||||
provider: "firecrawl",
|
||||
});
|
||||
});
|
||||
|
||||
it("notes unavailable web search providers under plugin policy", async () => {
|
||||
|
||||
@@ -337,10 +337,14 @@ async function promptWebToolsConfig(
|
||||
};
|
||||
}
|
||||
} else {
|
||||
workingConfig = await setupSearch(workingConfig, runtime, prompter);
|
||||
workingConfig = await setupSearch(workingConfig, runtime, prompter, {
|
||||
preserveDisabledSearchState: false,
|
||||
});
|
||||
const selectedSearch = workingConfig.tools?.web?.search;
|
||||
nextSearch = {
|
||||
...workingConfig.tools?.web?.search,
|
||||
enabled: workingConfig.tools?.web?.search?.provider ? true : existingSearch?.enabled,
|
||||
...selectedSearch,
|
||||
enabled:
|
||||
selectedSearch?.enabled ?? (selectedSearch?.provider ? true : existingSearch?.enabled),
|
||||
openaiCodex: {
|
||||
...existingSearch?.openaiCodex,
|
||||
...(nextSearch.openaiCodex as Record<string, unknown> | undefined),
|
||||
@@ -359,7 +363,7 @@ async function promptWebToolsConfig(
|
||||
);
|
||||
|
||||
const nextFetch = {
|
||||
...existingFetch,
|
||||
...workingConfig.tools?.web?.fetch,
|
||||
enabled: enableFetch,
|
||||
};
|
||||
|
||||
|
||||
@@ -96,7 +96,19 @@ function createSearchProviderEntry(id: string): PluginWebSearchProviderEntry {
|
||||
>;
|
||||
entries[metadata.pluginId] = { ...entries[metadata.pluginId], enabled: true };
|
||||
next.plugins = { ...next.plugins, entries };
|
||||
return next;
|
||||
if (id !== "firecrawl" || next.tools?.web?.fetch?.provider) {
|
||||
return next;
|
||||
}
|
||||
return {
|
||||
...next,
|
||||
tools: {
|
||||
...next.tools,
|
||||
web: {
|
||||
...next.tools?.web,
|
||||
fetch: { provider: "firecrawl" },
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
if (id === "kimi") {
|
||||
@@ -391,7 +403,7 @@ describe("setupSearch", () => {
|
||||
});
|
||||
const result = await setupSearch(cfg, runtime, prompter);
|
||||
expect(result.tools?.web?.search?.provider).toBe("brave");
|
||||
expect(result.tools?.web?.search?.enabled).toBeUndefined();
|
||||
expect(result.tools?.web?.search?.enabled).toBe(false);
|
||||
const missingNote = notes.find((n) => n.message.includes("No Brave Search API key stored"));
|
||||
expect(missingNote?.message).toContain("No Brave Search API key stored");
|
||||
} finally {
|
||||
@@ -403,6 +415,30 @@ describe("setupSearch", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps keyless Firecrawl fetch configured when search setup has no key", async () => {
|
||||
const original = process.env.FIRECRAWL_API_KEY;
|
||||
delete process.env.FIRECRAWL_API_KEY;
|
||||
try {
|
||||
const { prompter } = createPrompter({
|
||||
selectValue: "firecrawl",
|
||||
textValue: "",
|
||||
});
|
||||
|
||||
const result = await setupSearch({}, runtime, prompter);
|
||||
|
||||
expect(result.tools?.web?.search?.provider).toBe("firecrawl");
|
||||
expect(result.tools?.web?.search?.enabled).toBe(false);
|
||||
expect(result.tools?.web?.fetch?.provider).toBe("firecrawl");
|
||||
expect(result.plugins?.entries?.firecrawl?.enabled).toBe(true);
|
||||
} finally {
|
||||
if (original === undefined) {
|
||||
delete process.env.FIRECRAWL_API_KEY;
|
||||
} else {
|
||||
process.env.FIRECRAWL_API_KEY = original;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps existing key when user leaves input blank", async () => {
|
||||
const result = await runBlankPerplexityKeyEntry(
|
||||
"existing-key", // pragma: allowlist secret
|
||||
@@ -481,7 +517,7 @@ describe("setupSearch", () => {
|
||||
});
|
||||
expect(prompter.text).toHaveBeenCalled();
|
||||
expect(result.tools?.web?.search?.provider).toBe("grok");
|
||||
expect(result.tools?.web?.search?.enabled).toBeUndefined();
|
||||
expect(result.tools?.web?.search?.enabled).toBe(false);
|
||||
} finally {
|
||||
if (original === undefined) {
|
||||
delete process.env.XAI_API_KEY;
|
||||
|
||||
@@ -396,6 +396,46 @@ describe("runSearchSetupFlow", () => {
|
||||
expect(xaiConfig?.xSearch?.model).toBe("grok-4-1-fast");
|
||||
});
|
||||
|
||||
it("allows an explicit setup flow to reenable credential-ready web_search", async () => {
|
||||
const select = vi.fn().mockResolvedValueOnce("grok").mockResolvedValueOnce("no");
|
||||
const prompter = createWizardPrompter({
|
||||
select: select as never,
|
||||
});
|
||||
|
||||
const next = await runSearchSetupFlow(
|
||||
{
|
||||
plugins: {
|
||||
allow: ["xai"],
|
||||
entries: {
|
||||
xai: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: "xai-test-key",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
web: {
|
||||
search: {
|
||||
enabled: false,
|
||||
provider: "grok",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
createNonExitingRuntime(),
|
||||
prompter,
|
||||
{ preserveDisabledSearchState: false },
|
||||
);
|
||||
|
||||
expect(next.tools?.web?.search).toMatchObject({
|
||||
enabled: true,
|
||||
provider: "grok",
|
||||
});
|
||||
});
|
||||
|
||||
it("installs an external catalog search provider before enabling it", async () => {
|
||||
const select = vi.fn().mockResolvedValueOnce("brave");
|
||||
const text = vi.fn().mockResolvedValue("brave-test-key");
|
||||
|
||||
+19
-10
@@ -353,6 +353,7 @@ function preserveDisabledState(original: OpenClawConfig, result: OpenClawConfig)
|
||||
|
||||
type SetupSearchOptions = {
|
||||
quickstartDefaults?: boolean;
|
||||
preserveDisabledSearchState?: boolean;
|
||||
secretInputMode?: SecretInputMode;
|
||||
};
|
||||
|
||||
@@ -388,7 +389,9 @@ async function finalizeSearchProviderSetup(params: {
|
||||
}
|
||||
next = installed.cfg;
|
||||
}
|
||||
next = preserveDisabledState(params.originalConfig, next);
|
||||
if (params.opts?.preserveDisabledSearchState !== false) {
|
||||
next = preserveDisabledState(params.originalConfig, next);
|
||||
}
|
||||
if (!params.entry.runSetup) {
|
||||
return next;
|
||||
}
|
||||
@@ -399,7 +402,9 @@ async function finalizeSearchProviderSetup(params: {
|
||||
quickstartDefaults: params.opts?.quickstartDefaults,
|
||||
secretInputMode: params.opts?.secretInputMode,
|
||||
});
|
||||
return preserveDisabledState(params.originalConfig, next);
|
||||
return params.opts?.preserveDisabledSearchState === false
|
||||
? next
|
||||
: preserveDisabledState(params.originalConfig, next);
|
||||
}
|
||||
|
||||
export async function runSearchSetupFlow(
|
||||
@@ -686,16 +691,20 @@ export async function runSearchSetupFlow(
|
||||
|
||||
const search: SearchConfig = {
|
||||
...config.tools?.web?.search,
|
||||
enabled: false,
|
||||
provider: choice,
|
||||
};
|
||||
return {
|
||||
...config,
|
||||
tools: {
|
||||
...config.tools,
|
||||
web: {
|
||||
...config.tools?.web,
|
||||
search,
|
||||
return applySearchProviderSelectionConfig(
|
||||
{
|
||||
...config,
|
||||
tools: {
|
||||
...config.tools,
|
||||
web: {
|
||||
...config.tools?.web,
|
||||
search,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
entry,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -404,7 +404,8 @@ export async function resolveRuntimeWebProviderSelection<
|
||||
let keylessFallbackProvider: TProvider | undefined;
|
||||
|
||||
for (const provider of candidates) {
|
||||
if (provider.requiresCredential === false) {
|
||||
const isKeyless = provider.requiresCredential === false;
|
||||
if (isKeyless) {
|
||||
if (!params.configuredProvider && !params.allowKeylessAutoSelect) {
|
||||
continue;
|
||||
}
|
||||
@@ -412,13 +413,6 @@ export async function resolveRuntimeWebProviderSelection<
|
||||
keylessFallbackProvider ||= provider;
|
||||
continue;
|
||||
}
|
||||
selectedProvider = provider.id;
|
||||
selectedResolution = {
|
||||
source: "missing" as TSource,
|
||||
secretRefConfigured: false,
|
||||
fallbackUsedAfterRefFailure: false,
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
const path = params.inactivePathsForProvider(provider)[0] ?? "";
|
||||
@@ -507,6 +501,18 @@ export async function resolveRuntimeWebProviderSelection<
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
isKeyless &&
|
||||
selectedCandidateResolution.secretRefConfigured &&
|
||||
!selectedCandidateResolution.value
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isKeyless && !params.configuredProvider && !selectedCandidateResolution.value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (params.configuredProvider) {
|
||||
selectedProvider = provider.id;
|
||||
selectedResolution = selectedCandidateResolution;
|
||||
@@ -525,6 +531,24 @@ export async function resolveRuntimeWebProviderSelection<
|
||||
break;
|
||||
}
|
||||
|
||||
if (isKeyless) {
|
||||
selectedProvider = provider.id;
|
||||
selectedResolution = selectedCandidateResolution;
|
||||
if (selectedCandidateResolution.value) {
|
||||
setResolvedCredentialPath({
|
||||
resolvedConfig: params.resolvedConfig,
|
||||
path: selectedCandidatePath,
|
||||
value: selectedCandidateResolution.value,
|
||||
});
|
||||
params.setResolvedCredential({
|
||||
resolvedConfig: params.resolvedConfig,
|
||||
provider,
|
||||
value: selectedCandidateResolution.value,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (selectedCandidateResolution.value) {
|
||||
selectedProvider = provider.id;
|
||||
selectedResolution = selectedCandidateResolution;
|
||||
|
||||
@@ -256,6 +256,7 @@ function buildTestWebFetchProviders(): PluginWebFetchProviderEntry[] {
|
||||
id: "firecrawl",
|
||||
label: "firecrawl",
|
||||
hint: "firecrawl test provider",
|
||||
requiresCredential: false,
|
||||
envVars: ["FIRECRAWL_API_KEY"],
|
||||
placeholder: "fc-...",
|
||||
signupUrl: "https://example.com/firecrawl",
|
||||
@@ -582,6 +583,41 @@ describe("runtime web tools resolution", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("selects the configured keyless Firecrawl fetch provider without an API key", async () => {
|
||||
const { metadata } = await runRuntimeWebTools({
|
||||
config: asConfig({
|
||||
tools: {
|
||||
web: {
|
||||
fetch: {
|
||||
provider: "firecrawl",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(metadata.fetch.providerSource).toBe("configured");
|
||||
expect(metadata.fetch.selectedProvider).toBe("firecrawl");
|
||||
expect(metadata.fetch.selectedProviderKeySource).toBe("missing");
|
||||
});
|
||||
|
||||
it("does not auto-select keyless Firecrawl fetch without a credential", async () => {
|
||||
const { metadata } = await runRuntimeWebTools({
|
||||
config: asConfig({
|
||||
tools: {
|
||||
web: {
|
||||
fetch: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(metadata.fetch.providerSource).toBe("none");
|
||||
expect(metadata.fetch.selectedProvider).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not auto-select a keyless provider when no credentials are configured", async () => {
|
||||
const { metadata } = await runRuntimeWebTools({
|
||||
config: asConfig({
|
||||
|
||||
@@ -512,12 +512,11 @@ function readConfiguredFetchProviderCredentialFallback(params: {
|
||||
}
|
||||
|
||||
function inactivePathsForFetchProvider(provider: PluginWebFetchProviderEntry): string[] {
|
||||
if (provider.requiresCredential === false) {
|
||||
return [];
|
||||
}
|
||||
return provider.inactiveSecretPaths?.length
|
||||
? provider.inactiveSecretPaths
|
||||
: [provider.credentialPath];
|
||||
: provider.credentialPath
|
||||
? [provider.credentialPath]
|
||||
: [];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -179,6 +179,36 @@ describe("web fetch runtime", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses an explicitly configured keyless provider without an API key", () => {
|
||||
const provider = createFirecrawlProvider({
|
||||
requiresCredential: false,
|
||||
});
|
||||
resolvePluginWebFetchProvidersMock.mockReturnValue([provider]);
|
||||
|
||||
const resolved = resolveWebFetchDefinition({
|
||||
config: {
|
||||
tools: {
|
||||
web: {
|
||||
fetch: {
|
||||
provider: "firecrawl",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
});
|
||||
|
||||
expect(requireResolvedWebFetch(resolved).provider.id).toBe("firecrawl");
|
||||
});
|
||||
|
||||
it("does not auto-detect a keyless provider without a credential", () => {
|
||||
const provider = createFirecrawlProvider({
|
||||
requiresCredential: false,
|
||||
});
|
||||
resolvePluginWebFetchProvidersMock.mockReturnValue([provider]);
|
||||
|
||||
expect(resolveWebFetchDefinition({ config: {} })).toBeNull();
|
||||
});
|
||||
|
||||
it("auto-detects providers from configured fallback credentials", () => {
|
||||
const provider = createFirecrawlProvider({
|
||||
getConfiguredCredentialFallback: (config) => {
|
||||
|
||||
+33
-11
@@ -1,5 +1,12 @@
|
||||
/** Runtime provider selection and tool construction for the `web_fetch` tool. */
|
||||
import { normalizeLowercaseStringOrEmpty } from "@openclaw/normalization-core/string-coerce";
|
||||
import {
|
||||
hasWebProviderEntryCredential,
|
||||
providerRequiresCredential,
|
||||
readWebProviderEnvValue,
|
||||
resolveWebProviderConfig,
|
||||
resolveWebProviderDefinition,
|
||||
} from "../../packages/web-content-core/src/provider-runtime-shared.js";
|
||||
import type { OpenClawConfig } from "../config/types.js";
|
||||
import { logVerbose } from "../globals.js";
|
||||
import type {
|
||||
@@ -13,13 +20,6 @@ import {
|
||||
import { sortWebFetchProvidersForAutoDetect } from "../plugins/web-fetch-providers.shared.js";
|
||||
import { getActiveRuntimeWebToolsMetadata } from "../secrets/runtime-web-tools-state.js";
|
||||
import type { RuntimeWebFetchMetadata } from "../secrets/runtime-web-tools.types.js";
|
||||
import {
|
||||
hasWebProviderEntryCredential,
|
||||
providerRequiresCredential,
|
||||
readWebProviderEnvValue,
|
||||
resolveWebProviderConfig,
|
||||
resolveWebProviderDefinition,
|
||||
} from "../../packages/web-content-core/src/provider-runtime-shared.js";
|
||||
|
||||
// Runtime provider selection for the web_fetch tool. It resolves config,
|
||||
// credentials, runtime metadata, and sandbox-safe bundled provider scopes.
|
||||
@@ -38,10 +38,7 @@ type ResolveWebFetchDefinitionParams = {
|
||||
};
|
||||
|
||||
/** Resolves whether web_fetch is enabled for the current config/sandbox. */
|
||||
function resolveWebFetchEnabled(params: {
|
||||
fetch?: WebFetchConfig;
|
||||
sandboxed?: boolean;
|
||||
}): boolean {
|
||||
function resolveWebFetchEnabled(params: { fetch?: WebFetchConfig; sandboxed?: boolean }): boolean {
|
||||
if (typeof params.fetch?.enabled === "boolean") {
|
||||
return params.fetch.enabled;
|
||||
}
|
||||
@@ -78,6 +75,28 @@ function hasEntryCredential(
|
||||
});
|
||||
}
|
||||
|
||||
function hasAutoDetectCredential(
|
||||
provider: Pick<
|
||||
PluginWebFetchProviderEntry,
|
||||
| "envVars"
|
||||
| "getConfiguredCredentialFallback"
|
||||
| "getConfiguredCredentialValue"
|
||||
| "getCredentialValue"
|
||||
| "requiresCredential"
|
||||
>,
|
||||
config: OpenClawConfig | undefined,
|
||||
fetch: WebFetchConfig | undefined,
|
||||
): boolean {
|
||||
return hasEntryCredential(
|
||||
{
|
||||
...provider,
|
||||
requiresCredential: true,
|
||||
},
|
||||
config,
|
||||
fetch,
|
||||
);
|
||||
}
|
||||
|
||||
/** Reports whether a web_fetch provider has usable credentials. */
|
||||
export function isWebFetchProviderConfigured(params: {
|
||||
provider: Pick<
|
||||
@@ -128,6 +147,9 @@ function resolveWebFetchProviderId(params: {
|
||||
|
||||
for (const provider of providers) {
|
||||
if (!providerRequiresCredential(provider)) {
|
||||
if (!hasAutoDetectCredential(provider, params.config, params.fetch)) {
|
||||
continue;
|
||||
}
|
||||
logVerbose(
|
||||
`web_fetch: ${raw ? `invalid configured provider "${raw}", ` : ""}auto-detected keyless provider "${provider.id}"`,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user