Skip to content

Commit 61fc06f

Browse files
hxy91819Copilot
andcommitted
fix(gateway): preserve existing skill secrets on redacted round-trips
Co-authored-by: Copilot <[email protected]>
1 parent 8dae600 commit 61fc06f

2 files changed

Lines changed: 73 additions & 9 deletions

File tree

src/gateway/server-methods/skills.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { buildWorkspaceSkillStatus } from "../../agents/skills-status.js";
1414
import { loadWorkspaceSkillEntries, type SkillEntry } from "../../agents/skills.js";
1515
import { listAgentWorkspaceDirs } from "../../agents/workspace-dirs.js";
1616
import { loadConfig, writeConfigFile } from "../../config/config.js";
17-
import { redactConfigObject } from "../../config/redact-snapshot.js";
17+
import { redactConfigObject, REDACTED_SENTINEL } from "../../config/redact-snapshot.js";
1818
import type { OpenClawConfig } from "../../config/types.openclaw.js";
1919
import { fetchClawHubSkillDetail } from "../../infra/clawhub.js";
2020
import { formatErrorMessage } from "../../infra/errors.js";
@@ -313,7 +313,9 @@ export const skillsHandlers: GatewayRequestHandlers = {
313313
}
314314
if (typeof p.apiKey === "string") {
315315
const trimmed = normalizeSecretInput(p.apiKey);
316-
if (trimmed) {
316+
if (trimmed === REDACTED_SENTINEL) {
317+
// Keep the stored secret when a client round-trips a redacted response value.
318+
} else if (trimmed) {
317319
current.apiKey = trimmed;
318320
} else {
319321
delete current.apiKey;
@@ -327,6 +329,9 @@ export const skillsHandlers: GatewayRequestHandlers = {
327329
continue;
328330
}
329331
const trimmedVal = value.trim();
332+
if (trimmedVal === REDACTED_SENTINEL) {
333+
continue;
334+
}
330335
if (!trimmedVal) {
331336
delete nextEnv[trimmedKey];
332337
} else {

src/gateway/server-methods/skills.update.normalizes-api-key.test.ts

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
import { describe, expect, it, vi } from "vitest";
2+
import { REDACTED_SENTINEL } from "../../config/redact-snapshot.js";
23

34
let writtenConfig: unknown = null;
5+
let loadedConfig: unknown = {
6+
skills: {
7+
entries: {},
8+
},
9+
};
410

511
vi.mock("../../config/config.js", () => {
612
return {
7-
loadConfig: () => ({
8-
skills: {
9-
entries: {},
10-
},
11-
}),
13+
loadConfig: () => loadedConfig,
1214
writeConfigFile: async (cfg: unknown) => {
1315
writtenConfig = cfg;
1416
},
@@ -20,6 +22,11 @@ const { skillsHandlers } = await import("./skills.js");
2022
describe("skills.update", () => {
2123
it("strips embedded CR/LF from apiKey", async () => {
2224
writtenConfig = null;
25+
loadedConfig = {
26+
skills: {
27+
entries: {},
28+
},
29+
};
2330

2431
let ok: boolean | null = null;
2532
let error: unknown = null;
@@ -53,6 +60,11 @@ describe("skills.update", () => {
5360

5461
it("redacts apiKey and secret env values from the response but writes full values to config", async () => {
5562
writtenConfig = null;
63+
loadedConfig = {
64+
skills: {
65+
entries: {},
66+
},
67+
};
5668

5769
let responseResult: unknown = null;
5870
await skillsHandlers["skills.update"]({
@@ -90,10 +102,57 @@ describe("skills.update", () => {
90102

91103
// Response must not expose plaintext secrets
92104
const config = (responseResult as { config: Record<string, unknown> }).config;
93-
expect(config.apiKey).toBe("__OPENCLAW_REDACTED__");
105+
expect(config.apiKey).toBe(REDACTED_SENTINEL);
94106
const env = config.env as Record<string, string>;
95-
expect(env.GEMINI_API_KEY).toBe("__OPENCLAW_REDACTED__");
107+
expect(env.GEMINI_API_KEY).toBe(REDACTED_SENTINEL);
96108
// Non-secret env values should still be present
97109
expect(env.BRAVE_REGION).toBe("us");
98110
});
111+
112+
it("keeps existing secrets when clients submit redacted sentinel values", async () => {
113+
writtenConfig = null;
114+
loadedConfig = {
115+
skills: {
116+
entries: {
117+
"demo-skill": {
118+
apiKey: "secret-api-key-123",
119+
env: {
120+
GEMINI_API_KEY: "secret-env-key-456",
121+
BRAVE_REGION: "us",
122+
},
123+
},
124+
},
125+
},
126+
};
127+
128+
await skillsHandlers["skills.update"]({
129+
params: {
130+
skillKey: "demo-skill",
131+
apiKey: REDACTED_SENTINEL,
132+
env: {
133+
GEMINI_API_KEY: REDACTED_SENTINEL,
134+
BRAVE_REGION: "eu",
135+
},
136+
},
137+
req: {} as never,
138+
client: null as never,
139+
isWebchatConnect: () => false,
140+
context: {} as never,
141+
respond: () => {},
142+
});
143+
144+
expect(writtenConfig).toMatchObject({
145+
skills: {
146+
entries: {
147+
"demo-skill": {
148+
apiKey: "secret-api-key-123",
149+
env: {
150+
GEMINI_API_KEY: "secret-env-key-456",
151+
BRAVE_REGION: "eu",
152+
},
153+
},
154+
},
155+
},
156+
});
157+
});
99158
});

0 commit comments

Comments
 (0)