openai/codex-action

Public

mirrored from https://github.com/openai/codex-actionAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
pr19

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

action.yml

265lines · modepreview

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 "${{ inputs['codex-home'] }}"

    - 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
          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 }}"

    - 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"