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:
@@ -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 }
|
||||
Reference in New Issue
Block a user