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 +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 +23 -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: da837d4b00044d6028f0d62cfcf972c1da2a15121c182c7e6306e188e6b8c5b8
|
|
4
|
+
data.tar.gz: 7eabbcc8623e98f023f9c1d39c824719336ea83826047ec7afe59d97fafa595f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
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'
|
|
@@ -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,
|
|
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
|
+
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
|