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.
Files changed (168) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/builds/action_agent.css +1 -1
  3. data/app/assets/builds/action_agent.js +76 -73
  4. data/app/assets/builds/action_agent_recorder.js +78 -0
  5. data/app/assets/builds/action_agent_replay.js +226 -0
  6. data/app/controllers/action_agent/api/agent_runs_controller.rb +3 -1
  7. data/app/controllers/action_agent/api/agents_controller.rb +1 -0
  8. data/app/controllers/action_agent/api/api_keys_controller.rb +12 -2
  9. data/app/controllers/action_agent/api/base_controller.rb +63 -2
  10. data/app/controllers/action_agent/api/claude_logins_controller.rb +93 -0
  11. data/app/controllers/action_agent/api/code_sessions_controller.rb +71 -12
  12. data/app/controllers/action_agent/api/dashboard_assistant_controller.rb +2 -2
  13. data/app/controllers/action_agent/api/draft_pull_requests_controller.rb +248 -0
  14. data/app/controllers/action_agent/api/evaluation_fixes_controller.rb +27 -0
  15. data/app/controllers/action_agent/api/evaluations_controller.rb +58 -7
  16. data/app/controllers/action_agent/api/explorations_controller.rb +213 -0
  17. data/app/controllers/action_agent/api/github_app_manifests_controller.rb +125 -0
  18. data/app/controllers/action_agent/api/github_connections_controller.rb +40 -2
  19. data/app/controllers/action_agent/api/github_installations_controller.rb +280 -0
  20. data/app/controllers/action_agent/api/input_requests_controller.rb +77 -0
  21. data/app/controllers/action_agent/api/mcp_controller.rb +46 -12
  22. data/app/controllers/action_agent/api/members_controller.rb +36 -0
  23. data/app/controllers/action_agent/api/project_explorations_controller.rb +80 -0
  24. data/app/controllers/action_agent/api/project_install_pull_requests_controller.rb +305 -0
  25. data/app/controllers/action_agent/api/project_secrets_controller.rb +96 -0
  26. data/app/controllers/action_agent/api/project_setup_controller.rb +151 -0
  27. data/app/controllers/action_agent/api/project_sign_ins_controller.rb +112 -0
  28. data/app/controllers/action_agent/api/projects_controller.rb +512 -0
  29. data/app/controllers/action_agent/api/provider_keys_controller.rb +176 -24
  30. data/app/controllers/action_agent/api/provider_models_controller.rb +37 -15
  31. data/app/controllers/action_agent/api/recording_event_ingest_controller.rb +67 -0
  32. data/app/controllers/action_agent/api/sandbox_browser_tickets_controller.rb +50 -0
  33. data/app/controllers/action_agent/api/sandbox_browsers_controller.rb +65 -0
  34. data/app/controllers/action_agent/api/sandboxes_controller.rb +148 -8
  35. data/app/controllers/action_agent/api/scenario_catalog_entries_controller.rb +188 -0
  36. data/app/controllers/action_agent/api/scenario_catalogs_controller.rb +302 -0
  37. data/app/controllers/action_agent/api/session_recordings_controller.rb +165 -34
  38. data/app/controllers/action_agent/api/sessions_controller.rb +111 -0
  39. data/app/controllers/action_agent/api/telemetry_keys_controller.rb +21 -0
  40. data/app/controllers/action_agent/dashboard_controller.rb +17 -6
  41. data/app/controllers/action_agent/session_player_controller.rb +58 -0
  42. data/app/controllers/concerns/action_agent/api/agent_serialization.rb +1 -0
  43. data/app/controllers/concerns/action_agent/api/mcp_dashboard_tools.rb +627 -20
  44. data/app/controllers/concerns/action_agent/api/project_browser.rb +46 -0
  45. data/app/controllers/concerns/action_agent/api/project_secret_authorization.rb +57 -0
  46. data/app/jobs/action_agent/agent_execution_job.rb +9 -39
  47. data/app/jobs/action_agent/agent_resume_job.rb +84 -0
  48. data/app/jobs/action_agent/code_session_job.rb +39 -10
  49. data/app/jobs/action_agent/draft_pull_request_job.rb +43 -0
  50. data/app/jobs/action_agent/exploration_job.rb +80 -0
  51. data/app/jobs/action_agent/input_request_expiry_job.rb +22 -0
  52. data/app/jobs/action_agent/project_evaluation_job.rb +59 -0
  53. data/app/jobs/action_agent/project_setup_job.rb +21 -0
  54. data/app/jobs/action_agent/sandbox_cleanup_job.rb +20 -3
  55. data/app/jobs/action_agent/sandbox_provision_job.rb +77 -10
  56. data/app/jobs/action_agent/sandbox_run_job.rb +9 -37
  57. data/app/jobs/action_agent/verify_evaluation_fix_job.rb +37 -0
  58. data/app/models/action_agent/agent.rb +72 -26
  59. data/app/models/action_agent/agent_run.rb +134 -23
  60. data/app/models/action_agent/agent_template.rb +35 -1
  61. data/app/models/action_agent/api_key.rb +14 -0
  62. data/app/models/action_agent/catalog_scenario.rb +75 -0
  63. data/app/models/action_agent/code_session.rb +21 -5
  64. data/app/models/action_agent/draft_pull_request.rb +131 -0
  65. data/app/models/action_agent/evaluation.rb +148 -0
  66. data/app/models/action_agent/evaluation_run.rb +143 -53
  67. data/app/models/action_agent/evaluation_scenario_result.rb +15 -3
  68. data/app/models/action_agent/exploration.rb +847 -0
  69. data/app/models/action_agent/github_installation.rb +159 -0
  70. data/app/models/action_agent/input_request.rb +291 -0
  71. data/app/models/action_agent/model_pricing.rb +214 -34
  72. data/app/models/action_agent/project.rb +789 -0
  73. data/app/models/action_agent/project_secret.rb +339 -0
  74. data/app/models/action_agent/provider_key.rb +207 -4
  75. data/app/models/action_agent/recording_event.rb +145 -0
  76. data/app/models/action_agent/recording_snapshot.rb +7 -7
  77. data/app/models/action_agent/sandbox_session.rb +300 -16
  78. data/app/models/action_agent/scenario_catalog.rb +172 -0
  79. data/app/models/action_agent/scenario_product.rb +57 -0
  80. data/app/models/action_agent/scenario_set.rb +138 -0
  81. data/app/models/action_agent/session_recording.rb +121 -14
  82. data/app/models/action_agent/telemetry_trace.rb +74 -19
  83. data/app/models/concerns/action_agent/ownable.rb +84 -13
  84. data/app/serializers/action_agent/evaluation_serializer.rb +41 -10
  85. data/app/serializers/action_agent/input_request_serializer.rb +47 -0
  86. data/app/serializers/action_agent/scenario_catalog_serializer.rb +23 -0
  87. data/app/serializers/action_agent/session_summary_serializer.rb +146 -0
  88. data/app/services/action_agent/agent_execution_service.rb +450 -72
  89. data/app/services/action_agent/agent_scorecard.rb +50 -21
  90. data/app/services/action_agent/agent_sync.rb +5 -2
  91. data/app/services/action_agent/agent_tool_roster.rb +19 -4
  92. data/app/services/action_agent/agent_toolbox.rb +215 -9
  93. data/app/services/action_agent/browser_live_ticket.rb +78 -0
  94. data/app/services/action_agent/browser_sidecar.rb +213 -0
  95. data/app/services/action_agent/browser_sign_in.rb +163 -0
  96. data/app/services/action_agent/browser_tool_redaction.rb +92 -0
  97. data/app/services/action_agent/claude_code_auth.rb +70 -4
  98. data/app/services/action_agent/codex_session_events.rb +37 -0
  99. data/app/services/action_agent/dashboard_assistant_service.rb +24 -10
  100. data/app/services/action_agent/draft_pull_request_publisher.rb +719 -0
  101. data/app/services/action_agent/evaluation_fix.rb +101 -0
  102. data/app/services/action_agent/evaluation_fix_comparison.rb +66 -0
  103. data/app/services/action_agent/evaluation_report_import.rb +84 -5
  104. data/app/services/action_agent/evaluation_run_cost.rb +609 -0
  105. data/app/services/action_agent/evaluation_runner_service.rb +29 -10
  106. data/app/services/action_agent/evaluation_standing.rb +152 -0
  107. data/app/services/action_agent/explorer_execution_service.rb +517 -0
  108. data/app/services/action_agent/github_client.rb +405 -64
  109. data/app/services/action_agent/live_updates.rb +31 -0
  110. data/app/services/action_agent/local_sandbox_backend.rb +1263 -102
  111. data/app/services/action_agent/local_sandbox_claude_login.rb +151 -0
  112. data/app/services/action_agent/mcp_client.rb +6 -2
  113. data/app/services/action_agent/mcp_recording_middleware.rb +104 -145
  114. data/app/services/action_agent/mcp_tool_dispatcher.rb +36 -13
  115. data/app/services/action_agent/mock_sandbox_backend.rb +3 -1
  116. data/app/services/action_agent/playwright_mcp_client.rb +7 -0
  117. data/app/services/action_agent/project_boot_fix.rb +309 -0
  118. data/app/services/action_agent/project_capabilities.rb +200 -0
  119. data/app/services/action_agent/project_install_pull_request.rb +225 -0
  120. data/app/services/action_agent/project_preflight.rb +178 -0
  121. data/app/services/action_agent/project_secret_discovery.rb +142 -0
  122. data/app/services/action_agent/project_setup.rb +429 -0
  123. data/app/services/action_agent/provider_credentials.rb +102 -0
  124. data/app/services/action_agent/recording_event_ingest.rb +181 -0
  125. data/app/services/action_agent/runtime_tool_roster.rb +52 -0
  126. data/app/services/action_agent/sample_data/conference_ticket.rb +516 -0
  127. data/app/services/action_agent/sandbox_boot_spec.rb +558 -0
  128. data/app/services/action_agent/sandbox_browser.rb +299 -0
  129. data/app/services/action_agent/sandbox_browser_driver.rb +85 -0
  130. data/app/services/action_agent/sandbox_credential_paths.rb +26 -0
  131. data/app/services/action_agent/sandbox_mail.rb +103 -0
  132. data/app/services/action_agent/sandbox_manifest.rb +166 -13
  133. data/app/services/action_agent/sandbox_orchestrator.rb +290 -23
  134. data/app/services/action_agent/sandbox_patch.rb +253 -0
  135. data/app/services/action_agent/scenario_catalog_import.rb +255 -0
  136. data/app/services/action_agent/scenario_catalog_repository_import.rb +81 -0
  137. data/app/services/action_agent/scenario_evaluation_runner.rb +121 -17
  138. data/app/services/action_agent/secret_scrubber.rb +33 -4
  139. data/app/services/action_agent/session_index.rb +270 -0
  140. data/app/services/action_agent/session_recording_service.rb +11 -0
  141. data/app/services/action_agent/session_timeline.rb +505 -0
  142. data/app/views/action_agent/github_app_manifests/created.html.erb +55 -0
  143. data/app/views/action_agent/session_player/show.html.erb +19 -0
  144. data/config/routes.rb +153 -2
  145. data/lib/action_agent/claude_login_process.rb +107 -0
  146. data/lib/action_agent/engine.rb +55 -5
  147. data/lib/action_agent/secret_requests.rb +80 -0
  148. data/lib/action_agent/version.rb +1 -1
  149. data/lib/action_agent.rb +513 -22
  150. data/lib/generators/action_agent/install_generator.rb +45 -5
  151. data/lib/generators/action_agent/templates/action_agent.rb.erb +161 -0
  152. data/lib/generators/action_agent/templates/add_code_session_runner.rb.erb +9 -0
  153. data/lib/generators/action_agent/templates/migrations/015_add_provider_key_scope.rb.erb +72 -0
  154. data/lib/generators/action_agent/templates/migrations/016_create_active_agent_input_requests.rb.erb +85 -0
  155. data/lib/generators/action_agent/templates/migrations/017_create_active_agent_recording_events.rb.erb +57 -0
  156. data/lib/generators/action_agent/templates/migrations/018_add_browser_to_active_agent_sandbox_sessions.rb.erb +25 -0
  157. data/lib/generators/action_agent/templates/migrations/019_create_active_agent_projects.rb.erb +79 -0
  158. data/lib/generators/action_agent/templates/migrations/020_add_github_app_installations.rb.erb +62 -0
  159. data/lib/generators/action_agent/templates/migrations/021_create_active_agent_explorations.rb.erb +63 -0
  160. data/lib/generators/action_agent/templates/migrations/023_create_active_agent_draft_pull_requests.rb.erb +75 -0
  161. data/lib/generators/action_agent/templates/migrations/024_add_kind_to_active_agent_project_secrets.rb.erb +14 -0
  162. data/lib/generators/action_agent/templates/migrations/025_create_active_agent_scenario_catalogs.rb.erb +110 -0
  163. data/lib/generators/action_agent/templates/migrations/026_add_evaluation_fixes_to_code_sessions.rb.erb +21 -0
  164. data/lib/tasks/action_agent.rake +33 -0
  165. data/lib/tasks/browser.rake +22 -0
  166. data/lib/tasks/sandbox.rake +1 -1
  167. data/lib/tasks/scenario_catalogs.rake +71 -0
  168. 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
- resources :session_recordings, only: [ :index, :show, :destroy ] do
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
@@ -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 and API keys are posted to the dashboard in the
59
- # clear and encrypted at rest — filtering keeps them out of the request
60
- # logs in between, where the gem would otherwise print them verbatim.
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
- app.config.filter_parameters += [ :credential, :api_key, :access_token ]
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActionAgent
4
- VERSION = "1.8.0"
4
+ VERSION = "1.9.0"
5
5
  end