223 lines
8.5 KiB
YAML
223 lines
8.5 KiB
YAML
name: "Codex Exec Action"
|
|
description: "Run `codex exec` with a prompt."
|
|
author: "OpenAI"
|
|
inputs:
|
|
prompt:
|
|
description: "Prompt to pass to `codex exec`. `prompt` or `prompt_file` must be provided."
|
|
required: false
|
|
default: ""
|
|
prompt_file:
|
|
description: "Path to file that contains the prompt to pass to `codex exec`. `prompt` or `prompt_file` must be provided."
|
|
required: false
|
|
default: ""
|
|
output_file:
|
|
description: "Path to a JSON Schema file describing the model's final response shape."
|
|
required: false
|
|
default: ""
|
|
openai_api_key:
|
|
description: "OpenAI API key used by the Codex CLI."
|
|
required: true
|
|
working_directory:
|
|
description: "Optional working directory to `cd` into before running `codex exec`."
|
|
required: false
|
|
default: ""
|
|
sandbox:
|
|
description: |
|
|
Sandbox mode for Codex. One of `workspace-write` (default), `read-only` or `danger-full-access`.
|
|
required: false
|
|
default: "workspace-write"
|
|
codex_version:
|
|
description: "Version of `@openai/codex` to install."
|
|
required: false
|
|
default: "0.43.0-alpha.17"
|
|
codex_args:
|
|
description: "Additional args to pass through to `codex exec`. If this value starts with `[`, it will be parsed as a JSON array; otherwise, it will be parsed as a shell-like string."
|
|
required: false
|
|
default: ""
|
|
output_schema:
|
|
description: "Inline schema contents to use with `codex exec --output-schema`."
|
|
required: false
|
|
default: ""
|
|
output_schema_file:
|
|
description: "File path to the schema that should be passed to `codex exec --output-schema`."
|
|
required: false
|
|
default: ""
|
|
model:
|
|
description: "Model the agent should use"
|
|
required: false
|
|
default: ""
|
|
codex_home:
|
|
description: "Directory to use as the Codex home directory. If empty, the default Codex home directory will be used."
|
|
required: false
|
|
default: ""
|
|
safety_strategy:
|
|
description: |
|
|
Specify one of the following options (on Windows, the only supported option is `unsafe`):
|
|
|
|
* `drop_sudo` (default, IRREVERSIBLE) Drop sudo privileges (if any) from
|
|
the default user before running Codex, and run Codex as that user. This
|
|
is only supported on Linux and macOS runners. This option is
|
|
irreversible: if the default user has sudo privileges, they will be
|
|
removed permanently for the duration of the job.
|
|
* `unprivileged_user` Run Codex as the specified user specified by the
|
|
`codex_user` option (the user must already exist). Note the caller is
|
|
responsible for ensuring the specified user has the privileges it needs
|
|
to perform the requested actions. For example, the copy of the repo
|
|
created by `actions/checkout` is not world-readable by default.
|
|
* `read_only` Run Codex in a sandbox that can read any file on disk,
|
|
but cannot write to disk or access the network. Note Codex will still
|
|
run as the default user for this Action, which likely has sudo
|
|
privileges, so it could read `openai_api_key` from memory and reveal it
|
|
by printing it to the output of the GitHub Action.
|
|
* `unsafe` (NOT RECOMMENDED) Do not try to restrict Codex's privileges at all.
|
|
This is extremely dangerous, as the default user for this Action likely
|
|
has sudo privileges, which means it can read secrets stored in memory
|
|
(such as the value of `openai_api_key`) and print them to the output
|
|
of the GitHub Action or exfiltrate them in other ways.
|
|
required: false
|
|
default: "drop_sudo"
|
|
codex_user:
|
|
description: "If `safety_strategy` is set to `unprivileged_user`, this specifies the UNIX username to run Codex as."
|
|
required: false
|
|
default: ""
|
|
require_repo_write:
|
|
description: "Whether to require the triggering actor to have write access to the repository before running."
|
|
required: false
|
|
default: "true"
|
|
allow_bots:
|
|
description: "Allow runs triggered by GitHub Apps/bot accounts to bypass the write-access check."
|
|
required: false
|
|
default: "false"
|
|
outputs:
|
|
final_message:
|
|
description: "Raw output emitted by `codex exec`."
|
|
value: ${{ steps.run_codex.outputs.final_message }}
|
|
runs:
|
|
using: "composite"
|
|
steps:
|
|
- name: Validate Windows safety strategy
|
|
if: ${{ runner.os == 'Windows' }}
|
|
shell: bash
|
|
run: |
|
|
if [ "${{ inputs.safety_strategy }}" != "unsafe" ]; then
|
|
echo "On Windows, inputs.safety_strategy must be 'unsafe'" >&2
|
|
echo "because no viable sandboxing options are available at this time." >&2
|
|
exit 1
|
|
fi
|
|
|
|
- name: Ensure Node.js available
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: "20"
|
|
|
|
- name: Check repository write access
|
|
if: ${{ inputs.require_repo_write == 'true' }}
|
|
env:
|
|
ALLOW_BOTS: ${{ inputs.allow_bots }}
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
shell: bash
|
|
run: |
|
|
node "${{ github.action_path }}/dist/main.js" check-write-access --allow-bots "$ALLOW_BOTS"
|
|
|
|
- name: Install Codex CLI
|
|
shell: bash
|
|
run: npm install -g "@openai/codex@${{ inputs.codex_version }}"
|
|
|
|
- name: Install Codex Responses API proxy
|
|
shell: bash
|
|
run: npm install -g "@openai/codex-responses-api-proxy@${{ inputs.codex_version }}"
|
|
|
|
- name: Start Responses API proxy
|
|
id: start_proxy
|
|
env:
|
|
OPENAI_API_KEY: ${{ inputs.openai_api_key }}
|
|
shell: bash
|
|
run: |
|
|
tmpfile=$(mktemp)
|
|
(
|
|
printenv OPENAI_API_KEY | codex-responses-api-proxy --http-shutdown --server-info "$tmpfile"
|
|
) &
|
|
for _ in {1..10}; do
|
|
if [ -s "$tmpfile" ]; then
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
if [ ! -s "$tmpfile" ]; then
|
|
echo "responses-api-proxy did not write server info" >&2
|
|
exit 1
|
|
fi
|
|
echo "server_info_file=$tmpfile" >> "$GITHUB_OUTPUT"
|
|
|
|
# This step has an output named `port`.
|
|
- name: Read server info
|
|
id: read_server_info
|
|
shell: bash
|
|
run: node "${{ github.action_path }}/dist/main.js" read-server-info "${{ steps.start_proxy.outputs.server_info_file }}"
|
|
|
|
- name: Drop sudo privilege, if appropriate
|
|
if: ${{ inputs.safety_strategy == 'drop_sudo' }}
|
|
shell: bash
|
|
run: |
|
|
case "${RUNNER_OS}" in
|
|
Linux)
|
|
node "${{ github.action_path }}/dist/main.js" drop-sudo --user runner --group sudo
|
|
;;
|
|
macOS)
|
|
node "${{ github.action_path }}/dist/main.js" drop-sudo --user runner --group admin
|
|
;;
|
|
*)
|
|
echo "Unsupported OS for drop_sudo: ${RUNNER_OS}" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
- name: Verify sudo privilege removed
|
|
if: ${{ inputs.safety_strategy == 'drop_sudo' }}
|
|
shell: bash
|
|
run: |
|
|
if sudo -n true 2>/dev/null; then
|
|
echo "Expected sudo to be disabled, but sudo succeeded." >&2
|
|
exit 1
|
|
fi
|
|
echo "Confirmed sudo privilege is disabled."
|
|
|
|
- name: Run codex exec
|
|
id: run_codex
|
|
env:
|
|
CODEX_PROMPT: ${{ inputs.prompt }}
|
|
CODEX_PROMPT_FILE: ${{ inputs.prompt_file }}
|
|
CODEX_OUTPUT_FILE: ${{ inputs.output_file }}
|
|
CODEX_HOME: ${{ inputs.codex_home }}
|
|
CODEX_WORKING_DIRECTORY: ${{ inputs.working_directory || github.workspace }}
|
|
CODEX_SANDBOX: ${{ inputs.sandbox }}
|
|
CODEX_ARGS: ${{ inputs.codex_args }}
|
|
CODEX_OUTPUT_SCHEMA: ${{ inputs.output_schema }}
|
|
CODEX_OUTPUT_SCHEMA_FILE: ${{ inputs.output_schema_file }}
|
|
CODEX_MODEL: ${{ inputs.model }}
|
|
CODEX_SAFETY_STRATEGY: ${{ inputs.safety_strategy }}
|
|
CODEX_USER: ${{ inputs.codex_user }}
|
|
FORCE_COLOR: 1
|
|
shell: bash
|
|
run: |
|
|
node "${{ github.action_path }}/dist/main.js" run-codex-exec \
|
|
--prompt "${CODEX_PROMPT}" \
|
|
--prompt-file "${CODEX_PROMPT_FILE}" \
|
|
--output-file "$CODEX_OUTPUT_FILE" \
|
|
--codex-home "$CODEX_HOME" \
|
|
--cd "$CODEX_WORKING_DIRECTORY" \
|
|
--proxy-port "${{ steps.read_server_info.outputs.port }}" \
|
|
--extra-args "$CODEX_ARGS" \
|
|
--output-schema "$CODEX_OUTPUT_SCHEMA" \
|
|
--output-schema-file "$CODEX_OUTPUT_SCHEMA_FILE" \
|
|
--sandbox "$CODEX_SANDBOX" \
|
|
--model "$CODEX_MODEL" \
|
|
--safety-strategy "$CODEX_SAFETY_STRATEGY" \
|
|
--codex-user "$CODEX_USER"
|
|
|
|
- name: Shutdown codex webserver
|
|
env:
|
|
CODEX_SERVER_PORT: ${{ steps.read_server_info.outputs.port }}
|
|
shell: bash
|
|
run: curl --fail --silent --show-error "http://127.0.0.1:${CODEX_SERVER_PORT}/shutdown"
|