feat: add Smart Fridge shell and per-credential shell routing

Implement Samsung FridgeOS-themed shell (PLAN.md §3.3) with inventory
management, temperature controls, diagnostics, alerts, and other
appliance commands. Add per-credential shell routing so static
credentials can specify which shell to use via the `shell` config field,
passed through ssh.Permissions.Extensions.

Also extract shared ReadLine helper from bash to the shell package so
both shells can reuse terminal input handling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 22:34:29 +01:00
parent 84c6912435
commit 8e90f21d91
14 changed files with 746 additions and 70 deletions

View File

@@ -18,6 +18,7 @@ import (
"git.t-juice.club/torjus/oubliette/internal/notify"
"git.t-juice.club/torjus/oubliette/internal/shell"
"git.t-juice.club/torjus/oubliette/internal/shell/bash"
"git.t-juice.club/torjus/oubliette/internal/shell/fridge"
"git.t-juice.club/torjus/oubliette/internal/storage"
"golang.org/x/crypto/ssh"
)
@@ -38,6 +39,9 @@ func New(cfg config.Config, store storage.Store, logger *slog.Logger) (*Server,
if err := registry.Register(bash.NewBashShell(), 1); err != nil {
return nil, fmt.Errorf("registering bash shell: %w", err)
}
if err := registry.Register(fridge.NewFridgeShell(), 1); err != nil {
return nil, fmt.Errorf("registering fridge shell: %w", err)
}
s := &Server{
cfg: cfg,
@@ -138,10 +142,24 @@ func (s *Server) handleSession(channel ssh.Channel, requests <-chan *ssh.Request
defer channel.Close()
// Select a shell from the registry.
selectedShell, err := s.shellRegistry.Select()
if err != nil {
s.logger.Error("failed to select shell", "err", err)
return
// If the auth layer specified a shell preference, use it; otherwise random.
var selectedShell shell.Shell
if conn.Permissions != nil && conn.Permissions.Extensions["shell"] != "" {
shellName := conn.Permissions.Extensions["shell"]
sh, ok := s.shellRegistry.Get(shellName)
if ok {
selectedShell = sh
} else {
s.logger.Warn("configured shell not found, falling back to random", "shell", shellName)
}
}
if selectedShell == nil {
var err error
selectedShell, err = s.shellRegistry.Select()
if err != nil {
s.logger.Error("failed to select shell", "err", err)
return
}
}
ip := extractIP(conn.RemoteAddr())
@@ -304,7 +322,13 @@ func (s *Server) passwordCallback(conn ssh.ConnMetadata, password []byte) (*ssh.
}
if d.Accepted {
return nil, nil
var perms *ssh.Permissions
if d.Shell != "" {
perms = &ssh.Permissions{
Extensions: map[string]string{"shell": d.Shell},
}
}
return perms, nil
}
return nil, fmt.Errorf("rejected")
}

View File

@@ -108,7 +108,7 @@ func TestIntegrationSSHConnect(t *testing.T) {
AcceptAfter: 2,
CredentialTTLDuration: time.Hour,
StaticCredentials: []config.Credential{
{Username: "root", Password: "toor"},
{Username: "root", Password: "toor", Shell: "bash"},
},
},
Shell: config.ShellConfig{