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:
@@ -126,7 +126,7 @@ func RenderNow(p Paths, write WriteManaged) ([]string, error) {
|
||||
}
|
||||
var items map[string]Item
|
||||
_ = readJSON(p.config(), &items)
|
||||
config := ConfigOf(items)
|
||||
config := ConfigOf(Taken(items))
|
||||
mesh := ReadMeshTools(p)
|
||||
if mesh != nil {
|
||||
mesh.Refused = RefusedCounts(GuardDataOf(p, mesh).Log)
|
||||
@@ -446,6 +446,7 @@ type ServerState interface {
|
||||
Put(key string, value any) error
|
||||
Delete(key string) error
|
||||
Keys() ([]string, error)
|
||||
Get(key string) (json.RawMessage, bool, error)
|
||||
}
|
||||
|
||||
// ServerChange is one change to the `servers` state, as a watch hands it over.
|
||||
@@ -528,15 +529,22 @@ func OnServerChange(v *ServerView, c ServerChange, p Paths, write WriteManaged)
|
||||
return what + " " + c.Key, nil
|
||||
}
|
||||
|
||||
// RegisterServer registers (or, with no entry, unregisters) a server: a put (or delete) per scope in the
|
||||
// `servers` state, taken into this node's view at once so the answer says what it did here; every other
|
||||
// node takes it from its watch, and a node that joins later from the current state.
|
||||
// ServerAtTheTerminal is why a tool server is never registered through this module (novox/hq issue 340): every
|
||||
// session on the nodes it reaches starts it, a stdio server as a program run as whoever started the session, and
|
||||
// an http one is handed what the session asks — and the tool runner is not told who called it.
|
||||
const ServerAtTheTerminal = "a tool server is started, or handed what the agent asks it, by every session on the nodes it " +
|
||||
"reaches, as whoever starts that session — the operator, whose account holds root — and the tool runner is not " +
|
||||
"told who called it, so an agent on another node could register one. It is set at the controller's terminal " +
|
||||
"only, in this module's mcp_servers setting: mesh-controller settings set claude-code '{\"mcp_servers\": {...}}' " +
|
||||
"[--node <node>] (novox/hq issue 340). Nothing was registered"
|
||||
|
||||
// RegisterServer unregisters a server: a delete per scope in the `servers` state, taken into this node's view at
|
||||
// once so the answer says what it did here; every other node takes it from its watch. A registration — an entry
|
||||
// given — is refused (ServerAtTheTerminal): what is in the state from before stays, and is only ever removed here.
|
||||
func RegisterServer(p Paths, r Registration, servers ServerState, v *ServerView, write WriteManaged,
|
||||
others func() ([]string, error)) (map[string]any, error) {
|
||||
if r.Entry != nil {
|
||||
if problem := EntryProblem(r.Name, r.Entry); problem != "" {
|
||||
return map[string]any{"registered": false, "reason": problem}, nil
|
||||
}
|
||||
return nil, errors.New(ServerAtTheTerminal)
|
||||
}
|
||||
scopes := r.Nodes
|
||||
if len(scopes) == 0 {
|
||||
@@ -547,20 +555,10 @@ func RegisterServer(p Paths, r Registration, servers ServerState, v *ServerView,
|
||||
before, _ := json.Marshal(v.Effective())
|
||||
for _, scope := range scopes {
|
||||
key := KeyOf(scope, r.Name)
|
||||
var err error
|
||||
if r.Entry != nil {
|
||||
err = servers.Put(key, r.Entry)
|
||||
} else {
|
||||
err = servers.Delete(key)
|
||||
}
|
||||
if err != nil {
|
||||
if err := servers.Delete(key); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
op := "put"
|
||||
if r.Entry == nil {
|
||||
op = "delete"
|
||||
}
|
||||
v.Take(ServerChange{Key: key, Op: op, Value: r.Entry})
|
||||
v.Take(ServerChange{Key: key, Op: "delete"})
|
||||
}
|
||||
after, _ := json.Marshal(v.Effective())
|
||||
changed := string(before) != string(after)
|
||||
@@ -568,11 +566,7 @@ func RegisterServer(p Paths, r Registration, servers ServerState, v *ServerView,
|
||||
for _, s := range scopes {
|
||||
here = here || s == "all" || s == p.Node
|
||||
}
|
||||
verb := "registered"
|
||||
if r.Entry == nil {
|
||||
verb = "unregistered"
|
||||
}
|
||||
answer := map[string]any{verb: r.Name, "on": scopes}
|
||||
answer := map[string]any{"unregistered": r.Name, "on": scopes}
|
||||
switch {
|
||||
case !here:
|
||||
answer["here"] = "not this node"
|
||||
@@ -588,10 +582,8 @@ func RegisterServer(p Paths, r Registration, servers ServerState, v *ServerView,
|
||||
}
|
||||
answer["rendered"] = rendered
|
||||
}
|
||||
if r.Entry == nil {
|
||||
if _, still := v.Effective()[r.Name]; still {
|
||||
answer["still"] = r.Name + " still applies here from another registration (for every node, or for this one); unregister that too"
|
||||
}
|
||||
if _, still := v.Effective()[r.Name]; still {
|
||||
answer["still"] = r.Name + " still applies here from another registration (for every node, or for this one); unregister that too"
|
||||
}
|
||||
if len(r.Nodes) == 0 {
|
||||
// The question the operator wanted asked: here only, or more?
|
||||
@@ -604,8 +596,8 @@ func RegisterServer(p Paths, r Registration, servers ServerState, v *ServerView,
|
||||
}
|
||||
}
|
||||
if len(elsewhere) > 0 {
|
||||
answer["also"] = fmt.Sprintf("claude-code also runs on %s. To %s it there too, call again with nodes: \"all\" or a list of those nodes.",
|
||||
strings.Join(elsewhere, ", "), map[bool]string{true: "register", false: "unregister"}[r.Entry != nil])
|
||||
answer["also"] = fmt.Sprintf("claude-code also runs on %s. To unregister it there too, call again with nodes: \"all\" or a list of those nodes.",
|
||||
strings.Join(elsewhere, ", "))
|
||||
} else {
|
||||
answer["also"] = "To do the same on every node running claude-code, call again with nodes: \"all\"."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user