Skip to content

feat(onboard): add --from <Dockerfile> option for custom sandbox images#1059

Open
adityanjothi wants to merge 7 commits intoNVIDIA:mainfrom
adityanjothi:main
Open

feat(onboard): add --from <Dockerfile> option for custom sandbox images#1059
adityanjothi wants to merge 7 commits intoNVIDIA:mainfrom
adityanjothi:main

Conversation

@adityanjothi
Copy link
Copy Markdown

@adityanjothi adityanjothi commented Mar 28, 2026

Summary

Adds a --from <Dockerfile> flag to nemoclaw onboard so callers can build the sandbox image from a custom Dockerfile instead of the stock NemoClaw image. The entire parent directory of the specified file is used as the Docker build context. All NemoClaw build arguments are still injected at build time, so the flag works with every inference provider and integrates safely with --resume.

Changes

  • CLI (bin/nemoclaw.js): pre-pass extracts --from <path> before the unknown-arg validator; passes fromDockerfile to runOnboard()
  • createSandbox (bin/lib/onboard.js): new 6th param fromDockerfile; when set, copies the Dockerfile's parent directory via the existing copyBuildContextDir
    filter (excludes node_modules, .git, .venv, etc.) instead of staging the stock image
  • onboard(): reads fromDockerfile from opts or NEMOCLAW_FROM_DOCKERFILE env var (non-interactive/CI); records it in session.metadata for resume tracking
  • getResumeConfigConflicts(): flags --resume combined with a different --from path than the original session recorded
  • Docs (docs/reference/commands.md): documents the flag, env var, build context behaviour, and resume conflict behaviour

Type of Change

  • Code change with doc updates.

Testing

  • npx prek run --all-files passes (or equivalently make check).
  • npm test passes.

Checklist

General

Code Changes

  • Formatters applied — npx prek run --all-files auto-fixes formatting (or make format for targeted runs).
  • Tests added or updated for new or changed behavior.
  • No secrets, API keys, or credentials committed.
  • Doc pages updated for any user-facing behavior changes (new commands, changed defaults, new features, bug fixes that contradict existing docs).

Doc Changes

  • Follows the style guide.
  • Cross-references and links verified.

Summary by CodeRabbit

  • New Features

    • Added --from flag to onboard for building sandboxes from a custom Dockerfile.
    • Added NEMOCLAW_FROM_DOCKERFILE env var support for non-interactive use.
    • Resume now detects conflicts when a different Dockerfile path is supplied and surfaces field-specific resume errors.
  • Documentation

    • Updated command reference and help text to document the new flag, env var, and resume behavior.
  • Tests

    • Added unit and integration tests for resume conflict detection and custom-Dockerfile sandbox staging.

adityanjothi and others added 3 commits March 28, 2026 15:29
Accept an optional --from <path> flag on `nemoclaw onboard` so callers
can supply a custom Dockerfile instead of the stock NemoClaw image.

- CLI: pre-pass extracts --from before the unknown-arg validator
- createSandbox: new 6th param; custom path copies parent dir via the
  existing copyBuildContextDir filter (excludes node_modules, .git, etc.)
- onboard: reads fromDockerfile from opts or NEMOCLAW_FROM_DOCKERFILE env
  var (non-interactive); records it in session.metadata for resume safety
- getResumeConfigConflicts: flags --resume with a different --from path
- Tests: conflict detection, build context staging, missing-path exit
feat(onboard): add --from <Dockerfile> option for custom sandbox images
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 28, 2026

📝 Walkthrough

Walkthrough

Added an optional --from / NEMOCLAW_FROM_DOCKERFILE flow to nemoclaw onboard: CLI parsing, propagation into onboard(), resume conflict detection against session.metadata.fromDockerfile, staging a custom Dockerfile’s parent directory as the build context, and tests for happy/error paths.

Changes

Cohort / File(s) Summary
CLI Argument Parsing
bin/nemoclaw.js
Parse and validate --from <Dockerfile> (remove tokens from args), pass resolved path into runOnboard, and update usage/help text.
Onboarding Logic
bin/lib/onboard.js
Accept fromDockerfile in createSandbox(...) and onboard(); validate file, stage parent dir into temp build context, copy/rename to Dockerfile when needed, add --from <path> to lock command, persist resolved value to session.metadata, and include resume conflict checks with field-specific messages.
Session Metadata
bin/lib/onboard-session.js
Add buildSessionMetadata() with fromDockerfile (default null) and allow safe updates of metadata.fromDockerfile.
Documentation
docs/reference/commands.md
Document --from <Dockerfile> and NEMOCLAW_FROM_DOCKERFILE, explain staging behavior and resume-conflict semantics.
Tests
test/onboard.test.js
Add unit tests for getResumeConfigConflicts covering fromDockerfile permutations; integration-style tests for staging a custom Dockerfile parent dir and error path when file missing.

Sequence Diagram

sequenceDiagram
    actor User
    participant CLI as nemoclaw.js
    participant Onboard as onboard()
    participant Sandbox as createSandbox()
    participant FS as FileSystem
    participant Docker as Docker Build

    User->>CLI: nemoclaw onboard --from custom.Dockerfile
    CLI->>CLI: Extract & validate `--from` (or read NEMOCLAW_FROM_DOCKERFILE)
    CLI->>Onboard: runOnboard(opts with fromDockerfile)
    Onboard->>Onboard: Resolve fromDockerfile\nCheck session.metadata.fromDockerfile
    alt Resume conflict
        Onboard->>User: Error (fromDockerfile mismatch)
    else No conflict
        Onboard->>Sandbox: createSandbox(..., fromDockerfile)
        Sandbox->>FS: Verify custom Dockerfile exists
        Sandbox->>FS: Copy parent directory into staged build context
        Sandbox->>FS: Copy/rename file to staged `Dockerfile` if needed
        Sandbox->>Docker: Build image using staged context
        Docker-->>Sandbox: Build result
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I found a Dockerfile nest,
I copied the parent and did my best,
I check the resume, I guard the gate,
Mismatched paths I gently berate.
Sandbox sprung — a hopping build, elate!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 37.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main feature: adding a --from option to the onboard command for custom sandbox images.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

@adityanjothi adityanjothi changed the title [feat] Add support for NemoClaw onboard --from custom Dockerfile to build sandboxes for different use cases. feat(onboard): add --from <Dockerfile> option for custom sandbox images Mar 28, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (1)
docs/reference/commands.md (1)

61-61: Switch these sentences to active voice.

The changed prose uses passive constructions like “is used”, “can also be supplied”, and “is attempted”.

As per coding guidelines "Active voice required. Flag passive constructions."

Also applies to: 70-76

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/reference/commands.md` at line 61, Rewrite the passive sentences in the
docs text to active voice: change "The entire parent directory of the specified
file is used as the Docker build context" to something like "Docker uses the
entire parent directory of the specified file as the build context," change "any
files your Dockerfile references (scripts, config, etc.) must live alongside it"
to an active-form sentence ("Place any files your Dockerfile references ...
alongside it"), and similarly convert other passive phrases such as "can also be
supplied" and "is attempted" in the same section (and lines 70–76) into active
constructions that name the actor (e.g., "You can also supply..." or "Docker
attempts...") so the prose follows the Active voice required guideline.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bin/lib/onboard.js`:
- Around line 2807-2812: The current fromDockerfile value is only taken from the
current CLI/env so resuming can drop the originally recorded Dockerfile; update
the logic that computes fromDockerfile to: if opts.fromDockerfile is set use
that, else if resume is true and the onboardSession has a stored dockerfile path
use that stored value, and only then fall back to the env/non-interactive
branch; when persisting the dockerfile into session metadata (where the code
currently writes the original value) convert the path to an absolute path (e.g.,
via path.resolve) so relative CWD changes on resume still resolve correctly;
adjust uses of fromDockerfile (the variable passed into acquireOnboardLock and
later rebuild logic) to rely on this merged/normalized value.
- Around line 1327-1335: The current conflict check in onboard.js only flags
when both requestedFrom and recordedFrom are non-null; update the logic for the
fromDockerfile conflict so that any mismatch (one null and the other non-null OR
different resolved paths) is treated as a conflict: use the existing variables
requestedFrom and recordedFrom to compare and push to conflicts whenever
requestedFrom !== recordedFrom (including when one is null). Also update the
resume/error rendering for the fromDockerfile conflict to display the null case
as “stock image” so messages read clearly when one side is missing (reference
the session.metadata.fromDockerfile and the conflicts entry with field
"fromDockerfile").
- Around line 1776-1789: The current check only verifies existence so passing a
directory for --from leads to an uncaught EISDIR in fs.copyFileSync; update the
fromDockerfile handling (the fromResolved branch used with copyBuildContextDir,
stagedDockerfile and fs.copyFileSync) to verify that fromResolved is a regular
file (use fs.statSync or fs.lstatSync and isFile()) before attempting to copy;
if it is not a file, print a clear error like "Custom Dockerfile is not a file:
<path>" and exit(1) so the friendly CLI error path runs and temp build context
is cleaned up.

In `@docs/reference/commands.md`:
- Line 67: The docs claim the Dockerfile "must be named `Dockerfile`" but the
implementation in bin/lib/onboard.js remaps other basenames into
buildCtx/Dockerfile; update docs/reference/commands.md to reflect that any
Dockerfile basename is accepted and will be copied/renamed into
buildCtx/Dockerfile by onboard.js (referencing bin/lib/onboard.js and its
remapping behavior) — replace the restrictive sentence with one that explains
the remapping behavior and how the file will be used.

---

Nitpick comments:
In `@docs/reference/commands.md`:
- Line 61: Rewrite the passive sentences in the docs text to active voice:
change "The entire parent directory of the specified file is used as the Docker
build context" to something like "Docker uses the entire parent directory of the
specified file as the build context," change "any files your Dockerfile
references (scripts, config, etc.) must live alongside it" to an active-form
sentence ("Place any files your Dockerfile references ... alongside it"), and
similarly convert other passive phrases such as "can also be supplied" and "is
attempted" in the same section (and lines 70–76) into active constructions that
name the actor (e.g., "You can also supply..." or "Docker attempts...") so the
prose follows the Active voice required guideline.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: de8c19bc-e151-4760-8134-2eee8061febc

📥 Commits

Reviewing files that changed from the base of the PR and between eb4ba8c and aa4084e.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • bin/lib/onboard.js
  • bin/nemoclaw.js
  • docs/reference/commands.md
  • test/onboard.test.js

Comment on lines +1776 to +1789
if (fromDockerfile) {
const fromResolved = path.resolve(fromDockerfile);
if (!fs.existsSync(fromResolved)) {
console.error(` Custom Dockerfile not found: ${fromResolved}`);
process.exit(1);
}
// Copy the entire parent directory as build context. copyBuildContextDir
// already filters out node_modules, .git, .venv, __pycache__, etc.
copyBuildContextDir(path.dirname(fromResolved), buildCtx);
// If the caller pointed at a file not named "Dockerfile", copy it to the
// location openshell expects (buildCtx/Dockerfile).
if (path.basename(fromResolved) !== "Dockerfile") {
fs.copyFileSync(fromResolved, stagedDockerfile);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Validate that --from points to a regular file.

The new check only guards existence. Passing a directory here falls through to fs.copyFileSync() and throws an uncaught EISDIR, which skips the friendly CLI error path and leaves the temp build context behind.

🛠 Suggested validation
     if (!fs.existsSync(fromResolved)) {
       console.error(`  Custom Dockerfile not found: ${fromResolved}`);
       process.exit(1);
     }
+    if (!fs.statSync(fromResolved).isFile()) {
+      console.error(`  Custom Dockerfile must be a file: ${fromResolved}`);
+      process.exit(1);
+    }
     // Copy the entire parent directory as build context. copyBuildContextDir
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (fromDockerfile) {
const fromResolved = path.resolve(fromDockerfile);
if (!fs.existsSync(fromResolved)) {
console.error(` Custom Dockerfile not found: ${fromResolved}`);
process.exit(1);
}
// Copy the entire parent directory as build context. copyBuildContextDir
// already filters out node_modules, .git, .venv, __pycache__, etc.
copyBuildContextDir(path.dirname(fromResolved), buildCtx);
// If the caller pointed at a file not named "Dockerfile", copy it to the
// location openshell expects (buildCtx/Dockerfile).
if (path.basename(fromResolved) !== "Dockerfile") {
fs.copyFileSync(fromResolved, stagedDockerfile);
}
if (fromDockerfile) {
const fromResolved = path.resolve(fromDockerfile);
if (!fs.existsSync(fromResolved)) {
console.error(` Custom Dockerfile not found: ${fromResolved}`);
process.exit(1);
}
if (!fs.statSync(fromResolved).isFile()) {
console.error(` Custom Dockerfile must be a file: ${fromResolved}`);
process.exit(1);
}
// Copy the entire parent directory as build context. copyBuildContextDir
// already filters out node_modules, .git, .venv, __pycache__, etc.
copyBuildContextDir(path.dirname(fromResolved), buildCtx);
// If the caller pointed at a file not named "Dockerfile", copy it to the
// location openshell expects (buildCtx/Dockerfile).
if (path.basename(fromResolved) !== "Dockerfile") {
fs.copyFileSync(fromResolved, stagedDockerfile);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/lib/onboard.js` around lines 1776 - 1789, The current check only verifies
existence so passing a directory for --from leads to an uncaught EISDIR in
fs.copyFileSync; update the fromDockerfile handling (the fromResolved branch
used with copyBuildContextDir, stagedDockerfile and fs.copyFileSync) to verify
that fromResolved is a regular file (use fs.statSync or fs.lstatSync and
isFile()) before attempting to copy; if it is not a file, print a clear error
like "Custom Dockerfile is not a file: <path>" and exit(1) so the friendly CLI
error path runs and temp build context is cleaned up.

adityanjothi and others added 2 commits March 29, 2026 12:59
…andling

- Broaden fromDockerfile conflict detection to catch one-null mismatches
  (stock→custom and custom→stock) not just different non-null paths
- Replace null label substitution with purpose-built messages for each
  mismatch case so users know exactly what to do to resolve the conflict
- On --resume, fall back to the session-recorded fromDockerfile when
  --from is omitted so the original image is reused automatically
- Store fromDockerfile as an absolute path in session metadata so
  relative paths survive CWD changes between runs
- Update docs: any filename is accepted, not just 'Dockerfile'
fix(onboard): address review feedback on --from resume and conflict h…
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@bin/lib/onboard.js`:
- Around line 2886-2891: The createSession function currently only preserves
gatewayName from overrides.metadata, causing fromDockerfile to be lost when
saving sessions; modify createSession to include fromDockerfile in the returned
metadata object (use overrides.metadata?.fromDockerfile || null) alongside
gatewayName, and update filterSafeUpdates so it does not strip fromDockerfile
(ensure metadata filtering preserves metadata.gatewayName and
metadata.fromDockerfile) so subsequent updateSession calls retain the
fromDockerfile value.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 08732457-737c-4eb7-91ac-293393932827

📥 Commits

Reviewing files that changed from the base of the PR and between aa4084e and 06cc2db.

📒 Files selected for processing (3)
  • bin/lib/onboard.js
  • docs/reference/commands.md
  • test/onboard.test.js
✅ Files skipped from review due to trivial changes (1)
  • docs/reference/commands.md

adityanjothi and others added 2 commits March 29, 2026 13:28
- createSession now includes fromDockerfile in metadata alongside gatewayName
  via a buildSessionMetadata helper (keeps createSession within complexity limit)
- filterSafeUpdates preserves metadata.fromDockerfile so updateSession
  calls do not silently drop the recorded Dockerfile path
fix(onboard): persist fromDockerfile through session create and update
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
bin/lib/onboard-session.js (1)

373-378: Consider adding type validation for fromDockerfile.

The gatewayName field is validated as a string (line 373), but fromDockerfile is not type-checked before being stored. This could allow non-string truthy values to pass through. For consistency and defensive coding, consider validating the type:

♻️ Suggested change
   if (isObject(updates.metadata) && typeof updates.metadata.gatewayName === "string") {
     safe.metadata = {
       gatewayName: updates.metadata.gatewayName,
-      fromDockerfile: updates.metadata.fromDockerfile || null,
+      fromDockerfile: typeof updates.metadata.fromDockerfile === "string" ? updates.metadata.fromDockerfile : null,
     };
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@bin/lib/onboard-session.js` around lines 373 - 378, The metadata assignment
allows non-string values for fromDockerfile; update the block that sets
safe.metadata (checking isObject(updates.metadata) and typeof
updates.metadata.gatewayName === "string") to also validate
updates.metadata.fromDockerfile is a string before assigning it, otherwise set
fromDockerfile to null — i.e., use the existing safe.metadata/gatewayName logic
and apply a typeof check for fromDockerfile so only string values are stored.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@bin/lib/onboard-session.js`:
- Around line 373-378: The metadata assignment allows non-string values for
fromDockerfile; update the block that sets safe.metadata (checking
isObject(updates.metadata) and typeof updates.metadata.gatewayName === "string")
to also validate updates.metadata.fromDockerfile is a string before assigning
it, otherwise set fromDockerfile to null — i.e., use the existing
safe.metadata/gatewayName logic and apply a typeof check for fromDockerfile so
only string values are stored.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e51e24c0-487a-4950-8185-b245e8537c06

📥 Commits

Reviewing files that changed from the base of the PR and between 06cc2db and ecc59d1.

📒 Files selected for processing (1)
  • bin/lib/onboard-session.js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant