rails_ai_admin 0.1.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 (193) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +458 -0
  3. data/LICENSE +21 -0
  4. data/README.md +292 -0
  5. data/SECURITY.md +14 -0
  6. data/THIRD_PARTY_NOTICES.md +19 -0
  7. data/app/assets/builds/rails_ai_admin.LICENSE.txt +55 -0
  8. data/app/assets/builds/rails_ai_admin.css +345 -0
  9. data/app/assets/builds/rails_ai_admin.js +33 -0
  10. data/app/controllers/rails_ai_admin/application_controller.rb +255 -0
  11. data/app/controllers/rails_ai_admin/assistant_controller.rb +164 -0
  12. data/app/controllers/rails_ai_admin/audit_controller.rb +16 -0
  13. data/app/controllers/rails_ai_admin/collection_jobs_controller.rb +33 -0
  14. data/app/controllers/rails_ai_admin/collection_operations_controller.rb +79 -0
  15. data/app/controllers/rails_ai_admin/custom_controller.rb +26 -0
  16. data/app/controllers/rails_ai_admin/dashboard_controller.rb +17 -0
  17. data/app/controllers/rails_ai_admin/exports_controller.rb +31 -0
  18. data/app/controllers/rails_ai_admin/files_controller.rb +37 -0
  19. data/app/controllers/rails_ai_admin/imports_controller.rb +41 -0
  20. data/app/controllers/rails_ai_admin/mcp_controller.rb +81 -0
  21. data/app/controllers/rails_ai_admin/mcp_tokens_controller.rb +45 -0
  22. data/app/controllers/rails_ai_admin/operations_controller.rb +105 -0
  23. data/app/controllers/rails_ai_admin/references_controller.rb +16 -0
  24. data/app/controllers/rails_ai_admin/resources_controller.rb +290 -0
  25. data/app/controllers/rails_ai_admin/search_controller.rb +39 -0
  26. data/app/controllers/rails_ai_admin/selections_controller.rb +95 -0
  27. data/app/controllers/rails_ai_admin/surfaces_controller.rb +122 -0
  28. data/app/helpers/rails_ai_admin/dashboard_helper.rb +28 -0
  29. data/app/helpers/rails_ai_admin/fields_helper.rb +242 -0
  30. data/app/helpers/rails_ai_admin/filters_helper.rb +40 -0
  31. data/app/helpers/rails_ai_admin/history_helper.rb +36 -0
  32. data/app/helpers/rails_ai_admin/navigation_helper.rb +54 -0
  33. data/app/helpers/rails_ai_admin/tool_views_helper.rb +48 -0
  34. data/app/jobs/rails_ai_admin/assistant_turn_job.rb +15 -0
  35. data/app/jobs/rails_ai_admin/collections/execution_job.rb +12 -0
  36. data/app/jobs/rails_ai_admin/outbox/delivery_job.rb +13 -0
  37. data/app/models/rails_ai_admin/application_record.rb +8 -0
  38. data/app/models/rails_ai_admin/audit_event.rb +12 -0
  39. data/app/models/rails_ai_admin/batch_item.rb +8 -0
  40. data/app/models/rails_ai_admin/operation_run.rb +20 -0
  41. data/app/models/rails_ai_admin/selection.rb +9 -0
  42. data/app/models/rails_ai_admin/selection_item.rb +9 -0
  43. data/app/models/rails_ai_admin/work_item.rb +18 -0
  44. data/app/views/layouts/rails_ai_admin/application.html.erb +49 -0
  45. data/app/views/layouts/rails_ai_admin/frame.html.erb +5 -0
  46. data/app/views/layouts/rails_ai_admin/status.html.erb +13 -0
  47. data/app/views/rails_ai_admin/assistant/_ask.html.erb +16 -0
  48. data/app/views/rails_ai_admin/assistant/_transcript.html.erb +45 -0
  49. data/app/views/rails_ai_admin/assistant/frame.html.erb +1 -0
  50. data/app/views/rails_ai_admin/assistant/index.html.erb +14 -0
  51. data/app/views/rails_ai_admin/assistant/show.html.erb +44 -0
  52. data/app/views/rails_ai_admin/audit/index.html.erb +40 -0
  53. data/app/views/rails_ai_admin/collection_jobs/index.html.erb +5 -0
  54. data/app/views/rails_ai_admin/collection_jobs/show.html.erb +24 -0
  55. data/app/views/rails_ai_admin/collection_operations/confirmation.html.erb +25 -0
  56. data/app/views/rails_ai_admin/collection_operations/failure.html.erb +10 -0
  57. data/app/views/rails_ai_admin/collection_operations/new.html.erb +11 -0
  58. data/app/views/rails_ai_admin/dashboard/index.html.erb +33 -0
  59. data/app/views/rails_ai_admin/fields/_index.html.erb +1 -0
  60. data/app/views/rails_ai_admin/fields/_input.html.erb +21 -0
  61. data/app/views/rails_ai_admin/fields/_reference.html.erb +1 -0
  62. data/app/views/rails_ai_admin/fields/_reference_picker.html.erb +14 -0
  63. data/app/views/rails_ai_admin/fields/_row.html.erb +27 -0
  64. data/app/views/rails_ai_admin/fields/_show.html.erb +1 -0
  65. data/app/views/rails_ai_admin/imports/new.html.erb +34 -0
  66. data/app/views/rails_ai_admin/mcp_tokens/index.html.erb +33 -0
  67. data/app/views/rails_ai_admin/operations/confirmation.html.erb +32 -0
  68. data/app/views/rails_ai_admin/operations/failure.html.erb +5 -0
  69. data/app/views/rails_ai_admin/operations/form.html.erb +13 -0
  70. data/app/views/rails_ai_admin/resources/_associations.html.erb +12 -0
  71. data/app/views/rails_ai_admin/resources/_errors.html.erb +14 -0
  72. data/app/views/rails_ai_admin/resources/_recovery.html.erb +5 -0
  73. data/app/views/rails_ai_admin/resources/confirm_destroy.html.erb +11 -0
  74. data/app/views/rails_ai_admin/resources/edit.html.erb +1 -0
  75. data/app/views/rails_ai_admin/resources/form.html.erb +16 -0
  76. data/app/views/rails_ai_admin/resources/history.html.erb +27 -0
  77. data/app/views/rails_ai_admin/resources/index.html.erb +88 -0
  78. data/app/views/rails_ai_admin/resources/new.html.erb +1 -0
  79. data/app/views/rails_ai_admin/resources/show.html.erb +12 -0
  80. data/app/views/rails_ai_admin/search/index.html.erb +20 -0
  81. data/app/views/rails_ai_admin/selections/confirm_destroy.html.erb +13 -0
  82. data/app/views/rails_ai_admin/selections/destroyed.html.erb +15 -0
  83. data/app/views/rails_ai_admin/selections/show.html.erb +19 -0
  84. data/app/views/rails_ai_admin/shared/_input_fields.html.erb +23 -0
  85. data/app/views/rails_ai_admin/shared/_navigation.html.erb +8 -0
  86. data/app/views/rails_ai_admin/shared/_session.html.erb +5 -0
  87. data/app/views/rails_ai_admin/statuses/show.html.erb +4 -0
  88. data/app/views/rails_ai_admin/surfaces/_operation_review.html.erb +43 -0
  89. data/app/views/rails_ai_admin/surfaces/show.html.erb +4 -0
  90. data/config/locales/en.yml +549 -0
  91. data/config/routes.rb +64 -0
  92. data/db/assistant_migrate/20261103000001_create_rails_ai_admin_assistant.rb +138 -0
  93. data/db/assistant_migrate/20261112000001_upgrade_rails_ai_admin_assistant_to_ruby_llm_2_2.rb +102 -0
  94. data/db/assistant_migrate/20261114000001_add_streaming_and_access_tokens_to_rails_ai_admin_assistant.rb +29 -0
  95. data/db/migrate/20261029000001_create_rails_ai_admin_execution_tables.rb +35 -0
  96. data/db/migrate/20261029000002_add_callback_warning_to_rails_ai_admin_runs.rb +7 -0
  97. data/db/migrate/20261030000003_create_rails_ai_admin_work_items.rb +26 -0
  98. data/db/migrate/20261030000004_create_rails_ai_admin_selections.rb +28 -0
  99. data/db/migrate/20261030000005_create_rails_ai_admin_collection_runs.rb +41 -0
  100. data/db/migrate/20261113000001_add_record_index_to_rails_ai_admin_audit_events.rb +8 -0
  101. data/lib/generators/rails_ai_admin/assistant/assistant_generator.rb +25 -0
  102. data/lib/generators/rails_ai_admin/assistant/templates/admin_assistant.rb +18 -0
  103. data/lib/generators/rails_ai_admin/engine_migrations.rb +42 -0
  104. data/lib/generators/rails_ai_admin/install/engine_mounts.rb +19 -0
  105. data/lib/generators/rails_ai_admin/install/install_generator.rb +53 -0
  106. data/lib/generators/rails_ai_admin/install/templates/access_test.rb +17 -0
  107. data/lib/generators/rails_ai_admin/install/templates/admin_context.rb +25 -0
  108. data/lib/generators/rails_ai_admin/install/templates/admin_policy_adapter.rb +28 -0
  109. data/lib/generators/rails_ai_admin/install/templates/example_admin.rb +10 -0
  110. data/lib/generators/rails_ai_admin/install/templates/initializer.rb +7 -0
  111. data/lib/generators/rails_ai_admin/override/override_generator.rb +46 -0
  112. data/lib/generators/rails_ai_admin/resource/resource_generator.rb +88 -0
  113. data/lib/rails_ai_admin/assistant/continuation.rb +33 -0
  114. data/lib/rails_ai_admin/assistant/coordinator.rb +306 -0
  115. data/lib/rails_ai_admin/assistant/records.rb +71 -0
  116. data/lib/rails_ai_admin/assistant/runtime.rb +49 -0
  117. data/lib/rails_ai_admin/assistant/tools.rb +477 -0
  118. data/lib/rails_ai_admin/assistant/transcript.rb +140 -0
  119. data/lib/rails_ai_admin/assistant.rb +50 -0
  120. data/lib/rails_ai_admin/authorization/access.rb +296 -0
  121. data/lib/rails_ai_admin/authorization/adapter.rb +92 -0
  122. data/lib/rails_ai_admin/authorization/pundit_adapter.rb +195 -0
  123. data/lib/rails_ai_admin/collections/claim.rb +91 -0
  124. data/lib/rails_ai_admin/collections/contract.rb +113 -0
  125. data/lib/rails_ai_admin/collections/dispatcher.rb +126 -0
  126. data/lib/rails_ai_admin/collections/queries.rb +79 -0
  127. data/lib/rails_ai_admin/collections/result.rb +15 -0
  128. data/lib/rails_ai_admin/collections/runner.rb +208 -0
  129. data/lib/rails_ai_admin/configuration.rb +178 -0
  130. data/lib/rails_ai_admin/context.rb +29 -0
  131. data/lib/rails_ai_admin/doctor.rb +301 -0
  132. data/lib/rails_ai_admin/engine.rb +24 -0
  133. data/lib/rails_ai_admin/error.rb +16 -0
  134. data/lib/rails_ai_admin/field.rb +625 -0
  135. data/lib/rails_ai_admin/fields/association.rb +188 -0
  136. data/lib/rails_ai_admin/fields/computation.rb +43 -0
  137. data/lib/rails_ai_admin/fields/dependency_graph.rb +45 -0
  138. data/lib/rails_ai_admin/fields/file_value.rb +23 -0
  139. data/lib/rails_ai_admin/fields/hook_signatures.rb +18 -0
  140. data/lib/rails_ai_admin/fields/projection.rb +78 -0
  141. data/lib/rails_ai_admin/fields/registry.rb +48 -0
  142. data/lib/rails_ai_admin/fields/scalar_schema.rb +49 -0
  143. data/lib/rails_ai_admin/fields/type.rb +50 -0
  144. data/lib/rails_ai_admin/forms/plan.rb +90 -0
  145. data/lib/rails_ai_admin/input_schema.rb +76 -0
  146. data/lib/rails_ai_admin/integrations/propose_operation.rb +150 -0
  147. data/lib/rails_ai_admin/integrations/tool_views.rb +367 -0
  148. data/lib/rails_ai_admin/mcp.rb +130 -0
  149. data/lib/rails_ai_admin/navigation/list_state.rb +194 -0
  150. data/lib/rails_ai_admin/navigation/menu.rb +31 -0
  151. data/lib/rails_ai_admin/navigation/settings.rb +28 -0
  152. data/lib/rails_ai_admin/operation.rb +120 -0
  153. data/lib/rails_ai_admin/operations/default_handlers.rb +17 -0
  154. data/lib/rails_ai_admin/operations/execution_context.rb +38 -0
  155. data/lib/rails_ai_admin/operations/form_intent.rb +69 -0
  156. data/lib/rails_ai_admin/operations/registry.rb +80 -0
  157. data/lib/rails_ai_admin/operations/result.rb +21 -0
  158. data/lib/rails_ai_admin/operations/runner.rb +633 -0
  159. data/lib/rails_ai_admin/operations/runner_fingerprint.rb +12 -0
  160. data/lib/rails_ai_admin/outbox/delivery_context.rb +51 -0
  161. data/lib/rails_ai_admin/outbox/dispatcher.rb +189 -0
  162. data/lib/rails_ai_admin/outbox/registry.rb +36 -0
  163. data/lib/rails_ai_admin/outbox/request_digest.rb +42 -0
  164. data/lib/rails_ai_admin/outbox/writer.rb +43 -0
  165. data/lib/rails_ai_admin/presentation/association_panel.rb +90 -0
  166. data/lib/rails_ai_admin/presentation/audit_log.rb +63 -0
  167. data/lib/rails_ai_admin/presentation/csv_export.rb +85 -0
  168. data/lib/rails_ai_admin/presentation/csv_import.rb +142 -0
  169. data/lib/rails_ai_admin/presentation/dashboard.rb +122 -0
  170. data/lib/rails_ai_admin/presentation/history.rb +46 -0
  171. data/lib/rails_ai_admin/presentation/index_settings.rb +77 -0
  172. data/lib/rails_ai_admin/presentation/outcomes.rb +44 -0
  173. data/lib/rails_ai_admin/presentation/status.rb +40 -0
  174. data/lib/rails_ai_admin/prune.rb +61 -0
  175. data/lib/rails_ai_admin/queries/list.rb +162 -0
  176. data/lib/rails_ai_admin/queries/named_scope.rb +65 -0
  177. data/lib/rails_ai_admin/record_version.rb +40 -0
  178. data/lib/rails_ai_admin/registry.rb +45 -0
  179. data/lib/rails_ai_admin/request_log_filter.rb +24 -0
  180. data/lib/rails_ai_admin/resource.rb +249 -0
  181. data/lib/rails_ai_admin/resource_identity.rb +15 -0
  182. data/lib/rails_ai_admin/schema_suggestions.rb +105 -0
  183. data/lib/rails_ai_admin/selections/builder.rb +162 -0
  184. data/lib/rails_ai_admin/templates/catalog.rb +84 -0
  185. data/lib/rails_ai_admin/templates/inspector.rb +94 -0
  186. data/lib/rails_ai_admin/templates/metadata.rb +48 -0
  187. data/lib/rails_ai_admin/templates/safe_paths.rb +30 -0
  188. data/lib/rails_ai_admin/test_kit.rb +103 -0
  189. data/lib/rails_ai_admin/version.rb +5 -0
  190. data/lib/rails_ai_admin/work.rb +48 -0
  191. data/lib/rails_ai_admin.rb +112 -0
  192. data/lib/tasks/rails_ai_admin.rake +58 -0
  193. metadata +328 -0
@@ -0,0 +1,138 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Opt-in admin Chat storage, installed only by `rails_ai_admin:assistant`.
4
+ # Chats, messages and turns are Engine-owned. Tool calls and usages live in
5
+ # RubyLLM's own shared tables, isolated per chat by their polymorphic message
6
+ # and chat columns; they are created here only when the host has none yet.
7
+ class CreateRailsAiAdminAssistant < ActiveRecord::Migration[8.0]
8
+ def up
9
+ key = reference_type
10
+ create_ruby_llm_tables(key)
11
+
12
+ create_table :rails_ai_admin_assistant_chats, id: key do |t|
13
+ t.string :ref, null: false, limit: 36
14
+ t.string :actor_ref, :tenant_ref, :security_version, null: false, limit: 150
15
+ t.string :title, limit: 120
16
+ t.string :status, null: false, default: "active"
17
+ t.references :ruby_llm_model, type: key, foreign_key: { to_table: :ruby_llm_models }, index: { name: "raa_assistant_chats_model" }
18
+ t.boolean :cancelled, null: false, default: false
19
+ t.timestamps
20
+ end
21
+ add_index :rails_ai_admin_assistant_chats, :ref, unique: true, name: "raa_assistant_chats_ref"
22
+ add_index :rails_ai_admin_assistant_chats, [ :tenant_ref, :actor_ref, :updated_at ], name: "raa_assistant_chats_owner"
23
+ add_check_constraint :rails_ai_admin_assistant_chats, "status IN ('active', 'archived')", name: "raa_assistant_chats_status"
24
+
25
+ # Columns follow RubyLLM 2.1.1's message template; the 2.2 upgrade migration adds the rest.
26
+ create_table :rails_ai_admin_assistant_messages, id: key do |t|
27
+ t.references :chat, null: false, type: key, foreign_key: { to_table: :rails_ai_admin_assistant_chats }, index: { name: "raa_assistant_messages_chat" }
28
+ t.string :role, null: false
29
+ t.text :content
30
+ t.boolean :cache_until_here, null: false, default: false
31
+ t.string :cache_ttl
32
+ t.text :thinking_text, :thinking_signature
33
+ t.json :citations, :server_tool_calls, :raw_content, :raw_reasoning
34
+ t.string :finish_reason
35
+ t.timestamps
36
+ end
37
+
38
+ # One persistent inbox per chat: human messages and Surface continuations
39
+ # are admitted once by key and run strictly in sequence under a lease.
40
+ create_table :rails_ai_admin_assistant_turns, id: :string, limit: 36 do |t|
41
+ t.references :chat, null: false, type: key, foreign_key: { to_table: :rails_ai_admin_assistant_chats }, index: false
42
+ t.integer :sequence, null: false
43
+ t.string :kind, null: false
44
+ t.string :admission_key, null: false, limit: 100
45
+ t.text :content, null: false
46
+ t.string :status, null: false, default: "queued"
47
+ t.references :user_message, type: key, foreign_key: { to_table: :rails_ai_admin_assistant_messages }, index: { name: "raa_assistant_turns_message" }
48
+ t.string :lease_owner, limit: 36
49
+ t.datetime :lease_expires_at, :next_attempt_at, :completed_at
50
+ t.integer :generation, :attempts, null: false, default: 0
51
+ t.string :error_code, limit: 64
52
+ t.timestamps
53
+ end
54
+ add_index :rails_ai_admin_assistant_turns, [ :chat_id, :sequence ], unique: true, name: "raa_assistant_turns_sequence"
55
+ add_index :rails_ai_admin_assistant_turns, [ :chat_id, :admission_key ], unique: true, name: "raa_assistant_turns_admission"
56
+ add_index :rails_ai_admin_assistant_turns, [ :status, :lease_expires_at ], name: "raa_assistant_turns_due"
57
+ add_check_constraint :rails_ai_admin_assistant_turns, "kind IN ('message', 'continuation')", name: "raa_assistant_turns_kind"
58
+ add_check_constraint :rails_ai_admin_assistant_turns, "status IN ('queued', 'running', 'completed', 'failed', 'cancelled')", name: "raa_assistant_turns_status"
59
+ end
60
+
61
+ # RubyLLM's shared tables may belong to the host product; they are kept.
62
+ def down
63
+ drop_table :rails_ai_admin_assistant_turns
64
+ drop_table :rails_ai_admin_assistant_messages
65
+ drop_table :rails_ai_admin_assistant_chats
66
+ end
67
+
68
+ private
69
+
70
+ # Polymorphic tool-call/usage columns must hold admin message and chat keys,
71
+ # so admin keys follow an existing RubyLLM installation.
72
+ def reference_type
73
+ return :bigint unless table_exists?(:ruby_llm_tool_calls)
74
+ column = connection.columns(:ruby_llm_tool_calls).find { |candidate| candidate.name == "message_id" }
75
+ column&.type == :uuid ? :uuid : :bigint
76
+ end
77
+
78
+ # Mirrors the RubyLLM 2.1.1 install template for the tables admin Chat uses;
79
+ # UpgradeRailsAiAdminAssistantToRubyLlm22 brings them to 2.2.
80
+ def create_ruby_llm_tables(key)
81
+ json = connection.adapter_name.match?(/postg/i) ? :jsonb : :json
82
+ unless table_exists?(:ruby_llm_models)
83
+ create_table :ruby_llm_models, id: key do |t|
84
+ t.string :model_id, :name, :provider, null: false
85
+ t.string :family
86
+ t.datetime :model_created_at
87
+ t.integer :context_window, :max_output_tokens
88
+ t.date :knowledge_cutoff
89
+ t.datetime :unlisted_at
90
+ t.column :modalities, json, default: {}
91
+ t.column :capabilities, json, default: []
92
+ t.column :pricing, json, default: {}
93
+ t.column :metadata, json, default: {}
94
+ t.timestamps
95
+ t.index [ :provider, :model_id ], unique: true
96
+ t.index :provider
97
+ t.index :family
98
+ end
99
+ end
100
+ unless table_exists?(:ruby_llm_tool_calls)
101
+ create_table :ruby_llm_tool_calls, id: key do |t|
102
+ t.references :message, polymorphic: true, null: false, type: key, index: false
103
+ t.references :result, polymorphic: true, type: key, index: false
104
+ t.string :tool_call_id, :name, null: false
105
+ t.text :thought_signature
106
+ t.string :approval
107
+ t.boolean :remote, default: false, null: false
108
+ t.column :arguments, json, default: {}
109
+ t.column :mcp_state, json
110
+ t.column :mcp_result, json
111
+ t.timestamps
112
+ t.index [ :message_type, :message_id ]
113
+ t.index [ :result_type, :result_id ]
114
+ t.index :tool_call_id, unique: true
115
+ t.index :name
116
+ end
117
+ end
118
+ return if table_exists?(:ruby_llm_usages)
119
+ create_table :ruby_llm_usages, id: key do |t|
120
+ t.references :chat, polymorphic: true, type: key, index: false
121
+ t.references :message, polymorphic: true, type: key, index: false
122
+ t.references :owner, polymorphic: true, type: key, index: false
123
+ t.string :operation, :provider, :model, :status, null: false
124
+ t.integer :input_tokens, :output_tokens, :cache_read_tokens, :cache_write_tokens, :thinking_tokens
125
+ t.column :server_tool_use, json
126
+ t.decimal :input_cost, :output_cost, :cache_read_cost, :cache_write_cost, :thinking_cost, :total_cost, precision: 16, scale: 10
127
+ t.timestamps
128
+ t.index [ :chat_type, :chat_id ]
129
+ t.index [ :message_type, :message_id ]
130
+ t.index [ :owner_type, :owner_id ]
131
+ t.index :status
132
+ end
133
+ entry = ::RubyLLM::Accounting::Usage::Entry
134
+ quote = ->(values) { values.map { |value| connection.quote(value.to_s) }.join(", ") }
135
+ add_check_constraint :ruby_llm_usages, "operation IN (#{quote.call(entry::OPERATIONS)})"
136
+ add_check_constraint :ruby_llm_usages, "status IN (#{quote.call(entry::STATUSES)})"
137
+ end
138
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Adds the storage RubyLLM 2.2 reads and writes, following its
4
+ # upgrade_ruby_llm_to_2_2 template. Every step is skipped when already present,
5
+ # so it is safe beside a host that ran RubyLLM's own install or upgrade for the
6
+ # shared tables (tool calls, usages, queued messages, chat files).
7
+ class UpgradeRailsAiAdminAssistantToRubyLlm22 < ActiveRecord::Migration[8.0]
8
+ CHATS = :rails_ai_admin_assistant_chats
9
+ MESSAGES = :rails_ai_admin_assistant_messages
10
+
11
+ def up
12
+ add_column CHATS, :claim_token, :string unless column_exists?(CHATS, :claim_token)
13
+ add_column CHATS, :claimed_until, :datetime unless column_exists?(CHATS, :claimed_until)
14
+ unless index_exists?(CHATS, :claimed_until, name: "raa_assistant_chats_claim")
15
+ add_index CHATS, :claimed_until, where: "claim_token IS NOT NULL", name: "raa_assistant_chats_claim"
16
+ end
17
+ add_column MESSAGES, :phase, :string unless column_exists?(MESSAGES, :phase)
18
+ add_column MESSAGES, :continuation, :text unless column_exists?(MESSAGES, :continuation)
19
+
20
+ upgrade_tool_calls
21
+ upgrade_usages
22
+ key = reference_type
23
+ create_queued_messages(key)
24
+ create_chat_files(key)
25
+ end
26
+
27
+ # RubyLLM's shared tables may belong to the host product; they are kept.
28
+ def down
29
+ remove_index CHATS, name: "raa_assistant_chats_claim", if_exists: true
30
+ remove_column CHATS, :claimed_until, if_exists: true
31
+ remove_column CHATS, :claim_token, if_exists: true
32
+ remove_column MESSAGES, :continuation, if_exists: true
33
+ remove_column MESSAGES, :phase, if_exists: true
34
+ end
35
+
36
+ private
37
+
38
+ def upgrade_tool_calls
39
+ unless column_exists?(:ruby_llm_tool_calls, :provider_call_id)
40
+ if column_exists?(:ruby_llm_tool_calls, :provider_id)
41
+ rename_column :ruby_llm_tool_calls, :provider_id, :provider_call_id
42
+ else
43
+ add_column :ruby_llm_tool_calls, :provider_call_id, :string
44
+ end
45
+ end
46
+ add_column :ruby_llm_tool_calls, :namespace, :string unless column_exists?(:ruby_llm_tool_calls, :namespace)
47
+ add_column :ruby_llm_tool_calls, :claimed_until, :datetime unless column_exists?(:ruby_llm_tool_calls, :claimed_until)
48
+ end
49
+
50
+ # An operation check written by 2.1 lacks code_execution. Admin Chat never
51
+ # runs code, so it is left alone; a host that does runs RubyLLM's upgrade.
52
+ def upgrade_usages
53
+ %i[workflow_id workflow_name workflow_step_id workflow_step_name].each do |column|
54
+ add_column :ruby_llm_usages, column, :string unless column_exists?(:ruby_llm_usages, column)
55
+ end
56
+ add_index :ruby_llm_usages, :workflow_id unless index_exists?(:ruby_llm_usages, :workflow_id)
57
+ add_column :ruby_llm_usages, :characters, :integer unless column_exists?(:ruby_llm_usages, :characters)
58
+ add_column :ruby_llm_usages, :audio_seconds, :decimal, precision: 16, scale: 6 unless column_exists?(:ruby_llm_usages, :audio_seconds)
59
+ add_column :ruby_llm_usages, :search_units, :integer unless column_exists?(:ruby_llm_usages, :search_units)
60
+ %i[characters_cost audio_seconds_cost search_units_cost].each do |column|
61
+ add_column :ruby_llm_usages, column, :decimal, precision: 16, scale: 10 unless column_exists?(:ruby_llm_usages, column)
62
+ end
63
+ end
64
+
65
+ def create_queued_messages(key)
66
+ return if table_exists?(:ruby_llm_queued_messages)
67
+ create_table :ruby_llm_queued_messages, id: key do |t|
68
+ t.references :chat, polymorphic: true, null: false, type: key, index: false
69
+ t.references :sender, polymorphic: true, type: key, index: false
70
+ t.text :content
71
+ t.boolean :steer, default: true, null: false
72
+ t.timestamps
73
+ t.index [ :chat_type, :chat_id ]
74
+ t.index [ :sender_type, :sender_id ]
75
+ end
76
+ end
77
+
78
+ def create_chat_files(key)
79
+ unless table_exists?(:ruby_llm_chat_files)
80
+ create_table :ruby_llm_chat_files, id: key do |t|
81
+ t.references :chat, polymorphic: true, null: false, type: key, index: false
82
+ t.references :owner, polymorphic: true, type: key, index: false
83
+ t.string :path, null: false
84
+ t.string :content_type
85
+ t.bigint :byte_size
86
+ t.string :digest
87
+ t.text :text
88
+ t.string :read_digest
89
+ t.timestamps
90
+ t.index [ :chat_type, :chat_id, :path ]
91
+ t.index [ :owner_type, :owner_id ]
92
+ end
93
+ end
94
+ add_column :ruby_llm_chat_files, :read_digest, :string unless column_exists?(:ruby_llm_chat_files, :read_digest)
95
+ end
96
+
97
+ # New polymorphic columns hold the same keys as the existing tool calls.
98
+ def reference_type
99
+ column = connection.columns(:ruby_llm_tool_calls).find { |candidate| candidate.name == "message_id" }
100
+ column&.type == :uuid ? :uuid : :bigint
101
+ end
102
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Streaming replies keep the text of the request in progress on its turn, and
4
+ # MCP clients authenticate with personal access tokens. Steps already present
5
+ # are skipped.
6
+ class AddStreamingAndAccessTokensToRailsAiAdminAssistant < ActiveRecord::Migration[8.0]
7
+ def up
8
+ add_column :rails_ai_admin_assistant_turns, :draft, :text unless column_exists?(:rails_ai_admin_assistant_turns, :draft)
9
+ return if table_exists?(:rails_ai_admin_access_tokens)
10
+ create_table :rails_ai_admin_access_tokens, id: :string, limit: 36 do |t|
11
+ t.string :actor_ref, :tenant_ref, null: false, limit: 150
12
+ t.string :security_version, null: false, limit: 150
13
+ t.string :name, null: false, limit: 80
14
+ # SHA-256 of the token; the token itself is shown once and never stored.
15
+ t.string :token_digest, null: false, limit: 64
16
+ t.datetime :expires_at, null: false
17
+ t.datetime :last_used_at
18
+ t.datetime :revoked_at
19
+ t.timestamps
20
+ end
21
+ add_index :rails_ai_admin_access_tokens, :token_digest, unique: true, name: "raa_access_tokens_digest"
22
+ add_index :rails_ai_admin_access_tokens, [ :actor_ref, :tenant_ref ], name: "raa_access_tokens_owner"
23
+ end
24
+
25
+ def down
26
+ drop_table :rails_ai_admin_access_tokens, if_exists: true
27
+ remove_column :rails_ai_admin_assistant_turns, :draft, if_exists: true
28
+ end
29
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateRailsAiAdminExecutionTables < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :rails_ai_admin_operation_runs, id: :string, limit: 36 do |t|
6
+ t.string :actor_ref, :tenant_ref, :security_version, null: false, limit: 150
7
+ t.string :resource_key, :operation, :operation_version, null: false
8
+ t.string :target_id, :record_id, limit: 150
9
+ t.bigint :expected_version
10
+ t.string :input_digest, :approved_digest, limit: 64
11
+ t.string :status, null: false, default: "proposed"
12
+ t.string :origin, null: false, default: "ui"
13
+ t.string :approved_by_ref, limit: 150
14
+ t.datetime :confirmed_at, :expires_at, :completed_at
15
+ t.bigint :audit_event_id
16
+ t.timestamps
17
+ end
18
+ add_index :rails_ai_admin_operation_runs, [ :tenant_ref, :actor_ref, :created_at ], name: "raa_runs_owner"
19
+ add_index :rails_ai_admin_operation_runs, [ :status, :expires_at ], name: "raa_runs_expiry"
20
+ add_check_constraint :rails_ai_admin_operation_runs, "status IN ('proposed', 'running', 'succeeded')", name: "raa_runs_status"
21
+
22
+ create_table :rails_ai_admin_audit_events do |t|
23
+ t.string :actor_ref, :tenant_ref, null: false, limit: 150
24
+ t.string :resource_key, :operation, :status, :code, null: false
25
+ t.string :record_id, limit: 150
26
+ t.string :correlation_ref, null: false, limit: 36
27
+ t.string :origin, null: false
28
+ t.json :changed_fields, null: false, default: []
29
+ t.datetime :created_at, null: false
30
+ end
31
+ add_index :rails_ai_admin_audit_events, [ :tenant_ref, :created_at ], name: "raa_audit_tenant"
32
+ add_index :rails_ai_admin_audit_events, :correlation_ref, name: "raa_audit_correlation"
33
+ add_foreign_key :rails_ai_admin_operation_runs, :rails_ai_admin_audit_events, column: :audit_event_id
34
+ end
35
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AddCallbackWarningToRailsAiAdminRuns < ActiveRecord::Migration[8.0]
4
+ def change
5
+ add_column :rails_ai_admin_operation_runs, :warning_code, :string
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateRailsAiAdminWorkItems < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :rails_ai_admin_work_items, id: :string, limit: 36 do |t|
6
+ t.string :operation_run_id, null: false, limit: 36
7
+ t.string :work_key, :kind, :handler_version, :payload_digest, null: false
8
+ t.json :payload, null: false, default: {}
9
+ t.string :status, null: false, default: "pending"
10
+ t.bigint :expected_version
11
+ t.integer :generation, :attempts, null: false, default: 0
12
+ t.integer :effect_generation
13
+ t.string :effect_digest, limit: 64
14
+ t.string :lease_owner, limit: 36
15
+ t.datetime :lease_expires_at, :next_attempt_at, :effect_started_at, :completed_at
16
+ t.datetime :deadline_at, null: false
17
+ t.string :last_code
18
+ t.timestamps
19
+ end
20
+ add_index :rails_ai_admin_work_items, [ :operation_run_id, :work_key ], unique: true, name: "raa_work_intent_key"
21
+ add_index :rails_ai_admin_work_items, [ :status, :next_attempt_at ], name: "raa_work_pending"
22
+ add_index :rails_ai_admin_work_items, [ :status, :lease_expires_at ], name: "raa_work_lease"
23
+ add_foreign_key :rails_ai_admin_work_items, :rails_ai_admin_operation_runs, column: :operation_run_id
24
+ add_check_constraint :rails_ai_admin_work_items, "status IN ('pending', 'running', 'succeeded', 'failed', 'cancelled', 'reconciliation_required')", name: "raa_work_status"
25
+ end
26
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateRailsAiAdminSelections < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :rails_ai_admin_selections, id: :string, limit: 36 do |t|
6
+ t.string :actor_ref, :tenant_ref, :security_version, null: false, limit: 150
7
+ t.string :resource_key, null: false
8
+ t.string :resource_version, :manifest_digest, null: false, limit: 64
9
+ t.integer :item_count, null: false
10
+ t.datetime :expires_at, null: false
11
+ t.timestamps
12
+ end
13
+ add_index :rails_ai_admin_selections, [ :tenant_ref, :actor_ref, :created_at ], name: "raa_selections_owner"
14
+ add_index :rails_ai_admin_selections, :expires_at, name: "raa_selections_expiry"
15
+ add_check_constraint :rails_ai_admin_selections, "item_count BETWEEN 1 AND 100", name: "raa_selections_count"
16
+
17
+ create_table :rails_ai_admin_selection_items do |t|
18
+ t.string :selection_id, null: false, limit: 36
19
+ t.string :target_id, null: false, limit: 150
20
+ t.bigint :expected_version, null: false
21
+ t.integer :position, null: false
22
+ end
23
+ add_foreign_key :rails_ai_admin_selection_items, :rails_ai_admin_selections, column: :selection_id
24
+ add_index :rails_ai_admin_selection_items, [ :selection_id, :target_id ], unique: true, name: "raa_selection_targets"
25
+ add_index :rails_ai_admin_selection_items, [ :selection_id, :position ], unique: true, name: "raa_selection_order"
26
+ add_check_constraint :rails_ai_admin_selection_items, "expected_version >= 0 AND position BETWEEN 0 AND 99", name: "raa_selection_item_bounds"
27
+ end
28
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateRailsAiAdminCollectionRuns < ActiveRecord::Migration[8.0]
4
+ def change
5
+ remove_check_constraint :rails_ai_admin_operation_runs, "status IN ('proposed', 'running', 'succeeded')", name: "raa_runs_status"
6
+ change_table :rails_ai_admin_operation_runs do |t|
7
+ t.string :selection_id, :parent_run_id, limit: 36
8
+ t.string :selection_digest, :manifest_resource_version, limit: 64
9
+ t.json :input_payload, :manifest_payload
10
+ t.datetime :execution_deadline, :cancellation_requested_at
11
+ end
12
+ add_foreign_key :rails_ai_admin_operation_runs, :rails_ai_admin_selections, column: :selection_id
13
+ add_foreign_key :rails_ai_admin_operation_runs, :rails_ai_admin_operation_runs, column: :parent_run_id
14
+ add_index :rails_ai_admin_operation_runs, [ :parent_run_id, :target_id ], unique: true, name: "raa_child_target"
15
+ add_check_constraint :rails_ai_admin_operation_runs,
16
+ "status IN ('proposed', 'queued', 'running', 'succeeded', 'failed', 'partially_succeeded', 'cancelled', 'reconciliation_required')", name: "raa_runs_status"
17
+
18
+ create_table :rails_ai_admin_batch_items, id: :string, limit: 36 do |t|
19
+ t.string :parent_run_id, :operation_run_id, null: false, limit: 36
20
+ t.bigint :selection_item_id, null: false
21
+ t.string :target_id, null: false, limit: 150
22
+ t.bigint :expected_version, null: false
23
+ t.integer :position, null: false
24
+ t.string :status, null: false, default: "queued"
25
+ t.string :lease_owner, limit: 36
26
+ t.integer :generation, :attempts, null: false, default: 0
27
+ t.string :last_code, limit: 64
28
+ t.datetime :lease_expires_at, :next_attempt_at, :completed_at
29
+ t.timestamps
30
+ end
31
+ add_foreign_key :rails_ai_admin_batch_items, :rails_ai_admin_operation_runs, column: :parent_run_id
32
+ add_foreign_key :rails_ai_admin_batch_items, :rails_ai_admin_operation_runs, column: :operation_run_id
33
+ add_foreign_key :rails_ai_admin_batch_items, :rails_ai_admin_selection_items, column: :selection_item_id
34
+ add_index :rails_ai_admin_batch_items, :operation_run_id, unique: true, name: "raa_batch_child"
35
+ add_index :rails_ai_admin_batch_items, [ :parent_run_id, :target_id ], unique: true, name: "raa_batch_target"
36
+ add_index :rails_ai_admin_batch_items, [ :parent_run_id, :position ], unique: true, name: "raa_batch_order"
37
+ add_index :rails_ai_admin_batch_items, [ :status, :lease_expires_at, :next_attempt_at ], name: "raa_batch_due"
38
+ add_check_constraint :rails_ai_admin_batch_items, "status IN ('queued', 'running', 'succeeded', 'failed', 'cancelled')", name: "raa_batch_status"
39
+ add_check_constraint :rails_ai_admin_batch_items, "position BETWEEN 0 AND 99 AND expected_version >= 0 AND generation >= 0 AND attempts >= 0", name: "raa_batch_bounds"
40
+ end
41
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Serves a record's history page: one tenant, resource and record, newest first.
4
+ class AddRecordIndexToRailsAiAdminAuditEvents < ActiveRecord::Migration[8.0]
5
+ def change
6
+ add_index :rails_ai_admin_audit_events, [ :tenant_ref, :resource_key, :record_id, :id ], name: "raa_audit_record"
7
+ end
8
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require_relative "../engine_migrations"
5
+
6
+ module RailsAiAdmin
7
+ module Generators
8
+ # Adds opt-in admin Chat storage and a closed provider adapter. Nothing is
9
+ # enabled until the host sets `config.assistant = true` and returns a
10
+ # RubyLLM::Context from the adapter.
11
+ class AssistantGenerator < Rails::Generators::Base
12
+ include EngineMigrations
13
+ source_root File.expand_path("templates", __dir__)
14
+
15
+ def install
16
+ copy_engine_migrations("db/assistant_migrate")
17
+ target = "app/rails_ai_admin/admin_assistant.rb"
18
+ template "admin_assistant.rb", target unless File.exist?(File.join(destination_root, target))
19
+ say "The assistant stays unavailable: AdminAssistant#llm_context returns nil, and no resource grants assistant?."
20
+ say "Add ruby_llm (#{Configuration::OPTIONAL_GEMS.fetch('ruby_llm').fetch(:requirement).join(', ')}) to the Gemfile, run db:migrate, then set config.assistant = true, config.assistant_adapter = \"AdminAssistant\" and config.assistant_model."
21
+ say "Schedule rails_ai_admin:assistant:scan to recover lost jobs. Guide: #{GUIDES}/_advanced/admin-assistant.md"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,18 @@
1
+ # Builds the admin assistant's provider configuration for one admin Context.
2
+ # Returning nil keeps the assistant unavailable for that actor; this is the
3
+ # default. Read credentials from your secret store and never change global
4
+ # RubyLLM settings here: the admin's provider settings stay separate from your
5
+ # product's.
6
+ class AdminAssistant
7
+ def llm_context(context:)
8
+ nil
9
+ # key = Rails.application.credentials.dig(:admin_assistant, :openai_api_key)
10
+ # return unless key
11
+ #
12
+ # RubyLLM.context do |config|
13
+ # config.openai_api_key = key
14
+ # config.request_timeout = 60
15
+ # config.max_retries = 1
16
+ # end
17
+ end
18
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/migration"
4
+
5
+ module RailsAiAdmin
6
+ module Generators
7
+ # Copies each engine migration the host does not have yet. A migration the
8
+ # host already has is kept even with --force: it may have run, and
9
+ # replacing it under a new timestamp would run it twice. Schema changes
10
+ # ship as new migrations.
11
+ # The gem ships no docs/ directory, so generator output links the guides
12
+ # in the source repository (RAA-026).
13
+ GUIDES = "https://github.com/yorzi/rails_ai_admin/blob/main/docs/site"
14
+
15
+ module EngineMigrations # :nodoc:
16
+ def self.included(base)
17
+ base.include Rails::Generators::Migration
18
+ base.extend ClassMethods
19
+ end
20
+
21
+ module ClassMethods # :nodoc:
22
+ def next_migration_number(dirname)
23
+ [ Time.now.utc.strftime("%Y%m%d%H%M%S"), format("%014d", current_migration_number(dirname) + 1) ].max
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def copy_engine_migrations(directory)
30
+ Engine.root.join(directory).glob("*.rb").sort.each do |source|
31
+ stem = source.basename(".rb").to_s.sub(/\A\d+_/, "")
32
+ existing = self.class.migration_exists?(File.join(destination_root, "db/migrate"), stem)
33
+ if existing
34
+ say_status :exist, Pathname.new(existing).relative_path_from(Pathname.new(destination_root)).to_s, :blue
35
+ else
36
+ migration_template source.to_s, "db/migrate/#{stem}.rb"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAiAdmin
4
+ module Generators
5
+ # Uses Rails' loaded routes, including drawn files and namespace prefixes.
6
+ module EngineMounts # :nodoc:
7
+ def self.names(routes)
8
+ routes.routes.filter_map do |route|
9
+ endpoint = route.app
10
+ while endpoint.is_a?(ActionDispatch::Routing::Mapper::Constraints)
11
+ endpoint = endpoint.app
12
+ end
13
+ next unless endpoint.equal?(RailsAiAdmin::Engine) || endpoint.is_a?(RailsAiAdmin::Engine)
14
+ route.parts.empty? && route.verb.empty? ? route.name.to_s : :unsupported
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators"
4
+ require_relative "engine_mounts"
5
+ require_relative "../engine_migrations"
6
+
7
+ module RailsAiAdmin
8
+ module Generators
9
+ # Installs conventional tables and a deliberately closed host context.
10
+ class InstallGenerator < Rails::Generators::Base
11
+ include EngineMigrations
12
+ source_root File.expand_path("templates", __dir__)
13
+
14
+ def initialize(args = [], options = {}, config = {})
15
+ @host_application = config.fetch(:application) { Rails.application }
16
+ super
17
+ end
18
+
19
+ def install
20
+ unless @host_application && @host_application.root.realpath == Pathname.new(destination_root).realpath
21
+ raise Rails::Generators::Error, "Run install inside the target Rails application so its actual routes can be checked. No files were changed."
22
+ end
23
+ mounts = EngineMounts.names(@host_application.routes)
24
+ if mounts.empty? && @host_application.routes.named_routes.names.map(&:to_s).include?("rails_ai_admin")
25
+ raise Rails::Generators::Error, "The rails_ai_admin route name belongs to another endpoint. Resolve the conflict before installation. No files were changed."
26
+ end
27
+ if mounts.any? && mounts != [ "rails_ai_admin" ]
28
+ raise Rails::Generators::Error, "Use one RailsAiAdmin::Engine mount whose final helper is rails_ai_admin. Custom paths are supported; other aliases, name prefixes, dynamic parameters and multiple mounts require review. No files were changed."
29
+ end
30
+ %w[AdminContext AdminPolicyAdapter ExampleAdmin].each do |constant|
31
+ target = File.join(destination_root, "app/rails_ai_admin/#{constant.underscore}.rb")
32
+ if Object.const_defined?(constant) && !File.exist?(target)
33
+ raise Rails::Generators::Error, "Constant #{constant} already exists outside the generated path"
34
+ end
35
+ end
36
+ copy_engine_migrations("db/migrate")
37
+ template "initializer.rb", "config/initializers/rails_ai_admin.rb"
38
+ template "admin_context.rb", "app/rails_ai_admin/admin_context.rb"
39
+ {
40
+ "admin_policy_adapter.rb" => "app/rails_ai_admin/admin_policy_adapter.rb",
41
+ "example_admin.rb" => "app/rails_ai_admin/example_admin.rb",
42
+ "access_test.rb" => "test/rails_ai_admin/access_test.rb"
43
+ }.each do |source, target|
44
+ template source, target unless File.exist?(File.join(destination_root, target))
45
+ end
46
+ route 'mount RailsAiAdmin::Engine => "/admin", as: :rails_ai_admin' if mounts.empty?
47
+ say "Access remains closed. Implement AdminContext and an explicit policy adapter; then run db:migrate and rails_ai_admin:doctor."
48
+ say "ExampleAdmin is unregistered. Review it or generate your own resource. Run bin/rails test test/rails_ai_admin/access_test.rb."
49
+ say "Guide: #{GUIDES}/_getting_started/getting-started.md"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,17 @@
1
+ require "test_helper"
2
+
3
+ class RailsAiAdminAccessTest < ActionDispatch::IntegrationTest
4
+ # Closed means no admin page renders: a 401, or with config.authentication_path
5
+ # a redirect to sign-in.
6
+ test "anonymous access stays closed" do
7
+ path = Rails.application.routes.url_helpers.rails_ai_admin_path
8
+ get path
9
+ sign_in = RailsAiAdmin.configuration.authentication_path
10
+ if sign_in
11
+ assert_response :see_other
12
+ assert_equal sign_in, URI(response.location).path
13
+ else
14
+ assert_response :unauthorized
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,25 @@
1
+ class AdminContext
2
+ def resolve(controller)
3
+ # TODO: authenticate through your host's public session/authentication API.
4
+ # Return RailsAiAdmin::Context.new(actor:, tenant:, security_version:).
5
+ # Increment security_version when session, role or tenant authority changes.
6
+ nil
7
+ end
8
+
9
+ # Working example for an app made with `bin/rails generate authentication`
10
+ # and a boolean `admin` column on users. Replace the method above with it
11
+ # once you have reviewed it. Devise apps read controller.warden.user(:user).
12
+ #
13
+ # # One fixed tenant for an app without accounts. With an Account model,
14
+ # # pass the user's account instead and scope the policy by it.
15
+ # SingleTenant = Data.define(:id)
16
+ # TENANT = SingleTenant.new(id: "app")
17
+ #
18
+ # def resolve(controller)
19
+ # session = Session.find_by(id: controller.request.cookie_jar.signed[:session_id])
20
+ # user = session&.user
21
+ # return unless user&.admin?
22
+ # # Any change to the user, including the admin flag, invalidates open forms.
23
+ # RailsAiAdmin::Context.new(actor: user, tenant: TENANT, security_version: user.updated_at.to_i.to_s)
24
+ # end
25
+ end