mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix: preserve workspace auth labels in model status
This commit is contained in:
@@ -183,6 +183,7 @@ function createCommandsStatusRuntimeModuleMock() {
|
||||
statusChannel: string;
|
||||
provider?: string;
|
||||
model: string;
|
||||
workspaceDir?: string;
|
||||
primaryModelLabelOverride?: string;
|
||||
includeTranscriptUsage?: boolean;
|
||||
taskLineOverride?: string;
|
||||
@@ -225,6 +226,7 @@ function createCommandsStatusRuntimeModuleMock() {
|
||||
sessionEntry: params.sessionEntry,
|
||||
modelAuth,
|
||||
includeTranscriptUsage: params.includeTranscriptUsage,
|
||||
workspaceDir: params.workspaceDir,
|
||||
});
|
||||
return ["OpenClaw", `🧠 Model: ${primary}`, params.taskLineOverride]
|
||||
.filter(Boolean)
|
||||
@@ -427,6 +429,28 @@ describe("session_status tool", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("passes spawned workspace to session_status auth labels", async () => {
|
||||
resetSessionStore({
|
||||
"agent:main:spawned": {
|
||||
sessionId: "spawned-status",
|
||||
updatedAt: 10,
|
||||
spawnedWorkspaceDir: "/tmp/openclaw-spawned-workspace",
|
||||
providerOverride: "anthropic",
|
||||
modelOverride: "claude-opus-4-6",
|
||||
},
|
||||
});
|
||||
|
||||
const tool = getSessionStatusTool("agent:main:spawned");
|
||||
|
||||
await tool.execute("call-spawned-workspace-status", {});
|
||||
|
||||
expect(buildStatusMessageMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
workspaceDir: "/tmp/openclaw-spawned-workspace",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("errors for unknown session keys", async () => {
|
||||
resetSessionStore({
|
||||
main: { sessionId: "s1", updatedAt: 10 },
|
||||
|
||||
@@ -564,6 +564,7 @@ export function createSessionStatusTool(opts?: {
|
||||
statusSessionEntry.lastChannel ??
|
||||
statusSessionEntry.origin?.provider ??
|
||||
"unknown",
|
||||
workspaceDir: statusSessionEntry.spawnedWorkspaceDir,
|
||||
provider: providerForCard,
|
||||
model: defaultModelForCard,
|
||||
resolvedThinkLevel: statusSessionEntry.thinkingLevel as ThinkLevel | undefined,
|
||||
|
||||
@@ -4,6 +4,15 @@ import type { OpenClawConfig } from "../../config/config.js";
|
||||
|
||||
let mockStore: AuthProfileStore;
|
||||
let mockOrder: string[];
|
||||
const resolveEnvApiKeyMock = vi.hoisted(() =>
|
||||
vi.fn(
|
||||
(
|
||||
_provider?: string,
|
||||
_env?: NodeJS.ProcessEnv,
|
||||
_options?: { config?: OpenClawConfig; workspaceDir?: string },
|
||||
) => null as { apiKey: string; source: string } | null,
|
||||
),
|
||||
);
|
||||
const githubCopilotTokenRefProfile: AuthProfileStore["profiles"][string] = {
|
||||
type: "token",
|
||||
provider: "github-copilot",
|
||||
@@ -39,7 +48,11 @@ vi.mock("../../agents/model-auth.js", () => ({
|
||||
ensureAuthProfileStore: () => mockStore,
|
||||
resolveUsableCustomProviderApiKey: () => null,
|
||||
resolveAuthProfileOrder: () => mockOrder,
|
||||
resolveEnvApiKey: () => null,
|
||||
resolveEnvApiKey: (
|
||||
provider?: string,
|
||||
env?: NodeJS.ProcessEnv,
|
||||
options?: { config?: OpenClawConfig; workspaceDir?: string },
|
||||
) => resolveEnvApiKeyMock(provider, env, options),
|
||||
}));
|
||||
|
||||
const { resolveAuthLabel } = await import("./directive-handling.auth.js");
|
||||
@@ -73,6 +86,8 @@ describe("resolveAuthLabel ref-aware labels", () => {
|
||||
profiles: {},
|
||||
};
|
||||
mockOrder = [];
|
||||
resolveEnvApiKeyMock.mockReset();
|
||||
resolveEnvApiKeyMock.mockReturnValue(null);
|
||||
});
|
||||
|
||||
it("shows api-key (ref) for keyRef-only profiles in compact mode", async () => {
|
||||
@@ -112,4 +127,27 @@ describe("resolveAuthLabel ref-aware labels", () => {
|
||||
expect(result.label).toContain("github-copilot:default=token:ref");
|
||||
expect(result.label).not.toContain("token:missing");
|
||||
});
|
||||
|
||||
it("passes workspace scope to env auth labels", async () => {
|
||||
const cfg = { plugins: { allow: ["workspace-auth-label"] } } as OpenClawConfig;
|
||||
resolveEnvApiKeyMock.mockReturnValue({
|
||||
apiKey: "workspace-local-credentials",
|
||||
source: "workspace credentials",
|
||||
});
|
||||
|
||||
const result = await resolveAuthLabel(
|
||||
"anthropic",
|
||||
cfg,
|
||||
"/tmp/models.json",
|
||||
"/tmp/agent",
|
||||
"verbose",
|
||||
"/tmp/workspace",
|
||||
);
|
||||
|
||||
expect(resolveEnvApiKeyMock).toHaveBeenCalledWith("anthropic", process.env, {
|
||||
config: cfg,
|
||||
workspaceDir: "/tmp/workspace",
|
||||
});
|
||||
expect(result.source).toBe("workspace credentials");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -56,6 +56,7 @@ export const resolveAuthLabel = async (
|
||||
modelsPath: string,
|
||||
agentDir?: string,
|
||||
mode: ModelAuthDetailMode = "compact",
|
||||
workspaceDir?: string,
|
||||
): Promise<{ label: string; source: string }> => {
|
||||
const formatPath = (value: string) => shortenHomePath(value);
|
||||
const store = ensureAuthProfileStore(agentDir, {
|
||||
@@ -193,7 +194,7 @@ export const resolveAuthLabel = async (
|
||||
};
|
||||
}
|
||||
|
||||
const envKey = resolveEnvApiKey(provider);
|
||||
const envKey = resolveEnvApiKey(provider, process.env, { config: cfg, workspaceDir });
|
||||
if (envKey) {
|
||||
const isOAuthEnv =
|
||||
envKey.source.includes("ANTHROPIC_OAUTH_TOKEN") ||
|
||||
|
||||
@@ -93,6 +93,7 @@ export async function applyInlineDirectivesFastLane(
|
||||
surface: ctx.Surface,
|
||||
gatewayClientScopes: ctx.GatewayClientScopes,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
workspaceDir: params.workspaceDir,
|
||||
});
|
||||
|
||||
if (sessionEntry?.providerOverride) {
|
||||
|
||||
@@ -104,6 +104,7 @@ export async function handleDirectiveOnly(
|
||||
aliasIndex,
|
||||
allowedModelCatalog,
|
||||
resetModelOverride,
|
||||
workspaceDir: params.workspaceDir,
|
||||
surface: params.surface,
|
||||
sessionEntry,
|
||||
});
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
const authProfilesStoreMock = vi.hoisted(() => ({
|
||||
profiles: {} as Record<string, { type: "api_key"; provider: string; key: string }>,
|
||||
@@ -70,6 +73,7 @@ import { enqueueSystemEvent } from "../../infra/system-events.js";
|
||||
import { createEmptyPluginRegistry } from "../../plugins/registry-empty.js";
|
||||
import { setActivePluginRegistry } from "../../plugins/runtime.js";
|
||||
import type { ProviderPlugin } from "../../plugins/types.js";
|
||||
import { withEnvAsync } from "../../test-utils/env.js";
|
||||
import type { ElevatedLevel } from "../thinking.js";
|
||||
import { handleDirectiveOnly } from "./directive-handling.impl.js";
|
||||
import {
|
||||
@@ -92,6 +96,7 @@ vi.mock("../../agents/agent-scope.js", () => ({
|
||||
resolveAgentDir: vi.fn(() => "/tmp/agent"),
|
||||
resolveAgentEffectiveModelPrimary: vi.fn(() => undefined),
|
||||
resolveAgentModelFallbacksOverride: vi.fn(() => undefined),
|
||||
resolveAgentWorkspaceDir: vi.fn(() => "/tmp/workspace"),
|
||||
resolveSessionAgentId: vi.fn(() => "main"),
|
||||
}));
|
||||
|
||||
@@ -396,6 +401,77 @@ describe("/model chat UX", () => {
|
||||
expect(reply?.text).not.toContain("missing (missing)");
|
||||
});
|
||||
|
||||
it("uses workspace-scoped auth evidence in /model status labels", async () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-model-status-auth-label-"));
|
||||
const workspaceDir = path.join(tempRoot, "workspace");
|
||||
const pluginDir = path.join(workspaceDir, ".openclaw", "extensions", "workspace-model-auth");
|
||||
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-model-auth",
|
||||
configSchema: { type: "object" },
|
||||
setup: {
|
||||
providers: [
|
||||
{
|
||||
id: "anthropic",
|
||||
authEvidence: [
|
||||
{
|
||||
type: "local-file-with-env",
|
||||
fileEnvVar: "WORKSPACE_MODEL_CREDENTIALS",
|
||||
credentialMarker: "workspace-model-local-credentials",
|
||||
source: "workspace model credentials",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
try {
|
||||
await withEnvAsync(
|
||||
{
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: bundledDir,
|
||||
OPENCLAW_STATE_DIR: stateDir,
|
||||
WORKSPACE_MODEL_CREDENTIALS: credentialPath,
|
||||
},
|
||||
async () => {
|
||||
const reply = await resolveModelInfoReply({
|
||||
directives: parseInlineDirectives("/model status"),
|
||||
workspaceDir,
|
||||
cfg: {
|
||||
...baseConfig(),
|
||||
plugins: { allow: ["workspace-model-auth"] },
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"anthropic/claude-opus-4-6": {},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
allowedModelCatalog: [
|
||||
{ provider: "anthropic", id: "claude-opus-4-6", name: "Claude Opus 4.6" },
|
||||
],
|
||||
});
|
||||
|
||||
expect(reply?.text).toContain("workspace model credentials");
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("auto-applies closest match for typos", () => {
|
||||
const directives = parseInlineDirectives("/model anthropic/claud-opus-4-5");
|
||||
const cfg = { commands: { text: true } } as unknown as OpenClawConfig;
|
||||
|
||||
@@ -202,6 +202,7 @@ export async function maybeHandleModelDirectiveInfo(params: {
|
||||
aliasIndex: ModelAliasIndex;
|
||||
allowedModelCatalog: Array<{ provider: string; id?: string; name?: string }>;
|
||||
resetModelOverride: boolean;
|
||||
workspaceDir?: string;
|
||||
surface?: string;
|
||||
sessionEntry?: Pick<SessionEntry, "modelProvider" | "model">;
|
||||
}): Promise<ReplyPayload | undefined> {
|
||||
@@ -305,6 +306,7 @@ export async function maybeHandleModelDirectiveInfo(params: {
|
||||
modelsPath,
|
||||
params.agentDir,
|
||||
authMode,
|
||||
params.workspaceDir,
|
||||
);
|
||||
authByProvider.set(provider, formatAuthLabel(auth));
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ export type HandleDirectiveOnlyParams = HandleDirectiveOnlyCoreParams & {
|
||||
currentVerboseLevel?: VerboseLevel;
|
||||
currentReasoningLevel?: ReasoningLevel;
|
||||
currentElevatedLevel?: ElevatedLevel;
|
||||
workspaceDir?: string;
|
||||
surface?: string;
|
||||
gatewayClientScopes?: string[];
|
||||
senderIsOwner?: boolean;
|
||||
@@ -49,6 +50,7 @@ export type ApplyInlineDirectivesFastLaneParams = HandleDirectiveOnlyCoreParams
|
||||
commandAuthorized: boolean;
|
||||
senderIsOwner: boolean;
|
||||
ctx: MsgContext;
|
||||
workspaceDir?: string;
|
||||
agentId?: string;
|
||||
isGroup: boolean;
|
||||
agentCfg?: NonNullable<OpenClawConfig["agents"]>["defaults"];
|
||||
|
||||
@@ -344,6 +344,7 @@ export async function applyInlineDirectiveOverrides(params: {
|
||||
surface: ctx.Surface,
|
||||
gatewayClientScopes: ctx.GatewayClientScopes,
|
||||
senderIsOwner: command.senderIsOwner,
|
||||
workspaceDir,
|
||||
});
|
||||
let statusReply: ReplyPayload | undefined;
|
||||
if (directives.hasStatusDirective && allowTextCommands && command.isAuthorizedSender) {
|
||||
@@ -389,6 +390,7 @@ export async function applyInlineDirectiveOverrides(params: {
|
||||
commandAuthorized: command.isAuthorizedSender,
|
||||
senderIsOwner: command.senderIsOwner,
|
||||
ctx,
|
||||
workspaceDir,
|
||||
cfg,
|
||||
agentId,
|
||||
isGroup,
|
||||
|
||||
@@ -158,7 +158,10 @@ 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 statusWorkspaceDir =
|
||||
params.workspaceDir ??
|
||||
sessionEntry?.spawnedWorkspaceDir ??
|
||||
resolveAgentWorkspaceDir(cfg, statusAgentId);
|
||||
const modelRefs = resolveSelectedAndActiveModel({
|
||||
selectedProvider: provider,
|
||||
selectedModel: model,
|
||||
|
||||
Reference in New Issue
Block a user