diff --git a/modules/mesh-delivery-assigner/go.mod b/modules/mesh-delivery-assigner/go.mod index 39795ea8..c9e96c42 100644 --- a/modules/mesh-delivery-assigner/go.mod +++ b/modules/mesh-delivery-assigner/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-assigner go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-assigner/go.sum b/modules/mesh-delivery-assigner/go.sum index 6de5762d..21738611 100644 --- a/modules/mesh-delivery-assigner/go.sum +++ b/modules/mesh-delivery-assigner/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-builder/go.mod b/modules/mesh-delivery-builder/go.mod index 44661726..0f3ceef0 100644 --- a/modules/mesh-delivery-builder/go.mod +++ b/modules/mesh-delivery-builder/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-builder go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-builder/go.sum b/modules/mesh-delivery-builder/go.sum index 9f9f382b..08770b61 100644 --- a/modules/mesh-delivery-builder/go.sum +++ b/modules/mesh-delivery-builder/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-checker/go.mod b/modules/mesh-delivery-checker/go.mod index 3e680509..d74c83c7 100644 --- a/modules/mesh-delivery-checker/go.mod +++ b/modules/mesh-delivery-checker/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-checker go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-checker/go.sum b/modules/mesh-delivery-checker/go.sum index 9f9f382b..08770b61 100644 --- a/modules/mesh-delivery-checker/go.sum +++ b/modules/mesh-delivery-checker/go.sum @@ -1,7 +1,7 @@ git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a h1:oxgksCiq272/afXbUION/dxtAS5MPGnmyq6+AZQxmXg= git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-20261102193506-d221c633799a/go.mod h1:kpsX4Pgss5EeuBvel1zWa2ogj7MkR3jZso6oa/J0YUQ= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-forge-bridge/go.mod b/modules/mesh-delivery-forge-bridge/go.mod index f9796036..60ad48c5 100644 --- a/modules/mesh-delivery-forge-bridge/go.mod +++ b/modules/mesh-delivery-forge-bridge/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-forge-bridge go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-forge-bridge/go.sum b/modules/mesh-delivery-forge-bridge/go.sum index 6de5762d..21738611 100644 --- a/modules/mesh-delivery-forge-bridge/go.sum +++ b/modules/mesh-delivery-forge-bridge/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go index ba265a5a..6481181c 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer-tools/main.go @@ -1,6 +1,6 @@ // Command mesh-delivery-indexer-tools serves the indexer's doing tool (novox/hq issue 1945): the runtime launches it for a // call. indexer_take_in writes one value of the indexer's takeins state and waits for the indexer's answer there. The -// indexer is the other program: it serves no request subject (ADR 0437 §4); indexer_judge_again asks the same way, and +// indexer is the other program: it serves no request subject (ADR 0437 §4); indexer_judge asks the same way, and // indexer_verdicts reads the indexer's verdicts state. The read tools of the indexer's states are // mesh-delivery-view's (indexer_module, indexer_modules, indexer_candidate, indexer_candidates). package main @@ -56,7 +56,7 @@ func Tools(ask asker, now func() time.Time, read stdio.KeptState) []stdio.Tool { "unknown, and every reader holds the module as a refused one and keeps what runs; refused: the judge's problems), the " + "judge that made it and when. A module's name narrows it to that module. A waiting manifest is judged again at each " + "new indexer and refused only once the newest build of the indexer on the trunk still does not know the field; " + - "indexer_judge_again judges now.", + "indexer_judge judges now.", Input: map[string]any{"type": "object", "properties": map[string]any{ "module": map[string]any{"type": "string", "description": "only this module's verdicts"}, "all": map[string]any{"type": "boolean", "description": "list accepted verdicts too"}}}, @@ -151,7 +151,7 @@ func askTakeIn(s stdio.KeptState, bound time.Duration) asker { } func main() { - if err := stdio.Serve("", Tools(askTakeIn(stdio.State(index.TakeInsState), 25*time.Second), time.Now, stdio.State("verdicts"))); err != nil { + if err := stdio.Serve("", Tools(askTakeIn(stdio.State(index.TakeInsState), 25*time.Second), time.Now, stdio.State(index.VerdictsState))); err != nil { fmt.Fprintf(os.Stderr, "[mesh-delivery-indexer-tools] %v\n", err) os.Exit(1) } diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go index 5167a78b..1a17cd5d 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/main.go @@ -300,7 +300,8 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro Verdicts: awaitedStore{verdicts}, SayRefused: sayRefused(js, cred, logger.Printf), Predefined: awaitedStore{predefined}, ConfigCandidates: awaitedStore{configCandidates}, Logf: logger.Printf, Named: namedVia(calls.Call), Trunk: trunkVia(calls.Call), Contains: containsVia(calls.Call), Read: readVia(calls.Call), - ReadOpen: openVia(calls.Call), Packages: awaitedStore{packages}, Files: filesVia(calls.Call), GoSum: goSumVia(calls.Call), GoMod: goModVia(calls.Call)} + ReadOpen: openVia(calls.Call), Packages: awaitedStore{packages}, Files: filesVia(calls.Call), GoSum: goSumVia(calls.Call), GoMod: goModVia(calls.Call), + OwnBuild: ownBuildVia(calls.Call, cred.Node)} // The log's entries at the start are read before any fingerprint is computed or registered module indexed (below); each // new one indexes the module it names when that module is registered and the bucket holds none (registered.go). var started atomic.Bool diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/named_test.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/named_test.go index b3040ed6..8db30456 100644 --- a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/named_test.go +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/named_test.go @@ -50,6 +50,19 @@ func TestTheTrunkIsTheDirectoriesTheForgeListsTrees(t *testing.T) { // 1900/AC3 if _, has := gotArgs.(map[string]any)["paths"]; has { t.Error("the trunk asked for paths") } + // The build the indexer runs is the engine's word (Runs.FP of its composition verb), asked of its own machine. + var asked string + own := func(key string, body any) (json.RawMessage, error) { + asked = key + return json.RawMessage(`{"runs":{"fp":"src2:abc"}}`), nil + } + if fp, err := ownBuildVia(own, "n1")(context.Background()); err != nil || fp != "src2:abc" || asked != "seat:node-engine.composition@n1" { + t.Errorf("own build = %q %v asked %q", fp, err, asked) + } + none := func(string, any) (json.RawMessage, error) { return json.RawMessage(`{}`), nil } + if _, err := ownBuildVia(none, "n1")(context.Background()); err == nil { + t.Error("an engine that says nothing runs gave a build") + } } func TestReadViaReadsManifestsAndLocks(t *testing.T) { diff --git a/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/ownbuild.go b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/ownbuild.go new file mode 100644 index 00000000..79a31448 --- /dev/null +++ b/modules/mesh-delivery-indexer/cmd/mesh-delivery-indexer/ownbuild.go @@ -0,0 +1,37 @@ +package main + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + "git.novox.be/novox/mesh-sdk/go/delivery" +) + +// compositionVerb is the node-engine seat's read verb that says what runs of a module on a machine (its Runs.FP), asked of +// that machine's own engine as `seat:node-engine.composition@`. The indexer learns the build it runs from it, never +// from the trunk's value of itself or the haves bucket. +const compositionVerb = "seat:node-engine.composition" + +// ownBuildVia asks the engine of node which fingerprint of the indexer runs there. +func ownBuildVia(call toolCaller, node string) func(ctx context.Context) (string, error) { + return func(context.Context) (string, error) { + raw, err := call(compositionVerb+"@"+node, map[string]any{"module": delivery.IndexerModule}) + if err != nil { + return "", fmt.Errorf("%s: %w", compositionVerb, err) + } + var a struct { + Runs *struct { + FP string `json:"fp"` + } `json:"runs"` + } + if err := json.Unmarshal(raw, &a); err != nil { + return "", fmt.Errorf("the answer of %s is not a composition: %w", compositionVerb, err) + } + if a.Runs == nil || a.Runs.FP == "" { + return "", errors.New("the engine says no fingerprint of the indexer runs") + } + return a.Runs.FP, nil + } +} diff --git a/modules/mesh-delivery-indexer/go.mod b/modules/mesh-delivery-indexer/go.mod index 22d6195c..ff5091ac 100644 --- a/modules/mesh-delivery-indexer/go.mod +++ b/modules/mesh-delivery-indexer/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-indexer go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-indexer/go.sum b/modules/mesh-delivery-indexer/go.sum index 6de5762d..21738611 100644 --- a/modules/mesh-delivery-indexer/go.sum +++ b/modules/mesh-delivery-indexer/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-indexer/internal/index/candidates.go b/modules/mesh-delivery-indexer/internal/index/candidates.go index c4100f06..2d8fbad3 100644 --- a/modules/mesh-delivery-indexer/internal/index/candidates.go +++ b/modules/mesh-delivery-indexer/internal/index/candidates.go @@ -92,24 +92,27 @@ type Indexer struct { // closed message the indexer missed heals at the next one (issue 1126); nil asks none. ReadOpen func(ctx context.Context) ([]delivery.OpenPull, error) - mu sync.Mutex // guards source, toolchains and previews, which the watches and the forge messages all reach - source sourceNow - previews map[string]delivery.Preview // by previews key (previews.go) - toolchains map[string]map[string]delivery.Toolchain // language, then node (toolchains.go) - tried map[string]bool // repository, commit and directory a registered module's manifest was tried at (registered.go) - unread map[string]bool // registered modules whose last read the forge could not answer, read again by RetryUnread - moving map[string]delivery.Forge // by modules key: a merge naming a module another repository holds, waiting for that trunk to drop it (moves.go) - retrying atomic.Bool // a RetryUnread runs in the background (kickRetry) - retryAgain atomic.Bool // an event asked for one more RetryUnread after the running one - retryWG sync.WaitGroup // the background RetryUnread, for tests to wait on - recompute sync.Mutex // one Recompute at a time - untaken map[string]string // repository to the latest merge applied while the taken state was missing (taken.go) - filling atomic.Bool // a FillTaken runs in the background (KickFill) - fillAgain atomic.Bool // a kick arrived while it ran - atHeads map[string]map[string]delivery.PublishedPackages // a member head's published Go modules, by repository@head (candidatePublished) - ownMu sync.Mutex // guards ownFP - ownFP string // the indexer's own fingerprint its modules value named at the start pass (ownBuildIsNewest) - judgePending atomic.Bool // the start's judge pass failed: judged again at the next message (verdicts.go) + mu sync.Mutex // guards source, toolchains and previews, which the watches and the forge messages all reach + source sourceNow + previews map[string]delivery.Preview // by previews key (previews.go) + toolchains map[string]map[string]delivery.Toolchain // language, then node (toolchains.go) + tried map[string]bool // repository, commit and directory a registered module's manifest was tried at (registered.go) + unread map[string]bool // registered modules whose last read the forge could not answer, read again by RetryUnread + moving map[string]delivery.Forge // by modules key: a merge naming a module another repository holds, waiting for that trunk to drop it (moves.go) + retrying atomic.Bool // a RetryUnread runs in the background (kickRetry) + retryAgain atomic.Bool // an event asked for one more RetryUnread after the running one + retryWG sync.WaitGroup // the background RetryUnread, for tests to wait on + recompute sync.Mutex // one Recompute at a time + untaken map[string]string // repository to the latest merge applied while the taken state was missing (taken.go) + filling atomic.Bool // a FillTaken runs in the background (KickFill) + fillAgain atomic.Bool // a kick arrived while it ran + atHeads map[string]map[string]delivery.PublishedPackages // a member head's published Go modules, by repository@head (candidatePublished) + // OwnBuild answers the fingerprint of the build this program runs, from the node-engine's seat; nil knows none. + OwnBuild func(ctx context.Context) (string, error) + judgeMu sync.Mutex // one judge pass at a time (verdicts.go) + ownMu sync.Mutex // guards ownFP + ownFP string // the fingerprint of the build this program runs, asked at the start pass (learnOwnBuild) + judgePending atomic.Bool // the start's judge pass failed: judged again at the next message (verdicts.go) } func (ix *Indexer) logf(format string, args ...any) { diff --git a/modules/mesh-delivery-indexer/internal/index/verdicts.go b/modules/mesh-delivery-indexer/internal/index/verdicts.go index cbcd278c..f088e326 100644 --- a/modules/mesh-delivery-indexer/internal/index/verdicts.go +++ b/modules/mesh-delivery-indexer/internal/index/verdicts.go @@ -24,9 +24,11 @@ import ( // changed (a new indexer) judges every manifest again, and after every merge it takes in, since a rule between manifests // can turn on any of them. Nothing polls. -// VerdictsBucket is the indexer's state of verdicts. +// VerdictsBucket is the indexer's state of verdicts; VerdictsState is its local name, as the tools bundle reads it. var VerdictsBucket = delivery.VerdictsBucket +const VerdictsState = "verdicts" + // Judged is what one pass of the judge did: the manifests judged, the verdicts written, and the modules refused. type Judged struct { Judged, Written int @@ -47,6 +49,10 @@ type Judged struct { // watchman raises as manifest-outgrown. A manifest in fresh (the digests a merge just took in) with no verdict yet, and // one already refused, is judged in full. func (ix *Indexer) JudgeAll(ctx context.Context, fresh map[string]bool) (Judged, error) { + // A merge, the start pass and indexer_judge may all judge at once: one pass at a time, so none writes a verdict from + // modules another is rewriting. + ix.judgeMu.Lock() + defer ix.judgeMu.Unlock() var out Judged if ix.Verdicts == nil || ix.Modules == nil || ix.Manifests == nil { return out, nil @@ -146,11 +152,7 @@ func (ix *Indexer) JudgeAll(ctx context.Context, fresh map[string]bool) (Judged, // JudgeAtStart is JudgeAll of every manifest held, none fresh, as the indexer starts (a judge that changed judges every // one again); one that fails is judged again at the next message or source revision heard (kickJudge). func (ix *Indexer) JudgeAtStart(ctx context.Context) (Judged, error) { - if held, err := ix.heldModules(ctx); err == nil { - ix.ownMu.Lock() - ix.ownFP = held[delivery.IndexerModule].FP2 // this program's build, until the trunk names another - ix.ownMu.Unlock() - } + ix.learnOwnBuild(ctx) j, err := ix.JudgeAll(ctx, nil) ix.judgePending.Store(err != nil) return j, err @@ -203,11 +205,26 @@ func (ix *Indexer) writeVerdict(ctx context.Context, digest string, v delivery.M return wrote, err } -// ownBuildIsNewest says whether the build of the indexer this program is, is the newest the trunk holds: nothing gives a -// module the fingerprint it was built from, so it is the one the indexer's own modules value named when this program -// judged at its start (ownFP), and the trunk holds no newer one while that value still names it. A newer build of the -// indexer on the trunk, as one that pins a changed mesh-sdk always is, ends it at the trunk's merge, before it runs; false -// when the value names none or this program judged no start pass. +// learnOwnBuild asks where the indexer's program is told which build it runs (OwnBuild: the node-engine's seat, never the +// trunk's value of the indexer, which names a build that may be merged and not delivered). Asked at the start pass; where +// it is not answered the build stays unknown and every unknown field waits, which is the safe side. +func (ix *Indexer) learnOwnBuild(ctx context.Context) { + if ix.OwnBuild == nil { + return + } + fp, err := ix.OwnBuild(ctx) + if err != nil || fp == "" { + ix.logf("judge: the build this program runs is not known (%v), so a manifest with a field it does not know waits and is not refused", err) + return + } + ix.ownMu.Lock() + ix.ownFP = fp + ix.ownMu.Unlock() +} + +// ownBuildIsNewest says whether the build of the indexer this program runs (ownFP, from the node-engine) is the newest the +// trunk holds: the indexer's own modules value names it. A newer build merged and not yet delivered ends it at the trunk's +// merge, as one that pins a changed mesh-sdk always is; false when either is unknown. func (ix *Indexer) ownBuildIsNewest(held map[string]delivery.Module) bool { fp := held[delivery.IndexerModule].FP2 if fp == "" { @@ -277,10 +294,10 @@ func (ix *Indexer) predefinedList(ctx context.Context) catalogue.PredefinedList } // The tools that read and steer the verdicts (the instruction "tools are how a module is spoken to"): indexer_verdicts -// lists the manifests that wait or are refused, and indexer_judge_again judges one module again now. +// lists the manifests that wait or are refused, and indexer_judge judges one module again now. const ( VerdictsTool = "indexer_verdicts" - JudgeAgainTool = "indexer_judge_again" + JudgeAgainTool = "indexer_judge" judgeAgainPrefix = "judge." ) @@ -331,7 +348,7 @@ func ListVerdicts(keys []string, get func(key string) ([]byte, error), module st } // JudgeAgain judges the manifests again now, as at the start, and answers how the module's stands: the tool -// indexer_judge_again. A module the modules state does not hold, or whose manifest is not in the manifests state, is +// indexer_judge. A module the modules state does not hold, or whose manifest is not in the manifests state, is // answered so. func (ix *Indexer) JudgeAgain(ctx context.Context, module string) TakenIn { out := TakenIn{Module: module} @@ -367,7 +384,7 @@ func (ix *Indexer) JudgeAgain(ctx context.Context, module string) TakenIn { return out } -// NewJudgeAgain is the value indexer_judge_again writes for a module: its key in the takeins state and the value. +// NewJudgeAgain is the value indexer_judge writes for a module: its key in the takeins state and the value. func NewJudgeAgain(module, why, by string, at time.Time) (string, []byte, error) { key, data, err := NewTakeIn(module, why, by, at) if err != nil { diff --git a/modules/mesh-delivery-indexer/internal/index/verdicts_test.go b/modules/mesh-delivery-indexer/internal/index/verdicts_test.go index 33166028..866d02c1 100644 --- a/modules/mesh-delivery-indexer/internal/index/verdicts_test.go +++ b/modules/mesh-delivery-indexer/internal/index/verdicts_test.go @@ -2,6 +2,7 @@ package index import ( "context" + "errors" "strings" "testing" "time" @@ -138,6 +139,19 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { !strings.Contains(m.Problems[0], "does not know yet") { t.Fatalf("modules.later carries %+v", m) } + // The build this program runs is asked of the engine, and the trunk moving before the start pass changes nothing: the + // trunk's value of the indexer names a newer build than the one the engine says runs, so the field still waits. + ix.ownFP = "" + ix.OwnBuild = func(context.Context) (string, error) { return ownFP, nil } + if _, err := ix.JudgeAtStart(ctx); err != nil || !verdict(later).Waits() { + t.Fatalf("the trunk moved before the start pass: %v %+v", err, verdict(later)) + } + ix.OwnBuild = func(context.Context) (string, error) { return "", errors.New("the engine does not answer") } + ix.ownFP = "" + if _, err := ix.JudgeAtStart(ctx); err != nil || !verdict(later).Waits() { + t.Fatalf("an unknown build refused: %v %+v", err, verdict(later)) + } + ix.ownFP = ownFP // A renamed field is a known mistake: refused at once. renamed := hold("renamed", `{"module":"renamed","version":"1","needs":{"x":"y"}}`) if j, err := ix.JudgeAll(ctx, map[string]bool{renamed: true}); err != nil || len(j.Waiting) != 1 || !verdict(renamed).Refused() { @@ -151,7 +165,7 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { if m := module("later"); m.Verdict != delivery.VerdictRefused || m.VerdictOf("later").Waits() || len(m.Unknown) != 0 { t.Fatalf("modules.later after the refusal carries %+v", m) } - // The tools: indexer_verdicts lists the verdicts that wait or are refused (and every one with all), indexer_judge_again + // The tools: indexer_verdicts lists the verdicts that wait or are refused (and every one with all), indexer_judge // judges now and says how the module's verdict stands. keys, _ := ix.Verdicts.Keys(ctx) get := func(key string) ([]byte, error) { v, _, _, err := ix.Verdicts.Get(ctx, key); return v, err } @@ -163,13 +177,13 @@ func TestTheIndexerJudgesEveryManifestOnce(t *testing.T) { } ix.ownFP = ownFP if got := ix.JudgeAgain(ctx, "later"); !got.Stored || !strings.Contains(got.Answer, "waits") || !strings.Contains(got.Answer, "a-field-from-later") { - t.Fatalf("indexer_judge_again: %+v", got) + t.Fatalf("indexer_judge: %+v", got) } if rows, _ := ListVerdicts(keys, get, "later", false); len(rows) != 1 { t.Fatalf("indexer_verdicts of later after: %+v", rows) } if got := ix.JudgeAgain(ctx, "nobody"); got.Stored || !strings.Contains(got.Answer, "holds no manifest") { - t.Fatalf("indexer_judge_again of nobody: %+v", got) + t.Fatalf("indexer_judge of nobody: %+v", got) } if key, value, err := NewJudgeAgain("later", "why", "me", time.Now()); err != nil || key != "judge.later" { t.Fatalf("the ask: %q %v", key, err) diff --git a/modules/mesh-delivery-indexer/manifest_test.go b/modules/mesh-delivery-indexer/manifest_test.go index bb9343db..73d2c87c 100644 --- a/modules/mesh-delivery-indexer/manifest_test.go +++ b/modules/mesh-delivery-indexer/manifest_test.go @@ -109,13 +109,13 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build // Three invokes: the file of a registered module's manifest and lock files (registered.go), the forge's trees (the named paths' objects, since the indexer is the one reader of a manifest's paths: // ADR 0499 clauses 2 and 3, ADR 0495 clause 1; and the module directories of the trunk, issue 1900) and whether a // commit is in another (issue 1900). - if want := []string{"gitea.gitea_commit_trees", "gitea.gitea_contains", "gitea.gitea_get_file", "gitea.gitea_open_pulls"}; !reflect.DeepEqual(m.Invokes, want) { + if want := []string{"gitea.gitea_commit_trees", "gitea.gitea_contains", "gitea.gitea_get_file", "gitea.gitea_open_pulls", "seat:node-engine.composition"}; !reflect.DeepEqual(m.Invokes, want) { t.Errorf("invokes = %v, want %v", m.Invokes, want) } // 1945: indexer_take_in, served by its tools bundle through the takeins state; the process serves none. The same bundle // reads the verdicts state (indexer_verdicts) and asks the indexer to judge again through the takeins state - // (indexer_judge_again; issue 774, clause 3). - if want := []string{"indexer_take_in", "indexer_verdicts", "indexer_judge_again"}; !reflect.DeepEqual(m.Tools, want) { + // (indexer_judge; issue 774, clause 3). + if want := []string{"indexer_take_in", "indexer_verdicts", "indexer_judge"}; !reflect.DeepEqual(m.Tools, want) { t.Errorf("tools = %v, want %v", m.Tools, want) } // No streams, no bus, no seat (ADR 0437 §4); it says no event. diff --git a/modules/mesh-delivery-indexer/module.json b/modules/mesh-delivery-indexer/module.json index 2f4bc361..02c50b68 100644 --- a/modules/mesh-delivery-indexer/module.json +++ b/modules/mesh-delivery-indexer/module.json @@ -5,7 +5,7 @@ "tools": [ "indexer_take_in", "indexer_verdicts", - "indexer_judge_again" + "indexer_judge" ], "consumes": [ "mesh-build-go.toolchain.reported", @@ -40,7 +40,8 @@ "gitea.gitea_commit_trees", "gitea.gitea_contains", "gitea.gitea_get_file", - "gitea.gitea_open_pulls" + "gitea.gitea_open_pulls", + "seat:node-engine.composition" ], "own-secrets": { "broker": "${dir:state}/broker" diff --git a/modules/mesh-delivery-teller/go.mod b/modules/mesh-delivery-teller/go.mod index 721c8ed5..821d3f97 100644 --- a/modules/mesh-delivery-teller/go.mod +++ b/modules/mesh-delivery-teller/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-teller go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-teller/go.sum b/modules/mesh-delivery-teller/go.sum index 6de5762d..21738611 100644 --- a/modules/mesh-delivery-teller/go.sum +++ b/modules/mesh-delivery-teller/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-view/go.mod b/modules/mesh-delivery-view/go.mod index 499bb922..cf0fb7c4 100644 --- a/modules/mesh-delivery-view/go.mod +++ b/modules/mesh-delivery-view/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-view go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-view/go.sum b/modules/mesh-delivery-view/go.sum index 6de5762d..21738611 100644 --- a/modules/mesh-delivery-view/go.sum +++ b/modules/mesh-delivery-view/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-delivery-watchman/go.mod b/modules/mesh-delivery-watchman/go.mod index bd9b3bf2..66433c0d 100644 --- a/modules/mesh-delivery-watchman/go.mod +++ b/modules/mesh-delivery-watchman/go.mod @@ -3,7 +3,7 @@ module mesh-delivery-watchman go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-delivery-watchman/go.sum b/modules/mesh-delivery-watchman/go.sum index 6e5f2993..1c98fd23 100644 --- a/modules/mesh-delivery-watchman/go.sum +++ b/modules/mesh-delivery-watchman/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op h1:1BOWQJweNyvZMlpAHXGLiZQn9S+QXGcz3xh94lC0w6E= github.com/antithesishq/antithesis-sdk-go v0.8.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-enrolment/go.mod b/modules/mesh-enrolment/go.mod index 3ea0d929..678fdaa3 100644 --- a/modules/mesh-enrolment/go.mod +++ b/modules/mesh-enrolment/go.mod @@ -3,7 +3,7 @@ module mesh-enrolment go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-enrolment/go.sum b/modules/mesh-enrolment/go.sum index 6de5762d..21738611 100644 --- a/modules/mesh-enrolment/go.sum +++ b/modules/mesh-enrolment/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-grants/go.mod b/modules/mesh-grants/go.mod index 20f6a617..7ecad1b4 100644 --- a/modules/mesh-grants/go.mod +++ b/modules/mesh-grants/go.mod @@ -3,7 +3,7 @@ module mesh-grants go 1.26.0 require ( - git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 + git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats.go v1.54.0 ) diff --git a/modules/mesh-grants/go.sum b/modules/mesh-grants/go.sum index 6de5762d..21738611 100644 --- a/modules/mesh-grants/go.sum +++ b/modules/mesh-grants/go.sum @@ -1,5 +1,5 @@ -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004 h1:RBb1K9hAGBB58erfTx02hMVVE1s8TXL4YI4iCB/0cKU= -git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103115157-8e1043ed4004/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2 h1:MOuutmbKBL1GtTZU9W1+ja/K7TL05EFU6hEt3Swf/oE= +git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261103120252-42bd7b802ac2/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0= github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI= github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo= diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 51dfa914..217783e8 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -739,6 +739,53 @@ func TestGrants(t *testing.T) { } }) + // ADR 0601 clause 3: a held module's seats, hearings and reads are composed from the manifest its running build was judged + // on, so a principal that claims a seat it defines keeps its grants while its trunk manifest is refused or waits. + t.Run("a held module's seat is composed from the manifest its running build was judged on", func(t *testing.T) { + const router = `{"module":"messenger","version":"1","runs-as":"messenger","resources":[{"id":"account","type":"user","name":"messenger","shell":"/usr/bin/nologin","home":"/var/lib/messenger"}],"own-secrets":{"broker":"/var/lib/m/broker"},"secrets-owner":"messenger",` + + `"seats":[{"name":"operator-channel","scope":"mesh","serves":["open"]},{"name":"channel","scope":"mesh","kinded":true,"accepts":["show"]}],` + + `"claims":[{"name":"operator-channel","scope":"mesh","serves":["open"]}]}` + const telegram = `{"module":"telegram","version":"1","runs-as":"telegram","resources":[{"id":"account","type":"user","name":"telegram","shell":"/usr/bin/nologin","home":"/var/lib/telegram"}],"own-secrets":{"broker":"/var/lib/t/broker"},"secrets-owner":"telegram",` + + `"claims":[{"name":"channel","scope":"mesh","kind":"telegram","capabilities":["deliver","verified-sender"]}]}` + setup := func(held, runs bool) *fakeBus { + b := fixture() + b.manifestOf("messenger", router) + b.manifestOf("telegram", telegram) + b.assign("a", "messenger") + b.assign("b", "telegram") + if held { + // The trunk's manifest waits for a field; the build that runs was judged on the manifest before. + b.put(grants.ReadModules, "messenger", delivery.Module{V: delivery.Version, FP2: "src2:" + strings.Repeat("ab", 32), + Manifest: delivery.ManifestDigest([]byte(`{"module":"messenger","version":"2"}`)), Repository: "novox/x", Commit: "c", + Verdict: delivery.VerdictRefused, Problems: []string{"it waits"}, Unknown: []string{"core-credentials"}}) + if runs { + fp := "src2:" + strings.Repeat("cd", 32) + digest := delivery.ManifestDigest([]byte(router)) + b.put(grants.ReadManifests, digest, delivery.Manifest{V: delivery.Version, Module: "messenger", JSON: router}) + b.put(grants.ReadHave, "a.messenger", delivery.Have{V: delivery.Version, Phase: delivery.PhaseRunning, Health: delivery.HealthOK, FP: fp}) + key, _ := delivery.BuilderBuildKey("messenger", fp) + b.put(grants.ReadBuilds, key, map[string]any{"v": delivery.Version, "state": "done", "manifest": digest}) + } + } + return b + } + grantsOf := func(b *fakeBus) string { + for _, u := range shadowOf(t, b) { + if u.User == "b.telegram" { + return strings.Join(u.Publish, ",") + "|" + strings.Join(u.Subscribe, ",") + } + } + return "" + } + accepted, held, unseen := grantsOf(setup(false, false)), grantsOf(setup(true, true)), grantsOf(setup(true, false)) + if accepted == "" || held != accepted { + t.Errorf("a caller of a held module's seat lost grants:\n%s\nwas\n%s", held, accepted) + } + if unseen == accepted { + t.Errorf("the row proves nothing: the grants are the same without the held module's running manifest") + } + }) + t.Run("root-free: each engine's word fills the composition, failing closed (issue 1945)", func(t *testing.T) { const router = `{"module":"messenger","version":"1","runs-as":"messenger","resources":[{"id":"account","type":"user","name":"messenger","shell":"/usr/bin/nologin","home":"/var/lib/messenger"}],"own-secrets":{"broker":"/var/lib/m/broker"},"secrets-owner":"messenger",` + `"seats":[{"name":"operator-channel","scope":"mesh","serves":["open"]},{"name":"channel","scope":"mesh","kinded":true,"accepts":["show"]}],` + diff --git a/modules/mesh-grants/internal/grants/inputs.go b/modules/mesh-grants/internal/grants/inputs.go index 64f36d08..029a37e5 100644 --- a/modules/mesh-grants/internal/grants/inputs.go +++ b/modules/mesh-grants/internal/grants/inputs.go @@ -131,7 +131,11 @@ type Inputs struct { // Refused is every manifest the indexer holds that the part cannot read or mesh-sdk's reader refuses, assigned or // not, each with why: its module counts as placed nowhere, so it is said loudly, never passed over (issue 1945). Refused []Gap - Gaps []Gap + // Held is, by module, the manifest a held module's running build was judged on (its have's fingerprint, the builder's + // build of it): what the seats it defines, the streams others hear from it and its reads are composed from while its + // trunk manifest is refused or waits, so no other principal loses a grant to it (ADR 0601 clause 3). + Held map[string]catalogue.Manifest + Gaps []Gap } // HeldHash is one key of the vault's bus-accounts: the hash, and what it is of. The hash is never said by the part, and @@ -440,6 +444,7 @@ func ReadInputs(r Reader, cache *Manifests) Inputs { sort.Strings(fromTrunk) in.Gaps = append(in.Gaps, Gap{Input: "wanted manifests", Why: "no build record names the manifest at the wanted fingerprint, so the trunk's is used: " + strings.Join(fromTrunk, ", ")}) } + in.Held = heldManifests(r, in, cache) in.Hashes = readHashes(r, &in.Gaps) // The seat mesh-enrolment's keys are every token since the controller's end (issue 1126). in.Enrolling, _ = readEnrolling(r, &in.Gaps) @@ -573,3 +578,59 @@ func (in Inputs) ReadFailures() []string { } return out } + +// heldManifests are the manifests the running builds of the held modules (in.Refused) were judged on, by module: the have +// of the module on a node names the fingerprint that runs, and the builder's build of it the manifest. A module that runs +// nowhere, or whose build or manifest cannot be read, has none, and that is said as a gap. +func heldManifests(r Reader, in Inputs, cache *Manifests) map[string]catalogue.Manifest { + out := map[string]catalogue.Manifest{} + for _, g := range in.Refused { + module, ok := strings.CutPrefix(g.Input, "manifest of ") + if !ok { + continue + } + for _, node := range in.Nodes { + raw, found, err := r.Get(ReadHave, node+"."+module) + if err != nil || !found { + continue + } + h, err := delivery.DecodeHave(raw) + if err != nil || h.FP == "" { + continue + } + bk, err := delivery.BuilderBuildKey(module, h.FP) + if err != nil { + continue + } + braw, found, err := r.Get(ReadBuilds, bk) + if err != nil || !found { + continue + } + b, err := delivery.DecodeBuilderBuild(braw) + if err != nil || b.Manifest == "" { + continue + } + if m, err := cache.get(r, b.Manifest); err == nil { + out[module] = m + break + } + } + } + return out +} + +// declared are the manifests the seats, hearings and reads are composed from: the trunk's, and for a held module the +// manifest its running build was judged on (Held). +func (in Inputs) declared() map[string]catalogue.Manifest { + if len(in.Held) == 0 { + return in.Manifests + } + out := make(map[string]catalogue.Manifest, len(in.Manifests)+len(in.Held)) + for m, man := range in.Held { + out[m] = man + } + for m, man := range in.Manifests { + out[m] = man + } + return out +} diff --git a/modules/mesh-grants/internal/grants/records.go b/modules/mesh-grants/internal/grants/records.go index 2b9dcc71..5e338844 100644 --- a/modules/mesh-grants/internal/grants/records.go +++ b/modules/mesh-grants/internal/grants/records.go @@ -27,7 +27,8 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) { var gaps []Gap seats := map[string]catalogue.SeatDeclaration{} declarers := map[string]string{} - for _, m := range in.Manifests { + declared := in.declared() + for _, m := range declared { for _, s := range m.DefinesSeats { seats[s.Name] = s declarers[s.Name] = m.Module @@ -79,7 +80,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) { for _, sr := range catalogue.ReadsGranted(m, here, node) { d.KeyedReads = append(d.KeyedReads, buscompose.KeyedRead{Read: sr.Read, Key: sr.Key}) } - d.Hears, d.HearsAStream, d.Consumes = hearsOf(m, in.Manifests, node, values, d.Consumes) + d.Hears, d.HearsAStream, d.Consumes = hearsOf(m, declared, node, values, d.Consumes) d.Reads, d.ReadsObjects, d.ReadsRefused = readsOfObjects(m.Module, node, d.Reads, d.ReadsRefused, in.Manifests, values) return d } @@ -101,7 +102,7 @@ func records(in Inputs, union bool) (buscompose.Records, []Gap) { } // The node-engine's own manifest, placed on every node (ADR 0563): its state, logs and streams alone, created by this // part; it adds no principal, since each node's engine speaks as the node's account. - if m, ok := in.Manifests[buscompose.EngineModule]; ok { + if m, ok := declared[buscompose.EngineModule]; ok { out.Engine = &buscompose.Declared{Module: m.Module, State: bucketsFor(m, in.Nodes), Logs: logsOf(m), Streams: streamsOf(m)} } return out, gaps