A daemon says what to run, not how it is hosted
The mechanism was leaking into every module. Code of one's own meant a container and therefore an image; a script meant a service and a unit somebody else had to install. One intent — run this and keep it running — expressed two unrelated ways, with the hosting chosen before anything could be declared. A daemon names a bundle and a command. The host fetches it, refuses it unless it hashes to what was declared, unpacks it where the mesh keeps such things, writes the unit and puts it in the state asked for. The unit is the mesh's, generated whole and saying so, because an edit that survives until the next declaration and then vanishes is worse than one that is refused. Its identity is the bytes AND how it is run: two daemons from one bundle differing only in their command are different daemons, and tracking the digest alone would call the second unchanged and leave the first running. The unit is rendered deterministically for the same reason — environment from a map would be written in Go's iteration order, so every apply would see a different unit and restart an unchanged daemon for ever. restart-on is honoured as a service's is: a running process does not re-read its configuration, so replacing a file and finding the daemon already up leaves the machine behaving as before while every check passes. A full-host shape, not a portable one: it needs a process supervisor to install into. It does NOT need a container runtime, which is the point. Two guards caught this properly and both were updated deliberately rather than silenced: the vocabulary count, which exists because every addition widens what a compromised control plane can express, and the shape test that catches a kind the language has and a host cannot apply — added after `network` did exactly that. Claude-Session: https://claude.ai/code/session_01D6qtiYU3P9jk3pnAXyAFyx
This commit is contained in:
@@ -59,6 +59,20 @@ const (
|
||||
// (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"
|
||||
|
||||
// TypeDaemon is a long-running process the mesh keeps running, named by what it runs rather
|
||||
// than by how it is hosted.
|
||||
//
|
||||
// **The intent, not the mechanism.** Until this, an author decided the hosting before they
|
||||
// could declare anything: code of their own meant a `container` built from an image, a script
|
||||
// meant a `service` and a unit somebody else had to install. Same intent — run this and keep
|
||||
// it running — expressed two unrelated ways, and the choice baked into which kind was picked.
|
||||
//
|
||||
// A daemon names an artifact and what to run. The mesh unpacks the artifact where it keeps
|
||||
// such things, writes the unit, and puts it in the state asked for. One module may declare
|
||||
// several, in different languages, because a module is one piece of software and not one
|
||||
// process (novox/hq ADR 0040).
|
||||
TypeDaemon Type = "daemon"
|
||||
)
|
||||
|
||||
// Resource is one thing that should be true of the machine.
|
||||
@@ -393,6 +407,81 @@ func (a *Archive) validate(where string, _ bool) []string {
|
||||
return problems
|
||||
}
|
||||
|
||||
// Daemon is a long-running process the mesh installs, keeps running, and owns the unit for.
|
||||
//
|
||||
// The difference from Service is who owns the unit: a Service puts an EXISTING unit into a state
|
||||
// and deliberately does not install one, which is right for software that ships its own. A Daemon
|
||||
// is the mesh's own code — a bundle it built — so there is no unit until the mesh writes it, and
|
||||
// nothing else will.
|
||||
//
|
||||
// The difference from Container is the hosting, and a module should not have to choose: what a
|
||||
// daemon says is what to run, and the machine's own process supervisor is how. A module whose code
|
||||
// genuinely needs a container's isolation declares a container and says so.
|
||||
type Daemon struct {
|
||||
ID string `json:"id"`
|
||||
Type Type `json:"type"`
|
||||
// Name is what the unit is called, and what an operator will see in the process table.
|
||||
Name string `json:"name"`
|
||||
// Source is where to fetch the bundle from, and Digest is what it must hash to. The same
|
||||
// discipline as an archive, for the same reason: this crosses a network the mesh does not
|
||||
// control.
|
||||
Source string `json:"source"`
|
||||
Digest string `json:"digest"`
|
||||
// Run is the command, relative to the unpacked bundle. The first element is the program.
|
||||
//
|
||||
// **Named by the module, never inferred.** Guessing an entrypoint from which files exist makes
|
||||
// a daemon change what it runs when somebody adds a file.
|
||||
Run []string `json:"run"`
|
||||
// Env and EnvFile are what it runs with. A file rather than inline values is how a credential
|
||||
// reaches a daemon without passing through the declaration.
|
||||
Env map[string]string `json:"env,omitempty"`
|
||||
EnvFile []string `json:"env-file,omitempty"`
|
||||
// User is who it runs as. Absent means root, which is what the mesh's own modules need for
|
||||
// the things they do to a machine.
|
||||
User string `json:"user,omitempty"`
|
||||
// RestartOn names resources whose change means this must be restarted — the same rule a
|
||||
// service follows, and for the same reason: a running process does not re-read its
|
||||
// configuration, so replacing a file and finding the process already up leaves the machine
|
||||
// behaving the way it did before while every check passes.
|
||||
RestartOn []string `json:"restart-on,omitempty"`
|
||||
}
|
||||
|
||||
func (d *Daemon) Identity() string { return d.ID }
|
||||
func (d *Daemon) Kind() Type { return TypeDaemon }
|
||||
func (d *Daemon) Target() string { return d.Name }
|
||||
|
||||
func (d *Daemon) validate(where string, _ bool) []string {
|
||||
var problems []string
|
||||
if d.Name == "" {
|
||||
problems = append(problems, where+": a daemon needs a name, which is what its unit is called")
|
||||
}
|
||||
if strings.ContainsAny(d.Name, "/ \t") {
|
||||
// It becomes a unit name and a file on disk. A name with a separator in it would write
|
||||
// somewhere nobody meant.
|
||||
problems = append(problems, where+": a daemon's name becomes a unit name, so it cannot "+
|
||||
"contain a path separator or a space")
|
||||
}
|
||||
if d.Source == "" {
|
||||
problems = append(problems, where+": a daemon needs somewhere to fetch its bundle from")
|
||||
}
|
||||
if !strings.HasPrefix(d.Digest, "sha256:") || len(d.Digest) != len("sha256:")+64 {
|
||||
// The same rule an archive follows, and for the same reason: this crosses a network the
|
||||
// mesh does not control, and a reference that can be made to point elsewhere is not one.
|
||||
problems = append(problems, where+
|
||||
": a daemon's bundle is pinned by digest, as sha256:<64 hex characters>")
|
||||
}
|
||||
if len(d.Run) == 0 {
|
||||
problems = append(problems, where+": a daemon needs to say what to run")
|
||||
}
|
||||
for _, part := range d.Run {
|
||||
if part == "" {
|
||||
problems = append(problems, where+": a daemon's command has an empty element")
|
||||
break
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
// Service is a unit the host puts into a state. It does not install the unit.
|
||||
//
|
||||
// Two states, and they are orthogonal rather than one scale. A unit can be enabled and stopped
|
||||
@@ -654,6 +743,8 @@ func newOf(t Type) Resource {
|
||||
return &Archive{}
|
||||
case TypeAccess:
|
||||
return &Access{}
|
||||
case TypeDaemon:
|
||||
return &Daemon{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -661,8 +752,8 @@ func newOf(t Type) Resource {
|
||||
// Vocabulary is every kind this host speaks.
|
||||
func Vocabulary() []Type {
|
||||
return []Type{
|
||||
TypeAccess, TypeAction, TypeArchive, TypeContainer, TypeDirectory, TypeFile, TypeNetwork,
|
||||
TypePackage, TypeService, TypeUser,
|
||||
TypeAccess, TypeAction, TypeArchive, TypeContainer, TypeDaemon, TypeDirectory, TypeFile,
|
||||
TypeNetwork, TypePackage, TypeService, TypeUser,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user