package main // Asks and warrants (novox/hq ADR 0259): the router holds every ask, judges each answer and issues a // warrant; the asker performs the option with its own grants. The router performs nothing. // // - **An ask arrives on the seat's work queue under its asker's name** (`accept.ask.`), which the // bus grants that asker alone; the name is read from the subject, never from the body. It is recorded // in the bucket `asks` under `.` before the work is acknowledged, so a restart loses none. // - **It is refused at once, in words, on its asker's `decided` subject** when its words break a rule, // when no ready channel can carry any of its answers, or when its asker already has three open. An // option no channel can carry is named in the words, with where it can still be given. // - **Each option gets a one-time ticket per channel kind that can carry it**: a button carries only the // ticket. Bound to the ask, the option and the kind; dead when the ask closes or expires. // - **An answer is judged in order, refusing at the first failure and saying so on the channel**: the ticket // is known, unused and for this kind; the ask is open and unexpired; the sender is the operator's // linked account where the kind verifies its sender; the kind's capabilities, from the controller's // record of its claim, meet the option's level; for approve, the account was linked an hour ago or more. // Then the ask is closed by compare-and-set — a second answer loses — and the warrant published. // - **A warrant not yet heard by its asker is published again** every minute until the bus takes it. import ( "crypto/rand" "encoding/hex" "encoding/json" "errors" "fmt" "sort" "strings" "time" "git.novox.be/novox/mesh-sdk/go/asks" ) // ErrChanged is a compare-and-set that lost: another writer was first. var ErrChanged = errors.New("written by another writer first") // LinkedApproveAfter is how long a newly linked account waits before it can approve (decided by the // operator on 2026-10-08, until an authenticator-app code exists). const LinkedApproveAfter = time.Hour // StateOpen is an ask not yet ended; every other state is the asks.Outcome it ended with. const StateOpen = "open" // Ticket is one option's one-time ticket on one channel kind. type Ticket struct { Option string `json:"option"` Kind string `json:"kind"` Used bool `json:"used,omitempty"` } // AskRecord is one ask as the router keeps it, under `.` in the bucket `asks`. Its asker reads // it there under its own name. type AskRecord struct { Asker string `json:"asker"` Ask asks.Ask `json:"ask"` State string `json:"state"` Opened time.Time `json:"opened"` Closed time.Time `json:"closed,omitempty"` Tickets map[string]Ticket `json:"tickets,omitempty"` Warrant *asks.Warrant `json:"warrant,omitempty"` // Told: the warrant, or the end, was published to the asker. Told bool `json:"told,omitempty"` // Record is the message the ask is shown on: a condition's key, or its own. Record string `json:"record,omitempty"` // Only is the one account that may answer it, on its kind: an ask of the router's own, put to the account // an unlink or a relink concerns (novox/hq ADR 0259 §7). Only *Identity `json:"only,omitempty"` // Act is what the router does on its own ask's warrant: unlink, or link (selfact.go). Act *SelfAct `json:"act,omitempty"` } // Identity is one account linked as the operator on a channel kind. type Identity struct { Kind string `json:"kind"` Identity string `json:"identity"` Display string `json:"display,omitempty"` Linked time.Time `json:"linked"` ApproveFrom time.Time `json:"approve-from"` } // AskStore is the router's own state for asks and the operator's identities (ADR 0201). type AskStore interface { GetAsk(key string) (*AskRecord, uint64, error) CreateAsk(key string, r AskRecord) (uint64, error) UpdateAsk(key string, r AskRecord, revision uint64) (uint64, error) AskKeys() ([]string, error) GetIdentity(key string) (*Identity, error) PutIdentity(key string, id Identity) error DeleteIdentity(key string) error IdentityKeys() ([]string, error) GetLink(kind string) (*PendingLink, error) PutLink(kind string, l PendingLink) error DeleteLink(kind string) error GetUnlinked(kind string) (*Unlinked, error) PutUnlinked(kind string, u Unlinked) error // GetLinksAsked and PutLinksAsked keep when links were asked, by account and by kind (allowLink). GetLinksAsked(key string) ([]time.Time, error) PutLinksAsked(key string, times []time.Time) error } type askEntry struct { rec AskRecord rev uint64 } func askKey(asker, id string) string { return asker + "." + id } // loadAsks reads back every ask, and indexes the tickets of the open ones. func (h *Holder) loadAsks() error { if h.Asks == nil { return nil } keys, err := h.Asks.AskKeys() if err != nil { return err } for _, k := range keys { rec, rev, err := h.Asks.GetAsk(k) if err != nil { return err } if rec == nil { continue } h.keepAsk(k, *rec, rev) } return nil } func (h *Holder) keepAsk(key string, rec AskRecord, rev uint64) { h.mu.Lock() defer h.mu.Unlock() if h.asks == nil { h.asks = map[string]*askEntry{} h.tickets = map[string]string{} } h.asks[key] = &askEntry{rec: rec, rev: rev} // Kept after the ask ends, so a late answer is told why it was not taken. for t := range rec.Tickets { h.tickets[t] = key } } func (h *Holder) askOf(key string) *askEntry { h.mu.Lock() defer h.mu.Unlock() if h.asks == nil { return nil } return h.asks[key] } // openAsk is the ask a record shows, when it is still open. func (h *Holder) openAsk(r *Record) *AskRecord { if r == nil || r.Ask == "" { return nil } e := h.askOf(r.Ask) if e == nil || e.rec.State != StateOpen { return nil } rec := e.rec return &rec } // carriers are the kinds that can carry an answer of a level now: ready, offering a choice, and for // approve verifying their sender with an account of the operator's linked, where the controller says now // that no agent can become root (rootFree, asked live). func (h *Holder) carriers(level asks.Level) []string { if h.Bench == nil { return nil } var out []string for _, k := range h.Bench.Kinds() { // Every level needs a choice made in one act, shown exactly as the router rendered it (ADR 0234 §8). if h.Bench.Ready(k.Kind) != nil || !h.Bench.Promises(k.Kind, "choice") || !h.Bench.Promises(k.Kind, "exact-render") { continue } switch level { 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, true) == "" { out = append(out, k.Kind) } } // Destroy needs a code, which no channel carries yet. } return out } // RootVerdict is the controller's word on one machine (its verb root-free, novox/hq ADR 0259 §8): free only // when it measured, recently, that no agent can become root there without a person. type RootVerdict struct { Machine string `json:"machine"` Free bool `json:"free"` Why string `json:"why"` Judged string `json:"judged,omitempty"` } // RootFreeVerb is the controller's verb the router asks, as this module, which machines are root-free. const RootFreeVerb = "seat:" + ControllerSeat + ".root-free" // RootFreeWithin is how long the router waits for the controller's word; RootFreeKept how long a word is // used to render a message's buttons. An answer is always judged on a word asked then. const ( RootFreeWithin = 10 * time.Second RootFreeKept = 30 * time.Second ) // askRootFree asks the controller about machines, failing on no answer in time. func (h *Holder) askRootFree(machines []string) (map[string]RootVerdict, error) { if h.RootFree != nil { return h.RootFree(machines) } if h.Ask == nil { return nil, errors.New("this router cannot ask the controller") } type answer struct { raw json.RawMessage err error } done := make(chan answer, 1) go func() { raw, err := h.Ask(RootFreeVerb, map[string]any{"machines": machines}) done <- answer{raw, err} }() var a answer select { case a = <-done: case <-time.After(RootFreeWithin): return nil, fmt.Errorf("the controller did not answer within %s", RootFreeWithin) } if a.err != nil { return nil, a.err } var body struct { Machines []RootVerdict `json:"machines"` } if err := json.Unmarshal(a.raw, &body); err != nil { return nil, fmt.Errorf("the controller's answer could not be read: %v", err) } out := map[string]RootVerdict{} for _, v := range body.Machines { out[v.Machine] = v } return out, nil } // rootFree is why a kind's proof is not believed now, or "" when the controller says, now, that no agent can // become root without a person on the router's machine and on the kind's (novox/hq ADR 0259 §8, rule 2, as // the review of 2026-10-09 asked: a positive, recent pass, never the absence of a condition). Anything else // fails: a router that does not know its machine, a kind whose machine is not said, a controller that does // not answer, a machine missing from its answer, a word that is not free. cached allows a word up to // RootFreeKept old, for rendering; an answer is always judged with cached false. func (h *Holder) rootFree(k KindInfo, cached bool) string { if h.Node == "" { return "this router does not know which machine it runs on, so it cannot ask whether an agent can become root there" } if k.Node == "" { return "the mesh has not said which machine " + Display(k.Kind) + " runs on" } machines := []string{h.Node} if k.Node != h.Node { machines = append(machines, k.Node) } now := h.Now() key := strings.Join(machines, ",") h.mu.Lock() kept, ok := h.rootWord[key] h.mu.Unlock() var verdicts map[string]RootVerdict switch { case cached && ok && now.Sub(kept.at) < RootFreeKept && now.Sub(kept.at) >= 0: verdicts = kept.verdicts 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 h.mu.Lock() if h.rootWord == nil { h.rootWord = map[string]rootWord{} } h.rootWord[key] = rootWord{at: now, verdicts: got} h.mu.Unlock() } for _, m := range machines { v, said := verdicts[m] switch { case !said: return "the controller said nothing of whether an agent can become root on " + m case !v.Free: why := v.Why if why == "" { why = "it is not measured that it cannot" } return "a program working for you may become root on " + m + " without asking you (" + why + "), so an answer proven there proves nothing until that changes" } } 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. // force asks even a recent word again: taking an ask, whose refusal its asker keeps until something changes. func (h *Holder) warmRootFree(force bool) { 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 !force && 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 verdicts map[string]RootVerdict } // approveHolds is why an answer on a kind could not approve now, or "": the kind verifies its sender, an // account is linked there, and the controller says no agent can become root where it runs. func (h *Holder) approveHolds(kind string, cached bool) string { info, held := h.Bench.Kind(kind) switch { case !held: return "this channel is not one the mesh holds for answers" case !h.Bench.Promises(kind, "verified-sender"): return Display(kind) + " cannot prove who answered" case !h.linked(kind): return "no " + Display(kind) + " account is linked as you" } return h.rootFree(info, cached) } // answerable is whether an option of an ask is answered on a kind, by the ask's highest level: an ask that // offers an approval is answered only where it could be approved, whichever of its options is chosen, so an // acknowledgement from where approval is not proven never ends a question that waits for one (the review of // 2026-10-09). func answerLevel(a asks.Ask) asks.Level { if a.Highest().Rank() > asks.Acknowledge.Rank() { return a.Highest() } return asks.Acknowledge } // ConditionOwner is the module whose asks may be shown on a condition's message: the controller, which // raises the conditions. const ConditionOwner = "mesh-controller" // TakeAsk takes one ask or cancel from the seat's queue. asker is the subject's last token, which the bus // 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(true) // fresh, before the work: an ask refused for no channel stays refused with its asker h.work.Lock() defer h.work.Unlock() h.mu.Lock() h.init() h.mu.Unlock() if !asks.UsableID(strings.ReplaceAll(asker, "-", "_")) { h.Logf("[messenger] an ask from %q was taken and dropped: not a module's name", asker) return nil } if verb == "cancel" { var c struct { ID string `json:"id"` } if json.Unmarshal(body, &c) != nil || !asks.UsableID(c.ID) { h.Logf("[messenger] a cancel from %s names no ask", asker) return nil } return h.closeAsk(askKey(asker, c.ID), asks.OutcomeCancelled, "its asker took it back", nil, "", nil) } var a asks.Ask if err := json.Unmarshal(body, &a); err != nil { h.refuseAsk(asker, asks.Ask{ID: "unreadable"}, "it could not be read: "+err.Error()) return nil } key := askKey(asker, a.ID) if h.askOf(key) != nil { return nil // a redelivery: already recorded } now := h.Now() if err := a.Check(now); err != nil { h.refuseAsk(asker, a, err.Error()) return nil } words := []string{a.Headline, a.Explanation, a.OnExpiry} for _, o := range a.Options { words = append(words, o.Label, o.Does) } if refusal, ok := Check(strings.Join(words, "\n")); !ok { h.refuseAsk(asker, a, "its words carry "+refusal.String()+", which may not leave the mesh: move it into a reference or say it in words") return nil } open, replaced := 0, "" h.mu.Lock() for k, e := range h.asks { if e.rec.Asker != asker || e.rec.State != StateOpen { continue } if a.About != "" && e.rec.Ask.About == a.About { replaced = k continue } open++ } h.mu.Unlock() if open >= asks.MostOpen { h.refuseAsk(asker, a, fmt.Sprintf("its asker already has %d open asks", open)) return nil } rec := AskRecord{Asker: asker, Ask: a, State: StateOpen, Opened: now, Tickets: map[string]Ticket{}} // Every option on the kinds that carry the ask's highest level: an ask with an approval is answered only // where it could be approved (answerLevel). for _, kind := range h.carriers(answerLevel(a)) { for _, o := range a.Options { rec.Tickets[ticket()] = Ticket{Option: o.ID, Kind: kind} } } if len(rec.Tickets) == 0 { h.refuseAsk(asker, a, "no channel can carry any of its answers now: "+h.whyNot()) return nil } if replaced != "" { if err := h.closeAsk(replaced, asks.OutcomeReplaced, "a newer ask about the same thing replaced it", nil, "", nil); err != nil && !errors.Is(err, ErrChanged) { return err } } // Shown on the condition it is about, when that is open here; otherwise as a message of its own. h.mu.Lock() target := h.open["ask."+key] // Shown on a condition's message only when asked by the condition's owner: another asker naming a // condition's key does not take its message over. if a.About != "" && asker == ConditionOwner { if r := h.open[a.About]; r != nil && r.Cleared.IsZero() && r.Origin == "condition" { target = r } } h.mu.Unlock() if target != nil { rec.Record = target.Key } else { rec.Record = "ask." + key } if h.Asks != nil { rev, err := h.Asks.CreateAsk(key, rec) if errors.Is(err, ErrChanged) { got, rev, err := h.Asks.GetAsk(key) if err != nil || got == nil { return fmt.Errorf("the ask %s is on record and cannot be read: %v", key, err) } h.keepAsk(key, *got, rev) return nil } if err != nil { return fmt.Errorf("the ask %s could not be recorded: %w", key, err) } h.keepAsk(key, rec, rev) } else { h.keepAsk(key, rec, 0) } h.Logf("[messenger] %s asks %q (%s), shown on %s", asker, a.Headline, a.ID, rec.Record) if target != nil { h.mu.Lock() target.Ask = key h.mu.Unlock() if !target.silenced(now) { h.offerAsk(target) } h.persist(target) return nil } severity := Warning if a.Urgent { severity = Urgent } r := Record{Key: "ask." + key, Kind: "ask", Subject: asker, Severity: severity, Summary: a.Headline, Headline: a.Headline, Explanation: a.Explanation, Origin: "ask", More: "operator-channel.asks", Raised: now, Sent: map[string]string{}, Seen: now, Count: 1, Ask: key} h.mu.Lock() h.open[r.Key] = &r h.mu.Unlock() h.deliver(&r, "raised") h.persist(&r) return nil } // askKinds are the kinds an open ask about a record is offered on. func (h *Holder) askKinds(r *Record) map[string]bool { rec := h.openAsk(r) if rec == nil { return nil } out := map[string]bool{} for _, t := range rec.Tickets { out[t.Kind] = true } return out } // offerAsk shows a record's new ask: edited in place where the record was shown, a message of its own on a // channel that carries the ask and has not shown the record. func (h *Holder) offerAsk(r *Record) { kinds := h.askKinds(r) h.mu.Lock() sent := map[string]string{} for k, v := range r.Sent { sent[k] = v } h.mu.Unlock() now := h.Now() for _, ch := range h.channels() { id, shown := sent[ch.Name()] switch { case shown && id != "" && ch.CanEdit(): err := ch.Edit(id, h.sayOn(r, "asked", ch.Name())) h.noteChannel(ch.Name(), err) if err == nil { h.record(Sent{At: now, Channel: ch.Name(), Key: r.Key, What: "asked", Outcome: "edited"}) continue } h.enqueue(ch.Name(), item{Key: r.Key, What: "asked", Severity: r.Severity, At: now}) case kinds[ch.Name()] && ch.Ready() == nil: h.enqueue(ch.Name(), item{Key: r.Key, What: "asked", Severity: r.Severity, At: now}) } } h.flush(now, false) } // refuseAsk says to its asker that an ask was not taken, and why. func (h *Holder) refuseAsk(asker string, a asks.Ask, why string) { h.Logf("[messenger] refused an ask from %s (%s): %s", asker, a.ID, why) h.tell(asks.Warrant{Ask: a.ID, Asker: asker, About: a.About, Outcome: asks.OutcomeRefused, Words: why, At: h.Now()}) } // tell publishes a warrant, or an ask's end, to its asker alone, deduplicated by the ask. func (h *Holder) tell(w asks.Warrant) error { if h.Publish == nil { return errors.New("this router cannot reach the bus") } // A new message id each time (the review of 2026-10-09): an id anyone could foresee lets a publisher on // the same stream take it first, and the bus would drop the warrant as a duplicate. The asker acts on its // own record of the ask once, however many times it hears a warrant. err := h.Publish(asks.DecidedSubject(w.Asker), w, "warrant."+ticket()) if err != nil { h.Logf("[messenger] could not tell %s how its ask %s ended (%v); told again in a minute", w.Asker, w.Ask, err) } return err } // closeAsk ends an open ask by compare-and-set, tells its asker, and edits every copy. A second close // loses: ErrChanged when another outcome was first. confirm, when given, is called once the end is recorded // and the asker told or not (told), before any copy is edited: the answer's confirmation where it was given // comes first, and never before the record stands (issue 372). func (h *Holder) closeAsk(key string, outcome asks.Outcome, words string, w *asks.Warrant, used string, confirm func(told bool)) error { e := h.askOf(key) if e == nil { return nil } rec, rev := e.rec, e.rev if h.Asks != nil { got, r, err := h.Asks.GetAsk(key) if err != nil { return err } if got != nil { rec, rev = *got, r } } if rec.State != StateOpen { h.keepAsk(key, rec, rev) return ErrChanged } now := h.Now() rec.State = string(outcome) rec.Closed = now if t, ok := rec.Tickets[used]; ok { t.Used = true rec.Tickets[used] = t } if w == nil { w = &asks.Warrant{Ask: rec.Ask.ID, Asker: rec.Asker, About: rec.Ask.About, Outcome: outcome, Words: words, At: now} } rec.Warrant = w if h.Asks != nil { newRev, err := h.Asks.UpdateAsk(key, rec, rev) if err != nil { if errors.Is(err, ErrChanged) { if got, r, gerr := h.Asks.GetAsk(key); gerr == nil && got != nil { h.keepAsk(key, *got, r) } } return err } rev = newRev } h.keepAsk(key, rec, rev) if rec.Asker == SelfAsker { // The router's own ask: it does what was decided, and tells nobody else (selfact.go). h.markTold(key) if confirm != nil { confirm(true) } h.showClosed(rec) h.selfAct(rec, *w) return nil } told := h.tell(*w) == nil if told { h.markTold(key) } if confirm != nil { confirm(told) } h.showClosed(rec) return nil } // markTold records that the asker was told. func (h *Holder) markTold(key string) { e := h.askOf(key) if e == nil { return } rec, rev := e.rec, e.rev rec.Told = true if h.Asks != nil { newRev, err := h.Asks.UpdateAsk(key, rec, rev) if err != nil { return // read again and told again by the sweep } rev = newRev } h.keepAsk(key, rec, rev) } // showClosed edits every copy of an ask's message to say how it ended, its buttons gone. func (h *Holder) showClosed(rec AskRecord) { h.mu.Lock() r := h.open[rec.Record] h.mu.Unlock() if r == nil { return } told := h.toldLine(rec) // read before the lock: it reads the ask's record h.mu.Lock() ownMessage := r.Origin == "ask" // The first line is how it ended, which an ask's own message shows as its title (sayOn); the rest follows. switch { case rec.Warrant != nil && rec.Warrant.Outcome == asks.OutcomeChosen: r.Answered = chosenLine(rec.Warrant, rec.Closed.In(h.zone())) + "\n" + told case rec.State == string(asks.OutcomeExpired): r.Answered = "No longer answerable: nobody answered in time.\n" + strings.TrimSuffix(capital(rec.Ask.OnExpiry), ".") + "." case rec.State == string(asks.OutcomeReplaced) && !ownMessage: // On a condition's message, which the newer ask takes over and shows in place. r.Answered = "" case rec.State == string(asks.OutcomeReplaced): // Its own message: the newer ask looks for its message under its own key and opens one of its own, so // this one ends here, on every channel that showed it (issue 548). r.Answered = "Replaced by a newer question.\n" + rec.Asker + " asked again about the same thing; that question is the one to answer." default: r.Answered = "No longer needed." } h.mu.Unlock() if r.told() { h.edit(r, "answered", false) } if ownMessage { h.mu.Lock() r.Cleared = h.Now() r.Owed = "" // an ended question is owed to nobody (the review of PR 238) h.mu.Unlock() } h.persist(r) } // endLeftovers ends the own message of an ask that already ended and whose message stayed open: one left by a // router before issue 548 was fixed, whose replaced asks kept their messages for good. Its copies say how the // ask ended, as its end would have. func (h *Holder) endLeftovers() { h.mu.Lock() var ended []AskRecord for _, r := range h.open { if r.Origin != "ask" || r.Ask == "" || !r.Cleared.IsZero() { continue } if e := h.asks[r.Ask]; e != nil && e.rec.State != StateOpen && e.rec.Record == r.Key { ended = append(ended, e.rec) } } h.mu.Unlock() sort.Slice(ended, func(i, j int) bool { return ended[i].Record < ended[j].Record }) for _, rec := range ended { h.Logf("[messenger] the message of %s stayed open after its ask ended (%s): ended now", rec.Record, rec.State) h.showClosed(rec) } } // chosenLine is how a chosen ask ended, in the operator's words: "You chose Approve at 13:30 on Telegram." func chosenLine(w *asks.Warrant, at time.Time) string { who, where := "Somebody", "" if w.By != nil { if w.By.Who == asks.Operator { who = "You" } where = " on " + Display(w.By.Kind) } return who + " chose " + w.Label + " at " + at.Format("15:04") + where + "." } // toldLine is what the router knows of the asker after an answer: whether it was told. The asker says nothing // back of what it then did (the asks protocol carries no result to the router), so nothing more is claimed. func (h *Holder) toldLine(rec AskRecord) string { if rec.Asker == SelfAsker { return "The router does it itself." } if e := h.askOf(askKey(rec.Asker, rec.Ask.ID)); e != nil && e.rec.Told { return "Told to " + rec.Asker + ", which acts on it itself." } return "Told to " + rec.Asker + " as soon as the bus takes it; it acts on it itself." } // confirmChoice says, where an answer was given and in reply to it, that it was recorded: on Telegram the // tap's own acknowledgement (issue 372). Called only once the ask's end is recorded. func (h *Holder) confirmChoice(kind string, c asks.Choice, label, asker string, told bool) { words := "You chose " + label + ": recorded" switch { case asker == SelfAsker: words += "." case told: words += ", and " + asker + " was told." default: words += "; " + asker + " is told as soon as the bus takes it." } err := h.showOn(kind, Message{Title: words, Quiet: true, Reply: c.ID}) h.record(Sent{At: h.Now(), Channel: kind, Key: "(answer)", What: "confirmed " + label, Outcome: outcomeOf(err)}) } // confirmAgain says, in reply to the same answer given again, that it was already recorded. func (h *Holder) confirmAgain(kind string, c asks.Choice, label string, at time.Time) { err := h.showOn(kind, Message{Title: "You already chose " + label + " at " + at.In(h.zone()).Format("15:04") + ": recorded.", Quiet: true, Reply: c.ID}) h.record(Sent{At: h.Now(), Channel: kind, Key: "(answer)", What: "confirmed again " + label, Outcome: outcomeOf(err)}) } // sweepAsks ends asks past their expiry, and tells again an asker not yet told. Called every minute. func (h *Holder) sweepAsks(now time.Time) { h.mu.Lock() var expired, untold []string for k, e := range h.asks { switch { case e.rec.State == StateOpen && !now.Before(e.rec.Ask.Expires): expired = append(expired, k) case e.rec.State != StateOpen && !e.rec.Told && e.rec.Warrant != nil: untold = append(untold, k) } } h.mu.Unlock() h.endLeftovers() sort.Strings(expired) for _, k := range expired { e := h.askOf(k) if err := h.closeAsk(k, asks.OutcomeExpired, "nobody answered in time: "+e.rec.Ask.OnExpiry, nil, "", nil); err != nil && !errors.Is(err, ErrChanged) { h.Logf("[messenger] could not end the expired ask %s: %v", k, err) } } for _, k := range untold { e := h.askOf(k) if e != nil && h.tell(*e.rec.Warrant) == nil { h.markTold(k) } } } // offers are an open ask's buttons on one kind: each option offered there, by its ticket. func (h *Holder) offers(rec *AskRecord, kind string) []Choice { var out []Choice if answerLevel(rec.Ask) != asks.Acknowledge && rec.Asker != SelfAsker { // Rendered only while approval holds there now, by the controller's recent word. if h.approveHolds(kind, true) != "" { return nil } } else if rec.Asker == SelfAsker && answerLevel(rec.Ask) != asks.Acknowledge { if info, held := h.Bench.Kind(kind); !held || h.rootFree(info, true) != "" { return nil } } for _, o := range rec.Ask.Options { for t, tk := range rec.Tickets { if tk.Kind == kind && tk.Option == o.ID && !tk.Used { out = append(out, Choice{Token: t, Label: o.Label}) } } } return out } // elsewhere is the line an ask's message carries on a kind for the options it cannot carry there: where // else each can be given — "Release or Stop: answer on Telegram, or from the mesh MCP server." func (h *Holder) elsewhere(rec *AskRecord, kind string) string { var labels []string where := map[string]bool{} for _, o := range rec.Ask.Options { here := false for _, tk := range rec.Tickets { if tk.Option == o.ID && tk.Kind == kind { here = true } } if here { continue } labels = append(labels, o.Label) for _, tk := range rec.Tickets { if tk.Option == o.ID && tk.Kind != kind { where[tk.Kind] = true } } } if len(labels) == 0 { return "" } var kinds []string for k := range where { kinds = append(kinds, Display(k)) } sort.Strings(kinds) answer := "from the mesh MCP server" if len(kinds) > 0 { answer = "answer on " + strings.Join(kinds, " or ") + ", or from the mesh MCP server" } return joinOr(labels) + ": " + answer + "." } func joinOr(labels []string) string { if len(labels) <= 1 { return strings.Join(labels, "") } return strings.Join(labels[:len(labels)-1], ", ") + " or " + labels[len(labels)-1] } // 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, 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") } rec := e.rec if h.Asks != nil { if got, _, err := h.Asks.GetAsk(key); err == nil && got != nil { rec = *got } } now := h.Now() t, issued := rec.Tickets[c.Ticket] if w := rec.Warrant; issued && rec.State == string(asks.OutcomeChosen) && w != nil && w.Option == t.Option && w.By != nil && w.By.Kind == kind && w.By.Identity == c.Sender.Identity && (c.Sender.Authenticated || !h.Bench.Promises(kind, "verified-sender")) { // The same answer again from the same account — a second tap while the first was being confirmed, or one // redelivered after a restart: it is what was recorded, so it is said so, not refused as if it failed // (issue 372). Nothing more is done. h.confirmAgain(kind, c, w.Label, rec.Closed) return "chosen", nil } switch { case !issued: return h.refuseChoice(kind, c, "it belongs to no ask this router holds") case t.Kind != kind: return h.refuseChoice(kind, c, "it was not offered here") case rec.State != StateOpen: return h.refuseChoice(kind, c, "the question was already answered or has ended") case t.Used: return h.refuseChoice(kind, c, "it was already chosen once") case !now.Before(rec.Ask.Expires): return h.refuseChoice(kind, c, "the question expired at "+rec.Ask.Expires.In(h.zone()).Format("15:04")) } o, offered := rec.Ask.Option(t.Option) if !offered { return h.refuseChoice(kind, c, "it names an answer the question does not offer") } info, held := h.Bench.Kind(kind) if !held || !h.Bench.Promises(kind, "choice") || !h.Bench.Promises(kind, "exact-render") { return h.refuseChoice(kind, c, "this channel is not one the mesh holds for answers") } person := asks.Person{Who: asks.Operator, Kind: kind, Identity: c.Sender.Identity, Display: c.Sender.Display} verifies := h.Bench.Promises(kind, "verified-sender") var id *Identity if verifies { if !c.Sender.Authenticated { return h.refuseChoice(kind, c, "the service did not authenticate who sent it") } id = h.identity(kind, c.Sender.Identity) if rec.Only != nil { // An ask put to one account is answered by that account alone, linked or not any more. if rec.Only.Kind != kind || rec.Only.Identity != c.Sender.Identity { return h.refuseChoice(kind, c, "this question was put to another account") } only := *rec.Only id = &only } if id == nil { return h.refuseStranger(kind, c) } person.Verified = "user id verified" } else { if rec.Only != nil && rec.Only.Kind != kind { return h.refuseChoice(kind, c, "this question was put to another channel") } person.Verified = "a desk click: whoever was at the operator's session on " + c.Sender.Identity } // Judged at the ask's highest level, whichever option was chosen (answerLevel): an acknowledgement on an // ask that offers an approval needs what the approval needs, so it never ends that ask from where an // approval is not proven. level := o.Level if answerLevel(rec.Ask).Rank() > level.Rank() { level = answerLevel(rec.Ask) } proofs := []string{} switch level { case asks.Acknowledge: case asks.Approve: if !verifies { return h.refuseChoice(kind, c, Display(kind)+" cannot prove who answered, so it cannot answer a question "+ "that asks for an approval") } if now.Before(id.ApproveFrom) { 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 := root(info); why != "" { return h.refuseChoice(kind, c, why) } if o.Level == asks.Approve { proofs = append(proofs, "P1") } default: return h.refuseChoice(kind, c, "it needs a code from the authenticator app, which the mesh does not take yet") } w := &asks.Warrant{Ask: rec.Ask.ID, Asker: rec.Asker, About: rec.Ask.About, Outcome: asks.OutcomeChosen, Option: o.ID, Label: o.Label, Level: o.Level, By: &person, Channel: info.Module, Proofs: proofs, At: now, // The warrant answers this ask as it was taken and shown, with what each option binds (ADR 0259 §6). AskDigest: rec.Ask.Digest()} confirm := func(told bool) { h.confirmChoice(kind, c, o.Label, rec.Asker, told) } if err := h.closeAsk(key, asks.OutcomeChosen, "", w, c.Ticket, confirm); err != nil { if errors.Is(err, ErrChanged) { return h.refuseChoice(kind, c, "the question was already answered") } // The store's own words stay in the journal: they are not the operator's to read on the phone. h.Logf("[messenger] the answer to %s's ask %s could not be recorded: %v", rec.Asker, rec.Ask.ID, err) return h.refuseChoice(kind, c, "it could not be recorded") } h.record(Sent{At: now, Channel: kind, Key: rec.Record, What: "answer " + o.Label, Outcome: "warrant to " + rec.Asker}) h.Logf("[messenger] %s: a warrant for %s's ask %s", w.Says(), rec.Asker, rec.Ask.ID) return "chosen", nil } // refuseStranger refuses an answer from an account that is not the operator's, and tells the operator, by name // and number, on the account that is linked: a question's buttons reached somebody else (a forwarded message, // or a phone in other hands). Nothing is said to the other account. func (h *Holder) refuseStranger(kind string, c asks.Choice) (string, error) { why := "this account is not linked as the operator" h.Logf("[messenger] an answer on %s from %s was refused: %s", kind, c.Sender.Identity, why) h.record(Sent{At: h.Now(), Channel: kind, Key: "(answer)", What: "answer", Outcome: "refused: " + why}) who := c.Sender.Display if who == "" { who = "an account" } _ = h.showOn(kind, Message{Title: "Another account tried to answer for you", Body: fmt.Sprintf("The %s account %s (number %s) chose an answer on a question meant for you. It was refused "+ "and nothing was done. If you did not pass the question on, somebody else can read your messages.", Display(kind), who, c.Sender.Identity), Urgent: true}) return "refused", errorf("%s", why) } // refuseChoice says on the kind that an answer was not taken, and why. func (h *Holder) refuseChoice(kind string, c asks.Choice, why string) (string, error) { h.Logf("[messenger] an answer on %s from %s was refused: %s", kind, c.Sender.Identity, why) h.record(Sent{At: h.Now(), Channel: kind, Key: "(answer)", What: "answer", Outcome: "refused: " + why}) h.showOn(kind, Message{Title: "That answer was not taken", Body: capital(why) + ". Nothing was done.", Quiet: true, Reply: c.ID}) return "refused", errorf("%s", why) } // showOnHandle is showOn, answering the handle the message is known by. func (h *Holder) showOnHandle(kind string, m Message) (string, error) { for _, ch := range h.channels() { if ch.Name() == kind { id, err := ch.Send(m) h.noteChannel(kind, err) return id, err } } if h.Publish == nil { return "", errors.New("this router cannot reach the bus") } out := asks.Message{Handle: newHandle(), Title: m.Title, Body: m.Body, Urgent: m.Urgent, Silent: m.Quiet, Reply: m.Reply, To: m.To, Secret: m.Secret} if out.To == "" && h.Bench != nil && h.Bench.To != nil { out.To = h.Bench.To(kind) } for _, a := range m.Actions { out.Buttons = append(out.Buttons, asks.Button{Label: a.Label, Ticket: a.Token}) } err := h.Publish(asks.ChannelSubject(asks.Show, kind), out, out.Handle) h.noteChannel(kind, err) return out.Handle, err } // showOn says a message of the router's own on one kind, at once: an answer's refusal, details, a link's code. func (h *Holder) showOn(kind string, m Message) error { for _, ch := range h.channels() { if ch.Name() == kind { _, err := ch.Send(m) h.noteChannel(kind, err) return err } } if h.Publish == nil { return errors.New("this router cannot reach the bus") } out := asks.Message{Handle: newHandle(), Title: m.Title, Body: m.Body, Urgent: m.Urgent, Silent: m.Quiet, Reply: m.Reply} if h.Bench != nil && h.Bench.To != nil { out.To = h.Bench.To(kind) } if m.To != "" { out.To = m.To } out.Secret = m.Secret for _, a := range m.Actions { out.Buttons = append(out.Buttons, asks.Button{Label: a.Label, Ticket: a.Token}) } err := h.Publish(asks.ChannelSubject(asks.Send, kind), out, out.Handle) h.noteChannel(kind, err) return err } func ticket() string { var b [16]byte _, _ = rand.Read(b[:]) return hex.EncodeToString(b[:]) } // AskView is one ask as the tool `asks` answers it. type AskView struct { Key string `json:"key"` Asker string `json:"asker"` Headline string `json:"headline"` State string `json:"state"` Opened string `json:"opened"` Expires string `json:"expires"` Offered []string `json:"offered_on,omitempty"` Answer string `json:"answer,omitempty"` Told bool `json:"asker_told"` } // AsksNow is every ask, open first, newest first. func (h *Holder) AsksNow() []AskView { h.mu.Lock() defer h.mu.Unlock() var out []AskView for k, e := range h.asks { v := AskView{Key: k, Asker: e.rec.Asker, Headline: e.rec.Ask.Headline, State: e.rec.State, Opened: e.rec.Opened.UTC().Format(time.RFC3339), Expires: e.rec.Ask.Expires.UTC().Format(time.RFC3339), Told: e.rec.Told} kinds := map[string]bool{} for _, t := range e.rec.Tickets { kinds[t.Kind] = true } for k := range kinds { v.Offered = append(v.Offered, k) } sort.Strings(v.Offered) if w := e.rec.Warrant; w != nil { if w.Outcome == asks.OutcomeChosen { v.Answer = w.Says() } else { v.Answer = string(w.Outcome) + ": " + w.Words } } out = append(out, v) } sort.Slice(out, func(i, j int) bool { if (out[i].State == StateOpen) != (out[j].State == StateOpen) { return out[i].State == StateOpen } return out[i].Opened > out[j].Opened }) return out }