From 73946f39c3782eb46c89b8e1c0b8c09ad2398e55 Mon Sep 17 00:00:00 2001 From: Epinephrine Date: Mon, 21 Sep 2026 01:49:24 +0000 Subject: [PATCH 1/2] fix(cli): redact proxy credentials in config output --- src/cli/config-command.ts | 8 +++----- structure/config.md | 2 ++ tests/cli/cli-config-command.test.ts | 26 ++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/cli/config-command.ts b/src/cli/config-command.ts index 9a970299fec..5e0c94dd60c 100644 --- a/src/cli/config-command.ts +++ b/src/cli/config-command.ts @@ -19,12 +19,10 @@ const USAGE = `Usage: /** * Keys whose VALUE is a credential and must never be printed or exported. * - * `webhookUrl` is here because for Slack and Discord the URL itself is the authorization: - * anyone holding it can post to the channel. It looks like configuration rather than a secret, - * which is exactly why it needs to be named explicitly — none of the other patterns match it, - * so `ocx config show` printed it and `config export` wrote it to disk in the clear. + * URL-valued credentials must be named explicitly: neither `webhookUrl` nor a proxy URL's + * userinfo matches the other patterns. */ -const SECRET_KEYS = /^(apiKey|key|accessToken|refreshToken|idToken|token|password|clientSecret|webhookUrl)$/i; +const SECRET_KEYS = /^(apiKey|key|accessToken|refreshToken|idToken|token|password|clientSecret|webhookUrl|proxy)$/i; const BLOCKED_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]); /** diff --git a/structure/config.md b/structure/config.md index 12cf9acbd10..e7006630f89 100644 --- a/structure/config.md +++ b/structure/config.md @@ -552,3 +552,5 @@ exclusive `--socks5-off`. The start owner persists only an explicitly requested change; the off flag refuses to erase a non-SOCKS proxy. Invalid-address errors never echo user-supplied credentials, and status messages redact proxy URLs. The parser regression cases live in `tests/cli/start-args.test.ts`. +The config CLI likewise treats every `proxy` value as credential-bearing and masks it in show, +get, and mutation output, including provider-level proxy URLs with userinfo. diff --git a/tests/cli/cli-config-command.test.ts b/tests/cli/cli-config-command.test.ts index ed458846808..69d27291cb6 100644 --- a/tests/cli/cli-config-command.test.ts +++ b/tests/cli/cli-config-command.test.ts @@ -31,6 +31,7 @@ function freshConfig() { adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "forward", + proxy: "http://route_user:route_password@egress.test:3128", }, blsc: { adapter: "openai-chat", @@ -48,6 +49,31 @@ function freshConfig() { } describe("ocx config display redaction", () => { + test("provider proxy credentials stay masked in show, get, and set output", () => { + const dir = freshConfig(); + const secret = "route_password"; + try { + const show = runCli(["config", "show", "--json"], { OPENCODEX_HOME: dir }); + expect(show.status).toBe(0); + expect(show.stdout).not.toContain(secret); + expect(JSON.parse(show.stdout).providers.openai.proxy).toBe("********"); + + const get = runCli(["config", "get", "providers.openai.proxy"], { OPENCODEX_HOME: dir }); + expect(get.status).toBe(0); + expect(get.stdout.trim()).toBe("********"); + + const set = runCli([ + "config", "set", "providers.openai.proxy", + "http://next_user:next_password@egress.test:8080", "--json", + ], { OPENCODEX_HOME: dir }); + expect(set.status).toBe(0); + expect(set.stdout).not.toContain("next_password"); + expect(JSON.parse(set.stdout).value).toBe("********"); + } finally { + removeTreeWithRetry(dir); + } + }); + test("config show --json never prints secret-shaped modelCosts keys", () => { const dir = freshConfig(); try { From acdbb1cb778fb266a4d0a2e30a2384e0d390b963 Mon Sep 17 00:00:00 2001 From: luvs01 <27862058+luvs01@users.noreply.github.com> Date: Tue, 22 Sep 2026 07:25:00 +0900 Subject: [PATCH 2/2] fix(cli): keep non-secret proxy values readable in config output Mask only credential-bearing proxy URLs (userinfo stripped via redactUrlForLog, host/port kept); direct and credential-less values print unchanged. Clarify that config export stays raw for restore. --- src/cli/config-command.ts | 25 +++++++++++++++++++++---- structure/config.md | 6 ++++-- tests/cli/cli-config-command.test.ts | 22 +++++++++++++++++++--- 3 files changed, 44 insertions(+), 9 deletions(-) diff --git a/src/cli/config-command.ts b/src/cli/config-command.ts index 5e0c94dd60c..a1c3ecc5995 100644 --- a/src/cli/config-command.ts +++ b/src/cli/config-command.ts @@ -5,6 +5,7 @@ import { VISION_REASONING_EFFORTS, isVisionReasoningEffort } from "../reasoning- import type { OcxConfig } from "../types"; import { normalizeVisionReasoningForModel } from "../vision/reasoning"; import type { ServiceApiTokenState } from "../lib/service-secrets"; +import { redactUrlForLog } from "../lib/redact"; import { CliUsageError, printData, rejectArgs, runCliAction, takeFlag } from "./runtime-api"; const USAGE = `Usage: @@ -17,12 +18,15 @@ const USAGE = `Usage: ocx config import --yes [--json]`; /** - * Keys whose VALUE is a credential and must never be printed or exported. + * Keys whose VALUE is a credential and must never be printed by display commands. + * `config export` writes the raw config so an export can restore credentials; it does + * not call `redact`. * - * URL-valued credentials must be named explicitly: neither `webhookUrl` nor a proxy URL's - * userinfo matches the other patterns. + * URL-valued credentials must be named explicitly: `webhookUrl` matches none of the + * other patterns, and a proxy URL's userinfo is handled by the `proxy` branch in + * `redact` rather than by masking the whole value. */ -const SECRET_KEYS = /^(apiKey|key|accessToken|refreshToken|idToken|token|password|clientSecret|webhookUrl|proxy)$/i; +const SECRET_KEYS = /^(apiKey|key|accessToken|refreshToken|idToken|token|password|clientSecret|webhookUrl)$/i; const BLOCKED_SEGMENTS = new Set(["__proto__", "prototype", "constructor"]); /** @@ -95,6 +99,19 @@ async function readRemoteHubConfigNote(config: OcxConfig): Promise { const show = runCli(["config", "show", "--json"], { OPENCODEX_HOME: dir }); expect(show.status).toBe(0); expect(show.stdout).not.toContain(secret); - expect(JSON.parse(show.stdout).providers.openai.proxy).toBe("********"); + const shown = JSON.parse(show.stdout).providers; + // Credentialed URL: userinfo stripped, host/port kept for diagnostics. + expect(shown.openai.proxy).toBe("http://egress.test:3128/"); + expect(show.stdout).not.toContain("route_user"); + // "direct" and credential-less URLs carry no secret and stay readable. + expect(shown.blsc.proxy).toBe("direct"); + expect(shown.nocreds.proxy).toBe("http://127.0.0.1:7890"); const get = runCli(["config", "get", "providers.openai.proxy"], { OPENCODEX_HOME: dir }); expect(get.status).toBe(0); - expect(get.stdout.trim()).toBe("********"); + expect(get.stdout.trim()).toBe("http://egress.test:3128/"); + + const getDirect = runCli(["config", "get", "providers.blsc.proxy"], { OPENCODEX_HOME: dir }); + expect(getDirect.status).toBe(0); + expect(getDirect.stdout.trim()).toBe("direct"); const set = runCli([ "config", "set", "providers.openai.proxy", @@ -68,7 +84,7 @@ describe("ocx config display redaction", () => { ], { OPENCODEX_HOME: dir }); expect(set.status).toBe(0); expect(set.stdout).not.toContain("next_password"); - expect(JSON.parse(set.stdout).value).toBe("********"); + expect(JSON.parse(set.stdout).value).toBe("http://egress.test:8080/"); } finally { removeTreeWithRetry(dir); }