nox calls the seats that own each job, not the unassigned controller

The controller nox forwarded every line to is unassigned (issue 1126), so nox
answered nothing but enrol open. Each line now calls the seat verb or module
tool that owns its job through this machine's console (mesh_call); a line no
seat owns says so and names what to use, never failing silently.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
2026-11-02 10:41:32 +01:00
parent db104bdb08
commit 5cdf8295f3
7 changed files with 1277 additions and 226 deletions
+714
View File
@@ -0,0 +1,714 @@
package main
// Every command line nox takes, and the seat verb or module tool that owns its job now that the controller is
// unassigned (novox/hq issue 1126; the map is hq's .work/1126/nox-map.md). A line no seat or module owns says so and
// names what to use instead: it never fails silently, and it never pretends to have run.
import (
"encoding/json"
"fmt"
"sort"
"strconv"
"strings"
"time"
)
// line is a command line's words after the command's own: positionals and --flags, in any order.
type line struct {
pos []string
flags map[string]string
}
// boolFlags are the flags that take no value.
var boolFlags = map[string]bool{"json": true, "confirm": true, "collected": true, "brief": true, "all": true,
"reasoning": true, "clear": true}
func parseLine(args []string) (line, error) {
l := line{flags: map[string]string{}}
for i := 0; i < len(args); i++ {
a := args[i]
if !strings.HasPrefix(a, "--") || a == "--" {
l.pos = append(l.pos, a)
continue
}
name, value, has := strings.Cut(strings.TrimPrefix(a, "--"), "=")
if boolFlags[name] {
if has {
return l, usageErr("--%s takes no value", name)
}
l.flags[name] = "true"
continue
}
if !has {
if i+1 >= len(args) {
return l, usageErr("--%s needs a value", name)
}
i++
value = args[i]
}
l.flags[name] = value
}
return l, nil
}
func (l line) has(flag string) bool { return l.flags[flag] != "" }
// usageError is a line nox cannot read: nothing was called.
type usageError struct{ msg string }
func (u usageError) Error() string { return u.msg }
func usageErr(format string, a ...any) error { return usageError{fmt.Sprintf(format, a...)} }
// call is what one line asks: an address and its arguments.
type call struct {
Address string
Args map[string]any
}
// command is one of nox's lines.
type command struct {
words []string
usage string
does string
// build reads the line and answers the call; nil for a line no seat owns.
build func(l line) (call, error)
// allowed are the flags the line reads; any other is refused before anything is called.
allowed []string
// instead says, for a line with no owner, what to use; empty for one that is owned.
instead string
// render shapes a successful answer for reading; nil prints it as the console gave it.
render func(text string) string
}
// gone is the reason every unowned line gives.
const gone = "this was the controller's, and the controller is unassigned (novox/hq issue 1126). Nothing was called"
// commands is every line, longest words first within a word.
var commands = []command{
// What runs where.
{words: []string{"status"}, does: "what is wrong, what is quiet, what is moving (the delivery view)",
build: fixed("mesh-delivery-view.view_status")},
{words: []string{"nodes"}, does: "the nodes placement holds", build: fixed("mesh-placement.placement"),
render: renderNodes},
{words: []string{"node", "list"}, does: "the nodes placement holds", build: fixed("mesh-placement.placement"),
render: renderNodes},
{words: []string{"node", "show"}, usage: "<node>", does: "one node's placement: what it runs and its decided fields",
build: nodeArg("mesh-placement.placement")},
{words: []string{"node", "public-domain"}, usage: "<node> [<domain> | --clear] [--why <text>]",
does: "the domain a node composes its routed names under; with a domain or --clear, proposed to placement",
allowed: []string{"clear", "why"}, build: publicDomain},
{words: []string{"node", "add"}, instead: "a new machine joins through the seat mesh-enrolment: nox enrol issue <node> --why <text>"},
{words: []string{"node", "networks"}, instead: "no seat keeps a node's routed networks yet; mesh-placement.placement shows what a node decides"},
{words: []string{"plan"}, usage: "<node>", does: "what the assigner wants the node to run", build: optNode("mesh-delivery-view.assigner_wants")},
{words: []string{"haves"}, usage: "[<node>]", does: "what each node runs, as its node-engine said", build: optNode("mesh-delivery-view.node_haves")},
{words: []string{"switch"}, does: "what each node runs, as its node-engine said", build: fixed("mesh-delivery-view.node_haves")},
{words: []string{"composition"}, usage: "<node> <module>", does: "one module's composition on a node beside what runs",
build: func(l line) (call, error) {
if len(l.pos) != 2 {
return call{}, usageErr("give the node and the module")
}
return call{l.pos[0] + "/node-engine.composition", map[string]any{"module": l.pos[1]}}, nil
}},
{words: []string{"seats"}, does: "who holds each seat, and where", build: func(line) (call, error) {
return call{"mesh-placement.placement", map[string]any{"seats": "true"}}, nil
}},
{words: []string{"seat"}, instead: "a seat is held by placing its module: nox assign <node> <module> --why <text> (mesh-placement.propose)"},
{words: []string{"assign"}, usage: "<node> <module>... --why <text>", does: "propose placing modules on a node",
allowed: []string{"why"}, build: placeModules("place")},
{words: []string{"unassign"}, usage: "<node> <module>... --why <text>", does: "propose taking modules off a node",
allowed: []string{"why"}, build: placeModules("take-off")},
{words: []string{"pin"}, usage: "<node> <provision> <from-node> <module> --why <text>", does: "propose which provider a node gets a provision from",
allowed: []string{"why"}, build: func(l line) (call, error) {
if len(l.pos) != 4 {
return call{}, usageErr("give the node, the provision, the provider's node and its module")
}
return propose(l, map[string]any{"op": "pin", "node": l.pos[0], "provision": l.pos[1],
"provider": map[string]any{"node": l.pos[2], "module": l.pos[3]}})
}},
{words: []string{"unpin"}, usage: "<node> <provision> --why <text>", does: "propose putting that question back",
allowed: []string{"why"}, build: func(l line) (call, error) {
if len(l.pos) != 2 {
return call{}, usageErr("give the node and the provision")
}
return propose(l, map[string]any{"op": "unpin", "node": l.pos[0], "provision": l.pos[1]})
}},
{words: []string{"proposals"}, usage: "[<id>]", does: "the open proposals of placement", build: optArg("mesh-placement.proposals", "id")},
{words: []string{"overlay", "show"}, does: "every node's placement, with its tunnel", build: fixed("mesh-placement.placement")},
{words: []string{"overlay"}, instead: "a node's tunnel is a decided field of its placement: mesh-placement.propose with op decide"},
// Deliveries and builds.
{words: []string{"deliveries"}, usage: "[--state s] [--repository owner/repo] [--group g] [--all]",
does: "every delivery not final, and those that ended in the last day",
allowed: []string{"state", "repository", "group", "all"}, build: func(l line) (call, error) {
return call{"mesh-delivery.deliveries", pick(l, "state", "repository", "group", "all")}, nil
}},
{words: []string{"delivery"}, usage: "<id>", does: "one delivery or group whole", build: needArg("mesh-delivery.show", "id")},
{words: []string{"groups"}, usage: "[--all]", does: "every delivery group", allowed: []string{"all"},
build: func(l line) (call, error) { return call{"mesh-delivery.groups", pick(l, "all")}, nil }},
{words: []string{"stalled"}, does: "every delivery held past its state's bound", build: fixed("mesh-delivery.stalled")},
{words: []string{"checks"}, usage: "<owner/repo> [--number n | --commit c]", does: "what the mesh's checks said of a head",
allowed: []string{"number", "commit"}, build: func(l line) (call, error) {
if len(l.pos) != 1 {
return call{}, usageErr("give the repository, owner/repo")
}
a := pick(l, "number", "commit")
a["repository"] = l.pos[0]
return call{"mesh-delivery.checks", a}, nil
}},
{words: []string{"try"}, usage: "<owner/repo> <head> [--level l] [--trunk t] [--tests t]",
does: "try a pushed head before its pull request is opened", allowed: []string{"level", "trunk", "tests"},
build: func(l line) (call, error) {
if len(l.pos) != 2 {
return call{}, usageErr("give the repository and the head")
}
a := pick(l, "level", "trunk", "tests")
a["repository"], a["head"] = l.pos[0], l.pos[1]
return call{"mesh-delivery-checker.try", a}, nil
}},
{words: []string{"builds"}, usage: "[--since <RFC 3339 time>]", does: "every build that ended since (the last day by default)",
allowed: []string{"since"}, build: func(l line) (call, error) {
since := l.flags["since"]
if since == "" {
since = now().Add(-24 * time.Hour).UTC().Format(time.RFC3339)
}
return call{"mesh-delivery-builder.builder_builds_since", map[string]any{"since": since}}, nil
}},
{words: []string{"upgrade"}, usage: "[<module>] [--policy roll|together|record|default --why <text>]",
does: "whether each module's new builds wait for a person; with --policy, a person's choice",
allowed: []string{"policy", "why"}, build: func(l line) (call, error) {
if len(l.pos) > 1 {
return call{}, usageErr("give at most one module; rolling out is the delivery's: nox deliveries")
}
a := pick(l, "policy", "why")
if len(l.pos) == 1 {
a["module"] = l.pos[0]
}
if a["policy"] != nil && (a["module"] == nil || a["why"] == nil) {
return call{}, usageErr("a policy needs the module and --why")
}
return call{"mesh-wants.upgrade", a}, nil
}},
{words: []string{"bus"}, does: "what a bus upgrade would do", build: fixed("mesh-wants.bus")},
// Conditions, the self-check, the healers and hand acts.
{words: []string{"conditions", "show"}, usage: "<key>", does: "one condition whole", build: needArg("mesh-conditions.conditions", "key")},
{words: []string{"conditions", "silence"}, usage: "<key> --for <30m|4h|2d> --why <text> [--cause <word>]",
does: "send no message for it a while; a hand act", allowed: []string{"for", "why", "cause"},
build: func(l line) (call, error) {
if len(l.pos) != 1 || !l.has("for") || !l.has("why") {
return call{}, usageErr("give the key, --for and --why")
}
a := pick(l, "for", "why", "cause")
a["key"] = l.pos[0]
return call{"mesh-conditions.silence", a}, nil
}},
{words: []string{"conditions", "history"}, usage: "[--days N] [--key K] [--limit N]", does: "every raising, change and clearing lately",
allowed: []string{"days", "key", "limit"}, build: func(l line) (call, error) {
if l.has("key") {
return call{"mesh-conditions.conditions_history", pick(l, "key", "limit")}, nil
}
a := pick(l, "days")
a["history"] = "true"
return call{"mesh-conditions.conditions", a}, nil
}},
{words: []string{"conditions"}, usage: "[--scope S] [--severity S] [--machine M]", does: "what is wrong now, urgent first",
allowed: []string{"scope", "severity", "machine"}, build: func(l line) (call, error) {
if len(l.pos) > 0 {
return call{}, usageErr("%q is not a conditions line", l.pos[0])
}
return call{"mesh-conditions.conditions", pick(l, "scope", "severity", "machine")}, nil
}},
{words: []string{"doctor", "probes"}, does: "the self-check's probes", build: fixed("mesh-self-check.probes")},
{words: []string{"doctor", "run"}, does: "run every probe now", build: func(line) (call, error) {
return call{"mesh-self-check.doctor", map[string]any{"run": true}}, nil
}},
{words: []string{"doctor", "probe"}, usage: "<id>", does: "one probe's verdict whole", build: needArg("mesh-self-check.doctor", "probe")},
{words: []string{"doctor", "signals"}, does: "the watchman's signals and their ages", build: fixed("mesh-watchdogs.signals")},
{words: []string{"doctor"}, does: "the self-check's last verdict", build: fixed("mesh-self-check.doctor")},
{words: []string{"healers"}, usage: "[--days N]", does: "the healers, what they did lately, and their brake",
allowed: []string{"days"}, build: func(l line) (call, error) {
a, err := numbers(pick(l, "days"), "days")
return call{"mesh-healers.healers", a}, err
}},
{words: []string{"hand-act", "record"}, usage: "<what> --why <text> [--cause <word>] [--condition <key>]",
does: "record an act done by hand", allowed: []string{"why", "cause", "condition"},
build: func(l line) (call, error) {
if len(l.pos) == 0 || !l.has("why") {
return call{}, usageErr("give what was done and --why")
}
a := pick(l, "why", "cause", "condition")
a["what"] = strings.Join(l.pos, " ")
return call{"mesh-hand-acts.hand-act", a}, nil
}},
{words: []string{"hand-acts"}, usage: "[--days N] [--cause c] [--who w]", does: "what was done by hand lately",
allowed: []string{"days", "cause", "who"}, build: func(l line) (call, error) {
a, err := numbers(pick(l, "days", "cause", "who"), "days")
return call{"mesh-hand-acts.hand-acts", a}, err
}},
{words: []string{"stuck"}, does: "what stopped moving, as the watchman holds it", build: fixed("mesh-watchdogs.stuck")},
{words: []string{"dead-letters"}, usage: "[--consumer c]", does: "what a consumer gave up on", allowed: []string{"consumer"},
build: func(l line) (call, error) { return call{"mesh-watchdogs.dead-letters", pick(l, "consumer")}, nil }},
// Config, secrets, the vault, artifacts, licences, modules.
{words: []string{"settings", "show"}, usage: "<module> [--node n]", does: "a module's config values",
allowed: []string{"node"}, build: func(l line) (call, error) {
if len(l.pos) != 1 {
return call{}, usageErr("give the module")
}
a := pick(l, "node")
a["action"], a["module"] = "list", l.pos[0]
return call{"mesh-config.config", a}, nil
}},
{words: []string{"settings", "set"}, usage: "<module> <key> <value> --node n --why <text>",
does: "set one of a module's config values on one node", allowed: []string{"node", "why"},
build: func(l line) (call, error) { return configChange(l, "set") }},
{words: []string{"settings", "clear"}, usage: "<module> <key> --node n --why <text>",
does: "clear one of a module's config values on one node", allowed: []string{"node", "why"},
build: func(l line) (call, error) { return configChange(l, "clear") }},
{words: []string{"config"}, usage: "[list|set|clear|history|propose|proposals] [--module m] [--node n] [--key k] [--value v] [--why w]",
does: "the mesh's config values, at any level", allowed: []string{"module", "node", "key", "value", "why", "limit", "proposal", "pull", "clear"},
build: func(l line) (call, error) {
if len(l.pos) > 1 {
return call{}, usageErr("give at most one action")
}
a := pick(l, "module", "node", "key", "why", "proposal", "pull", "clear", "limit")
if len(l.pos) == 1 {
a["action"] = l.pos[0]
}
if v, ok := l.flags["value"]; ok {
a["value"] = jsonOrText(v)
}
if c, ok := a["clear"]; ok {
a["clear"] = c == "true"
}
a, err := numbers(a, "limit")
return call{"mesh-config.config", a}, err
}},
{words: []string{"secret", "export"}, does: "every secret the vault holds, as its copy sealed to the operator key",
build: fixed("mesh-vault.secret_vault_export")},
{words: []string{"secret", "accept"}, instead: "mesh-vault.accept takes a value from the controller alone, and no seat takes one from a person yet; a module's own secret is asked at a desk (mesh-vault.secret-ask)"},
{words: []string{"secret", "recover"}, instead: "opening a sealed copy locally is not built yet (beside nox enrol open); nox secret export gives the sealed copies"},
{words: []string{"secret"}, instead: "nox secret export; mesh-vault.rotate makes one anew"},
{words: []string{"operator", "key", "show"}, does: "what needs a person's eye in the vault, the operator key among it", build: fixed("mesh-vault.vault_status")},
{words: []string{"operator"}, instead: "no seat sets or makes the operator key yet; nox operator key show reads it (mesh-vault.vault_status)"},
{words: []string{"rotate"}, usage: "[--node n --module m --name s | --provider n --provision p] --why <text>",
does: "make a secret anew", allowed: []string{"node", "module", "name", "provider", "provision", "consumer", "consumer_module", "why"},
build: func(l line) (call, error) {
if len(l.pos) == 1 {
l.flags["provision"] = l.pos[0]
} else if len(l.pos) > 1 {
return call{}, usageErr("give the secret by its flags")
}
if !l.has("why") {
return call{}, usageErr("give --why")
}
return call{"mesh-vault.rotate", pick(l, "node", "module", "name", "provider", "provision", "consumer", "consumer_module", "why")}, nil
}},
{words: []string{"identity", "show"}, does: "the vault's signing key and authority", build: fixed("mesh-vault.vault_signers")},
{words: []string{"retire", "approve"}, usage: "<consumer>... --why <text>", does: "retire the set waiting for a person",
allowed: []string{"why"}, build: retirement("mesh-vault.provisioner_retire_approve")},
{words: []string{"retire", "reject"}, usage: "<consumer>... --why <text>", does: "keep the set waiting for a person active",
allowed: []string{"why"}, build: retirement("mesh-vault.provisioner_retire_reject")},
{words: []string{"retire"}, does: "every retirement waiting for a person", build: fixed("mesh-vault.provisioner_retirement")},
{words: []string{"cleanup", "delete"}, usage: "<consumer> --why <text> --confirm", does: "delete one retired holder from the ledger",
allowed: []string{"why", "confirm"}, build: func(l line) (call, error) {
if len(l.pos) != 1 || !l.has("why") {
return call{}, usageErr("give the consumer and --why")
}
a := pick(l, "why", "confirm")
a["consumer"] = l.pos[0]
return call{"mesh-vault.provisioner_delete", a}, nil
}},
{words: []string{"cleanup"}, does: "every retired holder per provider", build: fixed("mesh-vault.provisioner_retirement")},
{words: []string{"artifacts"}, usage: "[--collected]", does: "every artifact the mesh made: kept and why, or eligible",
allowed: []string{"collected"}, build: func(l line) (call, error) {
return call{"mesh-artifact-sweep.artifacts", pick(l, "collected")}, nil
}},
{words: []string{"collection"}, does: "every artifact the mesh made: kept and why, or eligible", build: fixed("mesh-artifact-sweep.artifacts")},
{words: []string{"collect"}, usage: "[--confirm --why <text>] [--most n]", does: "the sweep: a dry run, or with --confirm the sweep now",
allowed: []string{"confirm", "why", "most"}, build: func(l line) (call, error) {
if l.has("confirm") && !l.has("why") {
return call{}, usageErr("--confirm needs --why")
}
return call{"mesh-artifact-sweep.collect", pick(l, "confirm", "why", "most")}, nil
}},
{words: []string{"images"}, usage: "<node>", does: "every container image a machine's wants and haves name", build: needArg("mesh-artifact-sweep.images", "node")},
{words: []string{"mirrors"}, does: "the bases the mesh copied into its store", build: fixed("mesh-artifact-sweep.mirrors")},
{words: []string{"licence", "list"}, does: "every licence the manager holds", build: fixed("anthropic-licence-manager.licences")},
{words: []string{"licence", "use"}, usage: "<consumer> <licence>", does: "move a node's agent to another licence",
build: func(l line) (call, error) {
if len(l.pos) != 2 {
return call{}, usageErr("give the consumer (a node) and the licence")
}
return call{"anthropic-licence-manager.switch", map[string]any{"consumer": l.pos[0], "licence": l.pos[1]}}, nil
}},
{words: []string{"licence", "refresh"}, usage: "[<licence>]", does: "refresh a licence now, or every due one", build: optArg("anthropic-licence-manager.refresh", "licence")},
{words: []string{"licence", "key"}, does: "the manager's public key", build: fixed("anthropic-licence-manager.public-key")},
{words: []string{"licence"}, instead: "anthropic-licence-manager.adopt takes an API key from a file on the manager's node; nox licence list|use|refresh|key"},
{words: []string{"module", "list"}, does: "every module the indexer holds", build: fixed("mesh-delivery-view.indexer_modules")},
{words: []string{"module", "show"}, usage: "<module>", does: "what the indexer holds of one module", build: needArg("mesh-delivery-view.indexer_module", "module")},
{words: []string{"module", "moved"}, usage: "<module> --why <text>", does: "take a registered module in now", allowed: []string{"why"},
build: func(l line) (call, error) {
if len(l.pos) < 1 {
return call{}, usageErr("give the module")
}
return call{"mesh-delivery-indexer.indexer_take_in", map[string]any{"module": l.pos[0], "why": l.flags["why"]}}, nil
}},
{words: []string{"module"}, instead: "the indexer takes modules in from their repositories; nox module list|show|moved, and nothing forgets one by hand yet"},
{words: []string{"register"}, instead: "no seat registers a module from another repository yet (novox/hq ADR 0501 §4); nox module moved <module> takes a registered one in"},
{words: []string{"account", "issue"}, usage: "<user>", does: "mint one bus account the grants part wants, now", build: needArg("mesh-vault.issue-account", "user")},
// Issues and records.
{words: []string{"issues"}, usage: "[<words>] [--status s] [--limit n] [--owner o]", does: "the issues, newest first, or those that match",
allowed: []string{"status", "limit", "owner", "severity", "needs"}, build: func(l line) (call, error) {
a := pick(l, "status", "owner", "severity", "needs", "limit")
if len(l.pos) > 0 {
a["search"] = strings.Join(l.pos, " ")
}
a, err := numbers(a, "limit")
return call{"issue-tracker.list", a}, err
}},
{words: []string{"issue"}, usage: "<number> [--brief]", does: "one issue whole", allowed: []string{"brief"},
build: func(l line) (call, error) {
if len(l.pos) != 1 {
return call{}, usageErr("give the issue's number")
}
n, err := strconv.Atoi(l.pos[0])
if err != nil || n < 1 {
return call{}, usageErr("%q is not an issue's number", l.pos[0])
}
a := map[string]any{"number": n}
if l.has("brief") {
a["brief"] = true
}
return call{"issue-tracker.get", a}, nil
}},
{words: []string{"kb", "ask"}, usage: "<question>", does: "what holds about something, and why", build: func(l line) (call, error) {
if len(l.pos) == 0 {
return call{}, usageErr("give the question")
}
return call{"knowledge-base.ask", map[string]any{"question": strings.Join(l.pos, " ")}}, nil
}},
{words: []string{"kb", "get"}, usage: "<number> [--kind research] [--reasoning]", does: "one record by its number",
allowed: []string{"kind", "reasoning"}, build: func(l line) (call, error) {
if len(l.pos) != 1 {
return call{}, usageErr("give the record's number")
}
n, err := strconv.Atoi(l.pos[0])
if err != nil {
return call{}, usageErr("%q is not a record's number", l.pos[0])
}
a := pick(l, "kind")
a["number"] = n
if l.has("reasoning") {
a["reasoning"] = true
}
return call{"knowledge-base.get", a}, nil
}},
{words: []string{"progress"}, usage: "[<design>|mesh] [--detail minimal|detailed]", does: "how far the designs have come",
allowed: []string{"detail", "since"}, build: func(l line) (call, error) {
a := pick(l, "detail", "since")
a["of"] = "mesh"
if len(l.pos) == 1 {
a["of"] = l.pos[0]
if _, err := strconv.Atoi(l.pos[0]); err == nil {
a["of"] = "design " + l.pos[0]
}
}
return call{"knowledge-base.progress", a}, nil
}},
// Enrolment (the seat mesh-enrolment; `enrol open` is local, in main.go).
{words: []string{"enrol", "issue"}, usage: "<node> --why <text> [--expires 1h]", does: "a one-time token for a new machine, on the operator's Approve",
allowed: []string{"why", "expires"}, build: enrolIssue},
{words: []string{"token", "issue"}, usage: "--new <node> --why <text>", does: "the same as nox enrol issue",
allowed: []string{"new", "node", "why", "expires", "adopted"}, build: func(l line) (call, error) {
node := l.flags["new"]
if node == "" {
node = l.flags["node"]
}
if node != "" {
l.pos = append([]string{node}, l.pos...)
}
return enrolIssue(l)
}},
{words: []string{"enrol", "tokens"}, usage: "[<node>]", does: "every open enrolment, or one", build: optArg("mesh-enrolment.tokens", "node")},
{words: []string{"enrol", "revoke"}, usage: "<node> --why <text>", does: "end an enrolment at any stage", allowed: []string{"why"},
build: func(l line) (call, error) {
if len(l.pos) != 1 || !l.has("why") {
return call{}, usageErr("give the node and --why")
}
return call{"mesh-enrolment.revoke", map[string]any{"node": l.pos[0], "why": l.flags["why"]}}, nil
}},
{words: []string{"enrol", "again"}, usage: "<node>", does: "raise an enrolment's ask again", build: needArg("mesh-enrolment.again", "node")},
// Any address.
{words: []string{"call"}, usage: "<address> [<json arguments>]", does: "call any seat verb or module tool, e.g. ace/node-service-manager.status",
build: func(l line) (call, error) {
if len(l.pos) < 1 || len(l.pos) > 2 {
return call{}, usageErr("give the address and at most one JSON object of arguments")
}
return addressCall(l.pos[0], l.pos[1:])
}},
{words: []string{"ask"}, usage: "<module> <tool> [<json arguments>]", does: "call one of a module's tools",
build: func(l line) (call, error) {
if len(l.pos) < 2 || len(l.pos) > 3 {
return call{}, usageErr("give the module, the tool and at most one JSON object of arguments")
}
return addressCall(l.pos[0]+"."+l.pos[1], l.pos[2:])
}},
}
// unowned are the controller's own lines that no seat or module took over, and what to use where there is anything.
var unowned = map[string]string{
"migrate": "", "prepare": "", "serve": "", "declare": "", "broker": "", "board": "", "api": "", "facts": "",
"rehearse": "", "take": "", "converge": "", "adopt": "", "move-placement": "", "move-config": "", "durations": "",
"data": "each machine's backups: nox call <node>/node-backup.backed-up",
"forget-checks": "nox checks <owner/repo>", "queue": "nox builds; nox stuck", "cancel": "nox deliveries",
"clear": "nox deliveries", "kill": "nox deliveries", "replay": "nox dead-letters",
"pause": "nox call <node>/node-build-go.pause (or node-build-node, node-build-image)",
"resume": "nox call <node>/node-build-go.resume (or node-build-node, node-build-image)",
"push": "a merge is delivered by mesh-delivery: nox deliveries; one version on one machine is mesh-delivery-assigner.assigner_put",
"deliver": "nox deliveries", "plans": "nox deliveries", "delivery-stop": "nox stalled",
"delivery-backups-again": "nox deliveries", "delivery-walks": "nox deliveries", "delivery-order": "nox deliveries",
"rollout": "nox upgrade <module>; delivery is mesh-delivery's: nox deliveries",
}
// find answers the command whose words begin the line, the longest first, and what follows them.
func find(args []string) (*command, []string) {
var best *command
for i := range commands {
c := &commands[i]
if len(c.words) > len(args) || (best != nil && len(c.words) <= len(best.words)) {
continue
}
match := true
for j, w := range c.words {
if args[j] != w {
match = false
break
}
}
if match {
best = c
}
}
if best == nil {
return nil, nil
}
return best, args[len(best.words):]
}
// read builds the call of a line for a command that has an owner.
func (c *command) read(args []string) (call, error) {
l, err := parseLine(args)
if err != nil {
return call{}, err
}
allowed := map[string]bool{"json": true}
for _, f := range c.allowed {
allowed[f] = true
}
for f := range l.flags {
if !allowed[f] {
return call{}, usageErr("--%s is not a flag of this line", f)
}
}
return c.build(l)
}
// now is the clock; a variable so a test can say.
var now = time.Now
func fixed(address string) func(line) (call, error) {
return func(l line) (call, error) {
if len(l.pos) > 0 {
return call{}, usageErr("this line takes no %q", l.pos[0])
}
return call{address, map[string]any{}}, nil
}
}
func needArg(address, name string) func(line) (call, error) {
return func(l line) (call, error) {
if len(l.pos) != 1 {
return call{}, usageErr("give the %s", name)
}
return call{address, map[string]any{name: l.pos[0]}}, nil
}
}
func optArg(address, name string) func(line) (call, error) {
return func(l line) (call, error) {
switch len(l.pos) {
case 0:
return call{address, map[string]any{}}, nil
case 1:
return call{address, map[string]any{name: l.pos[0]}}, nil
}
return call{}, usageErr("give at most one %s", name)
}
}
func nodeArg(address string) func(line) (call, error) { return needArg(address, "node") }
func optNode(address string) func(line) (call, error) { return optArg(address, "node") }
// pick is the line's flags named, as string arguments.
func pick(l line, names ...string) map[string]any {
a := map[string]any{}
for _, n := range names {
if v, ok := l.flags[n]; ok {
a[n] = v
}
}
return a
}
// numbers turns the named arguments into numbers; the call's address is left to the caller.
func numbers(a map[string]any, names ...string) (map[string]any, error) {
for _, n := range names {
v, ok := a[n].(string)
if !ok {
continue
}
i, err := strconv.Atoi(v)
if err != nil {
return a, usageErr("--%s takes a number, not %q", n, v)
}
a[n] = i
}
return a, nil
}
// propose is a change of placement with its why.
func propose(l line, changes ...map[string]any) (call, error) {
if !l.has("why") {
return call{}, usageErr("a change of placement needs --why; the operator is asked to approve it")
}
list := make([]any, len(changes))
for i, c := range changes {
list[i] = c
}
return call{"mesh-placement.propose", map[string]any{"why": l.flags["why"], "changes": list}}, nil
}
func placeModules(op string) func(line) (call, error) {
return func(l line) (call, error) {
if len(l.pos) < 2 {
return call{}, usageErr("give the node and at least one module")
}
var changes []map[string]any
for _, m := range l.pos[1:] {
changes = append(changes, map[string]any{"op": op, "node": l.pos[0], "module": m})
}
return propose(l, changes...)
}
}
func publicDomain(l line) (call, error) {
switch {
case len(l.pos) == 1 && !l.has("clear"):
return call{"mesh-placement.placement", map[string]any{"node": l.pos[0]}}, nil
case len(l.pos) == 1:
return propose(l, map[string]any{"op": "decide", "node": l.pos[0], "field": "public-domain", "value": nil})
case len(l.pos) == 2 && !l.has("clear"):
return propose(l, map[string]any{"op": "decide", "node": l.pos[0], "field": "public-domain", "value": l.pos[1]})
}
return call{}, usageErr("give the node, and a domain or --clear")
}
func configChange(l line, action string) (call, error) {
want := 3
if action == "clear" {
want = 2
}
if len(l.pos) == 2 && action == "set" && strings.HasPrefix(strings.TrimSpace(l.pos[1]), "{") {
return call{}, usageErr("a whole settings file is not set at once any more: set each key, " +
"nox settings set <module> <key> <value> --node <node> --why <text> (mesh-config.config)")
}
if len(l.pos) != want || !l.has("node") || !l.has("why") {
return call{}, usageErr("give the module, the key%s, --node and --why", map[bool]string{true: " and the value"}[action == "set"])
}
a := map[string]any{"action": action, "module": l.pos[0], "key": l.pos[1], "node": l.flags["node"], "why": l.flags["why"]}
if action == "set" {
a["value"] = jsonOrText(l.pos[2])
}
return call{"mesh-config.config", a}, nil
}
// jsonOrText is a value as JSON when it reads as JSON (a number, true, an object), else the text itself.
func jsonOrText(v string) any {
var out any
if err := json.Unmarshal([]byte(v), &out); err == nil {
return out
}
return v
}
func retirement(address string) func(line) (call, error) {
return func(l line) (call, error) {
if len(l.pos) == 0 || !l.has("why") {
return call{}, usageErr("give the consumers waiting and --why")
}
consumers := make([]any, len(l.pos))
for i, c := range l.pos {
consumers[i] = c
}
return call{address, map[string]any{"consumers": consumers, "why": l.flags["why"]}}, nil
}
}
func enrolIssue(l line) (call, error) {
if len(l.pos) != 1 || !l.has("why") {
return call{}, usageErr("give the new machine's name and --why")
}
a := pick(l, "why", "expires")
a["node"] = l.pos[0]
return call{"mesh-enrolment.issue", a}, nil
}
func addressCall(address string, rest []string) (call, error) {
a := map[string]any{}
if len(rest) == 1 {
if err := json.Unmarshal([]byte(rest[0]), &a); err != nil {
return call{}, usageErr("the arguments are not a JSON object: %v", err)
}
}
return call{address, a}, nil
}
// renderNodes shapes placement's keys as one line per node: its name, address and site.
func renderNodes(text string) string {
var p struct {
Keys map[string]struct {
Placement struct {
Decided map[string]any `json:"decided"`
Modules []any `json:"modules"`
} `json:"placement"`
} `json:"keys"`
}
if err := json.Unmarshal([]byte(text), &p); err != nil || len(p.Keys) == 0 {
return text
}
names := make([]string, 0, len(p.Keys))
for n := range p.Keys {
names = append(names, n)
}
sort.Strings(names)
var b strings.Builder
for _, n := range names {
k := p.Keys[n]
fmt.Fprintf(&b, "%-10s %-15v %-8v %d modules\n", n, orDash(k.Placement.Decided["address"]),
orDash(k.Placement.Decided["site"]), len(k.Placement.Modules))
}
return b.String()
}
func orDash(v any) any {
if v == nil || v == "" {
return "-"
}
return v
}
+144
View File
@@ -0,0 +1,144 @@
package main
// How mesh-cli reaches the mesh now that the controller is unassigned (novox/hq issue 1126): through this machine's
// console, the mesh MCP server the module node-tools serves on loopback (provision mcp-endpoint, ADR 0152: the
// machine's login is the authority). mesh-cli calls `mesh_call` with a seat verb's or a module tool's address, and the
// console puts the call on the bus, the way every agent session on the machine does. mesh-cli holds no credential and
// keeps no bus account of its own.
import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
// consoleFile is where the module writes the console's address, from the mcp-endpoint it requires; a variable so a
// test can name its own.
var consoleFile = "/etc/mesh-cli/console"
// defaultConsole is the console's address when the file is not there yet: the port node-tools declares it serves.
const defaultConsole = "http://127.0.0.1:4270/mcp"
// callWithin bounds one call: the console's own bound on a call is 30 seconds, and a wait is longer.
var callWithin = 2 * time.Minute
// maxAnswer bounds what is read of the console's answer.
const maxAnswer = 16 << 20
// consoleAddress is where the console listens, and where that was read from, for what is said when it does not answer.
func consoleAddress() (string, string) {
if v := strings.TrimSpace(os.Getenv("MESH_CONSOLE")); v != "" {
return v, "MESH_CONSOLE"
}
if raw, err := os.ReadFile(consoleFile); err == nil {
if v := strings.TrimSpace(string(raw)); v != "" && !strings.Contains(v, "${") {
return v, consoleFile
}
}
return defaultConsole, "the console's declared port (" + consoleFile + " is not there)"
}
// answer is what the console said of one call: its text, and whether the console marked it an error (a refusal, a
// tool's failure, nothing serving, no answer in time).
type answer struct {
Text string
IsError bool
}
// errNoConsole is a console that did not answer at all: nothing was called.
var errNoConsole = errors.New("no console")
// consoleTool asks the console one of its own tools (mesh_call, mesh_search, mesh_describe) and answers what it said.
func consoleTool(ctx context.Context, tool string, arguments map[string]any) (answer, error) {
url, from := consoleAddress()
body, err := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": 1, "method": "tools/call",
"params": map[string]any{"name": tool, "arguments": arguments}})
if err != nil {
return answer{}, err
}
ctx, cancel := context.WithTimeout(ctx, callWithin)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body))
if err != nil {
return answer{}, fmt.Errorf("the console's address %q (from %s) is not usable: %v", url, from, err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json, text/event-stream")
resp, err := http.DefaultClient.Do(req)
if err != nil {
if ctx.Err() != nil {
return answer{}, ctx.Err()
}
return answer{}, fmt.Errorf("%w: nothing answers at %s (from %s), so nothing was called. The console is the "+
"module node-tools on this machine: `systemctl status` of its unit, or call from another machine: %v",
errNoConsole, url, from, err)
}
defer resp.Body.Close()
raw, err := io.ReadAll(io.LimitReader(resp.Body, maxAnswer))
if err != nil {
return answer{}, fmt.Errorf("the console's answer could not be read: %v; the call may have run", err)
}
if resp.StatusCode != http.StatusOK {
return answer{}, fmt.Errorf("the console at %s answered %s: %s", url, resp.Status, strings.TrimSpace(string(raw)))
}
return decodeAnswer(raw)
}
// decodeAnswer reads the console's JSON-RPC answer, given as JSON or as a server-sent event's data.
func decodeAnswer(raw []byte) (answer, error) {
payload := bytes.TrimSpace(raw)
if !bytes.HasPrefix(payload, []byte("{")) {
sc := bufio.NewScanner(bytes.NewReader(raw))
sc.Buffer(make([]byte, 0, 64<<10), maxAnswer)
payload = nil
for sc.Scan() {
if d, ok := strings.CutPrefix(sc.Text(), "data:"); ok {
payload = []byte(strings.TrimSpace(d))
}
}
}
var rpc struct {
Result *struct {
Content []struct {
Type string `json:"type"`
Text string `json:"text"`
} `json:"content"`
IsError bool `json:"isError"`
} `json:"result"`
Error *struct {
Message string `json:"message"`
} `json:"error"`
}
if err := json.Unmarshal(payload, &rpc); err != nil {
return answer{}, fmt.Errorf("the console's answer is not one mesh-cli reads (%v); the call may have run", err)
}
if rpc.Error != nil {
return answer{Text: rpc.Error.Message, IsError: true}, nil
}
if rpc.Result == nil {
return answer{}, errors.New("the console answered nothing; the call may have run")
}
var parts []string
for _, c := range rpc.Result.Content {
if c.Type == "text" || c.Type == "" {
parts = append(parts, c.Text)
}
}
return answer{Text: strings.Join(parts, "\n"), IsError: rpc.Result.IsError}, nil
}
// meshCall calls one address through the console.
func meshCall(ctx context.Context, address string, args map[string]any) (answer, error) {
if args == nil {
args = map[string]any{}
}
return consoleTool(ctx, "mesh_call", map[string]any{"address": address, "arguments": args})
}
+5 -5
View File
@@ -17,8 +17,8 @@ import (
// 774/S51/AC1, AC2 (novox/hq ADR 0587 §6, §8): a token sealed to a test operator key (as mesh-vault seals it: the text
// of the token, anonymous box, base64) is opened with the private half and printed as a token the engine's parser reads
// (base64url of version 1 JSON with node, broker, fingerprint, signer and secret), and nothing is asked of the engine's
// socket, which here is a path nobody listens on; the wrong key prints nothing and says the token is not sealed to it.
// (base64url of version 1 JSON with node, broker, fingerprint, signer and secret), and nothing is asked of the console, which
// here is an address nobody listens on; the wrong key prints nothing and says the token is not sealed to it.
func TestEnrolOpenOpensASealedTokenLocallyAndOnlyWithItsKey(t *testing.T) {
t.Setenv("MESH_VERB", "")
token := base64.RawURLEncoding.EncodeToString([]byte(`{"v":1,"node":"new1","broker":"tls://10.77.0.1:4222",` +
@@ -37,9 +37,9 @@ func TestEnrolOpenOpensASealedTokenLocallyAndOnlyWithItsKey(t *testing.T) {
}
return p
}
before, beforeIn := socket, standardInput
socket = filepath.Join(dir, "nobody-listens.sock")
t.Cleanup(func() { socket, standardInput = before, beforeIn })
beforeIn := standardInput
t.Setenv("MESH_CONSOLE", "http://127.0.0.1:1/mcp") // nothing listens: enrol open asks nothing of the mesh
t.Cleanup(func() { standardInput = beforeIn })
for _, c := range []struct {
name string
args []string
+166 -82
View File
@@ -1,11 +1,8 @@
// Command mesh-cli is the operator's command on every node (novox/hq ADR 0272): `mesh-cli <command line>` asks the
// controller to run one of its command lines, and prints what it said. In interactive zsh it is also `nox`.
// Command mesh-cli is the operator's command on every node: `mesh-cli <command line>`, in interactive zsh also `nox`.
//
// It holds no credential and reads no file of the mesh. It asks the node-engine on this machine over a local
// socket; the engine reads which account is asking from the kernel and asks the controller. The controller decides:
// on the control-node, from the operator's own account, the line runs as the controller's terminal; elsewhere it is
// an ordinary call, and a change made at the terminal alone is refused with the reason; from any other account
// nothing runs.
// Each line calls the seat verb or the module tool that owns its job (novox/hq issue 1126: the controller that ran
// these lines before is unassigned), through this machine's console, the mesh MCP server on loopback (console.go).
// A line no seat or module owns says so and names what to use instead. `enrol open` runs here and asks nothing.
package main
import (
@@ -15,16 +12,14 @@ import (
"io"
"os"
"os/signal"
"runtime/debug"
"sort"
"strings"
"syscall"
"git.novox.be/novox/mesh-sdk/go/cli"
)
// socket is where the node-engine answers; a variable so a test can name its own.
var socket = cli.Socket
// standardInput is mesh-cli's own standard input, and whether it is a terminal (or a device such as /dev/null),
// which is never read; a variable so a test can say.
// standardInput is mesh-cli's own standard input, and whether it is a terminal (or a device such as /dev/null); a
// variable so a test can say.
var standardInput = func() (io.Reader, bool) {
info, err := os.Stdin.Stat()
if err != nil {
@@ -33,39 +28,6 @@ var standardInput = func() (io.Reader, bool) {
return os.Stdin, info.Mode()&os.ModeCharDevice != 0
}
// carried is what mesh-cli carries of its standard input for the line: what a pipe or a file held, at most
// cli.MaxStdin, for the controller's terminal alone (a secret given with `secret accept … --from -`). A terminal is
// never read: the line is asked without. More than the bound is refused, and nothing is asked.
func carried() ([]byte, error) {
in, terminal := standardInput()
if terminal {
return nil, nil
}
body, err := io.ReadAll(io.LimitReader(in, cli.MaxStdin+1))
if err != nil {
return nil, fmt.Errorf("standard input could not be read (%v); nothing was asked", err)
}
if len(body) > cli.MaxStdin {
return nil, fmt.Errorf("standard input is more than %d bytes, which mesh-cli does not carry; nothing was asked", cli.MaxStdin)
}
return body, nil
}
const usage = `mesh-cli — the controller's command lines, from this node (novox/hq ADR 0272)
mesh-cli <command line> e.g. mesh-cli status, mesh-cli settings show zsh --node <node>
nox <command line> the same, in interactive zsh
nox enrol open --key <file> opens a sealed enrolment token here, asking nothing of the mesh (ADR 0587)
Run it as yourself, never under sudo: the node-engine tells the controller which account asks.
What a pipe or a file gives its standard input (at most 64 KiB) goes with the line to the controller's
terminal alone, e.g. a secret: mesh-cli secret accept <node> <module> <name> --from - < file
On the control-node, from its operator's account, the line runs as the controller's terminal.
Anywhere else it is an ordinary call, and a terminal-only change is refused with the reason.
'mesh-cli help' asks the controller for its own list of command lines.
`
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
code := run(ctx, os.Args[1:], os.Stdout, os.Stderr)
@@ -74,56 +36,178 @@ func main() {
}
func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
if len(args) == 0 || args[0] == "-h" || args[0] == "--help" {
fmt.Fprint(stderr, usage)
return 2
if len(args) == 0 || args[0] == "-h" || args[0] == "--help" || args[0] == "help" {
fmt.Fprint(stderr, usage())
if len(args) == 0 {
return 2
}
return 0
}
// A verb's own process carries the verb it runs for; a verb is never a person, so it is never the terminal.
// A verb's own process carries the verb it runs for; a verb is never a person.
if verb := os.Getenv("MESH_VERB"); verb != "" {
fmt.Fprintf(stderr, "mesh-cli: this process runs for the verb %q, and a verb is never the operator. "+
"Nothing was asked\n", verb)
"Nothing was called\n", verb)
return 1
}
switch args[0] {
case "version":
fmt.Fprintln(stdout, version())
return 0
case "search":
if len(args) < 2 {
fmt.Fprintln(stderr, "mesh-cli: give the words: nox search <words>")
return 2
}
return show(ctx, "mesh_search", map[string]any{"query": strings.Join(args[1:], " ")}, nil, stdout, stderr)
case "describe":
if len(args) != 2 {
fmt.Fprintln(stderr, "mesh-cli: give one address: nox describe <address>")
return 2
}
return show(ctx, "mesh_describe", map[string]any{"address": args[1]}, nil, stdout, stderr)
}
// `enrol open` is local: it opens a sealed enrolment token with the operator's private key and asks nothing of the
// node-engine or the mesh (novox/hq ADR 0587 §8).
// mesh (novox/hq ADR 0587 §8).
if len(args) >= 2 && args[0] == "enrol" && args[1] == "open" {
in, terminal := standardInput()
return enrolOpen(args[2:], in, terminal, stdout, stderr)
}
in, err := carried()
c, rest := find(args)
if c == nil {
if instead, ok := unowned[args[0]]; ok {
return noOwner(args[0], instead, stderr)
}
fmt.Fprintf(stderr, "mesh-cli: %q is not a command. nox help lists them; nox search <words> finds a seat verb "+
"or a tool, and nox call <address> calls it\n", args[0])
return 2
}
if c.build == nil {
return noOwner(strings.Join(c.words, " "), c.instead, stderr)
}
k, err := c.read(rest)
if err != nil {
fmt.Fprintf(stderr, "mesh-cli: %v\n", err)
return 1
fmt.Fprintf(stderr, "mesh-cli: %v. Nothing was called\n nox %s %s\n", err, strings.Join(c.words, " "), c.usage)
return 2
}
a, err := cli.AskWith(ctx, socket, cli.Request{Line: args, Stdin: in})
if err != nil {
if errors.Is(ctx.Err(), context.Canceled) {
fmt.Fprintln(stderr, "mesh-cli: stopped waiting; the line may still be running on the controller")
return 130
}
fmt.Fprintf(stderr, "mesh-cli: %v\n", err)
return 1
if k.Address == "mesh-delivery-view.view_status" {
return status(ctx, stdout, stderr)
}
_, _ = stdout.Write(a.Stdout)
_, _ = stderr.Write(a.Stderr)
if a.Cut {
fmt.Fprintln(stderr, "mesh-cli: the output was longer than one bus message, and was cut there")
}
if !a.Terminal && a.Why != "" {
fmt.Fprintln(stderr, "mesh-cli: "+a.Why)
}
if a.Unknown != "" {
// Asked, and what came of it not known: never said as nothing having run.
fmt.Fprintln(stderr, "mesh-cli: "+a.Unknown)
if a.Exit == 0 {
return 1
return show(ctx, "mesh_call", map[string]any{"address": k.Address, "arguments": k.Args}, c.render, stdout, stderr)
}
// noOwner says a line has no owner since the controller went, and what to use.
func noOwner(what, instead string, stderr io.Writer) int {
fmt.Fprintf(stderr, "mesh-cli: nox %s: %s.\n", what, gone)
if instead != "" {
fmt.Fprintf(stderr, " instead: %s\n", instead)
} else {
fmt.Fprintln(stderr, " no seat or module does this now; nox search <words> finds what does nearby")
}
return 1
}
// show calls one of the console's tools and prints what it answered: an answer marked an error on standard error, with
// exit 1.
func show(ctx context.Context, tool string, args map[string]any, render func(string) string, stdout, stderr io.Writer) int {
a, err := consoleTool(ctx, tool, args)
if err != nil {
return failed(err, stderr)
}
if a.IsError {
fmt.Fprintln(stderr, "mesh-cli: "+strings.TrimRight(a.Text, "\n"))
return 1
}
text := a.Text
if render != nil {
text = render(text)
}
fmt.Fprintln(stdout, strings.TrimRight(text, "\n"))
return 0
}
// failed says why a call did not come back, and never as nothing having run unless nothing answered.
func failed(err error, stderr io.Writer) int {
switch {
case errors.Is(err, context.Canceled):
fmt.Fprintln(stderr, "mesh-cli: stopped waiting; the call may still run")
return 130
case errors.Is(err, context.DeadlineExceeded):
fmt.Fprintln(stderr, "mesh-cli: no answer in time; the call may have run. Check before repeating a change")
return 1
}
fmt.Fprintf(stderr, "mesh-cli: %v\n", err)
return 1
}
// status is the delivery view's status. When the view does not answer, that is said first and loudly, and what the
// condition keeper and mesh-delivery hold is shown in its place, each under its address, with exit 1: never shown as
// the status.
func status(ctx context.Context, stdout, stderr io.Writer) int {
a, err := meshCall(ctx, "mesh-delivery-view.view_status", nil)
if err == nil && !a.IsError {
fmt.Fprintln(stdout, strings.TrimRight(a.Text, "\n"))
return 0
}
if errors.Is(err, errNoConsole) || errors.Is(err, context.Canceled) {
return failed(err, stderr)
}
why := a.Text
if err != nil {
why = err.Error()
}
fmt.Fprintf(stderr, "mesh-cli: the status (mesh-delivery-view.view_status) did not answer: %s\n"+
"mesh-cli: what follows is not the status: the open conditions and the deliveries, read on their own\n",
strings.TrimRight(why, "\n"))
for _, address := range []string{"mesh-conditions.conditions", "mesh-delivery.deliveries"} {
fmt.Fprintf(stdout, "== %s\n", address)
show(ctx, "mesh_call", map[string]any{"address": address, "arguments": map[string]any{}}, nil, stdout, stderr)
}
return 1
}
// version is what this binary was built from.
func version() string {
info, ok := debug.ReadBuildInfo()
if !ok {
return "mesh-cli (unknown build)"
}
v := "mesh-cli " + info.Main.Version
for _, s := range info.Settings {
if s.Key == "vcs.revision" {
v += " " + s.Value
}
}
if a.Refused != "" {
fmt.Fprintln(stderr, "mesh-cli: "+a.Refused)
if a.Exit == 0 {
return 1
return v
}
// usage is every line nox takes, grouped by its first word.
func usage() string {
var b strings.Builder
b.WriteString(`mesh-cli: the operator's command (nox in interactive zsh)
Each line calls the seat verb or module tool that owns the job, through this machine's console (the mesh MCP
server on loopback). The controller that ran these lines before is unassigned (novox/hq issue 1126).
`)
lines := make([]string, 0, len(commands))
for _, c := range commands {
if c.build == nil {
continue
}
lines = append(lines, fmt.Sprintf(" nox %s %s\n %s", strings.Join(c.words, " "), c.usage, c.does))
}
return a.Exit
sort.Strings(lines)
b.WriteString(strings.Join(lines, "\n"))
b.WriteString(`
nox search <words>
find a seat verb or a module tool
nox describe <address>
one address's arguments
nox enrol open --key <file>
open an enrolment token sealed to the operator key, here; asks nothing of the mesh
nox version
Every answer is the owner's own, as JSON. A line the controller ran that no seat owns says so and what to use.
`)
return b.String()
}
+247 -118
View File
@@ -3,153 +3,282 @@ package main
import (
"bytes"
"context"
"encoding/json"
"io"
"net"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/cli"
"time"
)
// The tests' own standard input is the runner's, which may be a pipe nobody closes: read as a terminal unless a test
// says otherwise.
// The tests' own standard input is the runner's, which may be a pipe nobody closes: read as a terminal.
func init() { standardInput = func() (io.Reader, bool) { return nil, true } }
// lastStdin is the standard input the engine was last given.
var lastStdin []byte
// asked is one call the stand-in console took: the console's tool, and for mesh_call the address and its arguments.
type asked struct {
Tool string
Address string
Args string
}
// engine answers one connection on a socket of the test's own as the node-engine would, and says what it was asked.
func engine(t *testing.T, answer cli.Answer) <-chan []string {
// console is a stand-in for this machine's console: it answers MCP tools/call over HTTP as node-tools does, with the
// answer the test gives per address (an error when it starts with "!"), and records what it was asked.
func console(t *testing.T, answers map[string]string) *[]asked {
t.Helper()
path := filepath.Join(t.TempDir(), "cli.sock")
l, err := net.Listen("unix", path)
if err != nil {
t.Skipf("no unix socket here: %v", err)
}
t.Cleanup(func() { l.Close() })
before := socket
socket = path
t.Cleanup(func() { socket = before })
asked := make(chan []string, 1)
lastStdin = nil
go func() {
conn, err := l.Accept()
if err != nil {
var got []asked
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/mcp" || r.Method != http.MethodPost {
http.NotFound(w, r)
return
}
defer conn.Close()
req, err := cli.ReadRequest(conn)
if err != nil {
return
var req struct {
Method string `json:"method"`
Params struct {
Name string `json:"name"`
Arguments struct {
Address string `json:"address"`
Arguments map[string]any `json:"arguments"`
Query string `json:"query"`
} `json:"arguments"`
} `json:"params"`
}
lastStdin = req.Stdin
asked <- req.Line
_ = cli.WriteAnswer(conn, answer)
}()
return asked
}
// The line is asked as given, what the controller printed is printed, and its exit is mesh-cli's.
func TestTheLineIsAskedAndTheAnswerIsPrinted(t *testing.T) {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Method != "tools/call" {
t.Errorf("the console was asked %v %v", req.Method, err)
}
a := req.Params.Arguments
args, _ := json.Marshal(a.Arguments)
got = append(got, asked{req.Params.Name, a.Address, string(args)})
key := a.Address
if req.Params.Name != "mesh_call" {
key = req.Params.Name
}
text, ok := answers[key]
if !ok {
text = `{"ok":true}`
}
isError := strings.HasPrefix(text, "!")
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"jsonrpc": "2.0", "id": 1, "result": map[string]any{
"content": []any{map[string]any{"type": "text", "text": strings.TrimPrefix(text, "!")}}, "isError": isError}})
}))
t.Cleanup(srv.Close)
t.Setenv("MESH_CONSOLE", srv.URL+"/mcp")
t.Setenv("MESH_VERB", "")
asked := engine(t, cli.Answer{Stdout: []byte("out\n"), Stderr: []byte("err\n"), Exit: 3, Terminal: true, Why: "the terminal"})
return &got
}
func runLine(args ...string) (int, string, string) {
var out, errOut bytes.Buffer
code := run(context.Background(), []string{"settings", "set", "zsh", `{"execute":"withhold"}`}, &out, &errOut)
if line := <-asked; strings.Join(line, "|") != `settings|set|zsh|{"execute":"withhold"}` {
t.Fatalf("asked %q", line)
}
if code != 3 || out.String() != "out\n" || errOut.String() != "err\n" {
t.Fatalf("exit %d, stdout %q, stderr %q", code, out.String(), errOut.String())
code := run(context.Background(), args, &out, &errOut)
return code, out.String(), errOut.String()
}
// Every owned line calls the seat verb or module tool that owns its job, with the arguments the owner reads
// (novox/hq issue 1126; hq .work/1126/nox-map.md), and prints its answer.
func TestEachLineCallsItsOwner(t *testing.T) {
now = func() time.Time { return time.Date(2026, 11, 2, 10, 0, 0, 0, time.UTC) }
t.Cleanup(func() { now = time.Now })
for _, c := range []struct {
line string
address string
args string
}{
{"node show ace", "mesh-placement.placement", `{"node":"ace"}`},
{"node public-domain ace", "mesh-placement.placement", `{"node":"ace"}`},
{"node public-domain ace example.org --why moved", "mesh-placement.propose", `{"changes":[{"field":"public-domain","node":"ace","op":"decide","value":"example.org"}],"why":"moved"}`},
{"plan ace", "mesh-delivery-view.assigner_wants", `{"node":"ace"}`},
{"haves", "mesh-delivery-view.node_haves", `{}`},
{"composition ace plex", "ace/node-engine.composition", `{"module":"plex"}`},
{"seats", "mesh-placement.placement", `{"seats":"true"}`},
{"assign ace plex sonarr --why=media", "mesh-placement.propose", `{"changes":[{"module":"plex","node":"ace","op":"place"},{"module":"sonarr","node":"ace","op":"place"}],"why":"media"}`},
{"unassign ace plex --why gone", "mesh-placement.propose", `{"changes":[{"module":"plex","node":"ace","op":"take-off"}],"why":"gone"}`},
{"pin ace postgres-database novox postgres --why one", "mesh-placement.propose", `{"changes":[{"node":"ace","op":"pin","provider":{"module":"postgres","node":"novox"},"provision":"postgres-database"}],"why":"one"}`},
{"unpin ace postgres-database --why back", "mesh-placement.propose", `{"changes":[{"node":"ace","op":"unpin","provision":"postgres-database"}],"why":"back"}`},
{"deliveries --state merged --json", "mesh-delivery.deliveries", `{"state":"merged"}`},
{"delivery novox/mesh-sdk@dff0ce2abb21", "mesh-delivery.show", `{"id":"novox/mesh-sdk@dff0ce2abb21"}`},
{"checks novox/mesh-catalog --number 591", "mesh-delivery.checks", `{"number":"591","repository":"novox/mesh-catalog"}`},
{"try novox/mesh-catalog abc123 --level unit", "mesh-delivery-checker.try", `{"head":"abc123","level":"unit","repository":"novox/mesh-catalog"}`},
{"builds", "mesh-delivery-builder.builder_builds_since", `{"since":"2026-11-01T10:00:00Z"}`},
{"upgrade nats --policy record --why bus", "mesh-wants.upgrade", `{"module":"nats","policy":"record","why":"bus"}`},
{"bus", "mesh-wants.bus", `{}`},
{"conditions --severity urgent", "mesh-conditions.conditions", `{"severity":"urgent"}`},
{"conditions show machine.g14.root-not-free", "mesh-conditions.conditions", `{"key":"machine.g14.root-not-free"}`},
{"conditions silence k1 --for 4h --why known", "mesh-conditions.silence", `{"for":"4h","key":"k1","why":"known"}`},
{"conditions history --days 3", "mesh-conditions.conditions", `{"days":"3","history":"true"}`},
{"conditions history --key k1", "mesh-conditions.conditions_history", `{"key":"k1"}`},
{"doctor", "mesh-self-check.doctor", `{}`},
{"doctor run", "mesh-self-check.doctor", `{"run":true}`},
{"doctor probe D6", "mesh-self-check.doctor", `{"probe":"D6"}`},
{"healers --days 2", "mesh-healers.healers", `{"days":2}`},
{"hand-act record restarted the view --why stuck --cause view", "mesh-hand-acts.hand-act", `{"cause":"view","what":"restarted the view","why":"stuck"}`},
{"hand-acts --days 3", "mesh-hand-acts.hand-acts", `{"days":3}`},
{"settings show zsh --node g14", "mesh-config.config", `{"action":"list","module":"zsh","node":"g14"}`},
{"settings set zsh execute withhold --node g14 --why test", "mesh-config.config", `{"action":"set","key":"execute","module":"zsh","node":"g14","value":"withhold","why":"test"}`},
{"settings clear zsh execute --node g14 --why test", "mesh-config.config", `{"action":"clear","key":"execute","module":"zsh","node":"g14","why":"test"}`},
{"config history --key k --limit 5", "mesh-config.config", `{"action":"history","key":"k","limit":5}`},
{"secret export", "mesh-vault.secret_vault_export", `{}`},
{"operator key show", "mesh-vault.vault_status", `{}`},
{"rotate --node ace --module plex --name token --why leak", "mesh-vault.rotate", `{"module":"plex","name":"token","node":"ace","why":"leak"}`},
{"retire approve c1 c2 --why done", "mesh-vault.provisioner_retire_approve", `{"consumers":["c1","c2"],"why":"done"}`},
{"collect --confirm --why space", "mesh-artifact-sweep.collect", `{"confirm":"true","why":"space"}`},
{"images ace", "mesh-artifact-sweep.images", `{"node":"ace"}`},
{"licence use g14 work", "anthropic-licence-manager.switch", `{"consumer":"g14","licence":"work"}`},
{"module show plex", "mesh-delivery-view.indexer_module", `{"module":"plex"}`},
{"issues apostrophe bug --status open --limit 5", "issue-tracker.list", `{"limit":5,"search":"apostrophe bug","status":"open"}`},
{"issue 1126 --brief", "issue-tracker.get", `{"brief":true,"number":1126}`},
{"kb ask what holds about the merge window", "knowledge-base.ask", `{"question":"what holds about the merge window"}`},
{"kb get 587 --reasoning", "knowledge-base.get", `{"number":587,"reasoning":true}`},
{"progress 47", "knowledge-base.progress", `{"of":"design 47"}`},
{"enrol issue new1 --why laptop --expires 2h", "mesh-enrolment.issue", `{"expires":"2h","node":"new1","why":"laptop"}`},
{"token issue --new new1 --why laptop", "mesh-enrolment.issue", `{"node":"new1","why":"laptop"}`},
{"enrol tokens", "mesh-enrolment.tokens", `{}`},
{"call ace/node-service-manager.status {\"unit\":\"plex\"}", "ace/node-service-manager.status", `{"unit":"plex"}`},
{"ask mesh-catalog catalog_modules", "mesh-catalog.catalog_modules", `{}`},
} {
got := console(t, nil)
args := strings.Fields(c.line)
if strings.HasPrefix(c.line, "call ") {
args = []string{"call", "ace/node-service-manager.status", `{"unit":"plex"}`}
}
code, out, errOut := runLine(args...)
if code != 0 || len(*got) != 1 || (*got)[0].Tool != "mesh_call" || (*got)[0].Address != c.address || (*got)[0].Args != c.args {
t.Errorf("nox %s: exit %d, asked %+v, stderr %q; want %s %s", c.line, code, *got, errOut, c.address, c.args)
continue
}
if !strings.Contains(out, `"ok":true`) {
t.Errorf("nox %s printed %q, not the owner's answer", c.line, out)
}
}
}
// Not the terminal, refused, or cut: each is said, and a refusal never exits 0.
func TestWhyARefusalAndACutAreSaid(t *testing.T) {
t.Setenv("MESH_VERB", "")
engine(t, cli.Answer{Why: "not the controller's terminal: agents on laptop may run as operator",
Refused: "set at the controller's terminal only", Cut: true})
var out, errOut bytes.Buffer
code := run(context.Background(), []string{"settings", "set", "x", "{}"}, &out, &errOut)
said := errOut.String()
if code == 0 || !strings.Contains(said, "not the controller's terminal") || !strings.Contains(said, "terminal only") ||
!strings.Contains(said, "cut") {
t.Fatalf("exit %d, said %q", code, said)
// A line the controller ran that no seat owns says so and what to use, exits 1 and calls nothing; a line nox cannot
// read says how it is written, exits 2 and calls nothing.
func TestALineWithNoOwnerOrNotReadCallsNothingAndSaysWhy(t *testing.T) {
for _, c := range []struct {
line string
code int
err string
}{
{"push ace", 1, "mesh-delivery-assigner.assigner_put"},
{"migrate", 1, "no seat or module does this now"},
{"secret accept ace plex token", 1, "mesh-vault.secret-ask"},
{"node add new1", 1, "nox enrol issue"},
{"pause", 1, "node-build-go.pause"},
{"operator key make", 1, "operator key show"},
{"assign ace plex", 2, "needs --why"},
{"issue twelve", 2, "not an issue's number"},
{"conditions --bogus x", 2, "--bogus is not a flag"},
{"settings set zsh {\"execute\":\"withhold\"} --node g14 --why x", 2, "set each key"},
{"healers --days soon", 2, "takes a number"},
{"frobnicate", 2, "is not a command"},
} {
got := console(t, nil)
args := strings.Fields(c.line)
code, _, errOut := runLine(args...)
if code != c.code || len(*got) != 0 || !strings.Contains(errOut, c.err) {
t.Errorf("nox %s: exit %d, asked %+v, stderr %q; want exit %d saying %q", c.line, code, *got, errOut, c.code, c.err)
}
if c.code == 1 && !strings.Contains(errOut, "issue 1126") {
t.Errorf("nox %s does not say the controller is unassigned: %q", c.line, errOut)
}
}
}
// A verb's own process is never the operator: refused before anything is asked.
func TestAVerbsProcessIsRefusedBeforeAsking(t *testing.T) {
t.Setenv("MESH_VERB", "command")
before := socket
socket = filepath.Join(t.TempDir(), "nothing-listens.sock")
t.Cleanup(func() { socket = before })
var out, errOut bytes.Buffer
if code := run(context.Background(), []string{"status"}, &out, &errOut); code != 1 ||
!strings.Contains(errOut.String(), "never the operator") {
t.Fatalf("exit %d, said %q", code, errOut.String())
// What the console says is printed as said: an answer it marks an error on standard error with exit 1, a console that
// does not answer as nothing called, a verb's process refused before anything is called; nodes are one line each.
func TestAnswersRefusalsAndSilenceAreSaid(t *testing.T) {
got := console(t, map[string]string{"mesh-enrolment.tokens": "!nothing serves seat:mesh-enrolment.tokens"})
if code, out, errOut := runLine("enrol", "tokens"); code != 1 || out != "" || !strings.Contains(errOut, "nothing serves") {
t.Errorf("an error answer: exit %d, stdout %q, stderr %q", code, out, errOut)
}
placement := `{"keys":{"novox":{"placement":{"decided":{"address":"10.10.0.1","site":"hub"},"modules":[{},{}]}},` +
`"ace":{"placement":{"decided":{"address":"10.10.0.2"},"modules":[{}]}}}}`
console(t, map[string]string{"mesh-placement.placement": placement})
if code, out, _ := runLine("nodes"); code != 0 || !strings.HasPrefix(out, "ace ") ||
!strings.Contains(out, "10.10.0.1") || !strings.Contains(out, "2 modules") {
t.Errorf("nodes: exit %d, %q", code, out)
}
got = console(t, map[string]string{"mesh_search": `{"matches":[]}`})
if code, out, _ := runLine("search", "restart", "a", "unit"); code != 0 || (*got)[0].Tool != "mesh_search" || !strings.Contains(out, "matches") {
t.Errorf("search: exit %d, asked %+v, %q", code, *got, out)
}
t.Setenv("MESH_CONSOLE", "http://127.0.0.1:1/mcp")
if code, _, errOut := runLine("conditions"); code != 1 || !strings.Contains(errOut, "nothing was called") {
t.Errorf("no console: exit %d, %q", code, errOut)
}
t.Setenv("MESH_VERB", "some.verb")
if code, _, errOut := runLine("conditions"); code != 1 || !strings.Contains(errOut, "a verb is never the operator") {
t.Errorf("a verb's process: exit %d, %q", code, errOut)
}
}
// An engine that does not answer is said, with where it was asked.
func TestAnEngineThatDoesNotAnswerIsSaid(t *testing.T) {
t.Setenv("MESH_VERB", "")
before := socket
socket = filepath.Join(t.TempDir(), "nothing-listens.sock")
t.Cleanup(func() { socket = before })
var out, errOut bytes.Buffer
if code := run(context.Background(), []string{"status"}, &out, &errOut); code != 1 ||
!strings.Contains(errOut.String(), "does not answer") {
t.Fatalf("exit %d, said %q", code, errOut.String())
// The status is the delivery view's. When the view does not answer, that is said first, the conditions and the
// deliveries are shown under their own addresses, and the exit is 1: they are never passed off as the status.
func TestStatusIsTheViewsAndItsSilenceIsSaid(t *testing.T) {
got := console(t, map[string]string{"mesh-delivery-view.view_status": `{"well":true}`})
if code, out, _ := runLine("status"); code != 0 || len(*got) != 1 || !strings.Contains(out, `"well":true`) {
t.Errorf("status: exit %d, asked %+v, %q", code, *got, out)
}
got = console(t, map[string]string{"mesh-delivery-view.view_status": "!mesh-delivery-view.view_status did not answer within 30s",
"mesh-conditions.conditions": `{"conditions":[]}`, "mesh-delivery.deliveries": `{"deliveries":[]}`})
code, out, errOut := runLine("status")
if code != 1 || len(*got) != 3 || !strings.Contains(errOut, "did not answer") || !strings.Contains(errOut, "not the status") ||
!strings.Contains(out, "== mesh-conditions.conditions") || !strings.Contains(out, "== mesh-delivery.deliveries") {
t.Errorf("status, the view silent: exit %d, asked %+v, stdout %q, stderr %q", code, *got, out, errOut)
}
}
// An outcome not known is said as such — the line may have run — and never as nothing having run.
func TestAnOutcomeNotKnownIsSaid(t *testing.T) {
t.Setenv("MESH_VERB", "")
engine(t, cli.NotKnown("call-4", "no answer within 6m0s"))
var out, errOut bytes.Buffer
code := run(context.Background(), []string{"push", "laptop"}, &out, &errOut)
if code == 0 || !strings.Contains(errOut.String(), "may have run") || !strings.Contains(errOut.String(), "call-4") {
t.Fatalf("exit %d, said %q", code, errOut.String())
// The console's address comes from the file the module writes from its mcp-endpoint; MESH_CONSOLE overrides it; with
// neither, the port node-tools declares. An answer given as a server-sent event is read like one given as JSON.
func TestTheConsoleIsFoundAndItsAnswerRead(t *testing.T) {
t.Setenv("MESH_CONSOLE", "")
dir := t.TempDir()
before := consoleFile
consoleFile = filepath.Join(dir, "console")
t.Cleanup(func() { consoleFile = before })
if url, _ := consoleAddress(); url != defaultConsole {
t.Errorf("no file: %s", url)
}
if err := os.WriteFile(consoleFile, []byte("http://127.0.0.1:4999/mcp\n"), 0o644); err != nil {
t.Fatal(err)
}
if url, from := consoleAddress(); url != "http://127.0.0.1:4999/mcp" || from != consoleFile {
t.Errorf("the file: %s from %s", url, from)
}
t.Setenv("MESH_CONSOLE", "http://127.0.0.1:5000/mcp")
if url, _ := consoleAddress(); url != "http://127.0.0.1:5000/mcp" {
t.Errorf("the variable: %s", url)
}
a, err := decodeAnswer([]byte("event: message\ndata: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"content\":[{\"type\":\"text\",\"text\":\"hi\"}],\"isError\":true}}\n\n"))
if err != nil || a.Text != "hi" || !a.IsError {
t.Errorf("an event: %+v %v", a, err)
}
if _, err := decodeAnswer([]byte("<html>")); err == nil {
t.Error("a page that is no answer was read as one")
}
}
// A pipe's or a file's standard input goes with the line, whole; a terminal's is never read; more than the bound is
// refused and nothing is asked (novox/hq ADR 0259 §10).
func TestStandardInputGoesWithTheLineAndATerminalIsNeverRead(t *testing.T) {
t.Setenv("MESH_VERB", "")
was := standardInput
t.Cleanup(func() { standardInput = was })
secret := "123456789:the-bot-token\n"
standardInput = func() (io.Reader, bool) { return strings.NewReader(secret), false }
asked := engine(t, cli.Answer{Terminal: true})
var out, errOut bytes.Buffer
if code := run(context.Background(), []string{"secret", "accept", "anchor", "telegram", "telegram-token", "--from", "-"},
&out, &errOut); code != 0 {
t.Fatalf("exit %d: %s", code, errOut.String())
}
<-asked
if string(lastStdin) != secret {
t.Fatalf("the engine was given %q", lastStdin)
}
if strings.Contains(out.String()+errOut.String(), "bot-token") {
t.Fatal("mesh-cli printed what it carried")
}
standardInput = func() (io.Reader, bool) { return strings.NewReader("typed"), true }
asked = engine(t, cli.Answer{})
_ = run(context.Background(), []string{"status"}, &out, &errOut)
<-asked
if len(lastStdin) != 0 {
t.Fatalf("a terminal's input was read: %q", lastStdin)
}
standardInput = func() (io.Reader, bool) { return bytes.NewReader(make([]byte, cli.MaxStdin+1)), false }
errOut.Reset()
if code := run(context.Background(), []string{"secret", "accept", "a", "b", "c", "--from", "-"}, &out, &errOut); code == 0 ||
!strings.Contains(errOut.String(), "nothing was asked") {
t.Fatalf("more than the bound was asked: %d %s", code, errOut.String())
// Every command's line is in the help, and none is defined twice.
func TestEveryLineIsInTheHelpOnce(t *testing.T) {
seen := map[string]bool{}
help := usage()
for _, c := range commands {
w := strings.Join(c.words, " ")
if seen[w] {
t.Errorf("nox %s is defined twice", w)
}
seen[w] = true
if c.build != nil && !strings.Contains(help, "nox "+w+" ") {
t.Errorf("nox %s is not in the help", w)
}
if c.build == nil && c.instead == "" {
t.Errorf("nox %s has neither an owner nor an instead", w)
}
if _, dup := unowned[c.words[0]]; dup && len(c.words) == 1 {
t.Errorf("nox %s is both owned and unowned", w)
}
}
}
+1 -9
View File
@@ -2,14 +2,6 @@ module mesh-cli
go 1.26.0
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102004146-dff0ce2abb21
require golang.org/x/crypto v0.57.0
require (
github.com/klauspost/compress v1.20.0 // indirect
github.com/nats-io/nats.go v1.54.0 // indirect
github.com/nats-io/nkeys v0.4.16 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
golang.org/x/sys v0.48.0 // indirect
)
require golang.org/x/sys v0.48.0 // indirect
-12
View File
@@ -1,15 +1,3 @@
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915 h1:x8k7IeMirMTxUo8BH+xUO+MDnHYgU+636jl1NIw1wvs=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101022206-8af6e7266915/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102004146-dff0ce2abb21 h1:4I4ayo0fZYP4PBfEX1sGDDd6arnw5IQJySJ3jcFobPM=
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261102004146-dff0ce2abb21/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA=
github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/nats-io/nats.go v1.54.0 h1:vsXoOxjHp/GmPUN+EcI7uOf/uB+iAP+kEsAFNQN0yzA=
github.com/nats-io/nats.go v1.54.0/go.mod h1:y+DZoD1oBOYfZTU681eTUiUjI0vbqYGixNVFHcjHJ0k=
github.com/nats-io/nkeys v0.4.16 h1:rd5oAuLOb8mnAycB0xleuEBNS1pVVnN0fv/FF34Eypg=
github.com/nats-io/nkeys v0.4.16/go.mod h1:llLgWoI0o4z/Q57q2R1kHfmocyhGV6VG/U18Glg1Afs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M=
golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA=
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=