Main reverted G's half merge (#189, reverted by8afc3ebin #191) and then took the reader release's S1-S5, which copied G's code. G's form holds where they differ (process_bus.go, seats.go, state.go and their tests); the release's kept names stay so main's callers keep building: SeatViewPrefix beside SeatReadPrefix, and QueueInvoke's empty strings on not-ok (with its test TestQueueInvoke). The tree differs from G's headf30dee54only by those and the release's two tests (objects cap option, seat states and stream kinds). Builds: design 47 G (the milestone's sdk part, as atf30dee54) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
88 lines
2.9 KiB
Go
88 lines
2.9 KiB
Go
package stdio
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"slices"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// The call a bundle reads its tool runner's capability words with, and the words of the families that sit beside
|
|
// `mesh/publish` and `mesh/subscribe` (design 47 G §576, §622). The words of the bus-state families are in buscalls.
|
|
const (
|
|
// CapabilitiesCall is answered with {words}: the capability words the tool runner states.
|
|
CapabilitiesCall = "mesh/capabilities"
|
|
// CapPutSessions is the put and get sessions' word (§457, §518), CapFindingCalls the acknowledged publish and the
|
|
// subscription for new messages the finding writer uses (§559). CapSeatSettleDelay (seat.go) is the third.
|
|
CapPutSessions = "put-sessions"
|
|
CapFindingCalls = "finding-calls"
|
|
)
|
|
|
|
// capabilityWords are the words the tool runner states, read once at the first call that needs one and kept for as long
|
|
// as the bundle runs: a bundle lives no longer than the tool runner that started it (design 47 G §576).
|
|
type capabilityWords struct {
|
|
mu sync.Mutex
|
|
read bool
|
|
words []string
|
|
}
|
|
|
|
var capabilities capabilityWords
|
|
|
|
// get answers the words, asking once with ask. A tool runner of before G answers `mesh/capabilities` as an unknown call,
|
|
// which reads as no word stated; any other failure is returned and asked again at the next call.
|
|
func (c *capabilityWords) get(ask func() (json.RawMessage, error)) ([]string, error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.read {
|
|
return c.words, nil
|
|
}
|
|
raw, err := ask()
|
|
switch {
|
|
case errors.Is(err, errors.ErrUnsupported):
|
|
c.words = nil
|
|
case err != nil:
|
|
return nil, fmt.Errorf("reading the tool runner's capability words: %w", err)
|
|
default:
|
|
var a struct {
|
|
Words []string `json:"words"`
|
|
}
|
|
if err := json.Unmarshal(raw, &a); err != nil {
|
|
return nil, fmt.Errorf("the tool runner's capability words: %w", err)
|
|
}
|
|
c.words = a.Words
|
|
}
|
|
c.read = true
|
|
return c.words, nil
|
|
}
|
|
|
|
// require is nil when word is among the stated ones, else an error naming the word and the node (design 47 G §488, §576).
|
|
func (c *capabilityWords) require(word string, ask func() (json.RawMessage, error)) error {
|
|
words, err := c.get(ask)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if slices.Contains(words, word) {
|
|
return nil
|
|
}
|
|
node := os.Getenv("MESH_NODE")
|
|
if node == "" {
|
|
node = "unknown"
|
|
}
|
|
return fmt.Errorf("the tool runner of node %s does not state the capability word %q: a call of its family is refused until it does", node, word)
|
|
}
|
|
|
|
// RequireCapability fails at once, naming the word and the node, when the tool runner this bundle runs under does not
|
|
// state word; it reads the words once (design 47 G §488, §576).
|
|
func RequireCapability(word string) error {
|
|
return capabilities.require(word, func() (json.RawMessage, error) {
|
|
c := awaitChannel()
|
|
if c == nil {
|
|
return nil, errors.New("a capability is read through the runtime that launched the bundle, and Serve has not run")
|
|
}
|
|
return c.ask(CapabilitiesCall, map[string]any{}, 30*time.Second)
|
|
})
|
|
}
|