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
2 changes: 1 addition & 1 deletion agents/hermes/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,7 @@ RUN node --experimental-strip-types \
ARG NEMOCLAW_HERMES_WRAPPER_SHA256=f4276e9833638b7a620176c88bd329d6b6d4948538a3227b727a1397146a0e0e
ARG NEMOCLAW_HERMES_CLI_ADAPTER_SHA256=989edf54a8c09c6efb348600a8aa2f264c0b71408eb9d7bcd579b92cbeccf9b1
ARG NEMOCLAW_HERMES_CLI_ADAPTER_VALIDATOR_SHA256=db4046e79e513eab67b069a8eda20167b8b65529cf26842531d2ad673c670330
ARG NEMOCLAW_HERMES_VALIDATOR_SHA256=5cdac58c052f1df745d70f0126bc658eff1daef745688dc7570ca7795c068228
ARG NEMOCLAW_HERMES_VALIDATOR_SHA256=b355d1365fb1d15475e327f312ceb854ae96f9ebed28cf96bc8817f550df2688
ARG NEMOCLAW_HERMES_TIRITH_FINALIZER_SHA256=a1e6b1c53ab297569abb87c29d15c294d729e46005bfd022136b4c447a791819
ARG NEMOCLAW_HERMES_CRON_RESTORE_CONTROLLER_SHA256=e8593cf1580bffa4663e91c079ba0ce31c3d26391f5b1718872701138ce250b0
# hadolint ignore=DL4006
Expand Down
18 changes: 18 additions & 0 deletions agents/hermes/validate-env-secret-boundary.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@
PLACEHOLDER_RE = re.compile(r"^(xoxb|xapp)-OPENSHELL-RESOLVE-ENV-[A-Z0-9_]+$")
KEY_NAME_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
API_SERVER_KEY_RE = re.compile(r"^[0-9a-f]{64}$")
HERMES_API_PORT_RANGE_START = 8642
HERMES_API_PORT_RANGE_END = 8652

ENV_FILE_ALLOWED_NONSECRET_KEYS = frozenset({"API_SERVER_HOST", "API_SERVER_PORT"})
# API_SERVER_KEY is the bearer token Hermes' own api_server (Hermes v0.16.0+)
Expand Down Expand Up @@ -363,6 +365,15 @@ def is_generated_api_server_key(value: str) -> bool:
return API_SERVER_KEY_RE.fullmatch(unquote(value)) is not None


def is_assigned_hermes_api_port(value: str) -> bool:
return (
len(value) == 4
and value.isascii()
and value.isdecimal()
and HERMES_API_PORT_RANGE_START <= int(value) <= HERMES_API_PORT_RANGE_END
)


def is_allowed_raw_secret_value(key: str, value: str) -> bool:
if key == "OPENCLAW_GATEWAY_TOKEN":
return True
Expand Down Expand Up @@ -513,6 +524,13 @@ def validate_runtime_env(env: dict[str, str] | None = None) -> int:
continue
if key == "HERMES_LAZY_INSTALL_TARGET":
continue
if key == "NEMOCLAW_HERMES_API_PORT":
if is_assigned_hermes_api_port(value):
continue
violation_count += 1
if len(violations) < MAX_VIOLATIONS:
violations.append(key)
continue
if key in RUNTIME_ALLOWED_NONSECRET_KEYS:
continue
if key in RUNTIME_ALLOWED_RAW_SECRET_KEYS and is_allowed_raw_secret_value(
Expand Down
15 changes: 13 additions & 2 deletions scripts/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3468,6 +3468,7 @@ repair_installer_nvidia_cdi_spec() {
run_installer_host_preflight() {
local preflight_module="${NEMOCLAW_SOURCE_ROOT}/dist/lib/onboard/preflight.js"
local gateway_management_module="${NEMOCLAW_SOURCE_ROOT}/dist/lib/onboard/gateway-management.js"
local portable_profile_module="${NEMOCLAW_SOURCE_ROOT}/dist/lib/onboard/experimental/portable-profile.js"
local host_readiness_module="${NEMOCLAW_SOURCE_ROOT}/dist/lib/readiness/host.js"
local onboard_admission_module="${NEMOCLAW_SOURCE_ROOT}/dist/lib/readiness/onboard-admission.js"
if ! command_exists node \
Expand All @@ -3488,6 +3489,16 @@ run_installer_host_preflight() {
const hostReadinessPath = process.argv[2];
const onboardAdmissionPath = process.argv[3];
const gatewayManagementPath = process.argv[4];
const portableProfilePath = process.argv[5];
let explicitlySelectedPortableProfile = false;
try {
const portableProfile = require(portableProfilePath);
if (typeof portableProfile.isPortableExperimentalProfile === "function") {
explicitlySelectedPortableProfile = Boolean(
portableProfile.isPortableExperimentalProfile()
);
}
} catch {}
try {
const { assessHost, planHostAdvisories } = require(preflightPath);
const { createHostReadinessReport } = require(hostReadinessPath);
Expand All @@ -3512,7 +3523,7 @@ run_installer_host_preflight() {
);
const admission = evaluateOnboardReadinessAdmission(readiness, {
explicitlyOptedOutGpuPassthrough: false,
allowUnsupportedRuntime: false,
allowUnsupportedRuntime: explicitlySelectedPortableProfile,
// The installer starts a NemoClaw-managed onboarding flow. Let the
// authoritative onboarding gate apply supported storage remediation,
// but only when the gateway declaration confirms NemoClaw ownership.
Expand Down Expand Up @@ -3584,7 +3595,7 @@ run_installer_host_preflight() {
} catch {
process.exit(0);
}
' "$preflight_module" "$host_readiness_module" "$onboard_admission_module" "$gateway_management_module"
' "$preflight_module" "$host_readiness_module" "$onboard_admission_module" "$gateway_management_module" "$portable_profile_module"
)"; then
status=0
else
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,7 @@ it("journals not-ready repair on the selected non-default gateway (#6492)", asyn
reference: "openshell/sandbox-from:new",
},
lifecycleGeneration: "replacement-generation",
lifecycleLiveIdentityFingerprint: fingerprintSandboxRecreateValue(
"openshell-replacement-id",
),
lifecycleLiveIdentityFingerprint: fingerprintSandboxRecreateValue("replacement-identity"),
};
const phases: Array<string | null> = [];
const updateSession = vi.fn((mutator: (value: Session) => Session | void) => {
Expand Down
10 changes: 9 additions & 1 deletion test/helpers/installer-readiness-stubs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ export function writeFailedOnboardSession(home: string): void {

export function writeInstallerReadinessModuleStubs(readinessDir: string): void {
const onboardDir = path.join(path.dirname(readinessDir), "onboard");
const experimentalDir = path.join(onboardDir, "experimental");
fs.mkdirSync(readinessDir, { recursive: true });
fs.mkdirSync(onboardDir, { recursive: true });
fs.mkdirSync(experimentalDir, { recursive: true });
fs.writeFileSync(
`${readinessDir}/host.js`,
`exports.createHostReadinessReport = (_options, collection) => ({ host: collection.assess() });\n`,
Expand All @@ -51,7 +53,9 @@ export function writeInstallerReadinessModuleStubs(readinessDir: string): void {
`${readinessDir}/onboard-admission.js`,
`exports.evaluateOnboardReadinessAdmission = (report, options) => {
const host = report.host;
const unsupportedRuntime = host.runtime === "podman" || host.isUnsupportedRuntime === true;
const unsupportedRuntime =
(host.runtime === "podman" || host.isUnsupportedRuntime === true) &&
!options.allowUnsupportedRuntime;
const cdiBlocks = host.cdiNvidiaGpuSpecNeedsRepair && !(host.isWsl && host.runtime === "docker-desktop");
const storageRemediationAvailable =
host.platform === "linux" &&
Expand Down Expand Up @@ -91,6 +95,10 @@ export function writeInstallerReadinessModuleStubs(readinessDir: string): void {
};
`,
);
fs.writeFileSync(
`${experimentalDir}/portable-profile.js`,
`exports.isPortableExperimentalProfile = (env = process.env) => env.NEMOCLAW_EXPERIMENTAL_PROFILE === "portable";\n`,
);
}

export function runStorageRemediationInstallerPreflight({
Expand Down
12 changes: 12 additions & 0 deletions test/hermes-env-secret-boundary-hardening.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,18 @@ describe("Hermes durable lazy-install target", () => {
expect(result.stderr).toBe("");
});

it.each([
{ case: "below the assigned range", value: "8641" },
{ case: "above the assigned range", value: "8653" },
{ case: "with a secret-shaped value", value: "secret-port-value" },
])("rejects a Hermes API port $case without exposing its value", ({ value }) => {
const result = runRuntimeEnvValidation({ NEMOCLAW_HERMES_API_PORT: value });

expect(result.status).toBe(1);
expect(result.stderr).toContain("NEMOCLAW_HERMES_API_PORT");
expect(result.stderr).not.toContain(value);
});

it("accepts the image-owned lazy target in the runtime environment (#8613)", () => {
const result = runRuntimeEnvValidation({
HERMES_LAZY_INSTALL_TARGET: "/sandbox/.hermes/lazy-packages",
Expand Down
32 changes: 32 additions & 0 deletions test/install-portable-profile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// 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";
Expand Down Expand Up @@ -46,4 +48,34 @@ describe("installer portable profile runtime override", () => {
expect(result.stdout).toBe("DOCKER_HOST=unix:///preexisting.sock\n");
expect(result.stderr).toBe("");
});

it("rejects an unknown experimental profile before install effects (#9007)", () => {
const fixture = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-invalid-profile-"));
const marker = path.join(fixture, "existing-state");
fs.writeFileSync(marker, "unchanged\n");
const stateBefore = fs.readdirSync(fixture);

const result = spawnSync(
"bash",
[INSTALLER_PAYLOAD, "--experimental-profile", "not-portable"],
{
cwd: fixture,
encoding: "utf-8",
env: {
...process.env,
HOME: fixture,
NEMOCLAW_EXPERIMENTAL_PROFILE: "",
TMPDIR: fixture,
XDG_CONFIG_HOME: path.join(fixture, "config"),
},
},
);

expect(result.status).toBe(1);
expect(`${result.stdout}${result.stderr}`).toContain(
"Unknown experimental profile: not-portable (expected: portable).",
);
expect(fs.readdirSync(fixture)).toEqual(stateBefore);
expect(fs.readFileSync(marker, "utf-8")).toBe("unchanged\n");
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});
Loading
Loading