diff --git a/.github/actions/restore-e2e-cli-artifact/action.yaml b/.github/actions/restore-e2e-cli-artifact/action.yaml index b1991707517..868bb0d20df 100644 --- a/.github/actions/restore-e2e-cli-artifact/action.yaml +++ b/.github/actions/restore-e2e-cli-artifact/action.yaml @@ -146,7 +146,7 @@ runs: { echo "::error::exact-commit CLI artifact payload digest mismatch"; exit 1; } while IFS= read -r member; do case "$member" in - dist | dist/* | nemoclaw | nemoclaw/dist | nemoclaw/dist/*) ;; + dist | dist/* | nemoclaw/dist/shared | nemoclaw/dist/shared/*) ;; *) echo "::error::CLI artifact contains an unsafe member: $member"; exit 1 ;; esac case "/$member/" in @@ -158,15 +158,25 @@ runs: { echo "::error::CLI artifact contains a link or special file"; exit 1; } [[ ! -e "$GITHUB_WORKSPACE/dist" && ! -L "$GITHUB_WORKSPACE/dist" ]] || { echo "::error::consumer unexpectedly built dist before artifact restore"; exit 1; } + [[ -d "$GITHUB_WORKSPACE/nemoclaw" && ! -L "$GITHUB_WORKSPACE/nemoclaw" ]] || + { echo "::error::consumer nemoclaw directory must be a non-symlink directory"; exit 1; } + [[ ! -e "$GITHUB_WORKSPACE/nemoclaw/dist" && ! -L "$GITHUB_WORKSPACE/nemoclaw/dist" ]] || { echo "::error::consumer unexpectedly built nemoclaw/dist before artifact restore"; exit 1; } restore_dir="$(mktemp -d "${RUNNER_TEMP}/nemoclaw-cli-restore.XXXXXX")" trap 'rm -rf -- "$restore_dir"' EXIT tar --no-same-owner --no-same-permissions -xf "$payload" -C "$restore_dir" - test -s "$restore_dir/dist/nemoclaw.js" || - { echo "::error::restored CLI artifact is missing dist/nemoclaw.js"; exit 1; } - test -s "$restore_dir/nemoclaw/dist/shared/sandbox-name.cjs" || - { echo "::error::restored CLI artifact is missing nemoclaw/dist/shared/sandbox-name.cjs"; exit 1; } + cli_entrypoint="$restore_dir/dist/nemoclaw.js" + [[ -f "$cli_entrypoint" && ! -L "$cli_entrypoint" && -s "$cli_entrypoint" ]] || + { echo "::error::restored CLI artifact entry point is missing or is not a nonempty regular file"; exit 1; } + for boundary in \ + openshell-policy-boundary.cjs \ + sandbox-name.cjs \ + snapshot-sanitizer-boundary.cjs; do + boundary_path="$restore_dir/nemoclaw/dist/shared/$boundary" + [[ -f "$boundary_path" && ! -L "$boundary_path" && -s "$boundary_path" ]] || + { echo "::error::restored CLI artifact shared module is missing or is not a nonempty regular file: $boundary"; exit 1; } + done jq -e --arg candidateSha "$CANDIDATE_SHA" ' type == "object" and (keys | sort) == ["nemoclawVersion", "sourceRevision"] and diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index eefbde241bb..c107939f8ae 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -392,12 +392,19 @@ jobs: { echo "::error::checked-out commit does not match the artifact candidate SHA"; exit 1; } [[ "$RUN_ATTEMPT" =~ ^[1-9][0-9]*$ && "$RUN_ID" =~ ^[1-9][0-9]*$ ]] || { echo "::error::workflow run identity is invalid"; exit 1; } - test -s dist/nemoclaw.js || - { echo "::error::candidate CLI build did not produce dist/nemoclaw.js"; exit 1; } - test -s dist/build-identity.json || - { echo "::error::candidate CLI build did not produce dist/build-identity.json"; exit 1; } - test -s nemoclaw/dist/shared/sandbox-name.cjs || - { echo "::error::candidate CLI build did not produce nemoclaw/dist/shared/sandbox-name.cjs"; exit 1; } + for required_file in dist/nemoclaw.js dist/build-identity.json; do + [[ -f "$required_file" && ! -L "$required_file" && -s "$required_file" ]] || + { echo "::error::candidate CLI build output is missing or is not a nonempty regular file: $required_file"; exit 1; } + done + for boundary in \ + openshell-policy-boundary.cjs \ + sandbox-name.cjs \ + snapshot-sanitizer-boundary.cjs; do + boundary_path="nemoclaw/dist/shared/$boundary" + [[ -f "$boundary_path" && ! -L "$boundary_path" && -s "$boundary_path" ]] || + { echo "::error::candidate CLI build shared module is missing or is not a nonempty regular file: $boundary"; exit 1; } + done + jq -e --arg candidateSha "$CANDIDATE_SHA" ' type == "object" and (keys | sort) == ["nemoclawVersion", "sourceRevision"] and @@ -418,7 +425,7 @@ jobs: --numeric-owner \ -cf "$payload" \ dist \ - nemoclaw/dist + nemoclaw/dist/shared payload_sha256="$(sha256sum "$payload" | awk '{print $1}')" source_tree="$(git rev-parse 'HEAD^{tree}')" lockfile_sha256="$(sha256sum package-lock.json | awk '{print $1}')" @@ -623,7 +630,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -801,7 +808,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -964,7 +971,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -1015,7 +1022,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -1289,7 +1296,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -1421,7 +1428,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -1543,7 +1550,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -1655,7 +1662,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -1738,7 +1745,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -1783,7 +1790,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -1862,7 +1869,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -1920,7 +1927,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell CLI @@ -2879,7 +2886,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Initialize runner comparison telemetry @@ -2953,7 +2960,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Install OpenShell CLI @@ -3040,7 +3047,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} - name: Initialize runner comparison telemetry @@ -3104,7 +3111,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -3211,7 +3218,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -3288,7 +3295,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -3354,7 +3361,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -3403,7 +3410,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -3468,7 +3475,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -3550,7 +3557,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -3695,7 +3702,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -3903,7 +3910,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -3993,7 +4000,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -4092,7 +4099,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -4256,7 +4263,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -4315,7 +4322,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -4393,7 +4400,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -4515,7 +4522,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -4631,7 +4638,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -4682,7 +4689,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -4749,7 +4756,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -4834,7 +4841,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -4896,7 +4903,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -4959,7 +4966,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -5021,7 +5028,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -5215,7 +5222,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -5313,7 +5320,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -5374,7 +5381,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -5436,7 +5443,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -5496,7 +5503,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -5555,7 +5562,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -5671,7 +5678,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -5729,7 +5736,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -5841,7 +5848,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -5944,7 +5951,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -6010,7 +6017,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -6082,7 +6089,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -6157,7 +6164,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -6236,7 +6243,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -6305,7 +6312,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -6401,7 +6408,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -6483,7 +6490,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -6538,7 +6545,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -6601,7 +6608,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -6678,7 +6685,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -6778,7 +6785,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -6871,7 +6878,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -6943,7 +6950,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -7008,7 +7015,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} @@ -7080,7 +7087,7 @@ jobs: build-cli: "false" - name: Restore exact-commit CLI artifact - uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d + uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c with: provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }} diff --git a/test/e2e/README.md b/test/e2e/README.md index 5c18324d8ab..f853180d69b 100644 --- a/test/e2e/README.md +++ b/test/e2e/README.md @@ -26,7 +26,8 @@ before those targets run; local runners must provide it themselves. ### Candidate CLI Artifact The candidate CLI comes from the source commit that an E2E run tests. -The `generate-matrix` job builds it once and publishes `dist/` as a content-addressed artifact. +The `generate-matrix` job builds it once. +The job publishes root `dist/` and `nemoclaw/dist/shared/` in one content-addressed artifact. The workflow has 62 artifact-using job definitions. Each selected job execution restores the artifact instead of running `npm run build:cli`. Each selected job still runs the pinned preparation action to install Node.js and project dependencies. @@ -58,17 +59,19 @@ Before download, the action rejects extra or missing provenance fields. It also compares the candidate checkout, repository, workflow SHA, run ID, and attempt with the provenance object. The action downloads the artifact by immutable ID and sets digest mismatch handling to `error`. -Before the action restores `dist/` into the workspace, it verifies these conditions: +Before the action restores root `dist/` and `nemoclaw/dist/shared/` into the workspace, it verifies these conditions: - The upload digest is present and well formed. - The candidate SHA matches the expected commit. - The manifest matches the source, workflow run, toolchain contract, and payload. -- The archive contains no path traversal, links, special files, or files outside `dist/`. -- The candidate checkout has no preexisting `dist/` path. +- The archive contains no path traversal, links, special files, or files outside root `dist/` and `nemoclaw/dist/shared/`. +- Neither root `dist/` nor `nemoclaw/dist/` already exists, including as a dangling symbolic link. +- The candidate checkout's `nemoclaw/` path is a directory and is not a symbolic link. +- The CLI entry point and required shared modules are nonempty regular files. - The staged `dist/build-identity.json` names the candidate commit SHA. -If a pre-restore check fails, the action stops before it moves `dist/` into the workspace. -After the checks pass, the action moves `dist/` and runs `bin/nemoclaw.js --version`. +If a pre-restore check fails, the action stops before it adds either directory to the workspace. +After the checks pass, the action restores root `dist/` and `nemoclaw/dist/shared/`, then runs `bin/nemoclaw.js --version`. If the version command fails, the action stops before the live test runs. This boundary keeps candidate source separate from the trusted workflow implementation. diff --git a/test/e2e/support/cli-artifact-workflow-boundary.test.ts b/test/e2e/support/cli-artifact-workflow-boundary.test.ts index a03c93ae770..792abccdd47 100644 --- a/test/e2e/support/cli-artifact-workflow-boundary.test.ts +++ b/test/e2e/support/cli-artifact-workflow-boundary.test.ts @@ -67,11 +67,22 @@ function workflowFixture(): Workflow { } type RestoreFixtureOptions = { - archive?: "valid" | "missing-shared" | "non-dist" | "link" | "traversal"; + archive?: + | "valid" + | "cli-directory" + | "missing-shared" + | "non-dist" + | "link" + | "shared-module-directory" + | "traversal"; buildIdentitySha?: string; expectedPayloadSha256?: string; manifestCandidateSha?: string; - preexistingDist?: "dangling-symlink" | "directory" | "nested-directory"; + preexistingDist?: + | "dangling-symlink" + | "directory" + | "plugin-directory" + | "symlinked-plugin-parent"; }; type ArchiveFixtureContext = { @@ -84,13 +95,19 @@ function sha256File(file: string): string { return createHash("sha256").update(fs.readFileSync(file)).digest("hex"); } -function writeDistTree( +function writeCliArchive( context: ArchiveFixtureContext, customizeDist: (dist: string) => void, + customizeShared: (shared: string) => void = () => undefined, ): void { const dist = path.join(context.payloadRoot, "dist"); + const shared = path.join(context.payloadRoot, "nemoclaw", "dist", "shared"); fs.mkdirSync(dist); - fs.writeFileSync(path.join(dist, "nemoclaw.js"), 'console.log("nemoclaw v0.0.0");\n'); + fs.mkdirSync(shared, { recursive: true }); + fs.writeFileSync( + path.join(dist, "nemoclaw.js"), + 'require("../nemoclaw/dist/shared/sandbox-name.cjs");\nconsole.log("nemoclaw v0.0.0");\n', + ); fs.writeFileSync( path.join(dist, "build-identity.json"), `${JSON.stringify({ @@ -98,41 +115,64 @@ function writeDistTree( sourceRevision: context.buildIdentitySha, })}\n`, ); - customizeDist(dist); -} - -function writeSharedTree(context: ArchiveFixtureContext): void { - const shared = path.join(context.payloadRoot, "nemoclaw", "dist", "shared"); - fs.mkdirSync(shared, { recursive: true }); - fs.writeFileSync(path.join(shared, "sandbox-name.cjs"), "module.exports = {};\n"); -} - -function writeArchive(context: ArchiveFixtureContext, members: string[]): void { - execFileSync("tar", ["-cf", context.payload, "-C", context.payloadRoot, ...members]); -} + for (const boundary of [ + "openshell-policy-boundary.cjs", + "sandbox-name.cjs", + "snapshot-sanitizer-boundary.cjs", + ]) { + fs.writeFileSync(path.join(shared, boundary), "module.exports = {};\n"); + } + customizeShared(shared); -function writeCompleteArchive( - context: ArchiveFixtureContext, - customizeDist: (dist: string) => void, -): void { - writeDistTree(context, customizeDist); - writeSharedTree(context); - writeArchive(context, ["dist", "nemoclaw/dist"]); + customizeDist(dist); + execFileSync("tar", [ + "-cf", + context.payload, + "-C", + context.payloadRoot, + "dist", + "nemoclaw/dist/shared", + ]); } function writeValidArchive(context: ArchiveFixtureContext): void { - writeCompleteArchive(context, () => undefined); + writeCliArchive(context, () => undefined); } function writeLinkArchive(context: ArchiveFixtureContext): void { - writeCompleteArchive(context, (dist) => { + writeCliArchive(context, (dist) => { fs.symlinkSync("nemoclaw.js", path.join(dist, "linked-cli.js")); }); } +function writeCliDirectoryArchive(context: ArchiveFixtureContext): void { + writeCliArchive(context, (dist) => { + const entrypoint = path.join(dist, "nemoclaw.js"); + fs.rmSync(entrypoint); + fs.mkdirSync(entrypoint); + fs.writeFileSync(path.join(entrypoint, "index.js"), 'console.log("nemoclaw v0.0.0");\n'); + }); +} + function writeMissingSharedArchive(context: ArchiveFixtureContext): void { - writeDistTree(context, () => undefined); - writeArchive(context, ["dist"]); + writeCliArchive( + context, + () => undefined, + (shared) => fs.rmSync(path.join(shared, "sandbox-name.cjs")), + ); +} + +function writeSharedModuleDirectoryArchive(context: ArchiveFixtureContext): void { + writeCliArchive( + context, + () => undefined, + (shared) => { + const modulePath = path.join(shared, "sandbox-name.cjs"); + fs.rmSync(modulePath); + fs.mkdirSync(modulePath); + fs.writeFileSync(path.join(modulePath, "index.js"), "module.exports = {};\n"); + }, + ); } function writeNonDistArchive(context: ArchiveFixtureContext): void { @@ -157,9 +197,12 @@ function writeTraversalArchive(context: ArchiveFixtureContext): void { } const ARCHIVE_FIXTURE_WRITERS = { + "cli-directory": writeCliDirectoryArchive, link: writeLinkArchive, "missing-shared": writeMissingSharedArchive, + "non-dist": writeNonDistArchive, + "shared-module-directory": writeSharedModuleDirectoryArchive, traversal: writeTraversalArchive, valid: writeValidArchive, } satisfies Record< @@ -178,16 +221,25 @@ function writePreexistingDistDirectory(workspace: string): void { fs.writeFileSync(path.join(workspace, "dist", "existing.txt"), "preserve\n"); } -function writePreexistingNestedDistDirectory(workspace: string): void { - const dist = path.join(workspace, "nemoclaw", "dist"); - fs.mkdirSync(dist, { recursive: true }); - fs.writeFileSync(path.join(dist, "existing.txt"), "preserve\n"); +function writePreexistingPluginDistDirectory(workspace: string): void { + const shared = path.join(workspace, "nemoclaw", "dist", "shared"); + fs.mkdirSync(shared, { recursive: true }); + fs.writeFileSync(path.join(shared, "existing.cjs"), "module.exports = {};\n"); +} + +function writeSymlinkedPluginParent(workspace: string): void { + const escaped = path.join(path.dirname(workspace), "escaped"); + fs.rmSync(path.join(workspace, "nemoclaw"), { force: true, recursive: true }); + fs.mkdirSync(escaped); + fs.symlinkSync(escaped, path.join(workspace, "nemoclaw"), "dir"); } const PREEXISTING_DIST_WRITERS = { "dangling-symlink": writeDanglingDistSymlink, directory: writePreexistingDistDirectory, - "nested-directory": writePreexistingNestedDistDirectory, + "plugin-directory": writePreexistingPluginDistDirectory, + "symlinked-plugin-parent": writeSymlinkedPluginParent, + none: () => undefined, } satisfies Record< NonNullable | "none", @@ -203,6 +255,7 @@ function runRestoreValidation(options: RestoreFixtureOptions = {}) { const toolDirectory = path.join(root, "tools"); fs.mkdirSync(path.join(workspace, "bin"), { recursive: true }); fs.mkdirSync(path.join(workspace, "nemoclaw"), { recursive: true }); + fs.mkdirSync(artifactDirectory, { recursive: true }); fs.mkdirSync(payloadRoot, { recursive: true }); fs.mkdirSync(toolDirectory, { recursive: true }); @@ -212,7 +265,6 @@ function runRestoreValidation(options: RestoreFixtureOptions = {}) { '#!/usr/bin/env node\nrequire("../dist/nemoclaw.js");\n', { mode: 0o755 }, ); - fs.writeFileSync(path.join(workspace, "nemoclaw", ".gitkeep"), ""); execFileSync("git", ["init", "--quiet"], { cwd: workspace }); execFileSync("git", ["add", "."], { cwd: workspace }); execFileSync( @@ -316,6 +368,7 @@ function expectRestoreFailure(options: RestoreFixtureOptions, message: string): expect(fixture.result.status, fixture.output).not.toBe(0); expect(fixture.output).toContain(message); expect(fs.existsSync(path.join(fixture.workspace, "dist"))).toBe(false); + expect(fs.existsSync(path.join(fixture.workspace, "nemoclaw", "dist"))).toBe(false); } finally { fixture.cleanup(); } @@ -426,6 +479,7 @@ describe("exact-commit CLI artifact workflow boundary", () => { path.join(fixture.workspace, "nemoclaw", "dist", "shared", "sandbox-name.cjs"), ), ).toBe(true); + expect( fs .readdirSync(fixture.runnerTemp) @@ -450,17 +504,31 @@ describe("exact-commit CLI artifact workflow boundary", () => { ); }); - it("rejects an archive member outside dist before artifact extraction (#7915)", () => { + it("rejects a payload missing a compiled shared module before activation (#7915)", () => { expectRestoreFailure( - { archive: "non-dist" }, - "CLI artifact contains an unsafe member: outside.txt", + { archive: "missing-shared" }, + "restored CLI artifact shared module is missing or is not a nonempty regular file: sandbox-name.cjs", ); }); - it("rejects a payload missing the compiled shared dependency before activation (#7915)", () => { + it("rejects a directory in place of the CLI entry point before activation (#7915)", () => { expectRestoreFailure( - { archive: "missing-shared" }, - "restored CLI artifact is missing nemoclaw/dist/shared/sandbox-name.cjs", + { archive: "cli-directory" }, + "restored CLI artifact entry point is missing or is not a nonempty regular file", + ); + }); + + it("rejects a directory in place of a shared module before activation (#7915)", () => { + expectRestoreFailure( + { archive: "shared-module-directory" }, + "restored CLI artifact shared module is missing or is not a nonempty regular file: sandbox-name.cjs", + ); + }); + + it("rejects an archive member outside dist before artifact extraction (#7915)", () => { + expectRestoreFailure( + { archive: "non-dist" }, + "CLI artifact contains an unsafe member: outside.txt", ); }); @@ -485,34 +553,54 @@ describe("exact-commit CLI artifact workflow boundary", () => { } }); - it("does not overwrite a dangling dist symlink (#7915)", () => { - const fixture = runRestoreValidation({ preexistingDist: "dangling-symlink" }); + it("does not overwrite a preexisting nemoclaw/dist directory (#7915)", () => { + const fixture = runRestoreValidation({ preexistingDist: "plugin-directory" }); try { expect(fixture.result.status, fixture.output).not.toBe(0); - expect(fixture.output).toContain("consumer unexpectedly built dist before artifact restore"); - expect(fs.lstatSync(path.join(fixture.workspace, "dist")).isSymbolicLink()).toBe(true); - expect(fs.readlinkSync(path.join(fixture.workspace, "dist"))).toBe("missing-dist"); + expect(fixture.output).toContain( + "consumer unexpectedly built nemoclaw/dist before artifact restore", + ); + expect( + fs.readFileSync( + path.join(fixture.workspace, "nemoclaw", "dist", "shared", "existing.cjs"), + "utf8", + ), + ).toBe("module.exports = {};\n"); + expect(fs.existsSync(path.join(fixture.workspace, "dist"))).toBe(false); } finally { fixture.cleanup(); } }); - it("does not overwrite a preexisting nemoclaw/dist directory (#7915)", () => { - const fixture = runRestoreValidation({ preexistingDist: "nested-directory" }); + it("rejects a symlinked nemoclaw directory without writing outside the workspace (#7915)", () => { + const fixture = runRestoreValidation({ preexistingDist: "symlinked-plugin-parent" }); try { expect(fixture.result.status, fixture.output).not.toBe(0); expect(fixture.output).toContain( - "consumer unexpectedly built nemoclaw/dist before artifact restore", + "consumer nemoclaw directory must be a non-symlink directory", + ); + expect(fs.lstatSync(path.join(fixture.workspace, "nemoclaw")).isSymbolicLink()).toBe(true); + expect(fs.existsSync(path.join(path.dirname(fixture.workspace), "escaped", "dist"))).toBe( + false, ); - expect( - fs.readFileSync(path.join(fixture.workspace, "nemoclaw", "dist", "existing.txt"), "utf8"), - ).toBe("preserve\n"); expect(fs.existsSync(path.join(fixture.workspace, "dist"))).toBe(false); } finally { fixture.cleanup(); } }); + it("does not overwrite a dangling dist symlink (#7915)", () => { + const fixture = runRestoreValidation({ preexistingDist: "dangling-symlink" }); + try { + expect(fixture.result.status, fixture.output).not.toBe(0); + expect(fixture.output).toContain("consumer unexpectedly built dist before artifact restore"); + expect(fs.lstatSync(path.join(fixture.workspace, "dist")).isSymbolicLink()).toBe(true); + expect(fs.readlinkSync(path.join(fixture.workspace, "dist"))).toBe("missing-dist"); + } finally { + fixture.cleanup(); + } + }); + it("rejects a compiled identity mismatch before artifact activation (#7915)", () => { expectRestoreFailure( { buildIdentitySha: "e".repeat(40) }, diff --git a/tools/e2e/cli-artifact-workflow-boundary.mts b/tools/e2e/cli-artifact-workflow-boundary.mts index fb69fec1685..002a3dc2639 100644 --- a/tools/e2e/cli-artifact-workflow-boundary.mts +++ b/tools/e2e/cli-artifact-workflow-boundary.mts @@ -19,7 +19,7 @@ export const CLI_ARTIFACT_DOWNLOAD_ACTION = export const CLI_ARTIFACT_UPLOAD_ACTION = "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a"; export const CLI_ARTIFACT_RESTORE_ACTION = - "NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d697860bed1040f7dab5a379e1c60310a90e8f9d"; + "NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@d34b11e8b895760d6505d54e2cfc520332cb2a9c"; export const CLI_ARTIFACT_PACKAGE_STEP = "Package exact-commit CLI"; export const CLI_ARTIFACT_PUBLISH_STEP = "Publish content-addressed CLI artifact"; export const CLI_ARTIFACT_RESTORE_STEP = "Restore exact-commit CLI artifact"; @@ -33,7 +33,7 @@ const DEFAULT_RESTORE_ACTION_PATH = join( "action.yaml", ); const RESTORE_ACTION_CONTENT_SHA256 = - "89d52595cbe63a11c0ba8bb11930474c8ce605b3b3773ce007fcd82775c46683"; + "f90d4532a64473817b183856e225b401b201d71aaafa75bd7cf8d63a3deeb76f"; const CLI_ARTIFACT_DOWNLOAD_STEP = "Download exact-commit CLI artifact"; const CLI_ARTIFACT_VERIFY_STEP = "Verify and restore exact-commit CLI artifact"; const CLI_ARTIFACT_PROVENANCE_STEP = "Record CLI artifact provenance"; @@ -164,12 +164,18 @@ export function validateCliArtifactRestoreAction( '*) echo "::error::CLI artifact contains an unsafe member', "CLI artifact contains a link or special file", '[[ ! -e "$GITHUB_WORKSPACE/dist" && ! -L "$GITHUB_WORKSPACE/dist" ]]', + '[[ -d "$GITHUB_WORKSPACE/nemoclaw" && ! -L "$GITHUB_WORKSPACE/nemoclaw" ]]', + '[[ ! -e "$GITHUB_WORKSPACE/nemoclaw/dist" && ! -L "$GITHUB_WORKSPACE/nemoclaw/dist" ]]', + 'restore_dir="$(mktemp -d', 'tar --no-same-owner --no-same-permissions -xf "$payload" -C "$restore_dir"', + '[[ -f "$cli_entrypoint" && ! -L "$cli_entrypoint" && -s "$cli_entrypoint" ]]', + '[[ -f "$boundary_path" && ! -L "$boundary_path" && -s "$boundary_path" ]]', + ".sourceRevision == $candidateSha", - 'test -s "$restore_dir/nemoclaw/dist/shared/sandbox-name.cjs"', 'mv "$restore_dir/nemoclaw/dist" "$GITHUB_WORKSPACE/nemoclaw/dist"', + 'mv "$restore_dir/dist" "$GITHUB_WORKSPACE/dist"', 'node "$GITHUB_WORKSPACE/bin/nemoclaw.js" --version', ]); @@ -225,14 +231,16 @@ function validateProducer(errors: string[], producer: WorkflowRecord): void { } requireFragments(errors, "CLI artifact package step", packageStep.run, [ 'git rev-parse --verify HEAD)" == "$CANDIDATE_SHA"', - "test -s dist/nemoclaw.js", - "test -s dist/build-identity.json", - "test -s nemoclaw/dist/shared/sandbox-name.cjs", + "for required_file in dist/nemoclaw.js dist/build-identity.json; do", + '[[ -f "$required_file" && ! -L "$required_file" && -s "$required_file" ]]', + '[[ -f "$boundary_path" && ! -L "$boundary_path" && -s "$boundary_path" ]]', + ".sourceRevision == $candidateSha", "candidate CLI build identity does not match the candidate commit SHA", "--sort=name", "--mtime=@0", - "nemoclaw/dist", + "nemoclaw/dist/shared", + "source_tree=\"$(git rev-parse 'HEAD^{tree}')\"", 'lockfile_sha256="$(sha256sum package-lock.json', 'artifact_name="nemoclaw-cli-${CANDIDATE_SHA}-${payload_sha256}"',