fix(sandbox): wrap fetch() to route HTTPS through EnvHttpProxyAgent - #3
Merged
Merged
Conversation
Follow-up to NVIDIA#2344 (the http.request wrapper for NVIDIA#2109). PR NVIDIA#2344 covers axios / follow-redirects / proxy-from-env, all of which flow through Node's http.request and EnvHttpProxyAgent. This change covers an additional code path the Microsoft Teams adapter uses: native fetch() with a custom undici dispatcher. When OpenClaw's Teams adapter handles a channel @mention it calls the Microsoft Graph API via: fetch('https://graph.microsoft.com/v1.0/teams/.../messages/...', { dispatcher: customUndiciAgent }) The custom dispatcher routes the request through its own connection pool, bypassing the default EnvHttpProxyAgent — which is the only thing routing HTTPS through the OpenShell L7 proxy. Direct egress to graph.microsoft.com is blocked by the sandbox network namespace and surfaces as ECONNREFUSED. Channel @mentions silently fail; DMs work because the webhook payload carries the message body and no Graph call is needed. Wrap globalThis.fetch in the same preload that wraps http.request. When fetch() is called with a custom dispatcher and an HTTPS URL, strip the dispatcher and let EnvHttpProxyAgent handle the request. Non-HTTPS URLs and dispatcher-free calls pass through unchanged so intra-sandbox HTTP traffic and any non-proxy use of the dispatcher option keep working. Refinements over the originally proposed fix: - URL extraction handles all three fetch() input forms — string, URL object (.href), Request object (.url) — in that order. A naive url?.url check would miss URL instances (which have .href, not .url) and silently leave the dispatcher attached, defeating the fix on callers that pass URL instances. - One-shot console.warn so the strip is auditable in logs without spamming on every call. The flag is closure-scoped so a process restart re-arms it. - Defensive typeof origFetch === function guard so a Node runtime that ever ships without globalThis.fetch silently no-ops instead of throwing at preload time. After NVIDIA#3109 the preload is delivered as a standalone module copied from /usr/local/lib/nemoclaw/preloads/ at boot, so this PR only touches the canonical http-proxy-fix.js — no heredoc to keep in sync. http-proxy-fix-sync.test.ts already runs the entrypoint block end-to- end and reads the generated file; extended with explicit assertions for http.request and globalThis.fetch wrapper presence so a future deletion of either wrapper trips the test. Verified end-to-end on a real proxy-enabled sandbox: bot replies to Teams channel @mentions; DM regression check passes; non-Graph fetch() and fetch() without a custom dispatcher unaffected.
lcsmontiel
marked this pull request as ready for review
May 19, 2026 03:19
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
msteams debounce flush failed: fetch failedfollowed byECONNREFUSEDagainstgraph.microsoft.comwithagent=custom.fetch()with a custom undicidispatcher. The custom dispatcher creates its own connection pool, bypassing Node's defaultEnvHttpProxyAgent— which is the only thing routing HTTPS through the OpenShell L7 proxy. Direct egress is blocked by the sandbox netns.http.requestrewrite for axios requests fail with ERR_BAD_RESPONSE inside NemoClaw sandbox — double proxy conflict with NODE_USE_ENV_PROXY NVIDIA/NemoClaw#2109). fix(sandbox): rewrite #2109 proxy fix as http.request wrapper (signed) NVIDIA/NemoClaw#2344 coversaxios/follow-redirects/proxy-from-env; this coversfetch() + custom dispatcher. Same preload file, same delivery path (via/usr/local/lib/nemoclaw/preloads/http-proxy-fix.jsafter the refactor(runtime): extract entrypoint preload modules NVIDIA/NemoClaw#3109 refactor).Reproduction
@mentionthe bot.DMs continue to work because the webhook payload carries the message body — no Graph API call is needed.
The fix
Add a
globalThis.fetchwrapper alongside the existinghttp.requestwrapper innemoclaw-blueprint/scripts/http-proxy-fix.js. Whenfetch()is called with a custom dispatcher and an HTTPS URL, strip the dispatcher and let the defaultEnvHttpProxyAgenthandle the request through the proxy.Non-HTTPS URLs and dispatcher-free
fetch()calls pass through unchanged — direct intra-sandbox HTTP traffic and any non-proxy use of the dispatcher option keep working.Design notes
fetch()input forms — string,URLobject (.href),Requestobject (.url) — in that order. A naiveurl?.urlwould missURLinstances (which have.href, not.url) and silently leave the dispatcher attached, defeating the fix.console.warnso the strip is auditable in logs without spamming on every call. The Teams adapter polls Graph frequently under load — per-call logs would flood. The warned flag is closure-scoped so a process restart re-arms it.typeof origFetch === 'function'guard so a Node runtime that ever ships withoutglobalThis.fetch(or a future embedding context that strips it) silently no-ops instead of throwing at preload time./usr/local/lib/nemoclaw/preloads/. This PR only touches the canonicalhttp-proxy-fix.js— no heredoc to keep in sync. The existinghttp-proxy-fix-sync.test.tsend-to-end test (which extracts the entrypoint block, runs it, and reads the generated/tmp/...file) is extended with explicit assertions that both thehttp.requestwrapper and theglobalThis.fetchwrapper are present in the generated preload, so a future accidental deletion of either trips CI.Why not patch undici / replace the dispatcher with a proxy-aware one?
Module._loadhook in fix(proxy): resolve axios + NODE_USE_ENV_PROXY double-proxy conflict NVIDIA/NemoClaw#2110 that fix(sandbox): rewrite #2109 proxy fix as http.request wrapper (signed) NVIDIA/NemoClaw#2344 superseded.EnvHttpProxyAgentroute is the smallest correct change.console.warn, so if a caller's dispatcher carried meaningful behavior other than direct routing, that's discoverable in logs.Relationship to NVIDIA#2344 / NVIDIA#2296 / NVIDIA#2109
http.request()(axios, follow-redirects, proxy-from-env)https.request()Upgrade: websocket (Discord)EnvHttpProxyAgentpicks FORWARD instead of CONNECT for WSws-proxy-fix.js)fetch()+ custom dispatcher (Teams Graph)EnvHttpProxyAgententirelySame root cause family (HTTP code path that doesn't respect
NODE_USE_ENV_PROXY), three different surface code paths, three orthogonal fixes.Test plan
npx vitest run --project cli test/http-proxy-fix-sync.test.ts— 1/1 pass (extracts entrypoint block, runs it, reads the generated preload; now asserts bothhttp.requestandglobalThis.fetchwrappers +delete newOpts.dispatcher)npx vitest run --project cli test/service-env.test.ts— passes (no regression in the persist block tests)npm run typecheck:cli— cleanshellcheck scripts/nemoclaw-start.sh— clean (unchanged in this PR)fetch()calls andfetch()calls without a custom dispatcher confirmed unaffected.Closes the channel-message gap left by NVIDIA#2344.