textus 0.30.0 → 0.35.1
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/ARCHITECTURE.md +2 -241
- data/CHANGELOG.md +113 -0
- data/README.md +83 -62
- data/SPEC.md +352 -211
- data/docs/conventions.md +42 -37
- data/lib/textus/boot.rb +89 -74
- data/lib/textus/cli/group/{refresh.rb → fetch.rb} +4 -4
- data/lib/textus/cli/verb/build.rb +1 -1
- data/lib/textus/cli/verb/fetch.rb +14 -0
- data/lib/textus/cli/verb/{refresh_stale.rb → fetch_stale.rb} +3 -3
- data/lib/textus/cli/verb/get.rb +1 -1
- data/lib/textus/cli/verb/hooks.rb +1 -1
- data/lib/textus/cli/verb/put.rb +1 -1
- data/lib/textus/cli/verb/rule_list.rb +7 -7
- data/lib/textus/cli.rb +2 -2
- data/lib/textus/container.rb +1 -2
- data/lib/textus/dispatcher.rb +3 -3
- data/lib/textus/doctor/check/{refresh_locks.rb → fetch_locks.rb} +7 -7
- data/lib/textus/doctor/check/proposal_targets.rb +45 -0
- data/lib/textus/doctor/check/rule_ambiguity.rb +3 -3
- data/lib/textus/doctor.rb +2 -1
- data/lib/textus/domain/action.rb +3 -3
- data/lib/textus/domain/freshness/evaluator.rb +3 -3
- data/lib/textus/domain/freshness/policy.rb +2 -2
- data/lib/textus/domain/freshness.rb +7 -7
- data/lib/textus/domain/outcome.rb +2 -2
- data/lib/textus/domain/permission.rb +2 -10
- data/lib/textus/domain/policy/base_guards.rb +25 -0
- data/lib/textus/domain/policy/evaluation.rb +18 -0
- data/lib/textus/domain/policy/{refresh.rb → fetch.rb} +1 -1
- data/lib/textus/domain/policy/guard.rb +35 -0
- data/lib/textus/domain/policy/guard_factory.rb +40 -0
- data/lib/textus/domain/policy/predicates/author_held.rb +33 -0
- data/lib/textus/domain/policy/predicates/etag_match.rb +32 -0
- data/lib/textus/domain/policy/predicates/fresh_within.rb +58 -0
- data/lib/textus/domain/policy/predicates/registry.rb +39 -0
- data/lib/textus/domain/policy/predicates/schema_valid.rb +30 -19
- data/lib/textus/domain/policy/predicates/target_is_canon.rb +33 -0
- data/lib/textus/domain/policy/predicates/zone_writable_by.rb +39 -0
- data/lib/textus/domain/staleness/intake_check.rb +6 -6
- data/lib/textus/envelope.rb +2 -2
- data/lib/textus/errors.rb +25 -28
- data/lib/textus/hooks/event_bus.rb +4 -4
- data/lib/textus/init.rb +23 -18
- data/lib/textus/maintenance/zone_mv.rb +1 -1
- data/lib/textus/manifest/capabilities.rb +29 -0
- data/lib/textus/manifest/data.rb +14 -10
- data/lib/textus/manifest/policy.rb +37 -21
- data/lib/textus/manifest/rules.rb +16 -14
- data/lib/textus/manifest/schema.rb +48 -58
- data/lib/textus/manifest.rb +3 -3
- data/lib/textus/mcp/server.rb +1 -1
- data/lib/textus/mcp/tool_schemas.rb +3 -3
- data/lib/textus/mcp/tools.rb +7 -7
- data/lib/textus/ports/audit_subscriber.rb +1 -1
- data/lib/textus/ports/{refresh → fetch}/detached.rb +4 -4
- data/lib/textus/ports/{refresh → fetch}/lock.rb +1 -1
- data/lib/textus/projection.rb +1 -1
- data/lib/textus/read/freshness.rb +9 -9
- data/lib/textus/read/get.rb +8 -8
- data/lib/textus/read/{get_or_refresh.rb → get_or_fetch.rb} +11 -11
- data/lib/textus/read/policy_explain.rb +14 -10
- data/lib/textus/read/pulse.rb +5 -4
- data/lib/textus/read/validator.rb +1 -1
- data/lib/textus/schema/tools.rb +5 -5
- data/lib/textus/version.rb +1 -1
- data/lib/textus/write/accept.rb +19 -55
- data/lib/textus/write/delete.rb +14 -2
- data/lib/textus/write/{refresh_all.rb → fetch_all.rb} +6 -6
- data/lib/textus/write/{refresh_orchestrator.rb → fetch_orchestrator.rb} +14 -14
- data/lib/textus/write/{refresh_worker.rb → fetch_worker.rb} +21 -14
- data/lib/textus/write/mv.rb +15 -3
- data/lib/textus/write/put.rb +14 -2
- data/lib/textus/write/reject.rb +11 -5
- metadata +24 -18
- data/lib/textus/cli/verb/refresh.rb +0 -14
- data/lib/textus/domain/authorizer.rb +0 -37
- data/lib/textus/domain/policy/predicates/accept_authority_signed.rb +0 -33
- data/lib/textus/domain/policy/promote.rb +0 -26
- data/lib/textus/domain/policy/promotion.rb +0 -57
- data/lib/textus/manifest/role_kinds.rb +0 -21
- data/lib/textus/write/authority_gate.rb +0 -24
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
module Textus
|
|
2
2
|
module Write
|
|
3
|
-
class
|
|
4
|
-
# Collaborator (not a Dispatcher verb): constructed directly by
|
|
5
|
-
#
|
|
3
|
+
class FetchOrchestrator
|
|
4
|
+
# Collaborator (not a Dispatcher verb): constructed directly by FetchWorker /
|
|
5
|
+
# GetOrFetch, which pass their derived hook_context in. That's why this takes
|
|
6
6
|
# hook_context: explicitly while verb use cases derive their own.
|
|
7
7
|
def initialize(worker:, store_root:, events:, hook_context: nil, detached_spawner: nil)
|
|
8
8
|
@worker = worker
|
|
@@ -14,9 +14,9 @@ module Textus
|
|
|
14
14
|
|
|
15
15
|
def execute(action, key:)
|
|
16
16
|
case action
|
|
17
|
-
when Textus::Domain::Action::Return
|
|
18
|
-
when Textus::Domain::Action::
|
|
19
|
-
when Textus::Domain::Action::
|
|
17
|
+
when Textus::Domain::Action::Return then Textus::Domain::Outcome::Skipped.new
|
|
18
|
+
when Textus::Domain::Action::FetchSync then run_sync(key)
|
|
19
|
+
when Textus::Domain::Action::FetchTimed then run_timed(action.budget_ms, key)
|
|
20
20
|
else raise ArgumentError.new("unknown action: #{action.inspect}")
|
|
21
21
|
end
|
|
22
22
|
end
|
|
@@ -25,13 +25,13 @@ module Textus
|
|
|
25
25
|
|
|
26
26
|
def run_sync(key)
|
|
27
27
|
envelope = @worker.run(key)
|
|
28
|
-
Textus::Domain::Outcome::
|
|
28
|
+
Textus::Domain::Outcome::Fetched.new(envelope: envelope)
|
|
29
29
|
rescue Textus::Error => e
|
|
30
30
|
Textus::Domain::Outcome::Failed.new(error: e)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
33
|
def run_timed(budget_ms, key)
|
|
34
|
-
return run_timed_with_fork(budget_ms, key) if Textus::Ports::
|
|
34
|
+
return run_timed_with_fork(budget_ms, key) if Textus::Ports::Fetch::Detached.supported?
|
|
35
35
|
|
|
36
36
|
run_timed_cooperative(budget_ms, key)
|
|
37
37
|
end
|
|
@@ -49,7 +49,7 @@ module Textus
|
|
|
49
49
|
thread.kill
|
|
50
50
|
return Textus::Domain::Outcome::Failed.new(
|
|
51
51
|
error: Textus::UsageError.new(
|
|
52
|
-
"
|
|
52
|
+
"fetch exceeded budget #{budget_ms}ms (no fork available — cooperative cancel)",
|
|
53
53
|
),
|
|
54
54
|
)
|
|
55
55
|
end
|
|
@@ -57,7 +57,7 @@ module Textus
|
|
|
57
57
|
if result.is_a?(Textus::Error)
|
|
58
58
|
Textus::Domain::Outcome::Failed.new(error: result)
|
|
59
59
|
else
|
|
60
|
-
Textus::Domain::Outcome::
|
|
60
|
+
Textus::Domain::Outcome::Fetched.new(envelope: result)
|
|
61
61
|
end
|
|
62
62
|
end
|
|
63
63
|
|
|
@@ -77,25 +77,25 @@ module Textus
|
|
|
77
77
|
# Single-flight: if a sibling process / earlier fork holds the
|
|
78
78
|
# per-leaf lock, don't fork another worker — they're already
|
|
79
79
|
# doing this work.
|
|
80
|
-
probe = Textus::Ports::
|
|
80
|
+
probe = Textus::Ports::Fetch::Lock.new(root: @store_root, key: key)
|
|
81
81
|
return Textus::Domain::Outcome::Detached.new unless probe.try_acquire
|
|
82
82
|
|
|
83
83
|
probe.release
|
|
84
84
|
|
|
85
85
|
payload = { key: key, started_at: Time.now.utc.iso8601, budget_ms: budget_ms }
|
|
86
86
|
payload[:ctx] = @hook_context if @hook_context
|
|
87
|
-
@events.publish(:
|
|
87
|
+
@events.publish(:fetch_backgrounded, **payload)
|
|
88
88
|
@detached_spawner.call(store_root: @store_root, key: key)
|
|
89
89
|
Textus::Domain::Outcome::Detached.new
|
|
90
90
|
elsif result.is_a?(Textus::Error)
|
|
91
91
|
Textus::Domain::Outcome::Failed.new(error: result)
|
|
92
92
|
else
|
|
93
|
-
Textus::Domain::Outcome::
|
|
93
|
+
Textus::Domain::Outcome::Fetched.new(envelope: result)
|
|
94
94
|
end
|
|
95
95
|
end
|
|
96
96
|
|
|
97
97
|
def default_spawner
|
|
98
|
-
Textus::Ports::
|
|
98
|
+
Textus::Ports::Fetch::Detached.method(:spawn)
|
|
99
99
|
end
|
|
100
100
|
end
|
|
101
101
|
end
|
|
@@ -2,20 +2,20 @@ require "timeout"
|
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
4
|
module Write
|
|
5
|
-
class
|
|
5
|
+
class FetchWorker
|
|
6
6
|
FETCH_TIMEOUT_SECONDS = IntakeFetch::FETCH_TIMEOUT_SECONDS
|
|
7
7
|
|
|
8
8
|
def initialize(container:, call:)
|
|
9
9
|
@container = container
|
|
10
10
|
@call = call
|
|
11
11
|
@manifest = container.manifest
|
|
12
|
+
@schemas = container.schemas
|
|
12
13
|
@events = container.events
|
|
13
14
|
@rpc = container.rpc
|
|
14
|
-
@authorizer = container.authorizer
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
# call(key) is the primary entry; run is kept as an alias for
|
|
18
|
-
# Orchestrator and
|
|
18
|
+
# Orchestrator and FetchAll which call worker.run(key).
|
|
19
19
|
def call(key)
|
|
20
20
|
run(key)
|
|
21
21
|
end
|
|
@@ -63,11 +63,11 @@ module Textus
|
|
|
63
63
|
|
|
64
64
|
def fetch_timeout_for(key)
|
|
65
65
|
rule = @manifest.rules.for(key)
|
|
66
|
-
rule&.
|
|
66
|
+
rule&.fetch&.fetch_timeout_seconds || FETCH_TIMEOUT_SECONDS
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
def fetch_with_events(key, mentry, remaining)
|
|
70
|
-
@events.publish(:
|
|
70
|
+
@events.publish(:fetch_started, ctx: hook_context, key: key, mode: :sync)
|
|
71
71
|
call_intake(key, mentry, remaining)
|
|
72
72
|
end
|
|
73
73
|
|
|
@@ -80,23 +80,30 @@ module Textus
|
|
|
80
80
|
args: { trigger_key: key, leaf_segments: remaining || [] })
|
|
81
81
|
end
|
|
82
82
|
rescue Timeout::Error
|
|
83
|
-
@events.publish(:
|
|
84
|
-
|
|
85
|
-
|
|
83
|
+
@events.publish(:fetch_failed, ctx: hook_context, key: key,
|
|
84
|
+
error_class: "Timeout::Error",
|
|
85
|
+
error_message: "intake '#{mentry.handler}' exceeded #{timeout}s")
|
|
86
86
|
raise UsageError.new("intake '#{mentry.handler}' exceeded #{timeout}s timeout")
|
|
87
87
|
rescue Textus::Error => e
|
|
88
|
-
@events.publish(:
|
|
89
|
-
|
|
88
|
+
@events.publish(:fetch_failed, ctx: hook_context, key: key, error_class: e.class.name,
|
|
89
|
+
error_message: e.message)
|
|
90
90
|
raise
|
|
91
91
|
rescue StandardError => e
|
|
92
|
-
@events.publish(:
|
|
93
|
-
|
|
92
|
+
@events.publish(:fetch_failed, ctx: hook_context, key: key, error_class: e.class.name,
|
|
93
|
+
error_message: e.message)
|
|
94
94
|
raise UsageError.new("intake '#{mentry.handler}' raised: #{e.class}: #{e.message}")
|
|
95
95
|
end
|
|
96
96
|
|
|
97
97
|
def persist_and_notify(key, mentry, result, before_etag)
|
|
98
98
|
normalized = self.class.normalize_action_result(result, format: mentry.format)
|
|
99
|
-
|
|
99
|
+
Textus::Domain::Policy::GuardFactory.new(
|
|
100
|
+
manifest: @manifest, schemas: @schemas,
|
|
101
|
+
).for(:fetch, key).check!(
|
|
102
|
+
Textus::Domain::Policy::Evaluation.new(
|
|
103
|
+
actor: @call.role, transition: :fetch, origin: nil,
|
|
104
|
+
target: key, envelope: nil, snapshot: @manifest
|
|
105
|
+
),
|
|
106
|
+
)
|
|
100
107
|
envelope = writer.put(
|
|
101
108
|
key,
|
|
102
109
|
mentry: mentry,
|
|
@@ -105,7 +112,7 @@ module Textus
|
|
|
105
112
|
),
|
|
106
113
|
)
|
|
107
114
|
change = detect_change(before_etag, envelope)
|
|
108
|
-
@events.publish(:
|
|
115
|
+
@events.publish(:entry_fetched, ctx: hook_context, key: key, envelope: envelope, change: change) unless change == :unchanged
|
|
109
116
|
envelope
|
|
110
117
|
end
|
|
111
118
|
|
data/lib/textus/write/mv.rb
CHANGED
|
@@ -6,7 +6,6 @@ module Textus
|
|
|
6
6
|
@call = call
|
|
7
7
|
@manifest = container.manifest
|
|
8
8
|
@events = container.events
|
|
9
|
-
@authorizer = container.authorizer
|
|
10
9
|
end
|
|
11
10
|
|
|
12
11
|
def call(old_key, new_key, dry_run: false)
|
|
@@ -38,8 +37,8 @@ module Textus
|
|
|
38
37
|
raise UnknownKey.new(old_key) unless reader.exists?(old_key)
|
|
39
38
|
|
|
40
39
|
validate_zone_and_format!(old_res.entry, new_res.entry)
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
guard_for(:mv, old_key).check!(eval_for(:mv, target_key: old_key))
|
|
41
|
+
guard_for(:mv, new_key).check!(eval_for(:mv, target_key: new_key))
|
|
43
42
|
raise UsageError.new("mv: target '#{new_key}' already exists at #{new_res.path}") if reader.exists?(new_key)
|
|
44
43
|
|
|
45
44
|
[old_res, new_res]
|
|
@@ -101,6 +100,19 @@ module Textus
|
|
|
101
100
|
}
|
|
102
101
|
end
|
|
103
102
|
|
|
103
|
+
def guard_for(transition, key, if_etag: nil)
|
|
104
|
+
Textus::Domain::Policy::GuardFactory.new(
|
|
105
|
+
manifest: @manifest, schemas: @container.schemas, extra: { if_etag: if_etag },
|
|
106
|
+
).for(transition, key)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def eval_for(transition, target_key:, envelope: nil)
|
|
110
|
+
Textus::Domain::Policy::Evaluation.new(
|
|
111
|
+
actor: @call.role, transition: transition, origin: nil,
|
|
112
|
+
target: target_key, envelope: envelope, snapshot: @manifest
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
104
116
|
def writer
|
|
105
117
|
@writer ||= Textus::Envelope::IO::Writer.from(container: @container, call: @call)
|
|
106
118
|
end
|
data/lib/textus/write/put.rb
CHANGED
|
@@ -5,14 +5,13 @@ module Textus
|
|
|
5
5
|
@container = container
|
|
6
6
|
@call = call
|
|
7
7
|
@manifest = container.manifest
|
|
8
|
-
@authorizer = container.authorizer
|
|
9
8
|
@events = container.events
|
|
10
9
|
end
|
|
11
10
|
|
|
12
11
|
def call(key, meta: nil, body: nil, content: nil, if_etag: nil)
|
|
13
12
|
Textus::Manifest::Data.validate_key!(key)
|
|
14
13
|
mentry = @manifest.resolver.resolve(key).entry
|
|
15
|
-
|
|
14
|
+
guard_for(:put, key, if_etag: if_etag).check!(eval_for(:put, target_key: key))
|
|
16
15
|
|
|
17
16
|
envelope = writer.put(
|
|
18
17
|
key,
|
|
@@ -33,6 +32,19 @@ module Textus
|
|
|
33
32
|
|
|
34
33
|
private
|
|
35
34
|
|
|
35
|
+
def guard_for(transition, key, if_etag: nil)
|
|
36
|
+
Textus::Domain::Policy::GuardFactory.new(
|
|
37
|
+
manifest: @manifest, schemas: @container.schemas, extra: { if_etag: if_etag },
|
|
38
|
+
).for(transition, key)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def eval_for(transition, target_key:, envelope: nil)
|
|
42
|
+
Textus::Domain::Policy::Evaluation.new(
|
|
43
|
+
actor: @call.role, transition: transition, origin: nil,
|
|
44
|
+
target: target_key, envelope: envelope, snapshot: @manifest
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
36
48
|
def hook_context
|
|
37
49
|
@hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
|
|
38
50
|
end
|
data/lib/textus/write/reject.rb
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
|
-
require_relative "authority_gate"
|
|
2
|
-
|
|
3
1
|
module Textus
|
|
4
2
|
module Write
|
|
5
3
|
class Reject
|
|
6
|
-
include AuthorityGate
|
|
7
|
-
|
|
8
4
|
def initialize(container:, call:)
|
|
9
5
|
@container = container
|
|
10
6
|
@call = call
|
|
11
7
|
@manifest = container.manifest
|
|
8
|
+
@schemas = container.schemas
|
|
12
9
|
@events = container.events
|
|
13
10
|
end
|
|
14
11
|
|
|
15
12
|
def call(pending_key)
|
|
16
|
-
|
|
13
|
+
guard.for(:reject, pending_key).check!(
|
|
14
|
+
Textus::Domain::Policy::Evaluation.new(
|
|
15
|
+
actor: @call.role, transition: :reject, origin: pending_key,
|
|
16
|
+
target: pending_key, envelope: nil, snapshot: @manifest
|
|
17
|
+
),
|
|
18
|
+
)
|
|
17
19
|
|
|
18
20
|
mentry = @manifest.resolver.resolve(pending_key).entry
|
|
19
21
|
unless mentry.in_proposal_zone?(@manifest.policy)
|
|
@@ -40,6 +42,10 @@ module Textus
|
|
|
40
42
|
|
|
41
43
|
private
|
|
42
44
|
|
|
45
|
+
def guard
|
|
46
|
+
@guard ||= Textus::Domain::Policy::GuardFactory.new(manifest: @manifest, schemas: @schemas)
|
|
47
|
+
end
|
|
48
|
+
|
|
43
49
|
def hook_context
|
|
44
50
|
@hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
|
|
45
51
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: textus
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.35.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patrick
|
|
@@ -119,10 +119,10 @@ files:
|
|
|
119
119
|
- lib/textus/call.rb
|
|
120
120
|
- lib/textus/cli.rb
|
|
121
121
|
- lib/textus/cli/group.rb
|
|
122
|
+
- lib/textus/cli/group/fetch.rb
|
|
122
123
|
- lib/textus/cli/group/hook.rb
|
|
123
124
|
- lib/textus/cli/group/key.rb
|
|
124
125
|
- lib/textus/cli/group/mcp.rb
|
|
125
|
-
- lib/textus/cli/group/refresh.rb
|
|
126
126
|
- lib/textus/cli/group/rule.rb
|
|
127
127
|
- lib/textus/cli/group/schema.rb
|
|
128
128
|
- lib/textus/cli/group/zone.rb
|
|
@@ -135,6 +135,8 @@ files:
|
|
|
135
135
|
- lib/textus/cli/verb/delete.rb
|
|
136
136
|
- lib/textus/cli/verb/deps.rb
|
|
137
137
|
- lib/textus/cli/verb/doctor.rb
|
|
138
|
+
- lib/textus/cli/verb/fetch.rb
|
|
139
|
+
- lib/textus/cli/verb/fetch_stale.rb
|
|
138
140
|
- lib/textus/cli/verb/freshness.rb
|
|
139
141
|
- lib/textus/cli/verb/get.rb
|
|
140
142
|
- lib/textus/cli/verb/hook_run.rb
|
|
@@ -149,8 +151,6 @@ files:
|
|
|
149
151
|
- lib/textus/cli/verb/pulse.rb
|
|
150
152
|
- lib/textus/cli/verb/put.rb
|
|
151
153
|
- lib/textus/cli/verb/rdeps.rb
|
|
152
|
-
- lib/textus/cli/verb/refresh.rb
|
|
153
|
-
- lib/textus/cli/verb/refresh_stale.rb
|
|
154
154
|
- lib/textus/cli/verb/reject.rb
|
|
155
155
|
- lib/textus/cli/verb/retain.rb
|
|
156
156
|
- lib/textus/cli/verb/rule_explain.rb
|
|
@@ -168,13 +168,14 @@ files:
|
|
|
168
168
|
- lib/textus/doctor.rb
|
|
169
169
|
- lib/textus/doctor/check.rb
|
|
170
170
|
- lib/textus/doctor/check/audit_log.rb
|
|
171
|
+
- lib/textus/doctor/check/fetch_locks.rb
|
|
171
172
|
- lib/textus/doctor/check/handler_allowlist.rb
|
|
172
173
|
- lib/textus/doctor/check/hooks.rb
|
|
173
174
|
- lib/textus/doctor/check/illegal_keys.rb
|
|
174
175
|
- lib/textus/doctor/check/intake_registration.rb
|
|
175
176
|
- lib/textus/doctor/check/manifest_files.rb
|
|
177
|
+
- lib/textus/doctor/check/proposal_targets.rb
|
|
176
178
|
- lib/textus/doctor/check/protocol_version.rb
|
|
177
|
-
- lib/textus/doctor/check/refresh_locks.rb
|
|
178
179
|
- lib/textus/doctor/check/rule_ambiguity.rb
|
|
179
180
|
- lib/textus/doctor/check/schema_parse_error.rb
|
|
180
181
|
- lib/textus/doctor/check/schema_violations.rb
|
|
@@ -183,7 +184,6 @@ files:
|
|
|
183
184
|
- lib/textus/doctor/check/templates.rb
|
|
184
185
|
- lib/textus/doctor/check/unowned_schema_fields.rb
|
|
185
186
|
- lib/textus/domain/action.rb
|
|
186
|
-
- lib/textus/domain/authorizer.rb
|
|
187
187
|
- lib/textus/domain/duration.rb
|
|
188
188
|
- lib/textus/domain/freshness.rb
|
|
189
189
|
- lib/textus/domain/freshness/evaluator.rb
|
|
@@ -191,13 +191,20 @@ files:
|
|
|
191
191
|
- lib/textus/domain/freshness/verdict.rb
|
|
192
192
|
- lib/textus/domain/outcome.rb
|
|
193
193
|
- lib/textus/domain/permission.rb
|
|
194
|
+
- lib/textus/domain/policy/base_guards.rb
|
|
195
|
+
- lib/textus/domain/policy/evaluation.rb
|
|
196
|
+
- lib/textus/domain/policy/fetch.rb
|
|
197
|
+
- lib/textus/domain/policy/guard.rb
|
|
198
|
+
- lib/textus/domain/policy/guard_factory.rb
|
|
194
199
|
- lib/textus/domain/policy/handler_allowlist.rb
|
|
195
200
|
- lib/textus/domain/policy/matcher.rb
|
|
196
|
-
- lib/textus/domain/policy/predicates/
|
|
201
|
+
- lib/textus/domain/policy/predicates/author_held.rb
|
|
202
|
+
- lib/textus/domain/policy/predicates/etag_match.rb
|
|
203
|
+
- lib/textus/domain/policy/predicates/fresh_within.rb
|
|
204
|
+
- lib/textus/domain/policy/predicates/registry.rb
|
|
197
205
|
- lib/textus/domain/policy/predicates/schema_valid.rb
|
|
198
|
-
- lib/textus/domain/policy/
|
|
199
|
-
- lib/textus/domain/policy/
|
|
200
|
-
- lib/textus/domain/policy/refresh.rb
|
|
206
|
+
- lib/textus/domain/policy/predicates/target_is_canon.rb
|
|
207
|
+
- lib/textus/domain/policy/predicates/zone_writable_by.rb
|
|
201
208
|
- lib/textus/domain/policy/retention.rb
|
|
202
209
|
- lib/textus/domain/retention.rb
|
|
203
210
|
- lib/textus/domain/sentinel.rb
|
|
@@ -234,6 +241,7 @@ files:
|
|
|
234
241
|
- lib/textus/maintenance/rule_lint.rb
|
|
235
242
|
- lib/textus/maintenance/zone_mv.rb
|
|
236
243
|
- lib/textus/manifest.rb
|
|
244
|
+
- lib/textus/manifest/capabilities.rb
|
|
237
245
|
- lib/textus/manifest/data.rb
|
|
238
246
|
- lib/textus/manifest/entry.rb
|
|
239
247
|
- lib/textus/manifest/entry/base.rb
|
|
@@ -250,7 +258,6 @@ files:
|
|
|
250
258
|
- lib/textus/manifest/entry/validators/publish_each.rb
|
|
251
259
|
- lib/textus/manifest/policy.rb
|
|
252
260
|
- lib/textus/manifest/resolver.rb
|
|
253
|
-
- lib/textus/manifest/role_kinds.rb
|
|
254
261
|
- lib/textus/manifest/rules.rb
|
|
255
262
|
- lib/textus/manifest/schema.rb
|
|
256
263
|
- lib/textus/mcp.rb
|
|
@@ -264,9 +271,9 @@ files:
|
|
|
264
271
|
- lib/textus/ports/audit_subscriber.rb
|
|
265
272
|
- lib/textus/ports/build_lock.rb
|
|
266
273
|
- lib/textus/ports/clock.rb
|
|
274
|
+
- lib/textus/ports/fetch/detached.rb
|
|
275
|
+
- lib/textus/ports/fetch/lock.rb
|
|
267
276
|
- lib/textus/ports/publisher.rb
|
|
268
|
-
- lib/textus/ports/refresh/detached.rb
|
|
269
|
-
- lib/textus/ports/refresh/lock.rb
|
|
270
277
|
- lib/textus/ports/sentinel_store.rb
|
|
271
278
|
- lib/textus/ports/storage/file_stat.rb
|
|
272
279
|
- lib/textus/ports/storage/file_store.rb
|
|
@@ -278,7 +285,7 @@ files:
|
|
|
278
285
|
- lib/textus/read/doctor.rb
|
|
279
286
|
- lib/textus/read/freshness.rb
|
|
280
287
|
- lib/textus/read/get.rb
|
|
281
|
-
- lib/textus/read/
|
|
288
|
+
- lib/textus/read/get_or_fetch.rb
|
|
282
289
|
- lib/textus/read/list.rb
|
|
283
290
|
- lib/textus/read/policy_explain.rb
|
|
284
291
|
- lib/textus/read/published.rb
|
|
@@ -300,16 +307,15 @@ files:
|
|
|
300
307
|
- lib/textus/uid.rb
|
|
301
308
|
- lib/textus/version.rb
|
|
302
309
|
- lib/textus/write/accept.rb
|
|
303
|
-
- lib/textus/write/authority_gate.rb
|
|
304
310
|
- lib/textus/write/delete.rb
|
|
311
|
+
- lib/textus/write/fetch_all.rb
|
|
312
|
+
- lib/textus/write/fetch_orchestrator.rb
|
|
313
|
+
- lib/textus/write/fetch_worker.rb
|
|
305
314
|
- lib/textus/write/intake_fetch.rb
|
|
306
315
|
- lib/textus/write/materializer.rb
|
|
307
316
|
- lib/textus/write/mv.rb
|
|
308
317
|
- lib/textus/write/publish.rb
|
|
309
318
|
- lib/textus/write/put.rb
|
|
310
|
-
- lib/textus/write/refresh_all.rb
|
|
311
|
-
- lib/textus/write/refresh_orchestrator.rb
|
|
312
|
-
- lib/textus/write/refresh_worker.rb
|
|
313
319
|
- lib/textus/write/reject.rb
|
|
314
320
|
- lib/textus/write/retention_sweep.rb
|
|
315
321
|
homepage: https://github.com/patrick204nqh/textus
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class CLI
|
|
3
|
-
class Verb
|
|
4
|
-
class Refresh < Verb
|
|
5
|
-
option :as_flag, "--as=ROLE"
|
|
6
|
-
|
|
7
|
-
def call(store)
|
|
8
|
-
key = positional.shift or raise UsageError.new("refresh requires a key")
|
|
9
|
-
emit(session_for(store).refresh(key).to_h_for_wire)
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
end
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Textus
|
|
4
|
-
module Domain
|
|
5
|
-
# Authorization service. Single source of truth for "given a manifest
|
|
6
|
-
# entry and a role, may this caller read/write?". Lives in Domain
|
|
7
|
-
# alongside Permission.
|
|
8
|
-
class Authorizer
|
|
9
|
-
def initialize(manifest:)
|
|
10
|
-
@manifest = manifest
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def can_write?(zone, role:)
|
|
14
|
-
@manifest.policy.permission_for(zone.to_s).allows_write?(role)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
def can_read?(zone, role:)
|
|
18
|
-
@manifest.policy.permission_for(zone.to_s).allows_read?(role)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def authorize_write!(mentry, role:)
|
|
22
|
-
return if can_write?(mentry.zone, role: role)
|
|
23
|
-
|
|
24
|
-
writers = @manifest.policy.zone_writers(mentry.zone)
|
|
25
|
-
raise WriteForbidden.new(mentry.key, mentry.zone, writers: writers)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def authorize_read!(mentry, role:)
|
|
29
|
-
return if can_read?(mentry.zone, role: role)
|
|
30
|
-
|
|
31
|
-
readers = @manifest.policy.zone_readers[mentry.zone]
|
|
32
|
-
readers = nil if readers == :all
|
|
33
|
-
raise ReadForbidden.new(mentry.key, mentry.zone, readers: readers)
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Domain
|
|
3
|
-
module Policy
|
|
4
|
-
module Predicates
|
|
5
|
-
# Promotion predicate: the role driving the promotion must have
|
|
6
|
-
# role_kind == :accept_authority in the active manifest.
|
|
7
|
-
#
|
|
8
|
-
# Accept/Reject already gate on this kind before reaching the
|
|
9
|
-
# promotion policy, so in the default control-flow this predicate
|
|
10
|
-
# trivially passes. It is kept so manifests can express the
|
|
11
|
-
# requirement explicitly in `rules[].promotion.requires`.
|
|
12
|
-
class AcceptAuthoritySigned
|
|
13
|
-
attr_reader :reason
|
|
14
|
-
|
|
15
|
-
def name
|
|
16
|
-
"accept_authority_signed"
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def call(role:, manifest:, entry: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
20
|
-
role_str = role&.to_s
|
|
21
|
-
return true if role_str.nil? || role_str.empty?
|
|
22
|
-
|
|
23
|
-
kind = manifest.policy.role_kind(role_str)
|
|
24
|
-
return true if kind == :accept_authority
|
|
25
|
-
|
|
26
|
-
@reason = "role '#{role_str}' has kind '#{kind.inspect}', expected ':accept_authority'"
|
|
27
|
-
false
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Domain
|
|
3
|
-
module Policy
|
|
4
|
-
class Promote
|
|
5
|
-
KNOWN = %i[schema_valid accept_authority_signed].freeze
|
|
6
|
-
attr_reader :requires
|
|
7
|
-
|
|
8
|
-
def initialize(requires:)
|
|
9
|
-
syms = Array(requires).map { |r| r.to_s.to_sym }
|
|
10
|
-
unknown = syms - KNOWN
|
|
11
|
-
unless unknown.empty?
|
|
12
|
-
raise Textus::UsageError.new(
|
|
13
|
-
"unknown promote requirement: #{unknown.first.inspect} (known: #{KNOWN.join(", ")})",
|
|
14
|
-
)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
@requires = syms
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
def demands?(req)
|
|
21
|
-
@requires.include?(req)
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
end
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
require_relative "predicates/schema_valid"
|
|
2
|
-
require_relative "predicates/accept_authority_signed"
|
|
3
|
-
|
|
4
|
-
module Textus
|
|
5
|
-
module Domain
|
|
6
|
-
module Policy
|
|
7
|
-
class Promotion
|
|
8
|
-
Result = Struct.new(:ok?, :reasons, keyword_init: true)
|
|
9
|
-
|
|
10
|
-
REGISTRY = {
|
|
11
|
-
"schema_valid" => -> { Predicates::SchemaValid.new },
|
|
12
|
-
"accept_authority_signed" => -> { Predicates::AcceptAuthoritySigned.new },
|
|
13
|
-
}.freeze
|
|
14
|
-
|
|
15
|
-
def self.from_names(names)
|
|
16
|
-
predicates = Array(names).map do |n|
|
|
17
|
-
ctor = REGISTRY[n.to_s] or raise Textus::UsageError.new(
|
|
18
|
-
"unknown promotion predicate: '#{n}' (known: #{REGISTRY.keys.join(", ")})",
|
|
19
|
-
)
|
|
20
|
-
ctor.call
|
|
21
|
-
end
|
|
22
|
-
new(predicates: predicates)
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
attr_reader :predicates
|
|
26
|
-
|
|
27
|
-
def initialize(predicates:)
|
|
28
|
-
@predicates = predicates
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def predicate_names
|
|
32
|
-
@predicates.map(&:name)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def evaluate(entry:, schemas:, manifest:, role:)
|
|
36
|
-
reasons = []
|
|
37
|
-
@predicates.each do |pred|
|
|
38
|
-
ok = invoke(pred, entry: entry, schemas: schemas, manifest: manifest, role: role)
|
|
39
|
-
reasons << "#{pred.name}: #{pred.reason || "predicate failed"}" unless ok
|
|
40
|
-
end
|
|
41
|
-
Result.new(ok?: reasons.empty?, reasons: reasons)
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
private
|
|
45
|
-
|
|
46
|
-
def invoke(pred, entry:, schemas:, manifest:, role:)
|
|
47
|
-
case pred.name
|
|
48
|
-
when "accept_authority_signed"
|
|
49
|
-
pred.call(role: role, manifest: manifest, entry: entry)
|
|
50
|
-
else
|
|
51
|
-
pred.call(entry: entry, schemas: schemas, manifest: manifest)
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
class Manifest
|
|
3
|
-
module RoleKinds
|
|
4
|
-
DEFAULT_MAPPING = {
|
|
5
|
-
"human" => :accept_authority,
|
|
6
|
-
"agent" => :proposer,
|
|
7
|
-
"builder" => :generator,
|
|
8
|
-
"runner" => :runner,
|
|
9
|
-
}.freeze
|
|
10
|
-
|
|
11
|
-
# Returns { role_name => kind_symbol }. When `roles:` is declared we use
|
|
12
|
-
# exactly that; defaults are *not* layered in (declaring roles is an opt-in
|
|
13
|
-
# to a fully user-defined vocabulary).
|
|
14
|
-
def self.resolve(raw_roles)
|
|
15
|
-
return DEFAULT_MAPPING if raw_roles.nil?
|
|
16
|
-
|
|
17
|
-
raw_roles.to_h { |r| [r["name"], r["kind"].to_sym] }.freeze
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
module Textus
|
|
2
|
-
module Write
|
|
3
|
-
# Shared gate for write verbs that require the caller to hold the
|
|
4
|
-
# manifest's accept_authority role. Provides one method, expressed
|
|
5
|
-
# as two early-returns rather than a ternary, so each failure mode
|
|
6
|
-
# reads on its own line.
|
|
7
|
-
module AuthorityGate
|
|
8
|
-
def assert_accept_authority!(verb)
|
|
9
|
-
return if @manifest.policy.role_kind(@call.role) == :accept_authority
|
|
10
|
-
|
|
11
|
-
authority = @manifest.policy.roles_with_kind(:accept_authority).first
|
|
12
|
-
if authority.nil?
|
|
13
|
-
raise ProposalError.new(
|
|
14
|
-
"no role with accept_authority kind is declared in this manifest; #{verb} is disabled",
|
|
15
|
-
)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
raise ProposalError.new(
|
|
19
|
-
"only #{authority} role can #{verb} proposals; got '#{@call.role}'",
|
|
20
|
-
)
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|