Skip to content

Commit 1b921e4

Browse files
committed
Fix Tavily SecretRef env fallback
1 parent 860c0ad commit 1b921e4

2 files changed

Lines changed: 180 additions & 15 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
2+
import { afterEach, describe, expect, it, vi } from "vitest";
3+
import { resolveTavilyApiKey } from "./config.js";
4+
5+
function configWithApiKey(apiKey: unknown, extra?: Partial<OpenClawConfig>): OpenClawConfig {
6+
return {
7+
...extra,
8+
plugins: {
9+
entries: {
10+
tavily: {
11+
config: {
12+
webSearch: {
13+
apiKey,
14+
},
15+
},
16+
},
17+
},
18+
},
19+
} as OpenClawConfig;
20+
}
21+
22+
describe("resolveTavilyApiKey", () => {
23+
afterEach(() => {
24+
vi.unstubAllEnvs();
25+
});
26+
27+
it("returns a configured literal apiKey", () => {
28+
vi.stubEnv("TAVILY_API_KEY", "env-key");
29+
30+
expect(resolveTavilyApiKey(configWithApiKey("configured-key"))).toBe("configured-key");
31+
});
32+
33+
it("falls back to process.env.TAVILY_API_KEY for a matching unresolved env SecretRef", () => {
34+
vi.stubEnv("TAVILY_API_KEY", "env-fallback-key");
35+
36+
expect(
37+
resolveTavilyApiKey(
38+
configWithApiKey({
39+
source: "env",
40+
provider: "default",
41+
id: "TAVILY_API_KEY",
42+
}),
43+
),
44+
).toBe("env-fallback-key");
45+
});
46+
47+
it("allows a configured env provider when its allowlist includes TAVILY_API_KEY", () => {
48+
vi.stubEnv("TAVILY_API_KEY", "env-fallback-key");
49+
50+
expect(
51+
resolveTavilyApiKey(
52+
configWithApiKey(
53+
{
54+
source: "env",
55+
provider: "managed-env",
56+
id: "TAVILY_API_KEY",
57+
},
58+
{
59+
secrets: {
60+
providers: {
61+
"managed-env": {
62+
source: "env",
63+
allowlist: ["TAVILY_API_KEY"],
64+
},
65+
},
66+
},
67+
} as Partial<OpenClawConfig>,
68+
),
69+
),
70+
).toBe("env-fallback-key");
71+
});
72+
73+
it.each([
74+
{
75+
name: "file SecretRef",
76+
apiKey: {
77+
source: "file",
78+
provider: "default",
79+
id: "/etc/secrets/tavily",
80+
},
81+
},
82+
{
83+
name: "exec SecretRef",
84+
apiKey: {
85+
source: "exec",
86+
provider: "default",
87+
id: "TAVILY_API_KEY",
88+
},
89+
},
90+
{
91+
name: "different env id",
92+
apiKey: {
93+
source: "env",
94+
provider: "default",
95+
id: "OTHER_API_KEY",
96+
},
97+
},
98+
{
99+
name: "env provider with a blocking allowlist",
100+
apiKey: {
101+
source: "env",
102+
provider: "managed-env",
103+
id: "TAVILY_API_KEY",
104+
},
105+
extra: {
106+
secrets: {
107+
providers: {
108+
"managed-env": {
109+
source: "env",
110+
allowlist: [],
111+
},
112+
},
113+
},
114+
} as Partial<OpenClawConfig>,
115+
},
116+
])("does not fall back to process.env.TAVILY_API_KEY for $name", ({ apiKey, extra }) => {
117+
vi.stubEnv("TAVILY_API_KEY", "env-fallback-key");
118+
119+
expect(resolveTavilyApiKey(configWithApiKey(apiKey, extra))).toBeUndefined();
120+
});
121+
122+
it("returns undefined when neither config nor env value is set", () => {
123+
expect(resolveTavilyApiKey(configWithApiKey(undefined))).toBeUndefined();
124+
});
125+
});

extensions/tavily/src/config.ts

Lines changed: 55 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
// Tavily helper module supports config behavior.
22
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
3+
import { canResolveEnvSecretRefInReadOnlyPath } from "openclaw/plugin-sdk/extension-shared";
34
import { resolvePositiveTimeoutSeconds } from "openclaw/plugin-sdk/provider-web-search";
4-
import {
5-
normalizeResolvedSecretInputString,
6-
normalizeSecretInput,
7-
} from "openclaw/plugin-sdk/secret-input";
5+
import { resolveSecretInputString, normalizeSecretInput } from "openclaw/plugin-sdk/secret-input";
86
import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
97

108
export const DEFAULT_TAVILY_BASE_URL = "https://api.tavily.com";
119
export const DEFAULT_TAVILY_SEARCH_TIMEOUT_SECONDS = 30;
1210
export const DEFAULT_TAVILY_EXTRACT_TIMEOUT_SECONDS = 60;
11+
const TAVILY_API_KEY_ENV_VAR = "TAVILY_API_KEY";
1312

1413
type TavilySearchConfig =
1514
| {
@@ -34,22 +33,63 @@ export function resolveTavilySearchConfig(cfg?: OpenClawConfig): TavilySearchCon
3433
return undefined;
3534
}
3635

37-
function normalizeConfiguredSecret(value: unknown, path: string): string | undefined {
38-
return normalizeSecretInput(
39-
normalizeResolvedSecretInputString({
40-
value,
41-
path,
42-
}),
43-
);
36+
type ConfiguredSecretResolution =
37+
| { status: "available"; value: string }
38+
| { status: "missing" }
39+
| { status: "blocked" };
40+
41+
function resolveConfiguredSecret(
42+
value: unknown,
43+
path: string,
44+
cfg?: OpenClawConfig,
45+
): ConfiguredSecretResolution {
46+
const resolved = resolveSecretInputString({
47+
value,
48+
path,
49+
defaults: cfg?.secrets?.defaults,
50+
mode: "inspect",
51+
});
52+
if (resolved.status === "available") {
53+
const normalized = normalizeSecretInput(resolved.value);
54+
return normalized ? { status: "available", value: normalized } : { status: "missing" };
55+
}
56+
if (resolved.status === "missing") {
57+
return { status: "missing" };
58+
}
59+
if (resolved.ref.source !== "env") {
60+
return { status: "blocked" };
61+
}
62+
const envVarName = resolved.ref.id.trim();
63+
if (envVarName !== TAVILY_API_KEY_ENV_VAR) {
64+
return { status: "blocked" };
65+
}
66+
if (
67+
!canResolveEnvSecretRefInReadOnlyPath({
68+
cfg,
69+
provider: resolved.ref.provider,
70+
id: envVarName,
71+
})
72+
) {
73+
return { status: "blocked" };
74+
}
75+
const envValue = normalizeSecretInput(process.env[envVarName]);
76+
return envValue ? { status: "available", value: envValue } : { status: "missing" };
4477
}
4578

4679
export function resolveTavilyApiKey(cfg?: OpenClawConfig): string | undefined {
4780
const search = resolveTavilySearchConfig(cfg);
48-
return (
49-
normalizeConfiguredSecret(search?.apiKey, "plugins.entries.tavily.config.webSearch.apiKey") ||
50-
normalizeSecretInput(process.env.TAVILY_API_KEY) ||
51-
undefined
81+
const resolved = resolveConfiguredSecret(
82+
search?.apiKey,
83+
"plugins.entries.tavily.config.webSearch.apiKey",
84+
cfg,
5285
);
86+
if (resolved.status === "available") {
87+
return resolved.value;
88+
}
89+
if (resolved.status === "blocked") {
90+
return undefined;
91+
}
92+
return normalizeSecretInput(process.env.TAVILY_API_KEY) || undefined;
5393
}
5494

5595
export function resolveTavilyBaseUrl(cfg?: OpenClawConfig): string {

0 commit comments

Comments
 (0)