SaylorTwift HF Staff commited on
Commit
2ee40b0
·
verified ·
1 Parent(s): aada7ee

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .github/actions/publish-dotslash/action.yml +52 -0
  2. .github/actions/publish-dotslash/requirements.txt +111 -0
  3. .github/actions/run-argument-comment-lint/action.yml +53 -0
  4. .github/actions/setup-akv-pkcs11-codesigning/action.yaml +349 -0
  5. .github/actions/setup-ci/action.yml +107 -0
  6. .github/actions/setup-msvc-env/action.yml +20 -0
  7. .github/actions/setup-msvc-env/setup-msvc-env.ps1 +257 -0
  8. .github/actions/setup-rusty-v8/action.yml +64 -0
  9. .github/actions/windows-code-sign/action.yml +73 -0
  10. .github/codex/labels/codex-triage.md +7 -0
  11. .github/scripts/macos-signing/codex-app-server.entitlements.plist +10 -0
  12. .github/scripts/macos-signing/codex-code-mode-host.entitlements.plist +10 -0
  13. .github/scripts/macos-signing/codex-responses-api-proxy.entitlements.plist +8 -0
  14. .github/scripts/macos-signing/codex-voice-host.entitlements.plist +8 -0
  15. .github/scripts/macos-signing/codex.entitlements.plist +10 -0
  16. .github/scripts/macos-signing/notarize_macos_binary_with_akv.sh +110 -0
  17. .github/scripts/macos-signing/notarize_macos_dmg_with_akv.sh +100 -0
  18. .github/scripts/macos-signing/notarize_with_akv.py +519 -0
  19. .github/scripts/macos-signing/provisioned_macos_cli_package.py +229 -0
  20. .github/scripts/macos-signing/sign_macos_cli_package.py +177 -0
  21. .github/scripts/macos-signing/sign_macos_code.sh +245 -0
  22. .github/scripts/macos-signing/test_notarize_with_akv.py +257 -0
  23. .github/scripts/macos-signing/test_provisioned_macos_cli_package.py +217 -0
  24. .github/scripts/macos-signing/test_sign_macos_cli_package.py +293 -0
  25. docs/CLA.md +49 -0
  26. docs/agents_md.md +3 -0
  27. docs/authentication.md +3 -0
  28. docs/config.md +15 -0
  29. docs/contributing.md +39 -0
  30. docs/example-config.md +3 -0
  31. docs/exec.md +3 -0
  32. docs/execpolicy.md +3 -0
  33. docs/getting-started.md +3 -0
  34. docs/install.md +65 -0
  35. docs/license.md +3 -0
  36. docs/open-source-fund.md +8 -0
  37. docs/sandbox.md +3 -0
  38. docs/skills.md +3 -0
  39. docs/slash_commands.md +3 -0
  40. patches/BUILD.bazel +32 -0
  41. patches/abseil_windows_gnullvm_thread_identity.patch +16 -0
  42. patches/bzip2_windows_stack_args.patch +23 -0
  43. patches/llvm_rusty_v8_custom_libcxx.patch +108 -0
  44. patches/llvm_windows_arm64_powl.patch +25 -0
  45. patches/llvm_windows_mingw_compat.patch +50 -0
  46. patches/ring_windows_msvc_include_dirs.patch +112 -0
  47. patches/rules_cc_rusty_v8_custom_libcxx.patch +44 -0
  48. patches/rules_foreign_cc_make_cppflags.patch +15 -0
  49. patches/rules_foreign_cc_make_msvc_stdint.patch +13 -0
  50. patches/rules_foreign_cc_make_xcompile.patch +10 -0
.github/actions/publish-dotslash/action.yml ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Publish DotSlash manifests
2
+ description: Run the pinned upstream publisher directly on an Ubuntu runner.
3
+
4
+ inputs:
5
+ tag:
6
+ description: Release tag to publish manifests for.
7
+ required: true
8
+ configs:
9
+ description: Newline-separated paths to DotSlash configuration files.
10
+ required: true
11
+
12
+ runs:
13
+ using: composite
14
+ steps:
15
+ - name: Checkout DotSlash publisher
16
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
17
+ with:
18
+ repository: facebook/dotslash-publish-release
19
+ ref: 9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2
20
+ path: .dotslash-publish-release
21
+ persist-credentials: false
22
+
23
+ - name: Install uv
24
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
25
+ with:
26
+ version: "0.12.13"
27
+ checksum: "745765a3b6e360ad76743599ae5c42e9278c7edf8bbff9fc76d05bf2623a04dd"
28
+ github-token: ""
29
+ enable-cache: false
30
+
31
+ - name: Publish DotSlash manifests
32
+ shell: bash
33
+ env:
34
+ ACTION_PATH: ${{ github.action_path }}
35
+ RELEASE_TAG: ${{ inputs.tag }}
36
+ DOTSLASH_CONFIGS: ${{ inputs.configs }}
37
+ run: |
38
+ set -euo pipefail
39
+
40
+ # Ubuntu runners already provide Python and gh. Avoid rebuilding the
41
+ # upstream Docker image and downloading its apt dependencies per release.
42
+ uv venv --python "$(command -v python3)" "${RUNNER_TEMP}/dotslash-publish-release-venv"
43
+ python="${RUNNER_TEMP}/dotslash-publish-release-venv/bin/python"
44
+ uv pip install --python "$python" \
45
+ --require-hashes --only-binary=:all: -r "${ACTION_PATH}/requirements.txt"
46
+
47
+ while IFS= read -r config; do
48
+ if [[ -n "$config" ]]; then
49
+ "$python" .dotslash-publish-release/process_config.py \
50
+ --config "$config" --tag "$RELEASE_TAG" --upload
51
+ fi
52
+ done <<< "$DOTSLASH_CONFIGS"
.github/actions/publish-dotslash/requirements.txt ADDED
@@ -0,0 +1,111 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Regenerate: uv pip compile --generate-hashes --no-header --no-annotate requirements.txt
2
+ blake3==1.0.8 \
3
+ --hash=sha256:003f61e8c41dd9931edddf1cc6a1bb680fb2ac0ad15493ef4a1df9adc59ce9df \
4
+ --hash=sha256:0171e85d56dec1219abdae5f49a0ed12cb3f86a454c29160a64fd8a8166bba37 \
5
+ --hash=sha256:04b95bcd6c1b61480923adc571ffab377618d170f528f6cf4f6a6fff94e3c309 \
6
+ --hash=sha256:0c492630d384fc4400e9860b16e09e0dbe79e960e95b237b9a1ea3f95ea3125d \
7
+ --hash=sha256:0eab3318ec02f8e16fe549244791ace2ada2c259332f0c77ab22cf94dfff7130 \
8
+ --hash=sha256:119829e13eca177f5626c43f753acc4813eb8ae414c2d70ef7571078177163c7 \
9
+ --hash=sha256:1257be19f2d381c868a34cc822fc7f12f817ddc49681b6d1a2790bfbda1a9865 \
10
+ --hash=sha256:19fc6f2b7edab8acff6895fc6e38c19bd79f4c089e21153020c75dfc7397d52d \
11
+ --hash=sha256:1af0e5a29aa56d4fba904452ae784740997440afd477a15e583c38338e641f41 \
12
+ --hash=sha256:23e181318f5a62e4e6ce7be0875b4f65543abe47fe48cc4f4e9ab8f2bf9e01c5 \
13
+ --hash=sha256:269f255b110840e52b6ce9db02217e39660ebad3e34ddd5bca8b8d378a77e4e1 \
14
+ --hash=sha256:2bacf9757e93ac376c612dc0d45931a591ea0caf5583ab93d99dd32a2905ec43 \
15
+ --hash=sha256:2c33dac2c6112bc23f961a7ca305c7e34702c8177040eb98d0389d13a347b9e1 \
16
+ --hash=sha256:2d78f06f3fb838b34c330e2987090376145cbe5944d8608a0c4779c779618f7b \
17
+ --hash=sha256:3168c457255b5d2a2fc356ba696996fcaff5d38284f968210d54376312107662 \
18
+ --hash=sha256:38209b10482c97e151681ea3e91cc7141f56adbbf4820a7d701a923124b41e6a \
19
+ --hash=sha256:3b5573b052777142b2cecc453d022c3f21aa4aba75011258410bb98f41c1a727 \
20
+ --hash=sha256:3bafdec95dfffa3f6571e529644744e280337df15ddd9728f224ba70c5779b23 \
21
+ --hash=sha256:3cec94ed5676821cf371e9c9d25a41b4f3ebdb5724719b31b2749653b7cc1dfa \
22
+ --hash=sha256:3fb83532f7456ddeb68dae1b36e1f7c52f9cb72852ac01159bbcb1a12b0f8be0 \
23
+ --hash=sha256:411f61cb817eb25547b91944301ccfa624ccdc69d70822866d8ad0b0e9e3ca64 \
24
+ --hash=sha256:421b99cdf1ff2d1bf703bc56c454f4b286fce68454dd8711abbcb5a0df90c19a \
25
+ --hash=sha256:444215a1e5201f8fa4e5c7352e938a7070cd33d66aeb1dd9b1103a64b6920f9e \
26
+ --hash=sha256:44c2815d4616fad7e2d757d121c0a11780f70ffc817547b3059b5c7e224031a7 \
27
+ --hash=sha256:46dc20976bd6c235959ef0246ec73420d1063c3da2839a9c87ca395cf1fd7943 \
28
+ --hash=sha256:497ef8096ec4ac1ffba9a66152cee3992337cebf8ea434331d8fd9ce5423d227 \
29
+ --hash=sha256:4b0543c57eb9d6dac9d4bced63e9f7f7b546886ac04cec8da3c3d9c8f30cbbb7 \
30
+ --hash=sha256:4bacaae75e98dee3b7da6c5ee3b81ee21a3352dd2477d6f1d1dbfd38cdbf158a \
31
+ --hash=sha256:4e02a3c499e35bf51fc15b2738aca1a76410804c877bcd914752cac4f71f052a \
32
+ --hash=sha256:4f54cff7f15d91dc78a63a2dd02a3dccdc932946f271e2adb4130e0b4cf608ba \
33
+ --hash=sha256:504d1399b7fb91dfe5c25722d2807990493185faa1917456455480c36867adb5 \
34
+ --hash=sha256:506a47897a11ebe8f3cdeb52f1365d6a2f83959e98ccb0c830f8f73277d4d358 \
35
+ --hash=sha256:511133bab85ff60ed143424ce484d08c60894ff7323f685d7a6095f43f0c85c3 \
36
+ --hash=sha256:513cc7f0f5a7c035812604c2c852a0c1468311345573de647e310aca4ab165ba \
37
+ --hash=sha256:53304b6df3f8d826850c1d977d9a69edbf3e48476b95f7cb6175ec650856a3c5 \
38
+ --hash=sha256:58ce8d45a5bb5326482de72ea1969a378634236186a970fef63058a5b7b8b435 \
39
+ --hash=sha256:5eb25bca3cee2e0dd746a214784fb36be6a43640c01c55b6b4e26196e72d076c \
40
+ --hash=sha256:63379be58438878eeb76ebe4f0efbeaabf42b79f2cff23b6126b7991588ced67 \
41
+ --hash=sha256:66ca28a673025c40db3eba21a9cac52f559f83637efa675b3f6bd8683f0415f3 \
42
+ --hash=sha256:67d9c42c42eb1c7aedcf901591c743266009fcf48babf6d6f8450f567cb94a84 \
43
+ --hash=sha256:67f1bc11bf59464ef092488c707b13dd4e872db36e25c453dfb6e0c7498df9f1 \
44
+ --hash=sha256:6a6eafc29e4f478d365a87d2f25782a521870c8514bb43734ac85ae9be71caf7 \
45
+ --hash=sha256:6ae7754c7d96e92a70a52e07c732d594cf9924d780f49fffd3a1e9235e0f5ba7 \
46
+ --hash=sha256:725c52c4d393c7bd1a10682df322d480734002a1389b320366c660568708846b \
47
+ --hash=sha256:73e0b4fa25f6e3078526a592fb38fca85ef204fd02eced6731e1cdd9396552d4 \
48
+ --hash=sha256:78731ce7fca46f776ae45fb5271a2a76c4a92c9687dd4337e84b2ae9a174b28f \
49
+ --hash=sha256:7adbbee5dd0c302218eb8acdfd82b7006930eb5798f56f79f9cca89f6f192662 \
50
+ --hash=sha256:7fcfe81b3ae3fb5d2e88be0d3259603ff95f0d5ed69f655c28fdaef31e49a470 \
51
+ --hash=sha256:83347ef28d774e84442c71fc5aa8817498753d47831a533536ead1a7ccd44a41 \
52
+ --hash=sha256:83605dbf43f581d8b7175b7f3bfe5388bad5a7c6ac175c9c11d669da31133f4b \
53
+ --hash=sha256:859cd57bac097a2cd63cb36d64c2f6f16c9edece5590f929e70157478e46dc9e \
54
+ --hash=sha256:88d527c247f9609dc1d45a08fd243e39f0d5300d54c57e048de24d4fa9240ebb \
55
+ --hash=sha256:8956bb9aec47b6c37ccce935a943588f1f5e6e2e85d43bb7cb76a574238f8a9b \
56
+ --hash=sha256:8f2ef8527a7a8afd99b16997d015851ccc0fe2a409082cebb980af2554e5c74c \
57
+ --hash=sha256:9456c829601d72852d8ba0af8dae0610f7def1d59f5942efde1e2ef93e8a8b57 \
58
+ --hash=sha256:94b2d4922c75174083988b8b0d2c78e72be7376ac59ab19c3e5d17a7c9c1cc0f \
59
+ --hash=sha256:97c076d58ee37eb5b2d8d91bb9db59c5a008fd59c71845dc57fe438aeeabaf10 \
60
+ --hash=sha256:98915260c378d4c45088f1e227b053834ed95aeca89336184f192489ad0dd5d4 \
61
+ --hash=sha256:9a8946cb6b1d2b2096daaaa89856f39887bce2b78503fa31b78173e3a86fa281 \
62
+ --hash=sha256:9ab5998e2abd9754819753bc2f1cf3edf82d95402bff46aeef45ed392a5468bf \
63
+ --hash=sha256:9c9fbdacfdeb68f7ca53bb5a7a5a593ec996eaf21155ad5b08d35e6f97e60877 \
64
+ --hash=sha256:9e1d70bf76c02846d0868a3d413eb6c430b76a315e12f1b2e59b5cf56c1f62a3 \
65
+ --hash=sha256:a2336d5b2a801a7256da21150348f41610a6c21dae885a3acb1ebbd7333d88d8 \
66
+ --hash=sha256:a25db3d36b55f5ed6a86470155cc749fc9c5b91c949b8d14f48658f9d960d9ec \
67
+ --hash=sha256:a33b9a1fb6d1d559a8e0d04b041e99419a6bb771311c774f6ff57ed7119c70ed \
68
+ --hash=sha256:a47939f04b89c5c6ff1e51e883e5efab1ea1bf01a02f4d208d216dddd63d0dd8 \
69
+ --hash=sha256:a585357d5d8774aad9ffc12435de457f9e35cde55e0dc8bc43ab590a6929e59f \
70
+ --hash=sha256:a5b9196a4768e5475475bdf2010dc76b2792f95805731a645196d180a7e12570 \
71
+ --hash=sha256:ab4e1dea4fa857944944db78e8f20d99ee2e16b2dea5a14f514fb0607753ac83 \
72
+ --hash=sha256:adccc3a139207e02bb7d7bb0715fe0b87069685aad5f3afff820b2f829467904 \
73
+ --hash=sha256:add39ffa9e5074264f12ab409cdf059bc967817644b455e3fad46139001cc108 \
74
+ --hash=sha256:af18817da1eb5bd65d068fb9c18235edc328a45b09025346fa65fe52b1b0c9b6 \
75
+ --hash=sha256:af394b50c6aa0b1b957a99453d1ee440ef67cd2d1b5669c731647dc723de8a3a \
76
+ --hash=sha256:b201961c6dcc7123e69fff6019298d9e4394c7e585a300e999664bca8070d83c \
77
+ --hash=sha256:b37f63c5952b32b5064096141b394ed17569d318637a06a2cae9bfedd852f0ef \
78
+ --hash=sha256:b4d672c24dc15ec617d212a338a4ca14b449829b6072d09c96c63b6e6b621aed \
79
+ --hash=sha256:bcb04966537777af56c1f399b35525aa70a1225816e121ff95071c33c0f7abca \
80
+ --hash=sha256:c2682ba10fb3b36fe8a6823be50910f1145a8a5158bc555145fadcc4afb984c3 \
81
+ --hash=sha256:c445eff665d21c3b3b44f864f849a2225b1164c08654beb23224a02f087b7ff1 \
82
+ --hash=sha256:c5bfe1a1ada3ed2c715f692ce1a3b221d7f6b4d5e6001d24774ef07de013192c \
83
+ --hash=sha256:c63ece266a43014cf29e772a82857cd8e90315ae3ed53e3c5204851596edd5f2 \
84
+ --hash=sha256:c65e373c8b47174b969ee61a89ee56922f722972eb650192845c8546df8d9db9 \
85
+ --hash=sha256:c7780666dc6be809b49442d6d5ce06fdbe33024a87560b58471103ec17644682 \
86
+ --hash=sha256:c84af132aa09abeadf9a0118c8fb26f4528f3f42c10ef8be0fcf31c478774ec4 \
87
+ --hash=sha256:c9ab5ef61aaa6df2f769548a071fddcacbb0f56102a9c48f40878b10ae1a2657 \
88
+ --hash=sha256:ca8bc7e816370118262da11783e1fdf8992f250789ae08e8182fb7c26e14ded0 \
89
+ --hash=sha256:d17eb6382634b3a5bc0c0e0454d5265b0becaeeadb6801ed25150b39a999d0cc \
90
+ --hash=sha256:d2c3151955efb09ba58cd3e1263521e15e9e3866a40d6bd3556d86fc968e8f95 \
91
+ --hash=sha256:d3fe26f145fcb82931d1820b55c0279f72f8f8e49450dd9d74efbfd409b28423 \
92
+ --hash=sha256:d8da4233984d51471bd4e4366feda1d90d781e712e0a504ea54b1f2b3577557b \
93
+ --hash=sha256:db54946792d2b8c6fa4be73e6e334519f13c1b52e7ff346b3e2ec8ad3eb59401 \
94
+ --hash=sha256:dd03ff08d1b6e4fdda1cd03826f971ae8966ef6f683a8c68aa27fb21904b5aa9 \
95
+ --hash=sha256:ddfc59b0176fb31168f08d5dd536e69b1f4f13b5a0f4b0c3be1003efd47f9308 \
96
+ --hash=sha256:e0fee93d5adcd44378b008c147e84f181f23715307a64f7b3db432394bbfce8b \
97
+ --hash=sha256:e171b169cb7ea618e362a4dddb7a4d4c173bbc08b9ba41ea3086dd1265530d4f \
98
+ --hash=sha256:e2df12f295f95a804338bd300e8fad4a6f54fd49bd4d9c5893855a230b5188a8 \
99
+ --hash=sha256:e4072196547484c95a5a09adbb952e9bb501949f03f9e2a85e7249ef85faaba8 \
100
+ --hash=sha256:e5122a61b3b004bbbd979bdf83a3aaab432da3e2a842d7ddf1c273f2503b4884 \
101
+ --hash=sha256:e5b5da177d62cc4b7edf0cea08fe4dec960c9ac27f916131efa890a01f747b93 \
102
+ --hash=sha256:e6cb2316949c614e64f6b7796b9eca28b1b57406d27debc99398a746e9b426c7 \
103
+ --hash=sha256:e7e12a777f6b798eb8d06f875d6e108e3008bd658d274d8c676dcf98e0f10537 \
104
+ --hash=sha256:e8ae3689f0c7bfa6ce6ae45cab110e4c3442125c4c23b28f1f097856de26e4d1 \
105
+ --hash=sha256:ed972ebd553c0c25363459e9fc71a38c045d8419e365b59acd8cd791eff13981 \
106
+ --hash=sha256:ef153c5860d5bf1cc71aece69b28097d2a392913eb323d6b52555c875d0439fc \
107
+ --hash=sha256:efb1ab89fa47b5acb36f1530c4eb83ae20aaec9ac17808ff014b2650fbc3c7ba \
108
+ --hash=sha256:f13c25c215c284adca0c07cffae1518cafcac3900f1d45fff51c7e6ab98efec1 \
109
+ --hash=sha256:fc9fc4a0a1a13991b23a589fb6fbb8290bd94a72c130e69e27c9769612516424 \
110
+ --hash=sha256:fe1b02ab49bfd969ef50b9f17482a2011c77536654af21807ba5c2674e0bb2a0 \
111
+ --hash=sha256:fe61d52da1d102aabb0e315d3fc613c11c103376aa0a8510aeee159939612f76
.github/actions/run-argument-comment-lint/action.yml ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Run argument comment lint
2
+ description: Run argument-comment-lint on codex-rs via Bazel.
3
+
4
+ inputs:
5
+ target:
6
+ description: Runner target passed to setup-bazel-ci.
7
+ required: true
8
+ buildbuddy-api-key:
9
+ description: BuildBuddy API key used by Bazel CI.
10
+ required: false
11
+ default: ""
12
+
13
+ runs:
14
+ using: composite
15
+ steps:
16
+ - uses: ./.github/actions/setup-bazel-ci
17
+ with:
18
+ target: ${{ inputs.target }}
19
+
20
+ - name: Install Linux sandbox build dependencies
21
+ if: ${{ runner.os == 'Linux' }}
22
+ shell: bash
23
+ run: |
24
+ sudo DEBIAN_FRONTEND=noninteractive apt-get update
25
+ sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
26
+
27
+ - name: Run argument comment lint on codex-rs via Bazel
28
+ if: ${{ runner.os != 'Windows' }}
29
+ env:
30
+ BUILDBUDDY_API_KEY: ${{ inputs.buildbuddy-api-key }}
31
+ shell: bash
32
+ run: |
33
+ bazel_targets="$(./tools/argument-comment-lint/list-bazel-targets.sh)"
34
+ ./.github/scripts/run-bazel-ci.sh \
35
+ -- \
36
+ build \
37
+ --config=argument-comment-lint \
38
+ --keep_going \
39
+ --build_metadata=COMMIT_SHA=${GITHUB_SHA} \
40
+ -- \
41
+ ${bazel_targets}
42
+
43
+ - name: Run argument comment lint on codex-rs via Bazel
44
+ if: ${{ runner.os == 'Windows' }}
45
+ env:
46
+ BUILDBUDDY_API_KEY: ${{ inputs.buildbuddy-api-key }}
47
+ shell: bash
48
+ run: |
49
+ ./.github/scripts/run-argument-comment-lint-bazel.sh \
50
+ --config=argument-comment-lint \
51
+ --platforms=//:local_windows \
52
+ --keep_going \
53
+ --build_metadata=COMMIT_SHA=${GITHUB_SHA}
.github/actions/setup-akv-pkcs11-codesigning/action.yaml ADDED
@@ -0,0 +1,349 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Set up AKV PKCS11 code signing
2
+ description: Download prebuilt rcodesign and Azure Key Vault PKCS11 provider artifacts, then export macOS signing environment.
3
+
4
+ inputs:
5
+ setup-mode:
6
+ description: signing configures Azure and exports signing env vars; tools-only only downloads signing tools.
7
+ required: false
8
+ default: signing
9
+ rcodesign-blob-uri:
10
+ description: Azure Blob URI for the prebuilt Linux/amd64 rcodesign binary.
11
+ required: true
12
+ rcodesign-sha256:
13
+ description: Expected SHA-256 digest for the prebuilt rcodesign binary.
14
+ required: true
15
+ akv-pkcs11-library-blob-uri:
16
+ description: Azure Blob URI for the prebuilt Linux/amd64 AKV PKCS11 provider library.
17
+ required: true
18
+ akv-pkcs11-library-sha256:
19
+ description: Expected SHA-256 digest for the prebuilt AKV PKCS11 provider library.
20
+ required: true
21
+ azure-client-id:
22
+ description: GitHub OIDC client ID for the Azure signer application.
23
+ required: true
24
+ azure-tenant-id:
25
+ description: Azure tenant ID for the signer application.
26
+ required: true
27
+ azure-subscription-id:
28
+ description: Azure subscription ID that owns the signing vault.
29
+ required: true
30
+ key-vault-name:
31
+ description: Azure Key Vault name containing the certificate-backed signing key.
32
+ required: true
33
+ key-name:
34
+ description: Key Vault certificate/key name used as the PKCS11 key label.
35
+ required: true
36
+ key-version:
37
+ description: Optional Key Vault key version to pin while signing.
38
+ required: false
39
+ default: ""
40
+ certificate-sha256:
41
+ description: Optional expected SHA-256 fingerprint for the downloaded public certificate.
42
+ required: false
43
+ default: ""
44
+
45
+ outputs:
46
+ pkcs11-library:
47
+ description: Path to the downloaded AKV PKCS11 provider library.
48
+ value: ${{ steps.paths.outputs.pkcs11_library }}
49
+ signing-certificate-pem:
50
+ description: Path to the downloaded public signing certificate.
51
+ value: ${{ steps.paths.outputs.signing_certificate_pem }}
52
+ rcodesign:
53
+ description: Path to the downloaded rcodesign binary.
54
+ value: ${{ steps.paths.outputs.rcodesign }}
55
+
56
+ runs:
57
+ using: composite
58
+ steps:
59
+ - name: Validate pinned signing artifacts
60
+ shell: bash
61
+ env:
62
+ SETUP_MODE: ${{ inputs.setup-mode }}
63
+ RCODESIGN_BLOB_URI: ${{ inputs.rcodesign-blob-uri }}
64
+ RCODESIGN_SHA256: ${{ inputs.rcodesign-sha256 }}
65
+ AKV_PKCS11_LIBRARY_BLOB_URI: ${{ inputs.akv-pkcs11-library-blob-uri }}
66
+ AKV_PKCS11_LIBRARY_SHA256: ${{ inputs.akv-pkcs11-library-sha256 }}
67
+ KEY_VAULT_NAME: ${{ inputs.key-vault-name }}
68
+ KEY_NAME: ${{ inputs.key-name }}
69
+ run: |
70
+ set -euo pipefail
71
+
72
+ case "$SETUP_MODE" in
73
+ signing|tools-only)
74
+ ;;
75
+ *)
76
+ echo "setup-mode must be 'signing' or 'tools-only', got '$SETUP_MODE'." >&2
77
+ exit 1
78
+ ;;
79
+ esac
80
+
81
+ for variable_name in RCODESIGN_SHA256 AKV_PKCS11_LIBRARY_SHA256; do
82
+ value="${!variable_name}"
83
+ if [[ ! "$value" =~ ^[0-9a-f]{64}$ ]]; then
84
+ echo "$variable_name must be a lowercase SHA-256 digest." >&2
85
+ exit 1
86
+ fi
87
+ done
88
+
89
+ for variable_name in RCODESIGN_BLOB_URI AKV_PKCS11_LIBRARY_BLOB_URI; do
90
+ value="${!variable_name}"
91
+ if [[ ! "$value" =~ ^az://[^/]+/[^/]+/.+ ]]; then
92
+ echo "$variable_name must use az://<account>/<container>/<blob>." >&2
93
+ exit 1
94
+ fi
95
+ done
96
+
97
+ if [[ "$SETUP_MODE" == "signing" ]]; then
98
+ for variable_name in \
99
+ KEY_VAULT_NAME \
100
+ KEY_NAME; do
101
+ if [[ -z "${!variable_name}" ]]; then
102
+ echo "$variable_name is required for AKV PKCS11 signing." >&2
103
+ exit 1
104
+ fi
105
+ done
106
+ fi
107
+
108
+ - name: Resolve signing tool paths
109
+ id: paths
110
+ shell: bash
111
+ run: |
112
+ set -euo pipefail
113
+
114
+ if [[ "${RUNNER_OS}" != "Linux" ]]; then
115
+ echo "Prebuilt AKV PKCS11 signing tools are only vendored for Linux runners, got ${RUNNER_OS}." >&2
116
+ exit 1
117
+ fi
118
+
119
+ if [[ "${RUNNER_ARCH}" != "X64" && "${RUNNER_ARCH}" != "AMD64" ]]; then
120
+ echo "Prebuilt AKV PKCS11 signing tools are only vendored for amd64 runners, got ${RUNNER_ARCH}." >&2
121
+ exit 1
122
+ fi
123
+
124
+ provider_root="${RUNNER_TEMP}/akv-pkcs11-provider"
125
+ rcodesign_root="${RUNNER_TEMP}/rcodesign-root"
126
+ signing_certificate_pem="${RUNNER_TEMP}/akv-signing-cert.pem"
127
+ library_name="libakv_pkcs_11.so"
128
+
129
+ mkdir -p "$provider_root" "$rcodesign_root/bin"
130
+
131
+ {
132
+ echo "pkcs11_library=$provider_root/$library_name"
133
+ echo "pkcs11_manifest=$provider_root/akv-pkcs11-provider.manifest"
134
+ echo "rcodesign_root=$rcodesign_root"
135
+ echo "rcodesign=$rcodesign_root/bin/rcodesign"
136
+ echo "signing_certificate_pem=$signing_certificate_pem"
137
+ } >> "$GITHUB_OUTPUT"
138
+
139
+ - name: Validate Azure credentials for private signing artifacts
140
+ shell: bash
141
+ env:
142
+ AZURE_CLIENT_ID: ${{ inputs.azure-client-id }}
143
+ AZURE_TENANT_ID: ${{ inputs.azure-tenant-id }}
144
+ AZURE_SUBSCRIPTION_ID: ${{ inputs.azure-subscription-id }}
145
+ run: |
146
+ set -euo pipefail
147
+
148
+ for variable_name in AZURE_CLIENT_ID AZURE_TENANT_ID AZURE_SUBSCRIPTION_ID; do
149
+ if [[ -z "${!variable_name}" ]]; then
150
+ echo "$variable_name is required for private AKV PKCS11 signing artifacts." >&2
151
+ exit 1
152
+ fi
153
+ done
154
+
155
+ - name: Log in to Azure with GitHub OIDC
156
+ uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
157
+ with:
158
+ client-id: ${{ inputs.azure-client-id }}
159
+ tenant-id: ${{ inputs.azure-tenant-id }}
160
+ subscription-id: ${{ inputs.azure-subscription-id }}
161
+
162
+ - name: Install prebuilt signing tools
163
+ shell: bash
164
+ env:
165
+ RCODESIGN_BLOB_URI: ${{ inputs.rcodesign-blob-uri }}
166
+ RCODESIGN_SHA256: ${{ inputs.rcodesign-sha256 }}
167
+ RCODESIGN: ${{ steps.paths.outputs.rcodesign }}
168
+ AKV_PKCS11_LIBRARY_BLOB_URI: ${{ inputs.akv-pkcs11-library-blob-uri }}
169
+ AKV_PKCS11_LIBRARY_SHA256: ${{ inputs.akv-pkcs11-library-sha256 }}
170
+ PKCS11_LIBRARY: ${{ steps.paths.outputs.pkcs11_library }}
171
+ PKCS11_MANIFEST: ${{ steps.paths.outputs.pkcs11_manifest }}
172
+ run: |
173
+ set -euo pipefail
174
+
175
+ download_az_blob_uri() {
176
+ local uri="$1"
177
+ local destination="$2"
178
+ local rest account container blob
179
+
180
+ rest="${uri#az://}"
181
+ account="${rest%%/*}"
182
+ rest="${rest#*/}"
183
+ container="${rest%%/*}"
184
+ blob="${rest#*/}"
185
+
186
+ if [[ -z "$account" || -z "$container" || -z "$blob" || "$blob" == "$rest" ]]; then
187
+ echo "Invalid Azure Blob URI. Expected az://<account>/<container>/<blob>." >&2
188
+ exit 1
189
+ fi
190
+
191
+ mkdir -p "$(dirname "$destination")"
192
+ rm -f "$destination"
193
+ if ! az storage blob download \
194
+ --account-name "$account" \
195
+ --container-name "$container" \
196
+ --name "$blob" \
197
+ --file "$destination" \
198
+ --auth-mode login \
199
+ --only-show-errors \
200
+ >/dev/null 2>&1; then
201
+ echo "Failed to download a private signing artifact from Azure Blob Storage." >&2
202
+ exit 1
203
+ fi
204
+ }
205
+
206
+ verify_sha256() {
207
+ local path="$1"
208
+ local expected="$2"
209
+ local actual
210
+
211
+ actual="$(shasum -a 256 "$path" | awk '{ print $1 }')"
212
+ if [[ "$actual" != "$expected" ]]; then
213
+ echo "SHA-256 verification failed for '$path'." >&2
214
+ exit 1
215
+ fi
216
+ }
217
+
218
+ echo "Downloading prebuilt rcodesign."
219
+ download_az_blob_uri "$RCODESIGN_BLOB_URI" "$RCODESIGN"
220
+ verify_sha256 "$RCODESIGN" "$RCODESIGN_SHA256"
221
+ chmod 0755 "$RCODESIGN"
222
+
223
+ echo "Downloading prebuilt AKV PKCS11 provider."
224
+ download_az_blob_uri "$AKV_PKCS11_LIBRARY_BLOB_URI" "$PKCS11_LIBRARY"
225
+ verify_sha256 "$PKCS11_LIBRARY" "$AKV_PKCS11_LIBRARY_SHA256"
226
+ chmod 0644 "$PKCS11_LIBRARY"
227
+
228
+ {
229
+ echo "runner_os=$RUNNER_OS"
230
+ echo "runner_arch=$RUNNER_ARCH"
231
+ echo "library_name=$(basename "$PKCS11_LIBRARY")"
232
+ } > "$PKCS11_MANIFEST"
233
+
234
+ - name: Verify downloaded signing tools
235
+ shell: bash
236
+ env:
237
+ RCODESIGN: ${{ steps.paths.outputs.rcodesign }}
238
+ RCODESIGN_SHA256: ${{ inputs.rcodesign-sha256 }}
239
+ PKCS11_LIBRARY: ${{ steps.paths.outputs.pkcs11_library }}
240
+ AKV_PKCS11_LIBRARY_SHA256: ${{ inputs.akv-pkcs11-library-sha256 }}
241
+ PKCS11_MANIFEST: ${{ steps.paths.outputs.pkcs11_manifest }}
242
+ run: |
243
+ set -euo pipefail
244
+
245
+ verify_sha256() {
246
+ local path="$1"
247
+ local expected="$2"
248
+ local actual
249
+
250
+ actual="$(shasum -a 256 "$path" | awk '{ print $1 }')"
251
+ if [[ "$actual" != "$expected" ]]; then
252
+ echo "SHA-256 verification failed for '$path'." >&2
253
+ exit 1
254
+ fi
255
+ }
256
+
257
+ if [[ ! -x "$RCODESIGN" ]]; then
258
+ echo "rcodesign is missing or not executable at '$RCODESIGN'." >&2
259
+ exit 1
260
+ fi
261
+
262
+ if [[ ! -f "$PKCS11_LIBRARY" ]]; then
263
+ echo "AKV PKCS11 provider library is missing at '$PKCS11_LIBRARY'." >&2
264
+ exit 1
265
+ fi
266
+
267
+ verify_sha256 "$RCODESIGN" "$RCODESIGN_SHA256"
268
+ verify_sha256 "$PKCS11_LIBRARY" "$AKV_PKCS11_LIBRARY_SHA256"
269
+
270
+ "$RCODESIGN" --version
271
+ "$RCODESIGN" notarize --help > /dev/null
272
+
273
+ if [[ -f "$PKCS11_MANIFEST" ]]; then
274
+ echo "AKV PKCS11 provider artifact manifest is present."
275
+ else
276
+ echo "AKV PKCS11 provider artifact manifest is absent." >&2
277
+ exit 1
278
+ fi
279
+
280
+ - name: Download signing certificate from Key Vault
281
+ if: ${{ inputs.setup-mode == 'signing' }}
282
+ shell: bash
283
+ env:
284
+ KEY_VAULT_NAME: ${{ inputs.key-vault-name }}
285
+ KEY_NAME: ${{ inputs.key-name }}
286
+ KEY_VERSION: ${{ inputs.key-version }}
287
+ CERTIFICATE_SHA256: ${{ inputs.certificate-sha256 }}
288
+ SIGNING_CERTIFICATE_PEM: ${{ steps.paths.outputs.signing_certificate_pem }}
289
+ run: |
290
+ set -euo pipefail
291
+
292
+ certificate_version_args=()
293
+ if [[ -n "$KEY_VERSION" ]]; then
294
+ certificate_version_args+=(--version "$KEY_VERSION")
295
+ fi
296
+
297
+ if ! az keyvault certificate download \
298
+ --vault-name "$KEY_VAULT_NAME" \
299
+ --name "$KEY_NAME" \
300
+ "${certificate_version_args[@]}" \
301
+ --file "$SIGNING_CERTIFICATE_PEM" \
302
+ --encoding PEM \
303
+ --only-show-errors \
304
+ >/dev/null 2>&1; then
305
+ echo "Failed to download the public signing certificate from Azure Key Vault." >&2
306
+ exit 1
307
+ fi
308
+
309
+ if [[ -n "$CERTIFICATE_SHA256" ]]; then
310
+ actual_sha256="$(
311
+ openssl x509 -in "$SIGNING_CERTIFICATE_PEM" -noout -fingerprint -sha256 |
312
+ awk -F= '{ print toupper($2) }' |
313
+ tr -d ':\r\n'
314
+ )"
315
+ expected_sha256="$(printf '%s' "$CERTIFICATE_SHA256" | tr '[:lower:]' '[:upper:]' | tr -d ':\r\n ')"
316
+ if [[ "$actual_sha256" != "$expected_sha256" ]]; then
317
+ echo "Downloaded signing certificate SHA-256 did not match the expected fingerprint." >&2
318
+ exit 1
319
+ fi
320
+ fi
321
+
322
+ - name: Export AKV PKCS11 signing environment
323
+ if: ${{ inputs.setup-mode == 'signing' }}
324
+ shell: bash
325
+ env:
326
+ RCODESIGN_ROOT: ${{ steps.paths.outputs.rcodesign_root }}
327
+ PKCS11_LIBRARY: ${{ steps.paths.outputs.pkcs11_library }}
328
+ SIGNING_CERTIFICATE_PEM: ${{ steps.paths.outputs.signing_certificate_pem }}
329
+ KEY_VAULT_NAME: ${{ inputs.key-vault-name }}
330
+ KEY_NAME: ${{ inputs.key-name }}
331
+ KEY_VERSION: ${{ inputs.key-version }}
332
+ run: |
333
+ set -euo pipefail
334
+
335
+ {
336
+ echo "$RCODESIGN_ROOT/bin"
337
+ } >> "$GITHUB_PATH"
338
+
339
+ {
340
+ echo "OAI_CODESIGN_BACKEND=akv-pkcs11"
341
+ echo "OAI_AKV_PKCS11_LIBRARY=$PKCS11_LIBRARY"
342
+ echo "OAI_AKV_SIGNING_CERTIFICATE_PEM=$SIGNING_CERTIFICATE_PEM"
343
+ echo "OAI_AKV_KEY_LABEL=$KEY_NAME"
344
+ echo "AZURE_CREDENTIAL_KIND=azurecli"
345
+ echo "AZURE_KEYVAULT_NAME=$KEY_VAULT_NAME"
346
+ if [[ -n "$KEY_VERSION" ]]; then
347
+ echo "AZURE_KEYVAULT_KEY_VERSION=$KEY_VERSION"
348
+ fi
349
+ } >> "$GITHUB_ENV"
.github/actions/setup-ci/action.yml ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: setup-ci
2
+ description: Prepare common tools and environment shared by CI jobs.
3
+ outputs:
4
+ bazel-output-base:
5
+ description: Filesystem path used for Bazel's output base.
6
+ value: ${{ steps.configure_ci_build_paths.outputs.bazel-output-base }}
7
+ cargo-target-dir:
8
+ description: Filesystem path used for Cargo's target directory.
9
+ value: ${{ steps.configure_ci_build_paths.outputs.cargo-target-dir }}
10
+
11
+ runs:
12
+ using: composite
13
+ steps:
14
+ # setup-ci expects either this step or the Unix fallback below to define
15
+ # CI_BUILD_ROOT. Windows uses a Dev Drive because Cargo and Bazel
16
+ # spend significant time reading and writing build/cache trees.
17
+ - name: Configure Dev Drive (Windows)
18
+ if: runner.os == 'Windows'
19
+ shell: pwsh
20
+ run: ./.github/scripts/setup-dev-drive.ps1
21
+
22
+ - name: Configure CI build root (Unix)
23
+ if: runner.os != 'Windows'
24
+ shell: bash
25
+ run: echo "CI_BUILD_ROOT=$HOME/.cache/codex-ci" >> "$GITHUB_ENV"
26
+
27
+ - name: Enable unprivileged user namespaces (Linux)
28
+ if: runner.os == 'Linux'
29
+ shell: bash
30
+ run: |
31
+ sudo sysctl -w kernel.unprivileged_userns_clone=1
32
+ if sudo sysctl -a 2>/dev/null | grep -q '^kernel.apparmor_restrict_unprivileged_userns'; then
33
+ sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
34
+ fi
35
+
36
+ - name: Configure CI build paths
37
+ id: configure_ci_build_paths
38
+ shell: bash
39
+ run: |
40
+ set -euo pipefail
41
+ # setup-bazel passes output_base explicitly, so keep both it and the
42
+ # user root under the shared build root. Keep these directory names tiny
43
+ # on every platform so Windows Bazel paths do not overflow argv or
44
+ # confuse test MANIFEST handling.
45
+ bazel_output_base="$CI_BUILD_ROOT/o"
46
+ bazel_output_user_root="$CI_BUILD_ROOT/b"
47
+ bazel_repository_cache="$CI_BUILD_ROOT/bazel-repository-cache"
48
+ bazel_repo_contents_cache="$CI_BUILD_ROOT/bazel-repo-contents-cache-$GITHUB_RUN_ID-$GITHUB_JOB"
49
+ cargo_target_dir="$CI_BUILD_ROOT/cargo-target"
50
+ tmp="$CI_BUILD_ROOT/tmp"
51
+
52
+ build_dirs=(
53
+ "$bazel_output_base"
54
+ "$bazel_output_user_root"
55
+ "$bazel_repository_cache"
56
+ "$bazel_repo_contents_cache"
57
+ "$cargo_target_dir"
58
+ "$tmp"
59
+ )
60
+ mkdir -p "${build_dirs[@]}"
61
+ echo "bazel-output-base=$bazel_output_base" >> "$GITHUB_OUTPUT"
62
+ echo "cargo-target-dir=$cargo_target_dir" >> "$GITHUB_OUTPUT"
63
+
64
+ {
65
+ echo "CODEX_REPO_ROOT=$GITHUB_WORKSPACE"
66
+ echo "BAZEL_OUTPUT_BASE=$bazel_output_base"
67
+ echo "BAZEL_OUTPUT_USER_ROOT=$bazel_output_user_root"
68
+ echo "BAZEL_REPOSITORY_CACHE=$bazel_repository_cache"
69
+ echo "BAZEL_REPO_CONTENTS_CACHE=$bazel_repo_contents_cache"
70
+ echo "CARGO_TARGET_DIR=$cargo_target_dir"
71
+ echo "TEMP=$tmp"
72
+ echo "TMP=$tmp"
73
+ } >> "$GITHUB_ENV"
74
+
75
+ - name: Prefer the Git CLI for Cargo git dependencies
76
+ shell: bash
77
+ run: echo "CARGO_NET_GIT_FETCH_WITH_CLI=true" >> "$GITHUB_ENV"
78
+
79
+ - name: Install DotSlash
80
+ uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
81
+
82
+ - name: Install just
83
+ uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2.62.49
84
+ with:
85
+ tool: just@1.51.0
86
+
87
+ # Some integration tests spawn DotSlash from a stable system path rather
88
+ # than inheriting the action-local PATH entry.
89
+ - name: Make DotSlash available in PATH (Unix)
90
+ if: runner.os != 'Windows'
91
+ shell: bash
92
+ run: |
93
+ if [[ -w /usr/local/bin ]]; then
94
+ cp "$(which dotslash)" /usr/local/bin
95
+ else
96
+ sudo cp "$(which dotslash)" /usr/local/bin
97
+ fi
98
+
99
+ - name: Make DotSlash available in PATH (Windows)
100
+ if: runner.os == 'Windows'
101
+ shell: pwsh
102
+ run: Copy-Item (Get-Command dotslash).Source -Destination "$env:LOCALAPPDATA\Microsoft\WindowsApps\dotslash.exe"
103
+
104
+ - name: Enable Git long paths (Windows)
105
+ if: runner.os == 'Windows'
106
+ shell: pwsh
107
+ run: git config --global core.longpaths true
.github/actions/setup-msvc-env/action.yml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: setup-msvc-env
2
+ description: Expose an MSVC developer environment for the requested Windows target.
3
+ inputs:
4
+ target:
5
+ description: Rust target triple that will be built on this Windows runner.
6
+ required: true
7
+ host-arch:
8
+ description: Optional Visual Studio host architecture override.
9
+ required: false
10
+ default: ""
11
+
12
+ runs:
13
+ using: composite
14
+ steps:
15
+ - name: Expose MSVC SDK environment
16
+ shell: pwsh
17
+ env:
18
+ HOST_ARCH: ${{ inputs.host-arch }}
19
+ TARGET: ${{ inputs.target }}
20
+ run: '& "$env:GITHUB_ACTION_PATH/setup-msvc-env.ps1" -Target "$env:TARGET" -HostArch "$env:HOST_ARCH"'
.github/actions/setup-msvc-env/setup-msvc-env.ps1 ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ param(
2
+ [Parameter(Mandatory = $true)]
3
+ [string]$Target,
4
+
5
+ [string]$HostArch = ""
6
+ )
7
+
8
+ # Cargo can cross-compile the Rust code for Windows ARM64 on a Windows x64
9
+ # runner, but rustup alone does not expose the matching MSVC/UCRT include and
10
+ # library paths. Ask Visual Studio for the target-specific developer
11
+ # environment, then persist the relevant variables through GITHUB_ENV so the
12
+ # later Cargo step sees the same environment as a normal VsDevCmd shell.
13
+ switch ($Target) {
14
+ "x86_64-pc-windows-msvc" {
15
+ $TargetArch = "x64"
16
+ $RequiredComponent = "Microsoft.VisualStudio.Component.VC.Tools.x86.x64"
17
+ }
18
+ "aarch64-pc-windows-msvc" {
19
+ $TargetArch = "arm64"
20
+ $RequiredComponent = "Microsoft.VisualStudio.Component.VC.Tools.ARM64"
21
+ }
22
+ default {
23
+ throw "Unsupported Windows MSVC target: $Target"
24
+ }
25
+ }
26
+
27
+ # VsDevCmd needs both sides of the cross compile: the architecture of the
28
+ # machine running the tools and the architecture of the binaries being linked.
29
+ # Infer the host from the runner unless a caller needs to override it.
30
+ if (-not $HostArch) {
31
+ $HostArch = if ($env:PROCESSOR_ARCHITEW6432 -eq "ARM64" -or $env:PROCESSOR_ARCHITECTURE -eq "ARM64") {
32
+ "arm64"
33
+ } else {
34
+ "x64"
35
+ }
36
+ }
37
+
38
+ $VsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
39
+ if (-not (Test-Path $VsWhere)) {
40
+ throw "vswhere.exe not found"
41
+ }
42
+
43
+ # Require the target VC tools component, not merely any Visual Studio install,
44
+ # so an x64 archive producer cannot silently link ARM64 tests with the wrong
45
+ # SDK/toolchain layout.
46
+ $InstallPath = & $VsWhere -latest -products * -requires $RequiredComponent -property installationPath 2>$null
47
+ if (-not $InstallPath) {
48
+ throw "Could not locate a Visual Studio installation with component $RequiredComponent"
49
+ }
50
+
51
+ $VsDevCmd = Join-Path $InstallPath "Common7\Tools\VsDevCmd.bat"
52
+ if (-not (Test-Path $VsDevCmd)) {
53
+ throw "VsDevCmd.bat not found at $VsDevCmd"
54
+ }
55
+
56
+ $VarsToExport = @(
57
+ "INCLUDE",
58
+ "LIB",
59
+ "LIBPATH",
60
+ "PATH",
61
+ "UCRTVersion",
62
+ "UniversalCRTSdkDir",
63
+ "VCINSTALLDIR",
64
+ "VCToolsInstallDir",
65
+ "WindowsLibPath",
66
+ "WindowsSdkBinPath",
67
+ "WindowsSdkDir",
68
+ "WindowsSDKLibVersion",
69
+ "WindowsSDKVersion"
70
+ )
71
+
72
+ # Run VsDevCmd inside cmd.exe because it is a batch file, then copy just the
73
+ # variables Cargo/rustc need into the GitHub Actions environment file. PowerShell
74
+ # cannot mutate the parent composite-action environment directly.
75
+ $EnvLines = & cmd.exe /c ('"{0}" -no_logo -arch={1} -host_arch={2} >nul && set' -f $VsDevCmd, $TargetArch, $HostArch)
76
+ $VcToolsInstallDir = $null
77
+ foreach ($Line in $EnvLines) {
78
+ if ($Line -notmatch "^(.*?)=(.*)$") {
79
+ continue
80
+ }
81
+
82
+ $Name = $Matches[1]
83
+ $Value = $Matches[2]
84
+ if ($VarsToExport -contains $Name) {
85
+ if ($Name -ieq "Path") {
86
+ $Name = "PATH"
87
+ }
88
+ if ($Name -eq "VCToolsInstallDir") {
89
+ $VcToolsInstallDir = $Value
90
+ }
91
+ "$Name=$Value" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
92
+ }
93
+ }
94
+
95
+ if (-not $VcToolsInstallDir) {
96
+ throw "VCToolsInstallDir was not exported by VsDevCmd.bat"
97
+ }
98
+
99
+ # Prefer Rust's bundled linker when rustup provides one, then Visual Studio's
100
+ # LLVM linker, and finally MSVC link.exe. This keeps the cross-compile path close
101
+ # to Rust's normal Windows MSVC behavior while still working on runner images
102
+ # where one of those linkers is absent.
103
+ $Linker = $null
104
+ $Rustc = Get-Command rustc -ErrorAction SilentlyContinue
105
+ if ($Rustc) {
106
+ $Sysroot = (& rustc --print sysroot 2>$null).Trim()
107
+ $RustHost = & rustc -vV 2>$null | Select-String "^host: " | ForEach-Object { $_.Line.Substring(6) }
108
+ if ($RustHost) {
109
+ $RustHost = $RustHost.Trim()
110
+ }
111
+ if ($Sysroot -and $RustHost) {
112
+ $RustLld = Join-Path $Sysroot "lib\rustlib\$RustHost\bin\rust-lld.exe"
113
+ if (Test-Path $RustLld) {
114
+ $Linker = $RustLld
115
+ }
116
+ }
117
+ }
118
+ if (-not $Linker) {
119
+ $Linker = Join-Path $InstallPath "VC\Tools\Llvm\x64\bin\lld-link.exe"
120
+ }
121
+ if (-not (Test-Path $Linker)) {
122
+ $Linker = Join-Path $VcToolsInstallDir "bin\Host${HostArch}\${TargetArch}\link.exe"
123
+ }
124
+ if (-not (Test-Path $Linker)) {
125
+ throw "Windows linker not found at $Linker"
126
+ }
127
+
128
+ # rustc passes `/arm64hazardfree` for ARM64 MSVC links. The lld variants on our
129
+ # Windows x64 archive producers reject that flag, including when rustc places it
130
+ # inside a response file. Compile a tiny forwarding wrapper that strips only
131
+ # that unsupported flag, then delegate every other argument to the real linker.
132
+ if ($TargetArch -eq "arm64" -and (Split-Path -Leaf $Linker) -match "lld") {
133
+ $WrapperDir = Join-Path $env:RUNNER_TEMP "msvc-lld-wrapper"
134
+ New-Item -Path $WrapperDir -ItemType Directory -Force | Out-Null
135
+ $WrapperPath = Join-Path $WrapperDir "lld-link-wrapper.exe"
136
+ $WrapperSource = @'
137
+ using System;
138
+ using System.Collections.Generic;
139
+ using System.Diagnostics;
140
+ using System.IO;
141
+ using System.Text;
142
+ using System.Text.RegularExpressions;
143
+
144
+ internal static class Program
145
+ {
146
+ private static int Main(string[] args)
147
+ {
148
+ var linker = Environment.GetEnvironmentVariable("MSVC_REAL_LINKER");
149
+ if (string.IsNullOrEmpty(linker))
150
+ {
151
+ Console.Error.WriteLine("MSVC_REAL_LINKER is not set");
152
+ return 1;
153
+ }
154
+
155
+ var startInfo = new ProcessStartInfo(linker)
156
+ {
157
+ UseShellExecute = false,
158
+ };
159
+ var filteredArgs = new List<string> { "-flavor", "link", "/defaultlib:ucrt", "/nodefaultlib:libucrt" };
160
+ foreach (var arg in args)
161
+ {
162
+ if (!string.Equals(arg, "/arm64hazardfree", StringComparison.OrdinalIgnoreCase))
163
+ {
164
+ filteredArgs.Add(QuoteArgument(FilterResponseFile(arg)));
165
+ }
166
+ }
167
+ startInfo.Arguments = string.Join(" ", filteredArgs);
168
+
169
+ using var process = Process.Start(startInfo);
170
+ if (process is null)
171
+ {
172
+ Console.Error.WriteLine($"Failed to start linker: {linker}");
173
+ return 1;
174
+ }
175
+
176
+ process.WaitForExit();
177
+ return process.ExitCode;
178
+ }
179
+
180
+ private static string FilterResponseFile(string argument)
181
+ {
182
+ if (argument.Length < 2 || argument[0] != '@')
183
+ {
184
+ return argument;
185
+ }
186
+
187
+ var responsePath = argument.Substring(1);
188
+ if (!File.Exists(responsePath))
189
+ {
190
+ return argument;
191
+ }
192
+
193
+ var filteredResponsePath = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName() + ".rsp");
194
+ var responseContents = Regex.Replace(
195
+ File.ReadAllText(responsePath),
196
+ "/arm64hazardfree",
197
+ string.Empty,
198
+ RegexOptions.IgnoreCase);
199
+ File.WriteAllText(filteredResponsePath, responseContents);
200
+ return "@" + filteredResponsePath;
201
+ }
202
+
203
+ private static string QuoteArgument(string argument)
204
+ {
205
+ if (argument.Length == 0)
206
+ {
207
+ return "\"\"";
208
+ }
209
+ if (argument.IndexOfAny(new[] { ' ', '\t', '"' }) < 0)
210
+ {
211
+ return argument;
212
+ }
213
+
214
+ var quoted = new StringBuilder("\"");
215
+ var backslashes = 0;
216
+ foreach (var character in argument)
217
+ {
218
+ if (character == '\\')
219
+ {
220
+ backslashes++;
221
+ continue;
222
+ }
223
+ if (character == '"')
224
+ {
225
+ quoted.Append('\\', (backslashes * 2) + 1);
226
+ quoted.Append(character);
227
+ backslashes = 0;
228
+ continue;
229
+ }
230
+
231
+ quoted.Append('\\', backslashes);
232
+ backslashes = 0;
233
+ quoted.Append(character);
234
+ }
235
+ quoted.Append('\\', backslashes * 2);
236
+ quoted.Append('"');
237
+ return quoted.ToString();
238
+ }
239
+ }
240
+ '@
241
+ $WrapperSourcePath = Join-Path $WrapperDir "lld-link-wrapper.cs"
242
+ $WrapperSource | Out-File -FilePath $WrapperSourcePath -Encoding utf8
243
+ $Csc = Join-Path $InstallPath "MSBuild\Current\Bin\Roslyn\csc.exe"
244
+ if (-not (Test-Path $Csc)) {
245
+ throw "csc.exe not found at $Csc"
246
+ }
247
+ & $Csc /nologo /target:exe /out:$WrapperPath $WrapperSourcePath
248
+ if ($LASTEXITCODE -ne 0) {
249
+ throw "Failed to compile lld-link wrapper"
250
+ }
251
+ "MSVC_REAL_LINKER=$Linker" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
252
+ $Linker = $WrapperPath
253
+ }
254
+
255
+ Write-Output "Using Windows linker: $Linker"
256
+ $CargoTarget = $Target.ToUpperInvariant().Replace("-", "_")
257
+ "CARGO_TARGET_${CargoTarget}_LINKER=$Linker" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
.github/actions/setup-rusty-v8/action.yml ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: setup-rusty-v8
2
+ description: Download and verify Codex-built rusty_v8 artifacts for Cargo builds.
3
+ inputs:
4
+ target:
5
+ description: Rust target triple with Codex-built V8 release artifacts.
6
+ required: true
7
+
8
+ runs:
9
+ using: composite
10
+ steps:
11
+ - name: Configure rusty_v8 artifact overrides and verify checksums
12
+ shell: bash
13
+ env:
14
+ TARGET: ${{ inputs.target }}
15
+ run: |
16
+ set -euo pipefail
17
+
18
+ version="$(python3 "${GITHUB_WORKSPACE}/.github/scripts/rusty_v8_bazel.py" resolved-v8-crate-version)"
19
+ release_tag="rusty-v8-v${version}"
20
+ base_url="https://github.com/openai/codex/releases/download/${release_tag}"
21
+ binding_dir="${RUNNER_TEMP}/rusty_v8"
22
+
23
+ profile="ptrcomp_sandbox_release"
24
+
25
+ if [[ "$TARGET" == *-pc-windows-msvc ]]; then
26
+ archive_name="rusty_v8_${profile}_${TARGET}.lib.gz"
27
+ else
28
+ archive_name="librusty_v8_${profile}_${TARGET}.a.gz"
29
+ fi
30
+ binding_name="src_binding_${profile}_${TARGET}.rs"
31
+ checksums_name="rusty_v8_${profile}_${TARGET}.sha256"
32
+
33
+ archive_path="${binding_dir}/${archive_name}"
34
+ binding_path="${binding_dir}/${binding_name}"
35
+ checksums_path="${binding_dir}/${checksums_name}"
36
+ trusted_checksums="${GITHUB_WORKSPACE}/third_party/v8/rusty_v8_${version//./_}_release_manifests.sha256"
37
+
38
+ if command -v sha256sum >/dev/null 2>&1; then
39
+ checksum_command=(sha256sum --check -)
40
+ else
41
+ checksum_command=(shasum -a 256 --check -)
42
+ fi
43
+
44
+ mkdir -p "${binding_dir}"
45
+ curl -fsSL "${base_url}/${checksums_name}" -o "${checksums_path}"
46
+ # Check the original manifest bytes even when either checksum file uses CRLF.
47
+ expected_manifest_checksum="$(grep -F " ${checksums_name}" "${trusted_checksums}" | cut -d ' ' -f 1)"
48
+ actual_manifest_checksum="$(python3 -c 'import hashlib, pathlib, sys; print(hashlib.sha256(pathlib.Path(sys.argv[1]).read_bytes()).hexdigest())' "${checksums_path}")"
49
+ if [[ "${actual_manifest_checksum}" != "${expected_manifest_checksum}" ]]; then
50
+ echo "Checksum mismatch for ${checksums_name}: expected ${expected_manifest_checksum}, got ${actual_manifest_checksum}" >&2
51
+ exit 1
52
+ fi
53
+ curl -fsSL "${base_url}/${archive_name}" -o "${archive_path}"
54
+ curl -fsSL "${base_url}/${binding_name}" -o "${binding_path}"
55
+
56
+ if [[ "$(wc -l < "${checksums_path}")" -ne 2 ]]; then
57
+ echo "Expected exactly two checksums for ${TARGET} in ${checksums_path}" >&2
58
+ exit 1
59
+ fi
60
+
61
+ # Existing Windows-built release manifests use CRLF line endings.
62
+ (cd "${binding_dir}" && tr -d '\r' < "${checksums_path}" | "${checksum_command[@]}")
63
+ echo "RUSTY_V8_ARCHIVE=${archive_path}" >> "${GITHUB_ENV}"
64
+ echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "${GITHUB_ENV}"
.github/actions/windows-code-sign/action.yml ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: windows-code-sign
2
+ description: Sign Windows binaries with Azure Trusted Signing.
3
+ inputs:
4
+ target:
5
+ description: Target triple for the artifacts to sign.
6
+ required: true
7
+ binaries:
8
+ description: Space-delimited binary basenames to sign.
9
+ default: "codex codex-responses-api-proxy codex-windows-sandbox-setup codex-command-runner"
10
+ client-id:
11
+ description: Azure Trusted Signing client ID.
12
+ required: true
13
+ tenant-id:
14
+ description: Azure tenant ID for Trusted Signing.
15
+ required: true
16
+ subscription-id:
17
+ description: Azure subscription ID for Trusted Signing.
18
+ required: true
19
+ endpoint:
20
+ description: Azure Trusted Signing endpoint.
21
+ required: true
22
+ account-name:
23
+ description: Azure Trusted Signing account name.
24
+ required: true
25
+ certificate-profile-name:
26
+ description: Certificate profile name for signing.
27
+ required: true
28
+
29
+ runs:
30
+ using: composite
31
+ steps:
32
+ - name: Azure login for Trusted Signing (OIDC)
33
+ uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
34
+ with:
35
+ client-id: ${{ inputs.client-id }}
36
+ tenant-id: ${{ inputs.tenant-id }}
37
+ subscription-id: ${{ inputs.subscription-id }}
38
+
39
+ - name: Prepare file list
40
+ id: prepare
41
+ shell: bash
42
+ env:
43
+ TARGET: ${{ inputs.target }}
44
+ BINARIES: ${{ inputs.binaries }}
45
+ run: |
46
+ set -euo pipefail
47
+
48
+ {
49
+ echo "files<<EOF"
50
+ for binary in ${BINARIES}; do
51
+ echo "${CARGO_TARGET_DIR}/${TARGET}/release/${binary}.exe"
52
+ done
53
+ echo "EOF"
54
+ } >> "$GITHUB_OUTPUT"
55
+
56
+ - name: Sign Windows binaries with Azure Trusted Signing
57
+ uses: azure/trusted-signing-action@1d365fec12862c4aa68fcac418143d73f0cea293 # v0.5.11
58
+ with:
59
+ endpoint: ${{ inputs.endpoint }}
60
+ trusted-signing-account-name: ${{ inputs.account-name }}
61
+ certificate-profile-name: ${{ inputs.certificate-profile-name }}
62
+ exclude-environment-credential: true
63
+ exclude-workload-identity-credential: true
64
+ exclude-managed-identity-credential: true
65
+ exclude-shared-token-cache-credential: true
66
+ exclude-visual-studio-credential: true
67
+ exclude-visual-studio-code-credential: true
68
+ exclude-azure-cli-credential: false
69
+ exclude-azure-powershell-credential: true
70
+ exclude-azure-developer-cli-credential: true
71
+ exclude-interactive-browser-credential: true
72
+ cache-dependencies: false
73
+ files: ${{ steps.prepare.outputs.files }}
.github/codex/labels/codex-triage.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ Troubleshoot whether the reported issue is valid.
2
+
3
+ Provide a concise and respectful comment summarizing the findings.
4
+
5
+ ### {CODEX_ACTION_ISSUE_TITLE}
6
+
7
+ {CODEX_ACTION_ISSUE_BODY}
.github/scripts/macos-signing/codex-app-server.entitlements.plist ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>com.apple.security.cs.allow-jit</key>
6
+ <true/>
7
+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
8
+ <true/>
9
+ </dict>
10
+ </plist>
.github/scripts/macos-signing/codex-code-mode-host.entitlements.plist ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>com.apple.security.cs.allow-jit</key>
6
+ <true/>
7
+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
8
+ <true/>
9
+ </dict>
10
+ </plist>
.github/scripts/macos-signing/codex-responses-api-proxy.entitlements.plist ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>com.apple.security.cs.allow-jit</key>
6
+ <true/>
7
+ </dict>
8
+ </plist>
.github/scripts/macos-signing/codex-voice-host.entitlements.plist ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>com.apple.security.device.audio-input</key>
6
+ <true/>
7
+ </dict>
8
+ </plist>
.github/scripts/macos-signing/codex.entitlements.plist ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>com.apple.security.cs.allow-jit</key>
6
+ <true/>
7
+ <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
8
+ <true/>
9
+ </dict>
10
+ </plist>
.github/scripts/macos-signing/notarize_macos_binary_with_akv.sh ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ # Notarize a standalone binary and retain its diagnostic report.
4
+
5
+ set -euo pipefail
6
+
7
+ usage() {
8
+ cat >&2 <<'EOF'
9
+ Usage: notarize_macos_binary_with_akv.sh --binary PATH [--report-dir PATH] [--max-wait-seconds SECONDS]
10
+
11
+ Options:
12
+ --binary PATH Signed standalone macOS binary to notarize.
13
+ --report-dir PATH Directory for notarization logs.
14
+ --max-wait-seconds SECONDS Maximum Apple notarization wait time.
15
+ EOF
16
+ }
17
+
18
+ binary_path=""
19
+ report_dir="${RUNNER_TEMP:-/tmp}/macos-binary-notarization-verification"
20
+ max_wait_seconds="600"
21
+
22
+ while [[ $# -gt 0 ]]; do
23
+ case "$1" in
24
+ --binary)
25
+ binary_path="${2:-}"
26
+ shift 2
27
+ ;;
28
+ --report-dir)
29
+ report_dir="${2:-}"
30
+ shift 2
31
+ ;;
32
+ --max-wait-seconds)
33
+ max_wait_seconds="${2:-}"
34
+ shift 2
35
+ ;;
36
+ -h|--help)
37
+ usage
38
+ exit 0
39
+ ;;
40
+ *)
41
+ echo "Unknown notarization argument: $1" >&2
42
+ usage
43
+ exit 2
44
+ ;;
45
+ esac
46
+ done
47
+
48
+ if [[ -z "$binary_path" ]]; then
49
+ echo "--binary is required." >&2
50
+ usage
51
+ exit 2
52
+ fi
53
+
54
+ if [[ ! -f "$binary_path" ]]; then
55
+ echo "Binary does not exist: $binary_path" >&2
56
+ exit 1
57
+ fi
58
+
59
+ if [[ ! "$max_wait_seconds" =~ ^[0-9]+$ ]]; then
60
+ echo "--max-wait-seconds must be a non-negative integer." >&2
61
+ exit 2
62
+ fi
63
+
64
+ for command_name in python3 zip; do
65
+ if ! command -v "$command_name" >/dev/null 2>&1; then
66
+ echo "$command_name was not found on PATH." >&2
67
+ exit 1
68
+ fi
69
+ done
70
+
71
+ missing_environment=0
72
+ for variable_name in \
73
+ APPLE_NOTARIZATION_AKV_KEY_NAME \
74
+ AZURE_KEYVAULT_NAME
75
+ do
76
+ if [[ -z "${!variable_name:-}" ]]; then
77
+ echo "$variable_name must be configured before notarizing a binary." >&2
78
+ missing_environment=1
79
+ fi
80
+ done
81
+
82
+ if [[ "$missing_environment" -ne 0 ]]; then
83
+ exit 2
84
+ fi
85
+
86
+ mkdir -p "$report_dir"
87
+
88
+ notarization_temp_dir="$(mktemp -d)"
89
+ trap 'rm -rf "$notarization_temp_dir" >/dev/null' EXIT
90
+
91
+ binary_name="$(basename "$binary_path")"
92
+ archive_path="$notarization_temp_dir/${binary_name}.zip"
93
+ (
94
+ cd "$(dirname "$binary_path")"
95
+ zip -q "$archive_path" "$binary_name"
96
+ )
97
+
98
+ notarization_log="$report_dir/${binary_name}-notarization.log"
99
+ python3 "$(dirname "$0")/notarize_with_akv.py" \
100
+ --file "$archive_path" \
101
+ --report-log "$report_dir/${binary_name}-notarization-developer-log.json" \
102
+ --max-wait-seconds "$max_wait_seconds" \
103
+ 2>&1 | tee "$notarization_log"
104
+
105
+ {
106
+ echo "binary_name=$binary_name"
107
+ echo "max_wait_seconds=$max_wait_seconds"
108
+ echo "binary_sha256=$(shasum -a 256 "$binary_path" | awk '{ print $1 }')"
109
+ echo "notarization=completed"
110
+ } >"$report_dir/${binary_name}-notarization-summary.txt"
.github/scripts/macos-signing/notarize_macos_dmg_with_akv.sh ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ # Notarize a signed disk image and staple its ticket.
4
+
5
+ set -euo pipefail
6
+
7
+ usage() {
8
+ cat >&2 <<'EOF'
9
+ Usage: notarize_macos_dmg_with_akv.sh --dmg PATH [--report-dir PATH] [--max-wait-seconds SECONDS]
10
+
11
+ Options:
12
+ --dmg PATH Signed DMG to submit to Apple notarization.
13
+ --report-dir PATH Directory for notarization logs.
14
+ --max-wait-seconds SECONDS Maximum Apple notarization wait time.
15
+ EOF
16
+ }
17
+
18
+ dmg_path=""
19
+ report_dir="${RUNNER_TEMP:-/tmp}/macos-notarization-verification"
20
+ max_wait_seconds="600"
21
+
22
+ while [[ $# -gt 0 ]]; do
23
+ case "$1" in
24
+ --dmg)
25
+ dmg_path="${2:-}"
26
+ shift 2
27
+ ;;
28
+ --report-dir)
29
+ report_dir="${2:-}"
30
+ shift 2
31
+ ;;
32
+ --max-wait-seconds)
33
+ max_wait_seconds="${2:-}"
34
+ shift 2
35
+ ;;
36
+ -h|--help)
37
+ usage
38
+ exit 0
39
+ ;;
40
+ *)
41
+ echo "Unknown notarization argument: $1" >&2
42
+ usage
43
+ exit 2
44
+ ;;
45
+ esac
46
+ done
47
+
48
+ if [[ -z "$dmg_path" ]]; then
49
+ echo "--dmg is required." >&2
50
+ usage
51
+ exit 2
52
+ fi
53
+
54
+ if [[ ! -f "$dmg_path" ]]; then
55
+ echo "DMG does not exist: $dmg_path" >&2
56
+ exit 1
57
+ fi
58
+
59
+ if [[ ! "$max_wait_seconds" =~ ^[0-9]+$ ]]; then
60
+ echo "--max-wait-seconds must be a non-negative integer." >&2
61
+ exit 2
62
+ fi
63
+
64
+ if ! command -v rcodesign > /dev/null 2>&1; then
65
+ echo "rcodesign was not found on PATH." >&2
66
+ exit 1
67
+ fi
68
+
69
+ missing_environment=0
70
+ for variable_name in \
71
+ APPLE_NOTARIZATION_AKV_KEY_NAME \
72
+ AZURE_KEYVAULT_NAME
73
+ do
74
+ if [[ -z "${!variable_name:-}" ]]; then
75
+ echo "$variable_name must be configured before notarizing a DMG." >&2
76
+ missing_environment=1
77
+ fi
78
+ done
79
+
80
+ if [[ "$missing_environment" -ne 0 ]]; then
81
+ exit 2
82
+ fi
83
+
84
+ mkdir -p "$report_dir"
85
+
86
+ notarization_log="$report_dir/dmg-notarization.log"
87
+ python3 "$(dirname "$0")/notarize_with_akv.py" \
88
+ --file "$dmg_path" \
89
+ --report-log "$report_dir/dmg-notarization-developer-log.json" \
90
+ --max-wait-seconds "$max_wait_seconds" \
91
+ 2>&1 | tee "$notarization_log"
92
+
93
+ rcodesign staple "$dmg_path" 2>&1 | tee -a "$notarization_log"
94
+
95
+ {
96
+ echo "dmg_path=$dmg_path"
97
+ echo "max_wait_seconds=$max_wait_seconds"
98
+ echo "dmg_sha256=$(shasum -a 256 "$dmg_path" | awk '{ print $1 }')"
99
+ echo "notarization_staple=completed"
100
+ } > "$report_dir/dmg-notarization-summary.txt"
.github/scripts/macos-signing/notarize_with_akv.py ADDED
@@ -0,0 +1,519 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Submit release artifacts for notarization and wait for the result."""
3
+
4
+ import argparse
5
+ import base64
6
+ import datetime
7
+ import hashlib
8
+ import hmac
9
+ import json
10
+ import os
11
+ import re
12
+ import subprocess
13
+ import sys
14
+ import time
15
+ import urllib.error
16
+ import urllib.parse
17
+ import urllib.request
18
+ from dataclasses import dataclass
19
+ from pathlib import Path
20
+ from typing import Any
21
+
22
+ APPLE_NOTARY_URL = "https://appstoreconnect.apple.com/notary/v2/submissions"
23
+ AZURE_KEYVAULT_API_VERSION = "7.4"
24
+ AWS_REGION = "us-west-2"
25
+ JWT_LIFETIME_SECONDS = 15 * 60
26
+ POLL_INTERVAL_SECONDS = 10
27
+ MAX_SINGLE_UPLOAD_BYTES = 5 * 1024 * 1024 * 1024
28
+
29
+
30
+ class NotarizationError(RuntimeError):
31
+ """Describe a notarization failure."""
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class NotarizationConfiguration:
36
+ """Hold the account and signing-key configuration."""
37
+
38
+ issuer_id: str
39
+ apple_key_id: str
40
+ vault_name: str
41
+ vault_key_name: str
42
+ vault_key_version: str
43
+
44
+ @classmethod
45
+ def from_environment(cls) -> "NotarizationConfiguration":
46
+ """Load and validate the notarization configuration."""
47
+
48
+ required = {
49
+ "vault_name": "AZURE_KEYVAULT_NAME",
50
+ "vault_key_name": "APPLE_NOTARIZATION_AKV_KEY_NAME",
51
+ }
52
+ values = {
53
+ field: os.environ.get(name, "").strip() for field, name in required.items()
54
+ }
55
+ missing = [name for field, name in required.items() if not values[field]]
56
+ if missing:
57
+ raise NotarizationError(
58
+ "Missing notarization configuration: " + ", ".join(missing)
59
+ )
60
+ if not re.fullmatch(r"[A-Za-z][A-Za-z0-9-]{2,23}", values["vault_name"]):
61
+ raise NotarizationError("Invalid signing vault name")
62
+ if not re.fullmatch(r"[A-Za-z0-9-]+", values["vault_key_name"]):
63
+ raise NotarizationError("Invalid notarization key name")
64
+ key_version = os.environ.get("APPLE_NOTARIZATION_AKV_KEY_VERSION", "").strip()
65
+ if key_version and not re.fullmatch(r"[0-9a-fA-F]{32}", key_version):
66
+ raise NotarizationError(
67
+ "Notarization key must resolve to a 32-character pinned version"
68
+ )
69
+
70
+ command = [
71
+ "az",
72
+ "keyvault",
73
+ "key",
74
+ "show",
75
+ "--vault-name",
76
+ values["vault_name"],
77
+ "--name",
78
+ values["vault_key_name"],
79
+ ]
80
+ if key_version:
81
+ command.extend(["--version", key_version])
82
+ command.extend(
83
+ [
84
+ "--query",
85
+ '{id:key.kid,apple_key_id:tags."apple-key-id",'
86
+ 'apple_issuer_id:tags."apple-issuer-id"}',
87
+ "--output",
88
+ "json",
89
+ "--only-show-errors",
90
+ ]
91
+ )
92
+
93
+ # Resolve the key identifier and version from the same lookup.
94
+ try:
95
+ result = subprocess.run(command, check=True, capture_output=True, text=True)
96
+ except (OSError, subprocess.CalledProcessError) as error:
97
+ raise NotarizationError(
98
+ "Notarization signing key could not be read"
99
+ ) from error
100
+ try:
101
+ metadata = json.loads(result.stdout)
102
+ returned_key_id = metadata["id"]
103
+ apple_key_id = metadata["apple_key_id"]
104
+ apple_issuer_id = metadata["apple_issuer_id"]
105
+ except (json.JSONDecodeError, KeyError, TypeError) as error:
106
+ raise NotarizationError("Notarization key metadata is invalid") from error
107
+
108
+ key_id_prefix = f"https://{values['vault_name']}.vault.azure.net/keys/{values['vault_key_name']}/"
109
+ if not isinstance(returned_key_id, str) or not returned_key_id.startswith(
110
+ key_id_prefix
111
+ ):
112
+ raise NotarizationError("Unexpected notarization signing key")
113
+ returned_key_version = returned_key_id[len(key_id_prefix) :]
114
+ if not re.fullmatch(r"[0-9a-fA-F]{32}", returned_key_version):
115
+ raise NotarizationError(
116
+ "Notarization key must resolve to a 32-character pinned version"
117
+ )
118
+ if key_version and returned_key_version.lower() != key_version.lower():
119
+ raise NotarizationError("Unexpected notarization key version")
120
+ if not isinstance(apple_key_id, str) or not apple_key_id.strip():
121
+ raise NotarizationError("Notarization key must have an apple-key-id tag")
122
+ if not isinstance(apple_issuer_id, str) or not apple_issuer_id.strip():
123
+ raise NotarizationError("Notarization key must have an apple-issuer-id tag")
124
+ if not re.fullmatch(
125
+ r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
126
+ r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}",
127
+ apple_issuer_id.strip(),
128
+ ):
129
+ raise NotarizationError(
130
+ "Notarization apple-issuer-id tag must contain a valid UUID"
131
+ )
132
+
133
+ values["issuer_id"] = apple_issuer_id.strip()
134
+ values["apple_key_id"] = apple_key_id.strip()
135
+ values["vault_key_version"] = returned_key_version
136
+ return cls(**values)
137
+
138
+ @property
139
+ def versioned_key_id(self) -> str:
140
+ """Return the versioned signing-key identifier."""
141
+
142
+ return (
143
+ f"https://{self.vault_name}.vault.azure.net/keys/"
144
+ f"{self.vault_key_name}/{self.vault_key_version}"
145
+ )
146
+
147
+
148
+ def base64url_encode(value: bytes) -> str:
149
+ """Encode bytes using the unpadded base64url format required by JWTs."""
150
+
151
+ return base64.urlsafe_b64encode(value).decode("ascii").rstrip("=")
152
+
153
+
154
+ def base64url_decode(value: str) -> bytes:
155
+ """Decode a base64url value and reject malformed input."""
156
+
157
+ try:
158
+ return base64.b64decode(
159
+ value + "=" * (-len(value) % 4), altchars=b"-_", validate=True
160
+ )
161
+ except (ValueError, UnicodeEncodeError) as error:
162
+ raise NotarizationError(
163
+ "Signing service returned an invalid signature"
164
+ ) from error
165
+
166
+
167
+ def create_apple_jwt(
168
+ configuration: NotarizationConfiguration, *, issued_at: int
169
+ ) -> str:
170
+ """Create an authentication token for notarization requests."""
171
+
172
+ header = {"alg": "ES256", "kid": configuration.apple_key_id, "typ": "JWT"}
173
+ # Restrict the token to notarization requests.
174
+ claims = {
175
+ "iss": configuration.issuer_id,
176
+ "iat": issued_at,
177
+ "exp": issued_at + JWT_LIFETIME_SECONDS,
178
+ "aud": "appstoreconnect-v1",
179
+ "scope": ["/notary/v2"],
180
+ }
181
+ encoded_header = base64url_encode(
182
+ json.dumps(header, separators=(",", ":")).encode("utf-8")
183
+ )
184
+ encoded_claims = base64url_encode(
185
+ json.dumps(claims, separators=(",", ":")).encode("utf-8")
186
+ )
187
+ signing_input = f"{encoded_header}.{encoded_claims}"
188
+ digest = hashlib.sha256(signing_input.encode("ascii")).digest()
189
+ key_url = configuration.versioned_key_id
190
+ request_body = json.dumps({"alg": "ES256", "value": base64url_encode(digest)})
191
+
192
+ try:
193
+ result = subprocess.run(
194
+ [
195
+ "az",
196
+ "rest",
197
+ "--method",
198
+ "post",
199
+ "--url",
200
+ f"{key_url}/sign?api-version={AZURE_KEYVAULT_API_VERSION}",
201
+ "--resource",
202
+ "https://vault.azure.net",
203
+ "--headers",
204
+ "Content-Type=application/json",
205
+ "--body",
206
+ request_body,
207
+ "--output",
208
+ "json",
209
+ "--only-show-errors",
210
+ ],
211
+ check=True,
212
+ capture_output=True,
213
+ text=True,
214
+ )
215
+ except (OSError, subprocess.CalledProcessError) as error:
216
+ detail = getattr(error, "stderr", "").strip()
217
+ message = "Signing service could not sign the authentication token"
218
+ if detail:
219
+ message = f"{message}: {detail}"
220
+ raise NotarizationError(message) from error
221
+
222
+ try:
223
+ response = json.loads(result.stdout)
224
+ returned_key_id = response["kid"]
225
+ encoded_signature = response["value"]
226
+ except (json.JSONDecodeError, KeyError, TypeError) as error:
227
+ raise NotarizationError(
228
+ "Signing service returned an invalid response"
229
+ ) from error
230
+
231
+ # Reject signatures generated with a different key version.
232
+ if returned_key_id != key_url:
233
+ raise NotarizationError("Signing service returned an unexpected key version")
234
+ if not isinstance(encoded_signature, str):
235
+ raise NotarizationError("Signing service returned an invalid signature")
236
+ signature = base64url_decode(encoded_signature)
237
+ # ES256 JWT signatures are 64 raw R || S bytes, not ASN.1/DER-encoded ECDSA signatures.
238
+ if len(signature) != 64:
239
+ raise NotarizationError("ES256 signature must contain 64 JOSE R || S bytes")
240
+ return f"{signing_input}.{base64url_encode(signature)}"
241
+
242
+
243
+ def json_request(
244
+ url: str, token: str, *, body: dict[str, Any] | None = None
245
+ ) -> dict[str, Any]:
246
+ """Send an authenticated JSON request to Apple's notarization service."""
247
+
248
+ data = None if body is None else json.dumps(body).encode("utf-8")
249
+ request = urllib.request.Request(
250
+ url,
251
+ data=data,
252
+ method="GET" if body is None else "POST",
253
+ headers={
254
+ "Accept": "application/json",
255
+ "Authorization": f"Bearer {token}",
256
+ "Content-Type": "application/json",
257
+ },
258
+ )
259
+ try:
260
+ with urllib.request.urlopen(request, timeout=60) as response:
261
+ result = json.load(response)
262
+ except urllib.error.HTTPError as error:
263
+ detail = error.read(1024).decode("utf-8", errors="replace")
264
+ raise NotarizationError(
265
+ f"Apple Notary API returned HTTP {error.code}: {detail}"
266
+ ) from error
267
+ except (OSError, json.JSONDecodeError) as error:
268
+ raise NotarizationError("Apple Notary API request failed") from error
269
+
270
+ if not isinstance(result, dict):
271
+ raise NotarizationError("Apple Notary API returned an invalid response")
272
+ return result
273
+
274
+
275
+ def sha256_file(path: Path) -> str:
276
+ """Hash an artifact incrementally so large release files stay off the heap."""
277
+
278
+ digest = hashlib.sha256()
279
+ with path.open("rb") as source:
280
+ for chunk in iter(lambda: source.read(1024 * 1024), b""):
281
+ digest.update(chunk)
282
+ return digest.hexdigest()
283
+
284
+
285
+ def hmac_sha256(key: bytes, value: str) -> bytes:
286
+ """Derive one AWS Signature Version 4 signing-key component."""
287
+
288
+ return hmac.new(key, value.encode("utf-8"), hashlib.sha256).digest()
289
+
290
+
291
+ def upload_to_apple_s3(
292
+ path: Path,
293
+ file_digest: str,
294
+ credentials: dict[str, Any],
295
+ *,
296
+ timestamp: datetime.datetime | None = None,
297
+ ) -> None:
298
+ """Upload an artifact using the provided temporary credentials."""
299
+
300
+ required = (
301
+ "awsAccessKeyId",
302
+ "awsSecretAccessKey",
303
+ "awsSessionToken",
304
+ "bucket",
305
+ "object",
306
+ )
307
+ if any(
308
+ not isinstance(credentials.get(name), str) or not credentials[name]
309
+ for name in required
310
+ ):
311
+ raise NotarizationError(
312
+ "Apple returned incomplete temporary upload credentials"
313
+ )
314
+
315
+ size = path.stat().st_size
316
+ if not 0 < size <= MAX_SINGLE_UPLOAD_BYTES:
317
+ raise NotarizationError("Notarization payload must be between 1 byte and 5 GiB")
318
+
319
+ now = timestamp or datetime.datetime.now(datetime.timezone.utc)
320
+ amz_date = now.strftime("%Y%m%dT%H%M%SZ")
321
+ date_stamp = now.strftime("%Y%m%d")
322
+ host = f"{credentials['bucket']}.s3.{AWS_REGION}.amazonaws.com"
323
+ object_path = "/" + urllib.parse.quote(
324
+ credentials["object"].lstrip("/"), safe="/-_.~"
325
+ )
326
+ headers = {
327
+ "content-type": "application/octet-stream",
328
+ "host": host,
329
+ "x-amz-content-sha256": file_digest,
330
+ "x-amz-date": amz_date,
331
+ "x-amz-security-token": credentials["awsSessionToken"],
332
+ }
333
+ # Build the signature required by the upload endpoint.
334
+ signed_headers = ";".join(sorted(headers))
335
+ canonical_headers = "".join(f"{name}:{headers[name]}\n" for name in sorted(headers))
336
+ canonical_request = "\n".join(
337
+ ["PUT", object_path, "", canonical_headers, signed_headers, file_digest]
338
+ )
339
+ credential_scope = f"{date_stamp}/{AWS_REGION}/s3/aws4_request"
340
+ string_to_sign = "\n".join(
341
+ [
342
+ "AWS4-HMAC-SHA256",
343
+ amz_date,
344
+ credential_scope,
345
+ hashlib.sha256(canonical_request.encode("utf-8")).hexdigest(),
346
+ ]
347
+ )
348
+ signing_key = ("AWS4" + credentials["awsSecretAccessKey"]).encode("utf-8")
349
+ for component in (date_stamp, AWS_REGION, "s3", "aws4_request"):
350
+ signing_key = hmac_sha256(signing_key, component)
351
+ signature = hmac.new(
352
+ signing_key, string_to_sign.encode("utf-8"), hashlib.sha256
353
+ ).hexdigest()
354
+ authorization = (
355
+ "AWS4-HMAC-SHA256 "
356
+ f"Credential={credentials['awsAccessKeyId']}/{credential_scope}, "
357
+ f"SignedHeaders={signed_headers}, Signature={signature}"
358
+ )
359
+
360
+ # Stream the artifact to avoid buffering its full contents.
361
+ with path.open("rb") as source:
362
+ request = urllib.request.Request(
363
+ f"https://{host}{object_path}",
364
+ data=source,
365
+ method="PUT",
366
+ headers={
367
+ **headers,
368
+ "Authorization": authorization,
369
+ "Content-Length": str(size),
370
+ },
371
+ )
372
+ try:
373
+ with urllib.request.urlopen(request, timeout=300):
374
+ pass
375
+ except urllib.error.HTTPError as error:
376
+ raise NotarizationError(
377
+ f"Apple notarization payload upload failed with HTTP {error.code}"
378
+ ) from error
379
+ except OSError as error:
380
+ raise NotarizationError(
381
+ "Apple notarization payload upload failed"
382
+ ) from error
383
+
384
+
385
+ def write_developer_log(
386
+ submission_id: str, token: str, destination: Path | None
387
+ ) -> None:
388
+ """Write the submission's optional diagnostic report."""
389
+
390
+ if destination is None:
391
+ return
392
+ response = json_request(f"{APPLE_NOTARY_URL}/{submission_id}/logs", token)
393
+ try:
394
+ log_url = response["data"]["attributes"]["developerLogUrl"]
395
+ except (KeyError, TypeError) as error:
396
+ raise NotarizationError(
397
+ "Apple did not provide a notarization developer log"
398
+ ) from error
399
+ if not isinstance(log_url, str) or not log_url.startswith("https://"):
400
+ raise NotarizationError(
401
+ "Apple returned an invalid notarization developer log URL"
402
+ )
403
+ try:
404
+ # Do not forward unrelated authorization headers to the download URL.
405
+ with urllib.request.urlopen(log_url, timeout=60) as response:
406
+ destination.write_bytes(response.read())
407
+ except urllib.error.HTTPError as error:
408
+ raise NotarizationError(
409
+ f"Apple notarization developer log download failed with HTTP {error.code}"
410
+ ) from error
411
+ except OSError as error:
412
+ raise NotarizationError(
413
+ "Apple notarization developer log download failed"
414
+ ) from error
415
+
416
+
417
+ def notarize(
418
+ path: Path,
419
+ configuration: NotarizationConfiguration,
420
+ *,
421
+ max_wait_seconds: int,
422
+ report_log: Path | None,
423
+ ) -> str:
424
+ """Submit an artifact, upload it to Apple, and wait for a terminal result."""
425
+
426
+ issued_at = int(time.time())
427
+ token = create_apple_jwt(configuration, issued_at=issued_at)
428
+ file_digest = sha256_file(path)
429
+ # Create the submission before uploading the artifact.
430
+ response = json_request(
431
+ APPLE_NOTARY_URL,
432
+ token,
433
+ body={"sha256": file_digest, "submissionName": path.name},
434
+ )
435
+ try:
436
+ submission_id = response["data"]["id"]
437
+ upload_credentials = response["data"]["attributes"]
438
+ except (KeyError, TypeError) as error:
439
+ raise NotarizationError(
440
+ "Apple returned an invalid notarization submission"
441
+ ) from error
442
+ if not isinstance(submission_id, str) or not submission_id:
443
+ raise NotarizationError("Apple did not return a notarization submission ID")
444
+ if not isinstance(upload_credentials, dict):
445
+ raise NotarizationError(
446
+ "Apple returned invalid notarization upload credentials"
447
+ )
448
+
449
+ print(f"Uploading notarization submission {submission_id} for {path.name}")
450
+ upload_to_apple_s3(path, file_digest, upload_credentials)
451
+ # A monotonic deadline keeps the wait limit stable if the system clock changes.
452
+ deadline = time.monotonic() + max_wait_seconds
453
+
454
+ # Wait for completion, equivalent to `notarytool submit --wait`.
455
+ while True:
456
+ # Refresh the token before it expires.
457
+ if int(time.time()) >= issued_at + JWT_LIFETIME_SECONDS - 60:
458
+ issued_at = int(time.time())
459
+ token = create_apple_jwt(configuration, issued_at=issued_at)
460
+ result = json_request(f"{APPLE_NOTARY_URL}/{submission_id}", token)
461
+ try:
462
+ status = result["data"]["attributes"]["status"]
463
+ except (KeyError, TypeError) as error:
464
+ raise NotarizationError(
465
+ "Apple returned an invalid notarization status"
466
+ ) from error
467
+
468
+ if status in {"Accepted", "Invalid", "Rejected"}:
469
+ # Save diagnostics for both accepted and rejected submissions.
470
+ write_developer_log(submission_id, token, report_log)
471
+ message = f"Notarization submission {submission_id} completed with status {status}"
472
+ if status != "Accepted":
473
+ raise NotarizationError(message)
474
+ print(message)
475
+ return submission_id
476
+ if status != "In Progress":
477
+ raise NotarizationError(f"Unexpected notarization status: {status!r}")
478
+
479
+ remaining = deadline - time.monotonic()
480
+ if remaining <= 0:
481
+ raise NotarizationError(
482
+ f"Notarization submission {submission_id} exceeded {max_wait_seconds} seconds"
483
+ )
484
+ print(f"Notarization submission {submission_id} is still in progress")
485
+ time.sleep(min(POLL_INTERVAL_SECONDS, remaining))
486
+
487
+
488
+ def main() -> int:
489
+ """Run notarization using command-line arguments."""
490
+
491
+ parser = argparse.ArgumentParser()
492
+ parser.add_argument("--file", type=Path, required=True)
493
+ parser.add_argument("--report-log", type=Path)
494
+ parser.add_argument("--max-wait-seconds", type=int, default=600)
495
+ arguments = parser.parse_args()
496
+
497
+ try:
498
+ if not arguments.file.is_file():
499
+ raise NotarizationError(
500
+ f"Notarization payload does not exist: {arguments.file}"
501
+ )
502
+ if arguments.max_wait_seconds < 0:
503
+ raise NotarizationError("--max-wait-seconds must be non-negative")
504
+ if arguments.report_log is not None:
505
+ arguments.report_log.parent.mkdir(parents=True, exist_ok=True)
506
+ notarize(
507
+ arguments.file,
508
+ NotarizationConfiguration.from_environment(),
509
+ max_wait_seconds=arguments.max_wait_seconds,
510
+ report_log=arguments.report_log,
511
+ )
512
+ except NotarizationError as error:
513
+ print(f"Notarization failed: {error}", file=sys.stderr)
514
+ return 1
515
+ return 0
516
+
517
+
518
+ if __name__ == "__main__":
519
+ raise SystemExit(main())
.github/scripts/macos-signing/provisioned_macos_cli_package.py ADDED
@@ -0,0 +1,229 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Prepare and verify provisioned CLI bundles using an explicitly approved profile."""
2
+
3
+ import argparse
4
+ import hashlib
5
+ import json
6
+ import os
7
+ import plistlib
8
+ import re
9
+ import shutil
10
+ import subprocess
11
+ from dataclasses import dataclass
12
+ from datetime import datetime, timezone
13
+ from pathlib import Path
14
+
15
+ BUNDLE_ID = "com.openai.codex.cli"
16
+ APP = Path("CodexCLI.app")
17
+ EXECUTABLE = APP / "Contents/MacOS/codex"
18
+ SIGNING = Path(__file__).resolve().parent
19
+ HELPERS = ("bin/codex-code-mode-host", "codex-path/rg", "codex-resources/zsh/bin/zsh")
20
+ LAUNCHER = """#!/bin/sh
21
+ set -eu
22
+ entry="$0"
23
+ while [ -L "$entry" ]; do
24
+ parent=$(CDPATH= cd -P -- "$(dirname -- "$entry")" && pwd)
25
+ entry=$(readlink "$entry")
26
+ case "$entry" in /*) ;; *) entry="$parent/$entry" ;; esac
27
+ done
28
+ bin_dir=$(CDPATH= cd -P -- "$(dirname -- "$entry")" && pwd)
29
+ exec "$bin_dir/../CodexCLI.app/Contents/MacOS/codex" "$@"
30
+ """
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class ProfileConfiguration:
35
+ """Independent release expectations; never infer these from the signed package."""
36
+
37
+ profile: Path
38
+ profile_sha256: str
39
+ certificate_sha256: str
40
+ team_id: str
41
+
42
+ def __post_init__(self):
43
+ for name in ("profile_sha256", "certificate_sha256"):
44
+ if not re.fullmatch(r"[0-9a-f]{64}", getattr(self, name)):
45
+ raise ValueError(f"{name} must be a lowercase SHA-256 digest")
46
+ if not re.fullmatch(r"[A-Z0-9]{10}", self.team_id):
47
+ raise ValueError("team_id must be a ten-character Apple Team ID")
48
+
49
+ def validate_signing_certificate(self, certificate: Path):
50
+ certificate_der = subprocess.check_output(
51
+ ["openssl", "x509", "-in", str(certificate), "-outform", "DER"]
52
+ )
53
+ if hashlib.sha256(certificate_der).hexdigest() != self.certificate_sha256:
54
+ raise ValueError("AKV signing certificate is not authorized by the profile")
55
+
56
+
57
+ def load_profile(configuration: ProfileConfiguration):
58
+ # Pin the portal-approved CMS as well as checking its signature. -noverify
59
+ # skips OpenSSL's non-Apple trust store, not CMS signature verification.
60
+ if (
61
+ hashlib.sha256(configuration.profile.read_bytes()).hexdigest()
62
+ != configuration.profile_sha256
63
+ ):
64
+ raise ValueError("Provisioning profile changed; review and update its pin")
65
+ profile = plistlib.loads(
66
+ subprocess.check_output(
67
+ [
68
+ "openssl",
69
+ "cms",
70
+ "-verify",
71
+ "-inform",
72
+ "DER",
73
+ "-noverify",
74
+ "-in",
75
+ str(configuration.profile),
76
+ ]
77
+ )
78
+ )
79
+ validate_profile(profile, configuration)
80
+ return profile
81
+
82
+
83
+ def validate_profile(profile, configuration: ProfileConfiguration):
84
+ allowed = profile["Entitlements"]
85
+ if (
86
+ profile["TeamIdentifier"] != [configuration.team_id]
87
+ or profile["ApplicationIdentifierPrefix"] != [configuration.team_id]
88
+ or allowed["com.apple.application-identifier"]
89
+ != f"{configuration.team_id}.{BUNDLE_ID}"
90
+ or allowed["com.apple.developer.team-identifier"] != configuration.team_id
91
+ or allowed["keychain-access-groups"] != [f"{configuration.team_id}.*"]
92
+ or allowed.get("get-task-allow", False)
93
+ or not profile.get("ProvisionsAllDevices")
94
+ or "ProvisionedDevices" in profile
95
+ ):
96
+ raise ValueError("Profile does not authorize the CLI Developer ID identity")
97
+ now = datetime.now(timezone.utc).replace(tzinfo=None)
98
+ if not profile["CreationDate"] <= now < profile["ExpirationDate"]:
99
+ raise ValueError("Provisioning profile is not currently valid")
100
+ if [
101
+ hashlib.sha256(cert).hexdigest() for cert in profile["DeveloperCertificates"]
102
+ ] != [configuration.certificate_sha256]:
103
+ raise ValueError("Profile does not authorize the approved signing certificate")
104
+
105
+
106
+ def entitlements(configuration: ProfileConfiguration):
107
+ base = plistlib.loads((SIGNING / "codex.entitlements.plist").read_bytes())
108
+ return {
109
+ **base,
110
+ "com.apple.application-identifier": f"{configuration.team_id}.{BUNDLE_ID}",
111
+ "com.apple.developer.team-identifier": configuration.team_id,
112
+ "keychain-access-groups": [f"{configuration.team_id}.{BUNDLE_ID}"],
113
+ }
114
+
115
+
116
+ def prepare(package, reports, configuration: ProfileConfiguration):
117
+ load_profile(configuration)
118
+ metadata = json.loads((package / "codex-package.json").read_text())
119
+ if (
120
+ metadata["variant"] != "codex"
121
+ or metadata["layoutVersion"] != 1
122
+ or metadata["target"] not in ("aarch64-apple-darwin", "x86_64-apple-darwin")
123
+ or metadata["entrypoint"] != "bin/codex"
124
+ ):
125
+ raise ValueError("Expected a canonical macOS CLI package")
126
+ for relative in ("bin/codex", *HELPERS):
127
+ path = package / relative
128
+ if path.is_symlink() or not path.is_file() or not os.access(path, os.X_OK):
129
+ raise ValueError(f"Expected a regular executable: {relative}")
130
+ # Refuse to overwrite a previously prepared or signed bundle.
131
+ (package / EXECUTABLE).parent.mkdir(parents=True, exist_ok=False)
132
+ (package / "bin/codex").rename(package / EXECUTABLE)
133
+ (package / "bin/codex").write_text(LAUNCHER)
134
+ (package / "bin/codex").chmod(0o755)
135
+ contents = package / APP / "Contents"
136
+ shutil.copyfile(configuration.profile, contents / "embedded.provisionprofile")
137
+ (contents / "Info.plist").write_bytes(
138
+ plistlib.dumps(
139
+ {
140
+ "CFBundleIdentifier": BUNDLE_ID,
141
+ "CFBundleExecutable": "codex",
142
+ "CFBundleName": "Codex CLI",
143
+ "CFBundlePackageType": "APPL",
144
+ "CFBundleVersion": "1",
145
+ }
146
+ )
147
+ )
148
+ reports.mkdir(parents=True, exist_ok=True)
149
+ # Keep expected entitlements separate from the shared verifier's extracted
150
+ # codex-entitlements.plist, otherwise it would overwrite its own expectation.
151
+ (reports / "codex-provisioned-entitlements.plist").write_bytes(
152
+ plistlib.dumps(entitlements(configuration))
153
+ )
154
+
155
+
156
+ def verify(package, reports, expected_target, configuration: ProfileConfiguration):
157
+ """Check profile and certificate pins; the signing driver verifies code."""
158
+ load_profile(configuration)
159
+ if (
160
+ package / APP / "Contents/embedded.provisionprofile"
161
+ ).read_bytes() != configuration.profile.read_bytes():
162
+ raise ValueError("Embedded profile differs from the reviewed profile")
163
+ if (package / "bin/codex").read_text() != LAUNCHER:
164
+ raise ValueError("Unexpected CLI launcher")
165
+ metadata = json.loads((package / "codex-package.json").read_text())
166
+ if metadata["target"] != expected_target:
167
+ raise ValueError(
168
+ "Package target differs from the requested verification target"
169
+ )
170
+ reports.mkdir(parents=True, exist_ok=True)
171
+ (reports / "codex-provisioned-entitlements.plist").write_bytes(
172
+ plistlib.dumps(entitlements(configuration))
173
+ )
174
+ # sign_macos_cli_package.py owns shared architecture, signature and entitlement
175
+ # checks. Here, require the exact certificate authorized by the profile.
176
+ for relative in (EXECUTABLE, *(Path(helper) for helper in HELPERS)):
177
+ binary = package / relative
178
+ target = package / APP if relative == EXECUTABLE else binary
179
+ prefix = reports / f"{binary.name}-cert-"
180
+ subprocess.run(
181
+ ["codesign", "-d", f"--extract-certificates={prefix}", str(target)],
182
+ check=True,
183
+ )
184
+ if (
185
+ hashlib.sha256(Path(f"{prefix}0").read_bytes()).hexdigest()
186
+ != configuration.certificate_sha256
187
+ ):
188
+ raise ValueError(f"Unexpected signing certificate: {relative}")
189
+
190
+
191
+ def parse_profile_args(parser, configuration: ProfileConfiguration | None):
192
+ """Require independent profile expectations unless the caller supplies them."""
193
+ if configuration is None:
194
+ parser.add_argument("--profile", type=Path, required=True)
195
+ parser.add_argument("--profile-sha256", required=True)
196
+ parser.add_argument("--certificate-sha256", required=True)
197
+ parser.add_argument("--team-id", required=True)
198
+ args = parser.parse_args()
199
+ if configuration is None:
200
+ configuration = ProfileConfiguration(
201
+ args.profile, args.profile_sha256, args.certificate_sha256, args.team_id
202
+ )
203
+ return args, configuration
204
+
205
+
206
+ def main(configuration: ProfileConfiguration | None = None):
207
+ parser = argparse.ArgumentParser(description=__doc__)
208
+ parser.add_argument("operation", choices=("prepare", "verify", "validate-profile"))
209
+ parser.add_argument("--package", type=Path, default=Path("package"))
210
+ parser.add_argument("--reports", type=Path, default=Path("provisioned-reports"))
211
+ parser.add_argument("--certificate", type=Path)
212
+ parser.add_argument(
213
+ "--target", choices=("aarch64-apple-darwin", "x86_64-apple-darwin")
214
+ )
215
+ args, configuration = parse_profile_args(parser, configuration)
216
+ if args.certificate:
217
+ configuration.validate_signing_certificate(args.certificate)
218
+ if args.operation == "validate-profile":
219
+ load_profile(configuration)
220
+ elif args.operation == "prepare":
221
+ prepare(args.package, args.reports, configuration)
222
+ else:
223
+ if not args.target:
224
+ parser.error("--target is required for verification")
225
+ verify(args.package, args.reports, args.target, configuration)
226
+
227
+
228
+ if __name__ == "__main__":
229
+ main()
.github/scripts/macos-signing/sign_macos_cli_package.py ADDED
@@ -0,0 +1,177 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Sign and verify macOS CLI packages using the shared AKV and native tools."""
2
+
3
+ import argparse
4
+ import os
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import provisioned_macos_cli_package as bundle
10
+
11
+
12
+ def main(configuration: bundle.ProfileConfiguration | None = None):
13
+ parser = argparse.ArgumentParser(description=__doc__)
14
+ parser.add_argument("operation", choices=("sign", "verify"))
15
+ parser.add_argument("package", type=Path)
16
+ args, configuration = bundle.parse_profile_args(parser, configuration)
17
+ package = args.package.resolve(strict=True)
18
+ if not package.is_dir():
19
+ parser.error("package must be an extracted CLI package directory")
20
+ repo_root = os.environ.get("CODEX_REPO_ROOT")
21
+ if not repo_root:
22
+ parser.error("CODEX_REPO_ROOT must be set")
23
+ reports = Path(repo_root) / "signing-verification"
24
+ reports.mkdir(parents=True, exist_ok=True)
25
+ provisioned = os.environ.get("PROVISIONED_MACOS", "false") == "true"
26
+ if provisioned:
27
+ if args.operation == "sign":
28
+ configuration.validate_signing_certificate(
29
+ Path(os.environ["OAI_AKV_SIGNING_CERTIFICATE_PEM"])
30
+ )
31
+ bundle.prepare(package, reports, configuration)
32
+ else:
33
+ bundle.verify(package, reports, os.environ["TARGET"], configuration)
34
+
35
+ for relative in ("bin/codex", *bundle.HELPERS):
36
+ binary = package / relative
37
+ name = binary.name
38
+ target = binary
39
+ identifier = name
40
+ entitlements = bundle.SIGNING / f"{name}.entitlements.plist"
41
+ if provisioned and relative == "bin/codex":
42
+ binary = package / bundle.EXECUTABLE
43
+ target = package / bundle.APP
44
+ identifier = bundle.BUNDLE_ID
45
+ entitlements = reports / "codex-provisioned-entitlements.plist"
46
+ if args.operation == "sign":
47
+ command = [
48
+ "bash",
49
+ str(bundle.SIGNING / "sign_macos_code.sh"),
50
+ "--target",
51
+ str(target),
52
+ "--identity",
53
+ "unused",
54
+ "--deep",
55
+ "false",
56
+ "--options",
57
+ "runtime",
58
+ "--timestamp",
59
+ "true",
60
+ ]
61
+ if relative.startswith("bin/"):
62
+ command.extend(
63
+ ["--identifier", identifier, "--entitlements", str(entitlements)]
64
+ )
65
+ else:
66
+ command.extend(["--identifier", f"com.openai.codex.{name}"])
67
+ subprocess.run(command, check=True)
68
+ with (reports / f"{name}-signature.yaml").open("wb") as output:
69
+ subprocess.run(
70
+ ["rcodesign", "print-signature-info", str(binary)],
71
+ stdout=output,
72
+ check=True,
73
+ )
74
+ if not provisioned:
75
+ subprocess.run(
76
+ [
77
+ "bash",
78
+ str(bundle.SIGNING / "notarize_macos_binary_with_akv.sh"),
79
+ "--binary",
80
+ str(binary),
81
+ "--report-dir",
82
+ str(reports / name),
83
+ ],
84
+ check=True,
85
+ )
86
+ else:
87
+ target_triple = os.environ["TARGET"]
88
+ architectures = {
89
+ "aarch64-apple-darwin": "arm64",
90
+ "x86_64-apple-darwin": "x86_64",
91
+ }
92
+ if target_triple not in architectures:
93
+ raise ValueError(f"Unexpected macOS target: {target_triple}")
94
+ subprocess.run(
95
+ ["lipo", str(binary), "-verify_arch", architectures[target_triple]],
96
+ check=True,
97
+ )
98
+ subprocess.run(
99
+ [
100
+ "codesign",
101
+ "--verify",
102
+ "--strict",
103
+ "--verbose=2",
104
+ "--test-requirement",
105
+ # Require Apple's Developer ID Application certificate chain.
106
+ (
107
+ "=anchor apple generic"
108
+ " and certificate 1[field.1.2.840.113635.100.6.2.6] exists"
109
+ " and certificate leaf[field.1.2.840.113635.100.6.1.13] exists"
110
+ ),
111
+ str(target),
112
+ ],
113
+ check=True,
114
+ )
115
+ signature = reports / f"{name}-signature.txt"
116
+ with signature.open("wb") as output:
117
+ subprocess.run(
118
+ ["codesign", "-d", "--verbose=4", str(target)],
119
+ stderr=output,
120
+ check=True,
121
+ )
122
+ actual = reports / f"{name}-entitlements.plist"
123
+ with actual.open("wb") as output:
124
+ subprocess.run(
125
+ ["codesign", "-d", "--entitlements", ":-", str(target)],
126
+ stdout=output,
127
+ check=True,
128
+ )
129
+ if relative.startswith("bin/"):
130
+ expected = reports / f"{name}-expected.plist"
131
+ subprocess.run(
132
+ [
133
+ "plutil",
134
+ "-convert",
135
+ "xml1",
136
+ "-o",
137
+ str(expected),
138
+ str(entitlements),
139
+ ],
140
+ check=True,
141
+ )
142
+ subprocess.run(["plutil", "-convert", "xml1", str(actual)], check=True)
143
+ subprocess.run(["diff", "-u", str(expected), str(actual)], check=True)
144
+ elif actual.stat().st_size:
145
+ raise ValueError(
146
+ f"Bundled helper {name} must not have signing entitlements"
147
+ )
148
+
149
+ if provisioned and args.operation == "sign":
150
+ archive = Path(os.environ["RUNNER_TEMP"]) / "provisioned-cli.zip"
151
+ subprocess.run(["zip", "-q", "-r", str(archive), "."], cwd=package, check=True)
152
+ command = [
153
+ sys.executable,
154
+ str(bundle.SIGNING / "notarize_with_akv.py"),
155
+ "--file",
156
+ str(archive),
157
+ "--report-log",
158
+ str(reports / "notarization-developer-log.json"),
159
+ "--max-wait-seconds",
160
+ "1200",
161
+ ]
162
+ with (
163
+ (reports / "notarization.log").open("w") as output,
164
+ subprocess.Popen(
165
+ command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
166
+ ) as process,
167
+ ):
168
+ assert process.stdout is not None
169
+ for line in process.stdout:
170
+ print(line, end="", flush=True)
171
+ output.write(line)
172
+ if returncode := process.wait():
173
+ raise subprocess.CalledProcessError(returncode, command)
174
+
175
+
176
+ if __name__ == "__main__":
177
+ main()
.github/scripts/macos-signing/sign_macos_code.sh ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ # Small compatibility wrapper around native codesign and rcodesign.
4
+ #
5
+ # Existing packaging scripts call this instead of choosing a signing backend
6
+ # directly. OAI_CODESIGN_BACKEND=akv-pkcs11 routes signing through rcodesign
7
+ # while preserving the option, entitlement, identifier, timestamp, and deep
8
+ # signing surface used by the native codesign path.
9
+
10
+ set -euo pipefail
11
+
12
+ usage() {
13
+ cat >&2 <<'EOF'
14
+ Usage: sign_macos_code.sh --target PATH --identity IDENTITY [options]
15
+
16
+ Options:
17
+ --deep true|false
18
+ --entitlements PATH
19
+ --identifier IDENTIFIER
20
+ --identity IDENTITY
21
+ --options FLAGS
22
+ --target PATH
23
+ --timestamp true|false|none
24
+ EOF
25
+ }
26
+
27
+ target=""
28
+ identity=""
29
+ options=""
30
+ entitlements_file=""
31
+ identifier=""
32
+ deep="false"
33
+ timestamp="true"
34
+
35
+ while [[ $# -gt 0 ]]; do
36
+ case "$1" in
37
+ --deep)
38
+ deep="${2:-}"
39
+ shift 2
40
+ ;;
41
+ --entitlements)
42
+ entitlements_file="${2:-}"
43
+ shift 2
44
+ ;;
45
+ --identifier)
46
+ identifier="${2:-}"
47
+ shift 2
48
+ ;;
49
+ --identity)
50
+ identity="${2:-}"
51
+ shift 2
52
+ ;;
53
+ --options)
54
+ options="${2:-}"
55
+ shift 2
56
+ ;;
57
+ --target)
58
+ target="${2:-}"
59
+ shift 2
60
+ ;;
61
+ --timestamp)
62
+ timestamp="${2:-}"
63
+ shift 2
64
+ ;;
65
+ -h|--help)
66
+ usage
67
+ exit 0
68
+ ;;
69
+ *)
70
+ echo "Unknown signing argument: $1" >&2
71
+ usage
72
+ exit 2
73
+ ;;
74
+ esac
75
+ done
76
+
77
+ if [[ -z "$target" ]]; then
78
+ echo "--target is required." >&2
79
+ usage
80
+ exit 2
81
+ fi
82
+
83
+ if [[ ! -e "$target" ]]; then
84
+ echo "Signing target does not exist: $target" >&2
85
+ exit 1
86
+ fi
87
+
88
+ case "$deep" in
89
+ true|false) ;;
90
+ *)
91
+ echo "--deep must be true or false, got '$deep'." >&2
92
+ exit 2
93
+ ;;
94
+ esac
95
+
96
+ case "$timestamp" in
97
+ true|false|none) ;;
98
+ *)
99
+ echo "--timestamp must be true, false, or none, got '$timestamp'." >&2
100
+ exit 2
101
+ ;;
102
+ esac
103
+
104
+ sign_with_codesign() {
105
+ if [[ -z "$identity" ]]; then
106
+ echo "Native codesign requires --identity." >&2
107
+ exit 2
108
+ fi
109
+
110
+ local -a args
111
+ args=(--force)
112
+
113
+ if [[ "$deep" == "true" ]]; then
114
+ args+=(--deep)
115
+ fi
116
+
117
+ if [[ -n "$options" ]]; then
118
+ args+=(--options "$options")
119
+ fi
120
+
121
+ case "$timestamp" in
122
+ true)
123
+ args+=(--timestamp)
124
+ ;;
125
+ false|none)
126
+ args+=(--timestamp=none)
127
+ ;;
128
+ esac
129
+
130
+ if [[ -n "$entitlements_file" ]]; then
131
+ args+=(--entitlements "$entitlements_file")
132
+ fi
133
+
134
+ if [[ -n "$identifier" ]]; then
135
+ args+=(--identifier "$identifier")
136
+ fi
137
+
138
+ args+=(--sign "$identity" "$target")
139
+ codesign "${args[@]}"
140
+ }
141
+
142
+ append_rcodesign_flags() {
143
+ local raw_options="$1"
144
+ local option=""
145
+
146
+ if [[ -z "$raw_options" ]]; then
147
+ return 0
148
+ fi
149
+
150
+ IFS=',' read -ra split_options <<< "$raw_options"
151
+ for option in "${split_options[@]}"; do
152
+ option="${option//[[:space:]]/}"
153
+ [[ -z "$option" ]] && continue
154
+
155
+ case "$option" in
156
+ host|hard|kill|expires|restrict|library|runtime|linker-signed)
157
+ rcodesign_args+=(--code-signature-flags "$option")
158
+ ;;
159
+ *)
160
+ echo "Unsupported rcodesign code signature option: $option" >&2
161
+ exit 2
162
+ ;;
163
+ esac
164
+ done
165
+ }
166
+
167
+ rcodesign_options_require_notarization() {
168
+ local raw_options="$1"
169
+ local option=""
170
+
171
+ if [[ -z "$raw_options" || "$timestamp" != "true" ]]; then
172
+ return 1
173
+ fi
174
+
175
+ IFS=',' read -ra split_options <<< "$raw_options"
176
+ for option in "${split_options[@]}"; do
177
+ option="${option//[[:space:]]/}"
178
+ if [[ "$option" == "runtime" ]]; then
179
+ return 0
180
+ fi
181
+ done
182
+
183
+ return 1
184
+ }
185
+
186
+ sign_with_rcodesign() {
187
+ : "${OAI_AKV_PKCS11_LIBRARY:?OAI_AKV_PKCS11_LIBRARY is required for AKV PKCS11 signing.}"
188
+ : "${OAI_AKV_SIGNING_CERTIFICATE_PEM:?OAI_AKV_SIGNING_CERTIFICATE_PEM is required for AKV PKCS11 signing.}"
189
+ : "${OAI_AKV_KEY_LABEL:?OAI_AKV_KEY_LABEL is required for AKV PKCS11 signing.}"
190
+
191
+ if ! command -v rcodesign >/dev/null 2>&1; then
192
+ echo "rcodesign was not found on PATH." >&2
193
+ exit 1
194
+ fi
195
+
196
+ local -a rcodesign_args
197
+ rcodesign_args=(
198
+ sign
199
+ --config-file /dev/null
200
+ --pkcs11-library "$OAI_AKV_PKCS11_LIBRARY"
201
+ --pkcs11-certificate-file "$OAI_AKV_SIGNING_CERTIFICATE_PEM"
202
+ --pkcs11-key-label "$OAI_AKV_KEY_LABEL"
203
+ )
204
+
205
+ if [[ "$deep" == "false" ]]; then
206
+ rcodesign_args+=(--shallow)
207
+ fi
208
+
209
+ case "$timestamp" in
210
+ true)
211
+ ;;
212
+ false|none)
213
+ rcodesign_args+=(--timestamp-url none)
214
+ ;;
215
+ esac
216
+
217
+ append_rcodesign_flags "$options"
218
+ if rcodesign_options_require_notarization "$options"; then
219
+ rcodesign_args+=(--for-notarization)
220
+ fi
221
+
222
+ if [[ -n "$entitlements_file" ]]; then
223
+ rcodesign_args+=(--entitlements-xml-file "$entitlements_file")
224
+ fi
225
+
226
+ if [[ -n "$identifier" ]]; then
227
+ rcodesign_args+=(--binary-identifier "$identifier")
228
+ fi
229
+
230
+ rcodesign_args+=("$target")
231
+ rcodesign "${rcodesign_args[@]}"
232
+ }
233
+
234
+ case "${OAI_CODESIGN_BACKEND:-codesign}" in
235
+ codesign|"")
236
+ sign_with_codesign
237
+ ;;
238
+ akv-pkcs11)
239
+ sign_with_rcodesign
240
+ ;;
241
+ *)
242
+ echo "Unsupported OAI_CODESIGN_BACKEND: ${OAI_CODESIGN_BACKEND}" >&2
243
+ exit 2
244
+ ;;
245
+ esac
.github/scripts/macos-signing/test_notarize_with_akv.py ADDED
@@ -0,0 +1,257 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import contextlib
2
+ import hashlib
3
+ import io
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ import tempfile
9
+ import unittest
10
+ from pathlib import Path
11
+ from unittest.mock import patch
12
+
13
+ SIGNING_DIRECTORY = Path(__file__).resolve().parent
14
+ sys.path.insert(0, str(SIGNING_DIRECTORY))
15
+ import notarize_with_akv as notary # noqa: E402
16
+
17
+ CONFIGURATION = notary.NotarizationConfiguration(
18
+ "01234567-89ab-cdef-0123-456789abcdef",
19
+ "APPLEKEY01",
20
+ "notary-vault",
21
+ "example-signing-key",
22
+ "0123456789abcdef" * 2,
23
+ )
24
+ ENVIRONMENT = {
25
+ "AZURE_KEYVAULT_NAME": CONFIGURATION.vault_name,
26
+ "APPLE_NOTARIZATION_AKV_KEY_NAME": CONFIGURATION.vault_key_name,
27
+ }
28
+ CREDENTIALS = {
29
+ "awsAccessKeyId": "temporary-access-key",
30
+ "awsSecretAccessKey": "temporary-secret-key",
31
+ "awsSessionToken": "temporary-session-token",
32
+ "bucket": "apple-notary-bucket",
33
+ "object": "uploads/release artifact.dmg",
34
+ }
35
+
36
+
37
+ def response(payload):
38
+ return io.BytesIO(
39
+ json.dumps(payload).encode() if isinstance(payload, dict) else payload
40
+ )
41
+
42
+
43
+ def azure_response(payload):
44
+ return subprocess.CompletedProcess([], 0, stdout=json.dumps(payload), stderr="")
45
+
46
+
47
+ class NotarizationTest(unittest.TestCase):
48
+ def test_validates_the_apple_key_tags_and_pins_its_version(self) -> None:
49
+ metadata = {
50
+ "id": CONFIGURATION.versioned_key_id,
51
+ "apple_key_id": "APPLEKEY01",
52
+ "apple_issuer_id": CONFIGURATION.issuer_id,
53
+ }
54
+ with (
55
+ patch.dict(os.environ, ENVIRONMENT, clear=True),
56
+ patch.object(
57
+ notary.subprocess,
58
+ "run",
59
+ return_value=azure_response(metadata),
60
+ ) as show,
61
+ ):
62
+ self.assertEqual(
63
+ notary.NotarizationConfiguration.from_environment(),
64
+ CONFIGURATION,
65
+ )
66
+ show.return_value = azure_response({**metadata, "apple_key_id": None})
67
+ with self.assertRaisesRegex(notary.NotarizationError, "apple-key-id tag"):
68
+ notary.NotarizationConfiguration.from_environment()
69
+ for issuer_id in (None, "", " "):
70
+ with self.subTest(issuer_id=issuer_id):
71
+ show.return_value = azure_response(
72
+ {**metadata, "apple_issuer_id": issuer_id}
73
+ )
74
+ with self.assertRaisesRegex(
75
+ notary.NotarizationError, "apple-issuer-id tag"
76
+ ):
77
+ notary.NotarizationConfiguration.from_environment()
78
+ show.return_value = azure_response(
79
+ {**metadata, "apple_issuer_id": "invalid-issuer"}
80
+ )
81
+ with self.assertRaisesRegex(notary.NotarizationError, "valid UUID"):
82
+ notary.NotarizationConfiguration.from_environment()
83
+ self.assertIn(
84
+ '{id:key.kid,apple_key_id:tags."apple-key-id",'
85
+ 'apple_issuer_id:tags."apple-issuer-id"}',
86
+ show.call_args.args[0],
87
+ )
88
+
89
+ invalid_environment = {
90
+ **ENVIRONMENT,
91
+ "APPLE_NOTARIZATION_AKV_KEY_NAME": "../bad",
92
+ }
93
+ with (
94
+ patch.dict(os.environ, invalid_environment, clear=True),
95
+ self.assertRaisesRegex(notary.NotarizationError, "key name"),
96
+ ):
97
+ notary.NotarizationConfiguration.from_environment()
98
+
99
+ def test_signs_the_notary_scoped_digest_with_the_pinned_key(self) -> None:
100
+ signature = bytes(range(64))
101
+ payload = {
102
+ "kid": CONFIGURATION.versioned_key_id,
103
+ "value": notary.base64url_encode(signature),
104
+ }
105
+ with patch.object(
106
+ notary.subprocess, "run", return_value=azure_response(payload)
107
+ ) as sign:
108
+ token = notary.create_apple_jwt(CONFIGURATION, issued_at=1_780_000_000)
109
+ header, claims, encoded_signature = token.split(".")
110
+ self.assertEqual(
111
+ json.loads(notary.base64url_decode(header))["kid"], "APPLEKEY01"
112
+ )
113
+ self.assertEqual(
114
+ json.loads(notary.base64url_decode(claims))["iss"], CONFIGURATION.issuer_id
115
+ )
116
+ self.assertEqual(
117
+ json.loads(notary.base64url_decode(claims))["scope"],
118
+ ["/notary/v2"],
119
+ )
120
+ self.assertEqual(notary.base64url_decode(encoded_signature), signature)
121
+ command = sign.call_args.args[0]
122
+ body = json.loads(command[command.index("--body") + 1])
123
+ self.assertEqual(body["alg"], "ES256")
124
+ self.assertEqual(
125
+ notary.base64url_decode(body["value"]),
126
+ hashlib.sha256(f"{header}.{claims}".encode()).digest(),
127
+ )
128
+
129
+ def test_rejects_wrong_key_versions_and_invalid_signatures(self) -> None:
130
+ cases = (
131
+ (
132
+ CONFIGURATION.versioned_key_id + "wrong",
133
+ bytes(64),
134
+ "unexpected key version",
135
+ ),
136
+ (CONFIGURATION.versioned_key_id, b"short", "64 JOSE"),
137
+ )
138
+ for key_id, signature, message in cases:
139
+ payload = {"kid": key_id, "value": notary.base64url_encode(signature)}
140
+ with (
141
+ self.subTest(message=message),
142
+ patch.object(
143
+ notary.subprocess,
144
+ "run",
145
+ return_value=azure_response(payload),
146
+ ),
147
+ self.assertRaisesRegex(notary.NotarizationError, message),
148
+ ):
149
+ notary.create_apple_jwt(CONFIGURATION, issued_at=1)
150
+
151
+ def test_uploads_polls_and_retains_sanitized_apple_diagnostics(self) -> None:
152
+ signature = azure_response(
153
+ {
154
+ "kid": CONFIGURATION.versioned_key_id,
155
+ "value": notary.base64url_encode(bytes(64)),
156
+ }
157
+ )
158
+ responses = [
159
+ response({"data": {"id": "submission-1", "attributes": CREDENTIALS}}),
160
+ response(b""),
161
+ response({"data": {"attributes": {"status": "In Progress"}}}),
162
+ response({"data": {"attributes": {"status": "Accepted"}}}),
163
+ response(
164
+ {
165
+ "data": {
166
+ "attributes": {
167
+ "developerLogUrl": "https://logs.example.com/log"
168
+ }
169
+ }
170
+ }
171
+ ),
172
+ response(b'{"status":"Accepted"}'),
173
+ ]
174
+ with tempfile.TemporaryDirectory() as directory:
175
+ artifact, log = (
176
+ Path(directory) / "codex.zip",
177
+ Path(directory) / "notary-log.json",
178
+ )
179
+ artifact.write_bytes(b"signed release binary")
180
+ output = io.StringIO()
181
+ with (
182
+ patch.object(notary.subprocess, "run", return_value=signature),
183
+ patch.object(
184
+ notary.urllib.request, "urlopen", side_effect=responses
185
+ ) as requests,
186
+ patch.object(notary.time, "sleep") as sleep,
187
+ contextlib.redirect_stdout(output),
188
+ ):
189
+ submission = notary.notarize(
190
+ artifact, CONFIGURATION, max_wait_seconds=600, report_log=log
191
+ )
192
+ self.assertEqual(submission, "submission-1")
193
+ self.assertEqual(json.loads(log.read_text()), {"status": "Accepted"})
194
+ sleep.assert_called_once_with(10)
195
+ upload = requests.call_args_list[1].args[0]
196
+ self.assertEqual(upload.get_method(), "PUT")
197
+ self.assertNotIn("temporary-secret-key", upload.get_header("Authorization"))
198
+ self.assertNotIn("temporary-secret-key", output.getvalue())
199
+
200
+
201
+ class NotarizationWrapperTest(unittest.TestCase):
202
+ def test_binary_and_dmg_wrappers_preserve_notarization_contracts(
203
+ self,
204
+ ) -> None:
205
+ with tempfile.TemporaryDirectory() as directory:
206
+ root, tools = Path(directory), Path(directory) / "tools"
207
+ tools.mkdir()
208
+ call_log = root / "calls.txt"
209
+ environment = {
210
+ **os.environ,
211
+ **ENVIRONMENT,
212
+ "PATH": f"{tools}:{os.environ['PATH']}",
213
+ "RUNNER_TEMP": str(root),
214
+ "CALL_LOG": str(call_log),
215
+ }
216
+ for name in ("az", "python3", "rcodesign"):
217
+ body = (
218
+ "exit 0"
219
+ if name == "az"
220
+ else f'printf "{name} %s\\n" "$*" >> "$CALL_LOG"'
221
+ )
222
+ executable = tools / name
223
+ executable.write_text(
224
+ f"#!/usr/bin/env bash\nset -euo pipefail\n{body}\n"
225
+ )
226
+ executable.chmod(0o755)
227
+ for kind in ("binary", "dmg"):
228
+ with self.subTest(kind=kind):
229
+ artifact = root / ("codex.dmg" if kind == "dmg" else "codex")
230
+ artifact.write_bytes(b"signed release artifact")
231
+ subprocess.run(
232
+ [
233
+ str(
234
+ SIGNING_DIRECTORY / f"notarize_macos_{kind}_with_akv.sh"
235
+ ),
236
+ f"--{kind}",
237
+ str(artifact),
238
+ "--report-dir",
239
+ str(root / "report"),
240
+ ],
241
+ env=environment,
242
+ check=True,
243
+ capture_output=True,
244
+ text=True,
245
+ )
246
+ calls = call_log.read_text().splitlines()
247
+ self.assertIn("notarize_with_akv.py --file", calls[0])
248
+ if kind == "dmg":
249
+ self.assertEqual(calls[1], f"rcodesign staple {artifact}")
250
+ else:
251
+ self.assertEqual(len(calls), 1)
252
+ self.assertEqual(list(root.rglob("*.p8")), [])
253
+ call_log.unlink()
254
+
255
+
256
+ if __name__ == "__main__":
257
+ unittest.main()
.github/scripts/macos-signing/test_provisioned_macos_cli_package.py ADDED
@@ -0,0 +1,217 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Exercise profile validation, relocation and signing without release credentials."""
2
+
3
+ import copy
4
+ import hashlib
5
+ import json
6
+ import plistlib
7
+ import shutil
8
+ import ssl
9
+ import subprocess
10
+ import tarfile
11
+ import tempfile
12
+ import unittest
13
+ from dataclasses import replace
14
+ from datetime import datetime, timedelta, timezone
15
+ from pathlib import Path
16
+
17
+ import provisioned_macos_cli_package as bundle
18
+
19
+
20
+ class ProvisioningTestCase(unittest.TestCase):
21
+ @classmethod
22
+ def setUpClass(cls):
23
+ directory = tempfile.TemporaryDirectory()
24
+ cls.addClassCleanup(directory.cleanup)
25
+ root = Path(directory.name)
26
+ key = root / "test-key.pem"
27
+ certificate = root / "test-certificate.pem"
28
+ subprocess.run(
29
+ [
30
+ "openssl",
31
+ "req",
32
+ "-x509",
33
+ "-newkey",
34
+ "rsa:2048",
35
+ "-nodes",
36
+ "-keyout",
37
+ str(key),
38
+ "-out",
39
+ str(certificate),
40
+ "-days",
41
+ "1",
42
+ "-subj",
43
+ "/CN=CLI provisioning test/OU=TESTTEAM01",
44
+ ],
45
+ check=True,
46
+ capture_output=True,
47
+ )
48
+ cert_der = ssl.PEM_cert_to_DER_cert(certificate.read_text())
49
+ now = datetime.now(timezone.utc).replace(tzinfo=None, microsecond=0)
50
+ cls.profile = {
51
+ "TeamIdentifier": ["TESTTEAM01"],
52
+ "ApplicationIdentifierPrefix": ["TESTTEAM01"],
53
+ "Entitlements": {
54
+ "com.apple.application-identifier": "TESTTEAM01.com.openai.codex.cli",
55
+ "com.apple.developer.team-identifier": "TESTTEAM01",
56
+ "keychain-access-groups": ["TESTTEAM01.*"],
57
+ },
58
+ "CreationDate": now - timedelta(days=1),
59
+ "ExpirationDate": now + timedelta(days=1),
60
+ "ProvisionsAllDevices": True,
61
+ "DeveloperCertificates": [cert_der],
62
+ }
63
+ payload = root / "profile.plist"
64
+ payload.write_bytes(plistlib.dumps(cls.profile))
65
+ profile_path = root / "test.provisionprofile"
66
+ subprocess.run(
67
+ [
68
+ "openssl",
69
+ "cms",
70
+ "-sign",
71
+ "-binary",
72
+ "-nodetach",
73
+ "-outform",
74
+ "DER",
75
+ "-in",
76
+ str(payload),
77
+ "-signer",
78
+ str(certificate),
79
+ "-inkey",
80
+ str(key),
81
+ "-out",
82
+ str(profile_path),
83
+ ],
84
+ check=True,
85
+ capture_output=True,
86
+ )
87
+ cls.configuration = bundle.ProfileConfiguration(
88
+ profile=profile_path,
89
+ profile_sha256=hashlib.sha256(profile_path.read_bytes()).hexdigest(),
90
+ certificate_sha256=hashlib.sha256(cert_der).hexdigest(),
91
+ team_id="TESTTEAM01",
92
+ )
93
+
94
+
95
+ class ProvisionedCliTests(ProvisioningTestCase):
96
+ def test_profile_configuration_fails_closed(self):
97
+ self.assertEqual(bundle.load_profile(self.configuration), self.profile)
98
+ for changes in (
99
+ {"profile_sha256": "0" * 64},
100
+ {"certificate_sha256": "0" * 64},
101
+ {"team_id": "OTHERTEAM1"},
102
+ {"profile_sha256": ""},
103
+ {"certificate_sha256": ""},
104
+ {"team_id": ""},
105
+ ):
106
+ with self.subTest(changes=changes), self.assertRaises(ValueError):
107
+ bundle.load_profile(replace(self.configuration, **changes))
108
+
109
+ def test_rejects_profile_with_wrong_identity_certificate_or_validity(self):
110
+ for field, value in (
111
+ ("TeamIdentifier", ["OTHERTEAM"]),
112
+ ("DeveloperCertificates", [b"another certificate"]),
113
+ (
114
+ "ExpirationDate",
115
+ datetime(2000, 1, 1, tzinfo=timezone.utc).replace(tzinfo=None),
116
+ ),
117
+ (
118
+ "CreationDate",
119
+ datetime(2099, 1, 1, tzinfo=timezone.utc).replace(tzinfo=None),
120
+ ),
121
+ ("ProvisionsAllDevices", False),
122
+ ("ProvisionedDevices", ["device"]),
123
+ ):
124
+ with self.subTest(field=field):
125
+ profile = copy.deepcopy(self.profile)
126
+ profile[field] = value
127
+ with self.assertRaises(ValueError):
128
+ bundle.validate_profile(profile, self.configuration)
129
+ for field, value in (
130
+ (
131
+ "com.apple.application-identifier",
132
+ f"{self.configuration.team_id}.another.app",
133
+ ),
134
+ ("keychain-access-groups", [f"{self.configuration.team_id}.another.app"]),
135
+ ("get-task-allow", True),
136
+ ):
137
+ with self.subTest(field=field):
138
+ profile = copy.deepcopy(self.profile)
139
+ profile["Entitlements"][field] = value
140
+ with self.assertRaises(ValueError):
141
+ bundle.validate_profile(profile, self.configuration)
142
+
143
+ def test_archive_relocation_copy_and_symlink_preserve_launcher(self):
144
+ with tempfile.TemporaryDirectory() as directory:
145
+ root = Path(directory)
146
+ package = root / "original package"
147
+ metadata = {
148
+ "variant": "codex",
149
+ "layoutVersion": 1,
150
+ "target": "aarch64-apple-darwin",
151
+ "entrypoint": "bin/codex",
152
+ }
153
+ for relative in ("bin/codex", *bundle.HELPERS):
154
+ path = package / relative
155
+ path.parent.mkdir(parents=True, exist_ok=True)
156
+ path.write_text('#!/bin/sh\nprintf "%s\\n" "$@"\nexit 23\n')
157
+ path.chmod(0o755)
158
+ (package / "codex-package.json").write_text(json.dumps(metadata))
159
+ original_binary = (package / "bin/codex").read_bytes()
160
+ bundle.prepare(package, root / "reports", self.configuration)
161
+ with self.assertRaises(FileExistsError):
162
+ bundle.prepare(package, root / "reports", self.configuration)
163
+ with self.assertRaisesRegex(ValueError, "Package target differs"):
164
+ bundle.verify(
165
+ package, root / "reports", "x86_64-apple-darwin", self.configuration
166
+ )
167
+ self.assertEqual(
168
+ (package / bundle.EXECUTABLE).read_bytes(), original_binary
169
+ )
170
+ self.assertEqual(
171
+ json.loads((package / "codex-package.json").read_text()), metadata
172
+ )
173
+
174
+ archive_path = root / "package.tar.gz"
175
+ with tarfile.open(archive_path, "w:gz") as archive:
176
+ archive.add(package, arcname=".")
177
+ relocated = root / "relocated package"
178
+ with tarfile.open(archive_path) as archive:
179
+ archive.extractall(relocated, filter="data")
180
+ shutil.rmtree(package)
181
+ # npm currently dereferences symlinks when copying packages. The
182
+ # launcher remains a script and the provisioned executable stays put.
183
+ copied = root / "copied package"
184
+ shutil.copytree(relocated, copied)
185
+ link = root / "installed codex"
186
+ link.symlink_to(Path("copied package/bin/codex"))
187
+ absolute_link = root / "absolute codex"
188
+ absolute_link.symlink_to(link)
189
+ for entry in (
190
+ relocated / "bin/codex",
191
+ copied / "bin/codex",
192
+ link,
193
+ absolute_link,
194
+ ):
195
+ with self.subTest(entry=entry):
196
+ result = subprocess.run(
197
+ [str(entry), "space argument", "*", "--flag"],
198
+ cwd="/",
199
+ capture_output=True,
200
+ text=True,
201
+ check=False,
202
+ )
203
+ self.assertEqual(
204
+ (result.returncode, result.stdout, result.stderr),
205
+ (23, "space argument\n*\n--flag\n", ""),
206
+ )
207
+ (copied / bundle.APP / "Contents/embedded.provisionprofile").write_bytes(
208
+ b"tampered"
209
+ )
210
+ with self.assertRaisesRegex(ValueError, "Embedded profile differs"):
211
+ bundle.verify(
212
+ copied, root / "reports", "aarch64-apple-darwin", self.configuration
213
+ )
214
+
215
+
216
+ if __name__ == "__main__":
217
+ unittest.main()
.github/scripts/macos-signing/test_sign_macos_cli_package.py ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Exercise package signing and native verification with generated credentials."""
2
+
3
+ import itertools
4
+ import json
5
+ import os
6
+ import shutil
7
+ import ssl
8
+ import subprocess
9
+ import sys
10
+ import tempfile
11
+ import unittest
12
+ import zipfile
13
+ from pathlib import Path
14
+
15
+ import provisioned_macos_cli_package as bundle
16
+ from test_provisioned_macos_cli_package import ProvisioningTestCase
17
+
18
+ NATIVE_TOOL = """#!/usr/bin/env python3
19
+ import json, os, plistlib, sys
20
+ from pathlib import Path
21
+ name = Path(sys.argv[0]).name
22
+ args = sys.argv[1:]
23
+ with open(os.environ["CALL_LOG"], "a") as log:
24
+ log.write(json.dumps([name, *args]) + "\\n")
25
+ if name == "codesign":
26
+ for arg in args:
27
+ if arg.startswith("--extract-certificates="):
28
+ Path(arg.split("=", 1)[1] + "0").write_bytes(Path(os.environ["MOCK_CERTIFICATE"]).read_bytes())
29
+ if "--test-requirement" in args and os.environ.get("MOCK_WRONG_SIGNING_IDENTITY"):
30
+ print("code failed to satisfy specified code requirement(s)", file=sys.stderr)
31
+ sys.exit(1)
32
+ if "--entitlements" in args:
33
+ target = Path(args[-1]).name
34
+ if target == "CodexCLI.app":
35
+ source = Path(os.environ["CODEX_REPO_ROOT"]) / "signing-verification/codex-provisioned-entitlements.plist"
36
+ elif target in ("codex", "codex-code-mode-host"):
37
+ source = Path(os.environ["MOCK_SIGNING_SCRIPTS"]) / (target + ".entitlements.plist")
38
+ else:
39
+ sys.exit(0)
40
+ if os.environ.get("MOCK_WRONG_ENTITLEMENTS"):
41
+ sys.stdout.buffer.write(plistlib.dumps({"unexpected": True}))
42
+ else:
43
+ sys.stdout.buffer.write(source.read_bytes())
44
+ elif name == "plutil":
45
+ source = Path(args[-1])
46
+ target = Path(args[args.index("-o") + 1]) if "-o" in args else source
47
+ target.write_bytes(plistlib.dumps(plistlib.loads(source.read_bytes())))
48
+ """
49
+
50
+
51
+ class MacosSigningTests(ProvisioningTestCase):
52
+ def fixture(self, root, provisioned, target="aarch64-apple-darwin"):
53
+ source = Path(bundle.__file__).parent
54
+ common = Path(os.path.commonpath((source, bundle.SIGNING)))
55
+ helpers = root / source.relative_to(common)
56
+ scripts = root / bundle.SIGNING.relative_to(common)
57
+ helpers.mkdir(parents=True, exist_ok=True)
58
+ scripts.mkdir(parents=True, exist_ok=True)
59
+ for name in ("provisioned_macos_cli_package.py", "sign_macos_cli_package.py"):
60
+ shutil.copyfile(source / name, helpers / name)
61
+ for path in bundle.SIGNING.glob("*.entitlements.plist"):
62
+ shutil.copyfile(path, scripts / path.name)
63
+ (root / "record.py").write_text(
64
+ "import json, os, sys\n"
65
+ "with open(os.environ['CALL_LOG'], 'a') as log:\n"
66
+ " log.write(json.dumps(sys.argv[1:]) + '\\n')\n"
67
+ "if os.environ.get('FAIL_TOOL') == sys.argv[1]:\n"
68
+ " print('fixture stdout', flush=True)\n"
69
+ " print('fixture stderr', file=sys.stderr, flush=True)\n"
70
+ " sys.exit(17)\n"
71
+ )
72
+ for name in ("sign_macos_code.sh", "notarize_macos_binary_with_akv.sh"):
73
+ (scripts / name).write_text(
74
+ 'python3 "$CODEX_REPO_ROOT/record.py" "$(basename "$0")" "$@"\n'
75
+ )
76
+ (scripts / "notarize_with_akv.py").write_text(
77
+ "import os, runpy, sys\n"
78
+ "sys.argv.insert(1, 'notarize_with_akv.py')\n"
79
+ "runpy.run_path(os.environ['CODEX_REPO_ROOT'] + '/record.py')\n"
80
+ )
81
+ for name in ("rcodesign", "codesign", "lipo", "plutil"):
82
+ path = root / name
83
+ path.write_text(NATIVE_TOOL)
84
+ path.chmod(0o755)
85
+ certificate_der = self.profile["DeveloperCertificates"][0]
86
+ certificate = root / "certificate.pem"
87
+ certificate.write_text(ssl.DER_cert_to_PEM_cert(certificate_der))
88
+ (root / "certificate.der").write_bytes(certificate_der)
89
+ package = root / "package with spaces"
90
+ for relative in ("bin/codex", *bundle.HELPERS):
91
+ path = package / relative
92
+ path.parent.mkdir(parents=True, exist_ok=True)
93
+ path.write_text("#!/bin/sh\nexit 0\n")
94
+ path.chmod(0o755)
95
+ (package / "codex-package.json").write_text(
96
+ json.dumps(
97
+ {
98
+ "variant": "codex",
99
+ "layoutVersion": 1,
100
+ "target": target,
101
+ "entrypoint": "bin/codex",
102
+ }
103
+ )
104
+ )
105
+ env = {
106
+ key: value
107
+ for key, value in os.environ.items()
108
+ if key not in ("GITHUB_WORKSPACE", "CODEX_PROVISIONING_HELPER")
109
+ }
110
+ env.update(
111
+ CODEX_REPO_ROOT=str(root),
112
+ RUNNER_TEMP=str(root),
113
+ TARGET=target,
114
+ PATH=f"{root}{os.pathsep}{os.environ['PATH']}",
115
+ CALL_LOG=str(root / "calls.jsonl"),
116
+ PROVISIONED_MACOS=str(provisioned).lower(),
117
+ OAI_AKV_SIGNING_CERTIFICATE_PEM=str(certificate),
118
+ MOCK_CERTIFICATE=str(root / "certificate.der"),
119
+ MOCK_SIGNING_SCRIPTS=str(scripts),
120
+ )
121
+ return package, helpers / "sign_macos_cli_package.py", env
122
+
123
+ def run_driver(self, driver, operation, package, env):
124
+ return subprocess.run(
125
+ [
126
+ sys.executable,
127
+ str(driver),
128
+ operation,
129
+ str(package),
130
+ "--profile",
131
+ str(self.configuration.profile),
132
+ "--profile-sha256",
133
+ self.configuration.profile_sha256,
134
+ "--certificate-sha256",
135
+ self.configuration.certificate_sha256,
136
+ "--team-id",
137
+ self.configuration.team_id,
138
+ ],
139
+ env=env,
140
+ capture_output=True,
141
+ text=True,
142
+ timeout=30,
143
+ check=False,
144
+ )
145
+
146
+ def test_standard_and_provisioned_signing_and_verification(self):
147
+ for provisioned, target in itertools.product(
148
+ (False, True), ("aarch64-apple-darwin", "x86_64-apple-darwin")
149
+ ):
150
+ with (
151
+ self.subTest(provisioned=provisioned, target=target),
152
+ tempfile.TemporaryDirectory() as directory,
153
+ ):
154
+ root = Path(directory).resolve()
155
+ package, driver, env = self.fixture(root, provisioned, target)
156
+ result = self.run_driver(driver, "sign", package, env)
157
+ self.assertEqual(result.returncode, 0, result.stderr)
158
+ result = self.run_driver(driver, "verify", package, env)
159
+ self.assertEqual(result.returncode, 0, result.stderr)
160
+ calls = [
161
+ json.loads(line)
162
+ for line in (root / "calls.jsonl").read_text().splitlines()
163
+ ]
164
+ signing = [call for call in calls if call[0] == "sign_macos_code.sh"]
165
+ self.assertEqual(
166
+ [call[call.index("--target") + 1] for call in signing],
167
+ [
168
+ str(package / relative)
169
+ for relative in (
170
+ bundle.APP if provisioned else "bin/codex",
171
+ *bundle.HELPERS,
172
+ )
173
+ ],
174
+ )
175
+ self.assertEqual(
176
+ signing[0][signing[0].index("--entitlements") + 1],
177
+ str(
178
+ root
179
+ / "signing-verification/codex-provisioned-entitlements.plist"
180
+ )
181
+ if provisioned
182
+ else str(
183
+ Path(env["MOCK_SIGNING_SCRIPTS"]) / "codex.entitlements.plist"
184
+ ),
185
+ )
186
+ self.assertEqual(
187
+ ["--entitlements" in call for call in signing],
188
+ [True, True, False, False],
189
+ )
190
+ self.assertEqual(
191
+ [call[0] for call in calls if call[0].startswith("notarize")],
192
+ ["notarize_with_akv.py"]
193
+ if provisioned
194
+ else ["notarize_macos_binary_with_akv.sh"] * 4,
195
+ )
196
+ self.assertEqual(
197
+ [call[1:] for call in calls if call[0] == "lipo"],
198
+ [
199
+ [
200
+ str(package / relative),
201
+ "-verify_arch",
202
+ "arm64" if target.startswith("aarch64") else "x86_64",
203
+ ]
204
+ for relative in (
205
+ bundle.EXECUTABLE if provisioned else "bin/codex",
206
+ *bundle.HELPERS,
207
+ )
208
+ ],
209
+ )
210
+ if provisioned:
211
+ with zipfile.ZipFile(root / "provisioned-cli.zip") as archive:
212
+ self.assertEqual(
213
+ archive.read(
214
+ "CodexCLI.app/Contents/embedded.provisionprofile"
215
+ ),
216
+ self.configuration.profile.read_bytes(),
217
+ )
218
+ self.assertEqual(
219
+ archive.read("bin/codex").decode(), bundle.LAUNCHER
220
+ )
221
+ self.assertTrue(
222
+ all(
223
+ helper in archive.namelist()
224
+ for helper in bundle.HELPERS
225
+ )
226
+ )
227
+
228
+ def test_signing_and_notarization_failures_stop_the_driver(self):
229
+ for tool in ("sign_macos_code.sh", "notarize_with_akv.py"):
230
+ with self.subTest(tool=tool), tempfile.TemporaryDirectory() as directory:
231
+ root = Path(directory).resolve()
232
+ package, driver, env = self.fixture(root, True)
233
+ result = self.run_driver(
234
+ driver, "sign", package, {**env, "FAIL_TOOL": tool}
235
+ )
236
+ self.assertNotEqual(result.returncode, 0)
237
+ calls = [
238
+ json.loads(line)
239
+ for line in (root / "calls.jsonl").read_text().splitlines()
240
+ ]
241
+ self.assertEqual(calls[-1][0], tool)
242
+ if tool == "notarize_with_akv.py":
243
+ self.assertEqual(
244
+ (root / "signing-verification/notarization.log").read_text(),
245
+ "fixture stdout\nfixture stderr\n",
246
+ )
247
+ self.assertIn("fixture stdout\nfixture stderr\n", result.stdout)
248
+ else:
249
+ self.assertFalse((root / "provisioned-cli.zip").exists())
250
+
251
+ def test_verification_rejects_wrong_signing_identity_and_entitlements(self):
252
+ for provisioned, failure in itertools.product(
253
+ (False, True), ("MOCK_WRONG_SIGNING_IDENTITY", "MOCK_WRONG_ENTITLEMENTS")
254
+ ):
255
+ with (
256
+ self.subTest(provisioned=provisioned, failure=failure),
257
+ tempfile.TemporaryDirectory() as directory,
258
+ ):
259
+ root = Path(directory).resolve()
260
+ package, driver, env = self.fixture(root, provisioned)
261
+ signed = self.run_driver(driver, "sign", package, env)
262
+ self.assertEqual(signed.returncode, 0, signed.stderr)
263
+ result = self.run_driver(
264
+ driver, "verify", package, {**env, failure: "true"}
265
+ )
266
+ self.assertNotEqual(result.returncode, 0)
267
+ if failure == "MOCK_WRONG_SIGNING_IDENTITY":
268
+ self.assertIn(
269
+ "code failed to satisfy specified code requirement(s)",
270
+ result.stderr,
271
+ )
272
+ else:
273
+ self.assertIn("unexpected", result.stdout)
274
+
275
+ def test_cli_requires_independent_profile_configuration(self):
276
+ with tempfile.TemporaryDirectory() as directory:
277
+ result = subprocess.run(
278
+ [
279
+ sys.executable,
280
+ str(Path(bundle.__file__).with_name("sign_macos_cli_package.py")),
281
+ "verify",
282
+ directory,
283
+ ],
284
+ capture_output=True,
285
+ text=True,
286
+ check=False,
287
+ )
288
+ self.assertEqual(result.returncode, 2)
289
+ self.assertIn("the following arguments are required", result.stderr)
290
+
291
+
292
+ if __name__ == "__main__":
293
+ unittest.main()
docs/CLA.md ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Individual Contributor License Agreement (v1.0, OpenAI)
2
+
3
+ _Based on the Apache Software Foundation Individual CLA v 2.2._
4
+
5
+ By commenting **“I have read the CLA Document and I hereby sign the CLA”**
6
+ on a Pull Request, **you (“Contributor”) agree to the following terms** for any
7
+ past and future “Contributions” submitted to the **OpenAI Codex CLI project
8
+ (the “Project”)**.
9
+
10
+ ---
11
+
12
+ ## 1. Definitions
13
+
14
+ - **“Contribution”** – any original work of authorship submitted to the Project
15
+ (code, documentation, designs, etc.).
16
+ - **“You” / “Your”** – the individual (or legal entity) posting the acceptance
17
+ comment.
18
+
19
+ ## 2. Copyright License
20
+
21
+ You grant **OpenAI, Inc.** and all recipients of software distributed by the
22
+ Project a perpetual, worldwide, non‑exclusive, royalty‑free, irrevocable
23
+ license to reproduce, prepare derivative works of, publicly display, publicly
24
+ perform, sublicense, and distribute Your Contributions and derivative works.
25
+
26
+ ## 3. Patent License
27
+
28
+ You grant **OpenAI, Inc.** and all recipients of the Project a perpetual,
29
+ worldwide, non‑exclusive, royalty‑free, irrevocable (except as below) patent
30
+ license to make, have made, use, sell, offer to sell, import, and otherwise
31
+ transfer Your Contributions alone or in combination with the Project.
32
+
33
+ If any entity brings patent litigation alleging that the Project or a
34
+ Contribution infringes a patent, the patent licenses granted by You to that
35
+ entity under this CLA terminate.
36
+
37
+ ## 4. Representations
38
+
39
+ 1. You are legally entitled to grant the licenses above.
40
+ 2. Each Contribution is either Your original creation or You have authority to
41
+ submit it under this CLA.
42
+ 3. Your Contributions are provided **“AS IS”** without warranties of any kind.
43
+ 4. You will notify the Project if any statement above becomes inaccurate.
44
+
45
+ ## 5. Miscellany
46
+
47
+ This Agreement is governed by the laws of the **State of California**, USA,
48
+ excluding its conflict‑of‑laws rules. If any provision is held unenforceable,
49
+ the remaining provisions remain in force.
docs/agents_md.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # AGENTS.md
2
+
3
+ For information about AGENTS.md, see [this documentation](https://developers.openai.com/codex/guides/agents-md).
docs/authentication.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Authentication
2
+
3
+ For information about Codex CLI authentication, see [this documentation](https://developers.openai.com/codex/auth).
docs/config.md ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Configuration
2
+
3
+ For basic configuration instructions, see [this documentation](https://developers.openai.com/codex/config-basic).
4
+
5
+ For advanced configuration instructions, see [this documentation](https://developers.openai.com/codex/config-advanced).
6
+
7
+ For a full configuration reference, see [this documentation](https://developers.openai.com/codex/config-reference).
8
+
9
+ ## Lifecycle hooks
10
+
11
+ Admins can set top-level `allow_managed_hooks_only = true` in
12
+ `requirements.toml` to ignore user, project, and session hook configs while
13
+ still allowing managed hooks from requirements and managed config layers. This
14
+ setting is only supported in `requirements.toml`; putting it in `config.toml`
15
+ does not enable managed-hooks-only mode.
docs/contributing.md ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Contributing
2
+
3
+ We welcome community contributions through the [openai/codex issue tracker](https://github.com/openai/codex/issues). Bug reports, root-cause analyses, and feature requests help us understand what matters most and improve Codex.
4
+
5
+ **We do not accept external code contributions or pull requests.**
6
+
7
+ ### Why we do not accept external code contributions
8
+
9
+ Effective changes to Codex require architectural context, an understanding of system-level constraints, and visibility into the project's roadmap. External pull requests often focus on issues that are lower priority, affect a small number of users, or need substantial changes to fit the broader system. Reviewing and iterating on those changes can take more time than implementing a fix directly, diverting attention from higher-priority work.
10
+
11
+ Community expertise is most valuable when shared through detailed bug reports, reproduction steps, logs, root-cause analysis, and design discussions in issues. Understanding the problem, identifying the right solution, and prioritizing the work are typically the hard parts; implementation is comparatively straightforward with the help of Codex itself.
12
+
13
+ For these reasons, we focus community contributions on issue reports, analysis, and feedback, while the Codex team handles code changes.
14
+
15
+ ### Reporting bugs
16
+
17
+ Before opening a new issue, search the issue tracker to see whether the problem has already been reported. If it has, add any new information to the existing issue.
18
+
19
+ When reporting a bug, include as much relevant detail as possible:
20
+
21
+ - Clear, detailed steps to reproduce the problem.
22
+ - Expected and actual behavior.
23
+ - Your Codex version, operating system, and other relevant environment details.
24
+ - Logs, error messages, or other diagnostic information, with sensitive information removed.
25
+ - Root-cause analysis, technical observations, or potential approaches to a fix, if available.
26
+
27
+ ### Requesting features
28
+
29
+ Open a feature request in the issue tracker, or upvote an existing request that describes the same need. Explain your use case, the behavior you would like, and why it would improve your workflow.
30
+
31
+ ### Community values
32
+
33
+ - **Be kind and inclusive.** Treat others with respect; we follow the [Contributor Covenant](https://www.contributor-covenant.org/).
34
+ - **Assume good intent.** Written communication is hard, so err on the side of generosity.
35
+ - **Share what you learn.** Reproduction details, logs, and analysis help the entire community.
36
+
37
+ ### Security
38
+
39
+ If you discover a security vulnerability, follow the [security policy](../SECURITY.md) instead of reporting it in a public issue.
docs/example-config.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Sample configuration
2
+
3
+ For a sample configuration file, see [this documentation](https://developers.openai.com/codex/config-sample).
docs/exec.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Non-interactive mode
2
+
3
+ For information about non-interactive mode, see [this documentation](https://developers.openai.com/codex/noninteractive).
docs/execpolicy.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Execution policy
2
+
3
+ For an overview of execution policy rules, see [this documentation](https://developers.openai.com/codex/exec-policy).
docs/getting-started.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Getting started with Codex CLI
2
+
3
+ For an overview of Codex CLI features, see [this documentation](https://developers.openai.com/codex/cli/features#running-in-interactive-mode).
docs/install.md ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Installing & building
2
+
3
+ ### System requirements
4
+
5
+ | Requirement | Details |
6
+ | --------------------------- | --------------------------------------------------------------- |
7
+ | Operating systems | macOS 12+, Ubuntu 20.04+/Debian 10+, or Windows 11 **via WSL2** |
8
+ | Git (optional, recommended) | 2.23+ for built-in PR helpers |
9
+ | RAM | 4-GB minimum (8-GB recommended) |
10
+
11
+ ### DotSlash
12
+
13
+ The GitHub Release also contains a [DotSlash](https://dotslash-cli.com/) file for the Codex CLI named `codex`. Using a DotSlash file makes it possible to make a lightweight commit to source control to ensure all contributors use the same version of an executable, regardless of what platform they use for development.
14
+
15
+ ### Build from source
16
+
17
+ ```bash
18
+ # Clone the repository and navigate to the root of the Cargo workspace.
19
+ git clone https://github.com/openai/codex.git
20
+ cd codex/codex-rs
21
+
22
+ # Install the Rust toolchain, if necessary.
23
+ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
24
+ source "$HOME/.cargo/env"
25
+ rustup component add rustfmt
26
+ rustup component add clippy
27
+ # Install helper tools used by the workspace justfile:
28
+ cargo install --locked just
29
+ # DotSlash fetches pinned development tools such as buildifier on first use.
30
+ cargo install --locked dotslash
31
+ # Install nextest for the `just test` helper.
32
+ cargo install --locked cargo-nextest
33
+
34
+ # Build Codex.
35
+ cargo build
36
+
37
+ # Launch the TUI with a sample prompt.
38
+ cargo run --bin codex -- "explain this codebase to me"
39
+
40
+ # After making changes, use the root justfile helpers (they default to codex-rs):
41
+ just fmt
42
+ just fix -p <crate-you-touched>
43
+
44
+ # Run the relevant tests (project-specific is fastest), for example:
45
+ just test -p codex-tui
46
+ # `just test` runs the test suite via nextest:
47
+ just test
48
+ # Avoid `--all-features` for routine local runs because it increases build
49
+ # time and `target/` disk usage by compiling additional feature combinations.
50
+ ```
51
+
52
+ ## Tracing / verbose logging
53
+
54
+ Codex is written in Rust, so it honors the `RUST_LOG` environment variable to configure its logging behavior.
55
+
56
+ The TUI records diagnostics in bounded local stores by default. Set `log_dir` explicitly to enable a plaintext TUI log for a run:
57
+
58
+ ```bash
59
+ codex -c log_dir=./.codex-log
60
+ tail -F ./.codex-log/codex-tui.log
61
+ ```
62
+
63
+ The non-interactive mode (`codex exec`) defaults to `RUST_LOG=error`, but messages are printed inline, so there is no need to monitor a separate file.
64
+
65
+ See the Rust documentation on [`RUST_LOG`](https://docs.rs/env_logger/latest/env_logger/#enabling-logging) for more information on the configuration options.
docs/license.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ ## License
2
+
3
+ This repository is licensed under the [Apache-2.0 License](../LICENSE).
docs/open-source-fund.md ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ ## Codex open source fund
2
+
3
+ We're excited to launch a **$1 million initiative** supporting open source projects that use Codex CLI and other OpenAI models.
4
+
5
+ - Grants are awarded up to **$25,000** API credits.
6
+ - Applications are reviewed **on a rolling basis**.
7
+
8
+ **Interested? [Apply here](https://openai.com/form/codex-open-source-fund/).**
docs/sandbox.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ ## Sandbox & approvals
2
+
3
+ For information about Codex sandboxing and approvals, see [this documentation](https://developers.openai.com/codex/security).
docs/skills.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Skills
2
+
3
+ For information about skills, refer to [this documentation](https://developers.openai.com/codex/skills).
docs/slash_commands.md ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # Slash commands
2
+
3
+ For an overview of Codex CLI slash commands, see [this documentation](https://developers.openai.com/codex/cli/slash-commands).
patches/BUILD.bazel ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ exports_files([
2
+ "abseil_windows_gnullvm_thread_identity.patch",
3
+ "bzip2_windows_stack_args.patch",
4
+ "llvm_rusty_v8_custom_libcxx.patch",
5
+ "llvm_windows_arm64_powl.patch",
6
+ "llvm_windows_mingw_compat.patch",
7
+ "rules_rust_build_script_tools_transition.patch",
8
+ "rules_rust_group_build_script_arg_files.patch",
9
+ "rules_rust_windows_execroot_separators.patch",
10
+ "rules_rust_windows_msvc_direct_link_args.patch",
11
+ "rules_rust_windows_process_wrapper_skip_temp_outputs.patch",
12
+ "rules_cc_rusty_v8_custom_libcxx.patch",
13
+ "rules_foreign_cc_make_cppflags.patch",
14
+ "rules_foreign_cc_make_xcompile.patch",
15
+ "rules_foreign_cc_make_msvc_stdint.patch",
16
+ "rules_rs_build_script_deps_annotation.patch",
17
+ "rules_rs_windows_msvc_linker.patch",
18
+ "rules_rs_zlib_snapshot_urls.patch",
19
+ "rusty_v8_prebuilt_out_dir.patch",
20
+ "ring_windows_msvc_include_dirs.patch",
21
+ "tikv-jemalloc-sys_make_env.patch",
22
+ "tikv-jemalloc-sys_dependency_flags.patch",
23
+ "v8_bazel_rules.patch",
24
+ "v8_module_deps.patch",
25
+ "v8_source_portability.patch",
26
+ "webrtc-sys_hermetic_darwin_sysroot.patch",
27
+ "windows-link.patch",
28
+ "windows-support-remote-case-aliases.patch",
29
+ "windows-support-native-tools.patch",
30
+ "xz_windows_stack_args.patch",
31
+ "zstd-sys_windows_msvc_include_dirs.patch",
32
+ ])
patches/abseil_windows_gnullvm_thread_identity.patch ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # What: avoid MinGW pthread TLS linkage in Abseil thread identity selection.
2
+ # Scope: one MinGW-specific preprocessor branch in Abseil's internal
3
+ # thread-identity mode selection; no other Abseil behavior changes.
4
+
5
+ diff --git a/absl/base/internal/thread_identity.h b/absl/base/internal/thread_identity.h
6
+ --- a/absl/base/internal/thread_identity.h
7
+ +++ b/absl/base/internal/thread_identity.h
8
+ @@ -217,6 +217,8 @@
9
+ #error ABSL_THREAD_IDENTITY_MODE cannot be directly set
10
+ #elif defined(ABSL_FORCE_THREAD_IDENTITY_MODE)
11
+ #define ABSL_THREAD_IDENTITY_MODE ABSL_FORCE_THREAD_IDENTITY_MODE
12
+ +#elif defined(__MINGW32__)
13
+ +#define ABSL_THREAD_IDENTITY_MODE ABSL_THREAD_IDENTITY_MODE_USE_CPP11
14
+ #elif defined(_WIN32) && !defined(__MINGW32__)
15
+ #define ABSL_THREAD_IDENTITY_MODE ABSL_THREAD_IDENTITY_MODE_USE_CPP11
16
+ #elif defined(__APPLE__) && defined(ABSL_HAVE_THREAD_LOCAL)
patches/bzip2_windows_stack_args.patch ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diff --git a/BUILD.bazel b/BUILD.bazel
2
+ --- a/BUILD.bazel
3
+ +++ b/BUILD.bazel
4
+ @@ -28,4 +28,11 @@ cc_library(
5
+ defines = [
6
+ "_FILE_OFFSET_BITS=64",
7
+ ],
8
+ + copts = select({
9
+ + "@platforms//os:windows": [
10
+ + "-fno-stack-protector",
11
+ + "-mno-stack-arg-probe",
12
+ + ],
13
+ + "//conditions:default": [],
14
+ + }),
15
+ includes = ["."],
16
+ diff --git a/MODULE.bazel b/MODULE.bazel
17
+ --- a/MODULE.bazel
18
+ +++ b/MODULE.bazel
19
+ @@ -4,3 +4,4 @@ module(
20
+ )
21
+
22
+ bazel_dep(name = "rules_cc", version = "0.0.10")
23
+ +bazel_dep(name = "platforms", version = "1.0.0")
patches/llvm_rusty_v8_custom_libcxx.patch ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diff --git a/toolchain/BUILD.bazel b/toolchain/BUILD.bazel
2
+ index fa00156236..c9e4348172 100644
3
+ --- a/toolchain/BUILD.bazel
4
+ +++ b/toolchain/BUILD.bazel
5
+ @@ -1,5 +1,6 @@
6
+ load("@bazel_lib//:bzl_library.bzl", "bzl_library")
7
+ +load("@bazel_skylib//lib:selects.bzl", "selects")
8
+ load("@bazel_skylib//rules:common_settings.bzl", "string_flag", "string_setting")
9
+ load("@rules_cc//cc/toolchains:actions.bzl", "cc_action_type_set")
10
+ load("@rules_cc//cc/toolchains:artifacts.bzl", "cc_artifact_name_pattern")
11
+ load("@rules_cc//cc/toolchains/impl:documented_api.bzl", "cc_args_list")
12
+ @@ -64,6 +65,15 @@ config_setting(
13
+ visibility = ["//visibility:public"],
14
+ )
15
+ -
16
+ +
17
+ +selects.config_setting_group(
18
+ + name = "runtimes_all_with_rusty_v8_custom_libcxx",
19
+ + match_all = [
20
+ + ":runtimes_all",
21
+ + "@@//third_party/v8:use_rusty_v8_custom_libcxx",
22
+ + ],
23
+ + visibility = ["//visibility:public"],
24
+ +)
25
+ +
26
+ config_setting(
27
+ name = "runtimes_stage1",
28
+ flag_values = {
29
+ @@ -270,11 +280,16 @@ cc_args_list(
30
+
31
+ # TODO(cerisier): extract those into proper semantic args list.
32
+ # TODO(zbarsky): This must match llvm/toolchains/llvm.bzl
33
+ +_DEFAULT_CXXSTDLIB_HEADER_ARGS = select({
34
+ + "@@//third_party/v8:use_rusty_v8_custom_libcxx": [],
35
+ + "//conditions:default": [
36
+ + "//toolchain/args:cxxstdlib_headers_include_search_paths",
37
+ + ],
38
+ +})
39
+ +
40
+ cc_args_list(
41
+ name = "linux_toolchain_args",
42
+ - args = [
43
+ - "//toolchain/args:cxxstdlib_headers_include_search_paths",
44
+ - ] + select({
45
+ + args = _DEFAULT_CXXSTDLIB_HEADER_ARGS + select({
46
+ "//platforms/config:musl": [
47
+ "//toolchain/args/linux:kernel_headers_include_search_paths",
48
+ "//toolchain/args/linux:musl_libc_headers_include_search_paths",
49
+ @@ -302,8 +317,7 @@ cc_args_list(
50
+ # TODO(zbarsky): This must match llvm/toolchains/llvm.bzl
51
+ cc_args_list(
52
+ name = "windows_toolchain_args",
53
+ - args = [
54
+ - "//toolchain/args:cxxstdlib_headers_include_search_paths",
55
+ + args = _DEFAULT_CXXSTDLIB_HEADER_ARGS + [
56
+ "//toolchain/args/windows:mingw_headers_include_search_paths",
57
+ ],
58
+ )
59
+ diff --git a/toolchain/llvm/llvm.bzl b/toolchain/llvm/llvm.bzl
60
+ index d36d8b94bd..900aa908d5 100644
61
+ --- a/toolchain/llvm/llvm.bzl
62
+ +++ b/toolchain/llvm/llvm.bzl
63
+ @@ -297,18 +297,23 @@ def declare_llvm_targets(*, suffix = ""):
64
+ ],
65
+ )
66
+
67
+ + default_cxxstdlib_target_headers = select({
68
+ + "@llvm//toolchain:runtimes_all_with_rusty_v8_custom_libcxx": [],
69
+ + "@llvm//toolchain:runtimes_stage1": [],
70
+ + "@llvm//toolchain:runtimes_stage1_hosted": [],
71
+ + "@llvm//toolchain:runtimes_none": [],
72
+ + "//conditions:default": [
73
+ + "@llvm//runtimes/cxxstdlib:headers_include_search_directory",
74
+ + "@llvm//runtimes/cxxstdlib:abi_headers_include_search_directory",
75
+ + ],
76
+ + })
77
+ +
78
+ # This must match //toolchain:linux_toolchain_args
79
+ include_path(
80
+ name = "linux_target_headers",
81
+ srcs = [
82
+ ":builtin_resource_dir",
83
+ - ] + select({
84
+ - "@llvm//toolchain:runtimes_all": [
85
+ - "@llvm//runtimes/cxxstdlib:headers_include_search_directory",
86
+ - "@llvm//runtimes/cxxstdlib:abi_headers_include_search_directory",
87
+ - ],
88
+ - "//conditions:default": [],
89
+ - }) + [
90
+ + ] + default_cxxstdlib_target_headers + [
91
+ "@kernel_headers//:kernel_headers_directory",
92
+ "@llvm//sanitizers:sanitizers_headers_include_search_directory",
93
+ ] + select({
94
+ @@ -324,13 +329,7 @@ def declare_llvm_targets(*, suffix = ""):
95
+ name = "windows_target_headers",
96
+ srcs = [
97
+ ":builtin_resource_dir",
98
+ - ] + select({
99
+ - "@llvm//toolchain:runtimes_all": [
100
+ - "@llvm//runtimes/cxxstdlib:headers_include_search_directory",
101
+ - "@llvm//runtimes/cxxstdlib:abi_headers_include_search_directory",
102
+ - ],
103
+ - "//conditions:default": [],
104
+ - }) + [
105
+ + ] + default_cxxstdlib_target_headers + [
106
+ "@mingw//:mingw_generated_headers_crt_directory",
107
+ "@mingw//:mingw_w64_headers_include_directory",
108
+ "@mingw//:mingw_w64_headers_crt_directory",
patches/llvm_windows_arm64_powl.patch ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # What: provide and link MinGW's ARM64 long-double `powl` compatibility shim.
2
+ # Why: Windows ARM64 uses 64-bit long doubles, but LLVM's Bazel source list
3
+ # omits the upstream `powl -> pow` implementation and its default link flags
4
+ # omit the MinGW extension archive that owns the shim.
5
+
6
+ diff --git a/runtimes/mingw/crt_sources.bzl b/runtimes/mingw/crt_sources.bzl
7
+ --- a/runtimes/mingw/crt_sources.bzl
8
+ +++ b/runtimes/mingw/crt_sources.bzl
9
+ @@ -420,4 +420,5 @@
10
+ MINGWEX_ARM64_SRCS = [
11
+ + "math/arm-common/powl.c",
12
+ "math/sincos.c",
13
+ "math/sincosf.c",
14
+ "math/arm-common/sincosl.c",
15
+ diff --git a/toolchain/args/windows/BUILD.bazel b/toolchain/args/windows/BUILD.bazel
16
+ --- a/toolchain/args/windows/BUILD.bazel
17
+ +++ b/toolchain/args/windows/BUILD.bazel
18
+ @@ -45,5 +45,6 @@ cc_args(
19
+ args = [
20
+ "-L{mingw_import_library_search_path}",
21
+ "-L{mingw_crt_library_search_path}",
22
+ -
23
+ + "-lmingwex",
24
+ +
25
+ # Clang will respect the user's chosen CRT variant.
patches/llvm_windows_mingw_compat.patch ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # What: provide MinGW compatibility archives required by Windows gnullvm links.
2
+ # Why: clang and prebuilt MSVC archives can request these names even though the
3
+ # hermetic MinGW runtime does not publish them by default.
4
+
5
+ diff --git a/runtimes/mingw/BUILD.bazel b/runtimes/mingw/BUILD.bazel
6
+ --- a/runtimes/mingw/BUILD.bazel
7
+ +++ b/runtimes/mingw/BUILD.bazel
8
+ @@ -386,14 +386,42 @@ stub_library(
9
+ name = "stdc++",
10
+ )
11
+
12
+ +# Clang may inject -lssp and -lssp_nonshared for windows-gnu links.
13
+ +# Provide compatibility archives in the MinGW runtime search directory.
14
+ +stub_library(
15
+ + name = "ssp",
16
+ +)
17
+ +
18
+ +stub_library(
19
+ + name = "ssp_nonshared",
20
+ +)
21
+ +
22
+ +# Prebuilt MSVC archives can carry /DEFAULTLIB:libcmt.lib and
23
+ +# /DEFAULTLIB:oldnames.lib directives. Provide MinGW-compatible stand-ins so
24
+ +# lld can satisfy the lookup under windows-gnullvm.
25
+ +stub_library(
26
+ + name = "libcmt",
27
+ +)
28
+ +
29
+ +copy_file(
30
+ + name = "oldnames",
31
+ + src = ":moldname",
32
+ + out = "liboldnames.a",
33
+ + visibility = ["//visibility:public"],
34
+ +)
35
+ +
36
+ copy_to_directory(
37
+ name = "mingw_crt_library_search_directory",
38
+ srcs = [
39
+ + ":libcmt",
40
+ ":m",
41
+ ":mingw32",
42
+ ":mingwex",
43
+ ":moldname",
44
+ + ":oldnames",
45
+ ":pthread",
46
+ + ":ssp",
47
+ + ":ssp_nonshared",
48
+ ":stdc++",
49
+ ":uuid",
50
+ ":winpthread",
patches/ring_windows_msvc_include_dirs.patch ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diff --git a/build.rs b/build.rs
2
+ --- a/build.rs
3
+ +++ b/build.rs
4
+ @@ -346,7 +346,27 @@
5
+ // we want to optimize for minimizing the build tools required: No Perl,
6
+ // no nasm, etc.
7
+ let generated_dir = if !is_git {
8
+ - c_root_dir.join(PREGENERATED)
9
+ + let pregenerated_dir = c_root_dir.join(PREGENERATED);
10
+ + if target.os == WINDOWS && target.env == "msvc"
11
+ + {
12
+ + let mut pending = vec![(pregenerated_dir.clone(), out_dir.clone())];
13
+ + while let Some((src_dir, dst_dir)) = pending.pop() {
14
+ + fs::create_dir_all(&dst_dir).unwrap();
15
+ + for entry in fs::read_dir(&src_dir).unwrap() {
16
+ + let entry = entry.unwrap();
17
+ + let src_path = entry.path();
18
+ + let dst_path = dst_dir.join(entry.file_name());
19
+ + if entry.file_type().unwrap().is_dir() {
20
+ + pending.push((src_path, dst_path));
21
+ + } else {
22
+ + fs::copy(&src_path, &dst_path).unwrap();
23
+ + }
24
+ + }
25
+ + }
26
+ + out_dir.clone()
27
+ + } else {
28
+ + pregenerated_dir
29
+ + }
30
+ } else {
31
+ generate_sources_and_preassemble(
32
+ &out_dir,
33
+ @@ -561,7 +581,13 @@
34
+ let compiler = c.get_compiler();
35
+ // FIXME: On Windows AArch64 we currently must use Clang to compile C code
36
+ let compiler = if target.os == WINDOWS && target.arch == AARCH64 && !compiler.is_like_clang() {
37
+ - let _ = c.compiler("clang");
38
+ + // Keep inherited MSVC flags compatible with the selected Clang driver.
39
+ + let clang = if compiler.is_like_msvc() {
40
+ + "clang-cl"
41
+ + } else {
42
+ + "clang"
43
+ + };
44
+ + let _ = c.compiler(clang);
45
+ c.get_compiler()
46
+ } else {
47
+ compiler
48
+ @@ -569,6 +595,15 @@
49
+
50
+ let _ = c.include(c_root_dir.join("include"));
51
+ let _ = c.include(include_dir);
52
+ + let _ = c.include(c_root_dir.join("third_party").join("fiat"));
53
+ + if compiler.is_like_msvc() {
54
+ + let _ = c.include(c_root_dir.join("crypto").join("curve25519"));
55
+ + let _ = c.include(c_root_dir.join("crypto").join("limbs"));
56
+ + let _ = c.include(c_root_dir.join("crypto").join("fipsmodule").join("aes"));
57
+ + let _ = c.include(c_root_dir.join("crypto").join("fipsmodule").join("bn"));
58
+ + let _ = c.include(c_root_dir.join("crypto").join("fipsmodule").join("ec"));
59
+ + let _ = c.include(c_root_dir.join("crypto").join("poly1305"));
60
+ + }
61
+ for f in cpp_flags(&compiler) {
62
+ let _ = c.flag(f);
63
+ }
64
+ diff --git a/crypto/curve25519/curve25519.c b/crypto/curve25519/curve25519.c
65
+ index 99d7d7fbb..2f69ad560 100644
66
+ --- a/crypto/curve25519/curve25519.c
67
+ +++ b/crypto/curve25519/curve25519.c
68
+ @@ -47,11 +47,11 @@
69
+ #if defined(__GNUC__)
70
+ #pragma GCC diagnostic ignored "-Wpedantic"
71
+ #endif
72
+ -#include "../../third_party/fiat/curve25519_64.h"
73
+ +#include "curve25519_64.h"
74
+ #elif defined(OPENSSL_64_BIT)
75
+ -#include "../../third_party/fiat/curve25519_64_msvc.h"
76
+ +#include "curve25519_64_msvc.h"
77
+ #else
78
+ -#include "../../third_party/fiat/curve25519_32.h"
79
+ +#include "curve25519_32.h"
80
+ #endif
81
+
82
+
83
+ diff --git a/crypto/curve25519/curve25519_64_adx.c b/crypto/curve25519/curve25519_64_adx.c
84
+ index 88964a9dd..b660f55f4 100644
85
+ --- a/crypto/curve25519/curve25519_64_adx.c
86
+ +++ b/crypto/curve25519/curve25519_64_adx.c
87
+ @@ -19,5 +19,5 @@
88
+ #pragma GCC diagnostic ignored "-Wpedantic"
89
+ #pragma GCC diagnostic ignored "-Wsign-conversion"
90
+
91
+ -#include "../../third_party/fiat/curve25519_64_adx.h"
92
+ +#include "curve25519_64_adx.h"
93
+ #endif
94
+ diff --git a/crypto/fipsmodule/ec/p256.c b/crypto/fipsmodule/ec/p256.c
95
+ index 0117916da..4224d1192 100644
96
+ --- a/crypto/fipsmodule/ec/p256.c
97
+ +++ b/crypto/fipsmodule/ec/p256.c
98
+ @@ -50,11 +50,11 @@
99
+ #if defined(__GNUC__)
100
+ #pragma GCC diagnostic ignored "-Wpedantic"
101
+ #endif
102
+ -#include "../../../third_party/fiat/p256_64.h"
103
+ +#include "p256_64.h"
104
+ #elif defined(OPENSSL_64_BIT)
105
+ -#include "../../../third_party/fiat/p256_64_msvc.h"
106
+ +#include "p256_64_msvc.h"
107
+ #else
108
+ -#include "../../../third_party/fiat/p256_32.h"
109
+ +#include "p256_32.h"
110
+ #endif
111
+
112
+
patches/rules_cc_rusty_v8_custom_libcxx.patch ADDED
@@ -0,0 +1,44 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diff --git a/cc/cc_library.bzl b/cc/cc_library.bzl
2
+ index c45c5fd954..2e3a01f12a 100644
3
+ --- a/cc/cc_library.bzl
4
+ +++ b/cc/cc_library.bzl
5
+ @@ -15,5 +15,39 @@
6
+
7
+ load("@cc_compatibility_proxy//:proxy.bzl", _cc_library = "cc_library")
8
+
9
+ +_RUSTY_V8_CUSTOM_LIBCXX_COPTS = select({
10
+ + "@@//third_party/v8:use_rusty_v8_custom_libcxx": [
11
+ + "-nostdinc++",
12
+ + "-D_LIBCPP_DISABLE_VISIBILITY_ANNOTATIONS",
13
+ + "-D_LIBCPP_HARDENING_MODE=_LIBCPP_HARDENING_MODE_EXTENSIVE",
14
+ + "-D_LIBCPP_INSTRUMENTED_WITH_ASAN=0",
15
+ + "-D_LIBCXXABI_DISABLE_VISIBILITY_ANNOTATIONS",
16
+ + ],
17
+ + "//conditions:default": [],
18
+ +})
19
+ +
20
+ +_RUSTY_V8_CUSTOM_LIBCXX_DEPS = select({
21
+ + "@@//third_party/v8:use_rusty_v8_custom_libcxx": [
22
+ + "@@//third_party/v8:rusty_v8_custom_libcxx_headers",
23
+ + ],
24
+ + "//conditions:default": [],
25
+ +})
26
+ +
27
+ +_RUSTY_V8_CUSTOM_LIBCXX_REPOS = [
28
+ + # V8 and ICU are patched in-tree. These external C++ dependencies still
29
+ + # build inside the artifact closure once the LLVM toolchain's default
30
+ + # libc++ headers are suppressed.
31
+ + "@abseil-cpp+",
32
+ + "@v8++http_archive+highway",
33
+ + "@v8++http_archive+simdutf",
34
+ +]
35
+ +
36
+ +def _should_use_rusty_v8_custom_libcxx():
37
+ + return native.repository_name() in _RUSTY_V8_CUSTOM_LIBCXX_REPOS
38
+ +
39
+ def cc_library(**kwargs):
40
+ + if _should_use_rusty_v8_custom_libcxx():
41
+ + kwargs["copts"] = (kwargs.get("copts", []) or []) + _RUSTY_V8_CUSTOM_LIBCXX_COPTS
42
+ + kwargs["deps"] = (kwargs.get("deps", []) or []) + _RUSTY_V8_CUSTOM_LIBCXX_DEPS
43
+ + kwargs["features"] = (kwargs.get("features", []) or []) + ["-module_maps"]
44
+ _cc_library(**kwargs)
patches/rules_foreign_cc_make_cppflags.patch ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diff --git a/foreign_cc/built_tools/make_build.bzl b/foreign_cc/built_tools/make_build.bzl
2
+ --- a/foreign_cc/built_tools/make_build.bzl
3
+ +++ b/foreign_cc/built_tools/make_build.bzl
4
+ @@ -114,6 +114,11 @@
5
+ "ARFLAGS": _join_flags_list(ctx.workspace_name, arflags),
6
+ "CC": absolute_cc,
7
+ "CFLAGS": _join_flags_list(ctx.workspace_name, non_sysroot_cflags),
8
+ + # Configure's preprocessor checks need the declared header paths too.
9
+ + "CPPFLAGS": " ".join([
10
+ + "$${CPPFLAGS:-}$$",
11
+ + _join_flags_list(ctx.workspace_name, non_sysroot_cflags),
12
+ + ]).strip(),
13
+ "LD": absolute_ld,
14
+ "LDFLAGS": _join_flags_list(ctx.workspace_name, non_sysroot_ldflags),
15
+ })
patches/rules_foreign_cc_make_msvc_stdint.patch ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diff --git a/foreign_cc/built_tools/make_build.bzl b/foreign_cc/built_tools/make_build.bzl
2
+ --- a/foreign_cc/built_tools/make_build.bzl
3
+ +++ b/foreign_cc/built_tools/make_build.bzl
4
+ @@ -35,6 +35,9 @@
5
+ build_str += " gcc"
6
+ dist_dir = "GccRel"
7
+ else:
8
+ + # Modern MSVC provides stdint.h, including the intmax_t types that
9
+ + # Make's legacy Windows config otherwise replaces with macros.
10
+ + build_str = 'export CL="$${CL:-}$$ /DHAVE_STDINT_H=1"\n' + build_str
11
+ dist_dir = "WinRel"
12
+
13
+ script = [
patches/rules_foreign_cc_make_xcompile.patch ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ diff --git a/toolchains/private/BUILD.bazel b/toolchains/private/BUILD.bazel
2
+ --- a/toolchains/private/BUILD.bazel
3
+ +++ b/toolchains/private/BUILD.bazel
4
+ @@ -23,5 +23,6 @@
5
+ make_tool(
6
+ name = "make_tool",
7
+ srcs = "@gnumake_src//:all_srcs",
8
+ + configure_xcompile = True,
9
+ tags = ["manual"],
10
+ )