mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:30:42 +00:00
fix: thread workspace auth evidence through model auth
This commit is contained in:
@@ -1116,7 +1116,9 @@ async function buildDynamicTools(input: DynamicToolBuildParams) {
|
||||
: undefined,
|
||||
modelApi: params.model.api,
|
||||
modelContextWindowTokens: params.model.contextWindow,
|
||||
modelAuthMode: resolveModelAuthMode(params.model.provider, params.config),
|
||||
modelAuthMode: resolveModelAuthMode(params.model.provider, params.config, undefined, {
|
||||
workspaceDir: input.effectiveWorkspace,
|
||||
}),
|
||||
currentChannelId: params.currentChannelId,
|
||||
currentThreadTs: params.currentThreadTs,
|
||||
currentMessageId: params.currentMessageId,
|
||||
|
||||
@@ -52,8 +52,9 @@ const log = createSubsystemLogger("model-auth");
|
||||
function resolveConfigAwareEnvApiKey(
|
||||
cfg: OpenClawConfig | undefined,
|
||||
provider: string,
|
||||
workspaceDir?: string,
|
||||
): EnvApiKeyResult | null {
|
||||
return resolveEnvApiKey(provider, process.env, { config: cfg });
|
||||
return resolveEnvApiKey(provider, process.env, { config: cfg, workspaceDir });
|
||||
}
|
||||
|
||||
function resolveProviderConfig(
|
||||
@@ -315,6 +316,7 @@ export function hasSyntheticLocalProviderAuthConfig(params: {
|
||||
export function hasRuntimeAvailableProviderAuth(params: {
|
||||
provider: string;
|
||||
cfg?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): boolean {
|
||||
const provider = normalizeProviderId(params.provider);
|
||||
@@ -322,7 +324,12 @@ export function hasRuntimeAvailableProviderAuth(params: {
|
||||
if (authOverride === "aws-sdk") {
|
||||
return true;
|
||||
}
|
||||
if (resolveEnvApiKey(provider, params.env)) {
|
||||
if (
|
||||
resolveEnvApiKey(provider, params.env, {
|
||||
config: params.cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
})
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (resolveUsableCustomProviderApiKey({ cfg: params.cfg, provider, env: params.env })) {
|
||||
@@ -489,6 +496,7 @@ export async function resolveApiKeyForProvider(params: {
|
||||
preferredProfile?: string;
|
||||
store?: AuthProfileStore;
|
||||
agentDir?: string;
|
||||
workspaceDir?: string;
|
||||
/** When true, treat profileId as a user-locked selection that must not be
|
||||
* silently overridden by env/config credentials. */
|
||||
lockedProfile?: boolean;
|
||||
@@ -553,7 +561,7 @@ export async function resolveApiKeyForProvider(params: {
|
||||
}
|
||||
|
||||
if (params.credentialPrecedence === "env-first") {
|
||||
const envResolved = resolveConfigAwareEnvApiKey(cfg, provider);
|
||||
const envResolved = resolveConfigAwareEnvApiKey(cfg, provider, params.workspaceDir);
|
||||
if (envResolved) {
|
||||
const resolvedMode: ResolvedProviderAuth["mode"] = envResolved.source.includes("OAUTH_TOKEN")
|
||||
? "oauth"
|
||||
@@ -575,7 +583,7 @@ export async function resolveApiKeyForProvider(params: {
|
||||
mode: "api-key",
|
||||
};
|
||||
}
|
||||
const localMarkerEnv = resolveConfigAwareEnvApiKey(cfg, provider);
|
||||
const localMarkerEnv = resolveConfigAwareEnvApiKey(cfg, provider, params.workspaceDir);
|
||||
if (localMarkerEnv && isNonSecretApiKeyMarker(localMarkerEnv.apiKey)) {
|
||||
return {
|
||||
apiKey: localMarkerEnv.apiKey,
|
||||
@@ -626,7 +634,7 @@ export async function resolveApiKeyForProvider(params: {
|
||||
}
|
||||
}
|
||||
|
||||
const envResolved = resolveConfigAwareEnvApiKey(cfg, provider);
|
||||
const envResolved = resolveConfigAwareEnvApiKey(cfg, provider, params.workspaceDir);
|
||||
if (envResolved) {
|
||||
const resolvedMode: ResolvedProviderAuth["mode"] = envResolved.source.includes("OAUTH_TOKEN")
|
||||
? "oauth"
|
||||
@@ -699,6 +707,7 @@ export function resolveModelAuthMode(
|
||||
provider?: string,
|
||||
cfg?: OpenClawConfig,
|
||||
store?: AuthProfileStore,
|
||||
options?: { workspaceDir?: string },
|
||||
): ModelAuthMode | undefined {
|
||||
const resolved = provider?.trim();
|
||||
if (!resolved) {
|
||||
@@ -739,7 +748,7 @@ export function resolveModelAuthMode(
|
||||
return "aws-sdk";
|
||||
}
|
||||
|
||||
const envKey = resolveConfigAwareEnvApiKey(cfg, resolved);
|
||||
const envKey = resolveConfigAwareEnvApiKey(cfg, resolved, options?.workspaceDir);
|
||||
if (envKey?.apiKey) {
|
||||
return envKey.source.includes("OAUTH_TOKEN") ? "oauth" : "api-key";
|
||||
}
|
||||
@@ -764,6 +773,7 @@ export async function hasAvailableAuthForProvider(params: {
|
||||
preferredProfile?: string;
|
||||
store?: AuthProfileStore;
|
||||
agentDir?: string;
|
||||
workspaceDir?: string;
|
||||
}): Promise<boolean> {
|
||||
const { provider, cfg, preferredProfile } = params;
|
||||
|
||||
@@ -771,7 +781,7 @@ export async function hasAvailableAuthForProvider(params: {
|
||||
if (authOverride === "aws-sdk") {
|
||||
return true;
|
||||
}
|
||||
if (resolveConfigAwareEnvApiKey(cfg, provider)) {
|
||||
if (resolveConfigAwareEnvApiKey(cfg, provider, params.workspaceDir)) {
|
||||
return true;
|
||||
}
|
||||
if (resolveUsableCustomProviderApiKey({ cfg, provider })) {
|
||||
@@ -816,6 +826,7 @@ export async function getApiKeyForModel(params: {
|
||||
preferredProfile?: string;
|
||||
store?: AuthProfileStore;
|
||||
agentDir?: string;
|
||||
workspaceDir?: string;
|
||||
lockedProfile?: boolean;
|
||||
credentialPrecedence?: ProviderCredentialPrecedence;
|
||||
}): Promise<ResolvedProviderAuth> {
|
||||
@@ -826,6 +837,7 @@ export async function getApiKeyForModel(params: {
|
||||
preferredProfile: params.preferredProfile,
|
||||
store: params.store,
|
||||
agentDir: params.agentDir,
|
||||
workspaceDir: params.workspaceDir,
|
||||
lockedProfile: params.lockedProfile,
|
||||
credentialPrecedence: params.credentialPrecedence,
|
||||
});
|
||||
|
||||
119
src/agents/model-auth.workspace-plugin.test.ts
Normal file
119
src/agents/model-auth.workspace-plugin.test.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { withEnvAsync } from "../test-utils/env.js";
|
||||
import type { AuthProfileStore } from "./auth-profiles.js";
|
||||
import { resolveEnvApiKey } from "./model-auth-env.js";
|
||||
import {
|
||||
hasAvailableAuthForProvider,
|
||||
resolveApiKeyForProvider,
|
||||
resolveModelAuthMode,
|
||||
} from "./model-auth.js";
|
||||
import { hasAuthForModelProvider } from "./model-provider-auth.js";
|
||||
|
||||
async function writeWorkspaceAuthEvidencePlugin(workspaceDir: string) {
|
||||
const pluginDir = path.join(workspaceDir, ".openclaw", "extensions", "workspace-cloud");
|
||||
await fs.mkdir(pluginDir, { recursive: true });
|
||||
await fs.writeFile(path.join(pluginDir, "index.ts"), "export default {}\n", "utf8");
|
||||
await fs.writeFile(
|
||||
path.join(pluginDir, "openclaw.plugin.json"),
|
||||
JSON.stringify({
|
||||
id: "workspace-cloud",
|
||||
configSchema: { type: "object" },
|
||||
setup: {
|
||||
providers: [
|
||||
{
|
||||
id: "workspace-cloud",
|
||||
authEvidence: [
|
||||
{
|
||||
type: "local-file-with-env",
|
||||
fileEnvVar: "WORKSPACE_CLOUD_CREDENTIALS",
|
||||
credentialMarker: "workspace-cloud-local-credentials",
|
||||
source: "workspace cloud credentials",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
describe("workspace plugin model auth evidence", () => {
|
||||
it("uses trusted workspace plugin auth evidence across runtime and picker auth checks", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-workspace-auth-"));
|
||||
const workspaceDir = path.join(tempRoot, "workspace");
|
||||
const bundledDir = path.join(tempRoot, "bundled");
|
||||
const stateDir = path.join(tempRoot, "state");
|
||||
const credentialsPath = path.join(tempRoot, "credentials.json");
|
||||
await fs.mkdir(bundledDir, { recursive: true });
|
||||
await fs.mkdir(stateDir, { recursive: true });
|
||||
await fs.writeFile(credentialsPath, "{}", "utf8");
|
||||
await writeWorkspaceAuthEvidencePlugin(workspaceDir);
|
||||
|
||||
const cfg: OpenClawConfig = {
|
||||
plugins: {
|
||||
allow: ["workspace-cloud"],
|
||||
},
|
||||
};
|
||||
const store: AuthProfileStore = { version: 1, profiles: {} };
|
||||
|
||||
try {
|
||||
await withEnvAsync(
|
||||
{
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: bundledDir,
|
||||
OPENCLAW_STATE_DIR: stateDir,
|
||||
WORKSPACE_CLOUD_CREDENTIALS: credentialsPath,
|
||||
},
|
||||
async () => {
|
||||
expect(resolveEnvApiKey("workspace-cloud", process.env, { config: cfg })).toBeNull();
|
||||
expect(
|
||||
resolveEnvApiKey("workspace-cloud", process.env, {
|
||||
config: cfg,
|
||||
workspaceDir,
|
||||
}),
|
||||
).toEqual({
|
||||
apiKey: "workspace-cloud-local-credentials",
|
||||
source: "workspace cloud credentials",
|
||||
});
|
||||
await expect(
|
||||
resolveApiKeyForProvider({
|
||||
provider: "workspace-cloud",
|
||||
cfg,
|
||||
workspaceDir,
|
||||
store,
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
apiKey: "workspace-cloud-local-credentials",
|
||||
source: "workspace cloud credentials",
|
||||
mode: "api-key",
|
||||
});
|
||||
expect(resolveModelAuthMode("workspace-cloud", cfg, store, { workspaceDir })).toBe(
|
||||
"api-key",
|
||||
);
|
||||
await expect(
|
||||
hasAvailableAuthForProvider({
|
||||
provider: "workspace-cloud",
|
||||
cfg,
|
||||
workspaceDir,
|
||||
store,
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
expect(
|
||||
hasAuthForModelProvider({
|
||||
provider: "workspace-cloud",
|
||||
cfg,
|
||||
workspaceDir,
|
||||
store,
|
||||
}),
|
||||
).toBe(true);
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
await fs.rm(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -32,6 +32,7 @@ export function resolveVisibleModelCatalog(params: {
|
||||
defaultModel?: string;
|
||||
agentId?: string;
|
||||
agentDir?: string;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
view?: ModelCatalogVisibilityView;
|
||||
}): ModelCatalogEntry[] {
|
||||
@@ -55,6 +56,7 @@ export function resolveVisibleModelCatalog(params: {
|
||||
);
|
||||
const hasAuth = createProviderAuthChecker({
|
||||
cfg: params.cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
agentDir: params.agentDir,
|
||||
env: params.env,
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ import { normalizeProviderId } from "./model-selection.js";
|
||||
export function hasAuthForModelProvider(params: {
|
||||
provider: string;
|
||||
cfg?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
agentDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
store?: AuthProfileStore;
|
||||
@@ -19,6 +20,7 @@ export function hasAuthForModelProvider(params: {
|
||||
hasRuntimeAvailableProviderAuth({
|
||||
provider,
|
||||
cfg: params.cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
})
|
||||
) {
|
||||
@@ -37,6 +39,7 @@ export function hasAuthForModelProvider(params: {
|
||||
|
||||
export function createProviderAuthChecker(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
agentDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}): (provider: string) => boolean {
|
||||
@@ -53,6 +56,7 @@ export function createProviderAuthChecker(params: {
|
||||
const value = hasAuthForModelProvider({
|
||||
provider: key,
|
||||
cfg: params.cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
agentDir: params.agentDir,
|
||||
env: params.env,
|
||||
store,
|
||||
|
||||
@@ -572,7 +572,9 @@ export async function compactEmbeddedPiSessionDirect(
|
||||
modelCompat: extractModelCompat(effectiveModel),
|
||||
modelApi: model.api,
|
||||
modelContextWindowTokens: ctxInfo.tokens,
|
||||
modelAuthMode: resolveModelAuthMode(model.provider, params.config),
|
||||
modelAuthMode: resolveModelAuthMode(model.provider, params.config, undefined, {
|
||||
workspaceDir: effectiveWorkspace,
|
||||
}),
|
||||
});
|
||||
const toolsEnabled = supportsModelTools(runtimeModel);
|
||||
const runtimePlanModelContext = {
|
||||
|
||||
@@ -762,7 +762,9 @@ export async function runEmbeddedAttempt(
|
||||
modelCompat: extractModelCompat(params.model),
|
||||
modelApi: params.model.api,
|
||||
modelContextWindowTokens: params.model.contextWindow,
|
||||
modelAuthMode: resolveModelAuthMode(params.model.provider, params.config),
|
||||
modelAuthMode: resolveModelAuthMode(params.model.provider, params.config, undefined, {
|
||||
workspaceDir: effectiveWorkspace,
|
||||
}),
|
||||
currentChannelId: params.currentChannelId,
|
||||
currentThreadTs: params.currentThreadTs,
|
||||
currentMessageId: params.currentMessageId,
|
||||
|
||||
@@ -1515,7 +1515,9 @@ export async function runReplyAgent(params: {
|
||||
(sessionKey ? activeSessionStore?.[sessionKey]?.responseUsage : undefined);
|
||||
const responseUsageMode = resolveResponseUsageMode(responseUsageRaw);
|
||||
if (responseUsageMode !== "off" && hasNonzeroUsage(usage)) {
|
||||
const authMode = resolveModelAuthMode(providerUsed, cfg);
|
||||
const authMode = resolveModelAuthMode(providerUsed, cfg, undefined, {
|
||||
workspaceDir: followupRun.run.workspaceDir,
|
||||
});
|
||||
const showCost = authMode === "api-key";
|
||||
const costConfig = showCost
|
||||
? resolveModelCostConfig({
|
||||
@@ -1678,7 +1680,9 @@ export async function runReplyAgent(params: {
|
||||
authMode:
|
||||
runResult.meta?.requestShaping?.authMode ??
|
||||
(cfg?.models?.providers && providerUsed in cfg.models.providers
|
||||
? (resolveModelAuthMode(providerUsed, cfg) ?? undefined)
|
||||
? (resolveModelAuthMode(providerUsed, cfg, undefined, {
|
||||
workspaceDir: followupRun.run.workspaceDir,
|
||||
}) ?? undefined)
|
||||
: undefined),
|
||||
thinking:
|
||||
runResult.meta?.requestShaping?.thinking ??
|
||||
|
||||
@@ -713,6 +713,7 @@ export async function promptDefaultModel(
|
||||
defaultProvider: DEFAULT_PROVIDER,
|
||||
defaultModel: resolved.model,
|
||||
agentDir: params.agentDir,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
if (models.length === 0) {
|
||||
@@ -749,7 +750,12 @@ export async function promptDefaultModel(
|
||||
const hasPreferredProvider = preferredProvider
|
||||
? filteredModels.some((entry) => matchesPreferredProvider?.(entry.provider))
|
||||
: false;
|
||||
const hasAuth = createProviderAuthChecker({ cfg, agentDir: params.agentDir, env: params.env });
|
||||
const hasAuth = createProviderAuthChecker({
|
||||
cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
agentDir: params.agentDir,
|
||||
env: params.env,
|
||||
});
|
||||
const literalPrefixProviders = await resolveCachedLiteralPrefixProviders();
|
||||
|
||||
// Show the literal form (e.g. nvidia/nvidia/...) in the "Keep current" label
|
||||
@@ -912,7 +918,12 @@ export async function promptModelAllowlist(params: {
|
||||
fallbackKeys.length > 0 ||
|
||||
(params.initialSelections?.length ?? 0) > 0 ||
|
||||
configuredRaw.length > 0;
|
||||
const hasAuth = createProviderAuthChecker({ cfg, agentDir: params.agentDir, env: params.env });
|
||||
const hasAuth = createProviderAuthChecker({
|
||||
cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
agentDir: params.agentDir,
|
||||
env: params.env,
|
||||
});
|
||||
const matchesPreferredProvider = preferredProvider
|
||||
? createPreferredProviderMatcher({
|
||||
preferredProvider,
|
||||
|
||||
@@ -31,6 +31,7 @@ export async function getRuntimeAuthForModel(params: {
|
||||
const resolvedAuth = await resolveModelApiKey({
|
||||
model: params.model,
|
||||
cfg: params.cfg,
|
||||
workspaceDir: params.workspaceDir,
|
||||
});
|
||||
|
||||
if (!resolvedAuth.apiKey || resolvedAuth.mode === "aws-sdk") {
|
||||
|
||||
Reference in New Issue
Block a user