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
12 changes: 12 additions & 0 deletions cmd/waired-agent/setup_desired.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ type setupDesired struct {
type setupProvider interface {
// setupEngineState reports (installed, ready) for one engine kind.
setupEngineState(ctx context.Context, engine string) (installed, ready bool)
// setupStateDir is the agent's state root, published to the executor
// so a bundled engine lands where this daemon will look for it.
setupStateDir() string
// setupModelState reports one catalog model's lifecycle state plus
// live pull bytes and any stored failure detail.
setupModelState(modelID string) (state string, completed, total int64, errText string)
Expand Down Expand Up @@ -314,6 +317,10 @@ func (r *setupReconciler) SetupState(ctx context.Context) management.SetupStateR

if d.engine != "" {
resp.EngineInstalled, resp.EngineReady = r.provider.setupEngineState(ctx, d.engine)
// Only published alongside a desired engine: it exists to tell
// an executor where to install, and there is nothing to install
// otherwise.
resp.StateDir = r.provider.setupStateDir()
}
return resp
}
Expand Down Expand Up @@ -541,6 +548,11 @@ func (p *agentInferenceProvider) setupEngineState(ctx context.Context, engine st
return true, r
}

// setupStateDir is the agent's state root. The executor installs the
// bundled engine relative to this, so it matches bundledOllamaBin's
// join (inference.go) by construction rather than by coincidence.
func (p *agentInferenceProvider) setupStateDir() string { return p.stateDir }

// setupModelState reports one catalog model's lifecycle state, live
// pull bytes (while downloading) and the stored failure detail.
func (p *agentInferenceProvider) setupModelState(modelID string) (string, int64, int64, string) {
Expand Down
30 changes: 30 additions & 0 deletions cmd/waired-agent/setup_desired_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type fakeSetupProvider struct {
benchStarts []int
pulls []string
pullErr error
stateDir string
}

func (f *fakeSetupProvider) setupEngineState(context.Context, string) (bool, bool) {
Expand All @@ -42,6 +43,12 @@ func (f *fakeSetupProvider) setupEngineState(context.Context, string) (bool, boo
return f.engineInstalled, f.engineReady
}

func (f *fakeSetupProvider) setupStateDir() string {
f.mu.Lock()
defer f.mu.Unlock()
return f.stateDir
}

func (f *fakeSetupProvider) setupModelState(string) (string, int64, int64, string) {
f.mu.Lock()
defer f.mu.Unlock()
Expand Down Expand Up @@ -626,6 +633,29 @@ func TestSetupStateProjection(t *testing.T) {
}
}

// TestSetupStatePublishesStateDir: the executor has no state dir of its
// own on the daemon path (runInitViaDaemon never receives one), and a
// CLI-side guess diverges silently from a daemon started with
// --state-dir. So the daemon declares it (waired#835 §11.1).
func TestSetupStatePublishesStateDir(t *testing.T) {
f := &fakeSetupProvider{stateDir: "/var/lib/waired"}
r, _ := leasedReconciler(t, f, "ollama", "")
if st := r.SetupState(context.Background()); st.StateDir != "/var/lib/waired" {
t.Fatalf("state = %+v, want the provider's state dir published", st)
}
}

// TestSetupStateOmitsStateDirWithoutDesiredEngine: the field exists to
// say where to install, so it has no business being served when nothing
// is to be installed.
func TestSetupStateOmitsStateDirWithoutDesiredEngine(t *testing.T) {
f := &fakeSetupProvider{stateDir: "/var/lib/waired"}
r, _ := leasedReconciler(t, f, "", "m-1")
if st := r.SetupState(context.Background()); st.StateDir != "" {
t.Fatalf("state = %+v, want no state dir without a desired engine", st)
}
}

// TestSetupStateBeforeAnyDesiredFrame: an executor that polls before the
// operator has clicked anything must see active=false rather than an
// error, so it can keep waiting out its grace.
Expand Down
11 changes: 11 additions & 0 deletions cmd/waired/login_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"bufio"
"context"
"encoding/json"
"errors"
"fmt"
Expand Down Expand Up @@ -117,6 +118,16 @@ func runInitViaDaemon(mgmtURL, control, deviceName string, noBrowser, nonInterac
fmt.Fprintf(os.Stderr,
"warn: coding-agent integration had problems (%v); re-run later: waired link --force all\n", err)
}
// §11: on this path init returned long before reaching the
// standalone engine block, so nothing here could ever install
// an engine and the wizard's first step could only report
// permission_denied. As the elevated executor holding the
// lease, do the install the browser just asked for. Blocking
// is correct: the model pull below has nothing to pull with
// until an engine exists.
if setupActive {
runSetupEngineInstall(context.Background(), sess, os.Stdout)
}
// #756: the daemon pulls the bundled model in the background
// after enroll, so the daemon-mediated init used to return while a
// multi-GB download ran invisibly. Block in the foreground with the
Expand Down
19 changes: 19 additions & 0 deletions cmd/waired/setup_executor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ func (d *fakeSetupDaemon) server(t *testing.T) *httptest.Server {
d.requests = append(d.requests, req)
d.state.ExecutorAttached = req.Attached
d.state.ExecutorElevated = req.Attached && req.Elevated
// Mirror the daemon's lease-bound install latch (§11.1) so the
// executor tests see the claim they would see in production.
switch {
case !req.Attached:
d.state.InstallClaimed = ""
case req.Phase == management.SetupExecutorPhaseInstalling && req.Engine != "":
d.state.InstallClaimed = req.Engine
case req.Phase == management.SetupExecutorPhaseDone ||
req.Phase == management.SetupExecutorPhaseFailed:
d.state.InstallClaimed = ""
}
_ = json.NewEncoder(w).Encode(d.state)
})
srv := httptest.NewServer(mux)
Expand All @@ -60,6 +71,14 @@ func (d *fakeSetupDaemon) setActive(active bool) {
d.state.Active = active
}

// setState replaces the served state wholesale, for tests that need to
// script more than the active flag.
func (d *fakeSetupDaemon) setState(st management.SetupStateResponse) {
d.mu.Lock()
defer d.mu.Unlock()
d.state = st
}

func (d *fakeSetupDaemon) noted() []management.SetupExecutorRequest {
d.mu.Lock()
defer d.mu.Unlock()
Expand Down
145 changes: 145 additions & 0 deletions cmd/waired/setup_install.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package main

import (
"context"
"io"
"os"
"runtime"

"github.com/waired-ai/waired-agent/internal/agentconfig"
"github.com/waired-ai/waired-agent/internal/management"
"github.com/waired-ai/waired-agent/internal/platform/elevation"
"github.com/waired-ai/waired-agent/internal/setup"
)

// setupInstallEngine is the install seam so the executor path is
// table-testable without downloading a ~GB engine. It is the same
// per-OS installOllama the interactive path uses (waired#835 §11.1
// requires reuse, not a second installer).
var setupInstallEngine = installOllama

// setupDetectEngine is the detection seam, for the same reason.
var setupDetectEngine = setup.DetectOllama

// setupHandState is the ownership-handoff seam. The real one shells out
// to chown and self-guards on euid 0 + an installed service, which a
// test running as root on a developer box would actually satisfy.
var setupHandState = handStateToServiceUser

// runSetupEngineInstall performs the engine install the browser wizard
// asked for, as the elevated executor holding the lease.
//
// This is the daemon-path counterpart of ensureBundledEngine
// (init_engine.go): on the daemon path `waired init` returns early at
// main.go's runInitViaDaemon branch and never reaches the standalone
// engine block, so without this the wizard's first step could only ever
// report permission_denied. The decision itself goes through the SAME
// engineInstallDecision as interactive init, so opt-out, already-present,
// reuse and not-elevated all resolve identically (§11.1).
//
// It never returns an error: the outcome is reported to the daemon,
// which is what NAVI renders. Like ensureBundledEngine, a failure here
// must not fail login.
func runSetupEngineInstall(ctx context.Context, s *executorSession, out io.Writer) {
setupEngineInstall(ctx, s, out, runtime.GOOS, elevation.IsElevated())
}

// setupEngineInstall is runSetupEngineInstall with the two host facts
// that vary by OS passed in, so the whole decision tree is table-testable
// on every OS from an unprivileged CI runner (repo rule: route
// GOOS-varying decisions through a function taking runtime.GOOS).
func setupEngineInstall(ctx context.Context, s *executorSession, out io.Writer, goos string, elevated bool) {
if !s.Supported() {
return
}
st := s.State()
if !st.Active || st.DesiredEngine == "" || st.EngineInstalled {
return
}
// vllm has its own installer with a different shape (a venv, not a
// tarball) and no wizard offers it yet; leave it to the daemon's
// existing reporting rather than half-supporting it here.
if st.DesiredEngine != "ollama" {
return
}
// A live lease already claimed this install. The claim is bound to
// the lease (§11.1), so a stale one cannot be here — whoever holds
// it is alive and working.
if st.InstallClaimed != "" {
return
}
// The daemon could not tell us where to install. Guessing would risk
// installing somewhere this daemon never looks, which presents to the
// operator as an install that "worked" and a step that never turns
// green.
if st.StateDir == "" {
s.Failed(st.DesiredEngine, "the background service did not report where to install the engine")
return
}

claimed := s.Installing(st.DesiredEngine)
if claimed.InstallClaimed != "" && claimed.InstallClaimed != st.DesiredEngine {
// Another executor got there first with a different engine.
return
}

bundledPresent := false
if p := bundledEnginePath(goos, st.StateDir); p != "" {
if fi, err := os.Stat(p); err == nil && fi.Mode().IsRegular() {
bundledPresent = true
}
}
// OllamaSourceBundled, not the interactive prompt's answer: there is
// no terminal question on this path, and we only get here when the
// daemon reports no engine installed at all — so there is nothing to
// reuse. A host that already has one never reaches this line.
action := engineInstallDecision(
goos, elevated, setupDetectEngine(ctx),
agentconfig.OllamaSourceBundled, bundledPresent,
os.Getenv("WAIRED_NO_OLLAMA") != "")

switch action {
case engineActionInstall:
writePromptf(out, "%s Installing the AI engine for the setup in your browser (one-time download)...\n", emo("📦", ">>"))
if err := setupInstallEngine(true, st.StateDir); err != nil {
writePromptf(out, "%s Engine install failed: %v\n", emo("⚠️", "!"), err)
s.Failed(st.DesiredEngine, err.Error())
return
}
// The tarball was extracted as root; hand the state dir back or
// the unprivileged daemon cannot read what we just installed
// (Linux only, no-op elsewhere).
setupHandState(st.StateDir)
writePromptf(out, "%s AI engine installed.\n", emo("✅", "*"))
s.Done(st.DesiredEngine)

case engineActionSkipPresent, engineActionSkipReuse:
// Nothing to install. Report done so the wizard advances instead
// of waiting on the daemon's next profile refresh.
s.Done(st.DesiredEngine)

case engineActionSkipNotElevated:
// The daemon already reports permission_denied for an unelevated
// lease; say it in the executor's own words so error_detail names
// the command that fixes it.
s.Failed(st.DesiredEngine,
"the setup command on this device is not running with administrator privileges; "+
elevation.Hint("waired init"))

case engineActionSkipOptOut:
// Engine installs are turned off on this host, but someone just
// asked for one in the browser. permission_denied is the closest
// of the eight codes ("this device will not do it"); the detail
// carries the real reason (waired#835 decisions 20260720 13:00).
writePrompt(out, "Engine install skipped (WAIRED_NO_OLLAMA).")
s.Failed(st.DesiredEngine,
"engine installs are turned off on this device (WAIRED_NO_OLLAMA)")
}
}

// setupEngineInstallWanted reports whether the daemon's state calls for
// an executor-driven engine install. Split out so the caller can decide
// without a second round trip's worth of duplicated conditions.
func setupEngineInstallWanted(st management.SetupStateResponse) bool {
return st.Active && st.DesiredEngine != "" && !st.EngineInstalled && st.InstallClaimed == ""
}
Loading
Loading