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.
This commit is contained in:
2026-10-20 09:28:09 +00:00
parent 09229d85cb
commit b72af5e918
2 changed files with 135 additions and 17 deletions
+20 -16
View File
@@ -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
+115 -1
View File
@@ -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
}