Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions .github/actions/restore-e2e-cli-artifact/action.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,19 @@ runs:
{ echo "::error::exact-commit CLI artifact is incomplete"; exit 1; }
[[ "$(node --version)" =~ ^v22\.[0-9]+\.[0-9]+$ ]] ||
{ echo "::error::consumer must restore the CLI under the pinned Node 22 toolchain"; exit 1; }

sha256_file() {
node --input-type=module --eval '
import { createHash } from "node:crypto";
import { createReadStream } from "node:fs";
const hash = createHash("sha256");
for await (const chunk of createReadStream(process.argv[1])) hash.update(chunk);
process.stdout.write(hash.digest("hex"));
' "$1"
}

source_tree="$(git rev-parse 'HEAD^{tree}')"
lockfile_sha256="$(sha256sum package-lock.json | awk '{print $1}')"
lockfile_sha256="$(sha256_file package-lock.json)"
jq -e \
--arg artifactName "$ARTIFACT_NAME" \
--arg candidateRepository "$CANDIDATE_REPOSITORY" \
Expand Down Expand Up @@ -147,7 +158,7 @@ runs:
.payload.sha256 == $payloadSha256
' "$manifest" >/dev/null ||
{ echo "::error::exact-commit CLI artifact provenance mismatch"; exit 1; }
actual_payload_sha256="$(sha256sum "$payload" | awk '{print $1}')"
actual_payload_sha256="$(sha256_file "$payload")"
[[ "$actual_payload_sha256" == "$PAYLOAD_SHA256" ]] ||
{ echo "::error::exact-commit CLI artifact payload digest mismatch"; exit 1; }
while IFS= read -r member; do
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/e2e-standard-profile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -396,7 +396,7 @@ jobs:

- name: Restore exact-commit CLI artifact
if: ${{ inputs.restore_cli }}
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@813ca162c2867a34ba3692ad60dba73f3282baea
with:
provenance-json: ${{ inputs.cli_artifact_provenance }}

Expand Down
28 changes: 14 additions & 14 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2574,7 +2574,7 @@ jobs:
build-cli: "false"

- name: Restore exact-commit CLI artifact
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@813ca162c2867a34ba3692ad60dba73f3282baea
with:
provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }}

Expand Down Expand Up @@ -2846,7 +2846,7 @@ jobs:
"${test_evidence_dir}/dcode-base-image.json"

- name: Restore exact-commit CLI artifact
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@813ca162c2867a34ba3692ad60dba73f3282baea
with:
provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }}

Expand Down Expand Up @@ -3012,7 +3012,7 @@ jobs:
build-cli: "false"

- name: Restore exact-commit CLI artifact
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@813ca162c2867a34ba3692ad60dba73f3282baea
with:
provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }}

Expand Down Expand Up @@ -3278,7 +3278,7 @@ jobs:
build-cli: "false"

- name: Restore exact-commit CLI artifact
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@813ca162c2867a34ba3692ad60dba73f3282baea
with:
provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }}

Expand Down Expand Up @@ -3353,7 +3353,7 @@ jobs:
build-cli: "false"

- name: Restore exact-commit CLI artifact
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@813ca162c2867a34ba3692ad60dba73f3282baea
with:
provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }}

Expand Down Expand Up @@ -3638,7 +3638,7 @@ jobs:
build-cli: "false"

- name: Restore exact-commit CLI artifact
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@813ca162c2867a34ba3692ad60dba73f3282baea
with:
provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }}

Expand Down Expand Up @@ -3775,7 +3775,7 @@ jobs:
build-cli: "false"

- name: Restore exact-commit CLI artifact
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@813ca162c2867a34ba3692ad60dba73f3282baea
with:
provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }}

Expand Down Expand Up @@ -4031,7 +4031,7 @@ jobs:
# The restore action executes the candidate CLI for its final identity
# check. Candidate-controlled state starts with dependency preparation.
- name: Restore exact-commit CLI artifact
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@813ca162c2867a34ba3692ad60dba73f3282baea
with:
provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }}

Expand Down Expand Up @@ -5144,7 +5144,7 @@ jobs:
build-cli: "false"

- name: Restore exact-commit CLI artifact
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@813ca162c2867a34ba3692ad60dba73f3282baea
with:
provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }}

Expand Down Expand Up @@ -5303,7 +5303,7 @@ jobs:
build-cli: "false"

- name: Restore exact-commit CLI artifact
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@813ca162c2867a34ba3692ad60dba73f3282baea
with:
provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }}

Expand Down Expand Up @@ -5559,7 +5559,7 @@ jobs:
build-cli: "false"

- name: Restore exact-commit CLI artifact
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@813ca162c2867a34ba3692ad60dba73f3282baea
with:
provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }}

Expand Down Expand Up @@ -5661,7 +5661,7 @@ jobs:
build-cli: "false"

- name: Restore exact-commit CLI artifact
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@813ca162c2867a34ba3692ad60dba73f3282baea
with:
provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }}

Expand Down Expand Up @@ -5740,7 +5740,7 @@ jobs:
build-cli: "false"

- name: Restore exact-commit CLI artifact
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@813ca162c2867a34ba3692ad60dba73f3282baea
with:
provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }}

Expand Down Expand Up @@ -5816,7 +5816,7 @@ jobs:
build-cli: "false"

- name: Restore exact-commit CLI artifact
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3
uses: NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@813ca162c2867a34ba3692ad60dba73f3282baea
with:
provenance-json: ${{ needs.generate-matrix.outputs.cli_artifact_provenance }}

Expand Down
1 change: 1 addition & 0 deletions test/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ Before the action restores root `dist/` and `nemoclaw/dist/shared/` into the wor
- 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 restore action uses a Node.js 22 process to stream each file as binary data when it verifies SHA-256 digests.
- 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.
Expand Down
40 changes: 40 additions & 0 deletions test/e2e/support/cli-artifact-workflow-boundary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,20 @@ function runRestoreValidation(options: RestoreFixtureOptions = {}) {
`#!/usr/bin/env bash\nset -euo pipefail\nif [[ "$#" -eq 1 && "$1" == "--version" ]]; then\n echo v22.23.1\n exit 0\nfi\nexec ${JSON.stringify(process.execPath)} "$@"\n`,
{ mode: 0o755 },
);
const lockfileSha256 = sha256File(path.join(workspace, "package-lock.json"));
fs.writeFileSync(
path.join(toolDirectory, "sha256sum"),
[
"#!/usr/bin/env bash",
"set -euo pipefail",
'case "${1:-}" in',
` package-lock.json|*/nemoclaw-cli.tar) printf '%s %s\\n' '${"0".repeat(64)}' "$1" ;;`,
` *) printf '%s %s\\n' '${lockfileSha256}' "$1" ;;`,
"esac",
"",
].join("\n"),
{ mode: 0o755 },
);
Comment thread
prekshivyas marked this conversation as resolved.
PREEXISTING_DIST_WRITERS[options.preexistingDist ?? "none"](workspace);

const action = readYaml<CompositeAction>(".github/actions/restore-e2e-cli-artifact/action.yaml");
Expand Down Expand Up @@ -738,6 +752,16 @@ describe("exact-commit CLI artifact workflow boundary", () => {
}
});

it("restores a binary payload when the host SHA-256 utility reports a different digest (#10569)", () => {
const fixture = runRestoreValidation();
try {
expect(fixture.result.status, fixture.output).toBe(0);
expect(fs.existsSync(path.join(fixture.workspace, "dist", "nemoclaw.js"))).toBe(true);
} finally {
fixture.cleanup();
}
});

it("rejects manifest provenance before artifact extraction (#7915)", () => {
expectRestoreFailure(
{ manifestCandidateSha: "e".repeat(40) },
Expand Down Expand Up @@ -898,6 +922,20 @@ describe("exact-commit CLI artifact workflow boundary", () => {
const source = readRepoText(".github/actions/restore-e2e-cli-artifact/action.yaml")
.replace("tar --no-same-owner --no-same-permissions", "tar")
.replace("sandbox-name.cjs", "missing-boundary.cjs")
.replace(
'lockfile_sha256="$(sha256_file package-lock.json)"',
[
'# lockfile_sha256="$(sha256_file package-lock.json)"',
'lockfile_sha256="$(openssl dgst -sha256 -r package-lock.json | cut -d " " -f 1)"',
].join("\n "),
)
.replace(
'actual_payload_sha256="$(sha256_file "$payload")"',
[
'# actual_payload_sha256="$(sha256_file "$payload")"',
'actual_payload_sha256="$(openssl dgst -sha256 -r "$payload" | cut -d " " -f 1)"',
].join("\n "),
)
.replace('[[ "$actual_payload_sha256" == "$PAYLOAD_SHA256" ]]', '[[ -s "$payload" ]]');
fs.writeFileSync(actionPath, source);

Expand All @@ -906,6 +944,8 @@ describe("exact-commit CLI artifact workflow boundary", () => {
"CLI artifact restore action must match its immutable workflow pin",
'CLI artifact payload verification must contain tar --no-same-owner --no-same-permissions -xf "$payload" -C "$restore_dir"',
"CLI artifact payload verification must contain sandbox-name.cjs",
"CLI artifact payload verification must assign lockfile_sha256 exactly once through the Node.js binary stream",
"CLI artifact payload verification must assign actual_payload_sha256 exactly once through the Node.js binary stream",
'CLI artifact payload verification must contain [[ "$actual_payload_sha256" == "$PAYLOAD_SHA256" ]]',
]),
);
Expand Down
44 changes: 44 additions & 0 deletions tools/e2e/cli-artifact-workflow-boundary.mts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,23 @@ function requireFragments(
}
}

function requireUniqueShellAssignment(
errors: string[],
owner: string,
source: unknown,
variable: string,
expected: string,
): void {
const script = typeof source === "string" ? source : "";
const assignments = script
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => !line.startsWith("#") && line.startsWith(`${variable}=`));
if (!isDeepStrictEqual(assignments, [expected])) {
errors.push(`${owner} must assign ${variable} exactly once through the Node.js binary stream`);
}
}

export function validateCliArtifactRestoreAction(
actionPath = DEFAULT_RESTORE_ACTION_PATH,
): string[] {
Expand Down Expand Up @@ -180,6 +197,13 @@ export function validateCliArtifactRestoreAction(
errors.push("CLI artifact restore action must pass validated identity to payload verification");
}
requireFragments(errors, "CLI artifact payload verification", restore?.run, [
"sha256_file() {",
"node --input-type=module --eval",
'import { createHash } from "node:crypto"',
'import { createReadStream } from "node:fs"',
'for await (const chunk of createReadStream(process.argv[1])) hash.update(chunk)',
'process.stdout.write(hash.digest("hex"))',
'lockfile_sha256="$(sha256_file package-lock.json)"',
".candidate.sha == $candidateSha",
".candidate.sourceTree == $sourceTree",
".candidate.lockfileSha256 == $lockfileSha256",
Expand All @@ -188,6 +212,7 @@ export function validateCliArtifactRestoreAction(
".workflow.runAttempt == $runAttempt",
".build.sourceRevision == $candidateSha",
".payload.sha256 == $payloadSha256",
'actual_payload_sha256="$(sha256_file "$payload")"',
'[[ "$actual_payload_sha256" == "$PAYLOAD_SHA256" ]]',
'*) echo "::error::CLI artifact contains an unsafe member',
"CLI artifact contains a link or special file",
Expand All @@ -208,6 +233,25 @@ export function validateCliArtifactRestoreAction(
'mv "$restore_dir/dist" "$GITHUB_WORKSPACE/dist"',
'node "$GITHUB_WORKSPACE/bin/nemoclaw.js" --version',
]);
requireUniqueShellAssignment(
errors,
"CLI artifact payload verification",
restore?.run,
"lockfile_sha256",
'lockfile_sha256="$(sha256_file package-lock.json)"',
);
requireUniqueShellAssignment(
errors,
"CLI artifact payload verification",
restore?.run,
"actual_payload_sha256",
'actual_payload_sha256="$(sha256_file "$payload")"',
);
if (typeof restore?.run === "string" && restore.run.includes("sha256sum")) {
errors.push(
"CLI artifact payload verification must hash files through the pinned Node.js binary stream",
);
}
return errors;
}

Expand Down
2 changes: 1 addition & 1 deletion tools/e2e/mcp-dev-workflow-boundary-digests.mts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export const MCP_DEV_TRUSTED_NODE_SETUP_CONTENT_SHA256 =
export const MCP_DEV_TRUSTED_PREFIX_CONTENT_SHA256 =
"ee28f7ecc4ab0aed53c83793e8c6f57045a49d0cca38ed80786a83eeb5c0b2fc";
export const MCP_DEV_POST_INSTALL_TRANSITION_CONTENT_SHA256 =
"62cf2ee01ac7192f41fc7b2b071de729da8bacec1e4f693da1ec6f0b1f4723c0";
"5b517388f3f47f92452e038a591cdea00501e76bec22144f1b8264e5c21b963f";

export function contentSha256(value: unknown): string {
return createHash("sha256")
Expand Down
4 changes: 2 additions & 2 deletions tools/e2e/workflow-boundary-policy.mts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ export const E2E_ACTION_PROVENANCE = {
},
restoreCliArtifact: {
reference:
"NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@c246409193a31133cab10c8a3589001cc0d59eb3",
contentSha256: "3a81ad631b839aa938eaaf1ad6777bab247204bf86fbca3c43c326a44dfb9c6c",
"NVIDIA/NemoClaw/.github/actions/restore-e2e-cli-artifact@813ca162c2867a34ba3692ad60dba73f3282baea",
contentSha256: "6f8d0138589b7c48a977d004f41cc60f7328d193d85ea5365f0a48b65f3e7485",
},
uploadArtifacts: {
reference:
Expand Down
Loading