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

@@ -21,6 +21,7 @@ type credKey struct {
type Decision struct {
Accepted bool
Reason string // "static_credential", "threshold_reached", "remembered_credential", "rejected"
Shell string // optional: route to specific shell (only set for static credentials)
}
type Authenticator struct {
@@ -50,7 +51,7 @@ func (a *Authenticator) Authenticate(ip, username, password string) Decision {
pMatch := subtle.ConstantTimeCompare([]byte(cred.Password), []byte(password))
if uMatch == 1 && pMatch == 1 {
a.failCounts[ip] = 0
return Decision{Accepted: true, Reason: "static_credential"}
return Decision{Accepted: true, Reason: "static_credential", Shell: cred.Shell}
}
}

View File

@@ -153,6 +153,39 @@ func TestExpiredCredentialsSweep(t *testing.T) {
}
}
func TestStaticCredentialShellPropagation(t *testing.T) {
a := newTestAuth(10, time.Hour,
config.Credential{Username: "samsung", Password: "fridge", Shell: "fridge"},
config.Credential{Username: "root", Password: "toor"},
)
// Static credential with shell set should propagate it.
d := a.Authenticate("1.2.3.4", "samsung", "fridge")
if !d.Accepted || d.Reason != "static_credential" {
t.Fatalf("got %+v, want accepted with static_credential", d)
}
if d.Shell != "fridge" {
t.Errorf("Shell = %q, want %q", d.Shell, "fridge")
}
// Static credential without shell should leave it empty.
d = a.Authenticate("1.2.3.4", "root", "toor")
if !d.Accepted || d.Reason != "static_credential" {
t.Fatalf("got %+v, want accepted with static_credential", d)
}
if d.Shell != "" {
t.Errorf("Shell = %q, want empty", d.Shell)
}
// Threshold-reached decision should not have a shell set.
a2 := newTestAuth(2, time.Hour)
a2.Authenticate("5.5.5.5", "user", "pass")
d = a2.Authenticate("5.5.5.5", "user", "pass")
if d.Shell != "" {
t.Errorf("threshold decision Shell = %q, want empty", d.Shell)
}
}
func TestConcurrentAccess(t *testing.T) {
a := newTestAuth(5, time.Hour)
var wg sync.WaitGroup