Class: Ask::AppServer::PermissionHandler

Inherits:
Object
  • Object
show all
Defined in:
lib/ask/app_server/permission_handler.rb

Overview

Protocol-aware permission handler that integrates with ask-agent's before_tool_call hook system.

When a blocked tool is called, this handler:

1. Sends an `interaction/requestPermission` protocol message to the client
2. Blocks the tool thread until the client responds (or timeout)
3. Returns { action: :proceed } if approved, { action: :block } if denied

Usage:

handler = PermissionHandler.new(mode: :on_request)
handler.on_request { |req_id, tool_name, args| send_protocol_message(...) }

# Wire into ask-agent session
session = Ask::Agent::Session.new(hooks: { before_tool: [handler] })

# When the client responds:
handler.handle_response(request_id, "approve")

Constant Summary collapse

DEFAULT_BLOCKED_TOOLS =

Default tools that require permission.

%i[write edit bash destroy].freeze
DEFAULT_TIMEOUT =

Default timeout in seconds.

300

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(mode: :on_request, blocked_tools: nil, timeout: DEFAULT_TIMEOUT) ⇒ PermissionHandler

Returns a new instance of PermissionHandler.

Parameters:

  • mode (Symbol) (defaults to: :on_request)

    :on_request (ask for dangerous tools) or :never (allow all)

  • blocked_tools (Array<Symbol>) (defaults to: nil)

    list of tool names that require permission

  • timeout (Integer) (defaults to: DEFAULT_TIMEOUT)

    seconds to wait for client response



36
37
38
39
40
41
42
43
44
# File 'lib/ask/app_server/permission_handler.rb', line 36

def initialize(mode: :on_request, blocked_tools: nil, timeout: DEFAULT_TIMEOUT)
  @mode = mode
  @blocked_tools = (blocked_tools || DEFAULT_BLOCKED_TOOLS).map(&:to_sym)
  @timeout = timeout
  @pending = {}
  @sender = nil
  @mutex = Mutex.new
  @logger = Logger.new($stdout, level: ENV["DEBUG"] ? Logger::DEBUG : Logger::WARN)
end

Instance Attribute Details

#modeObject (readonly)

Returns the value of attribute mode.



31
32
33
# File 'lib/ask/app_server/permission_handler.rb', line 31

def mode
  @mode
end

Instance Method Details

#before_tool_call(tool_call, _context = {}) ⇒ Object

Hook interface for Ask::Agent::Session's before_tool_call chain. Returns { action: :proceed } or { action: :block, reason: "..." }.



54
55
56
57
58
59
# File 'lib/ask/app_server/permission_handler.rb', line 54

def before_tool_call(tool_call, _context = {})
  return { action: :proceed } unless @blocked_tools.include?(tool_call.name.to_sym)
  return { action: :proceed } if @mode == :never

  request_approval(tool_call)
end

#cancel_all!Object

Cancel all pending permission requests (e.g., on session shutdown).



80
81
82
83
84
85
86
87
88
89
90
# File 'lib/ask/app_server/permission_handler.rb', line 80

def cancel_all!
  @mutex.synchronize do
    @pending.each_value do |entry|
      entry[:responded] = true
      entry[:approved] = false
      entry[:reason] = "Permission request cancelled"
      entry[:condition].signal
    end
    @pending.clear
  end
end

#handle_response(request_id, decision, reason: nil) ⇒ Object

Called by the server when the client responds to a permission request.

Parameters:

  • request_id (String)

    the ID that was sent in the permission request

  • decision (String)

    "approve" or "deny"

  • reason (String, nil) (defaults to: nil)

    optional reason from the client



66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/ask/app_server/permission_handler.rb', line 66

def handle_response(request_id, decision, reason: nil)
  @mutex.synchronize do
    entry = @pending[request_id]
    return false unless entry

    entry[:responded] = true
    entry[:approved] = (decision.to_s == "approve")
    entry[:reason] = reason
    entry[:condition].signal
    true
  end
end

#on_request(&block) ⇒ Object

Register a callback for sending the protocol message. The callback receives (request_id, tool_name, tool_arguments).



48
49
50
# File 'lib/ask/app_server/permission_handler.rb', line 48

def on_request(&block)
  @sender = block
end

#pending?Boolean

Are there any pending permission requests?

Returns:

  • (Boolean)


98
99
100
# File 'lib/ask/app_server/permission_handler.rb', line 98

def pending?
  pending_count > 0
end

#pending_countObject

Number of pending permission requests.



93
94
95
# File 'lib/ask/app_server/permission_handler.rb', line 93

def pending_count
  @mutex.synchronize { @pending.size }
end