Class: Ask::AppServer::Server

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

Overview

JSON-RPC server implementing the ZCode/Codex app-server protocol over stdio.

Communicates via NDJSON (Newline-Delimited JSON) over stdin/stdout. Supports session management, streaming events, and mid-execution injection.

Protocol methods:

session/create, session/list, session/resume, session/subscribe,
session/send, session/events, session/abort, workspace/readState

Notifications (server → client):

session/event, interaction/requestPermission, interaction/requestUserInput

The server also handles incoming responses to its outgoing requests (e.g., client responses to interaction/requestPermission).

Instance Method Summary collapse

Constructor Details

#initialize(session_manager: nil) ⇒ Server

Returns a new instance of Server.



22
23
24
25
26
27
28
29
30
31
32
33
34
# File 'lib/ask/app_server/server.rb', line 22

def initialize(session_manager: nil)
  @session_manager = session_manager || SessionManager.new
  @running = false
  @started_at = nil
  @input_queue = Queue.new
  @response_handlers = {}  # outgoing request_id => Proc
  @outgoing_id = 0
  @logger = Logger.new($stdout, level: ENV["DEBUG"] ? Logger::DEBUG : Logger::WARN)

  # Register the protocol method handlers
  @handlers = {}
  register_default_handlers
end

Instance Method Details

#register_permission_handler(handler) ⇒ Object

Register a PermissionHandler so the server can wire its protocol sender. The server will set up the handler's on_request callback to send interaction/requestPermission messages and route responses back.



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/ask/app_server/server.rb', line 109

def register_permission_handler(handler)
  handler.on_request do |request_id, tool_name, arguments|
    send_request("interaction/requestPermission", {
      requestId: request_id,
      toolName: tool_name,
      input: arguments,
      riskLevel: blocked_tool_risk_level(tool_name),
      reason: "Tool '#{tool_name}' requires approval"
    }) do |result, error|
      if result
        decision = result["decision"] || result[:decision] || "deny"
        handler.handle_response(request_id, decision)
      else
        handler.handle_response(request_id, "deny", reason: error&.dig("message"))
      end
    end
  end
end

#running?Boolean

Whether the server is running.

Returns:

  • (Boolean)


92
93
94
# File 'lib/ask/app_server/server.rb', line 92

def running?
  @running
end

#send_request(method, params, &block) ⇒ Object

Send an outgoing JSON-RPC request to the client. If a block is given, it will be called with (result, error) when the client responds.



99
100
101
102
103
104
# File 'lib/ask/app_server/server.rb', line 99

def send_request(method, params, &block)
  id = next_outgoing_id
  @response_handlers[id] = block if block
  write_line({ id: id, method: method, params: params })
  id
end

#startObject

Start the server — reads from stdin in a background thread. The main thread handles event pushing and shutdown.



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
73
74
75
76
77
78
79
80
81
82
83
84
# File 'lib/ask/app_server/server.rb', line 38

def start
  @running = true
  @started_at = Time.now

  # Reader thread: reads NDJSON lines from stdin
  @reader = Thread.new do
    while @running
      begin
        line = $stdin.gets
        break unless line
        line = line.strip
        next if line.empty?

        @input_queue << JSON.parse(line)
      rescue JSON::ParserError => e
        send_error(nil, -32700, "Parse error: #{e.message}")
      rescue => e
        @logger.error("Reader error: #{e.message}")
        break
      end
    end
    @input_queue << nil  # signal shutdown
  end

  # Event push thread: polls subscribed sessions and pushes notifications
  @pusher = Thread.new do
    while @running
      push_session_events
      sleep 0.1  # 100ms poll interval
    end
  end

  # Main loop: processes incoming messages
  while @running
    msg = @input_queue.pop
    break if msg.nil?

    handle_message(msg)
  end
rescue => e
  @logger.error("Server error: #{e.message}")
  raise
ensure
  @running = false
  @reader&.kill rescue nil
  @pusher&.kill rescue nil
end

#stopObject

Stop the server.



87
88
89
# File 'lib/ask/app_server/server.rb', line 87

def stop
  @running = false
end