harnex 0.6.2 → 0.6.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +78 -0
- data/TECHNICAL.md +11 -1
- data/lib/harnex/adapters/codex_appserver.rb +85 -11
- data/lib/harnex/adapters.rb +2 -1
- data/lib/harnex/commands/run.rb +3 -2
- data/lib/harnex/runtime/session.rb +15 -2
- data/lib/harnex/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 354a4709831de10c8d19c40fbc4efa5b6df3d3fb1d0459fe1f4960b986eef8ad
|
|
4
|
+
data.tar.gz: 43e0617dac379a90281798fb7f1bd6973177111341d25cd29a1f0c6166580ada
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3213b7c38243f315b9cb3dfed677c63211d7148e166393f8b599088c14d7d0128ebfd0e583068182c0fff8419e483abd8f0e0f2230c37a25beb4bb6d812d0463
|
|
7
|
+
data.tar.gz: 1aeb182b6e355df8dce1f8445ebfcc3bedea8c7a5655590ca3eeac900b5fdcbef38786a88ef9dc8d3d0eaf2717dc330a3e34e98965ef18af6ef6f5b105d50e95
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,83 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [Unreleased]
|
|
4
|
+
|
|
5
|
+
## [0.6.4] — 2026-05-06
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- JSON-RPC adapter (`codex app-server`): harnex now mediates Codex's
|
|
10
|
+
server-to-client approval requests via the protocol — auto-approves
|
|
11
|
+
`applyPatchApproval`, `execCommandApproval`,
|
|
12
|
+
`item/commandExecution/requestApproval`, and
|
|
13
|
+
`item/fileChange/requestApproval`. Previously the adapter rejected
|
|
14
|
+
every server-side request with `-32601 "Unsupported server request"`,
|
|
15
|
+
which meant Codex's default sandbox blocked shell exec, file changes,
|
|
16
|
+
git commits, and package-manager invocations whenever a dispatched
|
|
17
|
+
worker tried to do real work. Autonomous worker dispatches now run
|
|
18
|
+
cleanly under the default sandbox without needing
|
|
19
|
+
`--dangerously-bypass-approvals-and-sandbox` or
|
|
20
|
+
`-c sandbox_mode=danger-full-access`.
|
|
21
|
+
- `CodexAppServer#build_command` now appends operator-supplied codex
|
|
22
|
+
flags (passed via `harnex run codex -- -c key=value`) while still
|
|
23
|
+
filtering out the harnex-context entry that `--context` smuggles
|
|
24
|
+
through `@extra_args` (codex `app-server` rejects positional input).
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- `--legacy-pty` is now a long-term supported fallback rather than a
|
|
29
|
+
deprecated path. The 0.7.0-removal plan is dropped — the legacy PTY
|
|
30
|
+
adapter remains the right tool for interactive/TUI use cases and for
|
|
31
|
+
any operator who prefers terminal-native Codex chrome. JSON-RPC
|
|
32
|
+
remains the default for autonomous worker dispatches and structured
|
|
33
|
+
observability.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- JSON-RPC adapter: classify sub-5s pre-turn exits as `boot_failure`
|
|
38
|
+
(vs the existing `disconnected` terminal state), tracked by latching
|
|
39
|
+
on `turn/started`. `build_summary_actual` counts `boot_failure` exits
|
|
40
|
+
in `actual.disconnections` so early-boot deaths are not lost from
|
|
41
|
+
dispatch telemetry. First of three planned commits for issue #32;
|
|
42
|
+
remaining work (ensure-block telemetry write + optional `last_error`
|
|
43
|
+
capture) tracked separately.
|
|
44
|
+
|
|
45
|
+
## [0.6.3] — 2026-05-06
|
|
46
|
+
|
|
47
|
+
### Fixed
|
|
48
|
+
|
|
49
|
+
- JSON-RPC adapter (`codex app-server`): `--context` boot injection now
|
|
50
|
+
succeeds against real Codex CLI. Three schema mismatches in
|
|
51
|
+
`Adapters::CodexAppServer` caused 100% session disconnect on boot
|
|
52
|
+
with `"Invalid request: invalid type: null, expected a string"`:
|
|
53
|
+
- `ensure_thread!` and the `thread/started` notification handler read
|
|
54
|
+
`result["threadId"]`, but Codex's actual `thread/start` response is
|
|
55
|
+
`{"thread": {"id": "..."}}`. With `@thread_id = nil`, the subsequent
|
|
56
|
+
`turn/start` sent `threadId: null` and Codex's serde rejected it.
|
|
57
|
+
- `dispatch` sent `input: { content: [{type, text}] }`, but
|
|
58
|
+
`TurnStartParams.input` is an **array** of `UserInput`. Now sends
|
|
59
|
+
`input: [{type: "text", text: "..."}]`.
|
|
60
|
+
- `initialize` joined ALL `extra_args` into `@initial_prompt`, which
|
|
61
|
+
prepended Codex CLI flags (e.g. `-m gpt-5.5-mini -c
|
|
62
|
+
model_reasoning_effort=low`) into the prompt content. Now extracts
|
|
63
|
+
only the harnex-prefixed context element.
|
|
64
|
+
|
|
65
|
+
Re-opens and properly closes #29. The 0.6.2 fix shipped clean tests
|
|
66
|
+
but the test stubs mirrored harnex's wrong assumptions instead of
|
|
67
|
+
Codex's actual JSON-RPC schema, so production was 100% broken on the
|
|
68
|
+
default JSON-RPC path.
|
|
69
|
+
|
|
70
|
+
### Notes
|
|
71
|
+
|
|
72
|
+
- Test stubs in `codex_appserver_lifecycle_test.rb` and
|
|
73
|
+
`session_jsonrpc_test.rb` still mirror harnex's old assumptions.
|
|
74
|
+
Tracked as a follow-up (test rewrite using `codex app-server
|
|
75
|
+
generate-json-schema` as the source of truth, plus a contract-test
|
|
76
|
+
gate). Existing tests remain green; the structural improvement does
|
|
77
|
+
not block this release.
|
|
78
|
+
- `--legacy-pty` remains as the documented fallback. Removal still
|
|
79
|
+
scheduled for 0.7.0 once test-rewrite + contract gate land.
|
|
80
|
+
|
|
3
81
|
## [0.6.2] — 2026-05-06
|
|
4
82
|
|
|
5
83
|
### Fixed
|
data/TECHNICAL.md
CHANGED
|
@@ -330,6 +330,16 @@ The adapter reads the screen and returns a state hash:
|
|
|
330
330
|
- Notifications (`turn/started`, `turn/completed`, `item/completed`,
|
|
331
331
|
`error`, `thread/compacted`, …) fan into the events log.
|
|
332
332
|
`task_complete` is the harnex-side event for `turn/completed`.
|
|
333
|
+
- **Approval mediation**: Codex's app-server protocol delegates
|
|
334
|
+
sandbox/approval decisions to the JSON-RPC client via server-to-
|
|
335
|
+
client requests. Harnex auto-approves `applyPatchApproval`,
|
|
336
|
+
`execCommandApproval`, `item/commandExecution/requestApproval`, and
|
|
337
|
+
`item/fileChange/requestApproval`, so autonomous workers can run
|
|
338
|
+
shell commands, write files, commit, and invoke package managers
|
|
339
|
+
under Codex's default sandbox without any bypass flag. Other
|
|
340
|
+
server-to-client requests (permissions, user-input, dynamic-tool,
|
|
341
|
+
auth-refresh) currently fall through to `-32601` until use cases
|
|
342
|
+
appear.
|
|
333
343
|
- Disconnect is detected from JSON-RPC error responses, subprocess
|
|
334
344
|
EOF, parse errors, or a server `error` notification — no screen
|
|
335
345
|
regex required.
|
|
@@ -339,7 +349,7 @@ The adapter reads the screen and returns a state hash:
|
|
|
339
349
|
- See `docs/codex-appserver.md` for the full mapping table and
|
|
340
350
|
troubleshooting.
|
|
341
351
|
|
|
342
|
-
#### Codex Adapter (legacy PTY — `--legacy-pty`,
|
|
352
|
+
#### Codex Adapter (legacy PTY — `--legacy-pty`, long-term fallback)
|
|
343
353
|
|
|
344
354
|
- Launches with `--no-alt-screen` for inline screen output
|
|
345
355
|
- Detects prompt by looking for `›` prefix in recent lines
|
|
@@ -34,12 +34,25 @@ module Harnex
|
|
|
34
34
|
|
|
35
35
|
EVENTS = %w[task_complete turn_started item_completed disconnected].freeze
|
|
36
36
|
|
|
37
|
+
# Server→client approval requests harnex auto-approves so dispatched
|
|
38
|
+
# codex workers can run autonomously. Codex sends these via JSON-RPC
|
|
39
|
+
# when its sandbox/approval policy needs a client decision; without
|
|
40
|
+
# a handler the client returns -32601 and codex blocks the operation.
|
|
41
|
+
# Permissions / user-input / dynamic-tool / auth-refresh requests
|
|
42
|
+
# have richer response shapes and are deliberately not auto-handled
|
|
43
|
+
# — they fall through to -32601 until a use case appears.
|
|
44
|
+
APPROVAL_RESPONSES = {
|
|
45
|
+
"applyPatchApproval" => { decision: "approved" },
|
|
46
|
+
"execCommandApproval" => { decision: "approved" },
|
|
47
|
+
"item/commandExecution/requestApproval" => { decision: "approved" },
|
|
48
|
+
"item/fileChange/requestApproval" => { decision: "accept" }
|
|
49
|
+
}.freeze
|
|
50
|
+
|
|
37
51
|
attr_reader :thread_id, :current_turn_id, :last_completed_at, :initial_prompt
|
|
38
52
|
|
|
39
53
|
def initialize(extra_args = [])
|
|
40
54
|
super("codex", extra_args)
|
|
41
|
-
@initial_prompt = extra_args
|
|
42
|
-
@initial_prompt = nil if @initial_prompt.empty?
|
|
55
|
+
@initial_prompt = extract_initial_prompt(extra_args)
|
|
43
56
|
@client = nil
|
|
44
57
|
@thread_id = nil
|
|
45
58
|
@current_turn_id = nil
|
|
@@ -57,8 +70,13 @@ module Harnex
|
|
|
57
70
|
["codex", "app-server"]
|
|
58
71
|
end
|
|
59
72
|
|
|
73
|
+
# The harnex-context entry (set by `--context`) is delivered via
|
|
74
|
+
# JSON-RPC `turn/start`, not as a CLI argument — codex app-server
|
|
75
|
+
# rejects positional input and would exit immediately. Operator-
|
|
76
|
+
# supplied codex flags (passed via `harnex run codex -- ...`) are
|
|
77
|
+
# appended so e.g. `-c sandbox_mode=danger-full-access` works.
|
|
60
78
|
def build_command
|
|
61
|
-
base_command
|
|
79
|
+
base_command + cli_extra_args
|
|
62
80
|
end
|
|
63
81
|
|
|
64
82
|
def describe
|
|
@@ -121,6 +139,7 @@ module Harnex
|
|
|
121
139
|
end
|
|
122
140
|
|
|
123
141
|
@client.on_notification { |msg| handle_notification(msg) }
|
|
142
|
+
@client.on_request { |method, params| handle_server_request(method, params) }
|
|
124
143
|
@client.on_disconnect { |err| handle_disconnect(err) }
|
|
125
144
|
@client.start
|
|
126
145
|
perform_handshake
|
|
@@ -128,12 +147,19 @@ module Harnex
|
|
|
128
147
|
self
|
|
129
148
|
end
|
|
130
149
|
|
|
150
|
+
# Auto-approve known approval-style requests so dispatched workers
|
|
151
|
+
# can run without a human-in-the-loop. Returns the response body to
|
|
152
|
+
# serialize as JSON-RPC `result`, or `nil` to fall through to -32601.
|
|
153
|
+
def handle_server_request(method, _params)
|
|
154
|
+
APPROVAL_RESPONSES[method]
|
|
155
|
+
end
|
|
156
|
+
|
|
131
157
|
def dispatch(prompt:, model: nil, effort: nil)
|
|
132
158
|
ensure_open!
|
|
133
159
|
ensure_thread!
|
|
134
160
|
params = {
|
|
135
161
|
threadId: @thread_id,
|
|
136
|
-
input:
|
|
162
|
+
input: [{ type: "text", text: prompt.to_s }]
|
|
137
163
|
}
|
|
138
164
|
params[:model] = model if model
|
|
139
165
|
params[:effort] = effort if effort
|
|
@@ -183,7 +209,28 @@ module Harnex
|
|
|
183
209
|
return if @thread_id
|
|
184
210
|
|
|
185
211
|
result = @client.request("thread/start", {})
|
|
186
|
-
@thread_id = result
|
|
212
|
+
@thread_id = extract_thread_id(result)
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def extract_thread_id(payload)
|
|
216
|
+
return nil unless payload.is_a?(Hash)
|
|
217
|
+
|
|
218
|
+
payload.dig("thread", "id") || payload["threadId"] || payload["thread_id"]
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def extract_initial_prompt(extra_args)
|
|
222
|
+
return nil unless extra_args.is_a?(Array)
|
|
223
|
+
|
|
224
|
+
prefixed = extra_args.find { |a| a.is_a?(String) && a.start_with?("[harnex session id=") }
|
|
225
|
+
return prefixed if prefixed && !prefixed.empty?
|
|
226
|
+
|
|
227
|
+
nil
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Codex CLI flags only — strips the harnex-context entry that
|
|
231
|
+
# `--context` smuggles through @extra_args.
|
|
232
|
+
def cli_extra_args
|
|
233
|
+
@extra_args.reject { |a| a.is_a?(String) && a.start_with?("[harnex session id=") }
|
|
187
234
|
end
|
|
188
235
|
|
|
189
236
|
def perform_handshake
|
|
@@ -207,7 +254,7 @@ module Harnex
|
|
|
207
254
|
|
|
208
255
|
case method
|
|
209
256
|
when "thread/started"
|
|
210
|
-
@thread_id ||= params
|
|
257
|
+
@thread_id ||= extract_thread_id(params)
|
|
211
258
|
when "turn/started"
|
|
212
259
|
@current_turn_id = params["turnId"] || params["turn_id"]
|
|
213
260
|
@state = :busy
|
|
@@ -256,6 +303,7 @@ module Harnex
|
|
|
256
303
|
@id_mutex = Mutex.new
|
|
257
304
|
@write_mutex = Mutex.new
|
|
258
305
|
@notification_handler = nil
|
|
306
|
+
@request_handler = nil
|
|
259
307
|
@disconnect_handler = nil
|
|
260
308
|
@disconnect_signaled = false
|
|
261
309
|
@closed = false
|
|
@@ -266,6 +314,13 @@ module Harnex
|
|
|
266
314
|
@notification_handler = block
|
|
267
315
|
end
|
|
268
316
|
|
|
317
|
+
# Handler for server-initiated requests (id + method). The block
|
|
318
|
+
# receives (method, params) and returns the response body for the
|
|
319
|
+
# JSON-RPC `result` field, or nil to reject with -32601.
|
|
320
|
+
def on_request(&block)
|
|
321
|
+
@request_handler = block
|
|
322
|
+
end
|
|
323
|
+
|
|
269
324
|
def on_disconnect(&block)
|
|
270
325
|
@disconnect_handler = block
|
|
271
326
|
end
|
|
@@ -367,11 +422,7 @@ module Harnex
|
|
|
367
422
|
|
|
368
423
|
def dispatch_message(message)
|
|
369
424
|
if message["id"] && message["method"]
|
|
370
|
-
|
|
371
|
-
jsonrpc: "2.0",
|
|
372
|
-
id: message["id"],
|
|
373
|
-
error: { code: -32601, message: "Unsupported server request: #{message['method']}" }
|
|
374
|
-
})
|
|
425
|
+
handle_server_request(message)
|
|
375
426
|
return
|
|
376
427
|
end
|
|
377
428
|
|
|
@@ -392,6 +443,29 @@ module Harnex
|
|
|
392
443
|
@notification_handler&.call(message) if message["method"]
|
|
393
444
|
end
|
|
394
445
|
|
|
446
|
+
def handle_server_request(message)
|
|
447
|
+
result =
|
|
448
|
+
begin
|
|
449
|
+
@request_handler&.call(message["method"], message["params"] || {})
|
|
450
|
+
rescue StandardError
|
|
451
|
+
nil
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
if result.nil?
|
|
455
|
+
write_line({
|
|
456
|
+
jsonrpc: "2.0",
|
|
457
|
+
id: message["id"],
|
|
458
|
+
error: { code: -32601, message: "Unsupported server request: #{message['method']}" }
|
|
459
|
+
})
|
|
460
|
+
else
|
|
461
|
+
write_line({
|
|
462
|
+
jsonrpc: "2.0",
|
|
463
|
+
id: message["id"],
|
|
464
|
+
result: result
|
|
465
|
+
})
|
|
466
|
+
end
|
|
467
|
+
end
|
|
468
|
+
|
|
395
469
|
def signal_disconnect(error)
|
|
396
470
|
return if @disconnect_signaled
|
|
397
471
|
|
data/lib/harnex/adapters.rb
CHANGED
|
@@ -18,7 +18,8 @@ module Harnex
|
|
|
18
18
|
|
|
19
19
|
# Phase 3 flipped the default — `codex` resolves to CodexAppServer.
|
|
20
20
|
# Legacy PTY adapter is reachable via `legacy_pty: true` (driven by
|
|
21
|
-
# `harnex run codex --legacy-pty`)
|
|
21
|
+
# `harnex run codex --legacy-pty`); kept as a long-term supported
|
|
22
|
+
# fallback for interactive/TUI use cases.
|
|
22
23
|
def codex_appserver_enabled?
|
|
23
24
|
true
|
|
24
25
|
end
|
data/lib/harnex/commands/run.rb
CHANGED
|
@@ -37,8 +37,9 @@ module Harnex
|
|
|
37
37
|
--timeout SECS Max seconds to wait for detached registration (default: #{DEFAULT_TIMEOUT})
|
|
38
38
|
--inbox-ttl SECS Expire queued inbox messages after SECS (default: #{Inbox::DEFAULT_TTL})
|
|
39
39
|
--legacy-pty (codex only) Use the legacy PTY adapter instead of
|
|
40
|
-
the JSON-RPC `app-server` adapter.
|
|
41
|
-
|
|
40
|
+
the JSON-RPC `app-server` adapter. Long-term
|
|
41
|
+
supported fallback for interactive/TUI use; JSON-RPC
|
|
42
|
+
remains the default for autonomous dispatches.
|
|
42
43
|
-h, --help Show this help
|
|
43
44
|
|
|
44
45
|
Notes:
|
|
@@ -74,6 +74,7 @@ module Harnex
|
|
|
74
74
|
@usage_summary = {}
|
|
75
75
|
@ended_at = nil
|
|
76
76
|
@exit_reason = nil
|
|
77
|
+
@turn_started_seen = false
|
|
77
78
|
@last_completed_at = nil
|
|
78
79
|
@writer = nil
|
|
79
80
|
@pid = nil
|
|
@@ -377,6 +378,7 @@ module Harnex
|
|
|
377
378
|
when "thread/started"
|
|
378
379
|
@rpc_thread_id = params["threadId"] || params["thread_id"]
|
|
379
380
|
when "turn/started"
|
|
381
|
+
@turn_started_seen = true
|
|
380
382
|
@state_machine.force_busy!
|
|
381
383
|
emit_event("turn_started", turnId: params["turnId"] || params["turn_id"])
|
|
382
384
|
when "turn/completed"
|
|
@@ -707,12 +709,21 @@ module Harnex
|
|
|
707
709
|
|
|
708
710
|
def classify_exit
|
|
709
711
|
return "timeout" if @exit_code == 124
|
|
712
|
+
return "success" if @exit_code == 0 && session_summary_present?
|
|
713
|
+
return "boot_failure" if boot_failure_exit?
|
|
710
714
|
return "failure" unless @exit_code == 0
|
|
711
|
-
return "success" if session_summary_present?
|
|
712
715
|
|
|
713
716
|
"disconnected"
|
|
714
717
|
end
|
|
715
718
|
|
|
719
|
+
def boot_failure_exit?
|
|
720
|
+
return false unless adapter.transport == :stdio_jsonrpc
|
|
721
|
+
return false if @turn_started_seen
|
|
722
|
+
|
|
723
|
+
lifetime = (@ended_at || Time.now) - @started_at
|
|
724
|
+
lifetime <= 5
|
|
725
|
+
end
|
|
726
|
+
|
|
716
727
|
def session_summary_present?
|
|
717
728
|
@usage_summary.values.any? { |value| !value.nil? }
|
|
718
729
|
end
|
|
@@ -761,7 +772,9 @@ module Harnex
|
|
|
761
772
|
|
|
762
773
|
def build_summary_actual
|
|
763
774
|
counters = @event_counters.snapshot
|
|
764
|
-
|
|
775
|
+
if %w[disconnected boot_failure].include?(@exit_reason)
|
|
776
|
+
counters[:disconnections] = [counters[:disconnections], 1].max
|
|
777
|
+
end
|
|
765
778
|
|
|
766
779
|
{
|
|
767
780
|
model: meta_hash["model"],
|
data/lib/harnex/version.rb
CHANGED