From e96a7ada50057e73417abb5a0e356f151d17fbe5 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 7 Nov 2026 02:26:14 +0100 Subject: [PATCH] =?UTF-8?q?The=20node-engine=20keeps=20no=20copy=20of=20a?= =?UTF-8?q?=20seat's=20verbs:=20the=20seat=20list=20comes=20from=20the=20b?= =?UTF-8?q?us=20(issue=201921,=20ADR=200635=20=C2=A71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eight lists of seat verbs (checker, enrolment, artifact sweep, delivery, node-resolver, node-nfs-server, node-mounts, mesh-config's input) and their schema helpers were read by nothing but one test, and drifted from their seats (B0's verb input never reached the checker's copy). A guard row in the seats test finds any list of the catalogue's verbs written in the node-engine's code. Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../compose/resolution/artifact_sweep_seat.go | 43 --------- internal/compose/resolution/checker_seat.go | 33 ------- internal/compose/resolution/delivery_seat.go | 75 --------------- .../compose/resolution/delivery_seat_test.go | 30 ------ internal/compose/resolution/enrolment_seat.go | 67 ------------- internal/compose/resolution/node_resolver.go | 30 ------ internal/compose/resolution/seats.go | 15 --- internal/compose/resolution/seats_test.go | 94 +++++++++++++++++++ internal/compose/resolution/shares.go | 78 --------------- internal/compose/resolution/verbs.go | 59 ------------ 10 files changed, 94 insertions(+), 430 deletions(-) delete mode 100644 internal/compose/resolution/artifact_sweep_seat.go delete mode 100644 internal/compose/resolution/checker_seat.go delete mode 100644 internal/compose/resolution/delivery_seat.go delete mode 100644 internal/compose/resolution/delivery_seat_test.go delete mode 100644 internal/compose/resolution/enrolment_seat.go delete mode 100644 internal/compose/resolution/shares.go delete mode 100644 internal/compose/resolution/verbs.go diff --git a/internal/compose/resolution/artifact_sweep_seat.go b/internal/compose/resolution/artifact_sweep_seat.go deleted file mode 100644 index 98888e7..0000000 --- a/internal/compose/resolution/artifact_sweep_seat.go +++ /dev/null @@ -1,43 +0,0 @@ -package resolution - -import ( - mcat "git.novox.be/novox/mesh-sdk/go/catalogue" -) - -// artifactSweepVerbs are the seat's verbs, with the controller's arguments so a caller changes only the seat it asks. -func artifactSweepVerbs() []mcat.Verb { - return []mcat.Verb{ - {Name: "artifacts", Description: "Every artifact the mesh made that lives in its artifact store, each with its " + - "repository, digest, kind (image or archive) and state: kept, and why (a want names it, or one of the five " + - "newest builds of its module in the builder's log), eligible (made, kept for no reason, not yet let go), or " + - "unrecorded (in the store and named by nothing the mesh made: never removed). Reads only.", - Input: schema(map[string]string{ - "repository": "only this repository's references, as / (the counts stay whole)", - "collected": "\"true\": list the collected references too (counted always)", - }, nil, "collected")}, - {Name: "held", Description: "Whether the artifact store holds each reference given, asked with one HEAD each: " + - "held, gone (the store answered 404) or unknown with why. The builder asks it before it reuses a done key. " + - "Reads only.", - Input: listed(schema(map[string]string{ - "references": "the artifacts' references, artifact-store:////blobs/sha256:: a list, " + - "or one text separated by commas", - }, []string{"references"}), "references")}, - {Name: "collect", Description: "The artifact store's sweep, now: what is eligible is let go of. Without confirm a " + - "dry run: what would be let go, nothing held or deleted. With confirm and why: every kept archive held first, " + - "then each eligible artifact let go of, oldest first, an index with the platform manifests no kept index " + - "names, each deletion recorded in the hand-act log. Bounded by most (500 by default, at most 5000) and 45 " + - "seconds; what is left is said. Never touches a digest the mesh did not make.", - Input: schema(map[string]string{ - "confirm": "\"true\": let go of what is eligible (needs why); without it nothing is done", - "why": "with confirm: why, required and recorded", - "most": "let go of at most this many (500 by default, at most 5000)", - }, nil, "confirm")}, - {Name: "mirrors", Description: "The bases the mesh copied into its artifact store, each kept (a kept build stood on " + - "it), eligible (only builds no longer kept stood on it) or unrecorded (no build the builder's log holds stood on " + - "it: never removed), by what the builder's log says each build stood on. Records nothing by hand.", - Input: schema(map[string]string{}, nil)}, - {Name: "images", Description: "Every container image one machine's want and have name, each with the resources " + - "naming it: what the machine keeps when it prunes its images. Reads only.", - Input: schema(map[string]string{"node": "the machine's name"}, []string{"node"})}, - } -} diff --git a/internal/compose/resolution/checker_seat.go b/internal/compose/resolution/checker_seat.go deleted file mode 100644 index 337ffa9..0000000 --- a/internal/compose/resolution/checker_seat.go +++ /dev/null @@ -1,33 +0,0 @@ -package resolution - -import ( - mcat "git.novox.be/novox/mesh-sdk/go/catalogue" -) - -// checkerVerbs are the checker seat's verbs: ask a try, read one, end one by hand. Optional until the checker serves them -// everywhere. -func checkerVerbs() []mcat.Verb { - return []mcat.Verb{ - {Name: "try", Optional: true, Description: "Try a pushed head before its pull request is opened: the checker " + - "judges it as it judges a pull request's head, merged onto its trunk's head, its unit and integration " + - "levels through the build modules, and owes the forge nothing. Answers the try's id at once; try-read " + - "reads where it stands. A try of the same head that waits or runs is the answer to a second ask; at most " + - "4 wait or run at once, mesh-wide.", - Input: schema(map[string]string{"repository": "/", - "head": "the full commit to try, 40 or 64 hex digits", - "trunk": "the trunk commit to merge it onto, or the trunk's branch for its head now; the trunk's head when absent", - "tests": "a go test -run pattern: only those tests run, and nothing is kept", - "level": "unit or integration: only this level; both when absent"}, - []string{"repository", "head"}), - Replaces: []string{"sh merge-check.sh", "docker run postgres"}}, - {Name: "try-read", Optional: true, Description: "Where a try stands, as stored: queued, running, or its end " + - "(pass, fail, or error when it could not run), with what was said of it, the head and the trunk commit " + - "it was merged onto, the time it took and what failed first. Read-only.", - Input: schema(map[string]string{"id": "the try's id, try-"}, []string{"id"})}, - {Name: "try-end", Optional: true, Description: "End a try that waits or runs, as error, by hand: say why (kept in " + - "what it says). Its place among the tries is free at once and the next try of its head asks anew with a new " + - "id. A try that ended already is refused.", - Input: schema(map[string]string{"id": "the try's id, try-", "why": "why it is ended, in words"}, - []string{"id", "why"})}, - } -} diff --git a/internal/compose/resolution/delivery_seat.go b/internal/compose/resolution/delivery_seat.go deleted file mode 100644 index 851b592..0000000 --- a/internal/compose/resolution/delivery_seat.go +++ /dev/null @@ -1,75 +0,0 @@ -package resolution - -import ( - "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" - mcat "git.novox.be/novox/mesh-sdk/go/catalogue" -) - -// deliveryRemovedInR6 is the row of a verb release 6 removed from the delivery seat. -func deliveryRemovedInR6(name string) mcat.Verb { - return mcat.Verb{Name: name, Description: meshdeliveryclient.RemovedPrefix + meshdeliveryclient.RemovedVerbs()[name], Input: schema(nil, nil), Optional: true} -} - -// deliveryVerbs are the delivery seat's tools: what reads the merges and checks, and the acts of a person and healer H2 -// on a merge (ADR 0539 clause 17). -func deliveryVerbs() []mcat.Verb { - return []mcat.Verb{ - {Name: "deliveries", Description: "Every delivery not final, and those that ended in the last day, one line " + - "each: its id (owner/repository@commit), its state, its group, what it waits for and since when. Narrowed " + - "by state, repository or group.", - Input: schema(map[string]string{"state": "one state, e.g. delivering or held", - "repository": "owner/repository", "group": "a group's id (its branch name)", - "all": "\"true\": the final ones of the last thirty days too"}, nil, "all")}, - deliveryRemovedInR6("times"), - {Name: "show", Description: "One delivery or group whole: its delivery plan (what it builds, what each " + - "machine receives, what is not an ordinary send), every transition with when and why, the machine " + - "steps of its walk, its group and its order.", - Input: schema(map[string]string{"id": "a delivery's id, or a group's"}, []string{"id"})}, - {Name: "groups", Description: "Every delivery group: its members in order, why each pair is ordered " + - "(declared, built by, version skew, engine before controller, by name), its composed check and its " + - "state, derived from its members.", - Input: schema(map[string]string{"all": "\"true\": the groups that ended too"}, nil, "all")}, - deliveryRemovedInR6("what-if"), - {Name: "checks", Description: "What the mesh's checks said of a pull request's head or of one commit: each " + - "of the commit's mesh statuses (mesh/check, the one a main requires from R4e, hq's mesh/repo-check, " + - "mesh/delivery, …) with its state, " + - "description and when it was set; the merge check's full verdict as the controller said it — each " + - "layer's summary, the machine that ran it, when, its build id and its report; and whether the branch's " + - "protection would let it merge, every required status being success.", - Input: schema(map[string]string{"repository": "owner/repository", - "number": "a pull request's number: its head is read", - "commit": "a commit's sha, or the start of one, instead of a pull request"}, []string{"repository"}), - // Added after the seat's first holder shipped: optional until mesh-delivery serves it everywhere. - Optional: true}, - {Name: "table", Description: "The state table every delivery runs by: each transition with its guard, " + - "each state's bound and what healer H2 may do once it has passed; and the machine steps' table.", - Input: schema(map[string]string{}, nil)}, - {Name: "stalled", Description: "Every delivery held past its state's bound, with the transition the table " + - "lets healer H2 take for it, or none.", - Input: schema(map[string]string{}, nil)}, - {Name: "recheck", Description: "Check a rejected or ready delivery again: it goes back to proposed and the " + - "controller is asked for its check. With why.", - Input: schema(map[string]string{"id": "the delivery's id", "why": "why, kept with the transition"}, - []string{"id", "why"})}, - deliveryRemovedInR6("release"), - deliveryRemovedInR6("stop"), - {Name: "close", Description: "Healer H2's verb: take the one transition the table names for a delivery held " + - "past its bound. Refused for any other.", - Input: schema(map[string]string{"id": "the delivery's id", "why": "the condition it answers"}, - []string{"id", "why"})}, - deliveryRemovedInR6("retire-history"), - {Name: "settle", Description: "Ask the operator to settle deliveries held because their merge reached the " + - "trunk without a passing check: one ask on the operator's channel for all of them, as an Approve is asked. " + - "The verb moves nothing; on the operator's answer each goes to merged, and whether it is on the machines " + - "is derived as for any merge (novox/hq ADR 0556). Refused for a delivery not held, or held for another " + - "reason, naming why.", - Input: map[string]any{"type": "object", "additionalProperties": false, "required": []string{"ids", "why"}, - "properties": map[string]any{ - "ids": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "minItems": 1, - "description": "the held deliveries' ids (owner/repository@commit)"}, - "why": map[string]any{"type": "string", "description": "why they may be settled, shown to the operator"}, - }}, - // Added after the seat's release 6 holder shipped: optional until the gate serves it everywhere. - Optional: true}, - } -} diff --git a/internal/compose/resolution/delivery_seat_test.go b/internal/compose/resolution/delivery_seat_test.go deleted file mode 100644 index 5088f6d..0000000 --- a/internal/compose/resolution/delivery_seat_test.go +++ /dev/null @@ -1,30 +0,0 @@ -package resolution - -import ( - "testing" - - "git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient" -) - -// Each verb release 6 removed from the delivery seat is a row refusing it by name with the replacement the seat's client -// gives (the list is the contract's, not the SDK's: novox/hq ADR 0634 §2), and the rows are exactly those verbs. -func TestTheDeliverySeatRefusesTheRemovedVerbsByNameWithTheClientsReplacement(t *testing.T) { - removed := meshdeliveryclient.RemovedVerbs() - rows := 0 - for _, v := range deliveryVerbs() { - want, isRemoved := removed[v.Name] - if !isRemoved { - continue - } - rows++ - if v.Description != meshdeliveryclient.RemovedPrefix+want || !v.Optional { - t.Errorf("%s = %q (optional %v), want %q", v.Name, v.Description, v.Optional, meshdeliveryclient.RemovedPrefix+want) - } - } - if rows != len(removed) { - t.Errorf("%d rows refuse a removed verb, the client names %d", rows, len(removed)) - } - if len(removed) != 5 { - t.Errorf("the removed verbs are %v", removed) - } -} diff --git a/internal/compose/resolution/enrolment_seat.go b/internal/compose/resolution/enrolment_seat.go deleted file mode 100644 index 68ffb9f..0000000 --- a/internal/compose/resolution/enrolment_seat.go +++ /dev/null @@ -1,67 +0,0 @@ -package resolution - -import ( - mcat "git.novox.be/novox/mesh-sdk/go/catalogue" -) - -// enrolmentDecision is the record that made the seat. -const enrolmentDecision = "novox/hq ADR 0587" - -// enrolmentVerbs are the seat's verbs: what reads all it holds and does by hand all it does (ADR 0587 §8). Not optional: -// the seat is new with its holder, so no holder of before serves fewer. -func enrolmentVerbs() []mcat.Verb { - str := func(description string) map[string]any { - return map[string]any{"type": "string", "description": description} - } - return []mcat.Verb{ - {Name: mcat.EnrolmentVerbIssue, Description: "Issue a one-time token for a new machine, on the operator's Approve: " + - "refused at once, naming every finding, for a name that is not a node name or that a node record, an assignments " + - "key or an open enrolment holds, and for every call before the controller's lease is handed away. Otherwise " + - "reserves the lowest free private-network address, asks the operator under the seat's own name and, on the " + - "warrant, adds the node to the assignments with its tunnel key, asks the vault for the token and says enrolment-issued. " + - "The token itself is read only through tokens.", - Input: schema(map[string]string{ - "node": "the new machine's name in the mesh", - "why": "why it joins, shown to the operator with the ask", - "expires": "how long the token lasts, a duration such as 1h or 24h: one hour when not given, at most 24 hours", - // The operator's decision of 2026-11-02 01:05: the tunnel key is given at issue, so on the Approve the node - // enters the assignments with it and the hub peers it before redeem. - "overlay_key": "the new machine's private network (tunnel) public key, as `nox-mesh-host key` prints it", - }, []string{"node", "why", "overlay_key"})}, - {Name: mcat.EnrolmentVerbRedeem, 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 the assignments, 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: map[string]any{"type": "object", "additionalProperties": false, - "required": []string{"node", "public_key", "sealing_key", "proof"}, - "properties": map[string]any{ - "node": str("the node the token was issued for"), - "public_key": str("the identity key's public half, base64"), - "overlay_key": str("the private network key's public half"), - "sealing_key": str("the sealing key's public half, which the account is sealed to"), - "serving_key": str("the serving key's public half, which the certificate binds"), - "profile": map[string]any{"type": "object", "description": "what the machine is, as its record says it"}, - "tunnel": map[string]any{"type": "object", "description": "a tunnel found on the machine: everything but its private key"}, - "proof": str("the identity key's signature over the redeem's proof bytes (delivery.RedeemProof), base64"), - }}}, - {Name: mcat.EnrolmentVerbConfirm, Description: "Confirm an enrolment: asked by the node-engine dialled as its own " + - "node login alone, the runtime's proof of that caller being the confirmation. Says node-enrolled and ends the " + - "enrolment.", - Input: schema(map[string]string{}, nil)}, - {Name: mcat.EnrolmentVerbRevoke, Description: "End an enrolment at any stage, with why, without an Approve: withdraws " + - "an open ask, deletes the node's key and its address, has the vault retire an account it already minted for " + - "it, records the act as a hand act and says enrolment-revoked.", - Input: schema(map[string]string{"node": "the node", "why": "why, recorded with the hand act"}, - []string{"node", "why"})}, - {Name: mcat.EnrolmentVerbTokens, Description: "Read every open enrolment, or one: its stage, expiry, ask and revision, " + - "the bucket's fill against its cap. For a node at live, its " + - "token sealed to the operator key, asked of the vault at that moment and relayed as the vault sealed it. With " + - "history, the ended enrolments from the seat's events, as far back as the events are kept.", - Input: schema(map[string]string{"node": "one node", "history": "\"true\": the ended enrolments too"}, - nil, "history")}, - {Name: mcat.EnrolmentVerbAgain, Description: "Raise an issue's ask again when the operator's channel does not hold it, " + - "and nothing more.", - Input: schema(map[string]string{"node": "the node whose ask is raised again"}, []string{"node"})}, - } -} diff --git a/internal/compose/resolution/node_resolver.go b/internal/compose/resolution/node_resolver.go index ef796db..2016026 100644 --- a/internal/compose/resolution/node_resolver.go +++ b/internal/compose/resolution/node_resolver.go @@ -6,36 +6,6 @@ import ( mcat "git.novox.be/novox/mesh-sdk/go/catalogue" ) -// resolverVerbs is the contract every holder of node-resolver serves (novox/hq ADR 0247): what is routed -// where, route a set of domains, and take a route away. The same verbs are served on the machine itself to -// the modules there, so what a VPN pushed never crosses the bus to be routed; on the mesh they are the -// operator's way to read and correct the same. -func resolverVerbs() []mcat.Verb { - return []mcat.Verb{ - {Name: "routes", Description: "What this machine's own resolver sends where: the mesh's resolvers, " + - "which answer every name not routed elsewhere, and each link given servers of its own with the " + - "domains routed to them. Also the resolver file's outside writes it kept, newest first: when, who " + - "wrote it as far as the file says, whether a module took it, and when the resolver's own file was " + - "put back.", - Input: schema(map[string]string{}, nil), - Replaces: []string{"resolvectl status", "resolvectl dns", "resolvectl domain"}}, - {Name: "route", Description: "Send these domains, and every name under them, to these servers over " + - "this link — and only them: the link is never the machine's default route for names, and the " + - "mesh's own domain is refused. Replaces whatever the link was given before. A link that goes away " + - "takes its route with it.", - Input: schema(map[string]string{ - "link": "the network link the servers are reached over, by name (a VPN's tunnel interface)", - "domains": "the domains to route there, separated by spaces or commas", - "servers": "the servers' addresses, separated by spaces or commas", - }, []string{"link", "domains", "servers"}), - Replaces: []string{"resolvectl dns", "resolvectl domain", "resolvectl default-route"}}, - {Name: "unroute", Description: "Take one link's route away: its domains go back to the mesh's " + - "resolvers. Nothing changes when the link has none.", - Input: schema(map[string]string{"link": "the network link, by name"}, []string{"link"}), - Replaces: []string{"resolvectl revert"}}, - } -} - // holdsSeat says whether a module claims a seat, by its current name. func holdsSeat(m mcat.Manifest, seat string) bool { for _, c := range m.Claims { diff --git a/internal/compose/resolution/seats.go b/internal/compose/resolution/seats.go index 8813719..b29c3d8 100644 --- a/internal/compose/resolution/seats.go +++ b/internal/compose/resolution/seats.go @@ -4,21 +4,6 @@ import ( mcat "git.novox.be/novox/mesh-sdk/go/catalogue" ) -// configInput is the schema of mesh-config's verb: a value of the key's own type (a number for a number, never its -// text), and a limit that is a number; every other argument a string. -func configInput() map[string]any { - in := schema(map[string]string{"action": "list (when not given), set, clear or history", - "key": "the config key", "value": "for set: the value, of the key's type", - "module": "a module: its own value, with node", "node": "a node", - "why": "for set and clear: why", "by": "who gave the value at the controller's terminal", - "limit": "for history: at most this many, newest"}, nil) - props := in["properties"].(map[string]any) - props["value"] = map[string]any{"description": "for set: the value, of the key's type: a number, a string, " + - "a boolean, or for a trusted key an object or a list"} - props["limit"] = map[string]any{"type": "number", "description": "for history: at most this many, newest"} - return in -} - func providesAt(m mcat.Manifest, provision, scope string) bool { for _, o := range m.Provides { if o.Name == provision && o.At() == scope { diff --git a/internal/compose/resolution/seats_test.go b/internal/compose/resolution/seats_test.go index 4a72341..73a0c89 100644 --- a/internal/compose/resolution/seats_test.go +++ b/internal/compose/resolution/seats_test.go @@ -1,6 +1,12 @@ package resolution import ( + "go/ast" + "go/parser" + "go/token" + "io/fs" + "path/filepath" + "strconv" "strings" "testing" @@ -70,4 +76,92 @@ func TestTheHolderIsTheModuleNotTheMachine(t *testing.T) { t.Fatalf("the holder was not told apart from a neighbour: %+v", holder) } + // A seat's verbs are its definition's, read from the bus (novox/hq ADR 0635 §1): the node-engine keeps no copy of + // them, so no copy can drift from the seat as the seven it kept did (issue 1921). A copy is a list of the catalogue's + // verbs written in the code; the guard proves it finds one, then finds none in the node-engine. + t.Run("no seat's verbs are copied into the node-engine", func(t *testing.T) { + copied := `package x + +import mcat "git.novox.be/novox/mesh-sdk/go/catalogue" + +func someVerbs() []mcat.Verb { return []mcat.Verb{{Name: "routes"}} } +` + if got := verbCopies(t, "copied.go", copied); len(got) == 0 { + t.Fatal("the guard finds no copy in a file that holds one, so it proves nothing") + } + var found []string + root := filepath.Join("..", "..", "..") + err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() && (d.Name() == "testdata" || strings.HasPrefix(d.Name(), ".")) && p != root { + return filepath.SkipDir + } + if d.IsDir() || !strings.HasSuffix(p, ".go") || strings.HasSuffix(p, "_test.go") { + return nil + } + found = append(found, verbCopies(t, p, nil)...) + return nil + }) + if err != nil { + t.Fatal(err) + } + if len(found) > 0 { + t.Errorf("the node-engine copies a seat's verbs instead of reading the seat list from the bus: %v", found) + } + }) +} + +// verbCopies is where one Go file writes the catalogue's verbs itself: a function answering a list of them, or a verb +// written as a literal. +func verbCopies(t *testing.T, name string, src any) []string { + t.Helper() + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, name, src, parser.SkipObjectResolution) + if err != nil { + t.Fatal(err) + } + alias := "" + for _, imp := range f.Imports { + if p, _ := strconv.Unquote(imp.Path.Value); p == "git.novox.be/novox/mesh-sdk/go/catalogue" { + alias = "catalogue" + if imp.Name != nil { + alias = imp.Name.Name + } + } + } + if alias == "" { + return nil + } + isVerb := func(e ast.Expr) bool { + if a, ok := e.(*ast.ArrayType); ok { + e = a.Elt + } + sel, ok := e.(*ast.SelectorExpr) + if !ok || sel.Sel.Name != "Verb" { + return false + } + x, ok := sel.X.(*ast.Ident) + return ok && x.Name == alias + } + var out []string + ast.Inspect(f, func(n ast.Node) bool { + switch n := n.(type) { + case *ast.FuncDecl: + if n.Type.Results != nil { + for _, r := range n.Type.Results.List { + if isVerb(r.Type) { + out = append(out, fset.Position(n.Pos()).String()+" "+n.Name.Name) + } + } + } + case *ast.CompositeLit: + if n.Type != nil && isVerb(n.Type) { + out = append(out, fset.Position(n.Pos()).String()) + } + } + return true + }) + return out } diff --git a/internal/compose/resolution/shares.go b/internal/compose/resolution/shares.go deleted file mode 100644 index 7616c19..0000000 --- a/internal/compose/resolution/shares.go +++ /dev/null @@ -1,78 +0,0 @@ -package resolution - -import ( - mcat "git.novox.be/novox/mesh-sdk/go/catalogue" -) - -// nfsServerVerbs is the contract every holder of node-nfs-server serves (novox/hq ADR 0263). -func nfsServerVerbs() []mcat.Verb { - return []mcat.Verb{ - {Name: "exports", Description: "Every share this machine exports: its name, its path, read-write or " + - "read-only, the owner every client is mapped to (uid and gid), each node it is exported to with that " + - "node's private address and access (only the nodes the server grants it to and that ask for it), " + - "what is granted and not asked for or asked for and not granted, and whether the kernel holds it " + - "now. Also the exports found that are not the mesh's: a dataset's sharenfs property, a line in " + - "/etc/exports.", - Input: schema(map[string]string{}, nil), - Replaces: []string{"exportfs -v", "cat /etc/exports", "zfs get sharenfs"}}, - {Name: "clients", Description: "Which machines have mounted which share now, as the NFS server " + - "knows its clients.", - Input: schema(map[string]string{}, nil), - Replaces: []string{"ss -tn sport = :2049", "cat /proc/fs/nfsd/clients/*/info"}}, - {Name: "test", Description: "Whether this machine exports one share for the mesh now, and whether its " + - "NFS service is up; with an address, also whether the share is exported to that address: a node's " + - "own private address, when the server grants it the share and the node asks for it. What a machine " + - "mounting the share asks before it mounts.", - Input: schema(map[string]string{ - "share": "the share, by its name", - "address": "a node's private address, to ask whether the share is exported to it (optional)", - }, []string{"share"}), - Replaces: []string{"showmount -e"}}, - {Name: "reload", Description: "Have the kernel read this machine's export files again now (exportfs " + - "-ra) and answer the shares as it then holds them. The module's own process writes its export " + - "file; this is for after a change made outside it. Changes no shared path.", - Input: schema(map[string]string{}, nil), - Replaces: []string{"exportfs -ra"}}, - {Name: "adopt", Description: "Take over an export a person made by hand: clear a dataset's sharenfs " + - "property for a path the mesh's own export now serves — only when that export is live. Without " + - "confirm, says what it would do and changes nothing.", - Input: schema(map[string]string{ - "path": "the shared path whose hand-made export is taken over", - "confirm": "\"true\": change it (needs why); anything else is a dry run", - "why": "why, for the record", - }, []string{"path"}, "confirm"), - Replaces: []string{"zfs set sharenfs=off"}}, - } -} - -// mountsVerbs is the contract every holder of node-mounts serves (novox/hq ADR 0263). -func mountsVerbs() []mcat.Verb { - return []mcat.Verb{ - {Name: "list", Description: "Every mount point on this machine: its fstab line, the mesh's mount and " + - "automount units for it, its state (armed, mounted, absent, unreachable, failed) and since when, " + - "and which settings asked for it. A password in a mount's options is never shown.", - Input: schema(map[string]string{}, nil), - Replaces: []string{"cat /etc/fstab", "findmnt", "systemctl list-units --type=mount"}}, - {Name: "test", Description: "Whether one share's or occasional source's server answers from this " + - "machine now, without touching its mount point.", - Input: schema(map[string]string{"name": "the share or source, by its name"}, []string{"name"}), - Replaces: []string{"showmount -e", "ping"}}, - {Name: "mount", Description: "Mount one share or occasional source now, rather than at its first " + - "access.", - Input: schema(map[string]string{"name": "the share or source, by its name"}, []string{"name"}), - Replaces: []string{"mount"}}, - {Name: "unmount", Description: "Release one share or occasional source now; its automount stays " + - "armed, so the next access mounts it again.", - Input: schema(map[string]string{"name": "the share or source, by its name"}, []string{"name"}), - Replaces: []string{"umount"}}, - {Name: "adopt", Description: "Take over a mount a person wrote by hand: comment out the /etc/fstab " + - "line for a mount point the mesh's own units now serve, keeping a copy of the file — only when " + - "the mesh's automount for it is armed. Without confirm, says what it would do and changes nothing.", - Input: schema(map[string]string{ - "mountpoint": "the mount point whose fstab line is taken over", - "confirm": "\"true\": change it (needs why); anything else is a dry run", - "why": "why, for the record", - }, []string{"mountpoint"}, "confirm"), - Replaces: []string{"sed -i /etc/fstab"}}, - } -} diff --git a/internal/compose/resolution/verbs.go b/internal/compose/resolution/verbs.go deleted file mode 100644 index a57913e..0000000 --- a/internal/compose/resolution/verbs.go +++ /dev/null @@ -1,59 +0,0 @@ -package resolution - -import ( - "maps" - "slices" -) - -// schema is a JSON schema for an object of string properties, which is every argument the verbs -// above take. Kept small on purpose: a schema an agent cannot read is a tool it cannot call. The -// switches are the properties that are "true" or "false" and nothing else, said in the schema as an -// enum so a caller sees it and the verb can refuse any other word rather than read it as false. -// -// **The schema is the whole of what a verb takes** (novox/hq issue 244): an argument it does not -// name is refused when the verb is called, never passed over. -// withObject adds to a verb's input s the property name, an object of the string fields given, each required. -func withObject(s map[string]any, name, description string, fields map[string]string) map[string]any { - inner := schema(fields, slices.Sorted(maps.Keys(fields))) - inner["description"], inner["additionalProperties"] = description, false - s["properties"].(map[string]any)[name] = inner - return s -} - -func schema(properties map[string]string, required []string, switches ...string) map[string]any { - isSwitch := map[string]bool{} - for _, s := range switches { - if _, declared := properties[s]; !declared { - panic("a switch that is not a property: " + s) - } - isSwitch[s] = true - } - props := map[string]any{} - for name, description := range properties { - p := map[string]any{"type": "string", "description": description} - if isSwitch[name] { - p["enum"] = []string{"true", "false"} - } - props[name] = p - } - out := map[string]any{"type": "object", "properties": props} - if len(required) > 0 { - out["required"] = required - } - return out -} - -// listed makes the named properties of a schema lists of text: a caller gives them as a JSON array (or, as -// any argument, one text separated by commas). -func listed(in map[string]any, names ...string) map[string]any { - props, _ := in["properties"].(map[string]any) - for _, n := range names { - p, _ := props[n].(map[string]any) - if p == nil { - panic("a list that is not a property: " + n) - } - props[n] = map[string]any{"type": "array", "items": map[string]any{"type": "string"}, - "description": p["description"]} - } - return in -}