From 1e3439429f54fb258ec2e46950470db9e9642d63 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 21:08:12 +0200 Subject: [PATCH] =?UTF-8?q?K2:=20read=20a=20manifest=20for=20its=20build?= =?UTF-8?q?=20inputs=20in=20mesh-build-go=20and=20mesh-build-image=20(issu?= =?UTF-8?q?e=201751,=20ADR=200514=20=C2=A72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The build step parsed the manifest as a judge of config values, so claude-code, dunst and xterm, whose values fall back to a predefined key, got a fingerprint but their tools bundle was refused at the build. The builder holds no published list of predefined keys; ParseManifestForInputs is its read. mesh-build-node reads module.json with its own narrow struct and needs no change. --- .../mesh-build-go/cmd/mesh-build-go/build.go | 2 +- .../cmd/mesh-build-go/build_test.go | 21 ++ .../mesh-build-go/testdata/captured/README | 4 + .../testdata/captured/claude-code-module.json | 249 ++++++++++++++++++ .../cmd/mesh-build-image/build.go | 2 +- .../cmd/mesh-build-image/build_test.go | 53 ++++ .../mesh-build-image/testdata/captured/README | 5 + .../testdata/captured/claude-code-module.json | 249 ++++++++++++++++++ 8 files changed, 583 insertions(+), 2 deletions(-) create mode 100644 modules/mesh-build-go/cmd/mesh-build-go/testdata/captured/claude-code-module.json create mode 100644 modules/mesh-build-image/cmd/mesh-build-image/testdata/captured/claude-code-module.json diff --git a/modules/mesh-build-go/cmd/mesh-build-go/build.go b/modules/mesh-build-go/cmd/mesh-build-go/build.go index 2bcecbdd..b1f89d7e 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/build.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/build.go @@ -92,7 +92,7 @@ func (m *Module) build(ctx context.Context, args map[string]any) (any, error) { if err != nil { return nil, fmt.Errorf("no module in %q of workspace %s: %w", module, workspace, err) } - manifest, err := catalogue.ParseManifest(raw) + manifest, err := catalogue.ParseManifestForInputs(raw) if err != nil { return nil, fmt.Errorf("the manifest in %q: %w", module, err) } diff --git a/modules/mesh-build-go/cmd/mesh-build-go/build_test.go b/modules/mesh-build-go/cmd/mesh-build-go/build_test.go index 3cac66ec..549ed826 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/build_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/build_test.go @@ -237,3 +237,24 @@ func TestABuildOutputDirectoryLinkedOutsideTheModuleIsRefused(t *testing.T) { t.Errorf("a file outside the module was touched: %q, %v", body, err) } } + +// Issue 1751 (ADR 0514 §2, the review's K2): a manifest whose config value falls back to a predefined key builds. The +// builder holds no published list of the keys, so it reads the manifest for its inputs (ParseManifestForInputs), not +// as a judge of config values; claude-code's real manifest (testdata/captured) is the fixture. +func TestAManifestFallingBackToAPredefinedKeyBuilds(t *testing.T) { + raw, err := os.ReadFile(filepath.Join("testdata", "captured", "claude-code-module.json")) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(raw), `"predefined": "agent-managed-settings"`) { + t.Fatal("the captured manifest no longer falls back to a predefined key") + } + m, states, _ := buildModule(t, string(raw), "binary") + res, st := runBuild(t, m, states, "tools") + if st.State != delivery.StepPassed { + t.Fatalf("the step ended %s: %s", st.State, st.Reason) + } + if res.Artifact != "tools" || res.Binary != "claude-code" || !strings.HasPrefix(res.Digest, "sha256:") { + t.Errorf("result = %+v", res) + } +} diff --git a/modules/mesh-build-go/cmd/mesh-build-go/testdata/captured/README b/modules/mesh-build-go/cmd/mesh-build-go/testdata/captured/README index 194bc050..b298901c 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/testdata/captured/README +++ b/modules/mesh-build-go/cmd/mesh-build-go/testdata/captured/README @@ -24,3 +24,7 @@ The tar streams (the archives gunzipped) of the three archives above, captured t gunzip and sha256) at origin/main 0205b8f7 on 2026-10-21: directory f8af1d94...d1b6, within 294e2bec...07fb, file 055cffc2...428e. The digests above hold the gzip bytes of go1.27.1. The first try of issue 1589 on the merge check's build node answered other digests; the tests therefore compare the stream always and the digest only under go1.27.1. + +claude-code-module.json is modules/claude-code/module.json of this catalogue at 56c68be7 (copied 2026-10-22 for issue 1751, +the R4e review's finding K2): a real manifest whose config value managed_settings falls back to the predefined key +agent-managed-settings (ADR 0348 §7), which the builder holds no published list for. diff --git a/modules/mesh-build-go/cmd/mesh-build-go/testdata/captured/claude-code-module.json b/modules/mesh-build-go/cmd/mesh-build-go/testdata/captured/claude-code-module.json new file mode 100644 index 00000000..3b62d219 --- /dev/null +++ b/modules/mesh-build-go/cmd/mesh-build-go/testdata/captured/claude-code-module.json @@ -0,0 +1,249 @@ +{ + "module": "claude-code", + "version": "1", + "moved": true, + "slug": "agent", + "capabilities": [ + "package-manager" + ], + "requires": [ + "mcp-endpoint", + { + "name": "gitea-read", + "optional": true + } + ], + "binds": { + "mcp-endpoint": "${dir:state}/mcp-endpoint.json" + }, + "secrets": { + "gitea-read": "${dir:state}/gitea-read.secret" + }, + "uses": [ + "operator-channel" + ], + "state": [ + "servers", + "holdings", + "config", + "proposals", + "questions", + "memory", + "waits" + ], + "reads": [ + "claude-licence-manager.bindings" + ], + "tools": [ + "claude_code_status", + "claude_code_render", + "claude_code_guard", + "claude_code_pull", + "claude_code_grant", + "claude_code_add_api_key", + "claude_code_registrations", + "claude_code_memory", + "claude_code_mcp_list", + "claude_code_mcp_register", + "claude_code_mcp_unregister", + "claude_code_skill_list", + "claude_code_skill_register", + "claude_code_skill_unregister", + "claude_code_agent_list", + "claude_code_agent_register", + "claude_code_agent_unregister", + "claude_code_command_list", + "claude_code_command_register", + "claude_code_command_unregister", + "claude_code_hook_list", + "claude_code_hook_register", + "claude_code_hook_unregister", + "claude_code_output_style_list", + "claude_code_output_style_register", + "claude_code_output_style_unregister", + "claude_code_instructions_list", + "claude_code_instructions_register", + "claude_code_instructions_unregister", + "claude_code_instructions_propose", + "claude_code_proposals", + "claude_code_ask", + "claude_code_answer", + "claude_code_wait", + "claude_code_settings_get", + "claude_code_settings_set", + "claude_code_settings_clear", + "claude_code_permission_add", + "claude_code_permission_remove", + "claude_code_config_list", + "claude_code_config_show", + "claude_code_config_status", + "claude_code_config_import", + "claude_code_home_show", + "claude_code_home_remove", + "claude_code_home_removed", + "claude_code_home_restore", + "claude_code_judge", + "claude_code_judged" + ], + "data": { + "own": [ + { + "id": "agent-home", + "path": "${dir:agent-home}", + "class": "valuable", + "why": "the operator's agent's own files: its memory, history and projects" + } + ] + }, + "config": { + "role": { + "type": "string", + "description": "What this machine is for, in a sentence, written into the instructions every Claude Code session on it reads; empty says nothing.", + "trusted": true, + "default": "" + }, + "mcp_servers": { + "type": "object", + "description": "The tool servers every session on this machine is given beside the mesh's own, by name, each as Claude Code's managed MCP configuration takes it; empty adds none.", + "trusted": true, + "default": {} + }, + "managed_settings": { + "type": "object", + "description": "Keys of Claude Code's managed settings every session on this machine obeys, such as spinnerTipsEnabled, laid over the mesh's own; empty adds none. Falls back to agent-managed-settings, the mesh's or this node's.", + "trusted": true, + "predefined": "agent-managed-settings", + "default": {} + }, + "instructions": { + "type": "object", + "description": "The instructions every session on this machine reads, by name, each a text as one line or a list of its lines; empty adds none.", + "trusted": true, + "default": {} + }, + "output_styles": { + "type": "object", + "description": "The output styles offered to every session on this machine, by name, each a text as one line or a list of its lines; empty adds none.", + "trusted": true, + "default": {} + }, + "skills": { + "type": "object", + "description": "The skills offered to every session on this machine, by name, each its files by path, a text as one line or a list of its lines; a skill without its SKILL.md is left out; empty adds none.", + "trusted": true, + "default": {} + }, + "commands": { + "type": "object", + "description": "The slash commands offered to every session on this machine, by name, each a text as one line or a list of its lines; empty adds none.", + "trusted": true, + "default": {} + }, + "agents": { + "type": "object", + "description": "The agents offered to every session on this machine, by name, each a text as one line or a list of its lines; empty adds none.", + "trusted": true, + "default": {} + } + }, + "resources": [ + { + "id": "package", + "type": "package", + "package": "claude-code" + }, + { + "id": "managed", + "type": "directory", + "path": "/etc/claude-code", + "mode": "0755", + "unknown": "program" + }, + { + "id": "start", + "type": "file", + "path": "/usr/local/bin/claude-agent", + "mode": "0755", + "template": "files/claude-agent" + }, + { + "id": "agent-account-name", + "type": "file", + "path": "/etc/claude-code/agent-account", + "mode": "0644", + "template": "files/agent-account" + }, + { + "id": "agent-account", + "type": "user", + "name": "${machine:agent-account}", + "home": "${machine:agent-home}", + "root": "${machine:agent-root}" + }, + { + "id": "agent-home", + "type": "directory", + "path": "${machine:agent-home}/.claude", + "mode": "0700", + "owner": "${machine:agent-account}", + "unknown": "module" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "owner": "${machine:account}", + "place": ".", + "unknown": "program" + }, + { + "id": "facts", + "type": "file", + "path": "${dir:state}/facts.json", + "mode": "0600", + "owner": "${machine:account}", + "template": "files/facts.json" + }, + { + "id": "settings", + "type": "file", + "path": "${dir:state}/settings.json", + "mode": "0600", + "owner": "${machine:account}", + "takes-config": ["role", "mcp_servers", "managed_settings", "instructions", "output_styles", "skills", "commands", "agents"], + "merge": "json", + "template": "files/settings.json" + } + ], + "items": [ + { + "collection": "shell:zsh:normal", + "template": "files/zsh-normal" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/claude-code", + "binary": "claude-code", + "loads": [ + "claude-code" + ], + "env": { + "MESH_CLAUDE_CODE_STATE": "${dir:state}", + "MESH_CLAUDE_CODE_FACTS": "${dir:state}/facts.json", + "MESH_CLAUDE_CODE_SETTINGS": "${dir:state}/settings.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +} diff --git a/modules/mesh-build-image/cmd/mesh-build-image/build.go b/modules/mesh-build-image/cmd/mesh-build-image/build.go index baee275e..e138fd75 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/build.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/build.go @@ -126,7 +126,7 @@ func (m *Module) build(ctx context.Context, args map[string]any) (any, error) { if err != nil { return nil, fmt.Errorf("no module in %q of workspace %s: %w", module, workspace, err) } - manifest, err := catalogue.ParseManifest(raw) + manifest, err := catalogue.ParseManifestForInputs(raw) if err != nil { return nil, fmt.Errorf("the manifest in %q: %w", module, err) } diff --git a/modules/mesh-build-image/cmd/mesh-build-image/build_test.go b/modules/mesh-build-image/cmd/mesh-build-image/build_test.go index 46fadba6..d6d0dd64 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/build_test.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/build_test.go @@ -198,3 +198,56 @@ func TestBuildFailsForWhatIsNotAnImageOrNotThere(t *testing.T) { } } } + +// Issue 1751 (ADR 0514 §2, the review's K2): a manifest whose config value falls back to a predefined key builds. The +// builder holds no published list of the keys, so it reads the manifest for its inputs (ParseManifestForInputs), not +// as a judge of config values. The manifest is claude-code's real one (testdata/captured) with the image fixture's +// base and artifact added to its build, since no image module of the catalogue falls back to a predefined key today. +func TestAManifestFallingBackToAPredefinedKeyBuilds(t *testing.T) { + raw, err := os.ReadFile(filepath.Join("testdata", "captured", "claude-code-module.json")) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(raw), `"predefined": "agent-managed-settings"`) { + t.Fatal("the captured manifest no longer falls back to a predefined key") + } + m, _, states := setup(t, func(fixture string) string { + var real, image map[string]json.RawMessage + if err := json.Unmarshal(raw, &real); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal([]byte(fixture), &image); err != nil { + t.Fatal(err) + } + // The image fixture's base and artifact join claude-code's own artifacts (its archive of files/ stays). + var realBuild, imageBuild struct { + On json.RawMessage `json:"on,omitempty"` + Artifacts []json.RawMessage `json:"artifacts"` + } + if err := json.Unmarshal(real["build"], &realBuild); err != nil { + t.Fatal(err) + } + if err := json.Unmarshal(image["build"], &imageBuild); err != nil { + t.Fatal(err) + } + realBuild.On = imageBuild.On + realBuild.Artifacts = append(realBuild.Artifacts, imageBuild.Artifacts...) + b, err := json.Marshal(realBuild) + if err != nil { + t.Fatal(err) + } + real["build"] = b + out, err := json.Marshal(real) + if err != nil { + t.Fatal(err) + } + return string(out) + }) + res, st := runBuild(t, m, states, nil) + if st.State != delivery.StepPassed { + t.Fatalf("step = %+v", st) + } + if res.Digest != imageDigest || res.Artifact != "server" { + t.Errorf("result = %+v", res) + } +} diff --git a/modules/mesh-build-image/cmd/mesh-build-image/testdata/captured/README b/modules/mesh-build-image/cmd/mesh-build-image/testdata/captured/README index 11ed2340..90323c04 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/testdata/captured/README +++ b/modules/mesh-build-image/cmd/mesh-build-image/testdata/captured/README @@ -3,3 +3,8 @@ The stored steps (what a step ends on in the module's `steps` state) of a fetch the workspace, the commit, digests and the registry's address put to fixed words (STEPID, WORKSPACE, COMMIT, REGISTRY, one sha256 digest). Written by capture_test.go (MESH_UPDATE_CAPTURED=1) from the unit tests' fake runner on 2026-10-22, and compared by bus_integration_test.go with the steps a real bus holds. mesh-delivery-builder's tests copy these files. + +claude-code-module.json is modules/claude-code/module.json of this catalogue at 56c68be7 (copied 2026-10-22 for issue 1751, +the R4e review's finding K2): a real manifest whose config value managed_settings falls back to the predefined key +agent-managed-settings (ADR 0348 §7). No image module of the catalogue falls back to one today, so the test adds the image +fixture's base and artifact to its build. diff --git a/modules/mesh-build-image/cmd/mesh-build-image/testdata/captured/claude-code-module.json b/modules/mesh-build-image/cmd/mesh-build-image/testdata/captured/claude-code-module.json new file mode 100644 index 00000000..3b62d219 --- /dev/null +++ b/modules/mesh-build-image/cmd/mesh-build-image/testdata/captured/claude-code-module.json @@ -0,0 +1,249 @@ +{ + "module": "claude-code", + "version": "1", + "moved": true, + "slug": "agent", + "capabilities": [ + "package-manager" + ], + "requires": [ + "mcp-endpoint", + { + "name": "gitea-read", + "optional": true + } + ], + "binds": { + "mcp-endpoint": "${dir:state}/mcp-endpoint.json" + }, + "secrets": { + "gitea-read": "${dir:state}/gitea-read.secret" + }, + "uses": [ + "operator-channel" + ], + "state": [ + "servers", + "holdings", + "config", + "proposals", + "questions", + "memory", + "waits" + ], + "reads": [ + "claude-licence-manager.bindings" + ], + "tools": [ + "claude_code_status", + "claude_code_render", + "claude_code_guard", + "claude_code_pull", + "claude_code_grant", + "claude_code_add_api_key", + "claude_code_registrations", + "claude_code_memory", + "claude_code_mcp_list", + "claude_code_mcp_register", + "claude_code_mcp_unregister", + "claude_code_skill_list", + "claude_code_skill_register", + "claude_code_skill_unregister", + "claude_code_agent_list", + "claude_code_agent_register", + "claude_code_agent_unregister", + "claude_code_command_list", + "claude_code_command_register", + "claude_code_command_unregister", + "claude_code_hook_list", + "claude_code_hook_register", + "claude_code_hook_unregister", + "claude_code_output_style_list", + "claude_code_output_style_register", + "claude_code_output_style_unregister", + "claude_code_instructions_list", + "claude_code_instructions_register", + "claude_code_instructions_unregister", + "claude_code_instructions_propose", + "claude_code_proposals", + "claude_code_ask", + "claude_code_answer", + "claude_code_wait", + "claude_code_settings_get", + "claude_code_settings_set", + "claude_code_settings_clear", + "claude_code_permission_add", + "claude_code_permission_remove", + "claude_code_config_list", + "claude_code_config_show", + "claude_code_config_status", + "claude_code_config_import", + "claude_code_home_show", + "claude_code_home_remove", + "claude_code_home_removed", + "claude_code_home_restore", + "claude_code_judge", + "claude_code_judged" + ], + "data": { + "own": [ + { + "id": "agent-home", + "path": "${dir:agent-home}", + "class": "valuable", + "why": "the operator's agent's own files: its memory, history and projects" + } + ] + }, + "config": { + "role": { + "type": "string", + "description": "What this machine is for, in a sentence, written into the instructions every Claude Code session on it reads; empty says nothing.", + "trusted": true, + "default": "" + }, + "mcp_servers": { + "type": "object", + "description": "The tool servers every session on this machine is given beside the mesh's own, by name, each as Claude Code's managed MCP configuration takes it; empty adds none.", + "trusted": true, + "default": {} + }, + "managed_settings": { + "type": "object", + "description": "Keys of Claude Code's managed settings every session on this machine obeys, such as spinnerTipsEnabled, laid over the mesh's own; empty adds none. Falls back to agent-managed-settings, the mesh's or this node's.", + "trusted": true, + "predefined": "agent-managed-settings", + "default": {} + }, + "instructions": { + "type": "object", + "description": "The instructions every session on this machine reads, by name, each a text as one line or a list of its lines; empty adds none.", + "trusted": true, + "default": {} + }, + "output_styles": { + "type": "object", + "description": "The output styles offered to every session on this machine, by name, each a text as one line or a list of its lines; empty adds none.", + "trusted": true, + "default": {} + }, + "skills": { + "type": "object", + "description": "The skills offered to every session on this machine, by name, each its files by path, a text as one line or a list of its lines; a skill without its SKILL.md is left out; empty adds none.", + "trusted": true, + "default": {} + }, + "commands": { + "type": "object", + "description": "The slash commands offered to every session on this machine, by name, each a text as one line or a list of its lines; empty adds none.", + "trusted": true, + "default": {} + }, + "agents": { + "type": "object", + "description": "The agents offered to every session on this machine, by name, each a text as one line or a list of its lines; empty adds none.", + "trusted": true, + "default": {} + } + }, + "resources": [ + { + "id": "package", + "type": "package", + "package": "claude-code" + }, + { + "id": "managed", + "type": "directory", + "path": "/etc/claude-code", + "mode": "0755", + "unknown": "program" + }, + { + "id": "start", + "type": "file", + "path": "/usr/local/bin/claude-agent", + "mode": "0755", + "template": "files/claude-agent" + }, + { + "id": "agent-account-name", + "type": "file", + "path": "/etc/claude-code/agent-account", + "mode": "0644", + "template": "files/agent-account" + }, + { + "id": "agent-account", + "type": "user", + "name": "${machine:agent-account}", + "home": "${machine:agent-home}", + "root": "${machine:agent-root}" + }, + { + "id": "agent-home", + "type": "directory", + "path": "${machine:agent-home}/.claude", + "mode": "0700", + "owner": "${machine:agent-account}", + "unknown": "module" + }, + { + "id": "state", + "type": "directory", + "mode": "0700", + "owner": "${machine:account}", + "place": ".", + "unknown": "program" + }, + { + "id": "facts", + "type": "file", + "path": "${dir:state}/facts.json", + "mode": "0600", + "owner": "${machine:account}", + "template": "files/facts.json" + }, + { + "id": "settings", + "type": "file", + "path": "${dir:state}/settings.json", + "mode": "0600", + "owner": "${machine:account}", + "takes-config": ["role", "mcp_servers", "managed_settings", "instructions", "output_styles", "skills", "commands", "agents"], + "merge": "json", + "template": "files/settings.json" + } + ], + "items": [ + { + "collection": "shell:zsh:normal", + "template": "files/zsh-normal" + } + ], + "build": { + "artifacts": [ + { + "name": "tools", + "kind": "bundle", + "language": "go", + "system": "arch", + "from": "cmd/claude-code", + "binary": "claude-code", + "loads": [ + "claude-code" + ], + "env": { + "MESH_CLAUDE_CODE_STATE": "${dir:state}", + "MESH_CLAUDE_CODE_FACTS": "${dir:state}/facts.json", + "MESH_CLAUDE_CODE_SETTINGS": "${dir:state}/settings.json" + } + }, + { + "name": "files", + "kind": "archive", + "from": "files" + } + ] + } +}