// docker's Go tools bundle (novox/hq ADR 0188, ADR 0193): a process the node's tool runtime launches // and speaks MCP over stdio to, through the Go SDK. It answers for every container on this machine — // the mesh's and every other — and for the runtime's images, networks, volumes, events and // configuration. It runs as the operator account (ADR 0175 §4); docker.go says how it reaches the // daemon's socket. The host applies the module's resources; these tools answer about the runtime. 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): docker. if err := stdio.Serve("", tools(NewClient())); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } var containerArg = map[string]any{"type": "string", "description": "the container's name or id"} func tools(c *Client) []stdio.Tool { ctx := context.Background() act := func(verb, description string) stdio.Tool { return stdio.Tool{ Name: "docker_" + verb, Description: description, Input: map[string]any{"container": containerArg}, Run: func(args map[string]any) (any, error) { ref, err := text(args, "container") if err != nil { return nil, err } return c.Act(ctx, verb, ref) }, } } return []stdio.Tool{ { Name: "docker_list", Description: "Every container on this machine — the mesh's and every other — with its image, state, health, restarts, " + "published ports, mounts, compose project, and mesh_held/held_by (the assignment that holds it).", Input: map[string]any{ "held": map[string]any{"type": "string", "enum": []string{"all", "mesh", "other"}, "description": "whose: all (default), the mesh's, or the others"}, "state": map[string]any{"type": "string", "description": "only containers in this state (running, exited, created, restarting, paused, dead)"}, "match": map[string]any{"type": "string", "description": "only containers whose name or image contains this"}, }, Run: func(args map[string]any) (any, error) { list, err := c.Containers(ctx, optional(args, "held"), optional(args, "state"), optional(args, "match")) if err != nil { return nil, err } return map[string]any{"count": len(list), "containers": list}, nil }, }, { Name: "docker_inspect", Description: "One container whole, as docker inspects it, with mesh_held; its environment's values are left out (names kept), because that is where a container's secrets are.", Input: map[string]any{"container": containerArg}, Run: func(args map[string]any) (any, error) { ref, err := text(args, "container") if err != nil { return nil, err } return c.Inspect(ctx, ref) }, }, { Name: "docker_logs", Description: "The last lines one container wrote, both streams merged in order, each with its timestamp (default 200, at most 2000 lines; a line is cut at 4 KiB).", Input: map[string]any{ "container": containerArg, "lines": map[string]any{"type": "integer", "description": "how many lines from the end (default 200, at most 2000)"}, "since": map[string]any{"type": "string", "description": "only lines since then: a duration such as 30m or 2h, or a time"}, }, Run: func(args map[string]any) (any, error) { ref, err := text(args, "container") if err != nil { return nil, err } n, err := bounded(args, "lines", 200, 2000) if err != nil { return nil, err } return c.Logs(ctx, ref, n, optional(args, "since")) }, }, { Name: "docker_stats", Description: "What the running containers use now — CPU, memory, network and disk I/O, processes — the heaviest by memory first; or one container's.", Input: map[string]any{"container": map[string]any{"type": "string", "description": "one container (optional)"}}, Run: func(args map[string]any) (any, error) { stats, err := c.Stats(ctx, optional(args, "container")) if err != nil { return nil, err } return map[string]any{"count": len(stats), "containers": stats}, nil }, }, act("start", "Start one container. A container the mesh holds is started too, and the answer says the host restores what its declaration says at its next apply."), act("stop", "Stop one container (ten seconds, then killed). For a container the mesh holds, the answer says the host will start it again at its next apply if its declaration says running."), act("restart", "Restart one container (ten seconds to stop, then killed); the answer says whether the mesh holds it."), { Name: "docker_top", Description: "The processes running inside one container: pid, user, elapsed time, CPU, resident memory and command.", Input: map[string]any{"container": containerArg}, Run: func(args map[string]any) (any, error) { ref, err := text(args, "container") if err != nil { return nil, err } return c.Top(ctx, ref) }, }, { Name: "docker_images", Description: "The images on this machine, the largest first, each with its size and the containers using it (and whether one of them is the mesh's). " + "filter: all, dangling, unused or used.", Input: map[string]any{ "filter": map[string]any{"type": "string", "enum": []string{"all", "dangling", "unused", "used"}, "description": "which images (default all)"}, "match": map[string]any{"type": "string", "description": "only images whose repository:tag contains this"}, "limit": map[string]any{"type": "integer", "description": "how many to show (default 100, at most 1000); count says how many matched"}, }, Run: func(args map[string]any) (any, error) { n, err := bounded(args, "limit", 100, 1000) if err != nil { return nil, err } return c.Images(ctx, optional(args, "filter"), optional(args, "match"), n) }, }, { Name: "docker_prune", Description: "Reclaim space: dangling images and unused build cache, and — only when containers is true — stopped containers the mesh does not hold. " + "Never a volume, never a container the mesh holds, never an image a container uses. A dry run by default: it lists what would go; dry_run false removes it.", Input: map[string]any{ "dry_run": map[string]any{"type": "boolean", "description": "list only (default true)"}, "images": map[string]any{"type": "boolean", "description": "dangling images (default true)"}, "build_cache": map[string]any{"type": "boolean", "description": "build cache nothing refers to (default true)"}, "containers": map[string]any{"type": "boolean", "description": "stopped containers the mesh does not hold (default false); what they mounted is kept"}, "older_than_hours": map[string]any{"type": "integer", "description": "only what is older than this many hours (default 0: any age)"}, }, Run: func(args map[string]any) (any, error) { older := 0 if v, ok := args["older_than_hours"]; ok && v != nil && v != float64(0) { n, err := bounded(args, "older_than_hours", 0, 24*365) if err != nil { return nil, err } older = n } return c.Prune(ctx, PruneAsk{ DryRun: flag(args, "dry_run", true), Images: flag(args, "images", true), BuildCache: flag(args, "build_cache", true), Containers: flag(args, "containers", false), OlderThanH: older, }) }, }, { Name: "docker_disk_usage", Description: "What the runtime takes on disk (docker system df -v): per kind — images, containers, volumes, build cache — the total, the active and the reclaimable, and the largest of each.", Input: map[string]any{"top": map[string]any{"type": "integer", "description": "how many of the largest per kind (default 10, at most 100)"}}, Run: func(args map[string]any) (any, error) { n, err := bounded(args, "top", 10, 100) if err != nil { return nil, err } return c.DiskUsage(ctx, n) }, }, { Name: "docker_networks", Description: "Every network the runtime has: driver, scope, subnets and gateway, and the running containers on it with their addresses and whether the mesh holds them.", Run: func(map[string]any) (any, error) { return c.Networks(ctx) }, }, { Name: "docker_volumes", Description: "Every volume with the containers mounting it, whether the mesh holds any of them, whether it is anonymous, its compose project, and — when sizes is true (slower) — its size.", Input: map[string]any{ "unmounted": map[string]any{"type": "boolean", "description": "only volumes no container mounts (default false)"}, "sizes": map[string]any{"type": "boolean", "description": "measure each volume (default false: it walks every volume)"}, }, Run: func(args map[string]any) (any, error) { return c.Volumes(ctx, flag(args, "unmounted", false), flag(args, "sizes", false)) }, }, { Name: "docker_events", Description: "What the runtime did in a window ending now (default the last 60 minutes, at most 24 hours): containers created, started, died, health changes, images pulled — with mesh_held. Exec events are left out unless asked.", Input: map[string]any{ "minutes": map[string]any{"type": "integer", "description": "how far back (default 60, at most 1440)"}, "type": map[string]any{"type": "string", "description": "only one kind: container, image, network, volume, daemon, plugin or builder"}, "limit": map[string]any{"type": "integer", "description": "the latest this many (default 200, at most 2000)"}, "execs": map[string]any{"type": "boolean", "description": "include exec_* events (default false: health checks make many)"}, }, Run: func(args map[string]any) (any, error) { minutes, err := bounded(args, "minutes", 60, 1440) if err != nil { return nil, err } limit, err := bounded(args, "limit", 200, 2000) if err != nil { return nil, err } return c.Events(ctx, minutes, optional(args, "type"), limit, flag(args, "execs", false)) }, }, { Name: "docker_daemon_config", Description: "The runtime's configuration: /etc/docker/daemon.json as it is on disk, the daemon's essentials as it runs now (docker info: version, storage and logging drivers, " + "live restore, root directory, insecure registries, warnings), and where the two differ — keys a reload or only a restart would take.", Run: func(map[string]any) (any, error) { return c.DaemonConfig(ctx) }, }, { Name: "docker_unlabelled", Description: "The containers the mesh does not hold — the cleanup list — each with its image, state, compose project and directory, ports and mounts.", Run: func(map[string]any) (any, error) { return c.Unlabelled(ctx) }, }, { Name: "docker_problems", Description: "Every container that is not well: unhealthy, restarting, dead, killed for memory, exited with a failure, or restarted five times or more — with whether the mesh holds it.", Run: func(map[string]any) (any, error) { p, err := c.Problems(ctx) if err != nil { return nil, err } return map[string]any{"count": len(p), "containers": p}, nil }, }, { Name: "docker_ports", Description: "Every port the containers publish on this machine (address:port -> container port), and the containers on the host's network, which publish whatever they listen on.", Run: func(map[string]any) (any, error) { p, err := c.Ports(ctx) if err != nil { return nil, err } return map[string]any{"count": len(p), "ports": p}, nil }, }, } } 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 optional(args map[string]any, key string) string { s, _ := args[key].(string) return strings.TrimSpace(s) } func flag(args map[string]any, key string, def bool) bool { if b, ok := args[key].(bool); ok { return b } return def } // bounded is a whole number argument, defaulted when absent and held to a ceiling. func bounded(args map[string]any, key string, def, most int) (int, error) { v, ok := args[key] if !ok || v == nil { return def, nil } f, ok := v.(float64) if !ok || f != math.Trunc(f) || f < 1 { return 0, fmt.Errorf("%s must be a whole number of at least 1", key) } return int(math.Min(f, float64(most))), nil }