Give the env var its value box back, and stop labelling the secret
Build App (Preview) / compute-version (pull_request) Successful in 7s
Build App (Preview) / create-release (pull_request) Successful in 3s
Build App (Preview) / build-macos (pull_request) Successful in 2m56s
Build App (Preview) / build-windows (pull_request) Successful in 5m33s
Build App (Preview) / build-linux (pull_request) Successful in 6m47s
Build App (Preview) / prune-previews (pull_request) Successful in 4s
Build App (Preview) / compute-version (pull_request) Successful in 7s
Build App (Preview) / create-release (pull_request) Successful in 3s
Build App (Preview) / build-macos (pull_request) Successful in 2m56s
Build App (Preview) / build-windows (pull_request) Successful in 5m33s
Build App (Preview) / build-linux (pull_request) Successful in 6m47s
Build App (Preview) / prune-previews (pull_request) Successful in 4s
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) <noreply@anthropic.com>
This commit is contained in:
@@ -318,8 +318,19 @@ fn is_reserved_env_key(key: &str) -> bool {
|
|||||||
|| RESERVED_ENV_EXACT.iter().any(|e| upper == *e)
|
|| RESERVED_ENV_EXACT.iter().any(|e| upper == *e)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Compute a fingerprint string for the custom environment variables.
|
/// Compute a fingerprint for the custom environment variables.
|
||||||
/// Sorted alphabetically so order changes do not cause spurious recreation.
|
///
|
||||||
|
/// 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 {
|
fn compute_env_fingerprint(custom_env_vars: &[EnvVar]) -> String {
|
||||||
let mut parts: Vec<String> = Vec::new();
|
let mut parts: Vec<String> = Vec::new();
|
||||||
for env_var in custom_env_vars {
|
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.push(format!("{}={}", key, env_var.value));
|
||||||
}
|
}
|
||||||
parts.sort();
|
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
|
/// The shared Claude Code OAuth token to inject for this project, paired with
|
||||||
@@ -2473,6 +2487,29 @@ mod tests {
|
|||||||
assert_eq!(fp, "");
|
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]
|
#[test]
|
||||||
fn the_deprecated_small_fast_model_var_is_never_emitted() {
|
fn the_deprecated_small_fast_model_var_is_never_emitted() {
|
||||||
let rendered: Vec<String> = aliases(Some("m"), Some("h"))
|
let rendered: Vec<String> = aliases(Some("m"), Some("h"))
|
||||||
|
|||||||
@@ -43,26 +43,36 @@ export default function EnvVarsEditor({
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 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) => (
|
{vars.map((ev, i) => (
|
||||||
<div key={i} className="flex gap-2 items-center">
|
<div key={i} className="flex gap-2 items-center">
|
||||||
<input
|
<div className="w-2/5 shrink-0">
|
||||||
value={ev.key}
|
<input
|
||||||
onChange={(e) => updateVar(i, "key", e.target.value)}
|
value={ev.key}
|
||||||
onBlur={() => onSave(vars)}
|
onChange={(e) => updateVar(i, "key", e.target.value)}
|
||||||
placeholder="KEY"
|
onBlur={() => onSave(vars)}
|
||||||
aria-label={`Environment variable ${i + 1} name`}
|
placeholder="KEY"
|
||||||
disabled={disabled}
|
aria-label={`Environment variable ${i + 1} name`}
|
||||||
className={`w-2/5 ${monoInputClass}`}
|
disabled={disabled}
|
||||||
/>
|
className={monoInputClass}
|
||||||
<input
|
/>
|
||||||
value={ev.value}
|
</div>
|
||||||
onChange={(e) => updateVar(i, "value", e.target.value)}
|
<div className="flex-1 min-w-0">
|
||||||
onBlur={() => onSave(vars)}
|
<input
|
||||||
placeholder="value"
|
value={ev.value}
|
||||||
aria-label={`Environment variable ${i + 1} value`}
|
onChange={(e) => updateVar(i, "value", e.target.value)}
|
||||||
disabled={disabled}
|
onBlur={() => onSave(vars)}
|
||||||
className={`flex-1 ${monoInputClass}`}
|
placeholder="value"
|
||||||
/>
|
aria-label={`Environment variable ${i + 1} value`}
|
||||||
|
disabled={disabled}
|
||||||
|
className={monoInputClass}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="danger"
|
variant="danger"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
Reference in New Issue
Block a user