fix(canvas/config): load runtime+model from workspace metadata + hide misleading config.yaml error for hermes

Canvas Config tab had 3 bugs visible on hermes workspaces (#1894):

1. Runtime dropdown showed "LangGraph (default)" even when the workspace's
   actual runtime was hermes — because the form only loaded runtime from
   config.yaml, and hermes doesn't use the platform's config.yaml template.
2. Model field was empty for the same reason.
3. "No config.yaml found" error appeared on hermes workspaces despite
   everything being fine — hermes manages its own config at
   ~/.hermes/config.yaml on the workspace host.

Worse, clicking Save with the empty form would silently flip `runtime`
back from `hermes` to `LangGraph (default)`.

## Fix

- loadConfig now always fetches workspace metadata (runtime + model)
  via GET /workspaces/:id and GET /workspaces/:id/model BEFORE attempting
  the config.yaml fetch. These act as the source of truth for runtime
  and model when config.yaml doesn't set them.
- RUNTIMES_WITH_OWN_CONFIG set lists runtimes that manage their own
  config outside the platform template (hermes, external). For these:
  - Missing config.yaml is NOT an error — no red banner shown.
  - An informational gray banner tells the user where to edit the
    runtime's config (e.g. "edit ~/.hermes/config.yaml via Terminal tab
    or the hermes CLI" for hermes).

Closes #1894.

Verified 2026-04-23 on user's hongmingwang tenant which runs hermes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hongming Wang 2026-04-23 14:58:36 -07:00
parent 925a71887d
commit 06273b11ef

View File

@ -104,6 +104,13 @@ interface RuntimeOption {
// Fallback used when /templates can't be fetched (offline, older backend).
// Keep in sync with manifest.json workspace_templates as a defensive default.
// Model + env suggestions only flow when the backend is reachable.
// Runtimes that manage their own config outside the platform's config.yaml
// template. For these, a missing config.yaml is expected — the user manages
// config via the runtime's own mechanism (e.g. hermes edits
// ~/.hermes/config.yaml on the workspace EC2 via the Terminal tab or its
// own CLI). Showing a "No config.yaml found" error for these is misleading.
const RUNTIMES_WITH_OWN_CONFIG = new Set<string>(["hermes", "external"]);
const FALLBACK_RUNTIME_OPTIONS: RuntimeOption[] = [
{ value: "", label: "LangGraph (default)", models: [] },
{ value: "claude-code", label: "Claude Code", models: [] },
@ -134,14 +141,50 @@ export function ConfigTab({ workspaceId }: Props) {
const loadConfig = useCallback(async () => {
setLoading(true);
setError(null);
// ALWAYS load workspace metadata first (runtime + model). These are the
// source of truth regardless of whether the runtime uses our config.yaml
// template. Without this the form falls back to empty/default values on
// a hermes workspace (which doesn't use our template), creating the
// appearance that the saved runtime is unset — and worse, clicking Save
// would silently flip `runtime` from `hermes` back to the dropdown
// default `LangGraph`. See GH #1894.
let wsMetadataRuntime = "";
let wsMetadataModel = "";
try {
const ws = await api.get<{ runtime?: string }>(`/workspaces/${workspaceId}`);
wsMetadataRuntime = (ws.runtime || "").trim();
} catch { /* fall back to config.yaml */ }
try {
const m = await api.get<{ model?: string }>(`/workspaces/${workspaceId}/model`);
wsMetadataModel = (m.model || "").trim();
} catch { /* non-fatal */ }
try {
const res = await api.get<{ content: string }>(`/workspaces/${workspaceId}/files/config.yaml`);
const parsed = parseYaml(res.content);
setOriginalYaml(res.content);
setRawDraft(res.content);
setConfig({ ...DEFAULT_CONFIG, ...parsed } as ConfigData);
// Merge: config.yaml wins for fields it declares, but workspace metadata
// wins for runtime + model when config.yaml doesn't set them.
const merged = { ...DEFAULT_CONFIG, ...parsed } as ConfigData;
if (!merged.runtime && wsMetadataRuntime) merged.runtime = wsMetadataRuntime;
if (!merged.model && wsMetadataModel) merged.model = wsMetadataModel;
setConfig(merged);
} catch {
setError("No config.yaml found");
// No platform-managed config.yaml. Some runtimes (hermes, external)
// manage their own config outside this template; that's expected, not
// an error. Populate the form from workspace metadata so the user
// still sees the saved runtime + model.
const runtimeManagesOwnConfig = RUNTIMES_WITH_OWN_CONFIG.has(wsMetadataRuntime);
if (!runtimeManagesOwnConfig) {
setError("No config.yaml found");
}
setConfig({
...DEFAULT_CONFIG,
runtime: wsMetadataRuntime,
model: wsMetadataModel,
} as ConfigData);
} finally {
setLoading(false);
}
@ -511,6 +554,13 @@ export function ConfigTab({ workspaceId }: Props) {
{error && (
<div className="mx-3 mb-2 px-3 py-1.5 bg-red-900/30 border border-red-800 rounded text-xs text-red-400">{error}</div>
)}
{!error && RUNTIMES_WITH_OWN_CONFIG.has(config.runtime || "") && (
<div className="mx-3 mb-2 px-3 py-1.5 bg-zinc-900/50 border border-zinc-700 rounded text-xs text-zinc-400">
{config.runtime === "hermes"
? "Hermes manages its own config at ~/.hermes/config.yaml on the workspace host. Edit it via the Terminal tab or the hermes CLI, not this form."
: "This runtime manages its own config outside the platform template."}
</div>
)}
{success && (
<div className="mx-3 mb-2 px-3 py-1.5 bg-green-900/30 border border-green-800 rounded text-xs text-green-400">Saved</div>
)}