The supervisor drops to the run_as user (default llmgw-engine) when the agent is root: its credential and groups, a 0700 home under the work directory as the working directory, HOME and XDG_CACHE_HOME there. A node without the user keeps engines as root and logs why. node add creates the user in the video and render groups. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
93 lines
3.2 KiB
Go
93 lines
3.2 KiB
Go
package supervisor
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/user"
|
|
"path/filepath"
|
|
"strconv"
|
|
"syscall"
|
|
)
|
|
|
|
// Engines run as their own user, not as the agent's root: an engine reads a
|
|
// model's files and answers requests, and a model's files are whatever a
|
|
// registry published. Root in that process would be root on the node for
|
|
// anything that can make an engine misbehave — a malformed file, a template,
|
|
// a flag nobody thought of.
|
|
//
|
|
// The user is DefaultRunAs, made by `llmgw node add`, in the video and render
|
|
// groups for the GPU devices. A node without it keeps running engines as
|
|
// root, and says so, so an existing fleet is not broken by an upgrade.
|
|
|
|
// DefaultRunAs is the user engines run as.
|
|
const DefaultRunAs = "llmgw-engine"
|
|
|
|
// runAs is how an engine process is started as another user: its
|
|
// credential, a working directory it owns, and the environment that points
|
|
// its caches there.
|
|
type runAs struct {
|
|
name string
|
|
cred *syscall.Credential
|
|
home string
|
|
}
|
|
|
|
// lookupUser is user.Lookup, replaced by tests.
|
|
var lookupUser = user.Lookup
|
|
|
|
// resolveRunAs finds the user engines run as. nil means as this process —
|
|
// when it is not root, there is nobody to drop to; when the user does not
|
|
// exist, why is returned.
|
|
func resolveRunAs(name, workDir string) (*runAs, string) {
|
|
if name == "" || name == "root" || os.Geteuid() != 0 {
|
|
return nil, ""
|
|
}
|
|
u, err := lookupUser(name)
|
|
if err != nil {
|
|
return nil, fmt.Sprintf("there is no user %s, so engines run as root; `llmgw node add` makes it, "+
|
|
"or: useradd --system --shell /usr/sbin/nologin %s && usermod -aG video,render %s", name, name, name)
|
|
}
|
|
r, err := credentialFor(u, workDir)
|
|
if err != nil {
|
|
return nil, fmt.Sprintf("engines run as root: %v", err)
|
|
}
|
|
return r, ""
|
|
}
|
|
|
|
func credentialFor(u *user.User, workDir string) (*runAs, error) {
|
|
uid, err1 := strconv.ParseUint(u.Uid, 10, 32)
|
|
gid, err2 := strconv.ParseUint(u.Gid, 10, 32)
|
|
if err1 != nil || err2 != nil {
|
|
return nil, fmt.Errorf("user %s has ids %s:%s", u.Username, u.Uid, u.Gid)
|
|
}
|
|
var groups []uint32
|
|
if ids, err := u.GroupIds(); err == nil {
|
|
for _, g := range ids {
|
|
if n, err := strconv.ParseUint(g, 10, 32); err == nil {
|
|
groups = append(groups, uint32(n))
|
|
}
|
|
}
|
|
}
|
|
// A home of its own under the work directory, which is root's: caches
|
|
// (Triton, torch, Hugging Face) are written there and nowhere else.
|
|
home := filepath.Join(workDir, "engine-home")
|
|
if err := os.MkdirAll(home, 0o700); err != nil {
|
|
return nil, fmt.Errorf("making %s: %w", home, err)
|
|
}
|
|
if err := os.Chown(home, int(uid), int(gid)); err != nil {
|
|
return nil, fmt.Errorf("giving %s to %s: %w", home, u.Username, err)
|
|
}
|
|
// The work directory is 0750 root's; the engine has to pass through it
|
|
// to its home.
|
|
if err := os.Chmod(workDir, 0o751); err != nil {
|
|
return nil, err
|
|
}
|
|
return &runAs{name: u.Username, home: home,
|
|
cred: &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid), Groups: groups}}, nil
|
|
}
|
|
|
|
// env is what is added to an engine's environment when it runs as r: the
|
|
// last value of a key wins in exec.
|
|
func (r *runAs) env() []string {
|
|
return []string{"HOME=" + r.home, "XDG_CACHE_HOME=" + filepath.Join(r.home, ".cache"), "USER=" + r.name, "LOGNAME=" + r.name}
|
|
}
|