ace-assign 0.64.0 → 0.66.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/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +11 -0
- data/CHANGELOG.md +25 -0
- data/docs/protected-authority.md +23 -7
- data/docs/usage.md +117 -4
- data/exe/ace-scope-ready +55 -0
- data/handbook/workflow-instructions/assign/drive.wf.md +26 -1
- data/lib/ace/assign/atoms/attempt_state_machine.rb +8 -0
- data/lib/ace/assign/atoms/delivery_parameters.rb +4 -39
- data/lib/ace/assign/authority/assignment_inventory.rb +290 -0
- data/lib/ace/assign/authority/campaign_consumer_policy.rb +100 -0
- data/lib/ace/assign/authority/campaign_execution.rb +119 -0
- data/lib/ace/assign/authority/campaign_round_transfer.rb +82 -0
- data/lib/ace/assign/authority/candidate_transfer.rb +169 -2
- data/lib/ace/assign/authority/cleanup_result_protection.rb +53 -0
- data/lib/ace/assign/authority/client.rb +156 -6
- data/lib/ace/assign/authority/deployment.rb +201 -39
- data/lib/ace/assign/authority/deployment_history.rb +146 -0
- data/lib/ace/assign/authority/endcap.rb +131 -18
- data/lib/ace/assign/authority/endcap_attempts.rb +274 -0
- data/lib/ace/assign/authority/endcap_campaign_results.rb +232 -0
- data/lib/ace/assign/authority/endcap_campaign_rounds.rb +266 -0
- data/lib/ace/assign/authority/endcap_contexts.rb +23 -0
- data/lib/ace/assign/authority/endcap_inboxes.rb +3 -280
- data/lib/ace/assign/authority/endcap_prepared_work.rb +80 -0
- data/lib/ace/assign/authority/endcap_publication.rb +207 -0
- data/lib/ace/assign/authority/endcap_results.rb +36 -18
- data/lib/ace/assign/authority/endcap_review_requests.rb +183 -0
- data/lib/ace/assign/authority/endcap_reviews.rb +130 -0
- data/lib/ace/assign/authority/endcap_service_settlement.rb +152 -0
- data/lib/ace/assign/authority/endcap_services.rb +221 -18
- data/lib/ace/assign/authority/execution_scope_observation.rb +468 -41
- data/lib/ace/assign/authority/inbox_context_original.rb +113 -0
- data/lib/ace/assign/authority/launch_attempt_consumers.rb +51 -0
- data/lib/ace/assign/authority/launch_campaign_registration.rb +214 -0
- data/lib/ace/assign/authority/launch_control_channel.rb +313 -0
- data/lib/ace/assign/authority/launch_control_exclusions.rb +141 -0
- data/lib/ace/assign/authority/launch_driver.rb +298 -19
- data/lib/ace/assign/authority/launch_input_inhibition.rb +132 -0
- data/lib/ace/assign/authority/launch_lifecycle.rb +275 -100
- data/lib/ace/assign/authority/launch_prepared_work.rb +122 -0
- data/lib/ace/assign/authority/launch_review.rb +91 -0
- data/lib/ace/assign/authority/launch_scope_abort.rb +0 -7
- data/lib/ace/assign/authority/launch_scope_admission.rb +136 -5
- data/lib/ace/assign/authority/launch_scope_close.rb +68 -37
- data/lib/ace/assign/authority/launch_scope_release.rb +696 -17
- data/lib/ace/assign/authority/launch_steering.rb +353 -0
- data/lib/ace/assign/authority/launch_stop.rb +254 -0
- data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +229 -0
- data/lib/ace/assign/authority/original_inbox_selection.rb +41 -0
- data/lib/ace/assign/authority/posix_acl.rb +5 -2
- data/lib/ace/assign/authority/prepared_input.rb +130 -0
- data/lib/ace/assign/authority/prepared_queue.rb +185 -0
- data/lib/ace/assign/authority/prepared_task_context.rb +116 -0
- data/lib/ace/assign/authority/prepared_work.rb +304 -0
- data/lib/ace/assign/authority/prepared_worker.rb +69 -0
- data/lib/ace/assign/authority/protected_assignment_context.rb +226 -0
- data/lib/ace/assign/authority/readiness_hook.rb +120 -0
- data/lib/ace/assign/authority/router.rb +24 -2
- data/lib/ace/assign/authority/server.rb +103 -33
- data/lib/ace/assign/authority/service_cleanup_dispatch.rb +88 -0
- data/lib/ace/assign/authority/service_cleanup_evidence.rb +180 -0
- data/lib/ace/assign/authority/service_delivery_evidence.rb +101 -0
- data/lib/ace/assign/authority/service_evidence.rb +243 -12
- data/lib/ace/assign/authority/service_publication_evidence.rb +110 -0
- data/lib/ace/assign/authority/task_context_entry.rb +32 -0
- data/lib/ace/assign/authority/transfer_codec.rb +44 -12
- data/lib/ace/assign/cli/commands/assignment_target.rb +15 -2
- data/lib/ace/assign/cli/commands/attempt/base.rb +49 -0
- data/lib/ace/assign/cli/commands/attempt/finish.rb +20 -4
- data/lib/ace/assign/cli/commands/attempt/reconcile.rb +11 -0
- data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +25 -0
- data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +28 -0
- data/lib/ace/assign/cli/commands/authority/launch.rb +58 -6
- data/lib/ace/assign/cli/commands/authority/serve.rb +2 -1
- data/lib/ace/assign/cli/commands/authority/status.rb +1 -1
- data/lib/ace/assign/cli/commands/authority/task_context.rb +31 -0
- data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +25 -0
- data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +30 -0
- data/lib/ace/assign/cli/commands/authority/worker.rb +23 -0
- data/lib/ace/assign/cli/commands/campaign_export_result.rb +55 -0
- data/lib/ace/assign/cli/commands/campaign_record_round.rb +57 -0
- data/lib/ace/assign/cli/commands/create.rb +2 -0
- data/lib/ace/assign/cli/commands/delivery.rb +54 -4
- data/lib/ace/assign/cli/commands/fail.rb +6 -0
- data/lib/ace/assign/cli/commands/finish.rb +23 -2
- data/lib/ace/assign/cli/commands/fork_session.rb +6 -2
- data/lib/ace/assign/cli/commands/inbox_bind.rb +34 -0
- data/lib/ace/assign/cli/commands/protected_submission.rb +61 -0
- data/lib/ace/assign/cli/commands/resume.rb +12 -0
- data/lib/ace/assign/cli/commands/select.rb +2 -0
- data/lib/ace/assign/cli/commands/start.rb +2 -0
- data/lib/ace/assign/cli/commands/status.rb +4 -2
- data/lib/ace/assign/cli/commands/step.rb +3 -1
- data/lib/ace/assign/cli/commands/submit_candidate.rb +34 -0
- data/lib/ace/assign/cli/commands/submit_result.rb +61 -0
- data/lib/ace/assign/cli.rb +36 -5
- data/lib/ace/assign/errors.rb +139 -0
- data/lib/ace/assign/models/assignment.rb +26 -4
- data/lib/ace/assign/models/evidence_event.rb +3 -3
- data/lib/ace/assign/molecules/assignment_manager.rb +12 -2
- data/lib/ace/assign/molecules/attempt_reconciler.rb +20 -0
- data/lib/ace/assign/molecules/canonical_attempt_state.rb +151 -0
- data/lib/ace/assign/molecules/canonical_evidence.rb +5 -2
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +374 -0
- data/lib/ace/assign/molecules/evidence_journal.rb +720 -44
- data/lib/ace/assign/molecules/execution_scope_lineage.rb +27 -9
- data/lib/ace/assign/molecules/fork_session_launcher.rb +16 -6
- data/lib/ace/assign/molecules/journal_input_inhibition.rb +78 -0
- data/lib/ace/assign/molecules/journal_mutation.rb +82 -13
- data/lib/ace/assign/molecules/journal_prompt_mutation.rb +241 -0
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +178 -26
- data/lib/ace/assign/molecules/original_launch_binding.rb +37 -0
- data/lib/ace/assign/molecules/protected_control_exclusion.rb +303 -0
- data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +330 -0
- data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +164 -0
- data/lib/ace/assign/molecules/queue_scanner.rb +26 -11
- data/lib/ace/assign/molecules/receipt_verifier.rb +21 -15
- data/lib/ace/assign/molecules/step_writer.rb +27 -5
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +88 -0
- data/lib/ace/assign/organisms/assignment_executor.rb +4 -4
- data/lib/ace/assign/organisms/attempt_coordinator.rb +66 -76
- data/lib/ace/assign/organisms/prepared_work_builder.rb +194 -0
- data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +100 -0
- data/lib/ace/assign/organisms/task_assignment_creator.rb +4 -2
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +1 -112
- metadata +90 -11
- data/lib/ace/assign/cli/commands/inbox_reconcile.rb +0 -36
|
@@ -5,9 +5,15 @@ require "securerandom"
|
|
|
5
5
|
require "time"
|
|
6
6
|
require "fileutils"
|
|
7
7
|
require "open3"
|
|
8
|
+
require "ace/herdr/molecules/guarded_native_origin"
|
|
9
|
+
require_relative "launch_control_channel"
|
|
8
10
|
require_relative "deployment"
|
|
11
|
+
require_relative "deployment_history"
|
|
9
12
|
require_relative "../molecules/execution_scope_lineage"
|
|
13
|
+
require_relative "../molecules/original_launch_binding"
|
|
14
|
+
require_relative "../molecules/canonical_attempt_state"
|
|
10
15
|
require_relative "execution_scope_observation"
|
|
16
|
+
require_relative "prepared_work"
|
|
11
17
|
|
|
12
18
|
module Ace
|
|
13
19
|
module Assign
|
|
@@ -16,24 +22,34 @@ module Ace
|
|
|
16
22
|
# Process handles/streams are observations, never a replacement ledger.
|
|
17
23
|
class LaunchLifecycle
|
|
18
24
|
MUTATIONS = {
|
|
19
|
-
"register_assignment" => %w[mapping_id assignment_id
|
|
25
|
+
"register_assignment" => %w[mapping_id assignment_id definition_digest prepared_head prepared_tree manifest_sha256 expected_generation transfer],
|
|
20
26
|
"reserve_attempt" => %w[mapping_id assignment_id scope worker_uid runtime base_head launcher_process_binding expected_generation],
|
|
21
|
-
"record_launch" => %w[mapping_id assignment_id attempt_id launch_ticket process_binding expected_generation],
|
|
27
|
+
"record_launch" => %w[mapping_id assignment_id attempt_id launch_ticket process_binding guarded_origin expected_generation],
|
|
22
28
|
"bind_process" => %w[mapping_id assignment_id attempt_id launch_ticket process_binding expected_generation],
|
|
23
29
|
"release_launch" => %w[mapping_id assignment_id attempt_id launch_ticket process_binding expected_generation],
|
|
24
30
|
"abort_launch" => %w[mapping_id assignment_id attempt_id launch_ticket failure_evidence failure_digest expected_generation]
|
|
25
31
|
}.freeze
|
|
26
|
-
|
|
32
|
+
TRANSFER_OPERATIONS = {"register_assignment" => {direction: :upload, purpose: :candidate, roles: [:launcher]}, "prompt_attempt" => {direction: :upload, purpose: :prompt_text, roles: %i[launcher supervisor]}}.freeze
|
|
33
|
+
OPERATIONS = (MUTATIONS.keys + %w[assignment_inventory stop_attempt prompt_attempt prompt_status launch_review_intent launch_input_inhibit_selection launch_input_inhibit_completion launch_prompt_intent launch_prompt_completion launch_preflight registration_status attempt_status inspect_launch observe_execution_scope close_execution_scope]).freeze
|
|
27
34
|
TERMINAL = %w[succeeded failed stopped].freeze
|
|
28
35
|
|
|
29
|
-
attr_reader :mutex, :journals, :
|
|
30
|
-
def initialize(deployment:, kernel: Ace::Runtime::Molecules::ProtectedLinux.new, journals: nil, mutex: Mutex.new,
|
|
36
|
+
attr_reader :mutex, :journals, :deployment, :deployment_history
|
|
37
|
+
def initialize(deployment:, kernel: Ace::Runtime::Molecules::ProtectedLinux.new, journals: nil, mutex: Mutex.new, scope_observer_factory: nil, deployment_history: nil, control_exclusion_factory: nil)
|
|
31
38
|
@deployment, @kernel = deployment, kernel
|
|
39
|
+
if deployment_history && (!deployment_history.is_a?(DeploymentHistory) ||
|
|
40
|
+
!deployment_history.selects?(deployment))
|
|
41
|
+
raise ArgumentError, "protected history transaction does not select installed descriptor"
|
|
42
|
+
end
|
|
43
|
+
@deployment_history = deployment_history
|
|
32
44
|
@journals = journals || {}
|
|
33
|
-
@
|
|
45
|
+
@control_exclusion_factory = control_exclusion_factory || ->(**options) {
|
|
46
|
+
Molecules::LifecycleExclusion.control_exclusion(**options)
|
|
47
|
+
}
|
|
34
48
|
@mutex = mutex
|
|
35
49
|
@observations = {}
|
|
36
50
|
@streams = {}
|
|
51
|
+
@native_issuers = {}
|
|
52
|
+
@control_channels = {}
|
|
37
53
|
@slot_exclusions = {}
|
|
38
54
|
@scope_observers = {}
|
|
39
55
|
@scope_observer_factory = scope_observer_factory || ->(mapping_id) {
|
|
@@ -56,8 +72,17 @@ module Ace
|
|
|
56
72
|
mapping_id: params.fetch("mapping_id")).require_launch_bound!
|
|
57
73
|
end
|
|
58
74
|
|
|
59
|
-
def dispatch(request:, peer:, role:)
|
|
75
|
+
def dispatch(request:, peer:, role:, transfer: nil)
|
|
60
76
|
operation, params = request.values_at("operation", "params")
|
|
77
|
+
return assignment_inventory!(request: request, peer: peer, role: role) if operation == "assignment_inventory"
|
|
78
|
+
return stop_attempt!(request: request, peer: peer, role: role) if operation == "stop_attempt"
|
|
79
|
+
return prompt_attempt!(request: request, peer: peer, role: role, transfer: transfer) if operation == "prompt_attempt"
|
|
80
|
+
return prompt_status!(request: request, peer: peer, role: role) if operation == "prompt_status"
|
|
81
|
+
return launch_input_inhibit_selection!(request: request, peer: peer, role: role) if operation == "launch_input_inhibit_selection"
|
|
82
|
+
return launch_input_inhibit_completion!(request: request, peer: peer, role: role) if operation == "launch_input_inhibit_completion"
|
|
83
|
+
return launch_review_intent!(request: request, peer: peer, role: role) if operation == "launch_review_intent"
|
|
84
|
+
return launch_prompt_intent!(request: request, peer: peer, role: role) if operation == "launch_prompt_intent"
|
|
85
|
+
return launch_prompt_completion!(request: request, peer: peer, role: role) if operation == "launch_prompt_completion"
|
|
61
86
|
if operation == "observe_execution_scope"
|
|
62
87
|
raise ArgumentError, "scope observation mutation ID must be null" unless request.fetch("mutation_id").nil?
|
|
63
88
|
return {data: observe_execution_scope!(params: params, peer: peer, role: role), replayed: false}
|
|
@@ -112,7 +137,15 @@ module Ace
|
|
|
112
137
|
@kernel.live!(peer)
|
|
113
138
|
journal = journal_for(map)
|
|
114
139
|
digest = Digest::SHA256.hexdigest(JSON.generate(canonical(params)))
|
|
115
|
-
|
|
140
|
+
prepared = if operation == "register_assignment"
|
|
141
|
+
authorize_transfer!(request: request, peer: peer, role: role)
|
|
142
|
+
admitted = admit_prepared_registration!(params, map, transfer)
|
|
143
|
+
digest = Digest::SHA256.hexdigest(JSON.generate(canonical(params.merge("task_context_entry" => admitted.fetch(:task_context_entry)))))
|
|
144
|
+
params = params.merge("definition_bytes" => admitted.fetch(:definition_bytes))
|
|
145
|
+
admitted
|
|
146
|
+
end
|
|
147
|
+
native_start = nil
|
|
148
|
+
outcome = with_exclusion(params, map, journal) do |control_context|
|
|
116
149
|
if operation == "reserve_attempt"
|
|
117
150
|
existing = @mutex.synchronize { journal.mutation_result(request.fetch("mutation_id")) }
|
|
118
151
|
unless existing
|
|
@@ -141,18 +174,22 @@ module Ace
|
|
|
141
174
|
begin
|
|
142
175
|
fresh = false
|
|
143
176
|
released = false
|
|
144
|
-
result =
|
|
177
|
+
result = with_parent_campaign_registration(params, control_context, replay: !replay.nil?) do |campaign_guard|
|
|
178
|
+
journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: attempt_id,
|
|
145
179
|
mutation_id: request.fetch("mutation_id"), operation: operation, parameters_digest: digest,
|
|
146
180
|
expected_generation: operation == "reserve_attempt" ? 0 : (params.fetch("expected_generation") || 0), with_replay: true) do |events, commit, generation|
|
|
147
181
|
fresh = true
|
|
182
|
+
recheck_control_registration!(control_context, params, map, journal, commit: commit)
|
|
183
|
+
campaign_guard&.call(journal, commit)
|
|
148
184
|
plan = case operation
|
|
149
|
-
when "register_assignment" then register(params, map, journal, commit, generation
|
|
185
|
+
when "register_assignment" then register(params, map, journal, commit, generation, prepared: prepared,
|
|
186
|
+
lifecycle_control: control_context.fetch(:selection))
|
|
150
187
|
when "reserve_attempt" then reserve(params, map, journal, commit, generation, peer, attempt_id)
|
|
151
188
|
when "record_launch" then record(params, map, events, peer)
|
|
152
189
|
when "bind_process" then bind(params, map, events, peer)
|
|
153
190
|
when "release_launch"
|
|
154
191
|
released = true
|
|
155
|
-
release(params, map, events, peer)
|
|
192
|
+
release(params, map, events, peer, journal: journal, commit: commit)
|
|
156
193
|
when "abort_launch" then abort(params, map, events, peer, supervisor: role == :supervisor, journal: journal, commit: commit)
|
|
157
194
|
end
|
|
158
195
|
bounded_reply!(plan.fetch(:data), generation + 1)
|
|
@@ -162,6 +199,7 @@ module Ace
|
|
|
162
199
|
reservation_handle ||= @kernel.pin(peer) if operation == "reserve_attempt"
|
|
163
200
|
plan
|
|
164
201
|
end
|
|
202
|
+
end
|
|
165
203
|
fresh = !result.fetch(:replayed)
|
|
166
204
|
result = result.fetch(:data)
|
|
167
205
|
if fresh && operation == "reserve_attempt"
|
|
@@ -173,7 +211,8 @@ module Ace
|
|
|
173
211
|
stream = @streams.fetch(result.fetch("attempt_id"))
|
|
174
212
|
permission = {"operation" => "release", "launch_ticket" => result.fetch("launch_ticket"),
|
|
175
213
|
"attempt_id" => result.fetch("attempt_id"), "assignment_id" => result.fetch("assignment_id"),
|
|
176
|
-
"generation" => result.fetch("generation"), "journal_commit" => result.fetch("journal_commit")
|
|
214
|
+
"generation" => result.fetch("generation"), "journal_commit" => result.fetch("journal_commit"),
|
|
215
|
+
"prepared_input" => result.fetch("prepared_input")}
|
|
177
216
|
begin
|
|
178
217
|
wire.write(stream.fetch(:socket), permission, deadline: wire.deadline(1))
|
|
179
218
|
rescue StandardError
|
|
@@ -198,7 +237,8 @@ module Ace
|
|
|
198
237
|
"mutation_id" => "scope-admission-#{Digest::SHA256.hexdigest(response.dig(:data, 'attempt_id'))[0, 48]}",
|
|
199
238
|
"expected_generation" => parent.dig(:data, "generation"))
|
|
200
239
|
digest = Digest::SHA256.hexdigest(JSON.generate(canonical(admission.reject { |key, _| key == "mutation_id" })))
|
|
201
|
-
admit_native_service_held!(admission, map, journal, peer, role, digest)
|
|
240
|
+
admitted = admit_native_service_held!(admission, map, journal, peer, role, digest)
|
|
241
|
+
native_start = [admission, admitted] unless admitted.fetch(:replayed)
|
|
202
242
|
rescue Ace::Runtime::RuntimeUnavailableError, AttemptErrors::EvidenceUnavailable
|
|
203
243
|
# Reservation is already canonical. A failed/lost parent job is
|
|
204
244
|
# held for exact inspection; replay must not activate it again.
|
|
@@ -211,6 +251,17 @@ module Ace
|
|
|
211
251
|
end
|
|
212
252
|
response
|
|
213
253
|
end
|
|
254
|
+
if native_start
|
|
255
|
+
begin
|
|
256
|
+
complete_native_start!(native_start.first, map, journal, peer, role, native_start.last)
|
|
257
|
+
rescue Ace::Runtime::RuntimeUnavailableError, AttemptErrors::EvidenceUnavailable
|
|
258
|
+
# Canonical admission remains uncertain; original reserve reply is immutable.
|
|
259
|
+
nil
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
outcome
|
|
263
|
+
ensure
|
|
264
|
+
settle_native_issuer!(native_start.first, map) if native_start
|
|
214
265
|
end
|
|
215
266
|
|
|
216
267
|
def gate_ready(request:, peer:, socket:, deadline:)
|
|
@@ -276,27 +327,26 @@ module Ace
|
|
|
276
327
|
id = params.fetch("assignment_id")
|
|
277
328
|
token!(id)
|
|
278
329
|
journal = journal_for(map)
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
task = initial.fetch("task_id")
|
|
282
|
-
token!(task)
|
|
283
|
-
exclusion = exclusion_for(map, journal)
|
|
330
|
+
context = control_registration_context!(params, map, journal)
|
|
331
|
+
exclusion = context.fetch(:exclusion)
|
|
284
332
|
enter = proc do
|
|
285
333
|
@mutex.synchronize do
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
raise AttemptErrors::Conflict, "assignment registration changed while entering lifecycle exclusion"
|
|
289
|
-
end
|
|
290
|
-
yield journal, JSON.parse(JSON.generate(registration))
|
|
334
|
+
recheck_control_registration!(context, params, map, journal)
|
|
335
|
+
yield journal, JSON.parse(JSON.generate(context.fetch(:registration)))
|
|
291
336
|
end
|
|
292
337
|
end
|
|
293
|
-
with_slot(map) do
|
|
338
|
+
with_slot(context.fetch(:map), deployment: context.fetch(:descriptor)) do
|
|
294
339
|
if exclusive
|
|
295
|
-
|
|
296
|
-
|
|
340
|
+
enter_exclusive = lambda do |offset|
|
|
341
|
+
if offset == context.fetch(:keys).size
|
|
342
|
+
enter.call
|
|
343
|
+
else
|
|
344
|
+
exclusion.with_exclusive(context.fetch(:keys).fetch(offset)) { enter_exclusive.call(offset + 1) }
|
|
345
|
+
end
|
|
297
346
|
end
|
|
347
|
+
enter_exclusive.call(0)
|
|
298
348
|
else
|
|
299
|
-
exclusion.with_shared_multi(
|
|
349
|
+
exclusion.with_shared_multi(context.fetch(:keys)) { enter.call }
|
|
300
350
|
end
|
|
301
351
|
end
|
|
302
352
|
end
|
|
@@ -306,11 +356,24 @@ module Ace
|
|
|
306
356
|
@mutex.synchronize do
|
|
307
357
|
@observations.each_value { |observation| close_observation(observation) }
|
|
308
358
|
@observations.clear
|
|
359
|
+
@control_channels.each_value(&:close)
|
|
360
|
+
@control_channels.clear
|
|
361
|
+
@scope_observers.each_value { |observer| observer.close if observer.is_a?(ExecutionScopeObservation) }
|
|
362
|
+
@scope_observers.clear
|
|
309
363
|
end
|
|
310
364
|
end
|
|
311
365
|
|
|
312
366
|
private
|
|
313
367
|
|
|
368
|
+
def protected_descriptor_sha256!
|
|
369
|
+
reference = @deployment.artifact_reference
|
|
370
|
+
unless reference.is_a?(Hash) && reference["sha256"].is_a?(String) &&
|
|
371
|
+
Molecules::ExecutionScopeLineage::DIGEST.match?(reference["sha256"])
|
|
372
|
+
raise AttemptErrors::EvidenceUnavailable, "selected deployment has no protected whole-descriptor provenance"
|
|
373
|
+
end
|
|
374
|
+
reference.fetch("sha256")
|
|
375
|
+
end
|
|
376
|
+
|
|
314
377
|
def scope_observer_for(mapping_id)
|
|
315
378
|
@scope_observers[mapping_id] ||= @scope_observer_factory.call(mapping_id)
|
|
316
379
|
end
|
|
@@ -326,6 +389,11 @@ module Ace
|
|
|
326
389
|
envelope = {"status" => "ok", "data" => data.merge("generation" => generation, "journal_commit" => "0" * 64),
|
|
327
390
|
"transport" => {"replayed" => false}}
|
|
328
391
|
raise ArgumentError, "accepted launch reply exceeds transport bounds" if JSON.generate(envelope).bytesize + 1 > 16_384
|
|
392
|
+
if data.key?("prepared_input")
|
|
393
|
+
permission = data.slice("launch_ticket", "attempt_id", "assignment_id", "prepared_input").merge(
|
|
394
|
+
"operation" => "release", "generation" => generation, "journal_commit" => "0" * 64)
|
|
395
|
+
raise ArgumentError, "accepted release exceeds transport bounds" if JSON.generate(permission).bytesize + 1 > 16_384
|
|
396
|
+
end
|
|
329
397
|
end
|
|
330
398
|
|
|
331
399
|
def close_observation(observation)
|
|
@@ -344,45 +412,67 @@ module Ace
|
|
|
344
412
|
end
|
|
345
413
|
end
|
|
346
414
|
|
|
347
|
-
def exclusion_for(map,
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical lifecycle root is unavailable" unless result.success? && !common.strip.empty?
|
|
352
|
-
Molecules::LifecycleExclusion.new(root: File.join(common.strip, "ace", "lifecycle-exclusion"))
|
|
353
|
-
end
|
|
354
|
-
exclusion
|
|
415
|
+
def exclusion_for(map, _journal, deployment:, selection:)
|
|
416
|
+
@control_exclusion_factory.call(authority: deployment.authority(map.fetch("authority_id")),
|
|
417
|
+
project_id: map.fetch("project_id"), descriptor_sha256: deployment.artifact_reference.fetch("sha256"),
|
|
418
|
+
root_identity: selection&.fetch("root_identity"))
|
|
355
419
|
end
|
|
356
420
|
|
|
357
421
|
def with_exclusion(params, map, journal)
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
422
|
+
context = control_registration_context!(params, map, journal)
|
|
423
|
+
context = campaign_child_context!(params, context, map, journal)
|
|
424
|
+
enter = proc { with_selected_control_exclusion(context, params, map, journal) { yield context } }
|
|
425
|
+
enter.call
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
# Containment/recovery never settles Inbox work or grants fresh input.
|
|
429
|
+
# It retains the same canonical task/slot/assignment exclusions while
|
|
430
|
+
# permitting the proof prerequisites needed to recover unknown effects.
|
|
431
|
+
def with_containment_exclusion(params, map, journal)
|
|
432
|
+
context = control_registration_context!(params, map, journal)
|
|
433
|
+
with_selected_control_exclusion(context, params, map, journal) { yield context }
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
def with_selected_control_exclusion(context, params, map, journal)
|
|
437
|
+
exclusion = context.fetch(:exclusion)
|
|
438
|
+
owners = [context, context[:campaign]&.fetch(:context)].compact
|
|
439
|
+
keys = owners.flat_map { |owner| owner.fetch(:keys) }.uniq
|
|
440
|
+
exclusion.provision_keys!(keys: keys) if params.key?("definition_bytes")
|
|
441
|
+
slots = owners.sort_by { |owner| [owner.fetch(:descriptor).authority(owner.fetch(:map).fetch("authority_id")).fetch("state_root"), owner.fetch(:map).fetch("execution_scope").fetch("slot_id")] }
|
|
442
|
+
.uniq { |owner| [owner.fetch(:descriptor).authority(owner.fetch(:map).fetch("authority_id")).fetch("state_root"), owner.fetch(:map).fetch("execution_scope").fetch("slot_id")] }
|
|
443
|
+
enter_slot = lambda do |offset, &body|
|
|
444
|
+
return body.call if offset == slots.size
|
|
445
|
+
owner = slots.fetch(offset)
|
|
446
|
+
with_slot(owner.fetch(:map), deployment: owner.fetch(:descriptor)) { enter_slot.call(offset + 1, &body) }
|
|
447
|
+
end
|
|
448
|
+
enter_slot.call(0) do
|
|
449
|
+
exclusion.with_shared_multi(keys) do
|
|
450
|
+
@mutex.synchronize { recheck_control_registration!(context, params, map, journal) }
|
|
451
|
+
yield context
|
|
452
|
+
end
|
|
364
453
|
end
|
|
365
|
-
token!(task)
|
|
366
|
-
keys = [exclusion.task_key(task), exclusion.assignment_key(params.fetch("assignment_id"))]
|
|
367
|
-
with_slot(map) { exclusion.with_shared_multi(keys) { yield } }
|
|
368
454
|
end
|
|
369
455
|
|
|
370
456
|
# Slot ownership spans assignment chains and survives authority restart
|
|
371
457
|
# in the existing canonical journal. The file lock only serializes a
|
|
372
458
|
# fresh read/CAS or bounded manager action; its contents convey no facts.
|
|
373
|
-
def with_slot(map)
|
|
374
|
-
service =
|
|
459
|
+
def with_slot(map, deployment: @deployment, deadline: nil)
|
|
460
|
+
service = deployment.authority(map.fetch("authority_id"))
|
|
375
461
|
scope = map.fetch("execution_scope")
|
|
376
|
-
exclusion = @slot_exclusions[
|
|
462
|
+
exclusion = @slot_exclusions[service.fetch("state_root")] ||= Molecules::LifecycleExclusion.new(
|
|
377
463
|
root: File.join(service.fetch("state_root"), "execution-slot-exclusion"))
|
|
378
|
-
key = [object_id,
|
|
464
|
+
key = [object_id, service.fetch("state_root"), exclusion.slot_key(scope.fetch("slot_id"))]
|
|
379
465
|
held = Thread.current[:ace_assign_scope_exclusions] ||= {}
|
|
380
466
|
raise AttemptErrors::Conflict, "execution slot exclusion cannot be entered recursively" if held[key]
|
|
381
|
-
exclusion.with_exclusive(exclusion.slot_key(scope.fetch("slot_id"))) do
|
|
467
|
+
exclusion.with_exclusive(exclusion.slot_key(scope.fetch("slot_id")), deadline: deadline) do
|
|
382
468
|
held[key] = true
|
|
469
|
+
snapshots = Thread.current[:ace_assign_history_operations] ||= {}
|
|
470
|
+
owns_snapshot = !snapshots.key?(object_id)
|
|
471
|
+
snapshots[object_id] = {} if owns_snapshot
|
|
383
472
|
begin
|
|
384
473
|
yield
|
|
385
474
|
ensure
|
|
475
|
+
snapshots.delete(object_id) if owns_snapshot
|
|
386
476
|
held.delete(key)
|
|
387
477
|
end
|
|
388
478
|
end
|
|
@@ -426,27 +516,62 @@ module Ace
|
|
|
426
516
|
token!(params.fetch("attempt_id"))
|
|
427
517
|
end
|
|
428
518
|
|
|
429
|
-
def
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
519
|
+
def admit_prepared_registration!(params, map, transfer)
|
|
520
|
+
entry = TaskContextEntry.validate!(map.fetch("task_context_entry"))
|
|
521
|
+
raise AttemptErrors::MalformedTransfer, "Prepared registration transfer unavailable" unless transfer && transfer.count == 1
|
|
522
|
+
descriptor = params.fetch("transfer")
|
|
523
|
+
bytes = transfer.bytes
|
|
524
|
+
work = PreparedWork.admit(bytes: bytes, head: params.fetch("prepared_head"), tree: params.fetch("prepared_tree"),
|
|
525
|
+
sha256: descriptor.fetch("sha256"), size: descriptor.fetch("bytes"), root: @deployment.project(map.fetch("project_id")).fetch("candidate_root"))
|
|
526
|
+
unless work.manifest.values_at("assignment_id", "project_id") == [params.fetch("assignment_id"), map.fetch("project_id")] &&
|
|
527
|
+
work.manifest_sha256 == params.fetch("manifest_sha256")
|
|
528
|
+
raise ArgumentError, "prepared_input_mismatch: registration association"
|
|
529
|
+
end
|
|
530
|
+
definition_bytes = work.definition_bytes(head: params.fetch("prepared_head"), tree: params.fetch("prepared_tree"))
|
|
531
|
+
unless Digest::SHA256.hexdigest(definition_bytes) == params.fetch("definition_digest")
|
|
532
|
+
raise ArgumentError, "prepared_input_mismatch: derived definition digest"
|
|
434
533
|
end
|
|
534
|
+
{work: work, bytes: bytes.freeze, definition_bytes: definition_bytes,
|
|
535
|
+
task_context_entry: immutable_maintenance_projection(entry)}.freeze
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def register(params, map, journal, commit, generation, prepared:, lifecycle_control:)
|
|
539
|
+
assignment = validated_registration_definition!(params, map)
|
|
540
|
+
bytes = params.fetch("definition_bytes")
|
|
435
541
|
value = JSON.parse(bytes)
|
|
436
|
-
allowed = %w[session_id name description created_at updated_at source_config parent task_id project_id]
|
|
437
|
-
unless value.is_a?(Hash) && (value.keys - allowed).empty? &&
|
|
438
|
-
%w[session_id name created_at source_config task_id project_id].all? { |key| value[key].is_a?(String) && !value[key].empty? } &&
|
|
439
|
-
value["session_id"] == params["assignment_id"] && value["project_id"] == map["project_id"]
|
|
440
|
-
raise ArgumentError, "invalid managed assignment definition"
|
|
441
|
-
end
|
|
442
|
-
assignment = Models::Assignment.from_h(value)
|
|
443
|
-
raise ArgumentError, "definition is not managed" unless assignment.managed?
|
|
444
542
|
previous = definition(journal, params.fetch("assignment_id"), commit: commit)
|
|
543
|
+
if previous
|
|
544
|
+
original = inventory_definition!(journal, commit,
|
|
545
|
+
{selector: {"assignment_id" => params.fetch("assignment_id"), "attempt_id" => nil}, registration: previous})
|
|
546
|
+
if original.campaign_execution &&
|
|
547
|
+
[assignment.campaign_execution, assignment.parent, assignment.task_id, assignment.project_id] !=
|
|
548
|
+
[original.campaign_execution, original.parent, original.task_id, original.project_id]
|
|
549
|
+
raise AttemptErrors::Conflict, "registered campaign child linkage is immutable"
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
if previous && previous.fetch("lifecycle_control") != lifecycle_control
|
|
553
|
+
raise AttemptErrors::Conflict, "same assignment cannot replace original lifecycle control"
|
|
554
|
+
end
|
|
555
|
+
if previous && previous["definition_digest"] == params["definition_digest"] &&
|
|
556
|
+
previous["task_context_entry"] != prepared.fetch(:task_context_entry)
|
|
557
|
+
raise AttemptErrors::Conflict, "same prepared definition cannot replace original task context entry"
|
|
558
|
+
end
|
|
445
559
|
if previous && previous["definition_digest"] != params["definition_digest"] && active_events?(journal.read_events(params.fetch("assignment_id")))
|
|
446
560
|
raise AttemptErrors::Conflict, "active assignment definition cannot change"
|
|
447
561
|
end
|
|
562
|
+
if previous && previous["definition_digest"] == params["definition_digest"] &&
|
|
563
|
+
previous.values_at("prepared_bundle_bytes", "prepared_bundle_sha256") != [prepared.fetch(:bytes).bytesize, Digest::SHA256.hexdigest(prepared.fetch(:bytes))]
|
|
564
|
+
raise AttemptErrors::Conflict, "same prepared definition cannot replace original bundle bytes"
|
|
565
|
+
end
|
|
448
566
|
path = "execution/definitions/#{params.fetch('assignment_id')}-#{params.fetch('definition_digest')}.json"
|
|
449
|
-
|
|
567
|
+
bundle = prepared.fetch(:bytes)
|
|
568
|
+
bundle_sha = Digest::SHA256.hexdigest(bundle)
|
|
569
|
+
bundle_path = "execution/prepared/#{params.fetch('assignment_id')}-#{bundle_sha}.bundle"
|
|
570
|
+
{events: [], blobs: {path => bytes, bundle_path => bundle}, data: {"prepared_work" => value.fetch("prepared_work"),
|
|
571
|
+
"lifecycle_control" => lifecycle_control,
|
|
572
|
+
"task_context_entry" => prepared.fetch(:task_context_entry),
|
|
573
|
+
"prepared_bundle_ref" => bundle_path, "prepared_bundle_bytes" => bundle.bytesize, "prepared_bundle_sha256" => bundle_sha,
|
|
574
|
+
"selection_sha256" => prepared.fetch(:work).selection_sha256, "assignment_id" => params.fetch("assignment_id"),
|
|
450
575
|
"project_id" => map.fetch("project_id"), "mapping_id" => params.fetch("mapping_id"),
|
|
451
576
|
"phase" => "registered", "definition_ref" => path, "definition_digest" => params.fetch("definition_digest"),
|
|
452
577
|
"definition_generation" => previous && previous["definition_digest"] == params["definition_digest"] ? previous.fetch("definition_generation") : generation + 1, "task_id" => assignment.task_id}}
|
|
@@ -460,6 +585,9 @@ module Ace
|
|
|
460
585
|
raise AttemptErrors::UnauthorizedIdentity, "reservation identity differs"
|
|
461
586
|
end
|
|
462
587
|
scope = Atoms::AssignmentScope.canonicalize(params.fetch("scope"))
|
|
588
|
+
unless registration.dig("prepared_work", "scope") == scope && registration.dig("prepared_work", "task_id") == registration["task_id"]
|
|
589
|
+
raise AttemptErrors::Conflict, "reservation differs from prepared selection"
|
|
590
|
+
end
|
|
463
591
|
ensure_slot_available!(map, journal)
|
|
464
592
|
events = journal.read_events(params.fetch("assignment_id"))
|
|
465
593
|
events.group_by { |event| event["attempt_id"] }.each_value do |chain|
|
|
@@ -472,6 +600,11 @@ module Ace
|
|
|
472
600
|
end
|
|
473
601
|
head = params.fetch("base_head")
|
|
474
602
|
raise ArgumentError, "invalid base revision" unless head.is_a?(String) && head.match?(/\A[0-9a-f]{40}\z/)
|
|
603
|
+
registered_definition = inventory_definition!(journal, commit,
|
|
604
|
+
{selector: {"assignment_id" => params.fetch("assignment_id"), "attempt_id" => nil}, registration: registration})
|
|
605
|
+
if registered_definition.campaign_execution && head != registered_definition.campaign_execution.fetch("base")
|
|
606
|
+
raise AttemptErrors::Conflict, "campaign child base differs from pinned parent candidate"
|
|
607
|
+
end
|
|
475
608
|
ticket = SecureRandom.hex(24)
|
|
476
609
|
payload = {"assignment_id" => params.fetch("assignment_id"), "scope" => scope,
|
|
477
610
|
"project_id" => map.fetch("project_id"), "task_id" => registration.fetch("task_id"),
|
|
@@ -479,6 +612,7 @@ module Ace
|
|
|
479
612
|
"launcher_identity" => peer, "launch_ticket" => ticket}
|
|
480
613
|
provisioning = {"slot_id" => map.fetch("execution_scope").fetch("slot_id"),
|
|
481
614
|
"deployment_digest" => Digest::SHA256.hexdigest(JSON.generate(canonical(map))),
|
|
615
|
+
"descriptor_sha256" => protected_descriptor_sha256!,
|
|
482
616
|
"reservation_generation" => generation + 1}
|
|
483
617
|
{events: [{type: "intent", payload: payload}, {type: "scope_provisioning", payload: provisioning}], blobs: {}, data: payload.merge(
|
|
484
618
|
"attempt_id" => attempt_id, "phase" => "reserved", "mapping_id" => params.fetch("mapping_id"),
|
|
@@ -486,6 +620,17 @@ module Ace
|
|
|
486
620
|
end
|
|
487
621
|
|
|
488
622
|
def ensure_slot_available!(map, journal, commit: journal.ref_value)
|
|
623
|
+
if @deployment_history
|
|
624
|
+
protected = maintenance_journal_for(@deployment, map)
|
|
625
|
+
unless protected.ref_value == commit
|
|
626
|
+
raise AttemptErrors::Conflict, "historical admission canonical ref changed"
|
|
627
|
+
end
|
|
628
|
+
return with_maintenance_inbox_inventory(@deployment_history.candidate) do
|
|
629
|
+
maintenance_slot_lineages!(nil, protected, commit, selected_context: [@deployment, map])
|
|
630
|
+
raise AttemptErrors::Conflict, "historical admission canonical ref changed" unless protected.ref_value == commit
|
|
631
|
+
true
|
|
632
|
+
end
|
|
633
|
+
end
|
|
489
634
|
slot = map.fetch("execution_scope").fetch("slot_id")
|
|
490
635
|
journal.assignment_ids(commit: commit).each do |assignment_id|
|
|
491
636
|
journal.read_events(assignment_id, commit: commit).group_by { |event| event.fetch("attempt_id") }.each do |attempt_id, chain|
|
|
@@ -503,11 +648,15 @@ module Ace
|
|
|
503
648
|
raise AttemptErrors::EvidenceUnavailable, "canonical slot provisioning identity is incomplete"
|
|
504
649
|
end
|
|
505
650
|
owner = provisioning.first.fetch("payload")
|
|
506
|
-
unless owner.is_a?(Hash) && owner.keys.sort == %w[deployment_digest reservation_generation slot_id] &&
|
|
651
|
+
unless owner.is_a?(Hash) && owner.keys.sort == %w[deployment_digest descriptor_sha256 reservation_generation slot_id] &&
|
|
507
652
|
owner["reservation_generation"] == reservation.fetch("reservation_generation") &&
|
|
653
|
+
owner["descriptor_sha256"].is_a?(String) && Molecules::ExecutionScopeLineage::DIGEST.match?(owner["descriptor_sha256"]) &&
|
|
508
654
|
owner["deployment_digest"].is_a?(String) && Molecules::ExecutionScopeLineage::DIGEST.match?(owner["deployment_digest"])
|
|
509
655
|
raise AttemptErrors::EvidenceUnavailable, "canonical slot provisioning identity differs"
|
|
510
656
|
end
|
|
657
|
+
unless owner.fetch("descriptor_sha256") == protected_descriptor_sha256!
|
|
658
|
+
raise AttemptErrors::EvidenceUnavailable, "historical descriptor requires authenticated maintenance before normal slot reuse"
|
|
659
|
+
end
|
|
511
660
|
prior_map = @deployment.mapping(reservation.fetch("mapping_id"))
|
|
512
661
|
unless prior_map.fetch("execution_scope").fetch("slot_id") == owner.fetch("slot_id") &&
|
|
513
662
|
prior_map.fetch("project_id") == reservation.fetch("project_id")
|
|
@@ -538,6 +687,8 @@ module Ace
|
|
|
538
687
|
raise AttemptErrors::Conflict, "launch cannot record another child" unless state["phase"] == "reserved"
|
|
539
688
|
binding = validate_binding!(params.fetch("process_binding"), map, mapping_id: params.fetch("mapping_id"), launch_ticket: params.fetch("launch_ticket"), assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"), events: events)
|
|
540
689
|
child = binding.fetch("process_identity")
|
|
690
|
+
guard = Ace::Herdr::Molecules::GuardedNativeOrigin.verify!(params.fetch("guarded_origin"),
|
|
691
|
+
terminal_id: binding.fetch("terminal_id"), child: child)
|
|
541
692
|
@kernel.live!(child)
|
|
542
693
|
if observation[:child_handle]
|
|
543
694
|
unless @kernel.same?(observation.fetch(:child), child)
|
|
@@ -547,7 +698,7 @@ module Ace
|
|
|
547
698
|
observation[:child_handle] = @kernel.pin(child)
|
|
548
699
|
observation[:child] = child
|
|
549
700
|
end
|
|
550
|
-
{events: [], blobs: {}, data: state.merge("phase" => "recorded", "process_binding" => binding)}
|
|
701
|
+
{events: [], blobs: {}, data: state.merge("phase" => "recorded", "process_binding" => binding, "guarded_origin" => guard)}
|
|
551
702
|
end
|
|
552
703
|
|
|
553
704
|
def bind(params, map, events, peer)
|
|
@@ -568,7 +719,7 @@ module Ace
|
|
|
568
719
|
{events: [{type: "scope_child_bound", payload: child}, {type: "process_start", payload: payload}], blobs: {}, data: state.merge("phase" => "bound")}
|
|
569
720
|
end
|
|
570
721
|
|
|
571
|
-
def release(params, map, events, peer)
|
|
722
|
+
def release(params, map, events, peer, journal:, commit:)
|
|
572
723
|
state, observation = owned(params, events, peer)
|
|
573
724
|
unless state["phase"] == "bound" && state["process_binding"] == params["process_binding"] && @streams.key?(state.fetch("attempt_id"))
|
|
574
725
|
raise AttemptErrors::Conflict, "exact live bound gate is unavailable"
|
|
@@ -578,7 +729,9 @@ module Ace
|
|
|
578
729
|
raise AttemptErrors::Conflict, "canonical original child differs" unless original == params.fetch("process_binding")
|
|
579
730
|
@kernel.live!(observation.fetch(:child))
|
|
580
731
|
raise AttemptErrors::Conflict, "launcher has exited" unless launcher_live?(observation)
|
|
581
|
-
|
|
732
|
+
prepared = original_bound_prepared_registration!(journal: journal, params: params, commit: commit)
|
|
733
|
+
{events: [], blobs: {}, data: state.merge("phase" => "issued", "execution" => "potentially_executed",
|
|
734
|
+
"prepared_input" => compact_prepared_input(prepared))}
|
|
582
735
|
end
|
|
583
736
|
|
|
584
737
|
def abort(params, map, events, peer, supervisor: false, journal:, commit:)
|
|
@@ -678,7 +831,7 @@ module Ace
|
|
|
678
831
|
end
|
|
679
832
|
|
|
680
833
|
def definition(journal, id, commit: journal.ref_value)
|
|
681
|
-
journal.read_events(id).reverse.filter_map do |event|
|
|
834
|
+
journal.read_events(id, commit: commit).reverse.filter_map do |event|
|
|
682
835
|
next unless event["type"] == "authority_mutation" && event.dig("payload", "operation") == "register_assignment"
|
|
683
836
|
event.dig("payload", "data")
|
|
684
837
|
end.first
|
|
@@ -747,31 +900,47 @@ module Ace
|
|
|
747
900
|
map = @deployment.mapping(params.fetch("mapping_id"))
|
|
748
901
|
token!(params.fetch("assignment_id")); token!(params.fetch("attempt_id"))
|
|
749
902
|
journal = journal_for(map)
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
if %i[launcher supervisor].include?(role)
|
|
760
|
-
projection.merge!(state.slice("process_binding", "launcher_identity", "mapping_id"))
|
|
761
|
-
end
|
|
762
|
-
observation = @observations[params.fetch("attempt_id")]
|
|
763
|
-
expired = !%w[issued failed].include?(state["phase"]) && state["handshake_deadline"] && Time.iso8601(state.fetch("handshake_deadline")) <= Time.now.utc
|
|
764
|
-
child_exited = observation && observation[:child_handle] &&
|
|
765
|
-
observation[:child] == state.dig("process_binding", "process_identity") && @kernel.exited?(observation.fetch(:child_handle))
|
|
766
|
-
if !TERMINAL.include?(state["phase"]) && (!observation || !launcher_live?(observation) || expired || child_exited)
|
|
767
|
-
projection.merge!("phase" => "uncertain", "required_action" => "supervisor_inspect_exact_child_and_release_uncertainty")
|
|
768
|
-
end
|
|
769
|
-
if @result_owner
|
|
903
|
+
with_containment_exclusion(params, map, journal) do
|
|
904
|
+
@mutex.synchronize do
|
|
905
|
+
commit = journal.ref_value
|
|
906
|
+
events = journal.read_events(params.fetch("assignment_id"), commit: commit)
|
|
907
|
+
state = states(events)[params.fetch("attempt_id")]
|
|
908
|
+
raise AttemptErrors::NotFound, "launch not found" unless state && state["mapping_id"] == params["mapping_id"]
|
|
909
|
+
if role == :launcher && !@kernel.same?(state.fetch("launcher_identity"), peer)
|
|
910
|
+
raise AttemptErrors::UnauthorizedIdentity, "launch belongs to another launcher incarnation"
|
|
911
|
+
end
|
|
770
912
|
chain = events.select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
771
|
-
projection.
|
|
772
|
-
|
|
913
|
+
projection = state.slice("assignment_id", "attempt_id", "phase", "scope", "launch_ticket", "reservation_generation", "generation", "execution", "required_action", "handshake_deadline")
|
|
914
|
+
projection.merge!("state" => journal.canonical_attempt_state(chain), "generation" => journal.authority_generation(chain))
|
|
915
|
+
if %i[launcher supervisor].include?(role)
|
|
916
|
+
projection.merge!(state.slice("process_binding", "launcher_identity", "mapping_id"))
|
|
917
|
+
original = original_prompt_record!(chain, state, params: params) if chain.any? { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "record_launch" }
|
|
918
|
+
projection.merge!("original_binding_digest" => original&.fetch("binding_digest"), "terminal_event_id" => nil, "reservation_release_event_id" => nil)
|
|
919
|
+
if TERMINAL.include?(projection.fetch("state"))
|
|
920
|
+
lineage = Molecules::ExecutionScopeLineage.new(events: chain, project_id: map.fetch("project_id"),
|
|
921
|
+
assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"), mapping_id: params.fetch("mapping_id"))
|
|
922
|
+
released = scope_reservation_released?(chain, lineage, journal, commit)
|
|
923
|
+
if released
|
|
924
|
+
release = chain.find { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "scope_reservation_release" }
|
|
925
|
+
projection.merge!("terminal_event_id" => release.dig("payload", "data", "terminal_event_id"), "reservation_release_event_id" => release.fetch("digest"))
|
|
926
|
+
else
|
|
927
|
+
projection["terminal_event_id"] = terminal_scope_receipt!(chain, lineage, journal, commit).fetch("digest")
|
|
928
|
+
end
|
|
929
|
+
end
|
|
930
|
+
end
|
|
931
|
+
observation = @observations[params.fetch("attempt_id")]
|
|
932
|
+
expired = !%w[issued failed].include?(state["phase"]) && state["handshake_deadline"] && Time.iso8601(state.fetch("handshake_deadline")) <= Time.now.utc
|
|
933
|
+
child_exited = observation && observation[:child_handle] &&
|
|
934
|
+
observation[:child] == state.dig("process_binding", "process_identity") && @kernel.exited?(observation.fetch(:child_handle))
|
|
935
|
+
if !TERMINAL.include?(state["phase"]) && (!observation || !launcher_live?(observation) || expired || child_exited)
|
|
936
|
+
projection.merge!("phase" => "uncertain", "required_action" => "supervisor_inspect_exact_child_and_release_uncertainty")
|
|
937
|
+
end
|
|
938
|
+
if @result_owner
|
|
939
|
+
projection.merge!(@result_owner.result_status(journal: journal, events: chain, params: params,
|
|
940
|
+
map: map, peer: peer, role: role, commit: commit))
|
|
941
|
+
end
|
|
942
|
+
projection.merge("journal_commit" => commit)
|
|
773
943
|
end
|
|
774
|
-
projection.merge("journal_commit" => commit)
|
|
775
944
|
end
|
|
776
945
|
end
|
|
777
946
|
|
|
@@ -779,16 +948,9 @@ module Ace
|
|
|
779
948
|
events.group_by { |event| event["attempt_id"] }.values.any? { |chain| chain.any? { |event| event["type"] == "intent" } && !terminal_events?(chain) }
|
|
780
949
|
end
|
|
781
950
|
def terminal_events?(events)
|
|
782
|
-
|
|
783
|
-
case event["type"]
|
|
784
|
-
when "process_start" then "running"
|
|
785
|
-
when "transition" then event.dig("payload", "to")
|
|
786
|
-
when "receipt_accepted" then event.dig("payload", "receipt", "verdict")
|
|
787
|
-
when "reconciliation" then event.dig("payload", "resolution")
|
|
788
|
-
end
|
|
789
|
-
end.first
|
|
790
|
-
TERMINAL.include?(latest)
|
|
951
|
+
TERMINAL.include?(Molecules::CanonicalAttemptState.derive(events))
|
|
791
952
|
end
|
|
953
|
+
|
|
792
954
|
def strict!(value, keys)
|
|
793
955
|
raise ArgumentError, "authority fields differ" unless value.is_a?(Hash) && value.keys.sort == keys.sort
|
|
794
956
|
end
|
|
@@ -820,3 +982,16 @@ require_relative "launch_scope_admission"
|
|
|
820
982
|
require_relative "launch_scope_close"
|
|
821
983
|
require_relative "launch_scope_release"
|
|
822
984
|
require_relative "launch_scope_parent"
|
|
985
|
+
|
|
986
|
+
require_relative "launch_steering"
|
|
987
|
+
require_relative "launch_input_inhibition"
|
|
988
|
+
|
|
989
|
+
require_relative "launch_stop"
|
|
990
|
+
require_relative "assignment_inventory"
|
|
991
|
+
require_relative "launch_prepared_work"
|
|
992
|
+
|
|
993
|
+
require_relative "launch_review"
|
|
994
|
+
require_relative "launch_attempt_consumers"
|
|
995
|
+
require_relative "launch_control_exclusions"
|
|
996
|
+
|
|
997
|
+
require_relative "launch_campaign_registration"
|