diff --git a/src/crates/services/services-core/src/json_store.rs b/src/crates/services/services-core/src/json_store.rs index 2cacc4b385..312fe02e7c 100644 --- a/src/crates/services/services-core/src/json_store.rs +++ b/src/crates/services/services-core/src/json_store.rs @@ -306,6 +306,46 @@ impl JsonFileStore { if let Err(source) = fs::write(&tmp_path, &bytes).await { return Err(JsonFileStoreError::WriteTemp { source }); } + // Session artifacts carry full prompt/output content and must not be + // world-readable on multi-user hosts. The temp file inherits the + // process umask by default; force owner-only (0o600-equivalent) on + // unix before the rename publishes it. Best-effort: a + // set_permissions failure is logged, not fatal — the file is still + // written. + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + let mode = fs::metadata(&tmp_path) + .await + .map(|metadata| metadata.permissions().mode()); + match mode { + Ok(previous_mode) => { + // Preserve the owner read/write/execute bits and clear + // group/other access so the published file is + // 0o600-equivalent regardless of the process umask. + let restricted = previous_mode & 0o700; + if let Err(error) = fs::set_permissions( + &tmp_path, + std::fs::Permissions::from_mode(restricted), + ) + .await + { + warn!( + "Failed to restrict permissions on temporary file {}: {} (continuing; the file may be readable by other local users)", + tmp_path.display(), + error + ); + } + } + Err(error) => { + warn!( + "Failed to read permissions of temporary file {}: {} (continuing)", + tmp_path.display(), + error + ); + } + } + } let replacement = match policy { AtomicWritePolicy::BestEffortReplace => {