From 3b4d2d88865aca81a4c24d0f6e88ea13dfd76d9a Mon Sep 17 00:00:00 2001 From: Shakker Date: Wed, 29 Apr 2026 22:13:00 +0100 Subject: [PATCH] fix: pass workspace auth evidence into model auth labels --- src/agents/model-auth-label.test.ts | 25 ++++++ src/agents/model-auth-label.ts | 6 +- src/auto-reply/reply/commands-info.ts | 1 + src/auto-reply/reply/commands-models.test.ts | 6 ++ src/auto-reply/reply/commands-models.ts | 7 ++ src/auto-reply/reply/commands-status.test.ts | 79 +++++++++++++++++++ .../reply/get-reply-directives-apply.ts | 3 + src/auto-reply/reply/get-reply-directives.ts | 1 + .../reply/get-reply-inline-actions.ts | 1 + src/status/status-text.ts | 4 + src/status/status-text.types.ts | 1 + 11 files changed, 133 insertions(+), 1 deletion(-) diff --git a/src/agents/model-auth-label.test.ts b/src/agents/model-auth-label.test.ts index 58f6e702b3d..a7cb686a8c9 100644 --- a/src/agents/model-auth-label.test.ts +++ b/src/agents/model-auth-label.test.ts @@ -165,4 +165,29 @@ describe("resolveModelAuthLabel", () => { expect(mocks.loadAuthProfileStoreWithoutExternalProfiles).toHaveBeenCalledOnce(); expect(mocks.ensureAuthProfileStore).not.toHaveBeenCalled(); }); + + it("resolves env labels with config and workspace scope", () => { + mocks.ensureAuthProfileStore.mockReturnValue({ + version: 1, + profiles: {}, + } as never); + mocks.resolveAuthProfileOrder.mockReturnValue([]); + mocks.resolveEnvApiKey.mockReturnValue({ + apiKey: "workspace-cloud-local-credentials", + source: "workspace cloud credentials", + }); + + const cfg = { plugins: { allow: ["workspace-cloud"] } }; + const label = resolveModelAuthLabel({ + provider: "workspace-cloud", + cfg, + workspaceDir: "/tmp/workspace", + }); + + expect(label).toBe("api-key (workspace cloud credentials)"); + expect(mocks.resolveEnvApiKey).toHaveBeenCalledWith("workspace-cloud", process.env, { + config: cfg, + workspaceDir: "/tmp/workspace", + }); + }); }); diff --git a/src/agents/model-auth-label.ts b/src/agents/model-auth-label.ts index 95502ddc928..c0bb602a681 100644 --- a/src/agents/model-auth-label.ts +++ b/src/agents/model-auth-label.ts @@ -15,6 +15,7 @@ export function resolveModelAuthLabel(params: { cfg?: OpenClawConfig; sessionEntry?: Partial>; agentDir?: string; + workspaceDir?: string; includeExternalProfiles?: boolean; }): string | undefined { const resolvedProvider = params.provider?.trim(); @@ -57,7 +58,10 @@ export function resolveModelAuthLabel(params: { return `api-key${label ? ` (${label})` : ""}`; } - const envKey = resolveEnvApiKey(providerKey); + const envKey = resolveEnvApiKey(providerKey, process.env, { + config: params.cfg, + workspaceDir: params.workspaceDir, + }); if (envKey?.apiKey) { if (envKey.source.includes("OAUTH_TOKEN")) { return `oauth (${envKey.source})`; diff --git a/src/auto-reply/reply/commands-info.ts b/src/auto-reply/reply/commands-info.ts index a6967c1c9d1..04bc1228621 100644 --- a/src/auto-reply/reply/commands-info.ts +++ b/src/auto-reply/reply/commands-info.ts @@ -203,6 +203,7 @@ export const handleStatusCommand: CommandHandler = async (params, allowTextComma provider: params.provider, model: params.model, contextTokens: params.contextTokens, + workspaceDir: params.workspaceDir, resolvedThinkLevel: params.resolvedThinkLevel, resolvedFastMode: params.resolvedFastMode, resolvedVerboseLevel: params.resolvedVerboseLevel, diff --git a/src/auto-reply/reply/commands-models.test.ts b/src/auto-reply/reply/commands-models.test.ts index 33f3b66372a..7edb545c901 100644 --- a/src/auto-reply/reply/commands-models.test.ts +++ b/src/auto-reply/reply/commands-models.test.ts @@ -361,6 +361,12 @@ describe("handleModelsCommand", () => { const result = await handleModelsCommand(params, true); expect(result?.reply?.text).toContain("Models (anthropic ยท ๐Ÿ”‘ target-auth) โ€” showing 1-2 of 2"); + expect(modelAuthLabelMocks.resolveModelAuthLabel).toHaveBeenCalledWith( + expect.objectContaining({ + provider: "anthropic", + workspaceDir: "/tmp", + }), + ); }); it("returns a deprecation message for /models add when no provider is given", async () => { diff --git a/src/auto-reply/reply/commands-models.ts b/src/auto-reply/reply/commands-models.ts index f7c3b4539a2..b16c5e881a1 100644 --- a/src/auto-reply/reply/commands-models.ts +++ b/src/auto-reply/reply/commands-models.ts @@ -273,6 +273,7 @@ function resolveProviderLabel(params: { provider: string; cfg: OpenClawConfig; agentDir?: string; + workspaceDir?: string; sessionEntry?: ModelsCommandSessionEntry; }): string { const authLabel = resolveModelAuthLabel({ @@ -280,6 +281,7 @@ function resolveProviderLabel(params: { cfg: params.cfg, sessionEntry: params.sessionEntry, agentDir: params.agentDir, + workspaceDir: params.workspaceDir, }); if (!authLabel || authLabel === "unknown") { return params.provider; @@ -292,12 +294,14 @@ export function formatModelsAvailableHeader(params: { total: number; cfg: OpenClawConfig; agentDir?: string; + workspaceDir?: string; sessionEntry?: ModelsCommandSessionEntry; }): string { const providerLabel = resolveProviderLabel({ provider: params.provider, cfg: params.cfg, agentDir: params.agentDir, + workspaceDir: params.workspaceDir, sessionEntry: params.sessionEntry, }); return `Models (${providerLabel}) โ€” ${params.total} available`; @@ -421,6 +425,7 @@ export async function resolveModelsCommandReply(params: { provider, cfg: params.cfg, agentDir: params.agentDir, + workspaceDir: params.workspaceDir, sessionEntry: params.sessionEntry, }); return { @@ -452,6 +457,7 @@ export async function resolveModelsCommandReply(params: { total, cfg: params.cfg, agentDir: params.agentDir, + workspaceDir: params.workspaceDir, sessionEntry: params.sessionEntry, }), channelData: interactiveChannelData, @@ -480,6 +486,7 @@ export async function resolveModelsCommandReply(params: { provider, cfg: params.cfg, agentDir: params.agentDir, + workspaceDir: params.workspaceDir, sessionEntry: params.sessionEntry, }); const lines = [ diff --git a/src/auto-reply/reply/commands-status.test.ts b/src/auto-reply/reply/commands-status.test.ts index 54b5a94ba26..ef579460ea8 100644 --- a/src/auto-reply/reply/commands-status.test.ts +++ b/src/auto-reply/reply/commands-status.test.ts @@ -1,4 +1,5 @@ import fs from "node:fs"; +import os from "node:os"; import path from "node:path"; import { withTempHome } from "openclaw/plugin-sdk/test-env"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; @@ -16,6 +17,7 @@ import { failTaskRunByRunId, } from "../../tasks/task-executor.js"; import { resetTaskRegistryForTests } from "../../tasks/task-registry.js"; +import { withEnvAsync } from "../../test-utils/env.js"; import { buildStatusReply, buildStatusText } from "./commands-status.js"; import { baseCommandTestConfig, @@ -521,6 +523,83 @@ describe("buildStatusReply subagent summary", () => { expect(normalized).not.toContain("Fast ยท codex"); }); + it("uses workspace-scoped auth evidence in /status auth labels", async () => { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-status-auth-label-")); + const workspaceDir = path.join(tempRoot, "workspace"); + const pluginDir = path.join(workspaceDir, ".openclaw", "extensions", "workspace-auth-label"); + const bundledDir = path.join(tempRoot, "bundled"); + const stateDir = path.join(tempRoot, "state"); + const credentialPath = path.join(tempRoot, "credentials.json"); + fs.mkdirSync(pluginDir, { recursive: true }); + fs.mkdirSync(bundledDir, { recursive: true }); + fs.mkdirSync(stateDir, { recursive: true }); + fs.writeFileSync(path.join(pluginDir, "index.ts"), "export default {}\n", "utf8"); + fs.writeFileSync(credentialPath, "{}", "utf8"); + fs.writeFileSync( + path.join(pluginDir, "openclaw.plugin.json"), + JSON.stringify({ + id: "workspace-auth-label", + configSchema: { type: "object" }, + setup: { + providers: [ + { + id: "anthropic", + authEvidence: [ + { + type: "local-file-with-env", + fileEnvVar: "WORKSPACE_STATUS_CREDENTIALS", + credentialMarker: "workspace-status-local-credentials", + source: "workspace status credentials", + }, + ], + }, + ], + }, + }), + "utf8", + ); + + try { + await withEnvAsync( + { + OPENCLAW_BUNDLED_PLUGINS_DIR: bundledDir, + OPENCLAW_STATE_DIR: stateDir, + WORKSPACE_STATUS_CREDENTIALS: credentialPath, + }, + async () => { + const text = await buildStatusText({ + cfg: { + ...baseCfg, + plugins: { allow: ["workspace-auth-label"] }, + }, + sessionEntry: { + sessionId: "sess-status-workspace-auth", + updatedAt: 0, + }, + sessionKey: "agent:main:main", + parentSessionKey: "agent:main:main", + sessionScope: "per-sender", + statusChannel: "mobilechat", + workspaceDir, + provider: "anthropic", + model: "claude-opus-4-5", + contextTokens: 32_000, + resolvedFastMode: false, + resolvedVerboseLevel: "off", + resolvedReasoningLevel: "off", + resolveDefaultThinkingLevel: async () => undefined, + isGroup: false, + defaultGroupActivation: () => "mention", + }); + + expect(normalizeTestText(text)).toContain("workspace status credentials"); + }, + ); + } finally { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); + it("keeps /status on a session-pinned PI harness after config changes", async () => { registerStatusCodexHarness(); diff --git a/src/auto-reply/reply/get-reply-directives-apply.ts b/src/auto-reply/reply/get-reply-directives-apply.ts index 4bb9000623b..53a19660a87 100644 --- a/src/auto-reply/reply/get-reply-directives-apply.ts +++ b/src/auto-reply/reply/get-reply-directives-apply.ts @@ -97,6 +97,7 @@ export async function applyInlineDirectiveOverrides(params: { cfg: OpenClawConfig; agentId: string; agentDir: string; + workspaceDir: string; agentCfg: AgentDefaults; agentEntry?: AgentEntry; sessionEntry: SessionEntry; @@ -131,6 +132,7 @@ export async function applyInlineDirectiveOverrides(params: { cfg, agentId, agentDir, + workspaceDir, agentCfg, agentEntry, sessionEntry, @@ -358,6 +360,7 @@ export async function applyInlineDirectiveOverrides(params: { provider, model, contextTokens, + workspaceDir, resolvedThinkLevel: resolvedDefaultThinkLevel, resolvedVerboseLevel: currentVerboseLevel ?? "off", resolvedReasoningLevel: currentReasoningLevel ?? "off", diff --git a/src/auto-reply/reply/get-reply-directives.ts b/src/auto-reply/reply/get-reply-directives.ts index e1d7a783b66..3078db39c86 100644 --- a/src/auto-reply/reply/get-reply-directives.ts +++ b/src/auto-reply/reply/get-reply-directives.ts @@ -579,6 +579,7 @@ export async function resolveReplyDirectives(params: { cfg, agentId, agentDir, + workspaceDir, agentCfg, agentEntry, sessionEntry: targetSessionEntry, diff --git a/src/auto-reply/reply/get-reply-inline-actions.ts b/src/auto-reply/reply/get-reply-inline-actions.ts index c21cc7bad85..dce171e41f5 100644 --- a/src/auto-reply/reply/get-reply-inline-actions.ts +++ b/src/auto-reply/reply/get-reply-inline-actions.ts @@ -399,6 +399,7 @@ export async function handleInlineActions(params: { provider, model, contextTokens, + workspaceDir, resolvedThinkLevel, resolvedVerboseLevel: resolvedVerboseLevel ?? "off", resolvedReasoningLevel, diff --git a/src/status/status-text.ts b/src/status/status-text.ts index 2e81c03c659..85839668faa 100644 --- a/src/status/status-text.ts +++ b/src/status/status-text.ts @@ -1,6 +1,7 @@ import { resolveAgentConfig, resolveAgentDir, + resolveAgentWorkspaceDir, resolveDefaultAgentId, resolveSessionAgentId, resolveAgentModelFallbacksOverride, @@ -157,6 +158,7 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise