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:
@@ -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 == "" {
|
||||
|
||||
Reference in New Issue
Block a user