770 lines
26 KiB
Go
770 lines
26 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"
|
|
)
|
|
|
|
// 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
|
|
// 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 "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
|
|
}
|