From c44ef334e4c56bbafb0fa6db2ba6772a018ad0e6 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 24 Jun 2026 16:27:09 -0500 Subject: [PATCH 1/2] fix(opencode): redact secrets from config responses --- packages/opencode/src/config/config.ts | 71 +++++++++++++++ .../instance/httpapi/handlers/config.ts | 2 +- .../instance/httpapi/handlers/global.ts | 2 +- .../test/server/httpapi-config.test.ts | 87 +++++++++++++++++++ 4 files changed, 160 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 7f568f492073..400969eab2c1 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -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[] diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/config.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/config.ts index 3d0e8a06c09c..575203b50fa4 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/config.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/config.ts @@ -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) { diff --git a/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts b/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts index c1f588d5a146..19208a872c09 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts @@ -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) { diff --git a/packages/opencode/test/server/httpapi-config.test.ts b/packages/opencode/test/server/httpapi-config.test.ts index 91281b455ff5..5b80fd8972a9 100644 --- a/packages/opencode/test/server/httpapi-config.test.ts +++ b/packages/opencode/test/server/httpapi-config.test.ts @@ -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* () { From d3da6b8e271db721dd3f3bdf67142df624c39baf Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 24 Jun 2026 16:58:40 -0500 Subject: [PATCH 2/2] fix(opencode): redact provider model secrets --- packages/opencode/src/config/config.ts | 19 +++++++++++++ .../test/server/httpapi-config.test.ts | 27 ++++++++++++++++--- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 400969eab2c1..ab97020f7886 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -128,6 +128,22 @@ export function toPublicInfo(info: Info): Info { options: provider.options ? (redactProviderOptions(provider.options) as typeof provider.options) : undefined, + models: provider.models + ? Object.fromEntries( + Object.entries(provider.models).map(([id, model]) => [ + id, + { + ...model, + options: model.options + ? (redactProviderOptions(model.options) as typeof model.options) + : undefined, + headers: model.headers + ? Object.fromEntries(Object.keys(model.headers).map((key) => [key, redacted])) + : undefined, + }, + ]), + ) + : undefined, }, ]), ) @@ -168,6 +184,9 @@ export function toPublicInfo(info: Info): Info { function redactProviderOptions(value: unknown, key?: string): unknown { const normalized = key?.replaceAll(/[-_]/g, "").toLowerCase() + if (normalized === "headers" && isRecord(value)) { + return Object.fromEntries(Object.keys(value).map((key) => [key, redacted])) + } if ( normalized && (normalized.endsWith("apikey") || diff --git a/packages/opencode/test/server/httpapi-config.test.ts b/packages/opencode/test/server/httpapi-config.test.ts index 5b80fd8972a9..e8fff977e809 100644 --- a/packages/opencode/test/server/httpapi-config.test.ts +++ b/packages/opencode/test/server/httpapi-config.test.ts @@ -71,6 +71,9 @@ describe("config HttpApi", () => { "CANARY_PROVIDER_API_KEY", "CANARY_PROVIDER_CLIENT_SECRET", "CANARY_PROVIDER_NESTED_TOKEN", + "CANARY_PROVIDER_HEADER", + "CANARY_MODEL_API_KEY", + "CANARY_MODEL_HEADER", "CANARY_MCP_ENV", "CANARY_MCP_HEADER", "CANARY_MCP_CLIENT_SECRET", @@ -88,22 +91,31 @@ describe("config HttpApi", () => { apiKey: "{file:secret-0}", clientSecret: "{file:secret-1}", nested: { accessToken: "{file:secret-2}", temperature: 0.5 }, + headers: { "x-custom-auth": "{file:secret-3}" }, baseURL: "https://provider.example.com", }, + models: { + canary: { + name: "Canary Model", + status: "active", + options: { apiKey: "{file:secret-4}", temperature: 0.7 }, + headers: { "x-custom-auth": "{file:secret-5}" }, + }, + }, }, }, mcp: { local: { type: "local", command: ["canary-command"], - environment: { TOKEN: "{file:secret-3}" }, + environment: { TOKEN: "{file:secret-6}" }, 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" }, + headers: { Authorization: "{file:secret-7}" }, + oauth: { clientId: "canary-client", clientSecret: "{file:secret-8}", scope: "read" }, enabled: false, }, }, @@ -132,8 +144,17 @@ describe("config HttpApi", () => { apiKey: "[redacted]", clientSecret: "[redacted]", nested: { accessToken: "[redacted]", temperature: 0.5 }, + headers: { "x-custom-auth": "[redacted]" }, baseURL: "https://provider.example.com", }, + models: { + canary: { + name: "Canary Model", + status: "active", + options: { apiKey: "[redacted]", temperature: 0.7 }, + headers: { "x-custom-auth": "[redacted]" }, + }, + }, }, }, mcp: {