fix: use SHA-256 for container fingerprints instead of DefaultHasher
All checks were successful
Build App / build-macos (push) Successful in 2m23s
Build App / build-windows (push) Successful in 3m17s
Build App / build-linux (push) Successful in 4m30s
Sync Release to GitHub / sync-release (release) Successful in 2s

DefaultHasher is not stable across Rust compiler versions or binary
rebuilds, causing unnecessary container recreations on every app update.
Replace with SHA-256 for deterministic, cross-build-stable fingerprints.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 10:42:06 -08:00
parent 20a07c84f2
commit e07c0e6150
3 changed files with 25 additions and 22 deletions

View File

@@ -4681,6 +4681,7 @@ dependencies = [
"reqwest 0.12.28", "reqwest 0.12.28",
"serde", "serde",
"serde_json", "serde_json",
"sha2",
"tar", "tar",
"tauri", "tauri",
"tauri-build", "tauri-build",

View File

@@ -30,6 +30,7 @@ fern = { version = "0.7", features = ["date-based"] }
tar = "0.4" tar = "0.4"
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
iana-time-zone = "0.1" iana-time-zone = "0.1"
sha2 = "0.10"
[build-dependencies] [build-dependencies]
tauri-build = { version = "2", features = [] } tauri-build = { version = "2", features = [] }

View File

@@ -5,8 +5,7 @@ use bollard::container::{
use bollard::image::{CommitContainerOptions, RemoveImageOptions}; use bollard::image::{CommitContainerOptions, RemoveImageOptions};
use bollard::models::{ContainerSummary, HostConfig, Mount, MountTypeEnum, PortBinding}; use bollard::models::{ContainerSummary, HostConfig, Mount, MountTypeEnum, PortBinding};
use std::collections::HashMap; use std::collections::HashMap;
use std::collections::hash_map::DefaultHasher; use sha2::{Sha256, Digest};
use std::hash::{Hash, Hasher};
use super::client::get_docker; use super::client::get_docker;
use crate::models::{AuthMode, BedrockAuthMethod, ContainerInfo, EnvVar, GlobalAwsSettings, McpServer, McpTransportType, PortMapping, Project, ProjectPath}; use crate::models::{AuthMode, BedrockAuthMethod, ContainerInfo, EnvVar, GlobalAwsSettings, McpServer, McpTransportType, PortMapping, Project, ProjectPath};
@@ -129,20 +128,28 @@ fn merge_claude_instructions(
} }
} }
/// Hash a string with SHA-256 and return the hex digest.
fn sha256_hex(input: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(input.as_bytes());
format!("{:x}", hasher.finalize())
}
/// Compute a fingerprint for the Bedrock configuration so we can detect changes. /// Compute a fingerprint for the Bedrock configuration so we can detect changes.
fn compute_bedrock_fingerprint(project: &Project) -> String { fn compute_bedrock_fingerprint(project: &Project) -> String {
if let Some(ref bedrock) = project.bedrock_config { if let Some(ref bedrock) = project.bedrock_config {
let mut hasher = DefaultHasher::new(); let parts = vec![
format!("{:?}", bedrock.auth_method).hash(&mut hasher); format!("{:?}", bedrock.auth_method),
bedrock.aws_region.hash(&mut hasher); bedrock.aws_region.clone(),
bedrock.aws_access_key_id.hash(&mut hasher); bedrock.aws_access_key_id.as_deref().unwrap_or("").to_string(),
bedrock.aws_secret_access_key.hash(&mut hasher); bedrock.aws_secret_access_key.as_deref().unwrap_or("").to_string(),
bedrock.aws_session_token.hash(&mut hasher); bedrock.aws_session_token.as_deref().unwrap_or("").to_string(),
bedrock.aws_profile.hash(&mut hasher); bedrock.aws_profile.as_deref().unwrap_or("").to_string(),
bedrock.aws_bearer_token.hash(&mut hasher); bedrock.aws_bearer_token.as_deref().unwrap_or("").to_string(),
bedrock.model_id.hash(&mut hasher); bedrock.model_id.as_deref().unwrap_or("").to_string(),
bedrock.disable_prompt_caching.hash(&mut hasher); format!("{}", bedrock.disable_prompt_caching),
format!("{:x}", hasher.finish()) ];
sha256_hex(&parts.join("|"))
} else { } else {
String::new() String::new()
} }
@@ -157,9 +164,7 @@ fn compute_paths_fingerprint(paths: &[ProjectPath]) -> String {
.collect(); .collect();
parts.sort(); parts.sort();
let joined = parts.join(","); let joined = parts.join(",");
let mut hasher = DefaultHasher::new(); sha256_hex(&joined)
joined.hash(&mut hasher);
format!("{:x}", hasher.finish())
} }
/// Compute a fingerprint for port mappings so we can detect changes. /// Compute a fingerprint for port mappings so we can detect changes.
@@ -171,9 +176,7 @@ fn compute_ports_fingerprint(port_mappings: &[PortMapping]) -> String {
.collect(); .collect();
parts.sort(); parts.sort();
let joined = parts.join(","); let joined = parts.join(",");
let mut hasher = DefaultHasher::new(); sha256_hex(&joined)
joined.hash(&mut hasher);
format!("{:x}", hasher.finish())
} }
/// Build the JSON value for MCP servers config to be injected into ~/.claude.json. /// Build the JSON value for MCP servers config to be injected into ~/.claude.json.
@@ -250,9 +253,7 @@ fn compute_mcp_fingerprint(servers: &[McpServer]) -> String {
return String::new(); return String::new();
} }
let json = build_mcp_servers_json(servers); let json = build_mcp_servers_json(servers);
let mut hasher = DefaultHasher::new(); sha256_hex(&json)
json.hash(&mut hasher);
format!("{:x}", hasher.finish())
} }
pub async fn find_existing_container(project: &Project) -> Result<Option<String>, String> { pub async fn find_existing_container(project: &Project) -> Result<Option<String>, String> {