Skip to content

feat(update): route install through the recovery journal gate (W3-02e) - #411

Merged
mergify[bot] merged 2 commits into
mainfrom
claude/w3-02e-install-command
Oct 5, 2026
Merged

mergify[bot] merged 2 commits into
mainfrom
claude/w3-02e-install-command

Conversation

@Cuarroc

@Cuarroc Cuarroc commented Oct 4, 2026 •

Copy link
Copy Markdown
Owner

Der Update-Befehl startet den Installer jetzt nur noch über das Wiederherstellungs-Journal. Fehlt der Nachweis für geleerte Sitzungen, gesperrte Schreibzugriffe oder ein geprüftes Backup, kommt der Installer nie zum Zug. Das echte Herunterladen und Installieren kann nur am PC geprüft werden.

Report

What changed

  • delivery_recovery/install.rs (new): require_install_evidence re-observes write block, session freeze, empty registry and journal phase BackupVerified; install_through_journal runs update.install only inside RecoveryDriver::step (after begin_install persisted Installing), once.
  • pty.rs: install_in_maintenance (requires the maintenance freeze, shares the latch with install_when_idle), is_maintenance_active. install_when_idle is now test-only (allow(dead_code)).
  • main.rs install_update_when_idle: opens update-recovery.json, checks evidence before the download, installs via the gate.
  • Follow-ups from W3-02d: installer adapter (exit/UAC/sharing classification, no retry) #348: F1 comment at delivery_recovery/installer.rs:121 now states the real lifetime (until the installer exits). F2 RejectionReason::Swapped/HashMismatch carry expected and actual SHA, which reach the run_installer error (new test).
  • Note: src-tauri/src/installer.rs does not exist; the adapter is delivery_recovery/installer.rs.

Evidence (base origin/main a4a1b95; head f97a2ee; diff 281+/17-)

  • bash scripts/ci/gates.sh lane prepush -> all gates green (rust-suite, clippy, fe-test, red-first-plan), log /tmp/w3-02e-prepush.log; push hook passed; git ls-remote = f97a2ee.
  • bash scripts/ci/red-first.sh --plan -> 0 (3 specs).
  • Review worktree: bash scripts/ci/gates.sh lane prepush -> exit 1 at typecheck because node_modules was absent in the newly created worktree; no candidate test failed.
  • After npm ci: bash scripts/ci/gates.sh --from typecheck lane prepush -> exit 0 (typecheck, lint, 485 frontend tests, 585 HQ/script tests, clippy, 1,838 Rust tests; 17 skipped).
  • Tests: production_install_never_reaches_the_installer_without_evidence, production_install_persists_installing_then_runs_the_installer_once, swapped_installer_error_names_expected_and_actual_digest.
  • Gate order was W3-02c, W3-02d and state-2-set-budget-null all merged in origin/main.

Open point (behavior): nothing yet produces the journal with drain proof and backup (update-recovery.json), so until that producer lands (follow-up package) the command refuses with a named reason. That is the intended fail-closed result of this cut, but updates in a shipped build stay blocked until then.

Architecture drift: The review found that the new InstallOnly adapter discards the existing RecoveryEffects::install(candidate, staged_manifest) identity contract instead of using it. This is recorded as C1 below and remains open.

NICHT ABGEDECKT

  • Windows half (#[cfg(windows)] incl. ProcessLauncher, the F1 comment's code); merge-queue Windows lane.
  • Real download/installer, Tauri updater, GUI: PC only.
  • Journal producer (drain/backup) not part of this PR.

Review disposition

Round 1, candidate f97a2ee93f4ee56d041875876899b91c6052c802. Author family: Claude. Reviewers: Codex (this review session; the served model identifier is not exposed as runtime evidence) and Ollama Cloud glm-5.2:cloud (reported model glm-5.2, transport exit 0, 2026-10-04T23:59:07Z). FINDEN and GEGENPRÜFEN were performed against origin/main...HEAD; no style-only findings are recorded.

  • C1 — Codex — high — src-tauri/src/delivery_recovery/install.rs:73, existing contract src-tauri/src/delivery_recovery/driver.rs:77-86: deferred to release blocker W3-02h. InstallOnly::install discards both candidate and staged_manifest; main.rs:1213 then installs the separately downloaded updater bytes. Therefore a journal for candidate A can authorize the signed updater object for candidate B. Signature verification proves publisher authenticity, while the journal CAS proves only the phase; neither binds the installed bytes/version to signed_artifact_sha256 and candidate_version. Countercheck: no later comparison exists, and the journal's validation/promotion path assumes the ignored identity. Decision: open, not safely fixable in this package → orchestrator decision 05.10. (advisor Fable 5.1): deferred, resolved for this package as release-blocker follow-up W3-02h — the candidate is already 298/300 changed lines; binding version and bytes to the journal requires a red regression test plus code and must be split or the package recut.
  • C2 — Codex — high — src-tauri/src/main.rs:1209-1213, conflicting path src-tauri/src/main.rs:1153-1158: deferred to release blocker W3-02i. The install gate receives Store::is_maintenance_active() as a boolean snapshot. Concurrent leave_maintenance can roll back the SQLite write lock after that snapshot and before/during update.install; the PTY latch serializes only the session registry and does not hold the Store maintenance lock. Countercheck: the post-download live check still has the same check/use window, and no shared command gate or Store lease spans the installer. Decision: open, not safely fixable in this package → orchestrator decision 05.10. (advisor Fable 5.1): deferred, resolved for this package as release-blocker follow-up W3-02i — a shared maintenance lease/gate and a red concurrency regression test exceed the remaining two-line package budget and touch a safety seam.
  • F1 — GLM 5.2 — low — src-tauri/src/pty.rs:693-696: rejected. This requested a direct test of the defensive (true, false) branch but showed no incorrect behavior. The production precheck rejects the ordinary path; the second check under the PTY registry lock is deliberately the race-closing defense. C2 records the separate Store race.
  • F2 — GLM 5.2 — low — src-tauri/src/main.rs:1186-1203: rejected. The pre-download journal check is an early refusal, not the authorization point. install_through_journal checks again and DurableJournal::compare_and_write refuses a stale revision before invoking the effect; leave_maintenance does not mutate the journal.
  • F3 — GLM 5.2 — low — src-tauri/src/delivery_recovery/install.rs:58: rejected. RecoveryError has a complete fmt::Display implementation at src-tauri/src/delivery_recovery.rs:947-975; Effect(String) preserves the adapter reason. The claimed possible missing/minimal display path does not exist.

No code was changed during review, so no delta review was run. A second round is required only after fixes for C1/C2 and must review only that fix delta.

Reviewer verdict (round 1) was BLOCKIERT: C1 binds no installed candidate identity to the journal, and C2 allows the Store write block to be released concurrently with installation.

Orchestrator decision (05.10.)

Merged with C1/C2 deferred, on advice of Fable 5.1 (GPT-6 Astra unavailable: Codex weekly limit). Reason: origin/main installs with no journal, no backup evidence and no store write block (install_update_when_idle, main.rs ~:1182-1200), so this gate is strictly fail-closed relative to main; C1/C2 are incompleteness of the new gate, not regressions. Known consequence: until a journal producer exists, in-app self-update always refuses. Release blockers before the v1.5.0-beta tag, serial in lane mn: W3-02g journal producer, W3-02h (= C1, bind digest/version), W3-02i (= C2, maintenance lease).

🤖 Generated with Claude Code

Cuarroc and others added 2 commits October 5, 2026 01:26
install_update_when_idle now opens the update journal, re-observes the
session freeze, write block and drain, and runs update.install only inside
RecoveryDriver after begin_install persisted. Missing drain or backup
evidence never reaches the installer. Also: keep expected/actual SHA in the
swapped-installer error and correct the VerifiedImage hold comment.

Test-First: src-tauri/src/delivery_recovery/install.rs::delivery_recovery::install::tests::production_install_never_reaches_the_installer_without_evidence
Test-First: src-tauri/src/delivery_recovery/install.rs::delivery_recovery::install::tests::production_install_persists_installing_then_runs_the_installer_once
Test-First: src-tauri/src/delivery_recovery/installer.rs::delivery_recovery::installer::tests::swapped_installer_error_names_expected_and_actual_digest

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
No-Test: allow(dead_code) attribute only, no behavior change

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@Cuarroc
Cuarroc marked this pull request as ready for review October 4, 2026 23:41
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 4, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-04T23:48:15.858675Z f97a2ee Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f97a2ee93f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +73 to +74
fn install(&mut self, _: &FileIdentity, _: &StagedManifestIdentity) -> Fx<()> {
(self.0.take().ok_or("installer already consumed")?)()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Bind the installer bytes to the journal candidate

When a BackupVerified journal was prepared for candidate A but the supplied updater resource represents candidate B—for example, after the update feed changes—the driver passes A's identities here, but this implementation discards both and invokes the unbound closure, which installs B's downloaded bytes. The journal then records Installing and performs later validation/recovery using A's identity even though B was installed; compare the updater version and downloaded-byte digest with candidate and staged_manifest before advancing the journal.

Useful? React with 👍 / 👎.

Comment thread src-tauri/src/main.rs
.install_when_idle(|| update.install(&bytes).map_err(|error| error.to_string()))
delivery_recovery::install_through_journal(
&app.state::<PtyManager>(),
app.state::<Store>().is_maintenance_active(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Hold the store maintenance lease through installation

When leave_maintenance runs concurrently after this boolean is sampled, it can roll back the store's BEGIN IMMEDIATE while installation remains authorized. install_in_maintenance cannot close this race because it checks only the PTY flag, and leave_database_maintenance releases the store before clearing that flag; it can also release the store after the installer has begun. This permits database writes during an update despite the required live write block, so installation and leaving maintenance need shared serialization or an owned maintenance guard held for the entire installer effect.

Useful? React with 👍 / 👎.

Comment on lines +38 to +39
match journal.next_action() {
RecoveryAction::InstallCandidate { .. } => Ok(()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Revalidate the database before accepting persisted evidence

When a journal reaches BackupVerified and the process then restarts or leaves maintenance before installing, later writes can change projecta.db before maintenance is entered again. This branch accepts that old journal solely from its phase plus current boolean/PTY state; it never compares the currently quiesced database path and digest, or a maintenance generation, with the stored drain and backup evidence. Installation can therefore proceed with a stale backup, and recovery may discard writes accepted after that backup was created; bind the evidence to the current maintenance interval and re-observe the live database identity before accepting it.

Useful? React with 👍 / 👎.

Comment on lines +25 to +26
if !store_write_block {
return Err("write block is not active; refusing to install".into());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Prefix install refusals with the shared error vocabulary

These new refusal paths return ad hoc text such as write block is not active rather than the shared ERR_REFUSED prefix required by docs/architecture-rules.md rule 5. Callers that classify named refusals by the repository-wide prefix will treat every missing-evidence failure from this gate as an unclassified transport error, so all refusal branches in require_install_evidence should use the shared vocabulary.

AGENTS.md reference: AGENTS.md:L306-L309

Useful? React with 👍 / 👎.

Comment on lines +38 to +39
match journal.next_action() {
RecoveryAction::InstallCandidate { .. } => Ok(()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Reverify the backup snapshot before beginning installation

When the snapshot recorded by VerifiedBackup is deleted, truncated, replaced, or aliases the live database after the journal entered BackupVerified, this phase-only check still authorizes the destructive install. The persisted record proves what was observed earlier but does not ensure that recoverable bytes still exist now, so an installation failure can leave the recovery action pointing at a missing or corrupt snapshot. Reopen the recorded snapshot, ensure it is distinct from the live database, and verify its current digest while entering Installing.

Useful? React with 👍 / 👎.

@mergify

mergify Bot commented Oct 4, 2026 •

Copy link
Copy Markdown
Contributor

Queued — the merge queue status continues in this comment ↓.

@Cuarroc Cuarroc added the review-ok Pipeline: review disposition complete, no open high finding label Oct 5, 2026
@Cuarroc

Cuarroc commented Oct 5, 2026

Copy link
Copy Markdown
Owner Author

@Mergifyio queue

@mergify mergify Bot added the queued label Oct 5, 2026
@Cuarroc Cuarroc added do-not-merge Keep this pull request out of the merge queue and removed review-ok Pipeline: review disposition complete, no open high finding labels Oct 5, 2026
@Cuarroc

Cuarroc commented Oct 5, 2026

Copy link
Copy Markdown
Owner Author

@Mergifyio dequeue

@mergify

mergify Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Merge Queue Status

  • ✅ Entered queue — 2026-10-05 00:14 UTC · Rule: default · triggered by @Cuarroc with the @mergifyio queue command
  • 🚫 Left the queue — 2026-10-05 00:15 UTC · at f97a2ee93f4ee56d041875876899b91c6052c802

This pull request spent 35 seconds in the queue, with no time running CI.

Reason

Pull request #411 has been dequeued

Queue conditions are not satisfied:

  • any of: [🔀 queue conditions]
    • all of: [📌 queue conditions of queue rule default]
      • label != do-not-merge
      • label = review-ok

Hint

You should look at the reason for the failure and decide if the pull request needs to be fixed or if you want to requeue it.
If you do update this pull request, it will automatically be requeued once the queue conditions match again.
If you think this was a flaky issue instead, you can requeue the pull request, without updating it, by posting a @mergifyio queue comment.

Requeued — the merge queue status continues in this comment ↓.

@mergify

mergify Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

dequeue

☑️ The pull request is not queued

@mergify mergify Bot added dequeued and removed queued labels Oct 5, 2026
@Cuarroc Cuarroc removed dequeued do-not-merge Keep this pull request out of the merge queue labels Oct 5, 2026
@mergify mergify Bot added the dequeued label Oct 5, 2026
@Cuarroc Cuarroc removed the dequeued label Oct 5, 2026
@mergify mergify Bot added the dequeued label Oct 5, 2026
@Cuarroc Cuarroc added the review-ok Pipeline: review disposition complete, no open high finding label Oct 5, 2026
@Cuarroc

Cuarroc commented Oct 5, 2026

Copy link
Copy Markdown
Owner Author

@Mergifyio queue

@mergify mergify Bot added the queued label Oct 5, 2026
@mergify mergify Bot mentioned this pull request Oct 5, 2026
12 tasks done
@mergify mergify Bot removed the dequeued label Oct 5, 2026
@mergify
mergify Bot merged commit 2bdabc4 into main Oct 5, 2026
15 checks passed
@mergify
mergify Bot deleted the claude/w3-02e-install-command branch October 5, 2026 00:49
@mergify

mergify Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Merge Queue Status

This pull request spent 13 minutes 41 seconds in the queue, including 13 minutes 21 seconds running CI.

Required conditions to merge

@mergify mergify Bot removed the queued label Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review-ok Pipeline: review disposition complete, no open high finding

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant