Expand codex app-server script

This commit is contained in:
Codex
2026-05-25 04:10:13 +00:00
parent e85d0eb928
commit d03bf33a55
2 changed files with 443 additions and 54 deletions

View File

@@ -8,10 +8,10 @@ Docker Compose runs only the Go Telegram bot. Codex runs on the host through `co
2. Start the host Codex app-server: 2. Start the host Codex app-server:
```sh ```sh
scripts/start-codex-app-server scripts/start-codex-app-server start
``` ```
The script starts Codex detached, writes `run/codex-app-server.pid`, logs to `run/codex-app-server.log`, and is idempotent if the socket is already live. The script supports `start`, `stop`, `status`, and `check-updates [-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 the matching platform archive, verifies the release digest when available, replaces the configured `CODEX_BIN`, and restarts the app-server if it was running. If the upgraded server fails to start, the script restores the previous binary and starts it again.
3. Add at least one Telegram user and workspace: 3. Add at least one Telegram user and workspace:

View File

@@ -7,6 +7,8 @@ RUN_DIR="$ROOT/run"
PID_FILE="$RUN_DIR/codex-app-server.pid" PID_FILE="$RUN_DIR/codex-app-server.pid"
LOG_FILE="$RUN_DIR/codex-app-server.log" LOG_FILE="$RUN_DIR/codex-app-server.log"
STDIN_FIFO="$RUN_DIR/codex-app-server.stdin" STDIN_FIFO="$RUN_DIR/codex-app-server.stdin"
CODEX_RELEASE_REPO="${CODEX_RELEASE_REPO:-openai/codex}"
INSTALL_PREFIX=()
read_env_value() { read_env_value() {
local key="$1" local key="$1"
@@ -21,74 +23,461 @@ HOST_CODEX_SOCKET="${HOST_CODEX_SOCKET:-$RUN_DIR/codex.sock}"
mkdir -p "$RUN_DIR" mkdir -p "$RUN_DIR"
chmod 700 "$RUN_DIR" chmod 700 "$RUN_DIR"
if [[ -f "$PID_FILE" ]]; then usage() {
old_pid="$(tr -cd '0-9' < "$PID_FILE" || true)" cat <<USAGE
Usage: $0 <start|stop|status|check-updates> [options]
Commands:
start 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.
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.
USAGE
}
pid_from_file() {
tr -cd '0-9' < "$PID_FILE" 2>/dev/null || true
}
server_pid() {
local pid
pid="$(pid_from_file)"
if [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then
printf '%s\n' "$pid"
fi
}
is_running() {
[[ -n "$(server_pid)" ]]
}
remove_socket_if_safe() {
if [[ -e "$HOST_CODEX_SOCKET" ]]; then
if [[ -S "$HOST_CODEX_SOCKET" ]]; then
rm -f "$HOST_CODEX_SOCKET"
else
echo "socket path exists and is not a Unix socket: $HOST_CODEX_SOCKET" >&2
return 1
fi
fi
}
show_log_excerpt() {
sed -n '1,120p' "$LOG_FILE" >&2 || true
}
start_server() {
local old_pid pid start_codex_bin
old_pid="$(pid_from_file)"
if [[ -n "$old_pid" ]] && kill -0 "$old_pid" 2>/dev/null; then if [[ -n "$old_pid" ]] && kill -0 "$old_pid" 2>/dev/null; then
if [[ -S "$HOST_CODEX_SOCKET" ]]; then if [[ -S "$HOST_CODEX_SOCKET" ]]; then
echo "codex app-server already running: pid=$old_pid socket=$HOST_CODEX_SOCKET" echo "codex app-server already running: pid=$old_pid socket=$HOST_CODEX_SOCKET"
exit 0 return 0
fi fi
echo "pid $old_pid is running but socket is missing; refusing to start a second app-server" >&2 echo "pid $old_pid is running but socket is missing; refusing to start a second app-server" >&2
exit 1 return 1
fi fi
rm -f "$PID_FILE" rm -f "$PID_FILE"
fi remove_socket_if_safe
if [[ -e "$HOST_CODEX_SOCKET" ]]; then rm -f "$STDIN_FIFO"
if [[ -S "$HOST_CODEX_SOCKET" ]]; then mkfifo "$STDIN_FIFO"
rm -f "$HOST_CODEX_SOCKET" chmod 600 "$STDIN_FIFO"
else
echo "socket path exists and is not a Unix socket: $HOST_CODEX_SOCKET" >&2 : > "$LOG_FILE"
exit 1 # Codex app-server currently exits if detached with stdin closed. A detached
# wrapper keeps a private FIFO writer open and then runs Codex on the host.
start_codex_bin="$(codex_bin)"
setsid -f bash -c '
echo "$$" > "$3"
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
for _ in $(seq 1 50); do
[[ -f "$PID_FILE" ]] && break
sleep 0.1
done
pid="$(pid_from_file)"
if [[ -z "$pid" ]]; then
echo "codex app-server did not write a pid file; log follows:" >&2
show_log_excerpt
return 1
fi fi
fi
rm -f "$STDIN_FIFO" for _ in $(seq 1 100); do
mkfifo "$STDIN_FIFO" if [[ -S "$HOST_CODEX_SOCKET" ]]; then
chmod 600 "$STDIN_FIFO" sleep 0.5
if kill -0 "$pid" 2>/dev/null; then
echo "codex app-server started: pid=$pid socket=$HOST_CODEX_SOCKET log=$LOG_FILE"
return 0
fi
fi
if ! kill -0 "$pid" 2>/dev/null; then
echo "codex app-server exited before staying ready; log follows:" >&2
show_log_excerpt
rm -f "$PID_FILE"
return 1
fi
sleep 0.1
done
: > "$LOG_FILE" echo "codex app-server did not create socket within 10 seconds; log follows:" >&2
# Codex app-server currently exits if detached with stdin closed. A detached show_log_excerpt
# wrapper keeps a private FIFO writer open and then runs Codex on the host. return 1
setsid -f bash -c ' }
echo "$$" > "$3"
tail -f /dev/null > "$1" &
writer=$!
trap "kill $writer 2>/dev/null || true" EXIT
codex app-server --listen "$2" < "$1"
' codex-app-server "$STDIN_FIFO" "unix://$HOST_CODEX_SOCKET" "$PID_FILE" >> "$LOG_FILE" 2>&1
for _ in $(seq 1 50); do stop_server() {
local pid
pid="$(server_pid)"
if [[ -z "$pid" ]]; then
rm -f "$PID_FILE" "$STDIN_FIFO"
remove_socket_if_safe
echo "codex app-server is not running"
return 0
fi
echo "stopping codex app-server: pid=$pid"
kill "$pid" 2>/dev/null || true
for _ in $(seq 1 50); do
if ! kill -0 "$pid" 2>/dev/null; then
rm -f "$PID_FILE" "$STDIN_FIFO"
remove_socket_if_safe
echo "codex app-server stopped"
return 0
fi
sleep 0.1
done
echo "codex app-server did not stop after 5 seconds; killing pid=$pid" >&2
kill -KILL "$pid" 2>/dev/null || true
for _ in $(seq 1 20); do
if ! kill -0 "$pid" 2>/dev/null; then
rm -f "$PID_FILE" "$STDIN_FIFO"
remove_socket_if_safe
echo "codex app-server stopped"
return 0
fi
sleep 0.1
done
echo "failed to stop codex app-server pid=$pid" >&2
return 1
}
status_server() {
local pid
pid="$(server_pid)"
if [[ -n "$pid" ]]; then
if [[ -S "$HOST_CODEX_SOCKET" ]]; then
echo "codex app-server running: pid=$pid socket=$HOST_CODEX_SOCKET log=$LOG_FILE"
return 0
fi
echo "codex app-server pid=$pid is running but socket is missing: $HOST_CODEX_SOCKET" >&2
return 2
fi
if [[ -f "$PID_FILE" ]]; then if [[ -f "$PID_FILE" ]]; then
break echo "codex app-server not running; stale pid file: $PID_FILE" >&2
return 1
fi fi
sleep 0.1 echo "codex app-server not running"
done return 1
}
pid="$(tr -cd '0-9' < "$PID_FILE" 2>/dev/null || true)" require_cmd() {
if [[ -z "$pid" ]]; then if ! command -v "$1" >/dev/null 2>&1; then
echo "codex app-server did not write a pid file; log follows:" >&2 echo "missing required command: $1" >&2
sed -n '1,120p' "$LOG_FILE" >&2 || true return 1
exit 1 fi
fi }
for _ in $(seq 1 100); do codex_bin() {
if [[ -S "$HOST_CODEX_SOCKET" ]]; then if [[ -n "${CODEX_BIN:-}" ]]; then
sleep 0.5 printf '%s\n' "$CODEX_BIN"
if kill -0 "$pid" 2>/dev/null; then return 0
echo "codex app-server started: pid=$pid socket=$HOST_CODEX_SOCKET log=$LOG_FILE" fi
exit 0 command -v codex
}
codex_version_from() {
local bin="$1" line
line="$($bin --version 2>/dev/null || true)"
printf '%s\n' "$line" | sed -n 's/.*\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\).*/\1/p' | head -n 1
}
release_target() {
local os arch
os="$(uname -s)"
arch="$(uname -m)"
case "$os:$arch" in
Linux:x86_64|Linux:amd64) echo "x86_64-unknown-linux-musl" ;;
Linux:aarch64|Linux:arm64) echo "aarch64-unknown-linux-musl" ;;
Darwin:x86_64|Darwin:amd64) echo "x86_64-apple-darwin" ;;
Darwin:aarch64|Darwin:arm64) echo "aarch64-apple-darwin" ;;
*) echo "unsupported platform for Codex binary release: $os $arch" >&2; return 1 ;;
esac
}
version_gt() {
python3 - "$1" "$2" <<'PY'
import sys
def parse(v):
return tuple(int(p) for p in v.split('.')[:3])
sys.exit(0 if parse(sys.argv[1]) > parse(sys.argv[2]) else 1)
PY
}
latest_release_info() {
local target="$1" json_file="$2"
python3 - "$target" "$json_file" <<'PY'
import json
import sys
target, path = sys.argv[1], sys.argv[2]
asset_name = f"codex-{target}.tar.gz"
with open(path, "r", encoding="utf-8") as f:
release = json.load(f)
tag = release.get("tag_name", "")
version = tag
if version.startswith("rust-v"):
version = version[6:]
elif version.startswith("v"):
version = version[1:]
for asset in release.get("assets", []):
if asset.get("name") == asset_name:
print(version)
print(tag)
print(asset.get("browser_download_url", ""))
print(asset.get("digest", ""))
raise SystemExit(0)
print(f"release {tag or '<unknown>'} has no asset named {asset_name}", file=sys.stderr)
raise SystemExit(1)
PY
}
verify_digest() {
local file="$1" digest="$2" expected
if [[ -z "$digest" ]]; then
echo "release asset has no digest; skipping checksum verification" >&2
return 0
fi
if [[ "$digest" != sha256:* ]]; then
echo "unsupported release digest format: $digest" >&2
return 1
fi
expected="${digest#sha256:}"
if command -v sha256sum >/dev/null 2>&1; then
printf '%s %s\n' "$expected" "$file" | sha256sum -c - >/dev/null
elif command -v shasum >/dev/null 2>&1; then
local actual
actual="$(shasum -a 256 "$file" | awk '{print $1}')"
[[ "$actual" == "$expected" ]]
else
echo "missing sha256sum or shasum for checksum verification" >&2
return 1
fi
}
extract_codex_binary() {
local archive="$1" dest="$2" found
mkdir -p "$dest/extract"
tar -xzf "$archive" -C "$dest/extract"
found="$(find "$dest/extract" -type f -name codex -print | head -n 1)"
if [[ -z "$found" ]]; then
echo "downloaded archive does not contain a codex binary" >&2
return 1
fi
chmod +x "$found"
printf '%s\n' "$found"
}
run_install() {
if [[ "${#INSTALL_PREFIX[@]}" -gt 0 ]]; then
"${INSTALL_PREFIX[@]}" "$@"
else
"$@"
fi
}
choose_install_prefix() {
local bin="$1" dir
dir="$(dirname "$bin")"
INSTALL_PREFIX=()
if [[ -w "$dir" && ( ! -e "$bin" || -w "$bin" ) ]]; then
return 0
fi
if command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then
INSTALL_PREFIX=(sudo)
return 0
fi
echo "cannot replace $bin without write permission" >&2
echo "Run with suitable privileges or set CODEX_BIN to a user-writable Codex binary path." >&2
return 1
}
install_candidate() {
local candidate="$1" bin="$2" backup="$3" tmp_new="$bin.new.$$"
choose_install_prefix "$bin"
run_install cp -p "$bin" "$backup"
run_install install -m 0755 "$candidate" "$tmp_new"
run_install mv -f "$tmp_new" "$bin"
}
restore_backup() {
local bin="$1" backup="$2" tmp_failed="$bin.failed.$$"
if [[ ! -e "$backup" ]]; then
echo "backup missing; cannot restore $bin" >&2
return 1
fi
choose_install_prefix "$bin"
if [[ -e "$bin" ]]; then
run_install mv "$bin" "$tmp_failed" || true
fi
run_install mv "$backup" "$bin"
}
confirm_upgrade() {
local local_version="$1" latest_version="$2" bin="$3"
if [[ "${ASSUME_YES:-0}" == "1" ]]; then
return 0
fi
if [[ ! -t 0 ]]; then
echo "Codex $latest_version is available for $bin; rerun with check-updates -y to upgrade." >&2
return 2
fi
local reply
read -r -p "Upgrade Codex $local_version -> $latest_version at $bin? [y/N] " reply
[[ "$reply" == "y" || "$reply" == "Y" || "$reply" == "yes" || "$reply" == "YES" ]]
}
check_updates() {
ASSUME_YES=0
while [[ $# -gt 0 ]]; do
case "$1" in
-y|--yes) ASSUME_YES=1 ;;
-h|--help) usage; return 0 ;;
*) echo "unknown check-updates option: $1" >&2; usage; return 2 ;;
esac
shift
done
require_cmd curl
require_cmd tar
require_cmd python3
local bin local_version target json latest_version latest_tag download_url digest archive tmp candidate candidate_version was_running backup
bin="$(codex_bin)"
if [[ -z "$bin" ]]; then
echo "codex executable not found; set CODEX_BIN" >&2
return 1
fi
if [[ "$bin" != /* ]]; then
echo "CODEX_BIN must be an absolute path: $bin" >&2
return 1
fi
local_version="$(codex_version_from "$bin")"
if [[ -z "$local_version" ]]; then
echo "could not determine local Codex version from $bin" >&2
return 1
fi
target="$(release_target)"
tmp="$(mktemp -d "$RUN_DIR/codex-update.XXXXXX")"
trap "rm -rf '$tmp'" EXIT
json="$tmp/latest.json"
curl -fsSL "https://api.github.com/repos/$CODEX_RELEASE_REPO/releases/latest" -o "$json"
mapfile -t release_info < <(latest_release_info "$target" "$json")
latest_version="${release_info[0]:-}"
latest_tag="${release_info[1]:-}"
download_url="${release_info[2]:-}"
digest="${release_info[3]:-}"
if [[ -z "$latest_version" || -z "$download_url" ]]; then
echo "could not determine latest Codex release for $target" >&2
return 1
fi
if ! version_gt "$latest_version" "$local_version"; then
echo "Codex is already current: $local_version (latest $latest_version)"
return 0
fi
echo "Codex update available: $local_version -> $latest_version ($latest_tag)"
confirm_upgrade "$local_version" "$latest_version" "$bin"
archive="$tmp/codex-$target.tar.gz"
curl -fL "$download_url" -o "$archive"
verify_digest "$archive" "$digest"
candidate="$(extract_codex_binary "$archive" "$tmp")"
candidate_version="$(codex_version_from "$candidate")"
if [[ "$candidate_version" != "$latest_version" ]]; then
echo "downloaded Codex version $candidate_version does not match release $latest_version" >&2
return 1
fi
was_running=0
if is_running; then
was_running=1
fi
backup="$bin.bak.$(date -u +%Y%m%d%H%M%S)"
if [[ "$was_running" == "1" ]]; then
stop_server
fi
if ! install_candidate "$candidate" "$bin" "$backup"; then
if [[ "$was_running" == "1" ]]; then
start_server || true
fi
return 1
fi
if [[ "$was_running" == "1" ]]; then
if ! start_server; then
echo "new Codex failed to start; restoring previous binary" >&2
restore_backup "$bin" "$backup" || true
start_server || true
return 1
fi fi
fi fi
if ! kill -0 "$pid" 2>/dev/null; then
echo "codex app-server exited before staying ready; log follows:" >&2
sed -n '1,120p' "$LOG_FILE" >&2 || true
rm -f "$PID_FILE"
exit 1
fi
sleep 0.1
done
echo "codex app-server did not create socket within 10 seconds; log follows:" >&2 echo "Codex upgraded: $local_version -> $latest_version"
sed -n '1,120p' "$LOG_FILE" >&2 || true echo "backup: $backup"
exit 1 }
cmd="${1:-start}"
case "$cmd" in
start)
shift || true
if [[ $# -ne 0 ]]; then usage; exit 2; fi
start_server
;;
stop)
shift || true
if [[ $# -ne 0 ]]; then usage; exit 2; fi
stop_server
;;
status)
shift || true
if [[ $# -ne 0 ]]; then usage; exit 2; fi
status_server
;;
check-updates)
shift || true
check_updates "$@"
;;
-h|--help|help)
usage
;;
*)
echo "unknown command: $cmd" >&2
usage >&2
exit 2
;;
esac