Files
mesh-sdk/go/capabilities.go
T
jschoubben 5fa64be894 Revert the revert of G's mesh-sdk merge (#191): G's sdk stands again over reader release 3
Main reverted G's half merge (#189, reverted by 8afc3eb in #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 head f30dee54 only 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 at f30dee54)

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-13 12:49:56 +01:00

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)
})
}