openai/codex-action

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.6

Branches

Tags

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

Clone

HTTPS

Download ZIP

action.yml

351lines · modecode

1name: "Codex Exec Action"
2description: "Run `codex exec` with a prompt."
3author: "OpenAI"
4inputs:
5 prompt:
6 description: "Prompt to pass to Codex. `prompt` or `prompt-file` must be provided."
7 required: false
8 default: ""
9 prompt-file:
10 description: "Path to file that contains the prompt to pass to Codex. `prompt` or `prompt-file` must be provided."
11 required: false
12 default: ""
13 output-file:
14 description: "File where the final Codex message is written. Leave empty to skip writing a file."
15 required: false
16 default: ""
17 openai-api-key:
18 description: "OpenAI API key used by Codex."
19 required: false
20 default: ""
21 responses-api-endpoint:
22 description: "Optional Responses API endpoint override, e.g. https://example.openai.azure.com/openai/v1/responses. Defaults to the proxy's built-in endpoint when empty."
23 required: false
24 default: ""
25 working-directory:
26 description: "Working directory that Codex should use. Defaults to the repository root."
27 required: false
28 default: ""
29 sandbox:
30 description: |
31 Sandbox mode for Codex. One of `workspace-write` (default), `read-only` or `danger-full-access`.
32 required: false
33 default: "workspace-write"
34 codex-version:
35 description: "Version of `@openai/codex` to install."
36 required: false
37 default: ""
38 codex-args:
39 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."
40 required: false
41 default: ""
42 output-schema:
43 description: "Inline schema contents to use with `codex exec --output-schema`."
44 required: false
45 default: ""
46 output-schema-file:
47 description: "File path to the schema that should be passed to `codex exec --output-schema`."
48 required: false
49 default: ""
50 model:
51 description: "Model the agent should use."
52 required: false
53 default: ""
54 effort:
55 description: "Reasoning effort the agent should use."
56 required: false
57 default: ""
58 codex-home:
59 description: "Directory to use as the Codex home directory. If empty, the default Codex home directory will be used."
60 required: false
61 default: ""
62 safety-strategy:
63 description: |
64 Specify one of the following options (on Windows, the only supported option is `unsafe`):
65
66 * `drop-sudo` (default, IRREVERSIBLE) Drop sudo privileges (if any) from
67 the default user before running Codex, and run Codex as that user. This
68 is only supported on Linux and macOS runners. This option is
69 irreversible: if the default user has sudo privileges, they will be
70 removed permanently for the duration of the job.
71 * `unprivileged-user` Run Codex as the specified user specified by the
72 `codex-user` option (the user must already exist). Note the caller is
73 responsible for ensuring the specified user has the privileges it needs
74 to perform the requested actions. For example, the copy of the repo
75 created by `actions/checkout` is not world-readable by default.
76 * `read-only` Run Codex in a sandbox that can read any file on disk,
77 but cannot write to disk or access the network. Note Codex will still
78 run as the default user for this Action, which likely has sudo
79 privileges, so it could read `openai-api-key` from memory and reveal it
80 by printing it to the output of the GitHub Action.
81 * `unsafe` (NOT RECOMMENDED) Do not try to restrict Codex's privileges at all.
82 This is extremely dangerous, as the default user for this Action likely
83 has sudo privileges, which means it can read secrets stored in memory
84 (such as the value of `openai-api-key`) and print them to the output
85 of the GitHub Action or exfiltrate them in other ways.
86 required: false
87 default: "drop-sudo"
88 codex-user:
89 description: "If `safety-strategy` is set to `unprivileged-user`, this specifies the UNIX username to run Codex as."
90 required: false
91 default: ""
92 allow-users:
93 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."
94 required: false
95 default: ""
96 allow-bots:
97 description: "Allow runs triggered by GitHub Apps/bot accounts to bypass the write-access check."
98 required: false
99 default: "false"
100outputs:
101 final-message:
102 description: "Raw output emitted by `codex exec`."
103 value: ${{ steps.run_codex.outputs['final-message'] }}
104runs:
105 using: "composite"
106 steps:
107 - name: Validate Windows safety strategy
108 if: ${{ runner.os == 'Windows' }}
109 shell: bash
110 env:
111 SAFETY_STRATEGY: ${{ inputs['safety-strategy'] }}
112 run: |
113 if [ "$SAFETY_STRATEGY" != "unsafe" ]; then
114 echo "On Windows, inputs['safety-strategy'] must be 'unsafe'" >&2
115 echo "because no viable sandboxing options are available at this time." >&2
116 exit 1
117 fi
118
119 - name: Ensure Node.js available
120 # Pin to a commit hash because some repositories require it:
121 # https://github.com/openai/codex-action/issues/43
122 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
123 with:
124 node-version: "20"
125
126 - name: Check repository write access
127 env:
128 ACTION_PATH: ${{ github.action_path }}
129 GITHUB_TOKEN: ${{ github.token }}
130 ALLOW_BOTS: ${{ inputs['allow-bots'] }}
131 ALLOW_USERS: ${{ inputs['allow-users'] }}
132 shell: bash
133 run: |
134 node "$ACTION_PATH/dist/main.js" check-write-access \
135 --allow-bots "$ALLOW_BOTS" \
136 --allow-users "$ALLOW_USERS"
137
138 - name: Install Codex CLI
139 shell: bash
140 env:
141 CODEX_VERSION: ${{ inputs['codex-version'] }}
142 run: npm install -g "@openai/codex@${CODEX_VERSION}"
143
144 - name: Install Codex Responses API proxy
145 shell: bash
146 env:
147 CODEX_VERSION: ${{ inputs['codex-version'] }}
148 run: npm install -g "@openai/codex-responses-api-proxy@${CODEX_VERSION}"
149
150 - name: Resolve Codex home
151 id: resolve_home
152 shell: bash
153 env:
154 ACTION_PATH: ${{ github.action_path }}
155 CODEX_HOME_OVERRIDE: ${{ inputs['codex-home'] }}
156 SAFETY_STRATEGY: ${{ inputs['safety-strategy'] }}
157 CODEX_USER: ${{ inputs['codex-user'] }}
158 CODEX_RUN_ID: ${{ github.run_id }}
159 run: |
160 node "$ACTION_PATH/dist/main.js" resolve-codex-home \
161 --codex-home-override "$CODEX_HOME_OVERRIDE" \
162 --safety-strategy "$SAFETY_STRATEGY" \
163 --codex-user "$CODEX_USER" \
164 --github-run-id "$CODEX_RUN_ID"
165
166 - name: Determine server info path
167 id: derive_server_info
168 shell: bash
169 env:
170 CODEX_HOME: ${{ steps.resolve_home.outputs.codex-home }}
171 CODEX_RUN_ID: ${{ github.run_id }}
172 run: |
173 server_info_file="$CODEX_HOME/$CODEX_RUN_ID.json"
174 echo "server_info_file=$server_info_file" >> "$GITHUB_OUTPUT"
175
176 - name: Check Responses API proxy status
177 id: start_proxy
178 if: ${{ inputs['openai-api-key'] != '' }}
179 shell: bash
180 env:
181 SERVER_INFO_FILE: ${{ steps.derive_server_info.outputs.server_info_file }}
182 run: |
183 if [ -s "$SERVER_INFO_FILE" ]; then
184 echo "Responses API proxy already appears to be running (found $SERVER_INFO_FILE)."
185 echo "server_info_file_exists=true" >> "$GITHUB_OUTPUT"
186 else
187 echo "server_info_file_exists=false" >> "$GITHUB_OUTPUT"
188 fi
189
190 # This is its own step to minimize the runtime logic that has access to the
191 # API key. Note we use `env -u PROXY_API_KEY` to ensure extra copies of the
192 # key do not end up in the memory of the `codex-responses-api-proxy`
193 # process where environment variables are stored.
194 - name: Start Responses API proxy
195 if: ${{ inputs['openai-api-key'] != '' && steps.start_proxy.outputs.server_info_file_exists == 'false' }}
196 env:
197 SERVER_INFO_FILE: ${{ steps.derive_server_info.outputs.server_info_file }}
198 PROXY_API_KEY: ${{ inputs['openai-api-key'] }}
199 UPSTREAM_URL: ${{ inputs['responses-api-endpoint'] }}
200 shell: bash
201 run: |
202 args=(
203 codex-responses-api-proxy
204 --http-shutdown
205 --server-info "$SERVER_INFO_FILE"
206 )
207
208 if [ -n "$UPSTREAM_URL" ]; then
209 args+=(--upstream-url "$UPSTREAM_URL")
210 fi
211
212 (
213 printenv PROXY_API_KEY | env -u PROXY_API_KEY "${args[@]}"
214 ) &
215
216 - name: Wait for Responses API proxy
217 if: ${{ inputs['openai-api-key'] != '' && steps.start_proxy.outputs.server_info_file_exists == 'false' }}
218 shell: bash
219 env:
220 SERVER_INFO_FILE: ${{ steps.derive_server_info.outputs.server_info_file }}
221 run: |
222 for _ in {1..10}; do
223 if [ -s "$SERVER_INFO_FILE" ]; then
224 break
225 fi
226 sleep 1
227 done
228 if [ ! -s "$SERVER_INFO_FILE" ]; then
229 echo "responses-api-proxy did not write server info" >&2
230 exit 1
231 fi
232
233 if [ "${RUNNER_OS}" != "Windows" ]; then
234 sudo chmod 444 "$SERVER_INFO_FILE"
235 sudo chown root "$SERVER_INFO_FILE"
236 fi
237
238 # This step has an output named `port`.
239 - name: Read server info
240 id: read_server_info
241 if: ${{ inputs['openai-api-key'] != '' || inputs.prompt != '' || inputs['prompt-file'] != '' }}
242 shell: bash
243 env:
244 ACTION_PATH: ${{ github.action_path }}
245 SERVER_INFO_FILE: ${{ steps.derive_server_info.outputs.server_info_file }}
246 run: node "$ACTION_PATH/dist/main.js" read-server-info "$SERVER_INFO_FILE"
247
248 - name: Write Codex proxy config
249 if: ${{ inputs['openai-api-key'] != '' }}
250 shell: bash
251 env:
252 ACTION_PATH: ${{ github.action_path }}
253 CODEX_HOME: ${{ steps.resolve_home.outputs.codex-home }}
254 PROXY_PORT: ${{ steps.read_server_info.outputs.port }}
255 SAFETY_STRATEGY: ${{ inputs['safety-strategy'] }}
256 run: |
257 node "$ACTION_PATH/dist/main.js" write-proxy-config \
258 --codex-home "$CODEX_HOME" \
259 --port "$PROXY_PORT" \
260 --safety-strategy "$SAFETY_STRATEGY"
261
262 - name: Enable Linux user namespaces for bubblewrap
263 if: ${{ runner.os == 'Linux' && runner.environment == 'github-hosted' && (inputs['openai-api-key'] != '' || inputs.prompt != '' || inputs['prompt-file'] != '') }}
264 shell: bash
265 run: |
266 set -euo pipefail
267
268 # Bubblewrap needs unprivileged user namespaces on GitHub-hosted Linux
269 # runners. This step runs before drop-sudo, then becomes a no-op on
270 # later codex-action invocations in the same job because the sysctls
271 # already have the desired values. See issue #75 for the failure mode
272 # this is working around on newer Ubuntu images.
273 current_userns="$(sysctl -n kernel.unprivileged_userns_clone 2>/dev/null || true)"
274 if [ -n "$current_userns" ] && [ "$current_userns" != "1" ]; then
275 echo "Enabling kernel.unprivileged_userns_clone for bubblewrap."
276 sudo sysctl -w kernel.unprivileged_userns_clone=1
277 fi
278
279 # Ubuntu 24.04+ can additionally block unprivileged user namespaces via
280 # AppArmor, which causes bubblewrap to fail with
281 # `loopback: Failed RTM_NEWADDR: Operation not permitted`.
282 current_apparmor="$(sysctl -n kernel.apparmor_restrict_unprivileged_userns 2>/dev/null || true)"
283 if [ -n "$current_apparmor" ] && [ "$current_apparmor" != "0" ]; then
284 echo "Disabling kernel.apparmor_restrict_unprivileged_userns for bubblewrap."
285 sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
286 fi
287
288 - name: Drop sudo privilege, if appropriate
289 if: ${{ inputs['safety-strategy'] == 'drop-sudo' && inputs['openai-api-key'] != '' }}
290 shell: bash
291 env:
292 ACTION_PATH: ${{ github.action_path }}
293 run: |
294 case "${RUNNER_OS}" in
295 Linux)
296 node "$ACTION_PATH/dist/main.js" drop-sudo --user runner --group sudo
297 ;;
298 macOS)
299 node "$ACTION_PATH/dist/main.js" drop-sudo --user runner --group admin
300 ;;
301 *)
302 echo "Unsupported OS for drop-sudo: ${RUNNER_OS}" >&2
303 exit 1
304 ;;
305 esac
306
307 - name: Verify sudo privilege removed
308 if: ${{ inputs['safety-strategy'] == 'drop-sudo' && inputs['openai-api-key'] != '' }}
309 shell: bash
310 run: |
311 if sudo -n true 2>/dev/null; then
312 echo "Expected sudo to be disabled, but sudo succeeded." >&2
313 exit 1
314 fi
315 echo "Confirmed sudo privilege is disabled."
316
317 - name: Run codex exec
318 id: run_codex
319 if: ${{ inputs.prompt != '' || inputs['prompt-file'] != '' }}
320 env:
321 CODEX_PROMPT: ${{ inputs.prompt }}
322 CODEX_PROMPT_FILE: ${{ inputs['prompt-file'] }}
323 CODEX_OUTPUT_FILE: ${{ inputs['output-file'] }}
324 CODEX_HOME: ${{ steps.resolve_home.outputs.codex-home }}
325 CODEX_WORKING_DIRECTORY: ${{ inputs['working-directory'] || github.workspace }}
326 CODEX_SANDBOX: ${{ inputs.sandbox }}
327 CODEX_ARGS: ${{ inputs['codex-args'] }}
328 CODEX_OUTPUT_SCHEMA: ${{ inputs['output-schema'] }}
329 CODEX_OUTPUT_SCHEMA_FILE: ${{ inputs['output-schema-file'] }}
330 CODEX_MODEL: ${{ inputs.model }}
331 CODEX_EFFORT: ${{ inputs.effort }}
332 CODEX_SAFETY_STRATEGY: ${{ inputs['safety-strategy'] }}
333 CODEX_USER: ${{ inputs['codex-user'] }}
334 ACTION_PATH: ${{ github.action_path }}
335 FORCE_COLOR: 1
336 shell: bash
337 run: |
338 node "$ACTION_PATH/dist/main.js" run-codex-exec \
339 --prompt "${CODEX_PROMPT}" \
340 --prompt-file "${CODEX_PROMPT_FILE}" \
341 --output-file "$CODEX_OUTPUT_FILE" \
342 --codex-home "$CODEX_HOME" \
343 --cd "$CODEX_WORKING_DIRECTORY" \
344 --extra-args "$CODEX_ARGS" \
345 --output-schema "$CODEX_OUTPUT_SCHEMA" \
346 --output-schema-file "$CODEX_OUTPUT_SCHEMA_FILE" \
347 --sandbox "$CODEX_SANDBOX" \
348 --model "$CODEX_MODEL" \
349 --effort "$CODEX_EFFORT" \
350 --safety-strategy "$CODEX_SAFETY_STRATEGY" \
351 --codex-user "$CODEX_USER"
352