legion-apollo 0.5.4 → 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: 95c943936a3a2ac751b688b26c676197142ae6c2aa48aa3427a3dc0709cd86bd
4
- data.tar.gz: d76f10cc2a7a4984f419489a1df52de501de6878f218f7374575e7b8893e0ad6
3
+ metadata.gz: da837d4b00044d6028f0d62cfcf972c1da2a15121c182c7e6306e188e6b8c5b8
4
+ data.tar.gz: 7eabbcc8623e98f023f9c1d39c824719336ea83826047ec7afe59d97fafa595f
5
5
  SHA512:
6
- metadata.gz: 586271450e0c15b6b493f702b23feeec27020ab37a34ed790246e13215f501b771da84c1c49b475da148aa448791f9eb247cf1722c664ba55ba4b3082c454fea
7
- data.tar.gz: be2de9a03d9292ce01293363285ba83bfa0eeb45de922c36fcad298576d9a5ea0c3c6c24ed519c98c4f9707e1b645516fab0ff7ca1630fa061ee294b94747b15
6
+ metadata.gz: 2d5167b38adf9a921a97e72e0304a6b7c2d3b1706c105ffaf48dcbaa8e66af42b21c42e36d6b1f78d2b765fc1796ebffcb3aeed2a224a6343c43bb077aefd9ba
7
+ data.tar.gz: 6219138ff5582bcf5433b37ee3cadc59946df54c4d9cf64bba71e878e8062ed31fe9703bb66ae92f7849ed87eb0d8dba0f9a38c41d109e1f35ee5d127c97c55d
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.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]}"
@@ -290,7 +293,8 @@ module Legion
290
293
  end
291
294
  result = Legion::Apollo::Local.query(**payload.slice(:text, :limit, :min_confidence, :tags,
292
295
  :tier, :include_inferences, :include_history,
293
- :as_of))
296
+ :as_of),
297
+ requesting_principal_id: payload[:requesting_principal_id])
294
298
  return result unless result[:success]
295
299
 
296
300
  entries = normalize_local_entries(Array(result[:results]))
@@ -326,7 +330,8 @@ module Legion
326
330
  attempted = true
327
331
  local = Legion::Apollo::Local.query(**payload.slice(:text, :limit, :min_confidence, :tags,
328
332
  :tier, :include_inferences, :include_history,
329
- :as_of))
333
+ :as_of),
334
+ requesting_principal_id: payload[:requesting_principal_id])
330
335
  if local[:success]
331
336
  any_success = true
332
337
  entries.concat(normalize_local_entries(Array(local[:results]))) if local[:results]
@@ -558,6 +563,20 @@ module Legion
558
563
  end
559
564
  end
560
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
+
561
580
  def not_started_error
562
581
  { success: false, error: :not_started }
563
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.4
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