Answer the controller's own verbs, root-free among them, from the serving controller alone (hq ADR 0259 §8)

The confirmation review asked who may answer root-free on the bus. Composed from the controller's own
manifest, the module principal of the machine running the controller and that machine's runtime were
granted the controller seat's tool subjects too: either could answer root-free, and the runtime's
credential is one an agent on that machine may hold. The controller's seat is now served by the
controller principal alone, in grants and memberships; TestOnlyTheServingControllerMayAnswerRootFree
failed before (3 answerers) and passes. And a machine waiting for its first setuid search is not
root-free, whatever ADR 0266's quiet window does to the self-check.
This commit is contained in:
2026-10-09 13:51:18 +02:00
parent 0e46b8302d
commit 983bf65141
4 changed files with 166 additions and 1 deletions
+3
View File
@@ -113,6 +113,9 @@ func MembershipFor(node string, d Declared, where Placements) Membership {
m.Serves = append(m.Serves, Served{Subject: own + ".tool.{tool}", Queue: "serve." + d.Module})
}
for _, s := range d.Holds {
if servedOnlyByTheController(s) {
continue // answered by the serving controller alone, never through a membership
}
for _, verb := range s.Serves {
m.Seats = append(m.Seats, SeatServed{Seat: s.Name, Verb: verb, Subject: seatToolSubject(s, verb, node)})
}
+15 -1
View File
@@ -566,8 +566,12 @@ func PermissionsFor(p Principal) (Permissions, error) {
"$JS.API.CONSUMER.INFO."+consumerStream(p)+"."+consumerDurable(p),
"$JS.API.CONSUMER.MSG.NEXT."+consumerStream(p)+"."+consumerDurable(p))
// 3. Seats it holds: full participation.
// 3. Seats it holds: full participation — but the controller's own seat, whose verbs only the serving
// controller answers, on its own connection (servedOnlyByTheController).
for _, s := range p.Holds {
if servedOnlyByTheController(s) {
continue
}
if s.isNewTraffic() {
// Composed by SeatTrafficOf below, worker and all; only its tools are served here.
for _, t := range s.Serves {
@@ -661,6 +665,9 @@ func PermissionsFor(p Principal) (Permissions, error) {
pub = append(pub, own+".event."+e)
}
for _, s := range d.Holds {
if servedOnlyByTheController(s) {
continue
}
for _, t := range s.Serves {
sub = append(sub, seatToolSubject(s, t, p.Node))
}
@@ -795,6 +802,13 @@ func seatSubject(s Seat, kind, verb string) string {
// seat carries the node it is asked of, because a flat subject would reach every machine's holder
// and the queue group would silently pick a winner (novox/hq ADR 0132, design 33 §4). A holder
// subscribes its own node's; a user publishes any node's (`*`) and names the machine in the subject.
// servedOnlyByTheController says a seat's verbs are answered by the serving controller alone, on its own
// connection (the KindController grant), never by a module claiming the seat or a runtime carrying it: the
// controller's own seat. Its verbs decide what the mesh is — and `root-free` decides whether the router believes
// a verified sender (novox/hq ADR 0259 §8) — so a machine's runtime, whose credential an agent on that machine
// may hold, answering one would be an agent answering it (the confirmation review of 2026-10-09).
func servedOnlyByTheController(s Seat) bool { return s.Name == ControllerSeat }
func seatToolSubject(s Seat, verb, node string) string {
base := seatSubject(s, "tool", verb)
if s.Scope == "node" && node != "" {
+119
View File
@@ -0,0 +1,119 @@
package inventory
import (
"os"
"strings"
"testing"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/catalogue"
)
// grantMatches is a NATS subject pattern matching a subject: `*` one token, `>` the rest.
func grantMatches(pattern, subject string) bool {
p, s := strings.Split(pattern, "."), strings.Split(subject, ".")
for i, tok := range p {
if tok == ">" {
return len(s) > i
}
if i >= len(s) || (tok != "*" && tok != s[i]) {
return false
}
}
return len(p) == len(s)
}
func grantsAny(patterns []string, subject string) bool {
for _, p := range patterns {
if grantMatches(p, subject) {
return true
}
}
return false
}
// novox/hq ADR 0259 §8 (confirmation review of 2026-10-09): the router honours a verified sender only on the
// controller's `root-free` word, asked on the bus. So only the serving controller may answer that verb — be
// subscribed to its subject — and nobody may publish into the router's inbox but by answering a request it
// made (allow_responses). Composed here from the controller's own manifest, on a machine where the machine's
// runtime carries it beside an ordinary module, with the router, a channel, a person and an administrator: a
// runtime carrying the controller's module, a node-engine, a channel or anybody else answering `root-free` is
// an agent answering it.
func TestOnlyTheServingControllerMayAnswerRootFree(t *testing.T) {
raw, err := os.ReadFile("../../module.json")
if err != nil {
t.Fatal(err)
}
controller, err := catalogue.ParseManifest(raw)
if err != nil {
t.Fatal(err)
}
parse := func(s string) catalogue.Manifest {
m, err := catalogue.ParseManifest([]byte(s))
if err != nil {
t.Fatal(err)
}
return m
}
dir := `"resources": [{"id": "state", "type": "directory", "mode": "0700", "place": "."}]`
router := parse(`{"module": "messenger", "version": "1", "runs-as": "messenger",
"seats": [{"name": "operator-channel", "scope": "mesh", "serves": ["open"], "accepts": ["ask"],
"emits": ["decided"], "by-caller": ["ask", "decided"]}],
"claims": [{"name": "operator-channel", "scope": "mesh", "serves": ["open"]}],
"invokes": ["seat:mesh-controller.root-free", "seat:mesh-controller.conditions"],
"own-secrets": {"broker": "${dir:state}/broker"}, "secrets-owner": "messenger",
"resources": [{"id": "account", "type": "user", "name": "messenger", "shell": "/usr/bin/nologin", "home": "/var/lib/messenger"},
{"id": "state", "type": "directory", "mode": "0700", "place": ".", "owner": "messenger"}]}`)
ordinary := parse(`{"module": "lab-bystander", "version": "1", "own-secrets": {"broker": "${dir:state}/broker"}, ` + dir + `}`)
runtime := catalogue.Manifest{Module: broker.RuntimeModule}
manifests := []catalogue.Manifest{controller, router, ordinary, runtime}
seats := map[string]catalogue.SeatDeclaration{}
declarers := map[string]string{}
for _, m := range manifests {
for _, s := range m.DefinesSeats {
seats[s.Name], declarers[s.Name] = s, m.Module
}
}
for _, own := range catalogue.SeatsWithAProtocol() {
seats[own.Name] = catalogue.SeatDeclaration{Name: own.Name, Scope: own.Scope, Accepts: own.Accepts,
Emits: own.Emits, Serves: own.Serves}
}
records := broker.Records{Nodes: []string{"anchor", "laptop"}, Assigned: map[string][]broker.Declared{},
People: map[string][]string{"operator": {"*"}, "guest": {"mesh-controller.status"}},
Interchangeable: map[string]bool{}}
for _, m := range manifests {
records.Assigned["anchor"] = append(records.Assigned["anchor"], declaredFor(m, seats, declarers))
}
// And a second machine whose runtime carries an ordinary module: where agents run as the operator.
records.Assigned["laptop"] = []broker.Declared{declaredFor(ordinary, seats, declarers), declaredFor(runtime, seats, declarers)}
users, err := broker.Users(records)
if err != nil {
t.Fatal(err)
}
const verb = "mesh.seat.mesh-controller.tool.root-free"
answerers := 0
for _, u := range users {
p, err := broker.PermissionsFor(u)
if err != nil {
t.Fatal(err)
}
answers := grantsAny(p.Subscribe, verb)
if answers != (u.Kind == broker.KindController) {
t.Errorf("%s (%s) %s subscribe to %s", u.Username(), u.Kind,
map[bool]string{true: "may", false: "may not"}[answers], verb)
}
if answers {
answerers++
}
// Nobody publishes into the router's inbox but as an answer to what it asked.
for _, inbox := range []string{"_INBOX.anchor.messenger.x1.y", "_INBOX.anchor.messenger.>"} {
if u.Username() != "anchor.messenger" && grantsAny(p.Publish, inbox) {
t.Errorf("%s may publish into the router's inbox (%s) without being asked", u.Username(), inbox)
}
}
}
if answerers != 1 {
t.Errorf("%d principals may answer root-free, want the controller alone", answerers)
}
}