aidp 0.42.0 → 0.44.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: f44b47ad0d31015794ec7b2286dc659e85061eb3e6dad8c89ae018976e0d9b1d
4
- data.tar.gz: 6d8b28a431c3d55e037e32d0e54e583313af4d495982578b72ab3374155323c5
3
+ metadata.gz: d898c7dea670e05503b8777e0907e0d0f975d398fd3cf80d9ddc659e8806070c
4
+ data.tar.gz: c25d8e50f56ce0e89f93c3e5fea9d4fb0ca6d540c19ac5fd3d2710d38be57290
5
5
  SHA512:
6
- metadata.gz: d2f838a10c7789f69a253858d0d71aee754a24020722a845eba38cd29eb764be247fee615427afe67f13618cec384565c04cf0f25a25ddd2493962df0df3e0af
7
- data.tar.gz: 616bf1a6910461b907c5ab99bccb8dddc3312ee7816550d22d31c395b9faecb90e97bd4e5a032ba594a5d78538d9199a0ffbee203a8fe5c08ae8f784ae47b84e
6
+ metadata.gz: ec307da67468acc15043f72c0295f443a7650a4312d1767f12629fbf582d02354a76cf2476081a313cb434c48eb63f5ec06d6d7d90f39d00d972342725d0dbe5
7
+ data.tar.gz: 7bbb9cbfff2543d753b56ca940c19745b0b4678d9e4825c418639c5ec2cbeeac99069de846e75e8c8ef05a8d83b825d13d7e99b056a462e07a89c1f56a2cb96a
@@ -6,6 +6,7 @@ require "tty-spinner"
6
6
  require_relative "../harness/provider_info"
7
7
  require_relative "../harness/capability_registry"
8
8
  require_relative "../harness/config_manager"
9
+ require_relative "../security"
9
10
 
10
11
  module Aidp
11
12
  class CLI
@@ -21,12 +22,13 @@ module Aidp
21
22
  class ProvidersCommand
22
23
  include Aidp::MessageDisplay
23
24
 
24
- def initialize(prompt: TTY::Prompt.new, provider_info_class: nil, capability_registry_class: nil, config_manager_class: nil, project_dir: nil)
25
+ def initialize(prompt: TTY::Prompt.new, provider_info_class: nil, capability_registry_class: nil, config_manager_class: nil, project_dir: nil, mcp_tool_risk_classifier_class: nil)
25
26
  @prompt = prompt
26
27
  @provider_info_class = provider_info_class || Aidp::Harness::ProviderInfo
27
28
  @capability_registry_class = capability_registry_class || Aidp::Harness::CapabilityRegistry
28
29
  @config_manager_class = config_manager_class || Aidp::Harness::ConfigManager
29
30
  @project_dir = project_dir || Dir.pwd
31
+ @mcp_tool_risk_classifier_class = mcp_tool_risk_classifier_class || Aidp::Security::McpToolRiskClassifier
30
32
  end
31
33
 
32
34
  # Main entry point for providers info/refresh subcommands
@@ -198,10 +200,23 @@ module Aidp
198
200
  end
199
201
  end
200
202
 
203
+ refresh_mcp_risk_profile(config_manager, providers_to_refresh)
204
+
201
205
  display_message("\n" + "=" * 60, type: :muted)
202
206
  display_message("Refresh complete", type: :highlight)
203
207
  end
204
208
 
209
+ def refresh_mcp_risk_profile(config_manager, providers_to_refresh)
210
+ classifier = @mcp_tool_risk_classifier_class.new(config_manager, project_dir: @project_dir)
211
+ classifier.generate!(
212
+ providers: config_manager.provider_names,
213
+ force_refresh_providers: providers_to_refresh
214
+ )
215
+ display_message("✓ MCP risk profile refreshed", type: :success)
216
+ rescue => e
217
+ display_message("⚠ MCP risk profile refresh failed: #{e.message}", type: :warning)
218
+ end
219
+
205
220
  def display_help
206
221
  display_message("\nUsage: aidp providers <subcommand> [options]", type: :info)
207
222
  display_message("\nSubcommands:", type: :info)
data/lib/aidp/config.rb CHANGED
@@ -197,7 +197,9 @@ module Aidp
197
197
  polling_interval: 30,
198
198
  labels: {
199
199
  plan_trigger: "aidp-plan",
200
+ project_trigger: "aidp-project",
200
201
  build_trigger: "aidp-build",
202
+ blocked_trigger: "aidp-blocked",
201
203
  review_trigger: "aidp-review",
202
204
  fix_ci_trigger: "aidp-fix-ci",
203
205
  change_request_trigger: "aidp-request-changes",
@@ -32,6 +32,7 @@ module Aidp
32
32
  # Security module paths
33
33
  def self.security_dir(project_dir = Dir.pwd) = File.join(aidp_dir(project_dir), "security")
34
34
  def self.secrets_registry_file(project_dir = Dir.pwd) = File.join(security_dir(project_dir), "secrets_registry.json")
35
+ def self.mcp_risk_profile_file(project_dir = Dir.pwd) = File.join(security_dir(project_dir), "mcp_risk_profile.yml")
35
36
 
36
37
  # Database path
37
38
  def self.database_file(project_dir = Dir.pwd) = File.join(aidp_dir(project_dir), "aidp.db")
@@ -1094,6 +1094,7 @@ module Aidp
1094
1094
  # Execute with sanitized environment (secrets stripped) when security is enabled
1095
1095
  # This ensures agent processes cannot access registered secrets directly
1096
1096
  execute_block = lambda do
1097
+ @security_adapter.apply_provider_mcp_tool_risk!(provider_name)
1097
1098
  @provider_manager.execute_with_provider(
1098
1099
  provider_name,
1099
1100
  full_prompt,
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+ require "time"
5
+ require_relative "../config_paths"
6
+
7
+ module Aidp
8
+ module Security
9
+ # Deterministic MCP tool risk profile generated once during configuration.
10
+ class McpRiskProfile
11
+ VERSION = 1
12
+ VALID_FLAGS = %w[untrusted_input private_data egress].freeze
13
+ VALID_RISK_LEVELS = %w[low medium high].freeze
14
+
15
+ attr_reader :generated_at, :generator_model, :version, :tools
16
+
17
+ def self.load(project_dir = Dir.pwd)
18
+ path = Aidp::ConfigPaths.mcp_risk_profile_file(project_dir)
19
+ return new(tools: {}) unless File.exist?(path)
20
+
21
+ data = YAML.safe_load_file(path, permitted_classes: [Symbol], symbolize_names: true) || {}
22
+ new(
23
+ generated_at: data[:generated_at],
24
+ generator_model: data[:generator_model],
25
+ version: data[:version] || VERSION,
26
+ tools: data[:tools] || {}
27
+ )
28
+ rescue Psych::Exception => e
29
+ Aidp.log_error("security.mcp_risk_profile", "load_failed", path: path, error: e.message)
30
+ new(tools: {})
31
+ end
32
+
33
+ def initialize(generated_at: nil, generator_model: nil, version: VERSION, tools: {})
34
+ @generated_at = generated_at
35
+ @generator_model = generator_model
36
+ @version = version
37
+ @tools = normalize_tools(tools)
38
+ end
39
+
40
+ def save!(project_dir = Dir.pwd)
41
+ Aidp::ConfigPaths.ensure_security_dir(project_dir)
42
+ File.write(Aidp::ConfigPaths.mcp_risk_profile_file(project_dir), YAML.dump(to_h))
43
+ end
44
+
45
+ def tool(tool_name)
46
+ tools[tool_name.to_s]
47
+ end
48
+
49
+ def flags_for(tool_name)
50
+ tool(tool_name)&.fetch(:flags, []) || []
51
+ end
52
+
53
+ def risk_level_for(tool_name)
54
+ tool(tool_name)&.fetch(:risk_level, nil)
55
+ end
56
+
57
+ def empty?
58
+ tools.empty?
59
+ end
60
+
61
+ def to_h
62
+ {
63
+ "generated_at" => generated_at,
64
+ "generator_model" => generator_model,
65
+ "version" => version,
66
+ "tools" => tools.transform_values do |tool_data|
67
+ {
68
+ "flags" => tool_data[:flags],
69
+ "risk_level" => tool_data[:risk_level],
70
+ "rationale" => tool_data[:rationale]
71
+ }
72
+ end
73
+ }
74
+ end
75
+
76
+ private
77
+
78
+ def normalize_tools(raw_tools)
79
+ raw_tools.each_with_object({}) do |(name, tool_data), normalized|
80
+ data = symbolize(tool_data || {})
81
+ normalized[name.to_s] = {
82
+ flags: normalize_flags(data[:flags]),
83
+ risk_level: normalize_risk_level(data[:risk_level]),
84
+ rationale: data[:rationale].to_s
85
+ }
86
+ end
87
+ end
88
+
89
+ def normalize_flags(flags)
90
+ Array(flags).map(&:to_s).select { |flag| VALID_FLAGS.include?(flag) }.uniq.sort
91
+ end
92
+
93
+ def normalize_risk_level(level)
94
+ risk_level = level.to_s
95
+ VALID_RISK_LEVELS.include?(risk_level) ? risk_level : "low"
96
+ end
97
+
98
+ def symbolize(value)
99
+ case value
100
+ when Hash
101
+ value.each_with_object({}) { |(key, inner), hash| hash[key.to_sym] = symbolize(inner) }
102
+ when Array
103
+ value.map { |item| symbolize(item) }
104
+ else
105
+ value
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "time"
5
+ require_relative "../harness/provider_factory"
6
+ require_relative "../harness/provider_info"
7
+ require_relative "../harness/thinking_depth_manager"
8
+ require_relative "mcp_risk_profile"
9
+
10
+ module Aidp
11
+ module Security
12
+ # Generates an MCP tool risk profile using an AI call at configuration time.
13
+ class McpToolRiskClassifier
14
+ RISK_FLAGS = %w[untrusted_input private_data egress].freeze
15
+ RISK_LEVELS = %w[low medium high].freeze
16
+
17
+ GENERATION_PROMPT = <<~PROMPT
18
+ You are classifying MCP tools for Rule-of-Two security enforcement.
19
+
20
+ For each MCP tool, determine which risk flags should be enabled when that
21
+ tool is available to an agent:
22
+ - untrusted_input: processes or imports untrusted external/user-controlled content
23
+ - private_data: can access secrets, credentials, local files, repos, databases, or other sensitive data
24
+ - egress: can communicate externally, push data, call remote services, or send messages off-machine
25
+
26
+ Also assign a risk_level of low, medium, or high based on the combined impact.
27
+
28
+ Tools to classify:
29
+ {{tools_json}}
30
+
31
+ Respond with ONLY valid JSON in this format:
32
+ {
33
+ "tools": [
34
+ {
35
+ "name": "filesystem",
36
+ "flags": ["private_data"],
37
+ "risk_level": "medium",
38
+ "rationale": "Can read and write local files that may contain secrets."
39
+ }
40
+ ]
41
+ }
42
+ PROMPT
43
+
44
+ attr_reader :config, :project_dir, :provider_factory
45
+
46
+ def initialize(config, project_dir:, provider_factory: nil, provider_info_class: nil, time_source: Time)
47
+ @config = config
48
+ @project_dir = project_dir
49
+ @provider_factory = provider_factory || Aidp::Harness::ProviderFactory.new(config)
50
+ @provider_info_class = provider_info_class || Aidp::Harness::ProviderInfo
51
+ @time_source = time_source
52
+ end
53
+
54
+ def generate!(providers: nil, tier: "mini", force_refresh: false, force_refresh_providers: nil)
55
+ tools = collect_tools(
56
+ providers: providers,
57
+ force_refresh: force_refresh,
58
+ force_refresh_providers: force_refresh_providers
59
+ )
60
+ return write_empty_profile if tools.empty?
61
+
62
+ provider_name, model_name = select_model(tier)
63
+ response = call_ai(provider_name, model_name, build_prompt(tools))
64
+ profile = build_profile(response, model_name, expected_tools: tools)
65
+ profile.save!(project_dir)
66
+ profile
67
+ rescue => e
68
+ Aidp.log_error("security.mcp_classifier", "generation_failed",
69
+ error: e.message,
70
+ error_class: e.class.name)
71
+ raise
72
+ end
73
+
74
+ def collect_tools(providers: nil, force_refresh: false, force_refresh_providers: nil)
75
+ provider_names = Array(providers || config.configured_providers).map(&:to_s)
76
+ force_refresh_provider_names = Array(force_refresh_providers).map(&:to_s)
77
+
78
+ provider_names.each_with_object({}) do |provider_name, tools|
79
+ refresh_provider = refresh_provider?(provider_name, force_refresh, force_refresh_provider_names)
80
+ provider_info = @provider_info_class.new(provider_name, project_dir)
81
+ info = provider_info.info(force_refresh: refresh_provider)
82
+ validate_refreshed_provider_info!(provider_name, info, refresh_provider)
83
+ next unless info&.dig(:mcp_support)
84
+
85
+ enabled_mcp_servers(info).each do |server|
86
+ next if server_name(server).empty?
87
+
88
+ entry = (tools[server_name(server)] ||= {
89
+ name: server_name(server),
90
+ descriptions: [],
91
+ providers: []
92
+ })
93
+ description = server[:description].to_s.strip
94
+ entry[:descriptions] << description unless description.empty?
95
+ entry[:providers] << provider_name
96
+ end
97
+ end.values.map do |tool|
98
+ tool[:descriptions].uniq!
99
+ tool[:providers].uniq!
100
+ tool
101
+ end.sort_by { |tool| tool[:name] }
102
+ end
103
+
104
+ private
105
+
106
+ def write_empty_profile
107
+ profile = McpRiskProfile.new(
108
+ generated_at: @time_source.now.utc.iso8601,
109
+ generator_model: "none",
110
+ tools: {}
111
+ )
112
+ profile.save!(project_dir)
113
+ profile
114
+ end
115
+
116
+ def select_model(tier)
117
+ thinking_manager = Aidp::Harness::ThinkingDepthManager.new(config)
118
+ provider_name, model_name, = thinking_manager.select_model_for_tier(
119
+ tier,
120
+ provider: config.respond_to?(:default_provider) ? config.default_provider : nil
121
+ )
122
+ [provider_name, model_name]
123
+ end
124
+
125
+ def build_prompt(tools)
126
+ prompt = GENERATION_PROMPT.dup
127
+ prompt.gsub!("{{tools_json}}", JSON.pretty_generate(tools))
128
+ prompt
129
+ end
130
+
131
+ def call_ai(provider_name, model_name, prompt)
132
+ provider = provider_factory.create_provider(provider_name, model: model_name, output: nil, prompt: nil)
133
+ provider.send_message(prompt: prompt, session: nil)
134
+ end
135
+
136
+ def build_profile(response, model_name, expected_tools:)
137
+ parsed = parse_response(response)
138
+ classified_tools = index_classified_tools(parsed)
139
+ expected_names = expected_tools.map { |tool| tool[:name] }
140
+
141
+ log_unexpected_tools(classified_tools.keys - expected_names)
142
+
143
+ tools_hash = expected_names.each_with_object({}) do |tool_name, hash|
144
+ tool_profile = classified_tools[tool_name]
145
+ hash[tool_name] = tool_profile || conservative_tool_profile(tool_name)
146
+ end
147
+
148
+ McpRiskProfile.new(
149
+ generated_at: @time_source.now.utc.iso8601,
150
+ generator_model: model_name,
151
+ tools: tools_hash
152
+ )
153
+ end
154
+
155
+ def parse_response(response)
156
+ text = response.is_a?(String) ? response : response.to_s
157
+ json_match = text.match(/\{.*\}/m)
158
+ raise "No JSON found in AI response" unless json_match
159
+
160
+ JSON.parse(json_match[0], symbolize_names: true)
161
+ rescue JSON::ParserError => e
162
+ raise "Invalid JSON in AI response: #{e.message}"
163
+ end
164
+
165
+ def index_classified_tools(parsed)
166
+ Array(parsed[:tools]).each_with_object({}) do |tool, hash|
167
+ name = tool[:name].to_s.strip
168
+ next if name.empty?
169
+
170
+ normalized = normalize_tool_profile(tool)
171
+ hash[name] = normalized if normalized
172
+ end
173
+ end
174
+
175
+ def normalize_tool_profile(tool)
176
+ flags = tool[:flags]
177
+ return unless flags.is_a?(Array) && RISK_LEVELS.include?(tool[:risk_level].to_s)
178
+
179
+ normalized_flags = flags.map(&:to_s).select { |flag| RISK_FLAGS.include?(flag) }.uniq
180
+ return if flags.any? && normalized_flags.empty?
181
+
182
+ {
183
+ flags: normalized_flags,
184
+ risk_level: tool[:risk_level].to_s,
185
+ rationale: tool[:rationale].to_s.strip
186
+ }
187
+ end
188
+
189
+ def conservative_tool_profile(tool_name)
190
+ Aidp.log_warn("security.mcp_classifier", "missing_tool_classification",
191
+ tool_name: tool_name)
192
+
193
+ {
194
+ flags: RISK_FLAGS,
195
+ risk_level: "high",
196
+ rationale: "Conservative fallback: model response omitted or malformed classification for #{tool_name}."
197
+ }
198
+ end
199
+
200
+ def log_unexpected_tools(tool_names)
201
+ return if tool_names.empty?
202
+
203
+ Aidp.log_warn("security.mcp_classifier", "unexpected_tool_classifications",
204
+ tool_names: tool_names)
205
+ end
206
+
207
+ def enabled_mcp_servers(info)
208
+ Array(info[:mcp_servers]).select { |server| server_enabled?(server) }
209
+ end
210
+
211
+ def refresh_provider?(provider_name, force_refresh, force_refresh_provider_names)
212
+ force_refresh || force_refresh_provider_names.include?(provider_name)
213
+ end
214
+
215
+ def validate_refreshed_provider_info!(provider_name, info, refresh_provider)
216
+ return unless refresh_provider
217
+ raise "Provider metadata unavailable for #{provider_name}" unless info
218
+ return unless info[:mcp_support]
219
+ return if info.key?(:mcp_servers)
220
+
221
+ raise "MCP server metadata unavailable for #{provider_name}"
222
+ end
223
+
224
+ def server_enabled?(server)
225
+ server[:enabled] != false
226
+ end
227
+
228
+ def server_name(server)
229
+ server[:name].to_s.strip
230
+ end
231
+ end
232
+ end
233
+ end
@@ -17,7 +17,8 @@ module Aidp
17
17
  # adapter.check_agent_call_allowed!(operation: :git_push)
18
18
  # adapter.end_work_unit
19
19
  class WorkLoopAdapter
20
- attr_reader :project_dir, :config, :current_work_unit_id, :current_state
20
+ attr_reader :project_dir, :config, :current_work_unit_id, :current_state, :mcp_risk_profile
21
+ MCP_CONSERVATIVE_FLAGS = %i[untrusted_input private_data egress].freeze
21
22
 
22
23
  # Sources of untrusted input that trigger the untrusted_input flag
23
24
  UNTRUSTED_SOURCES = %w[
@@ -42,11 +43,13 @@ module Aidp
42
43
  issue_comment
43
44
  ].freeze
44
45
 
45
- def initialize(project_dir:, config: nil, enforcer: nil, secrets_proxy: nil)
46
+ def initialize(project_dir:, config: nil, enforcer: nil, secrets_proxy: nil, provider_info_class: nil)
46
47
  @project_dir = project_dir
47
48
  @config = config || load_security_config
48
49
  @enforcer = enforcer || Aidp::Security.enforcer
49
50
  @secrets_proxy = secrets_proxy || Aidp::Security.secrets_proxy
51
+ @provider_info_class = provider_info_class || Aidp::Harness::ProviderInfo
52
+ @mcp_risk_profile = load_mcp_risk_profile
50
53
  @current_work_unit_id = nil
51
54
  @current_state = nil
52
55
  end
@@ -131,6 +134,40 @@ module Aidp
131
134
  @current_state
132
135
  end
133
136
 
137
+ # Apply deterministic MCP tool risk flags generated during configuration.
138
+ # This uses the stored profile and does not perform any AI calls at runtime.
139
+ def apply_mcp_tool_risk!(tool_name)
140
+ return @current_state unless enabled? && @current_state
141
+
142
+ tool_profile = @mcp_risk_profile.tool(tool_name)
143
+ return apply_unclassified_mcp_tool_risk!(tool_name) unless tool_profile
144
+
145
+ enable_mcp_tool_flags(tool_name, tool_profile[:flags])
146
+ end
147
+
148
+ # Apply deterministic MCP tool risk flags for all MCP servers configured on
149
+ # the selected provider. This reads only persisted provider metadata so the
150
+ # work loop never re-introspects provider CLIs at runtime.
151
+ def apply_provider_mcp_tool_risk!(provider_name, force_refresh: false)
152
+ return @current_state unless enabled? && @current_state
153
+
154
+ if force_refresh
155
+ Aidp.log_debug("security.adapter", "ignored_provider_mcp_refresh_request",
156
+ provider: provider_name)
157
+ end
158
+
159
+ provider_mcp_server_names(provider_name).each do |tool_name|
160
+ apply_mcp_tool_risk!(tool_name)
161
+ end
162
+
163
+ @current_state
164
+ rescue => e
165
+ Aidp.log_warn("security.adapter", "mcp_provider_lookup_failed",
166
+ provider: provider_name,
167
+ error: e.message)
168
+ apply_unknown_mcp_risk!(provider_name)
169
+ end
170
+
134
171
  # Request credentials through the secrets proxy
135
172
  # This enables the private_data flag and returns a short-lived token
136
173
  # @param secret_name [String] The registered secret name
@@ -219,6 +256,60 @@ module Aidp
219
256
  {} # Fallback to empty config
220
257
  end
221
258
 
259
+ def load_mcp_risk_profile
260
+ Aidp::Security::McpRiskProfile.load(@project_dir)
261
+ rescue
262
+ Aidp::Security::McpRiskProfile.new(tools: {})
263
+ end
264
+
265
+ def provider_mcp_server_names(provider_name)
266
+ provider_info = @provider_info_class.new(provider_name, project_dir)
267
+ info = provider_info.load_info
268
+ raise "Provider metadata unavailable for #{provider_name}" unless info
269
+
270
+ return [] unless info&.dig(:mcp_support)
271
+ raise "MCP server metadata unavailable for #{provider_name}" unless info.key?(:mcp_servers)
272
+
273
+ enabled_mcp_servers(info).filter_map do |server|
274
+ name = server_name(server)
275
+ name unless name.empty?
276
+ end.uniq
277
+ end
278
+
279
+ def enabled_mcp_servers(info)
280
+ Array(info[:mcp_servers]).select { |server| server_enabled?(server) }
281
+ end
282
+
283
+ def server_enabled?(server)
284
+ server[:enabled] != false
285
+ end
286
+
287
+ def server_name(server)
288
+ server[:name].to_s.strip
289
+ end
290
+
291
+ def apply_unknown_mcp_risk!(provider_name)
292
+ MCP_CONSERVATIVE_FLAGS.each do |flag|
293
+ @current_state.enable(flag, source: "mcp_provider:#{provider_name}:unknown_tools")
294
+ end
295
+
296
+ @current_state
297
+ end
298
+
299
+ def apply_unclassified_mcp_tool_risk!(tool_name)
300
+ Aidp.log_warn("security.adapter", "mcp_tool_unclassified",
301
+ tool_name: tool_name)
302
+ enable_mcp_tool_flags(tool_name, MCP_CONSERVATIVE_FLAGS, source_suffix: ":unclassified")
303
+ end
304
+
305
+ def enable_mcp_tool_flags(tool_name, flags, source_suffix: "")
306
+ Array(flags).each do |flag|
307
+ @current_state.enable(flag.to_sym, source: "mcp_tool:#{tool_name}#{source_suffix}")
308
+ end
309
+
310
+ @current_state
311
+ end
312
+
222
313
  # Detect untrusted input sources in the context
223
314
  def detect_and_enable_untrusted_input(context)
224
315
  sources = []
data/lib/aidp/security.rb CHANGED
@@ -16,6 +16,8 @@ require_relative "security/trifecta_state"
16
16
  require_relative "security/rule_of_two_enforcer"
17
17
  require_relative "security/secrets_registry"
18
18
  require_relative "security/secrets_proxy"
19
+ require_relative "security/mcp_risk_profile"
20
+ require_relative "security/mcp_tool_risk_classifier"
19
21
  require_relative "security/work_loop_adapter"
20
22
  require_relative "security/watch_mode_handler"
21
23
 
@@ -9,6 +9,7 @@ require "json"
9
9
  require "ostruct"
10
10
  require_relative "in_memory_config_adapter"
11
11
  require_relative "in_memory_config_manager"
12
+ require_relative "../security"
12
13
 
13
14
  module Aidp
14
15
  module Setup
@@ -1648,6 +1649,11 @@ module Aidp
1648
1649
  existing[:plan_trigger] || "aidp-plan"
1649
1650
  )
1650
1651
 
1652
+ project_trigger = ask_with_default(
1653
+ "Label to trigger project planning",
1654
+ existing[:project_trigger] || "aidp-project"
1655
+ )
1656
+
1651
1657
  needs_input = ask_with_default(
1652
1658
  "Label for plans needing user input",
1653
1659
  existing[:needs_input] || "aidp-needs-input"
@@ -1663,6 +1669,11 @@ module Aidp
1663
1669
  existing[:build_trigger] || "aidp-build"
1664
1670
  )
1665
1671
 
1672
+ blocked_trigger = ask_with_default(
1673
+ "Label for blocked project sub-issues",
1674
+ existing[:blocked_trigger] || "aidp-blocked"
1675
+ )
1676
+
1666
1677
  review_trigger = ask_with_default(
1667
1678
  "Label to trigger code review",
1668
1679
  existing[:review_trigger] || "aidp-review"
@@ -1685,9 +1696,11 @@ module Aidp
1685
1696
 
1686
1697
  set(%i[watch labels], {
1687
1698
  plan_trigger: plan_trigger,
1699
+ project_trigger: project_trigger,
1688
1700
  needs_input: needs_input,
1689
1701
  ready_to_build: ready_to_build,
1690
1702
  build_trigger: build_trigger,
1703
+ blocked_trigger: blocked_trigger,
1691
1704
  review_trigger: review_trigger,
1692
1705
  ci_fix_trigger: ci_fix_trigger,
1693
1706
  auto_trigger: auto_trigger,
@@ -1855,9 +1868,11 @@ module Aidp
1855
1868
  def collect_required_labels(labels_config)
1856
1869
  default_colors = {
1857
1870
  plan_trigger: "0E8A16", # Green
1871
+ project_trigger: "0052CC", # Deep blue
1858
1872
  needs_input: "D93F0B", # Red
1859
1873
  ready_to_build: "0075CA", # Blue
1860
1874
  build_trigger: "5319E7", # Purple
1875
+ blocked_trigger: "B60205", # Dark red
1861
1876
  review_trigger: "FBCA04", # Yellow
1862
1877
  ci_fix_trigger: "D93F0B", # Red
1863
1878
  auto_trigger: "0C8BD6", # Blue (distinct from build)
@@ -1993,11 +2008,23 @@ module Aidp
1993
2008
  def save_config(yaml_content)
1994
2009
  Aidp::ConfigPaths.ensure_config_dir(project_dir)
1995
2010
  File.write(config_path, yaml_content)
2011
+ generate_mcp_risk_profile
1996
2012
 
1997
2013
  # Generate devcontainer if managed
1998
2014
  generate_devcontainer_file
1999
2015
  end
2000
2016
 
2017
+ def generate_mcp_risk_profile
2018
+ classifier = Aidp::Security::McpToolRiskClassifier.new(
2019
+ build_in_memory_config_adapter,
2020
+ project_dir: project_dir
2021
+ )
2022
+ classifier.generate!
2023
+ rescue => e
2024
+ @warnings << "Failed to generate MCP risk profile: #{e.message}"
2025
+ Aidp.log_warn("setup_wizard", "mcp_risk_profile_generation_failed", error: e.message)
2026
+ end
2027
+
2001
2028
  def display_warnings
2002
2029
  return if @warnings.empty?
2003
2030
 
data/lib/aidp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Aidp
4
- VERSION = "0.42.0"
4
+ VERSION = "0.44.0"
5
5
  end
@@ -27,7 +27,7 @@ module Aidp
27
27
  PROMPT
28
28
 
29
29
  HIERARCHICAL_PROVIDER_PROMPT = <<~PROMPT
30
- You are AIDP's planning specialist for large projects. Read the GitHub issue and existing comments.
30
+ You are AIDP's project planning specialist for large projects. Read the GitHub issue and existing comments.
31
31
  This is a LARGE project that should be broken down into independent sub-issues.
32
32
 
33
33
  Analyze the requirements and break them into logical sub-tasks that can be worked on independently.
@@ -59,6 +59,7 @@ module Aidp
59
59
  - Sub-issues should be sized for 1-3 days of work
60
60
  - Include 3-8 sub-issues for a large project
61
61
  - Be specific about skills needed (avoid generic terms)
62
+ - Prefer leaf-node tasks first so dependency order is explicit and mechanically executable
62
63
  - Only create sub-issues if the project truly warrants it (complexity, multiple components, etc.)
63
64
  PROMPT
64
65