Skip to content

Give hosts a seam onto the engine's models and controllers - #474

Merged
TonsOfFun merged 4 commits into
mainfrom
claude/issue-463-sparkle-30275-o5j4ko
Sep 24, 2026
Merged

TonsOfFun merged 4 commits into
mainfrom
claude/issue-463-sparkle-30275-o5j4ko

Conversation

@TonsOfFun

Copy link
Copy Markdown
Contributor

Resolves #463.

A host that mounts the engine inside an existing Rails app usually has concerns its own models and controllers already carry — a connection-switching concern that pins tables to one database, a session module its admin controllers read through. Until now the only way onto the engine's classes was to reopen them from a to_prepare block: undocumented, order-sensitive, and wrong twice over, because TelemetryTrace did not inherit ApplicationRecord and had to be patched on its own.

What changes

  • ActionAgent::TelemetryTrace inherits ActionAgent::ApplicationRecord like the other 20 engine models, dropping its duplicate AdapterAware include and gaining the ownership API (owner_association, for_owner) from the same place. Its table name stays fixed at active_agent_telemetry_traces rather than derived from table_name_prefix: the install generator's telemetry migration creates the table under that literal name whatever prefix the host sets (the dashboard-tables migration is the one that follows the prefix).
  • config.model_concerns and config.controller_concerns. Arrays of modules or class-name strings. ApplicationRecord includes the first as it loads, so every engine model carries them; ApplicationController includes the second as it loads, ahead of protect_from_forgery and before_action :authenticate_dashboard!, so a host concern's own callbacks run before the dashboard authenticates. A name is constantized when the class loads, so an initializer can name a constant the host has not autoloaded yet, and a typo raises NameError at boot naming it rather than being skipped. The ingest endpoint, Api::TracesController, inherits ActionController::API with its own bearer-token authentication and is not touched. In ApplicationRecord the include sits after abstract_class = true, because a concern whose included block reads table_name (a connection-switching concern does) would otherwise register a table for the abstract base.
  • base_controller_class= warns through ActionAgent.deprecator and points at controller_concerns. The accessor stays until 2.0 so initializers that set it keep booting.
  • Docs: a new Extending engine models and controllers section in the self-hosted guide, pointers from the dashboard and RubyLLM guides, the configuration index, the engine README, the install generator's initializer template, and CHANGELOG entries under Unreleased.

Tested against the host that motivated it

actionagent/test/host_integration_test.rb pins the host-facing contract in the shape the motivating install uses. Because the test process has long since loaded the engine's classes, the controller half comes from a child process that boots the dummy app with both lists configured (test/support/first_load_script.rb) and reports what the classes looked like on their first load: a concern's included block sees the abstract base with no table name and its inherited hook sees every model's table, the trace table among them; a concern's before_action precedes authenticate_dashboard! on every dashboard controller while Api::BaseController's forgery skip and Api::MCPController's authentication skip still hold; the ingest endpoint carries nothing; and a current_user_resolver reaches the concern's session reader while the engine's own private current_user keeps precedence over the concern's public one. In-process tests cover name resolution, the Module form, the NameError on a misnamed concern, the hierarchy (every ActionAgent:: Active Record model inherits ApplicationRecord, eager-loaded through Zeitwerk since Engine#eager_load! is a no-op), reset! and the deprecation.

The companion change in that host replaces its four to_prepare reopenings with the two settings. The one it keeps is Api::BaseController.protect_from_forgery, which is #461.

Verification

  • actionagent/test/** under gemfiles/rails8.gemfile: 453 runs, 0 failures. test/** (framework) runs green apart from the provider tests that need API keys, which fail identically without this change.
  • RuboCop on the changed files: no offenses.

🤖 Generated with Claude Code

https://claude.ai/code/session_01R333pNcMuXB4CZPfPKao4n


Generated by Claude Code

A host that mounts the engine has concerns its own models and controllers
already carry — a connection-switching concern, a session module — and the
only way onto the engine's classes was to reopen them from a to_prepare
block: undocumented, order-sensitive, and wrong twice over, because
TelemetryTrace inherited ActiveRecord::Base rather than ApplicationRecord
and had to be patched on its own.

TelemetryTrace now inherits ApplicationRecord like every other engine
model. Its table name stays fixed rather than derived from the prefix,
because the telemetry migration creates the table under its literal name.

ActionAgent.model_concerns is included into ApplicationRecord as it loads,
after abstract_class is set, so a concern's included block sees no table
name and its inherited hook sees every model; ActionAgent.controller_concerns
is included into ApplicationController ahead of its own callbacks, so a
concern's before_action runs before the dashboard authenticates. Entries are
modules or names resolved when the class loads, so a typo fails at boot
naming itself. The ingest endpoint inherits ActionController::API and is
not touched. Assigning the never-consumed base_controller_class now warns
and points at controller_concerns.

The new test boots the dummy app in a child process with both lists
configured and reads what the classes looked like on their first load;
reopening ApplicationController in the test process would reorder every
loaded subclass's callback chain.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R333pNcMuXB4CZPfPKao4n
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfFzc97pARf86bj8LNR59B
The dashboard API now verifies CSRF tokens (#461), so asserting that
Api::BaseController skips it would be wrong once that lands; it also
failed on Rails main, where a skip leaves differently named callbacks
in the chain. ApiForgeryProtectionTest exercises the behaviour instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfFzc97pARf86bj8LNR59B
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KfFzc97pARf86bj8LNR59B
@TonsOfFun
TonsOfFun merged commit a8e7618 into main Sep 24, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Host integration seams: engine models and controllers can only be extended by monkey-patching

2 participants