Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions workspace-server/internal/handlers/template_files_eic.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,16 @@ func writeFileViaEIC(ctx context.Context, instanceID, runtime, relPath string, c
"-i", keyPath,
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
// LogLevel=ERROR silences the benign "Warning: Permanently
// added '[127.0.0.1]:NNNNN' to known hosts" notice that ssh
// emits on every fresh tunnel connection. Without this, the
// notice lands on stderr and fools readFileViaEIC's "empty
// stdout + empty stderr → file not found" classifier into
// thinking the warning is a real ssh-layer error → 500
// instead of 404 (Hermes config.yaml load, hongming tenant,
// 2026-05-05 02:38). Real auth/tunnel errors stay visible
// because they're emitted at ERROR level.
"-o", "LogLevel=ERROR",
"-o", "ServerAliveInterval=15",
"-p", fmt.Sprintf("%d", localPort),
fmt.Sprintf("%s@127.0.0.1", osUser),
Expand Down Expand Up @@ -292,6 +302,16 @@ func readFileViaEIC(ctx context.Context, instanceID, runtime, relPath string) ([
"-i", keyPath,
"-o", "StrictHostKeyChecking=no",
"-o", "UserKnownHostsFile=/dev/null",
// LogLevel=ERROR silences the benign "Warning: Permanently
// added '[127.0.0.1]:NNNNN' to known hosts" notice that ssh
// emits on every fresh tunnel connection. Without this, the
// notice lands on stderr and fools readFileViaEIC's "empty
// stdout + empty stderr → file not found" classifier into
// thinking the warning is a real ssh-layer error → 500
// instead of 404 (Hermes config.yaml load, hongming tenant,
// 2026-05-05 02:38). Real auth/tunnel errors stay visible
// because they're emitted at ERROR level.
"-o", "LogLevel=ERROR",
"-o", "ServerAliveInterval=15",
"-p", fmt.Sprintf("%d", localPort),
fmt.Sprintf("%s@127.0.0.1", osUser),
Expand Down
32 changes: 32 additions & 0 deletions workspace-server/internal/handlers/template_files_eic_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package handlers

import (
"os"
"regexp"
"strings"
"testing"
)
Expand Down Expand Up @@ -66,6 +68,36 @@ func TestResolveWorkspaceFilePath_RejectsTraversal(t *testing.T) {
}
}

// TestSSHArgs_LogLevelErrorBothSites pins that BOTH ssh invocations
// (writeFileViaEIC + readFileViaEIC) include `-o LogLevel=ERROR`.
//
// Without that flag, ssh emits a "Warning: Permanently added
// '[127.0.0.1]:NNNNN' (ED25519) to the list of known hosts." line on
// every fresh tunnel connection (even with UserKnownHostsFile=/dev/null
// — that prevents persistence, not the warning). The warning lands on
// stderr, which fools readFileViaEIC's "empty stdout + empty stderr →
// file not found" classifier into thinking the warning is a real
// ssh-layer error and returning 500 instead of 404.
//
// Caught 2026-05-05 02:38 on hongming.moleculesai.app: opening Hermes
// workspace's Config tab returned 500 with body
// `ssh cat: exit status 1 (Warning: Permanently added '[127.0.0.1]:37951'…)`.
//
// LogLevel=ERROR silences info+warning while keeping real auth/tunnel
// errors visible. This test reads the source and asserts the flag
// appears at least twice (one per ssh block) — fires if a future edit
// removes it from either site.
func TestSSHArgs_LogLevelErrorBothSites(t *testing.T) {
src, err := os.ReadFile("template_files_eic.go")
if err != nil {
t.Fatalf("read source: %v", err)
}
matches := regexp.MustCompile(`"-o", "LogLevel=ERROR"`).FindAllIndex(src, -1)
if len(matches) < 2 {
t.Errorf("expected LogLevel=ERROR in BOTH ssh blocks (write + read); found %d occurrences", len(matches))
}
}

// TestShellQuote — the sole piece of variable data in the remote ssh
// command is the absolute path. It's already built from a map + Clean()
// so traversal is impossible, but we still single-quote as defence-in-
Expand Down
Loading