From e420f6587a013a0bad6b67e6c13d9cec0fcad0ad Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 8 Oct 2026 18:26:31 +0200 Subject: [PATCH] Let a user say it never becomes root, so the agents' account can be judged A declaration may now state root: never on a user (novox/hq ADR 0266), and a health statement carries it under contract 3; the judging follows. --- internal/declaration/declaration.go | 20 ++++++++++++++++++++ internal/link/messages.go | 10 ++++++++++ 2 files changed, 30 insertions(+) diff --git a/internal/declaration/declaration.go b/internal/declaration/declaration.go index 2472adf..5ab8e65 100644 --- a/internal/declaration/declaration.go +++ b/internal/declaration/declaration.go @@ -383,8 +383,25 @@ type User struct { // has it not. On the account rather than on the unit, because it is the account's: two units of // one account cannot disagree about it, and undeclaring one of them must not stop the other. Linger *bool `json:"linger,omitempty"` + + // Root says whether this account may become root without a person (novox/hq ADR 0266). "never" + // is the agents' own account: the login an agent session runs as on a machine where it must not + // reach root by itself. Empty asserts nothing, as Shell's does. + // + // **A statement the engine judges, never one it acts on.** The apply gives an account it creates + // no password, no sudo rule and no group beyond those declared, as it always has, and takes none + // away from one it finds: a sudo rule or a group granted by hand is a person's to remove, and a + // declaration that silently stripped them would be the mesh deciding what a person's machine + // grants. What "never" adds is the look: on every look the engine reads whether the account can + // become root by itself โ€” by its uid, a group that grants root, any sudo rule, or a secret the mesh + // placed that it can read โ€” and says it unhealthy while it can (internal/accounts), so the + // controller can tell a machine where it holds from one where it does not. + Root string `json:"root,omitempty"` } +// RootNever is the one value Root takes besides empty: the account never becomes root without a person. +const RootNever = "never" + // Network is a named network on this machine. // // **A name and nothing else.** Not a driver, a subnet or a gateway: each of those is something a @@ -478,6 +495,9 @@ func (u *User) validate(where string, _ bool) []string { if u.Home != "" && !strings.HasPrefix(u.Home, "/") { problems = append(problems, where+": a home directory is an absolute path") } + if u.Root != "" && u.Root != RootNever { + problems = append(problems, fmt.Sprintf("%s: root is %q or absent, and %q is neither", where, RootNever, u.Root)) + } return problems } diff --git a/internal/link/messages.go b/internal/link/messages.go index 54d2e3a..fdef05e 100644 --- a/internal/link/messages.go +++ b/internal/link/messages.go @@ -227,6 +227,12 @@ const LivenessContract = 1 // older host parses strictly and refuses the whole declaration for it. const ReadinessContract = 2 +// RootContract is the statement of a host that also reads a user's declared `root` and judges it (novox/hq +// ADR 0266): whether an account declared never to become root without a person can. Like the field before +// it, its presence is what tells the controller this host may be sent `root` on a user; an older host +// refuses the whole declaration for a field it does not know. +const RootContract = 3 + // Health is one statement of every long-running resource's state on this machine (to-be 48 ยง4). Said in // every report, as an event on each change, and again every minute while anything is not healthy โ€” so a // lost statement is not a lost fault. @@ -355,6 +361,10 @@ type ResourceHealth struct { // controller tell a unit that cannot run before a new login from one that is broken. Empty for anything // the machine's own manager or a container runtime runs. Account string `json:"account,omitempty"` + // Root is "never" on a verdict of kind KindAccount for an account declared never to become root without + // a person (novox/hq ADR 0266): healthy then also means the engine found no way for it to. Empty on + // every other verdict, and on an account judged for its groups alone. + Root string `json:"root,omitempty"` } // HealthSaid is the health event: a machine's statement between its reports, on HealthSubject.