From 87b224c8cb5510b638f48130722ffc7a6a616c0d Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:35:58 -0500 Subject: [PATCH 1/7] fix(security): refresh vendored incident trust inputs --- .github/workflows/openclaw-artifacts.yml | 6 +- .github/workflows/openclaw-checks.yml | 4 +- Dockerfile | 2 +- Dockerfile.windows | 2 +- .../third_party/certifi/certifi/cacert.pem | 25 ------ .../Source/webrtc/rtc_base/ssl_roots.h | 88 ++----------------- .../sslroots/generate_sslroots.py | 1 + .../webkitcorepy/webkitcorepy/__init__.py | 2 +- .../webkitcorepy/webkitcorepy/cacert.pem | 25 ------ 9 files changed, 15 insertions(+), 140 deletions(-) diff --git a/.github/workflows/openclaw-artifacts.yml b/.github/workflows/openclaw-artifacts.yml index 4d5ea084851c..eac938feb461 100644 --- a/.github/workflows/openclaw-artifacts.yml +++ b/.github/workflows/openclaw-artifacts.yml @@ -38,7 +38,7 @@ jobs: !/WebDriverTests/ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 with: - node-version: '26.3.0' + node-version: '26.12.1' - name: Test publication integrity gates run: python3 .github/openclaw/test_release.py - name: Measure runner capacity and Docker support @@ -72,7 +72,7 @@ jobs: !/WebDriverTests/ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 with: - node-version: '26.3.0' + node-version: '26.12.1' - name: Build unchanged upstream lane and record provenance env: LANE: ${{ matrix.lane }} @@ -96,7 +96,7 @@ jobs: sparse-checkout: .github - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 with: - node-version: '26.3.0' + node-version: '26.12.1' - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c with: name: bun-webkit-linux-amd64 diff --git a/.github/workflows/openclaw-checks.yml b/.github/workflows/openclaw-checks.yml index 8206b2dedc22..946ce5e4f287 100644 --- a/.github/workflows/openclaw-checks.yml +++ b/.github/workflows/openclaw-checks.yml @@ -31,7 +31,7 @@ jobs: /JSTests/stress/ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 with: - node-version: '26.3.0' + node-version: '26.12.1' - name: Verify pipeline integrity and runner support run: | python3 .github/openclaw/test_release.py @@ -152,6 +152,6 @@ jobs: !/WebDriverTests/ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 with: - node-version: '26.3.0' + node-version: '26.12.1' - name: Build the macOS fork artifact on Linux without publishing run: python3 .github/openclaw/artifacts.py build --lane bun-webkit-macos-arm64 --output "$RUNNER_TEMP/macos-fork-artifact" diff --git a/Dockerfile b/Dockerfile index 87d4f9950bad..33326d87be35 100644 --- a/Dockerfile +++ b/Dockerfile @@ -63,7 +63,7 @@ RUN curl -fsSL "https://github.com/facebook/zstd/releases/download/v${ZSTD_VERSI && zstd --version # Install Node (for icu/compress-data.ts; needs >=23.6 for default type stripping) -ARG NODE_VERSION=24.16.0 +ARG NODE_VERSION=24.22.1 RUN curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-$(uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/').tar.xz" \ | tar -xJ -C /usr/local --strip-components=1 \ && node --version diff --git a/Dockerfile.windows b/Dockerfile.windows index 2d2df0b75962..b091430909c2 100644 --- a/Dockerfile.windows +++ b/Dockerfile.windows @@ -202,7 +202,7 @@ RUN for pair in "x86_64 x64" "aarch64 arm64"; do \ # (>= 23.6 for type stripping), and patch for the udata.cpp hook. zstd's lib/ sources stay, at /zstd/lib: jsc.exe builds # the decoder from them to read the repacked data (shell/CMakeLists.txt, BUN_ICU_ZSTD_SOURCE_DIR). ARG ZSTD_VERSION=1.5.7 -ARG NODE_VERSION=24.16.0 +ARG NODE_VERSION=24.22.1 RUN apt-get update && apt-get install -y --no-install-recommends patch && rm -rf /var/lib/apt/lists/* && \ curl -fsSL "https://github.com/facebook/zstd/releases/download/v${ZSTD_VERSION}/zstd-${ZSTD_VERSION}.tar.gz" | tar xz -C /tmp && \ make -C /tmp/zstd-${ZSTD_VERSION}/programs zstd -j$(nproc) CC=clang-${LLVM_VERSION} && \ diff --git a/LayoutTests/imported/w3c/web-platform-tests/tools/third_party/certifi/certifi/cacert.pem b/LayoutTests/imported/w3c/web-platform-tests/tools/third_party/certifi/certifi/cacert.pem index 2713f541c449..39b514651927 100644 --- a/LayoutTests/imported/w3c/web-platform-tests/tools/third_party/certifi/certifi/cacert.pem +++ b/LayoutTests/imported/w3c/web-platform-tests/tools/third_party/certifi/certifi/cacert.pem @@ -363,31 +363,6 @@ LBfQdCVp9/5rPJS+TUtBjE7ic9DjkCJzQ83z7+pzzkWKsKZJ/0x9nXGIxHYdkFsd # MD5 Fingerprint: 49:79:04:b0:eb:87:19:ac:47:b0:bc:11:51:9b:74:d0 # SHA1 Fingerprint: d1:eb:23:a4:6d:17:d6:8f:d9:25:64:c2:f1:f1:60:17:64:d8:e3:49 # SHA256 Fingerprint: d7:a7:a0:fb:5d:7e:27:31:d7:71:e9:48:4e:bc:de:f7:1d:5f:0c:3e:0a:29:48:78:2b:c8:3e:e0:ea:69:9e:f4 ------BEGIN CERTIFICATE----- -MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb -MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow -GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj -YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL -MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE -BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM -GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP -ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua -BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe -3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 -YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR -rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm -ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU -oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF -MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v -QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t -b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF -AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q -GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz -Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 -G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi -l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 -smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== ------END CERTIFICATE----- # Issuer: CN=QuoVadis Root Certification Authority O=QuoVadis Limited OU=Root Certification Authority # Subject: CN=QuoVadis Root Certification Authority O=QuoVadis Limited OU=Root Certification Authority diff --git a/Source/ThirdParty/libwebrtc/Source/webrtc/rtc_base/ssl_roots.h b/Source/ThirdParty/libwebrtc/Source/webrtc/rtc_base/ssl_roots.h index 5fa82b07f321..d5a98a1fba0f 100644 --- a/Source/ThirdParty/libwebrtc/Source/webrtc/rtc_base/ssl_roots.h +++ b/Source/ThirdParty/libwebrtc/Source/webrtc/rtc_base/ssl_roots.h @@ -1,5 +1,5 @@ /* - * Copyright 2023 The WebRTC Project Authors. All rights reserved. + * Copyright 2026 The WebRTC Project Authors. All rights reserved. * * Use of this source code is governed by a BSD-style license * that can be found in the LICENSE file in the root of the source @@ -15,14 +15,14 @@ // This file is the root certificates in C form. -// It was generated at 2023-05-09T15:05:10+00:00 by the following script: -// `tools_webrtc/sslroots/generate_sslroots.py https://pki.goog/roots.pem` +// It was generated at 2026-10-21T20:28:50+00:00 by the following script: +// `tools_webrtc/sslroots/generate_sslroots.py roots.pem` // clang-format off // Don't bother formatting generated code, // also it would breaks subject/issuer lines. -// Source bundle `https://pki.goog/roots.pem` digest is [sha256:9c9b9685ad319b9747c3fe69b46a61c11a0efabdfa09ca6a8b0c3da421036d27] +// Source bundle `roots.pem` digest is [sha256:26bd8c0e7c962d940ac0f578af5630cc39ee3a7322af6bc16d4eb038232f466e] /* subject: Common Name: Baltimore CyberTrust Root, Organizational Unit: CyberTrust, Organization: Baltimore, Country: IE */ /* issuer: Common Name: Baltimore CyberTrust Root, Organizational Unit: CyberTrust, Organization: Baltimore, Country: IE */ @@ -1663,80 +1663,6 @@ const unsigned char kCertificateWithFingerprint_c3846bf24b9e93ca64274c0ec67c1ecc 0x7F,0xDB,0xBD,0x9F, }; -/* subject: Common Name: AAA Certificate Services, Organization: Comodo CA Limited, Locality: Salford, State/Province: Greater Manchester, Country: GB */ -/* issuer: Common Name: AAA Certificate Services, Organization: Comodo CA Limited, Locality: Salford, State/Province: Greater Manchester, Country: GB */ -/* link: https://crt.sh/?q=d7a7a0fb5d7e2731d771e9484ebcdef71d5f0c3e0a2948782bc83ee0ea699ef4 */ -const unsigned char kCertificateWithFingerprint_d7a7a0fb5d7e2731d771e9484ebcdef71d5f0c3e0a2948782bc83ee0ea699ef4_certificate[1078]={ -0x30,0x82,0x04,0x32,0x30,0x82,0x03,0x1A,0xA0,0x03,0x02,0x01,0x02,0x02,0x01,0x01, -0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05,0x00,0x30, -0x7B,0x31,0x0B,0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x47,0x42,0x31,0x1B, -0x30,0x19,0x06,0x03,0x55,0x04,0x08,0x0C,0x12,0x47,0x72,0x65,0x61,0x74,0x65,0x72, -0x20,0x4D,0x61,0x6E,0x63,0x68,0x65,0x73,0x74,0x65,0x72,0x31,0x10,0x30,0x0E,0x06, -0x03,0x55,0x04,0x07,0x0C,0x07,0x53,0x61,0x6C,0x66,0x6F,0x72,0x64,0x31,0x1A,0x30, -0x18,0x06,0x03,0x55,0x04,0x0A,0x0C,0x11,0x43,0x6F,0x6D,0x6F,0x64,0x6F,0x20,0x43, -0x41,0x20,0x4C,0x69,0x6D,0x69,0x74,0x65,0x64,0x31,0x21,0x30,0x1F,0x06,0x03,0x55, -0x04,0x03,0x0C,0x18,0x41,0x41,0x41,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63, -0x61,0x74,0x65,0x20,0x53,0x65,0x72,0x76,0x69,0x63,0x65,0x73,0x30,0x1E,0x17,0x0D, -0x30,0x34,0x30,0x31,0x30,0x31,0x30,0x30,0x30,0x30,0x30,0x30,0x5A,0x17,0x0D,0x32, -0x38,0x31,0x32,0x33,0x31,0x32,0x33,0x35,0x39,0x35,0x39,0x5A,0x30,0x7B,0x31,0x0B, -0x30,0x09,0x06,0x03,0x55,0x04,0x06,0x13,0x02,0x47,0x42,0x31,0x1B,0x30,0x19,0x06, -0x03,0x55,0x04,0x08,0x0C,0x12,0x47,0x72,0x65,0x61,0x74,0x65,0x72,0x20,0x4D,0x61, -0x6E,0x63,0x68,0x65,0x73,0x74,0x65,0x72,0x31,0x10,0x30,0x0E,0x06,0x03,0x55,0x04, -0x07,0x0C,0x07,0x53,0x61,0x6C,0x66,0x6F,0x72,0x64,0x31,0x1A,0x30,0x18,0x06,0x03, -0x55,0x04,0x0A,0x0C,0x11,0x43,0x6F,0x6D,0x6F,0x64,0x6F,0x20,0x43,0x41,0x20,0x4C, -0x69,0x6D,0x69,0x74,0x65,0x64,0x31,0x21,0x30,0x1F,0x06,0x03,0x55,0x04,0x03,0x0C, -0x18,0x41,0x41,0x41,0x20,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x65, -0x20,0x53,0x65,0x72,0x76,0x69,0x63,0x65,0x73,0x30,0x82,0x01,0x22,0x30,0x0D,0x06, -0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x01,0x05,0x00,0x03,0x82,0x01,0x0F, -0x00,0x30,0x82,0x01,0x0A,0x02,0x82,0x01,0x01,0x00,0xBE,0x40,0x9D,0xF4,0x6E,0xE1, -0xEA,0x76,0x87,0x1C,0x4D,0x45,0x44,0x8E,0xBE,0x46,0xC8,0x83,0x06,0x9D,0xC1,0x2A, -0xFE,0x18,0x1F,0x8E,0xE4,0x02,0xFA,0xF3,0xAB,0x5D,0x50,0x8A,0x16,0x31,0x0B,0x9A, -0x06,0xD0,0xC5,0x70,0x22,0xCD,0x49,0x2D,0x54,0x63,0xCC,0xB6,0x6E,0x68,0x46,0x0B, -0x53,0xEA,0xCB,0x4C,0x24,0xC0,0xBC,0x72,0x4E,0xEA,0xF1,0x15,0xAE,0xF4,0x54,0x9A, -0x12,0x0A,0xC3,0x7A,0xB2,0x33,0x60,0xE2,0xDA,0x89,0x55,0xF3,0x22,0x58,0xF3,0xDE, -0xDC,0xCF,0xEF,0x83,0x86,0xA2,0x8C,0x94,0x4F,0x9F,0x68,0xF2,0x98,0x90,0x46,0x84, -0x27,0xC7,0x76,0xBF,0xE3,0xCC,0x35,0x2C,0x8B,0x5E,0x07,0x64,0x65,0x82,0xC0,0x48, -0xB0,0xA8,0x91,0xF9,0x61,0x9F,0x76,0x20,0x50,0xA8,0x91,0xC7,0x66,0xB5,0xEB,0x78, -0x62,0x03,0x56,0xF0,0x8A,0x1A,0x13,0xEA,0x31,0xA3,0x1E,0xA0,0x99,0xFD,0x38,0xF6, -0xF6,0x27,0x32,0x58,0x6F,0x07,0xF5,0x6B,0xB8,0xFB,0x14,0x2B,0xAF,0xB7,0xAA,0xCC, -0xD6,0x63,0x5F,0x73,0x8C,0xDA,0x05,0x99,0xA8,0x38,0xA8,0xCB,0x17,0x78,0x36,0x51, -0xAC,0xE9,0x9E,0xF4,0x78,0x3A,0x8D,0xCF,0x0F,0xD9,0x42,0xE2,0x98,0x0C,0xAB,0x2F, -0x9F,0x0E,0x01,0xDE,0xEF,0x9F,0x99,0x49,0xF1,0x2D,0xDF,0xAC,0x74,0x4D,0x1B,0x98, -0xB5,0x47,0xC5,0xE5,0x29,0xD1,0xF9,0x90,0x18,0xC7,0x62,0x9C,0xBE,0x83,0xC7,0x26, -0x7B,0x3E,0x8A,0x25,0xC7,0xC0,0xDD,0x9D,0xE6,0x35,0x68,0x10,0x20,0x9D,0x8F,0xD8, -0xDE,0xD2,0xC3,0x84,0x9C,0x0D,0x5E,0xE8,0x2F,0xC9,0x02,0x03,0x01,0x00,0x01,0xA3, -0x81,0xC0,0x30,0x81,0xBD,0x30,0x1D,0x06,0x03,0x55,0x1D,0x0E,0x04,0x16,0x04,0x14, -0xA0,0x11,0x0A,0x23,0x3E,0x96,0xF1,0x07,0xEC,0xE2,0xAF,0x29,0xEF,0x82,0xA5,0x7F, -0xD0,0x30,0xA4,0xB4,0x30,0x0E,0x06,0x03,0x55,0x1D,0x0F,0x01,0x01,0xFF,0x04,0x04, -0x03,0x02,0x01,0x06,0x30,0x0F,0x06,0x03,0x55,0x1D,0x13,0x01,0x01,0xFF,0x04,0x05, -0x30,0x03,0x01,0x01,0xFF,0x30,0x7B,0x06,0x03,0x55,0x1D,0x1F,0x04,0x74,0x30,0x72, -0x30,0x38,0xA0,0x36,0xA0,0x34,0x86,0x32,0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F,0x63, -0x72,0x6C,0x2E,0x63,0x6F,0x6D,0x6F,0x64,0x6F,0x63,0x61,0x2E,0x63,0x6F,0x6D,0x2F, -0x41,0x41,0x41,0x43,0x65,0x72,0x74,0x69,0x66,0x69,0x63,0x61,0x74,0x65,0x53,0x65, -0x72,0x76,0x69,0x63,0x65,0x73,0x2E,0x63,0x72,0x6C,0x30,0x36,0xA0,0x34,0xA0,0x32, -0x86,0x30,0x68,0x74,0x74,0x70,0x3A,0x2F,0x2F,0x63,0x72,0x6C,0x2E,0x63,0x6F,0x6D, -0x6F,0x64,0x6F,0x2E,0x6E,0x65,0x74,0x2F,0x41,0x41,0x41,0x43,0x65,0x72,0x74,0x69, -0x66,0x69,0x63,0x61,0x74,0x65,0x53,0x65,0x72,0x76,0x69,0x63,0x65,0x73,0x2E,0x63, -0x72,0x6C,0x30,0x0D,0x06,0x09,0x2A,0x86,0x48,0x86,0xF7,0x0D,0x01,0x01,0x05,0x05, -0x00,0x03,0x82,0x01,0x01,0x00,0x08,0x56,0xFC,0x02,0xF0,0x9B,0xE8,0xFF,0xA4,0xFA, -0xD6,0x7B,0xC6,0x44,0x80,0xCE,0x4F,0xC4,0xC5,0xF6,0x00,0x58,0xCC,0xA6,0xB6,0xBC, -0x14,0x49,0x68,0x04,0x76,0xE8,0xE6,0xEE,0x5D,0xEC,0x02,0x0F,0x60,0xD6,0x8D,0x50, -0x18,0x4F,0x26,0x4E,0x01,0xE3,0xE6,0xB0,0xA5,0xEE,0xBF,0xBC,0x74,0x54,0x41,0xBF, -0xFD,0xFC,0x12,0xB8,0xC7,0x4F,0x5A,0xF4,0x89,0x60,0x05,0x7F,0x60,0xB7,0x05,0x4A, -0xF3,0xF6,0xF1,0xC2,0xBF,0xC4,0xB9,0x74,0x86,0xB6,0x2D,0x7D,0x6B,0xCC,0xD2,0xF3, -0x46,0xDD,0x2F,0xC6,0xE0,0x6A,0xC3,0xC3,0x34,0x03,0x2C,0x7D,0x96,0xDD,0x5A,0xC2, -0x0E,0xA7,0x0A,0x99,0xC1,0x05,0x8B,0xAB,0x0C,0x2F,0xF3,0x5C,0x3A,0xCF,0x6C,0x37, -0x55,0x09,0x87,0xDE,0x53,0x40,0x6C,0x58,0xEF,0xFC,0xB6,0xAB,0x65,0x6E,0x04,0xF6, -0x1B,0xDC,0x3C,0xE0,0x5A,0x15,0xC6,0x9E,0xD9,0xF1,0x59,0x48,0x30,0x21,0x65,0x03, -0x6C,0xEC,0xE9,0x21,0x73,0xEC,0x9B,0x03,0xA1,0xE0,0x37,0xAD,0xA0,0x15,0x18,0x8F, -0xFA,0xBA,0x02,0xCE,0xA7,0x2C,0xA9,0x10,0x13,0x2C,0xD4,0xE5,0x08,0x26,0xAB,0x22, -0x97,0x60,0xF8,0x90,0x5E,0x74,0xD4,0xA2,0x9A,0x53,0xBD,0xF2,0xA9,0x68,0xE0,0xA2, -0x6E,0xC2,0xD7,0x6C,0xB1,0xA3,0x0F,0x9E,0xBF,0xEB,0x68,0xE7,0x56,0xF2,0xAE,0xF2, -0xE3,0x2B,0x38,0x3A,0x09,0x81,0xB5,0x6B,0x85,0xD7,0xBE,0x2D,0xED,0x3F,0x1A,0xB7, -0xB2,0x63,0xE2,0xF5,0x62,0x2C,0x82,0xD4,0x6A,0x00,0x41,0x50,0xF1,0x39,0x83,0x9F, -0x95,0xE9,0x36,0x96,0x98,0x6E, -}; - /* subject: Common Name: COMODO Certification Authority, Organization: COMODO CA Limited, Locality: Salford, State/Province: Greater Manchester, Country: GB */ /* issuer: Common Name: COMODO Certification Authority, Organization: COMODO CA Limited, Locality: Salford, State/Province: Greater Manchester, Country: GB */ /* link: https://crt.sh/?q=0c2cd63df7806fa399ede809116b575bf87989f06518f9808c860503178baf66 */ @@ -2404,7 +2330,7 @@ const unsigned char kCertificateWithFingerprint_b085d70b964f191a73e4af0d54ae7a0e 0x37,0x98,0xBC,0xA3,0x8F,0x4C,0x4C,0xA9,0x34,0x2B,0x6C,0xEF,0xFB,0x95,0x9B,0x26, }; -const unsigned char* const kSSLCertCertificateList[36]={ +const unsigned char* const kSSLCertCertificateList[35]={ kCertificateWithFingerprint_16af57a9f676b0ab126095aa5ebadef22ab31119d644ac95cd4b93dbf3f26aeb_certificate, kCertificateWithFingerprint_3e9099b5015e8f486c00bcea9d111ee721faba355a89bcf1df69561e3dc6325c_certificate, kCertificateWithFingerprint_7d05ebb682339f8c9451ee094eebfefa7953a114edb2f44949452fab7d2fc185_certificate, @@ -2430,7 +2356,6 @@ kCertificateWithFingerprint_45140b3247eb9cc8c5b4f0d7b53091f73292089e6e5a63e2749d kCertificateWithFingerprint_2ce1cb0bf9d2f9e102993fbe215152c3b2dd0cabde1c68e5319b839154dbb7f5_certificate, kCertificateWithFingerprint_1465fa205397b876faa6f0a9958e5590e40fcc7faa4fb7c2c8677521fb5fb658_certificate, kCertificateWithFingerprint_c3846bf24b9e93ca64274c0ec67c1ecc5e024ffcacd2d74019350e81fe546ae4_certificate, -kCertificateWithFingerprint_d7a7a0fb5d7e2731d771e9484ebcdef71d5f0c3e0a2948782bc83ee0ea699ef4_certificate, kCertificateWithFingerprint_0c2cd63df7806fa399ede809116b575bf87989f06518f9808c860503178baf66_certificate, kCertificateWithFingerprint_1793927a0614549789adce2f8f34f7f0b66d0f3ae3a3b84d21ec15dbba4fadc7_certificate, kCertificateWithFingerprint_52f0e1c4e58ec629291b60317f074671b85d7ea80d5b07273463534b32b40234_certificate, @@ -2443,7 +2368,7 @@ kCertificateWithFingerprint_349dfa4058c5e263123b398ae795573c4e1313c83fe68f93556c kCertificateWithFingerprint_b085d70b964f191a73e4af0d54ae7a0e07aafdaf9b71dd0862138ab7325a24a2_certificate, }; -const size_t kSSLCertCertificateSizeList[36]={ +const size_t kSSLCertCertificateSizeList[35]={ 891, 955, 922, @@ -2469,7 +2394,6 @@ const size_t kSSLCertCertificateSizeList[36]={ 993, 1043, 1028, -1078, 1057, 653, 1500, diff --git a/Source/ThirdParty/libwebrtc/Source/webrtc/tools_webrtc/sslroots/generate_sslroots.py b/Source/ThirdParty/libwebrtc/Source/webrtc/tools_webrtc/sslroots/generate_sslroots.py index 14acff92fb26..7d482bd9d603 100644 --- a/Source/ThirdParty/libwebrtc/Source/webrtc/tools_webrtc/sslroots/generate_sslroots.py +++ b/Source/ThirdParty/libwebrtc/Source/webrtc/tools_webrtc/sslroots/generate_sslroots.py @@ -224,6 +224,7 @@ def _CreateOutputHeader(source_path_or_url: str, source_digest: str) -> str: ' */\n\n' '#ifndef RTC_BASE_SSL_ROOTS_H_\n' '#define RTC_BASE_SSL_ROOTS_H_\n\n' + '#include \n\n' '// This file is the root certificates in C form.\n\n' f'// It was generated at {now_utc.isoformat()} by the following script:\n' '// `tools_webrtc/sslroots/generate_sslroots.py ' diff --git a/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/__init__.py b/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/__init__.py index d3b7aad05e9a..64d8a83dfccb 100644 --- a/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/__init__.py +++ b/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/__init__.py @@ -61,7 +61,7 @@ else: AutoInstall.register(Package('setuptools', Version(59, 8, 0), aliases=['_distutils_hack', 'pkg_resources'])) -AutoInstall.register(Package('certifi', Version(2022, 12, 7))) +AutoInstall.register(Package('certifi', Version(2026, 10, 19))) AutoInstall.register(Package('chardet', Version(3, 0, 4))) AutoInstall.register(Package('dateutil', Version(2, 8, 1), pypi_name='python-dateutil', wheel=True)) AutoInstall.register(Package('entrypoints', Version(0, 3, 0))) diff --git a/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/cacert.pem b/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/cacert.pem index 6b4413143724..02075d8ea116 100644 --- a/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/cacert.pem +++ b/Tools/Scripts/libraries/webkitcorepy/webkitcorepy/cacert.pem @@ -120,31 +120,6 @@ serial=01 SHA256 Fingerprint=D7:A7:A0:FB:5D:7E:27:31:D7:71:E9:48:4E:BC:DE:F7:1D:5F:0C:3E:0A:29:48:78:2B:C8:3E:E0:EA:69:9E:F4 notBefore=Jan 1 00:00:00 2004 GMT notAfter=Dec 31 23:59:59 2028 GMT ------BEGIN CERTIFICATE----- -MIIEMjCCAxqgAwIBAgIBATANBgkqhkiG9w0BAQUFADB7MQswCQYDVQQGEwJHQjEb -MBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHDAdTYWxmb3JkMRow -GAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UEAwwYQUFBIENlcnRpZmlj -YXRlIFNlcnZpY2VzMB4XDTA0MDEwMTAwMDAwMFoXDTI4MTIzMTIzNTk1OVowezEL -MAkGA1UEBhMCR0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE -BwwHU2FsZm9yZDEaMBgGA1UECgwRQ29tb2RvIENBIExpbWl0ZWQxITAfBgNVBAMM -GEFBQSBDZXJ0aWZpY2F0ZSBTZXJ2aWNlczCCASIwDQYJKoZIhvcNAQEBBQADggEP -ADCCAQoCggEBAL5AnfRu4ep2hxxNRUSOvkbIgwadwSr+GB+O5AL686tdUIoWMQua -BtDFcCLNSS1UY8y2bmhGC1Pqy0wkwLxyTurxFa70VJoSCsN6sjNg4tqJVfMiWPPe -3M/vg4aijJRPn2jymJBGhCfHdr/jzDUsi14HZGWCwEiwqJH5YZ92IFCokcdmtet4 -YgNW8IoaE+oxox6gmf049vYnMlhvB/VruPsUK6+3qszWY19zjNoFmag4qMsXeDZR -rOme9Hg6jc8P2ULimAyrL58OAd7vn5lJ8S3frHRNG5i1R8XlKdH5kBjHYpy+g8cm -ez6KJcfA3Z3mNWgQIJ2P2N7Sw4ScDV7oL8kCAwEAAaOBwDCBvTAdBgNVHQ4EFgQU -oBEKIz6W8Qfs4q8p74Klf9AwpLQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQF -MAMBAf8wewYDVR0fBHQwcjA4oDagNIYyaHR0cDovL2NybC5jb21vZG9jYS5jb20v -QUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNqA0oDKGMGh0dHA6Ly9jcmwuY29t -b2RvLm5ldC9BQUFDZXJ0aWZpY2F0ZVNlcnZpY2VzLmNybDANBgkqhkiG9w0BAQUF -AAOCAQEACFb8AvCb6P+k+tZ7xkSAzk/ExfYAWMymtrwUSWgEdujm7l3sAg9g1o1Q -GE8mTgHj5rCl7r+8dFRBv/38ErjHT1r0iWAFf2C3BUrz9vHCv8S5dIa2LX1rzNLz -Rt0vxuBqw8M0Ayx9lt1awg6nCpnBBYurDC/zXDrPbDdVCYfeU0BsWO/8tqtlbgT2 -G9w84FoVxp7Z8VlIMCFlA2zs6SFz7JsDoeA3raAVGI/6ugLOpyypEBMs1OUIJqsi -l2D4kF501KKaU73yqWjgom7C12yxow+ev+to51byrvLjKzg6CYG1a4XXvi3tPxq3 -smPi9WIsgtRqAEFQ8TmDn5XpNpaYbg== ------END CERTIFICATE----- issuer= /C=US/O=Internet Security Research Group/CN=ISRG Root X1 subject= /C=US/O=Internet Security Research Group/CN=ISRG Root X1 From 372269047b8a0e2454ea4d8dbc0bf2c582be1335 Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:52:59 -0500 Subject: [PATCH 2/7] fix(ci): qualify the reviewed WebKit Node recipe --- .github/openclaw/artifacts.py | 14 ++++++--- .github/openclaw/test_release.py | 50 ++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/.github/openclaw/artifacts.py b/.github/openclaw/artifacts.py index 73a54c982c4e..8c9b9d0010e0 100644 --- a/.github/openclaw/artifacts.py +++ b/.github/openclaw/artifacts.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Owned packaging and provenance around the unchanged upstream lane runner.""" +"""Owned packaging and provenance around the pinned upstream lane runner.""" import argparse import hashlib import json @@ -15,6 +15,10 @@ ROOT = Path(__file__).resolve().parents[2] CONFIG = json.loads((ROOT / '.github/openclaw/variants.json').read_text()) RECIPE = ['.github/scripts/lanes.mjs', 'Dockerfile', 'Dockerfile.musl', 'Dockerfile.macos', 'Dockerfile.windows', 'icu', 'macos-cross'] +# Keep the engine comparison BASE; admit only this reviewed Node security update. +# Tree identities include file modes and every descendant of recipe directories. +RECIPE_REVISION = {'reviewed_source':'87b224c8cb5510b638f48130722ffc7a6a616c0d', + 'tree_sha256':'fabc54c96a4f33f485be7964cbe55f4ef88838db6ad5713e5abe1b515b99c6da'} def run(*args, **kwargs): return subprocess.run(args, check=True, text=True, **kwargs) @@ -34,7 +38,9 @@ def source(): if not re.fullmatch('[0-9a-f]{40}', sha): raise ValueError('full source SHA required') run('git', 'merge-base', '--is-ancestor', BASE, sha) - run('git', 'diff', '--exit-code', BASE, sha, '--', *RECIPE) + recipe = subprocess.check_output(['git', 'ls-tree', '--full-tree', '-z', sha, '--', *RECIPE]) + if hashlib.sha256(recipe).hexdigest() != RECIPE_REVISION['tree_sha256']: + raise ValueError('source recipe differs from the reviewed revision') run('git', 'diff', '--exit-code', 'HEAD') return sha @@ -65,7 +71,7 @@ def build(label, destination): provenance = destination / 'provenance' provenance.mkdir() command = json.loads(capture('node', '.github/scripts/lanes.mjs', 'build', label, '--output', str(artifact), '--dry-run')) - # Build the unchanged base locally. No image registry write or inherited workflow is used. + # Build the pinned recipe base locally. No image registry write or inherited workflow is used. base_tag = 'openclaw-webkit-toolchain:' + hashlib.sha256(lane['image'].encode()).hexdigest()[:24] base_command = command[:command.index('--target=artifact')] + ['--target=base', '--load', '--tag', base_tag, '--metadata-file', str(provenance/'toolchain-build.json'), '.'] run('docker', 'buildx', 'use', 'default') @@ -92,7 +98,7 @@ def build(label, destination): for commit in capture('git','rev-list','--reverse',BASE+'..'+sha).splitlines(): patch = subprocess.check_output(['git','show','--format=','--binary',commit]) patches.append({'commit':commit,'subject':capture('git','show','-s','--format=%s',commit),'patch_sha256':hashlib.sha256(patch).hexdigest()}) - manifest = {'schema_version':1,'source':sha,'base':BASE,'lane':lane,'patches':patches, + manifest = {'schema_version':1,'source':sha,'base':BASE,'recipe_revision':RECIPE_REVISION,'lane':lane,'patches':patches, 'recipe_sha256':{p:digest(ROOT/p) for p in paths},'toolchain_image_id':image['Id'], 'commands':{'base':base_command,'artifact':build_command}, 'tool_versions':tool_versions,'docker_version':capture('docker','version'),'buildx_version':capture('docker','buildx','version'), diff --git a/.github/openclaw/test_release.py b/.github/openclaw/test_release.py index a91e747ca5a0..9240cbba05fc 100644 --- a/.github/openclaw/test_release.py +++ b/.github/openclaw/test_release.py @@ -1,5 +1,7 @@ +import hashlib import importlib.util import json +import os from pathlib import Path import subprocess import tempfile @@ -9,6 +11,8 @@ spec=importlib.util.spec_from_file_location('release',Path(__file__).with_name('release.py')) release=importlib.util.module_from_spec(spec);spec.loader.exec_module(release) +spec=importlib.util.spec_from_file_location('artifacts',Path(__file__).with_name('artifacts.py')) +artifacts=importlib.util.module_from_spec(spec);spec.loader.exec_module(artifacts) SOURCE='a'*40 class ReleaseIntegrity(unittest.TestCase): @@ -100,4 +104,50 @@ def test_authorization_rejects_other_branch(self): with self.assertRaisesRegex(ValueError,'reviewed source'):release.authorize(SOURCE,42) execute.assert_not_called() +class ArtifactRecipe(unittest.TestCase): + def setUp(self): + self.temp=tempfile.TemporaryDirectory();self.addCleanup(self.temp.cleanup) + self.root=Path(self.temp.name) + self.addCleanup(os.chdir,Path.cwd());os.chdir(self.root) + self.git('init','-q') + self.git('config','user.name','Recipe fixture');self.git('config','user.email','fixture@example.invalid') + self.git('config','commit.gpgsign','false') + for name in artifacts.RECIPE: + p=self.root/name + if name in ('icu','macos-cross'):p=p/'input.txt' + p.parent.mkdir(parents=True,exist_ok=True);p.write_text('reviewed input\n') + (self.root/'Dockerfile').write_text('ARG NODE_VERSION=24.16.0\n') + (self.root/'source.txt').write_text('engine source\n') + self.commit('upstream base');self.base=self.git('rev-parse','HEAD') + (self.root/'Dockerfile').write_text('ARG NODE_VERSION=24.22.1\n') + self.commit('reviewed Node security update') + recipe=subprocess.check_output(['git','ls-tree','--full-tree','-z','HEAD','--',*artifacts.RECIPE]) + revision={'reviewed_source':self.git('rev-parse','HEAD'),'tree_sha256':hashlib.sha256(recipe).hexdigest()} + for name,value in [('BASE',self.base),('RECIPE_REVISION',revision)]: + p=patch.object(artifacts,name,value,create=True);p.start();self.addCleanup(p.stop) + def git(self,*args): + return subprocess.check_output(['git',*args],text=True).strip() + def commit(self,message): + self.git('add','.');self.git('commit','-qm',message) + def test_reviewed_recipe_survives_squash_and_later_engine_commits(self): + self.git('reset','--hard',self.base) + (self.root/'Dockerfile').write_text('ARG NODE_VERSION=24.22.1\n') + self.commit('squashed reviewed update') + self.assertEqual(artifacts.source(),self.git('rev-parse','HEAD')) + (self.root/'source.txt').write_text('security-patched engine source\n') + self.commit('engine change after reviewed recipe') + self.assertEqual(artifacts.source(),self.git('rev-parse','HEAD')) + def test_unreviewed_recipe_and_dirty_source_are_rejected(self): + (self.root/'source.txt').write_text('uncommitted engine change\n') + with self.assertRaises(subprocess.CalledProcessError):artifacts.source() + self.git('checkout','--','source.txt') + for name,mode in [('Dockerfile',False),('icu/input.txt',False),('Dockerfile',True)]: + with self.subTest(path=name,mode=mode): + p=self.root/name + if mode:p.chmod(0o755) + else:p.write_text('unreviewed recipe input\n') + self.commit('unreviewed recipe change') + with self.assertRaisesRegex(ValueError,'recipe differs'):artifacts.source() + self.git('reset','--hard','HEAD^') + if __name__=='__main__':unittest.main() From 670af3032b19a37504d389437c871ebbdf2f35a4 Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:19:55 -0500 Subject: [PATCH 3/7] fix(images): refresh musl toolchain default CA bundle --- Dockerfile.musl | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile.musl b/Dockerfile.musl index ebc80602503e..583302643103 100644 --- a/Dockerfile.musl +++ b/Dockerfile.musl @@ -12,6 +12,7 @@ ARG LINUX_ARCH="x86_64" # `--build-context base=docker-image://...`, which replaces this stage (.github/workflows/ci.yml, the `image` job). # Without that, it is built here like any other stage. Lane settings belong in `lane` below. FROM alpine:3.23 as base +RUN apk add --no-cache --upgrade ca-certificates-bundle=20261020-r0 # LLVM 23 is in Alpine edge only. `@edge` is a tagged repository: apk takes a package from it only when the package is # asked for with that tag, or when a tagged package needs something 3.23 does not have. So musl, libstdc++ and gcc stay From 57922359ff7b0355fdd8bdd2577cf42ebda80b46 Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:24:05 -0500 Subject: [PATCH 4/7] fix(ci): qualify the reviewed musl CA recipe --- .github/openclaw/artifacts.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/openclaw/artifacts.py b/.github/openclaw/artifacts.py index 8c9b9d0010e0..278540247430 100644 --- a/.github/openclaw/artifacts.py +++ b/.github/openclaw/artifacts.py @@ -15,10 +15,10 @@ ROOT = Path(__file__).resolve().parents[2] CONFIG = json.loads((ROOT / '.github/openclaw/variants.json').read_text()) RECIPE = ['.github/scripts/lanes.mjs', 'Dockerfile', 'Dockerfile.musl', 'Dockerfile.macos', 'Dockerfile.windows', 'icu', 'macos-cross'] -# Keep the engine comparison BASE; admit only this reviewed Node security update. +# Keep the engine comparison BASE; admit only these reviewed runtime and CA updates. # Tree identities include file modes and every descendant of recipe directories. -RECIPE_REVISION = {'reviewed_source':'87b224c8cb5510b638f48130722ffc7a6a616c0d', - 'tree_sha256':'fabc54c96a4f33f485be7964cbe55f4ef88838db6ad5713e5abe1b515b99c6da'} +RECIPE_REVISION = {'reviewed_source':'670af3032b19a37504d389437c871ebbdf2f35a4', + 'tree_sha256':'03e3c253c84d9738a17530c6490325c778f8d995212b598af66a283cc5d9389f'} def run(*args, **kwargs): return subprocess.run(args, check=True, text=True, **kwargs) From 0bef959f21c421ae649bc3e023d48aa50f482a03 Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Wed, 21 Oct 2026 18:28:26 -0500 Subject: [PATCH 5/7] fix(ci): retain history for strict artifact source gates --- .github/workflows/openclaw-artifacts.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/openclaw-artifacts.yml b/.github/workflows/openclaw-artifacts.yml index eac938feb461..3cd349ec4581 100644 --- a/.github/workflows/openclaw-artifacts.yml +++ b/.github/workflows/openclaw-artifacts.yml @@ -25,7 +25,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: - fetch-depth: 16 + fetch-depth: 0 persist-credentials: false sparse-checkout-cone-mode: false sparse-checkout: | @@ -59,7 +59,7 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 with: - fetch-depth: 16 + fetch-depth: 0 persist-credentials: false sparse-checkout-cone-mode: false sparse-checkout: | From f60f4fc331c4ef0c48fa491985ab8883f688908e Mon Sep 17 00:00:00 2001 From: joshavant <830519+joshavant@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:13:41 -0500 Subject: [PATCH 6/7] fix(ci): preserve paired qualification under patched Node --- .../patches/013-node-reference-fixtures.patch | 17 +++++++++++++++++ .github/openclaw/qualify.sh | 13 +++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 .github/openclaw/qualification/patches/013-node-reference-fixtures.patch diff --git a/.github/openclaw/qualification/patches/013-node-reference-fixtures.patch b/.github/openclaw/qualification/patches/013-node-reference-fixtures.patch new file mode 100644 index 000000000000..9f7e03e92b44 --- /dev/null +++ b/.github/openclaw/qualification/patches/013-node-reference-fixtures.patch @@ -0,0 +1,17 @@ +diff --git a/test/js/node/process/process.test.js b/test/js/node/process/process.test.js +--- a/test/js/node/process/process.test.js ++++ b/test/js/node/process/process.test.js +@@ -1056,3 +1056,3 @@ + let [out, exited] = await Promise.all([new Response(subprocess.stdout).text(), subprocess.exited]); +- expect(out.trim()).toEqual("v26.3.0"); ++ expect(out.trim()).toEqual("v26.12.1"); + expect(exited).toBe(0); +diff --git a/test/js/node/tls/node-tls-client-wrap-fixture.mjs b/test/js/node/tls/node-tls-client-wrap-fixture.mjs +--- a/test/js/node/tls/node-tls-client-wrap-fixture.mjs ++++ b/test/js/node/tls/node-tls-client-wrap-fixture.mjs +@@ -305,3 +305,4 @@ + const host = "127.0.0.1"; +- const options = { session, rejectUnauthorized: false }; ++ // Node binds reusable sessions to the peer identity, including over an existing socket. ++ const options = { session, host, rejectUnauthorized: false }; + // The listener is attached in the turn that makes the socket: a wrap starts its handshake at once. diff --git a/.github/openclaw/qualify.sh b/.github/openclaw/qualify.sh index 46307594e746..56d89df79c8f 100644 --- a/.github/openclaw/qualify.sh +++ b/.github/openclaw/qualify.sh @@ -20,6 +20,9 @@ git apply "$inputs/patches/000-checksums.patch" # Apply openclaw/bun#151 equally to both frozen-source arms. git apply "$inputs/patches/012-vm-finalization-fixture.patch" sha256sum "$inputs/patches/012-vm-finalization-fixture.patch" > "$QUALIFICATION_DIR/vm-finalization-fixture.sha256" +# Match the qualified host Node and preserve its authenticated session identity in both arms. +git apply "$inputs/patches/013-node-reference-fixtures.patch" +sha256sum "$inputs/patches/013-node-reference-fixtures.patch" > "$QUALIFICATION_DIR/node-reference-fixtures.sha256" bootstrap=$(command -v bun) "$bootstrap" install --frozen-lockfile (cd test && "$bootstrap" install --frozen-lockfile) @@ -58,9 +61,14 @@ for arm in baseline candidate; do cp "$inputs/selected.json" build/openclaw-ci/selected.json cp "$inputs/selected.json" "$results/selected.json" rm -f build/openclaw-ci/results.json - env -u BUN_WEBKIT_ARTIFACT_MANIFEST -u BUN_BUILD_CACHE_DIR -u BUN_BUILD_PREFETCH_DIR \ + # Retain both arms when a reference fails; the strict all-pass gate below still rejects failures. + if env -u BUN_WEBKIT_ARTIFACT_MANIFEST -u BUN_BUILD_CACHE_DIR -u BUN_BUILD_PREFETCH_DIR \ HOME="$results/home" TMPDIR="$results/tmp" PATH="$results/bin:$PATH" OPENCLAW_CI_PLATFORM=linux \ - "$candidate" scripts/openclaw-ci/tests.ts test > "$results/tests.log" 2>&1 + "$candidate" scripts/openclaw-ci/tests.ts test > "$results/tests.log" 2>&1; then + printf '0\n' > "$results/exit-code.log" + else + printf '%s\n' "$?" > "$results/exit-code.log" + fi cp build/openclaw-ci/results.json "$results/results.json" done python3 "$inputs/compare-fork-ci.py" "$QUALIFICATION_DIR/baseline" "$QUALIFICATION_DIR/candidate" --output "$QUALIFICATION_DIR/faithfulness.json" @@ -96,6 +104,7 @@ from pathlib import Path root=Path(sys.argv[1]) faith=json.loads((root/'faithfulness.json').read_text()) assert faith['identical_file_outcomes'] and faith['all_passed'] +assert all((root/arm/'exit-code.log').read_text().strip() == '0' for arm in ['baseline','candidate']), 'fork test runner failed' assert faith['selected_files']==44 and faith['result_files']==46 for name in ['als-plugin','als-native']: rows=json.loads((root/(name+'.json')).read_text())['results'] From 91d589bc4e0e0f9fff81045f27ea805c17b8a3fc Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Mon, 2 Nov 2026 18:00:24 +0800 Subject: [PATCH 7/7] test(tls): qualify constructor sessions and resumed peer certificates (#39) * test(tls): qualify Node constructor session normalization * test(tls): bound native Node compilation separately from behavior checks * fix(tls): retain resumed peer certificates in constructor proof --- .../node-session/constructor-session.patch | 62 +++++ .../qualification/node-session/qualify.py | 247 ++++++++++++++++++ .../test-tls-session-constructor.js | 158 +++++++++++ .github/workflows/openclaw-node-session.yml | 65 +++++ 4 files changed, 532 insertions(+) create mode 100644 .github/openclaw/qualification/node-session/constructor-session.patch create mode 100644 .github/openclaw/qualification/node-session/qualify.py create mode 100644 .github/openclaw/qualification/node-session/test-tls-session-constructor.js create mode 100644 .github/workflows/openclaw-node-session.yml diff --git a/.github/openclaw/qualification/node-session/constructor-session.patch b/.github/openclaw/qualification/node-session/constructor-session.patch new file mode 100644 index 000000000000..cb9cbc55d55b --- /dev/null +++ b/.github/openclaw/qualification/node-session/constructor-session.patch @@ -0,0 +1,62 @@ +diff --git a/lib/internal/tls/wrap.js b/lib/internal/tls/wrap.js +index 82f343de726023a1ed85c98340235c372b3d1089..d7e2f13ce7e91534f57b9a6c4bd00cde206b6892 100644 +--- a/lib/internal/tls/wrap.js ++++ b/lib/internal/tls/wrap.js +@@ -625,7 +625,7 @@ + */ + + function TLSSocket(socket, opts) { +- const tlsOptions = { ...opts }; ++ const tlsOptions = { ...opts, socket }; + validateOpenSSLEngineOptions(tlsOptions); + let enableTrace = tlsOptions.enableTrace; + +@@ -859,7 +859,7 @@ + + TLSSocket.prototype[kReinitializeHandle] = function reinitializeHandle(handle) { + const originalServername = this.ssl ? this._handle.getServername() : null; +- const originalSession = this.ssl ? this._handle.getSession() : null; ++ const originalSession = this.ssl ? this.getSession() : null; + + this.handle = this._wrapHandle(null, handle, false); + this.ssl = this._handle; +@@ -1006,7 +1006,7 @@ + ssl.onocspresponse = onocspresponse; + + if (options.session) +- ssl.setSession(options.session); ++ this.setSession(options.session); + + ssl.onnewsession = onnewsessionclient; + +@@ -1877,8 +1877,6 @@ + ); + } + +- options.session = getSessionForReuse(options.session, options); +- + const tlssock = new TLSSocket(options.socket, { + allowHalfOpen: options.allowHalfOpen, + pipe: !!options.path, +@@ -1886,7 +1884,6 @@ + isServer: false, + requestCert: true, + rejectUnauthorized: options.rejectUnauthorized !== false, +- session: options.session, + ALPNProtocols: options.ALPNProtocols, + requestOCSP: options.requestOCSP, + enableTrace: options.enableTrace, +diff --git a/src/crypto/crypto_common.cc b/src/crypto/crypto_common.cc +index 759353f08f035f3a805f01cecfad3e2aee6d21f7..50dbe4c2ce86f32ef830369e4279b614b6c2d830 100644 +--- a/src/crypto/crypto_common.cc ++++ b/src/crypto/crypto_common.cc +@@ -293,6 +293,9 @@ + // contains the `peer_certificate`, but on server it doesn't. + X509Pointer cert(is_server ? X509Pointer::PeerFrom(ssl) : X509Pointer()); + STACK_OF(X509)* ssl_certs = SSL_get_peer_cert_chain(ssl.get()); ++ // A resumed client can retain its peer certificate without a peer chain. ++ if (!is_server && (ssl_certs == nullptr || sk_X509_num(ssl_certs) == 0)) ++ cert = X509Pointer::PeerFrom(ssl); + if (!cert && (ssl_certs == nullptr || sk_X509_num(ssl_certs) == 0)) + return Undefined(env->isolate()); + diff --git a/.github/openclaw/qualification/node-session/qualify.py b/.github/openclaw/qualification/node-session/qualify.py new file mode 100644 index 000000000000..8e19ccbc3db3 --- /dev/null +++ b/.github/openclaw/qualification/node-session/qualify.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +"""Prove the constructor-session fix using real, source-bound Node builds.""" +import hashlib +import io +import json +import os +from pathlib import Path +import shutil +import subprocess +import sys +import tarfile +import zipfile + +# This path also runs after build/test failures; only its bounded final archive is uploaded. +if len(sys.argv) == 4 and sys.argv[1] == '--pack': + root, target = Path(sys.argv[2]), Path(sys.argv[3]) + files = sorted(root.iterdir()) + if not files or len(files) > 12 or any(not p.is_file() or p.is_symlink() for p in files): + raise ValueError('unexpected Node proof evidence file set') + if sum(p.stat().st_size for p in files) > 256 * 1024 ** 2: + raise ValueError('Node proof raw evidence exceeds 256 MiB') + manifest = {} + for p in files: + with p.open('rb') as stream: + manifest[p.name] = {'size': p.stat().st_size, + 'sha256': hashlib.file_digest(stream, 'sha256').hexdigest()} + pending = target.with_suffix(target.suffix + '.pending') + with tarfile.open(pending, 'w:gz', compresslevel=6) as archive: + for p in files: + archive.add(p, arcname=p.name, recursive=False) + content = (json.dumps(manifest, indent=2) + '\n').encode() + record = tarfile.TarInfo('archive-manifest.json');record.size = len(content) + archive.addfile(record, io.BytesIO(content)) + if pending.stat().st_size > 32 * 1024 ** 2: + raise ValueError('Node proof archive exceeds 32 MiB; pending archive will not be uploaded') + with pending.open('rb') as stream: + sha = hashlib.file_digest(stream, 'sha256').hexdigest() + pending.rename(target) + print('Node proof archive SHA256 ' + sha) + raise SystemExit(0) + +PIN = 'd44625a4b80c672215b469906ce583ce7eeeebaf' +WRAP = 'lib/internal/tls/wrap.js' +BASE_BLOBS = {WRAP: '82f343de726023a1ed85c98340235c372b3d1089', + 'src/crypto/crypto_common.cc': '759353f08f035f3a805f01cecfad3e2aee6d21f7'} +INPUTS = Path(__file__).resolve().parent +SOURCE = Path(sys.argv[1]).resolve() +OUTPUT = Path(sys.argv[2]).resolve() +BASELINE = Path(sys.argv[3]).resolve() +OUTPUT.mkdir(parents=True, exist_ok=False) +GIB = 1024 ** 3 +BUILD_TIMEOUTS = {'patched': 10800} +memory = int(next(line.split()[1] for line in Path('/proc/meminfo').read_text().splitlines() + if line.startswith('MemTotal:'))) * 1024 +cgroup_limit = Path('/sys/fs/cgroup/memory.max') +if cgroup_limit.exists() and cgroup_limit.read_text().strip().isdigit(): + memory = min(memory, int(cgroup_limit.read_text())) +jobs = min(4, os.cpu_count() or 1, max(1, memory // (3 * GIB))) +env = {key: value for key, value in os.environ.items() + if key not in ('GH_TOKEN', 'GITHUB_TOKEN', 'ACTIONS_RUNTIME_TOKEN', + 'ACTIONS_ID_TOKEN_REQUEST_TOKEN')} +env.update(CC='gcc', CXX='g++', CARGO_BUILD_JOBS=str(jobs)) + + +def command(argv, **kwargs): + return subprocess.run(argv, cwd=SOURCE, env=env, check=True, **kwargs) + + +def git(*args): + return subprocess.check_output(['git', *args], cwd=SOURCE).decode().strip() + + +def digest(path): + with path.open('rb') as stream: + return hashlib.file_digest(stream, 'sha256').hexdigest() + + +def write(name, value): + with (OUTPUT / name).open('x') as stream: + stream.write(json.dumps(value, indent=2) + '\n') + + +def build(arm): + try: + with (OUTPUT / (arm + '-build.log')).open('x') as stream: + command(['/usr/bin/time', '-v', 'make', '-j' + str(jobs)], + stdout=stream, stderr=subprocess.STDOUT, timeout=BUILD_TIMEOUTS[arm]) + finally: + # Check after either build outcome. Transient products stay on this disposable runner. + size = int(subprocess.check_output(['du', '-sk', str(SOURCE)]).split()[0]) * 1024 + if size > 24 * GIB: + raise ValueError('Node source/build workspace exceeded its 24-GiB budget') + executable = SOURCE / 'out/Release/node' + script = """ +const crypto = require('node:crypto'); +console.log(JSON.stringify({ + version: process.version, platform: process.platform, arch: process.arch, + openssl: process.versions.openssl, + builtin_sha256: crypto.createHash('sha256') + .update(process.binding('natives')['internal/tls/wrap']).digest('hex') +})); +""" + identity = json.loads(subprocess.check_output( + [str(executable), '--no-warnings', '-e', script], cwd=SOURCE, env=env)) + if identity['platform'] != 'linux' or identity['arch'] != 'x64': + raise ValueError('native Linux x64 Node required') + if identity['builtin_sha256'] != digest(SOURCE / WRAP): + raise ValueError('built Node does not embed the measured TLS owner source') + identity.update(source=PIN, executable_sha256=digest(executable), + workspace_bytes=size) + write(arm + '-identity.json', identity) + return executable, identity + + +def probe(executable, arm): + with (OUTPUT / (arm + '-test.log')).open('x') as stream: + result = subprocess.run([str(executable), '--expose-internals', 'test/parallel/test-tls-session-constructor.js'], + cwd=SOURCE, env=env, stdout=stream, + stderr=subprocess.STDOUT, timeout=120) + return check_probe(arm, result.returncode) + + +def check_probe(arm, exit_code): + lines = (OUTPUT / (arm + '-test.log')).read_text().splitlines() + rows = [json.loads(line.removeprefix('SESSION_CASE ')) for line in lines + if line.startswith('SESSION_CASE ')] + if arm == 'base': + if not (exit_code == 1 and len(rows) == 1 + and rows[0] == {'label': 'TLSv1.2/buffer/connected/explicit SNI', + 'version': 'TLSv1.2', 'connected': True, + 'expectedReuse': True, 'reused': False}): + raise ValueError('base did not reproduce the constructor resumption failure') + else: + summaries = [json.loads(line.removeprefix('SESSION_RESULT ')) for line in lines + if line.startswith('SESSION_RESULT ')] + fallback = [line for line in lines if line.startswith('SESSION_FALLBACK ')] + keys = {(row['label'], row['version'], row['connected']) for row in rows} + if not (exit_code == 0 and len(rows) == len(keys) == 80 + and all(row['reused'] is row['expectedReuse'] for row in rows) + and summaries == [{'passed': True, 'cases': 82}] + and set(fallback) == {'SESSION_FALLBACK TLSv1.2', 'SESSION_FALLBACK TLSv1.3'} + and len(fallback) == 2): + raise ValueError('patched Node constructor/identity/handle-swap matrix incomplete') + return {'exit_code': exit_code, 'reported_cases': len(rows)} + + +if git('rev-parse', 'HEAD') != PIN or git('status', '--porcelain'): + raise ValueError('Node source checkout is not the pristine frozen revision') +if any(git('rev-parse', 'HEAD:' + name) != blob for name, blob in BASE_BLOBS.items()): + raise ValueError('Node TLS source blob drift') +patch = INPUTS / 'constructor-session.patch' +fixture = INPUTS / 'test-tls-session-constructor.js' +# Reuse the already observed red baseline; its native build and fixture inputs are frozen. +if digest(BASELINE) != 'e7b9f513cfc578c97a1462d9762187a3968f0c17e7aff20ec6ed8d83f16ddcf2': + raise ValueError('baseline ZIP digest mismatch') +with zipfile.ZipFile(BASELINE) as archive: + if archive.namelist() != ['node-session-proof.tar.gz']: + raise ValueError('unexpected baseline ZIP entries') + info = archive.getinfo('node-session-proof.tar.gz') + if info.file_size > 32 * 1024**2: + raise ValueError('baseline archive exceeds size budget') + data = archive.read(info) +if hashlib.sha256(data).hexdigest() != 'bd4aded401d008b538e669af6b1403ef8e50c5dbc58566f11810f0da847c000f': + raise ValueError('baseline TAR digest mismatch') +with tarfile.open(fileobj=io.BytesIO(data), mode='r:gz') as archive: + entries = archive.getmembers() + expected = {'archive-manifest.json', 'inputs.json', 'tools.log', 'configure.log', + 'base-build.log', 'base-identity.json', 'base-test.log', + 'patched-build.log', 'patched-identity.json', 'patched-test.log'} + if (len(entries) != len(expected) or {entry.name for entry in entries} != expected + or any(not entry.isfile() for entry in entries) + or sum(entry.size for entry in entries) > 256 * 1024**2): + raise ValueError('unexpected baseline TAR inventory') + contents = {entry.name: archive.extractfile(entry).read() for entry in entries} +manifest = json.loads(contents.pop('archive-manifest.json')) +if set(manifest) != set(contents): + raise ValueError('baseline manifest inventory mismatch') +for name, content in contents.items(): + if (len(content) != manifest[name]['size'] + or hashlib.sha256(content).hexdigest() != manifest[name]['sha256']): + raise ValueError('baseline manifest mismatch: ' + name) +prior_inputs = json.loads(contents['inputs.json']) +base_identity = json.loads(contents['base-identity.json']) +if (prior_inputs['source'] != PIN or prior_inputs['fixture_sha256'] != digest(fixture) + or base_identity['source'] != PIN + or base_identity['builtin_sha256'] != digest(SOURCE / WRAP) + or '\tExit status: 0' not in contents['base-build.log'].decode()): + raise ValueError('baseline source, embedded wrapper, fixture or successful build mismatch') +for name in ('base-build.log', 'base-identity.json', 'base-test.log'): + (OUTPUT / name).write_bytes(contents[name]) +base_result = check_probe('base', 1) +write('baseline-provenance.json', { + 'kind': 'prior-run-baseline', 'run': 41183895846, 'artifact': 13039247663, + 'producer': 'e1fd554b61300f5853c9ce6f9629a6205de8ec0e', + 'zip_sha256': digest(BASELINE), 'tar_sha256': hashlib.sha256(data).hexdigest(), + 'inputs': prior_inputs, 'files': {name: manifest[name] for name in + ('base-build.log', 'base-identity.json', 'base-test.log')}}) +destination = SOURCE / 'test/parallel' / fixture.name +if destination.exists(): + raise ValueError('refusing to replace an existing Node test') +shutil.copyfile(fixture, destination) +write('inputs.json', {'source': PIN, 'patch_sha256': digest(patch), + 'fixture_sha256': digest(fixture), 'compiler_jobs': jobs, + 'memory_bytes': memory, 'build_timeout_seconds': BUILD_TIMEOUTS, + 'workspace_post_build_max_bytes': 24 * GIB, + 'artifact_max_bytes': 32 * 1024 ** 2, + 'artifact_max_input_bytes': 256 * 1024 ** 2, 'artifact_max_input_files': 12}) +with (OUTPUT / 'tools.log').open('x') as stream: + for argv in (['gcc', '--version'], ['g++', '--version'], ['python3', '--version'], + ['make', '--version'], ['rustc', '--version']): + command(argv, stdout=stream, stderr=subprocess.STDOUT) +command(['git', 'apply', '--check', str(patch)]) +command(['git', 'apply', str(patch)]) +if git('diff', '--name-only').splitlines() != sorted(BASE_BLOBS): + raise ValueError('Node patch changed an unexpected tracked source') +patched_source_sha256 = {name: digest(SOURCE / name) for name in BASE_BLOBS} +with (OUTPUT / 'configure.log').open('x') as stream: + command(['./configure', '--without-npm'], stdout=stream, stderr=subprocess.STDOUT) +patched, patched_identity = build('patched') +if patched_identity['executable_sha256'] == base_identity['executable_sha256']: + raise ValueError('patched Node executable did not change') +if any(patched_identity[key] != base_identity[key] + for key in ('version', 'platform', 'arch', 'openssl')): + raise ValueError('Node build platform or linked OpenSSL drift') +patched_result = probe(patched, 'patched') +with (OUTPUT / 'siblings.log').open('x') as stream: + command(['python3', 'tools/test.py', '--mode=release', '--timeout=120', + 'parallel/test-tls-session-reuse-different-servername', + 'parallel/test-tls-session-reuse-provided-socket', + 'parallel/test-tls-secure-session', + 'parallel/test-tls-session-constructor', + 'parallel/test-tls-peer-certificate-repeated-reads'], + stdout=stream, stderr=subprocess.STDOUT, timeout=600) +if digest(destination) != digest(fixture): + raise ValueError('Node test fixture changed during qualification') +if (git('diff', '--name-only').splitlines() != sorted(BASE_BLOBS) + or {name: digest(SOURCE / name) for name in BASE_BLOBS} != patched_source_sha256): + raise ValueError('Node source changed during qualification') +write('gate.json', {'passed': True, 'source': PIN, 'base': base_result, + 'patched': patched_result, 'sibling_files': 5, + 'patched_sources': patched_source_sha256, + 'baseline_kind': 'prior-run-baseline', + 'baseline_provenance_sha256': digest(OUTPUT / 'baseline-provenance.json'), + 'inputs_sha256': digest(OUTPUT / 'inputs.json'), + 'base_identity_sha256': digest(OUTPUT / 'base-identity.json'), + 'patched_identity_sha256': digest(OUTPUT / 'patched-identity.json')}) +print('native constructor-session prior baseline/fresh patched and sibling proof passed') diff --git a/.github/openclaw/qualification/node-session/test-tls-session-constructor.js b/.github/openclaw/qualification/node-session/test-tls-session-constructor.js new file mode 100644 index 000000000000..284d4fa55e3c --- /dev/null +++ b/.github/openclaw/qualification/node-session/test-tls-session-constructor.js @@ -0,0 +1,158 @@ +// Flags: --expose-internals +'use strict'; +const common = require('../common'); +if (!common.hasCrypto) + common.skip('missing crypto'); + +const assert = require('assert'); +const { once } = require('events'); +const net = require('net'); +const tls = require('tls'); +const fixtures = require('../common/fixtures'); +const { hasMultiLocalhost } = require('../common/net'); +const ca = fixtures.readKey('ca1-cert.pem'); +const lookup = (_host, options, callback) => { + callback(null, options.all ? [{ address: '127.0.0.1', family: 4 }] : + '127.0.0.1', 4); +}; +let cases = 0; + +async function capture(port, version) { + const client = tls.connect({ + host: 'agent1', port, lookup, ca, servername: 'agent1', + minVersion: version, maxVersion: version, + }); + const closed = new Promise((resolve) => client.once('close', resolve)); + const ticket = once(client, 'session'); + client.resume(); + try { + const [, [session]] = await Promise.all([once(client, 'secureConnect'), ticket]); + assert.strictEqual(client.authorized, true); + assert.strictEqual(client.isSessionReused(), false); + const raw = client._handle.getSession(); + assert(Buffer.isBuffer(session)); + assert(Buffer.isBuffer(raw)); + return { session, raw }; + } finally { + client.destroy(); + await closed; + } +} + +async function direct(port, version, session, connected, options, + expectedReuse, label, setter = false) { + const transport = net.connect({ host: 'agent1', port, lookup }); + let client; + try { + if (connected) + await once(transport, 'connect'); + client = new tls.TLSSocket(transport, { + ca, rejectUnauthorized: true, + minVersion: version, maxVersion: version, + ...options, + ...(setter ? {} : { session }), + }); + const closed = new Promise((resolve) => client.once('close', resolve)); + try { + if (setter) + client.setSession(session); + // A direct TLSSocket caller owns handshake startup and peer verification. + client.setServername('agent1'); + const secure = once(client, 'secure'); + client._start(); + client.resume(); + await secure; + assert.strictEqual(client._handle.verifyError(), null); + assert.strictEqual( + tls.checkServerIdentity('agent1', client.getPeerCertificate()), undefined); + const reused = client.isSessionReused(); + console.log('SESSION_CASE ' + JSON.stringify({ + label, version, connected, expectedReuse, reused, + })); + assert.strictEqual(reused, expectedReuse, label); + cases++; + } finally { + client.destroy(); + await closed; + } + } finally { + transport.destroy(); + } +} + +async function testVersion(version) { + const server = tls.createServer({ + key: fixtures.readKey('agent1-key.pem'), + cert: fixtures.readKey('agent1-cert.pem'), + minVersion: version, maxVersion: version, + }, (socket) => socket.end('ok')); + server.listen(0, '127.0.0.1'); + await once(server, 'listening'); + try { + const { port } = server.address(); + const { session, raw } = await capture(port, version); + for (const connected of [true, false]) { + for (const encoding of ['buffer', 'latin1']) { + const encode = (value) => encoding === 'buffer' ? value : + value.toString('latin1'); + const wrapped = encode(session); + const label = `${version}/${encoding}/${connected ? 'connected' : 'connecting'}`; + for (const [name, options, expected] of [ + ['explicit SNI', { servername: 'agent1' }, true], + ['socket host', {}, true], + ['positional socket', { socket: { _host: 'agent3' } }, true], + ['explicit host', { host: 'agent1' }, true], + ['SNI precedes host', { servername: 'agent1', host: 'agent3' }, true], + ['foreign SNI', { servername: 'agent3' }, false], + ['host precedes socket', { host: 'agent3' }, false], + ]) { + await direct(port, version, wrapped, connected, options, + expected, `${label}/${name}`); + } + await direct(port, version, wrapped, connected, {}, true, + `${label}/setSession`, true); + await direct(port, version, encode(raw), connected, + { servername: 'agent1' }, false, `${label}/verified raw`); + await direct(port, version, encode(raw), connected, + { servername: 'agent1', rejectUnauthorized: false }, + true, `${label}/unverified raw`); + } + } + if (hasMultiLocalhost()) { + const client = tls.connect({ + host: 'agent1', port, ca, servername: 'agent1', session, + minVersion: version, maxVersion: version, + autoSelectFamily: true, + lookup(_host, options, callback) { + assert.strictEqual(options.all, true); + callback(null, [ + { address: '127.0.0.2', family: 4 }, + { address: '127.0.0.1', family: 4 }, + ]); + }, + }); + const closed = new Promise((resolve) => client.once('close', resolve)); + client.resume(); + try { + await once(client, 'secureConnect'); + assert.strictEqual(client.authorized, true); + assert.strictEqual(client.isSessionReused(), true); + cases++; + console.log('SESSION_FALLBACK ' + version); + } finally { + client.destroy(); + await closed; + } + } + } finally { + const closed = once(server, 'close'); + server.close(); + await closed; + } +} + +(async function() { + await testVersion('TLSv1.2'); + await testVersion('TLSv1.3'); + console.log('SESSION_RESULT ' + JSON.stringify({ passed: true, cases })); +})().then(common.mustCall()); diff --git a/.github/workflows/openclaw-node-session.yml b/.github/workflows/openclaw-node-session.yml new file mode 100644 index 000000000000..acb943ce6f4c --- /dev/null +++ b/.github/workflows/openclaw-node-session.yml @@ -0,0 +1,65 @@ +name: OpenClaw Node TLS session proof +on: + pull_request: + paths: + - .github/workflows/openclaw-node-session.yml + - .github/openclaw/qualification/node-session/** + workflow_dispatch: +permissions: + actions: read + contents: read +concurrency: + group: openclaw-node-session-${{ github.ref }} + cancel-in-progress: true +jobs: + native-source-proof: + if: >- + github.repository == 'openclaw/WebKit' && + ((github.event_name == 'pull_request' && + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.head.ref == 'claude/node-session-constructor-proof-20261031') || + (github.event_name == 'workflow_dispatch' && + github.ref == 'refs/heads/claude/node-session-constructor-proof-20261031')) + runs-on: ubuntu-24.04 + timeout-minutes: 240 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + ref: ${{ github.event.pull_request.head.sha || github.sha }} + path: carrier + persist-credentials: false + sparse-checkout: .github/openclaw/qualification/node-session + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 + with: + repository: nodejs/node + ref: d44625a4b80c672215b469906ce583ce7eeeebaf + path: node-source + persist-credentials: false + - name: Fetch the frozen successful baseline-build evidence + shell: bash + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + test "$(gh api repos/openclaw/WebKit/actions/runs/41183895846 --jq '.head_sha')" = e1fd554b61300f5853c9ce6f9629a6205de8ec0e + test "$(gh api repos/openclaw/WebKit/actions/artifacts/13039247663 --jq '.workflow_run.id')" = 41183895846 + gh api repos/openclaw/WebKit/actions/artifacts/13039247663/zip > "$RUNNER_TEMP/node-baseline.zip" + - name: Prove the constructor fix on a fresh patched Node build + shell: bash + run: >- + python3 carrier/.github/openclaw/qualification/node-session/qualify.py + "$GITHUB_WORKSPACE/node-source" "$RUNNER_TEMP/node-session-proof" + "$RUNNER_TEMP/node-baseline.zip" + - name: Pack bounded evidence after either outcome + if: always() + shell: bash + run: >- + python3 carrier/.github/openclaw/qualification/node-session/qualify.py --pack + "$RUNNER_TEMP/node-session-proof" "$RUNNER_TEMP/node-session-proof.tar.gz" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a + if: always() + with: + name: native-node-session-proof + path: ${{ runner.temp }}/node-session-proof.tar.gz + if-no-files-found: warn + retention-days: 7