From 326d729c0b88aba67973fca6f907234b253768b0 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 5 Nov 2026 14:09:45 +0100 Subject: [PATCH] =?UTF-8?q?Placement=20and=20Identity-and-access=20contrac?= =?UTF-8?q?ts:=20mesh-module-assigner=20and=20mesh-wants=20whole=20with=20?= =?UTF-8?q?states,=20mesh-vault=20moves,=20mesh-node-enroller=20whole,=20n?= =?UTF-8?q?ode-secret-service=20moves;=20the=20two=20seats=20renamed=20(AD?= =?UTF-8?q?R=200634=20=C2=A77,=20=C2=A79;=20C-5,=20C-10;=20issue=201966)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- go/seats/meshvault/go.mod | 3 - .../go/seats/meshnodeenroller/contract.go | 260 +++++ .../seats/meshnodeenroller/contract_test.go | 60 ++ .../go/seats/meshnodeenroller/go.mod | 3 + .../go}/seats/meshvault/contract.go | 145 ++- .../go}/seats/meshvault/contract_test.go | 6 +- identity-and-access/go/seats/meshvault/go.mod | 3 + identity-and-access/module.json | 67 ++ identity-and-access/seats/index.json | 8 + .../mesh-node-enroller/fixtures/events.json | 12 + .../mesh-node-enroller/fixtures/states.json | 20 + .../seats/mesh-node-enroller}/seat.json | 156 ++- .../seats}/mesh-vault/fixtures/events.json | 2 +- .../seats/mesh-vault/fixtures/states.json | 22 + .../seats}/mesh-vault/fixtures/verbs.json | 0 .../seats}/mesh-vault/seat.json | 137 ++- .../seats}/node-secret-service/seat.json | 0 .../seats/meshnodeenroller/package-lock.json | 12 + .../ts/seats/meshnodeenroller/package.json | 13 + .../ts/seats/meshnodeenroller/src/index.ts | 191 ++++ .../ts/seats/meshnodeenroller}/tsconfig.json | 4 +- .../ts/seats/meshvault/package-lock.json | 12 + .../ts/seats/meshvault/package.json | 13 + .../ts}/seats/meshvault/src/index.ts | 90 +- .../ts/seats/meshvault/tsconfig.json | 12 + module.json | 20 - .../go/seats/meshmoduleassigner/contract.go | 665 +++++++++++++ .../seats/meshmoduleassigner/contract_test.go | 86 ++ placement/go/seats/meshmoduleassigner/go.mod | 3 + placement/go/seats/meshwants/contract.go | 331 ++++++ placement/go/seats/meshwants/contract_test.go | 76 ++ placement/go/seats/meshwants/go.mod | 3 + placement/module.json | 58 ++ placement/seats/index.json | 7 + .../mesh-module-assigner/fixtures/events.json | 29 + .../mesh-module-assigner/fixtures/states.json | 109 ++ .../seats/mesh-module-assigner/seat.json | 941 ++++++++++++++++++ .../seats/mesh-wants/fixtures/states.json | 47 + placement/seats/mesh-wants/seat.json | 279 ++++++ .../meshmoduleassigner/package-lock.json | 12 + .../ts/seats/meshmoduleassigner/package.json | 13 + .../ts/seats/meshmoduleassigner/src/index.ts | 488 +++++++++ .../ts/seats/meshmoduleassigner/tsconfig.json | 12 + .../ts/seats/meshwants/package-lock.json | 12 + placement/ts/seats/meshwants/package.json | 13 + placement/ts/seats/meshwants/src/index.ts | 211 ++++ placement/ts/seats/meshwants/tsconfig.json | 12 + seats/index.json | 7 +- seats/mesh-placement/seat.json | 137 --- seats/mesh-wants/seat.json | 51 - ts/seats/meshvault/package.json | 19 - 51 files changed, 4626 insertions(+), 266 deletions(-) delete mode 100644 go/seats/meshvault/go.mod create mode 100644 identity-and-access/go/seats/meshnodeenroller/contract.go create mode 100644 identity-and-access/go/seats/meshnodeenroller/contract_test.go create mode 100644 identity-and-access/go/seats/meshnodeenroller/go.mod rename {go => identity-and-access/go}/seats/meshvault/contract.go (75%) rename {go => identity-and-access/go}/seats/meshvault/contract_test.go (85%) create mode 100644 identity-and-access/go/seats/meshvault/go.mod create mode 100644 identity-and-access/module.json create mode 100644 identity-and-access/seats/index.json create mode 100644 identity-and-access/seats/mesh-node-enroller/fixtures/events.json create mode 100644 identity-and-access/seats/mesh-node-enroller/fixtures/states.json rename {seats/mesh-enrolment => identity-and-access/seats/mesh-node-enroller}/seat.json (56%) rename {seats => identity-and-access/seats}/mesh-vault/fixtures/events.json (98%) create mode 100644 identity-and-access/seats/mesh-vault/fixtures/states.json rename {seats => identity-and-access/seats}/mesh-vault/fixtures/verbs.json (100%) rename {seats => identity-and-access/seats}/mesh-vault/seat.json (83%) rename {seats => identity-and-access/seats}/node-secret-service/seat.json (100%) create mode 100644 identity-and-access/ts/seats/meshnodeenroller/package-lock.json create mode 100644 identity-and-access/ts/seats/meshnodeenroller/package.json create mode 100644 identity-and-access/ts/seats/meshnodeenroller/src/index.ts rename {ts/seats/meshvault => identity-and-access/ts/seats/meshnodeenroller}/tsconfig.json (77%) create mode 100644 identity-and-access/ts/seats/meshvault/package-lock.json create mode 100644 identity-and-access/ts/seats/meshvault/package.json rename {ts => identity-and-access/ts}/seats/meshvault/src/index.ts (79%) create mode 100644 identity-and-access/ts/seats/meshvault/tsconfig.json create mode 100644 placement/go/seats/meshmoduleassigner/contract.go create mode 100644 placement/go/seats/meshmoduleassigner/contract_test.go create mode 100644 placement/go/seats/meshmoduleassigner/go.mod create mode 100644 placement/go/seats/meshwants/contract.go create mode 100644 placement/go/seats/meshwants/contract_test.go create mode 100644 placement/go/seats/meshwants/go.mod create mode 100644 placement/module.json create mode 100644 placement/seats/index.json create mode 100644 placement/seats/mesh-module-assigner/fixtures/events.json create mode 100644 placement/seats/mesh-module-assigner/fixtures/states.json create mode 100644 placement/seats/mesh-module-assigner/seat.json create mode 100644 placement/seats/mesh-wants/fixtures/states.json create mode 100644 placement/seats/mesh-wants/seat.json create mode 100644 placement/ts/seats/meshmoduleassigner/package-lock.json create mode 100644 placement/ts/seats/meshmoduleassigner/package.json create mode 100644 placement/ts/seats/meshmoduleassigner/src/index.ts create mode 100644 placement/ts/seats/meshmoduleassigner/tsconfig.json create mode 100644 placement/ts/seats/meshwants/package-lock.json create mode 100644 placement/ts/seats/meshwants/package.json create mode 100644 placement/ts/seats/meshwants/src/index.ts create mode 100644 placement/ts/seats/meshwants/tsconfig.json delete mode 100644 seats/mesh-placement/seat.json delete mode 100644 seats/mesh-wants/seat.json delete mode 100644 ts/seats/meshvault/package.json diff --git a/go/seats/meshvault/go.mod b/go/seats/meshvault/go.mod deleted file mode 100644 index 10f7a40..0000000 --- a/go/seats/meshvault/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module git.novox.be/novox/mesh-contracts/go/seats/meshvault - -go 1.26.0 diff --git a/identity-and-access/go/seats/meshnodeenroller/contract.go b/identity-and-access/go/seats/meshnodeenroller/contract.go new file mode 100644 index 0000000..9f1cc08 --- /dev/null +++ b/identity-and-access/go/seats/meshnodeenroller/contract.go @@ -0,0 +1,260 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package meshnodeenroller is the contract of the seat "mesh-node-enroller", defined whole (seats/mesh-node-enroller/seat.json): The seat that enrols a new machine on the operator's Approve and a one-time token (ADR 0587). Renamed from mesh-enrolment (ADR 0634 §9). +package meshnodeenroller + +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + +// Seat is the seat's name. +const Seat = "mesh-node-enroller" + +const ( + VerbIssue = "issue" + VerbRedeem = "redeem" + VerbConfirm = "confirm" + VerbRevoke = "revoke" + VerbTokens = "tokens" + VerbAgain = "again" +) + +const ( + EventEnrolmentIssued = "enrolment-issued" + EventNodeEnrolled = "node-enrolled" + EventEnrolmentRefused = "enrolment-refused" + EventEnrolmentRevoked = "enrolment-revoked" + EventEnrolmentExpired = "enrolment-expired" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbIssue, VerbRedeem, VerbConfirm, VerbRevoke, VerbTokens, VerbAgain} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventEnrolmentIssued, EventNodeEnrolled, EventEnrolmentRefused, EventEnrolmentRevoked, EventEnrolmentExpired} +} + +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. +const ( + Family = "mesh-node-enroller.events" + FamilyMaxAgeDays = 7 +) + +// SubjectEnrolmentIssued is where enrolment-issued is published. +const SubjectEnrolmentIssued = "mesh.seat.mesh-node-enroller.event.enrolment-issued" + +// SubjectNodeEnrolled is where node-enrolled is published. +const SubjectNodeEnrolled = "mesh.seat.mesh-node-enroller.event.node-enrolled" + +// SubjectEnrolmentRefused is where enrolment-refused is published. +const SubjectEnrolmentRefused = "mesh.seat.mesh-node-enroller.event.enrolment-refused" + +// SubjectEnrolmentRevoked is where enrolment-revoked is published. +const SubjectEnrolmentRevoked = "mesh.seat.mesh-node-enroller.event.enrolment-revoked" + +// SubjectEnrolmentExpired is where enrolment-expired is published. +const SubjectEnrolmentExpired = "mesh.seat.mesh-node-enroller.event.enrolment-expired" + +const ( + StateEnrolments = "enrolments" +) + +// EnrolmentsByNodeNodeKey is the key of one value of the state enrolments under the form "node.". +func EnrolmentsByNodeNodeKey(node string) string { return "node." + node } + +// ParseEnrolmentsByNodeNodeKey reads the parts of a key of the form "node.", in the order of the form; the last result is false for a key of another form. +func ParseEnrolmentsByNodeNodeKey(key string) (string, bool) { + rest := key + if !strings.HasPrefix(rest, "node.") { + return "", false + } + rest = rest[5:] + if rest == "" { + return "", false + } + v1 := rest + rest = "" + if rest != "" { + return "", false + } + return v1, true +} + +// DecodeEnrolmentsByNodeNodeValue reads one value of the state enrolments under the form "node.", refusing a field EnrolmentsByNodeNodeValue does not know and any text after the value. +func DecodeEnrolmentsByNodeNodeValue(raw []byte) (EnrolmentsByNodeNodeValue, error) { + var v EnrolmentsByNodeNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// EnrolmentsByAddressAddressKey is the key of one value of the state enrolments under the form "address.
". +func EnrolmentsByAddressAddressKey(address string) string { return "address." + address } + +// ParseEnrolmentsByAddressAddressKey reads the parts of a key of the form "address.
", in the order of the form; the last result is false for a key of another form. +func ParseEnrolmentsByAddressAddressKey(key string) (string, bool) { + rest := key + if !strings.HasPrefix(rest, "address.") { + return "", false + } + rest = rest[8:] + if rest == "" { + return "", false + } + v1 := rest + rest = "" + if rest != "" { + return "", false + } + return v1, true +} + +// DecodeEnrolmentsByAddressAddressValue reads one value of the state enrolments under the form "address.
", refusing a field EnrolmentsByAddressAddressValue does not know and any text after the value. +func DecodeEnrolmentsByAddressAddressValue(raw []byte) (EnrolmentsByAddressAddressValue, error) { + var v EnrolmentsByAddressAddressValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// Enrolment is one open enrolment. +type Enrolment struct { + Address string `json:"address"` + Ask string `json:"ask,omitempty"` + Caller string `json:"caller,omitempty"` + Expires string `json:"expires,omitempty"` + Issued string `json:"issued,omitempty"` + KeysDigest string `json:"keys-digest,omitempty"` + Node string `json:"node"` + OverlayKey string `json:"overlay-key,omitempty"` + // asked, live or redeeming + Stage string `json:"stage"` + V int64 `json:"v,omitempty"` + Why string `json:"why,omitempty"` +} + +// IssueInput is what issue takes. +type IssueInput struct { + // how long the token lasts, a duration such as 1h or 24h: one hour when not given, at most 24 hours + Expires string `json:"expires,omitempty"` + // the new machine's name in the mesh + Node string `json:"node"` + // the new machine's private network (tunnel) public key, as `nox-mesh-host key` prints it + OverlayKey string `json:"overlay_key"` + // why it joins, shown to the operator with the ask + Why string `json:"why"` +} + +// IssueAnswer is what issue answers. +type IssueAnswer = map[string]any + +// RedeemInput is what redeem takes. +type RedeemInput struct { + // the node the token was issued for + Node string `json:"node"` + // the private network key's public half + OverlayKey string `json:"overlay_key,omitempty"` + // what the machine is, as its record says it + Profile map[string]any `json:"profile,omitempty"` + // the identity key's signature over the redeem's proof bytes (delivery.RedeemProof), base64 + Proof string `json:"proof"` + // the identity key's public half, base64 + PublicKey string `json:"public_key"` + // the sealing key's public half, which the account is sealed to + SealingKey string `json:"sealing_key"` + // the serving key's public half, which the certificate binds + ServingKey string `json:"serving_key,omitempty"` + // a tunnel found on the machine: everything but its private key + Tunnel map[string]any `json:"tunnel,omitempty"` +} + +// RedeemAnswer is what redeem answers. +type RedeemAnswer = map[string]any + +// ConfirmInput is what confirm takes. +type ConfirmInput struct { +} + +// ConfirmAnswer is what confirm answers. +type ConfirmAnswer = map[string]any + +// RevokeInput is what revoke takes. +type RevokeInput struct { + // the node + Node string `json:"node"` + // why, recorded with the hand act + Why string `json:"why"` +} + +// RevokeAnswer is what revoke answers. +type RevokeAnswer = map[string]any + +// TokensInput is what tokens takes. +type TokensInput struct { + // "true": the ended enrolments too + History string `json:"history,omitempty"` + // one node + Node string `json:"node,omitempty"` +} + +// The values of TokensInput's history. +const ( + TokensInputHistoryTrue = "true" + TokensInputHistoryFalse = "false" +) + +// TokensAnswer is what tokens answers. +type TokensAnswer = map[string]any + +// AgainInput is what again takes. +type AgainInput struct { + // the node whose ask is raised again + Node string `json:"node"` +} + +// AgainAnswer is what again answers. +type AgainAnswer = map[string]any + +// EnrolmentIssuedEvent is the body of the event enrolment-issued. +type EnrolmentIssuedEvent = Enrolment + +// NodeEnrolledEvent is the body of the event node-enrolled. +type NodeEnrolledEvent = Enrolment + +// EnrolmentRefusedEvent is the body of the event enrolment-refused. +type EnrolmentRefusedEvent = Enrolment + +// EnrolmentRevokedEvent is the body of the event enrolment-revoked. +type EnrolmentRevokedEvent = Enrolment + +// EnrolmentExpiredEvent is the body of the event enrolment-expired. +type EnrolmentExpiredEvent = Enrolment + +// EnrolmentsByNodeNodeValue is one value of the state enrolments under the key node.. +type EnrolmentsByNodeNodeValue = Enrolment + +// EnrolmentsByAddressAddressValue is one value of the state enrolments under the key address.
. +type EnrolmentsByAddressAddressValue struct { + Ask string `json:"ask,omitempty"` + Issued string `json:"issued,omitempty"` + Node string `json:"node"` + V int64 `json:"v,omitempty"` +} diff --git a/identity-and-access/go/seats/meshnodeenroller/contract_test.go b/identity-and-access/go/seats/meshnodeenroller/contract_test.go new file mode 100644 index 0000000..6e1d2bf --- /dev/null +++ b/identity-and-access/go/seats/meshnodeenroller/contract_test.go @@ -0,0 +1,60 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshnodeenroller + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-node-enroller/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "node-enrolled carries the enrolment", subject: "mesh.seat.mesh-node-enroller.event.node-enrolled", given: "{\"address\":\"10.0.0.9\",\"node\":\"newbox\",\"stage\":\"live\"}", wire: "{\"address\":\"10.0.0.9\",\"node\":\"newbox\",\"stage\":\"live\"}", + decode: func(b []byte) (any, error) { var v NodeEnrolledEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-node-enroller\\.event\\.node-enrolled$")}, + {name: "an open enrolment", subject: "", given: "{\"address\":\"10.0.0.9\",\"node\":\"newbox\",\"stage\":\"live\",\"v\":1}", wire: "{\"address\":\"10.0.0.9\",\"node\":\"newbox\",\"stage\":\"live\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v EnrolmentsByNodeNodeValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a reserved address", subject: "", given: "{\"node\":\"newbox\",\"v\":1}", wire: "{\"node\":\"newbox\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v EnrolmentsByAddressAddressValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/identity-and-access/go/seats/meshnodeenroller/go.mod b/identity-and-access/go/seats/meshnodeenroller/go.mod new file mode 100644 index 0000000..61f3627 --- /dev/null +++ b/identity-and-access/go/seats/meshnodeenroller/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller + +go 1.26.0 diff --git a/go/seats/meshvault/contract.go b/identity-and-access/go/seats/meshvault/contract.go similarity index 75% rename from go/seats/meshvault/contract.go rename to identity-and-access/go/seats/meshvault/contract.go index df00f7c..2f3fe11 100644 --- a/go/seats/meshvault/contract.go +++ b/identity-and-access/go/seats/meshvault/contract.go @@ -3,6 +3,13 @@ // Package meshvault is the contract of the seat "mesh-vault", defined whole (seats/mesh-vault/seat.json): The mesh's secrets. accept and move-asks, which no holder served and nothing called, are left out (ADR 0620 §3). The vault's own events stay under its own subjects (ADR 0620 §2). package meshvault +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + // Seat is the seat's name. const Seat = "mesh-vault" @@ -95,6 +102,102 @@ func HolderSubjectOwnRetired(holder string) string { return "mesh.mod." + holder + ".event.own-retired" } +const ( + StateSigners = "signers" + StateNodeLogins = "node-logins" +) + +// SignersByKeyKey is the key of one value of the state signers under the form "key". +func SignersByKeyKey() string { return "key" } + +// ParseSignersByKeyKey reads the parts of a key of the form "key", in the order of the form; the last result is false for a key of another form. +func ParseSignersByKeyKey(key string) bool { + rest := key + if !strings.HasPrefix(rest, "key") { + return false + } + rest = rest[3:] + if rest != "" { + return false + } + return true +} + +// DecodeSignersByKeyValue reads one value of the state signers under the form "key", refusing a field SignersByKeyValue does not know and any text after the value. +func DecodeSignersByKeyValue(raw []byte) (SignersByKeyValue, error) { + var v SignersByKeyValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// SignersByAuthorityKey is the key of one value of the state signers under the form "authority". +func SignersByAuthorityKey() string { return "authority" } + +// ParseSignersByAuthorityKey reads the parts of a key of the form "authority", in the order of the form; the last result is false for a key of another form. +func ParseSignersByAuthorityKey(key string) bool { + rest := key + if !strings.HasPrefix(rest, "authority") { + return false + } + rest = rest[9:] + if rest != "" { + return false + } + return true +} + +// DecodeSignersByAuthorityValue reads one value of the state signers under the form "authority", refusing a field SignersByAuthorityValue does not know and any text after the value. +func DecodeSignersByAuthorityValue(raw []byte) (SignersByAuthorityValue, error) { + var v SignersByAuthorityValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// NodeLoginsByNodeKey is the key of one value of the state node-logins under the form "". +func NodeLoginsByNodeKey(node string) string { return node } + +// ParseNodeLoginsByNodeKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseNodeLoginsByNodeKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeNodeLoginsByNodeValue reads one value of the state node-logins under the form "", refusing a field NodeLoginsByNodeValue does not know and any text after the value. +func DecodeNodeLoginsByNodeValue(raw []byte) (NodeLoginsByNodeValue, error) { + var v NodeLoginsByNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + // GetInput is what get takes. type GetInput struct { // the secret's name @@ -241,11 +344,11 @@ type IssueAccountAnswer = map[string]any type SignInput struct { // node-certificate or enrolment Kind string `json:"kind"` - // the machine, for a node certificate: the caller's own, or the node mesh-enrolment enrols + // the machine, for a node certificate: the caller's own, or the node mesh-node-enroller enrols Node string `json:"node,omitempty"` // the redeem answer's bytes in base64, for an enrolment Reply string `json:"reply,omitempty"` - // from mesh-enrolment: the serving key's public half the enrolling node presented + // from mesh-node-enroller: the serving key's public half the enrolling node presented ServingKey string `json:"serving_key,omitempty"` } @@ -267,7 +370,7 @@ type SignAnswer struct { Signer string `json:"signer"` } -// EnrolmentTokenInputTunnel is the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range. +// EnrolmentTokenInputTunnel is the node's first tunnel, built by mesh-node-enroller at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range. type EnrolmentTokenInputTunnel struct { // the joining node's tunnel address Address string `json:"address"` @@ -291,7 +394,7 @@ type EnrolmentTokenInput struct { Node string `json:"node"` // "true": answer the token sealed to the operator key Sealed string `json:"sealed,omitempty"` - // the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range + // the node's first tunnel, built by mesh-node-enroller at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range Tunnel *EnrolmentTokenInputTunnel `json:"tunnel,omitempty"` } @@ -431,3 +534,37 @@ type OwnRetiredHolderEvent struct { // the node Node string `json:"node"` } + +// SignersByKeyValue is one value of the state signers under the key key. +type SignersByKeyValue struct { + Fingerprint string `json:"fingerprint"` + // the signing key's public half, standard base64 + Key string `json:"key"` + Made string `json:"made"` + V int64 `json:"v,omitempty"` +} + +// SignersByAuthorityValue is one value of the state signers under the key authority. +type SignersByAuthorityValue struct { + // the authority certificate, PEM + Certificate string `json:"certificate"` + Fingerprint string `json:"fingerprint"` + Made string `json:"made"` + V int64 `json:"v,omitempty"` +} + +// NodeLoginsByNodeValue is one value of the state node-logins under the key . +type NodeLoginsByNodeValue struct { + Acts []string `json:"acts,omitempty"` + // the user list's revision that held both logins, from ready on + ListRevision int64 `json:"list-revision,omitempty"` + // the login in use + Login string `json:"login"` + Node string `json:"node"` + // the login rotating to, empty when none + Rotating string `json:"rotating,omitempty"` + Since string `json:"since"` + // begun, ready or confirmed; empty when no rotation + Stage string `json:"stage,omitempty"` + V int64 `json:"v,omitempty"` +} diff --git a/go/seats/meshvault/contract_test.go b/identity-and-access/go/seats/meshvault/contract_test.go similarity index 85% rename from go/seats/meshvault/contract_test.go rename to identity-and-access/go/seats/meshvault/contract_test.go index a358263..8b8c520 100644 --- a/go/seats/meshvault/contract_test.go +++ b/identity-and-access/go/seats/meshvault/contract_test.go @@ -29,7 +29,7 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.own-replaced$")}, {name: "account-issued under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.account-issued", given: "{}", wire: "{}", decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.account-issued$")}, - {name: "signed under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.signed", given: "{\"at\":\"2026-11-04T13:00:00Z\",\"caller\":\"mesh-enrolment\",\"digest\":\"sha256:0000000000000000000000000000000000000000000000000000000000000000\",\"kind\":\"enrolment\",\"signer\":\"sha256:1111111111111111111111111111111111111111111111111111111111111111\"}", wire: "{\"at\":\"2026-11-04T13:00:00Z\",\"caller\":\"mesh-enrolment\",\"digest\":\"sha256:0000000000000000000000000000000000000000000000000000000000000000\",\"kind\":\"enrolment\",\"signer\":\"sha256:1111111111111111111111111111111111111111111111111111111111111111\"}", + {name: "signed under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.signed", given: "{\"at\":\"2026-11-04T13:00:00Z\",\"caller\":\"mesh-node-enroller\",\"digest\":\"sha256:0000000000000000000000000000000000000000000000000000000000000000\",\"kind\":\"enrolment\",\"signer\":\"sha256:1111111111111111111111111111111111111111111111111111111111111111\"}", wire: "{\"at\":\"2026-11-04T13:00:00Z\",\"caller\":\"mesh-node-enroller\",\"digest\":\"sha256:0000000000000000000000000000000000000000000000000000000000000000\",\"kind\":\"enrolment\",\"signer\":\"sha256:1111111111111111111111111111111111111111111111111111111111111111\"}", decode: func(b []byte) (any, error) { var v SignedHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.signed$")}, {name: "rotation-begun under the vault's own subjects", subject: "mesh.mod.mesh-vault.event.rotation-begun", given: "{}", wire: "{}", decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.rotation-begun$")}, @@ -39,6 +39,10 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v map[string]any; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.rotation-done$")}, {name: "own-retired", subject: "mesh.mod.mesh-vault.event.own-retired", given: "{\"ask\":\"a1\",\"caller\":\"mesh-config\",\"module\":\"messenger\",\"name\":\"token\",\"node\":\"ace\"}", wire: "{\"ask\":\"a1\",\"caller\":\"mesh-config\",\"module\":\"messenger\",\"name\":\"token\",\"node\":\"ace\"}", decode: func(b []byte) (any, error) { var v OwnRetiredHolderEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.mod\\.[a-z0-9-]+\\.event\\.own-retired$")}, + {name: "the signing key", subject: "", given: "{\"fingerprint\":\"sha256:aa\",\"key\":\"AAAA\",\"made\":\"2026-11-05T12:00:00Z\",\"v\":1}", wire: "{\"fingerprint\":\"sha256:aa\",\"key\":\"AAAA\",\"made\":\"2026-11-05T12:00:00Z\",\"v\":1}", + decode: func(b []byte) (any, error) { var v SignersByKeyValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a node's login", subject: "", given: "{\"login\":\"node.ace\",\"node\":\"ace\",\"since\":\"2026-11-05T12:00:00Z\",\"v\":1}", wire: "{\"login\":\"node.ace\",\"node\":\"ace\",\"since\":\"2026-11-05T12:00:00Z\",\"v\":1}", + decode: func(b []byte) (any, error) { var v NodeLoginsByNodeValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "retire on a warrant", subject: "", given: "{\"ask\":\"a1\",\"module\":\"messenger\",\"name\":\"token\",\"node\":\"ace\",\"warrant\":\"w-1\"}", wire: "{\"ask\":\"a1\",\"module\":\"messenger\",\"name\":\"token\",\"node\":\"ace\",\"warrant\":\"w-1\"}", decode: func(b []byte) (any, error) { var v RetireInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "a node certificate signed", subject: "", given: "{\"authority\":\"-----BEGIN CERTIFICATE-----\",\"certificate\":\"-----BEGIN CERTIFICATE-----\",\"digest\":\"sha256:0000000000000000000000000000000000000000000000000000000000000000\",\"kind\":\"node-certificate\",\"node\":\"ace\",\"signer\":\"sha256:1111111111111111111111111111111111111111111111111111111111111111\"}", wire: "{\"authority\":\"-----BEGIN CERTIFICATE-----\",\"certificate\":\"-----BEGIN CERTIFICATE-----\",\"digest\":\"sha256:0000000000000000000000000000000000000000000000000000000000000000\",\"kind\":\"node-certificate\",\"node\":\"ace\",\"signer\":\"sha256:1111111111111111111111111111111111111111111111111111111111111111\"}", diff --git a/identity-and-access/go/seats/meshvault/go.mod b/identity-and-access/go/seats/meshvault/go.mod new file mode 100644 index 0000000..a6cf36c --- /dev/null +++ b/identity-and-access/go/seats/meshvault/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshvault + +go 1.26.0 diff --git a/identity-and-access/module.json b/identity-and-access/module.json new file mode 100644 index 0000000..20e2d2c --- /dev/null +++ b/identity-and-access/module.json @@ -0,0 +1,67 @@ +{ + "module": "mesh-identity-and-access-contracts", + "version": "1", + "build": { + "artifacts": [ + { + "name": "go-meshvault", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshvault" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-meshnodeenroller", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshnodeenroller" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-meshvaultclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/meshvaultclient" + ], + "series": "0.1" + }, + { + "name": "go-meshnodeenrollerclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/meshnodeenrollerclient" + ], + "series": "0.1" + }, + { + "name": "ts-meshvault", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshvault" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-meshnodeenroller", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshnodeenroller" + ], + "series": "0.1", + "generated-from": "." + } + ] + }, + "resources": [] +} diff --git a/identity-and-access/seats/index.json b/identity-and-access/seats/index.json new file mode 100644 index 0000000..72e28e3 --- /dev/null +++ b/identity-and-access/seats/index.json @@ -0,0 +1,8 @@ +{ + "format": 1, + "seats": [ + "mesh-node-enroller", + "mesh-vault", + "node-secret-service" + ] +} diff --git a/identity-and-access/seats/mesh-node-enroller/fixtures/events.json b/identity-and-access/seats/mesh-node-enroller/fixtures/events.json new file mode 100644 index 0000000..22c07c1 --- /dev/null +++ b/identity-and-access/seats/mesh-node-enroller/fixtures/events.json @@ -0,0 +1,12 @@ +[ + { + "name": "node-enrolled carries the enrolment", + "of": "event:node-enrolled", + "subject": "mesh.seat.mesh-node-enroller.event.node-enrolled", + "given": { + "node": "newbox", + "address": "10.0.0.9", + "stage": "live" + } + } +] diff --git a/identity-and-access/seats/mesh-node-enroller/fixtures/states.json b/identity-and-access/seats/mesh-node-enroller/fixtures/states.json new file mode 100644 index 0000000..e0e547f --- /dev/null +++ b/identity-and-access/seats/mesh-node-enroller/fixtures/states.json @@ -0,0 +1,20 @@ +[ + { + "name": "an open enrolment", + "of": "state:enrolments/node.", + "given": { + "v": 1, + "node": "newbox", + "address": "10.0.0.9", + "stage": "live" + } + }, + { + "name": "a reserved address", + "of": "state:enrolments/address.
", + "given": { + "v": 1, + "node": "newbox" + } + } +] diff --git a/seats/mesh-enrolment/seat.json b/identity-and-access/seats/mesh-node-enroller/seat.json similarity index 56% rename from seats/mesh-enrolment/seat.json rename to identity-and-access/seats/mesh-node-enroller/seat.json index b611558..3bbe3be 100644 --- a/seats/mesh-enrolment/seat.json +++ b/identity-and-access/seats/mesh-node-enroller/seat.json @@ -1,9 +1,10 @@ { "format": 1, - "seat": "mesh-enrolment", + "seat": "mesh-node-enroller", "scope": "mesh", - "grain": "seat-list", - "decision": "novox/hq ADR 0587", + "grain": "whole", + "decision": "novox/hq ADR 0587, ADR 0634, ADR 0635", + "description": "The seat that enrols a new machine on the operator's Approve and a one-time token (ADR 0587). Renamed from mesh-enrolment (ADR 0634 §9).", "verbs": [ { "name": "issue", @@ -33,11 +34,15 @@ "overlay_key" ], "type": "object" + }, + "answer": { + "type": "object", + "description": "the enrolment's stage, address and ask, or every finding refused at once" } }, { "name": "redeem", - "description": "Spend a token: asked by a joining node-engine dialled as enrol.\u003cnode\u003e alone, with the public halves of its keys and its identity key's proof over them. Spends the token, asks the vault for the node's account and certificate, writes the node into placement, and answers the account sealed to the node, its certificate and the vault's public half, signed by the vault. Asked again with the same keys it finishes what is not done and answers the same; with other keys it is refused.", + "description": "Spend a token: asked by a joining node-engine dialled as enrol. alone, with the public halves of its keys and its identity key's proof over them. Spends the token, asks the vault for the node's account and certificate, writes the node into placement, and answers the account sealed to the node, its certificate and the vault's public half, signed by the vault. Asked again with the same keys it finishes what is not done and answers the same; with other keys it is refused.", "input": { "additionalProperties": false, "properties": { @@ -81,6 +86,10 @@ "proof" ], "type": "object" + }, + "answer": { + "type": "object", + "description": "the account sealed to the node, its certificate and the vault's public half, signed by the vault" } }, { @@ -89,6 +98,10 @@ "input": { "properties": {}, "type": "object" + }, + "answer": { + "type": "object", + "description": "the enrolment ended" } }, { @@ -110,6 +123,10 @@ "why" ], "type": "object" + }, + "answer": { + "type": "object", + "description": "the enrolment ended" } }, { @@ -131,6 +148,10 @@ } }, "type": "object" + }, + "answer": { + "type": "object", + "description": "every open enrolment, or one" } }, { @@ -147,24 +168,139 @@ "node" ], "type": "object" + }, + "answer": { + "type": "object", + "description": "the ask raised again" } } ], "events": [ { - "name": "enrolment-issued" + "name": "enrolment-issued", + "body": { + "$ref": "#/defs/Enrolment" + } }, { - "name": "node-enrolled" + "name": "node-enrolled", + "body": { + "$ref": "#/defs/Enrolment" + } }, { - "name": "enrolment-refused" + "name": "enrolment-refused", + "body": { + "$ref": "#/defs/Enrolment" + } }, { - "name": "enrolment-revoked" + "name": "enrolment-revoked", + "body": { + "$ref": "#/defs/Enrolment" + } }, { - "name": "enrolment-expired" + "name": "enrolment-expired", + "body": { + "$ref": "#/defs/Enrolment" + } } - ] + ], + "family": { + "name": "mesh-node-enroller.events", + "max-age-days": 7 + }, + "states": [ + { + "name": "enrolments", + "template": "state", + "key-forms": [ + { + "key": "node.", + "value": { + "$ref": "#/defs/Enrolment" + }, + "description": "a node's enrolment, living until the token expires while live" + }, + { + "key": "address.
", + "value": { + "type": "object", + "description": "the private-network address reserved at issue", + "properties": { + "v": { + "type": "integer" + }, + "node": { + "type": "string" + }, + "ask": { + "type": "string" + }, + "issued": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "node" + ] + }, + "description": "an address reserved for a node" + } + ], + "history": 5, + "description": "open enrolments and the addresses reserved for them" + } + ], + "defs": { + "Enrolment": { + "type": "object", + "description": "one open enrolment", + "properties": { + "v": { + "type": "integer" + }, + "node": { + "type": "string" + }, + "address": { + "type": "string" + }, + "why": { + "type": "string" + }, + "caller": { + "type": "string" + }, + "ask": { + "type": "string" + }, + "stage": { + "type": "string", + "description": "asked, live or redeeming" + }, + "issued": { + "type": "string", + "format": "date-time" + }, + "expires": { + "type": "string", + "format": "date-time" + }, + "keys-digest": { + "type": "string" + }, + "overlay-key": { + "type": "string" + } + }, + "required": [ + "node", + "address", + "stage" + ] + } + } } diff --git a/seats/mesh-vault/fixtures/events.json b/identity-and-access/seats/mesh-vault/fixtures/events.json similarity index 98% rename from seats/mesh-vault/fixtures/events.json rename to identity-and-access/seats/mesh-vault/fixtures/events.json index ef46519..4a64125 100644 --- a/seats/mesh-vault/fixtures/events.json +++ b/identity-and-access/seats/mesh-vault/fixtures/events.json @@ -40,7 +40,7 @@ "of": "holder-event:signed", "subject": "mesh.mod.mesh-vault.event.signed", "given": { - "caller": "mesh-enrolment", + "caller": "mesh-node-enroller", "kind": "enrolment", "digest": "sha256:0000000000000000000000000000000000000000000000000000000000000000", "signer": "sha256:1111111111111111111111111111111111111111111111111111111111111111", diff --git a/identity-and-access/seats/mesh-vault/fixtures/states.json b/identity-and-access/seats/mesh-vault/fixtures/states.json new file mode 100644 index 0000000..f0b72c0 --- /dev/null +++ b/identity-and-access/seats/mesh-vault/fixtures/states.json @@ -0,0 +1,22 @@ +[ + { + "name": "the signing key", + "of": "state:signers/key", + "given": { + "v": 1, + "key": "AAAA", + "fingerprint": "sha256:aa", + "made": "2026-11-05T12:00:00Z" + } + }, + { + "name": "a node's login", + "of": "state:node-logins/", + "given": { + "v": 1, + "node": "ace", + "login": "node.ace", + "since": "2026-11-05T12:00:00Z" + } + } +] diff --git a/seats/mesh-vault/fixtures/verbs.json b/identity-and-access/seats/mesh-vault/fixtures/verbs.json similarity index 100% rename from seats/mesh-vault/fixtures/verbs.json rename to identity-and-access/seats/mesh-vault/fixtures/verbs.json diff --git a/seats/mesh-vault/seat.json b/identity-and-access/seats/mesh-vault/seat.json similarity index 83% rename from seats/mesh-vault/seat.json rename to identity-and-access/seats/mesh-vault/seat.json index 5a115a6..177adcc 100644 --- a/seats/mesh-vault/seat.json +++ b/identity-and-access/seats/mesh-vault/seat.json @@ -3,7 +3,7 @@ "seat": "mesh-vault", "scope": "mesh", "grain": "whole", - "decision": "novox/hq ADR 0161", + "decision": "novox/hq ADR 0161, ADR 0634, ADR 0635", "delivers": "secret", "verbs": [ { @@ -299,7 +299,7 @@ }, { "name": "sign", - "description": "Sign with the vault's key or authority (novox/hq ADR 0550 §2): kind node-certificate signs the certificate of a machine's internal name, asked by that machine's node-engine for the serving key its record states, built from bus state alone, or by mesh-enrolment for a node it enrols, for the serving key the node presented at redeem (ADR 0587 §3); kind enrolment signs a redeem answer under its own context, for mesh-enrolment alone. Answers the certificate or the signature and the signer's fingerprint, and says signed; never a key.", + "description": "Sign with the vault's key or authority (novox/hq ADR 0550 §2): kind node-certificate signs the certificate of a machine's internal name, asked by that machine's node-engine for the serving key its record states, built from bus state alone, or by mesh-node-enroller for a node it enrols, for the serving key the node presented at redeem (ADR 0587 §3); kind enrolment signs a redeem answer under its own context, for mesh-node-enroller alone. Answers the certificate or the signature and the signer's fingerprint, and says signed; never a key.", "optional": true, "input": { "properties": { @@ -308,7 +308,7 @@ "type": "string" }, "node": { - "description": "the machine, for a node certificate: the caller's own, or the node mesh-enrolment enrols", + "description": "the machine, for a node certificate: the caller's own, or the node mesh-node-enroller enrols", "type": "string" }, "reply": { @@ -316,7 +316,7 @@ "type": "string" }, "serving_key": { - "description": "from mesh-enrolment: the serving key's public half the enrolling node presented", + "description": "from mesh-node-enroller: the serving key's public half the enrolling node presented", "type": "string" } }, @@ -367,7 +367,7 @@ }, { "name": "enrolment-token", - "description": "Make a one-time token for a node mesh-enrolment issues on the operator's warrant: its secret kept with its copy sealed to the operator key and its hash written as the bus user enrol.. Answers only what the joining machine must trust (the bus's address, the bus certificate's fingerprint, the vault's public signing key), never the token; with sealed, the token sealed to the operator key, for tokens to relay; with drop, the token and its hash dropped, at the enrolment's end. Asked by mesh-enrolment alone, only from the lease write. The token carries the first tunnel mesh-enrolment built at issue (ADR 0587 §3, insight of 2026-11-02).", + "description": "Make a one-time token for a node mesh-node-enroller issues on the operator's warrant: its secret kept with its copy sealed to the operator key and its hash written as the bus user enrol.. Answers only what the joining machine must trust (the bus's address, the bus certificate's fingerprint, the vault's public signing key), never the token; with sealed, the token sealed to the operator key, for tokens to relay; with drop, the token and its hash dropped, at the enrolment's end. Asked by mesh-node-enroller alone, only from the lease write. The token carries the first tunnel mesh-node-enroller built at issue (ADR 0587 §3, insight of 2026-11-02).", "optional": true, "input": { "properties": { @@ -397,7 +397,7 @@ }, "tunnel": { "additionalProperties": false, - "description": "the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range", + "description": "the node's first tunnel, built by mesh-node-enroller at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range", "properties": { "address": { "description": "the joining node's tunnel address", @@ -471,7 +471,7 @@ }, { "name": "node-account", - "description": "Mint a joining node's bus account node., sealed to the sealing key it presented, and write its hash; asked again for the same node and keys, answers the account it already minted; with retire, the account it minted for a revoked enrolment retired. Writes the node's node-logins key naming node.. Asked by mesh-enrolment alone. Never the value in the clear.", + "description": "Mint a joining node's bus account node., sealed to the sealing key it presented, and write its hash; asked again for the same node and keys, answers the account it already minted; with retire, the account it minted for a revoked enrolment retired. Writes the node's node-logins key naming node.. Asked by mesh-node-enroller alone. Never the value in the clear.", "optional": true, "input": { "properties": { @@ -757,5 +757,128 @@ ] } } + ], + "states": [ + { + "name": "signers", + "template": "state", + "key-forms": [ + { + "key": "key", + "value": { + "type": "object", + "description": "the vault's signing key's public half", + "properties": { + "v": { + "type": "integer" + }, + "key": { + "type": "string", + "description": "the signing key's public half, standard base64" + }, + "fingerprint": { + "type": "string" + }, + "made": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "key", + "fingerprint", + "made" + ] + }, + "description": "the signing key's public half" + }, + { + "key": "authority", + "value": { + "type": "object", + "description": "the vault's authority certificate", + "properties": { + "v": { + "type": "integer" + }, + "certificate": { + "type": "string", + "description": "the authority certificate, PEM" + }, + "fingerprint": { + "type": "string" + }, + "made": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "certificate", + "fingerprint", + "made" + ] + }, + "description": "the authority certificate" + } + ], + "history": 5, + "description": "the vault's public signing halves; written by the vault alone" + }, + { + "name": "node-logins", + "template": "state", + "key-forms": [ + { + "key": "", + "value": { + "type": "object", + "description": "a node's two logins and the rotation's stage", + "properties": { + "v": { + "type": "integer" + }, + "node": { + "type": "string" + }, + "login": { + "type": "string", + "description": "the login in use" + }, + "rotating": { + "type": "string", + "description": "the login rotating to, empty when none" + }, + "stage": { + "type": "string", + "description": "begun, ready or confirmed; empty when no rotation" + }, + "since": { + "type": "string", + "format": "date-time" + }, + "list-revision": { + "type": "integer", + "description": "the user list's revision that held both logins, from ready on" + }, + "acts": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "node", + "login", + "since" + ] + }, + "description": "a node" + } + ], + "history": 5, + "description": "which login a node uses and the stage of its rotation; the key lives while the node does" + } ] } diff --git a/seats/node-secret-service/seat.json b/identity-and-access/seats/node-secret-service/seat.json similarity index 100% rename from seats/node-secret-service/seat.json rename to identity-and-access/seats/node-secret-service/seat.json diff --git a/identity-and-access/ts/seats/meshnodeenroller/package-lock.json b/identity-and-access/ts/seats/meshnodeenroller/package-lock.json new file mode 100644 index 0000000..8e7f57d --- /dev/null +++ b/identity-and-access/ts/seats/meshnodeenroller/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-identity-and-access-contracts-mesh-node-enroller", + "packages": { + "": { + "name": "@novox/mesh-identity-and-access-contracts-mesh-node-enroller", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/identity-and-access/ts/seats/meshnodeenroller/package.json b/identity-and-access/ts/seats/meshnodeenroller/package.json new file mode 100644 index 0000000..88192fc --- /dev/null +++ b/identity-and-access/ts/seats/meshnodeenroller/package.json @@ -0,0 +1,13 @@ +{ + "description": "The contract of the seat mesh-node-enroller, generated from its definition.", + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "src" + ], + "name": "@novox/mesh-identity-and-access-contracts-mesh-node-enroller", + "type": "module", + "types": "./src/index.ts", + "version": "0.1.0" +} diff --git a/identity-and-access/ts/seats/meshnodeenroller/src/index.ts b/identity-and-access/ts/seats/meshnodeenroller/src/index.ts new file mode 100644 index 0000000..4068a1d --- /dev/null +++ b/identity-and-access/ts/seats/meshnodeenroller/src/index.ts @@ -0,0 +1,191 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-node-enroller", defined whole (seats/mesh-node-enroller/seat.json): The seat that enrols a new machine on the operator's Approve and a one-time token (ADR 0587). Renamed from mesh-enrolment (ADR 0634 §9). + +export const seat = "mesh-node-enroller"; + +export const verbs = { + issue: "issue", + redeem: "redeem", + confirm: "confirm", + revoke: "revoke", + tokens: "tokens", + again: "again", +} as const; + +export const events = { + enrolmentIssued: "enrolment-issued", + nodeEnrolled: "node-enrolled", + enrolmentRefused: "enrolment-refused", + enrolmentRevoked: "enrolment-revoked", + enrolmentExpired: "enrolment-expired", +} as const; + +export const family = { name: "mesh-node-enroller.events", maxAgeDays: 7 } as const; + +export const enrolmentIssuedSubject = "mesh.seat.mesh-node-enroller.event.enrolment-issued"; + +export const nodeEnrolledSubject = "mesh.seat.mesh-node-enroller.event.node-enrolled"; + +export const enrolmentRefusedSubject = "mesh.seat.mesh-node-enroller.event.enrolment-refused"; + +export const enrolmentRevokedSubject = "mesh.seat.mesh-node-enroller.event.enrolment-revoked"; + +export const enrolmentExpiredSubject = "mesh.seat.mesh-node-enroller.event.enrolment-expired"; + +export const states = { + enrolments: "enrolments", +} as const; + +/** The key of one value of the state enrolments under the form "node.". */ +export function enrolmentsByNodeNodeKey(node: string): string { + return "node." + node; +} + +/** The parts of a key of the form "node.", or undefined for a key of another form. */ +export function parseEnrolmentsByNodeNodeKey(key: string): { node: string } | undefined { + let rest = key; + if (!rest.startsWith("node.")) return undefined; + rest = rest.slice(5); + if (rest === "") return undefined; + const node = rest; + rest = ""; + if (rest !== "") return undefined; + return { node }; +} + +/** The key of one value of the state enrolments under the form "address.
". */ +export function enrolmentsByAddressAddressKey(address: string): string { + return "address." + address; +} + +/** The parts of a key of the form "address.
", or undefined for a key of another form. */ +export function parseEnrolmentsByAddressAddressKey(key: string): { address: string } | undefined { + let rest = key; + if (!rest.startsWith("address.")) return undefined; + rest = rest.slice(8); + if (rest === "") return undefined; + const address = rest; + rest = ""; + if (rest !== "") return undefined; + return { address }; +} + +/** one open enrolment */ +export interface Enrolment { + "address": string; + "ask"?: string; + "caller"?: string; + "expires"?: string; + "issued"?: string; + "keys-digest"?: string; + "node": string; + "overlay-key"?: string; + /** asked, live or redeeming */ + "stage": string; + "v"?: number; + "why"?: string; +} + +/** is what issue takes. */ +export interface IssueInput { + /** how long the token lasts, a duration such as 1h or 24h: one hour when not given, at most 24 hours */ + "expires"?: string; + /** the new machine's name in the mesh */ + "node": string; + /** the new machine's private network (tunnel) public key, as `nox-mesh-host key` prints it */ + "overlay_key": string; + /** why it joins, shown to the operator with the ask */ + "why": string; +} + +/** is what issue answers. */ +export type IssueAnswer = Record; + +/** is what redeem takes. */ +export interface RedeemInput { + /** the node the token was issued for */ + "node": string; + /** the private network key's public half */ + "overlay_key"?: string; + /** what the machine is, as its record says it */ + "profile"?: Record; + /** the identity key's signature over the redeem's proof bytes (delivery.RedeemProof), base64 */ + "proof": string; + /** the identity key's public half, base64 */ + "public_key": string; + /** the sealing key's public half, which the account is sealed to */ + "sealing_key": string; + /** the serving key's public half, which the certificate binds */ + "serving_key"?: string; + /** a tunnel found on the machine: everything but its private key */ + "tunnel"?: Record; +} + +/** is what redeem answers. */ +export type RedeemAnswer = Record; + +/** is what confirm takes. */ +export interface ConfirmInput { +} + +/** is what confirm answers. */ +export type ConfirmAnswer = Record; + +/** is what revoke takes. */ +export interface RevokeInput { + /** the node */ + "node": string; + /** why, recorded with the hand act */ + "why": string; +} + +/** is what revoke answers. */ +export type RevokeAnswer = Record; + +/** is what tokens takes. */ +export interface TokensInput { + /** "true": the ended enrolments too */ + "history"?: "true" | "false"; + /** one node */ + "node"?: string; +} + +/** is what tokens answers. */ +export type TokensAnswer = Record; + +/** is what again takes. */ +export interface AgainInput { + /** the node whose ask is raised again */ + "node": string; +} + +/** is what again answers. */ +export type AgainAnswer = Record; + +/** is the body of the event enrolment-issued. */ +export type EnrolmentIssuedEvent = Enrolment; + +/** is the body of the event node-enrolled. */ +export type NodeEnrolledEvent = Enrolment; + +/** is the body of the event enrolment-refused. */ +export type EnrolmentRefusedEvent = Enrolment; + +/** is the body of the event enrolment-revoked. */ +export type EnrolmentRevokedEvent = Enrolment; + +/** is the body of the event enrolment-expired. */ +export type EnrolmentExpiredEvent = Enrolment; + +/** is one value of the state enrolments under the key node.. */ +export type EnrolmentsByNodeNodeValue = Enrolment; + +/** is one value of the state enrolments under the key address.
. */ +export interface EnrolmentsByAddressAddressValue { + "ask"?: string; + "issued"?: string; + "node": string; + "v"?: number; +} + diff --git a/ts/seats/meshvault/tsconfig.json b/identity-and-access/ts/seats/meshnodeenroller/tsconfig.json similarity index 77% rename from ts/seats/meshvault/tsconfig.json rename to identity-and-access/ts/seats/meshnodeenroller/tsconfig.json index c610421..60274c6 100644 --- a/ts/seats/meshvault/tsconfig.json +++ b/identity-and-access/ts/seats/meshnodeenroller/tsconfig.json @@ -3,9 +3,7 @@ "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", - "declaration": true, - "outDir": "dist", - "rootDir": "src", + "noEmit": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true diff --git a/identity-and-access/ts/seats/meshvault/package-lock.json b/identity-and-access/ts/seats/meshvault/package-lock.json new file mode 100644 index 0000000..c7aa797 --- /dev/null +++ b/identity-and-access/ts/seats/meshvault/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-identity-and-access-contracts-mesh-vault", + "packages": { + "": { + "name": "@novox/mesh-identity-and-access-contracts-mesh-vault", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/identity-and-access/ts/seats/meshvault/package.json b/identity-and-access/ts/seats/meshvault/package.json new file mode 100644 index 0000000..39944e5 --- /dev/null +++ b/identity-and-access/ts/seats/meshvault/package.json @@ -0,0 +1,13 @@ +{ + "description": "The contract of the seat mesh-vault, generated from its definition.", + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "src" + ], + "name": "@novox/mesh-identity-and-access-contracts-mesh-vault", + "type": "module", + "types": "./src/index.ts", + "version": "0.1.0" +} diff --git a/ts/seats/meshvault/src/index.ts b/identity-and-access/ts/seats/meshvault/src/index.ts similarity index 79% rename from ts/seats/meshvault/src/index.ts rename to identity-and-access/ts/seats/meshvault/src/index.ts index c78145a..112701a 100644 --- a/ts/seats/meshvault/src/index.ts +++ b/identity-and-access/ts/seats/meshvault/src/index.ts @@ -80,6 +80,54 @@ export function ownRetiredHolderSubject(holder: string): string { return "mesh.mod." + holder + ".event.own-retired"; } +export const states = { + signers: "signers", + nodeLogins: "node-logins", +} as const; + +/** The key of one value of the state signers under the form "key". */ +export function signersByKeyKey(): string { + return "key"; +} + +/** The parts of a key of the form "key", or undefined for a key of another form. */ +export function parseSignersByKeyKey(key: string): { } | undefined { + let rest = key; + if (!rest.startsWith("key")) return undefined; + rest = rest.slice(3); + if (rest !== "") return undefined; + return { }; +} + +/** The key of one value of the state signers under the form "authority". */ +export function signersByAuthorityKey(): string { + return "authority"; +} + +/** The parts of a key of the form "authority", or undefined for a key of another form. */ +export function parseSignersByAuthorityKey(key: string): { } | undefined { + let rest = key; + if (!rest.startsWith("authority")) return undefined; + rest = rest.slice(9); + if (rest !== "") return undefined; + return { }; +} + +/** The key of one value of the state node-logins under the form "". */ +export function nodeLoginsByNodeKey(node: string): string { + return node; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseNodeLoginsByNodeKey(key: string): { node: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const node = rest; + rest = ""; + if (rest !== "") return undefined; + return { node }; +} + /** is what get takes. */ export interface GetInput { /** the secret's name */ @@ -225,11 +273,11 @@ export type IssueAccountAnswer = Record; export interface SignInput { /** node-certificate or enrolment */ "kind": string; - /** the machine, for a node certificate: the caller's own, or the node mesh-enrolment enrols */ + /** the machine, for a node certificate: the caller's own, or the node mesh-node-enroller enrols */ "node"?: string; /** the redeem answer's bytes in base64, for an enrolment */ "reply"?: string; - /** from mesh-enrolment: the serving key's public half the enrolling node presented */ + /** from mesh-node-enroller: the serving key's public half the enrolling node presented */ "serving_key"?: string; } @@ -251,7 +299,7 @@ export interface SignAnswer { "signer": string; } -/** the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range */ +/** the node's first tunnel, built by mesh-node-enroller at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range */ export interface EnrolmentTokenInputTunnel { /** the joining node's tunnel address */ "address": string; @@ -275,7 +323,7 @@ export interface EnrolmentTokenInput { "node": string; /** "true": answer the token sealed to the operator key */ "sealed"?: "true" | "false"; - /** the node's first tunnel, built by mesh-enrolment at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range */ + /** the node's first tunnel, built by mesh-node-enroller at issue and carried in the token: the hub's public key, the hub's endpoint, the node's tunnel address, its own tunnel key and the network's range */ "tunnel"?: EnrolmentTokenInputTunnel; } @@ -398,3 +446,37 @@ export interface OwnRetiredHolderEvent { "node": string; } +/** is one value of the state signers under the key key. */ +export interface SignersByKeyValue { + "fingerprint": string; + /** the signing key's public half, standard base64 */ + "key": string; + "made": string; + "v"?: number; +} + +/** is one value of the state signers under the key authority. */ +export interface SignersByAuthorityValue { + /** the authority certificate, PEM */ + "certificate": string; + "fingerprint": string; + "made": string; + "v"?: number; +} + +/** is one value of the state node-logins under the key . */ +export interface NodeLoginsByNodeValue { + "acts"?: string[]; + /** the user list's revision that held both logins, from ready on */ + "list-revision"?: number; + /** the login in use */ + "login": string; + "node": string; + /** the login rotating to, empty when none */ + "rotating"?: string; + "since": string; + /** begun, ready or confirmed; empty when no rotation */ + "stage"?: string; + "v"?: number; +} + diff --git a/identity-and-access/ts/seats/meshvault/tsconfig.json b/identity-and-access/ts/seats/meshvault/tsconfig.json new file mode 100644 index 0000000..60274c6 --- /dev/null +++ b/identity-and-access/ts/seats/meshvault/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/module.json b/module.json index c850c06..ca42cfb 100644 --- a/module.json +++ b/module.json @@ -99,16 +99,6 @@ "series": "0.1", "generated-from": "." }, - { - "name": "go-meshvault", - "kind": "package", - "language": "go", - "paths": [ - "go/seats/meshvault" - ], - "series": "0.1", - "generated-from": "." - }, { "name": "go-nodeengine", "kind": "package", @@ -189,16 +179,6 @@ "series": "0.1", "generated-from": "." }, - { - "name": "ts-meshvault", - "kind": "package", - "language": "typescript", - "paths": [ - "ts/seats/meshvault" - ], - "series": "0.1", - "generated-from": "." - }, { "name": "ts-nodeengine", "kind": "package", diff --git a/placement/go/seats/meshmoduleassigner/contract.go b/placement/go/seats/meshmoduleassigner/contract.go new file mode 100644 index 0000000..843340f --- /dev/null +++ b/placement/go/seats/meshmoduleassigner/contract.go @@ -0,0 +1,665 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package meshmoduleassigner is the contract of the seat "mesh-module-assigner", defined whole (seats/mesh-module-assigner/seat.json): The seat that places modules on nodes (ADR 0567): the state of placement and the proposals that change it. Renamed from mesh-placement (ADR 0634 §9). +package meshmoduleassigner + +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + +// Seat is the seat's name. +const Seat = "mesh-module-assigner" + +const ( + VerbPlacement = "placement" + VerbProposals = "proposals" + VerbPropose = "propose" + VerbWithdraw = "withdraw" + VerbAgain = "again" +) + +const ( + EventPlacementProposed = "placement-proposed" + EventPlacementWithdrawn = "placement-withdrawn" + EventPlacementJudged = "placement-judged" + EventPlacementChanged = "placement-changed" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbPlacement, VerbProposals, VerbPropose, VerbWithdraw, VerbAgain} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventPlacementProposed, EventPlacementWithdrawn, EventPlacementJudged, EventPlacementChanged} +} + +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. +const ( + Family = "mesh-module-assigner.events" + FamilyMaxAgeDays = 7 +) + +// SubjectPlacementProposed is where placement-proposed is published. +const SubjectPlacementProposed = "mesh.seat.mesh-module-assigner.event.placement-proposed" + +// SubjectPlacementWithdrawn is where placement-withdrawn is published. +const SubjectPlacementWithdrawn = "mesh.seat.mesh-module-assigner.event.placement-withdrawn" + +// SubjectPlacementJudged is where placement-judged is published. +const SubjectPlacementJudged = "mesh.seat.mesh-module-assigner.event.placement-judged" + +// SubjectPlacementChanged is where placement-changed is published. +const SubjectPlacementChanged = "mesh.seat.mesh-module-assigner.event.placement-changed" + +const ( + StatePlacement = "placement" + StateProposals = "proposals" + StateCandidates = "candidates" + StatePulses = "pulses" + StateSeatHolders = "seat-holders" +) + +// PlacementByNodeNodeKey is the key of one value of the state placement under the form ".node". +func PlacementByNodeNodeKey(node string) string { return node + ".node" } + +// ParsePlacementByNodeNodeKey reads the parts of a key of the form ".node", in the order of the form; the last result is false for a key of another form. +func ParsePlacementByNodeNodeKey(key string) (string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".node") + if !found0 || v0 == "" { + return "", false + } + rest = after0 + if rest != "" { + return "", false + } + return v0, true +} + +// DecodePlacementByNodeNodeValue reads one value of the state placement under the form ".node", refusing a field PlacementByNodeNodeValue does not know and any text after the value. +func DecodePlacementByNodeNodeValue(raw []byte) (PlacementByNodeNodeValue, error) { + var v PlacementByNodeNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// PlacementByNodeModuleKey is the key of one value of the state placement under the form ".". +func PlacementByNodeModuleKey(node, module string) string { return node + "." + module } + +// ParsePlacementByNodeModuleKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParsePlacementByNodeModuleKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodePlacementByNodeModuleValue reads one value of the state placement under the form ".", refusing a field PlacementByNodeModuleValue does not know and any text after the value. +func DecodePlacementByNodeModuleValue(raw []byte) (PlacementByNodeModuleValue, error) { + var v PlacementByNodeModuleValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// ProposalsByProposalKey is the key of one value of the state proposals under the form "". +func ProposalsByProposalKey(proposal string) string { return proposal } + +// ParseProposalsByProposalKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseProposalsByProposalKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeProposalsByProposalValue reads one value of the state proposals under the form "", refusing a field ProposalsByProposalValue does not know and any text after the value. +func DecodeProposalsByProposalValue(raw []byte) (ProposalsByProposalValue, error) { + var v ProposalsByProposalValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// CandidatesByProposalNodeKey is the key of one value of the state candidates under the form ".". +func CandidatesByProposalNodeKey(proposal, node string) string { return proposal + "." + node } + +// ParseCandidatesByProposalNodeKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParseCandidatesByProposalNodeKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodeCandidatesByProposalNodeValue reads one value of the state candidates under the form ".", refusing a field CandidatesByProposalNodeValue does not know and any text after the value. +func DecodeCandidatesByProposalNodeValue(raw []byte) (CandidatesByProposalNodeValue, error) { + var v CandidatesByProposalNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// PulsesByAnswerProposalNodeKey is the key of one value of the state pulses under the form "answer..". +func PulsesByAnswerProposalNodeKey(proposal, node string) string { + return "answer." + proposal + "." + node +} + +// ParsePulsesByAnswerProposalNodeKey reads the parts of a key of the form "answer..", in the order of the form; the last result is false for a key of another form. +func ParsePulsesByAnswerProposalNodeKey(key string) (string, string, bool) { + rest := key + if !strings.HasPrefix(rest, "answer.") { + return "", "", false + } + rest = rest[7:] + v1, after1, found1 := strings.Cut(rest, ".") + if !found1 || v1 == "" { + return "", "", false + } + rest = after1 + if rest == "" { + return "", "", false + } + v3 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v1, v3, true +} + +// DecodePulsesByAnswerProposalNodeValue reads one value of the state pulses under the form "answer..", refusing a field PulsesByAnswerProposalNodeValue does not know and any text after the value. +func DecodePulsesByAnswerProposalNodeValue(raw []byte) (PulsesByAnswerProposalNodeValue, error) { + var v PulsesByAnswerProposalNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// PulsesByAskProposalKey is the key of one value of the state pulses under the form "ask.". +func PulsesByAskProposalKey(proposal string) string { return "ask." + proposal } + +// ParsePulsesByAskProposalKey reads the parts of a key of the form "ask.", in the order of the form; the last result is false for a key of another form. +func ParsePulsesByAskProposalKey(key string) (string, bool) { + rest := key + if !strings.HasPrefix(rest, "ask.") { + return "", false + } + rest = rest[4:] + if rest == "" { + return "", false + } + v1 := rest + rest = "" + if rest != "" { + return "", false + } + return v1, true +} + +// DecodePulsesByAskProposalValue reads one value of the state pulses under the form "ask.", refusing a field PulsesByAskProposalValue does not know and any text after the value. +func DecodePulsesByAskProposalValue(raw []byte) (PulsesByAskProposalValue, error) { + var v PulsesByAskProposalValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// SeatHoldersBySeatKey is the key of one value of the state seat-holders under the form "". +func SeatHoldersBySeatKey(seat string) string { return seat } + +// ParseSeatHoldersBySeatKey reads the parts of a key of the form "", in the order of the form; the last result is false for a key of another form. +func ParseSeatHoldersBySeatKey(key string) (string, bool) { + rest := key + if rest == "" { + return "", false + } + v0 := rest + rest = "" + if rest != "" { + return "", false + } + return v0, true +} + +// DecodeSeatHoldersBySeatValue reads one value of the state seat-holders under the form "", refusing a field SeatHoldersBySeatValue does not know and any text after the value. +func DecodeSeatHoldersBySeatValue(raw []byte) (SeatHoldersBySeatValue, error) { + var v SeatHoldersBySeatValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// SeatHoldersBySeatNodeKey is the key of one value of the state seat-holders under the form ".". +func SeatHoldersBySeatNodeKey(seat, node string) string { return seat + "." + node } + +// ParseSeatHoldersBySeatNodeKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParseSeatHoldersBySeatNodeKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodeSeatHoldersBySeatNodeValue reads one value of the state seat-holders under the form ".", refusing a field SeatHoldersBySeatNodeValue does not know and any text after the value. +func DecodeSeatHoldersBySeatNodeValue(raw []byte) (SeatHoldersBySeatNodeValue, error) { + var v SeatHoldersBySeatNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// Holding is one seat a module placed on the node holds. +type Holding struct { + Claim string `json:"claim"` + Kind string `json:"kind,omitempty"` + Module string `json:"module"` + Scope string `json:"scope,omitempty"` + Site string `json:"site,omitempty"` +} + +// NodeDecided is what a person decides of a node; no secret and no key material. +type NodeDecided struct { + Account string `json:"account,omitempty"` + AccountHome string `json:"account-home,omitempty"` + Address string `json:"address,omitempty"` + Adopted bool `json:"adopted,omitempty"` + AgentAccount string `json:"agent-account,omitempty"` + AgentAccountHome string `json:"agent-account-home,omitempty"` + Endpoint string `json:"endpoint,omitempty"` + Hub bool `json:"hub,omitempty"` + // on the hub, the name a person gave a peer of that tunnel, by its public key + PeerNames map[string]string `json:"peer-names,omitempty"` + PublicDomain string `json:"public-domain,omitempty"` + Site string `json:"site,omitempty"` + Taken []string `json:"taken,omitempty"` + // the tunnel found on the node when it was adopted + Tunnel map[string]any `json:"tunnel,omitempty"` +} + +// PlacedModule is one module placed on a node. +type PlacedModule struct { + // by provision, the provider each requirement that keeps data is bound to + Bindings map[string]Provider `json:"bindings,omitempty"` + HasLayer bool `json:"has-layer,omitempty"` + HasMeshWide bool `json:"has-mesh-wide,omitempty"` + // this node's settings layer of the module's placement + Layer map[string]any `json:"layer,omitempty"` + // the whole mesh's settings layer of the module's placement + MeshWide map[string]any `json:"mesh-wide,omitempty"` + // config values a person set in the whole mesh's layer, for a module in the settings form + MeshWideValues map[string]any `json:"mesh-wide-values,omitempty"` + Module string `json:"module"` + // the host port each declared port is published on, by the port the software listens on + Ports map[string]int64 `json:"ports,omitempty"` + // config values a person set in this node's layer, for a module in the settings form + Values map[string]any `json:"values,omitempty"` +} + +// Placement is one node's placement assembled. +type Placement struct { + Decided NodeDecided `json:"decided"` + Holds []Holding `json:"holds,omitempty"` + Modules []PlacedModule `json:"modules"` + Node string `json:"node"` + // by provision, the provider chosen for the node + Pins map[string]Provider `json:"pins,omitempty"` + Revision string `json:"revision,omitempty"` + V int64 `json:"v,omitempty"` +} + +// PlacementEventWarrant is who approved it, through which channel, when. +type PlacementEventWarrant struct { + At string `json:"at"` + Warrant string `json:"warrant"` + Words string `json:"words"` +} + +// PlacementEvent is what each placement event carries. +type PlacementEvent struct { + // for placement-withdrawn, why it ended + Ended string `json:"ended,omitempty"` + // for placement-changed, by node the revisions before and after and the modules added, removed or changed + Nodes []map[string]any `json:"nodes,omitempty"` + Proposal Proposal `json:"proposal"` + // who approved it, through which channel, when + Warrant *PlacementEventWarrant `json:"warrant,omitempty"` +} + +// Proposal is an open proposal. +type Proposal struct { + Again string `json:"again,omitempty"` + Ask string `json:"ask,omitempty"` + At string `json:"at"` + // by node whose key it changes, the revision of the key it was laid over (0: none) + Base map[string]int64 `json:"base"` + By string `json:"by"` + Changes []ProposalChange `json:"changes"` + ExportDigest string `json:"export-digest,omitempty"` + HandBack bool `json:"hand-back,omitempty"` + ID string `json:"id"` + Must []string `json:"must,omitempty"` + Seed bool `json:"seed,omitempty"` + // answering, failed or asked + State string `json:"state"` + V int64 `json:"v,omitempty"` + Verdict *Verdict `json:"verdict,omitempty"` + Warrant *Warrant `json:"warrant,omitempty"` + Why string `json:"why"` + Withdrawn string `json:"withdrawn,omitempty"` + Words string `json:"words,omitempty"` + Written string `json:"written,omitempty"` +} + +// ProposalChangeKeys is the public halves of a joining node's keys. +type ProposalChangeKeys struct { + Identity string `json:"identity"` + Overlay string `json:"overlay,omitempty"` + Sealing string `json:"sealing"` + Serving string `json:"serving,omitempty"` +} + +// ProposalChange is one change of a proposal. +type ProposalChange struct { + Bindings map[string]Provider `json:"bindings,omitempty"` + Claim string `json:"claim,omitempty"` + Decided *NodeDecided `json:"decided,omitempty"` + Field string `json:"field,omitempty"` + // the public halves of a joining node's keys + Keys *ProposalChangeKeys `json:"keys,omitempty"` + Kind string `json:"kind,omitempty"` + Layer map[string]any `json:"layer,omitempty"` + MeshWide map[string]any `json:"mesh-wide,omitempty"` + MeshWideValues map[string]any `json:"mesh-wide-values,omitempty"` + Module string `json:"module,omitempty"` + Node string `json:"node"` + // place, take-off, pin, unpin, hold, unhold, decide, config, seed, add-node or remove-node + Op string `json:"op"` + Ports map[string]int64 `json:"ports,omitempty"` + Provider *Provider `json:"provider,omitempty"` + Provision string `json:"provision,omitempty"` + Scope string `json:"scope,omitempty"` + Site string `json:"site,omitempty"` + // a decided field's value + Value any `json:"value,omitempty"` + Values map[string]any `json:"values,omitempty"` + // the node's whole placement, for a seed + Whole *Placement `json:"whole,omitempty"` +} + +// Provider is the module on a node that answers a requirement or holds a seat. +type Provider struct { + Module string `json:"module"` + Node string `json:"node"` +} + +// SeatHoldersHoldersItem is a type of the contract. +type SeatHoldersHoldersItem struct { + Module string `json:"module"` + Node string `json:"node"` +} + +// SeatHolders is one key of seat-holders. +type SeatHolders struct { + Holders []SeatHoldersHoldersItem `json:"holders"` + Node string `json:"node,omitempty"` + // the claim names no seat of the seat list + Outside bool `json:"outside,omitempty"` + Scope string `json:"scope"` + Seat string `json:"seat"` + V int64 `json:"v,omitempty"` +} + +// Verdict is the holder's judgement of a proposal's answers. +type Verdict struct { + // every node's answer to its candidate, by node + Answers map[string]map[string]any `json:"answers,omitempty"` + At string `json:"at"` + Carried map[string][]string `json:"carried,omitempty"` + Fails []string `json:"fails,omitempty"` + Intended map[string][]string `json:"intended,omitempty"` + Notes []string `json:"notes,omitempty"` + Silent []string `json:"silent,omitempty"` + // waits, pass or fail + State string `json:"state"` + Unintended map[string][]string `json:"unintended,omitempty"` + Until string `json:"until,omitempty"` +} + +// Warrant is the enrolment's warrant an add-node carries. +type Warrant struct { + Address string `json:"address"` + Ask string `json:"ask"` + Node string `json:"node"` + Warrant string `json:"warrant"` +} + +// PlacementInput is what placement takes. +type PlacementInput struct { + // "true": the changes in order, from the stream and the state's revisions + History string `json:"history,omitempty"` + // one module: the nodes it is placed on + Module string `json:"module,omitempty"` + // one node + Node string `json:"node,omitempty"` + // "true": who holds each seat, and on which node (the state seat-holders); with node, that node's + Seats string `json:"seats,omitempty"` +} + +// The values of PlacementInput's history. +const ( + PlacementInputHistoryTrue = "true" + PlacementInputHistoryFalse = "false" +) + +// The values of PlacementInput's seats. +const ( + PlacementInputSeatsTrue = "true" + PlacementInputSeatsFalse = "false" +) + +// PlacementAnswer is what placement answers. +type PlacementAnswer = map[string]any + +// ProposalsInput is what proposals takes. +type ProposalsInput struct { + // one proposal's id + ID string `json:"id,omitempty"` +} + +// ProposalsAnswer is what proposals answers. +type ProposalsAnswer = map[string]any + +// ProposeInput is what propose takes. +type ProposeInput struct { + // the changes, each an object with `op`: place (node, module, ports, bindings), take-off (node, module), pin (node, provision, provider {node, module}), unpin (node, provision), hold (node, module, claim, scope, kind, site), unhold (node, module, claim), decide (node, field, value: a decided field of the node, its address among them), config (node, module, mesh-wide, layer, mesh-wide-values, values), add-node (node, decided, keys: a joining node, mesh-enrolment's alone, the proposal's one change, with the enrolment's warrant, its tunnel key alone at the operator's yes; ADR 0587 §7), remove-node (node: a joining node never confirmed, taken off when its enrolment ends; mesh-enrolment's alone, on the same warrant) + Changes []map[string]any `json:"changes"` + // for an add-node alone: the enrolment's warrant {node, address, ask, warrant}, naming the change's node and address + Warrant map[string]any `json:"warrant,omitempty"` + // why, shown to the operator with the proposal + Why string `json:"why"` +} + +// ProposeAnswer is what propose answers. +type ProposeAnswer = map[string]any + +// WithdrawInput is what withdraw takes. +type WithdrawInput struct { + // the proposal's id + ID string `json:"id"` + // why, kept in the event + Why string `json:"why"` +} + +// WithdrawAnswer is what withdraw answers. +type WithdrawAnswer = map[string]any + +// AgainInput is what again takes. +type AgainInput struct { + // the proposal's id + ID string `json:"id"` +} + +// AgainAnswer is what again answers. +type AgainAnswer = map[string]any + +// PlacementProposedEvent is the body of the event placement-proposed. +type PlacementProposedEvent = PlacementEvent + +// PlacementWithdrawnEvent is the body of the event placement-withdrawn. +type PlacementWithdrawnEvent = PlacementEvent + +// PlacementJudgedEvent is the body of the event placement-judged. +type PlacementJudgedEvent = PlacementEvent + +// PlacementChangedEvent is the body of the event placement-changed. +type PlacementChangedEvent = PlacementEvent + +// PlacementByNodeNodeValueModulesItem is a type of the contract. +type PlacementByNodeNodeValueModulesItem struct { + // the digest of the module key's value it stands for + Digest string `json:"digest"` + Module string `json:"module"` +} + +// PlacementByNodeNodeValue is one value of the state placement under the key .node. +type PlacementByNodeNodeValue struct { + Decided NodeDecided `json:"decided"` + Holds []Holding `json:"holds,omitempty"` + // the modules placed, in the order the composition reads them + Modules []PlacementByNodeNodeValueModulesItem `json:"modules"` + Node string `json:"node"` + Pins map[string]Provider `json:"pins,omitempty"` + V int64 `json:"v,omitempty"` +} + +// PlacementByNodeModuleValue is one value of the state placement under the key .. +type PlacementByNodeModuleValue struct { + Node string `json:"node"` + Placed PlacedModule `json:"placed"` + // the value it replaces while a write is under way + Previous *PlacedModule `json:"previous,omitempty"` + V int64 `json:"v,omitempty"` +} + +// ProposalsByProposalValue is one value of the state proposals under the key . +type ProposalsByProposalValue = Proposal + +// CandidatesByProposalNodeValue is one value of the state candidates under the key .. +type CandidatesByProposalNodeValue struct { + // the node's placement with the proposal laid over it; absent when the proposal takes its key away + Placement *Placement `json:"placement,omitempty"` + Proposal string `json:"proposal"` + // the proposal changes this node's composition through another's key + Reached bool `json:"reached,omitempty"` + V int64 `json:"v,omitempty"` +} + +// PulsesByAnswerProposalNodeValue is one value of the state pulses under the key answer... +type PulsesByAnswerProposalNodeValue struct { + V int64 `json:"v,omitempty"` +} + +// PulsesByAskProposalValue is one value of the state pulses under the key ask.. +type PulsesByAskProposalValue struct { + V int64 `json:"v,omitempty"` +} + +// SeatHoldersBySeatValue is one value of the state seat-holders under the key . +type SeatHoldersBySeatValue = SeatHolders + +// SeatHoldersBySeatNodeValue is one value of the state seat-holders under the key .. +type SeatHoldersBySeatNodeValue = SeatHolders diff --git a/placement/go/seats/meshmoduleassigner/contract_test.go b/placement/go/seats/meshmoduleassigner/contract_test.go new file mode 100644 index 0000000..178ad88 --- /dev/null +++ b/placement/go/seats/meshmoduleassigner/contract_test.go @@ -0,0 +1,86 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshmoduleassigner + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-module-assigner/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "a proposal says placement-proposed", subject: "mesh.seat.mesh-module-assigner.event.placement-proposed", given: "{\"proposal\":{\"at\":\"2026-11-05T12:00:00Z\",\"base\":{\"ace\":3},\"by\":\"operator\",\"changes\":[{\"module\":\"plex\",\"node\":\"ace\",\"op\":\"place\",\"ports\":{\"32400\":32400}}],\"id\":\"p-20261105t120000-aabbccdd\",\"state\":\"answering\",\"why\":\"move plex\"}}", wire: "{\"proposal\":{\"at\":\"2026-11-05T12:00:00Z\",\"base\":{\"ace\":3},\"by\":\"operator\",\"changes\":[{\"module\":\"plex\",\"node\":\"ace\",\"op\":\"place\",\"ports\":{\"32400\":32400}}],\"id\":\"p-20261105t120000-aabbccdd\",\"state\":\"answering\",\"why\":\"move plex\"}}", + decode: func(b []byte) (any, error) { var v PlacementProposedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-module-assigner\\.event\\.placement-proposed$")}, + {name: "a node's key", subject: "", given: "{\"decided\":{\"address\":\"10.0.0.2\"},\"modules\":[{\"digest\":\"0123456789abcdef\",\"module\":\"plex\"}],\"node\":\"ace\",\"v\":1}", wire: "{\"decided\":{\"address\":\"10.0.0.2\"},\"modules\":[{\"digest\":\"0123456789abcdef\",\"module\":\"plex\"}],\"node\":\"ace\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v PlacementByNodeNodeValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a module's key", subject: "", given: "{\"node\":\"ace\",\"placed\":{\"bindings\":{\"database\":{\"module\":\"postgres\",\"node\":\"ace\"}},\"module\":\"plex\",\"ports\":{\"32400\":32400}},\"v\":1}", wire: "{\"node\":\"ace\",\"placed\":{\"bindings\":{\"database\":{\"module\":\"postgres\",\"node\":\"ace\"}},\"module\":\"plex\",\"ports\":{\"32400\":32400}},\"v\":1}", + decode: func(b []byte) (any, error) { + var v PlacementByNodeModuleValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "an open proposal", subject: "", given: "{\"at\":\"2026-11-05T12:00:00Z\",\"base\":{\"ace\":3},\"by\":\"operator\",\"changes\":[{\"module\":\"plex\",\"node\":\"ace\",\"op\":\"place\",\"ports\":{\"32400\":32400}}],\"id\":\"p-20261105t120000-aabbccdd\",\"state\":\"answering\",\"why\":\"move plex\"}", wire: "{\"at\":\"2026-11-05T12:00:00Z\",\"base\":{\"ace\":3},\"by\":\"operator\",\"changes\":[{\"module\":\"plex\",\"node\":\"ace\",\"op\":\"place\",\"ports\":{\"32400\":32400}}],\"id\":\"p-20261105t120000-aabbccdd\",\"state\":\"answering\",\"why\":\"move plex\"}", + decode: func(b []byte) (any, error) { + var v ProposalsByProposalValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a candidate", subject: "", given: "{\"proposal\":\"p-20261105t120000-aabbccdd\",\"v\":1}", wire: "{\"proposal\":\"p-20261105t120000-aabbccdd\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v CandidatesByProposalNodeValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "the bound on an answer", subject: "", given: "{\"v\":1}", wire: "{\"v\":1}", + decode: func(b []byte) (any, error) { + var v PulsesByAnswerProposalNodeValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a seat held once", subject: "", given: "{\"holders\":[{\"module\":\"mesh-delivery-assigner\",\"node\":\"novox\"}],\"scope\":\"mesh\",\"seat\":\"mesh-module-assigner\",\"v\":1}", wire: "{\"holders\":[{\"module\":\"mesh-delivery-assigner\",\"node\":\"novox\"}],\"scope\":\"mesh\",\"seat\":\"mesh-module-assigner\",\"v\":1}", + decode: func(b []byte) (any, error) { var v SeatHoldersBySeatValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "a node seat on a node", subject: "", given: "{\"holders\":[{\"module\":\"node-files\",\"node\":\"ace\"}],\"node\":\"ace\",\"scope\":\"node\",\"seat\":\"node-files\",\"v\":1}", wire: "{\"holders\":[{\"module\":\"node-files\",\"node\":\"ace\"}],\"node\":\"ace\",\"scope\":\"node\",\"seat\":\"node-files\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v SeatHoldersBySeatNodeValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/placement/go/seats/meshmoduleassigner/go.mod b/placement/go/seats/meshmoduleassigner/go.mod new file mode 100644 index 0000000..cda234d --- /dev/null +++ b/placement/go/seats/meshmoduleassigner/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/placement/go/seats/meshmoduleassigner + +go 1.26.0 diff --git a/placement/go/seats/meshwants/contract.go b/placement/go/seats/meshwants/contract.go new file mode 100644 index 0000000..fc93351 --- /dev/null +++ b/placement/go/seats/meshwants/contract.go @@ -0,0 +1,331 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// Package meshwants is the contract of the seat "mesh-wants", defined whole (seats/mesh-wants/seat.json): The seat that says what each node should run and whether each module's new builds wait for a person (ADR 0565, ADR 0539). +package meshwants + +import ( + "bytes" + "encoding/json" + "errors" + "strings" +) + +// Seat is the seat's name. +const Seat = "mesh-wants" + +const ( + VerbUpgrade = "upgrade" + VerbBus = "bus" +) + +const ( + EventPolicyChanged = "policy-changed" +) + +// Verbs is every verb the seat serves, in its definition's order: a new copy each call. +func Verbs() []string { + return []string{VerbUpgrade, VerbBus} +} + +// Events is every event published on the seat's subject: a new copy each call. +func Events() []string { + return []string{EventPolicyChanged} +} + +// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. +const ( + Family = "mesh-wants.events" + FamilyMaxAgeDays = 7 +) + +// SubjectPolicyChanged is where policy-changed is published. +const SubjectPolicyChanged = "mesh.seat.mesh-wants.event.policy-changed" + +const ( + StateWants = "wants" + StateDifferences = "differences" + StateCompositions = "compositions" +) + +// WantsByWantNodeModuleKey is the key of one value of the state wants under the form "want..". +func WantsByWantNodeModuleKey(node, module string) string { return "want." + node + "." + module } + +// ParseWantsByWantNodeModuleKey reads the parts of a key of the form "want..", in the order of the form; the last result is false for a key of another form. +func ParseWantsByWantNodeModuleKey(key string) (string, string, bool) { + rest := key + if !strings.HasPrefix(rest, "want.") { + return "", "", false + } + rest = rest[5:] + v1, after1, found1 := strings.Cut(rest, ".") + if !found1 || v1 == "" { + return "", "", false + } + rest = after1 + if rest == "" { + return "", "", false + } + v3 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v1, v3, true +} + +// DecodeWantsByWantNodeModuleValue reads one value of the state wants under the form "want..", refusing a field WantsByWantNodeModuleValue does not know and any text after the value. +func DecodeWantsByWantNodeModuleValue(raw []byte) (WantsByWantNodeModuleValue, error) { + var v WantsByWantNodeModuleValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// WantsByWantNodeNodeKey is the key of one value of the state wants under the form "want..node". +func WantsByWantNodeNodeKey(node string) string { return "want." + node + ".node" } + +// ParseWantsByWantNodeNodeKey reads the parts of a key of the form "want..node", in the order of the form; the last result is false for a key of another form. +func ParseWantsByWantNodeNodeKey(key string) (string, bool) { + rest := key + if !strings.HasPrefix(rest, "want.") { + return "", false + } + rest = rest[5:] + v1, after1, found1 := strings.Cut(rest, ".node") + if !found1 || v1 == "" { + return "", false + } + rest = after1 + if rest != "" { + return "", false + } + return v1, true +} + +// DecodeWantsByWantNodeNodeValue reads one value of the state wants under the form "want..node", refusing a field WantsByWantNodeNodeValue does not know and any text after the value. +func DecodeWantsByWantNodeNodeValue(raw []byte) (WantsByWantNodeNodeValue, error) { + var v WantsByWantNodeNodeValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// WantsByHeldModuleFingerprintKey is the key of one value of the state wants under the form "held..". +func WantsByHeldModuleFingerprintKey(module, fingerprint string) string { + return "held." + module + "." + fingerprint +} + +// ParseWantsByHeldModuleFingerprintKey reads the parts of a key of the form "held..", in the order of the form; the last result is false for a key of another form. +func ParseWantsByHeldModuleFingerprintKey(key string) (string, string, bool) { + rest := key + if !strings.HasPrefix(rest, "held.") { + return "", "", false + } + rest = rest[5:] + v1, after1, found1 := strings.Cut(rest, ".") + if !found1 || v1 == "" { + return "", "", false + } + rest = after1 + if rest == "" { + return "", "", false + } + v3 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v1, v3, true +} + +// DecodeWantsByHeldModuleFingerprintValue reads one value of the state wants under the form "held..", refusing a field WantsByHeldModuleFingerprintValue does not know and any text after the value. +func DecodeWantsByHeldModuleFingerprintValue(raw []byte) (WantsByHeldModuleFingerprintValue, error) { + var v WantsByHeldModuleFingerprintValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// DifferencesByNodeModuleKey is the key of one value of the state differences under the form ".". +func DifferencesByNodeModuleKey(node, module string) string { return node + "." + module } + +// ParseDifferencesByNodeModuleKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParseDifferencesByNodeModuleKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodeDifferencesByNodeModuleValue reads one value of the state differences under the form ".", refusing a field DifferencesByNodeModuleValue does not know and any text after the value. +func DecodeDifferencesByNodeModuleValue(raw []byte) (DifferencesByNodeModuleValue, error) { + var v DifferencesByNodeModuleValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// CompositionsByGroupHeadsKey is the key of one value of the state compositions under the form ".". +func CompositionsByGroupHeadsKey(group, heads string) string { return group + "." + heads } + +// ParseCompositionsByGroupHeadsKey reads the parts of a key of the form ".", in the order of the form; the last result is false for a key of another form. +func ParseCompositionsByGroupHeadsKey(key string) (string, string, bool) { + rest := key + v0, after0, found0 := strings.Cut(rest, ".") + if !found0 || v0 == "" { + return "", "", false + } + rest = after0 + if rest == "" { + return "", "", false + } + v2 := rest + rest = "" + if rest != "" { + return "", "", false + } + return v0, v2, true +} + +// DecodeCompositionsByGroupHeadsValue reads one value of the state compositions under the form ".", refusing a field CompositionsByGroupHeadsValue does not know and any text after the value. +func DecodeCompositionsByGroupHeadsValue(raw []byte) (CompositionsByGroupHeadsValue, error) { + var v CompositionsByGroupHeadsValue + dec := json.NewDecoder(bytes.NewReader(raw)) + dec.DisallowUnknownFields() + if err := dec.Decode(&v); err != nil { + return v, err + } + if dec.More() { + return v, errors.New("text after the value") + } + return v, nil +} + +// UpgradeInput is what upgrade takes. +type UpgradeInput struct { + // "true": each node's own composition, by its digest, and the assigner's findings + Compare string `json:"compare,omitempty"` + // one module + Module string `json:"module,omitempty"` + // with module: roll, together, record, or default to take a person's choice back + Policy string `json:"policy,omitempty"` + // with policy: why, required and kept + Why string `json:"why,omitempty"` +} + +// The values of UpgradeInput's compare. +const ( + UpgradeInputCompareTrue = "true" + UpgradeInputCompareFalse = "false" +) + +// UpgradeAnswer is what upgrade answers. +type UpgradeAnswer = map[string]any + +// BusInput is what bus takes. +type BusInput struct { +} + +// BusAnswer is what bus answers. +type BusAnswer = map[string]any + +// PolicyChangedEvent is the body of the event policy-changed. +type PolicyChangedEvent = map[string]any + +// WantsByWantNodeModuleValue is one value of the state wants under the key want... +type WantsByWantNodeModuleValue struct { + // again or skip + Backup string `json:"backup,omitempty"` + Composed string `json:"composed,omitempty"` + Fp string `json:"fp,omitempty"` + Hold bool `json:"hold,omitempty"` + Newest string `json:"newest,omitempty"` + Put bool `json:"put,omitempty"` + Release string `json:"release,omitempty"` + Removed bool `json:"removed,omitempty"` + // the rule that holds the node back, 1 to 10 + Rule int64 `json:"rule,omitempty"` + Self bool `json:"self,omitempty"` + V int64 `json:"v,omitempty"` + Words string `json:"words,omitempty"` + Yes string `json:"yes,omitempty"` +} + +// WantsByWantNodeNodeValue is one value of the state wants under the key want..node. +type WantsByWantNodeNodeValue struct { + Composed string `json:"composed"` + Self bool `json:"self,omitempty"` + V int64 `json:"v,omitempty"` + Words string `json:"words,omitempty"` +} + +// WantsByHeldModuleFingerprintValue is one value of the state wants under the key held... +type WantsByHeldModuleFingerprintValue struct { + Node string `json:"node"` + V int64 `json:"v,omitempty"` + Words string `json:"words,omitempty"` +} + +// DifferencesByNodeModuleValue is one value of the state differences under the key .. +type DifferencesByNodeModuleValue struct { + Rule int64 `json:"rule,omitempty"` + Sent string `json:"sent,omitempty"` + SentBuild string `json:"sent-build,omitempty"` + Unexplained bool `json:"unexplained,omitempty"` + V int64 `json:"v,omitempty"` + Want string `json:"want,omitempty"` + WantRemoved bool `json:"want-removed,omitempty"` + Words string `json:"words,omitempty"` +} + +// CompositionsByGroupHeadsValue is one value of the state compositions under the key .. +type CompositionsByGroupHeadsValue struct { + Group string `json:"group"` + // 12 lowercase hex digits + Heads string `json:"heads"` + Machine string `json:"machine,omitempty"` + Machines []string `json:"machines,omitempty"` + Sdk string `json:"sdk,omitempty"` + // pass, fail or waits + State string `json:"state"` + // milliseconds + Took int64 `json:"took,omitempty"` + V int64 `json:"v,omitempty"` + Warnings []string `json:"warnings,omitempty"` + Words string `json:"words,omitempty"` +} diff --git a/placement/go/seats/meshwants/contract_test.go b/placement/go/seats/meshwants/contract_test.go new file mode 100644 index 0000000..1355a9b --- /dev/null +++ b/placement/go/seats/meshwants/contract_test.go @@ -0,0 +1,76 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +package meshwants + +import ( + "encoding/json" + "reflect" + "regexp" + "testing" +) + +// TestFixtures reads every fixture of seats/mesh-wants/fixtures: each decodes into its type, a field the type does not +// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's. +func TestFixtures(t *testing.T) { + cases := []struct { + name, subject, given, wire string + decode func([]byte) (any, error) + subjectIs *regexp.Regexp + }{ + {name: "a want", subject: "", given: "{\"fp\":\"ab12\",\"rule\":3,\"v\":1}", wire: "{\"fp\":\"ab12\",\"rule\":3,\"v\":1}", + decode: func(b []byte) (any, error) { + var v WantsByWantNodeModuleValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a node's own want", subject: "", given: "{\"composed\":\"sha256:aa\",\"self\":true,\"v\":1}", wire: "{\"composed\":\"sha256:aa\",\"self\":true,\"v\":1}", + decode: func(b []byte) (any, error) { + var v WantsByWantNodeNodeValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a held fingerprint", subject: "", given: "{\"node\":\"ace\",\"v\":1}", wire: "{\"node\":\"ace\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v WantsByHeldModuleFingerprintValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a difference", subject: "", given: "{\"unexplained\":true,\"v\":1,\"want\":\"ab12\"}", wire: "{\"unexplained\":true,\"v\":1,\"want\":\"ab12\"}", + decode: func(b []byte) (any, error) { + var v DifferencesByNodeModuleValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "a composition", subject: "", given: "{\"group\":\"g1\",\"heads\":\"0123456789ab\",\"state\":\"pass\",\"v\":1}", wire: "{\"group\":\"g1\",\"heads\":\"0123456789ab\",\"state\":\"pass\",\"v\":1}", + decode: func(b []byte) (any, error) { + var v CompositionsByGroupHeadsValue + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + } + for _, c := range cases { + v, err := c.decode([]byte(c.given)) + if err != nil { + t.Errorf("%s: does not decode: %v", c.name, err) + continue + } + back, err := json.Marshal(v) + if err != nil { + t.Errorf("%s: does not encode: %v", c.name, err) + continue + } + var got, want any + if err := json.Unmarshal(back, &got); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(c.wire), &want); err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(got, want) { + t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire) + } + if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) { + t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs) + } + } +} diff --git a/placement/go/seats/meshwants/go.mod b/placement/go/seats/meshwants/go.mod new file mode 100644 index 0000000..a075868 --- /dev/null +++ b/placement/go/seats/meshwants/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-contracts/placement/go/seats/meshwants + +go 1.26.0 diff --git a/placement/module.json b/placement/module.json new file mode 100644 index 0000000..9be9cf6 --- /dev/null +++ b/placement/module.json @@ -0,0 +1,58 @@ +{ + "module": "mesh-placement-contracts", + "version": "1", + "build": { + "artifacts": [ + { + "name": "go-meshmoduleassigner", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshmoduleassigner" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-meshwants", + "kind": "package", + "language": "go", + "paths": [ + "go/seats/meshwants" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "go-meshmoduleassignerclient", + "kind": "package", + "language": "go", + "paths": [ + "go/clients/meshmoduleassignerclient" + ], + "series": "0.1" + }, + { + "name": "ts-meshmoduleassigner", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshmoduleassigner" + ], + "series": "0.1", + "generated-from": "." + }, + { + "name": "ts-meshwants", + "kind": "package", + "language": "typescript", + "paths": [ + "ts/seats/meshwants" + ], + "series": "0.1", + "generated-from": "." + } + ] + }, + "resources": [] +} diff --git a/placement/seats/index.json b/placement/seats/index.json new file mode 100644 index 0000000..e7d0756 --- /dev/null +++ b/placement/seats/index.json @@ -0,0 +1,7 @@ +{ + "format": 1, + "seats": [ + "mesh-module-assigner", + "mesh-wants" + ] +} diff --git a/placement/seats/mesh-module-assigner/fixtures/events.json b/placement/seats/mesh-module-assigner/fixtures/events.json new file mode 100644 index 0000000..1886c81 --- /dev/null +++ b/placement/seats/mesh-module-assigner/fixtures/events.json @@ -0,0 +1,29 @@ +[ + { + "name": "a proposal says placement-proposed", + "of": "event:placement-proposed", + "subject": "mesh.seat.mesh-module-assigner.event.placement-proposed", + "given": { + "proposal": { + "id": "p-20261105t120000-aabbccdd", + "by": "operator", + "why": "move plex", + "at": "2026-11-05T12:00:00Z", + "changes": [ + { + "op": "place", + "node": "ace", + "module": "plex", + "ports": { + "32400": 32400 + } + } + ], + "base": { + "ace": 3 + }, + "state": "answering" + } + } + } +] diff --git a/placement/seats/mesh-module-assigner/fixtures/states.json b/placement/seats/mesh-module-assigner/fixtures/states.json new file mode 100644 index 0000000..2ccef79 --- /dev/null +++ b/placement/seats/mesh-module-assigner/fixtures/states.json @@ -0,0 +1,109 @@ +[ + { + "name": "a node's key", + "of": "state:placement/.node", + "given": { + "v": 1, + "node": "ace", + "decided": { + "address": "10.0.0.2" + }, + "modules": [ + { + "module": "plex", + "digest": "0123456789abcdef" + } + ] + } + }, + { + "name": "a module's key", + "of": "state:placement/.", + "given": { + "v": 1, + "node": "ace", + "placed": { + "module": "plex", + "ports": { + "32400": 32400 + }, + "bindings": { + "database": { + "node": "ace", + "module": "postgres" + } + } + } + } + }, + { + "name": "an open proposal", + "of": "state:proposals/", + "given": { + "id": "p-20261105t120000-aabbccdd", + "by": "operator", + "why": "move plex", + "at": "2026-11-05T12:00:00Z", + "changes": [ + { + "op": "place", + "node": "ace", + "module": "plex", + "ports": { + "32400": 32400 + } + } + ], + "base": { + "ace": 3 + }, + "state": "answering" + } + }, + { + "name": "a candidate", + "of": "state:candidates/.", + "given": { + "v": 1, + "proposal": "p-20261105t120000-aabbccdd" + } + }, + { + "name": "the bound on an answer", + "of": "state:pulses/answer..", + "given": { + "v": 1 + } + }, + { + "name": "a seat held once", + "of": "state:seat-holders/", + "given": { + "v": 1, + "seat": "mesh-module-assigner", + "scope": "mesh", + "holders": [ + { + "module": "mesh-delivery-assigner", + "node": "novox" + } + ] + } + }, + { + "name": "a node seat on a node", + "of": "state:seat-holders/.", + "given": { + "v": 1, + "seat": "node-files", + "scope": "node", + "node": "ace", + "holders": [ + { + "module": "node-files", + "node": "ace" + } + ] + } + } +] diff --git a/placement/seats/mesh-module-assigner/seat.json b/placement/seats/mesh-module-assigner/seat.json new file mode 100644 index 0000000..8d987c9 --- /dev/null +++ b/placement/seats/mesh-module-assigner/seat.json @@ -0,0 +1,941 @@ +{ + "format": 1, + "seat": "mesh-module-assigner", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0567, ADR 0634, ADR 0635", + "description": "The seat that places modules on nodes (ADR 0567): the state of placement and the proposals that change it. Renamed from mesh-placement (ADR 0634 §9).", + "verbs": [ + { + "name": "placement", + "description": "Read placement: one node's key, the nodes one module is placed on, or every key, each with its revision, and how full the state is against its cap. With history, one node's or one module's changes in order: a revision the state keeps whole, an older one as the proposal's changes from its placement-changed event.", + "optional": true, + "input": { + "properties": { + "history": { + "description": "\"true\": the changes in order, from the stream and the state's revisions", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "module": { + "description": "one module: the nodes it is placed on", + "type": "string" + }, + "node": { + "description": "one node", + "type": "string" + }, + "seats": { + "description": "\"true\": who holds each seat, and on which node (the state seat-holders); with node, that node's", + "enum": [ + "true", + "false" + ], + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "placement as read: nodes, keys with their revisions, history, seat holders, the state's fill" + } + }, + { + "name": "proposals", + "description": "Every open proposal of placement: its changes, the revision of each key it was laid over, its candidates, every node's answer, the differences computed and answered, its verdict and its ask. Narrowed to one by id.", + "optional": true, + "input": { + "properties": { + "id": { + "description": "one proposal's id", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "the open proposals" + } + }, + { + "name": "propose", + "description": "Propose a change of placement, with why: laid over the current keys and refused at once, naming every finding, when the result cannot be placed (two modules on one host port, a provider not placed, a seat held once held twice, two nodes with one address, two hubs, a module the indexer does not hold, a node with no record). Otherwise the nodes it touches compose it in shadow and answer, and on a passing verdict the operator is asked to approve it. Who proposes is the caller, as the runtime proves it. An add-node from mesh-enrolment, whose warrant names its node and address, is written with no ask of its own: its Approve is the enrolment's (ADR 0587 §7).", + "optional": true, + "input": { + "additionalProperties": false, + "properties": { + "changes": { + "description": "the changes, each an object with `op`: place (node, module, ports, bindings), take-off (node, module), pin (node, provision, provider {node, module}), unpin (node, provision), hold (node, module, claim, scope, kind, site), unhold (node, module, claim), decide (node, field, value: a decided field of the node, its address among them), config (node, module, mesh-wide, layer, mesh-wide-values, values), add-node (node, decided, keys: a joining node, mesh-enrolment's alone, the proposal's one change, with the enrolment's warrant, its tunnel key alone at the operator's yes; ADR 0587 §7), remove-node (node: a joining node never confirmed, taken off when its enrolment ends; mesh-enrolment's alone, on the same warrant)", + "items": { + "required": [ + "op", + "node" + ], + "type": "object" + }, + "minItems": 1, + "type": "array" + }, + "warrant": { + "description": "for an add-node alone: the enrolment's warrant {node, address, ask, warrant}, naming the change's node and address", + "required": [ + "node", + "address", + "ask", + "warrant" + ], + "type": "object" + }, + "why": { + "description": "why, shown to the operator with the proposal", + "type": "string" + } + }, + "required": [ + "why", + "changes" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "the proposal's id and its verdict, or every finding refused at once" + } + }, + { + "name": "withdraw", + "description": "Withdraw an open proposal: its key and its candidates are deleted and placement-withdrawn carries it whole. With why.", + "optional": true, + "input": { + "properties": { + "id": { + "description": "the proposal's id", + "type": "string" + }, + "why": { + "description": "why, kept in the event", + "type": "string" + } + }, + "required": [ + "id", + "why" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "the proposal withdrawn" + } + }, + { + "name": "again", + "description": "Judge an open proposal now with the answers that stand, and raise its ask again when its verdict passes and the operator's channel does not hold the ask. Nothing more: the recovery of a proposal whose judgement or ask was lost.", + "optional": true, + "input": { + "properties": { + "id": { + "description": "the proposal's id", + "type": "string" + } + }, + "required": [ + "id" + ], + "type": "object" + }, + "answer": { + "type": "object", + "description": "what was judged or asked again" + } + } + ], + "events": [ + { + "name": "placement-proposed", + "body": { + "$ref": "#/defs/PlacementEvent" + } + }, + { + "name": "placement-withdrawn", + "body": { + "$ref": "#/defs/PlacementEvent" + } + }, + { + "name": "placement-judged", + "body": { + "$ref": "#/defs/PlacementEvent" + } + }, + { + "name": "placement-changed", + "body": { + "$ref": "#/defs/PlacementEvent" + } + } + ], + "family": { + "name": "mesh-module-assigner.events", + "max-age-days": 7 + }, + "states": [ + { + "name": "placement", + "template": "state", + "key-forms": [ + { + "key": ".node", + "value": { + "type": "object", + "description": "a node's key: what a person decided of it, its pins and holdings, and the modules placed on it; the commit of a write", + "properties": { + "v": { + "type": "integer" + }, + "node": { + "type": "string" + }, + "decided": { + "$ref": "#/defs/NodeDecided" + }, + "modules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "module": { + "type": "string" + }, + "digest": { + "type": "string", + "description": "the digest of the module key's value it stands for" + } + }, + "required": [ + "module", + "digest" + ] + }, + "description": "the modules placed, in the order the composition reads them" + }, + "pins": { + "type": "object", + "additionalProperties": { + "$ref": "#/defs/Provider" + } + }, + "holds": { + "type": "array", + "items": { + "$ref": "#/defs/Holding" + } + } + }, + "required": [ + "node", + "decided", + "modules" + ] + }, + "description": "a node's key" + }, + { + "key": ".", + "value": { + "type": "object", + "description": "a module's key: the module as placed", + "properties": { + "v": { + "type": "integer" + }, + "node": { + "type": "string" + }, + "placed": { + "$ref": "#/defs/PlacedModule" + }, + "previous": { + "$ref": "#/defs/PlacedModule", + "description": "the value it replaces while a write is under way" + } + }, + "required": [ + "node", + "placed" + ] + }, + "description": "one module on a node; no module is named node" + } + ], + "history": 64, + "description": "placements, one key per node and one per module placed on it, written only on the operator's Approve of a proposal by compare-and-set at the revision it was laid over" + }, + { + "name": "proposals", + "template": "state", + "key-forms": [ + { + "key": "", + "value": { + "$ref": "#/defs/Proposal" + }, + "description": "an open proposal, by its id" + } + ], + "history": 5, + "description": "open proposals; a key leaves when the proposal is applied, withdrawn, declined or expired" + }, + { + "name": "candidates", + "template": "state", + "key-forms": [ + { + "key": ".", + "value": { + "type": "object", + "description": "one node's candidate in one proposal", + "properties": { + "v": { + "type": "integer" + }, + "proposal": { + "type": "string" + }, + "reached": { + "type": "boolean", + "description": "the proposal changes this node's composition through another's key" + }, + "placement": { + "$ref": "#/defs/Placement", + "description": "the node's placement with the proposal laid over it; absent when the proposal takes its key away" + } + }, + "required": [ + "proposal" + ] + }, + "description": "a node's candidate" + } + ], + "history": 5, + "description": "the candidates a proposal needs answers on; a node watches its own" + }, + { + "name": "pulses", + "template": "state", + "key-forms": [ + { + "key": "answer..", + "value": { + "type": "object", + "description": "the bound on one node's answer", + "properties": { + "v": { + "type": "integer" + } + } + }, + "description": "the bound on a node's answer" + }, + { + "key": "ask.", + "value": { + "type": "object", + "description": "the bound on a raised ask", + "properties": { + "v": { + "type": "integer" + } + } + }, + "description": "the bound on a proposal's raised ask" + } + ], + "description": "bounds, each a key that lives for the bound; the server's delete marker when one ends is the event that fails a silent node or an unanswered ask" + }, + { + "name": "seat-holders", + "template": "state", + "key-forms": [ + { + "key": "", + "value": { + "$ref": "#/defs/SeatHolders" + }, + "description": "a seat held once for the mesh" + }, + { + "key": ".", + "value": { + "$ref": "#/defs/SeatHolders" + }, + "description": "a node seat on one node" + } + ], + "description": "who holds which seat on which node, composed from every node's placement" + } + ], + "defs": { + "Provider": { + "type": "object", + "description": "the module on a node that answers a requirement or holds a seat", + "properties": { + "node": { + "type": "string" + }, + "module": { + "type": "string" + } + }, + "required": [ + "node", + "module" + ] + }, + "PlacedModule": { + "type": "object", + "description": "one module placed on a node", + "properties": { + "module": { + "type": "string" + }, + "ports": { + "type": "object", + "additionalProperties": { + "type": "integer" + }, + "description": "the host port each declared port is published on, by the port the software listens on" + }, + "bindings": { + "type": "object", + "additionalProperties": { + "$ref": "#/defs/Provider" + }, + "description": "by provision, the provider each requirement that keeps data is bound to" + }, + "mesh-wide": { + "type": "object", + "description": "the whole mesh's settings layer of the module's placement" + }, + "has-mesh-wide": { + "type": "boolean" + }, + "layer": { + "type": "object", + "description": "this node's settings layer of the module's placement" + }, + "has-layer": { + "type": "boolean" + }, + "mesh-wide-values": { + "type": "object", + "description": "config values a person set in the whole mesh's layer, for a module in the settings form" + }, + "values": { + "type": "object", + "description": "config values a person set in this node's layer, for a module in the settings form" + } + }, + "required": [ + "module" + ] + }, + "Holding": { + "type": "object", + "description": "one seat a module placed on the node holds", + "properties": { + "claim": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "module": { + "type": "string" + }, + "site": { + "type": "string" + }, + "kind": { + "type": "string" + } + }, + "required": [ + "claim", + "module" + ] + }, + "NodeDecided": { + "type": "object", + "description": "what a person decides of a node; no secret and no key material", + "properties": { + "endpoint": { + "type": "string" + }, + "site": { + "type": "string" + }, + "hub": { + "type": "boolean" + }, + "address": { + "type": "string" + }, + "public-domain": { + "type": "string" + }, + "account": { + "type": "string" + }, + "account-home": { + "type": "string" + }, + "agent-account": { + "type": "string" + }, + "agent-account-home": { + "type": "string" + }, + "adopted": { + "type": "boolean" + }, + "taken": { + "type": "array", + "items": { + "type": "string" + } + }, + "tunnel": { + "type": "object", + "description": "the tunnel found on the node when it was adopted" + }, + "peer-names": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "on the hub, the name a person gave a peer of that tunnel, by its public key" + } + } + }, + "Placement": { + "type": "object", + "description": "one node's placement assembled", + "properties": { + "v": { + "type": "integer" + }, + "node": { + "type": "string" + }, + "revision": { + "type": "string" + }, + "decided": { + "$ref": "#/defs/NodeDecided" + }, + "modules": { + "type": "array", + "items": { + "$ref": "#/defs/PlacedModule" + } + }, + "pins": { + "type": "object", + "additionalProperties": { + "$ref": "#/defs/Provider" + }, + "description": "by provision, the provider chosen for the node" + }, + "holds": { + "type": "array", + "items": { + "$ref": "#/defs/Holding" + } + } + }, + "required": [ + "node", + "decided", + "modules" + ] + }, + "ProposalChange": { + "type": "object", + "description": "one change of a proposal", + "properties": { + "op": { + "type": "string", + "description": "place, take-off, pin, unpin, hold, unhold, decide, config, seed, add-node or remove-node" + }, + "node": { + "type": "string" + }, + "module": { + "type": "string" + }, + "ports": { + "type": "object", + "additionalProperties": { + "type": "integer" + } + }, + "bindings": { + "type": "object", + "additionalProperties": { + "$ref": "#/defs/Provider" + } + }, + "provision": { + "type": "string" + }, + "provider": { + "$ref": "#/defs/Provider" + }, + "claim": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "site": { + "type": "string" + }, + "field": { + "type": "string" + }, + "value": { + "description": "a decided field's value" + }, + "mesh-wide": { + "type": "object" + }, + "layer": { + "type": "object" + }, + "mesh-wide-values": { + "type": "object" + }, + "values": { + "type": "object" + }, + "whole": { + "$ref": "#/defs/Placement", + "description": "the node's whole placement, for a seed" + }, + "decided": { + "$ref": "#/defs/NodeDecided" + }, + "keys": { + "type": "object", + "properties": { + "identity": { + "type": "string" + }, + "overlay": { + "type": "string" + }, + "sealing": { + "type": "string" + }, + "serving": { + "type": "string" + } + }, + "required": [ + "identity", + "sealing" + ], + "description": "the public halves of a joining node's keys" + } + }, + "required": [ + "op", + "node" + ] + }, + "Warrant": { + "type": "object", + "description": "the enrolment's warrant an add-node carries", + "properties": { + "node": { + "type": "string" + }, + "address": { + "type": "string" + }, + "ask": { + "type": "string" + }, + "warrant": { + "type": "string" + } + }, + "required": [ + "node", + "address", + "ask", + "warrant" + ] + }, + "Verdict": { + "type": "object", + "description": "the holder's judgement of a proposal's answers", + "properties": { + "state": { + "type": "string", + "description": "waits, pass or fail" + }, + "at": { + "type": "string", + "format": "date-time" + }, + "fails": { + "type": "array", + "items": { + "type": "string" + } + }, + "silent": { + "type": "array", + "items": { + "type": "string" + } + }, + "until": { + "type": "string", + "format": "date-time" + }, + "intended": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "unintended": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "carried": { + "type": "object", + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "answers": { + "type": "object", + "additionalProperties": { + "type": "object" + }, + "description": "every node's answer to its candidate, by node" + }, + "notes": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "state", + "at" + ] + }, + "Proposal": { + "type": "object", + "description": "an open proposal", + "properties": { + "v": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "by": { + "type": "string" + }, + "why": { + "type": "string" + }, + "at": { + "type": "string", + "format": "date-time" + }, + "written": { + "type": "string", + "format": "date-time" + }, + "changes": { + "type": "array", + "items": { + "$ref": "#/defs/ProposalChange" + } + }, + "seed": { + "type": "boolean" + }, + "export-digest": { + "type": "string" + }, + "base": { + "type": "object", + "additionalProperties": { + "type": "integer" + }, + "description": "by node whose key it changes, the revision of the key it was laid over (0: none)" + }, + "must": { + "type": "array", + "items": { + "type": "string" + } + }, + "hand-back": { + "type": "boolean" + }, + "state": { + "type": "string", + "description": "answering, failed or asked" + }, + "verdict": { + "$ref": "#/defs/Verdict" + }, + "ask": { + "type": "string" + }, + "words": { + "type": "string" + }, + "again": { + "type": "string", + "format": "date-time" + }, + "warrant": { + "$ref": "#/defs/Warrant" + }, + "withdrawn": { + "type": "string" + } + }, + "required": [ + "id", + "by", + "why", + "at", + "changes", + "base", + "state" + ] + }, + "PlacementEvent": { + "type": "object", + "description": "what each placement event carries", + "properties": { + "proposal": { + "$ref": "#/defs/Proposal" + }, + "nodes": { + "type": "array", + "items": { + "type": "object" + }, + "description": "for placement-changed, by node the revisions before and after and the modules added, removed or changed" + }, + "warrant": { + "type": "object", + "description": "who approved it, through which channel, when", + "properties": { + "warrant": { + "type": "string" + }, + "words": { + "type": "string" + }, + "at": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "warrant", + "words", + "at" + ] + }, + "ended": { + "type": "string", + "description": "for placement-withdrawn, why it ended" + } + }, + "required": [ + "proposal" + ] + }, + "SeatHolders": { + "type": "object", + "description": "one key of seat-holders", + "properties": { + "v": { + "type": "integer" + }, + "seat": { + "type": "string" + }, + "scope": { + "type": "string" + }, + "node": { + "type": "string" + }, + "outside": { + "type": "boolean", + "description": "the claim names no seat of the seat list" + }, + "holders": { + "type": "array", + "items": { + "type": "object", + "properties": { + "module": { + "type": "string" + }, + "node": { + "type": "string" + } + }, + "required": [ + "module", + "node" + ] + } + } + }, + "required": [ + "seat", + "scope", + "holders" + ] + } + } +} diff --git a/placement/seats/mesh-wants/fixtures/states.json b/placement/seats/mesh-wants/fixtures/states.json new file mode 100644 index 0000000..cb38a39 --- /dev/null +++ b/placement/seats/mesh-wants/fixtures/states.json @@ -0,0 +1,47 @@ +[ + { + "name": "a want", + "of": "state:wants/want..", + "given": { + "v": 1, + "fp": "ab12", + "rule": 3 + } + }, + { + "name": "a node's own want", + "of": "state:wants/want..node", + "given": { + "v": 1, + "composed": "sha256:aa", + "self": true + } + }, + { + "name": "a held fingerprint", + "of": "state:wants/held..", + "given": { + "v": 1, + "node": "ace" + } + }, + { + "name": "a difference", + "of": "state:differences/.", + "given": { + "v": 1, + "want": "ab12", + "unexplained": true + } + }, + { + "name": "a composition", + "of": "state:compositions/.", + "given": { + "v": 1, + "group": "g1", + "heads": "0123456789ab", + "state": "pass" + } + } +] diff --git a/placement/seats/mesh-wants/seat.json b/placement/seats/mesh-wants/seat.json new file mode 100644 index 0000000..141b7d0 --- /dev/null +++ b/placement/seats/mesh-wants/seat.json @@ -0,0 +1,279 @@ +{ + "format": 1, + "seat": "mesh-wants", + "scope": "mesh", + "grain": "whole", + "decision": "novox/hq ADR 0565, ADR 0634, ADR 0635", + "description": "The seat that says what each node should run and whether each module's new builds wait for a person (ADR 0565, ADR 0539).", + "verbs": [ + { + "name": "upgrade", + "description": "Whether each module's new builds wait for a person: its policy (roll, together or record) and where it comes from (a person, the module, the bus, its irreplaceable data, the default); the bus is never rolled. With module, that one; with module, policy and why, a person's choice for it, recorded as a hand act; with policy default, the choice taken back. With compare, each node's own composition and the assigner's findings.", + "input": { + "properties": { + "compare": { + "description": "\"true\": each node's own composition, by its digest, and the assigner's findings", + "enum": [ + "true", + "false" + ], + "type": "string" + }, + "module": { + "description": "one module", + "type": "string" + }, + "policy": { + "description": "with module: roll, together, record, or default to take a person's choice back", + "type": "string" + }, + "why": { + "description": "with policy: why, required and kept", + "type": "string" + } + }, + "type": "object" + }, + "answer": { + "type": "object", + "description": "each module's policy, or the one chosen, or the comparison" + } + }, + { + "name": "bus", + "description": "What a bus upgrade would do: the fingerprint of the bus module each node runs against the indexer's newest, and the last bus steps from the hand-act log and the assigner's puts of the bus. Read-only: the step itself is the put on the operator's warrant.", + "input": { + "properties": {}, + "type": "object" + }, + "answer": { + "type": "object", + "description": "what a bus upgrade would do, read only" + } + } + ], + "events": [ + { + "name": "policy-changed", + "body": { + "type": "object" + } + } + ], + "family": { + "name": "mesh-wants.events", + "max-age-days": 7 + }, + "states": [ + { + "name": "wants", + "template": "state", + "key-forms": [ + { + "key": "want..", + "value": { + "type": "object", + "description": "a node's want for a module", + "properties": { + "v": { + "type": "integer" + }, + "fp": { + "type": "string" + }, + "composed": { + "type": "string" + }, + "removed": { + "type": "boolean" + }, + "newest": { + "type": "string" + }, + "rule": { + "type": "integer", + "description": "the rule that holds the node back, 1 to 10" + }, + "backup": { + "type": "string", + "description": "again or skip" + }, + "words": { + "type": "string" + }, + "put": { + "type": "boolean" + }, + "hold": { + "type": "boolean" + }, + "yes": { + "type": "string" + }, + "release": { + "type": "string" + }, + "self": { + "type": "boolean" + } + } + }, + "description": "a node's want for a module" + }, + { + "key": "want..node", + "value": { + "type": "object", + "description": "a node's own want", + "properties": { + "v": { + "type": "integer" + }, + "composed": { + "type": "string" + }, + "words": { + "type": "string" + }, + "self": { + "type": "boolean" + } + }, + "required": [ + "composed" + ] + }, + "description": "a node's own want, naming the digest of its own composition" + }, + { + "key": "held..", + "value": { + "type": "object", + "description": "a fingerprint the assigner holds", + "properties": { + "v": { + "type": "integer" + }, + "node": { + "type": "string" + }, + "words": { + "type": "string" + } + }, + "required": [ + "node" + ] + }, + "description": "a module's fingerprint the assigner holds" + } + ], + "history": 20, + "description": "what each node should run, and the fingerprints held back" + }, + { + "name": "differences", + "template": "state", + "key-forms": [ + { + "key": ".", + "value": { + "type": "object", + "description": "the difference between a want and what is sent", + "properties": { + "v": { + "type": "integer" + }, + "want": { + "type": "string" + }, + "want-removed": { + "type": "boolean" + }, + "sent": { + "type": "string" + }, + "sent-build": { + "type": "string" + }, + "rule": { + "type": "integer" + }, + "unexplained": { + "type": "boolean" + }, + "words": { + "type": "string" + } + } + }, + "description": "a module on a node" + } + ], + "history": 64, + "description": "differences between a want and what the node-engine is sent" + }, + { + "name": "compositions", + "template": "state", + "key-forms": [ + { + "key": ".", + "value": { + "type": "object", + "description": "the composition verdict of a group's check at its heads", + "properties": { + "v": { + "type": "integer" + }, + "group": { + "type": "string" + }, + "heads": { + "type": "string", + "description": "12 lowercase hex digits" + }, + "state": { + "type": "string", + "description": "pass, fail or waits" + }, + "words": { + "type": "string" + }, + "sdk": { + "type": "string" + }, + "machines": { + "type": "array", + "items": { + "type": "string" + } + }, + "machine": { + "type": "string" + }, + "warnings": { + "type": "array", + "items": { + "type": "string" + } + }, + "took": { + "type": "integer", + "description": "milliseconds" + } + }, + "required": [ + "group", + "heads", + "state" + ] + }, + "description": "a group's check at its heads" + } + ], + "history": 5, + "description": "composition verdicts of a group's check; the checker reads them" + } + ] +} diff --git a/placement/ts/seats/meshmoduleassigner/package-lock.json b/placement/ts/seats/meshmoduleassigner/package-lock.json new file mode 100644 index 0000000..b346586 --- /dev/null +++ b/placement/ts/seats/meshmoduleassigner/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-placement-contracts-mesh-module-assigner", + "packages": { + "": { + "name": "@novox/mesh-placement-contracts-mesh-module-assigner", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/placement/ts/seats/meshmoduleassigner/package.json b/placement/ts/seats/meshmoduleassigner/package.json new file mode 100644 index 0000000..5c25c3f --- /dev/null +++ b/placement/ts/seats/meshmoduleassigner/package.json @@ -0,0 +1,13 @@ +{ + "description": "The contract of the seat mesh-module-assigner, generated from its definition.", + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "src" + ], + "name": "@novox/mesh-placement-contracts-mesh-module-assigner", + "type": "module", + "types": "./src/index.ts", + "version": "0.1.0" +} diff --git a/placement/ts/seats/meshmoduleassigner/src/index.ts b/placement/ts/seats/meshmoduleassigner/src/index.ts new file mode 100644 index 0000000..8f958a4 --- /dev/null +++ b/placement/ts/seats/meshmoduleassigner/src/index.ts @@ -0,0 +1,488 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-module-assigner", defined whole (seats/mesh-module-assigner/seat.json): The seat that places modules on nodes (ADR 0567): the state of placement and the proposals that change it. Renamed from mesh-placement (ADR 0634 §9). + +export const seat = "mesh-module-assigner"; + +export const verbs = { + placement: "placement", + proposals: "proposals", + propose: "propose", + withdraw: "withdraw", + again: "again", +} as const; + +export const events = { + placementProposed: "placement-proposed", + placementWithdrawn: "placement-withdrawn", + placementJudged: "placement-judged", + placementChanged: "placement-changed", +} as const; + +export const family = { name: "mesh-module-assigner.events", maxAgeDays: 7 } as const; + +export const placementProposedSubject = "mesh.seat.mesh-module-assigner.event.placement-proposed"; + +export const placementWithdrawnSubject = "mesh.seat.mesh-module-assigner.event.placement-withdrawn"; + +export const placementJudgedSubject = "mesh.seat.mesh-module-assigner.event.placement-judged"; + +export const placementChangedSubject = "mesh.seat.mesh-module-assigner.event.placement-changed"; + +export const states = { + placement: "placement", + proposals: "proposals", + candidates: "candidates", + pulses: "pulses", + seatHolders: "seat-holders", +} as const; + +/** The key of one value of the state placement under the form ".node". */ +export function placementByNodeNodeKey(node: string): string { + return node + ".node"; +} + +/** The parts of a key of the form ".node", or undefined for a key of another form. */ +export function parsePlacementByNodeNodeKey(key: string): { node: string } | undefined { + let rest = key; + const at0 = rest.indexOf(".node"); + if (at0 <= 0) return undefined; + const node = rest.slice(0, at0); + rest = rest.slice(at0 + 5); + if (rest !== "") return undefined; + return { node }; +} + +/** The key of one value of the state placement under the form ".". */ +export function placementByNodeModuleKey(node: string, module: string): string { + return node + "." + module; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parsePlacementByNodeModuleKey(key: string): { node: string; module: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const node = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const module = rest; + rest = ""; + if (rest !== "") return undefined; + return { node, module }; +} + +/** The key of one value of the state proposals under the form "". */ +export function proposalsByProposalKey(proposal: string): string { + return proposal; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseProposalsByProposalKey(key: string): { proposal: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const proposal = rest; + rest = ""; + if (rest !== "") return undefined; + return { proposal }; +} + +/** The key of one value of the state candidates under the form ".". */ +export function candidatesByProposalNodeKey(proposal: string, node: string): string { + return proposal + "." + node; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parseCandidatesByProposalNodeKey(key: string): { proposal: string; node: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const proposal = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const node = rest; + rest = ""; + if (rest !== "") return undefined; + return { proposal, node }; +} + +/** The key of one value of the state pulses under the form "answer..". */ +export function pulsesByAnswerProposalNodeKey(proposal: string, node: string): string { + return "answer." + proposal + "." + node; +} + +/** The parts of a key of the form "answer..", or undefined for a key of another form. */ +export function parsePulsesByAnswerProposalNodeKey(key: string): { proposal: string; node: string } | undefined { + let rest = key; + if (!rest.startsWith("answer.")) return undefined; + rest = rest.slice(7); + const at1 = rest.indexOf("."); + if (at1 <= 0) return undefined; + const proposal = rest.slice(0, at1); + rest = rest.slice(at1 + 1); + if (rest === "") return undefined; + const node = rest; + rest = ""; + if (rest !== "") return undefined; + return { proposal, node }; +} + +/** The key of one value of the state pulses under the form "ask.". */ +export function pulsesByAskProposalKey(proposal: string): string { + return "ask." + proposal; +} + +/** The parts of a key of the form "ask.", or undefined for a key of another form. */ +export function parsePulsesByAskProposalKey(key: string): { proposal: string } | undefined { + let rest = key; + if (!rest.startsWith("ask.")) return undefined; + rest = rest.slice(4); + if (rest === "") return undefined; + const proposal = rest; + rest = ""; + if (rest !== "") return undefined; + return { proposal }; +} + +/** The key of one value of the state seat-holders under the form "". */ +export function seatHoldersBySeatKey(seat: string): string { + return seat; +} + +/** The parts of a key of the form "", or undefined for a key of another form. */ +export function parseSeatHoldersBySeatKey(key: string): { seat: string } | undefined { + let rest = key; + if (rest === "") return undefined; + const seat = rest; + rest = ""; + if (rest !== "") return undefined; + return { seat }; +} + +/** The key of one value of the state seat-holders under the form ".". */ +export function seatHoldersBySeatNodeKey(seat: string, node: string): string { + return seat + "." + node; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parseSeatHoldersBySeatNodeKey(key: string): { seat: string; node: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const seat = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const node = rest; + rest = ""; + if (rest !== "") return undefined; + return { seat, node }; +} + +/** one seat a module placed on the node holds */ +export interface Holding { + "claim": string; + "kind"?: string; + "module": string; + "scope"?: string; + "site"?: string; +} + +/** what a person decides of a node; no secret and no key material */ +export interface NodeDecided { + "account"?: string; + "account-home"?: string; + "address"?: string; + "adopted"?: boolean; + "agent-account"?: string; + "agent-account-home"?: string; + "endpoint"?: string; + "hub"?: boolean; + /** on the hub, the name a person gave a peer of that tunnel, by its public key */ + "peer-names"?: Record; + "public-domain"?: string; + "site"?: string; + "taken"?: string[]; + /** the tunnel found on the node when it was adopted */ + "tunnel"?: Record; +} + +/** one module placed on a node */ +export interface PlacedModule { + /** by provision, the provider each requirement that keeps data is bound to */ + "bindings"?: Record; + "has-layer"?: boolean; + "has-mesh-wide"?: boolean; + /** this node's settings layer of the module's placement */ + "layer"?: Record; + /** the whole mesh's settings layer of the module's placement */ + "mesh-wide"?: Record; + /** config values a person set in the whole mesh's layer, for a module in the settings form */ + "mesh-wide-values"?: Record; + "module": string; + /** the host port each declared port is published on, by the port the software listens on */ + "ports"?: Record; + /** config values a person set in this node's layer, for a module in the settings form */ + "values"?: Record; +} + +/** one node's placement assembled */ +export interface Placement { + "decided": NodeDecided; + "holds"?: Holding[]; + "modules": PlacedModule[]; + "node": string; + /** by provision, the provider chosen for the node */ + "pins"?: Record; + "revision"?: string; + "v"?: number; +} + +/** who approved it, through which channel, when */ +export interface PlacementEventWarrant { + "at": string; + "warrant": string; + "words": string; +} + +/** what each placement event carries */ +export interface PlacementEvent { + /** for placement-withdrawn, why it ended */ + "ended"?: string; + /** for placement-changed, by node the revisions before and after and the modules added, removed or changed */ + "nodes"?: Record[]; + "proposal": Proposal; + /** who approved it, through which channel, when */ + "warrant"?: PlacementEventWarrant; +} + +/** an open proposal */ +export interface Proposal { + "again"?: string; + "ask"?: string; + "at": string; + /** by node whose key it changes, the revision of the key it was laid over (0: none) */ + "base": Record; + "by": string; + "changes": ProposalChange[]; + "export-digest"?: string; + "hand-back"?: boolean; + "id": string; + "must"?: string[]; + "seed"?: boolean; + /** answering, failed or asked */ + "state": string; + "v"?: number; + "verdict"?: Verdict; + "warrant"?: Warrant; + "why": string; + "withdrawn"?: string; + "words"?: string; + "written"?: string; +} + +/** the public halves of a joining node's keys */ +export interface ProposalChangeKeys { + "identity": string; + "overlay"?: string; + "sealing": string; + "serving"?: string; +} + +/** one change of a proposal */ +export interface ProposalChange { + "bindings"?: Record; + "claim"?: string; + "decided"?: NodeDecided; + "field"?: string; + /** the public halves of a joining node's keys */ + "keys"?: ProposalChangeKeys; + "kind"?: string; + "layer"?: Record; + "mesh-wide"?: Record; + "mesh-wide-values"?: Record; + "module"?: string; + "node": string; + /** place, take-off, pin, unpin, hold, unhold, decide, config, seed, add-node or remove-node */ + "op": string; + "ports"?: Record; + "provider"?: Provider; + "provision"?: string; + "scope"?: string; + "site"?: string; + /** a decided field's value */ + "value"?: unknown; + "values"?: Record; + /** the node's whole placement, for a seed */ + "whole"?: Placement; +} + +/** the module on a node that answers a requirement or holds a seat */ +export interface Provider { + "module": string; + "node": string; +} + +export interface SeatHoldersHoldersItem { + "module": string; + "node": string; +} + +/** one key of seat-holders */ +export interface SeatHolders { + "holders": SeatHoldersHoldersItem[]; + "node"?: string; + /** the claim names no seat of the seat list */ + "outside"?: boolean; + "scope": string; + "seat": string; + "v"?: number; +} + +/** the holder's judgement of a proposal's answers */ +export interface Verdict { + /** every node's answer to its candidate, by node */ + "answers"?: Record>; + "at": string; + "carried"?: Record; + "fails"?: string[]; + "intended"?: Record; + "notes"?: string[]; + "silent"?: string[]; + /** waits, pass or fail */ + "state": string; + "unintended"?: Record; + "until"?: string; +} + +/** the enrolment's warrant an add-node carries */ +export interface Warrant { + "address": string; + "ask": string; + "node": string; + "warrant": string; +} + +/** is what placement takes. */ +export interface PlacementInput { + /** "true": the changes in order, from the stream and the state's revisions */ + "history"?: "true" | "false"; + /** one module: the nodes it is placed on */ + "module"?: string; + /** one node */ + "node"?: string; + /** "true": who holds each seat, and on which node (the state seat-holders); with node, that node's */ + "seats"?: "true" | "false"; +} + +/** is what placement answers. */ +export type PlacementAnswer = Record; + +/** is what proposals takes. */ +export interface ProposalsInput { + /** one proposal's id */ + "id"?: string; +} + +/** is what proposals answers. */ +export type ProposalsAnswer = Record; + +/** is what propose takes. */ +export interface ProposeInput { + /** the changes, each an object with `op`: place (node, module, ports, bindings), take-off (node, module), pin (node, provision, provider {node, module}), unpin (node, provision), hold (node, module, claim, scope, kind, site), unhold (node, module, claim), decide (node, field, value: a decided field of the node, its address among them), config (node, module, mesh-wide, layer, mesh-wide-values, values), add-node (node, decided, keys: a joining node, mesh-enrolment's alone, the proposal's one change, with the enrolment's warrant, its tunnel key alone at the operator's yes; ADR 0587 §7), remove-node (node: a joining node never confirmed, taken off when its enrolment ends; mesh-enrolment's alone, on the same warrant) */ + "changes": Record[]; + /** for an add-node alone: the enrolment's warrant {node, address, ask, warrant}, naming the change's node and address */ + "warrant"?: Record; + /** why, shown to the operator with the proposal */ + "why": string; +} + +/** is what propose answers. */ +export type ProposeAnswer = Record; + +/** is what withdraw takes. */ +export interface WithdrawInput { + /** the proposal's id */ + "id": string; + /** why, kept in the event */ + "why": string; +} + +/** is what withdraw answers. */ +export type WithdrawAnswer = Record; + +/** is what again takes. */ +export interface AgainInput { + /** the proposal's id */ + "id": string; +} + +/** is what again answers. */ +export type AgainAnswer = Record; + +/** is the body of the event placement-proposed. */ +export type PlacementProposedEvent = PlacementEvent; + +/** is the body of the event placement-withdrawn. */ +export type PlacementWithdrawnEvent = PlacementEvent; + +/** is the body of the event placement-judged. */ +export type PlacementJudgedEvent = PlacementEvent; + +/** is the body of the event placement-changed. */ +export type PlacementChangedEvent = PlacementEvent; + +export interface PlacementByNodeNodeValueModulesItem { + /** the digest of the module key's value it stands for */ + "digest": string; + "module": string; +} + +/** is one value of the state placement under the key .node. */ +export interface PlacementByNodeNodeValue { + "decided": NodeDecided; + "holds"?: Holding[]; + /** the modules placed, in the order the composition reads them */ + "modules": PlacementByNodeNodeValueModulesItem[]; + "node": string; + "pins"?: Record; + "v"?: number; +} + +/** is one value of the state placement under the key .. */ +export interface PlacementByNodeModuleValue { + "node": string; + "placed": PlacedModule; + /** the value it replaces while a write is under way */ + "previous"?: PlacedModule; + "v"?: number; +} + +/** is one value of the state proposals under the key . */ +export type ProposalsByProposalValue = Proposal; + +/** is one value of the state candidates under the key .. */ +export interface CandidatesByProposalNodeValue { + /** the node's placement with the proposal laid over it; absent when the proposal takes its key away */ + "placement"?: Placement; + "proposal": string; + /** the proposal changes this node's composition through another's key */ + "reached"?: boolean; + "v"?: number; +} + +/** is one value of the state pulses under the key answer... */ +export interface PulsesByAnswerProposalNodeValue { + "v"?: number; +} + +/** is one value of the state pulses under the key ask.. */ +export interface PulsesByAskProposalValue { + "v"?: number; +} + +/** is one value of the state seat-holders under the key . */ +export type SeatHoldersBySeatValue = SeatHolders; + +/** is one value of the state seat-holders under the key .. */ +export type SeatHoldersBySeatNodeValue = SeatHolders; + diff --git a/placement/ts/seats/meshmoduleassigner/tsconfig.json b/placement/ts/seats/meshmoduleassigner/tsconfig.json new file mode 100644 index 0000000..60274c6 --- /dev/null +++ b/placement/ts/seats/meshmoduleassigner/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/placement/ts/seats/meshwants/package-lock.json b/placement/ts/seats/meshwants/package-lock.json new file mode 100644 index 0000000..e274634 --- /dev/null +++ b/placement/ts/seats/meshwants/package-lock.json @@ -0,0 +1,12 @@ +{ + "lockfileVersion": 3, + "name": "@novox/mesh-placement-contracts-mesh-wants", + "packages": { + "": { + "name": "@novox/mesh-placement-contracts-mesh-wants", + "version": "0.1.0" + } + }, + "requires": true, + "version": "0.1.0" +} diff --git a/placement/ts/seats/meshwants/package.json b/placement/ts/seats/meshwants/package.json new file mode 100644 index 0000000..d4a29ee --- /dev/null +++ b/placement/ts/seats/meshwants/package.json @@ -0,0 +1,13 @@ +{ + "description": "The contract of the seat mesh-wants, generated from its definition.", + "exports": { + ".": "./src/index.ts" + }, + "files": [ + "src" + ], + "name": "@novox/mesh-placement-contracts-mesh-wants", + "type": "module", + "types": "./src/index.ts", + "version": "0.1.0" +} diff --git a/placement/ts/seats/meshwants/src/index.ts b/placement/ts/seats/meshwants/src/index.ts new file mode 100644 index 0000000..ad3dab8 --- /dev/null +++ b/placement/ts/seats/meshwants/src/index.ts @@ -0,0 +1,211 @@ +// Code generated by seatgen from the seat definitions; DO NOT EDIT. + +// The contract of the seat "mesh-wants", defined whole (seats/mesh-wants/seat.json): The seat that says what each node should run and whether each module's new builds wait for a person (ADR 0565, ADR 0539). + +export const seat = "mesh-wants"; + +export const verbs = { + upgrade: "upgrade", + bus: "bus", +} as const; + +export const events = { + policyChanged: "policy-changed", +} as const; + +export const family = { name: "mesh-wants.events", maxAgeDays: 7 } as const; + +export const policyChangedSubject = "mesh.seat.mesh-wants.event.policy-changed"; + +export const states = { + wants: "wants", + differences: "differences", + compositions: "compositions", +} as const; + +/** The key of one value of the state wants under the form "want..". */ +export function wantsByWantNodeModuleKey(node: string, module: string): string { + return "want." + node + "." + module; +} + +/** The parts of a key of the form "want..", or undefined for a key of another form. */ +export function parseWantsByWantNodeModuleKey(key: string): { node: string; module: string } | undefined { + let rest = key; + if (!rest.startsWith("want.")) return undefined; + rest = rest.slice(5); + const at1 = rest.indexOf("."); + if (at1 <= 0) return undefined; + const node = rest.slice(0, at1); + rest = rest.slice(at1 + 1); + if (rest === "") return undefined; + const module = rest; + rest = ""; + if (rest !== "") return undefined; + return { node, module }; +} + +/** The key of one value of the state wants under the form "want..node". */ +export function wantsByWantNodeNodeKey(node: string): string { + return "want." + node + ".node"; +} + +/** The parts of a key of the form "want..node", or undefined for a key of another form. */ +export function parseWantsByWantNodeNodeKey(key: string): { node: string } | undefined { + let rest = key; + if (!rest.startsWith("want.")) return undefined; + rest = rest.slice(5); + const at1 = rest.indexOf(".node"); + if (at1 <= 0) return undefined; + const node = rest.slice(0, at1); + rest = rest.slice(at1 + 5); + if (rest !== "") return undefined; + return { node }; +} + +/** The key of one value of the state wants under the form "held..". */ +export function wantsByHeldModuleFingerprintKey(module: string, fingerprint: string): string { + return "held." + module + "." + fingerprint; +} + +/** The parts of a key of the form "held..", or undefined for a key of another form. */ +export function parseWantsByHeldModuleFingerprintKey(key: string): { module: string; fingerprint: string } | undefined { + let rest = key; + if (!rest.startsWith("held.")) return undefined; + rest = rest.slice(5); + const at1 = rest.indexOf("."); + if (at1 <= 0) return undefined; + const module = rest.slice(0, at1); + rest = rest.slice(at1 + 1); + if (rest === "") return undefined; + const fingerprint = rest; + rest = ""; + if (rest !== "") return undefined; + return { module, fingerprint }; +} + +/** The key of one value of the state differences under the form ".". */ +export function differencesByNodeModuleKey(node: string, module: string): string { + return node + "." + module; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parseDifferencesByNodeModuleKey(key: string): { node: string; module: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const node = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const module = rest; + rest = ""; + if (rest !== "") return undefined; + return { node, module }; +} + +/** The key of one value of the state compositions under the form ".". */ +export function compositionsByGroupHeadsKey(group: string, heads: string): string { + return group + "." + heads; +} + +/** The parts of a key of the form ".", or undefined for a key of another form. */ +export function parseCompositionsByGroupHeadsKey(key: string): { group: string; heads: string } | undefined { + let rest = key; + const at0 = rest.indexOf("."); + if (at0 <= 0) return undefined; + const group = rest.slice(0, at0); + rest = rest.slice(at0 + 1); + if (rest === "") return undefined; + const heads = rest; + rest = ""; + if (rest !== "") return undefined; + return { group, heads }; +} + +/** is what upgrade takes. */ +export interface UpgradeInput { + /** "true": each node's own composition, by its digest, and the assigner's findings */ + "compare"?: "true" | "false"; + /** one module */ + "module"?: string; + /** with module: roll, together, record, or default to take a person's choice back */ + "policy"?: string; + /** with policy: why, required and kept */ + "why"?: string; +} + +/** is what upgrade answers. */ +export type UpgradeAnswer = Record; + +/** is what bus takes. */ +export interface BusInput { +} + +/** is what bus answers. */ +export type BusAnswer = Record; + +/** is the body of the event policy-changed. */ +export type PolicyChangedEvent = Record; + +/** is one value of the state wants under the key want... */ +export interface WantsByWantNodeModuleValue { + /** again or skip */ + "backup"?: string; + "composed"?: string; + "fp"?: string; + "hold"?: boolean; + "newest"?: string; + "put"?: boolean; + "release"?: string; + "removed"?: boolean; + /** the rule that holds the node back, 1 to 10 */ + "rule"?: number; + "self"?: boolean; + "v"?: number; + "words"?: string; + "yes"?: string; +} + +/** is one value of the state wants under the key want..node. */ +export interface WantsByWantNodeNodeValue { + "composed": string; + "self"?: boolean; + "v"?: number; + "words"?: string; +} + +/** is one value of the state wants under the key held... */ +export interface WantsByHeldModuleFingerprintValue { + "node": string; + "v"?: number; + "words"?: string; +} + +/** is one value of the state differences under the key .. */ +export interface DifferencesByNodeModuleValue { + "rule"?: number; + "sent"?: string; + "sent-build"?: string; + "unexplained"?: boolean; + "v"?: number; + "want"?: string; + "want-removed"?: boolean; + "words"?: string; +} + +/** is one value of the state compositions under the key .. */ +export interface CompositionsByGroupHeadsValue { + "group": string; + /** 12 lowercase hex digits */ + "heads": string; + "machine"?: string; + "machines"?: string[]; + "sdk"?: string; + /** pass, fail or waits */ + "state": string; + /** milliseconds */ + "took"?: number; + "v"?: number; + "warnings"?: string[]; + "words"?: string; +} + diff --git a/placement/ts/seats/meshwants/tsconfig.json b/placement/ts/seats/meshwants/tsconfig.json new file mode 100644 index 0000000..60274c6 --- /dev/null +++ b/placement/ts/seats/meshwants/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts"] +} diff --git a/seats/index.json b/seats/index.json index eed4efb..00b0a20 100644 --- a/seats/index.json +++ b/seats/index.json @@ -3,15 +3,11 @@ "seats": [ "mesh-store", "mesh-broker", - "mesh-vault", "mesh-config", "mesh-artifact-store", "mesh-catalog", "mesh-delivery", - "mesh-placement", "mesh-delivery-checker", - "mesh-wants", - "mesh-enrolment", "mesh-artifact-sweep", "npm-package-registry", "go-package-registry", @@ -42,7 +38,6 @@ "node-lock-screen", "node-clipboard", "node-bar", - "node-compositor", - "node-secret-service" + "node-compositor" ] } diff --git a/seats/mesh-placement/seat.json b/seats/mesh-placement/seat.json deleted file mode 100644 index cc3d44c..0000000 --- a/seats/mesh-placement/seat.json +++ /dev/null @@ -1,137 +0,0 @@ -{ - "format": 1, - "seat": "mesh-placement", - "scope": "mesh", - "grain": "seat-list", - "decision": "novox/hq ADR 0567", - "verbs": [ - { - "name": "placement", - "description": "Read placement: one node's key, the nodes one module is placed on, or every key, each with its revision, and how full the state is against its cap. With history, one node's or one module's changes in order: a revision the state keeps whole, an older one as the proposal's changes from its placement-changed event.", - "optional": true, - "input": { - "properties": { - "history": { - "description": "\"true\": the changes in order, from the stream and the state's revisions", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "module": { - "description": "one module: the nodes it is placed on", - "type": "string" - }, - "node": { - "description": "one node", - "type": "string" - }, - "seats": { - "description": "\"true\": who holds each seat, and on which node (the state seat-holders); with node, that node's", - "enum": [ - "true", - "false" - ], - "type": "string" - } - }, - "type": "object" - } - }, - { - "name": "proposals", - "description": "Every open proposal of placement: its changes, the revision of each key it was laid over, its candidates, every node's answer, the differences computed and answered, its verdict and its ask. Narrowed to one by id.", - "optional": true, - "input": { - "properties": { - "id": { - "description": "one proposal's id", - "type": "string" - } - }, - "type": "object" - } - }, - { - "name": "propose", - "description": "Propose a change of placement, with why: laid over the current keys and refused at once, naming every finding, when the result cannot be placed (two modules on one host port, a provider not placed, a seat held once held twice, two nodes with one address, two hubs, a module the indexer does not hold, a node with no record). Otherwise the nodes it touches compose it in shadow and answer, and on a passing verdict the operator is asked to approve it. Who proposes is the caller, as the runtime proves it. An add-node from mesh-enrolment, whose warrant names its node and address, is written with no ask of its own: its Approve is the enrolment's (ADR 0587 §7).", - "optional": true, - "input": { - "additionalProperties": false, - "properties": { - "changes": { - "description": "the changes, each an object with `op`: place (node, module, ports, bindings), take-off (node, module), pin (node, provision, provider {node, module}), unpin (node, provision), hold (node, module, claim, scope, kind, site), unhold (node, module, claim), decide (node, field, value: a decided field of the node, its address among them), config (node, module, mesh-wide, layer, mesh-wide-values, values), add-node (node, decided, keys: a joining node, mesh-enrolment's alone, the proposal's one change, with the enrolment's warrant, its tunnel key alone at the operator's yes; ADR 0587 §7), remove-node (node: a joining node never confirmed, taken off when its enrolment ends; mesh-enrolment's alone, on the same warrant)", - "items": { - "required": [ - "op", - "node" - ], - "type": "object" - }, - "minItems": 1, - "type": "array" - }, - "warrant": { - "description": "for an add-node alone: the enrolment's warrant {node, address, ask, warrant}, naming the change's node and address", - "required": [ - "node", - "address", - "ask", - "warrant" - ], - "type": "object" - }, - "why": { - "description": "why, shown to the operator with the proposal", - "type": "string" - } - }, - "required": [ - "why", - "changes" - ], - "type": "object" - } - }, - { - "name": "withdraw", - "description": "Withdraw an open proposal: its key and its candidates are deleted and placement-withdrawn carries it whole. With why.", - "optional": true, - "input": { - "properties": { - "id": { - "description": "the proposal's id", - "type": "string" - }, - "why": { - "description": "why, kept in the event", - "type": "string" - } - }, - "required": [ - "id", - "why" - ], - "type": "object" - } - }, - { - "name": "again", - "description": "Judge an open proposal now with the answers that stand, and raise its ask again when its verdict passes and the operator's channel does not hold the ask. Nothing more: the recovery of a proposal whose judgement or ask was lost.", - "optional": true, - "input": { - "properties": { - "id": { - "description": "the proposal's id", - "type": "string" - } - }, - "required": [ - "id" - ], - "type": "object" - } - } - ] -} diff --git a/seats/mesh-wants/seat.json b/seats/mesh-wants/seat.json deleted file mode 100644 index 8b50c1a..0000000 --- a/seats/mesh-wants/seat.json +++ /dev/null @@ -1,51 +0,0 @@ -{ - "format": 1, - "seat": "mesh-wants", - "scope": "mesh", - "grain": "seat-list", - "decision": "novox/hq ADR 0565", - "verbs": [ - { - "name": "upgrade", - "description": "Whether each module's new builds wait for a person: its policy (roll, together or record) and where it comes from (a person, the module, the bus, its irreplaceable data, the default); the bus is never rolled. With module, that one; with module, policy and why, a person's choice for it, recorded as a hand act; with policy default, the choice taken back. With compare, each node's own composition and the assigner's findings.", - "input": { - "properties": { - "compare": { - "description": "\"true\": each node's own composition, by its digest, and the assigner's findings", - "enum": [ - "true", - "false" - ], - "type": "string" - }, - "module": { - "description": "one module", - "type": "string" - }, - "policy": { - "description": "with module: roll, together, record, or default to take a person's choice back", - "type": "string" - }, - "why": { - "description": "with policy: why, required and kept", - "type": "string" - } - }, - "type": "object" - } - }, - { - "name": "bus", - "description": "What a bus upgrade would do: the fingerprint of the bus module each node runs against the indexer's newest, and the last bus steps from the hand-act log and the assigner's puts of the bus. Read-only: the step itself is the put on the operator's warrant.", - "input": { - "properties": {}, - "type": "object" - } - } - ], - "events": [ - { - "name": "policy-changed" - } - ] -} diff --git a/ts/seats/meshvault/package.json b/ts/seats/meshvault/package.json deleted file mode 100644 index b811690..0000000 --- a/ts/seats/meshvault/package.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "description": "The contract of the seat mesh-vault, generated from its definition.", - "devDependencies": { - "typescript": "^5.6.0" - }, - "exports": { - ".": "./dist/index.js" - }, - "files": [ - "dist" - ], - "name": "@novox/mesh-contracts-mesh-vault", - "scripts": { - "build": "tsc", - "prepare": "tsc" - }, - "type": "module", - "version": "0.1.0" -}