Add ORLY_POLICY_PATH for custom policy file location
Some checks failed
Go / build-and-release (push) Has been cancelled

- Add ORLY_POLICY_PATH environment variable to configure custom policy
  file path, overriding the default ~/.config/ORLY/policy.json location
- Enforce ABSOLUTE paths only - relay panics on startup if relative path
  is provided, preventing common misconfiguration errors
- Update PolicyManager to store and expose configPath for hot-reload saves
- Add ConfigPath() method to P struct delegating to internal PolicyManager
- Update NewWithManager() signature to accept optional custom path parameter
- Add BUG_REPORTS_AND_FEATURE_REQUEST_PROTOCOL.md with issue submission
  guidelines requiring environment details, reproduction steps, and logs
- Update README.md with system requirements (500MB minimum memory) and
  link to bug report protocol
- Update CLAUDE.md and README.md documentation for new ORLY_POLICY_PATH

Files modified:
- app/config/config.go: Add PolicyPath config field
- pkg/policy/policy.go: Add configPath storage and validation
- app/handle-policy-config.go: Use policyManager.ConfigPath()
- app/main.go: Pass cfg.PolicyPath to NewWithManager
- pkg/policy/*_test.go: Update test calls with new parameter
- BUG_REPORTS_AND_FEATURE_REQUEST_PROTOCOL.md: New file
- README.md, CLAUDE.md: Documentation updates

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-14 18:36:04 +01:00
parent 20293046d3
commit b58b91cd14
17 changed files with 335 additions and 20 deletions

View File

@@ -96,7 +96,7 @@ func TestBugReproduction_WithPolicyManager(t *testing.T) {
// Create policy with manager (enabled)
ctx := context.Background()
policy := NewWithManager(ctx, "ORLY", true)
policy := NewWithManager(ctx, "ORLY", true, "")
// Load policy from file
if err := policy.LoadFromFile(policyPath); err != nil {

View File

@@ -31,7 +31,7 @@ func setupTestPolicy(t *testing.T, appName string) (*P, func()) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
policy := NewWithManager(ctx, appName, true)
policy := NewWithManager(ctx, appName, true, "")
if policy == nil {
cancel()
os.RemoveAll(configDir)

View File

@@ -29,7 +29,7 @@ func setupHotreloadTestPolicy(t *testing.T, appName string) (*P, func()) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
policy := NewWithManager(ctx, appName, true)
policy := NewWithManager(ctx, appName, true, "")
if policy == nil {
cancel()
os.RemoveAll(configDir)

View File

@@ -514,12 +514,19 @@ type PolicyManager struct {
ctx context.Context
cancel context.CancelFunc
configDir string
configPath string // Path to policy.json file
scriptPath string // Default script path for backward compatibility
enabled bool
mutex sync.RWMutex
runners map[string]*ScriptRunner // Map of script path -> runner
}
// ConfigPath returns the path to the policy configuration file.
// This is used by hot-reload handlers to know where to save updated policy.
func (pm *PolicyManager) ConfigPath() string {
return pm.configPath
}
// P represents a complete policy configuration for a Nostr relay.
// It defines access control rules, kind filtering, and default behavior.
// Policies are evaluated in order: global rules, kind filtering, specific rules, then default policy.
@@ -695,6 +702,15 @@ func (p *P) IsEnabled() bool {
return p != nil && p.manager != nil && p.manager.IsEnabled()
}
// ConfigPath returns the path to the policy configuration file.
// Delegates to the internal PolicyManager.
func (p *P) ConfigPath() string {
if p == nil || p.manager == nil {
return ""
}
return p.manager.ConfigPath()
}
// getDefaultPolicyAction returns true if the default policy is "allow", false if "deny"
func (p *P) getDefaultPolicyAction() (allowed bool) {
switch p.DefaultPolicy {
@@ -711,10 +727,29 @@ func (p *P) getDefaultPolicyAction() (allowed bool) {
// NewWithManager creates a new policy with a policy manager for script execution.
// It initializes the policy manager, loads configuration from files, and starts
// background processes for script management and periodic health checks.
func NewWithManager(ctx context.Context, appName string, enabled bool) *P {
//
// The customPolicyPath parameter allows overriding the default policy file location.
// If empty, uses the default path: $HOME/.config/{appName}/policy.json
// If provided, it MUST be an absolute path (starting with /) or the function will panic.
func NewWithManager(ctx context.Context, appName string, enabled bool, customPolicyPath string) *P {
configDir := filepath.Join(xdg.ConfigHome, appName)
scriptPath := filepath.Join(configDir, "policy.sh")
configPath := filepath.Join(configDir, "policy.json")
// Determine the policy config path
var configPath string
if customPolicyPath != "" {
// Validate that custom path is absolute
if !filepath.IsAbs(customPolicyPath) {
panic(fmt.Sprintf("FATAL: ORLY_POLICY_PATH must be an ABSOLUTE path (starting with /), got: %q", customPolicyPath))
}
configPath = customPolicyPath
// Update configDir to match the custom path's directory for script resolution
configDir = filepath.Dir(customPolicyPath)
scriptPath = filepath.Join(configDir, "policy.sh")
log.I.F("using custom policy path: %s", configPath)
} else {
configPath = filepath.Join(configDir, "policy.json")
}
ctx, cancel := context.WithCancel(ctx)
@@ -722,6 +757,7 @@ func NewWithManager(ctx context.Context, appName string, enabled bool) *P {
ctx: ctx,
cancel: cancel,
configDir: configDir,
configPath: configPath,
scriptPath: scriptPath,
enabled: enabled,
runners: make(map[string]*ScriptRunner),

View File

@@ -825,7 +825,7 @@ func TestNewWithManager(t *testing.T) {
// Test with disabled policy (doesn't require policy.json file)
t.Run("disabled policy", func(t *testing.T) {
enabled := false
policy := NewWithManager(ctx, appName, enabled)
policy := NewWithManager(ctx, appName, enabled, "")
if policy == nil {
t.Fatal("Expected policy but got nil")

View File

@@ -31,7 +31,7 @@ func setupTagValidationTestPolicy(t *testing.T, appName string) (*P, func()) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
policy := NewWithManager(ctx, appName, true)
policy := NewWithManager(ctx, appName, true, "")
if policy == nil {
cancel()
os.RemoveAll(configDir)

View File

@@ -1 +1 @@
v0.35.2
v0.35.3