From 977f5570be148fc7741f3aa03f54cc5cfa4e4145 Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Sat, 7 Mar 2026 16:04:09 -0800 Subject: [PATCH 1/2] 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 }; }, }; } From 61966fba1fb9f194988b92a1272006100708497e Mon Sep 17 00:00:00 2001 From: Matt Van Horn Date: Sat, 7 Mar 2026 17:05:55 -0800 Subject: [PATCH 2/2] fix(secrets): add secretKeys tracking to resolveEnvBindings for consistent redaction resolveEnvBindings now returns { env, secretKeys } matching the pattern already used by resolveAdapterConfigForRuntime, so any caller can redact secret-sourced values by provenance rather than key-name heuristics alone. Co-Authored-By: Claude Opus 4.6 --- server/src/services/secrets.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server/src/services/secrets.ts b/server/src/services/secrets.ts index 9e65543f..f18dcb18 100644 --- a/server/src/services/secrets.ts +++ b/server/src/services/secrets.ts @@ -308,10 +308,11 @@ export function secretService(db: Db) { return normalized; }, - resolveEnvBindings: async (companyId: string, envValue: unknown) => { + resolveEnvBindings: async (companyId: string, envValue: unknown): Promise<{ env: Record; secretKeys: Set }> => { const record = asRecord(envValue); - if (!record) return {} as Record; + if (!record) return { env: {} as Record, secretKeys: new Set() }; const resolved: Record = {}; + const secretKeys = new Set(); for (const [key, rawBinding] of Object.entries(record)) { if (!ENV_KEY_RE.test(key)) { @@ -326,9 +327,10 @@ export function secretService(db: Db) { resolved[key] = binding.value; } else { resolved[key] = await resolveSecretValue(companyId, binding.secretId, binding.version); + secretKeys.add(key); } } - return resolved; + return { env: resolved, secretKeys }; }, resolveAdapterConfigForRuntime: async (companyId: string, adapterConfig: Record): Promise<{ config: Record; secretKeys: Set }> => {