Files
mesh-catalog/modules/mesh-config/cmd/mesh-config/proposals.go
T

929 lines
34 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
// A config value proposed for the operator's Approve (novox/hq ADR 0348 §8, ADR 0277's rules with mesh-config as
// the asker; design 57 K4).
//
// Whoever the bus admits may PROPOSE a value at one level — mesh config, node config or a module's own value on a
// node — a trusted one among them, which the verb sets only from the controller's terminal. A proposal changes
// nothing. It is judged as a set judges it, then kept in mesh-config_proposals (which this module's account alone
// writes) with the value whole, the value stored at that level when it was proposed (what the operator is shown
// the change against), who proposed it, why and when. It is asked of the operator **under mesh-config's own name**
// on the operator-channel seat — its own grant, from its manifest's `uses`, and its own cap of open asks — as an
// ask whose two answers, Approve and Decline, are both at the level approve, so only a channel that proves who
// answered carries either. The ask says the level, the key and the value, and that, approved, it takes effect at
// once on every node it reaches; each option binds the proposal's level, key, the digest of the exact value and of
// the value it was shown against.
//
// **On the answer** mesh-config reads the router's own record of its ask (which only the router writes, and this
// module reads under its own name alone), composes the ask again from the proposal and holds the warrant to it:
// the same ask digest, an option it offered, at its level, in time (asks.Warrant.For), and the act the option
// bound is this proposal's (asks.Option.Performs). A record whose value or shown value changed after it was asked
// is refused, and so is an Approve when the value stored at the level is no longer the one the operator was shown
// the change against. Then the value is set exactly as a set at the terminal is, with the same judgement and
// history, and who approved it is kept beside the value; the warrant is recorded in the controller's hand-act log.
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"sort"
"strconv"
"strings"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/asks"
"git.novox.be/novox/mesh-sdk/go/outward"
)
// Module is this module's name: the one the bus grants its asks under.
const Module = "mesh-config"
// ProposalVerb is the act a proposal's answers bind.
const ProposalVerb = Seat + "." + Verb
// ProposalLasts is how long the operator has to answer: a day less ten minutes (ADR 0277 §9), under the router's
// bound for an approval.
const ProposalLasts = asks.ApproveLasts - 10*time.Minute
// How long a proposal waits for the router's word on its new ask, and how often it looks (ADR 0277 §20).
const (
RouterAnswersWithin = 8 * time.Second
RouterAnswersEvery = 250 * time.Millisecond
)
// The two answers, both at the level approve.
const (
OptionApprove = "approve"
OptionDecline = "decline"
)
// Where a proposal stands.
const (
ProposalWaiting = "waiting"
ProposalApproved = "approved"
ProposalDeclined = "declined"
ProposalEnded = "ended"
ProposalRefused = "refused"
ProposalUnasked = "not asked"
)
// AskBus is the operator channel as this module reaches it: publish an ask under its own name, read the router's
// record of one of its asks, the kinds of the channel bench with what each promises, and the controller's
// hand-act log for a warrant acted on.
type AskBus interface {
Publish(subject string, body any, id string) error
Record(id string) (*stdio.StateEntry, error)
Kinds() ([]stdio.KindHeld, error)
Warranted(ask string) error
}
// Proposal is one value proposed, as mesh-config_proposals keeps it. Every field the ask is composed from is here,
// so the ask is composed again from it alone to hold the warrant to.
type Proposal struct {
ID string `json:"id"`
Action string `json:"action"`
Level Level `json:"level"`
Key string `json:"key"`
// Value is the value proposed, whole, and Digest its digest (valueDigest); a clear carries none.
Value json.RawMessage `json:"value,omitempty"`
Digest string `json:"digest"`
// Was is the value stored at the level when it was proposed, HadValue whether there was one, and WasDigest its
// digest: what the operator is shown the change against, and what must still stand on Approve.
Was json.RawMessage `json:"was,omitempty"`
HadValue bool `json:"had-value"`
WasDigest string `json:"was-digest"`
Trusted bool `json:"trusted,omitempty"`
// Reaches is each module on each node whose value it moves, as it was judged when proposed.
Reaches []string `json:"reaches"`
// From is who proposed it, as the bus proved the caller; Why is theirs; At is when.
From string `json:"from"`
Why string `json:"why"`
At time.Time `json:"at"`
// State is where it stands, Outcome what came of it in words, Decided when; Answered is the warrant a person's
// choice held on (Approve or Decline), and Approved who approved it.
State string `json:"state"`
Outcome string `json:"outcome,omitempty"`
Decided time.Time `json:"decided,omitempty"`
Answered string `json:"answered,omitempty"`
Approved string `json:"approved,omitempty"`
// Recorded says the controller's hand-act log holds the warrant; Record is why not, while it does not.
Recorded bool `json:"recorded,omitempty"`
Record string `json:"record,omitempty"`
}
// approval is what a decided proposal gives the set it makes: the warrant in words, and what the operator was
// shown the change against.
type approval struct {
Proposal string
By string
Says string
Was json.RawMessage
HadValue bool
WasDigest string
}
// valueDigest is the digest a proposal binds of a value: SHA-256 over its one encoding (compact), "none" when
// there is no value.
func valueDigest(raw json.RawMessage, had bool) string {
text := "none"
if had {
text = "value:" + string(compact(raw))
}
sum := sha256.Sum256([]byte(text))
return "sha256:" + hex.EncodeToString(sum[:])
}
func shownOrNone(raw json.RawMessage, had bool) string {
if !had {
return "no value set"
}
return string(compact(raw))
}
// fingerprint is a digest as the operator is shown it: its first 24 hexadecimal digits in groups of four.
func fingerprint(digest string) string {
h := strings.TrimPrefix(digest, "sha256:")
if len(h) < 24 {
return h
}
var g []string
for i := 0; i < 24; i += 4 {
g = append(g, h[i:i+4])
}
return strings.Join(g, " ")
}
// Fingerprint is the proposal as the operator is shown it: the value proposed, or for a clear the value it clears.
func (p Proposal) Fingerprint() string {
if p.Action == "clear" {
return fingerprint(p.WasDigest)
}
return fingerprint(p.Digest)
}
// about is what its ask is about: the value's place, so a newer proposal for it replaces an older one still open
// (ADR 0277 §10, at a level, a key).
func (p Proposal) about() string { return "config." + p.Level.ValueKey(p.Key) }
// Act is what an answer binds: this proposal's level, key, the digest of its exact value and of the value it was
// shown against, who proposed it and when, and the answer.
func (p Proposal) Act(option string) asks.Act {
return asks.Act{"verb": ProposalVerb, "option": option, "proposal": p.ID, "action": p.Action,
"level": p.Level.Kind, "node": p.Level.Node, "module": p.Level.Module, "key": p.Key, "value": p.Digest,
"was": p.WasDigest, "had-value": strconv.FormatBool(p.HadValue), "from": p.From,
"at": p.At.UTC().Format(time.RFC3339Nano)}
}
// ---- what the operator reads --------------------------------------------------------------------------
// Markers for what a phone is not shown.
const (
markAddress = "‹address›"
markPath = "‹path›"
markWithheld = "‹withheld›"
)
// sayable is a text as a channel that proves nothing is shown it, under the content rule (outward.Check): as it
// is, or the marker of what it carries.
func sayable(text string) (string, bool) {
refusal, ok := outward.Check(text)
if ok {
return text, true
}
switch refusal.Class {
case "address":
return markAddress, false
case "path":
return markPath, false
}
return markWithheld, false
}
// change is the change in one line, "+ key: value", "~ key: value (was: old)" or "- key (was: old)": each value
// exact (for a channel that proves who answers, ADR 0277 §6 and issue 383) or under the content rule.
func (p Proposal) change(exact bool) string {
say := func(raw json.RawMessage) string {
text := string(compact(raw))
if exact {
return text
}
s, _ := sayable(text)
return s
}
switch {
case p.Action == "clear":
return fmt.Sprintf("- %s (was: %s)", p.Key, say(p.Was))
case p.HadValue:
return fmt.Sprintf("~ %s: %s (was: %s)", p.Key, say(p.Value), say(p.Was))
}
return fmt.Sprintf("+ %s: %s", p.Key, say(p.Value))
}
// whole is whether every value is shown exactly under the content rule.
func (p Proposal) whole() bool { return p.change(false) == p.change(true) }
// reaches is every node it reaches, in words.
func (p Proposal) reaches() string {
nodes := map[string]bool{}
for _, r := range p.Reaches {
if i := strings.LastIndex(r, " on "); i > 0 {
nodes[r[i+4:]] = true
}
}
switch len(nodes) {
case 0:
return "none today, as it moves no module's value"
case 1:
return sortedKeys(nodes)[0] + " (" + strings.Join(p.Reaches, ", ") + ")"
}
list := sortedKeys(nodes)
return fmt.Sprintf("%d nodes, %s", len(list), strings.Join(list, ", "))
}
// summary is the ask's first line: the level, the key and the value, and that it takes effect at once.
func (p Proposal) summary() string {
verb := "Sets " + p.Key
if p.Action == "clear" {
verb = "Clears " + p.Key
}
trusted := ""
if p.Trusted {
trusted = ", a trusted value,"
}
reach := p.reaches()
s := fmt.Sprintf("%s%s at %s; approved, it takes effect at once on every node it reaches: %s.", verb, trusted,
p.Level, reach)
if len([]rune(s)) > asks.SummaryLength {
s = fmt.Sprintf("%s%s at %s; approved, it takes effect at once on every node it reaches (%d modules).", verb,
trusted, p.Level, len(p.Reaches))
}
return s
}
// point is a line cut to what a phone shows at a glance, saying so.
func point(line string) string {
const cut = "… (cut: read it in the note)"
if r := []rune(line); len(r) > asks.PointLength {
return string(r[:asks.PointLength-len([]rune(cut))]) + cut
}
return line
}
// headline is the ask's headline, shortened when the names make it longer than the router allows (ADR 0277 §5).
func (p Proposal) headline() string {
verb := "Set"
if p.Action == "clear" {
verb = "Clear"
}
where := "for the whole mesh"
switch p.Level.Kind {
case LevelNode:
where = "on " + p.Level.Node
case LevelModule:
where = "for " + p.Level.Module + " on " + p.Level.Node
}
for _, h := range []string{fmt.Sprintf("%s %s %s?", verb, p.Key, where), fmt.Sprintf("%s a config value %s?", verb,
where), verb + " a config value?"} {
if len([]rune(h)) <= asks.HeadlineLength {
if s, ok := sayable(h); ok {
return s
}
}
}
return verb + " a config value?"
}
// Ask is the proposal's ask, composed from it alone: its words, its two options each binding its act, who
// answers, its expiry and what it is about.
func (p Proposal) Ask() asks.Ask {
why, _ := sayable("Why: " + p.Why)
if why != "Why: "+p.Why {
why = "Why: given, and read in full with the proposal"
}
compose := func(exact bool) string {
return strings.Join([]string{p.summary(), point(p.change(exact)), point(why)}, "\n")
}
details := func(exact bool) string {
var d strings.Builder
d.WriteString(p.change(exact) + "\n")
from, _ := sayable(p.From)
fmt.Fprintf(&d, "Proposed by %s at %s UTC. Fingerprint %s.\n", from, p.At.UTC().Format("15:04 on 2 Jan"),
p.Fingerprint())
if !exact && !p.whole() {
d.WriteString("Where who answers is not proven, its values are shown as ‹address›, ‹path› or ‹withheld›.\n")
}
fmt.Fprintf(&d, "Read it whole, with this fingerprint: mesh-config's config verb with action proposals and "+
"proposal %s, through the mesh MCP server.", p.ID)
return d.String()
}
a := asks.Ask{ID: p.ID, Headline: p.headline(), Explanation: compose(false), Who: asks.Operator,
Expires: p.At.Add(ProposalLasts), OnExpiry: "the proposal is discarded; nothing changes", About: p.about(),
Details: details(false)}
if w := compose(true); w != a.Explanation {
a.Whole = w
}
if w := details(true); w != a.Details {
a.WholeDetails = w
}
does := "the value is set at once, and every node it reaches takes it"
if p.Action == "clear" {
does = "the value is cleared at once, and every node it reaches takes what is below it"
}
for _, o := range []struct{ id, label, does string }{
{OptionApprove, "Approve", does},
{OptionDecline, "Decline", "nothing changes; the proposal is discarded"},
} {
binds, _ := asks.ActDigest(p.Act(o.id))
a.Options = append(a.Options, asks.Option{ID: o.id, Label: o.label, Does: o.does, Level: asks.Approve,
Binds: binds})
}
return a
}
// problem says why a proposal as kept is not one this module made, or "": judged again before an Approve is
// acted on, so a record whose value or shown value changed after it was asked is refused (ADR 0277 §12).
func (p Proposal) problem() string {
switch {
case !asks.UsableID(p.ID):
return "its id is not one"
case p.Action != "set" && p.Action != "clear":
return "its action is neither set nor clear"
case !keyName.MatchString(p.Key):
return "its key is not one"
case p.At.IsZero():
return "it says no time"
case p.Action == "set" && valueDigest(p.Value, true) != p.Digest:
return "its value is not the value its digest names: it changed after it was asked"
case p.Action == "clear" && (len(p.Value) != 0 || p.Digest != valueDigest(nil, false)):
return "a clear carries no value"
case valueDigest(p.Was, p.HadValue) != p.WasDigest:
return "the value it was shown against is not the one its digest names: it changed after it was asked"
}
return ""
}
// ---- proposing -----------------------------------------------------------------------------------------
// ProposeAnswer is what a proposal answers.
type ProposeAnswer struct {
Said string `json:"said"`
Proposal string `json:"proposal"`
Asked string `json:"asked"`
Fingerprint string `json:"fingerprint"`
Expires time.Time `json:"expires"`
Router string `json:"router"`
Reaches []string `json:"reaches"`
Replaced []string `json:"replaced,omitempty"`
}
// AtTheTerminalInstead names the other way, said whenever a proposal cannot be asked (ADR 0277 §19).
const AtTheTerminalInstead = "the operator may set it at the controller's terminal instead, through the controller's " +
"config line, which carries the value to mesh-config"
// approverReady says why no channel that proves who answered can carry an approval, when this module can tell
// from the channel bench's kinds; an empty list says nothing, and the router is the judge (ADR 0259 §4).
func approverReady(bus AskBus) string {
kinds, err := bus.Kinds()
if err != nil || len(kinds) == 0 {
return ""
}
channels := 0
for _, k := range kinds {
if k.Seat != asks.ChannelSeat {
continue
}
channels++
if k.Promises("choice") && k.Promises("exact-render") && k.Promises("verified-sender") {
return ""
}
}
if channels == 0 {
return ""
}
return "no channel that proves who answered (one promising a verified sender, such as Telegram) is on the mesh"
}
func newProposalID() string {
var b [8]byte
_, _ = rand.Read(b[:])
return "cfg-" + hex.EncodeToString(b[:])
}
// endedWord is the router's word that an ask ended without a choice, heard while its proposer waits.
type endedWord struct {
Outcome asks.Outcome
Words string
}
func (c *Config) heardEnded(id string) (endedWord, bool) {
c.endedMu.Lock()
defer c.endedMu.Unlock()
w, ok := c.ended[id]
return w, ok
}
func (c *Config) tellEnded(id string, w endedWord) {
c.endedMu.Lock()
defer c.endedMu.Unlock()
if c.ended == nil {
c.ended = map[string]endedWord{}
}
c.ended[id] = w
}
// Propose judges a value as a set would, keeps it, and asks the operator under mesh-config's own name. Nothing is
// set here: only the operator's Approve sets it (Decide).
func (c *Config) Propose(call Call) (*ProposeAnswer, error) {
refuse := func(format string, a ...any) (*ProposeAnswer, error) {
return nil, errors.New(fmt.Sprintf(format, a...) + ". Nothing was proposed")
}
if c.Asks == nil {
return refuse("the operator cannot be asked from here; %s", AtTheTerminalInstead)
}
if call.By != "" {
return refuse("by names who gave a value at the controller's terminal; a proposal is asked of the operator")
}
if call.Proposal != "" {
return refuse("proposal names a proposal to read, with action proposals")
}
action := "set"
if call.Clear {
action = "clear"
if call.Value != nil {
return refuse("a clear carries no value")
}
}
c.mu.Lock()
prop, ask, replaced, err := c.propose(call, action)
c.mu.Unlock()
if err != nil {
return nil, err
}
answer := &ProposeAnswer{Proposal: prop.ID, Asked: ask.Headline, Fingerprint: prop.Fingerprint(),
Expires: ask.Expires, Reaches: prop.Reaches, Replaced: replaced,
Router: "the router has not said yet whether it took the ask; config's proposals shows it"}
// The router's word, before answering (ADR 0277 §20): it refuses at once an ask no channel can carry.
within, every := c.RouterWithin, c.RouterEvery
if within <= 0 {
within = RouterAnswersWithin
}
if every <= 0 {
every = RouterAnswersEvery
}
for deadline := time.Now().Add(within); time.Now().Before(deadline); time.Sleep(every) {
if w, ended := c.heardEnded(prop.ID); ended {
return nil, fmt.Errorf("the router did not ask the operator: the ask %s (%s). The proposal is ended and "+
"nothing is set; %s", w.Outcome, w.Words, AtTheTerminalInstead)
}
if e, err := c.Asks.Record(prop.ID); err == nil && e != nil {
answer.Router = "the router took the ask and shows it on the channels that can carry it"
break
}
}
answer.Said = fmt.Sprintf("proposed %s of %s at %s (fingerprint %s): the operator is asked %q on a channel that "+
"proves who answered. Nothing changes until they approve; on Approve it takes effect at once, and on Decline "+
"or at %s UTC it is discarded", action, prop.Key, prop.Level, prop.Fingerprint(), ask.Headline,
ask.Expires.UTC().Format("15:04 on 2 Jan"))
return answer, nil
}
// propose is Propose's judgement, its record and its ask; the caller holds the lock.
func (c *Config) propose(call Call, action string) (Proposal, asks.Ask, []string, error) {
fail := func(format string, a ...any) (Proposal, asks.Ask, []string, error) {
return Proposal{}, asks.Ask{}, nil, errors.New(fmt.Sprintf(format, a...) + ". Nothing was proposed")
}
level, err := call.level()
if err != nil {
return fail("%v", err)
}
if !keyName.MatchString(call.Key) {
return fail("name the key to propose: lower-case letters, digits, dots, dashes and underscores")
}
why := strings.TrimSpace(call.Why)
if why == "" || strings.Contains(why, "\n") {
return fail("say why, in one line: every change keeps its why (novox/hq ADR 0348 §9)")
}
if call.Caller == "" || call.Caller == Unproven {
return fail("the call names no proven caller, and a proposal keeps who made it: call config through the mesh, " +
"which names you")
}
s, err := c.sources()
if err != nil {
return Proposal{}, asks.Ask{}, nil, err
}
// **Judged as a set judges it, before anyone is asked** (ADR 0277 §4): no ask for a value the verb would refuse.
r, trusted, err := c.declared(s, level, call.Key)
if err != nil {
return fail("%v", err)
}
vk := level.ValueKey(call.Key)
was, had := s.Values[vk]
now := c.now()
prop := Proposal{ID: newProposalID(), Action: action, Level: level, Key: call.Key, HadValue: had,
WasDigest: valueDigest(was.Value, had), Trusted: trusted, From: call.Caller, Why: why, At: now,
State: ProposalWaiting, Digest: valueDigest(nil, false)}
if had {
prop.Was = compact(was.Value)
}
after := Sources{Assignments: s.Assignments, Declared: s.Declared, Predefined: s.Predefined,
Values: map[string]Value{}}
for k, v := range s.Values {
after.Values[k] = v
}
switch action {
case "set":
if call.Value == nil {
return fail("give the value to propose for %s", call.Key)
}
raw, err := json.Marshal(call.Value)
if err != nil {
return fail("the value cannot be read: %v", err)
}
var value any
_ = json.Unmarshal(raw, &value)
if problems := r.check(value); len(problems) > 0 {
return fail("%s is refused for %s at %s: %s", show(value), call.Key, level, strings.Join(problems, "; "))
}
prop.Value, prop.Digest = compact(raw), valueDigest(raw, true)
if had && string(compact(was.Value)) == string(prop.Value) {
return fail("%s at %s already holds exactly %s", call.Key, level, prop.Value)
}
after.Values[vk] = Value{Value: prop.Value}
case "clear":
if !had {
return fail("nothing is set for %s at %s, so there is nothing to clear", call.Key, level)
}
delete(after.Values, vk)
}
for _, m := range diff(Resolve(s), Resolve(after)) {
prop.Reaches = append(prop.Reaches, m.Module+" on "+m.Node)
}
if prop.Reaches == nil {
prop.Reaches = []string{}
}
// **The operator is shown exactly what they approve** where who answered is proven (ADR 0277 §6, §19): a value
// shaped like a secret could be shown nowhere, so it is not proposed; config holds no secret.
if refusal, ok := outward.CheckSecret(prop.change(true)); !ok {
return fail("the change carries %s, which leaves the mesh nowhere, so the operator could not be shown what "+
"they approve; config holds no secret, and %s", refusal, AtTheTerminalInstead)
}
// **Its own cap of open asks, and the same change once** (ADR 0277 §21); a newer proposal for the same place
// replaces an older one still open (§10).
all, err := c.proposals()
if err != nil {
return Proposal{}, asks.Ask{}, nil, err
}
var replace []Proposal
open := 0
for _, o := range all {
if o.State != ProposalWaiting || now.After(o.At.Add(ProposalLasts)) {
continue
}
if o.about() == prop.about() {
if o.Action == prop.Action && o.Digest == prop.Digest && o.WasDigest == prop.WasDigest {
return fail("the same change is proposed already, as %s, and waits for the operator's answer", o.ID)
}
replace = append(replace, o)
continue
}
open++
}
if open >= asks.MostOpen {
return fail("%d proposals already wait for the operator's answer, and mesh-config asks at most %d at once; "+
"config's proposals lists them", open, asks.MostOpen)
}
if why := approverReady(c.Asks); why != "" {
return fail("%s; %s", why, AtTheTerminalInstead)
}
ask := prop.Ask()
if err := ask.Check(now); err != nil {
return fail("%v", err)
}
if err := c.Store.Put(ProposalsState, prop.ID, prop); err != nil {
return fail("the proposal could not be kept, so the operator was not asked: %v", err)
}
var replaced []string
for _, o := range replace {
o.State, o.Outcome, o.Decided = ProposalEnded, "replaced by the newer proposal "+prop.ID+"; nothing was set", now
if err := c.Store.Put(ProposalsState, o.ID, o); err == nil {
replaced = append(replaced, o.ID)
}
}
if err := c.Asks.Publish(asks.AskSubject(Module), ask, ask.ID); err != nil {
prop.State, prop.Outcome = ProposalUnasked, "the ask could not be published: "+err.Error()
_ = c.Store.Put(ProposalsState, prop.ID, prop)
c.logf("[mesh-config] proposal %s could not be asked: %v", prop.ID, err)
return Proposal{}, asks.Ask{}, nil, fmt.Errorf("the operator could not be asked (%v); the proposal is kept "+
"and will never be set; %s", err, AtTheTerminalInstead)
}
c.logf("[mesh-config] proposal %s (%s %s at %s, fingerprint %s) asked of the operator", prop.ID, action, prop.Key,
prop.Level, prop.Fingerprint())
return prop, ask, replaced, nil
}
// proposals is every proposal kept, oldest first.
func (c *Config) proposals() ([]Proposal, error) {
var out []Proposal
err := c.each(ProposalsState, func(key string, raw []byte) error {
var p Proposal
if json.Unmarshal(raw, &p) != nil || p.ID != key {
return nil // not one this module wrote: never asked, never acted on
}
out = append(out, p)
return nil
})
if err != nil {
return nil, fmt.Errorf("the proposals cannot be read: %w", err)
}
sort.SliceStable(out, func(i, j int) bool { return out[i].At.Before(out[j].At) })
return out, nil
}
// ---- deciding --------------------------------------------------------------------------------------------
// routerRecord is the router's record of one ask, as far as this module reads it.
type routerRecord struct {
State string `json:"state"`
Ask *asks.Ask `json:"ask"`
Warrant *asks.Warrant `json:"warrant"`
}
// Decide decides every waiting proposal whose ask the router has closed, oldest warrant first, each held to the
// warrant as described at the top of this file; and asks the controller to record every warrant acted on that its
// hand-act log does not hold yet. It answers a line for each decision.
func (c *Config) Decide() ([]string, error) {
if c.Asks == nil {
return nil, nil
}
c.mu.Lock()
defer c.mu.Unlock()
all, err := c.proposals()
if err != nil {
return nil, err
}
now := c.now()
type closed struct {
p Proposal
w asks.Warrant
}
var ready []closed
var said []string
for _, p := range all {
if p.State != ProposalWaiting {
continue
}
e, err := c.Asks.Record(p.ID)
if err == nil && e == nil && now.After(p.At.Add(ProposalLasts+time.Hour)) {
// Ended, and the router keeps no record of it: no answer can be proven, so nothing is set.
said = append(said, c.end(p, "the router keeps no record of its ask (refused before it was taken, or "+
"forgotten); nothing was set"))
continue
}
if err != nil || e == nil {
continue // open, not yet recorded, or not readable now: read again on the next look
}
var rec routerRecord
if json.Unmarshal(e.Value, &rec) != nil || rec.State == "open" || rec.Warrant == nil {
continue
}
ready = append(ready, closed{p, *rec.Warrant})
}
sort.SliceStable(ready, func(i, j int) bool { return ready[i].w.At.Before(ready[j].w.At) })
for _, r := range ready {
said = append(said, c.decide(r.p, r.w))
}
c.recordWarrants(now)
return said, nil
}
// end records a proposal ended without the operator's choice.
func (c *Config) end(p Proposal, why string) string {
p.State, p.Outcome, p.Decided = ProposalEnded, why, c.now()
if err := c.Store.Put(ProposalsState, p.ID, p); err != nil {
return fmt.Sprintf("proposal %s: %s, and that could not be kept (%v)", p.ID, why, err)
}
return "proposal " + p.ID + ": " + why
}
// decide holds one closed ask's warrant to its proposal and, on an Approve that holds, sets the value. The caller
// holds the lock.
func (c *Config) decide(p Proposal, w asks.Warrant) string {
now := c.now()
keep := func(state, outcome string) string {
p.State, p.Outcome, p.Decided = state, outcome, now
if err := c.Store.Put(ProposalsState, p.ID, p); err != nil {
return fmt.Sprintf("proposal %s: %s, and that could not be kept (%v)", p.ID, outcome, err)
}
return "proposal " + p.ID + ": " + outcome
}
refused := func(why string) string {
c.logf("[mesh-config] REFUSED the answer to proposal %s: %s", p.ID, why)
return keep(ProposalRefused, "the answer is refused and nothing is set: "+why)
}
if w.Asker != Module || w.Ask != p.ID {
return refused(fmt.Sprintf("the router's record is for %s's ask %s", w.Asker, w.Ask))
}
if w.Outcome != asks.OutcomeChosen {
why := "the ask " + string(w.Outcome)
if w.Words != "" {
why += " (" + w.Words + ")"
}
return keep(ProposalEnded, why+"; nothing was set")
}
// **The record is held to what was asked** (ADR 0277 §11, §12): its value and the value it was shown against
// still digest to what the ask bound, the warrant answers the ask composed again from it, and the act about to be
// performed is the one the chosen option bound.
if problem := p.problem(); problem != "" {
return refused("the proposal is not the one asked: " + problem)
}
option, err := w.For(Module, p.Ask())
if err == nil {
err = option.Performs(p.Act(option.ID))
}
if err != nil {
return refused(err.Error())
}
switch option.ID {
case OptionDecline:
p.Answered = w.Says()
return keep(ProposalDeclined, "declined by the operator ("+w.Says()+"); nothing was set")
case OptionApprove:
p.Answered, p.Approved = w.Says(), approvedWords(p, w)
default:
return refused("the option " + option.ID + " is not one a proposal offers")
}
// Claimed before it is acted on: a warrant heard twice, or read again, sets nothing twice.
p.State, p.Outcome, p.Decided = ProposalApproved, "approved by the operator; being set", now
if err := c.Store.Put(ProposalsState, p.ID, p); err != nil {
return fmt.Sprintf("proposal %s: approved by the operator, and not set: the decision could not be kept (%v); "+
"it is read again on the next look", p.ID, err)
}
call := Call{Action: p.Action, Key: p.Key, Module: p.Level.Module, Node: p.Level.Node, Why: p.Why, Caller: p.From,
approval: &approval{Proposal: p.ID, By: "the operator", Says: p.Approved, Was: p.Was, HadValue: p.HadValue,
WasDigest: p.WasDigest}}
if p.Action == "set" {
var v any
_ = json.Unmarshal(p.Value, &v)
call.Value = v
if v == nil {
call.Value = json.RawMessage("null")
}
}
a, err := c.change(call)
if err != nil {
return keep(ProposalRefused, "approved by the operator, and refused on Approve, so nothing is set: "+err.Error())
}
return keep(ProposalApproved, "approved by the operator and set: "+a.Said)
}
// approvedWords is who approved it, as kept beside the value (ADR 0277 §15).
func approvedWords(p Proposal, w asks.Warrant) string {
who := "the operator"
if w.By != nil {
who = "the " + w.By.Who
if w.By.Identity != "" {
who += ", as " + w.By.Kind + " identity " + w.By.Identity
}
via := w.By.Kind
if w.By.Verified != "" {
via += " (" + w.By.Verified + ")"
}
who += ", via " + via
}
return fmt.Sprintf("approved by %s at %s, ask %s, proposed by %s", who, w.At.UTC().Format(time.RFC3339), p.ID, p.From)
}
// recordWarrants asks the controller to record each warrant acted on (Approve or Decline) that its hand-act log
// does not hold yet (ADR 0277 §17): the controller reads it from the router's own record. The caller holds the lock.
func (c *Config) recordWarrants(now time.Time) {
all, err := c.proposals()
if err != nil {
return
}
for _, p := range all {
if p.Recorded || p.Answered == "" || now.Sub(p.Decided) > 7*24*time.Hour {
continue
}
if err := c.Asks.Warranted(p.ID); err != nil {
p.Record = "not recorded in the hand-act log yet: " + err.Error()
} else {
p.Recorded, p.Record = true, ""
}
_ = c.Store.Put(ProposalsState, p.ID, p)
}
}
// HeardEnd takes the router's word on one of this module's asks. An ask ended without a choice ends its proposal
// at once, and is told to a proposer waiting on it; nothing is set on such a word, so it needs no proof beyond its
// subject. A choice is only ever taken from the router's record (Decide).
func (c *Config) HeardEnd(w asks.Warrant) []string {
if w.Asker != Module || !asks.UsableID(w.Ask) {
return nil
}
if w.Outcome == asks.OutcomeChosen {
said, err := c.Decide()
if err != nil {
said = append(said, "reading the operator's answers: "+err.Error())
}
return said
}
c.mu.Lock()
defer c.mu.Unlock()
defer c.tellEnded(w.Ask, endedWord{Outcome: w.Outcome, Words: w.Words})
e, err := c.Store.Get(ProposalsState, w.Ask)
if err != nil || e == nil {
return nil
}
var p Proposal
if json.Unmarshal(e.Value, &p) != nil || p.ID != w.Ask || p.State != ProposalWaiting {
return nil
}
why := "the ask " + string(w.Outcome)
if w.Words != "" {
why += " (" + w.Words + ")"
}
return []string{c.end(p, why+"; nothing was set")}
}
// ---- the list ----------------------------------------------------------------------------------------------
// ProposalRow is one proposal as a listing says it.
type ProposalRow struct {
ID string `json:"id"`
Action string `json:"action"`
Level Level `json:"level"`
Key string `json:"key"`
State string `json:"state"`
Outcome string `json:"outcome,omitempty"`
From string `json:"from"`
At time.Time `json:"at"`
Expires time.Time `json:"expires"`
Fingerprint string `json:"fingerprint"`
Approved string `json:"approved,omitempty"`
// What one proposal read whole says besides.
Value json.RawMessage `json:"value,omitempty"`
Was json.RawMessage `json:"was,omitempty"`
Why string `json:"why,omitempty"`
Reaches []string `json:"reaches,omitempty"`
Router string `json:"router,omitempty"`
Phone string `json:"the phone,omitempty"`
Warning string `json:"warning,omitempty"`
}
// Proposals is every proposal, newest first; with a proposal's id, that one whole, with whether the router's ask
// is the one composed from it.
func (c *Config) Proposals(call Call) ([]ProposalRow, error) {
all, err := c.proposals()
if err != nil {
return nil, err
}
out := []ProposalRow{}
for i := len(all) - 1; i >= 0; i-- {
p := all[i]
if call.Proposal != "" && p.ID != call.Proposal {
continue
}
if (call.Key != "" && p.Key != call.Key) || (call.Node != "" && p.Level.Node != call.Node) ||
(call.Module != "" && p.Level.Module != call.Module) {
continue
}
row := ProposalRow{ID: p.ID, Action: p.Action, Level: p.Level, Key: p.Key, State: p.State, Outcome: p.Outcome,
From: p.From, At: p.At, Expires: p.At.Add(ProposalLasts), Fingerprint: p.Fingerprint(), Approved: p.Approved}
if p.State == ProposalWaiting && c.now().After(row.Expires) {
row.State = "expired, not yet heard"
}
if call.Proposal != "" {
row.Value, row.Was, row.Why, row.Reaches = p.Value, p.Was, p.Why, p.Reaches
if problem := p.problem(); problem != "" {
row.Warning = "this proposal is never set: " + problem
}
if c.Asks != nil {
if e, err := c.Asks.Record(p.ID); err == nil && e != nil {
var rec routerRecord
if json.Unmarshal(e.Value, &rec) == nil {
row.Router = rec.State
switch {
case rec.Ask == nil:
row.Phone = "cannot be compared: the router's record keeps no ask"
case rec.Ask.Digest() == p.Ask().Digest():
row.Phone = "shows the ask composed from exactly this proposal (fingerprint " + p.Fingerprint() + ")"
default:
row.Phone = "shows ANOTHER ask than this proposal composes: do not approve it"
}
}
}
}
}
out = append(out, row)
}
if call.Proposal != "" && len(out) == 0 {
return nil, errors.New("no proposal " + strconv.Quote(call.Proposal) + " is kept")
}
return out, nil
}