From 9bb85a37f2ee45aa7b978cb677aa149c94a56bc9 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 11:27:02 +0100 Subject: [PATCH 1/3] Retire the spelling catalogue: the words check lists it and skips module paths in imports --- .gitignore | 1 + checks/words/main.go | 8 ++++++++ .../cmd/artifact-store-tools/definitions.go | 18 +++++++++--------- .../cmd/artifact-store-tools/tools.go | 2 +- .../cmd/claude-code/instructions.go | 4 ++-- modules/gitea/tools/index.ts | 2 +- modules/mesh-catalog/store.ts | 6 +++--- modules/mesh-catalog/tools/index.ts | 4 ++-- modules/mesh-config/cmd/mesh-config/config.go | 2 +- modules/mesh-config/cmd/mesh-config/sources.go | 4 ++-- modules/mesh-issues/cmd/mesh-issues/tracker.go | 2 +- retired-words | 1 + 12 files changed, 32 insertions(+), 22 deletions(-) diff --git a/.gitignore b/.gitignore index 9552fa38..952eee66 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ checks/placement/placement # Built by their modules' build step (issue 1577), never committed. modules/forticlient/cmd/forticlient-tools/forticlient-tools modules/networkmanager/uplink-tools +checks/words/words diff --git a/checks/words/main.go b/checks/words/main.go index 9db6b1e6..8f9b8893 100644 --- a/checks/words/main.go +++ b/checks/words/main.go @@ -206,6 +206,9 @@ func excerpt(s string, from, to int) string { return strings.Join(strings.Fields(s[a:b]), " ") } +// modulePath matches a string that is only a module or import path ("git.example.org/org/repo/go/pkg"). +var modulePath = regexp.MustCompile(`^[a-z0-9.-]+\.[a-z]+/[A-Za-z0-9_./-]+$`) + // goStrings returns a Go file's string literals, adjacent ones joined across `+` as the program joins them. func goStrings(path, rel string) ([]text, error) { src, err := os.ReadFile(path) @@ -231,6 +234,11 @@ func goStrings(path, rel string) ([]text, error) { if err != nil { v = lit } + if modulePath.MatchString(v) { + // An import path or another module path names code, not words an agent reads. + cur, joining = nil, false + continue + } if cur != nil && joining { cur.s += v } else { diff --git a/modules/artifact-store-tools/cmd/artifact-store-tools/definitions.go b/modules/artifact-store-tools/cmd/artifact-store-tools/definitions.go index 67cb50d7..ef4ef966 100644 --- a/modules/artifact-store-tools/cmd/artifact-store-tools/definitions.go +++ b/modules/artifact-store-tools/cmd/artifact-store-tools/definitions.go @@ -23,7 +23,7 @@ import ( // manifest's. const NamesFileEnv = "MESH_ARTIFACT_STORE_NAMES_FILE" -// CatalogueModules and CatalogueModule are the catalogue's tools these definitions are read through. +// CatalogueModules and CatalogueModule are the catalog's tools these definitions are read through. const ( CatalogueModules = "mesh-catalog.catalog_modules" CatalogueModule = "mesh-catalog.catalog_module" @@ -143,21 +143,21 @@ func PinsIn(manifest json.RawMessage, names StoreNames) []pinned { return out } -// Definitions is what the catalogue's definitions pin in the store, and what of them could not be read. +// Definitions is what the catalog's definitions pin in the store, and what of them could not be read. type Definitions struct { // Pins are by Key(repository, digest). Pins map[string][]Pin // Read is how many definitions were read; Unread says, per module, why its definition was not. Read int Unread map[string]string - // NotAsked says why the catalogue could not be asked at all, when it could not. + // NotAsked says why the catalog could not be asked at all, when it could not. NotAsked string // NamesNotRead says why the store's older names could not be read, when they could not. NamesNotRead string Names StoreNames } -// Catalogue reads the definitions through the catalogue's tools, and keeps each definition it read by its +// Catalogue reads the definitions through the catalog's tools, and keeps each definition it read by its // module and commit, which never change together. type Catalogue struct { Ask Asking @@ -171,21 +171,21 @@ type catalogueEntry struct { Commit string `json:"commit"` } -// Read asks the catalogue for every module and reads each definition, bounded by DefinitionsTimeout. +// Read asks the catalog for every module and reads each definition, bounded by DefinitionsTimeout. func (c *Catalogue) Read(ctx context.Context, names StoreNames) *Definitions { defs := &Definitions{Pins: map[string][]Pin{}, Unread: map[string]string{}, Names: names} if c == nil || c.Ask == nil { - defs.NotAsked = "this bundle has no way to ask the catalogue" + defs.NotAsked = "this bundle has no way to ask the catalog" return defs } raw, err := c.Ask(CatalogueModules, map[string]any{}) if err != nil { - defs.NotAsked = "the catalogue's modules could not be asked: " + err.Error() + defs.NotAsked = "the catalog's modules could not be asked: " + err.Error() return defs } answer, output, ok := answerOf(raw) if !ok { - defs.NotAsked = "the catalogue refused its modules: " + lastLine(output) + defs.NotAsked = "the catalog refused its modules: " + lastLine(output) return defs } var list struct { @@ -193,7 +193,7 @@ func (c *Catalogue) Read(ctx context.Context, names StoreNames) *Definitions { Error string `json:"error"` } if err := json.Unmarshal(answer, &list); err != nil || list.Error != "" || list.Modules == nil { - defs.NotAsked = "the catalogue's modules answer is not readable: " + firstOf(list.Error, errText(err), lastLine(output)) + defs.NotAsked = "the catalog's modules answer is not readable: " + firstOf(list.Error, errText(err), lastLine(output)) return defs } c.mu.Lock() diff --git a/modules/artifact-store-tools/cmd/artifact-store-tools/tools.go b/modules/artifact-store-tools/cmd/artifact-store-tools/tools.go index f7e1012c..65d0350e 100644 --- a/modules/artifact-store-tools/cmd/artifact-store-tools/tools.go +++ b/modules/artifact-store-tools/cmd/artifact-store-tools/tools.go @@ -54,7 +54,7 @@ func Tools(s *Store, c Controller, cat *Catalogue) []stdio.Tool { Name: "artifact_store_references", Description: "What the controller's records say of each manifest the artifact store holds: kept (a definition " + "names it, or one of the five most recent builds of a module the mesh holds), named by a definition (no " + - "record names it, and a definition in the catalogue pins it by digest, under the store's current name or " + + "record names it, and a definition in the catalog pins it by digest, under the store's current name or " + "an older one: the module is said), the holder of a kept archive, " + "eligible (the mesh made it and keeps it for no reason), the holder of an eligible archive, let go yet present, " + "a named document the controller keeps under a tag, or unrecorded — no record names it. A platform manifest " + diff --git a/modules/claude-code/cmd/claude-code/instructions.go b/modules/claude-code/cmd/claude-code/instructions.go index 12a6d926..117ab529 100644 --- a/modules/claude-code/cmd/claude-code/instructions.go +++ b/modules/claude-code/cmd/claude-code/instructions.go @@ -4,9 +4,9 @@ package main // the file under the agent's managed directory did not change by a byte when the module moved to Go; // a test holds it to that renderer's own output (testdata/rendered-by-typescript.json). func instructionsText(node, role string) string { - return "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalogue, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `" + + return "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalog, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `" + node + "`\n- **Role:** " + role + - "\n- The other nodes, their roles and what runs where: ask the controller (`mesh-controller.nodes`,\n `mesh-controller.node`). Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `.` (the mesh's own verbs\n are `mesh-controller.`: `status`, `plan`, `node`, `assign`, `push`, `settings`);\n a module on a machine is `/.`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the controller's verbs or the catalogue.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalogue that owns it, never on disk. If\n unsure, `mesh-controller.plan` for the node says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n" + "\n- The other nodes, their roles and what runs where: ask the controller (`mesh-controller.nodes`,\n `mesh-controller.node`). Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `.` (the mesh's own verbs\n are `mesh-controller.`: `status`, `plan`, `node`, `assign`, `push`, `settings`);\n a module on a machine is `/.`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the controller's verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `mesh-controller.plan` for the node says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n" } diff --git a/modules/gitea/tools/index.ts b/modules/gitea/tools/index.ts index 7b32e275..9a343d63 100644 --- a/modules/gitea/tools/index.ts +++ b/modules/gitea/tools/index.ts @@ -637,7 +637,7 @@ function allGiteaTools(gitea: GiteaClient, admin?: () => GiteaAdmin): ToolDefini }, { name: "gitea_commit_trees", - description: "A commit's trees from the forge's one recursive listing: the root tree, and the tree of every directory that holds a module.json (the root as an empty name), each by its git hash. What the forge bridge puts in a merge's message (novox/hq ADR 0437 §5). With paths, {directory: [path relative to it]}, the answer also carries paths, {directory: {path: git object id}} of each named file or directory at the commit (novox/hq ADR 0499 §3); a path the commit lacks fails naming the directory and the path. The commit is a full 40-hex sha. With listed, [directory], the answer also carries listed, {directory: {name: git blob id}} of the files directly under each (the catalogue's placement files, novox/hq ADR 0549 §2). A listing the forge cut is refused, never answered in part. Reads only.", + description: "A commit's trees from the forge's one recursive listing: the root tree, and the tree of every directory that holds a module.json (the root as an empty name), each by its git hash. What the forge bridge puts in a merge's message (novox/hq ADR 0437 §5). With paths, {directory: [path relative to it]}, the answer also carries paths, {directory: {path: git object id}} of each named file or directory at the commit (novox/hq ADR 0499 §3); a path the commit lacks fails naming the directory and the path. The commit is a full 40-hex sha. With listed, [directory], the answer also carries listed, {directory: {name: git blob id}} of the files directly under each (the catalog's placement files, novox/hq ADR 0549 §2). A listing the forge cut is refused, never answered in part. Reads only.", input: { owner: { type: "string", description: "the repository owner" }, repo: { type: "string", description: "the repository name" }, diff --git a/modules/mesh-catalog/store.ts b/modules/mesh-catalog/store.ts index 02db37e9..5a5e3023 100644 --- a/modules/mesh-catalog/store.ts +++ b/modules/mesh-catalog/store.ts @@ -66,7 +66,7 @@ CREATE TABLE IF NOT EXISTS module_version ( PRIMARY KEY (module, commit_sha) ); --- What the catalogue considers this module to be now. Separate from module_version because a +-- What the catalog considers this module to be now. Separate from module_version because a -- module has many versions and exactly one current one, and "current" is a decision rather than -- a consequence of being newest. CREATE TABLE IF NOT EXISTS module_current ( @@ -144,7 +144,7 @@ export class Graph { const url = env["DATABASE_URL"] ?? readMaybe(env["DATABASE_URL_FILE"]); if (!url) { throw new Error( - "no DATABASE_URL: the catalogue holds the module graph and cannot hold it in memory, " + + "no DATABASE_URL: the catalog holds the module graph and cannot hold it in memory, " + "because a graph that disappears on restart is not a record of anything", ); } @@ -246,7 +246,7 @@ export class Graph { } } - /** Every module the catalogue holds, with the commit it considers current. */ + /** Every module the catalog holds, with the commit it considers current. */ async modules(): Promise<{ module: string; commit: string; repository: string; path: string }[]> { const { rows } = await this.pool.query( `SELECT c.module, c.commit_sha, v.repository, v.path diff --git a/modules/mesh-catalog/tools/index.ts b/modules/mesh-catalog/tools/index.ts index 0c4efccd..57873380 100644 --- a/modules/mesh-catalog/tools/index.ts +++ b/modules/mesh-catalog/tools/index.ts @@ -12,7 +12,7 @@ export function getCatalogueTools(graph: Graph): ToolDefinition[] { { name: "catalog_modules", description: - "Every module this mesh holds, with the commit the catalogue considers current and where it came from.", + "Every module this mesh holds, with the commit the catalog considers current and where it came from.", input: {}, run: async () => ({ modules: await graph.modules() }), }, @@ -28,7 +28,7 @@ export function getCatalogueTools(graph: Graph): ToolDefinition[] { const module = String(args.module ?? ""); if (!module) return { error: "name a module" }; const found = await graph.show(module, args.commit ? String(args.commit) : undefined); - return found ? { module: found } : { error: `the catalogue holds no ${module}` }; + return found ? { module: found } : { error: `the catalog holds no ${module}` }; }, }, { diff --git a/modules/mesh-config/cmd/mesh-config/config.go b/modules/mesh-config/cmd/mesh-config/config.go index 53bffac7..a2103671 100644 --- a/modules/mesh-config/cmd/mesh-config/config.go +++ b/modules/mesh-config/cmd/mesh-config/config.go @@ -184,7 +184,7 @@ func (call Call) level() (Level, error) { case call.Module != "": return Level{}, fmt.Errorf("a module's own value is set for one node, and no node is named: there is no "+ "module value for the whole mesh (novox/hq ADR 0348 §3); name the node, or set mesh config for the "+ - "predefined key %s's value falls back to, or change its default in the catalogue", call.Module) + "predefined key %s's value falls back to, or change its default in the catalog", call.Module) case call.Node != "": return Level{Kind: LevelNode, Node: call.Node}, nil } diff --git a/modules/mesh-config/cmd/mesh-config/sources.go b/modules/mesh-config/cmd/mesh-config/sources.go index 34f8dddf..36adb3c8 100644 --- a/modules/mesh-config/cmd/mesh-config/sources.go +++ b/modules/mesh-config/cmd/mesh-config/sources.go @@ -27,7 +27,7 @@ func (c *Config) sources() (Sources, error) { }); err != nil { return s, fmt.Errorf("the assignments cannot be read: %w", err) } - // And every module the catalogue's placement names on a node (ADR 0549 §3): until ADR 0547's third milestone both + // And every module the catalog's placement names on a node (ADR 0549 §3): until ADR 0547's third milestone both // sources stand (ADR 0549 §5), so a pair either names is resolved, and none loses its values while the two differ. // Placement that cannot be read yet (the indexer's state not on the bus) leaves the assignments alone, said. standing := map[string]bool{} @@ -57,7 +57,7 @@ func (c *Config) sources() (Sources, error) { } return nil }); err != nil { - problems = append(problems, fmt.Sprintf("the catalogue's placement cannot be read, so the assignments alone are resolved: %v", err)) + problems = append(problems, fmt.Sprintf("the catalog's placement cannot be read, so the assignments alone are resolved: %v", err)) } if err := c.each(DeclaredConfigRead, func(key string, raw []byte) error { m, err := ParseModuleDeclarations(raw) diff --git a/modules/mesh-issues/cmd/mesh-issues/tracker.go b/modules/mesh-issues/cmd/mesh-issues/tracker.go index 7b80cba0..64ebd3c8 100644 --- a/modules/mesh-issues/cmd/mesh-issues/tracker.go +++ b/modules/mesh-issues/cmd/mesh-issues/tracker.go @@ -559,7 +559,7 @@ func cycleRules(is *Issue, from, to string) error { } if IsCore(is.LocatedIn) && is.Replay == "" && is.ReplayNone == "" { return fmt.Errorf("issue %d is a core issue (it is located in the controller, the node-engine, the tool "+ - "runner, the SDK, the delivery parts, or the catalogue's bus, forge or build modules): it resolves with replay, the replay's id "+ + "runner, the SDK, the delivery parts, or the catalog's bus, forge or build modules): it resolves with replay, the replay's id "+ "in mesh-lab's replays register, or replay-none, why no replay is possible (ADR 0237)", is.Number) } case StatusWontfix: diff --git a/retired-words b/retired-words index 870b2e2b..c7a00a95 100644 --- a/retired-words +++ b/retired-words @@ -2,6 +2,7 @@ # *Not:* words with no scope or with (tools). A copy, so a catalogue merge needs nothing else; novox/hq's # words.py compares it with the glossary. Regenerate with: python3 00-META/checks/words.py --list tools build machine +catalogue change plan control plane flavor From 002a0e1a1259093e570a4ccb6045c8e3057d8cef Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 11:19:06 +0100 Subject: [PATCH 2/3] Keep the claude-code fixture equal to the renderer's catalog wording --- .../cmd/claude-code/testdata/rendered-by-typescript.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/claude-code/cmd/claude-code/testdata/rendered-by-typescript.json b/modules/claude-code/cmd/claude-code/testdata/rendered-by-typescript.json index 5bddf03b..40e6a530 100644 --- a/modules/claude-code/cmd/claude-code/testdata/rendered-by-typescript.json +++ b/modules/claude-code/cmd/claude-code/testdata/rendered-by-typescript.json @@ -32,11 +32,11 @@ "withKey": { "managed-mcp.json": "{\n \"mcpServers\": {\n \"anton\": {\n \"type\": \"stdio\",\n \"command\": \"node\",\n \"args\": [\n \"/a/b.js\"\n ],\n \"env\": {}\n },\n \"docs\": {\n \"type\": \"stdio\",\n \"command\": \"docs-mcp\",\n \"args\": [\n \"--x\"\n ]\n },\n \"mesh\": {\n \"type\": \"http\",\n \"url\": \"http://127.0.0.1:4270/mcp\"\n },\n \"search\": {\n \"type\": \"http\",\n \"url\": \"https://s.example/mcp\"\n }\n }\n}\n", "managed-settings.json": "{\n \"attribution\": {\n \"commit\": \"\",\n \"pr\": \"\"\n },\n \"allowAllClaudeAiMcps\": true,\n \"apiKeyHelper\": \"/state/api-key-helper\"\n}\n", - "CLAUDE.md": "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalogue, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `workstation`\n- **Role:** the laptop\n- The other nodes, their roles and what runs where: ask the controller (`mesh-controller.nodes`,\n `mesh-controller.node`). Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `.` (the mesh's own verbs\n are `mesh-controller.`: `status`, `plan`, `node`, `assign`, `push`, `settings`);\n a module on a machine is `/.`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the controller's verbs or the catalogue.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalogue that owns it, never on disk. If\n unsure, `mesh-controller.plan` for the node says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n" + "CLAUDE.md": "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalog, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `workstation`\n- **Role:** the laptop\n- The other nodes, their roles and what runs where: ask the controller (`mesh-controller.nodes`,\n `mesh-controller.node`). Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `.` (the mesh's own verbs\n are `mesh-controller.`: `status`, `plan`, `node`, `assign`, `push`, `settings`);\n a module on a machine is `/.`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the controller's verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `mesh-controller.plan` for the node says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n" }, "plain": { "managed-mcp.json": "{\n \"mcpServers\": {\n \"mesh\": {\n \"type\": \"http\",\n \"url\": \"http://127.0.0.1:4270/mcp\"\n }\n }\n}\n", "managed-settings.json": "{\n \"attribution\": {\n \"commit\": \"\",\n \"pr\": \"\"\n },\n \"allowAllClaudeAiMcps\": true\n}\n", - "CLAUDE.md": "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalogue, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `workstation`\n- **Role:** not stated — set it in this module's settings for the node\n- The other nodes, their roles and what runs where: ask the controller (`mesh-controller.nodes`,\n `mesh-controller.node`). Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `.` (the mesh's own verbs\n are `mesh-controller.`: `status`, `plan`, `node`, `assign`, `push`, `settings`);\n a module on a machine is `/.`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the controller's verbs or the catalogue.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalogue that owns it, never on disk. If\n unsure, `mesh-controller.plan` for the node says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n" + "CLAUDE.md": "# This machine is a node of a Novox mesh\n\nWritten by the mesh's `claude-code` module. Edit the module's settings or the catalog, never this file:\nit is rewritten whenever the module renders.\n\n## Who this node is\n\n- **Node:** `workstation`\n- **Role:** not stated — set it in this module's settings for the node\n- The other nodes, their roles and what runs where: ask the controller (`mesh-controller.nodes`,\n `mesh-controller.node`). Nothing here lists them, because a copy drifts.\n\n## How a session on this mesh works\n\nThe console is the only way to the mesh: the MCP server named `mesh`. It offers five tools, and\neverything else is an address you find and call through them:\n\n- `mesh_search` — words in, matching addresses out. `mesh_describe` — one address's arguments.\n- `mesh_call` — call an address. A seat the mesh holds once is `.` (the mesh's own verbs\n are `mesh-controller.`: `status`, `plan`, `node`, `assign`, `push`, `settings`);\n a module on a machine is `/.`.\n- `mesh_overview` and `mesh_machine` — the mesh's seats and machines, and what one machine runs.\n\n- **Symptom first.** For an error, a failing service or anything unexpected, search the record with the\n literal text before forming a hypothesis: the records module's `records_search`, then\n `records_read`.\n- **Ask the mesh before changing it**, and change it through the controller's verbs or the catalog.\n- **A licence** through the `anthropic-licence-manager` seat's verbs. Never edit the agent's credentials\n file by hand, never print or ask for a token.\n\n## Hard rules\n\n- A file the mesh manages is changed through the verb or the catalog that owns it, never on disk. If\n unsure, `mesh-controller.plan` for the node says what the mesh writes there.\n- Never write to a store's database by hand; schema changes are numbered migrations.\n- Never push to a main branch: a branch, a pull request, and a human approval for every merge.\n- The mesh creates no symlinks, and nobody else does either.\n- A package is declared in a module, never installed by hand.\n\n## Conventions\n\n- Commit messages are concise, in the imperative, about why.\n- Test before pushing: nodes update unattended.\n- The playbooks in the record say how research, decisions, designs, issues and hand-offs are done.\n" } } \ No newline at end of file From b8ba9dd9669cbe747326dcf671095c605897a234 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 28 Oct 2026 11:28:29 +0100 Subject: [PATCH 3/3] Widen the no-binaries check to every tracked file: a program under checks/ is refused too --- checks/nobinaries/nobinaries.go | 18 ++++++++----- checks/nobinaries/nobinaries_test.go | 39 ++++++++++++++++++++++++++-- checks/nobinaries/reads-outside | 6 +---- 3 files changed, 50 insertions(+), 13 deletions(-) diff --git a/checks/nobinaries/nobinaries.go b/checks/nobinaries/nobinaries.go index 55395bfa..6df00793 100644 --- a/checks/nobinaries/nobinaries.go +++ b/checks/nobinaries/nobinaries.go @@ -1,4 +1,4 @@ -// Package nobinaries refuses a compiled program under modules/ (issue 1577): it is build output, made by the module's build step, +// Package nobinaries refuses a compiled program anywhere in the repository (issue 1577; first a checks/ binary slipped past a modules/-only rule): it is build output, made by the module's build step, // and `go build` in a module's folder writes one beside go.mod where `git add -A` takes it. package nobinaries @@ -27,10 +27,10 @@ func Kind(head []byte) string { return "" } -// Files are the files of the catalogue's root that the change holds under modules/: the tracked ones when the root is a git -// checkout (an ignored binary a local build left is not the change's), else every file there. +// Files are the files of the catalogue's root that the change holds, anywhere: the tracked ones when the root is a git +// checkout (an ignored binary a local build left is not the change's), else every file there but .git's. func Files(root string) ([]string, error) { - cmd := exec.Command("git", "-C", root, "ls-files", "-z", "--", "modules") + cmd := exec.Command("git", "-C", root, "ls-files", "-z") if out, err := cmd.Output(); err == nil && len(out) > 0 { var files []string for _, f := range strings.Split(strings.TrimRight(string(out), "\x00"), "\x00") { @@ -41,10 +41,16 @@ func Files(root string) ([]string, error) { return files, nil } var files []string - err := filepath.WalkDir(filepath.Join(root, "modules"), func(p string, d os.DirEntry, err error) error { - if err != nil || d.IsDir() || !d.Type().IsRegular() { + err := filepath.WalkDir(root, func(p string, d os.DirEntry, err error) error { + if err != nil { return err } + if d.IsDir() && d.Name() == ".git" { + return filepath.SkipDir + } + if d.IsDir() || !d.Type().IsRegular() { + return nil + } rel, _ := filepath.Rel(root, p) files = append(files, rel) return nil diff --git a/checks/nobinaries/nobinaries_test.go b/checks/nobinaries/nobinaries_test.go index d26aeecd..4d874e99 100644 --- a/checks/nobinaries/nobinaries_test.go +++ b/checks/nobinaries/nobinaries_test.go @@ -2,6 +2,7 @@ package nobinaries import ( "os" + "os/exec" "path/filepath" "testing" ) @@ -16,6 +17,7 @@ func TestFindNamesEachCompiledProgramAndNothingElse(t *testing.T) { "modules/e/short": []byte("\x7f"), "modules/f/empty": {}, "modules/g/Foo.class": {0xca, 0xfe, 0xba, 0xbe, 0, 0, 0, 52}, // Java, not Mach-O + "checks/words/words": []byte("\x7fELF\x02\x01\x01\x00rest"), // outside modules/ too } var names []string for f, b := range files { @@ -32,8 +34,8 @@ func TestFindNamesEachCompiledProgramAndNothingElse(t *testing.T) { if err != nil { t.Fatal(err) } - if len(got) != 3 { - t.Fatalf("found %v; wanted modules/a/a, modules/b/cmd/b/b.exe and modules/c/c only", got) + if len(got) != 4 { + t.Fatalf("found %v; wanted modules/a/a, modules/b/cmd/b/b.exe and modules/c/c and checks/words/words only", got) } } @@ -52,3 +54,36 @@ func TestTheCatalogueHoldsNoCompiledProgram(t *testing.T) { t.Errorf("%s: a compiled program is build output: remove it and add it to .gitignore (issue 1577)", f) } } + +// A compiled program outside modules/ is refused too (a binary under checks/ slipped past the modules/-only rule): Files holds +// every tracked file of a git checkout and every file of a plain folder, and Find names the binary in both. +func TestAProgramUnderChecksIsRefused(t *testing.T) { + for _, git := range []bool{true, false} { + root := t.TempDir() + p := filepath.Join(root, "checks", "words", "words") + if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(p, []byte("\x7fELF\x02\x01\x01\x00rest"), 0o644); err != nil { + t.Fatal(err) + } + if git { + for _, args := range [][]string{{"init", "-q"}, {"add", "-A"}} { + if out, err := exec.Command("git", append([]string{"-C", root}, args...)...).CombinedOutput(); err != nil { + t.Fatalf("git %v: %v: %s", args, err, out) + } + } + } + files, err := Files(root) + if err != nil { + t.Fatal(err) + } + found, err := Find(root, files) + if err != nil { + t.Fatal(err) + } + if len(found) != 1 { + t.Errorf("git=%v: found %v; wanted checks/words/words (ELF)", git, found) + } + } +} diff --git a/checks/nobinaries/reads-outside b/checks/nobinaries/reads-outside index 8c970936..2608ca02 100644 --- a/checks/nobinaries/reads-outside +++ b/checks/nobinaries/reads-outside @@ -1,5 +1 @@ -# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole; -# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept -# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing. -# every file under modules/ (git ls-files and a walk). -modules +# every tracked file of the repository (git ls-files, no path limit): the whole tree, so this package's verdict is never kept.