REG1: distribution checks a publish against the builder's reservation, asked by the artifact's name

The container image and OCI artifact seats' holder asks the seat mesh-builds' verb reserved in place of builder_versions; the manifest
invokes it and takes no builder package.

Builds: design 47 G §381, §478, §504

Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
2026-11-11 14:05:41 +01:00
parent 86342c4399
commit 713b59633e
8 changed files with 109 additions and 17 deletions
@@ -12,7 +12,6 @@ import (
"encoding/json"
"errors"
"fmt"
builds "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
"net/url"
"os"
"strings"
@@ -520,7 +519,7 @@ func tools(v *holder.Verbs, windows kept[holder.Window], callers func() (holder.
func main() {
base, _ := url.Parse("http://127.0.0.1:5001")
store := &holder.OCI{Base: base, Upstream: &holder.Upstream{}}
h := &holder.Holder{Objects: parts.BusObjects(holder.IncomingObjects), Builder: builds.BuilderVersions{Ask: stdio.Ask}, Store: store, Say: say, Finding: finding,
h := &holder.Holder{Objects: parts.BusObjects(holder.IncomingObjects), Builder: holder.Reserved{Ask: stdio.Ask}, Store: store, Say: say, Finding: finding,
Transfers: parts.BusTransfers[holder.Opening](holder.TransfersState), Cap: parts.BusCap(holder.IncomingObjects), Record: recordAct, Idle: parts.BusArm(holder.IdleState)}
files := &holder.Files{Container: or(os.Getenv("MESH_REGISTRY_CONTAINER"), "mesh-registry"), Run: holder.ExecRunner, UID: os.Getuid()}
v := &holder.Verbs{Holder: h, Catalog: store, Holds: kept[holder.Hold]{state: "holds", key: holder.Hold.Key}, Mirrors: mirrors{},
+1 -1
View File
@@ -8,7 +8,7 @@ require (
git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/clients/containerimageregistryclient v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000
github.com/nats-io/nats.go v1.54.0
@@ -2,7 +2,7 @@
// `oci-artifact-registry`, and of the old seat `mesh-artifact-store` until its window closes (novox/hq design 51 *The
// four registry seats*, ADR 0605, ADR 0611, ADR 0617 §2). A build module publishes into a seat through `publish` and
// `publish-part`: the parts land in distribution's own object store `incoming`, are hashed, checked against the
// builder's `builder_versions` and stored in the artifact store, which listens on its node's loopback; the store's port
// builder's reservation (`reserved`) and stored in the artifact store, which listens on its node's loopback; the store's port
// is the program's, reads open, writes from its own process alone (front.go).
package holder
@@ -286,12 +286,12 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
_, err := b.h.Publish(ArtifactSeat, builder, req("t"))
return err
}, "at their cap"},
{"a version the builder did not number", func(b *bed) error {
{"a version the builder did not reserve", func(b *bed) error {
b.h.Builder = fakeBuilder{ok: false}
b.store.has[part.Digest] = true // held already: the publish ends at once
_, err := b.h.Publish(ArtifactSeat, builder, req("t"))
return err
}, "the builder did not number it"},
}, "the builder did not reserve it"},
{"an image on the artifact seat", func(b *bed) error {
r := req("t")
r.Kind = KindImage
@@ -843,9 +843,9 @@ func TestTheHolderOfTheTwoImageSeats(t *testing.T) {
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
// The builder's question is its published package, taken by its series (ADR 0634 §1).
if m.Packages["git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"] != "0.1" {
t.Errorf("packages %v: want builds of series 0.1", m.Packages)
// The reservation is asked through the seat mesh-builds, so the manifest takes no package of the builder (design 47 §381).
if len(m.Packages) != 0 {
t.Errorf("packages %v: want none", m.Packages)
}
// E15: the provision artifact-store retires (ADR 0632 §3); the bootstrap keeps it as an alias on port 5100 while
// the running node-engines still find the store by it (issue 1966), and nothing else is provided or served.
@@ -0,0 +1,42 @@
package holder
import (
"encoding/json"
"fmt"
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshbuilds"
)
// Reserved is the holder's check of a publish against the builder's reservation (novox/hq design 47 §381, §478, §504): it asks the
// seat mesh-builds' verb reserved with the publish's name and version, a package's name in its ecosystem, and accepts the publish
// only when that version is the one reserved and an artifact of it has the publish's digest. It reads no catalog and a publish gains
// no field. It is the Builder the holder's Check asks.
type Reserved struct {
// Ask is the runtime's request and reply over the bus (mesh-sdk stdio.Ask).
Ask func(address string, body any) (json.RawMessage, error)
}
// Address is the seat verb asked, as the manifest's `invokes` names it.
const Address = "seat:" + meshbuilds.Seat + "." + meshbuilds.VerbReserved
// Recorded answers whether name@version with digest is reserved, and why not when it is not. An answer that cannot be read is an
// error, never a yes; a refusal by the builder (a name no entry carries, a version it did not reserve) is a no that names it.
func (r Reserved) Recorded(name, version, digest string) (bool, string, error) {
raw, err := r.Ask(Address, meshbuilds.ReservedInput{Name: name, Version: version})
if err != nil {
return false, "", fmt.Errorf("%s did not answer for %s@%s: %w", Address, name, version, err)
}
var a meshbuilds.ReservedAnswer
if err := json.Unmarshal(raw, &a); err != nil || a.Version == "" {
return false, "", fmt.Errorf("%s answered no reservation for %s@%s: %s", Address, name, version, raw)
}
if a.Version != version {
return false, fmt.Sprintf("%s reserved %s for %s, not %s", a.Module, a.Version, name, version), nil
}
for _, art := range a.Artifacts {
if art.Digest == digest {
return true, "", nil
}
}
return false, fmt.Sprintf("%s reserved %s@%s for another digest than %s", a.Module, name, version, digest), nil
}
@@ -0,0 +1,54 @@
package holder
import (
"encoding/json"
"errors"
"strings"
"testing"
)
// A publish is checked against the builder's reservation by the seat mesh-builds' verb reserved, asked with the package's name
// (design 47 §381, §478).
func TestReservedChecksAPublishAgainstTheReservation(t *testing.T) {
for _, c := range []struct {
name string
answer string
err error
ok bool
why string // part of the reason, or of the error
}{
{name: "the reserved version and digest", answer: `{"module":"mesh-image","version":"0.1.7","artifacts":[{"artifact":"mesh-image:image","digest":"sha256:aa"}]}`, ok: true},
{name: "another artifact of it carries the digest", answer: `{"module":"mesh-image","version":"0.1.7","artifacts":[{"artifact":"mesh-image:aux"},{"artifact":"mesh-image:image","digest":"sha256:aa"}]}`, ok: true},
{name: "another digest", answer: `{"module":"mesh-image","version":"0.1.7","artifacts":[{"artifact":"mesh-image:image","digest":"sha256:bb"}]}`, why: "another digest than sha256:aa"},
{name: "another version reserved", answer: `{"module":"mesh-image","version":"0.1.8","artifacts":[]}`, why: "reserved 0.1.8 for registry/mesh-image, not 0.1.7"},
{name: "the builder refused the name", err: errors.New("no entry carries registry/mesh-image"), why: "no entry carries registry/mesh-image"},
{name: "an answer that is none", answer: `{"error":"x"}`, why: "answered no reservation"},
} {
asked := ""
r := Reserved{Ask: func(address string, body any) (json.RawMessage, error) {
b, _ := json.Marshal(body)
asked = address + " " + string(b)
return json.RawMessage(c.answer), c.err
}}
ok, why, err := r.Recorded("registry/mesh-image", "0.1.7", "sha256:aa")
if asked != `seat:mesh-builds.reserved {"name":"registry/mesh-image","version":"0.1.7"}` {
t.Fatalf("%s: asked %s", c.name, asked)
}
if c.ok {
if !ok || err != nil {
t.Errorf("%s: not accepted: %v %v", c.name, why, err)
}
continue
}
if ok || !strings.Contains(why+fmtErr(err), c.why) {
t.Errorf("%s: ok=%v why=%q err=%v, want %q", c.name, ok, why, err, c.why)
}
}
}
func fmtErr(err error) string {
if err == nil {
return ""
}
return err.Error()
}
@@ -129,7 +129,7 @@ type Objects = parts.Objects
// TransferState is the state transfers.
type TransferState = parts.Transfers[Opening]
// Builder is the builder's tool builder_versions, asked before anything is stored (ADR 0605 §4).
// Builder is the builder's reservation (Reserved), asked before anything is stored (ADR 0605 §4, design 47 §381).
type Builder interface {
Recorded(name, version, digest string) (ok bool, why string, err error)
}
@@ -196,17 +196,17 @@ func (h *Holder) receiver() *parts.Receiver[Opening] {
ok, err := h.Store.Has(t.Shape.Name, digest)
return err == nil && ok
},
// builder_versions is asked before anything is stored (ADR 0605 §4).
// The reservation is asked before anything is stored (ADR 0605 §4, design 47 §381).
Check: func(t Transfer) error {
if t.Shape.Version == "" {
return nil
}
ok, why, err := h.Builder.Recorded(t.Shape.Name, t.Shape.Version, t.Shape.Digest)
if err != nil {
return errors.New("builder_versions did not answer, so nothing is stored: " + err.Error())
return errors.New("the reservation could not be read, so nothing is stored: " + err.Error())
}
if !ok {
return errors.New("the builder did not number it: " + why)
return errors.New("the builder did not reserve it: " + why)
}
return nil
},
+1 -4
View File
@@ -1,9 +1,6 @@
{
"module": "distribution",
"version": "2",
"packages": {
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds": "0.1"
},
"provides": [
{
"name": "artifact-store",
@@ -63,7 +60,7 @@
],
"invokes": [
"distribution.registry_callers",
"mesh-delivery-builder.builder_versions",
"seat:mesh-builds.reserved",
"mesh-delivery-view.node_haves",
"seat:node-engine.manifest-reads",
"seat:mesh-hand-acts.hand-act"