The review of 2026-10-09: an unknown account and sudo that could not be asked were answered not root, and only NOPASSWD on ALL counted, though NOPASSWD on systemctl, tee, bash or pacman reaches root as surely, and a password the person typed is kept by sudo for minutes. Any rule now counts, as the node-engine's own judge of an agent account does (ADR 0266); what cannot be judged fails the answer. The bundle built in the module's directory is removed and ignored.
78 lines
3.3 KiB
Go
78 lines
3.3 KiB
Go
// sudo'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 answers what
|
|
// sudo grants the operator account and whether the passwordless escalation every module's acting
|
|
// tools rely on works here. It changes nothing: the grant itself is the module's drop-in, which the
|
|
// host writes.
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
// binaryName is what the build names this bundle's executable: the manifest's `binary`.
|
|
const binaryName = "sudo-tools"
|
|
|
|
func bg() context.Context { return context.Background() }
|
|
|
|
func main() {
|
|
// An empty name serves as the module the runtime names (MESH_SERVED_MODULE): sudo.
|
|
if err := stdio.Serve("", tools(ThisMachine())); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
func tools(m *Machine) []stdio.Tool {
|
|
return []stdio.Tool{
|
|
{
|
|
Name: "sudo_rules",
|
|
Description: "What the runtime's account may run through sudo on this machine, as `sudo -n -l` says it: " +
|
|
"the defaults in force and each rule with its run-as, tags (NOPASSWD …) and commands, and whether one " +
|
|
"lets it run everything as root without a prompt. An error when sudo itself asks for a password.",
|
|
Input: schema(map[string]any{}),
|
|
Run: func(map[string]any) (any, error) { return m.ListRules() },
|
|
},
|
|
{
|
|
Name: "sudo_check",
|
|
Description: "Does the passwordless escalation the mesh's acting tools rely on work here, and which file grants it: " +
|
|
"every rule in /etc/sudoers and its drop-ins naming the operator account, one of its groups or ALL, in " +
|
|
"the order sudo reads them, the one that decides, and whether the module's own drop-in is present.",
|
|
Input: schema(map[string]any{}),
|
|
Run: func(map[string]any) (any, error) { return m.CheckEscalation() },
|
|
},
|
|
{
|
|
Name: "sudo_escalation",
|
|
Description: "Can these accounts become root on this machine without a person: any sudo rule for it " +
|
|
"(with or without a password, for any command), or a group that is root by another name (docker, disk, " +
|
|
"root, lxd, incus-admin, libvirt). Each account with yes or no and why; an account that cannot be " +
|
|
"judged fails the answer, never a no. Read as the runtime's account: a reading for a person — the " +
|
|
"agent account's verdict is the node-engine's (novox/hq ADR 0259 §8, ADR 0266).",
|
|
Input: schema(map[string]any{"accounts": map[string]any{"type": "array", "items": map[string]any{"type": "string"},
|
|
"description": "the accounts to judge"}}),
|
|
Run: func(a map[string]any) (any, error) {
|
|
var accounts []string
|
|
if list, ok := a["accounts"].([]any); ok {
|
|
for _, x := range list {
|
|
if s, ok := x.(string); ok {
|
|
accounts = append(accounts, s)
|
|
}
|
|
}
|
|
}
|
|
return m.Escalations(accounts)
|
|
},
|
|
},
|
|
{
|
|
Name: "sudo_drop_ins",
|
|
Description: "The files of /etc/sudoers.d with owner, mode and size, whether sudo reads each (a name with a dot " +
|
|
"or ending in ~, another owner or a group- or world-writable mode is skipped), whether each parses " +
|
|
"(visudo -cf), and whether sudo's rules as a whole parse. A file that does not parse locks sudo for everyone.",
|
|
Input: schema(map[string]any{}),
|
|
Run: func(map[string]any) (any, error) { return m.ListDropIns() },
|
|
},
|
|
}
|
|
}
|