package grants import ( "bytes" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "sort" "strings" "sync" "sync/atomic" "time" "git.novox.be/novox/mesh-sdk/go/asks" "git.novox.be/novox/mesh-sdk/go/delivery" ) // The part's own states, as its manifest declares them, and the keys of the two that hold one value. const ( StateUserList = "user-list" StateMemberships = "memberships" StateAssertions = "assertions" StateTakeover = "takeover" KeyList = "list" KeySwitch = "switch" // KeyPass is the last pass's result, and KeyAsked a tool's ask to compose again now, both in StateUserList. KeyPass = "pass" KeyAsked = "asked" ) // States is every state the part keeps. var States = []string{StateAssertions, StateMemberships, StateTakeover, StateUserList} // The events the part says. const ( EventUserList = "user-list-changed" EventMembership = "membership-changed" // EventAsserted is said when the part created a bucket, log or stream a manifest declares (issue 1790). EventAsserted = "asserted" ) // Emits is every event the part says. var Emits = []string{EventAsserted, EventMembership, EventUserList} // Store is the part's own state, by local name: the only thing the part writes. type Store interface { Get(state, key string) ([]byte, uint64, bool, error) Put(state, key string, value []byte) (uint64, error) } // Emitter says one of the part's events. type Emitter interface { Emit(event string, body any) error } // UserListState is the value of `user-list` in shadow: the list the grants part composed (users and grants, no hashes), // what the bus loaded, and how they differ. Inputs is the digest of all it says but At: a value is written again only // when it changes. type UserListState struct { V int `json:"v"` Mode string `json:"mode"` Inputs string `json:"inputs"` Digest string `json:"digest"` Users int `json:"users"` // LeftOut are the assignments the shadow list leaves out, each with why. LeftOut []Gap `json:"left_out"` Compare Comparison `json:"compare"` Loaded *delivery.UserListWritten `json:"loaded,omitempty"` // Credentials says per user whether the vault holds its hash and how its line would differ (ADR 0547 clause 9). Credentials Credentials `json:"credentials"` BusNode string `json:"bus_node,omitempty"` Gaps []Gap `json:"gaps"` At time.Time `json:"at"` } func (s UserListState) inputs() string { s.At, s.Inputs = time.Time{}, "" raw, _ := json.Marshal(s) sum := sha256.Sum256(raw) return "sha256:" + hex.EncodeToString(sum[:]) } // Result is what one composition did. type Result struct { UserList UserListState `json:"user_list"` Revision uint64 `json:"revision"` Written bool `json:"written"` Memberships int `json:"memberships"` MembershipsWritten int `json:"memberships_written"` Published []string `json:"published,omitempty"` NotPublished []string `json:"not_published,omitempty"` Assertions int `json:"assertions"` AssertionsWritten int `json:"assertions_written"` // Asserted is each module whose declared streams this pass created, ` at : created …`, and // NotAsserted each creation refused, ` : ` (issue 1790). Asserted []string `json:"asserted,omitempty"` NotAsserted []string `json:"not_asserted,omitempty"` // Refused is every manifest the indexer holds that this pass could not read, `manifest of : `: its // module counts as placed nowhere (issue 1945). Refused []string `json:"refused,omitempty"` // RootNotFree is each machine the composition needed a root-free word for and did not get one, `: not // free, `: a verified sender is composed as from no root-free machine there (issue 1945). RootNotFree []string `json:"root_not_free,omitempty"` // Sealed is the list sealed to the bus node as the key `user-list.sealed` names it (ADR 0547 clause 7). Sealed *SealedState `json:"sealed,omitempty"` Errors []string `json:"errors,omitempty"` PublishedOrAsserted string `json:"published_or_asserted"` } // NothingTakesEffect is what every composition says it did on the bus beyond its own state and the memberships. const NothingTakesEffect = "memberships (on the assignments stream, a first one or one that only adds to the last published or drops only a verb the seat catalogue retired; any other loss stays a shadow difference), and each bucket, log or stream a manifest declares that the bus lacks, created once with the manifest's limits (the operator, 2026-10-26 08:54); the user list, the grants and any change to a stream the bus holds stay in shadow until take-over" // Part is the grants part: it reads, calls and writes through these, and nothing else. type Part struct { Read Reader Call Caller Store Store Emit Emitter // Publish publishes memberships: the process's connection; nil in the tools, which ask the process instead. Publish Publisher // Create creates a declared stream the bus lacks: the process's connection; nil in the tools, which ask the process. Create Creator // Asker asks the operator (take-over, hand-back): the process's client of the router; nil in the tools, which write // the request and let the process ask it. Asker Asker // Logf logs what no state or condition says (an error of acting on an answer): the process's; nil in the tools. Logf func(string, ...any) // Objects is the part's object bucket of sealed lists: the process's connection; nil in the tools. Objects Objects // Say says a condition to the operator: the process's; nil in the tools. Say func(asks.Condition) error // Forget takes away the record the asker kept of an ask it failed to publish; Nonce is a test's id suffix. Forget func(id string) error Nonce func() string // deciding is set while a take-over is decided, so the composition it makes asks and reconciles nothing. deciding atomic.Bool Now func() time.Time cache Manifests mu sync.Mutex last Result made creations // askSeen is the revision of the last ask to compose (KeyAsked) this part acted on. askSeen uint64 // heldSaid is the reason of the held-list condition last said (empty: none raised); heldKnown is whether one was said // since the start, so the first pass after a start always says its state (a condition raised before a restart is cleared // by it). holdMu guards both. holdMu sync.Mutex heldSaid string heldKnown bool } // Ask is the value of KeyAsked: a tool's ask to compose now, and, from grants_assert, to create one module's missing // streams again even where they were created at this manifest before. type Ask struct { By string `json:"by"` At time.Time `json:"at"` Assert string `json:"assert,omitempty"` } // askedAssert is the module a new ask names to assert by hand, read once per ask; empty when none. func (p *Part) askedAssert() string { raw, rev, found, err := p.Store.Get(StateUserList, KeyAsked) if err != nil || !found || rev == p.askSeen { return "" } p.askSeen = rev var a Ask _ = json.Unmarshal(raw, &a) return a.Assert } func (p *Part) now() time.Time { if p.Now == nil { return time.Now().UTC() } return p.Now().UTC() } // Compose composes the user list, the memberships and the assertions from what the bus holds now, compares them, and // writes each of its own keys whose value changed, saying each change. It never stops at a refusal: a read refused is // a gap, a write refused is said in Errors, and the next event composes again. func (p *Part) Compose() Result { // No lock is held across a round trip on the bus: the reads, calls, publishes and writes below take none, and the // lock is taken only to keep the result, so a tool answers while a pass waits on the bus. res := Result{PublishedOrAsserted: NothingTakesEffect} defer func() { p.mu.Lock() p.last = res p.mu.Unlock() }() in := ReadInputs(p.Read, &p.cache) for _, g := range in.Refused { res.Refused = append(res.Refused, g.Input+": "+g.Why) } recs, left := Records(in) res.RootNotFree = WithRootFree(&recs, p.Call) st := UserListState{V: 1, Mode: p.SwitchState(), LeftOut: left, Loaded: in.Loaded, BusNode: in.BusNode, Gaps: append(append([]Gap(nil), in.Gaps...), MembershipGaps...), At: p.now()} if st.LeftOut == nil { st.LeftOut = []Gap{} } users, err := ComposeUsers(recs) if err != nil { res.Errors = append(res.Errors, "the shadow list could not be composed: "+err.Error()) } users = WithHashes(users, in.Hashes) st.Digest, st.Users = Digest(users), len(users) loaded, err := LoadedUsers(p.Call) st.Compare = Compare(users, loaded, Placed(in)).Excusing(ExcusedBy(MultiHeld(recs))) if err != nil { st.Compare.Unread = err.Error() } st.Credentials = CredentialsOf(users, loaded, in.Hashes) st.Inputs = st.inputs() res.UserList = st if raw, rev, found, err := p.Store.Get(StateUserList, KeyList); err != nil { res.Errors = append(res.Errors, fmt.Sprintf("its own %s cannot be read: %v", StateUserList, err)) } else { var was UserListState if found && json.Unmarshal(raw, &was) == nil && was.Inputs == st.Inputs { res.Revision = rev } else if data, err := json.Marshal(st); err == nil { if rev, err := p.Store.Put(StateUserList, KeyList, data); err != nil { res.Errors = append(res.Errors, fmt.Sprintf("its own %s cannot be written: %v", StateUserList, err)) } else { res.Revision, res.Written = rev, true p.say(&res, EventUserList, map[string]any{"digest": st.Digest, "revision": rev, "users": st.Users, "differing": len(st.Compare.Differing()), "mode": Shadow}) } } } // The list itself, sealed to the bus node and named in `user-list.sealed` (ADR 0547 clause 7). if list, n, leftOut, err := ComposeList(recs, in.Hashes); err != nil { res.Errors = append(res.Errors, "the user list could not be composed whole: "+err.Error()) } else { // The users the bus loaded: what a first list after take-over must keep where the part has not yet written one. var bus []string if st.Compare.Unread == "" { bus = names(loaded) } p.writeSealed(&res, list, n, leftOut, in, bus) } ms := Memberships(recs) res.Memberships = len(ms) for _, key := range sortedKeys(ms) { m, now := publish(p.Publish, ms[key]) switch { case now: res.Published = append(res.Published, key) p.say(&res, EventMembership, map[string]any{"node": m.Node, "module": m.Module, "digest": m.Digest, "published": true}) case m.Published != MembershipUnchanged: res.NotPublished = append(res.NotPublished, key+": "+m.Published) } data, _ := json.Marshal(m) if p.changed(&res, StateMemberships, key, data) { res.MembershipsWritten++ } } held, heldErr := HeldStreams(p.Call) forced := "" if p.Create != nil { forced = p.askedAssert() } for _, module := range sortedKeys(in.Manifests) { if !assigned(in, module) { continue } a := Assert(module, in.Digests[module], in.Manifests[module], held, heldErr) // Created only where the bus was read and lacks it, never changed where it holds it (issue 1790). if p.Create != nil && heldErr == nil { if made := createMissing(p.Create, &p.made, &a, in.Manifests[module], forced == module); len(made) > 0 { res.Asserted = append(res.Asserted, fmt.Sprintf("%s at %s: created %s", module, a.Manifest, strings.Join(made, ", "))) p.say(&res, EventAsserted, map[string]any{"module": module, "manifest": a.Manifest, "created": made}) } for _, why := range notCreated(a) { res.NotAsserted = append(res.NotAsserted, module+" "+why) } } res.Assertions++ data, _ := json.Marshal(a) if p.changed(&res, StateAssertions, module, data) { res.AssertionsWritten++ } } if _, _, found, err := p.Store.Get(StateTakeover, KeySwitch); err == nil && !found { data, _ := json.Marshal(Switch{V: 1, State: Shadow, Acts: []Act{}}) if _, err := p.Store.Put(StateTakeover, KeySwitch, data); err != nil { res.Errors = append(res.Errors, fmt.Sprintf("its own %s cannot be written: %v", StateTakeover, err)) } } if !p.deciding.Load() { if err := p.RaiseRequested(); err != nil { res.Errors = append(res.Errors, err.Error()) } if err := p.Reconcile(); err != nil { res.Errors = append(res.Errors, err.Error()) } } return res } // Last is the result of the last pass this part made, kept under the lock alone. func (p *Part) Last() Result { p.mu.Lock() defer p.mu.Unlock() return p.last } func assigned(in Inputs, module string) bool { for _, ms := range in.Assigned { for _, m := range ms { if m == module { return true } } } return false } // changed writes key when its value differs from the one held, and says whether it wrote. func (p *Part) changed(res *Result, state, key string, data []byte) bool { raw, _, found, err := p.Store.Get(state, key) if err != nil { res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be read: %v", state, key, err)) return false } if found && bytes.Equal(raw, data) { return false } if _, err := p.Store.Put(state, key, data); err != nil { res.Errors = append(res.Errors, fmt.Sprintf("its own %s %s cannot be written: %v", state, key, err)) return false } return true } func (p *Part) say(res *Result, event string, body any) { if p.Emit == nil { return } if err := p.Emit.Emit(event, body); err != nil { res.Errors = append(res.Errors, fmt.Sprintf("the event %s could not be said: %v", event, err)) } } func sortedKeys[V any](m map[string]V) []string { out := make([]string, 0, len(m)) for k := range m { out = append(out, k) } sort.Strings(out) return out } func names(us []User) []string { out := make([]string, 0, len(us)) for _, u := range us { out = append(out, u.User) } return out }