From b72af5e9184414a3dbccf9b4c3686fa638aace76 Mon Sep 17 00:00:00 2001 From: mesh-tools Date: Tue, 20 Oct 2026 09:28:09 +0000 Subject: [PATCH] Seal a core credential by the controller where the vault is silent, and refuse the bus machine where no user has a credential (issues 1385, 1383) Where the vault did not answer for a core credential and no box is kept, ADR 0396 has the controller seal it itself until the vault holds it: perform names those modules, and the send's declaration carries the controller's own seal in place of the stand-in vault box, as the controller's composition sent it. The machine that runs the bus is refused with busUserList's words where no user has a credential, as before; no other machine is. A machine whose recorded module cannot be kept is refused alone. --- cmd/mesh-controller/compose_perform.go | 36 ++++---- cmd/mesh-controller/compose_sdk.go | 116 ++++++++++++++++++++++++- 2 files changed, 135 insertions(+), 17 deletions(-) diff --git a/cmd/mesh-controller/compose_perform.go b/cmd/mesh-controller/compose_perform.go index 32d2f822..ba85b8c2 100644 --- a/cmd/mesh-controller/compose_perform.go +++ b/cmd/mesh-controller/compose_perform.go @@ -33,8 +33,15 @@ import ( // // perform returns whether it made or recorded anything: the reason the caller reads the stores again. func perform(ctx context.Context, open *stores, in *meshInput, node string, d compose.Declared) (bool, error) { + did, _, err := performSend(ctx, open, in, node, d) + return did, err +} + +// performSend is perform, and the core modules whose credentials the vault did not answer for with no box kept: the +// controller seals those itself until the vault holds them (core_box.go, ADR 0396), and the send carries that seal. +func performSend(ctx context.Context, open *stores, in *meshInput, node string, d compose.Declared) (bool, []string, error) { + did, sealedByController := false, []string(nil) inv := open.inventory - did := false // The seats passed on: recorded in the store with the controller's own hand act (ADR 0449 ยง4, issue 1317). if len(d.Passes.Passes) > 0 { @@ -72,7 +79,7 @@ func perform(ctx context.Context, open *stores, in *meshInput, node string, d co // down. The error is the one planFor met: the machine that needs it, and why none could be made. for _, p := range in.wouldMake { if err := makePair(ctx, inv, p); err != nil { - return did, fmt.Errorf("%s on %s needs %s from %s and no credential could be made for it: %w", + return did, nil, fmt.Errorf("%s on %s needs %s from %s and no credential could be made for it: %w", p.ConsumerModule, p.Consumer, p.Name, p.Provider, err) } did = true @@ -82,13 +89,13 @@ func perform(ctx context.Context, open *stores, in *meshInput, node string, d co if len(d.ForeseenPorts) > 0 { shelf, err := inv.Catalogue(ctx) if err != nil { - return did, err + return did, nil, err } for _, one := range d.ForeseenPorts { module, port, ok := strings.Cut(one, ":") wanted, perr := strconv.Atoi(port) if !ok || perr != nil { - return did, fmt.Errorf("the composition named a port the controller cannot read: %q", one) + return did, nil, fmt.Errorf("the composition named a port the controller cannot read: %q", one) } fixed := false for _, l := range nodeShelf(in, shelf, node)[module].Listens { @@ -97,7 +104,7 @@ func perform(ctx context.Context, open *stores, in *meshInput, node string, d co } } if _, err := inv.PortFor(ctx, node, module, wanted, fixed); err != nil { - return did, fmt.Errorf("%s needs %d reachable on %s and it could not be assigned: %w", module, wanted, node, err) + return did, nil, fmt.Errorf("%s needs %d reachable on %s and it could not be assigned: %w", module, wanted, node, err) } did = true } @@ -111,7 +118,7 @@ func perform(ctx context.Context, open *stores, in *meshInput, node string, d co continue // a grant's sealed copy, the certificate or the bus's list: made below or by nobody } if _, err := inv.SecretForModule(ctx, node, module, name); err != nil { - return did, err + return did, nil, err } did = true } @@ -121,7 +128,7 @@ func perform(ctx context.Context, open *stores, in *meshInput, node string, d co if ok && n.Stated && n.Contract >= compose.VaultBoxContract { resolved, err := compose.Resolve(in.Input, node) if err != nil { - return did, err + return did, nil, err } hands := n.Stated && n.Contract >= compose.HandedContract handing := catalogue.Rendering{Hands: hands, Provisioner: provisionerAccount(resolved.Plan)} @@ -129,30 +136,27 @@ func perform(ctx context.Context, open *stores, in *meshInput, node string, d co return handing.Handing(m) }, Allocating) if err != nil { - return did, err + return did, nil, err } if len(boxes.keep) > 0 { did = true } - // A core credential the vault did not answer for and no box was kept for is composed by the controller's own seal - // (core_box.go, ADR 0396); the composition over an Input has no such fallback, so the send is refused, said. - if len(boxes.byController) > 0 { - return did, fmt.Errorf("the vault did not answer for the core credentials of %s (%s) and no box of theirs is kept: "+ - "nothing is sent until it does", node, strings.Join(boxes.byController, ", ")) - } + // A core credential the vault did not answer for and no box was kept for is sealed by the controller itself until the + // vault holds it (core_box.go, ADR 0396): the composition stands in a box for it, and the send carries the seal. + sealedByController = boxes.byController } // The certificate of a machine that was never certified: issued and kept, with the authority when there is none yet. for _, one := range d.StoodIn { if strings.HasPrefix(one, "the certificate and the authority of ") { if _, _, err := certificateFor(ctx, open, node); err != nil { - return did, err + return did, nil, err } did = true break } } - return did, nil + return did, sealedByController, nil } // makePair makes one pair credential by the controller's own read of it: the credential for the pair, or the provider's one diff --git a/cmd/mesh-controller/compose_sdk.go b/cmd/mesh-controller/compose_sdk.go index bf0e9bff..2b08a559 100644 --- a/cmd/mesh-controller/compose_sdk.go +++ b/cmd/mesh-controller/compose_sdk.go @@ -1,16 +1,20 @@ package main import ( + "bytes" "context" + "encoding/json" "fmt" "io" "os" + "slices" "sort" "strings" "sync" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/compose" + "git.novox.be/novox/mesh-sdk/go/validate" "github.com/novox/mesh-controller/internal/inventory" ) @@ -92,6 +96,9 @@ func resolveNode(ctx context.Context, open *stores, node string) (catalogue.Reso if err != nil { return catalogue.Resolution{}, nil, err } + if err := in.refused[node]; err != nil { + return catalogue.Resolution{}, nil, err + } resolved, err := compose.Resolve(in.Input, node) if err != nil { if compose.IsUnresolvable(err) { @@ -123,6 +130,9 @@ func whoResolvesOn(ctx context.Context, open *stores, requirement string) (map[s on := map[string]bool{} refused := map[string]string{} for _, n := range in.sortedNodes() { + if err := in.refused[n.Name]; err != nil { + return nil, nil, fmt.Errorf("whether %s answers %q cannot be read: %w", n.Name, requirement, err) + } resolved, err := compose.Resolve(in.Input, n.Name) switch { case compose.IsUnresolvable(err): @@ -160,10 +170,16 @@ func composeQuestion(ctx context.Context, open *stores, node string, mode compos if err != nil { return sendable{}, nil, err } + if err := in.refused[node]; err != nil { + return sendable{}, nil, err + } d, problems, err := compose.ComposeIn(in.Input, node, mode) if err != nil { return sendable{}, nil, markUnresolvable(err) } + if err := refusedBus(in, node); err != nil { + return sendable{}, nil, err + } sayFound(ctx, d) out := sendableOf(d) if !withOrder { @@ -172,6 +188,25 @@ func composeQuestion(ctx context.Context, open *stores, node string, mode compos return out, problems, nil } +// refusedBus is why the node cannot be composed because it runs the bus and the bus's user list cannot be: no user has a +// credential, so the list would refuse every connection in the mesh (busUserList). The controller's own composition refused that +// machine with those words; no other machine is refused for it. nil when nothing refuses the node. +func refusedBus(in *meshInput, node string) error { + if in.busRefused == nil { + return nil + } + resolved, err := compose.Resolve(in.Input, node) + if err != nil { + return nil // it does not compose for its own reasons, which are said + } + for _, m := range resolved.Plan.Modules { + if m.BusUsers != "" && m.ClaimsSeat("mesh-broker") { + return in.busRefused + } + } + return nil +} + // markUnresolvable is an error of a node whose own set does not compose, as planFor marked it. func markUnresolvable(err error) error { if compose.IsUnresolvable(err) { @@ -191,15 +226,29 @@ func composeSend(ctx context.Context, open *stores, node string) (sendable, erro // composeSendValidated is composeSend and what the node-engine's validator refuses of the declaration it composed, for the // merge gate's two passes (composedAndValidated in its Allocating mode). func composeSendValidated(ctx context.Context, open *stores, node string) (sendable, []string, error) { + // The core modules whose credentials the controller sealed itself because the vault was silent and no box was kept + // (ADR 0396): the composition stands in a vault box for each, and a send carries the controller's own seal instead. + var byController []string for attempt := 0; ; attempt++ { in, err := inputFor(ctx, open) if err != nil { return sendable{}, nil, err } + if err := in.refused[node]; err != nil { + return sendable{}, nil, err + } d, problems, err := compose.ComposeIn(in.Input, node, compose.Foreseeing) if err != nil { return sendable{}, nil, markUnresolvable(err) } + if err := refusedBus(in, node); err != nil { + return sendable{}, nil, err + } + if len(byController) > 0 { + if d, problems, err = sealedByTheController(in, node, d, byController); err != nil { + return sendable{}, nil, err + } + } left := stillForeseen(d) resolved, rerr := compose.Resolve(in.Input, node) if rerr != nil { @@ -227,9 +276,11 @@ func composeSendValidated(ctx context.Context, open *stores, node string) (senda return sendable{}, nil, fmt.Errorf("%s was composed twice and still names what only a send makes (%s): nothing is sent "+ "that stands in for a value", node, strings.Join(left, "; ")) } - if _, err := perform(ctx, open, in, node, d); err != nil { + _, sealed, err := performSend(ctx, open, in, node, d) + if err != nil { return sendable{}, nil, err } + byController = sealed // What was made is in the stores now: the next read is a fresh one. forgetInput(ctx) } @@ -353,3 +404,66 @@ func handedPlans(in *meshInput, node string, resolved compose.Resolved) (map[str } return composed.Plans, nil } + +// sealedByTheController is the declaration with the controller's own seal in place of the vault's box for each core module the +// vault did not answer for and no box was kept for (core_box.go, ADR 0396): the file carries "sealed", as the controller's +// composition sent it, until the vault holds the credential. What the composition stood in for the box is replaced and no longer +// wanted; the validator is run again over the body. +func sealedByTheController(in *meshInput, node string, d compose.Declared, modules []string) (compose.Declared, []string, error) { + rendered, err := compose.Render(in.Input, node, compose.Foreseeing) + if err != nil { + return d, nil, err + } + stood, _ := compactBox(catalogue.ForeseenVaultBox) + var wanted []compose.CoreBoxWanted + var foreseen []string + for _, w := range d.CoreBoxesWanted { + if !slices.Contains(modules, w.Module) { + wanted = append(wanted, w) + continue + } + sealed := rendered.Rendering.Needed[w.Module][w.Name] + if sealed == "" { + return d, nil, fmt.Errorf("%s/%s on %s has no seal of the controller's to carry until the vault holds it", w.Module, w.Name, node) + } + path := in.Catalogue[w.Module].OwnSecrets[w.Name].Path + for _, r := range d.Resources { + box, isBox := r[catalogue.VaultSealedField] + if !isBox || r["id"] != w.Module+"."+catalogue.NeedID(w.Name) || r["path"] != path { + continue + } + if had, err := compactBox(box); err != nil || had != stood { + continue + } + delete(r, catalogue.VaultSealedField) + r["sealed"] = sealed + } + } + d.CoreBoxesWanted = wanted + for _, f := range d.Foreseen { + module, _, _ := strings.Cut(f, "/") + if !slices.Contains(modules, module) { + foreseen = append(foreseen, f) + } + } + d.Foreseen = foreseen + body, err := d.Body() + if err != nil { + return d, nil, err + } + return d, validate.Declaration(body), nil +} + +// compactBox is a value as the compact JSON it marshals to, so a box is compared by what it says and not by the type that +// holds it. +func compactBox(v any) (string, error) { + raw, err := json.Marshal(v) + if err != nil { + return "", err + } + var out bytes.Buffer + if err := json.Compact(&out, raw); err != nil { + return "", err + } + return out.String(), nil +}