legion-data 1.6.20 → 1.6.22

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 68c8710d74c2f470f06e8c669c539c4db12bc0ac25807775cc85beb4e972e3b7
4
- data.tar.gz: 5234ddf1a5d57c52e2af42c5264256327c746d3adcdc3117f3d73e04e127a481
3
+ metadata.gz: 1f925497f4f43ca263e4cd90d07bb3adddc947e3713bc8cab3a489ae5e162a68
4
+ data.tar.gz: c3eb803e0c3b65459ef74adb5a40d5afdcac41ba6adb64401b70809414c052bf
5
5
  SHA512:
6
- metadata.gz: f514db438336d661316ee66b7171ad4955a755053642a15fce766f32d35bded7c815ea9cc2e9372cb1d831a29413d30e6a9f607955b725a0a959b155ba9bfc72
7
- data.tar.gz: 660ed97282f052ba1ce6b7b20651f8a7cef8b7b792329cc07326f6edf9038cefa1242bbfbf6714623bd39b763f5afd7c908d223ec2331905d90bf9725fe7543e
6
+ metadata.gz: 057b2b9fa0518455dae0b1a5907ea303e17c191f4b34306e92124a48a1022a3f10eabfe34b07456085ecd4aca823ba86d352b4562865d94ca23d13ce857b0e6e
7
+ data.tar.gz: 99a7bef85d7f5045d638ea1f06a9c336da33c21b6ab6b46c6421de9e0aac50d3ab394535e539e7bafe2f7585103864f35444ba87dd59a90107831a6eb9f18abc
data/.gitignore CHANGED
@@ -18,3 +18,4 @@ legion.log
18
18
  .DS_Store
19
19
  # SQLite database files
20
20
  *.db
21
+ .worktrees
data/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.6.22] - 2026-04-06
6
+
7
+ ### Added
8
+ - Migration 063: `identity_providers` table (provider_type, facing, priority, trust_weight, capabilities)
9
+ - Migration 064: `principals` table (canonical_name regex constraint, kind, unique composite)
10
+ - Migration 065: `identities` table (principal/provider FKs, partial unique index on active)
11
+ - Migration 066: `identity_groups` table (source: ldap/entra/manual)
12
+ - Migration 067: `identity_group_memberships` table (status, trust_weight, discovered_by, tie-break index)
13
+ - Migration 068: `entity_type` column on `audit_records` with index
14
+ - Migration 069: `principal_id` FK on `nodes` table
15
+ - 5 Sequel models: `IdentityProvider`, `Principal`, `Identity`, `IdentityGroup`, `IdentityGroupMembership`
16
+ - `Identity` model wired through `SequelPlugin` `encrypted_column :profile` for at-rest encryption
17
+ - `Node` model gains `many_to_one :principal` association
18
+
19
+ ### Changed
20
+ - Migration mode gate: only `:infra` mode runs migrations when `Legion::Mode` is available
21
+ - `auto_migrate` settings check wired into `Data.setup` (skips migrations when `auto_migrate: false`)
22
+ - Mode guard added to both `Data.migrate` and `Migration.migrate` for defense-in-depth
23
+
24
+ ## [1.6.21] - 2026-04-05
25
+
26
+ ### Added
27
+ - Migration 062: `tool_embedding_cache` table for global embedding persistence
28
+
5
29
  ## [1.6.20] - 2026-04-03
6
30
 
7
31
  ### Fixed
data/CLAUDE.md CHANGED
@@ -8,7 +8,7 @@
8
8
  Manages persistent database storage for the LegionIO framework. Supports SQLite (default), MySQL, and PostgreSQL via Sequel ORM. Provides automatic schema migrations and data models for extensions, functions, runners, nodes, tasks, settings, digital workers, task relationships, Apollo shared knowledge tables (PostgreSQL only), tenants, webhooks, audit log, and archive tables. Also provides a parallel local SQLite database (`Legion::Data::Local`) for agentic cognitive state persistence.
9
9
 
10
10
  **GitHub**: https://github.com/LegionIO/legion-data
11
- **Version**: 1.6.11
11
+ **Version**: 1.6.21
12
12
  **License**: Apache-2.0
13
13
 
14
14
  ## Supported Databases
@@ -56,7 +56,7 @@ Legion::Data (singleton module)
56
56
  │ ├── .shutdown # Close local connection
57
57
  │ └── .reset! # Clear all state (testing)
58
58
 
59
- ├── Migration # Auto-migration system (57 migrations, Sequel DSL)
59
+ ├── Migration # Auto-migration system (58 migrations, Sequel DSL)
60
60
  │ └── migrations/
61
61
  │ ├── 001_add_schema_columns
62
62
  │ ├── 002_add_nodes
@@ -114,7 +114,8 @@ Legion::Data (singleton module)
114
114
  │ ├── 054_add_component_type_to_functions # component_type on functions (runner/hook/absorber, v3.0)
115
115
  │ ├── 055_add_definition_to_functions # definition text column on functions (v3.0)
116
116
  │ ├── 056_add_absorber_patterns # absorber_patterns table for pattern-matched acquisition
117
- └── 057_add_routing_key_to_runners # routing_key on runners (v3.0 AMQP)
117
+ ├── 057_add_routing_key_to_runners # routing_key on runners (v3.0 AMQP)
118
+ │ └── 058_add_tool_embedding_cache # tool_embedding_cache table for global embedding cache tier (Tools::EmbeddingCache L4)
118
119
 
119
120
  ├── Model # Sequel model loader
120
121
  │ └── Models/
@@ -279,7 +280,7 @@ Per-adapter credential defaults are defined in `Settings::CREDS`:
279
280
  | `lib/legion/data.rb` | Module entry, setup/shutdown lifecycle |
280
281
  | `lib/legion/data/connection.rb` | Sequel database connection (adapter selection) |
281
282
  | `lib/legion/data/migration.rb` | Migration runner |
282
- | `lib/legion/data/migrations/` | 48 numbered migration files (Sequel DSL) |
283
+ | `lib/legion/data/migrations/` | 58 numbered migration files (Sequel DSL) |
283
284
  | `lib/legion/data/model.rb` | Model autoloader |
284
285
  | `lib/legion/data/local.rb` | Local SQLite module for agentic cognitive state |
285
286
  | `lib/legion/data/models/` | Sequel models (Extension, Function, Runner, Node, Task, TaskLog, Setting, DigitalWorker, Relationship, ApolloEntry, ApolloRelation, ApolloExpertise, ApolloAccessLog, AuditLog, RbacRoleAssignment, RbacRunnerGrant, RbacCrossTeamGrant) |
@@ -320,6 +321,7 @@ Optional persistent storage initialized during `Legion::Service` startup (after
320
321
  13. Archive, memory traces, and tenant partition tables (migrations 021–025)
321
322
  14. Function embeddings for semantic runner discovery (migration 026 — description + vector columns on functions table)
322
323
  15. Financial logging for UAIS cost recovery (migration 048 — 7 tables: identity, asset, environment, accounting, execution, tags, usage rollup)
324
+ 16. Global tool embedding cache (migration 058 — `tool_embedding_cache` table, L4 tier for `Legion::Tools::EmbeddingCache`)
323
325
 
324
326
  ---
325
327
 
@@ -11,6 +11,11 @@ module Legion
11
11
  include Legion::Logging::Helper
12
12
 
13
13
  def migrate(connection = Legion::Data.connection, path = "#{__dir__}/migrations", **)
14
+ if defined?(Legion::Mode) && Legion::Mode.respond_to?(:current) && !Legion::Mode.infra?
15
+ log.info "Legion::Data::Migration skipped (mode: #{Legion::Mode.current}, requires: infra)"
16
+ return
17
+ end
18
+
14
19
  Legion::Settings[:data][:migrations][:version] = Sequel::Migrator.run(connection, path, **)
15
20
  log.info("Legion::Data::Migration ran successfully to version #{Legion::Settings[:data][:migrations][:version]}")
16
21
  Legion::Settings[:data][:migrations][:ran] = true
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ change do
5
+ create_table(:tool_embedding_cache) do
6
+ primary_key :id
7
+ String :content_hash, size: 32, null: false
8
+ String :model, size: 100, null: false
9
+ String :tool_name, size: 200, null: false
10
+ column :vector, :text, null: false
11
+ DateTime :embedded_at, null: false
12
+ DateTime :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP
13
+ unique %i[content_hash model]
14
+ index :tool_name
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ up do
5
+ next unless adapter_scheme == :postgres
6
+
7
+ create_table(:identity_providers) do
8
+ column :id, :uuid, default: Sequel.lit('gen_random_uuid()'), primary_key: true
9
+ String :name, null: false, unique: true
10
+ String :provider_type, null: false # authenticate, profile, fallback
11
+ String :facing, null: false # human, machine, both
12
+ Integer :priority, null: false, default: 100
13
+ Integer :trust_weight, null: false, default: 50
14
+ column :capabilities, :'text[]', default: Sequel.lit("'{}'")
15
+ String :source, null: false, default: 'gem' # gem, db
16
+ TrueClass :enabled, null: false, default: true
17
+ DateTime :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP
18
+ DateTime :updated_at, null: false, default: Sequel::CURRENT_TIMESTAMP
19
+ end
20
+ end
21
+
22
+ down do
23
+ next unless adapter_scheme == :postgres
24
+
25
+ drop_table?(:identity_providers)
26
+ end
27
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ up do
5
+ next unless adapter_scheme == :postgres
6
+
7
+ create_table(:principals) do
8
+ column :id, :uuid, default: Sequel.lit('gen_random_uuid()'), primary_key: true
9
+ String :canonical_name, null: false
10
+ String :kind, null: false # human, service, machine
11
+ String :display_name
12
+ TrueClass :active, null: false, default: true
13
+ DateTime :last_seen_at
14
+ DateTime :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP
15
+ DateTime :updated_at, null: false, default: Sequel::CURRENT_TIMESTAMP
16
+
17
+ constraint(:canonical_name_format, Sequel.lit("canonical_name ~ '^[a-z0-9][a-z0-9_-]*$'"))
18
+ unique %i[canonical_name kind]
19
+ end
20
+
21
+ add_index :principals, :canonical_name
22
+ add_index :principals, :kind
23
+ end
24
+
25
+ down do
26
+ next unless adapter_scheme == :postgres
27
+
28
+ drop_table?(:principals)
29
+ end
30
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ up do
5
+ next unless adapter_scheme == :postgres
6
+
7
+ create_table(:identities) do
8
+ column :id, :uuid, default: Sequel.lit('gen_random_uuid()'), primary_key: true
9
+ foreign_key :principal_id, :principals, type: :uuid, null: false, on_delete: :cascade
10
+ foreign_key :provider_id, :identity_providers, type: :uuid, null: false, on_delete: :cascade
11
+ String :provider_identity, null: false # external ID from provider
12
+ column :profile, :bytea
13
+ TrueClass :active, null: false, default: true
14
+ DateTime :last_authenticated_at
15
+ DateTime :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP
16
+ DateTime :updated_at, null: false, default: Sequel::CURRENT_TIMESTAMP
17
+
18
+ unique %i[principal_id provider_id provider_identity]
19
+ end
20
+
21
+ # Partial unique index: only one active identity per provider+provider_identity
22
+ run 'CREATE UNIQUE INDEX identities_active_provider_uniq ON identities (provider_id, provider_identity) WHERE active = true'
23
+
24
+ add_index :identities, :principal_id
25
+ add_index :identities, :provider_id
26
+ end
27
+
28
+ down do
29
+ next unless adapter_scheme == :postgres
30
+
31
+ drop_table?(:identities)
32
+ end
33
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ up do
5
+ next unless adapter_scheme == :postgres
6
+
7
+ create_table(:identity_groups) do
8
+ column :id, :uuid, default: Sequel.lit('gen_random_uuid()'), primary_key: true
9
+ String :name, null: false, unique: true
10
+ String :source, null: false, default: 'ldap' # ldap, entra, manual
11
+ String :description
12
+ TrueClass :active, null: false, default: true
13
+ DateTime :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP
14
+ DateTime :updated_at, null: false, default: Sequel::CURRENT_TIMESTAMP
15
+ end
16
+ end
17
+
18
+ down do
19
+ next unless adapter_scheme == :postgres
20
+
21
+ drop_table?(:identity_groups)
22
+ end
23
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ up do
5
+ next unless adapter_scheme == :postgres
6
+
7
+ create_table(:identity_group_memberships) do
8
+ column :id, :uuid, default: Sequel.lit('gen_random_uuid()'), primary_key: true
9
+ foreign_key :principal_id, :principals, type: :uuid, null: false, on_delete: :cascade
10
+ foreign_key :group_id, :identity_groups, type: :uuid, null: false, on_delete: :cascade
11
+ String :status, null: false, default: 'active' # active, stale, expired
12
+ String :discovered_by, null: false # provider name that discovered this membership
13
+ Integer :trust_weight, null: false, default: 50
14
+ DateTime :expires_at
15
+ DateTime :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP
16
+ DateTime :updated_at, null: false, default: Sequel::CURRENT_TIMESTAMP
17
+
18
+ unique %i[principal_id group_id discovered_by]
19
+ end
20
+
21
+ add_index :identity_group_memberships, :principal_id
22
+ add_index :identity_group_memberships, :group_id
23
+ add_index :identity_group_memberships, :status
24
+ run <<~SQL
25
+ CREATE INDEX idx_memberships_trust_tiebreak
26
+ ON identity_group_memberships (principal_id, trust_weight ASC,
27
+ CASE status WHEN 'expired' THEN 0 WHEN 'stale' THEN 1 WHEN 'active' THEN 2 END ASC)
28
+ SQL
29
+ end
30
+
31
+ down do
32
+ next unless adapter_scheme == :postgres
33
+
34
+ drop_table?(:identity_group_memberships)
35
+ end
36
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ up do
5
+ next unless adapter_scheme == :postgres
6
+ next unless table_exists?(:audit_records)
7
+
8
+ alter_table(:audit_records) do
9
+ add_column :entity_type, String, size: 100, null: true
10
+ end
11
+
12
+ add_index :audit_records, :entity_type, name: :idx_audit_records_entity_type
13
+ end
14
+
15
+ down do
16
+ next unless adapter_scheme == :postgres
17
+ next unless table_exists?(:audit_records)
18
+
19
+ alter_table(:audit_records) do
20
+ drop_column :entity_type
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ up do
5
+ next unless adapter_scheme == :postgres
6
+
7
+ alter_table(:nodes) do
8
+ add_column :principal_id, :uuid
9
+ add_foreign_key [:principal_id], :principals
10
+ end
11
+
12
+ add_index :nodes, :principal_id
13
+ end
14
+
15
+ down do
16
+ next unless adapter_scheme == :postgres
17
+
18
+ alter_table(:nodes) do
19
+ drop_column :principal_id
20
+ end
21
+ end
22
+ end
@@ -13,7 +13,8 @@ module Legion
13
13
  def models
14
14
  %w[extension function relationship chain task runner node setting digital_worker
15
15
  apollo_entry apollo_relation apollo_expertise apollo_access_log audit_log
16
- audit_record]
16
+ audit_record identity_provider principal identity identity_group
17
+ identity_group_membership]
17
18
  end
18
19
 
19
20
  def load
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ return unless Legion::Data::Connection.adapter == :postgres
4
+
5
+ module Legion
6
+ module Data
7
+ module Model
8
+ class Identity < Sequel::Model(:identities)
9
+ many_to_one :principal, class: 'Legion::Data::Model::Principal'
10
+ many_to_one :provider, class: 'Legion::Data::Model::IdentityProvider', key: :provider_id
11
+
12
+ if defined?(Legion::Data::Encryption::SequelPlugin)
13
+ plugin Legion::Data::Encryption::SequelPlugin
14
+ encrypted_column :profile
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ return unless Legion::Data::Connection.adapter == :postgres
4
+
5
+ module Legion
6
+ module Data
7
+ module Model
8
+ class IdentityGroup < Sequel::Model(:identity_groups)
9
+ one_to_many :memberships, class: 'Legion::Data::Model::IdentityGroupMembership', key: :group_id
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ return unless Legion::Data::Connection.adapter == :postgres
4
+
5
+ module Legion
6
+ module Data
7
+ module Model
8
+ class IdentityGroupMembership < Sequel::Model(:identity_group_memberships)
9
+ many_to_one :principal, class: 'Legion::Data::Model::Principal'
10
+ many_to_one :group, class: 'Legion::Data::Model::IdentityGroup', key: :group_id
11
+
12
+ def expired?
13
+ status == 'expired' || (expires_at && Time.now >= expires_at)
14
+ end
15
+
16
+ def stale?
17
+ status == 'stale'
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ return unless Legion::Data::Connection.adapter == :postgres
4
+
5
+ module Legion
6
+ module Data
7
+ module Model
8
+ class IdentityProvider < Sequel::Model(:identity_providers)
9
+ one_to_many :identities, class: 'Legion::Data::Model::Identity'
10
+
11
+ def parsed_capabilities
12
+ Array(capabilities)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -9,6 +9,7 @@ module Legion
9
9
  include Legion::Logging::Helper
10
10
 
11
11
  # one_to_many :task_log
12
+ many_to_one :principal, class: 'Legion::Data::Model::Principal'
12
13
 
13
14
  def parsed_metrics
14
15
  return nil unless metrics
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ return unless Legion::Data::Connection.adapter == :postgres
4
+
5
+ module Legion
6
+ module Data
7
+ module Model
8
+ class Principal < Sequel::Model(:principals)
9
+ one_to_many :identities, class: 'Legion::Data::Model::Identity'
10
+ one_to_many :group_memberships, class: 'Legion::Data::Model::IdentityGroupMembership'
11
+
12
+ def active_groups
13
+ group_memberships_dataset
14
+ .where(status: 'active')
15
+ .eager(:group)
16
+ .all
17
+ .map(&:group)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Data
5
- VERSION = '1.6.20'
5
+ VERSION = '1.6.22'
6
6
  end
7
7
  end
data/lib/legion/data.rb CHANGED
@@ -58,6 +58,8 @@ module Legion
58
58
  end
59
59
 
60
60
  def migrate
61
+ return if skip_migrations?
62
+
61
63
  Legion::Data::Migration.migrate
62
64
  end
63
65
 
@@ -174,6 +176,23 @@ module Legion
174
176
 
175
177
  private
176
178
 
179
+ def skip_migrations?
180
+ # Check auto_migrate setting
181
+ auto_migrate = Legion::Settings[:data][:migrations][:auto_migrate]
182
+ unless auto_migrate
183
+ log.info 'Legion::Data migrations skipped (auto_migrate: false)'
184
+ return true
185
+ end
186
+
187
+ # Check mode gate: only infra mode runs migrations (when Mode is available)
188
+ if defined?(Legion::Mode) && Legion::Mode.respond_to?(:current) && !Legion::Mode.infra?
189
+ log.info "Legion::Data migrations skipped (mode: #{Legion::Mode.current}, requires: infra)"
190
+ return true
191
+ end
192
+
193
+ false
194
+ end
195
+
177
196
  def setup_local
178
197
  return if Legion::Settings[:data].dig(:local, :enabled) == false
179
198
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-data
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.20
4
+ version: 1.6.22
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -191,6 +191,14 @@ files:
191
191
  - lib/legion/data/migrations/059_create_chains.rb
192
192
  - lib/legion/data/migrations/060_add_knowledge_tiers.rb
193
193
  - lib/legion/data/migrations/061_add_versioning_and_expiry.rb
194
+ - lib/legion/data/migrations/062_create_tool_embedding_cache.rb
195
+ - lib/legion/data/migrations/063_create_identity_providers.rb
196
+ - lib/legion/data/migrations/064_create_principals.rb
197
+ - lib/legion/data/migrations/065_create_identities.rb
198
+ - lib/legion/data/migrations/066_create_identity_groups.rb
199
+ - lib/legion/data/migrations/067_create_identity_group_memberships.rb
200
+ - lib/legion/data/migrations/068_add_entity_type_to_audit_records.rb
201
+ - lib/legion/data/migrations/069_add_principal_id_to_nodes.rb
194
202
  - lib/legion/data/model.rb
195
203
  - lib/legion/data/models/apollo_access_log.rb
196
204
  - lib/legion/data/models/apollo_entry.rb
@@ -202,7 +210,12 @@ files:
202
210
  - lib/legion/data/models/digital_worker.rb
203
211
  - lib/legion/data/models/extension.rb
204
212
  - lib/legion/data/models/function.rb
213
+ - lib/legion/data/models/identity.rb
214
+ - lib/legion/data/models/identity_group.rb
215
+ - lib/legion/data/models/identity_group_membership.rb
216
+ - lib/legion/data/models/identity_provider.rb
205
217
  - lib/legion/data/models/node.rb
218
+ - lib/legion/data/models/principal.rb
206
219
  - lib/legion/data/models/rbac_cross_team_grant.rb
207
220
  - lib/legion/data/models/rbac_role_assignment.rb
208
221
  - lib/legion/data/models/rbac_runner_grant.rb