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:
119
internal/shell/bash/commands.go
Normal file
119
internal/shell/bash/commands.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package bash
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type shellState struct {
|
||||
cwd string
|
||||
username string
|
||||
hostname string
|
||||
fs *filesystem
|
||||
}
|
||||
|
||||
type commandResult struct {
|
||||
output string
|
||||
exit bool
|
||||
}
|
||||
|
||||
func dispatch(state *shellState, line string) commandResult {
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) == 0 {
|
||||
return commandResult{}
|
||||
}
|
||||
|
||||
cmd := fields[0]
|
||||
args := fields[1:]
|
||||
|
||||
switch cmd {
|
||||
case "pwd":
|
||||
return commandResult{output: state.cwd}
|
||||
case "whoami":
|
||||
return commandResult{output: state.username}
|
||||
case "hostname":
|
||||
return commandResult{output: state.hostname}
|
||||
case "id":
|
||||
return cmdID(state)
|
||||
case "uname":
|
||||
return cmdUname(state, args)
|
||||
case "ls":
|
||||
return cmdLs(state, args)
|
||||
case "cd":
|
||||
return cmdCd(state, args)
|
||||
case "cat":
|
||||
return cmdCat(state, args)
|
||||
case "exit", "logout":
|
||||
return commandResult{exit: true}
|
||||
default:
|
||||
return commandResult{output: fmt.Sprintf("%s: command not found", cmd)}
|
||||
}
|
||||
}
|
||||
|
||||
func cmdID(state *shellState) commandResult {
|
||||
return commandResult{
|
||||
output: fmt.Sprintf("uid=0(%s) gid=0(%s) groups=0(%s)", state.username, state.username, state.username),
|
||||
}
|
||||
}
|
||||
|
||||
func cmdUname(state *shellState, args []string) commandResult {
|
||||
if len(args) > 0 && args[0] == "-a" {
|
||||
return commandResult{
|
||||
output: fmt.Sprintf("Linux %s 5.15.0-89-generic #99-Ubuntu SMP Mon Oct 30 20:42:41 UTC 2023 %s GNU/Linux", state.hostname, runtime.GOARCH),
|
||||
}
|
||||
}
|
||||
return commandResult{output: "Linux"}
|
||||
}
|
||||
|
||||
func cmdLs(state *shellState, args []string) commandResult {
|
||||
target := state.cwd
|
||||
if len(args) > 0 {
|
||||
target = resolvePath(state.cwd, args[0])
|
||||
}
|
||||
|
||||
names, err := state.fs.list(target)
|
||||
if err != nil {
|
||||
return commandResult{output: err.Error()}
|
||||
}
|
||||
|
||||
sort.Strings(names)
|
||||
return commandResult{output: strings.Join(names, " ")}
|
||||
}
|
||||
|
||||
func cmdCd(state *shellState, args []string) commandResult {
|
||||
target := "/root"
|
||||
if len(args) > 0 {
|
||||
target = resolvePath(state.cwd, args[0])
|
||||
}
|
||||
|
||||
if !state.fs.exists(target) {
|
||||
return commandResult{output: fmt.Sprintf("bash: cd: %s: No such file or directory", args[0])}
|
||||
}
|
||||
if !state.fs.isDirectory(target) {
|
||||
return commandResult{output: fmt.Sprintf("bash: cd: %s: Not a directory", args[0])}
|
||||
}
|
||||
|
||||
state.cwd = target
|
||||
return commandResult{}
|
||||
}
|
||||
|
||||
func cmdCat(state *shellState, args []string) commandResult {
|
||||
if len(args) == 0 {
|
||||
return commandResult{}
|
||||
}
|
||||
|
||||
var parts []string
|
||||
for _, arg := range args {
|
||||
p := resolvePath(state.cwd, arg)
|
||||
content, err := state.fs.read(p)
|
||||
if err != nil {
|
||||
parts = append(parts, err.Error())
|
||||
} else {
|
||||
parts = append(parts, strings.TrimRight(content, "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
return commandResult{output: strings.Join(parts, "\n")}
|
||||
}
|
||||
Reference in New Issue
Block a user