aidp 0.41.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 +4 -4
- data/lib/aidp/cli/providers_command.rb +16 -1
- data/lib/aidp/config_paths.rb +1 -0
- data/lib/aidp/execute/project_knowledge_manager.rb +300 -0
- data/lib/aidp/execute/work_loop_runner.rb +293 -15
- data/lib/aidp/prompt_optimization/context_composer.rb +5 -0
- data/lib/aidp/prompt_optimization/optimizer.rb +11 -0
- data/lib/aidp/prompt_optimization/project_knowledge_indexer.rb +98 -0
- data/lib/aidp/prompt_optimization/prompt_builder.rb +22 -0
- data/lib/aidp/prompt_optimization/relevance_scorer.rb +17 -0
- data/lib/aidp/security/mcp_risk_profile.rb +110 -0
- data/lib/aidp/security/mcp_tool_risk_classifier.rb +233 -0
- data/lib/aidp/security/work_loop_adapter.rb +93 -2
- data/lib/aidp/security.rb +2 -0
- data/lib/aidp/setup/wizard.rb +13 -0
- data/lib/aidp/version.rb +1 -1
- metadata +5 -1
|
@@ -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
|
|
data/lib/aidp/setup/wizard.rb
CHANGED
|
@@ -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
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.
|
|
4
|
+
version: 0.43.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Bart Agapinan
|
|
@@ -407,6 +407,7 @@ files:
|
|
|
407
407
|
- lib/aidp/execute/interactive_repl.rb
|
|
408
408
|
- lib/aidp/execute/persistent_tasklist.rb
|
|
409
409
|
- lib/aidp/execute/progress.rb
|
|
410
|
+
- lib/aidp/execute/project_knowledge_manager.rb
|
|
410
411
|
- lib/aidp/execute/prompt_evaluator.rb
|
|
411
412
|
- lib/aidp/execute/prompt_manager.rb
|
|
412
413
|
- lib/aidp/execute/repl_macros.rb
|
|
@@ -526,6 +527,7 @@ files:
|
|
|
526
527
|
- lib/aidp/pr_worktree_manager.rb
|
|
527
528
|
- lib/aidp/prompt_optimization/context_composer.rb
|
|
528
529
|
- lib/aidp/prompt_optimization/optimizer.rb
|
|
530
|
+
- lib/aidp/prompt_optimization/project_knowledge_indexer.rb
|
|
529
531
|
- lib/aidp/prompt_optimization/prompt_builder.rb
|
|
530
532
|
- lib/aidp/prompt_optimization/relevance_scorer.rb
|
|
531
533
|
- lib/aidp/prompt_optimization/source_code_fragmenter.rb
|
|
@@ -539,6 +541,8 @@ files:
|
|
|
539
541
|
- lib/aidp/rescue_logging.rb
|
|
540
542
|
- lib/aidp/safe_directory.rb
|
|
541
543
|
- lib/aidp/security.rb
|
|
544
|
+
- lib/aidp/security/mcp_risk_profile.rb
|
|
545
|
+
- lib/aidp/security/mcp_tool_risk_classifier.rb
|
|
542
546
|
- lib/aidp/security/rule_of_two_enforcer.rb
|
|
543
547
|
- lib/aidp/security/secrets_proxy.rb
|
|
544
548
|
- lib/aidp/security/secrets_registry.rb
|