a2a-ruby 1.0.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +137 -0
- data/.simplecov +46 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +33 -0
- data/CODE_OF_CONDUCT.md +128 -0
- data/CONTRIBUTING.md +165 -0
- data/Gemfile +43 -0
- data/Guardfile +34 -0
- data/LICENSE.txt +21 -0
- data/PUBLISHING_CHECKLIST.md +214 -0
- data/README.md +171 -0
- data/Rakefile +165 -0
- data/docs/agent_execution.md +309 -0
- data/docs/api_reference.md +792 -0
- data/docs/configuration.md +780 -0
- data/docs/events.md +475 -0
- data/docs/getting_started.md +668 -0
- data/docs/integration.md +262 -0
- data/docs/server_apps.md +621 -0
- data/docs/troubleshooting.md +765 -0
- data/lib/a2a/client/api_methods.rb +263 -0
- data/lib/a2a/client/auth/api_key.rb +161 -0
- data/lib/a2a/client/auth/interceptor.rb +288 -0
- data/lib/a2a/client/auth/jwt.rb +189 -0
- data/lib/a2a/client/auth/oauth2.rb +146 -0
- data/lib/a2a/client/auth.rb +137 -0
- data/lib/a2a/client/base.rb +316 -0
- data/lib/a2a/client/config.rb +210 -0
- data/lib/a2a/client/connection_pool.rb +233 -0
- data/lib/a2a/client/http_client.rb +524 -0
- data/lib/a2a/client/json_rpc_handler.rb +136 -0
- data/lib/a2a/client/middleware/circuit_breaker_interceptor.rb +245 -0
- data/lib/a2a/client/middleware/logging_interceptor.rb +371 -0
- data/lib/a2a/client/middleware/rate_limit_interceptor.rb +142 -0
- data/lib/a2a/client/middleware/retry_interceptor.rb +161 -0
- data/lib/a2a/client/middleware.rb +116 -0
- data/lib/a2a/client/performance_tracker.rb +60 -0
- data/lib/a2a/configuration/defaults.rb +34 -0
- data/lib/a2a/configuration/environment_loader.rb +76 -0
- data/lib/a2a/configuration/file_loader.rb +115 -0
- data/lib/a2a/configuration/inheritance.rb +101 -0
- data/lib/a2a/configuration/validator.rb +180 -0
- data/lib/a2a/configuration.rb +201 -0
- data/lib/a2a/errors.rb +291 -0
- data/lib/a2a/modules.rb +50 -0
- data/lib/a2a/monitoring/alerting.rb +490 -0
- data/lib/a2a/monitoring/distributed_tracing.rb +398 -0
- data/lib/a2a/monitoring/health_endpoints.rb +204 -0
- data/lib/a2a/monitoring/metrics_collector.rb +438 -0
- data/lib/a2a/monitoring.rb +463 -0
- data/lib/a2a/plugin.rb +358 -0
- data/lib/a2a/plugin_manager.rb +159 -0
- data/lib/a2a/plugins/example_auth.rb +81 -0
- data/lib/a2a/plugins/example_middleware.rb +118 -0
- data/lib/a2a/plugins/example_transport.rb +76 -0
- data/lib/a2a/protocol/agent_card.rb +8 -0
- data/lib/a2a/protocol/agent_card_server.rb +584 -0
- data/lib/a2a/protocol/capability.rb +496 -0
- data/lib/a2a/protocol/json_rpc.rb +254 -0
- data/lib/a2a/protocol/message.rb +8 -0
- data/lib/a2a/protocol/task.rb +8 -0
- data/lib/a2a/rails/a2a_controller.rb +258 -0
- data/lib/a2a/rails/controller_helpers.rb +499 -0
- data/lib/a2a/rails/engine.rb +167 -0
- data/lib/a2a/rails/generators/agent_generator.rb +311 -0
- data/lib/a2a/rails/generators/install_generator.rb +209 -0
- data/lib/a2a/rails/generators/migration_generator.rb +232 -0
- data/lib/a2a/rails/generators/templates/add_a2a_indexes.rb +57 -0
- data/lib/a2a/rails/generators/templates/agent_controller.rb +122 -0
- data/lib/a2a/rails/generators/templates/agent_controller_spec.rb +160 -0
- data/lib/a2a/rails/generators/templates/agent_readme.md +200 -0
- data/lib/a2a/rails/generators/templates/create_a2a_push_notification_configs.rb +68 -0
- data/lib/a2a/rails/generators/templates/create_a2a_tasks.rb +83 -0
- data/lib/a2a/rails/generators/templates/example_agent_controller.rb +228 -0
- data/lib/a2a/rails/generators/templates/initializer.rb +108 -0
- data/lib/a2a/rails/generators/templates/push_notification_config_model.rb +228 -0
- data/lib/a2a/rails/generators/templates/task_model.rb +200 -0
- data/lib/a2a/rails/tasks/a2a.rake +228 -0
- data/lib/a2a/server/a2a_methods.rb +520 -0
- data/lib/a2a/server/agent.rb +537 -0
- data/lib/a2a/server/agent_execution/agent_executor.rb +279 -0
- data/lib/a2a/server/agent_execution/request_context.rb +219 -0
- data/lib/a2a/server/apps/rack_app.rb +311 -0
- data/lib/a2a/server/apps/sinatra_app.rb +261 -0
- data/lib/a2a/server/default_request_handler.rb +350 -0
- data/lib/a2a/server/events/event_consumer.rb +116 -0
- data/lib/a2a/server/events/event_queue.rb +226 -0
- data/lib/a2a/server/example_agent.rb +248 -0
- data/lib/a2a/server/handler.rb +281 -0
- data/lib/a2a/server/middleware/authentication_middleware.rb +212 -0
- data/lib/a2a/server/middleware/cors_middleware.rb +171 -0
- data/lib/a2a/server/middleware/logging_middleware.rb +362 -0
- data/lib/a2a/server/middleware/rate_limit_middleware.rb +382 -0
- data/lib/a2a/server/middleware.rb +213 -0
- data/lib/a2a/server/push_notification_manager.rb +327 -0
- data/lib/a2a/server/request_handler.rb +136 -0
- data/lib/a2a/server/storage/base.rb +141 -0
- data/lib/a2a/server/storage/database.rb +266 -0
- data/lib/a2a/server/storage/memory.rb +274 -0
- data/lib/a2a/server/storage/redis.rb +320 -0
- data/lib/a2a/server/storage.rb +38 -0
- data/lib/a2a/server/task_manager.rb +534 -0
- data/lib/a2a/transport/grpc.rb +481 -0
- data/lib/a2a/transport/http.rb +415 -0
- data/lib/a2a/transport/sse.rb +499 -0
- data/lib/a2a/types/agent_card.rb +540 -0
- data/lib/a2a/types/artifact.rb +99 -0
- data/lib/a2a/types/base_model.rb +223 -0
- data/lib/a2a/types/events.rb +117 -0
- data/lib/a2a/types/message.rb +106 -0
- data/lib/a2a/types/part.rb +288 -0
- data/lib/a2a/types/push_notification.rb +139 -0
- data/lib/a2a/types/security.rb +167 -0
- data/lib/a2a/types/task.rb +154 -0
- data/lib/a2a/types.rb +88 -0
- data/lib/a2a/utils/helpers.rb +245 -0
- data/lib/a2a/utils/message_buffer.rb +278 -0
- data/lib/a2a/utils/performance.rb +247 -0
- data/lib/a2a/utils/rails_detection.rb +97 -0
- data/lib/a2a/utils/structured_logger.rb +306 -0
- data/lib/a2a/utils/time_helpers.rb +167 -0
- data/lib/a2a/utils/validation.rb +8 -0
- data/lib/a2a/version.rb +6 -0
- data/lib/a2a-rails.rb +58 -0
- data/lib/a2a.rb +198 -0
- metadata +437 -0
@@ -0,0 +1,212 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "base64"
|
4
|
+
require_relative "../../errors"
|
5
|
+
|
6
|
+
module A2A
|
7
|
+
module Server
|
8
|
+
module Middleware
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
##
|
14
|
+
# Authentication middleware for A2A requests
|
15
|
+
#
|
16
|
+
# Handles authentication for A2A requests based on various security schemes
|
17
|
+
# including OAuth2, JWT, API keys, and HTTP authentication.
|
18
|
+
#
|
19
|
+
# @example Basic usage
|
20
|
+
# middleware = AuthenticationMiddleware.new(
|
21
|
+
# schemes: ['bearer', 'api_key'],
|
22
|
+
# required: true
|
23
|
+
# )
|
24
|
+
#
|
25
|
+
module A2A
|
26
|
+
module Server
|
27
|
+
module Middleware
|
28
|
+
class AuthenticationMiddleware
|
29
|
+
attr_reader :schemes, :required, :authenticators
|
30
|
+
|
31
|
+
##
|
32
|
+
# Initialize authentication middleware
|
33
|
+
#
|
34
|
+
# @param schemes [Array<String>] Allowed authentication schemes
|
35
|
+
# @param required [Boolean] Whether authentication is required
|
36
|
+
# @param authenticators [Hash] Custom authenticator implementations
|
37
|
+
def initialize(schemes: [], required: false, authenticators: {})
|
38
|
+
@schemes = schemes.map(&:to_s)
|
39
|
+
@required = required
|
40
|
+
@authenticators = default_authenticators.merge(authenticators)
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Process authentication for a request
|
45
|
+
#
|
46
|
+
# @param request [A2A::Protocol::Request] The JSON-RPC request
|
47
|
+
# @param context [A2A::Server::Context] The request context
|
48
|
+
# @yield Block to continue the middleware chain
|
49
|
+
# @return [Object] The result from the next middleware or handler
|
50
|
+
def call(request, context)
|
51
|
+
# Extract authentication information
|
52
|
+
auth_info = extract_authentication(request, context)
|
53
|
+
|
54
|
+
if auth_info
|
55
|
+
scheme, credentials = auth_info
|
56
|
+
|
57
|
+
# Check if scheme is allowed
|
58
|
+
unless @schemes.empty? || @schemes.include?(scheme)
|
59
|
+
raise A2A::Errors::AuthorizationFailed, "Authentication scheme '#{scheme}' not supported"
|
60
|
+
end
|
61
|
+
|
62
|
+
# Authenticate using the appropriate authenticator
|
63
|
+
authenticate(scheme, credentials, context)
|
64
|
+
|
65
|
+
elsif @required
|
66
|
+
raise A2A::Errors::AuthenticationRequired,
|
67
|
+
"Authentication required. Supported schemes: #{@schemes.join(', ')}"
|
68
|
+
end
|
69
|
+
|
70
|
+
# Continue to next middleware
|
71
|
+
yield
|
72
|
+
end
|
73
|
+
|
74
|
+
##
|
75
|
+
# Add a custom authenticator
|
76
|
+
#
|
77
|
+
# @param scheme [String] The authentication scheme name
|
78
|
+
# @param authenticator [Proc] The authenticator proc
|
79
|
+
def add_authenticator(scheme, authenticator)
|
80
|
+
@authenticators[scheme.to_s] = authenticator
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
##
|
86
|
+
# Extract authentication information from request/context
|
87
|
+
#
|
88
|
+
# @param request [A2A::Protocol::Request] The request
|
89
|
+
# @param context [A2A::Server::Context] The context
|
90
|
+
# @return [Array<String>, nil] [scheme, credentials] or nil if no auth
|
91
|
+
def extract_authentication(request, context)
|
92
|
+
# Try to extract from various sources
|
93
|
+
|
94
|
+
# 1. Check context metadata for HTTP headers
|
95
|
+
auth_header = context.get_metadata(:authorization) ||
|
96
|
+
context.get_metadata("Authorization")
|
97
|
+
|
98
|
+
return parse_authorization_header(auth_header) if auth_header
|
99
|
+
|
100
|
+
# 2. Check request parameters for API key
|
101
|
+
if request.params.is_a?(Hash)
|
102
|
+
api_key = request.params["api_key"] || request.params[:api_key]
|
103
|
+
return ["api_key", api_key] if api_key
|
104
|
+
end
|
105
|
+
|
106
|
+
# 3. Check context for pre-set authentication
|
107
|
+
if context.authenticated?
|
108
|
+
# Return the first available authentication scheme
|
109
|
+
context.instance_variable_get(:@auth_schemes)&.first
|
110
|
+
end
|
111
|
+
|
112
|
+
nil
|
113
|
+
end
|
114
|
+
|
115
|
+
##
|
116
|
+
# Parse Authorization header
|
117
|
+
#
|
118
|
+
# @param header [String] The Authorization header value
|
119
|
+
# @return [Array<String>] [scheme, credentials]
|
120
|
+
def parse_authorization_header(header)
|
121
|
+
parts = header.strip.split(" ", 2)
|
122
|
+
return nil if parts.length != 2
|
123
|
+
|
124
|
+
scheme = parts[0].downcase
|
125
|
+
credentials = parts[1]
|
126
|
+
|
127
|
+
[scheme, credentials]
|
128
|
+
end
|
129
|
+
|
130
|
+
##
|
131
|
+
# Authenticate using the appropriate authenticator
|
132
|
+
#
|
133
|
+
# @param scheme [String] The authentication scheme
|
134
|
+
# @param credentials [String] The credentials
|
135
|
+
# @param context [A2A::Server::Context] The request context
|
136
|
+
def authenticate(scheme, credentials, context)
|
137
|
+
authenticator = @authenticators[scheme]
|
138
|
+
|
139
|
+
unless authenticator
|
140
|
+
raise A2A::Errors::AuthorizationFailed,
|
141
|
+
"No authenticator configured for scheme '#{scheme}'"
|
142
|
+
end
|
143
|
+
|
144
|
+
# Call the authenticator
|
145
|
+
result = authenticator.call(credentials, context)
|
146
|
+
|
147
|
+
raise A2A::Errors::AuthorizationFailed, "Authentication failed for scheme '#{scheme}'" unless result
|
148
|
+
|
149
|
+
# Set authentication in context
|
150
|
+
context.set_authentication(scheme, result)
|
151
|
+
end
|
152
|
+
|
153
|
+
##
|
154
|
+
# Default authenticator implementations
|
155
|
+
#
|
156
|
+
# @return [Hash] Hash of scheme name to authenticator proc
|
157
|
+
def default_authenticators
|
158
|
+
{
|
159
|
+
"bearer" => method(:authenticate_bearer),
|
160
|
+
"basic" => method(:authenticate_basic),
|
161
|
+
"api_key" => method(:authenticate_api_key)
|
162
|
+
}
|
163
|
+
end
|
164
|
+
|
165
|
+
##
|
166
|
+
# Authenticate Bearer token (JWT or API key)
|
167
|
+
#
|
168
|
+
# @param token [String] The bearer token
|
169
|
+
# @param context [A2A::Server::Context] The request context
|
170
|
+
# @return [Hash, nil] Authentication result or nil if invalid
|
171
|
+
def authenticate_bearer(token, _context)
|
172
|
+
# This is a basic implementation - in practice you would:
|
173
|
+
# 1. Validate JWT signature and expiration
|
174
|
+
# 2. Check API key against database
|
175
|
+
# 3. Extract user/session information
|
176
|
+
|
177
|
+
# For now, just return the token as valid
|
178
|
+
{ token: token, scheme: "bearer" }
|
179
|
+
end
|
180
|
+
|
181
|
+
##
|
182
|
+
# Authenticate Basic authentication
|
183
|
+
#
|
184
|
+
# @param credentials [String] Base64 encoded username:password
|
185
|
+
# @param context [A2A::Server::Context] The request context
|
186
|
+
# @return [Hash, nil] Authentication result or nil if invalid
|
187
|
+
def authenticate_basic(credentials, _context)
|
188
|
+
decoded = Base64.decode64(credentials)
|
189
|
+
username, = decoded.split(":", 2)
|
190
|
+
|
191
|
+
# In practice, validate against user database
|
192
|
+
# For now, just return the username
|
193
|
+
{ username: username, scheme: "basic" }
|
194
|
+
rescue StandardError
|
195
|
+
nil
|
196
|
+
end
|
197
|
+
|
198
|
+
##
|
199
|
+
# Authenticate API key
|
200
|
+
#
|
201
|
+
# @param api_key [String] The API key
|
202
|
+
# @param context [A2A::Server::Context] The request context
|
203
|
+
# @return [Hash, nil] Authentication result or nil if invalid
|
204
|
+
def authenticate_api_key(api_key, _context)
|
205
|
+
# In practice, validate against API key database
|
206
|
+
# For now, just return the key as valid
|
207
|
+
{ api_key: api_key, scheme: "api_key" }
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
##
|
4
|
+
# CORS (Cross-Origin Resource Sharing) middleware for A2A requests
|
5
|
+
#
|
6
|
+
# Handles CORS headers for A2A JSON-RPC requests to enable
|
7
|
+
# cross-origin requests from web browsers.
|
8
|
+
#
|
9
|
+
# @example Basic usage
|
10
|
+
# middleware = CorsMiddleware.new(
|
11
|
+
# origins: ['https://example.com'],
|
12
|
+
# methods: ['POST', 'OPTIONS'],
|
13
|
+
# headers: ['Content-Type', 'Authorization']
|
14
|
+
# )
|
15
|
+
#
|
16
|
+
module A2A
|
17
|
+
module Server
|
18
|
+
module Middleware
|
19
|
+
class CorsMiddleware
|
20
|
+
attr_reader :origins, :methods, :headers, :credentials, :max_age
|
21
|
+
|
22
|
+
##
|
23
|
+
# Initialize CORS middleware
|
24
|
+
#
|
25
|
+
# @param origins [Array<String>, String] Allowed origins (* for all)
|
26
|
+
# @param methods [Array<String>] Allowed HTTP methods
|
27
|
+
# @param headers [Array<String>] Allowed headers
|
28
|
+
# @param credentials [Boolean] Whether to allow credentials
|
29
|
+
# @param max_age [Integer] Preflight cache duration in seconds
|
30
|
+
# @param expose_headers [Array<String>] Headers to expose to client
|
31
|
+
def initialize(origins: "*", methods: %w[POST OPTIONS],
|
32
|
+
headers: %w[Content-Type Authorization],
|
33
|
+
credentials: false, max_age: 86_400, expose_headers: [])
|
34
|
+
@origins = normalize_origins(origins)
|
35
|
+
@methods = Array(methods).map(&:upcase)
|
36
|
+
@headers = Array(headers)
|
37
|
+
@credentials = credentials
|
38
|
+
@max_age = max_age
|
39
|
+
@expose_headers = Array(expose_headers)
|
40
|
+
end
|
41
|
+
|
42
|
+
##
|
43
|
+
# Process CORS for a request
|
44
|
+
#
|
45
|
+
# @param request [A2A::Protocol::Request] The JSON-RPC request
|
46
|
+
# @param context [A2A::Server::Context] The request context
|
47
|
+
# @yield Block to continue the middleware chain
|
48
|
+
# @return [Object] The result from the next middleware or handler
|
49
|
+
def call(_request, context)
|
50
|
+
# Extract HTTP method and origin from context
|
51
|
+
http_method = context.get_metadata(:http_method) || "POST"
|
52
|
+
origin = context.get_metadata(:origin) || context.get_metadata("Origin")
|
53
|
+
|
54
|
+
# Handle preflight requests
|
55
|
+
return handle_preflight(origin, context) if http_method.casecmp("OPTIONS").zero?
|
56
|
+
|
57
|
+
# Add CORS headers to the response
|
58
|
+
add_cors_headers(origin, context)
|
59
|
+
|
60
|
+
# Continue to next middleware
|
61
|
+
yield
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Check if an origin is allowed
|
66
|
+
#
|
67
|
+
# @param origin [String] The origin to check
|
68
|
+
# @return [Boolean] True if origin is allowed
|
69
|
+
def origin_allowed?(origin)
|
70
|
+
return true if @origins == "*"
|
71
|
+
return false if origin.nil?
|
72
|
+
|
73
|
+
@origins.any? do |allowed_origin|
|
74
|
+
if allowed_origin.include?("*")
|
75
|
+
# Handle wildcard patterns
|
76
|
+
pattern = Regexp.escape(allowed_origin).gsub('\*', ".*")
|
77
|
+
origin.match?(/\A#{pattern}\z/)
|
78
|
+
else
|
79
|
+
origin == allowed_origin
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# Get CORS headers for an origin
|
86
|
+
#
|
87
|
+
# @param origin [String] The request origin
|
88
|
+
# @return [Hash] CORS headers
|
89
|
+
def cors_headers(origin)
|
90
|
+
headers = {}
|
91
|
+
|
92
|
+
if origin_allowed?(origin)
|
93
|
+
headers["Access-Control-Allow-Origin"] = @origins == "*" ? "*" : origin
|
94
|
+
|
95
|
+
headers["Access-Control-Allow-Credentials"] = "true" if @credentials
|
96
|
+
|
97
|
+
headers["Access-Control-Expose-Headers"] = @expose_headers.join(", ") unless @expose_headers.empty?
|
98
|
+
end
|
99
|
+
|
100
|
+
headers
|
101
|
+
end
|
102
|
+
|
103
|
+
##
|
104
|
+
# Get preflight CORS headers
|
105
|
+
#
|
106
|
+
# @param origin [String] The request origin
|
107
|
+
# @return [Hash] Preflight CORS headers
|
108
|
+
def preflight_headers(origin)
|
109
|
+
headers = cors_headers(origin)
|
110
|
+
|
111
|
+
if origin_allowed?(origin)
|
112
|
+
headers["Access-Control-Allow-Methods"] = @methods.join(", ")
|
113
|
+
headers["Access-Control-Allow-Headers"] = @headers.join(", ")
|
114
|
+
headers["Access-Control-Max-Age"] = @max_age.to_s
|
115
|
+
end
|
116
|
+
|
117
|
+
headers
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
##
|
123
|
+
# Normalize origins configuration
|
124
|
+
#
|
125
|
+
# @param origins [Array, String] Origins configuration
|
126
|
+
# @return [Array<String>, String] Normalized origins
|
127
|
+
def normalize_origins(origins)
|
128
|
+
case origins
|
129
|
+
when String
|
130
|
+
origins == "*" ? "*" : [origins]
|
131
|
+
when Array
|
132
|
+
origins.include?("*") ? "*" : origins
|
133
|
+
else
|
134
|
+
["*"]
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
##
|
139
|
+
# Handle preflight OPTIONS request
|
140
|
+
#
|
141
|
+
# @param origin [String] The request origin
|
142
|
+
# @param context [A2A::Server::Context] The request context
|
143
|
+
# @return [Hash] Preflight response
|
144
|
+
def handle_preflight(origin, context)
|
145
|
+
# Add preflight headers to context for response
|
146
|
+
preflight_headers(origin).each do |key, value|
|
147
|
+
context.set_metadata("response_header_#{key.downcase}", value)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Return empty response for preflight
|
151
|
+
{
|
152
|
+
status: 200,
|
153
|
+
headers: preflight_headers(origin),
|
154
|
+
body: ""
|
155
|
+
}
|
156
|
+
end
|
157
|
+
|
158
|
+
##
|
159
|
+
# Add CORS headers to response context
|
160
|
+
#
|
161
|
+
# @param origin [String] The request origin
|
162
|
+
# @param context [A2A::Server::Context] The request context
|
163
|
+
def add_cors_headers(origin, context)
|
164
|
+
cors_headers(origin).each do |key, value|
|
165
|
+
context.set_metadata("response_header_#{key.downcase}", value)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|