textus 0.29.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 -235
- data/CHANGELOG.md +169 -0
- data/README.md +85 -64
- data/SPEC.md +366 -201
- data/docs/conventions.md +42 -37
- data/lib/textus/boot.rb +93 -76
- 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/hook_run.rb +2 -6
- data/lib/textus/cli/verb/hooks.rb +1 -1
- data/lib/textus/cli/verb/put.rb +5 -14
- data/lib/textus/cli/verb/retain.rb +19 -0
- data/lib/textus/cli/verb/rule_list.rb +8 -8
- data/lib/textus/cli.rb +21 -18
- data/lib/textus/container.rb +1 -2
- data/lib/textus/dispatcher.rb +11 -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/check.rb +8 -5
- data/lib/textus/doctor.rb +2 -1
- data/lib/textus/domain/action.rb +3 -3
- data/lib/textus/domain/duration.rb +22 -0
- 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} +2 -16
- 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/policy/retention.rb +26 -0
- data/lib/textus/domain/retention.rb +44 -0
- data/lib/textus/domain/staleness/intake_check.rb +6 -6
- data/lib/textus/envelope/io/reader.rb +4 -0
- data/lib/textus/envelope/io/writer.rb +8 -0
- data/lib/textus/envelope.rb +2 -2
- data/lib/textus/errors.rb +25 -28
- data/lib/textus/hooks/event_bus.rb +12 -24
- data/lib/textus/hooks/rpc_registry.rb +9 -35
- data/lib/textus/hooks/signature.rb +31 -0
- data/lib/textus/init.rb +24 -18
- data/lib/textus/maintenance/zone_mv.rb +1 -1
- data/lib/textus/manifest/capabilities.rb +29 -0
- data/lib/textus/manifest/data.rb +16 -8
- data/lib/textus/manifest/entry/base.rb +2 -2
- data/lib/textus/manifest/policy.rb +62 -19
- data/lib/textus/manifest/rules.rb +25 -14
- data/lib/textus/manifest/schema.rb +78 -38
- data/lib/textus/manifest.rb +6 -5
- data/lib/textus/mcp/server.rb +2 -10
- data/lib/textus/mcp/session.rb +7 -23
- 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 +19 -10
- data/lib/textus/read/pulse.rb +5 -4
- data/lib/textus/read/retainable.rb +17 -0
- data/lib/textus/read/validator.rb +1 -1
- data/lib/textus/role_scope.rb +3 -2
- 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 +15 -17
- 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} +23 -30
- data/lib/textus/write/intake_fetch.rb +23 -0
- data/lib/textus/write/mv.rb +17 -15
- data/lib/textus/write/put.rb +15 -17
- data/lib/textus/write/reject.rb +11 -5
- data/lib/textus/write/retention_sweep.rb +55 -0
- metadata +32 -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
|
@@ -2,20 +2,20 @@ require "timeout"
|
|
|
2
2
|
|
|
3
3
|
module Textus
|
|
4
4
|
module Write
|
|
5
|
-
class
|
|
6
|
-
FETCH_TIMEOUT_SECONDS =
|
|
5
|
+
class FetchWorker
|
|
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
|
|
|
@@ -117,21 +124,7 @@ module Textus
|
|
|
117
124
|
end
|
|
118
125
|
|
|
119
126
|
def writer
|
|
120
|
-
@writer ||= Textus::Envelope::IO::Writer.
|
|
121
|
-
file_store: @container.file_store,
|
|
122
|
-
manifest: @container.manifest,
|
|
123
|
-
schemas: @container.schemas,
|
|
124
|
-
audit_log: @container.audit_log,
|
|
125
|
-
call: @call,
|
|
126
|
-
reader: reader,
|
|
127
|
-
)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
def reader
|
|
131
|
-
@reader ||= Textus::Envelope::IO::Reader.new(
|
|
132
|
-
file_store: @container.file_store,
|
|
133
|
-
manifest: @container.manifest,
|
|
134
|
-
)
|
|
127
|
+
@writer ||= Textus::Envelope::IO::Writer.from(container: @container, call: @call)
|
|
135
128
|
end
|
|
136
129
|
end
|
|
137
130
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
require "timeout"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Write
|
|
5
|
+
# Invokes a :resolve_intake hook handler by name under a timeout.
|
|
6
|
+
# The transport-side fetch kernel shared by `textus put --fetch` and
|
|
7
|
+
# `textus hook run`. Maps Timeout::Error to a UsageError; leaves any
|
|
8
|
+
# other error to the caller (call sites differ in how they wrap those).
|
|
9
|
+
module IntakeFetch
|
|
10
|
+
FETCH_TIMEOUT_SECONDS = 30
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
def invoke(rpc:, handler:, config:, args:, label:, timeout: FETCH_TIMEOUT_SECONDS)
|
|
15
|
+
Timeout.timeout(timeout) do
|
|
16
|
+
rpc.invoke(:resolve_intake, handler, caps: nil, config: config, args: args)
|
|
17
|
+
end
|
|
18
|
+
rescue Timeout::Error
|
|
19
|
+
raise Textus::UsageError.new("#{label} '#{handler}' exceeded #{timeout}s timeout")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
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,22 +100,25 @@ module Textus
|
|
|
101
100
|
}
|
|
102
101
|
end
|
|
103
102
|
|
|
104
|
-
def
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
112
113
|
)
|
|
113
114
|
end
|
|
114
115
|
|
|
116
|
+
def writer
|
|
117
|
+
@writer ||= Textus::Envelope::IO::Writer.from(container: @container, call: @call)
|
|
118
|
+
end
|
|
119
|
+
|
|
115
120
|
def reader
|
|
116
|
-
@reader ||= Textus::Envelope::IO::Reader.
|
|
117
|
-
file_store: @container.file_store,
|
|
118
|
-
manifest: @container.manifest,
|
|
119
|
-
)
|
|
121
|
+
@reader ||= Textus::Envelope::IO::Reader.from(container: @container)
|
|
120
122
|
end
|
|
121
123
|
end
|
|
122
124
|
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,26 +32,25 @@ module Textus
|
|
|
33
32
|
|
|
34
33
|
private
|
|
35
34
|
|
|
36
|
-
def
|
|
37
|
-
|
|
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)
|
|
38
39
|
end
|
|
39
40
|
|
|
40
|
-
def
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
schemas: @container.schemas,
|
|
45
|
-
audit_log: @container.audit_log,
|
|
46
|
-
call: @call,
|
|
47
|
-
reader: reader,
|
|
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
|
|
48
45
|
)
|
|
49
46
|
end
|
|
50
47
|
|
|
51
|
-
def
|
|
52
|
-
@
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
48
|
+
def hook_context
|
|
49
|
+
@hook_context ||= Textus::Hooks::Context.for(container: @container, call: @call)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def writer
|
|
53
|
+
@writer ||= Textus::Envelope::IO::Writer.from(container: @container, call: @call)
|
|
56
54
|
end
|
|
57
55
|
end
|
|
58
56
|
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
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
module Textus
|
|
4
|
+
module Write
|
|
5
|
+
# Applies retention actions reported by Read::Retainable. `expire` deletes
|
|
6
|
+
# the leaf through the role gate; `archive` copies it to
|
|
7
|
+
# <root>/archive/<relative-path> first, then deletes. Rows whose zone the
|
|
8
|
+
# caller's role cannot write surface in `failed` rather than aborting.
|
|
9
|
+
class RetentionSweep
|
|
10
|
+
def initialize(container:, call:)
|
|
11
|
+
@container = container
|
|
12
|
+
@call = call
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def call(prefix: nil, zone: nil)
|
|
16
|
+
rows = Textus::Read::Retainable.new(container: @container, call: @call)
|
|
17
|
+
.call(prefix: prefix, zone: zone)
|
|
18
|
+
delete_op = Textus::Write::Delete.new(container: @container, call: @call)
|
|
19
|
+
expired = []
|
|
20
|
+
archived = []
|
|
21
|
+
failed = []
|
|
22
|
+
|
|
23
|
+
rows.each do |row|
|
|
24
|
+
key = row["key"]
|
|
25
|
+
begin
|
|
26
|
+
archive_leaf(row) if row["action"] == "archive"
|
|
27
|
+
delete_op.call(key)
|
|
28
|
+
(row["action"] == "archive" ? archived : expired) << key
|
|
29
|
+
rescue Textus::Error => e
|
|
30
|
+
failed << { "key" => key, "error" => e.message }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
{
|
|
35
|
+
"protocol" => Textus::PROTOCOL,
|
|
36
|
+
"ok" => failed.empty?,
|
|
37
|
+
"expired" => expired,
|
|
38
|
+
"archived" => archived,
|
|
39
|
+
"failed" => failed,
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def archive_leaf(row)
|
|
46
|
+
src = row["path"]
|
|
47
|
+
root = @container.root.to_s
|
|
48
|
+
rel = src.delete_prefix("#{root}/")
|
|
49
|
+
dest = File.join(root, "archive", rel)
|
|
50
|
+
FileUtils.mkdir_p(File.dirname(dest))
|
|
51
|
+
FileUtils.cp(src, dest)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
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,9 +151,8 @@ 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
|
+
- lib/textus/cli/verb/retain.rb
|
|
155
156
|
- lib/textus/cli/verb/rule_explain.rb
|
|
156
157
|
- lib/textus/cli/verb/rule_lint.rb
|
|
157
158
|
- lib/textus/cli/verb/rule_list.rb
|
|
@@ -167,13 +168,14 @@ files:
|
|
|
167
168
|
- lib/textus/doctor.rb
|
|
168
169
|
- lib/textus/doctor/check.rb
|
|
169
170
|
- lib/textus/doctor/check/audit_log.rb
|
|
171
|
+
- lib/textus/doctor/check/fetch_locks.rb
|
|
170
172
|
- lib/textus/doctor/check/handler_allowlist.rb
|
|
171
173
|
- lib/textus/doctor/check/hooks.rb
|
|
172
174
|
- lib/textus/doctor/check/illegal_keys.rb
|
|
173
175
|
- lib/textus/doctor/check/intake_registration.rb
|
|
174
176
|
- lib/textus/doctor/check/manifest_files.rb
|
|
177
|
+
- lib/textus/doctor/check/proposal_targets.rb
|
|
175
178
|
- lib/textus/doctor/check/protocol_version.rb
|
|
176
|
-
- lib/textus/doctor/check/refresh_locks.rb
|
|
177
179
|
- lib/textus/doctor/check/rule_ambiguity.rb
|
|
178
180
|
- lib/textus/doctor/check/schema_parse_error.rb
|
|
179
181
|
- lib/textus/doctor/check/schema_violations.rb
|
|
@@ -182,20 +184,29 @@ files:
|
|
|
182
184
|
- lib/textus/doctor/check/templates.rb
|
|
183
185
|
- lib/textus/doctor/check/unowned_schema_fields.rb
|
|
184
186
|
- lib/textus/domain/action.rb
|
|
185
|
-
- lib/textus/domain/
|
|
187
|
+
- lib/textus/domain/duration.rb
|
|
186
188
|
- lib/textus/domain/freshness.rb
|
|
187
189
|
- lib/textus/domain/freshness/evaluator.rb
|
|
188
190
|
- lib/textus/domain/freshness/policy.rb
|
|
189
191
|
- lib/textus/domain/freshness/verdict.rb
|
|
190
192
|
- lib/textus/domain/outcome.rb
|
|
191
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
|
|
192
199
|
- lib/textus/domain/policy/handler_allowlist.rb
|
|
193
200
|
- lib/textus/domain/policy/matcher.rb
|
|
194
|
-
- 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
|
|
195
205
|
- lib/textus/domain/policy/predicates/schema_valid.rb
|
|
196
|
-
- lib/textus/domain/policy/
|
|
197
|
-
- lib/textus/domain/policy/
|
|
198
|
-
- lib/textus/domain/policy/
|
|
206
|
+
- lib/textus/domain/policy/predicates/target_is_canon.rb
|
|
207
|
+
- lib/textus/domain/policy/predicates/zone_writable_by.rb
|
|
208
|
+
- lib/textus/domain/policy/retention.rb
|
|
209
|
+
- lib/textus/domain/retention.rb
|
|
199
210
|
- lib/textus/domain/sentinel.rb
|
|
200
211
|
- lib/textus/domain/staleness.rb
|
|
201
212
|
- lib/textus/domain/staleness/generator_check.rb
|
|
@@ -218,6 +229,7 @@ files:
|
|
|
218
229
|
- lib/textus/hooks/fire_report.rb
|
|
219
230
|
- lib/textus/hooks/loader.rb
|
|
220
231
|
- lib/textus/hooks/rpc_registry.rb
|
|
232
|
+
- lib/textus/hooks/signature.rb
|
|
221
233
|
- lib/textus/init.rb
|
|
222
234
|
- lib/textus/key/distance.rb
|
|
223
235
|
- lib/textus/key/grammar.rb
|
|
@@ -229,6 +241,7 @@ files:
|
|
|
229
241
|
- lib/textus/maintenance/rule_lint.rb
|
|
230
242
|
- lib/textus/maintenance/zone_mv.rb
|
|
231
243
|
- lib/textus/manifest.rb
|
|
244
|
+
- lib/textus/manifest/capabilities.rb
|
|
232
245
|
- lib/textus/manifest/data.rb
|
|
233
246
|
- lib/textus/manifest/entry.rb
|
|
234
247
|
- lib/textus/manifest/entry/base.rb
|
|
@@ -245,7 +258,6 @@ files:
|
|
|
245
258
|
- lib/textus/manifest/entry/validators/publish_each.rb
|
|
246
259
|
- lib/textus/manifest/policy.rb
|
|
247
260
|
- lib/textus/manifest/resolver.rb
|
|
248
|
-
- lib/textus/manifest/role_kinds.rb
|
|
249
261
|
- lib/textus/manifest/rules.rb
|
|
250
262
|
- lib/textus/manifest/schema.rb
|
|
251
263
|
- lib/textus/mcp.rb
|
|
@@ -259,9 +271,9 @@ files:
|
|
|
259
271
|
- lib/textus/ports/audit_subscriber.rb
|
|
260
272
|
- lib/textus/ports/build_lock.rb
|
|
261
273
|
- lib/textus/ports/clock.rb
|
|
274
|
+
- lib/textus/ports/fetch/detached.rb
|
|
275
|
+
- lib/textus/ports/fetch/lock.rb
|
|
262
276
|
- lib/textus/ports/publisher.rb
|
|
263
|
-
- lib/textus/ports/refresh/detached.rb
|
|
264
|
-
- lib/textus/ports/refresh/lock.rb
|
|
265
277
|
- lib/textus/ports/sentinel_store.rb
|
|
266
278
|
- lib/textus/ports/storage/file_stat.rb
|
|
267
279
|
- lib/textus/ports/storage/file_store.rb
|
|
@@ -273,12 +285,13 @@ files:
|
|
|
273
285
|
- lib/textus/read/doctor.rb
|
|
274
286
|
- lib/textus/read/freshness.rb
|
|
275
287
|
- lib/textus/read/get.rb
|
|
276
|
-
- lib/textus/read/
|
|
288
|
+
- lib/textus/read/get_or_fetch.rb
|
|
277
289
|
- lib/textus/read/list.rb
|
|
278
290
|
- lib/textus/read/policy_explain.rb
|
|
279
291
|
- lib/textus/read/published.rb
|
|
280
292
|
- lib/textus/read/pulse.rb
|
|
281
293
|
- lib/textus/read/rdeps.rb
|
|
294
|
+
- lib/textus/read/retainable.rb
|
|
282
295
|
- lib/textus/read/schema_envelope.rb
|
|
283
296
|
- lib/textus/read/stale.rb
|
|
284
297
|
- lib/textus/read/uid.rb
|
|
@@ -294,16 +307,17 @@ files:
|
|
|
294
307
|
- lib/textus/uid.rb
|
|
295
308
|
- lib/textus/version.rb
|
|
296
309
|
- lib/textus/write/accept.rb
|
|
297
|
-
- lib/textus/write/authority_gate.rb
|
|
298
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
|
|
314
|
+
- lib/textus/write/intake_fetch.rb
|
|
299
315
|
- lib/textus/write/materializer.rb
|
|
300
316
|
- lib/textus/write/mv.rb
|
|
301
317
|
- lib/textus/write/publish.rb
|
|
302
318
|
- lib/textus/write/put.rb
|
|
303
|
-
- lib/textus/write/refresh_all.rb
|
|
304
|
-
- lib/textus/write/refresh_orchestrator.rb
|
|
305
|
-
- lib/textus/write/refresh_worker.rb
|
|
306
319
|
- lib/textus/write/reject.rb
|
|
320
|
+
- lib/textus/write/retention_sweep.rb
|
|
307
321
|
homepage: https://github.com/patrick204nqh/textus
|
|
308
322
|
licenses:
|
|
309
323
|
- MIT
|
|
@@ -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
|