shellsession

package
v0.38.0 Latest Latest
Warning

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

Go to latest
Published: Aug 10, 2026 License: Apache-2.0 Imports: 17 Imported by: 0

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

View Source
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

View Source
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

func WithSpawn(ctx context.Context, cwd, shell string) context.Context

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

type Chunk struct {
	Offset int64
	Data   string
	Reset  bool
}

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

func NewManager(cfg Config) Manager

NewManager builds a Manager and starts its idle reaper.

type ReadResult

type ReadResult struct {
	Content    string
	FromOffset int64
	NextOffset int64
	Exists     bool
}

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.

Jump to

Keyboard shortcuts

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