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 +4 -4
- data/CHANGELOG.md +10 -0
- data/lib/legion/apollo/local/migrations/006_add_identity_and_access_scope.rb +25 -0
- data/lib/legion/apollo/local.rb +53 -20
- data/lib/legion/apollo/version.rb +1 -1
- data/lib/legion/apollo.rb +37 -4
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7150a2def38a098c16a2f35362210994cc77537ca6ee3e0e615823cf8e1797c2
|
|
4
|
+
data.tar.gz: 6e0bfa593f70915134195672dbe321f03c00c86e1f80fee06f5370f3c6578110
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/legion/apollo/local.rb
CHANGED
|
@@ -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,
|
|
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,
|
|
78
|
+
update_upsert_entry(existing, content, tag_json, merged_opts)
|
|
77
79
|
else
|
|
78
|
-
result = ingest_without_lock(content: content, tags: sorted_tags, **
|
|
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:
|
|
500
|
-
|
|
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:
|
|
845
|
-
content_hash:
|
|
846
|
-
tags:
|
|
847
|
-
embedding:
|
|
848
|
-
embedded_at:
|
|
849
|
-
confidence:
|
|
850
|
-
expires_at:
|
|
851
|
-
source_channel:
|
|
852
|
-
source_agent:
|
|
853
|
-
submitted_by:
|
|
854
|
-
|
|
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
|
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,
|
|
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
|
+
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
|