fix: hermes edition handling for status, configure, catalog and clawkeep - #364
Conversation
β¦ermes edition The status route read only `~/.openclaw/openclaw.json`, which does not exist on a Hermes device, so a signed-in box reported clawaiConfigured=false and the Remote Control panel kept prompting the user to sign in. Resolve the active provider, model and ClawBox AI profile from the Hermes config (via the mtime-memoised CLI helper) when the active harness is Hermes, keeping the OpenClaw path unchanged.
The Hermes SKU installs no openclaw binary, so findOpenclawBin's bare "openclaw" fallback turned every spawn into a raw "spawn openclaw ENOENT" deep inside a request. Add a typed OpenclawUnavailableError and an openclawIsAbsent() edition check, guard the shared spawnOpenclaw chokepoint with it, and skip the openclaw spawns in the model-catalog refresh, the local-ai disable path and the updater version/doctor probes. openclaw and dual editions are unaffected.
The configure route wrote OpenClaw config and shelled out to `openclaw config set` for every provider, so on Hermes the local-model (Gemma) switch and API-key providers failed with a spawn ENOENT and then told the user to check credentials that a local model does not have. On the hermes edition, route local models to applyLocalAiToHermes, ClawBox AI to applyClawaiToHermes, and API-key cloud providers to the Hermes credential store, and classify the catch-all error so a local or edition failure never reports a credential problem.
ClawKeep archives the OpenClaw agent through the openclaw CLI, so on the Hermes SKU it cannot run β yet it advised installing OpenClaw (`npm install -g openclaw`), which contradicts the edition. Surface supportedOnEdition from getStatus and, when false, show an honest "not available on this edition" card instead of the install instructions, with new copy in all ten locales.
π WalkthroughWalkthroughThe PR adds Hermes-aware AI model configuration and status handling, prevents OpenClaw CLI calls on unsupported editions, updates ClawKeep edition handling, and adds related tests and translations. ChangesHermes edition support
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant ConfigureRoute
participant HermesCloudProvider
participant HermesCLI
Client->>ConfigureRoute: submit provider configuration
ConfigureRoute->>HermesCloudProvider: apply API key
HermesCloudProvider->>HermesCLI: store credentials and resolve model
HermesCLI-->>HermesCloudProvider: provider and model result
HermesCloudProvider-->>ConfigureRoute: activation result
ConfigureRoute-->>Client: configuration response
Possibly related PRs
Suggested labels: Suggested reviewers: π₯ Pre-merge checks | β 4 | β 1β Failed checks (1 warning)
β Passed checks (4 passed)
β¨ Finishing Touchesπ§ͺ Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
π¦ ClawReviewClaws waving β here's what this change is about. This PR patches four places where code written for OpenClaw devices accidentally ran on Hermes β where the openclaw binary and ~/.openclaw config don't exist. The fixes cover: the AI-models status route (now reads Hermes config when the active harness is Hermes), a new typed guard at the spawnOpenclaw chokepoint, the configure route (routes local/ClawBox-AI/API-key providers through Hermes' own config store instead of shelling out), and ClawKeep (surfaces an honest 'not available on this edition' card instead of the misleading npm install remedy). Each fix ships with a regression test. At a glance
Good to know
β ClawReview π¦. I set the scene; CodeRabbit reviews the code; you decide. Conventions: docs. |
There was a problem hiding this comment.
Actionable comments posted: 10
π€ 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/app/setup-api/ai-models/configure/route.ts`:
- Around line 702-722: Update the Branch A condition in the openclawIsAbsent()
flow to recognize local providers by provider identity, not only isLocalScope:
route llama.cpp and Ollama requests through the existing local configuration and
applyLocalAiToHermes logic even when scope is "primary". Preserve the current
shouldPromoteLocalToPrimary behavior and model normalization, while leaving
non-local providers on their existing branches.
- Around line 709-720: Reorder the local-model setup around applyLocalAiToHermes
so Hermes activation succeeds before persisting local_ai_configured,
local_ai_provider, local_ai_model, and local_ai_configured_at via setMany. Keep
the existing provider and model normalization, then perform the state write only
after successful activation, matching the ordering used by
applyCloudProviderKeyToHermes.
- Around line 735-741: Update the Hermes setup flow around result.activated so
stored but unactivated keys return a 2xx success response, or explicitly handle
the 409 continuation path in AIModelsStep.tsx. Add an error discriminator to the
Hermes error classes and map validation failures, including missing or malformed
local model IDs and missing API keys, to HTTP 400 while preserving HTTP 502 for
Hermes CLI failures.
In `@src/app/setup-api/ai-models/status/route.ts`:
- Around line 314-315: Before the portal-token request that uses clawaiToken,
validate CLAWBOX_AI_DEVICE_INFO_URL as a trusted HTTPS origin using an explicit
allowlist, and reject non-HTTPS or unapproved origins before sending the Bearer
token. Configure the request to use redirect: "error" so the token cannot follow
an unvalidated redirect.
In `@src/lib/hermes-cloud-provider.ts`:
- Around line 92-99: Update the catch block in the model-option loading flow
around getModelOptions, isAllowedProvider, and scopeFromPayload to emit a
warning containing the caught error and relevant provider context before
preserving the existing fallback behavior. Use the moduleβs established logging
mechanism and keep returning with no selected model after logging.
- Around line 80-87: Replace raw Hermes stderr with authored, secret-safe error
messages in the failure paths around runHermesCli in
src/lib/hermes-cloud-provider.ts (lines 80-87 and 112-119),
src/lib/hermes-local-ai.ts, and src/lib/hermes-clawai.ts; log only redacted
diagnostics server-side. Update the configure route comment in
src/app/setup-api/ai-models/configure/route.ts (lines 749-757) to state that
client responses use authored messages rather than helper stderr.
In `@src/tests/routes/ai-models/catalog-edition.test.ts`:
- Line 9: Update the child_process mockβs spawn stub to return a
child-process-shaped object with the required stdout stream and event-listener
methods, and asynchronously emit close so the pending timeout is cleared.
Preserve the existing mock behavior while ensuring synchronous child.stdout
access succeeds and the Promise settles through the close path.
In `@src/tests/routes/ai-models/configure-hermes.test.ts`:
- Around line 155-169: Add a test in the local-model suite that posts a
llama.cpp configuration without the scope field, using the default primary scope
request shape, and assert the response succeeds and mockApplyLocalAiToHermes is
called. Keep the existing explicit scope: "local" coverage unchanged and retain
the no-OpenClaw assertion for this default-scope case.
In `@src/tests/routes/ai-models/configure.test.ts`:
- Around line 71-75: Re-seed openclawIsAbsent after vi.clearAllMocks() in both
test suites: import it in src/tests/routes/ai-models/configure.test.ts and
src/tests/routes/local-ai.test.ts, then add
vi.mocked(openclawIsAbsent).mockReturnValue(false) to each suiteβs beforeEach
re-seed block. Apply the corresponding changes at
src/tests/routes/ai-models/configure.test.ts lines 71-75 and
src/tests/routes/local-ai.test.ts lines 17-19; both sites require the same fix.
In `@src/tests/unit/openclaw-config.test.ts`:
- Around line 123-128: Add spawn: vi.fn() to the child_process mock used by the
openclawConfig tests, then update the Hermes runOpenclawConfigSet test to assert
spawn was not called after the OpenclawUnavailableError rejection. Keep the
existing execFile mock and typed-error assertion unchanged.
πͺ Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
βΉοΈ Review info
βοΈ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 02979dd1-84bf-47d0-9ef2-c12789de88cd
π Files selected for processing (17)
src/app/setup-api/ai-models/catalog/route.tssrc/app/setup-api/ai-models/configure/route.tssrc/app/setup-api/ai-models/status/route.tssrc/app/setup-api/local-ai/route.tssrc/components/ClawKeepApp.tsxsrc/lib/clawkeep-translations.tssrc/lib/clawkeep.tssrc/lib/hermes-cloud-provider.tssrc/lib/openclaw-config.tssrc/lib/updater.tssrc/tests/routes/ai-models/catalog-edition.test.tssrc/tests/routes/ai-models/configure-hermes.test.tssrc/tests/routes/ai-models/configure.test.tssrc/tests/routes/ai-models/status.test.tssrc/tests/routes/local-ai.test.tssrc/tests/unit/clawkeep-edition.test.tssrc/tests/unit/openclaw-config.test.ts
| if (openclawIsAbsent()) { | ||
| try { | ||
| if (isLocalScope && (isLlamaCpp || isOllama)) { | ||
| // On-device model (Gemma via llama.cpp, or Ollama): persist the same | ||
| // config-store keys the OpenClaw path would, then register it with | ||
| // Hermes as an available provider (activating it only on a fresh | ||
| // device, matching shouldPromoteLocalToPrimary). No gateway restart. | ||
| await setMany({ | ||
| local_ai_configured: true, | ||
| local_ai_provider: ocProvider, | ||
| local_ai_model: config.defaultModel, | ||
| local_ai_configured_at: new Date().toISOString(), | ||
| }); | ||
| await applyLocalAiToHermes({ | ||
| provider: ocProvider as "llamacpp" | "ollama", | ||
| // Hermes wants the bare model id, not the `llamacpp/β¦` qualified form. | ||
| model: config.defaultModel.replace(/^(?:llamacpp|ollama)\//, ""), | ||
| makeDefault: shouldPromoteLocalToPrimary, | ||
| }); | ||
| return NextResponse.json({ success: true }); | ||
| } |
There was a problem hiding this comment.
π― Functional Correctness | π Major | ποΈ Heavy lift
Local providers with primary scope fall into the cloud-provider branch on Hermes.
Branch A gates on isLocalScope. A llama.cpp or Ollama request with the default scope: "primary" skips it.
Trace { provider: "llamacpp", apiKey: "gemma-q4" } (primary scope β the shape configure.test.ts Line 665 already posts):
- Line 704:
isLocalScopeis false, so Branch A does not run. - Line 723: not ClawAI.
- Line 727:
authModeis"token"andnormalizedApiKeyis"gemma-q4", so Branch C runs. applyCloudProviderKeyToHermes({ openclawProvider: "llamacpp", β¦ })callshermesKeyProviderFor("llamacpp"), which returnsnull.- The helper throws
HermesCloudApplyError, and the route returns 502 "This provider is set up through the Hermes provider panel on this edition."
For llama.cpp the apiKey field carries the model name, not a credential β Line 627 documents this. The on-device model is therefore treated as an unmapped cloud provider.
{ provider: "llamacpp" } with no apiKey is also wrong: normalizedApiKey is empty, so Branch C is skipped and the Line 745 fallback returns 400 with the same provider-panel message.
Gate Branch A on the provider, not the scope.
π Proposed fix for the branch gate
- if (isLocalScope && (isLlamaCpp || isOllama)) {
+ if (isLlamaCpp || isOllama) {
// On-device model (Gemma via llama.cpp, or Ollama): persist the same
// config-store keys the OpenClaw path would, then register it with
// Hermes as an available provider (activating it only on a fresh
// device, matching shouldPromoteLocalToPrimary). No gateway restart.
await setMany({
local_ai_configured: true,
local_ai_provider: ocProvider,
local_ai_model: config.defaultModel,
local_ai_configured_at: new Date().toISOString(),
});
await applyLocalAiToHermes({
provider: ocProvider as "llamacpp" | "ollama",
// Hermes wants the bare model id, not the `llamacpp/β¦` qualified form.
model: config.defaultModel.replace(/^(?:llamacpp|ollama)\//, ""),
- makeDefault: shouldPromoteLocalToPrimary,
+ // A primary-scope request selects the model explicitly, so activate
+ // it; a local-scope request activates only on a fresh device.
+ makeDefault: !isLocalScope || shouldPromoteLocalToPrimary,
});
return NextResponse.json({ success: true });
}π 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.
| if (openclawIsAbsent()) { | |
| try { | |
| if (isLocalScope && (isLlamaCpp || isOllama)) { | |
| // On-device model (Gemma via llama.cpp, or Ollama): persist the same | |
| // config-store keys the OpenClaw path would, then register it with | |
| // Hermes as an available provider (activating it only on a fresh | |
| // device, matching shouldPromoteLocalToPrimary). No gateway restart. | |
| await setMany({ | |
| local_ai_configured: true, | |
| local_ai_provider: ocProvider, | |
| local_ai_model: config.defaultModel, | |
| local_ai_configured_at: new Date().toISOString(), | |
| }); | |
| await applyLocalAiToHermes({ | |
| provider: ocProvider as "llamacpp" | "ollama", | |
| // Hermes wants the bare model id, not the `llamacpp/β¦` qualified form. | |
| model: config.defaultModel.replace(/^(?:llamacpp|ollama)\//, ""), | |
| makeDefault: shouldPromoteLocalToPrimary, | |
| }); | |
| return NextResponse.json({ success: true }); | |
| } | |
| if (openclawIsAbsent()) { | |
| try { | |
| if (isLlamaCpp || isOllama) { | |
| // On-device model (Gemma via llama.cpp, or Ollama): persist the same | |
| // config-store keys the OpenClaw path would, then register it with | |
| // Hermes as an available provider (activating it only on a fresh | |
| // device, matching shouldPromoteLocalToPrimary). No gateway restart. | |
| await setMany({ | |
| local_ai_configured: true, | |
| local_ai_provider: ocProvider, | |
| local_ai_model: config.defaultModel, | |
| local_ai_configured_at: new Date().toISOString(), | |
| }); | |
| await applyLocalAiToHermes({ | |
| provider: ocProvider as "llamacpp" | "ollama", | |
| // Hermes wants the bare model id, not the `llamacpp/β¦` qualified form. | |
| model: config.defaultModel.replace(/^(?:llamacpp|ollama)\//, ""), | |
| // A primary-scope request selects the model explicitly, so activate | |
| // it; a local-scope request activates only on a fresh device. | |
| makeDefault: !isLocalScope || shouldPromoteLocalToPrimary, | |
| }); | |
| return NextResponse.json({ success: 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/app/setup-api/ai-models/configure/route.ts` around lines 702 - 722,
Update the Branch A condition in the openclawIsAbsent() flow to recognize local
providers by provider identity, not only isLocalScope: route llama.cpp and
Ollama requests through the existing local configuration and
applyLocalAiToHermes logic even when scope is "primary". Preserve the current
shouldPromoteLocalToPrimary behavior and model normalization, while leaving
non-local providers on their existing branches.
| await setMany({ | ||
| local_ai_configured: true, | ||
| local_ai_provider: ocProvider, | ||
| local_ai_model: config.defaultModel, | ||
| local_ai_configured_at: new Date().toISOString(), | ||
| }); | ||
| await applyLocalAiToHermes({ | ||
| provider: ocProvider as "llamacpp" | "ollama", | ||
| // Hermes wants the bare model id, not the `llamacpp/β¦` qualified form. | ||
| model: config.defaultModel.replace(/^(?:llamacpp|ollama)\//, ""), | ||
| makeDefault: shouldPromoteLocalToPrimary, | ||
| }); |
There was a problem hiding this comment.
ποΈ Data Integrity & Integration | π Major | β‘ Quick win
Persist the local-model state only after Hermes accepts it.
setMany marks local_ai_configured: true before applyLocalAiToHermes runs. If the Hermes CLI call fails, the route returns 502 while the config store already reports the local model as configured. Hermes has no matching provider block at that point, so the status route reports a model that the device cannot serve.
applyCloudProviderKeyToHermes in src/lib/hermes-cloud-provider.ts (Lines 121-127) uses the opposite order: it writes ai_model_configured only after activation succeeds. Apply the same order here.
π Proposed reorder
- await setMany({
- local_ai_configured: true,
- local_ai_provider: ocProvider,
- local_ai_model: config.defaultModel,
- local_ai_configured_at: new Date().toISOString(),
- });
await applyLocalAiToHermes({
provider: ocProvider as "llamacpp" | "ollama",
// Hermes wants the bare model id, not the `llamacpp/β¦` qualified form.
model: config.defaultModel.replace(/^(?:llamacpp|ollama)\//, ""),
makeDefault: shouldPromoteLocalToPrimary,
});
+ await setMany({
+ local_ai_configured: true,
+ local_ai_provider: ocProvider,
+ local_ai_model: config.defaultModel,
+ local_ai_configured_at: new Date().toISOString(),
+ });
return NextResponse.json({ success: true });π 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.
| await setMany({ | |
| local_ai_configured: true, | |
| local_ai_provider: ocProvider, | |
| local_ai_model: config.defaultModel, | |
| local_ai_configured_at: new Date().toISOString(), | |
| }); | |
| await applyLocalAiToHermes({ | |
| provider: ocProvider as "llamacpp" | "ollama", | |
| // Hermes wants the bare model id, not the `llamacpp/β¦` qualified form. | |
| model: config.defaultModel.replace(/^(?:llamacpp|ollama)\//, ""), | |
| makeDefault: shouldPromoteLocalToPrimary, | |
| }); | |
| await applyLocalAiToHermes({ | |
| provider: ocProvider as "llamacpp" | "ollama", | |
| // Hermes wants the bare model id, not the `llamacpp/β¦` qualified form. | |
| model: config.defaultModel.replace(/^(?:llamacpp|ollama)\//, ""), | |
| makeDefault: shouldPromoteLocalToPrimary, | |
| }); | |
| await setMany({ | |
| local_ai_configured: true, | |
| local_ai_provider: ocProvider, | |
| local_ai_model: config.defaultModel, | |
| local_ai_configured_at: new Date().toISOString(), | |
| }); |
π€ 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/app/setup-api/ai-models/configure/route.ts` around lines 709 - 720,
Reorder the local-model setup around applyLocalAiToHermes so Hermes activation
succeeds before persisting local_ai_configured, local_ai_provider,
local_ai_model, and local_ai_configured_at via setMany. Keep the existing
provider and model normalization, then perform the state write only after
successful activation, matching the ordering used by
applyCloudProviderKeyToHermes.
| if (result.activated) { | ||
| return NextResponse.json({ success: true }); | ||
| } | ||
| return NextResponse.json( | ||
| { error: "Key saved. Open the Hermes provider panel to pick a model for it." }, | ||
| { status: 409 }, | ||
| ); |
There was a problem hiding this comment.
ποΈ Data Integrity & Integration | π΅ Trivial | β‘ Quick win
π§© Analysis chain
π Script executed:
#!/bin/bash
# Find the client code that posts to the configure route and inspect its status handling.
rg -nP -C 10 'ai-models/configure' src/components/ src/hooks/ src/app/ -g '!**/route.ts'Repository: ID-Robots/clawbox
Length of output: 4835
π Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- route Hermes branch and error mapping ---'
sed -n '680,770p' src/app/setup-api/ai-models/configure/route.ts
printf '%s\n' '--- Hermes helper definitions and error classes ---'
rg -n -P -C 12 'class Hermes(Local|Cloud)ApplyError|apply(LocalAi|Clawai|CloudProviderKey)ToHermes|Hermes.*ApplyError' src
printf '%s\n' '--- client status and error handling ---'
rg -n -P -C 18 'showError|extractError|selectedProvider|provider.*hermes|hermes' src/components/AIModelsStep.tsxRepository: ID-Robots/clawbox
Length of output: 50374
π Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- Hermes helper error behavior ---'
sed -n '1,180p' src/lib/hermes-local-ai.ts
sed -n '1,150p' src/lib/hermes-cloud-provider.ts
sed -n '1,130p' src/lib/hermes-clawai.ts
printf '%s\n' '--- wizard response handling ---'
sed -n '930,990p' src/components/AIModelsStep.tsx
printf '%s\n' '--- route tests for the disputed responses ---'
rg -n -P -C 10 '409|502|activated|saved|ApplyError|status\)' src/tests/routes/ai-models/configure-hermes.test.tsRepository: ID-Robots/clawbox
Length of output: 25599
π Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
from pathlib import Path
client = Path("src/components/AIModelsStep.tsx").read_text()
route = Path("src/app/setup-api/ai-models/configure/route.ts").read_text()
local = Path("src/lib/hermes-local-ai.ts").read_text()
cloud = Path("src/lib/hermes-cloud-provider.ts").read_text()
clawai = Path("src/lib/hermes-clawai.ts").read_text()
assert 'if (!res.ok) return showError(await extractError(res, "Failed to configure"));' in client
assert 'showSuccessAndContinue();' in client
assert '{ status: 409 }' in route
assert '{ error: err.message }, { status: 502 }' in route
assert 'Local model id is missing or malformed.' in local
assert 'An API key is required.' in cloud
assert 'class HermesLocalApplyError extends Error {}' in local
assert 'class HermesCloudApplyError extends Error {' in cloud
assert 'class ClawaiApplyError extends Error {}' in clawai
print("wizard treats every non-2xx configure response as an error")
print("configure route returns 409 for stored-but-unactivated keys")
print("configure route returns 502 for all caught Hermes helper errors")
print("validation messages share helper error classes with CLI failure messages")
PYRepository: ID-Robots/clawbox
Length of output: 411
Align Hermes response semantics with the wizard.
- Return a success response for a stored but unactivated key, or handle
409explicitly inAIModelsStep.tsx. The wizard treats every non-2xx response as an error, so it displays the partial-success message as a failure and does not continue. - Add an error discriminator to the Hermes error classes. Map validation errors, such as
"Local model id is missing or malformed."and"An API key is required.", to400; keep Hermes CLI failures at502.
π€ 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/app/setup-api/ai-models/configure/route.ts` around lines 735 - 741,
Update the Hermes setup flow around result.activated so stored but unactivated
keys return a 2xx success response, or explicitly handle the 409 continuation
path in AIModelsStep.tsx. Add an error discriminator to the Hermes error classes
and map validation failures, including missing or malformed local model IDs and
missing API keys, to HTTP 400 while preserving HTTP 502 for Hermes CLI failures.
Source: Path instructions
| const tokenRaw = await getConfigValue(CLAWBOX_AI_TOKEN_CONFIG_KEY).catch(() => null); | ||
| const clawaiToken = typeof tokenRaw === "string" && tokenRaw.startsWith("claw_") ? tokenRaw : null; |
There was a problem hiding this comment.
π Security & Privacy | π Major | β‘ Quick win
π§© Analysis chain
π Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C 4 'CLAWBOX_AI_DEVICE_INFO_URL|PORTAL_DEVICE_INFO_URL|fetchPortalTier' src README.md
rg -n -C 4 'redirect:|Authorization: `Bearer' src/app/setup-api/ai-models/status/route.tsRepository: ID-Robots/clawbox
Length of output: 4085
Sensitive Data Exposure (CWE-319): Cleartext Transmission of Sensitive Information
Reachability: Internal
Reachability path
β Entry
src/tests/routes/ai-models/status.test.ts:47
β
βΌ
β Sink
src/app/setup-api/ai-models/status/route.ts
Restrict portal-token requests to trusted HTTPS endpoints.
Validate CLAWBOX_AI_DEVICE_INFO_URL against an HTTPS origin allowlist before sending the Bearer token. Set redirect: "error" or validate every redirect destination.
π€ 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/app/setup-api/ai-models/status/route.ts` around lines 314 - 315, Before
the portal-token request that uses clawaiToken, validate
CLAWBOX_AI_DEVICE_INFO_URL as a trusted HTTPS origin using an explicit
allowlist, and reject non-HTTPS or unapproved origins before sending the Bearer
token. Configure the request to use redirect: "error" so the token cannot follow
an unvalidated redirect.
| const add = await runHermesCli( | ||
| ["auth", "add", slug, "--type", "api-key", "--api-key", key], | ||
| { timeoutMs: 20_000 }, | ||
| ); | ||
| if (add.code !== 0) { | ||
| // Never echo the key. hermes stderr may name the provider but not the secret. | ||
| throw new HermesCloudApplyError(add.stderr || "Failed to save the API key."); | ||
| } |
There was a problem hiding this comment.
π Security & Privacy | π Major | β‘ Quick win
π§© Analysis chain
π Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- Hermes helper error construction ---'
rg -n -C 4 'stderr|class Hermes(Local|Cloud|Clawai)ApplyError|`@throws`' \
src/lib/hermes-cloud-provider.ts src/lib/hermes-local-ai.ts src/lib/hermes-clawai.ts
printf '%s\n' '--- Configure route Hermes catch and call sites ---'
rg -n -C 12 'HermesCloudApplyError|HermesLocalApplyError|ClawaiApplyError|applyCloudProviderKeyToHermes|err\.message' \
src/app/setup-api/ai-models/configure/route.ts
printf '%s\n' '--- Relevant imports and logger usage ---'
sed -n '1,145p' src/lib/hermes-cloud-provider.ts
sed -n '720,775p' src/app/setup-api/ai-models/configure/route.tsRepository: ID-Robots/clawbox
Length of output: 22000
Information Disclosure (CWE-209): Generation of Error Message Containing Sensitive Information
Reachability path
β Entry
src/tests/routes/ai-models/configure-hermes.test.ts:145
β
βΌ
β Hop
src/app/setup-api/ai-models/configure/route.ts:731
applyCloudProviderKeyToHermes
β
βΌ
β Sink
src/lib/hermes-cloud-provider.ts
Return authored Hermes error messages. The configure route echoes Hermes helper errors to the client. Replace raw stderr in src/lib/hermes-cloud-provider.ts, src/lib/hermes-local-ai.ts, and src/lib/hermes-clawai.ts with authored messages. Log only redacted diagnostics server-side. Keep the route comment aligned with this guarantee.
π Affects 2 files
src/lib/hermes-cloud-provider.ts#L80-L87(this comment)src/lib/hermes-cloud-provider.ts#L112-L119src/app/setup-api/ai-models/configure/route.ts#L749-L757
π€ 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-cloud-provider.ts` around lines 80 - 87, Replace raw Hermes
stderr with authored, secret-safe error messages in the failure paths around
runHermesCli in src/lib/hermes-cloud-provider.ts (lines 80-87 and 112-119),
src/lib/hermes-local-ai.ts, and src/lib/hermes-clawai.ts; log only redacted
diagnostics server-side. Update the configure route comment in
src/app/setup-api/ai-models/configure/route.ts (lines 749-757) to state that
client responses use authored messages rather than helper stderr.
| try { | ||
| const payload = await getModelOptions({ refresh: true }); | ||
| if (isAllowedProvider(payload, slug)) { | ||
| model = (await scopeFromPayload(payload, slug)).defaultModel || ""; | ||
| } | ||
| } catch { | ||
| // Catalog unreachable β treat as "no model yet" and leave the provider be. | ||
| } |
There was a problem hiding this comment.
π Maintainability & Code Quality | π΅ Trivial | β‘ Quick win
Log the swallowed catalog error.
The catch block discards every error. The comment attributes it to an unreachable catalog, but the block also absorbs errors from isAllowedProvider and scopeFromPayload, including type errors. The function then returns activated: false, and the caller reports "Key saved. Open the Hermes provider panel to pick a model for it."
A code defect and a network timeout produce the same user-visible result with no server-side trace. The device is embedded hardware with no interactive debugger.
Add a warning log.
As per path instructions: "TypeScript server-side libraries. Review for proper error handling and type safety".
β»οΈ Proposed logging
- } catch {
+ } catch (err) {
// Catalog unreachable β treat as "no model yet" and leave the provider be.
+ console.warn(
+ "[hermes-cloud-provider] model resolution failed for",
+ slug,
+ ":",
+ err instanceof Error ? err.message : err,
+ );
}π 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.
| try { | |
| const payload = await getModelOptions({ refresh: true }); | |
| if (isAllowedProvider(payload, slug)) { | |
| model = (await scopeFromPayload(payload, slug)).defaultModel || ""; | |
| } | |
| } catch { | |
| // Catalog unreachable β treat as "no model yet" and leave the provider be. | |
| } | |
| try { | |
| const payload = await getModelOptions({ refresh: true }); | |
| if (isAllowedProvider(payload, slug)) { | |
| model = (await scopeFromPayload(payload, slug)).defaultModel || ""; | |
| } | |
| } catch (err) { | |
| // Catalog unreachable β treat as "no model yet" and leave the provider be. | |
| console.warn( | |
| "[hermes-cloud-provider] model resolution failed for", | |
| slug, | |
| ":", | |
| err instanceof Error ? err.message : err, | |
| ); | |
| } |
π€ 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-cloud-provider.ts` around lines 92 - 99, Update the catch
block in the model-option loading flow around getModelOptions,
isAllowedProvider, and scopeFromPayload to emit a warning containing the caught
error and relevant provider context before preserving the existing fallback
behavior. Use the moduleβs established logging mechanism and keep returning with
no selected model after logging.
Source: Path instructions
| // the refresh must skip it cleanly rather than fork a missing binary once per | ||
| // provider on every boot warmup. | ||
|
|
||
| vi.mock("child_process", () => ({ spawn: vi.fn() })); |
There was a problem hiding this comment.
π©Ί Stability & Availability | π‘ Minor | β‘ Quick win
π§© Analysis chain
π Script executed:
#!/bin/bash
# Inspect fetchOpenclawCatalog to see how the spawn result is consumed.
ast-grep outline src/app/setup-api/ai-models/catalog/route.ts --items all
rg -n -C 15 'function fetchOpenclawCatalog' src/app/setup-api/ai-models/catalog/route.tsRepository: ID-Robots/clawbox
Length of output: 4025
π Script executed:
#!/bin/bash
sed -n '353,475p' src/app/setup-api/ai-models/catalog/route.ts
printf '\n--- test file ---\n'
cat -n src/tests/routes/ai-models/catalog-edition.test.ts
printf '\n--- related spawn mocks and refresh calls ---\n'
rg -n -C 8 'refreshInBackground|child_process|spawn' src/tests src/app/setup-api/ai-models/catalog/route.tsRepository: ID-Robots/clawbox
Length of output: 50374
π Script executed:
#!/bin/bash
printf '%s\n' '--- module tail ---'
sed -n '470,545p' src/app/setup-api/ai-models/catalog/route.ts
printf '%s\n' '--- test configuration ---'
fd -i 'package.json|vitest.config.*|vite.config.*' . -t f | head -20
for f in $(fd -i 'package.json|vitest.config.*|vite.config.*' . -t f | head -20); do
echo "--- $f"
sed -n '1,180p' "$f"
done
printf '%s\n' '--- Promise executor behavior probe ---'
node - <<'JS'
const spawn = () => undefined;
let timerFired = false;
function fetchCatalog() {
return new Promise((resolve, reject) => {
const child = spawn();
const timer = setTimeout(() => { timerFired = true; }, 300);
child.stdout.on("data", () => {});
resolve();
});
}
let caught = false;
fetchCatalog().catch(() => { caught = true; });
setTimeout(() => {
console.log(JSON.stringify({ caught, timerFired }));
process.exit(0);
}, 50);
JSRepository: ID-Robots/clawbox
Length of output: 8777
Return a closing child-process stub from the mock. The synchronous child.stdout access rejects the Promise and reaches .catch(). However, the five-minute timeout starts before that access and remains pending. Return a stub with the required stream and event methods, then emit close asynchronously to clear the timeout.
π€ 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/tests/routes/ai-models/catalog-edition.test.ts` at line 9, Update the
child_process mockβs spawn stub to return a child-process-shaped object with the
required stdout stream and event-listener methods, and asynchronously emit close
so the pending timeout is cleared. Preserve the existing mock behavior while
ensuring synchronous child.stdout access succeeds and the Promise settles
through the close path.
| it("switches the local model (Gemma) through Hermes without spawning openclaw", async () => { | ||
| const res = await POST(jsonRequest({ provider: "llamacpp", scope: "local" })); | ||
| const body = await res.json(); | ||
|
|
||
| expect(res.status).toBe(200); | ||
| expect(body.success).toBe(true); | ||
| expect(mockApplyLocalAiToHermes).toHaveBeenCalledWith( | ||
| expect.objectContaining({ provider: "llamacpp", model: "gemma4-e2b-it-q4_0", makeDefault: true }), | ||
| ); | ||
| expect(mockSetMany).toHaveBeenCalledWith(expect.objectContaining({ | ||
| local_ai_configured: true, | ||
| local_ai_provider: "llamacpp", | ||
| })); | ||
| expectNoOpenclawSpawn(); | ||
| }); |
There was a problem hiding this comment.
π Maintainability & Code Quality | π΅ Trivial | β‘ Quick win
Add coverage for a local provider with the default primary scope.
Every local-model test in this suite passes scope: "local". The route's Hermes branch at src/app/setup-api/ai-models/configure/route.ts Line 704 gates on isLocalScope, so a request such as { provider: "llamacpp", apiKey: "gemma-q4" } with the default primary scope never reaches applyLocalAiToHermes. configure.test.ts Line 665 already posts that exact shape against the OpenClaw path.
Add a test that posts a llama.cpp request without scope and asserts applyLocalAiToHermes is called. That test pins the branch-gate fix.
π€ 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/tests/routes/ai-models/configure-hermes.test.ts` around lines 155 - 169,
Add a test in the local-model suite that posts a llama.cpp configuration without
the scope field, using the default primary scope request shape, and assert the
response succeeds and mockApplyLocalAiToHermes is called. Keep the existing
explicit scope: "local" coverage unchanged and retain the no-OpenClaw assertion
for this default-scope case.
| // Edition guard: these tests exercise the OpenClaw path, so openclaw is | ||
| // present. The Hermes branch (openclawIsAbsent β true) is covered separately | ||
| // in configure-hermes.test.ts. | ||
| openclawIsAbsent: vi.fn().mockReturnValue(false), | ||
| OpenclawUnavailableError: class OpenclawUnavailableError extends Error {}, |
There was a problem hiding this comment.
π Maintainability & Code Quality | π‘ Minor | β‘ Quick win
openclawIsAbsent relies on a factory default that vi.clearAllMocks() removes. Both suites set mockReturnValue(false) inside the vi.mock("@/lib/openclaw-config", β¦) factory, and both call vi.clearAllMocks() in beforeEach. The mock therefore returns undefined at test time. undefined is falsy, so the OpenClaw path is still taken and both suites pass, but the intent is not enforced. Re-seed the value in beforeEach at each site.
src/tests/routes/ai-models/configure.test.ts#L71-L75: addvi.mocked(openclawIsAbsent).mockReturnValue(false);to the existing re-seed block at Lines 193-207, and importopenclawIsAbsenton Line 109.src/tests/routes/local-ai.test.ts#L17-L19: add the same re-seed tobeforeEachafter Line 76, and importopenclawIsAbsenton Line 28.
π Affects 2 files
src/tests/routes/ai-models/configure.test.ts#L71-L75(this comment)src/tests/routes/local-ai.test.ts#L17-L19
π€ 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/tests/routes/ai-models/configure.test.ts` around lines 71 - 75, Re-seed
openclawIsAbsent after vi.clearAllMocks() in both test suites: import it in
src/tests/routes/ai-models/configure.test.ts and
src/tests/routes/local-ai.test.ts, then add
vi.mocked(openclawIsAbsent).mockReturnValue(false) to each suiteβs beforeEach
re-seed block. Apply the corresponding changes at
src/tests/routes/ai-models/configure.test.ts lines 71-75 and
src/tests/routes/local-ai.test.ts lines 17-19; both sites require the same fix.
| it("refuses `openclaw config set` with a typed error on hermes instead of spawning", async () => { | ||
| process.env.CLAWBOX_EDITION = "hermes"; | ||
| await expect( | ||
| openclawConfig.runOpenclawConfigSet(["model.provider", "clawai"]), | ||
| ).rejects.toBeInstanceOf(openclawConfig.OpenclawUnavailableError); | ||
| }); |
There was a problem hiding this comment.
π― Functional Correctness | π‘ Minor | β‘ Quick win
π§© Analysis chain
π Script executed:
#!/bin/bash
set -euo pipefail
ast-grep outline src/tests/unit/openclaw-config.test.ts --items all
rg -n -C 5 'spawn|child_process|runOpenclawConfigSet|edition guard' src/tests/unit/openclaw-config.test.tsRepository: ID-Robots/clawbox
Length of output: 1916
π Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- test setup and target test ---'
sed -n '1,140p' src/tests/unit/openclaw-config.test.ts
printf '%s\n' '--- implementation references ---'
rg -n -C 8 'runOpenclawConfigSet|OpenclawUnavailableError|openclawIsAbsent|execFile|spawn' srcRepository: ID-Robots/clawbox
Length of output: 50374
π Script executed:
#!/bin/bash
set -euo pipefail
test_file="$(fd -t f -i '^openclaw-config\.test\.ts$' | head -n 1)"
config_file="$(fd -t f -i '^openclaw-config\.ts$' | head -n 1)"
printf '%s\n' "--- ${test_file} ---"
sed -n '1,135p' "$test_file"
printf '%s\n' "--- ${config_file} ---"
rg -n -C 12 'runOpenclawConfigSet|OpenclawUnavailableError|openclawIsAbsent|execFile|spawn' "$config_file"Repository: ID-Robots/clawbox
Length of output: 16120
π Script executed:
#!/bin/bash
set -euo pipefail
python3 - <<'PY'
from pathlib import Path
import re
test_path = Path("src/tests/unit/openclaw-config.test.ts")
source_path = Path("src/lib/openclaw-config.ts")
test = test_path.read_text()
source = source_path.read_text()
mock = re.search(r'vi\.mock\("child_process",\s*\(\)\s*=>\s*\(\{(.*?)\}\)\);', test, re.S)
spawn_guard = re.search(
r'if \(openclawIsAbsent\(\)\)\s*\{\s*return Promise\.reject\(new OpenclawUnavailableError\(\)\);\s*\}\s*.*?const child = spawn\(',
source,
re.S,
)
print("test child_process mock defines spawn:", bool(mock and re.search(r'\bspawn\s*:', mock.group(1))))
print("test child_process mock defines execFile:", bool(mock and re.search(r'\bexecFile\s*:', mock.group(1))))
print("test target observes mockExecFile:", "mockExecFile" in test)
print("implementation checks edition before spawn:", bool(spawn_guard))
PYRepository: ID-Robots/clawbox
Length of output: 336
Assert that Hermes does not invoke child_process.spawn.
The test mocks only execFile, but runOpenclawConfigSet uses spawn. Add spawn: vi.fn() to the mock and assert zero calls after the OpenclawUnavailableError rejection.
π€ 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/tests/unit/openclaw-config.test.ts` around lines 123 - 128, Add spawn:
vi.fn() to the child_process mock used by the openclawConfig tests, then update
the Hermes runOpenclawConfigSet test to assert spawn was not called after the
OpenclawUnavailableError rejection. Keep the existing execFile mock and
typed-error assertion unchanged.
Server-side fixes for OpenClaw assumptions that survive onto a Hermes device.
Found by flashing a device and using it, not in CI β the unit suite cannot see a
tool that isn't installed.
What this fixes
Remote Control kept asking a signed-in device to sign in. The AI status
route read its answer from the wrong place on this edition.
Operations that need the OpenClaw CLI ran on the edition that doesn't ship
it, failing late and opaquely inside a request. There is now a typed
edition check at the shared entry point, and the callers that reached it β
model-catalog refresh, local-AI disable, the updater probes β skip cleanly.
openclawanddualeditions are unaffected.The local model (Gemma) switch and API-key providers failed to configure.
The configure route took the OpenClaw path for every provider. On this edition
it now uses the Hermes equivalents, and a failure no longer reports a
credential problem for a local model that has no credentials.
ClawKeep offered a remedy that contradicts the edition. It depends on the
OpenClaw agent, so it cannot run here; it now says so plainly instead of
printing an install command. Copy added for all ten locales.
Tests
A regression test per fix, covering the edition-conditional behaviour in each β
status resolution, the guarded entry point, catalog refresh, the three configure
paths, error classification, and ClawKeep availability.
Verification
Reproduced read-only on a flashed device before fixing. Post-fix on-device
verification and the Linux build/e2e run are still outstanding β the build host
was unreachable while this was written.