Skip to content
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 thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

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