Carry a bundle's seat traffic under its own name and kind, and start a module as its own account (hq ADR 0259)

This commit is contained in:
jochen
2026-10-09 10:12:39 +02:00
committed by jschoubben
parent 66de6774e0
commit 6d9906b6c9
9 changed files with 922 additions and 4 deletions
@@ -40,6 +40,9 @@ func (b *subscribing) State(string, json.RawMessage) (json.RawMessage, error) {
func (b *subscribing) Watch(json.RawMessage, func(json.RawMessage) error) (func(), error) {
return func() {}, nil
}
func (b *subscribing) Seat(string, json.RawMessage, func(string, any) (json.RawMessage, error)) (json.RawMessage, error) {
return nil, nil
}
func (b *subscribing) Subscribe(d func(json.RawMessage) error) error {
b.mu.Lock()
defer b.mu.Unlock()
+39 -1
View File
@@ -60,6 +60,10 @@ type Bus interface {
Subscribe(deliver func(envelope json.RawMessage) error) error
State(verb string, params json.RawMessage) (json.RawMessage, error)
Watch(params json.RawMessage, deliver func(change json.RawMessage) error) (stop func(), err error)
// Seat answers a bundle's seat traffic (novox/hq ADR 0259 §3): `publish`, `prove` and `record` at once;
// `take` and `answer` bind once and hand each message or proof on through handOn, to whichever child
// of the module asked last — a restarted child asks again as its code runs again.
Seat(verb string, params json.RawMessage, handOn func(method string, params any) (json.RawMessage, error)) (json.RawMessage, error)
}
// EventTimeout bounds how long a bundle has to handle one event before it is offered again.
@@ -147,6 +151,17 @@ func Start(module, entry string, env []string, mesh Bus, logf func(string, ...an
// The child that last subscribed is the one events are handed to: a restarted child subscribes
// again as its code is imported, and from then on the module's events are its.
var subscriber *child
// The child that last asked to take a seat's work or answer its proofs is the one they are handed to.
var seatTaker *child
handOn := func(method string, params any) (json.RawMessage, error) {
mu.Lock()
c := seatTaker
mu.Unlock()
if c == nil {
return nil, errors.New(module + "'s bundle is not running to take it")
}
return c.ask(module, method, params, EventTimeout)
}
stopped := false
deliver := func(envelope json.RawMessage) error {
mu.Lock()
@@ -166,6 +181,12 @@ func Start(module, entry string, env []string, mesh Bus, logf func(string, ...an
start := func() (*child, error) {
cmd := exec.Command(entry)
cmd.Env = env
// **A module that names an account of its own runs as it** (novox/hq ADR 0259 §8): its secrets and
// state are that account's, and the operator's account — which every agent runs as — reaches
// neither. Never root, never the operator's.
if err := runAs(cmd, env); err != nil {
return nil, err
}
stdin, err := cmd.StdinPipe()
if err != nil {
return nil, err
@@ -237,7 +258,21 @@ func Start(module, entry string, env []string, mesh Bus, logf func(string, ...an
subscriber = c
mu.Unlock()
err = mesh.Subscribe(deliver)
case "mesh/state.get", "mesh/state.put", "mesh/state.delete", "mesh/state.keys":
case "mesh/seat.publish", "mesh/seat.prove", "mesh/seat.record":
var answered json.RawMessage
if answered, err = mesh.Seat(strings.TrimPrefix(m.Method, "mesh/seat."), m.Params, nil); err == nil {
result = answered
}
case "mesh/seat.take", "mesh/seat.answer":
mu.Lock()
seatTaker = c
mu.Unlock()
var answered json.RawMessage
if answered, err = mesh.Seat(strings.TrimPrefix(m.Method, "mesh/seat."), m.Params, handOn); err == nil {
result = answered
}
case "mesh/state.get", "mesh/state.put", "mesh/state.delete", "mesh/state.keys",
"mesh/state.create", "mesh/state.update":
var answered json.RawMessage
if answered, err = mesh.State(strings.TrimPrefix(m.Method, "mesh/state."), m.Params); err == nil {
result = answered
@@ -329,6 +364,9 @@ func Start(module, entry string, env []string, mesh Bus, logf func(string, ...an
if subscriber == c {
subscriber = nil
}
if seatTaker == c {
seatTaker = nil
}
wasStopped := stopped
// Started again at once if it ran a while; after a growing pause while it keeps exiting.
wait := RestartFirst
+68
View File
@@ -0,0 +1,68 @@
package launch
import (
"fmt"
"os/exec"
"os/user"
"strconv"
"strings"
"syscall"
)
// RunAs is the word in a bundle's environment naming the account it runs as (novox/hq ADR 0259 §8), and
// OperatorAccount the word naming the operator's account on this machine, which the runtime is given.
const (
RunAs = "MESH_RUN_AS"
OperatorAccount = "MESH_OPERATOR_ACCOUNT"
)
// lookupUser is user.Lookup, a variable so a test can name accounts this machine does not have.
var lookupUser = user.Lookup
func word(env []string, name string) string {
for _, kv := range env {
if k, v, ok := strings.Cut(kv, "="); ok && k == name {
return v
}
}
return ""
}
// runAs starts cmd as the account its environment names, with that account's home, or leaves it as the
// runtime's own when none is named. It refuses root and the operator's account: a module asks for an
// account of its own to keep what it holds from the agents, and every agent runs as the operator.
func runAs(cmd *exec.Cmd, env []string) error {
name := word(env, RunAs)
if name == "" {
return nil
}
if operator := word(env, OperatorAccount); operator != "" && name == operator {
return fmt.Errorf("%s names the operator's account %s; a module runs as an account of its own, never "+
"the one every agent runs as (novox/hq ADR 0259)", RunAs, name)
}
u, err := lookupUser(name)
if err != nil {
return fmt.Errorf("%s names the account %s, which this machine does not have: the module makes it with "+
"a user resource (novox/hq ADR 0259): %w", RunAs, name, err)
}
uid, err := strconv.ParseUint(u.Uid, 10, 32)
if err != nil {
return fmt.Errorf("the account %s has no usable uid %q", name, u.Uid)
}
gid, err := strconv.ParseUint(u.Gid, 10, 32)
if err != nil {
return fmt.Errorf("the account %s has no usable gid %q", name, u.Gid)
}
if uid == 0 || name == "root" {
return fmt.Errorf("%s names root; a module that asks for an account of its own is not given root", RunAs)
}
cmd.SysProcAttr = &syscall.SysProcAttr{Credential: &syscall.Credential{Uid: uint32(uid), Gid: uint32(gid)}}
var kept []string
for _, kv := range cmd.Env {
if !strings.HasPrefix(kv, "HOME=") && !strings.HasPrefix(kv, "USER=") && !strings.HasPrefix(kv, "LOGNAME=") {
kept = append(kept, kv)
}
}
cmd.Env = append(kept, "HOME="+u.HomeDir, "USER="+name, "LOGNAME="+name)
return nil
}
+48
View File
@@ -0,0 +1,48 @@
package launch
import (
"errors"
"os/exec"
"os/user"
"strings"
"testing"
)
// novox/hq ADR 0259 §8: a module naming an account of its own runs as it, never as root or the operator.
func TestABundleRunsAsTheAccountItNamesAndNeverRootOrTheOperator(t *testing.T) {
was := lookupUser
t.Cleanup(func() { lookupUser = was })
lookupUser = func(name string) (*user.User, error) {
switch name {
case "telegram":
return &user.User{Username: "telegram", Uid: "961", Gid: "961", HomeDir: "/var/lib/telegram"}, nil
case "root":
return &user.User{Username: "root", Uid: "0", Gid: "0", HomeDir: "/root"}, nil
case "toor":
return &user.User{Username: "toor", Uid: "0", Gid: "0", HomeDir: "/root"}, nil
}
return nil, errors.New("unknown user")
}
cmd := exec.Command("/bin/true")
cmd.Env = []string{"HOME=/root", "PATH=/usr/bin"}
if err := runAs(cmd, []string{RunAs + "=telegram", OperatorAccount + "=jo"}); err != nil {
t.Fatal(err)
}
if c := cmd.SysProcAttr.Credential; c == nil || c.Uid != 961 || c.Gid != 961 {
t.Fatalf("not started as telegram: %+v", cmd.SysProcAttr)
}
if env := strings.Join(cmd.Env, " "); !strings.Contains(env, "HOME=/var/lib/telegram") || strings.Contains(env, "HOME=/root") {
t.Fatalf("the account's home is not its own: %s", env)
}
for name, want := range map[string]string{"jo": "operator's account", "root": "names root", "toor": "names root",
"nobody-here": "does not have"} {
err := runAs(exec.Command("/bin/true"), []string{RunAs + "=" + name, OperatorAccount + "=jo"})
if err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("%s: %v, want a refusal saying %q", name, err, want)
}
}
plain := exec.Command("/bin/true")
if err := runAs(plain, nil); err != nil || plain.SysProcAttr != nil {
t.Error("a bundle naming no account was changed")
}
}
+71
View File
@@ -0,0 +1,71 @@
package launch
import (
"encoding/json"
"os"
"path/filepath"
"sync"
"testing"
"time"
)
// A bundle that asks to take a worker's work and answers each piece it is handed.
const takingBundle = `#!/bin/sh
while IFS= read -r line; do
id=$(printf '%s' "$line" | sed -n 's/.*"id":\([0-9]*\)[,}].*/\1/p')
case "$line" in
*'"method":"initialize"'*) printf '{"jsonrpc":"2.0","id":%s,"result":{}}\n' "$id" ;;
*'"method":"tools/list"'*)
printf '{"jsonrpc":"2.0","id":%s,"result":{"tools":[]}}\n' "$id"
printf '{"jsonrpc":"2.0","id":"s1","method":"mesh/seat.take","params":{"worker":"SEAT_CHANNEL_TELEGRAM_worker"}}\n' ;;
*'"method":"mesh/work"'*) printf '{"jsonrpc":"2.0","id":%s,"result":{"taken":true}}\n' "$id" ;;
esac
done
`
type seating struct {
subscribing
mu sync.Mutex
verb string
params string
handOn func(string, any) (json.RawMessage, error)
}
func (b *seating) Seat(verb string, params json.RawMessage, handOn func(string, any) (json.RawMessage, error)) (json.RawMessage, error) {
b.mu.Lock()
defer b.mu.Unlock()
b.verb, b.params, b.handOn = verb, string(params), handOn
return json.RawMessage(`{}`), nil
}
// novox/hq ADR 0259 §3: a bundle's seat traffic reaches the runtime's bus from its own channel, and the work
// it takes is handed back to it.
func TestABundleTakesASeatsWorkThroughItsOwnChannel(t *testing.T) {
entry := filepath.Join(t.TempDir(), "bundle")
if err := os.WriteFile(entry, []byte(takingBundle), 0o755); err != nil {
t.Fatal(err)
}
b := &seating{}
l, err := Start("telegram", entry, os.Environ(), b, t.Logf)
if err != nil {
t.Fatal(err)
}
t.Cleanup(l.Stop)
var handOn func(string, any) (json.RawMessage, error)
for i := 0; handOn == nil; i++ {
if i > 100 {
t.Fatal("the bundle never asked to take its work")
}
time.Sleep(20 * time.Millisecond)
b.mu.Lock()
handOn = b.handOn
b.mu.Unlock()
}
if b.verb != "take" || b.params != `{"worker":"SEAT_CHANNEL_TELEGRAM_worker"}` {
t.Fatalf("asked %s %s", b.verb, b.params)
}
got, err := handOn("mesh/work", map[string]any{"work": map[string]any{"subject": "mesh.seat.channel.accept.show.telegram"}})
if err != nil || string(got) != `{"taken":true}` {
t.Fatalf("the work was not handed to the bundle: %s %v", got, err)
}
}