Take only acknowledgements from a desk click, and make each answer a one-time ticket
mesh/merge-gate pass: builds dunst, messenger → g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery-group group fix/notifications-after-review ready: every member ready, and composed together they pass
mesh/delivery superseded: a newer head of the same pull request

Review found that any process of the operator's account can choose a dunst action,
so a click could release or stop a delivery in the operator's name; that tokens
lived for ever; that a warning's answers died in ten seconds; and that a restarted
notifier left dead buttons (hq ADR 0258).
This commit is contained in:
jochen
2026-10-08 14:38:24 +02:00
parent 541b24d96e
commit d83711f11e
19 changed files with 616 additions and 137 deletions
+19 -2
View File
@@ -39,10 +39,27 @@ const (
MostChoices = 4
// AnswerWait is how long an answer is waited for.
AnswerWait = 7 * 24 * time.Hour
// ChosenEvent is the event an answer is said as.
ChosenEvent = "action-chosen"
// ChosenEvent is the event an answer is said as; StartedEvent says this bundle started, so the answers
// an earlier one waited for are lost and the operator channel shows them again (novox/hq ADR 0258).
ChosenEvent = "action-chosen"
StartedEvent = "started"
)
// sayStarted says, once this bundle serves, that it started on this machine; retried while the runtime
// cannot take it yet, and said in the log when it never could.
func sayStarted() {
body := map[string]any{"machine": os.Getenv("MESH_NODE")}
var err error
for wait := time.Second; wait <= time.Minute; wait *= 2 {
time.Sleep(wait)
if err = emit(StartedEvent, body); err == nil {
return
}
}
fmt.Fprintf(os.Stderr, "[dunst] could not say it started (answers waited for before are lost until the "+
"operator channel shows them again): %v\n", err)
}
var tokenShape = regexp.MustCompile(`^[A-Za-z0-9_-]{1,64}$`)
// emit says an event as this module; a seam a test replaces.
+19 -1
View File
@@ -1,6 +1,7 @@
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
@@ -43,7 +44,7 @@ func TestANotificationsChosenAnswerIsSaidAsAnEvent(t *testing.T) {
t.Fatalf("%+v, %v, after %s: the id is answered before the answer", got, err, time.Since(start))
}
asked, _ := os.ReadFile(filepath.Join(bin, "log"))
if !strings.Contains(string(asked), "[--wait][--action=a1b2=Release][--action=c3d4=Stop][--]") {
if !strings.Contains(string(asked), "[--expire-time=0][--wait][--action=a1b2=Release][--action=c3d4=Stop][--]") {
t.Fatalf("notify-send was asked %s", asked)
}
deadline := time.Now().Add(3 * time.Second)
@@ -90,3 +91,20 @@ func TestActionsAreRefusedForWhatTheyCannotBe(t *testing.T) {
}
}
}
// **A bundle that started says so**, so the operator channel shows again the answers lost with the one
// before.
func TestABundleThatStartedSaysSo(t *testing.T) {
t.Setenv("MESH_NODE", "g14")
var said []string
before := emit
emit = func(event string, body any) error {
said = append(said, fmt.Sprintf("%s %v", event, body))
return nil
}
t.Cleanup(func() { emit = before })
sayStarted()
if len(said) != 1 || said[0] != "started map[machine:g14]" {
t.Fatalf("%v", said)
}
}
+5
View File
@@ -89,6 +89,11 @@ func Send(n Notification) (SendResult, error) {
if n.Icon != "" {
args = append(args, "--icon="+n.Icon)
}
if len(n.Actions) > 0 && n.ExpireMS < 0 {
// A notification that offers answers stays until it is dismissed: a warning's answers must not die
// with dunst's ten seconds (novox/hq ADR 0258).
n.ExpireMS = 0
}
if n.ExpireMS >= 0 {
args = append(args, "--expire-time="+strconv.Itoa(n.ExpireMS))
}
+1
View File
@@ -11,6 +11,7 @@ import (
)
func main() {
go sayStarted()
if err := stdio.Serve("", tools()); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)