shell

package
v0.14.7-dev Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 24, 2026 License: MIT Imports: 17 Imported by: 0

Documentation

Overview

Embedded coreutils fallback for the in-process shell.

The matrix shell / `outpost sshd` exec surface runs on whatever the host OS provides — which on Windows means `ls`, `cat`, `head`, `whoami`, … simply don't exist and every agentic caller gets 127. CoreutilsExec closes that gap: commands missing from PATH are resolved against the pure-Go tool registry in github.com/qiangli/coreutils (the sibling library `outpost git` already embeds), so the shell offers one identical core toolset on every platform.

Precedence is deliberate: a real executable on PATH always wins. On unix hosts with a full userland this middleware is a no-op, so existing behavior is unchanged; the fallback only fires where the platform has nothing to offer.

Package shell is the in-process bash interpreter (qiangli/sh / mvdan.cc/sh) wrapped in a PTY so xterm.js sees a real TTY: line discipline, echo, backspace, resize, and Ctrl-C all flow through the kernel TTY layer just as they would for a child `bash` process — except there is no child process.

The interactive read-edit-execute loop lives in mvdan.cc/sh/v3/interactive (a fork-only package). That layer hosts the ergochat/readline integration — arrow-key history navigation, cursor movement, Ctrl-R reverse search — that the upstream parser.Interactive API does not provide. The PTY slave fd is what readline drives in raw mode while reading a line; for command execution between prompts the slave goes back to whatever termios the running command sets, so curses programs (vim, htop) see a real TTY.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func BuildEnv

func BuildEnv() expand.Environ

BuildEnv returns the env that the in-process matrix shell should run in.

Starts from the outpost daemon's own process env (os.Environ()) and **prepends** to PATH a small fixed set of "user-shell-style" directories that launchd-spawned daemons consistently lack:

  • the directory containing the running outpost binary itself (without this, `$(which outpost)` returns empty inside the shell — hits any agentic flow that does `ls -la $(which outpost)` style introspection)
  • $HOME/bin and $HOME/.local/bin (the standard places a user puts locally-installed binaries)
  • /opt/homebrew/{bin,sbin} (macOS Homebrew on Apple Silicon — usually in launchd's default PATH but only on newer macOS versions)
  • /usr/local/bin and /usr/local/sbin (Intel Homebrew, MacPorts; common deploy target for `make install`)

On Windows, service-spawned sessions commonly inherit only the outpost directory in PATH, so BuildEnv also adds the standard Windows executable directories such as C:\Windows\System32 when they are missing.

Entries that don't exist or that PATH already contains are skipped, so running this on a host with a fully-correct PATH is a no-op. Dedup is case-insensitive on Windows to match PATH semantics there.

Returns an expand.Environ suitable for passing to interp.Env(...).

func BuildEnvWith

func BuildEnvWith(overrides map[string]string) expand.Environ

BuildEnvWith is BuildEnv with caller-supplied overrides applied on top. Each key in overrides replaces any existing entry of that name in the outpost process env; absent keys are appended. Used by NewSession to stamp TERM (from the SSH client's pty-req) so vim/htop/less know what escape sequences the terminal understands. Pass nil for no overrides (equivalent to BuildEnv).

func CoreutilsExec added in v0.7.2

func CoreutilsExec(next interp.ExecHandlerFunc) interp.ExecHandlerFunc

CoreutilsExec is an interp.ExecHandlers middleware: when the command name is not resolvable on PATH but is implemented by the embedded coreutils registry, run the embedded implementation in-process. Everything else (PATH hits, unknown names, path-qualified invocations like ./foo) falls through to the next handler unchanged.

func RunLocal added in v0.4.2

func RunLocal(ctx context.Context) (int, error)

RunLocal runs an interactive shell against the caller's stdio. No internal PTY is allocated — the caller's terminal is already a real TTY, so `interactive.Run` raws fd 0 directly via its `bindTTY` helper. Intended for `outpost shell`; the WebSocket/SSH paths still go through Session (which owns its own PTY pair).

History file and the detached-job registry are shared with the matrix-shell path: a `nohup foo &` started here surfaces under `outpost jobs` just like one started over the matrix tunnel.

Returns (exitCode, err). exitCode is 0 on clean exit (`exit` with no arg, Ctrl-D on an empty line), or N from `exit N`. err is non-nil only for setup failures or runner errors that aren't an exit-status carrier.

func RunLocalCommand added in v0.4.2

func RunLocalCommand(ctx context.Context, command string, stdin io.Reader, stdout, stderr io.Writer) (int, error)

RunLocalCommand parses and runs `command` once against the supplied stdio, returning the runner's exit code. Same env construction + detached-job registry hook as RunLocal. Used by `outpost shell -c`.

stdout/stderr default to os.Stdout/os.Stderr when nil; stdin defaults to os.Stdin. Returning (127, err) signals a parse failure.

Types

type Session

type Session struct {
	// contains filtered or unexported fields
}

Session is one interactive shell sitting between a tty pair and a runner. Caller writes to / reads from the master side of the PTY; the runner is hooked up to the slave side as stdin/stdout/stderr.

func NewSession

func NewSession(opts SessionOptions) (*Session, error)

NewSession allocates a PTY pair and constructs the runner. Caller is responsible for closing the returned Session.

func (*Session) Close

func (s *Session) Close() error

Close releases the PTY pair. Safe to call multiple times.

func (*Session) CloseSlave

func (s *Session) CloseSlave() error

CloseSlave closes the slave (runner-side) PTY fd only, leaving the master open so a reader can drain any kernel-buffered output. Used by the SSH exec-with-pty path: after the runner finishes we close the slave to signal EOF, wait for the PTY→channel goroutine to drain, then Close() the rest. Closing the master prematurely would drop bytes still in the kernel buffer — which is exactly the bug this method was added to fix.

func (*Session) Done

func (s *Session) Done() <-chan struct{}

Done returns a channel closed after Run returns.

func (*Session) Master

func (s *Session) Master() io.ReadWriteCloser

Master returns the master end. The caller pipes WebSocket bytes ↔ this.

func (*Session) Resize

func (s *Session) Resize(cols, rows uint16) error

Resize updates the PTY's window size — equivalent to a SIGWINCH inside the runner. cols/rows in characters.

func (*Session) Run

func (s *Session) Run(ctx context.Context) error

Run starts the interactive read-edit-execute loop, blocking until ctx is canceled, the user exits (the `exit` builtin or Ctrl-D on an empty line), or a fatal interp error.

All line editing — arrow-key history navigation, cursor movement, backspace/Ctrl-W/Ctrl-U editing, Ctrl-R reverse search, history persistence — is delegated to mvdan.cc/sh/v3/interactive (which wraps ergochat/readline). The PTY slave fd is the TTY readline drives in raw mode; the swap back to cooked between prompts is what lets curses programs spawned by a stmt see a real /dev/ttysNN.

Per-stmt cancellation: each parsed statement runs under a child context so a future signal-handling layer (Ctrl-C wiring on the PTY) can cancel just the current command without ending the session.

func (*Session) RunOnce

func (s *Session) RunOnce(ctx context.Context, command string) uint32

RunOnce parses `command` and runs it once through the PTY-backed runner, then returns. Used by the SSH `exec` path when the client asked for a TTY first (`ssh -tt host cmd`) — the command sees a real /dev/ttysNN so `tty`, `screen -dmS`, etc. behave like they do under real openssh. Caller pipes the channel ↔ s.Master() and tears down the session when RunOnce returns.

Returns a POSIX-style exit status (0 = ok, non-zero from the command or from a parse error → 127 / 1).

type SessionOptions

type SessionOptions struct {
	// Term is the TERM env var the runner should see (e.g. "xterm-256color"
	// from an SSH pty-req). Empty = inherit outpost's TERM (usually unset
	// in a daemon context, which makes vim/htop fall back to dumb mode).
	Term string
	// Cols/Rows are the initial PTY window dimensions in characters.
	// Both 0 = skip the initial resize.
	Cols uint16
	Rows uint16
	// Env is an optional set of env-var overrides applied on top of
	// the daemon's env. The SSH server uses this to stamp
	// SSH_AUTH_SOCK from per-session agent forwarding (`ssh -A`).
	// Empty/nil = no overrides.
	Env map[string]string
}

SessionOptions configures a new shell Session. All fields are optional — the zero value is "no PTY hints, inherit outpost's env verbatim", which matches the pre-options behavior used by the xterm.js /shell path.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL