module RailsAiAdmin::Integrations::ToolViews
Admin review Surfaces on the host-owned rails_tool_views runtime.
Required only when ‘config.surfaces` is enabled, so conventional boot never loads rails_tool_views. The admin reads none of the host’s global rails_tool_views settings (resolver, parent controller, adapter, auto_deliver) and changes none of them. Admin endpoints pass an explicit ‘raa:` Scope and bind the current admin Context for the duration of one call; policy and handler deny when no matching Context is bound, so another route (such as the host’s product engine mount) can never act on them.
Constants
- ASSISTANT_ADAPTER
- CONFIRM
- CONFIRM_SCHEMA
- CONTEXT_KEY
- DISMISS
- KEY
- MAX_INPUT_BYTES
- PARTIAL
- POLICY
- Review
- SCHEMA
-
Props hold only a server intent reference and its canonical wire input, so the component digest never depends on reloadable host code.
- TERMINAL
-
Business outcomes are recorded on the Surface with their failure audit.
- UUID
- VERSION
Public Class Methods
Source
# File lib/rails_ai_admin/integrations/tool_views.rb, line 123 def assistant_surface(context, chat, call_id) lookup = { adapter: ASSISTANT_ADAPTER, call_id: call_id, context_ref: chat.context_reference, actor_ref: context.actor_ref, tenant_ref: context.tenant_ref } prior = ::RailsToolViews::Presentation.find_by(lookup) return unless prior && lookup.all? { |attribute, value| prior.public_send(attribute) == value } with_context(context) { ::RailsToolViews.restore(prior.surface_id, scope: scope_for(context)) } rescue ::RailsToolViews::Error nil end
The Surface an earlier attempt presented for this persisted tool call, so a retried tool does not prepare a second intent. Byte-exact match.
Source
# File lib/rails_ai_admin/integrations/tool_views.rb, line 43 def available? defined?(::RailsToolViews::Registry) ? true : false end
Source
# File lib/rails_ai_admin/integrations/tool_views.rb, line 346 def canonical_input(input) raise AccessDenied, "INVALID_QUERY" unless input.is_a?(Hash) stringified = input.to_h { |key, value| [ key.to_s, value ] } wire = JSON.generate(stringified.sort.to_h) raise AccessDenied, "INVALID_QUERY" if wire.bytesize > MAX_INPUT_BYTES wire end
Source
# File lib/rails_ai_admin/integrations/tool_views.rb, line 206 def confirm(request) context = current_context review = review(context, request.props) if context && request.actor == context.actor_ref && request.tenant == context.tenant_ref raise ::RailsToolViews::Error.new("NOT_ALLOWED", status: 403) unless review run = review.run result = Operations::Runner.new.execute(context: context, intent_ref: run.id, input: JSON.parse(request.props.fetch("input")), human_confirmed: true, origin: :surface, binding: { resource_key: run.resource_key, operation: run.operation, target_id: run.target_id }) if result.success? message = result.replayed ? "This operation was already completed. No second change was made." : "Operation completed." applied = run.operation == "create" ? "The record was created with id #{result.record_id}." : "The change was applied." continue_chat(context, request, review, result.replayed ? "It had already been applied; no second change was made." : applied) ::RailsToolViews::Transition.new(state: { "outcome" => "succeeded", "message" => message }, agent: false) elsif TERMINAL.key?(result.code) continue_chat(context, request, review, "It was not applied (#{result.code}): #{TERMINAL.fetch(result.code)}") ::RailsToolViews::Transition.new(state: { "outcome" => "rejected", "code" => result.code, "message" => TERMINAL.fetch(result.code) }, agent: false) elsif result.code == "NOT_FOUND" raise ::RailsToolViews::Error.new("NOT_ALLOWED", status: 403) else # Nothing committed; the Surface revision is unchanged and retry is safe. raise ::RailsToolViews::Error.new("OPERATION_FAILED", status: 503, message: "The operation could not complete. Nothing was changed; try again.") end end
The human’s own submission of the Surface form is the confirmation.
Source
# File lib/rails_ai_admin/integrations/tool_views.rb, line 328 def continue_chat(context, request, review, outcome, decision: "confirmed") return unless RailsAiAdmin.configuration.assistant require "rails_ai_admin/assistant" Assistant::Continuation.admit!(context: context, context_ref: request.context_ref, review: review, outcome: outcome, decision: decision) rescue AccessDenied # A closed chat never blocks the human's committed business outcome. nil end
Same transaction as the Surface revision; nothing is admitted when the confirmation rolls back.
Source
# File lib/rails_ai_admin/integrations/tool_views.rb, line 80 def current_context = ActiveSupport::IsolatedExecutionState[CONTEXT_KEY] # Presents (or replays) the review Surface for an unconsumed intent. A # native intent replays per intent; an assistant intent belongs to its # chat and replays per persisted tool call. def present(context:, resource:, definition:, record:, intent_ref:, input:, chat: nil, call_id: nil, assistant_call: nil) ensure_registered! if definition.custom? raise AccessDenied unless resource.surface_operation?(definition.key) && definition.scope == :member else # A native create, update or delete is reviewed here only when the # assistant proposed it and the resource allows that change. raise AccessDenied unless (chat || assistant_call) && resource.assistant_change?(definition.key) end raise AccessDenied if chat && assistant_call origin = chat || assistant_call ? "assistant" : "ui" raise AccessDenied if assistant_call && definition.custom? && !resource.assistant_operation?(definition.key) raise AccessDenied unless intent_ref.is_a?(String) && intent_ref.match?(/\A[0-9a-f-]{36}\z/) raise AccessDenied if chat && !(call_id.is_a?(String) && chat.current_for?(context) && chat.actor_ref == context.actor_ref && chat.tenant_ref == context.tenant_ref) run = OperationRun.find_by(id: intent_ref, actor_ref: context.actor_ref, tenant_ref: context.tenant_ref) unless run && run.id == intent_ref && run.resource_key == resource.key.to_s && run.operation == definition.key.to_s && run.target_id == record.id&.to_s && run.status == "proposed" && run.origin == origin && run.parent_run_id.nil? raise AccessDenied end raise AccessDenied, "APPROVAL_EXPIRED" unless run.expires_at && run.expires_at > Time.current raise AccessDenied, "APPROVAL_INVALIDATED" unless run.security_version == context.security_version wire = canonical_input(input) raise AccessDenied, "APPROVAL_INVALIDATED" unless run.input_digest && run.input_digest == InputSchema.digest(input_values(context, resource, definition, record, wire)) reference, correlation = if chat [ chat.context_reference, { adapter: ASSISTANT_ADAPTER, call_id: call_id, context: chat.context_reference } ] elsif assistant_call [ assistant_call.fetch(:conversation), { adapter: "raa_assistant", call_id: assistant_call.fetch(:call_id), context: assistant_call.fetch(:conversation) } ] else [ "raa:intent:#{run.id}", { adapter: "raa_native_review", call_id: run.id, context: "raa:intent:#{run.id}" } ] end with_context(context) do ::RailsToolViews.present(KEY, props: { "intent" => run.id, "input" => wire }, scope: scope_for(context), context: reference, correlation: correlation) end end # The Surface an earlier attempt presented for this persisted tool call, # so a retried tool does not prepare a second intent. Byte-exact match. def assistant_surface(context, chat, call_id) lookup = { adapter: ASSISTANT_ADAPTER, call_id: call_id, context_ref: chat.context_reference, actor_ref: context.actor_ref, tenant_ref: context.tenant_ref } prior = ::RailsToolViews::Presentation.find_by(lookup) return unless prior && lookup.all? { |attribute, value| prior.public_send(attribute) == value } with_context(context) { ::RailsToolViews.restore(prior.surface_id, scope: scope_for(context)) } rescue ::RailsToolViews::Error nil end # Canonical wire strings for typed operation values, as native forms send. def wire_input(values) values.to_h do |name, value| wire = case value when BigDecimal then value.to_s("F") when Time, ActiveSupport::TimeWithZone then value.utc.iso8601(6) when Date then value.iso8601 else value end [ name.to_s, wire ] end end # Current authorized review for props, or nil. Never trusts the Surface. def review(context, props) return unless context.is_a?(Context) && props.is_a?(Hash) && props["intent"].is_a?(String) run = OperationRun.find_by(id: props["intent"], actor_ref: context.actor_ref, tenant_ref: context.tenant_ref) return unless run && run.id == props["intent"] && run.parent_run_id.nil? && run.selection_id.nil? resource = RailsAiAdmin.registry.fetch(run.resource_key) definition = RailsAiAdmin.operation_registry.fetch(resource, run.operation) if definition.custom? return unless definition.scope == :member && resource.surface_operation?(definition.key) else return unless run.origin == "assistant" && resource.assistant_change?(definition.key) end access = Authorization::Access.new(context, resource) Review.new(run, resource, definition, access, review_record(access, run)) rescue AccessDenied, ConfigurationError, ActiveRecord::RecordNotFound nil end def authorize(access) context = current_context return false unless context.is_a?(Context) && access.actor == context.actor_ref && access.tenant == context.tenant_ref review = review(context, access.props) return false unless review case access.operation when :present then review.run.status == "proposed" # A completed review is a receipt: like native replay it needs the # independent receipt grant, not the (possibly lapsed) command grant. when :read then review.run.status != "succeeded" || receipt?(review) when :action then %w[confirm dismiss].include?(access.action) else false end end # The record a review is about: a new one for a proposed create, the # created one afterwards, and none once a delete succeeded. def review_record(access, run) case run.operation when "create" then run.status == "succeeded" ? access.find!(run.record_id) : access.relation.new when "destroy" then run.status == "succeeded" ? nil : access.find!(run.target_id) else access.find!(run.target_id) end end # How a review names its record in the conversation and on the card. def subject(review) singular = review.resource.label.singularize return "#{singular} ##{review.run.target_id}" if review.record.nil? return "a new #{singular.downcase_first}" unless review.record.persisted? review.access.record_label(review.record) end def receipt?(review) # A native change's result is the record itself, behind its list grant. return review.access.authorize!(:index) unless review.definition.custom? review.access.authorize_receipt!(review.definition.key, review.record) rescue AccessDenied false end # The human's own submission of the Surface form is the confirmation. def confirm(request) context = current_context review = review(context, request.props) if context && request.actor == context.actor_ref && request.tenant == context.tenant_ref raise ::RailsToolViews::Error.new("NOT_ALLOWED", status: 403) unless review run = review.run result = Operations::Runner.new.execute(context: context, intent_ref: run.id, input: JSON.parse(request.props.fetch("input")), human_confirmed: true, origin: :surface, binding: { resource_key: run.resource_key, operation: run.operation, target_id: run.target_id }) if result.success? message = result.replayed ? "This operation was already completed. No second change was made." : "Operation completed." applied = run.operation == "create" ? "The record was created with id #{result.record_id}." : "The change was applied." continue_chat(context, request, review, result.replayed ? "It had already been applied; no second change was made." : applied) ::RailsToolViews::Transition.new(state: { "outcome" => "succeeded", "message" => message }, agent: false) elsif TERMINAL.key?(result.code) continue_chat(context, request, review, "It was not applied (#{result.code}): #{TERMINAL.fetch(result.code)}") ::RailsToolViews::Transition.new(state: { "outcome" => "rejected", "code" => result.code, "message" => TERMINAL.fetch(result.code) }, agent: false) elsif result.code == "NOT_FOUND" raise ::RailsToolViews::Error.new("NOT_ALLOWED", status: 403) else # Nothing committed; the Surface revision is unchanged and retry is safe. raise ::RailsToolViews::Error.new("OPERATION_FAILED", status: 503, message: "The operation could not complete. Nothing was changed; try again.") end end # Declining is a decision too (RAA-019): the unapproved intent expires # at once, nothing else changes, and a chat hears about it once. def dismiss(request) context = current_context review = review(context, request.props) if context && request.actor == context.actor_ref && request.tenant == context.tenant_ref raise ::RailsToolViews::Error.new("NOT_ALLOWED", status: 403) unless review expired = OperationRun.where(id: review.run.id, status: "proposed", approved_by_ref: nil) .update_all(expires_at: Time.current, updated_at: Time.current) raise ::RailsToolViews::Error.new("CONFLICT", status: 409, message: "This review was already decided.") unless expired == 1 continue_chat(context, request, review, "Nothing was changed.", decision: "dismissed") ::RailsToolViews::Transition.new(state: { "outcome" => "dismissed", "message" => "Dismissed. Nothing was changed." }, agent: false) end # The visible before/after values the operation's pure preflight # proposes, as the native confirmation page shows them. Empty once the # review cannot be confirmed or the preview is no longer valid. def proposed_changes(review, wire) return [] unless review.run.status == "proposed" return native_changes(review, wire) unless review.definition.custom? context = review.access.context preview = Operations::Runner.new.preview(context: context, resource: review.resource, operation: review.definition.key, target_id: review.record.id, expected_version: review.run.expected_version, input: JSON.parse(wire), origin: :surface) return [] unless preview.is_a?(Operations::Runner::Preview) readable = review.access.fields(:read, review.record, view: :show).index_by { |field| field.name.to_s } preview.changes.filter_map do |name, change| field = readable[name] next unless field [ field, field.format(change["before"], time_zone: context.time_zone), field.format(change["after"], time_zone: context.time_zone) ] end rescue AccessDenied, ConfigurationError, JSON::ParserError [] end # Typed display values. While proposed, only the actor's current input # grants; once committed, the recorded input through the operation's # declared contract (as receipt replay does), behind the receipt grant. def display_values(review, wire) # Native changes show before and after instead (native_changes). return [] unless review.definition.custom? values = JSON.parse(wire) fields = if review.run.status == "succeeded" review.definition.handler_class.input else begin review.definition.fields(review.access, review.record) rescue AccessDenied [] end end fields.filter_map do |field| next unless values.key?(field.name.to_s) text = begin field.format(field.coerce(values[field.name.to_s], time_zone: review.access.context.time_zone), time_zone: review.access.context.time_zone) rescue ArgumentError "Unavailable" end [ field, text ] end rescue JSON::ParserError [] end # Before and after for a native create or update, through the actor's # read grants; references show their labels. A field the actor may # write but not read shows only that it will be set. def native_changes(review, wire) return [] if review.run.operation == "destroy" context = review.access.context values = input_values(context, review.resource, review.definition, review.record, wire) persisted = review.record.persisted? readable = review.access.fields(:read, persisted ? review.record : nil, view: :show).index_by { |field| field.name.to_s } current = persisted ? review.access.project(review.record, view: :show) : {} resolver = Fields::Association.new(context, review.resource) fields = review.resource.fields.index_by { |field| field.name.to_s } values.filter_map do |name, after| field = fields[name] next unless field before = current[name] next if persisted && before == after show = ->(value) { readable.key?(name) ? native_text(field, value, resolver, context) : I18n.t("rails_ai_admin.surfaces.hidden_value") } [ field, persisted ? show.call(before) : "—", show.call(after) ] end rescue AccessDenied, ConfigurationError, JSON::ParserError [] end def native_text(field, value, resolver, context) return "—" if value.nil? case field.scalar_type when :belongs_to then resolver.reference(field, value.to_s).label.to_s when :references then Array(value).map { |id| resolver.reference(field, id).label.to_s }.join(", ").presence || "—" else field.format(value, time_zone: context.time_zone) end end private # Same transaction as the Surface revision; nothing is admitted when # the confirmation rolls back. def continue_chat(context, request, review, outcome, decision: "confirmed") return unless RailsAiAdmin.configuration.assistant require "rails_ai_admin/assistant" Assistant::Continuation.admit!(context: context, context_ref: request.context_ref, review: review, outcome: outcome, decision: decision) rescue AccessDenied # A closed chat never blocks the human's committed business outcome. nil end def register ::RailsToolViews.register(KEY, description: "Admin operation review", version: VERSION, schema: SCHEMA, partial: PARTIAL, policy: POLICY, actions: { confirm: { schema: CONFIRM_SCHEMA, policy: POLICY, handler: CONFIRM }, dismiss: { schema: CONFIRM_SCHEMA, policy: POLICY, handler: DISMISS } }, expose_to: []) rescue ::RailsToolViews::Error => error raise unless error.code == "DUPLICATE_COMPONENT" ::RailsToolViews.registry.fetch(KEY) end def canonical_input(input) raise AccessDenied, "INVALID_QUERY" unless input.is_a?(Hash) stringified = input.to_h { |key, value| [ key.to_s, value ] } wire = JSON.generate(stringified.sort.to_h) raise AccessDenied, "INVALID_QUERY" if wire.bytesize > MAX_INPUT_BYTES wire end def input_values(context, resource, definition, record, wire) access = Authorization::Access.new(context, resource) result = InputSchema.new(definition.fields(access, record), partial: definition.key == "update" && !definition.custom?).coerce(JSON.parse(wire), time_zone: context.time_zone) raise AccessDenied, "APPROVAL_INVALIDATED" unless result.valid? result.values end end
Source
# File lib/rails_ai_admin/integrations/tool_views.rb, line 231 def dismiss(request) context = current_context review = review(context, request.props) if context && request.actor == context.actor_ref && request.tenant == context.tenant_ref raise ::RailsToolViews::Error.new("NOT_ALLOWED", status: 403) unless review expired = OperationRun.where(id: review.run.id, status: "proposed", approved_by_ref: nil) .update_all(expires_at: Time.current, updated_at: Time.current) raise ::RailsToolViews::Error.new("CONFLICT", status: 409, message: "This review was already decided.") unless expired == 1 continue_chat(context, request, review, "Nothing was changed.", decision: "dismissed") ::RailsToolViews::Transition.new(state: { "outcome" => "dismissed", "message" => "Dismissed. Nothing was changed." }, agent: false) end
Declining is a decision too (RAA-019): the unapproved intent expires at once, nothing else changes, and a chat hears about it once.
Source
# File lib/rails_ai_admin/integrations/tool_views.rb, line 265 def display_values(review, wire) # Native changes show before and after instead (native_changes). return [] unless review.definition.custom? values = JSON.parse(wire) fields = if review.run.status == "succeeded" review.definition.handler_class.input else begin review.definition.fields(review.access, review.record) rescue AccessDenied [] end end fields.filter_map do |field| next unless values.key?(field.name.to_s) text = begin field.format(field.coerce(values[field.name.to_s], time_zone: review.access.context.time_zone), time_zone: review.access.context.time_zone) rescue ArgumentError "Unavailable" end [ field, text ] end rescue JSON::ParserError [] end
Typed display values. While proposed, only the actor’s current input grants; once committed, the recorded input through the operation’s declared contract (as receipt replay does), behind the receipt grant.
Source
# File lib/rails_ai_admin/integrations/tool_views.rb, line 49 def ensure_registered! raise ConfigurationError, "rails_tool_views is not loaded" unless available? component = begin ::RailsToolViews.registry.fetch(KEY) rescue ::RailsToolViews::Error => error raise unless error.code == "UNKNOWN_COMPONENT" register end unless component.policy.equal?(POLICY) && component.actions.fetch("confirm").handler.equal?(CONFIRM) && component.actions["dismiss"]&.handler.equal?(DISMISS) raise ConfigurationError, "#{KEY} is registered by other code" end component end
Re-registers after a host ‘reset_registry!`. The contract is constant, so an existing registration must be exactly this one.
Source
# File lib/rails_ai_admin/integrations/tool_views.rb, line 354 def input_values(context, resource, definition, record, wire) access = Authorization::Access.new(context, resource) result = InputSchema.new(definition.fields(access, record), partial: definition.key == "update" && !definition.custom?).coerce(JSON.parse(wire), time_zone: context.time_zone) raise AccessDenied, "APPROVAL_INVALIDATED" unless result.valid? result.values end
Source
# File lib/rails_ai_admin/integrations/tool_views.rb, line 294 def native_changes(review, wire) return [] if review.run.operation == "destroy" context = review.access.context values = input_values(context, review.resource, review.definition, review.record, wire) persisted = review.record.persisted? readable = review.access.fields(:read, persisted ? review.record : nil, view: :show).index_by { |field| field.name.to_s } current = persisted ? review.access.project(review.record, view: :show) : {} resolver = Fields::Association.new(context, review.resource) fields = review.resource.fields.index_by { |field| field.name.to_s } values.filter_map do |name, after| field = fields[name] next unless field before = current[name] next if persisted && before == after show = ->(value) { readable.key?(name) ? native_text(field, value, resolver, context) : I18n.t("rails_ai_admin.surfaces.hidden_value") } [ field, persisted ? show.call(before) : "—", show.call(after) ] end rescue AccessDenied, ConfigurationError, JSON::ParserError [] end
Before and after for a native create or update, through the actor’s read grants; references show their labels. A field the actor may write but not read shows only that it will be set.
Source
# File lib/rails_ai_admin/integrations/tool_views.rb, line 315 def native_text(field, value, resolver, context) return "—" if value.nil? case field.scalar_type when :belongs_to then resolver.reference(field, value.to_s).label.to_s when :references then Array(value).map { |id| resolver.reference(field, id).label.to_s }.join(", ").presence || "—" else field.format(value, time_zone: context.time_zone) end end
Source
# File lib/rails_ai_admin/integrations/tool_views.rb, line 85 def present(context:, resource:, definition:, record:, intent_ref:, input:, chat: nil, call_id: nil, assistant_call: nil) ensure_registered! if definition.custom? raise AccessDenied unless resource.surface_operation?(definition.key) && definition.scope == :member else # A native create, update or delete is reviewed here only when the # assistant proposed it and the resource allows that change. raise AccessDenied unless (chat || assistant_call) && resource.assistant_change?(definition.key) end raise AccessDenied if chat && assistant_call origin = chat || assistant_call ? "assistant" : "ui" raise AccessDenied if assistant_call && definition.custom? && !resource.assistant_operation?(definition.key) raise AccessDenied unless intent_ref.is_a?(String) && intent_ref.match?(/\A[0-9a-f-]{36}\z/) raise AccessDenied if chat && !(call_id.is_a?(String) && chat.current_for?(context) && chat.actor_ref == context.actor_ref && chat.tenant_ref == context.tenant_ref) run = OperationRun.find_by(id: intent_ref, actor_ref: context.actor_ref, tenant_ref: context.tenant_ref) unless run && run.id == intent_ref && run.resource_key == resource.key.to_s && run.operation == definition.key.to_s && run.target_id == record.id&.to_s && run.status == "proposed" && run.origin == origin && run.parent_run_id.nil? raise AccessDenied end raise AccessDenied, "APPROVAL_EXPIRED" unless run.expires_at && run.expires_at > Time.current raise AccessDenied, "APPROVAL_INVALIDATED" unless run.security_version == context.security_version wire = canonical_input(input) raise AccessDenied, "APPROVAL_INVALIDATED" unless run.input_digest && run.input_digest == InputSchema.digest(input_values(context, resource, definition, record, wire)) reference, correlation = if chat [ chat.context_reference, { adapter: ASSISTANT_ADAPTER, call_id: call_id, context: chat.context_reference } ] elsif assistant_call [ assistant_call.fetch(:conversation), { adapter: "raa_assistant", call_id: assistant_call.fetch(:call_id), context: assistant_call.fetch(:conversation) } ] else [ "raa:intent:#{run.id}", { adapter: "raa_native_review", call_id: run.id, context: "raa:intent:#{run.id}" } ] end with_context(context) do ::RailsToolViews.present(KEY, props: { "intent" => run.id, "input" => wire }, scope: scope_for(context), context: reference, correlation: correlation) end end
Presents (or replays) the review Surface for an unconsumed intent. A native intent replays per intent; an assistant intent belongs to its chat and replays per persisted tool call.
Source
# File lib/rails_ai_admin/integrations/tool_views.rb, line 245 def proposed_changes(review, wire) return [] unless review.run.status == "proposed" return native_changes(review, wire) unless review.definition.custom? context = review.access.context preview = Operations::Runner.new.preview(context: context, resource: review.resource, operation: review.definition.key, target_id: review.record.id, expected_version: review.run.expected_version, input: JSON.parse(wire), origin: :surface) return [] unless preview.is_a?(Operations::Runner::Preview) readable = review.access.fields(:read, review.record, view: :show).index_by { |field| field.name.to_s } preview.changes.filter_map do |name, change| field = readable[name] next unless field [ field, field.format(change["before"], time_zone: context.time_zone), field.format(change["after"], time_zone: context.time_zone) ] end rescue AccessDenied, ConfigurationError, JSON::ParserError [] end
The visible before/after values the operation’s pure preflight proposes, as the native confirmation page shows them. Empty once the review cannot be confirmed or the preview is no longer valid.
Source
# File lib/rails_ai_admin/integrations/tool_views.rb, line 197 def receipt?(review) # A native change's result is the record itself, behind its list grant. return review.access.authorize!(:index) unless review.definition.custom? review.access.authorize_receipt!(review.definition.key, review.record) rescue AccessDenied false end
Source
# File lib/rails_ai_admin/integrations/tool_views.rb, line 337 def register ::RailsToolViews.register(KEY, description: "Admin operation review", version: VERSION, schema: SCHEMA, partial: PARTIAL, policy: POLICY, actions: { confirm: { schema: CONFIRM_SCHEMA, policy: POLICY, handler: CONFIRM }, dismiss: { schema: CONFIRM_SCHEMA, policy: POLICY, handler: DISMISS } }, expose_to: []) rescue ::RailsToolViews::Error => error raise unless error.code == "DUPLICATE_COMPONENT" ::RailsToolViews.registry.fetch(KEY) end
Source
# File lib/rails_ai_admin/integrations/tool_views.rb, line 147 def review(context, props) return unless context.is_a?(Context) && props.is_a?(Hash) && props["intent"].is_a?(String) run = OperationRun.find_by(id: props["intent"], actor_ref: context.actor_ref, tenant_ref: context.tenant_ref) return unless run && run.id == props["intent"] && run.parent_run_id.nil? && run.selection_id.nil? resource = RailsAiAdmin.registry.fetch(run.resource_key) definition = RailsAiAdmin.operation_registry.fetch(resource, run.operation) if definition.custom? return unless definition.scope == :member && resource.surface_operation?(definition.key) else return unless run.origin == "assistant" && resource.assistant_change?(definition.key) end access = Authorization::Access.new(context, resource) Review.new(run, resource, definition, access, review_record(access, run)) rescue AccessDenied, ConfigurationError, ActiveRecord::RecordNotFound nil end
Current authorized review for props, or nil. Never trusts the Surface.
Source
# File lib/rails_ai_admin/integrations/tool_views.rb, line 181 def review_record(access, run) case run.operation when "create" then run.status == "succeeded" ? access.find!(run.record_id) : access.relation.new when "destroy" then run.status == "succeeded" ? nil : access.find!(run.target_id) else access.find!(run.target_id) end end
The record a review is about: a new one for a proposed create, the created one afterwards, and none once a delete succeeded.
Source
# File lib/rails_ai_admin/integrations/tool_views.rb, line 64 def scope_for(context) raise AccessDenied, "UNAUTHENTICATED" unless context.is_a?(Context) ::RailsToolViews::Scope.new(actor: context.actor_ref, tenant: context.tenant_ref) end
Source
# File lib/rails_ai_admin/integrations/tool_views.rb, line 190 def subject(review) singular = review.resource.label.singularize return "#{singular} ##{review.run.target_id}" if review.record.nil? return "a new #{singular.downcase_first}" unless review.record.persisted? review.access.record_label(review.record) end
How a review names its record in the conversation and on the card.
Source
# File lib/rails_ai_admin/integrations/tool_views.rb, line 134 def wire_input(values) values.to_h do |name, value| wire = case value when BigDecimal then value.to_s("F") when Time, ActiveSupport::TimeWithZone then value.utc.iso8601(6) when Date then value.iso8601 else value end [ name.to_s, wire ] end end
Canonical wire strings for typed operation values, as native forms send.
Source
# File lib/rails_ai_admin/integrations/tool_views.rb, line 70 def with_context(context) raise AccessDenied, "UNAUTHENTICATED" unless context.is_a?(Context) state = ActiveSupport::IsolatedExecutionState previous = state[CONTEXT_KEY] state[CONTEXT_KEY] = context yield ensure state[CONTEXT_KEY] = previous if state end
Binds the current admin Context for one admin endpoint call.