fix: preserve workspace auth scope in runtime paths

This commit is contained in:
Shakker
2026-04-29 20:43:42 +01:00
parent c4e249114d
commit 2fe3e779ff
6 changed files with 77 additions and 2 deletions

View File

@@ -390,6 +390,7 @@ export async function compactEmbeddedPiSessionDirect(
cfg: params.config,
profileId: authProfileId,
agentDir,
workspaceDir: resolvedWorkspace,
});
if (!apiKeyInfo.apiKey) {

View File

@@ -176,6 +176,12 @@ describe("createEmbeddedRunAuthController", () => {
await controller.initializeAuthProfile();
expect(mocks.getApiKeyForModel).toHaveBeenCalledWith(
expect.objectContaining({
agentDir: "/tmp/agent",
workspaceDir: "/tmp/workspace",
}),
);
expect(harness.runtimeModel.baseUrl).toBe("https://runtime.example.com/v1");
expect(harness.runtimeModel.headers).toEqual({
"api-key": "runtime-header-token",

View File

@@ -353,6 +353,7 @@ export function createEmbeddedRunAuthController(params: {
profileId: candidate,
store: params.authStore,
agentDir: params.agentDir,
workspaceDir: params.workspaceDir,
lockedProfile: candidate != null && candidate === params.lockedProfileId,
});
};

View File

@@ -10,6 +10,15 @@ import { requestHeartbeatNow } from "../../infra/heartbeat-wake.js";
import * as execModule from "../../process/exec.js";
import { onSessionTranscriptUpdate } from "../../sessions/transcript-events.js";
import { VERSION } from "../../version.js";
const runtimeModelAuthMocks = vi.hoisted(() => ({
getApiKeyForModel: vi.fn(),
getRuntimeAuthForModel: vi.fn(),
resolveApiKeyForProvider: vi.fn(),
}));
vi.mock("./runtime-model-auth.runtime.js", () => runtimeModelAuthMocks);
import {
clearGatewaySubagentRuntime,
createPluginRuntime,
@@ -107,6 +116,9 @@ function expectRunCommandOutcome(params: {
describe("plugin runtime command execution", () => {
beforeEach(() => {
vi.restoreAllMocks();
runtimeModelAuthMocks.getApiKeyForModel.mockReset();
runtimeModelAuthMocks.getRuntimeAuthForModel.mockReset();
runtimeModelAuthMocks.resolveApiKeyForProvider.mockReset();
resetConfigRuntimeState();
clearGatewaySubagentRuntime();
});
@@ -291,6 +303,57 @@ describe("plugin runtime command execution", () => {
expect(runtime.modelAuth.getApiKeyForModel).not.toBe(rawGetApiKey);
});
it("modelAuth wrappers preserve workspace scope while stripping credential steering", async () => {
const runtime = createPluginRuntime();
const model = {
id: "workspace-cloud/model",
provider: "workspace-cloud",
api: "openai-responses",
baseUrl: "https://workspace-cloud.example/v1",
};
const cfg = { plugins: { allow: ["workspace-cloud"] } } as OpenClawConfig;
runtimeModelAuthMocks.getApiKeyForModel.mockResolvedValue({
apiKey: "model-key",
source: "workspace cloud credentials",
mode: "api-key",
});
runtimeModelAuthMocks.resolveApiKeyForProvider.mockResolvedValue({
apiKey: "provider-key",
source: "workspace cloud credentials",
mode: "api-key",
});
await expect(
runtime.modelAuth.getApiKeyForModel({
model: model as never,
cfg,
workspaceDir: "/tmp/workspace",
agentDir: "/tmp/agent",
store: { version: 1, profiles: {} },
} as never),
).resolves.toMatchObject({ apiKey: "model-key" });
await expect(
runtime.modelAuth.resolveApiKeyForProvider({
provider: "workspace-cloud",
cfg,
workspaceDir: "/tmp/workspace",
agentDir: "/tmp/agent",
store: { version: 1, profiles: {} },
} as never),
).resolves.toMatchObject({ apiKey: "provider-key" });
expect(runtimeModelAuthMocks.getApiKeyForModel).toHaveBeenCalledWith({
model,
cfg,
workspaceDir: "/tmp/workspace",
});
expect(runtimeModelAuthMocks.resolveApiKeyForProvider).toHaveBeenCalledWith({
provider: "workspace-cloud",
cfg,
workspaceDir: "/tmp/workspace",
});
});
it("keeps subagent unavailable by default even after gateway initialization", async () => {
const { runtime } = createGatewaySubagentRunFixture();

View File

@@ -115,6 +115,7 @@ function createRuntimeModelAuth(): PluginRuntime["modelAuth"] {
getApiKeyForModel({
model: params.model,
cfg: params.cfg,
workspaceDir: params.workspaceDir,
}),
getRuntimeAuthForModel: (params) =>
getRuntimeAuthForModel({
@@ -126,6 +127,7 @@ function createRuntimeModelAuth(): PluginRuntime["modelAuth"] {
resolveApiKeyForProvider({
provider: params.provider,
cfg: params.cfg,
workspaceDir: params.workspaceDir,
}),
};
}

View File

@@ -241,10 +241,11 @@ export type PluginRuntimeCore = {
/** @deprecated Use runtime.tasks.flows for DTO-based TaskFlow access. */
taskFlow: import("./runtime-taskflow.types.js").PluginRuntimeTaskFlow;
modelAuth: {
/** Resolve auth for a model. Only provider/model and optional cfg are used. */
/** Resolve auth for a model. Only provider/model, optional cfg, and workspaceDir are used. */
getApiKeyForModel: (params: {
model: import("@mariozechner/pi-ai").Model<import("@mariozechner/pi-ai").Api>;
cfg?: import("../../config/types.openclaw.js").OpenClawConfig;
workspaceDir?: string;
}) => Promise<import("../../agents/model-auth-runtime-shared.js").ResolvedProviderAuth>;
/** Resolve request-ready auth for a model, including provider runtime exchanges. */
getRuntimeAuthForModel: (params: {
@@ -252,10 +253,11 @@ export type PluginRuntimeCore = {
cfg?: import("../../config/types.openclaw.js").OpenClawConfig;
workspaceDir?: string;
}) => Promise<import("./model-auth-types.js").ResolvedProviderRuntimeAuth>;
/** Resolve auth for a provider by name. Only provider and optional cfg are used. */
/** Resolve auth for a provider by name. Only provider, optional cfg, and workspaceDir are used. */
resolveApiKeyForProvider: (params: {
provider: string;
cfg?: import("../../config/types.openclaw.js").OpenClawConfig;
workspaceDir?: string;
}) => Promise<import("../../agents/model-auth-runtime-shared.js").ResolvedProviderAuth>;
};
};