mesh/merge-gate fail: builds messenger, sudo → ace, g14, novox, shanks; no bus step; a manifest the change touches fails the module check: modules/messenge…
mesh/repo-check fail: its merge-check.sh failed: files asking for a setting without saying whether it is trusted: 20
mesh/delivery-group group feat/asks-answered-on-any-channel rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request
The review of 2026-10-09: the router read the absence of an agent-root condition as a pass, and its minute hung on a break-glass file, taking the work it already held. An approval now rests on the controller's verb root-free, asked live for the router's machine and the channel's, failing closed on every other answer, and a stale word on the kinds promises no verified sender. Break-glass runs in the minute, says on every channel what root did, and ends the router's own open questions. An ask offering an approval is answered only where it could be approved; a message says who asks, what each answer does and what expiry does. The router's own asks bind the account they are about; a link is checked against one per kind as it is made; an unlinked account does not come back by a code within a day; a fresh link is unlinked from the desk; no link begins while the desk cannot show its code, and a code is marked secret on every path. Links are bounded per account in the router's state; a warrant's message id cannot be foreseen. Moves to mesh-sdk 76902998 (canonical digests).
983 lines
33 KiB
Go
983 lines
33 KiB
Go
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.<asker>`), 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 `<asker>.<id>` 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 `<asker>.<id>` 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, false) == "" {
|
|
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
|
|
if cached && ok && now.Sub(kept.at) < RootFreeKept && now.Sub(kept.at) >= 0 {
|
|
verdicts = kept.verdicts
|
|
} else {
|
|
got, err := h.askRootFree(machines)
|
|
if err != nil {
|
|
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 ""
|
|
}
|
|
|
|
// 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.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, "")
|
|
}
|
|
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, ""); 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.
|
|
func (h *Holder) closeAsk(key string, outcome asks.Outcome, words string, w *asks.Warrant, used string) 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)
|
|
h.showClosed(rec)
|
|
h.selfAct(rec, *w)
|
|
return nil
|
|
}
|
|
if h.tell(*w) == nil {
|
|
h.markTold(key)
|
|
}
|
|
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
|
|
}
|
|
h.mu.Lock()
|
|
switch {
|
|
case rec.Warrant != nil && rec.Warrant.Outcome == asks.OutcomeChosen:
|
|
r.Answered = rec.Warrant.Label + ": chosen at " + rec.Closed.In(h.zone()).Format("15:04") + " on " + Display(rec.Warrant.By.Kind)
|
|
case rec.State == string(asks.OutcomeExpired):
|
|
r.Answered = "No longer answerable: nobody answered in time. " + capital(rec.Ask.OnExpiry)
|
|
case rec.State == string(asks.OutcomeReplaced):
|
|
r.Answered = ""
|
|
default:
|
|
r.Answered = "No longer needed."
|
|
}
|
|
ownMessage := r.Origin == "ask"
|
|
h.mu.Unlock()
|
|
if r.told() {
|
|
h.edit(r, "answered", false)
|
|
}
|
|
if ownMessage && rec.State != string(asks.OutcomeReplaced) {
|
|
h.mu.Lock()
|
|
r.Cleared = h.Now()
|
|
h.mu.Unlock()
|
|
}
|
|
h.persist(r)
|
|
}
|
|
|
|
// 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()
|
|
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, ""); 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) (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]
|
|
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 := h.rootFree(info, false); 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()}
|
|
if err := h.closeAsk(key, asks.OutcomeChosen, "", w, c.Ticket); err != nil {
|
|
if errors.Is(err, ErrChanged) {
|
|
return h.refuseChoice(kind, c, "the question was already answered")
|
|
}
|
|
return h.refuseChoice(kind, c, "it could not be recorded: "+err.Error())
|
|
}
|
|
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
|
|
}
|