Skip to content

Commit 7729744

Browse files
committed
fix(install): manage auth-profile env refs via OPENCLAW_SERVICE_MANAGED_ENV_KEYS
1 parent 2844ec2 commit 7729744

3 files changed

Lines changed: 122 additions & 8 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Reproduction for #95895: Auth-profile provider API keys (e.g. GEMINI_API_KEY)
2+
// must be managed via OPENCLAW_SERVICE_MANAGED_ENV_KEYS in generated gateway
3+
// service files, not written as plaintext literals.
4+
import fs from "node:fs";
5+
import os from "node:os";
6+
import path from "node:path";
7+
import { fileURLToPath } from "node:url";
8+
9+
import { buildGatewayInstallPlan } from "../../src/commands/daemon-install-helpers.ts";
10+
11+
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
12+
const repoRoot = path.resolve(scriptDir, "../..");
13+
14+
async function main() {
15+
const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-repro-95895-"));
16+
const nodePath = process.execPath;
17+
const wrapperPath = path.resolve(repoRoot, "openclaw.mjs");
18+
19+
try {
20+
const plan = await buildGatewayInstallPlan({
21+
env: {
22+
HOME: tmpHome,
23+
GEMINI_API_KEY: "sk-gemini-repro-95895", // pragma: allowlist secret
24+
PATH: process.env.PATH,
25+
},
26+
port: 18789,
27+
runtime: "node",
28+
nodePath,
29+
wrapperPath,
30+
devMode: true,
31+
platform: process.platform,
32+
authStore: {
33+
version: 1,
34+
profiles: {
35+
"google:default": {
36+
type: "api_key",
37+
provider: "google",
38+
keyRef: { source: "env", provider: "default", id: "GEMINI_API_KEY" },
39+
},
40+
},
41+
},
42+
});
43+
44+
const managedKeys = plan.environment.OPENCLAW_SERVICE_MANAGED_ENV_KEYS;
45+
const geminiInline = plan.environment.GEMINI_API_KEY;
46+
47+
console.log("=== Reproduction for issue #95895 ===");
48+
console.log(`OPENCLAW_SERVICE_MANAGED_ENV_KEYS=${managedKeys ?? "(missing)"}`);
49+
console.log(`GEMINI_API_KEY inline=${geminiInline ?? "(not inline - good)"}`);
50+
51+
const isManaged =
52+
typeof managedKeys === "string" &&
53+
managedKeys
54+
.split(",")
55+
.map((k) => k.trim())
56+
.includes("GEMINI_API_KEY");
57+
58+
if (!isManaged) {
59+
console.error("FAIL: GEMINI_API_KEY from an auth profile is not managed.");
60+
process.exitCode = 1;
61+
return;
62+
}
63+
if (geminiInline !== undefined) {
64+
console.error("FAIL: GEMINI_API_KEY is still written as a plaintext literal.");
65+
process.exitCode = 1;
66+
return;
67+
}
68+
69+
console.log("PASS: Auth-profile GEMINI_API_KEY is managed, not written as plaintext.");
70+
} finally {
71+
fs.rmSync(tmpHome, { recursive: true, force: true });
72+
}
73+
}
74+
75+
main().catch((error: unknown) => {
76+
console.error(error);
77+
process.exitCode = 1;
78+
});

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

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -993,8 +993,8 @@ describe("buildGatewayInstallPlan", () => {
993993
},
994994
});
995995

996-
expect(plan.environment.OPENAI_API_KEY).toBe("sk-openai-test");
997-
expect(plan.environment.OPENCLAW_SERVICE_MANAGED_ENV_KEYS).toBeUndefined();
996+
expect(plan.environment.OPENAI_API_KEY).toBeUndefined();
997+
expect(plan.environment.OPENCLAW_SERVICE_MANAGED_ENV_KEYS).toBe("OPENAI_API_KEY");
998998
expect(mocks.hasAnyAuthProfileStoreSource).not.toHaveBeenCalled();
999999
expect(mocks.loadAuthProfileStoreForSecretsRuntime).not.toHaveBeenCalled();
10001000
});
@@ -1058,9 +1058,11 @@ describe("buildGatewayInstallPlan", () => {
10581058
expect(plan.environment.GIT_ASKPASS).toBeUndefined();
10591059
expect(plan.environment["BAD KEY"]).toBeUndefined();
10601060
expect(plan.environment.MISSING_TOKEN).toBeUndefined();
1061-
expect(plan.environment.OPENAI_API_KEY).toBe("sk-openai-test");
1062-
expect(plan.environment.ANTHROPIC_TOKEN).toBe("ant-test-token");
1063-
expect(plan.environment.OPENCLAW_SERVICE_MANAGED_ENV_KEYS).toBeUndefined();
1061+
expect(plan.environment.OPENAI_API_KEY).toBeUndefined();
1062+
expect(plan.environment.ANTHROPIC_TOKEN).toBeUndefined();
1063+
expect(plan.environment.OPENCLAW_SERVICE_MANAGED_ENV_KEYS).toBe(
1064+
"ANTHROPIC_TOKEN,OPENAI_API_KEY",
1065+
);
10641066
expect(warn).toHaveBeenCalledWith(
10651067
'Auth profile env ref "NODE_OPTIONS" blocked by host-env security policy',
10661068
"Auth profile",
@@ -1070,6 +1072,35 @@ describe("buildGatewayInstallPlan", () => {
10701072
"Auth profile",
10711073
);
10721074
});
1075+
1076+
it("marks auth-profile env refs as managed so they are not written as plaintext literals (#95895)", async () => {
1077+
mockNodeGatewayPlanFixture({
1078+
serviceEnvironment: {
1079+
OPENCLAW_PORT: "3000",
1080+
},
1081+
});
1082+
mocks.loadAuthProfileStoreForSecretsRuntime.mockReturnValue({
1083+
version: 1,
1084+
profiles: {
1085+
"google:default": {
1086+
type: "api_key",
1087+
provider: "google",
1088+
keyRef: { source: "env", provider: "default", id: "GEMINI_API_KEY" },
1089+
},
1090+
},
1091+
});
1092+
1093+
const plan = await buildGatewayInstallPlan({
1094+
env: isolatedPlanEnv({
1095+
GEMINI_API_KEY: "sk-gemini-test", // pragma: allowlist secret
1096+
}),
1097+
port: 3000,
1098+
runtime: "node",
1099+
});
1100+
1101+
expect(plan.environment.GEMINI_API_KEY).toBeUndefined();
1102+
expect(plan.environment.OPENCLAW_SERVICE_MANAGED_ENV_KEYS).toBe("GEMINI_API_KEY");
1103+
});
10731104
});
10741105

10751106
describe("buildGatewayInstallPlan — dotenv merge", () => {

src/commands/daemon-install-helpers.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -568,9 +568,14 @@ async function buildGatewayInstallEnvironment(params: {
568568
addServiceEnvPlanEntries(plan, configSecretRefEnvironment, { source: "config-secretref-env" });
569569
addServiceEnvPlanEntries(plan, execSecretRefPassEnvEnvironment, { source: "exec-passenv" });
570570
addServiceEnvPlanEntries(plan, authProfileEnvironment, { source: "auth-profile-env" });
571-
const managedServiceEnvKeys = formatManagedServiceEnvKeys(durableEnvironment, {
572-
omitKeys: Object.keys(params.serviceEnvironment),
573-
});
571+
// Auth-profile env refs (e.g. GEMINI_API_KEY resolved from an auth profile) are
572+
// also service-managed secrets. Include them when computing managed keys so the
573+
// generated service file references them via OPENCLAW_SERVICE_MANAGED_ENV_KEYS
574+
// instead of writing the plaintext value inline (#95895).
575+
const managedServiceEnvKeys = formatManagedServiceEnvKeys(
576+
{ ...durableEnvironment, ...authProfileEnvironment },
577+
{ omitKeys: Object.keys(params.serviceEnvironment) },
578+
);
574579
applyManagedServiceEnvRenderPolicy({
575580
plan,
576581
managedServiceEnvKeys,

0 commit comments

Comments
 (0)