actionagent 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +71 -12
- 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 +58 -7
- 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 +148 -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 +627 -20
- 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 +39 -10
- 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 +72 -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 +21 -5
- data/app/models/action_agent/draft_pull_request.rb +131 -0
- data/app/models/action_agent/evaluation.rb +148 -0
- data/app/models/action_agent/evaluation_run.rb +143 -53
- data/app/models/action_agent/evaluation_scenario_result.rb +15 -3
- 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/model_pricing.rb +214 -34
- 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 +207 -4
- 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 +300 -16
- 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/evaluation_serializer.rb +41 -10
- 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_scorecard.rb +50 -21
- 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/codex_session_events.rb +37 -0
- 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_report_import.rb +84 -5
- data/app/services/action_agent/evaluation_run_cost.rb +609 -0
- data/app/services/action_agent/evaluation_runner_service.rb +29 -10
- data/app/services/action_agent/evaluation_standing.rb +152 -0
- 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 +1263 -102
- 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 +290 -23
- 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 +121 -17
- 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 +153 -2
- 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 +513 -22
- data/lib/generators/action_agent/install_generator.rb +45 -5
- data/lib/generators/action_agent/templates/action_agent.rb.erb +161 -0
- data/lib/generators/action_agent/templates/add_code_session_runner.rb.erb +9 -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 +100 -2
|
@@ -11,12 +11,22 @@ module ActionAgent
|
|
|
11
11
|
# callback that refuses to serve an unauthenticated dashboard outside
|
|
12
12
|
# development, so it bypassed that safeguard too.
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
# What POST /api/sandboxes takes for a checkout's boot (see #create).
|
|
15
|
+
BOOT_OPTIONS = %i[bootstrap start_url keep_on_failure].freeze
|
|
16
|
+
BOOT_LOG_PAGE_BYTES = 64 * 1024
|
|
17
|
+
BOOT_LOG_MAX_PAGE_BYTES = 1024 * 1024
|
|
18
|
+
|
|
19
|
+
# Declared once per method: Rails replaces an earlier callback for the
|
|
20
|
+
# same method whatever its `only:`, so a second declaration would ungate
|
|
21
|
+
# the first one's actions.
|
|
22
|
+
before_action :require_execution_enabled!, only: [ :run, :compare, :resume_boot ]
|
|
15
23
|
# A checkout runs the owner's code (setup, server): the same gates as
|
|
16
24
|
# running an agent, like MCPServersController#launch.
|
|
17
25
|
before_action :gate_checkout!, only: [ :create ]
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
# A resume runs the checkout's code again, so it answers to the quota,
|
|
27
|
+
# but continues the boot its create already counted and spends nothing.
|
|
28
|
+
before_action :enforce_execution_quota!, only: [ :compare, :resume_boot ]
|
|
29
|
+
before_action :set_sandbox, only: [ :show, :run, :destroy, :boot, :boot_log, :resume_boot ]
|
|
20
30
|
|
|
21
31
|
# POST /api/sandboxes/compare
|
|
22
32
|
# Run multiple providers in a single sandbox using parallel generation jobs
|
|
@@ -88,7 +98,8 @@ module ActionAgent
|
|
|
88
98
|
# GET /api/sandboxes
|
|
89
99
|
# List available sandbox types and sample tasks, and the caller's own
|
|
90
100
|
# sandboxes (?sandbox_type= narrows them), with what the Settings ->
|
|
91
|
-
# Integrations view needs to offer Claude Code sessions
|
|
101
|
+
# Integrations view needs to offer Claude Code sessions and a browser
|
|
102
|
+
# in a checkout.
|
|
92
103
|
def index
|
|
93
104
|
render json: {
|
|
94
105
|
sandbox_types: SandboxSession::SANDBOX_TYPES,
|
|
@@ -97,6 +108,10 @@ module ActionAgent
|
|
|
97
108
|
sample_tasks: sample_tasks,
|
|
98
109
|
sandboxes: listed_sandboxes.map(&:summary),
|
|
99
110
|
code_sessions_supported: code_sessions_supported?,
|
|
111
|
+
codex_sessions_supported: codex_sessions_supported?,
|
|
112
|
+
pull_requests_supported: pull_requests_supported?,
|
|
113
|
+
codex_connected: owned(ProviderKey).where(provider: "codex").any? { |key| key.runtime_environment.present? },
|
|
114
|
+
browser_modes: browser_modes,
|
|
100
115
|
**claude_code_status
|
|
101
116
|
}
|
|
102
117
|
end
|
|
@@ -114,7 +129,14 @@ module ActionAgent
|
|
|
114
129
|
|
|
115
130
|
# POST /api/sandboxes
|
|
116
131
|
# Create a new sandbox session, owned by whoever opened it.
|
|
132
|
+
#
|
|
133
|
+
# A checkout also takes how it boots (see SandboxBootSpec.request_options):
|
|
134
|
+
# `bootstrap` ("auto", "always" or "never"), `start_url` and
|
|
135
|
+
# `keep_on_failure`.
|
|
117
136
|
def create
|
|
137
|
+
boot = checkout_requested? ? boot_options : nil
|
|
138
|
+
return if performed?
|
|
139
|
+
|
|
118
140
|
@sandbox = SandboxSession.new(sandbox_params)
|
|
119
141
|
# Guarded: the association only exists when the host app configured a
|
|
120
142
|
# user model, and a single-user install configures none.
|
|
@@ -125,7 +147,7 @@ module ActionAgent
|
|
|
125
147
|
@sandbox.agent_template = AgentTemplate.find_by(slug: params[:template_slug]) if params[:template_slug]
|
|
126
148
|
|
|
127
149
|
if @sandbox.save
|
|
128
|
-
@sandbox.provision!
|
|
150
|
+
@sandbox.provision!(boot: boot)
|
|
129
151
|
@sandbox.reload # Reload to get updated status after provisioning
|
|
130
152
|
# Counted once the checkout exists, as MCPServersController#launch
|
|
131
153
|
# counts a launched server.
|
|
@@ -186,6 +208,84 @@ module ActionAgent
|
|
|
186
208
|
}, status: :accepted
|
|
187
209
|
end
|
|
188
210
|
|
|
211
|
+
# GET /api/sandboxes/:session_id/boot
|
|
212
|
+
# How a checkout's boot went, step by step (see
|
|
213
|
+
# SandboxOrchestrator#boot_status). `boot` is null when the backend
|
|
214
|
+
# reports no steps, or holds nothing for the sandbox.
|
|
215
|
+
def boot
|
|
216
|
+
orchestrator = SandboxOrchestrator.new
|
|
217
|
+
status = @sandbox.app_runtime? && orchestrator.supports?(:boot_status) ? orchestrator.boot_status(@sandbox) : nil
|
|
218
|
+
# A project's checkout booted with its secrets, which the backend may
|
|
219
|
+
# not know to mask.
|
|
220
|
+
status = SecretScrubber.scrub(status, @sandbox.project_scrub_values) if status
|
|
221
|
+
render json: {
|
|
222
|
+
boot: status,
|
|
223
|
+
resumable: !!(status&.dig(:kept) && @sandbox.failed? && !@sandbox.past_expiry? && orchestrator.supports?(:resume_boot)),
|
|
224
|
+
logs: @sandbox.app_runtime? && orchestrator.supports?(:boot_log)
|
|
225
|
+
}
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# GET /api/sandboxes/:session_id/boot_log?step=NAME&offset=N&limit=N
|
|
229
|
+
# One page of a boot step's log, scrubbed (see
|
|
230
|
+
# SandboxOrchestrator#boot_log). Read on from `next_offset` until
|
|
231
|
+
# `eof`.
|
|
232
|
+
def boot_log
|
|
233
|
+
orchestrator = SandboxOrchestrator.new
|
|
234
|
+
unless @sandbox.app_runtime? && orchestrator.supports?(:boot_log)
|
|
235
|
+
return render json: { error: "This sandbox backend keeps no boot logs" }, status: :not_found
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
step = params[:step]
|
|
239
|
+
return render json: { error: "step is required" }, status: :bad_request unless step.is_a?(String) && step.present?
|
|
240
|
+
|
|
241
|
+
project_secrets = @sandbox.project_scrub_values
|
|
242
|
+
page = orchestrator.boot_log(@sandbox, step: step, offset: clamped_param(:offset, default: 0, min: 0, max: 2**62),
|
|
243
|
+
limit: clamped_param(:limit, default: BOOT_LOG_PAGE_BYTES, min: 1, max: BOOT_LOG_MAX_PAGE_BYTES), secrets: project_secrets)
|
|
244
|
+
return render json: { error: "No log for step #{step}" }, status: :not_found if page.nil?
|
|
245
|
+
|
|
246
|
+
render json: project_secrets.any? ? page.merge(text: SecretScrubber.scrub(page[:text], project_secrets)) : page
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# POST /api/sandboxes/:session_id/resume_boot
|
|
250
|
+
# Continues a checkout whose boot failed and was kept (`keep_on_failure`),
|
|
251
|
+
# from the step named `from`, or from the one that failed. The sandbox
|
|
252
|
+
# is provisioning again, and is polled until ready or failed as after
|
|
253
|
+
# #create.
|
|
254
|
+
def resume_boot
|
|
255
|
+
orchestrator = SandboxOrchestrator.new
|
|
256
|
+
unless @sandbox.app_runtime? && orchestrator.supports?(:resume_boot)
|
|
257
|
+
return render json: { error: "This sandbox backend cannot resume a boot" }, status: :unprocessable_entity
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
from = params[:from]
|
|
261
|
+
unless from.nil? || from.is_a?(String)
|
|
262
|
+
return render json: { error: "from must be a step name" }, status: :bad_request
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
status = orchestrator.boot_status(@sandbox) if orchestrator.supports?(:boot_status)
|
|
266
|
+
if orchestrator.supports?(:boot_status) && !status&.dig(:kept)
|
|
267
|
+
return render json: { error: "This sandbox kept no failed boot to resume: start it again", sandbox: @sandbox.summary },
|
|
268
|
+
status: :unprocessable_entity
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# Refused here rather than by the backend, whose refusal would fail
|
|
272
|
+
# the sandbox again and replace the error the failed boot left.
|
|
273
|
+
steps = status&.dig(:resumable_steps)
|
|
274
|
+
if from.present? && steps.is_a?(Array) && !steps.include?(from)
|
|
275
|
+
return render json: { error: "#{from.inspect} is not a step this boot can resume from (#{steps.join(", ")})" },
|
|
276
|
+
status: :unprocessable_entity
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
unless @sandbox.resume_boot!(from: from)
|
|
280
|
+
return render json: {
|
|
281
|
+
error: "Only a failed checkout that has not expired can be resumed: start it again",
|
|
282
|
+
sandbox: @sandbox.summary
|
|
283
|
+
}, status: :unprocessable_entity
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
render json: { sandbox: @sandbox.reload.summary }, status: :accepted
|
|
287
|
+
end
|
|
288
|
+
|
|
189
289
|
# DELETE /api/sandboxes/:session_id
|
|
190
290
|
# End sandbox session. Expiring it enqueues SandboxCleanupJob, which
|
|
191
291
|
# terminates whatever the backend runs for it (a checkout's processes
|
|
@@ -206,6 +306,22 @@ module ActionAgent
|
|
|
206
306
|
params[:sandbox_type].to_s == "app_runtime"
|
|
207
307
|
end
|
|
208
308
|
|
|
309
|
+
# A checkout's boot options, or a rendered 422 when they are malformed
|
|
310
|
+
# or ask for a bootstrap the backend cannot do.
|
|
311
|
+
def boot_options
|
|
312
|
+
raw = params.slice(*BOOT_OPTIONS).permit(*BOOT_OPTIONS).to_h.symbolize_keys
|
|
313
|
+
options = SandboxBootSpec.request_options(**raw)
|
|
314
|
+
if options["bootstrap"] == "always" && !SandboxOrchestrator.new.accepts_boot_config?
|
|
315
|
+
render json: { errors: [ "This sandbox backend cannot bootstrap a checkout" ] }, status: :unprocessable_entity
|
|
316
|
+
return
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
options
|
|
320
|
+
rescue SandboxBootSpec::Invalid => e
|
|
321
|
+
render json: { errors: [ e.message ] }, status: :unprocessable_entity
|
|
322
|
+
nil
|
|
323
|
+
end
|
|
324
|
+
|
|
209
325
|
# A checkout runs on its account's GitHub token and Claude Code
|
|
210
326
|
# credential, so in a multi-tenant install it is listed, shown and
|
|
211
327
|
# stopped only within the caller's current account, as
|
|
@@ -234,6 +350,29 @@ module ActionAgent
|
|
|
234
350
|
false
|
|
235
351
|
end
|
|
236
352
|
|
|
353
|
+
# The modes a checkout's browser can start in on the configured
|
|
354
|
+
# backend (SandboxBrowser): none when it runs no browsers, or cannot be
|
|
355
|
+
# loaded.
|
|
356
|
+
def browser_modes
|
|
357
|
+
SandboxOrchestrator.new.browser_modes.map(&:to_s)
|
|
358
|
+
rescue StandardError, LoadError
|
|
359
|
+
[]
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def codex_sessions_supported?
|
|
363
|
+
SandboxOrchestrator.new.supports_code_runner?("codex")
|
|
364
|
+
rescue StandardError, LoadError
|
|
365
|
+
false
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Whether the configured backend can read a checkout's changes, which
|
|
369
|
+
# opening a pull request and downloading a patch both need.
|
|
370
|
+
def pull_requests_supported?
|
|
371
|
+
SandboxOrchestrator.new.reads_checkouts?
|
|
372
|
+
rescue StandardError, LoadError
|
|
373
|
+
false
|
|
374
|
+
end
|
|
375
|
+
|
|
237
376
|
# Whether the caller's Claude Code can run sessions, by
|
|
238
377
|
# ClaudeCodeAuth's rule: an API key they connected, or this machine's
|
|
239
378
|
# own login. Never a credential.
|
|
@@ -246,11 +385,12 @@ module ActionAgent
|
|
|
246
385
|
# SandboxSession#runtime_environment finds the credential it hands a
|
|
247
386
|
# checkout: a provider key is account-owned before user-owned.
|
|
248
387
|
def claude_code_status
|
|
249
|
-
status = ClaudeCodeAuth.status(owned(ProviderKey))
|
|
388
|
+
status = ClaudeCodeAuth.status(owned(ProviderKey), sandboxes: listed_sandboxes, user_id: current_user&.id)
|
|
250
389
|
{
|
|
251
390
|
claude_code_auth: status[:mode],
|
|
252
391
|
claude_code_connected: status[:connected],
|
|
253
|
-
claude_code_login: status[:login]
|
|
392
|
+
claude_code_login: status[:login],
|
|
393
|
+
claude_code_sandboxes: status[:sandboxes]
|
|
254
394
|
}.compact
|
|
255
395
|
end
|
|
256
396
|
|
|
@@ -266,7 +406,7 @@ module ActionAgent
|
|
|
266
406
|
# An app_runtime sandbox also names the checkout: one of the owner's
|
|
267
407
|
# selected GitHub repositories and, optionally, a ref.
|
|
268
408
|
def sandbox_params
|
|
269
|
-
params.permit(:sandbox_type, :repository, :repository_ref)
|
|
409
|
+
params.slice(:sandbox_type, :repository, :repository_ref).permit(:sandbox_type, :repository, :repository_ref)
|
|
270
410
|
end
|
|
271
411
|
|
|
272
412
|
def free_tier_templates
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
module Api
|
|
5
|
+
# Editing a scenario catalog in the dashboard, one entry at a time: its
|
|
6
|
+
# products, their sets, and the sets' scenarios. Each change is checked
|
|
7
|
+
# as the YAML importer checks a document (the records' validations, then
|
|
8
|
+
# the catalog's document rebuilt and parsed), rewrites the catalog's
|
|
9
|
+
# document and digest, and writes it to Active Storage when the host has
|
|
10
|
+
# it, so an edit and an import leave the same thing behind.
|
|
11
|
+
#
|
|
12
|
+
# Every change needs :replace_scenarios on the catalog, as an import does.
|
|
13
|
+
# A key left blank is made from the name (or a scenario's prompt).
|
|
14
|
+
class ScenarioCatalogEntriesController < BaseController
|
|
15
|
+
before_action :require_owner!
|
|
16
|
+
before_action :set_catalog
|
|
17
|
+
before_action -> { authorize_action!(:replace_scenarios, @catalog) }
|
|
18
|
+
|
|
19
|
+
rescue_from ActiveRecord::RecordInvalid do |error|
|
|
20
|
+
render json: { errors: error.record.errors.full_messages }, status: :unprocessable_entity
|
|
21
|
+
end
|
|
22
|
+
rescue_from ActiveAgent::Evals::Catalog::InvalidDocument do |error|
|
|
23
|
+
render json: { errors: [ error.message ] }, status: :unprocessable_entity
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# POST /api/scenario_catalogs/:id/products
|
|
27
|
+
# { product: { key, name, description, agent_id, agent_name, project_id } }
|
|
28
|
+
def create_product
|
|
29
|
+
attributes = product_attributes
|
|
30
|
+
attributes[:key] = ScenarioCatalog.key_for(attributes[:name], @catalog.products.pluck(:key)) if attributes[:key].blank?
|
|
31
|
+
attributes[:name] = attributes[:key] if attributes[:name].blank?
|
|
32
|
+
product = change { @catalog.products.create!(attributes.merge(position: next_position(@catalog.products))) }
|
|
33
|
+
render json: { product: product.summary, catalog: full }, status: :created
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# PATCH /api/scenario_catalogs/:id/products/:product_id
|
|
37
|
+
def update_product
|
|
38
|
+
product = find_product
|
|
39
|
+
change { product.update!(product_attributes.compact_blank.merge(product_links)) }
|
|
40
|
+
render json: { product: product.reload.summary, catalog: full }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# DELETE /api/scenario_catalogs/:id/products/:product_id
|
|
44
|
+
# The evaluations its sets became stay, with their runs.
|
|
45
|
+
def destroy_product
|
|
46
|
+
product = find_product
|
|
47
|
+
change { product.destroy! }
|
|
48
|
+
render json: { catalog: full }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# POST /api/scenario_catalogs/:id/products/:product_id/sets
|
|
52
|
+
# { set: { key, name, description, judge: { kind, model } } }
|
|
53
|
+
def create_set
|
|
54
|
+
product = find_product
|
|
55
|
+
attributes = set_attributes
|
|
56
|
+
attributes[:key] = ScenarioCatalog.key_for(attributes[:name], product.sets.pluck(:key)) if attributes[:key].blank?
|
|
57
|
+
attributes[:name] = attributes[:key] if attributes[:name].blank?
|
|
58
|
+
set = change { product.sets.create!(attributes.merge(position: next_position(product.sets))) }
|
|
59
|
+
render json: { set: set.summary, catalog: full }, status: :created
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# PATCH /api/scenario_catalogs/:id/sets/:set_id
|
|
63
|
+
def update_set
|
|
64
|
+
set = find_set
|
|
65
|
+
change { set.update!(set_attributes.compact_blank) }
|
|
66
|
+
render json: { set: set.reload.summary, catalog: full }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# DELETE /api/scenario_catalogs/:id/sets/:set_id
|
|
70
|
+
def destroy_set
|
|
71
|
+
set = find_set
|
|
72
|
+
change { set.destroy! }
|
|
73
|
+
render json: { catalog: full }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# POST /api/scenario_catalogs/:id/sets/:set_id/scenarios
|
|
77
|
+
# { scenario: { key, prompt, notes, expect: { tools, contains, not_contains }, tags, production_only, enabled } }
|
|
78
|
+
def create_scenario
|
|
79
|
+
set = find_set
|
|
80
|
+
attributes = scenario_attributes
|
|
81
|
+
attributes[:key] = ScenarioCatalog.key_for(attributes[:prompt].to_s.split.first(6).join(" "), set.scenarios.pluck(:key)) if attributes[:key].blank?
|
|
82
|
+
if @catalog.scenario_count >= ScenarioCatalog::MAX_SCENARIOS
|
|
83
|
+
return render json: { errors: [ "A catalog holds at most #{ScenarioCatalog::MAX_SCENARIOS} scenarios" ] }, status: :unprocessable_entity
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
scenario = change { set.scenarios.create!(attributes.merge(position: next_position(set.scenarios))) }
|
|
87
|
+
render json: { scenario: scenario.summary, catalog: full }, status: :created
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# PATCH /api/scenario_catalogs/:id/sets/:set_id/scenarios/:scenario_id
|
|
91
|
+
def update_scenario
|
|
92
|
+
scenario = find_set.scenarios.find(params[:scenario_id])
|
|
93
|
+
change { scenario.update!(scenario_attributes.except(*blank_identity(scenario_attributes))) }
|
|
94
|
+
render json: { scenario: scenario.reload.summary, catalog: full }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# DELETE /api/scenario_catalogs/:id/sets/:set_id/scenarios/:scenario_id
|
|
98
|
+
def destroy_scenario
|
|
99
|
+
scenario = find_set.scenarios.find(params[:scenario_id])
|
|
100
|
+
change { scenario.destroy! }
|
|
101
|
+
render json: { catalog: full }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private
|
|
105
|
+
|
|
106
|
+
def set_catalog
|
|
107
|
+
@catalog = owned(ScenarioCatalog).find(params[:id])
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def find_product
|
|
111
|
+
@catalog.products.find(params[:product_id])
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def find_set
|
|
115
|
+
ScenarioSet.where(scenario_product_id: @catalog.products.select(:id)).find(params[:set_id])
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Runs +block+ and rebuilds the catalog's document in one transaction,
|
|
119
|
+
# so a change the document cannot hold is rolled back; then writes the
|
|
120
|
+
# document to storage. Returns the block's value.
|
|
121
|
+
def change
|
|
122
|
+
result = ScenarioCatalog.transaction do
|
|
123
|
+
value = yield
|
|
124
|
+
@catalog.reload.refresh_document!
|
|
125
|
+
value
|
|
126
|
+
end
|
|
127
|
+
@catalog.sync_to_storage!
|
|
128
|
+
result
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def full
|
|
132
|
+
ScenarioCatalogSerializer.full(@catalog.reload)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def next_position(relation)
|
|
136
|
+
(relation.maximum(:position) || -1) + 1
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def product_attributes
|
|
140
|
+
params.require(:product).permit(:key, :name, :description, :agent_name).to_h.symbolize_keys.merge(product_links)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# The agent and project a product names by id, among the owner's own.
|
|
144
|
+
# An empty id clears it.
|
|
145
|
+
def product_links
|
|
146
|
+
source = params[:product] || {}
|
|
147
|
+
links = {}
|
|
148
|
+
if source.key?(:agent_id)
|
|
149
|
+
links[:agent] = source[:agent_id].present? ? owner_agents.find(source[:agent_id]) : nil
|
|
150
|
+
end
|
|
151
|
+
if source.key?(:project_id)
|
|
152
|
+
links[:project] = source[:project_id].present? ? owned(Project).find(source[:project_id]) : nil
|
|
153
|
+
end
|
|
154
|
+
links
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def set_attributes
|
|
158
|
+
source = params.require(:set)
|
|
159
|
+
attributes = source.permit(:key, :name, :description).to_h.symbolize_keys
|
|
160
|
+
if source.key?(:judge)
|
|
161
|
+
judge = source[:judge].respond_to?(:permit) ? source[:judge].permit(:kind, :model).to_h.compact_blank : {}
|
|
162
|
+
attributes[:judge] = judge
|
|
163
|
+
end
|
|
164
|
+
attributes
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def scenario_attributes
|
|
168
|
+
source = params.require(:scenario)
|
|
169
|
+
attributes = source.permit(:key, :prompt, :notes, :production_only, :enabled).to_h.symbolize_keys
|
|
170
|
+
expect = source[:expect] || source[:expectations]
|
|
171
|
+
if expect.respond_to?(:permit)
|
|
172
|
+
attributes[:expectations] = Exploration::EXPECTATION_KEYS.each_with_object({}) do |name, hash|
|
|
173
|
+
values = Array(expect[name]).flat_map { |value| value.to_s.split(",") }.map(&:strip).reject(&:blank?)
|
|
174
|
+
hash[name] = values if values.any?
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
attributes[:tags] = Array(source[:tags]).flat_map { |tag| tag.to_s.split(",") }.map(&:strip).reject(&:blank?) if source.key?(:tags)
|
|
178
|
+
attributes
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# An update leaves the key and prompt alone when they arrive blank,
|
|
182
|
+
# rather than failing presence.
|
|
183
|
+
def blank_identity(attributes)
|
|
184
|
+
%i[key prompt].select { |name| attributes.key?(name) && attributes[name].blank? }
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|