A virtual jailed shell environment for Go apps backed by an io/fs#FS.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: lock kefka to fake commands

Assisted-by: Claude Opus 4.7 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>

Xe Iaso 1d68ebbb 90803a1a

+254 -5
+93 -1
cmd/kefka/main.go
··· 5 5 "errors" 6 6 "fmt" 7 7 "io" 8 + "io/fs" 8 9 "os" 9 10 "strings" 10 11 "time" ··· 13 14 "golang.org/x/term" 14 15 "mvdan.cc/sh/v3/interp" 15 16 "mvdan.cc/sh/v3/syntax" 17 + "tangled.org/xeiaso.net/kefka/command/registry" 18 + "tangled.org/xeiaso.net/kefka/command/registry/coreutils" 19 + "tangled.org/xeiaso.net/kefka/command/registry/wasmprog" 16 20 ) 17 21 18 22 var ( ··· 34 38 } 35 39 36 40 func run(ctx context.Context) error { 37 - sh, err := interp.New(interp.Interactive(true), interp.StdIO(os.Stdin, os.Stdout, os.Stderr)) 41 + reg := registry.New() 42 + coreutils.Register(reg) 43 + wasmprog.Register(reg) 44 + 45 + fsys := os.DirFS(".") 46 + 47 + middleware := func(next interp.ExecHandlerFunc) interp.ExecHandlerFunc { 48 + return func(ctx context.Context, args []string) error { 49 + return reg.Exec(ctx, fsys, args) 50 + } 51 + } 52 + 53 + sh, err := interp.New( 54 + interp.Interactive(true), 55 + interp.StdIO(os.Stdin, os.Stdout, os.Stderr), 56 + interp.ExecHandlers(middleware), 57 + interp.CallHandler(callHandler(reg, fsys, os.Stdout, os.Stderr)), 58 + interp.StatHandler(fsysStatHandler(reg, fsys)), 59 + interp.OpenHandler(fsysOpenHandler(reg, fsys)), 60 + interp.ReadDirHandler2(fsysReadDirHandler(reg, fsys)), 61 + ) 38 62 if err != nil { 39 63 return fmt.Errorf("can't make shell: %w", err) 40 64 } ··· 71 95 defer fin.Close() 72 96 return runReader(ctx, sh, fin, fname) 73 97 } 98 + 99 + // callHandler intercepts cd and pwd before interp's builtins handle them, 100 + // so we can route directory state through the registry's fsys-relative pwd 101 + // instead of interp's host-rooted Dir. Intercepted calls are replaced with 102 + // `:` (no-op) so interp's builtin doesn't run. 103 + func callHandler(reg *registry.Impl, fsys fs.FS, stdout, stderr io.Writer) interp.CallHandlerFunc { 104 + return func(ctx context.Context, args []string) ([]string, error) { 105 + if len(args) == 0 { 106 + return args, nil 107 + } 108 + switch args[0] { 109 + case "cd": 110 + target := "" 111 + if len(args) > 1 { 112 + target = args[1] 113 + } 114 + if err := reg.Chdir(fsys, target); err != nil { 115 + fmt.Fprintln(stderr, err) 116 + return []string{"false"}, nil 117 + } 118 + return []string{":"}, nil 119 + case "pwd": 120 + pwd := reg.Pwd() 121 + if pwd == "." { 122 + fmt.Fprintln(stdout, "/") 123 + } else { 124 + fmt.Fprintln(stdout, "/"+pwd) 125 + } 126 + return []string{":"}, nil 127 + } 128 + return args, nil 129 + } 130 + } 131 + 132 + func fsysStatHandler(reg *registry.Impl, fsys fs.FS) interp.StatHandlerFunc { 133 + return func(ctx context.Context, name string, followSymlinks bool) (fs.FileInfo, error) { 134 + resolved := reg.Resolve(name) 135 + if !followSymlinks { 136 + if r, ok := fsys.(fs.ReadLinkFS); ok { 137 + return r.Lstat(resolved) 138 + } 139 + } 140 + return fs.Stat(fsys, resolved) 141 + } 142 + } 143 + 144 + func fsysOpenHandler(reg *registry.Impl, fsys fs.FS) interp.OpenHandlerFunc { 145 + return func(ctx context.Context, name string, flag int, perm os.FileMode) (io.ReadWriteCloser, error) { 146 + if flag&(os.O_WRONLY|os.O_RDWR|os.O_CREATE|os.O_APPEND|os.O_TRUNC) != 0 { 147 + return nil, &os.PathError{Op: "open", Path: name, Err: fs.ErrPermission} 148 + } 149 + f, err := fsys.Open(reg.Resolve(name)) 150 + if err != nil { 151 + return nil, err 152 + } 153 + return readOnlyFile{f}, nil 154 + } 155 + } 156 + 157 + func fsysReadDirHandler(reg *registry.Impl, fsys fs.FS) interp.ReadDirHandlerFunc2 { 158 + return func(ctx context.Context, name string) ([]fs.DirEntry, error) { 159 + return fs.ReadDir(fsys, reg.Resolve(name)) 160 + } 161 + } 162 + 163 + type readOnlyFile struct{ fs.File } 164 + 165 + func (readOnlyFile) Write([]byte) (int, error) { return 0, fs.ErrPermission } 74 166 75 167 func runInteractive(ctx context.Context, sh *interp.Runner, stdin io.Reader, stdout, stderr io.Writer) error { 76 168 parser := syntax.NewParser()
+21
command/command.go
··· 1 + package command 2 + 3 + import ( 4 + "context" 5 + "io" 6 + "io/fs" 7 + 8 + "mvdan.cc/sh/v3/expand" 9 + ) 10 + 11 + type ExecContext struct { 12 + Stdin io.Reader 13 + Stdout, Stderr io.Writer 14 + Dir string 15 + Environ expand.Environ 16 + FS fs.FS 17 + } 18 + 19 + type Execer interface { 20 + Exec(ctx context.Context, ec *ExecContext, args []string) error 21 + }
+10
command/registry/coreutils/coreutils.go
··· 1 + package coreutils 2 + 3 + import ( 4 + "tangled.org/xeiaso.net/kefka/command/internal/ls" 5 + "tangled.org/xeiaso.net/kefka/command/registry" 6 + ) 7 + 8 + func Register(reg *registry.Impl) { 9 + reg.Register("ls", ls.Impl{}) 10 + }
+113
command/registry/registry.go
··· 1 + package registry 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "io/fs" 7 + "path" 8 + "strings" 9 + "sync" 10 + 11 + "mvdan.cc/sh/v3/interp" 12 + "tangled.org/xeiaso.net/kefka/command" 13 + ) 14 + 15 + type Impl struct { 16 + lock sync.Mutex 17 + commands map[string]command.Execer 18 + 19 + pwdMu sync.Mutex 20 + pwd string 21 + } 22 + 23 + func (i *Impl) Register(name string, cmd command.Execer) { 24 + i.lock.Lock() 25 + defer i.lock.Unlock() 26 + 27 + i.commands[name] = cmd 28 + } 29 + 30 + func (i *Impl) Get(name string) (command.Execer, bool) { 31 + i.lock.Lock() 32 + defer i.lock.Unlock() 33 + 34 + cmd, ok := i.commands[name] 35 + return cmd, ok 36 + } 37 + 38 + func New() *Impl { 39 + return &Impl{ 40 + commands: make(map[string]command.Execer), 41 + pwd: ".", 42 + } 43 + } 44 + 45 + // Pwd returns the current fsys-relative working directory. 46 + func (i *Impl) Pwd() string { 47 + i.pwdMu.Lock() 48 + defer i.pwdMu.Unlock() 49 + return i.pwd 50 + } 51 + 52 + // Resolve maps a shell path (absolute "/foo" or relative to pwd) into an 53 + // fs.FS-relative path. Attempts to escape above the fsys root clamp to ".". 54 + func (i *Impl) Resolve(p string) string { 55 + pwd := i.Pwd() 56 + if path.IsAbs(p) { 57 + p = strings.TrimPrefix(p, "/") 58 + } else { 59 + p = path.Join(pwd, p) 60 + } 61 + p = path.Clean(p) 62 + if p == "" || p == "." || p == ".." || strings.HasPrefix(p, "../") { 63 + return "." 64 + } 65 + return p 66 + } 67 + 68 + // Chdir changes the fsys-relative working directory. Validates against fsys. 69 + func (i *Impl) Chdir(fsys fs.FS, target string) error { 70 + if target == "" { 71 + target = "." 72 + } 73 + next := i.Resolve(target) 74 + 75 + info, err := fs.Stat(fsys, next) 76 + if err != nil { 77 + return fmt.Errorf("cd: %s: No such file or directory", target) 78 + } 79 + if !info.IsDir() { 80 + return fmt.Errorf("cd: %s: Not a directory", target) 81 + } 82 + 83 + i.pwdMu.Lock() 84 + i.pwd = next 85 + i.pwdMu.Unlock() 86 + return nil 87 + } 88 + 89 + func (i *Impl) Exec(ctx context.Context, fsys fs.FS, args []string) error { 90 + hc := interp.HandlerCtx(ctx) 91 + 92 + if len(args) == 0 { 93 + return nil 94 + } 95 + 96 + cmdName := args[0] 97 + cmdArgs := args[1:] 98 + 99 + cmd, ok := i.Get(cmdName) 100 + if !ok { 101 + fmt.Fprintf(hc.Stderr, "kefka: command not found: %s\n", cmdName) 102 + return interp.ExitStatus(127) 103 + } 104 + 105 + return cmd.Exec(ctx, &command.ExecContext{ 106 + Stdin: hc.Stdin, 107 + Stdout: hc.Stdout, 108 + Stderr: hc.Stderr, 109 + Dir: i.Pwd(), 110 + Environ: hc.Env, 111 + FS: fsys, 112 + }, cmdArgs) 113 + }
+12
command/registry/wasmprog/wasmprog.go
··· 1 + package wasmprog 2 + 3 + import ( 4 + "tangled.org/xeiaso.net/kefka/command/internal/python3" 5 + "tangled.org/xeiaso.net/kefka/command/registry" 6 + ) 7 + 8 + func Register(reg *registry.Impl) { 9 + // multiple entries for Python should exist 10 + reg.Register("python", python3.Impl{}) 11 + reg.Register("python3", python3.Impl{}) 12 + }
+3 -4
go.mod
··· 4 4 5 5 require ( 6 6 github.com/spf13/pflag v1.0.10 7 + github.com/tetratelabs/wazero v1.11.0 8 + golang.org/x/term v0.41.0 7 9 mvdan.cc/sh/v3 v3.13.1 8 10 ) 9 11 10 - require ( 11 - golang.org/x/sys v0.42.0 // indirect 12 - golang.org/x/term v0.41.0 // indirect 13 - ) 12 + require golang.org/x/sys v0.42.0 // indirect
+2
go.sum
··· 12 12 github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 13 13 github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= 14 14 github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 15 + github.com/tetratelabs/wazero v1.11.0 h1:+gKemEuKCTevU4d7ZTzlsvgd1uaToIDtlQlmNbwqYhA= 16 + github.com/tetratelabs/wazero v1.11.0/go.mod h1:eV28rsN8Q+xwjogd7f4/Pp4xFxO7uOGbLcD/LzB1wiU= 15 17 golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= 16 18 golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= 17 19 golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=