diff --git a/setup/provenance/README.md b/setup/provenance/README.md index 0460ac9..f00162d 100644 --- a/setup/provenance/README.md +++ b/setup/provenance/README.md @@ -77,7 +77,10 @@ matched the `digest` GitHub publishes for that release asset. The installer accepts a Codex archive only when `tar -tzf` prints exactly the entries in `codex-package.listing`. Both architectures share that listing. 0.165.0 dropped the bundled `codex-resources/zsh/` and added `codex-resources/voice/`, and the -archives grew past 128 MiB, so `CODEX_*_MAX_BYTES` is 256 MiB. To bump Codex, +archives grew past 128 MiB, so `CODEX_*_MAX_BYTES` is 256 MiB. The installer keeps the +whole verified tree as the version directory and links `~/.local/bin/codex` to +its `bin/codex`, because Codex runs `bin/codex-code-mode-host` and reads +`codex-path/` and `codex-resources/` relative to itself. To bump Codex, update `installers.conf`, regenerate the listing with `tar -tzf ARTIFACT`, and run `tests/setup/package-provenance-contract`. diff --git a/setup/scripts/install-packages b/setup/scripts/install-packages index dab3f48..4959932 100755 --- a/setup/scripts/install-packages +++ b/setup/scripts/install-packages @@ -417,20 +417,33 @@ _codex_version_matches() { [[ "$output" =~ (^|[^0-9])${version_pattern}([^0-9]|$) ]] } +# Codex ships as a package tree: bin/codex finds bin/codex-code-mode-host, +# codex-path/ and codex-resources/ beside itself, so the version directory keeps +# the whole verified tree and the active link points at its bin/codex. _install_codex() ( local artifact_arch version_dir target bin_link parent expected_listing member local stage archive listing staged_binary artifact_digest binary_digest + local retire_flat=0 stage="" - trap '[[ -z "$stage" ]] || rm -rf -- "$stage"' EXIT + # A retired flat install goes back if activation never happened. + trap '[[ -z "$stage" ]] || { [[ -d "$stage/retired" && ! -e "$version_dir" ]] \ + && mv -Tn -- "$stage/retired" "$version_dir"; rm -rf -- "$stage"; }' EXIT trap 'exit 130' INT trap 'exit 143' TERM _set_artifact_arch || return 1 version_dir="$HOME/.local/lib/panama/codex/${INSTALLER_PROVENANCE[CODEX_VERSION]}" - target="$version_dir/codex" + target="$version_dir/bin/codex" bin_link="$HOME/.local/bin/codex" artifact_digest="${INSTALLER_PROVENANCE[CODEX_${artifact_arch}_SHA256]}" binary_digest="${INSTALLER_PROVENANCE[CODEX_${artifact_arch}_BINARY_SHA256]}" - if [[ -e "$version_dir" || -L "$version_dir" ]]; then + # Earlier installs of this same reviewed artifact kept only the codex binary + # at the top of the version directory, without the helpers it needs. That + # directory carries our receipt, so it is replaced once the full tree verifies. + if [[ ! -e "$version_dir/bin" && ! -L "$version_dir/bin" ]] \ + && _runtime_receipt_matches "$version_dir" "$version_dir/codex" \ + "$artifact_digest" "$binary_digest"; then + retire_flat=1 + elif [[ -e "$version_dir" || -L "$version_dir" ]]; then _runtime_receipt_matches "$version_dir" "$target" \ "$artifact_digest" "$binary_digest" || return 1 [[ -x "$target" ]] || return 1 @@ -459,24 +472,24 @@ _install_codex() ( while IFS= read -r member; do _archive_path_is_safe "$member" || { rm -rf -- "$stage"; return 1; } done <<<"$listing" - mkdir -m 0700 "$stage/extract" "$stage/version" \ + mkdir -m 0700 "$stage/version" \ || { rm -rf -- "$stage"; return 1; } - tar -xzf "$archive" --no-same-owner --no-same-permissions -C "$stage/extract" \ + tar -xzf "$archive" --no-same-owner --no-same-permissions -C "$stage/version" \ || { rm -rf -- "$stage"; return 1; } - _tree_links_stay_inside "$stage/extract" || { rm -rf -- "$stage"; return 1; } - _tree_hardlinks_stay_inside "$stage/extract" || { rm -rf -- "$stage"; return 1; } - staged_binary="$stage/extract/bin/codex" + _tree_links_stay_inside "$stage/version" || { rm -rf -- "$stage"; return 1; } + _tree_hardlinks_stay_inside "$stage/version" || { rm -rf -- "$stage"; return 1; } + staged_binary="$stage/version/bin/codex" [[ -f "$staged_binary" && ! -L "$staged_binary" && -x "$staged_binary" && "$(sha256sum "$staged_binary" | awk '{ print $1 }')" == "$binary_digest" ]] \ || { rm -rf -- "$stage"; return 1; } _codex_version_matches "$staged_binary" \ || { rm -rf -- "$stage"; return 1; } - mv -- "$staged_binary" "$stage/version/codex" \ - || { rm -rf -- "$stage"; return 1; } _write_runtime_receipt "$stage/version" "$artifact_digest" "$binary_digest" \ || { rm -rf -- "$stage"; return 1; } - _activate_directory_no_replace "$stage/version" "$version_dir" \ - || { rm -rf -- "$stage"; return 1; } + if (( retire_flat )); then + mv -Tn -- "$version_dir" "$stage/retired" || return 1 + fi + _activate_directory_no_replace "$stage/version" "$version_dir" || return 1 _runtime_receipt_matches "$version_dir" "$target" \ "$artifact_digest" "$binary_digest" || { rm -rf -- "$stage"; return 1; } _codex_version_matches "$target" || { rm -rf -- "$stage"; return 1; } diff --git a/tests/setup/package-provenance-contract b/tests/setup/package-provenance-contract index e6ce825..281778b 100755 --- a/tests/setup/package-provenance-contract +++ b/tests/setup/package-provenance-contract @@ -1375,8 +1375,11 @@ grep -qFx 'nvm:alias default 24.20.0' "$test_tmp/cases/node-x86_64/commands.log" [[ "$($test_tmp/cases/codex-x86_64/home/.local/bin/codex --version)" == \ 'codex-cli 0.165.0' ]] || fail 'x86_64 Codex activation has the wrong version' [[ "$(readlink "$test_tmp/cases/codex-x86_64/home/.local/bin/codex")" == \ - "$test_tmp/cases/codex-x86_64/home/.local/lib/panama/codex/0.165.0/codex" ]] \ + "$test_tmp/cases/codex-x86_64/home/.local/lib/panama/codex/0.165.0/bin/codex" ]] \ || fail 'Codex did not atomically activate the reviewed version path' +[[ -x "$test_tmp/cases/codex-x86_64/home/.local/lib/panama/codex/0.165.0/bin/codex-code-mode-host" \ + && -f "$test_tmp/cases/codex-x86_64/home/.local/lib/panama/codex/0.165.0/codex-package.json" ]] \ + || fail 'Codex dropped the helpers its package ships beside bin/codex' # Unsupported CPUs stop before curl. RustDesk's reviewed RPM is x86_64-only, # so aarch64 is also an intentional, recorded soft failure without a download. @@ -1511,7 +1514,7 @@ done # Successful updates keep the old version directory and switch only the active # symlink after the replacement binary has passed its version check. for component_spec in 'Bun install_bun .bun/bin/bun .bun/versions/1.4.0/bin/bun' \ - 'Codex install_codex .local/bin/codex .local/lib/panama/codex/0.165.0/codex'; do + 'Codex install_codex .local/bin/codex .local/lib/panama/codex/0.165.0/bin/codex'; do read -r component function_name active_relative target_relative <<<"$component_spec" name="${component,,}-atomic-update" reset_installer_fixture @@ -1528,12 +1531,34 @@ for component_spec in 'Bun install_bun .bun/bin/bun .bun/versions/1.4.0/bin/bun' fi done +# Installs before the package-layout fix kept only a receipted flat +# 0.165.0/codex. That directory is ours, so the full tree replaces it. +name=codex-flat-repair +reset_installer_fixture +STUB_SEED_OLD=Codex expect_success run_installer_function "$name" install_codex +home="$test_tmp/cases/$name/home" +flat="$home/.local/lib/panama/codex/0.165.0" +rm -rf -- "$flat" +mkdir -p -- "$flat" +cp "$artifact_root/codex-x86_64.bin" "$flat/codex" +chmod +x "$flat/codex" +printf 'schema=1\nartifact_sha256=%s\nbinary_sha256=%s\n' \ + e97c4463d7f146692a955d7b0e1c8cf48f97cd4d616cd45c5dcab2e8d9743316 \ + c06a67fd8ac76508dc78bd99ce20c9f317b8d568beda59485f3f4134df9962d5 > "$flat/.panama-provenance" +ln -sfn "$flat/codex" "$home/.local/bin/codex" +STUB_REUSE_CASE=1 expect_success run_installer_function "$name" install_codex +[[ "$(readlink "$home/.local/bin/codex")" == "$flat/bin/codex" \ + && -x "$flat/bin/codex-code-mode-host" && ! -e "$flat/codex" ]] \ + || fail "$name did not replace the flat install with the package tree" +[[ ! -s "$test_tmp/cases/$name/softly-failed" ]] || fail "$name recorded a soft failure" +assert_no_runtime_staging "$name" + # Receipt or executable tampering preserves the directory and leaves an older # active link unchanged. A matching version string is not an attestation. for tamper_spec in \ 'Node install_node .nvm/versions/node/v24.20.0 bin/node v24.20.0' \ 'Bun install_bun .bun/versions/1.4.0 bin/bun 1.4.0' \ - 'Codex install_codex .local/lib/panama/codex/0.165.0 codex codex-cli_0.165.0'; do + 'Codex install_codex .local/lib/panama/codex/0.165.0 bin/codex codex-cli_0.165.0'; do read -r component function_name target_relative binary_relative version_text <<<"$tamper_spec" for tamper_kind in receipt binary; do name="${component,,}-$tamper_kind-tamper"