Skip to content

feat(hermes): add provider onboarding foundation#3237

Closed
shannonsands wants to merge 8 commits into
NVIDIA:mainfrom
NousResearch:ns322/hermes-provider-foundation
Closed

feat(hermes): add provider onboarding foundation#3237
shannonsands wants to merge 8 commits into
NVIDIA:mainfrom
NousResearch:ns322/hermes-provider-foundation

Conversation

@shannonsands

@shannonsands shannonsands commented May 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds the cleaned Hermes Provider onboarding foundation directly against main. This slice keeps managed-tool broker and Discord bridge work out of scope, supports host-managed Nous OAuth/API-key inference setup, and preserves bare nemoclaw onboard as the OpenClaw default while nemohermes onboard / nemoclaw onboard --agent hermes selects Hermes.

Related Issue

Linear: NS-322, NS-324.

Changes

  • Adds Hermes Provider auth choice for Nous Portal OAuth versus Nous API Key, with API key scoped to inference only.
  • Registers hermes-provider through OpenShell as OpenAI-compatible host-managed inference against https://inference-api.nousresearch.com/v1, while sandbox config uses https://inference.local/v1.
  • Fixes Hermes Provider rebuild preflight so OAuth/API-key rebuilds use host-side Hermes auth state instead of requiring unrelated OPENAI_API_KEY / already-stored NOUS_API_KEY env vars.
  • Loads the Hermes Provider model picker from the Nous Portal recommended-models endpoint when available, while keeping the static Hermes catalog as the offline/full-list fallback.
  • Expands Hermes Provider model prompting with a top-10 menu and full-list/custom fallback.
  • Collects and emits Slack allowed human member IDs for the Hermes Slack config path.
  • Keeps policy preset changes and Discord bridge/proxy work out of this PR; Discord remains blocked on upstream OpenShell/NemoClaw websocket handling.

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)

Verification

  • npx prek run --all-files passes
  • npm test passes
  • Tests added or updated for new or changed behavior
  • No secrets, API keys, or credentials committed
  • Docs updated for user-facing behavior changes
  • make 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)

Focused checks run locally:

  • npm run build:cli passes
  • npm run typecheck:cli passes
  • git diff --check passes
  • npx vitest run src/lib/inference/config.test.ts src/lib/inference/model-prompts.test.ts src/lib/inference/nous-models.test.ts test/hermes-provider-foundation.test.ts test/rebuild-credential-preflight.test.ts passes (48 tests)
  • node -e smoke via dist/lib/inference/nous-models returns the live Nous Portal top-10 recommendation order

Earlier focused PR set before the dynamic-model update:

  • npx vitest run test/rebuild-credential-preflight.test.ts src/lib/sandbox-channels.test.ts src/lib/messaging-channel-config.test.ts test/generate-hermes-config.test.ts test/hermes-provider-foundation.test.ts test/hermes-plugin-handlers.test.ts test/sandbox-init.test.ts test/recover-port-forward.test.ts src/lib/inference/config.test.ts src/lib/inference/model-prompts.test.ts src/lib/agent/runtime.test.ts src/lib/state/sandbox-session.test.ts src/lib/hermes-provider-auth.test.ts src/lib/oauth-device-code.test.ts test/process-recovery.test.ts src/lib/cli/oclif-pattern-discovery.test.ts src/lib/cli/oclif-metadata.test.ts src/lib/cli/public-oclif-help.test.ts test/shellquote-sandbox.test.ts test/snapshot-gateway-guard.test.ts test/uninstall.test.ts passed (189 tests)

Broad hook status:

  • Pre-commit/pre-push broad CLI coverage was attempted before and during this update, but it is not clean in this local runtime. It failed or hung on unrelated runtime-sensitive tests, including CLI dispatch seeing OpenShell gateway startup output, prompt/guard process status returning null, and shell helper timeouts. The branch was pushed with hook bypass for this draft PR after the focused checks passed.

Signed-off-by: Shannon Sands shannon.sands.1979@gmail.com

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Hermes Provider as a new inference provider option with OAuth and API key authentication support.
    • Added Slack user allowlist configuration for messaging.
    • Added dynamic model selection for Hermes Provider with Nous-recommended models.
    • Extended credentials support for NOUS_API_KEY.
  • Bug Fixes

    • Fixed ANSI color code handling in output parsing.

@copy-pr-bot

copy-pr-bot Bot commented May 8, 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 May 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces Hermes Provider support with OAuth 2.0 device-code authentication flow, host-side credential persistence, and full integration into the onboarding and rebuild workflows. It also adds Slack user allowlist support for messaging channels and extends model recommendation fetching from remote catalogs.

Changes

Hermes Provider OAuth & Device-Code Integration

Layer / File(s) Summary
OAuth Device Code Protocol
src/lib/oauth-device-code.ts, src/lib/oauth-device-code.test.ts
Implements OAuth 2.0 Device Authorization Grant flow with request/response types, polling, error handling, token refresh, and agent-key minting for Hermes onboarding.
Nous Model Recommendation Fetching
src/lib/inference/nous-models.ts, src/lib/inference/nous-models.test.ts
Adds model recommendation extraction from portal catalogs, option merging with deduplication, and remote catalog fetch with timeout and fallback handling.
Hermes Provider Credential Management
src/lib/hermes-provider-auth.ts, src/lib/hermes-provider-auth.test.ts
Implements host-side credential state persistence with atomic JSON writes, OAuth token refresh/TTL checking, agent-key minting and freshness validation, and inference provider registration.
Credential Infrastructure
src/lib/credentials/store.ts, src/lib/security/redact.ts
Adds NOUS_API_KEY to known credential allowlist and secret redaction regex for logging.
Inference Provider Configuration
src/lib/inference/config.ts, src/lib/inference/config.test.ts
Extends inference config with Hermes provider selection, model options constant, recommended models URL, and provider switch case.
Model Prompt UI Extensions
src/lib/inference/model-prompts.ts, src/lib/inference/model-prompts.test.ts
Adds Hermes model options to remote selection, introduces top-level model limit and full-list toggle, and conditionally routes "Other..." option.
Slack User Allowlist
src/lib/sandbox-channels.ts, src/lib/sandbox-channels.test.ts, src/lib/messaging-channel-config.test.ts
Adds Slack user-ID allowlist metadata fields, DM access mode, and environment variable generation for allowed user IDs.
Slack Messaging Config
agents/hermes/config/messaging-config.ts, test/generate-hermes-config.test.ts
Adds SLACK_ALLOWED_USERS environment line generation when allowlist is non-empty.
Onboarding Session State
src/lib/state/onboard-session.ts
Adds hermesAuthMethod field to session interfaces and persists through onboarding lifecycle.
Remote Provider Registration
src/lib/onboard/providers.ts
Adds Hermes Provider entry with endpoint URL, credential env, help URL, and extends non-interactive provider aliasing for Hermes variants.
Onboarding Workflow Integration
src/lib/onboard.ts
Integrates Hermes Provider into full onboarding: auth method selection, credential preparation, agent/provider selection, model prompting, and inference setup.
Rebuild Credential Preflight
src/lib/actions/sandbox/rebuild.ts
Adds Hermes-specific credential preflight validation using host OAuth/API-key state with targeted error messages and skips generic credential checks.
Sandbox State Utilities
src/lib/state/sandbox-session.ts, src/lib/state/sandbox-session.test.ts
Adds ANSI escape-code stripping to forward-list parsing for colorized CLI output.
Test Coverage
test/hermes-provider-foundation.test.ts, test/rebuild-credential-preflight.test.ts, test/credentials.test.ts
End-to-end Vitest suites for onboarding scenarios, rebuild preflight, OAuth credential flows, and credential allowlist validation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Whiskers twitch at Hermes' call,
OAuth flows through tokens all,
Slack users joined by careful hand,
Provider magic, oh so grand!
Device codes dance through CLI's plea,
Credentials persist, wild and free!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.31% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(hermes): add provider onboarding foundation' clearly and concisely describes the main change: introducing foundational Hermes Provider onboarding support. It aligns with the primary objective to add Hermes Provider auth choice and registration, matching the changeset's scope.
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.

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

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

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

@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

Caution

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

⚠️ Outside diff range comments (2)
src/lib/actions/sandbox/rebuild.ts (1)

242-260: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Treat a missing session.sandboxName as unknown, not matched.

!session?.sandboxName lets an orphaned or partially-written session supply provider and credentialEnv for this rebuild. A stale session from a different sandbox can then run the wrong preflight and block a valid rebuild before any destructive work starts.

Suggested fix
-  const sessionMatchesTarget = !session?.sandboxName || session.sandboxName === sandboxName;
+  const sessionMatchesTarget = session?.sandboxName === sandboxName;
   let rebuildCredentialEnv: string | null = null;
-  if (!sessionMatchesTarget) {
+  if (!sessionMatchesTarget) {
     // Session belongs to a different sandbox — its credentialEnv may be
     // wrong (e.g. hermes session while rebuilding openclaw). Resolve the
     // target sandbox provider from the registry instead so destructive
     // operations still get a credential preflight for the sandbox being rebuilt.
     rebuildCredentialEnv = getRebuildCredentialEnvFromRegistry(sb.provider);
-    log(
-      `Preflight warning: session belongs to '${session.sandboxName}', not '${sandboxName}' — using registry credential env ${rebuildCredentialEnv || "(none)"}`,
-    );
-    console.log(
-      `  ${D}Note: onboard session belongs to '${session.sandboxName}', not '${sandboxName}'. ` +
-        `Using the '${sandboxName}' registry entry for credential preflight.${R}`,
-    );
+    if (session?.sandboxName) {
+      log(
+        `Preflight warning: session belongs to '${session.sandboxName}', not '${sandboxName}' — using registry credential env ${rebuildCredentialEnv || "(none)"}`,
+      );
+      console.log(
+        `  ${D}Note: onboard session belongs to '${session.sandboxName}', not '${sandboxName}'. ` +
+          `Using the '${sandboxName}' registry entry for credential preflight.${R}`,
+      );
+    }
   } else {
     rebuildCredentialEnv = session?.credentialEnv || 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 `@src/lib/actions/sandbox/rebuild.ts` around lines 242 - 260, The logic in
sessionMatchesTarget currently treats a missing session.sandboxName as a match,
letting orphaned sessions supply provider/credentialEnv; change
sessionMatchesTarget to only be true when session.sandboxName is present and
equals sandboxName (i.e. session?.sandboxName === sandboxName). This will ensure
getRebuildCredentialEnvFromRegistry(sb.provider) is used when sandboxName is
unknown and that rebuildCredentialEnv and rebuildProvider (referenced by
rebuildCredentialEnv, sessionMatchesTarget, rebuildProvider,
getRebuildCredentialEnvFromRegistry) come from the registry unless the session
explicitly belongs to the target sandbox.
src/lib/onboard.ts (1)

4151-4164: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Custom dashboard ports can be falsely rejected during healthy-gateway reuse

Line 4163 still checks port === DASHBOARD_PORT, so when --control-ui-port is set, preflight can fail even if that custom port is already owned by the healthy runtime.

💡 Proposed fix
   for (const { port, label, envVar } of requiredPorts) {
     let portCheck = await checkPortAvailable(port);
     if (!portCheck.ok) {
-      if ((port === GATEWAY_PORT || port === DASHBOARD_PORT) && gatewayReuseState === "healthy") {
+      const isRequestedDashboardPort =
+        dashboardPortToCheck !== null && port === dashboardPortToCheck;
+      if ((port === GATEWAY_PORT || isRequestedDashboardPort) && gatewayReuseState === "healthy") {
         console.log(
           `  ✓ Port ${port} already owned by healthy ${cliDisplayName()} runtime (${label})`,
         );
         continue;
       }
🤖 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.ts` around lines 4151 - 4164, The preflight check erroneously
compares the numeric port to DASHBOARD_PORT, rejecting custom dashboard ports;
update the conditional inside the requiredPorts loop (where checkPortAvailable
is used) to detect the dashboard entry by its envVar instead of only
DASHBOARD_PORT—e.g. replace the `(port === GATEWAY_PORT || port ===
DASHBOARD_PORT)` check with `(port === GATEWAY_PORT || envVar ===
"NEMOCLAW_DASHBOARD_PORT")` (or include both port === DASHBOARD_PORT || envVar
=== "NEMOCLAW_DASHBOARD_PORT") so custom `--control-ui-port` values (the port
variable) are treated as the dashboard and can be reused when `gatewayReuseState
=== "healthy"`.
🧹 Nitpick comments (3)
test/hermes-provider-foundation.test.ts (1)

34-38: ⚡ Quick win

Make spawned onboarding tests hermetic by scrubbing inherited messaging env.

These spawns currently inherit all of process.env, so local/CI DISCORD_* or TELEGRAM_* values can leak into onboarding paths and destabilize assertions.

Proposed hardening
+function buildHermeticEnv(tmpDir: string, extra: Record<string, string> = {}) {
+  const env: NodeJS.ProcessEnv = { ...process.env, HOME: tmpDir, ...extra };
+  for (const key of Object.keys(env)) {
+    if (key.startsWith("DISCORD_") || key.startsWith("TELEGRAM_")) {
+      delete env[key];
+    }
+  }
+  return env;
+}
...
-    const result = spawnSync(process.execPath, [scriptPath], {
+    const result = spawnSync(process.execPath, [scriptPath], {
       cwd: repoRoot,
       encoding: "utf-8",
-      env: { ...process.env, HOME: tmpDir },
+      env: buildHermeticEnv(tmpDir),
     });
Based on learnings: “For hermetic messaging-channel tests… delete/remove unrelated messaging env vars such as DISCORD_* and TELEGRAM_*.”

Also applies to: 74-80, 122-130

🤖 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/hermes-provider-foundation.test.ts` around lines 34 - 38, The spawnSync
invocations in hermes-provider-foundation.test.ts currently pass a full copy of
process.env (e.g. the spawnSync call that produces result) which can leak
DISCORD_* or TELEGRAM_* values; create a sanitizedEnv by shallow-copying
process.env, delete any keys that start with "DISCORD_" or "TELEGRAM_" (e.g.
using Object.keys(safeEnv).forEach(k => { if (/^(DISCORD_|TELEGRAM_)/.test(k))
delete safeEnv[k]; })), then pass env: { ...safeEnv, HOME: tmpDir } to
spawnSync; apply the same change to the other spawnSync sites in this test file
so all onboarding tests are hermetic.
src/lib/state/onboard-session.ts (1)

130-130: ⚡ Quick win

Constrain hermesAuthMethod to known values before persisting.

Right now any string is accepted/read back. Restricting this to "oauth" or "api_key" prevents invalid session state from silently propagating.

Proposed tightening
+type HermesAuthMethod = "oauth" | "api_key";
...
-  hermesAuthMethod: string | null;
+  hermesAuthMethod: HermesAuthMethod | null;
...
-  hermesAuthMethod?: string;
+  hermesAuthMethod?: HermesAuthMethod | null;
...
+function readHermesAuthMethod(value: SessionJsonValue | undefined): HermesAuthMethod | null {
+  return value === "oauth" || value === "api_key" ? value : null;
+}
...
-    hermesAuthMethod: readString(data.hermesAuthMethod),
+    hermesAuthMethod: readHermesAuthMethod(data.hermesAuthMethod),
...
-  if (typeof updates.hermesAuthMethod === "string")
-    safe.hermesAuthMethod = updates.hermesAuthMethod;
+  if (updates.hermesAuthMethod === "oauth" || updates.hermesAuthMethod === "api_key") {
+    safe.hermesAuthMethod = updates.hermesAuthMethod;
+  } else if (updates.hermesAuthMethod === null) {
+    safe.hermesAuthMethod = null;
+  }

Also applies to: 314-315, 354-355, 716-717, 863-863

🤖 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/state/onboard-session.ts` at line 130, Constrain the hermesAuthMethod
field to a strict union and validate it before persisting/reading: change the
hermesAuthMethod declaration on the OnboardSession shape to the union type
"oauth" | "api_key" (or undefined), add a small type-guard/validation where the
session is written (validateHermesAuthMethod(value): value is "oauth" |
"api_key") and use it to reject or coerce any other string before saving, and
likewise validate/normalize when loading the session (return undefined or a safe
default for invalid values). Update all places that set or read hermesAuthMethod
to use the new union type and guard (references: hermesAuthMethod,
OnboardSession, and the read/write locations noted in the comment).
src/lib/onboard.ts (1)

1583-1598: ⚡ Quick win

selectOnboardAgent accepts resume and canPrompt but neither is used or forwarded

Line 9996 passes resume and canPrompt to selectOnboardAgent, but line 1592 only forwards { agentFlag, session } to agentOnboard.resolveAgent. Since resolveAgent only accepts those two parameters, remove resume and canPrompt from the selectOnboardAgent options to keep the contract accurate and avoid confusion.

🤖 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.ts` around lines 1583 - 1598, The selectOnboardAgent function
signature and option type should be simplified to remove unused resume and
canPrompt parameters: update the selectOnboardAgent declaration and its
parameter type to only accept agentFlag and session, and ensure the call to
agentOnboard.resolveAgent({ agentFlag, session }) remains unchanged; adjust any
TypeScript types or JSDoc for selectOnboardAgent to reflect the new
two-parameter contract so callers (e.g., the place passing resume/canPrompt) no
longer expect those options.
🤖 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/actions/sandbox/rebuild.ts`:
- Around line 108-125: The auth-method detection can incorrectly default to
"oauth" when session?.hermesAuthMethod and state?.auth_method are missing even
though an exported NOUS_API_KEY exists; update the logic around
normalizeHermesRebuildAuthMethod(...) and
hermesProviderAuth.HERMES_NOUS_API_KEY_CREDENTIAL_ENV so that the presence of
hydrateCredentialEnv(hermesProviderAuth.HERMES_NOUS_API_KEY_CREDENTIAL_ENV)
forces the chosen authMethod to "api_key". Concretely, compute envKey =
hydrateCredentialEnv(...), then set authMethod = "api_key" if envKey is truthy
(even if normalizeHermesRebuildAuthMethod returned null/undefined), and keep the
existing branches that check api_key vs oauth (functions/variables to touch:
normalizeHermesRebuildAuthMethod,
hermesProviderAuth.HERMES_NOUS_API_KEY_CREDENTIAL_ENV, hydrateCredentialEnv,
authMethod, credentialEnv, session?.hermesAuthMethod, state?.auth_method).

In `@src/lib/hermes-provider-auth.ts`:
- Around line 226-240: The registration currently ignores the negotiated base
URL and falls back to oauth.DEFAULT_INFERENCE_BASE_URL when persisting provider
info; update registerHermesInferenceProvider to pass the chosen baseUrl through
to the helper that persists API-key credentials, and modify
ensureHermesProviderApiKeyCredentials to accept a baseUrl parameter and use it
when calling upsertProvider (so HERMES_PROVIDER_NAME and the credential entry
are stored with the negotiated baseUrl rather than DEFAULT_INFERENCE_BASE_URL);
ensure all call sites of ensureHermesProviderApiKeyCredentials (including where
registerHermesInferenceProvider is invoked) are updated to forward the baseUrl.
- Line 1: Remove the top-level "// `@ts-nocheck`" and add explicit TypeScript
types across this CLI auth module: annotate all function signatures, parameter
and return types in exported functions, and type the imported symbols (e.g., the
oauth import, onboardProviders, and any credential/state objects) to match the
shapes used by the typed oauth-device-code.ts implementation; ensure the module
is type-checked under tsconfig.cli.json, add or reference appropriate
interfaces/types for device-code responses, token objects, and provider configs,
and update any untyped usages to satisfy the compiler.

In `@src/lib/inference/model-prompts.ts`:
- Around line 187-225: The topLevelLimit is ignored unless otherShowsFullList is
true because visibleOptions falls back to modelOptions; change visibleOptions to
always be modelOptions.slice(0, topLevelLimit) (use topLevelLimit computed
above) so the menu shows the top-N + "Other..." option regardless of
otherShowsFullList, adjust defaultChoice to treat the "Other..." entry
(visibleOptions.length + 1) as the fallback when defaultIndex is outside
visibleOptions regardless of shouldOfferFullList, and keep the branch that
handles index === visibleOptions.length but choose between
promptFullRemoteModelList and promptManualModelId based on shouldOfferFullList;
update references: topLevelLimit, shouldOfferFullList, visibleOptions,
defaultChoice, promptFullRemoteModelList, promptManualModelId, modelOptions,
options.otherShowsFullList.

In `@src/lib/oauth-device-code.ts`:
- Around line 210-218: The success path that parses TokenResponse only checks
for refresh_token; update the resp.status === 200 handling (the block that casts
to TokenResponse) to also validate that payload.access_token is present and
non-empty and throw an OAuthError (similar to the existing
"token_response_missing_refresh_token") when it's missing; ensure you reference
TokenResponse and reuse OAuthError for a clear error code/message so we never
return/persist a payload missing access_token.

---

Outside diff comments:
In `@src/lib/actions/sandbox/rebuild.ts`:
- Around line 242-260: The logic in sessionMatchesTarget currently treats a
missing session.sandboxName as a match, letting orphaned sessions supply
provider/credentialEnv; change sessionMatchesTarget to only be true when
session.sandboxName is present and equals sandboxName (i.e. session?.sandboxName
=== sandboxName). This will ensure
getRebuildCredentialEnvFromRegistry(sb.provider) is used when sandboxName is
unknown and that rebuildCredentialEnv and rebuildProvider (referenced by
rebuildCredentialEnv, sessionMatchesTarget, rebuildProvider,
getRebuildCredentialEnvFromRegistry) come from the registry unless the session
explicitly belongs to the target sandbox.

In `@src/lib/onboard.ts`:
- Around line 4151-4164: The preflight check erroneously compares the numeric
port to DASHBOARD_PORT, rejecting custom dashboard ports; update the conditional
inside the requiredPorts loop (where checkPortAvailable is used) to detect the
dashboard entry by its envVar instead of only DASHBOARD_PORT—e.g. replace the
`(port === GATEWAY_PORT || port === DASHBOARD_PORT)` check with `(port ===
GATEWAY_PORT || envVar === "NEMOCLAW_DASHBOARD_PORT")` (or include both port ===
DASHBOARD_PORT || envVar === "NEMOCLAW_DASHBOARD_PORT") so custom
`--control-ui-port` values (the port variable) are treated as the dashboard and
can be reused when `gatewayReuseState === "healthy"`.

---

Nitpick comments:
In `@src/lib/onboard.ts`:
- Around line 1583-1598: The selectOnboardAgent function signature and option
type should be simplified to remove unused resume and canPrompt parameters:
update the selectOnboardAgent declaration and its parameter type to only accept
agentFlag and session, and ensure the call to agentOnboard.resolveAgent({
agentFlag, session }) remains unchanged; adjust any TypeScript types or JSDoc
for selectOnboardAgent to reflect the new two-parameter contract so callers
(e.g., the place passing resume/canPrompt) no longer expect those options.

In `@src/lib/state/onboard-session.ts`:
- Line 130: Constrain the hermesAuthMethod field to a strict union and validate
it before persisting/reading: change the hermesAuthMethod declaration on the
OnboardSession shape to the union type "oauth" | "api_key" (or undefined), add a
small type-guard/validation where the session is written
(validateHermesAuthMethod(value): value is "oauth" | "api_key") and use it to
reject or coerce any other string before saving, and likewise validate/normalize
when loading the session (return undefined or a safe default for invalid
values). Update all places that set or read hermesAuthMethod to use the new
union type and guard (references: hermesAuthMethod, OnboardSession, and the
read/write locations noted in the comment).

In `@test/hermes-provider-foundation.test.ts`:
- Around line 34-38: The spawnSync invocations in
hermes-provider-foundation.test.ts currently pass a full copy of process.env
(e.g. the spawnSync call that produces result) which can leak DISCORD_* or
TELEGRAM_* values; create a sanitizedEnv by shallow-copying process.env, delete
any keys that start with "DISCORD_" or "TELEGRAM_" (e.g. using
Object.keys(safeEnv).forEach(k => { if (/^(DISCORD_|TELEGRAM_)/.test(k)) delete
safeEnv[k]; })), then pass env: { ...safeEnv, HOME: tmpDir } to spawnSync; apply
the same change to the other spawnSync sites in this test file so all onboarding
tests are hermetic.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 5baaf36e-d898-4cb3-bb3a-92d88d5a8495

📥 Commits

Reviewing files that changed from the base of the PR and between b1320d5 and 8ebe6c7.

📒 Files selected for processing (26)
  • agents/hermes/config/messaging-config.ts
  • src/lib/actions/sandbox/rebuild.ts
  • src/lib/credentials/store.ts
  • src/lib/hermes-provider-auth.test.ts
  • src/lib/hermes-provider-auth.ts
  • src/lib/inference/config.test.ts
  • src/lib/inference/config.ts
  • src/lib/inference/model-prompts.test.ts
  • src/lib/inference/model-prompts.ts
  • src/lib/inference/nous-models.test.ts
  • src/lib/inference/nous-models.ts
  • src/lib/messaging-channel-config.test.ts
  • src/lib/oauth-device-code.test.ts
  • src/lib/oauth-device-code.ts
  • src/lib/onboard.ts
  • src/lib/onboard/providers.ts
  • src/lib/sandbox-channels.test.ts
  • src/lib/sandbox-channels.ts
  • src/lib/security/redact.ts
  • src/lib/state/onboard-session.ts
  • src/lib/state/sandbox-session.test.ts
  • src/lib/state/sandbox-session.ts
  • test/credentials.test.ts
  • test/generate-hermes-config.test.ts
  • test/hermes-provider-foundation.test.ts
  • test/rebuild-credential-preflight.test.ts

Comment on lines +108 to +125
const authMethod =
normalizeHermesRebuildAuthMethod(session?.hermesAuthMethod) ||
normalizeHermesRebuildAuthMethod(state?.auth_method) ||
(credentialEnv === hermesProviderAuth.HERMES_NOUS_API_KEY_CREDENTIAL_ENV ? "api_key" : "oauth");

if (authMethod === "api_key") {
const hostStateKey = nonEmptyString(state?.api_key) || nonEmptyString(state?.access_token);
const envKey = hydrateCredentialEnv(hermesProviderAuth.HERMES_NOUS_API_KEY_CREDENTIAL_ENV);
log(
`Hermes Provider rebuild preflight: api_key state=${hostStateKey ? "present" : "missing"} env=${envKey ? "present" : "missing"}`,
);
if (hostStateKey || envKey) return true;
} else {
const refreshToken = nonEmptyString(state?.refresh_token);
log(
`Hermes Provider rebuild preflight: oauth refresh_token=${refreshToken ? "present" : "missing"}`,
);
if (refreshToken) return true;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Don't infer OAuth when NOUS_API_KEY is the only recovery material.

When session?.hermesAuthMethod and state?.auth_method are missing, this falls back to OAuth unless credentialEnv equals the Hermes API-key env. For Hermes rebuilds that value comes from the registry path and is OPENAI_API_KEY, so an exported NOUS_API_KEY is ignored and preflight fails even though the error text tells users to use it.

Suggested fix
 function preflightHermesProviderCredentials(
   sandboxName: string,
   session: Session | null,
   credentialEnv: string | null,
   log: (msg: string) => void,
 ): boolean {
   const state = hermesProviderAuth.loadHermesOAuthState(sandboxName);
+  const envKey = hydrateCredentialEnv(hermesProviderAuth.HERMES_NOUS_API_KEY_CREDENTIAL_ENV);
   const authMethod =
     normalizeHermesRebuildAuthMethod(session?.hermesAuthMethod) ||
     normalizeHermesRebuildAuthMethod(state?.auth_method) ||
+    (envKey ? "api_key" : null) ||
     (credentialEnv === hermesProviderAuth.HERMES_NOUS_API_KEY_CREDENTIAL_ENV ? "api_key" : "oauth");
 
   if (authMethod === "api_key") {
     const hostStateKey = nonEmptyString(state?.api_key) || nonEmptyString(state?.access_token);
-    const envKey = hydrateCredentialEnv(hermesProviderAuth.HERMES_NOUS_API_KEY_CREDENTIAL_ENV);
     log(
       `Hermes Provider rebuild preflight: api_key state=${hostStateKey ? "present" : "missing"} env=${envKey ? "present" : "missing"}`,
     );
📝 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
const authMethod =
normalizeHermesRebuildAuthMethod(session?.hermesAuthMethod) ||
normalizeHermesRebuildAuthMethod(state?.auth_method) ||
(credentialEnv === hermesProviderAuth.HERMES_NOUS_API_KEY_CREDENTIAL_ENV ? "api_key" : "oauth");
if (authMethod === "api_key") {
const hostStateKey = nonEmptyString(state?.api_key) || nonEmptyString(state?.access_token);
const envKey = hydrateCredentialEnv(hermesProviderAuth.HERMES_NOUS_API_KEY_CREDENTIAL_ENV);
log(
`Hermes Provider rebuild preflight: api_key state=${hostStateKey ? "present" : "missing"} env=${envKey ? "present" : "missing"}`,
);
if (hostStateKey || envKey) return true;
} else {
const refreshToken = nonEmptyString(state?.refresh_token);
log(
`Hermes Provider rebuild preflight: oauth refresh_token=${refreshToken ? "present" : "missing"}`,
);
if (refreshToken) return true;
const envKey = hydrateCredentialEnv(hermesProviderAuth.HERMES_NOUS_API_KEY_CREDENTIAL_ENV);
const authMethod =
normalizeHermesRebuildAuthMethod(session?.hermesAuthMethod) ||
normalizeHermesRebuildAuthMethod(state?.auth_method) ||
(envKey ? "api_key" : null) ||
(credentialEnv === hermesProviderAuth.HERMES_NOUS_API_KEY_CREDENTIAL_ENV ? "api_key" : "oauth");
if (authMethod === "api_key") {
const hostStateKey = nonEmptyString(state?.api_key) || nonEmptyString(state?.access_token);
log(
`Hermes Provider rebuild preflight: api_key state=${hostStateKey ? "present" : "missing"} env=${envKey ? "present" : "missing"}`,
);
if (hostStateKey || envKey) return true;
} else {
const refreshToken = nonEmptyString(state?.refresh_token);
log(
`Hermes Provider rebuild preflight: oauth refresh_token=${refreshToken ? "present" : "missing"}`,
);
if (refreshToken) 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/actions/sandbox/rebuild.ts` around lines 108 - 125, The auth-method
detection can incorrectly default to "oauth" when session?.hermesAuthMethod and
state?.auth_method are missing even though an exported NOUS_API_KEY exists;
update the logic around normalizeHermesRebuildAuthMethod(...) and
hermesProviderAuth.HERMES_NOUS_API_KEY_CREDENTIAL_ENV so that the presence of
hydrateCredentialEnv(hermesProviderAuth.HERMES_NOUS_API_KEY_CREDENTIAL_ENV)
forces the chosen authMethod to "api_key". Concretely, compute envKey =
hydrateCredentialEnv(...), then set authMethod = "api_key" if envKey is truthy
(even if normalizeHermesRebuildAuthMethod returned null/undefined), and keep the
existing branches that check api_key vs oauth (functions/variables to touch:
normalizeHermesRebuildAuthMethod,
hermesProviderAuth.HERMES_NOUS_API_KEY_CREDENTIAL_ENV, hydrateCredentialEnv,
authMethod, credentialEnv, session?.hermesAuthMethod, state?.auth_method).

@@ -0,0 +1,310 @@
// @ts-nocheck

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

# First, confirm the file exists and check line 1
head -20 src/lib/hermes-provider-auth.ts

Repository: NVIDIA/NemoClaw

Length of output: 804


🏁 Script executed:

# Check if tsconfig.cli.json exists and review its configuration
cat tsconfig.cli.json

Repository: NVIDIA/NemoClaw

Length of output: 693


🏁 Script executed:

# Look for any type errors that might require the `@ts-nocheck` pragma
# We'll need to see how the project is set up for typechecking
ls -la | grep -E "tsconfig|package.json"

Repository: NVIDIA/NemoClaw

Length of output: 264


🏁 Script executed:

wc -l src/lib/hermes-provider-auth.ts

Repository: NVIDIA/NemoClaw

Length of output: 95


🏁 Script executed:

cat -n src/lib/hermes-provider-auth.ts

Repository: NVIDIA/NemoClaw

Length of output: 10925


🏁 Script executed:

# Find other .ts files in src/lib and check if they use `@ts-nocheck` or are properly typed
find src/lib -name "*.ts" -type f | head -10

Repository: NVIDIA/NemoClaw

Length of output: 394


🏁 Script executed:

# Check a few representative CLI files to see their typing patterns
rg -l "function|async function" src/lib/*.ts | head -5

Repository: NVIDIA/NemoClaw

Length of output: 204


🏁 Script executed:

# Look for other `@ts-nocheck` pragmas in the codebase to understand the pattern
rg "@ts-nocheck" --type ts

Repository: NVIDIA/NemoClaw

Length of output: 2430


🏁 Script executed:

# Check if oauth-device-code and other imported modules have type definitions
head -30 src/lib/oauth-device-code.ts 2>/dev/null || echo "File not found or error"

Repository: NVIDIA/NemoClaw

Length of output: 1184


🏁 Script executed:

# Check the git history to see if this file was recently added
git log --oneline -n 20 src/lib/hermes-provider-auth.ts 2>/dev/null | head -5

Repository: NVIDIA/NemoClaw

Length of output: 126


🏁 Script executed:

# Verify the coding guideline is correctly stated
grep -A 2 "Type-check CLI code" coding_guidelines.md 2>/dev/null || rg "Type-check CLI" --type md

Repository: NVIDIA/NemoClaw

Length of output: 281


Remove @ts-nocheck and add TypeScript type annotations to this CLI auth module.

This file bypasses the required tsconfig.cli.json typecheck (per coding guidelines: "Type-check CLI code using tsconfig.cli.json"). Untyped function parameters and module interactions in this security-sensitive credential path can introduce state-shape and type errors unnoticed.

Removing the pragma alone is insufficient; the file needs type annotations on all function signatures, parameter types, and return types (e.g., the oauth and onboardProviders imports return untyped values that should be explicitly shaped). The imported oauth-device-code.ts module demonstrates the codebase already has properly typed infrastructure to work from.

🤖 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/hermes-provider-auth.ts` at line 1, Remove the top-level "//
`@ts-nocheck`" and add explicit TypeScript types across this CLI auth module:
annotate all function signatures, parameter and return types in exported
functions, and type the imported symbols (e.g., the oauth import,
onboardProviders, and any credential/state objects) to match the shapes used by
the typed oauth-device-code.ts implementation; ensure the module is type-checked
under tsconfig.cli.json, add or reference appropriate interfaces/types for
device-code responses, token objects, and provider configs, and update any
untyped usages to satisfy the compiler.

Comment on lines +226 to +240
function registerHermesInferenceProvider(
apiKey,
runOpenshell,
credentialEnv = HERMES_INFERENCE_CREDENTIAL_ENV,
baseUrl = oauth.DEFAULT_INFERENCE_BASE_URL,
) {
upsertProvider(
HERMES_PROVIDER_NAME,
"openai",
credentialEnv,
baseUrl,
{ [credentialEnv]: apiKey },
runOpenshell,
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Register the provider with the negotiated inference base URL.

Both call sites let registerHermesInferenceProvider() fall back to DEFAULT_INFERENCE_BASE_URL, so the endpoint saved in state is never used. That silently repoints sandbox or provider-specific Hermes setups back at production during registration.

Suggested fix
   if (runOpenshell) {
-    registerHermesInferenceProvider(state.agent_key, runOpenshell);
+    registerHermesInferenceProvider(
+      state.agent_key,
+      runOpenshell,
+      HERMES_INFERENCE_CREDENTIAL_ENV,
+      state.inference_base_url || oauth.DEFAULT_INFERENCE_BASE_URL,
+    );
   }

Apply the same idea to the API-key path by threading the intended base URL into ensureHermesProviderApiKeyCredentials().

Also applies to: 257-260, 286-292

🤖 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/hermes-provider-auth.ts` around lines 226 - 240, The registration
currently ignores the negotiated base URL and falls back to
oauth.DEFAULT_INFERENCE_BASE_URL when persisting provider info; update
registerHermesInferenceProvider to pass the chosen baseUrl through to the helper
that persists API-key credentials, and modify
ensureHermesProviderApiKeyCredentials to accept a baseUrl parameter and use it
when calling upsertProvider (so HERMES_PROVIDER_NAME and the credential entry
are stored with the negotiated baseUrl rather than DEFAULT_INFERENCE_BASE_URL);
ensure all call sites of ensureHermesProviderApiKeyCredentials (including where
registerHermesInferenceProvider is invoked) are updated to forward the baseUrl.

Comment on lines +187 to +225
const topLevelLimit =
options.topLevelModelLimit && options.topLevelModelLimit > 0
? Math.min(options.topLevelModelLimit, modelOptions.length)
: modelOptions.length;
const shouldOfferFullList =
options.otherShowsFullList === true && topLevelLimit < modelOptions.length;
const visibleOptions = shouldOfferFullList
? modelOptions.slice(0, topLevelLimit)
: modelOptions;
const defaultChoice =
shouldOfferFullList && defaultIndex >= visibleOptions.length
? visibleOptions.length + 1
: Math.min(defaultIndex, Math.max(visibleOptions.length - 1, 0)) + 1;

deps.writeLine("");
deps.writeLine(` ${label} models:`);
visibleOptions.forEach((option, index) => {
deps.writeLine(` ${index + 1}) ${option}`);
});
deps.writeLine(` ${visibleOptions.length + 1}) Other...`);
deps.writeLine("");

const choice = await deps.promptFn(` Choose model [${defaultChoice}]: `);
const navigation = deps.getNavigationChoiceFn(choice);
if (navigation === "back") {
return deps.backToSelection;
}
if (navigation === "exit") {
deps.exitFn();
}
const index = parseInt(choice || String(defaultChoice), 10) - 1;
if (Number.isFinite(index) && index >= 0 && index < visibleOptions.length) {
return visibleOptions[index];
}
if (shouldOfferFullList && index === visibleOptions.length) {
return promptFullRemoteModelList(label, modelOptions, defaultModel, validator, deps);
}

return promptManualModelId(` ${label} model id: `, label, validator, deps);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

topLevelModelLimit currently has no effect unless full-list mode is enabled.

visibleOptions falls back to the full model list whenever otherShowsFullList is false, so callers cannot get the documented “top N + Other...” menu with direct manual entry. This also makes hidden defaults resolve to the wrong default choice.

Suggested fix
   const defaultIndex = Math.max(0, modelOptions.indexOf(defaultModel));
   const topLevelLimit =
     options.topLevelModelLimit && options.topLevelModelLimit > 0
       ? Math.min(options.topLevelModelLimit, modelOptions.length)
       : modelOptions.length;
-  const shouldOfferFullList =
-    options.otherShowsFullList === true && topLevelLimit < modelOptions.length;
-  const visibleOptions = shouldOfferFullList
-    ? modelOptions.slice(0, topLevelLimit)
-    : modelOptions;
+  const hasHiddenOptions = topLevelLimit < modelOptions.length;
+  const shouldOfferFullList = options.otherShowsFullList === true && hasHiddenOptions;
+  const visibleOptions = hasHiddenOptions ? modelOptions.slice(0, topLevelLimit) : modelOptions;
   const defaultChoice =
-    shouldOfferFullList && defaultIndex >= visibleOptions.length
+    hasHiddenOptions && defaultIndex >= visibleOptions.length
       ? visibleOptions.length + 1
       : Math.min(defaultIndex, Math.max(visibleOptions.length - 1, 0)) + 1;
📝 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
const topLevelLimit =
options.topLevelModelLimit && options.topLevelModelLimit > 0
? Math.min(options.topLevelModelLimit, modelOptions.length)
: modelOptions.length;
const shouldOfferFullList =
options.otherShowsFullList === true && topLevelLimit < modelOptions.length;
const visibleOptions = shouldOfferFullList
? modelOptions.slice(0, topLevelLimit)
: modelOptions;
const defaultChoice =
shouldOfferFullList && defaultIndex >= visibleOptions.length
? visibleOptions.length + 1
: Math.min(defaultIndex, Math.max(visibleOptions.length - 1, 0)) + 1;
deps.writeLine("");
deps.writeLine(` ${label} models:`);
visibleOptions.forEach((option, index) => {
deps.writeLine(` ${index + 1}) ${option}`);
});
deps.writeLine(` ${visibleOptions.length + 1}) Other...`);
deps.writeLine("");
const choice = await deps.promptFn(` Choose model [${defaultChoice}]: `);
const navigation = deps.getNavigationChoiceFn(choice);
if (navigation === "back") {
return deps.backToSelection;
}
if (navigation === "exit") {
deps.exitFn();
}
const index = parseInt(choice || String(defaultChoice), 10) - 1;
if (Number.isFinite(index) && index >= 0 && index < visibleOptions.length) {
return visibleOptions[index];
}
if (shouldOfferFullList && index === visibleOptions.length) {
return promptFullRemoteModelList(label, modelOptions, defaultModel, validator, deps);
}
return promptManualModelId(` ${label} model id: `, label, validator, deps);
const topLevelLimit =
options.topLevelModelLimit && options.topLevelModelLimit > 0
? Math.min(options.topLevelModelLimit, modelOptions.length)
: modelOptions.length;
const hasHiddenOptions = topLevelLimit < modelOptions.length;
const shouldOfferFullList = options.otherShowsFullList === true && hasHiddenOptions;
const visibleOptions = hasHiddenOptions
? modelOptions.slice(0, topLevelLimit)
: modelOptions;
const defaultChoice =
hasHiddenOptions && defaultIndex >= visibleOptions.length
? visibleOptions.length + 1
: Math.min(defaultIndex, Math.max(visibleOptions.length - 1, 0)) + 1;
deps.writeLine("");
deps.writeLine(` ${label} models:`);
visibleOptions.forEach((option, index) => {
deps.writeLine(` ${index + 1}) ${option}`);
});
deps.writeLine(` ${visibleOptions.length + 1}) Other...`);
deps.writeLine("");
const choice = await deps.promptFn(` Choose model [${defaultChoice}]: `);
const navigation = deps.getNavigationChoiceFn(choice);
if (navigation === "back") {
return deps.backToSelection;
}
if (navigation === "exit") {
deps.exitFn();
}
const index = parseInt(choice || String(defaultChoice), 10) - 1;
if (Number.isFinite(index) && index >= 0 && index < visibleOptions.length) {
return visibleOptions[index];
}
if (shouldOfferFullList && index === visibleOptions.length) {
return promptFullRemoteModelList(label, modelOptions, defaultModel, validator, deps);
}
return promptManualModelId(` ${label} model id: `, label, validator, deps);
🤖 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/model-prompts.ts` around lines 187 - 225, The topLevelLimit
is ignored unless otherShowsFullList is true because visibleOptions falls back
to modelOptions; change visibleOptions to always be modelOptions.slice(0,
topLevelLimit) (use topLevelLimit computed above) so the menu shows the top-N +
"Other..." option regardless of otherShowsFullList, adjust defaultChoice to
treat the "Other..." entry (visibleOptions.length + 1) as the fallback when
defaultIndex is outside visibleOptions regardless of shouldOfferFullList, and
keep the branch that handles index === visibleOptions.length but choose between
promptFullRemoteModelList and promptManualModelId based on shouldOfferFullList;
update references: topLevelLimit, shouldOfferFullList, visibleOptions,
defaultChoice, promptFullRemoteModelList, promptManualModelId, modelOptions,
options.otherShowsFullList.

Comment on lines +210 to +218
if (resp.status === 200) {
const payload = (await resp.json()) as TokenResponse;
if (!payload.refresh_token) {
throw new OAuthError(
"token_response_missing_refresh_token",
"portal returned no refresh_token; cannot persist host-side auth state",
);
}
return payload;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate access_token on the initial token exchange.

This success path only rejects a 200 response when refresh_token is missing. If the portal returns a partial body, we persist unusable OAuth state here and the later agent-key mint ends up sending Bearer undefined, which obscures the real failure.

Suggested fix
     if (resp.status === 200) {
       const payload = (await resp.json()) as TokenResponse;
-      if (!payload.refresh_token) {
+      if (!payload.access_token || !payload.refresh_token) {
         throw new OAuthError(
-          "token_response_missing_refresh_token",
-          "portal returned no refresh_token; cannot persist host-side auth state",
+          "token_response_missing_tokens",
+          "portal returned no access_token or refresh_token; cannot persist host-side auth state",
         );
       }
       return payload;
     }
🤖 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/oauth-device-code.ts` around lines 210 - 218, The success path that
parses TokenResponse only checks for refresh_token; update the resp.status ===
200 handling (the block that casts to TokenResponse) to also validate that
payload.access_token is present and non-empty and throw an OAuthError (similar
to the existing "token_response_missing_refresh_token") when it's missing;
ensure you reference TokenResponse and reuse OAuthError for a clear error
code/message so we never return/persist a payload missing access_token.

@wscurran wscurran added NemoClaw CLI integration: hermes Hermes integration behavior labels May 8, 2026
@wscurran

wscurran commented May 8, 2026

Copy link
Copy Markdown
Contributor

✨ Thanks for submitting this PR that adds the Hermes Provider onboarding foundation. This change involves adding a new provider, modifying the onboarding process, and updating the OpenShell registration.

@wscurran wscurran requested a review from ericksoa May 8, 2026 16:56
ericksoa added a commit that referenced this pull request May 11, 2026
## Summary

Replays the Hermes Provider onboarding foundation from partner PR #3237
in a branch based on current `main`, with the credential boundary
adjusted to match NemoClaw/OpenShell security invariants.

This keeps bare `nemoclaw onboard` on the existing OpenClaw default
while allowing `nemohermes onboard` / `nemoclaw onboard --agent hermes`
to select Hermes Provider, choose Nous Portal OAuth or Nous API Key,
pick from Nous-recommended models with a full/custom fallback, and
register Hermes inference through OpenShell.

## Security / Architecture

- NemoClaw orchestrates Hermes/Nous auth, but does not durably persist
Hermes OAuth/API-key material in host-side NemoClaw files.
- Durable provider credentials are handed to OpenShell provider
registration and resolved from OpenShell primitives.
- OAuth is an in-memory authorization/minting step; the minted inference
credential is registered into OpenShell, and refresh/access tokens are
not written under `~/.nemoclaw`.
- API-key onboarding uses `NOUS_API_KEY` / `NEMOCLAW_PROVIDER_KEY` from
the current process and registers it into OpenShell; `NOUS_API_KEY` is
intentionally not added to NemoClaw's legacy host credential store
allowlist.
- Rebuild preflight checks the target Hermes Provider registration in
OpenShell and can register an exported `NOUS_API_KEY` before destructive
rebuild work.
- Session state stores only non-secret Hermes metadata such as
`hermesAuthMethod`; invalid auth methods are rejected/normalized.

## Credit

Based on the Hermes Provider foundation work from @shannonsands /
NousResearch in #3237. The replay commit includes Shannon as co-author
and retains Shannon's signed-off-by from the original contribution.

## Verification

- `npm run build:cli`
- `npm run typecheck:cli`
- `git diff --cached --check`
- `npx vitest run src/lib/hermes-provider-auth.test.ts
src/lib/oauth-device-code.test.ts src/lib/inference/config.test.ts
src/lib/inference/model-prompts.test.ts
src/lib/inference/nous-models.test.ts
src/lib/state/onboard-session.test.ts
src/lib/messaging-channel-config.test.ts
src/lib/sandbox-channels.test.ts src/lib/state/sandbox-session.test.ts
test/hermes-provider-foundation.test.ts
test/rebuild-credential-preflight.test.ts
test/generate-hermes-config.test.ts`
- `npx vitest run src/lib/oauth-device-code.test.ts`

---
Signed-off-by: Shannon Sands <shannon.sands.1979@gmail.com>
Signed-off-by: Aaron Erickson <aerickson@nvidia.com>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Hermes Provider added as an inference option with API-key and OAuth
onboarding, curated model defaults, remote recommended-model fetching,
and installer support for "hermes-provider"
  * Slack allowlist support via SLACK_ALLOWED_USERS env

* **Bug Fixes**
  * Strip terminal color codes from status output
  * Redact NOUS_API_KEY in logs

* **Tests**
* Expanded coverage for Hermes auth, OAuth device flow, model selection,
rebuild credential preflight, and onboarding flows

* **Docs**
  * Documented Hermes auth env vars and aliases

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/NVIDIA/NemoClaw/pull/3320)
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Aaron Erickson <aerickson@nvidia.com>
@ericksoa

Copy link
Copy Markdown
Contributor

Hi Shannon, thank you again for this PR and for the NousResearch partnership here. I wanted to leave the supersession trail explicitly on this thread so the attribution and review context are easy to follow.

We merged #3320 as the maintained replay of this Hermes Provider onboarding foundation. Functionally, #3320 carries forward the core work from this PR: Hermes Provider selection for nemohermes onboard / nemoclaw onboard --agent hermes, Nous Portal OAuth vs Nous API key onboarding, OpenShell hermes-provider registration against the Nous inference endpoint, the Nous-recommended model picker with full/custom fallback, Hermes rebuild preflight handling, Slack allowed-user configuration, and the focused coverage around those flows.

The main deltas in #3320 are around aligning the credential boundary with the current NemoClaw/OpenShell invariants and current main: it keeps durable provider credentials in OpenShell provider registration rather than NemoClaw host-side credential files, does not add NOUS_API_KEY to NemoClaw's legacy credential-store allowlist, treats OAuth/API-key material as an onboarding-time input rather than durable ~/.nemoclaw state, tightens agent manifest inference validation, adds installer/docs coverage for hermes-provider, and includes the follow-up test/CI hardening from review. The replay commit keeps Shannon as co-author and preserves the signed-off-by trail.

Really appreciate the contribution and the partnership from you and the NousResearch team. This PR gave us the foundation for the landed implementation; #3320 is the version we merged only so we could carry it across current main and tighten the credential ownership boundary for the release.

@ericksoa

Copy link
Copy Markdown
Contributor

Closing this as superseded by the merged shared implementation in #3320, with thanks again to Shannon and the NousResearch team for the foundation and collaboration here.

Please do let us know if there are further changes you would like based on the shared implementation now that #3320 has landed. We would be very happy to keep iterating with you from that merged base, whether as follow-up PRs, issues, or review notes on the parts of the provider flow that you think we should refine.

@ericksoa ericksoa closed this May 11, 2026
@wscurran wscurran added area: cli Command line interface, flags, terminal UX, or output area: providers Inference provider integrations and provider behavior feature PR adds or expands user-visible functionality and removed NemoClaw CLI labels Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: cli Command line interface, flags, terminal UX, or output area: providers Inference provider integrations and provider behavior feature PR adds or expands user-visible functionality integration: hermes Hermes integration behavior

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants