From 9b2f4fe79f08a1ff490dab24643a827b15fae0a7 Mon Sep 17 00:00:00 2001 From: Josh Knapp Date: Tue, 11 Aug 2026 15:27:19 -0700 Subject: [PATCH] Give the env var its value box back, and stop labelling the secret MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two separate faults, both reachable from one screenshot of the Global Environment Variables editor. The value input was collapsed to a sliver, so a variable looked like it had lost its value. `inputClass` carries `w-full`, and the `w-2/5` on the key input did not beat it — class-attribute order is not what resolves that conflict, stylesheet order is. The key therefore asked for the whole row, and the value input, whose `flex-1` gives it a basis of 0 and only the leftover space, got almost nothing. Widths now live on wrapper divs, where nothing competes with them. The fingerprint that detects custom-env changes was a plaintext `KEY=VALUE` join, and it is written as the `triple-c.custom-env-fingerprint` label. Labels are readable by anything on the host via `docker inspect`, `docker commit` copies them onto the project's snapshot image, and the recreation check logs both sides on a mismatch — so an API token set as a custom variable was published to all three. It is hashed now, exactly as `triple-c.git-token-hash` already was. Empty stays empty, so "nothing configured" still reads as an empty label. Changing the fingerprint format means every project's label mismatches once: expect a single container recreation per project on next start. Co-Authored-By: Claude Opus 5 (1M context) --- app/src-tauri/src/docker/container.rs | 43 +++++++++++++++-- app/src/components/projects/EnvVarsEditor.tsx | 46 +++++++++++-------- 2 files changed, 68 insertions(+), 21 deletions(-) diff --git a/app/src-tauri/src/docker/container.rs b/app/src-tauri/src/docker/container.rs index 2674cf9..f6698f0 100644 --- a/app/src-tauri/src/docker/container.rs +++ b/app/src-tauri/src/docker/container.rs @@ -318,8 +318,19 @@ fn is_reserved_env_key(key: &str) -> bool { || RESERVED_ENV_EXACT.iter().any(|e| upper == *e) } -/// Compute a fingerprint string for the custom environment variables. -/// Sorted alphabetically so order changes do not cause spurious recreation. +/// Compute a fingerprint for the custom environment variables. +/// +/// Sorted alphabetically so order changes do not cause spurious recreation, and +/// **hashed**, because this value is written as the +/// `triple-c.custom-env-fingerprint` label. Labels are readable by anything on +/// the host through `docker inspect`, `docker commit` copies them onto the +/// project's snapshot image, and `container_needs_recreation` logs both sides on +/// a mismatch — so a plaintext `KEY=VALUE` join published every custom +/// variable's *value*, API tokens included, to all three places. Same treatment +/// as `triple-c.git-token-hash`. +/// +/// Empty stays empty rather than becoming the hash of the empty string: an empty +/// label is how every other `triple-c.*` key says "nothing configured". fn compute_env_fingerprint(custom_env_vars: &[EnvVar]) -> String { let mut parts: Vec = Vec::new(); for env_var in custom_env_vars { @@ -330,7 +341,10 @@ fn compute_env_fingerprint(custom_env_vars: &[EnvVar]) -> String { parts.push(format!("{}={}", key, env_var.value)); } parts.sort(); - parts.join(",") + if parts.is_empty() { + return String::new(); + } + sha256_hex(&parts.join(",")) } /// The shared Claude Code OAuth token to inject for this project, paired with @@ -2473,6 +2487,29 @@ mod tests { assert_eq!(fp, ""); } + #[test] + fn the_custom_env_fingerprint_never_carries_the_value() { + // It goes into `triple-c.custom-env-fingerprint`, which `docker inspect` + // hands to anything on the host, `docker commit` copies onto the + // project's snapshot image, and the recreation check logs on a mismatch. + let secret = "33da01c1b320644920c20d6b5e0a1c6b3c3451c2"; + let fp = compute_env_fingerprint(&[EnvVar { + key: "TEA_TOKEN".to_string(), + value: secret.to_string(), + }]); + assert!(!fp.contains(secret), "fingerprint leaked the value: {}", fp); + assert!(!fp.contains("TEA_TOKEN"), "fingerprint leaked the key: {}", fp); + assert_eq!(fp.len(), 64, "expected a sha256 hex digest, got {:?}", fp); + + // It still has to move when the value does, or a rotated token would + // never reach the container. + let rotated = compute_env_fingerprint(&[EnvVar { + key: "TEA_TOKEN".to_string(), + value: "rotated".to_string(), + }]); + assert_ne!(fp, rotated); + } + #[test] fn the_deprecated_small_fast_model_var_is_never_emitted() { let rendered: Vec = aliases(Some("m"), Some("h")) diff --git a/app/src/components/projects/EnvVarsEditor.tsx b/app/src/components/projects/EnvVarsEditor.tsx index 1266283..ce6dde5 100644 --- a/app/src/components/projects/EnvVarsEditor.tsx +++ b/app/src/components/projects/EnvVarsEditor.tsx @@ -43,26 +43,36 @@ export default function EnvVarsEditor({

)} + {/* The row's widths live on wrapper divs, not on the inputs. `inputClass` + carries `w-full`, and a width utility on the input itself does not beat + it — class-attribute order is not what resolves the conflict, stylesheet + order is. Sizing the key input directly left it asking for the whole row + and collapsed the value input, whose `flex-1` basis of 0 gave it only the + leftover space, to an unusable sliver. */} {vars.map((ev, i) => (
- updateVar(i, "key", e.target.value)} - onBlur={() => onSave(vars)} - placeholder="KEY" - aria-label={`Environment variable ${i + 1} name`} - disabled={disabled} - className={`w-2/5 ${monoInputClass}`} - /> - updateVar(i, "value", e.target.value)} - onBlur={() => onSave(vars)} - placeholder="value" - aria-label={`Environment variable ${i + 1} value`} - disabled={disabled} - className={`flex-1 ${monoInputClass}`} - /> +
+ updateVar(i, "key", e.target.value)} + onBlur={() => onSave(vars)} + placeholder="KEY" + aria-label={`Environment variable ${i + 1} name`} + disabled={disabled} + className={monoInputClass} + /> +
+
+ updateVar(i, "value", e.target.value)} + onBlur={() => onSave(vars)} + placeholder="value" + aria-label={`Environment variable ${i + 1} value`} + disabled={disabled} + className={monoInputClass} + /> +