Say who owns a placed file at runtime, and which files the provisioner fills
mesh/merge-gate head 1cc588ebc060 merged into trunk 1203f96a24c8 — pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes wi…
mesh/repo-check head 1cc588ebc060 merged into trunk 1203f96a24c8 — pass: its merge-check.sh passed
mesh/delivery delivered

The provisioner (ADR 0349 §5) needs each file it places to say whether it is
owned, a seed or managed, and the node-engine needs to know which paths it
fills. The words map onto today's forms (drift written back, create-once,
into), so no write changes; the reader accepts the fields and states
OwnershipContract before any controller sends them. Issue 667.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 23:47:45 +00:00
co-authored by Claude Opus 5.5
parent 1203f96a24
commit 1cc588ebc0
8 changed files with 407 additions and 9 deletions
+125 -1
View File
@@ -237,8 +237,45 @@ type File struct {
// Owner is the user this belongs to, by name. Absent means root.
Owner string `json:"owner,omitempty"`
// OwnerAtRuntime says who owns the file once it is placed (novox/hq ADR 0349 §5): "owned", the mesh's
// whole, written back whenever it differs; "seed", written once and then the program's; or "managed",
// only its named keys or its marked block the mesh's, the program keeping the rest.
//
// **Three words for what the forms above already do.** Owned is an ordinary file, seed is create-once
// and managed is a file written into, so the word changes no write: a file that says it is applied
// exactly as its old form is, and one that does not say is owned by its old form (Ownership). The word
// is what the provisioner's files say of themselves, which the old forms only implied. Seed is enough
// alone; managed still needs `into`, which says the format. A word that contradicts the form is refused.
OwnerAtRuntime string `json:"owner-at-runtime,omitempty"`
}
// The words a file says who owns it at runtime with (novox/hq ADR 0349 §5).
const (
RuntimeOwned = "owned"
RuntimeSeed = "seed"
RuntimeManaged = "managed"
)
// Ownership is who owns the file at runtime: the word it says, or the one its old form implies — a file
// written into is managed, one created once is a seed, and any other is owned.
func (f *File) Ownership() string {
switch {
case f.OwnerAtRuntime != "":
return f.OwnerAtRuntime
case f.Into != "":
return RuntimeManaged
case f.CreateOnce:
return RuntimeSeed
default:
return RuntimeOwned
}
}
// Seeded reports whether the file is written only when absent: created once, by create-once or by the
// word seed.
func (f *File) Seeded() bool { return f.CreateOnce || f.OwnerAtRuntime == RuntimeSeed }
// Secret reports whether this file arrived sealed, which is what decides both that it must be
// opened before writing and that its contents must never appear in a report.
func (f *File) Secret() bool { return f.Sealed != "" }
@@ -320,6 +357,7 @@ func (f *File) validate(where string, _ bool) []string {
"secrets or create-once")
}
}
problems = append(problems, f.checkOwnership(where)...)
var said []string
for name, value := range map[string]string{
"content": f.Content, "sealed": f.Sealed, "bytes": f.Bytes,
@@ -361,6 +399,42 @@ func (f *File) validate(where string, _ bool) []string {
return append(problems, checkMode(where, f.Mode)...)
}
// checkOwnership refuses a word that contradicts the file's form (novox/hq ADR 0349 §5): owned is a whole
// file written back, so not created once nor written into; a seed is a whole file created once, so not
// written into; managed is a file written into, which says its format. Managed and created once together
// is already refused by the rule that a file written into is not created once.
func (f *File) checkOwnership(where string) []string {
switch f.OwnerAtRuntime {
case "":
case RuntimeOwned:
var also []string
if f.CreateOnce {
also = append(also, "create-once")
}
if f.Into != "" {
also = append(also, "into")
}
if len(also) > 0 {
return []string{fmt.Sprintf("%s: owned at runtime is the mesh's whole file, written back whenever it "+
"differs, and this also says %s", where, strings.Join(also, " and "))}
}
case RuntimeSeed:
if f.Into != "" {
return []string{where + ": a seed is a whole file written once, and this says into; a file the " +
"program shares with the mesh is managed"}
}
case RuntimeManaged:
if f.Into == "" {
return []string{where + ": a managed file names the format its keys or block are written into, " +
"with into \"json\" or \"block\""}
}
default:
return []string{fmt.Sprintf("%s: owner-at-runtime %q; a placed file is %q, %q or %q, or omits it to be "+
"owned by its form", where, f.OwnerAtRuntime, RuntimeOwned, RuntimeSeed, RuntimeManaged)}
}
return nil
}
// User is a login on the machine.
//
// The thing that makes a shell, a chat client or a desktop expressible at all: each is a package
@@ -1565,6 +1639,52 @@ type Declaration struct {
// this is its home, whether the operator's warrant to take it was carried out, the sub-paths its modules ask for,
// and the tool runner's account to put in its group (§11). Absent is a controller older than ADR 0328.
Libraries []Library
// Filled is the paths of the files the provisioner fills on this machine (novox/hq ADR 0349 §4, §5): each a
// file this declaration declares, which says who owns it at runtime. The provisioner's client has the
// node-engine write only these. Absent is a controller that hands no file to the provisioner, and every
// file is written from the declaration as before.
Filled []string
}
// Fills reports whether the provisioner fills the file at path on this machine.
func (d *Declaration) Fills(path string) bool { return slices.Contains(d.Filled, path) }
// checkFilled refuses a filled path that is not a declared file saying who owns it at runtime, or that is named
// twice or empty. Every file declared at the path must say it: the provisioner writes the path, and a file there
// the mesh writes some other way would be a second writer of it (ADR 0349 §5).
func checkFilled(filled []string, resources []Resource) []string {
var problems []string
files := map[string][]*File{}
for _, r := range resources {
if f, ok := r.(*File); ok {
files[f.Path] = append(files[f.Path], f)
}
}
seen := map[string]bool{}
for _, path := range filled {
switch {
case path == "":
problems = append(problems, "filled_files names a file with no path")
continue
case seen[path]:
problems = append(problems, fmt.Sprintf("filled_files names %q twice", path))
continue
}
seen[path] = true
if len(files[path]) == 0 {
problems = append(problems, fmt.Sprintf("filled_files names %q, and this declaration declares no "+
"file there", path))
continue
}
for _, f := range files[path] {
if f.OwnerAtRuntime == "" {
problems = append(problems, fmt.Sprintf("resource %q: the provisioner fills %q, and a file it "+
"fills says who owns it at runtime with owner-at-runtime", f.ID, path))
}
}
}
return problems
}
// Identity is one module's identity, as the controller allocated it (ADR 0328 §4): its user `mesh-<module>` with
@@ -1904,6 +2024,9 @@ type envelope struct {
// sends them only to a host whose health says link.LibraryContract.
Groups []Group `json:"groups,omitempty"`
Libraries []Library `json:"libraries,omitempty"`
// Filled is optional too (novox/hq ADR 0349 §5), as is a file's `owner-at-runtime`. **An older host refuses
// these keys**; a controller sends them only to a host whose health says link.OwnershipContract.
Filled []string `json:"filled_files,omitempty"`
}
func parse(raw []byte, allowActions bool) (*Declaration, error) {
@@ -1922,7 +2045,7 @@ func parse(raw []byte, allowActions bool) (*Declaration, error) {
d := &Declaration{Version: env.Version, For: env.For, Adoption: env.Adoption, Sequence: env.Sequence,
Epoch: env.Epoch, LeftOut: env.LeftOut, Generation: env.Generation, PutBack: env.PutBack,
Identities: env.Identities, Groups: env.Groups, Libraries: env.Libraries}
Identities: env.Identities, Groups: env.Groups, Libraries: env.Libraries, Filled: env.Filled}
problems := checkIdentities(env.Identities, env.Groups)
problems = append(problems, checkLibraries(env.Groups, env.Libraries)...)
if env.Generation < 0 {
@@ -2015,6 +2138,7 @@ func parse(raw []byte, allowActions bool) (*Declaration, error) {
problems = append(problems, checkWhileStopped(d.Resources)...)
problems = append(problems, checkAdoption(env.Adoption, d.Resources, allowActions)...)
problems = append(problems, checkReplaces(d.Resources)...)
problems = append(problems, checkFilled(env.Filled, d.Resources)...)
if env.Adoption == nil {
for _, r := range d.Resources {
if r.Kind() == TypeOpening {
+130
View File
@@ -0,0 +1,130 @@
package declaration
import (
"strings"
"testing"
)
// Defends novox/hq ADR 0349 §5 (issue 667): a file the provisioner places says who owns it at runtime
// — owned, seed or managed — and the declaration names the files the provisioner fills. The words are
// read and judged here before any controller sends them.
func TestAFileSaysWhoOwnsItAtRuntime(t *testing.T) {
d, err := Parse([]byte(`{"declaration":1,"resources":[
{"id":"o","type":"file","path":"/etc/o","content":"a","owner-at-runtime":"owned"},
{"id":"s","type":"file","path":"/etc/s","content":"a","owner-at-runtime":"seed"},
{"id":"s2","type":"file","path":"/etc/s2","content":"a","owner-at-runtime":"seed","create-once":true},
{"id":"mj","type":"file","path":"/etc/mj","into":"json","content":"{\"k\":1}","owner-at-runtime":"managed"},
{"id":"mb","type":"file","path":"/etc/mb","into":"block","content":"a\n","owner-at-runtime":"managed"}
]}`))
if err != nil {
t.Fatal(err)
}
want := map[string]string{"o": RuntimeOwned, "s": RuntimeSeed, "s2": RuntimeSeed, "mj": RuntimeManaged,
"mb": RuntimeManaged}
for _, r := range d.Resources {
f := r.(*File)
if f.OwnerAtRuntime != want[f.ID] || f.Ownership() != want[f.ID] {
t.Errorf("%s: read as %q, ownership %q; want %q", f.ID, f.OwnerAtRuntime, f.Ownership(), want[f.ID])
}
}
}
// A file that does not say is owned by its old form, so the word means exactly what that form does.
func TestAFileThatDoesNotSayIsOwnedByItsOldForm(t *testing.T) {
d, err := Parse([]byte(`{"declaration":1,"resources":[
{"id":"o","type":"file","path":"/etc/o","content":"a"},
{"id":"s","type":"file","path":"/etc/s","content":"a","create-once":true},
{"id":"mj","type":"file","path":"/etc/mj","into":"json","content":"{}"},
{"id":"mb","type":"file","path":"/etc/mb","into":"block","content":"a\n"}
]}`))
if err != nil {
t.Fatal(err)
}
want := map[string]string{"o": RuntimeOwned, "s": RuntimeSeed, "mj": RuntimeManaged, "mb": RuntimeManaged}
for _, r := range d.Resources {
f := r.(*File)
if f.OwnerAtRuntime != "" || f.Ownership() != want[f.ID] {
t.Errorf("%s: said %q, ownership %q; want unsaid and %q", f.ID, f.OwnerAtRuntime, f.Ownership(), want[f.ID])
}
if f.Seeded() != (want[f.ID] == RuntimeSeed) {
t.Errorf("%s: seeded %v", f.ID, f.Seeded())
}
}
}
func TestAnOwnershipThatContradictsTheFileIsRefused(t *testing.T) {
for name, c := range map[string]struct{ resource, refusal string }{
"another word": {`{"id":"f","type":"file","path":"/etc/x","content":"a","owner-at-runtime":"mine"}`,
`owner-at-runtime "mine"`},
"owned and create-once": {`{"id":"f","type":"file","path":"/etc/x","content":"a","owner-at-runtime":"owned","create-once":true}`,
"create-once"},
"owned and into": {`{"id":"f","type":"file","path":"/etc/x","into":"json","content":"{}","owner-at-runtime":"owned"}`,
"into"},
"seed and into": {`{"id":"f","type":"file","path":"/etc/x","into":"block","content":"a","owner-at-runtime":"seed"}`,
"into"},
"managed and whole": {`{"id":"f","type":"file","path":"/etc/x","content":"a","owner-at-runtime":"managed"}`,
"into"},
"managed and create-once": {`{"id":"f","type":"file","path":"/etc/x","into":"json","content":"{}","owner-at-runtime":"managed","create-once":true}`,
"create-once"},
// The rule that was there before stays: a file written into is never sealed or created once.
"managed and sealed": {`{"id":"f","type":"file","path":"/etc/x","into":"json","content":"{}","sealed":"x","owner-at-runtime":"managed"}`,
"sealed"},
} {
_, err := Parse([]byte(`{"declaration":1,"resources":[` + c.resource + `]}`))
if err == nil || !strings.Contains(err.Error(), c.refusal) {
t.Errorf("%s: want a refusal naming %q, got %v", name, c.refusal, err)
}
}
}
func TestTheDeclarationNamesTheFilesTheProvisionerFills(t *testing.T) {
d, err := Parse([]byte(`{"declaration":1,"filled_files":["/etc/a","/etc/b"],"resources":[
{"id":"a","type":"file","path":"/etc/a","content":"a","owner-at-runtime":"owned"},
{"id":"b","type":"file","path":"/etc/b","into":"block","content":"b\n","owner-at-runtime":"managed"},
{"id":"c","type":"file","path":"/etc/c","content":"c"}
]}`))
if err != nil {
t.Fatal(err)
}
if len(d.Filled) != 2 || d.Filled[0] != "/etc/a" || d.Filled[1] != "/etc/b" {
t.Fatalf("filled files read as %v", d.Filled)
}
if !d.Fills("/etc/a") || !d.Fills("/etc/b") || d.Fills("/etc/c") {
t.Errorf("Fills: a %v, b %v, c %v", d.Fills("/etc/a"), d.Fills("/etc/b"), d.Fills("/etc/c"))
}
// A declaration that names none fills none, as every declaration before this did.
d, err = Parse([]byte(`{"declaration":1,"resources":[{"id":"c","type":"file","path":"/etc/c","content":"c"}]}`))
if err != nil {
t.Fatal(err)
}
if d.Filled != nil || d.Fills("/etc/c") {
t.Errorf("a declaration naming no filled files fills %v", d.Filled)
}
}
func TestAFilledFileMustBeADeclaredFileThatSaysItsOwner(t *testing.T) {
for name, c := range map[string]struct{ filled, resources, refusal string }{
"not declared": {`["/etc/none"]`,
`{"id":"a","type":"file","path":"/etc/a","content":"a","owner-at-runtime":"owned"}`,
`"/etc/none"`},
"a directory, not a file": {`["/etc/d"]`,
`{"id":"d","type":"directory","path":"/etc/d"}`,
`"/etc/d"`},
"says no owner": {`["/etc/a"]`,
`{"id":"a","type":"file","path":"/etc/a","content":"a"}`,
"owner-at-runtime"},
"named twice": {`["/etc/a","/etc/a"]`,
`{"id":"a","type":"file","path":"/etc/a","content":"a","owner-at-runtime":"owned"}`,
"twice"},
"no path": {`[""]`,
`{"id":"a","type":"file","path":"/etc/a","content":"a","owner-at-runtime":"owned"}`,
"no path"},
} {
_, err := Parse([]byte(`{"declaration":1,"filled_files":` + c.filled + `,"resources":[` + c.resources + `]}`))
if err == nil || !strings.Contains(err.Error(), c.refusal) {
t.Errorf("%s: want a refusal naming %q, got %v", name, c.refusal, err)
}
}
}