activeagent 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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1058 -0
  3. data/README.md +9 -1
  4. data/lib/active_agent/base.rb +22 -1
  5. data/lib/active_agent/concerns/delegation.rb +4 -0
  6. data/lib/active_agent/concerns/input_requests.rb +178 -0
  7. data/lib/active_agent/concerns/parameterized.rb +7 -4
  8. data/lib/active_agent/concerns/provider.rb +1 -0
  9. data/lib/active_agent/concerns/tooling.rb +7 -2
  10. data/lib/active_agent/delegation/runner.rb +31 -0
  11. data/lib/active_agent/evals/catalog.rb +305 -0
  12. data/lib/active_agent/evals/ruby_llm.rb +411 -0
  13. data/lib/active_agent/evals/scenario.rb +10 -3
  14. data/lib/active_agent/evals/scenario_parser.rb +38 -10
  15. data/lib/active_agent/evals.rb +1 -0
  16. data/lib/active_agent/generation.rb +62 -2
  17. data/lib/active_agent/generation_job.rb +14 -3
  18. data/lib/active_agent/input_request/resume.rb +199 -0
  19. data/lib/active_agent/input_request.rb +279 -0
  20. data/lib/active_agent/providers/_base_provider.rb +153 -31
  21. data/lib/active_agent/providers/anthropic/transforms.rb +57 -28
  22. data/lib/active_agent/providers/anthropic_provider.rb +102 -20
  23. data/lib/active_agent/providers/common/responses/prompt.rb +21 -0
  24. data/lib/active_agent/providers/concerns/input_requests.rb +332 -0
  25. data/lib/active_agent/providers/concerns/mcp_serving.rb +267 -0
  26. data/lib/active_agent/providers/concerns/tool_choice_clearing.rb +18 -3
  27. data/lib/active_agent/providers/deep_seek_provider.rb +6 -0
  28. data/lib/active_agent/providers/deepseek/_types.rb +4 -0
  29. data/lib/active_agent/providers/deepseek/chat/_types.rb +54 -0
  30. data/lib/active_agent/providers/deepseek/chat/request.rb +121 -0
  31. data/lib/active_agent/providers/deepseek/options.rb +53 -0
  32. data/lib/active_agent/providers/deepseek_provider.rb +46 -0
  33. data/lib/active_agent/providers/log_subscriber.rb +13 -0
  34. data/lib/active_agent/providers/mcp_bridge.rb +691 -0
  35. data/lib/active_agent/providers/mcp_tool_cache.rb +230 -0
  36. data/lib/active_agent/providers/open_ai/_base.rb +1 -1
  37. data/lib/active_agent/providers/open_ai/chat_provider.rb +22 -3
  38. data/lib/active_agent/providers/open_ai/options.rb +1 -1
  39. data/lib/active_agent/providers/open_ai/responses_provider.rb +68 -15
  40. data/lib/active_agent/providers/open_router/options.rb +6 -2
  41. data/lib/active_agent/providers/ruby_llm/messages/_types.rb +3 -1
  42. data/lib/active_agent/providers/ruby_llm/options.rb +5 -0
  43. data/lib/active_agent/providers/ruby_llm_provider.rb +127 -29
  44. data/lib/active_agent/schema_tools.rb +12 -0
  45. data/lib/active_agent/telemetry/instrumentation.rb +10 -1
  46. data/lib/active_agent/version.rb +1 -1
  47. data/lib/active_agent.rb +2 -0
  48. data/lib/generators/active_agent/schema_tools/USAGE +11 -0
  49. data/lib/generators/active_agent/schema_tools/schema_tools_generator.rb +39 -4
  50. data/lib/generators/active_agent/schema_tools/templates/schema_tools.rb.tt +13 -0
  51. metadata +31 -2
data/CHANGELOG.md CHANGED
@@ -7,6 +7,1022 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.9.1] - 2026-10-16
11
+
12
+ Releases `activeagent` and `actionagent` 1.9.1, with the matching browser
13
+ sidecar version. Adds user feedback to the dashboard. Upgrade by running
14
+ `bin/rails generate action_agent:install --skip` and `bin/rails db:migrate`
15
+ before deploying the updated dashboard (migration 027).
16
+
17
+ ### Added
18
+
19
+ - Interaction feedback in `actionagent`: submit thumbs up/down and optional
20
+ comments through the authenticated feedback API, then review and filter votes
21
+ in an agent’s Feedback tab. Includes response span attribution, retry-safe vote
22
+ updates, workspace scoping, an upgrade migration and user feedback docs (#591).
23
+
24
+ ## [1.9.0] - 2026-10-14
25
+
26
+ Releases `activeagent` and `actionagent` 1.9.0 from one tag. A minor release.
27
+ The dashboard gains **Projects**: a GitHub repository boots in a checkout
28
+ sandbox with a browser of its own, which a person can watch live and take
29
+ over, and an agent is evaluated against the running app. A browser run can
30
+ stop for a person: `request_handoff` records where the agent stopped and
31
+ Session Replay's **Take Over Session** opens that page. A run can pause to ask
32
+ the user, for an approval or a secret, and resume with the answer. Checkouts
33
+ can go through a GitHub App, provider keys can be personal as well as the
34
+ organization's, the explorer agent proposes scenarios from a project's app,
35
+ and a sandbox can open a draft pull request. The framework gains a DeepSeek
36
+ provider; `mcps:` runs client-side on every provider through `MCPBridge`, with
37
+ `mcp_strategy:` and a cached tool list (needs the optional `mcp` gem); a
38
+ generation can pause to ask the user and require approval before a tool runs;
39
+ the RubyLLM provider reports cache and reasoning tokens, streams tool-call
40
+ fragments correctly, carries usage and stop reason for streamed turns and
41
+ takes a `protocol:` option on ruby_llm 2.x; the flat `json_schema`
42
+ response_format is accepted by every provider. Evaluation scenarios can live
43
+ in a YAML **catalog** of products and sets in the repository, imported from a
44
+ connected repository at a ref, written to Active Storage and run one set at a
45
+ time as an evaluation that records the catalog version it tested; every
46
+ What-to-fix card copies a Markdown fix brief for a coding harness, or hands
47
+ it to Claude Code in the project's checkout sandbox and verifies the change
48
+ against the same scenarios; a user can sign Claude Code in to a sandbox with
49
+ their own Claude subscription (`claude_code_auth = :sandbox_login`, opt-in);
50
+ and Active Storage is one engine option (`config.active_storage`). When a
51
+ project's boot fails, Claude Code fixes the step in its checkout and the
52
+ boot resumes; a project's browser opens from its own page; Catalogs become
53
+ **Scenarios**, written, generated from a project's app, exported and
54
+ imported in the dashboard; and destructive actions confirm in the
55
+ dashboard's own dialog. Carries the 1.8.2 owner fix.
56
+
57
+ Upgrading: run `bin/rails generate action_agent:install --skip` and
58
+ `bin/rails db:migrate`. The generator adds the migrations an install lacks,
59
+ now shipped as numbered templates: input requests, recording events, sandbox
60
+ browsers, projects and their secrets, GitHub App installations, explorations,
61
+ draft pull requests, scenario catalogs and evaluation fixes (migration 026:
62
+ code sessions linked to the fix they implement, and the sandbox's Claude
63
+ login owner). `AgentRun` gains the
64
+ `awaiting_input` status, which
65
+ a client that treats every status other than `pending` and `running` as
66
+ finished must handle. Session Replay is now **Sessions** in the sidebar.
67
+
68
+ ### Added
69
+
70
+ - **Scenario catalogs** (`activeagent`). `ActiveAgent::Evals::Catalog` reads a
71
+ YAML document of products, each with sets of scenarios, from one or more
72
+ files (`Catalog.load`) or strings (`Catalog.parse`), layering later
73
+ documents over earlier ones by key as `Suite.load` does. A suite document
74
+ (`suite:` and `groups:`) loads as a catalog of one product whose sets are
75
+ its groups. `catalog.scenarios(product:, set:, keys:,
76
+ include_production_only:)` narrows the scenarios, `suite_for(product, set)`
77
+ hands one set to the `Runner` as a `Suite` named `catalog/product/set`,
78
+ `to_yaml` writes the canonical document and `digest` is its SHA-256. A
79
+ `Scenario` now carries free-form `tags` and `params` from the document.
80
+ - **Scenario catalogs on the dashboard** (`actionagent`). The Scenarios page and
81
+ `/api/scenario_catalogs` import a catalog from pasted YAML, an uploaded
82
+ file or a connected repository at a ref (`.activeagents/evals/*.yml` by
83
+ default), store it as `scenario_catalogs`, `scenario_products`,
84
+ `scenario_sets` and `catalog_scenarios` records under the owner (install
85
+ generator migration 025), export the canonical YAML, and write it to
86
+ Active Storage once per digest when the engine has it, with a pull that
87
+ restores the records from the stored document. Running a set materializes
88
+ it as an evaluation of the product's agent named `catalog/product/set`,
89
+ refreshing scenarios by key with removed ones disabled, and queues a run
90
+ whose `selection.catalog` and the evaluation's `config.catalog` name the
91
+ catalog, product, set and document digest, so results, traces and
92
+ recordings resolve to the version they tested. A set may run against a
93
+ project instead: its sandbox boots at the project's checkout ref and every
94
+ replay reaches the app and its browser, which is how a branch's acceptance
95
+ set runs from the dashboard. Importing and running need the
96
+ `replace_scenarios` permission; a run is gated as an evaluation run is.
97
+ The MCP facade gains `catalogs_list`, `catalogs_import` and
98
+ `catalog_set_run`, and `action_agent:catalogs:import`, `:export` and
99
+ `:run` rake tasks do the same from a shell.
100
+ - **Manage scenarios in the dashboard** (`actionagent`). Catalogs are now
101
+ **Scenarios** in the sidebar, at `/scenarios`, with `/catalogs` redirecting;
102
+ the tables, the API and the YAML `catalog:` key are unchanged. A collection
103
+ can be started empty and its products, sets and scenarios added, edited and
104
+ deleted in place (prompt, notes as the rubric, expected tools, contains and
105
+ never-contains, tags, enabled, production-only), each change validated as an
106
+ import validates a document. The **Generate** tab picks a project, runs the
107
+ explorer in its sandbox's browser beside the live view, and accepts its
108
+ proposals (the same records as the project's Explorations) into a chosen
109
+ set. A collection, or any products, sets and scenarios checked, exports as
110
+ YAML to download or copy; an import (pasted, uploaded or from a repository)
111
+ previews by key what it creates, overwrites, removes or keeps before it
112
+ writes, and in the dashboard keeps what the document leaves out by default.
113
+ - **Fix a failed project boot with Claude Code** (`actionagent`). When a
114
+ project's boot fails with its checkout kept, on a backend that runs Claude
115
+ Code and resumes boots, and the owner's Claude Code is connected,
116
+ `ActionAgent::ProjectBootFix` starts a Claude Code session in the checkout
117
+ with the failed step, the boot's error and the end of the step's log (the
118
+ project's secrets masked, the log fenced as data, not instructions), asking
119
+ for the smallest change. The session may run `bundle install`, `lock` and
120
+ `update`, `gem search`, `list` and `info`, `git status` and `diff`, and
121
+ `npm`/`yarn install` when a JavaScript step failed, and nothing else. When
122
+ it changes files the boot resumes from the failed step and the files join
123
+ the install pull request (the secret scan and the `.github/` exclusion
124
+ still apply); when it changes nothing the boot stays failed with Claude
125
+ Code's answer. Claude Code goes first and the setup assistant after it,
126
+ sharing "Start it when a boot fails" and the three automatic attempts.
127
+ `POST /api/projects/:id/setup/claude_code` starts one by hand, and the
128
+ project's Setup card follows the session live.
129
+ - **A project's browser on its page** (`actionagent`). A **Browser** tab
130
+ beside Overview, Explorations and Environment (`?tab=browser`) starts,
131
+ watches, takes over and hands back the sandbox's browser; the header offers
132
+ **Watch live** while it runs. Settings → Integrations stays where every
133
+ runtime is managed: each sandbox links to its project, and the project
134
+ page links back as **Manage runtimes**. The New project checklist says
135
+ what the backend's browser can do instead of "not available yet".
136
+ - **Copy fix prompt** (`actionagent`). Every What-to-fix card on an
137
+ evaluation run has a *Copy fix prompt* button that puts a Markdown brief
138
+ on the clipboard for a coding harness: the agent, evaluation and run by
139
+ name and id, the fault and recommendation or the judge's instruction
140
+ change, the tools and MCP server involved, each scenario's prompt,
141
+ expectations, tool calls, diagnosis and answer, where to look in the
142
+ agent, and the `evaluations_run`, `evaluation_runs_get` and
143
+ `evaluation_runs_compare` calls that prove the fix, with the evaluation
144
+ id, scenario keys and models filled in.
145
+ - **Implement a fix with Claude Code** (`actionagent`). On an evaluation of a
146
+ project's agent, a fault or instruction card's *Implement with Claude Code*
147
+ starts one Claude Code session in a ready checkout sandbox with a brief the
148
+ server builds from the card (the sandbox cannot reach the dashboard, so the
149
+ brief says where the agent lives and that the dashboard verifies). The
150
+ session records the run and card it fixes (`evaluation_run_id`,
151
+ `fix_item`). When it succeeds with a diff, the backend restarts the
152
+ checkout's app runtime (`refresh_runtime`, on `:local`) and the dashboard
153
+ re-runs exactly the card's scenarios and models against that sandbox
154
+ (`verification_run_id`). The card shows each scenario before and after,
155
+ what was fixed and what regressed; *Try again* carries the previous diff and
156
+ results into a new session, *Re-run the full suite* checks for regressions, and
157
+ *Open draft PR* includes the comparison and both runs. One click starts one
158
+ session; nothing launches fixes in batches or on a schedule.
159
+ - **Sign in to Claude Code inside a sandbox** (`actionagent`). With
160
+ `config.claude_code_auth = :sandbox_login`, a signed-in user connects their
161
+ own Claude subscription from Settings → Integrations, a sandbox's Claude
162
+ Code panel or the fix dialog. The `:local` backend runs the unmodified
163
+ `claude auth login` under a PTY with the sandbox's own `CLAUDE_CONFIG_DIR`;
164
+ the user authorizes on Claude's site and pastes the one-time code, which is
165
+ written once to the CLI and never stored, logged or returned. The login
166
+ belongs to that user and that sandbox (`claude_login_user_id`), and no other
167
+ member's session runs beside it; they use a sandbox of their own, with
168
+ their own login or the account's API key. Sessions on it get no
169
+ Anthropic credential variables, credential paths are excluded from file
170
+ reads, diffs and draft pull requests, and disconnect, stop and expiry run
171
+ `claude auth logout` and remove the config. Backends without the four login
172
+ verbs refuse the mode, and a remote backend also needs
173
+ `config.claude_code_hosted_login_enabled`; what a host-registered backend
174
+ raises from a login verb comes out as `SandboxOrchestrator::BackendError`
175
+ and answers with the same fixed message. `:api_key` stays the default.
176
+ - **Active Storage as one engine option** (`actionagent`). `config.active_storage`
177
+ decides whether the dashboard attaches files anywhere: run attachments, a
178
+ recording's snapshots and oversized event payloads, and scenario catalogs.
179
+ `:auto` (the default) uses Active Storage when the host has it loaded and
180
+ migrated, `true` requires it and raises `ActionAgent::ConfigurationError`
181
+ at boot without it, `false` never attaches. `config.active_storage_service`
182
+ names the `config/storage.yml` service those attachments go to. The
183
+ models' `defined?(ActiveStorage)` guards now read the option, and the
184
+ unused `config.storage_service` accessor is gone.
185
+ - **Hand a browser run to a person** (`actionagent`). Agents with the
186
+ `playwright_mcp` tools gain `request_handoff`: the agent calls it when a
187
+ page asks for something only its owner may give (payment details, a login
188
+ code), the run's session recording stores the page URL and the non-secret
189
+ values already entered, and Session Replay's **Take Over Session** opens
190
+ that page for the person. Browser tool calls in such runs are recorded, so
191
+ the run plays back in Session Replay.
192
+
193
+ - **Conference Ticket Agent template** (`actionagent`). Registers on an
194
+ event's ticket page and stops before paying, built for the SF Ruby
195
+ Conference demo. The Playwright demo template now names the published
196
+ `@playwright/mcp` package.
197
+
198
+ - **Conference-ticket sample data** (`actionagent`). `bin/rails
199
+ action_agent:sample:conference_ticket` seeds a workspace with fictional
200
+ but complete data for the scenario: the agent, a week of runs with their
201
+ telemetry traces (browser tool spans, model calls, the handoff), a
202
+ completed session recording that offers Take Over at the ticket page, and
203
+ an evaluation whose two runs show a caught regression — the agent once
204
+ clicked Pay — and the fix. Every record carries a `sample` marker;
205
+ `action_agent:sample:clear` removes it all. `ACCOUNT_ID` names the
206
+ workspace on a multi-tenant install.
207
+
208
+ - **Typing and forms in the browser tools** (`actionagent`). The
209
+ `playwright_mcp` tools gain `browser_type`, `browser_fill_form`,
210
+ `browser_select_option` and `browser_press_key`, and address elements the
211
+ way current Playwright MCP releases expect (`target`), accepting the `ref`
212
+ the schemas ask the model for.
213
+
214
+ - **Support MCP across providers** (`activeagent`). Anthropic and OpenAI
215
+ Responses handle remote servers natively; ActiveAgent bridges other providers
216
+ and all local `command:` servers. `mcp_strategy:` selects automatic, client,
217
+ or required-native execution. Tool lists are cached for five minutes; a cold
218
+ cache connects during discovery, while a warm cache connects only when a tool
219
+ is called. `mcp_cache: false` bypasses the cache for one generation. Entries
220
+ are scoped by endpoint, credentials, environment, and `allowed_tools:`. The
221
+ client-side bridge requires the optional `mcp` gem.
222
+
223
+ - **Add a DeepSeek provider** (`activeagent`). `generate_with :deepseek` uses
224
+ DeepSeek's OpenAI-compatible API, `deepseek-flash` by default, and
225
+ `DEEPSEEK_API_KEY` as the credential fallback. JSON output and tool calling
226
+ are native. Instructions are sent as `system` messages, and provider defaults
227
+ such as thinking mode are left unchanged. Thinking is billed and disables
228
+ sampling parameters; prompts can opt out with `thinking: { type: "disabled" }`.
229
+
230
+ - **Cache and reasoning tokens from the RubyLLM provider** (`activeagent`).
231
+ `response.usage` now carries `cached_tokens`, `cache_creation_tokens` and
232
+ `reasoning_tokens` when RubyLLM counted them, and a response that calls
233
+ tools reports `tool_use` as its `finish_reason` however the API ends it;
234
+ OpenAI's Responses API ends one with `stop` (#502).
235
+
236
+ - **A `protocol:` option for the RubyLLM provider** (`activeagent`). Pins
237
+ which wire protocol carries a request, as RubyLLM's own `protocol:` does:
238
+ `:chat_completions` or `:responses` for OpenAI. Set it in `generate_with`
239
+ or `config/active_agent.yml`, beside `platform:`. ruby_llm 2.x sends OpenAI
240
+ chat to the Responses API (`/v1/responses`) where 1.16 used Chat
241
+ Completions, and an OpenAI-compatible server behind `openai_api_base` may
242
+ not implement it; `protocol: :chat_completions` keeps an agent on the old
243
+ endpoint, and `config.openai_protocol` in `RubyLLM.configure` keeps every
244
+ agent there. The option needs ruby_llm 2.x; with 1.16, which has no other
245
+ protocol to choose, it raises `ArgumentError` rather than being ignored
246
+ (#502).
247
+
248
+ - **Ship dashboard migrations as numbered templates** (`actionagent`).
249
+ `rails g action_agent:install` also emits every
250
+ `templates/migrations/NNN_<name>.rb.erb` in the generator, in number order and
251
+ after the other dashboard migrations, skipping any whose name `db/migrate`
252
+ already holds. A new engine migration is one new template file.
253
+
254
+ - **Ask the host before privileged dashboard actions** (`actionagent`).
255
+ `ActionAgent.permission_checker = ->(user, action, subject) { ... }` is asked
256
+ before a provider credential is stored, tested or deleted
257
+ (`:manage_credentials`), before GitHub is connected, its repositories chosen
258
+ or it is disconnected (`:manage_github`), and before an API key is created or
259
+ revoked (`:manage_api_keys`). A denial answers 403, except that the GitHub
260
+ connect and callback navigations return to Settings instead. The remaining
261
+ actions in `ActionAgent::PERMISSION_ACTIONS` are reserved for features that
262
+ need them. Unset, every action is allowed as before, which in multi-tenant
263
+ mode means every member of a tenant may perform every action; the engine
264
+ logs a warning at boot in that case. A checker that raises denies; in
265
+ multi-tenant mode a nil answer or a missing user also denies.
266
+
267
+ - **Record who created an API key** (`actionagent`). A key created in Settings
268
+ stores the signed-in user beside its owner, and an MCP call made with an
269
+ account's key runs as that user. Keys created earlier have no creator and
270
+ behave as before.
271
+
272
+ - **Organization and personal provider keys** (`actionagent`). A provider
273
+ key is an organization key, shared by the account's members, or a member's
274
+ personal key. With `ActionAgent.provider_key_scope = :personal_override`, a
275
+ member saves personal keys in Settings → API Keys, and the runs they start,
276
+ the dashboard assistant and the model pickers use them before the
277
+ organization's; the judge, sandboxes, Claude Code and Codex use organization
278
+ keys only, as do scenario replays on a multi-tenant install, so the
279
+ evaluation form offers only the providers those keys cover. A personal
280
+ Ollama host decides where the server sends requests, so saving or testing
281
+ one asks `permission_checker` for `:manage_credentials`, with the personal
282
+ key as the subject, as an organization key does. Every lookup goes through
283
+ `ActionAgent::ProviderCredentials.resolve(owner:, actor:, provider:)`: the
284
+ actor's personal key, then `provider_credentials_resolver`, then the
285
+ organization key, then `config/active_agent.yml`. A resolver that declares
286
+ `actor:` is told who is acting. With `multi_tenant` on, an owner that does
287
+ not resolve to the configured owner class, or a resolver that raises, fails
288
+ the generation with `ProviderCredentials::Unresolved` instead of using the
289
+ platform's credentials. The provider keys API takes `scope=organization` or
290
+ `scope=personal` and returns `scope`, `effective_source`, `set_by` and
291
+ `updated_at`, and `POST /api/provider_keys/test` sends a stored key only to
292
+ the stored host. The Organization page manages the
293
+ organization's keys and lists the members `ActionAgent.members_resolver`
294
+ returns, with "+ Invite Member" linking to `ActionAgent.member_invite_url`
295
+ (`GET /api/members`). **Upgrading:** run the new `add_provider_key_scope`
296
+ migration (`rails g action_agent:install` emits it); it stops and lists the
297
+ keys if an account holds two for one provider. A host that keeps its own
298
+ `provider_keys` table with a unique `(account_id, provider)` index replaces
299
+ it with `(account_id, scope_key, provider)`. Before turning on
300
+ `:personal_override`, change any `provider_credentials_resolver` that reads
301
+ `provider_keys` by provider alone so it reads organization rows only
302
+ (`ProviderKey.for_owner`), or remove it.
303
+
304
+ - **Declare optional sandbox backend verbs** (`actionagent`).
305
+ `SandboxOrchestrator` dispatches `changed_files`, `read_file`,
306
+ `start_browser`, `stop_browser` and `resume_boot` to a backend that defines
307
+ them, and `supports?` is false for one that does not. Their signatures are
308
+ documented on the orchestrator and in the dashboard guide; the `:local`
309
+ backend implements `changed_files` and `read_file` only, and the `:mock`
310
+ backend none of them. `read_file` refuses a path outside the checkout before
311
+ the backend sees it.
312
+
313
+ - **Check out sandboxes through a GitHub App** (`actionagent`). Configure
314
+ `github_app_id`, `github_app_private_key`, `github_app_slug`,
315
+ `github_app_client_id` and `github_app_client_secret` (or the matching
316
+ `GITHUB_APP_*` variables), and Settings -> Integrations offers **Install
317
+ the GitHub App**. An installation is linked only after the App's user
318
+ authorization shows the user is the account it is installed on or an
319
+ active admin of its organization, and that user's token is then dropped.
320
+ An installation belongs to one owner at most. Its repositories are chosen
321
+ like the OAuth connection's, and a checkout of one of them mints a one-hour
322
+ token limited to that repository's contents, once per provision, held in
323
+ memory only. When a repository is reachable both ways, the installation
324
+ is used. The App's JWT is signed with OpenSSL, so there is no new
325
+ dependency. Run `rails g action_agent:install` and `rails db:migrate` for
326
+ the `github_installations` table and
327
+ `sandbox_sessions.github_installation_id`. The OAuth connection works as
328
+ before. Installing, linking, listing and choosing an installation's
329
+ repositories, and unlinking it ask `ActionAgent.permission_checker` for
330
+ `:manage_github`. An installation GitHub reports removed or suspended is
331
+ marked, and **Check again** clears the mark once GitHub serves it again.
332
+
333
+ - **Create the GitHub App from Settings** (`actionagent`). On a
334
+ single-tenant dashboard, **Create GitHub App** posts a manifest to GitHub
335
+ and shows the new App's credentials once, with the configuration to add.
336
+ The dashboard stores none of them.
337
+
338
+ - **Mask GitHub tokens in sandbox output by their shape** (`actionagent`).
339
+ `SecretScrubber` masks `ghp_`, `gho_`, `ghu_`, `ghs_`, `ghr_` and
340
+ `github_pat_` tokens even when it is given no values, so a code session's
341
+ transcript masks a checkout token it never knew.
342
+
343
+ - **Store browser events on session recordings, and read any session as a
344
+ timeline** (`actionagent`). `POST <mount>/api/session_recordings/:id/events`
345
+ takes a batch of `rrweb`, `console` and `marker` events under the
346
+ `recording_events` key, authenticated by the recording's ingest token
347
+ (`SessionRecording#issue_ingest_token!`, stored as a digest, expiring with
348
+ the recording or its sandbox) or by a dashboard session that owns the
349
+ recording. Each batch's clock offset is applied, so events are stored in
350
+ server time, and an event more than a day before the batch's `sent_at` is
351
+ refused. Batches over `ActionAgent.recording_limits` answer 413 and are
352
+ counted as dropped. The engine adds `recording_events` to the app's
353
+ `filter_parameters`. `GET .../events?after=` reads them back in time order.
354
+ `GET <mount>/api/session_recordings/:id/timeline` and
355
+ `GET <mount>/api/sessions/{context,run,scenario_result}/:id/timeline` return
356
+ message, LLM, tool and browser lanes, derived when read from runs, messages,
357
+ generations and telemetry spans. An agent's browser tool calls are recorded
358
+ as `action` events with typed values masked. A recording can belong to a
359
+ conversation (`agent_context_id`) and records its `source`. Deleting a
360
+ recording asks the permission checker about `:manage_recordings`. Adds
361
+ migration `create_active_agent_recording_events`.
362
+
363
+ - **Boot a checkout that does not bundle the engine** (`actionagent`). An
364
+ `app_runtime` sandbox of a Rails app whose `Gemfile.lock` locks no
365
+ `actionagent` now installs it first: the dashboard's own engine version (or
366
+ the same git revision or path), the generators with `--skip`, asset builds
367
+ the app defines, `db:prepare`, then the manifest. `POST /api/sandboxes`
368
+ takes `bootstrap` (`auto` by default, `always` or `never`), `start_url` and
369
+ `keep_on_failure`. Before any of a bootstrapped checkout's commands runs, a
370
+ preflight refuses one without a root `config/application.rb`, or locking
371
+ Ruby older than 3.2 or railties older than 7.2. With `"auto"` a checkout
372
+ without a `Gemfile.lock` boots as before, and with `"always"` the preflight
373
+ refuses it. Each step has a log and a timeout of its own (1800 seconds for
374
+ the whole bootstrap, or `local_sandbox_boot_timeout` on `:local` when that
375
+ is longer), and `GET /api/sandboxes/:id/boot` and `…/boot_log` read them
376
+ back, scrubbed.
377
+ With `keep_on_failure`, `POST /api/sandboxes/:id/resume_boot` continues a
378
+ failed boot from the step that failed, on the same checkout and databases.
379
+ Once the MCP facade answers, `start_url` must not answer `5xx`. The boot is
380
+ a plain-JSON spec (`ActionAgent::SandboxBootSpec`) the orchestrator hands a
381
+ backend whose `create_sandbox` takes `boot_config:`; any other backend boots
382
+ as before. The `:local` backend implements `resume_boot`, `boot_status` and
383
+ `boot_log`, and the reaper releases a kept boot once its sandbox expires.
384
+
385
+ - **Sync a checkout's agents in the sandbox manifest** (`actionagent`).
386
+ `bin/rails action_agent:sandbox:manifest` also mirrors the app's
387
+ `app/agents` classes with `AgentSync`, so a sandbox's MCP facade serves
388
+ their `run_<slug>` tools. The manifest's API key and the synced agents each
389
+ belong to the only record of the class their model is owned through, when
390
+ there is exactly one (the only account for the key and the only user for the
391
+ agents, in an app that configures both), and to nobody in an app with no
392
+ owner model, where `AgentSync` now accepts a nil owner.
393
+
394
+ - **Create evaluations and merge scenarios over MCP** (`actionagent`). The
395
+ facade's `evaluations_create` tool creates an evaluation of one of the key's
396
+ agents, with scenarios or criteria, and runs nothing. `scenarios_merge` adds
397
+ scenarios to an evaluation and updates the ones whose keys it holds, through
398
+ the new `Evaluation#merge_scenarios!`, which never removes, disables or
399
+ reorders a scenario it was not given. A scenario without a key gets the next
400
+ `<group>_<n>` the evaluation does not use, and `key_prefix` namespaces
401
+ generated keys. Both tools ask the permission checker about
402
+ `:replace_scenarios` as the key's user, refuse an observed agent's suite as
403
+ `evaluations_run` does, and are held to 100 evaluations per agent, 2,000
404
+ scenarios per evaluation, 2 MiB of scenarios per call, and 200-character
405
+ scenario keys, groups and judge models. A refused call writes nothing.
406
+
407
+ - **Generate scenario keys around keys already in use** (`activeagent`).
408
+ `ScenarioParser.parse` and `.scenarios` take `reserved_keys:`, which
409
+ generated keys skip ignoring case, and `key_prefix:`, a namespace in front
410
+ of every generated key. A prefix with no letters or digits a key can use
411
+ raises `ParseError`. Keys a paste names are unchanged.
412
+
413
+ - **Pause a generation to ask the user, and resume it with the answer**
414
+ (`activeagent`). A tool returns an `ActiveAgent::InputRequest` (`:text`,
415
+ `:choice`, `:confirm` or `:secret`); the turn's other calls finish, nothing
416
+ is sent back to the model, and the response is `awaiting_input?` with
417
+ `input_requests` and a JSON-safe `checkpoint`. `Generation#resume_now(checkpoint:,
418
+ answers:)`, on the generation that paused or on one built the same way,
419
+ restores the conversation and dispatches each paused call again with its
420
+ answer readable through `input_answer` / `InputRequest.answer_for`; `false`
421
+ declines without running the tool. A `:secret` answer is scrubbed from tool
422
+ results, telemetry tool spans and tool errors. Pauses publish
423
+ `input_requested.active_agent` and run `on_input_request` callbacks; a
424
+ delegated agent that pauses returns `{ error: "input_required" }` to its
425
+ caller instead, without announcing the pause, and raises
426
+ `Delegation::InputRequiredError` under `on_exceeded: :raise`. Supported by
427
+ every built-in tool loop, streamed or not: Anthropic, OpenAI Chat
428
+ Completions, OpenAI Responses (reasoning items included) and RubyLLM. Under a
429
+ custom provider that does not run its calls through `dispatch_tool_calls`, a
430
+ tool that asks raises `InputRequest::UnsupportedProviderError`.
431
+
432
+ - **Let a dashboard run stop to ask a person, and continue with the answer**
433
+ (`actionagent`). A paused run is `awaiting_input`, with one
434
+ `ActionAgent::InputRequest` per paused tool call (migration `016`, emitted by
435
+ `rails g action_agent:install`). The request's owner is copied from the run's
436
+ agent, and its answer and the pause's checkpoint are encrypted at rest. The
437
+ `ask` tools (`ask_user`, `request_approval`) raise `text`, `choice` and
438
+ `confirm` requests. An agent's `approval_required_tools` holds a listed
439
+ tool's call until a person approves it, and changing the list makes the
440
+ agent's evaluations stale. `request_secret` is reserved for agents the
441
+ engine defines. `GET /api/input_requests` lists requests without their
442
+ answers. `POST /api/input_requests/:id/answer` and `/decline` check
443
+ `:answer_input_request` (with no checker, a multi-tenant install lets only
444
+ the run's actor answer), are refused while execution is disabled, and
445
+ return 409 for a settled or expired request and 422 for an invalid answer.
446
+ A `confirm` request is approved by `true` and declined by `false`. Once a
447
+ pause is settled, `AgentResumeJob` resumes the same run under its trace id.
448
+ MCP `run_<slug>` returns a paused run's request ids, and
449
+ `input_requests_list` and `input_requests_answer` (text and choice only)
450
+ join the MCP facade. `config.input_request_ttl` (one day by default) bounds
451
+ how long a request waits, and `InputRequestExpiryJob`, which a host
452
+ schedules, fails the runs whose requests expired unread.
453
+
454
+ - **Open a draft pull request from a sandbox** (`actionagent`). A ready
455
+ checkout's **Open draft PR** previews the exact diff of the files it would
456
+ publish, then publishes them from the dashboard's own process through
457
+ GitHub's Git Data API: a commit on the checkout commit, a new branch (an
458
+ existing one is refused, never overwritten) and a draft pull request. No
459
+ git process holds the token, which is minted at publish time for the one
460
+ repository, or is the OAuth connection's token when the user who connected
461
+ it publishes and its scopes allow writing. `.github/`, symlinks,
462
+ submodules, oversize files and files holding one of the sandbox's secrets
463
+ or a GitHub token are never published, and a file that changed since the
464
+ preview refuses the publish. One publish carries at most 300 files and
465
+ 10 MB, chosen from every changed file. **Update draft PR** fast-forwards
466
+ the branch with a commit message of its own; a draft GitHub refuses keeps
467
+ the branch and offers a regular pull request as a second step. Where
468
+ nothing can write, **Download patch** offers the same files for `git am`.
469
+ Publishing asks `ActionAgent.permission_checker` for
470
+ `:publish_pull_request`, also when its job runs, needs a ready or running
471
+ sandbox, and runs one at a time per sandbox; a publish that never finishes
472
+ is failed as stalled after 15 minutes. The `:local` backend now implements
473
+ `changed_files` and `read_file`, which gains `base: true` (a backend whose
474
+ `read_file` takes no `base:` offers no publishing), and checks every object
475
+ of the checkout commit it reads against its id. The OAuth connect callback
476
+ records the user who connected. Run `rails g action_agent:install` and
477
+ `rails db:migrate` for the `draft_pull_requests` table.
478
+
479
+ - **Answer input requests in the dashboard** (`actionagent`). The
480
+ Interactions nav item shows how many requests are waiting, and Interactions
481
+ opens with a Needs input lane listing them. The runner keeps a paused run in
482
+ flight, shows its requests inline, and after each answer polls the same run
483
+ until its reply arrives. A `?run=` link opens a run that way. One card
484
+ answers every kind: a text field or option buttons (through Generative UI),
485
+ Approve and Decline beside a `confirm` request's arguments, and a password
486
+ field for a `secret`, marked `data-aa-secret` and emptied once sent. The
487
+ Tools tab has an Approval switch per enabled tool that writes the agent's
488
+ `approval_required_tools`, using the `approval_names` the roster endpoint
489
+ now returns for each row.
490
+
491
+ - **Projects** (`actionagent`). A project boots a GitHub repository in a
492
+ checkout sandbox, installing the engine there when the repository lacks it,
493
+ and evaluates an agent against the running app. `ActionAgent::Project` and
494
+ `ActionAgent::ProjectSecret` (migration template `019`, owned by the account
495
+ first) are served under `/api/projects` and the new **Projects** view.
496
+ `GET /api/projects/capabilities` is a checklist of what the install still
497
+ needs, with the line that fixes each item; the mock backend is refused
498
+ outside the test environment. Before any sandbox exists, the repository is
499
+ checked through GitHub's contents API (Ruby, railties, a root
500
+ `config/application.rb`) and its environment variables are found in
501
+ `.env.example`, `.env.sample`, a new `secrets:` key in
502
+ `.activeagents/sandbox.yml` and `ENV` call sites. Both read the commit the
503
+ ref names and are cached per commit. Reading a repository the GitHub
504
+ connection has not selected needs `:manage_github`. Secrets are encrypted,
505
+ never returned, refused for names the sandbox sets or that change how code
506
+ loads, and need `:manage_project_secrets`, as do changing the ref they are
507
+ handed to (preflighted again, and stopping the old ref's sandbox) and
508
+ deleting a project that has them. A provider key variable can use the
509
+ organization's stored key, with consent and `:manage_credentials`, and
510
+ without copying it. Values reach only the steps that run the repository's
511
+ code and are scrubbed, with their URL-encoded and Base64 forms, from
512
+ errors, logs, code-session events and the MCP dashboard tools. The
513
+ project's agent is an App assistant over the sandbox's tools, or a proxy for
514
+ one of the checkout's own agents; Run evaluation boots an expired sandbox
515
+ again and waits for it. The first boot on `:local` asks for confirmation,
516
+ and `quota_checker` is asked about `:project`. A boot spec that does not
517
+ apply to a checkout now still hands it its `secrets`.
518
+
519
+ - **Run a browser per checkout sandbox** (`actionagent`,
520
+ `@activeagents/browser-sidecar`). `POST <mount>/api/sandboxes/:id/browser`
521
+ (`mode: "headless"` or `"headed"`, optional `capabilities`) starts one,
522
+ `GET` shows it and `DELETE` stops it. While it runs, every agent run and
523
+ evaluation against the sandbox reaches it as the MCP server
524
+ `browser:<session_id>`, and the toolbox's own `browser_*` tools are left
525
+ out so no tool name is offered twice. A run whose browser stopped after it
526
+ was queued fails before generation. Each browser records into a session
527
+ recording of its own: rrweb with input values and `contenteditable` text
528
+ masked, console errors and markers, posted gzipped to the recording's
529
+ ingest, which now accepts `Content-Encoding: gzip`. Deleting a sandbox
530
+ stops its browser before expiring it, and a browser stops itself 30
531
+ seconds before its sandbox expires, so the recording keeps its last
532
+ events. Starting asks the quota checker about `:browser_minutes`; each
533
+ stop, including the sandbox's expiry and the reaper, reports the minutes
534
+ used, counted to when the browser stopped itself at the latest. In a
535
+ multi-tenant install both are asked about the sandbox's account. `:local`
536
+ runs the new `@activeagents/browser-sidecar` npm package (in
537
+ `browser-sidecar/`, outside the gem, at the engine's version): Chromium
538
+ with a fresh profile over a pipe, Playwright MCP behind a bearer token and
539
+ `Host`/`Origin` checks, every connection through a proxy in the sidecar
540
+ that refuses loopback, link-local and private addresses but the app's
541
+ (after redirects too), and top-level navigation pinned to the sandbox app,
542
+ with a tab that a redirect takes elsewhere closed. Install it with
543
+ `bin/rails action_agent:browser:install` and check the machine with
544
+ `bin/rails action_agent:browser:doctor`; `ActionAgent.browser_sidecar_path`
545
+ runs a checkout of it instead. The browser token is encrypted like the
546
+ runtime token, never serialized, and masked in recordings and MCP tool
547
+ output. The sidecar also builds as an OCI image. Adds migration
548
+ `add_browser_to_active_agent_sandbox_sessions`.
549
+
550
+ - **Watch a sandbox's browser live and take it over** (`actionagent`,
551
+ `@activeagents/browser-sidecar`). A ready checkout sandbox in Settings →
552
+ Integrations gets a Browser panel to start and stop its browser, and,
553
+ while it runs, a live view with "Watch live", "Take over" and "Hand back".
554
+ The sidecar streams the page on screen over a WebSocket (`/live`),
555
+ following new tabs and the tab the agent selects, and relays the input of
556
+ the one person in control. `POST <mount>/api/sandboxes/:id/browser/tickets`
557
+ issues a ticket that lives 30 seconds and is accepted once: `mode:
558
+ "view"`, sent as the WebSocket's first message, needs access to the
559
+ sandbox, and `mode: "control"`, which every Take over asks for afresh,
560
+ also asks the permission checker about `:take_over_browser`. The
561
+ WebSocket also checks `Host` and the dashboard's
562
+ `Origin` (the one the browser was started from, plus
563
+ `ActionAgent.browser_live_origins`). While a person holds control, an
564
+ agent's browser call that would change the page waits for them to hand
565
+ back and then returns an error naming them; calls that only read the page
566
+ go ahead. Control is released 10 seconds after its holder's connection
567
+ drops, and the same person may continue driving from another tab before
568
+ then. Relayed input is never logged or recorded; a takeover's start and
569
+ end are recorded as `marker` events with `source: "human"`. The sandbox
570
+ listing reports `browser_modes`.
571
+
572
+ - **Report quantities to the usage recorder** (`actionagent`).
573
+ `ActionAgent.record_usage(owner, kind, quantity)` passes the quantity to a
574
+ `usage_recorder` that takes a third argument; one that takes two is still
575
+ called as `(owner, kind)`.
576
+
577
+ - **Review explorations' candidate scenarios** (`actionagent`). An
578
+ `ActionAgent::Exploration` holds the scenarios a walk through a project's
579
+ app proposed, for a person to accept or reject on the project's
580
+ Explorations tab before any reaches the evaluation. Each candidate's
581
+ expected tools are checked against the tools the project's agent can call
582
+ (`answerable`, `needs_tool` with the missing tools, or `unverified` when the
583
+ sandbox did not answer), and its text is scrubbed of the project's secrets
584
+ and the owner's credentials before it is stored. A prompt or rubric over
585
+ 4,000 characters, an expectation list over 50 entries, or a call or
586
+ exploration over 2 MiB of JSON is refused. Accepting merges into the
587
+ project's evaluation under `x<exploration id>_<candidate id>` keys with the
588
+ rubric as `notes`, never touches another scenario, asks the permission
589
+ checker about `:replace_scenarios`, and writes each scenario so that a Save
590
+ in the suite editor leaves it unchanged. The `/api/explorations` endpoints
591
+ list, show, submit, edit, accept and stop explorations, and the MCP
592
+ facade's `explorations_submit` tool lets an agent outside the dashboard
593
+ submit candidates and read back their verdicts. The review shows a budget
594
+ meter, Stop and review, and the executions an accept-and-run uses;
595
+ `config.exploration_preselect_limit` caps how many candidates it
596
+ pre-selects. Ships as migration template `021`.
597
+
598
+ - **Interactive project setup and the install pull request** (`actionagent`).
599
+ A failed project boot starts the project's setup assistant, an agent the
600
+ engine defines whose runs have exactly `read_step_log`, `set_env`,
601
+ `request_secret` and `retry_boot` and nothing else: no shell, no file reads,
602
+ no code session. At most three automatic runs follow each other before a
603
+ boot succeeds; `PATCH /api/projects/:id/setup` with `auto: false` switches
604
+ them off, and `POST /api/projects/:id/setup` (`:manage_project_secrets`)
605
+ starts one. A new run waits until the last one is done or no longer waiting
606
+ for an answer. Each run is an execution for `quota_checker`. `set_env`
607
+ stores a value that is not secret (source `setup_assistant`, passed as boot
608
+ `env`, never masked, shown in the Environment tab), refuses the names
609
+ project secrets refuse and leaves values a person set alone. The secrets API
610
+ refuses that source. A `request_secret` question names who asks, the
611
+ repository and the variable; answering it also needs
612
+ `:manage_project_secrets`, and the answer becomes a project secret the
613
+ resumed boot gets, never the transcript, telemetry or a job argument. The
614
+ Project page answers the requests waiting on the project's agents in place
615
+ (`GET /api/projects/:id/input_requests`) and reads "Waiting for you: N".
616
+ The sandbox manifest lists the app's models and columns (secret-looking
617
+ columns left out), and a project evaluated with the App assistant chooses
618
+ which it may filter on and read (`GET /api/projects/:id/app_models`,
619
+ `PUT /api/projects/:id/schema_tools`); every later boot writes
620
+ `app/agent_tools/<model>_tools.rb` with them, also once the repository
621
+ bundles the engine and boots from its own `sandbox.yml`, and removes the
622
+ files it wrote for models taken off the list. A tools file the dashboard
623
+ did not write is left alone. `POST /api/projects/:id/install_pull_request`
624
+ publishes the install through `DraftPullRequestPublisher` with the
625
+ project's allowlist: the Gemfile and lock, the initializer and routes, the
626
+ framework's config and application agent when the bootstrap wrote them, the
627
+ engine's migrations by name, the schema, the chosen schema tools, and a
628
+ generated `.activeagents/sandbox.yml` (setup and secret names) and
629
+ `.activeagents/evals/<project>.yml` (the evaluation's enabled scenarios, as
630
+ a suite `ActiveAgent::Evals::Suite.load` reads). A publish must take every
631
+ one of those install files the sandbox changed, and a file holding a
632
+ project secret is refused. While the pull request is open every boot checks
633
+ out its branch without installing again, and once GitHub reports it merged
634
+ the project is installed. A boot spec step marked `always` runs whether the
635
+ spec applies or not: after a checkout's own `sandbox.yml` setup when it does
636
+ not.
637
+
638
+ - **Declare schema tool columns from the generator** (`activeagent`).
639
+ `bin/rails generate active_agent:schema_tools Model --filterable a --returns
640
+ a b` writes those columns as the class's allowlist instead of commented
641
+ suggestions, leaving out a column that looks like a secret or that the
642
+ model lacks. `ActiveAgent::SchemaTools::SECRET_COLUMNS` names the pattern.
643
+ `--managed` heads the file with `ActiveAgent::SchemaTools::MANAGED_MARKER`,
644
+ which a dashboard project looks for before it rewrites or removes the file.
645
+
646
+ - **Explore a project's app with the engine's explorer agent**
647
+ (`actionagent`). `POST <mount>/api/projects/:id/explorations` (and
648
+ **Explore the app** on the Project page) asks the quota checker about the
649
+ new `:exploration` kind (402 and nothing started on a denial), gives the
650
+ project's ready sandbox a browser, records `:exploration` once and walks
651
+ the app within a budget of minutes, browser steps and an optional cost
652
+ (15 minutes and 150 steps by default). The explorer is an agent run of a
653
+ project-owned agent, traced and recorded: it is offered an allowlist of
654
+ the browser's tools pinned to the app, `sign_in(secret_ref:)`,
655
+ `read_last_email(to:)`, `propose_candidate` (verdicts against the target
656
+ agent's real tools, provenance with the pages, steps and recording range
657
+ filled in server-side) and `finish`. Browser results are scrubbed of the
658
+ project's secrets and cut to 24,000 characters each. Running out of
659
+ budget or of conversation room, or Stop and review, moves the exploration
660
+ to review with its candidates; a crash fails it and keeps them.
661
+
662
+ - **Sign a project's browser in without a model seeing the credentials**
663
+ (`actionagent`, `@activeagents/browser-sidecar`). A project secret now has
664
+ a `kind`: `env` (as before), `sign_in` (login URL, login, password and
665
+ optional field selectors) or `storage_state` (a saved browser sign-in).
666
+ Only `env` secrets reach the sandbox's environment, the two sign-in names
667
+ are refused for `env` secrets, and the password and a saved sign-in's
668
+ session values are scrubbed on their own. `sign_in` types the credentials
669
+ from the Rails process, never logging them, and answers only whether the
670
+ browser left the login form, or `unsupported` when the login page has no
671
+ password field.
672
+ `<mount>/api/projects/:id/sign_in` sets, checks, saves from the running
673
+ browser and removes them, asked as `:manage_project_secrets`. The sidecar
674
+ takes a `storage_state` to start with and answers `GET /storage-state`.
675
+ Adds migration `add_kind_to_active_agent_project_secrets`.
676
+
677
+ - **Read the mail a sandbox app sends** (`actionagent`). Sandbox backends set
678
+ `ACTION_AGENT_SANDBOX_MAIL_DIR`, which switches the sandbox app's Action
679
+ Mailer to file delivery, and `ActionAgent::SandboxMail.last_message`
680
+ reads the newest message for an address through the backend's
681
+ `read_file` verb.
682
+
683
+ - **Mask JSON-escaped secrets** (`actionagent`).
684
+ `ActionAgent::SecretScrubber.with_encodings` also lists each value as it
685
+ appears escaped inside a JSON string, so a secret with a quote or a
686
+ backslash is masked in JSON output too.
687
+
688
+ - **Give a project's evaluation runs the sandbox's browser**
689
+ (`actionagent`). A run started from the project page reaches the
690
+ sandbox's browser in every replay, starting it headless with the
691
+ project's saved sign-in when none runs and opening the start URL before
692
+ each replay, and records the browser in its selection. A browser that
693
+ cannot start fails the run before any replay, and one the run started is
694
+ stopped when it ends.
695
+
696
+ - **List and replay sessions in the dashboard** (`actionagent`). The Sessions
697
+ page (`<mount>/sessions`, from `GET <mount>/api/sessions`) lists every
698
+ conversation, evaluation scenario replay and lone agent browser recording,
699
+ newest first, with server-side filters for agent, `user=me`, source,
700
+ outcome and a last-activity date range, a page at a time by cursor.
701
+ `<mount>/replay/:id` replays a recording and `<mount>/replay/:kind/:id` a
702
+ conversation, run or scenario result: messages, model calls and tool calls
703
+ on one axis, with idle stretches shortened, a scrubber, stepping and speed.
704
+ A recorded browser replays beneath them in step, in a frame served at
705
+ `<mount>/session_player` that runs the new `action_agent_replay.js` bundle
706
+ (rrweb) under a policy that allows no other script and loads nothing from
707
+ the network. The engine adds the bundle to the Sprockets precompile list.
708
+ The Run Agent workbench, Interactions, an agent's interactions page and
709
+ evaluation scenario results link to their replays.
710
+
711
+ - **Record the Run Agent workbench for its conversation's replay**
712
+ (`actionagent`). While the workbench has a conversation open, the dashboard
713
+ records the page with rrweb, each visit into a recording of its own
714
+ (`source: "dashboard"`), which replays in the conversation's browser lane,
715
+ and says so beside the conversation. It stops when you leave the workbench
716
+ or switch conversation, and no other view is recorded. Field values are
717
+ masked. Elements marked `data-aa-secret` (the dashboard's credential fields
718
+ and key displays), hidden inputs and the CSRF token are left out, and the
719
+ server masks the owner's stored credentials in every batch before storing
720
+ it. The recorder is a new bundle, `action_agent_recorder.js`, imported only
721
+ while it records and added to the Sprockets precompile list. `POST
722
+ <mount>/api/session_recordings` with `agent_context_id` starts the caller's
723
+ recording of a visit to a conversation.
724
+ `ActionAgent.capture_dashboard_sessions = false` turns it off: no recorder
725
+ loads, and that endpoint and dashboard-session batches answer 403.
726
+
727
+ - **Require approval before a tool runs** (`activeagent`). The
728
+ `requires_approval:` prompt option lists tools whose calls pause with a
729
+ `:confirm` request carrying the tool name and arguments before the tool runs;
730
+ `true` runs it, `false` declines. It covers agent tools, delegations and
731
+ client-side MCP tools, and is never sent to the provider. An MCP declaration's
732
+ own `require_approval` (`"always"`, or a map of `always`/`never` tool lists)
733
+ gates its tools the same way. A remote MCP server that may offer a listed
734
+ tool runs client-side so that its calls are gated too: one whose
735
+ `allowed_tools:` include the tool, or one without `allowed_tools:` when no
736
+ tool in `tools:` has the name; `mcp_strategy: :server` with such a server
737
+ raises `ArgumentError`, as does a `requires_approval:` value that is not tool
738
+ names. A paused call to an MCP tool its server no longer offers returns an
739
+ `{ error: }` result on resume.
740
+
741
+ - **Resume a paused generation in a job** (`activeagent`).
742
+ `Generation#resume_later(checkpoint:, answers:, **job_options)` validates the
743
+ answers, refuses any answer to a `:secret` request, and enqueues
744
+ `GenerationJob` with a new `resume:` argument; params and actor travel as they
745
+ do for `generate_later`. `InputRequest#arguments` holds the arguments of the
746
+ paused call.
747
+
748
+ ### Changed
749
+
750
+ - **Confirm destructive actions in the dashboard's own dialog**
751
+ (`actionagent`). Every `window.confirm` is gone: a `useConfirm()` dialog
752
+ (an `alertdialog` with a focus trap, Escape to cancel and focus starting on
753
+ Cancel) asks instead, and deleting a project, an agent, an evaluation or a
754
+ scenario collection asks for its name to be typed first.
755
+ - **New project shows the repository first** (`actionagent`). The repository
756
+ picker heads the page and the capabilities checklist folds to one line,
757
+ opening itself when something blocks; each fix the checklist names links to
758
+ where it is made.
759
+ - **The install pull request never publishes a local gem path**
760
+ (`actionagent`). A Gemfile or lockfile change that newly points a gem at a
761
+ path outside the repository is refused.
762
+ - **Transcripts leave out hook, thinking and rate-limit events**
763
+ (`actionagent`). A code session's transcript shows what Claude Code did.
764
+ - **Releases check the browser sidecar against npm** (release workflow).
765
+ `@activeagents/browser-sidecar` is staged through npm trusted publishing
766
+ (`npm stage publish`, approved by a maintainer with 2FA), and a release
767
+ fails when `browser-sidecar/` differs from the version already on npm.
768
+ - **Keep the shared Playwright MCP browser out of multi-tenant installs**
769
+ (`actionagent`). With `multi_tenant = true` the toolbox's `playwright_mcp`
770
+ tools are neither offered nor called, and `PlaywrightMCPClient.instance`
771
+ raises: a run gets a browser only from its sandbox.
772
+
773
+ - **The dashboard page no longer carries the telemetry key**
774
+ (`actionagent`). `account.telemetry_api_key` is gone from the dashboard's
775
+ `data-props`, so nothing that reads the page's markup sees it. The
776
+ Organization page reads it from `GET <mount>/api/telemetry_key` when you
777
+ show or copy it. A host layout or script that read it from the props must
778
+ call that endpoint.
779
+
780
+ - **Mask dashboard API keys and the telemetry key in MCP tool output**
781
+ (`actionagent`). The MCP facade masks the same owner credentials as session
782
+ recording ingest, which adds the owner's dashboard API keys and telemetry
783
+ key to the provider keys, GitHub tokens and sandbox runtime tokens it
784
+ already masked.
785
+
786
+ - **Session Replay is now Sessions** (`actionagent`). The sidebar entry opens
787
+ the Sessions list, and `<mount>/replay` without an id opens it too. The
788
+ replay view plays a session's timeline and its rrweb recording instead of
789
+ loading the recording's page URL in an iframe and animating a cursor over
790
+ it. A recording that carries handoff state still offers Take over session.
791
+
792
+ - **Record an agent's browser tool calls as recording events**
793
+ (`actionagent`). `ActionAgent::MCPRecordingMiddleware#intercept` now stores
794
+ each call to a tool in `MCPRecordingMiddleware::PLAYWRIGHT_TOOLS` as one
795
+ `action` event, with typed values masked and the owner's credentials
796
+ scrubbed, instead of writing `RecordingAction`s through
797
+ `SessionRecordingService`. It no longer extracts screenshots from tool
798
+ results. `AgentExecutionService` wraps every tool call with it, so a run's
799
+ first browser call starts the run's recording (`source: "agent"`). The
800
+ constructor no longer starts a recording: `#recording` and
801
+ `#recording_service` find or start one on first use, and are `nil` when none
802
+ can be started. The Session Replay page reads `/actions`, so it lists these
803
+ recordings with no actions until the replay view moves to the timeline; read
804
+ them from `GET .../timeline` or `GET .../events?kind=action`. The
805
+ `record_navigate`, `record_click`, `record_type` and `capture_for_handoff`
806
+ helpers still write `RecordingAction`s. `PLAYWRIGHT_TOOLS` matches the tools
807
+ of `@playwright/mcp` 0.0.83, and `browser_press_sequentially`'s text is
808
+ masked like `browser_type`'s. `browser_scroll`, which Playwright MCP does
809
+ not have, is gone.
810
+
811
+ - **Announce run and sandbox changes without their content** (`actionagent`).
812
+ The Action Cable messages on `agent_run_<id>`, `agent_runs_<agent_id>` and
813
+ `sandbox_<session_id>` are now `{ type, id, status }`; a subscriber reads
814
+ the run or sandbox back over the JSON API. Nothing is broadcast when the host
815
+ has not loaded Action Cable. A host channel or client that read `run`,
816
+ `sandbox`, `task`, `error` or `provider` from these messages must refetch.
817
+
818
+ - **`GenerationJob` no longer logs its arguments** (`activeagent`). They can
819
+ hold a paused generation's conversation and the user's answers.
820
+
821
+ - **MCP declarations that set `require_approval` run client-side**
822
+ (`activeagent`). On Anthropic and OpenAI Responses, a `url:` declaration whose
823
+ `require_approval` is anything but `"never"` is served by ActiveAgent's MCP
824
+ bridge rather than handed to the provider, so its calls can pause for
825
+ approval; `mcp_strategy: :server` with one raises `ArgumentError`. Such
826
+ servers now connect from your app and need `gem "mcp"`; set
827
+ `require_approval: "never"` to keep one with the provider.
828
+
829
+ - **Add the `awaiting_input` run status** (`actionagent`). `AgentRun` status
830
+ `5` is a run waiting on its input requests: neither `in_progress?` nor
831
+ `finished?`, never picked up again by `AgentExecutionJob`, and cancellable
832
+ with `cancel!`, which also cancels its pending requests. A client that treats
833
+ every status other than `pending` and `running` as finished must handle it.
834
+
835
+ - **Filter `answer` and `value` from request logs** (`actionagent`). The engine
836
+ adds them to the host's `filter_parameters`, beside `credential`, `api_key`
837
+ and `access_token`, matching only parameters named exactly `answer` or
838
+ `value`, at any depth, including under Rails 8.2, which matches a host's
839
+ precompiled filters against the dotted parameter path. Rails also copies
840
+ `filter_parameters` into Active Record's `filter_attributes`, so a host
841
+ model attribute named `answer` or `value` shows as `[FILTERED]` in
842
+ `inspect` and in logged SQL binds.
843
+
844
+ - **CI covers ruby_llm 1.16 and 2.x with explicit bundles** (`activeagent`).
845
+ `gemfiles/ruby_llm_2.gemfile` runs the full suite on ruby_llm 2.x beside
846
+ `gemfiles/ruby_llm_1.gemfile`, so the 2.x adapter keeps its coverage even
847
+ when the default bundle resolves to 1.x (#502).
848
+
849
+ ### Fixed
850
+
851
+ - **An owner of another class reads no other tenant's rows** (`actionagent`).
852
+ `Ownable.for_owner` scoped by the owner's id alone, so an account-owned
853
+ model handed a user — the stored provider-key fallback of an agent run,
854
+ `ProviderKey.for_owner(agent.owner)`, when agents are owned per user — read
855
+ whichever account shared that user's id, and the run generated with that
856
+ account's key. The scope now matches the owner by class: a user handed to an
857
+ account-owned model maps to its tenant through `ActionAgent.tenant_for`, and
858
+ any other mismatch scopes to nothing. `TelemetryTrace.for_account` matches
859
+ the same way, and `owner=` raises `ArgumentError` for an owner that resolves
860
+ to nothing rather than writing its id into another class's column.
861
+ **Upgrading:** a multi-tenant install that configures both `account_class`
862
+ and `user_class` must own every model by one class or map between them.
863
+ Re-declare `owned_by :account, :user` on `Agent`, `SandboxSession`,
864
+ `SessionRecording` and `CodeSession` from `to_prepare` and set
865
+ `tenant_resolver`, as the install generator's template now shows; rows those
866
+ models stored under `user_id` with the account's id need `account_id`
867
+ backfilled, or the dashboard lists nothing for them.
868
+
869
+ - **Fix Anthropic structured output mapping** (`activeagent`). Preserve caller
870
+ `output_config` and ignore unsupported response formats.
871
+
872
+ - **Fix Anthropic JSON emulation on current models and with thinking**
873
+ (`activeagent`). `json_object` requests reach current Claude models
874
+ without a prefill, which they refuse with or without thinking, and so do
875
+ requests with thinking on. The JSON is read from the answer, inside a code
876
+ fence or not, and a retry drops an unparseable answer rather than ending on
877
+ it. Claude 4.5 and earlier models keep the lead-in while thinking is off.
878
+
879
+ - **Prevent Anthropic response fields from leaking into replayed requests**
880
+ (`activeagent`). Keep only request-supported message fields.
881
+
882
+ - **Name conflicting gems in provider load errors** (`activeagent`). Name
883
+ `ruby-openai` and show the Gemfile replacement only when that gem is
884
+ activated or defined `OpenAI`; when something else defines it, say where.
885
+
886
+ - **Start `command:` MCP servers that declare `env:`** (`activeagent`). The
887
+ declaration's symbolized environment keys reached the process spawn, which
888
+ requires Strings, so every stdio server that needed credentials in its
889
+ environment failed with a TypeError.
890
+
891
+ - **Return an MCP server's JSON-RPC error to the model, marked as a failure**
892
+ (`activeagent`). The client raises on an error envelope; the bridge hands
893
+ the message back as the tool's result instead of failing the generation.
894
+ That error, like a result the tool marks `isError`, reaches the model as
895
+ `{"error": message}`, with `is_error: true` on Anthropic, so it no longer
896
+ reads as a successful result.
897
+
898
+ - **Serve String-keyed `mcps:` declarations natively where the provider can**
899
+ (`activeagent`). Keys are symbolized before partitioning, so a declaration
900
+ loaded from YAML is no longer bridged on Anthropic or OpenAI Responses.
901
+
902
+ - **Keep credentials out of the `mcps:` declaration error** (`activeagent`).
903
+ The message names the declaration's keys, not its values.
904
+
905
+ - **Refuse an unknown `mcp_strategy:`** (`activeagent`) instead of treating it
906
+ as `:auto`.
907
+
908
+ - **Create the MCP tool cache's lock with the class** (`activeagent`), so the
909
+ first concurrent callers share it.
910
+
911
+ - **Apply `read_timeout:` to `url:` MCP servers** (`activeagent`). The option
912
+ was accepted on a remote server but neither validated nor applied, so its
913
+ requests waited as long as Net::HTTP allowed. It is validated for both
914
+ transports and sets the read timeout of the server's HTTP connection, 30
915
+ seconds by default, so a streamed response stays open while it keeps sending
916
+ events.
917
+
918
+ - **Bound a `command:` MCP server's whole answer by `read_timeout:`**
919
+ (`activeagent`). The wait restarted with every frame read, so a server that
920
+ kept sending notifications or pings without answering held the generation
921
+ open indefinitely. A server that misses the deadline is stopped, and
922
+ `MCPBridge::TimeoutError` names the server and the request. The handshake
923
+ keeps the `mcp` gem's 5-second bound on its `server/discover` probe instead
924
+ of waiting the full read timeout.
925
+
926
+ - **Use an explicit OpenRouter key before the environment** (`activeagent`).
927
+ An `api_key` or `access_token` passed to the OpenRouter provider wins over
928
+ `OPENROUTER_API_KEY` and the other OpenRouter variables, as it does for the
929
+ other providers. An explicit `api_key` is no longer dropped when no
930
+ OpenRouter variable is set. A blank explicit key is also used as given, so a
931
+ config value that renders as an empty string no longer falls back to the
932
+ environment; leave the key unset to use the environment.
933
+
934
+ - **Use an explicit OpenAI project before the environment** (`activeagent`).
935
+ A `project` or `project_id` passed to the OpenAI provider wins over
936
+ `OPENAI_PROJECT_ID`, and an explicit `project_id` is no longer dropped.
937
+
938
+ - **Stop Session Replay spinning when there is nothing to play**
939
+ (`actionagent`). With no recordings, a failed list request or a network
940
+ error, the page shows the empty state, or an error when the list failed.
941
+
942
+ - **List only the session recordings the caller can open** (`actionagent`).
943
+ The list uses the rule that opening a recording does, so a recording named
944
+ `lander_demo` is no longer listed for every caller.
945
+
946
+ - **Replay every action of a long recording** (`actionagent`). Session Replay
947
+ pages through `GET /api/session_recordings/:id/actions` with
948
+ `after_sequence`, up to 5,000 actions, instead of stopping at the first 100.
949
+ That endpoint's `has_more` is exact and its `limit` is clamped to 1..500.
950
+
951
+ - **Key show timeline entries `action_type`** (`actionagent`). They match
952
+ `/actions` entries; `type` remains as an alias.
953
+
954
+ - **Accept reasoning items in streamed OpenAI Responses** (`activeagent`). A
955
+ streamed response from a reasoning model raised `Unexpected Item Type`; its
956
+ reasoning items are now kept and sent back with the function calls they led to.
957
+
958
+ - **Send each RubyLLM tool result as its own message** (`activeagent`). A turn
959
+ with several tool calls sent their results merged into one message, under the
960
+ first call's id.
961
+
962
+ - **Fix a forced Anthropic `tool_choice` failing on a third turn**
963
+ (`activeagent`). Once the forced tool was used, the next turn cleared
964
+ `tool_choice`, and the turn after it raised `Anthropic::Errors::ConversionError`
965
+ reading the cleared value back.
966
+
967
+ - **A flat `json_schema` response_format under every provider**
968
+ (`activeagent`). The OpenAI providers and OpenRouter accept `name`,
969
+ `schema` and `strict` beside `type` as well as under `json_schema`, but
970
+ the RubyLLM provider raised "needs a schema for a json_schema
971
+ response_format" for the flat shape, the Anthropic provider sent
972
+ `output_config.format` with no schema, and an agent's `prompt` looked for
973
+ an `{action}.json` view instead, raising `ActionView::MissingTemplate`
974
+ for any provider. All three now read the flat shape, so a
975
+ `response_format` keeps working when `generate_with` changes provider
976
+ (#505).
977
+
978
+ - **Usage and stop reasons in RubyLLM streams** (`activeagent`). Streaming
979
+ preserves the final stop reason and token counts, including counts sent
980
+ across separate chunks. Cumulative counts are merged within a turn and
981
+ summed across tool turns. A later chunk that omits cache writes no longer
982
+ erases an earlier count with RubyLLM's synthetic zero. Cache-only and
983
+ reasoning-only usage is also preserved when input and output counts are
984
+ absent (#502).
985
+
986
+ - **Streamed tool calls through the RubyLLM provider** (`activeagent`).
987
+ An API streams a tool call's arguments as fragments of one JSON string,
988
+ and only the first fragment says which call it belongs to. The provider
989
+ took every later fragment for a call of its own with no name, so a
990
+ streamed tool call ran twice, once with no arguments and once under no
991
+ name, and two streamed calls ran on arguments joined into invalid JSON.
992
+ Fragments now go to the call they belong to, for OpenAI (Chat Completions
993
+ and Responses) and Anthropic, on ruby_llm 1.16 and 2.x. Arguments an API
994
+ sends whole, as Gemini does, are serialized as JSON instead of Ruby's
995
+ `Hash#to_s`; and Anthropic's empty opening input no longer leaves `{}` in
996
+ front of the arguments. Interleaved parallel OpenAI calls require
997
+ ruby_llm 2.x, since 1.16 discards their stream indices (#502).
998
+
999
+ ## [1.8.2] - 2026-10-01
1000
+
1001
+ Releases `activeagent` and `actionagent` 1.8.2 from one tag. A patch on 1.8.1:
1002
+ the dashboard engine's owner scopes match an owner by class, so an
1003
+ account-owned record is no longer read through a user that shares the
1004
+ account's id. No migrations. The `activeagent` gem changes only its version.
1005
+
1006
+ ### Fixed
1007
+
1008
+ - **An owner of another class reads no other tenant's rows** (`actionagent`).
1009
+ `Ownable.for_owner` scoped by the owner's id alone, so an account-owned
1010
+ model handed a user — the stored provider-key fallback of an agent run,
1011
+ `ProviderKey.for_owner(agent.owner)`, when agents are owned per user — read
1012
+ whichever account shared that user's id, and the run generated with that
1013
+ account's key. The scope now matches the owner by class: a user handed to an
1014
+ account-owned model maps to its tenant through `ActionAgent.tenant_for`, and
1015
+ any other mismatch scopes to nothing. `TelemetryTrace.for_account` matches
1016
+ the same way, and `owner=` raises `ArgumentError` for an owner that resolves
1017
+ to nothing rather than writing its id into another class's column.
1018
+ **Upgrading:** a multi-tenant install that configures both `account_class`
1019
+ and `user_class` must own every model by one class or map between them.
1020
+ Re-declare `owned_by :account, :user` on `Agent`, `SandboxSession`,
1021
+ `SessionRecording` and `CodeSession` from `to_prepare` and set
1022
+ `tenant_resolver`, as the install generator's template now shows; rows those
1023
+ models stored under `user_id` with the account's id need `account_id`
1024
+ backfilled, or the dashboard lists nothing for them.
1025
+
10
1026
  ## [1.8.1] - 2026-10-01
11
1027
 
12
1028
  ### Added
@@ -443,6 +1459,48 @@ output in the shape ruby_llm reads. No migrations.
443
1459
  `json_object`, which ruby_llm has no mode for, and a `json_schema`
444
1460
  without a schema now raise `ArgumentError` instead of sending a request
445
1461
  the API rejects (#501).
1462
+ - **`ActiveAgent::Evals::RubyLLM`** — the RubyLLM side of an evaluation,
1463
+ which hosts driving RubyLLM conversations had been writing for themselves.
1464
+ `require "active_agent/evals/ruby_llm"` (it requires `ruby_llm`;
1465
+ `require "active_agent/evals"` alone still does not) gives two helpers, for
1466
+ RubyLLM 1.16 and later and for 2.x:
1467
+ - `RubyLLM.judge(label:, model:, provider:, context:, correlation:)`, a
1468
+ `Judge` that answers from `context.chat(...)` and, with a `Correlation`,
1469
+ traces each call under the kind it serves. `temperature:` and a
1470
+ `configure:` hook set the chat up; any other keyword must be one
1471
+ `RubyLLM::Chat.new` takes, and anything else raises when the judge is
1472
+ built rather than failing every call. `on_usage:` reports each call's
1473
+ model, tokens and cost.
1474
+ - `RubyLLM.replay(messages)`, a `Replay` from a conversation's messages.
1475
+ `acts_as_chat` records of either RubyLLM generation are read through
1476
+ their own `to_llm`, so a 2.x table, or one between 2.x's upgrade and its
1477
+ cleanup, replays the same as `RubyLLM::Message` values. Tool calls keep
1478
+ the order the model made them in, each paired with the result that
1479
+ answers it; one is errored when that result reports an error at its top
1480
+ level — JSON whose `"error"` is a non-empty string, object or array, or
1481
+ `true`; an MCP result with `"isError": true`; or the inspected
1482
+ `{ error: "..." }` RubyLLM 1.x stores for a tool's error Hash with a
1483
+ String error — with the error (JSON-encoded, at most 1,200 bytes) as its
1484
+ detail. `input_tokens` counts the whole prompt, cache reads and writes
1485
+ included. Cost is RubyLLM's own price, left nil when any message is
1486
+ unpriced. The answer is the last reply after the last user message; a
1487
+ conversation that stopped at a tool call or on the user's own message
1488
+ has no answer and says so in its `error` (on RubyLLM 1.x, pass `answer:`
1489
+ for a tool that ends the turn with `halt`).
1490
+ - **`ActionAgent::ProviderKey.credentials_for(owner)` and
1491
+ `.apply_to(config, owner:)`** (`actionagent`) hand an owner's API keys to
1492
+ code outside the engine — `{ "openai" => "sk-..." }`, or written through
1493
+ `<provider>_api_key=` onto a `RubyLLM.context` config block or anything
1494
+ shaped like one, returning the providers written rather than the keys. A
1495
+ key is looked up in the order the engine's runs use: the host's
1496
+ `provider_credentials_resolver`, then the owner's saved row. A resolver
1497
+ answer that sends a provider to another endpoint (`uri_base`, `base_url`,
1498
+ `api_base`, `host`) leaves it out, so a gateway's key never reaches the
1499
+ public endpoint. On an install with an owner model, the owner must be an
1500
+ instance of the model it keeps keys by, since rows are scoped by its id
1501
+ alone; anything else raises `ArgumentError`. A saved credential that no
1502
+ longer decrypts is skipped with a warning naming the error class, never
1503
+ the value.
446
1504
 
447
1505
  ## [1.7.0] - 2026-09-24
448
1506