diff --git a/.github/workflows/nightly-e2e.yaml b/.github/workflows/nightly-e2e.yaml index ba807179744..be6206d0b78 100644 --- a/.github/workflows/nightly-e2e.yaml +++ b/.github/workflows/nightly-e2e.yaml @@ -4,9 +4,6 @@ # Nightly E2E tests: # # cloud-e2e Cloud inference (NVIDIA Endpoint API) on ubuntu-latest. -# cloud-experimental-e2e Experimental cloud inference test (main script skips embedded -# check-docs + final cleanup; follow-up steps run check-docs, -# skip/05-network-policy.sh, then cleanup.sh --verify with if: always()). # messaging-providers-e2e Validates messaging credential provider/placeholder/L7-proxy chain # for Telegram + Discord. Uses fake tokens. See PR #1081. # token-rotation-e2e Validates that rotating a messaging token and re-running onboard @@ -26,7 +23,7 @@ # Runs directly on the runner (not inside Docker) because OpenShell bootstraps # a K3s cluster inside a privileged Docker container — nesting would break networking. # -# NVIDIA_API_KEY for cloud-e2e and cloud-experimental-e2e: +# NVIDIA_API_KEY for cloud-e2e: # - Repository secret: Settings → Secrets and variables → Actions → Repository secrets. # - Environment secret: only available if the job sets `environment: `. # (Storing the key under Environments / NVIDIA_API_KEY without `environment:` here leaves the @@ -74,106 +71,6 @@ jobs: path: /tmp/nemoclaw-e2e-install.log if-no-files-found: ignore - cloud-experimental-e2e: - # Runs the full cloud-experimental E2E suite with docs parity isolated below. - # Landlock fix: OpenShell#810 (v0.0.32+). Docs parity: #2388 (typed registry). - if: github.repository == 'NVIDIA/NemoClaw' - runs-on: ubuntu-latest - # Main suite + check-docs + network-policy skip script can exceed 45m on cold runners. - timeout-minutes: 90 - steps: - - name: Checkout - uses: actions/checkout@v6 - - # Split Phase 5f (check-docs) and Phase 6 (cleanup) out of the main script so CI shows - # failures in dedicated steps; tear-down always runs last (if: always()). - - name: Run cloud-experimental E2E test - env: - NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} - GITHUB_TOKEN: ${{ github.token }} - # Non-interactive install (expect-driven Phase 3 optional). Runner has no expect; Phase 5e TUI skips if expect is absent. - RUN_E2E_CLOUD_EXPERIMENTAL_INTERACTIVE_INSTALL: "0" - NEMOCLAW_NON_INTERACTIVE: "1" - NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE: "1" - NEMOCLAW_RECREATE_SANDBOX: "1" - NEMOCLAW_POLICY_MODE: "custom" - NEMOCLAW_POLICY_PRESETS: "npm,pypi" - RUN_E2E_CLOUD_EXPERIMENTAL_SKIP_CHECK_DOCS: "1" - RUN_E2E_CLOUD_EXPERIMENTAL_SKIP_FINAL_CLEANUP: "1" - run: bash test/e2e/test-e2e-cloud-experimental.sh - - - name: Documentation checks (check-docs.sh) - if: always() - env: - GITHUB_TOKEN: ${{ github.token }} - run: | - set -euo pipefail - if [ -f "$HOME/.bashrc" ]; then - # shellcheck source=/dev/null - source "$HOME/.bashrc" 2>/dev/null || true - fi - export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" - if [ -s "$NVM_DIR/nvm.sh" ]; then - # shellcheck source=/dev/null - . "$NVM_DIR/nvm.sh" - fi - if [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then - export PATH="$HOME/.local/bin:$PATH" - fi - bash test/e2e/e2e-cloud-experimental/check-docs.sh - - - name: Network policy checks (skip/05-network-policy.sh) - if: always() - env: - NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} - GITHUB_TOKEN: ${{ github.token }} - SANDBOX_NAME: e2e-cloud-experimental - NEMOCLAW_SANDBOX_NAME: e2e-cloud-experimental - run: | - set -euo pipefail - if [ -f "$HOME/.bashrc" ]; then - # shellcheck source=/dev/null - source "$HOME/.bashrc" 2>/dev/null || true - fi - export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" - if [ -s "$NVM_DIR/nvm.sh" ]; then - # shellcheck source=/dev/null - . "$NVM_DIR/nvm.sh" - fi - if [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then - export PATH="$HOME/.local/bin:$PATH" - fi - bash test/e2e/e2e-cloud-experimental/skip/05-network-policy.sh - - - name: Tear down cloud-experimental sandbox (always) - if: always() - env: - SANDBOX_NAME: e2e-cloud-experimental - NEMOCLAW_SANDBOX_NAME: e2e-cloud-experimental - run: | - set -euo pipefail - if [ -f "$HOME/.bashrc" ]; then - # shellcheck source=/dev/null - source "$HOME/.bashrc" 2>/dev/null || true - fi - export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" - if [ -s "$NVM_DIR/nvm.sh" ]; then - # shellcheck source=/dev/null - . "$NVM_DIR/nvm.sh" - fi - if [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then - export PATH="$HOME/.local/bin:$PATH" - fi - bash test/e2e/e2e-cloud-experimental/cleanup.sh --verify - - - name: Upload install log on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: install-log-cloud-experimental - path: /tmp/nemoclaw-e2e-cloud-experimental-install.log - if-no-files-found: ignore - # ── Messaging Providers E2E ────────────────────────────────── # Validates the full provider/placeholder/L7-proxy chain for messaging # credentials (Telegram, Discord). Uses fake tokens by default — the L7 @@ -669,7 +566,6 @@ jobs: needs: [ cloud-e2e, - cloud-experimental-e2e, messaging-providers-e2e, token-rotation-e2e, sandbox-survival-e2e, diff --git a/Dockerfile b/Dockerfile index 414260fb681..4201b2d61cf 100644 --- a/Dockerfile +++ b/Dockerfile @@ -312,7 +312,7 @@ _allowed_ids = json.loads(base64.b64decode(os.environ.get('NEMOCLAW_MESSAGING_AL _discord_guilds = json.loads(base64.b64decode(os.environ.get('NEMOCLAW_DISCORD_GUILDS_B64', 'e30=') or 'e30=').decode('utf-8')); \ _token_keys = {'discord': 'token', 'telegram': 'botToken', 'slack': 'botToken'}; \ _env_keys = {'discord': 'DISCORD_BOT_TOKEN', 'telegram': 'TELEGRAM_BOT_TOKEN', 'slack': 'SLACK_BOT_TOKEN'}; \ -_ch_cfg = {ch: {'accounts': {'default': {_token_keys[ch]: f'openshell:resolve:env:{_env_keys[ch]}', 'enabled': True, **({'appToken': 'openshell:resolve:env:SLACK_APP_TOKEN'} if ch == 'slack' else {}), **({'proxy': proxy_url} if ch in ('telegram', 'discord') else {}), **({'groupPolicy': 'open'} if ch == 'telegram' else {}), **({'dmPolicy': 'allowlist', 'allowFrom': _allowed_ids[ch]} if ch in _allowed_ids and _allowed_ids[ch] else {})}}} for ch in msg_channels if ch in _token_keys}; \ +_ch_cfg = {ch: {'accounts': {'default': {_token_keys[ch]: f'openshell:resolve:env:{_env_keys[ch]}', 'enabled': True, 'healthMonitor': {'enabled': False}, **({'appToken': 'openshell:resolve:env:SLACK_APP_TOKEN'} if ch == 'slack' else {}), **({'proxy': proxy_url} if ch in ('telegram', 'discord') else {}), **({'groupPolicy': 'open'} if ch == 'telegram' else {}), **({'dmPolicy': 'allowlist', 'allowFrom': _allowed_ids[ch]} if ch in _allowed_ids and _allowed_ids[ch] else {})}}} for ch in msg_channels if ch in _token_keys}; \ _ch_cfg['discord'].update({'groupPolicy': 'allowlist', 'guilds': _discord_guilds}) if 'discord' in _ch_cfg and _discord_guilds else None; \ parsed = urlparse(chat_ui_url); \ chat_origin = f'{parsed.scheme}://{parsed.netloc}' if parsed.scheme and parsed.netloc else 'http://127.0.0.1:18789'; \ diff --git a/scripts/lib/sandbox-init.sh b/scripts/lib/sandbox-init.sh index fabdd5af43c..fd33b25f8d1 100755 --- a/scripts/lib/sandbox-init.sh +++ b/scripts/lib/sandbox-init.sh @@ -26,7 +26,7 @@ _SANDBOX_INIT_LOADED=1 # # File Owner Mode Writer Reader Sourced? # /tmp/nemoclaw-proxy-env.sh root 444 root sandbox YES (.bashrc/.profile) -# /tmp/gateway.log gateway 600 gateway gateway no +# /tmp/gateway.log gateway 644 gateway all no (world-readable for diagnostics) # /tmp/auto-pair.log sandbox 600 sandbox sandbox no # /tmp/.npm-cache/ sandbox 755 sandbox sandbox no (tool data) # /tmp/.cache/ sandbox 755 sandbox sandbox no (tool data) @@ -118,15 +118,26 @@ validate_tmp_permissions() { fi done - # Restricted log files — must be 600 + # Restricted log files — gateway.log may be 600 (Hermes) or 644 (OpenClaw, + # world-readable for diagnostics). auto-pair.log is 600. for f in /tmp/gateway.log /tmp/auto-pair.log; do [ -f "$f" ] || continue local perms perms="$(stat -c '%a' "$f" 2>/dev/null || stat -f '%Lp' "$f" 2>/dev/null || echo "unknown")" - if [ "$perms" != "600" ]; then - echo "[SECURITY] $f has unexpected permissions: mode=$perms (expected 600)" >&2 - failed=1 - fi + case "$f" in + */gateway.log) + if [ "$perms" != "600" ] && [ "$perms" != "644" ]; then + echo "[SECURITY] $f has unexpected permissions: mode=$perms (expected 600 or 644)" >&2 + failed=1 + fi + ;; + *) + if [ "$perms" != "600" ]; then + echo "[SECURITY] $f has unexpected permissions: mode=$perms (expected 600)" >&2 + failed=1 + fi + ;; + esac done return $failed @@ -206,7 +217,9 @@ lock_rc_files() { for rc_file in "${home_dir}/.bashrc" "${home_dir}/.profile"; do if [ -f "$rc_file" ]; then - chmod 444 "$rc_file" + if ! chmod 444 "$rc_file" 2>/dev/null; then + echo "[SECURITY] Could not lock ${rc_file} to 444 — continuing (best-effort, Landlock may enforce)" >&2 + fi fi done } diff --git a/scripts/nemoclaw-start.sh b/scripts/nemoclaw-start.sh index e527060c310..0295259cd53 100755 --- a/scripts/nemoclaw-start.sh +++ b/scripts/nemoclaw-start.sh @@ -719,20 +719,25 @@ openclaw() { GUARD for rc_file in "${_SANDBOX_HOME}/.bashrc" "${_SANDBOX_HOME}/.profile"; do - if [ -f "$rc_file" ] && grep -qF "$marker_begin" "$rc_file" 2>/dev/null; then + [ -f "$rc_file" ] || continue + # Try to write the guard snippet. All writes use || true because + # Landlock may block writes even though DAC (-w) says writable (#804). + if grep -qF "$marker_begin" "$rc_file" 2>/dev/null; then local tmp - tmp="$(mktemp)" + tmp="$(mktemp)" || continue awk -v b="$marker_begin" -v e="$marker_end" \ - '$0==b{s=1;next} $0==e{s=0;next} !s' "$rc_file" >"$tmp" + '$0==b{s=1;next} $0==e{s=0;next} !s' "$rc_file" >"$tmp" 2>/dev/null || { + rm -f "$tmp" + continue + } printf '%s\n' "$snippet" >>"$tmp" - cat "$tmp" >"$rc_file" + cat "$tmp" >"$rc_file" 2>/dev/null || true rm -f "$tmp" - elif [ -w "$rc_file" ] || [ -w "$(dirname "$rc_file")" ]; then - printf '\n%s\n' "$snippet" >>"$rc_file" + else + printf '\n%s\n' "$snippet" >>"$rc_file" 2>/dev/null || true fi done - # Final lock after all rc-file mutations are complete so Landlock - # read_only enforcement holds. + # Best-effort lock — Landlock may already enforce read-only. lock_rc_files "$_SANDBOX_HOME" } @@ -929,6 +934,77 @@ export no_proxy="$_NO_PROXY_VAL" # Dockerfile layer and hangs npm ci in k3s Docker-in-Docker. See # src/lib/sandbox-build-context.ts. A sync test enforces that the # embedded copy is byte-identical to the canonical file. +# ── Global sandbox safety net ────────────────────────────────── +# Catch-all handler for uncaught exceptions and unhandled rejections +# that would otherwise crash the gateway. In a sandbox environment, +# a crashed gateway means total loss of inference, chat, and TUI — +# worse than degraded service from a swallowed error. +# +# This MUST be the first --require preload so its handlers register +# before any library code runs. Specific guards (Slack, ciao) provide +# targeted handling; this catches everything else. +# +# Only active when OPENSHELL_SANDBOX=1 (set by OpenShell at runtime). +# Outside a sandbox, normal Node.js crash behavior is preserved. +_SANDBOX_SAFETY_NET="/tmp/nemoclaw-sandbox-safety-net.js" +emit_sandbox_sourced_file "$_SANDBOX_SAFETY_NET" <<'SAFETY_NET_EOF' +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// sandbox-safety-net.js — last-resort handler that keeps the gateway alive +// when any library throws an uncaught exception or unhandled rejection. +// Only active inside OpenShell sandboxes (OPENSHELL_SANDBOX=1). + +(function () { + 'use strict'; + if (process.env.OPENSHELL_SANDBOX !== '1') return; + + // Track whether we're inside an unhandledRejection we chose to swallow. + // OpenClaw's own handler calls process.exit(1) for non-transient rejections. + // We intercept process.exit during swallowed rejections to prevent that. + var _swallowing = false; + var _origExit = process.exit; + process.exit = function (code) { + if (_swallowing) { + try { + process.stderr.write( + '[sandbox-safety-net] blocked process.exit(' + code + + ') during swallowed rejection — gateway continues\n' + ); + } catch (_) {} + return; + } + return _origExit.call(process, code); + }; + + process.on('uncaughtException', function (err, origin) { + try { + process.stderr.write( + '[sandbox-safety-net] uncaughtException: ' + + (err && err.stack ? err.stack : String(err)) + + ' (origin: ' + origin + ') — swallowed, gateway continues\n' + ); + } catch (_) {} + }); + + process.on('unhandledRejection', function (reason, promise) { + _swallowing = true; + try { + process.stderr.write( + '[sandbox-safety-net] unhandledRejection: ' + + (reason && reason.stack ? reason.stack : String(reason)) + + ' — swallowed, gateway continues\n' + ); + } catch (_) {} + // Keep _swallowing=true through this tick so OpenClaw's handler + // (which runs in the same microtask delivery) hits our process.exit + // intercept. Reset on next tick. + Promise.resolve().then(function () { _swallowing = false; }); + }); +})(); +SAFETY_NET_EOF +export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require $_SANDBOX_SAFETY_NET" + _PROXY_FIX_SCRIPT="/tmp/nemoclaw-http-proxy-fix.js" if [ "${NODE_USE_ENV_PROXY:-}" = "1" ]; then emit_sandbox_sourced_file "$_PROXY_FIX_SCRIPT" <<'HTTP_PROXY_FIX_EOF' @@ -1153,6 +1229,72 @@ emit_sandbox_sourced_file "$_NEMOTRON_FIX_SCRIPT" <<'NEMOTRON_FIX_EOF' NEMOTRON_FIX_EOF export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require $_NEMOTRON_FIX_SCRIPT" +# mDNS / ciao network interface guard. +# The @homebridge/ciao mDNS library calls os.networkInterfaces() which +# throws a SystemError (uv_interface_addresses) inside sandboxes with +# restricted network namespaces (seccomp/Landlock). This crashes the +# gateway even though mDNS is not needed. The guard monkey-patches +# os.networkInterfaces to return an empty object on failure instead +# of throwing, and catches the uncaughtException as a fallback. +# Ref: https://github.com/NVIDIA/NemoClaw/issues/2340 +_CIAO_GUARD_SCRIPT="/tmp/nemoclaw-ciao-network-guard.js" +emit_sandbox_sourced_file "$_CIAO_GUARD_SCRIPT" <<'CIAO_GUARD_EOF' +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// ciao-network-guard.js — prevents @homebridge/ciao mDNS library from +// crashing the gateway when os.networkInterfaces() fails in restricted +// sandbox network namespaces. + +(function () { + 'use strict'; + + // Monkey-patch os.networkInterfaces to return empty on failure. + var os = require('os'); + var _origNetworkInterfaces = os.networkInterfaces; + os.networkInterfaces = function () { + try { + return _origNetworkInterfaces.call(os); + } catch (err) { + process.stderr.write( + '[guard] os.networkInterfaces() failed: ' + (err.message || err) + + ' — returning empty (mDNS disabled)\n' + ); + return {}; + } + }; + + // Fallback: catch uncaughtException from ciao if the monkey-patch + // doesn't cover all call sites. + process.on('uncaughtException', function (err, origin) { + if ( + err && err.code === 'ERR_SYSTEM_ERROR' && + String(err.message || '').indexOf('uv_interface_addresses') !== -1 + ) { + process.stderr.write( + '[guard] ciao/networkInterfaces crash caught: ' + (err.message || err) + + ' — gateway continues\n' + ); + return; + } + // Check stack for ciao/NetworkManager + if (err && err.stack && err.stack.indexOf('ciao') !== -1 && + String(err.message || '').indexOf('networkInterfaces') !== -1) { + process.stderr.write( + '[guard] ciao network error caught: ' + (err.message || err) + + ' — gateway continues\n' + ); + return; + } + // Not a ciao error — re-throw to preserve normal crash behavior. + process.stderr.write((err && err.stack) || String(err)); + process.stderr.write('\n'); + process.exit(1); + }); +})(); +CIAO_GUARD_EOF +export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--require $_CIAO_GUARD_SCRIPT" + # WebSocket CONNECT tunnel fix (NemoClaw#1570). # The `ws` library calls https.request() for wss:// WebSocket upgrades. # EnvHttpProxyAgent (NODE_USE_ENV_PROXY=1) sends a forward proxy request @@ -1195,6 +1337,8 @@ export http_proxy="$_PROXY_URL" export https_proxy="$_PROXY_URL" export no_proxy="$_NO_PROXY_VAL" PROXYEOF + # Global sandbox safety net for connect sessions — must be first. + echo "export NODE_OPTIONS=\"\${NODE_OPTIONS:+\$NODE_OPTIONS }--require $_SANDBOX_SAFETY_NET\"" # HTTP library double-proxy fix: also expose NODE_OPTIONS in connect # sessions so interactive shells and user commands started via # `openshell sandbox connect` benefit from the preload. (NemoClaw#2109) @@ -1207,6 +1351,12 @@ PROXYEOF fi # Nemotron inference fix for connect sessions. (NemoClaw#1193, #2051) echo "export NODE_OPTIONS=\"\${NODE_OPTIONS:+\$NODE_OPTIONS }--require $_NEMOTRON_FIX_SCRIPT\"" + # ciao network guard for connect sessions. + echo "export NODE_OPTIONS=\"\${NODE_OPTIONS:+\$NODE_OPTIONS }--require $_CIAO_GUARD_SCRIPT\"" + # Slack channel guard for connect sessions. The guard file is installed later + # by install_slack_channel_guard() — conditional on the file existing at + # source-time so connect sessions started before Slack is configured are safe. + echo "[ -f \"$_SLACK_GUARD_SCRIPT\" ] && export NODE_OPTIONS=\"\${NODE_OPTIONS:+\$NODE_OPTIONS }--require $_SLACK_GUARD_SCRIPT\"" # Tool cache redirects — generated from _TOOL_REDIRECTS (single source of truth) echo '# Tool cache redirects — /sandbox is Landlock read-only (#804)' for _redir in "${_TOOL_REDIRECTS[@]}"; do @@ -1332,7 +1482,7 @@ if [ "$(id -u)" -ne 0 ]; then # stream so openshell sandbox create can return once the container is ready. # TODO(#2277-P2): migrate to shared emit_restricted_log() helper touch /tmp/gateway.log - chmod 600 /tmp/gateway.log + chmod 644 /tmp/gateway.log # Separate log for auto-pair in non-root mode as well. # TODO(#2277-P2): migrate to shared emit_restricted_log() helper @@ -1343,7 +1493,7 @@ if [ "$(id -u)" -ne 0 ]; then # Pass the HTTP proxy-fix path so it is validated alongside proxy-env.sh # (both are trust-boundary files; tampering would let the sandbox user # inject code into any Node process via NODE_OPTIONS). - validate_tmp_permissions "$_PROXY_FIX_SCRIPT" "$_NEMOTRON_FIX_SCRIPT" + validate_tmp_permissions "$_SANDBOX_SAFETY_NET" "$_PROXY_FIX_SCRIPT" "$_NEMOTRON_FIX_SCRIPT" "$_CIAO_GUARD_SCRIPT" "$_SLACK_GUARD_SCRIPT" # Start gateway in background, auto-pair, then wait. # Pass OPENCLAW_GATEWAY_TOKEN only on this launch line so it lives solely @@ -1392,11 +1542,12 @@ if [ ${#NEMOCLAW_CMD[@]} -gt 0 ]; then exec gosu sandbox "${NEMOCLAW_CMD[@]}" fi -# SECURITY: Protect gateway log from sandbox user tampering +# Gateway log: owned by gateway user, world-readable for diagnostics. +# The sandbox user can read but not truncate/overwrite (not owner, sticky /tmp). # TODO(#2277-P2): migrate to shared emit_restricted_log() helper touch /tmp/gateway.log chown gateway:gateway /tmp/gateway.log -chmod 600 /tmp/gateway.log +chmod 644 /tmp/gateway.log # Separate log for auto-pair so sandbox user can write to it # TODO(#2277-P2): migrate to shared emit_restricted_log() helper @@ -1483,7 +1634,7 @@ harden_openclaw_symlinks # Pass the HTTP proxy-fix path so it is validated alongside proxy-env.sh # (both are trust-boundary files; tampering would let the sandbox user # inject code into any Node process via NODE_OPTIONS). -validate_tmp_permissions "$_PROXY_FIX_SCRIPT" "$_NEMOTRON_FIX_SCRIPT" +validate_tmp_permissions "$_SANDBOX_SAFETY_NET" "$_PROXY_FIX_SCRIPT" "$_NEMOTRON_FIX_SCRIPT" "$_CIAO_GUARD_SCRIPT" "$_SLACK_GUARD_SCRIPT" # Start the gateway as the 'gateway' user. # SECURITY: The sandbox user cannot kill this process because it runs diff --git a/test/e2e/test-messaging-providers.sh b/test/e2e/test-messaging-providers.sh index 6ef798cfe35..1b6197cf5b1 100755 --- a/test/e2e/test-messaging-providers.sh +++ b/test/e2e/test-messaging-providers.sh @@ -661,19 +661,11 @@ print('yes' if 'slack' in d else 'no') pass "M11e: Slack channel configured with placeholder tokens (guard needed)" # Diagnostics: check if the guard was installed and what NODE_OPTIONS looks like - info "Checking guard installation diagnostics (via openshell exec as root):" + info "Checking guard installation diagnostics:" guard_exists=$(openshell sandbox exec --name "$SANDBOX_NAME" -- ls -la /tmp/nemoclaw-slack-channel-guard.js 2>/dev/null || echo "EXEC_FAILED") info " Guard file: $guard_exists" node_opts=$(openshell sandbox exec --name "$SANDBOX_NAME" -- bash -c 'echo "$NODE_OPTIONS"' 2>/dev/null || echo "EXEC_FAILED") info " NODE_OPTIONS: $node_opts" - proxy_fix=$(openshell sandbox exec --name "$SANDBOX_NAME" -- ls -la /tmp/nemoclaw-http-proxy-fix.js 2>/dev/null || echo "EXEC_FAILED") - info " Proxy fix file: $proxy_fix" - # Check what processes are running - procs=$(openshell sandbox exec --name "$SANDBOX_NAME" -- ps aux 2>/dev/null | head -10 || echo "EXEC_FAILED") - info " Processes:" - echo "$procs" | while IFS= read -r line; do - info " $line" - done else skip "M11e: No Slack channel in config" fi diff --git a/test/http-proxy-fix-sync.test.ts b/test/http-proxy-fix-sync.test.ts index db2f0dea210..cf9a37bc534 100644 --- a/test/http-proxy-fix-sync.test.ts +++ b/test/http-proxy-fix-sync.test.ts @@ -66,7 +66,7 @@ describe("http-proxy-fix heredoc sync (#2109)", () => { it("validate_tmp_permissions is invoked with the fix path in both root and non-root branches", () => { const startScript = fs.readFileSync(START_SCRIPT, "utf-8"); - const calls = startScript.match(/validate_tmp_permissions "\$_PROXY_FIX_SCRIPT"/g) || []; + const calls = startScript.match(/validate_tmp_permissions\s+.*"\$_PROXY_FIX_SCRIPT"/g) || []; expect(calls.length).toBeGreaterThanOrEqual(2); }); diff --git a/test/nemotron-inference-fix.test.ts b/test/nemotron-inference-fix.test.ts index 5a87eb041ae..29bb914201d 100644 --- a/test/nemotron-inference-fix.test.ts +++ b/test/nemotron-inference-fix.test.ts @@ -34,8 +34,7 @@ describe("Nemotron inference fix preload (#1193, #2051)", () => { }); it("passes the preload path to validate_tmp_permissions in both root and non-root branches", () => { - const calls = - src.match(/validate_tmp_permissions\s+"[^"]*"\s+"\$_NEMOTRON_FIX_SCRIPT"/g) || []; + const calls = src.match(/validate_tmp_permissions\s+.*"\$_NEMOTRON_FIX_SCRIPT"/g) || []; expect(calls.length).toBeGreaterThanOrEqual(2); });