Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 37 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ ln -s "$PWD/scripts/codexcli-strict" ~/bin/codexcli-strict
```

The wrappers keep Codex state in
`~/.local/share/codexcli/codex-home`. Set `CODEXCLI_IMAGE` to use a different
image tag.
`~/.local/share/codexcli/codex-home`. The default runtime image is generic and
unchanged: `eigen-codexcli:latest`.

The image includes Poppler PDF text extraction tools such as `pdftotext`,
`pdfinfo`, and `pdftohtml`.
Expand Down Expand Up @@ -55,8 +55,43 @@ and `--instance=NAME` override `CODEXCLI_INSTANCE`. If a future Codex CLI
argument is named `--instance`, pass it after `--`, for example
`codexcli -- --instance value`.

## Project runtime images

Use an explicit runtime image when a repository needs project-specific tools or
dependencies inside Codex:

```sh
CODEXCLI_IMAGE=my-eigencv-runtime codexcli
codexcli --image my-eigencv-runtime
codexcli --image my-eigencv-runtime --instance render
```

Image selection precedence is:

1. `--image` overrides `CODEXCLI_IMAGE`.
2. `CODEXCLI_IMAGE` overrides the default.
3. The default remains `eigen-codexcli:latest`.

Wrapper flags such as `--image`, `--instance`, and `--doctor` must appear
before Codex arguments. If a future Codex CLI argument is named `--image`, pass
it after `--`, for example `codexcli -- --image value`.

All other positional arguments are passed unchanged to Codex.

## Troubleshooting project tools

If a repository tool fails with `ModuleNotFoundError` or a missing binary, stop.
Do not create fake dependency shims, and do not install project dependencies
into the generic runtime. Validate the tool on the host or use an explicit
project runtime image.

Non-goals for the generic runtime:

- no automatic `pip`, `npm`, or `apt` installs
- no host `.venv` mounting by default
- no Nix or devcontainer build support yet
- no fake dependency shims

## Repositories and worktrees

Run a wrapper from anywhere inside a Git checkout. The repository root is
Expand Down
7 changes: 7 additions & 0 deletions docs/runtime-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,13 @@ PDF migration workflows must extract text from the actual source PDFs. If
`pdftotext` is unavailable, or extraction is empty or unusable, Codex must stop
rather than reconstructing content from memory.

## Project runtime images

Project images are the supported way to run repo-specific tools inside Codex.
The generic runtime intentionally does not include every project's dependencies,
and project images should be explicit and reviewable. Missing dependencies
should fail loudly instead of causing shims or local workarounds.

## Git hooks and repository trust

The runtime sets `core.hooksPath=/dev/null` inside the container so Git commands
Expand Down
2 changes: 1 addition & 1 deletion scripts/codexcli
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ SCRIPT_DIR="$(cd -- "$(dirname -- "$SCRIPT_PATH")" && pwd)"
# shellcheck source=scripts/lib-codexcli-runtime.sh
source "$SCRIPT_DIR/lib-codexcli-runtime.sh"

CODEXCLI_IMAGE="${CODEXCLI_IMAGE:-eigen-codexcli:latest}"
CODEXCLI_DEFAULT_IMAGE="eigen-codexcli:latest"
codexcli_run "$@"
2 changes: 1 addition & 1 deletion scripts/codexcli-login
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ SCRIPT_DIR="$(cd -- "$(dirname -- "$SCRIPT_PATH")" && pwd)"
# shellcheck source=scripts/lib-codexcli-runtime.sh
source "$SCRIPT_DIR/lib-codexcli-runtime.sh"

CODEXCLI_IMAGE="${CODEXCLI_IMAGE:-eigen-codexcli:latest}"
CODEXCLI_DEFAULT_IMAGE="eigen-codexcli:latest"
# CODEXCLI_LOGIN_PORT is consumed by functions in lib-codexcli-runtime.sh.
# shellcheck disable=SC2034
CODEXCLI_LOGIN_PORT=true
Expand Down
2 changes: 1 addition & 1 deletion scripts/codexcli-strict
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ SCRIPT_DIR="$(cd -- "$(dirname -- "$SCRIPT_PATH")" && pwd)"
# shellcheck source=scripts/lib-codexcli-runtime.sh
source "$SCRIPT_DIR/lib-codexcli-runtime.sh"

CODEXCLI_IMAGE="${CODEXCLI_IMAGE:-eigen-codexcli:latest}"
CODEXCLI_DEFAULT_IMAGE="eigen-codexcli:latest"
# CODEXCLI_STRICT is consumed by functions in lib-codexcli-runtime.sh.
# shellcheck disable=SC2034
CODEXCLI_STRICT=true
Expand Down
91 changes: 88 additions & 3 deletions scripts/lib-codexcli-runtime.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,14 @@ codexcli_validate_instance() {
esac
}

codexcli_validate_image() {
case "$1" in
''|*[[:space:][:cntrl:]]*)
codexcli_die "invalid image: must not be empty or contain whitespace/control characters"
;;
esac
}

codexcli_effective_container_name() {
if [ -n "$CODEXCLI_INSTANCE" ]; then
printf '%s-%s\n' "$CODEXCLI_BASE_CONTAINER_NAME" "$CODEXCLI_INSTANCE"
Expand Down Expand Up @@ -68,11 +76,37 @@ codexcli_require_git_identity() {
fi
}

codexcli_project_env_files() {
local file found=''
local -a files=(
requirements.txt
pyproject.toml
flake.nix
.devcontainer/devcontainer.json
Containerfile
)

for file in "${files[@]}"; do
if [ -e "$CODEXCLI_WORKSPACE/$file" ]; then
if [ -n "$found" ]; then
found="$found, $file"
else
found="$file"
fi
fi
done

printf '%s' "${found:-<none>}"
}

codexcli_run() {
local doctor=false
local git_common_mount workspace_mount
local cli_instance_set=false
local cli_image=''
local cli_image_set=false
local env_instance_set=false
local env_image_set=false
local wrapper_ended=false
local -a podman_args

Expand All @@ -81,14 +115,38 @@ codexcli_run() {
else
CODEXCLI_INSTANCE=''
fi
if [ "${CODEXCLI_IMAGE+x}" = x ]; then
env_image_set=true
else
CODEXCLI_IMAGE=''
fi

while [ "$#" -gt 0 ]; do
case "$1" in
--image)
shift
[ "$#" -gt 0 ] || codexcli_die "--image requires a value"
case "$1" in
--|--doctor|--instance|--instance=*|--image|--image=*|--*)
codexcli_die "--image requires a value"
;;
esac
cli_image="$1"
cli_image_set=true
codexcli_validate_image "$cli_image"
shift
;;
--image=*)
cli_image="${1#--image=}"
cli_image_set=true
codexcli_validate_image "$cli_image"
shift
;;
--instance)
shift
[ "$#" -gt 0 ] || codexcli_die "--instance requires a value"
case "$1" in
--|--doctor|--instance|--instance=*)
--|--doctor|--instance|--instance=*|--image|--image=*)
codexcli_die "--instance requires a value"
;;
esac
Expand Down Expand Up @@ -122,6 +180,18 @@ codexcli_run() {
[ -n "$CODEXCLI_INSTANCE" ] || codexcli_die "CODEXCLI_INSTANCE must not be empty"
codexcli_validate_instance "$CODEXCLI_INSTANCE"
fi
if [ "$cli_image_set" = true ]; then
CODEXCLI_RUNTIME_IMAGE="$cli_image"
CODEXCLI_RUNTIME_IMAGE_SOURCE="CLI --image"
elif [ "$env_image_set" = true ]; then
codexcli_validate_image "$CODEXCLI_IMAGE"
CODEXCLI_RUNTIME_IMAGE="$CODEXCLI_IMAGE"
CODEXCLI_RUNTIME_IMAGE_SOURCE="CODEXCLI_IMAGE"
else
CODEXCLI_RUNTIME_IMAGE="${CODEXCLI_DEFAULT_IMAGE:-}"
CODEXCLI_RUNTIME_IMAGE_SOURCE="default"
codexcli_validate_image "$CODEXCLI_RUNTIME_IMAGE"
fi

if [ "$doctor" = true ] && [ "$#" -gt 0 ]; then
codexcli_die "--doctor does not accept Codex arguments"
Expand All @@ -130,9 +200,15 @@ codexcli_run() {
if [ "$wrapper_ended" = false ]; then
for arg in "$@"; do
case "$arg" in
--)
break
;;
--instance|--instance=*)
codexcli_die "--instance must appear before Codex arguments; use -- to pass it to Codex"
;;
--image|--image=*)
codexcli_die "--image must appear before Codex arguments; use -- to pass it to Codex"
;;
esac
done
fi
Expand All @@ -150,6 +226,9 @@ codexcli_run() {
CODEXCLI_INSTANCE_DISPLAY="${CODEXCLI_INSTANCE:-<default>}"
CODEXCLI_HOME_HOST="${HOME}/.local/share/codexcli/codex-home"
codexcli_require_git_identity
if [ "$doctor" = true ]; then
CODEXCLI_PROJECT_ENV_FILES="$(codexcli_project_env_files)"
fi

if [ "$doctor" = false ]; then
mkdir -p "$CODEXCLI_HOME_HOST"
Expand Down Expand Up @@ -193,6 +272,9 @@ codexcli_run() {
-e "CODEXCLI_DOCTOR_BASE_CONTAINER_NAME=$CODEXCLI_BASE_CONTAINER_NAME"
-e "CODEXCLI_DOCTOR_INSTANCE=$CODEXCLI_INSTANCE_DISPLAY"
-e "CODEXCLI_DOCTOR_CONTAINER_NAME=$CODEXCLI_CONTAINER_NAME"
-e "CODEXCLI_DOCTOR_RUNTIME_IMAGE=$CODEXCLI_RUNTIME_IMAGE"
-e "CODEXCLI_DOCTOR_RUNTIME_IMAGE_SOURCE=$CODEXCLI_RUNTIME_IMAGE_SOURCE"
-e "CODEXCLI_DOCTOR_PROJECT_ENV_FILES=$CODEXCLI_PROJECT_ENV_FILES"
-e "CODEXCLI_DOCTOR_HOME_HOST=$CODEXCLI_HOME_HOST"
)
else
Expand All @@ -211,7 +293,7 @@ codexcli_run() {
fi

if [ "$doctor" = true ]; then
exec podman "${podman_args[@]}" "$CODEXCLI_IMAGE" bash -lc '
exec podman "${podman_args[@]}" "$CODEXCLI_RUNTIME_IMAGE" bash -lc '
set -Eeuo pipefail
print_git_config() {
printf "%s: " "$1"
Expand All @@ -222,6 +304,9 @@ codexcli_run() {
printf "base container name: %s\n" "$CODEXCLI_DOCTOR_BASE_CONTAINER_NAME"
printf "instance: %s\n" "$CODEXCLI_DOCTOR_INSTANCE"
printf "effective container name: %s\n" "$CODEXCLI_DOCTOR_CONTAINER_NAME"
printf "runtime image: %s\n" "$CODEXCLI_DOCTOR_RUNTIME_IMAGE"
printf "runtime image source: %s\n" "$CODEXCLI_DOCTOR_RUNTIME_IMAGE_SOURCE"
printf "project environment files: %s\n" "$CODEXCLI_DOCTOR_PROJECT_ENV_FILES"
printf "host Codex home (normal mode): %s\n" "$CODEXCLI_DOCTOR_HOME_HOST"
printf "container CODEX_HOME: %s\n" "$CODEX_HOME"
printf "doctor mode: temporary Codex home; workspace and Git mounts are read-only\n"
Expand All @@ -241,7 +326,7 @@ codexcli_run() {
fi

podman_args+=(-it)
exec podman "${podman_args[@]}" "$CODEXCLI_IMAGE" bash -lc '
exec podman "${podman_args[@]}" "$CODEXCLI_RUNTIME_IMAGE" bash -lc '
set -Eeuo pipefail
exec codex --cd /workspace --sandbox workspace-write \
--ask-for-approval on-request "$@"
Expand Down
Loading