policy.go raw

   1  package policy
   2  
   3  import (
   4  	"bufio"
   5  	"bytes"
   6  	"context"
   7  	"encoding/json"
   8  	"fmt"
   9  	"io"
  10  	"os"
  11  	"os/exec"
  12  	"path/filepath"
  13  	"regexp"
  14  	"strconv"
  15  	"strings"
  16  	"sync"
  17  	"time"
  18  
  19  	"next.orly.dev/pkg/nostr/encoders/event"
  20  	"next.orly.dev/pkg/nostr/encoders/hex"
  21  	"github.com/adrg/xdg"
  22  	"github.com/sosodev/duration"
  23  	"next.orly.dev/pkg/lol/chk"
  24  	"next.orly.dev/pkg/lol/log"
  25  	"next.orly.dev/pkg/utils"
  26  )
  27  
  28  // parseDuration parses an ISO-8601 duration string into seconds.
  29  // ISO-8601 format: P[n]Y[n]M[n]DT[n]H[n]M[n]S
  30  // Examples: "P1D" (1 day), "PT1H" (1 hour), "P7DT12H" (7 days 12 hours), "PT30M" (30 minutes)
  31  // Uses the github.com/sosodev/duration library for strict ISO-8601 compliance.
  32  // Note: Years and Months are converted to approximate time.Duration values
  33  // (1 year ≈ 365.25 days, 1 month ≈ 30.44 days).
  34  func parseDuration(s string) (int64, error) {
  35  	if s == "" {
  36  		return 0, fmt.Errorf("empty duration string")
  37  	}
  38  
  39  	s = strings.TrimSpace(s)
  40  	if s == "" {
  41  		return 0, fmt.Errorf("empty duration string")
  42  	}
  43  
  44  	// Parse using the ISO-8601 duration library
  45  	d, err := duration.Parse(s)
  46  	if err != nil {
  47  		return 0, fmt.Errorf("invalid ISO-8601 duration %q: %v", s, err)
  48  	}
  49  
  50  	// Convert to time.Duration and then to seconds
  51  	timeDur := d.ToTimeDuration()
  52  	return int64(timeDur.Seconds()), nil
  53  }
  54  
  55  // Kinds defines whitelist and blacklist policies for event kinds.
  56  // Whitelist takes precedence over blacklist - if whitelist is present, only whitelisted kinds are allowed.
  57  // If only blacklist is present, all kinds except blacklisted ones are allowed.
  58  type Kinds struct {
  59  	// Whitelist is a list of event kinds that are allowed to be written to the relay. If any are present, implicitly all others are denied.
  60  	Whitelist []int `json:"whitelist,omitempty"`
  61  	// Blacklist is a list of event kinds that are not allowed to be written to the relay. If any are present, implicitly all others are allowed. Only takes effect in the absence of a Whitelist.
  62  	Blacklist []int `json:"blacklist,omitempty"`
  63  }
  64  
  65  // Rule defines policy criteria for a specific event kind.
  66  //
  67  // Rules are evaluated in the following order:
  68  // 1. If Script is present and running, it determines the outcome
  69  // 2. If Script fails or is not running, falls back to default_policy
  70  // 3. Otherwise, all specified criteria are evaluated as AND operations
  71  //
  72  // For pubkey allow/deny lists: whitelist takes precedence over blacklist.
  73  // If whitelist has entries, only whitelisted pubkeys are allowed.
  74  // If only blacklist has entries, all pubkeys except blacklisted ones are allowed.
  75  // =============================================================================
  76  // Rule Sub-Components (Value Objects)
  77  // =============================================================================
  78  
  79  // AccessControl defines who can read/write events.
  80  // This is a value object that encapsulates access control configuration.
  81  type AccessControl struct {
  82  	// WriteAllow is a list of pubkeys allowed to write. If any present, all others denied.
  83  	WriteAllow []string `json:"write_allow,omitempty"`
  84  	// WriteDeny is a list of pubkeys denied write. Only effective without WriteAllow.
  85  	WriteDeny []string `json:"write_deny,omitempty"`
  86  	// ReadAllow is a list of pubkeys allowed to read. If any present, all others denied.
  87  	ReadAllow []string `json:"read_allow,omitempty"`
  88  	// ReadDeny is a list of pubkeys denied read. Only effective without ReadAllow.
  89  	ReadDeny []string `json:"read_deny,omitempty"`
  90  	// WriteAllowFollows grants access to policy admin follows when enabled.
  91  	WriteAllowFollows bool `json:"write_allow_follows,omitempty"`
  92  	// FollowsWhitelistAdmins specifies admin pubkeys whose follows are whitelisted.
  93  	// DEPRECATED: Use ReadFollowsWhitelist and WriteFollowsWhitelist instead.
  94  	FollowsWhitelistAdmins []string `json:"follows_whitelist_admins,omitempty"`
  95  	// ReadFollowsWhitelist specifies pubkeys whose follows can READ events.
  96  	ReadFollowsWhitelist []string `json:"read_follows_whitelist,omitempty"`
  97  	// WriteFollowsWhitelist specifies pubkeys whose follows can WRITE events.
  98  	WriteFollowsWhitelist []string `json:"write_follows_whitelist,omitempty"`
  99  	// ReadAllowPermissive allows read access for ALL kinds on GLOBAL rule.
 100  	ReadAllowPermissive bool `json:"read_allow_permissive,omitempty"`
 101  	// WriteAllowPermissive allows write access bypassing kind whitelist on GLOBAL rule.
 102  	WriteAllowPermissive bool `json:"write_allow_permissive,omitempty"`
 103  	// WriteAllowIfTagged is a list of pubkeys. Authors not in WriteAllow can still
 104  	// publish if their event contains a p-tag referencing one of these pubkeys.
 105  	WriteAllowIfTagged []string `json:"write_allow_if_tagged,omitempty"`
 106  
 107  	// Binary caches (internal, not serialized)
 108  	writeAllowBin              [][]byte
 109  	writeDenyBin               [][]byte
 110  	readAllowBin               [][]byte
 111  	readDenyBin                [][]byte
 112  	followsWhitelistAdminsBin  [][]byte
 113  	followsWhitelistFollowsBin [][]byte
 114  	readFollowsWhitelistBin    [][]byte
 115  	writeFollowsWhitelistBin   [][]byte
 116  	readFollowsFollowsBin      [][]byte
 117  	writeFollowsFollowsBin     [][]byte
 118  	writeAllowIfTaggedBin      [][]byte
 119  }
 120  
 121  // Constraints defines limits and restrictions on events.
 122  // This is a value object that encapsulates event constraints.
 123  type Constraints struct {
 124  	// MaxExpiry is the maximum expiry time in seconds.
 125  	// Deprecated: Use MaxExpiryDuration instead.
 126  	MaxExpiry *int64 `json:"max_expiry,omitempty"` //nolint:staticcheck
 127  	// MaxExpiryDuration is the max expiry in ISO-8601 duration format.
 128  	MaxExpiryDuration string `json:"max_expiry_duration,omitempty"`
 129  	// SizeLimit is the maximum total serialized size in bytes.
 130  	SizeLimit *int64 `json:"size_limit,omitempty"`
 131  	// ContentLimit is the maximum content field size in bytes.
 132  	ContentLimit *int64 `json:"content_limit,omitempty"`
 133  	// RateLimit is the write rate limit in bytes per second.
 134  	RateLimit *int64 `json:"rate_limit,omitempty"`
 135  	// MaxAgeOfEvent is the max age in seconds for created_at timestamps.
 136  	MaxAgeOfEvent *int64 `json:"max_age_of_event,omitempty"`
 137  	// MaxAgeEventInFuture is the max future offset for created_at timestamps.
 138  	MaxAgeEventInFuture *int64 `json:"max_age_event_in_future,omitempty"`
 139  	// ProtectedRequired requires events to have a "-" tag (NIP-70).
 140  	ProtectedRequired bool `json:"protected_required,omitempty"`
 141  	// Privileged means event is only sent to authenticated parties.
 142  	Privileged bool `json:"privileged,omitempty"`
 143  
 144  	// Parsed cache (internal, not serialized)
 145  	maxExpirySeconds *int64
 146  }
 147  
 148  // TagValidationConfig defines tag validation rules.
 149  // This is a value object that encapsulates tag validation configuration.
 150  type TagValidationConfig struct {
 151  	// MustHaveTags is a list of tag key letters that must be present.
 152  	MustHaveTags []string `json:"must_have_tags,omitempty"`
 153  	// TagValidation is a map of tag_name -> regex pattern for validation.
 154  	TagValidation map[string]string `json:"tag_validation,omitempty"`
 155  	// IdentifierRegex is a regex pattern for "d" tag identifiers.
 156  	IdentifierRegex string `json:"identifier_regex,omitempty"`
 157  
 158  	// Compiled cache (internal, not serialized)
 159  	identifierRegexCache *regexp.Regexp
 160  }
 161  
 162  // =============================================================================
 163  // Rule (Composed from Sub-Components)
 164  // =============================================================================
 165  
 166  // Rule defines policies for a specific event kind or as a global default.
 167  // It is composed of sub-value objects for cleaner organization.
 168  type Rule struct {
 169  	// Description is a human-readable description of the rule.
 170  	Description string `json:"description"`
 171  	// Script is a path to a validation script.
 172  	Script string `json:"script,omitempty"`
 173  
 174  	// Embedded sub-components (fields are flattened in JSON for backward compatibility)
 175  	AccessControl
 176  	Constraints
 177  	TagValidationConfig
 178  }
 179  
 180  // hasAnyRules checks if the rule has any constraints configured
 181  func (r *Rule) hasAnyRules() bool {
 182  	// Check for any configured constraints
 183  	return len(r.WriteAllow) > 0 || len(r.WriteDeny) > 0 ||
 184  		len(r.ReadAllow) > 0 || len(r.ReadDeny) > 0 ||
 185  		len(r.writeAllowBin) > 0 || len(r.writeDenyBin) > 0 ||
 186  		len(r.readAllowBin) > 0 || len(r.readDenyBin) > 0 ||
 187  		r.SizeLimit != nil || r.ContentLimit != nil ||
 188  		r.MaxAgeOfEvent != nil || r.MaxAgeEventInFuture != nil ||
 189  		r.MaxExpiry != nil || r.MaxExpiryDuration != "" || r.maxExpirySeconds != nil || //nolint:staticcheck // Backward compat
 190  		len(r.MustHaveTags) > 0 ||
 191  		r.Script != "" || r.Privileged ||
 192  		r.WriteAllowFollows || len(r.FollowsWhitelistAdmins) > 0 ||
 193  		len(r.ReadFollowsWhitelist) > 0 || len(r.WriteFollowsWhitelist) > 0 ||
 194  		len(r.readFollowsWhitelistBin) > 0 || len(r.writeFollowsWhitelistBin) > 0 ||
 195  		len(r.TagValidation) > 0 ||
 196  		r.ProtectedRequired || r.IdentifierRegex != "" ||
 197  		r.ReadAllowPermissive || r.WriteAllowPermissive ||
 198  		len(r.WriteAllowIfTagged) > 0 || len(r.writeAllowIfTaggedBin) > 0
 199  }
 200  
 201  // populateBinaryCache converts hex-encoded pubkey strings to binary for faster comparison.
 202  // This should be called after unmarshaling the policy from JSON.
 203  func (r *Rule) populateBinaryCache() error {
 204  	var err error
 205  
 206  	// Convert WriteAllow hex strings to binary
 207  	if len(r.WriteAllow) > 0 {
 208  		r.writeAllowBin = make([][]byte, 0, len(r.WriteAllow))
 209  		for _, hexPubkey := range r.WriteAllow {
 210  			binPubkey, decErr := hex.Dec(hexPubkey)
 211  			if decErr != nil {
 212  				log.W.F("failed to decode WriteAllow pubkey %q: %v", hexPubkey, decErr)
 213  				continue
 214  			}
 215  			r.writeAllowBin = append(r.writeAllowBin, binPubkey)
 216  		}
 217  	}
 218  
 219  	// Convert WriteDeny hex strings to binary
 220  	if len(r.WriteDeny) > 0 {
 221  		r.writeDenyBin = make([][]byte, 0, len(r.WriteDeny))
 222  		for _, hexPubkey := range r.WriteDeny {
 223  			binPubkey, decErr := hex.Dec(hexPubkey)
 224  			if decErr != nil {
 225  				log.W.F("failed to decode WriteDeny pubkey %q: %v", hexPubkey, decErr)
 226  				continue
 227  			}
 228  			r.writeDenyBin = append(r.writeDenyBin, binPubkey)
 229  		}
 230  	}
 231  
 232  	// Convert ReadAllow hex strings to binary
 233  	if len(r.ReadAllow) > 0 {
 234  		r.readAllowBin = make([][]byte, 0, len(r.ReadAllow))
 235  		for _, hexPubkey := range r.ReadAllow {
 236  			binPubkey, decErr := hex.Dec(hexPubkey)
 237  			if decErr != nil {
 238  				log.W.F("failed to decode ReadAllow pubkey %q: %v", hexPubkey, decErr)
 239  				continue
 240  			}
 241  			r.readAllowBin = append(r.readAllowBin, binPubkey)
 242  		}
 243  	}
 244  
 245  	// Convert ReadDeny hex strings to binary
 246  	if len(r.ReadDeny) > 0 {
 247  		r.readDenyBin = make([][]byte, 0, len(r.ReadDeny))
 248  		for _, hexPubkey := range r.ReadDeny {
 249  			binPubkey, decErr := hex.Dec(hexPubkey)
 250  			if decErr != nil {
 251  				log.W.F("failed to decode ReadDeny pubkey %q: %v", hexPubkey, decErr)
 252  				continue
 253  			}
 254  			r.readDenyBin = append(r.readDenyBin, binPubkey)
 255  		}
 256  	}
 257  
 258  	// Parse MaxExpiryDuration into maxExpirySeconds
 259  	// MaxExpiryDuration takes precedence over MaxExpiry if both are set
 260  	if r.MaxExpiryDuration != "" {
 261  		seconds, parseErr := parseDuration(r.MaxExpiryDuration)
 262  		if parseErr != nil {
 263  			log.W.F("failed to parse MaxExpiryDuration %q: %v", r.MaxExpiryDuration, parseErr)
 264  		} else {
 265  			r.maxExpirySeconds = &seconds
 266  		}
 267  	} else if r.MaxExpiry != nil { //nolint:staticcheck // Backward compatibility
 268  		// Fall back to MaxExpiry (raw seconds) if MaxExpiryDuration not set
 269  		r.maxExpirySeconds = r.MaxExpiry //nolint:staticcheck // Backward compatibility
 270  	}
 271  
 272  	// Compile IdentifierRegex pattern
 273  	if r.IdentifierRegex != "" {
 274  		compiled, compileErr := regexp.Compile(r.IdentifierRegex)
 275  		if compileErr != nil {
 276  			log.W.F("failed to compile IdentifierRegex %q: %v", r.IdentifierRegex, compileErr)
 277  		} else {
 278  			r.identifierRegexCache = compiled
 279  		}
 280  	}
 281  
 282  	// Convert FollowsWhitelistAdmins hex strings to binary (DEPRECATED)
 283  	if len(r.FollowsWhitelistAdmins) > 0 {
 284  		r.followsWhitelistAdminsBin = make([][]byte, 0, len(r.FollowsWhitelistAdmins))
 285  		for _, hexPubkey := range r.FollowsWhitelistAdmins {
 286  			binPubkey, decErr := hex.Dec(hexPubkey)
 287  			if decErr != nil {
 288  				log.W.F("failed to decode FollowsWhitelistAdmins pubkey %q: %v", hexPubkey, decErr)
 289  				continue
 290  			}
 291  			r.followsWhitelistAdminsBin = append(r.followsWhitelistAdminsBin, binPubkey)
 292  		}
 293  	}
 294  
 295  	// Convert ReadFollowsWhitelist hex strings to binary
 296  	if len(r.ReadFollowsWhitelist) > 0 {
 297  		r.readFollowsWhitelistBin = make([][]byte, 0, len(r.ReadFollowsWhitelist))
 298  		for _, hexPubkey := range r.ReadFollowsWhitelist {
 299  			binPubkey, decErr := hex.Dec(hexPubkey)
 300  			if decErr != nil {
 301  				log.W.F("failed to decode ReadFollowsWhitelist pubkey %q: %v", hexPubkey, decErr)
 302  				continue
 303  			}
 304  			r.readFollowsWhitelistBin = append(r.readFollowsWhitelistBin, binPubkey)
 305  		}
 306  	}
 307  
 308  	// Convert WriteFollowsWhitelist hex strings to binary
 309  	if len(r.WriteFollowsWhitelist) > 0 {
 310  		r.writeFollowsWhitelistBin = make([][]byte, 0, len(r.WriteFollowsWhitelist))
 311  		for _, hexPubkey := range r.WriteFollowsWhitelist {
 312  			binPubkey, decErr := hex.Dec(hexPubkey)
 313  			if decErr != nil {
 314  				log.W.F("failed to decode WriteFollowsWhitelist pubkey %q: %v", hexPubkey, decErr)
 315  				continue
 316  			}
 317  			r.writeFollowsWhitelistBin = append(r.writeFollowsWhitelistBin, binPubkey)
 318  		}
 319  	}
 320  
 321  	// Convert WriteAllowIfTagged hex strings to binary
 322  	if len(r.WriteAllowIfTagged) > 0 {
 323  		r.writeAllowIfTaggedBin = make([][]byte, 0, len(r.WriteAllowIfTagged))
 324  		for _, hexPubkey := range r.WriteAllowIfTagged {
 325  			binPubkey, decErr := hex.Dec(hexPubkey)
 326  			if decErr != nil {
 327  				log.W.F("failed to decode WriteAllowIfTagged pubkey %q: %v", hexPubkey, decErr)
 328  				continue
 329  			}
 330  			r.writeAllowIfTaggedBin = append(r.writeAllowIfTaggedBin, binPubkey)
 331  		}
 332  	}
 333  
 334  	return err
 335  }
 336  
 337  // IsInFollowsWhitelist checks if the given pubkey is in this rule's follows whitelist.
 338  // The pubkey parameter should be binary ([]byte), not hex-encoded.
 339  func (r *Rule) IsInFollowsWhitelist(pubkey []byte) bool {
 340  	if len(pubkey) == 0 || len(r.followsWhitelistFollowsBin) == 0 {
 341  		return false
 342  	}
 343  	for _, follow := range r.followsWhitelistFollowsBin {
 344  		if utils.FastEqual(pubkey, follow) {
 345  			return true
 346  		}
 347  	}
 348  	return false
 349  }
 350  
 351  // UpdateFollowsWhitelist sets the follows list for this rule's FollowsWhitelistAdmins.
 352  // The follows should be binary pubkeys ([]byte), not hex-encoded.
 353  func (r *Rule) UpdateFollowsWhitelist(follows [][]byte) {
 354  	r.followsWhitelistFollowsBin = follows
 355  }
 356  
 357  // GetFollowsWhitelistAdminsBin returns the binary-encoded admin pubkeys for this rule.
 358  func (r *Rule) GetFollowsWhitelistAdminsBin() [][]byte {
 359  	return r.followsWhitelistAdminsBin
 360  }
 361  
 362  // HasFollowsWhitelistAdmins returns true if this rule has FollowsWhitelistAdmins configured.
 363  // DEPRECATED: Use HasReadFollowsWhitelist and HasWriteFollowsWhitelist instead.
 364  func (r *Rule) HasFollowsWhitelistAdmins() bool {
 365  	return len(r.FollowsWhitelistAdmins) > 0
 366  }
 367  
 368  // HasReadFollowsWhitelist returns true if this rule has ReadFollowsWhitelist configured.
 369  func (r *Rule) HasReadFollowsWhitelist() bool {
 370  	return len(r.ReadFollowsWhitelist) > 0
 371  }
 372  
 373  // HasWriteFollowsWhitelist returns true if this rule has WriteFollowsWhitelist configured.
 374  func (r *Rule) HasWriteFollowsWhitelist() bool {
 375  	return len(r.WriteFollowsWhitelist) > 0
 376  }
 377  
 378  // GetReadFollowsWhitelistBin returns the binary-encoded pubkeys for ReadFollowsWhitelist.
 379  func (r *Rule) GetReadFollowsWhitelistBin() [][]byte {
 380  	return r.readFollowsWhitelistBin
 381  }
 382  
 383  // GetWriteFollowsWhitelistBin returns the binary-encoded pubkeys for WriteFollowsWhitelist.
 384  func (r *Rule) GetWriteFollowsWhitelistBin() [][]byte {
 385  	return r.writeFollowsWhitelistBin
 386  }
 387  
 388  // UpdateReadFollowsWhitelist sets the follows list for this rule's ReadFollowsWhitelist.
 389  // The follows should be binary pubkeys ([]byte), not hex-encoded.
 390  func (r *Rule) UpdateReadFollowsWhitelist(follows [][]byte) {
 391  	r.readFollowsFollowsBin = follows
 392  }
 393  
 394  // UpdateWriteFollowsWhitelist sets the follows list for this rule's WriteFollowsWhitelist.
 395  // The follows should be binary pubkeys ([]byte), not hex-encoded.
 396  func (r *Rule) UpdateWriteFollowsWhitelist(follows [][]byte) {
 397  	r.writeFollowsFollowsBin = follows
 398  }
 399  
 400  // IsInReadFollowsWhitelist checks if the given pubkey is in this rule's read follows whitelist.
 401  // The pubkey parameter should be binary ([]byte), not hex-encoded.
 402  // Returns true if either:
 403  // 1. The pubkey is one of the ReadFollowsWhitelist pubkeys themselves, OR
 404  // 2. The pubkey is in the follows list of the ReadFollowsWhitelist pubkeys.
 405  func (r *Rule) IsInReadFollowsWhitelist(pubkey []byte) bool {
 406  	if len(pubkey) == 0 {
 407  		return false
 408  	}
 409  	// Check if pubkey is one of the whitelist pubkeys themselves
 410  	for _, wlPubkey := range r.readFollowsWhitelistBin {
 411  		if utils.FastEqual(pubkey, wlPubkey) {
 412  			return true
 413  		}
 414  	}
 415  	// Check if pubkey is in the follows list
 416  	for _, follow := range r.readFollowsFollowsBin {
 417  		if utils.FastEqual(pubkey, follow) {
 418  			return true
 419  		}
 420  	}
 421  	return false
 422  }
 423  
 424  // IsInWriteFollowsWhitelist checks if the given pubkey is in this rule's write follows whitelist.
 425  // The pubkey parameter should be binary ([]byte), not hex-encoded.
 426  // Returns true if either:
 427  // 1. The pubkey is one of the WriteFollowsWhitelist pubkeys themselves, OR
 428  // 2. The pubkey is in the follows list of the WriteFollowsWhitelist pubkeys.
 429  func (r *Rule) IsInWriteFollowsWhitelist(pubkey []byte) bool {
 430  	if len(pubkey) == 0 {
 431  		return false
 432  	}
 433  	// Check if pubkey is one of the whitelist pubkeys themselves
 434  	for _, wlPubkey := range r.writeFollowsWhitelistBin {
 435  		if utils.FastEqual(pubkey, wlPubkey) {
 436  			return true
 437  		}
 438  	}
 439  	// Check if pubkey is in the follows list
 440  	for _, follow := range r.writeFollowsFollowsBin {
 441  		if utils.FastEqual(pubkey, follow) {
 442  			return true
 443  		}
 444  	}
 445  	return false
 446  }
 447  
 448  // PolicyEvent represents an event with additional context for policy scripts.
 449  // It embeds the Nostr event and adds authentication and network context.
 450  type PolicyEvent struct {
 451  	*event.E
 452  	LoggedInPubkey string `json:"logged_in_pubkey,omitempty"`
 453  	IPAddress      string `json:"ip_address,omitempty"`
 454  	AccessType     string `json:"access_type,omitempty"` // "read" or "write"
 455  }
 456  
 457  // MarshalJSON implements custom JSON marshaling for PolicyEvent.
 458  // It safely serializes the embedded event and additional context fields.
 459  func (pe *PolicyEvent) MarshalJSON() ([]byte, error) {
 460  	if pe.E == nil {
 461  		return json.Marshal(
 462  			map[string]interface{}{
 463  				"logged_in_pubkey": pe.LoggedInPubkey,
 464  				"ip_address":       pe.IPAddress,
 465  			},
 466  		)
 467  	}
 468  
 469  	// Create a safe copy of the event for JSON marshaling
 470  	safeEvent := map[string]interface{}{
 471  		"id":         hex.Enc(pe.E.ID),
 472  		"pubkey":     hex.Enc(pe.E.Pubkey),
 473  		"created_at": pe.E.CreatedAt,
 474  		"kind":       pe.E.Kind,
 475  		"content":    string(pe.E.Content),
 476  		"tags":       pe.E.Tags,
 477  		"sig":        hex.Enc(pe.E.Sig),
 478  	}
 479  
 480  	// Add policy-specific fields
 481  	if pe.LoggedInPubkey != "" {
 482  		safeEvent["logged_in_pubkey"] = pe.LoggedInPubkey
 483  	}
 484  	if pe.IPAddress != "" {
 485  		safeEvent["ip_address"] = pe.IPAddress
 486  	}
 487  	if pe.AccessType != "" {
 488  		safeEvent["access_type"] = pe.AccessType
 489  	}
 490  
 491  	return json.Marshal(safeEvent)
 492  }
 493  
 494  // PolicyResponse represents a response from the policy script.
 495  // The script should return JSON with these fields to indicate its decision.
 496  type PolicyResponse struct {
 497  	ID     string `json:"id"`
 498  	Action string `json:"action"` // accept, reject, or shadowReject
 499  	Msg    string `json:"msg"`    // NIP-20 response message (only used for reject)
 500  }
 501  
 502  // ScriptRunner manages a single policy script process.
 503  // Each unique script path gets its own independent runner with its own goroutine.
 504  type ScriptRunner struct {
 505  	ctx           context.Context
 506  	cancel        context.CancelFunc
 507  	configDir     string
 508  	scriptPath    string
 509  	currentCmd    *exec.Cmd
 510  	currentCancel context.CancelFunc
 511  	mutex         sync.RWMutex
 512  	isRunning     bool
 513  	isStarting    bool
 514  	stdin         io.WriteCloser
 515  	stdout        io.ReadCloser
 516  	stderr        io.ReadCloser
 517  	responseChan  chan PolicyResponse
 518  	startupChan   chan error
 519  }
 520  
 521  // PolicyManager handles multiple policy script runners.
 522  // It manages the lifecycle of policy scripts, handles communication with them,
 523  // and provides resilient operation with automatic restart capabilities.
 524  // Each unique script path gets its own ScriptRunner instance.
 525  type PolicyManager struct {
 526  	ctx        context.Context
 527  	cancel     context.CancelFunc
 528  	configDir  string
 529  	configPath string // Path to policy.json file
 530  	scriptPath string // Default script path for backward compatibility
 531  	enabled    bool
 532  	mutex      sync.RWMutex
 533  	runners    map[string]*ScriptRunner // Map of script path -> runner
 534  }
 535  
 536  // ConfigPath returns the path to the policy configuration file.
 537  // This is used by hot-reload handlers to know where to save updated policy.
 538  func (pm *PolicyManager) ConfigPath() string {
 539  	return pm.configPath
 540  }
 541  
 542  // P represents a complete policy configuration for a Nostr relay.
 543  // It defines access control rules, kind filtering, and default behavior.
 544  // Policies are evaluated in order: global rules, kind filtering, specific rules, then default policy.
 545  type P struct {
 546  	// Kind is policies for accepting or rejecting events by kind number.
 547  	Kind Kinds `json:"kind"`
 548  	// rules is a map of rules for criteria that must be met for the event to be allowed to be written to the relay.
 549  	// Unexported to enforce use of public API methods (CheckPolicy, IsEnabled).
 550  	rules map[int]Rule
 551  	// Global is a rule set that applies to all events.
 552  	Global Rule `json:"global"`
 553  	// DefaultPolicy determines the default behavior when no rules deny an event ("allow" or "deny", defaults to "allow")
 554  	DefaultPolicy string `json:"default_policy"`
 555  
 556  	// PolicyAdmins is a list of hex-encoded pubkeys that can update policy configuration via kind 12345 events.
 557  	// These are SEPARATE from ACL relay admins - policy admins manage policy only.
 558  	PolicyAdmins []string `json:"policy_admins,omitempty"`
 559  	// PolicyFollowWhitelistEnabled enables automatic whitelisting of pubkeys followed by policy admins.
 560  	// When true and a rule has WriteAllowFollows=true, policy admin follows get read+write access.
 561  	PolicyFollowWhitelistEnabled bool `json:"policy_follow_whitelist_enabled,omitempty"`
 562  
 563  	// Owners is a list of hex-encoded pubkeys that have full control of the relay.
 564  	// These are merged with owners from the ORLY_OWNERS environment variable.
 565  	// Useful for cloud deployments where environment variables cannot be modified.
 566  	Owners []string `json:"owners,omitempty"`
 567  
 568  	// Unexported binary caches for faster comparison (populated from hex strings above)
 569  	policyAdminsBin [][]byte // Binary cache for policy admin pubkeys
 570  	policyFollows   [][]byte // Cached follow list from policy admins (kind 3 events)
 571  	ownersBin       [][]byte // Binary cache for policy-defined owner pubkeys
 572  
 573  	// followsMx protects all follows-related caches from concurrent access.
 574  	// This includes policyFollows, Global.readFollowsFollowsBin, Global.writeFollowsFollowsBin,
 575  	// and rule-specific follows whitelists.
 576  	// Use RLock for reads (CheckPolicy) and Lock for writes (Update*Follows*).
 577  	followsMx sync.RWMutex
 578  
 579  	// manager handles policy script execution.
 580  	// Unexported to enforce use of public API methods (CheckPolicy, IsEnabled).
 581  	manager *PolicyManager
 582  }
 583  
 584  // pJSON is a shadow struct for JSON unmarshalling with exported fields.
 585  type pJSON struct {
 586  	Kind                         Kinds        `json:"kind"`
 587  	Rules                        map[int]Rule `json:"rules"`
 588  	Global                       Rule         `json:"global"`
 589  	DefaultPolicy                string       `json:"default_policy"`
 590  	PolicyAdmins                 []string     `json:"policy_admins,omitempty"`
 591  	PolicyFollowWhitelistEnabled bool         `json:"policy_follow_whitelist_enabled,omitempty"`
 592  	Owners                       []string     `json:"owners,omitempty"`
 593  }
 594  
 595  // UnmarshalJSON implements custom JSON unmarshalling to handle unexported fields.
 596  func (p *P) UnmarshalJSON(data []byte) error {
 597  	var shadow pJSON
 598  	if err := json.Unmarshal(data, &shadow); err != nil {
 599  		return err
 600  	}
 601  	p.Kind = shadow.Kind
 602  	p.rules = shadow.Rules
 603  	p.Global = shadow.Global
 604  	p.DefaultPolicy = shadow.DefaultPolicy
 605  	p.PolicyAdmins = shadow.PolicyAdmins
 606  	p.PolicyFollowWhitelistEnabled = shadow.PolicyFollowWhitelistEnabled
 607  	p.Owners = shadow.Owners
 608  
 609  	// Populate binary cache for policy admins
 610  	if len(p.PolicyAdmins) > 0 {
 611  		p.policyAdminsBin = make([][]byte, 0, len(p.PolicyAdmins))
 612  		for _, hexPubkey := range p.PolicyAdmins {
 613  			binPubkey, err := hex.Dec(hexPubkey)
 614  			if err != nil {
 615  				log.W.F("failed to decode PolicyAdmin pubkey %q: %v", hexPubkey, err)
 616  				continue
 617  			}
 618  			p.policyAdminsBin = append(p.policyAdminsBin, binPubkey)
 619  		}
 620  	}
 621  
 622  	// Populate binary cache for policy-defined owners
 623  	if len(p.Owners) > 0 {
 624  		p.ownersBin = make([][]byte, 0, len(p.Owners))
 625  		for _, hexPubkey := range p.Owners {
 626  			binPubkey, err := hex.Dec(hexPubkey)
 627  			if err != nil {
 628  				log.W.F("failed to decode owner pubkey %q: %v", hexPubkey, err)
 629  				continue
 630  			}
 631  			p.ownersBin = append(p.ownersBin, binPubkey)
 632  		}
 633  	}
 634  
 635  	return nil
 636  }
 637  
 638  // New creates a new policy from JSON configuration.
 639  // If policyJSON is empty, returns a policy with default settings.
 640  // The default_policy field defaults to "allow" if not specified.
 641  // Returns an error if the policy JSON contains invalid values (e.g., invalid
 642  // ISO-8601 duration format for max_expiry_duration, invalid regex patterns, etc.).
 643  func New(policyJSON []byte) (p *P, err error) {
 644  	p = &P{
 645  		DefaultPolicy: "allow", // Set default value
 646  	}
 647  	if len(policyJSON) > 0 {
 648  		// Validate JSON before loading to fail fast on invalid configurations.
 649  		// This prevents silent failures where invalid values (like "T10M" instead
 650  		// of "PT10M" for max_expiry_duration) are ignored and constraints don't apply.
 651  		if err = p.ValidateJSON(policyJSON); err != nil {
 652  			return nil, fmt.Errorf("policy validation failed: %v", err)
 653  		}
 654  		if err = json.Unmarshal(policyJSON, p); chk.E(err) {
 655  			return nil, fmt.Errorf("failed to unmarshal policy JSON: %v", err)
 656  		}
 657  	}
 658  	// Ensure default policy is valid
 659  	if p.DefaultPolicy == "" {
 660  		p.DefaultPolicy = "allow"
 661  	}
 662  
 663  	// Populate binary caches for all rules (including global rule)
 664  	p.Global.populateBinaryCache()
 665  	for kind := range p.rules {
 666  		rule := p.rules[kind] // Get a copy
 667  		rule.populateBinaryCache()
 668  		p.rules[kind] = rule // Store the modified copy back
 669  	}
 670  
 671  	return
 672  }
 673  
 674  // IsPartyInvolved checks if the given pubkey is a party involved in the event.
 675  // A party is involved if they are either:
 676  // 1. The author of the event (ev.Pubkey == userPubkey)
 677  // 2. Mentioned in a p-tag of the event
 678  //
 679  // Both ev.Pubkey and userPubkey must be binary ([]byte), not hex-encoded.
 680  // P-tags may be stored in either binary-optimized format (33 bytes) or hex format.
 681  //
 682  // This is the single source of truth for "parties_involved" / "privileged" checks.
 683  func IsPartyInvolved(ev *event.E, userPubkey []byte) bool {
 684  	// Must be authenticated
 685  	if len(userPubkey) == 0 {
 686  		return false
 687  	}
 688  
 689  	// Check if user is the author
 690  	if bytes.Equal(ev.Pubkey, userPubkey) {
 691  		return true
 692  	}
 693  
 694  	// Check if user is in p tags
 695  	pTags := ev.Tags.GetAll([]byte("p"))
 696  	for _, pTag := range pTags {
 697  		// ValueHex() handles both binary and hex storage formats automatically
 698  		pt, err := hex.Dec(string(pTag.ValueHex()))
 699  		if err != nil {
 700  			// Skip malformed tags
 701  			continue
 702  		}
 703  		if bytes.Equal(pt, userPubkey) {
 704  			return true
 705  		}
 706  	}
 707  
 708  	return false
 709  }
 710  
 711  // IsEnabled returns whether the policy system is enabled and ready to process events.
 712  // This is the public API for checking if policy filtering should be applied.
 713  func (p *P) IsEnabled() bool {
 714  	return p != nil && p.manager != nil && p.manager.IsEnabled()
 715  }
 716  
 717  // ConfigPath returns the path to the policy configuration file.
 718  // Delegates to the internal PolicyManager.
 719  func (p *P) ConfigPath() string {
 720  	if p == nil || p.manager == nil {
 721  		return ""
 722  	}
 723  	return p.manager.ConfigPath()
 724  }
 725  
 726  // getDefaultPolicyAction returns true if the default policy is "allow", false if "deny"
 727  func (p *P) getDefaultPolicyAction() (allowed bool) {
 728  	switch p.DefaultPolicy {
 729  	case "deny":
 730  		return false
 731  	case "allow", "":
 732  		return true
 733  	default:
 734  		// Invalid value, default to allow
 735  		return true
 736  	}
 737  }
 738  
 739  // NewWithManager creates a new policy with a policy manager for script execution.
 740  // It initializes the policy manager, loads configuration from files, and starts
 741  // background processes for script management and periodic health checks.
 742  //
 743  // The customPolicyPath parameter allows overriding the default policy file location.
 744  // If empty, uses the default path: $HOME/.config/{appName}/policy.json
 745  // If provided, it MUST be an absolute path (starting with /) or the function will panic.
 746  func NewWithManager(ctx context.Context, appName string, enabled bool, customPolicyPath string) *P {
 747  	configDir := filepath.Join(xdg.ConfigHome, appName)
 748  	scriptPath := filepath.Join(configDir, "policy.sh")
 749  
 750  	// Determine the policy config path
 751  	var configPath string
 752  	if customPolicyPath != "" {
 753  		// Validate that custom path is absolute
 754  		if !filepath.IsAbs(customPolicyPath) {
 755  			panic(fmt.Sprintf("FATAL: ORLY_POLICY_PATH must be an ABSOLUTE path (starting with /), got: %q", customPolicyPath))
 756  		}
 757  		configPath = customPolicyPath
 758  		// Update configDir to match the custom path's directory for script resolution
 759  		configDir = filepath.Dir(customPolicyPath)
 760  		scriptPath = filepath.Join(configDir, "policy.sh")
 761  		log.I.F("using custom policy path: %s", configPath)
 762  	} else {
 763  		configPath = filepath.Join(configDir, "policy.json")
 764  	}
 765  
 766  	ctx, cancel := context.WithCancel(ctx)
 767  
 768  	manager := &PolicyManager{
 769  		ctx:        ctx,
 770  		cancel:     cancel,
 771  		configDir:  configDir,
 772  		configPath: configPath,
 773  		scriptPath: scriptPath,
 774  		enabled:    enabled,
 775  		runners:    make(map[string]*ScriptRunner),
 776  	}
 777  
 778  	// Load policy configuration from JSON file
 779  	policy := &P{
 780  		DefaultPolicy: "allow", // Set default value
 781  		manager:       manager,
 782  	}
 783  
 784  	if enabled {
 785  		if err := policy.LoadFromFile(configPath); err != nil {
 786  			log.W.F(
 787  				"policy enabled but config failed to load from %s: %v — disabling policy",
 788  				configPath, err,
 789  			)
 790  		} else {
 791  			log.I.F("loaded policy configuration from %s", configPath)
 792  
 793  			// Start the policy script if it exists and is enabled
 794  			go manager.startPolicyIfExists()
 795  			// Start periodic check for policy script availability
 796  			go manager.periodicCheck()
 797  		}
 798  	}
 799  
 800  	return policy
 801  }
 802  
 803  // getOrCreateRunner gets an existing runner for the script path or creates a new one.
 804  // This method is thread-safe and ensures only one runner exists per unique script path.
 805  func (pm *PolicyManager) getOrCreateRunner(scriptPath string) *ScriptRunner {
 806  	pm.mutex.Lock()
 807  	defer pm.mutex.Unlock()
 808  
 809  	// Check if runner already exists
 810  	if runner, exists := pm.runners[scriptPath]; exists {
 811  		return runner
 812  	}
 813  
 814  	// Create new runner
 815  	runnerCtx, runnerCancel := context.WithCancel(pm.ctx)
 816  	runner := &ScriptRunner{
 817  		ctx:          runnerCtx,
 818  		cancel:       runnerCancel,
 819  		configDir:    pm.configDir,
 820  		scriptPath:   scriptPath,
 821  		responseChan: make(chan PolicyResponse, 100),
 822  		startupChan:  make(chan error, 1),
 823  	}
 824  
 825  	pm.runners[scriptPath] = runner
 826  
 827  	// Start periodic check for this runner
 828  	go runner.periodicCheck()
 829  
 830  	return runner
 831  }
 832  
 833  // ScriptRunner methods
 834  
 835  // IsRunning returns whether the script is currently running.
 836  func (sr *ScriptRunner) IsRunning() bool {
 837  	sr.mutex.RLock()
 838  	defer sr.mutex.RUnlock()
 839  	return sr.isRunning
 840  }
 841  
 842  // ensureRunning ensures the script is running, starting it if necessary.
 843  func (sr *ScriptRunner) ensureRunning() error {
 844  	sr.mutex.Lock()
 845  	// Check if already running
 846  	if sr.isRunning {
 847  		sr.mutex.Unlock()
 848  		return nil
 849  	}
 850  
 851  	// Check if already starting
 852  	if sr.isStarting {
 853  		sr.mutex.Unlock()
 854  		// Wait for startup to complete
 855  		select {
 856  		case err := <-sr.startupChan:
 857  			if err != nil {
 858  				return fmt.Errorf("script startup failed: %v", err)
 859  			}
 860  			// Double-check it's actually running after receiving signal
 861  			sr.mutex.RLock()
 862  			running := sr.isRunning
 863  			sr.mutex.RUnlock()
 864  			if !running {
 865  				return fmt.Errorf("script startup completed but process is not running")
 866  			}
 867  			return nil
 868  		case <-time.After(10 * time.Second):
 869  			return fmt.Errorf("script startup timeout")
 870  		case <-sr.ctx.Done():
 871  			return fmt.Errorf("script context cancelled")
 872  		}
 873  	}
 874  
 875  	// Mark as starting
 876  	sr.isStarting = true
 877  	sr.mutex.Unlock()
 878  
 879  	// Start the script in a goroutine
 880  	go func() {
 881  		err := sr.Start()
 882  		sr.mutex.Lock()
 883  		sr.isStarting = false
 884  		sr.mutex.Unlock()
 885  		// Signal startup completion (non-blocking)
 886  		// Drain any stale value first, then send
 887  		select {
 888  		case <-sr.startupChan:
 889  		default:
 890  		}
 891  		select {
 892  		case sr.startupChan <- err:
 893  		default:
 894  			// Channel should be empty now, but if it's full, try again
 895  			sr.startupChan <- err
 896  		}
 897  	}()
 898  
 899  	// Wait for startup to complete
 900  	select {
 901  	case err := <-sr.startupChan:
 902  		if err != nil {
 903  			return fmt.Errorf("script startup failed: %v", err)
 904  		}
 905  		// Double-check it's actually running after receiving signal
 906  		sr.mutex.RLock()
 907  		running := sr.isRunning
 908  		sr.mutex.RUnlock()
 909  		if !running {
 910  			return fmt.Errorf("script startup completed but process is not running")
 911  		}
 912  		return nil
 913  	case <-time.After(10 * time.Second):
 914  		sr.mutex.Lock()
 915  		sr.isStarting = false
 916  		sr.mutex.Unlock()
 917  		return fmt.Errorf("script startup timeout")
 918  	case <-sr.ctx.Done():
 919  		sr.mutex.Lock()
 920  		sr.isStarting = false
 921  		sr.mutex.Unlock()
 922  		return fmt.Errorf("script context cancelled")
 923  	}
 924  }
 925  
 926  // Start starts the script process.
 927  func (sr *ScriptRunner) Start() error {
 928  	sr.mutex.Lock()
 929  	defer sr.mutex.Unlock()
 930  
 931  	if sr.isRunning {
 932  		return fmt.Errorf("script is already running")
 933  	}
 934  
 935  	if _, err := os.Stat(sr.scriptPath); os.IsNotExist(err) {
 936  		return fmt.Errorf("script does not exist at %s", sr.scriptPath)
 937  	}
 938  
 939  	// Create a new context for this command
 940  	cmdCtx, cmdCancel := context.WithCancel(sr.ctx)
 941  
 942  	// Make the script executable
 943  	if err := os.Chmod(sr.scriptPath, 0755); chk.E(err) {
 944  		cmdCancel()
 945  		return fmt.Errorf("failed to make script executable: %v", err)
 946  	}
 947  
 948  	// Start the script
 949  	cmd := exec.CommandContext(cmdCtx, sr.scriptPath)
 950  	cmd.Dir = sr.configDir
 951  
 952  	// Set up stdio pipes for communication
 953  	stdin, err := cmd.StdinPipe()
 954  	if chk.E(err) {
 955  		cmdCancel()
 956  		return fmt.Errorf("failed to create stdin pipe: %v", err)
 957  	}
 958  
 959  	stdout, err := cmd.StdoutPipe()
 960  	if chk.E(err) {
 961  		cmdCancel()
 962  		stdin.Close()
 963  		return fmt.Errorf("failed to create stdout pipe: %v", err)
 964  	}
 965  
 966  	stderr, err := cmd.StderrPipe()
 967  	if chk.E(err) {
 968  		cmdCancel()
 969  		stdin.Close()
 970  		stdout.Close()
 971  		return fmt.Errorf("failed to create stderr pipe: %v", err)
 972  	}
 973  
 974  	// Start the command
 975  	if err := cmd.Start(); chk.E(err) {
 976  		cmdCancel()
 977  		stdin.Close()
 978  		stdout.Close()
 979  		stderr.Close()
 980  		return fmt.Errorf("failed to start script: %v", err)
 981  	}
 982  
 983  	sr.currentCmd = cmd
 984  	sr.currentCancel = cmdCancel
 985  	sr.stdin = stdin
 986  	sr.stdout = stdout
 987  	sr.stderr = stderr
 988  	sr.isRunning = true
 989  
 990  	// Start response reader in background
 991  	go sr.readResponses()
 992  
 993  	// Log stderr output in background
 994  	go sr.logOutput(stdout, stderr)
 995  
 996  	// Monitor the process
 997  	go sr.monitorProcess()
 998  
 999  	log.I.F(
1000  		"policy script started: %s (pid=%d)", sr.scriptPath, cmd.Process.Pid,
1001  	)
1002  	return nil
1003  }
1004  
1005  // Stop stops the script gracefully.
1006  func (sr *ScriptRunner) Stop() error {
1007  	sr.mutex.Lock()
1008  
1009  	if !sr.isRunning || sr.currentCmd == nil {
1010  		sr.mutex.Unlock()
1011  		return fmt.Errorf("script is not running")
1012  	}
1013  
1014  	// Close stdin first to signal the script to exit
1015  	if sr.stdin != nil {
1016  		sr.stdin.Close()
1017  	}
1018  
1019  	// Cancel the context
1020  	if sr.currentCancel != nil {
1021  		sr.currentCancel()
1022  	}
1023  
1024  	// Get the process reference before releasing the lock
1025  	process := sr.currentCmd.Process
1026  	sr.mutex.Unlock()
1027  
1028  	// Wait for graceful shutdown with timeout
1029  	// Note: monitorProcess() is the one that calls cmd.Wait() and cleans up
1030  	// We just wait for it to finish by polling isRunning
1031  	gracefulShutdown := false
1032  	for i := 0; i < 50; i++ { // 5 seconds total (50 * 100ms)
1033  		time.Sleep(100 * time.Millisecond)
1034  		sr.mutex.RLock()
1035  		running := sr.isRunning
1036  		sr.mutex.RUnlock()
1037  		if !running {
1038  			gracefulShutdown = true
1039  			log.I.F("policy script stopped gracefully: %s", sr.scriptPath)
1040  			break
1041  		}
1042  	}
1043  
1044  	if !gracefulShutdown {
1045  		// Force kill after timeout
1046  		log.W.F(
1047  			"policy script did not stop gracefully, sending SIGKILL: %s",
1048  			sr.scriptPath,
1049  		)
1050  		if process != nil {
1051  			if err := process.Kill(); chk.E(err) {
1052  				log.E.F("failed to kill script process: %v", err)
1053  			}
1054  		}
1055  
1056  		// Wait a bit more for monitorProcess to clean up
1057  		for i := 0; i < 30; i++ { // 3 more seconds
1058  			time.Sleep(100 * time.Millisecond)
1059  			sr.mutex.RLock()
1060  			running := sr.isRunning
1061  			sr.mutex.RUnlock()
1062  			if !running {
1063  				break
1064  			}
1065  		}
1066  	}
1067  
1068  	return nil
1069  }
1070  
1071  // ProcessEvent sends an event to the script and waits for a response.
1072  func (sr *ScriptRunner) ProcessEvent(evt *PolicyEvent) (
1073  	*PolicyResponse, error,
1074  ) {
1075  	log.D.F("processing event: %s", evt.Serialize())
1076  	sr.mutex.RLock()
1077  	if !sr.isRunning || sr.stdin == nil {
1078  		sr.mutex.RUnlock()
1079  		return nil, fmt.Errorf("script is not running")
1080  	}
1081  	stdin := sr.stdin
1082  	sr.mutex.RUnlock()
1083  
1084  	// Serialize the event to JSON
1085  	eventJSON, err := json.Marshal(evt)
1086  	if chk.E(err) {
1087  		return nil, fmt.Errorf("failed to serialize event: %v", err)
1088  	}
1089  
1090  	// Send the event JSON to the script (newline-terminated)
1091  	if _, err := stdin.Write(append(eventJSON, '\n')); chk.E(err) {
1092  		// Check if it's a broken pipe error, which means the script has died
1093  		if strings.Contains(err.Error(), "broken pipe") || strings.Contains(err.Error(), "closed pipe") {
1094  			log.E.F(
1095  				"policy script %s stdin closed (broken pipe) - script may have crashed or exited prematurely",
1096  				sr.scriptPath,
1097  			)
1098  			// Mark as not running so it will be restarted on next periodic check
1099  			sr.mutex.Lock()
1100  			sr.isRunning = false
1101  			sr.mutex.Unlock()
1102  		}
1103  		return nil, fmt.Errorf("failed to write event to script: %v", err)
1104  	}
1105  
1106  	// Wait for response with timeout
1107  	select {
1108  	case response := <-sr.responseChan:
1109  		log.D.S("response", response)
1110  		return &response, nil
1111  	case <-time.After(5 * time.Second):
1112  		log.W.F(
1113  			"policy script %s response timeout - script may not be responding correctly (check for debug output on stdout)",
1114  			sr.scriptPath,
1115  		)
1116  		return nil, fmt.Errorf("script response timeout")
1117  	case <-sr.ctx.Done():
1118  		return nil, fmt.Errorf("script context cancelled")
1119  	}
1120  }
1121  
1122  // readResponses reads JSONL responses from the script
1123  func (sr *ScriptRunner) readResponses() {
1124  	if sr.stdout == nil {
1125  		return
1126  	}
1127  
1128  	scanner := bufio.NewScanner(sr.stdout)
1129  	nonJSONLineCount := 0
1130  	for scanner.Scan() {
1131  		line := scanner.Text()
1132  		if line == "" {
1133  			continue
1134  		}
1135  		log.D.F("policy response: %s", line)
1136  		var response PolicyResponse
1137  		if err := json.Unmarshal([]byte(line), &response); chk.E(err) {
1138  			// Check if this looks like debug output
1139  			if strings.HasPrefix(line, "{") {
1140  				// Looks like JSON but failed to parse
1141  				log.E.F(
1142  					"failed to parse policy response from %s: %v\nLine: %s",
1143  					sr.scriptPath, err, line,
1144  				)
1145  			} else {
1146  				// Definitely not JSON - probably debug output
1147  				nonJSONLineCount++
1148  				if nonJSONLineCount <= 3 {
1149  					log.W.F(
1150  						"policy script %s produced non-JSON output on stdout (should only output JSONL): %q",
1151  						sr.scriptPath, line,
1152  					)
1153  				} else if nonJSONLineCount == 4 {
1154  					log.W.F(
1155  						"policy script %s continues to produce non-JSON output - suppressing further warnings",
1156  						sr.scriptPath,
1157  					)
1158  				}
1159  				log.W.F(
1160  					"IMPORTANT: Policy scripts must ONLY write JSON responses to stdout. Use stderr or a log file for debug output.",
1161  				)
1162  			}
1163  			continue
1164  		}
1165  
1166  		// Send response to channel (non-blocking)
1167  		select {
1168  		case sr.responseChan <- response:
1169  		default:
1170  			log.W.F(
1171  				"policy response channel full for %s, dropping response",
1172  				sr.scriptPath,
1173  			)
1174  		}
1175  	}
1176  
1177  	if err := scanner.Err(); chk.E(err) {
1178  		log.E.F(
1179  			"error reading policy responses from %s: %v", sr.scriptPath, err,
1180  		)
1181  	}
1182  }
1183  
1184  // logOutput logs the output from stderr
1185  func (sr *ScriptRunner) logOutput(_ /* stdout */, stderr io.ReadCloser) {
1186  	defer stderr.Close()
1187  
1188  	// Only log stderr, stdout is used by readResponses
1189  	go func() {
1190  		scanner := bufio.NewScanner(stderr)
1191  		for scanner.Scan() {
1192  			line := scanner.Text()
1193  			if line != "" {
1194  				// Log script stderr output through relay logging system
1195  				log.I.F("[policy script %s] %s", sr.scriptPath, line)
1196  			}
1197  		}
1198  		if err := scanner.Err(); chk.E(err) {
1199  			log.E.F("error reading stderr from policy script %s: %v", sr.scriptPath, err)
1200  		}
1201  	}()
1202  }
1203  
1204  // monitorProcess monitors the script process and cleans up when it exits
1205  func (sr *ScriptRunner) monitorProcess() {
1206  	if sr.currentCmd == nil {
1207  		return
1208  	}
1209  
1210  	err := sr.currentCmd.Wait()
1211  
1212  	sr.mutex.Lock()
1213  	defer sr.mutex.Unlock()
1214  
1215  	// Clean up pipes
1216  	if sr.stdin != nil {
1217  		sr.stdin.Close()
1218  		sr.stdin = nil
1219  	}
1220  	if sr.stdout != nil {
1221  		sr.stdout.Close()
1222  		sr.stdout = nil
1223  	}
1224  	if sr.stderr != nil {
1225  		sr.stderr.Close()
1226  		sr.stderr = nil
1227  	}
1228  
1229  	sr.isRunning = false
1230  	sr.currentCmd = nil
1231  	sr.currentCancel = nil
1232  
1233  	if err != nil {
1234  		log.E.F(
1235  			"policy script exited with error: %s: %v, will retry periodically",
1236  			sr.scriptPath, err,
1237  		)
1238  	} else {
1239  		log.I.F("policy script exited normally: %s", sr.scriptPath)
1240  	}
1241  }
1242  
1243  // periodicCheck periodically checks if script becomes available and attempts to restart failed scripts.
1244  func (sr *ScriptRunner) periodicCheck() {
1245  	ticker := time.NewTicker(60 * time.Second)
1246  	defer ticker.Stop()
1247  
1248  	for {
1249  		select {
1250  		case <-sr.ctx.Done():
1251  			return
1252  		case <-ticker.C:
1253  			sr.mutex.RLock()
1254  			running := sr.isRunning
1255  			sr.mutex.RUnlock()
1256  
1257  			// Check if script is not running and try to start it
1258  			if !running {
1259  				if _, err := os.Stat(sr.scriptPath); err == nil {
1260  					// Script exists but not running, try to start
1261  					go func() {
1262  						if err := sr.Start(); err != nil {
1263  							log.E.F(
1264  								"failed to restart policy script %s: %v, will retry in next cycle",
1265  								sr.scriptPath, err,
1266  							)
1267  						} else {
1268  							log.I.F(
1269  								"policy script restarted successfully: %s",
1270  								sr.scriptPath,
1271  							)
1272  						}
1273  					}()
1274  				}
1275  			}
1276  		}
1277  	}
1278  }
1279  
1280  // LoadFromFile loads policy configuration from a JSON file.
1281  // Returns an error if the file doesn't exist, can't be read, or contains invalid JSON.
1282  func (p *P) LoadFromFile(configPath string) error {
1283  	if _, err := os.Stat(configPath); os.IsNotExist(err) {
1284  		return fmt.Errorf(
1285  			"policy configuration file does not exist: %s", configPath,
1286  		)
1287  	}
1288  
1289  	configData, err := os.ReadFile(configPath)
1290  	if err != nil {
1291  		return fmt.Errorf("failed to read policy configuration file: %v", err)
1292  	}
1293  
1294  	if len(configData) == 0 {
1295  		return fmt.Errorf("policy configuration file is empty")
1296  	}
1297  
1298  	if err := json.Unmarshal(configData, p); err != nil {
1299  		return fmt.Errorf("failed to parse policy configuration JSON: %v", err)
1300  	}
1301  
1302  	// Populate binary caches for all rules (including global rule)
1303  	p.Global.populateBinaryCache()
1304  	for kind, rule := range p.rules {
1305  		rule.populateBinaryCache()
1306  		p.rules[kind] = rule // Update the map with the modified rule
1307  	}
1308  
1309  	return nil
1310  }
1311  
1312  // CheckPolicy checks if an event is allowed based on the policy configuration.
1313  // The access parameter should be "write" for accepting events or "read" for filtering events.
1314  // Returns true if the event is allowed, false if denied, and an error if validation fails.
1315  //
1316  // Policy evaluation order (more specific rules take precedence):
1317  // 1. Kinds whitelist/blacklist - if kind is blocked, deny immediately
1318  // 2. Kind-specific rule - if exists for this kind, use it exclusively
1319  // 3. Global rule - fallback if no kind-specific rule exists
1320  // 4. Default policy - fallback if no rules apply
1321  //
1322  // Thread-safety: Uses followsMx.RLock to protect reads of follows whitelists during policy checks.
1323  // Write operations (Update*) acquire the write lock, which blocks concurrent reads.
1324  func (p *P) CheckPolicy(
1325  	access string, ev *event.E, loggedInPubkey []byte, ipAddress string,
1326  ) (allowed bool, err error) {
1327  	// Handle nil policy - this should not happen if policy is enabled
1328  	// If policy is enabled but p is nil, it's a configuration error
1329  	if p == nil {
1330  		log.F.Ln("FATAL: CheckPolicy called on nil policy - this indicates misconfiguration. " +
1331  			"If ORLY_POLICY_ENABLED=true, ensure policy configuration is valid.")
1332  		return false, fmt.Errorf("policy is nil but policy checking is enabled - check configuration")
1333  	}
1334  
1335  	// Handle nil event
1336  	if ev == nil {
1337  		return false, fmt.Errorf("event cannot be nil")
1338  	}
1339  
1340  	// Acquire read lock to protect follows whitelists during policy check
1341  	p.followsMx.RLock()
1342  	defer p.followsMx.RUnlock()
1343  
1344  	// ==========================================================================
1345  	// STEP 1: Check kinds whitelist/blacklist (applies before any rule checks)
1346  	// ==========================================================================
1347  	if !p.checkKindsPolicy(access, ev.Kind) {
1348  		return false, nil
1349  	}
1350  
1351  	// ==========================================================================
1352  	// STEP 2: Check KIND-SPECIFIC rule FIRST (more specific = higher priority)
1353  	// ==========================================================================
1354  	// If kind-specific rule exists and accepts, that's final - global is ignored.
1355  	rule, hasKindRule := p.rules[int(ev.Kind)]
1356  	if hasKindRule {
1357  		// Check if script is present and enabled for this kind
1358  		if rule.Script != "" && p.manager != nil {
1359  			if p.manager.IsEnabled() {
1360  				// Check if script file exists before trying to use it
1361  				if _, err := os.Stat(rule.Script); err == nil {
1362  					// Script exists, try to use it
1363  					log.D.F("using policy script for kind %d: %s", ev.Kind, rule.Script)
1364  					allowed, err := p.checkScriptPolicy(
1365  						access, ev, rule.Script, loggedInPubkey, ipAddress,
1366  					)
1367  					if err == nil {
1368  						// Script ran successfully, return its decision
1369  						return allowed, nil
1370  					}
1371  					// Script failed, fall through to apply other criteria
1372  					log.W.F("policy script check failed for kind %d: %v, applying other criteria",
1373  						ev.Kind, err)
1374  				} else {
1375  					// Script configured but doesn't exist
1376  					log.W.F("policy script configured for kind %d but not found at %s: %v, applying other criteria",
1377  						ev.Kind, rule.Script, err)
1378  				}
1379  				// Script doesn't exist or failed, fall through to apply other criteria
1380  			} else {
1381  				// Policy manager is disabled, fall back to default policy
1382  				log.D.F("policy manager is disabled for kind %d, falling back to default policy (%s)",
1383  					ev.Kind, p.DefaultPolicy)
1384  				return p.getDefaultPolicyAction(), nil
1385  			}
1386  		}
1387  
1388  		// Apply kind-specific rule-based filtering
1389  		return p.checkRulePolicy(access, ev, rule, loggedInPubkey)
1390  	}
1391  
1392  	// ==========================================================================
1393  	// STEP 3: No kind-specific rule - check GLOBAL rule as fallback
1394  	// ==========================================================================
1395  
1396  	// Check if global rule has any configuration
1397  	if p.Global.hasAnyRules() {
1398  		// Apply global rule filtering
1399  		return p.checkRulePolicy(access, ev, p.Global, loggedInPubkey)
1400  	}
1401  
1402  	// ==========================================================================
1403  	// STEP 4: No kind-specific or global rules - use default policy
1404  	// ==========================================================================
1405  	return p.getDefaultPolicyAction(), nil
1406  }
1407  
1408  // checkKindsPolicy checks if the event kind is allowed for the given access type.
1409  // Logic:
1410  // 1. If explicit whitelist exists, use it (but respect permissive flags for read/write)
1411  // 2. If explicit blacklist exists, use it (but respect permissive flags for read/write)
1412  // 3. Otherwise, kinds with defined rules are implicitly allowed, others denied (with permissive overrides)
1413  //
1414  // Permissive flags (set on Global rule):
1415  // - ReadAllowPermissive: Allows READ access for kinds not in whitelist (write still restricted)
1416  // - WriteAllowPermissive: Allows WRITE access for kinds not in whitelist (uses global rule constraints)
1417  func (p *P) checkKindsPolicy(access string, kind uint16) bool {
1418  	// If whitelist is present, only allow whitelisted kinds (with permissive overrides)
1419  	if len(p.Kind.Whitelist) > 0 {
1420  		for _, allowedKind := range p.Kind.Whitelist {
1421  			if kind == uint16(allowedKind) {
1422  				return true
1423  			}
1424  		}
1425  		// Kind not in whitelist - check permissive flags
1426  		if access == "read" && p.Global.ReadAllowPermissive {
1427  			log.D.F("read_allow_permissive: allowing read for kind %d not in whitelist", kind)
1428  			return true // Allow read even though kind not whitelisted
1429  		}
1430  		if access == "write" && p.Global.WriteAllowPermissive {
1431  			log.D.F("write_allow_permissive: allowing write for kind %d not in whitelist (global rules apply)", kind)
1432  			return true // Allow write even though kind not whitelisted, global rule will be applied
1433  		}
1434  		return false
1435  	}
1436  
1437  	// If blacklist is present, deny blacklisted kinds
1438  	if len(p.Kind.Blacklist) > 0 {
1439  		for _, deniedKind := range p.Kind.Blacklist {
1440  			if kind == uint16(deniedKind) {
1441  				// Kind is explicitly blacklisted - permissive flags don't override blacklist
1442  				return false
1443  			}
1444  		}
1445  		// Not in blacklist - check if rule exists for implicit whitelist
1446  		_, hasRule := p.rules[int(kind)]
1447  		if hasRule {
1448  			return true
1449  		}
1450  		// No kind-specific rule - check permissive flags
1451  		if access == "read" && p.Global.ReadAllowPermissive {
1452  			log.D.F("read_allow_permissive: allowing read for kind %d (not blacklisted, no rule)", kind)
1453  			return true
1454  		}
1455  		if access == "write" && p.Global.WriteAllowPermissive {
1456  			log.D.F("write_allow_permissive: allowing write for kind %d (not blacklisted, no rule)", kind)
1457  			return true
1458  		}
1459  		return false // Only allow if there's a rule defined
1460  	}
1461  
1462  	// No explicit whitelist or blacklist
1463  	// Behavior depends on whether default_policy is explicitly set:
1464  	// - If default_policy is explicitly "allow", allow all kinds (rules add constraints, not restrictions)
1465  	// - If default_policy is unset or "deny", use implicit whitelist (only allow kinds with rules)
1466  	// - If global rule has any configuration, allow kinds through for global rule checking
1467  	// - Permissive flags can override implicit whitelist behavior
1468  	if len(p.rules) > 0 {
1469  		// If default_policy is explicitly "allow", don't use implicit whitelist
1470  		if p.DefaultPolicy == "allow" {
1471  			return true
1472  		}
1473  		// Implicit whitelist mode - only allow kinds with specific rules
1474  		_, hasRule := p.rules[int(kind)]
1475  		if hasRule {
1476  			return true
1477  		}
1478  		// No kind-specific rule, but check if global rule exists
1479  		if p.Global.hasAnyRules() {
1480  			return true // Allow through for global rule check
1481  		}
1482  		// Check permissive flags for implicit whitelist override
1483  		if access == "read" && p.Global.ReadAllowPermissive {
1484  			log.D.F("read_allow_permissive: allowing read for kind %d (implicit whitelist override)", kind)
1485  			return true
1486  		}
1487  		if access == "write" && p.Global.WriteAllowPermissive {
1488  			log.D.F("write_allow_permissive: allowing write for kind %d (implicit whitelist override)", kind)
1489  			return true
1490  		}
1491  		return false
1492  	}
1493  	// No kind-specific rules - check if global rule exists
1494  	if p.Global.hasAnyRules() {
1495  		return true // Allow through for global rule check
1496  	}
1497  	// No rules at all - fall back to default policy
1498  	return p.getDefaultPolicyAction()
1499  }
1500  
1501  // checkGlobalFollowsWhitelistAccess checks if the user is explicitly granted access
1502  // via the global rule's follows whitelists (read_follows_whitelist or write_follows_whitelist).
1503  // This grants access that bypasses the default policy for kinds without specific rules.
1504  // Note: p should never be nil here - caller (CheckPolicy) already validates this.
1505  func (p *P) checkGlobalFollowsWhitelistAccess(access string, loggedInPubkey []byte) bool {
1506  	if len(loggedInPubkey) == 0 {
1507  		return false
1508  	}
1509  
1510  	if access == "read" {
1511  		// Check if user is in global read follows whitelist
1512  		if p.Global.HasReadFollowsWhitelist() && p.Global.IsInReadFollowsWhitelist(loggedInPubkey) {
1513  			return true
1514  		}
1515  		// Also check legacy WriteAllowFollows and FollowsWhitelistAdmins for read access
1516  		if p.Global.WriteAllowFollows && p.PolicyFollowWhitelistEnabled && p.IsPolicyFollow(loggedInPubkey) {
1517  			return true
1518  		}
1519  		if p.Global.HasFollowsWhitelistAdmins() && p.Global.IsInFollowsWhitelist(loggedInPubkey) {
1520  			return true
1521  		}
1522  	} else if access == "write" {
1523  		// Check if user is in global write follows whitelist
1524  		if p.Global.HasWriteFollowsWhitelist() && p.Global.IsInWriteFollowsWhitelist(loggedInPubkey) {
1525  			return true
1526  		}
1527  		// Also check legacy WriteAllowFollows and FollowsWhitelistAdmins for write access
1528  		if p.Global.WriteAllowFollows && p.PolicyFollowWhitelistEnabled && p.IsPolicyFollow(loggedInPubkey) {
1529  			return true
1530  		}
1531  		if p.Global.HasFollowsWhitelistAdmins() && p.Global.IsInFollowsWhitelist(loggedInPubkey) {
1532  			return true
1533  		}
1534  	}
1535  
1536  	return false
1537  }
1538  
1539  // checkGlobalRulePolicy checks if the event passes the global rule filter
1540  // Note: p should never be nil here - caller (CheckPolicy) already validates this.
1541  func (p *P) checkGlobalRulePolicy(
1542  	access string, ev *event.E, loggedInPubkey []byte,
1543  ) bool {
1544  	// Skip if no global rules are configured
1545  	if !p.Global.hasAnyRules() {
1546  		return true
1547  	}
1548  
1549  	// Apply global rule filtering
1550  	allowed, err := p.checkRulePolicy(access, ev, p.Global, loggedInPubkey)
1551  	if err != nil {
1552  		log.E.F("global rule policy check failed: %v", err)
1553  		return false
1554  	}
1555  	return allowed
1556  }
1557  
1558  // checkRulePolicy evaluates rule-based access control with the following logic:
1559  //
1560  // READ ACCESS (default-permissive):
1561  //   - Denied if in read_deny list
1562  //   - If read_allow, read_follows_whitelist, or privileged is set, user must pass one of those checks
1563  //   - Otherwise, read is allowed by default
1564  //
1565  // WRITE ACCESS (default-permissive):
1566  //   - Denied if in write_deny list
1567  //   - Universal constraints (size, tags, age) apply to writes only
1568  //   - If write_allow or write_follows_whitelist is set, user must pass one of those checks
1569  //   - Otherwise, write is allowed by default
1570  //
1571  // PRIVILEGED: Only applies to READ operations (party-involved check)
1572  func (p *P) checkRulePolicy(
1573  	access string, ev *event.E, rule Rule, loggedInPubkey []byte,
1574  ) (allowed bool, err error) {
1575  	log.T.F("checkRulePolicy: access=%s kind=%d readFollowsFollowsBin_len=%d readFollowsWhitelistBin_len=%d HasReadFollowsWhitelist=%v",
1576  		access, ev.Kind, len(rule.readFollowsFollowsBin), len(rule.readFollowsWhitelistBin), rule.HasReadFollowsWhitelist())
1577  
1578  	// ===================================================================
1579  	// STEP 1: Universal Constraints (WRITE ONLY - apply to everyone)
1580  	// ===================================================================
1581  
1582  	if access == "write" {
1583  		// Check size limits
1584  		if rule.SizeLimit != nil {
1585  			eventSize := int64(len(ev.Serialize()))
1586  			if eventSize > *rule.SizeLimit {
1587  				return false, nil
1588  			}
1589  		}
1590  
1591  		if rule.ContentLimit != nil {
1592  			contentSize := int64(len(ev.Content))
1593  			if contentSize > *rule.ContentLimit {
1594  				return false, nil
1595  			}
1596  		}
1597  
1598  		// Check required tags
1599  		if len(rule.MustHaveTags) > 0 {
1600  			for _, requiredTag := range rule.MustHaveTags {
1601  				if ev.Tags.GetFirst([]byte(requiredTag)) == nil {
1602  					return false, nil
1603  				}
1604  			}
1605  		}
1606  
1607  		// Check expiry time (uses maxExpirySeconds which is parsed from MaxExpiryDuration or MaxExpiry)
1608  		if rule.maxExpirySeconds != nil && *rule.maxExpirySeconds > 0 {
1609  			expiryTag := ev.Tags.GetFirst([]byte("expiration"))
1610  			if expiryTag == nil {
1611  				return false, nil // Must have expiry if max_expiry is set
1612  			}
1613  			// Parse expiry timestamp and validate it's within allowed duration from created_at
1614  			expiryStr := string(expiryTag.Value())
1615  			expiryTs, parseErr := strconv.ParseInt(expiryStr, 10, 64)
1616  			if parseErr != nil {
1617  				log.D.F("invalid expiration tag value %q: %v", expiryStr, parseErr)
1618  				return false, nil // Invalid expiry format
1619  			}
1620  			maxAllowedExpiry := ev.CreatedAt + *rule.maxExpirySeconds
1621  			if expiryTs >= maxAllowedExpiry {
1622  				log.D.F("expiration %d exceeds max allowed %d (created_at %d + max_expiry %d)",
1623  					expiryTs, maxAllowedExpiry, ev.CreatedAt, *rule.maxExpirySeconds)
1624  				return false, nil // Expiry too far in the future
1625  			}
1626  		}
1627  
1628  		// Check ProtectedRequired (NIP-70: events must have "-" tag)
1629  		if rule.ProtectedRequired {
1630  			protectedTag := ev.Tags.GetFirst([]byte("-"))
1631  			if protectedTag == nil {
1632  				log.D.F("protected_required: event missing '-' tag (NIP-70)")
1633  				return false, nil // Must have protected tag
1634  			}
1635  		}
1636  
1637  		// Check IdentifierRegex (validates "d" tag values)
1638  		if rule.identifierRegexCache != nil {
1639  			dTags := ev.Tags.GetAll([]byte("d"))
1640  			if len(dTags) == 0 {
1641  				log.D.F("identifier_regex: event missing 'd' tag")
1642  				return false, nil // Must have d tag if identifier_regex is set
1643  			}
1644  			for _, dTag := range dTags {
1645  				value := string(dTag.Value())
1646  				if !rule.identifierRegexCache.MatchString(value) {
1647  					log.D.F("identifier_regex: d tag value %q does not match pattern %q",
1648  						value, rule.IdentifierRegex)
1649  					return false, nil
1650  				}
1651  			}
1652  		}
1653  
1654  		// Check MaxAgeOfEvent (maximum age of event in seconds)
1655  		if rule.MaxAgeOfEvent != nil && *rule.MaxAgeOfEvent > 0 {
1656  			currentTime := time.Now().Unix()
1657  			maxAllowedTime := currentTime - *rule.MaxAgeOfEvent
1658  			if ev.CreatedAt < maxAllowedTime {
1659  				return false, nil // Event is too old
1660  			}
1661  		}
1662  
1663  		// Check MaxAgeEventInFuture (maximum time event can be in the future in seconds)
1664  		if rule.MaxAgeEventInFuture != nil && *rule.MaxAgeEventInFuture > 0 {
1665  			currentTime := time.Now().Unix()
1666  			maxFutureTime := currentTime + *rule.MaxAgeEventInFuture
1667  			if ev.CreatedAt > maxFutureTime {
1668  				return false, nil // Event is too far in the future
1669  			}
1670  		}
1671  
1672  		// Check tag validation rules (regex patterns)
1673  		// NOTE: TagValidation only validates tags that ARE present on the event.
1674  		// To REQUIRE a tag to exist, use MustHaveTags instead.
1675  		if len(rule.TagValidation) > 0 {
1676  			for tagName, regexPattern := range rule.TagValidation {
1677  				// Compile regex pattern (errors should have been caught in ValidateJSON)
1678  				regex, compileErr := regexp.Compile(regexPattern)
1679  				if compileErr != nil {
1680  					log.E.F("invalid regex pattern for tag %q: %v (skipping validation)", tagName, compileErr)
1681  					continue
1682  				}
1683  
1684  				// Get all tags with this name
1685  				tags := ev.Tags.GetAll([]byte(tagName))
1686  
1687  				// If no tags found, skip validation for this tag type
1688  				// (TagValidation validates format, not presence - use MustHaveTags for presence)
1689  				if len(tags) == 0 {
1690  					continue
1691  				}
1692  
1693  				// Validate each tag value against regex
1694  				for _, t := range tags {
1695  					value := string(t.Value())
1696  					if !regex.MatchString(value) {
1697  						log.D.F("tag validation failed: tag %q value %q does not match pattern %q",
1698  							tagName, value, regexPattern)
1699  						return false, nil
1700  					}
1701  				}
1702  			}
1703  		}
1704  	}
1705  
1706  	// ===================================================================
1707  	// STEP 2: Explicit Denials (highest priority blacklist)
1708  	// ===================================================================
1709  
1710  	if access == "write" {
1711  		// Check write deny list - deny specific users from submitting events
1712  		if len(rule.writeDenyBin) > 0 {
1713  			for _, deniedPubkey := range rule.writeDenyBin {
1714  				if utils.FastEqual(loggedInPubkey, deniedPubkey) {
1715  					return false, nil // Submitter explicitly denied
1716  				}
1717  			}
1718  		} else if len(rule.WriteDeny) > 0 {
1719  			// Fallback: binary cache not populated, use hex comparison
1720  			loggedInPubkeyHex := hex.Enc(loggedInPubkey)
1721  			for _, deniedPubkey := range rule.WriteDeny {
1722  				if loggedInPubkeyHex == deniedPubkey {
1723  					return false, nil // Submitter explicitly denied
1724  				}
1725  			}
1726  		}
1727  	} else if access == "read" {
1728  		// Check read deny list
1729  		if len(rule.readDenyBin) > 0 {
1730  			for _, deniedPubkey := range rule.readDenyBin {
1731  				if utils.FastEqual(loggedInPubkey, deniedPubkey) {
1732  					return false, nil // Explicitly denied
1733  				}
1734  			}
1735  		} else if len(rule.ReadDeny) > 0 {
1736  			// Fallback: binary cache not populated, use hex comparison
1737  			loggedInPubkeyHex := hex.Enc(loggedInPubkey)
1738  			for _, deniedPubkey := range rule.ReadDeny {
1739  				if loggedInPubkeyHex == deniedPubkey {
1740  					return false, nil // Explicitly denied
1741  				}
1742  			}
1743  		}
1744  	}
1745  
1746  	// ===================================================================
1747  	// STEP 3: Legacy WriteAllowFollows (grants BOTH read AND write access)
1748  	// ===================================================================
1749  
1750  	// WriteAllowFollows grants both read and write access to policy admin follows
1751  	// This check applies to BOTH read and write access types (legacy behavior)
1752  	if rule.WriteAllowFollows && p.PolicyFollowWhitelistEnabled {
1753  		if p.IsPolicyFollow(loggedInPubkey) {
1754  			log.D.F("policy admin follow granted %s access for kind %d", access, ev.Kind)
1755  			return true, nil // Allow access from policy admin follow
1756  		}
1757  	}
1758  
1759  	// FollowsWhitelistAdmins grants access to follows of specific admin pubkeys for this rule
1760  	// This is a per-rule alternative to WriteAllowFollows which uses global PolicyAdmins (DEPRECATED)
1761  	if rule.HasFollowsWhitelistAdmins() {
1762  		if rule.IsInFollowsWhitelist(loggedInPubkey) {
1763  			log.D.F("follows_whitelist_admins granted %s access for kind %d", access, ev.Kind)
1764  			return true, nil // Allow access from rule-specific admin follow
1765  		}
1766  	}
1767  
1768  	// ===================================================================
1769  	// STEP 4: New Follows Whitelist Checks (separate read/write)
1770  	// ===================================================================
1771  
1772  	if access == "read" {
1773  		// Check ReadFollowsWhitelist - if set, it acts as a whitelist
1774  		if rule.HasReadFollowsWhitelist() {
1775  			if rule.IsInReadFollowsWhitelist(loggedInPubkey) {
1776  				log.D.F("read_follows_whitelist granted read access for kind %d", ev.Kind)
1777  				return true, nil
1778  			}
1779  			// ReadFollowsWhitelist is set but user is not in it
1780  			// Continue to check other access methods (privileged, read_allow)
1781  		}
1782  	} else if access == "write" {
1783  		// Check WriteFollowsWhitelist - if set, it acts as a whitelist
1784  		if rule.HasWriteFollowsWhitelist() {
1785  			if rule.IsInWriteFollowsWhitelist(loggedInPubkey) {
1786  				log.D.F("write_follows_whitelist granted write access for kind %d", ev.Kind)
1787  				return true, nil
1788  			}
1789  			// WriteFollowsWhitelist is set but user is not in it - must check write_allow too
1790  		}
1791  	}
1792  
1793  	// ===================================================================
1794  	// STEP 5: Read Access Control
1795  	// ===================================================================
1796  
1797  	if access == "read" {
1798  		hasReadAllowList := len(rule.readAllowBin) > 0 || len(rule.ReadAllow) > 0
1799  		hasReadFollowsWhitelist := rule.HasReadFollowsWhitelist()
1800  		// Include deprecated FollowsWhitelistAdmins for backward compatibility (it grants read+write)
1801  		hasLegacyFollowsWhitelist := rule.HasFollowsWhitelistAdmins()
1802  		userIsPrivileged := rule.Privileged && IsPartyInvolved(ev, loggedInPubkey)
1803  
1804  		// Check if user is in read allow list
1805  		userInAllowList := false
1806  		if len(rule.readAllowBin) > 0 {
1807  			for _, allowedPubkey := range rule.readAllowBin {
1808  				if utils.FastEqual(loggedInPubkey, allowedPubkey) {
1809  					userInAllowList = true
1810  					break
1811  				}
1812  			}
1813  		} else if len(rule.ReadAllow) > 0 {
1814  			loggedInPubkeyHex := hex.Enc(loggedInPubkey)
1815  			for _, allowedPubkey := range rule.ReadAllow {
1816  				if loggedInPubkeyHex == allowedPubkey {
1817  					userInAllowList = true
1818  					break
1819  				}
1820  			}
1821  		}
1822  
1823  		// Determine if any read whitelist restriction is active
1824  		// Note: Legacy FollowsWhitelistAdmins also counts as a read restriction for backward compatibility
1825  		hasReadRestriction := hasReadAllowList || hasReadFollowsWhitelist || hasLegacyFollowsWhitelist || rule.Privileged
1826  
1827  		if hasReadRestriction {
1828  			// User must pass one of the configured access methods
1829  			if userInAllowList {
1830  				return true, nil
1831  			}
1832  			if userIsPrivileged {
1833  				return true, nil
1834  			}
1835  			// User is in ReadFollowsWhitelist was already checked in STEP 4
1836  			// User in legacy FollowsWhitelistAdmins was already checked in STEP 3
1837  			// If we reach here with a read restriction, deny access
1838  			return false, nil
1839  		}
1840  
1841  		// No read restriction configured - read is permissive by default
1842  		return true, nil
1843  	}
1844  
1845  	// ===================================================================
1846  	// STEP 6: Write Access Control
1847  	// ===================================================================
1848  
1849  	if access == "write" {
1850  		hasWriteAllowList := len(rule.writeAllowBin) > 0 || len(rule.WriteAllow) > 0
1851  		hasWriteFollowsWhitelist := rule.HasWriteFollowsWhitelist()
1852  		// Include deprecated FollowsWhitelistAdmins for backward compatibility
1853  		hasLegacyFollowsWhitelist := rule.HasFollowsWhitelistAdmins()
1854  
1855  		// Check if user is in write allow list
1856  		userInAllowList := false
1857  		if len(rule.writeAllowBin) > 0 {
1858  			for _, allowedPubkey := range rule.writeAllowBin {
1859  				if utils.FastEqual(loggedInPubkey, allowedPubkey) {
1860  					userInAllowList = true
1861  					break
1862  				}
1863  			}
1864  		} else if len(rule.WriteAllow) > 0 {
1865  			loggedInPubkeyHex := hex.Enc(loggedInPubkey)
1866  			for _, allowedPubkey := range rule.WriteAllow {
1867  				if loggedInPubkeyHex == allowedPubkey {
1868  					userInAllowList = true
1869  					break
1870  				}
1871  			}
1872  		}
1873  
1874  		// Determine if any write whitelist restriction is active
1875  		// Note: Legacy FollowsWhitelistAdmins also counts as a write restriction for backward compatibility
1876  		hasWriteIfTagged := len(rule.writeAllowIfTaggedBin) > 0 || len(rule.WriteAllowIfTagged) > 0
1877  		hasWriteRestriction := hasWriteAllowList || hasWriteFollowsWhitelist || hasLegacyFollowsWhitelist || hasWriteIfTagged
1878  
1879  		if hasWriteRestriction {
1880  			// User must pass one of the configured access methods
1881  			if userInAllowList {
1882  				return true, nil
1883  			}
1884  			// User in WriteFollowsWhitelist was already checked in STEP 4
1885  			// User in legacy FollowsWhitelistAdmins was already checked in STEP 3
1886  
1887  			// Check write_allow_if_tagged: allow if event p-tags a listed pubkey
1888  			if hasWriteIfTagged {
1889  				pTags := ev.Tags.GetAll([]byte("p"))
1890  				for _, pTag := range pTags {
1891  					pt, decErr := hex.Dec(string(pTag.ValueHex()))
1892  					if decErr != nil {
1893  						continue
1894  					}
1895  					for _, allowed := range rule.writeAllowIfTaggedBin {
1896  						if utils.FastEqual(pt, allowed) {
1897  							return true, nil
1898  						}
1899  					}
1900  				}
1901  			}
1902  
1903  			// If we reach here with a write restriction, deny access
1904  			return false, nil
1905  		}
1906  
1907  		// No write restriction configured - write is permissive by default
1908  		return true, nil
1909  	}
1910  
1911  	// ===================================================================
1912  	// STEP 7: Default Policy (fallback)
1913  	// ===================================================================
1914  
1915  	// If no specific rules matched, use the configured default policy
1916  	return p.getDefaultPolicyAction(), nil
1917  }
1918  
1919  // checkScriptPolicy runs the policy script to determine if event should be allowed
1920  func (p *P) checkScriptPolicy(
1921  	access string, ev *event.E, scriptPath string, loggedInPubkey []byte,
1922  	ipAddress string,
1923  ) (allowed bool, err error) {
1924  	if p.manager == nil {
1925  		return false, fmt.Errorf("policy manager is not initialized")
1926  	}
1927  
1928  	// If policy is disabled, fall back to default policy immediately
1929  	if !p.manager.IsEnabled() {
1930  		log.W.F(
1931  			"policy rule for kind %d is inactive (policy disabled), falling back to default policy (%s)",
1932  			ev.Kind, p.DefaultPolicy,
1933  		)
1934  		return p.getDefaultPolicyAction(), nil
1935  	}
1936  
1937  	// Check if script file exists
1938  	if _, err := os.Stat(scriptPath); os.IsNotExist(err) {
1939  		// Script doesn't exist, return error so caller can fall back to other criteria
1940  		return false, fmt.Errorf(
1941  			"policy script does not exist at %s", scriptPath,
1942  		)
1943  	}
1944  
1945  	// Get or create a runner for this specific script path
1946  	runner := p.manager.getOrCreateRunner(scriptPath)
1947  
1948  	// Policy is enabled, check if this runner is running
1949  	if !runner.IsRunning() {
1950  		// Try to start this runner and wait for it
1951  		log.D.F("starting policy script for kind %d: %s", ev.Kind, scriptPath)
1952  		if err := runner.ensureRunning(); err != nil {
1953  			// Startup failed, return error so caller can fall back to other criteria
1954  			return false, fmt.Errorf(
1955  				"failed to start policy script %s: %v", scriptPath, err,
1956  			)
1957  		}
1958  		log.I.F("policy script started for kind %d: %s", ev.Kind, scriptPath)
1959  	}
1960  
1961  	// Create policy event with additional context
1962  	policyEvent := &PolicyEvent{
1963  		E:              ev,
1964  		LoggedInPubkey: hex.Enc(loggedInPubkey),
1965  		IPAddress:      ipAddress,
1966  		AccessType:     access,
1967  	}
1968  
1969  	// Process event through policy script
1970  	response, scriptErr := runner.ProcessEvent(policyEvent)
1971  	if chk.E(scriptErr) {
1972  		log.E.F(
1973  			"policy rule for kind %d failed (script processing error: %v), falling back to default policy (%s)",
1974  			ev.Kind, scriptErr, p.DefaultPolicy,
1975  		)
1976  		// Fall back to default policy on script failure
1977  		return p.getDefaultPolicyAction(), nil
1978  	}
1979  
1980  	// Handle script response
1981  	switch response.Action {
1982  	case "accept":
1983  		return true, nil
1984  	case "reject":
1985  		return false, nil
1986  	case "shadowReject":
1987  		return false, nil // Treat as reject for policy purposes
1988  	default:
1989  		log.W.F(
1990  			"policy rule for kind %d returned unknown action '%s', falling back to default policy (%s)",
1991  			ev.Kind, response.Action, p.DefaultPolicy,
1992  		)
1993  		// Fall back to default policy for unknown actions
1994  		return p.getDefaultPolicyAction(), nil
1995  	}
1996  }
1997  
1998  // PolicyManager methods
1999  
2000  // periodicCheck periodically checks if the default policy script becomes available.
2001  // This is for backward compatibility with the default script path.
2002  func (pm *PolicyManager) periodicCheck() {
2003  	// Get or create runner for the default script path
2004  	// This will also start its own periodic check
2005  	pm.getOrCreateRunner(pm.scriptPath)
2006  }
2007  
2008  // startPolicyIfExists starts the default policy script if the file exists.
2009  // This is for backward compatibility with the default script path.
2010  // Only logs if the default script actually exists - missing default scripts are normal
2011  // when users configure rule-specific scripts.
2012  func (pm *PolicyManager) startPolicyIfExists() {
2013  	if _, err := os.Stat(pm.scriptPath); err == nil {
2014  		// Default script exists, try to start it
2015  		log.I.F("found default policy script at %s, starting...", pm.scriptPath)
2016  		runner := pm.getOrCreateRunner(pm.scriptPath)
2017  		if err := runner.Start(); err != nil {
2018  			log.E.F(
2019  				"failed to start default policy script: %v, will retry periodically",
2020  				err,
2021  			)
2022  		}
2023  	}
2024  	// Silently ignore if default script doesn't exist - it's fine if rules use custom scripts
2025  }
2026  
2027  // IsEnabled returns whether the policy manager is enabled.
2028  func (pm *PolicyManager) IsEnabled() bool {
2029  	return pm.enabled
2030  }
2031  
2032  // IsRunning returns whether the default policy script is currently running.
2033  // Deprecated: Use getOrCreateRunner(scriptPath).IsRunning() for specific scripts.
2034  func (pm *PolicyManager) IsRunning() bool {
2035  	pm.mutex.RLock()
2036  	defer pm.mutex.RUnlock()
2037  
2038  	// Check if default script runner exists and is running
2039  	if runner, exists := pm.runners[pm.scriptPath]; exists {
2040  		return runner.IsRunning()
2041  	}
2042  	return false
2043  }
2044  
2045  // GetScriptPath returns the default script path.
2046  func (pm *PolicyManager) GetScriptPath() string {
2047  	return pm.scriptPath
2048  }
2049  
2050  // Shutdown gracefully shuts down the policy manager and all running scripts.
2051  func (pm *PolicyManager) Shutdown() {
2052  	pm.cancel()
2053  
2054  	pm.mutex.Lock()
2055  	defer pm.mutex.Unlock()
2056  
2057  	// Stop all running scripts
2058  	for path, runner := range pm.runners {
2059  		if runner.IsRunning() {
2060  			log.I.F("stopping policy script: %s", path)
2061  			runner.Stop()
2062  		}
2063  		// Cancel the runner's context
2064  		runner.cancel()
2065  	}
2066  
2067  	// Clear runners map
2068  	pm.runners = make(map[string]*ScriptRunner)
2069  }
2070  
2071  // =============================================================================
2072  // Policy Hot Reload Methods
2073  // =============================================================================
2074  
2075  // ValidateJSON validates policy JSON without applying changes.
2076  // This is called BEFORE any modifications to ensure JSON is valid.
2077  // Returns error if validation fails - no changes are made to current policy.
2078  func (p *P) ValidateJSON(policyJSON []byte) error {
2079  	// Try to unmarshal into a temporary policy struct
2080  	tempPolicy := &P{}
2081  	if err := json.Unmarshal(policyJSON, tempPolicy); err != nil {
2082  		return fmt.Errorf("invalid JSON syntax: %v", err)
2083  	}
2084  
2085  	// Validate policy_admins are valid hex pubkeys (64 characters)
2086  	for _, admin := range tempPolicy.PolicyAdmins {
2087  		if len(admin) != 64 {
2088  			return fmt.Errorf("invalid policy_admin pubkey length: %q (expected 64 hex characters)", admin)
2089  		}
2090  		if _, err := hex.Dec(admin); err != nil {
2091  			return fmt.Errorf("invalid policy_admin pubkey format: %q: %v", admin, err)
2092  		}
2093  	}
2094  
2095  	// Validate owners are valid hex pubkeys (64 characters)
2096  	for _, owner := range tempPolicy.Owners {
2097  		if len(owner) != 64 {
2098  			return fmt.Errorf("invalid owner pubkey length: %q (expected 64 hex characters)", owner)
2099  		}
2100  		if _, err := hex.Dec(owner); err != nil {
2101  			return fmt.Errorf("invalid owner pubkey format: %q: %v", owner, err)
2102  		}
2103  	}
2104  
2105  	// Note: Owner-specific validation (non-empty owners) is done in ValidateOwnerPolicyUpdate
2106  
2107  	// Validate regex patterns in tag_validation rules and new fields
2108  	for kind, rule := range tempPolicy.rules {
2109  		for tagName, pattern := range rule.TagValidation {
2110  			if _, err := regexp.Compile(pattern); err != nil {
2111  				return fmt.Errorf("invalid regex pattern for tag %q in kind %d: %v", tagName, kind, err)
2112  			}
2113  		}
2114  		// Validate IdentifierRegex pattern
2115  		if rule.IdentifierRegex != "" {
2116  			if _, err := regexp.Compile(rule.IdentifierRegex); err != nil {
2117  				return fmt.Errorf("invalid identifier_regex pattern in kind %d: %v", kind, err)
2118  			}
2119  		}
2120  		// Validate MaxExpiryDuration format
2121  		if rule.MaxExpiryDuration != "" {
2122  			if _, err := parseDuration(rule.MaxExpiryDuration); err != nil {
2123  				return fmt.Errorf("invalid max_expiry_duration %q in kind %d: %v (format must be ISO-8601 duration, e.g. \"PT10M\" for 10 minutes, \"P7D\" for 7 days, \"P1DT12H\" for 1 day 12 hours)", rule.MaxExpiryDuration, kind, err)
2124  			}
2125  		}
2126  		// Validate FollowsWhitelistAdmins pubkeys
2127  		for _, admin := range rule.FollowsWhitelistAdmins {
2128  			if len(admin) != 64 {
2129  				return fmt.Errorf("invalid follows_whitelist_admins pubkey length in kind %d: %q (expected 64 hex characters)", kind, admin)
2130  			}
2131  			if _, err := hex.Dec(admin); err != nil {
2132  				return fmt.Errorf("invalid follows_whitelist_admins pubkey format in kind %d: %q: %v", kind, admin, err)
2133  			}
2134  		}
2135  	}
2136  
2137  	// Validate global rule tag_validation patterns
2138  	for tagName, pattern := range tempPolicy.Global.TagValidation {
2139  		if _, err := regexp.Compile(pattern); err != nil {
2140  			return fmt.Errorf("invalid regex pattern for tag %q in global rule: %v", tagName, err)
2141  		}
2142  	}
2143  
2144  	// Validate global rule IdentifierRegex pattern
2145  	if tempPolicy.Global.IdentifierRegex != "" {
2146  		if _, err := regexp.Compile(tempPolicy.Global.IdentifierRegex); err != nil {
2147  			return fmt.Errorf("invalid identifier_regex pattern in global rule: %v", err)
2148  		}
2149  	}
2150  
2151  	// Validate global rule MaxExpiryDuration format
2152  	if tempPolicy.Global.MaxExpiryDuration != "" {
2153  		if _, err := parseDuration(tempPolicy.Global.MaxExpiryDuration); err != nil {
2154  			return fmt.Errorf("invalid max_expiry_duration %q in global rule: %v (format must be ISO-8601 duration, e.g. \"PT10M\" for 10 minutes, \"P7D\" for 7 days, \"P1DT12H\" for 1 day 12 hours)", tempPolicy.Global.MaxExpiryDuration, err)
2155  		}
2156  	}
2157  
2158  	// Validate global rule FollowsWhitelistAdmins pubkeys
2159  	for _, admin := range tempPolicy.Global.FollowsWhitelistAdmins {
2160  		if len(admin) != 64 {
2161  			return fmt.Errorf("invalid follows_whitelist_admins pubkey length in global rule: %q (expected 64 hex characters)", admin)
2162  		}
2163  		if _, err := hex.Dec(admin); err != nil {
2164  			return fmt.Errorf("invalid follows_whitelist_admins pubkey format in global rule: %q: %v", admin, err)
2165  		}
2166  	}
2167  
2168  	// Validate default_policy value
2169  	if tempPolicy.DefaultPolicy != "" && tempPolicy.DefaultPolicy != "allow" && tempPolicy.DefaultPolicy != "deny" {
2170  		return fmt.Errorf("invalid default_policy value: %q (must be \"allow\" or \"deny\")", tempPolicy.DefaultPolicy)
2171  	}
2172  
2173  	// Validate permissive flags: if both read_allow_permissive AND write_allow_permissive are set
2174  	// with a kind whitelist or blacklist, this makes the whitelist/blacklist meaningless
2175  	hasKindRestriction := len(tempPolicy.Kind.Whitelist) > 0 || len(tempPolicy.Kind.Blacklist) > 0
2176  	if hasKindRestriction && tempPolicy.Global.ReadAllowPermissive && tempPolicy.Global.WriteAllowPermissive {
2177  		return fmt.Errorf("invalid policy: both read_allow_permissive and write_allow_permissive cannot be enabled together with a kind whitelist or blacklist (this would make the kind restriction meaningless)")
2178  	}
2179  
2180  	log.D.F("policy JSON validation passed")
2181  	return nil
2182  }
2183  
2184  // Reload loads policy from JSON bytes and applies it to the existing policy instance.
2185  // This validates JSON FIRST, then pauses the policy manager, updates configuration, and resumes.
2186  // Returns error if validation fails - no changes are made on validation failure.
2187  func (p *P) Reload(policyJSON []byte, configPath string) error {
2188  	// Step 1: Validate JSON FIRST (before making any changes)
2189  	if err := p.ValidateJSON(policyJSON); err != nil {
2190  		return fmt.Errorf("validation failed: %v", err)
2191  	}
2192  
2193  	// Step 2: Pause policy manager (stop script runners)
2194  	if err := p.Pause(); err != nil {
2195  		log.W.F("failed to pause policy manager (continuing anyway): %v", err)
2196  	}
2197  
2198  	// Step 3: Unmarshal JSON into a temporary struct
2199  	tempPolicy := &P{}
2200  	if err := json.Unmarshal(policyJSON, tempPolicy); err != nil {
2201  		// Resume before returning error
2202  		p.Resume()
2203  		return fmt.Errorf("failed to unmarshal policy JSON: %v", err)
2204  	}
2205  
2206  	// Step 4: Apply the new configuration (preserve manager reference)
2207  	p.followsMx.Lock()
2208  	p.Kind = tempPolicy.Kind
2209  	p.rules = tempPolicy.rules
2210  	p.Global = tempPolicy.Global
2211  	p.DefaultPolicy = tempPolicy.DefaultPolicy
2212  	p.PolicyAdmins = tempPolicy.PolicyAdmins
2213  	p.PolicyFollowWhitelistEnabled = tempPolicy.PolicyFollowWhitelistEnabled
2214  	p.Owners = tempPolicy.Owners
2215  	p.policyAdminsBin = tempPolicy.policyAdminsBin
2216  	p.ownersBin = tempPolicy.ownersBin
2217  	// Note: policyFollows is NOT reset here - it will be refreshed separately
2218  	p.followsMx.Unlock()
2219  
2220  	// Step 5: Populate binary caches for all rules
2221  	p.Global.populateBinaryCache()
2222  	for kind := range p.rules {
2223  		rule := p.rules[kind]
2224  		rule.populateBinaryCache()
2225  		p.rules[kind] = rule
2226  	}
2227  
2228  	// Step 6: Save to file (atomic write)
2229  	if err := p.SaveToFile(configPath); err != nil {
2230  		log.E.F("failed to persist policy to disk: %v (policy was updated in memory)", err)
2231  		// Continue anyway - policy is loaded in memory
2232  	}
2233  
2234  	// Step 7: Resume policy manager (restart script runners)
2235  	if err := p.Resume(); err != nil {
2236  		log.W.F("failed to resume policy manager: %v", err)
2237  	}
2238  
2239  	log.I.F("policy configuration reloaded successfully")
2240  	return nil
2241  }
2242  
2243  // Pause pauses the policy manager and stops all script runners.
2244  func (p *P) Pause() error {
2245  	if p.manager == nil {
2246  		return fmt.Errorf("policy manager is not initialized")
2247  	}
2248  
2249  	p.manager.mutex.Lock()
2250  	defer p.manager.mutex.Unlock()
2251  
2252  	// Stop all running scripts
2253  	for path, runner := range p.manager.runners {
2254  		if runner.IsRunning() {
2255  			log.I.F("pausing policy script: %s", path)
2256  			if err := runner.Stop(); err != nil {
2257  				log.W.F("failed to stop runner %s: %v", path, err)
2258  			}
2259  		}
2260  	}
2261  
2262  	log.I.F("policy manager paused")
2263  	return nil
2264  }
2265  
2266  // Resume resumes the policy manager and restarts script runners.
2267  func (p *P) Resume() error {
2268  	if p.manager == nil {
2269  		return fmt.Errorf("policy manager is not initialized")
2270  	}
2271  
2272  	// Restart the default policy script if it exists
2273  	go p.manager.startPolicyIfExists()
2274  
2275  	// Restart rule-specific scripts
2276  	for _, rule := range p.rules {
2277  		if rule.Script != "" {
2278  			if _, err := os.Stat(rule.Script); err == nil {
2279  				runner := p.manager.getOrCreateRunner(rule.Script)
2280  				go func(r *ScriptRunner, script string) {
2281  					if err := r.Start(); err != nil {
2282  						log.W.F("failed to restart policy script %s: %v", script, err)
2283  					}
2284  				}(runner, rule.Script)
2285  			}
2286  		}
2287  	}
2288  
2289  	log.I.F("policy manager resumed")
2290  	return nil
2291  }
2292  
2293  // SaveToFile persists the current policy configuration to disk using atomic write.
2294  // Uses temp file + rename pattern to ensure atomic writes.
2295  func (p *P) SaveToFile(configPath string) error {
2296  	// Create shadow struct for JSON marshalling
2297  	shadow := pJSON{
2298  		Kind:                         p.Kind,
2299  		Rules:                        p.rules,
2300  		Global:                       p.Global,
2301  		DefaultPolicy:                p.DefaultPolicy,
2302  		PolicyAdmins:                 p.PolicyAdmins,
2303  		PolicyFollowWhitelistEnabled: p.PolicyFollowWhitelistEnabled,
2304  		Owners:                       p.Owners,
2305  	}
2306  
2307  	// Marshal to JSON with indentation for readability
2308  	jsonData, err := json.MarshalIndent(shadow, "", "  ")
2309  	if err != nil {
2310  		return fmt.Errorf("failed to marshal policy to JSON: %v", err)
2311  	}
2312  
2313  	// Write to temp file first (atomic write pattern)
2314  	tempPath := configPath + ".tmp"
2315  	if err := os.WriteFile(tempPath, jsonData, 0644); err != nil {
2316  		return fmt.Errorf("failed to write temp file: %v", err)
2317  	}
2318  
2319  	// Rename temp file to actual config file (atomic on most filesystems)
2320  	if err := os.Rename(tempPath, configPath); err != nil {
2321  		// Clean up temp file on failure
2322  		os.Remove(tempPath)
2323  		return fmt.Errorf("failed to rename temp file: %v", err)
2324  	}
2325  
2326  	log.I.F("policy configuration saved to %s", configPath)
2327  	return nil
2328  }
2329  
2330  // =============================================================================
2331  // Policy Admin and Follow Checking Methods
2332  // =============================================================================
2333  
2334  // IsPolicyAdmin checks if the given pubkey is in the policy_admins list.
2335  // The pubkey parameter should be binary ([]byte), not hex-encoded.
2336  func (p *P) IsPolicyAdmin(pubkey []byte) bool {
2337  	if len(pubkey) == 0 {
2338  		return false
2339  	}
2340  
2341  	p.followsMx.RLock()
2342  	defer p.followsMx.RUnlock()
2343  
2344  	for _, admin := range p.policyAdminsBin {
2345  		if utils.FastEqual(admin, pubkey) {
2346  			return true
2347  		}
2348  	}
2349  	return false
2350  }
2351  
2352  // IsPolicyFollow checks if the given pubkey is in the policy admin follows list.
2353  // The pubkey parameter should be binary ([]byte), not hex-encoded.
2354  func (p *P) IsPolicyFollow(pubkey []byte) bool {
2355  	if len(pubkey) == 0 {
2356  		return false
2357  	}
2358  
2359  	p.followsMx.RLock()
2360  	defer p.followsMx.RUnlock()
2361  
2362  	for _, follow := range p.policyFollows {
2363  		if utils.FastEqual(pubkey, follow) {
2364  			return true
2365  		}
2366  	}
2367  	return false
2368  }
2369  
2370  // UpdatePolicyFollows replaces the policy follows list with a new set of pubkeys.
2371  // This is called when policy admins update their follow lists (kind 3 events).
2372  // The pubkeys should be binary ([]byte), not hex-encoded.
2373  func (p *P) UpdatePolicyFollows(follows [][]byte) {
2374  	p.followsMx.Lock()
2375  	defer p.followsMx.Unlock()
2376  
2377  	p.policyFollows = follows
2378  	log.I.F("policy follows list updated with %d pubkeys", len(follows))
2379  }
2380  
2381  // GetPolicyAdminsBin returns a copy of the binary policy admin pubkeys.
2382  // Used for checking if an event author is a policy admin.
2383  func (p *P) GetPolicyAdminsBin() [][]byte {
2384  	p.followsMx.RLock()
2385  	defer p.followsMx.RUnlock()
2386  
2387  	// Return a copy to prevent external modification
2388  	result := make([][]byte, len(p.policyAdminsBin))
2389  	for i, admin := range p.policyAdminsBin {
2390  		adminCopy := make([]byte, len(admin))
2391  		copy(adminCopy, admin)
2392  		result[i] = adminCopy
2393  	}
2394  	return result
2395  }
2396  
2397  // GetOwnersBin returns a copy of the binary owner pubkeys defined in the policy.
2398  // These are merged with environment-defined owners by the application layer.
2399  // Useful for cloud deployments where environment variables cannot be modified.
2400  func (p *P) GetOwnersBin() [][]byte {
2401  	if p == nil {
2402  		return nil
2403  	}
2404  
2405  	p.followsMx.RLock()
2406  	defer p.followsMx.RUnlock()
2407  
2408  	// Return a copy to prevent external modification
2409  	result := make([][]byte, len(p.ownersBin))
2410  	for i, owner := range p.ownersBin {
2411  		ownerCopy := make([]byte, len(owner))
2412  		copy(ownerCopy, owner)
2413  		result[i] = ownerCopy
2414  	}
2415  	return result
2416  }
2417  
2418  // GetOwners returns the hex-encoded owner pubkeys defined in the policy.
2419  // These are merged with environment-defined owners by the application layer.
2420  func (p *P) GetOwners() []string {
2421  	if p == nil {
2422  		return nil
2423  	}
2424  	return p.Owners
2425  }
2426  
2427  // IsPolicyFollowWhitelistEnabled returns whether the policy follow whitelist feature is enabled.
2428  // When enabled, pubkeys followed by policy admins are automatically whitelisted for access
2429  // when rules have WriteAllowFollows=true.
2430  func (p *P) IsPolicyFollowWhitelistEnabled() bool {
2431  	if p == nil {
2432  		return false
2433  	}
2434  	return p.PolicyFollowWhitelistEnabled
2435  }
2436  
2437  // =============================================================================
2438  // FollowsWhitelistAdmins Methods
2439  // =============================================================================
2440  
2441  // GetAllFollowsWhitelistAdmins returns all unique admin pubkeys from FollowsWhitelistAdmins
2442  // across all rules (including global). Returns hex-encoded pubkeys.
2443  // This is used at startup to validate that kind 3 events exist for these admins.
2444  func (p *P) GetAllFollowsWhitelistAdmins() []string {
2445  	if p == nil {
2446  		return nil
2447  	}
2448  
2449  	// Use map to deduplicate
2450  	admins := make(map[string]struct{})
2451  
2452  	// Check global rule
2453  	for _, admin := range p.Global.FollowsWhitelistAdmins {
2454  		admins[admin] = struct{}{}
2455  	}
2456  
2457  	// Check all kind-specific rules
2458  	for _, rule := range p.rules {
2459  		for _, admin := range rule.FollowsWhitelistAdmins {
2460  			admins[admin] = struct{}{}
2461  		}
2462  	}
2463  
2464  	// Convert map to slice
2465  	result := make([]string, 0, len(admins))
2466  	for admin := range admins {
2467  		result = append(result, admin)
2468  	}
2469  	return result
2470  }
2471  
2472  // GetRuleForKind returns the Rule for a specific kind, or nil if no rule exists.
2473  // This allows external code to access and modify rule-specific follows whitelists.
2474  func (p *P) GetRuleForKind(kind int) *Rule {
2475  	if p == nil || p.rules == nil {
2476  		return nil
2477  	}
2478  	if rule, exists := p.rules[kind]; exists {
2479  		return &rule
2480  	}
2481  	return nil
2482  }
2483  
2484  // UpdateRuleFollowsWhitelist updates the follows whitelist for a specific kind's rule.
2485  // The follows should be binary pubkeys ([]byte), not hex-encoded.
2486  // Thread-safe: uses followsMx to protect concurrent access.
2487  func (p *P) UpdateRuleFollowsWhitelist(kind int, follows [][]byte) {
2488  	if p == nil || p.rules == nil {
2489  		return
2490  	}
2491  	p.followsMx.Lock()
2492  	defer p.followsMx.Unlock()
2493  	if rule, exists := p.rules[kind]; exists {
2494  		rule.UpdateFollowsWhitelist(follows)
2495  		p.rules[kind] = rule
2496  	}
2497  }
2498  
2499  // UpdateGlobalFollowsWhitelist updates the follows whitelist for the global rule.
2500  // The follows should be binary pubkeys ([]byte), not hex-encoded.
2501  // Note: We directly modify p.Global's unexported field because Global is a value type (not *Rule),
2502  // so calling p.Global.UpdateFollowsWhitelist() would operate on a copy and discard changes.
2503  // Thread-safe: uses followsMx to protect concurrent access.
2504  func (p *P) UpdateGlobalFollowsWhitelist(follows [][]byte) {
2505  	if p == nil {
2506  		return
2507  	}
2508  	p.followsMx.Lock()
2509  	defer p.followsMx.Unlock()
2510  	p.Global.followsWhitelistFollowsBin = follows
2511  }
2512  
2513  // GetGlobalRule returns a pointer to the global rule for modification.
2514  func (p *P) GetGlobalRule() *Rule {
2515  	if p == nil {
2516  		return nil
2517  	}
2518  	return &p.Global
2519  }
2520  
2521  // GetRules returns the rules map for iteration.
2522  // Note: Returns a copy of the map keys to prevent modification.
2523  func (p *P) GetRulesKinds() []int {
2524  	if p == nil || p.rules == nil {
2525  		return nil
2526  	}
2527  	kinds := make([]int, 0, len(p.rules))
2528  	for kind := range p.rules {
2529  		kinds = append(kinds, kind)
2530  	}
2531  	return kinds
2532  }
2533  
2534  // =============================================================================
2535  // ReadFollowsWhitelist and WriteFollowsWhitelist Methods
2536  // =============================================================================
2537  
2538  // GetAllReadFollowsWhitelistPubkeys returns all unique pubkeys from ReadFollowsWhitelist
2539  // across all rules (including global). Returns hex-encoded pubkeys.
2540  // This is used at startup to validate that kind 3 events exist for these pubkeys.
2541  func (p *P) GetAllReadFollowsWhitelistPubkeys() []string {
2542  	if p == nil {
2543  		return nil
2544  	}
2545  
2546  	// Use map to deduplicate
2547  	pubkeys := make(map[string]struct{})
2548  
2549  	// Check global rule
2550  	for _, pk := range p.Global.ReadFollowsWhitelist {
2551  		pubkeys[pk] = struct{}{}
2552  	}
2553  
2554  	// Check all kind-specific rules
2555  	for _, rule := range p.rules {
2556  		for _, pk := range rule.ReadFollowsWhitelist {
2557  			pubkeys[pk] = struct{}{}
2558  		}
2559  	}
2560  
2561  	// Convert map to slice
2562  	result := make([]string, 0, len(pubkeys))
2563  	for pk := range pubkeys {
2564  		result = append(result, pk)
2565  	}
2566  	return result
2567  }
2568  
2569  // GetAllWriteFollowsWhitelistPubkeys returns all unique pubkeys from WriteFollowsWhitelist
2570  // across all rules (including global). Returns hex-encoded pubkeys.
2571  // This is used at startup to validate that kind 3 events exist for these pubkeys.
2572  func (p *P) GetAllWriteFollowsWhitelistPubkeys() []string {
2573  	if p == nil {
2574  		return nil
2575  	}
2576  
2577  	// Use map to deduplicate
2578  	pubkeys := make(map[string]struct{})
2579  
2580  	// Check global rule
2581  	for _, pk := range p.Global.WriteFollowsWhitelist {
2582  		pubkeys[pk] = struct{}{}
2583  	}
2584  
2585  	// Check all kind-specific rules
2586  	for _, rule := range p.rules {
2587  		for _, pk := range rule.WriteFollowsWhitelist {
2588  			pubkeys[pk] = struct{}{}
2589  		}
2590  	}
2591  
2592  	// Convert map to slice
2593  	result := make([]string, 0, len(pubkeys))
2594  	for pk := range pubkeys {
2595  		result = append(result, pk)
2596  	}
2597  	return result
2598  }
2599  
2600  // GetAllFollowsWhitelistPubkeys returns all unique pubkeys from both ReadFollowsWhitelist
2601  // and WriteFollowsWhitelist across all rules (including global). Returns hex-encoded pubkeys.
2602  // This is a convenience method for startup validation to check all required kind 3 events.
2603  func (p *P) GetAllFollowsWhitelistPubkeys() []string {
2604  	if p == nil {
2605  		return nil
2606  	}
2607  
2608  	// Use map to deduplicate
2609  	pubkeys := make(map[string]struct{})
2610  
2611  	// Get read follows whitelist pubkeys
2612  	for _, pk := range p.GetAllReadFollowsWhitelistPubkeys() {
2613  		pubkeys[pk] = struct{}{}
2614  	}
2615  
2616  	// Get write follows whitelist pubkeys
2617  	for _, pk := range p.GetAllWriteFollowsWhitelistPubkeys() {
2618  		pubkeys[pk] = struct{}{}
2619  	}
2620  
2621  	// Also include deprecated FollowsWhitelistAdmins for backward compatibility
2622  	for _, pk := range p.GetAllFollowsWhitelistAdmins() {
2623  		pubkeys[pk] = struct{}{}
2624  	}
2625  
2626  	// Convert map to slice
2627  	result := make([]string, 0, len(pubkeys))
2628  	for pk := range pubkeys {
2629  		result = append(result, pk)
2630  	}
2631  	return result
2632  }
2633  
2634  // UpdateRuleReadFollowsWhitelist updates the read follows whitelist for a specific kind's rule.
2635  // The follows should be binary pubkeys ([]byte), not hex-encoded.
2636  // Thread-safe: uses followsMx to protect concurrent access.
2637  func (p *P) UpdateRuleReadFollowsWhitelist(kind int, follows [][]byte) {
2638  	if p == nil || p.rules == nil {
2639  		return
2640  	}
2641  	p.followsMx.Lock()
2642  	defer p.followsMx.Unlock()
2643  	if rule, exists := p.rules[kind]; exists {
2644  		rule.UpdateReadFollowsWhitelist(follows)
2645  		p.rules[kind] = rule
2646  	}
2647  }
2648  
2649  // UpdateRuleWriteFollowsWhitelist updates the write follows whitelist for a specific kind's rule.
2650  // The follows should be binary pubkeys ([]byte), not hex-encoded.
2651  // Thread-safe: uses followsMx to protect concurrent access.
2652  func (p *P) UpdateRuleWriteFollowsWhitelist(kind int, follows [][]byte) {
2653  	if p == nil || p.rules == nil {
2654  		return
2655  	}
2656  	p.followsMx.Lock()
2657  	defer p.followsMx.Unlock()
2658  	if rule, exists := p.rules[kind]; exists {
2659  		rule.UpdateWriteFollowsWhitelist(follows)
2660  		p.rules[kind] = rule
2661  	}
2662  }
2663  
2664  // UpdateGlobalReadFollowsWhitelist updates the read follows whitelist for the global rule.
2665  // The follows should be binary pubkeys ([]byte), not hex-encoded.
2666  // Note: We directly modify p.Global's unexported field because Global is a value type (not *Rule),
2667  // so calling p.Global.UpdateReadFollowsWhitelist() would operate on a copy and discard changes.
2668  // Thread-safe: uses followsMx to protect concurrent access.
2669  func (p *P) UpdateGlobalReadFollowsWhitelist(follows [][]byte) {
2670  	if p == nil {
2671  		return
2672  	}
2673  	p.followsMx.Lock()
2674  	defer p.followsMx.Unlock()
2675  	p.Global.readFollowsFollowsBin = follows
2676  }
2677  
2678  // UpdateGlobalWriteFollowsWhitelist updates the write follows whitelist for the global rule.
2679  // The follows should be binary pubkeys ([]byte), not hex-encoded.
2680  // Note: We directly modify p.Global's unexported field because Global is a value type (not *Rule),
2681  // so calling p.Global.UpdateWriteFollowsWhitelist() would operate on a copy and discard changes.
2682  // Thread-safe: uses followsMx to protect concurrent access.
2683  func (p *P) UpdateGlobalWriteFollowsWhitelist(follows [][]byte) {
2684  	if p == nil {
2685  		return
2686  	}
2687  	p.followsMx.Lock()
2688  	defer p.followsMx.Unlock()
2689  	p.Global.writeFollowsFollowsBin = follows
2690  }
2691  
2692  // =============================================================================
2693  // Owner vs Policy Admin Update Validation
2694  // =============================================================================
2695  
2696  // ValidateOwnerPolicyUpdate validates a full policy update from an owner.
2697  // Owners can modify all fields but the owners list must be non-empty.
2698  func (p *P) ValidateOwnerPolicyUpdate(policyJSON []byte) error {
2699  	// First run standard validation
2700  	if err := p.ValidateJSON(policyJSON); err != nil {
2701  		return err
2702  	}
2703  
2704  	// Parse the new policy
2705  	tempPolicy := &P{}
2706  	if err := json.Unmarshal(policyJSON, tempPolicy); err != nil {
2707  		return fmt.Errorf("failed to parse policy JSON: %v", err)
2708  	}
2709  
2710  	// Owner-specific validation: owners list cannot be empty
2711  	if len(tempPolicy.Owners) == 0 {
2712  		return fmt.Errorf("owners list cannot be empty: at least one owner must be defined to prevent lockout")
2713  	}
2714  
2715  	return nil
2716  }
2717  
2718  // ValidatePolicyAdminUpdate validates a policy update from a policy admin.
2719  // Policy admins CANNOT modify: owners, policy_admins
2720  // Policy admins CAN: extend rules, add blacklists, add new kind rules
2721  func (p *P) ValidatePolicyAdminUpdate(policyJSON []byte, adminPubkey []byte) error {
2722  	// First run standard validation
2723  	if err := p.ValidateJSON(policyJSON); err != nil {
2724  		return err
2725  	}
2726  
2727  	// Parse the new policy
2728  	tempPolicy := &P{}
2729  	if err := json.Unmarshal(policyJSON, tempPolicy); err != nil {
2730  		return fmt.Errorf("failed to parse policy JSON: %v", err)
2731  	}
2732  
2733  	// Protected field check: owners must match current
2734  	if !stringSliceEqual(tempPolicy.Owners, p.Owners) {
2735  		return fmt.Errorf("policy admins cannot modify the 'owners' field: this is a protected field that only owners can change")
2736  	}
2737  
2738  	// Protected field check: policy_admins must match current
2739  	if !stringSliceEqual(tempPolicy.PolicyAdmins, p.PolicyAdmins) {
2740  		return fmt.Errorf("policy admins cannot modify the 'policy_admins' field: this is a protected field that only owners can change")
2741  	}
2742  
2743  	// Validate that the admin is not reducing owner-granted permissions
2744  	// This check ensures policy admins can only extend, not restrict
2745  	if err := p.validateNoPermissionReduction(tempPolicy); err != nil {
2746  		return fmt.Errorf("policy admins cannot reduce owner-granted permissions: %v", err)
2747  	}
2748  
2749  	return nil
2750  }
2751  
2752  // validateNoPermissionReduction checks that the new policy doesn't reduce
2753  // permissions that were granted in the current (owner) policy.
2754  //
2755  // Policy admins CAN:
2756  //   - ADD to allow lists (write_allow, read_allow)
2757  //   - ADD to deny lists (write_deny, read_deny) to blacklist non-admin users
2758  //   - INCREASE limits (size_limit, content_limit, max_age_of_event)
2759  //   - ADD new kinds to whitelist or blacklist
2760  //   - ADD new rules for kinds not defined by owner
2761  //
2762  // Policy admins CANNOT:
2763  //   - REMOVE from allow lists
2764  //   - DECREASE limits
2765  //   - REMOVE kinds from whitelist
2766  //   - REMOVE rules defined by owner
2767  //   - ADD new required tags (restrictions)
2768  //   - BLACKLIST owners or other policy admins
2769  func (p *P) validateNoPermissionReduction(newPolicy *P) error {
2770  	// Check kind whitelist - new policy must include all current whitelisted kinds
2771  	for _, kind := range p.Kind.Whitelist {
2772  		found := false
2773  		for _, newKind := range newPolicy.Kind.Whitelist {
2774  			if kind == newKind {
2775  				found = true
2776  				break
2777  			}
2778  		}
2779  		if !found {
2780  			return fmt.Errorf("cannot remove kind %d from whitelist", kind)
2781  		}
2782  	}
2783  
2784  	// Check each rule in the current policy
2785  	for kind, currentRule := range p.rules {
2786  		newRule, exists := newPolicy.rules[kind]
2787  		if !exists {
2788  			return fmt.Errorf("cannot remove rule for kind %d", kind)
2789  		}
2790  
2791  		// Check write_allow - new rule must include all current pubkeys
2792  		for _, pk := range currentRule.WriteAllow {
2793  			if !containsString(newRule.WriteAllow, pk) {
2794  				return fmt.Errorf("cannot remove pubkey %s from write_allow for kind %d", pk, kind)
2795  			}
2796  		}
2797  
2798  		// Check read_allow - new rule must include all current pubkeys
2799  		for _, pk := range currentRule.ReadAllow {
2800  			if !containsString(newRule.ReadAllow, pk) {
2801  				return fmt.Errorf("cannot remove pubkey %s from read_allow for kind %d", pk, kind)
2802  			}
2803  		}
2804  
2805  		// Check write_deny - cannot blacklist owners or policy admins
2806  		for _, pk := range newRule.WriteDeny {
2807  			if containsString(p.Owners, pk) {
2808  				return fmt.Errorf("cannot blacklist owner %s in write_deny for kind %d", pk, kind)
2809  			}
2810  			if containsString(p.PolicyAdmins, pk) {
2811  				return fmt.Errorf("cannot blacklist policy admin %s in write_deny for kind %d", pk, kind)
2812  			}
2813  		}
2814  
2815  		// Check read_deny - cannot blacklist owners or policy admins
2816  		for _, pk := range newRule.ReadDeny {
2817  			if containsString(p.Owners, pk) {
2818  				return fmt.Errorf("cannot blacklist owner %s in read_deny for kind %d", pk, kind)
2819  			}
2820  			if containsString(p.PolicyAdmins, pk) {
2821  				return fmt.Errorf("cannot blacklist policy admin %s in read_deny for kind %d", pk, kind)
2822  			}
2823  		}
2824  
2825  		// Check size limits - new limit cannot be smaller
2826  		if currentRule.SizeLimit != nil && newRule.SizeLimit != nil {
2827  			if *newRule.SizeLimit < *currentRule.SizeLimit {
2828  				return fmt.Errorf("cannot reduce size_limit for kind %d from %d to %d", kind, *currentRule.SizeLimit, *newRule.SizeLimit)
2829  			}
2830  		}
2831  
2832  		// Check content limits - new limit cannot be smaller
2833  		if currentRule.ContentLimit != nil && newRule.ContentLimit != nil {
2834  			if *newRule.ContentLimit < *currentRule.ContentLimit {
2835  				return fmt.Errorf("cannot reduce content_limit for kind %d from %d to %d", kind, *currentRule.ContentLimit, *newRule.ContentLimit)
2836  			}
2837  		}
2838  
2839  		// Check max_age_of_event - new limit cannot be smaller (smaller = more restrictive)
2840  		if currentRule.MaxAgeOfEvent != nil && newRule.MaxAgeOfEvent != nil {
2841  			if *newRule.MaxAgeOfEvent < *currentRule.MaxAgeOfEvent {
2842  				return fmt.Errorf("cannot reduce max_age_of_event for kind %d from %d to %d", kind, *currentRule.MaxAgeOfEvent, *newRule.MaxAgeOfEvent)
2843  			}
2844  		}
2845  
2846  		// Check must_have_tags - cannot add new required tags (more restrictive)
2847  		for _, tag := range newRule.MustHaveTags {
2848  			found := false
2849  			for _, currentTag := range currentRule.MustHaveTags {
2850  				if tag == currentTag {
2851  					found = true
2852  					break
2853  				}
2854  			}
2855  			if !found {
2856  				return fmt.Errorf("cannot add required tag %q for kind %d (only owners can add restrictions)", tag, kind)
2857  			}
2858  		}
2859  	}
2860  
2861  	// Check global rule write_deny - cannot blacklist owners or policy admins
2862  	for _, pk := range newPolicy.Global.WriteDeny {
2863  		if containsString(p.Owners, pk) {
2864  			return fmt.Errorf("cannot blacklist owner %s in global write_deny", pk)
2865  		}
2866  		if containsString(p.PolicyAdmins, pk) {
2867  			return fmt.Errorf("cannot blacklist policy admin %s in global write_deny", pk)
2868  		}
2869  	}
2870  
2871  	// Check global rule read_deny - cannot blacklist owners or policy admins
2872  	for _, pk := range newPolicy.Global.ReadDeny {
2873  		if containsString(p.Owners, pk) {
2874  			return fmt.Errorf("cannot blacklist owner %s in global read_deny", pk)
2875  		}
2876  		if containsString(p.PolicyAdmins, pk) {
2877  			return fmt.Errorf("cannot blacklist policy admin %s in global read_deny", pk)
2878  		}
2879  	}
2880  
2881  	// Check global rule size limits
2882  	if p.Global.SizeLimit != nil && newPolicy.Global.SizeLimit != nil {
2883  		if *newPolicy.Global.SizeLimit < *p.Global.SizeLimit {
2884  			return fmt.Errorf("cannot reduce global size_limit from %d to %d", *p.Global.SizeLimit, *newPolicy.Global.SizeLimit)
2885  		}
2886  	}
2887  
2888  	return nil
2889  }
2890  
2891  // ReloadAsOwner reloads the policy from an owner's kind 12345 event.
2892  // Owners can modify all fields but the owners list must be non-empty.
2893  func (p *P) ReloadAsOwner(policyJSON []byte, configPath string) error {
2894  	// Validate as owner update
2895  	if err := p.ValidateOwnerPolicyUpdate(policyJSON); err != nil {
2896  		return fmt.Errorf("owner policy validation failed: %v", err)
2897  	}
2898  
2899  	// Use existing Reload logic
2900  	return p.Reload(policyJSON, configPath)
2901  }
2902  
2903  // ReloadAsPolicyAdmin reloads the policy from a policy admin's kind 12345 event.
2904  // Policy admins cannot modify protected fields (owners, policy_admins) and
2905  // cannot reduce owner-granted permissions.
2906  func (p *P) ReloadAsPolicyAdmin(policyJSON []byte, configPath string, adminPubkey []byte) error {
2907  	// Validate as policy admin update
2908  	if err := p.ValidatePolicyAdminUpdate(policyJSON, adminPubkey); err != nil {
2909  		return fmt.Errorf("policy admin validation failed: %v", err)
2910  	}
2911  
2912  	// Use existing Reload logic
2913  	return p.Reload(policyJSON, configPath)
2914  }
2915  
2916  // stringSliceEqual checks if two string slices are equal (order-independent).
2917  func stringSliceEqual(a, b []string) bool {
2918  	if len(a) != len(b) {
2919  		return false
2920  	}
2921  
2922  	// Create maps for comparison
2923  	aMap := make(map[string]int)
2924  	for _, v := range a {
2925  		aMap[v]++
2926  	}
2927  
2928  	bMap := make(map[string]int)
2929  	for _, v := range b {
2930  		bMap[v]++
2931  	}
2932  
2933  	// Compare maps
2934  	for k, v := range aMap {
2935  		if bMap[k] != v {
2936  			return false
2937  		}
2938  	}
2939  
2940  	return true
2941  }
2942