Skip to content
Closed
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
25 changes: 25 additions & 0 deletions docs/reference/troubleshooting.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,31 @@ If they are missing on an older sandbox, upgrade NemoClaw and run:
$$nemoclaw <name> rebuild
```

### A request inside the sandbox fails with `CONNECT tunnel failed, response 403`

Sandbox outbound network access is denied by default and enforced by the OpenShell proxy.
When a request targets a host that no applied policy preset allows, the proxy refuses the tunnel and tools surface only the protocol-level error:

```text
fatal: unable to access 'https://example.com/foo/bar/': CONNECT tunnel failed, response 403
curl: (56) CONNECT tunnel failed, response 403
```

This is a network-policy denial, not a tool or certificate problem.
The first interactive `$$nemoclaw <name> connect` shell prints a one-line reminder of this; suppress it with `NEMOCLAW_NO_POLICY_HINT=1`.

To see which rule denied the request, read the merged logs from the host:

```bash
$$nemoclaw <name> logs --tail 50
```

If the host should be reachable, allow it with a preset or a [custom preset](../network-policy/customize-network-policy#custom-preset-files):

```bash
$$nemoclaw <name> policy-add <preset>
```

### Sandbox creation reports a TLS certificate mismatch

If sandbox creation reports a TLS or certificate mismatch, the OpenShell gateway certificate may have changed since the CLI last registered it.
Expand Down
63 changes: 63 additions & 0 deletions scripts/nemoclaw-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3031,6 +3031,69 @@ PYAPPROVEAFTER
return "$_nemoclaw_oc_status"
}
# nemoclaw-configure-guard end
# nemoclaw-policy-denial-hint begin
# #5978: outbound network is denied-by-default and enforced by the OpenShell L7
# proxy. From inside the sandbox, generic CLIs (curl, git, wget, python, …) see
# a policy denial only as the opaque protocol error
# "CONNECT tunnel failed, response 403" — with no pointer to the detailed
# allow/deny reason, which lives in the NemoClaw logs. Surface a one-line
# breadcrumb when a human first lands in an interactive connect shell so a later
# 403 is recognisable and actionable.
#
# This deliberately does NOT wrap or alter curl/git/wget: wrapping them to scan
# stderr turns their stderr into a pipe, which makes the tools treat it as a
# non-TTY and silently drop progress meters and colour — a worse regression than
# the missing breadcrumb. The hint is therefore tool-agnostic informational
# output that leaves every tool's stdout/stderr/TTY behaviour and exit code
# byte-for-byte unchanged, and covers every connect path that sources this file.
# Shown once per top-level interactive TTY session; suppress with
# NEMOCLAW_NO_POLICY_HINT=1.
_nemoclaw_policy_denial_hint_label() {
# OpenShell >=0.0.44 sets OPENSHELL_SANDBOX to the sandbox name; older
# versions set the boolean "1". Use the name when it looks like one, else a
# placeholder the user resolves with `nemoclaw list`.
case "${OPENSHELL_SANDBOX:-}" in
"" | 0 | 1 | true | TRUE | false | FALSE) printf '<name>' ;;
*)
# OPENSHELL_SANDBOX is untrusted input printed into a TTY: strip control
# characters (newlines, ESC, …) so a crafted sandbox name cannot spoof
# output or inject terminal escape sequences. Fall back to the placeholder
# if nothing printable remains.
_nemoclaw_policy_hint_label="$(printf '%s' "$OPENSHELL_SANDBOX" | LC_ALL=C tr -d '[:cntrl:]')"
[ -n "$_nemoclaw_policy_hint_label" ] || _nemoclaw_policy_hint_label='<name>'
printf '%s' "$_nemoclaw_policy_hint_label"
;;
esac
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
_nemoclaw_policy_denial_hint_text() {
{
printf ' Note: this sandbox restricts outbound network access by policy.\n'
printf " Blocked requests fail with 'CONNECT tunnel failed, response 403'.\n"
printf ' See which rule denied a request: nemoclaw %s logs --tail 50\n' \
"$(_nemoclaw_policy_denial_hint_label)"
} >&2
}
_nemoclaw_maybe_policy_denial_hint() {
# Once per shell process: a login shell can source this file through more than
# one system-wide hook (the login-profile hook and the interactive-bash hook;
# #2704), so guard against printing twice. Not exported, so it neither leaks
# into child processes nor suppresses sibling connect sessions.
[ -n "${_NEMOCLAW_POLICY_HINT_SHOWN:-}" ] && return 0
# Suppressed by the user.
case "${NEMOCLAW_NO_POLICY_HINT:-}" in 1 | true | TRUE | yes | YES) return 0 ;; esac
# Interactive human shells only — never automation (`bash -c`, scripts).
case $- in *i*) ;; *) return 0 ;; esac
# Real terminal on stderr (where the hint is written).
[ -t 2 ] || return 0
# Top-level connect shell only — don't repeat in every subshell/pane.
[ "${SHLVL:-1}" -le 1 ] || return 0
# Nothing is proxied (no egress restriction) ⇒ nothing to explain.
[ -n "${HTTPS_PROXY:-${https_proxy:-}}" ] || return 0
_NEMOCLAW_POLICY_HINT_SHOWN=1
_nemoclaw_policy_denial_hint_text
}
_nemoclaw_maybe_policy_denial_hint
# nemoclaw-policy-denial-hint end
GUARDENVEOF
# Global sandbox safety net for connect sessions — must be first.
echo "export NODE_OPTIONS=\"\${NODE_OPTIONS:+\$NODE_OPTIONS }--require $_SANDBOX_SAFETY_NET\""
Expand Down
23 changes: 23 additions & 0 deletions src/lib/actions/sandbox/connect-policy-hint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from "vitest";
import { buildPolicyDenialConnectHint } from "./connect";

// Host-side companion to the in-sandbox breadcrumb (#5978): the `nemoclaw
// <name> connect` flow knows the real sandbox name, so the hint is a directly
// runnable command (unlike the in-sandbox stanza, which falls back to `<name>`
// on OpenShell builds that set OPENSHELL_SANDBOX=1).
describe("policy-denial connect hint (#5978)", () => {
it("names the real sandbox in a runnable logs command", () => {
const hint = buildPolicyDenialConnectHint("qa-5978");
expect(hint).toContain("nemoclaw qa-5978 logs --tail 50");
expect(hint).not.toContain("<name>");
});

it("references the policy-denial signature so a later 403 is recognisable", () => {
expect(buildPolicyDenialConnectHint("qa-5978")).toContain(
"CONNECT tunnel failed, response 403",
);
});
});
18 changes: 18 additions & 0 deletions src/lib/actions/sandbox/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,21 @@ export type SandboxConnectOptions = {
probeOnly?: boolean;
};

// Host-side companion to the in-sandbox policy-denial breadcrumb (#5978). The
// in-sandbox hint can only print a `<name>` placeholder on OpenShell builds that
// set OPENSHELL_SANDBOX=1 (not the sandbox name); here on the host we know the
// real name, so the reporter's `connect` flow gets a directly runnable command.
export function buildPolicyDenialConnectHint(sandboxName: string): string {
return (
"If a request is blocked by network policy ('CONNECT tunnel failed, response 403'), " +
`see why with \`${CLI_NAME} ${sandboxName} logs --tail 50\`.`
);
}

function isPolicyDenialHintSuppressed(env: NodeJS.ProcessEnv = process.env): boolean {
return ["1", "true"].includes(String(env.NEMOCLAW_NO_POLICY_HINT || ""));
}

type SpawnLikeResult = {
status: number | null;
signal?: NodeJS.Signals | null;
Expand Down Expand Up @@ -1074,6 +1089,9 @@ export async function connectSandbox(
console.log(
` ${D}Type \`/exit\` to leave the chat, then \`exit\` to return to the host shell.${R}`,
);
if (!isPolicyDenialHintSuppressed()) {
console.log(` ${D}${buildPolicyDenialConnectHint(sandboxName)}${R}`);
}
console.log("");
}
const result = spawnSync(getOpenshellBinary(), ["sandbox", "connect", sandboxName], {
Expand Down
Loading
Loading