legion-apollo 0.5.3 → 0.5.5

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: a45052ee6c52f3642d1247538a691ace760e67bb5ec35f59fab03f42f95e8768
4
- data.tar.gz: 7adc4eba3d571ef523c0588be61179e27b519b4f0f5ec620e06b9253004468a5
3
+ metadata.gz: da837d4b00044d6028f0d62cfcf972c1da2a15121c182c7e6306e188e6b8c5b8
4
+ data.tar.gz: 7eabbcc8623e98f023f9c1d39c824719336ea83826047ec7afe59d97fafa595f
5
5
  SHA512:
6
- metadata.gz: 37c9de5b076d469c36f43a3cb55e608a053c146e97e9da63b5b4009772943adecff5f56ce6e416d105c52664c2616346a25cf6f2857c42b561ef84b5a95a007c
7
- data.tar.gz: d4e73cf548045b9fce1b6713aaeb0e0abaca29757534feaf484f62300ec2c80eed16a2a514d9388fa80fde9ada1d5321f97dad8904d4fc9864f81c6d0efe2a31
6
+ metadata.gz: 2d5167b38adf9a921a97e72e0304a6b7c2d3b1706c105ffaf48dcbaa8e66af42b21c42e36d6b1f78d2b765fc1796ebffcb3aeed2a224a6343c43bb077aefd9ba
7
+ data.tar.gz: 6219138ff5582bcf5433b37ee3cadc59946df54c4d9cf64bba71e878e8062ed31fe9703bb66ae92f7849ed87eb0d8dba0f9a38c41d109e1f35ee5d127c97c55d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.5.5] - 2026-05-15
4
+
5
+ ### Added
6
+ - `Legion::Apollo.ingest` and `Legion::Apollo::Local.ingest`/`upsert` now auto-inject `access_scope` (default `'global'`), `identity_canonical_name`, `identity_principal_id`, and `identity_id` from `Legion::Identity::Process` — zero call-site changes required; guarded by `defined?` so nodes without Identity loaded are unaffected
7
+ - Migration 006 adds the four identity/scope columns to `local_knowledge` with `access_scope` defaulting to `'global'` to match the Phase 1 Postgres schema
8
+
9
+ ### Fixed
10
+ - `query_merged` now forwards `requesting_principal_id` to `Apollo::Local.query` so the access-scope filter is honoured in merged results
11
+ - `Apollo::Local.query` accepts `requesting_principal_id:` and suppresses `private` entries whose `identity_principal_id` does not match the requesting principal (`nil` principal = system/background caller, all entries visible)
12
+
13
+ ## [0.5.4] - 2026-05-07
14
+
15
+ ### Fixed
16
+ - Preserve distinct merged query results when local or global Apollo entries arrive without a `content_hash`.
17
+ - Detect co-located Apollo reader and writer runners by module presence so in-process routing is not skipped.
18
+
3
19
  ## [0.5.3] - 2026-04-27
4
20
 
5
21
  ### Fixed
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ up do
5
+ alter_table(:local_knowledge) do
6
+ add_column :access_scope, String, null: false, default: 'global'
7
+ add_column :identity_canonical_name, String, null: true
8
+ add_column :identity_principal_id, Integer, null: true
9
+ add_column :identity_id, Integer, null: true
10
+
11
+ add_index :access_scope, name: :idx_local_knowledge_access_scope
12
+ add_index :identity_principal_id, name: :idx_local_knowledge_identity_principal_id
13
+ add_index :identity_id, name: :idx_local_knowledge_identity_id
14
+ end
15
+ end
16
+
17
+ down do
18
+ alter_table(:local_knowledge) do
19
+ drop_column :access_scope
20
+ drop_column :identity_canonical_name
21
+ drop_column :identity_principal_id
22
+ drop_column :identity_id
23
+ end
24
+ end
25
+ end
@@ -46,12 +46,13 @@ module Legion
46
46
  @started == true
47
47
  end
48
48
 
49
- def ingest(content:, tags: [], **opts) # rubocop:disable Metrics/MethodLength
49
+ def ingest(content:, tags: [], access_scope: 'global', **opts) # rubocop:disable Metrics/MethodLength
50
50
  return not_started_error unless started?
51
51
 
52
52
  tags = normalize_tags_input(tags)
53
53
  WRITE_MUTEX.synchronize do
54
- ingest_without_lock(content: content, tags: tags, **opts)
54
+ ingest_without_lock(content: content, tags: tags,
55
+ **inject_identity_context(opts).merge(access_scope: access_scope))
55
56
  end
56
57
  rescue StandardError => e
57
58
  handle_exception(
@@ -64,18 +65,19 @@ module Legion
64
65
  { success: false, error: e.message }
65
66
  end
66
67
 
67
- def upsert(content:, tags: [], **opts) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
68
+ def upsert(content:, tags: [], access_scope: 'global', **opts) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
68
69
  return not_started_error unless started?
69
70
 
70
71
  sorted_tags = normalize_tags_input(tags).sort
71
72
  tag_json = Legion::JSON.dump(sorted_tags)
73
+ merged_opts = inject_identity_context(opts).merge(access_scope: access_scope)
72
74
  WRITE_MUTEX.synchronize do
73
75
  existing = db[:local_knowledge].where(tags: tag_json).first
74
76
 
75
77
  if existing
76
- update_upsert_entry(existing, content, tag_json, opts)
78
+ update_upsert_entry(existing, content, tag_json, merged_opts)
77
79
  else
78
- result = ingest_without_lock(content: content, tags: sorted_tags, **opts)
80
+ result = ingest_without_lock(content: content, tags: sorted_tags, **merged_opts)
79
81
  result[:mode] = :inserted if result[:success] && result[:mode] != :deduplicated
80
82
  result
81
83
  end
@@ -91,7 +93,7 @@ module Legion
91
93
  { success: false, error: e.message }
92
94
  end
93
95
 
94
- def query(text:, limit: nil, min_confidence: nil, tags: nil, **opts) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity
96
+ def query(text:, limit: nil, min_confidence: nil, tags: nil, requesting_principal_id: nil, **opts) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/ParameterLists
95
97
  return not_started_error unless started?
96
98
 
97
99
  text = normalize_text_input(text)
@@ -111,7 +113,8 @@ module Legion
111
113
  include_history = opts.fetch(:include_history, false)
112
114
  candidates = filter_candidates(candidates, min_confidence: min_confidence, tags: tags,
113
115
  options: { include_inferences: include_inferences,
114
- include_history: include_history, as_of: as_of })
116
+ include_history: include_history, as_of: as_of,
117
+ requesting_principal_id: requesting_principal_id })
115
118
  candidates = cosine_rerank(text, candidates) if can_rerank?
116
119
  results = candidates.first(limit)
117
120
 
@@ -496,8 +499,13 @@ module Legion
496
499
  end
497
500
 
498
501
  def ingest_source_columns(opts)
499
- { source_channel: opts[:source_channel], source_agent: opts[:source_agent],
500
- submitted_by: opts[:submitted_by] }
502
+ { source_channel: opts[:source_channel],
503
+ source_agent: opts[:source_agent],
504
+ submitted_by: opts[:submitted_by],
505
+ access_scope: opts[:access_scope] || 'global',
506
+ identity_canonical_name: opts[:identity_canonical_name],
507
+ identity_principal_id: opts[:identity_principal_id],
508
+ identity_id: opts[:identity_id] }
501
509
  end
502
510
 
503
511
  def ingest_lineage_columns(opts)
@@ -653,6 +661,13 @@ module Legion
653
661
  tag_set.intersect?(entry_tags)
654
662
  end
655
663
  end
664
+ pid = options[:requesting_principal_id]
665
+ if pid
666
+ candidates = candidates.select do |c|
667
+ scope = c[:access_scope] || 'global'
668
+ scope != 'private' || c[:identity_principal_id] == pid
669
+ end
670
+ end
656
671
  candidates
657
672
  end
658
673
 
@@ -841,17 +856,21 @@ module Legion
841
856
 
842
857
  db.transaction do
843
858
  db[:local_knowledge].where(id: existing[:id]).update(
844
- content: content,
845
- content_hash: new_hash,
846
- tags: tags_json,
847
- embedding: embedding ? Legion::JSON.dump(embedding) : nil,
848
- embedded_at: embedded_at,
849
- confidence: opts.fetch(:confidence, existing[:confidence]),
850
- expires_at: expires_at,
851
- source_channel: opts.fetch(:source_channel, existing[:source_channel]),
852
- source_agent: opts.fetch(:source_agent, existing[:source_agent]),
853
- submitted_by: opts.fetch(:submitted_by, existing[:submitted_by]),
854
- updated_at: now
859
+ content: content,
860
+ content_hash: new_hash,
861
+ tags: tags_json,
862
+ embedding: embedding ? Legion::JSON.dump(embedding) : nil,
863
+ embedded_at: embedded_at,
864
+ confidence: opts.fetch(:confidence, existing[:confidence]),
865
+ expires_at: expires_at,
866
+ source_channel: opts.fetch(:source_channel, existing[:source_channel]),
867
+ source_agent: opts.fetch(:source_agent, existing[:source_agent]),
868
+ submitted_by: opts.fetch(:submitted_by, existing[:submitted_by]),
869
+ access_scope: opts.fetch(:access_scope, existing[:access_scope]) || 'global',
870
+ identity_canonical_name: opts.fetch(:identity_canonical_name, existing[:identity_canonical_name]),
871
+ identity_principal_id: opts.fetch(:identity_principal_id, existing[:identity_principal_id]),
872
+ identity_id: opts.fetch(:identity_id, existing[:identity_id]),
873
+ updated_at: now
855
874
  )
856
875
  rebuild_fts_entry!(existing[:id], content, tags_json)
857
876
  end
@@ -880,6 +899,20 @@ module Legion
880
899
  end
881
900
  end
882
901
 
902
+ def inject_identity_context(opts)
903
+ return opts unless defined?(Legion::Identity::Process)
904
+
905
+ id = Legion::Identity::Process.identity_hash
906
+ {
907
+ identity_canonical_name: id[:canonical_name],
908
+ identity_principal_id: id[:db_principal_id],
909
+ identity_id: id[:db_identity_id]
910
+ }.compact.merge(opts)
911
+ rescue StandardError => e
912
+ handle_exception(e, level: :warn, operation: 'apollo.local.inject_identity_context')
913
+ opts
914
+ end
915
+
883
916
  def not_started_error
884
917
  { success: false, error: :not_started }
885
918
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Legion
4
4
  module Apollo
5
- VERSION = '0.5.3'
5
+ VERSION = '0.5.5'
6
6
  end
7
7
  end
data/lib/legion/apollo.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'digest'
4
+ require 'legion/json'
4
5
  require 'legion/logging'
6
+ require 'legion/settings'
5
7
  require_relative 'apollo/version'
6
8
  require_relative 'apollo/settings'
7
9
  require_relative 'apollo/helpers/tag_normalizer'
@@ -93,13 +95,14 @@ module Legion
93
95
  end
94
96
  end
95
97
 
96
- def ingest(content:, tags: [], scope: :global, **opts) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
98
+ def ingest(content:, tags: [], scope: :global, access_scope: 'global', **opts) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
97
99
  return not_started_error unless started?
98
100
 
99
101
  normalized_tags = normalize_tags_input(tags)
100
102
  normalized_content = normalize_text_input(content)
101
103
  normalized_raw_content = normalize_raw_content_input(opts[:raw_content], fallback: normalized_content)
102
- payload = { **opts, content: normalized_content, raw_content: normalized_raw_content, tags: normalized_tags }
104
+ payload = { **inject_identity_context(opts), content: normalized_content,
105
+ raw_content: normalized_raw_content, tags: normalized_tags, access_scope: access_scope }
103
106
  log.info do
104
107
  "Apollo ingest requested scope=#{scope} content_length=#{payload[:content].to_s.length} " \
105
108
  "tags=#{payload[:tags].size} source_channel=#{payload[:source_channel]}"
@@ -224,8 +227,7 @@ module Legion
224
227
  def co_located_reader?
225
228
  return false unless data_available?
226
229
 
227
- defined?(Legion::Extensions::Apollo::Runners::Knowledge) &&
228
- Legion::Extensions::Apollo::Runners::Knowledge.respond_to?(:handle_query)
230
+ defined?(Legion::Extensions::Apollo::Runners::Knowledge) ? true : false
229
231
  rescue StandardError => e
230
232
  handle_exception(e, level: :debug, operation: 'apollo.co_located_reader')
231
233
  false
@@ -234,8 +236,7 @@ module Legion
234
236
  def co_located_writer?
235
237
  return false unless data_available?
236
238
 
237
- defined?(Legion::Extensions::Apollo::Runners::Knowledge) &&
238
- Legion::Extensions::Apollo::Runners::Knowledge.respond_to?(:handle_ingest)
239
+ defined?(Legion::Extensions::Apollo::Runners::Knowledge) ? true : false
239
240
  rescue StandardError => e
240
241
  handle_exception(e, level: :debug, operation: 'apollo.co_located_writer')
241
242
  false
@@ -292,7 +293,8 @@ module Legion
292
293
  end
293
294
  result = Legion::Apollo::Local.query(**payload.slice(:text, :limit, :min_confidence, :tags,
294
295
  :tier, :include_inferences, :include_history,
295
- :as_of))
296
+ :as_of),
297
+ requesting_principal_id: payload[:requesting_principal_id])
296
298
  return result unless result[:success]
297
299
 
298
300
  entries = normalize_local_entries(Array(result[:results]))
@@ -328,7 +330,8 @@ module Legion
328
330
  attempted = true
329
331
  local = Legion::Apollo::Local.query(**payload.slice(:text, :limit, :min_confidence, :tags,
330
332
  :tier, :include_inferences, :include_history,
331
- :as_of))
333
+ :as_of),
334
+ requesting_principal_id: payload[:requesting_principal_id])
332
335
  if local[:success]
333
336
  any_success = true
334
337
  entries.concat(normalize_local_entries(Array(local[:results]))) if local[:results]
@@ -407,7 +410,13 @@ module Legion
407
410
  end
408
411
 
409
412
  def dedup_and_rank(entries, limit:)
410
- sorted = entries
413
+ entries_with_hashes = entries.map do |e|
414
+ next e if e[:content_hash]
415
+
416
+ e.merge(content_hash: Digest::MD5.hexdigest(e[:content].to_s.strip.downcase.gsub(/\s+/, ' ')))
417
+ end
418
+
419
+ sorted = entries_with_hashes
411
420
  .sort_by { |e| -(e[:confidence] || 0) }
412
421
  .uniq { |e| e[:content_hash] }
413
422
 
@@ -554,6 +563,20 @@ module Legion
554
563
  end
555
564
  end
556
565
 
566
+ def inject_identity_context(opts)
567
+ return opts unless defined?(Legion::Identity::Process)
568
+
569
+ id = Legion::Identity::Process.identity_hash
570
+ {
571
+ identity_canonical_name: id[:canonical_name],
572
+ identity_principal_id: id[:db_principal_id],
573
+ identity_id: id[:db_identity_id]
574
+ }.compact.merge(opts)
575
+ rescue StandardError => e
576
+ handle_exception(e, level: :warn, operation: 'apollo.inject_identity_context')
577
+ opts
578
+ end
579
+
557
580
  def not_started_error
558
581
  { success: false, error: :not_started }
559
582
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: legion-apollo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.3
4
+ version: 0.5.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -88,6 +88,7 @@ files:
88
88
  - lib/legion/apollo/local/migrations/003_harden_graph_relationships.rb
89
89
  - lib/legion/apollo/local/migrations/004_add_versioning_tiers_inference.rb
90
90
  - lib/legion/apollo/local/migrations/005_add_raw_content_temporal_windows.rb
91
+ - lib/legion/apollo/local/migrations/006_add_identity_and_access_scope.rb
91
92
  - lib/legion/apollo/messages/access_boost.rb
92
93
  - lib/legion/apollo/messages/ingest.rb
93
94
  - lib/legion/apollo/messages/query.rb