Skip to content

Commit 26083be

Browse files
committed
fix(tools): require usable media auth profiles
1 parent 6cae35f commit 26083be

3 files changed

Lines changed: 151 additions & 3 deletions

File tree

src/agents/tools/model-config.helpers.test.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,81 @@ describe("hasProviderAuthForTool", () => {
6262
).toBe(true);
6363
});
6464

65+
it("keeps bearer token profiles as valid tool auth", () => {
66+
expect(
67+
hasProviderAuthForTool({
68+
provider: "hatchery",
69+
authStore: {
70+
version: 1,
71+
profiles: {
72+
"hatchery:default": {
73+
provider: "hatchery",
74+
type: "token",
75+
token: "tok-profile", // pragma: allowlist secret
76+
},
77+
},
78+
},
79+
}),
80+
).toBe(true);
81+
});
82+
83+
it("rejects blank API-key auth-store profiles for tool preflight", () => {
84+
expect(
85+
hasProviderAuthForTool({
86+
provider: "hatchery",
87+
authStore: {
88+
version: 1,
89+
profiles: {
90+
"hatchery:default": {
91+
provider: "hatchery",
92+
type: "api_key",
93+
key: " ",
94+
},
95+
},
96+
},
97+
}),
98+
).toBe(false);
99+
});
100+
101+
it("rejects expired bearer token profiles for tool preflight", () => {
102+
expect(
103+
hasProviderAuthForTool({
104+
provider: "hatchery",
105+
authStore: {
106+
version: 1,
107+
profiles: {
108+
"hatchery:default": {
109+
provider: "hatchery",
110+
type: "token",
111+
token: "tok-profile", // pragma: allowlist secret
112+
expires: Date.now() - 1_000,
113+
},
114+
},
115+
},
116+
}),
117+
).toBe(false);
118+
});
119+
120+
it("keeps env-backed API-key profiles as valid tool auth", () => {
121+
vi.stubEnv("HATCHERY_API_KEY", "sk-hatchery-env-profile"); // pragma: allowlist secret
122+
123+
expect(
124+
hasProviderAuthForTool({
125+
provider: "hatchery",
126+
authStore: {
127+
version: 1,
128+
profiles: {
129+
"hatchery:default": {
130+
provider: "hatchery",
131+
type: "api_key",
132+
keyRef: { source: "env", provider: "default", id: "HATCHERY_API_KEY" },
133+
},
134+
},
135+
},
136+
}),
137+
).toBe(true);
138+
});
139+
65140
it("rejects aws-sdk auth because tool execution requires an API key string", () => {
66141
const cfg = {
67142
models: {

src/agents/tools/model-config.helpers.ts

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,15 @@ import {
55
} from "../../config/model-input.js";
66
import type { AgentToolModelConfig } from "../../config/types.agents-shared.js";
77
import type { OpenClawConfig } from "../../config/types.openclaw.js";
8+
import { coerceSecretRef } from "../../config/types.secrets.js";
9+
import { normalizeOptionalSecretInput } from "../../utils/normalize-secret-input.js";
810
import {
911
externalCliDiscoveryForProviderAuth,
1012
ensureAuthProfileStore,
1113
hasAnyAuthProfileStoreSource,
1214
listProfilesForProvider,
1315
} from "../auth-profiles.js";
14-
import type { AuthProfileStore } from "../auth-profiles/types.js";
16+
import type { AuthProfileCredential, AuthProfileStore } from "../auth-profiles/types.js";
1517
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
1618
import {
1719
hasRuntimeAvailableProviderAuth,
@@ -43,14 +45,19 @@ export function resolveDefaultModelRef(cfg?: OpenClawConfig): { provider: string
4345

4446
export function hasAuthForProvider(params: {
4547
provider: string;
48+
cfg?: OpenClawConfig;
4649
agentDir?: string;
4750
authStore?: AuthProfileStore;
4851
}): boolean {
4952
if (resolveEnvApiKey(params.provider)?.apiKey) {
5053
return true;
5154
}
5255
if (params.authStore) {
53-
return listProfilesForProvider(params.authStore, params.provider).length > 0;
56+
return hasUsableProfileForProvider({
57+
store: params.authStore,
58+
provider: params.provider,
59+
cfg: params.cfg,
60+
});
5461
}
5562
const agentDir = params.agentDir?.trim();
5663
if (!agentDir) {
@@ -62,7 +69,71 @@ export function hasAuthForProvider(params: {
6269
const store = ensureAuthProfileStore(agentDir, {
6370
externalCli: externalCliDiscoveryForProviderAuth({ provider: params.provider }),
6471
});
65-
return listProfilesForProvider(store, params.provider).length > 0;
72+
return hasUsableProfileForProvider({
73+
store,
74+
provider: params.provider,
75+
cfg: params.cfg,
76+
});
77+
}
78+
79+
function hasUsableProfileForProvider(params: {
80+
store: AuthProfileStore;
81+
provider: string;
82+
cfg?: OpenClawConfig;
83+
}): boolean {
84+
return listProfilesForProvider(params.store, params.provider).some((profileId) => {
85+
const credential = params.store.profiles[profileId];
86+
return Boolean(credential && hasUsableProfileCredential({ credential, cfg: params.cfg }));
87+
});
88+
}
89+
90+
function hasUsableProfileCredential(params: {
91+
credential: AuthProfileCredential;
92+
cfg?: OpenClawConfig;
93+
}): boolean {
94+
if (params.credential.type === "api_key") {
95+
return (
96+
hasAvailableProfileSecretInput({
97+
value: params.credential.key,
98+
cfg: params.cfg,
99+
}) ||
100+
hasAvailableProfileSecretInput({
101+
value: params.credential.keyRef,
102+
cfg: params.cfg,
103+
})
104+
);
105+
}
106+
if (params.credential.type === "token") {
107+
if (typeof params.credential.expires === "number" && params.credential.expires <= Date.now()) {
108+
return false;
109+
}
110+
return (
111+
hasAvailableProfileSecretInput({
112+
value: params.credential.token,
113+
cfg: params.cfg,
114+
}) ||
115+
hasAvailableProfileSecretInput({
116+
value: params.credential.tokenRef,
117+
cfg: params.cfg,
118+
})
119+
);
120+
}
121+
return Boolean(
122+
normalizeOptionalSecretInput(params.credential.access) ||
123+
normalizeOptionalSecretInput(params.credential.refresh) ||
124+
normalizeOptionalSecretInput(params.credential.idToken),
125+
);
126+
}
127+
128+
function hasAvailableProfileSecretInput(params: { value: unknown; cfg?: OpenClawConfig }): boolean {
129+
const ref = coerceSecretRef(params.value, params.cfg?.secrets?.defaults);
130+
if (ref) {
131+
if (ref.source === "env") {
132+
return Boolean(normalizeOptionalSecretInput(process.env[ref.id]));
133+
}
134+
return true;
135+
}
136+
return Boolean(normalizeOptionalSecretInput(params.value));
66137
}
67138

68139
export function hasProviderAuthForTool(params: {
@@ -75,6 +146,7 @@ export function hasProviderAuthForTool(params: {
75146
if (
76147
hasAuthForProvider({
77148
provider: params.provider,
149+
cfg: params.cfg,
78150
agentDir: params.agentDir,
79151
authStore: params.authStore,
80152
})

src/agents/tools/video-generate-tool.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ function isVideoGenerationProviderConfigured(params: {
263263
}) ||
264264
hasAuthForProvider({
265265
provider: params.providerId,
266+
cfg: params.cfg,
266267
agentDir: params.agentDir,
267268
authStore: params.authStore,
268269
})

0 commit comments

Comments
 (0)