Class: Norn::UI::Gatekeeper

Inherits:
Object
  • Object
show all
Defined in:
lib/norn/ui/gatekeeper.rb

Instance Method Summary collapse

Constructor Details

#initialize(input: $stdin, output: $stdout) ⇒ Gatekeeper

Returns a new instance of Gatekeeper.



9
10
11
12
13
# File 'lib/norn/ui/gatekeeper.rb', line 9

def initialize(input: $stdin, output: $stdout)
  @input = input
  @output = output
  @prompt = TTY::Prompt.new(input: input, output: output)
end

Instance Method Details

#authorize_capabilities(tool_name, caps, args) ⇒ Object



15
16
17
18
19
20
21
22
23
24
# File 'lib/norn/ui/gatekeeper.rb', line 15

def authorize_capabilities(tool_name, caps, args)
  @output.puts "\nāš ļø  Security Escalation: Tool '#{tool_name}' requested unauthorized capabilities: #{caps.join(', ')}"
  @output.puts "Arguments: #{args.inspect}"

  choices = {
    "šŸ”“ Yes, authorize these capabilities" => true,
    "🚫 No, deny authorization" => false
  }
  @prompt.select("Do you want to authorize this operation?", choices)
end

#authorize_danger(tool, args) ⇒ Object



26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# File 'lib/norn/ui/gatekeeper.rb', line 26

def authorize_danger(tool, args)
  tool_name = tool.name
  
  if ["file_write", "file_edit"].include?(tool_name)
    # Interactive File Diff Previewer
    begin
      root = File.expand_path(Norn.workspace_root)
      abs_path = File.expand_path(args[:path].to_s, root)
      unless abs_path == root || abs_path.start_with?(root + File::SEPARATOR)
        raise SecurityError, "Path traversal attempt detected"
      end

      old_content = File.exist?(abs_path) ? File.read(abs_path) : ""
      new_content = if tool_name == "file_write"
                      args[:content].to_s
                    else
                      old_content.sub(args[:old_string].to_s, args[:new_string].to_s)
                    end

      @output.puts "\nšŸ“ Proposed changes to \e[1;32m#{args[:path]}\e[0m:"
      @output.puts Norn::DiffHelper.color_diff(old_content, new_content)
    rescue => e
      @output.puts "Diff Generation Error: #{e.message}"
      return false
    end
  else
    # Generic Danger Warning
    @output.puts "\nāš ļø  Warning: Norn wants to execute a potentially dangerous command: '#{tool_name}'"
    @output.puts "Arguments: #{args.inspect}"
  end

  choices = {
    "šŸ”“ Yes, proceed" => true,
    "🚫 No, abort/review" => false
  }
  
  # Only offer session-level approvals if the tool is not statically dangerous
  # OR if it explicitly allows session-level danger bypass.
  is_statically_dangerous = tool.respond_to?(:dangerous) && tool.dangerous
  allows_session_danger = tool.respond_to?(:allow_session_danger?) && tool.allow_session_danger?
  
  if !is_statically_dangerous || allows_session_danger
    choices["⚔ #{tool.session_approval_label(args)}"] = :session
  end
  
  @prompt.select("Execute this action?", choices)
end

#refine_arguments(tool, args, user_feedback, client) ⇒ Object



83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/norn/ui/gatekeeper.rb', line 83

def refine_arguments(tool, args, user_feedback, client)
  prompt = <<~PROMPT
    You are a precise tool parameter refiner. Your task is to update the parameters of a tool based on user feedback.

    Tool Name: #{tool.name}
    Tool Description: #{tool.description}
    Tool Schema: #{tool.parameters.to_json}

    Original Parameters: #{args.to_json}

    User Feedback: #{user_feedback}

    Analyze the user's feedback, apply requested changes to the original parameters, and ensure they conform to the tool schema.
    Output ONLY a raw, valid JSON object containing the updated parameters.
    Do NOT wrap output in markdown codeblocks. Do NOT add extra explanation or prose.
  PROMPT

  response_result = client.call([{ role: "user", content: prompt }])
  return response_result if response_result.failure?

  response = response_result.value!
  response_text = response.is_a?(Hash) ? response[:content] : response.to_s

  # Robust cleaning of markdown backticks if LLM ignores instruction
  cleaned_text = response_text.gsub(/```json|```/, "").strip

  begin
    parsed = JSON.parse(cleaned_text)
    Success(symbolize_keys(parsed))
  rescue => e
    Failure("Invalid JSON returned by refiner LLM: #{e.message}")
  end
end

#show_fallback_menu(tool_name, args) ⇒ Object



74
75
76
77
78
79
80
81
# File 'lib/norn/ui/gatekeeper.rb', line 74

def show_fallback_menu(tool_name, args)
  choices = {
    "Skip this execution (allow agent to continue without this result)" => :skip,
    "Edit command arguments inline (freeform feedback)" => :edit,
    "Abort the active agent session completely" => :abort
  }
  @prompt.select("\nOperation Aborted. What would you like to do next?", choices)
end