aidp 0.42.0 → 0.43.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: 013dc044889cc320404776d0691d2091f3cddda0d9ba854dc0afb92312396336
4
+ data.tar.gz: bb1c57bf8e7d87dfa7d7b169d69646ac76c0cb14fe2f72c70ef02b8543b43494
5
5
  SHA512:
6
- metadata.gz: d2f838a10c7789f69a253858d0d71aee754a24020722a845eba38cd29eb764be247fee615427afe67f13618cec384565c04cf0f25a25ddd2493962df0df3e0af
7
- data.tar.gz: 616bf1a6910461b907c5ab99bccb8dddc3312ee7816550d22d31c395b9faecb90e97bd4e5a032ba594a5d78538d9199a0ffbee203a8fe5c08ae8f784ae47b84e
6
+ metadata.gz: c7ea760cd1d62424ada60f64acc356cdf4a82db6e2166c203d9d41c489fed936a704afaee17e9a20d588fbf0ead8ebd816a991a3fa01d87644939c872b104b12
7
+ data.tar.gz: 9c3b34d7474a2f9e899bf50d9b61d1edcb67a1b4a7b41d78abf810517db8cb2082fe889ec7b1fda5266276b6f1406e577644f2ddca16142bcf66e66e79b1e338
@@ -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)
@@ -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
@@ -1993,11 +1994,23 @@ module Aidp
1993
1994
  def save_config(yaml_content)
1994
1995
  Aidp::ConfigPaths.ensure_config_dir(project_dir)
1995
1996
  File.write(config_path, yaml_content)
1997
+ generate_mcp_risk_profile
1996
1998
 
1997
1999
  # Generate devcontainer if managed
1998
2000
  generate_devcontainer_file
1999
2001
  end
2000
2002
 
2003
+ def generate_mcp_risk_profile
2004
+ classifier = Aidp::Security::McpToolRiskClassifier.new(
2005
+ build_in_memory_config_adapter,
2006
+ project_dir: project_dir
2007
+ )
2008
+ classifier.generate!
2009
+ rescue => e
2010
+ @warnings << "Failed to generate MCP risk profile: #{e.message}"
2011
+ Aidp.log_warn("setup_wizard", "mcp_risk_profile_generation_failed", error: e.message)
2012
+ end
2013
+
2001
2014
  def display_warnings
2002
2015
  return if @warnings.empty?
2003
2016
 
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.43.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aidp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.42.0
4
+ version: 0.43.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bart Agapinan
@@ -541,6 +541,8 @@ files:
541
541
  - lib/aidp/rescue_logging.rb
542
542
  - lib/aidp/safe_directory.rb
543
543
  - lib/aidp/security.rb
544
+ - lib/aidp/security/mcp_risk_profile.rb
545
+ - lib/aidp/security/mcp_tool_risk_classifier.rb
544
546
  - lib/aidp/security/rule_of_two_enforcer.rb
545
547
  - lib/aidp/security/secrets_proxy.rb
546
548
  - lib/aidp/security/secrets_registry.rb