feat(update): core sidecar self-update with version detection#372
feat(update): core sidecar self-update with version detection#372senamakel merged 8 commits intotinyhumansai:mainfrom
Conversation
…dule - Reformatted platform_triple function to improve readability by aligning braces. - Simplified async function calls and error handling in various places for better clarity. - Enhanced logging statements for improved observability during update processes. These changes enhance the maintainability of the codebase while ensuring consistent formatting across the module.
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 10 minutes and 15 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (15)
📝 WalkthroughWalkthroughThis PR implements a comprehensive self-update feature for openhuman-core across the stack. It adds HTTP/semantic versioning dependencies, creates a new core update module in Tauri (checking and downloading updates from GitHub), exposes Tauri commands and TypeScript wrappers, and establishes a new OpenHuman RPC domain with controllers and schemas for update checking and binary staging. Changes
Sequence Diagram(s)sequenceDiagram
participant App as Tauri App
participant Backend as Tauri Backend
participant Core as Core Process
participant GitHub as GitHub API
participant FS as File System
App->>Backend: check_and_update_core()
Backend->>Core: query_core_version (JSON-RPC)
Core-->>Backend: running_version
Backend->>Backend: compare versions
alt Update Available
Backend->>GitHub: fetch latest release
GitHub-->>Backend: release metadata + asset URL
Backend->>GitHub: download binary
GitHub-->>Backend: binary data
Backend->>FS: write to staging directory
Backend->>FS: set executable permissions
Backend->>Backend: acquire restart lock
Backend->>Core: shutdown signal
Core-->>Backend: shutdown complete
Backend->>Backend: wait for port to free
Backend->>Core: restart with new binary
Core-->>Backend: started
else No Update
Backend-->>App: return (no update needed)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Add a periodic update scheduler that checks GitHub Releases for newer core binary versions on a configurable interval (default: 1 hour). Controlled by `[update]` config section with `enabled = true` by default. - New `UpdateConfig` in config schema (enabled, interval_minutes) - Env var overrides: OPENHUMAN_AUTO_UPDATE_ENABLED, OPENHUMAN_AUTO_UPDATE_INTERVAL_MINUTES - Background scheduler spawned at server startup in run_server() - Reports to health registry as "update_checker" component Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (2)
app/src/utils/tauriCommands.ts (1)
214-223: Prefer arrow-function exports for new TS helpers.New helpers are function declarations; convert to arrow functions for consistency with codebase guidelines.
♻️ Suggested refactor
-export async function checkCoreUpdate(): Promise<CoreUpdateStatus | null> { +export const checkCoreUpdate = async (): Promise<CoreUpdateStatus | null> => { if (!isTauri()) { console.debug('[core-update] checkCoreUpdate: skipped — not running in Tauri'); return null; } console.debug('[core-update] checkCoreUpdate: invoking check_core_update'); const result = await invoke<CoreUpdateStatus>('check_core_update'); console.debug('[core-update] checkCoreUpdate: result', result); return result; -} +}; -export async function applyCoreUpdate(): Promise<void> { +export const applyCoreUpdate = async (): Promise<void> => { if (!isTauri()) { console.debug('[core-update] applyCoreUpdate: skipped — not running in Tauri'); return; } console.debug('[core-update] applyCoreUpdate: invoking apply_core_update'); await invoke<void>('apply_core_update'); console.debug('[core-update] applyCoreUpdate: done'); -} +};Applies to lines 214–223 and 229–237. Per coding guidelines for TypeScript files: "Prefer arrow functions over function declarations."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@app/src/utils/tauriCommands.ts` around lines 214 - 223, The exported helper checkCoreUpdate is a function declaration; convert it to an exported arrow-function const (e.g., export const checkCoreUpdate = async (): Promise<CoreUpdateStatus | null> => { ... }) preserving the body, return type, Tauri guard, invoke call and debug logs; do the same style change for the other helper in the nearby block (lines 229–237) so both follow the codebase guideline of using arrow-function exports while keeping names, types and behavior identical.src/openhuman/update/ops.rs (1)
15-16: Consider usingunwrap_or_else()with a fallback error object instead ofunwrap_or_default().While serialization of these simple struct types (
UpdateInfo,UpdateApplyResult) is extremely unlikely to fail, usingunwrap_or_else()with an explicit error response is more transparent. The health module (src/openhuman/health/core.rs:104) demonstrates this pattern better by providing a fallback error object rather than a silent empty value.🛠️ Suggested improvement
- let value = serde_json::to_value(&info).unwrap_or_default(); - RpcOutcome::single_log(value, "update_check completed") + let value = serde_json::to_value(&info).unwrap_or_else(|e| { + serde_json::json!({ "error": format!("serialization failed: {e}") }) + }); + RpcOutcome::single_log(value, "update_check completed") - let value = serde_json::to_value(&result).unwrap_or_default(); - RpcOutcome::single_log(value, "update_apply completed") + let value = serde_json::to_value(&result).unwrap_or_else(|e| { + serde_json::json!({ "error": format!("serialization failed: {e}") }) + }); + RpcOutcome::single_log(value, "update_apply completed")Also applies to: 49-50
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/openhuman/update/ops.rs` around lines 15 - 16, Replace uses of serde_json::to_value(...).unwrap_or_default() in ops.rs (for UpdateInfo/UpdateApplyResult) with unwrap_or_else so you build and return an explicit fallback error Value on serialization failure; locate the calls around RpcOutcome::single_log and change unwrap_or_default to unwrap_or_else(...) that constructs a JSON object/value containing an error message and the serialization error details (similar to the pattern in health/core.rs), then pass that value into RpcOutcome::single_log.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@app/src-tauri/src/core_update.rs`:
- Around line 308-309: ensure_running() is using the CoreProcessHandle::core_bin
captured at construction and will relaunch the old executable instead of the
newly staged binary; update the restart path to recompute or override the target
binary before calling handle.ensure_running(): call default_core_bin() (or
otherwise compute the path derived from dest) and set
CoreProcessHandle::core_bin (or pass the new path into ensure_running/start
methods) so that ensure_running() launches the staged binary rather than
current_exe(); modify CoreProcessHandle (or provide an
ensure_running_with_path/new_start_with_path) to accept an explicit binary path
if mutating core_bin in-place is not desirable.
- Around line 241-244: The early return that checks
is_outdated(&running_version, MINIMUM_CORE_VERSION) incorrectly prevents
apply_core_update from installing newer cores if the running version already
meets MINIMUM_CORE_VERSION; change the logic in the updater function so it does
not short-circuit against MINIMUM_CORE_VERSION — either remove this
MINIMUM_CORE_VERSION check or replace it with a comparison against the intended
target version (or add a force/update_target boolean parameter) so
apply_core_update can proceed when a newer release is requested; ensure to
reference and update the conditional around is_outdated(&running_version,
MINIMUM_CORE_VERSION) and callers like apply_core_update accordingly.
- Around line 188-202: Acquire the restart_lock before calling download_binary()
inside check_and_update_core()/apply_core_update() to prevent concurrent
downloads and writes; generate a unique temp filename instead of
dest.with_extension("tmp") (e.g., append a timestamp or UUID) when staging the
download to avoid simultaneous writes; ensure the core is shut down (call the
existing shutdown path used at line ~296) before moving/staging the new binary,
and only after shutdown perform the atomic rename of the unique temp file into
dest, handling errors with clear messages for rename/set_permissions operations.
In `@app/src-tauri/src/lib.rs`:
- Around line 137-149: check_core_update currently only compares the running
version to core_update::MINIMUM_CORE_VERSION via core_update::is_outdated and
never checks GitHub Releases, so implement a latest-release lookup (e.g., add or
call a function like core_update::query_latest_release or
core_update::get_latest_release_from_github) to fetch the newest published core
version and metadata, then return that latest_version and release info alongside
running_version and minimum_version; also add an update_available boolean
(running < latest_version) while preserving outdated (running < minimum_version)
so callers can distinguish compatibility vs. available update.
In `@src/openhuman/mod.rs`:
- Line 49: Add two new capability catalog entries for the user-facing RPCs
exposed by the update module: "update.check" and "update.apply". In the
capability catalog (about_app::catalog.rs) add entries with ids "update.check"
and "update.apply", human-friendly names and descriptions, and wire them to the
handlers implemented as update::ops::update_check and update::ops::update_apply
so they appear in the app capability list; ensure the entries follow the same
structure/fields as existing capabilities and are exported alongside the other
catalog items.
In `@src/openhuman/update/core.rs`:
- Around line 154-158: The function download_and_stage currently returns an
UpdateApplyResult that uses current_version() (the running process version)
instead of the version of the staged asset; change download_and_stage to
parse/determine the staged release version (e.g., from the release metadata or
staged asset manifest) and set that value as the installed_version in the
returned UpdateApplyResult so callers see the staged release version; update any
other places noted (same logic in the other block around lines 229-239) to
populate UpdateApplyResult.installed_version from the staged asset version
rather than current_version(), referencing download_and_stage and
current_version() to locate and update the return construction.
In `@src/openhuman/update/schemas.rs`:
- Around line 92-109: The update_apply RPC currently accepts unchecked
download_url, asset_name and staging_dir and stages downloaded bytes as
executables; fix this by removing or hardening direct download: in
crate::openhuman::update::rpc::update_apply (and any caller paths) either
restrict download_url to only GitHub release asset URLs (validate host ==
"api.github.com" and verify the asset matches the release manifest) OR require
and verify cryptographic integrity (mandatory SHA256 checksum and a signature
verified with a pinned public key) before writing to disk; additionally disallow
arbitrary staging_dir (accept only predefined safe directories or map a short
token to a safe path) and validate asset_name to be a safe filename (no path
separators) before creating the file or changing permissions.
---
Nitpick comments:
In `@app/src/utils/tauriCommands.ts`:
- Around line 214-223: The exported helper checkCoreUpdate is a function
declaration; convert it to an exported arrow-function const (e.g., export const
checkCoreUpdate = async (): Promise<CoreUpdateStatus | null> => { ... })
preserving the body, return type, Tauri guard, invoke call and debug logs; do
the same style change for the other helper in the nearby block (lines 229–237)
so both follow the codebase guideline of using arrow-function exports while
keeping names, types and behavior identical.
In `@src/openhuman/update/ops.rs`:
- Around line 15-16: Replace uses of
serde_json::to_value(...).unwrap_or_default() in ops.rs (for
UpdateInfo/UpdateApplyResult) with unwrap_or_else so you build and return an
explicit fallback error Value on serialization failure; locate the calls around
RpcOutcome::single_log and change unwrap_or_default to unwrap_or_else(...) that
constructs a JSON object/value containing an error message and the serialization
error details (similar to the pattern in health/core.rs), then pass that value
into RpcOutcome::single_log.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 351cab18-bd4e-496f-ae25-707e1a1761f0
⛔ Files ignored due to path filters (1)
app/src-tauri/Cargo.lockis excluded by!**/*.lock
📒 Files selected for processing (12)
app/src-tauri/Cargo.tomlapp/src-tauri/src/core_process.rsapp/src-tauri/src/core_update.rsapp/src-tauri/src/lib.rsapp/src/utils/tauriCommands.tssrc/core/all.rssrc/openhuman/mod.rssrc/openhuman/update/core.rssrc/openhuman/update/mod.rssrc/openhuman/update/ops.rssrc/openhuman/update/schemas.rssrc/openhuman/update/types.rs
| pub async fn download_and_stage( | ||
| download_url: &str, | ||
| asset_name: &str, | ||
| staging_dir: Option<PathBuf>, | ||
| ) -> Result<UpdateApplyResult, String> { |
There was a problem hiding this comment.
Return the staged release version in UpdateApplyResult.
current_version() is the version of the process doing the download, not the version of the asset you just staged. After a successful update this will still report the old version back to callers, which breaks the installed_version contract.
🛠️ Proposed fix
pub async fn download_and_stage(
download_url: &str,
asset_name: &str,
+ target_version: &str,
staging_dir: Option<PathBuf>,
) -> Result<UpdateApplyResult, String> {
@@
- let installed_version = current_version().to_string();
+ let installed_version = target_version.to_string();Also applies to: 229-239
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/openhuman/update/core.rs` around lines 154 - 158, The function
download_and_stage currently returns an UpdateApplyResult that uses
current_version() (the running process version) instead of the version of the
staged asset; change download_and_stage to parse/determine the staged release
version (e.g., from the release metadata or staged asset manifest) and set that
value as the installed_version in the returned UpdateApplyResult so callers see
the staged release version; update any other places noted (same logic in the
other block around lines 229-239) to populate
UpdateApplyResult.installed_version from the staged asset version rather than
current_version(), referencing download_and_stage and current_version() to
locate and update the return construction.
…king - CoreProcessHandle: add set_core_bin/effective_core_bin so ensure_running launches the newly staged binary instead of the original one - check_and_update_core: add `force` param — auto-check uses MINIMUM_CORE_VERSION, manual apply_core_update uses latest release (force=true) - Acquire restart_lock before download+staging to prevent concurrent updates; shutdown old process before staging; use unique temp filename - check_core_update Tauri command now queries GitHub for latest_version and returns update_available alongside outdated - Harden update_apply RPC: validate download URL is GitHub HTTPS, validate asset_name is safe filename starting with openhuman-core-, ignore caller staging_dir (always use safe default) - download_and_stage accepts target_version so installed_version reflects the staged release, not the running process - Add update.check and update.apply to about_app capability catalog - ops.rs: unwrap_or_default → unwrap_or_else with error context - tauriCommands.ts: convert to arrow-function exports, add latest_version and update_available to CoreUpdateStatus interface Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
updatedomain in Rust core (src/openhuman/update/): Addsopenhuman.update_checkandopenhuman.update_applyRPC methods that query GitHub Releases for neweropenhuman-corebinaries and download/stage them.app/src-tauri/src/core_update.rs): After starting the core sidecar, the app queriescore.version, compares against its minimum expected version, and if outdated: downloads the latest release binary, stages it, kills the old process, and restarts with the new binary. Emitscore-update:statusevents to the frontend.app/src/utils/tauriCommands.ts):checkCoreUpdate()andapplyCoreUpdate()wrappers so the UI can trigger manual update checks and apply updates.How it works
ensure_running()spawns the core sidecarcheck_and_update_core()queriescore.versionvia JSON-RPCrunning_version < MINIMUM_CORE_VERSION(the Tauri app's own package version):tinyhumansai/openhumanopenhuman-core-aarch64-apple-darwin)Test plan
cargo checkpasses for both rootCargo.tomlandapp/src-tauri/Cargo.tomlnpx tsc --noEmitpasses inapp/checkCoreUpdate()returns version info when core is runningapplyCoreUpdate()from frontend triggers download + restart flowcore-update:statusevents are emitted during update🤖 Generated with Claude Code
Summary by CodeRabbit
Release Notes