A3h item 2b: NewGateChannel is the gate's operator channel over a process connection

It asks and cancels as accepted kinds of the seat operator-channel through the connection's Accept, and reads the router's
record by a get in the seat holder's state asks, never through a bucket named in code.

Builds: design 47 G §677, §700; lead answers A-272, A-273

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 10:23:22 +01:00
parent 1d3f24250c
commit 886be5cca7
2 changed files with 122 additions and 0 deletions
@@ -0,0 +1,51 @@
package operatorchannelclient
import (
"fmt"
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/process"
)
// asksRead is a state of another module read by key: process.State is one.
type asksRead interface {
Get(key string) (*stdio.StateEntry, error)
}
// NewGateChannel is the operator channel as a process holder's gate reaches it over its own connection (design 47 G §677,
// §700; lead answers A-272, A-273): an ask and its cancel are sent on the seat operator-channel's accept subjects through
// conn's Accept, and the router's record is read by a get of the key `<module>.<id>` in the seat's state asks, whose bucket
// is formed from holder (the module the seat list names for the seat operator-channel) and the contract's StateAsks, on the
// process's own account. state keeps the asks, under keyPrefix (the gate's state approvals and its node).
func NewGateChannel(conn *process.Conn, state Store, keyPrefix string, holder func() (string, error)) *GateChannel {
return gateChannelOver(conn.Accept, func(read string) asksRead { return conn.Read(read) }, state, keyPrefix, holder)
}
// gateChannelOver is NewGateChannel over the two things it reaches a connection through.
func gateChannelOver(accept func(seat, kind string, body any, opts ...stdio.EmitOptions) error, read func(read string) asksRead,
state Store, keyPrefix string, holder func() (string, error)) *GateChannel {
return &GateChannel{State: state, KeyPrefix: keyPrefix,
Publish: func(module, subject string, body any, id string) error {
var kind string
switch subject {
case seat.AcceptSubjectAsk(module):
kind = seat.AcceptAsk
case seat.AcceptSubjectCancel(module):
kind = seat.AcceptCancel
default:
return fmt.Errorf("%s is no accept subject of the seat %s that %s may use", subject, seat.Seat, module)
}
return accept(seat.Seat, kind, body, stdio.EmitOptions{MessageID: id})
},
ReadRecord: func(module, id string) (*stdio.StateEntry, error) {
held, err := holder()
if err != nil {
return nil, fmt.Errorf("the router's record of %s cannot be read: %w", id, err)
}
if held == "" {
return nil, fmt.Errorf("the router's record of %s cannot be read: the seat list names no holder of the seat %s", id, seat.Seat)
}
return read(held + "." + seat.StateAsks).Get(seat.AsksByAskerIDKey(module, id))
}}
}
@@ -0,0 +1,71 @@
package operatorchannelclient
import (
"encoding/json"
"errors"
"testing"
"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"
)
type readOf struct {
state string
vals map[string]json.RawMessage
}
func (r readOf) Get(key string) (*stdio.StateEntry, error) {
if v, ok := r.vals[r.state+"|"+key]; ok {
return &stdio.StateEntry{Key: key, Value: v}, nil
}
return nil, nil
}
// The gate's channel over a process connection (design 47 G lead answers A-272, A-273): an ask and its cancel go out as
// accepted kinds of the seat operator-channel with the ask's id as the message id, the router's record is the get of
// `<module>.<id>` in the state asks of the seat's holder, and nothing else is reached: another subject, or a seat with no
// holder, is refused.
func TestTheGateChannelOverAConnectionAsksAndReadsOnlyThroughTheSeat(t *testing.T) {
type sent struct{ seat, kind, id string }
var out []sent
accept := func(s, kind string, _ any, opts ...stdio.EmitOptions) error {
out = append(out, sent{s, kind, opts[0].MessageID})
return nil
}
raw, _ := json.Marshal(seat.AsksByAskerIDValue{Asker: "shell", State: "open", Opened: FormatTime(now)})
vals := map[string]json.RawMessage{"messenger_holder.asks|shell.a1": raw, "other.asks|shell.a1": json.RawMessage(`{"state":"wrong"}`)}
read := func(r string) asksRead { return readOf{state: r, vals: vals} }
holder := func() (string, error) { return "messenger_holder", nil }
st := &mem{vals: map[string]json.RawMessage{}, revs: map[string]uint64{}}
g := gateChannelOver(accept, read, st, "n1.", holder)
g.Now = func() time.Time { return now }
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)
}
if err := g.Cancel("shell", "a1"); err != nil {
t.Fatal(err)
}
if len(out) != 2 || out[0] != (sent{seat.Seat, seat.AcceptAsk, "a1"}) || out[1] != (sent{seat.Seat, seat.AcceptCancel, "cancel.a1"}) {
t.Fatalf("sent %+v, want the ask and its cancel on the seat's accepts", out)
}
if rec, err := g.Record("shell", "a1"); err != nil || rec == nil || rec.State != "open" {
t.Fatalf("record %+v %v, want the one in the holder's state asks", rec, err)
}
if rec, err := g.Record("shell", "zz"); err != nil || rec != nil {
t.Fatalf("a record not there: %+v %v", rec, err)
}
if err := g.Publish("shell", "mesh.seat.operator-channel.accept.ask.other", nil, "x"); err == nil {
t.Fatal("an accept subject naming another caller was sent")
}
for _, c := range []func() (string, error){func() (string, error) { return "", nil }, func() (string, error) { return "", errors.New("unread") }} {
if _, err := gateChannelOver(accept, read, st, "n1.", c).Record("shell", "a1"); err == nil {
t.Fatal("a record was read with no holder of the seat")
}
}
}