diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 4469bd24..df5119fd 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -10,7 +10,7 @@ "plugins": [ { "name": "kbagent", - "version": "0.77.0", + "version": "0.77.1", "source": "./plugins/kbagent", "description": "AI-friendly interface to Keboola Connection projects — explore configs, jobs, lineage, call MCP tools, manage dev branches, and debug SQL in workspaces", "category": "development" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 780b7273..5130eb34 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -136,7 +136,7 @@ jobs: # `npm.cmd` invocation (Bug 1) and the force-include path resolution (Bug 2) # are exercised against a real `uv build` rather than mocks. build-windows: - name: Windows wheel build and export regression (issues #320, #529) + name: Windows wheel build, export and self-update regressions (issues #320, #529, #528) runs-on: windows-latest steps: - uses: actions/checkout@v5 @@ -172,6 +172,16 @@ jobs: - name: Test semantic-layer export on Windows (issue #529) run: uv run pytest tests/test_semantic_layer_service.py -k "export" -v + # The self-update helper is a PowerShell script authored on machines that + # cannot execute it. This suite runs it for real: it proves the script + # parses, that it records the installer's exit code, and -- the branch + # that actually protects the environment -- that it installs NOTHING + # while a watched process is still alive (issue #528). The rest of the + # suite also runs here with `should_defer()` returning its real Windows + # default and a real detached spawn. + - name: Test the deferred self-update helper on Windows (issue #528) + run: uv run pytest tests/test_update_runner.py -v + - name: Assert the SPA is bundled (Bug 1 fixed) run: python scripts/check_wheel_ui.py --expect-ui diff --git a/docs/superpowers/specs/2026-07-29-issue-528-deferred-windows-self-update-design.md b/docs/superpowers/specs/2026-07-29-issue-528-deferred-windows-self-update-design.md new file mode 100644 index 00000000..61716cd3 --- /dev/null +++ b/docs/superpowers/specs/2026-07-29-issue-528-deferred-windows-self-update-design.md @@ -0,0 +1,184 @@ +# Design: defer the Windows self-update out of the running environment + +**Issue:** [#528](https://github.com/keboola/cli/issues/528) +**Supersedes:** `2026-07-23-issue-528-safe-self-update-design.md` (shipped in v0.76.2) +**Status:** Implemented +**Target:** `kbagent update` and the startup auto-update hook + +## Why the v0.76.2 fix was not enough + +v0.76.2 moved every network call, probe, and command construction ahead of any +mutation, updated the independent MCP environment first, and made the kbagent +reinstall a terminal `uv tool install --force --reinstall` from an exact +release artifact. That was a correct fix for a real ordering bug -- the missing +`certifi/cacert.pem` in incident 2 -- and it did not fix the reported +corruption. The reporter hit it again on v0.76.2 -> v0.76.3. + +The reason is that ordering was never the mechanism. + +`uv tool install` recreates a tool environment **in place**. From +`crates/uv-tool/src/lib.rs`, `create_environment` removes any existing +environment and then calls `create_venv` at the same path. There is no +temporary build directory, no atomic swap, and no rollback. + +On POSIX this is safe by accident of the filesystem semantics: unlinking a file +another process holds open leaves that process's inode intact, so a running +kbagent survives having its own venv deleted and rebuilt underneath it. That is +why nobody on macOS or Linux ever saw this. + +On Windows it cannot work. uv's `kbagent.exe` trampoline loads the tool venv's +interpreter in-process, so those files are locked for as long as kbagent runs. +The removal deletes every file it can, reaches a locked one, and aborts. What +is left is not a mixture of old and new distributions -- it is a **partially +deleted** venv, which is exactly what the reported symptoms describe: `rich` +still present but `rich/_windows.py` gone, `typer` still present but +`typer/rich_utils.py` gone. Upstream tracks the same class of failure as +astral-sh/uv#11930 (`uv tool upgrade` of an in-use tool leaves the environment +inconsistent and the receipt lying about it). + +Running the installer from inside the environment it replaces is therefore +unsafe on Windows *by construction*, with any combination of uv flags. + +### The second, independent corruption vector + +Both update paths ran the installer through `subprocess.run(..., timeout=...)`, +which **kills the child** when the deadline expires -- on Windows a +`TerminateProcess`. Killing uv part-way through recreating a venv produces the +same half-deleted environment a file lock does, from our own code, on every +platform. The default deadline was 300s; a cold resolution of the `[server]` +extra on a Windows machine with real-time AV scanning is not reliably under +that. + +## Goals + +- Never run the installer from a process whose own environment is the target. +- Never terminate an installer mid-transaction. +- Never fall back to the unsafe path when the safe one is unavailable. +- Keep the POSIX behaviour that demonstrably works today. +- Report a deferred outcome exactly once, with recovery guidance on failure. +- Be verifiable in CI on macOS/Linux, where the failure cannot be reproduced. + +## Non-goals + +- Making `uv tool install` atomic. That is uv's to own. +- Updating while a long-lived kbagent (`serve`, `repl`) refuses to exit. The + helper declines and retries later; declining is always safe. +- A persistent background update daemon. +- Reworking the MCP environment, which is separate and never the one we run from. + +## Design + +### 1. `update_runner.run_install` -- bound the wait, not the installer + +A single execution helper for both entry points. Output goes to a log file +rather than a pipe (a pipe whose reader is gone would block a child we intend +to outlive), and on timeout it returns `STILL_RUNNING` **without killing**. The +caller reports that the install continues in the background and deliberately +offers no recovery command: a second installer aimed at an environment a live +uv is rewriting is the corruption, not the cure. + +### 2. `update_runner.request_deferred_update` -- install after we are gone + +On Windows (`should_defer()`, overridable with `KBAGENT_DEFER_UPDATE`) the +prepared install command is handed to a detached helper: + +1. Write a marker file first, so a helper that dies immediately still leaves + evidence a later run can report instead of a silent no-op. +2. Spawn `powershell.exe -NoProfile -NonInteractive -WindowStyle Hidden + -Command