messenger: name the ask's digest in every warrant, and bind the router's own asks to their act (hq ADR 0259 §6)

A warrant now carries the digest of the ask exactly as the router took it, so its asker acts only for
the ask the person was shown, with what each option binds. The router refuses an ask whose approving
option binds nothing (the SDK's Check). Its own unlink and relink asks bind the act they perform, and
it performs that act only on a warrant for that ask. mesh-sdk moves to d4077b4.
This commit is contained in:
2026-10-09 17:08:17 +02:00
parent a6965448f0
commit 8b00ffbfe9
5 changed files with 45 additions and 19 deletions
@@ -145,13 +145,22 @@ func stand(h *Holder, c *clock) {
h.Bench.Stand("desktop", Standing{Ready: true, At: c.now()})
}
// bound is what an option of the held delivery's ask binds: the asker's act, digested.
func bound(verb string) string {
d, err := asks.ActDigest(map[string]string{"verb": "mesh-delivery." + verb, "id": "novox/hq@0555"})
if err != nil {
panic(err)
}
return d
}
func heldAsk(c *clock, id string) asks.Ask {
return asks.Ask{ID: id, Headline: "Delivery of hq held for 36 hours",
Explanation: "Needs you: release it, or stop it. A delivery of hq has been held for 36 hours.",
Who: asks.Operator, Expires: c.now().Add(2 * time.Hour), OnExpiry: "the delivery stays held",
Options: []asks.Option{
{ID: "release", Label: "Release", Does: "the delivery goes on", Level: asks.Approve},
{ID: "stop", Label: "Stop", Does: "the delivery ends", Level: asks.Approve},
{ID: "release", Label: "Release", Does: "the delivery goes on", Level: asks.Approve, Binds: bound("release")},
{ID: "stop", Label: "Stop", Does: "the delivery ends", Level: asks.Approve, Binds: bound("stop")},
{ID: "silence", Label: "Silence for a week", Does: "nothing more is said of it for a week", Level: asks.Acknowledge},
}}
}
@@ -239,7 +248,8 @@ func TestWithoutALinkedAccountOnlyTheMeshMCPServerApproves(t *testing.T) {
func TestAnApproveFromTheLinkedAccountIsAWarrantForTheAskerAlone(t *testing.T) {
h, tg, dt, c, store, w := newRouter(t)
link(h, store, c, "42", c.now().Add(-2*time.Hour))
submit(t, h, "mesh-delivery", heldAsk(c, "d-1"))
asked := heldAsk(c, "d-1")
submit(t, h, "mesh-delivery", asked)
settle(h, c)
release := ticketFor(tg.sends[0], "Release")
out, err := h.Chosen("telegram", choiceOf(release, "42", c.now()))
@@ -251,9 +261,16 @@ func TestAnApproveFromTheLinkedAccountIsAWarrantForTheAskerAlone(t *testing.T) {
got[0].Says() != "the operator, via telegram (user id verified), chose Release" || strings.Join(got[0].Proofs, ",") != "P1" {
t.Fatalf("the warrant: %+v", got)
}
if _, err := got[0].For("mesh-delivery", heldAsk(c, "d-1")); err != nil {
if _, err := got[0].For("mesh-delivery", asked); err != nil {
t.Errorf("the asker would not act on it: %v", err)
}
// It answers the ask as it was taken, and no other ask under that id (ADR 0259 §6).
if got[0].AskDigest != asked.Digest() {
t.Errorf("the warrant names the digest %s, not the ask's %s", got[0].AskDigest, asked.Digest())
}
if _, err := got[0].For("mesh-delivery", heldAsk(c, "d-1")); err == nil {
t.Error("a warrant was taken for an ask under the same id with another expiry")
}
if rec := store.asks["mesh-delivery.d-1"]; rec.State != string(asks.OutcomeChosen) || !rec.Told || !rec.Tickets[release].Used {
t.Errorf("the record: %+v", rec)
}
@@ -706,7 +723,8 @@ func TestBreakGlassIsAFileRootMade(t *testing.T) {
func TestNoApprovalWhileAnAgentCanBecomeRootThere(t *testing.T) {
h, tg, _, c, store, w := newRouter(t)
link(h, store, c, "42", c.now().Add(-2*time.Hour))
submit(t, h, "mesh-delivery", heldAsk(c, "d-1"))
asked := heldAsk(c, "d-1")
submit(t, h, "mesh-delivery", asked)
settle(h, c)
release := ticketFor(tg.sends[0], "Release")
h.mu.Lock()
+3 -1
View File
@@ -678,7 +678,9 @@ func (h *Holder) judge(kind string, c asks.Choice, key string) (string, error) {
return h.refuseChoice(kind, c, "it needs a code from the authenticator app, which the mesh does not take yet")
}
w := &asks.Warrant{Ask: rec.Ask.ID, Asker: rec.Asker, About: rec.Ask.About, Outcome: asks.OutcomeChosen,
Option: o.ID, Label: o.Label, Level: o.Level, By: &person, Channel: info.Module, Proofs: proofs, At: now}
Option: o.ID, Label: o.Label, Level: o.Level, By: &person, Channel: info.Module, Proofs: proofs, At: now,
// The warrant answers this ask as it was taken and shown, with what each option binds (ADR 0259 §6).
AskDigest: rec.Ask.Digest()}
if err := h.closeAsk(key, asks.OutcomeChosen, "", w, c.Ticket); err != nil {
if errors.Is(err, ErrChanged) {
return h.refuseChoice(kind, c, "the question was already answered")
@@ -44,6 +44,14 @@ type Unlinked struct {
// askSelf puts a question of the router's own to one account, on its kind, and keeps it like any ask.
func (h *Holder) askSelf(a asks.Ask, only Identity, act SelfAct) error {
// Its options bind the act, as any asker's do (ADR 0259 §6): the warrant then performs this act alone.
binds, err := asks.ActDigest(act)
if err != nil {
return err
}
for i := range a.Options {
a.Options[i].Binds = binds
}
key := askKey(SelfAsker, a.ID)
now := h.Now()
rec := AskRecord{Asker: SelfAsker, Ask: a, State: StateOpen, Opened: now, Tickets: map[string]Ticket{},
@@ -85,6 +93,14 @@ func (h *Holder) selfAct(rec AskRecord, w asks.Warrant) {
if rec.Act == nil || w.Outcome != asks.OutcomeChosen {
return
}
o, err := w.For(SelfAsker, rec.Ask)
if err == nil {
err = o.Performs(*rec.Act)
}
if err != nil {
h.Logf("[messenger] REFUSED a warrant for its own ask %s: %v", rec.Ask.ID, err)
return
}
switch rec.Act.Do {
case "unlink":
h.unlinkNow(rec.Act.Kind, "approved by the account itself")
+1 -1
View File
@@ -2,4 +2,4 @@ module messenger
go 1.22
require git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008163924-7c32dfbdf969
require git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261009081503-d4077b473ea8
+2 -12
View File
@@ -1,12 +1,2 @@
git.novox.be/novox/mesh-sdk/go v0.1.7 h1:C0sTQmtTiyYH7bnqZb7PusXnqA37gKuT7Nqjn9gG47w=
git.novox.be/novox/mesh-sdk/go v0.1.7/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008143736-3837f86eb6d4 h1:tB+O9kkF4cDw8By2DLQTZDRAjp5l3DltS52ypgOqLkM=
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008143736-3837f86eb6d4/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008144136-2f2840488818 h1:o0yf4yyH8VfTe9FE963vSGEeMR662OXf38puZzNF/ug=
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008144136-2f2840488818/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008145004-62367ce15ad6 h1:JT7xM1bnLNInW7/oImV2OlXTrcQ4/GSM0Y8tAb+AhmY=
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008145004-62367ce15ad6/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008162031-55090da7e08f h1:BNvyWq899GwP7F3sY4ACieB5a5fnFAq+sJ9lP6HQ5qI=
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008162031-55090da7e08f/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008163924-7c32dfbdf969 h1:qivAvJIYHFx3yNA+bt52yNRZkGioHLOw9dqs9zw4OJ8=
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261008163924-7c32dfbdf969/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261009081503-d4077b473ea8 h1:soqhLNpEXThdq6PdiPy6ExxjJ+yjhh1N1n9E3j1CtrM=
git.novox.be/novox/mesh-sdk/go v0.1.8-0.20261009081503-d4077b473ea8/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=