claude-code: act on the review of the nox-mesh plugin

Refuse paths with empty, dot or parent segments and a file that is also a
directory; take an item only when its key says what it is; write the view
under its lock; expand nodes all and check node names; merge the plugin's
entries into the operator's own; render every file past one that fails;
in the home, never take over the person's file, never write through a
symbolic link, keep a deleted file deleted, keep the kind's directory; and
refuse settings that would deny the console or the marketplace.
This commit is contained in:
jochen
2026-10-05 14:29:53 +02:00
parent 1d8f1ceff8
commit 92f78db970
6 changed files with 314 additions and 41 deletions
+5 -3
View File
@@ -29,8 +29,9 @@ whenever the node's tool runtime collects the module's tools:
Under the operator's home: `~/.claude/.credentials.json`, only when the licence manager hands this node a
subscription token; and what is registered at the **home** scope for this node — a skill, subagent,
command, output style, or instructions as a rule file — each path recorded in the module's state
(`home-placed.json`). It writes, changes and removes only those, never a path the person made, and leaves a
placed file alone once it was changed by hand (hq ADR 0182). The status tool reads the rest of the home's
(`home-placed.json`). It writes, changes and removes only those — never a path the person made, even one with the
same content, and never through a directory that is a symbolic link. A placed file changed by hand is left
alone, and one the person deleted stays deleted until the item is unregistered (hq ADR 0182). The status tool reads the rest of the home's
items to report them; nothing else is read or written.
## Over NATS
@@ -56,7 +57,8 @@ manager), `claude_code_mcp_list`,
node alone, its answer names the other nodes running claude-code), `claude_code_mcp_unregister`.
The agent's configuration (hq ADR 0216), each registered at a **scope** — `mesh` (the default), `node`
(`nodes`, or this node) or `home` (the operator account's own `~/.claude` on `nodes`, or this node):
(`nodes`: a list, or `"all"` for every node running claude-code; absent is this node) or `home` (the
operator account's own `~/.claude` on those nodes):
- for each kind — `skill`, `agent`, `command`, `hook`, `output_style`, `instructions` —
`claude_code_<kind>_list`, `_register`, `_unregister`. A skill is its files (`files`, or `content` for a
+132 -27
View File
@@ -69,6 +69,14 @@ const SettingsName = "settings"
var itemName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,63}$`)
// nodeName is what a node may be called in a key.
var nodeName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}$`)
// meshOwnedKeys are the settings a registration may not set: the mesh's own, and those that would deny
// the mesh's console or its marketplace by another way.
var meshOwnedKeys = []string{"attribution", "allowAllClaudeAiMcps", "apiKeyHelper", "extraKnownMarketplaces", "enabledPlugins",
"allowedMcpServers", "deniedMcpServers", "allowManagedMcpServersOnly", "strictKnownMarketplaces", "blockedMarketplaces"}
// hookEvents are the events a hook may be registered for.
var hookEvents = map[string]bool{"PreToolUse": true, "PostToolUse": true, "UserPromptSubmit": true, "Notification": true,
"Stop": true, "SubagentStop": true, "SessionStart": true, "SessionEnd": true, "PreCompact": true}
@@ -139,9 +147,9 @@ func (it Item) Problem() string {
if len(it.Settings) == 0 {
return "no settings given"
}
for _, key := range []string{"attribution", "allowAllClaudeAiMcps", "apiKeyHelper", "extraKnownMarketplaces", "enabledPlugins"} {
for _, key := range meshOwnedKeys {
if _, ok := it.Settings[key]; ok {
return fmt.Sprintf("%q is one of the mesh's own keys; a registration cannot set it", key)
return fmt.Sprintf("%q is one of the mesh's own keys, or would turn off the mesh's console or plugin; a registration cannot set it", key)
}
}
} else if !itemName.MatchString(it.Name) {
@@ -159,8 +167,13 @@ func (it Item) Problem() string {
}
}
for rel := range it.Files {
if rel == "" || path.IsAbs(rel) || strings.Contains(rel, "\\") || path.Clean(rel) != rel || strings.HasPrefix(rel, "..") {
return fmt.Sprintf("%q is not a path inside the item", rel)
if problem := pathProblem(rel); problem != "" {
return problem
}
for other := range it.Files {
if strings.HasPrefix(other, rel+"/") {
return fmt.Sprintf("%q is a file and also the directory of %q", rel, other)
}
}
}
switch it.Kind {
@@ -179,6 +192,20 @@ func (it Item) Problem() string {
return ""
}
// pathProblem says why a file's path is not one inside the item, or "": relative, slash-separated, every
// segment a real name — never empty, `.` or `..`.
func pathProblem(rel string) string {
if rel == "" || path.IsAbs(rel) || strings.ContainsAny(rel, "\\\x00") {
return fmt.Sprintf("%q is not a path inside the item", rel)
}
for _, seg := range strings.Split(rel, "/") {
if seg == "" || seg == "." || seg == ".." {
return fmt.Sprintf("%q is not a path inside the item", rel)
}
}
return ""
}
func (it Item) size() int {
raw, _ := json.Marshal(it)
return len(raw)
@@ -213,14 +240,17 @@ func (v *ConfigView) Take(key, op string, item *Item) bool {
if !v.Applies(key) {
return false
}
_, node, _, _, _ := ParseKey(key)
v.mu.Lock()
if op == "put" && item != nil && item.Problem() == "" {
defer v.mu.Unlock()
// Only an item that is what its key says: a key decides which nodes take an item, the item where it
// lands, and the two must agree — a mesh key holding a home item would land in every home.
if op == "put" && item != nil && item.Problem() == "" && item.Key(node) == key {
v.items[key] = *item
} else {
delete(v.items, key)
}
v.mu.Unlock()
return v.writeThrough()
return v.writeThroughLocked()
}
// Prune drops what the view holds and the state no longer does: a registration removed while this node
@@ -231,13 +261,13 @@ func (v *ConfigView) Prune(present []string) bool {
keep[k] = true
}
v.mu.Lock()
defer v.mu.Unlock()
for k := range v.items {
if !keep[k] {
delete(v.items, k)
}
}
v.mu.Unlock()
return v.writeThrough()
return v.writeThroughLocked()
}
// Items is what applies here, by key.
@@ -251,8 +281,10 @@ func (v *ConfigView) Items() map[string]Item {
return out
}
func (v *ConfigView) writeThrough() bool {
now, _ := indented(v.Items())
// writeThroughLocked writes the view to its file, with v.mu held: the snapshot and the write are one step, so
// an older snapshot never lands after a newer one.
func (v *ConfigView) writeThroughLocked() bool {
now, _ := indented(v.items)
before, _ := os.ReadFile(v.p.config())
if string(before) == string(now) {
return false
@@ -481,9 +513,15 @@ func digest(s string) string {
return hex.EncodeToString(sum[:])
}
// PlaceHome brings the home in line with what the home scope wants: writes what is wanted and absent or
// its own, never a path the person made, and removes what it placed and is no longer wanted — unless the
// person changed it since. Answers what it did and what it left alone, and why.
// deletedByHand marks, in the record, a path the mesh placed and the person then deleted: their choice,
// kept until the item is unregistered.
const deletedByHand = "deleted-by-hand"
// PlaceHome brings the home in line with what the home scope wants (ADR 0182): it writes what is wanted and
// absent, or its own; it never writes a path the person made, nor through a directory that is a symbolic
// link; it removes what it placed and is no longer wanted, unless the person changed it since; and a file it
// placed that the person deleted stays deleted until the item is unregistered. Answers what it did and what
// it left alone, and why.
func PlaceHome(p Paths, want map[string]string) (done []string, left []string) {
dir := filepath.Join(p.Home, ".claude")
var placed Placed
@@ -498,22 +536,30 @@ func PlaceHome(p Paths, want map[string]string) (done []string, left []string) {
for _, rel := range paths {
full := filepath.Join(dir, filepath.FromSlash(rel))
content := want[rel]
if why := linkedParent(dir, rel); why != "" {
left = append(left, rel+": "+why+", left alone")
continue
}
current, err := os.ReadFile(full)
exists := err == nil
ours, wasPlaced := placed[rel]
switch {
case !exists && wasPlaced && ours == deletedByHand:
continue
case !exists && wasPlaced:
placed[rel] = deletedByHand
left = append(left, rel+": deleted by hand, left deleted until the item is unregistered")
continue
case exists && !wasPlaced:
if string(current) == content {
placed[rel] = digest(content) // the same bytes: taken over, nothing changes
continue
}
left = append(left, rel+": the person's own, left alone")
continue
case exists && wasPlaced && digest(string(current)) != ours && digest(string(current)) != digest(content):
left = append(left, rel+": changed by hand since it was placed, left alone")
case exists && ours == deletedByHand:
left = append(left, rel+": made again by hand after the mesh's was deleted, left alone")
continue
case exists && string(current) == content:
placed[rel] = digest(content)
continue
case exists && digest(string(current)) != ours:
left = append(left, rel+": changed by hand since it was placed, left alone")
continue
}
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
@@ -536,22 +582,50 @@ func PlaceHome(p Paths, want map[string]string) (done []string, left []string) {
continue
}
full := filepath.Join(dir, filepath.FromSlash(rel))
if ours == deletedByHand {
delete(placed, rel)
continue
}
if why := linkedParent(dir, rel); why != "" {
left = append(left, rel+": no longer registered, but "+why+", left alone")
continue
}
current, err := os.ReadFile(full)
if err == nil && digest(string(current)) != ours {
left = append(left, rel+": no longer registered, but changed by hand, left alone")
delete(placed, rel)
continue
}
_ = os.Remove(full)
removeEmptyParents(dir, filepath.Dir(full))
if err := os.Remove(full); err != nil && !os.IsNotExist(err) {
left = append(left, rel+": no longer registered, and could not be removed: "+err.Error())
continue
}
// Up to the kind's own directory, never it: `skills/<name>` goes when empty, `skills` stays.
removeEmptyParents(filepath.Join(dir, strings.SplitN(rel, "/", 2)[0]), filepath.Dir(full))
delete(placed, rel)
done = append(done, rel+": removed")
}
raw, _ := indented(placed)
_ = os.WriteFile(p.placed(), raw, 0o600)
if err := os.WriteFile(p.placed(), raw, 0o600); err != nil {
left = append(left, "the record of what was placed could not be saved: "+err.Error())
}
return done, left
}
// linkedParent says, when a directory between the agent directory and a file is a symbolic link, which one:
// writing through it would write wherever it points.
func linkedParent(dir, rel string) string {
parts := strings.Split(rel, "/")
at := dir
for _, seg := range parts[:len(parts)-1] {
at = filepath.Join(at, seg)
if info, err := os.Lstat(at); err == nil && info.Mode()&os.ModeSymlink != 0 {
return at + " is a symbolic link"
}
}
return ""
}
// removeEmptyParents removes empty directories from dir up to, not including, root.
func removeEmptyParents(root, dir string) {
for dir != root && strings.HasPrefix(dir, root+string(filepath.Separator)) {
@@ -714,6 +788,8 @@ func Register(p Paths, it Item, nodes []string, unregister bool, state ConfigSta
nodes = []string{""}
} else if len(nodes) == 0 {
nodes = []string{p.Node}
} else if problem := nodesProblem(nodes); problem != "" {
return map[string]any{verbOf(unregister): false, "reason": problem}, nil
}
if !unregister && it.Scope == ScopeHome {
for _, n := range nodes {
@@ -761,10 +837,10 @@ func Register(p Paths, it Item, nodes []string, unregister bool, state ConfigSta
}
if changed {
rendered, err := RenderNow(p, write)
if err != nil {
return nil, err
}
answer["rendered here"] = rendered
if err != nil {
answer["not written here"] = err.Error() // kept on the bus all the same; the next render tries again
}
answer["sessions"] = "a new session takes it; a running one at /reload-plugins"
} else if v.Applies(keys[0]) || len(keys) > 1 {
answer["here"] = "already so"
@@ -774,6 +850,35 @@ func Register(p Paths, it Item, nodes []string, unregister bool, state ConfigSta
return answer, nil
}
func verbOf(unregister bool) string {
return map[bool]string{false: "registered", true: "unregistered"}[unregister]
}
// nodesProblem says why a list of nodes cannot name keys, or "". "all" has been expanded before this.
func nodesProblem(nodes []string) string {
for _, n := range nodes {
if !nodeName.MatchString(n) {
return fmt.Sprintf("%q is not a node's name", n)
}
}
return ""
}
// ExpandNodes turns `all` into every node running the module; anything else is kept.
func ExpandNodes(nodes []string, running func() ([]string, error)) ([]string, error) {
if len(nodes) != 1 || nodes[0] != "all" {
return nodes, nil
}
all, err := running()
if err != nil {
return nil, fmt.Errorf("which nodes run claude-code: %w", err)
}
if len(all) == 0 {
return nil, fmt.Errorf("no node is known to run claude-code")
}
return all, nil
}
// List is every registration of a kind ("" for every kind) on the mesh, by key, read from the state.
func List(state ConfigState, kind string) (map[string]any, error) {
keys, err := state.Keys()
@@ -339,3 +339,119 @@ func TestWhatWasRemovedWhileANodeWasAwayIsDropped(t *testing.T) {
t.Fatalf("after pruning: %v", back.Items())
}
}
// The review's findings, each held by a test.
func TestAPathOrAKeyThatIsNotWhatItSaysIsRefused(t *testing.T) {
for label, files := range map[string]map[string]string{
"a dot": {"SKILL.md": "x", ".": "x"},
"an empty segment": {"SKILL.md": "x", "a//b": "x"},
"a parent segment": {"SKILL.md": "x", "a/../b": "x"},
"a file and directory": {"SKILL.md": "x", "a": "x", "a/b": "x"},
} {
if (Item{Kind: KindSkill, Name: "s", Scope: ScopeMesh, Files: files}).Problem() == "" {
t.Errorf("%s was accepted", label)
}
}
p, _ := node(t, "laptop")
v := NewConfigView(p)
home := Item{Kind: KindCommand, Name: "x", Scope: ScopeHome, Files: one("x", "y")}
if v.Take("mesh.command.x", "put", &home); len(v.Items()) != 0 {
t.Fatal("a mesh key holding a home item was taken")
}
for _, key := range []string{"mesh.command.x", "mesh.agent.x"} {
mesh := Item{Kind: KindCommand, Name: "x", Scope: ScopeMesh, Files: one("x", "y")}
v.Take(key, "put", &mesh)
}
if len(v.Items()) != 1 {
t.Fatalf("an item under a key of another kind was taken: %v", v.Items())
}
}
func TestAllIsEveryNodeAndANameThatCannotBeAKeyIsRefused(t *testing.T) {
nodes, err := ExpandNodes([]string{"all"}, func() ([]string, error) { return []string{"ace", "g14"}, nil })
if err != nil || strings.Join(nodes, ",") != "ace,g14" {
t.Fatalf("%v %v", nodes, err)
}
p, w := node(t, "laptop")
answer, _ := Register(p, Item{Kind: KindCommand, Name: "c", Scope: ScopeNode, Files: one("c", "x")}, []string{"a.b"}, false, memConfig{}, NewConfigView(p), writer(w))
if answer["registered"] != false {
t.Fatalf("a node name with a dot was used in a key: %v", answer)
}
if (Item{Kind: KindSettings, Name: SettingsName, Scope: ScopeMesh, Settings: map[string]any{"deniedMcpServers": []any{}}}).Problem() == "" {
t.Fatal("a registration could deny the mesh's console")
}
}
func TestTheOperatorsOwnPluginsAndMarketplacesAreKept(t *testing.T) {
out := Render(Facts{Console: "x"}, Settings{ManagedSettings: map[string]any{
"enabledPlugins": map[string]any{"theirs@market": true},
"extraKnownMarketplaces": map[string]any{"market": map[string]any{"source": map[string]any{"source": "github", "repo": "o/r"}}},
}}, nil, "/h", nil, Config{})
var managed struct {
Enabled map[string]any `json:"enabledPlugins"`
Known map[string]any `json:"extraKnownMarketplaces"`
}
_ = json.Unmarshal([]byte(out["managed-settings.json"]), &managed)
if managed.Enabled["theirs@market"] != true || managed.Enabled[Plugin+"@"+Plugin] != true || managed.Known["market"] == nil || managed.Known[Plugin] == nil {
t.Fatalf("%+v", managed)
}
}
func TestTheHomeLeavesThePersonsChoicesAlone(t *testing.T) {
p, _ := node(t, "laptop")
dir := filepath.Join(p.Home, ".claude")
// An identical file the person made is not taken over, so unregistering never removes it.
_ = os.MkdirAll(filepath.Join(dir, "agents"), 0o755)
writeFile(t, filepath.Join(dir, "agents", "same.md"), "x")
if _, left := PlaceHome(p, map[string]string{"agents/same.md": "x"}); len(left) != 1 {
t.Fatalf("an identical file of the person's was taken over: %v", left)
}
PlaceHome(p, map[string]string{})
if _, err := os.Stat(filepath.Join(dir, "agents", "same.md")); err != nil {
t.Fatal("the person's file was removed")
}
// A placed file the person deleted stays deleted while it is registered.
PlaceHome(p, map[string]string{"commands/c.md": "x"})
_ = os.Remove(filepath.Join(dir, "commands", "c.md"))
PlaceHome(p, map[string]string{"commands/c.md": "x"})
if _, err := os.Stat(filepath.Join(dir, "commands", "c.md")); err == nil {
t.Fatal("a file the person deleted was placed again")
}
// The kind's own directory stays when the mesh's last item in it goes.
PlaceHome(p, map[string]string{"skills/s/SKILL.md": "x"})
PlaceHome(p, map[string]string{})
if _, err := os.Stat(filepath.Join(dir, "skills", "s")); err == nil {
t.Fatal("the item's own directory was left")
}
if _, err := os.Stat(filepath.Join(dir, "skills")); err != nil {
t.Fatal("the kind's directory was removed")
}
// Never through a symbolic link.
elsewhere := t.TempDir()
if err := os.Symlink(elsewhere, filepath.Join(dir, "output-styles")); err != nil {
t.Skip("no symbolic links here")
}
PlaceHome(p, map[string]string{"output-styles/o.md": "x"})
if _, err := os.Stat(filepath.Join(elsewhere, "o.md")); err == nil {
t.Fatal("a file was written through a symbolic link")
}
}
func TestOneFileThatCannotBeWrittenDoesNotStopTheOthers(t *testing.T) {
p, _ := node(t, "laptop")
w := map[string]string{}
failing := func(name, content string) (string, error) {
if name == MarketplaceDir+"/" {
return "", os.ErrPermission
}
w[name] = content
return name + ": written", nil
}
if _, err := RenderNow(p, failing); err == nil {
t.Fatal("the failure was not reported")
}
if w["managed-settings.json"] == "" || w["managed-mcp.json"] == "" || w["CLAUDE.md"] == "" {
t.Fatalf("the other files were not written: %v", w)
}
}
@@ -41,6 +41,11 @@ func strArg(a map[string]any, k string) string {
return strings.TrimSpace(s)
}
// targetNodes reads the `nodes` argument: absent is this node, "all" every node running claude-code, else a list.
func targetNodes(a map[string]any) ([]string, error) {
return ExpandNodes(nodesOf(a["nodes"]), nodesRunningMe)
}
// scopeOf reads the `scope` argument; absent is the mesh, which is what a registration is most often for.
func scopeOf(a map[string]any) string {
if s := strArg(a, "scope"); s != "" {
@@ -76,7 +81,7 @@ func filesOf(kind, name string, a map[string]any) (map[string]string, error) {
func configTools(p Paths, state ConfigState, view *ConfigView) []stdio.Tool {
scopeArg := str(`mesh (every node; the default), node (the nodes given, or this one) or home (the operator account's own ~/.claude on the nodes given, or this one)`)
nodesArg := str(`for the node and home scopes: a comma-separated list of nodes; absent is this node`)
nodesArg := str(`for the node and home scopes: "all" for every node running claude-code, or a comma-separated list; absent is this node`)
var out []stdio.Tool
for _, k := range kindTools {
k := k
@@ -112,14 +117,22 @@ func configTools(p Paths, state ConfigState, view *ConfigView) []stdio.Tool {
}
it := Item{Kind: k.kind, Name: name, Scope: scopeOf(a), Files: files,
Event: strArg(a, "event"), Matcher: strArg(a, "matcher"), Command: strArg(a, "command")}
return Register(p, it, nodesOf(a["nodes"]), false, state, view, writeManaged)
nodes, err := targetNodes(a)
if err != nil {
return nil, err
}
return Register(p, it, nodes, false, state, view, writeManaged)
}},
stdio.Tool{Name: "claude_code_" + k.tool + "_unregister",
Description: "Remove a " + k.kind + " registered through this module, at its scope. At home, only what the mesh placed is removed, and not if it was changed by hand since.",
Input: map[string]any{"name": str("the name"), "scope": scopeArg, "nodes": nodesArg},
Run: func(a map[string]any) (any, error) {
it := Item{Kind: k.kind, Name: strArg(a, "name"), Scope: scopeOf(a)}
return Register(p, it, nodesOf(a["nodes"]), true, state, view, writeManaged)
nodes, err := targetNodes(a)
if err != nil {
return nil, err
}
return Register(p, it, nodes, true, state, view, writeManaged)
}},
)
}
@@ -160,7 +173,11 @@ func configTools(p Paths, state ConfigState, view *ConfigView) []stdio.Tool {
if len(given) == 0 {
return nil, errors.New("no settings given; to remove a scope's settings, claude_code_settings_clear")
}
return SetSettings(p, scopeOf(a), nodesOf(a["nodes"]), func(held map[string]any) map[string]any {
nodes, err := targetNodes(a)
if err != nil {
return nil, err
}
return SetSettings(p, scopeOf(a), nodes, func(held map[string]any) map[string]any {
if boolArg(a, "replace") {
return given
}
@@ -172,7 +189,11 @@ func configTools(p Paths, state ConfigState, view *ConfigView) []stdio.Tool {
Input: map[string]any{"scope": settingsScope, "nodes": nodesArg},
Run: func(a map[string]any) (any, error) {
it := Item{Kind: KindSettings, Name: SettingsName, Scope: scopeOf(a)}
return Register(p, it, nodesOf(a["nodes"]), true, state, view, writeManaged)
nodes, err := targetNodes(a)
if err != nil {
return nil, err
}
return Register(p, it, nodes, true, state, view, writeManaged)
}},
stdio.Tool{Name: "claude_code_permission_add",
Description: "Add a permission rule to Claude Code's managed settings, for every node or some: allow (runs without asking), ask (always asks) or deny (never runs). A rule is a tool and an optional specifier, e.g. Bash(git status:*), Read(./secrets/**), mcp__mesh__mesh_call.",
@@ -182,7 +203,11 @@ func configTools(p Paths, state ConfigState, view *ConfigView) []stdio.Tool {
if (list != "allow" && list != "ask" && list != "deny") || rule == "" {
return nil, errors.New("list is allow, ask or deny, and a rule is needed")
}
return SetSettings(p, scopeOf(a), nodesOf(a["nodes"]), func(held map[string]any) map[string]any {
nodes, err := targetNodes(a)
if err != nil {
return nil, err
}
return SetSettings(p, scopeOf(a), nodes, func(held map[string]any) map[string]any {
return mergeSettings(held, map[string]any{"permissions": map[string]any{list: []any{rule}}})
}, state, view)
}},
@@ -191,7 +216,11 @@ func configTools(p Paths, state ConfigState, view *ConfigView) []stdio.Tool {
Input: map[string]any{"list": str("allow, ask or deny"), "rule": str("the rule"), "scope": settingsScope, "nodes": nodesArg},
Run: func(a map[string]any) (any, error) {
list, rule := strArg(a, "list"), strArg(a, "rule")
return SetSettings(p, scopeOf(a), nodesOf(a["nodes"]), func(held map[string]any) map[string]any {
nodes, err := targetNodes(a)
if err != nil {
return nil, err
}
return SetSettings(p, scopeOf(a), nodes, func(held map[string]any) map[string]any {
perms, _ := held["permissions"].(map[string]any)
rules, _ := perms[list].([]any)
if len(rules) == 0 {
@@ -266,7 +295,11 @@ func configTools(p Paths, state ConfigState, view *ConfigView) []stdio.Tool {
if it.Scope == ScopeHome {
return nil, errors.New("it is already at home; import it to the mesh or node scope")
}
return Register(p, it, nodesOf(a["nodes"]), false, state, view, writeManaged)
nodes, err := targetNodes(a)
if err != nil {
return nil, err
}
return Register(p, it, nodes, false, state, view, writeManaged)
}},
)
return out
@@ -283,6 +316,8 @@ func SetSettings(p Paths, scope string, nodes []string, change func(held map[str
nodes = []string{""}
} else if len(nodes) == 0 {
nodes = []string{p.Node}
} else if problem := nodesProblem(nodes); problem != "" {
return map[string]any{"set": false, "reason": problem}, nil
}
answers := map[string]any{}
for _, n := range nodes {
+6 -2
View File
@@ -141,11 +141,15 @@ func RenderNow(p Paths, write WriteManaged) ([]string, error) {
}
return names[i] < names[j]
})
// Every file is attempted: one that cannot be written — a registration the vendor's layout refuses, a
// failed escalation — must not keep the licence, the tool servers or the instructions from landing.
var out []string
var failed []error
for _, n := range names {
line, err := write(n, files[n])
if err != nil {
return out, err
failed = append(failed, err)
continue
}
out = append(out, line)
}
@@ -157,7 +161,7 @@ func RenderNow(p Paths, write WriteManaged) ([]string, error) {
for _, line := range left {
out = append(out, "home "+line)
}
return out, nil
return out, errors.Join(failed...)
}
// ---- the licence ----------------------------------------------------------------------------------
+12 -1
View File
@@ -118,8 +118,19 @@ func Render(facts Facts, settings Settings, binding *Binding, helperPath string,
managed["attribution"] = map[string]any{"commit": "", "pr": ""}
managed["allowAllClaudeAiMcps"] = true
delete(managed, "apiKeyHelper")
// The plugin's marketplace and the plugin itself, as entries in the operator's own maps, the mesh's
// entry winning: the operator may know more marketplaces and enable more plugins.
for key, value := range MarketplaceKeys() {
managed[key] = value
entries := map[string]any{}
if held, ok := managed[key].(map[string]any); ok {
for k, v := range held {
entries[k] = v
}
}
for k, v := range value.(map[string]any) {
entries[k] = v
}
managed[key] = entries
}
if binding != nil && binding.Kind == "api-key" {
managed["apiKeyHelper"] = helperPath