ace-assign 0.64.0 → 0.66.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 (129) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +11 -0
  3. data/CHANGELOG.md +19 -0
  4. data/docs/protected-authority.md +23 -7
  5. data/docs/usage.md +117 -4
  6. data/exe/ace-scope-ready +55 -0
  7. data/handbook/workflow-instructions/assign/drive.wf.md +26 -1
  8. data/lib/ace/assign/atoms/attempt_state_machine.rb +8 -0
  9. data/lib/ace/assign/atoms/delivery_parameters.rb +4 -39
  10. data/lib/ace/assign/authority/assignment_inventory.rb +290 -0
  11. data/lib/ace/assign/authority/campaign_consumer_policy.rb +100 -0
  12. data/lib/ace/assign/authority/campaign_execution.rb +119 -0
  13. data/lib/ace/assign/authority/campaign_round_transfer.rb +82 -0
  14. data/lib/ace/assign/authority/candidate_transfer.rb +169 -2
  15. data/lib/ace/assign/authority/cleanup_result_protection.rb +53 -0
  16. data/lib/ace/assign/authority/client.rb +156 -6
  17. data/lib/ace/assign/authority/deployment.rb +201 -39
  18. data/lib/ace/assign/authority/deployment_history.rb +146 -0
  19. data/lib/ace/assign/authority/endcap.rb +131 -18
  20. data/lib/ace/assign/authority/endcap_attempts.rb +274 -0
  21. data/lib/ace/assign/authority/endcap_campaign_results.rb +232 -0
  22. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +266 -0
  23. data/lib/ace/assign/authority/endcap_contexts.rb +23 -0
  24. data/lib/ace/assign/authority/endcap_inboxes.rb +3 -280
  25. data/lib/ace/assign/authority/endcap_prepared_work.rb +80 -0
  26. data/lib/ace/assign/authority/endcap_publication.rb +207 -0
  27. data/lib/ace/assign/authority/endcap_results.rb +36 -18
  28. data/lib/ace/assign/authority/endcap_review_requests.rb +183 -0
  29. data/lib/ace/assign/authority/endcap_reviews.rb +130 -0
  30. data/lib/ace/assign/authority/endcap_service_settlement.rb +152 -0
  31. data/lib/ace/assign/authority/endcap_services.rb +221 -18
  32. data/lib/ace/assign/authority/execution_scope_observation.rb +468 -41
  33. data/lib/ace/assign/authority/inbox_context_original.rb +113 -0
  34. data/lib/ace/assign/authority/launch_attempt_consumers.rb +51 -0
  35. data/lib/ace/assign/authority/launch_campaign_registration.rb +214 -0
  36. data/lib/ace/assign/authority/launch_control_channel.rb +313 -0
  37. data/lib/ace/assign/authority/launch_control_exclusions.rb +141 -0
  38. data/lib/ace/assign/authority/launch_driver.rb +298 -19
  39. data/lib/ace/assign/authority/launch_input_inhibition.rb +132 -0
  40. data/lib/ace/assign/authority/launch_lifecycle.rb +275 -100
  41. data/lib/ace/assign/authority/launch_prepared_work.rb +122 -0
  42. data/lib/ace/assign/authority/launch_review.rb +91 -0
  43. data/lib/ace/assign/authority/launch_scope_abort.rb +0 -7
  44. data/lib/ace/assign/authority/launch_scope_admission.rb +136 -5
  45. data/lib/ace/assign/authority/launch_scope_close.rb +68 -37
  46. data/lib/ace/assign/authority/launch_scope_release.rb +696 -17
  47. data/lib/ace/assign/authority/launch_steering.rb +353 -0
  48. data/lib/ace/assign/authority/launch_stop.rb +254 -0
  49. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +229 -0
  50. data/lib/ace/assign/authority/original_inbox_selection.rb +41 -0
  51. data/lib/ace/assign/authority/posix_acl.rb +5 -2
  52. data/lib/ace/assign/authority/prepared_input.rb +130 -0
  53. data/lib/ace/assign/authority/prepared_queue.rb +185 -0
  54. data/lib/ace/assign/authority/prepared_task_context.rb +116 -0
  55. data/lib/ace/assign/authority/prepared_work.rb +304 -0
  56. data/lib/ace/assign/authority/prepared_worker.rb +69 -0
  57. data/lib/ace/assign/authority/protected_assignment_context.rb +226 -0
  58. data/lib/ace/assign/authority/readiness_hook.rb +120 -0
  59. data/lib/ace/assign/authority/router.rb +24 -2
  60. data/lib/ace/assign/authority/server.rb +103 -33
  61. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +88 -0
  62. data/lib/ace/assign/authority/service_cleanup_evidence.rb +180 -0
  63. data/lib/ace/assign/authority/service_delivery_evidence.rb +101 -0
  64. data/lib/ace/assign/authority/service_evidence.rb +243 -12
  65. data/lib/ace/assign/authority/service_publication_evidence.rb +110 -0
  66. data/lib/ace/assign/authority/task_context_entry.rb +32 -0
  67. data/lib/ace/assign/authority/transfer_codec.rb +44 -12
  68. data/lib/ace/assign/cli/commands/assignment_target.rb +15 -2
  69. data/lib/ace/assign/cli/commands/attempt/base.rb +49 -0
  70. data/lib/ace/assign/cli/commands/attempt/finish.rb +20 -4
  71. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +11 -0
  72. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +25 -0
  73. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +28 -0
  74. data/lib/ace/assign/cli/commands/authority/launch.rb +58 -6
  75. data/lib/ace/assign/cli/commands/authority/serve.rb +2 -1
  76. data/lib/ace/assign/cli/commands/authority/status.rb +1 -1
  77. data/lib/ace/assign/cli/commands/authority/task_context.rb +31 -0
  78. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +25 -0
  79. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +30 -0
  80. data/lib/ace/assign/cli/commands/authority/worker.rb +23 -0
  81. data/lib/ace/assign/cli/commands/campaign_export_result.rb +55 -0
  82. data/lib/ace/assign/cli/commands/campaign_record_round.rb +57 -0
  83. data/lib/ace/assign/cli/commands/create.rb +2 -0
  84. data/lib/ace/assign/cli/commands/delivery.rb +54 -4
  85. data/lib/ace/assign/cli/commands/fail.rb +6 -0
  86. data/lib/ace/assign/cli/commands/finish.rb +23 -2
  87. data/lib/ace/assign/cli/commands/fork_session.rb +6 -2
  88. data/lib/ace/assign/cli/commands/inbox_bind.rb +34 -0
  89. data/lib/ace/assign/cli/commands/protected_submission.rb +61 -0
  90. data/lib/ace/assign/cli/commands/resume.rb +12 -0
  91. data/lib/ace/assign/cli/commands/select.rb +2 -0
  92. data/lib/ace/assign/cli/commands/start.rb +2 -0
  93. data/lib/ace/assign/cli/commands/status.rb +4 -2
  94. data/lib/ace/assign/cli/commands/step.rb +3 -1
  95. data/lib/ace/assign/cli/commands/submit_candidate.rb +34 -0
  96. data/lib/ace/assign/cli/commands/submit_result.rb +61 -0
  97. data/lib/ace/assign/cli.rb +36 -5
  98. data/lib/ace/assign/errors.rb +139 -0
  99. data/lib/ace/assign/models/assignment.rb +26 -4
  100. data/lib/ace/assign/models/evidence_event.rb +3 -3
  101. data/lib/ace/assign/molecules/assignment_manager.rb +12 -2
  102. data/lib/ace/assign/molecules/attempt_reconciler.rb +20 -0
  103. data/lib/ace/assign/molecules/canonical_attempt_state.rb +151 -0
  104. data/lib/ace/assign/molecules/canonical_evidence.rb +5 -2
  105. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +374 -0
  106. data/lib/ace/assign/molecules/evidence_journal.rb +720 -44
  107. data/lib/ace/assign/molecules/execution_scope_lineage.rb +27 -9
  108. data/lib/ace/assign/molecules/fork_session_launcher.rb +16 -6
  109. data/lib/ace/assign/molecules/journal_input_inhibition.rb +78 -0
  110. data/lib/ace/assign/molecules/journal_mutation.rb +82 -13
  111. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +241 -0
  112. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +178 -26
  113. data/lib/ace/assign/molecules/original_launch_binding.rb +37 -0
  114. data/lib/ace/assign/molecules/protected_control_exclusion.rb +303 -0
  115. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +330 -0
  116. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +164 -0
  117. data/lib/ace/assign/molecules/queue_scanner.rb +26 -11
  118. data/lib/ace/assign/molecules/receipt_verifier.rb +21 -15
  119. data/lib/ace/assign/molecules/step_writer.rb +27 -5
  120. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +88 -0
  121. data/lib/ace/assign/organisms/assignment_executor.rb +4 -4
  122. data/lib/ace/assign/organisms/attempt_coordinator.rb +66 -76
  123. data/lib/ace/assign/organisms/prepared_work_builder.rb +194 -0
  124. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +100 -0
  125. data/lib/ace/assign/organisms/task_assignment_creator.rb +4 -2
  126. data/lib/ace/assign/version.rb +1 -1
  127. data/lib/ace/assign.rb +1 -112
  128. metadata +88 -9
  129. 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 definition_bytes definition_digest expected_generation],
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
- OPERATIONS = (MUTATIONS.keys + %w[launch_preflight registration_status attempt_status inspect_launch observe_execution_scope close_execution_scope]).freeze
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, :exclusions
30
- def initialize(deployment:, kernel: Ace::Runtime::Molecules::ProtectedLinux.new, journals: nil, mutex: Mutex.new, exclusions: {}, scope_observer_factory: nil)
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
- @exclusions = exclusions
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
- with_exclusion(params, map, journal) do
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 = journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: attempt_id,
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
- initial = definition(journal, id)
280
- raise AttemptErrors::NotFound, "canonical assignment registration is missing" unless initial
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
- registration = definition(journal, id)
287
- unless registration && registration.fetch("task_id") == task
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
- exclusion.with_exclusive(exclusion.task_key(task)) do
296
- exclusion.with_exclusive(exclusion.assignment_key(id)) { enter.call }
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([exclusion.task_key(task), exclusion.assignment_key(id)]) { enter.call }
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, journal)
348
- exclusion = @exclusions[map.fetch("project_id")] ||= begin
349
- common, error, result = Open3.capture3("git", "-C", journal.repo_root,
350
- "rev-parse", "--path-format=absolute", "--git-common-dir", stdin_data: "")
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
- exclusion = exclusion_for(map, journal)
359
- registration = definition(journal, params.fetch("assignment_id"))
360
- task = if params.key?("definition_bytes")
361
- JSON.parse(params.fetch("definition_bytes")).fetch("task_id")
362
- else
363
- registration&.fetch("task_id")
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 = @deployment.authority(map.fetch("authority_id"))
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[map.fetch("authority_id")] ||= Molecules::LifecycleExclusion.new(
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, map.fetch("authority_id"), scope.fetch("slot_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 register(params, map, journal, commit, generation)
430
- bytes = params.fetch("definition_bytes")
431
- unless bytes.is_a?(String) && bytes.bytesize <= 32_768 && params["definition_digest"].is_a?(String) &&
432
- Digest::SHA256.hexdigest(bytes) == params["definition_digest"]
433
- raise ArgumentError, "invalid assignment definition digest or size"
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
- {events: [], blobs: {path => bytes}, data: {"assignment_id" => params.fetch("assignment_id"),
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
- {events: [], blobs: {}, data: state.merge("phase" => "issued", "execution" => "potentially_executed")}
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
- @mutex.synchronize do
751
- commit = journal.ref_value
752
- events = journal.read_events(params.fetch("assignment_id"), commit: commit)
753
- state = states(events)[params.fetch("attempt_id")]
754
- raise AttemptErrors::NotFound, "launch not found" unless state && state["mapping_id"] == params["mapping_id"]
755
- if role == :launcher && !@kernel.same?(state.fetch("launcher_identity"), peer)
756
- raise AttemptErrors::UnauthorizedIdentity, "launch belongs to another launcher incarnation"
757
- end
758
- projection = state.slice("assignment_id", "attempt_id", "phase", "scope", "launch_ticket", "reservation_generation", "generation", "execution", "required_action", "handshake_deadline")
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.merge!(@result_owner.result_status(journal: journal, events: chain, params: params,
772
- map: map, peer: peer, role: role, commit: commit))
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
- latest = events.reverse.filter_map do |event|
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"