From 977f5570be148fc7741f3aa03f54cc5cfa4e4145 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Sat, 7 Mar 2026 16:04:09 -0800 Subject: [PATCH] fix(server): redact secret-sourced env vars in run logs by provenance resolveAdapterConfigForRuntime now returns a secretKeys set tracking which env vars came from secret_ref bindings. The onAdapterMeta callback uses this to redact them regardless of key name. Fixes #234 Co-Authored-By: Claude Opus 4.6 --- server/src/routes/agents.ts | 6 +++--- server/src/services/heartbeat.ts | 7 ++++++- server/src/services/secrets.ts | 10 ++++++---- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts index 008d9094..2724a6e2 100644 --- a/server/src/routes/agents.ts +++ b/server/src/routes/agents.ts @@ -211,7 +211,7 @@ export function agentRoutes(db: Db) { adapterConfig: Record, ) { if (adapterType !== "opencode_local") return; - const runtimeConfig = await secretsSvc.resolveAdapterConfigForRuntime(companyId, adapterConfig); + const { config: runtimeConfig } = await secretsSvc.resolveAdapterConfigForRuntime(companyId, adapterConfig); const runtimeEnv = asRecord(runtimeConfig.env) ?? {}; try { await ensureOpenCodeModelConfiguredAndAvailable({ @@ -386,7 +386,7 @@ export function agentRoutes(db: Db) { inputAdapterConfig, { strictMode: strictSecretsMode }, ); - const runtimeAdapterConfig = await secretsSvc.resolveAdapterConfigForRuntime( + const { config: runtimeAdapterConfig } = await secretsSvc.resolveAdapterConfigForRuntime( companyId, normalizedAdapterConfig, ); @@ -1226,7 +1226,7 @@ export function agentRoutes(db: Db) { } const config = asRecord(agent.adapterConfig) ?? {}; - const runtimeConfig = await secretsSvc.resolveAdapterConfigForRuntime(agent.companyId, config); + const { config: runtimeConfig } = await secretsSvc.resolveAdapterConfigForRuntime(agent.companyId, config); const result = await runClaudeLogin({ runId: `claude-login-${randomUUID()}`, agent: { diff --git a/server/src/services/heartbeat.ts b/server/src/services/heartbeat.ts index 0fb575d7..dbba40b2 100644 --- a/server/src/services/heartbeat.ts +++ b/server/src/services/heartbeat.ts @@ -1240,11 +1240,16 @@ export function heartbeatService(db: Db) { const mergedConfig = issueAssigneeOverrides?.adapterConfig ? { ...config, ...issueAssigneeOverrides.adapterConfig } : config; - const resolvedConfig = await secretsSvc.resolveAdapterConfigForRuntime( + const { config: resolvedConfig, secretKeys } = await secretsSvc.resolveAdapterConfigForRuntime( agent.companyId, mergedConfig, ); const onAdapterMeta = async (meta: AdapterInvocationMeta) => { + if (meta.env && secretKeys.size > 0) { + for (const key of secretKeys) { + if (key in meta.env) meta.env[key] = "***REDACTED***"; + } + } await appendRunEvent(currentRun, seq++, { eventType: "adapter.invoke", stream: "system", diff --git a/server/src/services/secrets.ts b/server/src/services/secrets.ts index 8a3595b4..9e65543f 100644 --- a/server/src/services/secrets.ts +++ b/server/src/services/secrets.ts @@ -331,15 +331,16 @@ export function secretService(db: Db) { return resolved; }, - resolveAdapterConfigForRuntime: async (companyId: string, adapterConfig: Record) => { + resolveAdapterConfigForRuntime: async (companyId: string, adapterConfig: Record): Promise<{ config: Record; secretKeys: Set }> => { const resolved = { ...adapterConfig }; + const secretKeys = new Set(); if (!Object.prototype.hasOwnProperty.call(adapterConfig, "env")) { - return resolved; + return { config: resolved, secretKeys }; } const record = asRecord(adapterConfig.env); if (!record) { resolved.env = {}; - return resolved; + return { config: resolved, secretKeys }; } const env: Record = {}; for (const [key, rawBinding] of Object.entries(record)) { @@ -355,10 +356,11 @@ export function secretService(db: Db) { env[key] = binding.value; } else { env[key] = await resolveSecretValue(companyId, binding.secretId, binding.version); + secretKeys.add(key); } } resolved.env = env; - return resolved; + return { config: resolved, secretKeys }; }, }; }