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
+70
View File
@@ -57,6 +57,49 @@ func (c Claim) At() string {
return c.Scope
}
// Modes an access may be granted at.
//
// **The grant states its extent, the way a listening port states its source** (the rule the
// manifest already keeps for `listens.from`). Absent narrows to read — the safe default, because
// the danger with an access is being given more than was meant, not less.
const (
AccessRead = "read"
AccessReadWrite = "read-write"
)
// Access is a pre-existing path on the machine that this module is GRANTED USE OF, and does not
// own.
//
// **The distinction this exists for** (novox/hq ADR 0051, 04-ISSUES/036): a `directory` resource
// is a thing the mesh owns — it creates it, sets its owner and mode, and removes it when it is
// empty and no longer declared ([ADR 0030](novox/hq)). Shared, pre-existing data is none of that.
// A media library, a download spool, an ingest folder is the **operator's**: it existed before the
// mesh, several modules read and write it at once, and the mesh must not create, chown, reconcile
// or remove it. It mounts it and owns nothing about it.
//
// Written as its own field rather than a flag on a directory because the two are opposite on every
// axis the host acts on, and 04-ISSUES/026 records what happens when *the directory my data lives
// in* and *a facility I was granted* are spelled the same: the second gets created as root and the
// ownership fields silently do not apply. Two modules may name the same access with no conflict —
// that is the whole point of it — whereas two owning one path is the fault the resolver refuses.
//
// If the path is absent when a machine applies, the host refuses clearly rather than creating it:
// the mesh does not own it, so conjuring it would be a lie the host then acts on.
type Access struct {
// Path is the absolute path on the machine, as the operator provides it.
Path string `json:"path"`
// Mode is "read" or "read-write". Absent narrows to read.
Mode string `json:"mode,omitempty"`
}
// At is this access's mode, with the default applied.
func (a Access) At() string {
if a.Mode == "" {
return AccessRead
}
return a.Mode
}
// Offer is something a module provides, and where the answer to it may live.
//
// **The distinction this exists for:** a shell, a display server and a private network have to be
@@ -157,6 +200,12 @@ type Manifest struct {
// Resources are what this module puts on a node, in the host's own vocabulary.
Resources []map[string]any `json:"resources,omitempty"`
// Accesses are pre-existing, operator-owned paths this module is granted use of but does not
// own — a shared media library, a download spool (novox/hq ADR 0051). Distinct from a
// `directory` resource, which the mesh creates and owns: an access is mounted and nothing
// about it is reconciled, and several modules may name the same one without conflict.
Accesses []Access `json:"accesses,omitempty"`
// Computed names something in the control plane that works this module's resources out per
// node, instead of them being fixed here.
//
@@ -415,6 +464,13 @@ func GrantID(provision, consumer string) string { return "grant-" + provision +
// BoundID is the resource identity of the file a module is told about a provision in.
func BoundID(requirement string) string { return "bound-" + requirement }
// AccessID is the resource identity of an operator-owned path this module is granted use of.
//
// Derived from the path rather than a name the module chose, so two modules granted the same
// access name the same identity within their own qualification — and neither has to invent a
// label for something that is not theirs.
func AccessID(path string) string { return "access-" + strings.TrimPrefix(path, "/") }
// Wants is everything that must be provided on the same node: what this module requires, and what
// it contributes to.
func (m Manifest) Wants() []string {
@@ -730,6 +786,20 @@ func ParseManifest(raw []byte) (Manifest, error) {
"%s receives contributions to %q and does not provide it", m.Module, to))
}
}
for _, a := range m.Accesses {
if !strings.HasPrefix(a.Path, "/") {
problems = append(problems, fmt.Sprintf(
"%s accesses %q, which is not an absolute path", m.Module, a.Path))
}
switch a.At() {
case AccessRead, AccessReadWrite:
default:
problems = append(problems, fmt.Sprintf(
"%s accesses %q at mode %q; an access is %q or %q",
m.Module, a.Path, a.Mode, AccessRead, AccessReadWrite))
}
}
for i, r := range m.Resources {
id, _ := r["id"].(string)
if id == "" {