Documentation
¶
Overview ¶
Package shellsession manages one persistent PTY-backed shell per chat session, rooted at the session's workspace root and outliving individual commands. Output lives in a bounded scrollback ring with monotonically increasing offsets; the agent never receives it streamed and must read scrollback explicitly. Run submits exactly one line, gated by HITL.
Index ¶
Constants ¶
const ( ToolsProviderName = "shell_session" ToolRun = "shell_session_run" ToolRead = "shell_session_read" )
Tool names. The provider ("shell_session") is one ToolsRepo exposing two function tools. Run is gated by the same HITL machinery that wraps every tool (default policy → approve); Read is ungated by policy (reference-only reads).
Variables ¶
var ErrNoSession = errors.New("shellsession: no live shell for this session")
ErrNoSession is returned by Write for a session with no live shell. Distinct from a spawn failure: the caller addressed something that is not there.
Functions ¶
func NewTools ¶
func NewTools(mgr Manager) taskengine.ToolsRepo
NewTools returns the shell_session ToolsRepo. Register it in the engine's LocalTools map under ToolsProviderName exactly like local_shell/local_fs, so it is HITL-wrapped and reachable to the agent only when shell tooling is on.
func WithSpawn ¶ added in v0.38.0
WithSpawn attaches a per-shell cwd and/or shell to ctx, honoured by the next shell this manager creates for the session named in that call. Empty strings fall back to the manager's CwdResolver and Config.Shell. The cwd is still validated against the workspace allowlist — an override chooses among permitted roots, it never escapes them.
Types ¶
type Chunk ¶
Chunk is one batch of terminal output delivered to a subscriber. Offset is the absolute scrollback offset where Data begins. Reset marks the initial snapshot a fresh subscriber receives (or a stream restart after the PTY was recreated), signalling the consumer to replace rather than append.
type Config ¶
type Config struct {
// CwdResolver returns the workspace root a new shell should be rooted at,
// given the tool/request context (which carries the session id). Required.
CwdResolver func(ctx context.Context) string
// Workspace is the operator's workspace-root allowlist, enforced against
// whatever CwdResolver returns; the only source of the default root.
// Nil means no allowlist, and an absolute cwd is taken as given.
Workspace *vfs.Factory
// Shell overrides the shell executable; empty picks a platform default.
Shell string
// ScrollbackBytes bounds retained output per shell (default 64 KiB).
ScrollbackBytes int
// IdleTimeout kills inactive shells (default 15m; <=0 disables reaping).
IdleTimeout time.Duration
// ScrubEnv, when set, maps the parent environment to the one a spawned
// shell inherits, so serve's own secrets never reach an agent-reachable
// PTY. Nil inherits the full environment.
ScrubEnv func([]string) []string
// Interactive spawns shells for a HUMAN at a real terminal: ECHO stays on
// and the shell draws its own prompt, because the operator must see what
// they type. The default (false) is the agent-facing posture — echo off,
// prompt suppressed — where output is scrollback for a model to read and a
// prompt is noise plus a login/host/cwd leak.
Interactive bool
// OnExit, when set, is invoked once per shell when it terminates, from a
// dedicated goroutine. Fires for every cause — process exit, Kill, idle
// reap, Shutdown — so a client can report the terminal as gone exactly
// once. Total: never called twice for the same shell.
OnExit func(sessionID string)
}
Config configures a Manager. Zero values fall back to sane defaults.
type Manager ¶
type Manager interface {
// Run ensures a shell exists for sessionID (rooted via the cwd resolver
// against ctx) and submits one line to it. ctx is used only for cwd
// resolution at creation time.
Run(ctx context.Context, sessionID, line string) (RunResult, error)
// Open ensures a shell exists for sessionID without submitting anything,
// so an interactive client can attach before the first keystroke.
// Idempotent: an already-live shell is returned as-is.
Open(ctx context.Context, sessionID string) error
// Write feeds raw bytes to sessionID's shell stdin VERBATIM — unlike Run
// it appends no newline and imposes no line discipline, because the bytes
// are a human's keystrokes (arrow keys, ^C, partial lines). Never creates
// a shell: an unknown session is ErrNoSession.
Write(sessionID string, data []byte) error
// Read returns scrollback for sessionID: bytes since `since` when since >= 0,
// otherwise the last `tailBytes`. Never creates a shell.
Read(sessionID string, since int64, tailBytes int) ReadResult
// Resize records the terminal geometry for sessionID and applies it to
// the live shell when there is one. Total: an unknown session, a reaped
// shell, or a non-positive dimension are no-ops, not errors. The size is
// remembered even with no live shell, so the next one is born at it.
Resize(sessionID string, rows, cols int)
// Subscribe registers fn for live output of sessionID, invoked from a
// dedicated goroutine so a slow consumer cannot stall the PTY. The
// current scrollback is delivered immediately as a Reset chunk.
Subscribe(sessionID string, fn func(Chunk)) (cancel func())
// Kill terminates and forgets sessionID's shell (session close/delete).
Kill(sessionID string)
// Shutdown kills every shell and stops the reaper.
Shutdown()
}
Manager owns the process-global set of per-session shells. All methods are safe for concurrent use and key on the internal chat-session id.
func NewManager ¶
NewManager builds a Manager and starts its idle reaper.
type ReadResult ¶
ReadResult is a scrollback slice: the content, the offset it starts at, and the current end marker to hand to the next read.
type ReadResultJSON ¶
type ReadResultJSON struct {
Content string `json:"content"`
FromOffset int64 `json:"from_offset"`
NextOffset int64 `json:"next_offset"`
Exists bool `json:"exists"`
Note string `json:"note,omitempty"`
}
ReadResultJSON is the structured result of a scrollback read.
type RunResult ¶
type RunResult struct {
Offset int64
Snapshot string
Started bool // a new shell was created for this run
}
RunResult is what Run returns after submitting a line: the scrollback end marker and a best-effort snapshot of the output captured within the initial window (empty when the command is still running silently).
type RunResultJSON ¶
type RunResultJSON struct {
Offset int64 `json:"offset"`
Output string `json:"output"`
Started bool `json:"started_new_shell,omitempty"`
Note string `json:"note,omitempty"`
}
RunResultJSON is the structured result the agent receives from a run: a marker and the initial output snapshot. The agent polls shell_session_read with the returned offset to follow long-running commands.