# Implementation Plan: Policy Hot Reload, Follow List Whitelisting, and Web UI **Issue:** https://git.nostrdev.com/mleku/next.orly.dev/issues/6 ## Overview This plan implements three interconnected features for ORLY's policy system: 1. **Dynamic Policy Configuration** via kind 12345 events (hot reload) 2. **Administrator Follow List Whitelisting** within the policy system 3. **Web Interface** for policy management with JSON editing ## Architecture Summary ### Current System Analysis **Policy System** ([pkg/policy/policy.go](pkg/policy/policy.go)): - Policy loaded from `~/.config/ORLY/policy.json` at startup - `P` struct with unexported `rules` field (map[int]Rule) - `PolicyManager` manages script runners for external policy scripts - `LoadFromFile()` method exists for loading policy from disk - No hot reload mechanism currently exists **ACL System** ([pkg/acl/follows.go](pkg/acl/follows.go)): - Separate from policy system - Manages admin/owner/follows lists for write access control - Fetches kind 3 events from relays - Has callback mechanism for updates **Event Handling** ([app/handle-event.go](app/handle-event.go)):213-226 - Special handling for NIP-43 events (join/leave requests) - Pattern: Check kind early, process, return early **Web UI**: - Svelte-based component architecture - Tab-based navigation in [app/web/src/App.svelte](app/web/src/App.svelte) - API endpoints follow `/api//` pattern ## Feature 1: Dynamic Policy Configuration (Kind 12345) ### Design **Event Kind:** 12345 (Relay Policy Configuration) **Purpose:** Allow admins/owners to update policy configuration via Nostr event **Security:** Only admins/owners can publish; only visible to admins/owners **Process Flow:** 1. Admin/owner creates kind 12345 event with JSON policy in `content` field 2. Relay receives event via WebSocket 3. Validate sender is admin/owner 4. Pause policy manager (stop script runners) 5. Parse and validate JSON configuration 6. Apply new policy configuration 7. Persist to `~/.config/ORLY/policy.json` 8. Resume policy manager (restart script runners) 9. Send OK response ### Implementation Steps #### Step 1.1: Define Kind Constant **File:** Create `pkg/protocol/policyconfig/policyconfig.go` ```go package policyconfig const ( // KindPolicyConfig is a relay-internal event for policy configuration updates // Only visible to admins and owners KindPolicyConfig uint16 = 12345 ) ``` #### Step 1.2: Add Policy Hot Reload Methods **File:** [pkg/policy/policy.go](pkg/policy/policy.go) Add methods to `P` struct: ```go // Reload loads policy from JSON bytes and applies it to the existing policy instance // This pauses the policy manager, updates configuration, and resumes func (p *P) Reload(policyJSON []byte) error // Pause pauses the policy manager and stops all script runners func (p *P) Pause() error // Resume resumes the policy manager and restarts script runners func (p *P) Resume() error // SaveToFile persists the current policy configuration to disk func (p *P) SaveToFile(configPath string) error ``` **Implementation Details:** - `Reload()` should: - Call `Pause()` to stop all script runners - Unmarshal JSON into policy struct (using shadow struct pattern) - Validate configuration - Populate binary caches - Call `SaveToFile()` to persist - Call `Resume()` to restart scripts - Return error if any step fails - `Pause()` should: - Iterate through `p.manager.runners` map - Call `Stop()` on each runner - Set a paused flag on the manager - `Resume()` should: - Clear paused flag - Call `startPolicyIfExists()` to restart default script - Restart any rule-specific scripts that were running - `SaveToFile()` should: - Marshal policy to JSON (using pJSON shadow struct) - Write atomically to config path (write to temp file, then rename) #### Step 1.3: Handle Kind 12345 Events **File:** [app/handle-event.go](app/handle-event.go) Add handling after NIP-43 special events (after line 226): ```go // Handle policy configuration update events (kind 12345) case policyconfig.KindPolicyConfig: // Process policy config update and return early if err = l.HandlePolicyConfigUpdate(env.E); chk.E(err) { log.E.F("failed to process policy config update: %v", err) if err = Ok.Error(l, env, err.Error()); chk.E(err) { return } return } // Send OK response if err = Ok.Ok(l, env, "policy configuration updated"); chk.E(err) { return } return ``` Create new file: `app/handle-policy-config.go` ```go // HandlePolicyConfigUpdate processes kind 12345 policy configuration events // Only admins and owners can update policy configuration func (l *Listener) HandlePolicyConfigUpdate(ev *event.E) error { // 1. Verify sender is admin or owner // 2. Parse JSON from event content // 3. Validate JSON structure // 4. Call l.policyManager.Reload(jsonBytes) // 5. Log success/failure return nil } ``` **Security Checks:** - Verify `ev.Pubkey` is in admins or owners list - Validate JSON syntax before applying - Catch all errors and return descriptive messages - Log all policy update attempts (success and failure) #### Step 1.4: Query Filtering (Optional) **File:** [app/handle-req.go](app/handle-req.go) Add filter to hide kind 12345 from non-admins: ```go // In handleREQ, after ACL checks: // Filter out policy config events (kind 12345) for non-admin users if !isAdminOrOwner(l.authedPubkey.Load(), l.Admins, l.Owners) { // Remove kind 12345 from filter for _, f := range filters { f.Kinds.Remove(policyconfig.KindPolicyConfig) } } ``` ## Feature 2: Administrator Follow List Whitelisting ### Design **Purpose:** Enable policy-based follow list whitelisting (separate from ACL follows) **Use Case:** Policy admins can designate follows who get special policy privileges **Configuration:** ```json { "policy_admins": ["admin_pubkey_hex_1", "admin_pubkey_hex_2"], "policy_follow_whitelist_enabled": true, "rules": { "1": { "write_allow_follows": true // Allow writes from policy admin follows } } } ``` ### Implementation Steps #### Step 2.1: Extend Policy Configuration Structure **File:** [pkg/policy/policy.go](pkg/policy/policy.go) Extend `P` struct: ```go type P struct { Kind Kinds `json:"kind"` rules map[int]Rule Global Rule `json:"global"` DefaultPolicy string `json:"default_policy"` // New fields for follow list whitelisting PolicyAdmins []string `json:"policy_admins,omitempty"` PolicyFollowWhitelistEnabled bool `json:"policy_follow_whitelist_enabled,omitempty"` // Unexported cached data policyAdminsBin [][]byte // Binary cache for admin pubkeys policyFollows [][]byte // Cached follow list from policy admins policyFollowsMx sync.RWMutex // Protect follows list manager *PolicyManager } ``` Extend `Rule` struct: ```go type Rule struct { // ... existing fields ... // New field for follow-based whitelisting WriteAllowFollows bool `json:"write_allow_follows,omitempty"` ReadAllowFollows bool `json:"read_allow_follows,omitempty"` } ``` Update `pJSON` shadow struct to include new fields. #### Step 2.2: Add Follow List Fetching **File:** [pkg/policy/policy.go](pkg/policy/policy.go) Add methods: ```go // FetchPolicyFollows fetches follow lists (kind 3) from database for policy admins // This is called during policy load and can be called periodically func (p *P) FetchPolicyFollows(db database.D) error { p.policyFollowsMx.Lock() defer p.policyFollowsMx.Unlock() // Clear existing follows p.policyFollows = nil // For each policy admin, query kind 3 events for _, adminPubkey := range p.policyAdminsBin { // Build filter for kind 3 from this admin // Query database for latest kind 3 event // Extract p-tags from event // Add to p.policyFollows list } return nil } // IsPolicyFollow checks if pubkey is in policy admin follows func (p *P) IsPolicyFollow(pubkey []byte) bool { p.policyFollowsMx.RLock() defer p.policyFollowsMx.RUnlock() for _, follow := range p.policyFollows { if utils.FastEqual(pubkey, follow) { return true } } return false } ``` #### Step 2.3: Integrate Follow Checking in Policy Rules **File:** [pkg/policy/policy.go](pkg/policy/policy.go) Update `checkRulePolicy()` method (around line 1062): ```go // In write access checks, after checking write_allow list: if access == "write" { // Check if follow-based whitelisting is enabled for this rule if rule.WriteAllowFollows && p.PolicyFollowWhitelistEnabled { if p.IsPolicyFollow(loggedInPubkey) { return true, nil // Allow write from policy admin follow } } // Continue with existing write_allow checks... } // Similar for read access: if access == "read" { if rule.ReadAllowFollows && p.PolicyFollowWhitelistEnabled { if p.IsPolicyFollow(loggedInPubkey) { return true, nil // Allow read from policy admin follow } } // Continue with existing read_allow checks... } ``` #### Step 2.4: Periodic Follow List Refresh **File:** [pkg/policy/policy.go](pkg/policy/policy.go) Add to `NewWithManager()`: ```go // Start periodic follow list refresh for policy admins if len(policy.PolicyAdmins) > 0 && policy.PolicyFollowWhitelistEnabled { go policy.startPeriodicFollowRefresh(ctx) } ``` Add method: ```go // startPeriodicFollowRefresh periodically fetches policy admin follow lists func (p *P) startPeriodicFollowRefresh(ctx context.Context) { ticker := time.NewTicker(15 * time.Minute) // Refresh every 15 minutes defer ticker.Stop() // Fetch immediately on startup if err := p.FetchPolicyFollows(p.db); err != nil { log.E.F("failed to fetch policy follows: %v", err) } for { select { case <-ctx.Done(): return case <-ticker.C: if err := p.FetchPolicyFollows(p.db); err != nil { log.E.F("failed to fetch policy follows: %v", err) } else { log.I.F("refreshed policy admin follow lists") } } } } ``` **Note:** Need to pass database reference to policy manager. Update `NewWithManager()` signature: ```go func NewWithManager(ctx context.Context, appName string, enabled bool, db *database.D) *P ``` ## Feature 3: Web Interface for Policy Management ### Design **Components:** 1. `PolicyView.svelte` - Main policy management UI 2. API endpoints for policy CRUD operations 3. JSON editor with validation 4. Follow list viewer **UI Features:** - View current policy configuration (read-only JSON display) - Edit policy JSON with syntax highlighting - Validate JSON before publishing - Publish kind 12345 event to update policy - View policy admin pubkeys - View follow lists for each policy admin - Add/remove policy admin pubkeys (updates and republishes config) ### Implementation Steps #### Step 3.1: Create Policy View Component **File:** `app/web/src/PolicyView.svelte` Structure: ```svelte

Policy Configuration Management

{#if isLoggedIn && (userRole === "owner" || userRole === "admin")}

Policy Configuration