-
Notifications
You must be signed in to change notification settings - Fork 2k
feat(cli): add gateway-token command to retrieve auth token #944
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
b6c8f6c
b926994
77477f4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,7 +22,11 @@ const YW = _useColor ? "\x1b[1;33m" : ""; | |
|
|
||
| const { ROOT, SCRIPTS, run, runCapture: _runCapture, runInteractive, shellQuote, validateName } = require("./lib/runner"); | ||
| const { resolveOpenshell } = require("./lib/resolve-openshell"); | ||
| const { startGatewayForRecovery } = require("./lib/onboard"); | ||
| const { | ||
| buildControlUiUrls, | ||
| fetchGatewayAuthTokenFromSandbox, | ||
| startGatewayForRecovery, | ||
| } = require("./lib/onboard"); | ||
| const { | ||
| ensureApiKey, | ||
| ensureGithubToken, | ||
|
|
@@ -647,6 +651,82 @@ function sandboxLogs(sandboxName, follow) { | |
| runOpenshell(args); | ||
| } | ||
|
|
||
| /** | ||
| * Wait for a TCP port to accept connections, with timeout. | ||
| */ | ||
| function waitForPort(port, timeoutMs = 10000) { | ||
| const net = require("net"); | ||
| const start = Date.now(); | ||
| return new Promise((resolve, reject) => { | ||
| function tryConnect() { | ||
| const sock = net.createConnection({ port, host: "127.0.0.1" }, () => { | ||
| sock.destroy(); | ||
| resolve(); | ||
| }); | ||
| sock.on("error", () => { | ||
| sock.destroy(); | ||
| if (Date.now() - start > timeoutMs) { | ||
| reject(new Error(`Port ${port} not ready after ${timeoutMs}ms`)); | ||
| } else { | ||
| setTimeout(tryConnect, 500); | ||
| } | ||
| }); | ||
| } | ||
| tryConnect(); | ||
| }); | ||
| } | ||
|
|
||
| async function sandboxGatewayToken(sandboxName) { | ||
| await ensureLiveSandboxOrExit(sandboxName); | ||
| const token = fetchGatewayAuthTokenFromSandbox(sandboxName); | ||
| if (!token) { | ||
| console.error(` ${_RD}Error${R}: Could not retrieve gateway auth token from sandbox '${sandboxName}'.`); | ||
| console.error(` Make sure the sandbox is running and accessible.`); | ||
| process.exit(1); | ||
| } | ||
| // Print warning to stderr before token to stdout | ||
| console.error(` ${YW}⚠️ Treat this token like a password. Do not share it or include it in logs/screencasts.${R}`); | ||
| console.log(token); | ||
| } | ||
|
|
||
| async function sandboxOpen(sandboxName) { | ||
| await ensureLiveSandboxOrExit(sandboxName); | ||
| // Start port forward and wait for it to be ready | ||
| runOpenshell(["forward", "start", "--background", "18789", sandboxName], { ignoreError: true }); | ||
| await waitForPort(18789, 10000); | ||
|
|
||
| const token = fetchGatewayAuthTokenFromSandbox(sandboxName); | ||
| if (!token) { | ||
| console.error(` ${_RD}Error${R}: Could not retrieve gateway auth token from sandbox '${sandboxName}'.`); | ||
| console.error(` Make sure the sandbox is running and accessible.`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| const urls = buildControlUiUrls(token); | ||
| const url = urls[0]; // Use the first URL (localhost) | ||
|
|
||
| // Log without token to avoid exposure in terminal history/screencasts | ||
| const safeUrl = url.replace(/#.*$/, ""); | ||
| console.log(` Opening Control UI at ${safeUrl} ...`); | ||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| // Open URL in default browser (platform-appropriate) | ||
| // Windows "start" is a cmd built-in, not an executable — requires shell: true | ||
| const isWin = process.platform === "win32"; | ||
| const opener = process.platform === "darwin" ? "open" : isWin ? "start" : "xdg-open"; | ||
| try { | ||
| const child = require("child_process").spawn(opener, isWin ? ["", url] : [url], { | ||
| stdio: "ignore", | ||
| detached: true, | ||
| shell: isWin, | ||
| }); | ||
| child.unref(); | ||
| } catch (_e) { | ||
| console.error(` ${_RD}Error${R}: Could not open browser. Please open this URL manually:`); | ||
| console.error(` ${url}`); | ||
| process.exit(1); | ||
|
Comment on lines
+712
to
+726
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: No, spawn("start", [url]) does not work on Windows because start is a cmd.exe builtin command, not a standalone executable. There is no start.exe in the PATH, so spawn (which does not use a shell by default) fails with ENOENT. To use start, you must either spawn cmd.exe explicitly with spawn("cmd.exe", ["/c", "start", "", url]) (the empty quoted string "" is needed as a window title placeholder before the URL if it has spaces), use spawn("start", [url], {shell: true}), or use exec which always spawns a shell. Command-launch failures like ENOENT are reported asynchronously via the ChildProcess error event, not thrown synchronously. The spawn function returns a ChildProcess instance immediately (even on failure to launch), and errors such as inability to find the executable are emitted on the 'error' event. Unhandled errors will crash the process with an uncaught exception, but they are not synchronous throws. Citations:
🏁 Script executed: # First, let's find and read the bin/nemoclaw.js file
fd nemoclaw.js --type fRepository: NVIDIA/NemoClaw Length of output: 74 🏁 Script executed: # Read lines 685-696 and some surrounding context to understand the function
sed -n '670,710p' bin/nemoclaw.js | cat -nRepository: NVIDIA/NemoClaw Length of output: 1777 🏁 Script executed: # Search for where this function is defined and what the 'url' parameter contains
rg -B 20 'const opener = process.platform' bin/nemoclaw.jsRepository: NVIDIA/NemoClaw Length of output: 1023 Use The code has three related issues:
🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
|
|
||
| async function sandboxPolicyAdd(sandboxName) { | ||
| const allPresets = policies.listPresets(); | ||
| const applied = policies.getAppliedPresets(sandboxName); | ||
|
|
@@ -724,6 +804,8 @@ function help() { | |
| nemoclaw <name> connect Shell into a running sandbox | ||
| nemoclaw <name> status Sandbox health + NIM status | ||
| nemoclaw <name> logs ${D}[--follow]${R} Stream sandbox logs | ||
| nemoclaw <name> open Open Control UI in browser with auth token | ||
| nemoclaw <name> gateway-token Print the gateway auth token ${D}(for scripts)${R} | ||
| nemoclaw <name> destroy Stop NIM + delete sandbox ${D}(--yes to skip prompt)${R} | ||
|
|
||
| ${G}Policy Presets:${R} | ||
|
|
@@ -800,15 +882,17 @@ const [cmd, ...args] = process.argv.slice(2); | |
| const actionArgs = args.slice(1); | ||
|
|
||
| switch (action) { | ||
| case "connect": await sandboxConnect(cmd); break; | ||
| case "status": await sandboxStatus(cmd); break; | ||
| case "logs": sandboxLogs(cmd, actionArgs.includes("--follow")); break; | ||
| case "policy-add": await sandboxPolicyAdd(cmd); break; | ||
| case "policy-list": sandboxPolicyList(cmd); break; | ||
| case "destroy": await sandboxDestroy(cmd, actionArgs); break; | ||
| case "connect": await sandboxConnect(cmd); break; | ||
| case "status": await sandboxStatus(cmd); break; | ||
| case "logs": sandboxLogs(cmd, actionArgs.includes("--follow")); break; | ||
| case "policy-add": await sandboxPolicyAdd(cmd); break; | ||
| case "policy-list": sandboxPolicyList(cmd); break; | ||
| case "gateway-token": await sandboxGatewayToken(cmd); break; | ||
| case "open": await sandboxOpen(cmd); break; | ||
| case "destroy": await sandboxDestroy(cmd, actionArgs); break; | ||
| default: | ||
| console.error(` Unknown action: ${action}`); | ||
| console.error(` Valid actions: connect, status, logs, policy-add, policy-list, destroy`); | ||
| console.error(` Valid actions: connect, status, logs, policy-add, policy-list, gateway-token, open, destroy`); | ||
| process.exit(1); | ||
| } | ||
| return; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Stop any existing Control UI forward before reusing port
18789.forward start --background 18789 ...is best-effort, andwaitForPort()only proves that something is listening on that port. If a stale OpenShell forward or another local service already owns127.0.0.1:18789, this command can open the wrong page and expose the#token=...fragment to that page’s JavaScript.bin/lib/onboard.js:1578-1582already does aforward stopfirst for this exact case.🛠️ Suggested fix
async function sandboxOpen(sandboxName) { await ensureLiveSandboxOrExit(sandboxName); // Start port forward and wait for it to be ready + runOpenshell(["forward", "stop", "18789"], { ignoreError: true }); runOpenshell(["forward", "start", "--background", "18789", sandboxName], { ignoreError: true }); await waitForPort(18789, 10000);📝 Committable suggestion
🤖 Prompt for AI Agents