Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci-quality-slice.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ jobs:
exit 1
fi

- uses: taiki-e/install-action@3d23c1bbdafe696dfccad2664945a04f47d03dc3 # just
- name: Check console print drift
run: just no-console-print

rust_fmt:
name: Rust format
needs: runner_policy
Expand Down
7 changes: 7 additions & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,7 @@ ci-validate:
python3 -m unittest discover -s scripts/tests -p 'test_*.py'
just ci-crate-lists
just check-release
just no-console-print
just publish-crates

# Run CI/workspace crate-list consistency checks.
Expand All @@ -481,6 +482,12 @@ ci-crate-lists:
publish-crates:
just with-lld cargo run -p xtask -- repo-consistency publish-crates

# Ratchet on println!/eprintln!/print!/eprint! in Rust files under crates/
# (including crate test targets; build.rs is skipped): every occurrence must be
# explicitly listed in tools/xtask/data/console_print_allowlist.json.
no-console-print:
cargo run -p xtask -- repo-consistency no-console-print

# Shellcheck the explicitly supplied changed shell scripts.
ci-shellcheck *scripts:
shellcheck {{ scripts }}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
use super::*;
use crate::logging::{
LoggingService, OpenAiLifecycleAttachment, PersistSink, RawMeshLifecycleOwners,
Clock, LoggingService, OpenAiLifecycleAttachment, PersistSink, RawMeshLifecycleOwners,
RawMeshRequestLifecycle, RequestSummaryEntry,
};
use crate::network::target_health::TargetHealthOutcome;
use anyhow::Result;
use mesh_llm_events::logging::proxy::ProxyRecord;
use mesh_llm_events::logging::replay::ReplayChannel;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering as AtomicOrdering};
use std::sync::{Arc, Mutex};

/// Deterministic counter clock for tests that assert on serialized record
/// contents. `SystemClock` stamps nanosecond-precision wall-clock time, and a
/// redaction assertion checking for bare digit substrings (e.g. a port
/// fixture) can collide with those random nanosecond digits; this clock keeps
/// timestamps fixed-format and predictable so such assertions can't flake.
#[derive(Default)]
struct DeterministicClock {
counter: AtomicU64,
}

impl Clock for DeterministicClock {
fn now(&self) -> String {
let n = self.counter.fetch_add(1, AtomicOrdering::Relaxed);
format!("2025-01-01T00:00:00.{n:09}Z")
}
}

#[derive(Default)]
struct TransportProxySink {
proxy_records: Mutex<Vec<ProxyRecord>>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ async fn transport_attempt_records_reuse_lifecycle_ids_and_keep_one_parent_termi
let service = Arc::new(LoggingService::new(
Default::default(),
Arc::clone(&sink) as Arc<dyn PersistSink>,
Box::new(crate::logging::SystemClock),
Box::new(DeterministicClock::default()),
));
let parent = RawMeshRequestLifecycle::register(
Arc::clone(&service),
Expand Down Expand Up @@ -249,7 +249,21 @@ async fn transport_attempt_records_reuse_lifecycle_ids_and_keep_one_parent_termi
]
);
for record in records {
let serialized = serde_json::to_string(&record).expect("serialize bounded record");
// attempt_id/request_id are random UUIDs and the timestamps are clock output,
// so they carry arbitrary hex and digits that collide with short numeric
// tokens like the port below. Scan every other field — including any added
// later — for leaked request data.
let mut scanned = serde_json::to_value(&record).expect("serialize bounded record");
let fields = scanned
.as_object_mut()
.expect("proxy record serializes to a JSON object");
for generated in ["attempt_id", "request_id", "started_at", "completed_at"] {
assert!(
fields.remove(generated).is_some(),
"expected generated field {generated} on the bounded record"
);
}
let serialized = serde_json::to_string(&scanned).expect("serialize scanned fields");
for forbidden in [
"9337",
"peer-id",
Expand Down
11 changes: 9 additions & 2 deletions crates/mesh-llm-host-runtime/src/runtime/run_auto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -277,8 +277,15 @@ pub(super) async fn run_runtime_cli(
.await?;

// Finish the release check before startup continues.
if !checked_updates && !options.command_is_update && !options.command_uses_machine_output {
autoupdate::check_for_update(crate::BUILD_VERSION).await;
if !checked_updates
&& !options.command_is_update
&& !options.command_uses_machine_output
&& let Some(notice) = autoupdate::check_for_update(crate::BUILD_VERSION).await
{
let _ = emit_event(OutputEvent::Info {
message: notice.message,
context: None,
});
}

let mut config = plugin::load_config(options.config.as_deref())?;
Expand Down
156 changes: 109 additions & 47 deletions crates/mesh-llm-system/src/autoupdate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,57 +46,83 @@ pub struct UpdateCommandOptions<'a> {
pub current_version: &'static str,
}

pub async fn check_for_update(current_version: &str) {
enum NoticeGuidance {
RunUpdateCommand,
#[cfg(not(windows))]
ReinstallScript,
#[cfg(windows)]
DownloadReleases,
}

/// A newer-version notice for callers to surface through their own output
/// facility; this crate performs no console I/O of its own.
#[derive(Clone, Debug)]
pub struct UpdateNotice {
pub message: String,
}

fn notice_message(current_version: &str, latest_version: &str, guidance: NoticeGuidance) -> String {
match guidance {
NoticeGuidance::RunUpdateCommand => format!(
"✨ New version: v{current_version} -> v{latest_version}. Run 'mesh-llm update'."
),
#[cfg(not(windows))]
NoticeGuidance::ReinstallScript => format!(
"✨ New version: v{current_version} -> v{latest_version}. Reinstall with: curl -fsSL {} | bash",
release_fetch::INSTALL_SCRIPT_URL
),
#[cfg(windows)]
NoticeGuidance::DownloadReleases => format!(
"✨ New version: v{current_version} -> v{latest_version}. Download from {RELEASES_URL}"
),
}
}

/// Checks for a newer release and returns a notice when one is available.
///
/// Returns `None` when the platform has no release assets, no release could be
/// fetched, or the local version is not older than the latest release. Callers
/// are responsible for presenting [`UpdateNotice::message`] through their own
/// output facility.
pub async fn check_for_update(current_version: &str) -> Option<UpdateNotice> {
if !platform_has_release_assets() {
return;
return None;
}
if let Some(release) = latest_release_info().await {
if !version_newer(&release.version, current_version) {
return;
let release = latest_release_info().await?;
if !version_newer(&release.version, current_version) {
return None;
}
// Determine whether this is a bundle install and, if so, whether the
// specific installed flavor's asset is present in the new release.
let bundle_asset = std::env::current_exe().ok().and_then(|exe| {
let (_, flavor) = bundle_install_dir(&exe, None)?;
current_release_target(flavor).and_then(|target| {
resolve_release_asset_name(&release, target, ReleaseAssetPreference::StableFirst)
})
});
let has_matching_bundle_asset = bundle_asset
.as_ref()
.is_some_and(|asset| release_fetch::release_has_asset(&release, asset));
let guidance = if has_matching_bundle_asset {
NoticeGuidance::RunUpdateCommand
} else {
// Either not a bundle install, or the installed flavor's asset is not
// published in the new release — fall back to generic guidance.
if !release_has_any_platform_asset(&release, std::env::consts::OS, std::env::consts::ARCH) {
return None;
}
// Determine whether this is a bundle install and, if so, whether the
// specific installed flavor's asset is present in the new release.
let bundle_asset = std::env::current_exe().ok().and_then(|exe| {
let (_, flavor) = bundle_install_dir(&exe, None)?;
current_release_target(flavor).and_then(|target| {
resolve_release_asset_name(&release, target, ReleaseAssetPreference::StableFirst)
})
});
match bundle_asset {
Some(ref asset) if release_fetch::release_has_asset(&release, asset) => {
eprintln!(
"✨ New version: v{current_version} -> v{}. Run 'mesh-llm update'.",
release.version
);
}
_ => {
// Either not a bundle install, or the installed flavor's asset
// is not published in the new release — fall back to generic guidance.
#[cfg(not(windows))]
if release_has_any_platform_asset(
&release,
std::env::consts::OS,
std::env::consts::ARCH,
) {
eprintln!(
"✨ New version: v{current_version} -> v{}. Reinstall with: curl -fsSL {INSTALL_SCRIPT_URL} | bash",
release.version
);
}
#[cfg(windows)]
if release_has_any_platform_asset(
&release,
std::env::consts::OS,
std::env::consts::ARCH,
) {
eprintln!(
"✨ New version: v{current_version} -> v{}. Download from {RELEASES_URL}",
release.version
);
}
}
#[cfg(not(windows))]
{
NoticeGuidance::ReinstallScript
}
}
#[cfg(windows)]
{
NoticeGuidance::DownloadReleases
}
};
Some(UpdateNotice {
message: notice_message(current_version, &release.version, guidance),
})
}

pub async fn maybe_auto_update(options: AutoUpdateOptions) -> Result<bool> {
Expand Down Expand Up @@ -429,4 +455,40 @@ mod tests {

let _ = std::fs::remove_dir_all(dir);
}

#[test]
fn test_notice_message_run_update_command() {
assert_eq!(
notice_message("0.76.0-rc3", "0.99.0", NoticeGuidance::RunUpdateCommand),
"✨ New version: v0.76.0-rc3 -> v0.99.0. Run 'mesh-llm update'."
);
}

#[cfg(not(windows))]
#[test]
fn test_notice_message_reinstall_script() {
let install_script_url = release_fetch::INSTALL_SCRIPT_URL;
assert_eq!(
notice_message("0.75.2", "1.4.0", NoticeGuidance::ReinstallScript),
format!(
"✨ New version: v0.75.2 -> v1.4.0. Reinstall with: curl -fsSL {install_script_url} | bash"
)
);
}

#[cfg(windows)]
#[test]
fn test_notice_message_download_releases() {
assert_eq!(
notice_message("0.75.2", "1.4.0", NoticeGuidance::DownloadReleases),
format!("✨ New version: v0.75.2 -> v1.4.0. Download from {RELEASES_URL}")
);
}

#[test]
fn test_update_notice_carries_complete_user_facing_text() {
let message = notice_message("0.1.0", "9.9.9", NoticeGuidance::RunUpdateCommand);
let UpdateNotice { message: carried } = UpdateNotice { message };
assert!(carried.starts_with("✨ New version: v0.1.0 -> v9.9.9."));
}
}
Loading
Loading