class RailsAiAdmin::Doctor
Reports support boundaries without probing credentials or calling providers.
Constants
- BACKLOG_AGE
- Check
- PATCHED_RAILS
-
The gemspec admits Rails 8.0.5.2 and up, the patched 8.0 release, but cannot also exclude 8.1 releases older than their patch, 8.1.4.1.
Public Class Methods
Source
# File lib/rails_ai_admin/doctor.rb, line 11 def initialize(configuration: RailsAiAdmin.configuration, registry: RailsAiAdmin.registry, application: Rails.application, operation_registry: RailsAiAdmin.operation_registry, work_registry: RailsAiAdmin.work_registry) @configuration, @registry, @application = configuration, registry, application @operation_registry, @work_registry = operation_registry, work_registry end
Public Instance Methods
Source
# File lib/rails_ai_admin/doctor.rb, line 53 def assistant_check unless @configuration.respond_to?(:assistant) && @configuration.assistant return Check.new("assistant", :closed, "Admin Chat is off. No RubyLLM code, tables or provider calls are used.") end require "rails_ai_admin/assistant" missing = [ Assistant::Chat, Assistant::Message, Assistant::Turn, ::RubyLLM::ActiveRecord::ToolCall, ::RubyLLM::ActiveRecord::Usage ] .reject(&:table_exists?).map(&:table_name) return Check.new("assistant", :error, "Run rails_ai_admin:assistant and db:migrate; missing tables: #{missing.join(', ')}.") if missing.any? adapter = @configuration.assistant_adapter.safe_constantize unless adapter.is_a?(Class) && adapter.method_defined?(:llm_context) && adapter.instance_method(:llm_context).parameters.include?([ :keyreq, :context ]) return Check.new("assistant", :error, "#{@configuration.assistant_adapter} must define llm_context(context:) returning a RubyLLM::Context or nil.") end job = @configuration.context_adapter&.safe_constantize unless job.is_a?(Class) && job.method_defined?(:resolve_job) return Check.new("assistant", :error, "Assistant turns run in jobs; the context adapter must define resolve_job.") end Check.new("assistant", :review, "Model #{@configuration.assistant_model}; tools apply each actor's current grants and the independent assistant? grant. " \ "Schedule rails_ai_admin:assistant:scan, keep provider credentials in the adapter's own RubyLLM::Context, and verify real-provider behavior separately.") rescue NameError, ConfigurationError => error Check.new("assistant", :error, "Assistant configuration failed (#{error.class.name}).") end
Reports whether opt-in admin Chat can run. It never calls the adapter or a provider, so it cannot prove credentials, model access or grants.
Source
# File lib/rails_ai_admin/doctor.rb, line 75 def checks config = @configuration collections = @registry.resources.sum { |resource| @operation_registry.custom(resource).count { |definition| definition.scope == :collection } } result = [ adapter_check("context", config.context_adapter, { resolve: [ 1, [] ] }), adapter_check("policy", config.authorization_adapter, { scope: [ 0, %i[context resource relation] ], allowed?: [ 0, %i[context action resource record] ], fields: [ 0, %i[context resource action record] ] }), autoload_check, rails_check, assistant_check, mcp_check, Check.new("files", :unsupported, "Private attachment management is not implemented."), selection_check, Check.new("queue", @work_registry.definitions.empty? && collections.zero? ? :closed : :review, "#{@work_registry.definitions.size} outbox handlers; #{collections} collection handlers; auto_dispatch=#{config.outbox_auto_dispatch}/#{config.collection_auto_dispatch}; " \ "jobs use the #{queue_name(config).inspect} queue. Schedule the pending scans, make sure a worker processes that queue, and verify persistent queues/downstream idempotency."), backlog_check, surface_check ] result.concat(override_checks) if @work_registry.definitions.any? || collections > 0 result << adapter_check("job_context", config.context_adapter, { resolve_job: [ 0, %i[actor_ref tenant_ref security_version] ] }) result << Check.new("work_delivery_policy", :review, "Delivery and receipt grants are independent of command preconditions; default denies. Verify fresh tenant/epoch, record-version policy and one effect per item in host tests.") end @registry.resources.each do |resource| begin if config.authorization_adapter == "RailsAiAdmin::Authorization::PunditAdapter" configured = Authorization::PunditAdapter.new.configured?(resource) policy_status = resource.policy.nil? ? :closed : (configured ? :review : :error) result << Check.new("policy:#{resource.key}", policy_status, "Explicit Pundit policy and policy-owned Scope required; field grants are independent. Mapping complete=#{configured}; authorization still needs host tests.") end model = resource.model_class same_pool = model.connection_pool.equal?(OperationRun.connection_pool) && model.connection_pool.equal?(AuditEvent.connection_pool) && model.connection_pool.equal?(WorkItem.connection_pool) version = RecordVersion.strategy(model) writable = !resource.readonly? status = writable && (!same_pool || version.nil? || model.composite_primary_key?) ? :error : :ok status = :error if writable && !%w[SQLite PostgreSQL].include?(model.connection.adapter_name) message = "same_pool=#{same_pool}; version=#{version || "missing"}; readonly=#{resource.readonly?}. Host callbacks must be database-only. " \ "List pages omit in-scope records that deny show; check scope/show agreement with TestKit.assert_scope_matches_show." result << Check.new("resource:#{resource.key}", status, message) result.concat(schema_checks(resource, model)) @operation_registry.custom(resource).each do |definition| models = definition.handler_class.atomic_models.map(&:constantize) valid = models.all? { |related| related.is_a?(Class) && related < ActiveRecord::Base && related.connection_pool.equal?(model.connection_pool) } result << Check.new("operation:#{resource.key}:#{definition.key}", valid ? :review : :error, "Scope=#{definition.scope}; declared atomic models share pool=#{valid}; handler/input metadata validated. Trusted Ruby is not a sandbox: test callbacks, preflight purity and external-effect boundaries. Duration checks are cooperative, not a hard SQL/Ruby interrupt. Collection inputs persist once: minimize payloads, never submit credentials, and review retention.") end rescue ActiveRecord::ActiveRecordError, NameError, ConfigurationError => error result << Check.new("resource:#{resource.key}", :error, "Inspect model/migrations (#{error.class.name}); no record content was printed.") end end result end
Source
# File lib/rails_ai_admin/doctor.rb, line 42 def mcp_check return Check.new("mcp", :closed, "The MCP endpoint is off.") unless @configuration.respond_to?(:mcp) && @configuration.mcp require "rails_ai_admin/mcp" return Check.new("mcp", :error, "Run rails_ai_admin:assistant and db:migrate; the access token table is missing.") unless Mcp::AccessToken.table_exists? active = Mcp::AccessToken.active.count Check.new("mcp", :review, "MCP clients use personal access tokens (#{active} active). Each request resolves the owner through resolve_job " \ "and applies the assistant? grant and the owner's current grants; proposals wait for confirmation in the admin. Serve it over HTTPS only.") end
Reports whether the opt-in MCP endpoint can authenticate tokens.
Source
# File lib/rails_ai_admin/doctor.rb, line 19 def surface_check surfaces = @configuration.respond_to?(:surfaces) && @configuration.surfaces unless surfaces return Check.new("surfaces", :closed, "Admin review Surfaces are off. The host owns any rails_tool_views runtime; the admin does not configure it.") end require "rails_ai_admin/integrations/tool_views" return Check.new("surfaces", :error, "surfaces is enabled but rails_tool_views is not loaded.") unless Integrations::ToolViews.available? if @configuration.respond_to?(:surfaces_parent_controller_problem) && (problem = @configuration.surfaces_parent_controller_problem) return Check.new("surfaces", :error, problem) end unless ::RailsToolViews::Surface.table_exists? && ::RailsToolViews::ActionExecution.table_exists? return Check.new("surfaces", :error, "Run the rails_tool_views installer migrations before enabling admin Surfaces.") end Integrations::ToolViews.ensure_registered! operations = @registry.resources.sum { |resource| resource.operation_declarations.keys.count { |key| resource.surface_operation?(key) } } assistant = @configuration.respond_to?(:assistant) && @configuration.assistant Check.new("surfaces", :review, "#{Integrations::ToolViews::KEY} is registered for #{operations} member operations through admin-owned endpoints. " \ "#{assistant ? 'Assistant proposals present it and confirmed outcomes continue their chat once.' : 'Assistant presentation is off.'}") rescue ConfigurationError => error Check.new("surfaces", :error, error.message) end
Admin Surfaces never change the host-owned rails_tool_views runtime; this reports whether the opt-in can work, not whether it is authorized.