openai/codex-action

Public

mirrored fromhttps://github.com/openai/codex-actionAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
main

Branches

Tags

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

Clone

HTTPS

Download ZIP

action.yml

357lines · 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 trusted GitHub bot accounts (github-actions[bot]) to bypass the write-access check."
98 required: false
99 default: "false"
100 allow-bot-users:
101 description: "Comma-separated list of GitHub bot usernames that can bypass the write-access check. '*' is not supported; list trusted bots explicitly. Entries may include or omit the trailing [bot] suffix."
102 required: false
103 default: ""
104outputs:
105 final-message:
106 description: "Raw output emitted by `codex exec`."
107 value: ${{ steps.run_codex.outputs['final-message'] }}
108runs:
109 using: "composite"
110 steps:
111 - name: Validate Windows safety strategy
112 if: ${{ runner.os == 'Windows' }}
113 shell: bash
114 env:
115 SAFETY_STRATEGY: ${{ inputs['safety-strategy'] }}
116 run: |
117 if [ "$SAFETY_STRATEGY" != "unsafe" ]; then
118 echo "On Windows, inputs['safety-strategy'] must be 'unsafe'" >&2
119 echo "because no viable sandboxing options are available at this time." >&2
120 exit 1
121 fi
122
123 - name: Ensure Node.js available
124 # Pin to a commit hash because some repositories require it:
125 # https://github.com/openai/codex-action/issues/43
126 uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
127 with:
128 node-version: "20"
129
130 - name: Check repository write access
131 env:
132 ACTION_PATH: ${{ github.action_path }}
133 GITHUB_TOKEN: ${{ github.token }}
134 ALLOW_BOTS: ${{ inputs['allow-bots'] }}
135 ALLOW_BOT_USERS: ${{ inputs['allow-bot-users'] }}
136 ALLOW_USERS: ${{ inputs['allow-users'] }}
137 shell: bash
138 run: |
139 node "$ACTION_PATH/dist/main.js" check-write-access \
140 --allow-bots "$ALLOW_BOTS" \
141 --allow-bot-users "$ALLOW_BOT_USERS" \
142 --allow-users "$ALLOW_USERS"
143
144 - name: Install Codex CLI
145 shell: bash
146 env:
147 CODEX_VERSION: ${{ inputs['codex-version'] }}
148 run: npm install -g "@openai/codex@${CODEX_VERSION}"
149
150 - name: Install Codex Responses API proxy
151 shell: bash
152 env:
153 CODEX_VERSION: ${{ inputs['codex-version'] }}
154 run: npm install -g "@openai/codex-responses-api-proxy@${CODEX_VERSION}"
155
156 - name: Resolve Codex home
157 id: resolve_home
158 shell: bash
159 env:
160 ACTION_PATH: ${{ github.action_path }}
161 CODEX_HOME_OVERRIDE: ${{ inputs['codex-home'] }}
162 SAFETY_STRATEGY: ${{ inputs['safety-strategy'] }}
163 CODEX_USER: ${{ inputs['codex-user'] }}
164 CODEX_RUN_ID: ${{ github.run_id }}
165 run: |
166 node "$ACTION_PATH/dist/main.js" resolve-codex-home \
167 --codex-home-override "$CODEX_HOME_OVERRIDE" \
168 --safety-strategy "$SAFETY_STRATEGY" \
169 --codex-user "$CODEX_USER" \
170 --github-run-id "$CODEX_RUN_ID"
171
172 - name: Determine server info path
173 id: derive_server_info
174 shell: bash
175 env:
176 CODEX_HOME: ${{ steps.resolve_home.outputs.codex-home }}
177 CODEX_RUN_ID: ${{ github.run_id }}
178 run: |
179 server_info_file="$CODEX_HOME/$CODEX_RUN_ID.json"
180 echo "server_info_file=$server_info_file" >> "$GITHUB_OUTPUT"
181
182 - name: Check Responses API proxy status
183 id: start_proxy
184 if: ${{ inputs['openai-api-key'] != '' }}
185 shell: bash
186 env:
187 SERVER_INFO_FILE: ${{ steps.derive_server_info.outputs.server_info_file }}
188 run: |
189 if [ -s "$SERVER_INFO_FILE" ]; then
190 echo "Responses API proxy already appears to be running (found $SERVER_INFO_FILE)."
191 echo "server_info_file_exists=true" >> "$GITHUB_OUTPUT"
192 else
193 echo "server_info_file_exists=false" >> "$GITHUB_OUTPUT"
194 fi
195
196 # This is its own step to minimize the runtime logic that has access to the
197 # API key. Note we use `env -u PROXY_API_KEY` to ensure extra copies of the
198 # key do not end up in the memory of the `codex-responses-api-proxy`
199 # process where environment variables are stored.
200 - name: Start Responses API proxy
201 if: ${{ inputs['openai-api-key'] != '' && steps.start_proxy.outputs.server_info_file_exists == 'false' }}
202 env:
203 SERVER_INFO_FILE: ${{ steps.derive_server_info.outputs.server_info_file }}
204 PROXY_API_KEY: ${{ inputs['openai-api-key'] }}
205 UPSTREAM_URL: ${{ inputs['responses-api-endpoint'] }}
206 shell: bash
207 run: |
208 args=(
209 codex-responses-api-proxy
210 --http-shutdown
211 --server-info "$SERVER_INFO_FILE"
212 )
213
214 if [ -n "$UPSTREAM_URL" ]; then
215 args+=(--upstream-url "$UPSTREAM_URL")
216 fi
217
218 (
219 printenv PROXY_API_KEY | env -u PROXY_API_KEY "${args[@]}"
220 ) &
221
222 - name: Wait for Responses API proxy
223 if: ${{ inputs['openai-api-key'] != '' && steps.start_proxy.outputs.server_info_file_exists == 'false' }}
224 shell: bash
225 env:
226 SERVER_INFO_FILE: ${{ steps.derive_server_info.outputs.server_info_file }}
227 run: |
228 for _ in {1..10}; do
229 if [ -s "$SERVER_INFO_FILE" ]; then
230 break
231 fi
232 sleep 1
233 done
234 if [ ! -s "$SERVER_INFO_FILE" ]; then
235 echo "responses-api-proxy did not write server info" >&2
236 exit 1
237 fi
238
239 if [ "${RUNNER_OS}" != "Windows" ]; then
240 sudo chmod 444 "$SERVER_INFO_FILE"
241 sudo chown root "$SERVER_INFO_FILE"
242 fi
243
244 # This step has an output named `port`.
245 - name: Read server info
246 id: read_server_info
247 if: ${{ inputs['openai-api-key'] != '' || inputs.prompt != '' || inputs['prompt-file'] != '' }}
248 shell: bash
249 env:
250 ACTION_PATH: ${{ github.action_path }}
251 SERVER_INFO_FILE: ${{ steps.derive_server_info.outputs.server_info_file }}
252 run: node "$ACTION_PATH/dist/main.js" read-server-info "$SERVER_INFO_FILE"
253
254 - name: Write Codex proxy config
255 if: ${{ inputs['openai-api-key'] != '' }}
256 shell: bash
257 env:
258 ACTION_PATH: ${{ github.action_path }}
259 CODEX_HOME: ${{ steps.resolve_home.outputs.codex-home }}
260 PROXY_PORT: ${{ steps.read_server_info.outputs.port }}
261 SAFETY_STRATEGY: ${{ inputs['safety-strategy'] }}
262 run: |
263 node "$ACTION_PATH/dist/main.js" write-proxy-config \
264 --codex-home "$CODEX_HOME" \
265 --port "$PROXY_PORT" \
266 --safety-strategy "$SAFETY_STRATEGY"
267
268 - name: Enable Linux user namespaces for bubblewrap
269 if: ${{ runner.os == 'Linux' && runner.environment == 'github-hosted' && (inputs['openai-api-key'] != '' || inputs.prompt != '' || inputs['prompt-file'] != '') }}
270 shell: bash
271 run: |
272 set -euo pipefail
273
274 # Bubblewrap needs unprivileged user namespaces on GitHub-hosted Linux
275 # runners. This step runs before drop-sudo, then becomes a no-op on
276 # later codex-action invocations in the same job because the sysctls
277 # already have the desired values. See issue #75 for the failure mode
278 # this is working around on newer Ubuntu images.
279 current_userns="$(sysctl -n kernel.unprivileged_userns_clone 2>/dev/null || true)"
280 if [ -n "$current_userns" ] && [ "$current_userns" != "1" ]; then
281 echo "Enabling kernel.unprivileged_userns_clone for bubblewrap."
282 sudo sysctl -w kernel.unprivileged_userns_clone=1
283 fi
284
285 # Ubuntu 24.04+ can additionally block unprivileged user namespaces via
286 # AppArmor, which causes bubblewrap to fail with
287 # `loopback: Failed RTM_NEWADDR: Operation not permitted`.
288 current_apparmor="$(sysctl -n kernel.apparmor_restrict_unprivileged_userns 2>/dev/null || true)"
289 if [ -n "$current_apparmor" ] && [ "$current_apparmor" != "0" ]; then
290 echo "Disabling kernel.apparmor_restrict_unprivileged_userns for bubblewrap."
291 sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
292 fi
293
294 - name: Drop sudo privilege, if appropriate
295 if: ${{ inputs['safety-strategy'] == 'drop-sudo' && inputs['openai-api-key'] != '' }}
296 shell: bash
297 env:
298 ACTION_PATH: ${{ github.action_path }}
299 run: |
300 case "${RUNNER_OS}" in
301 Linux)
302 node "$ACTION_PATH/dist/main.js" drop-sudo --user runner --group sudo
303 ;;
304 macOS)
305 node "$ACTION_PATH/dist/main.js" drop-sudo --user runner --group admin
306 ;;
307 *)
308 echo "Unsupported OS for drop-sudo: ${RUNNER_OS}" >&2
309 exit 1
310 ;;
311 esac
312
313 - name: Verify sudo privilege removed
314 if: ${{ inputs['safety-strategy'] == 'drop-sudo' && inputs['openai-api-key'] != '' }}
315 shell: bash
316 run: |
317 if sudo -n true 2>/dev/null; then
318 echo "Expected sudo to be disabled, but sudo succeeded." >&2
319 exit 1
320 fi
321 echo "Confirmed sudo privilege is disabled."
322
323 - name: Run codex exec
324 id: run_codex
325 if: ${{ inputs.prompt != '' || inputs['prompt-file'] != '' }}
326 env:
327 CODEX_PROMPT: ${{ inputs.prompt }}
328 CODEX_PROMPT_FILE: ${{ inputs['prompt-file'] }}
329 CODEX_OUTPUT_FILE: ${{ inputs['output-file'] }}
330 CODEX_HOME: ${{ steps.resolve_home.outputs.codex-home }}
331 CODEX_WORKING_DIRECTORY: ${{ inputs['working-directory'] || github.workspace }}
332 CODEX_SANDBOX: ${{ inputs.sandbox }}
333 CODEX_ARGS: ${{ inputs['codex-args'] }}
334 CODEX_OUTPUT_SCHEMA: ${{ inputs['output-schema'] }}
335 CODEX_OUTPUT_SCHEMA_FILE: ${{ inputs['output-schema-file'] }}
336 CODEX_MODEL: ${{ inputs.model }}
337 CODEX_EFFORT: ${{ inputs.effort }}
338 CODEX_SAFETY_STRATEGY: ${{ inputs['safety-strategy'] }}
339 CODEX_USER: ${{ inputs['codex-user'] }}
340 ACTION_PATH: ${{ github.action_path }}
341 FORCE_COLOR: 1
342 shell: bash
343 run: |
344 node "$ACTION_PATH/dist/main.js" run-codex-exec \
345 --prompt "${CODEX_PROMPT}" \
346 --prompt-file "${CODEX_PROMPT_FILE}" \
347 --output-file "$CODEX_OUTPUT_FILE" \
348 --codex-home "$CODEX_HOME" \
349 --cd "$CODEX_WORKING_DIRECTORY" \
350 --extra-args "$CODEX_ARGS" \
351 --output-schema "$CODEX_OUTPUT_SCHEMA" \
352 --output-schema-file "$CODEX_OUTPUT_SCHEMA_FILE" \
353 --sandbox "$CODEX_SANDBOX" \
354 --model "$CODEX_MODEL" \
355 --effort "$CODEX_EFFORT" \
356 --safety-strategy "$CODEX_SAFETY_STRATEGY" \
357 --codex-user "$CODEX_USER"
358