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 ¶
- func BuildEnv() expand.Environ
- func BuildEnvWith(overrides map[string]string) expand.Environ
- func CoreutilsExec(next interp.ExecHandlerFunc) interp.ExecHandlerFunc
- func RunLocal(ctx context.Context) (int, error)
- func RunLocalCommand(ctx context.Context, command string, stdin io.Reader, stdout, stderr io.Writer) (int, error)
- type Session
- func (s *Session) Close() error
- func (s *Session) CloseSlave() error
- func (s *Session) Done() <-chan struct{}
- func (s *Session) Master() io.ReadWriteCloser
- func (s *Session) Resize(cols, rows uint16) error
- func (s *Session) Run(ctx context.Context) error
- func (s *Session) RunOnce(ctx context.Context, command string) uint32
- type SessionOptions
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BuildEnv ¶
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 ¶
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
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) CloseSlave ¶
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 ¶
Resize updates the PTY's window size — equivalent to a SIGWINCH inside the runner. cols/rows in characters.
func (*Session) Run ¶
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 ¶
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.