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
9 changes: 9 additions & 0 deletions docs/get-started/quickstart-langchain-deepagents-code.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,15 @@ The command prints the sandbox name, NemoClaw harness, active `dcode` agent, con
`dcode --help` lists the managed aliases before the upstream Deep Agents Code help.
The sandbox name resolves when you run the command from a `nemoclaw <sandbox-name> connect` shell, which loads the NemoClaw runtime environment.

Inspect the generated Deep Agents configuration from the host with:

```bash
nemo-deepagents <sandbox-name> config get
```

NemoClaw parses `config.toml`, removes gateway auth data, and redacts credential-shaped values before printing it.
`config set` is not supported for this image-baked configuration; re-onboard the named sandbox to change its managed provider or model selection.
Comment thread
cjagwani marked this conversation as resolved.

</Accordion>

<Accordion title="Manage Python and sandbox state">
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@
"js-yaml": "^4.1.1",
"p-retry": "^4.6.2",
"qrcode-terminal": "^0.12.0",
"smol-toml": "^1.7.0",
"smol-toml": "1.7.0",
"yaml": "2.8.3"
},
"bundleDependencies": [
Expand Down
103 changes: 103 additions & 0 deletions src/lib/sandbox/config-format.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

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

const { parseConfig, serializeConfig } =
require("./config-format") as typeof import("./config-format");

describe("sandbox config formats", () => {
it("parses a comment-prefixed TOML config with nested tables (#6548)", () => {
const parsed = parseConfig(
[
"# Generated by NemoClaw. Do not edit by hand.",
"[models]",
'default = "openai:nvidia/meta/llama-3.1-8b-instruct"',
"[models.providers.openai]",
'models = ["nvidia/meta/llama-3.1-8b-instruct"]',
"enabled = true",
].join("\n"),
"toml",
);

expect(parsed).toEqual({
models: {
default: "openai:nvidia/meta/llama-3.1-8b-instruct",
providers: {
openai: { models: ["nvidia/meta/llama-3.1-8b-instruct"], enabled: true },
},
},
});
});

it("keeps JSON and YAML parsing behavior", () => {
expect(parseConfig('{"model":{"id":"nemotron"}}', "json")).toEqual({
model: { id: "nemotron" },
});
expect(parseConfig("model:\n id: nemotron\n", "yaml")).toEqual({
model: { id: "nemotron" },
});
});

it("rejects excessive TOML nesting with a generic source-safe error", () => {
const secret = "credential-canary-must-not-escape";
const acceptedPath = Array.from({ length: 63 }, (_, index) => `level${index}`).join(".");
const tablePath = Array.from({ length: 64 }, (_, index) => `level${index}`).join(".");
const source = `[${tablePath}]\npassword = "${secret}"`;

expect(parseConfig(`[${acceptedPath}]\nvalue = 1`, "toml")).toBeDefined();
expect(() => parseConfig(source, "toml")).toThrow("Config exceeds safe structural limits.");
try {
parseConfig(source, "toml");
} catch (error) {
expect(String(error)).not.toContain(secret);
expect(String(error)).not.toContain(tablePath);
}
});

it("rejects oversized TOML containers", () => {
const accepted = `values = [${Array.from({ length: 10_000 }, () => "0").join(",")}]`;
const source = `values = [${Array.from({ length: 10_001 }, () => "0").join(",")}]`;

expect((parseConfig(accepted, "toml").values as unknown[]).length).toBe(10_000);
expect(() => parseConfig(source, "toml")).toThrow("Config exceeds safe structural limits.");
});

it("rejects objects that exceed per-object and aggregate key budgets", () => {
const oversizedTomlObject = Array.from(
{ length: 10_001 },
(_, index) => `key${index} = 0`,
).join("\n");
const groups = Array.from({ length: 6 }, (_, group) =>
Object.fromEntries(Array.from({ length: 9_000 }, (_, index) => [`key${group}_${index}`, 0])),
);

expect(() => parseConfig(oversizedTomlObject, "toml")).toThrow(
"Config exceeds safe structural limits.",
);
expect(() => parseConfig(JSON.stringify({ groups }), "json")).toThrow(
"Config exceeds safe structural limits.",
);
});

it("rejects TOML that exceeds the total parsed-node budget", () => {
const array = Array.from({ length: 100 }, () => "0").join(",");
const source = Array.from({ length: 1_000 }, (_, index) => `key${index} = [${array}]`).join(
"\n",
);

expect(() => parseConfig(source, "toml")).toThrow("Config exceeds safe structural limits.");
});

it("rejects cyclic YAML aliases without recursive validation", () => {
const source = "root: &root\n child: *root\n";

expect(() => parseConfig(source, "yaml")).toThrow("Config exceeds safe structural limits.");
});

it("refuses to serialize TOML as another format", () => {
expect(() => serializeConfig({ model: "nemotron" }, "toml")).toThrow(
/config set is not supported for TOML-format agents/,
);
});
});
147 changes: 147 additions & 0 deletions src/lib/sandbox/config-format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

type ConfigObject = import("../security/credential-filter").ConfigObject;

const MAX_CONFIG_STRUCTURE_DEPTH = 64;
const MAX_CONFIG_STRUCTURE_NODES = 100_000;
const MAX_CONFIG_OBJECT_KEYS = 10_000;
const MAX_CONFIG_TOTAL_KEYS = 50_000;
const MAX_CONFIG_ARRAY_LENGTH = 10_000;

type ConfigWalkEntry =
| { kind: "visit"; depth: number; value: unknown }
| { kind: "leave"; value: object };

function configStructureLimitError(): Error {
return new Error("Config exceeds safe structural limits.");
}

function invalidConfigShapeError(): Error {
return new Error("Config is not a JSON-like object.");
}

function readOwnDataValue(value: object, key: string): unknown {
const descriptor = Object.getOwnPropertyDescriptor(value, key);
if (!descriptor || !("value" in descriptor)) throw invalidConfigShapeError();
return descriptor.value;
}

/**
* Validate parsed, sandbox-controlled config iteratively before any recursive
* validation, redaction, or serialization can consume it.
*/
function assertSafeConfigStructure(value: unknown): asserts value is ConfigObject {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
throw invalidConfigShapeError();
}

const stack: ConfigWalkEntry[] = [{ kind: "visit", value, depth: 0 }];
const activeAncestors = new WeakSet<object>();
let scheduledNodes = 1;
let totalObjectKeys = 0;

while (stack.length > 0) {
const entry = stack.pop();
if (!entry) break;
if (entry.kind === "leave") {
activeAncestors.delete(entry.value);
continue;
}
if (entry.depth > MAX_CONFIG_STRUCTURE_DEPTH) {
throw configStructureLimitError();
}

const entryType = typeof entry.value;
if (
entry.value === null ||
entry.value === undefined ||
entryType === "boolean" ||
entryType === "number" ||
entryType === "string"
) {
continue;
}
if (entryType !== "object") {
throw invalidConfigShapeError();
}

const objectValue = entry.value as object;
if (activeAncestors.has(objectValue)) throw configStructureLimitError();
activeAncestors.add(objectValue);
stack.push({ kind: "leave", value: objectValue });

let children: unknown[];
if (Array.isArray(entry.value)) {
if (entry.value.length > MAX_CONFIG_ARRAY_LENGTH) {
throw configStructureLimitError();
}
children = Array.from({ length: entry.value.length }, (_, index) => {
const descriptor = Object.getOwnPropertyDescriptor(entry.value, String(index));
if (!descriptor) return undefined;
if (!("value" in descriptor)) throw invalidConfigShapeError();
return descriptor.value;
});
} else {
const prototype = Object.getPrototypeOf(entry.value);
if (prototype !== Object.prototype && prototype !== null) {
throw invalidConfigShapeError();
}
const keys = Object.keys(entry.value);
totalObjectKeys += keys.length;
if (keys.length > MAX_CONFIG_OBJECT_KEYS || totalObjectKeys > MAX_CONFIG_TOTAL_KEYS) {
throw configStructureLimitError();
}
children = keys.map((key) => readOwnDataValue(entry.value as object, key));
}

scheduledNodes += children.length;
if (scheduledNodes > MAX_CONFIG_STRUCTURE_NODES) throw configStructureLimitError();
for (let index = children.length - 1; index >= 0; index -= 1) {
stack.push({ kind: "visit", value: children[index], depth: entry.depth + 1 });
}
}
}

/** Parse raw agent configuration according to its manifest-declared format. */
export function parseConfig(raw: string, format: string): ConfigObject {
let parsed: unknown;
if (format === "yaml") {
const YAML = require("yaml") as {
parse: (text: string) => unknown;
};
try {
parsed = YAML.parse(raw);
} catch {
throw new Error("Invalid YAML configuration syntax.");
}
} else if (format === "toml") {
const TOML = require("smol-toml") as {
parse: (text: string) => unknown;
};
try {
parsed = TOML.parse(raw);
} catch {
throw new Error("Invalid TOML configuration syntax.");
}
} else {
try {
parsed = JSON.parse(raw);
} catch {
throw new Error("Invalid JSON configuration syntax.");
}
}
assertSafeConfigStructure(parsed);
return parsed;
}

/** Serialize mutable agent configuration without corrupting TOML inputs. */
export function serializeConfig(config: ConfigObject, format: string): string {
if (format === "yaml") {
return require("yaml").stringify(config);
}
if (format === "toml") {
throw new Error("config set is not supported for TOML-format agents.");
}
return JSON.stringify(config, null, 2);
}
Loading
Loading