diff --git a/cmd/mesh-controller/holders_silent_test.go b/cmd/mesh-controller/holders_silent_test.go index 41987416..79362ec9 100644 --- a/cmd/mesh-controller/holders_silent_test.go +++ b/cmd/mesh-controller/holders_silent_test.go @@ -92,3 +92,26 @@ func TestHoldingNeedsAnAnswer(t *testing.T) { } } } + +// The control-node withholds the login shell's `execute` (novox/hq ADR 0268): its holder there serves +// nothing on the seat, and must not be judged silent for it, as the store's rows read it back. +func TestALoginShellWithholdingExecuteIsNotSilent(t *testing.T) { + defer catalogue.UseSeats(catalogue.DefaultSeats()) + var rows []catalogue.Seat + for _, s := range catalogue.DefaultSeats() { + stored := s + stored.Serves = nil + for _, v := range s.Serves { + v.Optional = false // the store never keeps the mark + stored.Serves = append(stored.Serves, v) + } + rows = append(rows, stored) + } + catalogue.UseSeats(rows) + recorded := []catalogue.Held{{Claim: catalogue.LoginShellSeat, Scope: catalogue.ScopeNode, Node: "anchor", Module: "zsh"}} + expected := holdersToHear(catalogue.SeatsWithAProtocol(), recorded, nil, map[string]bool{"anchor": true}, nil, time.Now()) + if _, asked := expected[catalogue.LoginShellSeat]; asked { + t.Fatalf("the login shell's holder is expected to answer, so withholding execute would be said silent: %v", + expected[catalogue.LoginShellSeat]) + } +} diff --git a/internal/catalogue/login_shell_execute_test.go b/internal/catalogue/login_shell_execute_test.go new file mode 100644 index 00000000..51b16e4c --- /dev/null +++ b/internal/catalogue/login_shell_execute_test.go @@ -0,0 +1,41 @@ +package catalogue + +import "testing" + +// The login shell's `execute` runs any command as the operator account, which can become root without a +// person, so the operator withheld it on the control-node until a call to it needs a person's approval +// (novox/hq ADR 0268). It is withheld per machine by the holder's own setting, so the seat must let a +// holder hold it without serving the verb there: `execute` is optional (ADR 0246's mark), and stays so in +// a seat row read back from the store, which never keeps the mark. +func TestTheLoginShellsExecuteIsOptionalSoAMachineMayWithholdIt(t *testing.T) { + check := func(t *testing.T) { + t.Helper() + seat, ok := SeatNamed(LoginShellSeat) + if !ok { + t.Fatal("node-login-shell is not defined") + } + if len(seat.Serves) != 1 || seat.Serves[0].Name != "execute" { + t.Fatalf("the login shell promises %+v, not execute alone", seat.Serves) + } + if !seat.Serves[0].Optional { + t.Fatal("execute is required, so a holder that withholds it on one machine could not hold the seat there") + } + withholding := Manifest{Module: "zsh", Version: "1", Claims: []Claim{{Name: LoginShellSeat, Scope: ScopeNode}}} + if err := CanHold(withholding, seat); err != nil { + t.Fatalf("a holder serving no execute is refused: %v", err) + } + serving := withholding + serving.Claims = []Claim{{Name: LoginShellSeat, Scope: ScopeNode, Serves: []string{"execute"}}} + if err := CanHold(serving, seat); err != nil { + t.Fatalf("a holder serving execute is refused: %v", err) + } + } + t.Run("compiled", check) + t.Run("read back from the store", func(t *testing.T) { + before := seats + t.Cleanup(func() { seats = before }) + UseSeats([]Seat{{Name: LoginShellSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0204", + Serves: []Verb{{Name: "execute"}}}}) + check(t) + }) +} diff --git a/internal/catalogue/seats.go b/internal/catalogue/seats.go index 8d42af16..c83aca05 100644 --- a/internal/catalogue/seats.go +++ b/internal/catalogue/seats.go @@ -256,8 +256,9 @@ var defaultSeats = append([]Seat{ {Name: EnvironmentSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0203"}, // The login shell (novox/hq ADR 0204, replacing the module-declared `login-shell` of ADR 0176): // the mesh's, so a second shell module claims the seat rather than declaring a second one, and - // the seat exists whether or not zsh's definition is registered. `execute` is the contract any - // node may call; the holder places every module's shell code in its slots. + // the seat exists whether or not zsh's definition is registered. `execute` is the contract a caller + // may call where the machine serves it (optional: ADR 0268); the holder places every module's shell + // code in its slots. {Name: LoginShellSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0204", Serves: loginShellVerbs()}, // A machine's power (novox/hq ADR 0211): its holder owns logind's power handling, places the @@ -721,9 +722,6 @@ func uplinkVerbs() []Verb { } } -// loginShellVerbs is the contract every holder of node-login-shell serves (novox/hq ADR 0176, ADR -// 0204): one command, run the way the operator's own terminal would run it, bounded below the -// runtime's thirty-second call limit so a hung command answers rather than times the caller out. // backupVerbs is the node-backup seat's protocol (novox/hq to-be 43): what is kept, take one now, // and restore beside the live data — never over it. func backupVerbs() []Verb { @@ -745,9 +743,18 @@ func backupVerbs() []Verb { } } +// loginShellVerbs is the contract of node-login-shell (novox/hq ADR 0176, ADR 0204): one command, run +// the way the operator's own terminal would run it, bounded below the runtime's thirty-second call limit +// so a hung command answers rather than times the caller out. +// +// **`execute` is optional** (novox/hq ADR 0268). It runs any command as the operator account, which can +// become root without a person, so a machine may withhold it: the control-node does, through the +// holder's own `execute` setting, until a call to it needs a person's approval (hq research 039). The mark +// is ADR 0246's: a holder that does not serve the verb on a machine still holds the seat there, and its +// silence on the bus is not judged — a holder withholding it serves nothing on the seat. func loginShellVerbs() []Verb { return []Verb{ - {Name: "execute", Description: "Run one command on this machine as the operator account, in a " + + {Name: "execute", Optional: true, Description: "Run one command on this machine as the operator account, in a " + "non-interactive login shell in its home; answers with what it printed and how it exited.", Input: schema(map[string]string{ "command": "the command line, as you would type it",