A seat says what it receives, and its holder places every module's contribution (hq ADR 0212)

Each contribution grain was a manifest field and a renderer of its own; a module now contributes
to any seat with a kind that seat receives, the holder places it with
${contribution:<seat>:<kind>}, and the contribution depends on the seat. node-hotkeys is the
first new seat to receive (triggers); the display session receives window-manager config.
This commit is contained in:
jochen
2026-10-04 16:48:01 +02:00
parent b36babcd7d
commit b7912172af
8 changed files with 339 additions and 18 deletions
+15 -5
View File
@@ -110,6 +110,8 @@ const (
var (
ofEnvironment = regexp.MustCompile(`\$\{environment:([^}]*)\}`)
ofShell = regexp.MustCompile(`\$\{shell:([^}]*)\}`)
// ofContributed is either kind of contributed text, matched together so both fill in one pass.
ofContributed = regexp.MustCompile(`\$\{(shell|contribution):([^}]*)\}`)
)
// variableName is a POSIX shell variable name, which is also what environment.d accepts.
@@ -223,6 +225,7 @@ func (m Manifest) contributionPlaceholderProblems() []string {
var problems []string
for _, r := range m.Resources {
problems = append(problems, placeholderProblems(m, r)...)
problems = append(problems, seatPlaceholderProblems(m, r)...)
}
return problems
}
@@ -545,7 +548,7 @@ func shellCode(modules []Manifest, shell, slot string) string {
// is filled before the shell's code is, and each is replaced in a single pass over what the holder
// wrote, so a contributed piece is never scanned again.
func contributionsInto(resource map[string]any, m Manifest, modules []Manifest, facts map[string]string) error {
if problems := placeholderProblems(m, resource); len(problems) > 0 {
if problems := append(placeholderProblems(m, resource), seatPlaceholderProblems(m, resource)...); len(problems) > 0 {
return fmt.Errorf("%s", problems[0])
}
content, ok := resource["content"].(string)
@@ -564,10 +567,17 @@ func contributionsInto(resource map[string]any, m Manifest, modules []Manifest,
return env.posix()
})
}
if ofShell.MatchString(content) {
content = ofShell.ReplaceAllStringFunc(content, func(placeholder string) string {
shell, slot, _ := strings.Cut(ofShell.FindStringSubmatch(placeholder)[1], ":")
return shellCode(modules, shell, slot)
// Shell code and seat contributions in one pass (novox/hq ADR 0212): both are contributed text
// the controller does not read, so neither may be scanned after the other is in place — a
// contributed line that happened to spell the other's placeholder would be filled.
if ofContributed.MatchString(content) {
content = ofContributed.ReplaceAllStringFunc(content, func(placeholder string) string {
found := ofContributed.FindStringSubmatch(placeholder)
first, second, _ := strings.Cut(found[2], ":")
if found[1] == "contribution" {
return seatContributions(modules, first, second)
}
return shellCode(modules, first, second)
})
}
resource["content"] = content
+14 -10
View File
@@ -41,16 +41,20 @@ func graphicalSessionSeats() []Seat {
"name": "the profile to save or apply (save and apply only)",
}, []string{"action"}), "action", "list", "save", "apply")},
}},
{Name: DisplaySessionSeat, Scope: ScopeNode, Decision: decided, Serves: []Verb{
{Name: "reload", Description: "Reload the session's configuration in place, keeping its windows.",
Input: schema(map[string]string{}, nil)},
{Name: "workspaces", Description: "The session's workspaces: each one's name, output, and whether " +
"it is visible or focused.",
Input: schema(map[string]string{}, nil)},
{Name: "windows", Description: "The session's windows: each one's title, class, workspace and " +
"whether it has focus; narrowed to one workspace when named.",
Input: schema(map[string]string{"workspace": "one workspace (optional)"}, nil)},
}},
// It receives window-manager configuration lines from every other module (novox/hq ADR 0212):
// bindings, start-up commands, rules — placed by the session's holder, never written into
// its directory by the contributor.
{Name: DisplaySessionSeat, Scope: ScopeNode, Decision: decided,
Receives: []Receivable{{Kind: "config", Comment: "#"}}, Serves: []Verb{
{Name: "reload", Description: "Reload the session's configuration in place, keeping its windows.",
Input: schema(map[string]string{}, nil)},
{Name: "workspaces", Description: "The session's workspaces: each one's name, output, and whether " +
"it is visible or focused.",
Input: schema(map[string]string{}, nil)},
{Name: "windows", Description: "The session's windows: each one's title, class, workspace and " +
"whether it has focus; narrowed to one workspace when named.",
Input: schema(map[string]string{"workspace": "one workspace (optional)"}, nil)},
}},
{Name: TerminalEmulatorSeat, Scope: ScopeNode, Decision: decided, Serves: []Verb{
{Name: "open", Description: "Open a terminal window in the operator's session, running a command " +
"or the login shell, in a directory or the account's home.",
+6
View File
@@ -539,6 +539,11 @@ type Manifest struct {
// the holder of node-login-shell put the slot's placeholder.
Shell []ShellCode `json:"shell,omitempty"`
// Contributions are configuration this module gives the holder of a seat it does not hold, in
// that tool's own grammar (novox/hq ADR 0212): a seat, a kind the seat receives, and the text. The
// holder places them; the module depends on the seat (ADR 0210 §3).
Contributions []SeatContribution `json:"contributions,omitempty"`
// Guards are ports of this module's the mesh refuses on an adopted node except from the
// private network and from the machine itself (novox/hq ADR 0100) — the store's port and the
// broker's management port. The ports the software uses; the mesh guards where the machine
@@ -1846,6 +1851,7 @@ func ParseManifest(raw []byte) (Manifest, error) {
problems = append(problems, m.environmentProblems()...)
problems = append(problems, m.shellProblems()...)
problems = append(problems, m.contributionPlaceholderProblems()...)
problems = append(problems, m.seatContributionProblems()...)
for i, r := range m.Resources {
id, _ := r["id"].(string)
+156
View File
@@ -0,0 +1,156 @@
package catalogue
import (
"fmt"
"regexp"
"strings"
)
// A module contributes to a seat it does not hold (novox/hq ADR 0212).
//
// ADR 0210 made a tool's configuration its seat holder's, and every other module's way in a
// contribution to the seat. The environment, the shell's slots and the power moments each became a
// field of their own; this is the general form, so a new seat that takes contributions is a row in
// the seat table rather than a change to the manifest: a contribution names a seat, a kind that
// seat receives, and text in the tool's own grammar, which the controller never reads.
// HotkeysSeat is the machine's hotkey daemon (novox/hq ADR 0212 §5).
const HotkeysSeat = "node-hotkeys"
// SeatContribution is one piece of configuration a module gives a seat's holder to place.
type SeatContribution struct {
// Seat is the seat whose holder places it.
Seat string `json:"seat"`
// Kind is which of the seat's receivable kinds it is.
Kind string `json:"kind"`
// Content is the text, in the tool's own grammar. Never interpreted.
Content string `json:"content"`
}
// ofContribution is where a holder places a kind: ${contribution:<seat>:<kind>}. Loose inside the
// braces, so a misspelt seat or kind is found and refused rather than written out as text.
var ofContribution = regexp.MustCompile(`\$\{contribution:([^}]*)\}`)
// receivable is what a seat receives of a kind, with the seat's canonical name; false when the seat
// is unknown or does not receive it.
func receivable(seat, kind string) (Seat, Receivable, bool) {
s, known := SeatNamed(seat)
if !known {
return Seat{}, Receivable{}, false
}
for _, r := range s.Receives {
if r.Kind == kind {
return s, r, true
}
}
return s, Receivable{}, false
}
// kindsOf names a seat's receivable kinds for a refusal.
func kindsOf(s Seat) string {
if len(s.Receives) == 0 {
return "it receives no contributions"
}
var kinds []string
for _, r := range s.Receives {
kinds = append(kinds, r.Kind)
}
return "it receives " + strings.Join(kinds, ", ")
}
// seatContributionProblems is what is wrong with this module's contributions, from the manifest
// alone (novox/hq ADR 0212 §2).
func (m Manifest) seatContributionProblems() []string {
var problems []string
for i, c := range m.Contributions {
s, _, ok := receivable(c.Seat, c.Kind)
switch {
case s.Name == "":
problems = append(problems, fmt.Sprintf(
"%s's contribution %d is to the seat %q, which the mesh does not define", m.Module, i+1, c.Seat))
case !ok:
problems = append(problems, fmt.Sprintf(
"%s's contribution %d to %s is of the kind %q; %s (novox/hq ADR 0212)",
m.Module, i+1, s.Name, c.Kind, kindsOf(s)))
}
if strings.TrimSpace(c.Content) == "" {
problems = append(problems, fmt.Sprintf("%s's contribution %d has no content", m.Module, i+1))
}
}
return problems
}
// seatPlaceholderProblems is what is wrong with one resource's ${contribution:…}: placed only in a
// file's content, naming a seat and a kind it receives, and only by a module that claims that seat
// — another would be a second writer of a file there is one of (novox/hq ADR 0212 §3).
func seatPlaceholderProblems(m Manifest, r map[string]any) []string {
var problems []string
for _, field := range sortedKeys(r) {
v, ok := r[field].(string)
if !ok {
continue
}
found := ofContribution.FindAllStringSubmatch(v, -1)
if len(found) == 0 {
continue
}
if field != "content" {
problems = append(problems, fmt.Sprintf(
"%s's resource %v names %s in its %s; contributions are placed only in a file's content",
m.Module, r["id"], found[0][0], field))
continue
}
for _, f := range found {
seat, kind, two := strings.Cut(f[1], ":")
s, _, ok := receivable(seat, kind)
if !two || !ok {
detail := "the mesh defines no seat " + fmt.Sprintf("%q", seat)
if s.Name != "" {
detail = s.Name + ": " + kindsOf(s)
}
problems = append(problems, fmt.Sprintf(
"%s's resource %v names %s; a contribution is ${contribution:<seat>:<kind>} (%s)",
m.Module, r["id"], f[0], detail))
continue
}
if !m.ClaimsSeat(s.Name) {
problems = append(problems, fmt.Sprintf(
"%s's resource %v names %s and %s does not claim %s; every module's contributions to a "+
"seat are placed by its holder alone (novox/hq ADR 0212)",
m.Module, r["id"], f[0], m.Module, s.Name))
}
}
}
return problems
}
// seatContributions is every module's contribution of one kind to one seat (novox/hq ADR 0212 §3):
// in module order, each module's pieces in the order it declared them, each module's preceded by a
// comment line naming it in the tool's grammar, and empty when nothing is contributed.
func seatContributions(modules []Manifest, seat, kind string) string {
s, r, ok := receivable(seat, kind)
if !ok {
return ""
}
var b strings.Builder
for _, m := range inModuleOrder(modules) {
named := false
for _, c := range m.Contributions {
if c.Kind != kind {
continue
}
if cs, known := SeatNamed(c.Seat); !known || cs.Name != s.Name {
continue
}
if !named {
fmt.Fprintf(&b, "%s %s\n", r.Comment, m.Module)
named = true
}
b.WriteString(c.Content)
if !strings.HasSuffix(c.Content, "\n") {
b.WriteString("\n")
}
}
}
return b.String()
}
@@ -0,0 +1,117 @@
package catalogue
import (
"reflect"
"strings"
"testing"
)
// Defends novox/hq ADR 0212: a seat says what it receives, its holder places it, and a contribution
// depends on the seat.
func hotkeysHolder() Manifest {
return Manifest{Module: "triggerhappy", Claims: []Claim{{Name: HotkeysSeat}}, Resources: []map[string]any{
{"id": "triggers", "type": "file", "path": "/etc/triggerhappy/triggers.d/mesh.conf",
"content": "# the mesh's triggers\n${contribution:node-hotkeys:trigger}"},
}}
}
func TestAContributionReachesTheHoldersFileInModuleOrderNamedByModule(t *testing.T) {
laptop := Manifest{Module: "laptop", Contributions: []SeatContribution{
{Seat: HotkeysSeat, Kind: "trigger", Content: "KEY_PROG1 1 play ${machine:account-home}"},
{Seat: HotkeysSeat, Kind: "trigger", Content: "KEY_F21 1 touchpad\n"},
}}
another := Manifest{Module: "another", Contributions: []SeatContribution{
{Seat: HotkeysSeat, Kind: "trigger", Content: "KEY_MUTE 1 mute ${shell:zsh:first}"},
}}
unrelated := Manifest{Module: "bar", Contributions: []SeatContribution{
{Seat: DisplaySessionSeat, Kind: "config", Content: "bar { }"},
}}
r := Resolution{Node: "laptop", Account: "op", Modules: []Manifest{hotkeysHolder(), laptop, another, unrelated}}
out, err := r.Declaration(Rendering{})
if err != nil {
t.Fatal(err)
}
var file map[string]any
for _, res := range out {
if res["id"] == "triggerhappy.triggers" {
file = res
}
}
if file == nil {
t.Fatalf("the holder's file was not composed: %v", out)
}
// Module order; each module named once; text never read, so neither ${machine:…} nor ${shell:…}
// inside a contribution is filled.
want := "# the mesh's triggers\n" +
"# another\nKEY_MUTE 1 mute ${shell:zsh:first}\n" +
"# laptop\nKEY_PROG1 1 play ${machine:account-home}\nKEY_F21 1 touchpad\n"
if got := file["content"]; got != want {
t.Fatalf("the holder's file is\n%s\nnot\n%s", got, want)
}
}
func TestNoContributionPlacesNothing(t *testing.T) {
r := Resolution{Node: "laptop", Account: "op", Modules: []Manifest{hotkeysHolder()}}
out, err := r.Declaration(Rendering{})
if err != nil {
t.Fatal(err)
}
for _, res := range out {
if res["id"] == "triggerhappy.triggers" && res["content"] != "# the mesh's triggers\n" {
t.Fatalf("an empty kind left %q", res["content"])
}
}
}
func TestAContributionToASeatThatDoesNotReceiveItIsRefused(t *testing.T) {
cases := map[string]string{
`{"seat":"node-hotkeys","kind":"config","content":"x"}`: "it receives trigger",
`{"seat":"node-nothing","kind":"trigger","content":"x"}`: "the mesh does not define",
`{"seat":"node-hotkeys","kind":"trigger","content":" "}`: "has no content",
`{"seat":"node-display-session","kind":"trigger","content":"x"}`: "it receives config",
}
for c, want := range cases {
raw := `{"module":"laptop","contributions":[` + c + `]}`
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("%s: accepted, or refused without %q: %v", c, want, err)
}
}
}
func TestAContributionPlaceholderOutsideTheHolderIsRefused(t *testing.T) {
raw := `{"module":"laptop","resources":[{"id":"t","type":"file","path":"/etc/t","content":"${contribution:node-hotkeys:trigger}"}]}`
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "laptop does not claim node-hotkeys") {
t.Errorf("a placeholder outside the holder was accepted: %v", err)
}
raw = `{"module":"triggerhappy","claims":[{"name":"node-hotkeys"}],"resources":[{"id":"t","type":"file","path":"/etc/t","content":"${contribution:node-hotkeys:keys}"}]}`
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "it receives trigger") {
t.Errorf("a placeholder for a kind the seat does not receive was accepted: %v", err)
}
}
func TestAContributionDependsOnItsSeat(t *testing.T) {
m := Manifest{Module: "laptop", Contributions: []SeatContribution{
{Seat: HotkeysSeat, Kind: "trigger", Content: "x"},
{Seat: DisplaySessionSeat, Kind: "config", Content: "y"},
}}
if got, want := DependsOn(m), []string{DisplaySessionSeat, HotkeysSeat}; !reflect.DeepEqual(got, want) {
t.Errorf("depends on %v, want %v", got, want)
}
catalogue := map[string]Manifest{"laptop": m, "triggerhappy": hotkeysHolder()}
if _, err := AssignRefusal(catalogue, "laptop", nil, []string{"laptop"}); err == nil ||
!strings.Contains(err.Error(), HotkeysSeat) {
t.Errorf("a contributor without the holder was accepted: %v", err)
}
}
func TestTheHotkeysSeatIsTheMeshsAndReceivesTriggers(t *testing.T) {
s, ok := SeatNamed(HotkeysSeat)
if !ok || s.Scope != ScopeNode || len(s.Receives) != 1 || s.Receives[0].Kind != "trigger" {
t.Fatalf("%+v %v", s, ok)
}
d, _ := SeatNamed(DisplaySessionSeat)
if len(d.Receives) != 1 || d.Receives[0].Kind != "config" {
t.Fatalf("the display session receives %+v", d.Receives)
}
}
+7
View File
@@ -114,6 +114,13 @@ func contributedTo(m Manifest) []string {
for _, c := range m.Shell {
out = append(out, placerOf(c.For))
}
// Any other seat's contribution (novox/hq ADR 0212 §4), by the seat's canonical name; one the
// mesh does not define is refused at registration and depends on nothing here.
for _, c := range m.Contributions {
if s, known := SeatNamed(c.Seat); known {
out = append(out, s.Name)
}
}
return out
}
+21
View File
@@ -40,10 +40,22 @@ type Seat struct {
// Serves carries each verb in full — name, description, schema — because a role's tools are the
// mesh's to define and an agent's to call (novox/hq ADR 0132, design 33 §2).
Serves []Verb
// Receives is what other modules may contribute to the seat's holder, by kind (novox/hq ADR
// 0212): each kind is text in the tool's own grammar, placed by the holder with
// ${contribution:<seat>:<kind>}. Compiled, never stored: like the protocol, it is the mesh's
// definition of the role, and the store's rows carry no column for it.
Receives []Receivable
// Decision is the record that made it a seat.
Decision string
}
// Receivable is one kind of contribution a seat receives (novox/hq ADR 0212 §2): its name, and the
// comment prefix of the tool's grammar, with which the controller names each contributing module.
type Receivable struct {
Kind string
Comment string
}
// defaultSeats is the set the mesh ships with — the seed for the control plane's seat table and the
// fallback when it has none (novox/hq ADR 0122). It is the one place the closed set 0110 defines is
// written; the store's table is seeded from it and thereafter is the live, editable copy.
@@ -176,6 +188,11 @@ var defaultSeats = append([]Seat{
// code modules contribute for the power moments, and publishes the machine's power states as
// its events. Every machine has one — every machine boots and shuts down. No verbs yet.
{Name: PowerSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0211"},
// The machine's hotkeys (novox/hq ADR 0212): the daemon that sees the keys the window manager
// does not — a laptop's vendor keys — run by one module per machine, which owns its
// configuration. Every other module with keys contributes trigger lines to it.
{Name: HotkeysSeat, Scope: ScopeNode, Decision: "novox/hq ADR 0212",
Receives: []Receivable{{Kind: "trigger", Comment: "#"}}},
// Deferred (novox/hq ADR 0121): renaming to mesh-private-network is a scope + server/client
// model change, not a rename, so it stays until that is built.
{Name: "the-private-network", Scope: ScopeNode, Decision: "novox/hq ADR 0110"},
@@ -238,6 +255,10 @@ func UseSeats(s []Seat) {
row.Accepts, row.Emits, row.Serves = d.Accepts, d.Emits, d.Serves
}
}
// What a seat receives is never stored (novox/hq ADR 0212), so it is always the compiled one.
if d, known := byName[row.Name]; known {
row.Receives = d.Receives
}
merged = append(merged, row)
}
seats = merged
+3 -3
View File
@@ -46,13 +46,13 @@ func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
delivered[s.Delivers] = s.Name
}
}
// Thirty-three with node-power (novox/hq ADR 0211); thirty-two since the graphical session's
// Thirty-four with node-hotkeys (novox/hq ADR 0212); thirty-three with node-power (ADR 0211); thirty-two since the graphical session's
// eleven (ADR 0208); twenty-one with
// node-package-manager and node-container-runtime (ADR 0207); nineteen with node-environment and
// node-login-shell (ADR 0203, ADR 0204); seventeen with node-build-agent (ADR 0190). One fewer
// once the retired mesh-build-machine row goes, when no registered manifest claims it any more.
if len(Seats()) != 33 {
t.Errorf("the mesh defines %d seats rather than 33; the set is closed, so a change here is "+
if len(Seats()) != 34 {
t.Errorf("the mesh defines %d seats rather than 34; the set is closed, so a change here is "+
"a decision (novox/hq ADR 0110): %s", len(Seats()), seatNames())
}
}