diff --git a/.github/scripts/smoke_macos_x86_v8.sh b/.github/scripts/smoke_macos_x86_v8.sh new file mode 100755 index 000000000000..891e1331da14 --- /dev/null +++ b/.github/scripts/smoke_macos_x86_v8.sh @@ -0,0 +1,111 @@ +#!/usr/bin/env bash + +set -euo pipefail + +usage() { + echo "usage: $0 " >&2 + exit 2 +} + +[[ $# -eq 4 ]] || usage + +archive="$1" +binding="$2" +sandbox="$3" +target_dir="$4" + +case "$sandbox" in + true | false) ;; + *) usage ;; +esac + +if [[ "$(uname -s)" != "Darwin" || "$(uname -m)" != "x86_64" ]]; then + echo "Intel macOS V8 smoke must run natively on x86_64 macOS." >&2 + exit 1 +fi +if [[ ! -f "$archive" || ! -f "$binding" ]]; then + echo "V8 archive or binding is missing." >&2 + exit 1 +fi + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +production_entitlements="${repo_root}/.github/scripts/macos-signing/codex.entitlements.plist" +expanded_entitlements="${target_dir}/codex-allow-unsigned-executable-memory.entitlements.plist" +crash_report_dir="${target_dir}/crash-reports" +crash_report_marker="${target_dir}/runtime-smoke-started" + +mkdir -p "$target_dir" "$crash_report_dir" +touch "$crash_report_marker" +cp "$production_entitlements" "$expanded_entitlements" +if /usr/libexec/PlistBuddy \ + -c "Print :com.apple.security.cs.allow-unsigned-executable-memory" \ + "$expanded_entitlements" >/dev/null 2>&1; then + /usr/libexec/PlistBuddy \ + -c "Set :com.apple.security.cs.allow-unsigned-executable-memory true" \ + "$expanded_entitlements" +else + /usr/libexec/PlistBuddy \ + -c "Add :com.apple.security.cs.allow-unsigned-executable-memory bool true" \ + "$expanded_entitlements" +fi + +cargo_features=() +if [[ "$sandbox" == "true" ]]; then + cargo_features=(--features sandbox) +fi + +( + cd "${repo_root}/codex-rs" + export CARGO_TARGET_DIR="$target_dir" + export RUSTY_V8_ARCHIVE="$archive" + export RUSTY_V8_SRC_BINDING_PATH="$binding" + + cargo test -p codex-v8-poc "${cargo_features[@]}" + cargo build --release -p codex-v8-poc --example code_mode_runtime_smoke \ + "${cargo_features[@]}" +) + +probe="${target_dir}/release/examples/code_mode_runtime_smoke" +if [[ ! -x "$probe" ]]; then + echo "Runtime smoke executable was not built at $probe." >&2 + exit 1 +fi +if [[ "$(lipo -archs "$probe")" != "x86_64" ]]; then + echo "Runtime smoke executable is not x86_64: $(lipo -archs "$probe")" >&2 + exit 1 +fi + +failures=0 +for entitlement_profile in production allow-unsigned-executable-memory; do + case "$entitlement_profile" in + production) entitlements="$production_entitlements" ;; + allow-unsigned-executable-memory) entitlements="$expanded_entitlements" ;; + esac + + signed_probe="${target_dir}/code_mode_runtime_smoke-${entitlement_profile}" + cp "$probe" "$signed_probe" + codesign --force --sign - --options runtime --entitlements "$entitlements" "$signed_probe" + codesign --verify --strict --verbose=2 "$signed_probe" + + for provider in ring aws-lc; do + echo "Running code-mode runtime smoke: entitlements=${entitlement_profile} provider=${provider}" + if "$signed_probe" "$provider"; then + echo "PASS entitlements=${entitlement_profile} provider=${provider}" + else + status=$? + echo "FAIL entitlements=${entitlement_profile} provider=${provider} status=${status}" >&2 + failures=$((failures + 1)) + fi + done +done + +diagnostic_reports="${HOME}/Library/Logs/DiagnosticReports" +if [[ -d "$diagnostic_reports" ]]; then + find "$diagnostic_reports" -type f -name '*.ips' -newer "$crash_report_marker" \ + -exec cp {} "$crash_report_dir" \; +fi + +if ((failures > 0)); then + echo "$failures Intel macOS code-mode runtime smoke configuration(s) failed." >&2 + exit 1 +fi diff --git a/.github/scripts/test_v8_canary_changes.py b/.github/scripts/test_v8_canary_changes.py index e4ce6424ed09..dfb28073e9a7 100644 --- a/.github/scripts/test_v8_canary_changes.py +++ b/.github/scripts/test_v8_canary_changes.py @@ -4,6 +4,7 @@ from pathlib import Path from v8_canary_changes import changed_files +from v8_canary_changes import canary_required from v8_canary_changes import merge_base from v8_canary_changes import resolved_v8_version from v8_canary_changes import windows_source_required @@ -56,6 +57,14 @@ def test_manual_dispatch_requires_source_build(self) -> None: ) ) + def test_runtime_smoke_changes_require_canary(self) -> None: + for path in ( + ".github/scripts/smoke_macos_x86_v8.sh", + "codex-rs/v8-poc/examples/code_mode_runtime_smoke.rs", + ): + with self.subTest(path=path): + self.assertTrue(canary_required({path}, "149.2.0", "149.2.0")) + def test_changed_files_excludes_changes_made_only_on_base_branch(self) -> None: with tempfile.TemporaryDirectory() as temp_dir: root = Path(temp_dir) diff --git a/.github/scripts/v8_canary_changes.py b/.github/scripts/v8_canary_changes.py index 0acc4ec3f6bc..c29295c7c302 100644 --- a/.github/scripts/v8_canary_changes.py +++ b/.github/scripts/v8_canary_changes.py @@ -24,6 +24,7 @@ ".github/scripts/run_bazel_with_buildbuddy.py", ".github/scripts/rusty_v8_bazel.py", ".github/scripts/rusty_v8_module_bazel.py", + ".github/scripts/smoke_macos_x86_v8.sh", ".github/scripts/v8_canary_changes.py", ".github/workflows/postmerge-ci.yml", ".github/workflows/rusty-v8-release.yml", @@ -31,6 +32,7 @@ "MODULE.bazel", "MODULE.bazel.lock", "codex-rs/Cargo.toml", + "codex-rs/v8-poc/**", "patches/BUILD.bazel", "patches/llvm_*.patch", "patches/rules_cc_*.patch", diff --git a/.github/workflows/rusty-v8-release.yml b/.github/workflows/rusty-v8-release.yml index 92be3761ddbd..5fe53264a5b4 100644 --- a/.github/workflows/rusty-v8-release.yml +++ b/.github/workflows/rusty-v8-release.yml @@ -99,6 +99,7 @@ jobs: target: aarch64-unknown-linux-gnu v8_cpu: arm64 variant: ptrcomp-sandbox + # Keep the build on arm64; smoke-macos-x86 tests its staged output on Intel. - runner: macos-15-xlarge bazel_config: ci-macos platform: macos_amd64 @@ -285,6 +286,70 @@ jobs: name: rusty-v8-${{ needs.metadata.outputs.v8_version }}-${{ matrix.variant }}-${{ matrix.target }} path: dist/${{ matrix.target }}/* + smoke-macos-x86: + name: Smoke test ${{ matrix.variant }} x86_64-apple-darwin on Intel + needs: + - metadata + - build + runs-on: macos-15-large + permissions: + actions: read + contents: read + strategy: + fail-fast: false + matrix: + include: + - sandbox: false + variant: release + - sandbox: true + variant: ptrcomp-sandbox + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up Rust toolchain + uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 + with: + toolchain: "1.95.0" + + - name: Download staged artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: rusty-v8-${{ needs.metadata.outputs.v8_version }}-${{ matrix.variant }}-x86_64-apple-darwin + path: dist/x86_64-apple-darwin + + - name: Smoke test staged artifact and code-mode runtime + env: + SANDBOX: ${{ matrix.sandbox }} + TARGET: x86_64-apple-darwin + shell: bash + run: | + set -euo pipefail + + archive="$(find "dist/${TARGET}" -maxdepth 1 -type f -name 'librusty_v8_*.a.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 + + .github/scripts/smoke_macos_x86_v8.sh \ + "${GITHUB_WORKSPACE}/${archive}" \ + "${GITHUB_WORKSPACE}/${binding}" \ + "${SANDBOX}" \ + "${RUNNER_TEMP}/rusty-v8-cargo-smoke-${TARGET}-${SANDBOX}" + + - name: Upload crash reports + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: rusty-v8-${{ needs.metadata.outputs.v8_version }}-${{ matrix.variant }}-x86_64-macos-crash-reports + path: ${{ runner.temp }}/rusty-v8-cargo-smoke-x86_64-apple-darwin-${{ matrix.sandbox }}/crash-reports/* + if-no-files-found: ignore + retention-days: 7 + build-windows-source: name: Build ptrcomp-sandbox ${{ matrix.target }} from source needs: metadata @@ -429,6 +494,7 @@ jobs: - metadata - build - build-windows-source + - smoke-macos-x86 runs-on: ubuntu-latest permissions: contents: write diff --git a/.github/workflows/v8-canary.yml b/.github/workflows/v8-canary.yml index 53d9585108e8..d5f74dbe09d2 100644 --- a/.github/workflows/v8-canary.yml +++ b/.github/workflows/v8-canary.yml @@ -119,6 +119,7 @@ jobs: target: aarch64-unknown-linux-gnu v8_cpu: arm64 variant: ptrcomp-sandbox + # Keep the build on arm64; smoke-macos-x86 tests its staged output on Intel. - runner: macos-15-xlarge bazel_config: ci-macos platform: macos_amd64 @@ -306,6 +307,76 @@ jobs: if: always() && !cancelled() uses: ./.github/actions/check-clean-worktree + smoke-macos-x86: + name: Smoke test ${{ matrix.variant }} x86_64-apple-darwin on Intel + needs: + - metadata + - build + if: ${{ needs.metadata.outputs.canary_required == 'true' }} + runs-on: macos-15-large + permissions: + actions: read + contents: read + strategy: + fail-fast: false + matrix: + include: + - sandbox: false + variant: release + - sandbox: true + variant: ptrcomp-sandbox + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + persist-credentials: false + + - name: Set up Rust toolchain + uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 + with: + toolchain: "1.95.0" + + - name: Download staged artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: v8-canary-${{ needs.metadata.outputs.v8_version }}-${{ matrix.variant }}-x86_64-apple-darwin + path: dist/x86_64-apple-darwin + + - name: Smoke test staged artifact and code-mode runtime + env: + SANDBOX: ${{ matrix.sandbox }} + TARGET: x86_64-apple-darwin + shell: bash + run: | + set -euo pipefail + + archive="$(find "dist/${TARGET}" -maxdepth 1 -type f -name 'librusty_v8_*.a.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 + + .github/scripts/smoke_macos_x86_v8.sh \ + "${GITHUB_WORKSPACE}/${archive}" \ + "${GITHUB_WORKSPACE}/${binding}" \ + "${SANDBOX}" \ + "${RUNNER_TEMP}/rusty-v8-cargo-smoke-${TARGET}-${SANDBOX}" + + - name: Upload crash reports + if: always() + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: v8-canary-${{ needs.metadata.outputs.v8_version }}-${{ matrix.variant }}-x86_64-macos-crash-reports + path: ${{ runner.temp }}/rusty-v8-cargo-smoke-x86_64-apple-darwin-${{ matrix.sandbox }}/crash-reports/* + if-no-files-found: ignore + retention-days: 7 + + - name: Check for a clean worktree + if: always() && !cancelled() + uses: ./.github/actions/check-clean-worktree + build-windows-source: name: Build ptrcomp-sandbox ${{ matrix.target }} from source needs: metadata diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 88601e938407..2e8b6a521fc1 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -4427,7 +4427,11 @@ dependencies = [ name = "codex-v8-poc" version = "0.0.0" dependencies = [ + "codex-code-mode", + "codex-utils-rustls-provider", "pretty_assertions", + "rustls", + "tokio", "v8", ] diff --git a/codex-rs/v8-poc/Cargo.toml b/codex-rs/v8-poc/Cargo.toml index 9615ab977f9d..8f0e2438dad3 100644 --- a/codex-rs/v8-poc/Cargo.toml +++ b/codex-rs/v8-poc/Cargo.toml @@ -18,5 +18,11 @@ workspace = true [dependencies] v8 = { workspace = true } +[target.'cfg(all(target_os = "macos", target_arch = "x86_64"))'.dependencies] +codex-code-mode = { workspace = true } +codex-utils-rustls-provider = { workspace = true } +rustls = { workspace = true, features = ["ring"] } +tokio = { workspace = true, features = ["macros", "rt"] } + [dev-dependencies] pretty_assertions = { workspace = true } diff --git a/codex-rs/v8-poc/examples/code_mode_runtime_smoke.rs b/codex-rs/v8-poc/examples/code_mode_runtime_smoke.rs new file mode 100644 index 000000000000..3c0444a1d405 --- /dev/null +++ b/codex-rs/v8-poc/examples/code_mode_runtime_smoke.rs @@ -0,0 +1,111 @@ +#[cfg(all(target_os = "macos", target_arch = "x86_64"))] +use codex_code_mode::CellId; +#[cfg(all(target_os = "macos", target_arch = "x86_64"))] +use codex_code_mode::ExecuteRequest; +#[cfg(all(target_os = "macos", target_arch = "x86_64"))] +use codex_code_mode::ExecuteToPendingOutcome; +#[cfg(all(target_os = "macos", target_arch = "x86_64"))] +use codex_code_mode::FunctionCallOutputContentItem; +#[cfg(all(target_os = "macos", target_arch = "x86_64"))] +use codex_code_mode::InProcessCodeModeSession; +#[cfg(all(target_os = "macos", target_arch = "x86_64"))] +use codex_code_mode::RuntimeResponse; + +#[cfg(all(target_os = "macos", target_arch = "x86_64"))] +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<(), String> { + let provider = parse_provider()?; + install_provider(provider)?; + exercise_provider(provider)?; + + let session = InProcessCodeModeSession::new(); + let outcome = session + .execute_to_pending(ExecuteRequest { + tool_call_id: "ci_runtime_smoke".to_string(), + enabled_tools: Vec::new(), + source: r#"text("runtime smoke ok");"#.to_string(), + yield_time_ms: Some(60_000), + max_output_tokens: Some(1_000), + }) + .await?; + session.shutdown().await?; + + let expected = ExecuteToPendingOutcome::Completed(RuntimeResponse::Result { + cell_id: CellId::new("1".to_string()), + content_items: vec![FunctionCallOutputContentItem::InputText { + text: "runtime smoke ok".to_string(), + }], + error_text: None, + }); + if outcome != expected { + return Err(format!("unexpected code-mode response: {outcome:?}")); + } + + println!("code-mode runtime smoke passed with {provider}"); + Ok(()) +} + +#[cfg(all(target_os = "macos", target_arch = "x86_64"))] +#[derive(Clone, Copy)] +enum Provider { + AwsLc, + Ring, +} + +#[cfg(all(target_os = "macos", target_arch = "x86_64"))] +impl std::fmt::Display for Provider { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::AwsLc => formatter.write_str("aws-lc"), + Self::Ring => formatter.write_str("ring"), + } + } +} + +#[cfg(all(target_os = "macos", target_arch = "x86_64"))] +fn parse_provider() -> Result { + let mut args = std::env::args().skip(1); + let provider = match args.next().as_deref() { + Some("aws-lc") => Provider::AwsLc, + Some("ring") => Provider::Ring, + Some(provider) => return Err(format!("unsupported rustls provider: {provider}")), + None => return Err("expected rustls provider argument: aws-lc or ring".to_string()), + }; + if args.next().is_some() { + return Err("expected exactly one rustls provider argument".to_string()); + } + Ok(provider) +} + +#[cfg(all(target_os = "macos", target_arch = "x86_64"))] +fn install_provider(provider: Provider) -> Result<(), String> { + match provider { + Provider::AwsLc => { + codex_utils_rustls_provider::ensure_rustls_crypto_provider(); + Ok(()) + } + Provider::Ring => rustls::crypto::ring::default_provider() + .install_default() + .map_err(|_| "failed to install ring rustls provider".to_string()), + } +} + +#[cfg(all(target_os = "macos", target_arch = "x86_64"))] +fn exercise_provider(provider: Provider) -> Result<(), String> { + let installed = rustls::crypto::CryptoProvider::get_default() + .ok_or_else(|| format!("{provider} rustls provider was not installed"))?; + let key_exchange_group = installed + .kx_groups + .first() + .ok_or_else(|| format!("{provider} rustls provider has no key exchange groups"))?; + key_exchange_group + .start() + .map(|_| ()) + .map_err(|error| format!("{provider} key exchange initialization failed: {error}")) +} + +#[cfg(not(all(target_os = "macos", target_arch = "x86_64")))] +fn main() { + eprintln!("code-mode runtime smoke is only supported on Intel macOS"); + std::process::exit(2); +}