Give every module an identity and ask once to keep a shared folder (hq ADR 0325, issue 575)

A module's containers that declare identity run as an identity the controller allocates once, mesh-wide,
from 50000-59999; each access names its users and whether its folder is taken; an access wait is excused
for a folder the mesh may not keep yet, and the operator is asked once per folder and machine, the folder
taken only on the warrant. All of it is sent only to an engine at contract 5. A placement for an access a
newer build brings is judged against that build (issue 575). mesh-host pinned to the ADR 0325 branch head.
This commit is contained in:
2026-10-12 19:05:13 +02:00
parent f10aedac58
commit 63602264ef
35 changed files with 2671 additions and 31 deletions
+23 -2
View File
@@ -117,10 +117,13 @@ type asked struct {
// Proposal is a settings layer proposed through a verb (proposals.go, novox/hq ADR 0277): about no
// condition, set on Approve by the serving controller itself, and left alone by the reconciling of conditions.
Proposal *settingsProposal `json:"proposal,omitempty"`
// Take is a shared folder the operator is asked to let the mesh keep (kept_folders.go, novox/hq ADR 0325 §8):
// about no condition, taken on Approve by the serving controller itself, asked once per folder and machine.
Take *folderTake `json:"take,omitempty"`
}
// ofACondition says an ask is one of a condition's: not a rehearsal, not a proposal.
func (r asked) ofACondition() bool { return !r.Rehearsal && r.Proposal == nil }
func (r asked) ofACondition() bool { return !r.Rehearsal && r.Proposal == nil && r.Take == nil }
// partKey is an ask's place among what is asked: its condition and its part.
func partKey(condition, part string) string { return condition + "#" + part }
@@ -192,6 +195,11 @@ type asker struct {
call func(ctx context.Context, a conditions.Action, args map[string]string) error
// setLayer sets a proposed settings layer on the operator's warrant (novox/hq ADR 0277); nil cannot.
setLayer setOnWarrant
// folders is every shared folder waiting to be kept (novox/hq ADR 0325 §8), and take what takes one on the
// operator's warrant; nil asks about none. machines are the mesh's machine names, allowed in an ask's words.
folders func(ctx context.Context) ([]folderTake, error)
take takeOnWarrant
machines func(ctx context.Context) ([]string, error)
// record writes the hand-act log.
record func(ctx context.Context, act link.HandAct) error
// routerRecord reads the router's record of an ask for a warrant missed; nil reads nothing.
@@ -554,6 +562,10 @@ func (a *asker) reconcile(ctx context.Context) error {
return err
}
}
// And every shared folder waiting to be kept, once per folder and machine (novox/hq ADR 0325 §8).
if err := a.askFolders(ctx); err != nil {
a.logf("the operator could not be asked about the folders waiting to be kept: %v", err)
}
why := "the router refused the ask"
if len(refusedWords) > 0 {
why += ": " + refusedWords[0]
@@ -818,7 +830,7 @@ func (a *asker) Decided(ctx context.Context, body []byte) error {
if err != nil {
return err
}
stillOpen := r.Rehearsal || r.Proposal != nil // a rehearsal and a proposal are about no condition
stillOpen := r.Rehearsal || r.Proposal != nil || r.Take != nil // a rehearsal, a proposal and a take are about no condition
for _, c := range open {
stillOpen = stillOpen || c.Key == r.Condition
}
@@ -876,6 +888,15 @@ func (a *asker) Decided(ctx context.Context, body []byte) error {
if acted == nil && outcome != "" && !strings.HasPrefix(outcome, "nothing") {
outcome = "done: " + outcome
}
case r.Take != nil && act.Verb == takeVerb:
// A shared folder taken on the operator's warrant (novox/hq ADR 0325 §8): the act is composed again from the
// record and held to what the option bound before the engine is asked.
outcome, acted = a.decideTake(ctx, *r, act, w)
if acted == nil {
outcome = "done: " + outcome
}
case r.Take != nil:
acted = fmt.Errorf("the take %s offers no act %s", r.ID, act.Verb)
case act.Arguments["silence"] != "":
acted = a.silence(ctx, act.Arguments["silence"], conditions.MaxSilence, byWords(w), why)
default:
+21
View File
@@ -572,6 +572,27 @@ func servingAsker(open *stores, conn *nats.Conn, js jetstream.JetStream, keeper
call: callAction(conn),
// A proposed settings layer is set by this controller itself on the warrant (novox/hq ADR 0277).
setLayer: setLayerIn(open),
// A shared folder waiting to be kept is asked about once, and taken on the warrant (novox/hq ADR 0325 §8).
folders: foldersIn(open),
take: takeFolderIn(open, func(ctx context.Context, act link.AccessActAsk) (link.HandOverAnswer, error) {
ident, err := open.Identity(ctx)
if err != nil {
return link.HandOverAnswer{}, fmt.Errorf("the mesh's signing key cannot be read, so nothing was asked of "+
"%s: %w", act.Node, err)
}
return link.AskTake(ctx, conn, ident, act, link.TakeWithin)
}),
machines: func(ctx context.Context) ([]string, error) {
nodes, err := open.inventory.Nodes(ctx)
if err != nil {
return nil, err
}
names := make([]string, 0, len(nodes))
for _, n := range nodes {
names = append(names, n.Name)
}
return names, nil
},
record: func(ctx context.Context, act link.HandAct) error {
_, err := link.RecordHandAct(ctx, conn, act)
return err
+10
View File
@@ -152,6 +152,16 @@ func moduleCheckFor(paths []string, longestMachine int, out io.Writer) error {
}
}
// **What ADR 0325 refuses once a module runs as its own identity is warned of until then** (novox/hq ADR 0325 §1–§3):
// a directory naming its owner, a container declaring no identity that mounts the module's own directory or a
// read-write access. Printed, never counted: every registered container module does one of them today, and the
// catalogues migrate against these lines. A module that declares identity is refused them instead (ParseManifest).
for _, name := range names {
for _, w := range catalogue.IdentityWarnings(shelf[name]) {
fmt.Fprintf(out, "warning: %s\n", w)
}
}
// **A file that asks for a setting and does not say whether it is trusted counts as trusted** (novox/hq issue
// 339): listed and counted, never refused, so an author can opt out a file nothing trusts.
unsaid := 0
+354
View File
@@ -0,0 +1,354 @@
package main
// The mesh takes a shared folder that already holds data once, on the operator's word (novox/hq ADR 0325 §7, §8).
//
// **What waits.** A node-engine that keeps folders states an access waiting, naming it (inventory.Wait.Access), while
// its folder is there and has not been taken; the controller excuses that wait only for a declared, placed folder not
// yet taken (checkAccessWait), and the module is said as its needs-operator condition, in words with no path.
//
// **One ask per folder and machine.** The serving controller's asker asks the operator once for each such folder on
// each machine — however many modules wait on it — at the level approve (ADR 0259), in the clause's words: "Let the
// mesh keep <path> on <machine>, now for <modules>: …". It is kept in the controller's asks beside a proposal
// (asked.Take), never a condition's: the condition is the module's, and two modules waiting on one folder are one
// question. While it is open no second is asked; one that expired unanswered is asked again while the folder waits;
// one about a folder that no longer waits is taken back. None is asked for a folder already taken, so a module that
// joins a taken folder is asked nothing (its access wait is not excused, and its entries are added at apply).
//
// **On the warrant, and only on it**, the controller asks the machine's engine to take the folder (link.AskTake),
// naming the access by its resource id and the warrant; the engine adds the entries of the users it was declared for
// that folder. On its word that it started, the folder is recorded taken (kept_folder), and from the next send every
// access to it says `taken`. Whether the filesystem must first allow ACLs is the engine's to see and set: its wait
// does not say so to the controller, so the ask does not either.
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"path/filepath"
"sort"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
"github.com/novox/mesh-controller/internal/conditions"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
"github.com/novox/mesh-controller/internal/outward"
)
// folderTake is one folder the operator is asked to let the mesh keep, as the controller's ask keeps it
// (asked.Take). Every field the ask is composed from is here, so the act on the warrant is composed from the record.
type folderTake struct {
Node string `json:"node"`
Path string `json:"path"`
// Access is the resource id of one access to it in the machine's declaration: what the take act names.
Access string `json:"access"`
// Accesses are the access ids (each module's own) that place it, Modules the modules waiting on it, both sorted.
Accesses []string `json:"accesses"`
Modules []string `json:"modules"`
At time.Time `json:"at"`
}
// takeVerb is the verb a take's answer binds: the controller's own, performed by itself on the warrant.
const takeVerb = askerName + ".take"
// key is the folder on its machine: what one ask is kept for.
func (t folderTake) key() string { return t.Node + "\x00" + filepath.Clean(t.Path) }
// digest binds everything the take does and was said for: the machine, the folder, the access it names and the
// modules the operator was told.
func (t folderTake) digest() string {
raw, _ := json.Marshal(struct {
Node, Path, Access string
Modules []string
}{t.Node, filepath.Clean(t.Path), t.Access, t.Modules})
sum := sha256.Sum256(raw)
return "sha256:" + hex.EncodeToString(sum[:])
}
// actions is the take's one answer, at the level approve, binding the exact act through boundAct.
func (t folderTake) actions(id string) []conditions.Action {
return []conditions.Action{{Label: "Let the mesh keep it", Verb: takeVerb, Machine: t.Node,
Level: conditions.LevelApprove, Arguments: map[string]string{"take": id, "node": t.Node,
"path": filepath.Clean(t.Path), "access": t.Access, "digest": t.digest()}}}
}
// takeWords is the clause's ask (ADR 0325 §8), with the folder as shown: "Let the mesh keep <path> on <machine>, now
// for <modules>: …".
func takeWords(shown, machine string, modules []string) string {
return fmt.Sprintf("Let the mesh keep %s on %s, now for %s: it adds their access entries, and those of any "+
"module given access later, to every folder and file there; the group bits `ls -l` shows become the "+
"entries' mask, and owners, groups and every other right stay as they are", shown, machine,
strings.Join(modules, ", "))
}
// ask is the take's ask, composed from it alone. The folder's path is said whole where the router's content rule
// lets it leave the mesh, and otherwise named by the shared folder it is ("the shared folder movies"), which is the
// access the module's wait names.
func (t folderTake) ask(id string, machines []string) (asks.Ask, map[string]int) {
shown := filepath.Clean(t.Path)
if _, ok := outward.Check(shown, machines...); !ok {
shown = "the shared folder " + strings.Join(t.Accesses, ", ")
}
headline := fmt.Sprintf("Let the mesh keep a shared folder on %s?", t.Node)
if len([]rune(headline)) > asks.HeadlineLength {
headline = "Let the mesh keep a shared folder?"
}
explanation := takeWords(shown, t.Node, t.Modules) + "."
if _, ok := outward.Check(explanation, machines...); !ok {
explanation = sayable(explanation, machines)
}
q := asks.Ask{ID: id, Headline: headline, Explanation: explanation,
Who: asks.Operator, Expires: t.At.Add(askApproveFor),
OnExpiry: "nothing is changed, and you are asked again while the folder waits", About: "take." + t.Node}
options := map[string]int{}
for i, act := range t.actions(id) {
binds, _ := asks.ActDigest(boundAct(act))
oid := optionID(act.Label)
options[oid] = i
q.Options = append(q.Options, asks.Option{ID: oid, Label: act.Label, Level: asks.Level(act.Level), Binds: binds,
Does: "the mesh adds the access entries there, and keeps them from then on without asking"})
}
return q, options
}
// takeOnWarrant performs a take the operator approved: the engine is asked, and the folder recorded taken on its
// word. It answers what the engine said.
type takeOnWarrant func(ctx context.Context, t folderTake, askID, by string) (string, error)
// askFolders asks the operator about every folder waiting to be kept that is not asked about yet, and takes back an
// open ask about one that no longer waits. Called at the end of each reconcile, after the conditions' asks, within
// the asks the router holds open at once.
func (a *asker) askFolders(ctx context.Context) error {
if a.folders == nil {
return nil
}
wanted, err := a.folders(ctx)
if err != nil {
return err
}
now := a.now()
all, err := a.store.All(ctx)
if err != nil {
return err
}
open, openNow := map[string]asked{}, 0
for _, r := range all {
if r.State != askOpen || !now.Before(r.Ask.Expires) {
continue
}
openNow++
if r.Take != nil {
open[r.Take.key()] = r
}
}
want := map[string]bool{}
for _, t := range wanted {
want[t.key()] = true
}
for key, r := range open {
if !want[key] {
if err := a.cancel(ctx, r, "the folder no longer waits to be kept"); err != nil {
return err
}
openNow--
}
}
var machines []string
if a.machines != nil {
if machines, err = a.machines(ctx); err != nil {
return err
}
}
for _, t := range wanted {
if _, asking := open[t.key()]; asking {
continue // one ask per folder and machine (§8)
}
if openNow >= askMostOpen {
return nil // asked when one of the open ones ends
}
t.At = now
id := newAskID()
q, options := t.ask(id, machines)
if err := q.Check(now); err != nil {
a.logf("the operator could not be asked to let the mesh keep a folder on %s: %v", t.Node, err)
continue
}
body, err := json.Marshal(q)
if err != nil {
return err
}
take := t
if err := a.store.Create(ctx, asked{ID: id, Condition: q.About, Ask: q, Actions: t.actions(id),
Options: options, State: askOpen, Opened: now, Take: &take}); err != nil {
return fmt.Errorf("the take could not be kept in the controller's asks, so it was not asked: %w", err)
}
if err := a.publish(ctx, asks.AskSubject(askerName), body, "ask."+id); err != nil {
_, _ = a.store.Change(ctx, id, func(x *asked) bool {
if x.State != askOpen || x.Acted != "" {
return false
}
x.State, x.Ended, x.Acted = askUnsent, a.now(), "nothing: it could not be published: "+err.Error()
return true
})
a.logf("the operator could not be asked to let the mesh keep a folder on %s: %v", t.Node, err)
continue
}
openNow++
a.logf("asked the operator to let the mesh keep a folder on %s for %s (%s)", t.Node,
strings.Join(t.Modules, ", "), id)
}
return nil
}
// decideTake is what the asker does with a warrant for a take (asker.Decided): the act only when it is the one the
// option bound — composed again from the record, the users' digest among it — and then the engine is asked.
func (a *asker) decideTake(ctx context.Context, r asked, act conditions.Action, w asks.Warrant) (string, error) {
t := *r.Take
recomposed := t.actions(r.ID)
option, offered := r.Ask.Option(w.Option)
if !offered || len(recomposed) != 1 {
return "", fmt.Errorf("the take %s offers no answer %q: nothing is taken", r.ID, w.Option)
}
if err := option.Performs(boundAct(recomposed[0])); err != nil {
return "", fmt.Errorf("the take kept for %s is not the one the operator was shown: %v", r.ID, err)
}
if err := option.Performs(boundAct(act)); err != nil {
return "", fmt.Errorf("the act chosen for %s is not the one the operator was shown: %v", r.ID, err)
}
if a.take == nil {
return "", errors.New("this controller cannot take a folder on a warrant")
}
by := fmt.Sprintf("approved by %s via %s at %s (ask %s)", byWords(w), viaWords(w),
w.At.Local().Format("2006-01-02 15:04"), r.ID)
return a.take(ctx, t, r.ID, by)
}
// takeFolderIn is takeOnWarrant on this controller's stores and bus: the engine is asked with the mesh's key, naming the
// ask the warrant answered; on its word that it started, the folder is recorded taken. A refusal records nothing.
func takeFolderIn(open *stores, ask func(ctx context.Context, act link.AccessActAsk) (link.HandOverAnswer, error)) takeOnWarrant {
return func(ctx context.Context, t folderTake, askID, by string) (string, error) {
// Who asked, as one short line the engine takes (at most link.MaxHandOverBy): the ask names the rest.
short := by
if len(short) > 180 {
short = "the operator, on the warrant for ask " + askID
}
answer, err := ask(ctx, link.AccessActAsk{Node: t.Node, Path: filepath.Clean(t.Path), By: short,
Access: t.Access, Warrant: askID})
if err != nil {
return "", err
}
if answer.Refused != "" {
return "", fmt.Errorf("%s refused to take the folder: %s", t.Node, answer.Refused)
}
if err := open.inventory.RecordTaken(ctx, t.Node, inventory.TakenFolder{Path: t.Path, By: by, Ask: askID,
Warrant: askID, Said: answer.Said}); err != nil {
return "", fmt.Errorf("%s started the take (%s), and it could NOT be recorded: %w", t.Node, answer.Said, err)
}
return fmt.Sprintf("%s takes the folder for %s: %s", t.Node, strings.Join(t.Modules, ", "), answer.Said), nil
}
}
// waitingFolder is one access wait excused for a folder not yet taken, as foldersWaiting gathers them.
type waitingFolder struct {
module, access string
// resource is the access's resource id on the machine, as the engine stated it.
resource string
}
// foldersWaiting is every folder waiting to be kept across the mesh (ADR 0325 §7, §8): from each machine's newest
// statement, each access wait of a module that is excused (checkWaiting) and waits for the operator's word rather
// than for a disk, grouped by machine and folder.
func foldersWaiting(ctx context.Context, inv *inventory.Inventory, healths map[string]inventory.NodeHealth) (
map[string]map[string][]waitingFolder, operatorWaitFacts) {
var f operatorWaitFacts
out := map[string]map[string][]waitingFolder{}
machines := make([]string, 0, len(healths))
for m := range healths {
machines = append(machines, m)
}
sort.Strings(machines)
for _, machine := range machines {
rs := healths[machine].Resources
var modules []string
for _, r := range rs {
for _, w := range r.Waits {
if r.State == link.StateWaiting && w.Access != "" && !w.Disk {
modules = append(modules, r.Module)
}
}
}
if len(modules) == 0 {
continue
}
readWaitFacts(ctx, inv, machine, modules, nil, &f)
for _, r := range checkWaiting(machine, rs, f) {
if r.State != link.StateWaiting {
continue
}
for _, w := range r.Waits {
if w.Access == "" || w.Disk {
continue
}
path, placed := f.placed[r.Module+"@"+machine][w.Access]
if !placed {
continue
}
if out[machine] == nil {
out[machine] = map[string][]waitingFolder{}
}
out[machine][path] = append(out[machine][path], waitingFolder{module: r.Module, access: w.Access,
resource: r.Resource})
}
}
}
return out, f
}
// foldersIn is the asker's folders on this controller's stores: every folder waiting to be kept, each with the
// modules waiting on it and the access the take names (the first waiting module's, by its resource id). Who the
// take adds entries for is what the engine was declared for that folder: the ask names the modules.
func foldersIn(open *stores) func(ctx context.Context) ([]folderTake, error) {
return func(ctx context.Context) ([]folderTake, error) {
healths, err := open.inventory.Healths(ctx)
if err != nil {
return nil, err
}
waiting, _ := foldersWaiting(ctx, open.inventory, healths)
return takesOf(waiting), nil
}
}
// takesOf is one take per machine and folder from the waits gathered, sorted.
func takesOf(waiting map[string]map[string][]waitingFolder) []folderTake {
var out []folderTake
for machine, folders := range waiting {
for path, ws := range folders {
t := folderTake{Node: machine, Path: path}
seenM, seenA := map[string]bool{}, map[string]bool{}
var resources []string
for _, w := range ws {
if !seenM[w.module] {
seenM[w.module] = true
t.Modules = append(t.Modules, w.module)
}
if !seenA[w.access] {
seenA[w.access] = true
t.Accesses = append(t.Accesses, w.access)
}
resources = append(resources, w.resource)
}
sort.Strings(t.Modules)
sort.Strings(t.Accesses)
sort.Strings(resources)
t.Access = resources[0]
out = append(out, t)
}
}
sort.Slice(out, func(i, j int) bool { return out[i].key() < out[j].key() })
return out
}
+360
View File
@@ -0,0 +1,360 @@
package main
import (
"context"
"crypto/ed25519"
"encoding/json"
"errors"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/asks"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/link"
)
// novox/hq ADR 0325 §7 and §8 on the controller's side: an access wait is excused only for a declared, placed folder
// not yet taken, or one whose disk is not there; the gate passes it as a wait for a person and the operator reads it
// as needs-operator in words with no path; the operator is asked once per folder and machine, never about a folder
// taken; and the folder is taken only on the warrant.
var radarrManifest = catalogue.Manifest{Module: "radarr", Version: "1",
Accesses: []catalogue.Access{{ID: "movies", Path: "/services/media/movies", Mode: catalogue.AccessReadWrite}}}
var plexManifest = catalogue.Manifest{Module: "plex", Version: "1",
Accesses: []catalogue.Access{{ID: "films", Path: "/services/media/movies", Mode: catalogue.AccessRead}}}
const moviesAt = "/storage/media/movies"
func keepWait(access string) inventory.Wait {
return inventory.Wait{Part: "radarr", Access: access, What: "let the mesh keep the shared folder " + access}
}
func diskWait(access string) inventory.Wait {
return inventory.Wait{Part: "radarr", Access: access, Disk: true, What: "mount the disk that holds " + access}
}
func accessFacts(taken ...string) operatorWaitFacts {
kept := map[string]bool{}
for _, p := range taken {
kept[p] = true
}
return operatorWaitFacts{
manifests: map[string]catalogue.Manifest{"radarr": radarrManifest, "plex": plexManifest},
given: map[string]map[string]time.Time{"radarr@ace": {}, "plex@ace": {}},
placed: map[string]map[string]string{"radarr@ace": {"movies": moviesAt}, "plex@ace": {"films": moviesAt},
"radarr@novox": {}},
taken: map[string]map[string]bool{"ace": kept},
}
}
// Row 7: excused for a declared, placed folder not yet taken, and for an absent folder whose disk is not there (taken
// or not); judged unhealthy when undeclared, unplaced, already taken, or naming a secret beside it. That a folder is
// root's is the engine's to see: it states that unhealthy and never waiting (§12).
func TestAnAccessWaitIsExcusedOnlyForAFolderTheMeshMayNotKeepYet(t *testing.T) {
for _, c := range []struct {
name string
machine string
w inventory.Wait
f operatorWaitFacts
says string
}{
{"declared, placed, not taken", "ace", keepWait("movies"), accessFacts(), ""},
{"its disk not there", "ace", diskWait("movies"), accessFacts(), ""},
{"its disk not there, the folder taken before", "ace", diskWait("movies"), accessFacts(moviesAt), ""},
{"already taken", "ace", keepWait("movies"), accessFacts(moviesAt), "taken on ace already"},
{"undeclared", "ace", keepWait("books"), accessFacts(), "does not declare"},
{"unplaced", "novox", keepWait("movies"), accessFacts(), "not placed on novox"},
{"taken folders unread", "ace", keepWait("movies"), operatorWaitFacts{manifests: accessFacts().manifests,
placed: accessFacts().placed}, "could not be read"},
{"an access and a secret", "ace", inventory.Wait{Part: "p", Access: "movies", Secret: "x", What: "w"},
accessFacts(), "one secret or one setting, or one access"},
} {
err := checkWait("radarr", c.machine, c.w, c.f)
switch {
case c.says == "" && err != nil:
t.Errorf("%s: refused: %v", c.name, err)
case c.says != "" && (err == nil || !strings.Contains(err.Error(), c.says)):
t.Errorf("%s: %v; want a refusal saying %q", c.name, err, c.says)
}
}
}
func accessWaiting(module, access string, w inventory.Wait) inventory.ResourceHealth {
return inventory.ResourceHealth{Module: module, Resource: module + "." + catalogue.AccessID("/services/media/movies"),
Kind: "access", Target: moviesAt, State: link.StateWaiting, Waits: []inventory.Wait{w}}
}
// Row 7, the gate and the condition: both excused waits pass the first-node gate as a wait for a person, and the
// module is said as needs-operator — in words that carry no path.
func TestAnAccessWaitPassesTheGateAndAsksForTheOperatorWithoutAPath(t *testing.T) {
now := time.Now()
for _, w := range []inventory.Wait{keepWait("movies"), diskWait("movies")} {
w.What += " at " + moviesAt // an engine's words carrying a path never reach the operator's
f := gateFacts{now: now, waits: accessFacts(), groupsAdded: map[string]bool{"radarr": false},
health: map[string]inventory.NodeHealth{"ace": {Node: "ace", HeardAt: now,
Resources: []inventory.ResourceHealth{accessWaiting("radarr", "movies", w)}}}}
h, why := moduleHealthWord("radarr", "ace", now.Add(-time.Minute), f)
if h != healthPerson || !strings.Contains(why, "waits for the operator") {
t.Fatalf("an excused access wait reads %v %q; want a wait for a person", h, why)
}
if strings.Contains(why, "/") {
t.Errorf("the gate's verdict carries a path: %q", why)
}
o := needsOperatorObservation("radarr", "ace", []inventory.Wait{w}, nil)
if o.Kind != kindNeedsOperator {
t.Fatalf("raised as %s", o.Kind)
}
for _, words := range []string{o.Summary, o.Headline, o.Explanation, o.Needs} {
if strings.Contains(words, "/") {
t.Errorf("the operator reads a path: %q", words)
}
}
want := "let the mesh keep the shared folder movies on ace when you are asked"
if w.Disk {
want = "mount the disk that holds movies on ace"
}
if !strings.Contains(o.Needs, want) {
t.Errorf("needs-operator asks %q; want %q", o.Needs, want)
}
}
// Taken already: not excused, and the gate does not pass it.
f := gateFacts{now: now, waits: accessFacts(moviesAt), groupsAdded: map[string]bool{"radarr": false},
health: map[string]inventory.NodeHealth{"ace": {Node: "ace", HeardAt: now,
Resources: []inventory.ResourceHealth{accessWaiting("radarr", "movies", keepWait("movies"))}}}}
if h, why := moduleHealthWord("radarr", "ace", now.Add(-time.Minute), f); h != healthNotYet ||
!strings.Contains(why, "taken") {
t.Fatalf("a wait on a taken folder reads %v %q", h, why)
}
}
// The engine's access-waits are kept as waits naming the access, and its words tell a disk from a keep.
func TestTheEnginesAccessWaitsAreReadAsWaits(t *testing.T) {
for what, disk := range map[string]bool{"let the mesh keep the shared folder movies": false,
"mount the disk that holds books": true} {
if got := (link.AccessWait{Kind: link.WaitKindAccess, Access: "movies", Part: "radarr", What: what}).WaitsForDisk(); got != disk {
t.Errorf("%q waits for a disk: %v", what, got)
}
}
}
// waitingOn is foldersWaiting's gathering for one folder: the given modules waiting on it by their access.
func waitingOn(machine string, ws ...waitingFolder) map[string]map[string][]waitingFolder {
return map[string]map[string][]waitingFolder{machine: {moviesAt: ws}}
}
func takeRig(t *testing.T, waiting func() map[string]map[string][]waitingFolder) (*askerRig, *[]string) {
r := newAskerRig(t)
var taken []string
r.a.folders = func(context.Context) ([]folderTake, error) { return takesOf(waiting()), nil }
r.a.machines = func(context.Context) ([]string, error) { return []string{"ace", "novox"}, nil }
r.a.take = func(_ context.Context, ft folderTake, askID, by string) (string, error) {
taken = append(taken, ft.Node+" "+ft.Path+" "+ft.Access+" "+askID+" "+by)
return "taking", nil
}
return r, &taken
}
func takeAsks(r *askerRig) []asked {
var out []asked
for _, a := range r.store {
if a.Take != nil {
out = append(out, a)
}
}
return out
}
// Row 8, the controller's part: one ask per folder and machine however many modules wait on it, in the clause's
// words; none again while it is open; taken back when the folder no longer waits; none for a folder taken (its waits
// are not excused, so it is never gathered).
func TestTheOperatorIsAskedOncePerFolderAndMachine(t *testing.T) {
both := waitingOn("ace",
waitingFolder{module: "radarr", access: "movies", resource: "radarr.access-services/media/movies"},
waitingFolder{module: "plex", access: "films", resource: "plex.access-services/media/movies"})
current := both
r, _ := takeRig(t, func() map[string]map[string][]waitingFolder { return current })
ctx := context.Background()
for i := 0; i < 3; i++ {
if err := r.a.reconcile(ctx); err != nil {
t.Fatal(err)
}
}
kept := takeAsks(r)
if len(kept) != 1 {
t.Fatalf("%d asks for one folder on one machine", len(kept))
}
q := kept[0].Ask
if err := q.Check(r.now); err != nil {
t.Fatal(err)
}
if q.Highest() != asks.Approve || len(q.Options) != 1 {
t.Fatalf("the ask offers %+v; want one answer at approve", q.Options)
}
// The path may not leave the mesh (the router's content rule): the folder is named by the shared folder it is.
want := "Let the mesh keep the shared folder films, movies on ace, now for plex, radarr: it adds their access " +
"entries, and those of any module given access later, to every folder and file there; the group bits `ls -l` " +
"shows become the entries' mask, and owners, groups and every other right stay as they are."
if q.Explanation != want {
t.Errorf("the ask says\n %q\nwant\n %q", q.Explanation, want)
}
if kept[0].Take.Access != "plex.access-services/media/movies" {
t.Errorf("the take names %q", kept[0].Take.Access)
}
// The folder no longer waits (taken, or no module uses it): the open ask is taken back, and nothing is asked.
current = nil
if err := r.a.reconcile(ctx); err != nil {
t.Fatal(err)
}
if got := takeAsks(r); len(got) != 1 || got[0].State != askCancelled {
t.Fatalf("the ask about a folder that no longer waits: %+v", got)
}
if err := r.a.reconcile(ctx); err != nil {
t.Fatal(err)
}
if len(takeAsks(r)) != 1 {
t.Fatal("asked about a folder nothing waits on")
}
// A module joining a folder already taken is never gathered: its wait is not excused.
f := accessFacts(moviesAt)
if err := checkWait("radarr", "ace", keepWait("movies"), f); err == nil {
t.Fatal("a wait on a taken folder was excused, and would have been asked about")
}
}
// Row 8: nothing is taken without the operator's warrant — an ask that ends without a choice, a warrant for another
// ask, or an act other than the one the option bound takes nothing; the warrant takes it once.
func TestAFolderIsTakenOnlyOnTheWarrant(t *testing.T) {
r, taken := takeRig(t, func() map[string]map[string][]waitingFolder {
return waitingOn("ace", waitingFolder{module: "radarr", access: "movies", resource: "radarr.access-movies"})
})
ctx := context.Background()
if err := r.a.reconcile(ctx); err != nil {
t.Fatal(err)
}
a := takeAsks(r)[0]
if len(*taken) != 0 {
t.Fatal("taken before any warrant")
}
// A warrant for another asker's ask.
other := warrantOn(a, "let-the-mesh-keep-it", r.now)
other.Asker = "someone-else"
answerWith(t, r, other)
// The record changed after the ask: the act no longer the one bound.
changed := r.store[a.ID]
changed.Take = &folderTake{Node: "ace", Path: "/", Access: "radarr.access-movies", Modules: a.Take.Modules}
r.store[a.ID] = changed
answerWith(t, r, warrantOn(a, "let-the-mesh-keep-it", r.now))
if len(*taken) != 0 || !strings.HasPrefix(r.store[a.ID].Acted, "failed") {
t.Fatalf("a take whose record changed after the ask: %v, %q", *taken, r.store[a.ID].Acted)
}
// Asked afresh, and the ask expires unanswered: nothing.
r2, taken2 := takeRig(t, func() map[string]map[string][]waitingFolder {
return waitingOn("ace", waitingFolder{module: "radarr", access: "movies", resource: "radarr.access-movies"})
})
if err := r2.a.reconcile(ctx); err != nil {
t.Fatal(err)
}
b := takeAsks(r2)[0]
expired := asks.Warrant{Ask: b.ID, Asker: "mesh-controller", Outcome: asks.OutcomeExpired, At: r2.now}
answerWith(t, r2, expired)
if len(*taken2) != 0 {
t.Fatal("taken on an ask that expired")
}
// The warrant: taken once, naming the access, the ask as the warrant, and who approved.
r3, taken3 := takeRig(t, func() map[string]map[string][]waitingFolder {
return waitingOn("ace", waitingFolder{module: "radarr", access: "movies", resource: "radarr.access-movies"})
})
if err := r3.a.reconcile(ctx); err != nil {
t.Fatal(err)
}
c := takeAsks(r3)[0]
w := warrantOn(c, "let-the-mesh-keep-it", r3.now)
answerWith(t, r3, w)
answerWith(t, r3, w)
if len(*taken3) != 1 || !strings.Contains((*taken3)[0], "ace "+moviesAt+" radarr.access-movies "+c.ID) ||
!strings.Contains((*taken3)[0], "telegram") {
t.Fatalf("the warrant took %v", *taken3)
}
if !strings.HasPrefix(r3.store[c.ID].Acted, "done") {
t.Fatalf("the take was recorded %q", r3.store[c.ID].Acted)
}
}
// The engine is asked only on a warrant, and the ask carries it: one with no warrant is never signed.
func TestATakeAskCarriesItsWarrantAndIsSigned(t *testing.T) {
_, key, _ := ed25519.GenerateKey(nil)
signer := keySigner{key}
ask := link.AccessActAsk{Node: "ace", Path: moviesAt, By: "the operator", Access: "radarr.access-services/media/movies"}
if _, err := link.SignTake(context.Background(), signer, ask); err == nil {
t.Fatal("a take with no warrant was signed")
}
ask.Warrant = "c0123456789abcdef"
body, err := link.SignTake(context.Background(), signer, ask)
if err != nil {
t.Fatal(err)
}
var signed link.SignedHandOver
if err := json.Unmarshal(body, &signed); err != nil {
t.Fatal(err)
}
if !ed25519.Verify(key.Public().(ed25519.PublicKey), append([]byte(link.AccessActContext), signed.Ask...), signed.Signature) {
t.Fatal("the take is not signed over its own context")
}
var got map[string]any
if err := json.Unmarshal(signed.Ask, &got); err != nil {
t.Fatal(err)
}
if got["act"] != "take" || got["warrant"] != "c0123456789abcdef" || got["access"] != ask.Access ||
got["path"] != moviesAt || got["nonce"] == "" {
t.Fatalf("the ask carries %v", got)
}
// And the controller records it taken only on the engine's word.
refusing := takeFolderIn(nil, func(context.Context, link.AccessActAsk) (link.HandOverAnswer, error) {
return link.HandOverAnswer{Refused: "the disk that holds the folder is not there"}, nil
})
if _, err := refusing(context.Background(), folderTake{Node: "ace", Path: moviesAt}, "c01", "the operator"); err == nil {
t.Fatal("a refused take was answered as done")
}
unreachable := takeFolderIn(nil, func(context.Context, link.AccessActAsk) (link.HandOverAnswer, error) {
return link.HandOverAnswer{}, errors.New("nothing on ace answers")
})
if _, err := unreachable(context.Background(), folderTake{Node: "ace", Path: moviesAt}, "c01", "the operator"); err == nil {
t.Fatal("an unanswered take was answered as done")
}
}
type keySigner struct{ key ed25519.PrivateKey }
func (s keySigner) Sign(_ context.Context, b []byte) ([]byte, error) {
return ed25519.Sign(s.key, b), nil
}
// The envelope carries `identities` only when composed — for an engine at link.IdentityContract — and an older
// engine's body is what it was.
func TestTheDeclarationSaysItsIdentitiesOnlyWhenComposed(t *testing.T) {
older, err := sendable{Resources: []map[string]any{{"id": "a"}}}.Body()
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(older), "identities") {
t.Fatalf("an older engine's body names identities: %s", older)
}
newer, err := sendable{Resources: []map[string]any{{"id": "a"}}, Identities: []map[string]any{
{"module": "radarr", "name": "mesh-radarr", "uid": 50000, "gid": 50000}}}.Body()
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(newer), `"identities":[{"gid":50000,"module":"radarr","name":"mesh-radarr","uid":50000}]`) {
t.Fatalf("the body: %s", newer)
}
if link.IdentityContract != 5 {
t.Fatalf("the contract the engine states for ADR 0325 is 5, and this controller reads %d", link.IdentityContract)
}
}
+6
View File
@@ -77,6 +77,12 @@ func stateHealth(ctx context.Context, inv *inventory.Inventory, k *conditions.Ke
for _, w := range r.Waits {
kept.Waits = append(kept.Waits, inventory.Wait{Part: w.Part, Secret: w.Secret, Setting: w.Setting, What: w.What})
}
// An access's waits (novox/hq ADR 0325 §7), kept beside the others as one more thing the operator gives: the
// access by its module's id, and whether it waits for a disk rather than for the operator's word.
for _, w := range r.AccessWaits {
kept.Waits = append(kept.Waits, inventory.Wait{Part: w.Part, Access: w.Access, Disk: w.WaitsForDisk(),
What: w.What})
}
resources = append(resources, kept)
}
// **A wait for the operator is checked before it is excused** (novox/hq ADR 0283): a waiting resource whose
+9
View File
@@ -487,6 +487,15 @@ func settingsCommand(ctx context.Context, args []string) error {
if err := inv.SetSettingsBy(ctx, *node, positionals[0], values, setByWords()); err != nil {
return err
}
// An owner a placement names is warned of (novox/hq ADR 0325 §1): accepted while registered modules still
// name one, refused once they have moved.
if shelf, err := inv.Catalogue(ctx); err == nil {
if m, known := shelf[positionals[0]]; known {
for _, w := range catalogue.PlacesOwnerWarnings(m, []catalogue.Layer{{From: where, Values: values}}) {
fmt.Println(w)
}
}
}
fmt.Printf("%s on %s:\n", positionals[0], where)
if len(added)+len(changed)+len(removed) == 0 {
fmt.Println(" nothing changed")
+94 -2
View File
@@ -44,6 +44,11 @@ func needsOperatorKey(module, node string) string {
type operatorWaitFacts struct {
manifests map[string]catalogue.Manifest
given map[string]map[string]time.Time
// placed is, per "<module>@<machine>", where each access of the module is on that machine, by id (novox/hq ADR
// 0325 §7); taken is, per machine, every folder the operator let the mesh keep there. A machine absent from
// taken could not be read, and no access wait of it is excused.
placed map[string]map[string]string
taken map[string]map[string]bool
}
// checkWait is nil when a wait is excused, and otherwise why not, in words. Pure.
@@ -52,9 +57,17 @@ func checkWait(module, machine string, w inventory.Wait, f operatorWaitFacts) er
if !known {
return fmt.Errorf("says it waits for %s, and the mesh holds no manifest of %s to check it against", waitNames(w), module)
}
named := 0
for _, n := range []string{w.Secret, w.Setting, w.Access} {
if n != "" {
named++
}
}
switch {
case w.Secret != "" && w.Setting != "", w.Secret == "" && w.Setting == "":
return fmt.Errorf("says it waits, naming %s, where a wait names one secret or one setting", waitNames(w))
case named != 1:
return fmt.Errorf("says it waits, naming %s, where a wait names one secret or one setting, or one access", waitNames(w))
case w.Access != "":
return checkAccessWait(module, machine, m, w, f)
case w.Setting != "":
if _, declared := m.Settings[w.Setting]; !declared {
return fmt.Errorf("says it waits for the setting %s, which %s does not declare", w.Setting, module)
@@ -80,6 +93,39 @@ func checkWait(module, machine string, w inventory.Wait, f operatorWaitFacts) er
return nil
}
// checkAccessWait is nil when an access wait is excused (novox/hq ADR 0325 §7): the module's manifest declares the
// access, its folder is placed on that machine, and either the folder is absent and its disk is not there (§9), or
// it has not yet been taken under the operator's warrant (§8). That a folder that is there is not owned by root or a
// root group is the node-engine's to see: it states such an access unhealthy and never waiting (§12). Any other
// access wait is judged unhealthy, saying why: a module that gains an access to a folder already taken is given its
// entries without a wait.
func checkAccessWait(module, machine string, m catalogue.Manifest, w inventory.Wait, f operatorWaitFacts) error {
declared := false
for _, a := range m.Accesses {
declared = declared || a.ID == w.Access
}
if !declared {
return fmt.Errorf("says it waits for the access %s, which %s does not declare", w.Access, module)
}
path, placed := f.placed[module+"@"+machine][w.Access]
if !placed {
return fmt.Errorf("says it waits for the access %s, which is not placed on %s", w.Access, machine)
}
if w.Disk {
return nil
}
taken, readable := f.taken[machine]
if !readable {
return fmt.Errorf("says it waits for the access %s, and which folders are taken on %s could not be read",
w.Access, machine)
}
if taken[path] {
return fmt.Errorf("says it waits for the access %s, whose folder is taken on %s already: a module that gains "+
"an access to a taken folder is given its entries without a wait", w.Access, machine)
}
return nil
}
// waitRefusedPrefix opens every reason checkWait gives, so the words of a refused wait are told from a check's own.
const waitRefusedPrefix = "says it waits"
@@ -88,6 +134,10 @@ func waitNames(w inventory.Wait) string {
switch {
case w.Secret != "" && w.Setting != "":
return "the secret " + w.Secret + " and the setting " + w.Setting
case w.Access != "" && (w.Secret != "" || w.Setting != ""):
return "the access " + w.Access + " and a secret or a setting"
case w.Access != "":
return "the access " + w.Access
case w.Secret != "":
return "the secret " + w.Secret
case w.Setting != "":
@@ -147,6 +197,14 @@ func operatorWaitSaid(module, machine string, waits []inventory.Wait) string {
var parts []string
for _, w := range waits {
part := fmt.Sprintf("%s (%s", w.What, waitNames(w))
// An access in the controller's own words, never the engine's: words with no path (novox/hq ADR 0325 §7).
switch {
case w.Access != "" && w.Disk:
part = fmt.Sprintf("the disk that holds %s mounted (%s", w.Access, waitNames(w))
case w.Access != "":
part = fmt.Sprintf("leave to keep the shared folder %s (%s, asked of you once for the folder", w.Access,
waitNames(w))
}
if w.Secret != "" {
part += fmt.Sprintf(", given with `nox secret ask %s %s %s`", machine, module, w.Secret)
}
@@ -180,6 +238,11 @@ func needsOperatorWords(module, node string, waits []inventory.Wait) words {
act, secret = fmt.Sprintf("type %s at %s's desk prompt", w.What, node), true
case w.Setting != "":
act = fmt.Sprintf("approve %s of %s on %s when it is proposed to you", w.Setting, module, node)
// An access, in words with no path (novox/hq ADR 0325 §7): the folder by the access's name.
case w.Access != "" && w.Disk:
act = fmt.Sprintf("mount the disk that holds %s on %s", w.Access, node)
case w.Access != "":
act = fmt.Sprintf("let the mesh keep the shared folder %s on %s when you are asked", w.Access, node)
}
if act != "" && !seen[act] {
seen[act] = true
@@ -217,6 +280,12 @@ func readWaitFacts(ctx context.Context, inv *inventory.Inventory, machine string
if f.given == nil {
f.given = map[string]map[string]time.Time{}
}
if f.placed == nil {
f.placed = map[string]map[string]string{}
}
if f.taken == nil {
f.taken = map[string]map[string]bool{}
}
if inv == nil {
return
}
@@ -238,6 +307,20 @@ func readWaitFacts(ctx context.Context, inv *inventory.Inventory, machine string
}
}
}
// Where each access of it is placed there (ADR 0325 §7): read once per module and machine.
if m, known := f.manifests[module]; known && len(m.Accesses) > 0 {
if _, read := f.placed[module+"@"+machine]; !read {
if layers, err := inv.SettingsFor(ctx, machine, module); err == nil {
placed := map[string]string{}
for _, a := range m.Accesses {
if path, has := catalogue.AccessPathOn(m, layers, a.ID); has && a.ID != "" {
placed[a.ID] = path
}
}
f.placed[module+"@"+machine] = placed
}
}
}
if _, read := f.given[module+"@"+machine]; read {
continue
}
@@ -245,6 +328,15 @@ func readWaitFacts(ctx context.Context, inv *inventory.Inventory, machine string
f.given[module+"@"+machine] = given
}
}
if _, read := f.taken[machine]; !read {
if taken, err := inv.KeptFolders(ctx, machine); err == nil {
kept := map[string]bool{}
for path := range taken {
kept[path] = true
}
f.taken[machine] = kept
}
}
}
// waitingModules is every module with a waiting resource in a statement.
+60 -1
View File
@@ -447,7 +447,7 @@ func declarationWith(ctx context.Context, open *stores, node string,
if err != nil {
return sendable{}, err
}
out := sendable{Resources: composed.Resources, Adoption: adoption,
out := sendable{Resources: composed.Resources, Adoption: adoption, Identities: composed.Identities,
Received: composed.Received, Mesh: with.Mesh, BusUsers: with.BusUsers,
LeftOut: sortedKeysOf(composed.LeftOut), leftOutWhy: composed.LeftOut, withheld: with.Withheld,
unbound: with.Unbound, foreseen: composed.Foreseen, unplaced: composed.Unplaced,
@@ -975,9 +975,17 @@ func renderingFor(ctx context.Context, open *stores, node string,
if err != nil {
return catalogue.Rendering{}, inventory.Node{}, err
}
// Each module's identity and the folders taken here (novox/hq ADR 0325 §3, §8), to an engine that keeps folders.
keepsFolders, identities, keptFolders, err := folderFacts(ctx, inv, node, plan, choosing)
if err != nil {
return catalogue.Rendering{}, inventory.Node{}, err
}
return catalogue.Rendering{
ReadsHealth: readsHealth,
JudgesRoot: judgesRoot,
KeepsFolders: keepsFolders,
Identities: identities,
KeptFolders: keptFolders,
BusMembership: memberships[node],
Settings: settings, Generators: gens, Grants: grants, Needed: needed, Foreseen: foreseen, Ports: ports,
Certificate: certificate, Authority: authority, Mesh: private, Names: names,
@@ -1010,6 +1018,57 @@ func engineJudgesRoot(ctx context.Context, inv *inventory.Inventory, node string
return had && stated.Contract >= link.RootContract, nil
}
// engineKeepsFolders says whether a machine's node-engine gives modules their identities and keeps shared folders
// (novox/hq ADR 0325), by its own newest statement, for the same reason as engineReadsHealth: an older engine parses
// strictly, and is sent none of it.
func engineKeepsFolders(ctx context.Context, inv *inventory.Inventory, node string) (bool, error) {
stated, had, err := inv.HealthOf(ctx, node)
if err != nil {
return false, err
}
return had && stated.Contract >= link.IdentityContract, nil
}
// folderFacts is what a machine's composition needs of ADR 0325: whether its engine keeps folders, each identity of
// a module of it that declares one, and the folders taken there. **An identity is allocated only on the send path**
// (Allocating), as a port is: a question about what would be sent reads what was allocated, and a module that has
// none yet is composed without one. Nothing is read for an engine that does not keep folders.
func folderFacts(ctx context.Context, inv *inventory.Inventory, node string, plan catalogue.Resolution,
choosing Choosing) (bool, map[string]catalogue.ModuleIdentity, map[string]bool, error) {
keeps, err := engineKeepsFolders(ctx, inv, node)
if err != nil || !keeps {
return false, nil, nil, err
}
identities := map[string]catalogue.ModuleIdentity{}
for _, m := range plan.Modules {
if !catalogue.DeclaresIdentity(m) {
continue
}
var id catalogue.ModuleIdentity
had := true
if choosing == Allocating {
id, err = inv.AllocateIdentity(ctx, m.Module)
} else {
id, had, err = inv.IdentityOf(ctx, m.Module)
}
if err != nil {
return false, nil, nil, err
}
if had {
identities[m.Module] = id
}
}
taken, err := inv.KeptFolders(ctx, node)
if err != nil {
return false, nil, nil, err
}
kept := map[string]bool{}
for path := range taken {
kept[path] = true
}
return true, identities, kept, nil
}
// zonesInTheMesh is every zone a module in the mesh declares, where the mesh placed it (novox/hq ADR
// 0199): the zone settled from that node's settings, the node's private address, the port the
// answering listen is published on there.
+6
View File
@@ -18,6 +18,9 @@ import (
// make a machine look out of date for ever, or send something `plan` never showed.
type sendable struct {
Resources []map[string]any
// Identities is the declaration's `identities` (novox/hq ADR 0325 §3): composed only for an engine that keeps
// folders, and not sent at all when empty — an older engine refuses a key it does not know, whole.
Identities []map[string]any
// Sequence orders this send against every other to the same node: one higher each time, taken
// under the node's hold just before the body is made (novox/hq 04-ISSUES/107). Zero is not sent
// at all, which a host reads as "no order claimed" — the shape of every declaration before this.
@@ -110,6 +113,9 @@ func (s sendable) Body() ([]byte, error) {
if s.Generation > 0 {
envelope["generation"] = s.Generation
}
if len(s.Identities) > 0 {
envelope["identities"] = s.Identities
}
// An empty declaration is deliberate here — the node owns nothing the mesh put there
// (novox/hq issue 127) — and the host refuses an empty body unless it is told the emptiness
// is meant, so a truncated or mis-composed body is never mistaken for "own nothing".