Merge remote-tracking branch 'origin/main' into codex/viyatb/guardian-reviewer-retries

This commit is contained in:
viyatb-oai
2026-06-08 21:24:47 -07:00
653 changed files with 29189 additions and 14221 deletions
+11 -5
View File
@@ -105,9 +105,9 @@ common:ci --disk_cache=
# Shared config for the main Bazel CI workflow.
common:ci-bazel --config=ci
common:ci-bazel --build_metadata=TAG_workflow=bazel
# Bazel CI cross-compiles in several legs, and the V8-backed code-mode tests
# are not stable in that setup yet. Keep running the rest of the Rust
# integration suites through the workspace-root launcher.
# Keep code-mode integration cases out of ordinary Bazel legs. The
# Windows-cross config below re-enables them after generating its Windows V8
# snapshot on the Windows runner.
common:ci-bazel --test_env=CODEX_BAZEL_TEST_SKIP_FILTERS=suite::code_mode::
# Shared config for Bazel-backed Rust linting.
@@ -186,12 +186,16 @@ common:ci-windows-cross --config=ci-windows
common:ci-windows-cross --build_metadata=TAG_windows_cross_compile=true
common:ci-windows-cross --host_platform=//:rbe
common:ci-windows-cross --strategy=TestRunner=local
# V8 embeds IsolateData offsets in snapshot builtins; Windows snapshots must be
# generated by a Windows mksnapshot binary rather than the Linux RBE host tool.
common:ci-windows-cross --strategy=V8Mksnapshot=local
common:ci-windows-cross --local_test_jobs=4
common:ci-windows-cross --test_env=RUST_TEST_THREADS=1
# Native Windows CI still covers the PowerShell tests. The cross-built gnullvm
# binaries currently hang in PowerShell AST parser tests when those binaries are
# run on the Windows runner.
common:ci-windows-cross --test_env=CODEX_BAZEL_TEST_SKIP_FILTERS=suite::code_mode::,powershell
# run on the Windows runner. Keep V8-backed code-mode tests enabled except for
# the hidden dynamic-tool callback test, which currently times out on Windows.
common:ci-windows-cross --test_env=CODEX_BAZEL_TEST_SKIP_FILTERS=powershell,suite::code_mode::code_mode_can_call_hidden_dynamic_tools
common:ci-windows-cross --platforms=//:windows_x86_64_gnullvm
common:ci-windows-cross --extra_execution_platforms=//:rbe,//:windows_x86_64_msvc
common:ci-windows-cross --extra_toolchains=//:windows_gnullvm_tests_on_msvc_host_toolchain
@@ -210,6 +214,8 @@ common --@v8//:v8_enable_sandbox=True
# artifact migration ships matching Rust feature selection for Cargo consumers.
common:v8-release-compat --@v8//:v8_enable_pointer_compression=False
common:v8-release-compat --@v8//:v8_enable_sandbox=False
common:v8-target-x64 --@v8//bazel/config:v8_target_cpu=x64
common:v8-target-arm64 --@v8//bazel/config:v8_target_cpu=arm64
# Match rusty_v8's upstream GN release contract for published artifacts: every
# target object uses Chromium's custom libc++ headers and the archive folds in
+1 -1
View File
@@ -3,7 +3,7 @@ version = 1
name = "codex"
[setup]
script = ""
script = "python ./.codex/environments/setup.py"
[[actions]]
name = "Run"
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env python3
"""Set up ignored files that should be shared with Codex worktrees."""
import shutil
import subprocess
from functools import cache
from pathlib import Path
@cache
def worktree_paths() -> tuple[Path, Path]:
script_dir = Path(__file__).resolve().parent
worktree_root = git_path(script_dir / "../..", "--show-toplevel")
common_git_dir = git_path(worktree_root, "--git-common-dir")
return worktree_root, common_git_dir.parent
def git_path(working_directory: Path, argument: str) -> Path:
output = subprocess.check_output(
[
"git",
"-C",
str(working_directory),
"rev-parse",
"--path-format=absolute",
argument,
],
text=True,
)
return Path(output.strip())
def copy_from_main_worktree_to_worktree(repo_relative_path: str) -> None:
relative_path = Path(repo_relative_path)
if relative_path.is_absolute() or ".." in relative_path.parts:
raise ValueError(f"path must be repository-relative: {repo_relative_path}")
worktree_root, main_worktree = worktree_paths()
source_path = main_worktree / relative_path
destination_path = worktree_root / relative_path
print(f" source: {source_path}")
print(f" destination: {destination_path}")
if source_path == destination_path:
print(" result: running in the main worktree; nothing to copy")
elif destination_path.exists():
print(" result: destination already exists; nothing to copy")
elif not source_path.is_file():
print(" result: source does not exist; nothing to copy")
else:
destination_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source_path, destination_path)
print(f" result: copied {repo_relative_path}")
def main() -> None:
print("Codex environment setup:")
# See codex-rs/docs/bazel.md for the repository's Bazel workflow.
copy_from_main_worktree_to_worktree("user.bazelrc")
if __name__ == "__main__":
main()
+29 -4
View File
@@ -29,7 +29,7 @@ Accept any of the following:
4. If `diagnose_ci_failure` is present, inspect failed run logs and classify the failure.
5. If the failure is likely caused by the current branch, patch code locally, commit, and push. Do not patch random flaky tests, CI infrastructure, dependency outages, runner issues, or other failures that are unrelated to the branch.
6. If `process_review_comment` is present, inspect surfaced review items and decide whether to address them.
7. If a review item is actionable and correct, patch code locally, commit, push, and then mark the associated review thread/comment as resolved once the fix is on GitHub.
7. If a review item is actionable and correct, patch code locally, commit, push, and then resolve the associated review thread only when allowed by the GitHub state mutation policy below.
8. Do not post replies to human-authored review comments/threads unless the user explicitly confirms the exact response. If a human review item is non-actionable, already addressed, or not valid, surface the item and recommended response to the user instead of replying on GitHub.
9. If the failure is likely flaky/unrelated and `retry_failed_checks` is present, rerun failed jobs with `--retry-failed-now`.
10. If both actionable review feedback and `retry_failed_checks` are present, prioritize review feedback first; a new commit will retrigger CI, so avoid rerunning flaky checks on the old SHA unless you intentionally defer the review change.
@@ -101,7 +101,7 @@ When you agree with a comment and it is actionable:
1. Patch code locally.
2. Commit with `codex: address PR review feedback (#<n>)`.
3. Push to the PR head branch.
4. After the push succeeds, mark the associated GitHub review thread/comment as resolved.
4. After the push succeeds, resolve the associated GitHub review thread only when allowed by the GitHub state mutation policy below.
5. Resume watching on the new SHA immediately (do not stop after reporting the push).
6. If monitoring was running in `--watch` mode, restart `--watch` immediately after the push in the same turn; do not wait for the user to ask again.
@@ -109,6 +109,31 @@ Do not post replies to human-authored GitHub review comments/threads automatical
If the watcher later surfaces your own approved reply because the authenticated operator is treated as a trusted review author, treat that self-authored item as already handled and do not reply again.
If a code review comment/thread is already marked as resolved in GitHub, treat it as non-actionable and safely ignore it unless new unresolved follow-up feedback appears.
## GitHub State Mutation Policy
You can read any PR state you need for monitoring. Writes must comply with this policy.
You can push PRs to update the code under review or to force CI re-runs as described above.
You can resolve review comment threads from the human who requested babysitting or from the Codex
review bot. When resolving, leave a comment prefixed with `[from Codex]: ` and explain what changes
you made and which commit includes them. Don't touch review threads if other humans other than the
user who requested babysitting have participated.
Before making any changes, fetch the PR state yourself instead of relying on the PR watcher script's
output.
Unless explicitly asked, do not:
* comment on other humans' review threads, communicate with the user in chat instead
* resolve review threads from humans other than the user
* interact with humans other than the user
* mark PRs as drafts or ready for review
* close or reopen PRs
In general, never act on GitHub in ways that would make it hard to tell whether you or the user did
something visible to other humans. When in doubt, ask the user for clarification in chat.
## Git Safety Rules
- Work only on the PR head branch.
@@ -133,10 +158,10 @@ Use this loop in a live Codex session:
3. First check whether the PR is now merged or otherwise closed; if so, report that terminal state and stop polling immediately.
4. Check CI summary, new review items, and mergeability/conflict status.
5. Diagnose CI failures and classify branch-related vs flaky/unrelated. If the overall run is still pending but `failed_jobs` already includes a failed job, fetch that job's logs and diagnose immediately instead of waiting for the whole workflow run to finish. Patch only when the failure is branch-related.
6. For each surfaced review item from another author, patch/commit/push and then resolve it if it is actionable. If it is non-actionable, already addressed, or requires a written answer, surface it to the user with a suggested response instead of posting automatically. If a later snapshot surfaces your own approved reply, treat it as informational and continue without responding again.
6. For each surfaced review item from another author, patch/commit/push if it is actionable, then resolve it only when allowed by the GitHub state mutation policy above. If it is non-actionable, already addressed, or requires a written answer, surface it to the user with a suggested response instead of posting automatically. If a later snapshot surfaces your own approved reply, treat it as informational and continue without responding again.
7. Process actionable review comments before flaky reruns when both are present; if a review fix requires a commit, push it and skip rerunning failed checks on the old SHA.
8. Retry failed checks only when `retry_failed_checks` is present and you are not about to replace the current SHA with a review/CI fix commit. Do not make code changes for unrelated flakes or infrastructure failures just to get CI green.
9. If you pushed a commit, resolved a review thread, or triggered a rerun, report the action briefly and continue polling (do not stop). If a human review comment needs a written GitHub response, stop and ask for confirmation before posting.
9. If you pushed a commit, resolved an eligible review thread, or triggered a rerun, report the action briefly and continue polling (do not stop). If a human review comment needs a written GitHub response, stop and ask for confirmation before posting.
10. After a review-fix push, proactively restart continuous monitoring (`--watch`) in the same turn unless a strict stop condition has already been reached.
11. If everything is passing, mergeable, not blocked on required review approval, and there are no unaddressed review items, report that the PR is currently ready to merge but keep the watcher running so new review comments are surfaced quickly while the PR remains open.
12. If blocked on a user-help-required issue (infra outage, exhausted flaky retries, unclear reviewer request, permissions), report the blocker and stop.
+2
View File
@@ -19,6 +19,8 @@ Limit discussion to the _net change_ of the commit. It is generally frowned upon
Avoid references to absolute paths on my local disk. When talking about a path that is within the repository, simply use the repo-relative path.
Avoid references to confidential information including but not limited to codenames or OpenAI-internal URLs.
It is generally helpful to discuss how the change was verified. That said, it is unnecessary to mention things that CI checks automatically, e.g., do not include "ran `just fmt`" as part of the test plan. Though identifying the new tests that were purposely introduced to verify the new behavior introduced by the pull request is often appropriate.
Make use of Markdown to format the pull request professionally. Ensure "code things" appear in single backticks when referenced inline. Fenced code blocks are useful when referencing code or showing a shell transcript. Also, make use of GitHub permalinks when citing existing pieces of code that are relevant to the change.
+17
View File
@@ -0,0 +1,17 @@
---
name: pushing-ci-changes
description: Pushing GitHub Actions changes, resolving push rejection, requesting upload exceptions.
---
The Codex repository prevents anyone from uploading changes to its CI configuration unless they've
been granted a temporary role.
In order to push changes to `.github/**/*.yml` and related files you'll need the user to read
go/workflow-approvals and request an approval through that flow. It is not possible for you to
make progress by requesting an exemption yourself.
If you know you're about to push changes that would fail due to these restrictions you should still
try to push anyways to confirm the user's account doesn't already have the necessary approval.
If you encounter a failed push due to these restrictions, share the go/workflow-approvals link with
the user and ask them to return control to you once the approval has propagated to GitHub.
+6
View File
@@ -2,6 +2,12 @@
/codex-rs/core/ @openai/codex-core-agent-team
/codex-rs/ext/extension-api/ @openai/codex-core-agent-team
/codex-rs/prompts/ @openai/codex-core-agent-team
/codex-rs/utils/path-uri/ @openai/codex-core-agent-team
# Keep macOS AKV signing changes reviewed by Codex maintainers.
/.github/actions/setup-akv-pkcs11-codesigning/ @openai/codex-core-agent-team
/.github/scripts/macos-signing/ @openai/codex-core-agent-team
/.github/workflows/rust-release.yml @openai/codex-core-agent-team
# Keep ownership changes reviewed by the same team.
/.github/CODEOWNERS @openai/codex-core-agent-team
-259
View File
@@ -1,259 +0,0 @@
name: macos-code-sign
description: Configure, sign, notarize, and clean up macOS code signing artifacts.
inputs:
target:
description: Rust compilation target triple (e.g. aarch64-apple-darwin).
required: true
binaries:
description: Space-delimited binary basenames to sign and notarize.
default: "codex codex-responses-api-proxy"
sign-binaries:
description: Whether to sign and notarize the macOS binaries.
required: false
default: "true"
sign-dmg:
description: Whether to sign and notarize the macOS dmg.
required: false
default: "true"
apple-certificate:
description: Base64-encoded Apple signing certificate (P12).
required: true
apple-certificate-password:
description: Password for the signing certificate.
required: true
apple-notarization-key-p8:
description: Base64-encoded Apple notarization key (P8).
required: true
apple-notarization-key-id:
description: Apple notarization key ID.
required: true
apple-notarization-issuer-id:
description: Apple notarization issuer ID.
required: true
runs:
using: composite
steps:
- name: Configure Apple code signing
shell: bash
env:
KEYCHAIN_PASSWORD: actions
APPLE_CERTIFICATE: ${{ inputs.apple-certificate }}
APPLE_CERTIFICATE_PASSWORD: ${{ inputs.apple-certificate-password }}
run: |
set -euo pipefail
if [[ -z "${APPLE_CERTIFICATE:-}" ]]; then
echo "APPLE_CERTIFICATE is required for macOS signing"
exit 1
fi
if [[ -z "${APPLE_CERTIFICATE_PASSWORD:-}" ]]; then
echo "APPLE_CERTIFICATE_PASSWORD is required for macOS signing"
exit 1
fi
cert_path="${RUNNER_TEMP}/apple_signing_certificate.p12"
echo "$APPLE_CERTIFICATE" | base64 -d > "$cert_path"
keychain_path="${RUNNER_TEMP}/codex-signing.keychain-db"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$keychain_path"
security set-keychain-settings -lut 21600 "$keychain_path"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$keychain_path"
keychain_args=()
cleanup_keychain() {
if ((${#keychain_args[@]} > 0)); then
security list-keychains -s "${keychain_args[@]}" || true
security default-keychain -s "${keychain_args[0]}" || true
else
security list-keychains -s || true
fi
if [[ -f "$keychain_path" ]]; then
security delete-keychain "$keychain_path" || true
fi
}
while IFS= read -r keychain; do
[[ -n "$keychain" ]] && keychain_args+=("$keychain")
done < <(security list-keychains | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/"//g')
if ((${#keychain_args[@]} > 0)); then
security list-keychains -s "$keychain_path" "${keychain_args[@]}"
else
security list-keychains -s "$keychain_path"
fi
security default-keychain -s "$keychain_path"
security import "$cert_path" -k "$keychain_path" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$keychain_path" > /dev/null
codesign_hashes=()
while IFS= read -r hash; do
[[ -n "$hash" ]] && codesign_hashes+=("$hash")
done < <(security find-identity -v -p codesigning "$keychain_path" \
| sed -n 's/.*\([0-9A-F]\{40\}\).*/\1/p' \
| sort -u)
if ((${#codesign_hashes[@]} == 0)); then
echo "No signing identities found in $keychain_path"
cleanup_keychain
rm -f "$cert_path"
exit 1
fi
if ((${#codesign_hashes[@]} > 1)); then
echo "Multiple signing identities found in $keychain_path:"
printf ' %s\n' "${codesign_hashes[@]}"
cleanup_keychain
rm -f "$cert_path"
exit 1
fi
APPLE_CODESIGN_IDENTITY="${codesign_hashes[0]}"
rm -f "$cert_path"
echo "APPLE_CODESIGN_IDENTITY=$APPLE_CODESIGN_IDENTITY" >> "$GITHUB_ENV"
echo "APPLE_CODESIGN_KEYCHAIN=$keychain_path" >> "$GITHUB_ENV"
echo "::add-mask::$APPLE_CODESIGN_IDENTITY"
- name: Sign macOS binaries
if: ${{ inputs.sign-binaries == 'true' }}
shell: bash
env:
TARGET: ${{ inputs.target }}
BINARIES: ${{ inputs.binaries }}
run: |
set -euo pipefail
if [[ -z "${APPLE_CODESIGN_IDENTITY:-}" ]]; then
echo "APPLE_CODESIGN_IDENTITY is required for macOS signing"
exit 1
fi
keychain_args=()
if [[ -n "${APPLE_CODESIGN_KEYCHAIN:-}" && -f "${APPLE_CODESIGN_KEYCHAIN}" ]]; then
keychain_args+=(--keychain "${APPLE_CODESIGN_KEYCHAIN}")
fi
entitlements_path="$GITHUB_ACTION_PATH/codex.entitlements.plist"
for binary in ${BINARIES}; do
path="codex-rs/target/${TARGET}/release/${binary}"
codesign --force --options runtime --timestamp --entitlements "$entitlements_path" --sign "$APPLE_CODESIGN_IDENTITY" "${keychain_args[@]}" "$path"
done
- name: Notarize macOS binaries
if: ${{ inputs.sign-binaries == 'true' }}
shell: bash
env:
TARGET: ${{ inputs.target }}
BINARIES: ${{ inputs.binaries }}
APPLE_NOTARIZATION_KEY_P8: ${{ inputs.apple-notarization-key-p8 }}
APPLE_NOTARIZATION_KEY_ID: ${{ inputs.apple-notarization-key-id }}
APPLE_NOTARIZATION_ISSUER_ID: ${{ inputs.apple-notarization-issuer-id }}
run: |
set -euo pipefail
for var in APPLE_NOTARIZATION_KEY_P8 APPLE_NOTARIZATION_KEY_ID APPLE_NOTARIZATION_ISSUER_ID; do
if [[ -z "${!var:-}" ]]; then
echo "$var is required for notarization"
exit 1
fi
done
notary_key_path="${RUNNER_TEMP}/notarytool.key.p8"
echo "$APPLE_NOTARIZATION_KEY_P8" | base64 -d > "$notary_key_path"
cleanup_notary() {
rm -f "$notary_key_path"
}
trap cleanup_notary EXIT
source "$GITHUB_ACTION_PATH/notary_helpers.sh"
notarize_binary() {
local binary="$1"
local source_path="codex-rs/target/${TARGET}/release/${binary}"
local archive_path="${RUNNER_TEMP}/${binary}.zip"
if [[ ! -f "$source_path" ]]; then
echo "Binary $source_path not found"
exit 1
fi
rm -f "$archive_path"
ditto -c -k --keepParent "$source_path" "$archive_path"
notarize_submission "$binary" "$archive_path" "$notary_key_path"
}
for binary in ${BINARIES}; do
notarize_binary "${binary}"
done
- name: Sign and notarize macOS dmg
if: ${{ inputs.sign-dmg == 'true' }}
shell: bash
env:
TARGET: ${{ inputs.target }}
APPLE_NOTARIZATION_KEY_P8: ${{ inputs.apple-notarization-key-p8 }}
APPLE_NOTARIZATION_KEY_ID: ${{ inputs.apple-notarization-key-id }}
APPLE_NOTARIZATION_ISSUER_ID: ${{ inputs.apple-notarization-issuer-id }}
run: |
set -euo pipefail
for var in APPLE_CODESIGN_IDENTITY APPLE_NOTARIZATION_KEY_P8 APPLE_NOTARIZATION_KEY_ID APPLE_NOTARIZATION_ISSUER_ID; do
if [[ -z "${!var:-}" ]]; then
echo "$var is required"
exit 1
fi
done
notary_key_path="${RUNNER_TEMP}/notarytool.key.p8"
echo "$APPLE_NOTARIZATION_KEY_P8" | base64 -d > "$notary_key_path"
cleanup_notary() {
rm -f "$notary_key_path"
}
trap cleanup_notary EXIT
source "$GITHUB_ACTION_PATH/notary_helpers.sh"
dmg_name="codex-${TARGET}.dmg"
dmg_path="codex-rs/target/${TARGET}/release/${dmg_name}"
if [[ ! -f "$dmg_path" ]]; then
echo "dmg $dmg_path not found"
exit 1
fi
keychain_args=()
if [[ -n "${APPLE_CODESIGN_KEYCHAIN:-}" && -f "${APPLE_CODESIGN_KEYCHAIN}" ]]; then
keychain_args+=(--keychain "${APPLE_CODESIGN_KEYCHAIN}")
fi
codesign --force --timestamp --sign "$APPLE_CODESIGN_IDENTITY" "${keychain_args[@]}" "$dmg_path"
notarize_submission "$dmg_name" "$dmg_path" "$notary_key_path"
xcrun stapler staple "$dmg_path"
- name: Remove signing keychain
if: ${{ always() }}
shell: bash
env:
APPLE_CODESIGN_KEYCHAIN: ${{ env.APPLE_CODESIGN_KEYCHAIN }}
run: |
set -euo pipefail
if [[ -n "${APPLE_CODESIGN_KEYCHAIN:-}" ]]; then
keychain_args=()
while IFS= read -r keychain; do
[[ "$keychain" == "$APPLE_CODESIGN_KEYCHAIN" ]] && continue
[[ -n "$keychain" ]] && keychain_args+=("$keychain")
done < <(security list-keychains | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/"//g')
if ((${#keychain_args[@]} > 0)); then
security list-keychains -s "${keychain_args[@]}"
security default-keychain -s "${keychain_args[0]}"
fi
if [[ -f "$APPLE_CODESIGN_KEYCHAIN" ]]; then
security delete-keychain "$APPLE_CODESIGN_KEYCHAIN"
fi
fi
@@ -1,46 +0,0 @@
#!/usr/bin/env bash
notarize_submission() {
local label="$1"
local path="$2"
local notary_key_path="$3"
if [[ -z "${APPLE_NOTARIZATION_KEY_ID:-}" || -z "${APPLE_NOTARIZATION_ISSUER_ID:-}" ]]; then
echo "APPLE_NOTARIZATION_KEY_ID and APPLE_NOTARIZATION_ISSUER_ID are required for notarization"
exit 1
fi
if [[ -z "$notary_key_path" || ! -f "$notary_key_path" ]]; then
echo "Notary key file $notary_key_path not found"
exit 1
fi
if [[ ! -f "$path" ]]; then
echo "Notarization payload $path not found"
exit 1
fi
local submission_json
submission_json=$(xcrun notarytool submit "$path" \
--key "$notary_key_path" \
--key-id "$APPLE_NOTARIZATION_KEY_ID" \
--issuer "$APPLE_NOTARIZATION_ISSUER_ID" \
--output-format json \
--wait)
local status submission_id
status=$(printf '%s\n' "$submission_json" | jq -r '.status // "Unknown"')
submission_id=$(printf '%s\n' "$submission_json" | jq -r '.id // ""')
if [[ -z "$submission_id" ]]; then
echo "Failed to retrieve submission ID for $label"
exit 1
fi
echo "::notice title=Notarization::$label submission ${submission_id} completed with status ${status}"
if [[ "$status" != "Accepted" ]]; then
echo "Notarization failed for ${label} (submission ${submission_id}, status ${status})"
exit 1
fi
}
@@ -0,0 +1,349 @@
name: Set up AKV PKCS11 code signing
description: Download prebuilt rcodesign and Azure Key Vault PKCS11 provider artifacts, then export macOS signing environment.
inputs:
setup-mode:
description: signing configures Azure and exports signing env vars; tools-only only downloads signing tools.
required: false
default: signing
rcodesign-blob-uri:
description: Azure Blob URI for the prebuilt Linux/amd64 rcodesign binary.
required: true
rcodesign-sha256:
description: Expected SHA-256 digest for the prebuilt rcodesign binary.
required: true
akv-pkcs11-library-blob-uri:
description: Azure Blob URI for the prebuilt Linux/amd64 AKV PKCS11 provider library.
required: true
akv-pkcs11-library-sha256:
description: Expected SHA-256 digest for the prebuilt AKV PKCS11 provider library.
required: true
azure-client-id:
description: GitHub OIDC client ID for the Azure signer application.
required: true
azure-tenant-id:
description: Azure tenant ID for the signer application.
required: true
azure-subscription-id:
description: Azure subscription ID that owns the signing vault.
required: true
key-vault-name:
description: Azure Key Vault name containing the certificate-backed signing key.
required: true
key-name:
description: Key Vault certificate/key name used as the PKCS11 key label.
required: true
key-version:
description: Optional Key Vault key version to pin while signing.
required: false
default: ""
certificate-sha256:
description: Optional expected SHA-256 fingerprint for the downloaded public certificate.
required: false
default: ""
outputs:
pkcs11-library:
description: Path to the downloaded AKV PKCS11 provider library.
value: ${{ steps.paths.outputs.pkcs11_library }}
signing-certificate-pem:
description: Path to the downloaded public signing certificate.
value: ${{ steps.paths.outputs.signing_certificate_pem }}
rcodesign:
description: Path to the downloaded rcodesign binary.
value: ${{ steps.paths.outputs.rcodesign }}
runs:
using: composite
steps:
- name: Validate pinned signing artifacts
shell: bash
env:
SETUP_MODE: ${{ inputs.setup-mode }}
RCODESIGN_BLOB_URI: ${{ inputs.rcodesign-blob-uri }}
RCODESIGN_SHA256: ${{ inputs.rcodesign-sha256 }}
AKV_PKCS11_LIBRARY_BLOB_URI: ${{ inputs.akv-pkcs11-library-blob-uri }}
AKV_PKCS11_LIBRARY_SHA256: ${{ inputs.akv-pkcs11-library-sha256 }}
KEY_VAULT_NAME: ${{ inputs.key-vault-name }}
KEY_NAME: ${{ inputs.key-name }}
run: |
set -euo pipefail
case "$SETUP_MODE" in
signing|tools-only)
;;
*)
echo "setup-mode must be 'signing' or 'tools-only', got '$SETUP_MODE'." >&2
exit 1
;;
esac
for variable_name in RCODESIGN_SHA256 AKV_PKCS11_LIBRARY_SHA256; do
value="${!variable_name}"
if [[ ! "$value" =~ ^[0-9a-f]{64}$ ]]; then
echo "$variable_name must be a lowercase SHA-256 digest." >&2
exit 1
fi
done
for variable_name in RCODESIGN_BLOB_URI AKV_PKCS11_LIBRARY_BLOB_URI; do
value="${!variable_name}"
if [[ ! "$value" =~ ^az://[^/]+/[^/]+/.+ ]]; then
echo "$variable_name must use az://<account>/<container>/<blob>." >&2
exit 1
fi
done
if [[ "$SETUP_MODE" == "signing" ]]; then
for variable_name in \
KEY_VAULT_NAME \
KEY_NAME; do
if [[ -z "${!variable_name}" ]]; then
echo "$variable_name is required for AKV PKCS11 signing." >&2
exit 1
fi
done
fi
- name: Resolve signing tool paths
id: paths
shell: bash
run: |
set -euo pipefail
if [[ "${RUNNER_OS}" != "Linux" ]]; then
echo "Prebuilt AKV PKCS11 signing tools are only vendored for Linux runners, got ${RUNNER_OS}." >&2
exit 1
fi
if [[ "${RUNNER_ARCH}" != "X64" && "${RUNNER_ARCH}" != "AMD64" ]]; then
echo "Prebuilt AKV PKCS11 signing tools are only vendored for amd64 runners, got ${RUNNER_ARCH}." >&2
exit 1
fi
provider_root="${RUNNER_TEMP}/akv-pkcs11-provider"
rcodesign_root="${RUNNER_TEMP}/rcodesign-root"
signing_certificate_pem="${RUNNER_TEMP}/akv-signing-cert.pem"
library_name="libakv_pkcs_11.so"
mkdir -p "$provider_root" "$rcodesign_root/bin"
{
echo "pkcs11_library=$provider_root/$library_name"
echo "pkcs11_manifest=$provider_root/akv-pkcs11-provider.manifest"
echo "rcodesign_root=$rcodesign_root"
echo "rcodesign=$rcodesign_root/bin/rcodesign"
echo "signing_certificate_pem=$signing_certificate_pem"
} >> "$GITHUB_OUTPUT"
- name: Validate Azure credentials for private signing artifacts
shell: bash
env:
AZURE_CLIENT_ID: ${{ inputs.azure-client-id }}
AZURE_TENANT_ID: ${{ inputs.azure-tenant-id }}
AZURE_SUBSCRIPTION_ID: ${{ inputs.azure-subscription-id }}
run: |
set -euo pipefail
for variable_name in AZURE_CLIENT_ID AZURE_TENANT_ID AZURE_SUBSCRIPTION_ID; do
if [[ -z "${!variable_name}" ]]; then
echo "$variable_name is required for private AKV PKCS11 signing artifacts." >&2
exit 1
fi
done
- name: Log in to Azure with GitHub OIDC
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
with:
client-id: ${{ inputs.azure-client-id }}
tenant-id: ${{ inputs.azure-tenant-id }}
subscription-id: ${{ inputs.azure-subscription-id }}
- name: Install prebuilt signing tools
shell: bash
env:
RCODESIGN_BLOB_URI: ${{ inputs.rcodesign-blob-uri }}
RCODESIGN_SHA256: ${{ inputs.rcodesign-sha256 }}
RCODESIGN: ${{ steps.paths.outputs.rcodesign }}
AKV_PKCS11_LIBRARY_BLOB_URI: ${{ inputs.akv-pkcs11-library-blob-uri }}
AKV_PKCS11_LIBRARY_SHA256: ${{ inputs.akv-pkcs11-library-sha256 }}
PKCS11_LIBRARY: ${{ steps.paths.outputs.pkcs11_library }}
PKCS11_MANIFEST: ${{ steps.paths.outputs.pkcs11_manifest }}
run: |
set -euo pipefail
download_az_blob_uri() {
local uri="$1"
local destination="$2"
local rest account container blob
rest="${uri#az://}"
account="${rest%%/*}"
rest="${rest#*/}"
container="${rest%%/*}"
blob="${rest#*/}"
if [[ -z "$account" || -z "$container" || -z "$blob" || "$blob" == "$rest" ]]; then
echo "Invalid Azure Blob URI. Expected az://<account>/<container>/<blob>." >&2
exit 1
fi
mkdir -p "$(dirname "$destination")"
rm -f "$destination"
if ! az storage blob download \
--account-name "$account" \
--container-name "$container" \
--name "$blob" \
--file "$destination" \
--auth-mode login \
--only-show-errors \
>/dev/null 2>&1; then
echo "Failed to download a private signing artifact from Azure Blob Storage." >&2
exit 1
fi
}
verify_sha256() {
local path="$1"
local expected="$2"
local actual
actual="$(shasum -a 256 "$path" | awk '{ print $1 }')"
if [[ "$actual" != "$expected" ]]; then
echo "SHA-256 verification failed for '$path'." >&2
exit 1
fi
}
echo "Downloading prebuilt rcodesign."
download_az_blob_uri "$RCODESIGN_BLOB_URI" "$RCODESIGN"
verify_sha256 "$RCODESIGN" "$RCODESIGN_SHA256"
chmod 0755 "$RCODESIGN"
echo "Downloading prebuilt AKV PKCS11 provider."
download_az_blob_uri "$AKV_PKCS11_LIBRARY_BLOB_URI" "$PKCS11_LIBRARY"
verify_sha256 "$PKCS11_LIBRARY" "$AKV_PKCS11_LIBRARY_SHA256"
chmod 0644 "$PKCS11_LIBRARY"
{
echo "runner_os=$RUNNER_OS"
echo "runner_arch=$RUNNER_ARCH"
echo "library_name=$(basename "$PKCS11_LIBRARY")"
} > "$PKCS11_MANIFEST"
- name: Verify downloaded signing tools
shell: bash
env:
RCODESIGN: ${{ steps.paths.outputs.rcodesign }}
RCODESIGN_SHA256: ${{ inputs.rcodesign-sha256 }}
PKCS11_LIBRARY: ${{ steps.paths.outputs.pkcs11_library }}
AKV_PKCS11_LIBRARY_SHA256: ${{ inputs.akv-pkcs11-library-sha256 }}
PKCS11_MANIFEST: ${{ steps.paths.outputs.pkcs11_manifest }}
run: |
set -euo pipefail
verify_sha256() {
local path="$1"
local expected="$2"
local actual
actual="$(shasum -a 256 "$path" | awk '{ print $1 }')"
if [[ "$actual" != "$expected" ]]; then
echo "SHA-256 verification failed for '$path'." >&2
exit 1
fi
}
if [[ ! -x "$RCODESIGN" ]]; then
echo "rcodesign is missing or not executable at '$RCODESIGN'." >&2
exit 1
fi
if [[ ! -f "$PKCS11_LIBRARY" ]]; then
echo "AKV PKCS11 provider library is missing at '$PKCS11_LIBRARY'." >&2
exit 1
fi
verify_sha256 "$RCODESIGN" "$RCODESIGN_SHA256"
verify_sha256 "$PKCS11_LIBRARY" "$AKV_PKCS11_LIBRARY_SHA256"
"$RCODESIGN" --version
"$RCODESIGN" notarize --help > /dev/null
if [[ -f "$PKCS11_MANIFEST" ]]; then
echo "AKV PKCS11 provider artifact manifest is present."
else
echo "AKV PKCS11 provider artifact manifest is absent." >&2
exit 1
fi
- name: Download signing certificate from Key Vault
if: ${{ inputs.setup-mode == 'signing' }}
shell: bash
env:
KEY_VAULT_NAME: ${{ inputs.key-vault-name }}
KEY_NAME: ${{ inputs.key-name }}
KEY_VERSION: ${{ inputs.key-version }}
CERTIFICATE_SHA256: ${{ inputs.certificate-sha256 }}
SIGNING_CERTIFICATE_PEM: ${{ steps.paths.outputs.signing_certificate_pem }}
run: |
set -euo pipefail
certificate_version_args=()
if [[ -n "$KEY_VERSION" ]]; then
certificate_version_args+=(--version "$KEY_VERSION")
fi
if ! az keyvault certificate download \
--vault-name "$KEY_VAULT_NAME" \
--name "$KEY_NAME" \
"${certificate_version_args[@]}" \
--file "$SIGNING_CERTIFICATE_PEM" \
--encoding PEM \
--only-show-errors \
>/dev/null 2>&1; then
echo "Failed to download the public signing certificate from Azure Key Vault." >&2
exit 1
fi
if [[ -n "$CERTIFICATE_SHA256" ]]; then
actual_sha256="$(
openssl x509 -in "$SIGNING_CERTIFICATE_PEM" -noout -fingerprint -sha256 |
awk -F= '{ print toupper($2) }' |
tr -d ':\r\n'
)"
expected_sha256="$(printf '%s' "$CERTIFICATE_SHA256" | tr '[:lower:]' '[:upper:]' | tr -d ':\r\n ')"
if [[ "$actual_sha256" != "$expected_sha256" ]]; then
echo "Downloaded signing certificate SHA-256 did not match the expected fingerprint." >&2
exit 1
fi
fi
- name: Export AKV PKCS11 signing environment
if: ${{ inputs.setup-mode == 'signing' }}
shell: bash
env:
RCODESIGN_ROOT: ${{ steps.paths.outputs.rcodesign_root }}
PKCS11_LIBRARY: ${{ steps.paths.outputs.pkcs11_library }}
SIGNING_CERTIFICATE_PEM: ${{ steps.paths.outputs.signing_certificate_pem }}
KEY_VAULT_NAME: ${{ inputs.key-vault-name }}
KEY_NAME: ${{ inputs.key-name }}
KEY_VERSION: ${{ inputs.key-version }}
run: |
set -euo pipefail
{
echo "$RCODESIGN_ROOT/bin"
} >> "$GITHUB_PATH"
{
echo "OAI_CODESIGN_BACKEND=akv-pkcs11"
echo "OAI_AKV_PKCS11_LIBRARY=$PKCS11_LIBRARY"
echo "OAI_AKV_SIGNING_CERTIFICATE_PEM=$SIGNING_CERTIFICATE_PEM"
echo "OAI_AKV_KEY_LABEL=$KEY_NAME"
echo "AZURE_CREDENTIAL_KIND=azurecli"
echo "AZURE_KEYVAULT_NAME=$KEY_VAULT_NAME"
if [[ -n "$KEY_VERSION" ]]; then
echo "AZURE_KEYVAULT_KEY_VERSION=$KEY_VERSION"
fi
} >> "$GITHUB_ENV"
@@ -0,0 +1,119 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage: archive-release-symbols-and-strip-binaries.sh \
--target <rust-target> \
--artifact-name <artifact-name> \
--release-dir <dir> \
--archive-dir <dir> \
--binaries "<space-delimited binary basenames>"
EOF
}
target=""
artifact_name=""
release_dir=""
archive_dir=""
binaries=""
while [[ $# -gt 0 ]]; do
case "$1" in
--target)
target="${2:?--target requires a value}"
shift 2
;;
--artifact-name)
artifact_name="${2:?--artifact-name requires a value}"
shift 2
;;
--release-dir)
release_dir="${2:?--release-dir requires a value}"
shift 2
;;
--archive-dir)
archive_dir="${2:?--archive-dir requires a value}"
shift 2
;;
--binaries)
binaries="${2:?--binaries requires a value}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unexpected argument: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ -z "$target" || -z "$artifact_name" || -z "$release_dir" || -z "$archive_dir" || -z "$binaries" ]]; then
usage >&2
exit 1
fi
symbols_root="${RUNNER_TEMP:-/tmp}/codex-symbols-${artifact_name}"
symbols_dir="${symbols_root}/codex-symbols-${artifact_name}"
archive_path="${archive_dir%/}/codex-symbols-${artifact_name}.tar.gz"
rm -rf "$symbols_root"
mkdir -p "$symbols_dir" "$archive_dir"
read -r -a binary_names <<< "$binaries"
case "$target" in
*apple-darwin)
for binary in "${binary_names[@]}"; do
binary_path="${release_dir%/}/${binary}"
dsym_path="${binary_path}.dSYM"
if [[ ! -f "$binary_path" ]]; then
echo "Binary $binary_path not found" >&2
exit 1
fi
if [[ ! -d "$dsym_path" ]]; then
echo "dSYM $dsym_path not found" >&2
exit 1
fi
cp -RL "$dsym_path" "${symbols_dir}/${binary}.dSYM"
strip -S -x "$binary_path"
done
;;
*linux*)
objcopy_bin="${OBJCOPY:-objcopy}"
strip_bin="${STRIP:-strip}"
for binary in "${binary_names[@]}"; do
binary_path="${release_dir%/}/${binary}"
debug_path="${symbols_dir}/${binary}.debug"
if [[ ! -f "$binary_path" ]]; then
echo "Binary $binary_path not found" >&2
exit 1
fi
"$objcopy_bin" --only-keep-debug "$binary_path" "$debug_path"
"$strip_bin" --strip-debug --strip-unneeded "$binary_path"
"$objcopy_bin" --add-gnu-debuglink="$debug_path" "$binary_path"
done
;;
*windows*)
for binary in "${binary_names[@]}"; do
pdb_path="${release_dir%/}/${binary}.pdb"
if [[ ! -f "$pdb_path" ]]; then
echo "PDB $pdb_path not found" >&2
exit 1
fi
cp "$pdb_path" "${symbols_dir}/${binary}.pdb"
done
;;
*)
echo "No symbols packaging support for target: $target" >&2
exit 1
;;
esac
rm -f "$archive_path"
tar -C "$symbols_root" -czf "$archive_path" "codex-symbols-${artifact_name}"
@@ -0,0 +1,131 @@
#!/usr/bin/env bash
# Submits a signed standalone macOS binary to Apple notarization through
# rcodesign. Standalone binaries cannot carry a stapled ticket, so the binary
# is submitted in a ZIP and the successful notarization log is retained.
set -euo pipefail
usage() {
cat >&2 <<'EOF'
Usage: notarize_macos_binary_with_rcodesign.sh --binary PATH [--report-dir PATH] [--max-wait-seconds SECONDS]
Options:
--binary PATH Signed standalone macOS binary to notarize.
--report-dir PATH Directory for notarization logs.
--max-wait-seconds SECONDS Maximum rcodesign notarization wait time.
EOF
}
binary_path=""
report_dir="${RUNNER_TEMP:-/tmp}/macos-binary-notarization-verification"
max_wait_seconds="600"
while [[ $# -gt 0 ]]; do
case "$1" in
--binary)
binary_path="${2:-}"
shift 2
;;
--report-dir)
report_dir="${2:-}"
shift 2
;;
--max-wait-seconds)
max_wait_seconds="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown notarization argument: $1" >&2
usage
exit 2
;;
esac
done
if [[ -z "$binary_path" ]]; then
echo "--binary is required." >&2
usage
exit 2
fi
if [[ ! -f "$binary_path" ]]; then
echo "Binary does not exist: $binary_path" >&2
exit 1
fi
if [[ ! "$max_wait_seconds" =~ ^[0-9]+$ ]]; then
echo "--max-wait-seconds must be a non-negative integer." >&2
exit 2
fi
for command_name in rcodesign zip; do
if ! command -v "$command_name" >/dev/null 2>&1; then
echo "$command_name was not found on PATH." >&2
exit 1
fi
done
missing_environment=0
for variable_name in \
APPLE_NOTARIZATION_ISSUER_ID \
APPLE_NOTARIZATION_KEY_ID \
APPLE_NOTARIZATION_KEY_P8
do
if [[ -z "${!variable_name:-}" ]]; then
echo "$variable_name must be set from CI secrets before notarizing a binary." >&2
missing_environment=1
fi
done
if [[ "$missing_environment" -ne 0 ]]; then
exit 2
fi
mkdir -p "$report_dir"
notarization_temp_dir="$(mktemp -d)"
trap 'rm -rf "$notarization_temp_dir" >/dev/null' EXIT
private_key_path="$notarization_temp_dir/AuthKey_${APPLE_NOTARIZATION_KEY_ID}.p8"
if ! printf '%s' "$APPLE_NOTARIZATION_KEY_P8" | base64 --decode >"$private_key_path" 2>/dev/null; then
if ! printf '%s' "$APPLE_NOTARIZATION_KEY_P8" | base64 -D >"$private_key_path" 2>/dev/null; then
echo "APPLE_NOTARIZATION_KEY_P8 must be a base64-encoded .p8 private key." >&2
exit 2
fi
fi
chmod 600 "$private_key_path"
api_key_path="$notarization_temp_dir/app-store-connect-api-key.json"
rcodesign encode-app-store-connect-api-key \
--output-path "$api_key_path" \
"$APPLE_NOTARIZATION_ISSUER_ID" \
"$APPLE_NOTARIZATION_KEY_ID" \
"$private_key_path" \
>"$report_dir/encode-app-store-connect-api-key.log" 2>&1
binary_name="$(basename "$binary_path")"
archive_path="$notarization_temp_dir/${binary_name}.zip"
(
cd "$(dirname "$binary_path")"
zip -q "$archive_path" "$binary_name"
)
notarization_log="$report_dir/${binary_name}-notarization.log"
rcodesign notarize \
--api-key-file "$api_key_path" \
--max-wait-seconds "$max_wait_seconds" \
--wait \
"$archive_path" \
2>&1 | tee "$notarization_log"
{
echo "binary_name=$binary_name"
echo "max_wait_seconds=$max_wait_seconds"
echo "binary_sha256=$(shasum -a 256 "$binary_path" | awk '{ print $1 }')"
echo "rcodesign_notarize=completed"
} >"$report_dir/${binary_name}-notarization-summary.txt"
@@ -0,0 +1,124 @@
#!/usr/bin/env bash
# Notarizes and staples a signed macOS DMG through rcodesign.
#
# This is the Linux-compatible notarization path for the AKV/PKCS#11 signing
# flow. It records notarization inputs and logs so workflow artifacts can be
# audited without exposing the App Store Connect private key.
set -euo pipefail
usage() {
cat >&2 <<'EOF'
Usage: notarize_macos_dmg_with_rcodesign.sh --dmg PATH [--report-dir PATH] [--max-wait-seconds SECONDS]
Options:
--dmg PATH Signed DMG to submit to Apple notarization.
--report-dir PATH Directory for notarization logs.
--max-wait-seconds SECONDS Maximum rcodesign notarization wait time.
EOF
}
dmg_path=""
report_dir="${RUNNER_TEMP:-/tmp}/macos-notarization-verification"
max_wait_seconds="600"
while [[ $# -gt 0 ]]; do
case "$1" in
--dmg)
dmg_path="${2:-}"
shift 2
;;
--report-dir)
report_dir="${2:-}"
shift 2
;;
--max-wait-seconds)
max_wait_seconds="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown notarization argument: $1" >&2
usage
exit 2
;;
esac
done
if [[ -z "$dmg_path" ]]; then
echo "--dmg is required." >&2
usage
exit 2
fi
if [[ ! -f "$dmg_path" ]]; then
echo "DMG does not exist: $dmg_path" >&2
exit 1
fi
if [[ ! "$max_wait_seconds" =~ ^[0-9]+$ ]]; then
echo "--max-wait-seconds must be a non-negative integer." >&2
exit 2
fi
if ! command -v rcodesign > /dev/null 2>&1; then
echo "rcodesign was not found on PATH." >&2
exit 1
fi
missing_environment=0
for variable_name in \
APPLE_NOTARIZATION_ISSUER_ID \
APPLE_NOTARIZATION_KEY_ID \
APPLE_NOTARIZATION_KEY_P8
do
if [[ -z "${!variable_name:-}" ]]; then
echo "$variable_name must be set from CI secrets before notarizing a DMG." >&2
missing_environment=1
fi
done
if [[ "$missing_environment" -ne 0 ]]; then
exit 2
fi
mkdir -p "$report_dir"
notarization_temp_dir="$(mktemp -d)"
trap 'rm -rf "$notarization_temp_dir" > /dev/null' EXIT
private_key_path="$notarization_temp_dir/AuthKey_${APPLE_NOTARIZATION_KEY_ID}.p8"
if ! printf '%s' "$APPLE_NOTARIZATION_KEY_P8" | base64 --decode > "$private_key_path" 2> /dev/null; then
if ! printf '%s' "$APPLE_NOTARIZATION_KEY_P8" | base64 -D > "$private_key_path" 2> /dev/null; then
echo "APPLE_NOTARIZATION_KEY_P8 must be a base64-encoded .p8 private key." >&2
exit 2
fi
fi
chmod 600 "$private_key_path"
api_key_path="$notarization_temp_dir/app-store-connect-api-key.json"
rcodesign encode-app-store-connect-api-key \
--output-path "$api_key_path" \
"$APPLE_NOTARIZATION_ISSUER_ID" \
"$APPLE_NOTARIZATION_KEY_ID" \
"$private_key_path" \
> "$report_dir/encode-app-store-connect-api-key.log" 2>&1
notarization_log="$report_dir/dmg-notarization.log"
rcodesign notarize \
--api-key-file "$api_key_path" \
--max-wait-seconds "$max_wait_seconds" \
--staple \
"$dmg_path" \
2>&1 | tee "$notarization_log"
{
echo "dmg_path=$dmg_path"
echo "max_wait_seconds=$max_wait_seconds"
echo "dmg_sha256=$(shasum -a 256 "$dmg_path" | awk '{ print $1 }')"
echo "rcodesign_notarize_staple=completed"
} > "$report_dir/dmg-notarization-summary.txt"
+245
View File
@@ -0,0 +1,245 @@
#!/usr/bin/env bash
# Small compatibility wrapper around native codesign and rcodesign.
#
# Existing packaging scripts call this instead of choosing a signing backend
# directly. OAI_CODESIGN_BACKEND=akv-pkcs11 routes signing through rcodesign
# while preserving the option, entitlement, identifier, timestamp, and deep
# signing surface used by the native codesign path.
set -euo pipefail
usage() {
cat >&2 <<'EOF'
Usage: sign_macos_code.sh --target PATH --identity IDENTITY [options]
Options:
--deep true|false
--entitlements PATH
--identifier IDENTIFIER
--identity IDENTITY
--options FLAGS
--target PATH
--timestamp true|false|none
EOF
}
target=""
identity=""
options=""
entitlements_file=""
identifier=""
deep="false"
timestamp="true"
while [[ $# -gt 0 ]]; do
case "$1" in
--deep)
deep="${2:-}"
shift 2
;;
--entitlements)
entitlements_file="${2:-}"
shift 2
;;
--identifier)
identifier="${2:-}"
shift 2
;;
--identity)
identity="${2:-}"
shift 2
;;
--options)
options="${2:-}"
shift 2
;;
--target)
target="${2:-}"
shift 2
;;
--timestamp)
timestamp="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown signing argument: $1" >&2
usage
exit 2
;;
esac
done
if [[ -z "$target" ]]; then
echo "--target is required." >&2
usage
exit 2
fi
if [[ ! -e "$target" ]]; then
echo "Signing target does not exist: $target" >&2
exit 1
fi
case "$deep" in
true|false) ;;
*)
echo "--deep must be true or false, got '$deep'." >&2
exit 2
;;
esac
case "$timestamp" in
true|false|none) ;;
*)
echo "--timestamp must be true, false, or none, got '$timestamp'." >&2
exit 2
;;
esac
sign_with_codesign() {
if [[ -z "$identity" ]]; then
echo "Native codesign requires --identity." >&2
exit 2
fi
local -a args
args=(--force)
if [[ "$deep" == "true" ]]; then
args+=(--deep)
fi
if [[ -n "$options" ]]; then
args+=(--options "$options")
fi
case "$timestamp" in
true)
args+=(--timestamp)
;;
false|none)
args+=(--timestamp=none)
;;
esac
if [[ -n "$entitlements_file" ]]; then
args+=(--entitlements "$entitlements_file")
fi
if [[ -n "$identifier" ]]; then
args+=(--identifier "$identifier")
fi
args+=(--sign "$identity" "$target")
codesign "${args[@]}"
}
append_rcodesign_flags() {
local raw_options="$1"
local option=""
if [[ -z "$raw_options" ]]; then
return 0
fi
IFS=',' read -ra split_options <<< "$raw_options"
for option in "${split_options[@]}"; do
option="${option//[[:space:]]/}"
[[ -z "$option" ]] && continue
case "$option" in
host|hard|kill|expires|restrict|library|runtime|linker-signed)
rcodesign_args+=(--code-signature-flags "$option")
;;
*)
echo "Unsupported rcodesign code signature option: $option" >&2
exit 2
;;
esac
done
}
rcodesign_options_require_notarization() {
local raw_options="$1"
local option=""
if [[ -z "$raw_options" || "$timestamp" != "true" ]]; then
return 1
fi
IFS=',' read -ra split_options <<< "$raw_options"
for option in "${split_options[@]}"; do
option="${option//[[:space:]]/}"
if [[ "$option" == "runtime" ]]; then
return 0
fi
done
return 1
}
sign_with_rcodesign() {
: "${OAI_AKV_PKCS11_LIBRARY:?OAI_AKV_PKCS11_LIBRARY is required for AKV PKCS11 signing.}"
: "${OAI_AKV_SIGNING_CERTIFICATE_PEM:?OAI_AKV_SIGNING_CERTIFICATE_PEM is required for AKV PKCS11 signing.}"
: "${OAI_AKV_KEY_LABEL:?OAI_AKV_KEY_LABEL is required for AKV PKCS11 signing.}"
if ! command -v rcodesign >/dev/null 2>&1; then
echo "rcodesign was not found on PATH." >&2
exit 1
fi
local -a rcodesign_args
rcodesign_args=(
sign
--config-file /dev/null
--pkcs11-library "$OAI_AKV_PKCS11_LIBRARY"
--pkcs11-certificate-file "$OAI_AKV_SIGNING_CERTIFICATE_PEM"
--pkcs11-key-label "$OAI_AKV_KEY_LABEL"
)
if [[ "$deep" == "false" ]]; then
rcodesign_args+=(--shallow)
fi
case "$timestamp" in
true)
;;
false|none)
rcodesign_args+=(--timestamp-url none)
;;
esac
append_rcodesign_flags "$options"
if rcodesign_options_require_notarization "$options"; then
rcodesign_args+=(--for-notarization)
fi
if [[ -n "$entitlements_file" ]]; then
rcodesign_args+=(--entitlements-xml-file "$entitlements_file")
fi
if [[ -n "$identifier" ]]; then
rcodesign_args+=(--binary-identifier "$identifier")
fi
rcodesign_args+=("$target")
rcodesign "${rcodesign_args[@]}"
}
case "${OAI_CODESIGN_BACKEND:-codesign}" in
codesign|"")
sign_with_codesign
;;
akv-pkcs11)
sign_with_rcodesign
;;
*)
echo "Unsupported OAI_CODESIGN_BACKEND: ${OAI_CODESIGN_BACKEND}" >&2
exit 2
;;
esac
+6 -14
View File
@@ -4,7 +4,8 @@ set -euo pipefail
# Run target-discovery queries with the same startup settings as the main
# build/test invocation so they can reuse the same Bazel server. Queries only
# enumerate labels, so they intentionally do not select CI or remote configs.
# enumerate labels, so they intentionally do not select a CI build/test config
# or remote execution.
if [[ $# -lt 2 || "${@: -2:1}" != "--" ]]; then
echo "Usage: $0 [<bazel query args>...] -- <query expression>" >&2
@@ -14,21 +15,16 @@ fi
query_args=("${@:1:$#-2}")
query_expression="${@: -1}"
bazel_startup_args=()
if [[ -n "${BAZEL_OUTPUT_USER_ROOT:-}" ]]; then
bazel_startup_args+=("--output_user_root=${BAZEL_OUTPUT_USER_ROOT}")
fi
run_bazel() {
if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
MSYS2_ARG_CONV_EXCL='*' bazel "$@"
MSYS2_ARG_CONV_EXCL='*' "$(dirname "${BASH_SOURCE[0]}")/run_bazel_with_buildbuddy.py" "$@"
return
fi
bazel "$@"
"$(dirname "${BASH_SOURCE[0]}")/run_bazel_with_buildbuddy.py" "$@"
}
bazel_query_args=(--noexperimental_remote_repo_contents_cache query)
bazel_query_args=(query)
if [[ -n "${BAZEL_REPO_CONTENTS_CACHE:-}" ]]; then
bazel_query_args+=("--repo_contents_cache=${BAZEL_REPO_CONTENTS_CACHE}")
@@ -43,8 +39,4 @@ if (( ${#query_args[@]} > 0 )); then
fi
bazel_query_args+=("$query_expression")
if (( ${#bazel_startup_args[@]} > 0 )); then
run_bazel "${bazel_startup_args[@]}" "${bazel_query_args[@]}"
else
run_bazel "${bazel_query_args[@]}"
fi
run_bazel "${bazel_query_args[@]}"
+42 -1
View File
@@ -22,6 +22,47 @@ REMOTE_EXECUTION_CONFIGS = {
"--config=ci-v8",
"--config=ci-windows-cross",
}
# Honor either explicit setting so the wrapper never overrides the caller's
# choice when it supplies the CI default below.
REMOTE_REPO_CONTENTS_CACHE_STARTUP_OPTIONS = {
"--experimental_remote_repo_contents_cache",
"--noexperimental_remote_repo_contents_cache",
}
def startup_args(args: Sequence[str], env: Mapping[str, str]) -> list[str]:
"""Return shared startup options that are missing from a Bazel invocation.
Bazel startup options must precede the command, and changing them restarts
the server and discards its analysis cache. GitHub Actions invokes Bazel
through several helpers, so normalize their startup options here while
preserving any explicit choice made by the caller.
"""
command_idx = next(
(idx for idx, arg in enumerate(args) if not arg.startswith("-")),
len(args),
)
configured_startup_args = args[:command_idx]
injected_args = []
output_user_root = env.get("BAZEL_OUTPUT_USER_ROOT")
if output_user_root and not any(
arg.startswith("--output_user_root=") for arg in configured_startup_args
):
injected_args.append(f"--output_user_root={output_user_root}")
if env.get("GITHUB_ACTIONS") == "true" and not any(
arg in REMOTE_REPO_CONTENTS_CACHE_STARTUP_OPTIONS
for arg in configured_startup_args
):
# Work around Bazel 9 overlay materialization failures seen in CI. This
# disables only the startup-level repo contents cache; keyed runs still
# use BuildBuddy.
injected_args.append("--noexperimental_remote_repo_contents_cache")
return injected_args
# Only authenticated workflow runs executing trusted upstream code may use the
# OpenAI BuildBuddy host. A pull request event without proof that its head is
# in the upstream repository fails closed to the generic host.
@@ -114,7 +155,7 @@ def bazel_args_with_remote_config(
def bazel_command(*args: str, env: Mapping[str, str] | None = None) -> list[str]:
env = os.environ if env is None else env
bazel = env.get("CODEX_BAZEL_BIN", "bazel")
return [bazel, *bazel_args_with_remote_config(args, env)]
return [bazel, *startup_args(args, env), *bazel_args_with_remote_config(args, env)]
def main() -> None:
@@ -182,6 +182,38 @@ class RunBazelWithBuildBuddyTest(unittest.TestCase):
["fake-bazel", "info", "execution_root"],
)
def test_bazel_command_normalizes_github_actions_startup_options(self) -> None:
env = {
"BAZEL_OUTPUT_USER_ROOT": "/tmp/bazel-output",
"GITHUB_ACTIONS": "true",
}
self.assertEqual(
run_bazel_with_buildbuddy.bazel_command("build", "//codex-rs/...", env=env),
[
"bazel",
"--output_user_root=/tmp/bazel-output",
"--noexperimental_remote_repo_contents_cache",
"build",
"//codex-rs/...",
],
)
self.assertEqual(
run_bazel_with_buildbuddy.bazel_command(
"--experimental_remote_repo_contents_cache",
"build",
"//codex-rs/...",
env=env,
),
[
"bazel",
"--output_user_root=/tmp/bazel-output",
"--experimental_remote_repo_contents_cache",
"build",
"//codex-rs/...",
],
)
def test_main_preserves_spaced_argument_and_child_exit_status(self) -> None:
spaced_arg = (
r"--test_env=PATH=C:\Program Files\PowerShell\7;C:\Program Files\Git\bin"
@@ -191,7 +223,9 @@ class RunBazelWithBuildBuddyTest(unittest.TestCase):
)
env = os.environ.copy()
env["CODEX_BAZEL_BIN"] = sys.executable
env.pop("BAZEL_OUTPUT_USER_ROOT", None)
env.pop("BUILDBUDDY_API_KEY", None)
env.pop("GITHUB_ACTIONS", None)
result = subprocess.run(
[
+36 -21
View File
@@ -20,8 +20,9 @@ jobs:
test:
# PRs use the sharded Windows cross-compiled test jobs below. Post-merge
# pushes to main also run the native Windows test job for broader Windows
# signal without putting PR latency back on the critical path. Cargo CI
# owns V8/code-mode test coverage for now.
# signal without putting PR latency back on the critical path. When
# authenticated RBE is available, the Windows-cross shards exercise the
# source-built V8/code-mode targets.
timeout-minutes: 30
strategy:
fail-fast: false
@@ -49,6 +50,9 @@ jobs:
# Configure a human readable name for each job
name: Bazel test on ${{ matrix.os }} for ${{ matrix.target }}
environment:
name: bazel
deployment: false
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -91,9 +95,9 @@ jobs:
# path. V8 consumers under `//codex-rs/...` still participate
# transitively through `//...`.
-//third_party/v8:all
# V8-backed code-mode tests are covered by Cargo CI. Bazel CI
# cross-compiles in several legs, and those tests are not stable in
# that setup yet.
# Keep V8-backed code-mode tests out of the ordinary macOS/Linux
# legs; authenticated Windows-cross shards below exercise the
# source-built gnullvm V8 path.
-//codex-rs/code-mode:code-mode-unit-tests
-//codex-rs/v8-poc:v8-poc-unit-tests
)
@@ -135,9 +139,9 @@ jobs:
key: ${{ steps.prepare_bazel.outputs.repository-cache-key }}
test-windows-shard:
# Split the Windows Bazel test leg across separate Windows
# hosts. Each shard still uses Linux RBE for build actions, but the test
# execution itself happens on its own Windows runner.
# Split the Windows Bazel test leg across separate Windows hosts. Jobs with
# BuildBuddy credentials use Linux RBE for build actions; test execution
# remains on a Windows runner.
timeout-minutes: 30
strategy:
fail-fast: false
@@ -148,9 +152,12 @@ jobs:
- 3
- 4
runs-on:
group: codex-runners
labels: codex-windows-x64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-x64
name: Bazel test on windows-latest for x86_64-pc-windows-gnullvm shard ${{ matrix.shard }}/4
environment:
name: bazel
deployment: false
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -183,9 +190,9 @@ jobs:
run: |
set -euo pipefail
bazel_test_query='tests(//...) except tests(//third_party/v8:all) except //codex-rs/code-mode:code-mode-unit-tests except //codex-rs/v8-poc:v8-poc-unit-tests except attr(tags, "manual", tests(//...))'
bazel_test_query='tests(//...) except tests(//third_party/v8:all) except attr(tags, "manual", tests(//...))'
mapfile -t bazel_targets < <(
MSYS2_ARG_CONV_EXCL='*' bazel query --output=label "${bazel_test_query}" \
./.github/scripts/run-bazel-query-ci.sh --output=label -- "${bazel_test_query}" \
| LC_ALL=C sort
)
@@ -260,9 +267,12 @@ jobs:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
timeout-minutes: 40
runs-on:
group: codex-runners
labels: codex-windows-x64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-x64
name: Bazel test on windows-latest for x86_64-pc-windows-gnullvm (native main)
environment:
name: bazel
deployment: false
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -289,9 +299,8 @@ jobs:
# path. V8 consumers under `//codex-rs/...` still participate
# transitively through `//...`.
-//third_party/v8:all
# Keep this aligned with the main Bazel job. The native Windows
# job preserves broad post-merge coverage, but code-mode/V8 tests
# are covered by Cargo CI rather than Bazel for now.
# Keep this job broad and cheap; authenticated Windows-cross jobs
# add source-built V8-backed code-mode coverage.
-//codex-rs/code-mode:code-mode-unit-tests
-//codex-rs/v8-poc:v8-poc-unit-tests
)
@@ -348,10 +357,13 @@ jobs:
- os: windows-latest
target: x86_64-pc-windows-gnullvm
runs_on:
group: codex-runners
labels: codex-windows-x64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-x64
runs-on: ${{ matrix.runs_on || matrix.os }}
name: Bazel clippy on ${{ matrix.os }} for ${{ matrix.target }}
environment:
name: bazel
deployment: false
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -441,10 +453,13 @@ jobs:
- os: windows-latest
target: x86_64-pc-windows-gnullvm
runs_on:
group: codex-runners
labels: codex-windows-x64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-x64
runs-on: ${{ matrix.runs_on || matrix.os }}
name: Verify release build on ${{ matrix.os }} for ${{ matrix.target }}
environment:
name: bazel
deployment: false
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+121
View File
@@ -0,0 +1,121 @@
name: python-runtime-build
on:
workflow_call:
inputs:
runtime_version:
description: "Runtime version to build, for example 0.136.0 or 0.136.0a2."
required: true
type: string
jobs:
build-python-runtime:
if: github.repository == 'openai/codex'
name: build-python-runtime
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Validate and resolve Python runtime release
id: python_runtime
shell: bash
env:
REQUESTED_RUNTIME_VERSION: ${{ inputs.runtime_version }}
run: |
set -euo pipefail
python3 - <<'PY'
import os
import re
from pathlib import Path
python_version = os.environ["REQUESTED_RUNTIME_VERSION"]
if match := re.fullmatch(r"([0-9]+\.[0-9]+\.[0-9]+)a([0-9]+)", python_version):
release_version = f"{match.group(1)}-alpha.{match.group(2)}"
elif re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", python_version):
release_version = python_version
else:
raise SystemExit(
"Python runtime version must be stable or a numbered alpha, "
f"for example 0.136.0 or 0.136.0a2; found {python_version}"
)
with Path(os.environ["GITHUB_OUTPUT"]).open("a") as output:
print(f"python_version={python_version}", file=output)
print(f"release_tag=rust-v{release_version}", file=output)
PY
- name: Download Python runtime release artifacts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PYTHON_RUNTIME_VERSION: ${{ steps.python_runtime.outputs.python_version }}
RELEASE_TAG: ${{ steps.python_runtime.outputs.release_tag }}
run: |
set -euo pipefail
mkdir -p dist/python-runtime dist/python-runtime-packages
gh release download "$RELEASE_TAG" \
--repo "${GITHUB_REPOSITORY}" \
--pattern "openai_codex_cli_bin-${PYTHON_RUNTIME_VERSION}-*.whl" \
--dir dist/python-runtime
gh release download "$RELEASE_TAG" \
--repo "${GITHUB_REPOSITORY}" \
--pattern "codex-package-*-unknown-linux-musl.tar.gz" \
--dir dist/python-runtime-packages
shopt -s nullglob
wheels=(dist/python-runtime/*.whl)
if [[ "${#wheels[@]}" -ne 6 ]]; then
echo "Expected 6 Python runtime wheels for ${PYTHON_RUNTIME_VERSION}, found ${#wheels[@]}."
exit 1
fi
packages=(dist/python-runtime-packages/*.tar.gz)
if [[ "${#packages[@]}" -ne 2 ]]; then
echo "Expected 2 Linux package archives for ${PYTHON_RUNTIME_VERSION}, found ${#packages[@]}."
exit 1
fi
- name: Build musllinux Python runtime wheels
env:
RELEASE_TAG: ${{ steps.python_runtime.outputs.release_tag }}
run: |
set -euo pipefail
python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv"
"${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build
while read -r target platform_tag; do
stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${target}-${platform_tag}"
python3 sdk/python/scripts/update_sdk_artifacts.py \
stage-runtime \
"$stage_dir" \
"dist/python-runtime-packages/codex-package-${target}.tar.gz" \
--codex-version "$RELEASE_TAG" \
--platform-tag "$platform_tag"
"${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build \
--wheel \
--outdir dist/python-runtime \
"$stage_dir"
done <<'EOF'
aarch64-unknown-linux-musl musllinux_1_1_aarch64
x86_64-unknown-linux-musl musllinux_1_1_x86_64
EOF
shopt -s nullglob
wheels=(dist/python-runtime/*.whl)
if [[ "${#wheels[@]}" -ne 8 ]]; then
echo "Expected 8 Python runtime wheels, found ${#wheels[@]}."
exit 1
fi
ls -lh dist/python-runtime
- name: Upload Python runtime wheels
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: python-runtime-wheels
path: dist/python-runtime/*
if-no-files-found: error
@@ -0,0 +1,100 @@
name: python-runtime-release
on:
workflow_dispatch:
inputs:
runtime_version:
description: "Runtime version to publish before updating the SDK pin, for example 0.136.0 or 0.136.0a2."
required: true
type: string
concurrency:
group: python-runtime-release-${{ inputs.runtime_version }}
cancel-in-progress: false
jobs:
prepare-python-runtime:
name: prepare-python-runtime
permissions:
contents: read
uses: ./.github/workflows/python-runtime-build.yml
with:
runtime_version: ${{ inputs.runtime_version }}
# PyPI must trust this top-level workflow for manual runtime publication.
publish-python-runtime:
if: github.repository == 'openai/codex'
name: publish-python-runtime
needs: prepare-python-runtime
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: read
id-token: write # Required for PyPI trusted publishing.
steps:
- name: Download Python runtime wheels
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: python-runtime-wheels
path: dist/python-runtime
- name: Publish Python runtime wheels to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
packages-dir: dist/python-runtime
skip-existing: true
- name: Verify Python runtime wheels are available on PyPI
env:
PYTHON_RUNTIME_VERSION: ${{ inputs.runtime_version }}
run: |
set -euo pipefail
for attempt in {1..30}; do
if python3 - <<'PY'
import json
import os
import urllib.error
import urllib.request
version = os.environ["PYTHON_RUNTIME_VERSION"]
tags = {
"macosx_10_9_x86_64",
"macosx_11_0_arm64",
"manylinux_2_17_aarch64",
"manylinux_2_17_x86_64",
"musllinux_1_1_aarch64",
"musllinux_1_1_x86_64",
"win_amd64",
"win_arm64",
}
expected = {
f"openai_codex_cli_bin-{version}-py3-none-{tag}.whl"
for tag in tags
}
try:
with urllib.request.urlopen(
f"https://pypi.org/pypi/openai-codex-cli-bin/{version}/json",
timeout=30,
) as response:
payload = json.load(response)
except urllib.error.URLError as error:
print(f"Could not read runtime {version} from PyPI: {error}.")
raise SystemExit(1) from error
actual = {file["filename"] for file in payload["urls"]}
if actual != expected:
print(f"Missing runtime wheels: {sorted(expected - actual)}")
print(f"Unexpected runtime files: {sorted(actual - expected)}")
raise SystemExit(1)
PY
then
exit 0
fi
echo "Runtime wheels are not available on PyPI yet; retrying (${attempt}/30)."
sleep 10
done
echo "Runtime wheels did not become available on PyPI."
exit 1
+115 -115
View File
@@ -4,29 +4,21 @@ on:
push:
tags:
- "python-v*"
workflow_dispatch:
inputs:
runtime_version:
description: "Runtime version to publish before updating the SDK pin, for example 0.136.0 or 0.136.0a2."
required: true
type: string
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
jobs:
# Publish the platform-specific Python runtime wheels before building the SDK
# package that pins them, or explicitly before updating the SDK runtime pin.
# PyPI project configuration must trust this workflow and job for publishing.
publish-python-runtime:
resolve-python-release:
if: github.repository == 'openai/codex'
name: publish-python-runtime
name: resolve-python-release
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: read
id-token: write # Required for PyPI trusted publishing.
outputs:
runtime_version: ${{ steps.python_release.outputs.runtime_version }}
sdk_version: ${{ steps.python_release.outputs.sdk_version }}
steps:
- name: Checkout repository
@@ -34,11 +26,9 @@ jobs:
with:
persist-credentials: false
- name: Validate SDK tag and resolve Python runtime release
id: python_runtime
- name: Validate SDK tag and resolve pinned runtime
id: python_release
shell: bash
env:
REQUESTED_RUNTIME_VERSION: ${{ inputs.runtime_version }}
run: |
set -euo pipefail
python3 - <<'PY'
@@ -47,107 +37,60 @@ jobs:
import tomllib
from pathlib import Path
event_name = os.environ["GITHUB_EVENT_NAME"]
if event_name == "workflow_dispatch":
python_version = os.environ["REQUESTED_RUNTIME_VERSION"]
elif event_name == "push":
sdk_version = os.environ["GITHUB_REF_NAME"].removeprefix("python-v")
if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+b[0-9]+", sdk_version):
raise SystemExit(
"Python SDK release tags must identify a beta release, "
"for example python-v0.1.0b1."
)
pyproject = tomllib.loads(Path("sdk/python/pyproject.toml").read_text())
prefix = "openai-codex-cli-bin=="
versions = [
dependency.removeprefix(prefix)
for dependency in pyproject["project"]["dependencies"]
if dependency.startswith(prefix)
]
if len(versions) != 1:
raise SystemExit(f"Expected exactly one pinned {prefix} dependency, found {versions}")
python_version = versions[0]
else:
raise SystemExit(f"Unsupported workflow event: {event_name}")
if match := re.fullmatch(r"([0-9]+\.[0-9]+\.[0-9]+)a([0-9]+)", python_version):
release_version = f"{match.group(1)}-alpha.{match.group(2)}"
elif re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", python_version):
release_version = python_version
else:
sdk_version = os.environ["GITHUB_REF_NAME"].removeprefix("python-v")
if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+b[0-9]+", sdk_version):
raise SystemExit(
"Python runtime version must be stable or a numbered alpha, "
f"for example 0.136.0 or 0.136.0a2; found {python_version}"
"Python SDK release tags must identify a beta release, "
"for example python-v0.1.0b1."
)
pyproject = tomllib.loads(Path("sdk/python/pyproject.toml").read_text())
prefix = "openai-codex-cli-bin=="
runtime_versions = [
dependency.removeprefix(prefix)
for dependency in pyproject["project"]["dependencies"]
if dependency.startswith(prefix)
]
if len(runtime_versions) != 1:
raise SystemExit(
f"Expected exactly one pinned {prefix} dependency, found {runtime_versions}"
)
with Path(os.environ["GITHUB_OUTPUT"]).open("a") as output:
print(f"python_version={python_version}", file=output)
print(f"release_tag=rust-v{release_version}", file=output)
print(f"runtime_version={runtime_versions[0]}", file=output)
print(f"sdk_version={sdk_version}", file=output)
PY
- name: Download Python runtime release artifacts
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PYTHON_RUNTIME_VERSION: ${{ steps.python_runtime.outputs.python_version }}
RELEASE_TAG: ${{ steps.python_runtime.outputs.release_tag }}
run: |
set -euo pipefail
mkdir -p dist/python-runtime dist/python-runtime-packages
gh release download "$RELEASE_TAG" \
--repo "${GITHUB_REPOSITORY}" \
--pattern "openai_codex_cli_bin-${PYTHON_RUNTIME_VERSION}-*.whl" \
--dir dist/python-runtime
gh release download "$RELEASE_TAG" \
--repo "${GITHUB_REPOSITORY}" \
--pattern "codex-package-*-unknown-linux-musl.tar.gz" \
--dir dist/python-runtime-packages
prepare-python-runtime:
name: prepare-python-runtime
needs: resolve-python-release
permissions:
contents: read
uses: ./.github/workflows/python-runtime-build.yml
with:
runtime_version: ${{ needs.resolve-python-release.outputs.runtime_version }}
shopt -s nullglob
wheels=(dist/python-runtime/*.whl)
if [[ "${#wheels[@]}" -ne 6 ]]; then
echo "Expected 6 Python runtime wheels for ${PYTHON_RUNTIME_VERSION}, found ${#wheels[@]}."
exit 1
fi
packages=(dist/python-runtime-packages/*.tar.gz)
if [[ "${#packages[@]}" -ne 2 ]]; then
echo "Expected 2 Linux package archives for ${PYTHON_RUNTIME_VERSION}, found ${#packages[@]}."
exit 1
fi
# Always publish the exact pinned runtime from this top-level workflow before
# building the SDK package. PyPI does not support reusable workflows as
# Trusted Publishers.
publish-python-runtime:
if: github.repository == 'openai/codex'
name: publish-python-runtime
needs:
- prepare-python-runtime
- resolve-python-release
runs-on: ubuntu-latest
environment: pypi
permissions:
contents: read
id-token: write # Required for PyPI trusted publishing.
- name: Build musllinux Python runtime wheels
env:
RELEASE_TAG: ${{ steps.python_runtime.outputs.release_tag }}
run: |
set -euo pipefail
python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv"
"${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build
while read -r target platform_tag; do
stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${target}-${platform_tag}"
python3 sdk/python/scripts/update_sdk_artifacts.py \
stage-runtime \
"$stage_dir" \
"dist/python-runtime-packages/codex-package-${target}.tar.gz" \
--codex-version "$RELEASE_TAG" \
--platform-tag "$platform_tag"
"${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build \
--wheel \
--outdir dist/python-runtime \
"$stage_dir"
done <<'EOF'
aarch64-unknown-linux-musl musllinux_1_1_aarch64
x86_64-unknown-linux-musl musllinux_1_1_x86_64
EOF
shopt -s nullglob
wheels=(dist/python-runtime/*.whl)
if [[ "${#wheels[@]}" -ne 8 ]]; then
echo "Expected 8 Python runtime wheels, found ${#wheels[@]}."
exit 1
fi
ls -lh dist/python-runtime
steps:
- name: Download Python runtime wheels
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: python-runtime-wheels
path: dist/python-runtime
- name: Publish Python runtime wheels to PyPI
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
@@ -155,10 +98,66 @@ jobs:
packages-dir: dist/python-runtime
skip-existing: true
- name: Verify Python runtime wheels are available on PyPI
env:
PYTHON_RUNTIME_VERSION: ${{ needs.resolve-python-release.outputs.runtime_version }}
run: |
set -euo pipefail
for attempt in {1..30}; do
if python3 - <<'PY'
import json
import os
import urllib.error
import urllib.request
version = os.environ["PYTHON_RUNTIME_VERSION"]
tags = {
"macosx_10_9_x86_64",
"macosx_11_0_arm64",
"manylinux_2_17_aarch64",
"manylinux_2_17_x86_64",
"musllinux_1_1_aarch64",
"musllinux_1_1_x86_64",
"win_amd64",
"win_arm64",
}
expected = {
f"openai_codex_cli_bin-{version}-py3-none-{tag}.whl"
for tag in tags
}
try:
with urllib.request.urlopen(
f"https://pypi.org/pypi/openai-codex-cli-bin/{version}/json",
timeout=30,
) as response:
payload = json.load(response)
except urllib.error.URLError as error:
print(f"Could not read runtime {version} from PyPI: {error}.")
raise SystemExit(1) from error
actual = {file["filename"] for file in payload["urls"]}
if actual != expected:
print(f"Missing runtime wheels: {sorted(expected - actual)}")
print(f"Unexpected runtime files: {sorted(actual - expected)}")
raise SystemExit(1)
PY
then
exit 0
fi
echo "Runtime wheels are not available on PyPI yet; retrying (${attempt}/30)."
sleep 10
done
echo "Runtime wheels did not become available on PyPI."
exit 1
build-python-sdk:
if: github.event_name == 'push' && github.repository == 'openai/codex'
if: github.repository == 'openai/codex'
name: build-python-sdk
needs: publish-python-runtime
needs:
- publish-python-runtime
- resolve-python-release
runs-on: ubuntu-latest
permissions:
contents: read
@@ -169,19 +168,20 @@ jobs:
with:
persist-credentials: false
- name: Validate tag and build Python SDK package
- name: Build Python SDK package
shell: bash
env:
SDK_VERSION: ${{ needs.resolve-python-release.outputs.sdk_version }}
run: |
set -euo pipefail
sdk_version="${GITHUB_REF_NAME#python-v}"
# Build in a glibc Linux image so release type generation installs
# the pinned manylinux runtime wheel.
docker run --rm \
--user "$(id -u):$(id -g)" \
-e HOME=/tmp/codex-python-sdk-home \
-e UV_LINK_MODE=copy \
-e SDK_VERSION="${sdk_version}" \
-e SDK_VERSION \
-e SDK_STAGE_DIR="${RUNNER_TEMP}/openai-codex" \
-e SDK_DIST_DIR="${GITHUB_WORKSPACE}/dist/python-sdk" \
-v "${GITHUB_WORKSPACE}:${GITHUB_WORKSPACE}" \
+35 -34
View File
@@ -98,6 +98,9 @@ jobs:
name: Argument comment lint - ${{ matrix.name }}
runs-on: ${{ matrix.runs_on || matrix.runner }}
timeout-minutes: 30
environment:
name: bazel
deployment: false
strategy:
fail-fast: false
matrix:
@@ -109,8 +112,8 @@ jobs:
- name: Windows
runner: windows-x64
runs_on:
group: codex-runners
labels: codex-windows-x64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-x64
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
@@ -167,8 +170,6 @@ jobs:
USE_SCCACHE: ${{ (startsWith(matrix.runner, 'windows') || (matrix.runner == 'macos-15-xlarge' && matrix.target == 'x86_64-apple-darwin')) && 'false' || 'true' }}
CARGO_INCREMENTAL: "0"
SCCACHE_CACHE_SIZE: 10G
# In rust-ci, representative release-profile checks use thin LTO for faster feedback.
CARGO_PROFILE_RELEASE_LTO: ${{ matrix.profile == 'release' && 'thin' || 'fat' }}
strategy:
fail-fast: false
@@ -184,38 +185,38 @@ jobs:
target: x86_64-unknown-linux-musl
profile: dev
runs_on:
group: codex-runners
labels: codex-linux-x64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-linux-x64
- runner: ubuntu-24.04
target: x86_64-unknown-linux-gnu
profile: dev
runs_on:
group: codex-runners
labels: codex-linux-x64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-linux-x64
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
profile: dev
runs_on:
group: codex-runners
labels: codex-linux-arm64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-linux-arm64
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
profile: dev
runs_on:
group: codex-runners
labels: codex-linux-arm64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-linux-arm64
- runner: windows-x64
target: x86_64-pc-windows-msvc
profile: dev
runs_on:
group: codex-runners
labels: codex-windows-x64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-x64
- runner: windows-arm64
target: aarch64-pc-windows-msvc
profile: dev
runs_on:
group: codex-runners
labels: codex-windows-arm64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-arm64
# Also run representative release builds on Mac and Linux because
# there could be release-only build errors we want to catch.
@@ -228,26 +229,26 @@ jobs:
target: x86_64-unknown-linux-musl
profile: release
runs_on:
group: codex-runners
labels: codex-linux-x64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-linux-x64
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
profile: release
runs_on:
group: codex-runners
labels: codex-linux-arm64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-linux-arm64
- runner: windows-x64
target: x86_64-pc-windows-msvc
profile: release
runs_on:
group: codex-runners
labels: codex-windows-x64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-x64
- runner: windows-arm64
target: aarch64-pc-windows-msvc
profile: release
runs_on:
group: codex-runners
labels: codex-windows-arm64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-arm64
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -478,8 +479,8 @@ jobs:
uses: ./.github/workflows/rust-ci-full-nextest-platform.yml
with:
runner: ubuntu-24.04
runner_group: codex-runners
runner_labels: codex-linux-x64
runner_group: ${{ github.event.repository.name }}-runners
runner_labels: ${{ github.event.repository.name }}-linux-x64
target: x86_64-unknown-linux-gnu
profile: ci-test
artifact_id: linux-x64-remote
@@ -492,8 +493,8 @@ jobs:
uses: ./.github/workflows/rust-ci-full-nextest-platform.yml
with:
runner: ubuntu-24.04-arm
runner_group: codex-runners
runner_labels: codex-linux-arm64
runner_group: ${{ github.event.repository.name }}-runners
runner_labels: ${{ github.event.repository.name }}-linux-arm64
target: aarch64-unknown-linux-gnu
profile: ci-test
artifact_id: linux-arm64
@@ -505,8 +506,8 @@ jobs:
uses: ./.github/workflows/rust-ci-full-nextest-platform.yml
with:
runner: windows-x64
runner_group: codex-runners
runner_labels: codex-windows-x64
runner_group: ${{ github.event.repository.name }}-runners
runner_labels: ${{ github.event.repository.name }}-windows-x64
target: x86_64-pc-windows-msvc
profile: ci-test
artifact_id: windows-x64
@@ -518,11 +519,11 @@ jobs:
uses: ./.github/workflows/rust-ci-full-nextest-platform.yml
with:
runner: windows-arm64
runner_group: codex-runners
runner_labels: codex-windows-arm64
runner_group: ${{ github.event.repository.name }}-runners
runner_labels: ${{ github.event.repository.name }}-windows-arm64
archive_runner: windows-x64
archive_runner_group: codex-runners
archive_runner_labels: codex-windows-x64
archive_runner_group: ${{ github.event.repository.name }}-runners
archive_runner_labels: ${{ github.event.repository.name }}-windows-x64
target: aarch64-pc-windows-msvc
profile: ci-test
artifact_id: windows-arm64
+5 -2
View File
@@ -159,6 +159,9 @@ jobs:
runs-on: ${{ matrix.runs_on || matrix.runner }}
timeout-minutes: ${{ matrix.timeout_minutes }}
needs: changed
environment:
name: bazel
deployment: false
strategy:
fail-fast: false
matrix:
@@ -173,8 +176,8 @@ jobs:
runner: windows-x64
timeout_minutes: 30
runs_on:
group: codex-runners
labels: codex-windows-x64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-x64
steps:
- name: Check whether argument comment lint should run
id: argument_comment_lint_gate
@@ -57,8 +57,8 @@ jobs:
runner_binary: argument-comment-lint.exe
cargo_dylint_binary: cargo-dylint.exe
runs_on:
group: codex-runners
labels: codex-windows-x64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-x64
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+58 -46
View File
@@ -2,23 +2,6 @@ name: rust-release-windows
on:
workflow_call:
inputs:
release-lto:
required: true
type: string
secrets:
AZURE_TRUSTED_SIGNING_CLIENT_ID:
required: true
AZURE_TRUSTED_SIGNING_TENANT_ID:
required: true
AZURE_TRUSTED_SIGNING_SUBSCRIPTION_ID:
required: true
AZURE_TRUSTED_SIGNING_ENDPOINT:
required: true
AZURE_TRUSTED_SIGNING_ACCOUNT_NAME:
required: true
AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME:
required: true
# Cargo's libgit2 transport has been flaky when fetching git dependencies with
# nested submodules. Prefer the system git CLI across every Cargo invocation.
@@ -29,17 +12,14 @@ jobs:
build-windows-binaries:
name: Build Windows binaries - ${{ matrix.runner }} - ${{ matrix.target }} - ${{ matrix.bundle }}
runs-on: ${{ matrix.runs_on }}
# Windows release builds can exceed an hour on fat-LTO mainline releases,
# so keep the timeout aligned with the top-level release build headroom.
# Windows release builds can exceed an hour, so keep the timeout aligned
# with the top-level release build headroom.
timeout-minutes: 90
permissions:
contents: read
defaults:
run:
working-directory: codex-rs
env:
CARGO_PROFILE_RELEASE_LTO: ${{ inputs.release-lto }}
strategy:
fail-fast: false
matrix:
@@ -49,43 +29,43 @@ jobs:
bundle: primary
binaries: "codex codex-responses-api-proxy"
runs_on:
group: codex-runners
labels: codex-windows-x64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-x64
- runner: windows-arm64
target: aarch64-pc-windows-msvc
bundle: primary
binaries: "codex codex-responses-api-proxy"
runs_on:
group: codex-runners
labels: codex-windows-arm64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-arm64
- runner: windows-x64
target: x86_64-pc-windows-msvc
bundle: helpers
binaries: "codex-windows-sandbox-setup codex-command-runner"
runs_on:
group: codex-runners
labels: codex-windows-x64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-x64
- runner: windows-arm64
target: aarch64-pc-windows-msvc
bundle: helpers
binaries: "codex-windows-sandbox-setup codex-command-runner"
runs_on:
group: codex-runners
labels: codex-windows-arm64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-arm64
- runner: windows-x64
target: x86_64-pc-windows-msvc
bundle: app-server
binaries: "codex-app-server"
runs_on:
group: codex-runners
labels: codex-windows-x64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-x64
- runner: windows-arm64
target: aarch64-pc-windows-msvc
bundle: app-server
binaries: "codex-app-server"
runs_on:
group: codex-runners
labels: codex-windows-arm64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-arm64
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -132,10 +112,22 @@ jobs:
- name: Stage Windows binaries
shell: bash
run: |
output_dir="target/${{ matrix.target }}/release/staged-${{ matrix.bundle }}"
release_dir="target/${{ matrix.target }}/release"
output_dir="$release_dir/staged-${{ matrix.bundle }}"
mkdir -p "$output_dir"
for binary in ${{ matrix.binaries }}; do
cp "target/${{ matrix.target }}/release/${binary}.exe" "$output_dir/${binary}.exe"
pdb_name="${binary//-/_}"
pdb_path="$release_dir/${pdb_name}.pdb"
if [[ ! -f "$pdb_path" ]]; then
pdb_path="$release_dir/${binary}.pdb"
fi
if [[ ! -f "$pdb_path" ]]; then
echo "PDB for $binary not found at $release_dir/${pdb_name}.pdb or $release_dir/${binary}.pdb" >&2
exit 1
fi
cp "$release_dir/${binary}.exe" "$output_dir/${binary}.exe"
cp "$pdb_path" "$output_dir/${binary}.pdb"
done
- name: Upload Windows binaries
@@ -150,6 +142,9 @@ jobs:
- build-windows-binaries
name: Build - ${{ matrix.runner }} - ${{ matrix.target }}
runs-on: ${{ matrix.runs_on }}
environment:
name: azure-artifact-signing
deployment: false
timeout-minutes: 90
permissions:
contents: read
@@ -167,13 +162,13 @@ jobs:
- runner: windows-x64
target: x86_64-pc-windows-msvc
runs_on:
group: codex-runners
labels: codex-windows-x64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-x64
- runner: windows-arm64
target: aarch64-pc-windows-msvc
runs_on:
group: codex-runners
labels: codex-windows-arm64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-windows-arm64
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -211,12 +206,29 @@ jobs:
with:
target: ${{ matrix.target }}
binaries: ${{ env.WINDOWS_BINARIES }}
client-id: ${{ secrets.AZURE_TRUSTED_SIGNING_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TRUSTED_SIGNING_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_TRUSTED_SIGNING_SUBSCRIPTION_ID }}
endpoint: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }}
client-id: ${{ secrets.AZURE_ARTIFACT_SIGNING_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_ARTIFACT_SIGNING_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_ARTIFACT_SIGNING_SUBSCRIPTION_ID }}
endpoint: ${{ secrets.AZURE_ARTIFACT_SIGNING_ENDPOINT }}
account-name: ${{ secrets.AZURE_ARTIFACT_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ secrets.AZURE_ARTIFACT_SIGNING_CERTIFICATE_PROFILE_NAME }}
- name: Build symbols archive
shell: bash
run: |
bash "${GITHUB_WORKSPACE}/.github/scripts/archive-release-symbols-and-strip-binaries.sh" \
--target "${{ matrix.target }}" \
--artifact-name "${{ matrix.target }}" \
--release-dir "target/${{ matrix.target }}/release" \
--archive-dir "symbols-dist/${{ matrix.target }}" \
--binaries "${WINDOWS_BINARIES}"
- name: Upload symbols archive
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: ${{ matrix.target }}-symbols
path: codex-rs/symbols-dist/${{ matrix.target }}/*
if-no-files-found: error
- name: Stage artifacts
shell: bash
File diff suppressed because it is too large Load Diff
+159
View File
@@ -64,6 +64,9 @@ jobs:
permissions:
contents: read
actions: read
environment:
name: bazel
deployment: false
strategy:
fail-fast: false
matrix:
@@ -73,72 +76,84 @@ jobs:
platform: linux_amd64
sandbox: false
target: x86_64-unknown-linux-gnu
v8_cpu: x64
variant: release
- runner: ubuntu-24.04
bazel_config: ci-v8
platform: linux_amd64
sandbox: true
target: x86_64-unknown-linux-gnu
v8_cpu: x64
variant: ptrcomp-sandbox
- runner: ubuntu-24.04-arm
bazel_config: ci-v8
platform: linux_arm64
sandbox: false
target: aarch64-unknown-linux-gnu
v8_cpu: arm64
variant: release
- runner: ubuntu-24.04-arm
bazel_config: ci-v8
platform: linux_arm64
sandbox: true
target: aarch64-unknown-linux-gnu
v8_cpu: arm64
variant: ptrcomp-sandbox
- runner: macos-15-xlarge
bazel_config: ci-macos
platform: macos_amd64
sandbox: false
target: x86_64-apple-darwin
v8_cpu: x64
variant: release
- runner: macos-15-xlarge
bazel_config: ci-macos
platform: macos_amd64
sandbox: true
target: x86_64-apple-darwin
v8_cpu: x64
variant: ptrcomp-sandbox
- runner: macos-15-xlarge
bazel_config: ci-macos
platform: macos_arm64
sandbox: false
target: aarch64-apple-darwin
v8_cpu: arm64
variant: release
- runner: macos-15-xlarge
bazel_config: ci-macos
platform: macos_arm64
sandbox: true
target: aarch64-apple-darwin
v8_cpu: arm64
variant: ptrcomp-sandbox
- runner: ubuntu-24.04
bazel_config: ci-v8
platform: linux_amd64_musl
sandbox: false
target: x86_64-unknown-linux-musl
v8_cpu: x64
variant: release
- runner: ubuntu-24.04-arm
bazel_config: ci-v8
platform: linux_arm64_musl
sandbox: false
target: aarch64-unknown-linux-musl
v8_cpu: arm64
variant: release
- runner: ubuntu-24.04
bazel_config: ci-v8
platform: linux_amd64_musl
sandbox: true
target: x86_64-unknown-linux-musl
v8_cpu: x64
variant: ptrcomp-sandbox
- runner: ubuntu-24.04-arm
bazel_config: ci-v8
platform: linux_arm64_musl
sandbox: true
target: aarch64-unknown-linux-musl
v8_cpu: arm64
variant: ptrcomp-sandbox
steps:
@@ -167,6 +182,7 @@ jobs:
PLATFORM: ${{ matrix.platform }}
SANDBOX: ${{ matrix.sandbox }}
TARGET: ${{ matrix.target }}
V8_CPU: ${{ matrix.v8_cpu }}
shell: bash
run: |
set -euo pipefail
@@ -184,6 +200,7 @@ jobs:
opt
"--platforms=@llvm//platforms:${PLATFORM}"
--config=rusty-v8-upstream-libcxx
"--config=v8-target-${V8_CPU}"
"${pair_target}"
--build_metadata=COMMIT_SHA=$(git rev-parse HEAD)
)
@@ -203,6 +220,7 @@ jobs:
PLATFORM: ${{ matrix.platform }}
SANDBOX: ${{ matrix.sandbox }}
TARGET: ${{ matrix.target }}
V8_CPU: ${{ matrix.v8_cpu }}
shell: bash
run: |
set -euo pipefail
@@ -213,6 +231,7 @@ jobs:
--compilation-mode opt
--output-dir "dist/${TARGET}"
--bazel-config "${BAZEL_CONFIG}"
--bazel-config "v8-target-${V8_CPU}"
)
if [[ "${SANDBOX}" == "true" ]]; then
stage_args+=(--sandbox)
@@ -266,10 +285,150 @@ jobs:
name: rusty-v8-${{ needs.metadata.outputs.v8_version }}-${{ matrix.variant }}-${{ matrix.target }}
path: dist/${{ matrix.target }}/*
build-windows-source:
name: Build ptrcomp-sandbox ${{ matrix.target }} from source
needs: metadata
runs-on: ${{ matrix.runner }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- runner: windows-2022
target: x86_64-pc-windows-msvc
- runner: windows-2022
target: aarch64-pc-windows-msvc
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Configure git for upstream checkout
shell: bash
run: git config --global core.symlinks true
- name: Check out upstream rusty_v8
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
repository: denoland/rusty_v8
ref: v${{ needs.metadata.outputs.v8_version }}
path: upstream-rusty-v8
submodules: recursive
- name: Set up Python
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.11"
architecture: x64
- name: Set up Codex Rust toolchain for Cargo smoke
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
with:
toolchain: "1.95.0"
targets: ${{ matrix.target }}
- name: Install rusty_v8 Rust toolchain
env:
TARGET: ${{ matrix.target }}
shell: bash
run: |
set -euo pipefail
rustup toolchain install 1.91.0 --profile minimal --no-self-update
rustup target add --toolchain 1.91.0 "${TARGET}"
- name: Write upstream submodule status
shell: bash
working-directory: upstream-rusty-v8
run: git submodule status --recursive > git_submodule_status.txt
- name: Restore upstream source-build cache
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
upstream-rusty-v8/target/sccache
upstream-rusty-v8/target/${{ matrix.target }}/release/gn_out
key: rusty-v8-source-${{ matrix.target }}-sandbox-${{ hashFiles('upstream-rusty-v8/Cargo.lock', 'upstream-rusty-v8/build.rs', 'upstream-rusty-v8/git_submodule_status.txt') }}
restore-keys: |
rusty-v8-source-${{ matrix.target }}-sandbox-
- name: Install and start sccache
shell: pwsh
env:
SCCACHE_CACHE_SIZE: 256M
SCCACHE_DIR: ${{ github.workspace }}/upstream-rusty-v8/target/sccache
SCCACHE_IDLE_TIMEOUT: 0
run: |
$version = "v0.8.2"
$platform = "x86_64-pc-windows-msvc"
$basename = "sccache-$version-$platform"
$url = "https://github.com/mozilla/sccache/releases/download/$version/$basename.tar.gz"
cd ~
curl -LO $url
tar -xzvf "$basename.tar.gz"
. $basename/sccache --start-server
echo "$(pwd)/$basename" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Install Chromium clang for ARM64 MSVC cross build
if: matrix.target == 'aarch64-pc-windows-msvc'
shell: bash
working-directory: upstream-rusty-v8
run: python3 tools/clang/scripts/update.py
- name: Build upstream rusty_v8 sandbox release pair
env:
SCCACHE_IDLE_TIMEOUT: 0
TARGET: ${{ matrix.target }}
V8_FROM_SOURCE: "1"
shell: bash
working-directory: upstream-rusty-v8
run: cargo +1.91.0 build --locked --release --target "${TARGET}" --features v8_enable_sandbox
- name: Stage upstream sandbox release pair
env:
TARGET: ${{ matrix.target }}
shell: bash
run: |
set -euo pipefail
python3 .github/scripts/rusty_v8_bazel.py stage-upstream-release-pair \
--source-root upstream-rusty-v8 \
--target "${TARGET}" \
--output-dir "dist/${TARGET}" \
--sandbox
- name: Smoke link staged artifact with Cargo
env:
TARGET: ${{ matrix.target }}
shell: bash
run: |
set -euo pipefail
archive="$(find "dist/${TARGET}" -maxdepth 1 -type f -name 'rusty_v8_*.lib.gz' -print -quit)"
binding="$(find "dist/${TARGET}" -maxdepth 1 -type f -name 'src_binding_*.rs' -print -quit)"
if [[ -z "${archive}" || -z "${binding}" ]]; then
echo "Missing staged archive or binding for ${TARGET}." >&2
exit 1
fi
(
cd codex-rs
RUSTY_V8_ARCHIVE="${GITHUB_WORKSPACE}/${archive}" \
RUSTY_V8_SRC_BINDING_PATH="${GITHUB_WORKSPACE}/${binding}" \
cargo +1.95.0 test -p codex-v8-poc --target "${TARGET}" --features sandbox --no-run
)
- name: Upload staged artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: rusty-v8-${{ needs.metadata.outputs.v8_version }}-ptrcomp-sandbox-${{ matrix.target }}
path: dist/${{ matrix.target }}/*
publish-release:
needs:
- metadata
- build
- build-windows-source
runs-on: ubuntu-latest
permissions:
contents: write
+7 -4
View File
@@ -8,8 +8,8 @@ on:
jobs:
python-sdk:
runs-on:
group: codex-runners
labels: codex-linux-x64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-linux-x64
timeout-minutes: 10
steps:
- name: Checkout repository
@@ -43,9 +43,12 @@ jobs:
sdks:
runs-on:
group: codex-runners
labels: codex-linux-x64
group: ${{ github.event.repository.name }}-runners
labels: ${{ github.event.repository.name }}-linux-x64
timeout-minutes: 10
environment:
name: bazel
deployment: false
steps:
- name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+19
View File
@@ -80,6 +80,9 @@ jobs:
permissions:
contents: read
actions: read
environment:
name: bazel
deployment: false
strategy:
fail-fast: false
matrix:
@@ -89,72 +92,84 @@ jobs:
platform: linux_amd64
sandbox: false
target: x86_64-unknown-linux-gnu
v8_cpu: x64
variant: release
- runner: ubuntu-24.04
bazel_config: ci-v8
platform: linux_amd64
sandbox: true
target: x86_64-unknown-linux-gnu
v8_cpu: x64
variant: ptrcomp-sandbox
- runner: ubuntu-24.04-arm
bazel_config: ci-v8
platform: linux_arm64
sandbox: false
target: aarch64-unknown-linux-gnu
v8_cpu: arm64
variant: release
- runner: ubuntu-24.04-arm
bazel_config: ci-v8
platform: linux_arm64
sandbox: true
target: aarch64-unknown-linux-gnu
v8_cpu: arm64
variant: ptrcomp-sandbox
- runner: macos-15-xlarge
bazel_config: ci-macos
platform: macos_amd64
sandbox: false
target: x86_64-apple-darwin
v8_cpu: x64
variant: release
- runner: macos-15-xlarge
bazel_config: ci-macos
platform: macos_amd64
sandbox: true
target: x86_64-apple-darwin
v8_cpu: x64
variant: ptrcomp-sandbox
- runner: macos-15-xlarge
bazel_config: ci-macos
platform: macos_arm64
sandbox: false
target: aarch64-apple-darwin
v8_cpu: arm64
variant: release
- runner: macos-15-xlarge
bazel_config: ci-macos
platform: macos_arm64
sandbox: true
target: aarch64-apple-darwin
v8_cpu: arm64
variant: ptrcomp-sandbox
- runner: ubuntu-24.04
bazel_config: ci-v8
platform: linux_amd64_musl
sandbox: false
target: x86_64-unknown-linux-musl
v8_cpu: x64
variant: release
- runner: ubuntu-24.04
bazel_config: ci-v8
platform: linux_amd64_musl
sandbox: true
target: x86_64-unknown-linux-musl
v8_cpu: x64
variant: ptrcomp-sandbox
- runner: ubuntu-24.04-arm
bazel_config: ci-v8
platform: linux_arm64_musl
sandbox: false
target: aarch64-unknown-linux-musl
v8_cpu: arm64
variant: release
- runner: ubuntu-24.04-arm
bazel_config: ci-v8
platform: linux_arm64_musl
sandbox: true
target: aarch64-unknown-linux-musl
v8_cpu: arm64
variant: ptrcomp-sandbox
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -183,6 +198,7 @@ jobs:
PLATFORM: ${{ matrix.platform }}
SANDBOX: ${{ matrix.sandbox }}
TARGET: ${{ matrix.target }}
V8_CPU: ${{ matrix.v8_cpu }}
shell: bash
run: |
set -euo pipefail
@@ -198,6 +214,7 @@ jobs:
build
"--platforms=@llvm//platforms:${PLATFORM}"
--config=rusty-v8-upstream-libcxx
"--config=v8-target-${V8_CPU}"
"${pair_target}"
--build_metadata=COMMIT_SHA=$(git rev-parse HEAD)
)
@@ -217,6 +234,7 @@ jobs:
PLATFORM: ${{ matrix.platform }}
SANDBOX: ${{ matrix.sandbox }}
TARGET: ${{ matrix.target }}
V8_CPU: ${{ matrix.v8_cpu }}
shell: bash
run: |
set -euo pipefail
@@ -226,6 +244,7 @@ jobs:
--target "${TARGET}"
--output-dir "dist/${TARGET}"
--bazel-config "${BAZEL_CONFIG}"
--bazel-config "v8-target-${V8_CPU}"
)
if [[ "${SANDBOX}" == "true" ]]; then
stage_args+=(--sandbox)
+8
View File
@@ -26,6 +26,8 @@ In the codex-rs folder where the rust code lives:
- Implementations may still use `async fn foo(&self, ...) -> T` when they satisfy that contract.
- Do not use `#[allow(async_fn_in_trait)]` as a shortcut around spelling the future contract explicitly.
- When writing tests, prefer comparing the equality of entire objects over fields one by one.
- Do not add tests for values that are statically defined.
- Do not add negative tests for logic that was removed.
- Do not add general product or user-facing documentation to the `docs/` folder. The official Codex documentation lives elsewhere. The exception is app-server API documentation, which is covered by the app-server guidance below.
- Prefer private modules and explicitly exported public crate API.
- If you change `ConfigToml` or nested config types, run `just write-config-schema` to update `codex-rs/core/config.schema.json`.
@@ -190,6 +192,12 @@ If you don’t have the tool:
- `cargo install --locked cargo-insta`
### Benchmarks
cargo benchmarks can be run with `just bench`, use the divan crate to write new ones.
Use `just bench-smoke` to dry-run the benchmark for a single iteration to ensure it works.
### Test assertions
- Tests should use pretty_assertions::assert_eq for clearer diffs. Import this at the top of the test module if it isn't already.
+16 -16
View File
@@ -415,18 +415,18 @@ crate.annotation(
inject_repo(crate, "alsa_lib")
bazel_dep(name = "v8", version = "14.7.173.20")
bazel_dep(name = "v8", version = "14.9.207.2")
archive_override(
module_name = "v8",
integrity = "sha256-v/x6I4X38a2wckzUIft3Dh0SUdkuOTokwxyF7lzW8Lc=",
integrity = "sha256-tflbZE5srqal6leMxJjK/ZQtwpF96OMGJ6avd5lice4=",
patch_strip = 3,
patches = [
"//patches:v8_module_deps.patch",
"//patches:v8_bazel_rules.patch",
"//patches:v8_source_portability.patch",
],
strip_prefix = "v8-14.7.173.20",
urls = ["https://github.com/v8/v8/archive/refs/tags/14.7.173.20.tar.gz"],
strip_prefix = "v8-14.9.207.2",
urls = ["https://github.com/v8/v8/archive/refs/tags/14.9.207.2.tar.gz"],
)
http_archive(
@@ -439,12 +439,12 @@ http_archive(
)
http_archive(
name = "v8_crate_147_4_0",
name = "v8_crate_149_2_0",
build_file = "//third_party/v8:v8_crate.BUILD.bazel",
sha256 = "2df8fffd507fb18ed000673a83d937f58e60fb07f3306b2274284125b15137cd",
strip_prefix = "v8-147.4.0",
sha256 = "46dccf61a364b61bbaac70a8ba64a1a1006e87123b7d62eaeec999a3ba31ecdb",
strip_prefix = "v8-149.2.0",
type = "tar.gz",
urls = ["https://static.crates.io/crates/v8/v8-147.4.0.crate"],
urls = ["https://static.crates.io/crates/v8/v8-149.2.0.crate"],
)
git_repository = use_repo_rule("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
@@ -452,7 +452,7 @@ git_repository = use_repo_rule("@bazel_tools//tools/build_defs/repo:git.bzl", "g
git_repository(
name = "rusty_v8_libcxx",
build_file = "//third_party/v8:libcxx.BUILD.bazel",
commit = "7ab65651aed6802d2599dcb7a73b1f82d5179d05",
commit = "99457fa555797f8c5ac3c076ca288d8481d3b23a",
remote = "https://chromium.googlesource.com/external/github.com/llvm/llvm-project/libcxx.git",
)
@@ -466,25 +466,25 @@ git_repository(
git_repository(
name = "rusty_v8_llvm_libc",
build_file = "//third_party/v8:llvm_libc.BUILD.bazel",
commit = "b3aa5bb702ff9e890179fd1e7d3ba346e17ecf8e",
commit = "cb952785ccee13811f293f3c419958d1e3ddafbf",
remote = "https://chromium.googlesource.com/external/github.com/llvm/llvm-project/libc.git",
)
http_file(
name = "rusty_v8_147_4_0_aarch64_pc_windows_msvc_archive",
name = "rusty_v8_149_2_0_aarch64_pc_windows_msvc_archive",
downloaded_file_path = "rusty_v8_release_aarch64-pc-windows-msvc.lib.gz",
sha256 = "1fa3f94d9e09cff1f6bcce94c478e5cb072c0755f6a0357abadb9dd3b48d8127",
sha256 = "923f2b6ccdc14526b814e171e34c9aafd7969f12304948857c6696d022f0fb3c",
urls = [
"https://github.com/denoland/rusty_v8/releases/download/v147.4.0/rusty_v8_release_aarch64-pc-windows-msvc.lib.gz",
"https://github.com/denoland/rusty_v8/releases/download/v149.2.0/rusty_v8_release_aarch64-pc-windows-msvc.lib.gz",
],
)
http_file(
name = "rusty_v8_147_4_0_x86_64_pc_windows_msvc_archive",
name = "rusty_v8_149_2_0_x86_64_pc_windows_msvc_archive",
downloaded_file_path = "rusty_v8_release_x86_64-pc-windows-msvc.lib.gz",
sha256 = "e2827ff98b1a9d4c0343000fc5124ac30dfab3007bc0129c168c9355fc2fcd7c",
sha256 = "12b5a791b54e92f748738ad8d0d12dad8d281a2d836638ad1aa6678e3b855d9a",
urls = [
"https://github.com/denoland/rusty_v8/releases/download/v147.4.0/rusty_v8_release_x86_64-pc-windows-msvc.lib.gz",
"https://github.com/denoland/rusty_v8/releases/download/v149.2.0/rusty_v8_release_x86_64-pc-windows-msvc.lib.gz",
],
)
+51 -37
View File
File diff suppressed because one or more lines are too long
+21 -55
View File
@@ -75,45 +75,25 @@ if (!platformPackage) {
throw new Error(`Unsupported target triple: ${targetTriple}`);
}
const codexBinaryName = process.platform === "win32" ? "codex.exe" : "codex";
const localVendorRoot = path.join(__dirname, "..", "vendor");
const packageBinaryPath = (vendorRoot) =>
path.join(vendorRoot, targetTriple, "bin", codexBinaryName);
const legacyBinaryPath = (vendorRoot) =>
path.join(vendorRoot, targetTriple, "codex", codexBinaryName);
function resolveNativePackage(vendorRoot) {
const packageRoot = path.join(vendorRoot, targetTriple);
const binaryPath = packageBinaryPath(vendorRoot);
if (existsSync(binaryPath)) {
return {
binaryPath,
pathDir: path.join(packageRoot, "codex-path"),
};
function findCodexExecutable() {
let vendorRoot;
try {
const packageJsonPath = require.resolve(`${platformPackage}/package.json`);
vendorRoot = path.join(path.dirname(packageJsonPath), "vendor");
} catch {
vendorRoot = path.join(__dirname, "..", "vendor");
}
const legacyPath = legacyBinaryPath(vendorRoot);
if (existsSync(legacyPath)) {
return {
binaryPath: legacyPath,
pathDir: path.join(packageRoot, "path"),
};
}
return null;
}
let nativePackage;
try {
const packageJsonPath = require.resolve(`${platformPackage}/package.json`);
nativePackage = resolveNativePackage(
path.join(path.dirname(packageJsonPath), "vendor"),
const codexExecutable = path.join(
vendorRoot,
targetTriple,
"bin",
process.platform === "win32" ? "codex.exe" : "codex",
);
} catch {
nativePackage = resolveNativePackage(localVendorRoot);
}
if (existsSync(codexExecutable)) {
return codexExecutable;
}
if (!nativePackage) {
const packageManager = detectPackageManager();
const updateCommand =
packageManager === "bun"
@@ -124,7 +104,7 @@ if (!nativePackage) {
);
}
const { binaryPath, pathDir } = nativePackage;
const binaryPath = findCodexExecutable();
// Use an asynchronous spawn instead of spawnSync so that Node is able to
// respond to signals (e.g. Ctrl-C / SIGINT) while the native binary is
@@ -132,16 +112,6 @@ const { binaryPath, pathDir } = nativePackage;
// and guarantees that when either the child terminates or the parent
// receives a fatal signal, both processes exit in a predictable manner.
function getUpdatedPath(newDirs) {
const pathSep = process.platform === "win32" ? ";" : ":";
const existingPath = process.env.PATH || "";
const updatedPath = [
...newDirs,
...existingPath.split(pathSep).filter(Boolean),
].join(pathSep);
return updatedPath;
}
/**
* Use heuristics to detect the package manager that was used to install Codex
* in order to give the user a hint about how to update it.
@@ -167,19 +137,15 @@ function detectPackageManager() {
return userAgent ? "npm" : null;
}
const additionalDirs = [];
if (existsSync(pathDir)) {
additionalDirs.push(pathDir);
}
const updatedPath = getUpdatedPath(additionalDirs);
const env = { ...process.env, PATH: updatedPath };
const packageManagerEnvVar =
detectPackageManager() === "bun"
? "CODEX_MANAGED_BY_BUN"
: "CODEX_MANAGED_BY_NPM";
env[packageManagerEnvVar] = "1";
env.CODEX_MANAGED_PACKAGE_ROOT = realpathSync(path.join(__dirname, ".."));
const env = {
...process.env,
[packageManagerEnvVar]: "1",
CODEX_MANAGED_PACKAGE_ROOT: realpathSync(path.join(__dirname, "..")),
};
const child = spawn(binaryPath, process.argv.slice(2), {
stdio: "inherit",
+5 -3
View File
@@ -1,11 +1,13 @@
[advisories]
# Reviewed 2026-04-15. Keep this list in sync with ../deny.toml.
# Reviewed 2026-06-07. Keep this list in sync with ../deny.toml.
ignore = [
"RUSTSEC-2024-0388", # derivative 2.2.0 via starlark; upstream crate is unmaintained
"RUSTSEC-2025-0057", # fxhash 0.2.1 via starlark_map; upstream crate is unmaintained
"RUSTSEC-2024-0388", # derivative 2.2.0 via starlark/starlark_syntax; upstream crate is unmaintained
"RUSTSEC-2025-0057", # fxhash 0.2.1 via starlark_map/bm25; upstream crate is unmaintained
"RUSTSEC-2024-0436", # paste 1.0.15 via starlark/ratatui; upstream crate is unmaintained
"RUSTSEC-2023-0089", # atomic-polyfill via postcard/heapless/pagable; upstream crate is unmaintained
"RUSTSEC-2024-0320", # yaml-rust via syntect; remove when syntect drops or updates it
"RUSTSEC-2025-0141", # bincode via syntect; remove when syntect drops or updates it
"RUSTSEC-2026-0118", # hickory-proto via rama-dns/rama-tcp; remove when rama updates to hickory 0.26.1 or hickory-net
"RUSTSEC-2026-0119", # hickory-proto via rama-dns/rama-tcp; remove when rama updates to hickory 0.26.1 or hickory-net
"RUSTSEC-2026-0173", # proc-macro-error2 via i18n-embed-fl/age/codex-secrets; remove when local secrets storage migrates off age or age drops i18n-embed-fl
]
+557 -356
View File
File diff suppressed because it is too large Load Diff
+9 -7
View File
@@ -83,6 +83,7 @@ members = [
"tools",
"v8-poc",
"utils/absolute-path",
"utils/path-uri",
"utils/cargo-bin",
"git-utils",
"utils/cache",
@@ -318,6 +319,7 @@ libc = "0.2.182"
log = "0.4"
lru = "0.16.3"
maplit = "1.0.2"
memchr = "2.7.6"
mime_guess = "2.0.5"
multimap = "0.10.0"
notify = "8.2.0"
@@ -383,7 +385,7 @@ sqlx = { version = "0.9.0", default-features = false, features = [
"time",
"uuid",
] }
starlark = "0.13.0"
starlark = { version = "0.14.2", default-features = false }
strum = "0.27.2"
strum_macros = "0.28.0"
supports-color = "3.0.2"
@@ -423,7 +425,7 @@ unicode-width = "0.2"
url = "2"
urlencoding = "2.1"
uuid = "1"
v8 = "=147.4.0"
v8 = "=149.2.0"
vt100 = "0.16.2"
walkdir = "2.5.0"
webbrowser = "1.0"
@@ -482,7 +484,6 @@ unwrap_used = "deny"
[workspace.metadata.cargo-shear]
ignored = [
"codex-agent-graph-store",
"codex-goal-extension",
"icu_provider",
"openssl-sys",
"codex-v8-poc",
@@ -500,11 +501,12 @@ debug = "none"
strip = "symbols"
[profile.release]
lto = "fat"
lto = "thin"
debug = "line-tables-only"
split-debuginfo = "off"
# Because we bundle some of these executables with the TypeScript CLI, we
# remove everything to make the binary as small as possible.
strip = "symbols"
# Keep release binaries symbolicateable until packaging has archived the
# sidecar symbols and stripped the binaries.
strip = false
# See https://github.com/openai/codex/issues/1411 for details.
codegen-units = 1
+2 -98
View File
@@ -1,99 +1,3 @@
# Codex CLI (Rust Implementation)
# Codex CLI
We provide Codex CLI as a standalone executable to ensure a zero-dependency install.
## Installing Codex
Today, the easiest way to install Codex is via `npm`:
```shell
npm i -g @openai/codex
codex
```
You can also install via Homebrew (`brew install --cask codex`) or download a platform-specific release directly from our [GitHub Releases](https://github.com/openai/codex/releases).
## Documentation quickstart
- First run with Codex? Start with [`docs/getting-started.md`](../docs/getting-started.md) (links to the walkthrough for prompts, keyboard shortcuts, and session management).
- Want deeper control? See [`docs/config.md`](../docs/config.md) and [`docs/install.md`](../docs/install.md).
## What's new in the Rust CLI
The Rust implementation is now the maintained Codex CLI and serves as the default experience. It includes a number of features that the legacy TypeScript CLI never supported.
### Config
Codex supports a rich set of configuration options. Note that the Rust CLI uses `config.toml` instead of `config.json`. See [`docs/config.md`](../docs/config.md) for details.
### Model Context Protocol Support
#### MCP client
Codex CLI functions as an MCP client that allows the Codex CLI and IDE extension to connect to MCP servers on startup. See the [`configuration documentation`](../docs/config.md#connecting-to-mcp-servers) for details.
#### MCP server (experimental)
Codex can be launched as an MCP _server_ by running `codex mcp-server`. This allows _other_ MCP clients to use Codex as a tool for another agent.
Use the [`@modelcontextprotocol/inspector`](https://github.com/modelcontextprotocol/inspector) to try it out:
```shell
npx @modelcontextprotocol/inspector codex mcp-server
```
Use `codex mcp` to add/list/get/remove MCP server launchers defined in `config.toml`, and `codex mcp-server` to run the MCP server directly.
### Notifications
You can enable notifications by configuring a script that is run whenever the agent finishes a turn. The [notify documentation](../docs/config.md#notify) includes a detailed example that explains how to get desktop notifications via [terminal-notifier](https://github.com/julienXX/terminal-notifier) on macOS. When Codex detects that it is running under WSL 2 inside Windows Terminal (`WT_SESSION` is set), the TUI automatically falls back to native Windows toast notifications so approval prompts and completed turns surface even though Windows Terminal does not implement OSC 9.
### `codex exec` to run Codex programmatically/non-interactively
To run Codex non-interactively, run `codex exec PROMPT` (you can also pass the prompt via `stdin`) and Codex will work on your task until it decides that it is done and exits. If you provide both a prompt argument and piped stdin, Codex appends stdin as a `<stdin>` block after the prompt so patterns like `echo "my output" | codex exec "Summarize this concisely"` work naturally. Output is printed to the terminal directly. You can set the `RUST_LOG` environment variable to see more about what's going on.
Use `codex exec --ephemeral ...` to run without persisting session rollout files to disk.
### Experimenting with the Codex Sandbox
To test to see what happens when a command is run under the sandbox provided by Codex, use the `sandbox` subcommand in Codex CLI:
```
# Uses the sandbox implementation for the current host OS:
# Seatbelt on macOS, the Linux sandbox on Linux, and Windows restricted token on Windows.
codex sandbox [COMMAND]...
# macOS-only diagnostic option
codex sandbox --log-denials [COMMAND]...
```
`codex sandbox` also accepts `--profile NAME` (`-p NAME`) to layer
`$CODEX_HOME/NAME.config.toml` onto the base user config for the sandboxed
command.
### Selecting a sandbox policy via `--sandbox`
The Rust CLI exposes a dedicated `--sandbox` (`-s`) flag that lets you pick the sandbox policy **without** having to reach for the generic `-c/--config` option:
```shell
# Run Codex with the default, read-only sandbox
codex --sandbox read-only
# Allow the agent to write within the current workspace while still blocking network access
codex --sandbox workspace-write
# Danger! Disable sandboxing entirely (only do this if you are already running in a container or other isolated env)
codex --sandbox danger-full-access
```
In `workspace-write`, Codex also includes `~/.codex/memories` in its writable roots so memory maintenance does not require an extra approval.
## Code Organization
This folder is the root of a Cargo workspace. It contains quite a bit of experimental code, but here are the key crates:
- [`core/`](./core) contains the business logic for Codex. Ultimately, we hope this becomes a library crate that is generally useful for building other Rust/native applications that use Codex.
- [`exec/`](./exec) "headless" CLI for use in automation.
- [`tui/`](./tui) CLI that launches a fullscreen TUI built with [Ratatui](https://ratatui.rs/).
- [`cli/`](./cli) CLI multitool that provides the aforementioned CLIs via subcommands.
If you want to contribute or inspect behavior in detail, start by reading the module-level `README.md` files under each crate and run the project workspace from the top-level `codex-rs` directory so shared config, features, and build scripts stay aligned.
[**Codex CLI Documentation**](https://developers.openai.com/codex/cli)
@@ -63,6 +63,8 @@ use crate::facts::SubAgentThreadStartedInput;
use crate::facts::ThreadInitializationMode;
use crate::facts::TrackEventsContext;
use crate::facts::TurnCodexErrorFact;
use crate::facts::TurnProfile;
use crate::facts::TurnProfileFact;
use crate::facts::TurnResolvedConfigFact;
use crate::facts::TurnStatus;
use crate::facts::TurnSteerRequestError;
@@ -396,6 +398,18 @@ fn sample_turn_resolved_config(thread_id: &str, turn_id: &str) -> TurnResolvedCo
}
}
fn sample_turn_profile() -> TurnProfile {
TurnProfile {
before_first_sampling_ms: 100,
sampling_ms: 700,
between_sampling_overhead_ms: 50,
tool_blocking_ms: 250,
after_last_sampling_ms: 134,
sampling_request_count: 2,
sampling_retry_count: 1,
}
}
fn sample_turn_steer_request(
thread_id: &str,
expected_turn_id: &str,
@@ -649,6 +663,18 @@ async fn ingest_turn_prerequisites(
)
.await;
}
reducer
.ingest(
AnalyticsFact::Custom(CustomAnalyticsFact::TurnProfile(Box::new(
TurnProfileFact {
turn_id: "turn-2".to_string(),
profile: sample_turn_profile(),
},
))),
out,
)
.await;
}
async fn ingest_review_prerequisites(
@@ -1227,6 +1253,8 @@ fn compaction_event_serializes_expected_shape() {
error: None,
active_context_tokens_before: 120_000,
active_context_tokens_after: 18_000,
retained_image_count: None,
compaction_summary_tokens: None,
started_at: 100,
completed_at: 106,
duration_ms: Some(6543),
@@ -1275,6 +1303,8 @@ fn compaction_event_serializes_expected_shape() {
"error": null,
"active_context_tokens_before": 120000,
"active_context_tokens_after": 18000,
"retained_image_count": null,
"compaction_summary_tokens": null,
"started_at": 100,
"completed_at": 106,
"duration_ms": 6543
@@ -1347,6 +1377,7 @@ fn thread_initialized_event_serializes_expected_shape() {
initialization_mode: ThreadInitializationMode::New,
subagent_source: None,
parent_thread_id: None,
forked_from_thread_id: None,
created_at: 1,
},
});
@@ -1379,6 +1410,7 @@ fn thread_initialized_event_serializes_expected_shape() {
"initialization_mode": "new",
"subagent_source": null,
"parent_thread_id": null,
"forked_from_thread_id": null,
"created_at": 1
}
})
@@ -1793,6 +1825,8 @@ async fn compaction_event_ingests_custom_fact() {
error: Some("context limit exceeded".to_string()),
active_context_tokens_before: 131_000,
active_context_tokens_after: 131_000,
retained_image_count: None,
compaction_summary_tokens: None,
started_at: 100,
completed_at: 101,
duration_ms: Some(1200),
@@ -2429,6 +2463,7 @@ fn subagent_thread_started_review_serializes_expected_shape() {
session_id: "session-root".to_string(),
thread_id: "thread-review".to_string(),
parent_thread_id: None,
forked_from_thread_id: None,
product_client_id: "codex-tui".to_string(),
client_name: "codex-tui".to_string(),
client_version: "1.0.0".to_string(),
@@ -2461,18 +2496,26 @@ fn subagent_thread_started_review_serializes_expected_shape() {
assert_eq!(payload["event_params"]["initialization_mode"], "new");
assert_eq!(payload["event_params"]["subagent_source"], "review");
assert_eq!(payload["event_params"]["parent_thread_id"], json!(null));
assert_eq!(
payload["event_params"]["forked_from_thread_id"],
json!(null)
);
}
#[test]
fn subagent_thread_started_thread_spawn_serializes_parent_thread_id() {
fn subagent_thread_started_thread_spawn_serializes_thread_lineage() {
let parent_thread_id =
codex_protocol::ThreadId::from_string("11111111-1111-1111-1111-111111111111")
.expect("valid thread id");
let forked_from_thread_id =
codex_protocol::ThreadId::from_string("22222222-2222-4222-8222-222222222222")
.expect("valid thread id");
let event = TrackEventRequest::ThreadInitialized(subagent_thread_started_event_request(
SubAgentThreadStartedInput {
session_id: "session-root".to_string(),
thread_id: "thread-spawn".to_string(),
parent_thread_id: Some(parent_thread_id.to_string()),
forked_from_thread_id: Some(forked_from_thread_id.to_string()),
product_client_id: "codex-tui".to_string(),
client_name: "codex-tui".to_string(),
client_version: "1.0.0".to_string(),
@@ -2497,6 +2540,10 @@ fn subagent_thread_started_thread_spawn_serializes_parent_thread_id() {
payload["event_params"]["parent_thread_id"],
"11111111-1111-1111-1111-111111111111"
);
assert_eq!(
payload["event_params"]["forked_from_thread_id"],
"22222222-2222-4222-8222-222222222222"
);
assert_eq!(payload["event_params"]["session_id"], "session-root");
}
@@ -2507,6 +2554,7 @@ fn subagent_thread_started_memory_consolidation_serializes_expected_shape() {
session_id: "session-root".to_string(),
thread_id: "thread-memory".to_string(),
parent_thread_id: None,
forked_from_thread_id: None,
product_client_id: "codex-tui".to_string(),
client_name: "codex-tui".to_string(),
client_version: "1.0.0".to_string(),
@@ -2533,6 +2581,7 @@ fn subagent_thread_started_other_serializes_expected_shape() {
session_id: "session-root".to_string(),
thread_id: "thread-guardian".to_string(),
parent_thread_id: None,
forked_from_thread_id: None,
product_client_id: "codex-tui".to_string(),
client_name: "codex-tui".to_string(),
client_version: "1.0.0".to_string(),
@@ -2558,6 +2607,7 @@ fn subagent_thread_started_other_serializes_explicit_parent_thread_id() {
session_id: "session-root".to_string(),
thread_id: "thread-guardian".to_string(),
parent_thread_id: Some(parent_thread_id.to_string()),
forked_from_thread_id: None,
product_client_id: "codex-tui".to_string(),
client_name: "codex-tui".to_string(),
client_version: "1.0.0".to_string(),
@@ -2588,6 +2638,7 @@ async fn subagent_thread_started_publishes_without_initialize() {
session_id: "session-root".to_string(),
thread_id: "thread-review".to_string(),
parent_thread_id: None,
forked_from_thread_id: None,
product_client_id: "codex-tui".to_string(),
client_name: "codex-tui".to_string(),
client_version: "1.0.0".to_string(),
@@ -2662,6 +2713,7 @@ async fn subagent_thread_started_inherits_parent_connection_for_new_thread() {
session_id: "session-root".to_string(),
thread_id: "thread-review".to_string(),
parent_thread_id: Some(parent_thread_id.to_string()),
forked_from_thread_id: None,
product_client_id: "parent-client".to_string(),
client_name: "parent-client".to_string(),
client_version: "1.0.0".to_string(),
@@ -2697,6 +2749,8 @@ async fn subagent_thread_started_inherits_parent_connection_for_new_thread() {
error: None,
active_context_tokens_before: 131_000,
active_context_tokens_after: 64_000,
retained_image_count: None,
compaction_summary_tokens: None,
started_at: 100,
completed_at: 101,
duration_ms: Some(1200),
@@ -2732,6 +2786,7 @@ async fn subagent_tool_items_inherit_parent_connection_metadata() {
session_id: "session-root".to_string(),
thread_id: "thread-subagent".to_string(),
parent_thread_id: Some("thread-1".to_string()),
forked_from_thread_id: None,
product_client_id: "codex-tui".to_string(),
client_name: "codex-tui".to_string(),
client_version: "1.0.0".to_string(),
@@ -2828,6 +2883,7 @@ fn plugin_used_event_serializes_expected_shape() {
"mcp_server_count": 2,
"connector_ids": ["calendar", "drive"],
"product_client_id": originator().value,
"mcp_server_names": ["mcp-1", "mcp-2"],
"thread_id": "thread-3",
"turn_id": "turn-3",
"model_slug": "gpt-5"
@@ -3262,7 +3318,6 @@ fn turn_event_serializes_expected_shape() {
status: Some(TurnStatus::Completed),
turn_error: None,
codex_error_kind: None,
codex_error_subreason: None,
codex_error_http_status_code: None,
steer_count: Some(0),
total_tool_call_count: None,
@@ -3278,6 +3333,13 @@ fn turn_event_serializes_expected_shape() {
output_tokens: None,
reasoning_output_tokens: None,
total_tokens: None,
before_first_sampling_ms: 100,
sampling_ms: 700,
between_sampling_overhead_ms: 50,
tool_blocking_ms: 250,
after_last_sampling_ms: 134,
sampling_request_count: 2,
sampling_retry_count: 1,
duration_ms: Some(1234),
started_at: Some(455),
completed_at: Some(456),
@@ -3328,7 +3390,6 @@ fn turn_event_serializes_expected_shape() {
"status": "completed",
"turn_error": null,
"codex_error_kind": null,
"codex_error_subreason": null,
"codex_error_http_status_code": null,
"steer_count": 0,
"total_tool_call_count": null,
@@ -3344,6 +3405,13 @@ fn turn_event_serializes_expected_shape() {
"output_tokens": null,
"reasoning_output_tokens": null,
"total_tokens": null,
"before_first_sampling_ms": 100,
"sampling_ms": 700,
"between_sampling_overhead_ms": 50,
"tool_blocking_ms": 250,
"after_last_sampling_ms": 134,
"sampling_request_count": 2,
"sampling_retry_count": 1,
"duration_ms": 1234,
"started_at": 455,
"completed_at": 456
@@ -4028,10 +4096,6 @@ async fn turn_lifecycle_emits_failed_turn_event() {
payload["event_params"]["codex_error_kind"],
json!("invalid_request")
);
assert_eq!(
payload["event_params"]["codex_error_subreason"],
json!("unknown turn environment id `env-2`")
);
assert_eq!(
payload["event_params"]["codex_error_http_status_code"],
json!(null)
+7
View File
@@ -19,6 +19,7 @@ use crate::facts::SkillInvokedInput;
use crate::facts::SubAgentThreadStartedInput;
use crate::facts::TrackEventsContext;
use crate::facts::TurnCodexErrorFact;
use crate::facts::TurnProfileFact;
use crate::facts::TurnResolvedConfigFact;
use crate::facts::TurnTokenUsageFact;
use crate::reducer::AnalyticsReducer;
@@ -257,6 +258,12 @@ impl AnalyticsEventsClient {
)));
}
pub fn track_turn_profile(&self, fact: TurnProfileFact) {
self.record_fact(AnalyticsFact::Custom(CustomAnalyticsFact::TurnProfile(
Box::new(fact),
)));
}
pub fn track_turn_codex_error(&self, fact: TurnCodexErrorFact) {
self.record_fact(AnalyticsFact::Custom(CustomAnalyticsFact::TurnCodexError(
Box::new(fact),
+19 -1
View File
@@ -157,6 +157,7 @@ pub(crate) struct ThreadInitializedEventParams {
pub(crate) initialization_mode: ThreadInitializationMode,
pub(crate) subagent_source: Option<String>,
pub(crate) parent_thread_id: Option<String>,
pub(crate) forked_from_thread_id: Option<String>,
pub(crate) created_at: u64,
}
@@ -757,6 +758,8 @@ pub(crate) struct CodexCompactionEventParams {
pub(crate) error: Option<String>,
pub(crate) active_context_tokens_before: i64,
pub(crate) active_context_tokens_after: i64,
pub(crate) retained_image_count: Option<usize>,
pub(crate) compaction_summary_tokens: Option<i64>,
pub(crate) started_at: u64,
pub(crate) completed_at: u64,
pub(crate) duration_ms: Option<u64>,
@@ -800,7 +803,6 @@ pub(crate) struct CodexTurnEventParams {
pub(crate) status: Option<TurnStatus>,
pub(crate) turn_error: Option<CodexErrorInfo>,
pub(crate) codex_error_kind: Option<CodexErrKind>,
pub(crate) codex_error_subreason: Option<String>,
pub(crate) codex_error_http_status_code: Option<u16>,
pub(crate) steer_count: Option<usize>,
pub(crate) total_tool_call_count: Option<usize>,
@@ -816,6 +818,13 @@ pub(crate) struct CodexTurnEventParams {
pub(crate) output_tokens: Option<i64>,
pub(crate) reasoning_output_tokens: Option<i64>,
pub(crate) total_tokens: Option<i64>,
pub(crate) before_first_sampling_ms: u64,
pub(crate) sampling_ms: u64,
pub(crate) between_sampling_overhead_ms: u64,
pub(crate) tool_blocking_ms: u64,
pub(crate) after_last_sampling_ms: u64,
pub(crate) sampling_request_count: u32,
pub(crate) sampling_retry_count: u32,
pub(crate) duration_ms: Option<u64>,
pub(crate) started_at: Option<u64>,
pub(crate) completed_at: Option<u64>,
@@ -865,6 +874,7 @@ pub(crate) struct CodexPluginMetadata {
pub(crate) struct CodexPluginUsedMetadata {
#[serde(flatten)]
pub(crate) plugin: CodexPluginMetadata,
pub(crate) mcp_server_names: Option<Vec<String>>,
pub(crate) thread_id: Option<String>,
pub(crate) turn_id: Option<String>,
pub(crate) model_slug: Option<String>,
@@ -961,6 +971,8 @@ pub(crate) fn codex_compaction_event_params(
error: input.error,
active_context_tokens_before: input.active_context_tokens_before,
active_context_tokens_after: input.active_context_tokens_after,
retained_image_count: input.retained_image_count,
compaction_summary_tokens: input.compaction_summary_tokens,
started_at: input.started_at,
completed_at: input.completed_at,
duration_ms: input.duration_ms,
@@ -971,8 +983,13 @@ pub(crate) fn codex_plugin_used_metadata(
tracking: &TrackEventsContext,
plugin: PluginTelemetryMetadata,
) -> CodexPluginUsedMetadata {
let mcp_server_names = plugin
.capability_summary
.as_ref()
.map(|summary| summary.mcp_server_names.clone());
CodexPluginUsedMetadata {
plugin: codex_plugin_metadata(plugin),
mcp_server_names,
thread_id: Some(tracking.thread_id.clone()),
turn_id: Some(tracking.turn_id.clone()),
model_slug: Some(tracking.model_slug.clone()),
@@ -1054,6 +1071,7 @@ pub(crate) fn subagent_thread_started_event_request(
initialization_mode: ThreadInitializationMode::New,
subagent_source: Some(subagent_source_name(&input.subagent_source)),
parent_thread_id: input.parent_thread_id,
forked_from_thread_id: input.forked_from_thread_id,
created_at: input.created_at,
};
ThreadInitializedEvent {
+21 -24
View File
@@ -30,9 +30,6 @@ use codex_protocol::request_permissions::RequestPermissionsResponse;
use serde::Serialize;
use std::path::PathBuf;
const INVALID_REQUEST_SUBREASON_MAX_BYTES: usize = 512;
const INVALID_REQUEST_SUBREASON_TRUNCATION_SUFFIX: &str = "...";
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct AcceptedLineFingerprint {
pub path_hash: String,
@@ -104,6 +101,23 @@ pub struct TurnTokenUsageFact {
pub token_usage: TokenUsage,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct TurnProfile {
pub before_first_sampling_ms: u64,
pub sampling_ms: u64,
pub between_sampling_overhead_ms: u64,
pub tool_blocking_ms: u64,
pub after_last_sampling_ms: u64,
pub sampling_request_count: u32,
pub sampling_retry_count: u32,
}
#[derive(Clone)]
pub struct TurnProfileFact {
pub turn_id: String,
pub profile: TurnProfile,
}
#[derive(Clone)]
pub struct TurnCodexErrorFact {
pub(crate) turn_id: String,
@@ -165,7 +179,6 @@ pub(crate) enum CodexErrKind {
#[derive(Clone)]
pub(crate) struct TurnCodexError {
pub(crate) kind: CodexErrKind,
pub(crate) subreason: Option<String>,
pub(crate) http_status_code: Option<u16>,
}
@@ -173,26 +186,6 @@ impl TurnCodexError {
fn from_codex_err(error: &CodexErr) -> Self {
Self {
kind: error.into(),
subreason: match error {
CodexErr::InvalidRequest(message) => {
// InvalidRequest can contain raw provider response bodies, so bound the
// analytics copy without changing the source CodexErr.
let subreason = if message.len() <= INVALID_REQUEST_SUBREASON_MAX_BYTES {
message.clone()
} else {
let truncated_len = message.floor_char_boundary(
INVALID_REQUEST_SUBREASON_MAX_BYTES
.saturating_sub(INVALID_REQUEST_SUBREASON_TRUNCATION_SUFFIX.len()),
);
format!(
"{}{INVALID_REQUEST_SUBREASON_TRUNCATION_SUFFIX}",
&message[..truncated_len]
)
};
Some(subreason)
}
_ => None,
},
http_status_code: error.http_status_code_value(),
}
}
@@ -348,6 +341,7 @@ pub struct SubAgentThreadStartedInput {
pub session_id: String,
pub thread_id: String,
pub parent_thread_id: Option<String>,
pub forked_from_thread_id: Option<String>,
pub product_client_id: String,
pub client_name: String,
pub client_version: String,
@@ -416,6 +410,8 @@ pub struct CodexCompactionEvent {
pub error: Option<String>,
pub active_context_tokens_before: i64,
pub active_context_tokens_after: i64,
pub retained_image_count: Option<usize>,
pub compaction_summary_tokens: Option<i64>,
pub started_at: u64,
pub completed_at: u64,
pub duration_ms: Option<u64>,
@@ -475,6 +471,7 @@ pub(crate) enum CustomAnalyticsFact {
GuardianReview(Box<GuardianReviewEventParams>),
TurnResolvedConfig(Box<TurnResolvedConfigFact>),
TurnTokenUsage(Box<TurnTokenUsageFact>),
TurnProfile(Box<TurnProfileFact>),
TurnCodexError(Box<TurnCodexErrorFact>),
SkillInvoked(SkillInvokedInput),
AppMentioned(AppMentionedInput),
+2
View File
@@ -39,6 +39,8 @@ pub use facts::SubAgentThreadStartedInput;
pub use facts::ThreadInitializationMode;
pub use facts::TrackEventsContext;
pub use facts::TurnCodexErrorFact;
pub use facts::TurnProfile;
pub use facts::TurnProfileFact;
pub use facts::TurnResolvedConfigFact;
pub use facts::TurnStatus;
pub use facts::TurnSteerRejectionReason;
+54 -100
View File
@@ -72,6 +72,8 @@ use crate::facts::SubAgentThreadStartedInput;
use crate::facts::ThreadInitializationMode;
use crate::facts::TurnCodexError;
use crate::facts::TurnCodexErrorFact;
use crate::facts::TurnProfile;
use crate::facts::TurnProfileFact;
use crate::facts::TurnResolvedConfigFact;
use crate::facts::TurnStatus;
use crate::facts::TurnSteerRejectionReason;
@@ -316,6 +318,7 @@ struct CompletedTurnState {
duration_ms: Option<u64>,
}
#[derive(Default)]
struct TurnState {
connection_id: Option<u64>,
thread_id: Option<String>,
@@ -323,6 +326,7 @@ struct TurnState {
resolved_config: Option<TurnResolvedConfigFact>,
started_at: Option<u64>,
token_usage: Option<TokenUsage>,
profile: Option<TurnProfile>,
completed: Option<CompletedTurnState>,
codex_error: Option<TurnCodexError>,
latest_diff: Option<String>,
@@ -464,6 +468,9 @@ impl AnalyticsReducer {
CustomAnalyticsFact::TurnTokenUsage(input) => {
self.ingest_turn_token_usage(*input, out).await;
}
CustomAnalyticsFact::TurnProfile(input) => {
self.ingest_turn_profile(*input, out).await;
}
CustomAnalyticsFact::TurnCodexError(input) => {
self.ingest_turn_codex_error(*input);
}
@@ -604,19 +611,7 @@ impl AnalyticsReducer {
let turn_id = input.turn_id.clone();
let thread_id = input.thread_id.clone();
let num_input_images = input.num_input_images;
let turn_state = self.turns.entry(turn_id.clone()).or_insert(TurnState {
connection_id: None,
thread_id: None,
num_input_images: None,
resolved_config: None,
started_at: None,
token_usage: None,
completed: None,
codex_error: None,
latest_diff: None,
steer_count: 0,
tool_counts: TurnToolCounts::default(),
});
let turn_state = self.turns.entry(turn_id.clone()).or_default();
turn_state.thread_id = Some(thread_id);
turn_state.num_input_images = Some(num_input_images);
turn_state.resolved_config = Some(input);
@@ -629,43 +624,30 @@ impl AnalyticsReducer {
out: &mut Vec<TrackEventRequest>,
) {
let turn_id = input.turn_id.clone();
let turn_state = self.turns.entry(turn_id.clone()).or_insert(TurnState {
connection_id: None,
thread_id: None,
num_input_images: None,
resolved_config: None,
started_at: None,
token_usage: None,
completed: None,
codex_error: None,
latest_diff: None,
steer_count: 0,
tool_counts: TurnToolCounts::default(),
});
let turn_state = self.turns.entry(turn_id.clone()).or_default();
turn_state.thread_id = Some(input.thread_id);
turn_state.token_usage = Some(input.token_usage);
self.maybe_emit_turn_event(&turn_id, out).await;
}
async fn ingest_turn_profile(
&mut self,
input: TurnProfileFact,
out: &mut Vec<TrackEventRequest>,
) {
let TurnProfileFact { turn_id, profile } = input;
let turn_state = self.turns.entry(turn_id.clone()).or_default();
turn_state.profile = Some(profile);
self.maybe_emit_turn_event(&turn_id, out).await;
}
fn ingest_turn_codex_error(&mut self, input: TurnCodexErrorFact) {
let TurnCodexErrorFact {
turn_id,
thread_id,
error,
} = input;
let turn_state = self.turns.entry(turn_id).or_insert(TurnState {
connection_id: None,
thread_id: None,
num_input_images: None,
resolved_config: None,
started_at: None,
token_usage: None,
completed: None,
codex_error: None,
latest_diff: None,
steer_count: 0,
tool_counts: TurnToolCounts::default(),
});
let turn_state = self.turns.entry(turn_id).or_default();
turn_state.thread_id.get_or_insert(thread_id);
turn_state.codex_error = Some(error);
}
@@ -818,19 +800,7 @@ impl AnalyticsReducer {
else {
return;
};
let turn_state = self.turns.entry(turn_id.clone()).or_insert(TurnState {
connection_id: None,
thread_id: None,
num_input_images: None,
resolved_config: None,
started_at: None,
token_usage: None,
completed: None,
codex_error: None,
latest_diff: None,
steer_count: 0,
tool_counts: TurnToolCounts::default(),
});
let turn_state = self.turns.entry(turn_id.clone()).or_default();
turn_state.connection_id = Some(connection_id);
turn_state.thread_id = Some(pending_request.thread_id);
turn_state.num_input_images = Some(pending_request.num_input_images);
@@ -1178,61 +1148,19 @@ impl AnalyticsReducer {
self.ingest_guardian_review_completed(notification, out);
}
ServerNotification::TurnStarted(notification) => {
let turn_state = self.turns.entry(notification.turn.id).or_insert(TurnState {
connection_id: None,
thread_id: None,
num_input_images: None,
resolved_config: None,
started_at: None,
token_usage: None,
completed: None,
codex_error: None,
latest_diff: None,
steer_count: 0,
tool_counts: TurnToolCounts::default(),
});
let turn_state = self.turns.entry(notification.turn.id).or_default();
turn_state.started_at = notification
.turn
.started_at
.and_then(|started_at| u64::try_from(started_at).ok());
}
ServerNotification::TurnDiffUpdated(notification) => {
let turn_state =
self.turns
.entry(notification.turn_id.clone())
.or_insert(TurnState {
connection_id: None,
thread_id: None,
num_input_images: None,
resolved_config: None,
started_at: None,
token_usage: None,
completed: None,
codex_error: None,
latest_diff: None,
steer_count: 0,
tool_counts: TurnToolCounts::default(),
});
let turn_state = self.turns.entry(notification.turn_id.clone()).or_default();
turn_state.thread_id = Some(notification.thread_id);
turn_state.latest_diff = Some(notification.diff);
}
ServerNotification::TurnCompleted(notification) => {
let turn_state =
self.turns
.entry(notification.turn.id.clone())
.or_insert(TurnState {
connection_id: None,
thread_id: None,
num_input_images: None,
resolved_config: None,
started_at: None,
token_usage: None,
completed: None,
codex_error: None,
latest_diff: None,
steer_count: 0,
tool_counts: TurnToolCounts::default(),
});
let turn_state = self.turns.entry(notification.turn.id.clone()).or_default();
turn_state.completed = Some(CompletedTurnState {
status: analytics_turn_status(notification.turn.status),
turn_error: notification
@@ -1268,6 +1196,7 @@ impl AnalyticsReducer {
let session_id = thread.session_id;
let thread_id = thread.id;
let parent_thread_id = thread.parent_thread_id;
let forked_from_thread_id = thread.forked_from_id;
let Some(connection_state) = self.connections.get(&connection_id) else {
return;
};
@@ -1299,6 +1228,7 @@ impl AnalyticsReducer {
initialization_mode,
subagent_source: thread_metadata.subagent_source.clone(),
parent_thread_id: thread_metadata.parent_thread_id,
forked_from_thread_id,
created_at: u64::try_from(thread.created_at).unwrap_or_default(),
},
},
@@ -1509,6 +1439,7 @@ impl AnalyticsReducer {
if turn_state.thread_id.is_none()
|| turn_state.num_input_images.is_none()
|| turn_state.resolved_config.is_none()
|| turn_state.profile.is_none()
|| turn_state.completed.is_none()
{
return;
@@ -2455,12 +2386,20 @@ fn codex_turn_event_params(
turn_state: &TurnState,
thread_metadata: &ThreadMetadataState,
) -> CodexTurnEventParams {
let (Some(thread_id), Some(num_input_images), Some(resolved_config), Some(completed)) = (
let (
Some(thread_id),
Some(num_input_images),
Some(resolved_config),
Some(profile),
Some(completed),
) = (
turn_state.thread_id.clone(),
turn_state.num_input_images,
turn_state.resolved_config.clone(),
turn_state.profile.clone(),
turn_state.completed.clone(),
) else {
)
else {
unreachable!("turn event params require a fully populated turn state");
};
let started_at = turn_state.started_at;
@@ -2486,6 +2425,15 @@ fn codex_turn_event_params(
workspace_kind,
is_first_turn,
} = resolved_config;
let TurnProfile {
before_first_sampling_ms,
sampling_ms,
between_sampling_overhead_ms,
tool_blocking_ms,
after_last_sampling_ms,
sampling_request_count,
sampling_retry_count,
} = profile;
let token_usage = turn_state.token_usage.clone();
let codex_error = turn_state.codex_error.as_ref();
CodexTurnEventParams {
@@ -2522,7 +2470,6 @@ fn codex_turn_event_params(
status: completed.status,
turn_error: completed.turn_error,
codex_error_kind: codex_error.map(|error| error.kind),
codex_error_subreason: codex_error.and_then(|error| error.subreason.clone()),
codex_error_http_status_code: codex_error.and_then(|error| error.http_status_code),
steer_count: Some(turn_state.steer_count),
total_tool_call_count: Some(turn_state.tool_counts.total),
@@ -2548,6 +2495,13 @@ fn codex_turn_event_params(
total_tokens: token_usage
.as_ref()
.map(|token_usage| token_usage.total_tokens),
before_first_sampling_ms,
sampling_ms,
between_sampling_overhead_ms,
tool_blocking_ms,
after_last_sampling_ms,
sampling_request_count,
sampling_retry_count,
duration_ms: completed.duration_ms,
started_at,
completed_at: Some(completed.completed_at),
+80 -9
View File
@@ -34,6 +34,30 @@
],
"type": "string"
},
"AgentMessageInputContent": {
"oneOf": [
{
"properties": {
"encrypted_content": {
"type": "string"
},
"type": {
"enum": [
"encrypted_content"
],
"title": "EncryptedContentAgentMessageInputContentType",
"type": "string"
}
},
"required": [
"encrypted_content",
"type"
],
"title": "EncryptedContentAgentMessageInputContent",
"type": "object"
}
]
},
"ApprovalsReviewer": {
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
"enum": [
@@ -1971,15 +1995,8 @@
"type": "string"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"ReasoningItemContent": {
@@ -2127,6 +2144,37 @@
"title": "MessageResponseItem",
"type": "object"
},
{
"properties": {
"author": {
"type": "string"
},
"content": {
"items": {
"$ref": "#/definitions/AgentMessageInputContent"
},
"type": "array"
},
"recipient": {
"type": "string"
},
"type": {
"enum": [
"agent_message"
],
"title": "AgentMessageResponseItemType",
"type": "string"
}
},
"required": [
"author",
"content",
"recipient",
"type"
],
"title": "AgentMessageResponseItem",
"type": "object"
},
{
"properties": {
"content": {
@@ -5921,6 +5969,29 @@
"title": "Account/rateLimits/readRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"account/usage/read"
],
"title": "Account/usage/readRequestMethod",
"type": "string"
},
"params": {
"type": "null"
}
},
"required": [
"id",
"method"
],
"title": "Account/usage/readRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -288,8 +288,8 @@
"environmentId": {
"default": null,
"type": [
"null",
"string"
"string",
"null"
]
},
"itemId": {
@@ -326,4 +326,4 @@
],
"title": "PermissionsRequestApprovalParams",
"type": "object"
}
}
@@ -525,6 +525,13 @@
"agentIdentity"
],
"type": "string"
},
{
"description": "Programmatic Codex auth backed by a personal access token.",
"enum": [
"personalAccessToken"
],
"type": "string"
}
]
},
@@ -2227,6 +2234,12 @@
},
"status": {
"$ref": "#/definitions/McpServerStartupState"
},
"threadId": {
"type": [
"string",
"null"
]
}
},
"required": [
@@ -2777,15 +2790,8 @@
"type": "string"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"ReasoningSummary": {
@@ -4916,6 +4922,23 @@
}
]
},
"TurnModerationMetadataNotification": {
"properties": {
"metadata": true,
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
}
},
"required": [
"metadata",
"threadId",
"turnId"
],
"type": "object"
},
"TurnPlanStep": {
"properties": {
"status": {
@@ -6255,6 +6278,26 @@
"title": "Model/verificationNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"turn/moderationMetadata"
],
"title": "Turn/moderationMetadataNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/TurnModerationMetadataNotification"
}
},
"required": [
"method",
"params"
],
"title": "Turn/moderationMetadataNotification",
"type": "object"
},
{
"properties": {
"method": {
+3 -3
View File
@@ -1593,8 +1593,8 @@
"environmentId": {
"default": null,
"type": [
"null",
"string"
"string",
"null"
]
},
"itemId": {
@@ -2005,4 +2005,4 @@
}
],
"title": "ServerRequest"
}
}
@@ -1833,6 +1833,29 @@
"title": "Account/rateLimits/readRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/v2/RequestId"
},
"method": {
"enum": [
"account/usage/read"
],
"title": "Account/usage/readRequestMethod",
"type": "string"
},
"params": {
"type": "null"
}
},
"required": [
"id",
"method"
],
"title": "Account/usage/readRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -3786,8 +3809,8 @@
"environmentId": {
"default": null,
"type": [
"null",
"string"
"string",
"null"
]
},
"itemId": {
@@ -4904,6 +4927,26 @@
"title": "Model/verificationNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"turn/moderationMetadata"
],
"title": "Turn/moderationMetadataNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/v2/TurnModerationMetadataNotification"
}
},
"required": [
"method",
"params"
],
"title": "Turn/moderationMetadataNotification",
"type": "object"
},
{
"properties": {
"method": {
@@ -5739,6 +5782,62 @@
"title": "AccountRateLimitsUpdatedNotification",
"type": "object"
},
"AccountTokenUsageDailyBucket": {
"properties": {
"startDate": {
"type": "string"
},
"tokens": {
"format": "int64",
"type": "integer"
}
},
"required": [
"startDate",
"tokens"
],
"type": "object"
},
"AccountTokenUsageSummary": {
"properties": {
"currentStreakDays": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"lifetimeTokens": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"longestRunningTurnSec": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"longestStreakDays": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"peakDailyTokens": {
"format": "int64",
"type": [
"integer",
"null"
]
}
},
"type": "object"
},
"AccountUpdatedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -5900,6 +5999,30 @@
"title": "AgentMessageDeltaNotification",
"type": "object"
},
"AgentMessageInputContent": {
"oneOf": [
{
"properties": {
"encrypted_content": {
"type": "string"
},
"type": {
"enum": [
"encrypted_content"
],
"title": "EncryptedContentAgentMessageInputContentType",
"type": "string"
}
},
"required": [
"encrypted_content",
"type"
],
"title": "EncryptedContentAgentMessageInputContent",
"type": "object"
}
]
},
"AgentPath": {
"type": "string"
},
@@ -6278,6 +6401,69 @@
],
"type": "object"
},
"AppTemplateSummary": {
"properties": {
"canonicalConnectorId": {
"type": [
"string",
"null"
]
},
"description": {
"type": [
"string",
"null"
]
},
"logoUrl": {
"type": [
"string",
"null"
]
},
"logoUrlDark": {
"type": [
"string",
"null"
]
},
"materializedAppIds": {
"items": {
"type": "string"
},
"type": "array"
},
"name": {
"type": "string"
},
"reason": {
"anyOf": [
{
"$ref": "#/definitions/v2/AppTemplateUnavailableReason"
},
{
"type": "null"
}
]
},
"templateId": {
"type": "string"
}
},
"required": [
"materializedAppIds",
"name",
"templateId"
],
"type": "object"
},
"AppTemplateUnavailableReason": {
"enum": [
"NOT_CONFIGURED_FOR_WORKSPACE",
"NO_ACTIVE_WORKSPACE"
],
"type": "string"
},
"AppToolApproval": {
"enum": [
"auto",
@@ -6491,6 +6677,13 @@
"agentIdentity"
],
"type": "string"
},
{
"description": "Programmatic Codex auth backed by a personal access token.",
"enum": [
"personalAccessToken"
],
"type": "string"
}
]
},
@@ -7827,12 +8020,12 @@
"null"
]
},
"allowedPermissions": {
"items": {
"type": "string"
"allowedPermissionProfiles": {
"additionalProperties": {
"type": "boolean"
},
"type": [
"array",
"object",
"null"
]
},
@@ -7873,6 +8066,12 @@
}
]
},
"defaultPermissions": {
"type": [
"string",
"null"
]
},
"enforceResidency": {
"anyOf": [
{
@@ -9519,6 +9718,28 @@
"title": "GetAccountResponse",
"type": "object"
},
"GetAccountTokenUsageResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"dailyUsageBuckets": {
"items": {
"$ref": "#/definitions/v2/AccountTokenUsageDailyBucket"
},
"type": [
"array",
"null"
]
},
"summary": {
"$ref": "#/definitions/v2/AccountTokenUsageSummary"
}
},
"required": [
"summary"
],
"title": "GetAccountTokenUsageResponse",
"type": "object"
},
"GitInfo": {
"properties": {
"branch": {
@@ -11241,6 +11462,12 @@
},
"status": {
"$ref": "#/definitions/v2/McpServerStartupState"
},
"threadId": {
"type": [
"string",
"null"
]
}
},
"required": [
@@ -12168,6 +12395,12 @@
},
"PluginDetail": {
"properties": {
"appTemplates": {
"items": {
"$ref": "#/definitions/v2/AppTemplateSummary"
},
"type": "array"
},
"apps": {
"items": {
"$ref": "#/definitions/v2/AppSummary"
@@ -12216,6 +12449,7 @@
}
},
"required": [
"appTemplates",
"apps",
"hooks",
"marketplaceName",
@@ -13525,15 +13759,8 @@
"type": "object"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"ReasoningEffortOption": {
@@ -13987,6 +14214,37 @@
"title": "MessageResponseItem",
"type": "object"
},
{
"properties": {
"author": {
"type": "string"
},
"content": {
"items": {
"$ref": "#/definitions/v2/AgentMessageInputContent"
},
"type": "array"
},
"recipient": {
"type": "string"
},
"type": {
"enum": [
"agent_message"
],
"title": "AgentMessageResponseItemType",
"type": "string"
}
},
"required": [
"author",
"content",
"recipient",
"type"
],
"title": "AgentMessageResponseItem",
"type": "object"
},
{
"properties": {
"content": {
@@ -18483,6 +18741,25 @@
}
]
},
"TurnModerationMetadataNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"metadata": true,
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
}
},
"required": [
"metadata",
"threadId",
"turnId"
],
"title": "TurnModerationMetadataNotification",
"type": "object"
},
"TurnPlanStep": {
"properties": {
"status": {
@@ -19239,4 +19516,4 @@
},
"title": "CodexAppServerProtocol",
"type": "object"
}
}
@@ -104,6 +104,62 @@
"title": "AccountRateLimitsUpdatedNotification",
"type": "object"
},
"AccountTokenUsageDailyBucket": {
"properties": {
"startDate": {
"type": "string"
},
"tokens": {
"format": "int64",
"type": "integer"
}
},
"required": [
"startDate",
"tokens"
],
"type": "object"
},
"AccountTokenUsageSummary": {
"properties": {
"currentStreakDays": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"lifetimeTokens": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"longestRunningTurnSec": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"longestStreakDays": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"peakDailyTokens": {
"format": "int64",
"type": [
"integer",
"null"
]
}
},
"type": "object"
},
"AccountUpdatedNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
@@ -265,6 +321,30 @@
"title": "AgentMessageDeltaNotification",
"type": "object"
},
"AgentMessageInputContent": {
"oneOf": [
{
"properties": {
"encrypted_content": {
"type": "string"
},
"type": {
"enum": [
"encrypted_content"
],
"title": "EncryptedContentAgentMessageInputContentType",
"type": "string"
}
},
"required": [
"encrypted_content",
"type"
],
"title": "EncryptedContentAgentMessageInputContent",
"type": "object"
}
]
},
"AgentPath": {
"type": "string"
},
@@ -643,6 +723,69 @@
],
"type": "object"
},
"AppTemplateSummary": {
"properties": {
"canonicalConnectorId": {
"type": [
"string",
"null"
]
},
"description": {
"type": [
"string",
"null"
]
},
"logoUrl": {
"type": [
"string",
"null"
]
},
"logoUrlDark": {
"type": [
"string",
"null"
]
},
"materializedAppIds": {
"items": {
"type": "string"
},
"type": "array"
},
"name": {
"type": "string"
},
"reason": {
"anyOf": [
{
"$ref": "#/definitions/AppTemplateUnavailableReason"
},
{
"type": "null"
}
]
},
"templateId": {
"type": "string"
}
},
"required": [
"materializedAppIds",
"name",
"templateId"
],
"type": "object"
},
"AppTemplateUnavailableReason": {
"enum": [
"NOT_CONFIGURED_FOR_WORKSPACE",
"NO_ACTIVE_WORKSPACE"
],
"type": "string"
},
"AppToolApproval": {
"enum": [
"auto",
@@ -856,6 +999,13 @@
"agentIdentity"
],
"type": "string"
},
{
"description": "Programmatic Codex auth backed by a personal access token.",
"enum": [
"personalAccessToken"
],
"type": "string"
}
]
},
@@ -2592,6 +2742,29 @@
"title": "Account/rateLimits/readRequest",
"type": "object"
},
{
"properties": {
"id": {
"$ref": "#/definitions/RequestId"
},
"method": {
"enum": [
"account/usage/read"
],
"title": "Account/usage/readRequestMethod",
"type": "string"
},
"params": {
"type": "null"
}
},
"required": [
"id",
"method"
],
"title": "Account/usage/readRequest",
"type": "object"
},
{
"properties": {
"id": {
@@ -4189,12 +4362,12 @@
"null"
]
},
"allowedPermissions": {
"items": {
"type": "string"
"allowedPermissionProfiles": {
"additionalProperties": {
"type": "boolean"
},
"type": [
"array",
"object",
"null"
]
},
@@ -4235,6 +4408,12 @@
}
]
},
"defaultPermissions": {
"type": [
"string",
"null"
]
},
"enforceResidency": {
"anyOf": [
{
@@ -5992,6 +6171,28 @@
"title": "GetAccountResponse",
"type": "object"
},
"GetAccountTokenUsageResponse": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"dailyUsageBuckets": {
"items": {
"$ref": "#/definitions/AccountTokenUsageDailyBucket"
},
"type": [
"array",
"null"
]
},
"summary": {
"$ref": "#/definitions/AccountTokenUsageSummary"
}
},
"required": [
"summary"
],
"title": "GetAccountTokenUsageResponse",
"type": "object"
},
"GitInfo": {
"properties": {
"branch": {
@@ -7763,6 +7964,12 @@
},
"status": {
"$ref": "#/definitions/McpServerStartupState"
},
"threadId": {
"type": [
"string",
"null"
]
}
},
"required": [
@@ -8690,6 +8897,12 @@
},
"PluginDetail": {
"properties": {
"appTemplates": {
"items": {
"$ref": "#/definitions/AppTemplateSummary"
},
"type": "array"
},
"apps": {
"items": {
"$ref": "#/definitions/AppSummary"
@@ -8738,6 +8951,7 @@
}
},
"required": [
"appTemplates",
"apps",
"hooks",
"marketplaceName",
@@ -10047,15 +10261,8 @@
"type": "object"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"ReasoningEffortOption": {
@@ -10509,6 +10716,37 @@
"title": "MessageResponseItem",
"type": "object"
},
{
"properties": {
"author": {
"type": "string"
},
"content": {
"items": {
"$ref": "#/definitions/AgentMessageInputContent"
},
"type": "array"
},
"recipient": {
"type": "string"
},
"type": {
"enum": [
"agent_message"
],
"title": "AgentMessageResponseItemType",
"type": "string"
}
},
"required": [
"author",
"content",
"recipient",
"type"
],
"title": "AgentMessageResponseItem",
"type": "object"
},
{
"properties": {
"content": {
@@ -12302,6 +12540,26 @@
"title": "Model/verificationNotification",
"type": "object"
},
{
"properties": {
"method": {
"enum": [
"turn/moderationMetadata"
],
"title": "Turn/moderationMetadataNotificationMethod",
"type": "string"
},
"params": {
"$ref": "#/definitions/TurnModerationMetadataNotification"
}
},
"required": [
"method",
"params"
],
"title": "Turn/moderationMetadataNotification",
"type": "object"
},
{
"properties": {
"method": {
@@ -16300,6 +16558,25 @@
}
]
},
"TurnModerationMetadataNotification": {
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"metadata": true,
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
}
},
"required": [
"metadata",
"threadId",
"turnId"
],
"title": "TurnModerationMetadataNotification",
"type": "object"
},
"TurnPlanStep": {
"properties": {
"status": {
@@ -31,6 +31,13 @@
"agentIdentity"
],
"type": "string"
},
{
"description": "Programmatic Codex auth backed by a personal access token.",
"enum": [
"personalAccessToken"
],
"type": "string"
}
]
},
@@ -667,15 +667,8 @@
"type": "string"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"ReasoningSummary": {
@@ -94,12 +94,12 @@
"null"
]
},
"allowedPermissions": {
"items": {
"type": "string"
"allowedPermissionProfiles": {
"additionalProperties": {
"type": "boolean"
},
"type": [
"array",
"object",
"null"
]
},
@@ -140,6 +140,12 @@
}
]
},
"defaultPermissions": {
"type": [
"string",
"null"
]
},
"enforceResidency": {
"anyOf": [
{
@@ -0,0 +1,80 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"AccountTokenUsageDailyBucket": {
"properties": {
"startDate": {
"type": "string"
},
"tokens": {
"format": "int64",
"type": "integer"
}
},
"required": [
"startDate",
"tokens"
],
"type": "object"
},
"AccountTokenUsageSummary": {
"properties": {
"currentStreakDays": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"lifetimeTokens": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"longestRunningTurnSec": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"longestStreakDays": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"peakDailyTokens": {
"format": "int64",
"type": [
"integer",
"null"
]
}
},
"type": "object"
}
},
"properties": {
"dailyUsageBuckets": {
"items": {
"$ref": "#/definitions/AccountTokenUsageDailyBucket"
},
"type": [
"array",
"null"
]
},
"summary": {
"$ref": "#/definitions/AccountTokenUsageSummary"
}
},
"required": [
"summary"
],
"title": "GetAccountTokenUsageResponse",
"type": "object"
}
@@ -462,15 +462,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"TextElement": {
@@ -462,15 +462,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"TextElement": {
@@ -23,6 +23,12 @@
},
"status": {
"$ref": "#/definitions/McpServerStartupState"
},
"threadId": {
"type": [
"string",
"null"
]
}
},
"required": [
@@ -185,15 +185,8 @@
"type": "object"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"ReasoningEffortOption": {
@@ -37,6 +37,69 @@
],
"type": "object"
},
"AppTemplateSummary": {
"properties": {
"canonicalConnectorId": {
"type": [
"string",
"null"
]
},
"description": {
"type": [
"string",
"null"
]
},
"logoUrl": {
"type": [
"string",
"null"
]
},
"logoUrlDark": {
"type": [
"string",
"null"
]
},
"materializedAppIds": {
"items": {
"type": "string"
},
"type": "array"
},
"name": {
"type": "string"
},
"reason": {
"anyOf": [
{
"$ref": "#/definitions/AppTemplateUnavailableReason"
},
{
"type": "null"
}
]
},
"templateId": {
"type": "string"
}
},
"required": [
"materializedAppIds",
"name",
"templateId"
],
"type": "object"
},
"AppTemplateUnavailableReason": {
"enum": [
"NOT_CONFIGURED_FOR_WORKSPACE",
"NO_ACTIVE_WORKSPACE"
],
"type": "string"
},
"HookEventName": {
"enum": [
"preToolUse",
@@ -78,6 +141,12 @@
},
"PluginDetail": {
"properties": {
"appTemplates": {
"items": {
"$ref": "#/definitions/AppTemplateSummary"
},
"type": "array"
},
"apps": {
"items": {
"$ref": "#/definitions/AppSummary"
@@ -126,6 +195,7 @@
}
},
"required": [
"appTemplates",
"apps",
"hooks",
"marketplaceName",
@@ -1,6 +1,30 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"AgentMessageInputContent": {
"oneOf": [
{
"properties": {
"encrypted_content": {
"type": "string"
},
"type": {
"enum": [
"encrypted_content"
],
"title": "EncryptedContentAgentMessageInputContentType",
"type": "string"
}
},
"required": [
"encrypted_content",
"type"
],
"title": "EncryptedContentAgentMessageInputContent",
"type": "object"
}
]
},
"ContentItem": {
"oneOf": [
{
@@ -369,6 +393,37 @@
"title": "MessageResponseItem",
"type": "object"
},
{
"properties": {
"author": {
"type": "string"
},
"content": {
"items": {
"$ref": "#/definitions/AgentMessageInputContent"
},
"type": "array"
},
"recipient": {
"type": "string"
},
"type": {
"enum": [
"agent_message"
],
"title": "AgentMessageResponseItemType",
"type": "string"
}
},
"required": [
"author",
"content",
"recipient",
"type"
],
"title": "AgentMessageResponseItem",
"type": "object"
},
{
"properties": {
"content": {
@@ -606,15 +606,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"TextElement": {
@@ -1,6 +1,10 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"AbsolutePathBuf": {
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"ApprovalsReviewer": {
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
"enum": [
@@ -718,15 +718,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"SandboxPolicy": {
@@ -632,15 +632,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"SessionSource": {
@@ -632,15 +632,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"SessionSource": {
@@ -632,15 +632,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"SessionSource": {
@@ -1,6 +1,34 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"AbsolutePathBuf": {
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
"type": "string"
},
"AgentMessageInputContent": {
"oneOf": [
{
"properties": {
"encrypted_content": {
"type": "string"
},
"type": {
"enum": [
"encrypted_content"
],
"title": "EncryptedContentAgentMessageInputContentType",
"type": "string"
}
},
"required": [
"encrypted_content",
"type"
],
"title": "EncryptedContentAgentMessageInputContent",
"type": "object"
}
]
},
"ApprovalsReviewer": {
"description": "Configures who approval requests are routed to for review. Examples include sandbox escapes, blocked network access, MCP approval prompts, and ARC escalations. Defaults to `user`. `auto_review` uses a carefully prompted subagent to gather relevant context and apply a risk-based decision framework before approving or denying the request. The legacy value `guardian_subagent` is accepted for compatibility.",
"enum": [
@@ -436,6 +464,37 @@
"title": "MessageResponseItem",
"type": "object"
},
{
"properties": {
"author": {
"type": "string"
},
"content": {
"items": {
"$ref": "#/definitions/AgentMessageInputContent"
},
"type": "array"
},
"recipient": {
"type": "string"
},
"type": {
"enum": [
"agent_message"
],
"title": "AgentMessageResponseItemType",
"type": "string"
}
},
"required": [
"author",
"content",
"recipient",
"type"
],
"title": "AgentMessageResponseItem",
"type": "object"
},
{
"properties": {
"content": {
@@ -718,15 +718,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"SandboxPolicy": {
@@ -632,15 +632,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"SessionSource": {
@@ -124,15 +124,8 @@
"type": "string"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"ReasoningSummary": {
@@ -718,15 +718,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"SandboxPolicy": {
@@ -632,15 +632,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"SessionSource": {
@@ -632,15 +632,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"SessionSource": {
@@ -606,15 +606,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"TextElement": {
@@ -0,0 +1,19 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"properties": {
"metadata": true,
"threadId": {
"type": "string"
},
"turnId": {
"type": "string"
}
},
"required": [
"metadata",
"threadId",
"turnId"
],
"title": "TurnModerationMetadataNotification",
"type": "object"
}
@@ -154,15 +154,8 @@
"type": "string"
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"ReasoningSummary": {
@@ -606,15 +606,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"TextElement": {
@@ -606,15 +606,8 @@
]
},
"ReasoningEffort": {
"description": "See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning",
"enum": [
"none",
"minimal",
"low",
"medium",
"high",
"xhigh"
],
"description": "A non-empty reasoning effort value advertised by the model.",
"minLength": 1,
"type": "string"
},
"TextElement": {
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AgentMessageInputContent = { "type": "encrypted_content", encrypted_content: string, };
+1 -1
View File
@@ -5,4 +5,4 @@
/**
* Authentication mode for OpenAI-backed providers.
*/
export type AuthMode = "apikey" | "chatgpt" | "chatgptAuthTokens" | "agentIdentity";
export type AuthMode = "apikey" | "chatgpt" | "chatgptAuthTokens" | "agentIdentity" | "personalAccessToken";
File diff suppressed because one or more lines are too long
@@ -5,4 +5,4 @@
/**
* See https://platform.openai.com/docs/guides/reasoning?api-mode=responses#get-started-with-reasoning
*/
export type ReasoningEffort = "none" | "minimal" | "low" | "medium" | "high" | "xhigh";
export type ReasoningEffort = string;
@@ -1,6 +1,7 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AgentMessageInputContent } from "./AgentMessageInputContent";
import type { ContentItem } from "./ContentItem";
import type { FunctionCallOutputBody } from "./FunctionCallOutputBody";
import type { LocalShellAction } from "./LocalShellAction";
@@ -10,7 +11,7 @@ import type { ReasoningItemContent } from "./ReasoningItemContent";
import type { ReasoningItemReasoningSummary } from "./ReasoningItemReasoningSummary";
import type { WebSearchAction } from "./WebSearchAction";
export type ResponseItem = { "type": "message", role: string, content: Array<ContentItem>, phase?: MessagePhase, } | { "type": "reasoning", summary: Array<ReasoningItemReasoningSummary>, content?: Array<ReasoningItemContent>, encrypted_content: string | null, } | { "type": "local_shell_call",
export type ResponseItem = { "type": "message", role: string, content: Array<ContentItem>, phase?: MessagePhase, } | { "type": "agent_message", author: string, recipient: string, content: Array<AgentMessageInputContent>, } | { "type": "reasoning", summary: Array<ReasoningItemReasoningSummary>, content?: Array<ReasoningItemContent>, encrypted_content: string | null, } | { "type": "local_shell_call",
/**
* Set when using the Responses API.
*/
File diff suppressed because one or more lines are too long
+1
View File
@@ -1,6 +1,7 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
export type { AbsolutePathBuf } from "./AbsolutePathBuf";
export type { AgentMessageInputContent } from "./AgentMessageInputContent";
export type { AgentPath } from "./AgentPath";
export type { ApplyPatchApprovalParams } from "./ApplyPatchApprovalParams";
export type { ApplyPatchApprovalResponse } from "./ApplyPatchApprovalResponse";
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AccountTokenUsageDailyBucket = { startDate: string, tokens: bigint, };
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AccountTokenUsageSummary = { lifetimeTokens: bigint | null, peakDailyTokens: bigint | null, longestRunningTurnSec: bigint | null, currentStreakDays: bigint | null, longestStreakDays: bigint | null, };
@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AppTemplateUnavailableReason } from "./AppTemplateUnavailableReason";
export type AppTemplateSummary = { templateId: string, name: string, description: string | null, canonicalConnectorId: string | null, logoUrl: string | null, logoUrlDark: string | null, materializedAppIds: Array<string>, reason: AppTemplateUnavailableReason | null, };
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type AppTemplateUnavailableReason = "NOT_CONFIGURED_FOR_WORKSPACE" | "NO_ACTIVE_WORKSPACE";
@@ -8,4 +8,4 @@ import type { ResidencyRequirement } from "./ResidencyRequirement";
import type { SandboxMode } from "./SandboxMode";
import type { WindowsSandboxSetupMode } from "./WindowsSandboxSetupMode";
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWindowsSandboxImplementations: Array<WindowsSandboxSetupMode> | null, allowedPermissions: Array<string> | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWindowsSandboxImplementations: Array<WindowsSandboxSetupMode> | null, allowedPermissionProfiles: { [key in string]?: boolean } | null, defaultPermissions: string | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, allowAppshots: boolean | null, computerUse: ComputerUseRequirements | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};
@@ -0,0 +1,7 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AccountTokenUsageDailyBucket } from "./AccountTokenUsageDailyBucket";
import type { AccountTokenUsageSummary } from "./AccountTokenUsageSummary";
export type GetAccountTokenUsageResponse = { summary: AccountTokenUsageSummary, dailyUsageBuckets: Array<AccountTokenUsageDailyBucket> | null, };
@@ -3,4 +3,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { McpServerStartupState } from "./McpServerStartupState";
export type McpServerStatusUpdatedNotification = { name: string, status: McpServerStartupState, error: string | null, };
export type McpServerStatusUpdatedNotification = { threadId: string | null, name: string, status: McpServerStartupState, error: string | null, };
@@ -3,8 +3,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
import type { AppSummary } from "./AppSummary";
import type { AppTemplateSummary } from "./AppTemplateSummary";
import type { PluginHookSummary } from "./PluginHookSummary";
import type { PluginSummary } from "./PluginSummary";
import type { SkillSummary } from "./SkillSummary";
export type PluginDetail = { marketplaceName: string, marketplacePath: AbsolutePathBuf | null, summary: PluginSummary, description: string | null, skills: Array<SkillSummary>, hooks: Array<PluginHookSummary>, apps: Array<AppSummary>, mcpServers: Array<string>, };
export type PluginDetail = { marketplaceName: string, marketplacePath: AbsolutePathBuf | null, summary: PluginSummary, description: string | null, skills: Array<SkillSummary>, hooks: Array<PluginHookSummary>, apps: Array<AppSummary>, appTemplates: Array<AppTemplateSummary>, mcpServers: Array<string>, };
@@ -0,0 +1,6 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { JsonValue } from "../serde_json/JsonValue";
export type TurnModerationMetadataNotification = { threadId: string, turnId: string, metadata: JsonValue, };
@@ -3,6 +3,8 @@
export type { Account } from "./Account";
export type { AccountLoginCompletedNotification } from "./AccountLoginCompletedNotification";
export type { AccountRateLimitsUpdatedNotification } from "./AccountRateLimitsUpdatedNotification";
export type { AccountTokenUsageDailyBucket } from "./AccountTokenUsageDailyBucket";
export type { AccountTokenUsageSummary } from "./AccountTokenUsageSummary";
export type { AccountUpdatedNotification } from "./AccountUpdatedNotification";
export type { ActivePermissionProfile } from "./ActivePermissionProfile";
export type { AddCreditsNudgeCreditType } from "./AddCreditsNudgeCreditType";
@@ -21,6 +23,8 @@ export type { AppMetadata } from "./AppMetadata";
export type { AppReview } from "./AppReview";
export type { AppScreenshot } from "./AppScreenshot";
export type { AppSummary } from "./AppSummary";
export type { AppTemplateSummary } from "./AppTemplateSummary";
export type { AppTemplateUnavailableReason } from "./AppTemplateUnavailableReason";
export type { AppToolApproval } from "./AppToolApproval";
export type { AppToolsConfig } from "./AppToolsConfig";
export type { ApprovalsReviewer } from "./ApprovalsReviewer";
@@ -139,6 +143,7 @@ export type { FsWriteFileResponse } from "./FsWriteFileResponse";
export type { GetAccountParams } from "./GetAccountParams";
export type { GetAccountRateLimitsResponse } from "./GetAccountRateLimitsResponse";
export type { GetAccountResponse } from "./GetAccountResponse";
export type { GetAccountTokenUsageResponse } from "./GetAccountTokenUsageResponse";
export type { GitInfo } from "./GitInfo";
export type { GrantedPermissionProfile } from "./GrantedPermissionProfile";
export type { GuardianApprovalReview } from "./GuardianApprovalReview";
@@ -445,6 +450,7 @@ export type { TurnError } from "./TurnError";
export type { TurnInterruptParams } from "./TurnInterruptParams";
export type { TurnInterruptResponse } from "./TurnInterruptResponse";
export type { TurnItemsView } from "./TurnItemsView";
export type { TurnModerationMetadataNotification } from "./TurnModerationMetadataNotification";
export type { TurnPlanStep } from "./TurnPlanStep";
export type { TurnPlanStepStatus } from "./TurnPlanStepStatus";
export type { TurnPlanUpdatedNotification } from "./TurnPlanUpdatedNotification";
@@ -2963,11 +2963,14 @@ permissionProfile?: string | null};
let client_request_json = fs::read_to_string(output_dir.join("ClientRequest.json"))?;
assert!(client_request_json.contains("remoteControl/pairing/start"));
assert!(client_request_json.contains("remoteControl/pairing/status"));
assert!(client_request_json.contains("remoteControl/client/list"));
assert!(client_request_json.contains("remoteControl/client/revoke"));
for schema in [
"RemoteControlPairingStartParams.json",
"RemoteControlPairingStartResponse.json",
"RemoteControlPairingStatusParams.json",
"RemoteControlPairingStatusResponse.json",
"RemoteControlClientsListParams.json",
"RemoteControlClientsListResponse.json",
"RemoteControlClientsRevokeParams.json",
@@ -36,6 +36,21 @@ pub enum AuthMode {
#[ts(rename = "agentIdentity")]
#[strum(serialize = "agentIdentity")]
AgentIdentity,
/// Programmatic Codex auth backed by a personal access token.
#[serde(rename = "personalAccessToken")]
#[ts(rename = "personalAccessToken")]
#[strum(serialize = "personalAccessToken")]
PersonalAccessToken,
}
impl AuthMode {
/// Returns whether this mode represents an authenticated human ChatGPT account.
pub fn has_chatgpt_account(self) -> bool {
match self {
Self::Chatgpt | Self::ChatgptAuthTokens | Self::PersonalAccessToken => true,
Self::ApiKey | Self::AgentIdentity => false,
}
}
}
macro_rules! experimental_reason_expr {
@@ -849,6 +864,12 @@ client_request_definitions! {
serialization: global("remote-control-pairing"),
response: v2::RemoteControlPairingStartResponse,
},
#[experimental("remoteControl/pairing/status")]
RemoteControlPairingStatus => "remoteControl/pairing/status" {
params: v2::RemoteControlPairingStatusParams,
serialization: global_shared_read("remote-control-pairing"),
response: v2::RemoteControlPairingStatusResponse,
},
#[experimental("remoteControl/client/list")]
RemoteControlClientsList => "remoteControl/client/list" {
params: v2::RemoteControlClientsListParams,
@@ -949,6 +970,12 @@ client_request_definitions! {
response: v2::GetAccountRateLimitsResponse,
},
GetAccountTokenUsage => "account/usage/read" {
params: #[ts(type = "undefined")] #[serde(skip_serializing_if = "Option::is_none")] Option<()>,
serialization: None,
response: v2::GetAccountTokenUsageResponse,
},
SendAddCreditsNudgeEmail => "account/sendAddCreditsNudgeEmail" {
params: v2::SendAddCreditsNudgeEmailParams,
serialization: global("account-auth"),
@@ -1549,6 +1576,8 @@ server_notification_definitions! {
ContextCompacted => "thread/compacted" (v2::ContextCompactedNotification),
ModelRerouted => "model/rerouted" (v2::ModelReroutedNotification),
ModelVerification => "model/verification" (v2::ModelVerificationNotification),
#[experimental("turn/moderationMetadata")]
TurnModerationMetadata => "turn/moderationMetadata" (v2::TurnModerationMetadataNotification),
Warning => "warning" (v2::WarningNotification),
GuardianWarning => "guardianWarning" (v2::GuardianWarningNotification),
DeprecationNotice => "deprecationNotice" (v2::DeprecationNoticeNotification),
@@ -2006,6 +2035,19 @@ mod tests {
"remote-control-pairing"
))
);
let remote_control_pairing_status = ClientRequest::RemoteControlPairingStatus {
request_id: request_id(),
params: v2::RemoteControlPairingStatusParams {
pairing_code: Some("pairing-code".to_string()),
manual_pairing_code: None,
},
};
assert_eq!(
remote_control_pairing_status.serialization_scope(),
Some(ClientRequestSerializationScope::GlobalSharedRead(
"remote-control-pairing"
))
);
let remote_control_clients_list = ClientRequest::RemoteControlClientsList {
request_id: request_id(),
params: v2::RemoteControlClientsListParams::default(),
@@ -2370,6 +2412,24 @@ mod tests {
Ok(())
}
#[test]
fn serialize_get_account_token_usage() -> Result<()> {
let request = ClientRequest::GetAccountTokenUsage {
request_id: RequestId::Integer(1),
params: None,
};
assert_eq!(request.id(), &RequestId::Integer(1));
assert_eq!(request.method(), "account/usage/read");
assert_eq!(
json!({
"method": "account/usage/read",
"id": 1,
}),
serde_json::to_value(&request)?,
);
Ok(())
}
#[test]
fn serialize_client_response() -> Result<()> {
let cwd = absolute_path("/tmp");
@@ -3222,6 +3282,21 @@ mod tests {
);
}
#[test]
fn turn_moderation_metadata_notification_is_marked_experimental() {
let notification =
ServerNotification::TurnModerationMetadata(v2::TurnModerationMetadataNotification {
thread_id: "thr_123".to_string(),
turn_id: "turn_123".to_string(),
metadata: json!({"presentation": "inline"}),
});
assert_eq!(
crate::experimental_api::ExperimentalApi::experimental_reason(&notification),
Some("turn/moderationMetadata")
);
}
#[test]
fn thread_realtime_started_notification_is_marked_experimental() {
let notification =
@@ -621,7 +621,7 @@ impl ThreadHistoryBuilder {
receiver_thread_ids: Vec::new(),
prompt: Some(payload.prompt.clone()),
model: Some(payload.model.clone()),
reasoning_effort: Some(payload.reasoning_effort),
reasoning_effort: Some(payload.reasoning_effort.clone()),
agents_states: HashMap::new(),
};
self.upsert_item_in_current_turn(item);
@@ -656,7 +656,7 @@ impl ThreadHistoryBuilder {
receiver_thread_ids,
prompt: Some(payload.prompt.clone()),
model: Some(payload.model.clone()),
reasoning_effort: Some(payload.reasoning_effort),
reasoning_effort: Some(payload.reasoning_effort.clone()),
agents_states,
});
}
@@ -138,6 +138,78 @@ pub struct CancelLoginAccountResponse {
pub status: CancelLoginAccountStatus,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AccountSessionsAddParams {
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub switch_to_added_account: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AccountSessionsListParams {
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub refresh_workspace_metadata: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AccountSessionsLogoutParams {
pub session_id: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AccountSessionsSwitchParams {
pub session_id: String,
pub account_id: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AccountSessionsResponse {
pub active_session_id: Option<String>,
pub sessions: Vec<AccountSession>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AccountSession {
pub session_id: String,
pub email: Option<String>,
pub user_id: Option<String>,
pub display_name: Option<String>,
pub image_url: Option<String>,
pub last_used_at: i64,
pub is_active: bool,
pub selected_workspace_account_id: Option<String>,
pub workspaces: Vec<AccountSessionWorkspace>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AccountSessionWorkspace {
pub account_id: String,
pub name: Option<String>,
pub image_url: Option<String>,
pub kind: Option<AccountSessionWorkspaceKind>,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub enum AccountSessionWorkspaceKind {
Personal,
Workspace,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -186,6 +258,33 @@ pub struct GetAccountRateLimitsResponse {
pub rate_limits_by_limit_id: Option<HashMap<String, RateLimitSnapshot>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct GetAccountTokenUsageResponse {
pub summary: AccountTokenUsageSummary,
pub daily_usage_buckets: Option<Vec<AccountTokenUsageDailyBucket>>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AccountTokenUsageSummary {
pub lifetime_tokens: Option<i64>,
pub peak_daily_tokens: Option<i64>,
pub longest_running_turn_sec: Option<i64>,
pub current_streak_days: Option<i64>,
pub longest_streak_days: Option<i64>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct AccountTokenUsageDailyBucket {
pub start_date: String,
pub tokens: i64,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
@@ -375,7 +375,8 @@ pub struct ConfigRequirements {
pub allowed_approvals_reviewers: Option<Vec<ApprovalsReviewer>>,
pub allowed_sandbox_modes: Option<Vec<SandboxMode>>,
pub allowed_windows_sandbox_implementations: Option<Vec<WindowsSandboxSetupMode>>,
pub allowed_permissions: Option<Vec<String>>,
pub allowed_permission_profiles: Option<BTreeMap<String, bool>>,
pub default_permissions: Option<String>,
pub allowed_web_search_modes: Option<Vec<WebSearchMode>>,
pub allow_managed_hooks_only: Option<bool>,
pub allow_appshots: Option<bool>,

Some files were not shown because too many files have changed in this diff Show More