claude-code: refuse registering what a session runs, so no agent can plant it in the operator's sessions
The tool runner is not told who called, so a hook, a managed setting, a permission rule or a tool server registered through these tools could come from an agent confined on another node and run as the operator there (hq issue 340). They are set at the controller's terminal now; a skill, subagent or command that runs something by itself is refused too, kept hooks and settings are never rendered, and claude_code_registrations lists what is registered and by which node.
This commit is contained in:
@@ -318,6 +318,18 @@ func (b *bus) Delete(key string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *bus) Get(key string) (json.RawMessage, bool, error) {
|
||||
v, ok := b.kept[key]
|
||||
if !ok {
|
||||
return nil, false, nil
|
||||
}
|
||||
raw, err := json.Marshal(v)
|
||||
return raw, true, err
|
||||
}
|
||||
|
||||
// existing is a server registered before registering was refused (novox/hq issue 340), as the state still holds it.
|
||||
func (b *bus) existing(key string, entry map[string]any) { _ = b.Put(key, entry) }
|
||||
|
||||
func (b *bus) Keys() ([]string, error) {
|
||||
var out []string
|
||||
for k := range b.kept {
|
||||
@@ -338,27 +350,15 @@ func (b *bus) join(p Paths, w map[string]string) *ServerView {
|
||||
|
||||
func noOthers() ([]string, error) { return nil, nil }
|
||||
|
||||
func TestRegisteringHerePutsItUnderThisNodesKeyAndAsksAboutTheOthers(t *testing.T) {
|
||||
p, w := node(t, "laptop")
|
||||
b := &bus{kept: map[string]map[string]any{}}
|
||||
v := b.join(p, w)
|
||||
r, err := RegisterServer(p, Registration{Name: "search", Entry: map[string]any{"type": "http", "url": "https://s.example/mcp"}}, b, v, writer(w),
|
||||
func() ([]string, error) { return []string{"laptop", "server", "desktop"}, nil })
|
||||
if err != nil || r["here"] != "changed" || !strings.Contains(r["also"].(string), "server, desktop") || b.kept["laptop.search"] == nil {
|
||||
t.Fatalf("%v %v %v", r, err, b.kept)
|
||||
}
|
||||
if !strings.Contains(w["managed-mcp.json"], `"search"`) {
|
||||
t.Fatal("not rendered")
|
||||
}
|
||||
}
|
||||
|
||||
// A server registered before stays (novox/hq issue 340): every node takes what the state holds for it, a node
|
||||
// that joins later reads the current set, and an unregistration reaches every node.
|
||||
func TestEveryNodeRegistrationReachesTheOthersAndALateNodeReadsIt(t *testing.T) {
|
||||
a, wa := node(t, "laptop")
|
||||
s, ws := node(t, "server")
|
||||
b := &bus{kept: map[string]map[string]any{}}
|
||||
va := b.join(a, wa)
|
||||
b.join(s, ws)
|
||||
_, _ = RegisterServer(a, Registration{Name: "docs", Entry: map[string]any{"type": "stdio", "command": "docs-mcp"}, Nodes: []string{"all"}}, b, va, writer(wa), noOthers)
|
||||
b.existing("all.docs", map[string]any{"type": "stdio", "command": "docs-mcp"})
|
||||
if Registered(s)["docs"] == nil || !strings.Contains(ws["managed-mcp.json"], "docs-mcp") {
|
||||
t.Fatalf("the other node did not take it: %v", Registered(s))
|
||||
}
|
||||
@@ -379,8 +379,8 @@ func TestANodesOwnRegistrationOverridesTheOneForEveryNode(t *testing.T) {
|
||||
b := &bus{kept: map[string]map[string]any{}}
|
||||
va := b.join(a, wa)
|
||||
b.join(s, ws)
|
||||
_, _ = RegisterServer(a, Registration{Name: "x", Entry: map[string]any{"type": "http", "url": "https://all"}, Nodes: []string{"all"}}, b, va, writer(wa), noOthers)
|
||||
_, _ = RegisterServer(a, Registration{Name: "x", Entry: map[string]any{"type": "http", "url": "https://laptop"}}, b, va, writer(wa), noOthers)
|
||||
b.existing("all.x", map[string]any{"type": "http", "url": "https://all"})
|
||||
b.existing("laptop.x", map[string]any{"type": "http", "url": "https://laptop"})
|
||||
if Registered(a)["x"]["url"] != "https://laptop" || Registered(s)["x"]["url"] != "https://all" {
|
||||
t.Fatalf("%v %v", Registered(a), Registered(s))
|
||||
}
|
||||
@@ -390,13 +390,13 @@ func TestANodesOwnRegistrationOverridesTheOneForEveryNode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestABadEntryIsRefusedBeforeAnythingIsPut(t *testing.T) {
|
||||
func TestABadEntryIsNeverTaken(t *testing.T) {
|
||||
p, w := node(t, "laptop")
|
||||
b := &bus{kept: map[string]map[string]any{}}
|
||||
v := b.join(p, w)
|
||||
r, _ := RegisterServer(p, Registration{Name: "mesh", Entry: map[string]any{"type": "http", "url": "https://x"}}, b, v, writer(w), noOthers)
|
||||
if r["registered"] != false || len(b.kept) != 0 {
|
||||
t.Fatalf("%v %v", r, b.kept)
|
||||
b.existing("laptop.mesh", map[string]any{"type": "http", "url": "https://x"})
|
||||
if Registered(p)["mesh"] != nil || strings.Contains(w["managed-mcp.json"], "https://x") {
|
||||
t.Fatalf("the mesh's own entry was replaced: %v", Registered(p))
|
||||
}
|
||||
if done, _ := OnServerChange(v, ServerChange{Key: "server.b", Op: "put", Value: map[string]any{"type": "http", "url": "https://b"}}, p, writer(w)); done != "" {
|
||||
t.Fatal("another node's key changed this one")
|
||||
|
||||
Reference in New Issue
Block a user