The operator would not read the Telegram ask for ADR 0343: thirty plain lines, every clause on one, no bold, no summary. A settings proposal was the same wall of text, its values masked so the approval was blind. - messenger: an ask's message is the asker's headline and explanation alone; who asked and when, what each answer does, the expiry, where else it is answered and the asker's Details travel as the message's note (asks.Message.Note). Answered, the message is one line, "You chose Approve at 16:06 on Telegram · <headline>". An explanation that does not read at a glance (asks.Glance) is shown and logged. On a kind that proves who answers, an ask's values are shown whole (asks.Ask.Whole), held to the secret rule alone (from the earlier work on issue 383, #178). - telegram: every message in HTML, never reworded: the title bold, a key line monospace, the note apart in a collapsed blockquote; markup escaped. A tap is compared by its words, whitespace aside, and a message shown before is still compared by the digest it was kept under. - desk-channel: the note follows the body. - mesh-knowledge: a record that needs the operator carries a brief (headline, summary, at most three points), the 30th per-record rule; its ask shows the brief and never the clauses. An ask made before is composed again as it was asked (form), so its warrant still holds. - claude-code: an instructions proposal's explanation is one summary line (the proposer's, or the module's), its text in the Details; a question's context is at most 300 characters and its recommendation one point. Older proposals and questions keep their form. The SDK is pinned at mesh-sdk 9588871 (fix/639-an-ask-reads-at-a-glance), which adds Message.Note and asks.Glance.
351 lines
11 KiB
Go
351 lines
11 KiB
Go
package main
|
|
|
|
// The channels, as the router sees them (novox/hq ADR 0259): kinds of the `channel` bench, each held by a
|
|
// module of its own. The router keeps no channel's client and knows no service. What it knows of a kind
|
|
// comes from two places, never from one alone:
|
|
//
|
|
// - **the controller**, in this module's membership: which kinds exist, which module holds each, and the
|
|
// capabilities its claim declares (`channel-capabilities/1`). An answer is judged by these.
|
|
// - **the channel**, on the `intake` bench under its own kind: whether it can send now (`standing`), and
|
|
// a message it could not deliver (`failed`).
|
|
//
|
|
// A message is sent by putting it on the kind's work queue (`mesh.seat.channel.accept.<verb>.<kind>`) under
|
|
// a handle the router makes; the channel keeps which of its own messages that handle is, so an edit names
|
|
// the handle. A queued message survives the channel being away.
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
)
|
|
|
|
// StandingFresh is how long a channel's word on whether it can send is believed: a channel says it at
|
|
// least every five minutes, so a word older than this is a channel that stopped saying.
|
|
const StandingFresh = 15 * time.Minute
|
|
|
|
// KindInfo is one kind of the channel bench as the controller issued it.
|
|
type KindInfo struct {
|
|
Kind string
|
|
Module string
|
|
Node string
|
|
Capabilities []string
|
|
}
|
|
|
|
// Standing is what a channel last said of itself.
|
|
type Standing struct {
|
|
Ready bool `json:"ready"`
|
|
Why string `json:"why,omitempty"`
|
|
EditsSilently bool `json:"edits-silently,omitempty"`
|
|
At time.Time `json:"at"`
|
|
// AnswersLostAt is when the answers the channel's messages waited for were last lost: the desk's notifier
|
|
// started again, its notifications still on screen and their buttons dead (issue 384). The router then says
|
|
// again what is still open and unanswered there (restart.go); the channel shows nothing from its own memory.
|
|
// Zero from a channel whose answers are never lost (Telegram's buttons live in the chat).
|
|
AnswersLostAt time.Time `json:"answers-lost-at"`
|
|
}
|
|
|
|
// Bench is every kind the router may send to, and what each said.
|
|
type Bench struct {
|
|
Now func() time.Time
|
|
// To is the account linked as the operator on a kind, where it verifies its sender: where its messages go.
|
|
To func(kind string) string
|
|
|
|
mu sync.Mutex
|
|
kinds map[string]KindInfo
|
|
standing map[string]Standing
|
|
known bool
|
|
knownAt time.Time
|
|
}
|
|
|
|
// KindsFresh is how long the controller's word on the kinds is believed for a proof: the router reads it every
|
|
// half minute, so an older word is one it could not read again. A stale word keeps the kinds for sending, and
|
|
// promises no verified sender (the review of 2026-10-09: a capability the controller withdrew must not stay).
|
|
const KindsFresh = 2 * time.Minute
|
|
|
|
// NewBench is a bench that knows no kind yet.
|
|
func NewBench(now func() time.Time) *Bench {
|
|
return &Bench{Now: now, kinds: map[string]KindInfo{}, standing: map[string]Standing{}}
|
|
}
|
|
|
|
// SetKinds takes the kinds the controller issued.
|
|
func (b *Bench) SetKinds(kinds []KindInfo) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
b.kinds = map[string]KindInfo{}
|
|
for _, k := range kinds {
|
|
b.kinds[k.Kind] = k
|
|
}
|
|
b.known = true
|
|
b.knownAt = b.Now()
|
|
}
|
|
|
|
// Stand takes what a kind said of itself.
|
|
func (b *Bench) Stand(kind string, s Standing) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
b.standing[kind] = s
|
|
}
|
|
|
|
// Kind is one kind, as the controller issued it.
|
|
func (b *Bench) Kind(kind string) (KindInfo, bool) {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
k, ok := b.kinds[kind]
|
|
return k, ok
|
|
}
|
|
|
|
// Kinds is every kind, by name.
|
|
func (b *Bench) Kinds() []KindInfo {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
out := make([]KindInfo, 0, len(b.kinds))
|
|
for _, k := range b.kinds {
|
|
out = append(out, k)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Kind < out[j].Kind })
|
|
return out
|
|
}
|
|
|
|
// Promises says whether a kind's claim declares a capability: the controller's record, never the channel's.
|
|
func (b *Bench) Promises(kind, capability string) bool {
|
|
k, ok := b.Kind(kind)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if capability == "verified-sender" {
|
|
b.mu.Lock()
|
|
at := b.knownAt
|
|
b.mu.Unlock()
|
|
if age := b.Now().Sub(at); age > KindsFresh || age < 0 {
|
|
return false
|
|
}
|
|
}
|
|
for _, c := range k.Capabilities {
|
|
if c == capability {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Ready says whether a kind can send now, in words when it cannot.
|
|
func (b *Bench) Ready(kind string) error {
|
|
b.mu.Lock()
|
|
_, held := b.kinds[kind]
|
|
s, said := b.standing[kind]
|
|
known := b.known
|
|
b.mu.Unlock()
|
|
switch {
|
|
case !held && !known:
|
|
return errors.New("the mesh has not yet said which channels exist")
|
|
case !held:
|
|
return fmt.Errorf("no module holds the channel %s", kind)
|
|
case !said:
|
|
return fmt.Errorf("%s has not said whether it can send", kind)
|
|
case b.Now().Sub(s.At) > StandingFresh:
|
|
return fmt.Errorf("%s has said nothing since %s", kind, s.At.UTC().Format(time.RFC3339))
|
|
case !s.Ready:
|
|
if s.Why == "" {
|
|
return fmt.Errorf("%s says it cannot send", kind)
|
|
}
|
|
return errors.New(s.Why)
|
|
case b.Promises(kind, "verified-sender") && b.To != nil && b.To(kind) == "":
|
|
// A channel proving its sender says only to the account linked as the operator: none linked, it has
|
|
// nobody to say anything to (the correctness review of 2026-10-08).
|
|
return fmt.Errorf("no %s account is linked as the operator: send /start to the bot", Display(kind))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (b *Bench) editsSilently(kind string) bool {
|
|
b.mu.Lock()
|
|
defer b.mu.Unlock()
|
|
return b.standing[kind].EditsSilently
|
|
}
|
|
|
|
// Roles: the away channel reaches the operator away from the machines; the desk is at them.
|
|
const (
|
|
RoleAway = "away"
|
|
RoleDesk = "desk"
|
|
)
|
|
|
|
// roleOf is a kind's role, from what its claim declares.
|
|
func (b *Bench) roleOf(kind string) string {
|
|
if b.Promises(kind, "reaches-away") {
|
|
return RoleAway
|
|
}
|
|
return RoleDesk
|
|
}
|
|
|
|
// Display is a kind as the operator reads it: "Telegram", "the desk".
|
|
func Display(kind string) string {
|
|
if kind == "desktop" {
|
|
return "the desk"
|
|
}
|
|
if kind == "" {
|
|
return kind
|
|
}
|
|
return strings.ToUpper(kind[:1]) + kind[1:]
|
|
}
|
|
|
|
// SeatChannel is one role's channel: the first kind of that role that can send, else the first of it.
|
|
type SeatChannel struct {
|
|
Bench *Bench
|
|
Role string
|
|
Publish func(subject string, body any, id string) error
|
|
}
|
|
|
|
func (c *SeatChannel) kind() string {
|
|
var first string
|
|
for _, k := range c.Bench.Kinds() {
|
|
if c.Bench.roleOf(k.Kind) != c.Role {
|
|
continue
|
|
}
|
|
if first == "" {
|
|
first = k.Kind
|
|
}
|
|
if c.Bench.Ready(k.Kind) == nil {
|
|
return k.Kind
|
|
}
|
|
}
|
|
return first
|
|
}
|
|
|
|
// Name is the kind it sends to now, or its role when no kind holds it.
|
|
func (c *SeatChannel) Name() string {
|
|
if k := c.kind(); k != "" {
|
|
return k
|
|
}
|
|
return c.Role
|
|
}
|
|
|
|
func (c *SeatChannel) Ready() error {
|
|
k := c.kind()
|
|
if k == "" {
|
|
if c.Role == RoleAway {
|
|
return errors.New("no channel reaching the operator away from the machines is held: a module claims a " +
|
|
"kind of the channel bench that promises reaches-away (novox/hq ADR 0259)")
|
|
}
|
|
return errors.New("no channel at the operator's machines is held")
|
|
}
|
|
return c.Bench.Ready(k)
|
|
}
|
|
|
|
func (c *SeatChannel) CanEdit() bool { return c.Bench.Promises(c.kind(), "edit") }
|
|
func (c *SeatChannel) SilentEdit() bool { return c.Bench.editsSilently(c.kind()) }
|
|
|
|
// Send puts a message on the kind's queue and answers the handle it is known by.
|
|
func (c *SeatChannel) Send(m Message) (string, error) {
|
|
handle := newHandle()
|
|
return handle, c.put(handle, m, false)
|
|
}
|
|
|
|
// Replace says a message again under the handle of one shown before: the channel takes the earlier one away
|
|
// and shows this one as a new message is shown — ringing as its kind rings (issue 373). An edit is quiet.
|
|
func (c *SeatChannel) Replace(handle string, m Message) error {
|
|
if handle == "" {
|
|
return errors.New("no message on record to replace")
|
|
}
|
|
return c.put(handle, m, false)
|
|
}
|
|
|
|
// Edit replaces the message a handle names.
|
|
func (c *SeatChannel) Edit(handle string, m Message) error {
|
|
if handle == "" {
|
|
return errors.New("no message on record to replace")
|
|
}
|
|
return c.put(handle, m, true)
|
|
}
|
|
|
|
func (c *SeatChannel) put(handle string, m Message, edit bool) error {
|
|
kind := c.kind()
|
|
if kind == "" {
|
|
return c.Ready()
|
|
}
|
|
return c.putOn(kind, handle, m, edit)
|
|
}
|
|
|
|
func (c *SeatChannel) putOn(kind, handle string, m Message, edit bool) error {
|
|
if c.Publish == nil {
|
|
return errors.New("this router cannot reach the bus")
|
|
}
|
|
verb := asks.Send
|
|
switch {
|
|
case edit:
|
|
verb = asks.Edit
|
|
case len(m.Actions) > 0:
|
|
verb = asks.Show
|
|
}
|
|
// Secret travels whatever the path: a link's code is kept by no channel (the review of 2026-10-09 found it
|
|
// dropped here, where the desk's own role sends).
|
|
out := asks.Message{Handle: handle, Title: m.Title, Body: m.Body, Note: m.Note, Urgent: m.Urgent, Silent: m.Quiet, Reply: m.Reply,
|
|
Secret: m.Secret}
|
|
if c.Bench.To != nil {
|
|
out.To = c.Bench.To(kind)
|
|
}
|
|
if m.To != "" {
|
|
out.To = m.To
|
|
}
|
|
for _, a := range m.Actions {
|
|
out.Buttons = append(out.Buttons, asks.Button{Label: a.Label, Ticket: a.Token})
|
|
}
|
|
return c.Publish(asks.ChannelSubject(verb, kind), out, handle+"."+fmt.Sprint(time.Now().UnixNano()))
|
|
}
|
|
|
|
func newHandle() string {
|
|
var b [9]byte
|
|
_, _ = rand.Read(b[:])
|
|
return "m" + hex.EncodeToString(b[:])
|
|
}
|
|
|
|
// Asker calls a tool through the mesh, as stdio.Ask does: how the open conditions are read.
|
|
type Asker func(key string, body any) (json.RawMessage, error)
|
|
|
|
// KindChannel is one kind as a channel: for a test, and for the status of each.
|
|
type KindChannel struct {
|
|
Bench *Bench
|
|
Kind string
|
|
Publish func(subject string, body any, id string) error
|
|
}
|
|
|
|
func (c *KindChannel) Name() string { return c.Kind }
|
|
func (c *KindChannel) Ready() error { return c.Bench.Ready(c.Kind) }
|
|
func (c *KindChannel) CanEdit() bool { return c.Bench.Promises(c.Kind, "edit") }
|
|
func (c *KindChannel) SilentEdit() bool { return c.Bench.editsSilently(c.Kind) }
|
|
func (c *KindChannel) Send(m Message) (string, error) {
|
|
s := &SeatChannel{Bench: c.Bench, Publish: c.Publish}
|
|
handle := newHandle()
|
|
return handle, s.putOn(c.Kind, handle, m, false)
|
|
}
|
|
func (c *KindChannel) Replace(handle string, m Message) error {
|
|
if handle == "" {
|
|
return errors.New("no message on record to replace")
|
|
}
|
|
s := &SeatChannel{Bench: c.Bench, Publish: c.Publish}
|
|
return s.putOn(c.Kind, handle, m, false)
|
|
}
|
|
func (c *KindChannel) Edit(handle string, m Message) error {
|
|
s := &SeatChannel{Bench: c.Bench, Publish: c.Publish}
|
|
return s.putOn(c.Kind, handle, m, true)
|
|
}
|
|
|
|
// kindChannels is every kind the controller says exists, as a channel; the two roles when it says none yet.
|
|
func (h *Holder) kindChannels() []Channel {
|
|
if h.Bench == nil || len(h.Bench.Kinds()) == 0 {
|
|
return h.channels()
|
|
}
|
|
var out []Channel
|
|
for _, k := range h.Bench.Kinds() {
|
|
out = append(out, &KindChannel{Bench: h.Bench, Kind: k.Kind, Publish: h.Publish})
|
|
}
|
|
return out
|
|
}
|