Every caller composes through mesh-sdk's go/compose since issues 1386 to 1388; the controller's second copy of the composition is dead code. The parent of this commit isfb556d6b, whose try (build-1792493691274712799, 17m28s, 2026-10-20 11:12 UTC) ran the equality test green on the live facts: every machine was sent and compared both ways (ADR 0453, issue 1384). Deleted, 40 functions: in plan.go planFor, declarationWith, renderingFor, grantsFor, theRestOfTheMesh and their helpers (boundOfGrant, busCredentialIssued, carriedBuilds, carriedOn, composeBusUsers, engineJudgesRoot, engineReadsHealth, engineTakesHanded, takesHanded, givenUnderAnOldKey, issuedToHolder, keyFor, licencesFor, managerPublicKeyFor, movedProvider, providerMoved, providerModuleOf, sayLeftOut, seatsOn, wireBuilds, zonesInTheMesh); in network.go generators, whoResolves, namesInTheMesh, onThePrivateNetwork; composedAndValidated, adoptionFor, adoptionOf, engineOpensVaultBox, namedModules, localNetworksOf, keepRecorded, passSeatsOn, theMeshPassesSeatsOn, assignmentsOf. Choosing (Reading, Allocating, Foreseeing) is gone: coreBoxesFor and coreBoxesOf take a plain allocating flag. composeForPush no longer takes the private network it ignored. The seat pass of a send is performSeatPasses, over the passes the composition names. Measured: cmd/mesh-controller non-test code 67,643 -> 65,821 lines (-1,822), its tests 57,691 -> 57,286, with the integration part of the equality test removed here because it calls the deleted functions. Not the issue's 3,000: that counts all of plan.go (2,192 lines), of which the callers, the verbs, the perform step and the sends remain (sendable, recorded builds, the held-back and ready-node logic, the plan verb, the gate's trial); and the 37 functions a name walk lists as unreachable are called through interfaces and method sets and are kept. Tests dropped, each with the mesh-sdk go/compose test that holds its cases: - seat_pass_test.go TestACompositionWithTwoClaimantsRecordsNoneAndSaysSo, TestACompositionChangesNothingWhileTheHolderIsAssignedOrNobodyClaims, TestAHolderAssignedButNotOnTheShelfIsNotTakenForGone: rest_test.go TestNoSeatIsPassedForTwoClaimantsAnAssignedHolderOrNoClaimant, TestAHolderAssignedButNotOnTheShelfIsNotTakenForGone, TestASeatWhoseHolderIsAssignedNowhereGoesToItsOnlyClaimant. TestACompositionPassesTheSeatAndWritesOneHandActByTheController and TestALostPassWritesNoActAndAFailedOneLeavesTheRecord stay as tests of the send's step (performSeatPasses); TestAThrowawayMeshPassesTheSeatInMemoryAndWritesNothing stays as TestAThrowawayMeshWritesNoHandActForAPass. - seat_pass_test.go TestNoSeatIsPassedWhenAMachinesAssignmentsCouldNotBeRead: no counterpart, and none is needed: the adapter reads every machine's assignments for the Input and a machine whose read fails fails the Input, so no seat is passed on a partial read. - network_test.go TestOnlyAMachineOnThePrivateNetworkIsNamed: network_test.go TestANodeNotOnThePrivateNetworkGetsNoAddressAndNoNetwork. - held_back_test.go TestASendCarriesTheCurrentBuildsAndALeftOutModuleKeepsItsOwn: node_test.go TestAModuleLeftOutKeepsTheBuildItWasLastSent (and compose_sdk_test.go's table, now literal). - modules_wire_test.go TestTheBuildOfACarriedCommitIsTheAssignmentsOrTheOneBuiltFromIt: node_test.go TestTheBodyCarriesTheOrderTheInputGivesAndTheBuildsOnlyToAnEngineThatReadsThem. - compose_equal_integration_test.go: the old path it compares against is deleted; its last run is the parent's try. Rewritten against mesh-sdk, same cases: issued_test.go (needs now come from compose.Resolve: Needed.IssuedToHolder and ProviderMoved), module_health_test.go and merge_gate_handing tests (the contract in the Input), provisioner_account_integration_test.go (the composition's refusal), one_resolution_integration_test.go (sayFound). Every test of the send, perform, the adapter and the verbs is kept; the tests that used planFor, declarationWith, generators, renderingFor or grantsFor as setup use resolveNode, composeSend, composeQuestion, renderingOf and grantsOf. compose_callers_test.go now fails when any deleted name is declared or called again. Committed by copying the files of a staging branch (stage/r4b-1389 at198be8217c, built fromfb556d6bby patch commits and compared with the author's worktree), so that this commit's parent isfb556d6b.
296 lines
11 KiB
Go
296 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdh"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// The core's own credentials, sealed by the vault (novox/hq ADR 0349 §16, issue 755; the lead's decision on issue 755,
|
|
// notes 4 and 5).
|
|
//
|
|
// For a machine whose engine opens the vault's box (link.VaultBoxContract), each own secret of a module on the core's
|
|
// list (catalogue.CoreCredentialModules) travels as the vault sealed it to that machine's key, which the controller
|
|
// asks for on the send path (`get`, answered to the controller alone and sealed only to the key that machine's engine
|
|
// stated: mesh-vault's core.go) and carries without being able to open it. A machine below that contract is sent the
|
|
// controller's own sealed value, exactly as before.
|
|
//
|
|
// **The vault seals every answer afresh**, so the box last got is kept (inventory.CoreBox) and carried again while the
|
|
// vault's generation and the machine's key are the same: a declaration that changed with every answer would never be
|
|
// current. Every question but a send reads the kept box and asks nothing.
|
|
//
|
|
// **Until the vault holds a machine's core credentials, the controller seals them itself** (novox/hq ADR 0396, issue
|
|
// 908; it replaces the ADR 0349 §16 rule that the controller never does). When the vault does not answer, a credential
|
|
// with a kept box keeps it — the machine keeps the file it has — and one with none has its module composed as for an
|
|
// engine below the contract: with the controller's own sealed value, never left out (a core module left out took the
|
|
// bus's port from the packet filter and cut the remote machines off). Either way the send raises a condition naming
|
|
// the vault and the machine, cleared by the first send the vault answers whole. Only a machine with no sealing key, or
|
|
// one whose key cannot be read, leaves its module out.
|
|
|
|
// coreAnswer is the vault's `get` answer as the controller reads it: the name, the generation and the box, never
|
|
// opened.
|
|
type coreAnswer struct {
|
|
Name string `json:"name"`
|
|
Generation int `json:"generation"`
|
|
Sealed json.RawMessage `json:"sealed"`
|
|
}
|
|
|
|
// coreAsk asks the vault for one secret sealed to the given key.
|
|
type coreAsk func(name, publicKey string) (coreAnswer, error)
|
|
|
|
// coreBoxes is what the vault's boxes make of one machine's composition.
|
|
type coreBoxes struct {
|
|
// sealed is each core credential's box, by module and name (catalogue.Rendering.VaultSealed).
|
|
sealed map[string]map[string]json.RawMessage
|
|
// notPlaced is each core module left out, with why (catalogue.Rendering.NotPlaced).
|
|
notPlaced map[string]string
|
|
// keep is each box newly answered, to keep.
|
|
keep []inventory.CoreBox
|
|
// byController is each core module the vault did not answer for and no box was kept for, which is composed with
|
|
// the controller's own seal until the vault holds it (ADR 0396).
|
|
byController []string
|
|
// held is each credential the vault did not answer for whose kept box is carried again; silent is why the vault
|
|
// did not answer, per credential.
|
|
held, silent []string
|
|
}
|
|
|
|
// coreModulesOf is every module of the plan on the core's list whose own secrets this machine's declaration carries,
|
|
// with their plain own secret names, sorted: one handed to the provisioner has its secrets from the vault through the
|
|
// client, and one issued outside the mesh is never the vault's to seal here.
|
|
func coreModulesOf(modules []catalogue.Manifest, handed func(catalogue.Manifest) bool) map[string][]string {
|
|
out := map[string][]string{}
|
|
for _, m := range modules {
|
|
if !catalogue.CarriesVaultSealed(m.Module) || (handed != nil && handed(m)) {
|
|
continue
|
|
}
|
|
var names []string
|
|
for name, own := range m.OwnSecrets.Plain() {
|
|
if own.IssuedBy != catalogue.IssuedOutside {
|
|
names = append(names, name)
|
|
}
|
|
}
|
|
if len(names) > 0 {
|
|
sort.Strings(names)
|
|
out[m.Module] = names
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// sealingKeyPEM is a machine's sealing key, recorded raw in base64, as the SPKI PEM the vault seals to.
|
|
func sealingKeyPEM(raw string) (string, error) {
|
|
b, err := base64.StdEncoding.DecodeString(strings.TrimSpace(raw))
|
|
if err != nil || len(b) != 32 {
|
|
return "", errors.New("the machine's sealing key is not a raw X25519 key")
|
|
}
|
|
pub, err := ecdh.X25519().NewPublicKey(b)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
der, err := x509.MarshalPKIXPublicKey(pub)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: der})), nil
|
|
}
|
|
|
|
// coreBoxesOf decides each core credential of a machine: on a send, the vault's answer (the kept box while its
|
|
// generation and key are the same), the kept box where the vault did not answer, or the module left to the controller's
|
|
// own seal where nothing was kept; on any other question, the kept box, or a stand-in the next send asks for. Pure: what to keep and what to
|
|
// say is answered, not done.
|
|
func coreBoxesOf(node, sealingKey string, core map[string][]string, kept map[string]map[string]inventory.CoreBox,
|
|
allocating bool, ask coreAsk) coreBoxes {
|
|
out := coreBoxes{sealed: map[string]map[string]json.RawMessage{}, notPlaced: map[string]string{}}
|
|
modules := make([]string, 0, len(core))
|
|
for m := range core {
|
|
modules = append(modules, m)
|
|
}
|
|
sort.Strings(modules)
|
|
for _, module := range modules {
|
|
if sealingKey == "" {
|
|
out.notPlaced[module] = fmt.Sprintf("%s has no sealing key, so the vault can seal %s's credentials to "+
|
|
"nothing there (novox/hq ADR 0349 §16)", node, module)
|
|
continue
|
|
}
|
|
key, err := sealingKeyPEM(sealingKey)
|
|
if err != nil {
|
|
out.notPlaced[module] = fmt.Sprintf("%s's credentials cannot be asked of the vault: %v", module, err)
|
|
continue
|
|
}
|
|
boxes := map[string]json.RawMessage{}
|
|
for _, name := range core[module] {
|
|
k, has := kept[module][name]
|
|
valid := has && k.SealedTo == sealingKey && len(k.Box) > 0
|
|
if !allocating {
|
|
if valid {
|
|
boxes[name] = k.Box
|
|
} else {
|
|
boxes[name] = catalogue.ForeseenVaultBox
|
|
}
|
|
continue
|
|
}
|
|
secret := "own/" + node + "/" + module + "/" + name
|
|
got, err := ask(secret, key)
|
|
if err == nil && (got.Generation <= 0 || canonicalBox(got.Sealed) == nil) {
|
|
err = errors.New("it answered no box")
|
|
}
|
|
if err != nil {
|
|
out.silent = append(out.silent, fmt.Sprintf("%s: %v", secret, err))
|
|
if valid {
|
|
boxes[name] = k.Box
|
|
out.held = append(out.held, module+"/"+name)
|
|
continue
|
|
}
|
|
out.byController = append(out.byController, module)
|
|
break
|
|
}
|
|
if valid && k.Generation == got.Generation {
|
|
boxes[name] = k.Box
|
|
continue
|
|
}
|
|
box := canonicalBox(got.Sealed)
|
|
boxes[name] = box
|
|
out.keep = append(out.keep, inventory.CoreBox{Module: module, Name: name, Generation: got.Generation,
|
|
SealedTo: sealingKey, Box: box})
|
|
}
|
|
if _, left := out.notPlaced[module]; !left && !slices.Contains(out.byController, module) {
|
|
out.sealed[module] = boxes
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// canonicalBox is the vault's box in the one form the declaration carries — the sealed box's fields, in their order,
|
|
// and nothing else — or nil when it is not one.
|
|
func canonicalBox(raw json.RawMessage) json.RawMessage {
|
|
var b struct {
|
|
V int `json:"v"`
|
|
Eph string `json:"eph"`
|
|
IV string `json:"iv"`
|
|
Tag string `json:"tag"`
|
|
Ct string `json:"ct"`
|
|
}
|
|
if err := json.Unmarshal(raw, &b); err != nil || b.V <= 0 || b.Eph == "" || b.Ct == "" {
|
|
return nil
|
|
}
|
|
out, _ := json.Marshal(b)
|
|
return out
|
|
}
|
|
|
|
// coreVault is how a send asks the vault for a core credential: its `get`, in the controller's name. A test stands
|
|
// in for it.
|
|
var coreVault = func(ctx context.Context) coreAsk {
|
|
vault := theVault(ctx)
|
|
return func(name, publicKey string) (coreAnswer, error) {
|
|
raw, err := vault("get", map[string]any{"name": name, "public_key": publicKey})
|
|
if err != nil {
|
|
return coreAnswer{}, err
|
|
}
|
|
var a coreAnswer
|
|
if err := json.Unmarshal(raw, &a); err != nil {
|
|
return coreAnswer{}, fmt.Errorf("the vault's answer is not one: %w", err)
|
|
}
|
|
return a, nil
|
|
}
|
|
}
|
|
|
|
// coreBoxesFor is coreBoxesOf for one machine, read from and kept in the store, and said on a send.
|
|
func coreBoxesFor(ctx context.Context, inv *inventory.Inventory, node string, modules []catalogue.Manifest,
|
|
handed func(catalogue.Manifest) bool, allocating bool) (coreBoxes, error) {
|
|
core := coreModulesOf(modules, handed)
|
|
if len(core) == 0 {
|
|
if allocating {
|
|
settleVaultSilent(ctx, node)
|
|
}
|
|
return coreBoxes{}, nil
|
|
}
|
|
key, err := inv.SealingKeyOf(ctx, node)
|
|
if err != nil {
|
|
return coreBoxes{}, err
|
|
}
|
|
kept, err := inv.KeptCoreBoxes(ctx, node)
|
|
if err != nil {
|
|
return coreBoxes{}, err
|
|
}
|
|
var ask coreAsk
|
|
if allocating {
|
|
ask = coreVault(ctx)
|
|
}
|
|
out := coreBoxesOf(node, key, core, kept, allocating, ask)
|
|
for _, b := range out.keep {
|
|
if err := inv.KeepCoreBox(ctx, node, b); err != nil {
|
|
return coreBoxes{}, err
|
|
}
|
|
}
|
|
if allocating {
|
|
sayVaultSilent(ctx, node, out)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// kindVaultSilent is a send that did not have the vault's box for a core credential of a machine.
|
|
const kindVaultSilent = "vault-silent"
|
|
|
|
// vaultSilentObservation is that, as a condition: `machine.<machine>.vault-silent`.
|
|
func vaultSilentObservation(node string, b coreBoxes) conditions.Observation {
|
|
o := conditions.Observation{Scope: conditions.ScopeMachine, ID: node, Kind: kindVaultSilent, Token: kindVaultSilent,
|
|
Machine: node, Severity: conditions.Warning, Source: coreDowngradeSource}
|
|
var said []string
|
|
if len(b.notPlaced) > 0 {
|
|
o.Severity = conditions.Urgent
|
|
var left []string
|
|
for m := range b.notPlaced {
|
|
left = append(left, m)
|
|
}
|
|
sort.Strings(left)
|
|
said = append(said, "not placed: "+strings.Join(left, ", "))
|
|
}
|
|
if len(b.byController) > 0 {
|
|
said = append(said, "sealed by the controller until the vault holds it: "+strings.Join(b.byController, ", "))
|
|
}
|
|
if len(b.held) > 0 {
|
|
said = append(said, "kept as last sent: "+strings.Join(b.held, ", "))
|
|
}
|
|
o.Summary = fmt.Sprintf("the vault did not answer for the core's credentials on %s; %s", node, strings.Join(said, "; "))
|
|
o.Said = o.Summary + " — " + strings.Join(b.silent, "; ")
|
|
return o
|
|
}
|
|
|
|
// sayVaultSilent raises the condition for a send the vault did not answer whole, and clears it for one it did.
|
|
func sayVaultSilent(ctx context.Context, node string, b coreBoxes) {
|
|
if len(b.silent) == 0 && len(b.notPlaced) == 0 {
|
|
settleVaultSilent(ctx, node)
|
|
return
|
|
}
|
|
fmt.Printf("HELD: %s\n", vaultSilentObservation(node, b).Said)
|
|
if conditionsFrom == nil {
|
|
return
|
|
}
|
|
if _, err := conditionsFrom.Observe(ctx, vaultSilentObservation(node, b)); err != nil {
|
|
fmt.Printf("the condition for the vault's silence on %s could not be raised: %v\n", node, err)
|
|
}
|
|
}
|
|
|
|
// settleVaultSilent clears the condition once a send had every core credential of the machine from the vault.
|
|
func settleVaultSilent(ctx context.Context, node string) {
|
|
if conditionsFrom == nil {
|
|
return
|
|
}
|
|
key := vaultSilentObservation(node, coreBoxes{}).Key()
|
|
if _, err := conditionsFrom.Clear(ctx, key, "the vault answered for every core credential of "+node); err != nil {
|
|
fmt.Printf("the condition %s could not be cleared: %v\n", key, err)
|
|
}
|
|
}
|