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.
- checksums.yaml +4 -4
- data/app/assets/builds/action_agent.css +1 -1
- data/app/assets/builds/action_agent.js +76 -73
- data/app/assets/builds/action_agent_recorder.js +78 -0
- data/app/assets/builds/action_agent_replay.js +226 -0
- data/app/controllers/action_agent/api/agent_runs_controller.rb +3 -1
- data/app/controllers/action_agent/api/agents_controller.rb +1 -0
- data/app/controllers/action_agent/api/api_keys_controller.rb +12 -2
- data/app/controllers/action_agent/api/base_controller.rb +63 -2
- data/app/controllers/action_agent/api/claude_logins_controller.rb +93 -0
- data/app/controllers/action_agent/api/code_sessions_controller.rb +53 -6
- data/app/controllers/action_agent/api/dashboard_assistant_controller.rb +2 -2
- data/app/controllers/action_agent/api/draft_pull_requests_controller.rb +248 -0
- data/app/controllers/action_agent/api/evaluation_fixes_controller.rb +27 -0
- data/app/controllers/action_agent/api/evaluations_controller.rb +9 -4
- data/app/controllers/action_agent/api/explorations_controller.rb +213 -0
- data/app/controllers/action_agent/api/github_app_manifests_controller.rb +125 -0
- data/app/controllers/action_agent/api/github_connections_controller.rb +40 -2
- data/app/controllers/action_agent/api/github_installations_controller.rb +280 -0
- data/app/controllers/action_agent/api/input_requests_controller.rb +77 -0
- data/app/controllers/action_agent/api/mcp_controller.rb +46 -12
- data/app/controllers/action_agent/api/members_controller.rb +36 -0
- data/app/controllers/action_agent/api/project_explorations_controller.rb +80 -0
- data/app/controllers/action_agent/api/project_install_pull_requests_controller.rb +305 -0
- data/app/controllers/action_agent/api/project_secrets_controller.rb +96 -0
- data/app/controllers/action_agent/api/project_setup_controller.rb +151 -0
- data/app/controllers/action_agent/api/project_sign_ins_controller.rb +112 -0
- data/app/controllers/action_agent/api/projects_controller.rb +512 -0
- data/app/controllers/action_agent/api/provider_keys_controller.rb +176 -24
- data/app/controllers/action_agent/api/provider_models_controller.rb +37 -15
- data/app/controllers/action_agent/api/recording_event_ingest_controller.rb +67 -0
- data/app/controllers/action_agent/api/sandbox_browser_tickets_controller.rb +50 -0
- data/app/controllers/action_agent/api/sandbox_browsers_controller.rb +65 -0
- data/app/controllers/action_agent/api/sandboxes_controller.rb +140 -8
- data/app/controllers/action_agent/api/scenario_catalog_entries_controller.rb +188 -0
- data/app/controllers/action_agent/api/scenario_catalogs_controller.rb +302 -0
- data/app/controllers/action_agent/api/session_recordings_controller.rb +165 -34
- data/app/controllers/action_agent/api/sessions_controller.rb +111 -0
- data/app/controllers/action_agent/api/telemetry_keys_controller.rb +21 -0
- data/app/controllers/action_agent/dashboard_controller.rb +17 -6
- data/app/controllers/action_agent/session_player_controller.rb +58 -0
- data/app/controllers/concerns/action_agent/api/agent_serialization.rb +1 -0
- data/app/controllers/concerns/action_agent/api/mcp_dashboard_tools.rb +623 -18
- data/app/controllers/concerns/action_agent/api/project_browser.rb +46 -0
- data/app/controllers/concerns/action_agent/api/project_secret_authorization.rb +57 -0
- data/app/jobs/action_agent/agent_execution_job.rb +9 -39
- data/app/jobs/action_agent/agent_resume_job.rb +84 -0
- data/app/jobs/action_agent/code_session_job.rb +26 -2
- data/app/jobs/action_agent/draft_pull_request_job.rb +43 -0
- data/app/jobs/action_agent/exploration_job.rb +80 -0
- data/app/jobs/action_agent/input_request_expiry_job.rb +22 -0
- data/app/jobs/action_agent/project_evaluation_job.rb +59 -0
- data/app/jobs/action_agent/project_setup_job.rb +21 -0
- data/app/jobs/action_agent/sandbox_cleanup_job.rb +20 -3
- data/app/jobs/action_agent/sandbox_provision_job.rb +77 -10
- data/app/jobs/action_agent/sandbox_run_job.rb +9 -37
- data/app/jobs/action_agent/verify_evaluation_fix_job.rb +37 -0
- data/app/models/action_agent/agent.rb +39 -26
- data/app/models/action_agent/agent_run.rb +134 -23
- data/app/models/action_agent/agent_template.rb +35 -1
- data/app/models/action_agent/api_key.rb +14 -0
- data/app/models/action_agent/catalog_scenario.rb +75 -0
- data/app/models/action_agent/code_session.rb +16 -4
- data/app/models/action_agent/draft_pull_request.rb +131 -0
- data/app/models/action_agent/evaluation.rb +84 -0
- data/app/models/action_agent/exploration.rb +847 -0
- data/app/models/action_agent/github_installation.rb +159 -0
- data/app/models/action_agent/input_request.rb +291 -0
- data/app/models/action_agent/project.rb +789 -0
- data/app/models/action_agent/project_secret.rb +339 -0
- data/app/models/action_agent/provider_key.rb +200 -3
- data/app/models/action_agent/recording_event.rb +145 -0
- data/app/models/action_agent/recording_snapshot.rb +7 -7
- data/app/models/action_agent/sandbox_session.rb +297 -14
- data/app/models/action_agent/scenario_catalog.rb +172 -0
- data/app/models/action_agent/scenario_product.rb +57 -0
- data/app/models/action_agent/scenario_set.rb +138 -0
- data/app/models/action_agent/session_recording.rb +121 -14
- data/app/models/action_agent/telemetry_trace.rb +74 -19
- data/app/models/concerns/action_agent/ownable.rb +84 -13
- data/app/serializers/action_agent/input_request_serializer.rb +47 -0
- data/app/serializers/action_agent/scenario_catalog_serializer.rb +23 -0
- data/app/serializers/action_agent/session_summary_serializer.rb +146 -0
- data/app/services/action_agent/agent_execution_service.rb +450 -72
- data/app/services/action_agent/agent_sync.rb +5 -2
- data/app/services/action_agent/agent_tool_roster.rb +19 -4
- data/app/services/action_agent/agent_toolbox.rb +215 -9
- data/app/services/action_agent/browser_live_ticket.rb +78 -0
- data/app/services/action_agent/browser_sidecar.rb +213 -0
- data/app/services/action_agent/browser_sign_in.rb +163 -0
- data/app/services/action_agent/browser_tool_redaction.rb +92 -0
- data/app/services/action_agent/claude_code_auth.rb +70 -4
- data/app/services/action_agent/dashboard_assistant_service.rb +24 -10
- data/app/services/action_agent/draft_pull_request_publisher.rb +719 -0
- data/app/services/action_agent/evaluation_fix.rb +101 -0
- data/app/services/action_agent/evaluation_fix_comparison.rb +66 -0
- data/app/services/action_agent/evaluation_runner_service.rb +4 -5
- data/app/services/action_agent/evaluation_standing.rb +15 -8
- data/app/services/action_agent/explorer_execution_service.rb +517 -0
- data/app/services/action_agent/github_client.rb +405 -64
- data/app/services/action_agent/live_updates.rb +31 -0
- data/app/services/action_agent/local_sandbox_backend.rb +1218 -83
- data/app/services/action_agent/local_sandbox_claude_login.rb +151 -0
- data/app/services/action_agent/mcp_client.rb +6 -2
- data/app/services/action_agent/mcp_recording_middleware.rb +104 -145
- data/app/services/action_agent/mcp_tool_dispatcher.rb +36 -13
- data/app/services/action_agent/mock_sandbox_backend.rb +3 -1
- data/app/services/action_agent/playwright_mcp_client.rb +7 -0
- data/app/services/action_agent/project_boot_fix.rb +309 -0
- data/app/services/action_agent/project_capabilities.rb +200 -0
- data/app/services/action_agent/project_install_pull_request.rb +225 -0
- data/app/services/action_agent/project_preflight.rb +178 -0
- data/app/services/action_agent/project_secret_discovery.rb +142 -0
- data/app/services/action_agent/project_setup.rb +429 -0
- data/app/services/action_agent/provider_credentials.rb +102 -0
- data/app/services/action_agent/recording_event_ingest.rb +181 -0
- data/app/services/action_agent/runtime_tool_roster.rb +52 -0
- data/app/services/action_agent/sample_data/conference_ticket.rb +516 -0
- data/app/services/action_agent/sandbox_boot_spec.rb +558 -0
- data/app/services/action_agent/sandbox_browser.rb +299 -0
- data/app/services/action_agent/sandbox_browser_driver.rb +85 -0
- data/app/services/action_agent/sandbox_credential_paths.rb +26 -0
- data/app/services/action_agent/sandbox_mail.rb +103 -0
- data/app/services/action_agent/sandbox_manifest.rb +166 -13
- data/app/services/action_agent/sandbox_orchestrator.rb +276 -22
- data/app/services/action_agent/sandbox_patch.rb +253 -0
- data/app/services/action_agent/scenario_catalog_import.rb +255 -0
- data/app/services/action_agent/scenario_catalog_repository_import.rb +81 -0
- data/app/services/action_agent/scenario_evaluation_runner.rb +119 -16
- data/app/services/action_agent/secret_scrubber.rb +33 -4
- data/app/services/action_agent/session_index.rb +270 -0
- data/app/services/action_agent/session_recording_service.rb +11 -0
- data/app/services/action_agent/session_timeline.rb +505 -0
- data/app/views/action_agent/github_app_manifests/created.html.erb +55 -0
- data/app/views/action_agent/session_player/show.html.erb +19 -0
- data/config/routes.rb +152 -1
- data/lib/action_agent/claude_login_process.rb +107 -0
- data/lib/action_agent/engine.rb +55 -5
- data/lib/action_agent/secret_requests.rb +80 -0
- data/lib/action_agent/version.rb +1 -1
- data/lib/action_agent.rb +507 -22
- data/lib/generators/action_agent/install_generator.rb +37 -0
- data/lib/generators/action_agent/templates/action_agent.rb.erb +158 -0
- data/lib/generators/action_agent/templates/migrations/015_add_provider_key_scope.rb.erb +72 -0
- data/lib/generators/action_agent/templates/migrations/016_create_active_agent_input_requests.rb.erb +85 -0
- data/lib/generators/action_agent/templates/migrations/017_create_active_agent_recording_events.rb.erb +57 -0
- data/lib/generators/action_agent/templates/migrations/018_add_browser_to_active_agent_sandbox_sessions.rb.erb +25 -0
- data/lib/generators/action_agent/templates/migrations/019_create_active_agent_projects.rb.erb +79 -0
- data/lib/generators/action_agent/templates/migrations/020_add_github_app_installations.rb.erb +62 -0
- data/lib/generators/action_agent/templates/migrations/021_create_active_agent_explorations.rb.erb +63 -0
- data/lib/generators/action_agent/templates/migrations/023_create_active_agent_draft_pull_requests.rb.erb +75 -0
- data/lib/generators/action_agent/templates/migrations/024_add_kind_to_active_agent_project_secrets.rb.erb +14 -0
- data/lib/generators/action_agent/templates/migrations/025_create_active_agent_scenario_catalogs.rb.erb +110 -0
- data/lib/generators/action_agent/templates/migrations/026_add_evaluation_fixes_to_code_sessions.rb.erb +21 -0
- data/lib/tasks/action_agent.rake +33 -0
- data/lib/tasks/browser.rake +22 -0
- data/lib/tasks/sandbox.rake +1 -1
- data/lib/tasks/scenario_catalogs.rake +71 -0
- 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
|
data/lib/generators/action_agent/templates/migrations/016_create_active_agent_input_requests.rb.erb
ADDED
|
@@ -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
|
data/lib/generators/action_agent/templates/migrations/021_create_active_agent_explorations.rb.erb
ADDED
|
@@ -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
|