tinyrelaycommit

Commit e69d1926bc0c

browse files at this commit | history

1diff --git a/internal/daemon/agents_test.go b/internal/daemon/agents_test.go
2new file mode 100644
3index 0000000..a2b2d62
4--- /dev/null
5+++ b/internal/daemon/agents_test.go
6@@ -0,0 +1,257 @@
7+package daemon
8+
9+import (
10+ "context"
11+ "encoding/json"
12+ "net/http"
13+ "net/http/httptest"
14+ "strconv"
15+ "strings"
16+ "testing"
17+ "time"
18+
19+ "github.com/FelineStateMachine/tinyrelay/internal/community"
20+ "github.com/FelineStateMachine/tinyrelay/internal/event"
21+ "github.com/FelineStateMachine/tinyrelay/internal/relay"
22+)
23+
24+const (
25+ testOwnerSecret = "0000000000000000000000000000000000000000000000000000000000000001"
26+ testAgentSecret = "0000000000000000000000000000000000000000000000000000000000000002"
27+ testMemberSecret = "0000000000000000000000000000000000000000000000000000000000000003"
28+ testModSecret = "0000000000000000000000000000000000000000000000000000000000000004"
29+)
30+
31+func signedEvent(t *testing.T, secret string, kind int, createdAt int64, tags [][]string, content string) event.Event {
32+ t.Helper()
33+ if tags == nil {
34+ tags = [][]string{}
35+ }
36+ e := event.Event{Kind: kind, CreatedAt: createdAt, Tags: tags, Content: content}
37+ if err := event.Sign(&e, secret); err != nil {
38+ t.Fatal(err)
39+ }
40+ return e
41+}
42+
43+func agentGrantEvent(t *testing.T, agent string, createdAt, expires int64, extra ...[]string) event.Event {
44+ t.Helper()
45+ tags := [][]string{{"d", agent}, {"p", agent}, {"name", "helper"}, {"expiration", strconv.FormatInt(expires, 10)}}
46+ return signedEvent(t, testOwnerSecret, event.KIND_AGENT_GRANT, createdAt, append(tags, extra...), "")
47+}
48+
49+func publishAs(t *testing.T, tenant *Tenant, e event.Event) error {
50+ t.Helper()
51+ _, err := tenant.Publish(context.Background(), e, relay.Session{PubKeys: []string{e.PubKey}})
52+ return err
53+}
54+
55+func TestAgentGrantPublishCreatesRoleRowAndRevokesOnDeletion(t *testing.T) {
56+ app, tenant := testTenant(t)
57+ ctx := context.Background()
58+ agent, _ := event.PublicKey(testAgentSecret)
59+ now := time.Now().Unix()
60+ grant := agentGrantEvent(t, agent, now, now+3600, []string{"k", "1"})
61+ if err := publishAs(t, tenant, grant); err != nil {
62+ t.Fatalf("grant rejected: %v", err)
63+ }
64+ if role, _ := tenant.community.Role(ctx, agent); role != "agent" {
65+ t.Fatalf("role = %q, want agent", role)
66+ }
67+ var owner, eventID, name string
68+ var expires int64
69+ if err := tenant.store.DB().QueryRow(`SELECT owner,event_id,name,expires_at FROM agent_grants WHERE agent=?`, agent).Scan(&owner, &eventID, &name, &expires); err != nil {
70+ t.Fatal(err)
71+ }
72+ if owner != tenant.Policy().Owner || eventID != grant.ID || name != "helper" || expires != now+3600 {
73+ t.Fatalf("grant row owner=%s event=%s name=%s expires=%d", owner, eventID, name, expires)
74+ }
75+ if err := publishAs(t, tenant, signedEvent(t, testAgentSecret, 1, now, nil, "hello")); err != nil {
76+ t.Fatalf("agent note rejected: %v", err)
77+ }
78+ if err := publishAs(t, tenant, signedEvent(t, testAgentSecret, 7, now, nil, "+")); err == nil || !strings.HasPrefix(err.Error(), "restricted: agent grant does not allow kind 7") {
79+ t.Fatalf("agent reaction: %v", err)
80+ }
81+ records, err := tenant.records.PublishMembership(ctx, now)
82+ if err != nil {
83+ t.Fatal(err)
84+ }
85+ labelled := map[int]bool{}
86+ for _, e := range records {
87+ for _, tag := range e.Tags {
88+ if len(tag) == 3 && tag[1] == agent && tag[2] == "agent" && (e.Kind == event.KIND_ROSTER || e.Kind == event.KIND_GROUP_MEMBERS) {
89+ labelled[e.Kind] = true
90+ }
91+ if e.Kind == event.KIND_ROLE_DEF && tag[0] == "d" && tag[1] == "agent" {
92+ labelled[e.Kind] = true
93+ }
94+ }
95+ }
96+ if !labelled[event.KIND_ROSTER] || !labelled[event.KIND_GROUP_MEMBERS] || !labelled[event.KIND_ROLE_DEF] {
97+ t.Fatalf("agent role missing from records: %v", labelled)
98+ }
99+ found := false
100+ for _, capability := range tenant.Capabilities(nil) {
101+ if capability.ID == "agents" && capability.Status == "enabled" {
102+ found = true
103+ }
104+ }
105+ if !found {
106+ t.Fatal("agents capability not advertised")
107+ }
108+ req := httptest.NewRequest(http.MethodGet, "http://relay.test/", nil)
109+ req.Header.Set("Accept", "application/nostr+json")
110+ res := httptest.NewRecorder()
111+ app.ServeHTTP(res, req)
112+ var info map[string]json.RawMessage
113+ if err := json.Unmarshal(res.Body.Bytes(), &info); err != nil || res.Code != 200 {
114+ t.Fatalf("info %d %s %v", res.Code, res.Body.String(), err)
115+ }
116+ if !strings.Contains(string(info["agents"]), `"id":"agents"`) {
117+ t.Fatalf("NIP-11 agents entry = %s", info["agents"])
118+ }
119+ deletion := signedEvent(t, testOwnerSecret, event.KIND_DELETION, now+1, [][]string{{"a", "30392:" + tenant.Policy().Owner + ":" + agent}}, "")
120+ if err := publishAs(t, tenant, deletion); err != nil {
121+ t.Fatalf("deletion rejected: %v", err)
122+ }
123+ if role, _ := tenant.community.Role(ctx, agent); role != "" {
124+ t.Fatalf("role after deletion = %q", role)
125+ }
126+ if err := publishAs(t, tenant, signedEvent(t, testAgentSecret, 1, now+2, nil, "again")); err == nil || !strings.Contains(err.Error(), "revoked") {
127+ t.Fatalf("revoked agent note: %v", err)
128+ }
129+ if err := publishAs(t, tenant, agentGrantEvent(t, agent, now+3, now+3600, []string{"k", "1"})); err != nil {
130+ t.Fatalf("new grant rejected: %v", err)
131+ }
132+ if err := publishAs(t, tenant, signedEvent(t, testAgentSecret, 1, now+4, nil, "back")); err != nil {
133+ t.Fatalf("re-granted agent rejected: %v", err)
134+ }
135+ if err := publishAs(t, tenant, agentGrantEvent(t, agent, now+5, now-1, []string{"k", "1"})); err != nil {
136+ t.Fatalf("expired grant rejected: %v", err)
137+ }
138+ if role, _ := tenant.community.Role(ctx, agent); role != "" {
139+ t.Fatalf("role after expired grant = %q", role)
140+ }
141+ var revoked int64
142+ if err := tenant.store.DB().QueryRow(`SELECT revoked_at FROM agent_grants WHERE agent=?`, agent).Scan(&revoked); err != nil || revoked == 0 {
143+ t.Fatalf("expired grant not revoked: %d %v", revoked, err)
144+ }
145+ if found := capabilityIDs(tenant)["agents"]; found {
146+ t.Fatal("agents capability advertised without an active agent")
147+ }
148+}
149+
150+func capabilityIDs(tenant *Tenant) map[string]bool {
151+ out := map[string]bool{}
152+ for _, capability := range tenant.Capabilities(nil) {
153+ out[capability.ID] = true
154+ }
155+ return out
156+}
157+
158+func TestAgentManagementMethodsAndPermissions(t *testing.T) {
159+ _, tenant := testTenant(t)
160+ ctx := context.Background()
161+ owner := tenant.Policy().Owner
162+ agent, _ := event.PublicKey(testAgentSecret)
163+ member, _ := event.PublicKey(testMemberSecret)
164+ moderator, _ := event.PublicKey(testModSecret)
165+ now := time.Now().Unix()
166+ for _, target := range []struct {
167+ pubkey, role string
168+ }{{member, "member"}, {moderator, "moderator"}} {
169+ raw, _ := json.Marshal(map[string]string{"role": target.role})
170+ if _, err := tenant.Execute(ctx, owner, "setmember", []json.RawMessage{json.RawMessage(strconv.Quote(target.pubkey)), raw}); err != nil {
171+ t.Fatal(err)
172+ }
173+ }
174+ if err := publishAs(t, tenant, agentGrantEvent(t, agent, now, now+3600, []string{"k", "1"})); err != nil {
175+ t.Fatal(err)
176+ }
177+ methods, err := tenant.Execute(ctx, owner, "supportedmethods", nil)
178+ if err != nil {
179+ t.Fatal(err)
180+ }
181+ for _, name := range []string{"listagents", "pauseagent", "resumeagent", "revokeagent", "pauseallagents", "resumeallagents"} {
182+ if !containsString(methods.([]string), name) {
183+ t.Fatalf("supportedmethods lacks %s", name)
184+ }
185+ }
186+ param := []json.RawMessage{json.RawMessage(strconv.Quote(agent))}
187+ for _, actor := range []string{member, agent, strings.Repeat("e", 64)} {
188+ for _, method := range []string{"listagents", "pauseagent", "revokeagent", "pauseallagents"} {
189+ if _, err := tenant.Execute(ctx, actor, method, param); err == nil || !strings.HasPrefix(err.Error(), "restricted:") {
190+ t.Fatalf("%s by non-admin: %v", method, err)
191+ }
192+ }
193+ }
194+ listed, err := tenant.Execute(ctx, moderator, "listagents", nil)
195+ if err != nil {
196+ t.Fatalf("moderator listagents: %v", err)
197+ }
198+ agents := listed.([]community.AgentSummary)
199+ if len(agents) != 1 || agents[0].Agent != agent || agents[0].Name != "helper" || agents[0].Owner != owner || agents[0].Paused || agents[0].RevokedAt != 0 || agents[0].Scope.Kinds[0] != 1 {
200+ t.Fatalf("listagents = %+v", agents)
201+ }
202+ if err := publishAs(t, tenant, signedEvent(t, testAgentSecret, 1, now, nil, "hello")); err != nil {
203+ t.Fatal(err)
204+ }
205+ listed, _ = tenant.Execute(ctx, owner, "listagents", nil)
206+ if last := listed.([]community.AgentSummary)[0].LastEvent; last != now {
207+ t.Fatalf("lastEvent = %d, want %d", last, now)
208+ }
209+ if _, err := tenant.Execute(ctx, moderator, "pauseagent", param); err != nil {
210+ t.Fatal(err)
211+ }
212+ if err := publishAs(t, tenant, signedEvent(t, testAgentSecret, 1, now+1, nil, "paused")); err == nil || !strings.Contains(err.Error(), "paused") {
213+ t.Fatalf("paused agent note: %v", err)
214+ }
215+ if _, err := tenant.Execute(ctx, owner, "resumeagent", param); err != nil {
216+ t.Fatal(err)
217+ }
218+ if err := publishAs(t, tenant, signedEvent(t, testAgentSecret, 1, now+2, nil, "resumed")); err != nil {
219+ t.Fatalf("resumed agent note: %v", err)
220+ }
221+ result, err := tenant.Execute(ctx, owner, "pauseallagents", nil)
222+ if err != nil || result.(map[string]any)["changed"] != 1 {
223+ t.Fatalf("pauseallagents = %v %v", result, err)
224+ }
225+ if err := publishAs(t, tenant, signedEvent(t, testAgentSecret, 1, now+3, nil, "all paused")); err == nil || !strings.Contains(err.Error(), "paused") {
226+ t.Fatalf("note during pause: %v", err)
227+ }
228+ if result, err := tenant.Execute(ctx, owner, "resumeallagents", nil); err != nil || result.(map[string]any)["changed"] != 1 {
229+ t.Fatalf("resumeallagents = %v %v", result, err)
230+ }
231+ if _, err := tenant.Execute(ctx, owner, "revokeagent", param); err != nil {
232+ t.Fatal(err)
233+ }
234+ if role, _ := tenant.community.Role(ctx, agent); role != "" {
235+ t.Fatalf("role after revoke = %q", role)
236+ }
237+ if err := publishAs(t, tenant, signedEvent(t, testAgentSecret, 1, now+4, nil, "revoked")); err == nil || !strings.Contains(err.Error(), "revoked") {
238+ t.Fatalf("revoked agent note: %v", err)
239+ }
240+ var stored int
241+ if err := tenant.store.DB().QueryRow(`SELECT count(*) FROM events WHERE kind=? AND d=?`, event.KIND_AGENT_GRANT, agent).Scan(&stored); err != nil || stored != 1 {
242+ t.Fatalf("grant event kept for audit: %d %v", stored, err)
243+ }
244+ if _, err := tenant.Execute(ctx, owner, "pauseagent", param); err == nil {
245+ t.Fatal("paused a revoked agent")
246+ }
247+ if _, err := tenant.Execute(ctx, owner, "revokeagent", []json.RawMessage{json.RawMessage(strconv.Quote(strings.Repeat("f", 64)))}); err == nil {
248+ t.Fatal("revoked an unknown agent")
249+ }
250+ audit, err := tenant.Execute(ctx, owner, "listaudit", nil)
251+ if err != nil {
252+ t.Fatal(err)
253+ }
254+ seen := map[string]bool{}
255+ for _, row := range audit.([]community.AuditRow) {
256+ seen[row.Action] = true
257+ }
258+ for _, action := range []string{"grantagent", "pauseagent", "resumeagent", "pauseallagents", "resumeallagents", "revokeagent"} {
259+ if !seen[action] {
260+ t.Fatalf("audit lacks %s: %v", action, seen)
261+ }
262+ }
263+}
264diff --git a/internal/daemon/capabilities.go b/internal/daemon/capabilities.go
265index 9af6d8b..f4e8dc8 100644
266--- a/internal/daemon/capabilities.go
267+++ b/internal/daemon/capabilities.go
268@@ -44,6 +44,9 @@ func (t *Tenant) Capabilities(peers *PeerMonitor) []Capability {
269 if t.community != nil {
270 setCapability(capabilities, "NIP-43", Capability{ID: "NIP-43", Status: "enabled", Reason: "membership protocol"})
271 setCapability(capabilities, "NIP-56", Capability{ID: "NIP-56", Status: "enabled", Reason: "moderation reports"})
272+ if active, err := t.community.ActiveAgentCount(context.Background(), time.Now().Unix()); err == nil && active > 0 {
273+ capabilities = append(capabilities, Capability{ID: "agents", Status: "enabled", Reason: "agent identities under owner-signed grants"})
274+ }
275 }
276 if t.blobs != nil && p.Features.Files {
277 setCapability(capabilities, "NIP-94", Capability{ID: "NIP-94", Status: "enabled", Reason: "file metadata descriptors"})
278diff --git a/internal/daemon/http.go b/internal/daemon/http.go
279index d09873a..c9a1e10 100644
280--- a/internal/daemon/http.go
281+++ b/internal/daemon/http.go
282@@ -322,10 +322,15 @@ func statusFor(err error) int {
283 func (t *Tenant) information(w http.ResponseWriter, r *http.Request) {
284 p := t.Policy()
285 nips := []int{}
286+ var agents *Capability
287 for _, capability := range t.Capabilities(nil) {
288 if capability.Status != "enabled" {
289 continue
290 }
291+ if capability.ID == "agents" {
292+ entry := capability
293+ agents = &entry
294+ }
295 if number, ok := capabilityNIPNumber(capability.ID); ok {
296 nips = append(nips, number)
297 }
298@@ -362,6 +367,9 @@ func (t *Tenant) information(w http.ResponseWriter, r *http.Request) {
299 if p.Features.Grasp && t.git != nil {
300 info["supported_grasps"] = t.git.SupportedGRASPs()
301 }
302+ if agents != nil {
303+ info["agents"] = *agents
304+ }
305 if p.Features.Sites.Enabled && !privatePolicy(p) {
306 base, _ := url.Parse(t.publicURL)
307 domain := t.siteDomain()
308diff --git a/internal/daemon/management.go b/internal/daemon/management.go
309index 69d7e6e..bc6f79d 100644
310--- a/internal/daemon/management.go
311+++ b/internal/daemon/management.go
312@@ -19,6 +19,7 @@ import (
313 "time"
314
315 "github.com/FelineStateMachine/tinyrelay/internal/catalog"
316+ "github.com/FelineStateMachine/tinyrelay/internal/community"
317 "github.com/FelineStateMachine/tinyrelay/internal/domains"
318 "github.com/FelineStateMachine/tinyrelay/internal/event"
319 "github.com/FelineStateMachine/tinyrelay/internal/policy"
320@@ -88,6 +89,8 @@ func (t *Tenant) executeManagement(ctx context.Context, actor, method string, pa
321 return t.identity(), true, nil
322 case "transferowner":
323 return t.transferOwner(ctx, actor, params)
324+ case "listagents", "pauseagent", "resumeagent", "revokeagent", "pauseallagents", "resumeallagents":
325+ return t.agentExecute(ctx, actor, method, params)
326 case "forkrelay":
327 return t.forkRelay(ctx, actor, params)
328 case "deleterelay":
329@@ -98,7 +101,7 @@ func (t *Tenant) executeManagement(ctx context.Context, actor, method string, pa
330 }
331
332 func managementAdapterMethod(method string) bool {
333- for _, name := range []string{"listconnectiontemplates", "listconnections", "setconnections", "changerelayname", "changerelaydescription", "changerelayicon", "adddomain", "setdomainsite", "checkdomain", "removedomain", "listdomains", "listblobs", "deleteblob", "listsites", "deleteevent", "listrecentevents", "searchevents", "storagestats", "publishview", "gitstorage", "resetrules", "getidentity", "transferowner", "forkrelay", "deleterelay"} {
334+ for _, name := range []string{"listconnectiontemplates", "listconnections", "setconnections", "changerelayname", "changerelaydescription", "changerelayicon", "adddomain", "setdomainsite", "checkdomain", "removedomain", "listdomains", "listblobs", "deleteblob", "listsites", "deleteevent", "listrecentevents", "searchevents", "storagestats", "publishview", "gitstorage", "resetrules", "getidentity", "transferowner", "forkrelay", "deleterelay", "listagents", "pauseagent", "resumeagent", "revokeagent", "pauseallagents", "resumeallagents"} {
335 if method == name {
336 return true
337 }
338@@ -125,7 +128,7 @@ func publicManagementMethod(method string) bool {
339 // ManagementMethods is the source-compatible NIP-86 registry. The hosted
340 // lease, trial, fuel, and entitlement controls are intentionally absent.
341 func ManagementMethods() []string {
342- return []string{"supportedmethods", "listaudit", "stats", "getpolicy", "setpolicy", "listviews", "banpubkey", "setblockedwords", "allowpubkey", "setmember", "unrulepubkey", "removemember", "listbannedpubkeys", "listallowedpubkeys", "listmembers", "listpeople", "createinvite", "listinvites", "revokeinvite", "listclaims", "listlisthistory", "restorelist", "createclaim", "deleteclaim", "removesubtree", "banevent", "allowevent", "listeventsneedingmoderation", "blockip", "unblockip", "listblockedips", "listreports", "resolvereport", "exportconfig", "importconfig", "deleterelay", "listblobs", "listsites", "deleteblob", "deleteevent", "listrecentevents", "searchevents", "pinevent", "unpinevent", "listpins", "allowkind", "disallowkind", "unrulekind", "storagestats", "gitstorage", "setretention", "listretention", "purgekind", "listallowedkinds", "listblockedkinds", "notifytest", "resetrules", "listpresets", "listconnectiontemplates", "listconnections", "setconnections", "applypreset", "forkrelay", "pullfrom", "pullstatus", "listjobs", "deliverystatus", "addjob", "removejob", "runjob", "backfill", "transferowner", "listdumps", "deletedump", "dumpnow", "backupnow", "listbackups", "deletebackup", "setsuccession", "clearsuccession", "successionstatus", "changerelayname", "changerelaydescription", "changerelayicon", "adddomain", "setdomainsite", "checkdomain", "removedomain", "listdomains"}
343+ return []string{"supportedmethods", "listaudit", "stats", "getpolicy", "setpolicy", "listviews", "banpubkey", "setblockedwords", "allowpubkey", "setmember", "unrulepubkey", "removemember", "listbannedpubkeys", "listallowedpubkeys", "listmembers", "listpeople", "createinvite", "listinvites", "revokeinvite", "listclaims", "listlisthistory", "restorelist", "createclaim", "deleteclaim", "removesubtree", "banevent", "allowevent", "listeventsneedingmoderation", "blockip", "unblockip", "listblockedips", "listreports", "resolvereport", "exportconfig", "importconfig", "deleterelay", "listblobs", "listsites", "deleteblob", "deleteevent", "listrecentevents", "searchevents", "pinevent", "unpinevent", "listpins", "allowkind", "disallowkind", "unrulekind", "storagestats", "gitstorage", "setretention", "listretention", "purgekind", "listallowedkinds", "listblockedkinds", "notifytest", "resetrules", "listpresets", "listconnectiontemplates", "listconnections", "setconnections", "applypreset", "forkrelay", "pullfrom", "pullstatus", "listjobs", "deliverystatus", "addjob", "removejob", "runjob", "backfill", "transferowner", "listdumps", "deletedump", "dumpnow", "backupnow", "listbackups", "deletebackup", "setsuccession", "clearsuccession", "successionstatus", "changerelayname", "changerelaydescription", "changerelayicon", "adddomain", "setdomainsite", "checkdomain", "removedomain", "listdomains", "listagents", "pauseagent", "resumeagent", "revokeagent", "pauseallagents", "resumeallagents"}
344 }
345
346 func managementRoleAllows(method, role string) bool {
347@@ -135,7 +138,7 @@ func managementRoleAllows(method, role string) bool {
348 if role != "moderator" {
349 return false
350 }
351- for _, name := range []string{"listconnectiontemplates", "listconnections", "listblobs", "listsites", "listrecentevents", "searchevents", "storagestats", "deleteevent"} {
352+ for _, name := range []string{"listconnectiontemplates", "listconnections", "listblobs", "listsites", "listrecentevents", "searchevents", "storagestats", "deleteevent", "listagents", "pauseagent", "resumeagent", "revokeagent", "pauseallagents", "resumeallagents"} {
353 if method == name {
354 return true
355 }
356@@ -276,6 +279,44 @@ func (t *Tenant) transferOwner(ctx context.Context, actor string, params []json.
357 return map[string]any{"owner": next, "previousOwner": current}, true, nil
358 }
359
360+// agentExecute serves the agent kill switch and inventory. The community
361+// service records each change in the audit log; this adapter only shapes
362+// parameters and logs counts.
363+func (t *Tenant) agentExecute(ctx context.Context, actor, method string, params []json.RawMessage) (any, bool, error) {
364+ now := time.Now().Unix()
365+ switch method {
366+ case "listagents":
367+ agents, err := t.community.Agents(ctx)
368+ return agents, true, err
369+ case "pauseallagents", "resumeallagents":
370+ changed, err := t.community.SetAllAgentsPaused(ctx, actor, method == "pauseallagents")
371+ if err != nil {
372+ return nil, true, err
373+ }
374+ t.app.telemetry.Logger().Info("agent grants changed", "tenant", t.meta.Name, "action", method, "changed", changed)
375+ return map[string]any{"changed": changed, "paused": method == "pauseallagents"}, true, nil
376+ }
377+ agent := stringParam(params, 0)
378+ if len(agent) != 64 {
379+ return nil, true, errors.New("invalid: agent public key required")
380+ }
381+ if _, err := hex.DecodeString(agent); err != nil {
382+ return nil, true, errors.New("invalid: agent public key required")
383+ }
384+ var grant community.AgentGrant
385+ var err error
386+ switch method {
387+ case "pauseagent", "resumeagent":
388+ grant, err = t.community.SetAgentPaused(ctx, actor, agent, method == "pauseagent")
389+ case "revokeagent":
390+ grant, err = t.community.RevokeAgent(ctx, actor, agent, now)
391+ }
392+ if err != nil {
393+ return nil, true, err
394+ }
395+ return grant, true, nil
396+}
397+
398 func (t *Tenant) deleteRelay(ctx context.Context, params []json.RawMessage) (any, bool, error) {
399 name := stringParam(params, 0)
400 if name == "" || !strings.EqualFold(name, t.meta.Name) {
401@@ -403,7 +444,7 @@ func copyForkConfig(ctx context.Context, source, target *storage.Store) error {
402 }
403
404 func copyForkPeople(ctx context.Context, source, target *storage.Store, owner string) error {
405- rows, err := source.DB().QueryContext(ctx, `SELECT pubkey,name,note,role,invited_by,via,created_at,keep_days FROM community_members WHERE role<>'owner'`)
406+ rows, err := source.DB().QueryContext(ctx, `SELECT pubkey,name,note,role,invited_by,via,created_at,keep_days FROM community_members WHERE role<>'owner' AND role<>'agent'`)
407 if err != nil {
408 return fmt.Errorf("fork members: %w", err)
409 }
410diff --git a/internal/daemon/services.go b/internal/daemon/services.go
411index c47c10f..985721c 100644
412--- a/internal/daemon/services.go
413+++ b/internal/daemon/services.go
414@@ -652,6 +652,7 @@ func (t *Tenant) commitImported(ctx context.Context, e event.Event, origin repli
415 if e.Kind == 30023 {
416 opts.Intents = append(opts.Intents, storage.Intent{Kind: "view-publish", EventID: e.ID, Target: "articles", Payload: "{}"})
417 }
418+ opts.BeforeCommit = t.agentBeforeCommit(e, opts.Now, opts.BeforeCommit)
419 _, err := t.store.Save(ctx, e, opts)
420 if err == nil {
421 t.notifyDevices(ctx, e)
422diff --git a/internal/daemon/tenant.go b/internal/daemon/tenant.go
423index e3c58e2..59190b2 100644
424--- a/internal/daemon/tenant.go
425+++ b/internal/daemon/tenant.go
426@@ -275,6 +275,7 @@ func (t *Tenant) Publish(ctx context.Context, e event.Event, s relay.Session) (s
427 })
428 }
429 }
430+ opts.BeforeCommit = t.agentBeforeCommit(e, now, opts.BeforeCommit)
431 opts.Intents = append(opts.Intents, t.replication.Prepare(e, replication.OriginClient)...)
432 if callbackIntents, callbackErr := t.PrepareReplicationCallbacks(ctx, e); callbackErr != nil {
433 return "", callbackErr
434@@ -392,6 +393,22 @@ func (t *Tenant) Publish(ctx context.Context, e event.Event, s relay.Session) (s
435 return "", nil
436 }
437
438+// agentBeforeCommit mirrors agent grants and grant deletions into the agent
439+// table and roster in the same transaction that stores the event.
440+func (t *Tenant) agentBeforeCommit(e event.Event, now int64, before func(context.Context, *sql.Tx) error) func(context.Context, *sql.Tx) error {
441+ if e.Kind != event.KIND_AGENT_GRANT && e.Kind != event.KIND_DELETION {
442+ return before
443+ }
444+ return func(txCtx context.Context, tx *sql.Tx) error {
445+ if before != nil {
446+ if err := before(txCtx, tx); err != nil {
447+ return err
448+ }
449+ }
450+ return t.community.ApplyAgentEventTx(txCtx, tx, e, now)
451+ }
452+}
453+
454 func (t *Tenant) vanish(ctx context.Context, e event.Event) error {
455 for _, tag := range e.Tags {
456 if len(tag) > 1 && tag[0] == "relay" && (tag[1] == "ALL_RELAYS" || strings.TrimSuffix(tag[1], "/") == strings.TrimSuffix(t.RelayURL(), "/")) {

About

ownerc01503e8035f
visibilitypublic
heade69d1926bc0c

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs