Class: Aidp::Security::WorkLoopAdapter

Inherits:
Object
  • Object
show all
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

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

Instance Method Summary collapse

Constructor Details

#initialize(project_dir:, config: nil, enforcer: nil, secrets_proxy: nil) ⇒ WorkLoopAdapter

Returns a new instance of WorkLoopAdapter.



45
46
47
48
49
50
51
52
# File 'lib/aidp/security/work_loop_adapter.rb', line 45

def initialize(project_dir:, config: nil, enforcer: nil, secrets_proxy: nil)
  @project_dir = project_dir
  @config = config || load_security_config
  @enforcer = enforcer || Aidp::Security.enforcer
  @secrets_proxy = secrets_proxy || Aidp::Security.secrets_proxy
  @current_work_unit_id = nil
  @current_state = nil
end

Instance Attribute Details

#configObject (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_stateObject (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_idObject (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

#project_dirObject (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

#begin_work_unit(work_unit_id:, context: {}) ⇒ TrifectaState

Begin tracking a work unit

Parameters:

  • work_unit_id (String)

    Unique identifier for the work unit

  • context (Hash) (defaults to: {})

    Work context containing source information

Returns:



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# File 'lib/aidp/security/work_loop_adapter.rb', line 64

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

Parameters:

  • operation (String, Symbol)

    The type of operation (e.g., :git_push)

  • requires_credentials (Boolean) (defaults to: false)

    Whether operation needs credentials

Returns:

Raises:



100
101
102
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
# File 'lib/aidp/security/work_loop_adapter.rb', line 100

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.message,
        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.message,
        current_state: @current_state.to_h)
      raise
    end
  end

  @current_state
end

#enabled?Boolean

Check if security enforcement is enabled

Returns:

  • (Boolean)


55
56
57
58
# File 'lib/aidp/security/work_loop_adapter.rb', line 55

def enabled?
  rule_of_two_config = @config[:rule_of_two] || {}
  rule_of_two_config.fetch(:enabled, true)
end

#end_work_unitHash

End tracking for current work unit

Returns:

  • (Hash)

    Final state summary



82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/aidp/security/work_loop_adapter.rb', line 82

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

Parameters:

  • secret_name (String)

    The registered secret name

  • scope (String) (defaults to: nil)

    The intended use of this credential

Returns:

  • (Hash)

    Token details from the secrets proxy

Raises:



140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/aidp/security/work_loop_adapter.rb', line 140

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_environmentHash

Get a sanitized environment for agent execution Strips all registered secrets from the environment

Returns:

  • (Hash)

    Sanitized environment hash



169
170
171
# File 'lib/aidp/security/work_loop_adapter.rb', line 169

def sanitized_environment
  @secrets_proxy.sanitized_environment
end

#statusObject

Get current security status for display



203
204
205
206
207
208
209
210
211
212
# File 'lib/aidp/security/work_loop_adapter.rb', line 203

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

Yields:

  • The block to execute

Returns:

  • (Object)

    The result of the block



176
177
178
# File 'lib/aidp/security/work_loop_adapter.rb', line 176

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

Parameters:

  • flag (Symbol)

    :untrusted_input, :private_data, or :egress

Returns:

  • (Hash)

    { allowed: boolean, reason: string }



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/aidp/security/work_loop_adapter.rb', line 183

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