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
@@ -1,30 +1,130 @@
1
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"
2
6
 
3
7
  module Ace
4
8
  module Assign
5
9
  module Authority
6
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
+
7
49
  # Source-only maintenance entry. Transport stop/drain/inhibition belongs
8
50
  # to the trusted installer; this method supplies the exact same locks
9
51
  # and canonical owner query, without a live endpoint or another ledger.
10
- def with_execution_slots(mapping_ids:, &block)
11
- unless mapping_ids.is_a?(Array) && mapping_ids.size.between?(1, 64) && mapping_ids.uniq == mapping_ids
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
12
58
  raise ArgumentError, "fixed execution slot selections must be unique and bounded"
13
59
  end
14
- maps = mapping_ids.map { |id| token!(id); [id, @deployment.mapping(id)] }
15
- .sort_by { |_id, map| [map.fetch("authority_id"), map.fetch("execution_scope").fetch("slot_id")] }
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
16
90
  enter = lambda do |offset|
17
- if offset < maps.size
18
- with_slot(maps.fetch(offset).last) { enter.call(offset + 1) }
91
+ if offset < locks.size
92
+ _id, owner, map = locks.fetch(offset)
93
+ with_slot(map, deployment: owner, deadline: deadline) { enter.call(offset + 1) }
19
94
  else
20
- @mutex.synchronize do
21
- commits = {}
22
- contexts = maps.map do |id, map|
23
- journal = journal_for(map)
24
- commit = commits.fetch(map.fetch("project_id")) { commits[map.fetch("project_id")] = journal.ref_value }
25
- {mapping_id: id.freeze, journal: journal, commit: commit&.freeze}.freeze
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
26
118
  end.freeze
27
- block.call(contexts)
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
28
128
  end
29
129
  end
30
130
  end
@@ -32,6 +132,12 @@ module Ace
32
132
  end
33
133
 
34
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
35
141
  map = @deployment.mapping(mapping_id)
36
142
  require_slot_snapshot!(map, journal, commit)
37
143
  ensure_slot_available!(map, journal, commit: commit)
@@ -39,9 +145,119 @@ module Ace
39
145
  true
40
146
  end
41
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
+
42
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
43
250
  slot_reusable!(mapping_id: mapping_id, journal: journal, commit: commit)
44
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
45
261
  slot = map.fetch("execution_scope").fetch("slot_id")
46
262
  lineages = journal.assignment_ids(commit: commit).flat_map do |assignment_id|
47
263
  journal.read_events(assignment_id, commit: commit).group_by { |event| event.fetch("attempt_id") }.filter_map do |attempt_id, events|
@@ -76,15 +292,413 @@ module Ace
76
292
 
77
293
  private
78
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
+
79
689
  def release_scope_reservation_held!(params, map, journal, peer, role, digest)
80
- @mutex.synchronize do
690
+ result = @mutex.synchronize do
81
691
  current = journal.read_events(params.fetch("assignment_id")).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
82
692
  scope_close_owner!(params, map, current, peer, role)
83
693
  journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
84
694
  mutation_id: params.fetch("mutation_id"), operation: "scope_reservation_release", parameters_digest: digest,
85
695
  expected_generation: params.fetch("expected_generation"), with_replay: true) do |events, commit, _generation|
86
696
  lineage = scope_close_owner!(params, map, events, peer, role)
87
- terminal = guarded_scope_abort_receipt!(events, lineage, journal, commit)
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)
88
702
  scope_observer_for(params.fetch("mapping_id")).verify_closed!(lineage)
89
703
  scope_settlement_complete!(journal: journal, events: events, params: params, map: map, commit: commit)
90
704
  if events.any? { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "scope_reservation_release" }
@@ -96,6 +710,11 @@ module Ace
96
710
  "terminal_event_id" => terminal.fetch("digest"), "reservation" => "released"}}
97
711
  end
98
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
99
718
  end
100
719
 
101
720
  # A crash after guarded abort must not strand a known-clean reservation.
@@ -116,13 +735,73 @@ module Ace
116
735
  release_scope_reservation_held!(params, map, journal, peer, role, digest)
117
736
  end
118
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
+
119
785
  def require_slot_snapshot!(map, journal, commit)
120
- key = [object_id, map.fetch("authority_id"), map.fetch("execution_scope").fetch("slot_id")]
786
+ key = [object_id, @deployment.authority(map.fetch("authority_id")).fetch("state_root"),
787
+ "execution-slot:#{map.fetch('execution_scope').fetch('slot_id')}"]
121
788
  unless Thread.current[:ace_assign_scope_exclusions]&.fetch(key, false) && journal.equal?(journal_for(map)) && journal.ref_value == commit
122
789
  raise AttemptErrors::EvidenceUnavailable, "slot query requires its held exclusion and exact current canonical ref"
123
790
  end
124
791
  end
125
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
+
126
805
  def guarded_scope_abort_receipt!(events, lineage, journal, commit)
127
806
  aborts = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "abort_launch" }
128
807
  terminal = aborts.last
@@ -161,7 +840,7 @@ module Ace
161
840
  releases = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "scope_reservation_release" }
162
841
  return false if releases.empty?
163
842
  raise AttemptErrors::EvidenceUnavailable, "canonical reservation release is repeated" unless releases.size == 1
164
- terminal = guarded_scope_abort_receipt!(events, lineage, journal, commit)
843
+ terminal = terminal_scope_receipt!(events, lineage, journal, commit)
165
844
  data = releases.first.dig("payload", "data")
166
845
  expected = {"assignment_id" => lineage.binding.fetch("assignment_id"), "attempt_id" => lineage.binding.fetch("attempt_id"),
167
846
  "mapping_id" => lineage.binding.fetch("mapping_id"), "scope_generation" => lineage.binding.fetch("scope_generation"),