Merge pull request 'Write the facts snapshot in format 2 with each machine's grants and credential bindings (design 47 R4c-2, issue 1394)' (#409) from feat/r4c2-new-keys into main

This commit was merged in pull request #409.
This commit is contained in:
2026-10-20 19:31:03 +00:00
4 changed files with 86 additions and 34 deletions
+21 -4
View File
@@ -169,10 +169,10 @@ func factsStore(ctx context.Context, open *stores) (string, error) {
return address, nil
}
// writtenFormat is the format of the snapshot this controller writes: 1, the format its own reader knew before mesh-sdk's
// go/facts took the type over (issue 1390, novox/hq ADR 0456). The readers (the merge gate, the check) read formats 1 and
// 2; the writer moves to 2, which also carries each machine's grants and bindings, in a later release of its own.
const writtenFormat = 1
// writtenFormat is the format of the snapshot this controller writes: mesh-sdk's go/facts current format, 2, which carries
// each machine's grants and credential bindings besides everything format 1 held (issue 1394, novox/hq ADR 0456 clause 2).
// Every reader (the merge gate, the build seat's check) reads formats 1 and 2.
const writtenFormat = snapshot.Format
// gatherFacts composes one snapshot from the store: read only, nothing made, nothing sent.
func gatherFacts(ctx context.Context, open *stores, busVersion string) (snapshot.Facts, error) {
@@ -367,6 +367,23 @@ func gatherFacts(ctx context.Context, open *stores, busVersion string) (snapshot
m.Settings = append(m.Settings, snapshot.Settings{Module: module, Values: scrub.Values(layer.Whole())})
}
}
// Who was issued a credential by this machine, by name only: never what the credential is, sealed or not.
issued, err := inv.SecretsFrom(ctx, n.Name)
if err != nil {
return snapshot.Facts{}, err
}
for _, g := range issued {
m.Grants = append(m.Grants, scrub.Grant(g.Name, g.Consumer, g.ConsumerModule, g.Local))
}
bound, err := inv.BindingsFor(ctx, n.Name)
if err != nil {
return snapshot.Facts{}, err
}
for consumer, byProvision := range bound {
for provision, c := range byProvision {
m.Bindings = append(m.Bindings, scrub.Binding(consumer, provision, c.Node, c.Module))
}
}
m.Declaration = declarationFacts(ctx, open, n.Name, gensErr, scrub)
if ctx.Err() != nil {
return snapshot.Facts{}, ctx.Err()
+26 -16
View File
@@ -9,12 +9,11 @@ import (
snapshot "git.novox.be/novox/mesh-sdk/go/facts"
)
// 1390 (novox/hq ADR 0456): the snapshot this controller writes is format 1, and the reader it shares with the build seat
// (mesh-sdk's go/facts) takes formats 1 and 2 and refuses one newer than it knows. The writer moves to format 2 in a release
// of its own. It failed first with the controller's own reader, which refused format 2 as newer than it knew.
func TestTheWriterWritesFormatOneAndTheReaderTakesBothFormats(t *testing.T) {
if writtenFormat != 1 {
t.Fatalf("the writer writes format %d, not 1", writtenFormat)
// 1394 (novox/hq ADR 0456 clause 2): the snapshot this controller writes is format 2, which carries each machine's grants and
// bindings, and the reader it shares with the build seat (mesh-sdk's go/facts) takes formats 1 and 2 and refuses one newer.
func TestTheWriterWritesFormatTwoAndTheReaderTakesBothFormats(t *testing.T) {
if writtenFormat != 2 {
t.Fatalf("the writer writes format %d, not 2", writtenFormat)
}
taken := time.Date(2026, 10, 20, 9, 0, 0, 0, time.UTC)
for format, want := range map[int]bool{1: true, 2: true, 3: false} {
@@ -32,10 +31,22 @@ func TestTheWriterWritesFormatOneAndTheReaderTakesBothFormats(t *testing.T) {
t.Errorf("format %d is not refused as newer: %v", format, err)
}
}
// What the writer composes carries no field of format 2: a reader of 1 and of 2 read it alike.
body, err := (snapshot.Facts{Format: writtenFormat, Taken: taken, Machines: []snapshot.Machine{{Name: "abcdef", Length: 6}}}).Encode()
if err != nil {
t.Fatal(err)
// Format 2 carries a machine's grants and bindings, and they are encoded in one order whatever order they were made in.
grants := []snapshot.Grant{{Provision: "s3-bucket", ConsumerNode: "bbbbbb", ConsumerModule: "files"},
{Provision: "s3-bucket", ConsumerNode: "aaaaaa", ConsumerModule: "files"}}
bindings := []snapshot.Binding{{ConsumerModule: "files", Provision: "s3-bucket", ProviderNode: "cccccc", ProviderModule: "objects"}}
encode := func(g []snapshot.Grant) []byte {
body, err := (snapshot.Facts{Format: writtenFormat, Taken: taken, Machines: []snapshot.Machine{{Name: "abcdef", Length: 6,
Grants: g, Bindings: bindings}}}).Encode()
if err != nil {
t.Fatal(err)
}
return body
}
body := encode(grants)
// 1394/AC3 (the bytes do not depend on the order the rows came in)
if reversed := encode([]snapshot.Grant{grants[1], grants[0]}); string(reversed) != string(body) {
t.Error("grants in another order encode to other bytes")
}
var raw struct {
Format int `json:"facts"`
@@ -44,15 +55,14 @@ func TestTheWriterWritesFormatOneAndTheReaderTakesBothFormats(t *testing.T) {
if err := json.Unmarshal(body, &raw); err != nil {
t.Fatal(err)
}
if raw.Format != 1 {
if raw.Format != 2 {
t.Errorf("the snapshot says format %d", raw.Format)
}
for _, m := range raw.Machines {
if _, has := m["grants"]; has {
t.Error("a format 1 snapshot carries grants")
}
if _, has := m["bindings"]; has {
t.Error("a format 1 snapshot carries bindings")
for _, key := range []string{"grants", "bindings"} {
if _, has := m[key]; !has {
t.Errorf("a format 2 snapshot carries no %s", key)
}
}
}
}
+22 -2
View File
@@ -9,6 +9,7 @@ import (
"git.novox.be/novox/mesh-sdk/go/catalogue"
snapshot "git.novox.be/novox/mesh-sdk/go/facts"
"github.com/novox/mesh-controller/internal/inventory"
)
// The facts snapshot (novox/hq to-be 45 §9): composed from the store, every machine under a pseudonym
@@ -55,6 +56,11 @@ func aMeshWithSecrets(t *testing.T) (*stores, []string) {
// Sealed, never kept plain (ADR 0004): the sealed blobs are what the store holds, and none may leave.
secrets = append(secrets, s.ForConsumer, s.ForProvider)
}
// A binding recorded for the consumer on the laptop, to the provider on the anchor.
if err := open.inventory.RecordBindings(ctx, "laptop", []inventory.Binding{{Machine: "laptop", Consumer: "files",
Provision: "s3-bucket", Provider: catalogue.Chosen{Node: "anchor", Module: "objects"}}}); err != nil {
t.Fatal(err)
}
return open, secrets
}
@@ -64,8 +70,8 @@ func TestTheFactsCarryNoSecretNoAddressAndNoName(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if f.Format != 1 {
t.Fatalf("the writer wrote format %d: it keeps format 1 until its own release moves it (issue 1390)", f.Format)
if f.Format != 2 {
t.Fatalf("the writer wrote format %d, not 2 (issue 1394)", f.Format)
}
body, err := f.Encode()
if err != nil {
@@ -89,6 +95,20 @@ func TestTheFactsCarryNoSecretNoAddressAndNoName(t *testing.T) {
}
}
// 1394/AC1: the provider's machine lists the grant, the consumer's machine the binding, machine names scrubbed.
anchorFacts, _ := f.Machine(snapshot.Pseudonym("machine", "anchor"))
laptopFacts, _ := f.Machine(snapshot.Pseudonym("machine", "laptop"))
wantGrant := snapshot.Grant{Provision: "s3-bucket", ConsumerNode: snapshot.Pseudonym("machine", "laptop"), ConsumerModule: "files"}
if len(anchorFacts.Grants) != 1 || anchorFacts.Grants[0] != wantGrant {
t.Errorf("the anchor's grants read as %+v, want %+v", anchorFacts.Grants, wantGrant)
}
wantBinding := snapshot.Binding{ConsumerModule: "files", Provision: "s3-bucket",
ProviderNode: snapshot.Pseudonym("machine", "anchor"), ProviderModule: "objects"}
if len(laptopFacts.Bindings) != 1 || laptopFacts.Bindings[0] != wantBinding {
t.Errorf("the laptop's bindings read as %+v, want %+v", laptopFacts.Bindings, wantBinding)
}
// 1394/AC2: the text scan above ran over a snapshot that holds those rows, so no sealed value is among them.
// What a check needs is there: every machine, its length, its modules, its declaration composing.
if len(f.Machines) != 2 || f.Longest() != len("laptop") {
t.Fatalf("machines %+v, longest %d", f.Machines, f.Longest())
+17 -12
View File
@@ -13,9 +13,11 @@ import (
// and streams, `reads` watching another module's bucket and never writing it, `consumes` hearing the named events,
// `invokes` calling the named tools, `uses` the seat operator-channel's by-caller subjects under the module's own name.
//
// The four manifests (the watchman's is the fourth, issue 1329) are copied from mesh-delivery's main at 9c15112d935a
// (modules/<module>/module.json, read 2026-10-20; the first three are unchanged since 6a23627426c6), whole, so a change
// to one of them there that changes its grant is a change to read here.
// The four manifests (the watchman's is the fourth, issue 1329): the bridge's and the indexer's are copied whole from
// mesh-delivery's branch feat/r4c2-new-keys at b7870ac9a6386467318454e9d5d4790987eb708d (modules/<module>/module.json,
// read 2026-10-20; issues 1448 and 1449 add the bridge's gitea_get_file and performed bucket and the indexer's manifests
// bucket), the teller's and the watchman's from mesh-delivery's main at 9c15112d935a, whole, so a change to one of them
// there that changes its grant is a change to read here.
const bridgeManifest = `{
"module": "mesh-delivery-forge-bridge",
"version": "1",
@@ -29,7 +31,8 @@ const bridgeManifest = `{
{ "name": "forge", "events": ["forge.**"] }
],
"state": [
{ "name": "heads", "history": 1 }
{ "name": "heads", "history": 1 },
{ "name": "performed", "history": 1 }
],
"reads": [
"mesh-delivery-gate.merges",
@@ -42,7 +45,8 @@ const bridgeManifest = `{
"gitea.gitea_note_append",
"gitea.gitea_add_comment",
"gitea.gitea_open_pulls",
"gitea.gitea_commit_trees"
"gitea.gitea_commit_trees",
"gitea.gitea_get_file"
],
"own-secrets": {
"broker": "${dir:state}/broker"
@@ -104,6 +108,7 @@ const indexerManifest = `{
],
"state": [
{ "name": "candidates", "history": 10 },
{ "name": "manifests", "history": 1 },
{ "name": "modules", "history": 20 }
],
"reads": [
@@ -437,8 +442,8 @@ func TestEachPartsCredentialGrantsExactlyItsDeclaration(t *testing.T) {
all := composed(t, partsRecords(t))
bridge, indexer, teller := all["mesh-delivery-forge-bridge@novox"], all["mesh-delivery-indexer@novox"], all["mesh-delivery-teller@novox"]
// The forge bridge: state heads, the stream FORGE, reads merges and owed, invokes seven tools of the forge.
if got, want := withPrefix(bridge.Publish, "$KV."), []string{"$KV.mesh-delivery-forge-bridge_heads.>"}; !slices.Equal(got, want) {
// The forge bridge: state heads and performed, the stream FORGE, reads merges and owed, invokes eight tools of the forge.
if got, want := withPrefix(bridge.Publish, "$KV."), []string{"$KV.mesh-delivery-forge-bridge_heads.>", "$KV.mesh-delivery-forge-bridge_performed.>"}; !slices.Equal(got, want) {
t.Errorf("the bridge writes %v; want exactly %v", got, want)
}
if got, want := eventsSaid(bridge), []string{"mesh.stream.mesh-delivery-forge-bridge.event.forge.>"}; !slices.Equal(got, want) {
@@ -458,7 +463,7 @@ func TestEachPartsCredentialGrantsExactlyItsDeclaration(t *testing.T) {
t.Error("the bridge hears an event of gitea's it does not consume")
}
called := []string{"gitea_add_comment", "gitea_commit_status", "gitea_commit_trees", "gitea_delivery_view",
"gitea_merge_pull_request", "gitea_note_append", "gitea_open_pulls"}
"gitea_get_file", "gitea_merge_pull_request", "gitea_note_append", "gitea_open_pulls"}
if got := toolsCalled(bridge, "gitea"); !slices.Equal(got, called) {
t.Errorf("the bridge calls %v; want exactly %v", got, called)
}
@@ -466,8 +471,8 @@ func TestEachPartsCredentialGrantsExactlyItsDeclaration(t *testing.T) {
t.Error("the bridge may call a tool of gitea's it does not invoke")
}
// The indexer: state candidates and modules, reads the controller's builds and the gate's source, hears FORGE.
if got, want := withPrefix(indexer.Publish, "$KV."), []string{"$KV.mesh-delivery-indexer_candidates.>", "$KV.mesh-delivery-indexer_modules.>"}; !slices.Equal(got, want) {
// The indexer: state candidates, manifests and modules, reads the controller's builds and the gate's source, hears FORGE.
if got, want := withPrefix(indexer.Publish, "$KV."), []string{"$KV.mesh-delivery-indexer_candidates.>", "$KV.mesh-delivery-indexer_manifests.>", "$KV.mesh-delivery-indexer_modules.>"}; !slices.Equal(got, want) {
t.Errorf("the indexer writes %v; want exactly %v", got, want)
}
if got := eventsSaid(indexer); len(got) != 0 {
@@ -509,7 +514,7 @@ func TestNoPartsCredentialReachesBeyondItsDeclaration(t *testing.T) {
all := composed(t, partsRecords(t))
parts := []string{"mesh-delivery-forge-bridge", "mesh-delivery-indexer", "mesh-delivery-teller"}
buckets := map[string][]string{
"mesh-delivery-forge-bridge": {"heads"}, "mesh-delivery-indexer": {"candidates", "modules"},
"mesh-delivery-forge-bridge": {"heads", "performed"}, "mesh-delivery-indexer": {"candidates", "manifests", "modules"},
"mesh-delivery-teller": {"answers"}, "mesh-delivery-gate": {"merges", "owed", "source", "asks", "deliveries"},
"mesh-controller": {"builds", "plans"},
}
@@ -570,7 +575,7 @@ func TestTheToolRunnerCarriesNoneOfTheDeliverysParts(t *testing.T) {
}}})
runner := all["runner@novox"]
buckets := map[string][]string{
"mesh-delivery-forge-bridge": {"heads"}, "mesh-delivery-indexer": {"candidates", "modules"},
"mesh-delivery-forge-bridge": {"heads", "performed"}, "mesh-delivery-indexer": {"candidates", "manifests", "modules"},
"mesh-delivery-teller": {"answers"}, "mesh-delivery-watchman": {"presence", "pulse", "stuck"},
}
for part, names := range buckets {