Class: Aidp::Security::WorkLoopAdapter
- Inherits:
-
Object
- Object
- Aidp::Security::WorkLoopAdapter
- Defined in:
- lib/aidp/security/work_loop_adapter.rb
Overview
Adapts the Rule of Two security framework to the WorkLoopRunner Tracks trifecta state per work unit and enforces policy before agent calls
Integration points:
- Work unit start/end lifecycle
- Untrusted input detection (issues, PRs, external data)
- Egress detection (git operations, API calls)
- Private data detection (registered secrets)
Usage:
adapter = WorkLoopAdapter.new(project_dir: Dir.pwd)
adapter.begin_work_unit(work_unit_id: "unit_123", context: context)
adapter.check_agent_call_allowed!(operation: :git_push)
adapter.end_work_unit
Constant Summary collapse
- MCP_CONSERVATIVE_FLAGS =
%i[untrusted_input private_data egress].freeze
- UNTRUSTED_SOURCES =
Sources of untrusted input that trigger the untrusted_input flag
%w[ github_issue github_pr github_comment external_url user_provided_url webhook_payload ].freeze
- EGRESS_OPERATIONS =
Operations that constitute egress (external communication)
%w[ git_push git_fetch api_call http_request webhook_send email_send file_upload pr_comment issue_comment ].freeze
Instance Attribute Summary collapse
-
#config ⇒ Object
readonly
Returns the value of attribute config.
-
#current_state ⇒ Object
readonly
Returns the value of attribute current_state.
-
#current_work_unit_id ⇒ Object
readonly
Returns the value of attribute current_work_unit_id.
-
#mcp_risk_profile ⇒ Object
readonly
Returns the value of attribute mcp_risk_profile.
-
#project_dir ⇒ Object
readonly
Returns the value of attribute project_dir.
Instance Method Summary collapse
-
#apply_mcp_tool_risk!(tool_name) ⇒ Object
Apply deterministic MCP tool risk flags generated during configuration.
-
#apply_provider_mcp_tool_risk!(provider_name, force_refresh: false) ⇒ Object
Apply deterministic MCP tool risk flags for all MCP servers configured on the selected provider.
-
#begin_work_unit(work_unit_id:, context: {}) ⇒ TrifectaState
Begin tracking a work unit.
-
#check_agent_call_allowed!(operation:, requires_credentials: false) ⇒ TrifectaState
Check if an agent call would be allowed and enable egress flag.
-
#enabled? ⇒ Boolean
Check if security enforcement is enabled.
-
#end_work_unit ⇒ Hash
End tracking for current work unit.
-
#initialize(project_dir:, config: nil, enforcer: nil, secrets_proxy: nil, provider_info_class: nil) ⇒ WorkLoopAdapter
constructor
A new instance of WorkLoopAdapter.
-
#request_credential(secret_name:, scope: nil) ⇒ Hash
Request credentials through the secrets proxy This enables the private_data flag and returns a short-lived token.
-
#sanitized_environment ⇒ Hash
Get a sanitized environment for agent execution Strips all registered secrets from the environment.
-
#status ⇒ Object
Get current security status for display.
-
#with_sanitized_environment { ... } ⇒ Object
Execute a block with sanitized environment.
-
#would_allow?(flag) ⇒ Hash
Check if current state would allow enabling a flag.
Constructor Details
#initialize(project_dir:, config: nil, enforcer: nil, secrets_proxy: nil, provider_info_class: nil) ⇒ WorkLoopAdapter
Returns a new instance of WorkLoopAdapter.
46 47 48 49 50 51 52 53 54 55 |
# File 'lib/aidp/security/work_loop_adapter.rb', line 46 def initialize(project_dir:, config: nil, enforcer: nil, secrets_proxy: nil, provider_info_class: nil) @project_dir = project_dir @config = config || load_security_config @enforcer = enforcer || Aidp::Security.enforcer @secrets_proxy = secrets_proxy || Aidp::Security.secrets_proxy @provider_info_class = provider_info_class || Aidp::Harness::ProviderInfo @mcp_risk_profile = load_mcp_risk_profile @current_work_unit_id = nil @current_state = nil end |
Instance Attribute Details
#config ⇒ Object (readonly)
Returns the value of attribute config.
20 21 22 |
# File 'lib/aidp/security/work_loop_adapter.rb', line 20 def config @config end |
#current_state ⇒ Object (readonly)
Returns the value of attribute current_state.
20 21 22 |
# File 'lib/aidp/security/work_loop_adapter.rb', line 20 def current_state @current_state end |
#current_work_unit_id ⇒ Object (readonly)
Returns the value of attribute current_work_unit_id.
20 21 22 |
# File 'lib/aidp/security/work_loop_adapter.rb', line 20 def current_work_unit_id @current_work_unit_id end |
#mcp_risk_profile ⇒ Object (readonly)
Returns the value of attribute mcp_risk_profile.
20 21 22 |
# File 'lib/aidp/security/work_loop_adapter.rb', line 20 def mcp_risk_profile @mcp_risk_profile end |
#project_dir ⇒ Object (readonly)
Returns the value of attribute project_dir.
20 21 22 |
# File 'lib/aidp/security/work_loop_adapter.rb', line 20 def project_dir @project_dir end |
Instance Method Details
#apply_mcp_tool_risk!(tool_name) ⇒ Object
Apply deterministic MCP tool risk flags generated during configuration. This uses the stored profile and does not perform any AI calls at runtime.
139 140 141 142 143 144 145 146 |
# File 'lib/aidp/security/work_loop_adapter.rb', line 139 def apply_mcp_tool_risk!(tool_name) return @current_state unless enabled? && @current_state tool_profile = @mcp_risk_profile.tool(tool_name) return apply_unclassified_mcp_tool_risk!(tool_name) unless tool_profile enable_mcp_tool_flags(tool_name, tool_profile[:flags]) end |
#apply_provider_mcp_tool_risk!(provider_name, force_refresh: false) ⇒ Object
Apply deterministic MCP tool risk flags for all MCP servers configured on the selected provider. This reads only persisted provider metadata so the work loop never re-introspects provider CLIs at runtime.
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/aidp/security/work_loop_adapter.rb', line 151 def apply_provider_mcp_tool_risk!(provider_name, force_refresh: false) return @current_state unless enabled? && @current_state if force_refresh Aidp.log_debug("security.adapter", "ignored_provider_mcp_refresh_request", provider: provider_name) end provider_mcp_server_names(provider_name).each do |tool_name| apply_mcp_tool_risk!(tool_name) end @current_state rescue => e Aidp.log_warn("security.adapter", "mcp_provider_lookup_failed", provider: provider_name, error: e.) apply_unknown_mcp_risk!(provider_name) end |
#begin_work_unit(work_unit_id:, context: {}) ⇒ TrifectaState
Begin tracking a work unit
67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/aidp/security/work_loop_adapter.rb', line 67 def begin_work_unit(work_unit_id:, context: {}) return nil unless enabled? @current_work_unit_id = work_unit_id @current_state = @enforcer.begin_work_unit(work_unit_id: work_unit_id) # Analyze context for untrusted input detect_and_enable_untrusted_input(context) Aidp.log_debug("security.adapter", "work_unit_started", work_unit_id: work_unit_id, initial_state: @current_state.to_h) @current_state end |
#check_agent_call_allowed!(operation:, requires_credentials: false) ⇒ TrifectaState
Check if an agent call would be allowed and enable egress flag
103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/aidp/security/work_loop_adapter.rb', line 103 def check_agent_call_allowed!(operation:, requires_credentials: false) return @current_state unless enabled? && @current_state operation_str = operation.to_s # Check if this operation constitutes egress if egress_operation?(operation_str) begin @current_state.enable(:egress, source: "agent_operation:#{operation_str}") rescue PolicyViolation => e Aidp.log_warn("security.adapter", "egress_blocked", operation: operation_str, reason: e., current_state: @current_state.to_h) raise end end # If operation requires credentials, check if we can enable private_data if requires_credentials begin @current_state.enable(:private_data, source: "credential_access:#{operation_str}") rescue PolicyViolation => e Aidp.log_warn("security.adapter", "credential_access_blocked", operation: operation_str, reason: e., current_state: @current_state.to_h) raise end end @current_state end |
#enabled? ⇒ Boolean
Check if security enforcement is enabled
58 59 60 61 |
# File 'lib/aidp/security/work_loop_adapter.rb', line 58 def enabled? rule_of_two_config = @config[:rule_of_two] || {} rule_of_two_config.fetch(:enabled, true) end |
#end_work_unit ⇒ Hash
End tracking for current work unit
85 86 87 88 89 90 91 92 93 94 95 96 |
# File 'lib/aidp/security/work_loop_adapter.rb', line 85 def end_work_unit return nil unless enabled? && @current_work_unit_id summary = @enforcer.end_work_unit(@current_work_unit_id) @current_work_unit_id = nil @current_state = nil Aidp.log_debug("security.adapter", "work_unit_ended", summary: summary) summary end |
#request_credential(secret_name:, scope: nil) ⇒ Hash
Request credentials through the secrets proxy This enables the private_data flag and returns a short-lived token
177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 |
# File 'lib/aidp/security/work_loop_adapter.rb', line 177 def request_credential(secret_name:, scope: nil) unless enabled? # If security is disabled, return direct access (legacy mode) env_var = @secrets_proxy.registry.env_var_for(secret_name) return {token: ENV[env_var], direct_access: true} if env_var raise UnregisteredSecretError.new(secret_name: secret_name) end # Check if enabling private_data would violate Rule of Two if @current_state&.would_create_trifecta?(:private_data) raise PolicyViolation.new( flag: :private_data, source: "credential_request:#{secret_name}", current_state: @current_state.to_h, message: "Cannot access credentials for '#{secret_name}' - would create lethal trifecta" ) end # Enable private_data flag @current_state&.enable(:private_data, source: "secrets_proxy:#{secret_name}") # Request token from proxy @secrets_proxy.request_token(secret_name: secret_name, scope: scope) end |
#sanitized_environment ⇒ Hash
Get a sanitized environment for agent execution Strips all registered secrets from the environment
206 207 208 |
# File 'lib/aidp/security/work_loop_adapter.rb', line 206 def sanitized_environment @secrets_proxy.sanitized_environment end |
#status ⇒ Object
Get current security status for display
240 241 242 243 244 245 246 247 248 249 |
# File 'lib/aidp/security/work_loop_adapter.rb', line 240 def status return {enabled: false} unless enabled? { enabled: true, active_work_unit: @current_work_unit_id, state: @current_state&.to_h, status_string: @current_state&.status_string || "No active work unit" } end |
#with_sanitized_environment { ... } ⇒ Object
Execute a block with sanitized environment
213 214 215 |
# File 'lib/aidp/security/work_loop_adapter.rb', line 213 def with_sanitized_environment(&block) @secrets_proxy.with_sanitized_environment(&block) end |
#would_allow?(flag) ⇒ Hash
Check if current state would allow enabling a flag
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 |
# File 'lib/aidp/security/work_loop_adapter.rb', line 220 def would_allow?(flag) return {allowed: true, reason: "Security disabled"} unless enabled? return {allowed: true, reason: "No active work unit"} unless @current_state if @current_state.would_create_trifecta?(flag) { allowed: false, reason: "Would create lethal trifecta", current_state: @current_state.to_h } else { allowed: true, reason: "Operation allowed", enabled_count: @current_state.enabled_count } end end |