Match approval denial text consistently, respect reaction expiry, and timestamp replacement grants after their base even within the same second. Verify denied requests cannot apply and prepared replacements advance addressable event ordering.
- commit
- 688625fe875d99addb80ff7582633e56ea6f93b5
- parent
- 7d475627c98871df2ec1345e4c91c911b6f4702e
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 24 | +++++++++++++++++------- |
| M | internal/ | 20 | +++++++++++++++++++- |
@@ -23,23 +23,30 @@ const agentGrantReasonMax = 500
2323 2424 type grantRequestReader interface {2525 QueryRowContext(context.Context, string, ...any) *sql.Row26+ QueryContext(context.Context, string, ...any) (*sql.Rows, error)2627 }2728 2829 func grantRequestOpen(ctx context.Context, db grantRequestReader, request event.Event, now int64) error {2930 if expires := event.Expiration(request); expires != 0 && expires <= now {3031 return errors.New("conflict: grant request has expired")3132 }32- var denied bool33- err := db.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM events AS reaction34- WHERE kind=7 AND pubkey=? AND trim(json_extract(raw,'$.content'))='-' AND EXISTS(35- SELECT 1 FROM tags WHERE event_id=reaction.id AND name='e' AND value=?))`, event.Tag(request, "p"), request.ID).Scan(&denied)33+ rows, err := db.QueryContext(ctx, `SELECT json_extract(raw,'$.content') FROM events AS reaction34+ WHERE kind=7 AND pubkey=? AND (expires=0 OR expires>?) AND EXISTS(35+ SELECT 1 FROM tags WHERE event_id=reaction.id AND name='e' AND value=?)`, event.Tag(request, "p"), now, request.ID)3636 if err != nil {3737 return err3838 }39- if denied {40- return errors.New("conflict: grant request was denied")39+ defer rows.Close()40+ for rows.Next() {41+ var content string42+ if err := rows.Scan(&content); err != nil {43+ return err44+ }45+ if strings.TrimSpace(content) == "-" {46+ return errors.New("conflict: grant request was denied")47+ }4148 }42- return nil49+ return rows.Err()4350 }4451 4552 type AgentGrantChanges struct {@@ -193,6 +200,9 @@ func (s *Service) reviewAgainstGrant(e event.Event, req AgentGrantRequest, befor
193200 afterEvent.Tags = grantRequestTags(beforeEvent.Tags, req.Changes)194201 afterEvent.Tags = append(afterEvent.Tags, []string{"grant-request", e.ID}, []string{"grant-base", req.Base}, []string{"e", e.ID, "", "grant-request"})195202 afterEvent.ID, afterEvent.Sig, afterEvent.CreatedAt = "", "", now203+ if afterEvent.CreatedAt <= beforeEvent.CreatedAt {204+ afterEvent.CreatedAt = beforeEvent.CreatedAt + 1205+ }196206 after, err := ParseAgentGrant(afterEvent, now)197207 if err != nil {198208 return AgentGrantReview{}, err@@ -114,7 +114,7 @@ func TestGrantApprovalDenialAndStaleReview(t *testing.T) {
114114 t.Fatal(err)115115 }116116 if scenario == "deny" {117- if err := publishAs(t, tenant, signedEvent(t, testOwnerSecret, 7, now-10, [][]string{{"e", request.ID}, {"p", agent}}, "-")); err != nil {117+ if err := publishAs(t, tenant, signedEvent(t, testOwnerSecret, 7, now-10, [][]string{{"e", request.ID}, {"p", agent}}, "\n-\t")); err != nil {118118 t.Fatal(err)119119 }120120 } else if err := publishAs(t, tenant, agentGrantEvent(t, agent, now-5, now+3600, []string{"k", "1"})); err != nil {@@ -155,3 +155,21 @@ func TestGrantRequestNotificationRequiresReview(t *testing.T) {
155155 t.Fatalf("grant request notification: %+v", notices)156156 }157157 }158+159+func TestGrantReviewReplacementAdvancesTimestamp(t *testing.T) {160+ _, tenant := testTenant(t)161+ now := time.Now().Unix()162+ agent, _ := event.PublicKey(testAgentSecret)163+ grant := agentGrantEvent(t, agent, now, now+3600, []string{"k", "9"})164+ if err := publishAs(t, tenant, grant); err != nil {165+ t.Fatal(err)166+ }167+ request := approvalGrantRequest(t, grant, now)168+ review, err := tenant.community.ReviewAgentGrantRequest(context.Background(), request, now)169+ if err != nil {170+ t.Fatal(err)171+ }172+ if review.Unsigned.CreatedAt <= grant.CreatedAt {173+ t.Fatal("replacement can lose the same-second event ordering tie")174+ }175+}