// pacman's tools bundle (novox/hq to-be 42 Phase 1, research 026/05): a process the node's runtime // launches and speaks MCP over stdio to, through the Go SDK (ADR 0188, ADR 0193). It reads the // package manager — search, info, what is installed and why, owners, files, updates, orphans, // foreign packages, history, mirrors, the configuration in force, the distribution's news — and acts // on it: a full upgrade, removing orphans, cleaning the cache, refreshing the mirrors. Acts go through // sudo -n, and a transaction runs as a unit of its own (acts.go says why). package main import ( "context" "fmt" "os" "regexp" stdio "git.novox.be/novox/mesh-sdk/go" ) // binaryName is what the build names this bundle's executable: the manifest's `binary`. const binaryName = "pacman-tools" func bg() context.Context { return context.Background() } func main() { // `pacman-tools keeper` is the module's process (keeper.go), `root` its root helper (root.go); without a // word it serves the tools. if len(os.Args) > 1 { switch os.Args[1] { case "keeper": os.Exit(keeper()) case "root": // the root helper (root.go, ADR 0510), and the command of a rollback's unit it starts os.Exit(rootHelper()) case "root-rollback": os.Exit(rootRollback(os.Args[2:])) } } // An empty name serves as the module the runtime names (MESH_SERVED_MODULE): pacman. if err := stdio.Serve("", tools(ThisMachine())); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } var ( pkgArg = map[string]any{"type": "string", "description": "a package's name"} limitArg = func(def, most int) map[string]any { return map[string]any{"type": "integer", "description": fmt.Sprintf("at most this many (default %d, at most %d)", def, most)} } sinceArg = map[string]any{"type": "string", "description": "a day, YYYY-MM-DD"} nodeArg = map[string]any{"type": "string", "description": "a node's name"} nodeName = regexp.MustCompile(`^[A-Za-z0-9_-]+$`) ) // nodeOf is the node argument, empty when absent; a node's name is one key token. func nodeOf(args map[string]any) (string, error) { n, err := text(args, "node", false) if err != nil { return "", err } if n != "" && !nodeName.MatchString(n) { return "", fmt.Errorf("node %q is not a node's name", n) } return n, nil } func tools(m *Machine) []stdio.Tool { return []stdio.Tool{ { Name: "pacman_search", Description: "Search the repositories (pacman -Ss): each package's repository, name, version, groups, whether it is installed and at which version, and its description.", Input: schema(map[string]any{"query": map[string]any{"type": "string", "description": "words, each a regular expression; all must match"}, "limit": limitArg(50, 500)}, "query"), Run: func(args map[string]any) (any, error) { q, err := text(args, "query", true) if err != nil { return nil, err } n, err := whole(args, "limit", 50, 1, 500) if err != nil { return nil, err } return m.Search(q, n) }, }, { Name: "pacman_info", Description: "One package's details (pacman -Qi, or -Si when it is not installed): version, description, dependencies, what requires it, sizes, dates, install reason; lists as lists.", Input: schema(map[string]any{"package": pkgArg}, "package"), Run: func(args map[string]any) (any, error) { p, err := text(args, "package", true) if err != nil { return nil, err } return m.Info(p) }, }, { Name: "pacman_installed", Description: "A node's installed packages, from the bus (ADR 0507 §2): version, why each is installed (explicit or dependency), its repository, " + "or foreign (in no repository); narrowed by name, reason or foreign; with totals, the time of the transaction they describe and whether they are current. " + "This node's by default. (r)", Input: schema(map[string]any{ "node": nodeArg, "match": map[string]any{"type": "string", "description": "only names holding this"}, "reason": map[string]any{"type": "string", "enum": []string{"explicit", "dependency"}}, "foreign": map[string]any{"type": "boolean", "description": "only foreign packages"}, "limit": limitArg(5000, 20000), }), Run: func(args map[string]any) (any, error) { node, err := nodeOf(args) if err != nil { return nil, err } match, err := text(args, "match", false) if err != nil { return nil, err } reason, err := text(args, "reason", false) if err != nil { return nil, err } if reason != "" && reason != "explicit" && reason != "dependency" { return nil, fmt.Errorf("reason is explicit or dependency") } foreign, err := flag(args, "foreign") if err != nil { return nil, err } n, err := whole(args, "limit", 5000, 1, 20000) if err != nil { return nil, err } return m.InstalledFrom(node, match, reason, foreign, n) }, }, { Name: "pacman_packages", Description: "Every node's packages at once, from the bus (ADR 0507 §6): with package, that package on every node (version, reason, repository " + "or foreign, the modules declaring it); without, per node its counts, the packages installed explicitly that no module assigned to it declares, " + "and its foreign packages apart, then the packages whose versions differ between nodes and the packages on some nodes only. " + "A node whose packages are over the bus's cap is marked not current. No machine is asked. (r)", Input: schema(map[string]any{ "package": map[string]any{"type": "string", "description": "one package, on every node"}, "limit": limitArg(500, 20000), }), Run: func(args map[string]any) (any, error) { p, err := text(args, "package", false) if err != nil { return nil, err } n, err := whole(args, "limit", 500, 1, 20000) if err != nil { return nil, err } return m.Packages(p, n) }, }, { Name: "pacman_owns", Description: "Which installed package owns a path (pacman -Qo); owned false when none does.", Input: schema(map[string]any{"path": map[string]any{"type": "string", "description": "an absolute path"}}, "path"), Run: func(args map[string]any) (any, error) { p, err := text(args, "path", true) if err != nil { return nil, err } return m.Owns(p) }, }, { Name: "pacman_files", Description: "The paths an installed package placed (pacman -Ql), bounded.", Input: schema(map[string]any{"package": pkgArg, "limit": limitArg(2000, 20000)}, "package"), Run: func(args map[string]any) (any, error) { p, err := text(args, "package", true) if err != nil { return nil, err } n, err := whole(args, "limit", 2000, 1, 20000) if err != nil { return nil, err } return m.Files(p, n) }, }, { Name: "pacman_updates", Description: "What a full upgrade would change, from the bus (ADR 0507 §4), on one node or on every node: each package from and to, whether the " + "node reached its mirrors (false: the list is the last it could confirm), and when the node's list last changed. The keeper reads it with " + "checkupdates at its start, after every transaction, after every mirror refresh and once a day. (r)", Input: schema(map[string]any{"node": nodeArg}), Run: func(args map[string]any) (any, error) { node, err := nodeOf(args) if err != nil { return nil, err } return m.UpdatesFor(node) }, }, { Name: "pacman_upgrade", Description: "Ask for a full system upgrade of a node (this one unless named): the ask is written to the bus, where that node's keeper " + "takes it and runs it as a run: the package record, the kernel check, the transaction as a unit of its own, and the after-upgrade " + "check (ADR 0508 §8). Answers at once with the ask and the distribution's news since the last upgrade — read the news first; " + "pacman_upgrades with the node shows the run that names the ask. Given a transaction unit, answers whether it is running, " + "finished and succeeded, with the tail of its log.", Input: schema(map[string]any{ "node": map[string]any{"type": "string", "description": "the node to upgrade (default: this one)"}, "unit": map[string]any{"type": "string", "description": "a transaction unit, to read how it goes (optional)"}, "lines": limitArg(60, 400), }), Run: func(args map[string]any) (any, error) { unit, err := text(args, "unit", false) if err != nil { return nil, err } n, err := whole(args, "lines", 60, 1, 400) if err != nil { return nil, err } if unit != "" { return m.UpgradeStatus(unit, n) } node, err := text(args, "node", false) if err != nil { return nil, err } if node == "" { node = thisNode() } a, err := AskUpgrade(upgradeState(StateAsks), node, m.Now()) if err != nil { return nil, err } return map[string]any{"asked": a, "follow": "pacman_upgrades with node " + node + " lists the run that names this ask", "news": m.News("")}, nil }, }, { Name: "pacman_upgrade_health", Description: "This node's upgrades, read from the bus, as the module's health check (ADR 0508 §11): unhealthy while no upgrade-order " + "is set, while the node's latest run is refused, failed or did not pass its after-upgrade check, hung past two hours, or held a " + "kernel, and while the week's round stands still at this node or at the node it waits for, naming it and since when. (r)", Input: schema(map[string]any{}), Run: func(map[string]any) (any, error) { return m.UpgradeHealth(), nil }, }, { Name: "pacman_upgrades", Description: "Every full upgrade and rollback of every node, from the bus (ADR 0508 §1): each run's node, kind, what started it, " + "its round, when it began and ended, its outcome (refused, finished, failed or running) and whether its after-upgrade check passed; " + "narrowed to a node and to days, newest first. With a node and a run, that run whole, every step with its time.", Input: schema(map[string]any{ "node": map[string]any{"type": "string", "description": "only this node's runs"}, "from": sinceArg, "until": map[string]any{"type": "string", "description": "a day, YYYY-MM-DD, the last one listed"}, "run": map[string]any{"type": "string", "description": "one run, by its id, with node"}, "limit": limitArg(50, 1000), }), Run: func(args map[string]any) (any, error) { var v [4]string for i, k := range []string{"node", "from", "until", "run"} { s, err := text(args, k, false) if err != nil { return nil, err } v[i] = s } n, err := whole(args, "limit", 50, 1, 1000) if err != nil { return nil, err } return ListUpgrades(upgradeState(StateRuns), v[0], v[1], v[2], v[3], n) }, }, { Name: "pacman_rollback", Description: "Return a node to the package record a run took (ADR 0508 §7). Without apply it says what that would change: each package " + "from its version now to the record's and where that version comes from (the node's package cache, the Arch Linux Archive's copy " + "of that exact version, or the archive's repository on the record's date), the packages installed since (removed only with " + "remove_added), and no foreign package. With apply it asks the node's keeper to carry it out as a run of its own, followed by " + "the after-upgrade check; pacman_upgrades follows it. This node's by default.", Input: schema(map[string]any{ "node": nodeArg, "run": map[string]any{"type": "string", "description": "the run whose record to return to (pacman_upgrades lists them)"}, "remove_added": map[string]any{"type": "boolean", "description": "also remove the packages installed since the record"}, "apply": map[string]any{"type": "boolean", "description": "carry it out"}, }, "run"), Run: func(args map[string]any) (any, error) { node, err := nodeOf(args) if err != nil { return nil, err } run, err := text(args, "run", true) if err != nil { return nil, err } if !nodeName.MatchString(run) { return nil, fmt.Errorf("run %q is not a run's id", run) } remove, err := flag(args, "remove_added") if err != nil { return nil, err } apply, err := flag(args, "apply") if err != nil { return nil, err } return m.Rollback(node, run, remove, apply) }, }, { Name: "pacman_round", Description: "Where the week's upgrade rounds stand (ADR 0508 §5, §6): each round's order and opening, and every node's state in it: " + "waiting (for the node named), turn, stalled (its turn came longer than round-start-minutes ago), running, passed, failed " + "(the round stops there) or refused, with since when. With pass, a node and a reason, asks the operator on their channel to let " + "the round go on past that node; it goes on only on the operator's Approve.", Input: schema(map[string]any{ "round": map[string]any{"type": "string", "description": "one round, as 2026-W43 (default: the newest)"}, "pass": map[string]any{"type": "string", "description": "a node to ask the operator to pass"}, "reason": map[string]any{"type": "string", "description": "why the node should be passed: required with pass, and shown to the operator"}, "limit": limitArg(4, 52), }), Run: func(args map[string]any) (any, error) { var v [3]string for i, k := range []string{"round", "pass", "reason"} { s, err := text(args, k, false) if err != nil { return nil, err } v[i] = s } if v[1] != "" { return AskPass(upgradeState(StateRounds), upgradeState(StateRuns), upgradeState(StateAsks), v[0], v[1], v[2], m.Now()) } n, err := whole(args, "limit", 4, 1, 52) if err != nil { return nil, err } return ListRounds(upgradeState(StateRounds), upgradeState(StateRuns), v[0], m.Now(), roundStartMinutes(), n) }, }, { Name: "pacman_orphans", Description: "Packages installed as dependencies that nothing requires any more (pacman -Qdt), with versions.", Input: schema(map[string]any{}), Run: func(map[string]any) (any, error) { return m.Orphans() }, }, { Name: "pacman_remove_orphans", Description: "Remove orphans (pacman -Rs, through sudo -n, as a unit of its own): the names given, each of which must be an orphan, " + "or every orphan with all: true. Changed configuration files are kept as .pacsave. Answers what was removed and the log.", Input: schema(map[string]any{ "names": map[string]any{"type": "array", "items": map[string]any{"type": "string"}, "description": "orphans to remove"}, "all": map[string]any{"type": "boolean", "description": "remove every orphan"}, }), Run: func(args map[string]any) (any, error) { all, err := flag(args, "all") if err != nil { return nil, err } var names []string if raw, ok := args["names"].([]any); ok { for i := range raw { n, err := text(map[string]any{"name": raw[i]}, "name", true) if err != nil { return nil, err } names = append(names, n) } } return m.RemoveOrphans(names, all) }, }, { Name: "pacman_cache", Description: "The package cache: files, bytes, interrupted downloads left behind, what paccache would remove keeping the last " + "keep versions of each package (or only packages no longer installed), and the paccache timer. With clean: true it removes them (sudo -n).", Input: schema(map[string]any{ "keep": map[string]any{"type": "integer", "description": "versions of each package to keep (default 3)"}, "uninstalled": map[string]any{"type": "boolean", "description": "only packages no longer installed"}, "clean": map[string]any{"type": "boolean", "description": "remove them, rather than say what would go"}, }), Run: func(args map[string]any) (any, error) { keep, err := whole(args, "keep", 3, 0, 100) if err != nil { return nil, err } un, err := flag(args, "uninstalled") if err != nil { return nil, err } clean, err := flag(args, "clean") if err != nil { return nil, err } return m.Cache(keep, un, clean) }, }, { Name: "pacman_history", Description: "What the package manager did, from /var/log/pacman.log: each install, upgrade, downgrade, reinstall and removal since a day (default thirty days back), narrowed to an action or a name, with counts and the last full upgrade.", Input: schema(map[string]any{ "since": sinceArg, "action": map[string]any{"type": "string", "enum": Actions}, "match": map[string]any{"type": "string", "description": "only packages whose name holds this"}, "limit": limitArg(500, 5000), }), Run: func(args map[string]any) (any, error) { since, err := text(args, "since", false) if err != nil { return nil, err } action, err := text(args, "action", false) if err != nil { return nil, err } match, err := text(args, "match", false) if err != nil { return nil, err } n, err := whole(args, "limit", 500, 1, 5000) if err != nil { return nil, err } return m.History(since, action, match, n) }, }, { Name: "pacman_mirrors", Description: "The mirror list in force (servers, commented ones, who generated it and when), reflector's options, its timer and its last run. With refresh: true, start reflector now (sudo -n), without waiting.", Input: schema(map[string]any{"refresh": map[string]any{"type": "boolean", "description": "rank and rewrite the list now"}}), Run: func(args map[string]any) (any, error) { refresh, err := flag(args, "refresh") if err != nil { return nil, err } return m.MirrorList(refresh) }, }, { Name: "pacman_foreign", Description: "A node's packages that no repository it syncs carries, from the bus: built from the AUR or by hand, which the mesh neither " + "upgrades nor removes (ADR 0507 §7). This node's by default. (r)", Input: schema(map[string]any{"node": nodeArg}), Run: func(args map[string]any) (any, error) { node, err := nodeOf(args) if err != nil { return nil, err } return m.ForeignFrom(node) }, }, { Name: "pacman_news", Description: "The distribution's news posts since a day, or since the last full upgrade by default — what an upgrade may need a person to do. Fetched over https; no network is answered as reachable: false.", Input: schema(map[string]any{"since": sinceArg}), Run: func(args map[string]any) (any, error) { since, err := text(args, "since", false) if err != nil { return nil, err } return m.News(since), nil }, }, { Name: "pacman_config", Description: "The configuration pacman runs with, as pacman-conf resolves it: every option, each repository with its signature level and how many servers, and whether /etc/pacman.conf is the module's.", Input: schema(map[string]any{}), Run: func(map[string]any) (any, error) { return m.Conf() }, }, } }