// ssh-client's Go tools bundle (novox/hq ADR 0188, ADR 0193; research 027/03): a process the node's // tool runtime launches and speaks MCP over stdio to, through the Go SDK. It answers for the // operator account's ~/.ssh — its hosts and where each came from, its keys, who may log in, the // hosts it knows — and changes two things on request: one authorized key revoked, one known host // refreshed. It runs as the operator account (ADR 0175 §4) and never reads a private key. package main import ( "context" "fmt" "math" "os" "strings" stdio "git.novox.be/novox/mesh-sdk/go" ) func main() { // An empty name serves as the module the runtime names (MESH_SERVED_MODULE): ssh-client. if err := stdio.Serve("", tools(NewClient())); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } var hostArg = map[string]any{"type": "string", "description": "the host, as you would give it to ssh"} func tools(c *Client) []stdio.Tool { ctx := context.Background() return []stdio.Tool{ { Name: "ssh_client_hosts", Description: "Every Host and Match section ssh reads for this account, in the order it reads them (an earlier one wins), each with its file and line " + "and where it came from: mesh (the mesh's own file or region), drop-in (another file in ~/.ssh/config.d) or operator; with duplicates and what is set outside any section.", Run: func(map[string]any) (any, error) { return c.Hosts() }, }, { Name: "ssh_client_resolve", Description: "What ssh would use for one host (ssh -G): host name, user, port, identity files, proxy, host-key checking — and which sections matched it.", Input: map[string]any{"host": hostArg}, Run: func(args map[string]any) (any, error) { h, err := text(args, "host") if err != nil { return nil, err } return c.Resolve(ctx, h) }, }, { Name: "ssh_client_check", Description: "Everything about ~/.ssh worth a look: modes of the directory and every file, private keys with no passphrase or weak or old, the mesh's region first with its include, " + "duplicate hosts, known_hosts entries for the mesh's machines that are missing or stale (scanned live unless scan is false), authorized keys without a comment, and debris. Says what it did not check.", Input: map[string]any{"scan": map[string]any{"type": "boolean", "description": "ask each of the mesh's machines for its host key now (default true; 5 s each, in parallel)"}}, Run: func(args map[string]any) (any, error) { return c.Check(ctx, flag(args, "scan", true)) }, }, { Name: "ssh_client_keys", Description: "Every private key under ~/.ssh by its public half: type, size, fingerprint, comment, mode, age, whether it has a passphrase, and whether ssh offers it by itself. The key itself is never read.", Run: func(map[string]any) (any, error) { k, err := c.Keys(ctx) if err != nil { return nil, err } return map[string]any{"count": len(k), "keys": k}, nil }, }, { Name: "ssh_client_authorized", Description: "Who may log in as this account by key: each line of authorized_keys by fingerprint, type, size, comment and options — never the key itself — with duplicates and unreadable lines.", Run: func(map[string]any) (any, error) { return c.Authorized() }, }, { Name: "ssh_client_revoke", Description: "Take one key out of authorized_keys by its fingerprint (every line carrying it), keeping the file as it was beside it first. " + "Refuses the last key (that would lock ssh out) and a key in the mesh's region (the next push would write it back).", Input: map[string]any{"fingerprint": map[string]any{"type": "string", "description": "SHA256:… as ssh_client_authorized lists it"}}, Run: func(args map[string]any) (any, error) { fp, err := text(args, "fingerprint") if err != nil { return nil, err } return c.Revoke(fp) }, }, { Name: "ssh_client_known_host", Description: "What known_hosts holds for one host and what the host offers now, by fingerprint: matches, changed, not known or unreachable. With refresh true, the host's entries are replaced " + "by what it offers now (ssh-keygen keeps known_hosts.old) — which trusts whatever answers, so only for a host whose key is known to have changed.", Input: map[string]any{ "host": hostArg, "port": map[string]any{"type": "integer", "description": "the ssh port (default 22)"}, "refresh": map[string]any{"type": "boolean", "description": "replace its entries with what it offers now (default false)"}, }, Run: func(args map[string]any) (any, error) { h, err := text(args, "host") if err != nil { return nil, err } port := 22 if v, ok := args["port"].(float64); ok { if v != math.Trunc(v) { return nil, fmt.Errorf("port must be a whole number") } port = int(v) } return c.KnownHost(ctx, h, port, flag(args, "refresh", false)) }, }, { Name: "ssh_client_test", Description: "Can this machine reach a host and log in: one batch-mode connection (no prompt, runs only `true`), answering the address reached, the method and key that authenticated, " + "or why it failed and which keys were offered. A key with a passphrase works only through an agent; the answer names the agent it used, or that there was none.", Input: map[string]any{"host": hostArg}, Run: func(args map[string]any) (any, error) { h, err := text(args, "host") if err != nil { return nil, err } return c.Test(ctx, h) }, }, } } func text(args map[string]any, key string) (string, error) { s, _ := args[key].(string) if s = strings.TrimSpace(s); s == "" { return "", fmt.Errorf("%s is required", key) } return s, nil } func flag(args map[string]any, key string, def bool) bool { if b, ok := args[key].(bool); ok { return b } return def }