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=