activeagent 1.8.1 → 1.9.0

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