ace-assign 0.64.0 → 0.66.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +11 -0
- data/CHANGELOG.md +25 -0
- data/docs/protected-authority.md +23 -7
- data/docs/usage.md +117 -4
- data/exe/ace-scope-ready +55 -0
- data/handbook/workflow-instructions/assign/drive.wf.md +26 -1
- data/lib/ace/assign/atoms/attempt_state_machine.rb +8 -0
- data/lib/ace/assign/atoms/delivery_parameters.rb +4 -39
- data/lib/ace/assign/authority/assignment_inventory.rb +290 -0
- data/lib/ace/assign/authority/campaign_consumer_policy.rb +100 -0
- data/lib/ace/assign/authority/campaign_execution.rb +119 -0
- data/lib/ace/assign/authority/campaign_round_transfer.rb +82 -0
- data/lib/ace/assign/authority/candidate_transfer.rb +169 -2
- data/lib/ace/assign/authority/cleanup_result_protection.rb +53 -0
- data/lib/ace/assign/authority/client.rb +156 -6
- data/lib/ace/assign/authority/deployment.rb +201 -39
- data/lib/ace/assign/authority/deployment_history.rb +146 -0
- data/lib/ace/assign/authority/endcap.rb +131 -18
- data/lib/ace/assign/authority/endcap_attempts.rb +274 -0
- data/lib/ace/assign/authority/endcap_campaign_results.rb +232 -0
- data/lib/ace/assign/authority/endcap_campaign_rounds.rb +266 -0
- data/lib/ace/assign/authority/endcap_contexts.rb +23 -0
- data/lib/ace/assign/authority/endcap_inboxes.rb +3 -280
- data/lib/ace/assign/authority/endcap_prepared_work.rb +80 -0
- data/lib/ace/assign/authority/endcap_publication.rb +207 -0
- data/lib/ace/assign/authority/endcap_results.rb +36 -18
- data/lib/ace/assign/authority/endcap_review_requests.rb +183 -0
- data/lib/ace/assign/authority/endcap_reviews.rb +130 -0
- data/lib/ace/assign/authority/endcap_service_settlement.rb +152 -0
- data/lib/ace/assign/authority/endcap_services.rb +221 -18
- data/lib/ace/assign/authority/execution_scope_observation.rb +468 -41
- data/lib/ace/assign/authority/inbox_context_original.rb +113 -0
- data/lib/ace/assign/authority/launch_attempt_consumers.rb +51 -0
- data/lib/ace/assign/authority/launch_campaign_registration.rb +214 -0
- data/lib/ace/assign/authority/launch_control_channel.rb +313 -0
- data/lib/ace/assign/authority/launch_control_exclusions.rb +141 -0
- data/lib/ace/assign/authority/launch_driver.rb +298 -19
- data/lib/ace/assign/authority/launch_input_inhibition.rb +132 -0
- data/lib/ace/assign/authority/launch_lifecycle.rb +275 -100
- data/lib/ace/assign/authority/launch_prepared_work.rb +122 -0
- data/lib/ace/assign/authority/launch_review.rb +91 -0
- data/lib/ace/assign/authority/launch_scope_abort.rb +0 -7
- data/lib/ace/assign/authority/launch_scope_admission.rb +136 -5
- data/lib/ace/assign/authority/launch_scope_close.rb +68 -37
- data/lib/ace/assign/authority/launch_scope_release.rb +696 -17
- data/lib/ace/assign/authority/launch_steering.rb +353 -0
- data/lib/ace/assign/authority/launch_stop.rb +254 -0
- data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +229 -0
- data/lib/ace/assign/authority/original_inbox_selection.rb +41 -0
- data/lib/ace/assign/authority/posix_acl.rb +5 -2
- data/lib/ace/assign/authority/prepared_input.rb +130 -0
- data/lib/ace/assign/authority/prepared_queue.rb +185 -0
- data/lib/ace/assign/authority/prepared_task_context.rb +116 -0
- data/lib/ace/assign/authority/prepared_work.rb +304 -0
- data/lib/ace/assign/authority/prepared_worker.rb +69 -0
- data/lib/ace/assign/authority/protected_assignment_context.rb +226 -0
- data/lib/ace/assign/authority/readiness_hook.rb +120 -0
- data/lib/ace/assign/authority/router.rb +24 -2
- data/lib/ace/assign/authority/server.rb +103 -33
- data/lib/ace/assign/authority/service_cleanup_dispatch.rb +88 -0
- data/lib/ace/assign/authority/service_cleanup_evidence.rb +180 -0
- data/lib/ace/assign/authority/service_delivery_evidence.rb +101 -0
- data/lib/ace/assign/authority/service_evidence.rb +243 -12
- data/lib/ace/assign/authority/service_publication_evidence.rb +110 -0
- data/lib/ace/assign/authority/task_context_entry.rb +32 -0
- data/lib/ace/assign/authority/transfer_codec.rb +44 -12
- data/lib/ace/assign/cli/commands/assignment_target.rb +15 -2
- data/lib/ace/assign/cli/commands/attempt/base.rb +49 -0
- data/lib/ace/assign/cli/commands/attempt/finish.rb +20 -4
- data/lib/ace/assign/cli/commands/attempt/reconcile.rb +11 -0
- data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +25 -0
- data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +28 -0
- data/lib/ace/assign/cli/commands/authority/launch.rb +58 -6
- data/lib/ace/assign/cli/commands/authority/serve.rb +2 -1
- data/lib/ace/assign/cli/commands/authority/status.rb +1 -1
- data/lib/ace/assign/cli/commands/authority/task_context.rb +31 -0
- data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +25 -0
- data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +30 -0
- data/lib/ace/assign/cli/commands/authority/worker.rb +23 -0
- data/lib/ace/assign/cli/commands/campaign_export_result.rb +55 -0
- data/lib/ace/assign/cli/commands/campaign_record_round.rb +57 -0
- data/lib/ace/assign/cli/commands/create.rb +2 -0
- data/lib/ace/assign/cli/commands/delivery.rb +54 -4
- data/lib/ace/assign/cli/commands/fail.rb +6 -0
- data/lib/ace/assign/cli/commands/finish.rb +23 -2
- data/lib/ace/assign/cli/commands/fork_session.rb +6 -2
- data/lib/ace/assign/cli/commands/inbox_bind.rb +34 -0
- data/lib/ace/assign/cli/commands/protected_submission.rb +61 -0
- data/lib/ace/assign/cli/commands/resume.rb +12 -0
- data/lib/ace/assign/cli/commands/select.rb +2 -0
- data/lib/ace/assign/cli/commands/start.rb +2 -0
- data/lib/ace/assign/cli/commands/status.rb +4 -2
- data/lib/ace/assign/cli/commands/step.rb +3 -1
- data/lib/ace/assign/cli/commands/submit_candidate.rb +34 -0
- data/lib/ace/assign/cli/commands/submit_result.rb +61 -0
- data/lib/ace/assign/cli.rb +36 -5
- data/lib/ace/assign/errors.rb +139 -0
- data/lib/ace/assign/models/assignment.rb +26 -4
- data/lib/ace/assign/models/evidence_event.rb +3 -3
- data/lib/ace/assign/molecules/assignment_manager.rb +12 -2
- data/lib/ace/assign/molecules/attempt_reconciler.rb +20 -0
- data/lib/ace/assign/molecules/canonical_attempt_state.rb +151 -0
- data/lib/ace/assign/molecules/canonical_evidence.rb +5 -2
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +374 -0
- data/lib/ace/assign/molecules/evidence_journal.rb +720 -44
- data/lib/ace/assign/molecules/execution_scope_lineage.rb +27 -9
- data/lib/ace/assign/molecules/fork_session_launcher.rb +16 -6
- data/lib/ace/assign/molecules/journal_input_inhibition.rb +78 -0
- data/lib/ace/assign/molecules/journal_mutation.rb +82 -13
- data/lib/ace/assign/molecules/journal_prompt_mutation.rb +241 -0
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +178 -26
- data/lib/ace/assign/molecules/original_launch_binding.rb +37 -0
- data/lib/ace/assign/molecules/protected_control_exclusion.rb +303 -0
- data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +330 -0
- data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +164 -0
- data/lib/ace/assign/molecules/queue_scanner.rb +26 -11
- data/lib/ace/assign/molecules/receipt_verifier.rb +21 -15
- data/lib/ace/assign/molecules/step_writer.rb +27 -5
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +88 -0
- data/lib/ace/assign/organisms/assignment_executor.rb +4 -4
- data/lib/ace/assign/organisms/attempt_coordinator.rb +66 -76
- data/lib/ace/assign/organisms/prepared_work_builder.rb +194 -0
- data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +100 -0
- data/lib/ace/assign/organisms/task_assignment_creator.rb +4 -2
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +1 -112
- metadata +90 -11
- data/lib/ace/assign/cli/commands/inbox_reconcile.rb +0 -36
|
@@ -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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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 <
|
|
18
|
-
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 =
|
|
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"),
|