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
+51 -5
View File
@@ -246,6 +246,21 @@ type Rendering struct {
// statement's contract is link.RootContract or later). To an older, strict engine the field is not
// sent, and the account it names is not judged — which the self-check says, as not judged.
JudgesRoot bool
// KeepsFolders says this machine's node-engine gives modules their identities and keeps shared folders
// (novox/hq ADR 0325: its statement's contract is link.IdentityContract or later). To an older, strict engine none of
// it is sent — no `identities`, no container `identity`, no access `users` or `taken`, no owner derived from an
// identity — and what it runs is composed as before.
KeepsFolders bool
// Identities is each module's allocated identity, by module (ADR 0325 §3): the same on every machine. A module
// that declares `identity` and has none here is composed without one, as to an older engine.
Identities map[string]ModuleIdentity
// KeptFolders is every folder the operator let the mesh keep on this machine (ADR 0325 §8), by its clean path.
KeptFolders map[string]bool
// AccountUIDs is the user number of each account a tool runner runs as here, where the mesh knows it: an access
// names the runner's account only with its number, which the node-engine requires (ADR 0325 §4). Nothing states
// it to the controller yet, so it is empty and no runner entry is sent.
AccountUIDs map[string]int
}
// RootField is a user resource's field saying the account must never become root without a person
@@ -320,6 +335,9 @@ type Composed struct {
// naming its module and why (novox/hq ADR 0255): that piece is left out and the machine is told
// everything else. Push and plan say it; the merge gate refuses a change that adds one.
Unplaced []string
// Identities is the declaration's `identities` (novox/hq ADR 0325 §3): [{module, name, uid, gid}] for each
// module composed with an identity. Empty to an engine that does not keep folders.
Identities []map[string]any
}
// ForeseenSealed is what stands for an own secret a send will make, in a composition asked ahead of
@@ -358,7 +376,9 @@ func (r Resolution) LeftOutBecause(settings SettingsBy, adopted bool) map[string
out[m.Module] = &UnreadManifestError{Module: m.Module, said: why}
continue
}
if err := JudgeSettings(m, settings[m.Module], adopted); err != nil {
// A placement stored ahead of the build that declares its access is passed by, not a reason to leave the
// module out (novox/hq issue 575): it was judged against that build where it was stored.
if err := ComposesWith(m, settings[m.Module], adopted); err != nil {
out[m.Module] = err
}
}
@@ -380,7 +400,8 @@ func (r Resolution) Compose(with Rendering) (Composed, error) {
received := map[string]map[string][]Contribution{}
leftOut := map[string]string{}
var foreseen, unplaced []string
resources, err := r.compose(with, owner, received, leftOut, &foreseen, &unplaced)
var identities []ModuleIdentity
resources, err := r.compose(with, owner, received, leftOut, &foreseen, &unplaced, &identities)
if err != nil {
return Composed{}, err
}
@@ -394,8 +415,12 @@ func (r Resolution) Compose(with Rendering) (Composed, error) {
})
}
sort.Strings(unplaced)
return Composed{Resources: resources, Owner: owner, Received: received, LeftOut: leftOut,
Foreseen: foreseen, Unplaced: unplaced}, nil
composed := Composed{Resources: resources, Owner: owner, Received: received, LeftOut: leftOut,
Foreseen: foreseen, Unplaced: unplaced}
if len(identities) > 0 {
composed.Identities = identitiesOf(identities)
}
return composed, nil
}
// BusMembershipID names the resource carrying a machine's membership for the new bus, and
@@ -406,7 +431,7 @@ const BusMembershipPath = "/var/lib/mesh/membership-next.json"
func (r Resolution) compose(with Rendering, owner map[string]string,
received map[string]map[string][]Contribution, leftOut map[string]string,
foreseen, unplaced *[]string) ([]map[string]any, error) {
foreseen, unplaced *[]string, identities *[]ModuleIdentity) ([]map[string]any, error) {
// **A setting is judged where it is stored, and an impossible one costs a module, not a
// machine** (novox/hq ADR 0163, rule 6). A definition that moved under a stored setting makes
// this module uncomposable; it is left out of the declaration — its held things kept, its
@@ -553,6 +578,16 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
}
resources := m.Resources
// The identity it runs as here (novox/hq ADR 0325 §3, §11): none to an older engine, nor without an allocation.
ident, hasIdent, err := identityFor(m, with)
if err != nil {
return nil, err
}
if hasIdent && identities != nil {
*identities = append(*identities, ident)
}
mountedByIdentity := dirsMountedByIdentity(m)
// What the mesh computes for this module goes FIRST, before the module's own resources.
//
// **Order is stated, not derived — the host does not sort** (novox/hq ADR 0005), so
@@ -1003,6 +1038,11 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
return nil, err
}
ownerInto(copied, placed)
// And its identity (novox/hq ADR 0325 §2, §3): a container's resolved, or taken out where none is composed;
// the owner of a directory its identity's containers mount. After the placement's owner: the identity wins.
if err := identityInto(copied, m, ident, hasIdent, mountedByIdentity); err != nil {
return nil, err
}
// **After settings, and that is the whole reason it is here.** A module's file
// content is where a setting lands, so a placeholder may only exist once the setting
// has been put in — filling secrets first would look at content that is not yet what
@@ -1120,6 +1160,12 @@ func (r Resolution) compose(with Rendering, owner map[string]string,
out = append(out, copied)
}
// **Who uses each of its shared folders** (novox/hq ADR 0325 §4, §5, §8): the access's `users` and `taken`, to
// an engine that keeps folders.
if with.KeepsFolders {
usersInto(out, owner, m, ident, hasIdent, r.toolsAccount(m, with), with)
}
// **What only the mesh knows, where this module asked for it.** The graph is the control
// plane's; making a name resolve is the module's software. Emitted as ordinary files under
// this module's name, so they are applied, reported and removed exactly as anything else