aidp 0.33.0 → 0.34.1
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 +35 -0
- data/lib/aidp/analyze/tree_sitter_scan.rb +3 -0
- data/lib/aidp/cli/eval_command.rb +399 -0
- data/lib/aidp/cli/harness_command.rb +1 -1
- data/lib/aidp/cli/security_command.rb +416 -0
- data/lib/aidp/cli/tools_command.rb +6 -4
- data/lib/aidp/cli.rb +170 -3
- data/lib/aidp/concurrency/exec.rb +3 -0
- data/lib/aidp/config.rb +113 -0
- data/lib/aidp/config_paths.rb +20 -0
- data/lib/aidp/daemon/runner.rb +8 -4
- data/lib/aidp/errors.rb +134 -0
- data/lib/aidp/evaluations/context_capture.rb +205 -0
- data/lib/aidp/evaluations/evaluation_record.rb +114 -0
- data/lib/aidp/evaluations/evaluation_storage.rb +250 -0
- data/lib/aidp/evaluations.rb +23 -0
- data/lib/aidp/execute/async_work_loop_runner.rb +4 -1
- data/lib/aidp/execute/interactive_repl.rb +6 -2
- data/lib/aidp/execute/prompt_evaluator.rb +359 -0
- data/lib/aidp/execute/repl_macros.rb +100 -1
- data/lib/aidp/execute/work_loop_runner.rb +399 -47
- data/lib/aidp/execute/work_loop_state.rb +4 -1
- data/lib/aidp/execute/workflow_selector.rb +3 -0
- data/lib/aidp/harness/ai_decision_engine.rb +79 -0
- data/lib/aidp/harness/capability_registry.rb +2 -0
- data/lib/aidp/harness/condition_detector.rb +3 -0
- data/lib/aidp/harness/config_loader.rb +3 -0
- data/lib/aidp/harness/enhanced_runner.rb +14 -11
- data/lib/aidp/harness/error_handler.rb +3 -0
- data/lib/aidp/harness/provider_factory.rb +3 -0
- data/lib/aidp/harness/provider_manager.rb +6 -0
- data/lib/aidp/harness/runner.rb +5 -1
- data/lib/aidp/harness/state/persistence.rb +3 -0
- data/lib/aidp/harness/state_manager.rb +3 -0
- data/lib/aidp/harness/status_display.rb +28 -20
- data/lib/aidp/harness/thinking_depth_manager.rb +32 -32
- data/lib/aidp/harness/ui/enhanced_tui.rb +4 -0
- data/lib/aidp/harness/ui/enhanced_workflow_selector.rb +4 -0
- data/lib/aidp/harness/ui/error_handler.rb +3 -0
- data/lib/aidp/harness/ui/job_monitor.rb +4 -0
- data/lib/aidp/harness/ui/navigation/submenu.rb +2 -0
- data/lib/aidp/harness/ui/navigation/workflow_selector.rb +6 -0
- data/lib/aidp/harness/ui/spinner_helper.rb +3 -0
- data/lib/aidp/harness/ui/workflow_controller.rb +3 -0
- data/lib/aidp/harness/ui.rb +11 -0
- data/lib/aidp/harness/user_interface.rb +3 -0
- data/lib/aidp/loader.rb +2 -2
- data/lib/aidp/logger.rb +3 -0
- data/lib/aidp/message_display.rb +31 -0
- data/lib/aidp/pr_worktree_manager.rb +18 -6
- data/lib/aidp/provider_manager.rb +3 -0
- data/lib/aidp/providers/base.rb +2 -0
- data/lib/aidp/security/rule_of_two_enforcer.rb +210 -0
- data/lib/aidp/security/secrets_proxy.rb +328 -0
- data/lib/aidp/security/secrets_registry.rb +227 -0
- data/lib/aidp/security/trifecta_state.rb +220 -0
- data/lib/aidp/security/watch_mode_handler.rb +306 -0
- data/lib/aidp/security/work_loop_adapter.rb +277 -0
- data/lib/aidp/security.rb +56 -0
- data/lib/aidp/setup/wizard.rb +4 -2
- data/lib/aidp/version.rb +1 -1
- data/lib/aidp/watch/auto_merger.rb +274 -0
- data/lib/aidp/watch/auto_pr_processor.rb +125 -7
- data/lib/aidp/watch/build_processor.rb +16 -1
- data/lib/aidp/watch/change_request_processor.rb +680 -286
- data/lib/aidp/watch/ci_fix_processor.rb +262 -4
- data/lib/aidp/watch/feedback_collector.rb +191 -0
- data/lib/aidp/watch/hierarchical_pr_strategy.rb +256 -0
- data/lib/aidp/watch/implementation_verifier.rb +142 -1
- data/lib/aidp/watch/plan_generator.rb +70 -13
- data/lib/aidp/watch/plan_processor.rb +12 -5
- data/lib/aidp/watch/projects_processor.rb +286 -0
- data/lib/aidp/watch/repository_client.rb +861 -53
- data/lib/aidp/watch/review_processor.rb +33 -6
- data/lib/aidp/watch/runner.rb +51 -11
- data/lib/aidp/watch/state_store.rb +233 -0
- data/lib/aidp/watch/sub_issue_creator.rb +221 -0
- data/lib/aidp/workflows/guided_agent.rb +4 -0
- data/lib/aidp/workstream_executor.rb +3 -0
- data/lib/aidp/worktree.rb +61 -11
- data/lib/aidp/worktree_branch_manager.rb +347 -101
- data/templates/implementation/iterative_implementation.md +46 -3
- metadata +21 -1
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tty-prompt"
|
|
4
|
+
require "tty-table"
|
|
5
|
+
require_relative "../config"
|
|
6
|
+
require_relative "../security"
|
|
7
|
+
|
|
8
|
+
module Aidp
|
|
9
|
+
class CLI
|
|
10
|
+
# CLI commands for security management
|
|
11
|
+
#
|
|
12
|
+
# Provides commands for:
|
|
13
|
+
# - aidp security status Show current security posture
|
|
14
|
+
# - aidp security register <name> Register a secret with the proxy
|
|
15
|
+
# - aidp security unregister <name> Remove a registered secret
|
|
16
|
+
# - aidp security list List registered secrets (names only)
|
|
17
|
+
# - aidp security audit Run security audit (RSpec tests)
|
|
18
|
+
class SecurityCommand
|
|
19
|
+
def initialize(project_dir: Dir.pwd, prompt: TTY::Prompt.new)
|
|
20
|
+
@project_dir = project_dir
|
|
21
|
+
@prompt = prompt
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Run security command
|
|
25
|
+
#
|
|
26
|
+
# @param args [Array<String>] Command arguments
|
|
27
|
+
# @return [Integer] Exit code
|
|
28
|
+
def run(args)
|
|
29
|
+
subcommand = args.shift
|
|
30
|
+
|
|
31
|
+
case subcommand
|
|
32
|
+
when "status"
|
|
33
|
+
run_status
|
|
34
|
+
when "register", "register-secret"
|
|
35
|
+
secret_name = args.shift
|
|
36
|
+
unless secret_name
|
|
37
|
+
@prompt.error("Error: secret name required")
|
|
38
|
+
@prompt.say("Usage: aidp security register <name> [--env-var VAR_NAME]")
|
|
39
|
+
return 1
|
|
40
|
+
end
|
|
41
|
+
# Parse optional --env-var flag
|
|
42
|
+
env_var = parse_env_var_option(args) || secret_name
|
|
43
|
+
run_register(secret_name, env_var)
|
|
44
|
+
when "unregister"
|
|
45
|
+
secret_name = args.shift
|
|
46
|
+
unless secret_name
|
|
47
|
+
@prompt.error("Error: secret name required")
|
|
48
|
+
@prompt.say("Usage: aidp security unregister <name>")
|
|
49
|
+
return 1
|
|
50
|
+
end
|
|
51
|
+
run_unregister(secret_name)
|
|
52
|
+
when "list", "secrets"
|
|
53
|
+
run_list
|
|
54
|
+
when "audit"
|
|
55
|
+
run_audit(args)
|
|
56
|
+
when "proxy-status"
|
|
57
|
+
run_proxy_status
|
|
58
|
+
when nil, "help", "--help", "-h"
|
|
59
|
+
show_help
|
|
60
|
+
0
|
|
61
|
+
else
|
|
62
|
+
@prompt.error("Unknown subcommand: #{subcommand}")
|
|
63
|
+
show_help
|
|
64
|
+
1
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Show help message
|
|
69
|
+
def show_help
|
|
70
|
+
@prompt.say("\nAIDP Security Management")
|
|
71
|
+
@prompt.say("\n" + "=" * 40)
|
|
72
|
+
@prompt.say("\nUsage:")
|
|
73
|
+
@prompt.say(" aidp security status Show current security posture")
|
|
74
|
+
@prompt.say(" aidp security register <name> Register a secret with the proxy")
|
|
75
|
+
@prompt.say(" aidp security unregister <name> Remove a registered secret")
|
|
76
|
+
@prompt.say(" aidp security list List registered secrets (names only)")
|
|
77
|
+
@prompt.say(" aidp security proxy-status Show secrets proxy status")
|
|
78
|
+
@prompt.say(" aidp security audit Run security audit tests")
|
|
79
|
+
@prompt.say("\nOptions for register:")
|
|
80
|
+
@prompt.say(" --env-var VAR_NAME Environment variable containing the secret")
|
|
81
|
+
@prompt.say(" (defaults to the secret name if not provided)")
|
|
82
|
+
@prompt.say("\nExamples:")
|
|
83
|
+
@prompt.say(" aidp security status")
|
|
84
|
+
@prompt.say(" aidp security register GITHUB_TOKEN")
|
|
85
|
+
@prompt.say(" aidp security register github_token --env-var GITHUB_TOKEN")
|
|
86
|
+
@prompt.say(" aidp security list")
|
|
87
|
+
@prompt.say("\n" + "=" * 40)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Run status command - show current security posture
|
|
91
|
+
def run_status
|
|
92
|
+
Aidp.log_debug("security_cli", "showing_status")
|
|
93
|
+
|
|
94
|
+
config = Aidp::Config.security_config(@project_dir)
|
|
95
|
+
rule_of_two = config[:rule_of_two] || {}
|
|
96
|
+
proxy_config = config[:secrets_proxy] || {}
|
|
97
|
+
|
|
98
|
+
@prompt.say("\n" + "=" * 50)
|
|
99
|
+
@prompt.say("AIDP Security Status")
|
|
100
|
+
@prompt.say("=" * 50)
|
|
101
|
+
|
|
102
|
+
# Rule of Two status
|
|
103
|
+
@prompt.say("\n Rule of Two Enforcement")
|
|
104
|
+
@prompt.say("-" * 30)
|
|
105
|
+
enabled = rule_of_two.fetch(:enabled, true)
|
|
106
|
+
policy = rule_of_two[:policy] || "strict"
|
|
107
|
+
status_icon = enabled ? "\u2713" : "\u2717"
|
|
108
|
+
@prompt.say(" Status: #{status_icon} #{enabled ? "Enabled" : "Disabled"}")
|
|
109
|
+
@prompt.say(" Policy: #{policy}")
|
|
110
|
+
|
|
111
|
+
# Enforcer status
|
|
112
|
+
enforcer = Aidp::Security.enforcer
|
|
113
|
+
summary = enforcer.status_summary
|
|
114
|
+
@prompt.say(" Active work units: #{summary[:active_work_units]}")
|
|
115
|
+
@prompt.say(" Completed work units: #{summary[:completed_work_units]}")
|
|
116
|
+
|
|
117
|
+
# Secrets Proxy status
|
|
118
|
+
@prompt.say("\n Secrets Proxy")
|
|
119
|
+
@prompt.say("-" * 30)
|
|
120
|
+
proxy_enabled = proxy_config.fetch(:enabled, true)
|
|
121
|
+
token_ttl = proxy_config[:token_ttl] || 300
|
|
122
|
+
status_icon = proxy_enabled ? "\u2713" : "\u2717"
|
|
123
|
+
@prompt.say(" Status: #{status_icon} #{proxy_enabled ? "Enabled" : "Disabled"}")
|
|
124
|
+
@prompt.say(" Token TTL: #{token_ttl} seconds")
|
|
125
|
+
|
|
126
|
+
# Registered secrets count
|
|
127
|
+
registry = Aidp::Security.secrets_registry
|
|
128
|
+
secrets = registry.list
|
|
129
|
+
@prompt.say(" Registered secrets: #{secrets.count}")
|
|
130
|
+
|
|
131
|
+
# Active tokens
|
|
132
|
+
proxy = Aidp::Security.secrets_proxy
|
|
133
|
+
active_tokens = proxy.active_tokens_summary
|
|
134
|
+
@prompt.say(" Active tokens: #{active_tokens.count}")
|
|
135
|
+
|
|
136
|
+
@prompt.say("\n" + "=" * 50)
|
|
137
|
+
@prompt.say("Use 'aidp security list' to see registered secrets")
|
|
138
|
+
@prompt.say("Use 'aidp security register <name>' to add secrets")
|
|
139
|
+
@prompt.say("")
|
|
140
|
+
|
|
141
|
+
0
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Run register command - register a secret with the proxy
|
|
145
|
+
def run_register(secret_name, env_var)
|
|
146
|
+
Aidp.log_info("security_cli", "registering_secret",
|
|
147
|
+
name: secret_name,
|
|
148
|
+
env_var: env_var)
|
|
149
|
+
|
|
150
|
+
registry = Aidp::Security.secrets_registry
|
|
151
|
+
|
|
152
|
+
# Check if already registered
|
|
153
|
+
if registry.registered?(secret_name)
|
|
154
|
+
@prompt.warn("Secret '#{secret_name}' is already registered")
|
|
155
|
+
existing = registry.get(secret_name)
|
|
156
|
+
@prompt.say(" Env var: #{existing[:env_var] || existing["env_var"]}")
|
|
157
|
+
return 1
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Check if env var exists
|
|
161
|
+
unless ENV.key?(env_var)
|
|
162
|
+
@prompt.warn("Warning: Environment variable '#{env_var}' is not currently set")
|
|
163
|
+
unless @prompt.yes?("Continue anyway?")
|
|
164
|
+
return 1
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Ask for optional description
|
|
169
|
+
description = @prompt.ask("Description (optional):") do |q|
|
|
170
|
+
q.required false
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Ask for optional scopes
|
|
174
|
+
scopes = @prompt.ask("Allowed scopes (comma-separated, optional):") do |q|
|
|
175
|
+
q.required false
|
|
176
|
+
end
|
|
177
|
+
scope_list = scopes&.split(",")&.map(&:strip)&.reject(&:empty?) || []
|
|
178
|
+
|
|
179
|
+
begin
|
|
180
|
+
result = registry.register(
|
|
181
|
+
name: secret_name,
|
|
182
|
+
env_var: env_var,
|
|
183
|
+
description: description,
|
|
184
|
+
scopes: scope_list
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
@prompt.ok("Secret '#{secret_name}' registered successfully")
|
|
188
|
+
@prompt.say(" ID: #{result[:id]}")
|
|
189
|
+
@prompt.say(" Env var: #{env_var}")
|
|
190
|
+
@prompt.say(" Registered at: #{result[:registered_at]}")
|
|
191
|
+
|
|
192
|
+
if scope_list.any?
|
|
193
|
+
@prompt.say(" Scopes: #{scope_list.join(", ")}")
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
@prompt.say("\nThe secret value will be proxied through short-lived tokens.")
|
|
197
|
+
@prompt.say("Agent processes will not have direct access to '#{env_var}'.")
|
|
198
|
+
|
|
199
|
+
0
|
|
200
|
+
rescue => e
|
|
201
|
+
@prompt.error("Failed to register secret: #{e.message}")
|
|
202
|
+
Aidp.log_error("security_cli", "registration_failed",
|
|
203
|
+
name: secret_name,
|
|
204
|
+
error: e.message)
|
|
205
|
+
1
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Run unregister command - remove a registered secret
|
|
210
|
+
def run_unregister(secret_name)
|
|
211
|
+
Aidp.log_info("security_cli", "unregistering_secret", name: secret_name)
|
|
212
|
+
|
|
213
|
+
registry = Aidp::Security.secrets_registry
|
|
214
|
+
|
|
215
|
+
unless registry.registered?(secret_name)
|
|
216
|
+
@prompt.error("Secret '#{secret_name}' is not registered")
|
|
217
|
+
return 1
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Confirm unregistration
|
|
221
|
+
unless @prompt.yes?("Are you sure you want to unregister '#{secret_name}'?")
|
|
222
|
+
@prompt.say("Cancelled")
|
|
223
|
+
return 0
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Revoke any active tokens for this secret
|
|
227
|
+
proxy = Aidp::Security.secrets_proxy
|
|
228
|
+
revoked_count = proxy.revoke_all_for_secret(secret_name)
|
|
229
|
+
|
|
230
|
+
if registry.unregister(name: secret_name)
|
|
231
|
+
@prompt.ok("Secret '#{secret_name}' unregistered")
|
|
232
|
+
if revoked_count > 0
|
|
233
|
+
@prompt.say(" Revoked #{revoked_count} active token(s)")
|
|
234
|
+
end
|
|
235
|
+
0
|
|
236
|
+
else
|
|
237
|
+
@prompt.error("Failed to unregister secret")
|
|
238
|
+
1
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Run list command - list registered secrets
|
|
243
|
+
def run_list
|
|
244
|
+
Aidp.log_debug("security_cli", "listing_secrets")
|
|
245
|
+
|
|
246
|
+
registry = Aidp::Security.secrets_registry
|
|
247
|
+
secrets = registry.list
|
|
248
|
+
|
|
249
|
+
if secrets.empty?
|
|
250
|
+
@prompt.say("\nNo secrets registered")
|
|
251
|
+
@prompt.say("Use 'aidp security register <name>' to register a secret")
|
|
252
|
+
return 0
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
@prompt.say("\n" + "=" * 60)
|
|
256
|
+
@prompt.say("Registered Secrets")
|
|
257
|
+
@prompt.say("=" * 60)
|
|
258
|
+
|
|
259
|
+
headers = ["Name", "Env Var", "Has Value", "Scopes", "Registered"]
|
|
260
|
+
rows = secrets.map do |secret|
|
|
261
|
+
scopes = secret[:scopes] || []
|
|
262
|
+
scope_str = scopes.any? ? scopes.join(", ") : "(any)"
|
|
263
|
+
has_value = secret[:has_value] ? "\u2713" : "\u2717"
|
|
264
|
+
registered = secret[:registered_at]&.split("T")&.first || "unknown"
|
|
265
|
+
|
|
266
|
+
[secret[:name], secret[:env_var], has_value, scope_str, registered]
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
table = TTY::Table.new(headers, rows)
|
|
270
|
+
@prompt.say(table.render(:unicode, padding: [0, 1]))
|
|
271
|
+
|
|
272
|
+
@prompt.say("\n#{secrets.count} secret(s) registered")
|
|
273
|
+
@prompt.say("")
|
|
274
|
+
|
|
275
|
+
0
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
# Run proxy-status command - show secrets proxy status
|
|
279
|
+
def run_proxy_status
|
|
280
|
+
Aidp.log_debug("security_cli", "showing_proxy_status")
|
|
281
|
+
|
|
282
|
+
proxy = Aidp::Security.secrets_proxy
|
|
283
|
+
active_tokens = proxy.active_tokens_summary
|
|
284
|
+
|
|
285
|
+
@prompt.say("\n" + "=" * 60)
|
|
286
|
+
@prompt.say("Secrets Proxy Status")
|
|
287
|
+
@prompt.say("=" * 60)
|
|
288
|
+
|
|
289
|
+
@prompt.say("\nActive Tokens: #{active_tokens.count}")
|
|
290
|
+
|
|
291
|
+
if active_tokens.any?
|
|
292
|
+
headers = ["Secret", "Scope", "Expires In", "Used"]
|
|
293
|
+
rows = active_tokens.map do |token|
|
|
294
|
+
ttl = token[:remaining_ttl]
|
|
295
|
+
expires = (ttl > 60) ? "#{ttl / 60}m #{ttl % 60}s" : "#{ttl}s"
|
|
296
|
+
used = token[:used] ? "\u2713" : "\u2717"
|
|
297
|
+
[token[:secret_name], token[:scope] || "(any)", expires, used]
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
table = TTY::Table.new(headers, rows)
|
|
301
|
+
@prompt.say(table.render(:unicode, padding: [0, 1]))
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
# Show usage log
|
|
305
|
+
usage_log = proxy.usage_log(limit: 10)
|
|
306
|
+
if usage_log.any?
|
|
307
|
+
@prompt.say("\nRecent Token Usage (last 10):")
|
|
308
|
+
usage_log.each do |entry|
|
|
309
|
+
@prompt.say(" - #{entry[:secret_name]} (#{entry[:scope] || "any"}) at #{entry[:used_at]}")
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
@prompt.say("")
|
|
314
|
+
0
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
# Default timeout for audit command (5 minutes)
|
|
318
|
+
AUDIT_TIMEOUT_SECONDS = 300
|
|
319
|
+
|
|
320
|
+
# Run audit command - run security audit tests
|
|
321
|
+
def run_audit(args)
|
|
322
|
+
Aidp.log_info("security_cli", "running_audit")
|
|
323
|
+
|
|
324
|
+
@prompt.say("\nRunning Security Audit...")
|
|
325
|
+
@prompt.say("=" * 40)
|
|
326
|
+
|
|
327
|
+
# Check for RSpec
|
|
328
|
+
rspec_path = File.join(@project_dir, "spec", "aidp", "security")
|
|
329
|
+
|
|
330
|
+
unless Dir.exist?(rspec_path)
|
|
331
|
+
@prompt.warn("Security spec directory not found: #{rspec_path}")
|
|
332
|
+
@prompt.say("Creating security audit scenarios...")
|
|
333
|
+
|
|
334
|
+
# Create the directory
|
|
335
|
+
FileUtils.mkdir_p(rspec_path)
|
|
336
|
+
@prompt.ok("Created #{rspec_path}")
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Run RSpec for security specs
|
|
340
|
+
@prompt.say("\nRunning security RSpec tests...")
|
|
341
|
+
|
|
342
|
+
# Check if there are any spec files
|
|
343
|
+
spec_files = Dir.glob(File.join(rspec_path, "**/*_spec.rb"))
|
|
344
|
+
|
|
345
|
+
if spec_files.empty?
|
|
346
|
+
@prompt.warn("No security spec files found")
|
|
347
|
+
@prompt.say("Add security tests to: #{rspec_path}")
|
|
348
|
+
return 0
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Run RSpec with timeout protection
|
|
352
|
+
cmd = "bundle exec rspec #{rspec_path} --format documentation"
|
|
353
|
+
@prompt.say("$ #{cmd}\n")
|
|
354
|
+
@prompt.say("(timeout: #{AUDIT_TIMEOUT_SECONDS / 60} minutes)\n")
|
|
355
|
+
|
|
356
|
+
exit_status = run_with_timeout(cmd, AUDIT_TIMEOUT_SECONDS)
|
|
357
|
+
|
|
358
|
+
case exit_status
|
|
359
|
+
when 0
|
|
360
|
+
@prompt.ok("\nSecurity audit passed")
|
|
361
|
+
when :timeout
|
|
362
|
+
@prompt.error("\nSecurity audit timed out after #{AUDIT_TIMEOUT_SECONDS / 60} minutes")
|
|
363
|
+
return 1
|
|
364
|
+
else
|
|
365
|
+
@prompt.error("\nSecurity audit failed")
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
exit_status.is_a?(Integer) ? exit_status : 1
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
private
|
|
372
|
+
|
|
373
|
+
# Run a command with timeout protection
|
|
374
|
+
# @param cmd [String] The command to execute
|
|
375
|
+
# @param timeout [Integer] Timeout in seconds
|
|
376
|
+
# @return [Integer, Symbol] Exit status or :timeout
|
|
377
|
+
def run_with_timeout(cmd, timeout)
|
|
378
|
+
pid = spawn(cmd)
|
|
379
|
+
start_time = Time.now
|
|
380
|
+
|
|
381
|
+
loop do
|
|
382
|
+
# Check if process has exited
|
|
383
|
+
result = Process.waitpid(pid, Process::WNOHANG)
|
|
384
|
+
return $?.exitstatus if result
|
|
385
|
+
|
|
386
|
+
# Check timeout
|
|
387
|
+
if Time.now - start_time > timeout
|
|
388
|
+
Process.kill("TERM", pid)
|
|
389
|
+
sleep 0.5
|
|
390
|
+
begin
|
|
391
|
+
Process.kill("KILL", pid)
|
|
392
|
+
rescue Errno::ESRCH
|
|
393
|
+
# Process already exited
|
|
394
|
+
end
|
|
395
|
+
Process.waitpid(pid)
|
|
396
|
+
return :timeout
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
sleep 0.5
|
|
400
|
+
end
|
|
401
|
+
rescue => e
|
|
402
|
+
Aidp.log_error("security_cli", "audit_execution_error", error: e.message)
|
|
403
|
+
1
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Parse --env-var option from args
|
|
407
|
+
def parse_env_var_option(args)
|
|
408
|
+
idx = args.index("--env-var")
|
|
409
|
+
return nil unless idx
|
|
410
|
+
|
|
411
|
+
args.delete_at(idx) # Remove --env-var
|
|
412
|
+
args.delete_at(idx) # Remove the value and return it
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|
|
@@ -21,9 +21,11 @@ module Aidp
|
|
|
21
21
|
#
|
|
22
22
|
# @param project_dir [String] Project directory path
|
|
23
23
|
# @param prompt [TTY::Prompt] TTY prompt instance
|
|
24
|
-
|
|
24
|
+
# @param query_class [Class] Query class for dependency injection (testing)
|
|
25
|
+
def initialize(project_dir: Dir.pwd, prompt: TTY::Prompt.new, query_class: nil)
|
|
25
26
|
@project_dir = project_dir
|
|
26
27
|
@prompt = prompt
|
|
28
|
+
@query_class = query_class || Metadata::Query
|
|
27
29
|
end
|
|
28
30
|
|
|
29
31
|
# Run tools command
|
|
@@ -79,7 +81,7 @@ module Aidp
|
|
|
79
81
|
@prompt.say("\nValidating tool metadata...")
|
|
80
82
|
|
|
81
83
|
cache = create_cache
|
|
82
|
-
query =
|
|
84
|
+
query = @query_class.new(cache: cache)
|
|
83
85
|
|
|
84
86
|
begin
|
|
85
87
|
query.directory
|
|
@@ -116,7 +118,7 @@ module Aidp
|
|
|
116
118
|
Aidp.log_info("tools", "Showing tool info", tool_id: tool_id)
|
|
117
119
|
|
|
118
120
|
cache = create_cache
|
|
119
|
-
query =
|
|
121
|
+
query = @query_class.new(cache: cache)
|
|
120
122
|
|
|
121
123
|
tool = query.find_by_id(tool_id)
|
|
122
124
|
|
|
@@ -155,7 +157,7 @@ module Aidp
|
|
|
155
157
|
Aidp.log_info("tools", "Listing all tools")
|
|
156
158
|
|
|
157
159
|
cache = create_cache
|
|
158
|
-
query =
|
|
160
|
+
query = @query_class.new(cache: cache)
|
|
159
161
|
|
|
160
162
|
query.directory
|
|
161
163
|
stats = query.statistics
|