From 9f2e16ff704cc7dd1ea11b60a0ec73f2a1a969cb Mon Sep 17 00:00:00 2001 From: mesh-admin Date: Wed, 14 Oct 2026 05:46:49 +0000 Subject: [PATCH] Keep the newest trunk build's list of predefined keys, and prove each refusal The list itself, read strictly (catalogue/predefined.go); the controller keeps a catalogue trunk build's list in its store when its commit's generation is larger than the one kept (migration 0098), so an older build's list heard later never replaces a newer one, and loads it with its store (hq ADR 0348 clause 7, issue 661). Part 2 of 2: the new files. Co-Authored-By: Claude Opus 5.5 --- cmd/mesh-controller/predefined.go | 93 +++++++++++++++++++ .../0098-the-list-of-predefined-keys.sql | 20 ++++ internal/inventory/predefined.go | 76 +++++++++++++++ internal/inventory/predefined_test.go | 69 ++++++++++++++ 4 files changed, 258 insertions(+) create mode 100644 cmd/mesh-controller/predefined.go create mode 100644 internal/inventory/migrations/0098-the-list-of-predefined-keys.sql create mode 100644 internal/inventory/predefined.go create mode 100644 internal/inventory/predefined_test.go diff --git a/cmd/mesh-controller/predefined.go b/cmd/mesh-controller/predefined.go new file mode 100644 index 00000000..7b93b748 --- /dev/null +++ b/cmd/mesh-controller/predefined.go @@ -0,0 +1,93 @@ +package main + +import ( + "context" + "fmt" + "path" + "strings" + + "github.com/novox/mesh-controller/internal/catalogue" + "github.com/novox/mesh-controller/internal/inventory" + "github.com/novox/mesh-controller/internal/link" +) + +// The list of predefined keys the mesh publishes (novox/hq ADR 0348 §7, design 57 package K2, issue 661). +// +// **The newest trunk build of the catalogue wins.** A build of the catalogue reads predefined.json at the commit +// it builds and carries it with the commit's generation (link.PredefinedList). Registration keeps it in the +// controller's store when that generation is larger than the one kept (inventory.KeepPredefined), so an older +// build's list heard later never replaces a newer one, and every manifest the controller reads again is judged +// against the list kept (catalogue.UsePredefined). mesh-config publishes it on the bus from there (design 57 +// package K3); the controller keeps no config value, only the keys. + +// fromTheCatalogue is whether a build is of the catalogue's repository, whose list alone the mesh publishes: by +// its name, in the URL form or the seat form. +func fromTheCatalogue(result link.BuildResult) bool { + for _, repository := range []string{result.Repository, sourceRepository(result)} { + name := strings.TrimSuffix(path.Base(strings.TrimRight(repository, "/")), ".git") + if name == catalogue.CatalogueRepository { + return true + } + } + return false +} + +func sourceRepository(result link.BuildResult) string { + if result.Source == nil { + return "" + } + return result.Source.Repository +} + +// carriedList is the list a build of the catalogue carried, read strictly; nil for a build of another +// repository, or one that carried none. +func carriedList(result link.BuildResult) (catalogue.PredefinedList, error) { + if result.Predefined == nil || !fromTheCatalogue(result) { + return nil, nil + } + list, err := catalogue.ParsePredefined(result.Predefined.Keys) + if err != nil { + return nil, fmt.Errorf("%s carried a list of predefined keys that is refused: %w", result.ID, err) + } + return list, nil +} + +// judgedWith is the list a build's manifest is first judged against at registration: the catalogue's own at the +// commit built, which its build judged it against, where it carried one; the list kept otherwise. +func judgedWith(result link.BuildResult) (catalogue.PredefinedList, error) { + list, err := carriedList(result) + if err != nil || list != nil { + return list, err + } + return catalogue.PublishedPredefined(), nil +} + +// loadPredefined makes the list kept in the store the one this controller judges manifests against. A store +// keeping none leaves the list it has. +func loadPredefined(ctx context.Context, inv *inventory.Inventory) error { + kept, has, err := inv.PredefinedKept(ctx) + if err != nil || !has { + return err + } + catalogue.UsePredefined(kept.Keys) + return nil +} + +// publishPredefined keeps a trunk build's list when it is newer than the one kept, says so, and loads what the +// store keeps then — this build's, or a newer one another controller process kept. +func publishPredefined(ctx context.Context, inv *inventory.Inventory, result link.BuildResult, + list catalogue.PredefinedList) error { + kept, err := inv.KeepPredefined(ctx, inventory.PredefinedRecord{Keys: list, Repository: result.Repository, + Commit: result.Commit, Generation: result.Predefined.Generation, Build: result.ID}) + if err != nil { + return err + } + if kept { + fmt.Printf("%s: the list of predefined keys at %s (generation %d) is published: %s\n", result.ID, + short(result.Commit), result.Predefined.Generation, strings.Join(list.Keys(), ", ")) + } else { + fmt.Printf("%s: the list of predefined keys at %s (generation %d) is not newer than the one kept, which "+ + "stands\n", result.ID, short(result.Commit), result.Predefined.Generation) + } + return loadPredefined(ctx, inv) +} diff --git a/internal/inventory/migrations/0098-the-list-of-predefined-keys.sql b/internal/inventory/migrations/0098-the-list-of-predefined-keys.sql new file mode 100644 index 00000000..fa4d42b4 --- /dev/null +++ b/internal/inventory/migrations/0098-the-list-of-predefined-keys.sql @@ -0,0 +1,20 @@ +-- The list of predefined keys (novox/hq ADR 0348 clause 7, design 57 package K2, issue 661). +-- +-- Mesh config and node config hold values only for the keys in the catalogue's `predefined.json`. A build of +-- the catalogue reads the file at the commit it builds and carries it; the controller keeps the newest trunk +-- build's, which every manifest it reads again is judged against, and which mesh-config publishes from here +-- (design 57 package K3). One list for the mesh: one row. +-- +-- **The newest trunk build wins**, by the generation of the commit built — how many commits it stands on, which +-- of two commits on one trunk is larger for the later — so a list heard late from an older build never replaces +-- a newer one. Not a config value: a key, its type, description, validation and whether it is trusted, all of it +-- public in the catalogue's repository. +create table predefined_list ( + one boolean primary key default true check (one), + keys jsonb not null, + repository text not null, + commit text not null, + generation bigint not null check (generation > 0), + build text not null, + kept_at timestamptz not null default now() +); diff --git a/internal/inventory/predefined.go b/internal/inventory/predefined.go new file mode 100644 index 00000000..03fc588e --- /dev/null +++ b/internal/inventory/predefined.go @@ -0,0 +1,76 @@ +package inventory + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/jackc/pgx/v5" + + "github.com/novox/mesh-controller/internal/catalogue" +) + +// The list of predefined keys the mesh publishes (novox/hq ADR 0348 §7, design 57 package K2): the newest trunk +// build's of the catalogue, by the generation of the commit built. + +// PredefinedRecord is the list kept, and the build it came from. +type PredefinedRecord struct { + Keys catalogue.PredefinedList + Repository string + Commit string + Generation int + Build string + KeptAt time.Time +} + +// KeepPredefined keeps a trunk build's list when its commit is newer than the one kept — a larger generation — +// and answers whether it did. An older build's list, or one of the same generation, heard later is not kept: the +// newer one stands, whatever order the outcomes arrive in. One statement, so two builds heard at once cannot both +// win. +func (i *Inventory) KeepPredefined(ctx context.Context, r PredefinedRecord) (bool, error) { + if r.Keys == nil { + return false, errors.New("no list to keep") + } + if r.Generation <= 0 { + return false, fmt.Errorf("the build %s did not say its commit's generation, so whether its list is newer "+ + "than the one kept cannot be said", r.Build) + } + if problems := catalogue.PredefinedProblems(r.Keys); len(problems) > 0 { + return false, fmt.Errorf("the list of %s is refused: %v", r.Build, problems) + } + text, err := r.Keys.Canonical() + if err != nil { + return false, err + } + tag, err := i.store.Pool().Exec(ctx, ` + insert into predefined_list (keys, repository, commit, generation, build) + values ($1, $2, $3, $4, $5) + on conflict (one) do update set keys = excluded.keys, repository = excluded.repository, + commit = excluded.commit, generation = excluded.generation, build = excluded.build, kept_at = now() + where predefined_list.generation < excluded.generation`, + text, r.Repository, r.Commit, r.Generation, r.Build) + if err != nil { + return false, err + } + return tag.RowsAffected() == 1, nil +} + +// PredefinedKept is the list kept, and false when none is yet. +func (i *Inventory) PredefinedKept(ctx context.Context) (PredefinedRecord, bool, error) { + var r PredefinedRecord + var text []byte + err := i.store.Pool().QueryRow(ctx, + `select keys, repository, commit, generation, build, kept_at from predefined_list`). + Scan(&text, &r.Repository, &r.Commit, &r.Generation, &r.Build, &r.KeptAt) + if errors.Is(err, pgx.ErrNoRows) { + return PredefinedRecord{}, false, nil + } + if err != nil { + return PredefinedRecord{}, false, err + } + if r.Keys, err = catalogue.ParsePredefined(text); err != nil { + return PredefinedRecord{}, false, fmt.Errorf("the list kept from %s cannot be read: %w", r.Build, err) + } + return r, true, nil +} diff --git a/internal/inventory/predefined_test.go b/internal/inventory/predefined_test.go new file mode 100644 index 00000000..d2ab52d2 --- /dev/null +++ b/internal/inventory/predefined_test.go @@ -0,0 +1,69 @@ +package inventory + +import ( + "testing" + + "github.com/novox/mesh-controller/internal/catalogue" +) + +// The list of predefined keys the controller keeps (novox/hq ADR 0348 §7 and its check "a newer trunk build's +// list replaces the published one, and an older build's list heard later does not"; design 57 package K2). + +func listOf(t *testing.T, text string) catalogue.PredefinedList { + t.Helper() + l, err := catalogue.ParsePredefined([]byte(text)) + if err != nil { + t.Fatal(err) + } + return l +} + +func TestANewerBuildsListReplacesTheKeptOneAndAnOlderOneHeardLaterDoesNot(t *testing.T) { + inv := ForTest(t) + ctx := t.Context() + if _, has, err := inv.PredefinedKept(ctx); err != nil || has { + t.Fatalf("a fresh store keeps a list: %v %v", has, err) + } + older := PredefinedRecord{Keys: listOf(t, `{"font-size": {"type": "number", "description": "points"}}`), + Repository: "novox/mesh-catalog", Commit: "aaaa", Generation: 100, Build: "b-older"} + newer := PredefinedRecord{Keys: listOf(t, `{"font-size": {"type": "number", "description": "points"}, + "agent-managed-settings": {"type": "object", "description": "a JSON object", "trusted": true}}`), + Repository: "novox/mesh-catalog", Commit: "bbbb", Generation: 101, Build: "b-newer"} + + if kept, err := inv.KeepPredefined(ctx, older); err != nil || !kept { + t.Fatalf("the first list was not kept: %v %v", kept, err) + } + if kept, err := inv.KeepPredefined(ctx, newer); err != nil || !kept { + t.Fatalf("a newer build's list did not replace the kept one: %v %v", kept, err) + } + // The older build's outcome heard after the newer one's. + if kept, err := inv.KeepPredefined(ctx, older); err != nil || kept { + t.Fatalf("an older build's list heard later replaced the newer one: %v %v", kept, err) + } + // Nor one of the same generation: the one kept stands. + same := newer + same.Keys, same.Build = older.Keys, "b-again" + if kept, err := inv.KeepPredefined(ctx, same); err != nil || kept { + t.Fatalf("a list of the same generation replaced the kept one: %v %v", kept, err) + } + got, has, err := inv.PredefinedKept(ctx) + if err != nil || !has { + t.Fatal(has, err) + } + if got.Build != "b-newer" || got.Commit != "bbbb" || got.Generation != 101 || len(got.Keys) != 2 || + !got.Keys["agent-managed-settings"].Trusted { + t.Fatalf("kept %+v", got) + } + + // A build that could not say its generation, or carried a wrong list, is never kept. + unordered := newer + unordered.Generation = 0 + if kept, err := inv.KeepPredefined(ctx, unordered); err == nil || kept { + t.Fatalf("a list without a generation was kept: %v", err) + } + wrong := newer + wrong.Generation, wrong.Keys = 200, catalogue.PredefinedList{"accent": {Type: "object", Description: "x"}} + if kept, err := inv.KeepPredefined(ctx, wrong); err == nil || kept { + t.Fatalf("a wrong list was kept: %v", err) + } +}