A3g item 2a: a definition with a marked verb is read with gated-ended, approvals and the verb by caller
seatdef.Resolve, which the seat generator and every reader of a definition go through, makes each marked verb by-caller and adds the event gated-ended (gated-ended.<caller>.<call>, kept last per subject one day) and the verb approvals (class read); the seat files stay as written. approvals is the gate's verb, so CanHold asks no holder to name it. Builds: design 47 G §677, §697; lead answers A-264, A-265, A-266 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
+3
-2
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
"git.novox.be/novox/mesh-sdk/go/operatorask"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
)
|
||||
|
||||
// A marked verb a module's process serves is one call with a gate before its running (design 47 G §677): the holder keeps
|
||||
@@ -32,8 +33,8 @@ type Held struct {
|
||||
|
||||
// GatedEndedEvent is the seat's event a call's end is said on, kept last per subject for GatedEndedKeptFor (§677).
|
||||
const (
|
||||
GatedEndedEvent = "gated-ended"
|
||||
GatedEndedKeptFor = 24 * time.Hour
|
||||
GatedEndedEvent = seatdef.GatedEndedEvent
|
||||
GatedEndedKeptFor = seatdef.GatedEndedKeptDays * 24 * time.Hour
|
||||
)
|
||||
|
||||
// The outcomes of a held call (§677).
|
||||
|
||||
@@ -35,8 +35,9 @@ func TestTheSeatTableCarriesTheEnvironmentAndTheLoginShell(t *testing.T) {
|
||||
if !ok || shell.Scope != ScopeNode || shell.Decision != "novox/hq ADR 0204" {
|
||||
t.Fatalf("node-login-shell is not a node seat: %+v (defined %v)", shell, ok)
|
||||
}
|
||||
if len(shell.Serves) != 1 || shell.Serves[0].Name != "execute" || shell.Serves[0].Description == "" {
|
||||
t.Fatalf("the login shell serves %+v, not execute alone", shell.Serves)
|
||||
// A-264, A-265: its marked execute gains the verb approvals beside it.
|
||||
if len(shell.Serves) != 2 || shell.Serves[0].Name != "execute" || shell.Serves[0].Description == "" || shell.Serves[1].Name != "approvals" {
|
||||
t.Fatalf("the login shell serves %+v, not execute and approvals", shell.Serves)
|
||||
}
|
||||
props, _ := shell.Serves[0].Input["properties"].(map[string]any)
|
||||
required, _ := shell.Serves[0].Input["required"].([]string)
|
||||
|
||||
@@ -14,12 +14,14 @@ func TestTheLoginShellsExecuteIsOptionalSoAMachineMayWithholdIt(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatal("node-login-shell is not defined")
|
||||
}
|
||||
if len(seat.Serves) != 1 || seat.Serves[0].Name != "execute" {
|
||||
t.Fatalf("the login shell promises %+v, not execute alone", seat.Serves)
|
||||
// A-264, A-265: its marked execute gains the verb approvals beside it.
|
||||
if len(seat.Serves) != 2 || seat.Serves[0].Name != "execute" || seat.Serves[1].Name != "approvals" {
|
||||
t.Fatalf("the login shell promises %+v, not execute and approvals", seat.Serves)
|
||||
}
|
||||
if !seat.Serves[0].Optional {
|
||||
t.Fatal("execute is required, so a holder that withholds it on one machine could not hold the seat there")
|
||||
}
|
||||
// A-266: no holder names the gate's verb approvals, so zsh's claim holds as it is written.
|
||||
withholding := Manifest{Module: "zsh", Version: "1", Claims: []Claim{{Name: LoginShellSeat, Scope: ScopeNode}}}
|
||||
if err := CanHold(withholding, seat); err != nil {
|
||||
t.Fatalf("a holder serving no execute is refused: %v", err)
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
)
|
||||
|
||||
// A Verb is one tool a role serves: its name, what it does, and the schema of its arguments and of
|
||||
@@ -135,7 +137,9 @@ func unservedVerbs(tools []string, promised []Verb) []string {
|
||||
}
|
||||
var missing []string
|
||||
for _, v := range promised {
|
||||
if !has[v.Name] && !v.Optional {
|
||||
// The verb approvals is the gate's, served wherever a marked verb is (the seat server or the tool runner), so no
|
||||
// holder names it (design 47 G lead answer A-266).
|
||||
if !has[v.Name] && !v.Optional && v.Name != seatdef.ApprovalsVerb {
|
||||
missing = append(missing, v.Name)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -59,13 +59,13 @@ func (c *Conn) AnswerSeat(seat string, definition func() (seatdef.Definition, bo
|
||||
if len(verbs) == 0 {
|
||||
return nil, fmt.Errorf("AnswerSeat(%q) answers no verb", seat)
|
||||
}
|
||||
if o.gate != nil && verbs["approvals"] == nil {
|
||||
if o.gate != nil && verbs[seatdef.ApprovalsVerb] == nil {
|
||||
all := make(map[string]SeatHandler, len(verbs)+1)
|
||||
for v, h := range verbs {
|
||||
all[v] = h
|
||||
}
|
||||
gate := o.gate
|
||||
all["approvals"] = func(caller string, _ json.RawMessage) (any, error) { return gate.Approvals(caller) }
|
||||
all[seatdef.ApprovalsVerb] = func(caller string, _ json.RawMessage) (any, error) { return gate.Approvals(caller) }
|
||||
verbs = all
|
||||
}
|
||||
names := make([]string, 0, len(verbs))
|
||||
|
||||
+53
-2
@@ -881,7 +881,7 @@ func checkMessages(field string, ms []Message, whole bool) Problems {
|
||||
// or a name the part and the definition both define, is refused.
|
||||
func Resolve(d Definition, parts map[string]Part) (Definition, error) {
|
||||
if len(d.Includes) == 0 {
|
||||
return d, nil
|
||||
return gated(d), nil
|
||||
}
|
||||
out := d
|
||||
out.Includes = nil
|
||||
@@ -916,7 +916,58 @@ func Resolve(d Definition, parts map[string]Part) (Definition, error) {
|
||||
if len(ps) > 0 {
|
||||
return Definition{}, ps
|
||||
}
|
||||
return out, nil
|
||||
return gated(out), nil
|
||||
}
|
||||
|
||||
// What a definition with a marked verb gains (design 47 G §677, §697; lead answer A-264).
|
||||
const (
|
||||
// GatedEndedEvent is the event a process holder says a held call's end on, subject
|
||||
// `gated-ended.<caller>.<call>`, kept last per subject for GatedEndedKeptDays.
|
||||
GatedEndedEvent = "gated-ended"
|
||||
// GatedEndedKeptDays is how long gated-ended keeps each call's end.
|
||||
GatedEndedKeptDays = 1
|
||||
// ApprovalsVerb is the verb that reads a holder's waiting calls, `act.` keys and claims, served by the seat server.
|
||||
ApprovalsVerb = "approvals"
|
||||
)
|
||||
|
||||
// gated is d as every reader reads it (A-264): when a verb of it needs approval, each marked verb is by-caller, and it
|
||||
// gains the event gated-ended and the verb approvals (class read) where it does not name them itself; the seat's files
|
||||
// stay as written. A definition with no marked verb is d.
|
||||
func gated(d Definition) Definition {
|
||||
marked := false
|
||||
for _, v := range d.Verbs {
|
||||
marked = marked || v.NeedsApproval
|
||||
}
|
||||
if !marked {
|
||||
return d
|
||||
}
|
||||
verbs := make([]Verb, 0, len(d.Verbs)+1)
|
||||
for _, v := range d.Verbs {
|
||||
if v.NeedsApproval {
|
||||
v.ByCaller = true
|
||||
}
|
||||
verbs = append(verbs, v)
|
||||
}
|
||||
if !slices.Contains(VerbNames(verbs), ApprovalsVerb) {
|
||||
verbs = append(verbs, Verb{Name: ApprovalsVerb, Class: ClassRead, ByCaller: true,
|
||||
Description: "The holder's calls of its marked verbs: each waiting or ended call with its caller, ask and state, " +
|
||||
"the act keys and the claims; a call's arguments only to that call's caller.",
|
||||
Input: map[string]any{"type": "object"},
|
||||
Answer: map[string]any{"type": "object", "required": []any{"calls", "acts", "claims"}, "properties": map[string]any{
|
||||
"calls": map[string]any{"type": "array"}, "acts": map[string]any{"type": "array"}, "claims": map[string]any{"type": "array"}}}})
|
||||
}
|
||||
d.Verbs = verbs
|
||||
if !slices.Contains(MessageNames(d.Events), GatedEndedEvent) {
|
||||
days := GatedEndedKeptDays
|
||||
events := append([]Message{}, d.Events...)
|
||||
d.Events = append(events, Message{Name: GatedEndedEvent, Subject: GatedEndedEvent + ".<caller>.<call>",
|
||||
Keep: &Keep{MaxAgeDays: &days, LastPerSubject: true, Object: true},
|
||||
Body: map[string]any{"type": "object", "required": []any{"call", "ask", "act", "verb", "outcome"}, "properties": map[string]any{
|
||||
"call": map[string]any{"type": "string"}, "ask": map[string]any{"type": "string"}, "act": map[string]any{"type": "string"},
|
||||
"verb": map[string]any{"type": "string"}, "outcome": map[string]any{"enum": []any{"ran", "declined", "expired", "refused"}},
|
||||
"answer": map[string]any{}, "approval": map[string]any{"type": "string"}, "error": map[string]any{"type": "string"}}}})
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
// EventSubject is where the seat's event is published; by a caller, the caller's name follows.
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package seatdef
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -205,6 +207,55 @@ func TestADefinitionIsReadStrictlyNamingWhatIsWrong(t *testing.T) {
|
||||
t.Errorf("a type defined twice: %v", err)
|
||||
}
|
||||
|
||||
// A-264 (design 47 G §677, §697): a definition with a marked verb is read with that verb by-caller, the event
|
||||
// gated-ended and the verb approvals, through a part or not; its files stay as written, and one with none is as written.
|
||||
marked, err := Parse([]byte(strings.Replace(whole, `"name":"list",`, `"name":"list","needs-approval":true,`, 1)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if VerbNames(marked.Verbs)[0] != "list" || len(marked.Verbs) != 1 || marked.Verbs[0].ByCaller {
|
||||
t.Fatalf("parsed %+v", marked.Verbs)
|
||||
}
|
||||
for _, includes := range [][]string{nil, {"registry"}} {
|
||||
marked.Includes = includes
|
||||
got, err := Resolve(marked, map[string]Part{"registry": part})
|
||||
if err != nil {
|
||||
t.Fatalf("includes %v: %v", includes, err)
|
||||
}
|
||||
var list, approvals *Verb
|
||||
for i := range got.Verbs {
|
||||
switch got.Verbs[i].Name {
|
||||
case "list":
|
||||
list = &got.Verbs[i]
|
||||
case ApprovalsVerb:
|
||||
approvals = &got.Verbs[i]
|
||||
}
|
||||
}
|
||||
var ended *Message
|
||||
for i := range got.Events {
|
||||
if got.Events[i].Name == GatedEndedEvent {
|
||||
ended = &got.Events[i]
|
||||
}
|
||||
}
|
||||
if list == nil || !list.ByCaller || approvals == nil || approvals.Class != ClassRead || !approvals.ByCaller || ended == nil ||
|
||||
ended.SubjectPattern() != "gated-ended.<caller>.<call>" || !ended.KeptLastPerSubject() || ended.KeptAgeDays() != 1 {
|
||||
t.Errorf("includes %v: verbs %+v, events %+v", includes, got.Verbs, got.Events)
|
||||
}
|
||||
if ps := append(checkVerbs(got.Verbs, true), checkMessages("events", got.Events, true)...); len(ps) > 0 {
|
||||
t.Errorf("includes %v: what is added is refused: %v", includes, ps)
|
||||
}
|
||||
if again, _ := Resolve(got, nil); len(again.Verbs) != len(got.Verbs) || len(again.Events) != len(got.Events) {
|
||||
t.Errorf("resolved twice: %d verbs, %d events", len(again.Verbs), len(again.Events))
|
||||
}
|
||||
raw, _ := json.Marshal(got)
|
||||
if back, err := Parse(raw); err != nil || len(back.Events) != len(got.Events) {
|
||||
t.Errorf("includes %v: the resolved definition does not read back: %v", includes, err)
|
||||
}
|
||||
}
|
||||
if plain, _ := Resolve(d, map[string]Part{"registry": part}); slices.Contains(VerbNames(plain.Verbs), ApprovalsVerb) {
|
||||
t.Errorf("a definition with no marked verb gains %v", VerbNames(plain.Verbs))
|
||||
}
|
||||
|
||||
// Fixtures name what they are an instance of.
|
||||
if _, err := ParseFixtures([]byte(`[{"name":"a","of":"verb:list.input","given":{}},{"name":"b","of":"event:created",
|
||||
"subject":"mesh.seat.mesh-broker.event.created","given":{}}]`)); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user