Skip to content
Open
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
72 changes: 45 additions & 27 deletions scripts/lib/entrypoint-env-wrapper.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,38 @@ nemoclaw_normalize_entrypoint_env_wrapper() {
local -a _nemoclaw_assignments=()
local _nemoclaw_self_index=-1
local _nemoclaw_index
local _nemoclaw_break_index
local _nemoclaw_token
local _nemoclaw_name
local _nemoclaw_seen_names="|"
local _nemoclaw_supported_names="|AWS_EC2_METADATA_DISABLED|CHAT_UI_URL"
_nemoclaw_supported_names="${_nemoclaw_supported_names}|HTTP_PROXY|HTTPS_PROXY|NO_PROXY"
_nemoclaw_supported_names="${_nemoclaw_supported_names}|http_proxy|https_proxy|no_proxy"
_nemoclaw_supported_names="${_nemoclaw_supported_names}|OPENCLAW_HOME|OPENCLAW_STATE_DIR"
_nemoclaw_supported_names="${_nemoclaw_supported_names}|OPENCLAW_WORKSPACE_DIR"
_nemoclaw_supported_names="${_nemoclaw_supported_names}|NEMOCLAW_AUTO_PAIR_DEADLINE_SECS"
_nemoclaw_supported_names="${_nemoclaw_supported_names}|NEMOCLAW_AUTO_PAIR_FAST_DEADLINE_SECS"
_nemoclaw_supported_names="${_nemoclaw_supported_names}|NEMOCLAW_AUTO_PAIR_FAST_REENTRY_INTERVAL_SECS"
_nemoclaw_supported_names="${_nemoclaw_supported_names}|NEMOCLAW_AUTO_PAIR_FAST_REENTRY_POLLS"
_nemoclaw_supported_names="${_nemoclaw_supported_names}|NEMOCLAW_AUTO_PAIR_RUN_TIMEOUT_SECS"
_nemoclaw_supported_names="${_nemoclaw_supported_names}|NEMOCLAW_AUTO_PAIR_SLOW_INTERVAL_SECS"
_nemoclaw_supported_names="${_nemoclaw_supported_names}|NEMOCLAW_CORPORATE_CA_B64"
_nemoclaw_supported_names="${_nemoclaw_supported_names}|NEMOCLAW_DASHBOARD_BIND|NEMOCLAW_DASHBOARD_PORT"
_nemoclaw_supported_names="${_nemoclaw_supported_names}|NEMOCLAW_EXTRA_PLACEHOLDER_KEYS"
_nemoclaw_supported_names="${_nemoclaw_supported_names}|NEMOCLAW_HERMES_DASHBOARD"
_nemoclaw_supported_names="${_nemoclaw_supported_names}|NEMOCLAW_HERMES_DASHBOARD_INTERNAL_PORT"
_nemoclaw_supported_names="${_nemoclaw_supported_names}|NEMOCLAW_HERMES_DASHBOARD_PORT"
_nemoclaw_supported_names="${_nemoclaw_supported_names}|NEMOCLAW_HERMES_DASHBOARD_TUI"
_nemoclaw_supported_names="${_nemoclaw_supported_names}|NEMOCLAW_MINIMAL_BOOTSTRAP"
_nemoclaw_supported_names="${_nemoclaw_supported_names}|NEMOCLAW_OBSERVABILITY"
_nemoclaw_supported_names="${_nemoclaw_supported_names}|NEMOCLAW_PROXY_HOST|NEMOCLAW_PROXY_PORT"
_nemoclaw_supported_names="${_nemoclaw_supported_names}|NEMOCLAW_SANDBOX_NAME"
_nemoclaw_supported_names="${_nemoclaw_supported_names}|NEMOCLAW_STARTUP_PROFILE_B64|"

# Locate only the exact self-wrapper grammar. A normal explicit command such
# as `env FOO=bar printenv` remains a user command and is not interpreted by
# this root entrypoint normalization.
# as `env FOO=bar printenv` carries no managed variable name in its leading
# assignment run, so it remains a user command and is not interpreted by this
# root entrypoint normalization.
for ((_nemoclaw_index = 1; _nemoclaw_index < ${#_nemoclaw_original_argv[@]}; _nemoclaw_index += 1)); do
_nemoclaw_token="${_nemoclaw_original_argv[$_nemoclaw_index]}"
case "$_nemoclaw_token" in
Expand All @@ -54,7 +79,12 @@ nemoclaw_normalize_entrypoint_env_wrapper() {

if [ "$_nemoclaw_self_index" -lt 0 ]; then
# A managed handoff must never silently degrade into an unmanaged command
# because the self-wrapper was absent or malformed.
# because the self-wrapper was absent or malformed. This normalization
# rejects NEMOCLAW_STARTUP_PROFILE_B64 and NEMOCLAW_CORPORATE_CA_B64 in any
# argument position. Any other managed name indicates a degraded handoff
# only when it appears in the leading assignment run. A user command tail
# that only looks like an assignment therefore stays a user command.
_nemoclaw_break_index="$_nemoclaw_index"
for _nemoclaw_token in "${_nemoclaw_original_argv[@]:1}"; do
case "$_nemoclaw_token" in
NEMOCLAW_STARTUP_PROFILE_B64=* | NEMOCLAW_CORPORATE_CA_B64=*)
Expand All @@ -64,6 +94,16 @@ nemoclaw_normalize_entrypoint_env_wrapper() {
;;
esac
done
for ((_nemoclaw_index = 1; _nemoclaw_index < _nemoclaw_break_index; _nemoclaw_index += 1)); do
_nemoclaw_name="${_nemoclaw_original_argv[$_nemoclaw_index]%%=*}"
case "$_nemoclaw_supported_names" in
*"|${_nemoclaw_name}|"*)
printf '%s\n' \
'[SECURITY] Malformed managed startup env wrapper; expected nemoclaw-start after assignments.' >&2
return 1
;;
esac
done
return 0
fi

Expand All @@ -82,30 +122,8 @@ nemoclaw_normalize_entrypoint_env_wrapper() {
printf '%s\n' '[SECURITY] Managed startup env wrapper contains a malformed assignment.' >&2
return 1
fi
case "$_nemoclaw_name" in
AWS_EC2_METADATA_DISABLED | \
CHAT_UI_URL | \
HTTP_PROXY | HTTPS_PROXY | NO_PROXY | \
http_proxy | https_proxy | no_proxy | \
OPENCLAW_HOME | OPENCLAW_STATE_DIR | OPENCLAW_WORKSPACE_DIR | \
NEMOCLAW_AUTO_PAIR_DEADLINE_SECS | \
NEMOCLAW_AUTO_PAIR_FAST_DEADLINE_SECS | \
NEMOCLAW_AUTO_PAIR_FAST_REENTRY_INTERVAL_SECS | \
NEMOCLAW_AUTO_PAIR_FAST_REENTRY_POLLS | \
NEMOCLAW_AUTO_PAIR_RUN_TIMEOUT_SECS | \
NEMOCLAW_AUTO_PAIR_SLOW_INTERVAL_SECS | \
NEMOCLAW_CORPORATE_CA_B64 | \
NEMOCLAW_DASHBOARD_BIND | NEMOCLAW_DASHBOARD_PORT | \
NEMOCLAW_EXTRA_PLACEHOLDER_KEYS | \
NEMOCLAW_HERMES_DASHBOARD | \
NEMOCLAW_HERMES_DASHBOARD_INTERNAL_PORT | \
NEMOCLAW_HERMES_DASHBOARD_PORT | \
NEMOCLAW_HERMES_DASHBOARD_TUI | \
NEMOCLAW_MINIMAL_BOOTSTRAP | \
NEMOCLAW_OBSERVABILITY | \
NEMOCLAW_PROXY_HOST | NEMOCLAW_PROXY_PORT | \
NEMOCLAW_SANDBOX_NAME | \
NEMOCLAW_STARTUP_PROFILE_B64) ;;
case "$_nemoclaw_supported_names" in
*"|${_nemoclaw_name}|"*) ;;
*)
printf '%s\n' \
"[SECURITY] Managed startup env wrapper contains unsupported variable '${_nemoclaw_name}'." >&2
Expand Down
36 changes: 35 additions & 1 deletion test/entrypoint-env-wrapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,29 @@ describe("OCI entrypoint env-wrapper normalization", () => {
expect(result.stdout).toContain("ARG=env\nARG=FOO=bar\nARG=printenv\nARG=FOO\n");
});

it("leaves a user command tail that only looks like a managed assignment", () => {
const result = runNormalizer([
"env",
"FOO=bar",
"/bin/sh",
"-c",
"NEMOCLAW_SANDBOX_NAME=probe",
]);

expect(result.status).toBe(0);
expect(result.stdout).toContain(
"ARG=env\nARG=FOO=bar\nARG=/bin/sh\nARG=-c\nARG=NEMOCLAW_SANDBOX_NAME=probe\n",
);
});
Comment on lines +87 to +100

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline test/entrypoint-env-wrapper.test.ts --items all
sed -n '1,155p' test/entrypoint-env-wrapper.test.ts

Repository: NVIDIA/NemoClaw

Length of output: 6925


Append (#8595) to both test titles. The enclosing describe title has no issue suffix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/entrypoint-env-wrapper.test.ts` around lines 87 - 100, Update the two
affected test titles in the enclosing describe block, including “leaves a user
command tail that only looks like a managed assignment,” by appending the exact
suffix “(`#8595`)” to each title while preserving their existing wording.

Sources: Coding guidelines, Learnings


it.each([
{
name: "rejects an interpreter variable outside the supported set",
argv: ["env", "NODE_OPTIONS=--require=/sandbox/untrusted.cjs", "nemoclaw-start"],
message: "unsupported variable 'NODE_OPTIONS'",
},
{
name: "rejects a repeated assignment",
argv: [
"env",
"NEMOCLAW_STARTUP_PROFILE_B64=first",
Expand All @@ -99,14 +116,31 @@ describe("OCI entrypoint env-wrapper normalization", () => {
message: "repeats variable 'NEMOCLAW_STARTUP_PROFILE_B64'",
},
{
name: "rejects a startup profile handed to another command",
argv: ["env", "NEMOCLAW_STARTUP_PROFILE_B64=profile", "/usr/bin/true"],
message: "Malformed managed startup env wrapper",
},
{
name: "rejects a break token between assignments and the terminator",
argv: ["env", "NEMOCLAW_CORPORATE_CA_B64=Y2E=", "not-an-assignment", "nemoclaw-start"],
message: "Malformed managed startup env wrapper",
},
])("fails closed for malformed or unsafe root handoff: $message", ({ argv, message }) => {
{
name: "rejects a managed name in the leading assignment run",
argv: ["env", "NEMOCLAW_AUTO_PAIR_FAST_REENTRY_INTERVAL_SECS=5", "/bin/sh"],
message: "Malformed managed startup env wrapper",
},
{
name: "rejects several managed names in the leading assignment run",
argv: ["env", "NEMOCLAW_DASHBOARD_PORT=9000", "OPENCLAW_HOME=/sandbox", "/bin/sh", "-c", ":"],
message: "Malformed managed startup env wrapper",
},
{
name: "rejects a corporate CA payload in the user command tail",
argv: ["env", "FOO=bar", "/bin/sh", "-c", "NEMOCLAW_CORPORATE_CA_B64=Y2E="],
message: "Malformed managed startup env wrapper",
},
])("fails closed for malformed or unsafe root handoff: $name", ({ argv, message }) => {
const result = runNormalizer(argv);

expect(result.status).toBe(1);
Expand Down
Loading