fix: pass workspace auth evidence into model auth labels

This commit is contained in:
Shakker
2026-04-29 22:13:00 +01:00
parent d3c6a8f0fb
commit 3b4d2d8886
11 changed files with 133 additions and 1 deletions

View File

@@ -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",
});
});
});

View File

@@ -15,6 +15,7 @@ export function resolveModelAuthLabel(params: {
cfg?: OpenClawConfig;
sessionEntry?: Partial<Pick<SessionEntry, "authProfileOverride">>;
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})`;

View File

@@ -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,

View File

@@ -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 () => {

View File

@@ -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 = [

View File

@@ -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();

View File

@@ -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",

View File

@@ -579,6 +579,7 @@ export async function resolveReplyDirectives(params: {
cfg,
agentId,
agentDir,
workspaceDir,
agentCfg,
agentEntry,
sessionEntry: targetSessionEntry,

View File

@@ -399,6 +399,7 @@ export async function handleInlineActions(params: {
provider,
model,
contextTokens,
workspaceDir,
resolvedThinkLevel,
resolvedVerboseLevel: resolvedVerboseLevel ?? "off",
resolvedReasoningLevel,

View File

@@ -1,6 +1,7 @@
import {
resolveAgentConfig,
resolveAgentDir,
resolveAgentWorkspaceDir,
resolveDefaultAgentId,
resolveSessionAgentId,
resolveAgentModelFallbacksOverride,
@@ -157,6 +158,7 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise<st
? resolveSessionAgentId({ sessionKey, config: cfg })
: resolveDefaultAgentId(cfg);
const statusAgentDir = resolveAgentDir(cfg, statusAgentId);
const statusWorkspaceDir = params.workspaceDir ?? resolveAgentWorkspaceDir(cfg, statusAgentId);
const modelRefs = resolveSelectedAndActiveModel({
selectedProvider: provider,
selectedModel: model,
@@ -169,6 +171,7 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise<st
cfg,
sessionEntry,
agentDir: statusAgentDir,
workspaceDir: statusWorkspaceDir,
includeExternalProfiles: false,
});
const activeModelAuth = Object.hasOwn(params, "activeModelAuthOverride")
@@ -179,6 +182,7 @@ export async function buildStatusText(params: BuildStatusTextParams): Promise<st
cfg,
sessionEntry,
agentDir: statusAgentDir,
workspaceDir: statusWorkspaceDir,
includeExternalProfiles: false,
})
: selectedModelAuth;

View File

@@ -16,6 +16,7 @@ export type BuildStatusTextParams = {
sessionScope?: SessionScope;
storePath?: string;
statusChannel: string;
workspaceDir?: string;
provider: string;
model: string;
contextTokens?: number;