fix(update): never self-update a frozen binary; point at its own channel - #552
Merged
Conversation
padak
force-pushed
the
fix/frozen-binary-self-update
branch
from
August 2, 2026 08:51
8713e2f to
6492f38
Compare
padak
added a commit
that referenced
this pull request
Aug 2, 2026
…in review Six findings from Devin review of #552. The first one mattered: the feature did not work at all in the artifacts CI actually ships. 1. The startup notification was unreachable in every released binary. maybe_auto_update() bails at _should_skip_all() -> _is_dev_install(), which reads the bundled direct_url.json. CI freezes from an editable `uv run` sync and --collect-all copies the whole .dist-info, so that file says "editable": true inside every shipped binary and the entire hook was skipped. My own end-to-end verification missed this because it built from a NON-editable install. _is_dev_install() now short-circuits on is_frozen_build(): the bundled marker describes the build machine, not the one running the binary. Covered by a test that exercises the real _should_skip_all() chain instead of stubbing it -- stubbing it is what hid the bug. 2. /usr/local/bin on Linux was attributed to apt/dnf. nfpm installs only to /usr/bin; /usr/local/bin is exactly where a hand-unpacked archive lands, and apt-get exists on essentially every Debian/Ubuntu host, so those users were silently handed an upgrade command for a package that was never installed. Dropped from NATIVE_SYSTEM_BIN_PREFIXES; the parametrized test now covers Linux as well as macOS. 3. The refusal summary printed "-> vNone available" when the release lookup failed. It now says the latest version is unknown. 4. upgrade_command held the prose hint for archive/system channels, while the gotchas entry tells consumers they may shell out to it. It is now runnable-or-empty, with the sentence in a new additive upgrade_hint key. 5. keboola-expert.md still told agents to answer version problems with "run kbagent update", which native binaries now refuse. Added a terse version-gate note and a §3 gotcha (prompt is 59632 B of the 62000 B budget). 6. The release note was one wall of text; split into four bullets, one logical change each, per the changelog.py authoring contract.
Member
Author
|
All six Devin findings are addressed in 438c194 and dc4636d (the review above predates both).
Also swept the remaining convention #17 surfaces that a behaviour change in |
kbagent ships both as a Python distribution and as a self-contained PyInstaller binary delivered by Chocolatey, WinGet, Homebrew, apt, dnf or a signed zip. Nothing in the code detected the difference: `grep -rn "sys.frozen\|_MEIPASS" src/` returned nothing. That matters because the release workflow freezes with `pyinstaller --collect-all keboola_agent_cli`, and PyInstaller's collect_all() is a superset of --copy-metadata. The whole .dist-info is bundled, so importlib.metadata resolves a real version inside the binary and the `0.0.0-dev` fallback never fires. Both update paths therefore planned `uv tool install --force --reinstall "keboola-cli[server] @ ..."`, which cannot upgrade a package-manager-owned binary -- it installs a SECOND, unrelated kbagent into the uv tool directory, which usually precedes the package manager's directory on PATH. The user silently starts running a different install than the one choco/brew/apt tracks while the real binary stays stale; with no Python on the machine it just fails on every startup. The startup hook was only ever suppressed for frozen builds by accident: CI freezes from an editable install, so the bundled direct_url.json carries "editable": true and _is_dev_install() happened to return True. Freezing from a non-editable install flips that and the reinstall fires. Both states were reproduced against real PyInstaller binaries; `kbagent update` was broken in both, since it never consulted _is_dev_install() at all. New frozen_dist module detects sys.frozen / sys._MEIPASS and maps the running binary's own path to the channel that placed it. The self-update stage becomes a notification carrying that channel's real command; an unattributable path degrades to ARCHIVE and the release page rather than a wrong-but-plausible guess. The guard sits ahead of the issue #528 `should_defer()` branch, so a frozen binary is never scheduled for the deferred Windows install either. - startup hook: notification throttled to once per version-cache TTL (it cannot resolve itself by re-exec'ing, so it would otherwise print on every command), and planning is short-circuited so the wheel-URL HEAD probe is not spent on a command we refuse to run. - `kbagent update`: refuses and names the channel, and no longer reports that deliberate refusal as a failed update. - `kbagent version`: advertises the channel command in place of "(run: kbagent update)", plus an additive install_channel key in --json (absent for Python installs, so their JSON shape is byte-identical). - keboola-mcp-server still auto-updates on frozen builds by design: it is a separate Python distribution the binary only spawns as a subprocess, and a pure-binary user with no Python is unaffected because install-method detection returns "none". Verified end-to-end against a real frozen binary placed in a Homebrew Cellar path: the hook prints "brew upgrade keboola-cli2" and the uv tool environment is left untouched. uv / pip installs behave exactly as before.
…son fix Release audit for 0.78.0 turned up two gaps in the agent-facing docs. Five reference files existed but nothing linked them, so the skill could never load them: config-metadata, storage-describe, lineage-deep, permissions and kai workflows. They are now rows in SKILL.md's workflow table (the hand-maintained one below the auto-generated command block, so `make skill-check` stays green). no changelog entry and no gotcha. It changes what an agent parsing --json on Windows sees -- the payload is no longer encoder-dependent, and JSON lines end LF rather than CRLF there -- so it gets both: a (since v0.78.0) gotcha and a changelog bullet under 0.78.0, the release that ships it. Not documented here: the Apache 2.0 LICENSE added by #544. pyproject.toml, nfpm.yaml and the Homebrew formula all still declare MIT, so which licence kbagent ships under is a decision to settle before it is announced anywhere.
…in review Six findings from Devin review of #552. The first one mattered: the feature did not work at all in the artifacts CI actually ships. 1. The startup notification was unreachable in every released binary. maybe_auto_update() bails at _should_skip_all() -> _is_dev_install(), which reads the bundled direct_url.json. CI freezes from an editable `uv run` sync and --collect-all copies the whole .dist-info, so that file says "editable": true inside every shipped binary and the entire hook was skipped. My own end-to-end verification missed this because it built from a NON-editable install. _is_dev_install() now short-circuits on is_frozen_build(): the bundled marker describes the build machine, not the one running the binary. Covered by a test that exercises the real _should_skip_all() chain instead of stubbing it -- stubbing it is what hid the bug. 2. /usr/local/bin on Linux was attributed to apt/dnf. nfpm installs only to /usr/bin; /usr/local/bin is exactly where a hand-unpacked archive lands, and apt-get exists on essentially every Debian/Ubuntu host, so those users were silently handed an upgrade command for a package that was never installed. Dropped from NATIVE_SYSTEM_BIN_PREFIXES; the parametrized test now covers Linux as well as macOS. 3. The refusal summary printed "-> vNone available" when the release lookup failed. It now says the latest version is unknown. 4. upgrade_command held the prose hint for archive/system channels, while the gotchas entry tells consumers they may shell out to it. It is now runnable-or-empty, with the sentence in a new additive upgrade_hint key. 5. keboola-expert.md still told agents to answer version problems with "run kbagent update", which native binaries now refuse. Added a terse version-gate note and a §3 gotcha (prompt is 59632 B of the 62000 B budget). 6. The release note was one wall of text; split into four bullets, one logical change each, per the changelog.py authoring contract.
Convention #17 lists these as silent-drift risks with no CI coverage, and `kbagent update` changing behaviour is exactly the case they exist for: - commands-reference.md: update/version entries note the refusal and the new install_channel / upgrade_hint keys. - context.py (AGENT_CONTEXT, served by `kbagent context`): same, in the `kbagent update` block. - CLAUDE.md "All CLI Commands": same, next to the existing self-update notes. check_command_sync.py stays green (251 commands).
padak
force-pushed
the
fix/frozen-binary-self-update
branch
from
August 2, 2026 09:23
dc4636d to
49633b2
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
kbagent ships both as a Python distribution and as a self-contained
PyInstaller binary delivered by Chocolatey, WinGet, Homebrew, apt, dnf or a
signed zip. Nothing in the code detected the difference:
grep -rn "sys.frozen\|_MEIPASS" src/returned nothing.That matters because the release workflow freezes with
pyinstaller --collect-all keboola_agent_cli, and PyInstaller's collect_all() is asuperset of --copy-metadata. The whole .dist-info is bundled, so
importlib.metadata resolves a real version inside the binary and the
0.0.0-devfallback never fires. Both update paths therefore planneduv tool install --force --reinstall "keboola-cli[server] @ ...", whichcannot upgrade a package-manager-owned binary -- it installs a SECOND,
unrelated kbagent into the uv tool directory, which usually precedes the
package manager's directory on PATH. The user silently starts running a
different install than the one choco/brew/apt tracks while the real binary
stays stale; with no Python on the machine it just fails on every startup.
The startup hook was only ever suppressed for frozen builds by accident: CI
freezes from an editable install, so the bundled direct_url.json carries
"editable": true and _is_dev_install() happened to return True. Freezing from
a non-editable install flips that and the reinstall fires. Both states were
reproduced against real PyInstaller binaries;
kbagent updatewas broken inboth, since it never consulted _is_dev_install() at all.
New frozen_dist module detects sys.frozen / sys._MEIPASS and maps the running
binary's own path to the channel that placed it. The self-update stage becomes
a notification carrying that channel's real command; an unattributable path
degrades to ARCHIVE and the release page rather than a wrong-but-plausible
guess. The guard sits ahead of the issue #528
should_defer()branch, so afrozen binary is never scheduled for the deferred Windows install either.
cannot resolve itself by re-exec'ing, so it would otherwise print on every
command), and planning is short-circuited so the wheel-URL HEAD probe is not
spent on a command we refuse to run.
kbagent update: refuses and names the channel, and no longer reports thatdeliberate refusal as a failed update.
kbagent version: advertises the channel command in place of"(run: kbagent update)", plus an additive install_channel key in --json
(absent for Python installs, so their JSON shape is byte-identical).
separate Python distribution the binary only spawns as a subprocess, and a
pure-binary user with no Python is unaffected because install-method
detection returns "none".
Verified end-to-end against a real frozen binary placed in a Homebrew Cellar
path: the hook prints "brew upgrade keboola-cli2" and the uv tool environment
is left untouched. uv / pip installs behave exactly as before.