From 86342c4399a4a1235653ce67f5f07b7f680d8f96 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 14:04:56 +0100 Subject: [PATCH] GIT12: the forge module's package registry checks a publish against the builder's reservation, asked by the package's name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The holder asks the seat mesh-builds' verb reserved with the publish's name and version in place of builder_versions, and stores only a version reserved for an artifact of the publish's digest; the manifest invokes the verb and takes no builder package. Builds: design 47 G §381, §478, §504 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- .../cmd/package-registry/retention_test.go | 10 ++-- modules/gitea/cmd/package-registry/seats.go | 5 +- .../whole_integration_test.go | 17 +++--- modules/gitea/go.mod | 2 +- modules/gitea/internal/packageseats/holder.go | 8 +-- .../internal/packageseats/holder_test.go | 2 +- .../gitea/internal/packageseats/reserved.go | 42 +++++++++++++++ .../internal/packageseats/reserved_test.go | 54 +++++++++++++++++++ modules/gitea/module.json | 5 +- 9 files changed, 120 insertions(+), 25 deletions(-) create mode 100644 modules/gitea/internal/packageseats/reserved.go create mode 100644 modules/gitea/internal/packageseats/reserved_test.go diff --git a/modules/gitea/cmd/package-registry/retention_test.go b/modules/gitea/cmd/package-registry/retention_test.go index 10116919d..6fd6081aa 100644 --- a/modules/gitea/cmd/package-registry/retention_test.go +++ b/modules/gitea/cmd/package-registry/retention_test.go @@ -450,12 +450,12 @@ func TestToolsAreTheOnesTheReadmeNamesAndSayWhatTheyDo(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) } // Both package seats are claimed serving the four verbs this bundle answers as ., over its own incoming - // and transfers, asking the builder's numbering (design 51 *Publishing*). + // and transfers, asking the builder's reservation (design 51 *Publishing*, design 47 §381). claimed := map[string]string{} for _, c := range m.Claims { claimed[c.Name] = strings.Join(c.Serves, ",") @@ -488,7 +488,7 @@ func TestToolsAreTheOnesTheReadmeNamesAndSayWhatTheyDo(t *testing.T) { t.Errorf("transfers has options (a lifetime): %s", req) } // The retention asks the builder's versions; the forge's other bundles ask the catalog half and record a re-read. - if strings.Join(m.Invokes, ",") != "seat:mesh-catalog.revision,seat:mesh-catalog.registrations,seat:mesh-hand-acts.hand-act,mesh-delivery-builder.builder_versions" { + if strings.Join(m.Invokes, ",") != "seat:mesh-catalog.revision,seat:mesh-catalog.registrations,seat:mesh-hand-acts.hand-act,seat:mesh-builds.reserved" { t.Errorf("the manifest invokes %v", m.Invokes) } if len(m.Tools) != 0 { diff --git a/modules/gitea/cmd/package-registry/seats.go b/modules/gitea/cmd/package-registry/seats.go index 5b409e603..4ec5770e8 100644 --- a/modules/gitea/cmd/package-registry/seats.go +++ b/modules/gitea/cmd/package-registry/seats.go @@ -19,7 +19,6 @@ import ( "encoding/json" "errors" "fmt" - builds "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "io" "net/http" "net/url" @@ -274,11 +273,11 @@ func recordAct(q meshhandactsclient.Request) error { // boundCap is incoming's cap from its binding, read once per bundle. var boundCap = parts.BusCap(packageseats.IncomingObjects) -// busHolder is the holder over the runtime's bus state, the builder's numbering and the forge. +// busHolder is the holder over the runtime's bus state, the builder's reservation and the forge. func busHolder(forge func() (*Forge, error)) (*packageseats.Holder, error) { return &packageseats.Holder{Objects: parts.BusObjects(packageseats.IncomingObjects), Transfers: parts.BusTransfers[packageseats.Opening](packageseats.TransfersState), - Builder: builds.BuilderVersions{Ask: stdio.Ask}, Store: seatStore{forge}, Say: say, Finding: finding, + Builder: packageseats.Reserved{Ask: stdio.Ask}, Store: seatStore{forge}, Say: say, Finding: finding, Cap: boundCap, Record: recordAct, Idle: parts.BusArm(packageseats.IdleState)}, nil } diff --git a/modules/gitea/cmd/package-registry/whole_integration_test.go b/modules/gitea/cmd/package-registry/whole_integration_test.go index 6c766e8af..5a52ce7a4 100644 --- a/modules/gitea/cmd/package-registry/whole_integration_test.go +++ b/modules/gitea/cmd/package-registry/whole_integration_test.go @@ -6,7 +6,7 @@ package main // the built package-registry bundle, played over its real stdin and stdout as the node's tool runtime plays it, its // `incoming` an object store and its `transfers` and `findings` key-value buckets of a nats-server of this test alone // (so the publish in parts runs through mesh-sdk's parts.BusObjects and BusTransfers, ruling 4 of the lead's 14:0x), the -// builder's builder_versions answered on mesh/ask, and the forge an +// builder's reserved answered on mesh/ask, and the forge an // httptest server that takes reads only with the tools' token and writes only with the packages account's. // // A Go package the builder numbered goes up in two chunks, is uploaded whole as the packages account and says @@ -37,7 +37,6 @@ import ( "github.com/nats-io/nats.go" "github.com/nats-io/nats.go/jetstream" - "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" "git.novox.be/novox/mesh-sdk/go/testbus" "gitea/internal/packageseats" @@ -117,11 +116,15 @@ func (p *player) handle(m wire) { } _ = json.Unmarshal(m.Params, &a) switch { - case m.Method == "mesh/ask" && a.Key == builds.BuilderVersionsAddress: - var q struct{ Name, Version, Digest string } + case m.Method == "mesh/ask" && a.Key == packageseats.Address: + var q struct{ Name, Version string } _ = json.Unmarshal(a.Body, &q) d, ok := p.numbered[q.Name+"@"+q.Version] - p.answer(m.ID, map[string]any{"recorded": ok && d == q.Digest, "why": "not numbered with that digest"}, nil) + if !ok { + p.answer(m.ID, nil, errors.New("no version of "+q.Name+" is reserved")) + break + } + p.answer(m.ID, map[string]any{"module": "m", "version": q.Version, "artifacts": []map[string]any{{"artifact": "m:a", "digest": d}}}, nil) case m.Method == "mesh/seat.publish": p.answer(m.ID, map[string]any{}, p.nc.Publish(a.Subject, a.Body)) case m.Method == "mesh/publish": @@ -395,12 +398,12 @@ func TestThePackageSeatsWhole(t *testing.T) { t.Errorf("an ended transfer is still kept: %v", keys) } - // One the builder did not number: refused before the forge sees it, said, and the seat's finding written. + // One the builder did not reserve: refused before the forge sees it, said, and the seat's finding written. other := bigZip(t, "git.example/acme/sdk@v0.1.16") od := "sha256:" + sha(other) transfer, _ = publish("0.1.16", od, len(other)) _ = send(transfer, od, other) - if o := event("refused"); !strings.Contains(fmt.Sprint(o["reason"]), "did not number") { + if o := event("refused"); !strings.Contains(fmt.Sprint(o["reason"]), "no version of git.example/acme/sdk is reserved") { t.Errorf("refused says %v", o) } findings, _ := js.KeyValue(ctx, "gitea_findings") diff --git a/modules/gitea/go.mod b/modules/gitea/go.mod index 62607444b..d84ba67cf 100644 --- a/modules/gitea/go.mod +++ b/modules/gitea/go.mod @@ -8,8 +8,8 @@ require ( git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-contracts/provisioning/go/clients/meshprovisionerclient v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-delivery/contracts/go/seats/gopackageregistry 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/contracts/go/seats/npmpackageregistry 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/sdk v0.0.0-00010101000000-000000000000 git.novox.be/novox/mesh-sdk/go v0.0.0-00010101000000-000000000000 github.com/nats-io/nats-server/v2 v2.11.17 diff --git a/modules/gitea/internal/packageseats/holder.go b/modules/gitea/internal/packageseats/holder.go index 7b03101fb..10f05af16 100644 --- a/modules/gitea/internal/packageseats/holder.go +++ b/modules/gitea/internal/packageseats/holder.go @@ -108,7 +108,7 @@ type Version struct { Published time.Time } -// Builder answers whether the builder numbered a version with a digest (the builder package's builds.BuilderVersions). +// Builder answers whether the builder reserved a version with a digest (Reserved). type Builder interface { Recorded(name, version, digest string) (ok bool, why string, err error) } @@ -165,14 +165,14 @@ func key(seat, transfer string) string { return seat + "_" + transfer } func (h *Holder) receiver() *parts.Receiver[Opening] { return &parts.Receiver[Opening]{Objects: h.Objects, Transfers: h.Transfers, Cap: IncomingCap, Bound: h.Cap, MaxChunk: MaxChunk, Idle: TransferIdle, Arm: h.Idle, Full: IncomingFullPct, Now: h.now, - // The builder's numbering is asked before anything is uploaded (ADR 0605 §4). + // The builder's reservation is asked before anything is uploaded (ADR 0605 §4, design 47 §381). Check: func(t Transfer) error { 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 }, diff --git a/modules/gitea/internal/packageseats/holder_test.go b/modules/gitea/internal/packageseats/holder_test.go index 63ebf4de8..0495e041a 100644 --- a/modules/gitea/internal/packageseats/holder_test.go +++ b/modules/gitea/internal/packageseats/holder_test.go @@ -252,7 +252,7 @@ func TestTheForgeHoldsBothPackageSeats(t *testing.T) { numbered bool data []byte want string - }{{"the builder did not number it", false, file, "did not number"}} { + }{{"the builder did not reserve it", false, file, "did not reserve"}} { b := newBed(c.numbered) if _, err := b.h.Publish(NpmSeat, nodeBuild, pub("x", "0.1.0", file)); err != nil { t.Fatal(err) diff --git a/modules/gitea/internal/packageseats/reserved.go b/modules/gitea/internal/packageseats/reserved.go new file mode 100644 index 000000000..ef5228a84 --- /dev/null +++ b/modules/gitea/internal/packageseats/reserved.go @@ -0,0 +1,42 @@ +package packageseats + +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 +} diff --git a/modules/gitea/internal/packageseats/reserved_test.go b/modules/gitea/internal/packageseats/reserved_test.go new file mode 100644 index 000000000..b5ea18483 --- /dev/null +++ b/modules/gitea/internal/packageseats/reserved_test.go @@ -0,0 +1,54 @@ +package packageseats + +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-sdk","version":"0.1.19","artifacts":[{"artifact":"mesh-sdk:ts","digest":"sha256:aa"}]}`, ok: true}, + {name: "another artifact of it carries the digest", answer: `{"module":"mesh-sdk","version":"0.1.19","artifacts":[{"artifact":"mesh-sdk:go"},{"artifact":"mesh-sdk:ts","digest":"sha256:aa"}]}`, ok: true}, + {name: "another digest", answer: `{"module":"mesh-sdk","version":"0.1.19","artifacts":[{"artifact":"mesh-sdk:ts","digest":"sha256:bb"}]}`, why: "another digest than sha256:aa"}, + {name: "another version reserved", answer: `{"module":"mesh-sdk","version":"0.1.20","artifacts":[]}`, why: "reserved 0.1.20 for @novox/mesh-sdk, not 0.1.19"}, + {name: "the builder refused the name", err: errors.New("no entry carries @novox/mesh-sdk"), why: "no entry carries @novox/mesh-sdk"}, + {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("@novox/mesh-sdk", "0.1.19", "sha256:aa") + if asked != `seat:mesh-builds.reserved {"name":"@novox/mesh-sdk","version":"0.1.19"}` { + 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() +} diff --git a/modules/gitea/module.json b/modules/gitea/module.json index 7cabb3024..5255a55f8 100644 --- a/modules/gitea/module.json +++ b/modules/gitea/module.json @@ -1,9 +1,6 @@ { "module": "gitea", "version": "1", - "packages": { - "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds": "0.1" - }, "moved": true, "requires": [ "postgres-database", @@ -651,7 +648,7 @@ "seat:mesh-catalog.revision", "seat:mesh-catalog.registrations", "seat:mesh-hand-acts.hand-act", - "mesh-delivery-builder.builder_versions" + "seat:mesh-builds.reserved" ], "consumes": [ "mesh-catalog.catalog-revision",