actionagent 1.8.1 → 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 (159) 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 +53 -6
  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 +9 -4
  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 +140 -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 +623 -18
  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 +26 -2
  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 +39 -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 +16 -4
  64. data/app/models/action_agent/draft_pull_request.rb +131 -0
  65. data/app/models/action_agent/evaluation.rb +84 -0
  66. data/app/models/action_agent/exploration.rb +847 -0
  67. data/app/models/action_agent/github_installation.rb +159 -0
  68. data/app/models/action_agent/input_request.rb +291 -0
  69. data/app/models/action_agent/project.rb +789 -0
  70. data/app/models/action_agent/project_secret.rb +339 -0
  71. data/app/models/action_agent/provider_key.rb +200 -3
  72. data/app/models/action_agent/recording_event.rb +145 -0
  73. data/app/models/action_agent/recording_snapshot.rb +7 -7
  74. data/app/models/action_agent/sandbox_session.rb +297 -14
  75. data/app/models/action_agent/scenario_catalog.rb +172 -0
  76. data/app/models/action_agent/scenario_product.rb +57 -0
  77. data/app/models/action_agent/scenario_set.rb +138 -0
  78. data/app/models/action_agent/session_recording.rb +121 -14
  79. data/app/models/action_agent/telemetry_trace.rb +74 -19
  80. data/app/models/concerns/action_agent/ownable.rb +84 -13
  81. data/app/serializers/action_agent/input_request_serializer.rb +47 -0
  82. data/app/serializers/action_agent/scenario_catalog_serializer.rb +23 -0
  83. data/app/serializers/action_agent/session_summary_serializer.rb +146 -0
  84. data/app/services/action_agent/agent_execution_service.rb +450 -72
  85. data/app/services/action_agent/agent_sync.rb +5 -2
  86. data/app/services/action_agent/agent_tool_roster.rb +19 -4
  87. data/app/services/action_agent/agent_toolbox.rb +215 -9
  88. data/app/services/action_agent/browser_live_ticket.rb +78 -0
  89. data/app/services/action_agent/browser_sidecar.rb +213 -0
  90. data/app/services/action_agent/browser_sign_in.rb +163 -0
  91. data/app/services/action_agent/browser_tool_redaction.rb +92 -0
  92. data/app/services/action_agent/claude_code_auth.rb +70 -4
  93. data/app/services/action_agent/dashboard_assistant_service.rb +24 -10
  94. data/app/services/action_agent/draft_pull_request_publisher.rb +719 -0
  95. data/app/services/action_agent/evaluation_fix.rb +101 -0
  96. data/app/services/action_agent/evaluation_fix_comparison.rb +66 -0
  97. data/app/services/action_agent/evaluation_runner_service.rb +4 -5
  98. data/app/services/action_agent/evaluation_standing.rb +15 -8
  99. data/app/services/action_agent/explorer_execution_service.rb +517 -0
  100. data/app/services/action_agent/github_client.rb +405 -64
  101. data/app/services/action_agent/live_updates.rb +31 -0
  102. data/app/services/action_agent/local_sandbox_backend.rb +1218 -83
  103. data/app/services/action_agent/local_sandbox_claude_login.rb +151 -0
  104. data/app/services/action_agent/mcp_client.rb +6 -2
  105. data/app/services/action_agent/mcp_recording_middleware.rb +104 -145
  106. data/app/services/action_agent/mcp_tool_dispatcher.rb +36 -13
  107. data/app/services/action_agent/mock_sandbox_backend.rb +3 -1
  108. data/app/services/action_agent/playwright_mcp_client.rb +7 -0
  109. data/app/services/action_agent/project_boot_fix.rb +309 -0
  110. data/app/services/action_agent/project_capabilities.rb +200 -0
  111. data/app/services/action_agent/project_install_pull_request.rb +225 -0
  112. data/app/services/action_agent/project_preflight.rb +178 -0
  113. data/app/services/action_agent/project_secret_discovery.rb +142 -0
  114. data/app/services/action_agent/project_setup.rb +429 -0
  115. data/app/services/action_agent/provider_credentials.rb +102 -0
  116. data/app/services/action_agent/recording_event_ingest.rb +181 -0
  117. data/app/services/action_agent/runtime_tool_roster.rb +52 -0
  118. data/app/services/action_agent/sample_data/conference_ticket.rb +516 -0
  119. data/app/services/action_agent/sandbox_boot_spec.rb +558 -0
  120. data/app/services/action_agent/sandbox_browser.rb +299 -0
  121. data/app/services/action_agent/sandbox_browser_driver.rb +85 -0
  122. data/app/services/action_agent/sandbox_credential_paths.rb +26 -0
  123. data/app/services/action_agent/sandbox_mail.rb +103 -0
  124. data/app/services/action_agent/sandbox_manifest.rb +166 -13
  125. data/app/services/action_agent/sandbox_orchestrator.rb +276 -22
  126. data/app/services/action_agent/sandbox_patch.rb +253 -0
  127. data/app/services/action_agent/scenario_catalog_import.rb +255 -0
  128. data/app/services/action_agent/scenario_catalog_repository_import.rb +81 -0
  129. data/app/services/action_agent/scenario_evaluation_runner.rb +119 -16
  130. data/app/services/action_agent/secret_scrubber.rb +33 -4
  131. data/app/services/action_agent/session_index.rb +270 -0
  132. data/app/services/action_agent/session_recording_service.rb +11 -0
  133. data/app/services/action_agent/session_timeline.rb +505 -0
  134. data/app/views/action_agent/github_app_manifests/created.html.erb +55 -0
  135. data/app/views/action_agent/session_player/show.html.erb +19 -0
  136. data/config/routes.rb +152 -1
  137. data/lib/action_agent/claude_login_process.rb +107 -0
  138. data/lib/action_agent/engine.rb +55 -5
  139. data/lib/action_agent/secret_requests.rb +80 -0
  140. data/lib/action_agent/version.rb +1 -1
  141. data/lib/action_agent.rb +507 -22
  142. data/lib/generators/action_agent/install_generator.rb +37 -0
  143. data/lib/generators/action_agent/templates/action_agent.rb.erb +158 -0
  144. data/lib/generators/action_agent/templates/migrations/015_add_provider_key_scope.rb.erb +72 -0
  145. data/lib/generators/action_agent/templates/migrations/016_create_active_agent_input_requests.rb.erb +85 -0
  146. data/lib/generators/action_agent/templates/migrations/017_create_active_agent_recording_events.rb.erb +57 -0
  147. data/lib/generators/action_agent/templates/migrations/018_add_browser_to_active_agent_sandbox_sessions.rb.erb +25 -0
  148. data/lib/generators/action_agent/templates/migrations/019_create_active_agent_projects.rb.erb +79 -0
  149. data/lib/generators/action_agent/templates/migrations/020_add_github_app_installations.rb.erb +62 -0
  150. data/lib/generators/action_agent/templates/migrations/021_create_active_agent_explorations.rb.erb +63 -0
  151. data/lib/generators/action_agent/templates/migrations/023_create_active_agent_draft_pull_requests.rb.erb +75 -0
  152. data/lib/generators/action_agent/templates/migrations/024_add_kind_to_active_agent_project_secrets.rb.erb +14 -0
  153. data/lib/generators/action_agent/templates/migrations/025_create_active_agent_scenario_catalogs.rb.erb +110 -0
  154. data/lib/generators/action_agent/templates/migrations/026_add_evaluation_fixes_to_code_sessions.rb.erb +21 -0
  155. data/lib/tasks/action_agent.rake +33 -0
  156. data/lib/tasks/browser.rake +22 -0
  157. data/lib/tasks/sandbox.rake +1 -1
  158. data/lib/tasks/scenario_catalogs.rake +71 -0
  159. metadata +96 -2
data/lib/action_agent.rb CHANGED
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionAgent
4
+ # Raised as the engine loads for a configuration it cannot run with.
5
+ class ConfigurationError < StandardError; end
6
+
4
7
  class << self
5
8
  # Table name prefix for the engine's models. The engine's own
6
9
  # migrations create `active_agent_*` tables, so the default matches.
@@ -61,6 +64,7 @@ require "solid_agent"
61
64
  require "action_agent/version"
62
65
  require "action_agent/engine"
63
66
  require "action_agent/compatibility"
67
+ require "action_agent/secret_requests"
64
68
 
65
69
  # Dashboard engine for visualizing telemetry data and managing agents.
66
70
  #
@@ -105,7 +109,44 @@ require "action_agent/compatibility"
105
109
  #
106
110
  module ActionAgent
107
111
  # What ActionAgent.claude_code_auth may be set to.
108
- CLAUDE_CODE_AUTH_MODES = %i[api_key local_login].freeze
112
+ CLAUDE_CODE_AUTH_MODES = %i[api_key local_login sandbox_login].freeze
113
+
114
+ # What ActionAgent.provider_key_scope may be set to.
115
+ PROVIDER_KEY_SCOPES = %i[organization personal_override].freeze
116
+
117
+ # The privileged actions ActionAgent.permission_checker is asked about:
118
+ #
119
+ # :manage_credentials store, test or delete an organization provider
120
+ # credential, hand the organization's to a
121
+ # project's code, or store or test a member's
122
+ # personal Ollama host (the subject is that
123
+ # personal key); a member's other personal keys
124
+ # need none
125
+ # :manage_github connect, disconnect, or choose the repositories of
126
+ # the GitHub connection or a GitHub App
127
+ # installation; link or unlink an installation;
128
+ # create a GitHub App from a manifest; read a
129
+ # repository the connection has not selected
130
+ # :manage_api_keys create or revoke a dashboard API key
131
+ # :publish_pull_request open a pull request from a sandbox's changes
132
+ # :answer_input_request answer or decline a run's request for input
133
+ # :manage_project_secrets set or remove a project's secrets, change the ref
134
+ # they are handed to, or delete a project that has
135
+ # them
136
+ # :take_over_browser drive a run's browser by hand
137
+ # :manage_recordings delete a session recording
138
+ # :replace_scenarios replace or merge an evaluation's scenarios
139
+ PERMISSION_ACTIONS = %i[
140
+ manage_credentials
141
+ manage_github
142
+ manage_api_keys
143
+ publish_pull_request
144
+ answer_input_request
145
+ manage_project_secrets
146
+ take_over_browser
147
+ manage_recordings
148
+ replace_scenarios
149
+ ].freeze
109
150
 
110
151
  class << self
111
152
  # Deprecation warnings for this gem, routed through Rails' machinery so a
@@ -159,8 +200,9 @@ module ActionAgent
159
200
  # config.agent_actor_resolver = ->(controller) { controller.current_user }
160
201
  #
161
202
  # Unset means the dashboard's signed-in user, and, for the MCP endpoint,
162
- # the API key's owner — the identity that authenticated the call. A host
163
- # whose keys are issued per end user overrides this to return that user.
203
+ # the user who created the API key, else the key's owner — the identity
204
+ # that authenticated the call. A host whose keys are issued per end user
205
+ # overrides this to return that user.
164
206
  #
165
207
  # Returning nil runs the agent unattributed, which a correctly written
166
208
  # host scope reads as "no access". That is the safe direction, and it is
@@ -209,9 +251,80 @@ module ActionAgent
209
251
  # @return [Hash, nil]
210
252
  attr_accessor :sandbox_limits
211
253
 
212
- # Storage service for screenshots/snapshots
213
- # @return [Object, nil] Object responding to #signed_url_for and #fetch_snapshot
214
- attr_accessor :storage_service
254
+ # Caps on the events a browser may post to a session recording, merged
255
+ # over RecordingEvent::DEFAULT_LIMITS. Keys:
256
+ #
257
+ # batch_events events in one batch
258
+ # batch_bytes bytes in one batch's request body
259
+ # recording_events events stored on one recording
260
+ # recording_bytes bytes of event JSON stored on one recording
261
+ #
262
+ # A batch over any of them is refused and counted on the recording.
263
+ # @return [Hash, nil]
264
+ attr_accessor :recording_limits
265
+
266
+ # Active Storage in the engine. The engine attaches files in a few
267
+ # places: the files uploaded with a run (AgentRun attachments), a session
268
+ # recording's snapshots and oversized event payloads, and a scenario
269
+ # catalog's YAML. Three settings:
270
+ #
271
+ # :auto (the default) use Active Storage when the host app has it
272
+ # loaded and its tables migrated; otherwise those features keep
273
+ # their metadata-only form (runs refuse files, recordings keep
274
+ # events inline, catalogs live in the database alone).
275
+ # true require it: the first engine model to load raises
276
+ # ConfigurationError when Active Storage is not loaded, so a
277
+ # host that depends on attachments finds out at boot.
278
+ # false never attach, even when the host has Active Storage.
279
+ #
280
+ # Decided as the models load, so set it in an initializer.
281
+ # @return [Symbol, Boolean]
282
+ attr_accessor :active_storage
283
+
284
+ # The Active Storage service the engine's attachments are written to, by
285
+ # its name in config/storage.yml (`:recordings`, `:gcs`). nil, the
286
+ # default, uses the host's default service. Rails checks the name as the
287
+ # models load, so a misspelling fails at boot rather than on the first
288
+ # upload.
289
+ # @return [Symbol, String, nil]
290
+ attr_accessor :active_storage_service
291
+
292
+ # Whether the engine's models apply their Active Storage macros
293
+ # (has_one_attached and friends), per +active_storage+.
294
+ # @raise [ConfigurationError] when the option is true and Active Storage
295
+ # is not loaded
296
+ # @return [Boolean]
297
+ def active_storage_macros?(loaded: defined?(::ActiveStorage) ? true : false)
298
+ case active_storage
299
+ when true
300
+ return true if loaded
301
+
302
+ raise ConfigurationError,
303
+ "ActionAgent.active_storage is true but Active Storage is not loaded: add `require \"active_storage/engine\"` " \
304
+ "to config/application.rb and run `bin/rails active_storage:install`, or set the option to :auto or false"
305
+ when false, nil then false
306
+ else loaded
307
+ end
308
+ end
309
+
310
+ # Whether an attachment can be written right now: the macros applied and
311
+ # the blobs table migrated. Never raises, so a host that skipped
312
+ # `rails active_storage:install` still runs agents; it just cannot
313
+ # attach files to them.
314
+ # @return [Boolean]
315
+ def active_storage_available?
316
+ return false unless active_storage_macros?
317
+
318
+ ::ActiveStorage::Blob.table_exists?
319
+ rescue StandardError
320
+ false
321
+ end
322
+
323
+ # The options the engine's attachment macros are declared with.
324
+ # @return [Hash]
325
+ def attachment_options
326
+ active_storage_service.present? ? { service: active_storage_service.to_sym } : {}
327
+ end
215
328
 
216
329
  # Bearer token required in single-tenant mode by the endpoints other
217
330
  # applications post to: trace ingest (<mount>/api/traces) and published
@@ -281,6 +394,11 @@ module ActionAgent
281
394
  # :trace_ingest — a POST to <mount>/api/traces; HTTP 429
282
395
  # :evaluation_report — a report <mount>/api/evaluation_reports would
283
396
  # store (never an identical retry); HTTP 429
397
+ # :project — creating a project; HTTP 402, and usage is
398
+ # recorded under the same kind once it exists
399
+ # :browser_minutes — starting a sandbox's browser; HTTP 402
400
+ # :exploration — starting the explorer on a project; HTTP 402,
401
+ # and usage is recorded once for each start
284
402
  #
285
403
  # The owner of an ingest kind is the tenant the key resolved to, nil on a
286
404
  # single-tenant install.
@@ -289,17 +407,111 @@ module ActionAgent
289
407
  # @return [Proc, nil]
290
408
  attr_accessor :quota_checker
291
409
 
410
+ # Decides whether the signed-in user may perform a privileged action,
411
+ # called as (user, action, subject):
412
+ #
413
+ # user the signed-in user (the dashboard's current_user), or nil
414
+ # action one of PERMISSION_ACTIONS
415
+ # subject the record the action applies to, unsaved when the action
416
+ # creates it, with its owner columns set
417
+ #
418
+ # A truthy answer allows and false denies. {.permitted?} describes how a
419
+ # nil answer, an exception and a missing user are treated. A denied API
420
+ # request gets HTTP 403, and a denied GitHub connect or callback returns
421
+ # to Settings.
422
+ #
423
+ # For :answer_input_request the subject is the InputRequest, whose
424
+ # `requested_by_id` is the id of the user the paused run acts for.
425
+ #
426
+ # config.permission_checker = ->(user, action, subject) {
427
+ # user.present? && (user.admin? ||
428
+ # action == :answer_input_request && subject.requested_by_id.in?([ nil, user.id ]))
429
+ # }
430
+ #
431
+ # Unset means everyone who can reach the dashboard may do everything,
432
+ # which is what a single-user install wants. In multi-tenant mode it means
433
+ # every member of a tenant may do everything except answer a request for
434
+ # input another member's run raised (InputRequest#answerable_by?), and the
435
+ # engine logs a warning at boot (see {.warn_about_unchecked_permissions}).
436
+ # @return [Proc, nil]
437
+ attr_accessor :permission_checker
438
+
292
439
  # Resolves LLM provider credentials for a run. Receives
293
440
  # (owner, provider_name) and returns a Hash merged into the agent's
294
441
  # generation options (e.g. { access_token: "sk-..." } or
295
442
  # { host: "http://localhost:11434" }), or nil to fall back to the
296
- # host app's config/active_agent.yml.
443
+ # dashboard's stored organization key and then the host app's
444
+ # config/active_agent.yml.
445
+ #
446
+ # A resolver that declares an +actor:+ keyword (or accepts any keyword
447
+ # with **) is also told who is acting: the user who started the run, the
448
+ # signed-in user for the dashboard assistant and the model pickers, nil
449
+ # for the evaluation judge and a run nobody started:
450
+ #
451
+ # config.provider_credentials_resolver = ->(owner, provider, actor: nil) {
452
+ # owner.provider_key_for(provider)&.generation_options
453
+ # }
454
+ #
455
+ # A personal key (see provider_key_scope) is tried before the resolver,
456
+ # so a resolver that reads the dashboard's provider_keys table by
457
+ # provider alone has to keep to organization rows
458
+ # (ActionAgent::ProviderKey.for_owner does).
297
459
  #
298
460
  # Unset means config/active_agent.yml is the only source, which is what
299
- # a self-hosted install wants.
461
+ # a self-hosted install wants. See {.provider_credentials} for how an
462
+ # exception is treated.
300
463
  # @return [Proc, nil]
301
464
  attr_accessor :provider_credentials_resolver
302
465
 
466
+ # Whether a member's own provider key may stand in for the
467
+ # organization's. One of PROVIDER_KEY_SCOPES:
468
+ #
469
+ # :organization (default) runs use the organization's key, the
470
+ # host resolver's, or config/active_agent.yml
471
+ # :personal_override a member may also save a personal key per
472
+ # provider in Settings, and the runs that member
473
+ # starts, the dashboard assistant and the model
474
+ # pickers use it before the organization's
475
+ #
476
+ # A personal key sends the organization's agent traffic to the member's
477
+ # own provider account, so an install opts in. Without an account_class
478
+ # there are no personal keys and the setting has no effect. The
479
+ # evaluation judge, sandboxes, and Claude Code and Codex connections
480
+ # always use organization keys. A personal Ollama host decides where the
481
+ # server sends requests, so storing one asks permission_checker for
482
+ # :manage_credentials.
483
+ # @return [Symbol]
484
+ attr_reader :provider_key_scope
485
+
486
+ def provider_key_scope=(value)
487
+ scope = value.nil? ? :organization : value.to_sym
488
+ unless PROVIDER_KEY_SCOPES.include?(scope)
489
+ raise ArgumentError, "provider_key_scope must be one of #{PROVIDER_KEY_SCOPES.join(', ')}, got #{value.inspect}"
490
+ end
491
+
492
+ @provider_key_scope = scope
493
+ end
494
+
495
+ # Lists the members of an owner for the Organization view's Team Members
496
+ # table, answering GET <mount>/api/members. Receives (owner) and returns
497
+ # an Array of Hashes with :id, :name, :email and :role; any other key is
498
+ # dropped before it reaches the browser.
499
+ #
500
+ # config.members_resolver = ->(account) {
501
+ # account.memberships.includes(:user).map { |m| { id: m.user.id, name: m.user.name, email: m.user.email, role: m.role } }
502
+ # }
503
+ #
504
+ # Unset, or when it raises (logged), the table lists the signed-in user
505
+ # alone.
506
+ # @return [Proc, nil]
507
+ attr_accessor :members_resolver
508
+
509
+ # Where the Organization view's "+ Invite Member" button links: the
510
+ # host app's own invitation page. Unset, the button is not shown; the
511
+ # engine has no invitations of its own.
512
+ # @return [String, nil]
513
+ attr_accessor :member_invite_url
514
+
303
515
  # Sandbox backends contributed by the host app, as
304
516
  # { "cloud_run" => "CloudRunService" }. The engine ships only :mock;
305
517
  # every real backend (Docker/Incus, Cloud Run, Kubernetes) lives in the
@@ -328,6 +540,34 @@ module ActionAgent
328
540
  # @return [Integer]
329
541
  attr_accessor :local_sandbox_boot_timeout
330
542
 
543
+ # A checkout of the browser sidecar (the repository's browser-sidecar/
544
+ # directory) for the :local backend to run instead of the package
545
+ # `bin/rails action_agent:browser:install` installs. For working on the
546
+ # sidecar itself: a checkout's version is not checked against the
547
+ # engine's. Unset, the installed package runs.
548
+ # @return [String, Pathname, nil]
549
+ attr_accessor :browser_sidecar_path
550
+
551
+ # The Node.js and npm executables the browser sidecar is installed and
552
+ # run with.
553
+ # @return [String]
554
+ attr_accessor :node_command, :npm_command
555
+
556
+ # How long the :local backend waits for a browser to start, in seconds.
557
+ # @return [Integer]
558
+ attr_accessor :browser_start_timeout
559
+
560
+ # Origins, besides the one a browser is started from, whose dashboard
561
+ # pages may open its live view, such as "http://127.0.0.1:3000" when the
562
+ # dashboard is also reached that way. A live view's WebSocket refuses
563
+ # every other Origin.
564
+ # @return [Array<String>]
565
+ attr_writer :browser_live_origins
566
+
567
+ def browser_live_origins
568
+ Array(@browser_live_origins).map(&:to_s)
569
+ end
570
+
331
571
  # The Claude Code executable a sandbox backend runs headless sessions
332
572
  # with. The :local backend runs it on the dashboard's machine.
333
573
  # @return [String]
@@ -368,18 +608,26 @@ module ActionAgent
368
608
  # or stores it; it only asks `claude auth status` whether there is one.
369
609
  # That login is the dashboard user's own, so this works with the :local
370
610
  # sandbox backend only, and other backends refuse Claude Code sessions.
371
- # @return [Symbol] :api_key or :local_login
611
+ # :sandbox_login lets a signed-in user authorize the unmodified CLI in
612
+ # one sandbox. Its credentials remain in that sandbox's private CLI
613
+ # config, outside the checkout, until stop/expiry/disconnect.
614
+ # @return [Symbol] :api_key, :local_login or :sandbox_login
372
615
  attr_reader :claude_code_auth
373
616
 
374
617
  def claude_code_auth=(value)
375
618
  mode = value.to_s.to_sym
376
619
  unless CLAUDE_CODE_AUTH_MODES.include?(mode)
377
- raise ArgumentError, "ActionAgent.claude_code_auth must be :api_key or :local_login, not #{value.inspect}"
620
+ raise ArgumentError, "ActionAgent.claude_code_auth must be :api_key, :local_login or :sandbox_login, not #{value.inspect}"
378
621
  end
379
622
 
380
623
  @claude_code_auth = mode
381
624
  end
382
625
 
626
+ # Hosted adapters must opt in after the operator has reviewed Anthropic's
627
+ # hosting terms. :local supports sandbox-scoped login without this flag.
628
+ attr_accessor :claude_code_hosted_login_enabled
629
+ attr_accessor :claude_code_login_timeout
630
+
383
631
  # Whether the dashboard may execute agents against real providers.
384
632
  # Disable to run the dashboard as a read-only observability surface.
385
633
  # @return [Boolean]
@@ -411,6 +659,24 @@ module ActionAgent
411
659
  # @return [Boolean, nil]
412
660
  attr_accessor :assistant_enabled
413
661
 
662
+ # Whether the dashboard records a person's view of the Run Agent
663
+ # workbench, as the browser lane of the conversation's replay. On unless
664
+ # set to false.
665
+ #
666
+ # While the workbench has a conversation open, the page is recorded with
667
+ # rrweb, each visit into a recording of its own (`source: "dashboard"`),
668
+ # and nowhere else in the dashboard. Field values are masked. Elements
669
+ # marked `data-aa-secret` (credential fields and displays), the CSRF
670
+ # token and hidden inputs are recorded as blank boxes. Each batch is
671
+ # scrubbed of the owner's stored credentials before it is kept. What the
672
+ # page shows otherwise, the conversation included, is recorded as shown.
673
+ #
674
+ # Off, the dashboard loads no recorder and refuses the batches a
675
+ # dashboard session posts. Conversations still replay from their
676
+ # messages, model calls and tool calls.
677
+ # @return [Boolean]
678
+ attr_accessor :capture_dashboard_sessions
679
+
414
680
  # Resolves a host application's runner for one scenario evaluation.
415
681
  # Return nil for the engine's normal Agent#test_execute path, or a callable
416
682
  # accepting evaluation:, owner:, scenarios:, models:, on_result: and
@@ -419,6 +685,14 @@ module ActionAgent
419
685
  # @return [Proc, nil]
420
686
  attr_accessor :scenario_evaluation_adapter_resolver
421
687
 
688
+ # How many answerable candidates an exploration's review pre-selects for
689
+ # acceptance: an Integer, or a Proc called with (owner) that returns one.
690
+ # A platform can cap a free plan's suite this way. Unset, or nil from the
691
+ # Proc, pre-selects every answerable candidate. See
692
+ # {.exploration_preselect_limit_for}.
693
+ # @return [Integer, Proc, nil]
694
+ attr_accessor :exploration_preselect_limit
695
+
422
696
  # Where the dashboard's upgrade CTAs should send people. Unset in a
423
697
  # self-hosted install, where there is nothing to upgrade, and the CTAs
424
698
  # say so instead of linking nowhere.
@@ -458,9 +732,17 @@ module ActionAgent
458
732
 
459
733
  # Called after the dashboard performs a metered action, as
460
734
  # (owner, kind) — the counterpart to quota_checker, for host apps that
461
- # track usage against a plan. The kinds are :execution, for each agent
462
- # run, and :evaluation_report, for each report the collector stores; an
463
- # identical retry is not counted again. Unset means nothing is counted.
735
+ # track usage against a plan. The kinds:
736
+ #
737
+ # :execution — each agent run
738
+ # :evaluation_report — each report the collector stores; an identical
739
+ # retry is not counted again
740
+ # :browser_minutes — each browser that stopped, with the minutes it
741
+ # ran, rounded up, as a third argument
742
+ #
743
+ # A recorder that takes a third argument receives the quantity, and nil
744
+ # for a kind that has none when the argument is required; one that takes
745
+ # two is called as (owner, kind). Unset means nothing is counted.
464
746
  # @return [Proc, nil]
465
747
  attr_accessor :usage_recorder
466
748
 
@@ -484,6 +766,13 @@ module ActionAgent
484
766
  # @return [ActiveSupport::Duration, Proc, nil]
485
767
  attr_accessor :trace_retention
486
768
 
769
+ # How long a run's request for input waits for an answer. Past it, the
770
+ # request expires and its run fails, once an answer, the request list,
771
+ # the run's page or InputRequestExpiryJob reaches it. One day by default;
772
+ # nil lets a request wait until it is answered or its run is cancelled.
773
+ # @return [ActiveSupport::Duration, nil]
774
+ attr_accessor :input_request_ttl
775
+
487
776
  # Whether API keys and provider credentials are encrypted at rest with
488
777
  # Active Record Encryption. On by default, which requires the host app
489
778
  # to have run `rails db:encryption:init`. Turning it off stores those
@@ -505,6 +794,23 @@ module ActionAgent
505
794
  # @return [String]
506
795
  attr_accessor :github_oauth_scopes
507
796
 
797
+ # The GitHub App checkout sandboxes get repository access through
798
+ # (Settings -> Integrations -> Install the GitHub App). Each falls back to
799
+ # the matching GITHUB_APP_* variable (GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY,
800
+ # GITHUB_APP_SLUG, GITHUB_APP_CLIENT_ID, GITHUB_APP_CLIENT_SECRET), and the
801
+ # dashboard offers no installation until all five are present. Register
802
+ # the App with "Request user authorization (OAuth) during installation"
803
+ # and "Redirect on update" turned on and
804
+ # <mount>/api/github_installations/callback as its callback URL, or create
805
+ # it from Settings with the manifest flow.
806
+ #
807
+ # The private key is the PEM GitHub generated for the App. A key whose
808
+ # line breaks were written as "\n" (one line in an env file) is read with
809
+ # real line breaks.
810
+ # @return [String, nil]
811
+ attr_writer :github_app_id, :github_app_private_key, :github_app_slug,
812
+ :github_app_client_id, :github_app_client_secret
813
+
508
814
  # MCP servers the host app itself serves or connects, appended to the
509
815
  # built-in catalog (MCPCatalog) so the MCP Services view lists them and
510
816
  # telemetry traffic attributes to them. Each entry is a hash shaped like
@@ -586,6 +892,36 @@ module ActionAgent
586
892
  github_client_id.present? && github_client_secret.present?
587
893
  end
588
894
 
895
+ def github_app_id
896
+ (@github_app_id.presence || ENV["GITHUB_APP_ID"].presence)&.to_s
897
+ end
898
+
899
+ def github_app_private_key
900
+ key = @github_app_private_key.presence || ENV["GITHUB_APP_PRIVATE_KEY"].presence
901
+ return nil if key.nil?
902
+
903
+ key.include?("\n") ? key : key.gsub("\\n", "\n")
904
+ end
905
+
906
+ def github_app_slug
907
+ @github_app_slug.presence || ENV["GITHUB_APP_SLUG"].presence
908
+ end
909
+
910
+ def github_app_client_id
911
+ @github_app_client_id.presence || ENV["GITHUB_APP_CLIENT_ID"].presence
912
+ end
913
+
914
+ def github_app_client_secret
915
+ @github_app_client_secret.presence || ENV["GITHUB_APP_CLIENT_SECRET"].presence
916
+ end
917
+
918
+ # Whether the GitHub App installation flow can run on this install: every
919
+ # github_app_* setting is present.
920
+ # @return [Boolean]
921
+ def github_app_configured?
922
+ [ github_app_id, github_app_private_key, github_app_slug, github_app_client_id, github_app_client_secret ].all?(&:present?)
923
+ end
924
+
589
925
  def multi_tenant?
590
926
  @multi_tenant == true
591
927
  end
@@ -612,6 +948,13 @@ module ActionAgent
612
948
  @execution_enabled != false
613
949
  end
614
950
 
951
+ # Returns whether the dashboard records the Run Agent workbench.
952
+ #
953
+ # @return [Boolean]
954
+ def capture_dashboard_sessions?
955
+ @capture_dashboard_sessions != false
956
+ end
957
+
615
958
  # Returns whether the dashboard assistant is available. Unconfigured, it
616
959
  # follows the environment: development and test yes, everywhere else no.
617
960
  #
@@ -635,10 +978,13 @@ module ActionAgent
635
978
  Pathname.new(@local_sandbox_root.presence || Rails.root.join("tmp", "action_agent", "sandboxes"))
636
979
  end
637
980
 
638
- # Tells the host app that +owner+ performed +kind+. Never raises: a
639
- # bookkeeping failure must not fail the action that was already taken.
640
- def record_usage(owner, kind)
641
- usage_recorder&.call(owner, kind)
981
+ # Tells the host app that +owner+ performed +kind+, +quantity+ times when
982
+ # given (see usage_recorder). Never raises: a bookkeeping failure must not
983
+ # fail the action that was already taken.
984
+ def record_usage(owner, kind, quantity = nil)
985
+ return nil if usage_recorder.nil?
986
+
987
+ usage_recorder.call(*usage_arguments(usage_recorder, owner, kind, quantity))
642
988
  rescue StandardError => e
643
989
  Rails.logger.warn("[ActionAgent] usage recording failed: #{e.message}")
644
990
  nil
@@ -657,6 +1003,21 @@ module ActionAgent
657
1003
  UNLIMITED_USAGE.dup
658
1004
  end
659
1005
 
1006
+ # The exploration_preselect_limit for +owner+, or nil for no limit. A
1007
+ # value that is not a non-negative Integer, and a Proc that raises, mean
1008
+ # no limit: the setting only trims a default selection, and the
1009
+ # reviewer can still select every candidate.
1010
+ #
1011
+ # @return [Integer, nil]
1012
+ def exploration_preselect_limit_for(owner)
1013
+ limit = exploration_preselect_limit
1014
+ limit = limit.call(owner) if limit.respond_to?(:call)
1015
+ limit.is_a?(Integer) && !limit.negative? ? limit : nil
1016
+ rescue StandardError => e
1017
+ Rails.logger.warn("[ActionAgent] exploration_preselect_limit failed: #{e.class}: #{e.message}")
1018
+ nil
1019
+ end
1020
+
660
1021
  # Asks the host app whether +owner+ may perform +kind+.
661
1022
  #
662
1023
  # @return [String, Hash, nil] denial message or payload, nil when allowed
@@ -666,19 +1027,103 @@ module ActionAgent
666
1027
  quota_checker.call(owner, kind)
667
1028
  end
668
1029
 
669
- # Provider options for +owner+, or {} when the host app has none and
670
- # config/active_agent.yml should be used as-is.
1030
+ # Whether +user+ may perform +action+ on +subject+, as permission_checker
1031
+ # answers. Always true when no checker is configured, in either mode.
1032
+ # Otherwise:
1033
+ #
1034
+ # - an exception from the checker denies, and is logged
1035
+ # - in multi-tenant mode a nil +user+ denies without asking the checker,
1036
+ # and a nil answer denies
1037
+ # - in single-tenant mode a nil answer allows
1038
+ #
1039
+ # @raise [ArgumentError] when +action+ is not one of PERMISSION_ACTIONS
1040
+ # @return [Boolean]
1041
+ def permitted?(user, action, subject = nil)
1042
+ unless PERMISSION_ACTIONS.include?(action)
1043
+ raise ArgumentError, "Unknown permission action #{action.inspect}; expected one of #{PERMISSION_ACTIONS.join(', ')}"
1044
+ end
1045
+ return true if permission_checker.nil?
1046
+ return false if multi_tenant? && user.nil?
1047
+
1048
+ answer = begin
1049
+ permission_checker.call(user, action, subject)
1050
+ rescue StandardError => e
1051
+ Rails.logger.error("[ActionAgent] permission_checker raised for #{action}, denying: #{e.class}: #{e.message}")
1052
+ return false
1053
+ end
1054
+ return !multi_tenant? if answer.nil?
1055
+
1056
+ answer ? true : false
1057
+ end
1058
+
1059
+ # Logs a warning when the install is multi-tenant and has no
1060
+ # permission_checker, because every member of a tenant may then perform
1061
+ # every privileged action. The engine calls it once the host's
1062
+ # initializers have run.
1063
+ #
1064
+ # @return [Boolean] whether it warned
1065
+ def warn_about_unchecked_permissions
1066
+ return false unless multi_tenant? && permission_checker.nil?
1067
+
1068
+ Rails.logger&.warn(
1069
+ "[ActionAgent] multi_tenant is on and no permission_checker is configured, so every member of a tenant " \
1070
+ "may store provider credentials, connect GitHub and create API keys. Set config.permission_checker to " \
1071
+ "restrict them."
1072
+ )
1073
+ true
1074
+ end
1075
+
1076
+ # Provider options the host's provider_credentials_resolver returns for
1077
+ # +owner+, or {} when it is unset or has none. +actor+ is passed only to
1078
+ # a resolver that declares an actor: keyword or accepts **; any other is
1079
+ # called with (owner, provider).
1080
+ #
1081
+ # A resolver that raises is logged and yields {} on a single-tenant
1082
+ # install. On a multi-tenant install it raises
1083
+ # ActionAgent::ProviderCredentials::Unresolved instead, so the run
1084
+ # fails rather than generating on the platform's own credentials.
1085
+ #
1086
+ # Callers resolving what a generation runs on use
1087
+ # ActionAgent::ProviderCredentials.resolve, which also tries the
1088
+ # dashboard's stored keys.
671
1089
  #
672
1090
  # @return [Hash]
673
- def provider_credentials(owner, provider)
1091
+ def provider_credentials(owner, provider, actor: nil)
674
1092
  return {} if provider_credentials_resolver.nil?
675
1093
 
676
- provider_credentials_resolver.call(owner, provider) || {}
1094
+ resolver = provider_credentials_resolver
1095
+ result = if resolver_accepts_actor?(resolver)
1096
+ resolver.call(owner, provider, actor: actor)
1097
+ else
1098
+ resolver.call(owner, provider)
1099
+ end
1100
+ result || {}
677
1101
  rescue StandardError => e
1102
+ if multi_tenant?
1103
+ raise ActionAgent::ProviderCredentials::Unresolved,
1104
+ "The provider_credentials_resolver failed for #{provider} (#{e.class})"
1105
+ end
1106
+
678
1107
  Rails.logger.warn("[ActionAgent] provider credential lookup failed: #{e.message}")
679
1108
  {}
680
1109
  end
681
1110
 
1111
+ # The members ActionAgent.members_resolver lists for +owner+, each
1112
+ # reduced to id, name, email and role. Nil when no resolver is set or it
1113
+ # raised (logged), so the caller can list the signed-in user instead.
1114
+ #
1115
+ # @return [Array<Hash>, nil]
1116
+ def members_for(owner)
1117
+ return nil if members_resolver.nil?
1118
+
1119
+ Array(members_resolver.call(owner)).map do |member|
1120
+ member.to_h.symbolize_keys.slice(:id, :name, :email, :role)
1121
+ end
1122
+ rescue StandardError => e
1123
+ Rails.logger.warn("[ActionAgent] members_resolver failed: #{e.class}: #{e.message}")
1124
+ nil
1125
+ end
1126
+
682
1127
  # Returns the trace model class to use.
683
1128
  #
684
1129
  # @return [Class] The trace model class
@@ -761,17 +1206,28 @@ module ActionAgent
761
1206
  @layout = nil
762
1207
  @sandbox_service = :mock
763
1208
  @sandbox_limits = nil
764
- @storage_service = nil
1209
+ @recording_limits = nil
1210
+ @active_storage = :auto
1211
+ @active_storage_service = nil
765
1212
  @ingest_api_key = nil
766
1213
  @base_controller_class = "ActionController::Base" # deprecated no-op
767
1214
  @model_concerns = []
768
1215
  @controller_concerns = []
769
1216
  @quota_checker = nil
1217
+ @permission_checker = nil
770
1218
  @provider_credentials_resolver = nil
1219
+ @provider_key_scope = :organization
1220
+ @members_resolver = nil
1221
+ @member_invite_url = nil
771
1222
  @sandbox_backends = {}
772
1223
  @local_sandboxes_enabled = nil
773
1224
  @local_sandbox_root = nil
774
1225
  @local_sandbox_boot_timeout = 600
1226
+ @browser_sidecar_path = nil
1227
+ @node_command = "node"
1228
+ @npm_command = "npm"
1229
+ @browser_start_timeout = 60
1230
+ @browser_live_origins = []
775
1231
  @claude_code_command = "claude"
776
1232
  @claude_code_permission_mode = "acceptEdits"
777
1233
  @claude_code_max_turns = nil
@@ -779,17 +1235,26 @@ module ActionAgent
779
1235
  @codex_command = "codex"
780
1236
  @codex_timeout = 1800
781
1237
  @claude_code_auth = :api_key
1238
+ @claude_code_hosted_login_enabled = false
1239
+ @claude_code_login_timeout = 300
782
1240
  @execution_enabled = true
783
1241
  @run_host_agent_classes = false
784
1242
  @assistant_enabled = nil
1243
+ @capture_dashboard_sessions = true
785
1244
 
786
1245
  @scenario_evaluation_adapter_resolver = nil
787
1246
  @table_name_prefix = "active_agent_"
788
1247
  @agent_polymorphic_name = nil
789
1248
  @encrypt_credentials = true
1249
+ @input_request_ttl = 1.day
790
1250
  @github_client_id = nil
791
1251
  @github_client_secret = nil
792
1252
  @github_oauth_scopes = "repo read:user"
1253
+ @github_app_id = nil
1254
+ @github_app_private_key = nil
1255
+ @github_app_slug = nil
1256
+ @github_app_client_id = nil
1257
+ @github_app_client_secret = nil
793
1258
  @trace_retention = nil
794
1259
  @trace_owner_resolver = nil
795
1260
  @usage_recorder = nil
@@ -803,6 +1268,7 @@ module ActionAgent
803
1268
  @schema_tools_path = "app/agent_tools"
804
1269
  @mcp_schema_tools = nil
805
1270
  @mcp_dashboard_tools = nil
1271
+ @exploration_preselect_limit = nil
806
1272
  end
807
1273
 
808
1274
  # Host-declared schema tool classes, resolved from names and filtered to
@@ -883,9 +1349,28 @@ module ActionAgent
883
1349
 
884
1350
  private
885
1351
 
1352
+ def resolver_accepts_actor?(resolver)
1353
+ parameters = resolver.respond_to?(:parameters) ? resolver.parameters : resolver.method(:call).parameters
1354
+ parameters.any? { |type, name| type == :keyrest || (%i[key keyreq].include?(type) && name == :actor) }
1355
+ end
1356
+
886
1357
  def resolve_concerns(entries)
887
1358
  Array(entries).map { |entry| entry.is_a?(Module) ? entry : entry.to_s.constantize }
888
1359
  end
1360
+
1361
+ # What +recorder+ is called with: the quantity when there is one and it
1362
+ # takes a third argument, nil in its place when it requires one, and
1363
+ # nothing more otherwise.
1364
+ def usage_arguments(recorder, owner, kind, quantity)
1365
+ parameters = (recorder.is_a?(Proc) || recorder.is_a?(Method) ? recorder : recorder.method(:call)).parameters
1366
+ third =
1367
+ if quantity.nil?
1368
+ parameters.count { |type, _| type == :req } >= 3
1369
+ else
1370
+ parameters.count { |type, _| %i[req opt].include?(type) } >= 3 || parameters.any? { |type, _| type == :rest }
1371
+ end
1372
+ third ? [ owner, kind, quantity ] : [ owner, kind ]
1373
+ end
889
1374
  end
890
1375
 
891
1376
  # Set defaults