catalogue: a module accesses operator-owned data, and does not own it

04-ISSUES/036: the media stack is several modules that must share the
library and download directories on one machine, but the manifest could
only say "a directory I own". Six modules each declared the same paths as
their own resources, and the resolver's duplicate-owner refusal — right
in general — would refuse the stack's only sensible assignment the first
time two of them landed on one node.

Add an `accesses` field: a pre-existing, operator-owned path a module is
granted use of but does not own (novox/hq ADR 0051). Distinct from a
`directory` resource on every axis the host acts on — the mesh creates,
chowns and reconciles a directory; it mounts an access and owns nothing.
An access is not a resource, so it never enters the duplicate-owner map
and several modules may name one path with no conflict. What is refused
is the contradiction: a path one module owns and another accesses.

Rendered into the declaration as an `access` resource, before the
container that mounts it, so the host can find it present or refuse
clearly. Unit tests cover co-resolution (the exact 036 case), the
unchanged owner-vs-owner refusal, the owner-vs-accessor refusal, and
access validation.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
2026-09-05 22:10:58 +02:00
parent 59fcb41855
commit aeb65a3e1d
4 changed files with 221 additions and 0 deletions
+33
View File
@@ -566,9 +566,18 @@ func checkClaims(modules []Manifest, node Node, elsewhere []Held) ([]Held, []str
// This costs no manifest field: the mesh already holds every resource of every module, so two
// declaring one path or one unit are visible without either having to know about the other. A
// declared claim is only for the abstract conflicts nothing in the resources reveals.
//
// **The refusal is about ownership, not use** (novox/hq ADR 0051, 04-ISSUES/036). Two modules
// owning one path is the fault this catches — the class of collision this repository keeps
// recording. Two modules *accessing* one operator-owned path is not a collision: it is the whole
// point of a media stack, where the library server, the managers and the download client must see
// the same directories. An access is not a resource and never enters the owner map, so co-access
// resolves with no refusal. What is refused is the contradiction — a path one module owns and
// another merely accesses — because shared data is the operator's and nobody's to own.
func checkResources(modules []Manifest) []string {
var problems []string
owner := map[string]string{}
ownedPath := map[string]string{} // path → owning module, for the access check below
for _, m := range modules {
for _, r := range m.Resources {
@@ -583,6 +592,30 @@ func checkResources(modules []Manifest) []string {
"%s and %s both declare the %s %q", other, m.Module, field, value))
}
owner[key] = m.Module
if field == "path" {
ownedPath[value] = m.Module
}
}
}
}
// An accessed path is the operator's, so no module may declare it as one of its own
// (novox/hq ADR 0051). Refused here rather than silently tolerated: an owner would create and
// chown the very directory another module was told to expect the operator to provide, and the
// two intentions cannot both hold. Two modules *accessing* it, by contrast, is never checked,
// which is what lets the stack in 04-ISSUES/036 co-resolve.
for _, m := range modules {
for _, a := range m.Accesses {
switch other := ownedPath[a.Path]; other {
case "":
// Nobody owns it — the ordinary, correct case for shared data.
case m.Module:
problems = append(problems, fmt.Sprintf(
"%s both owns and accesses %q — it is one or the other", m.Module, a.Path))
default:
problems = append(problems, fmt.Sprintf(
"%s accesses %q, which %s declares it owns — shared data is the operator's, "+
"owned by no module (novox/hq ADR 0051)", m.Module, a.Path, other))
}
}
}