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.