Compare commits
2 commits
dev
...
redact-con
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d3da6b8e27 | ||
|
|
c44ef334e4 |
4 changed files with 200 additions and 2 deletions
|
|
@ -114,6 +114,96 @@ type Info = ConfigV1.Info & {
|
||||||
plugin_origins?: ConfigPlugin.Origin[]
|
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,
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
: 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 === "headers" && isRecord(value)) {
|
||||||
|
return Object.fromEntries(Object.keys(value).map((key) => [key, redacted]))
|
||||||
|
}
|
||||||
|
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 = {
|
type State = {
|
||||||
config: Info
|
config: Info
|
||||||
directories: string[]
|
directories: string[]
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export const configHandlers = HttpApiBuilder.group(InstanceHttpApi, "config", (h
|
||||||
const configSvc = yield* Config.Service
|
const configSvc = yield* Config.Service
|
||||||
|
|
||||||
const get = Effect.fn("ConfigHttpApi.get")(function* () {
|
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) {
|
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* () {
|
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) {
|
const configUpdate = Effect.fn("GlobalHttpApi.configUpdate")(function* (ctx) {
|
||||||
|
|
|
||||||
|
|
@ -64,6 +64,114 @@ 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_PROVIDER_HEADER",
|
||||||
|
"CANARY_MODEL_API_KEY",
|
||||||
|
"CANARY_MODEL_HEADER",
|
||||||
|
"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 },
|
||||||
|
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-6}" },
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
remote: {
|
||||||
|
type: "remote",
|
||||||
|
url: "https://mcp.example.com",
|
||||||
|
headers: { Authorization: "{file:secret-7}" },
|
||||||
|
oauth: { clientId: "canary-client", clientSecret: "{file:secret-8}", 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 },
|
||||||
|
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: {
|
||||||
|
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(
|
it.live(
|
||||||
"serves config with active provider model status",
|
"serves config with active provider model status",
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue