From 1b7ee8c0a13b0caa46d8001722524fb539218983 Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Sat, 8 Aug 2026 03:25:39 +0000 Subject: [PATCH 1/4] fix(sandbox): reject managed startup env wrapper without nemoclaw-start terminator Signed-off-by: Tinson Lai --- scripts/lib/entrypoint-env-wrapper.sh | 70 ++++++++++++++++----------- test/entrypoint-env-wrapper.test.ts | 27 +++++++++++ 2 files changed, 70 insertions(+), 27 deletions(-) diff --git a/scripts/lib/entrypoint-env-wrapper.sh b/scripts/lib/entrypoint-env-wrapper.sh index 0e9e90368ed..02c98830794 100755 --- a/scripts/lib/entrypoint-env-wrapper.sh +++ b/scripts/lib/entrypoint-env-wrapper.sh @@ -33,13 +33,37 @@ 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, 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 @@ -54,7 +78,11 @@ 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. A trust-carrying payload + # is rejected wherever it sits; any other managed name is a degraded handoff + # only in the leading assignment run, so a user command tail that merely + # looks like an assignment 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=*) @@ -64,6 +92,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 @@ -82,30 +120,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 diff --git a/test/entrypoint-env-wrapper.test.ts b/test/entrypoint-env-wrapper.test.ts index 5e58805d17f..dce45894f6b 100644 --- a/test/entrypoint-env-wrapper.test.ts +++ b/test/entrypoint-env-wrapper.test.ts @@ -84,6 +84,21 @@ describe("OCI entrypoint env-wrapper normalization", () => { expect(result.stdout).toContain("ARG=env\nARG=FOO=bar\nARG=printenv\nARG=FOO\n"); }); + it("keeps 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", + ); + }); + it.each([ { argv: ["env", "NODE_OPTIONS=--require=/sandbox/untrusted.cjs", "nemoclaw-start"], @@ -106,6 +121,18 @@ describe("OCI entrypoint env-wrapper normalization", () => { argv: ["env", "NEMOCLAW_CORPORATE_CA_B64=Y2E=", "not-an-assignment", "nemoclaw-start"], message: "Malformed managed startup env wrapper", }, + { + argv: ["env", "NEMOCLAW_AUTO_PAIR_FAST_REENTRY_INTERVAL_SECS=5", "/bin/sh"], + message: "Malformed managed startup env wrapper", + }, + { + argv: ["env", "NEMOCLAW_DASHBOARD_PORT=9000", "OPENCLAW_HOME=/sandbox", "/bin/sh", "-c", ":"], + message: "Malformed managed startup env wrapper", + }, + { + 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: $message", ({ argv, message }) => { const result = runNormalizer(argv); From 4ad465563977442f5bb4b34d942ad25146cba8d1 Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Sat, 8 Aug 2026 03:35:26 +0000 Subject: [PATCH 2/4] docs(sandbox): clarify env wrapper rejection scope and name its test cases Signed-off-by: Tinson Lai --- scripts/lib/entrypoint-env-wrapper.sh | 14 ++++++++------ test/entrypoint-env-wrapper.test.ts | 11 +++++++++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/scripts/lib/entrypoint-env-wrapper.sh b/scripts/lib/entrypoint-env-wrapper.sh index 02c98830794..d257b1eaf5b 100755 --- a/scripts/lib/entrypoint-env-wrapper.sh +++ b/scripts/lib/entrypoint-env-wrapper.sh @@ -62,8 +62,9 @@ nemoclaw_normalize_entrypoint_env_wrapper() { _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` carries no managed variable name, so it 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 @@ -78,10 +79,11 @@ 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. A trust-carrying payload - # is rejected wherever it sits; any other managed name is a degraded handoff - # only in the leading assignment run, so a user command tail that merely - # looks like an assignment stays a user command. + # 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 diff --git a/test/entrypoint-env-wrapper.test.ts b/test/entrypoint-env-wrapper.test.ts index dce45894f6b..c6a3015612a 100644 --- a/test/entrypoint-env-wrapper.test.ts +++ b/test/entrypoint-env-wrapper.test.ts @@ -84,7 +84,7 @@ describe("OCI entrypoint env-wrapper normalization", () => { expect(result.stdout).toContain("ARG=env\nARG=FOO=bar\nARG=printenv\nARG=FOO\n"); }); - it("keeps a user command tail that only looks like a managed assignment", () => { + it("leaves a user command tail that only looks like a managed assignment", () => { const result = runNormalizer([ "env", "FOO=bar", @@ -101,10 +101,12 @@ describe("OCI entrypoint env-wrapper normalization", () => { 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", @@ -114,26 +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", }, { + 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: $message", ({ argv, message }) => { + ])("fails closed for malformed or unsafe root handoff: $name", ({ argv, message }) => { const result = runNormalizer(argv); expect(result.status).toBe(1); From e1e47dff214228e98999a41b1cd71a22c8d43c20 Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Sun, 9 Aug 2026 04:55:34 +0000 Subject: [PATCH 3/4] docs(sandbox): correct managed startup env wrapper rejection scope Signed-off-by: Tinson Lai --- scripts/lib/entrypoint-env-wrapper.sh | 15 ++++++++------- test/entrypoint-env-wrapper.test.ts | 4 ++-- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/scripts/lib/entrypoint-env-wrapper.sh b/scripts/lib/entrypoint-env-wrapper.sh index d257b1eaf5b..f375c36dbb1 100755 --- a/scripts/lib/entrypoint-env-wrapper.sh +++ b/scripts/lib/entrypoint-env-wrapper.sh @@ -62,9 +62,10 @@ nemoclaw_normalize_entrypoint_env_wrapper() { _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` 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. + # as `env FOO=bar printenv` carries no managed name in its leading assignment + # run. This root entrypoint normalization therefore leaves it unchanged. + # NEMOCLAW_STARTUP_PROFILE_B64 and NEMOCLAW_CORPORATE_CA_B64 are rejected in + # any argument position by the terminator-missing branch below. for ((_nemoclaw_index = 1; _nemoclaw_index < ${#_nemoclaw_original_argv[@]}; _nemoclaw_index += 1)); do _nemoclaw_token="${_nemoclaw_original_argv[$_nemoclaw_index]}" case "$_nemoclaw_token" in @@ -79,11 +80,11 @@ 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. This normalization - # rejects NEMOCLAW_STARTUP_PROFILE_B64 and NEMOCLAW_CORPORATE_CA_B64 in any + # because the self-wrapper was absent or malformed. This branch 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. + # only when it appears in the leading assignment run. A sequence whose + # tail alone assigns any other managed name stays a user command. _nemoclaw_break_index="$_nemoclaw_index" for _nemoclaw_token in "${_nemoclaw_original_argv[@]:1}"; do case "$_nemoclaw_token" in diff --git a/test/entrypoint-env-wrapper.test.ts b/test/entrypoint-env-wrapper.test.ts index c6a3015612a..f06d65aef15 100644 --- a/test/entrypoint-env-wrapper.test.ts +++ b/test/entrypoint-env-wrapper.test.ts @@ -131,8 +131,8 @@ describe("OCI entrypoint env-wrapper normalization", () => { 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", ":"], + name: "rejects a managed name after an unmanaged assignment", + argv: ["env", "FOO=bar", "OPENCLAW_HOME=/sandbox", "/bin/sh", "-c", ":"], message: "Malformed managed startup env wrapper", }, { From b40665063b95fca07633b92435e625d9e8f24745 Mon Sep 17 00:00:00 2001 From: Tinson Lai Date: Sun, 9 Aug 2026 05:14:00 +0000 Subject: [PATCH 4/4] test(sandbox): add issue suffix to env wrapper boundary titles Signed-off-by: Tinson Lai --- test/entrypoint-env-wrapper.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/entrypoint-env-wrapper.test.ts b/test/entrypoint-env-wrapper.test.ts index f06d65aef15..174bff3244e 100644 --- a/test/entrypoint-env-wrapper.test.ts +++ b/test/entrypoint-env-wrapper.test.ts @@ -84,7 +84,7 @@ 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", () => { + it("leaves a user command tail that only looks like a managed assignment (#8595)", () => { const result = runNormalizer([ "env", "FOO=bar", @@ -140,7 +140,7 @@ describe("OCI entrypoint env-wrapper normalization", () => { 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 }) => { + ])("fails closed for malformed or unsafe root handoff: $name (#8595)", ({ argv, message }) => { const result = runNormalizer(argv); expect(result.status).toBe(1);