Documentation
¶
Overview ¶
Package sandbox runs model-chosen shell commands under the operating system's sandbox (2.0 W4, #176): a seatbelt profile on macOS, bubblewrap on Linux. Writes are limited to the workspace, temp and cache directories; reads are not restricted; the network can be cut. Windows has no sandbox: commands run with the bash denylist alone.
Index ¶
- Constants
- func Available() (kind string, ok bool)
- func BwrapArgs(p Policy, command string) []string
- func DefaultWritable(home, workspace string) []string
- func GitDirs(workspace string) []string
- func GitPointers(workspace string) []string
- func GitProtected(gitDirs []string) []string
- func Hint(kind string, p Policy, output string) string
- func MakeGitProtected(gitDirs []string)
- func Normalize(paths []string) []string
- func Profile(p Policy) string
- func Resolve(path string) string
- func RestorePaths(before []PathState) error
- func SetAvailableForTest(kind string, ok bool) func()
- func Wrap(p Policy, command string) (argv []string, kind string)
- type PathState
- type Policy
- type Repo
Constants ¶
const ( KindSeatbelt = "seatbelt" KindBubblewrap = "bubblewrap" )
The sandbox kinds Available reports, also named in blocked-write hints.
const DefaultEnabled = false
DefaultEnabled is whether the sandbox is on when no config says otherwise. Off for 2.0 (opt in with "sandbox": {"enabled": true} in ~/.celeste/config.json), so upgrading never breaks a build that writes outside the workspace.
Variables ¶
This section is empty.
Functions ¶
func Available ¶
Available reports which OS sandbox this process can use: "seatbelt" on macOS when sandbox-exec works, "bubblewrap" on Linux when bwrap is installed and its self-test passes; false elsewhere (Windows has none). Checked once per process.
func BwrapArgs ¶
BwrapArgs is the bubblewrap argument list (everything after "bwrap") that runs command under p: the whole filesystem read-only, a fresh /dev and /proc, /run hidden behind a tmpfs (its sockets, the docker and session buses among them, reach outside the sandbox) except the directory /etc/resolv.conf points into (systemd's, NetworkManager's or resolvconf's), read-only, then each writable directory that exists bound read-write, then each read-only path that exists bound read-only over them. --unshare-pid is what lets an unprivileged bwrap mount /proc; --new-session takes the command off the terminal. bwrap itself leads the runner's new session and process group, so a timeout kills it whole.
func DefaultWritable ¶
DefaultWritable is the writable list for home and workspace: the workspace, the temp directories, the user cache directory and the build caches that exist (buildCaches). Resolved, de-duplicated and sorted.
func GitDirs ¶
GitDirs returns the git directories of the repository workspace is in (FindRepo), resolved and sorted: its git dir and, for a linked worktree, the repository's common dir (objects, refs). For a workspace at a plain repository's root they are inside it already; for a linked worktree (a subagent's isolated lane, or a user's own), a submodule or a subdirectory of a repository they are not, and without them git add and git commit fail under the sandbox. nil outside a repository and for a refused .git.
func GitPointers ¶
GitPointers returns the paths that tell git where workspace's git dirs are, which a sandboxed command must not change: each git dir's commondir and gitdir, the workspace repository's .git when it is a file, and workspace/.git when the repository's root is above the workspace (a .git there would be found first). They are read-only under the sandbox where they exist (ReadOnly); bubblewrap cannot bind over a missing one, so the runner also checks them after every sandboxed command (SnapshotPaths, RestorePaths). Resolved and sorted; nil outside a repository.
func GitProtected ¶
GitProtected returns the paths in each of gitDirs that git outside the sandbox would run programs from, or would follow to another git dir, and that therefore stay read-only to sandboxed commands: config and config.worktree (core.fsmonitor, merge and filter drivers, core.hooksPath), hooks, and commondir and gitdir (git takes its config from the dir a commondir names, in any git dir, a plain .git included). A symlinked one is listed as the link and as its target, so neither can be replaced.
func Hint ¶
Hint explains a failed command that ran under the kind sandbox with policy p, when its output looks like the sandbox refused it: the sandbox and the config key to change (spec §5 W4, ruling 10). "" when the output names nothing the sandbox blocks.
func MakeGitProtected ¶
func MakeGitProtected(gitDirs []string)
MakeGitProtected creates the GitProtected paths missing in each of gitDirs, as git would: an empty hooks directory and an empty config.worktree (read only when extensions.worktreeConfig is on), so bubblewrap, which can bind only over a path that exists, keeps them read-only too. config itself is never created. Errors are ignored: a path that cannot be created cannot be planted either.
func Profile ¶
Profile renders p as a seatbelt (SBPL) profile: everything is allowed but file writes, which are allowed only under the writable directories and to the terminal and pipe devices, never to the read-only paths; with the network off, everything but local unix sockets is denied too. Later rules win in SBPL, so each allowance follows its deny and the read-only paths' deny follows the allowance. That deny also covers each read-only path's ancestors themselves (not what is in them), so a directory holding one cannot be renamed aside and replaced.
func Resolve ¶
Resolve returns path made absolute, with symlinks resolved (the sandboxes compare real paths: /tmp is /private/tmp on macOS). For a path that does not exist yet, its deepest existing ancestor is resolved and the rest kept.
func RestorePaths ¶
RestorePaths puts every path in before back as it was: what appeared is removed, a regular file that changed or went away is rewritten. Only a regular file or nothing can be restored; anything else that changed is reported. It returns an error naming the paths it found changed, nil when none was. Run outside the sandbox after a sandboxed command (shellrun), it undoes a pointer the sandbox could not bind read-only because it did not exist yet (GitPointers): every process of a bubblewrap sandbox is gone by then, so none can plant it again.
func SetAvailableForTest ¶
SetAvailableForTest makes Available report kind and ok (bubblewrap runs as "bwrap" from PATH) until the returned function restores the real check. For tests in other packages.
Types ¶
type PathState ¶
type PathState struct {
Path string
Present bool // anything is there
Regular bool // a regular file, whose content is Data
Data []byte // a regular file's content, a symlink's target
Mode os.FileMode // the type and permission bits
}
PathState is one watched path as SnapshotPaths saw it.
func SnapshotPaths ¶
SnapshotPaths records each path: missing, a regular file and its content, a symlink and its target, or something else of a type.
type Policy ¶
type Policy struct {
Enabled bool
Workspace string // resolved
Writable []string // resolved, de-duplicated, sorted; includes the defaults
// ReadOnly stays read-only even inside a writable directory: each git
// dir's config, hooks and commondir (GitProtected) and the paths that
// lead git to them (GitPointers). Resolved.
ReadOnly []string
// Watch are the paths the runner puts back after each sandboxed
// command (SnapshotPaths, RestorePaths): GitPointers, which bubblewrap
// cannot bind read-only while they do not exist.
Watch []string
Network bool
}
Policy is what one shell command may do.
type Repo ¶
type Repo struct {
WorkTree string // the directory holding .git
DotGit string // that .git, a directory or a file
GitDir string // "" when the .git was refused
CommonDir string // the GitDir itself unless it is a linked worktree's
Refusal string // why the .git was refused, and what to do; "" when it was not
}
Repo is the repository a directory is in, as FindRepo found it.
func FindRepo ¶
FindRepo returns the repository dir is in, resolved: found is false when there is no .git in dir or above it. It reads the .git file or directory itself, as git does, rather than running git: the nearest ancestor's .git wins. The workspace's content controls that .git (a sandboxed command can write one), so nothing it names is believed unless git itself would have laid it out that way: a symlinked .git is refused; a .git file's "gitdir:" line must name a linked worktree's admin dir (<common>/worktrees/<name>, whose gitdir file points back to this .git and whose commondir is <common>, a real git dir) or a submodule's git dir (whose core.worktree points back to this directory). A refused .git gives a Repo with no GitDir, and Refusal says why. A plain .git directory is its own git dir and common dir; a commondir file in it is ignored.