From 77e6e48ecd12ad170bbaaeb9e185e9439b119aa9 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 12 Nov 2026 06:26:10 +0100 Subject: [PATCH] AM-H1: the node-engine's composition names the withheld subjects, read through mesh-grants' withheld MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The node-engine never reads the grants part's state: it asks the seat's verb withheld in its own name, and says withheld unknown when nothing answers. Builds: design 46 7 §6 (the composition reads them through the verb withheld; the node-engine's manifest gains mesh-grants in uses; "withheld unknown: the grants part does not answer"), lead answer A-239 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- cmd/mesh-host/engineseat.go | 15 ++++++++-- cmd/mesh-host/main.go | 2 +- internal/agent/composition.go | 11 ++++++++ internal/link/movesecret_test.go | 25 +++++++++++++++++ internal/link/withheld.go | 47 ++++++++++++++++++++++++++++++++ module.json | 3 ++ 6 files changed, 100 insertions(+), 3 deletions(-) create mode 100644 internal/link/withheld.go diff --git a/cmd/mesh-host/engineseat.go b/cmd/mesh-host/engineseat.go index 2b14c308..a1ccbe1d 100644 --- a/cmd/mesh-host/engineseat.go +++ b/cmd/mesh-host/engineseat.go @@ -18,6 +18,7 @@ import ( occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient" stdio "git.novox.be/novox/mesh-sdk/go" "github.com/novox/mesh-host/internal/accounts" + "github.com/novox/mesh-host/internal/agent" "github.com/novox/mesh-host/internal/apply" "github.com/novox/mesh-host/internal/link" "github.com/novox/mesh-host/internal/selfcompose" @@ -284,7 +285,7 @@ const compositionReadFor = 20 * time.Second // compositionFrom answers the verb composition from the node agent that runs now (issue 774); refused in words while // none does. -func compositionFrom(current *agentNow) func(module, digest string, diff bool) (any, error) { +func compositionFrom(current *agentNow, withheld func(ctx context.Context, module string) ([]link.WithheldSubject, string)) func(module, digest string, diff bool) (any, error) { return func(module, digest string, diff bool) (any, error) { ag := current.get() if ag == nil { @@ -292,7 +293,17 @@ func compositionFrom(current *agentNow) func(module, digest string, diff bool) ( } ctx, cancel := context.WithTimeout(context.Background(), compositionReadFor) defer cancel() - return ag.Composition(ctx, module, digest, diff) + view, err := ag.Composition(ctx, module, digest, diff) + if err != nil { + return view, err + } + // And what the grants part withholds of it here, through its verb withheld (design 46 approval mark §6). + subjects, unknown := withheld(ctx, module) + for _, s := range subjects { + view.Withheld = append(view.Withheld, agent.WithheldView{Subject: s.Subject, Why: s.Why}) + } + view.WithheldUnknown = unknown + return view, nil } } diff --git a/cmd/mesh-host/main.go b/cmd/mesh-host/main.go index eb47ba29..513a82b3 100644 --- a/cmd/mesh-host/main.go +++ b/cmd/mesh-host/main.go @@ -1022,7 +1022,7 @@ func runLink(ctx context.Context, opts options) error { queue.Acts = engineActs(opts, mine.Node, queue, setuidSearch) go reconcileActs(ctx, queue.Acts) current := &agentNow{} - queue.Acts.Composition = compositionFrom(current) + queue.Acts.Composition = compositionFrom(current, queue.WithheldOf) queue.Acts.Compose = composeFrom(current, &composerNow) queue.Acts.Packages = packagesFrom(mine.Node, queue, &composerNow) queue.Acts.FirstViews, queue.Acts.EntryAgain = firstViewsFrom(&composerNow), entryAgainFrom(&composerNow) diff --git a/internal/agent/composition.go b/internal/agent/composition.go index 37c89d99..9539952c 100644 --- a/internal/agent/composition.go +++ b/internal/agent/composition.go @@ -44,6 +44,17 @@ type CompositionView struct { // Own is, of the node's own composition (module node), its own fields beside what runs names (issue 774: ace's // own composition dropped the provisioner client's account, and nothing read could show it). Own *OwnView `json:"own,omitempty"` + // Withheld are the module's marked subjects the grants part withholds from this node's runtime, each with why, read + // through the seat mesh-grants' verb withheld; WithheldUnknown says when that verb did not answer (design 46 approval + // mark §6, lead answer A-239). + Withheld []WithheldView `json:"withheld,omitempty"` + WithheldUnknown string `json:"withheld-unknown,omitempty"` +} + +// WithheldView is one marked subject withheld, and why. +type WithheldView struct { + Subject string `json:"subject"` + Why string `json:"why"` } // OwnView is the node's own fields of a composition (engcomp.NodeOwn) beside what runs of them, and what the node's diff --git a/internal/link/movesecret_test.go b/internal/link/movesecret_test.go index fc017566..ca6385ce 100644 --- a/internal/link/movesecret_test.go +++ b/internal/link/movesecret_test.go @@ -139,4 +139,29 @@ func TestNatsAMoveIsAskedOfTheNodeAndTheVaultIsAskedInItsName(t *testing.T) { if _, err := (&Session{Membership: Membership{Node: "nowhere"}}).AskVault(t.Context(), "move-in", nil); !errors.Is(err, ErrNotAsked) { t.Fatalf("a queue with no link asked the vault: %v", err) } + // AM-H1 (design 46 approval mark §6, lead answer A-239): the composition reads what the grants part withholds through + // its verb withheld, asked in the engine's name, and says withheld unknown when nothing answers. + grants, err := conn.Subscribe("mesh.seat.mesh-grants.call.withheld.node~"+m.Node, func(msg *nats.Msg) { + _ = msg.Respond([]byte(`{"result":{"node":"` + m.Node + `","modules":[{"module":"shell","subjects":[` + + `{"subject":"mesh.mod.shell.tool.execute","why":"its tool runner does not state approval-mark/1"}]}]}}`)) + }) + if err != nil { + t.Fatal(err) + } + if err := conn.Flush(); err != nil { + t.Fatal(err) + } + if got, unknown := q.WithheldOf(t.Context(), "shell"); unknown != "" || len(got) != 1 || got[0].Subject != "mesh.mod.shell.tool.execute" { + t.Errorf("withheld of shell = %+v (%q)", got, unknown) + } + if got, unknown := q.WithheldOf(t.Context(), "other"); unknown != "" || len(got) != 0 { + t.Errorf("withheld of a module the grants part names none of = %+v (%q)", got, unknown) + } + _ = grants.Unsubscribe() + if err := conn.Flush(); err != nil { + t.Fatal(err) + } + if got, unknown := q.WithheldOf(t.Context(), "shell"); unknown != WithheldUnknown || got != nil { + t.Errorf("with the grants part down, withheld = %+v (%q)", got, unknown) + } } diff --git a/internal/link/withheld.go b/internal/link/withheld.go new file mode 100644 index 00000000..f0d005b7 --- /dev/null +++ b/internal/link/withheld.go @@ -0,0 +1,47 @@ +package link + +import ( + "context" + "encoding/json" +) + +// **The composition names the withheld subjects** (design 46 approval mark §6, lead answer A-239): the node-engine reads them +// through the seat mesh-grants' verb withheld, never from the grants part's state; when the verb does not answer, it says so. + +// GrantsSeat is the seat of the grants part, whose verb withheld names the marked subjects withheld from a node's runtime. +const GrantsSeat = "mesh-grants" + +// WithheldUnknown is what a composition says when the grants part does not answer withheld (design 46 approval mark §6). +const WithheldUnknown = "withheld unknown: the grants part does not answer" + +// WithheldSubject is one marked subject withheld from this node's runtime, and why. +type WithheldSubject struct { + Subject string `json:"subject"` + Why string `json:"why"` +} + +// WithheldOf is what the grants part withholds of module on this node, asked through its verb withheld: the subjects, or, +// when the verb does not answer or answers what is not its answer, WithheldUnknown as unknown. +func (q *Session) WithheldOf(ctx context.Context, module string) (subjects []WithheldSubject, unknown string) { + raw, err := q.askCalled(ctx, GrantsSeat, "withheld", map[string]string{"node": q.Membership.Node}, VaultWithin, + "the grants part's withheld") + if err != nil { + return nil, WithheldUnknown + } + var answer struct { + Node string `json:"node"` + Modules []struct { + Module string `json:"module"` + Subjects []WithheldSubject `json:"subjects"` + } `json:"modules"` + } + if json.Unmarshal(raw, &answer) != nil || answer.Node != q.Membership.Node { + return nil, WithheldUnknown + } + for _, m := range answer.Modules { + if m.Module == module { + subjects = append(subjects, m.Subjects...) + } + } + return subjects, "" +} diff --git a/module.json b/module.json index d97c6be5..9e262cfe 100644 --- a/module.json +++ b/module.json @@ -39,6 +39,9 @@ ] } ], + "uses": [ + "mesh-grants" + ], "invokes": [ "seat:mesh-catalog.entry", {"verb": "seat:mesh-catalog.revision", "fields": ["none-yet"]},