actionagent 1.8.0 → 1.9.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.
Files changed (168) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/action_agent.css +1 -1
  3. data/app/assets/builds/action_agent.js +76 -73
  4. data/app/assets/builds/action_agent_recorder.js +78 -0
  5. data/app/assets/builds/action_agent_replay.js +226 -0
  6. data/app/controllers/action_agent/api/agent_runs_controller.rb +3 -1
  7. data/app/controllers/action_agent/api/agents_controller.rb +1 -0
  8. data/app/controllers/action_agent/api/api_keys_controller.rb +12 -2
  9. data/app/controllers/action_agent/api/base_controller.rb +63 -2
  10. data/app/controllers/action_agent/api/claude_logins_controller.rb +93 -0
  11. data/app/controllers/action_agent/api/code_sessions_controller.rb +71 -12
  12. data/app/controllers/action_agent/api/dashboard_assistant_controller.rb +2 -2
  13. data/app/controllers/action_agent/api/draft_pull_requests_controller.rb +248 -0
  14. data/app/controllers/action_agent/api/evaluation_fixes_controller.rb +27 -0
  15. data/app/controllers/action_agent/api/evaluations_controller.rb +58 -7
  16. data/app/controllers/action_agent/api/explorations_controller.rb +213 -0
  17. data/app/controllers/action_agent/api/github_app_manifests_controller.rb +125 -0
  18. data/app/controllers/action_agent/api/github_connections_controller.rb +40 -2
  19. data/app/controllers/action_agent/api/github_installations_controller.rb +280 -0
  20. data/app/controllers/action_agent/api/input_requests_controller.rb +77 -0
  21. data/app/controllers/action_agent/api/mcp_controller.rb +46 -12
  22. data/app/controllers/action_agent/api/members_controller.rb +36 -0
  23. data/app/controllers/action_agent/api/project_explorations_controller.rb +80 -0
  24. data/app/controllers/action_agent/api/project_install_pull_requests_controller.rb +305 -0
  25. data/app/controllers/action_agent/api/project_secrets_controller.rb +96 -0
  26. data/app/controllers/action_agent/api/project_setup_controller.rb +151 -0
  27. data/app/controllers/action_agent/api/project_sign_ins_controller.rb +112 -0
  28. data/app/controllers/action_agent/api/projects_controller.rb +512 -0
  29. data/app/controllers/action_agent/api/provider_keys_controller.rb +176 -24
  30. data/app/controllers/action_agent/api/provider_models_controller.rb +37 -15
  31. data/app/controllers/action_agent/api/recording_event_ingest_controller.rb +67 -0
  32. data/app/controllers/action_agent/api/sandbox_browser_tickets_controller.rb +50 -0
  33. data/app/controllers/action_agent/api/sandbox_browsers_controller.rb +65 -0
  34. data/app/controllers/action_agent/api/sandboxes_controller.rb +148 -8
  35. data/app/controllers/action_agent/api/scenario_catalog_entries_controller.rb +188 -0
  36. data/app/controllers/action_agent/api/scenario_catalogs_controller.rb +302 -0
  37. data/app/controllers/action_agent/api/session_recordings_controller.rb +165 -34
  38. data/app/controllers/action_agent/api/sessions_controller.rb +111 -0
  39. data/app/controllers/action_agent/api/telemetry_keys_controller.rb +21 -0
  40. data/app/controllers/action_agent/dashboard_controller.rb +17 -6
  41. data/app/controllers/action_agent/session_player_controller.rb +58 -0
  42. data/app/controllers/concerns/action_agent/api/agent_serialization.rb +1 -0
  43. data/app/controllers/concerns/action_agent/api/mcp_dashboard_tools.rb +627 -20
  44. data/app/controllers/concerns/action_agent/api/project_browser.rb +46 -0
  45. data/app/controllers/concerns/action_agent/api/project_secret_authorization.rb +57 -0
  46. data/app/jobs/action_agent/agent_execution_job.rb +9 -39
  47. data/app/jobs/action_agent/agent_resume_job.rb +84 -0
  48. data/app/jobs/action_agent/code_session_job.rb +39 -10
  49. data/app/jobs/action_agent/draft_pull_request_job.rb +43 -0
  50. data/app/jobs/action_agent/exploration_job.rb +80 -0
  51. data/app/jobs/action_agent/input_request_expiry_job.rb +22 -0
  52. data/app/jobs/action_agent/project_evaluation_job.rb +59 -0
  53. data/app/jobs/action_agent/project_setup_job.rb +21 -0
  54. data/app/jobs/action_agent/sandbox_cleanup_job.rb +20 -3
  55. data/app/jobs/action_agent/sandbox_provision_job.rb +77 -10
  56. data/app/jobs/action_agent/sandbox_run_job.rb +9 -37
  57. data/app/jobs/action_agent/verify_evaluation_fix_job.rb +37 -0
  58. data/app/models/action_agent/agent.rb +72 -26
  59. data/app/models/action_agent/agent_run.rb +134 -23
  60. data/app/models/action_agent/agent_template.rb +35 -1
  61. data/app/models/action_agent/api_key.rb +14 -0
  62. data/app/models/action_agent/catalog_scenario.rb +75 -0
  63. data/app/models/action_agent/code_session.rb +21 -5
  64. data/app/models/action_agent/draft_pull_request.rb +131 -0
  65. data/app/models/action_agent/evaluation.rb +148 -0
  66. data/app/models/action_agent/evaluation_run.rb +143 -53
  67. data/app/models/action_agent/evaluation_scenario_result.rb +15 -3
  68. data/app/models/action_agent/exploration.rb +847 -0
  69. data/app/models/action_agent/github_installation.rb +159 -0
  70. data/app/models/action_agent/input_request.rb +291 -0
  71. data/app/models/action_agent/model_pricing.rb +214 -34
  72. data/app/models/action_agent/project.rb +789 -0
  73. data/app/models/action_agent/project_secret.rb +339 -0
  74. data/app/models/action_agent/provider_key.rb +207 -4
  75. data/app/models/action_agent/recording_event.rb +145 -0
  76. data/app/models/action_agent/recording_snapshot.rb +7 -7
  77. data/app/models/action_agent/sandbox_session.rb +300 -16
  78. data/app/models/action_agent/scenario_catalog.rb +172 -0
  79. data/app/models/action_agent/scenario_product.rb +57 -0
  80. data/app/models/action_agent/scenario_set.rb +138 -0
  81. data/app/models/action_agent/session_recording.rb +121 -14
  82. data/app/models/action_agent/telemetry_trace.rb +74 -19
  83. data/app/models/concerns/action_agent/ownable.rb +84 -13
  84. data/app/serializers/action_agent/evaluation_serializer.rb +41 -10
  85. data/app/serializers/action_agent/input_request_serializer.rb +47 -0
  86. data/app/serializers/action_agent/scenario_catalog_serializer.rb +23 -0
  87. data/app/serializers/action_agent/session_summary_serializer.rb +146 -0
  88. data/app/services/action_agent/agent_execution_service.rb +450 -72
  89. data/app/services/action_agent/agent_scorecard.rb +50 -21
  90. data/app/services/action_agent/agent_sync.rb +5 -2
  91. data/app/services/action_agent/agent_tool_roster.rb +19 -4
  92. data/app/services/action_agent/agent_toolbox.rb +215 -9
  93. data/app/services/action_agent/browser_live_ticket.rb +78 -0
  94. data/app/services/action_agent/browser_sidecar.rb +213 -0
  95. data/app/services/action_agent/browser_sign_in.rb +163 -0
  96. data/app/services/action_agent/browser_tool_redaction.rb +92 -0
  97. data/app/services/action_agent/claude_code_auth.rb +70 -4
  98. data/app/services/action_agent/codex_session_events.rb +37 -0
  99. data/app/services/action_agent/dashboard_assistant_service.rb +24 -10
  100. data/app/services/action_agent/draft_pull_request_publisher.rb +719 -0
  101. data/app/services/action_agent/evaluation_fix.rb +101 -0
  102. data/app/services/action_agent/evaluation_fix_comparison.rb +66 -0
  103. data/app/services/action_agent/evaluation_report_import.rb +84 -5
  104. data/app/services/action_agent/evaluation_run_cost.rb +609 -0
  105. data/app/services/action_agent/evaluation_runner_service.rb +29 -10
  106. data/app/services/action_agent/evaluation_standing.rb +152 -0
  107. data/app/services/action_agent/explorer_execution_service.rb +517 -0
  108. data/app/services/action_agent/github_client.rb +405 -64
  109. data/app/services/action_agent/live_updates.rb +31 -0
  110. data/app/services/action_agent/local_sandbox_backend.rb +1263 -102
  111. data/app/services/action_agent/local_sandbox_claude_login.rb +151 -0
  112. data/app/services/action_agent/mcp_client.rb +6 -2
  113. data/app/services/action_agent/mcp_recording_middleware.rb +104 -145
  114. data/app/services/action_agent/mcp_tool_dispatcher.rb +36 -13
  115. data/app/services/action_agent/mock_sandbox_backend.rb +3 -1
  116. data/app/services/action_agent/playwright_mcp_client.rb +7 -0
  117. data/app/services/action_agent/project_boot_fix.rb +309 -0
  118. data/app/services/action_agent/project_capabilities.rb +200 -0
  119. data/app/services/action_agent/project_install_pull_request.rb +225 -0
  120. data/app/services/action_agent/project_preflight.rb +178 -0
  121. data/app/services/action_agent/project_secret_discovery.rb +142 -0
  122. data/app/services/action_agent/project_setup.rb +429 -0
  123. data/app/services/action_agent/provider_credentials.rb +102 -0
  124. data/app/services/action_agent/recording_event_ingest.rb +181 -0
  125. data/app/services/action_agent/runtime_tool_roster.rb +52 -0
  126. data/app/services/action_agent/sample_data/conference_ticket.rb +516 -0
  127. data/app/services/action_agent/sandbox_boot_spec.rb +558 -0
  128. data/app/services/action_agent/sandbox_browser.rb +299 -0
  129. data/app/services/action_agent/sandbox_browser_driver.rb +85 -0
  130. data/app/services/action_agent/sandbox_credential_paths.rb +26 -0
  131. data/app/services/action_agent/sandbox_mail.rb +103 -0
  132. data/app/services/action_agent/sandbox_manifest.rb +166 -13
  133. data/app/services/action_agent/sandbox_orchestrator.rb +290 -23
  134. data/app/services/action_agent/sandbox_patch.rb +253 -0
  135. data/app/services/action_agent/scenario_catalog_import.rb +255 -0
  136. data/app/services/action_agent/scenario_catalog_repository_import.rb +81 -0
  137. data/app/services/action_agent/scenario_evaluation_runner.rb +121 -17
  138. data/app/services/action_agent/secret_scrubber.rb +33 -4
  139. data/app/services/action_agent/session_index.rb +270 -0
  140. data/app/services/action_agent/session_recording_service.rb +11 -0
  141. data/app/services/action_agent/session_timeline.rb +505 -0
  142. data/app/views/action_agent/github_app_manifests/created.html.erb +55 -0
  143. data/app/views/action_agent/session_player/show.html.erb +19 -0
  144. data/config/routes.rb +153 -2
  145. data/lib/action_agent/claude_login_process.rb +107 -0
  146. data/lib/action_agent/engine.rb +55 -5
  147. data/lib/action_agent/secret_requests.rb +80 -0
  148. data/lib/action_agent/version.rb +1 -1
  149. data/lib/action_agent.rb +513 -22
  150. data/lib/generators/action_agent/install_generator.rb +45 -5
  151. data/lib/generators/action_agent/templates/action_agent.rb.erb +161 -0
  152. data/lib/generators/action_agent/templates/add_code_session_runner.rb.erb +9 -0
  153. data/lib/generators/action_agent/templates/migrations/015_add_provider_key_scope.rb.erb +72 -0
  154. data/lib/generators/action_agent/templates/migrations/016_create_active_agent_input_requests.rb.erb +85 -0
  155. data/lib/generators/action_agent/templates/migrations/017_create_active_agent_recording_events.rb.erb +57 -0
  156. data/lib/generators/action_agent/templates/migrations/018_add_browser_to_active_agent_sandbox_sessions.rb.erb +25 -0
  157. data/lib/generators/action_agent/templates/migrations/019_create_active_agent_projects.rb.erb +79 -0
  158. data/lib/generators/action_agent/templates/migrations/020_add_github_app_installations.rb.erb +62 -0
  159. data/lib/generators/action_agent/templates/migrations/021_create_active_agent_explorations.rb.erb +63 -0
  160. data/lib/generators/action_agent/templates/migrations/023_create_active_agent_draft_pull_requests.rb.erb +75 -0
  161. data/lib/generators/action_agent/templates/migrations/024_add_kind_to_active_agent_project_secrets.rb.erb +14 -0
  162. data/lib/generators/action_agent/templates/migrations/025_create_active_agent_scenario_catalogs.rb.erb +110 -0
  163. data/lib/generators/action_agent/templates/migrations/026_add_evaluation_fixes_to_code_sessions.rb.erb +21 -0
  164. data/lib/tasks/action_agent.rake +33 -0
  165. data/lib/tasks/browser.rake +22 -0
  166. data/lib/tasks/sandbox.rake +1 -1
  167. data/lib/tasks/scenario_catalogs.rake +71 -0
  168. metadata +100 -2
@@ -190,7 +190,9 @@ module ActionAgent
190
190
  # The cost here is the agent's: what the sampled interactions cost to
191
191
  # operate. It is not what this run spent — scoring recorded generations
192
192
  # costs nothing until a judge is asked, and the judge's own spend is
193
- # recorded apart, under "_judge_usage" (see #judge_usage).
193
+ # recorded apart, under "_judge_usage" (see #judge_usage). It sums the
194
+ # generations that could be priced, and "priced" counts them, so a
195
+ # cohort with some unpriced generations reads as partial.
194
196
 
195
197
  def cohort_summaries(samples, per_sample_scores)
196
198
  samples
@@ -207,7 +209,8 @@ module ActionAgent
207
209
  input_tokens = samples.sum { |generation| generation.input_tokens.to_i }
208
210
  output_tokens = samples.sum { |generation| generation.output_tokens.to_i }
209
211
  costs = samples.filter_map do |generation|
210
- ModelPricing.estimate(model: generation.model, input_tokens: generation.input_tokens, output_tokens: generation.output_tokens)
212
+ ModelPricing.estimate(model: generation.model, provider: generation.provider.presence, input_tokens: generation.input_tokens,
213
+ output_tokens: generation.output_tokens)
211
214
  end
212
215
 
213
216
  {
@@ -217,7 +220,8 @@ module ActionAgent
217
220
  "avg_duration_ms" => durations_ms.any? ? (durations_ms.sum / durations_ms.size).round : nil,
218
221
  "input_tokens" => input_tokens,
219
222
  "output_tokens" => output_tokens,
220
- "cost" => costs.any? ? costs.sum.round(6) : nil
223
+ "cost" => costs.any? ? costs.sum.round(6) : nil,
224
+ "priced" => costs.size
221
225
  }
222
226
  end
223
227
 
@@ -254,10 +258,10 @@ module ActionAgent
254
258
 
255
259
  def record_judge_call(kind, response)
256
260
  usage = response.respond_to?(:usage) ? response.usage : nil
257
- input_tokens = usage.respond_to?(:input_tokens) ? usage.input_tokens.to_i : 0
261
+ input_tokens = judge_input_tokens(usage)
258
262
  output_tokens = usage.respond_to?(:output_tokens) ? usage.output_tokens.to_i : 0
259
263
  model = (response.respond_to?(:model) && response.model.presence) || @evaluation.judge_model.presence
260
- cost = ModelPricing.estimate(model: model, input_tokens: input_tokens, output_tokens: output_tokens)
264
+ cost = ModelPricing.estimate(model: model, provider: judge_provider, input_tokens: input_tokens, output_tokens: output_tokens)
261
265
 
262
266
  @judge_usage ||= { "calls" => 0, "input_tokens" => 0, "output_tokens" => 0, "cost" => nil, "model" => nil, "by_kind" => {} }
263
267
  @judge_usage["calls"] += 1
@@ -268,6 +272,22 @@ module ActionAgent
268
272
  @judge_usage["by_kind"][kind.to_s] = @judge_usage["by_kind"].fetch(kind.to_s, 0) + 1
269
273
  end
270
274
 
275
+ # Every token the judge was billed for reading. Anthropic reports the
276
+ # tokens read from or written to the prompt cache apart from
277
+ # `input_tokens`, so they are added; OpenAI's prompt count already holds
278
+ # its cached tokens. Priced at the input rate, which overstates a cache
279
+ # read: the figure is an upper bound.
280
+ def judge_input_tokens(usage)
281
+ return 0 unless usage.respond_to?(:input_tokens)
282
+
283
+ total = usage.input_tokens.to_i
284
+ return total unless judge_provider == :anthropic
285
+
286
+ total += usage.cached_tokens.to_i if usage.respond_to?(:cached_tokens)
287
+ total += usage.cache_creation_tokens.to_i if usage.respond_to?(:cache_creation_tokens)
288
+ total
289
+ end
290
+
271
291
  def sample_generations(model: nil)
272
292
  scope = @evaluation.agent.generations
273
293
  scope = scope.where(model: model) if model
@@ -574,13 +594,12 @@ module ActionAgent
574
594
 
575
595
  # The evaluated agent owner's credential for +name+ (Settings ->
576
596
  # Provider API Keys, or whatever the host app resolves); preferred over
577
- # the host's config/active_agent.yml credentials for the judge.
597
+ # the host's config/active_agent.yml credentials for the judge. Resolved
598
+ # with no actor: the judge scores for the organization, so it never runs
599
+ # on a member's personal key.
578
600
  def owner_provider_options(name)
579
601
  @owner_provider_options ||= {}
580
- @owner_provider_options[name.to_s] ||= begin
581
- from_host = ActionAgent.provider_credentials(owner, name.to_s)
582
- from_host.presence || ProviderKey.for_owner(owner).find_by(provider: name.to_s)&.generation_options || {}
583
- end
602
+ @owner_provider_options[name.to_s] ||= ProviderCredentials.resolve(owner: owner, actor: nil, provider: name).options
584
603
  end
585
604
 
586
605
  def owner
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionAgent
4
+ # Where an evaluation stands against the agent as it is now, so a pass
5
+ # rate on the Evaluations page describes the current agent rather than
6
+ # pooling every suite's last run, however old the code it scored.
7
+ #
8
+ # The evaluation's headline run is its newest complete run; a newer run
9
+ # still pending or failed shows beside it, never in its place. Its
10
+ # standing is one of:
11
+ #
12
+ # current — the headline run scored the agent's current version
13
+ # stale — it scored an earlier version, or a release the dashboard
14
+ # has not matched to the agent's current release
15
+ # unrecorded — the run recorded no version and the agent has no release
16
+ # versions at all, so nothing says which code it scored
17
+ # archived — the evaluation is archived (Evaluation#archive!)
18
+ # none — no complete run
19
+ #
20
+ # Only edits that change how a run goes count as a new version: what the
21
+ # model is given (the instructions, the action prompts, the tools, the MCP
22
+ # servers, the model config and the response format), and the tools whose
23
+ # calls wait for a person's approval, which pause a replay. A dashboard
24
+ # edit to the agent's appearance cuts an AgentVersion like any other edit,
25
+ # but the run before it scored the same agent and stays current.
26
+ class EvaluationStanding
27
+ STANDINGS = %w[current stale unrecorded archived none].freeze
28
+ VERSION_STATES = %w[current earlier unrecorded].freeze
29
+ # The configuration snapshot keys the model is given.
30
+ MODEL_FACING = %w[instructions action_prompts tools mcp_servers model_config response_format].freeze
31
+ # The configuration snapshot keys that change how a run goes.
32
+ SCORED = (MODEL_FACING + %w[approval_required_tools]).freeze
33
+
34
+ # Gives every evaluation its standing with one query per table rather
35
+ # than one per evaluation: the agents' latest versions and whether each
36
+ # has a release are read for the page at once.
37
+ def self.preload(evaluations)
38
+ evaluations = Array(evaluations)
39
+ agent_ids = evaluations.filter_map(&:agent_id).uniq
40
+ return if agent_ids.empty?
41
+
42
+ newest = AgentVersion.where(agent_id: agent_ids).group(:agent_id).maximum(:version_number)
43
+ latest = AgentVersion.where(agent_id: agent_ids).where(version_number: newest.values.uniq)
44
+ .select { |version| newest[version.agent_id] == version.version_number }.index_by(&:agent_id)
45
+ releases = AgentVersion.releases.where(agent_id: agent_ids).distinct.pluck(:agent_id).to_set
46
+
47
+ evaluations.each do |evaluation|
48
+ evaluation.standing_info = new(evaluation, latest_version: latest[evaluation.agent_id], has_releases: releases.include?(evaluation.agent_id))
49
+ end
50
+ end
51
+
52
+ attr_reader :evaluation
53
+
54
+ # @param headline_run [EvaluationRun, nil] the headline run when the
55
+ # caller already holds it (AgentScorecard selects one per evaluation)
56
+ def initialize(evaluation, latest_version: :unknown, has_releases: :unknown, headline_run: :unknown)
57
+ @evaluation = evaluation
58
+ @latest_version = latest_version
59
+ @has_releases = has_releases
60
+ @headline_run = headline_run
61
+ end
62
+
63
+ def headline_run
64
+ return @headline_run unless @headline_run == :unknown
65
+
66
+ @headline_run = evaluation.headline_run
67
+ end
68
+
69
+ # The same standing read against +run+ as the headline run.
70
+ def with_headline(run)
71
+ self.class.new(evaluation, latest_version: latest_version, has_releases: has_releases?, headline_run: run)
72
+ end
73
+
74
+ def standing
75
+ return "archived" if evaluation.archived?
76
+ return "none" unless headline_run
77
+
78
+ case version_state(headline_run)
79
+ when "current" then "current"
80
+ when "unrecorded" then has_releases? ? "stale" : "unrecorded"
81
+ else "stale"
82
+ end
83
+ end
84
+
85
+ # Whether +run+ scored the agent's current version: "current",
86
+ # "earlier", or "unrecorded" when it recorded no version.
87
+ def version_state(run)
88
+ version = run&.agent_version
89
+ return "unrecorded" unless version
90
+
91
+ # The agent's last recorded deploy is the truth about the code that
92
+ # runs: a release version matching it is current whatever the
93
+ # dashboard edited since, and one that does not is earlier even when
94
+ # it was recorded last (a report of an older deploy published late).
95
+ if version.release? && (deployed = evaluation.agent&.release_digest.presence)
96
+ return deployed == version.release_digest ? "current" : "earlier"
97
+ end
98
+
99
+ latest = latest_version
100
+ return "current" if latest.nil? || latest.id == version.id
101
+ return "current" if scored_configuration(version) == scored_configuration(latest)
102
+
103
+ "earlier"
104
+ end
105
+
106
+ # Passes per model label of the headline run, `{ label => { passed, total } }`,
107
+ # from its recorded summaries; empty without them.
108
+ def per_model
109
+ run = headline_run
110
+ return {} unless run
111
+
112
+ summaries = run.scores.is_a?(Hash) ? (run.scores["_models"].presence || run.scores["_cohorts"]) : nil
113
+ return {} unless summaries.is_a?(Hash)
114
+
115
+ summaries.filter_map do |label, stats|
116
+ next unless stats.is_a?(Hash)
117
+
118
+ total = (stats["scenarios"] || stats["samples"]).to_i
119
+ [ label, { passed: stats["passed"].to_i, total: total } ]
120
+ end.to_h
121
+ end
122
+
123
+ # What the index and the scorecards pool: a current or unrecorded
124
+ # headline run; a stale or archived evaluation is left out and said so.
125
+ def counted?
126
+ %w[current unrecorded].include?(standing)
127
+ end
128
+
129
+ private
130
+
131
+ def latest_version
132
+ return @latest_version unless @latest_version == :unknown
133
+
134
+ @latest_version = evaluation.agent&.latest_version
135
+ end
136
+
137
+ def has_releases?
138
+ return @has_releases unless @has_releases == :unknown
139
+
140
+ @has_releases = evaluation.agent&.agent_versions&.releases&.exists? || false
141
+ end
142
+
143
+ # A snapshot taken before an agent had an approval list reads as an empty
144
+ # one, so the list's arrival alone cuts no new version.
145
+ def scored_configuration(version)
146
+ snapshot = version.configuration_snapshot.to_h.stringify_keys
147
+ configuration = SCORED.to_h { |key| [ key, snapshot[key] ] }
148
+ configuration["approval_required_tools"] = Array(configuration["approval_required_tools"])
149
+ configuration
150
+ end
151
+ end
152
+ end