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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
Reference in New Issue
Block a user