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
4 changes: 2 additions & 2 deletions scripts/update-docker-pin.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ resolve_latest_digest() {
local token digest

# Step 1: get an auth token for the Docker Hub library repo
token=$(curl -fsSL --retry 3 --retry-delay 1 --retry-all-errors \
token=$(curl -fsSL --proto '=https' --proto-redir '=https' --retry 3 --retry-delay 1 --retry-all-errors \
--connect-timeout 10 --max-time 30 \
"https://auth.docker.io/token?service=registry.docker.io&scope=repository:library/${IMAGE}:pull" \
| python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")
Expand All @@ -44,7 +44,7 @@ resolve_latest_digest() {
fi

# Step 2: fetch the tag headers and use Docker-Content-Digest for the index.
digest=$(curl -fsSIL --retry 3 --retry-delay 1 --retry-all-errors \
digest=$(curl -fsSIL --proto '=https' --proto-redir '=https' --retry 3 --retry-delay 1 --retry-all-errors \
--connect-timeout 10 --max-time 30 \
-H "Authorization: Bearer ${token}" \
-H "Accept: application/vnd.docker.distribution.manifest.list.v2+json, application/vnd.oci.image.index.v1+json" \
Expand Down
4 changes: 2 additions & 2 deletions scripts/update-hermes-agent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ gh_api() {
local url="$1"
local -a auth=()
[[ -n "${GITHUB_TOKEN:-}" ]] && auth=(-H "Authorization: Bearer ${GITHUB_TOKEN}")
curl -fsSL --retry 3 --retry-delay 1 --retry-all-errors \
curl -fsSL --proto '=https' --proto-redir '=https' --retry 3 --retry-delay 1 --retry-all-errors \
--connect-timeout 10 --max-time 60 \
-H "Accept: application/vnd.github+json" "${auth[@]}" "$url"
}
Expand Down Expand Up @@ -327,7 +327,7 @@ trap 'rm -rf "$WORKDIR_TMP"' EXIT
TARBALL="${WORKDIR_TMP}/hermes-${TAG}.tar.gz"
TARBALL_URL="https://github.com/${GITHUB_REPO}/archive/refs/tags/${TAG}.tar.gz"
echo "Downloading ${TARBALL_URL}"
curl -fsSL --retry 3 --retry-delay 1 --retry-all-errors \
curl -fsSL --proto '=https' --proto-redir '=https' --retry 3 --retry-delay 1 --retry-all-errors \
--connect-timeout 10 --max-time 300 \
-o "$TARBALL" "$TARBALL_URL"

Expand Down
57 changes: 57 additions & 0 deletions test/release-pin-script-curl-proto-pin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { describe, expect, it } from "vitest";

const REPO_ROOT = path.join(import.meta.dirname, "..");
const DOCKER_PIN_SCRIPT = path.join(REPO_ROOT, "scripts", "update-docker-pin.sh");
const FAKE_DIGEST = `sha256:${"a".repeat(64)}`;

function writeExecutable(file: string, body: string) {
fs.writeFileSync(file, body, { mode: 0o755 });
}

describe("release-pin update scripts pin curl to HTTPS (#9979)", () => {
it("passes --proto/--proto-redir on every curl call update-docker-pin.sh makes", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-docker-pin-"));
const fixtureScript = path.join(tmp, "scripts", "update-docker-pin.sh");
const fakeBin = path.join(tmp, "bin");
const curlLog = path.join(tmp, "curl-argv.log");
fs.mkdirSync(path.dirname(fixtureScript), { recursive: true });
fs.mkdirSync(fakeBin, { recursive: true });
fs.copyFileSync(DOCKER_PIN_SCRIPT, fixtureScript);
fs.chmodSync(fixtureScript, 0o755);
fs.writeFileSync(
path.join(tmp, "Dockerfile"),
`FROM node:22-trixie-slim@sha256:${"0".repeat(64)}\n`,
);
writeExecutable(
path.join(fakeBin, "curl"),
[
"#!/usr/bin/env bash",
'printf \'%s\\n\' "$*" >> "$FAKE_CURL_LOG"',
'if [[ "$*" == *auth.docker.io* ]]; then',
' echo \'{"token":"faketoken"}\'',
'elif [[ "$*" == *registry-1.docker.io* ]]; then',
" printf 'HTTP/1.1 200 OK\\r\\nDocker-Content-Digest: %s\\r\\n\\r\\n' ",
` ${JSON.stringify(FAKE_DIGEST)}`,
"fi",
].join("\n"),
);

const result = spawnSync("bash", [fixtureScript, "--check"], {
env: { ...process.env, PATH: `${fakeBin}:${process.env.PATH}`, FAKE_CURL_LOG: curlLog },
encoding: "utf-8",
});

const curlArgv = fs.readFileSync(curlLog, "utf-8").trim();
const curlCallCount = curlArgv.split("\n").length;
const pinnedCallCount = curlArgv.split("--proto =https --proto-redir =https").length - 1;
expect(curlArgv, result.stdout + result.stderr).not.toBe("");
expect(pinnedCallCount).toBe(curlCallCount);
});
});
85 changes: 85 additions & 0 deletions test/update-hermes-agent-script.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,12 @@ describe("scripts/update-hermes-agent.sh", () => {
fs.chmodSync(script, 0o755);
fs.copyFileSync(HERMES_BASE_DOCKERFILE, path.join(repo, "agents", "hermes", "Dockerfile.base"));
fs.copyFileSync(HERMES_MANIFEST, path.join(repo, "agents", "hermes", "manifest.yaml"));
const curlLog = path.join(tmp, "curl-argv.log");
writeExecutable(
path.join(fakeBin, "curl"),
`#!/usr/bin/env bash
set -euo pipefail
printf '%s\\n' "$*" >> "$FAKE_CURL_LOG"
output=""
previous=""
for arg in "$@"; do
Expand Down Expand Up @@ -122,6 +124,7 @@ fi
HERMES_BASE_REF: baseRef,
FAKE_DOCKER_LOG: dockerLog,
FAKE_NEMOHERMES_LOG: nemohermesLog,
FAKE_CURL_LOG: curlLog,
NEMOCLAW_SOURCE_ROOT: undefined,
},
timeout: 10_000,
Expand All @@ -131,6 +134,88 @@ fi
expect(fs.readFileSync(dockerLog, "utf8")).toContain(`tag ${baseRef} ${pinnedRef}`);
expect(fs.readFileSync(nemohermesLog, "utf8")).toContain(`${pinnedRef}|hermes rebuild`);
expect(run.stdout).toContain("OK: sandbox reports Hermes Agent v0.19.0");
// #9979: the curl fetch must fail closed on a protocol-downgrade redirect.
const curlArgv = fs.readFileSync(curlLog, "utf8").trim();
const curlCallCount = curlArgv.split("\n").length;
const pinnedCallCount = curlArgv.split("--proto =https --proto-redir =https").length - 1;
expect(curlArgv).not.toBe("");
expect(pinnedCallCount).toBe(curlCallCount);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
});

it("pins the latest-release GitHub API lookup to HTTPS when --tag is omitted (#9979)", () => {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-hermes-update-latest-"));
try {
const repo = path.join(tmp, "repo");
const script = path.join(repo, "scripts", "update-hermes-agent.sh");
const fakeBin = path.join(tmp, "bin");
const curlLog = path.join(tmp, "curl-argv.log");
fs.mkdirSync(path.dirname(script), { recursive: true });
fs.mkdirSync(path.join(repo, "agents", "hermes"), { recursive: true });
fs.mkdirSync(fakeBin, { recursive: true });
fs.copyFileSync(SCRIPT, script);
fs.chmodSync(script, 0o755);
fs.copyFileSync(
HERMES_BASE_DOCKERFILE,
path.join(repo, "agents", "hermes", "Dockerfile.base"),
);
fs.copyFileSync(HERMES_MANIFEST, path.join(repo, "agents", "hermes", "manifest.yaml"));
writeExecutable(
path.join(fakeBin, "curl"),
`#!/usr/bin/env bash
set -euo pipefail
# Redact any bearer token before it ever touches disk, so a real
# credential can never end up in a test log even transiently.
printf '%s\\n' "$*" | sed -E 's/(Authorization: ?Bearer )[^ ]+/\\1[REDACTED]/' >> "$FAKE_CURL_LOG"
if [[ "$*" == *api.github.com* ]]; then
printf '{"tag_name":"v2026.6.5"}'
fi
`,
);

const run = spawnSync("bash", [script, "--check"], {
encoding: "utf8",
env: {
...process.env,
PATH: `${fakeBin}:${process.env.PATH}`,
HOME: path.join(tmp, "home"),
FAKE_CURL_LOG: curlLog,
// A deliberately fake, obviously-not-a-secret value: proves the
// auth path is exercised without ever risking a real token,
// even if one happens to be set in the host environment.
GITHUB_TOKEN: "test-not-a-real-token",
NEMOCLAW_SOURCE_ROOT: undefined,
},
timeout: 10_000,
});

// --check exits 0 (pins current) or 1 (pins stale); either is a
// completed run. Anything else means the fixture itself broke.
expect([0, 1], `${run.stdout}\n${run.stderr}`).toContain(run.status);
expect(run.stdout).toMatch(/^(OK|STALE): Dockerfile\.base pins Hermes/m);

// gh_api() is only reached when --tag is omitted; #9979 pins its
// request (which can carry an Authorization: Bearer GITHUB_TOKEN
// header) to HTTPS. Validate every logged invocation independently,
// not just an aggregate count, so one covered and one uncovered call
// cannot offset each other.
const curlCalls = fs
.readFileSync(curlLog, "utf8")
.split("\n")
.filter((line) => line.length > 0);
expect(curlCalls.length).toBeGreaterThan(0);
expect(
curlCalls.every(
(call) => call.includes("--proto =https") && call.includes("--proto-redir =https"),
),
).toBe(true);
expect(curlCalls.some((call) => call.includes("[REDACTED]"))).toBe(true);
expect(curlCalls.join("\n")).not.toContain("test-not-a-real-token");
expect(curlCalls.join("\n")).toContain(
"api.github.com/repos/NousResearch/hermes-agent/releases/latest",
);
} finally {
fs.rmSync(tmp, { recursive: true, force: true });
}
Expand Down
Loading