From 8b00ffbfe97c4bfe64ab2050953441e0c0a8e9cc Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 9 Oct 2026 10:17:15 +0200 Subject: [PATCH] =?UTF-8?q?messenger:=20name=20the=20ask's=20digest=20in?= =?UTF-8?q?=20every=20warrant,=20and=20bind=20the=20router's=20own=20asks?= =?UTF-8?q?=20to=20their=20act=20(hq=20ADR=200259=20=C2=A76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../messenger/cmd/messenger/answers_test.go | 28 +++++++++++++++---- modules/messenger/cmd/messenger/asks.go | 4 ++- modules/messenger/cmd/messenger/selfact.go | 16 +++++++++++ modules/messenger/go.mod | 2 +- modules/messenger/go.sum | 14 ++-------- 5 files changed, 45 insertions(+), 19 deletions(-) diff --git a/modules/messenger/cmd/messenger/answers_test.go b/modules/messenger/cmd/messenger/answers_test.go index 156abef0..d3ff82ca 100644 --- a/modules/messenger/cmd/messenger/answers_test.go +++ b/modules/messenger/cmd/messenger/answers_test.go @@ -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() diff --git a/modules/messenger/cmd/messenger/asks.go b/modules/messenger/cmd/messenger/asks.go index 3dd72ee8..a32e244d 100644 --- a/modules/messenger/cmd/messenger/asks.go +++ b/modules/messenger/cmd/messenger/asks.go @@ -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") diff --git a/modules/messenger/cmd/messenger/selfact.go b/modules/messenger/cmd/messenger/selfact.go index c631e62c..bfe02111 100644 --- a/modules/messenger/cmd/messenger/selfact.go +++ b/modules/messenger/cmd/messenger/selfact.go @@ -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") diff --git a/modules/messenger/go.mod b/modules/messenger/go.mod index fac061a6..1fb6f3e0 100644 --- a/modules/messenger/go.mod +++ b/modules/messenger/go.mod @@ -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 diff --git a/modules/messenger/go.sum b/modules/messenger/go.sum index fe8bb88f..6fecb888 100644 --- a/modules/messenger/go.sum +++ b/modules/messenger/go.sum @@ -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=