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
3 changes: 2 additions & 1 deletion docs/reference/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1415,7 +1415,8 @@ The tool's own stdout/stderr bytes and its exit code are left unchanged. The bre
The first interactive `$$nemoclaw <name> connect` shell also prints a one-line reminder of this denial signature and the `logs` command below.
The reminder is shown once per top-level interactive session, and only when all of these hold: an egress proxy is configured, the shell is interactive with a terminal attached to stderr, and it is a top-level shell (not a nested subshell or pane).
Suppress it with `NEMOCLAW_NO_POLICY_HINT=1`.
On OpenShell 0.0.44 or newer the reminder names your real sandbox; on older OpenShell it shows `<name>` as a placeholder — run `$$nemoclaw list` to see your sandbox names.
The reminder names the sandbox when NemoClaw receives a valid sandbox name during sandbox creation.
If no valid name is available, it shows `<name>`; run `$$nemoclaw list` to see your sandbox names.
If the reported sandbox name contains characters that are not valid in a sandbox name (uppercase letters, underscores, control characters, and similar) or exceeds 63 characters, the reminder shows the `<name>` placeholder for safety rather than echoing the untrusted value.
The reminder is intentionally proactive: the denial itself is surfaced by the OpenShell proxy, so the `curl`/`git` error text is left unchanged and the reminder points you to the logs instead.

Expand Down
106 changes: 85 additions & 21 deletions scripts/nemoclaw-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3420,6 +3420,45 @@ GATEWAYURLENVEOF
# WhatsApp reinjects it only for its gateway-backed login command.
printf "export NEMOCLAW_OPENCLAW_ALLOW_INSECURE_PRIVATE_WS='1'\n"
fi
# #7795: bake the sandbox name for the connect-shell hints below.
# OpenShell exports OPENSHELL_SANDBOX as the boolean "1" to every process it
# spawns inside the sandbox — this entrypoint included — and only its own
# root-owned PID 1 keeps the real name, which this unprivileged entrypoint
# cannot read. So the hints had no way to resolve the name and always fell
# back to the '<name>' placeholder. NEMOCLAW_SANDBOX_NAME is injected by the
# host at sandbox-create time (see buildSandboxRuntimeEnvArgs in
# src/lib/onboard/sandbox-create-launch.ts) and is the only in-container
# source of the name; capture it here for the renderer below.
#
# Apply the same RFC-1123 allowlist the renderer uses (mirrors
# NAME_VALID_PATTERN in src/lib/name-validation.ts). Missing or invalid
# values cannot reach a copyable command. An accepted value is limited to
# [a-z0-9-] and needs no further escaping.
# Evaluate the ranges in a subshell under the C locale so [a-z0-9-] stays
# ASCII and is not widened by the entrypoint's LC_COLLATE/LC_CTYPE.
local _sandbox_label_src _sandbox_label
_sandbox_label_src="${NEMOCLAW_SANDBOX_NAME:-}"
(
LC_ALL=C
_sandbox_label=""
case "$_sandbox_label_src" in
"" | 0 | 1 | true | TRUE | false | FALSE) ;;
[!a-z]* | *- | *[!a-z0-9-]*) ;;
*)
if [ "${#_sandbox_label_src}" -le 63 ]; then
_sandbox_label="$_sandbox_label_src"
fi
;;
esac
# Emit the negative case too, never nothing: the file is sourced into a
# shell the sandbox controls, so an explicit unset stops a pre-set value
# from surviving when no valid name is available.
if [ -n "$_sandbox_label" ]; then
printf "export _NEMOCLAW_SANDBOX_LABEL='%s'\n" "$_sandbox_label"
else
printf 'unset _NEMOCLAW_SANDBOX_LABEL\n'
fi
)
cat <<'GUARDENVEOF'
# nemoclaw-configure-guard begin
# #4538: a raw in-sandbox `openclaw doctor --fix` (run directly from a connect
Expand Down Expand Up @@ -3872,39 +3911,64 @@ openclaw() {
# behavior is this proactive connect-shell reminder. It does NOT make the
# denial-time curl/git/wget error itself denial-adjacent — that is intentional,
# given the source boundary above — so the tool error stays unchanged.
_nemoclaw_policy_denial_hint_label() {
# OpenShell >=0.0.44 sets OPENSHELL_SANDBOX to the sandbox name; older
# versions set the boolean "1". OPENSHELL_SANDBOX is untrusted input that is
# interpolated into a copyable `nemoclaw … logs` command, so allowlist it
# rather than merely stripping: only render it when it is a valid sandbox name.
# This mirrors NAME_VALID_PATTERN in src/lib/name-validation.ts
# (/^[a-z]([a-z0-9-]*[a-z0-9])?$/, max 63): starts with a lowercase letter,
# then lowercase alphanumerics/hyphens, no trailing hyphen. Anything else
# (digit-leading labels, control characters, ANSI escapes, shell
# metacharacters, whitespace) falls back to a placeholder the user resolves
# with `nemoclaw list`. Shell `case` globs match newlines as ordinary
# characters, so an embedded newline is rejected by the metacharacter class.
_nemoclaw_valid_sandbox_label() {
# Print $1 when it is a valid sandbox name, print nothing otherwise. Callers
# treat empty output as "unusable" and move on to the next source.
#
# The candidates are untrusted input interpolated into a copyable `nemoclaw …`
# command, so allowlist rather than merely strip: only render a value that is
# a valid sandbox name. This mirrors NAME_VALID_PATTERN in
# src/lib/name-validation.ts (/^[a-z]([a-z0-9-]*[a-z0-9])?$/, max 63): starts
# with a lowercase letter, then lowercase alphanumerics/hyphens, no trailing
# hyphen. Anything else (digit-leading labels, control characters, ANSI
# escapes, shell metacharacters, whitespace) is rejected, and the caller falls
# back to a placeholder the user resolves with `nemoclaw list`. Shell `case`
# globs match newlines as ordinary characters, so an embedded newline is
# rejected by the metacharacter class. The boolean forms are OpenShell's older
# "this is a sandbox" marker rather than a name.
#
# Evaluate the ranges under the C locale so [a-z0-9-] stays ASCII and is not
# widened by the caller's LC_COLLATE/LC_CTYPE (e.g. a locale that folds
# additional code points into [a-z]). Safe to set unconditionally: this helper
# is only ever called inside $(…) command substitution (a subshell), so the
# assignment cannot leak into the interactive shell.
LC_ALL=C
# Allowlist pattern mirrors NAME_VALID_PATTERN in src/lib/name-validation.ts
# (RFC-1123 label: /^[a-z]([a-z0-9-]*[a-z0-9])?$/, max 63). Keep them in sync.
case "${OPENSHELL_SANDBOX:-}" in
"" | 0 | 1 | true | TRUE | false | FALSE) printf '<name>' ;;
[!a-z]* | *- | *[!a-z0-9-]*) printf '<name>' ;;
case "${1:-}" in
"" | 0 | 1 | true | TRUE | false | FALSE) ;;
[!a-z]* | *- | *[!a-z0-9-]*) ;;
*)
if [ "${#OPENSHELL_SANDBOX}" -le 63 ]; then
printf '%s' "$OPENSHELL_SANDBOX"
else
printf '<name>'
if [ "${#1}" -le 63 ]; then
printf '%s' "$1"
fi
;;
esac
}
_nemoclaw_policy_denial_hint_label() {
# Render the first source that yields a valid sandbox name.
#
# OPENSHELL_SANDBOX is the runtime value. OpenShell exports it as the boolean
# "1" to sandbox processes. Keep it as the first candidate so a caller-provided
# valid sandbox name takes precedence over the generated fallback.
#
# _NEMOCLAW_SANDBOX_LABEL is the fallback that makes the hints work in the
# connect shell: the host-injected NEMOCLAW_SANDBOX_NAME, captured by the
# entrypoint when it generated this file. It is re-emitted (or explicitly
# unset) on every regeneration, so it cannot go stale, and it is allowlisted
# again here because the sandbox can reassign it after this file is sourced.
# Remove this fallback after the supported OpenShell contract supplies a
# validated sandbox name to every connect-shell process. Ref: #7795.
#
# Both call sites invoke this inside $(…) command substitution (a subshell),
# so the assignment below cannot leak into the interactive shell.
_nemoclaw_hint_label="$(_nemoclaw_valid_sandbox_label "${OPENSHELL_SANDBOX:-}")"
case "$_nemoclaw_hint_label" in
"") _nemoclaw_hint_label="$(_nemoclaw_valid_sandbox_label "${_NEMOCLAW_SANDBOX_LABEL:-}")" ;;
esac
case "$_nemoclaw_hint_label" in
"") printf '<name>' ;;
*) printf '%s' "$_nemoclaw_hint_label" ;;
esac
}
_nemoclaw_policy_denial_hint_text() {
{
printf ' Note: this sandbox restricts outbound network access by policy.\n'
Expand Down
40 changes: 40 additions & 0 deletions src/lib/onboard/sandbox-create-launch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,46 @@ describe("buildSandboxRuntimeEnvArgs", () => {
expect(omitted).toContain("NEMOCLAW_DASHBOARD_PORT=19000");
expect(omitted).toContain("NEMOCLAW_PROXY_HOST=host.docker.internal");
});

// OpenShell exports OPENSHELL_SANDBOX as the boolean "1" to sandbox processes,
// so this injection is the sandbox's only source for its own name. Without it
// the in-sandbox hints print a `<name>` placeholder instead of a copyable
// host-side command. It used to be injected only for LangChain Deep Agents
// Code.
it("injects NEMOCLAW_SANDBOX_NAME for every agent (#7795)", () => {
const base = {
chatUiUrl: "http://127.0.0.1:19000/",
manageDashboard: true,
getDashboardForwardPort: () => "19000",
hermesDashboardState: disabledHermesDashboardState,
extraPlaceholderKeys: [],
env: {} as NodeJS.ProcessEnv,
sandboxName: "my-assistant",
};

for (const agentName of ["openclaw", "hermes", "langchain-deepagents-code"]) {
const envArgs = buildSandboxRuntimeEnvArgs({
...base,
agent: { name: agentName, configPaths: { dir: "/sandbox/.openclaw" } } as any,
}).envArgs;
expect(envArgs, `${agentName} should receive the sandbox name`).toContain(
"NEMOCLAW_SANDBOX_NAME=my-assistant",
);
}
});

it("omits NEMOCLAW_SANDBOX_NAME when no sandbox name is known", () => {
const envArgs = buildSandboxRuntimeEnvArgs({
agent: { name: "openclaw", configPaths: { dir: "/sandbox/.openclaw" } } as any,
chatUiUrl: "http://127.0.0.1:19000/",
manageDashboard: true,
getDashboardForwardPort: () => "19000",
hermesDashboardState: disabledHermesDashboardState,
extraPlaceholderKeys: [],
env: {} as NodeJS.ProcessEnv,
}).envArgs;
expect(envArgs.some((arg) => arg.startsWith("NEMOCLAW_SANDBOX_NAME="))).toBe(false);
});
});

describe("prepareSandboxCreateLaunch", () => {
Expand Down
15 changes: 11 additions & 4 deletions src/lib/onboard/sandbox-create-launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,11 +151,18 @@ export function buildSandboxRuntimeEnvArgs(input: SandboxRuntimeEnvArgsInput): {
envArgs.push(formatEnvAssignment("NEMOCLAW_PROXY_PORT", sandboxProxyPort));
}

// Every sandbox needs to know its own name at runtime, not only the LangChain
// Deep Agents Code image. OpenShell exports OPENSHELL_SANDBOX as the boolean
// "1" to the processes it spawns inside the sandbox, so this injection is the
// only in-container source of the name. nemoclaw-start.sh bakes it into the
// connect-shell env so the in-sandbox hints can print a copyable host-side
// `nemoclaw <name> …` command instead of a `<name>` placeholder. (#7795)
const sandboxName = input.sandboxName;
if (sandboxName) {
envArgs.push(formatEnvAssignment("NEMOCLAW_SANDBOX_NAME", sandboxName));
}

if (agent?.name === "langchain-deepagents-code") {
const sandboxName = input.sandboxName;
if (sandboxName) {
envArgs.push(formatEnvAssignment("NEMOCLAW_SANDBOX_NAME", sandboxName));
}
envArgs.push(
formatEnvAssignment(
"NEMOCLAW_OBSERVABILITY",
Expand Down
Loading
Loading