catalogue: a module's own object bucket, its reader grant and a stream that keeps its history, so the knowledge base can keep its records on the bus (ADR 0592 §5, §6, §9)
Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
This commit is contained in:
@@ -612,6 +612,11 @@ type Manifest struct {
|
||||
// hears a part of it only where that setting's value names it.
|
||||
Streams []StreamDeclaration `json:"streams,omitempty"`
|
||||
|
||||
// Objects are the object buckets this module owns on the bus (novox/hq ADR 0592 §5): each named locally and
|
||||
// bounded in size; the mesh creates each once, with no age limit, and refuses a new object when it is full. Only its
|
||||
// module writes it; another reads it only through a `reads` entry and the bucket's grant (ADR 0592 §9).
|
||||
Objects []ObjectsDeclaration `json:"objects,omitempty"`
|
||||
|
||||
// Settings are the defaults this module gives its settings (novox/hq ADR 0262): each key a file,
|
||||
// a contribution or a served fact asks for as `${setting:<key>}`, its default, and why that
|
||||
// default. Only a preference has one — a font size, a width, a number of workers — and a value
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// A module's own object bucket (novox/hq ADR 0592 §5, §9).
|
||||
//
|
||||
// A module declares an object bucket as it declares a state (ADR 0201) or a stream (ADR 0363): named locally,
|
||||
// with its maximum size. The mesh creates it once, keeps every object with no age limit, and refuses a new object
|
||||
// when it is full rather than dropping one; only its module writes it. Another module reads it only as a restricted
|
||||
// stream is heard: a `reads` entry `<module>.<name>` naming it, and a module the owner's trusted config value of the
|
||||
// bucket's grant names (ADR 0363 §4, §5), read-only. This is the shape only: its checks are the indexer's judge's.
|
||||
|
||||
// The mesh's bounds on a module's object bucket: its size in MiB (ADR 0592 §5, §25; the bound ADR 0363 §1 sets for a
|
||||
// stream). A bucket says its size: there is no default.
|
||||
const (
|
||||
ObjectsLeastMiB = 1
|
||||
ObjectsMostMiB = 8192
|
||||
)
|
||||
|
||||
// FullWarnPercent is how full, in percent of its cap, a history stream or an object bucket is when the mesh raises its
|
||||
// condition, in one place for every module (novox/hq ADR 0592 §6, §25).
|
||||
const FullWarnPercent = 75
|
||||
|
||||
// ObjectsDeclaration is one object bucket a module owns.
|
||||
type ObjectsDeclaration struct {
|
||||
// Name is its local name, one plain name as a state's (lower-case letters, digits and hyphens).
|
||||
Name string `json:"name"`
|
||||
// MaxMiB bounds it, ObjectsLeastMiB to ObjectsMostMiB.
|
||||
MaxMiB int `json:"max-mib"`
|
||||
// Grant names the trusted config value of the module whose entries name the modules that may read it; absent,
|
||||
// no other module reads it.
|
||||
Grant *ObjectsGrant `json:"grant,omitempty"`
|
||||
}
|
||||
|
||||
// ObjectsGrant says which trusted config value of the owning module grants another module the read of a bucket
|
||||
// (novox/hq ADR 0592 §9): the value has a stream grant's shape (GrantedKeys), and a module any of its keys names, wherever
|
||||
// it runs or as `<machine>.<module>`, reads the whole bucket, read-only. One value may grant a module's streams and its
|
||||
// object buckets alike.
|
||||
type ObjectsGrant struct {
|
||||
// Config is the trusted config value of the module that grants it (ADR 0348 §8): set only at the controller's
|
||||
// terminal or on the operator's Approve.
|
||||
Config string `json:"config"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON reads an object bucket strictly: an unknown key is refused here, not passed over.
|
||||
func (o *ObjectsDeclaration) UnmarshalJSON(raw []byte) error {
|
||||
type plain ObjectsDeclaration
|
||||
var p plain
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&p); err != nil {
|
||||
return fmt.Errorf("an object bucket is {name, max-mib, grant}: %w", typedUnknown(err))
|
||||
}
|
||||
*o = ObjectsDeclaration(p)
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON reads an object bucket's grant strictly.
|
||||
func (g *ObjectsGrant) UnmarshalJSON(raw []byte) error {
|
||||
type plain ObjectsGrant
|
||||
var p plain
|
||||
dec := json.NewDecoder(bytes.NewReader(raw))
|
||||
dec.DisallowUnknownFields()
|
||||
if err := dec.Decode(&p); err != nil {
|
||||
return fmt.Errorf("an object bucket's grant is {config}: %w", typedUnknown(err))
|
||||
}
|
||||
*g = ObjectsGrant(p)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ObjectsBucket is the bucket's name on the bus, as a state's is: the module and the local name joined by an
|
||||
// underscore, which neither may hold, so two modules never derive one bucket.
|
||||
func ObjectsBucket(module, local string) string { return module + "_" + local }
|
||||
|
||||
// Bucket is this bucket's name on the bus for its module.
|
||||
func (o ObjectsDeclaration) Bucket(module string) string { return ObjectsBucket(module, o.Name) }
|
||||
|
||||
// ObjectsNamed is the module's object bucket of the local name, and whether it declares one.
|
||||
func (m Manifest) ObjectsNamed(local string) (ObjectsDeclaration, bool) {
|
||||
for _, o := range m.Objects {
|
||||
if o.Name == local {
|
||||
return o, true
|
||||
}
|
||||
}
|
||||
return ObjectsDeclaration{}, false
|
||||
}
|
||||
|
||||
// ObjectsReader says whether the restricting config values of a bucket's grant let module, on machine, read it
|
||||
// (novox/hq ADR 0592 §9): a bucket with no grant is read by its owner alone, and a value of another shape grants nothing.
|
||||
func (o ObjectsDeclaration) ObjectsReader(values []any, module, machine string) bool {
|
||||
if o.Grant == nil {
|
||||
return false
|
||||
}
|
||||
for _, v := range values {
|
||||
if len(GrantedKeys(v, module, machine)) > 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package catalogue
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// A module declares its own object bucket and a stream that keeps its history (ADR 0592 §5, §6, §9): both are read with
|
||||
// their bounds, the bucket's grant names its readers through a trusted config value as a restricted stream's does, and
|
||||
// an unknown key in either is refused, not passed over.
|
||||
func TestAModuleDeclaresAnObjectBucketAndAHistoryStream(t *testing.T) {
|
||||
m, err := decodeForTest([]byte(`{"module":"mesh-knowledge","version":"1",` +
|
||||
`"streams":[{"name":"versions","events":["version.**"],"max-mib":2048,"retention":"history","grant":{"config":"readers","events":"{key}.**"}}],` +
|
||||
`"objects":[{"name":"sections","max-mib":4096,"grant":{"config":"readers"}},{"name":"own","max-mib":1}]}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s := m.Streams[0]
|
||||
if !s.KeepsHistory() || s.Days() != 0 || s.Cap() != 2048 {
|
||||
t.Fatalf("history stream read as %+v (days %d)", s, s.Days())
|
||||
}
|
||||
o, ok := m.ObjectsNamed("sections")
|
||||
if !ok || o.MaxMiB != 4096 || o.Grant == nil || o.Grant.Config != "readers" || o.Bucket(m.Module) != "mesh-knowledge_sections" {
|
||||
t.Fatalf("object bucket read as %+v", o)
|
||||
}
|
||||
readers := []any{map[string]any{"records": []any{"knowledge-web", "ace.mesh-view"}}}
|
||||
if !o.ObjectsReader(readers, "knowledge-web", "g14") || !o.ObjectsReader(readers, "mesh-view", "ace") ||
|
||||
o.ObjectsReader(readers, "mesh-view", "g14") || o.ObjectsReader(readers, "other", "ace") {
|
||||
t.Fatal("the bucket's grant does not name its readers as a restricted stream's does")
|
||||
}
|
||||
own, _ := m.ObjectsNamed("own")
|
||||
if own.ObjectsReader(readers, "knowledge-web", "g14") {
|
||||
t.Fatal("a bucket with no grant is read by another module")
|
||||
}
|
||||
for name, raw := range map[string]string{
|
||||
"bucket": `{"module":"x","version":"1","objects":[{"name":"a","max-mib":1,"max-age-days":3}]}`,
|
||||
"bucket grant": `{"module":"x","version":"1","objects":[{"name":"a","max-mib":1,"grant":{"config":"r","events":"{key}"}}]}`,
|
||||
} {
|
||||
if _, err := decodeStrictObjects(raw); err == nil || !strings.Contains(err.Error(), "object bucket") {
|
||||
t.Errorf("%s: an unknown key was not refused in the object bucket's words: %v", name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decodeStrictObjects(raw string) (Manifest, error) {
|
||||
m, err := decodeForTest([]byte(raw))
|
||||
if err == nil && m.SetAside() != "" {
|
||||
return m, errString(m.SetAside())
|
||||
}
|
||||
return m, err
|
||||
}
|
||||
|
||||
type errString string
|
||||
|
||||
func (e errString) Error() string { return string(e) }
|
||||
+16
-2
@@ -35,6 +35,12 @@ const GrantKey = "{key}"
|
||||
// is the mesh's own limits retention — kept until it ages or the stream fills, dropping its oldest.
|
||||
const StreamRetentionWorkQueue = "work-queue"
|
||||
|
||||
// StreamRetentionHistory is a stream's declared retention that keeps its history (novox/hq ADR 0592 §6): every message
|
||||
// is kept with no maximum age, and when the stream reaches its maximum size the bus refuses a new message rather than
|
||||
// dropping its oldest. It takes no max-age-days, max-deliveries or duplicates; the mesh raises a condition when it
|
||||
// passes FullWarnPercent of its cap.
|
||||
const StreamRetentionHistory = "history"
|
||||
|
||||
// StreamLeastDeliveries and StreamMostDeliveries bound a work-queue stream's max-deliveries (novox/hq ADR
|
||||
// 0436 §6): how many times a message is handed over before it is given up as a dead letter.
|
||||
const (
|
||||
@@ -57,7 +63,8 @@ type StreamDeclaration struct {
|
||||
// Grant restricts it, when said.
|
||||
Grant *StreamGrant `json:"grant,omitempty"`
|
||||
// Retention is how the stream decides what to keep when it fills (novox/hq ADR 0436 §6): empty for the
|
||||
// mesh's own limits retention (dropping its oldest), or StreamRetentionWorkQueue.
|
||||
// mesh's own limits retention (dropping its oldest), StreamRetentionWorkQueue, or StreamRetentionHistory
|
||||
// (novox/hq ADR 0592 §6).
|
||||
Retention string `json:"retention,omitempty"`
|
||||
// MaxDeliveries bounds how many times a work-queue message is handed over, 1 to 10; refused without
|
||||
// retention work-queue (novox/hq ADR 0436 §6).
|
||||
@@ -104,14 +111,21 @@ func (g *StreamGrant) UnmarshalJSON(raw []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Days is the stream's age in days, the default where none is said.
|
||||
// Days is the stream's age in days, the default where none is said, and 0 (no maximum age) for a stream that keeps its
|
||||
// history.
|
||||
func (s StreamDeclaration) Days() int {
|
||||
if s.KeepsHistory() {
|
||||
return 0
|
||||
}
|
||||
if s.MaxAgeDays == 0 {
|
||||
return StreamDefaultDays
|
||||
}
|
||||
return s.MaxAgeDays
|
||||
}
|
||||
|
||||
// KeepsHistory says the stream keeps every message and refuses a new one when full (StreamRetentionHistory).
|
||||
func (s StreamDeclaration) KeepsHistory() bool { return s.Retention == StreamRetentionHistory }
|
||||
|
||||
// Cap is the stream's size in MiB, the default where none is said.
|
||||
func (s StreamDeclaration) Cap() int {
|
||||
if s.MaxMiB == 0 {
|
||||
|
||||
Reference in New Issue
Block a user