271 lines
11 KiB
YAML
271 lines
11 KiB
YAML
name: "Codex Exec Action"
|
|
description: "Run `codex exec` with a prompt."
|
|
author: "OpenAI"
|
|
inputs:
|
|
prompt:
|
|
description: "Prompt to pass to Codex. `prompt` or `prompt-file` must be provided."
|
|
required: false
|
|
default: ""
|
|
prompt-file:
|
|
description: "Path to file that contains the prompt to pass to Codex. `prompt` or `prompt-file` must be provided."
|
|
required: false
|
|
default: ""
|
|
output-file:
|
|
description: "File where the final Codex message is written. Leave empty to skip writing a file."
|
|
required: false
|
|
default: ""
|
|
openai-api-key:
|
|
description: "OpenAI API key used by Codex."
|
|
required: false
|
|
default: ""
|
|
working-directory:
|
|
description: "Working directory that Codex should use. Defaults to the repository root."
|
|
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: ""
|
|
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: ""
|
|
allow-users:
|
|
description: "Comma-separated list of GitHub usernames who can run this action, or '*' to allow all users. Note users who have write access to the GitHub repo have access by default and do not need to be listed here."
|
|
required: false
|
|
default: ""
|
|
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
|
|
env:
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
shell: bash
|
|
run: |
|
|
node "${{ github.action_path }}/dist/main.js" check-write-access \
|
|
--allow-bots "${{ inputs['allow-bots'] }}" \
|
|
--allow-users "${{ inputs['allow-users'] }}"
|
|
|
|
- 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: Resolve Codex home
|
|
id: resolve_home
|
|
shell: bash
|
|
run: |
|
|
node "${{ github.action_path }}/dist/main.js" resolve-codex-home \
|
|
--codex-home-override "${{ inputs['codex-home'] }}" \
|
|
--safety-strategy "${{ inputs['safety-strategy'] }}" \
|
|
--codex-user "${{ inputs['codex-user'] }}" \
|
|
--github-run-id "${{ github.run_id }}"
|
|
|
|
- name: Determine server info path
|
|
id: derive_server_info
|
|
shell: bash
|
|
run: |
|
|
server_info_file="${{ steps.resolve_home.outputs.codex-home }}/${{ github.run_id }}.json"
|
|
echo "server_info_file=$server_info_file" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Check Responses API proxy status
|
|
id: start_proxy
|
|
if: ${{ inputs['openai-api-key'] != '' }}
|
|
shell: bash
|
|
run: |
|
|
server_info_file="${{ steps.derive_server_info.outputs.server_info_file }}"
|
|
if [ -s "$server_info_file" ]; then
|
|
echo "Responses API proxy already appears to be running (found $server_info_file)."
|
|
echo "server_info_file_exists=true" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "server_info_file_exists=false" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
# This is its own step to minimize the runtime logic that has access to the
|
|
# API key. Note we use `env -u OPENAI_API_KEY` to ensure extra copies of the
|
|
# key do not end up in the memory of the `codex-responses-api-proxy` process
|
|
# where environment variables are stored.
|
|
- name: Start Responses API proxy
|
|
if: ${{ inputs['openai-api-key'] != '' && steps.start_proxy.outputs.server_info_file_exists == 'false' }}
|
|
env:
|
|
OPENAI_API_KEY: ${{ inputs['openai-api-key'] }}
|
|
shell: bash
|
|
run: |
|
|
(
|
|
printenv OPENAI_API_KEY | env -u OPENAI_API_KEY codex-responses-api-proxy --http-shutdown --server-info "${{ steps.derive_server_info.outputs.server_info_file }}"
|
|
) &
|
|
|
|
- name: Wait for Responses API proxy
|
|
if: ${{ inputs['openai-api-key'] != '' && steps.start_proxy.outputs.server_info_file_exists == 'false' }}
|
|
shell: bash
|
|
run: |
|
|
server_info_file="${{ steps.derive_server_info.outputs.server_info_file }}"
|
|
for _ in {1..10}; do
|
|
if [ -s "$server_info_file" ]; then
|
|
break
|
|
fi
|
|
sleep 1
|
|
done
|
|
if [ ! -s "$server_info_file" ]; then
|
|
echo "responses-api-proxy did not write server info" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [ "${RUNNER_OS}" != "Windows" ]; then
|
|
sudo chmod 444 "$server_info_file"
|
|
sudo chown root "$server_info_file"
|
|
fi
|
|
|
|
# This step has an output named `port`.
|
|
- name: Read server info
|
|
id: read_server_info
|
|
if: ${{ inputs['openai-api-key'] != '' || inputs.prompt != '' || inputs['prompt-file'] != '' }}
|
|
shell: bash
|
|
run: node "${{ github.action_path }}/dist/main.js" read-server-info "${{ steps.derive_server_info.outputs.server_info_file }}"
|
|
|
|
- name: Write Codex proxy config
|
|
if: ${{ inputs['openai-api-key'] != '' }}
|
|
shell: bash
|
|
run: |
|
|
node "${{ github.action_path }}/dist/main.js" write-proxy-config \
|
|
--codex-home "${{ steps.resolve_home.outputs.codex-home }}" \
|
|
--port "${{ steps.read_server_info.outputs.port }}" \
|
|
--safety-strategy "${{ inputs['safety-strategy'] }}"
|
|
|
|
- name: Drop sudo privilege, if appropriate
|
|
if: ${{ inputs['safety-strategy'] == 'drop-sudo' && inputs['openai-api-key'] != '' }}
|
|
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' && inputs['openai-api-key'] != '' }}
|
|
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
|
|
if: ${{ inputs.prompt != '' || inputs['prompt-file'] != '' }}
|
|
env:
|
|
CODEX_PROMPT: ${{ inputs.prompt }}
|
|
CODEX_PROMPT_FILE: ${{ inputs['prompt-file'] }}
|
|
CODEX_OUTPUT_FILE: ${{ inputs['output-file'] }}
|
|
CODEX_HOME: ${{ steps.resolve_home.outputs.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" \
|
|
--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"
|