package main // A machine's node-engine composes its own modules (novox/hq ADR 0547 §2, issue 1126), and asks the vault for their own // secrets sealed to its own key (ADR 0349 §11, ADR 0550 §5): the verb `own`, answered to the node-engine of a machine // alone, about that machine alone (the machine is the caller's, as the bus proved it, never an argument). Each value is // sealed to the sealing key the machine's engine stated (`node-key`), so what crosses the bus opens on that machine only, // and the composition carries it sealed (no composition carries a secret in the clear). A module handed to the // provisioner is left out: its client fills its secrets from `get` (ADR 0349 §17). import ( "errors" "fmt" "maps" "slices" "sort" "strings" "time" stdio "git.novox.be/novox/mesh-sdk/go" "git.novox.be/novox/mesh-sdk/go/catalogue" "git.novox.be/novox/mesh-sdk/go/seal" ) // NodeOwnSecret is one own secret of a module on the asking machine, sealed to the machine's key. type NodeOwnSecret struct { Module string `json:"module"` Name string `json:"name"` Origin string `json:"origin"` Generation int `json:"generation"` Sealed seal.Box `json:"sealed"` } // OwnAnswer is what `own` answers: the machine, the sealing key every value is sealed to (raw X25519, base64, as its // engine stated it), each own secret of its modules, and each pair credential it asked for that the vault holds. type OwnAnswer struct { Node string `json:"node"` Key string `json:"key"` Own []NodeOwnSecret `json:"own"` Credentials []NodeCredential `json:"credentials,omitempty"` Missing []CredentialAsked `json:"missing,omitempty"` // Handed is, by module of the machine, whether the vault judged it handed to the provisioner (catalogue.AssignmentHands // of the plan in its assignment), and so left its own secrets out of Own: the node composing itself hands exactly // those (mesh-sdk compose/frombus HandedReader, issue 774). Every module it holds a secret of, and every module // placed there whose plan reads; one it could not judge is not named, and the node judges it as before. Handed map[string]bool `json:"handed,omitempty"` } // CredentialAsked names one pair credential by its ends, as a node composing its own asks for it (mesh-sdk // compose.CredentialKey): Role is `consumer` when the asking machine consumes it, `provider` when it grants it. type CredentialAsked struct { Role string `json:"role"` Provision string `json:"provision"` Consumer string `json:"consumer"` ConsumerModule string `json:"consumer_module"` Provider string `json:"provider"` Local string `json:"local,omitempty"` } // NodeCredential is one pair credential answered, sealed to the asking machine's key, with the name the vault keeps it // under (`pair/...` per pair, `provision////` issued for one holder, or the provision's // one shared value `provision//`). type NodeCredential struct { CredentialAsked Name string `json:"name"` Generation int `json:"generation"` Sealed seal.Box `json:"sealed"` } // credentialNames are the names a pair credential may be kept under, the most particular first. func credentialNames(c CredentialAsked) []string { pair := "pair/" + c.Provider + "/" + c.Provision + "/" + c.Consumer + "/" + c.ConsumerModule if c.Local != "" { pair += "/" + c.Local } return []string{pair, "provision/" + c.Provider + "/" + c.Provision + "/" + c.Consumer + "/" + c.ConsumerModule, "provision/" + c.Provider + "/" + c.Provision} } // credentialRefusal says why node may not have c, or "": a consumer's credential to the machine its consuming module // stands on, a provider's to the providing machine, and nothing to a machine that is neither. func (v *Vault) credentialRefusal(node string, c CredentialAsked, plan *planReader) string { for _, part := range []string{c.Provision, c.Consumer, c.ConsumerModule, c.Provider} { if !namePart.MatchString(part) { return fmt.Sprintf("%q is not a name the mesh writes", part) } } if c.Local != "" && !namePart.MatchString(c.Local) { return fmt.Sprintf("%q is not a name the mesh writes", c.Local) } switch c.Role { case "consumer": if c.Consumer != node { return fmt.Sprintf("%s asks as a consumer on %s, and is not that machine", node, c.Consumer) } // The consuming module stands on the machine: read from the assignments and the assignments, never assumed (one // that cannot be read refuses). The assignments' mirror can be behind, so one it does not hold is confirmed directly. placed, err := plan.placedOn(c.ConsumerModule) if err != nil { return fmt.Sprintf(notYetPrefix+"where %s stands could not be read: %v", c.ConsumerModule, err) } if placed { return "" } raw, why, err := plan.read(c.ConsumerModule) if why != "" && v.Assigner.Read == nil { return why } if why == "" { if err != nil { return fmt.Sprintf(notYetPrefix+"the assignment %s.%s could not be read: %v", node, c.ConsumerModule, err) } if raw == nil { if raw, err = plan.confirmed(c.ConsumerModule); err != nil { return fmt.Sprintf(notYetPrefix+"the assignment %s.%s could not be read: %v", node, c.ConsumerModule, err) } } } if raw == nil { return fmt.Sprintf("%s is not on %s: neither placed nor assigned there, nor required by a module there", c.ConsumerModule, node) } return "" case "provider": if c.Provider != node { return fmt.Sprintf("%s asks as the provider %s, and is not that machine", node, c.Provider) } return "" } return fmt.Sprintf("the role %q is not consumer or provider", c.Role) } // planReader reads the assignments of one machine's modules for one answer of `own`, each module's once (issue 1126, // 2026-10-28: one read per own secret and per credential, 203 on novox, held the answer past the node-engine's bound), // and the machine's assignments once (issue 774, 2026-10-31: read once per credential, 143 bus reads in one answer to ace). type planReader struct { node string reader AssignmentReader confirm AssignmentReader why string got map[string]planRead // assignments reads the machine's placed modules; placed and placedErr keep the one read for the answer. assignments func() ([]string, error) placedRead bool placed []string placedErr error } type planRead struct { raw []byte err error } func (v *Vault) plansOf(node string) *planReader { read, why := v.assignmentReader(node) var confirm AssignmentReader if d, ok := v.Askers.(Declared); ok && d.Confirm != nil { confirm = standing(d.Confirm) } return &planReader{node: node, confirm: confirm, reader: read, why: why, got: map[string]planRead{}, assignments: func() ([]string, error) { return v.placedModules(node) }} } // placedOn says module is placed on the machine by the catalogue, from the machine's assignments read once for the answer. func (p *planReader) placedOn(module string) (bool, error) { if !p.placedRead { p.placed, p.placedErr = p.assignments() p.placedRead = true } return slices.Contains(p.placed, module), p.placedErr } // placedModules is the machine's placed modules, read once for the answer. func (p *planReader) placedModules() ([]string, error) { if p.assignments == nil { return nil, errors.New("the machine's assignments is not read here") } p.placedOn("") return p.placed, p.placedErr } // read is module's assignment on the machine, nil when there is none, or why the vault reads none. func (p *planReader) read(module string) ([]byte, string, error) { if p.why != "" { return nil, p.why, nil } r, ok := p.got[module] if !ok { r.raw, r.err = p.reader(p.node + "." + module) p.got[module] = r } return r.raw, "", r.err } // confirmed is module's assignment read directly, for one the reader does not hold (a watched mirror can be behind), // nil when there is none or nothing reads directly. func (p *planReader) confirmed(module string) ([]byte, error) { if p.confirm == nil { return nil, nil } raw, err := p.confirm(p.node + "." + module) if err == nil && raw != nil { p.got[module] = planRead{raw: raw} } return raw, err } // assignmentReader is the vault's reader of the assignments, or why it has none: the rule `own` keeps reads them, // and without them it refuses rather than hands (a rule that cannot be read is no rule). func (v *Vault) assignmentReader(node string) (AssignmentReader, string) { d, ok := v.Askers.(Declared) if !ok || d.Read == nil { return nil, "the vault cannot read the assignments, so it cannot tell what " + node + "'s modules are; nothing is handed" } return standing(d.Read), "" } // sharedRefusal says why node may not have the provision's one shared value `provision//` for c, // or "": the vault's record of who holds it names the consumer's module among its consumers, or the machine as its // provider. Without that record nothing is handed: the shared value is not one any machine may have by naming a // provider (ADR 0550 §5). func (v *Vault) sharedRefusal(node string, c CredentialAsked) (string, error) { name := "provision/" + c.Provider + "/" + c.Provision sh, ok, err := v.Store.ReadShared(name) if err != nil { return "", err } if !ok { return fmt.Sprintf("the vault keeps no record of who holds %s", name), nil } switch c.Role { case "consumer": want := consumerOf(c.Consumer, c.ConsumerModule, c.Local) for _, held := range sh.Consumers { if held == want { return "", nil } } return fmt.Sprintf("%s is held by %s, not by %s", name, strings.Join(sh.Consumers, ", "), want), nil case "provider": if sh.Provider == node { return "", nil } return fmt.Sprintf("%s is provided from %s, not %s", name, sh.Provider, node), nil } return fmt.Sprintf("the role %q is not consumer or provider", c.Role), nil } // composeOrigin is the vault's origin of a value as the composition reads it: made by the mesh, or given (accepted at a // terminal, typed at a desk, issued by a provider's software). func composeOrigin(origin string) string { if origin == "" || origin == OriginMade { return OriginMade } return OriginAccepted } // OwnOfNode answers the node-engine of a machine the own secrets of its modules, each sealed to the machine's stated key. // Every answer says how long it took, and a refusal is said too (issue 774, 2026-10-31: ace's asks went unanswered past // the node-engine's 15 s and the vault's journal named none of them, so whether they arrived could not be told). func (v *Vault) OwnOfNode(caller string, asked ...CredentialAsked) (OwnAnswer, error) { began := v.Now() a, err := v.ownOfNode(caller, asked...) took := v.Now().Sub(began).Round(time.Millisecond) if err != nil { v.log("own for %s, %d credentials asked, refused after %s: %v", caller, len(asked), took, err) return a, err } v.log("handed %s's node-engine the %d own secrets of its modules and %d credentials (%d not held), sealed to its key, in %s", a.Node, len(a.Own), len(a.Credentials), len(a.Missing), took) return a, nil } func (v *Vault) ownOfNode(caller string, asked ...CredentialAsked) (OwnAnswer, error) { node, ok := nodeEngineOf(caller) if !ok { if caller == "" || caller == stdio.Unproven { caller = "an unproven caller" } return OwnAnswer{}, fmt.Errorf("own answers a machine's node-engine alone, about its own machine; %s is not a node-engine", caller) } key, had, err := v.Store.NodeKey(node) if err != nil { return OwnAnswer{}, err } if !had { return OwnAnswer{}, fmt.Errorf("the vault holds no secret named own/%s yet: %s's node-engine has stated no sealing key "+ "(node-key), so there is no key of its own to seal to", node, node) } to, err := publicPEM(key) if err != nil { return OwnAnswer{}, fmt.Errorf("%s's sealing key as recorded is not usable: %w", node, err) } // Only this machine's own records are read, each module's assignment once, and every hand-out recorded in one synced // write after all are sealed: none is handed when that record cannot be written (issue 1126, 2026-10-28). all, err := v.Store.ListPrefix("own/" + node + "/") if err != nil { return OwnAnswer{}, err } plan := v.plansOf(node) var handings []Handing out := OwnAnswer{Node: node, Key: key, Handed: map[string]bool{}} for _, r := range all { parts := strings.Split(r.Name, "/") if len(parts) != 4 || parts[0] != "own" || parts[1] != node { continue } handed, err := plan.handed(parts[2]) if err != nil { return OwnAnswer{}, err } out.Handed[parts[2]] = handed if handed { continue } value, err := seal.Open(r.AtRest, v.Own.PrivateKey) if err != nil { return OwnAnswer{}, fmt.Errorf("%s does not open with the vault's key: %w", r.Name, err) } sealed, err := seal.Seal(value, to) if err != nil { return OwnAnswer{}, err } handings = append(handings, Handing{To: caller, Name: r.Name, Generation: r.Generation, Outcome: "handed"}) out.Own = append(out.Own, NodeOwnSecret{Module: parts[2], Name: parts[3], Origin: composeOrigin(r.Origin), Generation: r.Generation, Sealed: sealed}) } // And every module placed there that it holds no secret of yet, where its plan reads: the node hands it as judged here. if placed, err := plan.placedModules(); err == nil { for _, module := range placed { if _, judged := out.Handed[module]; judged { continue } if handed, err := plan.handed(module); err == nil { out.Handed[module] = handed } } } sort.Slice(out.Own, func(i, j int) bool { if out.Own[i].Module != out.Own[j].Module { return out.Own[i].Module < out.Own[j].Module } return out.Own[i].Name < out.Own[j].Name }) for _, c := range asked { if why := v.credentialRefusal(node, c, plan); why != "" { return OwnAnswer{}, fmt.Errorf("%s may not ask for the credential of %s from %s to %s on %s: %s", caller, c.Provision, c.Provider, c.ConsumerModule, c.Consumer, why) } found := false names := credentialNames(c) for i, name := range names { r, held, err := v.Store.Read(name) if err != nil { return OwnAnswer{}, err } if !held { continue } if i == len(names)-1 { // The provision's one shared value: handed only to a machine the record of its holders names. why, err := v.sharedRefusal(node, c) if err != nil { return OwnAnswer{}, err } if why != "" { v.log("%s is not handed %s: %s", node, name, why) continue } } value, err := seal.Open(r.AtRest, v.Own.PrivateKey) if err != nil { return OwnAnswer{}, fmt.Errorf("%s does not open with the vault's key: %w", name, err) } sealed, err := seal.Seal(value, to) if err != nil { return OwnAnswer{}, err } handings = append(handings, Handing{To: caller, Name: name, Generation: r.Generation, Outcome: "handed"}) out.Credentials = append(out.Credentials, NodeCredential{CredentialAsked: c, Name: name, Generation: r.Generation, Sealed: sealed}) found = true break } if !found { out.Missing = append(out.Missing, c) } } if err := v.recordHandings(handings); err != nil { return OwnAnswer{}, fmt.Errorf("nothing is handed out to %s: who was handed its %d values could not be recorded: %w", caller, len(handings), err) } return out, nil } // handed says the plan of module on node hands it to the provisioner, so its secrets are filled from `get`, not composed. // A plan that cannot be read is an error, never "not handed": `own` refuses rather than hands a secret the provisioner // holds. func (p *planReader) handed(module string) (bool, error) { node := p.node raw, why, err := p.read(module) if why != "" { return false, errors.New(why) } if err != nil { return false, fmt.Errorf(notYetPrefix+"the assignment %s.%s could not be read, so whether it is handed to the "+ "provisioner is not known: %w", node, module, err) } if raw == nil { return false, nil } hands, err := handsOf(raw) if err != nil { return false, fmt.Errorf("the assignment %s.%s is not the controller's: %w", node, module, err) } return hands, nil } // handsOf is whether an assignment's value carries a plan that hands its module to the provisioner: mesh-sdk's one // rule, catalogue.Plan.Hands (issue 774). A plan naming provisions alone (a route the seat keys for a module not // handed) hands nothing, and that module's files and own secrets stay its node's composition. Whether a plan exists // is never the rule; a reader that asks only who binds whom reads the provisions and says so ("not handing", which // the catalogue check checks/planhands holds every module to). func handsOf(raw []byte) (bool, error) { return catalogue.AssignmentHands(raw) } // hands is handsOf for a value already read as the controller's: one that cannot be read hands nothing. func hands(raw []byte) bool { h, err := handsOf(raw) return err == nil && h } // HandedJudged is how the vault judges one module of a machine for `own`: handed (its own secrets left out, for the // provisioner's client to fill from `get`), and why, as the plan in its assignment says it; or why it could not judge. type HandedJudged struct { Module string `json:"module"` Handed bool `json:"handed"` Why string `json:"why"` } // HandedOf is the vault's judgement of each module of node, as `own` would answer it now (issue 1966): every module it // keeps an own secret of there and every module placed there, sorted. Read from the same plans `own` reads, so what it // says is what the next answer leaves out. A node composes by its manifest, its build and the machine, and the plans it // makes are what this reads next: a module named here as not handed for want of a plan is handed by its node's next // composition, where the machine takes it. func (v *Vault) HandedOf(node string) ([]HandedJudged, error) { if !namePart.MatchString(node) { return nil, fmt.Errorf("%q is not a machine's name", node) } all, err := v.Store.ListPrefix("own/" + node + "/") if err != nil { return nil, err } modules := map[string]bool{} for _, r := range all { if parts := strings.Split(r.Name, "/"); len(parts) == 4 { modules[parts[2]] = true } } plan := v.plansOf(node) placed, placedErr := plan.placedModules() if placedErr != nil { // Never dropped: an answer that leaves out the modules placed there, with no word of it, would read as "the others // are not placed" (issue 1966 review note). return nil, fmt.Errorf("the assignments of %s could not be read, so the modules placed there are not all judged: %w", node, placedErr) } for _, m := range placed { modules[m] = true } out := make([]HandedJudged, 0, len(modules)) for _, module := range slices.Sorted(maps.Keys(modules)) { out = append(out, plan.judged(module)) } return out, nil } // judged is handed with its why, as HandedOf says it. func (p *planReader) judged(module string) HandedJudged { j := HandedJudged{Module: module} raw, why, err := p.read(module) switch { case why != "": j.Why = "not judged: " + why return j case err != nil: j.Why = fmt.Sprintf("not judged: the assignment %s.%s could not be read: %v", p.node, module, err) return j case raw == nil: j.Why = "no assignment holds a plan of it yet: its own secrets are answered to its node" return j } hands, err := handsOf(raw) switch { case err != nil: j.Why = fmt.Sprintf("not judged: the assignment %s.%s is not the controller's: %v", p.node, module, err) case hands: j.Handed, j.Why = true, "its plan hands it: its own secrets are its provisioner client's, from get" default: j.Why = "its plan hands nothing (none, or provisions alone): its own secrets are answered to its node" } return j }