Serve a trusted holder from a runtime of its own account, refuse it in the machine's runtime, and say while an agent can become root where it runs (hq ADR 0259 §8)
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery ready: it delivers once merged
mesh/delivery-group group feat/asks-answered-on-any-channel rejected: a member's own check failed
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery ready: it delivers once merged
mesh/delivery-group group feat/asks-answered-on-any-channel rejected: a member's own check failed
This commit is contained in:
@@ -60,14 +60,14 @@ func assertBusObjects(ctx context.Context, inv *inventory.Inventory, r broker.Ra
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, s := range trafficStreams {
|
||||
if err := r.EnsureStream(s); err != nil {
|
||||
return nil, fmt.Errorf("asserting the work queue %s: %w", s.Name, err)
|
||||
}
|
||||
}
|
||||
// Every one tried, and every failure named: one module's consumer the bus refuses is no reason
|
||||
// the modules after it in the list hear nothing (novox/hq issue 208, where this runs on each send).
|
||||
var failed []error
|
||||
for _, s := range trafficStreams {
|
||||
if err := r.EnsureStream(s); err != nil {
|
||||
failed = append(failed, fmt.Errorf("the work queue %s: %w", s.Name, err))
|
||||
}
|
||||
}
|
||||
for _, c := range trafficWorkers {
|
||||
if err := r.EnsureConsumer(c); err != nil {
|
||||
failed = append(failed, fmt.Errorf("the worker %s on %s: %w", c.Name, c.Stream, err))
|
||||
@@ -159,6 +159,22 @@ func seatTrafficObjects(ctx context.Context, inv *inventory.Inventory) ([]broker
|
||||
return nil, nil, err
|
||||
}
|
||||
streams, workers := broker.SeatTrafficObjects(users)
|
||||
// And the queue of every such seat the catalogue declares, held or not: work queues from registration,
|
||||
// so what is submitted before a holder is assigned waits for it (the correctness review of 2026-10-08).
|
||||
declared, err := inv.DeclaredTrafficSeats(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
have := map[string]bool{}
|
||||
for _, s := range streams {
|
||||
have[s.Name] = true
|
||||
}
|
||||
for _, s := range broker.TrafficQueues(declared) {
|
||||
if !have[s.Name] {
|
||||
streams = append(streams, s)
|
||||
have[s.Name] = true
|
||||
}
|
||||
}
|
||||
return streams, workers, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -116,6 +116,11 @@ var probeRegistry = []probe{
|
||||
{ID: probeDeliveriesID, Asserts: "no delivery is held past its state's bound unsaid: mesh-delivery's " +
|
||||
"`stalled`, each with the transition its table lets healer H2 take", From: "ADR 0239",
|
||||
Kind: kindDeliveryStalled, Phase: 3, run: probeDeliveries},
|
||||
// Root where the trusted parties run (novox/hq ADR 0259 §8): while an agent can become root there without a
|
||||
// person, an answer proven there proves nothing.
|
||||
{ID: "D-root", Asserts: "no agent can become root without a person on a machine where the router or a channel " +
|
||||
"proving its sender runs: not by its own account, and not through a tool that runs its command as an account " +
|
||||
"that can", From: "ADR 0259 §8", Kind: kindAgentRoot, Phase: 2, run: probeAgentRoot},
|
||||
{ID: "DW", Asserts: "the watchdogs of the signals table ran within three of their intervals",
|
||||
From: "ADR 0227 rule 6: the watchers are watched", Kind: "watchdogs-silent", Phase: 1, run: probeWatchdogs},
|
||||
// The core's health definitions (novox/hq to-be 45 §8, ADR 0236): what a core component's new build is
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/conditions"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
)
|
||||
|
||||
// The self-check's probe of who can become root where the trusted parties run (novox/hq ADR 0259 §8, rule 3).
|
||||
//
|
||||
// The router and every channel proving its sender run as accounts of their own, so that no agent reads what
|
||||
// they hold or speaks as them. **Root on their machine undoes all of it.** So on every machine where a module
|
||||
// of its own account runs, this probe asks two questions, and raises an urgent condition while either is yes:
|
||||
//
|
||||
// 1. can an account an agent runs as become root without a person there — passwordless sudo, or a group
|
||||
// that is root by another name (docker, disk)? The agent's account is the one the coding-agent module on
|
||||
// that machine names (`agent_account`), and the operator's account while it names none;
|
||||
// 2. can an agent run a command it chooses, through the mesh's own tools, as an account that can — the login
|
||||
// shell's `execute` runs as the machine's runtime account, which the mesh's acting tools give passwordless
|
||||
// sudo?
|
||||
//
|
||||
// The measurement is the sudo module's on that machine (`sudo_escalation`); a machine it does not run on, or
|
||||
// that does not answer, is a probe that could not run — said, never taken for "no".
|
||||
|
||||
// kindAgentRoot is the condition an agent able to become root where a trusted party runs raises.
|
||||
const kindAgentRoot = "agent-root"
|
||||
|
||||
// The tools the probe asks, of the modules on each machine.
|
||||
const (
|
||||
agentModule = "claude-code"
|
||||
agentStatusTool = "claude_code_status"
|
||||
sudoModule = "sudo"
|
||||
sudoEscalation = "sudo_escalation"
|
||||
loginShellSeat = "node-login-shell"
|
||||
)
|
||||
|
||||
// escalation is the sudo module's answer for one account.
|
||||
type escalation struct {
|
||||
Account string `json:"account"`
|
||||
Root bool `json:"root_without_a_person"`
|
||||
Why string `json:"why"`
|
||||
}
|
||||
|
||||
// agentRootFacts is what one machine says, as the probe reads it.
|
||||
type agentRootFacts struct {
|
||||
Machine string
|
||||
Trusted []string // the modules of their own account on it
|
||||
Agent string // the account agents run as there; "" when none run there
|
||||
AgentNamed bool // the coding-agent module named it, rather than it being taken for the operator's
|
||||
Runtime string // the account the machine's runtime runs as
|
||||
LoginShell bool // the login shell seat is held there, running commands as the runtime's account
|
||||
Answers map[string]escalation
|
||||
}
|
||||
|
||||
// agentRootObservations judges one machine's facts: an urgent condition while an agent can become root there
|
||||
// without a person, one way or the other, naming which.
|
||||
func agentRootObservations(f agentRootFacts) []conditions.Observation {
|
||||
var ways []string
|
||||
if f.Agent != "" {
|
||||
if e := f.Answers[f.Agent]; e.Root {
|
||||
named := "the operator's account, which agents run as while the coding-agent module names no other"
|
||||
if f.AgentNamed {
|
||||
named = "the account agents run as"
|
||||
}
|
||||
ways = append(ways, fmt.Sprintf("%s (%s) can become root without a person: %s", f.Agent, named, e.Why))
|
||||
}
|
||||
}
|
||||
if f.LoginShell && f.Runtime != "" {
|
||||
if e := f.Answers[f.Runtime]; e.Root {
|
||||
ways = append(ways, fmt.Sprintf("the login shell runs any command an agent gives it as %s, which can "+
|
||||
"become root without a person: %s", f.Runtime, e.Why))
|
||||
}
|
||||
}
|
||||
if len(ways) == 0 {
|
||||
return nil
|
||||
}
|
||||
sort.Strings(f.Trusted)
|
||||
return []conditions.Observation{{Scope: conditions.ScopeMachine, ID: f.Machine, Token: kindAgentRoot,
|
||||
Machine: f.Machine, Kind: kindAgentRoot, Severity: conditions.Urgent,
|
||||
Summary: fmt.Sprintf("an agent can become root on %s without a person, where %s run as accounts of their "+
|
||||
"own; until it cannot, the router approves nothing proven there (novox/hq ADR 0259 §8): %s",
|
||||
f.Machine, strings.Join(f.Trusted, ", "), strings.Join(ways, "; ")),
|
||||
Headline: "Root without you on " + f.Machine,
|
||||
Needs: "choose how programs working for you on " + f.Machine + " stop becoming root without asking you.",
|
||||
Explanation: "The modules that prove your answers from your phone run on " + f.Machine + ", and a program " +
|
||||
"working for you there can become root without asking you, so it could answer in your name. Until " +
|
||||
"that changes, answers from your phone can only acknowledge.",
|
||||
Resolved: "Nothing on " + f.Machine + " becomes root without you any more"}}
|
||||
}
|
||||
|
||||
// probeAgentRoot is the probe: every machine a module of its own account runs on, judged.
|
||||
func probeAgentRoot(ctx context.Context, d *doctor) ([]conditions.Observation, error) {
|
||||
inv := d.open.inventory
|
||||
entries, err := inv.Catalogued(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
facts := map[string]*agentRootFacts{}
|
||||
agentOn, sudoOn := map[string]bool{}, map[string]bool{}
|
||||
for _, e := range entries {
|
||||
for _, node := range e.On {
|
||||
switch {
|
||||
case e.Manifest.RunsAs != "":
|
||||
f := facts[node]
|
||||
if f == nil {
|
||||
f = &agentRootFacts{Machine: node, Answers: map[string]escalation{}}
|
||||
facts[node] = f
|
||||
}
|
||||
f.Trusted = append(f.Trusted, e.Manifest.Module)
|
||||
case e.Manifest.Module == agentModule:
|
||||
agentOn[node] = true
|
||||
case e.Manifest.Module == sudoModule:
|
||||
sudoOn[node] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, e := range entries {
|
||||
if e.Manifest.ClaimsSeat(loginShellSeat) {
|
||||
for _, node := range e.On {
|
||||
if f := facts[node]; f != nil {
|
||||
f.LoginShell = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
machines := make([]string, 0, len(facts))
|
||||
for m := range facts {
|
||||
machines = append(machines, m)
|
||||
}
|
||||
sort.Strings(machines)
|
||||
var out []conditions.Observation
|
||||
var unread []string
|
||||
for _, m := range machines {
|
||||
f := facts[m]
|
||||
record, err := inv.NodeByName(ctx, m)
|
||||
if err != nil {
|
||||
unread = append(unread, m+": "+err.Error())
|
||||
continue
|
||||
}
|
||||
f.Runtime = record.Account
|
||||
if agentOn[m] {
|
||||
f.Agent = record.Account
|
||||
if a, err := link.AskModuleToolOn(ctx, d.js.Conn(), agentModule, agentStatusTool, m, map[string]any{}, 10*time.Second); err == nil && a.Error == "" {
|
||||
var status struct {
|
||||
AgentAccount string `json:"agent_account"`
|
||||
}
|
||||
if json.Unmarshal(a.Result, &status) == nil && status.AgentAccount != "" {
|
||||
f.Agent, f.AgentNamed = status.AgentAccount, true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !sudoOn[m] {
|
||||
unread = append(unread, m+": the sudo module is not assigned there, so who can become root is not measured")
|
||||
continue
|
||||
}
|
||||
var accounts []string
|
||||
for _, a := range []string{f.Agent, f.Runtime} {
|
||||
if a != "" && !slices.Contains(accounts, a) {
|
||||
accounts = append(accounts, a)
|
||||
}
|
||||
}
|
||||
if len(accounts) == 0 {
|
||||
continue
|
||||
}
|
||||
a, err := link.AskModuleToolOn(ctx, d.js.Conn(), sudoModule, sudoEscalation, m, map[string]any{"accounts": accounts}, 15*time.Second)
|
||||
if err == nil && a.Error != "" {
|
||||
err = fmt.Errorf("%s", a.Error)
|
||||
}
|
||||
if err != nil {
|
||||
unread = append(unread, m+": "+err.Error())
|
||||
continue
|
||||
}
|
||||
var answers []escalation
|
||||
if err := json.Unmarshal(a.Result, &answers); err != nil {
|
||||
unread = append(unread, m+": the sudo module's answer could not be read: "+err.Error())
|
||||
continue
|
||||
}
|
||||
for _, e := range answers {
|
||||
f.Answers[e.Account] = e
|
||||
}
|
||||
out = append(out, agentRootObservations(*f)...)
|
||||
}
|
||||
if len(unread) > 0 {
|
||||
return out, fmt.Errorf("who can become root could not be measured: %s", strings.Join(unread, "; "))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/conditions"
|
||||
)
|
||||
|
||||
// novox/hq ADR 0259 §8, rule 3: the probe fails today — agents run as the operator's account, which has
|
||||
// passwordless sudo, and the login shell runs their commands as it — and passes only once neither holds.
|
||||
func TestAnAgentAbleToBecomeRootWhereTheRouterRunsIsSaid(t *testing.T) {
|
||||
root := escalation{Root: true, Why: "(ALL : ALL) NOPASSWD: ALL"}
|
||||
none := escalation{Why: "no rule lets it without a password"}
|
||||
base := func() agentRootFacts {
|
||||
return agentRootFacts{Machine: "anchor", Trusted: []string{"telegram", "messenger"}, Runtime: "ops",
|
||||
Answers: map[string]escalation{}}
|
||||
}
|
||||
|
||||
today := base()
|
||||
today.Agent, today.LoginShell = "ops", true
|
||||
today.Answers["ops"] = root
|
||||
got := agentRootObservations(today)
|
||||
if len(got) != 1 || got[0].Severity != conditions.Urgent || got[0].Kind != kindAgentRoot ||
|
||||
!strings.Contains(got[0].Summary, "the operator's account") || !strings.Contains(got[0].Summary, "the login shell") {
|
||||
t.Fatalf("today: %+v", got)
|
||||
}
|
||||
|
||||
agentsMoved := base()
|
||||
agentsMoved.Agent, agentsMoved.AgentNamed, agentsMoved.LoginShell = "agents", true, true
|
||||
agentsMoved.Answers["agents"], agentsMoved.Answers["ops"] = none, root
|
||||
if got := agentRootObservations(agentsMoved); len(got) != 1 || strings.Contains(got[0].Summary, "agents (") ||
|
||||
!strings.Contains(got[0].Summary, "the login shell") {
|
||||
t.Errorf("agents of their own account, the login shell still the runtime's: %+v", got)
|
||||
}
|
||||
|
||||
closed := base()
|
||||
closed.Agent, closed.AgentNamed = "agents", true
|
||||
closed.Answers["agents"], closed.Answers["ops"] = none, root
|
||||
if got := agentRootObservations(closed); len(got) != 0 {
|
||||
t.Errorf("agents of their own account and no login shell there: %+v", got)
|
||||
}
|
||||
|
||||
noAgents := base()
|
||||
noAgents.Answers["ops"] = root
|
||||
if got := agentRootObservations(noAgents); len(got) != 0 {
|
||||
t.Errorf("no agent runs there and no login shell is held: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Its words are plain, as every condition's are (novox/hq ADR 0253).
|
||||
func TestTheRootConditionIsSaidInPlainWords(t *testing.T) {
|
||||
f := agentRootFacts{Machine: "anchor", Trusted: []string{"telegram"}, Runtime: "ops", Agent: "ops",
|
||||
Answers: map[string]escalation{"ops": {Root: true, Why: "x"}}}
|
||||
o := agentRootObservations(f)[0]
|
||||
if why, ok := conditions.PlainWords(conditions.Words{Headline: o.Headline, Explanation: o.Explanation,
|
||||
Needs: o.Needs, Resolved: o.Resolved}, "anchor"); !ok {
|
||||
t.Errorf("not plain: %s", why)
|
||||
}
|
||||
}
|
||||
@@ -135,6 +135,12 @@ func handOver(ctx context.Context, seatName, to string, adding bool) error {
|
||||
if !ok || nodeName == "" || module == "" {
|
||||
return fmt.Errorf("the new holder is named <node>/<module>, not %q", to)
|
||||
}
|
||||
// A kinded bench is held once per kind, by the claims themselves (novox/hq ADR 0234 §2, ADR 0259): the
|
||||
// record of who holds a seat has no kind, so a handover would name one holder for every kind.
|
||||
if catalogue.KindedBenches[seatName] {
|
||||
return fmt.Errorf("%s is a kinded bench: each kind is held by the module claiming it, and is not handed "+
|
||||
"over by `seat` — assign the module that claims the kind, or unassign the one that does", seatName)
|
||||
}
|
||||
open, err := openStores(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
@@ -62,3 +64,13 @@ func TestAClaimOutsideTheSetIsShownNotHidden(t *testing.T) {
|
||||
t.Fatalf("a claim outside the set was not shown: %+v", outside)
|
||||
}
|
||||
}
|
||||
|
||||
// A kinded bench is not handed over by `seat`: each kind is held by its claim (novox/hq ADR 0259).
|
||||
func TestAKindedBenchIsNotHandedOver(t *testing.T) {
|
||||
for _, bench := range []string{"channel", "intake"} {
|
||||
err := handOver(context.Background(), bench, "anchor/telegram", false)
|
||||
if err == nil || !strings.Contains(err.Error(), "is a kinded bench") {
|
||||
t.Errorf("%s: %v", bench, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ func PlacementsOf(r Records, interchangeable map[string]bool) Placements {
|
||||
for _, s := range d.Holds {
|
||||
if s.Kinded && s.Kind != "" {
|
||||
p.Kinds = append(p.Kinds, KindHeld{Seat: s.Name, Kind: s.Kind, Module: d.Module, Node: node,
|
||||
Capabilities: s.Capabilities})
|
||||
Capabilities: placedCapabilities(s.Capabilities, d.RunsAs)})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,6 +190,20 @@ func PlacementsOf(r Records, interchangeable map[string]bool) Placements {
|
||||
return p
|
||||
}
|
||||
|
||||
// placedCapabilities is what a kind's claim promises, as far as its placement lets the router believe it
|
||||
// (novox/hq ADR 0259 §8): `verified-sender` only from a holder that runs as an account of its own, on a bus
|
||||
// account of its own — never one the machine's runtime carries as the operator's account.
|
||||
func placedCapabilities(declared []string, runsAs string) []string {
|
||||
var out []string
|
||||
for _, c := range declared {
|
||||
if c == "verified-sender" && runsAs == "" {
|
||||
continue
|
||||
}
|
||||
out = append(out, c)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func kindListed(list []KindHeld, k KindHeld) bool {
|
||||
for _, x := range list {
|
||||
if x.Seat == k.Seat && x.Kind == k.Kind && x.Module == k.Module && x.Node == k.Node {
|
||||
|
||||
@@ -77,6 +77,9 @@ type Seat struct {
|
||||
Records []string
|
||||
// Capabilities are what this principal's claim of a kinded bench promises (ADR 0234 §2).
|
||||
Capabilities []string
|
||||
// DeclaredBy is the module that declares the seat. On a kinded bench it alone submits work to a kind
|
||||
// and answers its proofs (novox/hq ADR 0259 §8): the router, not any user or watcher of the bench.
|
||||
DeclaredBy string
|
||||
}
|
||||
|
||||
// A Principal is one user of the bus. Its permissions are derived from what it declares and
|
||||
@@ -705,6 +708,16 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
pub = append(pub, stateGrants(stateAccess{Module: d.Module, Node: p.Node, Keeps: stateNames(d.State),
|
||||
PerMachine: perMachineNames(d.State), Reads: d.Reads, KeyedReads: d.KeyedReads})...)
|
||||
}
|
||||
// **Never the traffic of a trusted holder** (novox/hq ADR 0259 §8): the machine's runtime runs as the
|
||||
// operator's account, which every agent runs as, so a module saying warrants or speaking for a kind
|
||||
// that proves its sender is never composed into it — refused here, naming it, whatever registration
|
||||
// let through.
|
||||
for _, d := range p.Carries {
|
||||
if why := trustedTraffic(d); why != "" {
|
||||
return Permissions{}, fmt.Errorf("%s on %s is carried by the machine's runtime, and %s: it runs "+
|
||||
"as an account of its own, never the runtime's (novox/hq ADR 0259)", d.Module, p.Node, why)
|
||||
}
|
||||
}
|
||||
// **And the seat traffic of the modules it carries** (novox/hq ADR 0259 §3): a bundle reaches the
|
||||
// bus only through its runtime, so the runtime is granted the union. That one module's code does
|
||||
// not publish under another's name or kind through it is the runtime's to keep, from the seat
|
||||
|
||||
@@ -137,7 +137,8 @@ func SeatTrafficOf(module string, holds, uses, watches []Seat) SeatTraffic {
|
||||
switch {
|
||||
case namesVerb(s.ByCaller, a):
|
||||
t.Publish = append(t.Publish, seatSubject(s, "accept", a+"."+module))
|
||||
case s.Kinded:
|
||||
case s.Kinded && module == s.DeclaredBy:
|
||||
// Work for a kind is put on its queue by the bench's own router, and by no other user.
|
||||
t.Publish = append(t.Publish, seatSubject(s, "accept", a+".*"))
|
||||
}
|
||||
}
|
||||
@@ -158,8 +159,11 @@ func SeatTrafficOf(module string, holds, uses, watches []Seat) SeatTraffic {
|
||||
for _, e := range w.Emits {
|
||||
t.Subscribe = append(t.Subscribe, seatSubject(w, "event", e+".*"))
|
||||
}
|
||||
for _, v := range w.Proofs {
|
||||
t.Answers = append(t.Answers, seatSubject(w, "proof", v+".*"))
|
||||
if module == w.DeclaredBy {
|
||||
// A code is answered by the bench's own router, and by no other watcher.
|
||||
for _, v := range w.Proofs {
|
||||
t.Answers = append(t.Answers, seatSubject(w, "proof", v+".*"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -228,15 +232,16 @@ func SeatTrafficObjects(users []Principal) ([]Stream, []Consumer) {
|
||||
Subjects: []string{"mesh.seat." + seat + ".accept.>"},
|
||||
Retention: RetentionWorkQueue,
|
||||
MaxAge: 7 * 24 * 60 * 60,
|
||||
Why: "work submitted to the " + seat + " seat; its holders take it, each kind its own, " +
|
||||
"and it queues while nobody does",
|
||||
Why: "work submitted to the " + seat + " seat; its holders take it, each kind its own, and it queues while nobody does",
|
||||
}
|
||||
consumers[w.Consumer] = Consumer{
|
||||
Name: w.Consumer,
|
||||
Stream: w.Stream,
|
||||
Filters: []string{w.Filter},
|
||||
AckWaitSeconds: 60,
|
||||
MaxDeliver: 5,
|
||||
// No bound on redelivery: a channel away for a day keeps its work, offered again later and
|
||||
// later by its holder's runtime (novox/hq ADR 0259; the correctness review of 2026-10-08).
|
||||
MaxDeliver: 0,
|
||||
Why: module + " holds " + seat + "; it pulls one ask at a time and acknowledges once it has " +
|
||||
"recorded it, so a crash redelivers rather than loses",
|
||||
}
|
||||
@@ -264,3 +269,37 @@ func SeatTrafficObjects(users []Principal) ([]Stream, []Consumer) {
|
||||
sort.Slice(cs, func(i, j int) bool { return cs[i].Name < cs[j].Name })
|
||||
return ss, cs
|
||||
}
|
||||
|
||||
// trustedTraffic is why a module's seat traffic is the trusted holder's (novox/hq ADR 0259 §8), or "": it
|
||||
// says a seat's event to one caller each (a warrant), or holds a kind of a kinded bench that proves its sender.
|
||||
func trustedTraffic(d Declared) string {
|
||||
for _, s := range d.Holds {
|
||||
for _, e := range s.Emits {
|
||||
if namesVerb(s.ByCaller, e) {
|
||||
return "it says " + s.Name + "'s " + e + " to one caller each"
|
||||
}
|
||||
}
|
||||
if s.Kinded && namesVerb(s.Capabilities, "verified-sender") {
|
||||
return "it holds " + s.Name + " of kind " + s.Kind + ", which proves its sender"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// TrafficQueues is the work queue of every seat naming its caller or its kind that accepts work, held or not
|
||||
// (novox/hq ADR 0259 §3): what is submitted before a holder is assigned waits for it.
|
||||
func TrafficQueues(seats []Seat) []Stream {
|
||||
var out []Stream
|
||||
seen := map[string]bool{}
|
||||
for _, s := range seats {
|
||||
if len(s.Accepts) == 0 || !s.isNewTraffic() || seen[s.Name] {
|
||||
continue
|
||||
}
|
||||
seen[s.Name] = true
|
||||
out = append(out, Stream{Name: seatStreamName(s.Name), Subjects: []string{"mesh.seat." + s.Name + ".accept.>"},
|
||||
Retention: RetentionWorkQueue, MaxAge: 7 * 24 * 60 * 60,
|
||||
Why: "work submitted to the " + s.Name + " seat; its holders take it, each kind its own, and it queues while nobody does"})
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -13,12 +13,13 @@ func operatorChannel() Seat {
|
||||
}
|
||||
|
||||
func channelSeat(kind string) Seat {
|
||||
return Seat{Name: "channel", Scope: "mesh", Accepts: []string{"show", "edit", "send"}, Kinded: true, Kind: kind}
|
||||
return Seat{Name: "channel", Scope: "mesh", Accepts: []string{"show", "edit", "send"}, Kinded: true, Kind: kind,
|
||||
DeclaredBy: "messenger"}
|
||||
}
|
||||
|
||||
func intakeSeat(kind string) Seat {
|
||||
return Seat{Name: "intake", Scope: "mesh", Emits: []string{"choice", "link"}, Proofs: []string{"code"},
|
||||
Kinded: true, Kind: kind}
|
||||
Kinded: true, Kind: kind, DeclaredBy: "messenger"}
|
||||
}
|
||||
|
||||
func allowed(patterns []string, subject string) bool {
|
||||
@@ -80,7 +81,7 @@ func TestOnlyTheHolderPublishesAWarrant(t *testing.T) {
|
||||
Nodes: []string{"anchor"},
|
||||
Assigned: map[string][]Declared{"anchor": {
|
||||
{Module: "messenger", Holds: []Seat{operatorChannel()}, Uses: []Seat{channelSeat("")},
|
||||
Watches: []Seat{{Name: "intake", Emits: []string{"choice", "link"}, Kinded: true, Proofs: []string{"code"}}}},
|
||||
Watches: []Seat{{Name: "intake", Emits: []string{"choice", "link"}, Kinded: true, Proofs: []string{"code"}, DeclaredBy: "messenger"}}},
|
||||
{Module: "mesh-delivery", Uses: []Seat{operatorChannel()}},
|
||||
{Module: "telegram", Holds: []Seat{channelSeat("telegram"), intakeSeat("telegram")}},
|
||||
}},
|
||||
@@ -150,7 +151,7 @@ func TestAKindedHolderReachesItsOwnKindAndNoOther(t *testing.T) {
|
||||
func TestTheWatcherAnswersProofsAndHearsEveryKind(t *testing.T) {
|
||||
got := perms(t, Principal{Kind: KindModule, Node: "anchor", Module: "messenger",
|
||||
Uses: []Seat{channelSeat("")},
|
||||
Watches: []Seat{{Name: "intake", Emits: []string{"choice"}, Kinded: true, Proofs: []string{"code"}}}})
|
||||
Watches: []Seat{{Name: "intake", Emits: []string{"choice"}, Kinded: true, Proofs: []string{"code"}, DeclaredBy: "messenger"}}})
|
||||
for _, s := range []string{"mesh.seat.intake.event.choice.telegram", "mesh.seat.intake.proof.code.desktop"} {
|
||||
if !allowed(got.Subscribe, s) {
|
||||
t.Errorf("the router does not hear %s", s)
|
||||
@@ -248,7 +249,7 @@ func TestTheRoutersMembershipNamesEveryKindAndItsCapabilities(t *testing.T) {
|
||||
desk.Capabilities = []string{"choice"}
|
||||
router := Declared{Module: "messenger", Holds: []Seat{operatorChannel()}, Uses: []Seat{channelSeat("")}}
|
||||
records := Records{Nodes: []string{"anchor", "laptop"}, Assigned: map[string][]Declared{
|
||||
"anchor": {router, {Module: "telegram", Holds: []Seat{tg}}},
|
||||
"anchor": {router, {Module: "telegram", Holds: []Seat{tg}, RunsAs: "telegram"}},
|
||||
"laptop": {{Module: "desk-channel", Holds: []Seat{desk}}},
|
||||
}}
|
||||
where := PlacementsOf(records, nil)
|
||||
@@ -270,3 +271,70 @@ func TestTheRoutersMembershipNamesEveryKindAndItsCapabilities(t *testing.T) {
|
||||
t.Error("an asker is told the channels")
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0259 §8: only the bench's own router answers its proofs and puts work on a kind's queue.
|
||||
func TestOnlyTheBenchsRouterAnswersProofsAndSubmitsWork(t *testing.T) {
|
||||
other := perms(t, Principal{Kind: KindModule, Node: "anchor", Module: "eavesdropper",
|
||||
Uses: []Seat{channelSeat("")},
|
||||
Watches: []Seat{{Name: "intake", Emits: []string{"choice"}, Kinded: true, Proofs: []string{"code"}, DeclaredBy: "messenger"}}})
|
||||
if allowed(other.Subscribe, "mesh.seat.intake.proof.code.telegram") {
|
||||
t.Error("a watcher that is not the router answers codes")
|
||||
}
|
||||
if allowed(other.Publish, "mesh.seat.channel.accept.show.telegram") {
|
||||
t.Error("a user that is not the router puts work on a kind's queue")
|
||||
}
|
||||
if !allowed(other.Subscribe, "mesh.seat.intake.event.choice.telegram") {
|
||||
t.Error("a watcher no longer hears the bench's events")
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0259 §8: the machine's runtime runs as the operator's account; it never carries a module
|
||||
// that says warrants or speaks for a kind proving its sender, and such a module has its own account.
|
||||
func TestTheMachinesRuntimeNeverCarriesATrustedHolder(t *testing.T) {
|
||||
tg := channelSeat("telegram")
|
||||
tg.Capabilities = []string{"choice", "verified-sender"}
|
||||
for name, d := range map[string]Declared{
|
||||
"the router": {Module: "messenger", Holds: []Seat{operatorChannel()}},
|
||||
"a verified channel": {Module: "telegram", Holds: []Seat{tg}},
|
||||
} {
|
||||
if _, err := PermissionsFor(Principal{Kind: KindNodeTools, Node: "anchor", Module: RuntimeModule,
|
||||
Carries: []Declared{d}}); err == nil || !strings.Contains(err.Error(), "an account of its own") {
|
||||
t.Errorf("%s was composed into the machine's runtime: %v", name, err)
|
||||
}
|
||||
}
|
||||
desk := channelSeat("desktop")
|
||||
desk.Capabilities = []string{"choice"}
|
||||
if _, err := PermissionsFor(Principal{Kind: KindNodeTools, Node: "anchor", Module: RuntimeModule,
|
||||
Carries: []Declared{{Module: "desk-channel", Holds: []Seat{desk}}}}); err != nil {
|
||||
t.Errorf("a channel proving nothing was refused: %v", err)
|
||||
}
|
||||
users, err := Users(Records{Nodes: []string{"anchor"}, Assigned: map[string][]Declared{"anchor": {
|
||||
{Module: RuntimeModule}, {Module: "telegram", Holds: []Seat{tg}, RunsAs: "telegram"},
|
||||
{Module: "messenger", Holds: []Seat{operatorChannel()}, RunsAs: "messenger"},
|
||||
}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, u := range users {
|
||||
if u.Kind == KindNodeTools {
|
||||
for _, d := range u.Carries {
|
||||
if d.RunsAs != "" {
|
||||
t.Errorf("the machine's runtime carries %s", d.Module)
|
||||
}
|
||||
}
|
||||
if _, err := PermissionsFor(u); err != nil {
|
||||
t.Errorf("the runtime could not be composed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0259 §8: verified-sender reaches the router only from a holder of its own account.
|
||||
func TestVerifiedSenderIsBelievedOnlyFromAHolderOfItsOwnAccount(t *testing.T) {
|
||||
if got := placedCapabilities([]string{"choice", "verified-sender"}, ""); namesVerb(got, "verified-sender") {
|
||||
t.Errorf("a carried holder keeps verified-sender: %v", got)
|
||||
}
|
||||
if got := placedCapabilities([]string{"choice", "verified-sender"}, "telegram"); !namesVerb(got, "verified-sender") {
|
||||
t.Errorf("a holder of its own account lost verified-sender: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,9 @@ type Declared struct {
|
||||
// Checks are the module's own tools its health asks, each `<module>.<tool>` (novox/hq ADR 0240, to-be
|
||||
// 48 §3): the machine's node-engine asks them of its own node tools, and is granted that and no more.
|
||||
Checks []string
|
||||
// RunsAs is the account the module runs as in a runtime of its own (novox/hq ADR 0259 §8): it is never
|
||||
// carried by the machine's runtime, and reaches the bus on its own account.
|
||||
RunsAs string
|
||||
}
|
||||
|
||||
// Records is what composing a user list needs to know about the mesh, and nothing more.
|
||||
@@ -120,10 +123,13 @@ func Users(r Records) ([]Principal, error) {
|
||||
})
|
||||
}
|
||||
if runtimeHere {
|
||||
out = append(out, Principal{
|
||||
Kind: KindNodeTools, Node: node, Module: RuntimeModule,
|
||||
Carries: append([]Declared(nil), r.Assigned[node]...),
|
||||
})
|
||||
var carried []Declared
|
||||
for _, d := range r.Assigned[node] {
|
||||
if d.RunsAs == "" {
|
||||
carried = append(carried, d)
|
||||
}
|
||||
}
|
||||
out = append(out, Principal{Kind: KindNodeTools, Node: node, Module: RuntimeModule, Carries: carried})
|
||||
}
|
||||
}
|
||||
for _, node := range sortedCopy(r.Enrolling) {
|
||||
|
||||
@@ -1066,9 +1066,23 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
|
||||
}
|
||||
owner[fmt.Sprint(process["id"])] = RuntimeModule
|
||||
out = append(out, process)
|
||||
// What each module's bundles are given is read as the account the runtime runs as.
|
||||
// And a runtime of its own for each module of its own account, after it (novox/hq ADR 0259 §8).
|
||||
owns, err := r.ownRuntimes(with)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, p := range owns {
|
||||
id := fmt.Sprint(p["id"])
|
||||
owner[id] = strings.TrimSuffix(id, "."+OwnRuntimeID())
|
||||
out = append(out, p)
|
||||
}
|
||||
// What each module's bundles are given is read as the account the runtime runs as — not what a
|
||||
// module of its own account is given, which its own account reads.
|
||||
words := map[string]map[string]string{}
|
||||
for _, m := range r.Modules {
|
||||
if m.RunsAs != "" {
|
||||
continue
|
||||
}
|
||||
w, err := bundleWords(m, with)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
// The router of novox/hq ADR 0259: it declares the operator's seat and the two kinded benches.
|
||||
func router() Manifest {
|
||||
return Manifest{Module: "messenger", Tools: []string{"open", "history", "notify"},
|
||||
State: []StateDeclaration{{Name: "asks"}},
|
||||
State: []StateDeclaration{{Name: "asks"}}, RunsAs: "messenger", SecretsOwner: "messenger",
|
||||
DefinesSeats: []SeatDeclaration{
|
||||
{Name: "operator-channel", Scope: ScopeMesh, Accepts: []string{"ask", "cancel"}, Emits: []string{"decided"},
|
||||
ByCaller: []string{"ask", "cancel", "decided"}, Records: []string{"asks"},
|
||||
@@ -98,6 +98,7 @@ func TestEachKindIsItsOwnHolderWhenResolved(t *testing.T) {
|
||||
func TestCapabilitiesAreTheVocabularysAndOnlyOnAKindedClaim(t *testing.T) {
|
||||
good := aChannel("telegram", "telegram")
|
||||
good.Claims[0].Capabilities = []string{"deliver", "choice", "verified-sender", "max-length:4096"}
|
||||
good.RunsAs = "telegram"
|
||||
if got := problemsFor(t, Shelf{"messenger": router(), "telegram": good}); got != "" {
|
||||
t.Fatalf("the vocabulary was refused: %s", got)
|
||||
}
|
||||
@@ -114,3 +115,47 @@ func TestCapabilitiesAreTheVocabularysAndOnlyOnAKindedClaim(t *testing.T) {
|
||||
t.Errorf("capabilities on a seat that is not kinded stood: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0259 §8: a module saying warrants, or speaking for a kind that proves its sender, runs as an
|
||||
// account of its own — never carried by the machine's runtime, which runs as the operator's account.
|
||||
func TestATrustedHolderMustRunAsAnAccountOfItsOwn(t *testing.T) {
|
||||
r := router()
|
||||
r.RunsAs = ""
|
||||
if got := problemsFor(t, Shelf{"messenger": r}); !strings.Contains(got, "messenger must run as an account of its own") {
|
||||
t.Errorf("a router on the machine's runtime stood: %s", got)
|
||||
}
|
||||
tg := aChannel("telegram", "telegram")
|
||||
tg.Claims[0].Capabilities = []string{"choice", "verified-sender"}
|
||||
if got := problemsFor(t, Shelf{"messenger": router(), "telegram": tg}); !strings.Contains(got, "telegram must run as an account of its own") {
|
||||
t.Errorf("a verified channel on the machine's runtime stood: %s", got)
|
||||
}
|
||||
desk := aChannel("desk-channel", "desktop")
|
||||
desk.Claims[0].Capabilities = []string{"choice"}
|
||||
if got := problemsFor(t, Shelf{"messenger": router(), "desk-channel": desk}); got != "" {
|
||||
t.Errorf("a channel proving nothing was held to it: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunsAsIsAnAccountOfTheModulesOwn(t *testing.T) {
|
||||
ok := Manifest{Module: "telegram", RunsAs: "telegram", SecretsOwner: "telegram",
|
||||
OwnSecrets: OwnSecrets{"broker": {Path: "/var/lib/telegram/broker"}},
|
||||
Resources: []map[string]any{{"id": "account", "type": "user", "name": "telegram"}}}
|
||||
if got := RunsAsProblems(ok); len(got) != 0 {
|
||||
t.Fatalf("a sound runs-as was refused: %v", got)
|
||||
}
|
||||
for want, change := range map[string]func(*Manifest){
|
||||
"never root": func(m *Manifest) { m.RunsAs, m.SecretsOwner = "root", "root" },
|
||||
"not an account name": func(m *Manifest) { m.RunsAs = "${machine:account}" },
|
||||
"which it does not make": func(m *Manifest) { m.Resources = nil },
|
||||
"declares no own secret": func(m *Manifest) { m.OwnSecrets = nil },
|
||||
"they are the account's own": func(m *Manifest) { m.SecretsOwner = "" },
|
||||
} {
|
||||
m := ok
|
||||
m.Resources = append([]map[string]any(nil), ok.Resources...)
|
||||
m.OwnSecrets = OwnSecrets{"broker": {Path: "/x"}}
|
||||
change(&m)
|
||||
if got := strings.Join(RunsAsProblems(m), "; "); !strings.Contains(got, want) {
|
||||
t.Errorf("want %q, got %q", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -642,6 +642,13 @@ type Manifest struct {
|
||||
// cannot use.
|
||||
SecretsOwner string `json:"secrets-owner,omitempty"`
|
||||
|
||||
// RunsAs is the account this module's tools bundle runs as, in a runtime of its own on a bus account of
|
||||
// its own (novox/hq ADR 0259 §8): never the machine's runtime, which runs as the operator's account and
|
||||
// carries every module on the machine. The account is one the module makes (a `user` resource of that
|
||||
// name), owns its secrets (`secrets-owner`), and is neither root nor the operator's. Required of a module
|
||||
// that says a warrant, or speaks for a channel kind that proves its sender.
|
||||
RunsAs string `json:"runs-as,omitempty"`
|
||||
|
||||
// Keeps is where this module wants every operator-sealed secret in the mesh written — the
|
||||
// vault's field, and so far nobody else's (novox/hq ADR 0085, amended).
|
||||
//
|
||||
@@ -1617,6 +1624,7 @@ func ParseManifest(raw []byte) (Manifest, error) {
|
||||
// prefix. Whether a seat anybody names exists, and whether a holder answers for it, are
|
||||
// facts about the catalogue and are checked at registration (CatalogueProblems).
|
||||
problems = append(problems, declaredSeatProblems(m)...)
|
||||
problems = append(problems, RunsAsProblems(m)...)
|
||||
if m.Computed != "" && len(m.Resources) > 0 {
|
||||
// One or the other. A module that both ships files and has them computed would leave
|
||||
// nobody able to say where a given file came from.
|
||||
|
||||
@@ -165,6 +165,10 @@ func (r Resolution) runtimeProcess(with Rendering) (map[string]any, error) {
|
||||
if with.Adopted && m.Filtering != nil {
|
||||
continue
|
||||
}
|
||||
if m.RunsAs != "" {
|
||||
// Served by a runtime of its own, on its own account (ownRuntimes): never the machine's.
|
||||
continue
|
||||
}
|
||||
words, err := bundleWords(m, with)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -223,6 +227,84 @@ func (r Resolution) runtimeProcess(with Rendering) (map[string]any, error) {
|
||||
return process, nil
|
||||
}
|
||||
|
||||
// OwnRuntimeID names the process a module of its own account is served by (novox/hq ADR 0259 §8).
|
||||
func OwnRuntimeID() string { return "own-runtime" }
|
||||
|
||||
// ownRuntimes are the processes the modules of their own account are served by (novox/hq ADR 0259 §8): each
|
||||
// the machine's runtime program — the same build, run from the same source — serving that one module alone,
|
||||
// as the module's own account, on the module's own bus credential. Never the machine's runtime, which runs
|
||||
// as the operator's account and carries every module on the machine.
|
||||
func (r Resolution) ownRuntimes(with Rendering) ([]map[string]any, error) {
|
||||
var own []Manifest
|
||||
for _, m := range r.Modules {
|
||||
if m.RunsAs != "" && !(with.Adopted && m.Filtering != nil) {
|
||||
own = append(own, m)
|
||||
}
|
||||
}
|
||||
if len(own) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var runtime *Manifest
|
||||
for i := range r.Modules {
|
||||
if r.Modules[i].Module == RuntimeModule {
|
||||
runtime = &r.Modules[i]
|
||||
}
|
||||
}
|
||||
if runtime == nil || len(runtime.Bundles) != 1 || runtime.Bundles[0].Binary == "" {
|
||||
return nil, fmt.Errorf("%s runs as its own account in a runtime of its own, and %s is not here to run it "+
|
||||
"from: assign %s to %s first (novox/hq ADR 0259)", own[0].Module, RuntimeModule, RuntimeModule, r.Node)
|
||||
}
|
||||
program := runtime.Bundles[0]
|
||||
var out []map[string]any
|
||||
for _, m := range own {
|
||||
credential, declared := m.OwnSecrets["broker"]
|
||||
if !declared {
|
||||
return nil, fmt.Errorf("%s runs as its own account and declares no own secret broker", m.Module)
|
||||
}
|
||||
var served, restartOn []string
|
||||
for _, b := range m.Bundles {
|
||||
for _, load := range b.Loads {
|
||||
if launcher, has := b.Launchers[load]; has {
|
||||
load = launcher
|
||||
}
|
||||
served = append(served, m.Module+"="+BundlePath(m.Module, b.Name)+"/"+load)
|
||||
}
|
||||
if len(b.Loads) > 0 {
|
||||
restartOn = append(restartOn, m.Module+"."+BundleID(b.Name))
|
||||
}
|
||||
}
|
||||
if len(served) == 0 {
|
||||
return nil, fmt.Errorf("%s runs as its own account and its build produced no bundle to serve", m.Module)
|
||||
}
|
||||
sort.Strings(served)
|
||||
restartOn = append(restartOn, m.Module+"."+NeedID("broker"))
|
||||
sort.Strings(restartOn)
|
||||
env := map[string]string{RuntimeToolModules: strings.Join(served, ","), RuntimeBrokerFile: credential.Path}
|
||||
words, err := bundleWords(m, with)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(words) > 0 {
|
||||
body, err := json.Marshal(map[string]map[string]string{m.Module: words})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
env[RuntimeToolEnv] = string(body)
|
||||
}
|
||||
process := map[string]any{
|
||||
"id": m.Module + "." + OwnRuntimeID(), "type": "process", "name": m.Module + "-runtime",
|
||||
"source": program.Source, "digest": program.Digest,
|
||||
"run": []any{"./" + program.Binary}, "env": env, "restart-on": toAny(restartOn),
|
||||
"user": m.RunsAs,
|
||||
}
|
||||
if err := artifactsInto(process, RuntimeModule, with); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, process)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func toAny(in []string) []any {
|
||||
out := make([]any, 0, len(in))
|
||||
for _, s := range in {
|
||||
|
||||
@@ -457,3 +457,49 @@ func TestAGoToolsBundleIsServedByItsBinary(t *testing.T) {
|
||||
t.Error("a Go bundle loading a file it does not contain was admitted")
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0259 §8: a module of its own account is served by a runtime of its own — the machine's
|
||||
// runtime program, as that account, on that module's own credential — and never by the machine's runtime,
|
||||
// which runs as the operator's account and is given none of its words.
|
||||
func TestAModuleOfItsOwnAccountIsServedByARuntimeOfItsOwn(t *testing.T) {
|
||||
with := Rendering{ArtifactStore: "anchor.internal:5101",
|
||||
Needed: map[string]map[string]string{RuntimeModule: {"broker": "sealed-credential"}, "telegram": {"broker": "own"}}}
|
||||
goRuntime := Manifest{Module: RuntimeModule, Version: "1",
|
||||
OwnSecrets: OwnSecrets{"broker": {Path: "/var/lib/mesh/" + RuntimeModule + "/broker"}},
|
||||
Build: &Build{Artifacts: []Artifact{{Name: "runtime", Kind: ArtifactBundle, Language: "go",
|
||||
System: "arch", From: "cmd/node-tools"}}}}
|
||||
goRuntime, err := goRuntime.Resolve([]Built{{Name: "runtime", Kind: ArtifactBundle,
|
||||
Reference: ArtifactStoreScheme + RuntimeModule + "/runtime/blobs/" + bundleDigest, Digest: bundleDigest}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
telegram := aToolsModule(t, "telegram", "tools/index.js")
|
||||
telegram.RunsAs, telegram.SecretsOwner = "telegram", "telegram"
|
||||
telegram.OwnSecrets = OwnSecrets{"broker": {Path: "/var/lib/telegram/broker"}}
|
||||
out, err := Resolution{Node: "anchor", Account: "ops",
|
||||
Modules: []Manifest{aToolsModule(t, "nftables", "tools/index.js"), telegram, goRuntime}}.Declaration(with)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
machine := fileNamed(out, RuntimeModule+"."+RuntimeProcessID())
|
||||
if served := machine["env"].(map[string]string)[RuntimeToolModules]; strings.Contains(served, "telegram") || !strings.Contains(served, "nftables") {
|
||||
t.Errorf("the machine's runtime serves %q", served)
|
||||
}
|
||||
own := fileNamed(out, "telegram."+OwnRuntimeID())
|
||||
if own == nil {
|
||||
t.Fatalf("telegram has no runtime of its own: %v", ids(out))
|
||||
}
|
||||
env := own["env"].(map[string]string)
|
||||
if own["user"] != "telegram" || fmt.Sprint(own["run"]) != "[./node-tools]" ||
|
||||
env[RuntimeBrokerFile] != "/var/lib/telegram/broker" ||
|
||||
env[RuntimeToolModules] != "telegram="+BundleRoot+"/telegram/tools/tools/index.js" {
|
||||
t.Errorf("its own runtime: user %v run %v env %v", own["user"], own["run"], env)
|
||||
}
|
||||
if _, told := env[RuntimeOperatorAccount]; told {
|
||||
t.Error("a runtime of a module's own account is told the operator's account")
|
||||
}
|
||||
// Without the machine's runtime to run it from, it is refused in words.
|
||||
if _, err := (Resolution{Node: "anchor", Modules: []Manifest{telegram}}).ownRuntimes(with); err == nil {
|
||||
t.Error("a module of its own account composed without a runtime program")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,6 +321,16 @@ func CatalogueProblems(shelf Shelf) []string {
|
||||
}
|
||||
}
|
||||
}
|
||||
// **A trusted holder runs as its own account** (novox/hq ADR 0259 §8): a module saying warrants, or
|
||||
// speaking for a kind that proves its sender, is never carried by a machine's runtime.
|
||||
for _, module := range shelfOrder(shelf) {
|
||||
m := shelf[module]
|
||||
if why := TrustedHolding(m, declared); why != "" && m.RunsAs == "" {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s must run as an account of its own (runs-as): %s, and the machine's runtime runs as the "+
|
||||
"operator's account, which every agent runs as (novox/hq ADR 0259)", module, why))
|
||||
}
|
||||
}
|
||||
// A read of a module's state that module does not keep (novox/hq ADR 0201) — said only where the
|
||||
// owner is on the shelf, as a consumer may be installed before its emitter.
|
||||
var manifests []Manifest
|
||||
@@ -416,3 +426,65 @@ func shelfOrder(shelf Shelf) []string {
|
||||
sort.Strings(out)
|
||||
return out
|
||||
}
|
||||
|
||||
var accountName = regexp.MustCompile(`^[a-z_][a-z0-9_-]{0,30}$`)
|
||||
|
||||
// RunsAsProblems is what one manifest's `runs-as` is held to (novox/hq ADR 0259 §8): an account of the
|
||||
// module's own making — a `user` resource of that name — that owns its secrets, with a bus account of its own,
|
||||
// and that is neither root nor the operator's.
|
||||
func RunsAsProblems(m Manifest) []string {
|
||||
if m.RunsAs == "" {
|
||||
return nil
|
||||
}
|
||||
var problems []string
|
||||
say := func(format string, a ...any) { problems = append(problems, fmt.Sprintf(format, a...)) }
|
||||
switch {
|
||||
case !accountName.MatchString(m.RunsAs):
|
||||
say("%s runs as %q, which is not an account name of the module's own", m.Module, m.RunsAs)
|
||||
return problems
|
||||
case m.RunsAs == "root":
|
||||
say("%s runs as root; a module of its own account runs as an account it makes, never root", m.Module)
|
||||
}
|
||||
made := false
|
||||
for _, r := range m.Resources {
|
||||
if fmt.Sprint(r["type"]) == "user" && fmt.Sprint(r["name"]) == m.RunsAs {
|
||||
made = true
|
||||
}
|
||||
}
|
||||
if !made {
|
||||
say("%s runs as %s, which it does not make: a user resource named %s", m.Module, m.RunsAs, m.RunsAs)
|
||||
}
|
||||
if _, has := m.OwnSecrets["broker"]; !has {
|
||||
say("%s runs as its own account and declares no own secret broker: its runtime reaches the bus on an "+
|
||||
"account of its own", m.Module)
|
||||
}
|
||||
if m.SecretsOwner != m.RunsAs {
|
||||
say("%s runs as %s, and its secrets belong to %q: they are the account's own", m.Module, m.RunsAs, m.SecretsOwner)
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// TrustedHolding is why a module must run as its own account (novox/hq ADR 0259 §8), or "": it holds a seat
|
||||
// whose events it says to one caller each (a warrant), or speaks for a kind of a kinded bench that proves
|
||||
// its sender. Neither may be carried by the machine's runtime, which runs as the operator's account.
|
||||
func TrustedHolding(m Manifest, declared map[string]SeatDeclaration) string {
|
||||
for _, c := range m.Claims {
|
||||
s, ok := declared[c.Name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, e := range s.Emits {
|
||||
if s.NamedByCaller(e) {
|
||||
return fmt.Sprintf("it holds %s, whose %s it says to one caller each", c.Name, e)
|
||||
}
|
||||
}
|
||||
if s.Kinded {
|
||||
for _, capability := range c.Capabilities {
|
||||
if capability == "verified-sender" {
|
||||
return fmt.Sprintf("it holds %s of kind %s, which proves its sender", c.Name, c.Kind)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -141,7 +141,7 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
|
||||
ev := strings.TrimSuffix(event, ".*")
|
||||
if catalogue.SeatSays(s.Emits, ev) {
|
||||
watches = append(watches, broker.Seat{Name: s.Name, Scope: s.Scope, Emits: []string{ev},
|
||||
Kinded: true, Proofs: s.Proofs})
|
||||
Kinded: true, Proofs: s.Proofs, DeclaredBy: declarers[s.Name]})
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -168,6 +168,8 @@ func declaredFor(m catalogue.Manifest, seats map[string]catalogue.SeatDeclaratio
|
||||
Reads: m.Reads,
|
||||
// And the tools its health asks (novox/hq ADR 0240): the machine's node-engine is granted them.
|
||||
Checks: catalogue.HealthChecks(m),
|
||||
// And whether it runs as an account of its own (novox/hq ADR 0259 §8).
|
||||
RunsAs: m.RunsAs,
|
||||
}
|
||||
// Whether it can be given an account at all: delivered as its own secret named broker, so one
|
||||
// that declares none has nowhere to read it (novox/hq issue 195).
|
||||
@@ -222,7 +224,8 @@ func (i *Inventory) DeclaredBuckets(ctx context.Context) ([]broker.Bucket, error
|
||||
|
||||
func asSeat(s catalogue.SeatDeclaration, declarer string) broker.Seat {
|
||||
seat := broker.Seat{Name: s.Name, Scope: s.Scope, Accepts: s.Accepts, Emits: s.Emits,
|
||||
Serves: catalogue.VerbNames(s.Serves), Kinded: s.Kinded, ByCaller: s.ByCaller, Proofs: s.Proofs}
|
||||
Serves: catalogue.VerbNames(s.Serves), Kinded: s.Kinded, ByCaller: s.ByCaller, Proofs: s.Proofs,
|
||||
DeclaredBy: declarer}
|
||||
// A seat's records are its declaring module's buckets, named as the bus holds them (ADR 0259 §3).
|
||||
for _, r := range s.Records {
|
||||
if declarer != "" {
|
||||
@@ -300,3 +303,22 @@ func heldHere(claimed []broker.Seat, holdings []catalogue.Held, node, module str
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// DeclaredTrafficSeats is every seat any registered module declares that names its caller or its kind
|
||||
// (novox/hq ADR 0259 §3), as the bus needs it: assigned or not, so its work queue exists from registration.
|
||||
func (i *Inventory) DeclaredTrafficSeats(ctx context.Context) ([]broker.Seat, error) {
|
||||
declared, err := i.Catalogue(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot read the catalogue: %w", err)
|
||||
}
|
||||
var out []broker.Seat
|
||||
for _, m := range declared {
|
||||
for _, s := range m.DefinesSeats {
|
||||
seat := asSeat(s, m.Module)
|
||||
if seat.Kinded || len(seat.ByCaller) > 0 {
|
||||
out = append(out, seat)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user