diff --git a/claude_desktop/CHANGELOG.md b/claude_desktop/CHANGELOG.md index 5bfd873568..4fe49dfb29 100644 --- a/claude_desktop/CHANGELOG.md +++ b/claude_desktop/CHANGELOG.md @@ -1,3 +1,62 @@ +## 07308545.3 (19-08-2026) +- Fix an incompletely installed Codex CLI, which silently broke every Codex tool call. Since + codex-cli 0.147.0 the CLI does not execute shell commands or file reads itself; it delegates + them to a companion `codex-code-mode-host` binary that it looks up next to its own executable. + `81-codex_cli.sh` downloaded the `codex-.tar.gz` release asset, which contains only the + `codex` executable, so that companion binary was never installed. Measured on the running add-on + (codex-cli 0.147.0, `/data/codex/bin` holding only `.version`, the launcher and `codex-real`): + `codex exec` starts, authenticates and answers, but every tool call fails with + `failed to spawn code-mode host /data/codex/bin/codex-code-mode-host: No such file or directory` + and the run still exits 0 — so Codex answered from the prompt text alone and the failure looked + like success. `--disable code_mode` does not avoid it. + - The installer now downloads the `codex-package-.tar.gz` release asset, which is the + complete package tree upstream's own installer uses, and installs all of it. Not a list of + known file names: whatever the archive contains is moved into place by position, so a helper + added by a future release arrives beside the entrypoint on its own instead of being extracted + and then dropped — cherry-picking today's two binaries works today, but it is the same mistake + at a smaller scale. For release 0.148.0 that means the entrypoint as + `/data/codex/bin/codex-real`, `codex-code-mode-host` beside it, and `codex-package.json`, + `codex-resources/` (bundled bubblewrap and zsh) and `codex-path/` (bundled ripgrep) in + `/data/codex`. The installed tree grows from ~246 MB to ~300 MB, and `/data/codex` is now + explicitly add-on-owned in its entirety: every path the new release ships replaces the + installed copy of that path outright rather than merging into it, so nothing should be kept + there by hand. A path upstream stops shipping altogether is not pruned — it is left behind as + dead weight that the new entrypoint no longer looks for. Codex's own state stays in + `~/.codex` and is never touched. + - An incomplete install is no longer advertised. `82-claude_tools.sh` registers the Codex MCP + server whenever the launcher at `/data/codex/bin/codex` is executable and re-checks nothing + else, and both the launcher and the package tree persist in `/data` independently of each + other. The launcher is therefore now written only for an install that has its executable, its + code-mode host, its package manifest and its version stamp, and is removed together with the + `/usr/local/bin/codex` symlink otherwise. The stamp is part of that test because it is deleted + before the first file of a replacement is moved and written after the last, so a stamp-less + prefix is exactly the tree that may mix two releases. This covers the cases that reach the + launcher without a fresh install: a boot that cannot reach the release metadata and finds a + pre-existing incomplete install, and a launcher left behind by an interrupted replacement. + Nothing under `/data/codex` is deleted beyond that launcher — the executable, the package + tree and the ChatGPT sign-in stay, so a later boot completes the install without another + download or another login. + - That layout is load-bearing, so the install prefix was chosen to satisfy it rather than + changed. Codex canonicalises its own executable path, requires the parent directory to be + named `bin`, and reads the manifest and helper directories from that directory's parent — the + existing `/data/codex/bin` prefix already matches, and the executable's file name is not part + of the contract, so `codex-real` and the subscription-only `codex` launcher wrapping it are + both unchanged, as is the `/usr/local/bin/codex` symlink and the MCP registration. + - Existing installs repair themselves. The "already installed, skip the download" test now also + requires the code-mode host and the package manifest to be present, so an add-on that already + has a working `codex-real` and no helpers reinstalls on the next start instead of staying + quietly broken. + - `claude-tools-doctor.sh` now reports whether the package layout is complete, because the + failure mode this fixes is invisible in `codex --version`, in the version stamp and in the + exit code. +- Known limitation, unchanged by this release and not caused by it: Codex's own Linux sandbox + cannot start in this container. Running both the system `bwrap` 0.8.0 and the bundled one + directly with `--dev-bind / / --unshare-net /bin/true` fails identically with + `bwrap: loopback: Failed RTM_NEWADDR: Operation not permitted`, so it is a container capability + limitation rather than a packaging one. With the shipped `codex_sandbox_mode: workspace-write` + default, tool calls therefore still fail with that bwrap error; `danger-full-access` is the only + mode that currently executes commands, and the container is already the security boundary. + ## 07308545.1 (17-08-2026) - Minor bugs fixed diff --git a/claude_desktop/README.md b/claude_desktop/README.md index ffcbf587c2..2b1ba37f42 100644 --- a/claude_desktop/README.md +++ b/claude_desktop/README.md @@ -209,14 +209,25 @@ registers `codex mcp-server` in both Claude Code and Claude Desktop. A Claude session can therefore delegate a task to ChatGPT Codex and read its result back through MCP. -Codex is not baked into the image because its Linux binary is large and the -feature is off by default. At each startup, the add-on resolves the latest -stable upstream release. It downloads the architecture-specific binary into -persistent `/data/codex/bin` only when the installed release is missing or -outdated, verifies the GitHub-published SHA-256 digest before extraction or -execution, validates the staged binary with `--version`, and replaces the -existing binary atomically. If release metadata or the download is unavailable, -startup continues and a previously working installation is retained. +Codex is not baked into the image because its Linux distribution is large and +the feature is off by default. At each startup, the add-on resolves the latest +stable upstream release. It downloads the architecture-specific package into +persistent `/data/codex` only when the installed release is missing, incomplete +or outdated, verifies the GitHub-published SHA-256 digest before extraction or +execution, and validates the staged package with `--version` before it replaces +the installed one. The complete upstream package is installed, not just the +`codex` executable: Codex delegates every shell and file-read tool call to a +companion `codex-code-mode-host` binary that it looks up next to itself, so an +executable installed on its own can answer but can never run anything. If +release metadata or the download is unavailable, startup continues and a +previously working installation is retained. + +`/data/codex` belongs to the add-on: everything below it — `bin/`, +`codex-package.json`, `codex-resources/` and `codex-path/` — is replaced as a +unit whenever a new release is installed, so it is not a place to keep files by +hand. Codex's own state (`auth.json`, `config.toml`) lives in `~/.codex` and is +never touched by an install. The installed package is roughly 300 MB, and an +upgrade briefly needs room for the archive and both releases at once. ### Signing in with a ChatGPT subscription diff --git a/claude_desktop/config.yaml b/claude_desktop/config.yaml index 2814570839..34b01db128 100644 --- a/claude_desktop/config.yaml +++ b/claude_desktop/config.yaml @@ -136,5 +136,5 @@ schema: slug: claude_desktop udev: true url: https://github.com/alexbelgium/hassio-addons -version: "07308545.1" +version: "07308545.3" video: true diff --git a/claude_desktop/rootfs/etc/cont-init.d/81-codex_cli.sh b/claude_desktop/rootfs/etc/cont-init.d/81-codex_cli.sh index aaf6a6a864..988483dfd6 100755 --- a/claude_desktop/rootfs/etc/cont-init.d/81-codex_cli.sh +++ b/claude_desktop/rootfs/etc/cont-init.d/81-codex_cli.sh @@ -10,10 +10,20 @@ set -o pipefail # The install prefix is /data/codex, NOT $HOME/.codex/bin: /data is persistent regardless of the # configurable data_location, and the managed MCP merge treats commands under $HOME as # user-installed. Codex state (auth.json, config.toml) remains in the runtime user's home. +# +# Codex is distributed as a package tree, not as a lone executable: since 0.147.0 every shell and +# file-read tool call is executed by a companion binary, codex-code-mode-host, that Codex looks up +# next to itself. Upstream publishes that tree as the codex-package- release asset, and the +# whole tree is installed here. Its layout is load-bearing and must not be flattened: Codex +# canonicalises its own executable path, requires the parent directory to be named `bin`, and then +# reads codex-package.json, codex-resources/ and codex-path/ from that directory's parent. The +# executable's file name is not part of that contract, which is why codex-real keeps its name. CODEX_ROOT="/data/codex" CODEX_PREFIX="${CODEX_ROOT}/bin" CODEX_BIN="${CODEX_PREFIX}/codex" CODEX_REAL="${CODEX_PREFIX}/codex-real" +CODEX_HOST="${CODEX_PREFIX}/codex-code-mode-host" +CODEX_MANIFEST="${CODEX_ROOT}/codex-package.json" CODEX_STAMP="${CODEX_PREFIX}/.version" CODEX_LINK="/usr/local/bin/codex" CODEX_RELEASE_API="https://api.github.com/repos/openai/codex/releases/latest" @@ -28,6 +38,60 @@ run_as_runtime_user() { s6-setuidgid abc env HOME="$RUNTIME_HOME" CODEX_HOME="$RUNTIME_HOME/.codex" "$@" } +# What "installed" means, in one place. A Codex that is missing its code-mode host, its package +# manifest or its version stamp still starts, authenticates and answers — it simply cannot run a +# single tool call — so presence of the executable alone is not a usable install. The stamp counts +# because it is removed before the first file of a replacement is moved and written after the last, +# so its absence next to an executable means the tree may mix two releases. +codex_install_is_complete() { + [ -x "$CODEX_REAL" ] \ + && [ -x "$CODEX_HOST" ] \ + && [ -f "$CODEX_MANIFEST" ] \ + && [ -f "$CODEX_STAMP" ] +} + +# Move a verified package tree from staging into the install prefix. Called only from an `if` +# condition, where `set -e` does not apply, so every step reports failure explicitly. +# +# Whatever the package ships is installed, rather than the file names this add-on happens to know +# about today: a helper added by a future release has to arrive beside codex-real on its own, or it +# fails exactly the way the missing code-mode host did. Only paths the archive actually contains are +# touched — /data/codex also holds this install's staging directory, so the tree below it is never +# cleared wholesale. +# +# The long, failure-prone part of an install — the download and its digest check — is already done +# by the time this runs; what is left is same-filesystem renames of an already validated tree. They +# are not one atomic operation, so the version stamp is removed first: any interruption leaves a +# stamp-less prefix, which the next boot treats as "not installed" and replaces wholesale. The +# entrypoint is moved last, so a prefix whose codex-real is the new release is a prefix whose +# helper binaries are the new release too. +install_codex_package() { + local staged="$1" + local entry name + rm -f -- "$CODEX_STAMP" || return 1 + # Everything beside bin/ first — the manifest and the helper directories (codex-resources/ and + # codex-path/ today, holding bubblewrap, zsh and ripgrep) — then everything the package puts in + # bin/ except the entrypoint, then the entrypoint. The existing launcher and version stamp are + # never matched: the launcher is skipped by name and the stamp is a dot file. + for entry in "${staged}"/*; do + name="${entry##*/}" + if [ ! -e "$entry" ] || [ "$name" = "bin" ]; then + continue + fi + rm -rf -- "${CODEX_ROOT:?}/${name}" || return 1 + mv -f -- "$entry" "${CODEX_ROOT}/${name}" || return 1 + done + for entry in "${staged}"/bin/*; do + name="${entry##*/}" + if [ ! -e "$entry" ] || [ "$name" = "codex" ]; then + continue + fi + rm -rf -- "${CODEX_PREFIX:?}/${name}" || return 1 + mv -f -- "$entry" "${CODEX_PREFIX}/${name}" || return 1 + done + mv -f -- "${staged}/bin/codex" "$CODEX_REAL" || return 1 +} + if ! bashio::config.true 'install_codex_cli'; then # Non-destructive: preserve the binary and completed ChatGPT sign-in for a later re-enable. # 82-claude_tools.sh removes only the MCP registration and managed guidance. @@ -44,7 +108,7 @@ case "$(uname -m)" in ;; esac -CODEX_ASSET="codex-${CODEX_TARGET}.tar.gz" +CODEX_ASSET="codex-package-${CODEX_TARGET}.tar.gz" mkdir -p "$CODEX_PREFIX" # Migrate the PR's earlier direct-binary layout to the enforced wrapper layout without another @@ -116,38 +180,47 @@ PY fi if [ -z "$release_info" ]; then - if [ -x "$CODEX_REAL" ] && run_as_runtime_user "$CODEX_REAL" --version > /dev/null 2>&1; then + if codex_install_is_complete && run_as_runtime_user "$CODEX_REAL" --version > /dev/null 2>&1; then bashio::log.warning "Unable to resolve the latest verified Codex release; keeping the existing install" else - bashio::log.warning "Unable to resolve the latest verified Codex release; Codex is unavailable this boot" - exit 0 + bashio::log.warning "Unable to resolve the latest verified Codex release; the installed Codex is missing or incomplete and stays unavailable until a boot can reach the release metadata" fi else IFS=$'\t' read -r CODEX_WANTED CODEX_SHA256 CODEX_URL <<< "$release_info" - if [ -x "$CODEX_REAL" ] \ + # An install is complete only if the code-mode host and the package manifest are there too: + # every install made before this add-on switched to the package asset has a working codex-real + # and no helpers, and repairs itself here rather than needing a fresh /data. Running the binary + # also rejects one built for another architecture, which a restored backup could leave behind. + if codex_install_is_complete \ && [ "$(cat "$CODEX_STAMP" 2> /dev/null || true)" = "$CODEX_WANTED" ] \ && run_as_runtime_user "$CODEX_REAL" --version > /dev/null 2>&1; then bashio::log.info "Codex CLI ${CODEX_WANTED} already installed (latest stable)" else bashio::log.info "Installing latest stable Codex CLI ${CODEX_WANTED} (${CODEX_TARGET}); this is a large one-time download" archive="${codex_tmp}/${CODEX_ASSET}" - extracted="${codex_tmp}/codex-${CODEX_TARGET}" + staged="${codex_tmp}/package" - # Fail open for add-on startup but fail closed for the candidate binary: its official + # Fail open for add-on startup but fail closed for the candidate release: its official # release digest must match before extraction or execution, and replacement happens only - # after the staged binary successfully runs. - if curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 10 --max-time 600 \ - -o "$archive" "$CODEX_URL" \ + # after the staged tree is complete and its entrypoint successfully runs. The candidate is + # exercised in staging with its own codex-package.json and helper directories in place, so + # the layout Codex will resolve at runtime is the layout that was validated. + if mkdir -p "$staged" \ + && chmod 0755 "$staged" \ + && curl -fsSL --retry 3 --retry-delay 2 --connect-timeout 10 --max-time 600 \ + -o "$archive" "$CODEX_URL" \ && printf '%s %s\n' "$CODEX_SHA256" "$archive" | sha256sum -c - > /dev/null \ - && tar -xzf "$archive" -C "$codex_tmp" \ - && [ -f "$extracted" ] \ - && chmod 0755 "$extracted" \ - && run_as_runtime_user "$extracted" --version > /dev/null 2>&1 \ - && mv -f "$extracted" "$CODEX_REAL"; then + && tar -xzf "$archive" -C "$staged" \ + && [ -f "${staged}/codex-package.json" ] \ + && [ -f "${staged}/bin/codex" ] \ + && [ -f "${staged}/bin/codex-code-mode-host" ] \ + && chmod 0755 "${staged}/bin/codex" "${staged}/bin/codex-code-mode-host" \ + && run_as_runtime_user "${staged}/bin/codex" --version > /dev/null 2>&1 \ + && install_codex_package "$staged"; then printf '%s' "$CODEX_WANTED" > "$CODEX_STAMP" bashio::log.info "Codex CLI installed: $("$CODEX_REAL" --version 2> /dev/null || echo unknown)" - elif [ -x "$CODEX_REAL" ]; then + elif codex_install_is_complete; then bashio::log.warning "Verified Codex ${CODEX_WANTED} installation failed; keeping the existing install" else bashio::log.warning "Verified Codex ${CODEX_WANTED} installation failed; Codex is unavailable this boot" @@ -155,7 +228,16 @@ else fi fi -if [ ! -x "$CODEX_REAL" ]; then +# The launcher is the add-on's single "Codex is usable" signal: 82-claude_tools.sh registers the +# Codex MCP server when it is executable and re-checks nothing else. Write it only for a complete +# install, and remove it — together with the PATH symlink — for an incomplete one. Both the launcher +# and the package tree live in /data and survive restarts independently, so a launcher left from an +# earlier boot would otherwise outlive the install it was written for and advertise a Codex whose +# every tool call fails. The executable, the package tree and the ChatGPT sign-in are all left in +# place: a later boot completes the install without another download or another login. +if ! codex_install_is_complete; then + rm -f -- "$CODEX_BIN" "$CODEX_LINK" + bashio::log.warning "Codex is not completely installed; not registering it this boot" exit 0 fi diff --git a/claude_desktop/rootfs/usr/local/bin/claude-tools-doctor.sh b/claude_desktop/rootfs/usr/local/bin/claude-tools-doctor.sh index be3a1ef2e6..93aa5551c2 100755 --- a/claude_desktop/rootfs/usr/local/bin/claude-tools-doctor.sh +++ b/claude_desktop/rootfs/usr/local/bin/claude-tools-doctor.sh @@ -183,6 +183,16 @@ if bashio::config.true 'install_codex_cli'; then if [ -x "$codex_bin" ]; then printf '%-30s %s\n' "installed" "$("$codex_bin" --version 2> /dev/null || echo 'FAILED TO RUN')" printf '%-30s %s\n' "installed version stamp" "$(cat /data/codex/bin/.version 2> /dev/null || echo 'MISSING')" + # Codex runs every shell and file-read tool call through this companion binary. When it is + # absent the CLI still starts, authenticates and answers, but each tool call fails and the + # run still exits 0 — so report it explicitly rather than leaving it to be inferred. + if [ -x /data/codex/bin/codex-code-mode-host ] \ + && [ -f /data/codex/codex-package.json ] \ + && [ -f /data/codex/bin/.version ]; then + printf '%-30s %s\n' "package layout" "complete" + else + printf '%-30s %s\n' "package layout" "INCOMPLETE - tool calls will fail; restart the add-on to reinstall" + fi printf '%-30s %s\n' "release policy" "latest stable, SHA-256 verified" printf '%-30s %s\n' "authentication policy" "ChatGPT subscription only"