Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c72f6ca8cb |
@@ -402,22 +402,19 @@ func seatAnnouncement(handlers map[string]link.ToolHandler) micro.Info {
|
||||
var endpoints []micro.EndpointInfo
|
||||
for _, verb := range verbs {
|
||||
schema, _ := json.Marshal(about[verb].Input)
|
||||
// The same shape every tool runtime announces in (node-tools' announce package): the name is
|
||||
// `<seat>__<verb>`, as the protocol's characters allow; the metadata is what identifies it.
|
||||
endpoints = append(endpoints, micro.EndpointInfo{
|
||||
Name: catalogue.ControllerSeatName + "__" + verb,
|
||||
Name: verb,
|
||||
Subject: link.SeatToolSubject(catalogue.ControllerSeatName, verb),
|
||||
QueueGroup: "seat." + catalogue.ControllerSeatName,
|
||||
Metadata: map[string]string{
|
||||
"kind": "seat", "module": catalogue.ControllerSeatName, "tool": verb,
|
||||
"seat": catalogue.ControllerSeatName, "scope": "mesh", "interchangeable": "false",
|
||||
"description": about[verb].Description, "schema": string(schema),
|
||||
"seat": catalogue.ControllerSeatName, "scope": "mesh",
|
||||
},
|
||||
})
|
||||
}
|
||||
return micro.Info{
|
||||
ServiceIdentity: micro.ServiceIdentity{
|
||||
Name: catalogue.ControllerSeatName, ID: "controller", Version: "0.1.0",
|
||||
Name: catalogue.ControllerSeatName, ID: "controller", Version: "1.0.0",
|
||||
Metadata: map[string]string{"seat": catalogue.ControllerSeatName, "scope": "mesh"},
|
||||
},
|
||||
Description: "the mesh's own verbs, answered by the holder of the mesh-controller seat",
|
||||
|
||||
@@ -281,14 +281,10 @@ func TestTheControllerAnnouncesTheVerbsItServes(t *testing.T) {
|
||||
t.Fatalf("%d endpoints announced for %d verbs served", len(info.Endpoints), len(handlers))
|
||||
}
|
||||
for _, e := range info.Endpoints {
|
||||
verb := e.Metadata["tool"]
|
||||
if _, served := handlers[verb]; !served || e.Name != catalogue.ControllerSeatName+"__"+verb {
|
||||
t.Errorf("%s (%s) is announced and not served under that name", e.Name, verb)
|
||||
if _, served := handlers[e.Name]; !served {
|
||||
t.Errorf("%s is announced and not served", e.Name)
|
||||
}
|
||||
if e.Metadata["kind"] != "seat" || e.Metadata["seat"] != catalogue.ControllerSeatName {
|
||||
t.Errorf("%s is not announced as the seat's verb: %v", e.Name, e.Metadata)
|
||||
}
|
||||
if e.Subject != link.SeatToolSubject(catalogue.ControllerSeatName, verb) || e.QueueGroup != "seat."+catalogue.ControllerSeatName {
|
||||
if e.Subject != link.SeatToolSubject(catalogue.ControllerSeatName, e.Name) || e.QueueGroup != "seat."+catalogue.ControllerSeatName {
|
||||
t.Errorf("%s is announced on %s/%s, not where it is served", e.Name, e.Subject, e.QueueGroup)
|
||||
}
|
||||
if e.Metadata["description"] == "" || e.Metadata["schema"] == "" || e.Metadata["scope"] != "mesh" {
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A backend behind the proxy learns the client used TLS and which name it asked for, so the addresses
|
||||
// it writes into its own pages are the ones a client can use (2026-10-03: a forge's Go import tag
|
||||
// named an http clone URL, and Go refused the module path).
|
||||
func TestABackendIsToldTheRequestWasHTTPSAndForWhichName(t *testing.T) {
|
||||
var proto, host, fwdHost, fwdFor string
|
||||
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
proto, host, fwdHost, fwdFor = r.Header.Get("X-Forwarded-Proto"), r.Host, r.Header.Get("X-Forwarded-Host"), r.Header.Get("X-Forwarded-For")
|
||||
}))
|
||||
defer backend.Close()
|
||||
where, _ := url.Parse(backend.URL)
|
||||
req := httptest.NewRequest(http.MethodGet, "https://git.example.org/novox/mesh-sdk/go?go-get=1", nil)
|
||||
req.TLS = &tls.ConnectionState{}
|
||||
req.Host = "git.example.org"
|
||||
req.RemoteAddr = "192.0.2.7:51000"
|
||||
towards(where).ServeHTTP(httptest.NewRecorder(), req)
|
||||
if proto != "https" || fwdHost != "git.example.org" || host != "git.example.org" || fwdFor != "192.0.2.7" {
|
||||
t.Errorf("the backend was told proto=%q host=%q forwarded-host=%q for=%q", proto, host, fwdHost, fwdFor)
|
||||
}
|
||||
}
|
||||
@@ -273,7 +273,7 @@ func (t *table) set(routes map[string][]rule, public map[string]bool) {
|
||||
log.Printf("route %s points at %q, which is not a URL: %v", host, r.target, err)
|
||||
continue
|
||||
}
|
||||
r.to = towards(where)
|
||||
r.to = httputil.NewSingleHostReverseProxy(where)
|
||||
if r.insecure {
|
||||
r.to.Transport = &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
|
||||
}
|
||||
@@ -1060,17 +1060,3 @@ func asPort(v any) (int, bool) {
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
// towards proxies to one backend and tells it what the client asked: **X-Forwarded-Proto, -Host and
|
||||
// -For**, set from the request this proxy received. A backend that builds its own addresses — a forge
|
||||
// writing its clone URL into a page, a login redirect — otherwise sees the plain HTTP hop from this
|
||||
// proxy and writes `http://`, though every client reached it over TLS: Go refused the forge's module
|
||||
// path for exactly that on 2026-10-03, its import tag naming an http clone URL.
|
||||
// The standard library's NewSingleHostReverseProxy sets only X-Forwarded-For.
|
||||
func towards(where *url.URL) *httputil.ReverseProxy {
|
||||
return &httputil.ReverseProxy{Rewrite: func(pr *httputil.ProxyRequest) {
|
||||
pr.SetURL(where)
|
||||
pr.Out.Host = pr.In.Host
|
||||
pr.SetXForwarded()
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -465,9 +465,7 @@ func PermissionsFor(p Principal) (Permissions, error) {
|
||||
pub = append(pub, invoked...)
|
||||
// It says what it serves and may ask what answers (novox/hq ADR 0197): the runtime answers
|
||||
// discovery for each module and seat it carries, and the console it is asks the bus.
|
||||
// One service per runtime process, named for the runtime: the bus lets a principal answer each
|
||||
// request once, so the runtime announces everything it carries under its own name.
|
||||
sub = append(sub, announcing(append([]string{RuntimeModule}, serves...)...)...)
|
||||
sub = append(sub, announcing(serves...)...)
|
||||
pub = append(pub, discovering()...)
|
||||
// Nothing about consumers: it consumes nothing. A module's reactions to events are its
|
||||
// own long-lived process, which ADR 0175 leaves where it is; what moves here is tools.
|
||||
@@ -795,14 +793,12 @@ func invokedSubjects(invokes []string) ([]string, error) {
|
||||
// protocol's discovery (novox/hq ADR 0197): the questions asked of every service, and those asked of
|
||||
// each name it serves — its own and no other's, so it cannot answer for a service it is not.
|
||||
func announcing(names ...string) []string {
|
||||
out := []string{"$SRV.PING", "$SRV.INFO", "$SRV.STATS"}
|
||||
out := []string{"$SRV.PING", "$SRV.INFO"}
|
||||
for _, n := range names {
|
||||
if !safeSubject.MatchString(n) {
|
||||
continue
|
||||
}
|
||||
for _, verb := range []string{"PING", "INFO", "STATS"} {
|
||||
out = append(out, "$SRV."+verb+"."+n, "$SRV."+verb+"."+n+".>")
|
||||
}
|
||||
out = append(out, "$SRV.PING."+n, "$SRV.PING."+n+".>", "$SRV.INFO."+n, "$SRV.INFO."+n+".>")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
+4
-4
@@ -25,7 +25,7 @@ accounts {
|
||||
users = [
|
||||
{ user: "controller", password: "$2a$11$cccccccccccccccccccccc", permissions: {
|
||||
publish: { allow: ["$JS.ACK.CONTROL.controller.>", "$JS.ACK.EVENTS.controller.>", "$JS.API.>", "_INBOX.enrol.>", "mesh.assignment.>", "mesh.control.>", "mesh.mod.*.tool.>", "mesh.node.>", "mesh.seat.mesh-build-machine.accept.>", "mesh.seat.mesh-controller.event.applied", "mesh.seat.mesh-controller.event.built-before", "mesh.seat.mesh-controller.event.refused", "mesh.seat.node-build-agent.accept.>"] }
|
||||
subscribe: { allow: ["$JS.API.>", "$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.gitea.event.pull.merged", "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"] }
|
||||
subscribe: { allow: ["$JS.API.>", "$SRV.INFO", "$SRV.INFO.mesh-controller", "$SRV.INFO.mesh-controller.>", "$SRV.PING", "$SRV.PING.mesh-controller", "$SRV.PING.mesh-controller.>", "_DELIVER.controller", "_DELIVER.controller.>", "_INBOX.controller.>", "mesh.control.>", "mesh.mod.gitea.event.pull.merged", "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"] }
|
||||
allow_responses: { max: 1, ttl: "1m" }
|
||||
} }
|
||||
{ user: "enrol.one", password: "$2a$11$eeeeeeeeeeeeeeeeeeeeee", permissions: {
|
||||
@@ -38,17 +38,17 @@ accounts {
|
||||
} }
|
||||
{ user: "one.telegram", password: "$2a$11$tttttttttttttttttttttt", permissions: {
|
||||
publish: { allow: ["$JS.ACK.EVENTS.one_telegram.>", "$JS.ACK.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker.>", "$JS.API.CONSUMER.INFO.EVENTS.one_telegram", "$JS.API.CONSUMER.INFO.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker", "$JS.API.CONSUMER.MSG.NEXT.EVENTS.one_telegram", "$JS.API.CONSUMER.MSG.NEXT.SEAT_TELEGRAM_SENDER.SEAT_TELEGRAM_SENDER_worker", "$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.one.telegram", "mesh.seat.telegram-sender.event.delivered", "mesh.seat.telegram-sender.event.failed"] }
|
||||
subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.telegram", "$SRV.INFO.telegram.>", "$SRV.PING", "$SRV.PING.telegram", "$SRV.PING.telegram.>", "$SRV.STATS", "$SRV.STATS.telegram", "$SRV.STATS.telegram.>", "_INBOX.one.telegram.>", "mesh.assignment.one.telegram", "mesh.mod.telegram.tool.>", "mesh.seat.telegram-sender.accept.send"] }
|
||||
subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.telegram", "$SRV.INFO.telegram.>", "$SRV.PING", "$SRV.PING.telegram", "$SRV.PING.telegram.>", "_INBOX.one.telegram.>", "mesh.assignment.one.telegram", "mesh.mod.telegram.tool.>", "mesh.seat.telegram-sender.accept.send"] }
|
||||
allow_responses: { max: 1, ttl: "1m" }
|
||||
} }
|
||||
{ user: "two.audit", password: "$2a$11$aaaaaaaaaaaaaaaaaaaaaa", permissions: {
|
||||
publish: { allow: ["$JS.ACK.EVENTS.two_audit.>", "$JS.API.CONSUMER.INFO.EVENTS.two_audit", "$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_audit", "$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.two.audit"] }
|
||||
subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.audit", "$SRV.INFO.audit.>", "$SRV.PING", "$SRV.PING.audit", "$SRV.PING.audit.>", "$SRV.STATS", "$SRV.STATS.audit", "$SRV.STATS.audit.>", "_INBOX.two.audit.>", "mesh.assignment.two.audit", "mesh.mod.audit.tool.>", "mesh.mod.shop.event.order.placed"] }
|
||||
subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.audit", "$SRV.INFO.audit.>", "$SRV.PING", "$SRV.PING.audit", "$SRV.PING.audit.>", "_INBOX.two.audit.>", "mesh.assignment.two.audit", "mesh.mod.audit.tool.>", "mesh.mod.shop.event.order.placed"] }
|
||||
allow_responses: { max: 1, ttl: "1m" }
|
||||
} }
|
||||
{ user: "two.shop", password: "$2a$11$ssssssssssssssssssssss", permissions: {
|
||||
publish: { allow: ["$JS.ACK.EVENTS.two_shop.>", "$JS.API.CONSUMER.INFO.EVENTS.two_shop", "$JS.API.CONSUMER.MSG.NEXT.EVENTS.two_shop", "$JS.API.DIRECT.GET.ASSIGNMENTS.mesh.assignment.two.shop", "mesh.mod.shop.event.order.placed", "mesh.seat.telegram-sender.accept.send"] }
|
||||
subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.shop", "$SRV.INFO.shop.>", "$SRV.PING", "$SRV.PING.shop", "$SRV.PING.shop.>", "$SRV.STATS", "$SRV.STATS.shop", "$SRV.STATS.shop.>", "_INBOX.two.shop.>", "mesh.assignment.two.shop", "mesh.mod.shop.tool.>"] }
|
||||
subscribe: { allow: ["$SRV.INFO", "$SRV.INFO.shop", "$SRV.INFO.shop.>", "$SRV.PING", "$SRV.PING.shop", "$SRV.PING.shop.>", "_INBOX.two.shop.>", "mesh.assignment.two.shop", "mesh.mod.shop.tool.>"] }
|
||||
allow_responses: { max: 1, ttl: "1m" }
|
||||
} }
|
||||
]
|
||||
|
||||
@@ -87,12 +87,6 @@ func (m Manifest) Resolve(built []Built) (Manifest, error) {
|
||||
loads := append([]string(nil), a.Loads...)
|
||||
if a.Loads == nil && len(m.Tools) > 0 {
|
||||
loads = append([]string(nil), a.Entrypoints...)
|
||||
// A bundle compiled to a binary has no entrypoints: the binary is what it is, and what
|
||||
// the runtime starts to serve it (novox/hq ADR 0193). So a Go tools bundle is served
|
||||
// as Go — the runtime execs it — exactly as a TypeScript one is through its launcher.
|
||||
if bin := BinaryOf(a); bin != "" {
|
||||
loads = []string{bin}
|
||||
}
|
||||
}
|
||||
// **Kept, never routed** (ADR 0155): the builder publishes to the store at the address
|
||||
// it reached it by, and a manifest carrying that address names an installation —
|
||||
@@ -248,11 +242,6 @@ func (b *Build) problems(module string) []string {
|
||||
for _, e := range a.Entrypoints {
|
||||
found = found || e == load
|
||||
}
|
||||
// A bundle compiled to a binary is one executable: the runtime loads that or nothing
|
||||
// (novox/hq ADR 0193).
|
||||
if bin := BinaryOf(a); bin != "" {
|
||||
found = load == bin
|
||||
}
|
||||
if !found {
|
||||
problems = append(problems, fmt.Sprintf(
|
||||
"%s: %q says the runtime loads %q, which is not among its entrypoints — "+
|
||||
|
||||
@@ -389,46 +389,3 @@ func TestARuntimeCompiledToABinaryRunsItself(t *testing.T) {
|
||||
t.Errorf("the Go runtime is not told what to serve or whose it is: %v %v", env, process["user"])
|
||||
}
|
||||
}
|
||||
|
||||
// novox/hq ADR 0193: a Go tools bundle is served — its binary is what the runtime starts, delivered
|
||||
// like any tools bundle, named to the runtime where a TypeScript bundle names its launcher.
|
||||
func TestAGoToolsBundleIsServedByItsBinary(t *testing.T) {
|
||||
with := Rendering{ArtifactStore: "anchor.internal:5101",
|
||||
Needed: map[string]map[string]string{RuntimeModule: {"broker": "sealed-credential"}}}
|
||||
lamp := Manifest{Module: "lamp", Version: "1", Tools: []string{"on"},
|
||||
Build: &Build{Artifacts: []Artifact{{Name: "tools", Kind: ArtifactBundle, Language: "go",
|
||||
System: "arch", From: "cmd/lamp-tools"}}}}
|
||||
if p := lamp.Build.problems("lamp"); len(p) != 0 {
|
||||
t.Fatalf("a Go tools bundle was refused: %v", p)
|
||||
}
|
||||
lamp, err := lamp.Resolve([]Built{{Name: "tools", Kind: ArtifactBundle,
|
||||
Reference: ArtifactStoreScheme + "lamp/tools/blobs/" + bundleDigest, Digest: bundleDigest}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fmt.Sprint(lamp.Bundles[0].Loads) != "[lamp-tools]" {
|
||||
t.Fatalf("the runtime loads %v from a Go bundle, want its binary", lamp.Bundles[0].Loads)
|
||||
}
|
||||
out, err := Resolution{Node: "anchor", Account: "ops", Modules: []Manifest{lamp, theRuntime(t)}}.Declaration(with)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fileNamed(out, "lamp."+BundleID("tools")) == nil {
|
||||
t.Errorf("the Go bundle is not delivered: %v", ids(out))
|
||||
}
|
||||
env := fileNamed(out, RuntimeModule+"."+RuntimeProcessID())["env"].(map[string]string)
|
||||
if env[RuntimeToolModules] != "lamp="+BundlePath("lamp", "tools")+"/lamp-tools" {
|
||||
t.Errorf("the runtime is told %q, want the binary", env[RuntimeToolModules])
|
||||
}
|
||||
|
||||
// An artifact may say it explicitly; naming anything but the binary is refused.
|
||||
said := Manifest{Module: "lamp", Version: "1", Build: &Build{Artifacts: []Artifact{{Name: "tools",
|
||||
Kind: ArtifactBundle, Language: "go", System: "arch", Binary: "lamp", Loads: []string{"lamp"}}}}}
|
||||
if p := said.Build.problems("lamp"); len(p) != 0 {
|
||||
t.Errorf("loads naming the binary was refused: %v", p)
|
||||
}
|
||||
said.Build.Artifacts[0].Loads = []string{"tools/index.js"}
|
||||
if p := said.Build.problems("lamp"); len(p) == 0 {
|
||||
t.Error("a Go bundle loading a file it does not contain was admitted")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
// DiscoverySubjects are where one service instance is asked to say what it is.
|
||||
func DiscoverySubjects(name, id string) []string {
|
||||
var out []string
|
||||
for _, verb := range []string{"PING", "INFO", "STATS"} {
|
||||
for _, verb := range []string{"PING", "INFO"} {
|
||||
out = append(out, "$SRV."+verb, "$SRV."+verb+"."+name, "$SRV."+verb+"."+name+"."+id)
|
||||
}
|
||||
return out
|
||||
@@ -35,16 +35,6 @@ func (b OverNATS) Announce(info micro.Info, logger *log.Logger) (func(), error)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Statistics the protocol asks for; the controller keeps none per verb, so it answers its
|
||||
// identity and its endpoints with nothing counted — an honest zero, not a refusal.
|
||||
stats := micro.Stats{ServiceIdentity: info.ServiceIdentity, Type: micro.StatsResponseType}
|
||||
for _, e := range info.Endpoints {
|
||||
stats.Endpoints = append(stats.Endpoints, µ.EndpointStats{Name: e.Name, Subject: e.Subject, QueueGroup: e.QueueGroup})
|
||||
}
|
||||
statsBody, err := json.Marshal(stats)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var subs []*nats.Subscription
|
||||
done := make(chan struct{})
|
||||
stop := func() {
|
||||
@@ -56,11 +46,8 @@ func (b OverNATS) Announce(info micro.Info, logger *log.Logger) (func(), error)
|
||||
for _, subject := range DiscoverySubjects(info.Name, info.ID) {
|
||||
subject := subject
|
||||
body := infoBody
|
||||
switch {
|
||||
case len(subject) >= 9 && subject[:9] == "$SRV.PING":
|
||||
if len(subject) >= 9 && subject[:9] == "$SRV.PING" {
|
||||
body = pingBody
|
||||
case len(subject) >= 10 && subject[:10] == "$SRV.STATS":
|
||||
body = statsBody
|
||||
}
|
||||
bind := func() (*nats.Subscription, error) {
|
||||
return b.Conn.Subscribe(subject, func(msg *nats.Msg) {
|
||||
|
||||
Reference in New Issue
Block a user