Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
a3504c8
feat(cli): add connection info command to reprint the connection block
laitingsheng Jul 24, 2026
12d688d
refactor(cli): keep onboard entrypoint net-neutral for connection info
laitingsheng Jul 25, 2026
71e2076
Merge remote-tracking branch 'origin/main' into feat/connection-info-…
laitingsheng Jul 25, 2026
4ceac20
test(cli): behaviour-oriented issue-linked titles for connection info
laitingsheng Jul 25, 2026
5101fbe
Merge remote-tracking branch 'origin/main' into feat/connection-info-…
laitingsheng Jul 25, 2026
f0f2887
merge: resolve conflicts with main
github-actions[bot] Jul 26, 2026
39c9fec
Merge remote-tracking branch 'origin/main' into feat/connection-info-…
sandl99 Jul 28, 2026
3d98c44
feat(cli): enrich dashboard-url connection guidance
sandl99 Jul 28, 2026
67974cd
Merge remote-tracking branch 'origin/main' into feat/connection-info-…
sandl99 Jul 28, 2026
c578254
Merge branch 'main' into feat/connection-info-command
sandl99 Jul 28, 2026
1e17215
test(cli): isolate dashboard-url runtime state
sandl99 Jul 28, 2026
bf705fb
Merge remote-tracking branch 'origin/feat/connection-info-command' in…
sandl99 Jul 28, 2026
f56df0a
merge: refresh dashboard guidance with main
prekshivyas Jul 28, 2026
d1e8240
fix(cli): use invoked binary in dashboard guidance
prekshivyas Jul 28, 2026
5d9a241
test(cli): cover invoked dashboard binary
prekshivyas Jul 28, 2026
df9fc8d
merge: refresh dashboard guidance with main
prekshivyas Jul 28, 2026
21af1a6
merge: refresh dashboard guidance with main
laitingsheng Jul 30, 2026
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
21 changes: 21 additions & 0 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -1512,6 +1512,27 @@ NemoClaw's `--tail <lines>` flag is a line-count flag; the lower-level `openshel
$$nemoclaw my-assistant logs [--follow] [--tail <lines>|-n <lines>] [--since <duration>]
```

### `$$nemoclaw <name> connection info`

Reprint the connection block shown at the end of onboarding for a running sandbox.
The output repeats what onboarding printed for this sandbox.
For OpenClaw and other dashboard agents it includes the browser dashboard URL, the terminal connect command, and the management commands.
For terminal-runtime agents it shows the connect and run commands.
Use this when the onboarding output has scrolled away and you need the connection details again without onboarding a second time.
This command only reads state; it does not change the sandbox, gateway, credentials, or policies.

```bash
$$nemoclaw my-assistant connection info
```

If the sandbox is not running, the command reports that it could not read the connection details and points you to `$$nemoclaw <name> status`.

<Warning>
For OpenClaw sandboxes the block includes an authenticated dashboard URL.
Treat it like a password.
Do not log it, share it, or commit it to version control.
</Warning>

### `$$nemoclaw <name> dashboard-url`

<AgentOnly variant="openclaw">
Expand Down
115 changes: 115 additions & 0 deletions src/commands/sandbox/connection/info.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { Args } from "@oclif/core";

import type { AgentDefinition } from "../../../lib/agent/defs";
import { NemoClawCommand } from "../../../lib/cli/nemoclaw-oclif-command";
import {
ConnectionInfoCommandError,
runConnectionInfoCommand,
} from "../../../lib/connection-info-command";
import type { SandboxEntry } from "../../../lib/state/registry";

type PrintDashboardFn = (
sandboxName: string,
model: string,
provider: string,
nimContainer: string | null,
agent: AgentDefinition | null,
ready: boolean,
) => void;

type ConnectionInfoRuntimeBridge = {
getSandbox: (sandboxName: string) => SandboxEntry | null;
fetchToken: (sandboxName: string) => string | null;
loadAgent: (agentName: string) => AgentDefinition | null;
isTerminalAgent: (agent: AgentDefinition) => boolean;
printDashboard: PrintDashboardFn;
};

let runtimeBridgeFactory = (): ConnectionInfoRuntimeBridge => {
const onboard = require("../../../lib/onboard") as {
fetchGatewayAuthTokenFromSandbox: (sandboxName: string) => string | null;
};
const dashboard = require("../../../lib/onboard/dashboard") as {
getOnboardDashboardHelpers: () => { printDashboard: PrintDashboardFn };
};
const registry = require("../../../lib/state/registry") as {
getSandbox: (name: string) => SandboxEntry | null;
};
const defs = require("../../../lib/agent/defs") as {
loadAgent: (name: string) => AgentDefinition;
isTerminalAgent: (agent: AgentDefinition) => boolean;
};
return {
getSandbox: (sandboxName: string) => {
try {
return registry.getSandbox(sandboxName);
} catch {
return null;
}
},
fetchToken: onboard.fetchGatewayAuthTokenFromSandbox,
loadAgent: (agentName: string) => defs.loadAgent(agentName),
isTerminalAgent: (agent: AgentDefinition) => defs.isTerminalAgent(agent),
printDashboard: dashboard.getOnboardDashboardHelpers().printDashboard,
};
};

export function setConnectionInfoRuntimeBridgeFactoryForTest(
factory: () => ConnectionInfoRuntimeBridge,
): void {
runtimeBridgeFactory = factory;
}

function getRuntimeBridge(): ConnectionInfoRuntimeBridge {
return runtimeBridgeFactory();
}

export default class ConnectionInfoCliCommand extends NemoClawCommand {
static id = "sandbox:connection:info";
static strict = true;
static summary = "Print the connection details";
static description =
"Reprint the connection block shown at the end of onboarding for a running sandbox: dashboard URL, terminal connect command, and management commands.";
static usage = ["<name>"];
static examples = ["<%= config.bin %> sandbox connection info alpha"];
static args = {
sandboxName: Args.string({
name: "sandbox",
description: "Sandbox name",
required: true,
}),
};

public async run(): Promise<void> {
const { args } = await this.parse(ConnectionInfoCliCommand);
process.stdout.once("error", (err: NodeJS.ErrnoException) => {
if (err.code === "EPIPE") {
this.setExitCode(0);
return;
}
throw err;
});

const runtime = getRuntimeBridge();
try {
runConnectionInfoCommand(args.sandboxName, {
getSandbox: runtime.getSandbox,
fetchToken: runtime.fetchToken,
loadAgent: runtime.loadAgent,
isTerminalAgent: runtime.isTerminalAgent,
printDashboard: runtime.printDashboard,
log: (message: string) => console.log(message),
});
this.setExitCode(0);
} catch (error) {
if (error instanceof ConnectionInfoCommandError) {
this.failWithLines(error.lines, error.exitCode);
return;
}
throw error;
}
}
}
17 changes: 17 additions & 0 deletions src/lib/cli/connection-info-route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from "vitest";

import { sandboxActionTokens } from "./command-registry";
import { sandboxRouteTokens } from "./public-route-metadata";

describe("connection info public route (#7473)", () => {
it("maps the nested command id onto the connection info grammar", () => {
expect(sandboxRouteTokens("sandbox:connection:info")).toEqual(["connection", "info"]);
});

it("registers connection as a valid public sandbox action", () => {
expect(sandboxActionTokens()).toContain("connection");
});
});
7 changes: 7 additions & 0 deletions src/lib/cli/public-display-defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,13 @@ const PUBLIC_DISPLAY_LAYOUT: Record<string, readonly PublicDisplayLayout[]> = {
flags: "[--probe-only]",
},
],
"sandbox:connection:info": [
{
group: "Sandbox Management",
order: 3.1,
description: "Reprint the onboarding connection block",
},
],
"sandbox:dashboard-url": [
{
group: "Sandbox Management",
Expand Down
128 changes: 128 additions & 0 deletions src/lib/connection-info-command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it, vi } from "vitest";

import type { AgentDefinition } from "./agent/defs";
import { ConnectionInfoCommandError, runConnectionInfoCommand } from "./connection-info-command";
import type { SandboxEntry } from "./state/registry";

function entry(overrides: Partial<SandboxEntry> & { name: string }): SandboxEntry {
return overrides as SandboxEntry;
}

describe("connection info command (#7473)", () => {
it("reprints the connection block for a running OpenClaw sandbox", () => {
const printDashboard = vi.fn();
const fetchToken = vi.fn(() => "secret-token");
const loadAgent = vi.fn();

runConnectionInfoCommand("alpha", {
getSandbox: () =>
entry({ name: "alpha", agent: "openclaw", model: "llama", provider: "nvidia" }),
fetchToken,
loadAgent,
isTerminalAgent: () => false,
printDashboard,
log: vi.fn(),
});

expect(fetchToken).toHaveBeenCalledWith("alpha");
expect(loadAgent).not.toHaveBeenCalled();
expect(printDashboard).toHaveBeenCalledWith("alpha", "llama", "nvidia", null, null, true);
});

it("fails clearly when the OpenClaw gateway token cannot be read", () => {
const printDashboard = vi.fn();
let caught: ConnectionInfoCommandError | null = null;
try {
runConnectionInfoCommand("alpha", {
getSandbox: () => entry({ name: "alpha", agent: "openclaw", model: "m", provider: "p" }),
fetchToken: () => null,
loadAgent: vi.fn(),
isTerminalAgent: () => false,
printDashboard,
log: vi.fn(),
});
} catch (error) {
caught = error as ConnectionInfoCommandError;
}

expect(caught).toBeInstanceOf(ConnectionInfoCommandError);
expect(caught?.lines.join("\n")).toContain("Make sure the sandbox is running");
expect(printDashboard).not.toHaveBeenCalled();
});

it("fails when the sandbox does not exist", () => {
expect(() =>
runConnectionInfoCommand("ghost", {
getSandbox: () => null,
fetchToken: vi.fn(),
loadAgent: vi.fn(),
isTerminalAgent: () => false,
printDashboard: vi.fn(),
log: vi.fn(),
}),
).toThrow(/does not exist/);
});

it("passes the resolved agent definition for a non-OpenClaw gateway sandbox without a token pre-check", () => {
const printDashboard = vi.fn();
const fetchToken = vi.fn(() => null);
const agentDef = { displayName: "Hermes" } as unknown as AgentDefinition;
const loadAgent = vi.fn(() => agentDef);

runConnectionInfoCommand("beta", {
getSandbox: () => entry({ name: "beta", agent: "hermes", model: "m2", provider: "p2" }),
fetchToken,
loadAgent,
isTerminalAgent: () => false,
printDashboard,
log: vi.fn(),
});

expect(loadAgent).toHaveBeenCalledWith("hermes");
expect(fetchToken).not.toHaveBeenCalled();
expect(printDashboard).toHaveBeenCalledWith("beta", "m2", "p2", null, agentDef, true);
});

it("prints the terminal connect block for a terminal-runtime sandbox", () => {
const printDashboard = vi.fn();
const fetchToken = vi.fn();
const out: string[] = [];
const agentDef = {
displayName: "Deep Agents Code",
runtime: { kind: "terminal", interactive_command: "dcode" },
} as unknown as AgentDefinition;

runConnectionInfoCommand("gamma", {
getSandbox: () => entry({ name: "gamma", agent: "deepagents", model: "m3", provider: "p3" }),
fetchToken,
loadAgent: () => agentDef,
isTerminalAgent: () => true,
printDashboard,
log: (message: string) => out.push(message),
});

const output = out.join("\n");
expect(output).toContain("nemoclaw gamma connect");
expect(output).toContain("then run: dcode");
expect(printDashboard).not.toHaveBeenCalled();
expect(fetchToken).not.toHaveBeenCalled();
});

it("renders best-effort when a legacy entry has no recorded model or provider", () => {
const printDashboard = vi.fn();

runConnectionInfoCommand("legacy", {
getSandbox: () => entry({ name: "legacy", agent: "openclaw" }),
fetchToken: () => "token",
loadAgent: vi.fn(),
isTerminalAgent: () => false,
printDashboard,
log: vi.fn(),
});

expect(printDashboard).toHaveBeenCalledWith("legacy", "", "", null, null, true);
});
});
Loading
Loading