Skip to content

2026 05 27 0hlj - #6977

Closed
maggiezha wants to merge 24 commits into
NVIDIA:mainfrom
maggiezha:2026-05-27-0hlj
Closed

2026 05 27 0hlj#6977
maggiezha wants to merge 24 commits into
NVIDIA:mainfrom
maggiezha:2026-05-27-0hlj

Conversation

@maggiezha

@maggiezha maggiezha commented Jul 15, 2026

Copy link
Copy Markdown

Summary

Related Issue

Changes

Type of Change

  • Code change (feature, bug fix, or refactor)
  • Code change with doc updates
  • 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:
  • Non-success, skipped, or missing CI check accepted by maintainer — check name, approval link, and follow-up issue:

Verification

  • PR description includes a Signed-off-by: line and every commit appears as Verified in GitHub
  • Normal pre-commit, commit-msg, and pre-push hooks passed, or npm run check:diff passed when hooks were skipped or unavailable
  • Targeted behavior tests pass for the current change set, or tests are marked not applicable above — command/result or justification:
  • Applicable broad gate passed — npm test for broad runtime/test-harness changes; npm run check for repo-wide validation/coverage changes — command/result:
  • Quality Gates section completed with required justifications or waivers
  • No secrets, API keys, or credentials committed
  • npm run docs builds without warnings (doc changes only)
  • Doc pages follow the style guide (doc changes only)
  • New doc pages include SPDX header and frontmatter (new pages only)

Signed-off-by: Your Name your-email@example.com

Summary by CodeRabbit

  • New Features
    • Added CPU and GPU Kubernetes deployment options with autoscaling, health/readiness endpoints, and load-testing workflows.
    • Added support for Nemotron Ultra via NVIDIA Inference Hub.
    • Added Tavily as a selectable web-search provider (alongside Brave).
    • Added local secrets initialization and improved provider-specific credential handling.
  • Documentation
    • Added/expanded comprehensive CPU/GPU, Helm, HPA, observability, and fork setup guides.
  • Chores
    • Deprecated combined CPU/GPU helper scripts in favor of separate workflows.

maggiezha and others added 21 commits May 27, 2026 23:53
Split NVIDIA Build (nvapi-*) and Inference Hub (sk-*) env vars, load keys from
~/.nemoclaw/secrets.env, route Ultra via inference-api, and fix Tavily on rebuild resume.
Keep Tavily/Telegram support alongside upstream Brave egress verification and provider profile registration.
Removed unnecessary details about Dual NVIDIA keys and Secrets file while retaining key features for clarity.
Updated the README.fork.md to include new information and images.
Set toolSearch false for Ultra and document web_search usage so Tavily runs instead of failing tool_search_code.
Introduce the nemoclaw-cpu Helm chart with metrics-server-based autoscaling,
install/recover/reset/load-test scripts, and documentation for running
Inference Hub agents on MicroK8s (min 1, max 7 replicas per 8 vCPU).
Added an image and recovery instructions to README.
Introduce nemoclaw-gpu (Ollama + agent, DCGM gpu_utilization_percent HPA) with load-test and ops scripts tuned for 4× L40S. Split deploy guides into README-gpu/README-cpu and refresh the CPU chart helpers.
Removed 'GPU' from the title of the deployment README.
Updated images in README-gpu.md to include width and height attributes for better display.
Updated image syntax and added additional screenshots in the README for GPU deployment.
Updated README for GPU deployment with HPA details.
Signed-off-by: maggiezha <42832776+maggiezha@users.noreply.github.com>
@copy-pr-bot

copy-pr-bot Bot commented Jul 15, 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.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds CPU and GPU Kubernetes Helm deployments with HPA, metrics, load testing, recovery tooling, and operational documentation. It also adds Nemotron Ultra routing, secure local credential staging, Tavily web-search support, Telegram configuration handling, related examples, and tests.

Changes

Kubernetes deployment and HPA

Layer / File(s) Summary
CPU and GPU Helm charts
deploy/helm/nemoclaw-cpu/*, deploy/helm/nemoclaw-gpu/*
Adds independent CPU and GPU charts with agent workloads, services, probes, resource settings, persistence, metrics, and mode-specific HPA templates.
HPA operations and load testing
deploy/helm/*/scripts/*, deploy/helm/*/files/*
Adds installers, recovery/reset workflows, monitoring helpers, CPU/GPU load generators, readiness checks, and scale-up/scale-down polling.
Deployment documentation
deploy/README*.md, deploy/helm/README.md, .cursor/skills/nemoclaw-k8s-hpa/*
Documents chart selection, HPA metrics, installation, validation, troubleshooting, and operational commands.

Inference, credentials, and web search

Layer / File(s) Summary
NVIDIA model routing
src/lib/inference/*, src/lib/onboard.ts, nemoclaw-blueprint/*, Dockerfile
Routes Nemotron Ultra through NVIDIA Inference Hub, separates credential environments, and updates model defaults and provider metadata.
Local credential storage
src/lib/credentials/*, src/commands/credentials/init-secrets.ts, secrets.env.example
Adds secure ~/.nemoclaw/secrets.env initialization, parsing, staging, endpoint-specific validation, and CLI support.
Tavily and Telegram integration
src/lib/onboard/*, nemoclaw-blueprint/policies/presets/tavily.yaml, scripts/generate-openclaw-config.py
Adds provider-aware web-search onboarding, Tavily policy/configuration, Telegram plugin enablement, resume validation, and verification scripts.

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

Possibly related PRs

Suggested labels: feature, area: docs

Poem

A rabbit watched the pods take flight,
CPU carrots climbed by night.
Tavily searched, Ultra replied,
Secrets tucked safely inside.
GPU clouds hummed in the queue—
“Hop-hop, the charts are new!”

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.37% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title is just a date/branch identifier and does not describe the change, so it is too vague to judge as a proper summary. Replace it with a concise, descriptive title that summarizes the main change, such as updating the CPU deployment docs and Helm chart defaults.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@github-actions

github-actions Bot commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

PR Review Advisor — Blocking findings reported

Advisor assessment: Blockers require maintainer review
Next action: Review the blockers below.
Findings: 2 blockers · 0 warnings · 0 suggestions

Model lanes

  • GPT-5.6 Terra (primary): Failed after a partial review · low confidence · 2 blockers · 0 warnings · 0 suggestions
  • Nemotron 3 Ultra (second opinion): Completed · high confidence · 3 blockers · 13 warnings · 0 suggestions

Nemotron output stays in workflow artifacts and does not change the assessment above.

E2E guidance

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

Recommended E2E: cloud-onboard, credential-sanitization, full-e2e, hermes-e2e, security-posture, inference-routing, network-policy, onboard-repair, onboard-resume

Blockers

PRA-1 Blocker — Overwrite stale NVIDIA endpoint during resumed route repair

  • Location: src/lib/onboard/machine/handlers/provider-inference.ts:181
  • Category: correctness
  • Problem: The resume branch derives the credential environment from the selected NVIDIA model but only fills endpointUrl when it is empty. A persisted nvidia-prod session created before the Ultra route change can therefore keep the old Integrate endpoint while receiving an Inference Hub credential, so resume configures inference against the wrong upstream.
  • Impact: Resuming a previously configured Nemotron Ultra sandbox can fail inference or send its selected-provider request to a route incompatible with its repaired credential, rather than converging to the selected model's supported route.
  • Fix: For resumed nvidia-prod selections, set endpointUrl unconditionally from resolveNvidiaCloudModelRoute(model), alongside credentialEnv, so persisted endpoint metadata cannot override the current model route.
  • Verification: Inspect the resumed nvidia-prod branch and exercise its handler unit test with a completed session containing Nemotron Ultra, a stale nonempty integrate.api.nvidia.com endpointUrl, and a ready route.
  • Test coverage: Add a provider-inference handler test asserting that resume replaces a stale nonempty nvidia-prod endpointUrl and credentialEnv with the route returned for NVIDIA_NEMOTRON_ULTRA_MODEL before setup/reconciliation.
  • Evidence: src/lib/onboard/machine/handlers/provider-inference.ts:177-181 sets credentialEnv from resolveNvidiaCloudModelRoute(model) but uses `endpointUrl = endpointUrl || nvidiaRoute.apiBaseUrl`. src/lib/inference/config.ts:197-204 resolves Nemotron Ultra to inference-api.nvidia.com and NVIDIA_INFERENCE_HUB_API_KEY. src/lib/onboard/machine/handlers/provider-inference.test.ts covers ready resume and forced provider recovery but does not cover stale nvidia-prod endpoint replacement.

PRA-2 Blocker — Limit cluster recovery cleanup to chart-owned resources

  • Location: deploy/helm/nemoclaw-cpu/scripts/cluster-recover.sh:28
  • Category: correctness
  • Problem: The recovery scripts initially target chart labels but then delete every HPA, Job, Pod, Deployment, and ReplicaSet in the selected namespace. The namespace is configurable and the default can contain unrelated workloads, so recovering this chart destroys resources it does not own.
  • Impact: An operator running recovery in a shared or accidentally selected namespace can irreversibly terminate unrelated workloads and delete their autoscaling and batch resources.
  • Fix: Delete only resources selected by this chart's stable ownership labels (and the named release resources); remove namespace-wide `--all` deletion or require an explicit, separately confirmed destructive namespace-wipe operation.
  • Verification: Read the commands following the first label-scoped deletion in both CPU and GPU cluster-recover scripts; they use `--all` for HPA, Job, Pod, and later Deployment/ReplicaSet cleanup.
  • Test coverage: Add shell-level checked-in tests (or command-construction tests) asserting recovery emits only chart-label- or release-name-scoped delete selectors and never namespace-wide `--all` deletes for workload resources.
  • Evidence: deploy/helm/nemoclaw-cpu/scripts/cluster-recover.sh:25 scopes only the initial deploy/service/HPA delete by `app.kubernetes.io/name=nemoclaw-cpu`; lines 27-35 delete HPA, Job, and Pod resources with `--all` and later delete deploy/rs/hpa/job with `--all`. deploy/helm/nemoclaw-gpu/scripts/cluster-recover.sh:21-30 has the equivalent namespace-wide cleanup after its initial label-scoped command. The scripts accept NAMESPACE from the environment, rather than enforcing a dedicated namespace.

Workflow run details

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

Added port 9000 for the CPU deployment section.
Removed the third port entry from the table.
Signed-off-by: maggiezha <42832776+maggiezha@users.noreply.github.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: 5

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
deploy/helm/nemoclaw-cpu/files/agent-metrics.mjs (1)

1-83: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

CI blocks this new .mjs file — must resolve before merge.

The Codebase Growth Guardrails check explicitly fails on this file: "blocked JavaScript source file added (.mjs)." This needs to be resolved — either by getting this file type allow-listed with maintainer sign-off, or by using an already-permitted extension/location for the module — before this PR can pass CI.

🤖 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 `@deploy/helm/nemoclaw-cpu/files/agent-metrics.mjs` around lines 1 - 83,
Resolve the Codebase Growth Guardrails failure for the new module by either
moving the metrics implementation to an already-permitted module
extension/location or obtaining the required maintainer allow-list approval for
.mjs. Preserve the exported recordLlmLatency and llmMetricsLines API and all
existing metrics behavior.

Source: Pipeline failures

src/lib/onboard/policy-selection.ts (1)

209-214: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Replace the stale preset instead of only deleting it during resume.

For example, a recorded brave preset with active Tavily configuration is filtered out here, but this function never adds tavily before returning. Resume can therefore reapply no web-search egress policy. Add the resolved provider preset when web search is enabled and cover provider switching with a regression test.

🤖 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/policy-selection.ts` around lines 209 - 214, Update the
resume policy flow around isStaleBuiltinWebSearchPolicyPreset and
clampedRecordedPolicyPresets so a stale recorded web-search preset is replaced
with the currently resolved provider preset when web search is enabled, rather
than only filtered out. Preserve existing pruning behavior for other presets,
and add a regression test covering switching providers such as brave to tavily
during resume.
🟠 Major comments (46)
deploy/helm/nemoclaw-cpu/templates/hpa.yaml-1-2 (1)

1-2: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Missing SPDX license headers. These three new files omit the SPDX header that the repo's other new files in this PR (e.g. values.yaml, the shell scripts) consistently include.

  • deploy/helm/nemoclaw-cpu/templates/hpa.yaml#L1-L2: add a # SPDX-FileCopyrightText: / # SPDX-License-Identifier: header before the Helm template directives.
  • deploy/helm/nemoclaw-cpu/observability.md#L1-L2: add an HTML-comment SPDX header (<!-- SPDX-FileCopyrightText: ... --> / <!-- SPDX-License-Identifier: ... -->) before the title.
  • deploy/helm/nemoclaw-cpu/monitoring/prometheus-adapter-values.yaml#L1-L2: add a #-comment SPDX header before the existing usage comment.
🤖 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 `@deploy/helm/nemoclaw-cpu/templates/hpa.yaml` around lines 1 - 2, All three
new files are missing the repository-required SPDX headers. Add the standard
two-line SPDX header before existing content in
deploy/helm/nemoclaw-cpu/templates/hpa.yaml using # comments,
deploy/helm/nemoclaw-cpu/observability.md using an HTML comment, and
deploy/helm/nemoclaw-cpu/monitoring/prometheus-adapter-values.yaml using #
comments; preserve the Helm directives, title, and usage comment after the
headers.

Source: Coding guidelines

README.md-6-7 (1)

6-7: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Personal fork/branch reference left in main README.

This callout points to a personal fork (maggiezha/NemoClaw) and branch 2026-05-27-0hlj, which matches this PR's own source branch. This reads like a leftover self-referential note rather than intentional documentation, and it will be broken/meaningless once this PR merges and the branch is deleted.

🤖 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 `@README.md` around lines 6 - 7, Remove the personal fork and branch callout
from the main README, including the reference to README.fork.md, so no
PR-specific or self-referential documentation remains.
deploy/README.md-1-2 (1)

1-2: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Missing SPDX license headers on new files.

As per coding guidelines, **/*.{ts,tsx,js,mjs,cjs,py,sh,yaml,yml,json,md,mdx,css,svg} files must include the appropriate SPDX license header (HTML comment for Markdown, # comment for YAML). These four new files added in this PR are all missing it:

  • deploy/README.md#L1-L2: add an HTML-comment SPDX header (<!-- SPDX-FileCopyrightText: ... --> / <!-- SPDX-License-Identifier: Apache-2.0 -->) before the H1.
  • deploy/helm/nemoclaw-cpu/monitoring/kube-prometheus-microk8s.yaml#L1-L2: add a #-comment SPDX header above the existing top comment lines.
  • deploy/helm/nemoclaw-cpu/templates/configmap.yaml#L1-L2: add a #-comment SPDX header above apiVersion: v1.
  • deploy/helm/nemoclaw-gpu/templates/hpa.yaml#L1-L2: add a #-comment SPDX header above the {{- if .Values.autoscaling.enabled }} line.
🤖 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 `@deploy/README.md` around lines 1 - 2, Add the repository-standard SPDX
copyright and Apache-2.0 license headers to all four new files: use HTML
comments before the heading in deploy/README.md, and # comment headers at the
top of deploy/helm/nemoclaw-cpu/monitoring/kube-prometheus-microk8s.yaml,
deploy/helm/nemoclaw-cpu/templates/configmap.yaml, and
deploy/helm/nemoclaw-gpu/templates/hpa.yaml, preserving each file’s existing
content and template directives.

Source: Coding guidelines

deploy/helm/nemoclaw-gpu/templates/servicemonitor.yaml-1-1 (1)

1-1: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add SPDX headers to the new YAML sources.

  • deploy/helm/nemoclaw-gpu/templates/servicemonitor.yaml#L1-L1: add the Apache-2.0 SPDX header before the Helm directive.
  • deploy/helm/nemoclaw-cpu/templates/namespace.yaml#L1-L1: add the Apache-2.0 SPDX header before the Helm directive.
  • deploy/helm/nemoclaw-gpu/values-step2-hpa.yaml#L1-L1: add the Apache-2.0 SPDX header before the existing comments.

As per coding guidelines, “Every source file must include the appropriate SPDX license header.”

🤖 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 `@deploy/helm/nemoclaw-gpu/templates/servicemonitor.yaml` at line 1, Add the
standard Apache-2.0 SPDX header before the Helm directive in
deploy/helm/nemoclaw-gpu/templates/servicemonitor.yaml and
deploy/helm/nemoclaw-cpu/templates/namespace.yaml; add the same header before
the existing comments in deploy/helm/nemoclaw-gpu/values-step2-hpa.yaml.

Source: Coding guidelines

deploy/helm/nemoclaw-cpu/scripts/hpa-reset.sh-139-143 (1)

139-143: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Return failure when the rollout fails.

After hpa_common_wait_rollout fails, the script diagnoses the deployment but continues and exits successfully. Exit nonzero after diagnostics so automation does not treat an unsuccessful reset as complete.

Proposed fix
   if ! hpa_common_wait_rollout "${DEPLOYMENT}" "${NAMESPACE}" "${ROLLOUT_TIMEOUT}"; then
     hpa_common_diagnose_rollout "${NAMESPACE}" "${DEPLOYMENT}"
+    exit 1
   fi
🤖 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 `@deploy/helm/nemoclaw-cpu/scripts/hpa-reset.sh` around lines 139 - 143, Update
the WAIT_ROLLOUT failure branch around hpa_common_wait_rollout to invoke
hpa_common_diagnose_rollout first, then exit with a nonzero status so an
unsuccessful rollout reset is reported as failed.
nemoclaw-blueprint/blueprint.yaml-57-60 (1)

57-60: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Correct the default NVIDIA model ID.

The model identifier has a duplicated nvidia/ namespace; NVIDIA documents nvidia/llama-3.1-nemotron-ultra-253b-v1.

Suggested change
-        model: "nvidia/nvidia/llama-3.1-nemotron-ultra-253b-v1"
+        model: "nvidia/llama-3.1-nemotron-ultra-253b-v1"
🤖 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 `@nemoclaw-blueprint/blueprint.yaml` around lines 57 - 60, Update the default
model value in the Nemotron Ultra configuration to use the documented
single-namespace identifier `nvidia/llama-3.1-nemotron-ultra-253b-v1`, removing
the duplicated `nvidia/` segment while leaving the endpoint and credential_env
unchanged.

Source: Coding guidelines

nemoclaw-blueprint/policies/presets/tavily.yaml-16-18 (1)

16-18: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Restrict the Tavily path allowlist.

The preset is declared for Search API access, but "/**" permits the allowed Node and curl processes to issue GET and POST requests to every endpoint on api.tavily.com. Allow only the required paths, such as /search, or explicitly enumerate any additional endpoints required by the integration.

🤖 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 `@nemoclaw-blueprint/policies/presets/tavily.yaml` around lines 16 - 18,
Restrict the GET and POST rules in the Tavily preset’s rules allowlist to the
specific API paths required by the Search API integration, such as /search,
instead of permitting /**. Enumerate any additional required endpoints
explicitly while preserving the existing method permissions.

Source: Path instructions

docs/security/credential-storage.mdx-40-43 (1)

40-43: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Clarify the host-disk storage claim.

The page says provider credentials are not persisted to host disk, but this new section instructs users to persist API keys in ~/.nemoclaw/secrets.env. Qualify the earlier claim as referring to gateway-stored credentials and explicitly state that the optional local secrets file is host-persisted and must be protected.

🤖 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 `@docs/security/credential-storage.mdx` around lines 40 - 43, Update the
credential-storage documentation to qualify the earlier no-host-disk-persistence
claim as applying only to gateway-stored credentials, and explicitly state that
the optional ~/.nemoclaw/secrets.env file persists API keys on the host and
requires protection with the documented file and directory permissions.
deploy/helm/nemoclaw-cpu/files/cpu-spin-worker.mjs-1-11 (1)

1-11: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Resolve the codebase-growth guardrail failure before merging.

CI explicitly rejects this newly added .mjs source file, so the PR remains red regardless of runtime correctness. Use an already-approved mechanism for the worker, or update the guardrail through the repository’s accepted process and update any chart consumer if the path changes.

🤖 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 `@deploy/helm/nemoclaw-cpu/files/cpu-spin-worker.mjs` around lines 1 - 11,
Replace the newly added cpu-spin worker source in the deploy chart with an
already-approved worker mechanism, or follow the repository’s accepted process
to allow this file type and update all chart consumers if its path changes. Keep
the worker behavior used by the CPU spin/HPA testing flow intact and ensure the
codebase-growth guardrail passes.

Source: Pipeline failures

deploy/helm/README.md-1-1 (1)

1-1: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add SPDX headers to all new Helm Markdown files.

All three files omit the required HTML-comment SPDX header.

  • deploy/helm/README.md#L1-L1: insert the copyright and Apache-2.0 SPDX header before the title.
  • deploy/helm/nemoclaw-gpu/README.md#L1-L1: insert the same header before the title.
  • deploy/helm/nemoclaw-cpu/README.md#L1-L1: insert the same header before the title.

As per coding guidelines, “Every source file must include the appropriate SPDX license header; use HTML comments for Markdown.”

🤖 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 `@deploy/helm/README.md` at line 1, Add the required HTML-comment copyright and
Apache-2.0 SPDX header before the title in deploy/helm/README.md,
deploy/helm/nemoclaw-gpu/README.md, and deploy/helm/nemoclaw-cpu/README.md.

Source: Coding guidelines

scripts/generate-openclaw-config.py-869-897 (1)

869-897: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Replace the managed instruction when the provider changes.

Configuring Brave and later Tavily appends both backend instructions to AGENTS.md. Switching back then returns early because the old Brave phrase remains, leaving contradictory guidance. Replace one marker-delimited managed block instead of checking only usage_line.

🤖 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/generate-openclaw-config.py` around lines 869 - 897, Update
_append_web_search_answer_instruction to manage a single marker-delimited
instruction block in AGENTS.md. Detect and replace the existing managed block
whenever the configured provider changes, rather than returning based only on
usage_line; preserve unrelated AGENTS.md content and append the block when none
exists.
scripts/generate-openclaw-config.py-831-833 (1)

831-833: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject unsupported providers instead of silently switching to Brave.

A typo such as tavliy generates a Brave configuration and credential reference even though Tavily was requested.

Proposed fix
 provider = (env.get("NEMOCLAW_WEB_SEARCH_PROVIDER") or "brave").strip().lower()
-if provider not in ("brave", "tavily"):
-    provider = "brave"
+if provider not in {"brave", "tavily"}:
+    raise ValueError(
+        "NEMOCLAW_WEB_SEARCH_PROVIDER must be either 'brave' or 'tavily'"
+    )

As per coding guidelines, “Do not add configuration, fallback, migration, compatibility, or extension layers without a current requirement.”

🤖 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/generate-openclaw-config.py` around lines 831 - 833, Update the
provider validation in the configuration-generation flow around the provider
variable to reject unsupported non-empty values instead of replacing them with
"brave". Preserve the default to Brave only when NEMOCLAW_WEB_SEARCH_PROVIDER is
unset or blank, and raise the existing appropriate configuration error for
values other than "brave" or "tavily".

Source: Coding guidelines

deploy/helm/nemoclaw-gpu/files/agent-metrics.mjs-6-6 (1)

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

Validate LLM_LATENCY_WINDOW_SIZE before using it.

A nonnumeric value becomes NaN, making the eviction condition permanently false and allowing llmDurationsMs to grow without bounds.

Proposed fix
-const LLM_LATENCY_WINDOW = Number(process.env.LLM_LATENCY_WINDOW_SIZE || 128);
+const configuredWindow = Number(process.env.LLM_LATENCY_WINDOW_SIZE ?? 128);
+const LLM_LATENCY_WINDOW =
+  Number.isSafeInteger(configuredWindow) && configuredWindow > 0 ? configuredWindow : 128;

Also applies to: 22-23

🤖 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 `@deploy/helm/nemoclaw-gpu/files/agent-metrics.mjs` at line 6, Validate the
value used to initialize LLM_LATENCY_WINDOW before applying it to the
llmDurationsMs eviction logic. Ensure nonnumeric, non-finite, or invalid window
sizes fall back to the existing default so the eviction condition remains
effective and the duration array stays bounded.
src/lib/credentials/secrets-env.ts-126-134 (1)

126-134: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Enforce secret-file ownership and permissions, and create it exclusively.

The loader accepts group/world-readable files, and existsSync followed by writeFileSync leaves a target-file race. On POSIX, reject files not owned by the current user or with any 0o077 bits, and create the template with flag: "wx".

Proposed direction
 const stat = fs.fstatSync(fd);
 if (!stat.isFile()) return lastStagedSecrets;
+if (
+  process.platform !== "win32" &&
+  ((typeof process.getuid === "function" && stat.uid !== process.getuid()) ||
+    (stat.mode & 0o077) !== 0)
+) {
+  console.error(`  Refusing to load ${secretsFile}: expected owner-only permissions.`);
+  return lastStagedSecrets;
+}

-fs.writeFileSync(secretsFile, SECRETS_ENV_TEMPLATE, { encoding: "utf-8", mode: 0o600 });
+fs.writeFileSync(secretsFile, SECRETS_ENV_TEMPLATE, {
+  encoding: "utf-8",
+  mode: 0o600,
+  flag: "wx",
+});

Also applies to: 184-189

🤖 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/credentials/secrets-env.ts` around lines 126 - 134, Harden the
secrets-file loading and template-creation logic around the `stat` checks and
existing file-write path: on POSIX, accept only regular files owned by the
current user with no group/world permission bits (`0o077`). Replace the
`existsSync`-then-`writeFileSync` creation flow with exclusive template creation
using the write call’s `wx` flag, while preserving the existing fallback
behavior when validation or creation fails.
deploy/helm/nemoclaw-gpu/monitoring/dcgm-servicemonitor.yaml-1-2 (1)

1-2: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add the required SPDX license header.

Proposed fix
+# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
 # Scrape NVIDIA DCGM exporter (already installed by microk8s enable gpu / GPU operator).

As per coding guidelines, every YAML source file must include the appropriate SPDX license header.

🤖 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 `@deploy/helm/nemoclaw-gpu/monitoring/dcgm-servicemonitor.yaml` around lines 1
- 2, Add the repository’s standard SPDX license header at the top of
monitoring/dcgm-servicemonitor.yaml, before the existing DCGM exporter comment
and apiVersion declaration. Preserve the manifest content unchanged.

Source: Coding guidelines

README.fork.md-1-9 (1)

1-9: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Get maintainer approval before adding fork-specific product documentation.

This brands the upstream repository around a personal fork, branch, and supported integration set, while the PR provides no accepted scope, ownership, lifecycle, compatibility, security, or validation decision. Remove it from the upstream PR or move it to the approved Community Solutions surface unless maintainers explicitly accept ownership.

As per coding guidelines, supported integrations and product surfaces require an accepted scope decision and defined ownership, lifecycle, compatibility, security, and validation expectations.

🤖 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 `@README.fork.md` around lines 1 - 9, Remove the fork-specific branding and
integration claims from README.fork.md, including the personal fork, branch, and
supported product surfaces, unless maintainer approval explicitly permits them.
Keep upstream documentation references only, or move the content to the approved
Community Solutions surface.

Source: Coding guidelines

src/lib/onboard/web-search-flow.ts-90-98 (1)

90-98: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Send the Tavily key as a bearer token.
Tavily’s search API expects Authorization: Bearer ...; api_key does not belong in the JSON body. This probe can reject valid credentials and block Tavily onboarding.

Proposed fix
   function validateTavilySearchApiKey(apiKey: string): CurlProbeResult {
     return runCurlProbe([
       "-sS",
       "-H",
       "Content-Type: application/json",
+      "-H",
+      `Authorization: Bearer ${apiKey}`,
       "-d",
-      JSON.stringify({ api_key: apiKey, query: "ping", max_results: 1 }),
+      JSON.stringify({ query: "ping", max_results: 1 }),
       "https://api.tavily.com/search",
     ]);
   }
🤖 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/web-search-flow.ts` around lines 90 - 98, Update
validateTavilySearchApiKey to send the API key via an Authorization Bearer
header and remove api_key from the JSON request body, while preserving the
existing search probe payload and endpoint.

Source: MCP tools

deploy/helm/nemoclaw-cpu/templates/deployment.yaml-40-74 (1)

40-74: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Harden the agent container security context.

The agent container still runs with the image defaults. Set allowPrivilegeEscalation: false, drop all capabilities, enable readOnlyRootFilesystem: true, and run as non-root. If anything needs writes, mount a writable emptyDir only where required.

Proposed hardening
       containers:
         - name: agent
+          securityContext:
+            allowPrivilegeEscalation: false
+            capabilities:
+              drop:
+                - ALL
+            readOnlyRootFilesystem: true
+            runAsNonRoot: 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 `@deploy/helm/nemoclaw-cpu/templates/deployment.yaml` around lines 40 - 74,
Harden the agent container defined under the `containers` entry by adding a
security context that disables privilege escalation, drops all Linux
capabilities, enables `readOnlyRootFilesystem`, and enforces non-root execution.
Review the agent runtime for required write locations and add narrowly scoped
writable `emptyDir` mounts only where necessary, leaving the existing read-only
`/app` mount unchanged.

Source: Linters/SAST tools

deploy/helm/nemoclaw-gpu/files/load-generator.mjs-1-1 (1)

1-1: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Resolve the codebase growth guardrail failure.

CI explicitly rejects this newly added .mjs source file. Move or convert it to an allowed location/type, or obtain the required maintainer-approved exception before merge.

🤖 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 `@deploy/helm/nemoclaw-gpu/files/load-generator.mjs` at line 1, Resolve the
codebase guardrail failure for the newly added load-generator source by moving
it to an allowed location or converting it to an approved file type. If
retaining the .mjs file is necessary, obtain and record the required
maintainer-approved exception before merging; preserve the load generator’s
behavior.

Source: Pipeline failures

deploy/helm/nemoclaw-cpu/templates/_helpers.tpl-79-86 (1)

79-86: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Ensure the HPA maximum cannot be lower than its minimum.

This helper can return maxCpus or an explicit maxReplicas below the value produced by hpaMinReplicas, yielding an invalid HPA. Fail template rendering for invalid bounds or enforce maxReplicas >= minReplicas.

🤖 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 `@deploy/helm/nemoclaw-cpu/templates/_helpers.tpl` around lines 79 - 86, Update
the nemoclaw-cpu.hpaMaxReplicas helper to ensure its resolved maximum is never
below the value from hpaMinReplicas. Validate explicit autoscaling.maxReplicas
and the maxCpus-derived value, failing template rendering for invalid bounds or
otherwise enforcing a maximum at least equal to hpaMinReplicas while preserving
the existing fallback behavior.
.cursor/skills/nemoclaw-k8s-hpa/reference.md-1-1 (1)

1-1: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add the required Markdown SPDX header.

This new source file starts directly with its heading. Add the Apache-2.0 SPDX comments before it.

As per coding guidelines, every source file must include an appropriate SPDX license header, using HTML comments for Markdown.

🤖 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 @.cursor/skills/nemoclaw-k8s-hpa/reference.md at line 1, Add the required
Apache-2.0 SPDX license header as HTML comments at the beginning of the Markdown
file, before the existing “NemoClaw K8s HPA — Reference” heading.

Source: Coding guidelines

deploy/README-cpu.md-1-3 (1)

1-3: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add SPDX license headers to the new deployment READMEs. Both deploy/README-cpu.md and deploy/README-gpu.md are new files in this PR and omit the SPDX header required by the coding guidelines for Markdown files (HTML-comment style).

  • deploy/README-cpu.md#L1-L3: add an SPDX header before the H1, e.g. <!-- SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. --> / <!-- SPDX-License-Identifier: Apache-2.0 -->.
  • deploy/README-gpu.md#L1-L4: add the same SPDX header block before the H1.
🤖 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 `@deploy/README-cpu.md` around lines 1 - 3, Add the required two-line HTML SPDX
header before the H1 in deploy/README-cpu.md at lines 1-3 and
deploy/README-gpu.md at lines 1-4, using the standard NVIDIA copyright and
Apache-2.0 license identifiers; leave the README content unchanged.

Source: Coding guidelines

deploy/helm/nemoclaw-cpu/values-step2-hpa-performance.yaml-1-2 (1)

1-2: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add the required SPDX license header.

As per coding guidelines, “Every source file must include the appropriate SPDX license header.”

🤖 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 `@deploy/helm/nemoclaw-cpu/values-step2-hpa-performance.yaml` around lines 1 -
2, Add the repository’s standard SPDX license header at the beginning of the
values-step2-hpa-performance.yaml file, before the existing performance HPA
comments, while preserving the current configuration content.

Source: Coding guidelines

deploy/helm/nemoclaw-gpu/templates/deployment.yaml-1-2 (1)

1-2: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Add the required SPDX license header.

As per coding guidelines, “Every source file must include the appropriate SPDX license header.”

🤖 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 `@deploy/helm/nemoclaw-gpu/templates/deployment.yaml` around lines 1 - 2, Add
the repository’s required SPDX license header at the beginning of the Kubernetes
Deployment manifest, before the apiVersion declaration. Use the standard license
identifier and comment format established by nearby source files.

Source: Coding guidelines

deploy/helm/nemoclaw-gpu/scripts/cluster-recover.sh-13-13 (1)

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

Require explicit opt-in before restarting MicroK8s.

RESTART_MICROK8S defaults to 1, so release recovery stops the entire cluster and disrupts workloads in every namespace. Default this to 0 and require operators to opt in.

Proposed fix
-RESTART_MICROK8S="${RESTART_MICROK8S:-1}"
+RESTART_MICROK8S="${RESTART_MICROK8S:-0}"

Also applies to: 31-36

🤖 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 `@deploy/helm/nemoclaw-gpu/scripts/cluster-recover.sh` at line 13, Update the
RESTART_MICROK8S default in cluster-recover.sh from 1 to 0 so recovery does not
restart MicroK8s unless operators explicitly opt in with RESTART_MICROK8S=1.
Preserve the existing conditional restart behavior.
deploy/helm/nemoclaw-cpu/scripts/hpa-common.sh-38-46 (1)

38-46: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Scope destructive recovery operations to the selected Helm release.

Both recovery paths operate namespace-wide instead of selecting resources owned by the release.

  • deploy/helm/nemoclaw-cpu/scripts/hpa-common.sh#L38-L46: add the release selector when patching and deleting pods.
  • deploy/helm/nemoclaw-gpu/scripts/cluster-recover.sh#L19-L29: replace --all cleanup with the GPU release’s labels.
🤖 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 `@deploy/helm/nemoclaw-cpu/scripts/hpa-common.sh` around lines 38 - 46, The
recovery operations in hpa_common_clear_stuck_pods must target only the selected
Helm release: add the release selector to both pod patching and deletion in
deploy/helm/nemoclaw-cpu/scripts/hpa-common.sh lines 38-46. In
deploy/helm/nemoclaw-gpu/scripts/cluster-recover.sh lines 19-29, replace
namespace-wide --all cleanup with the GPU release’s labels.
deploy/helm/nemoclaw-cpu/scripts/hpa-common.sh-60-72 (1)

60-72: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Do not pass the inference API key through Helm arguments.

--set "inference.apiKey=${api_key}" exposes the credential in process arguments and may retain it in Helm release values. Reference a pre-created Kubernetes Secret instead of transporting the raw key through Helm.

🤖 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 `@deploy/helm/nemoclaw-cpu/scripts/hpa-common.sh` around lines 60 - 72, Remove
the raw inference.apiKey assignment from the helm_args array used by the HPA
upgrade flow. Configure the chart to reference the pre-created Kubernetes Secret
through the appropriate existing values key, ensuring the credential is never
passed in process arguments or stored in Helm release values.
src/lib/inference/provider-models.ts-11-15 (1)

11-15: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep NVIDIA Build model discovery on the Integrate API URL.

BUILD_ENDPOINT_URL feeds the Build /models validation path, while the added routing tests establish that NVIDIA Build uses NVIDIA_INTEGRATE_API_BASE_URL and Inference Hub uses NVIDIA_INFERENCE_API_BASE_URL. This change can reject valid Build models by querying the wrong catalog.

Proposed fix
-import { NVIDIA_INFERENCE_API_BASE_URL } from "./config";
+import { NVIDIA_INTEGRATE_API_BASE_URL } from "./config";

-export const BUILD_ENDPOINT_URL = NVIDIA_INFERENCE_API_BASE_URL;
+export const BUILD_ENDPOINT_URL = NVIDIA_INTEGRATE_API_BASE_URL;
🤖 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/provider-models.ts` around lines 11 - 15, Update
BUILD_ENDPOINT_URL to use NVIDIA_INTEGRATE_API_BASE_URL so the Build /models
validation path queries the Integrate catalog, while keeping
NVIDIA_INFERENCE_API_BASE_URL for Inference Hub routing.
deploy/helm/nemoclaw-cpu/scripts/hpa-common.sh-149-168 (1)

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

Validate the configured HPA maximum.

spec_max and the expected max are collected but never compared, so this function returns success even when the HPA exceeds the requested replica ceiling.

Proposed fix
   if [[ "${spec_min}" -lt "${min}" ]]; then
     echo "HPA spec.minReplicas=${spec_min} invalid" >&2
     return 1
   fi
+
+  if [[ "${spec_max}" -gt "${max}" ]]; then
+    echo "HPA spec.maxReplicas=${spec_max} exceeds ${max}" >&2
+    return 1
+  fi
🤖 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 `@deploy/helm/nemoclaw-cpu/scripts/hpa-common.sh` around lines 149 - 168,
Update the HPA validation flow around spec_max to compare the configured HPA
maximum with the expected max value. Return a nonzero status and emit an error
when spec_max exceeds max, while preserving the existing minimum validation and
replica-floor enforcement behavior.
deploy/helm/nemoclaw-gpu/templates/deployment.yaml-26-43 (1)

26-43: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Add pod and container securityContext settings. Both containers still use the default privileged context. Set runAsNonRoot, drop capabilities, disable privilege escalation, and apply a runtime-default seccomp profile; if you switch to a read-only root filesystem, add writable mounts for /root/.ollama and any temp paths the containers need.

🤖 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 `@deploy/helm/nemoclaw-gpu/templates/deployment.yaml` around lines 26 - 43, Add
pod-level and container-level securityContext settings in the deployment
template: enforce non-root execution, use the runtime-default seccomp profile,
drop Linux capabilities, and disable privilege escalation for both containers.
If enabling read-only root filesystems, provide writable mounts for
/root/.ollama and all required temporary paths.

Source: Linters/SAST tools

scripts/test-tavily-flow.sh-68-70 (1)

68-70: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Restore the real PATH before live Tavily validation.

Line 69 keeps the fake curl shim first in PATH, so --live receives the shimmed 200 response instead of calling Tavily. Save the original path and restore it before Line 136.

Proposed fix.
+ORIGINAL_PATH="$PATH"
 export HOME="$TMP"
 export PATH="$FAKE_BIN:$PATH"
 
 if [[ "$LIVE" -eq 1 ]]; then
+  export PATH="$ORIGINAL_PATH"

Also applies to: 136-162

🤖 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/test-tavily-flow.sh` around lines 68 - 70, Save the original PATH
before prepending FAKE_BIN in the test setup, then restore that saved PATH
before the live Tavily validation block beginning at the live-mode flow. Keep
the fake PATH for mock tests, but ensure the --live path invokes the real curl
and reaches Tavily.
deploy/helm/nemoclaw-gpu/scripts/hpa-load-test.sh-353-387 (1)

353-387: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Return a failure when either scaling assertion fails.

Missing scale-up only emits an error, while scale-down timing out is silently accepted. Track both outcomes and exit nonzero so automation can trust this load test.

🤖 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 `@deploy/helm/nemoclaw-gpu/scripts/hpa-load-test.sh` around lines 353 - 387,
The load test currently does not fail when scale-up or scale-down assertions
fail. Update the scale-down loop to track whether replicas reached 1, retain the
existing scale-up result via SCALE_UP_OK, and return a nonzero status when
either outcome is unsuccessful; preserve the existing completion and HPA logging
behavior.
scripts/test-tavily-flow.sh-40-48 (1)

40-48: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not convert every Vitest failure into a skip.

Assertion failures and regressions currently produce a successful continuation labeled as a missing native binding. Only skip the known binding error; propagate all other failures.

🤖 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/test-tavily-flow.sh` around lines 40 - 48, Update the Vitest
invocation in the test flow so only the known missing native binding error is
treated as a skip; preserve and propagate assertion failures, regressions, and
other non-binding errors instead of labeling them unavailable. Use the command’s
captured output and exit status to distinguish these cases, while retaining the
existing success and continuation behavior for the recognized binding failure.
deploy/helm/nemoclaw-gpu/files/agent-server.mjs-54-63 (1)

54-63: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve streaming semantics or reject streaming requests.

For stream: true, this buffers the complete SSE response with text() and rewrites its content type to JSON. Pipe the upstream body and headers, or reject streaming explicitly instead of claiming OpenAI compatibility.

🤖 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 `@deploy/helm/nemoclaw-gpu/files/agent-server.mjs` around lines 54 - 63, Update
the upstream request handling around the hubRes response to preserve streaming
semantics: when the request body has stream enabled, forward the upstream
response body and its relevant headers, including the SSE content type, without
buffering through text() or rewriting it as JSON. Alternatively, explicitly
reject streaming requests with an appropriate unsupported-status response; keep
the existing JSON behavior for non-streaming requests.
deploy/helm/nemoclaw-gpu/files/agent-server.mjs-24-30 (1)

24-30: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound the request body size.

readBody buffers arbitrary client input in memory. Enforce a small maximum and return 413 before concatenating oversized bodies.

🤖 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 `@deploy/helm/nemoclaw-gpu/files/agent-server.mjs` around lines 24 - 30, Update
readBody to track accumulated request bytes against a small maximum while
receiving chunks, reject or signal an oversized body with HTTP 413 before
Buffer.concat, and stop processing further input once the limit is exceeded.
Preserve normal UTF-8 body resolution for requests within the limit.
deploy/helm/nemoclaw-gpu/files/agent-server.mjs-83-89 (1)

83-89: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Require the configured model to exist before reporting Ready.

The names.length > 0 fallback marks the pod Ready when any unrelated model is installed. Remove that fallback so Kubernetes only routes traffic after MODEL is available.

Proposed fix.
-    const ok =
-      names.some((n) => n === MODEL || n.startsWith(`${want}:`) || n.includes(MODEL)) ||
-      names.length > 0;
+    const ok = names.some(
+      (n) => n === MODEL || n.startsWith(`${want}:`) || n.includes(MODEL),
+    );
🤖 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 `@deploy/helm/nemoclaw-gpu/files/agent-server.mjs` around lines 83 - 89, Update
the readiness check in the model discovery flow to remove the names.length > 0
fallback from ok. Ensure ok is true only when a discovered model exactly matches
MODEL, matches its desired tag, or contains MODEL, so the pod reports Ready only
when the configured model is available.
deploy/helm/nemoclaw-gpu/scripts/install-hpa.sh-62-77 (1)

62-77: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not suppress Prometheus installation failures.

The || true paths allow this script to continue with a failed or stale Prometheus release. Always reconcile the required values and propagate Helm or readiness failures.

Proposed fix.
-  if ! helm status "${PROM_RELEASE}" -n "${MONITORING_NS}" >/dev/null 2>&1; then
-    helm upgrade --install "${PROM_RELEASE}" prometheus-community/kube-prometheus-stack \
+  helm upgrade --install "${PROM_RELEASE}" prometheus-community/kube-prometheus-stack \
       --namespace "${MONITORING_NS}" \
       --create-namespace \
       -f "${PROM_VALUES}" \
       --set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false \
       --set prometheus.prometheusSpec.podMonitorSelectorNilUsesHelmValues=false \
       --set prometheus.prometheusSpec.ruleSelectorNilUsesHelmValues=false \
       --timeout "${PROM_HELM_TIMEOUT}" \
-      --wait >/dev/null 2>&1 || true
-  fi
+      --wait >/dev/null
 
   kubectl wait --for=condition=ready pod \
     -l app.kubernetes.io/name=prometheus \
     -n "${MONITORING_NS}" \
-    --timeout=600s >/dev/null 2>&1 || true
+    --timeout=600s >/dev/null
🤖 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 `@deploy/helm/nemoclaw-gpu/scripts/install-hpa.sh` around lines 62 - 77, Update
the Prometheus reconciliation block using helm upgrade --install so it always
applies the required values instead of only running when helm status reports no
release, and remove the || true suppressions from both the Helm command and
kubectl wait. Ensure Helm installation/upgrade and Prometheus pod readiness
failures propagate and stop the script.
deploy/helm/nemoclaw-gpu/values.yaml-23-27 (1)

23-27: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Pin the Ollama image to an immutable version. latest can drift between nodes and restarts, so the same release may run different Ollama builds over time. Use a tested tag or digest here.

🤖 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 `@deploy/helm/nemoclaw-gpu/values.yaml` around lines 23 - 27, Update the image
tag under the ollama configuration to a tested, immutable Ollama version instead
of latest; alternatively use a pinned image digest. Keep the existing repository
and pullPolicy unchanged.
deploy/helm/nemoclaw-gpu/scripts/install-hpa.sh-115-118 (1)

115-118: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the metrics-server gate conditional
deploy/helm/nemoclaw-gpu/scripts/install-hpa.sh:115-118 blocks GPU-only HPA installs on metrics.k8s.io, even though this script later requires custom.metrics.k8s.io for gpu_utilization_percent. Gate it only for resource-metric HPAs, or remove it here.

🤖 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 `@deploy/helm/nemoclaw-gpu/scripts/install-hpa.sh` around lines 115 - 118,
Update the metrics-server readiness check in install-hpa.sh so it runs only when
resource-metric HPAs are being installed, and does not block GPU-only HPA
installations that rely on custom.metrics.k8s.io. Preserve the existing failure
message and exit behavior when the resource-metrics requirement applies.
deploy/helm/nemoclaw-cpu/scripts/install-hpa.sh-80-80 (1)

80-80: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Do not report successful HPA operations when postconditions fail.

  • deploy/helm/nemoclaw-cpu/scripts/install-hpa.sh#L80-L80: propagate HPA bounds verification failure.
  • deploy/helm/nemoclaw-gpu/scripts/hpa-reset.sh#L102-L105: diagnose the rollout failure, then exit nonzero.
🤖 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 `@deploy/helm/nemoclaw-cpu/scripts/install-hpa.sh` at line 80, Propagate
failures from hpa_common_verify_hpa_bounds in
deploy/helm/nemoclaw-cpu/scripts/install-hpa.sh:80 by removing the unconditional
success suppression. In deploy/helm/nemoclaw-gpu/scripts/hpa-reset.sh:102-105,
diagnose rollout failure and exit with a nonzero status instead of reporting
success.
deploy/helm/nemoclaw-cpu/scripts/install-hpa.sh-49-49 (1)

49-49: 🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Do not store the API key in Helm release values.

--set inference.apiKey=... exposes the credential through process arguments and Helm release metadata. Create or reference a Kubernetes Secret without passing the value through Helm.

🤖 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 `@deploy/helm/nemoclaw-cpu/scripts/install-hpa.sh` at line 49, Remove the
inference.apiKey value assignment from the Helm invocation in install-hpa.sh.
Configure the deployment to reference an existing Kubernetes Secret for the API
key, ensuring the credential is never passed through process arguments or stored
in Helm release values.
deploy/helm/nemoclaw-cpu/files/load-generator.mjs-1-147 (1)

1-147: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

The new .mjs deployment sources are blocked by the same CI guardrail.

  • deploy/helm/nemoclaw-cpu/files/load-generator.mjs#L1-L147: move the load generator to an accepted source format/location or obtain an exception.
  • deploy/helm/nemoclaw-cpu/files/agent-server.mjs#L1-L213: move the agent server to an accepted source format/location or obtain an exception.
🤖 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 `@deploy/helm/nemoclaw-cpu/files/load-generator.mjs` around lines 1 - 147, The
CI guardrail blocks both new .mjs deployment sources. Update
deploy/helm/nemoclaw-cpu/files/load-generator.mjs (lines 1-147) and
deploy/helm/nemoclaw-cpu/files/agent-server.mjs (lines 1-213) by moving them to
an accepted source format or location, or obtain and apply the required CI
exception; preserve their existing runtime behavior.

Source: Pipeline failures

deploy/helm/nemoclaw-cpu/files/agent-server.mjs-26-32 (1)

26-32: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Enforce a request-body size limit.

readBody retains every chunk until the client finishes, allowing an accessible client to exhaust pod memory. Stop reading and return 413 after a fixed maximum size.

🤖 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 `@deploy/helm/nemoclaw-cpu/files/agent-server.mjs` around lines 26 - 32, Update
readBody to enforce a fixed maximum request-body size while accumulating chunks,
tracking total bytes and stopping or rejecting once the limit is exceeded.
Ensure the request is no longer consumed beyond the limit and the caller
responds with HTTP 413, while preserving normal UTF-8 body handling for requests
within the limit.
deploy/helm/nemoclaw-gpu/scripts/hpa-reset.sh-46-52 (1)

46-52: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Restrict destructive cleanup to this Helm release.

The script strips finalizers and force-deletes every pod in NAMESPACE, including unrelated workloads when the configurable namespace is shared. Select only release-owned pods and do not clear unrelated finalizers.

Also applies to: 71-77

🤖 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 `@deploy/helm/nemoclaw-gpu/scripts/hpa-reset.sh` around lines 46 - 52, Update
clear_pod_finalizers and the corresponding pod-deletion logic to target only
pods owned by this Helm release, using the release’s existing identifying labels
or selectors rather than every pod in NAMESPACE. Apply the same ownership filter
before stripping finalizers and force-deleting pods, while preserving the
current cleanup behavior for matching release pods.
deploy/helm/nemoclaw-cpu/files/agent-server.mjs-163-180 (1)

163-180: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Gate and bound the CPU load-test endpoint.

Any reachable client can invoke /bench repeatedly with arbitrarily large ms values, consuming worker threads indefinitely. Disable this endpoint by default or require authorization, and cap ms.

🤖 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 `@deploy/helm/nemoclaw-cpu/files/agent-server.mjs` around lines 163 - 180,
Update the /bench handling in the request server to disable the CPU load-test
endpoint by default or require explicit authorization before invoking
cpuSpinWorkers. Also cap the resolved spinMs value, including query, header, and
fallback inputs, to a safe maximum while preserving the existing response
behavior for authorized requests.
Dockerfile-412-415 (1)

412-415: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use the upstream model id everywhere this model is referenced. nvidia/nvidia/llama-3.1-nemotron-ultra-253b-v1 has one namespace too many; switch this ARG and the matching NEMOCLAW_PRIMARY_MODEL_REF to nvidia/llama-3.1-nemotron-ultra-253b-v1.

🤖 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 `@Dockerfile` around lines 412 - 415, Update the NEMOCLAW_MODEL and
NEMOCLAW_PRIMARY_MODEL_REF ARG values to use
nvidia/llama-3.1-nemotron-ultra-253b-v1, removing the duplicate nvidia namespace
while leaving NEMOCLAW_PROVIDER_KEY unchanged.


NAMESPACE="${NAMESPACE:-nemoclaw}"
RELEASE="${RELEASE:-nemoclaw}"
RESTART_MICROK8S="${RESTART_MICROK8S:-1}"

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.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Constrain recovery to the CPU release and make cluster restart opt-in.

The --all deletions remove GPU and unrelated workloads in the namespace, while RESTART_MICROK8S=1 stops every workload cluster-wide by default. Apply release labels to every deletion and default the restart flag to 0.

Proposed direction
-RESTART_MICROK8S="${RESTART_MICROK8S:-1}"
+RESTART_MICROK8S="${RESTART_MICROK8S:-0}"

-kubectl delete hpa -n "${NAMESPACE}" --all ...
-kubectl delete job -n "${NAMESPACE}" --all ...
-kubectl delete pods -n "${NAMESPACE}" --all ...
+kubectl delete hpa,job,pods -n "${NAMESPACE}" \
+  -l 'app.kubernetes.io/name=nemoclaw-cpu' ...

Also applies to: 28-43

🤖 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 `@deploy/helm/nemoclaw-cpu/scripts/cluster-recover.sh` at line 13, Update the
cluster recovery script to default RESTART_MICROK8S to 0, and constrain every
kubectl deletion in the recovery flow to resources belonging to the CPU release
by applying the appropriate release label selector. Preserve the existing
recovery behavior while preventing GPU or unrelated namespace workloads from
being deleted and keeping cluster-wide MicroK8s restart opt-in.

Comment on lines +64 to +70
clear_pod_finalizers() {
local pod
for pod in $(kubectl get pods -n "${NAMESPACE}" -o jsonpath='{.items[*].metadata.name}' 2>/dev/null); do
[[ -z "${pod}" ]] && continue
kubectl patch pod "${pod}" -n "${NAMESPACE}" -p '{"metadata":{"finalizers":null}}' --type=merge \
>/dev/null 2>&1 || true
done

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.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Restrict destructive cleanup to this Helm release.

The script strips finalizers from and force-deletes every pod in NAMESPACE, despite documenting agent-pod cleanup. In a shared namespace, this terminates unrelated workloads and bypasses their cleanup controllers. Select only pods owned by ${RELEASE} and ${DEPLOYMENT}.

Also applies to: 90-93

🤖 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 `@deploy/helm/nemoclaw-cpu/scripts/hpa-reset.sh` around lines 64 - 70, Restrict
the destructive cleanup in clear_pod_finalizers and the corresponding
force-delete flow to pods owned by both ${RELEASE} and ${DEPLOYMENT}. Update the
kubectl pod selection or filtering before patching/deleting so unrelated pods in
NAMESPACE are never affected.

Comment on lines +1 to +82
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//
// Shared Prometheus helpers for agent /metrics (LLM latency, HTTP counters).

const LLM_LATENCY_WINDOW = Number(process.env.LLM_LATENCY_WINDOW_SIZE || 128);
const llmDurationsMs = [];
let llmDurationSumSec = 0;
let llmDurationCount = 0;
let llmRequestsOk = 0;
let llmRequestsError = 0;
const llmHistogramBucketsSec = [0.25, 0.5, 1, 2.5, 5, 10, 30, 60, 120, 300];
const llmHistogramCounts = Array.from({ length: llmHistogramBucketsSec.length + 1 }, () => 0);

export function recordLlmLatency(durationMs, ok) {
const sec = Math.max(0, durationMs) / 1000;
llmDurationSumSec += sec;
llmDurationCount += 1;
if (ok) llmRequestsOk += 1;
else llmRequestsError += 1;

llmDurationsMs.push(durationMs);
if (llmDurationsMs.length > LLM_LATENCY_WINDOW) llmDurationsMs.shift();

let bucketIdx = llmHistogramBucketsSec.findIndex((bound) => sec <= bound);
if (bucketIdx === -1) bucketIdx = llmHistogramBucketsSec.length;
for (let i = bucketIdx; i < llmHistogramCounts.length; i += 1) {
llmHistogramCounts[i] += 1;
}
}

function percentileMs(sorted, p) {
if (!sorted.length) return 0;
const idx = Math.ceil(sorted.length * p) - 1;
return sorted[Math.max(0, idx)];
}

function llmLatencySnapshotMs() {
if (!llmDurationsMs.length) {
return { p50: 0, p95: 0, avg: 0 };
}
const sorted = [...llmDurationsMs].sort((a, b) => a - b);
const sum = sorted.reduce((acc, v) => acc + v, 0);
return {
p50: percentileMs(sorted, 0.5),
p95: percentileMs(sorted, 0.95),
avg: sum / sorted.length,
};
}

export function llmMetricsLines() {
const { p50, p95, avg } = llmLatencySnapshotMs();
const lines = [
"# HELP nemoclaw_llm_requests_total Chat/completions proxied to inference backend",
"# TYPE nemoclaw_llm_requests_total counter",
`nemoclaw_llm_requests_total{result="success"} ${llmRequestsOk}`,
`nemoclaw_llm_requests_total{result="error"} ${llmRequestsError}`,
"# HELP nemoclaw_llm_request_duration_seconds LLM chat/completions end-to-end proxy latency",
"# TYPE nemoclaw_llm_request_duration_seconds histogram",
];

for (let i = 0; i < llmHistogramBucketsSec.length; i += 1) {
lines.push(
`nemoclaw_llm_request_duration_seconds_bucket{le="${llmHistogramBucketsSec[i]}"} ${llmHistogramCounts[i]}`,
);
}
lines.push(
`nemoclaw_llm_request_duration_seconds_bucket{le="+Inf"} ${llmHistogramCounts[llmHistogramCounts.length - 1]}`,
`nemoclaw_llm_request_duration_seconds_sum ${llmDurationSumSec}`,
`nemoclaw_llm_request_duration_seconds_count ${llmDurationCount}`,
"# HELP nemoclaw_llm_latency_p50_milliseconds Rolling p50 LLM latency (recent window)",
"# TYPE nemoclaw_llm_latency_p50_milliseconds gauge",
`nemoclaw_llm_latency_p50_milliseconds ${Math.round(p50)}`,
"# HELP nemoclaw_llm_latency_p95_milliseconds Rolling p95 LLM latency (recent window; HPA-friendly)",
"# TYPE nemoclaw_llm_latency_p95_milliseconds gauge",
`nemoclaw_llm_latency_p95_milliseconds ${Math.round(p95)}`,
"# HELP nemoclaw_llm_latency_avg_milliseconds Rolling average LLM latency (recent window)",
"# TYPE nemoclaw_llm_latency_avg_milliseconds gauge",
`nemoclaw_llm_latency_avg_milliseconds ${Math.round(avg)}`,
);
return lines;
}

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.

📐 Maintainability & Code Quality | 🔴 Critical | 🏗️ Heavy lift

Resolve the codebase-growth guardrail failure.

CI explicitly rejects this newly added .mjs source file. Convert it to an allowed source format/location or obtain the required guardrail approval before merge.

🧰 Tools
🪛 GitHub Actions: CI / Codebase Growth Guardrails / 0_codebase-growth-guardrails.txt

[error] 1-1: CI check failed: blocked JavaScript source file added (.mjs). File marked as added.

🤖 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 `@deploy/helm/nemoclaw-gpu/files/agent-metrics.mjs` around lines 1 - 82, The
new agent-metrics.mjs source file violates the repository’s codebase-growth
guardrail. Convert this metrics implementation to an approved source format or
allowed location, preserving the exported recordLlmLatency and llmMetricsLines
APIs and their behavior; otherwise obtain the required guardrail approval before
merging.

Source: Pipeline failures

Comment on lines +1 to +181
#!/usr/bin/env node
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
//
// GPU agent pod: health + Prometheus metrics + OpenAI-compatible proxy to local Ollama.

import http from "node:http";
import { llmMetricsLines, recordLlmLatency } from "./agent-metrics.mjs";

const PORT = Number(process.env.PORT || 8081);
const BASE_URL = (process.env.INFERENCE_BASE_URL || "http://127.0.0.1:11434/v1").replace(/\/$/, "");
const OLLAMA_BASE = (process.env.OLLAMA_BASE_URL || "http://127.0.0.1:11434").replace(/\/$/, "");
const MODEL = process.env.INFERENCE_MODEL || "";

let inflight = 0;
let totalRequests = 0;
let inferenceReachable = 0;
let inferenceCache = { ok: false, at: 0 };
const INFERENCE_CACHE_MS = Number(process.env.INFERENCE_READY_CACHE_MS || 15_000);
let inferenceReadyEver = false;
let inferenceFailStreak = 0;
const INFERENCE_FAIL_MAX = Number(process.env.INFERENCE_FAIL_MAX || 8);

function readBody(req) {
return new Promise((resolve, reject) => {
const chunks = [];
req.on("data", (c) => chunks.push(c));
req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
req.on("error", reject);
});
}

async function proxyChatCompletions(req, res) {
let raw;
try {
raw = await readBody(req);
} catch {
res.writeHead(400, { "content-type": "text/plain" });
res.end("bad request\n");
return;
}
let body;
try {
body = raw ? JSON.parse(raw) : {};
} catch {
res.writeHead(400, { "content-type": "text/plain" });
res.end("invalid json\n");
return;
}
if (!body.model) body.model = MODEL;
const llmStart = performance.now();
let llmOk = false;
try {
const hubRes = await fetch(`${BASE_URL}/chat/completions`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
signal: AbortSignal.timeout(300_000),
});
llmOk = hubRes.ok;
const text = await hubRes.text();
res.writeHead(hubRes.status, { "content-type": "application/json" });
res.end(text);
} catch (err) {
res.writeHead(502, { "content-type": "application/json" });
res.end(JSON.stringify({ error: String(err) }));
} finally {
recordLlmLatency(performance.now() - llmStart, llmOk);
}
}

async function checkInference() {
const now = Date.now();
if (now - inferenceCache.at < INFERENCE_CACHE_MS) return inferenceCache.ok;
try {
const res = await fetch(`${OLLAMA_BASE}/api/tags`, {
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) {
inferenceCache = { ok: false, at: now };
return false;
}
const data = await res.json();
const names = (data.models || []).map((m) => m.name || m.model || "");
const want = MODEL.split(":")[0];
const ok =
names.some((n) => n === MODEL || n.startsWith(`${want}:`) || n.includes(MODEL)) ||
names.length > 0;
if (ok) {
inferenceReadyEver = true;
inferenceFailStreak = 0;
inferenceCache = { ok: true, at: now };
return true;
}
inferenceFailStreak += 1;
if (inferenceReadyEver && (inflight > 0 || inferenceFailStreak < INFERENCE_FAIL_MAX)) {
inferenceCache = { ok: true, at: now };
return true;
}
inferenceCache = { ok: false, at: now };
return false;
} catch {
inferenceFailStreak += 1;
if (inferenceReadyEver && (inflight > 0 || inferenceFailStreak < INFERENCE_FAIL_MAX)) {
inferenceCache = { ok: true, at: now };
return true;
}
inferenceCache = { ok: false, at: now };
return false;
}
}

function metricsText() {
return [
"# HELP nemoclaw_http_requests_total Total HTTP requests to agent pod",
"# TYPE nemoclaw_http_requests_total counter",
`nemoclaw_http_requests_total ${totalRequests}`,
"# HELP nemoclaw_http_inflight_requests In-flight HTTP requests",
"# TYPE nemoclaw_http_inflight_requests gauge",
`nemoclaw_http_inflight_requests ${inflight}`,
"# HELP nemoclaw_inference_reachable 1 if local Ollama model is ready",
"# TYPE nemoclaw_inference_reachable gauge",
`nemoclaw_inference_reachable ${inferenceReachable}`,
...llmMetricsLines(),
"",
].join("\n");
}

const server = http.createServer(async (req, res) => {
totalRequests += 1;
inflight += 1;
try {
if (req.url === "/healthz" || req.url === "/health") {
res.writeHead(200, { "content-type": "text/plain" });
res.end("ok\n");
return;
}
if (req.url === "/readyz" || req.url === "/ready") {
const ok = await checkInference();
inferenceReachable = ok ? 1 : 0;
res.writeHead(ok ? 200 : 503, { "content-type": "text/plain" });
res.end(ok ? "ready\n" : "ollama model not ready\n");
return;
}
if (req.url === "/metrics") {
res.writeHead(200, { "content-type": "text/plain; version=0.0.4" });
res.end(metricsText());
return;
}
const pathOnly = (req.url || "").split("?")[0];
if (
(pathOnly === "/v1/chat/completions" || pathOnly === "/chat/completions") &&
req.method === "POST"
) {
await proxyChatCompletions(req, res);
return;
}
if (req.url === "/" && req.method === "GET") {
res.writeHead(200, { "content-type": "application/json" });
res.end(
JSON.stringify({
service: "nemoclaw-gpu-agent",
model: MODEL,
inferenceBaseUrl: BASE_URL,
ollamaBaseUrl: OLLAMA_BASE,
endpoints: ["/healthz", "/readyz", "/metrics", "POST /v1/chat/completions"],
note: "Local Ollama on GPU; scale replicas with kubectl or HPA (one pod per GPU)",
}),
);
return;
}
res.writeHead(404);
res.end("not found\n");
} finally {
inflight -= 1;
}
});

server.listen(PORT, () => {
console.log(`nemoclaw-gpu-agent listening on :${PORT} model=${MODEL}`);
});

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.

📐 Maintainability & Code Quality | 🔴 Critical | 🏗️ Heavy lift

Resolve the codebase-growth guardrail before merge.

CI explicitly rejects this added .mjs source file. Move the implementation to an accepted source location/language or obtain the required maintainer-approved exception.

🧰 Tools
🪛 GitHub Actions: CI / Codebase Growth Guardrails / 0_codebase-growth-guardrails.txt

[error] 1-1: CI check failed: blocked JavaScript source file added (.mjs). File marked as added.

🤖 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 `@deploy/helm/nemoclaw-gpu/files/agent-server.mjs` around lines 1 - 181,
Resolve the codebase-growth guardrail for the agent server implementation by
moving the code from the added agent-server.mjs source file into an accepted
source location or language, preserving symbols such as proxyChatCompletions,
checkInference, metricsText, and the HTTP server behavior; alternatively, obtain
and record the required maintainer-approved exception before merging.

Source: Pipeline failures

Comment on lines +56 to +73
else
JOB_PARALLELISM="${JOB_PARALLELISM:-2}"
LOAD_MULTIPLIER="${LOAD_MULTIPLIER:-2}"
MAX_TOKENS="${MAX_TOKENS:-128}"
HPA_TARGET_GPU="${HPA_TARGET_GPU:-40}"
INFLIGHT_PER_GPU="${INFLIGHT_PER_GPU:-64}"
LOAD_COMPENSATION_SAFETY="${LOAD_COMPENSATION_SAFETY:-3}"
MAX_COMPENSATION="${MAX_COMPENSATION:-8}"
MAX_INFLIGHT_PER_POD="${MAX_INFLIGHT_PER_POD:-512}"
WARMUP_SEC="${WARMUP_SEC:-90}"
BOOTSTRAP_INFLIGHT="${BOOTSTRAP_INFLIGHT:-8}"
NEW_POD_WARMUP_PARALLEL="${NEW_POD_WARMUP_PARALLEL:-8}"
RAMP_SEC="${RAMP_SEC:-20}"
ESCALATE_INTERVAL_SEC="${ESCALATE_INTERVAL_SEC:-15}"
ESCALATE_FACTOR="${ESCALATE_FACTOR:-0.4}"
ESCALATE_MAX_MULT="${ESCALATE_MAX_MULT:-2}"
SCALE_UP_POLL_SEC="${SCALE_UP_POLL_SEC:-10}"
fi

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.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

Initialize NEW_POD_RAMP_SEC for clusters with fewer than four GPUs.

The else branch leaves this variable unset, but Line 265 expands it under set -u, terminating the script before the Job is created.

Proposed fix.
 else
   JOB_PARALLELISM="${JOB_PARALLELISM:-2}"
+  NEW_POD_RAMP_SEC="${NEW_POD_RAMP_SEC:-0}"

Also applies to: 264-265

🤖 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 `@deploy/helm/nemoclaw-gpu/scripts/hpa-load-test.sh` around lines 56 - 73,
Initialize NEW_POD_RAMP_SEC in the else branch alongside the other load-test
defaults, assigning the intended default value before the later expansion in the
Job creation flow. Ensure the variable is always set for clusters with fewer
than four GPUs so set -u cannot terminate the script.

@coderabbitai coderabbitai Bot mentioned this pull request Jul 16, 2026
21 tasks
@maggiezha

maggiezha commented Jul 16, 2026

Copy link
Copy Markdown
Author

close it as it was a testing with CodeRabbit team to make it work with my branch that was forked from main, the changes are from adding K8s autoscaling for NemoClaw, I will need to clean the code

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.

2 participants