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
11 changes: 11 additions & 0 deletions docs/get-started/quickstart-langchain-deepagents-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ The image installs a hash-locked, pinned Deep Agents Code release with NVIDIA pr
NemoClaw writes `/sandbox/.deepagents/config.toml` with an OpenAI-compatible provider pointed at `https://inference.local/v1`, uses a scoped placeholder API key for that managed route, and sets `use_responses_api = false` for Chat Completions compatibility.
NemoClaw/OpenShell keeps real provider credentials in credential handling and does not write them into the Deep Agents config file.

## Choose the Default Sandbox

When you manage multiple sandboxes, use the Deep Agents alias to promote a registered Deep Agents Code sandbox to the default.

```bash
nemo-deepagents use <sandbox-name>
```

The command updates NemoClaw's host-side registry.
It does not modify the sandbox or the `dcode` configuration.

## Use the Harness

Connect to the sandbox, then launch the terminal UI.
Expand Down
15 changes: 15 additions & 0 deletions docs/reference/commands-nemohermes.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,21 @@ nemohermes list [--json]
nemohermes list --json
```

### `nemohermes use <name>`

Promote a registered sandbox to the default.
This is the first-class replacement for hand-editing `~/.nemoclaw/sandboxes.json`; it updates the registry through the same atomic, lock-guarded path that `nemohermes onboard` uses for the initial default.
Subsequent commands and the `NEMOCLAW_SANDBOX_NAME` resolution order then pick up the new default automatically.
Pass `--json` to receive a machine-readable result indicating whether the registry was updated, the sandbox was already the default, or the name is unknown.

`nemohermes use` is a thin selector and never mutates the sandbox itself.
It fails with a non-zero exit and a known-sandbox list when the requested name is not registered, so scripts can branch safely on the outcome.

```bash
nemohermes use <name>
nemohermes use <name> --json
```

### `nemohermes deploy`

<Warning>
Expand Down
15 changes: 15 additions & 0 deletions docs/reference/commands.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,21 @@ $$nemoclaw list [--json]
$$nemoclaw list --json
```

### `$$nemoclaw use <name>`

Promote a registered sandbox to the default.
This is the first-class replacement for hand-editing `~/.nemoclaw/sandboxes.json`; it updates the registry through the same atomic, lock-guarded path that `$$nemoclaw onboard` uses for the initial default.
Subsequent commands and the `NEMOCLAW_SANDBOX_NAME` resolution order then pick up the new default automatically.
Pass `--json` to receive a machine-readable result indicating whether the registry was updated, the sandbox was already the default, or the name is unknown.

`$$nemoclaw use` is a thin selector and never mutates the sandbox itself.
It fails with a non-zero exit and a known-sandbox list when the requested name is not registered, so scripts can branch safely on the outcome.

```bash
$$nemoclaw use <name>
$$nemoclaw use <name> --json
```

### `$$nemoclaw deploy`

<Warning>
Expand Down
49 changes: 49 additions & 0 deletions src/commands/use.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { Args } from "@oclif/core";
import { NemoClawCommand } from "../lib/cli/nemoclaw-oclif-command";
import { buildUseCommandDeps, runUseCommand } from "../lib/use-command-deps";

export default class UseCommand extends NemoClawCommand {
static id = "use";
static strict = true;
static enableJsonFlag = true;
static summary = "Set the default sandbox";
static description =
"Promote a registered sandbox to the default. Updates the sandbox registry atomically so subsequent commands and scripts use the chosen sandbox without hand-editing on-disk state.";
static usage = ["use <name> [--json]"];
static examples = ["<%= config.bin %> use alpha", "<%= config.bin %> use alpha --json"];
static args = {
sandboxName: Args.string({
name: "name",
description: "Sandbox name to promote to the default",
required: true,
}),
};
static flags = {};

public async run(): Promise<unknown> {
const { args } = await this.parse(UseCommand);
const deps = buildUseCommandDeps();
const result = runUseCommand(args.sandboxName, deps);
const json = this.jsonEnabled();
if (result.outcome === "not-found") {
if (json) {
process.exitCode = 1;
return result;
}
const known = result.knownSandboxes.length > 0 ? result.knownSandboxes.join(", ") : "(none)";
this.error(`Sandbox not found: ${result.sandboxName}. Known sandboxes: ${known}.`, {
exit: 1,
});
}
if (json) return result;
if (result.outcome === "already-default") {
this.log(`Sandbox '${result.sandboxName}' is already the default.`);
return;
}
const previous = result.previousDefault ? ` (was '${result.previousDefault}')` : "";
this.log(`Default sandbox set to '${result.sandboxName}'${previous}.`);
}
}
8 changes: 8 additions & 0 deletions src/lib/cli/public-display-defaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,14 @@ const PUBLIC_DISPLAY_LAYOUT: Record<string, readonly PublicDisplayLayout[]> = {
description: "Run uninstall.sh (local only; no remote fallback)",
},
],
use: [
{
group: "Sandbox Management",
order: 2.5,
usage: "nemoclaw use <name>",
flags: "[--json]",
},
],
update: [
{
group: "Upgrade",
Expand Down
112 changes: 112 additions & 0 deletions src/lib/use-command-deps.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// 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 { runUseCommand, type UseCommandDeps } from "./use-command-deps";

function makeDeps(
overrides: Partial<{
sandboxes: ReadonlyArray<string>;
defaultSandbox: string | null;
setDefault: (name: string) => boolean;
}> = {},
): UseCommandDeps & {
setDefault: ReturnType<typeof vi.fn>;
listSandboxes: ReturnType<typeof vi.fn>;
} {
const sandboxes = (overrides.sandboxes ?? []).map((name) => ({ name }));
const defaultSandbox = overrides.defaultSandbox ?? null;
const setDefault = vi.fn(overrides.setDefault ?? ((_name: string) => true));
const listSandboxes = vi.fn(() => ({ sandboxes, defaultSandbox }));
return { listSandboxes, setDefault };
}

describe("runUseCommand", () => {
it("reports unknown sandbox with the known list and skips the registry write", () => {
const deps = makeDeps({ sandboxes: ["alpha", "beta"], defaultSandbox: "alpha" });

const result = runUseCommand("gamma", deps);

expect(result).toEqual({
outcome: "not-found",
sandboxName: "gamma",
knownSandboxes: ["alpha", "beta"],
});
expect(deps.setDefault).not.toHaveBeenCalled();
});

it("returns already-default and skips the registry write when the chosen sandbox is the default", () => {
const deps = makeDeps({ sandboxes: ["alpha", "beta"], defaultSandbox: "alpha" });

const result = runUseCommand("alpha", deps);

expect(result).toEqual({ outcome: "already-default", sandboxName: "alpha" });
expect(deps.setDefault).not.toHaveBeenCalled();
});

it("promotes the chosen sandbox and reports the previous default", () => {
const deps = makeDeps({ sandboxes: ["alpha", "beta"], defaultSandbox: "alpha" });

const result = runUseCommand("beta", deps);

expect(result).toEqual({
outcome: "set",
sandboxName: "beta",
previousDefault: "alpha",
});
expect(deps.setDefault).toHaveBeenCalledTimes(1);
expect(deps.setDefault).toHaveBeenCalledWith("beta");
});

it("reports the first default when the registry currently has none", () => {
const deps = makeDeps({ sandboxes: ["alpha"], defaultSandbox: null });

const result = runUseCommand("alpha", deps);

expect(result).toEqual({
outcome: "set",
sandboxName: "alpha",
previousDefault: null,
});
expect(deps.setDefault).toHaveBeenCalledWith("alpha");
});

it("downgrades to not-found when the registry refuses the write due to a concurrent removal", () => {
const deps = makeDeps({
sandboxes: ["alpha", "beta"],
defaultSandbox: "alpha",
setDefault: () => false,
});

const result = runUseCommand("beta", deps);

expect(result).toEqual({
outcome: "not-found",
sandboxName: "beta",
knownSandboxes: ["alpha", "beta"],
});
expect(deps.setDefault).toHaveBeenCalledWith("beta");
});

it("refreshes the known sandbox list after a failed setDefault so the diagnostic excludes the concurrently removed sandbox", () => {
const listSandboxes = vi
.fn()
.mockReturnValueOnce({
sandboxes: [{ name: "alpha" }, { name: "beta" }],
defaultSandbox: "alpha",
})
.mockReturnValueOnce({ sandboxes: [{ name: "alpha" }], defaultSandbox: "alpha" });
const setDefault = vi.fn(() => false);
const deps: UseCommandDeps = { listSandboxes, setDefault };

const result = runUseCommand("beta", deps);

expect(result).toEqual({
outcome: "not-found",
sandboxName: "beta",
knownSandboxes: ["alpha"],
});
expect(listSandboxes).toHaveBeenCalledTimes(2);
expect(setDefault).toHaveBeenCalledWith("beta");
});
});
58 changes: 58 additions & 0 deletions src/lib/use-command-deps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import * as registry from "./state/registry";

export interface UseCommandDeps {
readonly listSandboxes: () => {
readonly sandboxes: ReadonlyArray<{ readonly name: string }>;
readonly defaultSandbox: string | null;
};
readonly setDefault: (name: string) => boolean;
}

export type UseCommandResult =
| {
readonly outcome: "set";
readonly sandboxName: string;
readonly previousDefault: string | null;
}
| {
readonly outcome: "already-default";
readonly sandboxName: string;
}
| {
readonly outcome: "not-found";
readonly sandboxName: string;
readonly knownSandboxes: ReadonlyArray<string>;
};

export function buildUseCommandDeps(): UseCommandDeps {
return {
listSandboxes: () => registry.listSandboxes(),
setDefault: (name) => registry.setDefault(name),
};
}

export function runUseCommand(sandboxName: string, deps: UseCommandDeps): UseCommandResult {
const current = deps.listSandboxes();
const known = current.sandboxes.map((sb) => sb.name);
if (!known.includes(sandboxName)) {
return { outcome: "not-found", sandboxName, knownSandboxes: known };
}
if (current.defaultSandbox === sandboxName) {
return { outcome: "already-default", sandboxName };
}
const updated = deps.setDefault(sandboxName);
if (!updated) {
// setDefault rechecks existence under the registry lock. Refresh after a
// concurrent removal so the not-found diagnostic reflects post-lock state.
const refreshed = deps.listSandboxes();
return {
outcome: "not-found",
sandboxName,
knownSandboxes: refreshed.sandboxes.map((sb) => sb.name),
};
}
return { outcome: "set", sandboxName, previousDefault: current.defaultSandbox };
}
69 changes: 69 additions & 0 deletions test/nemo-deepagents-alias.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,25 @@ function runNemoClaw(
}
}

function createDeepAgentsRegistry(): { home: string; registryPath: string } {
const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemo-deepagents-use-"));
const registryDir = path.join(home, ".nemoclaw");
const registryPath = path.join(registryDir, "sandboxes.json");
fs.mkdirSync(registryDir, { recursive: true });
fs.writeFileSync(
registryPath,
JSON.stringify({
sandboxes: {
"dcode-alpha": { name: "dcode-alpha", agent: "langchain-deepagents-code" },
"dcode-beta": { name: "dcode-beta", agent: "langchain-deepagents-code" },
},
defaultSandbox: "dcode-alpha",
}),
{ mode: 0o600 },
);
return { home, registryPath };
}

describe("nemo-deepagents alias", () => {
it("package-style nemo-deepagents symlink exists and is executable", () => {
expect(fs.existsSync(DEEPAGENTS_CLI)).toBe(true);
Expand All @@ -101,9 +120,59 @@ describe("nemo-deepagents alias", () => {
expect(code).toBe(0);
expect(out).toContain("NemoDeepAgents");
expect(out).toContain("nemo-deepagents onboard");
expect(out).toContain("nemo-deepagents use <name>");
expect(out).not.toContain("nemoclaw onboard");
});

it("promotes a registered Deep Agents sandbox through the alias command", () => {
const { home, registryPath } = createDeepAgentsRegistry();

try {
const { code, out } = runDeepAgents("use dcode-beta", { HOME: home });

expect(code).toBe(0);
expect(out).toContain("Default sandbox set to 'dcode-beta' (was 'dcode-alpha').");
expect(JSON.parse(fs.readFileSync(registryPath, "utf8"))).toEqual(
expect.objectContaining({ defaultSandbox: "dcode-beta" }),
);
} finally {
fs.rmSync(home, { force: true, recursive: true });
}
});

it("reports an already-default Deep Agents sandbox through the alias command", () => {
const { home } = createDeepAgentsRegistry();

try {
const { code, out } = runDeepAgents("use dcode-alpha", { HOME: home });

expect(code).toBe(0);
expect(out).toContain("Sandbox 'dcode-alpha' is already the default.");
} finally {
fs.rmSync(home, { force: true, recursive: true });
}
});

it("returns structured not-found output through the alias command", () => {
const { home, registryPath } = createDeepAgentsRegistry();

try {
const { code, out } = runDeepAgents("use dcode-missing --json", { HOME: home });

expect(code).toBe(1);
expect(JSON.parse(out)).toEqual({
outcome: "not-found",
sandboxName: "dcode-missing",
knownSandboxes: ["dcode-alpha", "dcode-beta"],
});
expect(JSON.parse(fs.readFileSync(registryPath, "utf8"))).toEqual(
expect.objectContaining({ defaultSandbox: "dcode-alpha" }),
);
} finally {
fs.rmSync(home, { force: true, recursive: true });
}
});

it("routes nemo-deepagents uninstall as a global command, not a sandbox connect command", () => {
const { code, out } = runDeepAgents("uninstall --help");
expect(code).toBe(0);
Expand Down
Loading
Loading