Fix Codex startup approvals and companion upgrades

This commit is contained in:
Codex
2026-09-05 04:18:41 +00:00
parent 85326d845d
commit b16a9c0ed2
5 changed files with 192 additions and 41 deletions
@@ -1,6 +1,6 @@
---
name: codex-app-server-upgrade
description: Safely check, update, or self-upgrade the host Codex app-server and Codex CLI binary for this project. Use when asked to upgrade Codex, run app-server update checks, restart Codex after an update, or let Codex invoke its own app-server upgrade through the project script.
description: Safely check, update, or self-upgrade the host Codex app-server, Codex CLI binary, and its code-mode host for this project. Use when asked to upgrade Codex, repair or update its code-mode host, run app-server update checks, restart Codex after an update, or let Codex invoke its own app-server upgrade through the project script.
---
# Codex App Server Upgrade
@@ -19,6 +19,14 @@ If the current directory is not the project root, find the nearest repository co
## Commands
Start with automatic approval review when needed:
```bash
scripts/start-codex-app-server start --approve-for-me
```
For a durable setting across restarts and upgrades, set `CODEX_APPROVE_FOR_ME=1` in `.env`.
Check status:
```bash
@@ -41,7 +49,9 @@ scripts/start-codex-app-server check-updates -y
## Self-Upgrade Behavior
When the app-server is running, `check-updates -y` downloads and validates the new Codex binary first, then starts a detached worker to stop the app-server process group, replace the binary, and start the app-server again. This is the correct path even when Codex itself initiated the command.
When the app-server is running, `check-updates -y` downloads and validates matching Codex, code-mode host, and Linux `bwrap` binaries from the same release first. It then starts a detached worker to stop the app-server process group, replace all downloaded components, and start the app-server again. Release archives may contain target-suffixed executable names; the installer normalizes them to their stable installed paths.
If Codex is already current but its sibling `codex-code-mode-host` binary is missing, `check-updates` downloads and installs the checked companion binary. On Linux it also repairs the bundled `bwrap` helper when needed.
Expect the current Codex connection or tool call to be interrupted after the handoff. After a short delay, verify the outcome with:
@@ -57,7 +67,7 @@ sed -n '1,160p' run/codex-app-server-upgrade.log
## Safety Rules
- Do not manually `kill`, `mv`, or overwrite the Codex binary for this workflow.
- Do not manually `kill`, `mv`, or overwrite the Codex or code-mode host binaries for this workflow.
- Do not assume `/usr/local/bin/codex`, a specific home directory, or any machine-specific path.
- Use `CODEX_BIN=/absolute/path/to/codex` only when the user or environment explicitly requires a non-default binary.
- If the script reports that it cannot replace the binary without write permission, stop and report that non-interactive privileges or a writable `CODEX_BIN` are required.
@@ -1,4 +1,4 @@
interface:
display_name: "Codex App Server Upgrade"
short_description: "Safely self-upgrade Codex app-server."
default_prompt: "Safely check for and apply a Codex app-server upgrade using the project script."
short_description: "Safely upgrade Codex and its code-mode host."
default_prompt: "Use $codex-app-server-upgrade to safely update Codex and its code-mode host."
+2
View File
@@ -18,6 +18,8 @@ DB_DIR=./data
# Leave empty to use Codex defaults.
DEFAULT_MODEL=
DEFAULT_SANDBOX=workspace-write
# Set to 1 to route approval requests through Codex automatic review.
CODEX_APPROVE_FOR_ME=0
POLL_TIMEOUT_SECONDS=30
# Container should usually match the host user so SQLite files remain writable.
+1 -1
View File
@@ -11,7 +11,7 @@ Docker Compose runs only the Go Telegram bot. Codex runs on the host through `co
scripts/start-codex-app-server start
```
The script supports `start`, `stop`, `status`, `check-updates [-y]`, and the alias `check-upgrade [-y]`. `start` launches Codex detached, writes `run/codex-app-server.pid`, logs to `run/codex-app-server.log`, and is idempotent if the socket is already live. `check-updates` compares the local `codex` binary with the latest OpenAI Codex GitHub release. With `-y`, it downloads and validates the matching platform archive before stopping a running app-server. If the app-server is running, the final stop/replace/start step is handed to a detached worker so the upgrade can complete even when invoked from Codex itself. If the upgraded server fails to start, the worker restores the previous binary and starts it again.
The script supports `start [--approve-for-me]`, `stop`, `status`, `check-updates [-y]`, and the alias `check-upgrade [-y]`. `start` launches Codex detached, writes `run/codex-app-server.pid`, logs to `run/codex-app-server.log`, and is idempotent if the socket is already live. `--approve-for-me` routes approval requests through Codex automatic review using the `workspace-write` sandbox. Set `CODEX_APPROVE_FOR_ME=1` in `.env` to retain that setting across restarts and upgrades. `check-updates` compares the local `codex` binary with the latest OpenAI Codex GitHub release. With `-y`, it downloads and validates the matching platform archives for Codex, the code-mode host, and Linux `bwrap` before stopping a running app-server. If the app-server is running, the final stop/replace/start step is handed to a detached worker so the upgrade can complete even when invoked from Codex itself. If the upgraded server fails to start, the worker restores the previous components and starts it again.
3. Add at least one Telegram user and workspace:
+174 -35
View File
@@ -28,6 +28,7 @@ read_env_value() {
HOST_CODEX_SOCKET="${HOST_CODEX_SOCKET:-$(read_env_value HOST_CODEX_SOCKET)}"
HOST_CODEX_SOCKET="${HOST_CODEX_SOCKET:-$RUN_DIR/codex.sock}"
CODEX_APPROVE_FOR_ME="${CODEX_APPROVE_FOR_ME:-$(read_env_value CODEX_APPROVE_FOR_ME)}"
mkdir -p "$RUN_DIR"
chmod 700 "$RUN_DIR"
@@ -37,15 +38,17 @@ usage() {
Usage: $0 <start|stop|status|check-updates|check-upgrade> [options]
Commands:
start Start codex app-server if it is not already running.
start [--approve-for-me]
Start codex app-server if it is not already running.
stop Stop codex app-server and remove stale runtime files.
status Print whether codex app-server is running.
check-updates [-y] Check GitHub releases and optionally install the latest Codex binary.
check-updates [-y] Check GitHub releases and optionally install Codex and its companion binaries.
check-upgrade [-y] Alias for check-updates.
Environment:
CODEX_BIN Codex executable to replace. Defaults to the codex found on PATH.
CODEX_RELEASE_REPO GitHub repo for releases. Defaults to openai/codex.
CODEX_APPROVE_FOR_ME Enable automatic approval review when starting Codex (1/true/yes).
USAGE
}
@@ -114,7 +117,13 @@ codex_bin() {
}
start_server() {
local old_pid pid start_codex_bin
local old_pid pid start_codex_bin approve_for_me
approve_for_me="${1:-$CODEX_APPROVE_FOR_ME}"
case "$approve_for_me" in
1|true|yes|on) approve_for_me=1 ;;
0|false|no|off|"") approve_for_me=0 ;;
*) echo "invalid CODEX_APPROVE_FOR_ME value: $approve_for_me" >&2; return 2 ;;
esac
old_pid="$(pid_from_file)"
if [[ -n "$old_pid" ]] && kill -0 "$old_pid" 2>/dev/null; then
if [[ -S "$HOST_CODEX_SOCKET" ]]; then
@@ -145,8 +154,12 @@ start_server() {
tail -f /dev/null > "$1" &
writer=$!
trap "kill $writer 2>/dev/null || true" EXIT
"$4" app-server --listen "$2" < "$1"
' codex-app-server "$STDIN_FIFO" "unix://$HOST_CODEX_SOCKET" "$PID_FILE" "$start_codex_bin" >> "$LOG_FILE" 2>&1
codex_args=(app-server --listen "$2")
if [[ "$5" == "1" ]]; then
codex_args=(--approve-for-me "${codex_args[@]}")
fi
"$4" "${codex_args[@]}" < "$1"
' codex-app-server "$STDIN_FIFO" "unix://$HOST_CODEX_SOCKET" "$PID_FILE" "$start_codex_bin" "$approve_for_me" >> "$LOG_FILE" 2>&1
for _ in $(seq 1 50); do
[[ -f "$PID_FILE" ]] && break
@@ -312,6 +325,7 @@ target, path = sys.argv[1], sys.argv[2]
needs_bwrap = "linux" in target
codex_asset_name = f"codex-{target}.tar.gz"
bwrap_asset_name = f"bwrap-{target}.tar.gz" if needs_bwrap else None
code_mode_host_asset_name = f"codex-code-mode-host-{target}.tar.gz"
with open(path, "r", encoding="utf-8") as f:
release = json.load(f)
tag = release.get("tag_name", "")
@@ -323,7 +337,11 @@ elif version.startswith("v"):
assets = {asset.get("name"): asset for asset in release.get("assets", [])}
codex_asset = assets.get(codex_asset_name)
bwrap_asset = assets.get(bwrap_asset_name) if needs_bwrap else None
required_assets = [(codex_asset_name, codex_asset)]
code_mode_host_asset = assets.get(code_mode_host_asset_name)
required_assets = [
(codex_asset_name, codex_asset),
(code_mode_host_asset_name, code_mode_host_asset),
]
if needs_bwrap:
required_assets.append((bwrap_asset_name, bwrap_asset))
missing = [name for name, asset in required_assets if asset is None]
@@ -338,6 +356,8 @@ if bwrap_asset is not None:
else:
print("")
print("")
print(code_mode_host_asset.get("browser_download_url", ""))
print(code_mode_host_asset.get("digest", ""))
print(version)
print(tag)
PY
@@ -399,6 +419,32 @@ extract_bwrap_binary() {
printf '%s\n' "$found"
}
extract_code_mode_host_binary() {
local archive="$1" dest="$2" found
local -a matches=()
mkdir -p "$dest/code-mode-host-extract"
tar -xzf "$archive" -C "$dest/code-mode-host-extract"
mapfile -d '' -t matches < <(
find "$dest/code-mode-host-extract" -type f \
\( -name codex-code-mode-host -o -name 'codex-code-mode-host-*' \) -print0
)
if [[ "${#matches[@]}" -eq 0 ]]; then
echo "downloaded archive does not contain a code-mode host executable" >&2
return 1
fi
if [[ "${#matches[@]}" -ne 1 ]]; then
echo "downloaded archive contains multiple code-mode host executables" >&2
return 1
fi
found="${matches[0]}"
chmod +x "$found"
if ! "$found" --help >/dev/null 2>&1; then
echo "downloaded code-mode host executable failed validation" >&2
return 1
fi
printf '%s\n' "$found"
}
bundled_bwrap_path() {
local bin="$1"
printf '%s/codex-resources/bwrap\n' "$(dirname "$bin")"
@@ -414,6 +460,17 @@ bwrap_required_for_target() {
[[ "$1" == *linux* ]]
}
code_mode_host_path() {
local bin="$1"
printf '%s/codex-code-mode-host\n' "$(dirname "$bin")"
}
code_mode_host_installed() {
local bin="$1" host
host="$(code_mode_host_path "$bin")"
[[ -x "$host" && ! -L "$host" ]]
}
run_install() {
if [[ "${#INSTALL_PREFIX[@]}" -gt 0 ]]; then
"${INSTALL_PREFIX[@]}" "$@"
@@ -456,8 +513,27 @@ install_bundled_bwrap() {
run_install mv -f "$tmp_new" "$bundled"
}
install_code_mode_host() {
local candidate="$1" bin="$2" backup="$3" host host_dir tmp_new host_backup host_missing
host="$(code_mode_host_path "$bin")"
host_dir="$(dirname "$host")"
tmp_new="$host.new.$$"
host_backup="$backup.code-mode-host"
host_missing="$backup.code-mode-host.missing"
run_install mkdir -p "$host_dir"
if [[ -e "$host" ]]; then
run_install cp -p "$host" "$host_backup"
else
run_install touch "$host_missing"
fi
run_install install -m 0755 "$candidate" "$tmp_new"
run_install mv -f "$tmp_new" "$host"
}
install_candidate() {
local candidate="$1" bwrap_candidate="$2" bin="$3" backup="$4" tmp_new="$bin.new.$$"
local candidate="$1" bwrap_candidate="$2" code_mode_host_candidate="$3" bin="$4" backup="$5" tmp_new
tmp_new="$bin.new.$$"
choose_install_prefix "$bin"
run_install cp -p "$bin" "$backup"
run_install install -m 0755 "$candidate" "$tmp_new"
@@ -465,10 +541,11 @@ install_candidate() {
if [[ -n "$bwrap_candidate" ]]; then
install_bundled_bwrap "$bwrap_candidate" "$bin" "$backup"
fi
install_code_mode_host "$code_mode_host_candidate" "$bin" "$backup"
}
restore_backup() {
local bin="$1" backup="$2" tmp_failed="$bin.failed.$$" bundled bwrap_backup bwrap_missing
local bin="$1" backup="$2" tmp_failed="$bin.failed.$$" bundled bwrap_backup bwrap_missing host host_backup host_missing
if [[ ! -e "$backup" ]]; then
echo "backup missing; cannot restore $bin" >&2
return 1
@@ -489,6 +566,17 @@ restore_backup() {
run_install rm -f "$bundled"
run_install rm -f "$bwrap_missing"
fi
host="$(code_mode_host_path "$bin")"
host_backup="$backup.code-mode-host"
host_missing="$backup.code-mode-host.missing"
if [[ -e "$host_backup" ]]; then
run_install mkdir -p "$(dirname "$host")"
run_install mv -f "$host_backup" "$host"
elif [[ -e "$host_missing" ]]; then
run_install rm -f "$host"
run_install rm -f "$host_missing"
fi
}
confirm_upgrade() {
@@ -506,14 +594,15 @@ confirm_upgrade() {
}
apply_upgrade() {
local candidate="$1" bwrap_candidate="$2" bin="$3" backup="$4" local_version="$5" latest_version="$6" was_running=0
local candidate="$1" bwrap_candidate="$2" code_mode_host_candidate="$3" bin="$4" backup="$5" local_version="$6" latest_version="$7" was_running=0
if is_running; then
was_running=1
stop_server
fi
if ! install_candidate "$candidate" "$bwrap_candidate" "$bin" "$backup"; then
if ! install_candidate "$candidate" "$bwrap_candidate" "$code_mode_host_candidate" "$bin" "$backup"; then
echo "failed to install Codex update" >&2
restore_backup "$bin" "$backup" || true
if [[ "$was_running" == "1" ]]; then
start_server || true
fi
@@ -534,12 +623,12 @@ apply_upgrade() {
}
handoff_upgrade() {
local candidate="$1" bwrap_candidate="$2" bin="$3" backup="$4" update_dir="$5" local_version="$6" latest_version="$7"
local candidate="$1" bwrap_candidate="$2" code_mode_host_candidate="$3" bin="$4" backup="$5" update_dir="$6" local_version="$7" latest_version="$8"
: > "$UPGRADE_LOG_FILE"
setsid -f bash -c '
sleep 1
"$0" __apply-upgrade "$1" "$2" "$3" "$4" "$5" "$6" "$7"
' "$0" "$candidate" "$bwrap_candidate" "$bin" "$backup" "$update_dir" "$local_version" "$latest_version" >> "$UPGRADE_LOG_FILE" 2>&1
"$0" __apply-upgrade "$1" "$2" "$3" "$4" "$5" "$6" "$7" "$8"
' "$0" "$candidate" "$bwrap_candidate" "$code_mode_host_candidate" "$bin" "$backup" "$update_dir" "$local_version" "$latest_version" >> "$UPGRADE_LOG_FILE" 2>&1
echo "Codex upgrade handoff started; app-server will restart if replacement succeeds. log=$UPGRADE_LOG_FILE"
}
@@ -559,8 +648,8 @@ check_updates() {
require_cmd python3
require_cmd ps
local bin local_version target json latest_version latest_tag codex_download_url codex_digest bwrap_download_url bwrap_digest
local codex_archive bwrap_archive tmp candidate bwrap_candidate candidate_version backup
local bin local_version target json latest_version latest_tag codex_download_url codex_digest bwrap_download_url bwrap_digest code_mode_host_download_url code_mode_host_digest
local codex_archive bwrap_archive code_mode_host_archive tmp candidate bwrap_candidate code_mode_host_candidate candidate_version backup missing_bwrap missing_code_mode_host
bin="$(codex_bin)"
if [[ -z "$bin" ]]; then
echo "codex executable not found; set CODEX_BIN" >&2
@@ -585,8 +674,10 @@ check_updates() {
codex_digest="${release_info[1]:-}"
bwrap_download_url="${release_info[2]:-}"
bwrap_digest="${release_info[3]:-}"
latest_version="${release_info[4]:-}"
latest_tag="${release_info[5]:-}"
code_mode_host_download_url="${release_info[4]:-}"
code_mode_host_digest="${release_info[5]:-}"
latest_version="${release_info[6]:-}"
latest_tag="${release_info[7]:-}"
if [[ -z "$latest_version" || -z "$codex_download_url" ]]; then
rm -rf "$tmp"
echo "could not determine latest Codex release for $target" >&2
@@ -597,28 +688,59 @@ check_updates() {
echo "could not determine latest bundled bwrap release for $target" >&2
return 1
fi
if [[ -z "$code_mode_host_download_url" ]]; then
rm -rf "$tmp"
echo "could not determine latest code-mode host release for $target" >&2
return 1
fi
if ! version_gt "$latest_version" "$local_version"; then
if ! bwrap_required_for_target "$target" || bundled_bwrap_installed "$bin"; then
missing_bwrap=0
missing_code_mode_host=0
if bwrap_required_for_target "$target" && ! bundled_bwrap_installed "$bin"; then
missing_bwrap=1
fi
if ! code_mode_host_installed "$bin"; then
missing_code_mode_host=1
fi
if [[ "$missing_bwrap" == "0" && "$missing_code_mode_host" == "0" ]]; then
rm -rf "$tmp"
echo "Codex is already current: $local_version (latest $latest_version)"
return 0
fi
echo "Codex is already current: $local_version (latest $latest_version); installing missing bundled bwrap"
bwrap_archive="$tmp/bwrap-$target.tar.gz"
curl -fL "$bwrap_download_url" -o "$bwrap_archive"
verify_digest "$bwrap_archive" "$bwrap_digest"
bwrap_candidate="$(extract_bwrap_binary "$bwrap_archive" "$tmp")"
echo "Codex is already current: $local_version (latest $latest_version); installing missing companion binaries"
bwrap_candidate=""
code_mode_host_candidate=""
if [[ "$missing_bwrap" == "1" ]]; then
bwrap_archive="$tmp/bwrap-$target.tar.gz"
curl -fL "$bwrap_download_url" -o "$bwrap_archive"
verify_digest "$bwrap_archive" "$bwrap_digest"
bwrap_candidate="$(extract_bwrap_binary "$bwrap_archive" "$tmp")"
fi
if [[ "$missing_code_mode_host" == "1" ]]; then
code_mode_host_archive="$tmp/codex-code-mode-host-$target.tar.gz"
curl -fL "$code_mode_host_download_url" -o "$code_mode_host_archive"
verify_digest "$code_mode_host_archive" "$code_mode_host_digest"
code_mode_host_candidate="$(extract_code_mode_host_binary "$code_mode_host_archive" "$tmp")"
fi
backup="$bin.bak.$(date -u +%Y%m%d%H%M%S)"
choose_install_prefix "$bin"
if install_bundled_bwrap "$bwrap_candidate" "$bin" "$backup"; then
if [[ -n "$bwrap_candidate" ]]; then
install_bundled_bwrap "$bwrap_candidate" "$bin" "$backup"
run_install rm -f "$backup.bwrap.missing"
rm -rf "$tmp"
echo "Bundled bwrap installed: $(bundled_bwrap_path "$bin")"
return 0
fi
if [[ -n "$code_mode_host_candidate" ]]; then
install_code_mode_host "$code_mode_host_candidate" "$bin" "$backup"
run_install rm -f "$backup.code-mode-host.missing"
fi
rm -rf "$tmp"
return 1
if [[ "$missing_bwrap" == "1" ]]; then
echo "Bundled bwrap installed: $(bundled_bwrap_path "$bin")"
fi
if [[ "$missing_code_mode_host" == "1" ]]; then
echo "Code-mode host installed: $(code_mode_host_path "$bin")"
fi
return 0
fi
echo "Codex update available: $local_version -> $latest_version ($latest_tag)"
confirm_upgrade "$local_version" "$latest_version" "$bin"
@@ -635,6 +757,10 @@ check_updates() {
else
bwrap_candidate=""
fi
code_mode_host_archive="$tmp/codex-code-mode-host-$target.tar.gz"
curl -fL "$code_mode_host_download_url" -o "$code_mode_host_archive"
verify_digest "$code_mode_host_archive" "$code_mode_host_digest"
code_mode_host_candidate="$(extract_code_mode_host_binary "$code_mode_host_archive" "$tmp")"
candidate_version="$(codex_version_from "$candidate")"
if [[ "$candidate_version" != "$latest_version" ]]; then
rm -rf "$tmp"
@@ -646,11 +772,11 @@ check_updates() {
choose_install_prefix "$bin"
if is_running; then
handoff_upgrade "$candidate" "$bwrap_candidate" "$bin" "$backup" "$tmp" "$local_version" "$latest_version"
handoff_upgrade "$candidate" "$bwrap_candidate" "$code_mode_host_candidate" "$bin" "$backup" "$tmp" "$local_version" "$latest_version"
return 0
fi
if apply_upgrade "$candidate" "$bwrap_candidate" "$bin" "$backup" "$local_version" "$latest_version"; then
if apply_upgrade "$candidate" "$bwrap_candidate" "$code_mode_host_candidate" "$bin" "$backup" "$local_version" "$latest_version"; then
rm -rf "$tmp"
return 0
fi
@@ -659,7 +785,7 @@ check_updates() {
}
apply_upgrade_worker() {
local candidate="$1" bwrap_candidate="$2" bin="$3" backup="$4" update_dir="$5" local_version="$6" latest_version="$7" rc=0
local candidate="$1" bwrap_candidate="$2" code_mode_host_candidate="$3" bin="$4" backup="$5" update_dir="$6" local_version="$7" latest_version="$8" rc=0
if [[ ! -x "$candidate" ]]; then
echo "upgrade candidate is missing or not executable: $candidate" >&2
rm -rf "$update_dir"
@@ -670,7 +796,12 @@ apply_upgrade_worker() {
rm -rf "$update_dir"
return 1
fi
if ! apply_upgrade "$candidate" "$bwrap_candidate" "$bin" "$backup" "$local_version" "$latest_version"; then
if [[ ! -x "$code_mode_host_candidate" ]]; then
echo "code-mode host candidate is missing or not executable: $code_mode_host_candidate" >&2
rm -rf "$update_dir"
return 1
fi
if ! apply_upgrade "$candidate" "$bwrap_candidate" "$code_mode_host_candidate" "$bin" "$backup" "$local_version" "$latest_version"; then
rc=1
fi
rm -rf "$update_dir"
@@ -681,8 +812,16 @@ cmd="${1:-help}"
case "$cmd" in
start)
shift || true
if [[ $# -ne 0 ]]; then usage; exit 2; fi
start_server
start_approve_for_me="$CODEX_APPROVE_FOR_ME"
while [[ $# -gt 0 ]]; do
case "$1" in
--approve-for-me) start_approve_for_me=1 ;;
-h|--help) usage; exit 0 ;;
*) echo "unknown start option: $1" >&2; usage; exit 2 ;;
esac
shift
done
start_server "$start_approve_for_me"
;;
stop)
shift || true
@@ -700,7 +839,7 @@ case "$cmd" in
;;
__apply-upgrade)
shift || true
if [[ $# -ne 7 ]]; then echo "invalid upgrade worker arguments" >&2; exit 2; fi
if [[ $# -ne 8 ]]; then echo "invalid upgrade worker arguments" >&2; exit 2; fi
apply_upgrade_worker "$@"
;;
-h|--help|help)