mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix(gateway): preserve existing skill secrets on redacted round-trips
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -14,7 +14,7 @@ import { buildWorkspaceSkillStatus } from "../../agents/skills-status.js";
|
||||
import { loadWorkspaceSkillEntries, type SkillEntry } from "../../agents/skills.js";
|
||||
import { listAgentWorkspaceDirs } from "../../agents/workspace-dirs.js";
|
||||
import { loadConfig, writeConfigFile } from "../../config/config.js";
|
||||
import { redactConfigObject } from "../../config/redact-snapshot.js";
|
||||
import { redactConfigObject, REDACTED_SENTINEL } from "../../config/redact-snapshot.js";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { fetchClawHubSkillDetail } from "../../infra/clawhub.js";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
@@ -313,7 +313,9 @@ export const skillsHandlers: GatewayRequestHandlers = {
|
||||
}
|
||||
if (typeof p.apiKey === "string") {
|
||||
const trimmed = normalizeSecretInput(p.apiKey);
|
||||
if (trimmed) {
|
||||
if (trimmed === REDACTED_SENTINEL) {
|
||||
// Keep the stored secret when a client round-trips a redacted response value.
|
||||
} else if (trimmed) {
|
||||
current.apiKey = trimmed;
|
||||
} else {
|
||||
delete current.apiKey;
|
||||
@@ -327,6 +329,9 @@ export const skillsHandlers: GatewayRequestHandlers = {
|
||||
continue;
|
||||
}
|
||||
const trimmedVal = value.trim();
|
||||
if (trimmedVal === REDACTED_SENTINEL) {
|
||||
continue;
|
||||
}
|
||||
if (!trimmedVal) {
|
||||
delete nextEnv[trimmedKey];
|
||||
} else {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { REDACTED_SENTINEL } from "../../config/redact-snapshot.js";
|
||||
|
||||
let writtenConfig: unknown = null;
|
||||
let loadedConfig: unknown = {
|
||||
skills: {
|
||||
entries: {},
|
||||
},
|
||||
};
|
||||
|
||||
vi.mock("../../config/config.js", () => {
|
||||
return {
|
||||
loadConfig: () => ({
|
||||
skills: {
|
||||
entries: {},
|
||||
},
|
||||
}),
|
||||
loadConfig: () => loadedConfig,
|
||||
writeConfigFile: async (cfg: unknown) => {
|
||||
writtenConfig = cfg;
|
||||
},
|
||||
@@ -20,6 +22,11 @@ const { skillsHandlers } = await import("./skills.js");
|
||||
describe("skills.update", () => {
|
||||
it("strips embedded CR/LF from apiKey", async () => {
|
||||
writtenConfig = null;
|
||||
loadedConfig = {
|
||||
skills: {
|
||||
entries: {},
|
||||
},
|
||||
};
|
||||
|
||||
let ok: boolean | null = null;
|
||||
let error: unknown = null;
|
||||
@@ -53,6 +60,11 @@ describe("skills.update", () => {
|
||||
|
||||
it("redacts apiKey and secret env values from the response but writes full values to config", async () => {
|
||||
writtenConfig = null;
|
||||
loadedConfig = {
|
||||
skills: {
|
||||
entries: {},
|
||||
},
|
||||
};
|
||||
|
||||
let responseResult: unknown = null;
|
||||
await skillsHandlers["skills.update"]({
|
||||
@@ -90,10 +102,57 @@ describe("skills.update", () => {
|
||||
|
||||
// Response must not expose plaintext secrets
|
||||
const config = (responseResult as { config: Record<string, unknown> }).config;
|
||||
expect(config.apiKey).toBe("__OPENCLAW_REDACTED__");
|
||||
expect(config.apiKey).toBe(REDACTED_SENTINEL);
|
||||
const env = config.env as Record<string, string>;
|
||||
expect(env.GEMINI_API_KEY).toBe("__OPENCLAW_REDACTED__");
|
||||
expect(env.GEMINI_API_KEY).toBe(REDACTED_SENTINEL);
|
||||
// Non-secret env values should still be present
|
||||
expect(env.BRAVE_REGION).toBe("us");
|
||||
});
|
||||
|
||||
it("keeps existing secrets when clients submit redacted sentinel values", async () => {
|
||||
writtenConfig = null;
|
||||
loadedConfig = {
|
||||
skills: {
|
||||
entries: {
|
||||
"demo-skill": {
|
||||
apiKey: "secret-api-key-123",
|
||||
env: {
|
||||
GEMINI_API_KEY: "secret-env-key-456",
|
||||
BRAVE_REGION: "us",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
await skillsHandlers["skills.update"]({
|
||||
params: {
|
||||
skillKey: "demo-skill",
|
||||
apiKey: REDACTED_SENTINEL,
|
||||
env: {
|
||||
GEMINI_API_KEY: REDACTED_SENTINEL,
|
||||
BRAVE_REGION: "eu",
|
||||
},
|
||||
},
|
||||
req: {} as never,
|
||||
client: null as never,
|
||||
isWebchatConnect: () => false,
|
||||
context: {} as never,
|
||||
respond: () => {},
|
||||
});
|
||||
|
||||
expect(writtenConfig).toMatchObject({
|
||||
skills: {
|
||||
entries: {
|
||||
"demo-skill": {
|
||||
apiKey: "secret-api-key-123",
|
||||
env: {
|
||||
GEMINI_API_KEY: "secret-env-key-456",
|
||||
BRAVE_REGION: "eu",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user