Repository navigation
feat: ship CompozyOS desktop app - #336
Conversation
|
Too many files changed for review (584 files, 100 file limit). |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
Disabled knowledge base sources:
WalkthroughThe PR adds a Tauri desktop shell with runtime discovery, provisioning, updates, health monitoring, app control, and release tooling. It also adds desktop configuration and state contracts, drain-status reporting, product-language checks, and broad CompozyOS terminology updates. ChangesDesktop shell and runtime
Release and CLI integration
Repository terminology and configuration
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
React Doctor found no new issues. 🎉 Reviewed by React Doctor for commit |
There was a problem hiding this comment.
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
🟠 Major comments (22)
internal/desktoprelease/channel_config.go-83-90 (1)
83-90: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winValidate the complete Minisign public-key format.
validatePublicKeyaccepts any base64 value whose decoded bytes contain"minisign public key". A malformed key can pass validation and be written into the updater configuration. The desktop updater then cannot authenticate release signatures.Require the documented public-key structure and a valid decoded key packet before writing the configuration. Replace the placeholder payload in
internal/desktoprelease/release_test.go:239-269with a valid test key, and add a rejection case for malformed key material.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/desktoprelease/channel_config.go` around lines 83 - 90, Update validatePublicKey to require the complete documented Minisign public-key structure and successfully parse/validate its decoded key packet, rather than only checking for the marker text. Reject malformed key material before configuration is written. Replace the placeholder payload in the existing release tests with a valid Minisign public key and add a malformed-key rejection case.scripts/publish-desktop-release.sh-69-75 (1)
69-75: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftBind runtime payloads to the signed runtime manifest.
BuildFeedsvalidates runtime hashes when it createsruntime.json. This script receivesruntime_dirseparately and uploads its files without comparing them tofeed_dir/runtime.json. It then verifies only the remote manifest signature.A stale or changed runtime payload can publish successfully and later fail client SHA-256 validation. Validate every runtime file against the signed manifest before upload. Download each declared runtime payload after upload and verify its size and SHA-256 before reporting success.
Based on supplied context,
internal/desktoprelease/manifest.go:27-69binds runtime hashes only during feed construction.Also applies to: 95-115
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/publish-desktop-release.sh` around lines 69 - 75, Update the runtime upload flow around the runtime_path loop to load the signed feed_dir/runtime.json and validate every runtime_dir payload against its declared filename, size, and SHA-256 before uploading. After each upload, download the declared object and recheck its size and SHA-256, failing on missing, extra, or mismatched payloads before reporting success; retain the existing manifest signature verification.scripts/notarize-staple-dmg.sh-19-23 (1)
19-23: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winValidate the embedded app bundle before notarization.
app_pathis only checked beforenotarytool submit "${dmg_path}";scripts/normalize-desktop-artifacts.shalso keeps the macOS app tarball and DMG in separatebundledirectories. Mount the DMG read-only, locate the embedded.app, and compare itsCFBundleShortVersionStringto${release_version}before submission so an old DMG with a different app cannot pass.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/notarize-staple-dmg.sh` around lines 19 - 23, Before the notarytool submit step, mount dmg_path read-only, locate the embedded .app inside the mounted DMG, and validate its CFBundleShortVersionString against release_version using the existing PlistBuddy check. Ensure the mounted volume is cleaned up on success and failure, and keep the existing app_path validation separate.scripts/generate-desktop-update-key.sh-15-17 (1)
15-17: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winRestrict generated private-key permissions.
The script calls
bunx@tauri-apps/cli@2.11.4 signer generate --write-keys "${output}"without setting an access-restrictedumaskor enforcing0600on the generatedTAURI_SIGNING_PRIVATE_KEYfile. With a commonumask 022, the private key can be created readable by other local users. Setumask 077before generation, then set the output file mode to0600.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/generate-desktop-update-key.sh` around lines 15 - 17, Update the key-generation flow in scripts/generate-desktop-update-key.sh to set umask 077 before invoking the signer generate command, then enforce mode 0600 on the generated output file after generation.scripts/normalize-desktop-artifacts.sh-15-17 (1)
15-17: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winMake
copy_onefail whenfindfails.
findruns in Bash process substitution, soset -u pipefaildoes not expose its non-zero exit status to thewhileloop. Iffindreports one match and then exits non-zero for a permission/cleanup error, this function can copy only that partial result. Capture and checkfind’s status before evaluating the match count; use-print0if the fix must preserve paths with special characters.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/normalize-desktop-artifacts.sh` around lines 15 - 17, Update copy_one’s find/match collection so a non-zero find exit status is captured and causes the function to fail before evaluating the match count or copying results. Preserve all successfully discovered matches, and use null-delimited output with -print0 and matching read handling if needed to support paths containing special characters.scripts/verify-desktop-signature.sh-19-26 (1)
19-26: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winMake base64 decoding portable.
On macOS,
base64 --decodeat Line 19 and Line 26 fails because BSDbase64uses-D. The signature verifier cannot validate macOS release artifacts.Select the decoder by platform, or use a portable decoding command.
Proposed fix
+decode_base64() { + if [[ "$(uname -s)" == "Darwin" ]]; then + base64 -D + else + base64 --decode + fi +} + -printf '%s' "${public_key}" | base64 --decode >"${verify_dir}/public.key" +printf '%s' "${public_key}" | decode_base64 >"${verify_dir}/public.key" ... - base64 --decode <"${signature}" >"${verify_dir}/signature.minisig" + decode_base64 <"${signature}" >"${verify_dir}/signature.minisig"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/verify-desktop-signature.sh` around lines 19 - 26, Make the base64 decoding in the verification script portable across GNU and BSD systems by selecting the appropriate decode option or command for the current platform. Update both the public-key decoding before the signature case and the encoded-signature decoding in the default branch, while leaving the existing .minisig copy path unchanged.packages/site/lib/og/templates/blog.tsx-63-63 (1)
63-63: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick winUse the canonical eyebrow implementation for this label.
Line 63 renders
COMPOZYOS BLOGin a styled<span>with inlinetextTransform: "uppercase". Use<Eyebrow>or theeyebrowutility class for the label.As per coding guidelines, every uppercase label in
packages/site/**/*.{tsx,mdx}must use<Eyebrow>or theeyebrowutility class.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/site/lib/og/templates/blog.tsx` at line 63, Replace the styled span rendering “COMPOZYOS BLOG” with the canonical Eyebrow component or eyebrow utility class, removing the inline uppercase styling while preserving the label’s existing accent color.Source: Coding guidelines
desktop/src-tauri/src/app_state.rs-45-67 (1)
45-67: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy liftSerialize
persistso concurrent publishers cannot write a stale record.
publishandupdate_statusrelease thestatemutex before they callpersist. Two threads can therefore interleave: thread A captures state S1, thread B captures S2 and writes S2, then thread A writes S1. The persistedapp_recordand the rendered boot payload then hold an older state than the in-memory state.The path is reachable.
desktop/src-tauri/src/control.rs(lines 95-108) spawns a thread per control connection, and eachapply_updatecall publishes (desktop/src-tauri/src/controller.rs, lines 128-131 and 169-171).desktop/src-tauri/src/shell.rsalso spawns a retry thread that runs the coordinator.app_recordis the cross-language contract that the Go reader consumes throughdesktop/schema/schema.go, so a stale write is observable outside the process.Hold a dedicated ordering lock across the snapshot and the write.
🔒️ Proposed fix using a persistence ordering lock
#[derive(Clone)] pub struct AppStatePublisher { app: AppHandle<Wry>, home: CompozyHome, started_at: DateTime<Utc>, diagnostic: Option<ShellError>, state: Arc<Mutex<PublishedState>>, + persist_order: Arc<Mutex<()>>, } @@ state: Arc::new(Mutex::new(PublishedState { shell: ShellState::Resolving, update: AppUpdateStatus::default(), })), + persist_order: Arc::new(Mutex::new(())), } } pub fn publish(&self, state: ShellState) { + let _order = self + .persist_order + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); let update = { let mut published = self .state .lock() .unwrap_or_else(|poisoned| poisoned.into_inner()); published.shell = state.clone(); published.update.clone() }; self.persist(state, update); } pub fn update_status(&self, mutate: impl FnOnce(&mut AppUpdateStatus)) { + let _order = self + .persist_order + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()); let (shell, update) = {Also applies to: 85-111
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/app_state.rs` around lines 45 - 67, Serialize persistence in both publish and update_status by adding or reusing a dedicated ordering lock held across the state snapshot and persist call. Ensure the lock is acquired before locking state and remains held through self.persist, so concurrent publishers cannot write an older snapshot after a newer one; do not rely on the existing state mutex alone.desktop/src-tauri/src/control.rs-79-83 (1)
79-83: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
ControlServer::startstill only uses the non-atomicsocket_is_livecheck,remove_socket, andbind; it starts accepting beforeset_socket_permissions.Enforce single-instance exclusively with an advisory lock around socket ownership.
tauri-plugin-single-instancehandles a second app launch forwarding arguments, butapp.sockcan still be stolen or created by a concurrent process that starts after the probe and before the binder succeeds. Use a sibling lock file with exclusive file locking from start-up until the control server binds and persists ownership through shutdown.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/control.rs` around lines 79 - 83, Update ControlServer::start to replace the socket_is_live/remove_socket race-prone ownership check with an exclusive advisory lock on a sibling lock file, acquired before probing or binding and retained for the control server’s lifetime through shutdown. Ensure the lock is held until bind and set_socket_permissions complete, and release it only when the server stops; return the existing AlreadyListening error when the lock cannot be acquired.desktop/src-tauri/src/control.rs-110-117 (1)
110-117: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winDo not terminate the accept loop on transient accept errors.
Line 113 breaks the loop for every error kind other than
WouldBlock.acceptreturns transient errors such asInterrupted(EINTR),ConnectionAborted(ECONNABORTED), and file-descriptor exhaustion (EMFILE/ENFILE). After a break, the loop removes the socket file and app control stays unavailable for the rest of the process lifetime.ControlServerhas no restart path, and the only signal is one log line.Continue on recoverable error kinds with a short backoff.
🛡️ Proposed fix
Err(error) if error.kind() == ErrorKind::WouldBlock => { thread::sleep(Duration::from_millis(20)); } + Err(error) + if matches!( + error.kind(), + ErrorKind::Interrupted | ErrorKind::ConnectionAborted + ) => + { + continue; + } + Err(error) if error.raw_os_error() == Some(libc::EMFILE) => { + crate::logging::error(format!( + "accept app control connection deferred: {error}" + )); + thread::sleep(Duration::from_millis(100)); + } Err(error) => { crate::logging::error(format!("accept app control connection: {error}")); break; }If you prefer no new dependency on
libc, match onErrorKind::Otherwith a bounded retry counter instead of theraw_os_errorcheck.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/control.rs` around lines 110 - 117, Update the accept loop’s generic Err(error) branch in ControlServer to continue retrying recoverable transient accept failures, including Interrupted, ConnectionAborted, and descriptor-exhaustion errors, with a short backoff instead of breaking. Preserve logging, and retain termination for non-recoverable errors so the server does not spin indefinitely.desktop/src-tauri/src/controller.rs-471-478 (1)
471-478: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winOffload
shell_controlfrom the main thread during update applies.
shell_controlis a synchronous Tauri 2 command, andapply_updateruns blocking updater work before returning. Move long-running update handling to an async command andtauri::async_runtime::spawn_blocking, or else the boot window cannot process the rendered"Updating ComozyOS"state until the update completes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/controller.rs` around lines 471 - 478, Update shell_control to use an asynchronous Tauri command and offload its synchronous controller.handle call to tauri::async_runtime::spawn_blocking, preserving the trimmed method and params and returning the existing Result<Value, ControlError> outcome after the blocking work completes. Ensure apply_update no longer blocks the boot window’s main thread while the update is running.desktop/src-tauri/src/control.rs-76-91 (1)
76-91: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winNarrow the socket permissions before the listener accepts connections.
bindat line 86 creates the socket file with the process umask, normally0755.listenat line 87 then starts accepting connections.set_socket_permissionsnarrows the mode to0600only at line 91. Between lines 87 and 91 any local user can connect and issueupdate.apply,navigate, ordiagnose.diagnosereturns the app and runtime log paths (desktop/src-tauri/src/controller.rs, lines 310-317), andupdate.applytriggers a runtime mutation.Set the mode before
listen, and create the parent directory with owner-only permissions.🔒️ Proposed fix
if let Some(parent) = path.parent() { fs::create_dir_all(parent).map_err(ControlServerError::Io)?; + set_directory_permissions(parent)?; } @@ listener.bind(&address).map_err(ControlServerError::Io)?; - listener.listen(32).map_err(ControlServerError::Io)?; + set_socket_permissions(path)?; + listener.listen(32).map_err(ControlServerError::Io)?; listener .set_nonblocking(true) .map_err(ControlServerError::Io)?; - set_socket_permissions(path)?;Add the directory helper next to
set_socket_permissions:#[cfg(unix)] fn set_directory_permissions(path: &Path) -> Result<(), ControlServerError> { use std::os::unix::fs::PermissionsExt; fs::set_permissions(path, fs::Permissions::from_mode(0o700)).map_err(ControlServerError::Io) } #[cfg(not(unix))] fn set_directory_permissions(_path: &Path) -> Result<(), ControlServerError> { Ok(()) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/control.rs` around lines 76 - 91, Update the socket setup flow around `set_socket_permissions` so the socket mode is narrowed to owner-only before `listener.listen(32)` runs, then retain the existing nonblocking setup. Add and invoke a `set_directory_permissions` helper after `fs::create_dir_all` to enforce mode 0700 on the parent directory, with the same Unix and non-Unix conditional behavior described in the comment.desktop/src-tauri/src/errors.rs-13-18 (1)
13-18: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick winExtend
NAMED_SECRETto the secret classes the repository guidelines name.The current alternation matches only
claim_token,authorization,api_key, andaccess_token. Values such asrefresh_token=…,client_secret=…,code_verifier=…,code=…, andmcp_token=…pass through unredacted.sanitize_public_textalso feedslogging::error, so an unmatched secret reaches the desktop log file and the public error surface.🔒️ Proposed fix
static NAMED_SECRET: LazyLock<Regex> = LazyLock::new(|| { Regex::new( - r#"(?i)(claim_token|authorization|api[_-]?key|access[_-]?token)\s*[:=]\s*[^\s,;\"']+"#, + r#"(?i)((?:proxy[_-]?)?authorization|claim[_-]?token|mcp[_-]?token|api[_-]?key|(?:access|refresh|id|session|bearer)[_-]?token|client[_-]?secret|code[_-]?(?:verifier|challenge)|password|passphrase|secret)\s*[:=]\s*[^\s,;\"']+"#, ) .expect("named secret regex is valid") });Based on learnings from the coding guidelines: "Never expose raw claim tokens, MCP tokens, OAuth codes, PKCE verifiers, or secret bindings in logs, APIs, errors, messages, SSE, UI, or memory".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/errors.rs` around lines 13 - 18, Extend the NAMED_SECRET regex alternation to match refresh_token, client_secret, code_verifier, code, and mcp_token bindings in addition to the existing secret names, preserving its case-insensitive matching and current key-value boundary behavior so sanitize_public_text redacts all repository-defined secret classes.Source: Coding guidelines
desktop/src-tauri/src/health_monitor.rs-99-110 (1)
99-110: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winKeep the health monitor running on version skew.
health_monitor::spawnis only called fromopen_product, which runs after a compatible handshake. The skew branches show the skew UI and callreturn, so the monitor thread exits permanently. If the operator replaces the runtime with a compatible version, this path no longer re-evaluates compatibility. Re-arm a new monitor on skew, or keep the loop running and perform another compatibility check next tick.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/health_monitor.rs` around lines 99 - 110, Update the skew handling in health_monitor::spawn so Compatibility::SkewOlder and Compatibility::SkewNewer display the skew UI without terminating the monitor. Keep the monitoring loop alive and ensure compatibility is checked again on the next tick, or re-arm an equivalent monitor using the existing spawn flow.desktop/src-tauri/src/home.rs-68-103 (1)
68-103: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winKeep COMPOZY_HOME resolution independent from
env::current_dir().
ResolveHomeDir()andResolveHomePathsFrom()resolveCOMPOZY_HOMEdirectly, even for relative inputs, sodesktop/src-tauri/src/home.rsshould not join relative overrides to the process current directory. The existing"relative/home"case should not resolve both sides the same way if the Go side only returns the relative value.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/home.rs` around lines 68 - 103, Update resolve_with and absolute_expanded so relative COMPOZY_HOME overrides remain relative instead of being joined with current_dir; remove the current_dir-based resolution while preserving tilde expansion and absolute paths. Keep the Rust result aligned with ResolveHomeDir()/ResolveHomePathsFrom(), including the existing relative/home behavior.internal/cli/daemon.go-159-181 (1)
159-181: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winRelease the update lock before starting the foreground runner.
runDaemonForegroundModeacquires update.lock and defers release untilrunner.Run(ctx)returns. In foreground mode this holds the global cross-process runtime mutation lock for the whole daemon lifetime, so another command using the same lock takesErrRuntimeMutationInProgressuntil that foreground session shuts down. For the detached path, the parent releases the lock afterwaitForDaemonStart; hold the lock for the same duration, or release it earlier in both paths.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/cli/daemon.go` around lines 159 - 181, The update lock acquired in runDaemonForegroundMode must be released before starting the foreground runner, rather than being deferred for the daemon’s entire lifetime. Release updateLock after the startup-critical work completes and remove or adjust the deferred release so runner.Run does not retain the global mutation lock; preserve error propagation from lock release.desktop/src-tauri/src/runtime/provision.rs-76-150 (1)
76-150: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winEnsure only one provisioning/update path can hold
provision/stage_update.
stage_updateis called after updatingshell.rs’s runtime updater, so the update path already holdsUpdateLock.provisionis called fromShellCoordinator::provision_runtime, but this happens afterrecover_runtimereleasesUpdateLock, so another invocation or process can mutatehome.update_lock, the shared staging paths, andhome.app_binaryat the same time. Add a caller-heldUpdateLockaround resolve/provision, or a single-instance/provision lock before reachingprovision.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/runtime/provision.rs` around lines 76 - 150, The provisioning flow lacks exclusive coordination with stage_update, allowing concurrent mutations of the update lock, staging paths, and installed binary. Add a caller-held UpdateLock or equivalent single-instance provisioning lock before ShellCoordinator::provision_runtime resolves and invokes provision, while preserving stage_update’s existing lock ownership and ensuring the lock covers the full resolve/provision operation.internal/cli/app_types.go-102-124 (1)
102-124: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winValidate each app JSON shape with its own schema.
validateAppStateJSONuses one root schema forapp.jsonandAppStatusReport. The rootrequiredlist does not constrain either shape: it ignores the record-level required fields (started_at,state), and it ignores the status-level required fields (installed,running,update). Split validation into two definitions: one for the persisted record and one for the status report.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/cli/app_types.go` around lines 102 - 124, Split the shared validation used by validateAppStateJSON into separate schemas for the persisted app.json record and AppStatusReport. Ensure the record schema requires started_at and state, while the status schema requires installed, running, and update, and route each shape through its corresponding definition.internal/cli/app.go-205-218 (1)
205-218: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
appRecordRunningskips the schema-version gate thatresolveAppStatusenforces.
resolveAppStatusininternal/cli/app_types.gorejectsapp.jsonwhenschema_version != appStateSchemaVersionand returnsappStateSchemaUnknownCode.appRecordRunningdecodes onlypidandstarted_at, soapp opentreats a record written by a newer app version as authoritative and sends anavigatecontrol call. The two commands then disagree about the same file.Reuse one liveness reader that validates
schema_versionfirst, and return a typed error for an unknown version.♻️ Proposed direction
-func appRecordRunning(homePaths compozyconfig.HomePaths, deps commandDeps) bool { - raw, err := os.ReadFile(filepath.Join(homePaths.HomeDir, "app.json")) - if err != nil { - return false - } - var identity struct { - PID int `json:"pid"` - StartedAt time.Time `json:"started_at"` - } - if err := json.Unmarshal(raw, &identity); err != nil || identity.PID <= 0 { - return false - } - return deps.processAlive(identity.PID) && deps.processMatchesStartTime(identity.PID, identity.StartedAt) -} +func appRecordRunning( + ctx context.Context, + homePaths compozyconfig.HomePaths, + deps commandDeps, +) (bool, error) { + report, err := resolveAppStatus(ctx, deps, homePaths) + if err != nil { + return false, err + } + return report.Running, nil +}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/cli/app.go` around lines 205 - 218, Update appRecordRunning and the shared app-state reading flow to validate schema_version against appStateSchemaVersion before using PID or StartedAt, matching resolveAppStatus behavior. Reuse a single liveness reader for this validation and return a typed error representing an unknown schema version; ensure app open does not treat unsupported app.json records as authoritative or issue the navigate control call.internal/daemon/drain.go-94-121 (1)
94-121: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winRead the drain state before counting active work.
The function counts active executions and claims first (lines 94-111), then reads
d.DrainState()at line 113. If admission closes between the counts and the state read, the response combines old counts with the new state and can reportSafeToStop: truewhile work admitted after the count is still in flight.desktop/src-tauri/src/runtime/quiesce.rstreats that readout as authoritative and stops the process.Read the state first. Then any count is taken at a moment when admission was already closed, and
SafeToStop: trueis conservative.🔒 Proposed fix
+ state := d.DrainState() activeExecutions := 0 executionsKnown := sessions == nil if promptState, ok := sessions.(sessionPromptStateReader); ok { executionsKnown = true for _, info := range sessions.List() { if info != nil && promptState.IsPrompting(info.ID) { activeExecutions++ } } } activeClaims := 0 if claimCounter != nil { count, err := claimCounter.CountActiveTaskRunClaims(ctx) if err != nil { return contract.DrainStatusResponse{}, fmt.Errorf("daemon: count active task claims: %w", err) } activeClaims = count } - - state := d.DrainState() return composeDrainStatus(🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/daemon/drain.go` around lines 94 - 121, Update the drain-status function to call d.DrainState() before counting active executions and claims, then pass that captured state to composeDrainStatus. Preserve the existing count error handling and response construction while ensuring all work counts are collected after the admission state snapshot.desktop/src-tauri/src/runtime/supervisor.rs-132-171 (1)
132-171: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy liftBound the lock-contention retries;
attemptdoes not increment on these paths.
start_ownedincrementsattemptonly at line 171, after it acquires the lock. Threecontinuestatements run before that point:
- Line 141, after
reclaim_if_stalereturnsOk(true).- Line 157, after
wait_for_competing_startreturnsNoneand the lock file is gone.Each of these repeats the
while attempt < ATTEMPT_BACKOFF.len()body with the sameattemptvalue. If a competing process repeatedly creates and removesapp-start.lock, or repeatedly leaves a stale lock owned by a dead pid, the loop never terminates. TheOk(true)path and the "lock disappeared" path also apply no sleep, so the loop spins without a delay and consumes CPU.Add a separate bounded counter for contention rounds so every path makes progress toward a terminal result.
🔒 Proposed fix to bound contention rounds
pub fn start_owned( &self, binary: &Path, home: &CompozyHome, ) -> Result<StartResult, ShellError> { let lock_path = home.root.join("app-start.lock"); let mut attempt = 0; + let mut contention_rounds = 0usize; + const MAX_CONTENTION_ROUNDS: usize = 16; while attempt < ATTEMPT_BACKOFF.len() { if let Some(identity) = self.readiness.ready(None) { return Ok(StartResult::Attached(identity)); } let lock = match StartLock::acquire(&lock_path) { Ok(lock) => lock, Err(error) if error.kind() == io::ErrorKind::AlreadyExists => { + contention_rounds += 1; + if contention_rounds > MAX_CONTENTION_ROUNDS { + return Err(start_error(home)); + } match StartLock::reclaim_if_stale(&lock_path, self.readiness) { - Ok(true) => continue, + Ok(true) => { + self.delay.sleep(self.poll_interval); + continue; + } Ok(false) => {} Err(error) => { crate::logging::error(format!( "reclaim runtime start lock {}: {error}", lock_path.display() )); return Err(start_error(home)); } } if let Some(identity) = self.wait_for_competing_start(&lock_path) { return Ok(StartResult::Attached(identity)); } if lock_path.exists() { return Err(start_error(home)); } + self.delay.sleep(self.poll_interval); continue; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/runtime/supervisor.rs` around lines 132 - 171, Bound lock-contention retries in start_owned with a separate counter that advances before both continue paths handling reclaim_if_stale Ok(true) and a disappeared lock after wait_for_competing_start. Apply the existing backoff delay for each contention round and return start_error(home) once the contention limit is reached, while preserving the current attached and successful lock-acquisition behavior.desktop/src-tauri/tests/fake_daemon.rs-104-114 (1)
104-114: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winHandle
ECHILDinterminate_fixture.
SystemSpawner::spawn_detachedusessetsid, andSetsid::setsidremoves the caller from its process group, sowaitpid(pid, 0)can fail withECHILDafter the signal is sent. Treat that failure as successful cleanup afterlibc::killreturns0, or otherwise reap this fixture after termination.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/tests/fake_daemon.rs` around lines 104 - 114, Update terminate_fixture so a successful libc::kill is treated as successful cleanup even when waitpid returns ECHILD due to the detached process setup. Preserve failure handling for unsuccessful termination signals, and otherwise continue reaping the fixture when waitpid succeeds.
🟡 Minor comments (13)
desktop/src-tauri/pages/boot.js-96-118 (1)
96-118: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winGuard
runControlagainst a concurrentrenderfrom the host.
renderis called while a control request is in flight.apply_updateindesktop/src-tauri/src/controller.rspublishesShellState::Updating(lines 128-131) before it callsupdater.apply, andAppStatePublisher::persistrenders the boot window on every publish (desktop/src-tauri/src/app_state.rs, lines 106-110).Two effects follow:
configureButtonsetsbutton.disabled = false, removesaria-busy, and rewritestextContentanddataset.label. The pending state is lost, and the user can click again. The secondupdate.applyis rejected by the runtime mutation lock, so the user sees an error instead of progress.- After the in-flight request settles, the
finallyblock writes backlabel, which was captured at line 97 before the re-render. The button then shows a stale label that can disagree withdataset.target.Track a pending flag and skip the label/disabled restore when
renderalready reconfigured the button.🐛 Proposed fix using a pending token
+ let controlGeneration = 0; + const configureButton = (button, options) => { button.hidden = !options.visible; button.disabled = false; button.removeAttribute("aria-busy"); button.classList.toggle("boot__control--primary", options.primary); button.textContent = options.label; button.dataset.label = options.label; }; @@ const runControl = async (button, method, params, pendingLabel) => { const label = button.dataset.label; + const generation = ++controlGeneration; button.style.inlineSize = `${button.getBoundingClientRect().width}px`; button.disabled = true; button.setAttribute("aria-busy", "true"); button.textContent = pendingLabel; setResult(""); try { const response = await invoke(method, params); if (method === "diagnose") { showDiagnostics(response); return; } setResult(method === "retry" ? "Retry accepted." : "Update started."); } catch (error) { setResult(controlError(error), true); } finally { - button.disabled = false; - button.removeAttribute("aria-busy"); - button.textContent = label; - button.style.removeProperty("inline-size"); + if (generation === controlGeneration) { + button.disabled = false; + button.removeAttribute("aria-busy"); + button.textContent = label; + } + button.style.removeProperty("inline-size"); } };Then bump the generation in
renderso an in-flight restore is discarded:const render = payload => { const input = payload !== null && typeof payload === "object" ? payload : {}; + controlGeneration += 1; const state = Object.hasOwn(states, input.state) ? input.state : "error";Also applies to: 120-124
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/pages/boot.js` around lines 96 - 118, Update runControl to track a pending request token or generation and only restore the button’s label, disabled state, aria-busy attribute, and inline size if render has not reconfigured that control while the request was in flight. Increment the corresponding generation in render/configureButton whenever the host re-renders controls, so an in-flight finally block cannot overwrite the current dataset.label or pending UI state.internal/daemon/drain_status_test.go-62-70 (1)
62-70: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAssert the count fields so a field swap fails the test.
The test asserts
SafeToStop,AuthoritativeClaimsSettled, andSessionExecutionSettled. It never assertsActiveSessionExecutionsorActiveTaskClaims. IfcomposeDrainStatusswapped those two response fields, every case would still pass, because the settled flags are computed from the input arguments and not from the response fields.DrainStatusResponseis the shared schema for HTTP, UDS, and CLI (internal/api/contract/drain.go, lines 11-20), so a swap is an observable contract break.Add the two pass-through assertions. Also include the expected value in the failure messages at lines 66 and 69.
As per path instructions: "Verify tests can fail when business logic changes".
💚 Proposed additional assertions
if status.SafeToStop != tc.wantSafe { t.Fatalf("composeDrainStatus() = %#v, want safe_to_stop=%t", status, tc.wantSafe) } + if status.ActiveSessionExecutions != tc.activeExecutions { + t.Fatalf( + "active_session_executions = %d, want %d", + status.ActiveSessionExecutions, + tc.activeExecutions, + ) + } + if status.ActiveTaskClaims != tc.activeClaims { + t.Fatalf("active_task_claims = %d, want %d", status.ActiveTaskClaims, tc.activeClaims) + } + if status.AdmissionClosed != (tc.state == DrainStateDraining) { + t.Fatalf("admission_closed = %#v", status) + } if status.AuthoritativeClaimsSettled != (tc.claimsKnown && tc.activeClaims == 0) { - t.Fatalf("authoritative claims projection = %#v", status) + t.Fatalf( + "authoritative_claims_settled = %t, want %t", + status.AuthoritativeClaimsSettled, + tc.claimsKnown && tc.activeClaims == 0, + ) } if status.SessionExecutionSettled != (tc.executionsKnown && tc.activeExecutions == 0) { - t.Fatalf("session execution projection = %#v", status) + t.Fatalf( + "session_execution_settled = %t, want %t", + status.SessionExecutionSettled, + tc.executionsKnown && tc.activeExecutions == 0, + ) }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/daemon/drain_status_test.go` around lines 62 - 70, Extend the composeDrainStatus test to assert ActiveSessionExecutions equals tc.activeExecutions and ActiveTaskClaims equals tc.activeClaims, so response-field swaps fail. Update the existing AuthoritativeClaimsSettled and SessionExecutionSettled failure messages to include their expected values, while preserving the current settled-flag calculations.Source: Path instructions
internal/api/core/handlers_test.go-4465-4476 (1)
4465-4476: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winThe stub cannot express "draining but not settled", so the settlement flags are untested.
DrainStatusreturnsAuthoritativeClaimsSettled: trueandSessionExecutionSettled: trueunconditionally, and derivesSafeToStopfromdraining. Safety to stop depends on settlement, not on admission closure. Two consequences follow at the assertion sites:
- Lines 4381-4384 assert all four flags are true while draining, but the two settlement flags are constants in the stub, so the assertion cannot fail if the handler stops projecting them.
- Lines 4424-4426 never assert the settlement flags after undrain, so an inverted projection stays undetected.
Make the stub flags configurable and add the drain-but-unsettled case, where
SafeToStopmust be false.💚 Proposed change
func (c *testDrainController) DrainStatus(context.Context) (contract.DrainStatusResponse, error) { state := c.DrainState() draining := state == contract.DrainStateDraining + settled := c.settled.Load() return contract.DrainStatusResponse{ State: state, AdmissionClosed: draining, - AuthoritativeClaimsSettled: true, - SessionExecutionSettled: true, - SafeToStop: draining, + AuthoritativeClaimsSettled: settled, + SessionExecutionSettled: settled, + SafeToStop: draining && settled, }, nil }Add the
settled atomic.Boolfield totestDrainController, set it to true in the existing case, and add a subtest that drains withsettledfalse and assertsSafeToStopis false.As per path instructions: "Verify tests can fail when business logic changes" and "Ensure tests verify behavior outcomes, not just function calls".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/api/core/handlers_test.go` around lines 4465 - 4476, Update testDrainController and its DrainStatus method to use a configurable settled atomic.Bool for both settlement flags, and derive SafeToStop from both draining and settled. Set settled true in the existing settled case, add a draining-but-unsettled subtest with settled false, and assert SafeToStop is false; also assert the settlement flags after undrain.Source: Path instructions
desktop/src-tauri/src/logging.rs-18-20 (1)
18-20: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winThe 512-byte cap also truncates log records.
sanitize_public_texttruncates toMAX_SAFE_MESSAGE_BYTES(512). That bound exists for the public UI surface indesktop/src-tauri/src/errors.rs. Applying it to the log file removes diagnostic detail exactly where full detail helps, and redaction already ran before truncation. Split the two concerns so logs keep the full redacted text.🔧 Proposed change
Expose a redaction-only helper in
desktop/src-tauri/src/errors.rs:/// Redacts secrets and control characters without applying the public-surface length bound. pub fn redact_public_text(input: &str) -> String { /* current body without truncate_utf8 */ } pub fn sanitize_public_text(input: &str) -> String { truncate_utf8(redact_public_text(input).trim(), MAX_SAFE_MESSAGE_BYTES) }Then use it here:
-use crate::errors::sanitize_public_text; +use crate::errors::redact_public_text; pub fn error(message: impl AsRef<str>) { - log::error!("{}", sanitize_public_text(message.as_ref())); + log::error!("{}", redact_public_text(message.as_ref())); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/logging.rs` around lines 18 - 20, Split redaction from public-message truncation in sanitize_public_text: add the exposed redact_public_text helper in errors.rs containing the existing redaction and control-character logic without truncate_utf8, then have sanitize_public_text reuse it before trimming and applying MAX_SAFE_MESSAGE_BYTES. Update logging::error to call redact_public_text so log records retain the full redacted message.desktop/src-tauri/src/links.rs-89-96 (1)
89-96: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick winReject backslash traversal in product routes.
valid_product_pathonly checks/segments, socompozyos://open/..\..\etcpasses andnavigate_pendingsetstarget.set_path(route)with/..\..\etc. Reject segments containing traversal components, and include a case-insensitive normalized traversal check that also covers backslashes.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/links.rs` around lines 89 - 96, Update valid_product_path to reject traversal components separated by either forward or backslashes, including case-insensitive normalized forms, before navigate_pending passes the route to target.set_path. Preserve rejection of ".", "..", and NUL-containing segments while ensuring paths such as backslash-based ..\..\ traversal cannot pass validation.desktop/src-tauri/src/errors.rs-170-184 (1)
170-184: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winThe response-body assertion cannot fail.
bodyis never passed toShellError::new. The message argument is a fixed literal. The assertion!error.safe_message.contains(body)therefore holds regardless of production behavior, so the test cannot detect a regression that embeds a response body. Pass the body through the constructor to make the invariant executable.💚 Proposed fix
let body = "claim_token=compozy_claim_response-secret"; let error = ShellError::new( ShellErrorCode::RuntimeUnhealthy, - "The runtime returned an invalid response.", + format!("The runtime returned an invalid response. {body}"), PathBuf::from("/tmp/compozy.log"), ); assert!(!error.safe_message.contains(body)); - assert_eq!( - error.safe_message, - "The runtime returned an invalid response." - ); + assert!(!error.safe_message.contains("compozy_claim_")); + assert!(error.safe_message.starts_with("The runtime returned an invalid response."));🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/errors.rs` around lines 170 - 184, Update the test should_never_embed_http_response_body so the response body variable is passed into the ShellError construction path, exercising the production behavior that sanitizes or excludes it from safe_message. Preserve the existing assertions that safe_message omits the body and equals the expected generic message.desktop/src-tauri/tests/update_system.rs-464-487 (1)
464-487: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winThe test name claims a case the body does not cover.
The name states the malformed and missing feeds stay "distinct from up to date". The loop asserts only
FeedInvalidandFeedNotFound. No case establishes that an up-to-date feed yields a different, non-Failedevent. The stated invariant is therefore only half enforced. Add the up-to-date case, or rename the test to match the assertions.💚 Proposed addition
#[test] fn should_keep_malformed_and_missing_tauri_feeds_distinct_from_up_to_date() { + // Establish the contrast case first: a valid feed at the current version is not a failure. + let current = app_feed(b"signed app update", b"signed app update"); + let current_directory = tempfile::tempdir().expect("temp directory opens"); + let (_current_app, current_updater) = mock_app_updater( + current.0.origin.join("feed.json").expect("feed URL joins"), + current.1, + current_directory.path().join("app-update-intent.json"), + ); + assert!( + !matches!(current_updater.check(true), AppUpdateEvent::Failed { .. }), + "a valid feed must not report a failure" + ); for (routes, expected) in [Adjust the contrast case to the version the updater treats as current.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/tests/update_system.rs` around lines 464 - 487, Update should_keep_malformed_and_missing_tauri_feeds_distinct_from_up_to_date to include a fixture containing the updater’s current-version feed, and assert that updater.check(true) returns the corresponding non-failure up-to-date event. Keep the existing FeedInvalid and FeedNotFound assertions, using the version value the updater recognizes as current for the contrast case.desktop/src-tauri/src/runtime/artifacts.rs-263-280 (1)
263-280: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winThe IPv6 loopback literal never matches
host_str.
Url::host_strserializes IPv6 hosts with brackets, sohttp://[::1]...returns"[::1]", not"::1". Add the bracketed arm so loopback fixture URLs can pass validation.🐛 Proposed fix
let local_fixture = artifact.url.scheme() == "http" && matches!( artifact.url.host_str(), - Some("localhost" | "127.0.0.1" | "::1") + Some("localhost" | "127.0.0.1" | "[::1]") );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/runtime/artifacts.rs` around lines 263 - 280, Update the local_fixture host match in validate_artifact to accept the bracketed IPv6 loopback value "[::1]" returned by Url::host_str, while preserving the existing localhost and 127.0.0.1 cases.internal/cli/app_platform.go-53-63 (1)
53-63: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winDo not fail discovery when the macOS version read fails.
mdfindalready proved the bundle exists. Ifdefaults readfails, for example whenCFBundleShortVersionStringis absent, the function returns an error and theappcommand reports a failure instead of an installed app.🐛 Proposed fix
- versionOutput, err := run( + versionOutput, versionErr := run( ctx, "defaults", "read", filepath.Join(bundlePath, "Contents", "Info"), "CFBundleShortVersionString", ) - if err != nil { - return appInstallation{}, fmt.Errorf("read macOS app version: %w", err) + if versionErr != nil { + return appInstallation{Installed: true}, nil } return appInstallation{Installed: true, Version: strings.TrimSpace(versionOutput)}, nil🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@internal/cli/app_platform.go` around lines 53 - 63, Update the macOS app discovery flow around the version read in the relevant installation-discovery function so a failure from defaults does not return an error after mdfind has confirmed the bundle exists. Return an installed app with an appropriate empty or unavailable version when reading CFBundleShortVersionString fails, while preserving the trimmed version for successful reads.desktop/src-tauri/src/runtime/mutation.rs-46-61 (1)
46-61: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winDeclare the minimum Rust version for
std::fs::File::try_lock.
desktop/src-tauri/Cargo.tomlonly declaresedition = "2024"and norust-version, while this code usesFile::try_lock(),File::unlock(), andstd::fs::TryLockError. Add arust-versiontoolchain requirement of Rust 1.89 or newer so the MSRV is documented and enforced.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/runtime/mutation.rs` around lines 46 - 61, Declare rust-version = "1.89" or newer in the desktop/src-tauri Cargo.toml package metadata to document and enforce the MSRV required by File::try_lock, File::unlock, and TryLockError usage in acquire.desktop/src-tauri/src/runtime/provision.rs-329-344 (1)
329-344: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winAvoid reporting "0 bytes" for disk-space failures.
map_ioreceivesrequired = 0from thecreate_dir_all,sync_all,rename,sha256_file, andremove_filecall sites (lines 165-182, 235, 253, 311). If those calls fail withENOSPC,shell_errorrenders "The runtime needs 0 bytes of free space." Make the required size optional so the message omits an unknown amount.🐛 Proposed fix
- #[error("runtime staging needs {required} bytes of free space")] - DiskSpace { required: u64 }, + #[error("runtime staging ran out of free space")] + DiskSpace { required: Option<u64> },- ProvisionError::DiskSpace { required } => ShellError::new( - ShellErrorCode::ProvisionDiskSpace, - format!("The runtime needs {required} bytes of free space."), - home.app_log.clone(), - ), + ProvisionError::DiskSpace { required } => ShellError::new( + ShellErrorCode::ProvisionDiskSpace, + match required { + Some(bytes) => format!("The runtime needs {bytes} bytes of free space."), + None => "The runtime installation ran out of free disk space.".to_owned(), + }, + home.app_log.clone(), + ),Then pass
Nonefrom the call sites that do not know the size andSome(size)fromwrite_executable.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/runtime/provision.rs` around lines 329 - 344, Update map_io to accept an optional required byte count and return DiskSpace with that optional value. Pass None from create_dir_all, sync_all, rename, sha256_file, and remove_file call sites where the size is unknown, while passing Some(size) from write_executable; ensure shell_error omits the byte amount when it is unavailable.desktop/src-tauri/src/runtime/supervisor_tests.rs-356-363 (1)
356-363: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick winAssert that
on_quitsends no signal.The test name states that quit never signals the runtime, but the assertions only check the daemon record file. A regression that terminates the child on quit still passes. Add an assertion on the recorded terminations.
💚 Proposed test strengthening
supervisor.on_quit(Some(42)); + assert_eq!(spawner.terminated.load(Ordering::SeqCst), 0); assert_eq!( fs::read(&home.daemon_info).expect("daemon record remains"), b"durable daemon record" );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/runtime/supervisor_tests.rs` around lines 356 - 363, Strengthen the `on_quit` test by asserting that the supervisor’s recorded terminations remain empty after calling `supervisor.on_quit(Some(42))`. Keep the existing daemon-record assertion, and use the test fixture’s existing termination-recording symbol to verify no runtime signal was sent.desktop/src-tauri/src/update/app_update_tauri.rs-148-161 (1)
148-161: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick winDisable legacy Minisign signature support for update payloads.
key.verify(bytes, &signature, true)enables legacy non-prehashed Minisign signatures. This cached payload check only needs the prehashed form, so passfalsehere to keep the accepted signature form minimal.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@desktop/src-tauri/src/update/app_update_tauri.rs` around lines 148 - 161, Update verify_signature to pass false as the final argument to PublicKey::verify, disabling legacy non-prehashed Minisign signatures while preserving prehashed signature verification.
Checkpoint via cy-loop-tasks (iteration 2, phase B mode=tasks). Co-Authored-By: Codex <[email protected]>
Checkpoint via cy-loop-tasks (iteration 4, phase B mode=tasks). Co-Authored-By: Codex <[email protected]>
Checkpoint via cy-loop-tasks (iteration 5, phase B mode=tasks). Hard-cut public product language to CompozyOS, ship desktop documentation and official-skill routing, and enforce the boundary with UT-113. Co-Authored-By: Codex <[email protected]>
Checkpoint via cy-loop-tasks (iteration 6, phase B mode=tasks). Co-Authored-By: Codex <[email protected]>
Checkpoint via cy-loop-tasks (iteration 7, phase B mode=tasks). Planning-only QA checkpoint: 13 APP scenarios, five desktop journeys, and 17 isolated per-platform charters. No QA lab or product process was started. Co-Authored-By: Codex <[email protected]>
Checkpoint via cy-loop-tasks (iteration 10, phase D review round 1). Co-Authored-By: Codex <[email protected]>
6adb2e8 to
a5a8f7e
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
a5a8f7e to
8e121c9
Compare
Summary
This PR delivers the CompozyOS desktop app workstream end to end across the native shell, runtime contracts, agent-facing CLI, update system, release pipeline, public product language, documentation, and release-grade QA tracking.
The desktop app is a thin Tauri shell over the real CompozyOS daemon. It attaches to an existing daemon when possible, starts or provisions an app-owned runtime when needed, keeps native links and window behavior deterministic, and exposes the same lifecycle to agents through structured
compozy appcommands.What changed
Native desktop shell
Agent and runtime surfaces
compozy appstatus, open, retry, diagnose, quit, and update controls with public-safe errors and a bounded control-socket protocol.[app]configuration lifecycle support, including shared Go/Rust duration grammar, validation corpus, canonical defaults, and config tool-surface registration.Update and release integrity
Product language, site, and docs
compozyas the binary/command name.Deep review
One full, spec-aware deep-review round ran against a frozen diff using Codex
gpt-5.6-terrawithxhighreasoning.Verification
make gate-full: PASS, current fingerprint4345373d98dda62105dcdab96e049679fac86bf7..cache/gate/logs/full-1786369113.log(finished 2026-08-10T13:40:30Z).cargo check --all-targetspassed.-race.make verify.teardown.jsonrecordsclean: truewith zero surviving processes.QA verdict
The code and automated gates are green, but the release verdict remains BLOCKED — NO SHIP.
blocked-verify, 0 untested/fail.The canonical report is
docs/qa/reports/2026-08-10-desktop-app-release.md.Compozy Impact Audit
compozy__*tool IDs, toolsets, descriptors, schemas, digests, risk flags, or capability gates changed. The agent-facing addition is the structured CLI/control-socketcompozy appsurface.config.tomllifecycle.COMPOZY_HOME. No workspace/session/agent datum orworkspace_idpropagation path changed across CLI, HTTP, UDS, core, store, web, SSE, cache, or events.Release follow-through
Before shipping a desktop release, run the blocked Windows, Linux, signed-update, native visual, and three-webview SSE charters recorded in the QA report. The implementation intentionally fails closed until that evidence exists.
Summary by CodeRabbit
New Features
Documentation & Style