Class: Harnex::Adapters::CodexAppServer
- Defined in:
- lib/harnex/adapters/codex_appserver.rb
Overview
Codex ‘app-server` adapter — JSON-RPC over stdio.
Talks to a spawned ‘codex app-server` subprocess by writing newline-delimited JSON-RPC messages on stdin and reading responses + notifications from stdout. Replaces the pane-scraping heuristics in `Adapters::Codex` (legacy, kept behind –legacy-pty).
Defined Under Namespace
Classes: JsonRpcClient
Constant Summary collapse
- CLIENT_TITLE =
"harnex"- CLIENT_NAME =
"harnex"- OPT_OUT_NOTIFICATIONS =
%w[ item/agentMessage/delta item/reasoning/summaryTextDelta item/reasoning/summaryPartAdded item/reasoning/textDelta ].freeze
- REQUEST_METHODS =
%w[ initialize thread/start turn/start turn/interrupt thread/resume ].freeze
- NOTIFICATION_METHODS =
%w[ thread/started turn/started turn/completed item/started item/completed thread/status/changed thread/tokenUsage/updated thread/compacted account/rateLimits/updated error ].freeze
- EVENTS =
%w[task_complete turn_started item_completed disconnected].freeze
- STOP_TERM_GRACE_SECONDS =
0.5- STOP_KILL_GRACE_SECONDS =
1.0- APPROVAL_RESPONSES =
Server→client approval requests harnex auto-approves so dispatched codex workers can run autonomously. Codex sends these via JSON-RPC when its sandbox/approval policy needs a client decision; without a handler the client returns -32601 and codex blocks the operation. Permissions / user-input / dynamic-tool / auth-refresh requests have richer response shapes and are deliberately not auto-handled — they fall through to -32601 until a use case appears.
{ "applyPatchApproval" => { decision: "approved" }, "execCommandApproval" => { decision: "approved" }, "item/commandExecution/requestApproval" => { decision: "accept" }, "item/fileChange/requestApproval" => { decision: "accept" } }.freeze
Constants inherited from Base
Instance Attribute Summary collapse
-
#current_turn_id ⇒ Object
readonly
Returns the value of attribute current_turn_id.
-
#initial_prompt ⇒ Object
readonly
Returns the value of attribute initial_prompt.
-
#last_completed_at ⇒ Object
readonly
Returns the value of attribute last_completed_at.
-
#thread_id ⇒ Object
readonly
Returns the value of attribute thread_id.
Attributes inherited from Base
Instance Method Summary collapse
- #base_command ⇒ Object
-
#build_command ⇒ Object
The harnex-context entry (set by ‘–context`) is delivered via JSON-RPC `turn/start`, not as a CLI argument — codex app-server rejects positional input and would exit immediately.
- #build_send_payload(text:, submit:, enter_only:, screen_text:, force: false) ⇒ Object
- #close ⇒ Object
- #describe ⇒ Object
- #dispatch(prompt:, model: nil, effort: nil) ⇒ Object
-
#handle_server_request(method, _params) ⇒ Object
Auto-approve known approval-style requests so dispatched workers can run without a human-in-the-loop.
-
#initialize(extra_args = []) ⇒ CodexAppServer
constructor
A new instance of CodexAppServer.
-
#inject_exit(_writer, **_kwargs) ⇒ Object
No-op: closing the subprocess is handled via #close.
-
#input_state(_screen_text = nil) ⇒ Object
Override: state is RPC-driven, screen text is ignored.
- #interrupt(turn_id: nil) ⇒ Object
- #on_disconnect(&block) ⇒ Object
- #on_notification(&block) ⇒ Object
- #pid ⇒ Object
- #resume(thread_id:) ⇒ Object
-
#start_rpc(env: nil, cwd: nil, read_io: nil, write_io: nil, pid: nil) ⇒ Object
Start the JSON-RPC client.
- #state ⇒ Object
- #terminate_subprocess(term_grace_seconds: STOP_TERM_GRACE_SECONDS, kill_grace_seconds: STOP_KILL_GRACE_SECONDS) ⇒ Object
- #transport ⇒ Object
Methods inherited from Base
#infer_repo_path, #parse_session_summary, #send_wait_seconds, #wait_for_sendable, #wait_for_sendable_state?
Constructor Details
#initialize(extra_args = []) ⇒ CodexAppServer
Returns a new instance of CodexAppServer.
55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 55 def initialize(extra_args = []) super("codex", extra_args) reject_unsupported_codex_flags! @initial_prompt = extract_initial_prompt(extra_args) @client = nil @thread_id = nil @current_turn_id = nil @state = :disconnected @last_completed_at = nil @notification_handler = nil @disconnect_handler = nil end |
Instance Attribute Details
#current_turn_id ⇒ Object (readonly)
Returns the value of attribute current_turn_id.
53 54 55 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 53 def current_turn_id @current_turn_id end |
#initial_prompt ⇒ Object (readonly)
Returns the value of attribute initial_prompt.
53 54 55 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 53 def initial_prompt @initial_prompt end |
#last_completed_at ⇒ Object (readonly)
Returns the value of attribute last_completed_at.
53 54 55 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 53 def last_completed_at @last_completed_at end |
#thread_id ⇒ Object (readonly)
Returns the value of attribute thread_id.
53 54 55 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 53 def thread_id @thread_id end |
Instance Method Details
#base_command ⇒ Object
72 73 74 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 72 def base_command ["codex", "app-server"] end |
#build_command ⇒ Object
The harnex-context entry (set by ‘–context`) is delivered via JSON-RPC `turn/start`, not as a CLI argument — codex app-server rejects positional input and would exit immediately. Operator- supplied codex flags (passed via `harnex run codex – …`) are appended so e.g. `-c sandbox_mode=danger-full-access` works.
81 82 83 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 81 def build_command base_command + cli_extra_args end |
#build_send_payload(text:, submit:, enter_only:, screen_text:, force: false) ⇒ Object
106 107 108 109 110 111 112 113 114 115 116 117 118 119 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 106 def build_send_payload(text:, submit:, enter_only:, screen_text:, force: false) state = input_state(nil) if !force && submit && !enter_only && state[:input_ready] != true raise ArgumentError, (state, enter_only: enter_only) end raise ArgumentError, "Codex app-server cannot stage input without submitting it" unless submit || enter_only raise ArgumentError, "Codex app-server does not support submit-only input" if enter_only { dispatch: { prompt: text.to_s }, input_state: state, force: force } end |
#close ⇒ Object
192 193 194 195 196 197 198 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 192 def close return unless @client @client.close @client = nil @state = :disconnected end |
#describe ⇒ Object
85 86 87 88 89 90 91 92 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 85 def describe { transport: transport, request_methods: REQUEST_METHODS, notification_methods: NOTIFICATION_METHODS, events: EVENTS } end |
#dispatch(prompt:, model: nil, effort: nil) ⇒ Object
160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 160 def dispatch(prompt:, model: nil, effort: nil) ensure_open! ensure_thread! params = { threadId: @thread_id, input: [{ type: "text", text: prompt.to_s }] } params[:model] = model if model params[:effort] = effort if effort result = @client.request("turn/start", params) @current_turn_id = result.dig("turn", "id") @state = :busy @current_turn_id end |
#handle_server_request(method, _params) ⇒ Object
Auto-approve known approval-style requests so dispatched workers can run without a human-in-the-loop. Returns the response body to serialize as JSON-RPC ‘result`, or `nil` to fall through to -32601.
156 157 158 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 156 def handle_server_request(method, _params) APPROVAL_RESPONSES[method] end |
#inject_exit(_writer, **_kwargs) ⇒ Object
No-op: closing the subprocess is handled via #close.
122 123 124 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 122 def inject_exit(_writer, **_kwargs) nil end |
#input_state(_screen_text = nil) ⇒ Object
Override: state is RPC-driven, screen text is ignored.
99 100 101 102 103 104 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 99 def input_state(_screen_text = nil) { state: @state.to_s, input_ready: @state == :prompt } end |
#interrupt(turn_id: nil) ⇒ Object
176 177 178 179 180 181 182 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 176 def interrupt(turn_id: nil) ensure_open! target = turn_id || @current_turn_id return nil if target.nil? @client.request("turn/interrupt", { threadId: @thread_id, turnId: target }) end |
#on_disconnect(&block) ⇒ Object
130 131 132 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 130 def on_disconnect(&block) @disconnect_handler = block end |
#on_notification(&block) ⇒ Object
126 127 128 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 126 def on_notification(&block) @notification_handler = block end |
#pid ⇒ Object
207 208 209 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 207 def pid @client&.pid end |
#resume(thread_id:) ⇒ Object
184 185 186 187 188 189 190 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 184 def resume(thread_id:) ensure_open! result = @client.request("thread/resume", { threadId: thread_id }) @thread_id = thread_id @state = :prompt result end |
#start_rpc(env: nil, cwd: nil, read_io: nil, write_io: nil, pid: nil) ⇒ Object
Start the JSON-RPC client. In production, spawns the codex subprocess. In tests, callers may pass pre-built IO objects.
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 136 def start_rpc(env: nil, cwd: nil, read_io: nil, write_io: nil, pid: nil) if read_io && write_io @client = JsonRpcClient.new(read_io: read_io, write_io: write_io, pid: pid) else spawn_pid, child_stdin, child_stdout = spawn_subprocess(env, cwd) @client = JsonRpcClient.new(read_io: child_stdout, write_io: child_stdin, pid: spawn_pid) end @client.on_notification { |msg| handle_notification(msg) } @client.on_request { |method, params| handle_server_request(method, params) } @client.on_disconnect { |err| handle_disconnect(err) } @client.start perform_handshake @state = :prompt self end |
#state ⇒ Object
94 95 96 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 94 def state @state end |
#terminate_subprocess(term_grace_seconds: STOP_TERM_GRACE_SECONDS, kill_grace_seconds: STOP_KILL_GRACE_SECONDS) ⇒ Object
200 201 202 203 204 205 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 200 def terminate_subprocess(term_grace_seconds: STOP_TERM_GRACE_SECONDS, kill_grace_seconds: STOP_KILL_GRACE_SECONDS) @client&.terminate_process( term_grace_seconds: term_grace_seconds, kill_grace_seconds: kill_grace_seconds ) end |
#transport ⇒ Object
68 69 70 |
# File 'lib/harnex/adapters/codex_appserver.rb', line 68 def transport :stdio_jsonrpc end |