A directory the mesh places: ${dir:<id>} and the pathless directory resource

The first executable slice of ADR 0112 / to-be 27, sized to what the
operator settled tonight: a module definition names no host path for
its own data. A directory resource may omit path; composition resolves
it to <root>/<module>/<id>, the root a node's setting on Rendering with
/var/lib as the default — which reproduces exactly the layout novox
converged to by hand. ${dir:<id>} names the place from a resource's
path, content, mounts, environment and env-files, the same shape as
${bound:…}. A directory that states a path keeps it and still answers
by name — that is the adopted-data placement, mssql its live case.

Resolved in the controller at composition, so the wire format and the
host change not at all; a reference naming no directory refuses at the
manifest and again at composition; nested fills are rebuilt, never
written into the manifest's own maps, because one manifest composes
for many nodes.
This commit is contained in:
2026-09-26 17:51:54 +02:00
parent e88d3bd485
commit d2cbc9dbdc
4 changed files with 370 additions and 0 deletions
+17
View File
@@ -168,6 +168,13 @@ type Rendering struct {
// with an address — before references were kept without one — from an image a module runs
// straight from a public registry.
Built map[string]bool
// DataRoot is where this node keeps the directories the mesh places for its modules
// (novox/hq ADR 0112, to-be 27): a directory resource that states no path resolves to
// <DataRoot>/<module>/<id>, and ${dir:<id>} names that place from the module's own files,
// mounts and environment. A node setting fixed at installation; empty means the default,
// /var/lib — see dir_into.go.
DataRoot string
}
// machinePort is where a module's port lives on this machine, or the port itself when the mesh has
@@ -519,6 +526,9 @@ func (r Resolution) compose(with Rendering, owner map[string]string) ([]map[stri
}
// And what its bindings say, for the half of a connection that is not secret.
known := knownFor(m, r.Needs, r.Node)
// And where this node places the directories the module declared without a path
// (novox/hq ADR 0112) — resolved once per module, named by ${dir:…} from any resource.
dirs := dirsFor(m, with)
// And the machine underneath, which no binding of its own can tell it.
thisMachine := machineFacts(r, with.Names)
@@ -541,6 +551,13 @@ func (r Resolution) compose(with Rendering, owner map[string]string) ([]map[stri
// Said in the catalogue, not on the machine: the host parses strictly and knows no
// such field, and the reason is for a reader of the manifest.
delete(copied, SecretsInEnvironment)
// **Placed before anything reads a path.** A pathless directory receives the path
// this node resolves for it, and every ${dir:…} — in paths, mounts, content and
// environment — becomes that path, so what follows sees only concrete places
// (novox/hq ADR 0112). The host receives paths exactly as it always has.
if err := dirInto(copied, dirs, m.Module); 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