feat: add shell interface, registry, and bash shell emulator

Implement Phase 1.4: replaces the hardcoded banner/timeout stub with a
proper shell system. Adds a Shell interface with weighted registry for
shell selection, a RecordingChannel wrapper (pass-through for now, prep
for Phase 2.3 replay), and a bash-like shell with fake filesystem,
terminal line reader, and command handling (pwd, ls, cd, cat, whoami,
hostname, id, uname, exit). Sessions now log command/output pairs to
the store and record the shell name.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 20:24:48 +01:00
parent ae9924ffbb
commit 8189a108d1
17 changed files with 1503 additions and 41 deletions

View File

@@ -9,11 +9,19 @@ import (
)
type Config struct {
SSH SSHConfig `toml:"ssh"`
Auth AuthConfig `toml:"auth"`
Storage StorageConfig `toml:"storage"`
LogLevel string `toml:"log_level"`
LogFormat string `toml:"log_format"` // "text" (default) or "json"
SSH SSHConfig `toml:"ssh"`
Auth AuthConfig `toml:"auth"`
Storage StorageConfig `toml:"storage"`
Shell ShellConfig `toml:"shell"`
LogLevel string `toml:"log_level"`
LogFormat string `toml:"log_format"` // "text" (default) or "json"
}
type ShellConfig struct {
Hostname string `toml:"hostname"`
Banner string `toml:"banner"`
FakeUser string `toml:"fake_user"`
Shells map[string]map[string]any `toml:"-"` // per-shell config extracted manually
}
type StorageConfig struct {
@@ -56,6 +64,14 @@ func Load(path string) (*Config, error) {
return nil, fmt.Errorf("parsing config: %w", err)
}
// Second pass: extract per-shell sub-tables (e.g. [shell.bash]).
var raw map[string]any
if err := toml.Unmarshal(data, &raw); err == nil {
if shellSection, ok := raw["shell"].(map[string]any); ok {
cfg.Shell.Shells = extractShellTables(shellSection)
}
}
applyDefaults(cfg)
if err := validate(cfg); err != nil {
@@ -96,6 +112,36 @@ func applyDefaults(cfg *Config) {
if cfg.Storage.RetentionInterval == "" {
cfg.Storage.RetentionInterval = "1h"
}
if cfg.Shell.Hostname == "" {
cfg.Shell.Hostname = "ubuntu-server"
}
if cfg.Shell.Banner == "" {
cfg.Shell.Banner = "Welcome to Ubuntu 22.04.3 LTS (GNU/Linux 5.15.0-89-generic x86_64)\r\n\r\n"
}
}
// knownShellKeys are top-level keys in [shell] that are not per-shell sub-tables.
var knownShellKeys = map[string]bool{
"hostname": true,
"banner": true,
"fake_user": true,
}
// extractShellTables pulls per-shell config sub-tables from the raw [shell] section.
func extractShellTables(section map[string]any) map[string]map[string]any {
result := make(map[string]map[string]any)
for key, val := range section {
if knownShellKeys[key] {
continue
}
if sub, ok := val.(map[string]any); ok {
result[key] = sub
}
}
if len(result) == 0 {
return nil
}
return result
}
func validate(cfg *Config) error {