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
@@ -19,6 +19,37 @@ module ActionAgent
19
19
 
20
20
  source_root File.expand_path("templates", __dir__)
21
21
 
22
+ # A numbered migration template is templates/migrations/NNN_<name>.rb.erb,
23
+ # emitted as db/migrate/<timestamp>_<name>.rb: "015_add_widget_scope.rb.erb"
24
+ # becomes "<timestamp>_add_widget_scope.rb". A migration added to the
25
+ # engine ships this way, as one new file and no generator change. The
26
+ # templates are emitted after every other dashboard migration, in NNN
27
+ # order, so a later number may alter what an earlier one created. A
28
+ # template is skipped when db/migrate already holds a migration of its
29
+ # name. A traces-only install skips them all.
30
+ NUMBERED_MIGRATION = /\A\d{3}_(?<name>[a-z0-9_]+)\.rb\.erb\z/
31
+
32
+ class << self
33
+ # Where the numbered migration templates live.
34
+ attr_writer :numbered_migrations_path
35
+
36
+ def numbered_migrations_path
37
+ @numbered_migrations_path || File.expand_path("templates/migrations", __dir__)
38
+ end
39
+
40
+ # The numbered migration templates, in the order they are emitted, as
41
+ # [template path, migration name] pairs. Files whose names do not
42
+ # follow the convention are left out.
43
+ def numbered_migrations
44
+ Dir.children(numbered_migrations_path).sort.filter_map do |file|
45
+ match = NUMBERED_MIGRATION.match(file)
46
+ [ File.join(numbered_migrations_path, file), match[:name] ] if match
47
+ end
48
+ rescue Errno::ENOENT
49
+ []
50
+ end
51
+ end
52
+
22
53
  desc "Installs the ActiveAgent Dashboard with telemetry storage"
23
54
 
24
55
  class_option :multi_tenant, type: :boolean, default: false,
@@ -138,6 +169,12 @@ module ActionAgent
138
169
  unless existing_migration?("add_code_session_runner")
139
170
  migration_template("add_code_session_runner.rb.erb", "db/migrate/add_code_session_runner.rb")
140
171
  end
172
+
173
+ self.class.numbered_migrations.each do |template, name|
174
+ next if existing_migration?(name)
175
+
176
+ migration_template(template, "db/migrate/#{name}.rb")
177
+ end
141
178
  end
142
179
 
143
180
  def add_route
@@ -79,6 +79,95 @@ ActionAgent.configure do |config|
79
79
  # With an owner model configured, an owner that does not resolve sees
80
80
  # nothing rather than everything. An empty dashboard for a signed-in user
81
81
  # means the resolver above returned nil.
82
+ #
83
+ # Each model is owned by one class: agents, sandboxes, recordings and code
84
+ # sessions name the user first, keys and connections the account. With
85
+ # both classes configured, the dashboard still resolves the account as the
86
+ # owner, so a platform that scopes everything by account re-declares the
87
+ # user-first models from `to_prepare` and maps a user to its account:
88
+ #
89
+ # Rails.application.config.to_prepare do
90
+ # [ActionAgent::Agent, ActionAgent::SandboxSession,
91
+ # ActionAgent::SessionRecording, ActionAgent::CodeSession].each do |model|
92
+ # model.owned_by :account, :user
93
+ # end
94
+ # end
95
+ # config.tenant_resolver = ->(owner) { owner.is_a?(User) ? owner.account : owner }
96
+ #
97
+ # An owner of another class than the one a model is owned by reads nothing
98
+ # and is refused as a writer, so leaving this out shows an empty dashboard
99
+ # rather than another tenant's records.
100
+
101
+ # ==========================================================================
102
+ # Permissions
103
+ # ==========================================================================
104
+ #
105
+ # Who may perform the dashboard's privileged actions: storing provider
106
+ # credentials (:manage_credentials), connecting GitHub (:manage_github),
107
+ # creating and revoking API keys (:manage_api_keys), answering a paused
108
+ # run's request for input (:answer_input_request), and the rest of
109
+ # ActionAgent::PERMISSION_ACTIONS. Called with the signed-in user, the
110
+ # action and the record it applies to; true allows, false denies with a
111
+ # 403. Unset, anyone who passes authentication may do all of them. With
112
+ # multi_tenant on, that is every member of every tenant, and the engine
113
+ # logs a warning at boot until a checker is set.
114
+ #
115
+ # An exception from the checker denies. In multi-tenant mode a nil answer,
116
+ # or no signed-in user, also denies; in single-tenant mode a nil answer
117
+ # allows.
118
+ #
119
+ # config.permission_checker = ->(user, action, subject) {
120
+ # user.present? && user.admin?
121
+ # }
122
+
123
+ # ==========================================================================
124
+ # Provider keys and members
125
+ # ===================================================================
126
+ #
127
+ # Agent runs, the evaluation judge and the assistant use the account's
128
+ # provider keys (Settings -> API Keys, or the Organization page) before
129
+ # config/active_agent.yml. With :personal_override, each member may also
130
+ # save a personal key per provider, which the runs they start use first.
131
+ # A personal key sends the organization's agent traffic to that member's
132
+ # own provider account, so it is off by default. It needs account_class;
133
+ # the judge, sandboxes, Claude Code and Codex always use the account's keys.
134
+ # A personal Ollama host decides where the server sends requests, so
135
+ # saving or testing one asks the permission_checker above for
136
+ # :manage_credentials, with the personal key (subject.personal?) as the
137
+ # subject.
138
+ #
139
+ # config.provider_key_scope = :personal_override
140
+ #
141
+ # Credentials your app keeps elsewhere, tried after a personal key and
142
+ # before the account's stored key. Declare actor: to learn who is acting
143
+ # (nil for the judge and unattributed runs). With multi_tenant on, a
144
+ # resolver that raises fails the run rather than falling back to
145
+ # config/active_agent.yml. If it reads the provider_keys table, keep it
146
+ # to organization rows (ActionAgent::ProviderKey.for_owner does).
147
+ #
148
+ # config.provider_credentials_resolver = ->(owner, provider, actor: nil) {
149
+ # owner.provider_credentials_for(provider)
150
+ # }
151
+ #
152
+ # The Organization page's Team Members table, as
153
+ # [{ id:, name:, email:, role: }], and where "+ Invite Member" links. Both
154
+ # unset, the table lists the signed-in user and the button is hidden.
155
+ #
156
+ # config.members_resolver = ->(account) {
157
+ # account.members.map { |user| { id: user.id, name: user.name, email: user.email, role: user.role } }
158
+ # }
159
+ # config.member_invite_url = "/team/invitations/new"
160
+
161
+ # ==========================================================================
162
+ # Input requests
163
+ # ==========================================================================
164
+ #
165
+ # How long a paused run's request for input waits for an answer. A later
166
+ # answer is refused with a 409, and the run fails. Schedule
167
+ # ActionAgent::InputRequestExpiryJob to fail the runs whose requests
168
+ # nobody answers or looks at.
169
+ #
170
+ # config.input_request_ttl = 1.day
82
171
 
83
172
  # ==========================================================================
84
173
  # Host integration
@@ -106,6 +195,63 @@ ActionAgent.configure do |config|
106
195
  #
107
196
  # config.assistant_enabled = true
108
197
 
198
+ # ==========================================================================
199
+ # GitHub
200
+ # ===================================================================
201
+
202
+ #
203
+ # Checkout sandboxes clone repositories chosen in Settings -> Integrations.
204
+ # Through a GitHub App, each checkout gets a one-hour token limited to its
205
+ # one repository. Settings -> Integrations -> Create GitHub App registers
206
+ # one for you and shows these values once; otherwise register the App with
207
+ # "Request user authorization (OAuth) during installation" and "Redirect on
208
+ # update" turned on and the callback URL
209
+ # <mount>/api/github_installations/callback. Each setting falls back to its
210
+ # GITHUB_APP_* environment variable.
211
+ #
212
+ # config.github_app_id = Rails.application.credentials.dig(:github_app, :id)
213
+ # config.github_app_slug = Rails.application.credentials.dig(:github_app, :slug)
214
+ # config.github_app_client_id = Rails.application.credentials.dig(:github_app, :client_id)
215
+ # config.github_app_client_secret = Rails.application.credentials.dig(:github_app, :client_secret)
216
+ # config.github_app_private_key = Rails.application.credentials.dig(:github_app, :private_key)
217
+ #
218
+ # Or an OAuth App, connected per owner with the callback URL
219
+ # <mount>/api/github_connection/callback (GITHUB_CLIENT_ID and
220
+ # GITHUB_CLIENT_SECRET when unset).
221
+ #
222
+ # config.github_client_id = Rails.application.credentials.dig(:github, :client_id)
223
+ # config.github_client_secret = Rails.application.credentials.dig(:github, :client_secret)
224
+
225
+ # ==========================================================================
226
+ # Recording the Run Agent workbench
227
+ # ==========================================================================
228
+ #
229
+ # While the Run Agent workbench has a conversation open, the dashboard
230
+ # records the page with rrweb, and the recording replays as the
231
+ # conversation's browser lane. Field values are masked, elements marked
232
+ # data-aa-secret and hidden inputs are left out, and the owner's stored
233
+ # credentials are masked in every batch; what the page shows otherwise, the
234
+ # conversation included, is recorded as shown. Set it to false to record
235
+ # nothing: conversations still replay from their messages, model calls and
236
+ # tool calls.
237
+ #
238
+ # config.capture_dashboard_sessions = false
239
+
240
+ # ==========================================================================
241
+ # Active Storage
242
+ # ==========================================================================
243
+ #
244
+ # The dashboard attaches files in a few places: the files uploaded with a
245
+ # run, a session recording's snapshots and oversized event payloads, and a
246
+ # scenario catalog's YAML. By default (:auto) it uses Active Storage when
247
+ # this app has it loaded and migrated, and otherwise keeps the
248
+ # metadata-only form of each feature. true requires it (boot raises
249
+ # without it); false never attaches. The service names one from
250
+ # config/storage.yml; unset, attachments use the app's default service.
251
+ #
252
+ # config.active_storage = :auto
253
+ # config.active_storage_service = :recordings
254
+
109
255
  # ==========================================================================
110
256
  # Sandboxes
111
257
  # ==========================================================================
@@ -150,6 +296,18 @@ ActionAgent.configure do |config|
150
296
  #
151
297
  # config.claude_code_auth = :local_login
152
298
  #
299
+ # :sandbox_login adds the same subscription sign-in flow to Settings ->
300
+ # Integrations and evaluation fix cards. Requires current_user_resolver
301
+ # and an unmodified Claude Code CLI. Credentials stay in one sandbox's
302
+ # private CLAUDE_CONFIG_DIR, outside its checkout; they are never imported
303
+ # into ProviderKey. The account API key remains the fallback.
304
+ # config.claude_code_auth = :sandbox_login
305
+ # config.claude_code_login_timeout = 300
306
+ # Remote backends must implement all four login verbs and refresh_runtime.
307
+ # Keep hosted login disabled until the operator has reviewed Anthropic's
308
+ # current commercial hosting requirements and configured tenant isolation.
309
+ # config.claude_code_hosted_login_enabled = false
310
+ #
153
311
  # Expired sandboxes are stopped by `bin/rails action_agent:sandbox:reap`.
154
312
  # Schedule it every few minutes.
155
313
 
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Gives provider keys a scope: "organization" for the key an account shares,
4
+ # "user:<id>" for one member's own key. The unique index on
5
+ # (account_id, scope_key, provider) keeps one organization key per provider
6
+ # and one personal key per member and provider. set_by_id names the user who
7
+ # last saved a key from the dashboard.
8
+ #
9
+ # Existing rows become organization rows. An install whose rows break the
10
+ # new index (two keys for one account and provider) stops here and lists
11
+ # them, because only one of each pair can stay and this migration does not
12
+ # choose. Guarded, so it is safe to re-run once they are resolved.
13
+ class AddProviderKeyScope < ActiveRecord::Migration<%= migration_version %>
14
+ ORGANIZATION = "organization"
15
+
16
+ def up
17
+ return unless table_exists?(table)
18
+
19
+ refuse_duplicates!
20
+ add_column table, :scope_key, :string, null: false, default: ORGANIZATION unless column_exists?(table, :scope_key)
21
+ add_column table, :set_by_id, :bigint unless column_exists?(table, :set_by_id)
22
+ add_index table, %i[account_id scope_key provider], unique: true, name: index_name unless index_name_exists?(table, index_name)
23
+ end
24
+
25
+ # Refused while personal keys exist: without scope_key they would read as
26
+ # the organization's keys.
27
+ def down
28
+ return unless table_exists?(table)
29
+
30
+ if column_exists?(table, :scope_key)
31
+ personal = select_values("SELECT id FROM #{quoted_table} WHERE scope_key <> #{connection.quote(ORGANIZATION)} ORDER BY id")
32
+ if personal.any?
33
+ raise ActiveRecord::IrreversibleMigration,
34
+ "#{table} holds personal keys (ids #{personal.join(', ')}). Delete them before rolling back."
35
+ end
36
+ end
37
+
38
+ remove_index table, name: index_name if index_name_exists?(table, index_name)
39
+ remove_column table, :set_by_id if column_exists?(table, :set_by_id)
40
+ remove_column table, :scope_key if column_exists?(table, :scope_key)
41
+ end
42
+
43
+ private
44
+
45
+ def refuse_duplicates!
46
+ scoped = column_exists?(table, :scope_key)
47
+ columns = scoped ? "id, account_id, provider, scope_key" : "id, account_id, provider"
48
+ rows = select_rows("SELECT #{columns} FROM #{quoted_table} WHERE account_id IS NOT NULL ORDER BY id")
49
+ groups = rows.group_by { |(_id, account_id, provider, scope_key)| [ account_id, provider, scope_key || ORGANIZATION ] }
50
+ duplicates = groups.select { |_key, members| members.size > 1 }
51
+ return if duplicates.empty?
52
+
53
+ listed = duplicates.map do |(account_id, provider, scope_key), members|
54
+ "account_id #{account_id}, provider #{provider}, scope #{scope_key}: ids #{members.map(&:first).join(', ')}"
55
+ end
56
+ raise ActiveRecord::MigrationError,
57
+ "#{table} holds more than one key for the same account, provider and scope. Keep one of each and " \
58
+ "delete the rest, then run the migration again:\n #{listed.join("\n ")}"
59
+ end
60
+
61
+ def table
62
+ "#{ActionAgent.table_name_prefix}provider_keys"
63
+ end
64
+
65
+ def quoted_table
66
+ connection.quote_table_name(table)
67
+ end
68
+
69
+ def index_name
70
+ "index_#{table}_on_scope"
71
+ end
72
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Input requests: a question a paused run puts to a person, its answer, and
4
+ # the checkpoint the run resumes from. Also adds the list of tools an agent
5
+ # runs only after a person approves the call.
6
+ #
7
+ # Table names follow ActionAgent.table_name_prefix, and JSON columns are
8
+ # jsonb on PostgreSQL and json elsewhere, the same way as
9
+ # create_active_agent_dashboard_tables.
10
+ class CreateActiveAgentInputRequests < ActiveRecord::Migration<%= migration_version %>
11
+ def change
12
+ prefix = ActionAgent.table_name_prefix
13
+
14
+ create_table "#{prefix}input_requests" do |t|
15
+ t.string :subject_type, null: false
16
+ t.bigint :subject_id, null: false
17
+ # Shared by the requests one pause raised.
18
+ t.string :pause_key, null: false
19
+ t.string :kind, null: false
20
+ t.text :prompt, null: false
21
+ t.column :options, json_type
22
+ t.column :answer_schema, json_type
23
+ t.string :tool_call_id
24
+ t.string :tool_name
25
+ # The paused call's arguments, kept for a confirm request only.
26
+ t.column :arguments, json_type
27
+ t.integer :status, default: 0, null: false
28
+ # Encrypted at rest (text: ciphertext is longer than the value).
29
+ t.text :answer, **long_text
30
+ t.text :checkpoint, **long_text
31
+ t.bigint :requested_by_id
32
+ t.bigint :answered_by_id
33
+ t.datetime :answered_at
34
+ t.datetime :expires_at
35
+ t.bigint :user_id
36
+ t.bigint :account_id
37
+ t.timestamps
38
+ t.index [ :subject_type, :subject_id ]
39
+ t.index :pause_key
40
+ t.index [ :status, :expires_at ]
41
+ t.index :user_id
42
+ t.index :account_id
43
+ end
44
+
45
+ agents = "#{prefix}agents"
46
+ reversible do |direction|
47
+ direction.up do
48
+ unless column_exists?(agents, :approval_required_tools)
49
+ add_column agents, :approval_required_tools, json_type, **json_default([])
50
+ end
51
+ end
52
+ direction.down do
53
+ remove_column agents, :approval_required_tools if column_exists?(agents, :approval_required_tools)
54
+ end
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def json_type
61
+ @json_type ||= postgres? ? :jsonb : :json
62
+ end
63
+
64
+ # MySQL rejects a default on a JSON column outright, so the column is
65
+ # created without one there.
66
+ def json_default(value, null: nil)
67
+ return {} unless postgres?
68
+
69
+ null.nil? ? { default: value } : { default: value, null: null }
70
+ end
71
+
72
+ # MySQL's TEXT holds 64 KB, less than the encrypted checkpoint of a long
73
+ # conversation. Other adapters reject the option.
74
+ def long_text
75
+ mysql? ? { size: :long } : {}
76
+ end
77
+
78
+ def postgres?
79
+ connection.adapter_name.to_s.downcase.include?("postgres")
80
+ end
81
+
82
+ def mysql?
83
+ connection.adapter_name.to_s.downcase.match?(/mysql|trilogy/)
84
+ end
85
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Recording events: what a browser recorded during a session (rrweb DOM
4
+ # events, console lines, markers, the agent's own browser actions, and human
5
+ # input during a takeover), stored in batches. Each row holds one kind of
6
+ # event from one batch, its payload gzip JSON either inline or attached
7
+ # through Active Storage.
8
+ #
9
+ # Session recordings gain the conversation they belong to, where they came
10
+ # from, the digest and expiry of the token a browser posts events with, and
11
+ # the counters the per-recording caps are checked against.
12
+ #
13
+ # Table names follow ActionAgent.table_name_prefix, the same way as
14
+ # create_active_agent_dashboard_tables.
15
+ class CreateActiveAgentRecordingEvents < ActiveRecord::Migration<%= migration_version %>
16
+ def change
17
+ prefix = ActionAgent.table_name_prefix
18
+
19
+ create_table "#{prefix}recording_events" do |t|
20
+ t.bigint :session_recording_id, null: false
21
+ t.string :kind, null: false
22
+ # The batch's events span this range of server time, once the batch's
23
+ # clock offset is applied.
24
+ t.datetime :occurred_from, null: false
25
+ t.datetime :occurred_to, null: false
26
+ # Where this row's first event sat in its batch: orders the rows of one
27
+ # batch that share a start time.
28
+ t.integer :batch_index, default: 0, null: false
29
+ # Server receive time minus the client's send time, in milliseconds.
30
+ t.bigint :clock_offset_ms, default: 0, null: false
31
+ t.integer :event_count, default: 0, null: false
32
+ # The payload's size as JSON, before compression.
33
+ t.integer :byte_size, default: 0, null: false
34
+ # The limit makes MySQL create a column larger than a 64 KB BLOB.
35
+ t.binary :payload, limit: 16.megabytes
36
+ t.bigint :user_id
37
+ t.bigint :account_id
38
+ t.timestamps
39
+ t.index [ :session_recording_id, :occurred_from, :batch_index ]
40
+ t.index :user_id
41
+ t.index :account_id
42
+ end
43
+
44
+ change_table "#{prefix}session_recordings" do |t|
45
+ t.bigint :agent_context_id
46
+ # "agent" or "dashboard"; null on recordings made before this column.
47
+ t.string :source
48
+ t.string :ingest_token_digest
49
+ t.datetime :ingest_token_expires_at
50
+ t.integer :event_count, default: 0, null: false
51
+ t.bigint :event_bytes, default: 0, null: false
52
+ t.integer :dropped_event_count, default: 0, null: false
53
+ t.index :agent_context_id
54
+ t.index :ingest_token_digest, unique: true
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Browser sessions: a sandbox can run one browser, driven over MCP by the
4
+ # agents run against it. Each sandbox session records the browser's mode and
5
+ # status, where its MCP endpoint answers and the bearer token it expects
6
+ # (encrypted, like runtime_mcp_token), where a person can watch it, and when
7
+ # it started, which is what its minutes are counted from.
8
+ #
9
+ # Table names follow ActionAgent.table_name_prefix, the same way as
10
+ # create_active_agent_dashboard_tables.
11
+ class AddBrowserToActiveAgentSandboxSessions < ActiveRecord::Migration<%= migration_version %>
12
+ def change
13
+ change_table "#{ActionAgent.table_name_prefix}sandbox_sessions" do |t|
14
+ # "headless" or "headed".
15
+ t.string :browser_mode
16
+ # "starting", "running", "stopped" or "failed"; null before a browser
17
+ # was ever started.
18
+ t.string :browser_status
19
+ t.string :browser_mcp_url
20
+ t.string :browser_live_url
21
+ t.text :browser_token
22
+ t.datetime :browser_started_at
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Projects: a repository the dashboard boots in a checkout sandbox, the
4
+ # secrets that boot needs, and the agent and evaluation the project tests the
5
+ # booted app with. A sandbox session records the project it was booted for.
6
+ #
7
+ # Table names follow ActionAgent.table_name_prefix, and JSON columns are
8
+ # jsonb on PostgreSQL and json elsewhere, the same way as
9
+ # create_active_agent_dashboard_tables.
10
+ class CreateActiveAgentProjects < ActiveRecord::Migration<%= migration_version %>
11
+ def change
12
+ prefix = ActionAgent.table_name_prefix
13
+
14
+ create_table "#{prefix}projects" do |t|
15
+ t.string :name, null: false
16
+ # owner/name on GitHub, and the ref a boot checks out (the repository's
17
+ # default branch when blank).
18
+ t.string :repository, null: false
19
+ t.string :default_ref
20
+ t.string :start_url, null: false, default: "/"
21
+ t.string :status, null: false, default: "draft"
22
+ # detected: the repository does not bundle the engine; bootstrapped: a
23
+ # sandbox installed it; installed: the repository bundles it already.
24
+ t.string :install_state, null: false, default: "detected"
25
+ t.bigint :current_sandbox_session_id
26
+ t.bigint :target_agent_id
27
+ t.bigint :evaluation_id
28
+ t.column :settings, json_type, **json_default({})
29
+ t.bigint :account_id
30
+ t.bigint :user_id
31
+ t.timestamps
32
+ t.index :account_id
33
+ t.index :user_id
34
+ t.index :current_sandbox_session_id
35
+ t.index :target_agent_id
36
+ end
37
+
38
+ create_table "#{prefix}project_secrets" do |t|
39
+ t.bigint :project_id, null: false
40
+ t.string :name, null: false
41
+ # Encrypted at rest (text: ciphertext is longer than the value). Empty
42
+ # for a secret that uses the organization's provider key instead.
43
+ t.text :value
44
+ t.string :source, null: false, default: "entered"
45
+ t.string :provider
46
+ t.datetime :consented_at
47
+ t.bigint :set_by_id
48
+ t.bigint :account_id
49
+ t.bigint :user_id
50
+ t.timestamps
51
+ t.index [ :project_id, :name ], unique: true
52
+ t.index :account_id
53
+ t.index :user_id
54
+ end
55
+
56
+ change_table "#{prefix}sandbox_sessions" do |t|
57
+ t.bigint :project_id
58
+ t.index :project_id
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def json_type
65
+ @json_type ||= postgres? ? :jsonb : :json
66
+ end
67
+
68
+ # MySQL rejects a default on a JSON column outright, so the column is
69
+ # created without one there.
70
+ def json_default(value, null: nil)
71
+ return {} unless postgres?
72
+
73
+ null.nil? ? { default: value } : { default: value, null: null }
74
+ end
75
+
76
+ def postgres?
77
+ connection.adapter_name.to_s.downcase.include?("postgres")
78
+ end
79
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ # GitHub App installations an owner linked (Settings -> Integrations), the
4
+ # repositories they chose on each, and the installation a checkout sandbox
5
+ # clones through.
6
+ #
7
+ # Table names follow ActionAgent.table_name_prefix, and JSON columns are
8
+ # jsonb on PostgreSQL and json elsewhere, the same way as
9
+ # create_active_agent_dashboard_tables.
10
+ class AddGithubAppInstallations < ActiveRecord::Migration<%= migration_version %>
11
+ def change
12
+ prefix = ActionAgent.table_name_prefix
13
+
14
+ create_table "#{prefix}github_installations" do |t|
15
+ # GitHub's installation id. Unique across every owner: one installation
16
+ # is linked to one owner at most.
17
+ t.bigint :installation_id, null: false
18
+ # The GitHub user or organization the App is installed on.
19
+ t.bigint :github_account_id, null: false
20
+ t.string :github_account_login, null: false
21
+ t.string :github_account_type, null: false
22
+ # "all" or "selected", as the installation was configured on GitHub.
23
+ t.string :repository_selection
24
+ # The permissions GitHub granted the installation.
25
+ t.column :permissions, json_type, **json_default({})
26
+ # The repositories the owner made available, shaped like
27
+ # github_connections.repositories.
28
+ t.column :repositories, json_type, **json_default([])
29
+ t.datetime :suspended_at
30
+ t.datetime :removed_at
31
+ t.bigint :user_id
32
+ t.bigint :account_id
33
+ t.timestamps
34
+ t.index :installation_id, unique: true, name: "index_#{prefix}github_installations_on_installation"
35
+ t.index :account_id, name: "index_#{prefix}github_installations_on_account"
36
+ t.index :user_id, name: "index_#{prefix}github_installations_on_user"
37
+ end
38
+
39
+ change_table "#{prefix}sandbox_sessions" do |t|
40
+ t.bigint :github_installation_id
41
+ t.index :github_installation_id, name: "index_#{prefix}sandbox_sessions_on_github_installation"
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def json_type
48
+ @json_type ||= postgres? ? :jsonb : :json
49
+ end
50
+
51
+ # MySQL rejects a default on a JSON column outright, so the column is
52
+ # created without one there.
53
+ def json_default(value, null: nil)
54
+ return {} unless postgres?
55
+
56
+ null.nil? ? { default: value } : { default: value, null: null }
57
+ end
58
+
59
+ def postgres?
60
+ connection.adapter_name.to_s.downcase.include?("postgres")
61
+ end
62
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Explorations: a walk through a project's running app, by the engine's
4
+ # explorer agent or by an outside agent, and the candidate scenarios it
5
+ # proposed for review. Accepted candidates are merged into the project's
6
+ # evaluation; the rest stay here with their verdicts and provenance.
7
+ #
8
+ # Table names follow ActionAgent.table_name_prefix, and JSON columns are
9
+ # jsonb on PostgreSQL and json elsewhere, the same way as
10
+ # create_active_agent_dashboard_tables.
11
+ class CreateActiveAgentExplorations < ActiveRecord::Migration<%= migration_version %>
12
+ def change
13
+ prefix = ActionAgent.table_name_prefix
14
+
15
+ create_table "#{prefix}explorations" do |t|
16
+ # At least one is set: a project's explorations merge into the
17
+ # project's evaluation, and one without a project into its own.
18
+ t.bigint :project_id
19
+ t.bigint :evaluation_id
20
+ t.bigint :agent_run_id
21
+ t.bigint :sandbox_session_id
22
+ t.bigint :session_recording_id
23
+ # explorer: the engine's explorer agent; external: candidates an
24
+ # outside agent submitted.
25
+ t.string :source, null: false, default: "external"
26
+ t.string :status, null: false, default: "pending"
27
+ t.string :start_url
28
+ t.string :stop_reason
29
+ t.text :error_message
30
+ t.datetime :started_at
31
+ t.datetime :finished_at
32
+ # { minutes, steps, cost }, the limits and what was used of them.
33
+ t.column :budget, json_type, **json_default({})
34
+ t.column :usage, json_type, **json_default({})
35
+ t.column :candidates, json_type, **json_default([])
36
+ t.bigint :account_id
37
+ t.bigint :user_id
38
+ t.timestamps
39
+ t.index :project_id
40
+ t.index :evaluation_id
41
+ t.index :account_id
42
+ t.index :user_id
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def json_type
49
+ @json_type ||= postgres? ? :jsonb : :json
50
+ end
51
+
52
+ # MySQL rejects a default on a JSON column outright, so the column is
53
+ # created without one there.
54
+ def json_default(value, null: nil)
55
+ return {} unless postgres?
56
+
57
+ null.nil? ? { default: value } : { default: value, null: null }
58
+ end
59
+
60
+ def postgres?
61
+ connection.adapter_name.to_s.downcase.include?("postgres")
62
+ end
63
+ end