Files
mesh-sdk/go/catalogue/handed.go
T
jschoubben 1c7356c247
mesh/delivery-group group feat/sdk1 rejected: composed together: the checker's verdict at the members' heads (428d048dd50a): unit: git.novox.be/novox/mesh-sdk/…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: unit: git.novox.be/novox/mesh-sdk/go/process.TestStatePutWritesAKeyWhole: a Put through a short blackout: 0, mesh-mail's state send…
catalogue: the composition's rules and refusals leave for the node-engine, so mesh-sdk renders no declaration and refuses no composition (ADR 0593 §7)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 19:19:25 +01:00

262 lines
13 KiB
Go

package catalogue
import (
"encoding/json"
"errors"
"fmt"
"strings"
)
// Handed is what a moved module's build kept for the provisioner (novox/hq ADR 0349 §2): its templates archive, as
// recorded (store-relative, routed at composition like any archive), and its digest; and, for each file resource
// filled from a template, that template's path inside the archive. Written by Resolve, never by hand.
type Handed struct {
Source string `json:"source"`
Digest string `json:"digest"`
Templates map[string]string `json:"templates"`
// Items is, for each item the module contributes, by ItemKey (its collection, and `/<name>` for a named one),
// that item's template inside the archive (collections.go, design 57 L1).
Items map[string]string `json:"items,omitempty"`
}
// Plan is a moved module's plan on one node: what the node's provisioner client fills its files from. The shape
// of the assignments mirror's `provisioning` (mesh-controller internal/inventory Provisioning).
type Plan struct {
Templates PlanTemplates `json:"templates"`
Files []PlanFile `json:"files"`
Placement map[string]string `json:"placement,omitempty"`
// Secrets are the module's own secrets this declaration names (design 57 V4, issue 647): each its templates
// name, and each it keeps in a file of its own, with that file's path. By name, never a value: the client asks
// the vault for each, and the vault hands one only when the plan names it.
Secrets []PlanSecret `json:"secrets,omitempty"`
// Items are the module's items of collections (design 57 L1): each filled by the client like a file and recorded
// in mesh-provisioner_collect for its node. Collections are the collections its files name, as their collector,
// each with its comment. By template and name, never a contributor's text.
Items []PlanItem `json:"items,omitempty"`
Collections []PlanCollection `json:"collections,omitempty"`
// Provisions are what it requires here, each with the provider resolution chose to serve it (issue 760), so the
// vault sees a consumer bind to a provider and unbind from it (novox/hq ADR 0349 clause 8); and, for a
// credential the vault keeps for it alone, the name its files call it and the file holding it (issue 757). By
// name, never a value.
Provisions []PlanProvision `json:"provisions,omitempty"`
}
// Hands is whether the plan hands its module to the provisioner: it names something the node's provisioner client fills
// (a template archive, a file, an own secret or an item), and the node composes none of it. A plan naming provisions alone
// hands nothing (catalogue.Keyed, issue 774): the seat keys what a provider's helper answers of a module not handed, and
// that module's files and own secrets stay its node's composition. The one rule every reader of a plan asks; whether a
// plan exists is never it (on 2026-10-30 the vault read every route-only plan as handed and answered twenty modules'
// own secrets to nobody).
func (p Plan) Hands() bool {
return p.Templates.Source != "" || len(p.Files) > 0 || len(p.Secrets) > 0 || len(p.Items) > 0
}
// PlanItem is one item the module contributes: to which collection, and its template inside the archive.
//
// A named item has its Name and Attributes (design 57 L3, issue 777): the client records them with the item and
// places it under a placeholder selecting by them. An item whose `if-capability` the machine lacks is not in the plan.
type PlanItem struct {
Collection string `json:"collection"`
Name string `json:"name,omitempty"`
Attributes map[string]string `json:"attributes,omitempty"`
Template string `json:"template"`
}
// PlanCollection is one collection the module collects, and the comment introducing each contributor's item.
type PlanCollection struct {
Name string `json:"name"`
Comment string `json:"comment,omitempty"`
}
// PlanProvision is one provision the module requires, under one local name, and the provider serving it: the module
// (ProviderModule) on the machine (Provider) that resolution chose for this consumer. Where several providers offer
// a mesh-scoped provision, it is the one this consumer is bound to (a pin, the seat's holder, or where its data is),
// never the others (issue 760). Local is the name the consumer keeps the credential under where it keeps several
// from one requirement (ADR 0094), empty for the ordinary one; a consumer has one provider per provision and local
// name.
//
// Name and Path are set only for a credential the vault keeps for this holder alone, which its machine's client
// fills (issue 757): the value the provider's software issued for this module on this machine. Name is what the
// module's files call it (`${secret:<name>}`) and Path the file holding it alone.
//
// CredentialForm is the form the provider's offer declares for its credential (Manifest.CredentialFormOf, issue
// 768): the vault makes a first value in that form for a provider it holds no row of. Empty where the provider's
// manifest was not in hand, and the vault then makes nothing.
type PlanProvision struct {
Name string `json:"name,omitempty"`
Provision string `json:"provision"`
Local string `json:"local,omitempty"`
Provider string `json:"provider"`
ProviderModule string `json:"provider-module"`
CredentialForm string `json:"credential-form,omitempty"`
// CredentialTool is, for an offer whose credential the provider's software issues (issue 928), the provider's tool
// the vault asks for each holder's value; empty for any other form.
CredentialTool string `json:"credential-tool,omitempty"`
Path string `json:"path,omitempty"`
// Contributes is what the consumer's manifest tells its provider for this provision (Manifest.Contributes: a
// database's name, its extensions), so the provider's helper learns it from the bus (issue 776). Nil where the
// manifest contributes nothing for it, and the entry is then as it was.
Contributes map[string]any `json:"contributes,omitempty"`
// At, From and As are what today's binding file tells the consumer of this provision (issue 788): where the
// provider is on the private network, the machine, and who the consumer is at its end (its slug first), so the
// provider's helper writes them into the consumer's entry and the client fills `${bound:<provision>:at|from|as}`
// from there. Empty where the resolution knew none.
At string `json:"at,omitempty"`
From string `json:"from,omitempty"`
As string `json:"as,omitempty"`
// Names are what the mesh calls the consumer through this provision (issue 792), keyed as its binding file names
// them (`name`, `internal-name`, `name-<local>`, `internal-name-<local>`): composed by the controller, which
// alone knows the node's domains, and carried unchanged by the provider's helper into the consumer's entry. Nil
// where the consumer contributes nothing to the provision, and the entry is then as it was.
Names map[string]string `json:"names,omitempty"`
// Contributed is what the consumer gives its provider through this provision, opaque here (issue 774, research
// 072 row 34): the bytes of a ContributedForm, each contribution composed as its provider receives it and where
// the consumer's machine is. What a contribution's values mean is the provider's own contract, published by the
// provider's repository (route-proxy's for a route), which decodes them; the seat, its plans, its client and the
// helper carry them unread. On the provision's first entry alone; nil where the consumer contributes nothing to it.
Contributed json.RawMessage `json:"contributed,omitempty"`
}
// ContributedVersion is the version of ContributedForm this SDK writes and reads.
const ContributedVersion = 1
// ContributedForm is the generic form of PlanProvision.Contributed: every contribution one consumer makes to one
// requirement, composed (settled with its node's settings, its endpoint's port filled in, its names joined to the
// node's domains: Resolution.composed, the one composition a received file was written from), and At, where the
// consumer's machine is on the private network, empty when it is on none. The provisioner knows no field of any
// contribution: Values are the bytes of an object in the provider's contract.
type ContributedForm struct {
V int `json:"v"`
At string `json:"at,omitempty"`
Each []ContributedOne `json:"each"`
}
// ContributedOne is one contribution: Local its local name among several (`contributes` naming several), empty for a
// requirement's one, and Values the contribution's object.
type ContributedOne struct {
Local string `json:"local,omitempty"`
Values json.RawMessage `json:"values"`
}
// DecodeContributed reads PlanProvision.Contributed strictly: this version, no field it does not know, at least one
// contribution, each local name once, each value a JSON object. What a value says is not judged here.
func DecodeContributed(raw json.RawMessage) (ContributedForm, error) {
var f ContributedForm
dec := json.NewDecoder(strings.NewReader(string(raw)))
dec.DisallowUnknownFields()
if err := dec.Decode(&f); err != nil {
return ContributedForm{}, fmt.Errorf("what a consumer contributed is not readable: %w", err)
}
if dec.More() {
return ContributedForm{}, errors.New("what a consumer contributed carries more than one document")
}
if f.V != ContributedVersion {
return ContributedForm{}, fmt.Errorf("what a consumer contributed is of version %d, this SDK reads %d", f.V,
ContributedVersion)
}
if len(f.Each) == 0 {
return ContributedForm{}, errors.New("what a consumer contributed holds no contribution")
}
seen := map[string]bool{}
for _, one := range f.Each {
if seen[one.Local] {
return ContributedForm{}, fmt.Errorf("what a consumer contributed names %q twice", one.Local)
}
seen[one.Local] = true
var object map[string]json.RawMessage
if err := json.Unmarshal(one.Values, &object); err != nil || object == nil {
return ContributedForm{}, fmt.Errorf("the contribution %q is not a JSON object", one.Local)
}
}
return f, nil
}
// PlanSecret is one of the module's own secrets, by its name in the manifest; Path is the file holding it alone,
// when the manifest's own-secrets places one.
type PlanSecret struct {
Name string `json:"name"`
Path string `json:"path,omitempty"`
}
// PlanTemplates is where the templates archive is fetched from on this node, and what it hashes to.
type PlanTemplates struct {
Source string `json:"source"`
Digest string `json:"digest"`
}
// PlanFile is one file the client fills.
type PlanFile struct {
ID string `json:"id"`
Path string `json:"path"`
Template string `json:"template"`
OwnerAtRuntime string `json:"owner-at-runtime"`
Takes string `json:"takes,omitempty"`
}
// The words a placed file says who owns it at runtime with (novox/hq ADR 0349 §5).
const (
RuntimeOwned = "owned"
RuntimeSeed = "seed"
RuntimeManaged = "managed"
)
// OwnerAtRuntimeField is the key a declared file says who owns it at runtime by (the node-engine's
// `owner-at-runtime`).
const OwnerAtRuntimeField = "owner-at-runtime"
// handedBy is what a moved module's build keeps, from its templates archive as built, or nil for a module not
// moved.
func handedBy(m Manifest, archive Built) *Handed {
if !m.Moved {
return nil
}
dir := m.TemplatesDir()
h := &Handed{Source: Recorded(archive.Reference), Digest: archive.Digest, Templates: map[string]string{}}
for _, r := range m.Resources {
name, has, wrong := templateOf(m.Module, dir, r)
if !has || wrong != "" {
continue
}
h.Templates[fmt.Sprint(r["id"])] = strings.TrimPrefix(name, dir+"/")
}
for _, it := range m.Items {
if it.Template == "" || itemTemplateProblem(m, it.Template) != "" {
continue
}
if h.Items == nil {
h.Items = map[string]string{}
}
h.Items[it.key()] = strings.TrimPrefix(it.Template, dir+"/")
}
return h
}
// AssignmentHands is whether a module's value in the seat mesh-provisioner's plans (the assignments mirror's shape,
// `{"provisioning": <Plan>}`) hands its module to the provisioner: Plan.Hands of the plan it carries, false with no
// plan. A value that does not read is an error, never "not handed". The one rule the vault judges `own` by and a node
// composing itself follows (Rendering.Assigned, issue 774).
func AssignmentHands(raw []byte) (bool, error) {
var a struct {
Provisioning *Plan `json:"provisioning"`
}
if err := json.Unmarshal(raw, &a); err != nil {
return false, err
}
return a.Provisioning != nil && a.Provisioning.Hands(), nil
}
// HandedProvisionField marks, between composing a handed module's credential file for something it requires and
// placing it, one the vault keeps for this holder alone (issue 757): its value is the PlanProvision without its path.
// Never on the wire.
const HandedProvisionField = "handed-provision"
// HandedSecretField marks, between composing a module's own secret files and placing them, the one a handing
// composition hands to the provisioner: its value is the secret's name. Never on the wire.
const HandedSecretField = "handed-secret"
// SettingField is the settings form's word for a config placeholder's namespace.
const SettingField = "setting"
// PlacementKinds are the placeholder namespaces placement answers (ADR 0349 clause 2), as a plan's keys begin.
var PlacementKinds = []string{"dir", "access", "port", "seat", "machine", BackupNamespace}