declaration: an access is mounted, and the host owns nothing about it

The tenth shape (novox/hq ADR 0051). Shared, pre-existing data — a media
library, a download spool several modules use — is the operator's, not
the mesh's. A `directory` resource is the host's own: it creates it,
chowns it, sets its mode and removes it when empty. An access is the
opposite on every axis.

Add the `access` type to the vocabulary. Its applier confirms the path is
present and changes nothing: it does not create, chown, reconcile or set
a mode. Absent is refused clearly — the operator must provide it — rather
than created, because a bind mount whose source is missing is made as
root by the container runtime with the wrong ownership (04-ISSUES/026).
Undeclaring an access forgets the record and never touches the path,
which is the data loss ADR 0030 prevents, on a directory the mesh never
made.

Full hosts speak it (it gates a bind mount, which needs the container
runtime); the vocabulary guard test records the decision that made it the
tenth shape. Unit tests cover present, absent-refused, and
undeclared-left-alone.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
2026-09-05 22:19:33 +02:00
parent 291cab1091
commit f06eea5fa3
5 changed files with 212 additions and 4 deletions
+54 -1
View File
@@ -50,6 +50,15 @@ const (
// reason it is a shape rather than an action, because an action leaves nothing the host can
// undo and the network would outlive the module (novox/hq ADR 0029).
TypeNetwork Type = "network"
// TypeAccess is a pre-existing, operator-owned path a module is granted use of but does not
// own (novox/hq ADR 0051). The opposite of a directory on every axis the host acts on: the
// host creates, chowns and reconciles a directory, and removes it when it is empty; it does
// none of that to an access. It confirms the path is present — refusing clearly if the
// operator has not provided it, rather than creating it as a bind mount source would
// (04-ISSUES/026) — and leaves everything about it alone. Several modules declaring one
// access is ordinary, because none of them owns it.
TypeAccess Type = "access"
)
// Resource is one thing that should be true of the machine.
@@ -281,6 +290,48 @@ func (n *Network) validate(where string, _ bool) []string {
return problems
}
// Modes an access may be granted at. Plain words, not the octal a directory's mode is: an access
// is not a thing the host chmods, it is a statement of how this module reaches what the operator
// owns.
const (
AccessRead = "read"
AccessReadWrite = "read-write"
)
// Access is a pre-existing, operator-owned path this module is granted use of but does not own.
//
// **The distinction 04-ISSUES/036 and 026 turn on.** A `directory` resource is the mesh's own —
// it creates it, sets its owner and mode, and removes it when empty ([ADR 0030](novox/hq)). A
// media library, a download spool 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. The
// host confirms it is there and mounts it; nothing else.
type Access struct {
ID string `json:"id"`
Type Type `json:"type"`
Path string `json:"path"`
// Mode is how this module reaches the path: read or read-write. Absent narrows to read.
Mode string `json:"mode,omitempty"`
}
func (a *Access) Identity() string { return a.ID }
func (a *Access) Kind() Type { return TypeAccess }
func (a *Access) Target() string { return a.Path }
func (a *Access) validate(where string, _ bool) []string {
var problems []string
if !strings.HasPrefix(a.Path, "/") {
problems = append(problems, where+": an access needs an absolute path, and "+
a.Path+" is not one")
}
switch a.Mode {
case "", AccessRead, AccessReadWrite:
default:
problems = append(problems, fmt.Sprintf(
"%s: an access is %q or %q, not %q", where, AccessRead, AccessReadWrite, a.Mode))
}
return problems
}
func (u *User) Identity() string { return u.ID }
func (u *User) Kind() Type { return TypeUser }
func (u *User) Target() string { return u.Name }
@@ -555,6 +606,8 @@ func newOf(t Type) Resource {
return &User{}
case TypeArchive:
return &Archive{}
case TypeAccess:
return &Access{}
}
return nil
}
@@ -562,7 +615,7 @@ func newOf(t Type) Resource {
// Vocabulary is every kind this host speaks.
func Vocabulary() []Type {
return []Type{
TypeAction, TypeArchive, TypeContainer, TypeDirectory, TypeFile, TypeNetwork,
TypeAccess, TypeAction, TypeArchive, TypeContainer, TypeDirectory, TypeFile, TypeNetwork,
TypePackage, TypeService, TypeUser,
}
}