agent-harness 0.9.0 → 0.10.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 +4 -4
- data/.release-please-manifest.json +1 -1
- data/CHANGELOG.md +12 -0
- data/README.md +36 -5
- data/lib/agent_harness/authentication.rb +47 -7
- data/lib/agent_harness/errors.rb +3 -0
- data/lib/agent_harness/providers/codex.rb +26 -3
- data/lib/agent_harness/providers/github_copilot.rb +69 -74
- data/lib/agent_harness/version.rb +1 -1
- data/lib/agent_harness.rb +26 -2
- 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: 3549cf974343fcdb961dea202dc22b84f820671a29f9139f1e36bb9c8a6363d4
|
|
4
|
+
data.tar.gz: a35417365aba964248a50cc44d543fd6e11e18a7c27946d7f7750cdfa6347cf0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: beb28e93b4f37ad2328f70fa4ec0e88677e0b2bf0bff2c2826fb3086aab4b9c0714692f8b17268ec3ecb58815f19021880237c6fff59821d73942cb18b008e5f
|
|
7
|
+
data.tar.gz: d12b1399447b77bf9b570b6791b7426d92e52d691b094f27329a118f1e1ba472daf794a27bc2150a0d8b9a47d825f668dc875f128ac2986e44236792c6d07d10
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.10.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.9.0...agent-harness/v0.10.0) (2026-04-21)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **codex:** expose JSONL transcript parser ([#148](https://github.com/viamin/agent-harness/issues/148)) ([05312ea](https://github.com/viamin/agent-harness/commit/05312eaf9c11fff50931e511ee6e534838eb8746))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* **copilot:** github-copilot-cli does not support the -p flag used by build_command ([#141](https://github.com/viamin/agent-harness/issues/141)) ([d06fbc4](https://github.com/viamin/agent-harness/commit/d06fbc414489d6c3bc93a122d0eb2a5771ddbb26))
|
|
14
|
+
|
|
3
15
|
## [0.9.0](https://github.com/viamin/agent-harness/compare/agent-harness/v0.8.0...agent-harness/v0.9.0) (2026-04-19)
|
|
4
16
|
|
|
5
17
|
|
data/README.md
CHANGED
|
@@ -501,6 +501,29 @@ AgentHarness.auth_status(:claude)
|
|
|
501
501
|
|
|
502
502
|
For providers without a built-in auth check (including `:api_key` providers), `auth_valid?` returns `false` and `auth_status` returns an error indicating the check is not implemented. Custom providers can implement an `auth_status` instance method to provide their own check.
|
|
503
503
|
|
|
504
|
+
### Auth Flow Capabilities
|
|
505
|
+
|
|
506
|
+
Before rendering provider-specific auth controls, check whether the flow is supported:
|
|
507
|
+
|
|
508
|
+
```ruby
|
|
509
|
+
AgentHarness.auth_url_supported?(:claude)
|
|
510
|
+
# => true
|
|
511
|
+
|
|
512
|
+
AgentHarness.auth_url_supported?(:codex)
|
|
513
|
+
# => false
|
|
514
|
+
|
|
515
|
+
AgentHarness.refresh_auth_supported?(:claude)
|
|
516
|
+
# => true
|
|
517
|
+
|
|
518
|
+
AgentHarness.refresh_auth_supported?(:codex)
|
|
519
|
+
# => false
|
|
520
|
+
|
|
521
|
+
AgentHarness.auth_capabilities(:codex)
|
|
522
|
+
# => { auth_type: :api_key, auth_url: false, refresh: false }
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
Provider aliases are resolved the same way as other auth APIs, so `:anthropic` reports the same capabilities as `:claude`. Unknown providers raise `AgentHarness::ProviderNotFoundError`, matching `auth_url` and `refresh_auth` provider lookup behavior.
|
|
526
|
+
|
|
504
527
|
### Auth Error Detection
|
|
505
528
|
|
|
506
529
|
When a CLI agent fails due to expired or invalid authentication, `send_message` raises `AuthenticationError` with the provider name. Authentication errors are always surfaced directly to the caller (never auto-switched to another provider) so your application can trigger the appropriate re-auth flow:
|
|
@@ -509,9 +532,15 @@ When a CLI agent fails due to expired or invalid authentication, `send_message`
|
|
|
509
532
|
begin
|
|
510
533
|
AgentHarness.send_message("Hello", provider: :claude)
|
|
511
534
|
rescue AgentHarness::AuthenticationError => e
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
535
|
+
provider = e.provider
|
|
536
|
+
|
|
537
|
+
if AgentHarness.auth_url_supported?(provider)
|
|
538
|
+
redirect_to AgentHarness.auth_url(provider)
|
|
539
|
+
elsif AgentHarness.refresh_auth_supported?(provider)
|
|
540
|
+
render :reauth_token_form, locals: { provider: provider }
|
|
541
|
+
else
|
|
542
|
+
render :auth_expired_without_refresh, locals: { provider: provider, message: e.message }
|
|
543
|
+
end
|
|
515
544
|
end
|
|
516
545
|
```
|
|
517
546
|
|
|
@@ -524,7 +553,7 @@ AgentHarness.auth_url(:claude)
|
|
|
524
553
|
# => "https://claude.ai/oauth/authorize"
|
|
525
554
|
```
|
|
526
555
|
|
|
527
|
-
This raises `
|
|
556
|
+
This raises `AgentHarness::UnsupportedAuthFlowError` for `:api_key` providers or providers whose OAuth URL flow is not implemented. The exception inherits from `AgentHarness::Error` and `StandardError`, so host applications can rescue it with their normal app-level error handling.
|
|
528
557
|
|
|
529
558
|
### Credential Refresh
|
|
530
559
|
|
|
@@ -537,7 +566,9 @@ AgentHarness.refresh_auth(:claude, token: "new-oauth-token")
|
|
|
537
566
|
|
|
538
567
|
Any existing expiry metadata in the credentials file is cleared on refresh so that `auth_valid?` returns `true` immediately after a successful refresh.
|
|
539
568
|
|
|
540
|
-
This raises `
|
|
569
|
+
This raises `AgentHarness::UnsupportedAuthFlowError` for `:api_key` providers or providers whose credential refresh flow is not implemented. Credential file paths respect the `CLAUDE_CONFIG_DIR` environment variable.
|
|
570
|
+
|
|
571
|
+
If you currently rescue `NotImplementedError` for unsupported auth URL generation or credential refresh, update that code to rescue `AgentHarness::UnsupportedAuthFlowError` or the broader `AgentHarness::Error` instead.
|
|
541
572
|
|
|
542
573
|
## Provider Health Checks
|
|
543
574
|
|
|
@@ -35,19 +35,46 @@ module AgentHarness
|
|
|
35
35
|
end
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# Get authentication flow capabilities for a provider.
|
|
39
|
+
#
|
|
40
|
+
# @param provider_name [Symbol] the provider name
|
|
41
|
+
# @return [Hash] capabilities with :auth_type, :auth_url, :refresh keys
|
|
42
|
+
# @raise [ProviderNotFoundError] if provider is unknown
|
|
43
|
+
def auth_capabilities(provider_name)
|
|
44
|
+
provider_name = provider_name.to_sym
|
|
45
|
+
provider = resolve_provider(provider_name)
|
|
46
|
+
canonical_name = Providers::Registry.instance.canonical_name(provider_name)
|
|
47
|
+
flow_supported = claude_oauth_flow_provider?(provider_name, canonical_name)
|
|
48
|
+
|
|
49
|
+
{
|
|
50
|
+
auth_type: provider.auth_type,
|
|
51
|
+
auth_url: flow_supported,
|
|
52
|
+
refresh: flow_supported
|
|
53
|
+
}
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Check whether OAuth URL generation is supported for a provider.
|
|
57
|
+
#
|
|
58
|
+
# @param provider_name [Symbol] the provider name
|
|
59
|
+
# @return [Boolean] true if auth_url can be called for the provider
|
|
60
|
+
# @raise [ProviderNotFoundError] if provider is unknown
|
|
61
|
+
def auth_url_supported?(provider_name)
|
|
62
|
+
auth_capabilities(provider_name)[:auth_url]
|
|
63
|
+
end
|
|
64
|
+
|
|
38
65
|
# Generate an OAuth URL for a provider
|
|
39
66
|
#
|
|
40
67
|
# Only supported for :oauth auth type providers.
|
|
41
68
|
#
|
|
42
69
|
# @param provider_name [Symbol] the provider name
|
|
43
70
|
# @return [String] the OAuth authorization URL
|
|
44
|
-
# @raise [
|
|
71
|
+
# @raise [UnsupportedAuthFlowError] if provider doesn't support OAuth
|
|
45
72
|
def auth_url(provider_name)
|
|
46
73
|
provider_name = provider_name.to_sym
|
|
47
74
|
provider = resolve_provider(provider_name)
|
|
48
75
|
|
|
49
76
|
unless provider.auth_type == :oauth
|
|
50
|
-
raise
|
|
77
|
+
raise UnsupportedAuthFlowError,
|
|
51
78
|
"Provider #{provider_name} uses #{provider.auth_type} auth and does not support OAuth URL generation"
|
|
52
79
|
end
|
|
53
80
|
|
|
@@ -55,29 +82,38 @@ module AgentHarness
|
|
|
55
82
|
when :claude, :anthropic
|
|
56
83
|
claude_auth_url
|
|
57
84
|
else
|
|
58
|
-
raise
|
|
85
|
+
raise UnsupportedAuthFlowError,
|
|
59
86
|
"OAuth URL generation is not yet implemented for provider #{provider_name}"
|
|
60
87
|
end
|
|
61
88
|
end
|
|
62
89
|
|
|
90
|
+
# Check whether credential refresh is supported for a provider.
|
|
91
|
+
#
|
|
92
|
+
# @param provider_name [Symbol] the provider name
|
|
93
|
+
# @return [Boolean] true if refresh_auth can be called for the provider
|
|
94
|
+
# @raise [ProviderNotFoundError] if provider is unknown
|
|
95
|
+
def refresh_auth_supported?(provider_name)
|
|
96
|
+
auth_capabilities(provider_name)[:refresh]
|
|
97
|
+
end
|
|
98
|
+
|
|
63
99
|
# Refresh authentication credentials for a provider
|
|
64
100
|
#
|
|
65
101
|
# For OAuth providers, stores a pre-exchanged token directly.
|
|
66
102
|
# This method accepts a token (not an authorization code) because
|
|
67
103
|
# the OAuth code-exchange flow is provider-specific and should be
|
|
68
104
|
# handled by the caller or a CLI login command before calling this.
|
|
69
|
-
# For API key providers, raises
|
|
105
|
+
# For API key providers, raises UnsupportedAuthFlowError.
|
|
70
106
|
#
|
|
71
107
|
# @param provider_name [Symbol] the provider name
|
|
72
108
|
# @param token [String] OAuth token to store (must be non-blank)
|
|
73
109
|
# @return [Hash] result with :success key
|
|
74
|
-
# @raise [
|
|
110
|
+
# @raise [UnsupportedAuthFlowError] if provider doesn't support credential refresh
|
|
75
111
|
def refresh_auth(provider_name, token: nil)
|
|
76
112
|
provider_name = provider_name.to_sym
|
|
77
113
|
provider = resolve_provider(provider_name)
|
|
78
114
|
|
|
79
115
|
unless provider.auth_type == :oauth
|
|
80
|
-
raise
|
|
116
|
+
raise UnsupportedAuthFlowError,
|
|
81
117
|
"Provider #{provider_name} uses #{provider.auth_type} auth and does not support credential refresh"
|
|
82
118
|
end
|
|
83
119
|
|
|
@@ -85,13 +121,17 @@ module AgentHarness
|
|
|
85
121
|
when :claude, :anthropic
|
|
86
122
|
refresh_claude_auth(token: token)
|
|
87
123
|
else
|
|
88
|
-
raise
|
|
124
|
+
raise UnsupportedAuthFlowError,
|
|
89
125
|
"Credential refresh is not yet implemented for provider #{provider_name}"
|
|
90
126
|
end
|
|
91
127
|
end
|
|
92
128
|
|
|
93
129
|
private
|
|
94
130
|
|
|
131
|
+
def claude_oauth_flow_provider?(requested_name, canonical_name)
|
|
132
|
+
[:claude, :anthropic].include?(requested_name) || canonical_name == :claude
|
|
133
|
+
end
|
|
134
|
+
|
|
95
135
|
def resolve_provider(provider_name)
|
|
96
136
|
klass = Providers::Registry.instance.get(provider_name)
|
|
97
137
|
canonical_name = Providers::Registry.instance.canonical_name(provider_name)
|
data/lib/agent_harness/errors.rb
CHANGED
|
@@ -66,6 +66,9 @@ module AgentHarness
|
|
|
66
66
|
# subscription to API-metered usage.
|
|
67
67
|
class AuthMismatchError < AuthenticationError; end
|
|
68
68
|
|
|
69
|
+
# Raised when a provider does not support the requested authentication flow.
|
|
70
|
+
class UnsupportedAuthFlowError < Error; end
|
|
71
|
+
|
|
69
72
|
# Configuration errors
|
|
70
73
|
class ConfigurationError < Error; end
|
|
71
74
|
|
|
@@ -139,6 +139,28 @@ module AgentHarness
|
|
|
139
139
|
def smoke_test_contract
|
|
140
140
|
Base::DEFAULT_SMOKE_TEST_CONTRACT
|
|
141
141
|
end
|
|
142
|
+
|
|
143
|
+
def parse_cli_jsonl_transcript(raw_output, max_events: nil)
|
|
144
|
+
return new.send(:parse_jsonl_output, "") if max_events && max_events <= 0
|
|
145
|
+
|
|
146
|
+
output = max_events ? tail_nonempty_lines(raw_output, limit: max_events).join("\n") : raw_output
|
|
147
|
+
|
|
148
|
+
new.send(:parse_jsonl_output, output)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def tail_nonempty_lines(text, limit:)
|
|
154
|
+
return [] if limit <= 0
|
|
155
|
+
|
|
156
|
+
text.to_s.each_line.each_with_object([]) do |line, lines|
|
|
157
|
+
stripped = line.strip
|
|
158
|
+
next if stripped.empty?
|
|
159
|
+
|
|
160
|
+
lines.shift if lines.size >= limit
|
|
161
|
+
lines << stripped
|
|
162
|
+
end
|
|
163
|
+
end
|
|
142
164
|
end
|
|
143
165
|
|
|
144
166
|
def name
|
|
@@ -603,10 +625,11 @@ module AgentHarness
|
|
|
603
625
|
when "turn.completed"
|
|
604
626
|
turn_usage = build_token_usage(event["usage"])
|
|
605
627
|
result = event["result"]
|
|
628
|
+
result_parts = result.is_a?(String) ? [result] : extract_task_complete_parts(event)
|
|
606
629
|
wrapped_completion_without_new_output =
|
|
607
630
|
pending_turn_usage_source == :wrapped &&
|
|
608
631
|
pending_turn_usage &&
|
|
609
|
-
|
|
632
|
+
result_parts.nil? &&
|
|
610
633
|
(turn_usage.nil? || current_turn_parts.empty? || current_turn_parts.equal?(pending_wrapped_output_parts))
|
|
611
634
|
|
|
612
635
|
if wrapped_completion_without_new_output
|
|
@@ -663,8 +686,8 @@ module AgentHarness
|
|
|
663
686
|
pending_wrapped_same_turn_finalization = false
|
|
664
687
|
end
|
|
665
688
|
|
|
666
|
-
if
|
|
667
|
-
current_turn_parts =
|
|
689
|
+
if result_parts
|
|
690
|
+
current_turn_parts = result_parts
|
|
668
691
|
saw_assistant_output = true
|
|
669
692
|
current_turn_finalized_output = true
|
|
670
693
|
end
|
|
@@ -8,12 +8,12 @@ module AgentHarness
|
|
|
8
8
|
class GithubCopilot < Base
|
|
9
9
|
include TokenUsageParsing
|
|
10
10
|
|
|
11
|
-
PACKAGE_NAME = "@githubnext/github-copilot-cli"
|
|
12
|
-
SUPPORTED_CLI_VERSION = "0.1.36"
|
|
13
|
-
SUPPORTED_CLI_REQUIREMENT = Gem::Requirement.new(">= #{SUPPORTED_CLI_VERSION}", "< 0.2.0").freeze
|
|
14
|
-
|
|
15
11
|
MODEL_PATTERN = /^gpt-[\d.o-]+(?:-turbo)?(?:-mini)?$/i
|
|
16
12
|
JSON_OUTPUT_MIN_VERSION = Gem::Version.new("0.0.422").freeze
|
|
13
|
+
SUBCOMMAND_CLI_MIN_VERSION = Gem::Version.new("0.1.0").freeze
|
|
14
|
+
UNSUPPORTED_SUBCOMMAND_CLI_MESSAGE =
|
|
15
|
+
"github-copilot-cli 0.1.x does not expose a non-interactive send interface; " \
|
|
16
|
+
"the what-the-shell subcommand is interactive and cannot be used by AgentHarness."
|
|
17
17
|
|
|
18
18
|
SMOKE_TEST_CONTRACT = {
|
|
19
19
|
prompt: "Reply with exactly OK.",
|
|
@@ -34,41 +34,22 @@ module AgentHarness
|
|
|
34
34
|
|
|
35
35
|
def available?
|
|
36
36
|
executor = AgentHarness.configuration.command_executor
|
|
37
|
-
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def installation_contract(version: SUPPORTED_CLI_VERSION)
|
|
41
|
-
version = version.strip if version.respond_to?(:strip)
|
|
42
|
-
validate_install_version!(version)
|
|
43
|
-
package_spec = "#{PACKAGE_NAME}@#{version}".freeze
|
|
44
|
-
install_command_prefix = ["npm", "install", "-g", "--ignore-scripts"].freeze
|
|
45
|
-
install_command = (install_command_prefix + [package_spec]).freeze
|
|
46
|
-
version_requirement = SUPPORTED_CLI_REQUIREMENT.requirements
|
|
47
|
-
.map { |op, ver| "#{op} #{ver}".freeze }
|
|
48
|
-
.freeze
|
|
49
|
-
|
|
50
|
-
contract = {
|
|
51
|
-
source: {
|
|
52
|
-
type: :npm,
|
|
53
|
-
package: PACKAGE_NAME
|
|
54
|
-
}.freeze,
|
|
55
|
-
install_command_prefix: install_command_prefix,
|
|
56
|
-
install_command: install_command,
|
|
57
|
-
binary_name: binary_name,
|
|
58
|
-
default_version: SUPPORTED_CLI_VERSION,
|
|
59
|
-
version: version,
|
|
60
|
-
version_requirement: version_requirement,
|
|
61
|
-
supported_version_requirement: SUPPORTED_CLI_REQUIREMENT.to_s
|
|
62
|
-
}
|
|
37
|
+
return false unless executor.which(binary_name)
|
|
63
38
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
contract.freeze
|
|
39
|
+
!subcommand_cli_version?(copilot_cli_version(executor: executor))
|
|
40
|
+
rescue
|
|
41
|
+
false
|
|
68
42
|
end
|
|
69
43
|
|
|
70
|
-
def
|
|
71
|
-
|
|
44
|
+
def installation_contract(version: nil)
|
|
45
|
+
# The published @githubnext/github-copilot-cli package only has
|
|
46
|
+
# 0.1.x releases, and those expose an interactive subcommand instead
|
|
47
|
+
# of the non-interactive -p prompt path AgentHarness uses.
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def install_command(version: nil)
|
|
52
|
+
installation_contract(version: version)&.fetch(:install_command)
|
|
72
53
|
end
|
|
73
54
|
|
|
74
55
|
def provider_metadata_overrides
|
|
@@ -134,26 +115,26 @@ module AgentHarness
|
|
|
134
115
|
|
|
135
116
|
private
|
|
136
117
|
|
|
137
|
-
def
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
118
|
+
def copilot_cli_version(executor:)
|
|
119
|
+
result = executor.execute([binary_name, "--version"], timeout: 5, env: {})
|
|
120
|
+
extract_version(result)
|
|
121
|
+
rescue
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
143
124
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
raise ArgumentError,
|
|
148
|
-
"Unsupported GitHub Copilot CLI version #{version.inspect}; " \
|
|
149
|
-
"supported versions must satisfy #{SUPPORTED_CLI_REQUIREMENT}"
|
|
150
|
-
end
|
|
125
|
+
def subcommand_cli_version?(version)
|
|
126
|
+
!version.nil? && version >= SUBCOMMAND_CLI_MIN_VERSION
|
|
127
|
+
end
|
|
151
128
|
|
|
152
|
-
|
|
129
|
+
def extract_version(result)
|
|
130
|
+
return nil unless result.success?
|
|
153
131
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
132
|
+
version_string = [result.stdout, result.stderr].compact.join("\n")[/\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?/]
|
|
133
|
+
return nil if version_string.nil? || version_string.empty?
|
|
134
|
+
|
|
135
|
+
Gem::Version.new(version_string)
|
|
136
|
+
rescue ArgumentError
|
|
137
|
+
nil
|
|
157
138
|
end
|
|
158
139
|
end
|
|
159
140
|
|
|
@@ -194,18 +175,22 @@ module AgentHarness
|
|
|
194
175
|
}
|
|
195
176
|
end
|
|
196
177
|
|
|
197
|
-
def dangerous_mode_flags(probe_timeout: nil, env: {})
|
|
198
|
-
|
|
178
|
+
def dangerous_mode_flags(probe_timeout: nil, env: {}, version: nil)
|
|
179
|
+
version ||= copilot_cli_version(probe_timeout: probe_timeout, env: env)
|
|
180
|
+
return [] if subcommand_cli_version?(version)
|
|
181
|
+
return [] unless supports_json_output_format?(version: version)
|
|
199
182
|
|
|
200
183
|
["--allow-all"]
|
|
201
184
|
end
|
|
202
185
|
|
|
203
|
-
def supports_sessions?
|
|
204
|
-
|
|
186
|
+
def supports_sessions?(probe_timeout: nil, env: {}, version: nil)
|
|
187
|
+
legacy_prompt_cli?(version: version, probe_timeout: probe_timeout, env: env)
|
|
205
188
|
end
|
|
206
189
|
|
|
207
|
-
def session_flags(session_id)
|
|
190
|
+
def session_flags(session_id, version: nil, probe_timeout: nil, env: {})
|
|
208
191
|
return [] unless session_id && !session_id.empty?
|
|
192
|
+
return [] unless legacy_prompt_cli?(version: version, probe_timeout: probe_timeout, env: env)
|
|
193
|
+
|
|
209
194
|
["--resume", session_id]
|
|
210
195
|
end
|
|
211
196
|
|
|
@@ -221,7 +206,7 @@ module AgentHarness
|
|
|
221
206
|
output_format: :text,
|
|
222
207
|
sandbox_aware: false,
|
|
223
208
|
uses_subcommand: false,
|
|
224
|
-
non_interactive_flag:
|
|
209
|
+
non_interactive_flag: nil,
|
|
225
210
|
legitimate_exit_codes: [0],
|
|
226
211
|
stderr_is_diagnostic: true,
|
|
227
212
|
parses_rate_limit_reset: false
|
|
@@ -324,11 +309,15 @@ module AgentHarness
|
|
|
324
309
|
protected
|
|
325
310
|
|
|
326
311
|
def build_command(prompt, options)
|
|
327
|
-
cmd = [self.class.binary_name, "-p", prompt]
|
|
328
312
|
env = options.fetch(:_command_env) { build_env(options) }
|
|
329
313
|
runtime = options[:provider_runtime]
|
|
314
|
+
version = copilot_cli_version(probe_timeout: options[:_version_probe_timeout], env: env)
|
|
330
315
|
|
|
331
|
-
if
|
|
316
|
+
raise unsupported_subcommand_cli_error if subcommand_cli_version?(version)
|
|
317
|
+
|
|
318
|
+
cmd = [self.class.binary_name, "-p", prompt]
|
|
319
|
+
|
|
320
|
+
if supports_json_output_format?(version: version)
|
|
332
321
|
cmd += ["--output-format", "json"]
|
|
333
322
|
else
|
|
334
323
|
# Silent mode suppresses the model/stats decoration older CLIs print in
|
|
@@ -340,11 +329,11 @@ module AgentHarness
|
|
|
340
329
|
cmd += ["--model", model] if model
|
|
341
330
|
if options[:dangerous_mode] && supports_dangerous_mode?
|
|
342
331
|
cmd += programmatic_tool_approval_flags
|
|
343
|
-
cmd += dangerous_mode_flags(
|
|
332
|
+
cmd += dangerous_mode_flags(version: version)
|
|
344
333
|
end
|
|
345
334
|
|
|
346
335
|
if options[:session] && !options[:session].empty?
|
|
347
|
-
cmd += session_flags(options[:session])
|
|
336
|
+
cmd += session_flags(options[:session], version: version)
|
|
348
337
|
end
|
|
349
338
|
|
|
350
339
|
cmd
|
|
@@ -385,9 +374,22 @@ module AgentHarness
|
|
|
385
374
|
["--allow-all-tools"]
|
|
386
375
|
end
|
|
387
376
|
|
|
388
|
-
def supports_json_output_format?(probe_timeout: nil, env: {})
|
|
389
|
-
version
|
|
390
|
-
!version.nil? && version >= JSON_OUTPUT_MIN_VERSION
|
|
377
|
+
def supports_json_output_format?(probe_timeout: nil, env: {}, version: nil)
|
|
378
|
+
version ||= copilot_cli_version(probe_timeout: probe_timeout, env: env)
|
|
379
|
+
!version.nil? && !subcommand_cli_version?(version) && version >= JSON_OUTPUT_MIN_VERSION
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def legacy_prompt_cli?(probe_timeout: nil, env: {}, version: nil)
|
|
383
|
+
version ||= copilot_cli_version(probe_timeout: probe_timeout, env: env)
|
|
384
|
+
!version.nil? && !subcommand_cli_version?(version)
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
def subcommand_cli_version?(version)
|
|
388
|
+
self.class.send(:subcommand_cli_version?, version)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def unsupported_subcommand_cli_error
|
|
392
|
+
ProviderError.new(UNSUPPORTED_SUBCOMMAND_CLI_MESSAGE)
|
|
391
393
|
end
|
|
392
394
|
|
|
393
395
|
def copilot_cli_version(probe_timeout: nil, env: {})
|
|
@@ -443,14 +445,7 @@ module AgentHarness
|
|
|
443
445
|
end
|
|
444
446
|
|
|
445
447
|
def extract_version(result)
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
version_string = [result.stdout, result.stderr].compact.join("\n")[/\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.-]+)?/]
|
|
449
|
-
return nil if version_string.nil? || version_string.empty?
|
|
450
|
-
|
|
451
|
-
Gem::Version.new(version_string)
|
|
452
|
-
rescue ArgumentError
|
|
453
|
-
nil
|
|
448
|
+
self.class.send(:extract_version, result)
|
|
454
449
|
end
|
|
455
450
|
|
|
456
451
|
def parse_jsonl_output(output)
|
data/lib/agent_harness.rb
CHANGED
|
@@ -184,19 +184,43 @@ module AgentHarness
|
|
|
184
184
|
Authentication.auth_status(provider_name)
|
|
185
185
|
end
|
|
186
186
|
|
|
187
|
+
# Get authentication flow capabilities for a provider
|
|
188
|
+
# @param provider_name [Symbol] the provider name
|
|
189
|
+
# @return [Hash] capabilities with :auth_type, :auth_url, :refresh keys
|
|
190
|
+
# @raise [ProviderNotFoundError] if provider is unknown
|
|
191
|
+
def auth_capabilities(provider_name)
|
|
192
|
+
Authentication.auth_capabilities(provider_name)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# Check whether OAuth URL generation is supported for a provider
|
|
196
|
+
# @param provider_name [Symbol] the provider name
|
|
197
|
+
# @return [Boolean] true if auth_url can be called for the provider
|
|
198
|
+
# @raise [ProviderNotFoundError] if provider is unknown
|
|
199
|
+
def auth_url_supported?(provider_name)
|
|
200
|
+
Authentication.auth_url_supported?(provider_name)
|
|
201
|
+
end
|
|
202
|
+
|
|
187
203
|
# Generate an OAuth URL for a provider
|
|
188
204
|
# @param provider_name [Symbol] the provider name
|
|
189
205
|
# @return [String] the OAuth authorization URL
|
|
190
|
-
# @raise [
|
|
206
|
+
# @raise [UnsupportedAuthFlowError] if provider doesn't support OAuth
|
|
191
207
|
def auth_url(provider_name)
|
|
192
208
|
Authentication.auth_url(provider_name)
|
|
193
209
|
end
|
|
194
210
|
|
|
211
|
+
# Check whether credential refresh is supported for a provider
|
|
212
|
+
# @param provider_name [Symbol] the provider name
|
|
213
|
+
# @return [Boolean] true if refresh_auth can be called for the provider
|
|
214
|
+
# @raise [ProviderNotFoundError] if provider is unknown
|
|
215
|
+
def refresh_auth_supported?(provider_name)
|
|
216
|
+
Authentication.refresh_auth_supported?(provider_name)
|
|
217
|
+
end
|
|
218
|
+
|
|
195
219
|
# Refresh authentication credentials for a provider
|
|
196
220
|
# @param provider_name [Symbol] the provider name
|
|
197
221
|
# @param token [String, nil] OAuth token to store
|
|
198
222
|
# @return [Hash] result with :success key
|
|
199
|
-
# @raise [
|
|
223
|
+
# @raise [UnsupportedAuthFlowError] if provider doesn't support credential refresh
|
|
200
224
|
def refresh_auth(provider_name, token: nil)
|
|
201
225
|
Authentication.refresh_auth(provider_name, token: token)
|
|
202
226
|
end
|