fix(opencode): redact secrets from config responses

This commit is contained in:
Aiden Cline
2026-06-24 16:27:09 -05:00
parent 4b83f5d8f0
commit c44ef334e4
4 changed files with 160 additions and 2 deletions
+71
View File
@@ -114,6 +114,77 @@ type Info = ConfigV1.Info & {
plugin_origins?: ConfigPlugin.Origin[]
}
const redacted = "[redacted]"
export function toPublicInfo(info: Info): Info {
return {
...info,
provider: info.provider
? Object.fromEntries(
Object.entries(info.provider).map(([id, provider]) => [
id,
{
...provider,
options: provider.options
? (redactProviderOptions(provider.options) as typeof provider.options)
: undefined,
},
]),
)
: undefined,
mcp: info.mcp
? Object.fromEntries(
Object.entries(info.mcp).map(([id, server]) => {
if (!("type" in server)) return [id, server]
if (server.type === "local") {
return [
id,
{
...server,
environment: server.environment
? Object.fromEntries(Object.keys(server.environment).map((key) => [key, redacted]))
: undefined,
},
]
}
return [
id,
{
...server,
headers: server.headers
? Object.fromEntries(Object.keys(server.headers).map((key) => [key, redacted]))
: undefined,
oauth:
server.oauth && server.oauth.clientSecret
? { ...server.oauth, clientSecret: redacted }
: server.oauth,
},
]
}),
)
: undefined,
}
}
function redactProviderOptions(value: unknown, key?: string): unknown {
const normalized = key?.replaceAll(/[-_]/g, "").toLowerCase()
if (
normalized &&
(normalized.endsWith("apikey") ||
normalized.endsWith("token") ||
normalized.includes("secret") ||
normalized.includes("password") ||
normalized.includes("credential") ||
normalized === "accesskeyid" ||
normalized === "authorization" ||
normalized.endsWith("cookie"))
)
return redacted
if (Array.isArray(value)) return value.map((item) => redactProviderOptions(item))
if (!isRecord(value)) return value
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redactProviderOptions(item, key)]))
}
type State = {
config: Info
directories: string[]
@@ -12,7 +12,7 @@ export const configHandlers = HttpApiBuilder.group(InstanceHttpApi, "config", (h
const configSvc = yield* Config.Service
const get = Effect.fn("ConfigHttpApi.get")(function* () {
return yield* configSvc.get()
return Config.toPublicInfo(yield* configSvc.get())
})
const update = Effect.fn("ConfigHttpApi.update")(function* (ctx) {
@@ -80,7 +80,7 @@ export const globalHandlers = HttpApiBuilder.group(RootHttpApi, "global", (handl
})
const configGet = Effect.fn("GlobalHttpApi.configGet")(function* () {
return yield* config.getGlobal()
return Config.toPublicInfo(yield* config.getGlobal())
})
const configUpdate = Effect.fn("GlobalHttpApi.configUpdate")(function* (ctx) {
@@ -64,6 +64,93 @@ describe("config HttpApi", () => {
}),
)
it.live(
"redacts resolved provider and MCP secrets",
Effect.gen(function* () {
const secrets = [
"CANARY_PROVIDER_API_KEY",
"CANARY_PROVIDER_CLIENT_SECRET",
"CANARY_PROVIDER_NESTED_TOKEN",
"CANARY_MCP_ENV",
"CANARY_MCP_HEADER",
"CANARY_MCP_CLIENT_SECRET",
]
const tmp = yield* tmpdirEffect({
init: (dir) =>
Promise.all(secrets.map((secret, index) => Bun.write(path.join(dir, `secret-${index}`), secret))),
config: {
formatter: false,
lsp: false,
provider: {
canary: {
name: "Canary Provider",
options: {
apiKey: "{file:secret-0}",
clientSecret: "{file:secret-1}",
nested: { accessToken: "{file:secret-2}", temperature: 0.5 },
baseURL: "https://provider.example.com",
},
},
},
mcp: {
local: {
type: "local",
command: ["canary-command"],
environment: { TOKEN: "{file:secret-3}" },
enabled: false,
},
remote: {
type: "remote",
url: "https://mcp.example.com",
headers: { Authorization: "{file:secret-4}" },
oauth: { clientId: "canary-client", clientSecret: "{file:secret-5}", scope: "read" },
enabled: false,
},
},
},
})
const response = yield* Effect.promise(() =>
Promise.resolve(
app().request("/config", {
headers: {
"x-opencode-directory": tmp.path,
},
}),
),
)
const text = yield* Effect.promise(() => response.text())
const body = JSON.parse(text)
expect(response.status).toBe(200)
secrets.forEach((secret) => expect(text).not.toContain(secret))
expect(body).toMatchObject({
provider: {
canary: {
name: "Canary Provider",
options: {
apiKey: "[redacted]",
clientSecret: "[redacted]",
nested: { accessToken: "[redacted]", temperature: 0.5 },
baseURL: "https://provider.example.com",
},
},
},
mcp: {
local: {
command: ["canary-command"],
environment: { TOKEN: "[redacted]" },
},
remote: {
url: "https://mcp.example.com",
headers: { Authorization: "[redacted]" },
oauth: { clientId: "canary-client", clientSecret: "[redacted]", scope: "read" },
},
},
})
}),
)
it.live(
"serves config with active provider model status",
Effect.gen(function* () {