A-7 tools: mesh_approvals reads and steers what was asked; mesh_describe says a mark and its source
The mesh MCP server gains mesh_approvals: with no argument the calls asked on the machine in the last day, newest first, with state and fingerprint, the marks the gate applies with their sources and the catalog revision, and the state's fill against its cap (every machine's keys together); an ask read whole with the act's digest; record asking the hand-act again, and cancel taking an open ask back, each recorded in the hand-act log first and refused when it cannot be. mesh_describe answers needs-approval with its source, and a marked call that reached no one names withheld and mesh-grants.grants_user_list. Builds: design 46 7-approval-mark §18, §6 (the mesh MCP server's withheld answer), §22 (eight tools) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
@@ -24,6 +24,7 @@ import (
|
||||
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -103,12 +104,13 @@ func discovery() []map[string]any {
|
||||
}), "description": "Ask the tool runner's four first views again, now, and say what each answers. A hand act, recorded in " +
|
||||
"the runner's log with its reason; for when a picture is doubted, or after an owner's state was made again."},
|
||||
waitTool(),
|
||||
approvalsTool(),
|
||||
}
|
||||
}
|
||||
|
||||
func isDiscovery(name string) bool {
|
||||
switch name {
|
||||
case verbOverview, verbMachine, verbSearch, verbDescribe, verbCall, verbRuntimes, verbFirstViews, verbFirstAgain, verbWait:
|
||||
case verbOverview, verbMachine, verbSearch, verbDescribe, verbCall, verbRuntimes, verbFirstViews, verbFirstAgain, verbWait, verbApprovals:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -949,6 +951,9 @@ func (s *Surface) discover(ctx context.Context, name string, args map[string]any
|
||||
if name == verbWait {
|
||||
return s.waitAnswer(ctx, args)
|
||||
}
|
||||
if name == verbApprovals {
|
||||
return s.approvalsAnswer(args)
|
||||
}
|
||||
x, err := s.index()
|
||||
if err != nil {
|
||||
return failure("the mesh's discovery failed: " + err.Error())
|
||||
@@ -1109,6 +1114,10 @@ func (s *Surface) discover(ctx context.Context, name string, args map[string]any
|
||||
if t.Stale != nil {
|
||||
described["found"] = t.Stale.say()
|
||||
}
|
||||
if ok, source := s.markedTool(t); ok {
|
||||
described["needs-approval"] = map[string]any{"source": source,
|
||||
"says": "the first call asks the operator and runs nothing; the same call again within 30 minutes of the Approve runs once"}
|
||||
}
|
||||
return answerText(described)
|
||||
|
||||
case verbCall:
|
||||
@@ -1129,6 +1138,11 @@ func (s *Surface) discover(ctx context.Context, name string, args map[string]any
|
||||
}
|
||||
if err != nil {
|
||||
why := whyItFailed(t.Address, err)
|
||||
if ok, _ := s.markedTool(t); ok && errors.Is(err, bus.ErrNoResponders) {
|
||||
// A marked subject withheld from a machine whose tool runner does not honour the mark (§6).
|
||||
why += "; it needs the operator's approval, so it may be withheld where the machine's tool runner does not state " +
|
||||
"approval-mark/1: mesh-grants.grants_user_list names it under withheld"
|
||||
}
|
||||
if t.Stale != nil {
|
||||
why += " (" + t.Stale.say() + ")"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package console
|
||||
|
||||
import (
|
||||
"log"
|
||||
"strings"
|
||||
|
||||
"github.com/novox/mesh-tools/node-tools/internal/approval"
|
||||
)
|
||||
|
||||
// verbApprovals is the mesh MCP server's tool that reads and steers what the gate asked on this machine (design 46
|
||||
// 7-approval-mark §18).
|
||||
const verbApprovals = "mesh_approvals"
|
||||
|
||||
// Approvals is the gate of this machine's tool runner, as mesh_approvals and mesh_describe read it.
|
||||
type Approvals interface {
|
||||
List() ([]approval.Listed, error)
|
||||
Read(id string) (approval.Whole, error)
|
||||
Applied() []approval.Marks
|
||||
Describe(module, seat, name string) (bool, string)
|
||||
Again(id string) error
|
||||
Cancel(id string) error
|
||||
// Fill is the state approvals' bytes against its cap, every machine's keys together.
|
||||
Fill() (bytes uint64, max int64, err error)
|
||||
}
|
||||
|
||||
func approvalsTool() map[string]any {
|
||||
str := func(desc string) map[string]any { return map[string]any{"type": "string", "description": desc} }
|
||||
return map[string]any{"name": verbApprovals, "inputSchema": map[string]any{"type": "object", "properties": map[string]any{
|
||||
"ask": str("an ask's id: its whole arguments and the act's digest"),
|
||||
"record": str("an ask's id: ask mesh-hand-acts.warranted again for its run, recorded in the hand-act log first"),
|
||||
"cancel": str("an ask's id: take that open ask back, recorded in the hand-act log first"),
|
||||
"why": str("why, for record and cancel, kept in the hand-act log"),
|
||||
}}, "description": "The calls that needed the operator's approval on this machine: with no argument, those asked in " +
|
||||
"the last day, newest first, with their state and fingerprint, the marks the tool runner applies (each module's " +
|
||||
"marked tools and verbs with their source and the catalog revision read) and its state's fill; `ask` reads one " +
|
||||
"whole, `record` asks its hand-act again, `cancel` takes an open ask back."}
|
||||
}
|
||||
|
||||
// approvalsAnswer is mesh_approvals.
|
||||
func (s *Surface) approvalsAnswer(args map[string]any) map[string]any {
|
||||
if s.Approvals == nil {
|
||||
return failure("this tool runner has no approval gate")
|
||||
}
|
||||
str := func(k string) string { v, _ := args[k].(string); return strings.TrimSpace(v) }
|
||||
why := str("why")
|
||||
if why == "" {
|
||||
why = "asked by hand with no reason given"
|
||||
}
|
||||
act := func(id, what, cause string, do func(string) error) map[string]any {
|
||||
// A hand act, recorded through the seat mesh-hand-acts before it is done (§18); one that cannot be recorded is refused.
|
||||
log.Printf("[mesh-tools] hand act: %s %s ask %s: %s", s.who, what, id, why)
|
||||
if err := s.recordAct(map[string]any{"what": what + " ask " + id, "why": why, "cause": cause, "verb": verbApprovals}); err != nil {
|
||||
return failure("the hand act was not recorded through mesh-hand-acts, so nothing was done: " + err.Error())
|
||||
}
|
||||
if err := do(id); err != nil {
|
||||
return failure(err.Error())
|
||||
}
|
||||
return answerText(map[string]any{"done": what + " ask " + id, "recorded": "mesh-hand-acts.hand-act"})
|
||||
}
|
||||
switch {
|
||||
case str("record") != "":
|
||||
return act(str("record"), "asked the hand-act of", "approval-record", s.Approvals.Again)
|
||||
case str("cancel") != "":
|
||||
return act(str("cancel"), "cancelled", "approval-cancel", s.Approvals.Cancel)
|
||||
case str("ask") != "":
|
||||
w, err := s.Approvals.Read(str("ask"))
|
||||
if err != nil {
|
||||
return failure(err.Error())
|
||||
}
|
||||
return answerText(w)
|
||||
}
|
||||
asks, err := s.Approvals.List()
|
||||
out := map[string]any{"asks": asks, "marks": s.Approvals.Applied()}
|
||||
if err != nil {
|
||||
out["asks"], out["asks_why"] = []approval.Listed{}, "the state approvals cannot be read: "+err.Error()
|
||||
}
|
||||
if b, max, err := s.Approvals.Fill(); err != nil {
|
||||
out["fill_why"] = err.Error()
|
||||
} else {
|
||||
out["fill"] = map[string]any{"bytes": b, "max_bytes": max, "of": "every machine's keys together"}
|
||||
}
|
||||
return answerText(out)
|
||||
}
|
||||
|
||||
// marked is what mesh_describe and a call that reached no one say of a marked tool or verb (§6, §18).
|
||||
func (s *Surface) marked(module, seat, name string) (bool, string) {
|
||||
if s.Approvals == nil {
|
||||
return false, ""
|
||||
}
|
||||
return s.Approvals.Describe(module, seat, name)
|
||||
}
|
||||
|
||||
// markedTool is marked for a resolved address.
|
||||
func (s *Surface) markedTool(t target) (bool, string) {
|
||||
if t.Seat {
|
||||
return s.marked("", t.Tool.Module, t.Tool.Name)
|
||||
}
|
||||
return s.marked(t.Tool.Module, "", t.Tool.Name)
|
||||
}
|
||||
@@ -63,6 +63,8 @@ type Surface struct {
|
||||
Flat bool
|
||||
// Record, when set, records a hand act in place of the seat mesh-hand-acts' verb (a test's).
|
||||
Record func(args map[string]any) error
|
||||
// Approvals is this machine's approval gate, read by mesh_approvals and mesh_describe; nil when the runner has none.
|
||||
Approvals Approvals
|
||||
}
|
||||
|
||||
// MethodSeats is the console's JSON-RPC method beside MCP's that answers every seat's verbs and holders, in the shape
|
||||
|
||||
@@ -3,6 +3,7 @@ package console
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"os"
|
||||
"reflect"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/novox/mesh-tools/node-tools/internal/approval"
|
||||
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
||||
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
|
||||
)
|
||||
@@ -194,7 +196,7 @@ func TestMeshWaitIsAnnouncedAmongSeven(t *testing.T) {
|
||||
}
|
||||
}
|
||||
sort.Strings(names)
|
||||
want := []string{"mesh_call", "mesh_describe", "mesh_first_view_again", "mesh_first_views", "mesh_machine", "mesh_overview", "mesh_runtimes", "mesh_search", "mesh_wait"}
|
||||
want := []string{"mesh_approvals", "mesh_call", "mesh_describe", "mesh_first_view_again", "mesh_first_views", "mesh_machine", "mesh_overview", "mesh_runtimes", "mesh_search", "mesh_wait"}
|
||||
if !reflect.DeepEqual(names, want) {
|
||||
t.Errorf("announced %v, want %v", names, want)
|
||||
}
|
||||
@@ -207,8 +209,43 @@ func TestMeshWaitIsAnnouncedAmongSeven(t *testing.T) {
|
||||
t.Errorf("mesh_wait's schema has no %s: %s", word, schema)
|
||||
}
|
||||
}
|
||||
// Design 46 7-approval-mark §18: mesh_approvals lists, and records a hand act before it cancels; refused without a record.
|
||||
gate := &fakeApprovals{}
|
||||
var recorded []string
|
||||
s := &Surface{who: "n1.node-tools", Approvals: gate, Record: func(a map[string]any) error {
|
||||
if a["why"] == "unrecordable" {
|
||||
return errors.New("mesh-hand-acts does not answer")
|
||||
}
|
||||
recorded = append(recorded, a["what"].(string))
|
||||
return nil
|
||||
}}
|
||||
if out := s.discover(context.Background(), verbApprovals, map[string]any{}); out["isError"] == true || !strings.Contains(fmtAnswer(out), "every machine") {
|
||||
t.Errorf("the list %v", out)
|
||||
}
|
||||
if out := s.discover(context.Background(), verbApprovals, map[string]any{"cancel": "a1", "why": "no longer needed"}); out["isError"] == true ||
|
||||
gate.cancelled != "a1" || len(recorded) != 1 || recorded[0] != "cancelled ask a1" {
|
||||
t.Errorf("a cancel %v, recorded %v", out, recorded)
|
||||
}
|
||||
if out := s.discover(context.Background(), verbApprovals, map[string]any{"cancel": "a2", "why": "unrecordable"}); out["isError"] != true || gate.cancelled != "a1" {
|
||||
t.Errorf("a cancel without a record %v", out)
|
||||
}
|
||||
}
|
||||
|
||||
func fmtAnswer(out map[string]any) string { raw, _ := json.Marshal(out); return string(raw) }
|
||||
|
||||
// fakeApprovals is a gate as mesh_approvals reads it.
|
||||
type fakeApprovals struct{ cancelled string }
|
||||
|
||||
func (f *fakeApprovals) List() ([]approval.Listed, error) {
|
||||
return []approval.Listed{{Ask: "a1", State: "asked"}}, nil
|
||||
}
|
||||
func (f *fakeApprovals) Read(id string) (approval.Whole, error) { return approval.Whole{}, nil }
|
||||
func (f *fakeApprovals) Applied() []approval.Marks { return nil }
|
||||
func (f *fakeApprovals) Describe(_, _, _ string) (bool, string) { return false, "" }
|
||||
func (f *fakeApprovals) Again(string) error { return nil }
|
||||
func (f *fakeApprovals) Cancel(id string) error { f.cancelled = id; return nil }
|
||||
func (f *fakeApprovals) Fill() (uint64, int64, error) { return 10, 64 << 20, nil }
|
||||
|
||||
// A client that hangs up or cancels ends its wait at once: the subscription is closed and nothing is left
|
||||
// waiting for the timeout (the review of issue 745). Over the console's own HTTP, as an agent host calls it.
|
||||
func TestMeshWaitEndsWhenItsClientHangsUp(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user