Files
mesh-delivery/modules/mesh-grants/internal/grants/catalog.go
T
jschoubben 7bc4836eb0 AM-D3: a runtime whose tool runner does not honour the mark is denied every marked subject
The grants part follows the assignment half, reads each node's reading and the
runner its node-engine says running, and where the runner does not state
approval-mark/1 or the reading is not the running runner's, composes the
subscribe deny of every marked tool and verb the runtime carries, named in
grants_user_list's withheld and answered by the seat's verb withheld.

Builds: design 46 7 §6 (the deny's subjects, its cases, withheld in grants_user_list and the verb withheld), design 47 G §195, §309, lead answer A-203 (the assignment half followed by revision and assignment-revision; withheld's forms, keyed by node in the user list's state as the lead confirmed)

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 06:17:59 +01:00

253 lines
9.7 KiB
Go

package grants
import (
"context"
"encoding/json"
"fmt"
"slices"
"sync"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshcatalogclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshwantsclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshcatalog"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshdelivery"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/seats/meshwants"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/views"
"mesh-grants/internal/buscompose"
)
// **The grants part reads the catalog half** (design 47 G §614, §444, §684, A-5, A-52): through meshcatalogclient's branch follower
// (Branches), adopted on the part's views.ProcessReader, which holds the part's finding writer, so the part's `view-waits` is the
// reader's. The manifest of the build a want or a have names is the catalog graph's entry of that fingerprint (Resolve), never a
// builder's build.
// catalogHalfVerbs are the verbs every part that reads the catalog half names in its `invokes` (design 47 G §614): the revision of a
// branch, the registrations, the gate's source and the entry by fingerprint. (The gate's source is named in the short form: the
// long form with its field `branches`, §558, is not read by mesh-sdk's manifest reader yet.)
func catalogHalfVerbs() []string {
return []string{"seat:" + meshcatalog.Seat + "." + meshcatalog.VerbRevision, "seat:" + meshcatalog.Seat + "." + meshcatalog.VerbRegistrations,
meshcatalogclient.SourceVerb, meshcatalogclient.EntryVerb,
// And the assignment half's revision (design 46 approval mark §6, design 47 G §195).
meshwantsclient.RevisionVerb}
}
// catalogHalfEvents are the events every such part names in its `consumes`, as `<seat>.<event>`: the catalog's revision and
// registration-changed, and the gate's source-moved.
func catalogHalfEvents() []string {
return []string{meshcatalog.Seat + "." + meshcatalog.EventCatalogRevision, meshcatalog.Seat + "." + meshcatalog.EventRegistrationChanged,
meshdelivery.Seat + "." + meshdelivery.EventSourceMoved, meshwants.Seat + "." + meshwants.EventAssignmentRevision}
}
// CatalogHalf is what the part reads of the catalog half: the entry by its fingerprint, the branches whose first view is not in,
// and what a tool says of both.
type CatalogHalf interface {
// Resolve reads the entry of fingerprint: from a followed branch's half, else asked of `entry` once and kept.
Resolve(fingerprint string) (meshcatalog.CatalogEntry, meshcatalogclient.EntryState)
// Waiting says why the part has not every first view of the catalog half, or nil when it has them all: the branches
// followed whose first view is not in, or that none is followed yet.
Waiting() []string
// Statuses are the first view of each followed branch, in the order followed.
Statuses() []views.Status
// Entries are the entries kept by fingerprint, with their state.
Entries() []meshcatalogclient.EntryRow
}
// CatalogOf is the part's catalog half over the branch follower. Context is the process's life the entry asks run in; Wake
// starts a pass, and is called when a followed branch's first view comes in, which may change nothing a view's own change says.
type CatalogOf struct {
Branches *meshcatalogclient.Branches
Context context.Context
Wake func()
mu sync.Mutex
watched map[string]bool
}
// Resolve reads fingerprint's entry (meshcatalogclient.Branches.Resolve).
func (c *CatalogOf) Resolve(fingerprint string) (meshcatalog.CatalogEntry, meshcatalogclient.EntryState) {
return c.Branches.Resolve(c.Context, fingerprint)
}
// Waiting says which first views of the catalog half are not in.
func (c *CatalogOf) Waiting() []string {
followed := c.Branches.Followed()
if len(followed) == 0 {
return []string{"no branch is followed yet (the registrations and the gate's source are not answered)"}
}
var out []string
for _, b := range followed {
if cat, ok := c.Branches.Catalog(b); ok {
select {
case <-cat.Ready():
default:
out = append(out, "the catalog half of the branch "+b)
c.wakeAt(b, cat)
}
}
}
return out
}
// wakeAt starts a pass when branch's first view comes in, once per branch.
func (c *CatalogOf) wakeAt(branch string, cat *meshcatalogclient.Catalog) {
c.mu.Lock()
if c.watched == nil {
c.watched = map[string]bool{}
}
done := c.watched[branch]
c.watched[branch] = true
c.mu.Unlock()
if done || c.Wake == nil {
return
}
go func() {
select {
case <-cat.Ready():
c.Wake()
case <-c.Context.Done():
}
}()
}
// Statuses are the followed branches' first views.
func (c *CatalogOf) Statuses() []views.Status {
var out []views.Status
for _, b := range c.Branches.Followed() {
if cat, ok := c.Branches.Catalog(b); ok {
out = append(out, cat.Follower.Status())
}
}
return out
}
// Entries are the entries kept by fingerprint.
func (c *CatalogOf) Entries() []meshcatalogclient.EntryRow { return c.Branches.Entries.Rows() }
// catalogReader is what a Reader that holds the catalog half answers (Views does): ReadInputs reads the want's entry through it.
type catalogReader interface {
Resolve(fingerprint string) (meshcatalog.CatalogEntry, meshcatalogclient.EntryState)
Waiting() []string
}
// ErrNoCatalog is an entry asked of a reader that holds no catalog half (a tool's).
var ErrNoCatalog = fmt.Errorf("this reader holds no catalog half")
// Resolve reads an entry by its fingerprint through the part's catalog half; none held answers asked.
func (v *Views) Resolve(fingerprint string) (meshcatalog.CatalogEntry, meshcatalogclient.EntryState) {
if v == nil || v.Catalog == nil {
return meshcatalog.CatalogEntry{}, ""
}
return v.Catalog.Resolve(fingerprint)
}
// Waiting says which first views of the catalog half are not in; nil where none is held (a tool's reader waits for none).
func (v *Views) Waiting() []string {
if v == nil || v.Catalog == nil {
return nil
}
return v.Catalog.Waiting()
}
// entryManifest is the manifest of the entry of fingerprint in the catalog half, kept by fingerprint. state is the entry's
// state as the half answers it ("" where the reader holds no catalog half); a manifest is answered only with an `answered` entry.
func (c *Manifests) entryManifest(r Reader, fingerprint string) (catalogue.Manifest, meshcatalogclient.EntryState, error) {
cr, ok := r.(catalogReader)
if !ok {
return catalogue.Manifest{}, "", ErrNoCatalog
}
c.mu.Lock()
m, ok := c.byFp[fingerprint]
c.mu.Unlock()
if ok {
return m, meshcatalogclient.EntryAnswered, nil
}
entry, state := cr.Resolve(fingerprint)
if state != meshcatalogclient.EntryAnswered {
return catalogue.Manifest{}, state, fmt.Errorf("the catalog half's entry of %s is %s", fingerprint, orNone(string(state)))
}
raw, err := json.Marshal(entry.Manifest)
if err != nil || len(entry.Manifest) == 0 {
return catalogue.Manifest{}, state, fmt.Errorf("the entry of %s holds no manifest", fingerprint)
}
// Decoded, never judged, as a manifest by digest is (Manifests.get).
m, err = catalogue.Decode(raw)
if err != nil {
return catalogue.Manifest{}, state, fmt.Errorf("the manifest of the entry of %s does not decode: %w", fingerprint, err)
}
c.mu.Lock()
defer c.mu.Unlock()
if c.byFp == nil {
c.byFp = map[string]catalogue.Manifest{}
}
c.byFp[fingerprint] = m
return m, state, nil
}
func orNone(s string) string {
if s == "" {
return "not held"
}
return s
}
// catalogWaiting is why this pass composes before the catalog half's first views (design 47 G §684): the part keeps the user list it
// last wrote and adds what the entries of the builds wanted give.
func catalogWaiting(r Reader) string {
cr, ok := r.(catalogReader)
if !ok {
return ""
}
w := cr.Waiting()
if len(w) == 0 {
return ""
}
slices.Sort(w)
return fmt.Sprintf("the first views of the catalog half are not in (%v), so each account is composed from its want's entry by fingerprint and the list in force is kept", w)
}
// **The grants part reads the assignment half** (design 46 approval mark §6, design 47 G §195, lead answer A-203): the view of
// meshwantsclient's Assignment, a first view by mesh-wants' `revision`, then its `assignment-revision`, adopted on the part's
// views reader; from it each node's capability reading and the runner that announced it.
// AssignmentHalf is what the part reads of the assignment half: the revision it holds, false while it holds none.
type AssignmentHalf interface {
Revision() (meshwants.AssignmentRevision, bool)
}
// readingsReader is what a Reader that holds the assignment half answers (Views does).
type readingsReader interface {
Readings() (map[string]buscompose.RunnerReading, bool)
}
// Readings are each node's capability reading of the assignment half the part holds, by node; false where it holds none,
// and true with none where the reader follows no assignment half (a tool's reads nothing of it).
func (v *Views) Readings() (map[string]buscompose.RunnerReading, bool) {
if v == nil || v.Assignment == nil {
return nil, true
}
rev, ok := v.Assignment.Revision()
if !ok {
return nil, false
}
out := map[string]buscompose.RunnerReading{}
for _, c := range rev.Capabilities {
out[c.Node] = buscompose.RunnerReading{Capabilities: c.Readings, Fingerprint: c.Fingerprint}
}
return out, true
}
// readingsOf is each node's reading through r, and why the pass composes before the assignment half's first view.
func readingsOf(r Reader) (map[string]buscompose.RunnerReading, string) {
rr, ok := r.(readingsReader)
if !ok {
return nil, ""
}
readings, held := rr.Readings()
if !held {
return nil, "the first view of the assignment half is not in, so no node's capability reading is known and the list in force is kept"
}
return readings, ""
}