agent-harness 0.37.4 → 0.38.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6322fafc832efbe89431c6fabc107e63fc04b134ce0b2f70550b31bd2bab7851
4
- data.tar.gz: 22d341b307329cd20ad0d642cde90d248b45fa697b64d1859dec2418ca5a2bdd
3
+ metadata.gz: 07b7223207e544b8728f23b9800ddc774b0c70d16cdd899ccac23550e4a10270
4
+ data.tar.gz: d004705a37d7d83fa6e27f16f2b3593ffb81a06f30980e4aa699a92bb9f06237
5
5
  SHA512:
6
- metadata.gz: 9651e9e6a23298a62919bd119a9c051e4f96138de537129560a70a59e8344a610b2b861641ecb0dbee85d1e3dacc74177fc3a5495aac031fce7ab325e68dca8e
7
- data.tar.gz: 74b0101bed2183c2f3a1124b906be769cbab825dbf0fe1be22852458bc907cd499075f6fc04ad3dda77def25b7de576afb04a681a8826f469df1ab6d5c414955
6
+ metadata.gz: 9d59ea2adf8708b5eb6177fa2314d01e6eb83cb5b8d7748ccc19b7bc07394b1edea040015071df52d53ffb39b1684920092d393a94eeb690058356e05a4eafe5
7
+ data.tar.gz: d9851948407b188ed95b18413b1eb647245cfe76ea0770e1952972c2a3f83c2a8bd979c90161cc04d4af78241d7dcc83f197451523af233314c28b8d853f9f12
@@ -1,3 +1,3 @@
1
1
  {
2
- ".": "0.37.4"
2
+ ".": "0.38.0"
3
3
  }
data/CHANGELOG.md CHANGED
@@ -5,6 +5,20 @@
5
5
  * add runner model compatibility contract (`AgentHarness.model_compatibility`) with structured `ModelCompatibility::Result` outcomes. Codex exposes static facts for CLI-gated models (e.g. `gpt-5.5` requires Codex CLI `>= 0.116.0`), a baseline supported-model list, supported auth modes, and a `DEFAULT_COMPATIBLE_MODEL_ID` fallback so downstream orchestrators can validate tier/model assignments before scheduling agent runs ([#259](https://github.com/viamin/agent-harness/issues/259)).
6
6
  * **auth:** add provider-owned PKCE code-exchange API for Claude OAuth (`AgentHarness::Authentication.exchange_code`). Takes an authorization code plus PKCE verifier (and `redirect_uri`/`client_id`), posts an `authorization_code` grant to the Claude token endpoint, and persists the resulting access/refresh tokens in the native `claudeAiOauth` shape. Adds `exchange_code_supported?` and a `code_exchange` key to `auth_capabilities` ([#266](https://github.com/viamin/agent-harness/issues/266)).
7
7
 
8
+ ## [0.38.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.37.5...agent-harness/v0.38.0) (2026-09-24)
9
+
10
+
11
+ ### Features
12
+
13
+ * **codex:** expose account-local discovery for container recovery ([#428](https://github.com/viamin/agent-harness/issues/428)) ([a67c11f](https://github.com/viamin/agent-harness/commit/a67c11f46aaf99cd440cbb69edbd3272f3ed9312))
14
+
15
+ ## [0.37.5](https://github.com/viamin/agent-harness/compare/agent-harness/v0.37.4...agent-harness/v0.37.5) (2026-09-21)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * **codex:** allow GPT-5.6 tier variants with subscription auth ([#415](https://github.com/viamin/agent-harness/issues/415)) ([22c83df](https://github.com/viamin/agent-harness/commit/22c83df391a73f1f8f6172c1203d1d16c9e6dfcc))
21
+
8
22
  ## [0.37.4](https://github.com/viamin/agent-harness/compare/agent-harness/v0.37.3...agent-harness/v0.37.4) (2026-09-20)
9
23
 
10
24
 
data/README.md CHANGED
@@ -419,6 +419,24 @@ result.fallback_model_id # => "gpt-5.2-codex"
419
419
  result.source # => :static_contract
420
420
  ```
421
421
 
422
+ For account-local Codex discovery, use the same executor and credential
423
+ environment as execution:
424
+
425
+ ```ruby
426
+ provider = AgentHarness::Providers::Codex.new(executor: container_executor)
427
+ discovery = provider.discover_available_models(env: subscription_env, timeout: 15)
428
+ discovery.models # normalized visible entries from model/list
429
+ discovery.recommended_model_id # provider default, not proof of execution
430
+ ```
431
+
432
+ This method performs a fresh, paginated app-server exchange within the supplied
433
+ execution context. It supports execute-only container transports using Node
434
+ (already required by the Codex npm installation). It never changes auth mode or
435
+ selects a model on the caller's behalf. Apply project policy, verify the chosen
436
+ model with `smoke_test`, and persist successful evidence in the calling system.
437
+ `classify_model_rejection_from_result(stdout:, stderr:)` excludes ordinary
438
+ assistant/tool stdout from rejection classification.
439
+
422
440
  Outcomes follow three explicit shapes:
423
441
 
424
442
  - **Supported** — `result.supported?` is `true`. The runner contract
@@ -3,6 +3,7 @@
3
3
  require "json"
4
4
  require "net/http"
5
5
  require "uri"
6
+ require_relative "codex_model_discovery"
6
7
 
7
8
  module AgentHarness
8
9
  module Providers
@@ -12,6 +13,7 @@ module AgentHarness
12
13
  class Codex < Base
13
14
  include RateLimitResetParsing
14
15
  include McpConfigFileSupport
16
+ include CodexModelDiscovery
15
17
 
16
18
  StreamingEvent = Struct.new(
17
19
  :type, :turn, :tokens, :error_message, :tool_name, :raw_event
@@ -66,9 +68,9 @@ module AgentHarness
66
68
  "gpt-5.5-codex" => {minimum_cli_version: "0.116.0"},
67
69
  "gpt-5.5-pro" => {auth_modes: [:api_key].freeze},
68
70
  "gpt-5.6" => {auth_modes: [:api_key].freeze},
69
- "gpt-5.6-luna" => {auth_modes: [:api_key].freeze},
70
- "gpt-5.6-sol" => {auth_modes: [:api_key].freeze},
71
- "gpt-5.6-terra" => {auth_modes: [:api_key].freeze},
71
+ "gpt-5.6-luna" => {auth_modes: %i[api_key subscription].freeze},
72
+ "gpt-5.6-sol" => {auth_modes: %i[api_key subscription].freeze},
73
+ "gpt-5.6-terra" => {auth_modes: %i[api_key subscription].freeze},
72
74
  "gpt-5.3-codex" => {auth_modes: [:api_key].freeze}
73
75
  }.each_value(&:freeze).freeze
74
76
 
@@ -546,6 +548,24 @@ module AgentHarness
546
548
  parser_instance.send(:classify_model_rejection, output, configured_model: configured_model)
547
549
  end
548
550
 
551
+ # Successful assistant/tool output can quote the exact rejection text.
552
+ # Only CLI stderr and explicit JSONL error envelopes are evidence.
553
+ def classify_model_rejection_from_result(stdout:, stderr:, configured_model: nil)
554
+ texts = stdout.to_s.each_line.filter_map do |line|
555
+ event = parse_stdout_jsonl_event(line.strip)
556
+ next unless event.is_a?(Hash)
557
+
558
+ event = unwrap_classification_event(event)
559
+ extract_jsonl_error_text(event) if event.is_a?(Hash)
560
+ end
561
+ texts << stderr.to_s
562
+ texts.each do |text|
563
+ rejection = classify_model_rejection(text, configured_model: configured_model)
564
+ return rejection if rejection
565
+ end
566
+ nil
567
+ end
568
+
549
569
  private
550
570
 
551
571
  def classify_stdout_chunk(text, buffer)
@@ -1095,11 +1115,15 @@ module AgentHarness
1095
1115
  end
1096
1116
 
1097
1117
  def fetch_model_list(rejected_model_id:, allowed_model_ids:, env:, timeout:)
1098
- result = @executor.execute_interactive(
1099
- [self.class.binary_name, "app-server", "--listen", "stdio://"],
1100
- timeout: timeout,
1101
- env: env
1102
- ) { |stdin, stdout| exchange_model_list_requests(stdin, stdout) }
1118
+ result = if @executor.respond_to?(:execute_interactive)
1119
+ @executor.execute_interactive(
1120
+ [self.class.binary_name, "app-server", "--listen", "stdio://"],
1121
+ timeout: timeout,
1122
+ env: env
1123
+ ) { |stdin, stdout| exchange_model_list_requests(stdin, stdout) }
1124
+ else
1125
+ execute_model_discovery(env: env, timeout: timeout)
1126
+ end
1103
1127
  return unavailable_model_discovery(:app_server_failed, stderr: result.stderr) unless result.success?
1104
1128
 
1105
1129
  response = parse_app_server_response(result.stdout, 2)
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AgentHarness
4
+ module Providers
5
+ # Runs the interactive protocol beside the CLI, including for executors that
6
+ # only expose execute (Docker/remote transports). Codex's npm installation
7
+ # already requires Node. No shell, host credentials, or host CLI is used.
8
+ module CodexModelDiscovery
9
+ SCRIPT = <<~'JS'
10
+ const { spawn } = require('node:child_process');
11
+ const child = spawn(process.argv[1], ['app-server', '--listen', 'stdio://'], {stdio: ['pipe', 'pipe', 'pipe']});
12
+ let buffer = '', bytes = 0, page = 0, done = false;
13
+ const models = [], cursors = new Set();
14
+ const send = value => child.stdin.write(JSON.stringify(value) + '\n');
15
+ const finish = (code, value) => {
16
+ if (done) return;
17
+ done = true;
18
+ clearTimeout(timer);
19
+ if (value) process.stdout.write(JSON.stringify(value) + '\n');
20
+ process.exitCode = code;
21
+ child.stdin.end();
22
+ child.kill('SIGTERM');
23
+ setTimeout(() => child.kill('SIGKILL'), 250).unref();
24
+ };
25
+ const timer = setTimeout(() => finish(1), Number(process.argv[2]));
26
+ child.on('error', () => finish(1));
27
+ child.stdin.on('error', () => finish(1));
28
+ child.stderr.on('data', () => {});
29
+ child.on('close', () => { if (!done) finish(1); });
30
+ child.stdout.on('data', chunk => {
31
+ bytes += chunk.length;
32
+ if (bytes > 1048576) return finish(1);
33
+ buffer += chunk.toString();
34
+ let end;
35
+ while (!done && (end = buffer.indexOf('\n')) >= 0) {
36
+ const line = buffer.slice(0, end); buffer = buffer.slice(end + 1);
37
+ let msg; try { msg = JSON.parse(line); } catch { continue; }
38
+ if (msg.id !== 1 && msg.id !== 2) continue;
39
+ if (msg.error) return finish(0, {id: 2, error: msg.error});
40
+ if (msg.id === 1) {
41
+ send({method: 'initialized', params: {}});
42
+ send({id: 2, method: 'model/list', params: {limit: 100}});
43
+ } else {
44
+ if (!Array.isArray(msg.result?.data)) return finish(1);
45
+ models.push(...msg.result.data);
46
+ const cursor = msg.result.nextCursor;
47
+ if (!cursor) return finish(0, {id: 2, result: {data: models}});
48
+ if (++page >= 10 || cursors.has(cursor)) return finish(1);
49
+ cursors.add(cursor);
50
+ send({id: 2, method: 'model/list', params: {limit: 100, cursor}});
51
+ }
52
+ }
53
+ });
54
+ send({id: 1, method: 'initialize', params: {clientInfo: {name: 'agent-harness', version: '1.0.0'}}});
55
+ JS
56
+
57
+ # Fresh account-local discovery. The caller owns selection policy and must
58
+ # still smoke-test its selected model; model/list is not execution proof.
59
+ def discover_available_models(env:, timeout: 15)
60
+ result = execute_model_discovery(env: env, timeout: timeout)
61
+ return unavailable_model_discovery(:app_server_failed) unless result.success?
62
+
63
+ response = parse_app_server_response(result.stdout, 2)
64
+ return unavailable_model_discovery(:model_list_missing_response) unless response
65
+ return unavailable_model_discovery(:model_list_error) if response["error"]
66
+
67
+ entries = Array(response.dig("result", "data")).filter_map { |entry| normalize_model_entry(entry) }
68
+ .reject { |entry| entry[:hidden] }.uniq { |entry| entry[:id] }
69
+ return unavailable_model_discovery(:no_compatible_model) if entries.empty?
70
+
71
+ preferred = entries.find { |entry| entry[:is_default] } || entries.first
72
+ self.class::ModelDiscovery.new(status: :available, models: entries,
73
+ recommended_model_id: preferred[:id], source: :codex_app_server_model_list)
74
+ rescue TimeoutError
75
+ unavailable_model_discovery(:app_server_timeout)
76
+ end
77
+
78
+ private
79
+
80
+ def execute_model_discovery(env:, timeout:)
81
+ @executor.execute(
82
+ ["node", "-e", SCRIPT, self.class.binary_name, [(timeout * 1000).to_i - 500, 100].max.to_s],
83
+ env: env, timeout: timeout
84
+ )
85
+ end
86
+ end
87
+ end
88
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AgentHarness
4
- VERSION = "0.37.4"
4
+ VERSION = "0.38.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: agent-harness
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.37.4
4
+ version: 0.38.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bart Agapinan
@@ -128,6 +128,7 @@ files:
128
128
  - lib/agent_harness/providers/anthropic.rb
129
129
  - lib/agent_harness/providers/base.rb
130
130
  - lib/agent_harness/providers/codex.rb
131
+ - lib/agent_harness/providers/codex_model_discovery.rb
131
132
  - lib/agent_harness/providers/cursor.rb
132
133
  - lib/agent_harness/providers/gemini.rb
133
134
  - lib/agent_harness/providers/github_copilot.rb