diff --git a/cmd/mesh-controller/seat_dependencies_test.go b/cmd/mesh-controller/seat_dependencies_test.go index bea72be..4888337 100644 --- a/cmd/mesh-controller/seat_dependencies_test.go +++ b/cmd/mesh-controller/seat_dependencies_test.go @@ -15,7 +15,7 @@ import ( func serviceManagerHolder() catalogue.Manifest { return catalogue.Manifest{Module: "systemd", Version: "1", Claims: []catalogue.Claim{{Name: catalogue.ServiceManagerSeat, Scope: catalogue.ScopeNode, - Serves: []string{"units", "status", "start", "stop", "restart", "enable", "disable", "journal"}}}, + Serves: []string{"units", "status", "start", "stop", "restart", "enable", "disable", "journal", "failed"}}}, Resources: []map[string]any{{"id": "systemd", "type": "package", "package": "systemd"}}} } diff --git a/internal/catalogue/operators_machine_test.go b/internal/catalogue/operators_machine_test.go index ddac43d..40551f0 100644 --- a/internal/catalogue/operators_machine_test.go +++ b/internal/catalogue/operators_machine_test.go @@ -42,7 +42,7 @@ func TestTheServiceManagerSeatServesTheUnitVerbs(t *testing.T) { if seat.Scope != ScopeNode { t.Fatalf("the service manager is a role each machine has once, and the seat is %s-scoped", seat.Scope) } - want := []string{"units", "status", "start", "stop", "restart", "enable", "disable", "journal"} + want := []string{"units", "status", "start", "stop", "restart", "enable", "disable", "journal", "failed"} var got []string for _, v := range seat.Serves { got = append(got, v.Name) diff --git a/internal/catalogue/seats.go b/internal/catalogue/seats.go index ff30ca7..950afe7 100644 --- a/internal/catalogue/seats.go +++ b/internal/catalogue/seats.go @@ -561,7 +561,8 @@ func SeatsWithAProtocol() []Seat { // serviceManagerVerbs is the contract every holder of node-service-manager serves (novox/hq ADR // 0177): the units on the machine in both scopes, read and acted on by name. Every verb takes an // optional scope — "system" when absent, "user" for the operator account's own manager — so a -// caller asks for a user unit the way it asks for a system one. +// caller asks for a user unit the way it asks for a system one; `failed` alone reads both managers +// when none is named. func serviceManagerVerbs() []Verb { scoped := func(more map[string]string, required []string) map[string]any { props := map[string]string{"scope": "\"system\" (the default) or \"user\": the operator account's own manager"} @@ -586,8 +587,26 @@ func serviceManagerVerbs() []Verb { Input: scoped(unit, []string{"unit"})}, {Name: "disable", Description: "Stop one unit starting at boot (or at login, in user scope).", Input: scoped(unit, []string{"unit"})}, - {Name: "journal", Description: "The last lines of one unit's journal.", - Input: scoped(map[string]string{"unit": unit["unit"], "lines": "how many lines from the end (default 100)"}, []string{"unit"})}, + // **A window, not only a tail** (the operator's direction 2026-10-07): an incident is read for the + // minutes it happened in, and with no window on the verb a person reached for a shell. Every + // argument is the holder's to validate — passed to journalctl as one word of its own, never through + // a shell — and what the unit printed of a secret is redacted before it is answered. + {Name: "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 secret the unit printed is shown as " + + "[redacted: ].", + Input: scoped(map[string]string{ + "unit": unit["unit"], + "lines": "how many lines from the end of what matches (default 100, at most 2000)", + "since": "the window's start: an RFC 3339 time (2026-10-07T09:30:00Z) or relative to now (-30min, -2h, yesterday) (optional)", + "until": "the window's end, in the same forms (optional; now when absent)", + "match": "only the lines holding this text, as written — a fixed string, not a pattern (optional)", + "priority": "only entries this severe or more: 0-7 or emerg, alert, crit, err, warning, notice, info, debug (optional)", + }, []string{"unit"})}, + // What has failed, on the seat rather than as one holder's own tool: whatever holds the role answers + // it, so a caller asks every machine the same way. + {Name: "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, never as nothing failed.", + Input: schema(map[string]string{"scope": "\"system\" or \"user\": only that manager (both when absent)"}, nil)}, } }