A module says what it builds, and the built manifest is a different

document

The manifest in a repository names artifacts; the manifest the mesh holds
names digests. Keeping them the same file would mean a repository
carrying a digest — wrong the moment anybody edits anything, and pinning
a value nobody could have checked.

So a resource says `"artifact": "server"`, and resolving a build rewrites
it to the image reference or the archive's source and digest, removing
the build-time word entirely. The host has never heard of an artifact and
its strict decoder would refuse one, at the worst moment.

A module that builds nothing is ordinary and needs no build section —
most of what a person installs is configuration, and a field that exists
to be left blank is a field nobody fills in correctly.

Refusals worth having:

- an artifact declared and not produced blames THE BUILD, not the
  resource. Both are failures and the remedies are in different places;
  telling somebody to fix the wrong one costs an afternoon. Found by
  injection: the first version's message could not be told apart from
  the resource-level one, so the check was not actually tested.
- a build reads its own repository and nothing else. An input path
  leaving it makes what gets built depend on whatever happens to be on
  the machine building it.
- two artifacts with one name, because a resource naming it could mean
  either.
This commit is contained in:
2026-08-30 03:29:17 +02:00
parent d978712d7f
commit 44ba100595
3 changed files with 328 additions and 0 deletions
+37
View File
@@ -171,6 +171,14 @@ type Manifest struct {
// secret is the one thing that must not be.
Serves map[string]map[string]any `json:"serves,omitempty"`
// Build says how this module's artifacts are produced from its source.
//
// The manifest in a repository names artifacts; the manifest the mesh holds names digests.
// **They are not the same document**, and that is deliberate: a digest is not knowable until
// something is built, and a repository that carried one would be a repository whose file is
// wrong the moment anybody edits anything.
Build *Build `json:"build,omitempty"`
// Binds is where this module wants to be told about something it requires, per requirement.
//
// Because "this machine needs a database from the anchor" is useless to the program that
@@ -199,6 +207,34 @@ type Manifest struct {
Grants map[string]string `json:"grants,omitempty"`
}
// Build says how to produce this module's artifacts from its source.
//
// **Absent means nothing is built.** A module can be entirely configuration — a shell's rc file,
// a set of firewall rules — and having to declare an empty build for it would be a field that
// exists to be left blank.
type Build struct {
// Artifacts are what the source produces, each named so a resource can refer to it before
// anybody knows its digest.
Artifacts []Artifact `json:"artifacts,omitempty"`
}
// Artifact is one thing built from a module's source.
type Artifact struct {
// Name is how resources refer to it. Local to the module.
Name string `json:"name"`
// Kind is "image" or "archive".
Kind string `json:"kind"`
// From is what it is built from, relative to the repository root: a Dockerfile for an image,
// a directory for an archive.
From string `json:"from"`
}
// Kinds an artifact may be.
const (
ArtifactImage = "image"
ArtifactArchive = "archive"
)
// SecretID is the resource identity of the file a module is given a credential in.
func SecretID(requirement string) string { return "secret-" + requirement }
@@ -307,6 +343,7 @@ func ParseManifest(raw []byte) (Manifest, error) {
"%s contributes nothing to %q; if it only needs one, require it", m.Module, to))
}
}
problems = append(problems, m.Build.problems(m.Module)...)
for to := range m.Serves {
var offered bool
for _, o := range m.Offers() {