Module: A2A::Rails::ControllerHelpers

Extended by:
ActiveSupport::Concern
Includes:
Utils::RailsDetection
Included in:
A2aController
Defined in:
lib/a2a/rails/controller_helpers.rb

Overview

Controller helpers for A2A Rails integration

This module provides helper methods for Rails controllers to handle A2A requests, generate agent cards, and integrate with Rails authentication systems.

Examples:

Basic usage

class MyAgentController < ApplicationController
  include A2A::Rails::ControllerHelpers

  a2a_skill "greeting" do |skill|
    skill.description = "Greet users"
  end

  a2a_method "greet" do |params|
    { message: "Hello, #{params[:name]}!" }
  end
end

Instance Method Summary collapse

Methods included from Utils::RailsDetection

#rails_application, #rails_available?, #rails_development?, #rails_environment, #rails_logger, #rails_production?, #rails_version, #rails_version_requires_validation?, #rails_version_supported?

Instance Method Details

#a2a_request?Boolean

Check if current request is an A2A request

Returns:

  • (Boolean)

    True if this is an A2A request



199
200
201
202
203
# File 'lib/a2a/rails/controller_helpers.rb', line 199

def a2a_request?
  request.path.start_with?(A2A.config.mount_path) ||
    request.headers["Content-Type"]&.include?("application/json-rpc") ||
    params[:controller] == "a2a/rails/a2a"
end

#agent_card_urlObject (private)

URL helpers for agent card generation



462
463
464
# File 'lib/a2a/rails/controller_helpers.rb', line 462

def agent_card_url
  "#{request.base_url}#{A2A.config.mount_path}/agent-card"
end

#api_key_authenticated?Boolean (private)

Returns:

  • (Boolean)


452
453
454
455
456
457
458
459
# File 'lib/a2a/rails/controller_helpers.rb', line 452

def api_key_authenticated?
  api_key = request.headers["X-API-Key"] || params[:api_key]
  return false unless api_key.present?

  # Basic API key validation - applications should override this
  # In a real application, this would check against a database or configuration
  api_key.length >= 32 # Simple validation
end

#authenticate_a2a_requestObject (private)



296
297
298
299
300
301
# File 'lib/a2a/rails/controller_helpers.rb', line 296

def authenticate_a2a_request
  return unless A2A.config.authentication_required
  return if current_user_authenticated?

  raise A2A::Errors::AuthenticationError, "Authentication required"
end

#build_a2a_error_response(error, id = nil) ⇒ Object (private)



284
285
286
287
288
289
# File 'lib/a2a/rails/controller_helpers.rb', line 284

def build_a2a_error_response(error, id = nil)
  A2A::Protocol::JsonRpc.build_response(
    error: error.to_json_rpc_error,
    id: id
  )
end

#build_additional_interfacesObject (private)



333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# File 'lib/a2a/rails/controller_helpers.rb', line 333

def build_additional_interfaces
  interfaces = []

  # Add gRPC interface if available
  if defined?(A2A::Transport::Grpc)
    interfaces << {
      transport: "GRPC",
      url: grpc_endpoint_url
    }
  end

  # Add HTTP+JSON interface
  interfaces << {
    transport: "HTTP+JSON",
    url: http_json_endpoint_url
  }

  interfaces
end

#build_agent_metadata(config) ⇒ Object (private)



412
413
414
415
416
417
418
419
420
421
# File 'lib/a2a/rails/controller_helpers.rb', line 412

def (config)
   = {
    controller: controller_name,
    action: action_name,
    rails_version: rails_version || "unknown",
    created_at: Time.now.iso8601
  }

  .merge(config[:metadata] || {})
end

#build_provider_infoObject (private)



384
385
386
387
388
389
390
391
392
393
394
395
# File 'lib/a2a/rails/controller_helpers.rb', line 384

def build_provider_info
  app = rails_application
  {
    name: app&.class&.module_parent_name || "Unknown",
    version: begin
      app&.config&.version || "1.0.0"
    rescue StandardError
      "1.0.0"
    end,
    url: root_url
  }
end

#build_security_configObject (private)



353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/a2a/rails/controller_helpers.rb', line 353

def build_security_config
  auth_config = self.class.a2a_auth_config

  case auth_config[:strategy]
  when :jwt
    {
      security_schemes: {
        jwt_auth: {
          type: "http",
          scheme: "bearer",
          bearer_format: "JWT"
        }
      },
      security: [{ jwt_auth: [] }]
    }
  when :api_key
    {
      security_schemes: {
        api_key_auth: {
          type: "apiKey",
          in: "header",
          name: "X-API-Key"
        }
      },
      security: [{ api_key_auth: [] }]
    }
  else
    {}
  end
end

#collect_capabilities_hashObject (private)



324
325
326
327
328
329
330
331
# File 'lib/a2a/rails/controller_helpers.rb', line 324

def collect_capabilities_hash
  {
    streaming: A2A.config.streaming_enabled,
    push_notifications: A2A.config.push_notifications_enabled,
    state_transition_history: true,
    extensions: []
  }
end

#collect_skillsObject (private)



309
310
311
312
313
314
315
316
317
318
319
320
321
322
# File 'lib/a2a/rails/controller_helpers.rb', line 309

def collect_skills
  capabilities = self.class._a2a_capabilities || []
  capabilities.map do |capability|
    {
      id: capability.name,
      name: capability.name.humanize,
      description: capability.description,
      tags: capability.tags || [],
      examples: capability.examples || [],
      input_modes: capability.input_modes || A2A.config.default_input_modes,
      output_modes: capability.output_modes || A2A.config.default_output_modes
    }
  end
end

#current_user_authenticated?Boolean

Check if current user is authenticated for A2A requests

This method integrates with various Rails authentication systems.

Returns:

  • (Boolean)

    True if user is authenticated



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/a2a/rails/controller_helpers.rb', line 212

def current_user_authenticated?
  auth_config = self.class.a2a_auth_config

  case auth_config[:strategy]
  when :devise
    respond_to?(:current_user) && current_user.present?
  when :jwt
    jwt_authenticated?
  when :api_key
    api_key_authenticated?
  when :custom
    auth_config[:block]&.call(request) || false
  else
    true # No authentication required
  end
end

#current_user_infoHash

Get current user information for authenticated cards

Returns:

  • (Hash)

    User information hash



234
235
236
237
238
239
240
241
242
243
244
245
# File 'lib/a2a/rails/controller_helpers.rb', line 234

def 
  if respond_to?(:current_user) && current_user.present?
    {
      id: current_user.id,
      email: current_user.email,
      name: current_user.name || current_user.email,
      roles: current_user_roles
    }
  else
    {}
  end
end

#current_user_permissionsArray<String>

Get current user permissions for authenticated cards

Returns:

  • (Array<String>)

    List of user permissions



252
253
254
255
256
257
258
259
260
261
262
263
264
265
# File 'lib/a2a/rails/controller_helpers.rb', line 252

def current_user_permissions
  if respond_to?(:current_user) && current_user.present?
    # Try common permission methods
    if current_user.respond_to?(:permissions)
      current_user.permissions
    elsif current_user.respond_to?(:roles)
      current_user.roles.map(&:name)
    else
      []
    end
  else
    []
  end
end

#current_user_rolesObject (private)



423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'lib/a2a/rails/controller_helpers.rb', line 423

def current_user_roles
  if respond_to?(:current_user) && current_user.present?
    if current_user.respond_to?(:roles)
      current_user.roles.map(&:name)
    elsif current_user.respond_to?(:role)
      [current_user.role]
    else
      ["user"]
    end
  else
    []
  end
end

#documentation_urlObject (private)



476
477
478
# File 'lib/a2a/rails/controller_helpers.rb', line 476

def documentation_url
  "#{request.base_url}/docs/a2a"
end

#enhance_authenticated_card(card_data) ⇒ Object (private)



401
402
403
404
405
406
407
408
409
410
# File 'lib/a2a/rails/controller_helpers.rb', line 401

def enhance_authenticated_card(card_data)
  card_data.merge(
    authenticated_user: ,
    permissions: current_user_permissions,
    authentication_context: {
      strategy: self.class.a2a_auth_config[:strategy],
      authenticated_at: Time.now.iso8601
    }
  )
end

#generate_agent_card(authenticated: false) ⇒ A2A::Types::AgentCard

Generate agent card for this controller

Parameters:

  • authenticated (Boolean) (defaults to: false)

    Whether to generate an authenticated card

Returns:



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
# File 'lib/a2a/rails/controller_helpers.rb', line 143

def generate_agent_card(authenticated: false)
  config = self.class.a2a_agent_config

  # Build base agent card
  card_data = {
    name: config[:name] || controller_name.humanize,
    description: config[:description] || "A2A agent for #{controller_name}",
    version: config[:version] || "1.0.0",
    url: agent_card_url,
    preferred_transport: "JSONRPC",
    skills: collect_skills,
    capabilities: collect_capabilities_hash,
    default_input_modes: A2A.config.default_input_modes,
    default_output_modes: A2A.config.default_output_modes,
    additional_interfaces: build_additional_interfaces,
    security: build_security_config,
    provider: build_provider_info,
    protocol_version: A2A.config.protocol_version,
    supports_authenticated_extended_card: supports_authenticated_card?,
    documentation_url: documentation_url,
    metadata: (config)
  }

  # Add authenticated-specific information
  card_data = enhance_authenticated_card(card_data) if authenticated && current_user_authenticated?

  A2A::Types::AgentCard.new(**card_data)
end

#grpc_endpoint_urlObject (private)



466
467
468
469
470
# File 'lib/a2a/rails/controller_helpers.rb', line 466

def grpc_endpoint_url
  # Convert HTTP URL to gRPC URL (typically different port)
  base_url = request.base_url.gsub(/:\d+/, ":#{grpc_port}")
  "#{base_url}/a2a.grpc"
end

#grpc_portObject (private)



480
481
482
483
# File 'lib/a2a/rails/controller_helpers.rb', line 480

def grpc_port
  # Default gRPC port - applications can override this
  rails_production? ? 443 : 50_051
end

#handle_a2a_error(error) ⇒ Object (private)

Exception handlers



486
487
488
# File 'lib/a2a/rails/controller_helpers.rb', line 486

def handle_a2a_error(error)
  render json: build_a2a_error_response(error), status: :bad_request
end

#handle_a2a_rpcHash

Handle A2A JSON-RPC requests

This method processes incoming JSON-RPC requests and delegates them to the appropriate A2A method handlers.

Returns:

  • (Hash)

    JSON-RPC response



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/a2a/rails/controller_helpers.rb', line 115

def handle_a2a_rpc
  request_body = request.body.read

  begin
    json_rpc_request = A2A::Protocol::JsonRpc.parse_request(request_body)

    # Handle batch requests
    if json_rpc_request.is_a?(Array)
      responses = json_rpc_request.map { |req| handle_single_a2a_request(req) }
      render json: responses
    else
      response = handle_single_a2a_request(json_rpc_request)
      render json: response
    end
  rescue A2A::Errors::A2AError => e
    render json: build_a2a_error_response(e), status: :bad_request
  rescue StandardError => e
    error = A2A::Errors::InternalError.new(e.message)
    render json: build_a2a_error_response(error), status: :internal_server_error
  end
end

#handle_authentication_error(error) ⇒ Object (private)



494
495
496
# File 'lib/a2a/rails/controller_helpers.rb', line 494

def handle_authentication_error(error)
  render json: build_a2a_error_response(error), status: :unauthorized
end

#handle_single_a2a_request(json_rpc_request) ⇒ Object (private)



269
270
271
272
273
274
275
276
277
278
279
280
281
282
# File 'lib/a2a/rails/controller_helpers.rb', line 269

def handle_single_a2a_request(json_rpc_request)
  # Check method-level authentication
  if method_requires_authentication?(json_rpc_request.method) && !current_user_authenticated?
    raise A2A::Errors::AuthenticationError, "Authentication required for method: #{json_rpc_request.method}"
  end

  # Delegate to the A2A request handler from Server::Agent
  handle_a2a_request(json_rpc_request)
rescue A2A::Errors::A2AError => e
  build_a2a_error_response(e, json_rpc_request.id)
rescue StandardError => e
  error = A2A::Errors::InternalError.new(e.message)
  build_a2a_error_response(error, json_rpc_request.id)
end

#handle_task_not_found(error) ⇒ Object (private)



490
491
492
# File 'lib/a2a/rails/controller_helpers.rb', line 490

def handle_task_not_found(error)
  render json: build_a2a_error_response(error), status: :not_found
end

#http_json_endpoint_urlObject (private)



472
473
474
# File 'lib/a2a/rails/controller_helpers.rb', line 472

def http_json_endpoint_url
  "#{request.base_url}#{A2A.config.mount_path}/http"
end

#jwt_authenticated?Boolean (private)

Returns:

  • (Boolean)


437
438
439
440
441
442
443
444
445
446
447
448
449
450
# File 'lib/a2a/rails/controller_helpers.rb', line 437

def jwt_authenticated?
  auth_header = request.headers["Authorization"]
  return false unless auth_header&.start_with?("Bearer ")

  token = auth_header.split.last

  begin
    # Basic JWT validation - applications should override this
    JWT.decode(token, nil, false)
    true
  rescue JWT::DecodeError
    false
  end
end

#method_requires_authentication?(method_name) ⇒ Boolean (private)

Returns:

  • (Boolean)


291
292
293
294
# File 'lib/a2a/rails/controller_helpers.rb', line 291

def method_requires_authentication?(method_name)
  auth_config = self.class.a2a_auth_config
  auth_config[:methods].include?(method_name.to_s)
end

#render_agent_card(authenticated: false, format: :json) ⇒ Object

Render agent card as JSON response

Parameters:

  • authenticated (Boolean) (defaults to: false)

    Whether to render authenticated card

  • format (Symbol) (defaults to: :json)

    Response format (:json, :jws)



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# File 'lib/a2a/rails/controller_helpers.rb', line 178

def render_agent_card(authenticated: false, format: :json)
  card = generate_agent_card(authenticated: authenticated)

  case format
  when :json
    render json: card.to_h
  when :jws
    # TODO: Implement JWS signing
    render json: { error: "JWS format not yet implemented" }, status: :not_implemented
  else
    render json: card.to_h
  end
rescue StandardError => e
  render json: { error: e.message }, status: :internal_server_error
end

#set_a2a_headersObject (private)



303
304
305
306
307
# File 'lib/a2a/rails/controller_helpers.rb', line 303

def set_a2a_headers
  response.headers["X-A2A-Version"] = A2A::VERSION
  response.headers["X-A2A-Protocol-Version"] = A2A.config.protocol_version
  response.headers["Content-Type"] = "application/json"
end

#supports_authenticated_card?Boolean (private)

Returns:

  • (Boolean)


397
398
399
# File 'lib/a2a/rails/controller_helpers.rb', line 397

def supports_authenticated_card?
  self.class.a2a_auth_config[:strategy] != :none
end