igniter-extensions 0.5.2
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 +7 -0
- data/README.md +381 -0
- data/lib/igniter/extensions/contracts/aggregate_pack.rb +103 -0
- data/lib/igniter/extensions/contracts/audit/builder.rb +132 -0
- data/lib/igniter/extensions/contracts/audit/event.rb +34 -0
- data/lib/igniter/extensions/contracts/audit/snapshot.rb +44 -0
- data/lib/igniter/extensions/contracts/audit_pack.rb +60 -0
- data/lib/igniter/extensions/contracts/branch_pack.rb +199 -0
- data/lib/igniter/extensions/contracts/capabilities/declaration.rb +31 -0
- data/lib/igniter/extensions/contracts/capabilities/error.rb +35 -0
- data/lib/igniter/extensions/contracts/capabilities/policy.rb +20 -0
- data/lib/igniter/extensions/contracts/capabilities/report.rb +47 -0
- data/lib/igniter/extensions/contracts/capabilities/violation.rb +30 -0
- data/lib/igniter/extensions/contracts/capabilities_pack.rb +146 -0
- data/lib/igniter/extensions/contracts/collection_pack.rb +212 -0
- data/lib/igniter/extensions/contracts/commerce_pack.rb +91 -0
- data/lib/igniter/extensions/contracts/compose_pack.rb +213 -0
- data/lib/igniter/extensions/contracts/content_addressing/cache.rb +59 -0
- data/lib/igniter/extensions/contracts/content_addressing/content_key.rb +63 -0
- data/lib/igniter/extensions/contracts/content_addressing/declaration.rb +47 -0
- data/lib/igniter/extensions/contracts/content_addressing_pack.rb +90 -0
- data/lib/igniter/extensions/contracts/creator/profile.rb +196 -0
- data/lib/igniter/extensions/contracts/creator/report.rb +85 -0
- data/lib/igniter/extensions/contracts/creator/scaffold.rb +461 -0
- data/lib/igniter/extensions/contracts/creator/scope.rb +79 -0
- data/lib/igniter/extensions/contracts/creator/wizard.rb +269 -0
- data/lib/igniter/extensions/contracts/creator/workflow.rb +189 -0
- data/lib/igniter/extensions/contracts/creator/workflow_step.rb +51 -0
- data/lib/igniter/extensions/contracts/creator/write_result.rb +48 -0
- data/lib/igniter/extensions/contracts/creator/write_step.rb +63 -0
- data/lib/igniter/extensions/contracts/creator/writer.rb +131 -0
- data/lib/igniter/extensions/contracts/creator_pack.rb +128 -0
- data/lib/igniter/extensions/contracts/dataflow/aggregate_operators.rb +119 -0
- data/lib/igniter/extensions/contracts/dataflow/aggregate_state.rb +60 -0
- data/lib/igniter/extensions/contracts/dataflow/builder.rb +66 -0
- data/lib/igniter/extensions/contracts/dataflow/collection_result.rb +70 -0
- data/lib/igniter/extensions/contracts/dataflow/diff.rb +37 -0
- data/lib/igniter/extensions/contracts/dataflow/item_result.rb +44 -0
- data/lib/igniter/extensions/contracts/dataflow/result.rb +58 -0
- data/lib/igniter/extensions/contracts/dataflow/session.rb +173 -0
- data/lib/igniter/extensions/contracts/dataflow/window_filter.rb +49 -0
- data/lib/igniter/extensions/contracts/dataflow_pack.rb +66 -0
- data/lib/igniter/extensions/contracts/debug/pack_audit.rb +181 -0
- data/lib/igniter/extensions/contracts/debug/pack_snapshot.rb +46 -0
- data/lib/igniter/extensions/contracts/debug/profile_snapshot.rb +50 -0
- data/lib/igniter/extensions/contracts/debug/report.rb +50 -0
- data/lib/igniter/extensions/contracts/debug_pack.rb +115 -0
- data/lib/igniter/extensions/contracts/differential/divergence.rb +37 -0
- data/lib/igniter/extensions/contracts/differential/formatter.rb +85 -0
- data/lib/igniter/extensions/contracts/differential/report.rb +83 -0
- data/lib/igniter/extensions/contracts/differential/runner.rb +136 -0
- data/lib/igniter/extensions/contracts/differential_pack.rb +61 -0
- data/lib/igniter/extensions/contracts/execution_report_pack.rb +38 -0
- data/lib/igniter/extensions/contracts/incremental/formatter.rb +60 -0
- data/lib/igniter/extensions/contracts/incremental/node_state.rb +30 -0
- data/lib/igniter/extensions/contracts/incremental/result.rb +65 -0
- data/lib/igniter/extensions/contracts/incremental/session.rb +146 -0
- data/lib/igniter/extensions/contracts/incremental_pack.rb +40 -0
- data/lib/igniter/extensions/contracts/invariants/builder.rb +27 -0
- data/lib/igniter/extensions/contracts/invariants/cases_report.rb +47 -0
- data/lib/igniter/extensions/contracts/invariants/error.rb +34 -0
- data/lib/igniter/extensions/contracts/invariants/invariant.rb +30 -0
- data/lib/igniter/extensions/contracts/invariants/report.rb +45 -0
- data/lib/igniter/extensions/contracts/invariants/suite.rb +36 -0
- data/lib/igniter/extensions/contracts/invariants/violation.rb +39 -0
- data/lib/igniter/extensions/contracts/invariants_pack.rb +88 -0
- data/lib/igniter/extensions/contracts/journal_pack.rb +55 -0
- data/lib/igniter/extensions/contracts/language/formula_pack.rb +185 -0
- data/lib/igniter/extensions/contracts/language/piecewise_pack.rb +166 -0
- data/lib/igniter/extensions/contracts/language/scale_pack.rb +147 -0
- data/lib/igniter/extensions/contracts/lookup_pack.rb +50 -0
- data/lib/igniter/extensions/contracts/mcp/creator_session.rb +105 -0
- data/lib/igniter/extensions/contracts/mcp/tool_argument.rb +35 -0
- data/lib/igniter/extensions/contracts/mcp/tool_definition.rb +33 -0
- data/lib/igniter/extensions/contracts/mcp/tool_result.rb +28 -0
- data/lib/igniter/extensions/contracts/mcp_pack.rb +335 -0
- data/lib/igniter/extensions/contracts/provenance/builder.rb +80 -0
- data/lib/igniter/extensions/contracts/provenance/lineage.rb +59 -0
- data/lib/igniter/extensions/contracts/provenance/node_trace.rb +53 -0
- data/lib/igniter/extensions/contracts/provenance/text_formatter.rb +62 -0
- data/lib/igniter/extensions/contracts/provenance_pack.rb +52 -0
- data/lib/igniter/extensions/contracts/reactive/builder.rb +43 -0
- data/lib/igniter/extensions/contracts/reactive/dispatch_result.rb +59 -0
- data/lib/igniter/extensions/contracts/reactive/engine.rb +79 -0
- data/lib/igniter/extensions/contracts/reactive/event.rb +36 -0
- data/lib/igniter/extensions/contracts/reactive/matcher.rb +20 -0
- data/lib/igniter/extensions/contracts/reactive/plan.rb +58 -0
- data/lib/igniter/extensions/contracts/reactive/subscription.rb +29 -0
- data/lib/igniter/extensions/contracts/reactive_pack.rb +169 -0
- data/lib/igniter/extensions/contracts/saga/compensation.rb +25 -0
- data/lib/igniter/extensions/contracts/saga/compensation_record.rb +28 -0
- data/lib/igniter/extensions/contracts/saga/compensation_set.rb +47 -0
- data/lib/igniter/extensions/contracts/saga/formatter.rb +39 -0
- data/lib/igniter/extensions/contracts/saga/result.rb +56 -0
- data/lib/igniter/extensions/contracts/saga/runner.rb +124 -0
- data/lib/igniter/extensions/contracts/saga_pack.rb +56 -0
- data/lib/igniter/extensions/contracts.rb +445 -0
- data/lib/igniter/extensions.rb +6 -0
- data/lib/igniter-extensions.rb +3 -0
- metadata +152 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "audit/event"
|
|
4
|
+
require_relative "audit/snapshot"
|
|
5
|
+
require_relative "audit/builder"
|
|
6
|
+
|
|
7
|
+
module Igniter
|
|
8
|
+
module Extensions
|
|
9
|
+
module Contracts
|
|
10
|
+
module AuditPack
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
REPORT_CONTRIBUTOR = Module.new do
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def augment(report:, result:, profile:) # rubocop:disable Lint/UnusedMethodArgument
|
|
17
|
+
snapshot = AuditPack.snapshot(result)
|
|
18
|
+
report.add_section(:audit_summary, {
|
|
19
|
+
graph: snapshot.graph,
|
|
20
|
+
event_count: snapshot.event_count,
|
|
21
|
+
event_types: snapshot.event_types,
|
|
22
|
+
state_count: snapshot.states.length,
|
|
23
|
+
output_names: snapshot.output_names
|
|
24
|
+
})
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def manifest
|
|
29
|
+
Igniter::Contracts::PackManifest.new(
|
|
30
|
+
name: :extensions_audit,
|
|
31
|
+
registry_contracts: [Igniter::Contracts::PackManifest.diagnostic(:audit_summary)],
|
|
32
|
+
metadata: { category: :developer }
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def install_into(kernel)
|
|
37
|
+
kernel.diagnostics_contributors.register(:audit_summary, REPORT_CONTRIBUTOR)
|
|
38
|
+
kernel
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def snapshot(result)
|
|
42
|
+
Audit::Builder.build(result)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def report(environment, inputs: nil, compiled_graph: nil, &block)
|
|
46
|
+
result =
|
|
47
|
+
if block
|
|
48
|
+
environment.run(inputs: inputs || {}, &block)
|
|
49
|
+
elsif compiled_graph
|
|
50
|
+
environment.execute(compiled_graph, inputs: inputs || {})
|
|
51
|
+
else
|
|
52
|
+
raise ArgumentError, "audit_report requires a block or compiled_graph"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
snapshot(result)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module BranchPack
|
|
7
|
+
UNSET = Object.new.freeze
|
|
8
|
+
|
|
9
|
+
class << self
|
|
10
|
+
def manifest
|
|
11
|
+
Igniter::Contracts::PackManifest.new(
|
|
12
|
+
name: :extensions_branch,
|
|
13
|
+
registry_contracts: [Igniter::Contracts::PackManifest.dsl_keyword(:branch)]
|
|
14
|
+
)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def install_into(kernel)
|
|
18
|
+
kernel.dsl_keywords.register(:branch, branch_keyword)
|
|
19
|
+
kernel
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def branch_keyword
|
|
23
|
+
Igniter::Contracts::DslKeyword.new(:branch) do |name, on:, builder:, depends_on: [], &block|
|
|
24
|
+
raise ArgumentError, "branch :#{name} requires a block" unless block
|
|
25
|
+
|
|
26
|
+
selector_name = on.to_sym
|
|
27
|
+
dependency_names = [selector_name, *Array(depends_on).map(&:to_sym)].uniq
|
|
28
|
+
definition = Definition.new(name: name, selector_name: selector_name)
|
|
29
|
+
definition.instance_eval(&block)
|
|
30
|
+
definition.validate!
|
|
31
|
+
|
|
32
|
+
builder.add_operation(
|
|
33
|
+
kind: :compute,
|
|
34
|
+
name: name,
|
|
35
|
+
depends_on: dependency_names,
|
|
36
|
+
callable: lambda do |**values|
|
|
37
|
+
definition.resolve(values)
|
|
38
|
+
end
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def invoke_value(callable_or_value, kwargs)
|
|
44
|
+
return callable_or_value unless callable_or_value.respond_to?(:call)
|
|
45
|
+
|
|
46
|
+
parameters = callable_or_value.parameters
|
|
47
|
+
accepts_any_keywords = parameters.any? { |kind, _name| kind == :keyrest }
|
|
48
|
+
return callable_or_value.call(**kwargs) if accepts_any_keywords
|
|
49
|
+
|
|
50
|
+
accepted = parameters.select { |kind, _name| %i[key keyreq].include?(kind) }.map(&:last)
|
|
51
|
+
callable_or_value.call(**kwargs.slice(*accepted))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class Definition
|
|
55
|
+
def initialize(name:, selector_name:)
|
|
56
|
+
@name = name.to_sym
|
|
57
|
+
@selector_name = selector_name.to_sym
|
|
58
|
+
@cases = []
|
|
59
|
+
@default_case = nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def on(match = UNSET, id: nil, value: UNSET, **options, &block)
|
|
63
|
+
matcher_kind, matcher_value = normalize_match(match, options)
|
|
64
|
+
resolved_value = normalize_value(value, block)
|
|
65
|
+
|
|
66
|
+
@cases << {
|
|
67
|
+
id: (id || :"case_#{@cases.length + 1}").to_sym,
|
|
68
|
+
matcher: matcher_kind,
|
|
69
|
+
match: matcher_value,
|
|
70
|
+
value: resolved_value
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def default(id: :default, value: UNSET, &block)
|
|
75
|
+
raise ArgumentError, "branch :#{@name} can define only one default" if @default_case
|
|
76
|
+
|
|
77
|
+
@default_case = {
|
|
78
|
+
id: id.to_sym,
|
|
79
|
+
matcher: :default,
|
|
80
|
+
match: :default,
|
|
81
|
+
value: normalize_value(value, block)
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def validate!
|
|
86
|
+
raise ArgumentError, "branch :#{@name} requires at least one on clause" if @cases.empty?
|
|
87
|
+
raise ArgumentError, "branch :#{@name} requires a default clause" unless @default_case
|
|
88
|
+
|
|
89
|
+
duplicate_ids = (@cases.map { |entry| entry.fetch(:id) } + [@default_case.fetch(:id)])
|
|
90
|
+
.group_by { |id| id }
|
|
91
|
+
.select { |_id, group| group.length > 1 }
|
|
92
|
+
.keys
|
|
93
|
+
unless duplicate_ids.empty?
|
|
94
|
+
raise ArgumentError,
|
|
95
|
+
"branch :#{@name} has duplicate case ids: #{duplicate_ids.join(", ")}"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
overlapping = overlapping_literals
|
|
99
|
+
return if overlapping.empty?
|
|
100
|
+
|
|
101
|
+
raise ArgumentError,
|
|
102
|
+
"branch :#{@name} has overlapping literal matches: #{overlapping.map(&:inspect).join(", ")}"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def resolve(kwargs)
|
|
106
|
+
selector_value = kwargs.fetch(@selector_name)
|
|
107
|
+
selected = @cases.find { |entry| match?(entry, selector_value) } || @default_case
|
|
108
|
+
|
|
109
|
+
{
|
|
110
|
+
case: selected.fetch(:id),
|
|
111
|
+
value: BranchPack.invoke_value(selected.fetch(:value), kwargs),
|
|
112
|
+
matcher: selected.fetch(:matcher),
|
|
113
|
+
matched_on: selected.fetch(:match),
|
|
114
|
+
selector: @selector_name,
|
|
115
|
+
selector_value: selector_value
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def normalize_match(match, options)
|
|
122
|
+
matcher_options = options.slice(:eq, :in, :matches)
|
|
123
|
+
provided = matcher_options.reject { |_key, value| value.equal?(UNSET) || value.nil? }
|
|
124
|
+
|
|
125
|
+
raise ArgumentError, "branch :#{@name} on cannot combine positional match with eq:, in:, or matches:" if !match.equal?(UNSET) && !provided.empty?
|
|
126
|
+
|
|
127
|
+
raise ArgumentError, "branch :#{@name} on requires a positional match or one of eq:, in:, or matches:" if match.equal?(UNSET) && provided.empty?
|
|
128
|
+
|
|
129
|
+
if provided.length > 1
|
|
130
|
+
raise ArgumentError,
|
|
131
|
+
"branch :#{@name} on supports only one matcher option at a time"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
return [:eq, match] unless match.equal?(UNSET)
|
|
135
|
+
|
|
136
|
+
matcher_kind, matcher_value = provided.first
|
|
137
|
+
|
|
138
|
+
case matcher_kind
|
|
139
|
+
when :eq
|
|
140
|
+
[:eq, matcher_value]
|
|
141
|
+
when :in
|
|
142
|
+
array = Array(matcher_value)
|
|
143
|
+
raise ArgumentError, "branch :#{@name} in: requires a non-empty array" if array.empty?
|
|
144
|
+
|
|
145
|
+
[:in, array.freeze]
|
|
146
|
+
when :matches
|
|
147
|
+
raise ArgumentError, "branch :#{@name} matches: requires a Regexp" unless matcher_value.is_a?(Regexp)
|
|
148
|
+
|
|
149
|
+
[:matches, matcher_value]
|
|
150
|
+
else
|
|
151
|
+
raise ArgumentError, "branch :#{@name} received an unknown matcher #{matcher_kind.inspect}"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def normalize_value(value, block)
|
|
156
|
+
if !value.equal?(UNSET) && block
|
|
157
|
+
raise ArgumentError,
|
|
158
|
+
"branch :#{@name} case cannot combine value: with a block"
|
|
159
|
+
end
|
|
160
|
+
raise ArgumentError, "branch :#{@name} case requires value: or a block" if value.equal?(UNSET) && !block
|
|
161
|
+
|
|
162
|
+
block || value
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def overlapping_literals
|
|
166
|
+
literals = @cases.flat_map do |entry|
|
|
167
|
+
case entry.fetch(:matcher)
|
|
168
|
+
when :eq
|
|
169
|
+
[entry.fetch(:match)]
|
|
170
|
+
when :in
|
|
171
|
+
entry.fetch(:match)
|
|
172
|
+
else
|
|
173
|
+
[]
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
literals.group_by { |value| value }
|
|
178
|
+
.select { |_value, group| group.length > 1 }
|
|
179
|
+
.keys
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def match?(entry, selector_value)
|
|
183
|
+
case entry.fetch(:matcher)
|
|
184
|
+
when :eq
|
|
185
|
+
selector_value == entry.fetch(:match)
|
|
186
|
+
when :in
|
|
187
|
+
entry.fetch(:match).include?(selector_value)
|
|
188
|
+
when :matches
|
|
189
|
+
!!(selector_value.to_s =~ entry.fetch(:match))
|
|
190
|
+
else
|
|
191
|
+
false
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Capabilities
|
|
7
|
+
class Declaration
|
|
8
|
+
attr_reader :callable, :capabilities
|
|
9
|
+
|
|
10
|
+
def initialize(callable:, capabilities:)
|
|
11
|
+
@callable = callable
|
|
12
|
+
@capabilities = Array(capabilities).flatten.compact.map(&:to_sym).uniq.freeze
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def call(**kwargs)
|
|
17
|
+
callable.call(**kwargs)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def declared_capabilities
|
|
21
|
+
capabilities
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def pure?
|
|
25
|
+
capabilities.include?(:pure)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Capabilities
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
|
|
9
|
+
class CapabilityViolationError < Error
|
|
10
|
+
attr_reader :report
|
|
11
|
+
|
|
12
|
+
def initialize(message = nil, report:)
|
|
13
|
+
@report = report
|
|
14
|
+
super(message || default_message)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h
|
|
18
|
+
{
|
|
19
|
+
message: message,
|
|
20
|
+
report: report.to_h
|
|
21
|
+
}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def default_message
|
|
27
|
+
return "capability policy violated" if report.violations.empty?
|
|
28
|
+
|
|
29
|
+
report.violations.map(&:message).join("; ")
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Capabilities
|
|
7
|
+
class Policy
|
|
8
|
+
attr_reader :denied, :required, :on_undeclared
|
|
9
|
+
|
|
10
|
+
def initialize(denied: [], required: [], on_undeclared: :ignore)
|
|
11
|
+
@denied = Array(denied).map(&:to_sym).uniq.freeze
|
|
12
|
+
@required = Array(required).map(&:to_sym).uniq.freeze
|
|
13
|
+
@on_undeclared = on_undeclared.to_sym
|
|
14
|
+
freeze
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Capabilities
|
|
7
|
+
class Report
|
|
8
|
+
attr_reader :required_capabilities, :profile_capabilities, :violations, :undeclared_nodes
|
|
9
|
+
|
|
10
|
+
def initialize(required_capabilities:, profile_capabilities:, violations:, undeclared_nodes:)
|
|
11
|
+
@required_capabilities = required_capabilities.transform_keys(&:to_sym).transform_values do |value|
|
|
12
|
+
Array(value).map(&:to_sym).freeze
|
|
13
|
+
end.freeze
|
|
14
|
+
@profile_capabilities = Array(profile_capabilities).map(&:to_sym).uniq.freeze
|
|
15
|
+
@violations = Array(violations).freeze
|
|
16
|
+
@undeclared_nodes = Array(undeclared_nodes).map(&:to_sym).freeze
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def valid?
|
|
21
|
+
violations.empty?
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def invalid?
|
|
25
|
+
!valid?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def summary
|
|
29
|
+
return "valid" if valid?
|
|
30
|
+
|
|
31
|
+
"invalid - #{violations.length} capability violation(s)"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_h
|
|
35
|
+
{
|
|
36
|
+
valid: valid?,
|
|
37
|
+
required_capabilities: required_capabilities,
|
|
38
|
+
profile_capabilities: profile_capabilities,
|
|
39
|
+
undeclared_nodes: undeclared_nodes,
|
|
40
|
+
violations: violations.map(&:to_h)
|
|
41
|
+
}
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Igniter
|
|
4
|
+
module Extensions
|
|
5
|
+
module Contracts
|
|
6
|
+
module Capabilities
|
|
7
|
+
class Violation
|
|
8
|
+
attr_reader :kind, :node_name, :capabilities, :message
|
|
9
|
+
|
|
10
|
+
def initialize(kind:, node_name:, capabilities:, message:)
|
|
11
|
+
@kind = kind.to_sym
|
|
12
|
+
@node_name = node_name.to_sym
|
|
13
|
+
@capabilities = Array(capabilities).map(&:to_sym).freeze
|
|
14
|
+
@message = message.to_s
|
|
15
|
+
freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_h
|
|
19
|
+
{
|
|
20
|
+
kind: kind,
|
|
21
|
+
node_name: node_name,
|
|
22
|
+
capabilities: capabilities,
|
|
23
|
+
message: message
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "capabilities/declaration"
|
|
4
|
+
require_relative "capabilities/violation"
|
|
5
|
+
require_relative "capabilities/report"
|
|
6
|
+
require_relative "capabilities/policy"
|
|
7
|
+
require_relative "capabilities/error"
|
|
8
|
+
|
|
9
|
+
module Igniter
|
|
10
|
+
module Extensions
|
|
11
|
+
module Contracts
|
|
12
|
+
module CapabilitiesPack
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
def manifest
|
|
16
|
+
Igniter::Contracts::PackManifest.new(
|
|
17
|
+
name: :extensions_capabilities,
|
|
18
|
+
metadata: { category: :validation }
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def install_into(kernel)
|
|
23
|
+
kernel
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def declare(*capabilities, callable: nil, &block)
|
|
27
|
+
target = callable || block
|
|
28
|
+
raise ArgumentError, "capability declaration requires a callable or block" unless target
|
|
29
|
+
|
|
30
|
+
Capabilities::Declaration.new(callable: target, capabilities: capabilities)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def pure(callable: nil, &block)
|
|
34
|
+
declare(:pure, callable: callable, &block)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def policy(denied: [], required: [], on_undeclared: :ignore)
|
|
38
|
+
Capabilities::Policy.new(
|
|
39
|
+
denied: denied,
|
|
40
|
+
required: required,
|
|
41
|
+
on_undeclared: on_undeclared
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def required_capabilities(compiled_graph)
|
|
46
|
+
compiled_graph.operations.reject(&:output?).each_with_object({}) do |operation, memo|
|
|
47
|
+
capabilities = capabilities_for_operation(operation)
|
|
48
|
+
memo[operation.name] = capabilities unless capabilities.empty?
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def capabilities_for(compiled_graph, node_name)
|
|
53
|
+
operation = compiled_graph.operations.find { |entry| entry.name == node_name.to_sym }
|
|
54
|
+
return [] unless operation
|
|
55
|
+
|
|
56
|
+
capabilities_for_operation(operation)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def profile_capabilities(profile)
|
|
60
|
+
profile.pack_manifests
|
|
61
|
+
.flat_map do |manifest_entry|
|
|
62
|
+
manifest_entry.provides_capabilities.empty? ? Array(manifest_entry.metadata[:capabilities]) : manifest_entry.provides_capabilities
|
|
63
|
+
end
|
|
64
|
+
.map(&:to_sym)
|
|
65
|
+
.uniq
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def report(compiled_graph, profile: nil, policy: nil)
|
|
69
|
+
requirements = required_capabilities(compiled_graph)
|
|
70
|
+
undeclared_nodes = compiled_graph.operations.reject(&:output?).map(&:name) - requirements.keys
|
|
71
|
+
violations = violations_for(requirements, policy: policy, undeclared_nodes: undeclared_nodes)
|
|
72
|
+
maybe_warn_about_undeclared(policy, undeclared_nodes)
|
|
73
|
+
|
|
74
|
+
Capabilities::Report.new(
|
|
75
|
+
required_capabilities: requirements,
|
|
76
|
+
profile_capabilities: profile ? profile_capabilities(profile) : [],
|
|
77
|
+
violations: violations,
|
|
78
|
+
undeclared_nodes: undeclared_nodes
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def check!(compiled_graph, policy:, profile: nil)
|
|
83
|
+
result = report(compiled_graph, profile: profile, policy: policy)
|
|
84
|
+
raise Capabilities::CapabilityViolationError.new(nil, report: result) if result.invalid?
|
|
85
|
+
|
|
86
|
+
result
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def capabilities_for_operation(operation)
|
|
90
|
+
capabilities = Array(operation.attributes[:capabilities]).map(&:to_sym)
|
|
91
|
+
callable = operation.attributes[:callable]
|
|
92
|
+
capabilities.concat(Array(callable.declared_capabilities).map(&:to_sym)) if callable.respond_to?(:declared_capabilities)
|
|
93
|
+
capabilities.uniq
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def violations_for(requirements, policy:, undeclared_nodes:)
|
|
97
|
+
return [] unless policy
|
|
98
|
+
|
|
99
|
+
violations = []
|
|
100
|
+
|
|
101
|
+
requirements.each do |node_name, capabilities|
|
|
102
|
+
denied = capabilities & policy.denied
|
|
103
|
+
if denied.any?
|
|
104
|
+
violations << Capabilities::Violation.new(
|
|
105
|
+
kind: :denied_capability,
|
|
106
|
+
node_name: node_name,
|
|
107
|
+
capabilities: denied,
|
|
108
|
+
message: "node #{node_name} uses denied capabilities: #{denied.join(", ")}"
|
|
109
|
+
)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
missing = policy.required - capabilities
|
|
113
|
+
next unless missing.any?
|
|
114
|
+
|
|
115
|
+
violations << Capabilities::Violation.new(
|
|
116
|
+
kind: :missing_required_capability,
|
|
117
|
+
node_name: node_name,
|
|
118
|
+
capabilities: missing,
|
|
119
|
+
message: "node #{node_name} is missing required capabilities: #{missing.join(", ")}"
|
|
120
|
+
)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
if policy.on_undeclared == :error
|
|
124
|
+
undeclared_nodes.each do |node_name|
|
|
125
|
+
violations << Capabilities::Violation.new(
|
|
126
|
+
kind: :undeclared_capabilities,
|
|
127
|
+
node_name: node_name,
|
|
128
|
+
capabilities: [],
|
|
129
|
+
message: "node #{node_name} does not declare capabilities"
|
|
130
|
+
)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
violations
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def maybe_warn_about_undeclared(policy, undeclared_nodes)
|
|
138
|
+
return unless policy&.on_undeclared == :warn
|
|
139
|
+
return if undeclared_nodes.empty?
|
|
140
|
+
|
|
141
|
+
Warning.warn("WARNING: undeclared capabilities for nodes: #{undeclared_nodes.join(", ")}\n")
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|