Merge main, and close what the confirmation review found (hq ADR 0272)
mesh/merge-gate pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered

- Any failure of a request the link may have delivered now says the line
  may have run; "nothing ran" is said only of a request the link knows it
  never asked (no link open, nobody listening: link.ErrNotAsked).
- A test holds that a connection saying nothing is answered within its
  deadline, set at accept, and gives its slot back.
- The README says the socket refuses a process outside the machine's own
  namespaces, so flatpak, toolbox and distrobox shells are refused.
This commit is contained in:
jochen
2026-10-09 13:09:48 +02:00
parent 655f515aa7
commit f684967c3b
7 changed files with 101 additions and 11 deletions
+13
View File
@@ -151,6 +151,19 @@ carries. The claim is not. The foundation is four container services, and:
So `foundation.lock` here is a real bundle with a placeholder's content. Saying that plainly
beats shipping a host that claims a foundation it has never raised.
## mesh-cli's socket (novox/hq ADR 0272)
The engine serves `/run/nox-mesh-host/cli.sock` for `mesh-cli`, the operator's command (`internal/meshcli`). It
reads the asking account from the kernel (`SO_PEERCRED`) and asks the controller on this node's own subject; the
controller decides what the line may do, and the engine runs nothing it receives. One line per connection, each
connection's deadline set the moment it is accepted, two lines at once per account and eight for the machine,
"busy" beyond that.
**A process outside the machine's own namespaces is refused**, whatever its user id: the peer's mount and user
namespace must be the engine's. So `mesh-cli` run inside a flatpak, a toolbox or distrobox container, or any other
container — even one running as the operator's own id — is refused with the reason; run it from the machine's own
shell.
## A capability is detected, never assumed
The reason this is the first thing built rather than a detail of it.
+2 -1
View File
@@ -230,7 +230,8 @@ func (l *natsLink) Ask(ctx context.Context, subject string, body []byte) ([]byte
case err == nil:
return reply, nil
case errors.Is(err, nats.ErrNoResponders):
return nil, fmt.Errorf("nothing on this machine answers %s", subject)
// The server says nobody subscribes the subject: delivered to no one.
return nil, fmt.Errorf("%w: nothing on this machine answers %s", ErrNotAsked, subject)
}
if refused := l.refusal(subject, before); refused != "" {
return nil, fmt.Errorf("%s: %v%s", subject, err, refused)
+6 -2
View File
@@ -552,15 +552,19 @@ func (q *Queue) AskController(ctx context.Context, body []byte) ([]byte, error)
bus := q.bus
q.mu.Unlock()
if bus == nil {
return nil, errors.New("this machine has no link to the bus open now")
return nil, fmt.Errorf("%w: this machine has no link to the bus open now", ErrNotAsked)
}
asker, ok := bus.(ToolBus)
if !ok {
return nil, errors.New("the link to the bus open now cannot ask the controller")
return nil, fmt.Errorf("%w: the link to the bus open now cannot ask the controller", ErrNotAsked)
}
return asker.Ask(ctx, CLISubject(q.Membership.Node), body)
}
// ErrNotAsked is a request the link knows it never delivered: no link open, or nobody listening on its subject. Any
// other failure of a request may come after it was delivered, so what came of it is not known (novox/hq ADR 0272).
var ErrNotAsked = errors.New("not asked")
// ReadToolAnswer reads a tool's reply envelope — `{result, error}`, as the node tools answer every call —
// as a health answer: an error is not healthy, and so is a result that does not say `healthy: true`.
func ReadToolAnswer(reply []byte) (bool, string, error) {
+9 -4
View File
@@ -8,6 +8,7 @@ import (
"time"
"git.novox.be/novox/mesh-sdk/go/cli"
"github.com/novox/mesh-host/internal/link"
)
// The controller answers a line as it answers every call (novox/hq ADR 0272, review): in an envelope, `result` or
@@ -15,6 +16,9 @@ import (
// followed by the same account on the same node until it ends. Its side holds the same field names (mesh-controller
// internal/link, the mesh-cli field names' test).
// ErrNotAsked is the link's: a request it knows it never delivered.
var ErrNotAsked = link.ErrNotAsked
// AskFunc asks the controller one request on this node's subject and returns its reply.
type AskFunc func(ctx context.Context, body []byte) ([]byte, error)
@@ -40,11 +44,12 @@ func AskTheController(ctx context.Context, ask AskFunc, asked Asked) cli.Answer
body, _ := json.Marshal(asked)
reply, err := ask(ctx, body)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) || ctx.Err() != nil {
return cli.NotKnown("", "no answer from the controller in time")
// **Only a request the link knows it never asked ran nothing**: no link open, nobody listening. Any other
// failure may come after the request was delivered, and the line may have run (review of ADR 0272).
if errors.Is(err, ErrNotAsked) {
return cli.Refusal("the controller was not reached, so nothing ran: " + err.Error())
}
// Never delivered to a controller: no link, no subscriber, the bus refusing the publish.
return cli.Refusal("the controller was not reached, so nothing ran: " + err.Error())
return cli.NotKnown("", "no answer from the controller ("+err.Error()+")")
}
call := ""
for {
+27 -2
View File
@@ -83,16 +83,41 @@ func TestNoAnswerInTimeSaysTheLineMayHaveRun(t *testing.T) {
// A request the bus never carried to a controller ran nothing, and says so.
func TestARequestNobodyTookSaysNothingRan(t *testing.T) {
c := &controllerSays{script: []func(Asked) ([]byte, error){func(Asked) ([]byte, error) {
return nil, fmt.Errorf("nothing on this machine answers mesh.control.laptop.cli")
return nil, fmt.Errorf("%w: nothing on this machine answers mesh.control.laptop.cli", ErrNotAsked)
}}}
a := AskTheController(context.Background(), c.ask, Asked{Line: []string{"status"}, Account: "op"})
if !strings.Contains(a.Refused, "nothing ran") || a.Unknown != "" {
t.Fatalf("a request nobody took reads %+v", a)
}
c = &controllerSays{script: []func(Asked) ([]byte, error){func(Asked) ([]byte, error) {
return nil, errors.New("this machine has no link to the bus open now")
return nil, fmt.Errorf("%w: this machine has no link to the bus open now", ErrNotAsked)
}}}
if a := AskTheController(context.Background(), c.ask, Asked{Line: []string{"status"}}); !strings.Contains(a.Refused, "nothing ran") {
t.Fatalf("no link reads %+v", a)
}
}
// **Any error once the request may have been delivered is not known, never "nothing ran"** (review of ADR 0272):
// only a request the link says it never asked — no link open, nobody listening — ran nothing.
func TestOnlyARequestNeverAskedSaysNothingRan(t *testing.T) {
for _, c := range []struct {
err error
nothing bool
}{
{fmt.Errorf("%w: this machine has no link to the bus open now", ErrNotAsked), true},
{fmt.Errorf("%w: nothing on this machine answers mesh.control.laptop.cli", ErrNotAsked), true},
{errors.New("nats: connection closed"), false},
{errors.New("mesh.control.laptop.cli: permissions violation"), false},
{context.DeadlineExceeded, false},
} {
err := c.err
ctl := &controllerSays{script: []func(Asked) ([]byte, error){func(Asked) ([]byte, error) { return nil, err }}}
a := AskTheController(context.Background(), ctl.ask, Asked{Line: []string{"push"}, Account: "op"})
if c.nothing && (!strings.Contains(a.Refused, "nothing ran") || a.Unknown != "") {
t.Errorf("%v reads %+v; it was never asked", err, a)
}
if !c.nothing && (a.Refused != "" || !strings.Contains(a.Unknown, "may have run")) {
t.Errorf("%v reads %+v; it may have been asked", err, a)
}
}
}
+42
View File
@@ -0,0 +1,42 @@
package meshcli
import (
"context"
"net"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/cli"
)
// **A connection that says nothing is bounded from the moment it is accepted** (review of ADR 0272): its deadline is
// set before any lookup, so it is answered and closed within readWithin, holds no slot after, and asks nothing.
func TestAConnectionThatSaysNothingIsAnsweredWithinItsDeadline(t *testing.T) {
was := readWithin
readWithin = 200 * time.Millisecond
t.Cleanup(func() { readWithin = was })
asked := false
socket := serving(t, func(context.Context, Asked) cli.Answer {
asked = true
return cli.Answer{}
})
conn, err := net.Dial("unix", socket)
if err != nil {
t.Fatal(err)
}
defer conn.Close()
_ = conn.SetDeadline(time.Now().Add(5 * time.Second))
a, err := cli.ReadAnswer(conn)
if err != nil {
t.Fatalf("a silent connection was not answered within its deadline: %v", err)
}
if asked || a.Refused == "" || !strings.Contains(a.Refused, "nothing was asked") {
t.Fatalf("a silent connection was asked (%v) or answered %+v", asked, a)
}
// Its slot was given back: the account's next line is not busy.
b, err := cli.Ask(context.Background(), socket, []string{"status"})
if err != nil || strings.Contains(b.Refused, "busy") || !asked {
t.Fatalf("the next line after a silent one reads %+v (%v), asked %v", b, err, asked)
}
}
+2 -2
View File
@@ -42,8 +42,8 @@ type Asked struct {
type Forward func(ctx context.Context, asked Asked) cli.Answer
// readWithin bounds how long a connection may take to say its line: a program that connects and says nothing
// holds nothing for long.
const readWithin = 30 * time.Second
// holds nothing for long. A variable so a test need not wait.
var readWithin = 30 * time.Second
// atOnce bounds how many lines are asked at the same time, and perAccount how many of them one account's: a person
// types one at a time, so more is a program on the machine. One over either bound is answered busy at once — the