The last setuid search's start and each library's own take ask are keys of their own, which no number of other acts moves (issue 774, ADR 0563 §2, §4; rev-l3 round 1)

This commit is contained in:
2026-10-28 20:35:33 +01:00
parent a5d1f7dde4
commit 2a0c428ef9
3 changed files with 86 additions and 10 deletions
+58 -10
View File
@@ -329,17 +329,11 @@ func (h *HandActs) Search(caller, why string) (any, error) {
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")
}
acts, err := h.read()
if err != nil {
// 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
}
for i := len(acts) - 1; i >= 0; i-- {
a := acts[i]
if a.Act == ActSetuidSearch && a.State == HandDone && now.Sub(a.At) < SearchEvery {
return nil, fmt.Errorf("a search was started at %s by %s, and one is started in any hour: the next may start "+
"at %s", a.At.Format(time.RFC3339), a.Caller, a.At.Add(SearchEvery).Format(time.RFC3339))
}
}
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 {
@@ -354,6 +348,52 @@ func (h *HandActs) Search(caller, why string) (any, error) {
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 {
@@ -399,9 +439,13 @@ func (h *HandActs) AskAccess(caller, act, access, why string, auto bool) (any, e
// 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 // unread is never a reason to ask again
return true
}
for _, a := range acts {
if a.Act == act && a.Subject == subject && (a.Auto || a.State == HandAsked) {
@@ -465,6 +509,10 @@ func (h *HandActs) ask(act, caller, why string, auto bool, j Judged) (any, error
_, _, _ = 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
+19
View File
@@ -86,5 +86,24 @@ func TestASetuidSearchAskIsBelievedOnlySignedForThisNodeAndOnce(t *testing.T) {
if got := acts.(map[string]any)["acts"].([]HandAct); len(got) != 1 || got[0].Caller != "ace.node-tools" || got[0].State != HandDone {
t.Errorf("the last search is recorded as %+v", got)
}
// The bound is kept in a key of its own (rev-l3 round 1): two hundred judgements before a search, or the acts
// gone from the record, never let a second search start within the hour.
h.JudgeDirectories = func(string) (string, error) { return "judged", nil }
for range 200 {
if _, err := h.Call(EngineCall{Verb: ActJudgeDirectories, Caller: "ace.node-tools"}); err != nil {
t.Fatal(err)
}
}
at = now.Add(time.Hour + 2*time.Minute)
if _, err := h.Call(EngineCall{Verb: ActSetuidSearch, Caller: "ace.node-tools"}); err == nil || !strings.Contains(err.Error(), "the next may start") {
t.Errorf("a search after two hundred judgements: %v", err)
}
keys, _ := h.State.Keys("act.")
for _, k := range keys {
_ = h.State.Delete(k)
}
if _, err := h.Call(EngineCall{Verb: ActSetuidSearch, Caller: "ace.node-tools"}); err == nil || !strings.Contains(err.Error(), "the next may start") || started != 2 {
t.Errorf("a search with the acts gone: %v, %d started", err, started)
}
})
}