Skip to content

Commit 6041b7a

Browse files
committed
fix(auto-reply): redact secrets in /debug show and /debug set output
PR #88496 routed /config show and /config set chat output through the shared schema-aware redaction path, but the sibling /debug commands in the same handler were left untouched. /debug show JSON-stringified the full runtime override tree verbatim and /debug set echoed the raw value, so a secret-shaped override (e.g. gateway.auth.token, channels.*.botToken) set via /debug set was rendered in plaintext to chat-visible output. Apply redactConfigObject(overrides, schema.uiHints) to the override tree before rendering /debug show, and reuse formatConfigSetValueLabel for the /debug set acknowledgement, matching the existing /config redaction contract. Non-secret fields and env placeholders are preserved.
1 parent c1219d1 commit 6041b7a

2 files changed

Lines changed: 75 additions & 3 deletions

File tree

src/auto-reply/reply/commands-config.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -248,7 +248,9 @@ export const handleDebugCommand: CommandHandler = async (params, allowTextComman
248248
reply: { text: "⚙️ Debug overrides: (none)" },
249249
};
250250
}
251-
const json = JSON.stringify(overrides, null, 2);
251+
const schema = loadGatewayRuntimeConfigSchema();
252+
const redactedOverrides = redactConfigObject(overrides, schema.uiHints);
253+
const json = JSON.stringify(redactedOverrides, null, 2);
252254
return {
253255
shouldContinue: false,
254256
reply: {
@@ -292,8 +294,14 @@ export const handleDebugCommand: CommandHandler = async (params, allowTextComman
292294
reply: { text: `⚠️ ${result.error ?? "Invalid override."}` },
293295
};
294296
}
295-
const valueLabel =
296-
typeof debugCommand.value === "string"
297+
const parsedOverridePath = parseConfigPath(debugCommand.path);
298+
const valueLabel = parsedOverridePath.path
299+
? formatConfigSetValueLabel({
300+
path: parsedOverridePath.path,
301+
value: debugCommand.value,
302+
uiHints: loadGatewayRuntimeConfigSchema().uiHints,
303+
})
304+
: typeof debugCommand.value === "string"
297305
? `"${debugCommand.value}"`
298306
: JSON.stringify(debugCommand.value);
299307
return {

src/auto-reply/reply/commands-gating.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,20 @@ vi.mock("./debug-commands.js", () => ({
182182
if (!raw.startsWith("/debug")) {
183183
return null;
184184
}
185+
const parts = raw.trim().split(/\s+/);
186+
const action = parts[1];
187+
if (action === "set") {
188+
const assignment = raw.slice(raw.indexOf(" set ") + 5).trim();
189+
const equalsIndex = assignment.indexOf("=");
190+
return {
191+
action: "set",
192+
path: assignment.slice(0, equalsIndex),
193+
value: JSON.parse(assignment.slice(equalsIndex + 1)),
194+
};
195+
}
196+
if (action === "unset") {
197+
return { action: "unset", path: parts.slice(2).join(" ") };
198+
}
185199
return { action: "show" };
186200
}),
187201
}));
@@ -527,6 +541,56 @@ describe("command gating", () => {
527541
expect(output).not.toContain("OPENCLAW_CONFIG_SET_CANARY_TOKEN_65623");
528542
});
529543

544+
it("redacts secret-shaped fields from /debug show replies", async () => {
545+
getConfigOverridesMock.mockReturnValueOnce({
546+
gateway: {
547+
auth: {
548+
token: "OPENCLAW_DEBUG_SHOW_CANARY_TOKEN_65623",
549+
},
550+
},
551+
channels: {
552+
telegram: {
553+
botToken: "OPENCLAW_DEBUG_SHOW_CANARY_BOT_TOKEN_65623",
554+
},
555+
},
556+
messages: {
557+
ackReaction: ":)",
558+
},
559+
});
560+
const params = buildParams("/debug show", {
561+
commands: { debug: true, text: true },
562+
channels: { whatsapp: { allowFrom: ["*"] } },
563+
} as OpenClawConfig);
564+
params.command.senderIsOwner = true;
565+
566+
const result = await handleDebugCommand(params, true);
567+
const output = result?.reply?.text ?? "";
568+
569+
expect(output).toContain("Debug overrides (memory-only)");
570+
expect(output).toContain(REDACTED_SENTINEL);
571+
expect(output).toContain("ackReaction");
572+
expect(output).not.toContain("OPENCLAW_DEBUG_SHOW_CANARY_TOKEN_65623");
573+
expect(output).not.toContain("OPENCLAW_DEBUG_SHOW_CANARY_BOT_TOKEN_65623");
574+
});
575+
576+
it("redacts secret-shaped values from /debug set acknowledgements", async () => {
577+
const params = buildParams(
578+
'/debug set gateway.auth.token="OPENCLAW_DEBUG_SET_CANARY_TOKEN_65623"',
579+
{
580+
commands: { debug: true, text: true },
581+
channels: { whatsapp: { allowFrom: ["*"] } },
582+
} as OpenClawConfig,
583+
);
584+
params.command.senderIsOwner = true;
585+
586+
const result = await handleDebugCommand(params, true);
587+
const output = result?.reply?.text ?? "";
588+
589+
expect(output).toContain("Debug override set: gateway.auth.token=");
590+
expect(output).toContain(REDACTED_SENTINEL);
591+
expect(output).not.toContain("OPENCLAW_DEBUG_SET_CANARY_TOKEN_65623");
592+
});
593+
530594
it("returns explicit unauthorized replies for native privileged commands", async () => {
531595
const configParams = buildParams("/config show", {
532596
commands: { config: true, text: true },

0 commit comments

Comments
 (0)