Documentation
¶
Overview ¶
Package moatinit implements the container entrypoint as testable Go.
It is the Go port of internal/deps/scripts/moat-init.sh, with behavioral parity as the contract: same phase ordering, same fail-closed vs best-effort classification per operation, and verbatim user-facing error wording. The catalog of ported requirements lives in docs/plans/2026-07-01-moat-init-go-rewrite-plan.md (Appendix A/B).
The package moves the entrypoint's business logic — env parsing, branch and mode selection, phase ordering, error classification, argument and config assembly, exclude computation, root/moatuser detection — into unit-testable Go. Mechanical, security-sensitive, or well-understood system operations stay delegated to the audited tools already in the base image, invoked as targeted subprocesses:
- gosu — the privilege drop (Go selects the branch, gosu transitions)
- socat — the SSH agent TCP↔unix bridge (long-lived child)
- tar — the workspace volume byte copy (Go owns excludes/args/rc checks)
All identity, filesystem, subprocess, and DNS operations go through the Sys seam so phases can be exercised under `go test` against an injected temp root with no container and no root privileges.
The compiled binary (cmd/moat-init) is embedded into the moat host binary by internal/initbin and shipped into run images as /usr/local/bin/moat-init (the container ENTRYPOINT) by writeEntrypoint.
Index ¶
- func Plan(ctx *Context) []string
- func Run(ctx *Context) int
- type Cmd
- type Config
- type Context
- type OSSys
- func (s *OSSys) AppendFile(path string, data []byte) error
- func (s *OSSys) Chmod(path string, perm fs.FileMode) error
- func (s *OSSys) Chown(path string, uid, gid int) error
- func (s *OSSys) CopyFilePreserving(src, dst string) error
- func (s *OSSys) CopyTreePreserving(src, dst string) error
- func (s *OSSys) Environ() []string
- func (s *OSSys) Exec(argv []string, env []string) error
- func (s *OSSys) Getenv(key string) string
- func (s *OSSys) Geteuid() int
- func (s *OSSys) Getpid() int
- func (s *OSSys) Lchown(path string, uid, gid int) error
- func (s *OSSys) LookPath(file string) (string, error)
- func (s *OSSys) LookupGroupByGID(gid string) (string, bool)
- func (s *OSSys) LookupGroupByName(name string) (string, bool)
- func (s *OSSys) LookupUser(name string) (User, bool)
- func (s *OSSys) MkdirAll(path string, perm fs.FileMode) error
- func (s *OSSys) Pipe(src, dst Cmd) (int, int, error)
- func (s *OSSys) ProcessAlive(pid int) bool
- func (s *OSSys) ReadFile(path string) ([]byte, error)
- func (s *OSSys) RealPath(path string) string
- func (s *OSSys) Remove(path string) error
- func (s *OSSys) ResolveAnyFirst(host string) string
- func (s *OSSys) ResolveIPv4First(host string) string
- func (s *OSSys) Run(c Cmd) (int, error)
- func (s *OSSys) Setenv(key, value string)
- func (s *OSSys) Sleep(d time.Duration)
- func (s *OSSys) StartDetached(c Cmd) (int, error)
- func (s *OSSys) Stat(path string) (fs.FileInfo, error)
- func (s *OSSys) Unsetenv(key string)
- func (s *OSSys) WalkDir(root string, fn fs.WalkDirFunc) error
- func (s *OSSys) WriteFile(path string, data []byte, perm fs.FileMode) error
- type Phase
- type Sys
- type User
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Plan ¶
Plan returns the ordered actions the entrypoint would take for the current environment and identity, without performing any of them — a side-effect-free dry-run (only read-only stats and lookups). It is the debugging affordance the shell script could never offer, and the release pipeline's positive functional gate: a regenerated-but-defective binary that lost the privilege-drop or scrub phases fails the gate regardless of its checksum.
One line per decision, "<phase>: <action>". Wording here is NOT part of the parity contract (the script has no equivalent); the fatal stderr wordings pinned by the golden tests are.
func Run ¶
Run executes all phases in order. On success it never returns: the final phase replaces the process image via Sys.Exec. It returns the process exit code on failure.
Fail-closed: reaching the end of the phase list means the exec dispatch did not run (it can only be skipped in an incomplete build), and starting the user command without the privilege-drop contract would silently run it as root — so refuse instead.
Types ¶
type Cmd ¶
type Cmd struct {
Argv []string
Dir string // working directory ("" = inherit)
Env []string // nil = inherit the current process environment
Stdin io.Reader // nil = /dev/null
Stdout io.Writer // nil = discard
Stderr io.Writer // nil = discard
// LogFile, when set, redirects both stdout and stderr to this file
// (container-absolute; re-rooted like other fs paths), truncating it —
// the Go form of `>/var/log/dockerd.log 2>&1`. Overrides Stdout/Stderr.
LogFile string
// Timeout, when positive, bounds the run so a hanging probe inside a
// bounded retry loop cannot consume the loop's whole budget (plan
// Appendix B: per-attempt timeout below the loop budget). A timed-out
// command reports a non-zero exit code.
Timeout time.Duration
}
Cmd describes a subprocess invocation.
type Config ¶
type Config struct {
ExtraHosts string // MOAT_EXTRA_HOSTS: space-separated name:target pairs for /etc/hosts
SSHTCPAddr string // MOAT_SSH_TCP_ADDR: TCP address of the host-side SSH agent proxy
ClaudeInit string // MOAT_CLAUDE_INIT: staging dir for ~/.claude
CodexInit string // MOAT_CODEX_INIT: staging dir for ~/.codex
// CodexSubscriptionAuth gates the Codex CLI version check: when "1", the
// staged auth.json is synthetic and only a verified Codex range can read it.
CodexSubscriptionAuth string // MOAT_CODEX_SUBSCRIPTION_AUTH
GeminiInit string // MOAT_GEMINI_INIT: staging dir for ~/.gemini
CopilotInit string // MOAT_COPILOT_INIT: staging dir for ~/.copilot
InitFiles string // MOAT_INIT_FILES: tab-delimited <path>\t<base64> records
Clipboard string // MOAT_CLIPBOARD: "1" starts Xvfb :99 and exports DISPLAY
GitUserName string // MOAT_GIT_USER_NAME
GitUserEmail string // MOAT_GIT_USER_EMAIL
GitSSHGitHub string // MOAT_GIT_SSH_GITHUB: "1" sets the github.com insteadOf rewrite
DockerDIND string // MOAT_DOCKER_DIND: "1" starts dockerd (dind mode)
DockerGID string // MOAT_DOCKER_GID: non-empty enables host-socket group mode
WorkspaceVolume string // MOAT_WORKSPACE_VOLUME: "1" populates /workspace from staging
WorkspaceStaging string // MOAT_WORKSPACE_STAGING: staging source (default /mnt/host-workspace)
WorkspaceExcludes string // MOAT_WORKSPACE_EXCLUDES: newline-delimited ./-prefixed patterns
VolumeChown string // MOAT_VOLUME_CHOWN: space-separated named-volume mount roots
PreRun string // MOAT_PRE_RUN: hook command run in /workspace before the main command
Home string // HOME of the entrypoint process (target home on the non-root path)
}
Config is the entrypoint's environment contract, read once at startup and frozen — no MOAT_* control variable is re-read after a user-controlled phase (e.g. the pre_run hook) has executed.
One deliberate exception mirrors the script: MOAT_INIT_FILES is also removed from the live process environment right after the init-files phase (the script's `unset MOAT_INIT_FILES`), so children spawned by later phases never see it.
func LoadConfig ¶
LoadConfig snapshots the entrypoint environment from sys.
type Context ¶
type Context struct {
Sys Sys
Cfg *Config
Argv []string // the user command (the entrypoint's "$@")
Stdin io.Reader // inherited by interactive children (pre_run hook)
Stdout io.Writer // inherited by children that surface output (pre_run hook)
Stderr io.Writer
}
Context carries the frozen configuration, the Sys seam, the user command, and the stderr stream through every phase.
type OSSys ¶
type OSSys struct {
Root string
// resolveTimeout bounds each DNS lookup attempt so a retry loop's total
// budget is honored even when the resolver hangs (plan Appendix B:
// per-attempt timeout must stay below the loop budget).
ResolveTimeout time.Duration
}
OSSys is the production Sys backed by the real operating system. Root, when non-empty, re-roots all filesystem paths beneath it (used by integration tests to run phases against a temp directory).
func (*OSSys) CopyFilePreserving ¶
CopyFilePreserving mirrors `cp -p src dst`: bytes, mode, and timestamps are preserved (failure is an error); ownership preservation is attempted but, like cp -p without appropriate privileges, its failure is not an error.
func (*OSSys) CopyTreePreserving ¶
CopyTreePreserving mirrors `cp -rp src dst` where dst is the destination path of the copied tree (POSIX -R semantics: symlinks are duplicated as symlinks, never followed).
func (*OSSys) Pipe ¶
Pipe runs `src | dst` and returns both exit codes, mirroring the script's capture of the source tar's status alongside the destination's ($? after a POSIX pipeline only reports the rightmost command).
func (*OSSys) ProcessAlive ¶
func (*OSSys) ResolveAnyFirst ¶
func (*OSSys) ResolveIPv4First ¶
func (*OSSys) StartDetached ¶
StartDetached launches a long-lived background child, exactly like the shell's `cmd ... &`: the child shares the entrypoint's process group (job control is off in non-interactive sh, so backgrounded children are NOT re-grouped — parity requires the same signal delivery the shell had), and it survives the entrypoint's exec because exec replaces the process image without touching children. A reaper goroutine collects the child if it dies before the handoff, so ProcessAlive mirrors the shell's `kill -0` on a reaped background job instead of seeing a signalable zombie.
type Phase ¶
Phase is one ordered step of the entrypoint. Phases return nil to continue, or an exitError to abort with that code. Any other error is a programming bug and is reported as a generic fatal.
type Sys ¶
type Sys interface {
// Identity. Lookups use pure-Go os/user (files NSS: /etc/passwd,
// /etc/group) — parity with the script's `id`/`getent` for the standard
// moat images; documented divergence for LDAP/SSSD-backed custom images
// (detection only — the privilege drop itself is delegated to gosu).
Geteuid() int
LookupUser(name string) (User, bool)
LookupGroupByName(name string) (gid string, ok bool)
LookupGroupByGID(gid string) (name string, ok bool)
// Process environment. Mirrors the shell's export/unset so that children
// spawned later (pre_run hook, gosu, the exec'd command) inherit exactly
// what they would have under the script.
Getenv(key string) string
Setenv(key, value string)
Unsetenv(key string)
Environ() []string
// Filesystem.
Stat(path string) (fs.FileInfo, error)
MkdirAll(path string, perm fs.FileMode) error
Chmod(path string, perm fs.FileMode) error
Chown(path string, uid, gid int) error
Lchown(path string, uid, gid int) error
ReadFile(path string) ([]byte, error)
WriteFile(path string, data []byte, perm fs.FileMode) error
AppendFile(path string, data []byte) error
Remove(path string) error
CopyFilePreserving(src, dst string) error // cp -p
CopyTreePreserving(src, dst string) error // cp -rp
WalkDir(root string, fn fs.WalkDirFunc) error
Getpid() int
// RealPath maps a container-absolute path to the path a SUBPROCESS must
// use to reach it. In production this is the identity; under an
// injected test root it prefixes the root, so phases that hand paths to
// tar/git/socat (argv or working directory) stay testable against the
// same tree the filesystem methods manipulate.
RealPath(path string) string
// Subprocesses.
LookPath(file string) (string, error)
Run(c Cmd) (exitCode int, err error)
StartDetached(c Cmd) (pid int, err error)
ProcessAlive(pid int) bool
Pipe(src, dst Cmd) (srcExit, dstExit int, err error)
// DNS. ResolveIPv4First is the port of `getent ahostsv4 | awk '{print
// $1; exit}'`; ResolveAnyFirst of the `getent hosts` fallback. Both
// consult /etc/hosts then DNS (Go resolver) and return "" on failure.
ResolveIPv4First(host string) string
ResolveAnyFirst(host string) string
Sleep(d time.Duration)
// Exec replaces the process image (syscall.Exec). It only returns on
// error. argv[0] is resolved via PATH when not absolute.
Exec(argv []string, env []string) error
}
Sys abstracts every identity, filesystem, subprocess, and DNS operation the entrypoint performs, so phases can be exercised under `go test` against an injected temp root with no container and no root privileges. OSSys is the production implementation; tests embed *OSSys (pointed at a t.TempDir() root) and shadow the identity/subprocess/DNS methods they need to fake.
Filesystem methods take absolute container paths (/etc/hosts, /home/moatuser/...); OSSys re-roots them under Root when set.