systemd: the journal verb reads a window, and failed is the seat's verb
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.
This commit is contained in:
jochen
2026-10-07 19:15:20 +02:00
parent c74f407abd
commit 66106d93ac
7 changed files with 728 additions and 28 deletions
+82 -11
View File
@@ -29,7 +29,6 @@ import (
"os"
"os/exec"
"regexp"
"strconv"
"strings"
"time"
)
@@ -288,27 +287,92 @@ func (m *Manager) Act(scope Scope, verb, unit string) (map[string]any, error) {
return answer, nil
}
// Journal is the last lines of one unit's journal.
func (m *Manager) Journal(scope Scope, unit string, lines int) (map[string]any, error) {
// Journal is the last lines of one unit's journal that a query keeps, its secrets redacted.
//
// **Every argument is one word of journalctl's argv, never a shell's** (journal.go): the unit is refused
// when it would read as an option, a window bound is given as --since=<v> so a relative "-30min" is its
// value and never a flag, and the rest is validated to the forms journalctl reads before anything runs —
// under sudo, a word read as an option would be root's option.
//
// **A match is a fixed string, applied here to the redacted lines**, not journalctl's --grep, which is a
// pattern and depends on how journalctl was built; and applied after redaction, so a caller cannot find a
// secret by asking which lines hold it. journalctl is then asked for a bounded scan of the window's last
// lines, and the answer says how many were read, so a match that found fewer than asked is not read as
// all there is when the scan was full.
func (m *Manager) Journal(scope Scope, unit string, q JournalQuery) (map[string]any, error) {
if err := unitArg(unit); err != nil {
return nil, err
}
out, err := m.call(scope, "journalctl", "--no-pager", "-n", strconv.Itoa(lines), "-u", unit, "-o", "short-iso")
q, err := q.valid()
if err != nil {
return nil, err
}
kept := []string{}
read := q.Lines
if q.Match != "" {
read = MatchScan
}
out, err := m.call(scope, "journalctl", q.argv(unit, read)...)
if err != nil {
return nil, err
}
known, envErr := m.unitSecrets(scope, unit)
all := []string{}
for _, l := range strings.Split(out, "\n") {
if l != "" {
kept = append(kept, l)
all = append(all, l)
}
}
return map[string]any{"unit": unit, "scope": string(scope), "lines": kept}, nil
scanned := len(all)
kept, redacted := []string{}, 0
for _, l := range all {
l, n := redact(l, known)
redacted += n
if q.Match != "" && !strings.Contains(l, q.Match) {
continue
}
if len(l) > LongestLine {
l = l[:LongestLine] + "…"
}
kept = append(kept, l)
}
if len(kept) > q.Lines {
kept = kept[len(kept)-q.Lines:]
}
answer := map[string]any{"unit": unit, "scope": string(scope), "lines": kept, "count": len(kept)}
for k, v := range map[string]string{"since": q.Since, "until": q.Until, "match": q.Match, "priority": q.Priority} {
if v != "" {
answer[k] = v
}
}
if q.Match != "" {
answer["scanned"] = scanned
if scanned >= MatchScan {
answer["note"] = fmt.Sprintf("the match was looked for in the window's last %d lines only: narrow the window to reach earlier ones", MatchScan)
}
}
if envErr != nil {
answer["redaction"] = "only what a line's shape says is a secret: the unit's environment could not be read (" + envErr.Error() + ")"
}
if redacted > 0 {
answer["redacted"] = redacted
answer["leak"] = "this unit's journal holds secrets, shown as [redacted: <what it was>]: rotate each one after the program stops printing it"
}
return answer, nil
}
// Failed is every failed unit in both managers. A manager that does not answer is reported as such,
// beside the other's answer — never as "nothing failed".
func (m *Manager) Failed() map[string]any {
// unitSecrets are the values of the unit's own Environment= that must not be answered. Read with
// systemctl show, which needs no escalation; a unit that does not exist has none.
func (m *Manager) unitSecrets(scope Scope, unit string) ([]knownSecret, error) {
out, err := m.call(scope, "systemctl", "show", unit, "--no-pager", "--property=Environment", "--value")
if err != nil {
return nil, err
}
return secretsIn(environment(strings.TrimSpace(out))), nil
}
// Failed is every failed unit in the managers asked — both when none is named. A manager that does not
// answer is reported as such, beside the other's answer — never as "nothing failed".
func (m *Manager) Failed(scopes ...Scope) map[string]any {
in := func(scope Scope) any {
units, err := m.Units(scope, "")
if err != nil {
@@ -322,7 +386,14 @@ func (m *Manager) Failed() map[string]any {
}
return failed
}
return map[string]any{"system": in(System), "user": in(User)}
if len(scopes) == 0 {
scopes = []Scope{System, User}
}
answer := map[string]any{}
for _, s := range scopes {
answer[string(s)] = in(s)
}
return answer
}
// unitArg refuses a unit name systemctl or journalctl would read as an option — which under sudo would be