The engine judges its node seat's hand-over and take as it judges the controller's ask, answers root-free for its own machine, and asks a library's take itself once (issue 774, ADR 0563 §3, §4, §6)
This commit is contained in:
@@ -0,0 +1,277 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
meshasker "git.novox.be/novox/mesh-sdk/go/asker"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"github.com/novox/mesh-host/internal/accounts"
|
||||
"github.com/novox/mesh-host/internal/apply"
|
||||
"github.com/novox/mesh-host/internal/link"
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
)
|
||||
|
||||
// The node seat node-engine's judgements on this machine (novox/hq ADR 0563): what a hand-over and a library's take
|
||||
// would set and what the disk holds now, and whether this machine is root-free.
|
||||
|
||||
// diskOf is what is on disk at a directory, as an act compares it when the operator's yes arrives: its owner and
|
||||
// group, its mode, and which file it is.
|
||||
func diskOf(info os.FileInfo) string {
|
||||
owner := "unknown"
|
||||
if uid, gid, ok := apply.OwnerOf(info); ok {
|
||||
owner = fmt.Sprintf("%d:%d", uid, gid)
|
||||
}
|
||||
which := ""
|
||||
if st, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||
which = fmt.Sprintf(" %v:%v", st.Dev, st.Ino)
|
||||
}
|
||||
return fmt.Sprintf("%s %o%s", owner, info.Mode().Perm(), which)
|
||||
}
|
||||
|
||||
// handOverJudgeFrom judges a hand-over through the node seat as the controller's ask is judged (judgeFound): the
|
||||
// declared owner and mode it will set, and the disk as it is.
|
||||
func handOverJudgeFrom(statePath string) func(string) (link.Judged, error) {
|
||||
return func(path string) (link.Judged, error) {
|
||||
found, info, err := judgeFound(statePath, path)
|
||||
if err != nil {
|
||||
return link.Judged{}, err
|
||||
}
|
||||
return link.Judged{Subject: path, Path: path, Sets: orUnsaid(found.Declared), Disk: diskOf(info),
|
||||
Words: "A directory of " + moduleOf(found.ID) + " is used as found: the mesh left its owner and mode."}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func moduleOf(id string) string {
|
||||
if at := strings.LastIndex(id, "."); at > 0 {
|
||||
return id[:at]
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// accessJudgeFrom judges the take act on a library through the node seat (ADR 0328 §7, ADR 0563 §4): declared on this
|
||||
// machine, its home, and its root holding data the mesh has not taken; what the take sets, and the disk as it is.
|
||||
func accessJudgeFrom(statePath string) func(string, string) (link.Judged, error) {
|
||||
return func(act, access string) (link.Judged, error) {
|
||||
d, err := declaredNow(statePath, nil)
|
||||
if errors.Is(err, store.ErrNothingDeclared) {
|
||||
return link.Judged{}, errors.New("this node keeps no declaration, so it is declared no library")
|
||||
}
|
||||
if err != nil {
|
||||
return link.Judged{}, fmt.Errorf("what runs on this node does not read: %w", err)
|
||||
}
|
||||
for _, l := range d.Libraries {
|
||||
if apply.LibraryAccess(l.Library) != access {
|
||||
continue
|
||||
}
|
||||
if !l.Home {
|
||||
return link.Judged{}, fmt.Errorf("this machine is not the home of the library %s, and a library is "+
|
||||
"taken on its home", l.Library)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), lookBound)
|
||||
defer cancel()
|
||||
if v := apply.JudgeLibrary(ctx, l); v.Kind != apply.VerdictWaitKeep {
|
||||
return link.Judged{}, fmt.Errorf("the library %s does not wait for the operator's word to be kept "+
|
||||
"(it is judged %s), so there is nothing to take", l.Library, v.Kind)
|
||||
}
|
||||
info, err := os.Lstat(l.Root)
|
||||
if err != nil {
|
||||
return link.Judged{}, fmt.Errorf("the library %s's root cannot be read: %v", l.Library, err)
|
||||
}
|
||||
return link.Judged{Subject: access, Path: filepath.Clean(l.Root),
|
||||
Sets: "the group " + l.Group + ", group write and setgid on everything under its root",
|
||||
Disk: diskOf(info), Words: "The library " + l.Library + " holds data the mesh has not taken."}, nil
|
||||
}
|
||||
return link.Judged{}, fmt.Errorf("the access %s names no library declared on this machine", access)
|
||||
}
|
||||
}
|
||||
|
||||
// askTakes asks the take of every library the look found waiting for the operator's word, once per library (ADR 0563
|
||||
// §4): asked by the engine itself, never a second time while one is open or after one was asked; access-act asks again
|
||||
// by hand.
|
||||
func askTakes(acts *link.HandActs, waiting []string, say func(string)) {
|
||||
if acts == nil {
|
||||
return
|
||||
}
|
||||
for _, access := range waiting {
|
||||
if acts.AskedFor(link.ActAccess, access) {
|
||||
continue
|
||||
}
|
||||
if _, err := acts.AskAccess(link.EngineSeat+"-"+acts.Node, link.ActTake, access,
|
||||
"its root holds data outside its group", true); err != nil && say != nil {
|
||||
say("the take of " + access + " could not be asked: " + err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// lastAccounts is the newest look at every account (the health loop's), which root-free judges from.
|
||||
var lastAccounts atomic.Pointer[accounts.Statement]
|
||||
|
||||
// rootFreshFor is how old the verdict root-free judges from may be (ADR 0563 §6, the controller's verdictFreshFor).
|
||||
const rootFreshFor = 15 * time.Minute
|
||||
|
||||
// judgeRootFree is this machine's root-free (ADR 0259 §8, ADR 0563 §6): free only on the engine's own verdict, at most
|
||||
// rootFreshFor old, that every account declared never to become root without a person cannot, at least one such
|
||||
// account declared, and the login shell's execute not served here. Anything else is no pass, and says why.
|
||||
func judgeRootFree(node string, st *accounts.Statement, now time.Time, served bool, servedWhy string) link.RootVerdict {
|
||||
v := link.RootVerdict{Machine: node, Judged: now.UTC()}
|
||||
var not []string
|
||||
confined := ""
|
||||
switch {
|
||||
case st == nil:
|
||||
not = append(not, "this machine's engine has judged no account yet")
|
||||
case now.Sub(st.At) > rootFreshFor:
|
||||
not = append(not, fmt.Sprintf("the engine's newest judgement of its accounts was made at %s, more than %d "+
|
||||
"minutes ago, and a verdict that old is not a verdict on now", st.At.UTC().Format(time.RFC3339),
|
||||
int(rootFreshFor.Minutes())))
|
||||
default:
|
||||
var named []string
|
||||
for _, a := range st.Accounts {
|
||||
if !a.Root {
|
||||
continue
|
||||
}
|
||||
named = append(named, a.Name)
|
||||
if a.State != accounts.Healthy {
|
||||
not = append(not, "the account "+a.Name+" "+orNoneKnownWord(a.Reason))
|
||||
}
|
||||
}
|
||||
if len(named) == 0 {
|
||||
not = append(not, "no account here is declared never to become root, so agents run as the operator's "+
|
||||
"account, which may become root")
|
||||
} else if len(not) == 0 {
|
||||
confined = "the account " + strings.Join(named, ", ") + " cannot become root without a person (judged " +
|
||||
st.At.UTC().Format(time.RFC3339) + ")"
|
||||
}
|
||||
}
|
||||
if served {
|
||||
not = append(not, "the login shell runs any command an agent gives it as the machine's runtime account, which "+
|
||||
"can become root ("+servedWhy+")")
|
||||
}
|
||||
if len(not) > 0 {
|
||||
v.Why = strings.Join(not, "; ")
|
||||
return v
|
||||
}
|
||||
v.Free, v.Why = true, confined+"; the login shell's execute is not served here"
|
||||
return v
|
||||
}
|
||||
|
||||
func orNoneKnownWord(s string) string {
|
||||
if s == "" {
|
||||
return "is not judged"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// settingFileSuffix is the resource the login shell's holder renders its setting execute into (ADR 0268: zsh's
|
||||
// tools-conf, `execute=serve` or `execute=withhold`), read beside the bus's discovery.
|
||||
const settingFileSuffix = ".tools-conf"
|
||||
|
||||
// executeServedHere is whether the login shell's execute is served on this machine (ADR 0268), failing closed: served
|
||||
// while the bus's discovery hears it answered here or could not be asked, or while the holder's setting file applied
|
||||
// here says anything but withhold or cannot be read. why says which.
|
||||
func executeServedHere(statePath string, heard func() (bool, error)) (bool, string) {
|
||||
var why []string
|
||||
if heard == nil {
|
||||
why = append(why, "the bus could not be asked whether execute is answered here")
|
||||
} else if h, err := heard(); err != nil {
|
||||
why = append(why, "the bus could not be asked whether execute is answered here: "+err.Error())
|
||||
} else if h {
|
||||
why = append(why, "the bus hears execute answered here")
|
||||
}
|
||||
known, err := store.Load(statePath)
|
||||
if err != nil {
|
||||
why = append(why, "what this machine applied could not be read: "+err.Error())
|
||||
}
|
||||
for _, r := range known.Resources {
|
||||
if r.Type != "file" || !strings.HasSuffix(r.ID, settingFileSuffix) {
|
||||
continue
|
||||
}
|
||||
if value, err := settingIn(r.Target); err != nil {
|
||||
why = append(why, moduleOf(r.ID)+"'s setting execute cannot be read: "+err.Error())
|
||||
} else if value != "withhold" {
|
||||
why = append(why, moduleOf(r.ID)+"'s setting execute here is "+value)
|
||||
}
|
||||
}
|
||||
return len(why) > 0, strings.Join(why, "; ")
|
||||
}
|
||||
|
||||
// settingIn is the value of execute in a setting file, "" when it says none.
|
||||
func settingIn(path string) (string, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
value := ""
|
||||
lines := bufio.NewScanner(f)
|
||||
for lines.Scan() {
|
||||
t := strings.TrimSpace(lines.Text())
|
||||
if key, v, ok := strings.Cut(t, "="); ok && !strings.HasPrefix(t, "#") && strings.TrimSpace(key) == "execute" {
|
||||
value = strings.TrimSpace(v)
|
||||
}
|
||||
}
|
||||
if value == "" {
|
||||
value = "not said"
|
||||
}
|
||||
return value, lines.Err()
|
||||
}
|
||||
|
||||
// keepWaiting is every library a look found waiting for the operator's word to keep it (ADR 0328 §8).
|
||||
func keepWaiting(vs []link.ResourceHealth) []string {
|
||||
var out []string
|
||||
for _, v := range vs {
|
||||
for _, w := range v.AccessWaits {
|
||||
if w.Reason == link.WaitKeep {
|
||||
out = append(out, w.Access)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// engineActs is the node seat this engine holds (ADR 0563): judged and performed by the same code as the controller's
|
||||
// signed asks, its asks to the operator over the session's link, its acts kept beside the state.
|
||||
func engineActs(opts options, node string, queue *link.Session, search *accounts.SetuidCache) *link.HandActs {
|
||||
state := &link.FileState{Path: filepath.Join(filepath.Dir(opts.state), link.HandActsFile)}
|
||||
h := &link.HandActs{Node: node, State: state,
|
||||
JudgeHandOver: handOverJudgeFrom(opts.state), HandOver: handOverFrom(opts.state),
|
||||
JudgeAccess: accessJudgeFrom(opts.state), AccessAct: accessActFrom(opts.state, opts.timeout),
|
||||
JudgeDirectories: judgeDirectoriesFrom(opts.state), SetuidSearch: queue.SetuidSearch, Searching: search.Running,
|
||||
RootFree: func() link.RootVerdict {
|
||||
served, why := executeServedHere(opts.state, func() (bool, error) {
|
||||
return queue.HeardServed("node-login-shell", "execute")
|
||||
})
|
||||
return judgeRootFree(node, lastAccounts.Load(), time.Now(), served, why)
|
||||
},
|
||||
Say: queue.SayEngine}
|
||||
asker := asks.EngineAsker(node)
|
||||
h.Asker = &meshasker.Client{Module: asker, State: state, Publish: queue.PublishAsk,
|
||||
Record: func(id string) (*stdio.StateEntry, error) { return queue.RouterRecord(asker + "." + id) }}
|
||||
return h
|
||||
}
|
||||
|
||||
// reconcileEvery is how often the engine catches up its open asks from the router's records (a warrant heard while no
|
||||
// link was open, an ask past its expiry): besides each link's opening.
|
||||
const reconcileEvery = 5 * time.Minute
|
||||
|
||||
func reconcileActs(ctx context.Context, h *link.HandActs) {
|
||||
t := time.NewTicker(reconcileEvery)
|
||||
defer t.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
h.Reconcile()
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
-14
@@ -129,16 +129,30 @@ func foundNow() []store.Applied {
|
||||
// never follows a path it did not state: a hand-over is root giving a directory to an account at the next
|
||||
// apply, and a path that resolves elsewhere would give that elsewhere.
|
||||
func handOverFound(statePath, path, by string) (string, error) {
|
||||
found, _, err := judgeFound(statePath, path)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := store.AddHandedOver(statePath, store.HandOver{ID: found.ID, Path: path, At: time.Now().UTC(), By: by}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s (%s) is handed to the mesh by %s: the next apply gives it %s, and it is the mesh's from "+
|
||||
"then on — removed when empty and no longer declared", path, found.ID, by, orUnsaid(found.Declared)), nil
|
||||
}
|
||||
|
||||
// judgeFound is the judgement of a hand-over's path, recording nothing: the directory this machine uses as found at
|
||||
// that path, as the engine states it, and what is on disk there.
|
||||
func judgeFound(statePath, path string) (store.Applied, os.FileInfo, error) {
|
||||
if !filepath.IsAbs(path) {
|
||||
return "", fmt.Errorf("hand-over takes the directory's absolute path, and %q is not one; nothing was handed over", path)
|
||||
return store.Applied{}, nil, fmt.Errorf("hand-over takes the directory's absolute path, and %q is not one; nothing was handed over", path)
|
||||
}
|
||||
if filepath.Clean(path) != path {
|
||||
return "", fmt.Errorf("%q is not the directory's path as the engine states it (no `..`, no doubled or trailing "+
|
||||
return store.Applied{}, nil, fmt.Errorf("%q is not the directory's path as the engine states it (no `..`, no doubled or trailing "+
|
||||
"separator); nothing was handed over", path)
|
||||
}
|
||||
known, err := store.Load(statePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
return store.Applied{}, nil, err
|
||||
}
|
||||
var found *store.Applied
|
||||
for _, r := range foundDirectories(known) {
|
||||
@@ -149,31 +163,27 @@ func handOverFound(statePath, path, by string) (string, error) {
|
||||
}
|
||||
}
|
||||
if found == nil {
|
||||
return "", errors.New(path + " is not a directory this machine uses as found (the module's condition names " +
|
||||
return store.Applied{}, nil, errors.New(path + " is not a directory this machine uses as found (the module's condition names " +
|
||||
"the ones that are); nothing was handed over")
|
||||
}
|
||||
info, err := os.Lstat(path)
|
||||
switch {
|
||||
case err != nil:
|
||||
return "", fmt.Errorf("%s cannot be read on this machine: %v; nothing was handed over", path, err)
|
||||
return store.Applied{}, nil, fmt.Errorf("%s cannot be read on this machine: %v; nothing was handed over", path, err)
|
||||
case info.Mode()&os.ModeSymlink != 0:
|
||||
return "", fmt.Errorf("%s is a link, and the mesh hands over no link (novox/hq ADR 0012); nothing was handed over", path)
|
||||
return store.Applied{}, nil, fmt.Errorf("%s is a link, and the mesh hands over no link (novox/hq ADR 0012); nothing was handed over", path)
|
||||
case !info.IsDir():
|
||||
return "", fmt.Errorf("%s is not a directory on this machine; nothing was handed over", path)
|
||||
return store.Applied{}, nil, fmt.Errorf("%s is not a directory on this machine; nothing was handed over", path)
|
||||
}
|
||||
resolved, err := filepath.EvalSymlinks(path)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%s cannot be resolved on this machine: %v; nothing was handed over", path, err)
|
||||
return store.Applied{}, nil, fmt.Errorf("%s cannot be resolved on this machine: %v; nothing was handed over", path, err)
|
||||
}
|
||||
if resolved != path {
|
||||
return "", fmt.Errorf("%s is reached through a link (it is %s), and the mesh hands over nothing through one "+
|
||||
return store.Applied{}, nil, fmt.Errorf("%s is reached through a link (it is %s), and the mesh hands over nothing through one "+
|
||||
"(novox/hq issue 339); nothing was handed over", path, resolved)
|
||||
}
|
||||
if err := store.AddHandedOver(statePath, store.HandOver{ID: found.ID, Path: path, At: time.Now().UTC(), By: by}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%s (%s) is handed to the mesh by %s: the next apply gives it %s, and it is the mesh's from "+
|
||||
"then on — removed when empty and no longer declared", path, found.ID, by, orUnsaid(found.Declared)), nil
|
||||
return *found, info, nil
|
||||
}
|
||||
|
||||
// handOverFrom is the engine's hand-over for the controller's ask: the state beside statePath, the path as asked.
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
meshasker "git.novox.be/novox/mesh-sdk/go/asker"
|
||||
"git.novox.be/novox/mesh-sdk/go/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/declaration"
|
||||
"github.com/novox/mesh-host/internal/apply"
|
||||
"github.com/novox/mesh-host/internal/link"
|
||||
@@ -53,4 +55,33 @@ func TestALibrarySaysItsWaitWithoutWordsToParse(t *testing.T) {
|
||||
if strings.Contains(media.Reason, dir) || len(books.AccessWaits) != 1 || books.AccessWaits[0].Reason != link.WaitDisk {
|
||||
t.Fatalf("a missing root does not wait for its disk, or a reason names a path: %+v %+v", books, media)
|
||||
}
|
||||
// 774 (ADR 0563 §4): the library's home engine asks its take of the operator itself, once: no second ask while one is
|
||||
// open, nor after it ended; and a library waiting for its disk is not asked.
|
||||
t.Run("the home engine asks the take once", func(t *testing.T) {
|
||||
var asked []asks.Ask
|
||||
state := &link.FileState{Path: filepath.Join(t.TempDir(), link.HandActsFile)}
|
||||
h := &link.HandActs{Node: "n1", State: state,
|
||||
JudgeAccess: func(act, access string) (link.Judged, error) {
|
||||
return link.Judged{Subject: access, Path: root, Sets: "the group mesh-media", Disk: "0:0 755"}, nil
|
||||
},
|
||||
AccessAct: func(link.AccessAct) (string, error) { return "taken", nil }}
|
||||
h.Asker = &meshasker.Client{Module: asks.EngineAsker("n1"), State: state,
|
||||
Publish: func(subject string, body any, id string) error {
|
||||
if a, ok := body.(asks.Ask); ok {
|
||||
asked = append(asked, a)
|
||||
}
|
||||
return nil
|
||||
}}
|
||||
waiting := keepWaiting(vs)
|
||||
askTakes(h, waiting, nil)
|
||||
askTakes(h, waiting, nil)
|
||||
if len(waiting) != 1 || waiting[0] != "library:media" || len(asked) != 1 {
|
||||
t.Fatalf("waiting %v asked %d times", waiting, len(asked))
|
||||
}
|
||||
h.Heard(asks.Warrant{Ask: asked[0].ID, Asker: asks.EngineAsker("n1"), Outcome: asks.OutcomeExpired, At: time.Now()})
|
||||
askTakes(h, waiting, nil)
|
||||
if len(asked) != 1 {
|
||||
t.Fatalf("the take was asked again after its ask ended: %d", len(asked))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1132,6 +1132,10 @@ func runLink(ctx context.Context, opts options) error {
|
||||
Say: say,
|
||||
Timeout: opts.timeout,
|
||||
}
|
||||
// And the node seat node-engine this engine holds (novox/hq ADR 0563): its verbs on every link, its own asks to the
|
||||
// operator under node-engine-<node>, and its acts kept beside its state.
|
||||
queue.Acts = engineActs(opts, mine.Node, queue, setuidSearch)
|
||||
go reconcileActs(ctx, queue.Acts)
|
||||
current := &agentNow{}
|
||||
setRunsHere(runsFromAgent(current))
|
||||
factsOfNow := func(ctx context.Context, ag *agent.Agent) {
|
||||
@@ -1376,6 +1380,7 @@ func judgeWhatRuns(ctx context.Context, j *liveness.Judge, queue *link.Session,
|
||||
if a := accountJudge; a != nil {
|
||||
as, accountsChanged := a.Look(ctx)
|
||||
accountSt = &as
|
||||
lastAccounts.Store(&as)
|
||||
owed = owed || accountsChanged
|
||||
if accountsChanged {
|
||||
for _, v := range as.Accounts {
|
||||
@@ -1390,6 +1395,9 @@ func judgeWhatRuns(ctx context.Context, j *liveness.Judge, queue *link.Session,
|
||||
accessSt, accessesChanged := accesses.look(time.Now())
|
||||
keeping.poke(ctx)
|
||||
owed = owed || accessesChanged
|
||||
// And a library whose root holds data outside its group is asked of the operator by this engine, once (novox/hq
|
||||
// ADR 0563 §4).
|
||||
askTakes(queue.Acts, keepWaiting(accessSt), say)
|
||||
if accessesChanged {
|
||||
for _, v := range accessSt {
|
||||
if v.State != link.StateHealthy {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -9,6 +12,7 @@ import (
|
||||
"github.com/novox/mesh-host/internal/accounts"
|
||||
"github.com/novox/mesh-host/internal/link"
|
||||
"github.com/novox/mesh-host/internal/liveness"
|
||||
"github.com/novox/mesh-host/internal/store"
|
||||
"github.com/novox/mesh-host/internal/units"
|
||||
)
|
||||
|
||||
@@ -103,6 +107,61 @@ func TestTheStatementSaysAnAccountJudgedForRoot(t *testing.T) {
|
||||
if got["claude-code.agent"] != "never" || got["openrazer.account"] != "" {
|
||||
t.Fatalf("root said: %v", got)
|
||||
}
|
||||
// 774 (ADR 0563 §6): the node seat's root-free passes only on the engine's own positive verdict at most 15 minutes old
|
||||
// with execute not served here; a stale, missing or negative verdict, or execute served, is no pass and says why.
|
||||
t.Run("this machine's root-free", func(t *testing.T) {
|
||||
agent := accounts.Verdict{Account: accounts.Account{Module: "claude-code", ID: "claude-code.agent", Name: "agent", Root: true},
|
||||
State: accounts.Healthy, Since: at}
|
||||
free := &accounts.Statement{At: at, Accounts: []accounts.Verdict{agent}}
|
||||
for _, row := range []struct {
|
||||
name string
|
||||
st *accounts.Statement
|
||||
after time.Duration
|
||||
served bool
|
||||
free bool
|
||||
why string
|
||||
}{
|
||||
{name: "a positive verdict", st: free, after: 14 * time.Minute, free: true, why: "the account agent cannot become root"},
|
||||
{name: "a stale verdict", st: free, after: 16 * time.Minute, why: "more than 15 minutes ago"},
|
||||
{name: "no verdict", st: nil, why: "judged no account yet"},
|
||||
{name: "a way to root", st: as, why: "in the group docker, which grants root"},
|
||||
{name: "no agent account", st: &accounts.Statement{At: at}, why: "agents run as the operator's account"},
|
||||
{name: "execute served", st: free, served: true, why: "the login shell runs any command"},
|
||||
} {
|
||||
v := judgeRootFree("ace", row.st, at.Add(row.after), row.served, "the bus hears execute answered here")
|
||||
if v.Free != row.free || !strings.Contains(v.Why, row.why) || v.Machine != "ace" {
|
||||
t.Errorf("%s: %+v", row.name, v)
|
||||
}
|
||||
}
|
||||
// execute is served while the holder's setting file says anything but withhold, or the bus hears it.
|
||||
dir := t.TempDir()
|
||||
statePath, conf := filepath.Join(dir, "state.json"), filepath.Join(dir, "tools.conf")
|
||||
if err := store.Save(statePath, store.State{Resources: []store.Applied{{ID: "zsh.tools-conf", Type: "file", Target: conf}}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
quiet := func() (bool, error) { return false, nil }
|
||||
for _, row := range []struct {
|
||||
name, setting string
|
||||
heard func() (bool, error)
|
||||
served bool
|
||||
}{
|
||||
{name: "withheld and not heard", setting: "# the mesh's\nexecute=withhold\n", heard: quiet},
|
||||
{name: "served", setting: "execute=serve\n", heard: quiet, served: true},
|
||||
{name: "withheld and heard", setting: "execute=withhold\n", heard: func() (bool, error) { return true, nil }, served: true},
|
||||
{name: "the bus not asked", setting: "execute=withhold\n", heard: func() (bool, error) { return false, errors.New("no link") }, served: true},
|
||||
{name: "no setting file", heard: quiet, served: true},
|
||||
} {
|
||||
_ = os.Remove(conf)
|
||||
if row.setting != "" {
|
||||
if err := os.WriteFile(conf, []byte(row.setting), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if served, why := executeServedHere(statePath, row.heard); served != row.served {
|
||||
t.Errorf("%s: served %v (%s)", row.name, served, why)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// A verdict waiting for the setuid search says how far it got, in the reason the root-free verb repeats and as
|
||||
|
||||
Reference in New Issue
Block a user