A3e item 1: operatorchannelclient implements the gate's channel and re-exports its Kept

Builds: design 47 G §677, §700, lead answers A-219, A-224, A-228

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 06:25:56 +01:00
parent 33d9b28869
commit 1d3f24250c
4 changed files with 156 additions and 18 deletions
@@ -18,6 +18,7 @@ import (
"time"
seat "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-sdk/go/approval"
"git.novox.be/novox/mesh-sdk/go/operatorask"
)
@@ -178,7 +179,7 @@ const (
MostOpen = 3
ApproveLasts = 24 * time.Hour
DestroyLasts = 10 * time.Minute
HeadlineLength = 60
HeadlineLength = approval.HeadlineLength
LabelLength = 24
)
@@ -19,6 +19,7 @@ import (
seat "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/approval"
)
// Client asks as one module.
@@ -57,23 +58,10 @@ func (c *Client) now() time.Time {
return time.Now()
}
// Kept is one ask as the module keeps it.
//
// An asker that asks for a call keeps beside it the act the ask binds, the call's whole arguments, and whether the act's
// run was recorded in the hand-act log (design 46 7-approval-mark §13): Take keeps all three as it marks the ask.
type Kept struct {
Ask seat.Ask `json:"ask"`
Asked time.Time `json:"asked"`
Acted bool `json:"acted,omitempty"`
Ended string `json:"ended,omitempty"`
Answer *seat.Warrant `json:"answer,omitempty"`
// Act is the act the ask's options bind, as its asker digested it.
Act Act `json:"act,omitempty"`
// Arguments are the asked call's arguments whole.
Arguments json.RawMessage `json:"arguments,omitempty"`
// Recorded says the act's run is in the hand-act log (`mesh-hand-acts.warranted` answered).
Recorded bool `json:"recorded,omitempty"`
}
// Kept is one ask as the module keeps it: mesh-sdk's go/approval's form (design 47 G lead answer A-228) over the generated
// contract's ask and warrant. An asker that asks for a call keeps beside it the act the ask binds, the call's whole
// arguments, and whether the act's run was recorded in the hand-act log (design 46 7-approval-mark §13).
type Kept = approval.Kept[seat.Ask, seat.Warrant]
func (c *Client) key(id string) string { return c.KeyPrefix + "ask." + id }
@@ -8,6 +8,7 @@ import (
seat "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/approval"
"git.novox.be/novox/mesh-sdk/go/process"
)
@@ -200,3 +201,61 @@ func TestWhereReadsTheRecordsWarrantTyped(t *testing.T) {
t.Fatalf("Where = %q, %+v, %v; want chosen and the record's warrant", state, w, err)
}
}
// The gate's channel (design 47 G lead answers A-219, A-228): the gate's ask raised to the operator at the level approve,
// the router's record read in the gate's form with its warrant whole, and a warrant claimed once, only for the act its
// option binds.
func TestTheGateChannelTranslatesTheGatesForms(t *testing.T) {
st := &mem{vals: map[string]json.RawMessage{}, revs: map[string]uint64{}}
records := map[string]seat.AsksByAskerIDValue{}
g := &GateChannel{State: st, KeyPrefix: "n1.", Now: func() time.Time { return now },
Publish: func(module, subject string, body any, id string) error {
if subject != seat.AcceptSubjectAsk(module) {
t.Fatalf("published on %s", subject)
}
return nil
},
ReadRecord: func(module, id string) (*stdio.StateEntry, error) {
rec, ok := records[module+"."+id]
if !ok {
return nil, nil
}
raw, _ := json.Marshal(rec)
return &stdio.StateEntry{Value: raw}, nil
}}
digest, _ := ActDigest(releaseHQ)
a := approval.Ask{ID: "a1", About: digest, Headline: "Run wipe?", Explanation: "Wipes.", Expires: FormatTime(now.Add(time.Hour)),
OnExpiry: "nothing runs", Options: []approval.Option{{ID: "approve", Label: "Approve", Does: "runs", Binds: digest}}}
if err := g.Ask("shell", a); err != nil {
t.Fatal(err)
}
var k Kept
if e, _ := st.Get("n1.ask.a1"); e == nil || json.Unmarshal(e.Value, &k) != nil || k.Ask.Who != Operator || k.Ask.Options[0].Level != Approve {
t.Fatalf("kept %+v", k)
}
if rec, err := g.Record("shell", "a1"); rec != nil || err != nil {
t.Fatalf("a record not yet there: %+v %v", rec, err)
}
w := seat.Warrant{Ask: "a1", Asker: "shell", Outcome: OutcomeChosen, Option: "approve", Level: Approve, At: FormatTime(now),
AskDigest: Digest(k.Ask), Channel: "telegram", By: &seat.Person{Who: Operator, Kind: "telegram"}}
records["shell.a1"] = seat.AsksByAskerIDValue{Asker: "shell", State: "chosen", Opened: FormatTime(now), Warrant: &w}
rec, err := g.Record("shell", "a1")
if err != nil || rec == nil || rec.State != "chosen" || rec.Warrant == nil || rec.Warrant.Channel != "telegram" || rec.Warrant.Option != "approve" {
t.Fatalf("record %+v %v", rec, err)
}
if h, err := g.Claim("shell", *rec.Warrant, map[string]string{"verb": "other"}, func() { t.Fatal("ran another act") }); h != approval.Claimed || err == nil {
t.Fatalf("another act: %s %v", h, err)
}
delete(st.vals, "n1.ask.a1")
_ = g.Ask("shell", a)
runs := 0
for i := 0; i < 2; i++ {
h, err := g.Claim("shell", *rec.Warrant, releaseHQ, func() { runs++ })
if want := []approval.Claim{approval.Claimed, approval.ClaimDone}[i]; h != want || err != nil {
t.Fatalf("claim %d: %s %v", i, h, err)
}
}
if runs != 1 || g.Decided("shell") != seat.SubjectDecided("shell") {
t.Fatalf("runs %d", runs)
}
}
@@ -0,0 +1,90 @@
package operatorchannelclient
import (
"encoding/json"
"fmt"
"time"
seat "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/approval"
"git.novox.be/novox/mesh-sdk/go/operatorask"
)
// GateChannel is the operator channel as mesh-sdk's approval gate reaches it (design 47 G lead answers A-219, A-228): it
// translates the gate's own forms to and from the generated contract's, asking under the marked module's name through a
// Client whose asks are kept in the gate's state under KeyPrefix.
type GateChannel struct {
// State is the gate's state approvals; KeyPrefix is put before every key the client keeps there.
State Store
KeyPrefix string
Now func() time.Time
// Publish says an accept on the seat under module's name; ReadRecord reads the router's record `<module>.<id>` of the
// seat's state asks, both on the gate's holder's own account (design 47 G §700).
Publish func(module, subject string, body any, id string) error
ReadRecord func(module, id string) (*stdio.StateEntry, error)
}
var _ approval.Channel = (*GateChannel)(nil)
func (g *GateChannel) client(module string) *Client {
return &Client{Module: module, State: g.State, KeyPrefix: g.KeyPrefix, Now: g.Now,
Publish: func(subject string, body any, id string) error { return g.Publish(module, subject, body, id) },
Record: func(id string) (*stdio.StateEntry, error) { return g.ReadRecord(module, id) }}
}
// Ask raises the gate's ask to the operator, every option at the level approve.
func (g *GateChannel) Ask(module string, a approval.Ask) error {
ask := seat.Ask{ID: a.ID, About: a.About, Who: Operator, Headline: a.Headline, Explanation: a.Explanation,
Expires: a.Expires, OnExpiry: a.OnExpiry}
for _, o := range a.Options {
ask.Options = append(ask.Options, seat.Option{ID: o.ID, Label: o.Label, Does: o.Does, Level: Approve, Binds: o.Binds})
}
return g.client(module).Ask(ask)
}
// Record is the router's record of module's ask in the gate's form, nil while there is none.
func (g *GateChannel) Record(module, id string) (*approval.Record, error) {
e, err := g.ReadRecord(module, id)
if err != nil || e == nil {
return nil, err
}
var rec seat.AsksByAskerIDValue
if err := json.Unmarshal(e.Value, &rec); err != nil {
return nil, fmt.Errorf("the router's record of %s cannot be read: %w", id, err)
}
out := &approval.Record{State: rec.State, Answer: rec.Answer, Opened: rec.Opened, Expires: rec.Expires, OfferedOn: rec.OfferedOn}
if w := rec.Warrant; w != nil {
whole, err := json.Marshal(w)
if err != nil {
return nil, err
}
out.Warrant = &approval.Warrant{Ask: w.Ask, Asker: w.Asker, Option: w.Option, Channel: w.Channel, At: w.At, Whole: whole}
}
return out, nil
}
// Claim takes the warrant once with Take, holding the chosen option to act before run.
func (g *GateChannel) Claim(module string, w approval.Warrant, act operatorask.Act, run func()) (approval.Claim, error) {
var sw seat.Warrant
if err := json.Unmarshal(w.Whole, &sw); err != nil {
return approval.ClaimRefused, fmt.Errorf("the warrant for the ask %s cannot be read: %w", w.Ask, err)
}
h, err := g.client(module).Take(sw, func(o seat.Option, _ seat.Warrant) error {
if err := Performs(o, act); err != nil {
return err
}
run()
return nil
})
return approval.Claim(h), err
}
// Cancel takes module's ask back.
func (g *GateChannel) Cancel(module, id string) error { return g.client(module).Cancel(id) }
// Decided is the subject of the router's `decided` for module.
func (g *GateChannel) Decided(module string) string { return seat.SubjectDecided(module) }
// Seat is the seat the gate's asks are raised on.
func (g *GateChannel) Seat() string { return seat.Seat }