Declare the controller's grants bucket, written by the controller alone and readable through mesh-controller.grants, so the assigner can read its sixth rule's key (issue 1394, subtask C9a; ADR 0528 clause 2); the SDK pin moves to the layouts' commit, captured beside

This commit is contained in:
2026-10-23 15:39:40 +02:00
parent 791786d5b8
commit 548681cc4e
11 changed files with 92 additions and 9 deletions
+1 -1
View File
@@ -3,7 +3,7 @@ module github.com/novox/mesh-controller
go 1.26.0 go 1.26.0
require ( require (
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261022190740-104533e8c0c9 git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261023132058-f625d98713e4
github.com/jackc/pgx/v5 v5.10.0 github.com/jackc/pgx/v5 v5.10.0
github.com/nats-io/nats-server/v2 v2.11.17 github.com/nats-io/nats-server/v2 v2.11.17
github.com/nats-io/nats.go v1.54.0 github.com/nats-io/nats.go v1.54.0
+3 -2
View File
@@ -17,9 +17,10 @@ import (
const StatesAnnouncedBucket = announce.KeptBucket const StatesAnnouncedBucket = announce.KeptBucket
// controllerReadable are the controller's states others may read: the record mirrors (the packages each node declares // controllerReadable are the controller's states others may read: the record mirrors (the packages each node declares
// among them, which the pacman module reads, ADR 0507 §5), the chain's buckets, the checks running and the plans building. // among them, which the pacman module reads, ADR 0507 §5), the chain's buckets, the checks running, the plans building and
// the grants issued, which the assigner reads (ADR 0528 clause 2).
var controllerReadable = []string{MirrorAssignments, MirrorDeclaredConfig, MirrorPredefined, ChainBuilds, ChainRollouts, var controllerReadable = []string{MirrorAssignments, MirrorDeclaredConfig, MirrorPredefined, ChainBuilds, ChainRollouts,
ChecksRunning, PlansBuilding, MirrorDeclaredPackages} ChecksRunning, PlansBuilding, MirrorDeclaredPackages, ControllerGrants}
// ControllerAnnouncement is what the controller announces it keeps (ADR 0495 clause 2): every bucket of its own by its local // ControllerAnnouncement is what the controller announces it keeps (ADR 0495 clause 2): every bucket of its own by its local
// name, the ones others may read marked readable. It announces itself, as any module does from its code. // name, the ones others may read marked readable. It announces itself, as any module does from its code.
+1 -1
View File
@@ -39,7 +39,7 @@ func TestTheControllerAnnouncesWhatItKeeps(t *testing.T) {
} }
} }
want := []string{ChainBuilds, ChecksRunning, MirrorDeclaredConfig, PlansBuilding, MirrorPredefined, ChainRollouts, want := []string{ChainBuilds, ChecksRunning, MirrorDeclaredConfig, PlansBuilding, MirrorPredefined, ChainRollouts,
MirrorAssignments, MirrorDeclaredPackages} MirrorAssignments, MirrorDeclaredPackages, ControllerGrants}
slices.Sort(want) slices.Sort(want)
if !slices.Equal(readable, want) { if !slices.Equal(readable, want) {
t.Fatalf("readable %v, want %v", readable, want) t.Fatalf("readable %v, want %v", readable, want)
+28 -2
View File
@@ -152,6 +152,16 @@ const (
ChainAssignmentBytes = 64 << 20 ChainAssignmentBytes = 64 << 20
) )
// GrantsBucket holds, keyed `<module>.<fingerprint hex>`, each fingerprint of a module whose bus credential the controller
// has issued (novox/hq ADR 0528 clause 2, ADR 0429 §6 rule 6, issue 1394): written by the controller alone once the bus's
// user list composed with the module's manifest at that fingerprint was sent to the bus's machine; read by the assigner,
// whose sixth rule moves a node's want to a fingerprint only once its grant exists. The name and layout are mesh-sdk's
// (delivery.ControllerGrantsBucket, ControllerGrantKey).
var GrantsBucket = BucketName(ControllerSeat, ControllerGrants)
// ControllerGrants is the grants bucket's local name, as a reader names it in `reads` after `mesh-controller.`.
const ControllerGrants = "grants"
// ChainBuckets are the three buckets of the chain: builds, rollouts and assignments. // ChainBuckets are the three buckets of the chain: builds, rollouts and assignments.
func ChainBuckets() []string { return []string{BuildsBucket, RolloutsBucket, AssignmentsBucket} } func ChainBuckets() []string { return []string{BuildsBucket, RolloutsBucket, AssignmentsBucket} }
@@ -224,14 +234,16 @@ func IsControllerBucket(bucket string) bool {
bucket == DeclaredPackagesBucket || bucket == DeclaredPackagesBucket ||
bucket == MergeWindowBucket || bucket == RolloutSilentBucket || bucket == CheckRunningBucket || bucket == MergeWindowBucket || bucket == RolloutSilentBucket || bucket == CheckRunningBucket ||
bucket == DeliveryMergesBucket || bucket == BuildsBucket || bucket == RolloutsBucket || bucket == DeliveryMergesBucket || bucket == BuildsBucket || bucket == RolloutsBucket ||
bucket == PlansBuildingBucket || bucket == delivery.HaveBucket || bucket == StatesAnnouncedBucket bucket == PlansBuildingBucket || bucket == delivery.HaveBucket || bucket == StatesAnnouncedBucket ||
bucket == GrantsBucket
} }
// ControllerBuckets are the controller's own buckets, in the order they are asserted. // ControllerBuckets are the controller's own buckets, in the order they are asserted.
func ControllerBuckets() []string { func ControllerBuckets() []string {
return []string{LeaseBucket, CallsBucket, HandActsBucket, ConditionsBucket, ConditionHistoryBucket, AskedBucket, return []string{LeaseBucket, CallsBucket, HandActsBucket, ConditionsBucket, ConditionHistoryBucket, AskedBucket,
AssignmentsBucket, DeclaredConfigBucket, PredefinedBucket, DeclaredPackagesBucket, MergeWindowBucket, RolloutSilentBucket, CheckRunningBucket, AssignmentsBucket, DeclaredConfigBucket, PredefinedBucket, DeclaredPackagesBucket, MergeWindowBucket, RolloutSilentBucket, CheckRunningBucket,
DeliveryMergesBucket, PlansBuildingBucket, BuildsBucket, RolloutsBucket, delivery.HaveBucket, StatesAnnouncedBucket} DeliveryMergesBucket, PlansBuildingBucket, BuildsBucket, RolloutsBucket, delivery.HaveBucket, StatesAnnouncedBucket,
GrantsBucket}
} }
// ControllerBucketsAsserter is what raising the controller's buckets needs of a connection. // ControllerBucketsAsserter is what raising the controller's buckets needs of a connection.
@@ -402,6 +414,20 @@ func (j *JetStream) EnsureControllerBuckets() error {
}); err != nil { }); err != nil {
return fmt.Errorf("asserting bucket %s: %w", StatesAnnouncedBucket, err) return fmt.Errorf("asserting bucket %s: %w", StatesAnnouncedBucket, err)
} }
// **The grants issued: no age** (ADR 0528 clause 2, issue 1394). One key a module's fingerprint, never removed: a grant
// that expired would hold back a want whose credential exists.
if _, err := js.CreateOrUpdateKeyValue(ctx, jetstream.KeyValueConfig{
Bucket: GrantsBucket,
Description: "each fingerprint of a module whose bus credential the controller issued (novox/hq ADR 0528): " +
"`<module>.<fingerprint hex>` holds the accounts issued, put once the bus's user list composed with the module's " +
"manifest at it was sent; read by the assigner through `reads`; written by the controller alone",
History: delivery.ControllerGrantsHistory,
MaxValueSize: 4 << 10,
MaxBytes: 64 << 20,
Storage: jetstream.FileStorage,
}); err != nil {
return fmt.Errorf("asserting bucket %s: %w", GrantsBucket, err)
}
// **The merges mesh-delivery announced: keys that age** (ADR 0420 §7, issue 1025). One message a key; a week. // **The merges mesh-delivery announced: keys that age** (ADR 0420 §7, issue 1025). One message a key; a week.
if _, err := js.CreateOrUpdateKeyValue(ctx, jetstream.KeyValueConfig{ if _, err := js.CreateOrUpdateKeyValue(ctx, jetstream.KeyValueConfig{
Bucket: DeliveryMergesBucket, Bucket: DeliveryMergesBucket,
+1 -1
View File
@@ -120,7 +120,7 @@ func TestTheMirrorsListAndTheWritersTableAgree(t *testing.T) {
module := Principal{Kind: KindModule, Module: ControllerSeat, Node: "one"} module := Principal{Kind: KindModule, Module: ControllerSeat, Node: "one"}
rowed := map[string]bool{} rowed := map[string]bool{}
for _, row := range WritersTable { for _, row := range WritersTable {
if !strings.HasSuffix(row.State, " mirror") && !strings.HasSuffix(row.State, " of the chain") { if !strings.HasSuffix(row.State, " mirror") && !strings.HasSuffix(row.State, " of the chain") && row.State != "the grants issued" {
continue continue
} }
for _, subject := range row.Subjects { for _, subject := range row.Subjects {
+44
View File
@@ -0,0 +1,44 @@
package broker
import (
"slices"
"testing"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// Issue 1394/C9a (ADR 0528 clause 2): the grants bucket is mesh-sdk's, one of the controller's own buckets asserted at its
// start, written by the controller alone; the assigner, naming mesh-controller.grants in `reads`, reads it and writes
// nothing of it.
func TestTheAssignerReadsTheGrantsAndWritesNone(t *testing.T) {
if GrantsBucket != delivery.ControllerGrantsBucket {
t.Fatalf("the grants bucket is %s, mesh-sdk names %s", GrantsBucket, delivery.ControllerGrantsBucket)
}
if !slices.Contains(controllerReadableStates(), ControllerGrants) {
t.Fatalf("a module cannot name %s in `reads`: %v", GrantsBucket, controllerReadableStates())
}
if _, other := MirrorsWrittenByAnother[ControllerGrants]; other {
t.Error("the controller writes the grants itself")
}
if !IsControllerBucket(GrantsBucket) || !slices.Contains(ControllerBuckets(), GrantsBucket) {
t.Errorf("%s is not among the controller's buckets", GrantsBucket)
}
perms, err := PermissionsFor(Principal{Kind: KindModule, Node: "novox", Module: "mesh-delivery-assigner", PasswordHash: "x",
Reads: []string{"mesh-controller.grants"}})
if err != nil {
t.Fatal(err)
}
stream := "KV_" + GrantsBucket
has(t, perms.Publish, "$JS.API.STREAM.INFO."+stream)
has(t, perms.Publish, "$JS.API.CONSUMER.CREATE."+stream+".>")
for _, p := range perms.Publish {
if SubjectsOverlap(p, "$KV."+GrantsBucket+".mesh-x") {
t.Errorf("the assigner may write %s through %q", GrantsBucket, p)
}
}
controller, err := PermissionsFor(Principal{Kind: KindController, PasswordHash: "x"})
if err != nil {
t.Fatal(err)
}
has(t, controller.Publish, "$KV."+GrantsBucket+".>")
}
+6
View File
@@ -460,6 +460,12 @@ var DegradableGrants = []DegradableGrant{
Reason: "carried only at genesis: the controller composes its own permissions and pushes them to the bus at its " + Reason: "carried only at genesis: the controller composes its own permissions and pushes them to the bus at its " +
"first push, and writes the chain only after that, holding the lease (ADR 0408 §2 and §8, issue 1089)", "first push, and writes the chain only after that, holding the lease (ADR 0408 §2 and §8, issue 1089)",
}, },
{
Subject: "$KV." + GrantsBucket + ".>",
Reason: "carried only at genesis: the controller composes its own permissions and pushes them to the bus at its " +
"first push, and writes a grant only after a send of the bus's user list, which comes after that (ADR 0528 " +
"clause 2, issue 1394)",
},
{ {
Subject: "$KV." + RolloutsBucket + ".>", Subject: "$KV." + RolloutsBucket + ".>",
Reason: "carried only at genesis: the controller composes its own permissions and pushes them to the bus at its " + Reason: "carried only at genesis: the controller composes its own permissions and pushes them to the bus at its " +
+1 -1
View File
@@ -29,7 +29,7 @@ accounts {
] ]
users = [ users = [
{ user: "controller", password: "$2a$11$cccccccccccccccccccccc", permissions: { { user: "controller", password: "$2a$11$cccccccccccccccccccccc", permissions: {
publish: { allow: ["$JS.ACK.CONTROL.controller.>", "$JS.ACK.DEAD_LETTER_NOTICES.controller.>", "$JS.ACK.EVENTS.controller-merges.>", "$JS.ACK.EVENTS.controller.>", "$JS.API.>", "$KV.SEAT_MESH_BUILD_MACHINE_cancelled.>", "$KV.SEAT_NODE_BUILD_AGENT_cancelled.>", "$KV.mesh-controller_asked.>", "$KV.mesh-controller_assignments.>", "$KV.mesh-controller_builds.>", "$KV.mesh-controller_calls.>", "$KV.mesh-controller_checks-running.>", "$KV.mesh-controller_condition-history.>", "$KV.mesh-controller_conditions.>", "$KV.mesh-controller_declared-config.>", "$KV.mesh-controller_declared-packages.>", "$KV.mesh-controller_delivery-merges.>", "$KV.mesh-controller_hand-acts.>", "$KV.mesh-controller_lease.>", "$KV.mesh-controller_merge-window.>", "$KV.mesh-controller_plans-building.>", "$KV.mesh-controller_predefined.>", "$KV.mesh-controller_rollout-silent.>", "$KV.mesh-controller_rollouts.>", "$KV.mesh-controller_states-announced.>", "$SRV.INFO", "_INBOX.enrol.>", "mesh.again.>", "mesh.assignment.>", "mesh.events.dead.>", "mesh.mod.*.tool.>", "mesh.mod.mesh-controller.event.states", "mesh.node.>", "mesh.seat.mesh-build-machine.accept.>", "mesh.seat.mesh-build-machine.tool.>", "mesh.seat.mesh-controller.event.applied", "mesh.seat.mesh-controller.event.assignment-changed", "mesh.seat.mesh-controller.event.built-before", "mesh.seat.mesh-controller.event.checked", "mesh.seat.mesh-controller.event.condition-changed", "mesh.seat.mesh-controller.event.condition-cleared", "mesh.seat.mesh-controller.event.condition-raised", "mesh.seat.mesh-controller.event.declared-config-changed", "mesh.seat.mesh-controller.event.doctor-heartbeat", "mesh.seat.mesh-controller.event.healer-acted", "mesh.seat.mesh-controller.event.plan-moved", "mesh.seat.mesh-controller.event.predefined-changed", "mesh.seat.mesh-controller.event.refused", "mesh.seat.mesh-controller.event.rolled-back", "mesh.seat.mesh-controller.event.secret-replaced", "mesh.seat.mesh-controller.event.sent", "mesh.seat.mesh-controller.tool.plans", "mesh.seat.mesh-controller.tool.push", "mesh.seat.mesh-controller.tool.upgrade", "mesh.seat.mesh-delivery.tool.close", "mesh.seat.mesh-delivery.tool.groups", "mesh.seat.mesh-delivery.tool.release", "mesh.seat.mesh-delivery.tool.stalled", "mesh.seat.mesh-delivery.tool.stop", "mesh.seat.mesh-delivery.tool.times", "mesh.seat.node-backup.tool.backed-up.*", "mesh.seat.node-backup.tool.now.*", "mesh.seat.node-build-agent.accept.>", "mesh.seat.node-build-agent.check.>", "mesh.seat.node-build-agent.tool.>", "mesh.seat.node-intrusion-prevention.tool.banned.*", "mesh.seat.node-launcher.tool.secret.*", "mesh.seat.node-service-manager.tool.restart.*", "mesh.seat.operator-channel.tool.tell"] } publish: { allow: ["$JS.ACK.CONTROL.controller.>", "$JS.ACK.DEAD_LETTER_NOTICES.controller.>", "$JS.ACK.EVENTS.controller-merges.>", "$JS.ACK.EVENTS.controller.>", "$JS.API.>", "$KV.SEAT_MESH_BUILD_MACHINE_cancelled.>", "$KV.SEAT_NODE_BUILD_AGENT_cancelled.>", "$KV.mesh-controller_asked.>", "$KV.mesh-controller_assignments.>", "$KV.mesh-controller_builds.>", "$KV.mesh-controller_calls.>", "$KV.mesh-controller_checks-running.>", "$KV.mesh-controller_condition-history.>", "$KV.mesh-controller_conditions.>", "$KV.mesh-controller_declared-config.>", "$KV.mesh-controller_declared-packages.>", "$KV.mesh-controller_delivery-merges.>", "$KV.mesh-controller_grants.>", "$KV.mesh-controller_hand-acts.>", "$KV.mesh-controller_lease.>", "$KV.mesh-controller_merge-window.>", "$KV.mesh-controller_plans-building.>", "$KV.mesh-controller_predefined.>", "$KV.mesh-controller_rollout-silent.>", "$KV.mesh-controller_rollouts.>", "$KV.mesh-controller_states-announced.>", "$SRV.INFO", "_INBOX.enrol.>", "mesh.again.>", "mesh.assignment.>", "mesh.events.dead.>", "mesh.mod.*.tool.>", "mesh.mod.mesh-controller.event.states", "mesh.node.>", "mesh.seat.mesh-build-machine.accept.>", "mesh.seat.mesh-build-machine.tool.>", "mesh.seat.mesh-controller.event.applied", "mesh.seat.mesh-controller.event.assignment-changed", "mesh.seat.mesh-controller.event.built-before", "mesh.seat.mesh-controller.event.checked", "mesh.seat.mesh-controller.event.condition-changed", "mesh.seat.mesh-controller.event.condition-cleared", "mesh.seat.mesh-controller.event.condition-raised", "mesh.seat.mesh-controller.event.declared-config-changed", "mesh.seat.mesh-controller.event.doctor-heartbeat", "mesh.seat.mesh-controller.event.healer-acted", "mesh.seat.mesh-controller.event.plan-moved", "mesh.seat.mesh-controller.event.predefined-changed", "mesh.seat.mesh-controller.event.refused", "mesh.seat.mesh-controller.event.rolled-back", "mesh.seat.mesh-controller.event.secret-replaced", "mesh.seat.mesh-controller.event.sent", "mesh.seat.mesh-controller.tool.plans", "mesh.seat.mesh-controller.tool.push", "mesh.seat.mesh-controller.tool.upgrade", "mesh.seat.mesh-delivery.tool.close", "mesh.seat.mesh-delivery.tool.groups", "mesh.seat.mesh-delivery.tool.release", "mesh.seat.mesh-delivery.tool.stalled", "mesh.seat.mesh-delivery.tool.stop", "mesh.seat.mesh-delivery.tool.times", "mesh.seat.node-backup.tool.backed-up.*", "mesh.seat.node-backup.tool.now.*", "mesh.seat.node-build-agent.accept.>", "mesh.seat.node-build-agent.check.>", "mesh.seat.node-build-agent.tool.>", "mesh.seat.node-intrusion-prevention.tool.banned.*", "mesh.seat.node-launcher.tool.secret.*", "mesh.seat.node-service-manager.tool.restart.*", "mesh.seat.operator-channel.tool.tell"] }
subscribe: { allow: ["$JS.API.>", "$JS.EVENT.ADVISORY.CONSUMER.DELETED.>", "$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>", "$SRV.INFO", "$SRV.INFO.mesh-controller", "$SRV.INFO.mesh-controller.>", "$SRV.PING", "$SRV.PING.mesh-controller", "$SRV.PING.mesh-controller.>", "$SRV.STATS", "$SRV.STATS.mesh-controller", "$SRV.STATS.mesh-controller.>", "_DELIVER.controller", "_DELIVER.controller.>", "_INBOX.controller.>", "mesh.control.>", "mesh.mod.*.event.provisioner.failing", "mesh.mod.*.event.provisioner.recovered", "mesh.mod.*.event.provisioner.retirement", "mesh.mod.gitea.event.pull.merged", "mesh.mod.gitea.event.pull.updated", "mesh.mod.mesh-catalog.event.catching-up", "mesh.mod.mesh-catalog.event.upgraded", "mesh.seat.mesh-build-machine.event.built", "mesh.seat.mesh-controller.tool.>", "mesh.seat.node-build-agent.event.built", "mesh.seat.operator-channel.event.decided.mesh-controller"] } subscribe: { allow: ["$JS.API.>", "$JS.EVENT.ADVISORY.CONSUMER.DELETED.>", "$JS.EVENT.ADVISORY.CONSUMER.MAX_DELIVERIES.>", "$SRV.INFO", "$SRV.INFO.mesh-controller", "$SRV.INFO.mesh-controller.>", "$SRV.PING", "$SRV.PING.mesh-controller", "$SRV.PING.mesh-controller.>", "$SRV.STATS", "$SRV.STATS.mesh-controller", "$SRV.STATS.mesh-controller.>", "_DELIVER.controller", "_DELIVER.controller.>", "_INBOX.controller.>", "mesh.control.>", "mesh.mod.*.event.provisioner.failing", "mesh.mod.*.event.provisioner.recovered", "mesh.mod.*.event.provisioner.retirement", "mesh.mod.gitea.event.pull.merged", "mesh.mod.gitea.event.pull.updated", "mesh.mod.mesh-catalog.event.catching-up", "mesh.mod.mesh-catalog.event.upgraded", "mesh.seat.mesh-build-machine.event.built", "mesh.seat.mesh-controller.tool.>", "mesh.seat.node-build-agent.event.built", "mesh.seat.operator-channel.event.decided.mesh-controller"] }
allow_responses: { max: 1, ttl: "1m" } allow_responses: { max: 1, ttl: "1m" }
} } } }
+4
View File
@@ -242,6 +242,10 @@ var WritersTable = []WriterRow{
{State: "the rollouts of the chain", Writer: "controller (lease holder), compare-and-set on the key's revision", {State: "the rollouts of the chain", Writer: "controller (lease holder), compare-and-set on the key's revision",
KeptIn: "key-value " + RolloutsBucket + ", sixty-four revisions of each key, none removed", KeptIn: "key-value " + RolloutsBucket + ", sixty-four revisions of each key, none removed",
Others: "read through `reads` as mesh-controller." + ChainRollouts, Subjects: kvOf(RolloutsBucket), Writes: isController}, Others: "read through `reads` as mesh-controller." + ChainRollouts, Subjects: kvOf(RolloutsBucket), Writes: isController},
// The grants issued (novox/hq ADR 0528 clause 2, issue 1394): the controller's, read by the assigner for its sixth rule.
{State: "the grants issued", Writer: "controller (lease holder), once the bus's user list carrying a module's manifest at a fingerprint was sent",
KeptIn: "key-value " + GrantsBucket + ", one key a module's fingerprint, none removed",
Others: "read through `reads` as mesh-controller." + ControllerGrants, Subjects: kvOf(GrantsBucket), Writes: isController},
// What each machine has of each module (novox/hq ADR 0429 §1, design 47): written by that machine's node-engine under // What each machine has of each module (novox/hq ADR 0429 §1, design 47): written by that machine's node-engine under
// its own node's keys, never by the controller; the view and the watchman read it. // its own node's keys, never by the controller; the view and the watchman read it.
{State: "a machine's have", Writer: "the node-engine of the machine, under its own keys `<node>.>`", {State: "a machine's have", Writer: "the node-engine of the machine, under its own keys `<node>.>`",
+2
View File
@@ -46,6 +46,8 @@ var designRows = []string{
// The chain beside the assignments, the record itself and no mirror of one (ADR 0408, design 47, issue 1089). // The chain beside the assignments, the record itself and no mirror of one (ADR 0408, design 47, issue 1089).
"the builds of the chain", "the builds of the chain",
"the rollouts of the chain", "the rollouts of the chain",
// The grants issued, which the assigner's sixth rule reads (ADR 0528 clause 2, issue 1394).
"the grants issued",
// What each machine has of each module, written by its node-engine under its own keys (ADR 0429 §1, issue 1136). // What each machine has of each module, written by its node-engine under its own keys (ADR 0429 §1, issue 1136).
"a machine's have", "a machine's have",
// The contents of a declaration's large files, in the bus's object bucket (ADR 0486 §2, issue 1697). // The contents of a declaration's large files, in the bus's object bucket (ADR 0486 §2, issue 1697).
+1 -1
View File
@@ -9,7 +9,7 @@ a second place to clean besides the catalogue itself.
mesh-catalog e0bc919d4945bb5f13f971f74643a0b714cf45da modules/*/module.json, modules/nats/Dockerfile mesh-catalog e0bc919d4945bb5f13f971f74643a0b714cf45da modules/*/module.json, modules/nats/Dockerfile
mesh-host 9d9e3a46c4f5943cfffb413d962ce48b75e2fd39 examples/foundation-first-node.lock mesh-host 9d9e3a46c4f5943cfffb413d962ce48b75e2fd39 examples/foundation-first-node.lock
mesh-sdk 104533e8c0c9116fd1cbb02c474bafbbe229ce22 conformance/events/module-event.json mesh-sdk f625d98713e478e37962e59e4421213770735d34 conformance/events/module-event.json
To move them, from this repository's root, with the two repositories checked out beside it: To move them, from this repository's root, with the two repositories checked out beside it: