Skip to content

Commit ba9fb4d

Browse files
committed
fix: persist auth profile env refs for daemon install
1 parent 549cb65 commit ba9fb4d

2 files changed

Lines changed: 106 additions & 0 deletions

File tree

src/commands/daemon-install-helpers.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import { afterEach, describe, expect, it, vi } from "vitest";
22

33
const mocks = vi.hoisted(() => ({
4+
loadAuthProfileStoreForSecretsRuntime: vi.fn(),
45
resolvePreferredNodePath: vi.fn(),
56
resolveGatewayProgramArguments: vi.fn(),
67
resolveSystemNodeInfo: vi.fn(),
78
renderSystemNodeWarning: vi.fn(),
89
buildServiceEnvironment: vi.fn(),
910
}));
1011

12+
vi.mock("../agents/auth-profiles.js", () => ({
13+
loadAuthProfileStoreForSecretsRuntime: mocks.loadAuthProfileStoreForSecretsRuntime,
14+
}));
15+
1116
vi.mock("../daemon/runtime-paths.js", () => ({
1217
resolvePreferredNodePath: mocks.resolvePreferredNodePath,
1318
resolveSystemNodeInfo: mocks.resolveSystemNodeInfo,
@@ -63,6 +68,10 @@ function mockNodeGatewayPlanFixture(
6368
programArguments: ["node", "gateway"],
6469
workingDirectory,
6570
});
71+
mocks.loadAuthProfileStoreForSecretsRuntime.mockReturnValue({
72+
version: 1,
73+
profiles: {},
74+
});
6675
mocks.resolveSystemNodeInfo.mockResolvedValue({
6776
path: "/opt/node",
6877
version,
@@ -232,6 +241,67 @@ describe("buildGatewayInstallPlan", () => {
232241
expect(plan.environment.HOME).toBe("/Users/service");
233242
expect(plan.environment.OPENCLAW_PORT).toBe("3000");
234243
});
244+
245+
it("merges env-backed auth-profile refs into the service environment", async () => {
246+
mockNodeGatewayPlanFixture({
247+
serviceEnvironment: {
248+
OPENCLAW_PORT: "3000",
249+
},
250+
});
251+
mocks.loadAuthProfileStoreForSecretsRuntime.mockReturnValue({
252+
version: 1,
253+
profiles: {
254+
"openai:default": {
255+
type: "api_key",
256+
provider: "openai",
257+
keyRef: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
258+
},
259+
"anthropic:default": {
260+
type: "token",
261+
provider: "anthropic",
262+
tokenRef: { source: "env", provider: "default", id: "ANTHROPIC_TOKEN" },
263+
},
264+
},
265+
});
266+
267+
const plan = await buildGatewayInstallPlan({
268+
env: {
269+
OPENAI_API_KEY: "sk-openai-test", // pragma: allowlist secret
270+
ANTHROPIC_TOKEN: "ant-test-token",
271+
},
272+
port: 3000,
273+
runtime: "node",
274+
});
275+
276+
expect(plan.environment.OPENAI_API_KEY).toBe("sk-openai-test");
277+
expect(plan.environment.ANTHROPIC_TOKEN).toBe("ant-test-token");
278+
});
279+
280+
it("skips unresolved auth-profile env refs", async () => {
281+
mockNodeGatewayPlanFixture({
282+
serviceEnvironment: {
283+
OPENCLAW_PORT: "3000",
284+
},
285+
});
286+
mocks.loadAuthProfileStoreForSecretsRuntime.mockReturnValue({
287+
version: 1,
288+
profiles: {
289+
"openai:default": {
290+
type: "api_key",
291+
provider: "openai",
292+
keyRef: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
293+
},
294+
},
295+
});
296+
297+
const plan = await buildGatewayInstallPlan({
298+
env: {},
299+
port: 3000,
300+
runtime: "node",
301+
});
302+
303+
expect(plan.environment.OPENAI_API_KEY).toBeUndefined();
304+
});
235305
});
236306

237307
describe("gatewayInstallErrorHint", () => {

src/commands/daemon-install-helpers.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import {
2+
loadAuthProfileStoreForSecretsRuntime,
3+
type AuthProfileStore,
4+
} from "../agents/auth-profiles.js";
15
import { formatCliCommand } from "../cli/command-format.js";
26
import { collectConfigServiceEnvVars } from "../config/env-vars.js";
37
import type { OpenClawConfig } from "../config/types.js";
@@ -19,6 +23,33 @@ export type GatewayInstallPlan = {
1923
environment: Record<string, string | undefined>;
2024
};
2125

26+
function collectAuthProfileServiceEnvVars(params: {
27+
env: Record<string, string | undefined>;
28+
authStore?: AuthProfileStore;
29+
}): Record<string, string> {
30+
const authStore = params.authStore ?? loadAuthProfileStoreForSecretsRuntime();
31+
const entries: Record<string, string> = {};
32+
33+
for (const credential of Object.values(authStore.profiles)) {
34+
const ref =
35+
credential.type === "api_key"
36+
? credential.keyRef
37+
: credential.type === "token"
38+
? credential.tokenRef
39+
: undefined;
40+
if (!ref || ref.source !== "env") {
41+
continue;
42+
}
43+
const value = params.env[ref.id]?.trim();
44+
if (!value) {
45+
continue;
46+
}
47+
entries[ref.id] = value;
48+
}
49+
50+
return entries;
51+
}
52+
2253
export async function buildGatewayInstallPlan(params: {
2354
env: Record<string, string | undefined>;
2455
port: number;
@@ -28,6 +59,7 @@ export async function buildGatewayInstallPlan(params: {
2859
warn?: DaemonInstallWarnFn;
2960
/** Full config to extract env vars from (env vars + inline env keys). */
3061
config?: OpenClawConfig;
62+
authStore?: AuthProfileStore;
3163
}): Promise<GatewayInstallPlan> {
3264
const { devMode, nodePath } = await resolveDaemonInstallRuntimeInputs({
3365
env: params.env,
@@ -61,6 +93,10 @@ export async function buildGatewayInstallPlan(params: {
6193
// Config env vars are added first so service-specific vars take precedence.
6294
const environment: Record<string, string | undefined> = {
6395
...collectConfigServiceEnvVars(params.config),
96+
...collectAuthProfileServiceEnvVars({
97+
env: params.env,
98+
authStore: params.authStore,
99+
}),
64100
};
65101
Object.assign(environment, serviceEnvironment);
66102

0 commit comments

Comments
 (0)