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
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ $ nemoclaw my-assistant snapshot restore 2026-04-21T07-35-55-987Z
$ nemoclaw my-assistant snapshot restore v3 --to my-assistant-clone
```

### `openshell term`
## `openshell term`

Open the OpenShell TUI to monitor sandbox activity and approve network egress requests.
Run this on the host where the sandbox is running.
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ $ nemoclaw my-assistant snapshot restore 2026-04-21T07-35-55-987Z
$ nemoclaw my-assistant snapshot restore v3 --to my-assistant-clone
```

### `openshell term`
## `openshell term`

Open the OpenShell TUI to monitor sandbox activity and approve network egress requests.
Run this on the host where the sandbox is running.
Expand Down
45 changes: 31 additions & 14 deletions nemoclaw-blueprint/scripts/ws-proxy-fix.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ const _PATCHED = Symbol.for("nemoclaw.wsProxyFix");
const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy;
if (!proxyUrl)
return;
if (globalThis[_PATCHED])
const patchedFlag = Reflect.get(globalThis, _PATCHED) === true;
if (patchedFlag)
return;
let proxy;
try {
Expand All @@ -68,7 +69,7 @@ const _PATCHED = Symbol.for("nemoclaw.wsProxyFix");
// Override createConnection to route through the proxy's CONNECT tunnel.
// The typing is intentionally loosened because the actual Node.js runtime
// signature is broader than what @types/node declares.
agent.createConnection = function (options, callback) {
Reflect.set(agent, "createConnection", function (options, callback) {
const connectReq = node_http_1.default.request({
host: proxyHost,
port: proxyPort,
Expand All @@ -88,7 +89,7 @@ const _PATCHED = Symbol.for("nemoclaw.wsProxyFix");
}
const tlsSocket = node_tls_1.default.connect({
socket,
servername: options.servername || targetHost,
servername: typeof options.servername === "string" ? options.servername : targetHost,
});
callback(null, tlsSocket);
});
Expand All @@ -100,7 +101,7 @@ const _PATCHED = Symbol.for("nemoclaw.wsProxyFix");
// createConnection expects a synchronous return; the real socket arrives
// via the callback. Return a placeholder that Node.js will discard.
return new node_net_1.default.Socket();
};
});
return agent;
}
// ---------- Target check -------------------------------------------------
Expand All @@ -123,9 +124,23 @@ const _PATCHED = Symbol.for("nemoclaw.wsProxyFix");
return false;
}
// ---------- Patch https.request() ---------------------------------------
// Capture the original — typed as a loose callable so we can invoke it
// with the normalised (options, cb) form without fighting overload resolution.
const origRequest = node_https_1.default.request;
// Capture the original so we can call it after normalising arguments.
const requestRef = node_https_1.default.request;
function callOriginalRequest(input, options, callback) {
if (typeof input === "string" || input instanceof node_url_1.URL) {
if (typeof options === "function") {
return requestRef(input, options);
}
if (options) {
return callback ? requestRef(input, options, callback) : requestRef(input, options);
}
return callback ? requestRef(input, {}, callback) : requestRef(input);
}
if (typeof options === "function") {
return requestRef(input, options);
}
return requestRef(input, callback);
}
function wsProxyFixedRequest(input, options, callback) {
// --- Normalise arguments (Node.js accepts multiple call signatures) ---
let opts;
Expand All @@ -136,7 +151,7 @@ const _PATCHED = Symbol.for("nemoclaw.wsProxyFix");
opts = {};
}
else {
opts = options || {};
opts = options ?? {};
cb = callback;
}
const url = typeof input === "string" ? new node_url_1.URL(input) : input;
Expand All @@ -159,19 +174,21 @@ const _PATCHED = Symbol.for("nemoclaw.wsProxyFix");
host = opts.host.replace(/:\d+$/, "");
}
if (isDiscordWsUpgrade(host, opts.headers)) {
if (!host) {
return callOriginalRequest(input, options, callback);
}
// Discord WebSocket upgrade — inject CONNECT tunnel agent unless the
// caller already provides a custom (non-default) agent.
if (!opts.agent || opts.agent === node_https_1.default.globalAgent) {
const port = parseInt(String(opts.port), 10) || 443;
opts = { ...opts, agent: createTunnelAgent(host, port) };
}
return origRequest.call(node_https_1.default, opts, cb);
return cb ? requestRef(opts, cb) : requestRef(opts);
}
// Non-WebSocket — pass through original arguments unchanged.
// eslint-disable-next-line prefer-rest-params
return origRequest.apply(node_https_1.default, arguments);
// Non-WebSocket — pass through the original arguments unchanged.
return callOriginalRequest(input, options, callback);
}
// Replace https.request with our patched version.
node_https_1.default.request = wsProxyFixedRequest;
globalThis[_PATCHED] = true;
Reflect.set(node_https_1.default, "request", wsProxyFixedRequest);
Reflect.set(globalThis, _PATCHED, true);
})();
64 changes: 44 additions & 20 deletions nemoclaw-blueprint/scripts/ws-proxy-fix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { URL } from "node:url";
const _PATCHED = Symbol.for("nemoclaw.wsProxyFix");

type RequestCallback = (res: http.IncomingMessage) => void;
type TunnelConnectionOptions = { servername?: string };

/**
* Merged options after normalising the multiple call signatures of
Expand All @@ -54,7 +55,8 @@ interface ReqOpts extends https.RequestOptions {
const proxyUrl = process.env.HTTPS_PROXY || process.env.https_proxy;
if (!proxyUrl) return;

if ((globalThis as Record<symbol, unknown>)[_PATCHED]) return;
const patchedFlag = Reflect.get(globalThis, _PATCHED) === true;
if (patchedFlag) return;

let proxy: URL;
try {
Expand Down Expand Up @@ -82,10 +84,13 @@ interface ReqOpts extends https.RequestOptions {
// Override createConnection to route through the proxy's CONNECT tunnel.
// The typing is intentionally loosened because the actual Node.js runtime
// signature is broader than what @types/node declares.
(agent as unknown as Record<string, unknown>).createConnection = function (
options: Record<string, unknown>,
callback: (err: Error | null, socket?: tls.TLSSocket) => void,
): net.Socket {
Reflect.set(
agent,
"createConnection",
function (
options: TunnelConnectionOptions,
callback: (err: Error | null, socket?: tls.TLSSocket) => void,
): net.Socket {
const connectReq = http.request({
host: proxyHost,
port: proxyPort,
Expand All @@ -112,7 +117,7 @@ interface ReqOpts extends https.RequestOptions {
}
const tlsSocket = tls.connect({
socket,
servername: (options.servername as string) || targetHost,
servername: typeof options.servername === "string" ? options.servername : targetHost,
});
callback(null, tlsSocket);
},
Expand All @@ -127,7 +132,8 @@ interface ReqOpts extends https.RequestOptions {
// createConnection expects a synchronous return; the real socket arrives
// via the callback. Return a placeholder that Node.js will discard.
return new net.Socket();
};
},
);

return agent;
}
Expand Down Expand Up @@ -158,12 +164,28 @@ interface ReqOpts extends https.RequestOptions {

// ---------- Patch https.request() ---------------------------------------

// Capture the original — typed as a loose callable so we can invoke it
// with the normalised (options, cb) form without fighting overload resolution.
const origRequest = https.request as (
options: ReqOpts,
// Capture the original so we can call it after normalising arguments.
const requestRef = https.request;

function callOriginalRequest(
input: string | URL | ReqOpts,
options?: RequestCallback | ReqOpts,
callback?: RequestCallback,
) => http.ClientRequest;
): http.ClientRequest {
if (typeof input === "string" || input instanceof URL) {
if (typeof options === "function") {
return requestRef(input, options);
}
if (options) {
return callback ? requestRef(input, options, callback) : requestRef(input, options);
}
return callback ? requestRef(input, {}, callback) : requestRef(input);
}
if (typeof options === "function") {
return requestRef(input, options);
}
return requestRef(input, callback);
}

function wsProxyFixedRequest(
input: string | URL | ReqOpts,
Expand All @@ -179,7 +201,7 @@ interface ReqOpts extends https.RequestOptions {
cb = options;
opts = {};
} else {
opts = (options as ReqOpts) || {};
opts = options ?? {};
cb = callback;
}
const url = typeof input === "string" ? new URL(input) : input;
Expand All @@ -202,24 +224,26 @@ interface ReqOpts extends https.RequestOptions {
host = opts.host.replace(/:\d+$/, "");
}
if (isDiscordWsUpgrade(host, opts.headers)) {
if (!host) {
return callOriginalRequest(input, options, callback);
}
// Discord WebSocket upgrade — inject CONNECT tunnel agent unless the
// caller already provides a custom (non-default) agent.
if (!opts.agent || opts.agent === https.globalAgent) {
const port = parseInt(String(opts.port), 10) || 443;
opts = { ...opts, agent: createTunnelAgent(host!, port) };
opts = { ...opts, agent: createTunnelAgent(host, port) };
}
return origRequest.call(https, opts, cb);
return cb ? requestRef(opts, cb) : requestRef(opts);
}

// Non-WebSocket — pass through original arguments unchanged.
// eslint-disable-next-line prefer-rest-params
return (origRequest as unknown as Function).apply(https, arguments);
// Non-WebSocket — pass through the original arguments unchanged.
return callOriginalRequest(input, options, callback);
}

// Replace https.request with our patched version.
(https as unknown as Record<string, unknown>).request = wsProxyFixedRequest;
Reflect.set(https, "request", wsProxyFixedRequest);

(globalThis as Record<symbol, unknown>)[_PATCHED] = true;
Reflect.set(globalThis, _PATCHED, true);
})();

export {};
Loading
Loading