mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-07-09 16:00:52 +00:00
fix(ui): hide environment variable values and mask inputs as password (#284)
## Summary - Display only environment variable names in instance info panel (hide values) - Display only environment variable names in logs view (hide values) - Change environment variable value inputs to password type in settings editor ## Motivation Environment variable values (which may contain sensitive data like API keys, tokens, or secrets) were displayed in plain text across multiple UI surfaces. This change: 1. **Reduces visual exposure** — instance info and logs view now show only variable names 2. **Masks input fields** — the settings editor uses `type="password"` for value inputs, preventing shoulder-surfing and accidental exposure in screenshots or screen recordings --------- Co-authored-by: Pascal André <pascalandr@gmail.com>
This commit is contained in:
parent
cb3d4841e1
commit
7f431aba8b
11 changed files with 126 additions and 13 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { Component, createSignal, For, Show } from "solid-js"
|
||||
import { Plus, Trash2, Key, Globe } from "lucide-solid"
|
||||
import { Plus, Trash2, Key, Globe, Shield, ShieldOff } from "lucide-solid"
|
||||
import { useConfig } from "../stores/preferences"
|
||||
import { useI18n } from "../lib/i18n"
|
||||
|
||||
|
|
@ -14,10 +14,13 @@ const EnvironmentVariablesEditor: Component<EnvironmentVariablesEditorProps> = (
|
|||
addEnvironmentVariable,
|
||||
removeEnvironmentVariable,
|
||||
updateEnvironmentVariables,
|
||||
isSecureEnvVar,
|
||||
toggleSecureEnvVar,
|
||||
} = useConfig()
|
||||
const [envVars, setEnvVars] = createSignal<Record<string, string>>(serverSettings().environmentVariables || {})
|
||||
const [newKey, setNewKey] = createSignal("")
|
||||
const [newValue, setNewValue] = createSignal("")
|
||||
const [newVarSecure, setNewVarSecure] = createSignal(true)
|
||||
|
||||
const entries = () => Object.entries(envVars())
|
||||
|
||||
|
|
@ -27,10 +30,11 @@ const EnvironmentVariablesEditor: Component<EnvironmentVariablesEditorProps> = (
|
|||
|
||||
if (!key) return
|
||||
|
||||
addEnvironmentVariable(key, value)
|
||||
addEnvironmentVariable(key, value, newVarSecure())
|
||||
setEnvVars({ ...envVars(), [key]: value })
|
||||
setNewKey("")
|
||||
setNewValue("")
|
||||
setNewVarSecure(true)
|
||||
}
|
||||
|
||||
function handleRemoveVariable(key: string) {
|
||||
|
|
@ -45,6 +49,10 @@ const EnvironmentVariablesEditor: Component<EnvironmentVariablesEditorProps> = (
|
|||
updateEnvironmentVariables(updated)
|
||||
}
|
||||
|
||||
function handleSecureToggle(key: string) {
|
||||
toggleSecureEnvVar(key)
|
||||
}
|
||||
|
||||
function handleKeyPress(e: KeyboardEvent) {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
|
|
@ -81,14 +89,29 @@ const EnvironmentVariablesEditor: Component<EnvironmentVariablesEditorProps> = (
|
|||
title={t("envEditor.fields.name.readOnlyTitle")}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
type={isSecureEnvVar(key) ? "password" : "text"}
|
||||
value={value}
|
||||
disabled={props.disabled}
|
||||
onInput={(e) => handleUpdateVariable(key, e.currentTarget.value)}
|
||||
class="flex-1 px-2.5 py-1.5 text-sm bg-surface-base border border-base rounded text-primary focus-ring-accent disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
placeholder={t("envEditor.fields.value.placeholder")}
|
||||
autocomplete={isSecureEnvVar(key) ? "new-password" : "off"}
|
||||
spellcheck={isSecureEnvVar(key) ? false : undefined}
|
||||
autocapitalize={isSecureEnvVar(key) ? "off" : undefined}
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => handleSecureToggle(key)}
|
||||
disabled={props.disabled}
|
||||
class={`p-1.5 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
|
||||
isSecureEnvVar(key)
|
||||
? "icon-accent-hover text-accent"
|
||||
: "icon-muted icon-accent-hover"
|
||||
}`}
|
||||
title={isSecureEnvVar(key) ? t("envEditor.fields.secure.enabled") : t("envEditor.fields.secure.disabled")}
|
||||
>
|
||||
{isSecureEnvVar(key) ? <Shield class="w-3.5 h-3.5" /> : <ShieldOff class="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleRemoveVariable(key)}
|
||||
disabled={props.disabled}
|
||||
|
|
@ -117,15 +140,28 @@ const EnvironmentVariablesEditor: Component<EnvironmentVariablesEditorProps> = (
|
|||
placeholder={t("envEditor.fields.name.placeholder")}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
type={newVarSecure() ? "password" : "text"}
|
||||
value={newValue()}
|
||||
onInput={(e) => setNewValue(e.currentTarget.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
disabled={props.disabled}
|
||||
class="flex-1 px-2.5 py-1.5 text-sm bg-surface-base border border-base rounded text-primary focus-ring-accent disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
placeholder={t("envEditor.fields.value.placeholder")}
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setNewVarSecure(!newVarSecure())}
|
||||
disabled={props.disabled}
|
||||
class={`p-1.5 rounded transition-colors disabled:opacity-50 disabled:cursor-not-allowed ${
|
||||
newVarSecure()
|
||||
? "icon-accent-hover text-accent"
|
||||
: "icon-muted icon-accent-hover"
|
||||
}`}
|
||||
title={newVarSecure() ? t("envEditor.fields.secure.enabled") : t("envEditor.fields.secure.disabled")}
|
||||
>
|
||||
{newVarSecure() ? <Shield class="w-3.5 h-3.5" /> : <ShieldOff class="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
<button
|
||||
onClick={handleAddVariable}
|
||||
disabled={props.disabled || !newKey().trim()}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import type { Instance } from "../types/instance"
|
|||
import { useOptionalInstanceMetadataContext } from "../lib/contexts/instance-metadata-context"
|
||||
import InstanceServiceStatus from "./instance-service-status"
|
||||
import { useI18n } from "../lib/i18n"
|
||||
import { useConfig } from "../stores/preferences"
|
||||
import { showConfirmDialog } from "../stores/alerts"
|
||||
import { disposeInstance } from "../stores/instances"
|
||||
import { showToastNotification } from "../lib/notifications"
|
||||
|
|
@ -18,6 +19,7 @@ const log = getLogger("actions")
|
|||
|
||||
const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
||||
const { t } = useI18n()
|
||||
const { isSecureEnvVar } = useConfig()
|
||||
const metadataContext = useOptionalInstanceMetadataContext()
|
||||
const isLoadingMetadata = metadataContext?.isLoading ?? (() => false)
|
||||
const instanceAccessor = metadataContext?.instance ?? (() => props.instance)
|
||||
|
|
@ -156,8 +158,8 @@ const InstanceInfo: Component<InstanceInfoProps> = (props) => {
|
|||
<span class="text-xs font-mono font-medium flex-1 text-primary" title={key}>
|
||||
{key}
|
||||
</span>
|
||||
<span class="text-xs font-mono flex-1 text-secondary" title={value}>
|
||||
{value}
|
||||
<span class="text-xs font-mono flex-1 text-secondary" title={isSecureEnvVar(key) ? t("envEditor.fields.secure.masked") : value}>
|
||||
{isSecureEnvVar(key) ? "***" : value}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { Component, For, createSignal, createEffect, Show, onMount, onCleanup, c
|
|||
import { instances, getInstanceLogs, isInstanceLogStreaming, setInstanceLogStreaming } from "../stores/instances"
|
||||
import { ChevronDown } from "lucide-solid"
|
||||
import { useI18n } from "../lib/i18n"
|
||||
import { useConfig } from "../stores/preferences"
|
||||
|
||||
interface LogsViewProps {
|
||||
instanceId: string
|
||||
|
|
@ -11,6 +12,7 @@ const logsScrollState = new Map<string, { scrollTop: number; autoScroll: boolean
|
|||
|
||||
const LogsView: Component<LogsViewProps> = (props) => {
|
||||
const { t } = useI18n()
|
||||
const { isSecureEnvVar } = useConfig()
|
||||
let scrollRef: HTMLDivElement | undefined
|
||||
const savedState = logsScrollState.get(props.instanceId)
|
||||
const [autoScroll, setAutoScroll] = createSignal(savedState?.autoScroll ?? false)
|
||||
|
|
@ -112,10 +114,12 @@ const LogsView: Component<LogsViewProps> = (props) => {
|
|||
{([key, value]) => (
|
||||
<div class="env-var-item">
|
||||
<span class="env-var-key">{key}</span>
|
||||
<span class="env-var-separator">=</span>
|
||||
<span class="env-var-value" title={value}>
|
||||
{value}
|
||||
</span>
|
||||
<Show when={!isSecureEnvVar(key)}>
|
||||
<span class="env-var-separator">=</span>
|
||||
<span class="env-var-value" title={value}>
|
||||
{value}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ export const settingsMessages = {
|
|||
"envEditor.fields.name.placeholder": "Variable name",
|
||||
"envEditor.fields.name.readOnlyTitle": "Variable name (read-only)",
|
||||
"envEditor.fields.value.placeholder": "Variable value",
|
||||
"envEditor.fields.secure.enabled": "Value masked (secure)",
|
||||
"envEditor.fields.secure.disabled": "Click to mask value",
|
||||
"envEditor.fields.secure.masked": "Value hidden (secure)",
|
||||
"envEditor.actions.remove.title": "Remove variable",
|
||||
"envEditor.actions.add.title": "Add variable",
|
||||
"envEditor.empty": "No environment variables configured. Add variables above to customize the OpenCode environment.",
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ export const settingsMessages = {
|
|||
"envEditor.fields.name.placeholder": "Nombre de la variable",
|
||||
"envEditor.fields.name.readOnlyTitle": "Nombre de la variable (solo lectura)",
|
||||
"envEditor.fields.value.placeholder": "Valor de la variable",
|
||||
"envEditor.fields.secure.enabled": "Valor oculto (seguro)",
|
||||
"envEditor.fields.secure.disabled": "Haz clic para ocultar el valor",
|
||||
"envEditor.fields.secure.masked": "Valor oculto (seguro)",
|
||||
"envEditor.actions.remove.title": "Quitar variable",
|
||||
"envEditor.actions.add.title": "Agregar variable",
|
||||
"envEditor.empty": "No hay variables de entorno configuradas. Agrega variables arriba para personalizar el entorno de OpenCode.",
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ export const settingsMessages = {
|
|||
"envEditor.fields.name.placeholder": "Nom de la variable",
|
||||
"envEditor.fields.name.readOnlyTitle": "Nom de la variable (lecture seule)",
|
||||
"envEditor.fields.value.placeholder": "Valeur de la variable",
|
||||
"envEditor.fields.secure.enabled": "Valeur masquée (sécurisé)",
|
||||
"envEditor.fields.secure.disabled": "Cliquez pour masquer la valeur",
|
||||
"envEditor.fields.secure.masked": "Valeur masquée (sécurisé)",
|
||||
"envEditor.actions.remove.title": "Supprimer la variable",
|
||||
"envEditor.actions.add.title": "Ajouter une variable",
|
||||
"envEditor.empty": "Aucune variable d'environnement configurée. Ajoutez des variables ci-dessus pour personnaliser l'environnement OpenCode.",
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ export const settingsMessages = {
|
|||
"envEditor.fields.name.placeholder": "שם משתנה",
|
||||
"envEditor.fields.name.readOnlyTitle": "שם משתנה (לקריאה בלבד)",
|
||||
"envEditor.fields.value.placeholder": "ערך משתנה",
|
||||
"envEditor.fields.secure.enabled": "הערך מוסתר (מאובטח)",
|
||||
"envEditor.fields.secure.disabled": "לחץ להסתרת הערך",
|
||||
"envEditor.fields.secure.masked": "הערך מוסתר (מאובטח)",
|
||||
"envEditor.actions.remove.title": "הסר משתנה",
|
||||
"envEditor.actions.add.title": "הוסף משתנה",
|
||||
"envEditor.empty": "לא הוגדרו משתני סביבה. הוסף משתנים למעלה להתאמת סביבת OpenCode.",
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ export const settingsMessages = {
|
|||
"envEditor.fields.name.placeholder": "変数名",
|
||||
"envEditor.fields.name.readOnlyTitle": "変数名 (読み取り専用)",
|
||||
"envEditor.fields.value.placeholder": "変数値",
|
||||
"envEditor.fields.secure.enabled": "値は非表示(セキュア)",
|
||||
"envEditor.fields.secure.disabled": "クリックして値を非表示",
|
||||
"envEditor.fields.secure.masked": "値は非表示(セキュア)",
|
||||
"envEditor.actions.remove.title": "変数を削除",
|
||||
"envEditor.actions.add.title": "変数を追加",
|
||||
"envEditor.empty": "環境変数が設定されていません。上で変数を追加して OpenCode 環境をカスタマイズしてください。",
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ export const settingsMessages = {
|
|||
"envEditor.fields.name.placeholder": "Имя переменной",
|
||||
"envEditor.fields.name.readOnlyTitle": "Имя переменной (только чтение)",
|
||||
"envEditor.fields.value.placeholder": "Значение переменной",
|
||||
"envEditor.fields.secure.enabled": "Значение скрыто (безопасно)",
|
||||
"envEditor.fields.secure.disabled": "Нажмите, чтобы скрыть значение",
|
||||
"envEditor.fields.secure.masked": "Значение скрыто (безопасно)",
|
||||
"envEditor.actions.remove.title": "Удалить переменную",
|
||||
"envEditor.actions.add.title": "Добавить переменную",
|
||||
"envEditor.empty": "Переменные окружения не настроены. Добавьте переменные выше, чтобы настроить окружение OpenCode.",
|
||||
|
|
|
|||
|
|
@ -43,6 +43,9 @@ export const settingsMessages = {
|
|||
"envEditor.fields.name.placeholder": "变量名",
|
||||
"envEditor.fields.name.readOnlyTitle": "变量名(只读)",
|
||||
"envEditor.fields.value.placeholder": "变量值",
|
||||
"envEditor.fields.secure.enabled": "值已隐藏(安全)",
|
||||
"envEditor.fields.secure.disabled": "点击隐藏值",
|
||||
"envEditor.fields.secure.masked": "值已隐藏(安全)",
|
||||
"envEditor.actions.remove.title": "移除变量",
|
||||
"envEditor.actions.add.title": "添加变量",
|
||||
"envEditor.empty": "未配置环境变量。在上方添加变量以自定义 OpenCode 环境。",
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ interface ServerConfigBucket {
|
|||
listeningMode?: ListeningMode
|
||||
logLevel?: ServerLogLevel
|
||||
environmentVariables?: Record<string, string>
|
||||
secureEnvVars?: string[]
|
||||
opencodeBinary?: string
|
||||
speech?: Partial<SpeechSettings>
|
||||
}
|
||||
|
|
@ -310,7 +311,7 @@ function normalizeUiState(input?: UiStateBucket | null): NormalizedUiState {
|
|||
|
||||
function normalizeServerConfig(
|
||||
input?: ServerConfigBucket | null,
|
||||
): Required<Pick<ServerConfigBucket, "listeningMode" | "logLevel" | "environmentVariables" | "opencodeBinary">> & { speech: SpeechSettings } {
|
||||
): Required<Pick<ServerConfigBucket, "listeningMode" | "logLevel" | "environmentVariables" | "opencodeBinary" | "secureEnvVars">> & { speech: SpeechSettings } {
|
||||
const source = input ?? {}
|
||||
const listeningMode = source.listeningMode === "all" ? "all" : "local"
|
||||
const logLevel =
|
||||
|
|
@ -319,8 +320,14 @@ function normalizeServerConfig(
|
|||
: "DEBUG"
|
||||
const opencodeBinary = typeof source.opencodeBinary === "string" && source.opencodeBinary.trim() ? source.opencodeBinary : "opencode"
|
||||
const environmentVariables = normalizeRecord(source.environmentVariables)
|
||||
const secureEnvVars = normalizeSecureEnvVars(source.secureEnvVars)
|
||||
const speech = normalizeSpeechSettings(source.speech)
|
||||
return { listeningMode, logLevel, opencodeBinary, environmentVariables, speech }
|
||||
return { listeningMode, logLevel, opencodeBinary, environmentVariables, secureEnvVars, speech }
|
||||
}
|
||||
|
||||
function normalizeSecureEnvVars(input?: unknown): string[] {
|
||||
if (!Array.isArray(input)) return []
|
||||
return input.filter((item): item is string => typeof item === "string" && item.length > 0)
|
||||
}
|
||||
|
||||
function getModelKey(model: { providerId: string; modelId: string }): string {
|
||||
|
|
@ -469,9 +476,29 @@ function updateEnvironmentVariables(envVars: Record<string, string>): void {
|
|||
)
|
||||
}
|
||||
|
||||
function addEnvironmentVariable(key: string, value: string): void {
|
||||
function addEnvironmentVariable(key: string, value: string, secure: boolean = true): void {
|
||||
const current = serverSettings().environmentVariables
|
||||
updateEnvironmentVariables({ ...current, [key]: value })
|
||||
|
||||
const secureList = serverSettings().secureEnvVars
|
||||
const upperKey = key.toUpperCase()
|
||||
const exists = secureList.some((name) => name.toUpperCase() === upperKey)
|
||||
|
||||
if (secure) {
|
||||
if (!exists) {
|
||||
const next = [...secureList, key]
|
||||
void patchConfigOwner("server", { secureEnvVars: next }).catch((error) =>
|
||||
log.error("Failed to add secure env var", error),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if (exists) {
|
||||
const next = secureList.filter((name) => name.toUpperCase() !== upperKey)
|
||||
void patchConfigOwner("server", { secureEnvVars: next }).catch((error) =>
|
||||
log.error("Failed to remove secure env var", error),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function removeEnvironmentVariable(key: string): void {
|
||||
|
|
@ -480,6 +507,23 @@ function removeEnvironmentVariable(key: string): void {
|
|||
updateEnvironmentVariables(rest)
|
||||
}
|
||||
|
||||
function isSecureEnvVar(key: string): boolean {
|
||||
const secureList = serverSettings().secureEnvVars
|
||||
return secureList.some((name) => name.toUpperCase() === key.toUpperCase())
|
||||
}
|
||||
|
||||
function toggleSecureEnvVar(key: string): void {
|
||||
const secureList = serverSettings().secureEnvVars
|
||||
const upperKey = key.toUpperCase()
|
||||
const exists = secureList.some((name) => name.toUpperCase() === upperKey)
|
||||
const next = exists
|
||||
? secureList.filter((name) => name.toUpperCase() !== upperKey)
|
||||
: [...secureList, key]
|
||||
void patchConfigOwner("server", { secureEnvVars: next }).catch((error) =>
|
||||
log.error("Failed to update secure env vars", error),
|
||||
)
|
||||
}
|
||||
|
||||
function updateLastUsedBinary(path: string): void {
|
||||
const target = path && path.trim().length > 0 ? path : "opencode"
|
||||
void patchConfigOwner("server", { opencodeBinary: target }).catch((error) => log.error("Failed to set default binary", error))
|
||||
|
|
@ -724,6 +768,8 @@ interface ConfigContextValue {
|
|||
updateEnvironmentVariables: typeof updateEnvironmentVariables
|
||||
addEnvironmentVariable: typeof addEnvironmentVariable
|
||||
removeEnvironmentVariable: typeof removeEnvironmentVariable
|
||||
isSecureEnvVar: typeof isSecureEnvVar
|
||||
toggleSecureEnvVar: typeof toggleSecureEnvVar
|
||||
updateLastUsedBinary: typeof updateLastUsedBinary
|
||||
updateLogLevel: typeof updateLogLevel
|
||||
updateSpeechSettings: typeof updateSpeechSettings
|
||||
|
|
@ -780,6 +826,8 @@ const configContextValue: ConfigContextValue = {
|
|||
updateEnvironmentVariables,
|
||||
addEnvironmentVariable,
|
||||
removeEnvironmentVariable,
|
||||
isSecureEnvVar,
|
||||
toggleSecureEnvVar,
|
||||
updateLastUsedBinary,
|
||||
updateLogLevel,
|
||||
updateSpeechSettings,
|
||||
|
|
@ -869,6 +917,8 @@ export {
|
|||
updateEnvironmentVariables,
|
||||
addEnvironmentVariable,
|
||||
removeEnvironmentVariable,
|
||||
isSecureEnvVar,
|
||||
toggleSecureEnvVar,
|
||||
updateLastUsedBinary,
|
||||
updateLogLevel,
|
||||
updateSpeechSettings,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue