diff --git a/checks/particulars/main.go b/checks/particulars/main.go index 48b75108..6f0c745b 100644 --- a/checks/particulars/main.go +++ b/checks/particulars/main.go @@ -8,7 +8,7 @@ // CamelCase boundary parts one, so a longer word holding a name is another word. // Three names hold a node name and are not one, so they are blanked before a line is read: the forge's host // (git.novox.be) and organisation path (novox/), the hardware model (asus-zephyrus-g14, ROG Zephyrus G14) -// and the software's own name (Novox Mesh, novox-mesh). +// and the software's own name and npm scope (Novox Mesh, novox-mesh, @novox/mesh-sdk). // // go run . // @@ -33,8 +33,10 @@ var forge = regexp.MustCompile(`(?i)git\.novox\.be(?:/novox/[\w.-]+)?|(?:^|[^/\w // ownNames are the hardware model's and the software's own names, blanked before a line is read: the model // in any separator and case (asus-zephyrus-g14, ASUS_ZEPHYRUS_G14, ROG Zephyrus G14), the software as two -// words or hyphened, whole (Novox Mesh, novox mesh, novox-mesh), so novox-mesh-vault stays a node name joined. -var ownNames = regexp.MustCompile(`(?i)(?:asus[-_ ])?zephyrus[-_ ]g14|(?:^|[^\w-])novox[- ]mesh(?:$|[^\w-])`) +// words or hyphened, whole (Novox Mesh, novox mesh, novox-mesh), so novox-mesh-vault stays a node name joined; +// and the software's npm scope (@novox/mesh-sdk, @novox:registry=), its package path as the Go module path is, +// where no word stands before the @, so a session's who@node (fix-1@novox) is still a node name. +var ownNames = regexp.MustCompile(`(?i)(?:asus[-_ ])?zephyrus[-_ ]g14|(?:^|[^\w-])novox[- ]mesh(?:$|[^\w-])|(?:^|[^\w.-])@novox(?:$|[^\w-])`) func main() { root := "." @@ -173,12 +175,24 @@ func hits(line string, names []string) []string { line = ownNames.ReplaceAllStringFunc(line, blank) var out []string for _, n := range names { - q := regexp.QuoteMeta(n) - camel := regexp.QuoteMeta(strings.ToUpper(n[:1]) + n[1:]) - rx := regexp.MustCompile(`(?i:(?:^|[^A-Za-z0-9])` + q + `(?:$|[^A-Za-z0-9]))|[a-z0-9]` + camel + `(?:$|[^a-z])|(?:^|[^A-Za-z0-9])` + q + `[A-Z0-9]`) - if rx.MatchString(line) { + if matcher(n).MatchString(line) { out = append(out, strings.ToLower(n)) } } return out } + +// matchers holds each name's matcher, compiled once: the check reads every line of every module. +var matchers = map[string]*regexp.Regexp{} + +// matcher is the matcher of one name: as a word, or as a part of a CamelCase identifier. +func matcher(n string) *regexp.Regexp { + if rx, ok := matchers[n]; ok { + return rx + } + q := regexp.QuoteMeta(n) + camel := regexp.QuoteMeta(strings.ToUpper(n[:1]) + n[1:]) + rx := regexp.MustCompile(`(?i:(?:^|[^A-Za-z0-9])` + q + `(?:$|[^A-Za-z0-9]))|[a-z0-9]` + camel + `(?:$|[^a-z])|(?:^|[^A-Za-z0-9])` + q + `[A-Z0-9]`) + matchers[n] = rx + return rx +} diff --git a/checks/particulars/main_test.go b/checks/particulars/main_test.go index ac1c970c..faf8914d 100644 --- a/checks/particulars/main_test.go +++ b/checks/particulars/main_test.go @@ -169,3 +169,32 @@ func TestANodeNameBesideTheHardwareOrTheSoftwareIsRefused(t *testing.T) { } } } + +// 1838/AC2: the software's npm scope (@novox/mesh-sdk, @novox:registry=) is its package path, as the Go module +// path is; a session's who@node naming a node is still a node name. +func TestTheSoftwaresNpmScopeIsNotANodeName(t *testing.T) { + for _, s := range []string{ + "printf '%s\\n' ${shellQuote(`@novox:registry=${registry}`)}", + "const url = `${asked}@novox%2fmesh-sdk`;", + "# The @novox scope resolves from the mesh's own package registry", + `"dependencies": { "@novox/mesh-sdk": "^0.1.0" }`, + } { + if got := scan("f", s, names); len(got) != 0 { + t.Errorf("%q refused: %v", s, got) + } + } + for _, s := range []string{"by fix-1@novox", "jochen@novox:"} { + if got := scan("f", s, names); len(got) != 1 { + t.Errorf("%q gave %v, want one finding", s, got) + } + } +} + +// 1838/AC1: reading every module, the check compiles its matchers once, not once a line: a line read costs a +// few allocations, not a regexp compiled per name (the whole catalogue took 41 s with one per line). +func TestALineIsReadWithoutCompilingAMatcher(t *testing.T) { + line := "a line of code that names nothing at all, with git.novox.be/novox/mesh-sdk in it" + if n := testing.AllocsPerRun(100, func() { hits(line, names) }); n > 20 { + t.Errorf("a line costs %.0f allocations, want at most 20", n) + } +} diff --git a/modules/blueman/README.md b/modules/blueman/README.md index 6d1e6027..5cc1692b 100644 --- a/modules/blueman/README.md +++ b/modules/blueman/README.md @@ -52,7 +52,7 @@ through an injected runner and a fake root in the tests. ## What changes when it is assigned -| | g14 | shanks | +| | node-b | node-d | |---|---|---| | package | none: `blueman` 2.4.6 is installed, explicitly, from `extra` | the same | | start | none: dex starts the applet from the package's entry, in the login session's scope; the tray icon with it | none on disk. **The applet running now came from the predecessor's window-manager line** (a child of i3, since the session of 2026-10-04 16:00). That session began before the `i3` module dropped the line and installed `dex`, so the next login is the first that starts it from the entry | @@ -62,7 +62,7 @@ The workstations are already in the state this module describes. ## Migration (ADR 0182) -Nothing is required on either machine. On shanks, log out and in once, or run `blueman_restart`, and +Nothing is required on either machine. On node-d, log out and in once, or run `blueman_restart`, and the applet runs from its one start. `blueman_check` then answers `ok`. ## Leaves as found diff --git a/modules/brother/cmd/brother/main.go b/modules/brother/cmd/brother/main.go index 34f0d577..87f82b0c 100644 --- a/modules/brother/cmd/brother/main.go +++ b/modules/brother/cmd/brother/main.go @@ -160,7 +160,7 @@ func tools(settingsFile string, now func() time.Time) []stdio.Tool { Input: map[string]any{"type": "object", "properties": map[string]any{ "object": str("a stored document's key: a scan's key, as scan.added names it, or the key node-files.upload answered"), "path": str("an absolute path of a file on the machine node names"), - "node": str("the machine path is on, such as g14"), + "node": str("the machine path is on, such as node-b"), "url": str("an http or https address of a document or a page"), "ask": str("the ask id of an earlier print that was answered as started: alone, it reads that print's outcome"), "copies": num("1 to 99 (default 1)"), diff --git a/modules/claude-code/cmd/claude-code/memory.go b/modules/claude-code/cmd/claude-code/memory.go index 0315301d..f6479f5f 100644 --- a/modules/claude-code/cmd/claude-code/memory.go +++ b/modules/claude-code/cmd/claude-code/memory.go @@ -813,7 +813,7 @@ func (m *MemoryCarrier) unwritten(key, project, path string, err error) { } // OlderBeside is where the older of two versions too large to share one file is kept: beside the file, named for -// the node it was changed on ("MEMORY.md" from shanks: "MEMORY.older-from-shanks.md"). +// the node it was changed on ("MEMORY.md" from node-d: "MEMORY.older-from-node-d.md"). func OlderBeside(path, node string) string { ext := filepath.Ext(path) return strings.TrimSuffix(path, ext) + ".older-from-" + node + ext @@ -900,7 +900,7 @@ func (m *MemoryCarrier) Take(key, op string, value json.RawMessage, revision uin } // Only noted here, never applied: the runtime hands the watch's values over one at a time and gives the whole // current set ten seconds, so a watch whose handler reads the state, waits for a look or writes a file never - // starts on a node with a few hundred memory files (issue 367, seen on shanks on 2026-10-13). Drain applies + // starts on a node with a few hundred memory files (issue 367, seen on node-d on 2026-10-13). Drain applies // what was noted, the newest revision of each key. m.queueMu.Lock() if m.queue == nil { diff --git a/modules/claude-code/cmd/claude-code/proposal_tools.go b/modules/claude-code/cmd/claude-code/proposal_tools.go index 84af4558..f0945b3d 100644 --- a/modules/claude-code/cmd/claude-code/proposal_tools.go +++ b/modules/claude-code/cmd/claude-code/proposal_tools.go @@ -117,7 +117,7 @@ func proposalTools(p Paths) []stdio.Tool { Input: map[string]any{ "scope": str("mesh (every node) or node (the nodes given, or this one)"), "nodes": str("for scope node: a comma-separated list of nodes; absent is this node"), - "name": str("the section's name: lower-case letters, digits and -, e.g. handoff-from-g14"), + "name": str("the section's name: lower-case letters, digits and -, e.g. handoff-from-node-b"), "text": str("the section's text, markdown, as every session will read it"), "remove": map[string]any{"type": "boolean", "description": "propose removing the approved section of this name and scope instead"}, }, @@ -173,7 +173,7 @@ func questionTools(p Paths, notLaunched error) []stdio.Tool { }, "required": []string{"label", "means"}}}, "why": str("why the recommended option is the one you recommend, at most 120 bytes"), "on_expiry": str("what you do if nobody answers before it expires, e.g. 'I leave the pull request open'"), - "by": str("required: your session's name, as who@node or who, e.g. fix-589@g14"), + "by": str("required: your session's name, as who@node or who, e.g. fix-589@node-b"), "expires_minutes": map[string]any{"type": "integer", "description": "minutes until it expires, 5 to 1430; 60 when absent"}, }, diff --git a/modules/claude-code/cmd/claude-code/questions.go b/modules/claude-code/cmd/claude-code/questions.go index 7247d352..24dfed3c 100644 --- a/modules/claude-code/cmd/claude-code/questions.go +++ b/modules/claude-code/cmd/claude-code/questions.go @@ -351,7 +351,7 @@ func AskQuestion(p Paths, in QuestionInput, store QuestionStore, bus AskBus, now by = who } if by == "" { - return refuse("it does not say who asks: give by, your session's name as who@node, such as fix-589@g14") + return refuse("it does not say who asks: give by, your session's name as who@node, such as fix-589@node-b") } life := in.Expires if life == 0 { diff --git a/modules/lab/files/lab.env b/modules/lab/files/lab.env index 35aa1a29..a94f435b 100644 --- a/modules/lab/files/lab.env +++ b/modules/lab/files/lab.env @@ -1,3 +1,4 @@ MESH_LAB_FORGE=${config:forge} +MESH_LAB_FORGE_OWNER=${config:forge-owner} MESH_LAB_FORGE_BINDING=${dir:state}/package-registry.json MESH_LAB_FORGE_SECRET=${dir:state}/package-registry.secret diff --git a/modules/lab/module.json b/modules/lab/module.json index ef82b87c..856f328b 100644 --- a/modules/lab/module.json +++ b/modules/lab/module.json @@ -37,6 +37,12 @@ "description": "The forge the lab's machines take their packages from, as its address: the operator's, with no default (novox/hq ADR 0112).", "trusted": true, "required": true + }, + "forge-owner": { + "type": "string", + "description": "The owner on the forge the mesh's repositories and npm packages are under, such as the organisation that holds them: the operator's, with no default (novox/hq ADR 0507 §9).", + "trusted": true, + "required": true } }, "resources": [ diff --git a/modules/lab/test/base.test.ts b/modules/lab/test/base.test.ts index 93a6f8ec..7a7462e6 100644 --- a/modules/lab/test/base.test.ts +++ b/modules/lab/test/base.test.ts @@ -60,8 +60,8 @@ test("no run starts while the base image is rebuilt", () => { }); test("a rebuild is mesh-lab's base build from the forge's main, with no secret in its script", () => { - const s = baseScript("/work/base-x", "https://forge.invalid", "/work/.base-x.git-credentials"); - assert.match(s, /clone --quiet --depth 1 'https:\/\/forge.invalid\/novox\/mesh-lab.git'/); + const s = baseScript("/work/base-x", "https://forge.invalid", "mesh-org", "/work/.base-x.git-credentials"); + assert.match(s, /clone --quiet --depth 1 'https:\/\/forge.invalid\/mesh-org\/mesh-lab.git'/); assert.match(s, /src\/cli.ts base build/); assert.ok(s.indexOf("rm -f '/work/.base-x.git-credentials'") < s.indexOf("npm ci"), "the credential outlives the clone"); }); diff --git a/modules/lab/test/owner.test.ts b/modules/lab/test/owner.test.ts new file mode 100644 index 00000000..c0497c7a --- /dev/null +++ b/modules/lab/test/owner.test.ts @@ -0,0 +1,26 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { readdirSync, readFileSync } from "node:fs"; +import { join } from "node:path"; + +// The forge's owner differs between meshes (novox/hq ADR 0507 §9, issue 1838): the lab takes it as a config value +// the operator gives, with no default, and no file of the lab names it. +// 1838/AC4 + +const here = join(import.meta.dirname, ".."); + +test("the forge's owner is a required config value with no default, and reaches the lab's env-file", () => { + const manifest = JSON.parse(readFileSync(join(here, "module.json"), "utf8")) as { config: Record> }; + const owner = manifest.config["forge-owner"]; + assert.ok(owner, "the manifest declares no forge-owner"); + assert.equal(owner.required, true); + assert.ok(!("default" in owner), `forge-owner has the default ${JSON.stringify(owner.default)}`); + assert.match(readFileSync(join(here, "files", "lab.env"), "utf8"), /^MESH_LAB_FORGE_OWNER=\$\{config:forge-owner\}$/m); +}); + +test("no file of the lab's code names an owner on the forge", () => { + for (const f of readdirSync(join(here, "tools")).filter((f) => f.endsWith(".ts"))) { + const src = readFileSync(join(here, "tools", f), "utf8"); + assert.doesNotMatch(src, /\}\/[a-z][\w.-]*\/\$\{repo\}|\}\/[a-z][\w.-]*\/mesh-lab\.git|\/api\/packages\/[a-z][\w.-]*\/npm\//, f); + } +}); diff --git a/modules/lab/test/runs.test.ts b/modules/lab/test/runs.test.ts index b2071208..5c478b0d 100644 --- a/modules/lab/test/runs.test.ts +++ b/modules/lab/test/runs.test.ts @@ -92,8 +92,8 @@ test("a run fails at its setup, naming the checkout, when nothing of it can be i // checkouts are. test("a run checks out mesh-tools-go and says where its checkouts are", () => { assert.ok(REPOSITORIES.includes("mesh-tools-go")); - const s = script("/work", "run-x", "http://forge", ["test/integration/genesis-keeps-its-bus.test.ts"], {}); - assert.match(s, /novox\/mesh-tools-go\.git' '\/work\/run-x\/mesh-tools-go'/); + const s = script("/work", "run-x", "http://forge", "mesh-org", ["test/integration/genesis-keeps-its-bus.test.ts"], {}); + assert.match(s, /mesh-org\/mesh-tools-go\.git' '\/work\/run-x\/mesh-tools-go'/); assert.match(s, /export MESH_LAB_REPOS='\/work\/run-x'/); assert.doesNotMatch(s, /cd '\/work\/run-x'\/\$repo && npm ci/); for (const repo of NPM_REPOSITORIES) assert.ok(s.includes(`'/work/run-x/${repo}'/package-lock.json`), repo); @@ -113,22 +113,22 @@ function aBinding(as: string, secret: string): { binding: string; secret: string } test("the forge credential is the bound account and its sealed secret, for the forge's own origin", () => { - const b = aBinding("lab-shanks", "s3cret-of-forty-characters"); - assert.equal(forgeCredential("http://novox.internal:20000", b.binding, b.secret), - "http://lab-shanks:s3cret-of-forty-characters@novox.internal:20000"); - assert.equal(forgeCredential("http://novox.internal:20000/", b.binding, ""), undefined); + const b = aBinding("lab-node-d", "s3cret-of-forty-characters"); + assert.equal(forgeCredential("http://node-c.internal:20000", b.binding, b.secret), + "http://lab-node-d:s3cret-of-forty-characters@node-c.internal:20000"); + assert.equal(forgeCredential("http://node-c.internal:20000/", b.binding, ""), undefined); assert.equal(forgeCredential("", b.binding, b.secret), undefined); - assert.equal(forgeCredential("http://novox.internal:20000", "/nowhere", b.secret), undefined); + assert.equal(forgeCredential("http://node-c.internal:20000", "/nowhere", b.secret), undefined); }); test("a run clones through git's credential store, and its script carries no secret", () => { - const b = aBinding("lab-shanks", "s3cret-of-forty-characters"); - const credential = forgeCredential("http://novox.internal:20000", b.binding, b.secret); + const b = aBinding("lab-node-d", "s3cret-of-forty-characters"); + const credential = forgeCredential("http://node-c.internal:20000", b.binding, b.secret); const path = credentialPath("/work", "run-x"); - const s = script("/work", "run-x", "http://novox.internal:20000", ["t.test.ts"], {}, path); + const s = script("/work", "run-x", "http://node-c.internal:20000", "mesh-org", ["t.test.ts"], {}, path); assert.ok(credential); assert.doesNotMatch(s, /s3cret/); - assert.match(s, /git -c credential\.helper= -c 'credential\.helper=store --file=\/work\/\.run-x\.git-credentials' clone --quiet --depth 50 --branch 'main' 'http:\/\/novox\.internal:20000\/novox\/mesh-tools-go\.git'/); + assert.match(s, /git -c credential\.helper= -c 'credential\.helper=store --file=\/work\/\.run-x\.git-credentials' clone --quiet --depth 50 --branch 'main' 'http:\/\/node-c\.internal:20000\/mesh-org\/mesh-tools-go\.git'/); assert.match(s, /export GIT_TERMINAL_PROMPT=0/); assert.equal(path.startsWith("/work/run-x/"), false, "the credential is kept out of the run's own directory"); assert.equal(credentialArgs(undefined), ""); @@ -139,10 +139,10 @@ test("a run clones through git's credential store, and its script carries no sec test("git presents the stored credential to the forge, and only when one is given", () => { const dir = mkdtempSync(join(tmpdir(), "lab-git-")); const store = join(dir, "git-credentials"); - writeFileSync(store, "http://lab-shanks:pw@forge.invalid:20000\n", { mode: 0o600 }); + writeFileSync(store, "http://lab-node-d:pw@forge.invalid:20000\n", { mode: 0o600 }); const fill = spawnSync("sh", ["-c", `printf 'protocol=http\\nhost=forge.invalid:20000\\n\\n' | git ${credentialArgs(store)}credential fill`], { encoding: "utf8", env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } }); - assert.match(fill.stdout, /username=lab-shanks/); + assert.match(fill.stdout, /username=lab-node-d/); assert.match(fill.stdout, /password=pw/); const other = spawnSync("sh", ["-c", `printf 'protocol=http\\nhost=elsewhere.invalid\\n\\n' | git ${credentialArgs(store)}credential fill`], { encoding: "utf8", env: { ...process.env, GIT_TERMINAL_PROMPT: "0" } }); @@ -168,7 +168,7 @@ test("a stopped script removes the credential, whatever ends it", () => { }); test("the script removes the credential as soon as its clones are done", () => { - const s = script("/work", "run-x", "http://forge", ["t.test.ts"], {}, "/work/.run-x.git-credentials"); + const s = script("/work", "run-x", "http://forge", "mesh-org", ["t.test.ts"], {}, "/work/.run-x.git-credentials"); const lastClone = s.lastIndexOf("clone --quiet"); const removed = s.indexOf("rm -f '/work/.run-x.git-credentials'", lastClone); const building = s.indexOf("npm ci"); @@ -252,9 +252,9 @@ test("the lab's tools bundle names its bound credential and binding in its env", assert.equal(env.MESH_LAB_FORGE_BINDING, m.binds["npm-package-registry"]); }); -// The next setup failure of novox/hq issue 624 (run-2026-10-13T13-15-26-634Z on shanks): npm 12 refused +// The next setup failure of novox/hq issue 624 (run-2026-10-13T13-15-26-634Z on node-d): npm 12 refused // mesh-tools' locked @novox/mesh-sdk with EALLOWREMOTE. The lab named the registry by its forge setting's -// address (http://novox.internal:20000/…), the lock file holds the address the forge writes every tarball +// address (http://node-c.internal:20000/…), the lock file holds the address the forge writes every tarball // URL at (https://git.novox.be/…), and npm 12 installs a locked tarball only from its configured registry. // A run names the registry by the address the registry names itself by. import { npmRegistry } from "../tools/runs.ts"; @@ -270,54 +270,54 @@ function aRegistry(answer: () => Response | Promise): { get: typeof fe const packument = (tarball: string) => new Response(JSON.stringify({ name: "@novox/mesh-sdk", "dist-tags": { latest: "0.1.14" }, - versions: { "0.1.1": { dist: { tarball: "http://old.example/api/packages/novox/npm/x.tgz" } }, "0.1.14": { dist: { tarball } } }, + versions: { "0.1.1": { dist: { tarball: "http://old.example/api/packages/mesh-org/npm/x.tgz" } }, "0.1.14": { dist: { tarball } } }, }), { status: 200, headers: { "content-type": "application/json" } }); test("a run names the package registry by the address the registry gives its tarballs", async () => { - const r = aRegistry(() => packument("https://forge.example.org/api/packages/novox/npm/%40novox%2Fmesh-sdk/-/0.1.14/mesh-sdk-0.1.14.tgz")); - const got = await npmRegistry("http://forge.example.org:3000/", r.get, "https://forge.example.org/api/packages/novox/npm/"); - assert.deepEqual(got, { registry: "https://forge.example.org/api/packages/novox/npm/" }); - assert.deepEqual(r.asked, ["http://forge.example.org:3000/api/packages/novox/npm/@novox%2fmesh-sdk"]); + const r = aRegistry(() => packument("https://forge.example.org/api/packages/mesh-org/npm/%40novox%2Fmesh-sdk/-/0.1.14/mesh-sdk-0.1.14.tgz")); + const got = await npmRegistry("http://forge.example.org:3000/", "mesh-org", r.get, "https://forge.example.org/api/packages/mesh-org/npm/"); + assert.deepEqual(got, { registry: "https://forge.example.org/api/packages/mesh-org/npm/" }); + assert.deepEqual(r.asked, ["http://forge.example.org:3000/api/packages/mesh-org/npm/@novox%2fmesh-sdk"]); // The forge's own scheme is taken too, with its port. - const plain = await npmRegistry("http://forge.example.org:3000", - aRegistry(() => packument("http://forge.example.org:3000/api/packages/novox/npm/%40novox%2Fmesh-sdk/-/0.1.14/mesh-sdk-0.1.14.tgz")).get); - assert.deepEqual(plain, { registry: "http://forge.example.org:3000/api/packages/novox/npm/" }); + const plain = await npmRegistry("http://forge.example.org:3000", "mesh-org", + aRegistry(() => packument("http://forge.example.org:3000/api/packages/mesh-org/npm/%40novox%2Fmesh-sdk/-/0.1.14/mesh-sdk-0.1.14.tgz")).get); + assert.deepEqual(plain, { registry: "http://forge.example.org:3000/api/packages/mesh-org/npm/" }); }); // What the registry answers may come over plain HTTP, so it never decides where packages come from: only // the forge's public root or its own address, scheme, host and port, under the npm path, with nothing after // it (review of mesh-catalog #275, ADR 0346). test("a registry that does not give an address the lab takes leaves the forge's, and the reason is said", async () => { - const sdk = "/api/packages/novox/npm/%40novox%2Fmesh-sdk/-/0.1.14/mesh-sdk-0.1.14.tgz"; + const sdk = "/api/packages/mesh-org/npm/%40novox%2Fmesh-sdk/-/0.1.14/mesh-sdk-0.1.14.tgz"; for (const [answer, why] of [ [() => new Response("not found", { status: 404 }), /it answered 404/], [() => new Response("{}", { status: 200 }), /no tarball/], [() => { throw new Error("connect ECONNREFUSED"); }, /ECONNREFUSED/], - [() => packument(`https://forge.example.org/elsewhere/mesh-sdk-0.1.14.tgz`), /not under \/api\/packages\/novox\/npm\//], + [() => packument(`https://forge.example.org/elsewhere/mesh-sdk-0.1.14.tgz`), /not under \/api\/packages\/mesh-org\/npm\//], [() => packument(`https://attacker.example.net${sdk}`), /at https:\/\/attacker\.example\.net, which is neither the forge's public root nor its own address/], - [() => packument(`https://attacker.example.net/x?u=https://novox.internal${sdk}`), /query or a fragment/], - [() => packument(`https://novox.internal${sdk}?x=1`), /query or a fragment/], - [() => packument(`https://novox.internal${sdk}#frag`), /query or a fragment/], - [() => packument(`https://attacker.example.net/#https://novox.internal${sdk}`), /query or a fragment/], - [() => packument(`ftp://novox.internal${sdk}`), /at ftp:\/\/novox\.internal, which is neither/], - [() => packument(`file:///api/packages/novox/npm/x.tgz`), /which is neither/], - [() => packument(`https://novox.internal/api/packages/novox/npm/\nregistry=https://attacker.example.net/\nallow-remote=all`), /whitespace or a control character/], - [() => packument(`https://user:pw@novox.internal${sdk}`), /user or password/], + [() => packument(`https://attacker.example.net/x?u=https://node-c.internal${sdk}`), /query or a fragment/], + [() => packument(`https://node-c.internal${sdk}?x=1`), /query or a fragment/], + [() => packument(`https://node-c.internal${sdk}#frag`), /query or a fragment/], + [() => packument(`https://attacker.example.net/#https://node-c.internal${sdk}`), /query or a fragment/], + [() => packument(`ftp://node-c.internal${sdk}`), /at ftp:\/\/node-c\.internal, which is neither/], + [() => packument(`file:///api/packages/mesh-org/npm/x.tgz`), /which is neither/], + [() => packument(`https://node-c.internal/api/packages/mesh-org/npm/\nregistry=https://attacker.example.net/\nallow-remote=all`), /whitespace or a control character/], + [() => packument(`https://user:pw@node-c.internal${sdk}`), /user or password/], ] as [() => Response, RegExp][]) { - const got = await npmRegistry("http://novox.internal:20000", aRegistry(answer).get); - assert.equal(got.registry, "http://novox.internal:20000/api/packages/novox/npm/", String(why)); + const got = await npmRegistry("http://node-c.internal:20000", "mesh-org", aRegistry(answer).get); + assert.equal(got.registry, "http://node-c.internal:20000/api/packages/mesh-org/npm/", String(why)); assert.match(got.problem ?? "", /did not give an address the lab takes/); assert.match(got.problem ?? "", why); } }); test("a run's npm configuration names the registry it was given, and the forge's own address only without one", () => { - const given = script("/work", "run-x", "http://novox.internal:20000", ["t.test.ts"], {}, undefined, - "https://forge.example.org/api/packages/novox/npm/"); - assert.match(given, / '@novox:registry=https:\/\/forge\.example\.org\/api\/packages\/novox\/npm\/' > '\/work\/run-x\/\.npmrc'\n/); - assert.doesNotMatch(given, /@novox:registry=http:\/\/novox\.internal/); - const none = script("/work", "run-x", "http://novox.internal:20000", ["t.test.ts"], {}); - assert.match(none, /'@novox:registry=http:\/\/novox\.internal:20000\/api\/packages\/novox\/npm\/'/); + const given = script("/work", "run-x", "http://node-c.internal:20000", "mesh-org", ["t.test.ts"], {}, undefined, + "https://forge.example.org/api/packages/mesh-org/npm/"); + assert.match(given, / '@novox:registry=https:\/\/forge\.example\.org\/api\/packages\/mesh-org\/npm\/' > '\/work\/run-x\/\.npmrc'\n/); + assert.doesNotMatch(given, /@novox:registry=http:\/\/node-c\.internal/); + const none = script("/work", "run-x", "http://node-c.internal:20000", "mesh-org", ["t.test.ts"], {}); + assert.match(none, /'@novox:registry=http:\/\/node-c\.internal:20000\/api\/packages\/mesh-org\/npm\/'/); }); // The public root the forge serves in the lab's binding (novox/hq ADR 0346) is the one other origin a @@ -325,20 +325,20 @@ test("a run's npm configuration names the registry it was given, and the forge's import { publicRootOf } from "../tools/runs.ts"; test("a tarball is taken at the forge's public root, and at no other port or scheme of its host", async () => { - const sdk = "/api/packages/novox/npm/%40novox%2Fmesh-sdk/-/0.1.14/mesh-sdk-0.1.14.tgz"; - const root = "https://git.example.org/api/packages/novox/npm/"; + const sdk = "/api/packages/mesh-org/npm/%40novox%2Fmesh-sdk/-/0.1.14/mesh-sdk-0.1.14.tgz"; + const root = "https://git.example.org/api/packages/mesh-org/npm/"; const at = (tarball: string, publicRoot?: string) => - npmRegistry("http://novox.internal:20000", aRegistry(() => packument(tarball)).get, publicRoot); + npmRegistry("http://node-c.internal:20000", "mesh-org", aRegistry(() => packument(tarball)).get, publicRoot); assert.deepEqual(await at(`https://git.example.org${sdk}`, root), { registry: root }); for (const tarball of [`https://git.example.org:8443${sdk}`, `http://git.example.org${sdk}`, `https://example.org${sdk}`]) { const got = await at(tarball, root); - assert.equal(got.registry, "http://novox.internal:20000/api/packages/novox/npm/", tarball); + assert.equal(got.registry, "http://node-c.internal:20000/api/packages/mesh-org/npm/", tarball); assert.match(got.problem ?? "", /neither the forge's public root nor its own address/, tarball); } // Without a public root, the forge's public name is not trusted on its own word. assert.match((await at(`https://git.example.org${sdk}`)).problem ?? "", /neither/); // And the forge's own address only at its own port. - assert.match((await at(`http://novox.internal${sdk}`, root)).problem ?? "", /neither/); + assert.match((await at(`http://node-c.internal${sdk}`, root)).problem ?? "", /neither/); }); test("the public root is read from the binding, and only an https root at the npm path is taken", () => { @@ -346,39 +346,39 @@ test("the public root is read from the binding, and only an https root at the np const binding = (root: unknown) => { const f = join(dir, `b-${Math.random()}.json`); writeFileSync(f, JSON.stringify({ provision: "npm-package-registry", at: "home", serves: { scheme: "http", port: 3000, - "npm-path": "/api/packages/novox/npm/", ...(root === undefined ? {} : { "public-root": root }) } })); + "npm-path": "/api/packages/mesh-org/npm/", ...(root === undefined ? {} : { "public-root": root }) } })); return f; }; - assert.deepEqual(publicRootOf(binding("https://git.example.org/api/packages/novox/npm/")), - { root: "https://git.example.org/api/packages/novox/npm/" }); - assert.deepEqual(publicRootOf(binding("https://git.example.org:8443/api/packages/novox/npm/")), - { root: "https://git.example.org:8443/api/packages/novox/npm/" }); + assert.deepEqual(publicRootOf(binding("https://git.example.org/api/packages/mesh-org/npm/"), "mesh-org"), + { root: "https://git.example.org/api/packages/mesh-org/npm/" }); + assert.deepEqual(publicRootOf(binding("https://git.example.org:8443/api/packages/mesh-org/npm/"), "mesh-org"), + { root: "https://git.example.org:8443/api/packages/mesh-org/npm/" }); for (const [root, why] of [ [undefined, /serves no public-root/], - ["http://git.example.org/api/packages/novox/npm/", /is not https:\/\//], + ["http://git.example.org/api/packages/mesh-org/npm/", /is not https:\/\//], ["https://git.example.org/elsewhere/", /is not https:\/\//], - ["https://git.example.org/api/packages/novox/npm/?x=1", /is not https:\/\//], - ["https://u:p@git.example.org/api/packages/novox/npm/", /is not https:\/\//], - ["https://git.example.org/api/packages/novox/npm/\nallow-remote=all", /whitespace or a control character/], - ["https://${bound:route:name-web}/api/packages/novox/npm/", /not a URL|is not https/], + ["https://git.example.org/api/packages/mesh-org/npm/?x=1", /is not https:\/\//], + ["https://u:p@git.example.org/api/packages/mesh-org/npm/", /is not https:\/\//], + ["https://git.example.org/api/packages/mesh-org/npm/\nallow-remote=all", /whitespace or a control character/], + ["https://${bound:route:name-web}/api/packages/mesh-org/npm/", /not a URL|is not https/], ] as [unknown, RegExp][]) { - const got = publicRootOf(binding(root)); + const got = publicRootOf(binding(root), "mesh-org"); assert.equal(got.root, undefined, String(root)); assert.match(got.problem ?? "", why, String(root)); } - assert.match(publicRootOf(join(dir, "none.json")).problem ?? "", /could not be read/); - assert.match(publicRootOf(undefined).problem ?? "", /no npm-package-registry binding/); + assert.match(publicRootOf(join(dir, "none.json"), "mesh-org").problem ?? "", /could not be read/); + assert.match(publicRootOf(undefined, "mesh-org").problem ?? "", /no npm-package-registry binding/); rmSync(dir, { recursive: true, force: true }); }); // A forge setting that is not http(s) is never a trusted origin (review of #280): a `file:` tarball's origin // is "null", as is a `file:` forge's, and the two must not match. test("a forge setting that is not http(s) trusts no origin of its own", async () => { - const got = await npmRegistry("file:///srv/forge", aRegistry(() => packument("file:///api/packages/novox/npm/x.tgz")).get); + const got = await npmRegistry("file:///srv/forge", "mesh-org", aRegistry(() => packument("file:///api/packages/mesh-org/npm/x.tgz")).get); assert.match(got.problem ?? "", /neither the forge's public root nor its own address/); - const root = "https://git.example.org/api/packages/novox/npm/"; - const viaRoot = await npmRegistry("ftp://novox.internal", aRegistry(() => - packument("ftp://novox.internal/api/packages/novox/npm/x.tgz")).get, root); + const root = "https://git.example.org/api/packages/mesh-org/npm/"; + const viaRoot = await npmRegistry("ftp://node-c.internal", "mesh-org", aRegistry(() => + packument("ftp://node-c.internal/api/packages/mesh-org/npm/x.tgz")).get, root); assert.match(viaRoot.problem ?? "", /neither/); }); @@ -401,7 +401,7 @@ test("every file takes its text from a template in files/", () => { }); // Defends novox/hq issue 693: a run whose only test skipped as NOT REACHED read "0 passed, 0 failed", exit 0, -// and lab_status said passed. The log's tail, as run-2026-10-14T00-21-44-014Z on shanks wrote it. +// and lab_status said passed. The log's tail, as run-2026-10-14T00-21-44-014Z on node-d wrote it. const oldLog = [ "STOPPED at step 16 of 24, base: \t\t\t\tMake sure you have all the referenced content and try again.", "\ufe63 genesis adopts its bus as the nats module in place: one bus, its streams kept, the node connected (588152.016874ms) # ADR 0333 §5 NOT REACHED: the first run stopped at step 16 of 24, base, before the broker step:", diff --git a/modules/lab/tools/index.ts b/modules/lab/tools/index.ts index 63c7c41f..bd18ec0f 100644 --- a/modules/lab/tools/index.ts +++ b/modules/lab/tools/index.ts @@ -8,7 +8,7 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { - checkDir, cleanCredentials, clearChecks, credentialArgs, forgeCredential, installPlan, listRuns, npmRegistry, NPM_REPOSITORIES, publicRootOf, readStatus, REPOSITORIES, running, start, + checkDir, repositoryURL, cleanCredentials, clearChecks, credentialArgs, forgeCredential, installPlan, listRuns, npmRegistry, NPM_REPOSITORIES, publicRootOf, readStatus, REPOSITORIES, running, start, stop, tail, writeCredential, baseBuildLog, buildingBase, listBaseBuilds, startBaseBuild, credentialPath, launchRun, launchRebuild, BASE_MAX_AGE_DAYS, describeBase, readBaseImage, baseProblem, } from "./runs.js"; @@ -19,13 +19,17 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] { // ADR 0192): read from the env-file the mesh fills, at each call, so a changed setting is used // without restarting the runtime. MESH_LAB_FORGE itself still wins, for a hand-run instance. const forgeOf = (): string => (env.MESH_LAB_FORGE ?? wordIn(env.MESH_LAB_ENV_FILE, "MESH_LAB_FORGE")).replace(/\/+$/, ""); + // The owner on the forge the mesh's repositories and packages are under, the operator's too (novox/hq ADR 0507 + // §9), read the same way. + const ownerOf = (): string => (env.MESH_LAB_FORGE_OWNER ?? wordIn(env.MESH_LAB_ENV_FILE, "MESH_LAB_FORGE_OWNER")).trim(); + const noOwner = "the lab's forge owner is not set: config for lab, {\"forge-owner\": \"\"}"; // The forge credential the mesh binds and seals to the lab (novox/hq issue 624), read at each call. // The registry's public root the forge serves in the lab's binding (novox/hq ADR 0346), read at each call. const bindingOf = (): string | undefined => env.MESH_LAB_FORGE_BINDING ?? (wordIn(env.MESH_LAB_ENV_FILE, "MESH_LAB_FORGE_BINDING") || undefined); - const registryOf = async (forge: string) => { - const root = publicRootOf(bindingOf()); - const registry = await npmRegistry(forge, fetch, root.root); + const registryOf = async (forge: string, owner: string) => { + const root = publicRootOf(bindingOf(), owner); + const registry = await npmRegistry(forge, owner, fetch, root.root); return registry.problem && root.problem ? { ...registry, problem: `${registry.problem}; ${root.problem}` } : registry; }; const credentialOf = (forge: string): string | undefined => forgeCredential(forge, @@ -39,6 +43,8 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] { run: async () => { const forge = forgeOf(); if (!forge) return { ok: false, output: "the lab's forge is not set: settings for lab, {\"forge\": \"\"}" }; + const owner = ownerOf(); + if (!owner) return { ok: false, output: noOwner }; // What earlier checks left is cleared, and what could not be is said; this check works in a fresh // directory of its own either way, and removes it when it is done. const leftovers = clearChecks(work); @@ -46,13 +52,13 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] { let check: ReturnType; let problems: string[]; try { - const clone = spawnSync("git", ["clone", "--quiet", "--depth", "1", `${forge}/novox/mesh-lab.git`, dir], { encoding: "utf8" }); + const clone = spawnSync("git", ["clone", "--quiet", "--depth", "1", repositoryURL(forge, owner, "mesh-lab"), dir], { encoding: "utf8" }); if (clone.status !== 0) return { ok: false, output: [String(clone.stderr).trim(), ...leftovers].join("\n") }; spawnSync("npm", ["ci", "--no-audit", "--no-fund", "--loglevel=error"], { cwd: dir, encoding: "utf8" }); check = spawnSync("node", ["--experimental-strip-types", "src/cli.ts", "check"], { cwd: dir, encoding: "utf8" }); - problems = setupProblems(forge, `${dir}/setup`, credentialOf(forge)); + problems = setupProblems(forge, owner, `${dir}/setup`, credentialOf(forge)); // And the address a run names the package registry by (npmRegistry): said when the registry does not say it. - const registry = await registryOf(forge); + const registry = await registryOf(forge, owner); if (registry.problem) problems.push(registry.problem); } finally { rmSync(dir, { recursive: true, force: true }); @@ -91,6 +97,8 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] { run: async (args) => { const forge = forgeOf(); if (!forge) return { started: false, reason: "the lab's forge is not set: settings for lab, {\"forge\": \"\"}" }; + const owner = ownerOf(); + if (!owner) return { started: false, reason: noOwner }; const tests = String(args.tests ?? "").split(",").map((s) => s.trim()).filter(Boolean); if (tests.length === 0) return { started: false, reason: "name at least one bed test file" }; let refs: Record = {}; @@ -105,8 +113,8 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] { if (stranger.length > 0) return { started: false, reason: `the lab does not build ${stranger.join(", ")}` }; // **One job at a time, not on a base image past its bound, nor while it is rebuilt** (novox/hq issue 698, // review 256): the registry asked first, then the check and the start with nothing awaited between. - const launched = await launchRun(work, { registry: () => registryOf(forge), base: () => readBaseImage() }, - (registry) => start(work, forge, tests, refs, credentialOf(forge), registry)); + const launched = await launchRun(work, { registry: () => registryOf(forge, owner), base: () => readBaseImage() }, + (registry) => start(work, forge, owner, tests, refs, credentialOf(forge), registry)); if (!launched.started) return launched; return { started: true, run: launched.job, base: describeBase(launched.base), registry: launched.registry, ...(launched.problem ? { problem: launched.problem } : {}) }; @@ -134,10 +142,12 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] { if (args.rebuild !== true) return reading; const forge = forgeOf(); if (!forge) return { ...reading, started: false, reason: "the lab's forge is not set: settings for lab, {\"forge\": \"\"}" }; + const owner = ownerOf(); + if (!owner) return { ...reading, started: false, reason: noOwner }; // Not under a run, nor beside another rebuild: the registry asked first, then the check and the start with // nothing awaited between (review 256). - const launched = await launchRebuild(work, { registry: () => registryOf(forge), base: () => before }, - (registry) => startBaseBuild(work, forge, credentialOf(forge), registry)); + const launched = await launchRebuild(work, { registry: () => registryOf(forge, owner), base: () => before }, + (registry) => startBaseBuild(work, forge, owner, credentialOf(forge), registry)); if (launched.started === false) return { ...reading, started: false, reason: launched.reason }; return { ...reading, started: true, build: launched.job }; }, @@ -174,7 +184,7 @@ export function getLabTools(env: NodeJS.ProcessEnv): ToolDefinition[] { * answer for, and a checkout it installs with nothing installable (installPlan). Empty when a run's setup * would go through. */ -export function setupProblems(forge: string, dir: string, credential?: string): string[] { +export function setupProblems(forge: string, owner: string, dir: string, credential?: string): string[] { const problems: string[] = []; spawnSync("rm", ["-rf", dir]); // The credential a run would offer, in a file of its own outside the check's clones, removed after. @@ -186,7 +196,7 @@ export function setupProblems(forge: string, dir: string, credential?: string): }); try { for (const repo of REPOSITORIES) { - const url = `${forge}/novox/${repo}.git`; + const url = repositoryURL(forge, owner, repo); const remote = git(["ls-remote", "--heads", url, "main"]); if (remote.status !== 0 || !remote.stdout.includes("refs/heads/main")) { const said = remote.stderr.trim().split("\n")[0] ?? ""; diff --git a/modules/lab/tools/runs.ts b/modules/lab/tools/runs.ts index c65bfb41..76095ff2 100644 --- a/modules/lab/tools/runs.ts +++ b/modules/lab/tools/runs.ts @@ -60,8 +60,11 @@ export function installStep(root: string, build: boolean): string { fi`; } -/** Where the forge serves the mesh's npm packages, under its own address (gitea's npm registry for the novox org). */ -export const NPM_PATH = "/api/packages/novox/npm/"; +/** + * Where the forge serves the mesh's npm packages, under its own address: gitea's npm registry for the owner the + * mesh's repositories are under, the operator's config value (novox/hq ADR 0507 §9). + */ +export const npmPath = (owner: string): string => `/api/packages/${encodeURIComponent(owner)}/npm/`; /** * The registry's public root, as the forge serves it in the lab's npm-package-registry binding @@ -70,7 +73,8 @@ export const NPM_PATH = "/api/packages/novox/npm/"; * nothing else. Undefined, with why, when the binding has none or it is not that shape — a forge on a * machine with no public domain serves none (ADR 0346 §3). */ -export function publicRootOf(bindingFile: string | undefined): { root?: string; problem?: string } { +export function publicRootOf(bindingFile: string | undefined, owner: string): { root?: string; problem?: string } { + const NPM_PATH = npmPath(owner); if (!bindingFile) return { problem: "the lab has no npm-package-registry binding" }; let said: unknown; try { @@ -112,8 +116,9 @@ export function publicRootOf(bindingFile: string | undefined): { root?: string; * reaches the run's npm configuration but a scheme, a host and a port. Anything else falls back to the * forge's own address and says why, so a run still starts and lab_check names it. */ -export async function npmRegistry(forge: string, get: typeof fetch = fetch, publicRoot?: string): +export async function npmRegistry(forge: string, owner: string, get: typeof fetch = fetch, publicRoot?: string): Promise<{ registry: string; problem?: string }> { + const NPM_PATH = npmPath(owner); const asked = `${forge.replace(/\/+$/, "")}${NPM_PATH}`; const url = `${asked}@novox%2fmesh-sdk`; const fallback = (why: string) => ({ @@ -453,6 +458,10 @@ export function running(work: string): RunStatus | undefined { return listRuns(work).find((s) => !["passed", "failed", "stopped", "lost"].includes(s.state)); } +/** A repository's clone address on the forge: under the owner the mesh's repositories are under (ADR 0507 §9). */ +export const repositoryURL = (forge: string, owner: string, repo: string): string => + `${forge}/${encodeURIComponent(owner)}/${repo}.git`; + const shellQuote = (s: string) => `'${s.replace(/'/g, `'\\''`)}'`; /** @@ -461,8 +470,8 @@ const shellQuote = (s: string) => `'${s.replace(/'/g, `'\\''`)}'`; * The environment is the one the lab's README describes for a run against sibling checkouts, pointed * at this run's own tree, so what is built and claimed is exactly what was checked out. */ -export function script(work: string, id: string, forge: string, tests: string[], refs: Record, - credential?: string, registry = `${forge}${NPM_PATH}`): string { +export function script(work: string, id: string, forge: string, owner: string, tests: string[], refs: Record, + credential?: string, registry = `${forge}${npmPath(owner)}`): string { const dir = runDir(work, id); const setState = (state: string) => `node -e ${shellQuote( @@ -471,7 +480,7 @@ export function script(work: string, id: string, forge: string, tests: string[], const clones = REPOSITORIES.map((repo) => { const ref = refs[repo] ?? "main"; return [ - `git ${credentialArgs(credential)}clone --quiet --depth 50 --branch ${shellQuote(ref)} ${shellQuote(`${forge}/novox/${repo}.git`)} ${shellQuote(join(dir, repo))}`, + `git ${credentialArgs(credential)}clone --quiet --depth 50 --branch ${shellQuote(ref)} ${shellQuote(repositoryURL(forge, owner, repo))} ${shellQuote(join(dir, repo))}`, `echo "${repo} $(git -C ${shellQuote(join(dir, repo))} rev-parse HEAD)" >> ${shellQuote(join(dir, "commits.txt"))}`, ].join("\n"); }).join("\n"); @@ -515,7 +524,7 @@ node --experimental-strip-types src/cli.ts suite ${tests.map(shellQuote).join(" } /** start begins a run and returns at once with its status. */ -export function start(work: string, forge: string, tests: string[], refs: Record, +export function start(work: string, forge: string, owner: string, tests: string[], refs: Record, credential?: string, registry?: string): RunStatus { const id = `run-${new Date().toISOString().replace(/[:.]/g, "-")}`; const dir = runDir(work, id); @@ -525,7 +534,7 @@ export function start(work: string, forge: string, tests: string[], refs: Record // What a crashed or rebooted run left behind goes before this run writes its own. cleanCredentials(work); const credentials = writeCredential(credentialPath(work, id), credential); - writeFileSync(join(dir, "run.sh"), script(work, id, forge, tests, refs, credentials, registry), { mode: 0o700 }); + writeFileSync(join(dir, "run.sh"), script(work, id, forge, owner, tests, refs, credentials, registry), { mode: 0o700 }); // The wrapper records how the run ended, then removes the checkouts and keeps the log and status: a // run's tree is its own, and the next run starts from fresh ones (novox/hq ADR 0172). @@ -715,13 +724,13 @@ export function baseBuildLog(work: string, id: string, lines = 40): string { * baseScript is what one rebuild runs: mesh-lab's main from the forge, installed the way a run installs it, * then its `base build`, which replaces the image and reads back that it works before publishing it. */ -export function baseScript(dir: string, forge: string, credential?: string, registry = `${forge}${NPM_PATH}`): string { +export function baseScript(dir: string, forge: string, owner: string, credential?: string, registry = `${forge}${npmPath(owner)}`): string { const lab = join(dir, "mesh-lab"); return `set -euo pipefail export GIT_TERMINAL_PROMPT=0 ${credentialGuard(credential)} cd ${shellQuote(dir)} -git ${credentialArgs(credential)}clone --quiet --depth 1 ${shellQuote(`${forge}/novox/mesh-lab.git`)} ${shellQuote(lab)} +git ${credentialArgs(credential)}clone --quiet --depth 1 ${shellQuote(repositoryURL(forge, owner, "mesh-lab"))} ${shellQuote(lab)} ${credential ? `rm -f ${shellQuote(credential)}` : ""} echo "mesh-lab $(git -C ${shellQuote(lab)} rev-parse HEAD)" printf '%s\\n' ${shellQuote(`@novox:registry=${registry}`)} > ${shellQuote(join(dir, ".npmrc"))} @@ -733,7 +742,7 @@ node --experimental-strip-types src/cli.ts base build } /** startBaseBuild rebuilds the base image in the background and answers at once with the build. */ -export function startBaseBuild(work: string, forge: string, credential?: string, registry?: string): BaseBuild { +export function startBaseBuild(work: string, forge: string, owner: string, credential?: string, registry?: string): BaseBuild { const id = `base-${new Date().toISOString().replace(/[:.]/g, "-")}`; const dir = join(work, id); mkdirSync(dir, { recursive: true }); @@ -742,7 +751,7 @@ export function startBaseBuild(work: string, forge: string, credential?: string, // What a crashed or rebooted run or rebuild left behind goes before this one writes its own. cleanCredentials(work); const credentials = writeCredential(credentialPath(work, id), credential); - writeFileSync(join(dir, "build.sh"), baseScript(dir, forge, credentials, registry), { mode: 0o700 }); + writeFileSync(join(dir, "build.sh"), baseScript(dir, forge, owner, credentials, registry), { mode: 0o700 }); const wrapper = `bash ${shellQuote(join(dir, "build.sh"))} > ${shellQuote(join(dir, "build.log"))} 2>&1; code=$? node -e ${shellQuote( `const f=${JSON.stringify(buildStatusPath(work, id))};const s=JSON.parse(require("fs").readFileSync(f,"utf8"));s.state=process.argv[1]==="0"?"built":"failed";s.exit=Number(process.argv[1]);s.ended=new Date().toISOString();require("fs").writeFileSync(f,JSON.stringify(s,null,2))`, diff --git a/modules/memory-pressure/README.md b/modules/memory-pressure/README.md index ee76aecc..6722c8a2 100644 --- a/modules/memory-pressure/README.md +++ b/modules/memory-pressure/README.md @@ -99,8 +99,8 @@ On the laptop: 1. `systemctl --user disable --now mem-guard.service`, then delete `~/.config/systemd/user/mem-guard.service` and `~/scripts/mem-guard.sh`. -2. `/etc/systemd/oomd.conf.d/g14-oomd.conf`: the same values as the module's drop-in. -3. `/etc/sysctl.d/90-g14-zram.conf`: the same key as the module's file. +2. `/etc/systemd/oomd.conf.d/node-b-oomd.conf`: the same values as the module's drop-in. +3. `/etc/sysctl.d/90-node-b-zram.conf`: the same key as the module's file. The desktop had none of these. diff --git a/modules/mesh-mail-notifier/cmd/mesh-mail-notifier/send_test.go b/modules/mesh-mail-notifier/cmd/mesh-mail-notifier/send_test.go index d1f32000..2c2466a5 100644 --- a/modules/mesh-mail-notifier/cmd/mesh-mail-notifier/send_test.go +++ b/modules/mesh-mail-notifier/cmd/mesh-mail-notifier/send_test.go @@ -4,14 +4,14 @@ import "testing" // TestSendKeyNamesThisMachine proves Send asks its own machine's node-notifier, not the bare seat: a bare // "seat:node-notifier.send" is answered by whichever machine's node-notifier takes it first, so notices sent -// from g14 and shanks were lost to "503 no responders" or another machine's desktop (novox/hq issue 865). +// from node-b and node-d were lost to "503 no responders" or another machine's desktop (novox/hq issue 865). // desk-channel names the machine it asks the same way ("seat:node-notifier.send@"+machine, // modules/desk-channel/cmd/desk-channel/main.go); sendKey must read this machine's own name the same way the // SDK's own Emit does, from MESH_NODE, which the runtime sets. func TestSendKeyNamesThisMachine(t *testing.T) { - t.Setenv("MESH_NODE", "g14") + t.Setenv("MESH_NODE", "node-b") - if got, want := sendKey(), "seat:node-notifier.send@g14"; got != want { + if got, want := sendKey(), "seat:node-notifier.send@node-b"; got != want { t.Fatalf("sendKey() = %q, want %q (it must name this machine, as desk-channel does)", got, want) } } diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/asked_again_test.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/asked_again_test.go index 844aa47e..9d2b8631 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/asked_again_test.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/asked_again_test.go @@ -26,11 +26,11 @@ func (f *scriptedSecrets) Get(context.Context, string) (string, int, error) { // that holds the value later fills the file with no change of the plan. func TestARefusedSecretIsAskedAgainAfterRefusedForAndFilledWhenTheVaultHasIt(t *testing.T) { now := time.Date(2026, 10, 16, 16, 20, 0, 0, time.UTC) - fake := &scriptedSecrets{errs: []error{errors.New("the vault refused it: the assignment g14.claude-code could not be read")}} - c := &Client{Node: "g14", Secrets: fake, Now: func() time.Time { return now }} + fake := &scriptedSecrets{errs: []error{errors.New("the vault refused it: the assignment node-b.claude-code could not be read")}} + c := &Client{Node: "node-b", Secrets: fake, Now: func() time.Time { return now }} s := &state{secrets: map[string]heldSecret{}} n := Need{Kind: "secret", Name: "gitea-read"} - name := "provision/novox/gitea-read/g14/claude-code" + name := "provision/node-c/gitea-read/node-b/claude-code" if _, _, fail := c.vaultValue(context.Background(), s, n, name); fail == "" { t.Fatal("a refusal did not fail the need") @@ -50,7 +50,7 @@ func TestANotYetSecretIsAskedAgainOnEveryPass(t *testing.T) { now := time.Date(2026, 10, 16, 16, 20, 0, 0, time.UTC) notYet := fmt.Errorf("%w: the vault holds no secret named x yet", errNotYet) fake := &scriptedSecrets{errs: []error{notYet, notYet}} - c := &Client{Node: "g14", Secrets: fake, Now: func() time.Time { return now }} + c := &Client{Node: "node-b", Secrets: fake, Now: func() time.Time { return now }} s := &state{secrets: map[string]heldSecret{}} n := Need{Kind: "secret", Name: "gitea-read"} for i := 0; i < 2; i++ { diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/asks_test.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/asks_test.go index 9cf10e90..32eee780 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/asks_test.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/asks_test.go @@ -16,7 +16,7 @@ import ( // the module does not declare, a module not placed on the machine, a secret and a malformed ask are refused, each // with its reason. -// toolRunner is g14's tool runner on the bus, asking as its account may. +// toolRunner is node-b's tool runner on the bus, asking as its account may. type toolRunner struct { t *testing.T nc *nats.Conn @@ -121,7 +121,7 @@ func TestNatsAModulesCodeReadsItsConfigAndIsToldOfAChange(t *testing.T) { // Taken off the machine: said, and no longer answered. b.del(t, AssignmentsBucket, node+".claude-code") until(t, heard, `claude-code {"kind":"config"}`) - if a := runner.askConfig("claude-code", "managed_settings"); !strings.Contains(a.Error, "not placed on g14") { + if a := runner.askConfig("claude-code", "managed_settings"); !strings.Contains(a.Error, "not placed on node-b") { t.Fatalf("a module taken off the machine was answered %+v", a) } } @@ -138,7 +138,7 @@ func TestNatsWhatAModulesCodeMayNotReadIsRefusedWithTheReason(t *testing.T) { for _, c := range []struct{ ask, refused string }{ {`{"module":"claude-code","kind":"config","key":"no_such_key"}`, "declares no config value no_such_key"}, - {`{"module":"dunst","kind":"config","key":"font-size"}`, "dunst is not placed on g14"}, + {`{"module":"dunst","kind":"config","key":"font-size"}`, "dunst is not placed on node-b"}, {`{"module":"claude-code","kind":"secret","key":"gitea-read"}`, "not answered on the tool runner's word"}, {`{"module":"claude-code","kind":"bound","key":"mcp-endpoint:url"}`, "not answered yet"}, {`{"module":"claude-code","kind":"setting","key":"role"}`, "not a kind the client answers"}, diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/bound_gitea_test.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/bound_gitea_test.go index 9794aa39..40ad886f 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/bound_gitea_test.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/bound_gitea_test.go @@ -28,8 +28,8 @@ func giteaBoundExpected(internalToken, password string) string { return "GITEA__security__INTERNAL_TOKEN=" + internalToken + "\n" + "GITEA__database__DB_TYPE=postgres\n" + "GITEA__database__HOST=10.7.0.1:5432\n" + - "GITEA__database__NAME=mesh_g14_gitea\n" + - "GITEA__database__USER=mesh_g14_gitea\n" + + "GITEA__database__NAME=mesh_node-b_gitea\n" + + "GITEA__database__USER=mesh_node-b_gitea\n" + "GITEA__database__PASSWD=" + password + "\n" + "GITEA__webhook__ALLOWED_HOST_LIST=172.17.0.1\n" } @@ -39,15 +39,15 @@ func giteaRun(t *testing.T, entry string, extra string) (*engine, *aBus, *report b := startBus(t) registryOf679(t, b) vault := startVault(t, b) - vault.keep("own/g14/gitea/internal-token", "tok-never-on-the-bus", false) - vault.keep("pair/novox/postgres-database/g14/gitea", "pw-never-on-the-bus", false) + vault.keep("own/node-b/gitea/internal-token", "tok-never-on-the-bus", false) + vault.keep("pair/node-c/postgres-database/node-b/gitea", "pw-never-on-the-bus", false) heard := b.listen(t) e := startEngine(t, map[string]string{giteaEnvPath: "owned"}) tpl, _ := templates(t, map[string]string{"server.env": giteaTemplate(t) + extra}) b.assign(t, "gitea", Plan{Templates: tpl, Files: []PlannedFile{{ID: "env", Path: giteaEnvPath, Template: "server.env", OwnerAtRuntime: "owned"}}, Secrets: []PlannedSecret{{Name: "internal-token"}}, - Provisions: []PlannedProvision{{Name: "postgres-database", Provision: "postgres-database", Provider: "novox", + Provisions: []PlannedProvision{{Name: "postgres-database", Provision: "postgres-database", Provider: "node-c", ProviderModule: "postgres", CredentialForm: "pair"}}}) kv, err := b.domain.CreateKeyValue(context.Background(), jetstream.KeyValueConfig{Bucket: "postgres_bound"}) if err != nil { @@ -60,19 +60,19 @@ func giteaRun(t *testing.T, entry string, extra string) (*engine, *aBus, *report } func TestNatsAHandedGiteasServerEnvIsFilledAsTheControllerComposedIt(t *testing.T) { - e, _, _ := giteaRun(t, `{"provider":"novox","provider-module":"postgres","provision":"postgres-database",`+ - `"consumer":"g14","module":"gitea","as":"mesh_g14_gitea","user":"mesh_g14_gitea",`+ - `"values":{"at":"10.7.0.1","from":"novox","port":5432},"vault-name":"pair/novox/postgres-database/g14/gitea"}`, + e, _, _ := giteaRun(t, `{"provider":"node-c","provider-module":"postgres","provision":"postgres-database",`+ + `"consumer":"node-b","module":"gitea","as":"mesh_node-b_gitea","user":"mesh_node-b_gitea",`+ + `"values":{"at":"10.7.0.1","from":"node-c","port":5432},"vault-name":"pair/node-c/postgres-database/node-b/gitea"}`, "FROM=${bound:postgres-database:from}\n") eventually(t, "server.env is filled to the controller's text", func() bool { - return e.read(t, giteaEnvPath) == giteaBoundExpected("tok-never-on-the-bus", "pw-never-on-the-bus")+"FROM=novox\n" + return e.read(t, giteaEnvPath) == giteaBoundExpected("tok-never-on-the-bus", "pw-never-on-the-bus")+"FROM=node-c\n" }) } // An entry without `at`, or without `from`, fills nothing: the module fails naming the key, never a file with a hole. func TestNatsAnEntryWithoutAtOrFromLeavesGiteasServerEnvUnfilled(t *testing.T) { - for key, values := range map[string]string{"at": `{"from":"novox","port":5432}`, "from": `{"at":"10.7.0.1","port":5432}`} { - e, _, heard := giteaRun(t, `{"as":"mesh_g14_gitea","user":"mesh_g14_gitea","values":`+values+`}`, + for key, values := range map[string]string{"at": `{"from":"node-c","port":5432}`, "from": `{"at":"10.7.0.1","port":5432}`} { + e, _, heard := giteaRun(t, `{"as":"mesh_node-b_gitea","user":"mesh_node-b_gitea","values":`+values+`}`, "FROM=${bound:postgres-database:from}\n") eventually(t, "the module fails for the missing "+key, func() bool { r, _ := heard.last("gitea") @@ -99,24 +99,24 @@ func TestNatsAHandedGiteasOriginsAreFilledFromTheRouteProvidersEntry(t *testing. tpl, _ := templates(t, map[string]string{"origins": string(raw)}) b.assign(t, "gitea", Plan{Templates: tpl, Files: []PlannedFile{{ID: "origins", Path: originsPath, Template: "origins", OwnerAtRuntime: "owned"}}, - Provisions: []PlannedProvision{{Provision: "route", Provider: "novox", ProviderModule: "route-adapter"}}}) + Provisions: []PlannedProvision{{Provision: "route", Provider: "node-c", ProviderModule: "route-adapter"}}}) kv, err := b.domain.CreateKeyValue(context.Background(), jetstream.KeyValueConfig{Bucket: "route-adapter_bound"}) if err != nil { t.Fatal(err) } b.kv["route-adapter_bound"] = kv - b.put(t, "route-adapter_bound", node+".gitea.route", `{"provider":"novox","provider-module":"route-adapter",`+ - `"provision":"route","consumer":"g14","module":"gitea","as":"mesh_g14_gitea",`+ - `"values":{"name":"git.example.org","internal-name":"git.novox.internal","name-web":"git.example.org"}}`) + b.put(t, "route-adapter_bound", node+".gitea.route", `{"provider":"node-c","provider-module":"route-adapter",`+ + `"provision":"route","consumer":"node-b","module":"gitea","as":"mesh_node-b_gitea",`+ + `"values":{"name":"git.example.org","internal-name":"git.node-c.internal","name-web":"git.example.org"}}`) aClient(t, b, e, t.TempDir()) eventually(t, "origins is filled to the controller's text", func() bool { return e.read(t, originsPath) == "https://git.example.org\n" }) } -// The same where route-proxy, not route-adapter, answers the route, as on novox (issue 792): filled by its machine's client from the route provider's entry, which +// The same where route-proxy, not route-adapter, answers the route, as on node-c (issue 792): filled by its machine's client from the route provider's entry, which // carries the public name the controller composed for gitea, to the text the controller wrote for the binding file. -func TestNatsAHandedGiteasOriginsAreFilledFromRouteProxysEntryOnNovox(t *testing.T) { +func TestNatsAHandedGiteasOriginsAreFilledFromRouteProxysEntryOnNodeC(t *testing.T) { const originsPath = "/etc/gitea/origins" raw, err := os.ReadFile("../../../gitea/files/origins") if err != nil { @@ -129,15 +129,15 @@ func TestNatsAHandedGiteasOriginsAreFilledFromRouteProxysEntryOnNovox(t *testing tpl, _ := templates(t, map[string]string{"origins": string(raw)}) b.assign(t, "gitea", Plan{Templates: tpl, Files: []PlannedFile{{ID: "origins", Path: originsPath, Template: "origins", OwnerAtRuntime: "owned"}}, - Provisions: []PlannedProvision{{Provision: "route", Provider: "novox", ProviderModule: "route-proxy"}}}) + Provisions: []PlannedProvision{{Provision: "route", Provider: "node-c", ProviderModule: "route-proxy"}}}) kv, err := b.domain.CreateKeyValue(context.Background(), jetstream.KeyValueConfig{Bucket: "route-proxy_bound"}) if err != nil { t.Fatal(err) } b.kv["route-proxy_bound"] = kv - b.put(t, "route-proxy_bound", node+".gitea.route", `{"provider":"novox","provider-module":"route-proxy",`+ - `"provision":"route","consumer":"g14","module":"gitea","as":"mesh_g14_gitea",`+ - `"values":{"name":"git.example.org","internal-name":"git.novox.internal","name-web":"git.example.org"}}`) + b.put(t, "route-proxy_bound", node+".gitea.route", `{"provider":"node-c","provider-module":"route-proxy",`+ + `"provision":"route","consumer":"node-b","module":"gitea","as":"mesh_node-b_gitea",`+ + `"values":{"name":"git.example.org","internal-name":"git.node-c.internal","name-web":"git.example.org"}}`) aClient(t, b, e, t.TempDir()) eventually(t, "origins is filled to the controller's text", func() bool { return e.read(t, originsPath) == "https://git.example.org\n" diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/client_test.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/client_test.go index cc089ba4..f3c51964 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/client_test.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/client_test.go @@ -35,7 +35,7 @@ import ( // node-engine's own tests prove its writes; these prove the client asks for the right ones — and counting each // write that moved a file, which is what the node-engine reloads or restarts the program for. -const node = "g14" +const node = "node-b" // aBus is a server of this test alone with the client's buckets, the registry written as mesh-provisioner writes it. type aBus struct { @@ -343,7 +343,7 @@ func templates(t *testing.T, files map[string]string) (Templates, *httptest.Serv return Templates{Source: srv.URL + "/files.tar.gz", Digest: digestOf(body)}, srv } -// aClient runs a client on g14's account until the returned stop is called. +// aClient runs a client on node-b's account until the returned stop is called. func aClient(t *testing.T, b *aBus, e *engine, state string) (*Client, func()) { t.Helper() nc, err := nats.Connect(b.url, nats.UserInfo("provisioner."+node, "x"), @@ -432,7 +432,7 @@ func TestNatsATemplateFillsFromEachOwnersKeys(t *testing.T) { // The needs, one per placeholder, under the node's and the module's name. eventually(t, "the needs are recorded", func() bool { got := b.keys(t, NeedsBucket, node+".dunst.") - return len(got) == 2 && got[0] == "g14.dunst.config.font-size" && got[1] == "g14.dunst.machine.account-home" + return len(got) == 2 && got[0] == "node-b.dunst.config.font-size" && got[1] == "node-b.dunst.machine.account-home" }) kept, err := b.kv[AppliedBucket].Get(context.Background(), node+".dunst") if err != nil || !strings.Contains(string(kept.Value()), `"status":"applied"`) { @@ -573,8 +573,8 @@ func TestNatsWithNothingOverTheBusItAsksForNoWriteNoSecretAndNoReport(t *testing b := startBus(t) heard := b.listen(t) vault := startVault(t, b) - vault.keep("own/g14/app/app-token", "tok", false) - vault.keep("own/g14/app/db-password", "pw", false) + vault.keep("own/node-b/app/app-token", "tok", false) + vault.keep("own/node-b/app/db-password", "pw", false) e := startEngine(t, map[string]string{appConf: "owned", appDB: "owned"}) b.config(t, "app", "font-size", "13") b.assign(t, "app", app(t)) @@ -585,7 +585,7 @@ func TestNatsWithNothingOverTheBusItAsksForNoWriteNoSecretAndNoReport(t *testing return r.Status == StatusApplied }) _, askedBefore := e.counts(appConf) - vaultBefore := vault.times("own/g14/app/app-token") + vault.times("own/g14/app/db-password") + vaultBefore := vault.times("own/node-b/app/app-token") + vault.times("own/node-b/app/db-password") _, reportsBefore := heard.last("app") // Well over any former drift interval used in these tests (100ms, 200ms): nothing reaches the client. @@ -594,7 +594,7 @@ func TestNatsWithNothingOverTheBusItAsksForNoWriteNoSecretAndNoReport(t *testing if _, askedAfter := e.counts(appConf); askedAfter != askedBefore { t.Errorf("the node-engine was asked to write %s again, with nothing over the bus", appConf) } - if n := vault.times("own/g14/app/app-token") + vault.times("own/g14/app/db-password"); n != vaultBefore { + if n := vault.times("own/node-b/app/app-token") + vault.times("own/node-b/app/db-password"); n != vaultBefore { t.Error("the vault was asked again, with nothing over the bus") } if _, after := heard.last("app"); after != reportsBefore { @@ -661,7 +661,7 @@ func TestNatsWhatCannotBeFilledFailsWithTheReason(t *testing.T) { e := startEngine(t, map[string]string{dunstrc: "owned", "/etc/app.conf": "owned"}) aClient(t, b, e, t.TempDir()) - b.put(t, AssignmentsBucket, node+".broken", `{"node":"g14","module":"broken","provisioning":{"templates":`+ + b.put(t, AssignmentsBucket, node+".broken", `{"node":"node-b","module":"broken","provisioning":{"templates":`+ `{"source":"x","digest":"sha256:00"},"files":[],"surprise":1}}`) p := dunst(t) p.Templates.Digest = "sha256:" + strings.Repeat("0", 64) @@ -684,10 +684,10 @@ func TestNatsWhatCannotBeFilledFailsWithTheReason(t *testing.T) { func TestANeedsKeyHoldsAnyName(t *testing.T) { for n, want := range map[Need]string{ - {Kind: "config", Name: "font-size"}: "g14.dunst.config.font-size", - {Kind: "seat", Name: "mesh-broker:4222"}: "g14.dunst.seat.mesh-broker=3A4222", - {Kind: "config", Name: "a=b c"}: "g14.dunst.config.a=3Db=20c", - {Kind: "machine", Name: "account-home"}: "g14.dunst.machine.account-home", + {Kind: "config", Name: "font-size"}: "node-b.dunst.config.font-size", + {Kind: "seat", Name: "mesh-broker:4222"}: "node-b.dunst.seat.mesh-broker=3A4222", + {Kind: "config", Name: "a=b c"}: "node-b.dunst.config.a=3Db=20c", + {Kind: "machine", Name: "account-home"}: "node-b.dunst.machine.account-home", } { if got := NeedKey(node, "dunst", n); got != want { t.Errorf("%v: %s, want %s", n, got, want) diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/collect_select_test.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/collect_select_test.go index 036af0eb..2b246c73 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/collect_select_test.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/collect_select_test.go @@ -51,8 +51,8 @@ func TestNatsASelectingPlaceholderTakesOnlyTheItemsCarryingItsAttributesInOrder( eventually(t, "the file holds each placeholder's items", func() bool { return e.read(t, barsConf) == want }) eventually(t, "the named items are recorded under their module and name", func() bool { got := b.keys(t, CollectBucket, node+".blocks.") - return len(got) == 4 && got[0] == "g14.blocks.another" && got[1] == "g14.blocks.power/battery" && - got[2] == "g14.blocks.power/clock" && got[3] == "g14.blocks.power/cpu" + return len(got) == 4 && got[0] == "node-b.blocks.another" && got[1] == "node-b.blocks.power/battery" && + got[2] == "node-b.blocks.power/clock" && got[3] == "node-b.blocks.power/cpu" }) // An item no longer contributed goes, and only that one. @@ -91,13 +91,13 @@ func TestASelectorIsReadFromAPlaceholderOfAnyNamespace(t *testing.T) { func TestAnItemsKeyHoldsItsName(t *testing.T) { key := CollectKey(node, "blocks", itemID("power", "battery")) - if key != "g14.blocks.power/battery" { + if key != "node-b.blocks.power/battery" { t.Fatal(key) } if c, id, ok := itemOfKey(node, key); !ok || c != "blocks" || id != "power/battery" { t.Errorf("reads back as %q %q %v", c, id, ok) } - for _, bad := range []string{"g14.blocks.power/", "g14.blocks./x", "g14.blocks.a/b/c"} { + for _, bad := range []string{"node-b.blocks.power/", "node-b.blocks./x", "node-b.blocks.a/b/c"} { if _, _, ok := itemOfKey(node, bad); ok { t.Errorf("%s read as an item's key", bad) } @@ -115,15 +115,15 @@ func TestAnItemsKeyHoldsItsName(t *testing.T) { func TestAnItemOfAPlanReadsItsNameAndAttributesAndRefusesAnUnknownField(t *testing.T) { plan := func(item string) string { - return `{"node":"g14","module":"power","provisioning":{"templates":{"source":"x","digest":"sha256:` + + return `{"node":"node-b","module":"power","provisioning":{"templates":{"source":"x","digest":"sha256:` + strings.Repeat("0", 64) + `"},"files":[],"items":[` + item + `]}}` } - _, p, err := planOf("g14", "g14.power", []byte(plan( + _, p, err := planOf("node-b", "node-b.power", []byte(plan( `{"collection":"blocks","name":"battery","attributes":{"bar":"top"},"template":"battery"}`))) if err != nil || len(p.Items) != 1 || p.Items[0].Name != "battery" || p.Items[0].Attributes["bar"] != "top" { t.Fatalf("read %+v (%v)", p.Items, err) } - if _, _, err := planOf("g14", "g14.power", []byte(plan( + if _, _, err := planOf("node-b", "node-b.power", []byte(plan( `{"collection":"blocks","template":"battery","colour":"red"}`))); err == nil || !strings.Contains(err.Error(), "colour") { t.Errorf("an unknown item field was accepted: %v", err) } diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/collect_test.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/collect_test.go index 71c7503a..2c889055 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/collect_test.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/collect_test.go @@ -47,10 +47,10 @@ func TestNatsZshsFileHoldsEveryPlacedContributorsItemAndLosesOneWhoseContributor // Each item recorded under the node, the collection and its contributor; filled, the contributor's report says so. eventually(t, "the items are recorded", func() bool { got := b.keys(t, CollectBucket, node+".") - return len(got) == 3 && got[0] == "g14.shell=3Azsh=3Anormal.mesh-cli" && - got[1] == "g14.shell=3Azsh=3Anormal.powerlevel10k" && got[2] == "g14.zsh-widgets.fzf" + return len(got) == 3 && got[0] == "node-b.shell=3Azsh=3Anormal.mesh-cli" && + got[1] == "node-b.shell=3Azsh=3Anormal.powerlevel10k" && got[2] == "node-b.zsh-widgets.fzf" }) - kept, err := b.kv[CollectBucket].Get(context.Background(), "g14.shell=3Azsh=3Anormal.powerlevel10k") + kept, err := b.kv[CollectBucket].Get(context.Background(), "node-b.shell=3Azsh=3Anormal.powerlevel10k") if err != nil { t.Fatal(err) } @@ -66,7 +66,7 @@ func TestNatsZshsFileHoldsEveryPlacedContributorsItemAndLosesOneWhoseContributor // zsh records the collections it reads as its needs, like any value. eventually(t, "zsh's needs are recorded", func() bool { got := b.keys(t, NeedsBucket, node+".zsh.") - return len(got) == 2 && got[0] == "g14.zsh.collected.zsh-widgets" && got[1] == "g14.zsh.shell.zsh=3Anormal" + return len(got) == 2 && got[0] == "node-b.zsh.collected.zsh-widgets" && got[1] == "node-b.zsh.shell.zsh=3Anormal" }) // A contributor's own value changes: its item, and so zshrc, follow. @@ -136,9 +136,9 @@ func TestNatsACollectionNotPlannedAndAnItemNamingASecretFail(t *testing.T) { func TestAnItemsKeyHoldsAnyCollectionAndReadsBack(t *testing.T) { for collection, want := range map[string]string{ - "zsh-widgets": "g14.zsh-widgets.fzf", - "shell:zsh:normal": "g14.shell=3Azsh=3Anormal.fzf", - "contribution:node-hotkeys:trigger": "g14.contribution=3Anode-hotkeys=3Atrigger.fzf", + "zsh-widgets": "node-b.zsh-widgets.fzf", + "shell:zsh:normal": "node-b.shell=3Azsh=3Anormal.fzf", + "contribution:node-hotkeys:trigger": "node-b.contribution=3Anode-hotkeys=3Atrigger.fzf", } { key := CollectKey(node, collection, "fzf") if key != want { @@ -149,7 +149,7 @@ func TestAnItemsKeyHoldsAnyCollectionAndReadsBack(t *testing.T) { t.Errorf("%s reads back as %q %q %v", key, c, who, ok) } } - for _, bad := range []string{"g14.x", "ace.zsh-widgets.fzf", "g14.=ZZ.fzf", "g14.a.b.c", "g14.Upper.fzf"} { + for _, bad := range []string{"node-b.x", "node-a.zsh-widgets.fzf", "node-b.=ZZ.fzf", "node-b.a.b.c", "node-b.Upper.fzf"} { if _, _, ok := itemOfKey(node, bad); ok { t.Errorf("%s read as an item's key", bad) } diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/consumers_test.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/consumers_test.go index e48d403b..ba384435 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/consumers_test.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/consumers_test.go @@ -93,25 +93,25 @@ func TestNatsAProvidersConsumersAreFilledFromTheVaultAndFollowEachBindAndRemake( heard := b.listen(t) wire := sniff(t, b) vault := startVault(t, b) - vault.keep(pairOf("ace", "letta", ""), "letta-pw-never-on-the-bus", false) - vault.keep(pairOf("shanks", "gitea", "cache"), "gitea-pw-never-on-the-bus", false) - // Bound to this provider: ace's letta, and shanks's gitea under a local name. Not: a provision of another + vault.keep(pairOf("node-a", "letta", ""), "letta-pw-never-on-the-bus", false) + vault.keep(pairOf("node-d", "gitea", "cache"), "gitea-pw-never-on-the-bus", false) + // Bound to this provider: node-a's letta, and node-d's gitea under a local name. Not: a provision of another // provider, another provision, and a credential its provider's software issued per holder (issue 757). - b.bind(t, "ace", "letta", fromHere(""), - PlannedProvision{Provision: "postgres-database", Local: "old", Provider: "ace", ProviderModule: "postgres"}) - b.bind(t, "shanks", "gitea", PlannedProvision{Provision: "redis", Provider: node, ProviderModule: "postgres"}, + b.bind(t, "node-a", "letta", fromHere(""), + PlannedProvision{Provision: "postgres-database", Local: "old", Provider: "node-a", ProviderModule: "postgres"}) + b.bind(t, "node-d", "gitea", PlannedProvision{Provision: "redis", Provider: node, ProviderModule: "postgres"}, fromHere("cache"), PlannedProvision{Name: "token", Provision: "forge-read", Provider: node, ProviderModule: "postgres"}) // Nor a bind whose offer names another credential form than a login per consumer (issue 768). - b.bind(t, "ace", "shared", PlannedProvision{Provision: "postgres-database", Provider: node, + b.bind(t, "node-a", "shared", PlannedProvision{Provision: "postgres-database", Provider: node, ProviderModule: "postgres", CredentialForm: "shared"}) e := startEngine(t, map[string]string{consumersFile: "owned"}) b.assign(t, "postgres", postgresPlan(t, "${consumers:postgres-database}")) aClient(t, b, e, t.TempDir()) - letta := listed{Consumer: "ace", Module: "letta", Credential: pairOf("ace", "letta", ""), + letta := listed{Consumer: "node-a", Module: "letta", Credential: pairOf("node-a", "letta", ""), Value: "letta-pw-never-on-the-bus"} - gitea := listed{Consumer: "shanks", Module: "gitea", Local: "cache", Credential: pairOf("shanks", "gitea", "cache"), + gitea := listed{Consumer: "node-d", Module: "gitea", Local: "cache", Credential: pairOf("node-d", "gitea", "cache"), Value: "gitea-pw-never-on-the-bus"} eventually(t, "the file lists each bound consumer with its credential, in the plans' order", func() bool { return sameList(consumersIn(t, e), letta, gitea) @@ -134,21 +134,21 @@ func TestNatsAProvidersConsumersAreFilledFromTheVaultAndFollowEachBindAndRemake( eventually(t, "a consumer that binds is listed when the vault holds its credential, in its assignment's order", func() bool { return sameList(consumersIn(t, e), letta, photos, gitea) }) // The vault remakes a credential. - vault.keep(pairOf("ace", "letta", ""), "letta-pw-remade-never-on-the-bus", true) + vault.keep(pairOf("node-a", "letta", ""), "letta-pw-remade-never-on-the-bus", true) letta.Value = "letta-pw-remade-never-on-the-bus" eventually(t, "a credential the vault remakes reaches the file", func() bool { return sameList(consumersIn(t, e), letta, photos, gitea) }) // A consumer unbinds. - b.del(t, AssignmentsBucket, "shanks.gitea") + b.del(t, AssignmentsBucket, "node-d.gitea") eventually(t, "a consumer that unbinds leaves the file", func() bool { return sameList(consumersIn(t, e), letta, photos) }) // Another provider's consumer's credential changing is no reason to ask. - before := vault.times(pairOf("ace", "letta", "")) - vault.keep("pair/ace/postgres-database/ace/letta/old", "elsewhere", true) + before := vault.times(pairOf("node-a", "letta", "")) + vault.keep("pair/node-a/postgres-database/node-a/letta/old", "elsewhere", true) time.Sleep(300 * time.Millisecond) - if vault.times("pair/ace/postgres-database/ace/letta/old") != 0 || vault.times(pairOf("ace", "letta", "")) != before { + if vault.times("pair/node-a/postgres-database/node-a/letta/old") != 0 || vault.times(pairOf("node-a", "letta", "")) != before { t.Error("a change of another provider's consumer made the client ask") } @@ -194,7 +194,7 @@ func appPlan(t *testing.T) Plan { "PORT=${bound:postgres-database:port}\nUSER=${bound:postgres-database:user}\n"}) return Plan{Templates: tpl, Files: []PlannedFile{{ID: "db", Path: appBound, Template: "database.env", OwnerAtRuntime: "owned"}}, - Provisions: []PlannedProvision{{Provision: "postgres-database", Provider: "novox", ProviderModule: "postgres"}}} + Provisions: []PlannedProvision{{Provision: "postgres-database", Provider: "node-c", ProviderModule: "postgres"}}} } func TestNatsAConsumersBoundValuesAreFilledFromItsProvidersEntry(t *testing.T) { @@ -216,24 +216,24 @@ func TestNatsAConsumersBoundValuesAreFilledFromItsProvidersEntry(t *testing.T) { } b.kv["postgres_bound"] = kv // The provider has not answered it yet: waited for. - b.put(t, "postgres_bound", "ace.app.postgres-database", `{"values":{"host":"elsewhere"}}`) + b.put(t, "postgres_bound", "node-a.app.postgres-database", `{"values":{"host":"elsewhere"}}`) b.assign(t, "app", appPlan(t)) // a change of its own is the only reason it looks again: there is no timer eventually(t, "the consumer waits for its own entry", func() bool { r, _ := heard.last("app") - return r.Status == StatusWaiting && strings.Contains(strings.Join(r.WaitsFor, " "), "postgres on novox") + return r.Status == StatusWaiting && strings.Contains(strings.Join(r.WaitsFor, " "), "postgres on node-c") }) - b.put(t, "postgres_bound", node+".app.postgres-database", `{"provider":"novox","provider-module":"postgres",`+ - `"provision":"postgres-database","consumer":"g14","module":"app","as":"mesh_g14_app","user":"mesh_g14_app",`+ - `"values":{"host":"novox.mesh","port":5432},"vault-name":"pair/novox/postgres-database/g14/app"}`) + b.put(t, "postgres_bound", node+".app.postgres-database", `{"provider":"node-c","provider-module":"postgres",`+ + `"provision":"postgres-database","consumer":"node-b","module":"app","as":"mesh_node-b_app","user":"mesh_node-b_app",`+ + `"values":{"host":"node-c.mesh","port":5432},"vault-name":"pair/node-c/postgres-database/node-b/app"}`) eventually(t, "the file is filled from its provider's entry", func() bool { - return e.read(t, appBound) == "HOST=novox.mesh\nPORT=5432\nUSER=mesh_g14_app\n" + return e.read(t, appBound) == "HOST=node-c.mesh\nPORT=5432\nUSER=mesh_node-b_app\n" }) - b.put(t, "postgres_bound", node+".app.postgres-database", `{"user":"mesh_g14_app",`+ - `"values":{"host":"novox.lan","port":5432}}`) + b.put(t, "postgres_bound", node+".app.postgres-database", `{"user":"mesh_node-b_app",`+ + `"values":{"host":"node-c.lan","port":5432}}`) eventually(t, "a changed entry refills the file", func() bool { - return e.read(t, appBound) == "HOST=novox.lan\nPORT=5432\nUSER=mesh_g14_app\n" + return e.read(t, appBound) == "HOST=node-c.lan\nPORT=5432\nUSER=mesh_node-b_app\n" }) - b.put(t, "postgres_bound", node+".app.postgres-database", `{"refused":"app on g14 keeps several holders"}`) + b.put(t, "postgres_bound", node+".app.postgres-database", `{"refused":"app on node-b keeps several holders"}`) eventually(t, "an entry the provider refused fails the consumer with its reason", func() bool { r, _ := heard.last("app") return r.Status == StatusFailed && strings.Contains(r.Reason, "keeps several holders") @@ -246,17 +246,17 @@ func TestNatsAProviderListsAConsumerWhoseEntryNamesItsLoginFile(t *testing.T) { b := startBus(t) registryOf679(t, b) vault := startVault(t, b) - vault.keep(pairOf("ace", "nextcloud", ""), "nc-pw-never-on-the-bus", false) - vault.keep(pairOf("ace", "forge", ""), "forge-pw", false) - b.bind(t, "ace", "nextcloud", PlannedProvision{Name: "database", Provision: "postgres-database", Provider: node, + vault.keep(pairOf("node-a", "nextcloud", ""), "nc-pw-never-on-the-bus", false) + vault.keep(pairOf("node-a", "forge", ""), "forge-pw", false) + b.bind(t, "node-a", "nextcloud", PlannedProvision{Name: "database", Provision: "postgres-database", Provider: node, ProviderModule: "postgres", CredentialForm: "pair", Path: "/s/database.secret"}) - b.bind(t, "ace", "forge", PlannedProvision{Name: "database", Provision: "postgres-database", Provider: node, + b.bind(t, "node-a", "forge", PlannedProvision{Name: "database", Provision: "postgres-database", Provider: node, ProviderModule: "postgres", CredentialForm: "issued", Path: "/s/forge.secret"}) e := startEngine(t, map[string]string{consumersFile: "owned"}) b.assign(t, "postgres", postgresPlan(t, "${consumers:postgres-database}")) aClient(t, b, e, t.TempDir()) eventually(t, "the consumer with a login file is listed, the per-holder one is not", func() bool { - return sameList(consumersIn(t, e), listed{Consumer: "ace", Module: "nextcloud", - Credential: pairOf("ace", "nextcloud", ""), Value: "nc-pw-never-on-the-bus"}) + return sameList(consumersIn(t, e), listed{Consumer: "node-a", Module: "nextcloud", + Credential: pairOf("node-a", "nextcloud", ""), Value: "nc-pw-never-on-the-bus"}) }) } diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/moved_forge_test.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/moved_forge_test.go index 9eb14cbb..22b443c7 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/moved_forge_test.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/moved_forge_test.go @@ -48,13 +48,13 @@ func TestNatsAMovedGiteaAndClaudeCodeHaveTheirFilesFilledFromTheBus(t *testing.T origins = "/etc/gitea/origins" facts = "/var/lib/claude-code/state/facts.json" readCred = "/var/lib/claude-code/state/gitea-read.secret" - credential = "https://agent-g14:gta-never-on-the-bus@git.example.org" + credential = "https://agent-node-b:gta-never-on-the-bus@git.example.org" ) b := startBus(t) registryOf679(t, b) vault := startVault(t, b) - vault.keep("own/g14/gitea/internal-token", "tok-never-on-the-bus", false) - vault.keep("pair/novox/postgres-database/g14/gitea", "pw-never-on-the-bus", false) + vault.keep("own/node-b/gitea/internal-token", "tok-never-on-the-bus", false) + vault.keep("pair/node-c/postgres-database/node-b/gitea", "pw-never-on-the-bus", false) vault.keep(forgeHolder, credential, false) e := startEngine(t, map[string]string{serverEnv: "owned", origins: "owned", facts: "owned", readCred: "owned"}) @@ -65,16 +65,16 @@ func TestNatsAMovedGiteaAndClaudeCodeHaveTheirFilesFilledFromTheBus(t *testing.T {ID: "origins", Path: origins, Template: "origins", OwnerAtRuntime: "owned"}}, Secrets: []PlannedSecret{{Name: "internal-token"}}, Provisions: []PlannedProvision{ - {Name: "postgres-database", Provision: "postgres-database", Provider: "novox", ProviderModule: "postgres", + {Name: "postgres-database", Provision: "postgres-database", Provider: "node-c", ProviderModule: "postgres", CredentialForm: "pair"}, - {Provision: "route", Provider: "novox", ProviderModule: "route-proxy"}}}) + {Provision: "route", Provider: "node-c", ProviderModule: "route-proxy"}}}) ccTpl, _ := templates(t, map[string]string{"facts.json": catalogFile(t, "claude-code/files/facts.json")}) b.assign(t, "claude-code", Plan{Templates: ccTpl, Files: []PlannedFile{ {ID: "facts", Path: facts, Template: "facts.json", OwnerAtRuntime: "owned"}}, Placement: map[string]string{"machine:name": node}, Provisions: []PlannedProvision{ {Provision: "mcp-endpoint", Provider: node, ProviderModule: "node-tools"}, - {Name: "gitea-read", Provision: "gitea-read", Provider: "novox", Path: readCred}}}) + {Name: "gitea-read", Provision: "gitea-read", Provider: "node-c", Path: readCred}}}) for _, bucket := range []string{"postgres_bound", "route-proxy_bound", "node-tools_bound"} { kv, err := b.domain.CreateKeyValue(context.Background(), jetstream.KeyValueConfig{Bucket: bucket}) @@ -83,21 +83,21 @@ func TestNatsAMovedGiteaAndClaudeCodeHaveTheirFilesFilledFromTheBus(t *testing.T } b.kv[bucket] = kv } - b.put(t, "postgres_bound", node+".gitea.postgres-database", `{"provider":"novox","provider-module":"postgres",`+ - `"provision":"postgres-database","consumer":"g14","module":"gitea","as":"mesh_g14_gitea","user":"mesh_g14_gitea",`+ - `"values":{"at":"10.7.0.1","from":"novox","port":5432},"vault-name":"pair/novox/postgres-database/g14/gitea"}`) - b.put(t, "route-proxy_bound", node+".gitea.route", `{"provider":"novox","provider-module":"route-proxy",`+ - `"provision":"route","consumer":"g14","module":"gitea","as":"mesh_g14_gitea",`+ - `"values":{"name":"git.example.org","internal-name":"git.novox.internal","name-web":"git.example.org"}}`) - b.put(t, "node-tools_bound", node+".claude-code.mcp-endpoint", `{"provider":"g14","provider-module":"node-tools",`+ - `"provision":"mcp-endpoint","consumer":"g14","module":"claude-code","as":"mesh_g14_claude-code",`+ + b.put(t, "postgres_bound", node+".gitea.postgres-database", `{"provider":"node-c","provider-module":"postgres",`+ + `"provision":"postgres-database","consumer":"node-b","module":"gitea","as":"mesh_node-b_gitea","user":"mesh_node-b_gitea",`+ + `"values":{"at":"10.7.0.1","from":"node-c","port":5432},"vault-name":"pair/node-c/postgres-database/node-b/gitea"}`) + b.put(t, "route-proxy_bound", node+".gitea.route", `{"provider":"node-c","provider-module":"route-proxy",`+ + `"provision":"route","consumer":"node-b","module":"gitea","as":"mesh_node-b_gitea",`+ + `"values":{"name":"git.example.org","internal-name":"git.node-c.internal","name-web":"git.example.org"}}`) + b.put(t, "node-tools_bound", node+".claude-code.mcp-endpoint", `{"provider":"node-b","provider-module":"node-tools",`+ + `"provision":"mcp-endpoint","consumer":"node-b","module":"claude-code","as":"mesh_node-b_claude-code",`+ `"values":{"port":4270}}`) aClient(t, b, e, t.TempDir()) for path, want := range map[string]string{ serverEnv: giteaBoundExpected("tok-never-on-the-bus", "pw-never-on-the-bus"), origins: "https://git.example.org\n", - facts: "{\n \"node\": \"g14\",\n \"console\": \"http://127.0.0.1:4270/mcp\"\n}\n", + facts: "{\n \"node\": \"node-b\",\n \"console\": \"http://127.0.0.1:4270/mcp\"\n}\n", readCred: credential, } { eventually(t, path+" is filled to the controller's text", func() bool { return e.read(t, path) == want }) diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/providers_test.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/providers_test.go index 4841a24c..07b6e617 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/providers_test.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/providers_test.go @@ -10,20 +10,20 @@ import ( // client reads that strictly, before the controller writes it, and fills nothing from an entry with no credential; // the vault acts on it. func TestAPlanNamingItsProvidersIsRead(t *testing.T) { - value := `{"node":"g14","module":"app","provisioning":{` + + value := `{"node":"node-b","module":"app","provisioning":{` + `"templates":{"source":"x","digest":"sha256:` + strings.Repeat("0", 64) + `"},` + `"files":[{"id":"conf","path":"/etc/app.conf","template":"app.conf","owner-at-runtime":"owned"}],` + - `"provisions":[{"provision":"postgres-database","provider":"novox","provider-module":"postgres"},` + - `{"provision":"s3-bucket","local":"hot","provider":"ace","provider-module":"minio"},` + - `{"name":"forge-read","provision":"forge-read","provider":"novox","provider-module":"forge",` + + `"provisions":[{"provision":"postgres-database","provider":"node-c","provider-module":"postgres"},` + + `{"provision":"s3-bucket","local":"hot","provider":"node-a","provider-module":"minio"},` + + `{"name":"forge-read","provision":"forge-read","provider":"node-c","provider-module":"forge",` + `"path":"/var/lib/app/forge.secret"}]}}` - _, p, err := planOf("g14", "g14.app", []byte(value)) + _, p, err := planOf("node-b", "node-b.app", []byte(value)) if err != nil { t.Fatal(err) } - want := []PlannedProvision{{Provision: "postgres-database", Provider: "novox", ProviderModule: "postgres"}, - {Provision: "s3-bucket", Local: "hot", Provider: "ace", ProviderModule: "minio"}, - {Name: "forge-read", Provision: "forge-read", Provider: "novox", ProviderModule: "forge", + want := []PlannedProvision{{Provision: "postgres-database", Provider: "node-c", ProviderModule: "postgres"}, + {Provision: "s3-bucket", Local: "hot", Provider: "node-a", ProviderModule: "minio"}, + {Name: "forge-read", Provision: "forge-read", Provider: "node-c", ProviderModule: "forge", Path: "/var/lib/app/forge.secret"}} if len(p.Provisions) != len(want) { t.Fatalf("the plan's provisions are read as %+v, want %+v", p.Provisions, want) @@ -55,16 +55,16 @@ func TestAPlanNamingItsProvidersIsRead(t *testing.T) { // Each entry names the credential form of its provider's offer (issue 768), which the vault acts on: the client // reads it strictly, before the controller writes it, and refuses a form it does not know. func TestAPlanNamingEachProvidersCredentialFormIsRead(t *testing.T) { - value := `{"node":"g14","module":"app","provisioning":{` + + value := `{"node":"node-b","module":"app","provisioning":{` + `"templates":{"source":"x","digest":"sha256:` + strings.Repeat("0", 64) + `"},` + `"files":[{"id":"conf","path":"/etc/app.conf","template":"app.conf","owner-at-runtime":"owned"}],` + - `"provisions":[{"provision":"postgres-database","provider":"novox","provider-module":"postgres",` + + `"provisions":[{"provision":"postgres-database","provider":"node-c","provider-module":"postgres",` + `"credential-form":"pair"},` + - `{"provision":"sonarr-api","provider":"ace","provider-module":"sonarr","credential-form":"shared"},` + - `{"name":"forge-read","provision":"forge-read","provider":"novox","provider-module":"forge",` + + `{"provision":"sonarr-api","provider":"node-a","provider-module":"sonarr","credential-form":"shared"},` + + `{"name":"forge-read","provision":"forge-read","provider":"node-c","provider-module":"forge",` + `"credential-form":"issued","path":"/var/lib/app/forge.secret"},` + - `{"provision":"smtp-relay","provider":"ace","provider-module":"relay","credential-form":"given"}]}}` - _, p, err := planOf("g14", "g14.app", []byte(value)) + `{"provision":"smtp-relay","provider":"node-a","provider-module":"relay","credential-form":"given"}]}}` + _, p, err := planOf("node-b", "node-b.app", []byte(value)) if err != nil { t.Fatal(err) } @@ -76,7 +76,7 @@ func TestAPlanNamingEachProvidersCredentialFormIsRead(t *testing.T) { t.Fatalf("the credential forms are read as %v", forms) } bad := strings.Replace(value, `"credential-form":"pair"`, `"credential-form":"one-for-all"`, 1) - if _, _, err := planOf("g14", "g14.app", []byte(bad)); err == nil || !strings.Contains(err.Error(), "one-for-all") { + if _, _, err := planOf("node-b", "node-b.app", []byte(bad)); err == nil || !strings.Contains(err.Error(), "one-for-all") { t.Fatalf("a credential form the client does not know was taken: %v", err) } } @@ -84,12 +84,12 @@ func TestAPlanNamingEachProvidersCredentialFormIsRead(t *testing.T) { // A consumer's plan entry may carry what its module contributes to the provider (issue 776): the client reads it // strictly, keeps it on the entry, and still refuses any other field it does not know. func TestAPlanEntryCarryingItsContributionIsRead(t *testing.T) { - value := `{"node":"g14","module":"app","provisioning":{` + + value := `{"node":"node-b","module":"app","provisioning":{` + `"templates":{"source":"x","digest":"sha256:` + strings.Repeat("0", 64) + `"},` + `"files":[{"id":"conf","path":"/etc/app.conf","template":"app.conf","owner-at-runtime":"owned"}],` + - `"provisions":[{"provision":"postgres-database","provider":"novox","provider-module":"postgres",` + + `"provisions":[{"provision":"postgres-database","provider":"node-c","provider-module":"postgres",` + `"contributes":{"name":"nextcloud","extensions":["pg_trgm"]}}]}}` - _, p, err := planOf("g14", "g14.app", []byte(value)) + _, p, err := planOf("node-b", "node-b.app", []byte(value)) if err != nil { t.Fatal(err) } @@ -98,7 +98,7 @@ func TestAPlanEntryCarryingItsContributionIsRead(t *testing.T) { t.Fatalf("the plan's provisions are read as %+v, want the contribution %v kept", p.Provisions, want) } bad := strings.Replace(value, `"contributes":`, `"contribution":`, 1) - if _, _, err := planOf("g14", "g14.app", []byte(bad)); err == nil { + if _, _, err := planOf("node-b", "node-b.app", []byte(bad)); err == nil { t.Error("a plan entry with a field the client does not know was taken") } } diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/pulled_in_test.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/pulled_in_test.go index 23388bec..e4c8d90f 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/pulled_in_test.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/pulled_in_test.go @@ -28,14 +28,14 @@ func TestAPulledInModulesValueIsReadForItsPlan(t *testing.T) { if err != nil { t.Fatal(err) } - value := fmt.Sprintf(`{"node":"g14","module":"mesh-cli","required-by":"desk","provisioning":%s}`, p) - module, got, err := planOf("g14", "g14.mesh-cli", []byte(value)) + value := fmt.Sprintf(`{"node":"node-b","module":"mesh-cli","required-by":"desk","provisioning":%s}`, p) + module, got, err := planOf("node-b", "node-b.mesh-cli", []byte(value)) if err != nil || module != "mesh-cli" || len(got.Items) != 1 || got.Items[0].Collection != "shell:zsh:normal" { t.Fatalf("a pulled-in module's value reads as %s %+v (%v)", module, got, err) } // Still strict in the plan. spoilt := strings.Replace(value, `"items":`, `"surprise":1,"items":`, 1) - if _, _, err := planOf("g14", "g14.mesh-cli", []byte(spoilt)); err == nil { + if _, _, err := planOf("node-b", "node-b.mesh-cli", []byte(spoilt)); err == nil { t.Fatalf("a pulled-in module's plan with an unknown field is acted on: %s", spoilt) } } @@ -45,8 +45,8 @@ func TestNatsAPulledInModuleIsFilledWithItsSecretAndItsItemIsRecorded(t *testing heard := b.listen(t) vault := startVault(t, b) const token, password = "tok-765-never-on-the-bus", "pw-765-never-on-the-bus" - vault.keep("own/g14/app/app-token", token, false) - vault.keep("own/g14/app/db-password", password, false) + vault.keep("own/node-b/app/app-token", token, false) + vault.keep("own/node-b/app/db-password", password, false) e := startEngine(t, map[string]string{appConf: "owned", appDB: "owned", zshrc: "owned"}) b.config(t, "app", "font-size", "13") b.pullIn(t, "app", "desk", app(t)) @@ -60,7 +60,7 @@ func TestNatsAPulledInModuleIsFilledWithItsSecretAndItsItemIsRecorded(t *testing eventually(t, "its secret's own file holds it alone", func() bool { return e.read(t, appDB) == password }) eventually(t, "the pulled-in contributor's item is recorded", func() bool { got := b.keys(t, CollectBucket, node+".") - return len(got) == 1 && got[0] == "g14.shell=3Azsh=3Anormal.mesh-cli" + return len(got) == 1 && got[0] == "node-b.shell=3Azsh=3Anormal.mesh-cli" }) eventually(t, "zshrc holds the pulled-in contributor's item", func() bool { return e.read(t, zshrc) == "# zsh\n# mesh-cli\nalias nox=mesh-cli\n# then\n# end\n" diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/route_names_test.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/route_names_test.go index 08ca9042..6b2db560 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/route_names_test.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/route_names_test.go @@ -55,7 +55,7 @@ func routeEntryFor(t *testing.T, module, planEntry string) string { bus := &memBus{buckets: map[string]map[string][]byte{provider.AssignmentsBucket: { node + "." + module: []byte(`{"node":"` + node + `","module":"` + module + `","provisioning":{"provisions":[` + planEntry + `]}}`)}}} - p := provider.New(provider.Options{Node: "novox", Module: "route-proxy", Bus: bus, Logf: t.Logf, + p := provider.New(provider.Options{Node: "node-c", Module: "route-proxy", Bus: bus, Logf: t.Logf, Offers: map[string]provider.Offer{"route": {Credential: provider.NoCredential, Serves: map[string]any{}}}}) out, err := p.Pass(context.Background()) if err != nil { @@ -81,7 +81,7 @@ func routeRun(t *testing.T, module, id, path, templatePath, entryKey, firstEntry tpl, _ := templates(t, map[string]string{id: string(raw)}) b.assign(t, module, Plan{Templates: tpl, Files: []PlannedFile{{ID: id, Path: path, Template: id, OwnerAtRuntime: "owned"}}, - Provisions: []PlannedProvision{{Provision: "route", Provider: "novox", ProviderModule: "route-proxy"}}}) + Provisions: []PlannedProvision{{Provision: "route", Provider: "node-c", ProviderModule: "route-proxy"}}}) kv, err := b.domain.CreateKeyValue(context.Background(), jetstream.KeyValueConfig{Bucket: "route-proxy_bound"}) if err != nil { t.Fatal(err) @@ -95,14 +95,14 @@ func routeRun(t *testing.T, module, id, path, templatePath, entryKey, firstEntry } // gitea's plan entry for its `web` route, as the controller writes it for a node whose public domain is example.org. -const giteaRoutePlanEntry = `{"provision":"route","provider":"novox","provider-module":"route-proxy","credential-form":"pair",` + - `"at":"novox.internal","from":"novox","as":"mesh_g14_gitea",` + - `"names":{"name-web":"git.example.org","internal-name-web":"git.novox.internal",` + - `"name-internal-api-refused":"git.example.org","internal-name-internal-api-refused":"git.novox.internal"}}` +const giteaRoutePlanEntry = `{"provision":"route","provider":"node-c","provider-module":"route-proxy","credential-form":"pair",` + + `"at":"node-c.internal","from":"node-c","as":"mesh_node-b_gitea",` + + `"names":{"name-web":"git.example.org","internal-name-web":"git.node-c.internal",` + + `"name-internal-api-refused":"git.example.org","internal-name-internal-api-refused":"git.node-c.internal"}}` -const keycloakRoutePlanEntry = `{"provision":"route","provider":"novox","provider-module":"route-proxy","credential-form":"pair",` + - `"at":"novox.internal","from":"novox","as":"mesh_g14_keycloak",` + - `"names":{"name":"keycloak.example.org","internal-name":"keycloak.novox.internal"}}` +const keycloakRoutePlanEntry = `{"provision":"route","provider":"node-c","provider-module":"route-proxy","credential-form":"pair",` + + `"at":"node-c.internal","from":"node-c","as":"mesh_node-b_keycloak",` + + `"names":{"name":"keycloak.example.org","internal-name":"keycloak.node-c.internal"}}` func TestNatsGiteasOriginsAreFilledFromRouteProxysEntryForThePlanEntryTheControllerWrites(t *testing.T) { e, _, _ := routeRun(t, "gitea", "origins", "/etc/gitea/origins", "../../../gitea/files/origins", "gitea.route", @@ -123,8 +123,8 @@ func TestNatsKeycloaksHostnameIsFilledFromRouteProxysEntryForThePlanEntryTheCont // A module that failed on a missing name is filled once the provider's entry carries it: the entry first held no // names (as the provider wrote it before the controller composed them), then the controller's plan entry arrives. func TestNatsAModuleThatFailedOnAMissingNameIsFilledWhenTheNameArrives(t *testing.T) { - bare := routeEntryFor(t, "gitea", `{"provision":"route","provider":"novox","provider-module":"route-proxy",`+ - `"as":"mesh_g14_gitea"}`) + bare := routeEntryFor(t, "gitea", `{"provision":"route","provider":"node-c","provider-module":"route-proxy",`+ + `"as":"mesh_node-b_gitea"}`) e, b, heard := routeRun(t, "gitea", "origins", "/etc/gitea/origins", "../../../gitea/files/origins", "gitea.route", bare) eventually(t, "the module fails for the missing name", func() bool { r, _ := heard.last("gitea") diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/secrets_test.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/secrets_test.go index 1538edb5..0b57e677 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/secrets_test.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/secrets_test.go @@ -22,7 +22,7 @@ import ( // sealing to the key the caller sends, handing only what it allows that caller (the vault's own tests prove its // rule), and saying `secret-changed` with no value. -// standInVault answers get on the bus, for provisioner.g14 alone and only the names it allows. +// standInVault answers get on the bus, for provisioner.node-b alone and only the names it allows. type standInVault struct { t *testing.T conn *nats.Conn @@ -155,8 +155,8 @@ func TestNatsAMovedModulesFileIsFilledWithItsSecretFromTheVault(t *testing.T) { wire := sniff(t, b) vault := startVault(t, b) const token, password = "tok-3f9a1c-never-on-the-bus", "pw-77b2e0-never-on-the-bus" - vault.keep("own/g14/app/app-token", token, false) - vault.keep("own/g14/app/db-password", password, false) + vault.keep("own/node-b/app/app-token", token, false) + vault.keep("own/node-b/app/db-password", password, false) e := startEngine(t, map[string]string{appConf: "owned", appDB: "owned"}) b.config(t, "app", "font-size", "13") b.assign(t, "app", app(t)) @@ -172,7 +172,7 @@ func TestNatsAMovedModulesFileIsFilledWithItsSecretFromTheVault(t *testing.T) { }) eventually(t, "the needs name the secrets, never their values", func() bool { got := b.keys(t, NeedsBucket, node+".app.") - return strings.Join(got, " ") == "g14.app.config.font-size g14.app.secret.app-token g14.app.secret.db-password" + return strings.Join(got, " ") == "node-b.app.config.font-size node-b.app.secret.app-token node-b.app.secret.db-password" }) if wire.heard() == 0 { t.Fatal("the sniffer heard nothing: it proves nothing") @@ -182,9 +182,9 @@ func TestNatsAMovedModulesFileIsFilledWithItsSecretFromTheVault(t *testing.T) { t.Errorf("the value crossed the bus in the clear: %q", said) } } - if vault.times("own/g14/app/app-token") != 1 || vault.times("own/g14/app/db-password") != 1 { - t.Errorf("the vault was asked %d and %d times, want once each", vault.times("own/g14/app/app-token"), - vault.times("own/g14/app/db-password")) + if vault.times("own/node-b/app/app-token") != 1 || vault.times("own/node-b/app/db-password") != 1 { + t.Errorf("the vault was asked %d and %d times, want once each", vault.times("own/node-b/app/app-token"), + vault.times("own/node-b/app/db-password")) } } @@ -192,8 +192,8 @@ func TestNatsASecretRotatedInTheVaultReachesItsFileWithoutANewDeclaration(t *tes b := startBus(t) heard := b.listen(t) vault := startVault(t, b) - vault.keep("own/g14/app/app-token", "first", false) - vault.keep("own/g14/app/db-password", "pw", false) + vault.keep("own/node-b/app/app-token", "first", false) + vault.keep("own/node-b/app/db-password", "pw", false) e := startEngine(t, map[string]string{appConf: "owned", appDB: "owned"}) b.config(t, "app", "font-size", "13") b.assign(t, "app", app(t)) @@ -204,7 +204,7 @@ func TestNatsASecretRotatedInTheVaultReachesItsFileWithoutANewDeclaration(t *tes t.Fatal(err) } - vault.keep("own/g14/app/app-token", "second", true) + vault.keep("own/node-b/app/app-token", "second", true) eventually(t, "the rotated value reaches its file", func() bool { return strings.Contains(e.read(t, appConf), "token = second\n") }) @@ -223,13 +223,13 @@ func TestNatsASecretRotatedInTheVaultReachesItsFileWithoutANewDeclaration(t *tes t.Errorf("the other secret's file moved %d times: only what changed is written", moved) } // What another machine's secret, or a secret no plan here names, changes is no reason to ask. - vault.keep("own/ace/app/app-token", "elsewhere", true) - vault.keep("own/g14/other/token", "unplanned", true) + vault.keep("own/node-a/app/app-token", "elsewhere", true) + vault.keep("own/node-b/other/token", "unplanned", true) time.Sleep(300 * time.Millisecond) - if n := vault.times("own/g14/app/app-token"); n != 2 { + if n := vault.times("own/node-b/app/app-token"); n != 2 { t.Errorf("the vault was asked for app-token %d times, want 2", n) } - if vault.times("own/ace/app/app-token")+vault.times("own/g14/other/token") != 0 { + if vault.times("own/node-a/app/app-token")+vault.times("own/node-b/other/token") != 0 { t.Error("a change of a secret this machine's plans do not name made the client ask") } } @@ -238,8 +238,8 @@ func TestNatsASecretTheVaultDoesNotHoldYetIsWaitedFor(t *testing.T) { b := startBus(t) heard := b.listen(t) vault := startVault(t, b) - vault.keep("own/g14/app/db-password", "pw", false) - vault.allowed["own/g14/app/app-token"] = true // the plan names it; the vault does not hold it yet + vault.keep("own/node-b/app/db-password", "pw", false) + vault.allowed["own/node-b/app/app-token"] = true // the plan names it; the vault does not hold it yet e := startEngine(t, map[string]string{appConf: "owned", appDB: "owned"}) b.config(t, "app", "font-size", "13") b.assign(t, "app", app(t)) @@ -255,7 +255,7 @@ func TestNatsASecretTheVaultDoesNotHoldYetIsWaitedFor(t *testing.T) { } // 1553: a file whose own values are all there is filled, though another file of the module waits. eventually(t, "the secret's own file is filled while the template waits", func() bool { return e.read(t, appDB) == "pw" }) - vault.keep("own/g14/app/app-token", "arrived", true) + vault.keep("own/node-b/app/app-token", "arrived", true) eventually(t, "it is filled once the vault holds it", func() bool { return strings.Contains(e.read(t, appConf), "token = arrived\n") && e.read(t, appDB) == "pw" }) @@ -267,9 +267,9 @@ func TestNatsARefusalFailsTheModuleWithTheVaultsReasonAndIsNotAskedEveryPass(t * b := startBus(t) heard := b.listen(t) vault := startVault(t, b) - vault.keep("own/g14/app/app-token", "tok", false) - vault.keep("own/g14/app/db-password", "pw", false) - vault.refuse = "provisioner.g14 may not ask for own/g14/app/app-token: app's plan on g14 names no secret app-token" + vault.keep("own/node-b/app/app-token", "tok", false) + vault.keep("own/node-b/app/db-password", "pw", false) + vault.refuse = "provisioner.node-b may not ask for own/node-b/app/app-token: app's plan on node-b names no secret app-token" e := startEngine(t, map[string]string{appConf: "owned", appDB: "owned"}) b.config(t, "app", "font-size", "13") b.assign(t, "app", app(t)) @@ -285,7 +285,7 @@ func TestNatsARefusalFailsTheModuleWithTheVaultsReasonAndIsNotAskedEveryPass(t * time.Sleep(100 * time.Millisecond) } time.Sleep(200 * time.Millisecond) - if n := vault.times("own/g14/app/app-token"); n != 1 { + if n := vault.times("own/node-b/app/app-token"); n != 1 { t.Errorf("a refused secret was asked %d times across several reconciles, want once", n) } if _, asked := e.counts(appConf); asked != 0 { @@ -338,16 +338,16 @@ func TestAPlansSecretsAreReadStrictly(t *testing.T) { const ( forgeFile = "/var/lib/claude-code/state/gitea-read.secret" settingsFile = "/etc/claude-code/managed-settings.json" - forgeHolder = "provision/novox/gitea-read/" + node + "/claude-code" + forgeHolder = "provision/node-c/gitea-read/" + node + "/claude-code" ) -// claudeCode is claude-code's plan with the credential of what it requires, gitea-read from novox, kept in a file of +// claudeCode is claude-code's plan with the credential of what it requires, gitea-read from node-c, kept in a file of // its own and named in a template. func claudeCode(t *testing.T) Plan { tpl, _ := templates(t, map[string]string{"managed-settings.json": `{"forge":"${secret:gitea-read}"}`}) return Plan{Templates: tpl, Files: []PlannedFile{{ID: "settings", Path: settingsFile, Template: "managed-settings.json", OwnerAtRuntime: "owned"}}, - Provisions: []PlannedProvision{{Name: "gitea-read", Provision: "gitea-read", Provider: "novox", + Provisions: []PlannedProvision{{Name: "gitea-read", Provision: "gitea-read", Provider: "node-c", Path: forgeFile}}} } @@ -356,8 +356,8 @@ func TestNatsAModulesRequiredCredentialIssuedForItIsFilledFromTheVaultAndFollows heard := b.listen(t) wire := sniff(t, b) vault := startVault(t, b) - const first = "https://agent-g14:gta-first-never-on-the-bus@forge.example" - const second = "https://agent-g14:gta-second-never-on-the-bus@forge.example" + const first = "https://agent-node-b:gta-first-never-on-the-bus@forge.example" + const second = "https://agent-node-b:gta-second-never-on-the-bus@forge.example" vault.keep(forgeHolder, first, false) e := startEngine(t, map[string]string{forgeFile: "owned", settingsFile: "owned"}) b.assign(t, "claude-code", claudeCode(t)) @@ -378,13 +378,13 @@ func TestNatsAModulesRequiredCredentialIssuedForItIsFilledFromTheVaultAndFollows return e.read(t, forgeFile) == second }) // Another machine's or another module's holder value changing is no reason to ask. - vault.keep("provision/novox/gitea-read/ace/claude-code", "elsewhere", true) - vault.keep("provision/novox/gitea-read/"+node+"/zsh", "unplanned", true) + vault.keep("provision/node-c/gitea-read/node-a/claude-code", "elsewhere", true) + vault.keep("provision/node-c/gitea-read/"+node+"/zsh", "unplanned", true) time.Sleep(300 * time.Millisecond) if n := vault.times(forgeHolder); n != 2 { t.Errorf("the vault was asked for the holder value %d times, want 2", n) } - if vault.times("provision/novox/gitea-read/ace/claude-code")+vault.times("provision/novox/gitea-read/"+node+"/zsh") != 0 { + if vault.times("provision/node-c/gitea-read/node-a/claude-code")+vault.times("provision/node-c/gitea-read/"+node+"/zsh") != 0 { t.Error("a change of a value no plan here names made the client ask") } if wire.heard() == 0 { @@ -402,8 +402,8 @@ func TestNatsARequiredCredentialTheVaultRefusesFailsTheModule(t *testing.T) { b := startBus(t) heard := b.listen(t) vault := startVault(t, b) - vault.keep(forgeHolder, "https://agent-g14:tok@forge.example", false) - vault.refuse = "provisioner.g14 may not ask for " + forgeHolder + ": claude-code's plan on g14 names no credential of gitea-read from novox" + vault.keep(forgeHolder, "https://agent-node-b:tok@forge.example", false) + vault.refuse = "provisioner.node-b may not ask for " + forgeHolder + ": claude-code's plan on node-b names no credential of gitea-read from node-c" e := startEngine(t, map[string]string{forgeFile: "owned", settingsFile: "owned"}) b.assign(t, "claude-code", claudeCode(t)) aClient(t, b, e, t.TempDir()) @@ -423,12 +423,12 @@ func TestAPlansProvisionsAreReadStrictly(t *testing.T) { Files: []PlannedFile{{ID: "conf", Path: appConf, Template: "app.conf", OwnerAtRuntime: "owned"}}, Secrets: []PlannedSecret{{Name: "own"}}} } - good := PlannedProvision{Name: "gitea-read", Provision: "gitea-read", Provider: "novox", Path: forgeFile} + good := PlannedProvision{Name: "gitea-read", Provision: "gitea-read", Provider: "node-c", Path: forgeFile} for why, change := range map[string]func(*PlannedProvision){ "a name that is not a secret's": func(p *PlannedProvision) { p.Name = "Gitea Read" }, "a name an own secret has": func(p *PlannedProvision) { p.Name = "own" }, "no provision": func(p *PlannedProvision) { p.Provision = "" }, - "a provider that is no machine": func(p *PlannedProvision) { p.Provider = "../ace" }, + "a provider that is no machine": func(p *PlannedProvision) { p.Provider = "../node-a" }, "no provider": func(p *PlannedProvision) { p.Provider = "" }, "a relative path": func(p *PlannedProvision) { p.Path = "var/x" }, "a template's path": func(p *PlannedProvision) { p.Path = appConf }, @@ -455,14 +455,14 @@ func TestAPlansProvisionsAreReadStrictly(t *testing.T) { const ( dbFile = "/var/lib/nextcloud/state/database.secret" dbConf = "/etc/nextcloud/config.php" - dbHolder = "pair/novox/postgres-database/" + node + "/nextcloud" + dbHolder = "pair/node-c/postgres-database/" + node + "/nextcloud" ) func nextcloud(t *testing.T) Plan { tpl, _ := templates(t, map[string]string{"config.php": `dbpass=${secret:database}`}) return Plan{Templates: tpl, Files: []PlannedFile{{ID: "config", Path: dbConf, Template: "config.php", OwnerAtRuntime: "owned"}}, - Provisions: []PlannedProvision{{Name: "database", Provision: "postgres-database", Provider: "novox", + Provisions: []PlannedProvision{{Name: "database", Provision: "postgres-database", Provider: "node-c", ProviderModule: "postgres", CredentialForm: "pair", Path: dbFile}}} } @@ -494,7 +494,7 @@ func TestNatsAModulesOwnLoginFromAPerLoginProviderIsFilledFromTheVaultAndFollows return e.read(t, dbFile) == second }) // Another consumer's login changing is no reason to ask. - other := "pair/novox/postgres-database/ace/nextcloud" + other := "pair/node-c/postgres-database/node-a/nextcloud" vault.keep(other, "elsewhere", true) time.Sleep(300 * time.Millisecond) if n := vault.times(dbHolder); n != 2 { @@ -511,7 +511,7 @@ func TestNatsAModulesOwnLoginFromAPerLoginProviderIsFilledFromTheVaultAndFollows } func TestAConsumersLoginIsNamedWithItsLocalName(t *testing.T) { - if got := PairName("novox", "postgres-database", "g14", "umami", "stats"); got != "pair/novox/postgres-database/g14/umami/stats" { + if got := PairName("node-c", "postgres-database", "node-b", "umami", "stats"); got != "pair/node-c/postgres-database/node-b/umami/stats" { t.Errorf("a login under a local name: %s", got) } } @@ -522,8 +522,8 @@ func TestNatsAKnownSecretsFileIsFilledWhileATemplateWaits1553(t *testing.T) { b := startBus(t) heard := b.listen(t) vault := startVault(t, b) - vault.keep("own/g14/app/app-token", "tok", false) - vault.keep("own/g14/app/db-password", "pw", false) + vault.keep("own/node-b/app/app-token", "tok", false) + vault.keep("own/node-b/app/db-password", "pw", false) e := startEngine(t, map[string]string{appConf: "owned", appDB: "owned"}) // No config font-size: the template waits for it. b.assign(t, "app", app(t)) @@ -546,8 +546,8 @@ func TestNatsATemplateWithAWaitingValueKeepsItsFileAsItIs1553(t *testing.T) { b := startBus(t) heard := b.listen(t) vault := startVault(t, b) - vault.keep("own/g14/app/app-token", "tok", false) - vault.keep("own/g14/app/db-password", "pw", false) + vault.keep("own/node-b/app/app-token", "tok", false) + vault.keep("own/node-b/app/db-password", "pw", false) e := startEngine(t, map[string]string{appConf: "owned", appDB: "owned"}) b.assign(t, "app", app(t)) aClient(t, b, e, t.TempDir()) diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/testdata/accounts.conf b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/testdata/accounts.conf index 85e77ee0..42c7ca44 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/testdata/accounts.conf +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/testdata/accounts.conf @@ -10,14 +10,14 @@ accounts { jetstream: enabled users = [ { user: "domain", password: "x" } - { user: "provisioner.g14", password: "x", permissions: { - publish: { allow: ["$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.g14.>", "$JS.API.CONSUMER.CREATE.KV_mesh-controller_assignments.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_applied.*.$KV.mesh-provisioner_applied.g14.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_collect.*.$KV.mesh-provisioner_collect.g14.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_needs.*.$KV.mesh-provisioner_needs.g14.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_registry.>", "$JS.API.CONSUMER.CREATE.KV_postgres_bound.*.$KV.postgres_bound.g14.>", "$JS.API.CONSUMER.CREATE.KV_route-adapter_bound.*.$KV.route-adapter_bound.g14.>", "$JS.API.CONSUMER.CREATE.KV_route-proxy_bound.*.$KV.route-proxy_bound.g14.>", "$JS.API.CONSUMER.CREATE.KV_node-tools_bound.*.$KV.node-tools_bound.g14.>", "$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.>", "$JS.API.CONSUMER.DELETE.KV_mesh-controller_assignments.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_applied.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_collect.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_needs.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_registry.>", "$JS.API.CONSUMER.DELETE.KV_postgres_bound.>", "$JS.API.CONSUMER.DELETE.KV_route-adapter_bound.>", "$JS.API.CONSUMER.DELETE.KV_route-proxy_bound.>", "$JS.API.CONSUMER.DELETE.KV_node-tools_bound.>", "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.g14.>", "$JS.API.DIRECT.GET.KV_mesh-controller_assignments.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_applied.$KV.mesh-provisioner_applied.g14.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_collect.$KV.mesh-provisioner_collect.g14.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_needs.$KV.mesh-provisioner_needs.g14.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_registry.>", "$JS.API.DIRECT.GET.KV_postgres_bound.$KV.postgres_bound.g14.>", "$JS.API.DIRECT.GET.KV_route-adapter_bound.$KV.route-adapter_bound.g14.>", "$JS.API.DIRECT.GET.KV_route-proxy_bound.$KV.route-proxy_bound.g14.>", "$JS.API.DIRECT.GET.KV_node-tools_bound.$KV.node-tools_bound.g14.>", "$JS.API.STREAM.INFO.KV_mesh-config_resolved", "$JS.API.STREAM.INFO.KV_mesh-controller_assignments", "$JS.API.STREAM.INFO.KV_mesh-provisioner_applied", "$JS.API.STREAM.INFO.KV_mesh-provisioner_collect", "$JS.API.STREAM.INFO.KV_mesh-provisioner_needs", "$JS.API.STREAM.INFO.KV_mesh-provisioner_registry", "$JS.API.STREAM.INFO.KV_postgres_bound", "$JS.API.STREAM.INFO.KV_route-adapter_bound", "$JS.API.STREAM.INFO.KV_route-proxy_bound", "$JS.API.STREAM.INFO.KV_node-tools_bound", "$JS.FC.KV_mesh-config_resolved.>", "$JS.FC.KV_mesh-controller_assignments.>", "$JS.FC.KV_mesh-provisioner_applied.>", "$JS.FC.KV_mesh-provisioner_collect.>", "$JS.FC.KV_mesh-provisioner_needs.>", "$JS.FC.KV_mesh-provisioner_registry.>", "$JS.FC.KV_postgres_bound.>", "$JS.FC.KV_route-adapter_bound.>", "$JS.FC.KV_route-proxy_bound.>", "$JS.FC.KV_node-tools_bound.>", "$KV.mesh-provisioner_applied.g14.>", "$KV.mesh-provisioner_collect.g14.>", "$KV.mesh-provisioner_needs.g14.>", "mesh.seat.mesh-vault.call.get.provisioner~g14", "prov.applied.g14.>", "prov.changed.g14.>"] } - subscribe: { allow: ["_INBOX.provisioner.g14.>", "mesh.mod.mesh-vault.event.secret-changed", "prov.ask.g14"] } + { user: "provisioner.node-b", password: "x", permissions: { + publish: { allow: ["$JS.API.CONSUMER.CREATE.KV_mesh-config_resolved.*.$KV.mesh-config_resolved.node-b.>", "$JS.API.CONSUMER.CREATE.KV_mesh-controller_assignments.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_applied.*.$KV.mesh-provisioner_applied.node-b.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_collect.*.$KV.mesh-provisioner_collect.node-b.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_needs.*.$KV.mesh-provisioner_needs.node-b.>", "$JS.API.CONSUMER.CREATE.KV_mesh-provisioner_registry.>", "$JS.API.CONSUMER.CREATE.KV_postgres_bound.*.$KV.postgres_bound.node-b.>", "$JS.API.CONSUMER.CREATE.KV_route-adapter_bound.*.$KV.route-adapter_bound.node-b.>", "$JS.API.CONSUMER.CREATE.KV_route-proxy_bound.*.$KV.route-proxy_bound.node-b.>", "$JS.API.CONSUMER.CREATE.KV_node-tools_bound.*.$KV.node-tools_bound.node-b.>", "$JS.API.CONSUMER.DELETE.KV_mesh-config_resolved.>", "$JS.API.CONSUMER.DELETE.KV_mesh-controller_assignments.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_applied.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_collect.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_needs.>", "$JS.API.CONSUMER.DELETE.KV_mesh-provisioner_registry.>", "$JS.API.CONSUMER.DELETE.KV_postgres_bound.>", "$JS.API.CONSUMER.DELETE.KV_route-adapter_bound.>", "$JS.API.CONSUMER.DELETE.KV_route-proxy_bound.>", "$JS.API.CONSUMER.DELETE.KV_node-tools_bound.>", "$JS.API.DIRECT.GET.KV_mesh-config_resolved.$KV.mesh-config_resolved.node-b.>", "$JS.API.DIRECT.GET.KV_mesh-controller_assignments.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_applied.$KV.mesh-provisioner_applied.node-b.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_collect.$KV.mesh-provisioner_collect.node-b.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_needs.$KV.mesh-provisioner_needs.node-b.>", "$JS.API.DIRECT.GET.KV_mesh-provisioner_registry.>", "$JS.API.DIRECT.GET.KV_postgres_bound.$KV.postgres_bound.node-b.>", "$JS.API.DIRECT.GET.KV_route-adapter_bound.$KV.route-adapter_bound.node-b.>", "$JS.API.DIRECT.GET.KV_route-proxy_bound.$KV.route-proxy_bound.node-b.>", "$JS.API.DIRECT.GET.KV_node-tools_bound.$KV.node-tools_bound.node-b.>", "$JS.API.STREAM.INFO.KV_mesh-config_resolved", "$JS.API.STREAM.INFO.KV_mesh-controller_assignments", "$JS.API.STREAM.INFO.KV_mesh-provisioner_applied", "$JS.API.STREAM.INFO.KV_mesh-provisioner_collect", "$JS.API.STREAM.INFO.KV_mesh-provisioner_needs", "$JS.API.STREAM.INFO.KV_mesh-provisioner_registry", "$JS.API.STREAM.INFO.KV_postgres_bound", "$JS.API.STREAM.INFO.KV_route-adapter_bound", "$JS.API.STREAM.INFO.KV_route-proxy_bound", "$JS.API.STREAM.INFO.KV_node-tools_bound", "$JS.FC.KV_mesh-config_resolved.>", "$JS.FC.KV_mesh-controller_assignments.>", "$JS.FC.KV_mesh-provisioner_applied.>", "$JS.FC.KV_mesh-provisioner_collect.>", "$JS.FC.KV_mesh-provisioner_needs.>", "$JS.FC.KV_mesh-provisioner_registry.>", "$JS.FC.KV_postgres_bound.>", "$JS.FC.KV_route-adapter_bound.>", "$JS.FC.KV_route-proxy_bound.>", "$JS.FC.KV_node-tools_bound.>", "$KV.mesh-provisioner_applied.node-b.>", "$KV.mesh-provisioner_collect.node-b.>", "$KV.mesh-provisioner_needs.node-b.>", "mesh.seat.mesh-vault.call.get.provisioner~node-b", "prov.applied.node-b.>", "prov.changed.node-b.>"] } + subscribe: { allow: ["_INBOX.provisioner.node-b.>", "mesh.mod.mesh-vault.event.secret-changed", "prov.ask.node-b"] } allow_responses: { max: 1, ttl: "1m" } } } - { user: "g14.node-tools", password: "x", permissions: { - publish: { allow: ["$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.g14.*", "$SRV.INFO", "$SRV.INFO.>", "$SRV.PING", "$SRV.PING.>", "mesh.control.g14.tools-alive", "mesh.mod.*.call.*.*.g14~node-tools", "mesh.mod.*.call.*.g14~node-tools", "mesh.mod.*.tool.>", "mesh.seat.*.call.*.*.g14~node-tools", "mesh.seat.*.call.*.g14~node-tools", "mesh.seat.*.tool.>", "prov.ask.g14"], deny: ["mesh.seat.node-launcher.tool.secret", "mesh.seat.node-launcher.tool.secret.*", "mesh.mod.*.tool.node-launcher.secret", "mesh.mod.*.tool.node-launcher.secret.*", "mesh.seat.node-launcher.call.secret.>", "mesh.mod.*.call.node-launcher.secret.>"] } - subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.claude-code", "$SRV.INFO.claude-code.>", "$SRV.INFO.node-tools", "$SRV.INFO.node-tools.>", "$SRV.PING", "$SRV.PING.claude-code", "$SRV.PING.claude-code.>", "$SRV.PING.node-tools", "$SRV.PING.node-tools.>", "$SRV.STATS", "$SRV.STATS.claude-code", "$SRV.STATS.claude-code.>", "$SRV.STATS.node-tools", "$SRV.STATS.node-tools.>", "_INBOX.g14.node-tools.>", "mesh.assignment.g14.*", "mesh.mod.claude-code.call.>", "mesh.mod.claude-code.tool.>", "prov.changed.g14.>"] } + { user: "node-b.node-tools", password: "x", permissions: { + publish: { allow: ["$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.node-b.*", "$SRV.INFO", "$SRV.INFO.>", "$SRV.PING", "$SRV.PING.>", "mesh.control.node-b.tools-alive", "mesh.mod.*.call.*.*.node-b~node-tools", "mesh.mod.*.call.*.node-b~node-tools", "mesh.mod.*.tool.>", "mesh.seat.*.call.*.*.node-b~node-tools", "mesh.seat.*.call.*.node-b~node-tools", "mesh.seat.*.tool.>", "prov.ask.node-b"], deny: ["mesh.seat.node-launcher.tool.secret", "mesh.seat.node-launcher.tool.secret.*", "mesh.mod.*.tool.node-launcher.secret", "mesh.mod.*.tool.node-launcher.secret.*", "mesh.seat.node-launcher.call.secret.>", "mesh.mod.*.call.node-launcher.secret.>"] } + subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.claude-code", "$SRV.INFO.claude-code.>", "$SRV.INFO.node-tools", "$SRV.INFO.node-tools.>", "$SRV.PING", "$SRV.PING.claude-code", "$SRV.PING.claude-code.>", "$SRV.PING.node-tools", "$SRV.PING.node-tools.>", "$SRV.STATS", "$SRV.STATS.claude-code", "$SRV.STATS.claude-code.>", "$SRV.STATS.node-tools", "$SRV.STATS.node-tools.>", "_INBOX.node-b.node-tools.>", "mesh.assignment.node-b.*", "mesh.mod.claude-code.call.>", "mesh.mod.claude-code.tool.>", "prov.changed.node-b.>"] } allow_responses: { max: 1, ttl: "1m" } } } ] diff --git a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/whole_plan_test.go b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/whole_plan_test.go index 1c2efa79..08339f08 100644 --- a/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/whole_plan_test.go +++ b/modules/mesh-provisioner-client/cmd/mesh-provisioner-client/whole_plan_test.go @@ -16,24 +16,24 @@ func TestAPlanProvisionWithEveryFieldTheControllerWritesIsRead(t *testing.T) { if err != nil { t.Fatal(err) } - provision := `"provisions":[{"provision":"route","local":"web","provider":"novox","provider-module":"route-proxy",` + - `"credential-form":"pair","contributes":{"label":"git"},"at":"10.10.0.1","from":"novox","as":"gitea",` + - `"names":{"name-web":"git.example.org"}},{"name":"gitea-read","provision":"gitea-read","provider":"novox",` + + provision := `"provisions":[{"provision":"route","local":"web","provider":"node-c","provider-module":"route-proxy",` + + `"credential-form":"pair","contributes":{"label":"git"},"at":"10.10.0.1","from":"node-c","as":"gitea",` + + `"names":{"name-web":"git.example.org"}},{"name":"gitea-read","provision":"gitea-read","provider":"node-c",` + `"provider-module":"gitea","credential-form":"issued","credential-tool":"gitea_agent_token",` + `"path":"/var/lib/claude-code/gitea-read.secret"}],"items":` - value := fmt.Sprintf(`{"node":"g14","module":"mesh-cli","provisioning":%s}`, + value := fmt.Sprintf(`{"node":"node-b","module":"mesh-cli","provisioning":%s}`, strings.Replace(string(p), `"items":`, provision, 1)) - _, got, err := planOf("g14", "g14.mesh-cli", []byte(value)) + _, got, err := planOf("node-b", "node-b.mesh-cli", []byte(value)) if err != nil { t.Fatalf("a plan with the controller's fields is refused: %v", err) } - if len(got.Provisions) != 2 || got.Provisions[0].At != "10.10.0.1" || got.Provisions[0].From != "novox" || + if len(got.Provisions) != 2 || got.Provisions[0].At != "10.10.0.1" || got.Provisions[0].From != "node-c" || got.Provisions[0].As != "gitea" || got.Provisions[0].Names["name-web"] != "git.example.org" || got.Provisions[1].CredentialTool != "gitea_agent_token" { t.Fatalf("the plan's provisions read as %+v", got.Provisions) } spoilt := strings.Replace(value, `"at":`, `"surprise":1,"at":`, 1) - if _, _, err := planOf("g14", "g14.mesh-cli", []byte(spoilt)); err == nil { + if _, _, err := planOf("node-b", "node-b.mesh-cli", []byte(spoilt)); err == nil { t.Fatalf("a provision with a field nobody writes is read: %s", spoilt) } } diff --git a/modules/messenger/README.md b/modules/messenger/README.md index 0fbd1a41..e012824b 100644 --- a/modules/messenger/README.md +++ b/modules/messenger/README.md @@ -106,17 +106,17 @@ what is said is decided by the state now and by when a thing happened, never by what arrives meanwhile goes out, coalesced, when the minute ends. **A message reads in one glance** (novox/hq ADR 0253). Its title is the condition's headline — a few -plain words such as "openrazer not working on g14" — prefixed "Urgent:", "Still open:" or "Now urgent:" +plain words such as "openrazer not working on node-b" — prefixed "Urgent:", "Still open:" or "Now urgent:" where that applies; its body is the condition's explanation (what happened, what it means, whether the operator needs to act) and since when, in the operator's time zone. No key, id, commit, command or markup: those stay in the controller's `conditions` and in this module's history. A clearance is one line, the -condition's resolved line and how long it was open: "Resolved: openrazer works again on g14, after 26 min". +condition's resolved line and how long it was open: "Resolved: openrazer works again on node-b, after 26 min". A condition from a controller older than plain words, or a module's notice without a headline, is said from its summary, with code spans taken out (`cmd/messenger/words.go`). **A condition's message says which issue tracks it** (novox/hq ADR 0285, issue 400). Below the explanation, one line from the issue tracker's `tracking` verb, asked outside the work at most once a minute and used for -ten minutes: "Tracked as issue 386: fix-386 on g14 is fixing it since 21:10.", "…: nobody is working on it.", +ten minutes: "Tracked as issue 386: fix-386 on node-b is fixing it since 21:10.", "…: nobody is working on it.", "…: its fix is merged and on its way.", "…: it waits for your answer.", up to three issues and then a count; "No issue tracks this yet." when no issue links the condition's key; "Whether an issue tracks this could not be checked." when the tracker gave no answer in ten minutes. "Nothing for you to do." becomes "Needs you: diff --git a/modules/messenger/cmd/messenger/condition.go b/modules/messenger/cmd/messenger/condition.go index 1378d596..83c69552 100644 --- a/modules/messenger/cmd/messenger/condition.go +++ b/modules/messenger/cmd/messenger/condition.go @@ -100,7 +100,7 @@ func (c Condition) When(event string) time.Time { return time.Time{} } -// SubjectWords is the subject as a person reads it: "machine ace", "plan 41", "provider keycloak". +// SubjectWords is the subject as a person reads it: "machine node-a", "plan 41", "provider keycloak". func (c Condition) SubjectWords() string { parts := []string{} if c.Scope != "" { diff --git a/modules/messenger/cmd/messenger/content.go b/modules/messenger/cmd/messenger/content.go index 2c0b5552..beede419 100644 --- a/modules/messenger/cmd/messenger/content.go +++ b/modules/messenger/cmd/messenger/content.go @@ -145,7 +145,7 @@ func randomRun(text string) (Refusal, bool) { } // isPath: absolute, home-relative or dot-relative, or two separators deep. A mesh address names one -// machine and one tool (`ace/postgres.query`) and has one; a ratio ("3/4") has digits only. +// machine and one tool (`node-a/postgres.query`) and has one; a ratio ("3/4") has digits only. func isPath(w string) bool { if w == "" { return false @@ -162,7 +162,7 @@ func isPath(w string) bool { } // isHostName: two names or more, the last a top-level one. A condition key's last name is its kind -// (`machine.ace.silent`), which none of these is. +// (`machine.node-a.silent`), which none of these is. func isHostName(w string) bool { w = strings.ToLower(w) if w == "localhost" { diff --git a/modules/messenger/cmd/messenger/main.go b/modules/messenger/cmd/messenger/main.go index 57bccaaa..b58a0e4f 100644 --- a/modules/messenger/cmd/messenger/main.go +++ b/modules/messenger/cmd/messenger/main.go @@ -675,14 +675,14 @@ func tools(h *Holder, l *listening) []stdio.Tool { "Roles and words only: an address, a path or a secret is refused. Deduplicated, capped and routed " + "like the controller's conditions.", Input: map[string]any{ - "key": str("what makes it the same message the next time, e.g. backup.ace.failed"), + "key": str("what makes it the same message the next time, e.g. backup.node-a.failed"), "severity": map[string]any{"type": "string", "enum": []string{Urgent, Warning}}, "summary": str("one line in the mesh's words"), "subject": str("what it is about: a machine's role, a module, a plan"), - "headline": str("a few plain words the operator reads as the title, e.g. backup failed on ace: no ids, keys or commands"), + "headline": str("a few plain words the operator reads as the title, e.g. backup failed on node-a: no ids, keys or commands"), "explanation": str("one or two plain sentences: what happened, what it means for the operator, and whether " + "they need to act"), - "resolved": str("the one short line said when it is over, e.g. backups run again on ace"), + "resolved": str("the one short line said when it is over, e.g. backups run again on node-a"), "clear": map[string]any{"type": "boolean", "description": "it is over: the message is edited to say so"}, }, Run: func(a map[string]any) (any, error) { @@ -696,7 +696,7 @@ func tools(h *Holder, l *listening) []stdio.Tool { "words are, and offering Details like any message. Never escalated, never kept open, never reminded; " + "its own cap of 10 a channel an hour, apart from the conditions' and notify's.", Input: map[string]any{ - "headline": str("a few plain words the operator reads as the title, e.g. new mail arrived for ace"), + "headline": str("a few plain words the operator reads as the title, e.g. new mail arrived for node-a"), "lines": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "one or two plain lines"}, "subject": str("what it is about: a machine's role, a module, a plan"), }, diff --git a/modules/messenger/cmd/messenger/tracking.go b/modules/messenger/cmd/messenger/tracking.go index 9e0a484e..6a8d2202 100644 --- a/modules/messenger/cmd/messenger/tracking.go +++ b/modules/messenger/cmd/messenger/tracking.go @@ -326,7 +326,7 @@ func issueState(is TrackedIssue, now time.Time, loc *time.Location) string { return "nobody is working on it" } -// workerWords is who@node as said: "fix-400 on g14". +// workerWords is who@node as said: "fix-400 on node-b". func workerWords(worker string) string { who, node, ok := strings.Cut(worker, "@") if !ok { diff --git a/modules/messenger/cmd/messenger/words.go b/modules/messenger/cmd/messenger/words.go index 1a450d85..82707c52 100644 --- a/modules/messenger/cmd/messenger/words.go +++ b/modules/messenger/cmd/messenger/words.go @@ -9,7 +9,7 @@ package main // controller where the condition is made, so every channel says the same thing. A condition from a // controller older than them, or a module's notice without a headline, is said from its summary, // with what the operator cannot read taken out (plainSummary). -// - A clearance is one short line: "Resolved: openrazer works again on g14, after 26 min". +// - A clearance is one short line: "Resolved: openrazer works again on node-b, after 26 min". // - A reminder, an escalation and a reopening say so in the title, before the same headline. // - Times are the operator's (the setting time-zone; this machine's zone when not given), and said as a // clock time today, a weekday this week, a date before that. @@ -168,7 +168,7 @@ func fallbackHeadline(r *Record) string { return capital(what) + " on " + about } -// machineOf is the machine a record's subject words name: "machine ace" or "x on ace". +// machineOf is the machine a record's subject words name: "machine node-a" or "x on node-a". func machineOf(subject string) string { if _, on, ok := strings.Cut(subject, " on "); ok { return on diff --git a/modules/nats/cmd/nats-tools/main.go b/modules/nats/cmd/nats-tools/main.go index 4de769dd..ef230c5c 100644 --- a/modules/nats/cmd/nats-tools/main.go +++ b/modules/nats/cmd/nats-tools/main.go @@ -55,7 +55,7 @@ func tools(monitor Monitor, users func(context.Context) ([]byte, error), written }}, {Name: "nats_connections", Description: "Every client connected to the bus, named by the mesh user it is (a machine's host `node.`, a machine's runtime `.node-tools`, the controller, a person): address, uptime, round trip, traffic and pending bytes. Narrowed by a user or connection name, sorted by out_bytes (default), in_bytes, pending, msgs or subscriptions.", - Input: map[string]any{"user": str("part of a user or connection name, e.g. g14"), "sort": str("out_bytes, in_bytes, pending, msgs or subscriptions"), "limit": num("at most this many (default 50)")}, + Input: map[string]any{"user": str("part of a user or connection name, e.g. node-b"), "sort": str("out_bytes, in_bytes, pending, msgs or subscriptions"), "limit": num("at most this many (default 50)")}, Run: func(a map[string]any) (any, error) { connz, err := get("/connz?auth=1&limit=1024") if err != nil { @@ -115,7 +115,7 @@ func tools(monitor Monitor, users func(context.Context) ([]byte, error), written }}, {Name: "nats_users", Description: "The bus's users as the controller composed them — never a password hash. Without a user, each with how many grants it holds; with part of a user's name, its publish and subscribe grants in full.", - Input: map[string]any{"user": str("part of a user name, e.g. g14.node-tools or controller")}, + Input: map[string]any{"user": str("part of a user name, e.g. node-b.node-tools or controller")}, Run: func(a map[string]any) (any, error) { c, err := conf() if err != nil { @@ -125,7 +125,7 @@ func tools(monitor Monitor, users func(context.Context) ([]byte, error), written }}, {Name: "nats_user_can", Description: "Whether a bus user may publish or subscribe to a subject, and the grant that allows it — the question behind a request that timed out because the bus refused it. Also whether the running server holds it: `in_force` is true only when the user list on the machine allows it and the server reloaded its configuration after that list was written (novox/hq issue 353: a list sent is not a list the bus enforces until it reloads).", - Input: map[string]any{"user": str("the user exactly, e.g. g14.node-tools"), "action": str("publish or subscribe"), "subject": str("a concrete subject, e.g. $KV.claude-code_servers.all.x")}, + Input: map[string]any{"user": str("the user exactly, e.g. node-b.node-tools"), "action": str("publish or subscribe"), "subject": str("a concrete subject, e.g. $KV.claude-code_servers.all.x")}, Run: func(a map[string]any) (any, error) { c, err := conf() if err != nil { diff --git a/modules/only-office/cmd/only-office/convert_test.go b/modules/only-office/cmd/only-office/convert_test.go index 27f4a787..8e5dd4ba 100644 --- a/modules/only-office/cmd/only-office/convert_test.go +++ b/modules/only-office/cmd/only-office/convert_test.go @@ -45,7 +45,7 @@ func rig(wait time.Duration) (*Service, *fakeBus) { return &Service{Asks: b, Now: time.Now, Wait: wait, Poll: time.Millisecond}, b } -const get, put = "http://novox.internal:20001/b/doc.docx?X-Amz-Signature=a", "http://novox.internal:20001/b/doc.pdf?X-Amz-Signature=b" +const get, put = "http://node-c.internal:20001/b/doc.docx?X-Amz-Signature=a", "http://node-c.internal:20001/b/doc.pdf?X-Amz-Signature=b" // 1595/AC1: one ask with those fields is published on convert-asks, and the verb answers what the converter wrote under // the ask's id. diff --git a/modules/only-office/cmd/only-office/converter_integration_test.go b/modules/only-office/cmd/only-office/converter_integration_test.go index fd9378f1..6e998608 100644 --- a/modules/only-office/cmd/only-office/converter_integration_test.go +++ b/modules/only-office/cmd/only-office/converter_integration_test.go @@ -68,7 +68,7 @@ func TestTheToolSideAndTheConverterConvertOverARealBus(t *testing.T) { Retention: jetstream.WorkQueuePolicy, Discard: jetstream.DiscardNew, MaxAge: 24 * time.Hour, MaxBytes: 8 << 20}); err != nil { t.Fatal(err) } - account := process.Account{URL: s.ClientURL(), Node: "novox", Module: "only-office", User: "novox.only-office.converter", Password: "pw"} + account := process.Account{URL: s.ClientURL(), Node: "node-c", Module: "only-office", User: "node-c.only-office.converter", Password: "pw"} if _, err := js.CreateConsumer(ctx, AsksStream, jetstream.ConsumerConfig{Durable: account.QueueConsumerName(), AckPolicy: jetstream.AckExplicitPolicy, AckWait: 30 * time.Second, MaxDeliver: 3}); err != nil { t.Fatal(err) @@ -85,7 +85,7 @@ func TestTheToolSideAndTheConverterConvertOverARealBus(t *testing.T) { go runConverter(runCtx, account, w) svc := &Service{Asks: natsBus{js: js, kv: kv}, Now: time.Now, Wait: 20 * time.Second, Poll: 50 * time.Millisecond} - got, err := svc.Convert(ctx, "http://novox.internal:20001/b/in.docx?X-Amz-Signature=a", "docx", tgt.srv.URL+"/b/in.pdf?sig=1", "") + got, err := svc.Convert(ctx, "http://node-c.internal:20001/b/in.docx?X-Amz-Signature=a", "docx", tgt.srv.URL+"/b/in.pdf?sig=1", "") if err != nil { t.Fatal(err) } diff --git a/modules/only-office/cmd/only-office/converter_test.go b/modules/only-office/cmd/only-office/converter_test.go index fe40c820..8fe357f8 100644 --- a/modules/only-office/cmd/only-office/converter_test.go +++ b/modules/only-office/cmd/only-office/converter_test.go @@ -101,7 +101,7 @@ func worker(d *fakeDocs) (*converterWorker, *answers) { func askBody(t *testing.T, target string, sha string) []byte { raw, err := json.Marshal(Ask{ID: "ask1", Op: OpConvert, At: time.Now(), Deadline: time.Now().Add(time.Minute), - Source: "http://novox.internal:20001/b/in.docx?X-Amz-Signature=a", Type: "docx", Target: target, SHA256: sha}) + Source: "http://node-c.internal:20001/b/in.docx?X-Amz-Signature=a", Type: "docx", Target: target, SHA256: sha}) if err != nil { t.Fatal(err) } @@ -142,7 +142,7 @@ func TestTheConverterPostsTheRequestAndPutsThePDF(t *testing.T) { t.Fatalf("%d conversion requests", len(d.requests)) } r := d.requests[0] - if r["url"] != "http://novox.internal:20001/b/in.docx?X-Amz-Signature=a" || r["filetype"] != "docx" || r["outputtype"] != "pdf" || + if r["url"] != "http://node-c.internal:20001/b/in.docx?X-Amz-Signature=a" || r["filetype"] != "docx" || r["outputtype"] != "pdf" || r["async"] != false || r["key"] != strings.Repeat("ab", 32) || d.accept != "application/json" { t.Fatalf("request %v accept %q", r, d.accept) } diff --git a/modules/slack/README.md b/modules/slack/README.md index 86698958..6e8141aa 100644 --- a/modules/slack/README.md +++ b/modules/slack/README.md @@ -120,7 +120,7 @@ and a fake link reader in the tests. ## What changes when it is assigned -| | g14 | shanks | +| | node-b | node-d | |---|---|---| | package | none: `slack-desktop` 4.51.191-1, AUR, installed explicitly | the same | | start | i3's configuration gains the module's start. **Until the predecessor's `~/.config/autostart/slack.desktop` is deleted, the next login starts Slack twice.** The second start hands over to the first and exits, because Slack is single-instance. Running now: started by dex from that entry at the login of 2026-10-04 16:26 | the same entry, the same until deleted. Running now: since 2026-10-04 17:05, **with its stdout on a pipe nobody reads** (that session predates the `i3` module's login entry), so `slack_check` names it until `slack_restart` or the next login | @@ -135,7 +135,7 @@ and a fake link reader in the tests. off. Leave the setting off: ticking it makes the entry again, and `slack_check` names it. 2. **Delete `~/.config/i3/config.d/50-slack.conf`.** It is the operator's own file, and its rules are now the module's. -3. **shanks only:** run `slack_restart`, or log out and in, so that Slack's output is read. +3. **node-d only:** run `slack_restart`, or log out and in, so that Slack's output is read. `slack_check` then answers `ok`. Deleting both files before the module is assigned would leave Slack without a start and without its rules until it is. diff --git a/modules/sudo/cmd/sudo-tools/manifest_test.go b/modules/sudo/cmd/sudo-tools/manifest_test.go index 3d3bb454..3f2b252a 100644 --- a/modules/sudo/cmd/sudo-tools/manifest_test.go +++ b/modules/sudo/cmd/sudo-tools/manifest_test.go @@ -51,7 +51,7 @@ func TestTheDropInGrantsExactlyTheOperatorAccountAndParses(t *testing.T) { if err != nil { t.Skip("visudo is not installed here; the rendered drop-in is not checked") } - for _, account := range []string{"operator", "ace", "jochen-s"} { + for _, account := range []string{"operator", "node-a", "jochen-s"} { file := filepath.Join(t.TempDir(), "10-mesh-operator") rendered := strings.ReplaceAll(content, "${machine:account}", account) if err := os.WriteFile(file, []byte(rendered), 0o440); err != nil { diff --git a/modules/sudo/cmd/sudo-tools/sudo_test.go b/modules/sudo/cmd/sudo-tools/sudo_test.go index 73fb1784..3035b278 100644 --- a/modules/sudo/cmd/sudo-tools/sudo_test.go +++ b/modules/sudo/cmd/sudo-tools/sudo_test.go @@ -5,7 +5,7 @@ import ( "testing" ) -const listNovox = `Matching Defaults entries for operator on anchor: +const listAnchor = `Matching Defaults entries for operator on anchor: env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/bin User operator may run the following commands on anchor: @@ -14,7 +14,7 @@ User operator may run the following commands on anchor: ` func TestSudoListIsParsedIntoDefaultsAndRules(t *testing.T) { - r := ParseList(listNovox, "x") + r := ParseList(listAnchor, "x") if r.Account != "operator" || r.Host != "anchor" { t.Fatalf("who: %+v", r) }