Skip to content

Commit c0d1641

Browse files
feat: Add SSH remote terminal support to web interface (#352)
Adds SSH-based remote terminal access to the web chat interface. Users can now connect to remote servers via SSH, with auto-installation of the infer binary. Includes a Docker Compose example demonstrating both local and remote modes. ## Summary of Changes This PR enables users to: 1. Connect to remote servers via SSH from the web terminal 2. Auto-install infer binary on remote servers when missing 3. Switch between local and remote servers via a dropdown 4. Use portable Linux binaries (CGO-free) for better compatibility 5. Test with a complete Docker Compose example ## Key Features ### SSH Remote Mode - Added ability to connect to remote servers via SSH from the web terminal - Secure authentication with SSH keys or passwords - Persistent connections with session management ### Auto-installation - Automatically installs `infer` binary on remote servers if missing - Supports multiple architectures and operating systems - Fallback mechanisms for installation failures ### Web Terminal Enhancements - Added server selection dropdown in the web interface - Support for multiple concurrent server connections - Improved error handling and user feedback ### Portable Linux Builds - Changed Linux builds to use CGO_ENABLED=0 for maximum compatibility - Statically linked binaries that work on any Linux distribution - Reduced dependency requirements on target systems ### Docker Compose Example - Complete example with local + remote Ubuntu server setup - Environment configuration templates - Step-by-step documentation in README.md ## Technical Details ### New Files: - `internal/web/ssh_client.go` - SSH client implementation - `internal/web/ssh_session.go` - SSH session management - `internal/web/remote_installer.go` - Remote binary installation - `examples/web-terminal/` - Complete example setup ### Modified Files: - `cmd/chat.go` - Added SSH mode flag and configuration - `internal/web/server.go` - Enhanced web server with SSH support - `internal/web/session_manager.go` - Multi-server session management - `internal/web/static/app.js` - Frontend server selection UI - `internal/web/templates/index.html` - Updated HTML template - Build configuration updates for portable binaries ## Testing The included Docker Compose example provides a complete test environment: - Local infer instance for direct testing - Remote Ubuntu server for SSH testing - Pre-configured environment variables - Step-by-step setup instructions ## Compatibility - Backward compatible with existing local mode - Works with any SSH-compatible server (Linux, macOS, BSD) - Supports both password and key-based authentication - Portable binaries work on glibc and musl-based systems --------- Signed-off-by: Eden Reich <[email protected]> Co-authored-by: semantic-release-bot <[email protected]>
1 parent 6953bf1 commit c0d1641

File tree

29 files changed

+2250
-451
lines changed

29 files changed

+2250
-451
lines changed

.flox/env/manifest.lock

Lines changed: 318 additions & 317 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.flox/env/manifest.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ ripgrep.version = "^15.1.0"
2929
markdownlint-cli.pkg-path = "markdownlint-cli"
3030
markdownlint-cli.version = "^0.47.0"
3131
claude-code.pkg-path = "claude-code"
32-
claude-code.version = "^2.0.74"
32+
claude-code.version = "^2.0.76"
3333
docker.pkg-path = "docker"
3434
docker.version = "^29.1.2"
3535
docker-compose.pkg-path = "docker-compose"

.github/workflows/artifacts.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,22 +56,22 @@ jobs:
5656
echo "commit=$COMMIT" >> $GITHUB_OUTPUT
5757
echo "date=$DATE" >> $GITHUB_OUTPUT
5858
59-
- name: Build binary (Linux in Alpine container)
59+
- name: Build binary (Linux portable - pure Go, no CGO dependencies)
6060
if: matrix.goos == 'linux'
6161
run: |
6262
docker run --rm \
6363
-v $PWD:/workspace \
6464
-w /workspace \
65-
-e CGO_ENABLED=1 \
65+
-e CGO_ENABLED=0 \
6666
golang:1.25-alpine3.23 \
67-
sh -c "apk add --no-cache gcc musl-dev sqlite-dev && go build -tags libsqlite3 -ldflags '-w -s -X github.com/inference-gateway/cli/cmd.version=${{ steps.version.outputs.version }} -X github.com/inference-gateway/cli/cmd.commit=${{ steps.version.outputs.commit }} -X github.com/inference-gateway/cli/cmd.date=${{ steps.version.outputs.date }}' -o infer-${{ matrix.goos }}-${{ matrix.goarch }} ."
67+
sh -c "go build -ldflags '-w -s -X github.com/inference-gateway/cli/cmd.version=${{ steps.version.outputs.version }} -X github.com/inference-gateway/cli/cmd.commit=${{ steps.version.outputs.commit }} -X github.com/inference-gateway/cli/cmd.date=${{ steps.version.outputs.date }}' -o infer-${{ matrix.goos }}-${{ matrix.goarch }} ."
6868
69-
- name: Build binary (macOS native)
69+
- name: Build binary (macOS with CGO for clipboard image support)
7070
if: matrix.goos == 'darwin'
7171
env:
7272
CGO_ENABLED: ${{ matrix.cgo }}
7373
run: |
74-
go build -tags libsqlite3 -ldflags "-w -s -X github.com/inference-gateway/cli/cmd.version=${{ steps.version.outputs.version }} -X github.com/inference-gateway/cli/cmd.commit=${{ steps.version.outputs.commit }} -X github.com/inference-gateway/cli/cmd.date=${{ steps.version.outputs.date }}" -o infer-${{ matrix.goos }}-${{ matrix.goarch }} .
74+
go build -ldflags "-w -s -X github.com/inference-gateway/cli/cmd.version=${{ steps.version.outputs.version }} -X github.com/inference-gateway/cli/cmd.commit=${{ steps.version.outputs.commit }} -X github.com/inference-gateway/cli/cmd.date=${{ steps.version.outputs.date }}" -o infer-${{ matrix.goos }}-${{ matrix.goarch }} .
7575
7676
- name: Upload artifact
7777
uses: actions/upload-artifact@v5

.github/workflows/ci.yml

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -97,17 +97,9 @@ jobs:
9797
go-version-file: 'go.mod'
9898
cache: true
9999

100-
- name: Install build dependencies
101-
run: |
102-
sudo apt-get update
103-
sudo apt-get install -y gcc libsqlite3-dev
104-
105100
- name: Build project
106-
env:
107-
CGO_ENABLED: 1
108101
run: |
109102
go build \
110-
-tags libsqlite3 \
111103
-ldflags "\
112104
-w -s \
113105
-X github.com/inference-gateway/cli/cmd.version=${{ github.ref_name }} \
@@ -129,12 +121,5 @@ jobs:
129121
go-version-file: 'go.mod'
130122
cache: true
131123

132-
- name: Install build dependencies
133-
run: |
134-
sudo apt-get update
135-
sudo apt-get install -y gcc libsqlite3-dev
136-
137124
- name: Run tests
138-
env:
139-
CGO_ENABLED: 1
140-
run: go test -tags libsqlite3 ./...
125+
run: go test ./...

.infer/config.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,12 @@ agent:
249249
- If accepted, YOU will execute this plan. Make it specific and actionable!
250250
- Call RequestPlanApproval ONLY when your plan is complete and ready
251251
- If you need clarification, ASK - don't guess!
252+
system_prompt_remote: |-
253+
Remote system administration agent. You are operating on a remote machine via SSH.
254+
255+
FOCUS: System operations, service management, monitoring, diagnostics, and infrastructure tasks.
256+
257+
CONTEXT: This is a shared system environment, not a project workspace. Users may be managing servers, containers, services, or general infrastructure.
252258
system_reminders:
253259
enabled: true
254260
interval: 4
@@ -625,3 +631,10 @@ web:
625631
port: 3000
626632
host: localhost
627633
session_inactivity_mins: 5
634+
ssh:
635+
enabled: false
636+
use_ssh_config: true
637+
known_hosts_path: ~/.ssh/known_hosts
638+
auto_install: true
639+
install_version: latest
640+
servers: []

CHANGELOG.md

Lines changed: 56 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

CLAUDE.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ The project uses Clean Architecture with domain-driven design patterns.
1313
### Development Commands
1414

1515
```bash
16-
# Build the binary
16+
# Build the binary (pure Go, no CGO required)
1717
task build
1818

1919
# Run all tests
@@ -33,6 +33,9 @@ task check
3333

3434
# Complete development workflow (format, build, test)
3535
task dev
36+
37+
# Verify dependencies
38+
task verify:deps
3639
```
3740

3841
### Module Management
@@ -715,7 +718,7 @@ Key third-party libraries:
715718
- **Lipgloss**: Styling for terminal output
716719
- **go-redis**: Redis client for storage backend
717720
- **lib/pq**: PostgreSQL driver
718-
- **modernc.org/sqlite**: CGO-free SQLite driver
721+
- **modernc.org/sqlite**: Pure Go SQLite driver (no CGO required)
719722
- **metoro-io/mcp-golang**: MCP (Model Context Protocol) client library
720723

721724
## Development Environment

Taskfile.yml

Lines changed: 40 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,7 @@ tasks:
1919
build:
2020
desc: Build the CLI binary
2121
cmds:
22-
- |
23-
if ! command -v gcc >/dev/null 2>&1 && ! command -v clang >/dev/null 2>&1; then
24-
echo "ERROR: C compiler not found. Required for SQLite support."
25-
echo "Install: build-essential (Linux) or Xcode CLI Tools (macOS)"
26-
exit 1
27-
fi
28-
- CGO_ENABLED=1 go build -tags libsqlite3 -ldflags "-w -s -X github.com/inference-gateway/cli/cmd.version={{.VERSION}} -X github.com/inference-gateway/cli/cmd.commit={{.COMMIT}} -X github.com/inference-gateway/cli/cmd.date={{.DATE}}" -o {{.BINARY_NAME}} {{.MAIN_PACKAGE}}
22+
- go build -ldflags "-w -s -X github.com/inference-gateway/cli/cmd.version={{.VERSION}} -X github.com/inference-gateway/cli/cmd.commit={{.COMMIT}} -X github.com/inference-gateway/cli/cmd.date={{.DATE}}" -o {{.BINARY_NAME}} {{.MAIN_PACKAGE}}
2923
sources:
3024
- "**/*.go"
3125
- go.mod
@@ -36,7 +30,7 @@ tasks:
3630
install:
3731
desc: Install the CLI from source
3832
cmds:
39-
- CGO_ENABLED=1 go build -tags libsqlite3 -ldflags "-w -s -X github.com/inference-gateway/cli/cmd.version={{.VERSION}} -X github.com/inference-gateway/cli/cmd.commit={{.COMMIT}} -X github.com/inference-gateway/cli/cmd.date={{.DATE}}" -o $(go env GOPATH)/bin/{{.BINARY_NAME}} {{.MAIN_PACKAGE}}
33+
- go build -ldflags "-w -s -X github.com/inference-gateway/cli/cmd.version={{.VERSION}} -X github.com/inference-gateway/cli/cmd.commit={{.COMMIT}} -X github.com/inference-gateway/cli/cmd.date={{.DATE}}" -o $(go env GOPATH)/bin/{{.BINARY_NAME}} {{.MAIN_PACKAGE}}
4034

4135
clean:
4236
desc: Clean build artifacts
@@ -47,17 +41,17 @@ tasks:
4741
test:
4842
desc: Run all tests
4943
cmds:
50-
- CGO_ENABLED=1 go test -tags libsqlite3 ./...
44+
- go test ./...
5145

5246
test:verbose:
5347
desc: Run tests with verbose output
5448
cmds:
55-
- CGO_ENABLED=1 go test -tags libsqlite3 -v ./...
49+
- go test -v ./...
5650

5751
test:coverage:
5852
desc: Run tests with coverage report
5953
cmds:
60-
- CGO_ENABLED=1 go test -tags libsqlite3 -cover ./...
54+
- go test -cover ./...
6155

6256
lint:
6357
desc: Run linter (requires golangci-lint)
@@ -85,35 +79,18 @@ tasks:
8579
cmds:
8680
- go mod download
8781

88-
verify:cgo:
89-
desc: Verify CGO and SQLite dependencies
82+
verify:deps:
83+
desc: Verify Go dependencies
9084
cmds:
9185
- |
92-
echo "Checking C compiler..."
93-
if command -v gcc >/dev/null 2>&1; then
94-
echo "✓ GCC found: $(gcc --version | head -n1)"
95-
elif command -v clang >/dev/null 2>&1; then
96-
echo "✓ Clang found: $(clang --version | head -n1)"
97-
else
98-
echo "✗ No C compiler found"
99-
echo "Install: build-essential (Linux) or Xcode CLI Tools (macOS)"
100-
exit 1
101-
fi
102-
103-
echo "Checking SQLite library..."
104-
if pkg-config --exists sqlite3 2>/dev/null; then
105-
echo "✓ SQLite found: $(pkg-config --modversion sqlite3)"
106-
else
107-
echo "⚠ SQLite not detected via pkg-config (may still work if installed)"
108-
fi
109-
110-
echo "Checking CGO..."
111-
CGO_STATUS=$(go env CGO_ENABLED)
112-
if [ "$CGO_STATUS" = "1" ]; then
113-
echo "✓ CGO enabled"
114-
else
115-
echo "⚠ CGO disabled (will be enabled for build tasks)"
116-
fi
86+
echo "Checking Go version..."
87+
go version
88+
echo ""
89+
echo "Checking dependencies..."
90+
go mod verify
91+
echo "✓ All dependencies verified"
92+
echo ""
93+
echo "Note: This CLI uses pure Go dependencies (no CGO required)"
11794
11895
run:
11996
desc: Run the CLI locally
@@ -151,18 +128,15 @@ tasks:
151128
- test
152129

153130
release:build:
154-
desc: Build release binary for current platform only (multi-platform builds use GitHub Actions with native runners)
131+
desc: Build release binary for current platform only
155132
cmds:
156133
- mkdir -p dist
157134
- |
158-
echo "Building for native platform only..."
135+
echo "Building for native platform..."
159136
GOOS=$(go env GOOS)
160137
GOARCH=$(go env GOARCH)
161-
CGO_ENABLED=1 go build -tags libsqlite3 -ldflags "-w -s -X github.com/inference-gateway/cli/cmd.version={{.VERSION}} -X github.com/inference-gateway/cli/cmd.commit={{.COMMIT}} -X github.com/inference-gateway/cli/cmd.date={{.DATE}}" -o dist/{{.BINARY_NAME}}-${GOOS}-${GOARCH} {{.MAIN_PACKAGE}}
138+
go build -ldflags "-w -s -X github.com/inference-gateway/cli/cmd.version={{.VERSION}} -X github.com/inference-gateway/cli/cmd.commit={{.COMMIT}} -X github.com/inference-gateway/cli/cmd.date={{.DATE}}" -o dist/{{.BINARY_NAME}}-${GOOS}-${GOARCH} {{.MAIN_PACKAGE}}
162139
echo "✓ Built dist/{{.BINARY_NAME}}-${GOOS}-${GOARCH}"
163-
echo ""
164-
echo "Note: Cross-platform builds with dynamic SQLite linking require native runners."
165-
echo "Use GitHub Actions for multi-platform releases."
166140
- cd dist && sha256sum {{.BINARY_NAME}}-* > checksums.txt
167141
- |
168142
if command -v cosign >/dev/null 2>&1; then
@@ -173,18 +147,38 @@ tasks:
173147
fi
174148
175149
release:build:darwin:
176-
desc: Build macOS binary with CGO enabled (for clipboard image support)
150+
desc: Build macOS binary
151+
vars:
152+
GOARCH:
153+
sh: go env GOARCH
154+
cmds:
155+
- mkdir -p dist
156+
- GOOS=darwin GOARCH={{.GOARCH}} go build -ldflags "-w -s -X github.com/inference-gateway/cli/cmd.version={{.VERSION}} -X github.com/inference-gateway/cli/cmd.commit={{.COMMIT}} -X github.com/inference-gateway/cli/cmd.date={{.DATE}}" -o dist/{{.BINARY_NAME}}-darwin-{{.GOARCH}} {{.MAIN_PACKAGE}}
157+
158+
release:build:linux:
159+
desc: Build portable Linux binary using Docker
177160
vars:
178161
GOARCH:
179162
sh: go env GOARCH
180163
cmds:
181164
- mkdir -p dist
182-
- CGO_ENABLED=1 GOOS=darwin GOARCH={{.GOARCH}} go build -tags libsqlite3 -ldflags "-w -s -X github.com/inference-gateway/cli/cmd.version={{.VERSION}} -X github.com/inference-gateway/cli/cmd.commit={{.COMMIT}} -X github.com/inference-gateway/cli/cmd.date={{.DATE}}" -o dist/{{.BINARY_NAME}}-darwin-{{.GOARCH}} {{.MAIN_PACKAGE}}
165+
- |
166+
echo "Building portable Linux binary using Docker..."
167+
docker run --rm \
168+
-v "{{.PWD}}":/build \
169+
-w /build \
170+
golang:1.25-alpine \
171+
sh -c 'CGO_ENABLED=0 GOOS=linux GOARCH={{.GOARCH}} \
172+
go build \
173+
-ldflags "-w -s -X github.com/inference-gateway/cli/cmd.version={{.VERSION}} -X github.com/inference-gateway/cli/cmd.commit={{.COMMIT}} -X github.com/inference-gateway/cli/cmd.date={{.DATE}}" \
174+
-o dist/{{.BINARY_NAME}}-linux-{{.GOARCH}} .'
175+
echo "✓ Built portable dist/{{.BINARY_NAME}}-linux-{{.GOARCH}} (pure Go, all storage backends supported)"
183176
184177
container:build:
185178
desc: Build container image locally for testing
186179
cmds:
187180
- task: release:build
181+
- task: release:build:linux
188182
- |
189183
docker build \
190184
--build-context binaries=dist/ \

cmd/chat.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,34 @@ and have a conversational interface with the inference gateway.`,
4242
if cmd.Flags().Changed("port") {
4343
cfg.Web.Port, _ = cmd.Flags().GetInt("port")
4444
}
45+
if cmd.Flags().Changed("host") {
46+
cfg.Web.Host, _ = cmd.Flags().GetString("host")
47+
}
48+
49+
// SSH remote mode flags
50+
if cmd.Flags().Changed("ssh-host") {
51+
cfg.Web.SSH.Enabled = true
52+
sshHost, _ := cmd.Flags().GetString("ssh-host")
53+
sshUser, _ := cmd.Flags().GetString("ssh-user")
54+
sshPort, _ := cmd.Flags().GetInt("ssh-port")
55+
sshCommand, _ := cmd.Flags().GetString("ssh-command")
56+
noInstall, _ := cmd.Flags().GetBool("ssh-no-install")
57+
58+
// Create a single server config from CLI flags
59+
cfg.Web.Servers = []config.SSHServerConfig{
60+
{
61+
Name: "CLI Remote Server",
62+
ID: "cli-remote",
63+
RemoteHost: sshHost,
64+
RemotePort: sshPort,
65+
RemoteUser: sshUser,
66+
CommandPath: sshCommand,
67+
AutoInstall: func() *bool { b := !noInstall; return &b }(),
68+
Description: "Remote server configured via CLI flags",
69+
},
70+
}
71+
}
72+
4573
return StartWebChatSession(cfg, V)
4674
}
4775

@@ -345,4 +373,10 @@ func init() {
345373
rootCmd.AddCommand(chatCmd)
346374
chatCmd.Flags().Bool("web", false, "Start web terminal interface")
347375
chatCmd.Flags().Int("port", 0, "Web server port (default: 3000)")
376+
chatCmd.Flags().String("host", "", "Web server host (default: localhost)")
377+
chatCmd.Flags().String("ssh-host", "", "Remote SSH server hostname")
378+
chatCmd.Flags().String("ssh-user", "", "Remote SSH username")
379+
chatCmd.Flags().Int("ssh-port", 22, "Remote SSH port")
380+
chatCmd.Flags().Bool("ssh-no-install", false, "Disable auto-installation of infer on remote")
381+
chatCmd.Flags().String("ssh-command", "infer", "Path to infer binary on remote")
348382
}

cmd/root.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,13 @@ func initConfig() { // nolint:funlen
8484
v.SetDefault("web.port", defaults.Web.Port)
8585
v.SetDefault("web.host", defaults.Web.Host)
8686
v.SetDefault("web.session_inactivity_mins", defaults.Web.SessionInactivityMins)
87+
v.SetDefault("web.ssh", defaults.Web.SSH)
88+
v.SetDefault("web.ssh.enabled", defaults.Web.SSH.Enabled)
89+
v.SetDefault("web.ssh.use_ssh_config", defaults.Web.SSH.UseSSHConfig)
90+
v.SetDefault("web.ssh.known_hosts_path", defaults.Web.SSH.KnownHostsPath)
91+
v.SetDefault("web.ssh.auto_install", defaults.Web.SSH.AutoInstall)
92+
v.SetDefault("web.ssh.install_version", defaults.Web.SSH.InstallVersion)
93+
v.SetDefault("web.servers", defaults.Web.Servers)
8794
v.SetDefault("git", defaults.Git)
8895
v.SetDefault("storage", defaults.Storage)
8996
v.SetDefault("conversation", defaults.Conversation)

0 commit comments

Comments
 (0)