| 1 | package daemon |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "encoding/json" |
| 6 | "strings" |
| 7 | |
| 8 | "github.com/FelineStateMachine/tinyrelay/internal/event" |
| 9 | ) |
| 10 | |
| 11 | // Keep the decision visible after the addressable grant has been superseded |
| 12 | // or expired. Only archived grants tied to these exact requests are read. |
| 13 | func (t *Tenant) grantApprovalHistory(ctx context.Context, items []approvalItem) ([]event.Event, error) { |
| 14 | ids, authors := []string{}, []string{} |
| 15 | for _, item := range items { |
| 16 | if item.Type == "grant" { |
| 17 | ids = append(ids, item.ID) |
| 18 | authors = append(authors, item.Asked...) |
| 19 | } |
| 20 | } |
| 21 | if len(ids) == 0 { |
| 22 | return nil, nil |
| 23 | } |
| 24 | idJSON, err := json.Marshal(ids) |
| 25 | if err != nil { |
| 26 | return nil, err |
| 27 | } |
| 28 | authorJSON, err := json.Marshal(uniqueStrings(authors)) |
| 29 | if err != nil { |
| 30 | return nil, err |
| 31 | } |
| 32 | rows, err := t.store.DB().QueryContext(ctx, `SELECT raw FROM ( |
| 33 | SELECT raw,created_at FROM events WHERE kind=30392 AND pubkey IN (SELECT value FROM json_each(?)) |
| 34 | UNION ALL SELECT raw,created_at FROM agent_grant_revisions WHERE author IN (SELECT value FROM json_each(?)) |
| 35 | ) WHERE EXISTS (SELECT 1 FROM json_each(json_extract(raw,'$.tags' )) AS tag |
| 36 | WHERE json_extract(tag.value,'$[0]' )='grant-request' AND json_extract(tag.value,'$[1]' ) IN (SELECT value FROM json_each(?))) |
| 37 | ORDER BY created_at DESC LIMIT ?`, string(authorJSON), string(authorJSON), string(idJSON), approvalScan) |
| 38 | if err != nil { |
| 39 | return nil, err |
| 40 | } |
| 41 | defer rows.Close() |
| 42 | var grants []event.Event |
| 43 | for rows.Next() { |
| 44 | var raw string |
| 45 | if err := rows.Scan(&raw); err != nil { |
| 46 | return nil, err |
| 47 | } |
| 48 | grant, err := event.Parse([]byte(raw)) |
| 49 | if err != nil { |
| 50 | return nil, err |
| 51 | } |
| 52 | grants = append(grants, grant) |
| 53 | } |
| 54 | return grants, rows.Err() |
| 55 | } |
| 56 | |
| 57 | // A grant is approved only by the operator's replacement grant. Reactions |
| 58 | // cannot confer permissions or make an unapplied request look successful. |
| 59 | func settleGrantApproval(item *approvalItem, rows []event.Event, now int64) { |
| 60 | for _, row := range rows { |
| 61 | if row.Kind != event.KIND_AGENT_GRANT || row.PubKey == item.Asker || !containsString(item.Asked, row.PubKey) || event.Tag(row, "grant-request" ) != item.ID || event.Tag(row, "grant-base" ) != event.Tag(item.Event, "E" ) || event.Tag(row, "d" ) != item.Asker { |
| 62 | continue |
| 63 | } |
| 64 | item.State = "answered" |
| 65 | item.Answer = &approvalAnswer{ID: row.ID, Kind: row.Kind, Author: row.PubKey, Decision: "approved" , CreatedAt: row.CreatedAt} |
| 66 | return |
| 67 | } |
| 68 | for _, row := range rows { |
| 69 | if row.Kind != kindReaction || strings.TrimSpace(row.Content) != "-" || !containsString(item.Asked, row.PubKey) { |
| 70 | continue |
| 71 | } |
| 72 | item.State = "answered" |
| 73 | item.Answer = &approvalAnswer{ID: row.ID, Kind: row.Kind, Author: row.PubKey, Decision: "denied" , CreatedAt: row.CreatedAt} |
| 74 | return |
| 75 | } |
| 76 | if item.Expires > 0 && item.Expires <= now { |
| 77 | item.State = "expired" |
| 78 | } |
| 79 | } |
| 80 | |
| 81 | func (t *Tenant) reviewGrantApproval(ctx context.Context, item *approvalItem, now int64) { |
| 82 | if item.Type != "grant" || item.State != "open" { |
| 83 | return |
| 84 | } |
| 85 | review, err := t.community.ReviewAgentGrantRequest(ctx, item.Event, now) |
| 86 | if err != nil { |
| 87 | item.GrantError = err.Error() |
| 88 | if strings.HasPrefix(item.GrantError, "conflict:" ) { |
| 89 | item.State = "expired" |
| 90 | } |
| 91 | return |
| 92 | } |
| 93 | item.Grant = &review |
| 94 | } |