messenger: an early desk unlink bars nothing, root lifts any bar, no root-free ask inside the work, no button without its effect (the confirmation review of 2026-10-09)

N1-router: another account's /start offered the desk an unlink of the operator's account in its first
hour, and a desk click, which a program on the desk machine can forge, unlinked it and barred it for a
day, again and again. An unlink at the desk in the first hour now bars nothing: any account, the
operator's too, links again at once with a code from the desk; the day's bar stays for an account
removed by its own approval or by root, and root's allow-link lifts any bar. The words say which.

Lows: the controller's root-free word is asked before the work is taken (it waited up to 10 s inside
it) and the answer is judged with that fresh word; inside the work only a recent word is read, and a
failed ask drops the old one. An answer whose effect the content rule withholds is not offered.
mesh-sdk moves to 16984aa. Each test failed before its fix.
This commit is contained in:
2026-10-09 17:08:17 +02:00
parent ff79defc2b
commit d136806745
8 changed files with 260 additions and 21 deletions
+5 -1
View File
@@ -94,6 +94,10 @@ func (h *Holder) choices(r *Record, kind string) []Choice {
// stands, closes the ask with a warrant for its asker (asks.go); Details are shown where they were asked.
// Anything else is refused, and said there. The same answer is acted on once.
func (h *Holder) Chosen(kind string, c asks.Choice) (string, error) {
// The controller's root-free word is asked before the work is taken (the confirmation review of 2026-10-09):
// up to RootFreeWithin, which no ask, answer or link should wait behind. Asked fresh here, for this answer,
// and judged with in it.
root := h.rootWordFor(kind)
h.work.Lock()
defer h.work.Unlock()
h.mu.Lock()
@@ -124,7 +128,7 @@ func (h *Holder) Chosen(kind string, c asks.Choice) (string, error) {
return "too old", errorf("it was given %s ago", roughly(now.Sub(c.At)))
}
if key != "" {
out, err := h.judge(kind, c, key)
out, err := h.judge(kind, c, key, root)
h.remember(c.ID, out)
return out, err
}
+60 -5
View File
@@ -180,7 +180,7 @@ func (h *Holder) carriers(level asks.Level) []string {
case asks.Acknowledge:
out = append(out, k.Kind)
case asks.Approve:
if h.Bench.Promises(k.Kind, "verified-sender") && h.linked(k.Kind) && h.rootFree(k, false) == "" {
if h.Bench.Promises(k.Kind, "verified-sender") && h.linked(k.Kind) && h.rootFree(k, true) == "" {
out = append(out, k.Kind)
}
}
@@ -270,11 +270,20 @@ func (h *Holder) rootFree(k KindInfo, cached bool) string {
kept, ok := h.rootWord[key]
h.mu.Unlock()
var verdicts map[string]RootVerdict
if cached && ok && now.Sub(kept.at) < RootFreeKept && now.Sub(kept.at) >= 0 {
switch {
case cached && ok && now.Sub(kept.at) < RootFreeKept && now.Sub(kept.at) >= 0:
verdicts = kept.verdicts
} else {
case cached:
// Read inside the work, which never waits on the controller: no recent word is no pass. warmRootFree
// asks outside it.
return "the controller's word on whether an agent can become root there is not recent"
default:
got, err := h.askRootFree(machines)
if err != nil {
// A word that could not be asked again is not kept from before: no recent word, no pass.
h.mu.Lock()
delete(h.rootWord, key)
h.mu.Unlock()
return "whether an agent can become root where it runs could not be asked: " + err.Error()
}
verdicts = got
@@ -302,6 +311,51 @@ func (h *Holder) rootFree(k KindInfo, cached bool) string {
return ""
}
// warmRootFree asks the controller's root-free word for every kind that proves its sender, outside the work,
// so what is rendered and offered inside it reads a recent word.
func (h *Holder) warmRootFree() {
if h.Bench == nil {
return
}
now := h.Now()
for _, k := range h.Bench.Kinds() {
if !h.Bench.Promises(k.Kind, "verified-sender") || !h.linked(k.Kind) || h.Node == "" || k.Node == "" {
continue
}
key := h.Node
if k.Node != h.Node {
key += "," + k.Node
}
h.mu.Lock()
kept, ok := h.rootWord[key]
h.mu.Unlock()
if ok && now.Sub(kept.at) >= 0 && now.Sub(kept.at) < RootFreeKept/2 {
continue // recent enough: asked again when half its time has gone
}
_ = h.rootFree(k, false)
}
}
// rootWordFor asks the controller's root-free word for a kind fresh, before the work is taken, and answers
// what judging an answer of that kind then reads: the word for that very kind's placement, or why there is none.
// A kind that does not prove its sender is asked nothing: its answers never approve.
func (h *Holder) rootWordFor(kind string) func(KindInfo) string {
if h.Bench == nil {
return func(KindInfo) string { return "the router does not know the channels yet" }
}
asked, held := h.Bench.Kind(kind)
if !held || !h.Bench.Promises(kind, "verified-sender") {
return func(KindInfo) string { return Display(kind) + " cannot prove who answered" }
}
why := h.rootFree(asked, false)
return func(info KindInfo) string {
if info.Kind != asked.Kind || info.Node != asked.Node || info.Module != asked.Module {
return "where " + Display(kind) + " runs changed while the answer was judged; answer again"
}
return why
}
}
// rootWord is the controller's last word on a set of machines, and when it was asked.
type rootWord struct {
at time.Time
@@ -342,6 +396,7 @@ const ConditionOwner = "mesh-controller"
// grants that module alone. An error is returned only when the ask could not be recorded, so the queue
// offers it again; a refusal is said to the asker and the work is taken.
func (h *Holder) TakeAsk(verb, asker string, body json.RawMessage) error {
h.warmRootFree() // before the work: offering an approval reads the recent word inside it
h.work.Lock()
defer h.work.Unlock()
h.mu.Lock()
@@ -745,7 +800,7 @@ func joinOr(labels []string) string {
// judge is an answer to an ask's option, chosen on a kind: refused at the first failure, in words said on
// the kind; otherwise the ask closed and the warrant published (ADR 0259 §5).
func (h *Holder) judge(kind string, c asks.Choice, key string) (string, error) {
func (h *Holder) judge(kind string, c asks.Choice, key string, root func(KindInfo) string) (string, error) {
e := h.askOf(key)
if e == nil {
return h.refuseChoice(kind, c, "it belongs to no ask this router holds")
@@ -823,7 +878,7 @@ func (h *Holder) judge(kind string, c asks.Choice, key string) (string, error) {
return h.refuseChoice(kind, c, "this account was linked at "+id.Linked.In(h.zone()).Format("15:04")+
" and can answer this question from "+id.ApproveFrom.In(h.zone()).Format("15:04"))
}
if why := h.rootFree(info, false); why != "" {
if why := root(info); why != "" {
return h.refuseChoice(kind, c, why)
}
if o.Level == asks.Approve {
+16 -1
View File
@@ -554,6 +554,7 @@ func (h *Holder) cleared(key string, quiet bool) {
// Tick does what time asks: reminders, retries, what is held, and forgetting what cleared long
// enough ago that a new raise is a new message.
func (h *Holder) Tick() {
h.warmRootFree() // outside the work: what is offered inside it reads a recent word
h.work.Lock()
defer h.work.Unlock()
h.mu.Lock()
@@ -609,6 +610,7 @@ func (h *Holder) Tick() {
// Flush sends what is due on each channel; the runtime calls it every few seconds, so a held
// message waits its window and not a minute more.
func (h *Holder) Flush() {
h.warmRootFree() // outside the work: what is offered inside it reads a recent word
h.work.Lock()
defer h.work.Unlock()
h.mu.Lock()
@@ -642,6 +644,7 @@ func (h *Holder) sayOn(r *Record, what, kind string) Message {
if kind == "" {
return m
}
withheld := map[string]bool{} // the labels of answers whose effect could not be said
if rec := h.openAsk(r); rec != nil {
// Who asks, as the bus proves it (the asker is the subject's, never the body's), what each answer does,
// and what happens if nobody answers (the review of 2026-10-09).
@@ -650,13 +653,25 @@ func (h *Holder) sayOn(r *Record, what, kind string) Message {
m.Body = strings.TrimSpace(m.Body + "\n" + line)
}
}
// **An answer whose effect is not said is not offered** (the confirmation review of 2026-10-09): its line
// refused by the content rule, its button went out all the same, and a tap would authorise what the
// person was not shown.
for _, o := range rec.Ask.Options {
if _, ok := Check(o.Label + ": " + o.Does + "."); !ok || strings.TrimSpace(o.Does) == "" {
withheld[o.Label] = true
}
}
if line := h.elsewhere(rec, kind); line != "" {
if _, ok := Check(line); ok {
m.Body = strings.TrimSpace(m.Body + "\n" + line)
}
}
}
m.Actions = h.choices(r, kind)
for _, c := range h.choices(r, kind) {
if !withheld[c.Label] {
m.Actions = append(m.Actions, c)
}
}
return m
}
+2
View File
@@ -186,6 +186,7 @@ func (h *Holder) offerDeskUnlink(id Identity) bool {
// Link takes a link asked for on a kind: a code shown where only the operator sees it, or a refusal said on
// the kind.
func (h *Holder) Link(kind string, l asks.LinkAsked) string {
h.warmRootFree() // outside the work: a question it asks reads a recent word
h.work.Lock()
defer h.work.Unlock()
reply := func(title, body string) string {
@@ -276,6 +277,7 @@ func (h *Holder) Link(kind string, l asks.LinkAsked) string {
// Prove answers a code sent on a kind: the link it confirms, or why not. Never said in an event.
func (h *Holder) Prove(kind string, c asks.Code) asks.ProofAnswer {
h.warmRootFree() // outside the work: a question it asks reads a recent word
h.work.Lock()
defer h.work.Unlock()
no := func(words string) asks.ProofAnswer { return asks.ProofAnswer{Accepted: false, Words: words} }
@@ -5,6 +5,9 @@ package main
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"regexp"
"strings"
"sync"
"testing"
@@ -394,3 +397,139 @@ func TestTwoAnswersAtOnceMakeOneWarrant(t *testing.T) {
t.Errorf("not exactly one chosen: %v", outs)
}
}
// The confirmation review of 2026-10-09, N1-router: another account's /start offers the desk an unlink of the
// operator's account while it is in its first hour; a desk click — which a program on the desk machine can forge
// — unlinked it and barred it for a day, again and again, so the operator never approved anything. A desk
// unlink in the first hour bars nothing: the operator links again at once with a code from the desk, and the
// next link waits for nobody. The bar is for an account removed deliberately later.
func TestAnUnlinkFromTheDeskInTheFirstHourBarsNothing(t *testing.T) {
h, tg, dt, c, store, _ := newRouter(t)
link(h, store, c, "42", c.now().Add(-10*time.Minute)) // the operator's own, linked just now
h.Link("telegram", asks.LinkAsked{ID: "l1", At: c.now(), Sender: asks.Sender{Identity: "666", Display: "X", Authenticated: true}})
asked := dt.sends[len(dt.sends)-1]
ch := choiceOf(ticketFor(asked, "Unlink it"), "g14", c.now()) // a click nobody can prove was the operator's
ch.Sender.Authenticated = false
if out, err := h.Chosen("desktop", ch); err != nil || out != "chosen" {
t.Fatalf("%s %v", out, err)
}
if len(store.ids) != 0 {
t.Fatalf("not unlinked: %v", store.ids)
}
if u := h.recentUnlink("telegram"); u != nil {
t.Errorf("a desk unlink in the first hour bars relinking: %+v", u)
}
// The operator sends /start again: a code is shown at once, and the code links without waiting for anybody.
c.pass(time.Minute)
stand(h, c)
h.Link("telegram", asks.LinkAsked{ID: "l2", At: c.now(), Sender: asks.Sender{Identity: "42", Display: "Jo", Authenticated: true}})
code := ""
for _, m := range dt.sends {
if strings.Contains(m.Body, "number 42") && strings.Contains(m.Body, "send this code") {
code = codeIn(t, m.Body)
}
}
if code == "" {
t.Fatalf("no code was shown to relink the operator's account; said %q", tg.sends[len(tg.sends)-1].Body)
}
if a := h.Prove("telegram", asks.Code{Sender: asks.Sender{Identity: "42", Authenticated: true}, Code: code, Purpose: "link"}); !a.Accepted ||
!strings.Contains(a.Words, "Linked") {
t.Fatalf("the operator's code was not taken at once: %+v", a)
}
}
// And break-glass lifts any bar: root's allow-link at the router's machine, with no link waiting, ends a day's bar.
func TestBreakGlassLiftsABarOnRelinking(t *testing.T) {
h, tg, _, c, store, _ := newRouter(t)
link(h, store, c, "42", c.now().Add(-2*time.Hour))
_, _ = h.Unlink("telegram")
_, _ = h.Chosen("telegram", choiceOf(ticketFor(tg.sends[len(tg.sends)-1], "Unlink"), "42", c.now()))
if h.recentUnlink("telegram") == nil {
t.Fatal("the control: a deliberate unlink bars relinking for a day")
}
h.StateDir = t.TempDir()
was := fileOwner
t.Cleanup(func() { fileOwner = was })
fileOwner = func(os.FileInfo) uint32 { return 0 }
_ = os.WriteFile(filepath.Join(h.StateDir, "allow-link.telegram"), nil, 0o600)
h.Tick()
if u := h.recentUnlink("telegram"); u != nil {
t.Errorf("root's allow-link left the bar: %+v", u)
}
}
// codeIn is the six digits a link's notice shows, "123 456".
func codeIn(t *testing.T, body string) string {
t.Helper()
m := regexp.MustCompile(`(\d{3}) (\d{3})\.`).FindStringSubmatch(body)
if m == nil {
t.Fatalf("no code in %q", body)
}
return m[1] + m[2]
}
// The confirmation review of 2026-10-09 (low): the router asked the controller's root-free word — up to ten
// seconds — while holding its work, so every ask, answer and link waited behind one slow controller. It now
// asks before it takes the work, and judges with the word it was given then.
func TestTheRootFreeWordIsNotAskedWhileTheWorkIsHeld(t *testing.T) {
h, tg, _, c, store, w := newRouter(t)
link(h, store, c, "42", c.now().Add(-2*time.Hour))
submit(t, h, "mesh-delivery", heldAsk(c, "d-1"))
settle(h, c)
release := ticketFor(tg.sends[0], "Release")
asking, slow := make(chan struct{}), make(chan struct{})
var once sync.Once
free := h.RootFree
h.RootFree = func(machines []string) (map[string]RootVerdict, error) {
once.Do(func() { close(asking); <-slow })
return free(machines)
}
done := make(chan string, 1)
go func() {
out, _ := h.Chosen("telegram", choiceOf(release, "42", c.now()))
done <- out
}()
<-asking
linked := make(chan struct{})
go func() {
h.Link("telegram", asks.LinkAsked{ID: "l9", At: c.now(), Sender: asks.Sender{Identity: "77", Authenticated: true}})
close(linked)
}()
select {
case <-linked:
case <-time.After(2 * time.Second):
close(slow)
t.Fatal("a link waited for the controller's root-free word: the work was held while it was asked")
}
close(slow)
if out := <-done; out != "chosen" || len(w.of("mesh-delivery")) != 1 {
t.Fatalf("the answer after the word: %s, %v", out, w.of("mesh-delivery"))
}
}
// The confirmation review of 2026-10-09 (low, on M2): an answer whose line — what it does — the content rule
// withholds is not offered either; its button went out alone, and a tap would choose what was not shown.
func TestAnAnswerWhoseEffectIsWithheldIsNotOffered(t *testing.T) {
h, _, _, c, store, _ := newRouter(t)
link(h, store, c, "42", c.now().Add(-2*time.Hour))
submit(t, h, "mesh-delivery", heldAsk(c, "d-1"))
key := askKey("mesh-delivery", "d-1")
h.mu.Lock()
e := h.asks[key]
e.rec.Ask.Options[0].Does = "the delivery reads /etc/shadow"
h.asks[key] = e
r := h.open["ask."+key]
h.mu.Unlock()
m := h.sayOn(r, "asked", "telegram")
for _, a := range m.Actions {
if a.Label == "Release" {
t.Errorf("Release was offered with its effect withheld: %+v", m.Actions)
}
}
if strings.Contains(m.Body, "/etc/shadow") {
t.Errorf("the withheld words were said: %q", m.Body)
}
if len(m.Actions) == 0 {
t.Error("the answers whose effect was said were not offered either")
}
}
+35 -9
View File
@@ -49,6 +49,11 @@ func (a SelfAct) act() asks.Act {
type Unlinked struct {
Identity Identity `json:"identity"`
At time.Time `json:"at"`
// Early is an unlink at the desk in the account's first hour: it bars nothing (the confirmation review of
// 2026-10-09, N1-router). A desk click is anyone's at the desk machine, and a bar it set — the account not
// linked again for a day, the next link waiting for it — let a forged click lock the operator out, again
// and again. The bar is for an account removed deliberately: by its own approval, or by root.
Early bool `json:"early,omitempty"`
}
// askSelf puts a question of the router's own to one account, on its kind, and keeps it like any ask.
@@ -123,10 +128,10 @@ func (h *Holder) selfAct(rec AskRecord, w asks.Warrant) {
"machine, can unlink it now.", Urgent: true})
return
}
h.unlinkNow(rec.Act.Kind, rec.Act.Identity, "at the desk, within the hour after it was linked")
h.unlinkNow(rec.Act.Kind, rec.Act.Identity, "at the desk, within the hour after it was linked", true)
return
}
h.unlinkNow(rec.Act.Kind, rec.Act.Identity, "approved by the account itself")
h.unlinkNow(rec.Act.Kind, rec.Act.Identity, "approved by the account itself", false)
case "link":
if rec.Act.Link != nil && rec.Act.Link.Identity == rec.Act.Identity {
h.linkNow(*rec.Act.Link, "approved by the account it replaces")
@@ -136,6 +141,7 @@ func (h *Holder) selfAct(rec AskRecord, w asks.Warrant) {
// Unlink asks the account linked on a kind whether it is to be unlinked: an approve ask put to it alone.
func (h *Holder) Unlink(kind string) (map[string]any, error) {
h.warmRootFree() // outside the work: a question it asks reads a recent word
h.work.Lock()
defer h.work.Unlock()
ids := h.identitiesOf(kind)
@@ -160,7 +166,7 @@ func (h *Holder) Unlink(kind string) (map[string]any, error) {
// unlinkNow unlinks one account of a kind: told first, by name, then every channel. It unlinks the account it
// is named, and nothing when another is linked now.
func (h *Holder) unlinkNow(kind, identity, how string) {
func (h *Holder) unlinkNow(kind, identity, how string, early bool) {
if h.Asks == nil {
return
}
@@ -175,10 +181,14 @@ func (h *Holder) unlinkNow(kind, identity, how string) {
h.Logf("[messenger] the unlink of %s on %s could not be recorded: %v", id.Display, kind, err)
return
}
h.unlinked(kind, Unlinked{Identity: *id, At: h.Now()})
h.unlinked(kind, Unlinked{Identity: *id, At: h.Now(), Early: early})
after := "A new link within a day waits for that account's approval, and that account itself can be linked " +
"again only after a day."
if early {
after = "Any account, this one too, can be linked again at once with a code shown on your desk."
}
h.announce(Message{Title: "No " + Display(kind) + " account is linked as you any more",
Body: fmt.Sprintf("Unlinked: %s (number %s), %s. A new link within a day waits for that account's approval, "+
"and that account itself can be linked again only after a day.", id.Display, id.Identity, how), Urgent: true})
Body: fmt.Sprintf("Unlinked: %s (number %s), %s. %s", id.Display, id.Identity, how, after), Urgent: true})
h.record(Sent{At: h.Now(), Channel: kind, Key: "(link)", What: "unlinked", Outcome: id.Display + ", " + how})
}
@@ -250,7 +260,7 @@ func (h *Holder) breakGlass() {
switch what {
case "unlink":
if ids := h.identitiesOf(kind); len(ids) > 0 {
h.unlinkNow(kind, ids[0].Identity, where+", as root")
h.unlinkNow(kind, ids[0].Identity, where+", as root", false)
done = fmt.Sprintf("the %s account %s (number %s) was unlinked", Display(kind), ids[0].Display, ids[0].Identity)
} else {
done = "nothing was unlinked: no " + Display(kind) + " account is linked"
@@ -262,6 +272,11 @@ func (h *Holder) breakGlass() {
} else {
done = "nothing was linked: no link with its code given waits on " + Display(kind)
}
// And whatever bar an unlink set is lifted: root decided who may be linked.
if h.recentUnlink(kind) != nil {
h.liftBar(kind)
done += "; the bar on linking an account again after an unlink was lifted"
}
}
ended := h.endSelfAsks("root decided " + where)
body := "Root " + where + " asked it: " + done + "."
@@ -315,7 +330,18 @@ func (h *Holder) unlinked(kind string, u Unlinked) {
}
}
// recentUnlink is the account unlinked on a kind less than a day ago, if any.
// liftBar forgets the last unlink of a kind: root's allow-link at the router's machine.
func (h *Holder) liftBar(kind string) {
h.mu.Lock()
delete(h.lastUnlink, kind)
h.mu.Unlock()
if h.Asks != nil {
_ = h.Asks.PutUnlinked(kind, Unlinked{})
}
}
// recentUnlink is the account unlinked deliberately on a kind less than a day ago, if any: an early desk
// unlink bars nothing.
func (h *Holder) recentUnlink(kind string) *Unlinked {
h.mu.Lock()
u, ok := h.lastUnlink[kind]
@@ -325,7 +351,7 @@ func (h *Holder) recentUnlink(kind string) *Unlinked {
u, ok = *got, true
}
}
if !ok || h.Now().Sub(u.At) >= RelinkWaits {
if !ok || u.Early || u.At.IsZero() || h.Now().Sub(u.At) >= RelinkWaits {
return nil
}
return &u
+1 -1
View File
@@ -2,4 +2,4 @@ module messenger
go 1.22
require git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261009095928-76902998cd39
require git.novox.be/novox/mesh-sdk/go v0.1.10-0.20261009115850-16984aafc689
+2 -4
View File
@@ -1,4 +1,2 @@
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261009081503-d4077b473ea8 h1:soqhLNpEXThdq6PdiPy6ExxjJ+yjhh1N1n9E3j1CtrM=
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261009081503-d4077b473ea8/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261009095928-76902998cd39 h1:WHW6CgbuTxP7M+qRBOgzsiG9vT49xdkZ/rarc9/vKMA=
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261009095928-76902998cd39/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.10-0.20261009115850-16984aafc689 h1:Ti2P9nwders7YQ/hq3X/dPo+CXMj5pdUcfA5P/c12CU=
git.novox.be/novox/mesh-sdk/go v0.1.10-0.20261009115850-16984aafc689/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=