fix(opencode): redact credentials in debug config (#50956)
Co-authored-by: rekram1-node <rekram1-node@users.noreply.github.com>
This commit is contained in:
parent
1d6c3c0e29
commit
82d4c89031
3 changed files with 84 additions and 2 deletions
|
|
@ -1,14 +1,14 @@
|
|||
import { EOL } from "os"
|
||||
import { Effect } from "effect"
|
||||
import { effectCmd } from "../../effect-cmd"
|
||||
import { redactConfig } from "./redact"
|
||||
|
||||
export const ConfigCommand = effectCmd({
|
||||
command: "config",
|
||||
describe: "show resolved configuration",
|
||||
builder: (yargs) => yargs,
|
||||
handler: Effect.fn("Cli.debug.config")(function* () {
|
||||
const { Config } = yield* Effect.promise(() => import("@/config/config"))
|
||||
const config = yield* Config.Service.use((cfg) => cfg.get())
|
||||
process.stdout.write(JSON.stringify(config, null, 2) + EOL)
|
||||
process.stdout.write(JSON.stringify(redactConfig(config), null, 2) + EOL)
|
||||
}),
|
||||
})
|
||||
|
|
|
|||
20
packages/opencode/src/cli/cmd/debug/redact.ts
Normal file
20
packages/opencode/src/cli/cmd/debug/redact.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// Redact only the debug output; the resolved configuration must remain usable by providers.
|
||||
const secretName = /(?:api.?key|secret|password|token$|authorization$|cookie$|credential|private.?key)/i
|
||||
|
||||
export function redactConfig(value: unknown, headers = false): unknown {
|
||||
if (Array.isArray(value)) return value.map((item) => redactConfig(item, headers))
|
||||
if (value === null || typeof value !== "object") return value
|
||||
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, item]) => {
|
||||
if (typeof item === "string" && (headers || secretName.test(key))) return [key, "***"]
|
||||
if (typeof item === "string" && /^https?:\/\//i.test(item)) {
|
||||
if (!URL.canParse(item)) return [key, "***"]
|
||||
const url = new URL(item)
|
||||
if (url.username || url.password || [...url.searchParams.keys()].some((name) => secretName.test(name)))
|
||||
return [key, "***"]
|
||||
}
|
||||
return [key, redactConfig(item, headers || key.toLowerCase() === "headers")]
|
||||
}),
|
||||
)
|
||||
}
|
||||
62
packages/opencode/test/cli/debug-config.test.ts
Normal file
62
packages/opencode/test/cli/debug-config.test.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { describe, expect } from "bun:test"
|
||||
import { Effect } from "effect"
|
||||
import { cliIt } from "../lib/cli-process"
|
||||
import { redactConfig } from "@/cli/cmd/debug/redact"
|
||||
|
||||
const config = {
|
||||
provider: {
|
||||
example: {
|
||||
options: {
|
||||
apiKey: "sk-example",
|
||||
timeout: 1200,
|
||||
headers: { Authorization: "Bearer example", "X-API-Key": "key" },
|
||||
},
|
||||
models: { demo: { variants: { fast: { api_key: "variant-secret" } } } },
|
||||
},
|
||||
},
|
||||
mcp: {
|
||||
remote: { oauth: { clientSecret: "oauth-secret", clientId: "public" }, headers: { "x-custom": "opaque" } },
|
||||
local: { environment: { SERVICE_TOKEN: "env-secret", PATH: "/usr/bin" } },
|
||||
},
|
||||
url: "https://user:pass@example.com/path",
|
||||
normal: { context_tokens: 200000, name: "example" },
|
||||
}
|
||||
|
||||
describe("debug config redaction", () => {
|
||||
cliIt.live("always masks resolved credentials", ({ opencode }) =>
|
||||
Effect.gen(function* () {
|
||||
const content = JSON.stringify({ provider: config.provider })
|
||||
const env = { OPENCODE_CONFIG_CONTENT: content }
|
||||
const result = yield* opencode.spawn(["debug", "config"], { env })
|
||||
opencode.expectExit(result, 0, "debug config")
|
||||
expect(JSON.parse(result.stdout).provider.example.options).toMatchObject({
|
||||
apiKey: "***",
|
||||
timeout: 1200,
|
||||
headers: { Authorization: "***", "X-API-Key": "***" },
|
||||
})
|
||||
expect(result.stdout).not.toContain("sk-example")
|
||||
expect(result.stdout).not.toContain("Bearer example")
|
||||
}),
|
||||
)
|
||||
|
||||
cliIt.live("does not mutate the input and preserves unrelated settings", () =>
|
||||
Effect.sync(() => {
|
||||
const before = structuredClone(config)
|
||||
expect(redactConfig(config)).toEqual({
|
||||
provider: {
|
||||
example: {
|
||||
options: { apiKey: "***", timeout: 1200, headers: { Authorization: "***", "X-API-Key": "***" } },
|
||||
models: { demo: { variants: { fast: { api_key: "***" } } } },
|
||||
},
|
||||
},
|
||||
mcp: {
|
||||
remote: { oauth: { clientSecret: "***", clientId: "public" }, headers: { "x-custom": "***" } },
|
||||
local: { environment: { SERVICE_TOKEN: "***", PATH: "/usr/bin" } },
|
||||
},
|
||||
url: "***",
|
||||
normal: config.normal,
|
||||
})
|
||||
expect(config).toEqual(before)
|
||||
}),
|
||||
)
|
||||
})
|
||||
Loading…
Reference in a new issue