Documentation
¶
Index ¶
- Constants
- Variables
- func ExecSandboxHelper(spec SandboxSpec, argv []string) error
- func FirstReadAllowCredentialConflict(userHome, devstrapHome string, readAllow []string) string
- func ProcessStartTime(pid int) (int64, error)
- type BubblewrapSandbox
- type EditorAdapter
- type FSEvent
- type FSEventKind
- type Keychain
- type LandlockSandbox
- type LinuxSandbox
- func (LinuxSandbox) Available() error
- func (LinuxSandbox) Command(ctx context.Context, spec SandboxSpec, argv []string) (SandboxCommand, error)
- func (LinuxSandbox) Limitations() []string
- func (LinuxSandbox) Name() string
- func (LinuxSandbox) NetworkDenyEnforcement() NetworkEnforcement
- func (LinuxSandbox) ReadConfineEnforcement() ReadConfineEnforcement
- type NativeWatcher
- type NetworkEnforcement
- type PollWatcher
- type ReadConfineEnforcement
- type RuntimeInfo
- type Sandbox
- type SandboxCapabilities
- type SandboxCommand
- type SandboxReadConfinement
- type SandboxSpec
- type SandboxViolation
- type SandboxViolationReporter
- type ServiceManager
- type ServiceSpec
- type ServiceStatus
- type Set
- type SystemEditor
- type SystemKeychain
- func (k SystemKeychain) Delete(_ context.Context, service, account string) error
- func (k SystemKeychain) Load(_ context.Context, service, account string) ([]byte, error)
- func (k SystemKeychain) Name() string
- func (k SystemKeychain) Store(_ context.Context, service, account string, secret []byte) error
- type SystemdUserManager
- func (m SystemdUserManager) DefaultLabel() string
- func (m SystemdUserManager) Install(ctx context.Context, spec ServiceSpec) ([]string, error)
- func (m SystemdUserManager) Name() string
- func (m SystemdUserManager) Status(ctx context.Context, label string) (ServiceStatus, error)
- func (m SystemdUserManager) Uninstall(ctx context.Context, label string) ([]string, error)
- type UnsupportedKeychain
- type UnsupportedSandbox
- type UnsupportedServiceManager
- func (m UnsupportedServiceManager) DefaultLabel() string
- func (m UnsupportedServiceManager) Install(context.Context, ServiceSpec) ([]string, error)
- func (m UnsupportedServiceManager) Name() string
- func (m UnsupportedServiceManager) Status(context.Context, string) (ServiceStatus, error)
- func (m UnsupportedServiceManager) Uninstall(context.Context, string) ([]string, error)
- type Watcher
Constants ¶
const NoKeychainEnv = "DEVSTRAP_NO_KEYCHAIN"
NoKeychainEnv, when set to "1", forces the file-backed key store instead of the OS keychain. This is required for headless/CI runs and hermetic end-to-end tests, where an OS keychain call can block on a GUI unlock prompt.
const SandboxBackendEnv = "DEVSTRAP_SANDBOX_BACKEND"
SandboxBackendEnv forces the Linux sandbox backend instead of the bwrap-then-landlock probe order: "bwrap" pins the full-fidelity mount sandbox (credential masks, total netns deny), "landlock" pins the fallback (useful when bwrap's user namespaces break nested-sandbox tools, and for CI to exercise the landlock path on hosts where bwrap works). A forced backend never silently falls back: its own availability error surfaces so `--sandbox require` sees the real reason.
const SandboxHelperCommand = "sandbox-helper"
SandboxHelperCommand is the hidden re-exec shim subcommand backing the Landlock fallback; exported so the CLI registers the cobra command under exactly the name this package renders into the wrapped argv. Landlock restricts the calling process and cannot be applied between fork and exec by os/exec (golang/go#68595), so the parent re-execs devstrap itself: the shim applies the ruleset to its own process, then execve()s the agent argv in the same PID — ctx-kill and exit-code semantics are untouched, and the ruleset persists across execve.
Variables ¶
var ( ErrUnsupported = errors.New("platform feature unsupported") ErrEditorNotFound = errors.New("editor command not found") ErrSecretNotFound = errors.New("platform secret not found") )
var ErrInvalidSandboxBackend = errors.New("invalid sandbox backend")
ErrInvalidSandboxBackend marks a sandbox backend override whose VALUE is invalid (e.g. a typo in DEVSTRAP_SANDBOX_BACKEND). It is deliberately distinct from ErrUnsupported: an unsupported host may degrade to advisory mode, but a mistyped explicit configuration knob must fail closed — treating it as a capability gap would let a typo silently disable the OS sandbox.
Functions ¶
func ExecSandboxHelper ¶ added in v0.1.2
func ExecSandboxHelper(spec SandboxSpec, argv []string) error
ExecSandboxHelper is the sandbox-helper body: apply the Landlock ruleset to THIS process (go-landlock sets no_new_privs; the ruleset persists across execve), then replace the image with the agent argv. Same PID, so the parent's exec.CommandContext kill and exit-code propagation are untouched. Only returns on error; the CLI maps that to the wrapper-failed exit code.
func FirstReadAllowCredentialConflict ¶ added in v0.1.2
FirstReadAllowCredentialConflict returns the first --read-allow root that overlaps a protected credential path, or "" if none. Read confinement drops bwrap's credential masks (subsumed by the allow-list) and Landlock cannot subtract from an allowed root, so a read-allow root that contains or sits inside a credential path would silently re-expose it on those backends — this lets the CLI fail closed instead. Seatbelt would still deny it (its credential deny is emitted last), but the guard keeps all backends honest.
func ProcessStartTime ¶ added in v0.1.2
ProcessStartTime returns the process's raw /proc starttime identity (clock ticks since boot). The value is opaque and is only suitable for equality comparisons on the same host and boot.
Types ¶
type BubblewrapSandbox ¶ added in v0.1.2
type BubblewrapSandbox struct{}
BubblewrapSandbox confines agent children with bubblewrap (AGEN-03 / P4-GIT-03 Linux slice): an unprivileged user-namespace mount sandbox that ships in every major distro. Availability is probed, not stat'ed, because distro policy (Ubuntu 23.10+/24.04 AppArmor userns restriction, container seccomp) routinely leaves bwrap installed but unable to create namespaces — on such hosts `auto` mode degrades to a loud warning and `require` refuses to run.
func (BubblewrapSandbox) Available ¶ added in v0.1.2
func (s BubblewrapSandbox) Available() error
func (BubblewrapSandbox) Command ¶ added in v0.1.2
func (s BubblewrapSandbox) Command(_ context.Context, spec SandboxSpec, argv []string) (SandboxCommand, error)
func (BubblewrapSandbox) Name ¶ added in v0.1.2
func (BubblewrapSandbox) Name() string
func (BubblewrapSandbox) ReadConfineEnforcement ¶ added in v0.1.2
func (BubblewrapSandbox) ReadConfineEnforcement() ReadConfineEnforcement
ReadConfineEnforcement implements SandboxReadConfinement: bubblewrap exposes only the read-confinement roots (`--ro-bind-try`) instead of the whole filesystem, so it always kernel-enforces read confinement.
type EditorAdapter ¶
type FSEventKind ¶
type FSEventKind string
const ( FSEventUnknown FSEventKind = "unknown" FSEventScan FSEventKind = "scan" )
type LandlockSandbox ¶ added in v0.1.2
type LandlockSandbox struct{}
LandlockSandbox is the layered Linux fallback for hosts where bubblewrap cannot create namespaces (AGEN-03 / P4-GIT-03 slice 3). It confines writes to the worktree and per-run temp dir via a hidden re-exec shim; unlike bubblewrap it is additive-allow, so it deliberately does NOT deny credential reads (spec/18 decision) — the degrade is surfaced through SandboxCapabilities.
func (LandlockSandbox) Available ¶ added in v0.1.2
func (s LandlockSandbox) Available() error
func (LandlockSandbox) Command ¶ added in v0.1.2
func (s LandlockSandbox) Command(_ context.Context, spec SandboxSpec, argv []string) (SandboxCommand, error)
func (LandlockSandbox) Name ¶ added in v0.1.2
func (LandlockSandbox) Name() string
func (LandlockSandbox) ReadConfineEnforcement ¶ added in v0.1.2
func (LandlockSandbox) ReadConfineEnforcement() ReadConfineEnforcement
ReadConfineEnforcement implements SandboxReadConfinement: under read confinement the additive-allow RODirs grant is restricted to the roots (the strict V3 floor covers read+execute), so Landlock kernel-enforces read confinement — and, unlike its default additive-allow reads, this finally gives it a credential-read boundary.
type LinuxSandbox ¶ added in v0.1.2
type LinuxSandbox struct{}
LinuxSandbox lazily selects bubblewrap, then landlock, then unsupported (P4-GIT-03 slice 3). Both probes are cached OnceValues, so Name/Available/Command converge on one decision per process and the expensive bwrap namespace launch still happens at most once.
func (LinuxSandbox) Available ¶ added in v0.1.2
func (LinuxSandbox) Available() error
func (LinuxSandbox) Command ¶ added in v0.1.2
func (LinuxSandbox) Command(ctx context.Context, spec SandboxSpec, argv []string) (SandboxCommand, error)
func (LinuxSandbox) Limitations ¶ added in v0.1.2
func (LinuxSandbox) Limitations() []string
Limitations implements SandboxCapabilities: empty for a full-fidelity bubblewrap host, populated with the landlock-fallback degrade contract when that backend was selected (prefixed with why bwrap was passed over so the one notice line tells the whole story), and — for either backend — with the seccomp line when the kernel cannot install the syscall denylist.
func (LinuxSandbox) Name ¶ added in v0.1.2
func (LinuxSandbox) Name() string
func (LinuxSandbox) NetworkDenyEnforcement ¶ added in v0.1.2
func (LinuxSandbox) NetworkDenyEnforcement() NetworkEnforcement
NetworkDenyEnforcement implements SandboxCapabilities: bubblewrap's network namespace removes the network entirely; the landlock fallback denies only TCP bind/connect, and only from kernel ABI v4 on — below that a DenyNetwork policy would run with the network fully open, which resolveAgentSandbox refuses under `require`. The TCP-only grade is deliberately not reported as total: UDP, QUIC, and unix-domain sockets stay open under landlock.
func (LinuxSandbox) ReadConfineEnforcement ¶ added in v0.1.2
func (LinuxSandbox) ReadConfineEnforcement() ReadConfineEnforcement
ReadConfineEnforcement implements SandboxReadConfinement by delegating to the selected backend: both bubblewrap (`--ro-bind-try` roots) and the Landlock fallback (RODirs restricted to the roots) kernel-enforce read confinement, so either selection enforces it.
type NativeWatcher ¶
func (NativeWatcher) Name ¶
func (w NativeWatcher) Name() string
type NetworkEnforcement ¶ added in v0.1.2
type NetworkEnforcement int
NetworkEnforcement grades how completely a Sandbox can enforce SandboxSpec.DenyNetwork. A plain boolean overclaims: Landlock's "deny" covers only TCP bind/connect, which must not read as netns-grade isolation (adversarial review P2 — DNS/QUIC/unix-socket exfiltration stays possible).
const ( // NetworkDenyNone means the deny cannot be kernel-enforced at all; a // `require`-mode run whose policy demands a network deny refuses to // launch. NetworkDenyNone NetworkEnforcement = iota // NetworkDenyPartialTCP denies only TCP bind/connect — UDP, QUIC, and // unix-domain sockets stay open (Landlock ABI >= 4). Satisfies `require` // but is surfaced as a warning, never as a full deny. NetworkDenyPartialTCP // NetworkDenyTotal removes the child's network entirely (bubblewrap's // network namespace). NetworkDenyTotal )
type PollWatcher ¶
func (PollWatcher) Name ¶
func (w PollWatcher) Name() string
type ReadConfineEnforcement ¶ added in v0.1.2
type ReadConfineEnforcement int
ReadConfineEnforcement grades whether a Sandbox can honor SandboxSpec.ReadConfine. Like NetworkEnforcement, this is a capability the caller checks before promising `--read-confine on` under `--sandbox require`: an explicit knob must never silently no-op.
const ( // ReadConfineNone means the backend cannot restrict reads to an allow-list // (no such backend exists today; reserved for a future degraded adapter). ReadConfineNone ReadConfineEnforcement = iota // ReadConfineEnforced means reads are kernel-confined to ReadConfineRoots. ReadConfineEnforced )
type RuntimeInfo ¶
func Runtime ¶
func Runtime() RuntimeInfo
type Sandbox ¶
type Sandbox interface {
Name() string
// Available reports whether the sandbox can be used on this host; the
// returned error explains why not (wrapped in ErrUnsupported).
Available() error
// Command returns the argv wrapped in the sandbox launcher, any extra file
// descriptors the launcher references, and a cleanup function.
Command(ctx context.Context, spec SandboxSpec, argv []string) (SandboxCommand, error)
}
Sandbox wraps an agent argv in an OS-enforced confinement (AGEN-03 / P4-GIT-03). Unlike the wrapper-level argv/file policies (which are advisory and interpreter-bypassable), a Sandbox is enforced by the kernel for the child and everything it spawns.
type SandboxCapabilities ¶ added in v0.1.2
type SandboxCapabilities interface {
// Limitations returns human-readable degrade notes for the selected
// backend; callers print them as one notice line.
Limitations() []string
// NetworkDenyEnforcement reports how completely SandboxSpec.DenyNetwork
// will be kernel-enforced for the selected backend.
NetworkDenyEnforcement() NetworkEnforcement
}
SandboxCapabilities is an optional interface a Sandbox implements when its confinement can be weaker than the platform's full-fidelity backend. Absence of the interface (or an empty Limitations) means full fidelity. Encoding this in Available()'s error would be wrong — available-but-degraded is not an error — and Name() is not structured.
type SandboxCommand ¶ added in v0.1.2
type SandboxCommand struct {
// Argv is the fully wrapped command to exec.
Argv []string
// ExtraFiles are wired to exec.Cmd.ExtraFiles: entry i is inherited by the
// launcher as fd 3+i, and Argv may reference those fd numbers (bubblewrap's
// --seccomp <fd> reads the compiled filter this way). Nil when the backend
// needs no inherited fds.
ExtraFiles []*os.File
// Cleanup is always non-nil-safe to call once the command has completed: it
// closes ExtraFiles and removes any generated profile file.
Cleanup func()
}
SandboxCommand is the result of wrapping an agent argv in a Sandbox launcher.
type SandboxReadConfinement ¶ added in v0.1.2
type SandboxReadConfinement interface {
ReadConfineEnforcement() ReadConfineEnforcement
}
SandboxReadConfinement is an optional interface a Sandbox implements when it can enforce SandboxSpec.ReadConfine. Kept separate from SandboxCapabilities so adding it does not break that interface's existing implementers.
type SandboxSpec ¶
type SandboxSpec struct {
// WorktreeDir is the agent worktree — the only project location the
// child may write.
WorktreeDir string
// TmpDir is the child's temp directory (writable).
TmpDir string
// LogDir is where the generated profile file lives. It is NOT granted to
// the child: the agent log is written by the parent process, so the child
// only ever sees inherited pipes — keeping LogDir read-only for the child
// prevents it from tampering with its own 0600 log or the profile.
LogDir string
// UserHome is the REAL user home. The agent child env repoints HOME to
// the worktree (SECU-02), but the filesystem still contains the user's
// dotfiles — sensitive-read denies are anchored here.
UserHome string
// DevstrapHome is the DevStrap home (~/.devstrap); its keys directory is
// read-denied.
DevstrapHome string
// DenyNetwork blocks all network access for the child.
DenyNetwork bool
// DenySensitiveReads blocks reads of credential-bearing paths under
// UserHome (.ssh, .aws, .gnupg, .config/gh, .kube, .docker) and
// DevstrapHome/keys.
DenySensitiveReads bool
// DenyDangerousSyscalls installs the seccomp syscall denylist (mount,
// ptrace, kernel-module, keyring, io_uring, and other escape-primitive
// syscalls return EPERM). Unconditional hardening on both Linux backends
// when the sandbox is enabled; a kernel without seccomp-filter support
// degrades to a limitation, not an error. macOS ignores it (Seatbelt has
// no seccomp analogue).
DenyDangerousSyscalls bool
// ViolationTag, when set, macOS SBPL deny rules embed `(with message
// <tag>)` so post-run log correlation can attribute denials to this run;
// empty disables tagging.
ViolationTag string
// ReadConfine restricts the child's reads to ReadConfineRoots (the
// worktree/tmp, the OS toolchain/system roots, the $HOME build caches, and
// ReadAllowExtra) instead of the allow-default / read-only-root model, so
// the rest of $HOME and other projects become unreadable. Opt-in — the CLI
// turns it on by default only for the readonly policy. Credential denies
// still stack on top (defense in depth).
ReadConfine bool
// ReadAllowExtra are additional absolute read roots (repeatable
// --read-allow) folded into the read-confinement allow-list.
ReadAllowExtra []string
// GitDirs are absolute git storage paths a linked agent worktree must be
// able to WRITE for `git add`/`git commit` to succeed: the shared object
// store, refs, and reflogs in the clone's git-common-dir, plus the
// per-worktree admin dir (index/HEAD/COMMIT_EDITMSG). A DevStrap agent
// worktree is a git *linked* worktree whose .git lives in the parent clone,
// so without these the child's commit is EPERM'd and `agent pr` has nothing
// to push (P7-SANDBOX-01). The common dir's hooks/ and config are
// deliberately EXCLUDED — granting them would let the child plant a hook or
// config that executes UNSANDBOXED on a later git operation. Also folded
// into the read-confinement allow-list so git reads work under --read-confine.
GitDirs []string
}
SandboxSpec describes the confinement an agent child process should run under. Paths must be absolute; implementations resolve symlinks themselves (macOS Seatbelt matches on real paths, and /tmp, TMPDIR, and cloud-drive roots are routinely symlinks).
type SandboxViolation ¶ added in v0.1.2
type SandboxViolation struct {
Operation string
Path string
Detail string // the raw (unscrubbed) log line; caller scrubs before persist
}
SandboxViolation is one kernel denial parsed from an OS sandbox's audit log.
type SandboxViolationReporter ¶ added in v0.1.2
type SandboxViolationReporter interface {
CollectViolations(ctx context.Context, tag string, since time.Time) ([]SandboxViolation, error)
}
SandboxViolationReporter is implemented by backends that can enumerate the denials a completed run triggered. tag is the SandboxSpec.ViolationTag the run was launched with; since bounds the log query to this run's window.
type ServiceManager ¶
type ServiceManager interface {
Name() string
// DefaultLabel is the OS-idiomatic label used when the caller does not pass
// one (a reverse-DNS launchd label, a bare systemd unit name).
DefaultLabel() string
// Install returns OS-idiomatic advisory notes (e.g. the Linux linger note)
// alongside any error, so the CLI never has to branch on the OS.
Install(ctx context.Context, spec ServiceSpec) (notes []string, err error)
// Uninstall returns OS-idiomatic advisory notes (mirroring Install) so the
// CLI never has to branch on the OS; the unit/plist file is always removed
// even when the OS manager is unreachable (the headless/SSH case).
Uninstall(ctx context.Context, label string) (notes []string, err error)
Status(ctx context.Context, label string) (ServiceStatus, error)
}
type ServiceSpec ¶
type ServiceSpec struct {
Label string
Description string
ExecPath string
Args []string
Env map[string]string
// WorkingDir, when set, is the service's working directory.
WorkingDir string
// StdoutPath/StderrPath are launchd-only log destinations; systemd routes to
// journald and ignores them.
StdoutPath string
StderrPath string
// RestartOnFailure asks the supervisor to relaunch the service when it exits
// with a failure; RestartDelaySeconds throttles the relaunch (0 → adapter
// default of 30s).
RestartOnFailure bool
RestartDelaySeconds int
}
type ServiceStatus ¶
type ServiceStatus struct {
Installed bool
Running bool
Detail string
// UnitPath is the on-disk plist/unit file backing the service.
UnitPath string
// ExecPath is the binary the installed unit/plist launches (best-effort
// parse of our own rendered file; empty when unparseable).
ExecPath string
// ExecPathMissing is true when ExecPath is non-empty and no longer exists
// on disk — the brew-upgrade / deleted-binary case (P7-XP-05).
ExecPathMissing bool
}
type Set ¶
type Set struct {
OS string
Watcher Watcher
Service ServiceManager
Keychain Keychain
Editor EditorAdapter
Sandbox Sandbox
}
type SystemEditor ¶
type SystemEditor struct{}
func (SystemEditor) Name ¶
func (SystemEditor) Name() string
type SystemKeychain ¶
func (SystemKeychain) Delete ¶
func (k SystemKeychain) Delete(_ context.Context, service, account string) error
func (SystemKeychain) Name ¶
func (k SystemKeychain) Name() string
type SystemdUserManager ¶ added in v0.1.2
type SystemdUserManager struct {
UnitDir string
}
SystemdUserManager installs the run-loop as a systemd --user service. A zero-value UnitDir resolves to $XDG_CONFIG_HOME/systemd/user (or ~/.config/systemd/user).
func (SystemdUserManager) DefaultLabel ¶ added in v0.1.2
func (m SystemdUserManager) DefaultLabel() string
func (SystemdUserManager) Install ¶ added in v0.1.2
func (m SystemdUserManager) Install(ctx context.Context, spec ServiceSpec) ([]string, error)
func (SystemdUserManager) Name ¶ added in v0.1.2
func (m SystemdUserManager) Name() string
func (SystemdUserManager) Status ¶ added in v0.1.2
func (m SystemdUserManager) Status(ctx context.Context, label string) (ServiceStatus, error)
type UnsupportedKeychain ¶
func (UnsupportedKeychain) Name ¶
func (k UnsupportedKeychain) Name() string
type UnsupportedSandbox ¶
type UnsupportedSandbox struct {
Platform string
}
UnsupportedSandbox is the explicit no-sandbox placeholder for platforms without an implemented adapter (macOS Seatbelt and Linux bubblewrap-then-landlock are implemented; see spec/14 for remaining slices). Callers treat Available() != nil as "run unconfined and say so".
func (UnsupportedSandbox) Available ¶
func (s UnsupportedSandbox) Available() error
func (UnsupportedSandbox) Command ¶
func (s UnsupportedSandbox) Command(context.Context, SandboxSpec, []string) (SandboxCommand, error)
func (UnsupportedSandbox) Name ¶
func (s UnsupportedSandbox) Name() string
type UnsupportedServiceManager ¶
func (UnsupportedServiceManager) DefaultLabel ¶ added in v0.1.2
func (m UnsupportedServiceManager) DefaultLabel() string
func (UnsupportedServiceManager) Install ¶
func (m UnsupportedServiceManager) Install(context.Context, ServiceSpec) ([]string, error)
func (UnsupportedServiceManager) Name ¶
func (m UnsupportedServiceManager) Name() string
func (UnsupportedServiceManager) Status ¶
func (m UnsupportedServiceManager) Status(context.Context, string) (ServiceStatus, error)
Source Files
¶
- detect_linux.go
- editor.go
- fsnotify_watcher.go
- platform.go
- procstart_linux.go
- runtime.go
- sandbox.go
- sandbox_bwrap_args.go
- sandbox_landlock.go
- sandbox_landlock_args.go
- sandbox_linux.go
- sandbox_linux_chooser.go
- sandbox_paths.go
- sandbox_paths_resolve.go
- sandbox_profile.go
- sandbox_read_confine.go
- sandbox_seccomp_linux.go
- sandbox_seccomp_names.go
- sandbox_violations.go
- service_launchd.go
- service_linux.go
- service_systemd.go