mesh/delivery-group group feat/journal-window-on-the-seat delivering: 0 of 2 delivered
mesh/merge-gate pass: builds systemd → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery held for a person: its group feat/journal-window-on-the-seat's composed check did not pass for the heads that merged; a person decides that…
An incident is read for the minutes it happened in (the operator's direction 2026-10-07): journal takes since, until, priority and a fixed-string match. Every value is one word of journalctl's argv, held to the forms journalctl reads, so nothing reaches a shell or is read as an option under sudo. What the unit printed of a secret is redacted, as docker_logs does, before the match is applied, so a match cannot find one. systemd_failed becomes the seat's failed, with an optional scope. Needs the controller's seat with these verbs (mesh-controller, same branch): an older controller refuses a claim serving a verb its seat does not promise.
169 lines
6.1 KiB
Go
169 lines
6.1 KiB
Go
// systemd's tools: the node-service-manager seat's nine verbs — the units on this machine in both scopes,
|
|
// read and acted on by name, their journal, and what has failed (novox/hq ADR 0177). The node
|
|
// tools runtime launches this bundle as a process of its own and serves what it serves (ADR 0188, ADR 0193);
|
|
// it runs as the operator account, so acts on the system manager, and reads of its journal, escalate with
|
|
// sudo -n, and the user scope is the account's own manager (client.go). The host applies units; this answers
|
|
// about them. stdout is the MCP channel; this says nothing else.
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/user"
|
|
"strconv"
|
|
"strings"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
const seat = "node-service-manager"
|
|
|
|
func str(description string) map[string]any {
|
|
return map[string]any{"type": "string", "description": description}
|
|
}
|
|
|
|
var (
|
|
scopeArg = str(`"system" (the default) or "user": the operator account's own manager`)
|
|
unitArgS = str("the unit's name, as the service manager knows it")
|
|
)
|
|
|
|
func scopeOf(a map[string]any) (Scope, error) {
|
|
s, _ := a["scope"].(string)
|
|
switch s {
|
|
case "", "system":
|
|
return System, nil
|
|
case "user":
|
|
return User, nil
|
|
}
|
|
return "", fmt.Errorf("scope %q: \"system\" or \"user\"", s)
|
|
}
|
|
|
|
func unitOf(a map[string]any) (string, error) {
|
|
u, _ := a["unit"].(string)
|
|
if u = strings.TrimSpace(u); u == "" {
|
|
return "", fmt.Errorf("a unit is required")
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
func tools(m *Manager) []stdio.Tool {
|
|
act := func(verb, description string) stdio.Tool {
|
|
return stdio.Tool{Name: seat + "." + verb, Description: description,
|
|
Input: map[string]any{"scope": scopeArg, "unit": unitArgS},
|
|
Run: func(a map[string]any) (any, error) {
|
|
scope, err := scopeOf(a)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
unit, err := unitOf(a)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.Act(scope, verb, unit)
|
|
}}
|
|
}
|
|
return []stdio.Tool{
|
|
{Name: seat + ".units",
|
|
Description: "The units the service manager knows in a scope, each with its load, active and sub state; narrowed to a pattern when asked.",
|
|
Input: map[string]any{"scope": scopeArg, "pattern": str("a glob the unit's name must match (optional)")},
|
|
Run: func(a map[string]any) (any, error) {
|
|
scope, err := scopeOf(a)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
pattern, _ := a["pattern"].(string)
|
|
units, err := m.Units(scope, pattern)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"scope": string(scope), "units": units}, nil
|
|
}},
|
|
{Name: seat + ".status",
|
|
Description: "One unit as the service manager sees it now: its states, whether it starts at boot, its main process, and mesh_declared — true when its unit file is one the mesh wrote (a unit the mesh only puts into a state is not recognised from here).",
|
|
Input: map[string]any{"scope": scopeArg, "unit": unitArgS},
|
|
Run: func(a map[string]any) (any, error) {
|
|
scope, err := scopeOf(a)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
unit, err := unitOf(a)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.Status(scope, unit)
|
|
}},
|
|
act("start", "Start one unit. For a unit the mesh declares, the answer says the host will restore what its declaration says at its next apply."),
|
|
act("stop", "Stop one unit; for a unit the mesh declares, the answer says the host will restore its declared state."),
|
|
act("restart", "Restart one unit."),
|
|
act("enable", "Make one unit start at boot (or at the account's login, in user scope)."),
|
|
act("disable", "Stop one unit starting at boot (or at login, in user scope)."),
|
|
{Name: seat + ".journal",
|
|
Description: "The last lines of one unit's journal (at most 2000), in a time window and narrowed to a priority " +
|
|
"and to lines holding a text when asked — a system service's included: the read is escalated, so it is the " +
|
|
"service's own lines and not only the operator account's. A secret the unit printed is shown as " +
|
|
"[redacted: <what it was>].",
|
|
Input: map[string]any{"scope": scopeArg, "unit": unitArgS,
|
|
"lines": str("how many lines from the end of what matches (default 100, at most 2000)"),
|
|
"since": str("the window's start: an RFC 3339 time (2026-10-07T09:30:00Z) or relative to now (-30min, -2h, yesterday) (optional)"),
|
|
"until": str("the window's end, in the same forms (optional; now when absent)"),
|
|
"match": str("only the lines holding this text, as written — a fixed string, not a pattern (optional)"),
|
|
"priority": str("only entries this severe or more: 0-7 or emerg, alert, crit, err, warning, notice, info, debug (optional)")},
|
|
Run: func(a map[string]any) (any, error) {
|
|
scope, err := scopeOf(a)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
unit, err := unitOf(a)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
q, err := journalQuery(a)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.Journal(scope, unit, q)
|
|
}},
|
|
// Was the module's own systemd_failed; on the seat since the operator's direction of 2026-10-07, so
|
|
// whatever holds the role answers it and every machine is asked the same way.
|
|
{Name: seat + ".failed",
|
|
Description: "Every failed unit on this machine, in the system manager and in the operator account's; a manager that does not answer is reported with its error, not as nothing failed.",
|
|
Input: map[string]any{"scope": str(`"system" or "user": only that manager (both when absent)`)},
|
|
Run: func(a map[string]any) (any, error) {
|
|
if s, _ := a["scope"].(string); s == "" {
|
|
return m.Failed(), nil
|
|
}
|
|
scope, err := scopeOf(a)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return m.Failed(scope), nil
|
|
}},
|
|
}
|
|
}
|
|
|
|
func fromEnv() *Manager {
|
|
me, _ := user.Current()
|
|
name := ""
|
|
if me != nil {
|
|
name = me.Username
|
|
}
|
|
account := strings.TrimSpace(os.Getenv("MESH_OPERATOR_ACCOUNT"))
|
|
if account == "" {
|
|
account = name
|
|
}
|
|
uid := os.Getuid()
|
|
if me != nil {
|
|
if n, err := strconv.Atoi(me.Uid); err == nil {
|
|
uid = n
|
|
}
|
|
}
|
|
return &Manager{Account: account, UID: uid, User: name, Run: execRunner, Read: readFile, Env: os.Environ()}
|
|
}
|
|
|
|
func main() {
|
|
if err := stdio.Serve("", tools(fromEnv())); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(1)
|
|
}
|
|
}
|