mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix(secrets): align SecretRef inspect/strict behavior across preload/runtime paths (#66818)
* Config: add inspect/strict SecretRef string resolver * CLI: pass resolved/source config snapshots to plugin preload * Slack: keep HTTP route registration config-only * Providers: normalize SecretRef handling for auth and web tools * Secrets: add Exa web search target to registry and docs * Telegram: resolve env SecretRef tokens at runtime * Agents: resolve custom provider env SecretRef ids * Providers: fail closed on blocked SecretRef fallback * Telegram: enforce env SecretRef policy for runtime token refs * Status/Providers/Telegram: tighten SecretRef preload and fallback handling * Providers: enforce env SecretRef policy checks in fallback auth paths * fix: add SecretRef lifecycle changelog entry (#66818) (thanks @joshavant)
This commit is contained in:
@@ -1,13 +1,12 @@
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import {
|
||||
normalizeResolvedSecretInputString,
|
||||
normalizeSecretInput,
|
||||
} from "openclaw/plugin-sdk/secret-input";
|
||||
import { resolveDefaultSecretProviderAlias } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { resolveSecretInputString, normalizeSecretInput } from "openclaw/plugin-sdk/secret-input";
|
||||
|
||||
export const DEFAULT_FIRECRAWL_BASE_URL = "https://api.firecrawl.dev";
|
||||
export const DEFAULT_FIRECRAWL_SEARCH_TIMEOUT_SECONDS = 30;
|
||||
export const DEFAULT_FIRECRAWL_SCRAPE_TIMEOUT_SECONDS = 60;
|
||||
export const DEFAULT_FIRECRAWL_MAX_AGE_MS = 172_800_000;
|
||||
const FIRECRAWL_API_KEY_ENV_VAR = "FIRECRAWL_API_KEY";
|
||||
|
||||
type WebSearchConfig = NonNullable<OpenClawConfig["tools"]>["web"] extends infer Web
|
||||
? Web extends { search?: infer Search }
|
||||
@@ -104,33 +103,101 @@ export function resolveFirecrawlFetchConfig(cfg?: OpenClawConfig): FirecrawlFetc
|
||||
return firecrawl as FirecrawlFetchConfig;
|
||||
}
|
||||
|
||||
function normalizeConfiguredSecret(value: unknown, path: string): string | undefined {
|
||||
return normalizeSecretInput(
|
||||
normalizeResolvedSecretInputString({
|
||||
value,
|
||||
path,
|
||||
}),
|
||||
);
|
||||
type ConfiguredSecretResolution =
|
||||
| { status: "available"; value: string }
|
||||
| { status: "missing" }
|
||||
| { status: "blocked" };
|
||||
|
||||
function canResolveEnvSecretRefInReadOnlyPath(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
provider: string;
|
||||
id: string;
|
||||
}): boolean {
|
||||
const providerConfig = params.cfg?.secrets?.providers?.[params.provider];
|
||||
if (!providerConfig) {
|
||||
return params.provider === resolveDefaultSecretProviderAlias(params.cfg ?? {}, "env");
|
||||
}
|
||||
if (providerConfig.source !== "env") {
|
||||
return false;
|
||||
}
|
||||
const allowlist = providerConfig.allowlist;
|
||||
return !allowlist || allowlist.includes(params.id);
|
||||
}
|
||||
|
||||
function resolveConfiguredSecret(
|
||||
value: unknown,
|
||||
path: string,
|
||||
cfg?: OpenClawConfig,
|
||||
): ConfiguredSecretResolution {
|
||||
const resolved = resolveSecretInputString({
|
||||
value,
|
||||
path,
|
||||
defaults: cfg?.secrets?.defaults,
|
||||
mode: "inspect",
|
||||
});
|
||||
if (resolved.status === "available") {
|
||||
const normalized = normalizeSecretInput(resolved.value);
|
||||
return normalized ? { status: "available", value: normalized } : { status: "missing" };
|
||||
}
|
||||
if (resolved.status === "missing") {
|
||||
return { status: "missing" };
|
||||
}
|
||||
if (resolved.ref.source !== "env") {
|
||||
return { status: "blocked" };
|
||||
}
|
||||
const envVarName = resolved.ref.id.trim();
|
||||
if (envVarName !== FIRECRAWL_API_KEY_ENV_VAR) {
|
||||
return { status: "blocked" };
|
||||
}
|
||||
if (
|
||||
!canResolveEnvSecretRefInReadOnlyPath({
|
||||
cfg,
|
||||
provider: resolved.ref.provider,
|
||||
id: envVarName,
|
||||
})
|
||||
) {
|
||||
return { status: "blocked" };
|
||||
}
|
||||
const envValue = normalizeSecretInput(process.env[envVarName]);
|
||||
return envValue ? { status: "available", value: envValue } : { status: "missing" };
|
||||
}
|
||||
|
||||
export function resolveFirecrawlApiKey(cfg?: OpenClawConfig): string | undefined {
|
||||
const pluginConfig = cfg?.plugins?.entries?.firecrawl?.config as PluginEntryConfig;
|
||||
const search = resolveFirecrawlSearchConfig(cfg);
|
||||
const fetch = resolveFirecrawlFetchConfig(cfg);
|
||||
return (
|
||||
normalizeConfiguredSecret(
|
||||
pluginConfig?.webFetch?.apiKey,
|
||||
"plugins.entries.firecrawl.config.webFetch.apiKey",
|
||||
) ||
|
||||
normalizeConfiguredSecret(
|
||||
search?.apiKey,
|
||||
"plugins.entries.firecrawl.config.webSearch.apiKey",
|
||||
) ||
|
||||
normalizeConfiguredSecret(search?.apiKey, "tools.web.search.firecrawl.apiKey") ||
|
||||
normalizeConfiguredSecret(fetch?.apiKey, "tools.web.fetch.firecrawl.apiKey") ||
|
||||
normalizeSecretInput(process.env.FIRECRAWL_API_KEY) ||
|
||||
undefined
|
||||
);
|
||||
const configuredCandidates: Array<{ value: unknown; path: string }> = [
|
||||
{
|
||||
value: pluginConfig?.webFetch?.apiKey,
|
||||
path: "plugins.entries.firecrawl.config.webFetch.apiKey",
|
||||
},
|
||||
{
|
||||
value: search?.apiKey,
|
||||
path: "plugins.entries.firecrawl.config.webSearch.apiKey",
|
||||
},
|
||||
{
|
||||
value: search?.apiKey,
|
||||
path: "tools.web.search.firecrawl.apiKey",
|
||||
},
|
||||
{
|
||||
value: fetch?.apiKey,
|
||||
path: "tools.web.fetch.firecrawl.apiKey",
|
||||
},
|
||||
];
|
||||
let blockedConfiguredSecret = false;
|
||||
for (const candidate of configuredCandidates) {
|
||||
const resolved = resolveConfiguredSecret(candidate.value, candidate.path, cfg);
|
||||
if (resolved.status === "available") {
|
||||
return resolved.value;
|
||||
}
|
||||
if (resolved.status === "blocked") {
|
||||
blockedConfiguredSecret = true;
|
||||
}
|
||||
}
|
||||
if (blockedConfiguredSecret) {
|
||||
return undefined;
|
||||
}
|
||||
return normalizeSecretInput(process.env[FIRECRAWL_API_KEY_ENV_VAR]) || undefined;
|
||||
}
|
||||
|
||||
export function resolveFirecrawlBaseUrl(cfg?: OpenClawConfig): string {
|
||||
|
||||
@@ -474,6 +474,137 @@ describe("firecrawl tools", () => {
|
||||
expect(resolveFirecrawlBaseUrl({} as OpenClawConfig)).not.toBe(DEFAULT_FIRECRAWL_BASE_URL);
|
||||
});
|
||||
|
||||
it("resolves env SecretRefs for Firecrawl API key without requiring a runtime snapshot", () => {
|
||||
vi.stubEnv("FIRECRAWL_API_KEY", "firecrawl-env-ref-key");
|
||||
const cfg = {
|
||||
plugins: {
|
||||
entries: {
|
||||
firecrawl: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "FIRECRAWL_API_KEY",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(resolveFirecrawlApiKey(cfg)).toBe("firecrawl-env-ref-key");
|
||||
});
|
||||
|
||||
it("does not use env fallback when a non-env SecretRef is configured but unavailable", () => {
|
||||
vi.stubEnv("FIRECRAWL_API_KEY", "firecrawl-env-fallback");
|
||||
const cfg = {
|
||||
plugins: {
|
||||
entries: {
|
||||
firecrawl: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: {
|
||||
source: "file",
|
||||
provider: "vault",
|
||||
id: "/firecrawl/api-key",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(resolveFirecrawlApiKey(cfg)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not read arbitrary env SecretRef ids for Firecrawl API key resolution", () => {
|
||||
vi.stubEnv("UNRELATED_SECRET", "should-not-be-read");
|
||||
const cfg = {
|
||||
plugins: {
|
||||
entries: {
|
||||
firecrawl: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "UNRELATED_SECRET",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(resolveFirecrawlApiKey(cfg)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not resolve env SecretRefs when provider allowlist excludes FIRECRAWL_API_KEY", () => {
|
||||
vi.stubEnv("FIRECRAWL_API_KEY", "firecrawl-env-ref-key");
|
||||
const cfg = {
|
||||
secrets: {
|
||||
providers: {
|
||||
"firecrawl-env": {
|
||||
source: "env",
|
||||
allowlist: ["OTHER_FIRECRAWL_API_KEY"],
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
firecrawl: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "firecrawl-env",
|
||||
id: "FIRECRAWL_API_KEY",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(resolveFirecrawlApiKey(cfg)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not resolve env SecretRefs when provider source is not env", () => {
|
||||
vi.stubEnv("FIRECRAWL_API_KEY", "firecrawl-env-ref-key");
|
||||
const cfg = {
|
||||
secrets: {
|
||||
providers: {
|
||||
"firecrawl-env": {
|
||||
source: "file",
|
||||
path: "/tmp/secrets.json",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
entries: {
|
||||
firecrawl: {
|
||||
config: {
|
||||
webSearch: {
|
||||
apiKey: {
|
||||
source: "env",
|
||||
provider: "firecrawl-env",
|
||||
id: "FIRECRAWL_API_KEY",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(resolveFirecrawlApiKey(cfg)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("only allows the official Firecrawl API host for fetch endpoints", () => {
|
||||
expect(firecrawlClientTesting.resolveEndpoint("https://api.firecrawl.dev", "/v2/scrape")).toBe(
|
||||
"https://api.firecrawl.dev/v2/scrape",
|
||||
|
||||
Reference in New Issue
Block a user