shell

package
v0.2.0 Latest Latest
Warning

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

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

Documentation

Overview

Package shell runs the built-in bash tool as a persistent per-session shell, on top of the sandbox's stateless Exec + file primitives — no new backend surface. cwd, exported variables, functions, aliases, and shell options survive across calls via a snapshot on the container's writable layer; the other five built-in tools use Exec/ReadFile/WriteFile directly and share nothing with the shell's state.

Each call runs as its own exec process, so the sandbox's outside-the-container deadline applies to the command verbatim and cannot be forged from inside — the property a single always-running shell cannot keep, because with the command running AS the resident shell, foreground-versus-background is shell-internal state the command can rewrite.

What this package does NOT do is decide whether a tool call may run twice. The snapshot lives in the sandbox, and a sandbox is cattle: its filesystem is agent-writable, and it can be reaped and re-provisioned under a retry, so it is neither a trustworthy nor a durable ledger. At-most-once belongs to the executor and the work queue, whose store is the event log.

Divergences from a resident shell — all of them, each pinned by a test:

  • The `jobs` table does not carry. A backgrounded process survives (it keeps running, reachable by pid), but the next call is a new shell with an empty job table.
  • Plain (non-exported) variables do not carry; exported ones do. Nothing in `declare` separates a user's plain variables from bash's own internals, so the snapshot draws the line at `export`.
  • Traps do not carry, and an EXIT trap fires at most once, within the call that installed it. It fires only if the command exits THROUGH it; a command that installs one and then returns normally has it discarded unfired, because the template clears the trap to run its own save. There is no session-long shell for a trap to fire at the end of either way. A command that both installs its own EXIT trap and exits through it also skips that call's snapshot — see the next divergence for what that costs.
  • A call whose shell never finishes its snapshot keeps the PREVIOUS call's state. Replacing the shell (`exec`), having it killed outright (`kill -9 $$`, an OOM kill), and exiting through an EXIT trap of one's own all skip the save. Such a call drops its own mutations and the session carries on from the last complete snapshot; a resident shell would have died with the process instead.
  • A timed-out call's mutations are dropped. A resident shell would keep the ones made before the kill; a SIGKILL leaves no chance to snapshot them, so dropping them is the only behaviour available consistently on both the killed path and the dodged-the-kill-and-overran path.
  • The timeout bounds the whole call — restore, command, snapshot — not the command alone. The bracket is milliseconds, but a very short timeout pays for it.
  • xtrace (`set -x`) does not carry, though every other option does. A carried xtrace would have the restore re-enable it and then trace the template's own machinery — the internal state path, the tool-call id — into every later call's stderr, including calls that never asked for it. The call that runs `set -x` still traces its own prologue; the next call starts clean.
  • A command that shadows the template's own machinery costs its own call, not the session — with one architectural exception, a function named `builtin`. Every name the template owns is `__map_*` and excluded from the snapshot; every builtin it depends on — in the save AND in the restore — is reached through `builtin`, which a shadowing function cannot intercept, and where an alias could still reach the word (anything re-parsed at runtime) it is quoted as well, which a shadowing alias cannot survive. But `builtin` is the one word that routes around a function, so a function NAMED `builtin` is unguardable: written to return 0 it spins the save until the deadline (its own call only); written to break one builtin while delegating the rest it can make the save write an empty snapshot, earn the marker, and reset the command's OWN session (never another — head is per-session). No keyword can enumerate and serialize the shell, so there is no builtin-free path to fall back on. It is deliberate self-sabotage, contained like `rm -rf` of the state dir by the sandbox boundary, not by the template.

The snapshot is the agent's own shell state, not a security boundary: a command running as root in the container can rewrite or delete it, and only sabotages its own session by doing so. The guarantees that matter — the deadline, and at-most-once — are enforced outside the container, where it cannot reach them.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Request

type Request struct {
	Command string        // user command bytes, run verbatim
	Restart bool          // reset the shell (cwd/env/functions/aliases/options) first
	Timeout time.Duration // per-command; 0 means only the context bounds it
}

Request is one bash tool call.

type Result

type Result struct {
	Stdout    string
	Stderr    string
	ExitCode  int
	TimedOut  bool
	Truncated bool
	Restarted bool
}

Result mirrors sandbox.ExecResult with the restart flag the tool reports back.

func Run

func Run(ctx context.Context, sb sandbox.Sandbox, session, id domain.ID, req Request) (Result, error)

Run executes one bash tool call against sb, carrying the shell's state in the container between calls. session scopes the state; id names this call's command file.

Jump to

Keyboard shortcuts

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