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
66 changes: 66 additions & 0 deletions .github/workflows/nightly-e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@
# openclaw-discord-pairing-e2e
# Validates hermetic Discord pairing request approval across
# gateway and connect-shell OpenClaw state roots (#4061).
# issue-4462-scope-upgrade-approval-e2e
# Validates real CLI scope-upgrade approval and confirms
# the approved agent run stays on gateway mode (#4462).
# issue-4462-gateway-pinned-approval-characterization-e2e
# Characterizes legacy gateway-pinned scope approval
# against a real sandbox, then recovers with the fix.
# messaging-compatible-endpoint-e2e
# Validates Telegram + OpenAI-compatible endpoint inference routing
# through inference.local with a hermetic local mock (#2766).
Expand Down Expand Up @@ -90,6 +96,8 @@ on:
openclaw-tui-chat-correlation-e2e,
issue-3600-gpu-proof-optional-e2e,
openclaw-discord-pairing-e2e,
issue-4462-scope-upgrade-approval-e2e,
issue-4462-gateway-pinned-approval-characterization-e2e,
messaging-compatible-endpoint-e2e,
kimi-inference-compat-e2e,
bedrock-runtime-compatible-anthropic-e2e,
Expand Down Expand Up @@ -396,6 +404,58 @@ jobs:
secrets:
NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }}
BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }}
# ── OpenClaw Scope-Upgrade Approval E2E (#4462) ────────────────
# Positive proof: in a real sandbox, accept either a visible pending CLI
# scope upgrade or the fixed watcher's immediate approval, then confirm
# openclaw agent still uses the gateway path rather than embedded fallback.
issue-4462-scope-upgrade-approval-e2e:
if: >-
github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' ||
inputs.jobs == '' ||
contains(format(',{0},', inputs.jobs), ',issue-4462-scope-upgrade-approval-e2e,'))
uses: ./.github/workflows/e2e-script.yaml
with:
ref: ${{ inputs.target_ref || github.ref }}
script: test/e2e/test-issue-4462-scope-upgrade-approval.sh
timeout_minutes: 60
artifact_name: "issue-4462-scope-upgrade-approval-logs"
artifact_path: |
/tmp/nemoclaw-e2e-issue-4462-scope-upgrade-install.log
/tmp/nemoclaw-issue-4462-scope-upgrade-approval.log
/tmp/nemoclaw-issue-4462-scope-upgrade-agent.log
/tmp/nemoclaw-issue-4462-scope-upgrade-state.log
env_json: '{"NEMOCLAW_4462_MODE":"approval","NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_AUTO_PAIR_DEADLINE_SECS":"30","NEMOCLAW_AUTO_PAIR_FAST_DEADLINE_SECS":"3","NEMOCLAW_AUTO_PAIR_SLOW_INTERVAL_SECS":"600","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-issue-4462-scope-upgrade"}'
nvidia_api_key: true
github_token: false
secrets:
NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }}
BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }}
# ── OpenClaw Gateway-Pinned Approval Characterization (#4462) ──
# Diagnostic proof: in a real sandbox, wait for the fixed watcher to exit,
# force the legacy gateway-pinned approve path, record the observed
# OpenClaw outcome, and recover through the fixed proxy-env guard if needed.
issue-4462-gateway-pinned-approval-characterization-e2e:
if: >-
github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' ||
inputs.jobs == '' ||
contains(format(',{0},', inputs.jobs), ',issue-4462-gateway-pinned-approval-characterization-e2e,'))
uses: ./.github/workflows/e2e-script.yaml
with:
ref: ${{ inputs.target_ref || github.ref }}
script: test/e2e/test-issue-4462-scope-upgrade-approval.sh
timeout_minutes: 60
artifact_name: "issue-4462-gateway-pinned-approval-characterization-logs"
artifact_path: |
/tmp/nemoclaw-e2e-issue-4462-scope-upgrade-repro-install.log
/tmp/nemoclaw-issue-4462-scope-upgrade-repro-approval.log
/tmp/nemoclaw-issue-4462-scope-upgrade-repro-agent.log
/tmp/nemoclaw-issue-4462-scope-upgrade-repro-state.log
env_json: '{"NEMOCLAW_4462_AGENT_LOG":"/tmp/nemoclaw-issue-4462-scope-upgrade-repro-agent.log","NEMOCLAW_4462_APPROVAL_LOG":"/tmp/nemoclaw-issue-4462-scope-upgrade-repro-approval.log","NEMOCLAW_4462_AUTO_PAIR_DEADLINE_SECS":"12","NEMOCLAW_4462_AUTO_PAIR_FAST_DEADLINE_SECS":"1","NEMOCLAW_4462_AUTO_PAIR_RUN_TIMEOUT_SECS":"2","NEMOCLAW_4462_AUTO_PAIR_SLOW_INTERVAL_SECS":"1","NEMOCLAW_4462_INSTALL_LOG":"/tmp/nemoclaw-e2e-issue-4462-scope-upgrade-repro-install.log","NEMOCLAW_4462_MODE":"legacy-repro","NEMOCLAW_4462_STATE_LOG":"/tmp/nemoclaw-issue-4462-scope-upgrade-repro-state.log","NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE":"1","NEMOCLAW_NON_INTERACTIVE":"1","NEMOCLAW_RECREATE_SANDBOX":"1","NEMOCLAW_SANDBOX_NAME":"e2e-issue-4462-scope-upgrade-repro"}'
nvidia_api_key: true
github_token: false
secrets:
NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }}
BRAVE_API_KEY: ${{ secrets.BRAVE_API_KEY }}
messaging-compatible-endpoint-e2e:
if: >-
github.repository == 'NVIDIA/NemoClaw' && (github.event_name != 'workflow_dispatch' ||
Expand Down Expand Up @@ -1863,6 +1923,8 @@ jobs:
openclaw-tui-chat-correlation-e2e,
issue-3600-gpu-proof-optional-e2e,
openclaw-discord-pairing-e2e,
issue-4462-scope-upgrade-approval-e2e,
issue-4462-gateway-pinned-approval-characterization-e2e,
messaging-compatible-endpoint-e2e,
channels-add-remove-e2e,
channels-stop-start-e2e,
Expand Down Expand Up @@ -1968,6 +2030,8 @@ jobs:
openclaw-tui-chat-correlation-e2e,
issue-3600-gpu-proof-optional-e2e,
openclaw-discord-pairing-e2e,
issue-4462-scope-upgrade-approval-e2e,
issue-4462-gateway-pinned-approval-characterization-e2e,
messaging-compatible-endpoint-e2e,
channels-add-remove-e2e,
channels-stop-start-e2e,
Expand Down Expand Up @@ -2130,6 +2194,8 @@ jobs:
openclaw-tui-chat-correlation-e2e,
issue-3600-gpu-proof-optional-e2e,
openclaw-discord-pairing-e2e,
issue-4462-scope-upgrade-approval-e2e,
issue-4462-gateway-pinned-approval-characterization-e2e,
messaging-compatible-endpoint-e2e,
channels-add-remove-e2e,
channels-stop-start-e2e,
Expand Down
36 changes: 29 additions & 7 deletions scripts/nemoclaw-start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1435,14 +1435,26 @@ ALLOWED_MODES = {'webchat', 'cli'}

RUN_TIMEOUT_SECS = _env_seconds('NEMOCLAW_AUTO_PAIR_RUN_TIMEOUT_SECS', 10)

def run(*args):
# Workaround boundary (NemoClaw#4462): OpenClaw owns the gateway/device
# approval semantics. In OpenClaw 2026.5.x, a gateway-pinned
# `openclaw devices approve <scope-upgrade>` can request the upgraded scopes
# for its own connection and return the same pending-scope error it is trying
# to resolve. List calls must stay gateway-pinned so we inspect the live
# gateway, but approval calls temporarily remove OPENCLAW_GATEWAY_URL to use
# OpenClaw's local pairing fallback. Remove this when OpenClaw approve can
# complete scope upgrades through the gateway using only operator.pairing.
def run(*args, strip_gateway_url=False):
# Bound every openclaw CLI invocation so a wedged child cannot pin
# the watcher beyond DEADLINE (CodeRabbit #4292): subprocess.run with
# no timeout would hold a hung `openclaw devices list/approve` past
# the fast→slow transition and the 8h deadline check.
env = None
if strip_gateway_url:
env = os.environ.copy()
env.pop('OPENCLAW_GATEWAY_URL', None)
try:
proc = subprocess.run(
args, capture_output=True, text=True, timeout=RUN_TIMEOUT_SECS,
args, capture_output=True, text=True, timeout=RUN_TIMEOUT_SECS, env=env,
)
return proc.returncode, proc.stdout.strip(), proc.stderr.strip()
except subprocess.TimeoutExpired as exc:
Expand Down Expand Up @@ -1491,16 +1503,18 @@ while time.time() < DEADLINE:
HANDLED.add(request_id)
print(f'[auto-pair] rejected unknown client={client_id} mode={client_mode}')
continue
arc, aout, aerr = run(OPENCLAW, 'devices', 'approve', request_id, '--json')
arc, aout, aerr = run(
OPENCLAW, 'devices', 'approve', request_id, '--json', strip_gateway_url=True,
)
# rc=124 is the timeout sentinel from run() — do NOT add the
# request to HANDLED on a transient timeout, so the next poll
# can retry (CodeRabbit #4292). Permanent failures (other
# non-zero rc) still get HANDLED so we don't spin on a stuck
# bad request.
# can retry (CodeRabbit #4292). Other approve failures stay
# retryable too; only intentionally rejected unknown clients
# and confirmed successful approvals are marked handled.
if arc == 124:
continue
HANDLED.add(request_id)
if arc == 0:
HANDLED.add(request_id)
APPROVED += 1
print(f'[auto-pair] approved request={request_id} client={client_id} mode={client_mode}')
elif aout or aerr:
Expand Down Expand Up @@ -1754,6 +1768,14 @@ PROXYEOF
cat <<'GUARDENVEOF'
# nemoclaw-configure-guard begin
openclaw() {
# NemoClaw#4462: keep user-initiated device approval usable from an
# interactive sandbox shell until upstream OpenClaw can approve scope
# upgrades through the gateway without requesting the upgraded scopes for
# the approval command itself. Other commands keep OPENCLAW_GATEWAY_URL.
if [ "${1:-}" = "devices" ] && [ "${2:-}" = "approve" ]; then
( unset OPENCLAW_GATEWAY_URL; command openclaw "$@" )
return $?
fi
case "$1" in
configure)
echo "Error: 'openclaw configure' cannot modify config inside the sandbox." >&2
Expand Down
33 changes: 23 additions & 10 deletions src/lib/actions/sandbox/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -611,16 +611,24 @@ function ensureSandboxInferenceRouteOrExit(
// pass covers the case where the watcher has exited or is otherwise stuck
// when the user runs `nemoclaw <sandbox> connect`. The script sources
// `/tmp/nemoclaw-proxy-env.sh` (written by `nemoclaw-start.sh`) so the
// in-sandbox `openclaw devices list/approve` invocations target the
// running gateway with its token, and applies the same allowlist as the
// startup watcher — `openclaw-control-ui` clients plus `webchat`/`cli`
// in-sandbox `openclaw devices list` invocation targets the running gateway
// with its token. Approvals then use OpenClaw's local fallback by removing
// OPENCLAW_GATEWAY_URL only from the child env, and apply the same allowlist
// as the startup watcher — `openclaw-control-ui` clients plus `webchat`/`cli`
// modes. Unknown clients are ignored, not approved.
//
// Workaround boundary (NemoClaw#4462): OpenClaw owns device-pairing approval
// semantics. In OpenClaw 2026.5.x, a gateway-pinned `devices approve` for a
// scope-upgrade can request the upgraded scopes for its own connection and
// return the pending-scope failure it is trying to resolve. Remove this local
// fallback path when OpenClaw approve can complete scope upgrades through the
// gateway using only operator.pairing.
//
// Failure modes (timeout, sandbox-exec errors, missing openclaw, gateway
// unreachable) are swallowed: the connect flow must not be blocked by a
// best-effort approval. Internal timeouts (2s list + 1s × MAX_APPROVALS)
// fit within the outer spawnSync cap, so a partial-completion mid-loop
// kill cannot strand allowlisted requests within a normal batch.
// best-effort approval. Internal timeouts (2s list + 1s x MAX_APPROVALS
// attempts) fit within the outer spawnSync cap, so a partial-completion
// mid-loop kill cannot strand allowlisted requests within a normal batch.
const CONNECT_AUTO_PAIR_MAX_APPROVALS = 8;
const CONNECT_AUTO_PAIR_TIMEOUT_MS = 12_000;

Expand Down Expand Up @@ -660,9 +668,10 @@ pending = data.get('pending')
if not isinstance(pending, list):
sys.exit(0)
approved_count = 0
attempted_count = 0
seen_request_ids = set()
for device in pending:
if approved_count >= MAX_APPROVALS:
if attempted_count >= MAX_APPROVALS:
break
if not isinstance(device, dict):
continue
Expand All @@ -674,12 +683,16 @@ for device in pending:
if client_id not in ALLOWED_CLIENTS and client_mode not in ALLOWED_MODES:
continue
seen_request_ids.add(request_id)
approve_env = os.environ.copy()
approve_env.pop('OPENCLAW_GATEWAY_URL', None)
attempted_count += 1
try:
subprocess.run(
approve_proc = subprocess.run(
[OPENCLAW, 'devices', 'approve', request_id, '--json'],
capture_output=True, text=True, timeout=1,
capture_output=True, text=True, timeout=1, env=approve_env,
)
approved_count += 1
if approve_proc.returncode == 0:
approved_count += 1
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
continue
PYAPPROVE
Expand Down
Loading
Loading