actionagent 1.8.1 → 1.9.1
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/feedback_ingest_controller.rb +34 -0
- data/app/controllers/action_agent/api/feedbacks_controller.rb +40 -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/interaction_feedback.rb +24 -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 +155 -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/generators/action_agent/templates/migrations/027_create_active_agent_interaction_feedbacks.rb.erb +17 -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,
|
|
@@ -98,7 +109,9 @@ module ActionAgent
|
|
|
98
109
|
sandboxes: listed_sandboxes.map(&:summary),
|
|
99
110
|
code_sessions_supported: code_sessions_supported?,
|
|
100
111
|
codex_sessions_supported: codex_sessions_supported?,
|
|
112
|
+
pull_requests_supported: pull_requests_supported?,
|
|
101
113
|
codex_connected: owned(ProviderKey).where(provider: "codex").any? { |key| key.runtime_environment.present? },
|
|
114
|
+
browser_modes: browser_modes,
|
|
102
115
|
**claude_code_status
|
|
103
116
|
}
|
|
104
117
|
end
|
|
@@ -116,7 +129,14 @@ module ActionAgent
|
|
|
116
129
|
|
|
117
130
|
# POST /api/sandboxes
|
|
118
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`.
|
|
119
136
|
def create
|
|
137
|
+
boot = checkout_requested? ? boot_options : nil
|
|
138
|
+
return if performed?
|
|
139
|
+
|
|
120
140
|
@sandbox = SandboxSession.new(sandbox_params)
|
|
121
141
|
# Guarded: the association only exists when the host app configured a
|
|
122
142
|
# user model, and a single-user install configures none.
|
|
@@ -127,7 +147,7 @@ module ActionAgent
|
|
|
127
147
|
@sandbox.agent_template = AgentTemplate.find_by(slug: params[:template_slug]) if params[:template_slug]
|
|
128
148
|
|
|
129
149
|
if @sandbox.save
|
|
130
|
-
@sandbox.provision!
|
|
150
|
+
@sandbox.provision!(boot: boot)
|
|
131
151
|
@sandbox.reload # Reload to get updated status after provisioning
|
|
132
152
|
# Counted once the checkout exists, as MCPServersController#launch
|
|
133
153
|
# counts a launched server.
|
|
@@ -188,6 +208,84 @@ module ActionAgent
|
|
|
188
208
|
}, status: :accepted
|
|
189
209
|
end
|
|
190
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
|
+
|
|
191
289
|
# DELETE /api/sandboxes/:session_id
|
|
192
290
|
# End sandbox session. Expiring it enqueues SandboxCleanupJob, which
|
|
193
291
|
# terminates whatever the backend runs for it (a checkout's processes
|
|
@@ -208,6 +306,22 @@ module ActionAgent
|
|
|
208
306
|
params[:sandbox_type].to_s == "app_runtime"
|
|
209
307
|
end
|
|
210
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
|
+
|
|
211
325
|
# A checkout runs on its account's GitHub token and Claude Code
|
|
212
326
|
# credential, so in a multi-tenant install it is listed, shown and
|
|
213
327
|
# stopped only within the caller's current account, as
|
|
@@ -236,12 +350,29 @@ module ActionAgent
|
|
|
236
350
|
false
|
|
237
351
|
end
|
|
238
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
|
+
|
|
239
362
|
def codex_sessions_supported?
|
|
240
363
|
SandboxOrchestrator.new.supports_code_runner?("codex")
|
|
241
364
|
rescue StandardError, LoadError
|
|
242
365
|
false
|
|
243
366
|
end
|
|
244
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
|
+
|
|
245
376
|
# Whether the caller's Claude Code can run sessions, by
|
|
246
377
|
# ClaudeCodeAuth's rule: an API key they connected, or this machine's
|
|
247
378
|
# own login. Never a credential.
|
|
@@ -254,11 +385,12 @@ module ActionAgent
|
|
|
254
385
|
# SandboxSession#runtime_environment finds the credential it hands a
|
|
255
386
|
# checkout: a provider key is account-owned before user-owned.
|
|
256
387
|
def claude_code_status
|
|
257
|
-
status = ClaudeCodeAuth.status(owned(ProviderKey))
|
|
388
|
+
status = ClaudeCodeAuth.status(owned(ProviderKey), sandboxes: listed_sandboxes, user_id: current_user&.id)
|
|
258
389
|
{
|
|
259
390
|
claude_code_auth: status[:mode],
|
|
260
391
|
claude_code_connected: status[:connected],
|
|
261
|
-
claude_code_login: status[:login]
|
|
392
|
+
claude_code_login: status[:login],
|
|
393
|
+
claude_code_sandboxes: status[:sandboxes]
|
|
262
394
|
}.compact
|
|
263
395
|
end
|
|
264
396
|
|
|
@@ -274,7 +406,7 @@ module ActionAgent
|
|
|
274
406
|
# An app_runtime sandbox also names the checkout: one of the owner's
|
|
275
407
|
# selected GitHub repositories and, optionally, a ref.
|
|
276
408
|
def sandbox_params
|
|
277
|
-
params.permit(:sandbox_type, :repository, :repository_ref)
|
|
409
|
+
params.slice(:sandbox_type, :repository, :repository_ref).permit(:sandbox_type, :repository, :repository_ref)
|
|
278
410
|
end
|
|
279
411
|
|
|
280
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
|