catalogue's compiled set is read from git.novox.be/novox/mesh-contracts/go/seats; the hand-written seat literal and its
verb builders go. The bar's block keeps its checks as code beside the data. A case of the seat set's test holds the
generated set equal, entry for entry, to the list compiled at 5a38a12 (testdata), but for the whole seats' additions
and the vault's two verbs; the three new registry seats are the only new entries. A holder of mesh-broker now serves its
eight verbs.
SDK-12: the vault's seat no longer promises accept and move-asks, which no holder served and nothing called; retire is
optional (design 32). The intake and move rows join one table; the move's own test goes with the two verbs.
Requires mesh-contracts (no version on the forge yet: a go.work, uncommitted, uses the sibling worktree until the
group pins it).
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
144 lines
5.2 KiB
Go
144 lines
5.2 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
)
|
|
|
|
// A Verb is one tool a role serves: its name, what it does, and the schema of its arguments and of
|
|
// its answer (novox/hq ADR 0132, design 33 §2).
|
|
//
|
|
// **A name alone is not callable by something that has never seen the mesh before**, which is the
|
|
// whole population a tool surface exists for. So a seat's protocol carries the definition, in the
|
|
// form an agent protocol already uses — a JSON schema for the input — so nothing translates between
|
|
// a seat's idea of an argument and the caller's.
|
|
//
|
|
// A manifest may still write a bare verb name (`"serves": ["price"]`); that is a Verb with only a
|
|
// name, and the module's runtime answers `tools` with the rest. The two forms read into one type so
|
|
// nothing downstream cares which was written.
|
|
type Verb struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description,omitempty"`
|
|
Input map[string]any `json:"input,omitempty"`
|
|
Output map[string]any `json:"output,omitempty"`
|
|
// Optional marks a verb added to a mesh seat whose holder lives in another repository (design 33 §7,
|
|
// additive within a version): a holder that serves it is accepted, and one that does not yet still
|
|
// holds the seat. Without it the addition would be a deadlock — this controller refusing the holder that
|
|
// does not serve the verb, the controller before it refusing the holder that does — and every check of
|
|
// the catalogue between the two would fail on a module nobody touched. Once every holder serves it,
|
|
// the mark is removed and the verb is a condition of holding like the rest. Never stored or said: the
|
|
// seat set's protocol is the compiled one.
|
|
Optional bool `json:"-"`
|
|
// Replaces are the shell commands this verb is the mesh's way to do, each as it is typed —
|
|
// `journalctl`, `systemctl restart` — so the agent finds the verb by the command it would have run
|
|
// and is told to call it instead (novox/hq ADR 0245): the mesh MCP server's search matches them, the agent's
|
|
// instructions list them, and the guard on its shell names the verb when it refuses a work-around.
|
|
// A command line is matched by its words in order, the first being the command's name; `edit
|
|
// /etc/hosts` and `HOSTALIASES` name the guard's two local work-arounds for a mesh name.
|
|
Replaces []string `json:"replaces,omitempty"`
|
|
}
|
|
|
|
func (v *Verb) UnmarshalJSON(raw []byte) error {
|
|
trimmed := bytes.TrimSpace(raw)
|
|
if len(trimmed) > 0 && trimmed[0] == '"' {
|
|
var name string
|
|
if err := json.Unmarshal(trimmed, &name); err != nil {
|
|
return err
|
|
}
|
|
*v = Verb{Name: name}
|
|
return nil
|
|
}
|
|
// Strictly, like the manifest around it: a misspelt key in a tool's definition would otherwise
|
|
// describe a tool nobody can call and refuse nothing.
|
|
type plain Verb
|
|
var p plain
|
|
decoder := json.NewDecoder(bytes.NewReader(trimmed))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&p); err != nil {
|
|
return fmt.Errorf("a served verb is a name or {name, description, input, output}: %w", typedUnknown(err))
|
|
}
|
|
if p.Name == "" {
|
|
return fmt.Errorf("a served verb has no name: %s", trimmed)
|
|
}
|
|
*v = Verb(p)
|
|
return nil
|
|
}
|
|
|
|
// VerbNames are the names alone, for the grants and the checks that care about nothing else.
|
|
func VerbNames(verbs []Verb) []string {
|
|
out := make([]string, 0, len(verbs))
|
|
for _, v := range verbs {
|
|
out = append(out, v.Name)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func schema(properties map[string]string, required []string, switches ...string) map[string]any {
|
|
isSwitch := map[string]bool{}
|
|
for _, s := range switches {
|
|
if _, declared := properties[s]; !declared {
|
|
panic("a switch that is not a property: " + s)
|
|
}
|
|
isSwitch[s] = true
|
|
}
|
|
props := map[string]any{}
|
|
for name, description := range properties {
|
|
p := map[string]any{"type": "string", "description": description}
|
|
if isSwitch[name] {
|
|
p["enum"] = []string{"true", "false"}
|
|
}
|
|
props[name] = p
|
|
}
|
|
out := map[string]any{"type": "object", "properties": props}
|
|
if len(required) > 0 {
|
|
out["required"] = required
|
|
}
|
|
return out
|
|
}
|
|
|
|
// listed makes the named properties of a schema lists of text: a caller gives them as a JSON array (or, as
|
|
// any argument, one text separated by commas).
|
|
func listed(in map[string]any, names ...string) map[string]any {
|
|
props, _ := in["properties"].(map[string]any)
|
|
for _, n := range names {
|
|
p, _ := props[n].(map[string]any)
|
|
if p == nil {
|
|
panic("a list that is not a property: " + n)
|
|
}
|
|
props[n] = map[string]any{"type": "array", "items": map[string]any{"type": "string"},
|
|
"description": p["description"]}
|
|
}
|
|
return in
|
|
}
|
|
|
|
// unpromised is what a claim says it serves and the seat's protocol never promised.
|
|
func unpromised(serves []string, promised []Verb) []string {
|
|
has := map[string]bool{}
|
|
for _, v := range promised {
|
|
has[v.Name] = true
|
|
}
|
|
var extra []string
|
|
for _, s := range serves {
|
|
if !has[s] {
|
|
extra = append(extra, s)
|
|
}
|
|
}
|
|
return extra
|
|
}
|
|
|
|
// unservedVerbs is what a seat promises and a claimant's offer for it does not answer.
|
|
func unservedVerbs(tools []string, promised []Verb) []string {
|
|
has := map[string]bool{}
|
|
for _, t := range tools {
|
|
has[t] = true
|
|
}
|
|
var missing []string
|
|
for _, v := range promised {
|
|
if !has[v.Name] && !v.Optional {
|
|
missing = append(missing, v.Name)
|
|
}
|
|
}
|
|
return missing
|
|
}
|