Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ permissions:
pull-requests: read

env:
DAGGER_VERSION: "0.20.6"
DAGGER_VERSION: "0.20.8"

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
Expand Down
63 changes: 45 additions & 18 deletions agentd/agentd.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ type Daemon struct {
debug bool

// Sandbox configuration.
noSandbox bool // skip the gVisor/nix runtime entirely
runscPath string // path to the runsc binary
sandboxStateDir string // --root flag for runsc
sandboxBundleDir string // base directory for OCI bundles
Expand Down Expand Up @@ -139,6 +140,14 @@ func NewDaemon(configPath string) *Daemon {
}
}

// SetNoSandbox disables the gVisor sandbox runtime. When set, agentd skips the
// runsc/nix startup checks and runs without a sandbox runner; sandboxed agents
// then fail to launch (native/tmux agents and the API still work). Intended for
// environments where gVisor is unavailable (e.g. a minimal capstan VM).
func (d *Daemon) SetNoSandbox(noSandbox bool) {
d.noSandbox = noSandbox
}

// SetRunscPath overrides the auto-detected runsc binary path.
func (d *Daemon) SetRunscPath(path string) {
d.runscPath = path
Expand Down Expand Up @@ -225,25 +234,34 @@ func (d *Daemon) AgentStatuses() []api.AgentStatus {
func (d *Daemon) Run(ctx context.Context) error {
log.Println("agentd: initializing agent manager")

// 1. Verify required binaries. agentd runs on stereOS (NixOS) and
// requires both runsc (gVisor) and nix to be available.
if _, err := exec.LookPath("nix"); err != nil {
return fmt.Errorf("nix not found in PATH: %w", err)
}
// 1. Initialize the sandbox runtime, unless disabled. gVisor (runsc) and
// nix are only needed for sandboxed agents; --no-sandbox skips both so
// agentd can run where they're unavailable (e.g. a minimal capstan VM).
// Sandboxed agents then fail to launch (guarded below); native/tmux agents
// and the API still work.
if d.noSandbox {
log.Println("agentd: sandbox runtime disabled (--no-sandbox); sandboxed agents will not launch")
} else {
// Verify required binaries. agentd runs on stereOS (NixOS) and
// requires both runsc (gVisor) and nix to be available.
if _, err := exec.LookPath("nix"); err != nil {
return fmt.Errorf("nix not found in PATH: %w", err)
}

// Initialize sandbox runner. gVisor (runsc) is required — agentd
// cannot start without it since sandboxed is the default agent type.
runner, err := sandbox.NewRunner(d.runscPath, d.sandboxStateDir)
if err != nil {
return fmt.Errorf("sandbox runtime unavailable: %w", err)
}
d.runner = runner
d.runner.Debug = d.debug
log.Printf("agentd: sandbox runtime initialized (runsc=%s, state=%s)", runner.RunscPath, d.sandboxStateDir)
// Initialize sandbox runner. gVisor (runsc) is required — agentd
// cannot start without it since sandboxed is the default agent type.
runner, err := sandbox.NewRunner(d.runscPath, d.sandboxStateDir)
if err != nil {
return fmt.Errorf("sandbox runtime unavailable: %w", err)
}
d.runner = runner
d.runner.Debug = d.debug
log.Printf("agentd: sandbox runtime initialized (runsc=%s, state=%s)", runner.RunscPath, d.sandboxStateDir)

// Clean up any orphaned containers from a previous crash.
if err := d.runner.Cleanup(ctx); err != nil {
log.Printf("agentd: warning: sandbox cleanup: %v", err)
// Clean up any orphaned containers from a previous crash.
if err := d.runner.Cleanup(ctx); err != nil {
log.Printf("agentd: warning: sandbox cleanup: %v", err)
}
}

// 2. Start tmux server.
Expand All @@ -253,7 +271,16 @@ func (d *Daemon) Run(ctx context.Context) error {
log.Printf("agentd: starting tmux server (run-as=%s)", AgentUser)
d.tmux = tmux.NewServerAs(d.tmuxSocketPath, AgentUser)
if err := d.tmux.Start(); err != nil {
return fmt.Errorf("starting tmux server: %w", err)
if d.noSandbox {
// Non-fatal: agentd still serves its API and status. Native (tmux)
// agents need a working tmux server + the agent user, so they cannot
// launch until that's available. This lets agentd run in a minimal
// environment (e.g. a capstan VM with no sudo/agent user) for
// control-plane bring-up.
log.Printf("agentd: warning: tmux server unavailable (%v); native agents cannot launch", err)
} else {
return fmt.Errorf("starting tmux server: %w", err)
}
}
Comment on lines 273 to 284

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unconditional tmux non-fatal change affects all deployments

The tmux startup failure is now silently downgraded for every agentd invocation, not only when --no-sandbox is active. In a standard stereOS deployment where type = "native" agents are configured, a broken agent user or missing tmux binary no longer causes a visible startup failure — agentd starts, the reconcile loop runs, and every native agent logs a per-cycle "error starting agent" error indefinitely, while the root cause (tmux never started) is only printed once at boot. Previously this was a hard startup failure via return fmt.Errorf(...).

Consider gating the non-fatal path on d.noSandbox, so standard sandboxed deployments retain the original fail-fast behavior and only the capstan control-plane path is lenient.

Suggested change
if err := d.tmux.Start(); err != nil {
return fmt.Errorf("starting tmux server: %w", err)
// Non-fatal: agentd still serves its API and status. Native (tmux)
// agents need a working tmux server + the agent user, so they cannot
// launch until that's available; sandboxed agents are unaffected. This
// lets agentd run in a minimal environment (e.g. a capstan VM with no
// sudo/agent user) for control-plane bring-up.
log.Printf("agentd: warning: tmux server unavailable (%v); native agents cannot launch", err)
}
if err := d.tmux.Start(); err != nil {
if d.noSandbox {
// Non-fatal: agentd still serves its API and status. Native (tmux)
// agents need a working tmux server + the agent user, so they cannot
// launch until that's available; sandboxed agents are unaffected. This
// lets agentd run in a minimal environment (e.g. a capstan VM with no
// sudo/agent user) for control-plane bring-up.
log.Printf("agentd: warning: tmux server unavailable (%v); native agents cannot launch", err)
} else {
return fmt.Errorf("starting tmux server: %w", err)
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: agentd/agentd.go
Line: 273-280

Comment:
**Unconditional tmux non-fatal change affects all deployments**

The tmux startup failure is now silently downgraded for every agentd invocation, not only when `--no-sandbox` is active. In a standard stereOS deployment where `type = "native"` agents are configured, a broken agent user or missing tmux binary no longer causes a visible startup failure — agentd starts, the reconcile loop runs, and every native agent logs a per-cycle `"error starting agent"` error indefinitely, while the root cause (tmux never started) is only printed once at boot. Previously this was a hard startup failure via `return fmt.Errorf(...)`.

Consider gating the non-fatal path on `d.noSandbox`, so standard sandboxed deployments retain the original fail-fast behavior and only the capstan control-plane path is lenient.

```suggestion
	if err := d.tmux.Start(); err != nil {
		if d.noSandbox {
			// Non-fatal: agentd still serves its API and status. Native (tmux)
			// agents need a working tmux server + the agent user, so they cannot
			// launch until that's available; sandboxed agents are unaffected. This
			// lets agentd run in a minimal environment (e.g. a capstan VM with no
			// sudo/agent user) for control-plane bring-up.
			log.Printf("agentd: warning: tmux server unavailable (%v); native agents cannot launch", err)
		} else {
			return fmt.Errorf("starting tmux server: %w", err)
		}
	}
```

How can I resolve this? If you propose a fix, please make it concise.

defer func() {
log.Println("agentd: stopping tmux server")
Expand Down
7 changes: 7 additions & 0 deletions agentd/agentd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,13 @@ var _ = Describe("Daemon", func() {
})
})

Describe("SetNoSandbox", func() {
It("should not panic when disabling the sandbox runtime", func() {
d := agentd.NewDaemon("")
Expect(func() { d.SetNoSandbox(true) }).NotTo(Panic())
})
})

Describe("SetLaunchConcurrency", func() {
It("should not panic when setting concurrency", func() {
d := agentd.NewDaemon("")
Expand Down
4 changes: 4 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func main() {
runscPath := flag.String("runsc-path", "", "path to runsc binary (auto-detected if empty)")
sandboxStateDir := flag.String("sandbox-state-dir", agentd.DefaultSandboxStateDir, "root directory for runsc container state")
sandboxBundleDir := flag.String("sandbox-bundle-dir", agentd.DefaultSandboxBundleDir, "base directory for OCI bundles")
noSandbox := flag.Bool("no-sandbox", false, "disable the gVisor sandbox runtime (skips runsc/nix; sandboxed agents won't launch)")
debug := flag.Bool("debug", false, "enable debug logging (logs commands, env keys, captures pane output on exit)")
flag.Parse()

Expand Down Expand Up @@ -60,6 +61,9 @@ func main() {
if *sandboxBundleDir != agentd.DefaultSandboxBundleDir {
daemon.SetSandboxBundleDir(*sandboxBundleDir)
}
if *noSandbox {
daemon.SetNoSandbox(true)
}
if *debug {
daemon.SetDebug(true)
}
Expand Down
Loading