Four providers waited for a yes on 2026-10-31 and nobody could read what waited: the have said only "where its data is kept". The read verb composition answers it from the engine: the latest revision of the want's build or the one asked by digest, every revision the bucket keeps, what runs and where that is known from, the want and the have, the move's judgement and whether it waits for a yes, and with diff the resources that differ, a received file naming its consumers. revisionNamed now reads the same revisions.
784 lines
27 KiB
Go
784 lines
27 KiB
Go
package link
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
"git.novox.be/novox/mesh-sdk/go/asker"
|
|
"git.novox.be/novox/mesh-sdk/go/asks"
|
|
)
|
|
|
|
// The node seat node-engine's acts (novox/hq ADR 0563).
|
|
//
|
|
// A hand-over and an access act are the engine's own asks, performed only on the warrant (§3): a call performs nothing.
|
|
// The engine judges every value as it does for the controller's signed ask, then refuses in words or asks the operator
|
|
// under its own asker name (asks.EngineAsker) at the level approve, binding the digest of the verb, the node, every
|
|
// argument and the owner, group and mode it will set. A warrant is claimed once by compare-and-set in the engine's own
|
|
// state (asker.Client.Take), held to the ask it answers, and to the act bound and the disk as judged, judged again now;
|
|
// only then is the act performed and recorded with the warrant. A decline, an expiry or a changed disk says refused and
|
|
// changes nothing. One ask is open per path or access.
|
|
//
|
|
// The read-only verbs need no warrant (§2): judge-directories changes nothing; setuid-search is bounded here, one at a
|
|
// time and at most one started in an hour. Every act, with who asked as the runtime proved it, is kept in the engine's own
|
|
// declared state acts (ActsBucket, created by the grants part), under its own node's keys alone, read by the verb acts.
|
|
|
|
// The acts of the seat, as its verbs name them.
|
|
const (
|
|
ActHandOver = "hand-over"
|
|
ActAccess = "access-act"
|
|
ActJudgeDirectories = "judge-directories"
|
|
ActSetuidSearch = "setuid-search"
|
|
// ActComposition reads a module's composition beside what runs of it; read-only, no warrant (issue 774).
|
|
ActComposition = "composition"
|
|
)
|
|
|
|
// The states of an act.
|
|
const (
|
|
HandAsked = "asked"
|
|
HandDone = "done"
|
|
HandRefused = "refused"
|
|
HandAnswered = "answered"
|
|
)
|
|
|
|
// SearchEvery is how often a setuid search may be started on a node (ADR 0563 §2): one in any hour.
|
|
const SearchEvery = time.Hour
|
|
|
|
// askMargin is how long past an ask's expiry the router is given to say it ended, before the engine closes it itself.
|
|
const askMargin = 5 * time.Minute
|
|
|
|
// ActsBucket is the node-engine's declared state acts (its manifest's state, ADR 0563 §2, §3), the bucket the grants
|
|
// part creates (mesh-sdk buscompose EngineActsBucket): keys `<node>.act.<id>`, `<node>.open.<act>.<digest>` and
|
|
// `<node>.ask.<id>`, each written by this node's engine alone.
|
|
const ActsBucket = "mesh-host_acts"
|
|
|
|
// Judged is what the engine judged of one act before it asks: what it is about, what it will set (the owner, group and
|
|
// mode, in words), and what the disk holds now (Disk), compared again when the warrant arrives.
|
|
type Judged struct {
|
|
// Subject is the path of a hand-over, or the access of an access act.
|
|
Subject string
|
|
// Path is the directory the act changes, as the engine states it.
|
|
Path string
|
|
// Sets is the owner, group and mode the act will set, in words; bound in the ask's digest.
|
|
Sets string
|
|
// Disk is what the engine read on disk when it judged: owner, group, mode and the file it is.
|
|
Disk string
|
|
// Words is what the operator is shown of it, without a path (ADR 0234 §6): the path is in the whole words alone.
|
|
Words string
|
|
}
|
|
|
|
// HandAct is one act asked through the seat, as acts reads it.
|
|
type HandAct struct {
|
|
ID string `json:"id"`
|
|
Act string `json:"act"`
|
|
Subject string `json:"subject,omitempty"`
|
|
Caller string `json:"caller"`
|
|
Why string `json:"why,omitempty"`
|
|
At time.Time `json:"at"`
|
|
State string `json:"state"`
|
|
Said string `json:"said,omitempty"`
|
|
Ended time.Time `json:"ended,omitempty"`
|
|
// Ask is the operator's ask of a hand-over or an access act; Sets and Disk what was judged when it was asked.
|
|
Ask string `json:"ask,omitempty"`
|
|
Sets string `json:"sets,omitempty"`
|
|
Disk string `json:"disk,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
// Expires is when the ask ends unanswered.
|
|
Expires time.Time `json:"expires,omitempty"`
|
|
// By, Via, Proofs are the operator's warrant, recorded with the act (ADR 0563 §3).
|
|
By string `json:"by,omitempty"`
|
|
Via string `json:"via,omitempty"`
|
|
Proofs []string `json:"proofs,omitempty"`
|
|
// Auto says the engine asked it itself, on finding a library's root holding data outside its group (§4).
|
|
Auto bool `json:"auto,omitempty"`
|
|
}
|
|
|
|
// RootVerdict is the engine's answer to root-free (ADR 0563 §6), in the controller's words.
|
|
type RootVerdict struct {
|
|
Machine string `json:"machine"`
|
|
Free bool `json:"free"`
|
|
Why string `json:"why"`
|
|
Judged time.Time `json:"judged"`
|
|
}
|
|
|
|
// HandActs is the engine's side of the seat. Every func is the engine's own judgement or act; nil refuses.
|
|
type HandActs struct {
|
|
Node string
|
|
// State is where the acts and the asks are kept: this node's keys of ActsBucket (BusState), each key one act.
|
|
State ActStore
|
|
// Asker asks the operator as asks.EngineAsker(Node), with State as its store.
|
|
Asker *asker.Client
|
|
// JudgeHandOver judges a hand-over's path, refusing in words; HandOver records it on the warrant.
|
|
JudgeHandOver func(path string) (Judged, error)
|
|
HandOver HandOverFunc
|
|
// JudgeAccess judges an access act on an access, refusing in words; AccessAct runs it on the warrant.
|
|
JudgeAccess func(act, access string) (Judged, error)
|
|
AccessAct AccessActFunc
|
|
// JudgeDirectories, SetuidSearch and Searching are the read-only verbs' and whether a search runs now.
|
|
JudgeDirectories JudgeDirectoriesFunc
|
|
SetuidSearch SetuidSearchFunc
|
|
Searching func() bool
|
|
// RootFree is this machine's root-free verdict now.
|
|
RootFree func() RootVerdict
|
|
// Composition reads one module's composition beside what runs of it, read-only (the verb composition, issue 774):
|
|
// the latest of its want's build or the one whose digest is given, and with diff the resources that differ.
|
|
Composition func(module, digest string, diff bool) (any, error)
|
|
// Say says one of the seat's events, asked, done or refused; nil says nothing.
|
|
Say func(event string, body any)
|
|
Now func() time.Time
|
|
// Nonce keeps two asks of one second apart; nil is random.
|
|
Nonce func() string
|
|
|
|
mu sync.Mutex
|
|
}
|
|
|
|
func (h *HandActs) now() time.Time {
|
|
if h.Now != nil {
|
|
return h.Now()
|
|
}
|
|
return time.Now()
|
|
}
|
|
|
|
func (h *HandActs) nonce() string {
|
|
if h.Nonce != nil {
|
|
return h.Nonce()
|
|
}
|
|
var b [2]byte
|
|
_, _ = rand.Read(b[:])
|
|
return hex.EncodeToString(b[:])
|
|
}
|
|
|
|
func (h *HandActs) say(event string, a HandAct) {
|
|
if h.Say != nil {
|
|
h.Say(event, map[string]any{"act": a.Act, "subject": a.Subject, "caller": a.Caller, "ask": a.Ask,
|
|
"state": a.State, "said": a.Said, "node": h.Node})
|
|
}
|
|
}
|
|
|
|
// Call is the seat's one entry: a verb, the caller as the runtime proved it, and its arguments.
|
|
func (h *HandActs) Call(c EngineCall) (any, error) {
|
|
arg := func(k string) string { s, _ := c.Args[k].(string); return strings.TrimSpace(s) }
|
|
switch c.Verb {
|
|
case ActHandOver:
|
|
return h.AskHandOver(c.Caller, arg("path"), arg("why"), false)
|
|
case ActAccess:
|
|
return h.AskAccess(c.Caller, arg("act"), arg("access"), arg("why"), false)
|
|
case ActJudgeDirectories:
|
|
return h.Judge(c.Caller)
|
|
case ActSetuidSearch:
|
|
return h.Search(c.Caller, arg("why"))
|
|
case "root-free":
|
|
if h.RootFree == nil {
|
|
return nil, errors.New("this engine judges no machine root-free")
|
|
}
|
|
return map[string]any{"machines": []RootVerdict{h.RootFree()}}, nil
|
|
case ActComposition:
|
|
if h.Composition == nil {
|
|
return nil, errors.New("this engine reads no composition yet: its node agent has not started")
|
|
}
|
|
diff := arg("diff") == "true"
|
|
if b, ok := c.Args["diff"].(bool); ok {
|
|
diff = b
|
|
}
|
|
return h.Composition(arg("module"), arg("digest"), diff)
|
|
case "acts":
|
|
if id := arg("id"); id != "" {
|
|
return h.Act(id)
|
|
}
|
|
limit, _ := strconv.Atoi(arg("limit"))
|
|
if f, ok := c.Args["limit"].(float64); ok {
|
|
limit = int(f)
|
|
}
|
|
return h.Acts(limit)
|
|
}
|
|
return nil, fmt.Errorf("the node seat %s serves no verb %q", EngineSeat, c.Verb)
|
|
}
|
|
|
|
// ---- the record --------------------------------------------------------------------------------------------------
|
|
|
|
// ActStore is the engine's own state, this node's keys alone (BusState prefixes them with the node): each act one key,
|
|
// written by compare-and-set on its revision, an open ask one key made by a create.
|
|
type ActStore interface {
|
|
Get(key string) (*stdio.StateEntry, error)
|
|
Create(key string, value any) (uint64, error)
|
|
Update(key string, value any, revision uint64) (uint64, error)
|
|
Delete(key string) error
|
|
// Keys are the keys under prefix.
|
|
Keys(prefix string) ([]string, error)
|
|
}
|
|
|
|
func actKey(id string) string { return "act." + id }
|
|
|
|
// openKey is the one key an open ask of act on subject holds: made by a create, so a second ask of it is refused.
|
|
func openKey(act, subject string) string {
|
|
sum := sha256.Sum256([]byte(subject))
|
|
return "open." + act + "." + hex.EncodeToString(sum[:12])
|
|
}
|
|
|
|
// read is every act kept, oldest first.
|
|
func (h *HandActs) read() ([]HandAct, error) {
|
|
keys, err := h.State.Keys("act.")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("the engine's acts cannot be listed: %w", err)
|
|
}
|
|
var out []HandAct
|
|
for _, k := range keys {
|
|
a, _, err := h.readOne(k)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if a != nil {
|
|
out = append(out, *a)
|
|
}
|
|
}
|
|
sort.SliceStable(out, func(i, j int) bool { return out[i].At.Before(out[j].At) })
|
|
return out, nil
|
|
}
|
|
|
|
func (h *HandActs) readOne(key string) (*HandAct, uint64, error) {
|
|
e, err := h.State.Get(key)
|
|
if err != nil || e == nil {
|
|
return nil, 0, err
|
|
}
|
|
var a HandAct
|
|
if err := json.Unmarshal(e.Value, &a); err != nil {
|
|
return nil, 0, fmt.Errorf("the engine's act %s cannot be read: %w", key, err)
|
|
}
|
|
return &a, e.Revision, nil
|
|
}
|
|
|
|
// judgedKey keeps the node's judgements and refused searches, the last keptJudged of them: any caller may ask them, so
|
|
// they roll in one key and never add one (ADR 0563 §2: a wrong caller gains nothing, and fills no bucket).
|
|
const (
|
|
judgedKey = "judged"
|
|
keptJudged = 50
|
|
)
|
|
|
|
// roll adds a to the rolling key by compare-and-set, the oldest falling out.
|
|
func (h *HandActs) roll(a HandAct) error {
|
|
for range 5 {
|
|
e, err := h.State.Get(judgedKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var kept []HandAct
|
|
if e != nil {
|
|
if err := json.Unmarshal(e.Value, &kept); err != nil {
|
|
kept = nil // an unreadable roll is started again, never a reason to refuse
|
|
}
|
|
}
|
|
kept = append(kept, a)
|
|
if len(kept) > keptJudged {
|
|
kept = kept[len(kept)-keptJudged:]
|
|
}
|
|
if e == nil {
|
|
_, err = h.State.Create(judgedKey, kept)
|
|
} else {
|
|
_, err = h.State.Update(judgedKey, kept, e.Revision)
|
|
}
|
|
if !errors.Is(err, stdio.ErrChanged) {
|
|
return err
|
|
}
|
|
}
|
|
return errors.New("the engine's judgements kept changing under its write")
|
|
}
|
|
|
|
func (h *HandActs) rolled() ([]HandAct, error) {
|
|
e, err := h.State.Get(judgedKey)
|
|
if err != nil || e == nil {
|
|
return nil, err
|
|
}
|
|
var kept []HandAct
|
|
if err := json.Unmarshal(e.Value, &kept); err != nil {
|
|
return nil, fmt.Errorf("the engine's judgements cannot be read: %w", err)
|
|
}
|
|
return kept, nil
|
|
}
|
|
|
|
func (h *HandActs) record(a HandAct) error {
|
|
_, err := h.State.Create(actKey(a.ID), a)
|
|
return err
|
|
}
|
|
|
|
// end marks the open act of an ask ended, once, by compare-and-set on its revision, and frees its open key: false when
|
|
// it was not open.
|
|
func (h *HandActs) end(ask string, set func(*HandAct)) (HandAct, bool, error) {
|
|
for range 5 {
|
|
a, rev, err := h.readOne(actKey(ask))
|
|
if err != nil || a == nil || a.State != HandAsked {
|
|
return HandAct{}, false, err
|
|
}
|
|
set(a)
|
|
a.Ended = h.now().UTC()
|
|
if _, err := h.State.Update(actKey(ask), *a, rev); errors.Is(err, stdio.ErrChanged) {
|
|
continue
|
|
} else if err != nil {
|
|
return HandAct{}, false, err
|
|
}
|
|
_ = h.State.Delete(openKey(a.Act, a.Subject))
|
|
return *a, true, nil
|
|
}
|
|
return HandAct{}, false, errors.New("the engine's act " + ask + " kept changing under its write")
|
|
}
|
|
|
|
// Act is the verb acts with an id: that one act.
|
|
func (h *HandActs) Act(id string) (any, error) {
|
|
a, _, err := h.readOne(actKey(id))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if a == nil {
|
|
return nil, fmt.Errorf("this engine keeps no act %q", id)
|
|
}
|
|
return map[string]any{"node": h.Node, "acts": []HandAct{*a}}, nil
|
|
}
|
|
|
|
// Acts is the verb acts: every act kept, newest first, at most limit when given.
|
|
func (h *HandActs) Acts(limit int) (any, error) {
|
|
acts, err := h.read()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
judged, err := h.rolled()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
acts = append(acts, judged...)
|
|
sort.SliceStable(acts, func(i, j int) bool { return acts[i].At.Before(acts[j].At) })
|
|
out := make([]HandAct, 0, len(acts))
|
|
for i := len(acts) - 1; i >= 0; i-- {
|
|
out = append(out, acts[i])
|
|
}
|
|
if limit > 0 && len(out) > limit {
|
|
out = out[:limit]
|
|
}
|
|
return map[string]any{"node": h.Node, "acts": out}, nil
|
|
}
|
|
|
|
// ---- the read-only verbs (§2) -------------------------------------------------------------------------------------
|
|
|
|
// Judge is judge-directories: the engine's judgement, read-only, recorded with its caller.
|
|
func (h *HandActs) Judge(caller string) (any, error) {
|
|
if h.JudgeDirectories == nil {
|
|
return nil, errors.New("this engine judges no directory")
|
|
}
|
|
said, err := h.JudgeDirectories(caller)
|
|
a := HandAct{ID: h.id(ActJudgeDirectories), Act: ActJudgeDirectories, Caller: caller, At: h.now().UTC(), State: HandAnswered}
|
|
if err != nil {
|
|
a.State, a.Said = HandRefused, err.Error()
|
|
}
|
|
if rerr := h.roll(a); rerr != nil && err == nil {
|
|
return nil, rerr
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"node": h.Node, "said": said}, nil
|
|
}
|
|
|
|
// Search is setuid-search: refused while one runs and when one was started within the hour, naming when the next may
|
|
// start; otherwise the last search is thrown away and a full one starts, recorded with its caller.
|
|
func (h *HandActs) Search(caller, why string) (any, error) {
|
|
if h.SetuidSearch == nil {
|
|
return nil, errors.New("this engine keeps no search for setuid programs")
|
|
}
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
now := h.now().UTC()
|
|
if h.Searching != nil && h.Searching() {
|
|
return nil, errors.New("a search for setuid programs runs on this machine now; no second is started while it does")
|
|
}
|
|
// The last start is a key of its own, claimed by compare-and-set before the search starts: no number of other acts,
|
|
// nor the record of acts, moves it (ADR 0563 §2).
|
|
if err := h.claimSearch(now, caller); err != nil {
|
|
return nil, err
|
|
}
|
|
said, err := h.SetuidSearch(caller)
|
|
a := HandAct{ID: h.id(ActSetuidSearch), Act: ActSetuidSearch, Caller: caller, Why: why, At: now, State: HandDone, Said: said}
|
|
if err != nil {
|
|
a.State, a.Said = HandRefused, err.Error()
|
|
}
|
|
// A search started is bounded to one an hour and kept as an act of its own; a refused one, which any caller may
|
|
// cause, rolls with the judgements.
|
|
record := h.record
|
|
if err != nil {
|
|
record = h.roll
|
|
}
|
|
if rerr := record(a); rerr != nil {
|
|
return nil, rerr
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"node": h.Node, "said": said}, nil
|
|
}
|
|
|
|
// searchKey keeps the last search's start on this node, and libKey each library's own take ask: keys of their own, which
|
|
// no other act's key moves, kept for the bucket's age (ninety days, the node-engine's manifest).
|
|
const searchKey = "search.last"
|
|
|
|
func libKey(access string) string {
|
|
sum := sha256.Sum256([]byte(access))
|
|
return "lib." + hex.EncodeToString(sum[:12]) + ".asked"
|
|
}
|
|
|
|
// started is the last search's start as kept, and who asked it.
|
|
type started struct {
|
|
At time.Time `json:"at"`
|
|
By string `json:"by"`
|
|
}
|
|
|
|
// claimSearch refuses a search within SearchEvery of the last start, naming when the next may start, and otherwise
|
|
// claims this one by compare-and-set: a second claim at the same moment loses.
|
|
func (h *HandActs) claimSearch(now time.Time, caller string) error {
|
|
e, err := h.State.Get(searchKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var rev uint64
|
|
if e != nil {
|
|
var last started
|
|
if err := json.Unmarshal(e.Value, &last); err != nil {
|
|
return fmt.Errorf("the last search's start cannot be read, so none is started: %w", err)
|
|
}
|
|
if now.Sub(last.At) < SearchEvery {
|
|
return fmt.Errorf("a search was started at %s by %s, and one is started in any hour: the next may start "+
|
|
"at %s", last.At.Format(time.RFC3339), last.By, last.At.Add(SearchEvery).Format(time.RFC3339))
|
|
}
|
|
rev = e.Revision
|
|
}
|
|
claim := started{At: now, By: caller}
|
|
if rev == 0 {
|
|
_, err = h.State.Create(searchKey, claim)
|
|
} else {
|
|
_, err = h.State.Update(searchKey, claim, rev)
|
|
}
|
|
if errors.Is(err, stdio.ErrChanged) {
|
|
return errors.New("another search was started at this moment; no second is started")
|
|
}
|
|
return err
|
|
}
|
|
|
|
// ---- the asks (§3, §4) --------------------------------------------------------------------------------------------
|
|
|
|
func (h *HandActs) id(act string) string {
|
|
return act + "-" + h.now().UTC().Format("20060102t150405") + "-" + h.nonce()
|
|
}
|
|
|
|
// bound is the act an ask's yes binds: the verb, the node, every argument and what it will set (ADR 0563 §3).
|
|
func (h *HandActs) bound(a HandAct) asks.Act {
|
|
act := asks.Act{"verb": a.Act, "node": h.Node, "arg.subject": a.Subject, "arg.why": a.Why, "sets": a.Sets}
|
|
if a.Path != "" {
|
|
act["path"] = a.Path
|
|
}
|
|
return act
|
|
}
|
|
|
|
// AskHandOver is hand-over: judged, refused in words or asked of the operator. auto is the engine asking by itself.
|
|
func (h *HandActs) AskHandOver(caller, path, why string, auto bool) (any, error) {
|
|
if h.JudgeHandOver == nil || h.HandOver == nil {
|
|
return nil, errors.New("this engine takes no hand-over")
|
|
}
|
|
j, err := h.JudgeHandOver(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return h.ask(ActHandOver, caller, why, auto, j)
|
|
}
|
|
|
|
// AskAccess is access-act: the act take on an access, judged on the library's home, refused or asked of the operator.
|
|
func (h *HandActs) AskAccess(caller, act, access, why string, auto bool) (any, error) {
|
|
if h.JudgeAccess == nil || h.AccessAct == nil {
|
|
return nil, errors.New("this engine runs no act on an accessed folder")
|
|
}
|
|
if act != ActTake {
|
|
return nil, fmt.Errorf("the act asked is %q, and the one act on an access is %q", act, ActTake)
|
|
}
|
|
j, err := h.JudgeAccess(act, access)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return h.ask(ActAccess, caller, why, auto, j)
|
|
}
|
|
|
|
// AskedFor says whether an act on subject was ever asked by the engine itself: a library's take is asked by itself once
|
|
// (ADR 0563 §4), and again only by hand.
|
|
func (h *HandActs) AskedFor(act, subject string) bool {
|
|
if act == ActAccess {
|
|
e, err := h.State.Get(libKey(subject))
|
|
return err != nil || e != nil // unread is never a reason to ask again
|
|
}
|
|
acts, err := h.read()
|
|
if err != nil {
|
|
return true
|
|
}
|
|
for _, a := range acts {
|
|
if a.Act == act && a.Subject == subject && (a.Auto || a.State == HandAsked) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (h *HandActs) ask(act, caller, why string, auto bool, j Judged) (any, error) {
|
|
if h.Asker == nil {
|
|
return nil, errors.New("this engine reaches no operator to ask: nothing was asked and nothing changed")
|
|
}
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
now := h.now().UTC()
|
|
a := HandAct{ID: h.id(act), Act: act, Subject: j.Subject, Path: j.Path, Caller: caller, Why: why, At: now,
|
|
State: HandAsked, Sets: j.Sets, Disk: j.Disk, Expires: now.Add(asks.ApproveLasts - time.Minute), Auto: auto}
|
|
a.Ask = a.ID
|
|
// One ask open per path or access: its open key made by a create, which a second ask cannot make.
|
|
if _, err := h.State.Create(openKey(act, j.Subject), map[string]string{"ask": a.Ask}); errors.Is(err, stdio.ErrChanged) {
|
|
held := "another"
|
|
if e, _ := h.State.Get(openKey(act, j.Subject)); e != nil {
|
|
var o struct {
|
|
Ask string `json:"ask"`
|
|
}
|
|
if json.Unmarshal(e.Value, &o) == nil && o.Ask != "" {
|
|
held = o.Ask
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("%s of %s is asked already: the ask %s waits for the operator's answer", act, j.Subject, held)
|
|
} else if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := h.record(a); err != nil {
|
|
_ = h.State.Delete(openKey(act, j.Subject))
|
|
return nil, err
|
|
}
|
|
bind, err := asks.ActDigest(h.bound(a))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
headline, does, stays := "Hand a directory on "+h.Node+" to the mesh?", "The mesh gives it "+j.Sets+".",
|
|
"The directory stays as it was found."
|
|
if act == ActAccess {
|
|
headline, does, stays = "Keep the library "+strings.TrimPrefix(j.Subject, "library:")+" on "+h.Node+"?",
|
|
"The library's group, group write and setgid are set on everything under its root.",
|
|
"The library waits and nothing under its root changes."
|
|
}
|
|
if len([]rune(headline)) > asks.HeadlineLength {
|
|
headline = string([]rune(headline)[:asks.HeadlineLength])
|
|
}
|
|
asked := asks.Ask{ID: a.Ask, Headline: headline, Who: asks.Operator,
|
|
Explanation: j.Words + " Your yes sets " + j.Sets + "; nothing changes until you answer.",
|
|
Whole: j.Words + " (" + j.Path + ") Your yes sets " + j.Sets + "; nothing changes until you answer.",
|
|
Details: "Asked by " + caller + " on " + h.Node + orWhy(why),
|
|
Options: []asks.Option{{ID: "yes", Label: "Yes", Does: does, Level: asks.Approve, Binds: bind},
|
|
{ID: "no", Label: "No, leave it", Does: stays, Level: asks.Acknowledge}},
|
|
Expires: a.Expires, OnExpiry: stays, About: EngineSeat + "-" + h.Node + "-" + act}
|
|
if err := h.Asker.Ask(asked); err != nil {
|
|
_, _, _ = h.end(a.Ask, func(x *HandAct) { x.State, x.Said = HandRefused, "the operator could not be asked: "+err.Error() })
|
|
return nil, fmt.Errorf("the operator could not be asked, and nothing changed: %w", err)
|
|
}
|
|
// Once asked, a library's own take ask is kept's own take ask: once, in a key of its own (ADR 0563 §4).
|
|
if auto && act == ActAccess {
|
|
_, _ = h.State.Create(libKey(j.Subject), map[string]any{"ask": a.Ask, "at": now})
|
|
}
|
|
h.say(HandAsked, a)
|
|
return map[string]any{"asked": a.Ask, "node": h.Node, "says": "the operator is asked on their channel; only " +
|
|
"their yes changes anything. acts shows where it stands."}, nil
|
|
}
|
|
|
|
func orWhy(why string) string {
|
|
if why == "" {
|
|
return ""
|
|
}
|
|
return ": " + why
|
|
}
|
|
|
|
// Warranted is the act on the operator's answer, handed once by asker.Client.Take after it held the warrant to the
|
|
// ask: a no is recorded refused; a yes is held to the act the option bound and the disk as judged, judged again now,
|
|
// and only then performed and recorded done with the warrant.
|
|
func (h *HandActs) Warranted(o asks.Option, w asks.Warrant) error {
|
|
a, _, err := h.readOne(actKey(w.Ask))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if a == nil || a.State != HandAsked {
|
|
return fmt.Errorf("the warrant answers the ask %s, which is not open here: nothing is done", w.Ask)
|
|
}
|
|
refuse := func(why string) error {
|
|
ended, ok, err := h.end(w.Ask, func(x *HandAct) { x.State, x.Said = HandRefused, why; warrantOn(x, w) })
|
|
if err == nil && ok {
|
|
h.say(HandRefused, ended)
|
|
}
|
|
return err
|
|
}
|
|
if o.ID != "yes" {
|
|
return refuse("the operator said no")
|
|
}
|
|
var j Judged
|
|
switch a.Act {
|
|
case ActHandOver:
|
|
j, err = h.JudgeHandOver(a.Subject)
|
|
case ActAccess:
|
|
j, err = h.JudgeAccess(ActTake, a.Subject)
|
|
default:
|
|
err = fmt.Errorf("the act %s is not performed on a warrant", a.Act)
|
|
}
|
|
if err != nil {
|
|
return refuse("judged again on the operator's yes, it is refused: " + err.Error())
|
|
}
|
|
now := *a
|
|
now.Sets, now.Path = j.Sets, j.Path
|
|
if err := o.Performs(h.bound(now)); err != nil {
|
|
return refuse("what the yes bound is not what would be done now: " + err.Error())
|
|
}
|
|
if j.Disk != a.Disk {
|
|
return refuse("the disk changed since the operator was asked (" + a.Disk + ", now " + j.Disk + "): ask again")
|
|
}
|
|
// Recorded with the way it came (ADR 0563 §5): the operator's yes to this engine's ask, not a terminal's line.
|
|
by := operatorOf(w) + ", to " + EngineSeat + "'s ask " + w.Ask
|
|
var said string
|
|
switch a.Act {
|
|
case ActHandOver:
|
|
said, err = h.HandOver(a.Subject, by)
|
|
case ActAccess:
|
|
said, err = h.AccessAct(AccessAct{Act: ActTake, Access: a.Subject, Path: j.Path, Warrant: w.Ask, By: by})
|
|
}
|
|
if err != nil {
|
|
return refuse(err.Error())
|
|
}
|
|
ended, ok, err := h.end(w.Ask, func(x *HandAct) { x.State, x.Said = HandDone, said; warrantOn(x, w) })
|
|
if err == nil && ok {
|
|
h.say(HandDone, ended)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func warrantOn(a *HandAct, w asks.Warrant) {
|
|
a.Proofs = w.Proofs
|
|
if w.By != nil {
|
|
a.By, a.Via = w.By.Who, w.By.Kind
|
|
if w.By.Verified != "" {
|
|
a.Via += " (" + w.By.Verified + ")"
|
|
}
|
|
}
|
|
}
|
|
|
|
func operatorOf(w asks.Warrant) string {
|
|
if w.By == nil {
|
|
return "the operator"
|
|
}
|
|
return "the " + w.By.Who + " via " + w.By.Kind
|
|
}
|
|
|
|
// Heard is a warrant heard on the engine's decided subject: taken once (asker.Client.Take), and an ask that ended with
|
|
// no choice recorded refused.
|
|
func (h *HandActs) Heard(w asks.Warrant) {
|
|
if h.Asker == nil {
|
|
return
|
|
}
|
|
// A warrant the ask kept here does not hold to (another digest, another option, too late) ends nothing: it is no
|
|
// answer to the ask, which stays open for the operator's.
|
|
if handled, _ := h.Asker.Take(w, h.Warranted); handled == asker.Recorded {
|
|
why := "the ask ended " + string(w.Outcome)
|
|
if w.Words != "" {
|
|
why += ": " + w.Words
|
|
}
|
|
if ended, ok, _ := h.end(w.Ask, func(x *HandAct) { x.State, x.Said = HandRefused, why; warrantOn(x, w) }); ok {
|
|
h.say(HandRefused, ended)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reconcile catches up what the engine missed while it was away: a warrant the router holds for an open ask is taken,
|
|
// and an ask past its expiry and a margin is closed refused, expired.
|
|
func (h *HandActs) Reconcile() {
|
|
if h.Asker == nil {
|
|
return
|
|
}
|
|
acts, err := h.read()
|
|
if err != nil {
|
|
return
|
|
}
|
|
for _, a := range acts {
|
|
if a.State != HandAsked || a.Ask == "" {
|
|
continue
|
|
}
|
|
if _, w, err := h.Asker.Where(a.Ask); err == nil && w != nil {
|
|
h.Heard(*w)
|
|
continue
|
|
}
|
|
if !a.Expires.IsZero() && h.now().After(a.Expires.Add(askMargin)) {
|
|
if ended, ok, _ := h.end(a.Ask, func(x *HandAct) { x.State, x.Said = HandRefused, "the ask expired unanswered" }); ok {
|
|
h.say(HandRefused, ended)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MemState is an ActStore in memory: an engine's acts while no bus is reached in a test.
|
|
type MemState struct {
|
|
mu sync.Mutex
|
|
keys map[string]stdio.StateEntry
|
|
top uint64
|
|
}
|
|
|
|
func (m *MemState) Get(key string) (*stdio.StateEntry, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if e, ok := m.keys[key]; ok {
|
|
return &e, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (m *MemState) Create(key string, value any) (uint64, error) { return m.put(key, value, 0, true) }
|
|
|
|
func (m *MemState) Update(key string, value any, revision uint64) (uint64, error) {
|
|
return m.put(key, value, revision, false)
|
|
}
|
|
|
|
func (m *MemState) put(key string, value any, revision uint64, create bool) (uint64, error) {
|
|
raw, err := json.Marshal(value)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
was, kept := m.keys[key]
|
|
if (create && kept) || (!create && (!kept || was.Revision != revision)) {
|
|
return 0, stdio.ErrChanged
|
|
}
|
|
if m.keys == nil {
|
|
m.keys = map[string]stdio.StateEntry{}
|
|
}
|
|
m.top++
|
|
m.keys[key] = stdio.StateEntry{Key: key, Value: raw, Revision: m.top}
|
|
return m.top, nil
|
|
}
|
|
|
|
func (m *MemState) Delete(key string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
delete(m.keys, key)
|
|
return nil
|
|
}
|
|
|
|
func (m *MemState) Keys(prefix string) ([]string, error) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
var out []string
|
|
for k := range m.keys {
|
|
if strings.HasPrefix(k, prefix) {
|
|
out = append(out, k)
|
|
}
|
|
}
|
|
sort.Strings(out)
|
|
return out, nil
|
|
}
|