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.
Files changed (163) 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 +53 -6
  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 +9 -4
  16. data/app/controllers/action_agent/api/explorations_controller.rb +213 -0
  17. data/app/controllers/action_agent/api/feedback_ingest_controller.rb +34 -0
  18. data/app/controllers/action_agent/api/feedbacks_controller.rb +40 -0
  19. data/app/controllers/action_agent/api/github_app_manifests_controller.rb +125 -0
  20. data/app/controllers/action_agent/api/github_connections_controller.rb +40 -2
  21. data/app/controllers/action_agent/api/github_installations_controller.rb +280 -0
  22. data/app/controllers/action_agent/api/input_requests_controller.rb +77 -0
  23. data/app/controllers/action_agent/api/mcp_controller.rb +46 -12
  24. data/app/controllers/action_agent/api/members_controller.rb +36 -0
  25. data/app/controllers/action_agent/api/project_explorations_controller.rb +80 -0
  26. data/app/controllers/action_agent/api/project_install_pull_requests_controller.rb +305 -0
  27. data/app/controllers/action_agent/api/project_secrets_controller.rb +96 -0
  28. data/app/controllers/action_agent/api/project_setup_controller.rb +151 -0
  29. data/app/controllers/action_agent/api/project_sign_ins_controller.rb +112 -0
  30. data/app/controllers/action_agent/api/projects_controller.rb +512 -0
  31. data/app/controllers/action_agent/api/provider_keys_controller.rb +176 -24
  32. data/app/controllers/action_agent/api/provider_models_controller.rb +37 -15
  33. data/app/controllers/action_agent/api/recording_event_ingest_controller.rb +67 -0
  34. data/app/controllers/action_agent/api/sandbox_browser_tickets_controller.rb +50 -0
  35. data/app/controllers/action_agent/api/sandbox_browsers_controller.rb +65 -0
  36. data/app/controllers/action_agent/api/sandboxes_controller.rb +140 -8
  37. data/app/controllers/action_agent/api/scenario_catalog_entries_controller.rb +188 -0
  38. data/app/controllers/action_agent/api/scenario_catalogs_controller.rb +302 -0
  39. data/app/controllers/action_agent/api/session_recordings_controller.rb +165 -34
  40. data/app/controllers/action_agent/api/sessions_controller.rb +111 -0
  41. data/app/controllers/action_agent/api/telemetry_keys_controller.rb +21 -0
  42. data/app/controllers/action_agent/dashboard_controller.rb +17 -6
  43. data/app/controllers/action_agent/session_player_controller.rb +58 -0
  44. data/app/controllers/concerns/action_agent/api/agent_serialization.rb +1 -0
  45. data/app/controllers/concerns/action_agent/api/mcp_dashboard_tools.rb +623 -18
  46. data/app/controllers/concerns/action_agent/api/project_browser.rb +46 -0
  47. data/app/controllers/concerns/action_agent/api/project_secret_authorization.rb +57 -0
  48. data/app/jobs/action_agent/agent_execution_job.rb +9 -39
  49. data/app/jobs/action_agent/agent_resume_job.rb +84 -0
  50. data/app/jobs/action_agent/code_session_job.rb +26 -2
  51. data/app/jobs/action_agent/draft_pull_request_job.rb +43 -0
  52. data/app/jobs/action_agent/exploration_job.rb +80 -0
  53. data/app/jobs/action_agent/input_request_expiry_job.rb +22 -0
  54. data/app/jobs/action_agent/project_evaluation_job.rb +59 -0
  55. data/app/jobs/action_agent/project_setup_job.rb +21 -0
  56. data/app/jobs/action_agent/sandbox_cleanup_job.rb +20 -3
  57. data/app/jobs/action_agent/sandbox_provision_job.rb +77 -10
  58. data/app/jobs/action_agent/sandbox_run_job.rb +9 -37
  59. data/app/jobs/action_agent/verify_evaluation_fix_job.rb +37 -0
  60. data/app/models/action_agent/agent.rb +39 -26
  61. data/app/models/action_agent/agent_run.rb +134 -23
  62. data/app/models/action_agent/agent_template.rb +35 -1
  63. data/app/models/action_agent/api_key.rb +14 -0
  64. data/app/models/action_agent/catalog_scenario.rb +75 -0
  65. data/app/models/action_agent/code_session.rb +16 -4
  66. data/app/models/action_agent/draft_pull_request.rb +131 -0
  67. data/app/models/action_agent/evaluation.rb +84 -0
  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/interaction_feedback.rb +24 -0
  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 +200 -3
  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 +297 -14
  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/input_request_serializer.rb +47 -0
  85. data/app/serializers/action_agent/scenario_catalog_serializer.rb +23 -0
  86. data/app/serializers/action_agent/session_summary_serializer.rb +146 -0
  87. data/app/services/action_agent/agent_execution_service.rb +450 -72
  88. data/app/services/action_agent/agent_sync.rb +5 -2
  89. data/app/services/action_agent/agent_tool_roster.rb +19 -4
  90. data/app/services/action_agent/agent_toolbox.rb +215 -9
  91. data/app/services/action_agent/browser_live_ticket.rb +78 -0
  92. data/app/services/action_agent/browser_sidecar.rb +213 -0
  93. data/app/services/action_agent/browser_sign_in.rb +163 -0
  94. data/app/services/action_agent/browser_tool_redaction.rb +92 -0
  95. data/app/services/action_agent/claude_code_auth.rb +70 -4
  96. data/app/services/action_agent/dashboard_assistant_service.rb +24 -10
  97. data/app/services/action_agent/draft_pull_request_publisher.rb +719 -0
  98. data/app/services/action_agent/evaluation_fix.rb +101 -0
  99. data/app/services/action_agent/evaluation_fix_comparison.rb +66 -0
  100. data/app/services/action_agent/evaluation_runner_service.rb +4 -5
  101. data/app/services/action_agent/evaluation_standing.rb +15 -8
  102. data/app/services/action_agent/explorer_execution_service.rb +517 -0
  103. data/app/services/action_agent/github_client.rb +405 -64
  104. data/app/services/action_agent/live_updates.rb +31 -0
  105. data/app/services/action_agent/local_sandbox_backend.rb +1218 -83
  106. data/app/services/action_agent/local_sandbox_claude_login.rb +151 -0
  107. data/app/services/action_agent/mcp_client.rb +6 -2
  108. data/app/services/action_agent/mcp_recording_middleware.rb +104 -145
  109. data/app/services/action_agent/mcp_tool_dispatcher.rb +36 -13
  110. data/app/services/action_agent/mock_sandbox_backend.rb +3 -1
  111. data/app/services/action_agent/playwright_mcp_client.rb +7 -0
  112. data/app/services/action_agent/project_boot_fix.rb +309 -0
  113. data/app/services/action_agent/project_capabilities.rb +200 -0
  114. data/app/services/action_agent/project_install_pull_request.rb +225 -0
  115. data/app/services/action_agent/project_preflight.rb +178 -0
  116. data/app/services/action_agent/project_secret_discovery.rb +142 -0
  117. data/app/services/action_agent/project_setup.rb +429 -0
  118. data/app/services/action_agent/provider_credentials.rb +102 -0
  119. data/app/services/action_agent/recording_event_ingest.rb +181 -0
  120. data/app/services/action_agent/runtime_tool_roster.rb +52 -0
  121. data/app/services/action_agent/sample_data/conference_ticket.rb +516 -0
  122. data/app/services/action_agent/sandbox_boot_spec.rb +558 -0
  123. data/app/services/action_agent/sandbox_browser.rb +299 -0
  124. data/app/services/action_agent/sandbox_browser_driver.rb +85 -0
  125. data/app/services/action_agent/sandbox_credential_paths.rb +26 -0
  126. data/app/services/action_agent/sandbox_mail.rb +103 -0
  127. data/app/services/action_agent/sandbox_manifest.rb +166 -13
  128. data/app/services/action_agent/sandbox_orchestrator.rb +276 -22
  129. data/app/services/action_agent/sandbox_patch.rb +253 -0
  130. data/app/services/action_agent/scenario_catalog_import.rb +255 -0
  131. data/app/services/action_agent/scenario_catalog_repository_import.rb +81 -0
  132. data/app/services/action_agent/scenario_evaluation_runner.rb +119 -16
  133. data/app/services/action_agent/secret_scrubber.rb +33 -4
  134. data/app/services/action_agent/session_index.rb +270 -0
  135. data/app/services/action_agent/session_recording_service.rb +11 -0
  136. data/app/services/action_agent/session_timeline.rb +505 -0
  137. data/app/views/action_agent/github_app_manifests/created.html.erb +55 -0
  138. data/app/views/action_agent/session_player/show.html.erb +19 -0
  139. data/config/routes.rb +155 -1
  140. data/lib/action_agent/claude_login_process.rb +107 -0
  141. data/lib/action_agent/engine.rb +55 -5
  142. data/lib/action_agent/secret_requests.rb +80 -0
  143. data/lib/action_agent/version.rb +1 -1
  144. data/lib/action_agent.rb +507 -22
  145. data/lib/generators/action_agent/install_generator.rb +37 -0
  146. data/lib/generators/action_agent/templates/action_agent.rb.erb +158 -0
  147. data/lib/generators/action_agent/templates/migrations/015_add_provider_key_scope.rb.erb +72 -0
  148. data/lib/generators/action_agent/templates/migrations/016_create_active_agent_input_requests.rb.erb +85 -0
  149. data/lib/generators/action_agent/templates/migrations/017_create_active_agent_recording_events.rb.erb +57 -0
  150. data/lib/generators/action_agent/templates/migrations/018_add_browser_to_active_agent_sandbox_sessions.rb.erb +25 -0
  151. data/lib/generators/action_agent/templates/migrations/019_create_active_agent_projects.rb.erb +79 -0
  152. data/lib/generators/action_agent/templates/migrations/020_add_github_app_installations.rb.erb +62 -0
  153. data/lib/generators/action_agent/templates/migrations/021_create_active_agent_explorations.rb.erb +63 -0
  154. data/lib/generators/action_agent/templates/migrations/023_create_active_agent_draft_pull_requests.rb.erb +75 -0
  155. data/lib/generators/action_agent/templates/migrations/024_add_kind_to_active_agent_project_secrets.rb.erb +14 -0
  156. data/lib/generators/action_agent/templates/migrations/025_create_active_agent_scenario_catalogs.rb.erb +110 -0
  157. data/lib/generators/action_agent/templates/migrations/026_add_evaluation_fixes_to_code_sessions.rb.erb +21 -0
  158. data/lib/generators/action_agent/templates/migrations/027_create_active_agent_interaction_feedbacks.rb.erb +17 -0
  159. data/lib/tasks/action_agent.rake +33 -0
  160. data/lib/tasks/browser.rake +22 -0
  161. data/lib/tasks/sandbox.rake +1 -1
  162. data/lib/tasks/scenario_catalogs.rake +71 -0
  163. metadata +100 -2
data/config/routes.rb CHANGED
@@ -32,6 +32,9 @@ ActionAgent::Engine.routes.draw do
32
32
  # Authenticated like trace ingest, with a bearer token.
33
33
  resources :evaluation_reports, only: [ :create ]
34
34
 
35
+ post "feedback", to: "feedback_ingest#create"
36
+ get "feedback", to: "feedbacks#index"
37
+
35
38
  # A JSON API has no :new or :edit forms to serve.
36
39
  resources :agents, except: [ :new, :edit ] do
37
40
  member do
@@ -71,6 +74,15 @@ ActionAgent::Engine.routes.draw do
71
74
  end
72
75
  end
73
76
 
77
+ # What paused runs are waiting on a person for, and the answers that
78
+ # resume them.
79
+ resources :input_requests, only: [ :index ] do
80
+ member do
81
+ post :answer
82
+ post :decline
83
+ end
84
+ end
85
+
74
86
  # Sandboxes. The engine ships the in-memory and local backends; an
75
87
  # operator registers the rest (see ActionAgent.sandbox_backends).
76
88
  resources :sandboxes, param: :id, only: [ :index, :create, :show, :destroy ] do
@@ -79,6 +91,11 @@ ActionAgent::Engine.routes.draw do
79
91
  end
80
92
  member do
81
93
  post :run
94
+ # A checkout's boot: its steps, a step's log, and continuing a
95
+ # failed boot the backend kept.
96
+ get :boot
97
+ get :boot_log
98
+ post :resume_boot
82
99
  end
83
100
  # Claude Code sessions in an app_runtime sandbox's checkout.
84
101
  resources :code_sessions, only: [ :index, :create, :show ] do
@@ -86,6 +103,71 @@ ActionAgent::Engine.routes.draw do
86
103
  post :cancel
87
104
  end
88
105
  end
106
+ resource :claude_login, only: [ :show, :create, :destroy ], controller: "claude_logins" do
107
+ post :code
108
+ end
109
+ # The checkout's browser (SandboxBrowser), and tickets into its live view.
110
+ resource :browser, only: [ :show, :create, :destroy ], controller: "sandbox_browsers" do
111
+ resources :tickets, only: [ :create ], controller: "sandbox_browser_tickets"
112
+ end
113
+
114
+ # A pull request opened from the checkout's changes, its preview, and
115
+ # the patch to download instead.
116
+ resource :pull_request, only: [ :show, :create ], controller: "draft_pull_requests" do
117
+ post :preview
118
+ get :patch
119
+ end
120
+ end
121
+ get "evaluations/:evaluation_id/runs/:run_id/fixes", to: "evaluation_fixes#index"
122
+
123
+ # Projects: a repository booted in a checkout sandbox and evaluated,
124
+ # with the secrets its boot needs (names only in responses).
125
+ resources :projects, only: [ :index, :show, :create, :update, :destroy ] do
126
+ collection do
127
+ get :capabilities
128
+ get :preflight
129
+ get :discover_secrets
130
+ end
131
+ member do
132
+ post :boot
133
+ get :boot, action: :boot_status
134
+ get :boot_log
135
+ get :synced_agents
136
+ patch :target
137
+ post :run_evaluation
138
+ end
139
+ put :secrets, to: "project_secrets#upsert"
140
+ resources :secrets, controller: "project_secrets", only: [ :index, :update, :destroy ], param: :name
141
+ # The setup assistant, the requests for input waiting on the project's
142
+ # agents, and the models the App assistant may read.
143
+ post :setup, to: "project_setup#start"
144
+ post "setup/claude_code", to: "project_setup#fix_with_claude_code", as: :setup_claude_code
145
+ patch :setup, to: "project_setup#update"
146
+ get :input_requests, to: "project_setup#input_requests"
147
+ get :app_models, to: "project_setup#app_models"
148
+ put :schema_tools, to: "project_setup#schema_tools"
149
+ # The pull request that installs the engine in the repository.
150
+ resource :install_pull_request, only: [ :show, :create ], controller: "project_install_pull_requests" do
151
+ post :preview
152
+ get :patch
153
+ end
154
+
155
+ # The explorer agent's start, and how the project's browser signs in.
156
+ resources :explorations, controller: "project_explorations", only: [ :create ]
157
+ resource :sign_in, controller: "project_sign_ins", only: [ :show, :update, :destroy ] do
158
+ post :check
159
+ post :save_browser
160
+ end
161
+ end
162
+
163
+ # Explorations: candidate scenarios found by walking a project's app,
164
+ # reviewed and accepted into its evaluation (see Exploration).
165
+ resources :explorations, only: [ :index, :show, :create ] do
166
+ member do
167
+ post :accept
168
+ post :stop
169
+ patch "candidates/:candidate_id", action: :update_candidate, as: :candidate
170
+ end
89
171
  end
90
172
 
91
173
  # Tool inventory — auto-detected from the tool roster each generation
@@ -108,8 +190,17 @@ ActionAgent::Engine.routes.draw do
108
190
  end
109
191
  end
110
192
 
111
- resources :session_recordings, only: [ :index, :show, :destroy ] do
193
+ # create is the Run Agent workbench's recording of a conversation.
194
+ resources :session_recordings, only: [ :index, :show, :create, :destroy ] do
112
195
  member do
196
+ # A recorder holding the recording's ingest token posts its events
197
+ # here without a dashboard session; any other post is a dashboard
198
+ # session's, checked for forgery like the rest of the API.
199
+ post :events, to: "recording_event_ingest#create",
200
+ constraints: ->(request) { request.authorization.to_s.match?(/\ABearer\s/i) }
201
+ post :events, action: :create_events
202
+ get :events
203
+ get :timeline
113
204
  get :actions
114
205
  get "snapshot/:action_id", action: :snapshot, as: :snapshot
115
206
  post :export
@@ -123,6 +214,12 @@ ActionAgent::Engine.routes.draw do
123
214
  end
124
215
  end
125
216
 
217
+ # The sessions a caller can replay, and the timeline of a conversation,
218
+ # a run or an evaluation scenario's replay, with or without a recording.
219
+ get "sessions", to: "sessions#index", as: :sessions
220
+ get "sessions/:kind/:id/timeline", to: "sessions#timeline", as: :session_timeline,
221
+ constraints: { kind: /context|run|scenario_result/ }
222
+
126
223
  resource :analytics, only: [], controller: "analytics" do
127
224
  get "/", action: :index
128
225
  end
@@ -154,6 +251,33 @@ ActionAgent::Engine.routes.draw do
154
251
  end
155
252
  end
156
253
 
254
+ # Scenario catalogs: YAML catalogs of scenarios (products, sets), imported
255
+ # from a document, a file or a connected repository, exported as YAML,
256
+ # synced to Active Storage, and run one set at a time as an evaluation.
257
+ # The dashboard's Scenarios page edits them entry by entry
258
+ # (ScenarioCatalogEntriesController) and previews an import before it
259
+ # is applied.
260
+ resources :scenario_catalogs, only: [ :index, :show, :create, :update, :destroy ] do
261
+ post :preview, on: :collection
262
+ member do
263
+ get :export
264
+ post :sync
265
+ post "sets/:set_id/run", action: :run_set, as: :run_set
266
+ post "sets/:set_id/materialize", action: :materialize_set, as: :materialize_set
267
+ end
268
+ end
269
+ scope "scenario_catalogs/:id", controller: "scenario_catalog_entries", as: :scenario_catalog do
270
+ post "products", action: :create_product, as: :products
271
+ patch "products/:product_id", action: :update_product, as: :product
272
+ delete "products/:product_id", action: :destroy_product
273
+ post "products/:product_id/sets", action: :create_set, as: :product_sets
274
+ patch "sets/:set_id", action: :update_set, as: :set
275
+ delete "sets/:set_id", action: :destroy_set
276
+ post "sets/:set_id/scenarios", action: :create_scenario, as: :set_scenarios
277
+ patch "sets/:set_id/scenarios/:scenario_id", action: :update_scenario, as: :set_scenario
278
+ delete "sets/:set_id/scenarios/:scenario_id", action: :destroy_scenario
279
+ end
280
+
157
281
  # Credentials: dashboard API keys (token shown once on create) and the
158
282
  # owner's own LLM provider credentials, both encrypted at rest.
159
283
  resources :api_keys, only: [ :index, :create, :destroy ]
@@ -172,6 +296,24 @@ ActionAgent::Engine.routes.draw do
172
296
  get :callback
173
297
  end
174
298
 
299
+ # The owner's GitHub App installations: install sends the admin to GitHub,
300
+ # which returns to callback; each installation's repositories are chosen
301
+ # with PATCH, and DELETE unlinks one (the App stays installed on GitHub).
302
+ resources :github_installations, only: [ :index, :update, :destroy ] do
303
+ collection do
304
+ get :install
305
+ get :callback
306
+ end
307
+ member do
308
+ get :repositories
309
+ end
310
+ end
311
+
312
+ # Creating the GitHub App from a manifest on a self-hosted dashboard.
313
+ resource :github_app_manifest, only: [ :create ] do
314
+ get :callback
315
+ end
316
+
175
317
  # Model catalogs for the agent builder (Ollama queried live from the
176
318
  # configured host; hosted providers curated).
177
319
  resources :provider_models, only: [ :index ]
@@ -181,6 +323,14 @@ ActionAgent::Engine.routes.draw do
181
323
  # against a plan answers through ActionAgent.usage_resolver, and a bare
182
324
  # mount reports unlimited rather than 404.
183
325
  resource :usage, only: [ :show ], controller: "usage"
326
+
327
+ # The Organization view's Team Members table. The host lists them
328
+ # through ActionAgent.members_resolver; without one it is the signed-in
329
+ # user alone.
330
+ resources :members, only: [ :index ]
331
+
332
+ # The owner's trace ingest key, read by the Organization view.
333
+ resource :telemetry_key, only: [ :show ], controller: "telemetry_keys"
184
334
  end
185
335
 
186
336
  # The account's agents presented as an authenticated MCP server (tools +
@@ -202,6 +352,10 @@ ActionAgent::Engine.routes.draw do
202
352
  match "mcp", to: "api/mcp#unsupported", via: [ :get, :delete ],
203
353
  constraints: ->(request) { request.delete? || !ActionAgent::Engine.html_request?(request) }
204
354
 
355
+ # The frame the session player runs in, holding the replay bundle and no
356
+ # data. See SessionPlayerController.
357
+ get "session_player", to: "session_player#show", as: :session_player
358
+
205
359
  # Everything else under the mount is a client-side route: render the
206
360
  # dashboard and let the browser resolve it. Anchored last so it can only
207
361
  # 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.1"
4
+ VERSION = "1.9.1"
5
5
  end