Skip to content

feat(inference): persist serving profile lifecycle provenance - #8401

Open
prekshivyas wants to merge 16 commits into
feat/fixed-local-serving-profilesfrom
agent/8384-serving-profile-ux
Open

feat(inference): persist serving profile lifecycle provenance#8401
prekshivyas wants to merge 16 commits into
feat/fixed-local-serving-profilesfrom
agent/8384-serving-profile-ux

Conversation

@prekshivyas

@prekshivyas prekshivyas commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds discoverable serving profiles and carries the exact selected catalog preset and recipe identity through onboarding review, resume, status, diagnostics, recovery, and uninstall. Existing automatic defaults remain unchanged when no profile is selected, and legacy sessions and unlabeled runtimes remain compatible.

Related Issues

Fixes #8246
Fixes #8384
Parent epic: #8379

Changes

  • Add human and JSON profiles list discovery with stable IDs, display names, backend, model, topology, selection mode, support state, download estimates, and compatibility reasons.
  • Add model-independent onboard --profile <id-or-display-name> selection, with terminal-safe errors for unknown, ambiguous, disabled, incompatible, or conflicting selections before effects.
  • Show the selected profile, recipe, support state, runtime image, and download estimates on the review screen.
  • Persist secret-free catalog, preset, recipe, model, runtime-image, and digest provenance in onboarding sessions and sandbox registry records.
  • Automatically reuse the recorded profile on resume, reject selection or catalog drift before effects, and preserve legacy resume behavior when provenance is absent.
  • Expose profile and recipe provenance in human status, JSON status, and debug diagnostics.
  • Bind host-local vLLM recovery and uninstall to an owner-only receipt containing exact container, authentication, and serving-profile identity; retain compatibility with legacy authenticated unlabeled runtimes.
  • Build on the fixed local vLLM and llama.cpp catalog profiles from feat(inference): add fixed local serving profiles #8399; this PR is stacked on that dependency and carries only the generic lifecycle/provenance delta.
  • Document discovery, review, resume, status, recovery, uninstall, and legacy behavior.

Station catalog migration and physical hardware qualification remain separately tracked under the parent epic.

Type of Change

  • Code change with doc updates
  • Code change (feature, bug fix, or refactor)
  • Doc only (prose changes, no code sample modifications)
  • Doc only (includes code sample changes)

Quality Gates

  • Tests added or updated for changed behavior
  • Existing tests cover changed behavior — justification:
  • Tests not applicable — justification:
  • Docs updated for user-facing behavior changes
  • Docs not applicable — justification:
  • Sensitive paths changed (security, policy, credentials, preflight, onboarding, inference, runner, sandbox, or messaging)
  • Sensitive-path review completed or maintainer-approved waiver recorded — reviewer/approval link/justification: Local implementation and independent final review covered secret-free provenance, environment restoration, authenticated loopback runtime ownership, recovery drift rejection, and fail-closed uninstall boundaries.
  • Non-success, skipped, or missing CI check accepted by maintainer — check name, approval link, and follow-up issue:

Documentation Writer Review

  • Documentation writer subagent reviewed the completed changes
  • Result: docs-updated
  • Evidence: The isolated implementation and documentation, reviewed against exact feat(inference): add fixed local serving profiles #8399 base 7c25b6175 for profile discovery, review, immutable provenance, resume drift rejection, status/diagnostics, authenticated recovery, legacy compatibility, and ownership-aware uninstall were reviewed. npm run docs, focused profile tests, git diff --check, and privacy checks passed; no private or unannounced model details or credentials were added.
  • Agent: Codex Desktop

DGX Station Hardware Evidence

Verification

  • PR description includes a Signed-off-by: line and every authored commit is signed and includes DCO sign-off
  • Normal pre-commit and pre-push hooks passed
  • Targeted behavior tests pass — 13 focused suites passed with 317 tests, including discovery, selection, review, session persistence, resume drift, registry/status, recovery receipts, and uninstall ownership
  • Applicable broad gates passed — repository checks, CLI typecheck, catalog compile/check, documentation build, and diff checks
  • Quality Gates section completed with required justifications or waivers
  • No secrets, API keys, or credentials committed
  • npm run docs builds without errors (Fern reported 2 existing unprinted warnings)
  • Doc pages follow the documentation style guide
  • New doc pages include SPDX header and frontmatter (no new pages)

Signed-off-by: Prekshi Vyas prekshiv@nvidia.com

Summary by CodeRabbit

  • New Features

    • Added profiles:list to discover installed serving profiles, view compatibility, and output results as text or JSON.
    • Added managed local-model onboarding for vLLM and llama.cpp on supported DGX Spark systems.
    • Added profile selection by ID or name, validation, resume tracking, and configuration details.
    • Added secure runtime installation, recovery, and ownership-aware cleanup for local models.
    • Added host-local vLLM recovery and authenticated runtime management.
  • Documentation

    • Expanded setup, installer, local inference, host-state, onboarding, and uninstall guidance.
  • Bug Fixes

    • Improved validation for conflicting options, incompatible profiles, catalog changes, and unsafe cleanup scenarios.

Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds discoverable serving profiles, generic onboarding selection, feature-gated DGX Spark local-model runtimes, provenance persistence, secure runtime recovery, ownership-aware cleanup, and related documentation.

Changes

Managed serving profiles

Layer / File(s) Summary
Catalog contracts and profile discovery
managed-inference/..., src/lib/inference/serving/..., src/commands/profiles/list.ts
Adds profile metadata, recipes, compatibility evaluation, provenance, rendering, and profiles list output.
Profile selection and onboarding
src/lib/onboard/..., scripts/install.sh
Adds --profile, local-model feature gates, runtime selection, validation, environment scoping, resume checks, and dedicated onboarding.
Managed local runtimes
src/lib/inference/llama-cpp/..., src/lib/inference/vllm.ts, src/lib/inference/serving/vllm-host-local-lifecycle.ts
Adds secure llama.cpp installation, authenticated host-local vLLM lifecycle handling, receipts, recovery, and catalog-bound runtime state.
Cleanup and state integration
src/lib/inference/local-model-profile/..., src/lib/actions/uninstall/..., src/lib/state/..., src/lib/actions/sandbox/...
Adds ownership-verified cleanup, uninstall wiring, cache handling, provenance persistence, and status reporting.
Documentation and validation
docs/..., test/..., ci/...
Documents profile and runtime behavior and updates focused tests and budgets.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

  • NVIDIA/NemoClaw#8249: Introduces host-local vLLM catalog and lifecycle code extended by this change.
  • NVIDIA/NemoClaw#8399: Adds fixed local serving profiles extended here with discovery, provenance, onboarding, and lifecycle validation.
  • NVIDIA/NemoClaw#8338: Shares the llama.cpp host-local runtime contract extended by this change.

Suggested labels: feature, area: inference, area: onboarding, area: local-models, area: providers, area: docs

Suggested reviewers: senthilr-nv, ericksoa, cv

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The pull request includes an unrelated whitespace-only edit in test/onboard-selection.test.ts. Revert the blank-line removal in test/onboard-selection.test.ts or explain why it is required for this pull request.
Docstring Coverage ⚠️ Warning Docstring coverage is 10.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement catalog-backed discovery, selection, validation, provenance persistence, lifecycle handling, documentation, and tests for [#8246] and [#8384].
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main lifecycle provenance change for inference serving profiles.
✨ Finishing Touches 💡 2
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch agent/8384-serving-profile-ux

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

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

@github-code-quality

github-code-quality Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Overview

Languages: TypeScript

TypeScript / code-coverage/plugin

The overall coverage in commit b235cfc in the agent/8384-serving-p... branch remains at 96%, unchanged from commit 422fe95 in the feat/fixed-local-ser... branch.

TypeScript / code-coverage/cli

The overall coverage in commit b235cfc in the agent/8384-serving-p... branch remains at 82%, unchanged from commit 8227087 in the feat/fixed-local-ser... branch.

Show a code coverage summary of the most impacted files.
File feat/fixed-local-ser... 8227087 agent/8384-serving-p... b235cfc +/-
src/lib/inference/vllm.ts 89% 88% -1%
src/lib/actions...all/run-plan.ts 84% 85% +1%
src/lib/inferen...ter-registry.ts 84% 89% +5%
src/lib/onboard...le/onboarder.ts 0% 79% +79%
src/lib/inferen...ed-installer.ts 0% 84% +84%
src/lib/inferen...file/cleanup.ts 0% 85% +85%
src/lib/inferen...al-lifecycle.ts 0% 86% +86%
src/lib/inferen...profile-list.ts 0% 86% +86%
src/lib/inferen...e-provenance.ts 0% 86% +86%
src/lib/onboard...profile/plan.ts 0% 90% +90%

Updated August 06, 2026 01:33 UTC

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

PR Review Advisor — No blocking findings reported

Advisor assessment: No blocking advisor findings reported
Next action: No advisor follow-up needed.
Findings: 0 blockers · 0 warnings · 0 suggestions

Model lanes

  • GPT-5.6 Terra (primary): Completed · high confidence · 0 blockers · 0 warnings · 0 suggestions
  • Nemotron 3 Ultra (second opinion): Completed · low confidence · 0 blockers · 0 warnings · 0 suggestions
  • Model comparison: normalized findings match; normalized terminology decisions differ; normalized E2E selections differ; severity counts match.
1 additional E2E selection from the second opinion

Advisory only. The primary lane did not select these E2E jobs or targets.

  • full-e2e: The completed second-opinion lane identified E2E coverage that the primary lane omitted.

Second-opinion terminology and E2E selections are advisory. They do not change the primary assessment or E2E / PR Gate.

4 semantic terminology decisions

Terminology decisions are advisory. They affect the assessment only when a separate finding identifies concrete semantic impact.

  • established — serving profile at docs/inference/set-up-vllm.mdx:85: Continue to use serving profile for catalog-selected managed inference selections.
  • justified — support state at docs/reference/commands.mdx:246: Continue to show support state separately from compatibility and selection mode.
  • define — profile provenance at docs/reference/commands.mdx:339: Keep the adjacent explanation of the recorded fields and resume behavior when this term appears on user-facing surfaces.
  • established — ownership receipt at src/lib/inference/serving/vllm-host-local-lifecycle.ts:222: Continue to use ownership receipt for validated lifecycle ownership records.

E2E guidance

Advisory only. E2E / PR Gate selects and runs jobs independently.

Recommended E2E: cloud-onboard, inference-routing, network-policy, onboard-repair, onboard-resume, rebuild-openclaw, state-backup-restore, ubuntu-repo-docker-post-reboot-recovery

3 optional E2E recommendations
  • spark-install
  • vllm-docker-storage
  • llama-cpp-dgx-spark-qualification

Workflow run details

This automated review informs maintainers. Warnings and suggestions do not require a response. A maintainer decides whether to merge.

Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/commands/profiles/list.ts`:
- Around line 10-29: Update ProfilesListCommand to record maintainer approval
for the serving-profile product surface, including ownership, compatibility,
security, lifecycle, and validation expectations associated with issue `#8384`. If
those requirements cannot be established in the command metadata or surrounding
implementation, route the surface through Community Solutions instead of leaving
it undocumented.

In `@src/lib/inference/serving/profile-list.test.ts`:
- Around line 11-12: Update the test around listServingProfiles to mock or
inject deterministic implementations for os.hostname(),
createHostReadinessReport(), and resolveManagedInferenceServing(), preventing CI
host state from affecting results. Add assertions covering both compatible and
incompatible profile entries while preserving the existing catalog-loading
setup.

In `@src/lib/onboard/command.ts`:
- Around line 180-227: Move selectedServingPreset and
validateServingProfileCompatibility out of the onboarding command into the
serving feature module, alongside catalog discovery and compatibility
evaluation. Expose a serving-module API that resolves the requested ID/display
name, handles unknown, ambiguous, and disabled profiles, and returns or rejects
incompatible profiles. Update the onboarding command to use that API while
retaining only input handling and validateServingProfileConflicts
environment-policy checks.
- Around line 365-378: The environment handling in runOnboardCommand and
applyServingProfileEnvironment must use one consistent environment object.
Select the caller-provided deps.env when available, otherwise process.env, then
pass that object through option validation, applyPortableEnvironment,
applyServingProfileEnvironment, and restoration so validation and onboarding
observe the same variables.
🪄 Autofix

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: Enterprise

Run ID: 542a9fb7-6a34-4c86-8a5e-e19709014bd3

📥 Commits

Reviewing files that changed from the base of the PR and between b148bc1 and 888d557.

📒 Files selected for processing (14)
  • ci/source-architecture-budget.json
  • docs/inference/set-up-vllm.mdx
  • docs/reference/commands.mdx
  • managed-inference/presets/llama-cpp.dgx-spark-gb10.single.nemotron-3-nano-30b-a3b.yaml
  • managed-inference/presets/vllm.dgx-spark-gb10.dual.deepseek-v4-flash-0731.yaml
  • managed-inference/schemas/preset.schema.json
  • src/commands/profiles/list.ts
  • src/lib/inference/serving/profile-list.test.ts
  • src/lib/inference/serving/profile-list.ts
  • src/lib/inference/serving/types.ts
  • src/lib/onboard/command-support.ts
  • src/lib/onboard/command.test.ts
  • src/lib/onboard/command.ts
  • test/package-contract/cli/command-registry.test.ts

Comment thread src/commands/profiles/list.ts
Comment thread src/lib/inference/serving/profile-list.test.ts Outdated
Comment thread src/lib/onboard/command.ts Outdated
Comment thread src/lib/onboard/command.ts Outdated
Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/onboard/agents-manifest.ts`:
- Around line 182-187: The applyAgentsManifestEnv flow must preserve the
caller’s original NEMOCLAW_EXTRA_AGENTS_JSON value. In runOnboardCommand,
capture that value before invoking applyAgentsManifestEnv, then restore it in
the existing finally cleanup, deleting the property when it was previously
unset.
🪄 Autofix

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: Enterprise

Run ID: 907fd589-0441-4a54-a4ed-f84e1afe109c

📥 Commits

Reviewing files that changed from the base of the PR and between d7ab743 and 39a8e6f.

📒 Files selected for processing (5)
  • src/lib/inference/serving/profile-list.test.ts
  • src/lib/inference/serving/profile-list.ts
  • src/lib/onboard/agents-manifest.ts
  • src/lib/onboard/command.test.ts
  • src/lib/onboard/command.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/lib/inference/serving/profile-list.test.ts
  • src/lib/onboard/command.ts

Comment thread src/lib/onboard/agents-manifest.ts
Comment thread src/lib/onboard/command.ts Fixed
Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Comment thread src/lib/onboard/command.ts Fixed

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/onboard/command.ts`:
- Around line 365-367: Move the applyAgentsManifestEnv setup for
options.agentsManifest inside the existing try block so manifest-load failures
still execute finally; initialize experimentalProfile and servingProfile
restoration callbacks to no-ops before entering try, preserving cleanup of any
previously applied values in deps.env. Add a regression test covering an
unavailable or invalid manifest and verifying environment restoration.
🪄 Autofix

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: Enterprise

Run ID: 6680dd1e-d651-47f0-9848-fd2656fd5918

📥 Commits

Reviewing files that changed from the base of the PR and between 39a8e6f and 12468a7.

📒 Files selected for processing (2)
  • src/lib/onboard/command.test.ts
  • src/lib/onboard/command.ts

Comment thread src/lib/onboard/command.ts
Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
…m/NVIDIA/NemoClaw into agent/8384-serving-profile-ux

# Conflicts:
#	src/lib/actions/onboard.ts
Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@prekshivyas prekshivyas changed the title feat(inference): add serving profile discovery feat(inference): persist serving profile lifecycle provenance Aug 6, 2026
Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
@prekshivyas

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@prekshivyas
prekshivyas changed the base branch from main to feat/fixed-local-serving-profiles August 6, 2026 00:54

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 8

🧹 Nitpick comments (12)
src/lib/inference/serving/adapter-registry.ts (1)

374-381: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Separate the invalid-name message from the owned-name message.

The condition on lines 375-378 fails for two distinct reasons: an environment name that does not match SAFE_ENVIRONMENT_NAME, and a name that collides with HOST_LOCAL_MATERIALIZER_OWNED_ENVIRONMENT. Both return "host-local vLLM recipe overrides a materializer-owned environment value". A recipe author who declares hf_home in lowercase receives an override message that does not describe the actual defect.

♻️ Proposed split of the two failure reasons
-  if (
-    Object.keys(recipe.spec.runtime.environment).some(
-      (name) =>
-        !SAFE_ENVIRONMENT_NAME.test(name) || HOST_LOCAL_MATERIALIZER_OWNED_ENVIRONMENT.has(name),
-    )
-  ) {
+  if (Object.keys(runtime.environment).some((name) => !SAFE_ENVIRONMENT_NAME.test(name))) {
+    return "host-local vLLM environment names must match the registered format";
+  }
+  if (
+    Object.keys(runtime.environment).some((name) =>
+      HOST_LOCAL_MATERIALIZER_OWNED_ENVIRONMENT.has(name),
+    )
+  ) {
     return "host-local vLLM recipe overrides a materializer-owned environment value";
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/inference/serving/adapter-registry.ts` around lines 374 - 381, Update
the validation around the recipe environment names to distinguish invalid names
from materializer-owned names: return an invalid-environment-name message when a
name fails SAFE_ENVIRONMENT_NAME, and retain the override message only for names
in HOST_LOCAL_MATERIALIZER_OWNED_ENVIRONMENT. Preserve the existing validation
behavior for valid, non-owned names.
src/lib/inference/serving/resolver.test.ts (1)

99-113: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use satisfies for the preset fixture and record why non-readiness requirements are dropped.

Line 112 casts the preset with as ManagedInferenceServingPreset, while line 98 uses satisfies for the recipe. The cast suppresses type checking, so a future shape change in ManagedInferenceServingPreset will not fail this fixture at compile time.

Lines 106-108 keep only requirements that contain a readiness key. Any non-readiness requirement in the shipped preset, such as a feature gate, disappears from the fixture. The explicit-selection tests at lines 283 and 304 then exercise a preset that is easier to satisfy than the shipped one. Add a short comment stating that the filter is intentional and that requirement-evaluation coverage lives in the requirements-not-met tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/inference/serving/resolver.test.ts` around lines 99 - 113, The preset
fixture should use a type-checking `satisfies ManagedInferenceServingPreset`
expression instead of an `as` cast, preserving compile-time validation like the
recipe fixture. In the fixture-building code around `sourcePreset`, add a brief
comment explaining that filtering to readiness requirements is intentional and
that requirement-evaluation coverage is provided by the requirements-not-met
tests.
src/lib/onboard/command.test.ts (1)

124-127: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the as never session doubles with a typed session shape.

Lines 125, 140, 150, and 158 build the loadSession double with as never. never is assignable to every type, so the compiler accepts any return shape. If ResolveOnboardOptionsDeps["loadSession"] changes, these tests keep compiling while asserting against a shape that production no longer produces.

loadSession is declared in src/lib/onboard/command.ts as () => { servingProfileProvenance?: ServingProfileProvenance | null } | null. The literals already match that type, so the cast can be removed. Use satisfies if the surrounding call site needs the wider session type.

♻️ Proposed removal of the type-check bypass
-        loadSession: () => ({ servingProfileProvenance: recorded }) as never,
+        loadSession: () => ({ servingProfileProvenance: recorded }),
-      resolve({ resume: true }, { loadSession: () => ({}) as never }).servingProfile,
+      resolve({ resume: true }, { loadSession: () => ({}) }).servingProfile,

Also applies to: 148-158

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/onboard/command.test.ts` around lines 124 - 127, Replace the `as
never` casts on the `loadSession` doubles in the affected tests with the
declared session shape `{ servingProfileProvenance?: ServingProfileProvenance |
null } | null`. Update the `loadSession` fixtures around the visible dependency
setup, using `satisfies` only if needed for contextual typing, while preserving
their existing values and behavior.

Source: Coding guidelines

src/lib/inference/serving/profile-provenance.test.ts (1)

19-26: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert every provenance field named by this test.

The assertion does not check model, runtimeImage, estimatedImageDownloadBytes, or estimatedModelDownloadBytes. A valid but incorrect value for any of these fields passes both parsing and current-catalog validation because both operations consume the same generated object.

Compare these fields with the resolved recipe and runtime metadata from catalog.

As per path instructions, “Review tests for behavioral confidence rather than implementation lock-in.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/inference/serving/profile-provenance.test.ts` around lines 19 - 26,
The provenance test currently validates only a subset of fields, allowing
incorrect generated values to pass. Extend the assertions around
parseServingProfileProvenance and assertServingProfileProvenanceCurrent to
compare model, runtimeImage, estimatedImageDownloadBytes, and
estimatedModelDownloadBytes against the resolved recipe and runtime metadata
from catalog, while preserving the existing behavioral assertions.

Source: Path instructions

src/lib/inference/vllm-models.ts (1)

512-513: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Share one port constant between the serve command and recovery.

This list hardcodes --port 8000. src/lib/inference/serving/vllm-host-local-lifecycle.ts line 25 declares HOST_LOCAL_VLLM_PORT = 8000 independently, and line 312 of that file rejects any container whose published port differs. The two literals must stay equal, otherwise recovery silently stops matching the managed container. Export one constant and use it in both places.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/inference/vllm-models.ts` around lines 512 - 513, Replace the
hardcoded 8000 in the vLLM serve command’s port arguments with an exported
shared port constant, then import and reuse that constant in the local lifecycle
module’s HOST_LOCAL_VLLM_PORT definition. Ensure both serving and recovery
reference the same value so published-port validation remains consistent.
src/lib/inference/llama-cpp/managed-installer.ts (1)

582-582: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Explain the discarded receipt read.

The return value of readPrivateRegularFile is unused here. The call exists to fail closed when an existing runtime.json is not an owner-only regular file. Add a short comment so a later refactor does not delete the call as dead code.

♻️ Proposed comment
+    // Fail closed when a prior receipt exists with unsafe ownership or permissions.
     readPrivateRegularFile(path.join(privateStateDir, MANAGED_LLAMA_CPP_RUNTIME_RECEIPT_FILE));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/inference/llama-cpp/managed-installer.ts` at line 582, Add a short
explanatory comment immediately before the readPrivateRegularFile call in the
managed installer, stating that the read intentionally validates the existing
runtime receipt as an owner-only regular file and fails closed when validation
fails. Keep the call and its discarded return value unchanged.
src/lib/inference/llama-cpp/managed-installer.test.ts (1)

118-123: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the redundant vi.unstubAllEnvs() call.

The cli Vitest project already enables unstubEnvs, so environment stubs are restored automatically. Keep only the temporary-directory cleanup, which Vitest does not manage.

♻️ Proposed cleanup
 afterEach(() => {
-  vi.unstubAllEnvs();
   for (const directory of temporaryDirectories.splice(0)) {
     fs.rmSync(directory, { force: true, recursive: true });
   }
 });

Based on learnings: Vitest files under src run in the cli project, which enables clearMocks, restoreMocks, unstubEnvs, and unstubGlobals; teardown should only clean resources Vitest does not manage.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/inference/llama-cpp/managed-installer.test.ts` around lines 118 -
123, Remove the redundant vi.unstubAllEnvs() call from the afterEach teardown
and retain the temporaryDirectories cleanup loop, since Vitest’s cli project
already restores environment stubs automatically.

Source: Learnings

src/lib/inference/vllm.test.ts (1)

209-213: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

The capture stub now depends on another mock's call history.

The dockerCapture implementation branches on mocks.dockerRunDetached.mock.calls.length > 0. Two effects follow. First, the stub couples to an unrelated mock rather than to the inspected command. Second, every container capture after launch bypasses ownershipHandlers, so the "Unexpected extra ambient vLLM ownership inspection" guard at line 201 can no longer fire once a container has been launched. That reduces the protection the guard previously gave.

Consider driving the running state from an explicit flag that the harness sets, and keep the ownership queue authoritative for pre-launch inspections.

As per path instructions: "Flag copied production algorithms, broad mocks that bypass the behavior under test, and conditionals that make a test pass without exercising its claim."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/inference/vllm.test.ts` around lines 209 - 213, The dockerCapture
stub should not infer container state from dockerRunDetached mock history or
bypass ownership inspection after launch. Add an explicit harness state flag
that the launch setup updates, use it only to report the expected running
container state, and keep ownershipHandlers authoritative for all pre-launch
container captures so the unexpected-inspection guard remains effective.

Source: Path instructions

src/lib/inference/serving/vllm-managed-support.ts (1)

17-21: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use one import path for each re-exported host-local vLLM function.

Import recoverHostLocalManagedVllmEndpoint from vllm-managed-support.ts in local.ts. Keep direct lifecycle imports only for symbols that the barrel does not export. This keeps module mocks aligned with production imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/inference/serving/vllm-managed-support.ts` around lines 17 - 21,
Update local.ts to import recoverHostLocalManagedVllmEndpoint from the
vllm-managed-support barrel instead of importing it directly from
vllm-host-local-lifecycle.js. Retain direct lifecycle imports only for functions
not re-exported by the barrel, ensuring each re-exported host-local vLLM
function uses a single import path.
src/lib/actions/uninstall/all-gateway-ports.test.ts (1)

216-220: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align the mock payload with the real result shape.

runUninstallPlan reads only result.status from runLocalModelRuntimeCleanup, so this stdout is unused. The payload also uses skipped, while cleanupLocalModelRuntimes returns removed and preserved. A future reader can take this as the contract. Drop the stdout payload or use the real field names.

♻️ Proposed simplification
-        runLocalModelRuntimeCleanup: () => ({
-          status: 0,
-          stdout: JSON.stringify({ ok: true, removed: [], skipped: [] }),
-          stderr: "",
-        }),
+        runLocalModelRuntimeCleanup: () => ({ status: 0, stdout: "", stderr: "" }),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/actions/uninstall/all-gateway-ports.test.ts` around lines 216 - 220,
Update the runLocalModelRuntimeCleanup mock used by runUninstallPlan to match
the real result contract: remove the unused stdout payload and replace the
skipped field with preserved if the payload remains. Keep status: 0 and the
existing cleanup behavior unchanged.
src/lib/inference/local-model-profile/cleanup.test.ts (1)

142-175: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add the negative case this title claims.

The title says removal happens "only when key and fingerprint match", but the test asserts the matching case alone. The mismatch branch at cleanup.ts Lines 387-394 is not covered by any test in this file. Add a case where the container label fingerprint or the container VLLM_API_KEY value differs from the persisted key, then assert ok: false and that forceRm is not called.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/inference/local-model-profile/cleanup.test.ts` around lines 142 -
175, Add a negative test alongside the matching case for
cleanupLocalModelRuntimes, using a managed host-local vLLM container whose
authentication fingerprint or VLLM_API_KEY differs from the persisted key.
Assert the result has ok: false and verify the forceRm mock is not called, while
preserving the existing positive-case coverage.
src/lib/actions/uninstall/run-plan.ts (1)

1440-1444: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Share the managed llama.cpp state directory name.

This code hardcodes "managed-llama-cpp". src/lib/inference/local-model-profile/cleanup.ts Line 476 builds the same path from the same literal. If one side changes, uninstall silently stops invoking cleanup while the cleanup module still expects state. Export the directory name next to the other MANAGED_LLAMA_CPP_* constants and use it in both places.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/actions/uninstall/run-plan.ts` around lines 1440 - 1444, Export a
shared managed llama.cpp state-directory constant alongside the existing
MANAGED_LLAMA_CPP_* constants, then replace the "managed-llama-cpp" literal in
run-plan’s hasLlamaState path and cleanup.ts’s corresponding path construction
with that constant. Update imports as needed while preserving existing cleanup
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/install.sh`:
- Around line 5033-5045: Update the local-model setup around the
NEMOCLAW_ENABLE_LOCAL_MODEL_PROFILE case so invalid runtimes on non-DGX Spark
hosts are rejected before NEMOCLAW_NO_EXPRESS is exported, or defer that export
until after the Station resume boundary. Preserve the existing valid vllm and
llama-cpp behavior while ensuring maybe_offer_express_install() does not clear
resume state for rejected selections.

In `@src/lib/inference/local-model-profile/cleanup.ts`:
- Around line 491-504: Set timeout: 10_000 on the Docker invocations in the
cleanup flow, including deps.run(["info"], ...) within the main try block and
the removal calls in cleanupHostLocalVllm and cleanupLlamaCpp. Preserve the
existing options and failure-handling behavior so timeout results are processed
normally.

In `@src/lib/inference/local.ts`:
- Around line 548-552: Update the hostLocalEndpoint selection to use the
injected recoverHostLocalManagedVllmEndpointImpl when provided, otherwise fall
back directly to recoverHostLocalManagedVllmEndpoint, without checking
getManagedBaseUrlImpl. Preserve injection of () => null as the mechanism for
tests that need to suppress recovery.

In `@src/lib/inference/serving/adapter-registry.ts`:
- Around line 99-114: Update the argument ownership definitions used by
buildVllmServeCommand and modelArguments to include "--model" in both host-local
materializer-owned sets. This must reject recipe-defined --model arguments and
prevent forwarding them while preserving the positional model.id emitted by
buildVllmServeCommand.

In `@src/lib/inference/serving/resolver.ts`:
- Around line 155-168: Update versionAtLeast to compare version components
exactly without converting them to Number; use BigInt or normalized digit-string
comparison after validation so values beyond Number.MAX_SAFE_INTEGER remain
distinguishable. Preserve missing components as zero and the existing
lexicographic component ordering, and add a boundary test covering adjacent
oversized components to ensure readiness qualification fails when actual is
smaller.

In `@src/lib/inference/vllm.ts`:
- Around line 2027-2046: Update the failed auth-boundary branch in the
managed-auth flow of the vLLM install logic to force-remove the container with
dockerForceRm using the start container identifier, then delete
HOST_LOCAL_VLLM_RUNTIME_RECEIPT_FILE before returning failure. Replace the
existing dockerStop cleanup so no stale container or receipt remains.

In `@src/lib/onboard/command.ts`:
- Around line 257-262: Update the comparison in the requested/current provenance
validation block to use node:util’s isDeepStrictEqual instead of comparing
JSON.stringify results. Preserve the existing mismatch failure behavior and
message, and reuse the existing utility import pattern used by
profile-provenance.ts.

In `@test/install-local-model-profile.test.ts`:
- Around line 22-31: Update the environment setup in the test harness around
INSTALLER_UNDER_TEST to clear every NEMOCLAW profile-selection variable read by
the installer, including serving preset, vLLM model and extra arguments, and
managed-cluster peer settings; then overlay only the scenario-specific values
from env so parent-process values cannot affect the branch under test.

---

Nitpick comments:
In `@src/lib/actions/uninstall/all-gateway-ports.test.ts`:
- Around line 216-220: Update the runLocalModelRuntimeCleanup mock used by
runUninstallPlan to match the real result contract: remove the unused stdout
payload and replace the skipped field with preserved if the payload remains.
Keep status: 0 and the existing cleanup behavior unchanged.

In `@src/lib/actions/uninstall/run-plan.ts`:
- Around line 1440-1444: Export a shared managed llama.cpp state-directory
constant alongside the existing MANAGED_LLAMA_CPP_* constants, then replace the
"managed-llama-cpp" literal in run-plan’s hasLlamaState path and cleanup.ts’s
corresponding path construction with that constant. Update imports as needed
while preserving existing cleanup behavior.

In `@src/lib/inference/llama-cpp/managed-installer.test.ts`:
- Around line 118-123: Remove the redundant vi.unstubAllEnvs() call from the
afterEach teardown and retain the temporaryDirectories cleanup loop, since
Vitest’s cli project already restores environment stubs automatically.

In `@src/lib/inference/llama-cpp/managed-installer.ts`:
- Line 582: Add a short explanatory comment immediately before the
readPrivateRegularFile call in the managed installer, stating that the read
intentionally validates the existing runtime receipt as an owner-only regular
file and fails closed when validation fails. Keep the call and its discarded
return value unchanged.

In `@src/lib/inference/local-model-profile/cleanup.test.ts`:
- Around line 142-175: Add a negative test alongside the matching case for
cleanupLocalModelRuntimes, using a managed host-local vLLM container whose
authentication fingerprint or VLLM_API_KEY differs from the persisted key.
Assert the result has ok: false and verify the forceRm mock is not called, while
preserving the existing positive-case coverage.

In `@src/lib/inference/serving/adapter-registry.ts`:
- Around line 374-381: Update the validation around the recipe environment names
to distinguish invalid names from materializer-owned names: return an
invalid-environment-name message when a name fails SAFE_ENVIRONMENT_NAME, and
retain the override message only for names in
HOST_LOCAL_MATERIALIZER_OWNED_ENVIRONMENT. Preserve the existing validation
behavior for valid, non-owned names.

In `@src/lib/inference/serving/profile-provenance.test.ts`:
- Around line 19-26: The provenance test currently validates only a subset of
fields, allowing incorrect generated values to pass. Extend the assertions
around parseServingProfileProvenance and assertServingProfileProvenanceCurrent
to compare model, runtimeImage, estimatedImageDownloadBytes, and
estimatedModelDownloadBytes against the resolved recipe and runtime metadata
from catalog, while preserving the existing behavioral assertions.

In `@src/lib/inference/serving/resolver.test.ts`:
- Around line 99-113: The preset fixture should use a type-checking `satisfies
ManagedInferenceServingPreset` expression instead of an `as` cast, preserving
compile-time validation like the recipe fixture. In the fixture-building code
around `sourcePreset`, add a brief comment explaining that filtering to
readiness requirements is intentional and that requirement-evaluation coverage
is provided by the requirements-not-met tests.

In `@src/lib/inference/serving/vllm-managed-support.ts`:
- Around line 17-21: Update local.ts to import
recoverHostLocalManagedVllmEndpoint from the vllm-managed-support barrel instead
of importing it directly from vllm-host-local-lifecycle.js. Retain direct
lifecycle imports only for functions not re-exported by the barrel, ensuring
each re-exported host-local vLLM function uses a single import path.

In `@src/lib/inference/vllm-models.ts`:
- Around line 512-513: Replace the hardcoded 8000 in the vLLM serve command’s
port arguments with an exported shared port constant, then import and reuse that
constant in the local lifecycle module’s HOST_LOCAL_VLLM_PORT definition. Ensure
both serving and recovery reference the same value so published-port validation
remains consistent.

In `@src/lib/inference/vllm.test.ts`:
- Around line 209-213: The dockerCapture stub should not infer container state
from dockerRunDetached mock history or bypass ownership inspection after launch.
Add an explicit harness state flag that the launch setup updates, use it only to
report the expected running container state, and keep ownershipHandlers
authoritative for all pre-launch container captures so the unexpected-inspection
guard remains effective.

In `@src/lib/onboard/command.test.ts`:
- Around line 124-127: Replace the `as never` casts on the `loadSession` doubles
in the affected tests with the declared session shape `{
servingProfileProvenance?: ServingProfileProvenance | null } | null`. Update the
`loadSession` fixtures around the visible dependency setup, using `satisfies`
only if needed for contextual typing, while preserving their existing values and
behavior.
🪄 Autofix

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: Enterprise

Run ID: 61a38d80-fe57-4e35-918a-14af1933372b

📥 Commits

Reviewing files that changed from the base of the PR and between 12468a7 and 6c5cc6d.

📒 Files selected for processing (74)
  • ci/test-file-size-budget.json
  • docs/inference/choose-local-inference-server.mdx
  • docs/inference/set-up-vllm.mdx
  • docs/reference/commands.mdx
  • docs/reference/host-files-and-state.mdx
  • managed-inference/presets/local-model-profile.llama-cpp.spark.v1.yaml
  • managed-inference/presets/local-model-profile.vllm.spark.v1.yaml
  • managed-inference/recipes/vllm.qwen3-6-35b-a3b-nvfp4.spark-single.v1.yaml
  • managed-inference/schemas/preset.schema.json
  • scripts/install.sh
  • src/lib/actions/onboard.ts
  • src/lib/actions/sandbox/status-snapshot.ts
  • src/lib/actions/sandbox/status-text.ts
  • src/lib/actions/sandbox/status.test.ts
  • src/lib/actions/uninstall/all-gateway-ports.test.ts
  • src/lib/actions/uninstall/run-plan-local-model-profile.test.ts
  • src/lib/actions/uninstall/run-plan.ts
  • src/lib/adapters/docker/local-model-runtime.ts
  • src/lib/inference/llama-cpp/host-local-runtime.ts
  • src/lib/inference/llama-cpp/managed-installer.test.ts
  • src/lib/inference/llama-cpp/managed-installer.ts
  • src/lib/inference/local-model-profile/cleanup-entry.ts
  • src/lib/inference/local-model-profile/cleanup-path-safety.test.ts
  • src/lib/inference/local-model-profile/cleanup.test.ts
  • src/lib/inference/local-model-profile/cleanup.ts
  • src/lib/inference/local.ts
  • src/lib/inference/serving/adapter-registry.ts
  • src/lib/inference/serving/catalog-loader.test.ts
  • src/lib/inference/serving/catalog.test.ts
  • src/lib/inference/serving/catalog.ts
  • src/lib/inference/serving/host-local-vllm-selection.ts
  • src/lib/inference/serving/profile-list.test.ts
  • src/lib/inference/serving/profile-list.ts
  • src/lib/inference/serving/profile-provenance.test.ts
  • src/lib/inference/serving/profile-provenance.ts
  • src/lib/inference/serving/resolver.test.ts
  • src/lib/inference/serving/resolver.ts
  • src/lib/inference/serving/runtime-auth-fingerprint.ts
  • src/lib/inference/serving/types.ts
  • src/lib/inference/serving/vllm-host-local-lifecycle.test.ts
  • src/lib/inference/serving/vllm-host-local-lifecycle.ts
  • src/lib/inference/serving/vllm-managed-support.ts
  • src/lib/inference/vllm-models.ts
  • src/lib/inference/vllm.test.ts
  • src/lib/inference/vllm.ts
  • src/lib/onboard.ts
  • src/lib/onboard/command-agents.test.ts
  • src/lib/onboard/command-support.ts
  • src/lib/onboard/command.test.ts
  • src/lib/onboard/command.ts
  • src/lib/onboard/local-model-profile/integration.ts
  • src/lib/onboard/local-model-profile/onboarder.test.ts
  • src/lib/onboard/local-model-profile/onboarder.ts
  • src/lib/onboard/local-model-profile/plan.test.ts
  • src/lib/onboard/local-model-profile/plan.ts
  • src/lib/onboard/machine/handlers/provider-inference.ts
  • src/lib/onboard/sandbox-registration.test.ts
  • src/lib/onboard/sandbox-registration.ts
  • src/lib/onboard/session-bootstrap.test.ts
  • src/lib/onboard/session-bootstrap.ts
  • src/lib/onboard/session-updates.ts
  • src/lib/onboard/setup-nim-flow.test.ts
  • src/lib/onboard/setup-nim-flow.ts
  • src/lib/onboard/summary.test.ts
  • src/lib/onboard/summary.ts
  • src/lib/onboard/types.ts
  • src/lib/state/onboard-session.test.ts
  • src/lib/state/onboard-session.ts
  • src/lib/state/registry-normalization.test.ts
  • src/lib/state/registry.ts
  • src/lib/state/registry/persistence.ts
  • src/lib/state/registry/types.ts
  • test/install-local-model-profile.test.ts
  • test/onboard-selection.test.ts
💤 Files with no reviewable changes (1)
  • test/onboard-selection.test.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/lib/onboard/command-agents.test.ts
  • src/lib/onboard/command-support.ts
  • src/lib/inference/serving/profile-list.ts

Comment on lines +257 to +262
if (requested && JSON.stringify(requested) !== JSON.stringify(current)) {
fail(
deps,
` --profile ${requested.preset.id} does not match resumed profile ${current.preset.id}.`,
);
}

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Compare provenance records structurally, not by JSON.stringify.

Line 257 compares requested and current by serialized string. The two records reach this point by different routes. requested is built directly by servingProfileProvenance, so its key order follows the object literal in src/lib/inference/serving/profile-provenance.ts. current is the value returned by assertServingProfileProvenanceCurrent, which returns structuredClone(recorded) and therefore preserves the key order of the persisted session JSON.

Today both orders agree. If any future writer, normalizer, or persistence path reorders keys, the strings differ while the objects stay deeply equal. The user is then blocked from resuming with the same --profile they originally selected, and the error text names the same preset ID on both sides, which gives no way to diagnose the failure.

node:util already provides isDeepStrictEqual, and profile-provenance.ts uses it for the same comparison.

♻️ Proposed structural comparison
+import { isDeepStrictEqual } from "node:util";
-  if (requested && JSON.stringify(requested) !== JSON.stringify(current)) {
+  if (requested && !isDeepStrictEqual(requested, current)) {
     fail(
       deps,
       `  --profile ${requested.preset.id} does not match resumed profile ${current.preset.id}.`,
     );
   }
📝 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 (requested && JSON.stringify(requested) !== JSON.stringify(current)) {
fail(
deps,
` --profile ${requested.preset.id} does not match resumed profile ${current.preset.id}.`,
);
}
if (requested && !isDeepStrictEqual(requested, current)) {
fail(
deps,
` --profile ${requested.preset.id} does not match resumed profile ${current.preset.id}.`,
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/onboard/command.ts` around lines 257 - 262, Update the comparison in
the requested/current provenance validation block to use node:util’s
isDeepStrictEqual instead of comparing JSON.stringify results. Preserve the
existing mismatch failure behavior and message, and reuse the existing utility
import pattern used by profile-provenance.ts.

@coderabbitai coderabbitai Bot left a comment

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.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

🛑 Comments failed to post (7)
scripts/install.sh (1)

5033-5045: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Confirm the Station resume clearing path reachable from NEMOCLAW_NO_EXPRESS=1.
rg -n -C 12 'clear_station_express_resume\(\)' scripts/install.sh
rg -n -C 6 'NEMOCLAW_NO_EXPRESS' scripts/install.sh

# Confirm the call ordering between the exports in main and maybe_offer_express_install.
rg -n 'prepare_installer_host|maybe_offer_express_install|NEMOCLAW_ENABLE_LOCAL_MODEL_PROFILE' scripts/install.sh

# Confirm the local model profile requires the DGX Spark qualification.
rg -n -C 4 'dgx_spark' managed-inference/presets

Repository: NVIDIA/NemoClaw

Length of output: 14504


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the host-preparation call path and the local-profile selection path.
sed -n '4688,4710p' scripts/install.sh
sed -n '4838,4905p' scripts/install.sh
sed -n '5018,5120p' scripts/install.sh
rg -n -C 8 'LOCAL_MODEL_RUNTIME|local-model-profile|host.platform.dgx_spark|resolve.*profile|profile.*resolve' scripts managed-inference

Repository: NVIDIA/NemoClaw

Length of output: 26301


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the code that consumes the local-model feature gate and runtime.
rg -n -C 10 'NEMOCLAW_ENABLE_LOCAL_MODEL_PROFILE|NEMOCLAW_LOCAL_MODEL_RUNTIME|local-model-profile-v1|local-model-profile\.(vllm|llama-cpp)' .

# Inspect profile loading and readiness/error handling without executing repository code.
rg -n -C 12 'ServingPreset|requirements|qualification|profile.*selection|selection.*disabled|featureGate' --glob '*.ts' --glob '*.js' --glob '*.sh' --glob '*.yaml' .

# Use a read-only static verifier to report the relevant ordering and branch predicates.
python3 - <<'PY'
from pathlib import Path

p = Path("scripts/install.sh")
s = p.read_text()
needles = [
    'export NEMOCLAW_NO_EXPRESS=1',
    'prepare_installer_host',
    'maybe_offer_express_install',
    'clear_station_express_resume',
    'if [ "$platform" = "DGX Station" ]; then',
]
for needle in needles:
    print(f"{needle!r}: {[i + 1 for i, line in enumerate(s.splitlines()) if needle in line]}")

start = s.index("maybe_offer_express_install()")
end = s.index("\n}", start) + 2
body = s[start:end]
print("single_station_clear_branch:",
      'clear_station_express_resume' in body and
      'NEMOCLAW_NO_EXPRESS' in body and
      'station_dual_pair_resume_pending' in body)
PY

Repository: NVIDIA/NemoClaw

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Extract only the static-verifier results and the production consumers from the
# previous output, avoiding test fixtures and broad catalog matches.
rg -n 'single_station_clear_branch|export NEMOCLAW_NO_EXPRESS=1|prepare_installer_host|maybe_offer_express_install|clear_station_express_resume' \
  /tmp/coderabbit-shell-logs/shell-output-FE1PVE

rg -l --glob '!**/*.test.ts' --glob '!**/*.test-support.ts' \
  'NEMOCLAW_ENABLE_LOCAL_MODEL_PROFILE|NEMOCLAW_LOCAL_MODEL_RUNTIME|local-model-profile-v1' \
  src scripts

# Inspect the resolver implementation and the local-profile integration points.
for f in $(rg -l --glob '!**/*.test.ts' --glob '!**/*.test-support.ts' \
  'NEMOCLAW_ENABLE_LOCAL_MODEL_PROFILE|NEMOCLAW_LOCAL_MODEL_RUNTIME|local-model-profile-v1' src scripts); do
  echo "===== $f ====="
  rg -n -C 8 'NEMOCLAW_ENABLE_LOCAL_MODEL_PROFILE|NEMOCLAW_LOCAL_MODEL_RUNTIME|local-model-profile-v1|no-match|requirements-not-met|selection' "$f"
done

Repository: NVIDIA/NemoClaw

Length of output: 15183


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Read the local-profile planning path and the resolver result handling.
sed -n '1,190p' src/lib/onboard/local-model-profile/plan.ts
rg -n -C 12 'buildLocalModelProfilePlan|resolveManagedInferenceServing|requirements-not-met|no-match|local model profile' \
  src/lib/onboard src/lib/inference --glob '!**/*.test.ts'

# Show the exact Spark qualification in both local-model presets.
for f in managed-inference/presets/local-model-profile.*.yaml; do
  echo "===== $f ====="
  sed -n '1,45p' "$f"
done

Repository: NVIDIA/NemoClaw

Length of output: 50371


Preserve Station resume state for invalid local-model selections. With --local-model-runtime=vllm, NEMOCLAW_NO_EXPRESS=1 causes maybe_offer_express_install() to clear the single-Station resume state before the local-model onboarder rejects the non-Spark host. Reject local-model runtimes when the detected platform is not DGX Spark, or defer this export until after the Station resume boundary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/install.sh` around lines 5033 - 5045, Update the local-model setup
around the NEMOCLAW_ENABLE_LOCAL_MODEL_PROFILE case so invalid runtimes on
non-DGX Spark hosts are rejected before NEMOCLAW_NO_EXPRESS is exported, or
defer that export until after the Station resume boundary. Preserve the existing
valid vllm and llama-cpp behavior while ensuring maybe_offer_express_install()
does not clear resume state for rejected selections.
src/lib/inference/local-model-profile/cleanup.ts (1)

491-504: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Check whether the docker adapter applies a default timeout.
set -euo pipefail

fd 'run.ts|container.ts' src/lib/adapters/docker --exec cat -n

Repository: NVIDIA/NemoClaw

Length of output: 4273


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- cleanup symbols and Docker calls ---'
rg -n -C 5 'inspectOwnedResource|cleanupHostLocalVllm|cleanupLlamaCpp|runLocalModelRuntimeCleanup|deps\.run|docker' src/lib/inference/local-model-profile/cleanup.ts

printf '%s\n' '--- runner implementation and timeout semantics ---'
fd -t f . src/lib | rg '(^|/)runner(\.|/)|runner' | head -80
rg -n -C 8 'timeout|kill|spawn|ignoreError|suppressOutput' src/lib --glob '*.ts' | head -240

printf '%s\n' '--- cleanup callers ---'
rg -n -C 6 'runLocalModelRuntimeCleanup|local-model-profile/cleanup|cleanup\.ts' src --glob '*.{ts,tsx}'

Repository: NVIDIA/NemoClaw

Length of output: 32539


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- runner.ts ---'
cat -n src/lib/runner.ts

printf '%s\n' '--- cleanup implementation ---'
sed -n '60,95p;350,470p;470,530p' src/lib/inference/local-model-profile/cleanup.ts

printf '%s\n' '--- cleanup tests and timeout assertions ---'
rg -n -C 5 'timeout|info|forceRm|network.*rm|container.*removal|unresponsive|Docker is unavailable' src/lib/inference src/lib/actions/uninstall --glob '*.{ts,tsx}'

Repository: NVIDIA/NemoClaw

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- runner.ts timeout implementation ---'
rg -n -C 12 'export function run|function run|spawnSync|timeout|timedOut|status' src/lib/runner.ts

printf '%s\n' '--- cleanup implementation ---'
sed -n '60,92p' src/lib/inference/local-model-profile/cleanup.ts
sed -n '350,470p' src/lib/inference/local-model-profile/cleanup.ts
sed -n '470,525p' src/lib/inference/local-model-profile/cleanup.ts

printf '%s\n' '--- cleanup test files ---'
fd -t f . src/lib/inference/local-model-profile | sort
rg -n -C 4 'cleanupLocalModelRuntimes|deps\.run|deps\.forceRm|timeout|Docker is unavailable|removal failed' src/lib/inference/local-model-profile --glob '*.{ts,tsx}'

Repository: NVIDIA/NemoClaw

Length of output: 41757


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

cleanup = Path("src/lib/inference/local-model-profile/cleanup.ts").read_text()
runner = Path("src/lib/runner.ts").read_text()

checks = {
    "inspect calls have timeout": bool(re.search(
        r'capture\(\[kind,\s*"inspect",\s*name\],\s*\{\s*ignoreError:\s*true,\s*timeout:\s*10_000',
        cleanup, re.S)),
    "docker info has no timeout": not bool(re.search(
        r'deps\.run\(\["info"\],\s*\{[^}]*\btimeout\s*:', cleanup, re.S)),
    "host-local force removal has no timeout": not bool(re.search(
        r'deps\.forceRm\(inspected\.id,\s*\{[^}]*\btimeout\s*:', cleanup, re.S)),
    "llama.cpp force removal has no timeout": not bool(re.search(
        r'deps\.forceRm\(container\.id,\s*\{[^}]*\btimeout\s*:', cleanup, re.S)),
    "network removal has no timeout": not bool(re.search(
        r'deps\.run\(\["network",\s*"rm",\s*network\.id\],\s*\{[^}]*\btimeout\s*:', cleanup, re.S)),
    "runner forwards timeout to spawnSync": bool(re.search(
        r'spawnSync\(exe,\s*args,\s*\{\s*\.\.\.spawnOpts', runner, re.S)),
    "runner documents timeout as spawn error": "call times out" in runner,
}
for name, value in checks.items():
    print(f"{name}: {value}")
if not all(checks.values()):
    raise SystemExit("unexpected source shape")
PY

Repository: NVIDIA/NemoClaw

Length of output: 436


Set an explicit timeout on all Docker cleanup calls.

deps.run(["info"], ...) and the removal calls at lines 399, 441, and 461 lack timeouts. An unresponsive Docker daemon can block synchronous uninstall indefinitely. Pass timeout: 10_000 to each call so timeout results reach the existing failure handling.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/inference/local-model-profile/cleanup.ts` around lines 491 - 504, Set
timeout: 10_000 on the Docker invocations in the cleanup flow, including
deps.run(["info"], ...) within the main try block and the removal calls in
cleanupHostLocalVllm and cleanupLlamaCpp. Preserve the existing options and
failure-handling behavior so timeout results are processed normally.
src/lib/inference/local.ts (1)

548-552: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Locate callers of the managed vLLM provider state/binding APIs and check which pass getManagedBaseUrlImpl.
set -euo pipefail

rg -nP --type=ts -C6 '\bgetManagedBaseUrlImpl\b' -g '!**/*.test.ts'
echo '--- all call sites of the provider state/binding entrypoints ---'
rg -nP --type=ts -C4 '\bgetManagedVllmProviders?(State|Binding)\s*\(' -g '!**/*.test.ts'

Repository: NVIDIA/NemoClaw

Length of output: 153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate files ---'
fd -t f 'local\.ts$|README\.md$|CONTRIBUTING\.md$' src

printf '%s\n' '--- local.ts structure ---'
ast-grep outline src/lib/inference/local.ts

printf '%s\n' '--- relevant definitions and references ---'
rg -n -C5 'recoverHostLocalManagedVllmEndpoint|getManagedBaseUrlImpl|getManagedVllmProviderState|getManagedVllmProviderBinding|getManagedVllmProviders' src --glob '*.ts' --glob '*.tsx'

Repository: NVIDIA/NemoClaw

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- provider-state implementation ---'
sed -n '420,595p' src/lib/inference/local.ts

printf '%s\n' '--- production option plumbing ---'
rg -n -C8 'getManagedVllmBaseUrlImpl|getManagedBaseUrlImpl|probeLocalProviderHealth\(' src/lib --glob '*.ts' --glob '!*.test.ts'

printf '%s\n' '--- focused tests for provider-state precedence ---'
sed -n '100,210p' src/lib/inference/local-vllm-auth.test.ts
sed -n '360,400p' src/lib/inference/local-vllm-auth.test.ts

printf '%s\n' '--- relevant documentation ---'
rg -n -C4 'getManagedBaseUrlImpl|Station lifecycle|host-local|managed vLLM' src/lib/README.md src/lib/inference/README.md

Repository: NVIDIA/NemoClaw

Length of output: 21031


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- all host-local seam uses ---'
rg -n -C5 'recoverHostLocalManagedVllmEndpointImpl' src --glob '*.ts' --glob '*.tsx'

printf '%s\n' '--- all Station seam uses, excluding tests ---'
rg -n -C5 'getManagedBaseUrlImpl|getManagedVllmBaseUrlImpl' src --glob '*.ts' --glob '*.tsx' --glob '!*.test.ts'

printf '%s\n' '--- focused host-local/provider-state tests ---'
rg -n -C8 'host.local|host-local|multiple managed|present|recoverHost' \
  src/lib/inference/local-vllm-auth.test.ts \
  src/lib/inference/serving/vllm-host-local-lifecycle.test.ts

Repository: NVIDIA/NemoClaw

Length of output: 18366


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
def current_host_local_endpoint(options, default_endpoint):
    if options.get("recoverHostLocalManagedVllmEndpointImpl") is not None:
        return options["recoverHostLocalManagedVllmEndpointImpl"]
    if options.get("getManagedBaseUrlImpl") is not None:
        return None
    return default_endpoint

def provider_state(options, station_present, cluster_present, host_present):
    host = current_host_local_endpoint(
        options,
        "host-local-endpoint" if host_present else None,
    )
    present_count = int(cluster_present) + int(host is not None) + int(station_present)
    return host, present_count

options = {"getManagedBaseUrlImpl": "station-test-seam"}
host, present_count = provider_state(options, station_present=True, cluster_present=False, host_present=True)
assert host is None
assert present_count == 1

host, present_count = provider_state(options, station_present=True, cluster_present=False, host_present=True)
assert host is None
assert present_count != 2

options = {
    "getManagedBaseUrlImpl": "station-test-seam",
    "recoverHostLocalManagedVllmEndpointImpl": "host-test-seam",
}
host, present_count = provider_state(options, station_present=True, cluster_present=False, host_present=True)
assert host == "host-test-seam"
assert present_count == 2

print("current implementation suppresses host-local recovery when only getManagedBaseUrlImpl is supplied")
print("the suppressed endpoint is absent from presentCount and cannot trigger the multiple-runtime conflict")
print("an explicit host-local seam preserves independent injection and conflict detection")
PY

Repository: NVIDIA/NemoClaw

Length of output: 435


Do not gate host-local recovery on getManagedBaseUrlImpl.

getManagedBaseUrlImpl is the Station compatibility seam. It must not suppress host-local detection or conflict checks. Use recoverHostLocalManagedVllmEndpointImpl ?? recoverHostLocalManagedVllmEndpoint; tests that need suppression can inject () => null.

Proposed change
-  const hostLocalEndpoint = options.recoverHostLocalManagedVllmEndpointImpl
-    ? options.recoverHostLocalManagedVllmEndpointImpl({ loadApiKey })
-    : options.getManagedBaseUrlImpl
-      ? null
-      : recoverHostLocalManagedVllmEndpoint({ loadApiKey });
+  const hostLocalEndpoint = (
+    options.recoverHostLocalManagedVllmEndpointImpl ?? recoverHostLocalManagedVllmEndpoint
+  )({ loadApiKey });
📝 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.

  const hostLocalEndpoint = (
    options.recoverHostLocalManagedVllmEndpointImpl ?? recoverHostLocalManagedVllmEndpoint
  )({ loadApiKey });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/inference/local.ts` around lines 548 - 552, Update the
hostLocalEndpoint selection to use the injected
recoverHostLocalManagedVllmEndpointImpl when provided, otherwise fall back
directly to recoverHostLocalManagedVllmEndpoint, without checking
getManagedBaseUrlImpl. Preserve injection of () => null as the mechanism for
tests that need to suppress recovery.
src/lib/inference/serving/adapter-registry.ts (1)

99-114: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show how the host-local vLLM materializer builds the serving command line.
fd -t f 'vllm-host-local-lifecycle.ts' -x cat -n {}

# Find every place the materializer emits model-selection arguments.
rg -n -C 6 -- '--model|--served-model-name|servedName|spec\.model\.id' src/lib/inference/serving

Repository: NVIDIA/NemoClaw

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate materializer and selection files ---'
fd -t f -i 'vllm|material|selection|adapter-registry' src/lib/inference/serving

printf '%s\n' '--- host-local materializer symbols and command construction ---'
rg -n -C 12 --glob '*.ts' \
  'materializ|VllmModelDef|modelArgs|servedModelId|command|docker.*run|vllm.*serve|structuredArguments' \
  src/lib/inference/serving

printf '%s\n' '--- argument ownership and validation definitions ---'
sed -n '1,140p' src/lib/inference/serving/adapter-registry.ts
sed -n '330,390p' src/lib/inference/serving/adapter-registry.ts
sed -n '1,180p' src/lib/inference/serving/host-local-vllm-selection.ts

Repository: NVIDIA/NemoClaw

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- host-local serving files ---'
fd -t f -i 'host-local|vllm' src/lib/inference/serving | grep -v '\.test\.ts$'

printf '%s\n' '--- host-local command builders ---'
rg -n -C 10 --glob '!*.test.ts' \
  'HOST_LOCAL|host-local|modelArgs|servedModelId|recipe\.spec\.model\.id|["'\'']--model["'\'']|vllm serve|arguments:' \
  src/lib/inference/serving

printf '%s\n' '--- all host-local lifecycle/materializer references ---'
rg -n -C 8 --glob '!*.test.ts' \
  'vllm\.host-local|host-local\.lifecycle|materializerRef|startContainer|command\.arguments' \
  src/lib/inference/serving

Repository: NVIDIA/NemoClaw

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- host-local selection materialization ---'
sed -n '1,170p' src/lib/inference/serving/host-local-vllm-selection.ts

printf '%s\n' '--- vLLM model definition and command assembly ---'
fd -t f 'vllm-models.ts|vllm.ts|vllm-prompt.ts' src/lib -x sh -c '
  echo "--- $1"
  rg -n -C 14 -- "modelArgs|servedModelId|model\.id|--model|serve" "$1"
' sh {}

printf '%s\n' '--- lifecycle calls into the host-local serving path ---'
rg -n -C 16 --glob '!*.test.ts' \
  'resolveHostLocalVllmSelection|materializeHostLocalVllmSelection|VllmModelDef|modelArgs|serveEnvironment|install.*Vllm|start.*Vllm' \
  src/lib

Repository: NVIDIA/NemoClaw

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- vLLM run argv construction ---'
sed -n '507,620p' src/lib/inference/vllm.ts

printf '%s\n' '--- host-local install launch path ---'
sed -n '1740,1915p' src/lib/inference/vllm.ts

printf '%s\n' '--- command assembly references ---'
rg -n -C 10 --glob '!*.test.ts' \
  'buildVllmRunArgs|model\.modelArgs|model\.servedModelId|model\.id|fixedServeCommand|--served-model-name' \
  src/lib/inference/vllm.ts src/lib/inference/vllm-models.ts

Repository: NVIDIA/NemoClaw

Length of output: 38671


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
import re

registry = Path("src/lib/inference/serving/adapter-registry.ts").read_text()
selection = Path("src/lib/inference/serving/host-local-vllm-selection.ts").read_text()
models = Path("src/lib/inference/vllm-models.ts").read_text()

def set_entries(source, name):
    match = re.search(
        rf"{re.escape(name)}\s*=\s*new Set\(\[(.*?)\]\);",
        source,
        re.S,
    )
    if not match:
        raise SystemExit(f"missing set: {name}")
    return re.findall(r'"([^"]+)"', match.group(1))

owned_registry = set_entries(registry, "HOST_LOCAL_MATERIALIZER_OWNED_ARGUMENTS")
owned_selection = set_entries(selection, "MATERIALIZER_OWNED_ARGUMENTS")

model_arguments = ["--model", "recipe-model", "--max-model-len", "4096"]
filtered = [
    token for i, token in enumerate(model_arguments)
    if not (i == 0 and token in owned_selection)
]
argv = ["serve", "recipe-model", "--max-model-len", "262144", *filtered]

print("registry owns --model:", "--model" in owned_registry)
print("selection owns --model:", "--model" in owned_selection)
print("materialized argv:", argv)
print("model-selection occurrences:", [
    (i, token) for i, token in enumerate(argv) if token in ("recipe-model", "--model")
])

assert "--model" not in owned_registry
assert "--model" not in owned_selection
assert argv == [
    "serve", "recipe-model", "--max-model-len", "262144",
    "--model", "recipe-model", "--max-model-len", "4096",
]
PY

printf '%s\n' '--- exact command builder ---'
sed -n '683,721p' src/lib/inference/vllm-models.ts

Repository: NVIDIA/NemoClaw

Length of output: 2067


Reserve --model for the host-local materializer.

buildVllmServeCommand emits model.id positionally, while modelArguments() forwards a recipe-defined --model. This creates conflicting model selectors. Add --model to both host-local materializer-owned argument sets so validation rejects it and materialization cannot forward it.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/inference/serving/adapter-registry.ts` around lines 99 - 114, Update
the argument ownership definitions used by buildVllmServeCommand and
modelArguments to include "--model" in both host-local materializer-owned sets.
This must reject recipe-defined --model arguments and prevent forwarding them
while preserving the positional model.id emitted by buildVllmServeCommand.
src/lib/inference/serving/resolver.ts (1)

155-168: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use exact numeric comparison for version components.

Line 159 converts arbitrary-length version components to Number. This loses precision. For example, actual 9007199254740992 incorrectly satisfies minimum 9007199254740993 because both values round to the same number.

Compare validated components as BigInt values, or compare normalized digit strings. Add a boundary test for components larger than Number.MAX_SAFE_INTEGER. This breaks the fail-closed readiness qualification required before profile selection.

Proposed fix
-  const actualParts = actual.split(".").map(Number);
-  const minimumParts = minimum.split(".").map(Number);
+  const actualParts = actual.split(".").map((part) => BigInt(part));
+  const minimumParts = minimum.split(".").map((part) => BigInt(part));
   const width = Math.max(actualParts.length, minimumParts.length);
   for (let index = 0; index < width; index += 1) {
-    const actualPart = actualParts[index] ?? 0;
-    const minimumPart = minimumParts[index] ?? 0;
+    const actualPart = actualParts[index] ?? 0n;
+    const minimumPart = minimumParts[index] ?? 0n;
📝 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.

function versionAtLeast(actual: unknown, minimum: string): boolean {
  if (typeof actual !== "string") return false;
  const versionPattern = /^\d+(?:\.\d+)*$/u;
  if (!versionPattern.test(actual) || !versionPattern.test(minimum)) return false;
  const actualParts = actual.split(".").map((part) => BigInt(part));
  const minimumParts = minimum.split(".").map((part) => BigInt(part));
  const width = Math.max(actualParts.length, minimumParts.length);
  for (let index = 0; index < width; index += 1) {
    const actualPart = actualParts[index] ?? 0n;
    const minimumPart = minimumParts[index] ?? 0n;
    if (actualPart !== minimumPart) return actualPart > minimumPart;
  }
  return true;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/inference/serving/resolver.ts` around lines 155 - 168, Update
versionAtLeast to compare version components exactly without converting them to
Number; use BigInt or normalized digit-string comparison after validation so
values beyond Number.MAX_SAFE_INTEGER remain distinguishable. Preserve missing
components as zero and the existing lexicographic component ordering, and add a
boundary test covering adjacent oversized components to ensure readiness
qualification fails when actual is smaller.
src/lib/inference/vllm.ts (1)

2027-2046: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect host-local receipt lifecycle and restart-policy handling.
set -euo pipefail

fd 'vllm-host-local-lifecycle.ts' src | xargs -r cat -n
rg -n -C 4 'unless-stopped|dockerStop|dockerForceRm' --type=ts src/lib/inference/vllm.ts

Repository: NVIDIA/NemoClaw

Length of output: 18172


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '1900,2060p' src/lib/inference/vllm.ts
rg -n -C 5 'HOST_LOCAL_VLLM_RUNTIME_RECEIPT_FILE|persistHostLocalVllmRuntimeReceipt|unlink|rmSync|writeLocalAdapterJsonFile|managedVllmStateDir' src/lib/inference src/lib

Repository: NVIDIA/NemoClaw

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- vllm install lifecycle ---'
sed -n '1935,2055p' src/lib/inference/vllm.ts

printf '%s\n' '--- receipt references ---'
rg -n 'HOST_LOCAL_VLLM_RUNTIME_RECEIPT_FILE|persistHostLocalVllmRuntimeReceipt|runtimeReceiptPath|unlinkSync|rmSync' \
  src/lib/inference/vllm.ts src/lib/inference/vllm-host-local-lifecycle.ts src/lib/local-adapter-lifecycle.ts src/lib/vllm-api-key.ts 2>/dev/null || true

printf '%s\n' '--- relevant imports ---'
sed -n '1,35p' src/lib/inference/vllm.ts

Repository: NVIDIA/NemoClaw

Length of output: 5458


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- lifecycle module path ---'
fd -i 'vllm.*lifecycle.*\.ts' src

printf '%s\n' '--- vllm imports and lifecycle API ---'
sed -n '30,60p' src/lib/inference/vllm.ts
for f in $(fd -i 'vllm.*lifecycle.*\.ts' src); do
  printf '\n--- %s ---\n' "$f"
  rg -n -C 4 'export|receipt|unlink|rmSync|cleanup|recover|docker(Start|Stop|ForceRm)' "$f"
done

printf '%s\n' '--- recovery callers ---'
rg -n -C 5 'recoverHostLocalManagedVllmEndpoint|validateHostLocalVllmRuntimeReceipt' src --glob '*.ts'

Repository: NVIDIA/NemoClaw

Length of output: 50371


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- host-local cleanup implementation ---'
sed -n '1,55p' src/lib/inference/local-model-profile/cleanup.ts
sed -n '330,430p' src/lib/inference/local-model-profile/cleanup.ts

printf '%s\n' '--- auth-boundary implementation ---'
rg -n -C 12 'function verifyDualStationVllmAuthBoundary|verifyDualStationVllmAuthBoundary' src/lib/inference/vllm.ts src/lib/inference/serving --glob '*.ts'

printf '%s\n' '--- host-local lifecycle test coverage for stopped containers ---'
rg -n -C 6 'Running|stopped|restart|receipt|cleanup' src/lib/inference/serving/vllm-host-local-lifecycle.test.ts

Repository: NVIDIA/NemoClaw

Length of output: 13184


🌐 Web query:

Docker official documentation unless-stopped manually stopped container daemon restart behavior

💡 Result:

When you use the unless-stopped restart policy, the Docker daemon will automatically restart a container if it exits, unless it was explicitly stopped by a user or process [1][2]. Regarding the interaction between manual stops and daemon restarts: - If you manually stop a container (e.g., via docker stop), the Docker daemon will not restart it, even if the Docker daemon itself is subsequently restarted [1][2]. The container remains in a stopped state until it is manually started again [1][3]. - If the container was running when the Docker daemon was stopped or restarted (i.e., it was not manually stopped first), the unless-stopped policy will cause the container to automatically start when the daemon comes back online [1][4]. In essence, the unless-stopped policy effectively "remembers" that you intentionally stopped the container and respects that intent by preventing the daemon from automatically bringing it back up [1][5]. If the container stops for any other reason (such as a crash or a system reboot that kills the daemon), it will restart [1][3]. Note that certain commands, such as docker kill, may be interpreted by the daemon as a request to stop the container, which can result in the container failing to restart after a daemon or host reboot [6].

Citations:


Security Misconfiguration (CWE-459)

Reachability: Internal · Exploitability: Difficult

Remove the container and receipt when the auth-boundary check fails. dockerStop prevents automatic restart but leaves the container and receipt. A later explicit start can pass recovery using the stale receipt without rerunning the auth-boundary check. Use dockerForceRm(start.containerId, ...) and delete HOST_LOCAL_VLLM_RUNTIME_RECEIPT_FILE.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/inference/vllm.ts` around lines 2027 - 2046, Update the failed
auth-boundary branch in the managed-auth flow of the vLLM install logic to
force-remove the container with dockerForceRm using the start container
identifier, then delete HOST_LOCAL_VLLM_RUNTIME_RECEIPT_FILE before returning
failure. Replace the existing dockerStop cleanup so no stale container or
receipt remains.
test/install-local-model-profile.test.ts (1)

22-31: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Clear all profile-selection inputs from the parent environment.

Lines 22-31 retain unspecified NEMOCLAW_* values from process.env.
An ambient serving preset, vLLM model, vLLM extra-arguments value, or managed-cluster peer value can change the installer branch before the harness runs.
Clear every profile-selection input that the installer reads, then add only the value under test.

Proposed fix
       NEMOCLAW_LOCAL_MODEL_RUNTIME: "",
       NEMOCLAW_MODEL: "",
+      NEMOCLAW_MANAGED_CLUSTER_PEERS: "",
       NEMOCLAW_NO_EXPRESS: "",
       NEMOCLAW_PROVIDER: "",
+      NEMOCLAW_SERVING_PRESET: "",
+      NEMOCLAW_VLLM_EXTRA_ARGS_JSON: "",
+      NEMOCLAW_VLLM_MODEL: "",
       NEMOCLAW_VLLM_PORT: "",

As per coding guidelines, deterministic tests must undo environment stubs.

📝 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.

    env: {
      ...process.env,
      INSTALLER_UNDER_TEST: INSTALLER,
      NEMOCLAW_ENABLE_LOCAL_MODEL_PROFILE: "",
      NEMOCLAW_LOCAL_MODEL_RUNTIME: "",
      NEMOCLAW_MODEL: "",
      NEMOCLAW_MANAGED_CLUSTER_PEERS: "",
      NEMOCLAW_NO_EXPRESS: "",
      NEMOCLAW_PROVIDER: "",
      NEMOCLAW_SERVING_PRESET: "",
      NEMOCLAW_VLLM_EXTRA_ARGS_JSON: "",
      NEMOCLAW_VLLM_MODEL: "",
      NEMOCLAW_VLLM_PORT: "",
      ...env,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/install-local-model-profile.test.ts` around lines 22 - 31, Update the
environment setup in the test harness around INSTALLER_UNDER_TEST to clear every
NEMOCLAW profile-selection variable read by the installer, including serving
preset, vLLM model and extra arguments, and managed-cluster peer settings; then
overlay only the scenario-specific values from env so parent-process values
cannot affect the branch under test.

Source: Coding guidelines

Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Signed-off-by: Prekshi Vyas <prekshiv@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants