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 `/` 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:}`) 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::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-`, `internal-name-`): 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": }`) 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}