From d420491ca110bebf9815234fa7011b73228a47ee Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 4 Nov 2026 18:13:41 +0100 Subject: [PATCH] R2-G-sdk-contracts 4: a state may be declared read-only Only the processes whose own bus.state names such a state write it; the module's tools read it and never write it. nats declares its warrant spend record so, and the judge's rule and the grants build on the field. Decoded and written back; given twice it is refused as every field is. Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig --- go/catalogue/state.go | 10 +++++++--- go/catalogue/state_test.go | 13 +++++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/go/catalogue/state.go b/go/catalogue/state.go index 85a747e..19f07eb 100644 --- a/go/catalogue/state.go +++ b/go/catalogue/state.go @@ -31,9 +31,13 @@ type StateDeclaration struct { // KeyTTL says each key of the bucket lives the time its writer gives it, and the server writes a delete marker // when it ends (novox/hq ADR 0433 ยง1): the bucket is made with the server's time to live per message. KeyTTL bool `json:"key-ttl,omitempty"` + // ReadOnly says only the module's processes whose own bus.state names the state write it; the module's tools, + // the tool runner's account, read it and never write it (the lead's ruling, 2026-11-04: nats' warrant spend record + // `spent`). The judge's rule and the grants follow from it. + ReadOnly bool `json:"read-only,omitempty"` } -// UnmarshalJSON reads a bucket as its bare name, or as {name, history, ttl-seconds}. +// UnmarshalJSON reads a bucket as its bare name, or as {name, history, ttl-seconds, per-machine, key-ttl, read-only}. func (s *StateDeclaration) UnmarshalJSON(raw []byte) error { trimmed := bytes.TrimSpace(raw) if len(trimmed) > 0 && trimmed[0] == '"' { @@ -44,7 +48,7 @@ func (s *StateDeclaration) UnmarshalJSON(raw []byte) error { dec := json.NewDecoder(bytes.NewReader(trimmed)) dec.DisallowUnknownFields() if err := dec.Decode(&full); err != nil { - return fmt.Errorf("a state is either a name or {name, history, ttl-seconds, per-machine, key-ttl}: %w", typedUnknown(err)) + return fmt.Errorf("a state is either a name or {name, history, ttl-seconds, per-machine, key-ttl, read-only}: %w", typedUnknown(err)) } *s = StateDeclaration(full) return nil @@ -52,7 +56,7 @@ func (s *StateDeclaration) UnmarshalJSON(raw []byte) error { // MarshalJSON writes back the short form when there is nothing else to say. func (s StateDeclaration) MarshalJSON() ([]byte, error) { - if s.History == 0 && s.TTLSeconds == 0 && !s.PerMachine && !s.KeyTTL { + if s.History == 0 && s.TTLSeconds == 0 && !s.PerMachine && !s.KeyTTL && !s.ReadOnly { return json.Marshal(s.Name) } type plain StateDeclaration diff --git a/go/catalogue/state_test.go b/go/catalogue/state_test.go index cf6cb7e..0da094f 100644 --- a/go/catalogue/state_test.go +++ b/go/catalogue/state_test.go @@ -2,6 +2,7 @@ package catalogue import ( "encoding/json" + "strings" "testing" ) @@ -28,17 +29,25 @@ func TestAManifestMaySayWhatStateItKeepsAndReads(t *testing.T) { } // 1154/AC1: a state entry may say key-ttl; it is read, written back, and a bare name or an entry without it reads as before. +// G-sdk-contracts 4: read-only likewise, and given twice it is refused. func TestAStateMayHaveKeysThatExpireOneByOne(t *testing.T) { m, err := decodeForTest([]byte(`{"module":"mesh-delivery-watchman","version":"1",` + - `"state":[{"name":"pulse","key-ttl":true},"stuck",{"name":"seen","history":5}]}`)) + `"state":[{"name":"pulse","key-ttl":true},"stuck",{"name":"seen","history":5},{"name":"spent","read-only":true}]}`)) if err != nil { t.Fatal(err) } if !m.State[0].KeyTTL || m.State[1].KeyTTL || m.State[2].KeyTTL { t.Fatalf("key-ttl not read as said: %+v", m.State) } + if !m.State[3].ReadOnly || m.State[0].ReadOnly || m.State[1].ReadOnly { + t.Fatalf("read-only not read as said: %+v", m.State) + } out, _ := json.Marshal(m.State) - if string(out) != `[{"name":"pulse","key-ttl":true},"stuck",{"name":"seen","history":5}]` { + if string(out) != `[{"name":"pulse","key-ttl":true},"stuck",{"name":"seen","history":5},{"name":"spent","read-only":true}]` { t.Fatalf("written back as %s", out) } + if _, err := decodeForTest([]byte(`{"module":"nats","version":"1","state":[{"name":"spent","read-only":true,"read-only":false}]}`)); err == nil || + !strings.Contains(err.Error(), "the field state[0].read-only is given twice") { + t.Fatalf("read-only given twice: %v", err) + } }