ace-assign 0.63.1 → 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 (147) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +11 -0
  3. data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +2 -2
  4. data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
  5. data/.ace-defaults/assign/catalog/steps/release-minor.step.yml +4 -2
  6. data/.ace-defaults/assign/catalog/steps/release.step.yml +4 -2
  7. data/.ace-defaults/assign/catalog/steps/update-pr-desc.step.yml +1 -1
  8. data/.ace-defaults/assign/presets/fix-bug.yml +15 -3
  9. data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +16 -4
  10. data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +15 -3
  11. data/.ace-defaults/assign/presets/work-on-task.yml +16 -4
  12. data/CHANGELOG.md +62 -0
  13. data/docs/demo/canonical-skill-source.tape.yml +4 -4
  14. data/docs/protected-authority.md +282 -0
  15. data/docs/usage.md +205 -3
  16. data/exe/ace-scope-ready +55 -0
  17. data/handbook/workflow-instructions/assign/drive.wf.md +28 -3
  18. data/handbook/workflow-instructions/assign/prepare.wf.md +2 -2
  19. data/lib/ace/assign/atoms/attempt_state_machine.rb +8 -0
  20. data/lib/ace/assign/atoms/delivery_parameters.rb +19 -0
  21. data/lib/ace/assign/authority/assignment_inventory.rb +290 -0
  22. data/lib/ace/assign/authority/campaign_consumer_policy.rb +100 -0
  23. data/lib/ace/assign/authority/campaign_execution.rb +119 -0
  24. data/lib/ace/assign/authority/campaign_round_transfer.rb +82 -0
  25. data/lib/ace/assign/authority/candidate_transfer.rb +327 -0
  26. data/lib/ace/assign/authority/cleanup_result_protection.rb +53 -0
  27. data/lib/ace/assign/authority/client.rb +257 -0
  28. data/lib/ace/assign/authority/deployment.rb +627 -0
  29. data/lib/ace/assign/authority/deployment_history.rb +146 -0
  30. data/lib/ace/assign/authority/endcap.rb +455 -0
  31. data/lib/ace/assign/authority/endcap_attempts.rb +274 -0
  32. data/lib/ace/assign/authority/endcap_campaign_results.rb +232 -0
  33. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +266 -0
  34. data/lib/ace/assign/authority/endcap_contexts.rb +23 -0
  35. data/lib/ace/assign/authority/endcap_inboxes.rb +37 -0
  36. data/lib/ace/assign/authority/endcap_prepared_work.rb +80 -0
  37. data/lib/ace/assign/authority/endcap_publication.rb +207 -0
  38. data/lib/ace/assign/authority/endcap_results.rb +398 -0
  39. data/lib/ace/assign/authority/endcap_review_requests.rb +183 -0
  40. data/lib/ace/assign/authority/endcap_reviews.rb +130 -0
  41. data/lib/ace/assign/authority/endcap_service_settlement.rb +152 -0
  42. data/lib/ace/assign/authority/endcap_services.rb +598 -0
  43. data/lib/ace/assign/authority/evidence_transfer.rb +67 -0
  44. data/lib/ace/assign/authority/execution_scope_observation.rb +903 -0
  45. data/lib/ace/assign/authority/inbox_context_original.rb +113 -0
  46. data/lib/ace/assign/authority/launch_attempt_consumers.rb +51 -0
  47. data/lib/ace/assign/authority/launch_campaign_registration.rb +214 -0
  48. data/lib/ace/assign/authority/launch_control_channel.rb +313 -0
  49. data/lib/ace/assign/authority/launch_control_exclusions.rb +141 -0
  50. data/lib/ace/assign/authority/launch_driver.rb +393 -0
  51. data/lib/ace/assign/authority/launch_input_inhibition.rb +132 -0
  52. data/lib/ace/assign/authority/launch_lifecycle.rb +997 -0
  53. data/lib/ace/assign/authority/launch_prepared_work.rb +122 -0
  54. data/lib/ace/assign/authority/launch_review.rb +91 -0
  55. data/lib/ace/assign/authority/launch_scope_abort.rb +81 -0
  56. data/lib/ace/assign/authority/launch_scope_admission.rb +215 -0
  57. data/lib/ace/assign/authority/launch_scope_close.rb +146 -0
  58. data/lib/ace/assign/authority/launch_scope_parent.rb +44 -0
  59. data/lib/ace/assign/authority/launch_scope_release.rb +859 -0
  60. data/lib/ace/assign/authority/launch_steering.rb +353 -0
  61. data/lib/ace/assign/authority/launch_stop.rb +254 -0
  62. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +229 -0
  63. data/lib/ace/assign/authority/original_inbox_selection.rb +41 -0
  64. data/lib/ace/assign/authority/posix_acl.rb +59 -0
  65. data/lib/ace/assign/authority/prepared_input.rb +130 -0
  66. data/lib/ace/assign/authority/prepared_queue.rb +185 -0
  67. data/lib/ace/assign/authority/prepared_task_context.rb +116 -0
  68. data/lib/ace/assign/authority/prepared_work.rb +304 -0
  69. data/lib/ace/assign/authority/prepared_worker.rb +69 -0
  70. data/lib/ace/assign/authority/private_directory.rb +33 -0
  71. data/lib/ace/assign/authority/protected_assignment_context.rb +226 -0
  72. data/lib/ace/assign/authority/readiness_hook.rb +120 -0
  73. data/lib/ace/assign/authority/receipt_transfer.rb +48 -0
  74. data/lib/ace/assign/authority/router.rb +76 -0
  75. data/lib/ace/assign/authority/server.rb +300 -0
  76. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +88 -0
  77. data/lib/ace/assign/authority/service_cleanup_evidence.rb +180 -0
  78. data/lib/ace/assign/authority/service_delivery_evidence.rb +101 -0
  79. data/lib/ace/assign/authority/service_evidence.rb +286 -0
  80. data/lib/ace/assign/authority/service_publication_evidence.rb +110 -0
  81. data/lib/ace/assign/authority/task_context_entry.rb +32 -0
  82. data/lib/ace/assign/authority/transfer_codec.rb +187 -0
  83. data/lib/ace/assign/cli/commands/assignment_target.rb +15 -2
  84. data/lib/ace/assign/cli/commands/attempt/base.rb +49 -0
  85. data/lib/ace/assign/cli/commands/attempt/finish.rb +20 -4
  86. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +11 -0
  87. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +25 -0
  88. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +28 -0
  89. data/lib/ace/assign/cli/commands/authority/launch.rb +97 -0
  90. data/lib/ace/assign/cli/commands/authority/serve.rb +31 -0
  91. data/lib/ace/assign/cli/commands/authority/status.rb +26 -0
  92. data/lib/ace/assign/cli/commands/authority/task_context.rb +31 -0
  93. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +25 -0
  94. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +30 -0
  95. data/lib/ace/assign/cli/commands/authority/terminate.rb +29 -0
  96. data/lib/ace/assign/cli/commands/authority/worker.rb +23 -0
  97. data/lib/ace/assign/cli/commands/campaign_export_result.rb +55 -0
  98. data/lib/ace/assign/cli/commands/campaign_record_round.rb +57 -0
  99. data/lib/ace/assign/cli/commands/create.rb +11 -1
  100. data/lib/ace/assign/cli/commands/delivery.rb +107 -0
  101. data/lib/ace/assign/cli/commands/fail.rb +6 -0
  102. data/lib/ace/assign/cli/commands/finish.rb +23 -2
  103. data/lib/ace/assign/cli/commands/fork_session.rb +6 -2
  104. data/lib/ace/assign/cli/commands/inbox_bind.rb +34 -0
  105. data/lib/ace/assign/cli/commands/protected_submission.rb +61 -0
  106. data/lib/ace/assign/cli/commands/resume.rb +38 -0
  107. data/lib/ace/assign/cli/commands/select.rb +2 -0
  108. data/lib/ace/assign/cli/commands/start.rb +2 -0
  109. data/lib/ace/assign/cli/commands/status.rb +4 -2
  110. data/lib/ace/assign/cli/commands/step.rb +3 -1
  111. data/lib/ace/assign/cli/commands/submit_candidate.rb +34 -0
  112. data/lib/ace/assign/cli/commands/submit_result.rb +61 -0
  113. data/lib/ace/assign/cli.rb +56 -4
  114. data/lib/ace/assign/errors.rb +139 -0
  115. data/lib/ace/assign/models/assignment.rb +26 -4
  116. data/lib/ace/assign/models/evidence_event.rb +4 -2
  117. data/lib/ace/assign/molecules/assignment_manager.rb +12 -2
  118. data/lib/ace/assign/molecules/attempt_reconciler.rb +51 -12
  119. data/lib/ace/assign/molecules/canonical_attempt_state.rb +151 -0
  120. data/lib/ace/assign/molecules/canonical_evidence.rb +153 -0
  121. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +374 -0
  122. data/lib/ace/assign/molecules/evidence_journal.rb +922 -91
  123. data/lib/ace/assign/molecules/execution_identity_resolver.rb +49 -9
  124. data/lib/ace/assign/molecules/execution_scope_lineage.rb +345 -0
  125. data/lib/ace/assign/molecules/fork_session_launcher.rb +16 -6
  126. data/lib/ace/assign/molecules/journal_input_inhibition.rb +78 -0
  127. data/lib/ace/assign/molecules/journal_mutation.rb +271 -0
  128. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +241 -0
  129. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +182 -26
  130. data/lib/ace/assign/molecules/original_launch_binding.rb +37 -0
  131. data/lib/ace/assign/molecules/proposal_journal.rb +151 -0
  132. data/lib/ace/assign/molecules/protected_control_exclusion.rb +303 -0
  133. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +330 -0
  134. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +164 -0
  135. data/lib/ace/assign/molecules/queue_scanner.rb +26 -11
  136. data/lib/ace/assign/molecules/receipt_verifier.rb +68 -32
  137. data/lib/ace/assign/molecules/step_writer.rb +27 -5
  138. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +88 -0
  139. data/lib/ace/assign/organisms/assignment_executor.rb +10 -4
  140. data/lib/ace/assign/organisms/attempt_coordinator.rb +302 -13
  141. data/lib/ace/assign/organisms/delivery_coordinator.rb +342 -0
  142. data/lib/ace/assign/organisms/prepared_work_builder.rb +194 -0
  143. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +100 -0
  144. data/lib/ace/assign/organisms/task_assignment_creator.rb +11 -4
  145. data/lib/ace/assign/version.rb +1 -1
  146. data/lib/ace/assign.rb +4 -102
  147. metadata +186 -8
@@ -0,0 +1,859 @@
1
+ # frozen_string_literal: true
2
+ require_relative "../molecules/terminal_scope_receipt"
3
+ require_relative "maintenance_workspace_retirement"
4
+ require "ace/runtime/molecules/execution_boot_baseline"
5
+ require "ace/herdr/molecules/delivery_record_store"
6
+
7
+ module Ace
8
+ module Assign
9
+ module Authority
10
+ class LaunchLifecycle
11
+ # Original Installer preparation under its operator lock/inhibition.
12
+ # This is not a wire operation or a maintenance eligibility grant.
13
+ def prepare_execution_journals!(candidate_deployment:, initializer:)
14
+ if Thread.current[:ace_assign_maintenance_contexts]&.key?(object_id)
15
+ raise AttemptErrors::Conflict, "canonical preparation cannot run inside maintenance"
16
+ end
17
+ unless @deployment_history && @deployment_history.selects?(@deployment, selection: :original) &&
18
+ @deployment_history.selects?(candidate_deployment, selection: :candidate)
19
+ raise AttemptErrors::EvidenceUnavailable, "canonical preparation requires exact protected history"
20
+ end
21
+ raise AttemptErrors::EvidenceUnavailable, "canonical preparation child is unavailable" unless initializer.respond_to?(:call)
22
+ snapshots = {}
23
+ @deployment.maintenance_inventory(candidate_deployment).each do |id, owner, map|
24
+ project = owner.project(map.fetch("project_id"))
25
+ service = owner.authority(map.fetch("authority_id"))
26
+ identity = project.values_at("journal_repository", "evidence_git_ref", "evidence_checkout_root")
27
+ verify_maintenance_root!(service.fetch("state_root"), service.fetch("uid"))
28
+ identity.values_at(0, 2).each { |root| verify_maintenance_root!(root, service.fetch("uid")) }
29
+ next if snapshots.key?(identity)
30
+ journal = maintenance_journal_for(owner, map)
31
+ unless [journal.repo_root, journal.ref, journal.checkout_root] == identity
32
+ raise AttemptErrors::EvidenceUnavailable, "canonical preparation journal differs from fixed descriptor"
33
+ end
34
+ # The original operator's fixed accepted-source child drops to
35
+ # this selected authority before all Ruby/Git mutation. Its reply
36
+ # is only a selector; independently read the configured journal.
37
+ value = initializer.call(mapping_id: id, descriptor_sha256: owner.artifact_reference.fetch("sha256"))
38
+ unless value.is_a?(String) && value.match?(/\A[0-9a-f]{40}\z/) && journal.ref_value == value
39
+ raise AttemptErrors::EvidenceUnavailable, "canonical preparation child reply differs from selected journal"
40
+ end
41
+ journal.verify_commit!(value)
42
+ journal.canonical_event_inventory!(commit: value)
43
+ raise AttemptErrors::EvidenceUnavailable, "canonical preparation journal changed" unless journal.ref_value == value
44
+ snapshots[identity.freeze] = value.dup.freeze
45
+ end
46
+ snapshots.freeze
47
+ end
48
+
49
+ # Source-only maintenance entry. Transport stop/drain/inhibition belongs
50
+ # to the trusted installer; this method supplies the exact same locks
51
+ # and canonical owner query, without a live endpoint or another ledger.
52
+ def with_execution_slots(mapping_ids:, candidate_deployment:, deadline: nil, &block)
53
+ maintenance_deadline!(deadline) unless deadline.nil?
54
+ if Thread.current[:ace_assign_maintenance_contexts]&.key?(object_id)
55
+ raise AttemptErrors::Conflict, "maintenance inventory cannot be entered recursively"
56
+ end
57
+ unless block && mapping_ids.is_a?(Array) && mapping_ids.size.between?(1, 256) && mapping_ids.uniq == mapping_ids
58
+ raise ArgumentError, "fixed execution slot selections must be unique and bounded"
59
+ end
60
+ mapping_ids.each { |id| token!(id) }
61
+ if @deployment_history && !@deployment_history.selects?(@deployment, selection: :original)
62
+ raise AttemptErrors::EvidenceUnavailable, "maintenance requires original protected transaction descriptor"
63
+ end
64
+ if @deployment_history && !@deployment_history.selects?(candidate_deployment, selection: :candidate)
65
+ raise AttemptErrors::EvidenceUnavailable, "maintenance candidate differs from protected history transaction"
66
+ end
67
+ inventory = @deployment.maintenance_inventory(candidate_deployment)
68
+ missing = mapping_ids - inventory.map(&:first)
69
+ raise ArgumentError, "maintenance mapping is unavailable" unless missing.empty?
70
+ # Installer publication cannot omit a removed or added fixed root.
71
+ changed = (@deployment.data.fetch("launch_mappings").keys | candidate_deployment.data.fetch("launch_mappings").keys).select do |id|
72
+ original = @deployment.data.fetch("launch_mappings")[id]
73
+ candidate = candidate_deployment.data.fetch("launch_mappings")[id]
74
+ original != candidate || (original && candidate &&
75
+ (@deployment.project(original.fetch("project_id")) != candidate_deployment.project(candidate.fetch("project_id")) ||
76
+ @deployment.authority(original.fetch("authority_id")) != candidate_deployment.authority(candidate.fetch("authority_id"))))
77
+ end
78
+ unless (changed - mapping_ids).empty?
79
+ raise ArgumentError, "maintenance selection omits changed original/candidate mappings"
80
+ end
81
+ selected = inventory.select { |id, _owner, _map| mapping_ids.include?(id) }
82
+ locks = selected.sort_by do |_id, owner, map|
83
+ [owner.authority(map.fetch("authority_id")).fetch("state_root"),
84
+ "execution-slot:#{map.fetch('execution_scope').fetch('slot_id')}"]
85
+ end
86
+ locks.each do |_id, owner, map|
87
+ service = owner.authority(map.fetch("authority_id"))
88
+ verify_maintenance_root!(service.fetch("state_root"), service.fetch("uid"))
89
+ end
90
+ enter = lambda do |offset|
91
+ if offset < locks.size
92
+ _id, owner, map = locks.fetch(offset)
93
+ with_slot(map, deployment: owner, deadline: deadline) { enter.call(offset + 1) }
94
+ else
95
+ with_maintenance_mutex(deadline) do
96
+ active = Thread.current[:ace_assign_maintenance_contexts] ||= {}
97
+ raise AttemptErrors::Conflict, "maintenance inventory cannot be entered recursively" if active[object_id]
98
+ snapshots = {}
99
+ contexts = selected.map do |id, owner, map|
100
+ project = owner.project(map.fetch("project_id"))
101
+ identity = project.values_at("journal_repository", "evidence_git_ref", "evidence_checkout_root")
102
+ journal, commit = snapshots.fetch(identity) do
103
+ service = owner.authority(map.fetch("authority_id"))
104
+ identity.values_at(0, 2).each { |root| verify_maintenance_root!(root, service.fetch("uid")) }
105
+ journal = maintenance_journal_for(owner, map)
106
+ commit = journal.ref_value
107
+ unless commit.is_a?(String) && commit.match?(/\A[0-9a-f]{40}\z/)
108
+ raise AttemptErrors::EvidenceUnavailable, "maintenance canonical commit is unavailable"
109
+ end
110
+ unless [journal.repo_root, journal.ref, journal.checkout_root] == identity
111
+ raise AttemptErrors::EvidenceUnavailable, "maintenance journal differs from fixed descriptor"
112
+ end
113
+ journal.verify_commit!(commit)
114
+ journal.assignment_ids(commit: commit)
115
+ snapshots[identity] = [journal, commit.dup.freeze]
116
+ end
117
+ {mapping_id: id.dup.freeze, journal: journal, commit: commit}.freeze
118
+ end.freeze
119
+ active[object_id] = selected.zip(contexts).to_h { |entry, context| [context.fetch(:mapping_id), [entry, context]] }
120
+ begin
121
+ with_maintenance_inbox_inventory(candidate_deployment, deadline: deadline) do
122
+ maintenance_deadline!(deadline) unless deadline.nil?
123
+ block.call(contexts)
124
+ end
125
+ ensure
126
+ active.delete(object_id)
127
+ end
128
+ end
129
+ end
130
+ end
131
+ enter.call(0)
132
+ end
133
+
134
+ def slot_reusable!(mapping_id:, journal:, commit:)
135
+ if Thread.current[:ace_assign_maintenance_contexts]&.key?(object_id)
136
+ require_maintenance_context!(mapping_id, journal, commit)
137
+ maintenance_slot_lineages!(mapping_id, journal, commit)
138
+ require_maintenance_context!(mapping_id, journal, commit)
139
+ return true
140
+ end
141
+ map = @deployment.mapping(mapping_id)
142
+ require_slot_snapshot!(map, journal, commit)
143
+ ensure_slot_available!(map, journal, commit: commit)
144
+ require_slot_snapshot!(map, journal, commit)
145
+ true
146
+ end
147
+
148
+ # Original proofs for the trusted maintenance consumer, never fresh
149
+ # pointer selection. Entries remain evidence after this block; only
150
+ # its held complete inventory grants a live maintenance operation.
151
+ # Empty history supplies no authority over an unknown host instance.
152
+ def maintenance_boot_baselines!(mapping_id:, journal:, commit:)
153
+ require_maintenance_context!(mapping_id, journal, commit)
154
+ contexts = Thread.current[:ace_assign_maintenance_contexts].fetch(object_id)
155
+ contexts.each_value { |_entry, context| slot_reusable!(**context) }
156
+ entries = maintenance_slot_lineages!(mapping_id, journal, commit).map do |lineage|
157
+ binding = lineage.binding
158
+ events = journal.read_events(binding.fetch("assignment_id"), commit: commit).select do |event|
159
+ event.fetch("attempt_id") == binding.fetch("attempt_id") && event["type"] == "scope_provisioning"
160
+ end
161
+ unless events.one?
162
+ raise AttemptErrors::EvidenceUnavailable, "maintenance original descriptor selection is ambiguous"
163
+ end
164
+ immutable_maintenance_projection(binding.slice("project_id", "mapping_id", "assignment_id", "attempt_id",
165
+ "scope_generation", "boot_id", "slot_id", "deployment_digest", "boot_baseline_selection",
166
+ "network_installation_selection", "network_namespace_identity").merge(
167
+ "descriptor_sha256" => events.first.fetch("payload").fetch("descriptor_sha256"),
168
+ "scope_binding_event_id" => lineage.binding_event.fetch("digest"), "proof_id" => lineage.proof_id,
169
+ "journal_commit" => commit, "baseline" => verify_historical_boot_baseline!(binding)))
170
+ end.freeze
171
+ contexts.each_value do |_entry, context|
172
+ require_maintenance_context!(context.fetch(:mapping_id), context.fetch(:journal), context.fetch(:commit))
173
+ end
174
+ entries
175
+ end
176
+
177
+ # A target observation is authoritative only inside the same complete
178
+ # held maintenance transaction. Historical owner/map selection comes
179
+ # from the original provisioning fact, never the current deployment.
180
+ def maintenance_workspace_target!(mapping_id:, journal:, commit:, assignment_id:, attempt_id:,
181
+ binding_event_digest:, release_event_digest:, descriptor_sha256:, source_commit: commit)
182
+ require_maintenance_context!(mapping_id, journal, commit)
183
+ contexts = Thread.current[:ace_assign_maintenance_contexts].fetch(object_id)
184
+ contexts.each_value { |_entry, context| slot_reusable!(**context) }
185
+ result = immutable_workspace_target_projection!(mapping_id: mapping_id, journal: journal, commit: commit,
186
+ assignment_id: assignment_id, attempt_id: attempt_id, binding_event_digest: binding_event_digest,
187
+ release_event_digest: release_event_digest, descriptor_sha256: descriptor_sha256, source_commit: source_commit)
188
+ contexts.each_value do |_entry, context|
189
+ require_maintenance_context!(context.fetch(:mapping_id), context.fetch(:journal), context.fetch(:commit))
190
+ end
191
+ result
192
+ rescue KeyError, TypeError, ArgumentError, Ace::Runtime::RuntimeUnavailableError
193
+ raise AttemptErrors::EvidenceUnavailable, "maintenance workspace original identity is incomplete"
194
+ end
195
+
196
+ # This source capability exists only within the complete original and
197
+ # candidate maintenance exclusions. The fixed adapter already owns EX.
198
+ def with_workspace_retirement!(mapping_id:, journal:, commit:, target:, evidence_owner:, deadline:)
199
+ maintenance_deadline!(deadline)
200
+ selectors = retirement_target_selectors!(mapping_id, target)
201
+ projection = maintenance_workspace_target!(mapping_id: mapping_id, journal: journal, commit: commit, **selectors)
202
+ verify_retirement_target!(target, projection)
203
+ contexts = Thread.current[:ace_assign_maintenance_contexts].fetch(object_id)
204
+ session = MaintenanceWorkspaceRetirement::Session.open!(lifecycle: self, mapping_id: mapping_id,
205
+ journal: journal, commit: commit, target: target, projection: projection, evidence_owner: evidence_owner,
206
+ deadline: deadline, contexts: contexts)
207
+ yield session
208
+ ensure
209
+ session&.close!
210
+ end
211
+
212
+ def maintenance_workspace_readback!(mapping_id:, journal:, commit:, target:, evidence_owner:, deadline:)
213
+ maintenance_deadline!(deadline)
214
+ require_maintenance_context!(mapping_id, journal, commit)
215
+ projection = immutable_workspace_target_projection!(mapping_id: mapping_id, journal: journal, commit: commit,
216
+ **retirement_target_selectors!(mapping_id, target))
217
+ verify_retirement_target!(target, projection)
218
+ evidence_owner.with_retirement_evidence!(phase: "completed", target: target, projection: projection, deadline: deadline) do |evidence|
219
+ verify = -> {
220
+ maintenance_deadline!(deadline)
221
+ require_maintenance_context!(mapping_id, journal, commit)
222
+ unless evidence.is_a?(MaintenanceWorkspaceRetirement::Evidence)
223
+ raise AttemptErrors::EvidenceUnavailable, "retirement readback requires source-owned evidence"
224
+ end
225
+ evidence.verify!(owner: evidence_owner, target: target, projection: projection, phase: "completed", deadline: deadline)
226
+ }
227
+ evidence.with_consumption(verify) do
228
+ verify_retirement_contexts!(evidence: evidence, deadline: deadline)
229
+ result = yield projection, evidence
230
+ verify_retirement_contexts!(evidence: evidence, deadline: deadline)
231
+ result
232
+ end
233
+ ensure
234
+ evidence.expire! if evidence.is_a?(MaintenanceWorkspaceRetirement::Evidence)
235
+ end
236
+ end
237
+
238
+ def retire_released_parent!(mapping_id:, journal:, commit:)
239
+ if (contexts = Thread.current[:ace_assign_maintenance_contexts]&.[](object_id))
240
+ # The method itself enforces the whole transaction preflight. A
241
+ # caller cannot validate one root then retire before checking others.
242
+ contexts.each_value { |_entry, context| slot_reusable!(**context) }
243
+ require_maintenance_context!(mapping_id, journal, commit)
244
+ _id, owner, _map = contexts.fetch(mapping_id).first
245
+ lineages = maintenance_slot_lineages!(mapping_id, journal, commit)
246
+ result = maintenance_scope_observer_for(owner, mapping_id).retire_released_parent!(lineages)
247
+ contexts.each_value { |_entry, context| require_maintenance_context!(context.fetch(:mapping_id), context.fetch(:journal), context.fetch(:commit)) }
248
+ return result
249
+ end
250
+ slot_reusable!(mapping_id: mapping_id, journal: journal, commit: commit)
251
+ map = @deployment.mapping(mapping_id)
252
+ if @deployment_history
253
+ protected = maintenance_journal_for(@deployment, map)
254
+ return with_maintenance_inbox_inventory(@deployment_history.candidate) do
255
+ lineages = maintenance_slot_lineages!(mapping_id, protected, commit, selected_context: [@deployment, map])
256
+ result = scope_observer_for(mapping_id).retire_released_parent!(lineages)
257
+ require_slot_snapshot!(map, journal, commit)
258
+ result
259
+ end
260
+ end
261
+ slot = map.fetch("execution_scope").fetch("slot_id")
262
+ lineages = journal.assignment_ids(commit: commit).flat_map do |assignment_id|
263
+ journal.read_events(assignment_id, commit: commit).group_by { |event| event.fetch("attempt_id") }.filter_map do |attempt_id, events|
264
+ reservation = events.find { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "reserve_attempt" }
265
+ next unless reservation
266
+ prior_id = reservation.dig("payload", "data", "mapping_id")
267
+ next unless @deployment.mapping(prior_id).fetch("execution_scope").fetch("slot_id") == slot
268
+ Molecules::ExecutionScopeLineage.new(events: events, project_id: map.fetch("project_id"),
269
+ assignment_id: assignment_id, attempt_id: attempt_id, mapping_id: prior_id)
270
+ end
271
+ end
272
+ result = scope_observer_for(mapping_id).retire_released_parent!(lineages)
273
+ require_slot_snapshot!(map, journal, commit)
274
+ result
275
+ end
276
+
277
+ # Current connected release producer is the guarded pre-release abort.
278
+ # Finish/stop owners will call the same settlement/proof core in their
279
+ # terminal CAS; those consumer handlers are not this task's ownership.
280
+ def release_scope_reservation!(params:, peer:, role:)
281
+ strict!(params, %w[mapping_id assignment_id attempt_id mutation_id expected_generation])
282
+ %w[assignment_id attempt_id mutation_id].each { |key| token!(params.fetch(key)) }
283
+ generation!(params.fetch("expected_generation"))
284
+ @kernel.live!(peer)
285
+ map = @deployment.mapping(params.fetch("mapping_id"))
286
+ journal = journal_for(map)
287
+ digest = Digest::SHA256.hexdigest(JSON.generate(canonical(params.reject { |key, _| key == "mutation_id" })))
288
+ with_exclusion(params, map, journal) do
289
+ release_scope_reservation_held!(params, map, journal, peer, role, digest)
290
+ end
291
+ end
292
+
293
+ private
294
+
295
+ def retirement_target_selectors!(mapping_id, target)
296
+ unless target.is_a?(Hash) && target.keys.sort == MaintenanceWorkspaceRetirement::TARGET_FIELDS.sort &&
297
+ target["mapping_id"] == mapping_id
298
+ raise AttemptErrors::EvidenceUnavailable, "retirement target is not the exact closed original selection"
299
+ end
300
+ {assignment_id: target.fetch("assignment_id"), attempt_id: target.fetch("attempt_id"),
301
+ binding_event_digest: target.fetch("binding_event_digest"), release_event_digest: target.fetch("release_event_digest"),
302
+ descriptor_sha256: target.fetch("descriptor_sha256"), source_commit: target.fetch("journal_commit")}
303
+ end
304
+
305
+ def verify_retirement_target!(target, projection)
306
+ expected = projection.slice(*MaintenanceWorkspaceRetirement::TARGET_FIELDS).merge(
307
+ "resource" => "workspace:#{projection.fetch('project_id')}:#{projection.fetch('mapping_id')}:#{projection.fetch('assignment_id')}")
308
+ raise AttemptErrors::EvidenceUnavailable, "retirement target original identity differs" unless target == expected
309
+ end
310
+
311
+ def verify_retirement_contexts!(evidence:, deadline:)
312
+ contexts = Thread.current[:ace_assign_maintenance_contexts].fetch(object_id)
313
+ contexts.each_value do |_entry, context|
314
+ maintenance_deadline!(deadline)
315
+ require_maintenance_context!(context.fetch(:mapping_id), context.fetch(:journal), context.fetch(:commit))
316
+ snapshot = maintenance_slot_snapshot!(context.fetch(:mapping_id), context.fetch(:journal), context.fetch(:commit))
317
+ maintenance_scope_observer_for(snapshot.fetch(:owner), snapshot.fetch(:mapping_id)).verify_maintenance_closed!(
318
+ snapshot.fetch(:lineages), retirement: evidence)
319
+ require_maintenance_context!(context.fetch(:mapping_id), context.fetch(:journal), context.fetch(:commit))
320
+ end
321
+ maintenance_deadline!(deadline)
322
+ end
323
+
324
+ def with_maintenance_inbox_inventory(candidate, deadline: nil)
325
+ unless @deployment_history && @deployment_history.selects?(candidate, selection: :candidate)
326
+ # No history grant is silently synthesized for arbitrary descriptors.
327
+ return yield
328
+ end
329
+ require "ace/herdr/organisms/inbox"
330
+ roots = (@deployment_history.descriptors + [candidate]).flat_map do |owner|
331
+ owner.data.fetch("projects").values.flat_map do |project|
332
+ project.fetch("inbox_contexts", {}).values.map { |context| context.fetch("deliveries_dir") }
333
+ end
334
+ end.uniq.sort
335
+ roots.each { |root| verify_maintenance_inbox_root!(root) }
336
+ enter = lambda do |offset|
337
+ return yield if offset == roots.size
338
+ Ace::Herdr::Organisms::Inbox.with_retained_records(deliveries_dir: roots.fetch(offset), deadline: deadline) { enter.call(offset + 1) }
339
+ end
340
+ enter.call(0)
341
+ rescue Ace::Herdr::Molecules::DeliveryRecordStore::LockUnavailable
342
+ raise AttemptErrors::MaintenanceBusy, "maintenance retained Inbox exclusion is busy"
343
+ end
344
+
345
+ def with_maintenance_mutex(deadline)
346
+ return @mutex.synchronize { yield } if deadline.nil?
347
+ maintenance_deadline!(deadline)
348
+ unless @mutex.try_lock
349
+ raise AttemptErrors::MaintenanceBusy, "maintenance authority is busy"
350
+ end
351
+ begin
352
+ maintenance_deadline!(deadline)
353
+ yield
354
+ ensure
355
+ @mutex.unlock
356
+ end
357
+ end
358
+
359
+ def maintenance_deadline!(deadline)
360
+ unless (deadline.is_a?(Integer) || deadline.is_a?(Float)) && deadline.finite?
361
+ raise ArgumentError, "maintenance deadline must be finite"
362
+ end
363
+ if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
364
+ raise AttemptErrors::MaintenanceBusy, "maintenance admission deadline expired"
365
+ end
366
+ end
367
+
368
+ def verify_maintenance_inbox_root!(root)
369
+ PrivateDirectory.verify!(root)
370
+ @deployment.send(:verify_inbox_acl!, root, private_leaf: true)
371
+ true
372
+ rescue AttemptErrors::ReceiptRejected, Ace::Runtime::RuntimeUnavailableError
373
+ raise AttemptErrors::EvidenceUnavailable, "maintenance retained inbox root is not protected"
374
+ end
375
+
376
+ # Canonical original facts only. This private projection cannot grant
377
+ # maintenance eligibility or replace current resource/closure checks.
378
+ def immutable_workspace_target_projection!(mapping_id:, journal:, commit:, assignment_id:, attempt_id:,
379
+ binding_event_digest:, release_event_digest:, descriptor_sha256:, source_commit:)
380
+ contexts = Thread.current[:ace_assign_maintenance_contexts].fetch(object_id)
381
+ journal.verify_canonical_prefix!(commit: source_commit, canonical_commit: commit)
382
+ matches = maintenance_slot_snapshot!(mapping_id, journal, source_commit).fetch(:lineages).select do |lineage|
383
+ lineage.binding.values_at("assignment_id", "attempt_id", "mapping_id") == [assignment_id, attempt_id, mapping_id] &&
384
+ lineage.binding_event.fetch("digest") == binding_event_digest
385
+ end
386
+ raise AttemptErrors::EvidenceUnavailable, "maintenance workspace lineage differs" unless matches.one?
387
+ lineage = matches.first
388
+ events = journal.read_events(assignment_id, commit: source_commit).select { |event| event.fetch("attempt_id") == attempt_id }
389
+ provisioning = events.select { |event| event["type"] == "scope_provisioning" }
390
+ releases = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "scope_reservation_release" }
391
+ unless provisioning.one? && releases.one? && releases.first.fetch("digest") == release_event_digest &&
392
+ provisioning.first.dig("payload", "descriptor_sha256") == descriptor_sha256
393
+ raise AttemptErrors::EvidenceUnavailable, "maintenance workspace original selectors differ"
394
+ end
395
+ original = @deployment_history.descriptor!(sha256: descriptor_sha256)
396
+ map = original.mapping(lineage.binding.fetch("mapping_id"))
397
+ repository = map.fetch("workspace_repository_id")
398
+ cleanup_config = map.fetch("workspace_cleanup_config")
399
+ cwd = map.fetch("worker_cwd")
400
+ context_owner = contexts.fetch(mapping_id).first.fetch(1)
401
+ observer_owner = context_owner.artifact_reference.fetch("sha256") == descriptor_sha256 ? context_owner : original
402
+ observer = maintenance_scope_observer_for(observer_owner, lineage.binding.fetch("mapping_id"))
403
+ resource = observer.maintenance_workspace_resource!(lineage)
404
+ declarations = observer.maintenance_parent_resource_declarations!(lineage)
405
+ immutable_maintenance_projection(lineage.binding.slice("project_id", "mapping_id", "assignment_id", "attempt_id").merge(
406
+ "descriptor_sha256" => descriptor_sha256, "journal_commit" => source_commit,
407
+ "binding_event_digest" => binding_event_digest, "release_event_digest" => release_event_digest,
408
+ "proof_event_digest" => lineage.proof_event.fetch("digest"), "worker_cwd" => cwd,
409
+ "workspace_resource" => resource, "workspace_repository_id" => repository, "workspace_cleanup_config" => cleanup_config,
410
+ "workspace_exclusion" => observer.workspace_exclusion_projection!(lineage),
411
+ "parent_resource_declarations" => declarations, "resource_identities" => lineage.binding.fetch("resource_identities"),
412
+ "original_mapping_digest" => original.mapping_digest(lineage.binding.fetch("mapping_id"))))
413
+ end
414
+
415
+ def maintenance_scope_observer_for(owner, mapping_id)
416
+ return scope_observer_for(mapping_id) if owner.equal?(@deployment)
417
+ @maintenance_scope_observers ||= {}
418
+ key = [owner.artifact_reference.fetch("sha256"), mapping_id]
419
+ @maintenance_scope_observers[key] ||= ExecutionScopeObservation.new(mapping_id: mapping_id, deployment: owner, kernel: @kernel)
420
+ end
421
+
422
+ def maintenance_slot_lineages!(mapping_id, journal, commit, selected_context: nil)
423
+ snapshot = maintenance_slot_snapshot!(mapping_id, journal, commit, selected_context: selected_context)
424
+ maintenance_scope_observer_for(snapshot.fetch(:owner), snapshot.fetch(:mapping_id)).verify_maintenance_closed!(snapshot.fetch(:lineages))
425
+ snapshot.fetch(:lineages)
426
+ end
427
+
428
+ # Complete immutable history and settlement authentication, shared only
429
+ # by ordinary fresh eligibility and proof-bound retirement readback.
430
+ def maintenance_slot_snapshot!(mapping_id, journal, commit, selected_context: nil)
431
+ unless @deployment_history
432
+ raise AttemptErrors::EvidenceUnavailable, "complete original authentication requires retained deployment history"
433
+ end
434
+ unless journal.evidence_mode == :protected
435
+ raise AttemptErrors::EvidenceUnavailable, "maintenance requires original protected journal reader"
436
+ end
437
+ if selected_context
438
+ selected_owner, selected_map = selected_context
439
+ mapping_id = selected_owner.data.fetch("launch_mappings").find { |_id, fixed| fixed == selected_map }&.first
440
+ raise AttemptErrors::EvidenceUnavailable, "historical admission map is unavailable" unless mapping_id
441
+ else
442
+ _id, selected_owner, selected_map = Thread.current[:ace_assign_maintenance_contexts].fetch(object_id).fetch(mapping_id).first
443
+ end
444
+ lineages = []
445
+ retained_attempts = []
446
+ journal.assignment_ids(commit: commit).each do |assignment_id|
447
+ journal.read_events(assignment_id, commit: commit).group_by { |event| event.fetch("attempt_id") }.each do |attempt_id, events|
448
+ raise AttemptErrors::EvidenceUnavailable, "maintenance canonical chain corrupt" unless Models::EvidenceEvent.chain_valid?(events)
449
+ reservations = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "reserve_attempt" }
450
+ # A chain with effect work but no permanent reservation is never
451
+ # hidden behind the absence of an attributable slot selector.
452
+ if reservations.empty?
453
+ if events.any? { |event| %w[service_claim service_transition inbox_binding scope_provisioning].include?(event["type"]) }
454
+ raise AttemptErrors::EvidenceUnavailable, "maintenance effect chain lacks reservation"
455
+ end
456
+ next
457
+ end
458
+ provisioning = events.select { |event| event["type"] == "scope_provisioning" }
459
+ unless reservations.one? && provisioning.one?
460
+ raise AttemptErrors::EvidenceUnavailable, "maintenance reservation identity ambiguous"
461
+ end
462
+ reservation = reservations.first.dig("payload", "data")
463
+ identity = provisioning.first.fetch("payload")
464
+ unless identity.is_a?(Hash) && identity.keys.sort == %w[deployment_digest descriptor_sha256 reservation_generation slot_id] &&
465
+ identity["reservation_generation"] == reservation.fetch("reservation_generation")
466
+ raise AttemptErrors::EvidenceUnavailable, "maintenance provisioning selector differs"
467
+ end
468
+ original = @deployment_history.descriptor!(sha256: identity.fetch("descriptor_sha256"))
469
+ original_map = original.mapping(reservation.fetch("mapping_id"))
470
+ unless Digest::SHA256.hexdigest(JSON.generate(canonical(original_map))) == identity.fetch("deployment_digest") &&
471
+ original_map.fetch("project_id") == reservation.fetch("project_id") &&
472
+ original_map.fetch("execution_scope").fetch("slot_id") == identity.fetch("slot_id") &&
473
+ original.project(original_map.fetch("project_id")).values_at("journal_repository", "evidence_git_ref", "evidence_checkout_root") ==
474
+ [journal.repo_root, journal.ref, journal.checkout_root]
475
+ raise AttemptErrors::EvidenceUnavailable, "original descriptor canonical association differs"
476
+ end
477
+ retained_attempts << [assignment_id, attempt_id, events, original, original_map]
478
+ next unless identity.fetch("slot_id") == selected_map.fetch("execution_scope").fetch("slot_id")
479
+ unless original.maintenance_association(original_map) == selected_owner.maintenance_association(selected_map)
480
+ raise AttemptErrors::EvidenceUnavailable, "original physical slot association differs"
481
+ end
482
+ lineage = maintenance_released_lineage!(journal, commit, assignment_id, attempt_id, events, original, original_map)
483
+ lineages << lineage
484
+ end
485
+ end
486
+ selected_attempts = retained_attempts.select { |entry| entry.last.fetch("execution_scope").fetch("slot_id") == selected_map.fetch("execution_scope").fetch("slot_id") }
487
+ maintenance_current_inventory!(journal, commit, selected_attempts, all_attempts: retained_attempts,
488
+ slot_filter: selected_map.fetch("execution_scope").fetch("slot_id"))
489
+ {owner: selected_owner, mapping_id: mapping_id, lineages: lineages.freeze}.freeze
490
+ rescue KeyError, TypeError, ArgumentError
491
+ raise AttemptErrors::EvidenceUnavailable, "maintenance original identity is incomplete"
492
+ end
493
+
494
+ # Only immutable Git introduction verification is shared within one
495
+ # held slot operation. Current records, keys, inventory, closure and
496
+ # canonical ref checks remain fresh on every eligibility query.
497
+ def historical_event_commit!(journal, assignment_id:, event_digest:, commit:)
498
+ memo = Thread.current[:ace_assign_history_operations]&.[](object_id)
499
+ held = Thread.current[:ace_assign_scope_exclusions]&.keys&.any? { |key| key.first == object_id }
500
+ unless memo && held && journal.evidence_mode == :protected
501
+ raise AttemptErrors::EvidenceUnavailable, "historical prefix requires its held protected operation"
502
+ end
503
+ identity = [journal.object_id, journal.repo_root, journal.ref, journal.checkout_root,
504
+ commit, assignment_id, event_digest].map { |value| value.is_a?(String) ? value.dup.freeze : value }.freeze
505
+ memo.fetch(identity) do
506
+ memo[identity] = journal.event_commit!(assignment_id: assignment_id, event_digest: event_digest, commit: commit).dup.freeze
507
+ end
508
+ end
509
+
510
+ # Immutable terminal authentication is also used by read-only inventory
511
+ # outside maintenance exclusions. Only a genuinely held operation may
512
+ # reuse its bounded memo; both paths use the same canonical Git owner.
513
+ def terminal_event_commit!(journal, assignment_id:, event_digest:, commit:)
514
+ memo = Thread.current[:ace_assign_history_operations]&.[](object_id)
515
+ held = Thread.current[:ace_assign_scope_exclusions]&.keys&.any? { |key| key.first == object_id }
516
+ if memo && held && journal.evidence_mode == :protected
517
+ historical_event_commit!(journal, assignment_id: assignment_id, event_digest: event_digest, commit: commit)
518
+ else
519
+ journal.event_commit!(assignment_id: assignment_id, event_digest: event_digest, commit: commit)
520
+ end
521
+ end
522
+
523
+ def immutable_maintenance_projection(value)
524
+ case value
525
+ when Hash then value.to_h { |key, item| [key.dup.freeze, immutable_maintenance_projection(item)] }.freeze
526
+ when Array then value.map { |item| immutable_maintenance_projection(item) }.freeze
527
+ when String then value.dup.freeze
528
+ else value.freeze
529
+ end
530
+ end
531
+
532
+ def verify_historical_boot_baseline!(binding)
533
+ Ace::Runtime::Molecules::ExecutionBootBaseline.new.verify!(
534
+ selection: binding.fetch("boot_baseline_selection"),
535
+ expected: binding.slice("slot_id", "boot_id", "deployment_digest").merge(
536
+ "installer_artifact" => binding.fetch("network_installation_selection").fetch("installer_artifact")))
537
+ rescue Ace::Runtime::RuntimeUnavailableError, KeyError, TypeError, NoMethodError
538
+ raise AttemptErrors::EvidenceUnavailable, "historical original boot proof unavailable"
539
+ end
540
+
541
+ def maintenance_released_lineage!(journal, commit, assignment_id, attempt_id, events, original, map)
542
+ releases = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "scope_reservation_release" }
543
+ raise AttemptErrors::EvidenceUnavailable, "maintenance reservation lacks unique release" unless releases.one?
544
+ release = releases.first
545
+ prefix_commit = historical_event_commit!(journal, assignment_id: assignment_id, event_digest: release.fetch("digest"), commit: commit)
546
+ verify_released_lineage_at_prefix!(journal, prefix_commit, assignment_id, attempt_id, events, original, map)
547
+ end
548
+
549
+ # The immutable release owner is shared by held maintenance and
550
+ # read-only discovery. Each caller obtains the introduction from the
551
+ # existing canonical Git owner; this method grants no maintenance lock.
552
+ def verify_released_lineage_at_prefix!(journal, prefix_commit, assignment_id, attempt_id, events, original, map, terminal_introduction: nil)
553
+ releases = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "scope_reservation_release" }
554
+ raise AttemptErrors::EvidenceUnavailable, "reservation lacks unique release" unless releases.one?
555
+ release = releases.first
556
+ prefix = journal.read_events(assignment_id, commit: prefix_commit).select { |event| event["attempt_id"] == attempt_id }
557
+ unless prefix == events.take(events.index(release) + 1) && events.last == release
558
+ raise AttemptErrors::EvidenceUnavailable, "post-release canonical work or release prefix differs"
559
+ end
560
+ id = release.dig("payload", "data", "mapping_id")
561
+ lineage = Molecules::ExecutionScopeLineage.new(events: prefix, project_id: map.fetch("project_id"),
562
+ assignment_id: assignment_id, attempt_id: attempt_id, mapping_id: id)
563
+ unless lineage.binding && lineage.seal_event && lineage.proof_id
564
+ raise AttemptErrors::EvidenceUnavailable, "historical released parent lacks canonical closed proof"
565
+ end
566
+ verify_historical_boot_baseline!(lineage.binding)
567
+ lineage.require_positive!(scope_generation: lineage.binding.fetch("scope_generation"),
568
+ scope_binding_event_id: lineage.binding_event.fetch("digest"), seal_event_id: lineage.seal_event.fetch("digest"), proof_id: lineage.proof_id)
569
+ terminal = if prefix.any? { |event| event["type"] == "attempt_stopped" }
570
+ if terminal_introduction
571
+ verify_stopped_terminal_at_prefix!(prefix, journal, terminal_introduction, deployment: original)
572
+ else
573
+ terminal_scope_stopped!(prefix, journal, prefix_commit, deployment: original)
574
+ end
575
+ elsif prefix.any? { |event| event["type"] == "receipt_accepted" }
576
+ terminal_scope_receipt!(prefix, lineage, journal, prefix_commit, deployment: original)
577
+ else
578
+ guarded_scope_abort_receipt!(prefix, lineage, journal, prefix_commit)
579
+ end
580
+ expected = {"assignment_id" => assignment_id, "attempt_id" => attempt_id, "mapping_id" => id,
581
+ "scope_generation" => lineage.binding.fetch("scope_generation"), "scope_binding_event_id" => lineage.binding_event.fetch("digest"),
582
+ "proof_id" => lineage.proof_id, "terminal_event_id" => terminal.fetch("digest"), "reservation" => "released"}
583
+ data = release.dig("payload", "data")
584
+ unless data.is_a?(Hash) && data.except("generation") == expected && prefix.index(terminal) < prefix.index(release) &&
585
+ data["generation"] == journal.authority_generation(prefix) &&
586
+ lineage.binding.fetch("deployment_digest") == prefix.find { |event| event["type"] == "scope_provisioning" }.dig("payload", "deployment_digest")
587
+ raise AttemptErrors::EvidenceUnavailable, "historical release parent/terminal binding differs"
588
+ end
589
+ params = expected.slice("assignment_id", "attempt_id", "mapping_id")
590
+ service_events = prefix.any? { |event| %w[service_claim service_transition].include?(event["type"]) }
591
+ if !@result_owner && (service_events ||
592
+ journal.service_request_records(commit: prefix_commit).any? { |record| record.values_at("assignment_id", "attempt_id") == [assignment_id, attempt_id] })
593
+ raise AttemptErrors::EvidenceUnavailable, "historical settlement receipt owner unavailable"
594
+ end
595
+ if @result_owner
596
+ @result_owner.service_settlement_complete!(journal: journal, events: prefix, params: params, map: map, commit: prefix_commit)
597
+ end
598
+ lineage
599
+ end
600
+
601
+ def maintenance_current_inventory!(journal, commit, attempts, all_attempts: nil, slot_filter: nil)
602
+ indexed = attempts.to_h { |entry| [entry.values_at(0, 1), entry] }
603
+ raise AttemptErrors::EvidenceUnavailable, "maintenance attempt attribution conflicts" unless indexed.size == attempts.size
604
+ verified = {}
605
+ verify_attempt = lambda do |entry|
606
+ assignment_id, attempt_id, events, original, map = entry
607
+ verified[[assignment_id, attempt_id]] ||= maintenance_released_lineage!(journal, commit,
608
+ assignment_id, attempt_id, events, original, map)
609
+ end
610
+ journal.service_request_records(commit: commit).each do |record|
611
+ entry = indexed[record.values_at("assignment_id", "attempt_id")]
612
+ if !entry && all_attempts&.any? { |known| known.values_at(0, 1) == record.values_at("assignment_id", "attempt_id") &&
613
+ known.last.fetch("project_id") == record["project_id"] &&
614
+ known[2].find { |event| event.dig("payload", "operation") == "reserve_attempt" }.dig("payload", "data", "mapping_id") == record["mapping_id"] }
615
+ next
616
+ end
617
+ raise AttemptErrors::EvidenceUnavailable, "current service request is unregistered" unless entry
618
+ verify_attempt.call(entry)
619
+ unless record["project_id"] == entry.last.fetch("project_id") &&
620
+ record["mapping_id"] == entry[2].find { |event| event.dig("payload", "operation") == "reserve_attempt" }.dig("payload", "data", "mapping_id") &&
621
+ %w[succeeded failed-settled].include?(record["state"])
622
+ raise AttemptErrors::EvidenceUnavailable, "current service request is not historically settled"
623
+ end
624
+ journal.service_request(record.fetch("request_id"), commit: commit)
625
+ release = entry[2].find { |event| event.dig("payload", "operation") == "scope_reservation_release" }
626
+ prefix = historical_event_commit!(journal, assignment_id: entry.first, event_digest: release.fetch("digest"), commit: commit)
627
+ unless journal.service_request(record.fetch("request_id"), commit: prefix) == record
628
+ raise AttemptErrors::EvidenceUnavailable, "current service request changed after release"
629
+ end
630
+ end
631
+ require "ace/herdr/organisms/inbox"
632
+ registrations = attempts.flat_map do |entry|
633
+ entry[2].select { |event| event["type"] == "inbox_binding" }.map do |event|
634
+ fixed = entry[3].inbox_context(entry[2].find { |item| item.dig("payload", "operation") == "reserve_attempt" }.dig("payload", "data", "mapping_id"),
635
+ event.dig("payload", "inbox_context_id"))
636
+ [fixed.fetch("deliveries_dir"), event.dig("payload", "event_id"), entry, event]
637
+ end
638
+ end
639
+ unless registrations.map { |entry| entry.values_at(0, 1) }.uniq.size == registrations.size
640
+ raise AttemptErrors::EvidenceUnavailable, "current inbox identity attribution conflicts"
641
+ end
642
+ fixed_roots = @deployment_history.descriptors.flat_map do |owner|
643
+ owner.data.fetch("projects").values.select do |project|
644
+ project.values_at("journal_repository", "evidence_git_ref", "evidence_checkout_root") == [journal.repo_root, journal.ref, journal.checkout_root]
645
+ end.flat_map do |project|
646
+ project.fetch("inbox_contexts", {}).values.filter_map do |context|
647
+ if all_attempts
648
+ native = owner.mapping(context.fetch("native_mapping_id"))
649
+ next unless slot_filter == native.fetch("execution_scope").fetch("slot_id")
650
+ end
651
+ context.fetch("deliveries_dir")
652
+ end
653
+ end
654
+ end.uniq
655
+ fixed_roots.each do |root|
656
+ unless Thread.current[:ace_herdr_retained_inventory_locks]&.key?(root)
657
+ raise AttemptErrors::EvidenceUnavailable, "current inbox inventory lifetime lock is unavailable"
658
+ end
659
+ records = Ace::Herdr::Organisms::Inbox.retained_records(deliveries_dir: root)
660
+ expected = registrations.select { |registration| registration.first == root }
661
+ unless records.map(&:event_id).sort == expected.map { |registration| registration[1] }.sort
662
+ raise AttemptErrors::EvidenceUnavailable, "current inbox retained registration set differs"
663
+ end
664
+ records.each do |record|
665
+ registration = expected.find { |entry| entry[1] == record.event_id }
666
+ entry, registered = registration.values_at(2, 3)
667
+ verify_attempt.call(entry)
668
+ public_identity = {"event_id" => record.event_id, "attempt_id" => record.inbox.fetch("attempt_id"),
669
+ "payload_sha256" => record.answer_digest}
670
+ unless public_identity == registered.dig("payload", "registration")
671
+ raise AttemptErrors::EvidenceUnavailable, "current retained inbox registration differs"
672
+ end
673
+ original = record.inbox.fetch("origin_target")
674
+ native = entry[2].find { |event| event["type"] == "scope_native_bound" }&.fetch("payload")
675
+ unless original.is_a?(Hash) && native && original["session"] == native.fetch("workspace_id")
676
+ raise AttemptErrors::EvidenceUnavailable, "current retained inbox original native identity differs"
677
+ end
678
+ child = entry[2].find { |event| event["type"] == "scope_child_bound" }&.dig("payload", "original_process_binding")
679
+ if child && original.values_at("session", "pane", "terminal_id") != child.values_at("session", "pane", "terminal_id")
680
+ raise AttemptErrors::EvidenceUnavailable, "current retained inbox original child identity differs"
681
+ end
682
+ end
683
+ end
684
+ true
685
+ rescue KeyError, TypeError, ArgumentError, JSON::ParserError, Ace::Herdr::Error
686
+ raise AttemptErrors::EvidenceUnavailable, "current retained inventory is unverifiable"
687
+ end
688
+
689
+ def release_scope_reservation_held!(params, map, journal, peer, role, digest)
690
+ result = @mutex.synchronize do
691
+ current = journal.read_events(params.fetch("assignment_id")).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
692
+ scope_close_owner!(params, map, current, peer, role)
693
+ journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
694
+ mutation_id: params.fetch("mutation_id"), operation: "scope_reservation_release", parameters_digest: digest,
695
+ expected_generation: params.fetch("expected_generation"), with_replay: true) do |events, commit, _generation|
696
+ lineage = scope_close_owner!(params, map, events, peer, role)
697
+ native_issuer_pending!(params, map)
698
+ if pending_prompt_issuers?(events, journal, commit)
699
+ raise AttemptErrors::EvidenceUnavailable, "Original prompt issuer has unresolved native writes"
700
+ end
701
+ terminal = terminal_scope_receipt!(events, lineage, journal, commit)
702
+ scope_observer_for(params.fetch("mapping_id")).verify_closed!(lineage)
703
+ scope_settlement_complete!(journal: journal, events: events, params: params, map: map, commit: commit)
704
+ if events.any? { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "scope_reservation_release" }
705
+ raise AttemptErrors::Conflict, "reservation is already released"
706
+ end
707
+ {events: [], blobs: {}, data: {"assignment_id" => params.fetch("assignment_id"), "attempt_id" => params.fetch("attempt_id"),
708
+ "mapping_id" => params.fetch("mapping_id"), "scope_generation" => lineage.binding.fetch("scope_generation"),
709
+ "scope_binding_event_id" => lineage.binding_event.fetch("digest"), "proof_id" => lineage.proof_id,
710
+ "terminal_event_id" => terminal.fetch("digest"), "reservation" => "released"}}
711
+ end
712
+ end
713
+ # Wake the retained original launcher only after canonical release.
714
+ # Its EOF is not proof; the driver authenticates the status snapshot.
715
+ channel = @mutex.synchronize { @control_channels[steering_key(params, map)] }
716
+ channel&.close
717
+ result
718
+ end
719
+
720
+ # A crash after guarded abort must not strand a known-clean reservation.
721
+ # Resume against the current canonical generation under the same slot
722
+ # exclusion; preserve the original immutable public abort reply.
723
+ def resume_scope_release_held!(response, map, journal, peer, role)
724
+ data = response.fetch(:data)
725
+ params = data.slice("mapping_id", "assignment_id", "attempt_id")
726
+ @mutex.synchronize do
727
+ commit = journal.ref_value
728
+ events = journal.read_events(params.fetch("assignment_id"), commit: commit).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
729
+ lineage = scope_close_owner!(params, map, events, peer, role)
730
+ return if scope_reservation_released?(events, lineage, journal, commit)
731
+ params["expected_generation"] = journal.authority_generation(events)
732
+ end
733
+ params["mutation_id"] = "scope-release-#{Digest::SHA256.hexdigest(params.fetch('attempt_id'))[0, 48]}"
734
+ digest = Digest::SHA256.hexdigest(JSON.generate(canonical(params.reject { |key, _| key == "mutation_id" })))
735
+ release_scope_reservation_held!(params, map, journal, peer, role, digest)
736
+ end
737
+
738
+ def verify_maintenance_root!(root, uid)
739
+ Ace::Runtime::Molecules::ProtectedSocket.root_path!(root, directory: true, owner: uid)
740
+ unless (File.stat(root).mode & 0o077).zero?
741
+ raise AttemptErrors::EvidenceUnavailable, "maintenance state root is not private"
742
+ end
743
+ true
744
+ rescue Ace::Runtime::RuntimeUnavailableError, SystemCallError
745
+ raise AttemptErrors::EvidenceUnavailable, "maintenance fixed state root is unavailable"
746
+ end
747
+
748
+ def maintenance_journal_for(owner, map)
749
+ project = owner.project(map.fetch("project_id"))
750
+ if owner.equal?(@deployment)
751
+ current = journal_for(map)
752
+ return current if current.evidence_mode == :protected
753
+ end
754
+ @maintenance_journals ||= {}
755
+ key = project.values_at("journal_repository", "evidence_git_ref", "evidence_checkout_root")
756
+ @maintenance_journals[key] ||= begin
757
+ require_relative "service_evidence"
758
+ reader = nil
759
+ journal = Molecules::EvidenceJournal.new(repo_root: key[0], ref: key[1], checkout_root: key[2],
760
+ mode: :protected,
761
+ evidence_reader: ->(reference, record, state, pending) { reader.call(reference, record, state, pending) },
762
+ service_authorizer: ->(*) {
763
+ raise AttemptErrors::EvidenceUnavailable, "historical maintenance reader cannot mutate service records"
764
+ })
765
+ reader = ServiceEvidence.new(journal: journal)
766
+ journal
767
+ end
768
+ end
769
+
770
+ def require_maintenance_context!(mapping_id, journal, commit)
771
+ pair = Thread.current[:ace_assign_maintenance_contexts]&.dig(object_id, mapping_id)
772
+ unless pair && pair.last.fetch(:journal).equal?(journal) && pair.last.fetch(:commit) == commit
773
+ raise AttemptErrors::EvidenceUnavailable, "maintenance query requires its exact live inventory context"
774
+ end
775
+ _id, owner, map = pair.first
776
+ key = [object_id, owner.authority(map.fetch("authority_id")).fetch("state_root"),
777
+ "execution-slot:#{map.fetch('execution_scope').fetch('slot_id')}"]
778
+ unless Thread.current[:ace_assign_scope_exclusions]&.fetch(key, false)
779
+ raise AttemptErrors::EvidenceUnavailable, "maintenance slot exclusion is unavailable"
780
+ end
781
+ raise AttemptErrors::Conflict, "maintenance canonical ref changed" unless journal.ref_value == commit
782
+ true
783
+ end
784
+
785
+ def require_slot_snapshot!(map, journal, commit)
786
+ key = [object_id, @deployment.authority(map.fetch("authority_id")).fetch("state_root"),
787
+ "execution-slot:#{map.fetch('execution_scope').fetch('slot_id')}"]
788
+ unless Thread.current[:ace_assign_scope_exclusions]&.fetch(key, false) && journal.equal?(journal_for(map)) && journal.ref_value == commit
789
+ raise AttemptErrors::EvidenceUnavailable, "slot query requires its held exclusion and exact current canonical ref"
790
+ end
791
+ end
792
+
793
+ def terminal_scope_receipt!(events, lineage, journal, commit, deployment: @deployment)
794
+ if events.any? { |event| event["type"] == "attempt_stopped" }
795
+ return terminal_scope_stopped!(events, journal, commit, deployment: deployment)
796
+ end
797
+ if events.any? { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "abort_launch" }
798
+ return guarded_scope_abort_receipt!(events, lineage, journal, commit)
799
+ end
800
+ reservation = events.find { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "reserve_attempt" }
801
+ mapping = deployment.mapping(reservation.fetch("payload").fetch("data").fetch("mapping_id"))
802
+ Molecules::TerminalScopeReceipt.verify!(events: events, lineage: lineage, journal: journal, commit: commit, mapping: mapping)
803
+ end
804
+
805
+ def guarded_scope_abort_receipt!(events, lineage, journal, commit)
806
+ aborts = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "abort_launch" }
807
+ terminal = aborts.last
808
+ data = terminal&.dig("payload", "data")
809
+ observation = data&.fetch("abort_observation", nil)
810
+ unless data && data["phase"] == "failed" && observation.is_a?(Hash) && observation["release"] == "not_issued" &&
811
+ observation.keys.sort == (SCOPE_ABORT_FIELDS + ["release"]).sort &&
812
+ observation["kind"] == "protected_scope_before_release" && journal.canonical_attempt_state(events) == "failed" &&
813
+ events.none? { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "release_launch" }
814
+ raise AttemptErrors::EvidenceUnavailable, "verified guarded terminal release is unavailable"
815
+ end
816
+ selectors = observation.reject { |key, _| %w[kind release].include?(key) }
817
+ lineage.require_positive!(**selectors.transform_keys(&:to_sym))
818
+ bytes = journal.blob(data.fetch("failure_ref"), commit: commit)
819
+ unless bytes.is_a?(String) && bytes.bytesize.between?(1, 16_384) && Digest::SHA256.hexdigest(bytes) == data.fetch("failure_digest")
820
+ raise AttemptErrors::EvidenceUnavailable, "guarded terminal failure bytes changed"
821
+ end
822
+ failure = JSON.parse(bytes.dup.force_encoding(Encoding::UTF_8), create_additions: false, max_nesting: 8, allow_duplicate_key: false, allow_comments: false)
823
+ unless failure == observation.reject { |key, _| key == "release" }
824
+ raise AttemptErrors::EvidenceUnavailable, "guarded terminal failure selectors changed"
825
+ end
826
+ preceding = events.take_while { |event| event["digest"] != terminal.fetch("digest") }
827
+ transition = preceding.last
828
+ unless transition && %w[transition reconciliation].include?(transition["type"]) &&
829
+ transition.dig("payload", "reason") == "protected_scope_closed_before_release" &&
830
+ (transition.dig("payload", "to") || transition.dig("payload", "resolution")) == "failed" &&
831
+ transition.dig("payload", "abort_observation") == observation
832
+ raise AttemptErrors::EvidenceUnavailable, "guarded terminal transition differs"
833
+ end
834
+ terminal
835
+ rescue KeyError, JSON::ParserError, EncodingError
836
+ raise AttemptErrors::EvidenceUnavailable, "guarded terminal lineage is incomplete"
837
+ end
838
+
839
+ def scope_reservation_released?(events, lineage, journal, commit)
840
+ releases = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "scope_reservation_release" }
841
+ return false if releases.empty?
842
+ raise AttemptErrors::EvidenceUnavailable, "canonical reservation release is repeated" unless releases.size == 1
843
+ terminal = terminal_scope_receipt!(events, lineage, journal, commit)
844
+ data = releases.first.dig("payload", "data")
845
+ expected = {"assignment_id" => lineage.binding.fetch("assignment_id"), "attempt_id" => lineage.binding.fetch("attempt_id"),
846
+ "mapping_id" => lineage.binding.fetch("mapping_id"), "scope_generation" => lineage.binding.fetch("scope_generation"),
847
+ "scope_binding_event_id" => lineage.binding_event.fetch("digest"), "proof_id" => lineage.proof_id,
848
+ "terminal_event_id" => terminal.fetch("digest"), "reservation" => "released"}
849
+ unless data.is_a?(Hash) && data.reject { |key, _| key == "generation" } == expected &&
850
+ events.index(releases.first) > events.index(terminal) &&
851
+ data["generation"] == events.take(events.index(releases.first) + 1).count { |event| event["type"] == "authority_mutation" }
852
+ raise AttemptErrors::EvidenceUnavailable, "canonical reservation release selectors differ"
853
+ end
854
+ true
855
+ end
856
+ end
857
+ end
858
+ end
859
+ end