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
data/config/routes.rb
CHANGED
|
@@ -71,6 +71,15 @@ ActionAgent::Engine.routes.draw do
|
|
|
71
71
|
end
|
|
72
72
|
end
|
|
73
73
|
|
|
74
|
+
# What paused runs are waiting on a person for, and the answers that
|
|
75
|
+
# resume them.
|
|
76
|
+
resources :input_requests, only: [ :index ] do
|
|
77
|
+
member do
|
|
78
|
+
post :answer
|
|
79
|
+
post :decline
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
74
83
|
# Sandboxes. The engine ships the in-memory and local backends; an
|
|
75
84
|
# operator registers the rest (see ActionAgent.sandbox_backends).
|
|
76
85
|
resources :sandboxes, param: :id, only: [ :index, :create, :show, :destroy ] do
|
|
@@ -79,6 +88,11 @@ ActionAgent::Engine.routes.draw do
|
|
|
79
88
|
end
|
|
80
89
|
member do
|
|
81
90
|
post :run
|
|
91
|
+
# A checkout's boot: its steps, a step's log, and continuing a
|
|
92
|
+
# failed boot the backend kept.
|
|
93
|
+
get :boot
|
|
94
|
+
get :boot_log
|
|
95
|
+
post :resume_boot
|
|
82
96
|
end
|
|
83
97
|
# Claude Code sessions in an app_runtime sandbox's checkout.
|
|
84
98
|
resources :code_sessions, only: [ :index, :create, :show ] do
|
|
@@ -86,6 +100,71 @@ ActionAgent::Engine.routes.draw do
|
|
|
86
100
|
post :cancel
|
|
87
101
|
end
|
|
88
102
|
end
|
|
103
|
+
resource :claude_login, only: [ :show, :create, :destroy ], controller: "claude_logins" do
|
|
104
|
+
post :code
|
|
105
|
+
end
|
|
106
|
+
# The checkout's browser (SandboxBrowser), and tickets into its live view.
|
|
107
|
+
resource :browser, only: [ :show, :create, :destroy ], controller: "sandbox_browsers" do
|
|
108
|
+
resources :tickets, only: [ :create ], controller: "sandbox_browser_tickets"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# A pull request opened from the checkout's changes, its preview, and
|
|
112
|
+
# the patch to download instead.
|
|
113
|
+
resource :pull_request, only: [ :show, :create ], controller: "draft_pull_requests" do
|
|
114
|
+
post :preview
|
|
115
|
+
get :patch
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
get "evaluations/:evaluation_id/runs/:run_id/fixes", to: "evaluation_fixes#index"
|
|
119
|
+
|
|
120
|
+
# Projects: a repository booted in a checkout sandbox and evaluated,
|
|
121
|
+
# with the secrets its boot needs (names only in responses).
|
|
122
|
+
resources :projects, only: [ :index, :show, :create, :update, :destroy ] do
|
|
123
|
+
collection do
|
|
124
|
+
get :capabilities
|
|
125
|
+
get :preflight
|
|
126
|
+
get :discover_secrets
|
|
127
|
+
end
|
|
128
|
+
member do
|
|
129
|
+
post :boot
|
|
130
|
+
get :boot, action: :boot_status
|
|
131
|
+
get :boot_log
|
|
132
|
+
get :synced_agents
|
|
133
|
+
patch :target
|
|
134
|
+
post :run_evaluation
|
|
135
|
+
end
|
|
136
|
+
put :secrets, to: "project_secrets#upsert"
|
|
137
|
+
resources :secrets, controller: "project_secrets", only: [ :index, :update, :destroy ], param: :name
|
|
138
|
+
# The setup assistant, the requests for input waiting on the project's
|
|
139
|
+
# agents, and the models the App assistant may read.
|
|
140
|
+
post :setup, to: "project_setup#start"
|
|
141
|
+
post "setup/claude_code", to: "project_setup#fix_with_claude_code", as: :setup_claude_code
|
|
142
|
+
patch :setup, to: "project_setup#update"
|
|
143
|
+
get :input_requests, to: "project_setup#input_requests"
|
|
144
|
+
get :app_models, to: "project_setup#app_models"
|
|
145
|
+
put :schema_tools, to: "project_setup#schema_tools"
|
|
146
|
+
# The pull request that installs the engine in the repository.
|
|
147
|
+
resource :install_pull_request, only: [ :show, :create ], controller: "project_install_pull_requests" do
|
|
148
|
+
post :preview
|
|
149
|
+
get :patch
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# The explorer agent's start, and how the project's browser signs in.
|
|
153
|
+
resources :explorations, controller: "project_explorations", only: [ :create ]
|
|
154
|
+
resource :sign_in, controller: "project_sign_ins", only: [ :show, :update, :destroy ] do
|
|
155
|
+
post :check
|
|
156
|
+
post :save_browser
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Explorations: candidate scenarios found by walking a project's app,
|
|
161
|
+
# reviewed and accepted into its evaluation (see Exploration).
|
|
162
|
+
resources :explorations, only: [ :index, :show, :create ] do
|
|
163
|
+
member do
|
|
164
|
+
post :accept
|
|
165
|
+
post :stop
|
|
166
|
+
patch "candidates/:candidate_id", action: :update_candidate, as: :candidate
|
|
167
|
+
end
|
|
89
168
|
end
|
|
90
169
|
|
|
91
170
|
# Tool inventory — auto-detected from the tool roster each generation
|
|
@@ -108,8 +187,17 @@ ActionAgent::Engine.routes.draw do
|
|
|
108
187
|
end
|
|
109
188
|
end
|
|
110
189
|
|
|
111
|
-
|
|
190
|
+
# create is the Run Agent workbench's recording of a conversation.
|
|
191
|
+
resources :session_recordings, only: [ :index, :show, :create, :destroy ] do
|
|
112
192
|
member do
|
|
193
|
+
# A recorder holding the recording's ingest token posts its events
|
|
194
|
+
# here without a dashboard session; any other post is a dashboard
|
|
195
|
+
# session's, checked for forgery like the rest of the API.
|
|
196
|
+
post :events, to: "recording_event_ingest#create",
|
|
197
|
+
constraints: ->(request) { request.authorization.to_s.match?(/\ABearer\s/i) }
|
|
198
|
+
post :events, action: :create_events
|
|
199
|
+
get :events
|
|
200
|
+
get :timeline
|
|
113
201
|
get :actions
|
|
114
202
|
get "snapshot/:action_id", action: :snapshot, as: :snapshot
|
|
115
203
|
post :export
|
|
@@ -123,6 +211,12 @@ ActionAgent::Engine.routes.draw do
|
|
|
123
211
|
end
|
|
124
212
|
end
|
|
125
213
|
|
|
214
|
+
# The sessions a caller can replay, and the timeline of a conversation,
|
|
215
|
+
# a run or an evaluation scenario's replay, with or without a recording.
|
|
216
|
+
get "sessions", to: "sessions#index", as: :sessions
|
|
217
|
+
get "sessions/:kind/:id/timeline", to: "sessions#timeline", as: :session_timeline,
|
|
218
|
+
constraints: { kind: /context|run|scenario_result/ }
|
|
219
|
+
|
|
126
220
|
resource :analytics, only: [], controller: "analytics" do
|
|
127
221
|
get "/", action: :index
|
|
128
222
|
end
|
|
@@ -142,7 +236,7 @@ ActionAgent::Engine.routes.draw do
|
|
|
142
236
|
|
|
143
237
|
# Agent output evaluations. A scenario suite also manages its scenarios
|
|
144
238
|
# here, and exposes each run's per-scenario, per-model results.
|
|
145
|
-
resources :evaluations, only: [ :index, :show, :create, :destroy ] do
|
|
239
|
+
resources :evaluations, only: [ :index, :show, :create, :update, :destroy ] do
|
|
146
240
|
member do
|
|
147
241
|
post :run
|
|
148
242
|
get "runs/:run_id", action: :show_run, as: :run_result
|
|
@@ -154,6 +248,33 @@ ActionAgent::Engine.routes.draw do
|
|
|
154
248
|
end
|
|
155
249
|
end
|
|
156
250
|
|
|
251
|
+
# Scenario catalogs: YAML catalogs of scenarios (products, sets), imported
|
|
252
|
+
# from a document, a file or a connected repository, exported as YAML,
|
|
253
|
+
# synced to Active Storage, and run one set at a time as an evaluation.
|
|
254
|
+
# The dashboard's Scenarios page edits them entry by entry
|
|
255
|
+
# (ScenarioCatalogEntriesController) and previews an import before it
|
|
256
|
+
# is applied.
|
|
257
|
+
resources :scenario_catalogs, only: [ :index, :show, :create, :update, :destroy ] do
|
|
258
|
+
post :preview, on: :collection
|
|
259
|
+
member do
|
|
260
|
+
get :export
|
|
261
|
+
post :sync
|
|
262
|
+
post "sets/:set_id/run", action: :run_set, as: :run_set
|
|
263
|
+
post "sets/:set_id/materialize", action: :materialize_set, as: :materialize_set
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
scope "scenario_catalogs/:id", controller: "scenario_catalog_entries", as: :scenario_catalog do
|
|
267
|
+
post "products", action: :create_product, as: :products
|
|
268
|
+
patch "products/:product_id", action: :update_product, as: :product
|
|
269
|
+
delete "products/:product_id", action: :destroy_product
|
|
270
|
+
post "products/:product_id/sets", action: :create_set, as: :product_sets
|
|
271
|
+
patch "sets/:set_id", action: :update_set, as: :set
|
|
272
|
+
delete "sets/:set_id", action: :destroy_set
|
|
273
|
+
post "sets/:set_id/scenarios", action: :create_scenario, as: :set_scenarios
|
|
274
|
+
patch "sets/:set_id/scenarios/:scenario_id", action: :update_scenario, as: :set_scenario
|
|
275
|
+
delete "sets/:set_id/scenarios/:scenario_id", action: :destroy_scenario
|
|
276
|
+
end
|
|
277
|
+
|
|
157
278
|
# Credentials: dashboard API keys (token shown once on create) and the
|
|
158
279
|
# owner's own LLM provider credentials, both encrypted at rest.
|
|
159
280
|
resources :api_keys, only: [ :index, :create, :destroy ]
|
|
@@ -172,6 +293,24 @@ ActionAgent::Engine.routes.draw do
|
|
|
172
293
|
get :callback
|
|
173
294
|
end
|
|
174
295
|
|
|
296
|
+
# The owner's GitHub App installations: install sends the admin to GitHub,
|
|
297
|
+
# which returns to callback; each installation's repositories are chosen
|
|
298
|
+
# with PATCH, and DELETE unlinks one (the App stays installed on GitHub).
|
|
299
|
+
resources :github_installations, only: [ :index, :update, :destroy ] do
|
|
300
|
+
collection do
|
|
301
|
+
get :install
|
|
302
|
+
get :callback
|
|
303
|
+
end
|
|
304
|
+
member do
|
|
305
|
+
get :repositories
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# Creating the GitHub App from a manifest on a self-hosted dashboard.
|
|
310
|
+
resource :github_app_manifest, only: [ :create ] do
|
|
311
|
+
get :callback
|
|
312
|
+
end
|
|
313
|
+
|
|
175
314
|
# Model catalogs for the agent builder (Ollama queried live from the
|
|
176
315
|
# configured host; hosted providers curated).
|
|
177
316
|
resources :provider_models, only: [ :index ]
|
|
@@ -181,6 +320,14 @@ ActionAgent::Engine.routes.draw do
|
|
|
181
320
|
# against a plan answers through ActionAgent.usage_resolver, and a bare
|
|
182
321
|
# mount reports unlimited rather than 404.
|
|
183
322
|
resource :usage, only: [ :show ], controller: "usage"
|
|
323
|
+
|
|
324
|
+
# The Organization view's Team Members table. The host lists them
|
|
325
|
+
# through ActionAgent.members_resolver; without one it is the signed-in
|
|
326
|
+
# user alone.
|
|
327
|
+
resources :members, only: [ :index ]
|
|
328
|
+
|
|
329
|
+
# The owner's trace ingest key, read by the Organization view.
|
|
330
|
+
resource :telemetry_key, only: [ :show ], controller: "telemetry_keys"
|
|
184
331
|
end
|
|
185
332
|
|
|
186
333
|
# The account's agents presented as an authenticated MCP server (tools +
|
|
@@ -202,6 +349,10 @@ ActionAgent::Engine.routes.draw do
|
|
|
202
349
|
match "mcp", to: "api/mcp#unsupported", via: [ :get, :delete ],
|
|
203
350
|
constraints: ->(request) { request.delete? || !ActionAgent::Engine.html_request?(request) }
|
|
204
351
|
|
|
352
|
+
# The frame the session player runs in, holding the replay bundle and no
|
|
353
|
+
# data. See SessionPlayerController.
|
|
354
|
+
get "session_player", to: "session_player#show", as: :session_player
|
|
355
|
+
|
|
205
356
|
# Everything else under the mount is a client-side route: render the
|
|
206
357
|
# dashboard and let the browser resolve it. Anchored last so it can only
|
|
207
358
|
# ever catch what the routes above did not, and refuses /api paths so a
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# A standalone PTY supervisor, deliberately without Rails, a logger, or a DB
|
|
4
|
+
# connection. The one-use FIFO carries the pasted code only in kernel memory.
|
|
5
|
+
# Files contain public flow state and the CLI's authorize URL, never output,
|
|
6
|
+
# codes, credentials, or the PKCE verifier. A separate process lets requests
|
|
7
|
+
# arrive at different web workers without losing the PTY.
|
|
8
|
+
require "pty"
|
|
9
|
+
require "io/console"
|
|
10
|
+
require "json"
|
|
11
|
+
require "fileutils"
|
|
12
|
+
require "uri"
|
|
13
|
+
|
|
14
|
+
directory, command, duration = ARGV
|
|
15
|
+
directory = File.expand_path(directory)
|
|
16
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + Float(duration)
|
|
17
|
+
status_path = File.join(directory, "status.json")
|
|
18
|
+
fifo_path = File.join(directory, "code.pipe")
|
|
19
|
+
write_status = lambda do |status, url = nil|
|
|
20
|
+
data = { status: status }
|
|
21
|
+
data[:authorize_url] = url if url
|
|
22
|
+
temporary = "#{status_path}.tmp"
|
|
23
|
+
File.write(temporary, JSON.generate(data), mode: "w", perm: 0o600)
|
|
24
|
+
File.rename(temporary, status_path)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
reader = writer = pipe = nil
|
|
28
|
+
pid = nil
|
|
29
|
+
cancelled = false
|
|
30
|
+
completed = false
|
|
31
|
+
Signal.trap("TERM") { cancelled = true }
|
|
32
|
+
Signal.trap("INT") { cancelled = true }
|
|
33
|
+
|
|
34
|
+
begin
|
|
35
|
+
File.umask(0o077)
|
|
36
|
+
File.mkfifo(fifo_path, 0o600)
|
|
37
|
+
pipe = File.open(fifo_path, File::RDWR | File::NONBLOCK)
|
|
38
|
+
reader, writer, pid = PTY.spawn(command, "auth", "login", chdir: ENV.fetch("CLAUDE_CONFIG_DIR"))
|
|
39
|
+
reader.winsize = [ 40, 4096 ]
|
|
40
|
+
reader.echo = false
|
|
41
|
+
write_status.call("starting")
|
|
42
|
+
buffer = +""
|
|
43
|
+
submitted = false
|
|
44
|
+
loop do
|
|
45
|
+
break if cancelled
|
|
46
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
47
|
+
write_status.call("expired")
|
|
48
|
+
break
|
|
49
|
+
end
|
|
50
|
+
exited = Process.waitpid2(pid, Process::WNOHANG)
|
|
51
|
+
if exited
|
|
52
|
+
completed = exited.last.success?
|
|
53
|
+
write_status.call(exited.last.success? ? "completed" : "failed")
|
|
54
|
+
break
|
|
55
|
+
end
|
|
56
|
+
readable = IO.select([ reader, pipe ], nil, nil, 0.1)&.first || []
|
|
57
|
+
if readable.include?(pipe) && !submitted
|
|
58
|
+
code = pipe.read_nonblock(4096, exception: false)
|
|
59
|
+
if code.is_a?(String) && code.match?(/\A[^\s\x00-\x1f\x7f]{1,2048}\n\z/)
|
|
60
|
+
submitted = true
|
|
61
|
+
# No subsequent PTY output is parsed or retained. Even an echoing or
|
|
62
|
+
# failing CLI cannot leak this code through the public flow state.
|
|
63
|
+
buffer.clear
|
|
64
|
+
write_status.call("submitted")
|
|
65
|
+
writer.write(code)
|
|
66
|
+
writer.flush
|
|
67
|
+
code.clear
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
next unless readable.include?(reader)
|
|
71
|
+
|
|
72
|
+
begin
|
|
73
|
+
chunk = reader.read_nonblock(8192, exception: false)
|
|
74
|
+
next unless chunk.is_a?(String) && !submitted
|
|
75
|
+
|
|
76
|
+
buffer << chunk.force_encoding("UTF-8").scrub
|
|
77
|
+
buffer = buffer[-32_768..] || buffer
|
|
78
|
+
clean = buffer.gsub(/\e\[[0-9;?]*[A-Za-z]/, "")
|
|
79
|
+
url = clean[%r{https://(?:claude\.ai|platform\.claude\.com)/oauth/authorize\?[^\s\e]+}]
|
|
80
|
+
if url && URI.parse(url).query.to_s.include?("client_id=")
|
|
81
|
+
write_status.call("awaiting_code", url)
|
|
82
|
+
end
|
|
83
|
+
rescue Errno::EIO, EOFError
|
|
84
|
+
# A PTY reports EIO when the CLI exits. Reap it on the next iteration.
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
write_status.call("cancelled") if cancelled
|
|
88
|
+
rescue StandardError
|
|
89
|
+
write_status.call("failed") rescue nil
|
|
90
|
+
ensure
|
|
91
|
+
# PTY.spawn gives the CLI a session/process group of its own.
|
|
92
|
+
if pid
|
|
93
|
+
Process.kill("TERM", -pid) rescue nil
|
|
94
|
+
grace = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 1
|
|
95
|
+
until (Process.waitpid(pid, Process::WNOHANG) rescue true)
|
|
96
|
+
break if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= grace
|
|
97
|
+
sleep 0.02
|
|
98
|
+
end
|
|
99
|
+
Process.kill("KILL", -pid) rescue nil
|
|
100
|
+
Process.waitpid(pid) rescue nil
|
|
101
|
+
end
|
|
102
|
+
[ reader, writer, pipe ].compact.each { |io| io.close rescue nil }
|
|
103
|
+
File.unlink(fifo_path) rescue nil
|
|
104
|
+
# A timed-out or interrupted login may have written a partial credential.
|
|
105
|
+
# Only a successfully completed CLI login may leave its config behind.
|
|
106
|
+
FileUtils.rm_rf(ENV.fetch("CLAUDE_CONFIG_DIR")) unless completed
|
|
107
|
+
end
|
data/lib/action_agent/engine.rb
CHANGED
|
@@ -42,6 +42,12 @@ module ActionAgent
|
|
|
42
42
|
config.action_agent = ActiveSupport::OrderedOptions.new
|
|
43
43
|
config.app_middleware.insert_before Rails::Rack::Logger, ActionAgent::AssistantRequestFilter
|
|
44
44
|
|
|
45
|
+
# After, not in, an initializer: the host configures multi_tenant and
|
|
46
|
+
# permission_checker in its own initializers, which run after the engine's.
|
|
47
|
+
config.after_initialize do
|
|
48
|
+
ActionAgent.warn_about_unchecked_permissions
|
|
49
|
+
end
|
|
50
|
+
|
|
45
51
|
# Whether a request is a browser asking for a page, as opposed to an API
|
|
46
52
|
# or MCP client: the routes use it to tell the dashboard's client-side
|
|
47
53
|
# deep links apart from protocol traffic on the same path.
|
|
@@ -55,11 +61,51 @@ module ActionAgent
|
|
|
55
61
|
# directory to the host app's asset paths is what lets a plain
|
|
56
62
|
# `mount ActionAgent::Engine` work without the host running a
|
|
57
63
|
# JavaScript build — or having a JavaScript build at all.
|
|
58
|
-
# Provider credentials
|
|
59
|
-
#
|
|
60
|
-
#
|
|
64
|
+
# Provider credentials, API keys and answers to a run's requests for
|
|
65
|
+
# input (a secret among them) are posted to the dashboard in the clear
|
|
66
|
+
# and encrypted at rest — filtering keeps them out of the request logs
|
|
67
|
+
# in between, where the gem would otherwise print them verbatim.
|
|
68
|
+
# `answer` and `value` match whole names only, at any depth, so a host
|
|
69
|
+
# param or model attribute such as `values` or `default_value` is not
|
|
70
|
+
# filtered with them.
|
|
61
71
|
initializer "action_agent.filter_parameters" do |app|
|
|
62
|
-
|
|
72
|
+
# The last filters: RecordingEventIngest::BATCH_KEY, a batch that carries
|
|
73
|
+
# page content and console output, and an input request's answer. Each
|
|
74
|
+
# matches its key exactly, so a host parameter that merely contains those
|
|
75
|
+
# words is still logged. `answer` and `value` may also follow a dot:
|
|
76
|
+
# Rails 8.2 precompiles the host's filters into one expression and, once
|
|
77
|
+
# any of them names a nested key (Active Record Encryption adds
|
|
78
|
+
# `input_request.answer`), matches it against the dotted parameter path,
|
|
79
|
+
# where `\Aanswer\z` would no longer find `evaluation.answer`.
|
|
80
|
+
app.config.filter_parameters += [ :credential, :api_key, :access_token, :password, :claude_login_code, /\Arecording_events\z/, /(?:\A|\.)(?:answer|value)\z/i ]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# The project setup assistant's request_secret answers go to its
|
|
84
|
+
# handler. Registered on every reload, so the handler is always the
|
|
85
|
+
# current class's.
|
|
86
|
+
initializer "action_agent.secret_requests" do |app|
|
|
87
|
+
app.config.to_prepare do
|
|
88
|
+
ActionAgent::SecretRequests.register(ActionAgent::ProjectSetup::AGENT_CLASS_NAME, ActionAgent::ProjectSetup::SecretHandler.new)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# In a sandbox app (a checkout the dashboard booted, which bundles this
|
|
93
|
+
# engine), its backend sets ActionAgent::SandboxMail::DIRECTORY_ENV, and
|
|
94
|
+
# mail is written to files in that directory instead of being sent, so
|
|
95
|
+
# an agent exploring the app can read a sign-up's verification link.
|
|
96
|
+
# Registered as a load hook after the app's own configuration has been
|
|
97
|
+
# applied, so it wins over the environment file's delivery method. Set
|
|
98
|
+
# only in the sandbox's environment, it never reaches the checkout.
|
|
99
|
+
initializer "action_agent.sandbox_mail" do |app|
|
|
100
|
+
directory = ENV["ACTION_AGENT_SANDBOX_MAIL_DIR"].presence
|
|
101
|
+
next unless directory
|
|
102
|
+
|
|
103
|
+
location = File.expand_path(directory, app.root.to_s)
|
|
104
|
+
ActiveSupport.on_load(:action_mailer) do
|
|
105
|
+
self.delivery_method = :file
|
|
106
|
+
self.file_settings = { location: location }
|
|
107
|
+
self.perform_deliveries = true
|
|
108
|
+
end
|
|
63
109
|
end
|
|
64
110
|
|
|
65
111
|
# This engine's constants are spelled the way Zeitwerk's own inflector
|
|
@@ -207,8 +253,12 @@ module ActionAgent
|
|
|
207
253
|
# i.e. the dashboard's only page 500s on every sprockets-rails host.
|
|
208
254
|
# Propshaft serves everything on the load path and has no precompile
|
|
209
255
|
# list, so the respond_to? check is what distinguishes them.
|
|
256
|
+
# action_agent_replay.js is the session player's own bundle, loaded
|
|
257
|
+
# only by SessionPlayerController's frame. action_agent_recorder.js is
|
|
258
|
+
# imported by the dashboard while the Run Agent workbench is open, from
|
|
259
|
+
# the URL the dashboard page names (DashboardController).
|
|
210
260
|
if app.config.assets.respond_to?(:precompile)
|
|
211
|
-
app.config.assets.precompile |= %w[action_agent.js action_agent.css]
|
|
261
|
+
app.config.assets.precompile |= %w[action_agent.js action_agent.css action_agent_replay.js action_agent_recorder.js]
|
|
212
262
|
end
|
|
213
263
|
end
|
|
214
264
|
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActionAgent
|
|
4
|
+
# The agents allowed to ask a person for a secret with the `request_secret`
|
|
5
|
+
# tool, each with the handler the answer is delivered to.
|
|
6
|
+
#
|
|
7
|
+
# Only agents the engine defines register here. The secret never reaches
|
|
8
|
+
# the model, so an agent may ask for one only when engine code is there to
|
|
9
|
+
# receive it. An agent is matched on its `agent_class_name`, which a
|
|
10
|
+
# dashboard user can also type, so a handler checks the run it is given is
|
|
11
|
+
# one it expects before keeping the value.
|
|
12
|
+
#
|
|
13
|
+
# A handler is a block, or an object with #call. An object may also
|
|
14
|
+
# answer these, each optional:
|
|
15
|
+
#
|
|
16
|
+
# refusal(run:, name:) why the run may not ask for +name+, or
|
|
17
|
+
# nil; the model reads it as the call's
|
|
18
|
+
# error and nobody is asked
|
|
19
|
+
# prompt(run:, name:, prompt:) the question a person reads, in place
|
|
20
|
+
# of the model's +prompt+
|
|
21
|
+
# answerable_by?(request, user) whether +user+ may answer +request+, on
|
|
22
|
+
# top of InputRequest#answerable_by?
|
|
23
|
+
#
|
|
24
|
+
# @example
|
|
25
|
+
# ActionAgent::SecretRequests.register("ActionAgent::SetupAgent") do |run:, name:, value:|
|
|
26
|
+
# ProjectSecret.store!(run:, name:, value:)
|
|
27
|
+
# end
|
|
28
|
+
#
|
|
29
|
+
# @api private
|
|
30
|
+
module SecretRequests
|
|
31
|
+
@handlers = {}
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
# @param agent_class_name [String]
|
|
35
|
+
# @param handler [#call, nil] the handler, unless given as a block
|
|
36
|
+
# @yieldparam run [AgentRun] the run the answer belongs to
|
|
37
|
+
# @yieldparam name [String] the secret's name, as the model asked for it
|
|
38
|
+
# @yieldparam value [String] the answer
|
|
39
|
+
def register(agent_class_name, handler = nil, &block)
|
|
40
|
+
handler ||= block
|
|
41
|
+
raise ArgumentError, "A secret request handler needs a block or an object with #call" unless handler.respond_to?(:call)
|
|
42
|
+
|
|
43
|
+
@handlers[agent_class_name.to_s] = handler
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def unregister(agent_class_name)
|
|
47
|
+
@handlers.delete(agent_class_name.to_s)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# @param agent [Agent, nil]
|
|
51
|
+
# @return [#call, nil]
|
|
52
|
+
def handler_for(agent)
|
|
53
|
+
name = agent&.agent_class_name.to_s
|
|
54
|
+
name.empty? ? nil : @handlers[name]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Why +run+'s agent may not ask for +name+, or nil.
|
|
58
|
+
def refusal(agent, run:, name:)
|
|
59
|
+
handler = handler_for(agent)
|
|
60
|
+
handler.respond_to?(:refusal) ? handler.refusal(run: run, name: name) : nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# The question a person is asked for +name+: the handler's wording, or
|
|
64
|
+
# the model's +prompt+.
|
|
65
|
+
def prompt(agent, run:, name:, prompt:)
|
|
66
|
+
handler = handler_for(agent)
|
|
67
|
+
handler.respond_to?(:prompt) ? handler.prompt(run: run, name: name, prompt: prompt) : prompt
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Whether +request+'s handler lets +user+ answer it. True for anything
|
|
71
|
+
# but a `secret` request whose agent's handler decides.
|
|
72
|
+
def answerable_by?(request, user)
|
|
73
|
+
return true unless request.kind == "secret"
|
|
74
|
+
|
|
75
|
+
handler = handler_for(request.subject.try(:agent))
|
|
76
|
+
handler.respond_to?(:answerable_by?) ? handler.answerable_by?(request, user) == true : true
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
data/lib/action_agent/version.rb
CHANGED