platform

package
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Jul 13, 2026 License: MIT Imports: 30 Imported by: 0

Documentation

Index

Constants

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

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

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

View Source
var (
	ErrUnsupported    = errors.New("platform feature unsupported")
	ErrEditorNotFound = errors.New("editor command not found")
	ErrSecretNotFound = errors.New("platform secret not found")
)
View Source
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

func FirstReadAllowCredentialConflict(userHome, devstrapHome string, readAllow []string) string

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

func ProcessStartTime(pid int) (int64, error)

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 EditorAdapter interface {
	Name() string
	Open(ctx context.Context, dir, editor string) error
}

type FSEvent

type FSEvent struct {
	Kind FSEventKind
	Path string
	At   time.Time
}

type FSEventKind

type FSEventKind string
const (
	FSEventUnknown FSEventKind = "unknown"
	FSEventScan    FSEventKind = "scan"
)

type Keychain

type Keychain interface {
	Name() string
	Store(ctx context.Context, service, account string, secret []byte) error
	Load(ctx context.Context, service, account string) ([]byte, error)
	Delete(ctx context.Context, service, account string) error
}

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

type NativeWatcher struct {
	Debounce   time.Duration
	MaxLatency time.Duration
}

func (NativeWatcher) Name

func (w NativeWatcher) Name() string

func (NativeWatcher) Watch

func (w NativeWatcher) Watch(ctx context.Context, root string, events chan<- FSEvent) error

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

type PollWatcher struct {
	Interval time.Duration
}

func (PollWatcher) Name

func (w PollWatcher) Name() string

func (PollWatcher) Watch

func (w PollWatcher) Watch(ctx context.Context, root string, events chan<- FSEvent) error

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

type RuntimeInfo struct {
	OS   string
	Arch string
}

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
}

func Detect

func Detect() Set

type SystemEditor

type SystemEditor struct{}

func (SystemEditor) Name

func (SystemEditor) Name() string

func (SystemEditor) Open

func (SystemEditor) Open(ctx context.Context, dir, editor string) error

type SystemKeychain

type SystemKeychain struct {
	Platform string
	Target   string
}

func (SystemKeychain) Delete

func (k SystemKeychain) Delete(_ context.Context, service, account string) error

func (SystemKeychain) Load

func (k SystemKeychain) Load(_ context.Context, service, account string) ([]byte, error)

func (SystemKeychain) Name

func (k SystemKeychain) Name() string

func (SystemKeychain) Store

func (k SystemKeychain) Store(_ context.Context, service, account string, secret []byte) error

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)

func (SystemdUserManager) Uninstall added in v0.1.2

func (m SystemdUserManager) Uninstall(ctx context.Context, label string) ([]string, error)

type UnsupportedKeychain

type UnsupportedKeychain struct {
	Platform string
	Target   string
}

func (UnsupportedKeychain) Delete

func (UnsupportedKeychain) Load

func (UnsupportedKeychain) Name

func (k UnsupportedKeychain) Name() string

func (UnsupportedKeychain) Store

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 (UnsupportedSandbox) Name

func (s UnsupportedSandbox) Name() string

type UnsupportedServiceManager

type UnsupportedServiceManager struct {
	Platform string
	Target   string
}

func (UnsupportedServiceManager) DefaultLabel added in v0.1.2

func (m UnsupportedServiceManager) DefaultLabel() string

func (UnsupportedServiceManager) Install

func (UnsupportedServiceManager) Name

func (UnsupportedServiceManager) Status

func (UnsupportedServiceManager) Uninstall

type Watcher

type Watcher interface {
	Name() string
	Watch(ctx context.Context, root string, events chan<- FSEvent) error
}

Jump to

Keyboard shortcuts

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