aidp 0.22.0 → 0.24.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/README.md +145 -31
- data/lib/aidp/cli.rb +19 -2
- data/lib/aidp/execute/work_loop_runner.rb +252 -45
- data/lib/aidp/execute/work_loop_unit_scheduler.rb +27 -2
- data/lib/aidp/harness/condition_detector.rb +42 -8
- data/lib/aidp/harness/config_manager.rb +7 -0
- data/lib/aidp/harness/config_schema.rb +25 -0
- data/lib/aidp/harness/configuration.rb +69 -6
- data/lib/aidp/harness/error_handler.rb +117 -44
- data/lib/aidp/harness/provider_manager.rb +64 -0
- data/lib/aidp/harness/provider_metrics.rb +138 -0
- data/lib/aidp/harness/runner.rb +110 -35
- data/lib/aidp/harness/simple_user_interface.rb +4 -0
- data/lib/aidp/harness/state/ui_state.rb +0 -10
- data/lib/aidp/harness/state_manager.rb +1 -15
- data/lib/aidp/harness/test_runner.rb +39 -2
- data/lib/aidp/logger.rb +34 -4
- data/lib/aidp/providers/adapter.rb +241 -0
- data/lib/aidp/providers/anthropic.rb +75 -7
- data/lib/aidp/providers/base.rb +29 -1
- data/lib/aidp/providers/capability_registry.rb +205 -0
- data/lib/aidp/providers/codex.rb +14 -0
- data/lib/aidp/providers/error_taxonomy.rb +195 -0
- data/lib/aidp/providers/gemini.rb +3 -2
- data/lib/aidp/setup/devcontainer/backup_manager.rb +11 -4
- data/lib/aidp/setup/provider_registry.rb +107 -0
- data/lib/aidp/setup/wizard.rb +189 -31
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/build_processor.rb +357 -27
- data/lib/aidp/watch/plan_generator.rb +16 -1
- data/lib/aidp/watch/plan_processor.rb +54 -3
- data/lib/aidp/watch/repository_client.rb +78 -4
- data/lib/aidp/watch/repository_safety_checker.rb +12 -3
- data/lib/aidp/watch/runner.rb +52 -10
- data/lib/aidp/workflows/guided_agent.rb +53 -0
- data/lib/aidp/worktree.rb +67 -10
- data/templates/work_loop/decide_whats_next.md +21 -0
- data/templates/work_loop/diagnose_failures.md +21 -0
- metadata +10 -3
- /data/{bin → exe}/aidp +0 -0
data/lib/aidp/logger.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "logger"
|
|
4
4
|
require "json"
|
|
5
5
|
require "fileutils"
|
|
6
|
+
require "pathname"
|
|
6
7
|
|
|
7
8
|
module Aidp
|
|
8
9
|
# Unified structured logger for all AIDP operations
|
|
@@ -84,12 +85,30 @@ module Aidp
|
|
|
84
85
|
private
|
|
85
86
|
|
|
86
87
|
def determine_log_level
|
|
87
|
-
# Priority:
|
|
88
|
-
level_str = ENV["AIDP_LOG_LEVEL"]
|
|
89
|
-
|
|
88
|
+
# Priority: explicit env override > DEBUG flags > config > default
|
|
89
|
+
level_str = if ENV["AIDP_LOG_LEVEL"]
|
|
90
|
+
ENV["AIDP_LOG_LEVEL"]
|
|
91
|
+
elsif debug_env_enabled?
|
|
92
|
+
"debug"
|
|
93
|
+
elsif @config[:level]
|
|
94
|
+
@config[:level]
|
|
95
|
+
else
|
|
96
|
+
"info"
|
|
97
|
+
end
|
|
98
|
+
level_sym = level_str.to_s.to_sym
|
|
90
99
|
LEVELS.key?(level_sym) ? level_sym : :info
|
|
91
100
|
end
|
|
92
101
|
|
|
102
|
+
def debug_env_enabled?
|
|
103
|
+
raw = ENV["AIDP_DEBUG"] || ENV["DEBUG"]
|
|
104
|
+
return false if raw.nil?
|
|
105
|
+
|
|
106
|
+
normalized = raw.to_s.strip.downcase
|
|
107
|
+
return true if %w[true on yes debug].include?(normalized)
|
|
108
|
+
|
|
109
|
+
/\A\d+\z/.match?(normalized) ? normalized.to_i.positive? : false
|
|
110
|
+
end
|
|
111
|
+
|
|
93
112
|
def should_log?(level)
|
|
94
113
|
LEVELS[level] >= LEVELS[@level]
|
|
95
114
|
end
|
|
@@ -106,7 +125,7 @@ module Aidp
|
|
|
106
125
|
end
|
|
107
126
|
|
|
108
127
|
def setup_logger
|
|
109
|
-
info_path =
|
|
128
|
+
info_path = determine_log_file_path
|
|
110
129
|
@logger = create_logger(info_path)
|
|
111
130
|
# Emit instrumentation after logger is available (avoid recursive Aidp.log_* calls during bootstrap)
|
|
112
131
|
return unless @instrument_internal
|
|
@@ -181,6 +200,17 @@ module Aidp
|
|
|
181
200
|
JSON.generate(entry)
|
|
182
201
|
end
|
|
183
202
|
|
|
203
|
+
def determine_log_file_path
|
|
204
|
+
custom = (ENV["AIDP_LOG_FILE"] || @config[:file]).to_s.strip
|
|
205
|
+
relative_path = custom.empty? ? INFO_LOG : custom
|
|
206
|
+
|
|
207
|
+
if Pathname.new(relative_path).absolute?
|
|
208
|
+
relative_path
|
|
209
|
+
else
|
|
210
|
+
File.join(@project_dir, relative_path)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
184
214
|
# Redaction patterns for common secrets
|
|
185
215
|
REDACTION_PATTERNS = [
|
|
186
216
|
# API keys and tokens (with capture groups)
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aidp
|
|
4
|
+
module Providers
|
|
5
|
+
# ProviderAdapter defines the standardized interface that all provider implementations
|
|
6
|
+
# must conform to. This ensures consistent behavior across different AI model providers
|
|
7
|
+
# while allowing for provider-specific implementations.
|
|
8
|
+
#
|
|
9
|
+
# Design Philosophy:
|
|
10
|
+
# - Adapters are stateless; delegate throttling, retries, and escalation to coordinator
|
|
11
|
+
# - Store provider-specific regex matchers adjacent to adapters for maintainability
|
|
12
|
+
# - Single semantic flags map to provider-specific equivalents
|
|
13
|
+
#
|
|
14
|
+
# @see https://github.com/viamin/aidp/issues/243
|
|
15
|
+
module Adapter
|
|
16
|
+
# Core interface methods that all providers must implement
|
|
17
|
+
|
|
18
|
+
# Provider identifier (e.g., "anthropic", "cursor", "gemini")
|
|
19
|
+
# @return [String] unique lowercase identifier for this provider
|
|
20
|
+
def name
|
|
21
|
+
raise NotImplementedError, "#{self.class} must implement #name"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Human-friendly display name for UI
|
|
25
|
+
# @return [String] display name (e.g., "Anthropic Claude", "Cursor AI")
|
|
26
|
+
def display_name
|
|
27
|
+
name
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Send a message to the provider and get a response
|
|
31
|
+
# @param prompt [String] the prompt to send
|
|
32
|
+
# @param session [String, nil] optional session identifier for context
|
|
33
|
+
# @param options [Hash] additional options for the request
|
|
34
|
+
# @return [Hash, String] provider response
|
|
35
|
+
def send_message(prompt:, session: nil, **options)
|
|
36
|
+
raise NotImplementedError, "#{self.class} must implement #send_message"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Capability declaration methods
|
|
40
|
+
|
|
41
|
+
# Check if the provider supports Model Context Protocol
|
|
42
|
+
# @return [Boolean] true if MCP is supported
|
|
43
|
+
def supports_mcp?
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Fetch MCP servers configured for this provider
|
|
48
|
+
# @return [Array<Hash>] array of MCP server configurations
|
|
49
|
+
def fetch_mcp_servers
|
|
50
|
+
[]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Check if the provider is available on this system
|
|
54
|
+
# @return [Boolean] true if provider CLI or API is accessible
|
|
55
|
+
def available?
|
|
56
|
+
true
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Declare provider capabilities
|
|
60
|
+
# @return [Hash] capabilities hash with feature flags
|
|
61
|
+
# @example
|
|
62
|
+
# {
|
|
63
|
+
# reasoning_tiers: ["mini", "standard", "thinking"],
|
|
64
|
+
# context_window: 200_000,
|
|
65
|
+
# supports_json_mode: true,
|
|
66
|
+
# supports_tool_use: true,
|
|
67
|
+
# supports_vision: false,
|
|
68
|
+
# supports_file_upload: true,
|
|
69
|
+
# streaming: true
|
|
70
|
+
# }
|
|
71
|
+
def capabilities
|
|
72
|
+
{
|
|
73
|
+
reasoning_tiers: [],
|
|
74
|
+
context_window: 100_000,
|
|
75
|
+
supports_json_mode: false,
|
|
76
|
+
supports_tool_use: false,
|
|
77
|
+
supports_vision: false,
|
|
78
|
+
supports_file_upload: false,
|
|
79
|
+
streaming: false
|
|
80
|
+
}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Dangerous permissions abstraction
|
|
84
|
+
|
|
85
|
+
# Check if the provider supports dangerous/elevated permissions mode
|
|
86
|
+
# @return [Boolean] true if dangerous mode is supported
|
|
87
|
+
def supports_dangerous_mode?
|
|
88
|
+
false
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Get the provider-specific flag(s) for enabling dangerous mode
|
|
92
|
+
# Maps the semantic `dangerous: true` flag to provider-specific equivalents
|
|
93
|
+
# @return [Array<String>] provider-specific CLI flags
|
|
94
|
+
# @example Anthropic
|
|
95
|
+
# ["--dangerously-skip-permissions"]
|
|
96
|
+
# @example Gemini (hypothetical)
|
|
97
|
+
# ["--yolo"]
|
|
98
|
+
def dangerous_mode_flags
|
|
99
|
+
[]
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Check if dangerous mode is currently enabled
|
|
103
|
+
# @return [Boolean] true if dangerous mode is active
|
|
104
|
+
def dangerous_mode_enabled?
|
|
105
|
+
@dangerous_mode_enabled ||= false
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Enable or disable dangerous mode
|
|
109
|
+
# @param enabled [Boolean] whether to enable dangerous mode
|
|
110
|
+
# @return [void]
|
|
111
|
+
def dangerous_mode=(enabled)
|
|
112
|
+
@dangerous_mode_enabled = enabled
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Error classification and handling
|
|
116
|
+
|
|
117
|
+
# Get error classification regex patterns for this provider
|
|
118
|
+
# @return [Hash<Symbol, Array<Regexp>>] mapping of error categories to regex patterns
|
|
119
|
+
# @example
|
|
120
|
+
# {
|
|
121
|
+
# rate_limited: [/rate.?limit/i, /quota.*exceeded/i],
|
|
122
|
+
# auth_expired: [/authentication.*failed/i, /invalid.*api.*key/i],
|
|
123
|
+
# quota_exceeded: [/quota.*exceeded/i, /usage.*limit/i],
|
|
124
|
+
# transient: [/timeout/i, /connection.*reset/i, /temporary.*error/i],
|
|
125
|
+
# permanent: [/invalid.*model/i, /unsupported.*operation/i]
|
|
126
|
+
# }
|
|
127
|
+
def error_patterns
|
|
128
|
+
{}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Classify an error into the standardized error taxonomy
|
|
132
|
+
# @param error [StandardError] the error to classify
|
|
133
|
+
# @return [Symbol] error category (:rate_limited, :auth_expired, :quota_exceeded, :transient, :permanent)
|
|
134
|
+
def classify_error(error)
|
|
135
|
+
message = error.message.to_s
|
|
136
|
+
|
|
137
|
+
# First check provider-specific patterns
|
|
138
|
+
error_patterns.each do |category, patterns|
|
|
139
|
+
patterns.each do |pattern|
|
|
140
|
+
return category if message.match?(pattern)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Fall back to ErrorTaxonomy for classification
|
|
145
|
+
require_relative "error_taxonomy"
|
|
146
|
+
Aidp::Providers::ErrorTaxonomy.classify_message(message)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# Get normalized error metadata
|
|
150
|
+
# @param error [StandardError] the error to process
|
|
151
|
+
# @return [Hash] normalized error information
|
|
152
|
+
def error_metadata(error)
|
|
153
|
+
{
|
|
154
|
+
provider: name,
|
|
155
|
+
error_category: classify_error(error),
|
|
156
|
+
error_class: error.class.name,
|
|
157
|
+
message: redact_secrets(error.message),
|
|
158
|
+
timestamp: Time.now.iso8601,
|
|
159
|
+
retryable: retryable_error?(error)
|
|
160
|
+
}
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Check if an error is retryable
|
|
164
|
+
# @param error [StandardError] the error to check
|
|
165
|
+
# @return [Boolean] true if the error should be retried
|
|
166
|
+
def retryable_error?(error)
|
|
167
|
+
category = classify_error(error)
|
|
168
|
+
[:transient].include?(category)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Logging and metrics
|
|
172
|
+
|
|
173
|
+
# Get logging metadata for this provider
|
|
174
|
+
# @return [Hash] metadata for structured logging
|
|
175
|
+
def logging_metadata
|
|
176
|
+
{
|
|
177
|
+
provider: name,
|
|
178
|
+
display_name: display_name,
|
|
179
|
+
supports_mcp: supports_mcp?,
|
|
180
|
+
available: available?,
|
|
181
|
+
dangerous_mode: dangerous_mode_enabled?
|
|
182
|
+
}
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Redact secrets from log messages
|
|
186
|
+
# @param message [String] message potentially containing secrets
|
|
187
|
+
# @return [String] message with secrets redacted
|
|
188
|
+
def redact_secrets(message)
|
|
189
|
+
# Redact common secret patterns
|
|
190
|
+
message = message.gsub(/api[_-]?key[:\s=]+[^\s&]+/i, "api_key=[REDACTED]")
|
|
191
|
+
message = message.gsub(/token[:\s=]+[^\s&]+/i, "token=[REDACTED]")
|
|
192
|
+
message = message.gsub(/password[:\s=]+[^\s&]+/i, "password=[REDACTED]")
|
|
193
|
+
message = message.gsub(/bearer\s+[^\s&]+/i, "bearer [REDACTED]")
|
|
194
|
+
message.gsub(/sk-[a-zA-Z0-9_-]{20,}/i, "sk-[REDACTED]")
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Configuration validation
|
|
198
|
+
|
|
199
|
+
# Validate provider configuration
|
|
200
|
+
# @param config [Hash] configuration to validate
|
|
201
|
+
# @return [Hash] validation result with :valid, :errors, :warnings keys
|
|
202
|
+
def validate_config(config)
|
|
203
|
+
errors = []
|
|
204
|
+
warnings = []
|
|
205
|
+
|
|
206
|
+
# Validate required fields
|
|
207
|
+
unless config[:type]
|
|
208
|
+
errors << "Provider type is required"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
unless ["usage_based", "subscription", "passthrough"].include?(config[:type])
|
|
212
|
+
errors << "Provider type must be one of: usage_based, subscription, passthrough"
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
# Validate models if present
|
|
216
|
+
if config[:models] && !config[:models].is_a?(Array)
|
|
217
|
+
errors << "Models must be an array"
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
{
|
|
221
|
+
valid: errors.empty?,
|
|
222
|
+
errors: errors,
|
|
223
|
+
warnings: warnings
|
|
224
|
+
}
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Provider health and status
|
|
228
|
+
|
|
229
|
+
# Check provider health
|
|
230
|
+
# @return [Hash] health status information
|
|
231
|
+
def health_status
|
|
232
|
+
{
|
|
233
|
+
provider: name,
|
|
234
|
+
available: available?,
|
|
235
|
+
healthy: available?,
|
|
236
|
+
timestamp: Time.now.iso8601
|
|
237
|
+
}
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
@@ -44,6 +44,68 @@ module Aidp
|
|
|
44
44
|
self.class.available?
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
+
# ProviderAdapter interface methods
|
|
48
|
+
|
|
49
|
+
def capabilities
|
|
50
|
+
{
|
|
51
|
+
reasoning_tiers: ["mini", "standard", "thinking"],
|
|
52
|
+
context_window: 200_000,
|
|
53
|
+
supports_json_mode: true,
|
|
54
|
+
supports_tool_use: true,
|
|
55
|
+
supports_vision: false,
|
|
56
|
+
supports_file_upload: true,
|
|
57
|
+
streaming: true
|
|
58
|
+
}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def supports_dangerous_mode?
|
|
62
|
+
true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def dangerous_mode_flags
|
|
66
|
+
["--dangerously-skip-permissions"]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def error_patterns
|
|
70
|
+
{
|
|
71
|
+
rate_limited: [
|
|
72
|
+
/rate.?limit/i,
|
|
73
|
+
/too.?many.?requests/i,
|
|
74
|
+
/429/,
|
|
75
|
+
/overloaded/i
|
|
76
|
+
],
|
|
77
|
+
auth_expired: [
|
|
78
|
+
/oauth.*token.*expired/i,
|
|
79
|
+
/authentication.*error/i,
|
|
80
|
+
/invalid.*api.*key/i,
|
|
81
|
+
/unauthorized/i,
|
|
82
|
+
/401/
|
|
83
|
+
],
|
|
84
|
+
quota_exceeded: [
|
|
85
|
+
/quota.*exceeded/i,
|
|
86
|
+
/usage.*limit/i,
|
|
87
|
+
/credit.*exhausted/i
|
|
88
|
+
],
|
|
89
|
+
transient: [
|
|
90
|
+
/timeout/i,
|
|
91
|
+
/connection.*reset/i,
|
|
92
|
+
/temporary.*error/i,
|
|
93
|
+
/service.*unavailable/i,
|
|
94
|
+
/503/,
|
|
95
|
+
/502/,
|
|
96
|
+
/504/
|
|
97
|
+
],
|
|
98
|
+
permanent: [
|
|
99
|
+
/invalid.*model/i,
|
|
100
|
+
/unsupported.*operation/i,
|
|
101
|
+
/not.*found/i,
|
|
102
|
+
/404/,
|
|
103
|
+
/bad.*request/i,
|
|
104
|
+
/400/
|
|
105
|
+
]
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
47
109
|
def send_message(prompt:, session: nil)
|
|
48
110
|
raise "claude CLI not available" unless self.class.available?
|
|
49
111
|
|
|
@@ -156,6 +218,8 @@ module Aidp
|
|
|
156
218
|
TIMEOUT_STATIC_ANALYSIS
|
|
157
219
|
when /REFACTORING_RECOMMENDATIONS/
|
|
158
220
|
TIMEOUT_REFACTORING_RECOMMENDATIONS
|
|
221
|
+
when /IMPLEMENTATION/
|
|
222
|
+
TIMEOUT_IMPLEMENTATION
|
|
159
223
|
else
|
|
160
224
|
nil # Use default
|
|
161
225
|
end
|
|
@@ -163,16 +227,20 @@ module Aidp
|
|
|
163
227
|
end
|
|
164
228
|
|
|
165
229
|
# Check if we should skip permissions based on devcontainer configuration
|
|
230
|
+
# Overrides base class to add logging and Claude-specific config check
|
|
166
231
|
def should_skip_permissions?
|
|
167
|
-
#
|
|
168
|
-
|
|
232
|
+
# Use base class devcontainer detection
|
|
233
|
+
if in_devcontainer_or_codespace?
|
|
234
|
+
debug_log("🔓 Detected devcontainer/codespace environment - enabling full permissions", level: :info)
|
|
235
|
+
return true
|
|
236
|
+
end
|
|
169
237
|
|
|
170
|
-
#
|
|
171
|
-
|
|
172
|
-
|
|
238
|
+
# Fallback: Check harness context for Claude-specific configuration
|
|
239
|
+
if @harness_context&.config&.respond_to?(:should_use_full_permissions?)
|
|
240
|
+
return @harness_context.config.should_use_full_permissions?("claude")
|
|
241
|
+
end
|
|
173
242
|
|
|
174
|
-
|
|
175
|
-
config.should_use_full_permissions?("claude")
|
|
243
|
+
false
|
|
176
244
|
end
|
|
177
245
|
|
|
178
246
|
# Parse stream-json output from Claude CLI
|
data/lib/aidp/providers/base.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "tty-prompt"
|
|
4
4
|
require "tty-spinner"
|
|
5
|
+
require_relative "adapter"
|
|
5
6
|
|
|
6
7
|
module Aidp
|
|
7
8
|
module Providers
|
|
@@ -9,6 +10,7 @@ module Aidp
|
|
|
9
10
|
|
|
10
11
|
class Base
|
|
11
12
|
include Aidp::MessageDisplay
|
|
13
|
+
include Aidp::Providers::Adapter
|
|
12
14
|
|
|
13
15
|
# Activity indicator states
|
|
14
16
|
ACTIVITY_STATES = {
|
|
@@ -33,6 +35,7 @@ module Aidp
|
|
|
33
35
|
TIMEOUT_DOCUMENTATION_ANALYSIS = 300 # 5 minutes - documentation analysis
|
|
34
36
|
TIMEOUT_STATIC_ANALYSIS = 450 # 7.5 minutes - static analysis
|
|
35
37
|
TIMEOUT_REFACTORING_RECOMMENDATIONS = 600 # 10 minutes - refactoring
|
|
38
|
+
TIMEOUT_IMPLEMENTATION = 900 # 15 minutes - implementation (write files, run tests, fix issues)
|
|
36
39
|
|
|
37
40
|
attr_reader :activity_state, :last_activity_time, :start_time, :step_name
|
|
38
41
|
|
|
@@ -299,7 +302,7 @@ module Aidp
|
|
|
299
302
|
error_message = e.message
|
|
300
303
|
|
|
301
304
|
# Check if error is rate limiting
|
|
302
|
-
if e.message.match?(/rate.?limit/i) || e.message.match?(/quota/i)
|
|
305
|
+
if e.message.match?(/rate.?limit/i) || e.message.match?(/quota/i) || e.message.match?(/session limit/i)
|
|
303
306
|
rate_limited = true
|
|
304
307
|
end
|
|
305
308
|
|
|
@@ -391,6 +394,31 @@ module Aidp
|
|
|
391
394
|
spinner&.stop
|
|
392
395
|
end
|
|
393
396
|
|
|
397
|
+
# Check if we should skip permissions based on devcontainer/codespace environment
|
|
398
|
+
# This enables providers to run with elevated permissions in safe development environments
|
|
399
|
+
# Returns true if running in a devcontainer or GitHub Codespace
|
|
400
|
+
def in_devcontainer_or_codespace?
|
|
401
|
+
ENV["REMOTE_CONTAINERS"] == "true" || ENV["CODESPACES"] == "true"
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
# Check if provider should skip sandbox permissions
|
|
405
|
+
# Providers can override this to add additional logic beyond environment detection
|
|
406
|
+
def should_skip_permissions?
|
|
407
|
+
# First, check for devcontainer/codespace environment (most reliable)
|
|
408
|
+
return true if in_devcontainer_or_codespace?
|
|
409
|
+
|
|
410
|
+
# Fallback: Check if harness context is available and has configuration
|
|
411
|
+
return false unless @harness_context
|
|
412
|
+
|
|
413
|
+
# Get configuration from harness
|
|
414
|
+
config = @harness_context.config
|
|
415
|
+
return false unless config
|
|
416
|
+
|
|
417
|
+
# Use configuration method to determine if full permissions should be used
|
|
418
|
+
# Provider subclasses should pass their provider name
|
|
419
|
+
false # Base implementation returns false, subclasses should override
|
|
420
|
+
end
|
|
421
|
+
|
|
394
422
|
private
|
|
395
423
|
end
|
|
396
424
|
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aidp
|
|
4
|
+
module Providers
|
|
5
|
+
# CapabilityRegistry maintains a queryable registry of provider capabilities
|
|
6
|
+
# and features. This enables runtime feature detection and provider selection
|
|
7
|
+
# based on required capabilities.
|
|
8
|
+
#
|
|
9
|
+
# @see https://github.com/viamin/aidp/issues/243
|
|
10
|
+
class CapabilityRegistry
|
|
11
|
+
# Standard capability keys
|
|
12
|
+
CAPABILITY_KEYS = [
|
|
13
|
+
:reasoning_tiers, # Array of supported reasoning tiers (mini, standard, thinking, etc.)
|
|
14
|
+
:context_window, # Maximum context window size in tokens
|
|
15
|
+
:supports_json_mode, # Boolean: supports JSON mode output
|
|
16
|
+
:supports_tool_use, # Boolean: supports tool/function calling
|
|
17
|
+
:supports_vision, # Boolean: supports image/vision inputs
|
|
18
|
+
:supports_file_upload, # Boolean: supports file uploads
|
|
19
|
+
:streaming, # Boolean: supports streaming responses
|
|
20
|
+
:supports_mcp, # Boolean: supports Model Context Protocol
|
|
21
|
+
:max_tokens, # Maximum tokens per response
|
|
22
|
+
:supports_dangerous_mode # Boolean: supports elevated permissions mode
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
def initialize
|
|
26
|
+
@capabilities = {}
|
|
27
|
+
@providers = {}
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Register a provider and its capabilities
|
|
31
|
+
# @param provider [Aidp::Providers::Base] provider instance
|
|
32
|
+
# @return [void]
|
|
33
|
+
def register(provider)
|
|
34
|
+
provider_name = provider.name
|
|
35
|
+
@providers[provider_name] = provider
|
|
36
|
+
|
|
37
|
+
# Collect capabilities from provider
|
|
38
|
+
caps = provider.capabilities.dup
|
|
39
|
+
caps[:supports_mcp] = provider.supports_mcp?
|
|
40
|
+
caps[:supports_dangerous_mode] = provider.supports_dangerous_mode?
|
|
41
|
+
|
|
42
|
+
@capabilities[provider_name] = caps
|
|
43
|
+
|
|
44
|
+
Aidp.log_debug("CapabilityRegistry", "registered provider",
|
|
45
|
+
provider: provider_name,
|
|
46
|
+
capabilities: caps.keys)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Unregister a provider
|
|
50
|
+
# @param provider_name [String] provider identifier
|
|
51
|
+
# @return [void]
|
|
52
|
+
def unregister(provider_name)
|
|
53
|
+
@capabilities.delete(provider_name)
|
|
54
|
+
@providers.delete(provider_name)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Get capabilities for a specific provider
|
|
58
|
+
# @param provider_name [String] provider identifier
|
|
59
|
+
# @return [Hash, nil] capabilities hash or nil if not found
|
|
60
|
+
def capabilities_for(provider_name)
|
|
61
|
+
@capabilities[provider_name]
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Check if a provider has a specific capability
|
|
65
|
+
# @param provider_name [String] provider identifier
|
|
66
|
+
# @param capability [Symbol] capability key
|
|
67
|
+
# @param value [Object, nil] optional value to match
|
|
68
|
+
# @return [Boolean] true if provider has the capability
|
|
69
|
+
def has_capability?(provider_name, capability, value = nil)
|
|
70
|
+
caps = @capabilities[provider_name]
|
|
71
|
+
return false unless caps
|
|
72
|
+
|
|
73
|
+
if value.nil?
|
|
74
|
+
# Just check if capability exists and is truthy
|
|
75
|
+
caps.key?(capability) && caps[capability]
|
|
76
|
+
else
|
|
77
|
+
# Check if capability matches specific value
|
|
78
|
+
caps[capability] == value
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Find providers that match capability requirements
|
|
83
|
+
# @param requirements [Hash] capability requirements
|
|
84
|
+
# @return [Array<String>] array of matching provider names
|
|
85
|
+
# @example
|
|
86
|
+
# registry.find_providers(supports_vision: true, min_context_window: 100_000)
|
|
87
|
+
def find_providers(**requirements)
|
|
88
|
+
matching = []
|
|
89
|
+
|
|
90
|
+
@capabilities.each do |provider_name, caps|
|
|
91
|
+
matches = requirements.all? do |key, required_value|
|
|
92
|
+
case key
|
|
93
|
+
when :min_context_window
|
|
94
|
+
caps[:context_window] && caps[:context_window] >= required_value
|
|
95
|
+
when :max_context_window
|
|
96
|
+
caps[:context_window] && caps[:context_window] <= required_value
|
|
97
|
+
when :reasoning_tier
|
|
98
|
+
caps[:reasoning_tiers]&.include?(required_value)
|
|
99
|
+
else
|
|
100
|
+
# Exact match for boolean and other values
|
|
101
|
+
caps[key] == required_value
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
matching << provider_name if matches
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
matching
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Get all registered providers
|
|
112
|
+
# @return [Array<String>] array of provider names
|
|
113
|
+
def registered_providers
|
|
114
|
+
@providers.keys
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Get detailed information about all registered providers
|
|
118
|
+
# @return [Hash] provider information indexed by provider name
|
|
119
|
+
def provider_info
|
|
120
|
+
info = {}
|
|
121
|
+
|
|
122
|
+
@providers.each do |provider_name, provider|
|
|
123
|
+
caps = @capabilities[provider_name] || {}
|
|
124
|
+
|
|
125
|
+
info[provider_name] = {
|
|
126
|
+
display_name: provider.display_name,
|
|
127
|
+
available: provider.available?,
|
|
128
|
+
capabilities: caps,
|
|
129
|
+
dangerous_mode_enabled: provider.dangerous_mode_enabled?,
|
|
130
|
+
health_status: provider.health_status
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
info
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Check capability compatibility between providers
|
|
138
|
+
# @param provider_name1 [String] first provider
|
|
139
|
+
# @param provider_name2 [String] second provider
|
|
140
|
+
# @return [Hash] compatibility report
|
|
141
|
+
def compatibility_report(provider_name1, provider_name2)
|
|
142
|
+
caps1 = @capabilities[provider_name1]
|
|
143
|
+
caps2 = @capabilities[provider_name2]
|
|
144
|
+
|
|
145
|
+
return {error: "Provider not found"} unless caps1 && caps2
|
|
146
|
+
|
|
147
|
+
common = {}
|
|
148
|
+
differences = {}
|
|
149
|
+
|
|
150
|
+
all_keys = (caps1.keys + caps2.keys).uniq
|
|
151
|
+
|
|
152
|
+
all_keys.each do |key|
|
|
153
|
+
val1 = caps1[key]
|
|
154
|
+
val2 = caps2[key]
|
|
155
|
+
|
|
156
|
+
if val1 == val2
|
|
157
|
+
common[key] = val1
|
|
158
|
+
else
|
|
159
|
+
differences[key] = {provider_name1 => val1, provider_name2 => val2}
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
{
|
|
164
|
+
common_capabilities: common,
|
|
165
|
+
differences: differences,
|
|
166
|
+
compatibility_score: common.size.to_f / all_keys.size
|
|
167
|
+
}
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Get capability statistics across all providers
|
|
171
|
+
# @return [Hash] statistics about capability support
|
|
172
|
+
def capability_statistics
|
|
173
|
+
stats = {}
|
|
174
|
+
|
|
175
|
+
CAPABILITY_KEYS.each do |key|
|
|
176
|
+
stats[key] = {
|
|
177
|
+
total_providers: @providers.size,
|
|
178
|
+
supporting_providers: 0,
|
|
179
|
+
providers: []
|
|
180
|
+
}
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
@capabilities.each do |provider_name, caps|
|
|
184
|
+
caps.each do |key, value|
|
|
185
|
+
next unless stats.key?(key)
|
|
186
|
+
|
|
187
|
+
if value.is_a?(TrueClass) || (value.is_a?(Array) && !value.empty?) || (value.is_a?(Integer) && value > 0)
|
|
188
|
+
stats[key][:supporting_providers] += 1
|
|
189
|
+
stats[key][:providers] << provider_name
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
stats
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Clear all registered providers
|
|
198
|
+
# @return [void]
|
|
199
|
+
def clear
|
|
200
|
+
@capabilities.clear
|
|
201
|
+
@providers.clear
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
data/lib/aidp/providers/codex.rb
CHANGED
|
@@ -79,6 +79,20 @@ module Aidp
|
|
|
79
79
|
args += ["--session", session]
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
# In devcontainer, ensure sandbox mode and approval policy are set
|
|
83
|
+
# These are already set via environment variables in devcontainer.json
|
|
84
|
+
# but we verify and log them here for visibility
|
|
85
|
+
if in_devcontainer_or_codespace?
|
|
86
|
+
unless ENV["CODEX_SANDBOX_MODE"] == "danger-full-access"
|
|
87
|
+
ENV["CODEX_SANDBOX_MODE"] = "danger-full-access"
|
|
88
|
+
debug_log("🔓 Set CODEX_SANDBOX_MODE=danger-full-access for devcontainer", level: :info)
|
|
89
|
+
end
|
|
90
|
+
unless ENV["CODEX_APPROVAL_POLICY"] == "never"
|
|
91
|
+
ENV["CODEX_APPROVAL_POLICY"] = "never"
|
|
92
|
+
debug_log("🔓 Set CODEX_APPROVAL_POLICY=never for devcontainer", level: :info)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
82
96
|
# Use debug_execute_command for better debugging
|
|
83
97
|
result = debug_execute_command("codex", args: args, timeout: timeout_seconds, streaming: streaming_enabled)
|
|
84
98
|
|