diff --git a/.github/workflows/code-scanning.yaml b/.github/workflows/code-scanning.yaml index d00a7707b3f..63d7745bf04 100644 --- a/.github/workflows/code-scanning.yaml +++ b/.github/workflows/code-scanning.yaml @@ -66,25 +66,12 @@ jobs: scripts/shellcheck-json1-to-sarif.mts sparse-checkout-cone-mode: false - - name: Detect trusted ShellCheck converter - id: converter - run: | - set -euo pipefail - if [ -f trusted-shellcheck-converter/scripts/shellcheck-json1-to-sarif.mts ]; then - echo "present=true" >> "$GITHUB_OUTPUT" - else - echo "present=false" >> "$GITHUB_OUTPUT" - echo "Trusted base revision does not yet contain the ShellCheck converter; conversion and upload begin after this helper lands on the base branch." - fi - - name: Setup Node.js - if: steps.converter.outputs.present == 'true' uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: 22.19.0 - name: Install ShellCheck - if: steps.converter.outputs.present == 'true' run: | set -euo pipefail probe="$RUNNER_TEMP/shellcheck-json1-probe.sh" @@ -117,7 +104,6 @@ jobs: - name: Collect shell files id: shell-files - if: steps.converter.outputs.present == 'true' working-directory: source run: | git ls-files -z -- '*.sh' 'install.sh' 'uninstall.sh' | sort -zu > "$GITHUB_WORKSPACE/shell-files.txt" @@ -128,7 +114,7 @@ jobs: fi - name: Generate ShellCheck SARIF - if: steps.converter.outputs.present == 'true' && steps.shell-files.outputs.has_files == 'true' + if: steps.shell-files.outputs.has_files == 'true' working-directory: source run: | # Ubuntu's packaged ShellCheck may not support --format=sarif. @@ -170,7 +156,7 @@ jobs: - name: Check SARIF has runs id: sarif-runs - if: steps.converter.outputs.present == 'true' && steps.shell-files.outputs.has_files == 'true' + if: steps.shell-files.outputs.has_files == 'true' run: | run_count="$(jq '.runs | length' shellcheck.sarif)" if [ "$run_count" -gt 0 ]; then @@ -181,7 +167,7 @@ jobs: fi - name: Upload ShellCheck SARIF - if: steps.converter.outputs.present == 'true' && steps.shell-files.outputs.has_files == 'true' && steps.sarif-runs.outputs.has_runs == 'true' + if: steps.shell-files.outputs.has_files == 'true' && steps.sarif-runs.outputs.has_runs == 'true' uses: github/codeql-action/upload-sarif@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4 with: sarif_file: shellcheck.sarif diff --git a/.github/workflows/codebase-growth-guardrails.yaml b/.github/workflows/codebase-growth-guardrails.yaml index 21fc0ea7df1..761e749e39d 100644 --- a/.github/workflows/codebase-growth-guardrails.yaml +++ b/.github/workflows/codebase-growth-guardrails.yaml @@ -113,26 +113,10 @@ jobs: ref: ${{ github.event.pull_request.base.sha }} persist-credentials: false - - name: Detect guardrail tools on the base revision - id: tools - # The tools live in the base tree. On the PR that first adds them, the - # base revision predates them, so skip until the change lands on base. - run: | - set -euo pipefail - if [ -f tools/growth-guardrails/test-size-budget.mts ] \ - && [ -f tools/growth-guardrails/test-conditionals.mts ]; then - echo "present=true" >> "$GITHUB_OUTPUT" - else - echo "present=false" >> "$GITHUB_OUTPUT" - echo "Trusted base revision does not yet contain the growth-guardrail tools; the policy applies once this change lands on the base branch." - fi - - name: Install trusted dependencies - if: steps.tools.outputs.present == 'true' run: npm ci --ignore-scripts --no-audit --no-fund - name: Require changed test files to stay within size budget - if: steps.tools.outputs.present == 'true' env: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number }} @@ -145,7 +129,6 @@ jobs: node --experimental-strip-types tools/growth-guardrails/test-size-budget.mts - name: Require changed test files not to add if statements - if: steps.tools.outputs.present == 'true' env: GH_TOKEN: ${{ github.token }} PR_NUMBER: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/installer-hash-check.yaml b/.github/workflows/installer-hash-check.yaml index 4265f551413..4dbeb427aac 100644 --- a/.github/workflows/installer-hash-check.yaml +++ b/.github/workflows/installer-hash-check.yaml @@ -4,10 +4,9 @@ # Verifies pinned installer SHA-256 hashes still match upstream scripts. # Checked: allowlisted OpenShell installer and Brev release assets. # Reports the required network-backed drift check on every PR, every push to -# main, and weekly. Pull requests execute checker code from their base commit; -# the immutable bootstrap is used only for the PR that first adds that action. -# A new release-manifest allowlist entry must therefore land on main in a -# prerequisite PR before a later PR changes runtime selectors to that release. +# main, and weekly. Pull requests execute checker code from their base commit. +# A new release-manifest allowlist entry must land on main in a prerequisite PR +# before a later PR changes runtime selectors to that release. name: Security / Installer Hash Check @@ -42,9 +41,8 @@ jobs: with: node-version: 22.19.0 - # The full PR-head checkout below supplies data only. Its checker and pin - # parser are never executed: later steps run exclusively from either - # .trusted-installer-hash or .bootstrap-installer-hash. + # The checkout of the latest PR commit supplies data only. The checker and + # pin parser execute from the checkout of the PR base SHA below. - name: Checkout pull request head if: github.event_name == 'pull_request' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -72,127 +70,12 @@ jobs: scripts/checks/extract-installer-pins.mts sparse-checkout-cone-mode: false - - name: Detect base-trusted installer hash action - id: trusted-installer-hash - if: github.event_name == 'pull_request' - shell: bash - run: | - if [[ -f .trusted-installer-hash/.github/actions/ci-installer-hash-check/action.yaml ]]; then - echo "available=true" >> "$GITHUB_OUTPUT" - else - echo "available=false" >> "$GITHUB_OUTPUT" - fi - - # invalidState: the first PR that introduces this action has no copy in - # its base commit. Running the mutable PR-side checker would let that PR - # authorize its own installer pins. - # sourceBoundary: this exact commit and reviewed Git tree contain the - # trusted action and checker; the PR head supplies only inspected files. - # whyNotSourceFix: a base commit cannot contain a new action before the - # introducing PR merges, so the bootstrap must name immutable code once. - # regressionTest: test/pr-workflow-contract.test.ts rejects mutable - # checker execution, non-immutable refs, and a mismatched reviewed tree. - # manualReviewEvidence: on 2026-07-02, independent Git object inspection - # confirmed commit cb5e9aefab2b16fedc0995149fc3520da0d5e0c7 has - # tree 1fdf59efe40b78c407e222fd42043b23a61e199a. The reviewed bootstrap - # script SHA-256 is 179e1572932eedc1a8ed974d534e9f2a5c34db7ebe971000dc20b77ed9d9feb3; - # its parser SHA-256 is - # e1d6b63a7b0378a3d28ee71d347ade2da75b3fcf2ff55aa55a9b54d2bc2fc13a; - # and its composite-action SHA-256 is - # 9c48c64cc934032c99a0aa9aa08b1164757988dc2842e1df88d1b7252ce1183f. - # removalCondition: remove the bootstrap checkout after this workflow has - # landed on every supported PR base. The fallback is refused after the - # explicit 180-day review window ending 2026-12-29T19:35:41Z. - - name: Enforce immutable installer hash bootstrap expiry - if: >- - github.event_name == 'pull_request' && - steps.trusted-installer-hash.outputs.available != 'true' - shell: bash - run: | - set -euo pipefail - node <<'NODE' - const commit = "cb5e9aefab2b16fedc0995149fc3520da0d5e0c7"; - const expiresAt = "2026-12-29T19:35:41Z"; - const expiresAtMs = Date.parse(expiresAt); - const canonicalExpiresAt = - Number.isFinite(expiresAtMs) && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/u.test(expiresAt) - ? new Date(expiresAtMs).toISOString().replace(".000Z", "Z") - : ""; - - if (!/^[a-f0-9]{40}$/u.test(commit) || canonicalExpiresAt !== expiresAt) { - console.error( - "::error::Immutable installer hash bootstrap expiry configuration is invalid; " + - "refusing the fallback. Expected a 40-character commit SHA and canonical UTC expiry.", - ); - process.exit(1); - } - - if (Date.now() >= expiresAtMs) { - console.error( - `::error::Immutable installer hash bootstrap ${commit} expired at ${expiresAt}. ` + - "Remove the bootstrap fallback or replace it with newly reviewed immutable checker code.", - ); - process.exit(1); - } - - const daysRemaining = Math.ceil((expiresAtMs - Date.now()) / 86_400_000); - console.log( - `Immutable installer hash bootstrap ${commit} remains valid for ${daysRemaining} day(s), ` + - `until ${expiresAt}.`, - ); - NODE - - - name: Checkout immutable installer hash bootstrap - if: >- - github.event_name == 'pull_request' && - steps.trusted-installer-hash.outputs.available != 'true' - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: cb5e9aefab2b16fedc0995149fc3520da0d5e0c7 - path: .bootstrap-installer-hash - persist-credentials: false - sparse-checkout: | - .github/actions/ci-installer-hash-check - scripts/check-installer-hash.sh - scripts/checks/extract-installer-pins.mts - sparse-checkout-cone-mode: false - - - name: Verify immutable installer hash bootstrap tree - if: >- - github.event_name == 'pull_request' && - steps.trusted-installer-hash.outputs.available != 'true' - shell: bash - run: | - set -euo pipefail - readonly expected_commit="cb5e9aefab2b16fedc0995149fc3520da0d5e0c7" - readonly expected_tree="1fdf59efe40b78c407e222fd42043b23a61e199a" - actual_commit="$(git -C .bootstrap-installer-hash rev-parse HEAD)" - actual_tree="$(git -C .bootstrap-installer-hash rev-parse 'HEAD^{tree}')" - if [[ "${actual_commit}" != "${expected_commit}" ]]; then - echo "::error::Immutable installer hash bootstrap checkout does not match the reviewed commit." >&2 - exit 1 - fi - if [[ "${actual_tree}" != "${expected_tree}" ]]; then - echo "::error::Immutable installer hash bootstrap checkout does not match the reviewed tree." >&2 - exit 1 - fi - - name: Verify pull request installer hashes from base-trusted code - if: >- - github.event_name == 'pull_request' && - steps.trusted-installer-hash.outputs.available == 'true' + if: github.event_name == 'pull_request' uses: ./.trusted-installer-hash/.github/actions/ci-installer-hash-check with: repo-root: ${{ github.workspace }} - - name: Verify pull request installer hashes from immutable bootstrap - if: >- - github.event_name == 'pull_request' && - steps.trusted-installer-hash.outputs.available != 'true' - uses: ./.bootstrap-installer-hash/.github/actions/ci-installer-hash-check - with: - repo-root: ${{ github.workspace }} - - name: Verify trusted event installer hashes if: github.event_name != 'pull_request' uses: ./.github/actions/ci-installer-hash-check diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 740fddd49d2..f2fc16a6d67 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -173,46 +173,9 @@ jobs: .github/actions/ci-install-dependencies.sh sparse-checkout-cone-mode: false - - name: Detect trusted installer integration action - id: trusted-installer-integration - shell: bash - run: | - if [ -f .trusted-ci-actions/.github/actions/ci-installer-integration/action.yaml ]; then - echo "available=true" >> "$GITHUB_OUTPUT" - else - echo "available=false" >> "$GITHUB_OUTPUT" - fi - - name: Run installer integration tests - if: ${{ steps.trusted-installer-integration.outputs.available == 'true' }} uses: ./.trusted-ci-actions/.github/actions/ci-installer-integration - - name: Setup Node.js for installer integration - if: ${{ steps.trusted-installer-integration.outputs.available != 'true' }} - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: "22" - cache: npm - - - name: Install installer integration dependencies - if: ${{ steps.trusted-installer-integration.outputs.available != 'true' }} - shell: bash - run: | - npm install --ignore-scripts - cd nemoclaw && npm install --ignore-scripts - - - name: Build installer integration artifacts - if: ${{ steps.trusted-installer-integration.outputs.available != 'true' }} - shell: bash - run: | - npm run build:cli - cd nemoclaw && npm run build - - - name: Run installer integration tests (bootstrap) - if: ${{ steps.trusted-installer-integration.outputs.available != 'true' }} - shell: bash - run: CI=true npx vitest run --project installer-integration - wechat-runtime-audit: needs: changes if: needs.changes.outputs.code == 'true' @@ -235,54 +198,12 @@ jobs: .github/actions/ci-reviewed-npm-audit/verify-and-install-npm.sh sparse-checkout-cone-mode: false - - name: Detect trusted WeChat runtime audit - id: trusted-wechat-audit - shell: bash - run: | - if [ -f .trusted-wechat-audit/.github/actions/ci-wechat-runtime-audit/action.yaml ]; then - echo "available=true" >> "$GITHUB_OUTPUT" - else - echo "available=false" >> "$GITHUB_OUTPUT" - fi - - # One-time bootstrap for this PR only. The action is pinned to a signed, - # immutable commit that contains only the reviewed audit implementation. - # Every later PR must resolve the action from its base SHA above. - # Removal condition: delete this checkout and its paired audit step in the - # first follow-up after PR #6739 merges, before the next release tag. - - name: Checkout pinned bootstrap WeChat runtime audit - if: ${{ steps.trusted-wechat-audit.outputs.available != 'true' && github.event.pull_request.number == 6739 && github.event.pull_request.head.repo.full_name == 'HOYALIM/NemoClaw' }} - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - repository: HOYALIM/NemoClaw - ref: 0d2256d71d5bbba3bcaaaa4d01714fa56f22d1e2 - path: .trusted-wechat-audit-bootstrap - persist-credentials: false - sparse-checkout: | - .github/actions/ci-wechat-runtime-audit - sparse-checkout-cone-mode: false - - - name: Reject unavailable trusted WeChat runtime audit - if: ${{ steps.trusted-wechat-audit.outputs.available != 'true' && (github.event.pull_request.number != 6739 || github.event.pull_request.head.repo.full_name != 'HOYALIM/NemoClaw') }} - shell: bash - run: | - echo "::error::Trusted base SHA does not contain ci-wechat-runtime-audit" - exit 1 - - name: Audit locked WeChat runtime graph - if: ${{ steps.trusted-wechat-audit.outputs.available == 'true' }} uses: ./.trusted-wechat-audit/.github/actions/ci-wechat-runtime-audit with: target-root: ${{ github.workspace }} report-dir: artifacts/wechat-runtime-audit - - name: Audit locked WeChat runtime graph (pinned bootstrap) - if: ${{ steps.trusted-wechat-audit.outputs.available != 'true' && github.event.pull_request.number == 6739 && github.event.pull_request.head.repo.full_name == 'HOYALIM/NemoClaw' }} - uses: ./.trusted-wechat-audit-bootstrap/.github/actions/ci-wechat-runtime-audit - with: - target-root: ${{ github.workspace }} - report-dir: artifacts/wechat-runtime-audit - - name: Upload WeChat runtime audit evidence if: ${{ always() }} uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -319,64 +240,12 @@ jobs: scripts/lib/reviewed-npm-audit.mts sparse-checkout-cone-mode: false - - name: Detect trusted reviewed npm audit schema - id: trusted-reviewed-npm-audit - shell: bash - run: | - if grep -Fq 'NEMOCLAW_REVIEWED_NPM_AUDIT_TARGET_ROOT' \ - .trusted-reviewed-npm-audit/.github/actions/ci-reviewed-npm-audit/action.yaml \ - && grep -Fq 'resolveTrustedAuditConfigPath(TRUSTED_REPO_ROOT)' \ - .trusted-reviewed-npm-audit/scripts/audit-reviewed-npm-graph.mts \ - && [ -f .trusted-reviewed-npm-audit/ci/npm-audit-exceptions.json ] \ - && [ -f .trusted-reviewed-npm-audit/ci/reviewed-npm-audit.json ] \ - && [ -f .trusted-reviewed-npm-audit/scripts/lib/openclaw-npm-remediation.mts ] \ - && [ -f .trusted-reviewed-npm-audit/scripts/lib/reviewed-npm-audit.mts ]; then - echo "available=true" >> "$GITHUB_OUTPUT" - else - echo "available=false" >> "$GITHUB_OUTPUT" - fi - - # One-time bootstrap for this PR only. The action, driver, and helper are - # pinned to its signed first commit; every later PR must use its base SHA. - - name: Checkout pinned bootstrap reviewed npm audit - if: ${{ steps.trusted-reviewed-npm-audit.outputs.available != 'true' && github.event.pull_request.number == 6830 && github.event.pull_request.head.repo.full_name == 'HOYALIM/NemoClaw' }} - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - repository: HOYALIM/NemoClaw - ref: 0c7dd29394d2c4db660c4d09f3654c0789e200d0 - path: .trusted-reviewed-npm-audit-bootstrap - persist-credentials: false - sparse-checkout: | - .github/actions/ci-reviewed-npm-audit - ci/npm-audit-exceptions.json - ci/reviewed-npm-audit.json - scripts/audit-reviewed-npm-graph.mts - scripts/lib/openclaw-npm-remediation.mts - scripts/lib/reviewed-npm-archive.mts - scripts/lib/reviewed-npm-audit.mts - sparse-checkout-cone-mode: false - - - name: Reject unavailable trusted reviewed npm audit - if: ${{ steps.trusted-reviewed-npm-audit.outputs.available != 'true' && (github.event.pull_request.number != 6830 || github.event.pull_request.head.repo.full_name != 'HOYALIM/NemoClaw') }} - shell: bash - run: | - echo "::error::Trusted base SHA does not contain the schema-2 reviewed npm audit" - exit 1 - - name: Audit reviewed production npm graphs - if: ${{ steps.trusted-reviewed-npm-audit.outputs.available == 'true' }} uses: ./.trusted-reviewed-npm-audit/.github/actions/ci-reviewed-npm-audit with: target-root: ${{ github.workspace }} report-dir: artifacts/reviewed-npm-audit - - name: Audit reviewed production npm graphs (pinned bootstrap) - if: ${{ steps.trusted-reviewed-npm-audit.outputs.available != 'true' && github.event.pull_request.number == 6830 && github.event.pull_request.head.repo.full_name == 'HOYALIM/NemoClaw' }} - uses: ./.trusted-reviewed-npm-audit-bootstrap/.github/actions/ci-reviewed-npm-audit - with: - target-root: ${{ github.workspace }} - report-dir: artifacts/reviewed-npm-audit - cli-test-shards: needs: changes if: needs.changes.outputs.code == 'true' @@ -409,87 +278,12 @@ jobs: .github/actions/ci-install-dependencies.sh sparse-checkout-cone-mode: false - - name: Detect trusted E2E support sharding - id: trusted-shard-capabilities - shell: bash - run: | - if grep -Fq -- '--project e2e-support' \ - .trusted-ci-actions/.github/actions/ci-cli-coverage-shard/action.yaml; then - echo "e2e-support=true" >> "$GITHUB_OUTPUT" - else - echo "e2e-support=false" >> "$GITHUB_OUTPUT" - fi - if grep -Fq -- 'name: Install pinned Pi search tools' \ - .trusted-ci-actions/.github/actions/ci-cli-coverage-shard/action.yaml; then - echo "pi-search-tools=true" >> "$GITHUB_OUTPUT" - else - echo "pi-search-tools=false" >> "$GITHUB_OUTPUT" - fi - - # The PR action comes from the base commit. Provision Pi's search tools here until - # the trusted action contains the same pinned, version-verified contract. - - name: Install pinned Pi search tools (bootstrap) - if: ${{ steps.trusted-shard-capabilities.outputs.pi-search-tools != 'true' }} - shell: bash - env: - FD_FIND_VERSION: "9.0.0-1" - RIPGREP_VERSION: "14.1.0-1" - run: | - set -euo pipefail - sudo apt-get update -qq - sudo apt-get install -y --no-install-recommends \ - "fd-find=${FD_FIND_VERSION}" \ - "ripgrep=${RIPGREP_VERSION}" - - INSTALLED_FD_FIND_VERSION="$(dpkg-query -W -f='${Version}' fd-find)" - INSTALLED_RIPGREP_VERSION="$(dpkg-query -W -f='${Version}' ripgrep)" - if [ "$INSTALLED_FD_FIND_VERSION" != "$FD_FIND_VERSION" ]; then - echo "::error::fd-find package version $INSTALLED_FD_FIND_VERSION does not match $FD_FIND_VERSION" - exit 1 - fi - if [ "$INSTALLED_RIPGREP_VERSION" != "$RIPGREP_VERSION" ]; then - echo "::error::ripgrep package version $INSTALLED_RIPGREP_VERSION does not match $RIPGREP_VERSION" - exit 1 - fi - - command -v fdfind >/dev/null - command -v rg >/dev/null - EXPECTED_FD_BINARY_VERSION="${FD_FIND_VERSION%%-*}" - EXPECTED_RG_BINARY_VERSION="${RIPGREP_VERSION%%-*}" - FD_BINARY_VERSION="$(fdfind --version)" - RG_BINARY_VERSION="$(rg --version)" - RG_BINARY_VERSION="${RG_BINARY_VERSION%%$'\n'*}" - if [ "$FD_BINARY_VERSION" != "fdfind $EXPECTED_FD_BINARY_VERSION" ]; then - echo "::error::fdfind binary version $FD_BINARY_VERSION does not match fdfind $EXPECTED_FD_BINARY_VERSION" - exit 1 - fi - if [ "$RG_BINARY_VERSION" != "ripgrep $EXPECTED_RG_BINARY_VERSION" ]; then - echo "::error::rg binary version $RG_BINARY_VERSION does not match ripgrep $EXPECTED_RG_BINARY_VERSION" - exit 1 - fi - - name: Run CLI coverage shard uses: ./.trusted-ci-actions/.github/actions/ci-cli-coverage-shard with: shard: ${{ matrix.shard }} shard-count: "8" - # The PR action comes from the base commit. Until that trusted action - # contains the E2E-support project, keep this PR and older-base PRs - # covered without executing a mutable replacement action. - - name: Validate changed live E2E mock parity (bootstrap) - if: ${{ steps.trusted-shard-capabilities.outputs.e2e-support != 'true' && matrix.shard == 1 }} - run: npx tsx scripts/checks/e2e-mock-parity.mts --base HEAD^1 --head HEAD^2 - - - name: Run E2E support shard (bootstrap) - if: ${{ steps.trusted-shard-capabilities.outputs.e2e-support != 'true' }} - env: - E2E_SUPPORT_SHARD: ${{ matrix.shard }} - E2E_SUPPORT_SHARD_COUNT: "8" - run: >- - npx vitest run --project e2e-support - --shard="${E2E_SUPPORT_SHARD}/${E2E_SUPPORT_SHARD_COUNT}" - cli-tests: needs: - changes diff --git a/agents/openclaw/dependency-review.md b/agents/openclaw/dependency-review.md index 1b73ddd4021..392b8d0b6db 100644 --- a/agents/openclaw/dependency-review.md +++ b/agents/openclaw/dependency-review.md @@ -33,8 +33,7 @@ The reviewed audit wrapper reports lower-severity production findings and blocks - Installation boundary: the image materializes the reviewed lock into a root-owned dedicated npm cache and adds the exact package metadata needed by npm's offline resolver. Before that cache becomes immutable, the shared `scripts/lib/reviewed-npm-archive.mts` implementation re-packs every locked archive offline from the final cache and rejects registry-origin drift, metadata or packed-byte SRI drift, unsafe filenames, missing archives, and symlinks. The sandbox user copies that verified immutable source into a writable cache used for registry metadata lookup, archive packing, and the OpenClaw plugin install; no retrieval step falls back to `HOME/.npm`. The copy is deleted in the same image layer, and the trusted cache is never writable. The installer runs in offline, legacy-peer mode, then `verify-wechat-runtime-lock.mts` rejects integrity, version, dependency-set, or peer-range drift and refuses an image OpenClaw version below the plugin's locked peer minimum. - Default CI gate: `wechat-runtime-audit` in `.github/workflows/pr.yaml` and `.github/workflows/main.yaml` invokes the reviewed `.github/actions/ci-wechat-runtime-audit` implementation. The pull request workflow resolves the action from the PR base SHA. - Because PR #6739's base SHA predates the action, only that PR can use the pinned bootstrap action from signed immutable commit `HOYALIM/NemoClaw@0d2256d71d5bbba3bcaaaa4d01714fa56f22d1e2`. - Other PRs fail closed if their base lacks the action. + If the PR base SHA does not contain the action, the pull request workflow fails. The `main.yaml` workflow uses the merged action. The action uses Node.js `22.19.0`. It downloads `npm@10.9.4` and verifies the archive against the committed Subresource Integrity (SRI) value. @@ -49,7 +48,6 @@ The reviewed audit wrapper reports lower-severity production findings and blocks Any final nonzero status fails the action. The report directory stores each attempt in `npm-audit-signatures-attempt-.txt`. After a failed attempt, the action copies available npm debug logs to `npm-audit-signature-debug/`. - Removal condition: delete the PR #6739 bootstrap checkout, its paired conditional audit step, and the bootstrap-specific test assertions in the first follow-up after this PR merges, before the next release tag; all later PRs must use the normal base-SHA action path. - Advisory command: `npm ci --ignore-scripts --omit=dev --legacy-peer-deps --prefix agents/openclaw/wechat-runtime && npm audit --omit=dev --audit-level=low --json --prefix agents/openclaw/wechat-runtime && npm audit signatures --prefix agents/openclaw/wechat-runtime`. - Advisory review: `2026-07-12`; result: `0` known vulnerabilities across the resolved production graph. - Regression tests: `test/wechat-locked-install.test.ts` keeps the manifest runtime-lock paths and installer verification dispatch synchronized; `test/verify-wechat-runtime-lock.test.ts` proves the installed graph and OpenClaw peer-range compatibility fail closed; `test/wechat-runtime-audit-workflow.test.ts` keeps the Docker cache lifecycle, audit threshold, bounded download-only signature retry, invalid-signature denial, and real npm-pack boundary synchronized. diff --git a/ci/source-shape-test-budget.json b/ci/source-shape-test-budget.json index 29b62dceb32..c64ef36d64a 100644 --- a/ci/source-shape-test-budget.json +++ b/ci/source-shape-test-budget.json @@ -446,6 +446,12 @@ "test": "requires one advisor lane to publish the PR comment", "category": "security" }, + { + "file": "test/pr-workflow-contract.test.ts", + "test": "executes pull request installer hash checks only from the PR base SHA", + "category": "security" + }, + { "file": "test/runtime-provider-source-shape.test.ts", "test": "keeps migrated provider identities and implementations behind the one bundle composition", diff --git a/docs/security/openclaw-2026.6.10-dependency-review.md b/docs/security/openclaw-2026.6.10-dependency-review.md index b5df1686569..b16931e09fc 100644 --- a/docs/security/openclaw-2026.6.10-dependency-review.md +++ b/docs/security/openclaw-2026.6.10-dependency-review.md @@ -395,7 +395,12 @@ No real Microsoft Teams tenant proof is included in this PR. The work remains tr The low `body-parser` and moderate Hono and `protobufjs` findings remain documented at the configured `high` threshold. Current NemoClaw closes the WeChat residual with `agents/openclaw/wechat-runtime/package-lock.json` and post-install graph verification. - `src/lib/messaging/channels/manifests.test.ts` remains below the shared `test-size:check` threshold and does not need extraction in this dependency bump. -- The npm audit result in this note remains a point-in-time snapshot. Default PR and main CI rematerialize the production-compatible graph from the reviewed local archives, audit it and the committed mcporter lock with `npm audit --omit=dev --json` through the reviewed evaluator, upload the raw reports and normalized policy results, and fail on unaccepted findings at the configured `high` threshold. The separate `wechat-runtime-audit` gate uses Node `22.19.0` and npm `10.9.4`, installs the committed WeChat production lock with scripts disabled, fails on any low-or-higher production advisory, verifies registry signatures, exercises the reviewed archive through a copied writable cache, and uploads its evidence. Pull requests execute that WeChat audit action from the base SHA; because PR #6739's base predates the action, that PR alone may bootstrap it from signed immutable commit `HOYALIM/NemoClaw@0d2256d71d5bbba3bcaaaa4d01714fa56f22d1e2`, while every other PR fails closed if its base lacks the action. The production installer routes registry metadata lookup, archive packing, and installation through the disposable writable-cache boundary so retrieval cannot fall back to `HOME/.npm`; the trusted source cache remains read-only and the disposable copy is removed in the same image layer. +- The npm audit result in this note remains a point-in-time snapshot. + Default PR and main CI rematerialize the production-compatible graph from the reviewed local archives, audit it and the committed mcporter lock with `npm audit --omit=dev --json` through the reviewed evaluator, upload the raw reports and normalized policy results, and fail on unaccepted findings at the configured `high` threshold. + The separate `wechat-runtime-audit` gate uses Node `22.19.0` and npm `10.9.4`, installs the committed WeChat production lock with scripts disabled, fails on any low-or-higher production advisory, verifies registry signatures, exercises the reviewed archive through a copied writable cache, and uploads its evidence. + Pull requests execute that WeChat audit action from the PR base SHA. + If the PR base SHA does not contain the action, the pull request workflow fails. + The production installer routes registry metadata lookup, archive packing, and installation through the disposable writable-cache boundary so retrieval cannot fall back to `HOME/.npm`; the trusted source cache remains read-only and the disposable copy is removed in the same image layer. - The stale nonterminal rebuild-resume repair in `src/lib/actions/sandbox/rebuild-resume-session.ts` remains a migration compatibility shim tracked against #4533's onboard FSM/resume compatibility boundary. Its removal condition is to delete it after a session-version migration proves recreate sessions are always persisted at a resumable pre-sandbox boundary; `src/lib/actions/sandbox/rebuild-resume-session.test.ts` covers the helper directly, `test/onboard-resume-provider-recovery.test.ts` carries the onboard-suite producer-level regression for `machine.state='openclaw'`, and `src/lib/actions/sandbox/rebuild-resume-snapshot.test.ts` owns the rebuild handoff regression. - Production OpenClaw image build paths call `scripts/check-production-build-args.sh` before production `docker build` or `docker/build-push-action` use. `test/openclaw-dependency-review.test.ts` keeps that workflow contract documented. - The rebuild-reasoning cases added by this PR live in the focused `rebuild-resume-reasoning.test.ts` file; the smaller route-provenance additions remain with their `rebuild-resume-config.ts` boundary tests. diff --git a/scripts/check-installer-hash.sh b/scripts/check-installer-hash.sh index 75c58426e3e..90d94f869fe 100755 --- a/scripts/check-installer-hash.sh +++ b/scripts/check-installer-hash.sh @@ -80,8 +80,8 @@ sha256_file() { # sourceBoundary: NVIDIA/OpenShell owns the release assets and their published # digests; NemoClaw owns this independent verification of its local pin table. # In pull-request CI, this checker and its pin parser execute only from the -# base-trusted checkout or the immutable bootstrap checkout, never from the PR -# head; installer files from the PR head are treated strictly as input data. +# base-trusted checkout. Installer files from the latest PR commit are input data +# and are never sourced or executed. # whyNotSourceFix: an upstream release cannot validate which artifacts a # downstream installer consumes, so this comparison must remain in NemoClaw. # regressionTest: test/installer-hash-check.test.ts proves download failures and @@ -105,10 +105,10 @@ check_openshell_release_assets() { # invalidState: target-controlled shell formatting hides, duplicates, or # mixes a release version while the trusted release-asset check still reports # success. - # sourceBoundary: this parser executes beside the checker only from the - # base-trusted checkout or immutable bootstrap, never from the PR head. It - # defines the accepted static shell subset; PR-head installers are input data - # only and are never sourced or executed. + # sourceBoundary: In pull-request CI, this parser and checker execute only + # from the base-trusted checkout. The parser defines the accepted static shell + # subset. Installer files from the latest PR commit are input data and are + # never sourced or executed. # whyNotSourceFix: installers need shell-native lookup before dependencies are # available, and sourcing target-controlled shell here would execute PR code. # regressionTest: test/installer-hash-check.test.ts covers resilient formatting diff --git a/test/growth-guardrails-workflow-boundary.test.ts b/test/growth-guardrails-workflow-boundary.test.ts index 8553e9248c0..72c9c6cf510 100644 --- a/test/growth-guardrails-workflow-boundary.test.ts +++ b/test/growth-guardrails-workflow-boundary.test.ts @@ -102,7 +102,7 @@ describe("growth-guardrails workflow trust boundary", () => { " - name: Check out the trusted base revision", " - name: Execute an untrusted action\n uses: attacker/payload@main\n\n - name: Check out the trusted base revision", ), - /must contain exactly 7 approved steps, not 8/, + /must contain exactly 6 approved steps, not 7/, ], [ "a non-approved shell field", diff --git a/test/pr-workflow-contract.test.ts b/test/pr-workflow-contract.test.ts index f9957c76638..759cc027c42 100644 --- a/test/pr-workflow-contract.test.ts +++ b/test/pr-workflow-contract.test.ts @@ -71,11 +71,6 @@ const trustedPrActionPaths = { const trustedCheckoutAction = "actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1"; const trustedSetupNodeAction = "actions/setup-node@820762786026740c76f36085b0efc47a31fe5020"; -const installerHashBootstrapCommit = "cb5e9aefab2b16fedc0995149fc3520da0d5e0c7"; -const installerHashBootstrapTree = "1fdf59efe40b78c407e222fd42043b23a61e199a"; -const installerHashBootstrapCreatedAt = "2026-07-02T19:35:41Z"; -const installerHashBootstrapExpiresAt = "2026-12-29T19:35:41Z"; - const trustedActionDirs = [ ".github/actions/ci-static-checks", ".github/actions/ci-build-typecheck", @@ -285,11 +280,52 @@ function codeFilterMatchesChangedPaths(workflow: CiWorkflow, paths: string[]): b }); } +function installerHashTrustViolations(workflow: CiWorkflow): string[] { + const steps = workflow.jobs["check-hash"]?.steps ?? []; + const baseCheckout = steps.find( + (step) => step.name === "Checkout base-trusted installer hash action", + ); + const prCheck = steps.find( + (step) => step.name === "Verify pull request installer hashes from base-trusted code", + ); + const allowedExecutors = new Set([ + "./.trusted-installer-hash/.github/actions/ci-installer-hash-check", + "./.github/actions/ci-installer-hash-check", + ]); + + return [ + ...(baseCheckout ? [] : ["missing base-trusted installer hash checkout"]), + ...(baseCheckout?.uses === trustedCheckoutAction + ? [] + : ["base-trusted installer hash checkout must use the pinned checkout action"]), + ...(baseCheckout?.with?.ref === "${{ github.event.pull_request.base.sha }}" + ? [] + : ["base-trusted installer hash checkout must use the PR base SHA"]), + ...(baseCheckout?.with?.path === ".trusted-installer-hash" + ? [] + : ["base-trusted installer hash checkout must use the trusted action path"]), + ...(prCheck?.if === "github.event_name == 'pull_request'" && + prCheck.uses === "./.trusted-installer-hash/.github/actions/ci-installer-hash-check" + ? [] + : ["pull request installer hashes must use only the base-trusted action"]), + ...steps.flatMap((step) => [ + ...(step.uses === "./.github/actions/ci-installer-hash-check" && + step.if !== "github.event_name != 'pull_request'" + ? ["installer hash action from the latest PR commit must not execute for pull requests"] + : []), + ...(step.uses?.includes("ci-installer-hash-check") && !allowedExecutors.has(step.uses) + ? [`unapproved installer hash executor: ${step.uses}`] + : []), + ]), + ]; +} + describe("pull request and main workflow contracts", () => { const prWorkflow = readYaml(".github/workflows/pr.yaml"); const mainWorkflow = readYaml(".github/workflows/main.yaml"); const dcoWorkflow = readYaml(".github/workflows/dco-check.yaml"); const installerHashWorkflow = readYaml(".github/workflows/installer-hash-check.yaml"); + const installerHashAction = readYaml( ".github/actions/ci-installer-hash-check/action.yaml", ); @@ -313,84 +349,59 @@ describe("pull request and main workflow contracts", () => { ), }; - it("fails closed when the immutable installer hash bootstrap expiry is mutated", () => { - const expiryStep = requiredWorkflowStep( - installerHashWorkflow.jobs["check-hash"], - "Enforce immutable installer hash bootstrap expiry", + // source-shape-contract: security -- PR base SHA action execution prevents pull-request code from authorizing installer hashes + it("executes pull request installer hash checks only from the PR base SHA", () => { + expect(installerHashTrustViolations(installerHashWorkflow)).toEqual([]); + + const headCheckout = structuredClone(installerHashWorkflow); + requiredWorkflowStep( + headCheckout.jobs["check-hash"], + "Checkout base-trusted installer hash action", + ).with = { + ref: "${{ github.event.pull_request.head.sha }}", + path: ".trusted-installer-hash", + }; + + const missingBaseCheckout = structuredClone(installerHashWorkflow); + missingBaseCheckout.jobs["check-hash"].steps = missingBaseCheckout.jobs[ + "check-hash" + ].steps?.filter((step) => step.name !== "Checkout base-trusted installer hash action"); + + const mutableExecutor = structuredClone(installerHashWorkflow); + requiredWorkflowStep( + mutableExecutor.jobs["check-hash"], + "Verify pull request installer hashes from base-trusted code", + ).uses = "./.github/actions/ci-installer-hash-check"; + + const bootstrapExecutor = structuredClone(installerHashWorkflow); + bootstrapExecutor.jobs["check-hash"].steps?.push({ + name: "Run installer hash bootstrap", + uses: "./.bootstrap-installer-hash/.github/actions/ci-installer-hash-check", + }); + + const prOnlyLocalExecutor = structuredClone(installerHashWorkflow); + prOnlyLocalExecutor.jobs["check-hash"].steps?.push({ + name: "Run local installer hash action for pull requests", + if: "github.event_name == 'pull_request'", + uses: "./.github/actions/ci-installer-hash-check", + }); + + expect(installerHashTrustViolations(headCheckout)).toContain( + "base-trusted installer hash checkout must use the PR base SHA", ); - const expired = runWorkflowShellStep( - { - ...expiryStep, - run: expiryStep.run?.replace(installerHashBootstrapExpiresAt, "2000-12-27T23:26:13Z"), - }, - {}, + expect(installerHashTrustViolations(missingBaseCheckout)).toContain( + "missing base-trusted installer hash checkout", ); - const malformedExpiry = runWorkflowShellStep( - { - ...expiryStep, - run: expiryStep.run?.replace(installerHashBootstrapExpiresAt, "not-a-canonical-utc-date"), - }, - {}, + expect(installerHashTrustViolations(mutableExecutor)).toContain( + "pull request installer hashes must use only the base-trusted action", ); - const mutableRef = runWorkflowShellStep( - { - ...expiryStep, - run: expiryStep.run?.replace(installerHashBootstrapCommit, "main"), - }, - {}, + expect(installerHashTrustViolations(bootstrapExecutor)).toContain( + "unapproved installer hash executor: ./.bootstrap-installer-hash/.github/actions/ci-installer-hash-check", ); - const valid = runWorkflowShellStep(expiryStep, {}); - - expect(valid.status).toBe(0); - expect(valid.stdout).toContain("remains valid"); - expect(expired.status).not.toBe(0); - expect(expired.stderr).toContain("expired at 2000-12-27T23:26:13Z"); - expect(expired.stderr).toContain("Remove the bootstrap fallback"); - expect(malformedExpiry.status).not.toBe(0); - expect(malformedExpiry.stderr).toContain("expiry configuration is invalid"); - expect(mutableRef.status).not.toBe(0); - expect(mutableRef.stderr).toContain("refusing the fallback"); - }); - it("fails closed when the immutable installer hash bootstrap tree differs", () => { - const treeStep = requiredWorkflowStep( - installerHashWorkflow.jobs["check-hash"], - "Verify immutable installer hash bootstrap tree", - ); - const fakeBin = mkdtempSync(join(tmpdir(), "nemoclaw-bootstrap-git-")); - const fakeGit = join(fakeBin, "git"); - writeFileSync( - fakeGit, - [ - "#!/bin/sh", - 'case "$*" in', - ' *"HEAD^{tree}"*) printf \'%s\\n\' "${FAKE_TREE}" ;;', - ` *) printf '%s\\n' ${installerHashBootstrapCommit} ;;`, - "esac", - ].join("\n"), - { mode: 0o755 }, + expect(installerHashTrustViolations(prOnlyLocalExecutor)).toContain( + "installer hash action from the latest PR commit must not execute for pull requests", ); - - try { - const env = { - GITHUB_WORKSPACE: tmpdir(), - PATH: `${fakeBin}:${process.env.PATH ?? ""}`, - }; - const valid = runWorkflowShellStep(treeStep, { - ...env, - FAKE_TREE: installerHashBootstrapTree, - }); - const mismatch = runWorkflowShellStep(treeStep, { - ...env, - FAKE_TREE: "0000000000000000000000000000000000000000", - }); - - expect(valid.status).toBe(0); - expect(mismatch.status).not.toBe(0); - expect(mismatch.stderr).toContain("does not match the reviewed tree"); - } finally { - rmSync(fakeBin, { recursive: true, force: true }); - } }); it("validates CLI shard inputs before using them in shell commands", () => { diff --git a/test/reviewed-npm-audit-workflow.test.ts b/test/reviewed-npm-audit-workflow.test.ts index b2352442b1a..8ed51c8fbd2 100644 --- a/test/reviewed-npm-audit-workflow.test.ts +++ b/test/reviewed-npm-audit-workflow.test.ts @@ -41,13 +41,6 @@ type Workflow = { }; const REPO_ROOT = path.join(import.meta.dirname, ".."); -const BOOTSTRAP_SHA = "0c7dd29394d2c4db660c4d09f3654c0789e200d0"; -// Removal condition: delete the PR-6830 fork bootstrap after this PR merges and -// the base branch contains the schema-v2 reviewed npm audit action. -const BOOTSTRAP_IF = - "${{ steps.trusted-reviewed-npm-audit.outputs.available != 'true' && github.event.pull_request.number == 6830 && github.event.pull_request.head.repo.full_name == 'HOYALIM/NemoClaw' }}"; -const REJECT_UNAVAILABLE_IF = - "${{ steps.trusted-reviewed-npm-audit.outputs.available != 'true' && (github.event.pull_request.number != 6830 || github.event.pull_request.head.repo.full_name != 'HOYALIM/NemoClaw') }}"; const DOMEXCEPTION_INTEGRITY = "sha512-tlc/FcYIv5i8RYsl2iDil4A0gOihaas1R5jPcIC4Zw3GhjKsVilw90aHcVlhZPTBLGBzd379S+VcnsDjd9ChiA=="; diff --git a/tools/growth-guardrails/workflow-boundary.mts b/tools/growth-guardrails/workflow-boundary.mts index fbd82888e9f..588b0242b17 100644 --- a/tools/growth-guardrails/workflow-boundary.mts +++ b/tools/growth-guardrails/workflow-boundary.mts @@ -53,21 +53,17 @@ const APPROVED_STEP_SHAPES = [ name: "Check out the trusted base revision", sha256: "4ec2659f39a08af0cb6abf60ab63a6732ab2375f538fc5b80601d566fdbcbc5a", }, - { - name: "Detect guardrail tools on the base revision", - sha256: "f10b24b97320e991cc060be9f5e98bda2705abd8d0814da123cfd5ca8672a442", - }, { name: "Install trusted dependencies", - sha256: "bf5757db70862f1e068748855d97ab5ae6a4a43ebd7ed812baa9f45269bdf6c3", + sha256: "2388fa3f5694ed0db8df96bf5f2bbcc3f377d5fe66e433b32d0ef71e88d0f362", }, { name: "Require changed test files to stay within size budget", - sha256: "eec9020e81cf9d972592cc3129f6656ec0d46618623fb368674e8e823d8c1457", + sha256: "5d631c12f457e947bd7bd83447ab2a294cbe4d8430dbed4e92d09d7f7594dd3b", }, { name: "Require changed test files not to add if statements", - sha256: "9aa363162eb2dc6740d9a27c52e6171b2a5587017eb039773a0f4ee0b32f8cde", + sha256: "991a7036d27aeb45fde2f3178936fba6ac87b90200b79c933015f9d63525d3cf", }, ] as const;