ace-lab 0.8.0 → 0.13.0

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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +363 -92
  3. data/README.md +66 -100
  4. data/docs/usage.md +529 -404
  5. data/lib/ace/lab/atoms/install_schema.rb +373 -0
  6. data/lib/ace/lab/cli/commands/install.rb +161 -0
  7. data/lib/ace/lab/cli/commands/prepare.rb +118 -0
  8. data/lib/ace/lab/cli/commands/project_add.rb +101 -0
  9. data/lib/ace/lab/cli/commands/status.rb +109 -0
  10. data/lib/ace/lab/cli/commands/verify.rb +115 -0
  11. data/lib/ace/lab/cli.rb +21 -13
  12. data/lib/ace/lab/install_manifest_path.rb +23 -0
  13. data/lib/ace/lab/models/status_report.rb +38 -0
  14. data/lib/ace/lab/molecules/account_provisioner.rb +196 -0
  15. data/lib/ace/lab/molecules/config_state_classifier.rb +145 -0
  16. data/lib/ace/lab/molecules/directory_provisioner.rb +208 -0
  17. data/lib/ace/lab/molecules/grant_provisioner.rb +250 -0
  18. data/lib/ace/lab/molecules/grant_resolver.rb +49 -23
  19. data/lib/ace/lab/molecules/hitl_routes.rb +263 -0
  20. data/lib/ace/lab/molecules/install_manifest_loader.rb +64 -0
  21. data/lib/ace/lab/molecules/key_provisioner.rb +736 -0
  22. data/lib/ace/lab/molecules/lab_state_scanner.rb +55 -0
  23. data/lib/ace/lab/molecules/overseer_provisioner.rb +188 -0
  24. data/lib/ace/lab/molecules/project_registry.rb +251 -0
  25. data/lib/ace/lab/molecules/repository_provisioner.rb +141 -0
  26. data/lib/ace/lab/molecules/role_verifier.rb +223 -0
  27. data/lib/ace/lab/organisms/install.rb +213 -0
  28. data/lib/ace/lab/organisms/prepare.rb +156 -0
  29. data/lib/ace/lab/organisms/project_add.rb +170 -0
  30. data/lib/ace/lab/organisms/status_service.rb +70 -0
  31. data/lib/ace/lab/organisms/verify.rb +768 -0
  32. data/lib/ace/lab/version.rb +1 -1
  33. data/lib/ace/lab.rb +34 -6
  34. metadata +34 -102
  35. data/lib/ace/lab/atoms/protected_publication_input.rb +0 -50
  36. data/lib/ace/lab/atoms/protected_workspace_prune_input.rb +0 -114
  37. data/lib/ace/lab/atoms/protected_workspace_prune_preview.rb +0 -86
  38. data/lib/ace/lab/atoms/service_input.rb +0 -92
  39. data/lib/ace/lab/cli/commands/assignment.rb +0 -45
  40. data/lib/ace/lab/cli/commands/service.rb +0 -240
  41. data/lib/ace/lab/models/service_receipt.rb +0 -18
  42. data/lib/ace/lab/molecules/hitl_authorizer.rb +0 -63
  43. data/lib/ace/lab/molecules/ordinary_admission_resolver.rb +0 -131
  44. data/lib/ace/lab/molecules/ordinary_listener.rb +0 -262
  45. data/lib/ace/lab/molecules/ordinary_merge_read.rb +0 -45
  46. data/lib/ace/lab/molecules/ordinary_native_read.rb +0 -106
  47. data/lib/ace/lab/molecules/ordinary_owner_admission.rb +0 -139
  48. data/lib/ace/lab/molecules/ordinary_owner_config.rb +0 -232
  49. data/lib/ace/lab/molecules/ordinary_receiver_config.rb +0 -94
  50. data/lib/ace/lab/molecules/ordinary_socket.rb +0 -126
  51. data/lib/ace/lab/molecules/ordinary_worktree_read.rb +0 -216
  52. data/lib/ace/lab/molecules/protected_cleanup_owner_admission.rb +0 -163
  53. data/lib/ace/lab/molecules/protected_cleanup_owner_client.rb +0 -165
  54. data/lib/ace/lab/molecules/protected_cleanup_owner_identity.rb +0 -239
  55. data/lib/ace/lab/molecules/protected_publication_publisher.rb +0 -110
  56. data/lib/ace/lab/molecules/protected_service_handler.rb +0 -43
  57. data/lib/ace/lab/molecules/protected_service_policy.rb +0 -156
  58. data/lib/ace/lab/molecules/service_executor.rb +0 -99
  59. data/lib/ace/lab/molecules/service_policy.rb +0 -289
  60. data/lib/ace/lab/organisms/authority_composition.rb +0 -87
  61. data/lib/ace/lab/organisms/ordinary_assignment_client.rb +0 -149
  62. data/lib/ace/lab/organisms/ordinary_authority_composition.rb +0 -123
  63. data/lib/ace/lab/organisms/ordinary_merge_receiver.rb +0 -688
  64. data/lib/ace/lab/organisms/ordinary_merge_request.rb +0 -456
  65. data/lib/ace/lab/organisms/protected_cleanup_owner.rb +0 -361
  66. data/lib/ace/lab/organisms/protected_service_client.rb +0 -129
  67. data/lib/ace/lab/organisms/protected_service_listener.rb +0 -181
  68. data/lib/ace/lab/organisms/protected_service_publication.rb +0 -268
  69. data/lib/ace/lab/organisms/protected_service_receiver.rb +0 -267
  70. data/lib/ace/lab/organisms/protected_service_recovery.rb +0 -185
  71. data/lib/ace/lab/organisms/protected_service_request.rb +0 -128
  72. data/lib/ace/lab/organisms/protected_service_worker.rb +0 -89
  73. data/lib/ace/lab/organisms/service_request_service.rb +0 -249
@@ -1,240 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "ace/support/cli"
5
- require "ace/assign"
6
- require_relative "support"
7
- require_relative "../../organisms/protected_service_request"
8
- require_relative "../../organisms/ordinary_merge_request"
9
- require_relative "../../organisms/ordinary_merge_receiver"
10
- require_relative "../../organisms/ordinary_authority_composition"
11
- require_relative "../../molecules/ordinary_listener"
12
- require_relative "../../molecules/ordinary_receiver_config"
13
-
14
- module Ace
15
- module Lab
16
- module CLI
17
- module Commands
18
- module Service
19
- module Output
20
- def service
21
- @service ||= Organisms::ServiceRequestService.new
22
- end
23
-
24
- def protected_service
25
- @protected_service ||= Organisms::ProtectedServiceRequest.new
26
- end
27
-
28
- def protected_refusal(error)
29
- code = (error.is_a?(ArgumentError) || error.is_a?(SecurityError)) ? "protected_service_refused" : "protected_service_unavailable"
30
- message = error.is_a?(JSON::ParserError) ? "protected installed selection unavailable" : error.message.to_s.scrub[0, 512]
31
- emit({"status" => "error", "error" => {"code" => code, "message" => message}})
32
- end
33
-
34
- def emit(envelope)
35
- puts JSON.generate(envelope)
36
- return if envelope["status"] == "ok"
37
- error = envelope.fetch("error")
38
- code = error.fetch("code")
39
- message = error.fetch("message")
40
- raise Ace::Support::Cli::Error, "#{code}: #{message}"
41
- end
42
- end
43
-
44
- class Request < Ace::Support::Cli::Command
45
- include Ace::Support::Cli::Base
46
- include Runtime
47
- include Output
48
-
49
- desc "Request one authorized, configured service operation"
50
- option :project, type: :string, required: true, desc: "Project stable ID"
51
- option :assignment, type: :string, required: true, desc: "Assignment ID"
52
- option :attempt, type: :string, required: true, desc: "Managed attempt ID"
53
- option :operation, type: :string, required: true, desc: "Named operation"
54
- option :input, type: :string, required: true, desc: "Structured JSON input file"
55
- option :authorization, type: :string, required: true, desc: "Exact authorization reference"
56
- option :request_id, type: :string, required: true, desc: "Idempotency key"
57
- option :dry_run, type: :boolean, desc: "Validate without claiming or executing"
58
- option :mode, type: :string, desc: "ordinary-merge selects the ordinary fixed merge producer"
59
- option :queue_entry, type: :string, desc: "Original queue entry (ordinary merge)"
60
-
61
- option :mapping, type: :string, desc: "Original protected mapping ID"
62
- option :scope, type: :string, desc: "Original hierarchical prepared scope"
63
- option :service, type: :string, desc: "Installed receiver ID"
64
- option :candidate_head, type: :string, desc: "Exact approved candidate head"
65
- option :candidate_generation, type: :integer, desc: "Original candidate generation"
66
- option :expected_generation, type: :integer, desc: "Original authority generation"
67
-
68
- def call(project:, assignment:, attempt:, operation:, input:, authorization:, request_id:, **options)
69
- reject_identity_flags!(options)
70
- mode = options[:mode]
71
- if mode && mode != Organisms::OrdinaryMergeRequest::MODE
72
- return emit(Organisms::OrdinaryMergeRequest.refusal_envelope("ordinary_merge_invalid_request",
73
- "unknown service mode", request_id: request_id, input_digest: nil))
74
- end
75
- if mode
76
- begin
77
- return emit(ordinary_request(project: project, assignment: assignment, attempt: attempt,
78
- queue_entry: options[:queue_entry], operation: operation, input: input,
79
- authorization: authorization, request_id: request_id, **options))
80
- rescue Ace::Assign::AttemptErrors::EvidenceUnavailable, Ace::Lab::InvalidConfigurationError => e
81
- # Infra failures (unreadable canonical evidence, malformed
82
- # installed configuration, unavailable topology) are the
83
- # caller-side blocker, never a raw crash and never a durable
84
- # reference (review r1 finding B).
85
- return emit(Organisms::OrdinaryMergeRequest.refusal_envelope(
86
- "ordinary_merge_unavailable", e.message, request_id: request_id, input_digest: nil
87
- ))
88
- end
89
- end
90
- selected = protected_service.selected?(options)
91
- if selected
92
- return emit(protected_service.request(project: project, assignment: assignment, attempt: attempt,
93
- operation: operation, input_path: input, authorization: authorization, request_id: request_id, **options))
94
- end
95
- emit(service.request(project: project, assignment: assignment, attempt: attempt,
96
- operation: operation, input_path: input, authorization: authorization,
97
- request_id: request_id, dry_run: options[:dry_run]))
98
- rescue ArgumentError, Ace::Assign::Error, Ace::Git::Error, Ace::Runtime::RuntimeUnavailableError, JSON::ParserError, SecurityError, KeyError, SystemCallError => error
99
- raise if selected == false || mode
100
- protected_refusal(error)
101
- end
102
-
103
- private
104
-
105
- def ordinary_producer
106
- @ordinary_producer ||= Organisms::OrdinaryMergeRequest.new
107
- end
108
-
109
- # The ordinary refusal envelope is emitted exactly as received;
110
- # it is classified for the non-zero exit like every refusal.
111
- def ordinary_request(**arguments)
112
- ordinary_producer.request(**arguments)
113
- end
114
- end
115
-
116
- class Status < Ace::Support::Cli::Command
117
- include Ace::Support::Cli::Base
118
- include Runtime
119
- include Output
120
-
121
- desc "Read the authoritative outcome of a service request"
122
- option :request, type: :string, required: true, desc: "Request ID"
123
- option :format, type: :string, desc: "Output format (json only)"
124
- option :mode, type: :string, desc: "ordinary-merge selects the read-only ordinary status"
125
- option :reference, type: :string, desc: "Opaque ordinary merge reference"
126
- option :queue_entry, type: :string, desc: "Original queue entry (ordinary merge)"
127
-
128
- option :project, type: :string, desc: "Original project ID"
129
- option :assignment, type: :string, desc: "Original assignment ID"
130
- option :attempt, type: :string, desc: "Original attempt ID"
131
- option :mapping, type: :string, desc: "Original protected mapping ID"
132
- option :scope, type: :string, desc: "Original hierarchical prepared scope"
133
- option :candidate_head, type: :string, desc: "Exact approved candidate head"
134
- option :candidate_generation, type: :integer, desc: "Original candidate generation"
135
- option :input_digest, type: :string, desc: "Original normalized input SHA-256"
136
- option :target, type: :string, desc: "Original target resource ID"
137
- option :artifact_digest, type: :string, desc: "Original artifact SHA-256 (omit for null)"
138
-
139
- def call(request:, **options)
140
- reject_identity_flags!(options)
141
- mode = options[:mode]
142
- if mode && mode != Organisms::OrdinaryMergeRequest::MODE
143
- return emit(Organisms::OrdinaryMergeRequest.refusal_envelope("ordinary_merge_invalid_request",
144
- "unknown service mode", request_id: request, input_digest: nil,
145
- schema: Ace::Assign::Atoms::FixedMergeSchemas::STATUS_ERROR_SCHEMA_ID))
146
- end
147
- if mode
148
- ensure_json_format!(options)
149
- begin
150
- return emit(ordinary_producer.status(project: options[:project], assignment: options[:assignment],
151
- attempt: options[:attempt], queue_entry: options[:queue_entry], reference: options[:reference],
152
- request: request, input_digest: options[:input_digest], **options))
153
- rescue Ace::Assign::AttemptErrors::EvidenceUnavailable, Ace::Lab::InvalidConfigurationError => e
154
- return emit(Organisms::OrdinaryMergeRequest.refusal_envelope(
155
- "ordinary_merge_unavailable", e.message, request_id: request, input_digest: nil,
156
- schema: Ace::Assign::Atoms::FixedMergeSchemas::STATUS_ERROR_SCHEMA_ID
157
- ))
158
- end
159
- end
160
- selected = protected_service.selected?(options)
161
- if selected
162
- return emit(protected_service.status(request_id: request, **options))
163
- end
164
- emit(service.status(request_id: request))
165
- rescue ArgumentError, Ace::Assign::Error, Ace::Git::Error, Ace::Runtime::RuntimeUnavailableError, JSON::ParserError, SecurityError, KeyError, SystemCallError => error
166
- raise if selected == false || mode
167
- protected_refusal(error)
168
- end
169
-
170
- private
171
-
172
- def ordinary_producer
173
- @ordinary_producer ||= Organisms::OrdinaryMergeRequest.new
174
- end
175
- end
176
-
177
- # The ordinary receiver serve entrypoint (spec 8x3.t.xz9.4
178
- # execution clauses; 8x3.t.xz9.5 integration handoff): binds the
179
- # configured unix socket, mutually authenticates every connection
180
- # through OS peer credentials, admits through the canonical Assign
181
- # coordinator, executes the authorized fixed operation — one REAL
182
- # Forgejo merge under the scoped host-installed service identity —
183
- # and settles the exact terminal receipt proven by real forge
184
- # readback. Configuration is one explicit deployment-owned file
185
- # plus the host-installed authorization and forge-merge documents;
186
- # the host owns their placement and credentials. Blocks until
187
- # stopped.
188
- class Listen < Ace::Support::Cli::Command
189
- include Ace::Support::Cli::Base
190
- include Output
191
-
192
- desc "Serve ordinary Assign, HITL and fixed merge under the installed journal owner"
193
- option :mode, type: :string, required: true,
194
- desc: "ordinary-merge selects the ordinary fixed merge receiver"
195
- option :project, type: :string, required: true, desc: "Project stable ID"
196
- option :config, type: :string, required: true,
197
- desc: "Deployment-owned receiver configuration file (YAML)"
198
-
199
- def call(mode:, project:, config:, **options)
200
- unless options.empty?
201
- return emit(Organisms::OrdinaryMergeRequest.refusal_envelope(
202
- "ordinary_merge_invalid_request",
203
- "unknown listen selector: #{options.keys.sort.join(", ")}",
204
- request_id: "unknown", input_digest: nil
205
- ))
206
- end
207
- unless mode == Organisms::OrdinaryMergeRequest::MODE
208
- return emit(Organisms::OrdinaryMergeRequest.refusal_envelope(
209
- "ordinary_merge_invalid_request", "unknown service mode",
210
- request_id: "unknown", input_digest: nil
211
- ))
212
- end
213
- receiver_config = Molecules::OrdinaryReceiverConfig.load(config)
214
- unless receiver_config.project == project
215
- return emit(Organisms::OrdinaryMergeRequest.refusal_envelope(
216
- "ordinary_merge_selection_mismatch",
217
- "configured project does not match the requested project",
218
- request_id: "unknown", input_digest: nil
219
- ))
220
- end
221
- unless receiver_config.receiver_uid == Process.uid
222
- raise Ace::Lab::InvalidConfigurationError, "receiver must run as its configured UID"
223
- end
224
- server = Organisms::OrdinaryAuthorityComposition.new(receiver_config: receiver_config).build
225
- signals = %w[INT TERM].to_h { |signal| [signal, Signal.trap(signal) { server.request_stop }] }
226
- server.serve
227
- rescue Ace::Lab::InvalidConfigurationError, Ace::Assign::Error, Ace::Runtime::Error, Ace::Hitl::Lifecycle::Error => e
228
- emit(Organisms::OrdinaryMergeRequest.refusal_envelope(
229
- "ordinary_merge_unavailable", e.message,
230
- request_id: "unknown", input_digest: nil
231
- ))
232
- ensure
233
- signals&.each { |signal, handler| Signal.trap(signal, handler) }
234
- end
235
- end
236
- end
237
- end
238
- end
239
- end
240
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ace
4
- module Lab
5
- module Models
6
- # Non-secret result submitted to the assignment evidence coordinator.
7
- # The coordinator independently verifies every binding before acceptance.
8
- module ServiceReceipt
9
- BINDING_FIELDS = %w[request_id assignment_id attempt_id project_id operation
10
- input_digest target candidate_head executor_uid transport].freeze
11
-
12
- def self.build(binding, executor_result)
13
- BINDING_FIELDS.to_h { |field| [field, binding.fetch(field)] }.merge(executor_result)
14
- end
15
- end
16
- end
17
- end
18
- end
@@ -1,63 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "etc"
4
-
5
- module Ace
6
- module Lab
7
- module Molecules
8
- # The lab-side HITL delivery authorization (spec 8wq.t.34i): which
9
- # authenticated transport may submit correlated answers for a
10
- # Captain, derived from the SAME trusted deployment document that
11
- # carries the service principals — read through the grant
12
- # resolver's verified traversal, never from the cascade. The peer
13
- # uid is a KERNEL fact resolved by the HITL boundary; this policy
14
- # never trusts a claimed name.
15
- class HitlAuthorizer
16
- # @param principals [Hash] authorization.principals from the
17
- # trusted document (uid-string or username keys, per-project)
18
- # @param transport_uids [Array<Integer>] uids authorized to act
19
- # as the configured transport
20
- # @param service_uid [Integer, nil] the trusted HITL service uid
21
- def initialize(principals: {}, transport_uids: [], service_uid: nil)
22
- @principals = principals || {}
23
- @transport_uids = Array(transport_uids)
24
- @service_uid = service_uid
25
- end
26
-
27
- # Build from the trusted deployment document at a fixed path
28
- # (same file the service policy resolves principals from).
29
- # @return [HitlAuthorizer]
30
- def self.from_trusted_document(path, loader = GrantResolver)
31
- document = loader.trusted_document(path)
32
- hitl = document["hitl"].is_a?(Hash) ? document["hitl"] : {}
33
- principals = document.dig("authorization", "principals")
34
- new(
35
- principals: principals.is_a?(Hash) ? principals : {},
36
- transport_uids: Array(hitl["transport_uids"]),
37
- service_uid: hitl["service_uid"]
38
- )
39
- end
40
-
41
- # @return [Integer, nil] the trusted HITL service identity
42
- def service_uid
43
- @service_uid.is_a?(Integer) ? @service_uid : nil
44
- end
45
-
46
- # The transport may submit a correlated answer from an authorized
47
- # Captain when its KERNEL-VERIFIED uid is a configured transport
48
- # AND the transport's principal is visible for the request's
49
- # project. Unknown identity is an error-shaped refusal, never
50
- # permission.
51
- def delivery_authorized?(uid:, project_id:)
52
- return false unless @transport_uids.include?(uid)
53
-
54
- policy = @principals[uid.to_s]
55
- return false unless policy.is_a?(Hash)
56
-
57
- projects = Array(policy["projects"]).map(&:to_s)
58
- projects.empty? || projects.include?(project_id.to_s)
59
- end
60
- end
61
- end
62
- end
63
- end
@@ -1,131 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "time"
4
- require_relative "service_policy"
5
-
6
- module Ace
7
- module Lab
8
- module Molecules
9
- # The scoped ordinary-merge decision/admission resolver (spec 8x3.t.xz9.4,
10
- # execution clauses). The deployment-owned YAML authorization file remains
11
- # the sole source of truth; this resolver only makes the exact root
12
- # authorizations natively executable inside the receiver: one per-request
13
- # authorization selector resolves to a typed decision within the closed
14
- # refusal-code family — or it refuses. Scoping rules:
15
- #
16
- # - only `operation: merge` decisions of the ordinary shape resolve;
17
- # anything else (missing reference, shape mismatch, non-merge
18
- # operation, any binding mismatch, expiry) is a typed
19
- # `ordinary_merge_authorization_invalid` decision;
20
- # - exact binding match on every closed key; there are NO wildcards,
21
- # defaults, prefix grants or broad grants of any kind — a decision
22
- # authorizes exactly one bound request and nothing else;
23
- # - expiry bounds the decision itself.
24
- #
25
- # Per-request refresh (spec 8x3.t.xz9.5 integration handoff): a
26
- # long-lived receiver would otherwise decide every sequential candidate
27
- # against its boot-time snapshot of the authorization document. When a
28
- # trusted path is installed, every decision re-reads the document
29
- # through the trusted loader and resolves against the FRESH grants —
30
- # a root-revoked or expired decision fails closed at the next request
31
- # without any receiver restart. The refresh is scoped: it reloads and
32
- # re-validates the exact-request grant only; it never broadens
33
- # authorization beyond the resolved reference.
34
- class OrdinaryAdmissionResolver
35
- BINDING_KEYS = ServicePolicy::ORDINARY_BINDING_KEYS.freeze
36
- DECISION_KEYS = ServicePolicy::ORDINARY_AUTHORIZATION_KEYS.freeze
37
- OPERATION = "merge"
38
-
39
- Decision = Struct.new(:authorized?, :decision, :code, :message, keyword_init: true)
40
-
41
- # @param policy_document [Hash] boot-time trusted document (the
42
- # configuration digest is derived from it once, upstream)
43
- # @param path [String, nil] trusted policy path to re-read per decision
44
- # @param loader [Proc, nil] trusted reader (default: GrantResolver);
45
- # the only override point, for non-privileged test documents
46
- def initialize(policy_document, path: nil, loader: nil, proposal_resolver: nil, caller_uid: nil)
47
- unless policy_document.is_a?(Hash) && policy_document["authorizations"].is_a?(Hash)
48
- raise Ace::Lab::InvalidConfigurationError,
49
- "trusted policy document must contain an authorizations mapping"
50
- end
51
- @authorizations = policy_document.fetch("authorizations")
52
- @configuration_digest = ServicePolicy.configuration_digest(policy_document)
53
- @path = path
54
- @proposal_resolver = proposal_resolver
55
- @caller_uid = caller_uid
56
- @loader = loader || ->(trusted_path) { GrantResolver.trusted_document(trusted_path) }
57
- freeze
58
- end
59
-
60
- # Resolve one authorization selector against the exact ordinary merge
61
- # request binding. Returns a typed Decision; `authorized?` is true only
62
- # for an exact, unexpired, ordinary-merge-shaped match.
63
- def decide(reference, binding)
64
- authorizations = current_authorizations
65
- unless reference.is_a?(String) && reference.match?(ServicePolicy::ID)
66
- return refuse("invalid authorization reference")
67
- end
68
- unless binding.is_a?(Hash) && binding.fetch("operation", nil) == OPERATION
69
- return refuse("only the ordinary merge operation is resolvable")
70
- end
71
- if reference.start_with?("proposal-")
72
- return refuse("canonical proposal authority is unavailable") unless @proposal_resolver && @caller_uid.is_a?(Integer)
73
- begin
74
- decision = @proposal_resolver.call(reference, binding.slice(
75
- "operation", "project_id", "assignment_id", "attempt_id", "input_digest", "candidate_head").merge(
76
- "target" => {"resource" => binding.fetch("pr_url"), "artifact_digest" => nil},
77
- "caller_uid" => @caller_uid))
78
- return Decision.new(authorized?: true, decision: decision, code: nil, message: nil)
79
- rescue Ace::Assign::Error
80
- return refuse("canonical proposal does not authorize this exact ordinary merge")
81
- end
82
- end
83
- decision = authorizations[reference]
84
- return refuse("authorization reference is unresolved") unless decision.is_a?(Hash)
85
- unless DECISION_KEYS.all? { |key| decision.key?(key) }
86
- return refuse("authorization reference is not an ordinary merge decision")
87
- end
88
- unless decision.fetch("operation") == OPERATION
89
- return refuse("authorization does not cover the ordinary merge operation")
90
- end
91
- mismatch = BINDING_KEYS.find { |key| decision.fetch(key) != binding.fetch(key, nil) }
92
- return refuse("authorization does not match the exact ordinary merge request") if mismatch
93
- begin
94
- expired = Time.iso8601(decision.fetch("expires_at").to_s) <= Time.now.utc
95
- rescue ArgumentError
96
- return refuse("authorization decision has an invalid expiry")
97
- end
98
- return refuse("authorization has expired") if expired
99
- Decision.new(authorized?: true, decision: decision, code: nil, message: nil)
100
- end
101
-
102
- private
103
-
104
- # The authorizations mapping the CURRENT decision resolves against:
105
- # the boot snapshot when no trusted path is installed, otherwise the
106
- # freshly re-read document. A failed trusted re-read fails closed
107
- # (InvalidConfigurationError — the unavailable family), never falls
108
- # back to the stale snapshot.
109
- def current_authorizations
110
- return @authorizations unless @path
111
- document = @loader.call(@path)
112
- unless document.is_a?(Hash) && document["authorizations"].is_a?(Hash)
113
- raise Ace::Lab::InvalidConfigurationError,
114
- "invalid lab configuration: trusted authorization refresh at #{@path} " \
115
- "did not yield an authorizations mapping"
116
- end
117
- unless ServicePolicy.configuration_digest(document) == @configuration_digest
118
- raise Ace::Lab::InvalidConfigurationError,
119
- "ordinary receiver configuration changed; restart with the installed configuration"
120
- end
121
- document.fetch("authorizations")
122
- end
123
-
124
- def refuse(message)
125
- Decision.new(authorized?: false, decision: nil,
126
- code: "ordinary_merge_authorization_invalid", message: message)
127
- end
128
- end
129
- end
130
- end
131
- end