legion-apollo 0.5.4 → 0.5.6

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: 95c943936a3a2ac751b688b26c676197142ae6c2aa48aa3427a3dc0709cd86bd
4
- data.tar.gz: d76f10cc2a7a4984f419489a1df52de501de6878f218f7374575e7b8893e0ad6
3
+ metadata.gz: 7150a2def38a098c16a2f35362210994cc77537ca6ee3e0e615823cf8e1797c2
4
+ data.tar.gz: 6e0bfa593f70915134195672dbe321f03c00c86e1f80fee06f5370f3c6578110
5
5
  SHA512:
6
- metadata.gz: 586271450e0c15b6b493f702b23feeec27020ab37a34ed790246e13215f501b771da84c1c49b475da148aa448791f9eb247cf1722c664ba55ba4b3082c454fea
7
- data.tar.gz: be2de9a03d9292ce01293363285ba83bfa0eeb45de922c36fcad298576d9a5ea0c3c6c24ed519c98c4f9707e1b645516fab0ff7ca1630fa061ee294b94747b15
6
+ metadata.gz: 97783044f23209d697578c0f56d570bc48b9531e5d569f23e4db98bf55cb5d0c4602d6a0fedeb8e7c04a89618d4586c25bc7ca89f1a8d922e8618c5b724cc13e
7
+ data.tar.gz: 8498fae869abc9f54520b216ed88e2aea006ee6e520d8421d44cf5781c76ae9c158553e3c34a7e9e1e37d44594248a5c6daae1c7b55336c9692e7b668f00fbb4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
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
+
3
13
  ## [0.5.4] - 2026-05-07
4
14
 
5
15
  ### 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.4'
5
+ VERSION = '0.5.6'
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'
@@ -72,6 +74,7 @@ module Legion
72
74
  normalized_tags = normalize_tags_input(tags)
73
75
  limit ||= apollo_setting(:default_limit, 5)
74
76
  min_confidence ||= apollo_setting(:min_confidence, 0.3)
77
+ opts = inject_requesting_principal_id(opts)
75
78
  log.info { "Apollo query requested scope=#{scope} text_length=#{text.to_s.length} limit=#{limit}" }
76
79
  log.debug do
77
80
  "Apollo query scope=#{scope} limit=#{limit} min_confidence=#{min_confidence} tags=#{normalized_tags.size}"
@@ -93,13 +96,14 @@ module Legion
93
96
  end
94
97
  end
95
98
 
96
- def ingest(content:, tags: [], scope: :global, **opts) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
99
+ def ingest(content:, tags: [], scope: :global, access_scope: 'global', **opts) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
97
100
  return not_started_error unless started?
98
101
 
99
102
  normalized_tags = normalize_tags_input(tags)
100
103
  normalized_content = normalize_text_input(content)
101
104
  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 }
105
+ payload = { **inject_identity_context(opts), content: normalized_content,
106
+ raw_content: normalized_raw_content, tags: normalized_tags, access_scope: access_scope }
103
107
  log.info do
104
108
  "Apollo ingest requested scope=#{scope} content_length=#{payload[:content].to_s.length} " \
105
109
  "tags=#{payload[:tags].size} source_channel=#{payload[:source_channel]}"
@@ -290,7 +294,8 @@ module Legion
290
294
  end
291
295
  result = Legion::Apollo::Local.query(**payload.slice(:text, :limit, :min_confidence, :tags,
292
296
  :tier, :include_inferences, :include_history,
293
- :as_of))
297
+ :as_of),
298
+ requesting_principal_id: payload[:requesting_principal_id])
294
299
  return result unless result[:success]
295
300
 
296
301
  entries = normalize_local_entries(Array(result[:results]))
@@ -326,7 +331,8 @@ module Legion
326
331
  attempted = true
327
332
  local = Legion::Apollo::Local.query(**payload.slice(:text, :limit, :min_confidence, :tags,
328
333
  :tier, :include_inferences, :include_history,
329
- :as_of))
334
+ :as_of),
335
+ requesting_principal_id: payload[:requesting_principal_id])
330
336
  if local[:success]
331
337
  any_success = true
332
338
  entries.concat(normalize_local_entries(Array(local[:results]))) if local[:results]
@@ -558,6 +564,33 @@ module Legion
558
564
  end
559
565
  end
560
566
 
567
+ def inject_identity_context(opts)
568
+ return opts unless defined?(Legion::Identity::Process)
569
+
570
+ id = Legion::Identity::Process.identity_hash
571
+ {
572
+ identity_canonical_name: id[:canonical_name],
573
+ identity_principal_id: id[:db_principal_id],
574
+ identity_id: id[:db_identity_id]
575
+ }.compact.merge(opts)
576
+ rescue StandardError => e
577
+ handle_exception(e, level: :warn, operation: 'apollo.inject_identity_context')
578
+ opts
579
+ end
580
+
581
+ def inject_requesting_principal_id(opts)
582
+ return opts if opts.key?(:requesting_principal_id)
583
+ return opts unless defined?(Legion::Identity::Process)
584
+
585
+ principal_id = Legion::Identity::Process.db_principal_id
586
+ return opts if principal_id.nil?
587
+
588
+ opts.merge(requesting_principal_id: principal_id)
589
+ rescue StandardError => e
590
+ handle_exception(e, level: :warn, operation: 'apollo.inject_requesting_principal_id')
591
+ opts
592
+ end
593
+
561
594
  def not_started_error
562
595
  { success: false, error: :not_started }
563
596
  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.4
4
+ version: 0.5.6
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