Say the login session a mesh-cli line comes from, and pin the SDK at go/v0.1.9 (hq ADR 0272)
mesh/merge-gate pass: builds mesh-host → 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 delivered

A service running as the operator — the tool runner, a unit of the
account's user manager — shares the operator's user id, and was as much the
terminal on the control-node as a person typing. The engine now reads the
asking process's cgroup and sends its login session's scope
(session-<id>.scope under the account's own user-<uid>.slice), or nothing;
the controller makes nothing else the terminal. The SDK is pinned at its
tag now that go/v0.1.9 exists.
This commit is contained in:
jochen
2026-10-09 13:40:17 +02:00
parent 1ac7741c63
commit 4a5dcb0209
6 changed files with 82 additions and 5 deletions
+36 -1
View File
@@ -19,6 +19,7 @@ import (
"os/user"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
@@ -35,6 +36,11 @@ type Asked struct {
UID uint32 `json:"uid"`
// Follow is a call a line runs as, asked about again by the account that asked it (AskTheController).
Follow string `json:"follow,omitempty"`
// Session is the login session the asking process runs in — its systemd scope, `session-<id>.scope` under the
// account's own slice — as the kernel's cgroup says it; empty for anything else: a system service, a user unit,
// a process another account's session started. Only a login session is a person at a terminal (novox/hq ADR
// 0272): the controller makes nothing else the terminal.
Session string `json:"session,omitempty"`
}
// Forward asks the controller and follows the line to what came of it (AskTheController): what it printed, that
@@ -84,6 +90,35 @@ func (s *slots) give(uid uint32) {
// test can say what the kernel would; it is the only source of the account.
var peerOf = peerCredentials
// sessionOf reads the login session a process runs in, from the kernel. A variable so a test can say.
var sessionOf = func(pid int32, uid uint32) string {
raw, err := os.ReadFile(fmt.Sprintf("/proc/%d/cgroup", pid))
if err != nil {
return ""
}
return loginSessionIn(string(raw), uid)
}
// loginSessionIn is the login session scope a process's cgroup names, for its own account: the unified hierarchy's
// line, `0::/user.slice/user-<uid>.slice/session-<id>.scope`, and nothing below or beside it. Anything else is no
// login session — a service under system.slice, a unit under the account's user manager (user@<uid>.service), a
// scope of another account's.
func loginSessionIn(cgroup string, uid uint32) string {
for _, line := range strings.Split(cgroup, "\n") {
path, ok := strings.CutPrefix(line, "0::")
if !ok {
continue
}
prefix := fmt.Sprintf("/user.slice/user-%d.slice/", uid)
scope, ok := strings.CutPrefix(path, prefix)
if !ok || strings.Contains(scope, "/") || !strings.HasPrefix(scope, "session-") || !strings.HasSuffix(scope, ".scope") {
return ""
}
return scope
}
return ""
}
// sameNamespaces refuses a peer whose mount or user namespace is not the engine's. A variable so a test can say.
var sameNamespaces = namespacesOf
@@ -204,5 +239,5 @@ func answer(ctx context.Context, conn *net.UnixConn, held *slots, forward Forwar
_ = conn.SetDeadline(time.Now().Add(cli.AskWithin))
asking, cancel := context.WithTimeout(ctx, cli.AskWithin-30*time.Second)
defer cancel()
reply(forward(asking, Asked{Line: req.Line, Account: account, UID: uid}))
reply(forward(asking, Asked{Line: req.Line, Account: account, UID: uid, Session: sessionOf(pid, uid)}))
}