The controller ended by its unassignment (ADR 0590), so its seat, the grants seat's lease write and hand-back and placement's take-over from it serve nothing. The dependence check becomes strict (clause 5): a read of the lease, a claim, a require or the module itself is refused like any other declaration. held and artifacts-gone are the artifact sweep seat's (ADR 0571 §2). Issue 1126 B1, B2. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
161 lines
6.2 KiB
Go
161 lines
6.2 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"maps"
|
|
"slices"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// schema is a JSON schema for an object of string properties, which is every argument the verbs
|
|
// above take. Kept small on purpose: a schema an agent cannot read is a tool it cannot call. The
|
|
// switches are the properties that are "true" or "false" and nothing else, said in the schema as an
|
|
// enum so a caller sees it and the verb can refuse any other word rather than read it as false.
|
|
//
|
|
// **The schema is the whole of what a verb takes** (novox/hq issue 244): an argument it does not
|
|
// name is refused when the verb is called, never passed over.
|
|
// withObject adds to a verb's input s the property name, an object of the string fields given, each required.
|
|
func withObject(s map[string]any, name, description string, fields map[string]string) map[string]any {
|
|
inner := schema(fields, slices.Sorted(maps.Keys(fields)))
|
|
inner["description"], inner["additionalProperties"] = description, false
|
|
s["properties"].(map[string]any)[name] = inner
|
|
return s
|
|
}
|
|
|
|
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
|
|
}
|