|
| 1 | +#!/bin/bash |
| 2 | +# Shim to launch the user-installed (or auto-downloaded) codex CLI. |
| 3 | +# |
| 4 | +# Problems this solves in strict confinement: |
| 5 | +# 1. lemonade-server sets HOME=$SNAP_COMMON, so codex would look for its |
| 6 | +# config in the wrong place. We derive the real home from SNAP_USER_COMMON |
| 7 | +# and restore it before exec-ing codex. |
| 8 | +# 2. If codex isn't installed yet, we download the platform-specific |
| 9 | +# @openai/codex tarball from the npm registry using curl and extract the |
| 10 | +# statically-linked native binary directly — no Node.js required. |
| 11 | + |
| 12 | +CODEX_BIN="$SNAP_USER_COMMON/tools/bin/codex" |
| 13 | + |
| 14 | +_install_codex() { |
| 15 | + echo "codex not found at $CODEX_BIN" >&2 |
| 16 | + echo "Downloading @openai/codex from the npm registry..." >&2 |
| 17 | + |
| 18 | + mkdir -p "$SNAP_USER_COMMON/tools/bin" |
| 19 | + |
| 20 | + # Resolve the latest version tag and integrity from npm metadata |
| 21 | + local metadata version integrity |
| 22 | + metadata=$(curl -sf "https://registry.npmjs.org/@openai/codex/latest") |
| 23 | + version=$(printf '%s\n' "$metadata" | sed -n 's/.*"version":"\([^"]*\)".*/\1/p' | head -1) |
| 24 | + # Extract the dist.integrity field (e.g., "sha512-<base64>") for the resolved version |
| 25 | + integrity=$(printf '%s\n' "$metadata" | sed -n 's/.*"integrity":"\([^"]*\)".*/\1/p' | head -1) |
| 26 | + |
| 27 | + if [ -z "$version" ] || [ -z "$integrity" ]; then |
| 28 | + echo "ERROR: could not resolve @openai/codex version and integrity from npm registry." >&2 |
| 29 | + echo "Check your network connection, or install manually (after verifying checksum):" >&2 |
| 30 | + echo " curl -fsSL https://registry.npmjs.org/@openai/codex/-/codex-VERSION-linux-x64.tgz \\" >&2 |
| 31 | + echo " | tar -xzOf - package/vendor/x86_64-unknown-linux-musl/codex/codex \\" >&2 |
| 32 | + echo " > '$CODEX_BIN' && chmod +x '$CODEX_BIN'" >&2 |
| 33 | + exit 1 |
| 34 | + fi |
| 35 | + |
| 36 | + echo "Downloading codex v${version} (linux-x64 native binary)..." >&2 |
| 37 | + local tarball="https://registry.npmjs.org/@openai/codex/-/codex-${version}-linux-x64.tgz" |
| 38 | + |
| 39 | + # Download tarball to a temporary file so we can verify its integrity. |
| 40 | + local tmp_tgz |
| 41 | + tmp_tgz=$(mktemp "${TMPDIR:-/tmp}/codex-tarball.XXXXXX") || { |
| 42 | + echo "ERROR: failed to create temporary file for @openai/codex download." >&2 |
| 43 | + exit 1 |
| 44 | + } |
| 45 | + |
| 46 | + if ! curl -fsSL "$tarball" -o "$tmp_tgz"; then |
| 47 | + echo "ERROR: download of @openai/codex v${version}-linux-x64 failed." >&2 |
| 48 | + rm -f "$tmp_tgz" |
| 49 | + rm -f "$CODEX_BIN" |
| 50 | + exit 1 |
| 51 | + fi |
| 52 | + |
| 53 | + # Verify tarball integrity against the npm dist.integrity field (sha512-<base64>). |
| 54 | + local expected_prefix="sha512-" |
| 55 | + local expected actual |
| 56 | + expected="$integrity" |
| 57 | + if [ "${expected#"$expected_prefix"}" = "$expected" ]; then |
| 58 | + echo "ERROR: unsupported integrity format '$integrity' (expected sha512-*)." >&2 |
| 59 | + rm -f "$tmp_tgz" |
| 60 | + rm -f "$CODEX_BIN" |
| 61 | + exit 1 |
| 62 | + fi |
| 63 | + expected="${expected#"$expected_prefix"}" |
| 64 | + |
| 65 | + # Compute base64-encoded SHA-512 of the downloaded tarball. |
| 66 | + if ! actual=$(openssl dgst -sha512 -binary "$tmp_tgz" 2>/dev/null | openssl base64 -A); then |
| 67 | + echo "ERROR: failed to compute SHA-512 checksum for @openai/codex tarball." >&2 |
| 68 | + rm -f "$tmp_tgz" |
| 69 | + rm -f "$CODEX_BIN" |
| 70 | + exit 1 |
| 71 | + fi |
| 72 | + |
| 73 | + if [ "$actual" != "$expected" ]; then |
| 74 | + echo "ERROR: integrity check failed for @openai/codex v${version}-linux-x64 tarball." >&2 |
| 75 | + echo "Expected (from npm dist.integrity): $expected" >&2 |
| 76 | + echo "Actual (computed): $actual" >&2 |
| 77 | + rm -f "$tmp_tgz" |
| 78 | + rm -f "$CODEX_BIN" |
| 79 | + exit 1 |
| 80 | + fi |
| 81 | + |
| 82 | + # The linux-x64 tarball ships a statically-linked musl binary; extract it directly. |
| 83 | + if ! tar -xzOf "$tmp_tgz" package/vendor/x86_64-unknown-linux-musl/codex/codex \ |
| 84 | + > "$CODEX_BIN"; then |
| 85 | + echo "ERROR: failed to extract @openai/codex v${version}-linux-x64 binary." >&2 |
| 86 | + rm -f "$tmp_tgz" |
| 87 | + rm -f "$CODEX_BIN" |
| 88 | + exit 1 |
| 89 | + fi |
| 90 | + |
| 91 | + rm -f "$tmp_tgz" |
| 92 | + chmod +x "$CODEX_BIN" |
| 93 | + echo "codex v${version} installed to $CODEX_BIN" >&2 |
| 94 | +} |
| 95 | + |
| 96 | +if [ ! -x "$CODEX_BIN" ]; then |
| 97 | + _install_codex |
| 98 | +fi |
| 99 | + |
| 100 | +# OPENAI_BASE_URL is set by lemonade-server to the server's /v1/ endpoint. |
| 101 | +# Forward it as CODEX_OSS_BASE_URL so codex points at lemonade instead of |
| 102 | +# the default Ollama port, and inject -c oss_provider=ollama so the |
| 103 | +# LM Studio/Ollama picker is skipped. |
| 104 | +CODEX_OSS_BASE_URL="${OPENAI_BASE_URL%/}" |
| 105 | +export CODEX_OSS_BASE_URL |
| 106 | + |
| 107 | +# Extract the model name passed via -m / --model so we can look up the actual |
| 108 | +# context window from the running lemonade server. |
| 109 | +MODEL_NAME="" |
| 110 | +for arg in "$@"; do |
| 111 | + if [ -n "$_NEXT_IS_MODEL" ]; then |
| 112 | + MODEL_NAME="$arg" |
| 113 | + break |
| 114 | + fi |
| 115 | + case "$arg" in |
| 116 | + -m|--model) _NEXT_IS_MODEL=1 ;; |
| 117 | + -m=*|--model=*) MODEL_NAME="${arg#*=}"; break ;; |
| 118 | + esac |
| 119 | +done |
| 120 | + |
| 121 | +# Query /api/v1/health for the loaded model's actual ctx_size and derive a |
| 122 | +# sensible tool_output_token_limit (1/8 of context window, min 8192). |
| 123 | +CONTEXT_ARGS=() |
| 124 | +if [ -n "$MODEL_NAME" ] && [ -n "$OPENAI_BASE_URL" ]; then |
| 125 | + SERVER_BASE="${OPENAI_BASE_URL%%/v1*}" |
| 126 | + ctx_size=$(curl -sf "$SERVER_BASE/api/v1/health" 2>/dev/null \ |
| 127 | + | jq -r --arg model "$MODEL_NAME" \ |
| 128 | + '.all_models_loaded[]? | select(.model_name == $model) | .recipe_options.ctx_size // empty' \ |
| 129 | + 2>/dev/null | head -n1) |
| 130 | + if [ -n "$ctx_size" ] && [ "$ctx_size" -gt 0 ] 2>/dev/null; then |
| 131 | + tool_limit=$(( ctx_size / 8 )) |
| 132 | + [ "$tool_limit" -lt 8192 ] && tool_limit=8192 |
| 133 | + CONTEXT_ARGS=( |
| 134 | + -c "model_context_window=$ctx_size" |
| 135 | + -c "model_auto_compact_token_limit=$tool_limit" |
| 136 | + ) |
| 137 | + fi |
| 138 | +fi |
| 139 | + |
| 140 | +# Use SNAP_USER_COMMON as HOME so codex stores its config within the snap's |
| 141 | +# user data directory (~/.codex → $SNAP_USER_COMMON/.codex), which is |
| 142 | +# accessible under strict confinement without a personal-files plug. |
| 143 | +exec env HOME="$SNAP_USER_COMMON" "$CODEX_BIN" \ |
| 144 | + -c "oss_provider=\"ollama\"" "${CONTEXT_ARGS[@]}" "$@" |
0 commit comments