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.
- checksums.yaml +4 -4
- data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +11 -0
- data/CHANGELOG.md +19 -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 +88 -9
- data/lib/ace/assign/cli/commands/inbox_reconcile.rb +0 -36
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative "../atoms/evidence_digest"
|
|
3
|
+
|
|
4
|
+
module Ace
|
|
5
|
+
module Assign
|
|
6
|
+
module Authority
|
|
7
|
+
class LaunchLifecycle
|
|
8
|
+
# One authenticated immutable prefix; never the latest registration.
|
|
9
|
+
def original_prepared_registration!(journal:, params:, commit:)
|
|
10
|
+
original_prepared_registration_for_phase!(journal: journal, params: params, commit: commit, phase: :issued) do |map, state|
|
|
11
|
+
yield(map, state) if block_given?
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def original_bound_prepared_registration!(journal:, params:, commit:)
|
|
18
|
+
original_prepared_registration_for_phase!(journal: journal, params: params, commit: commit, phase: :bound)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def original_prepared_registration_for_phase!(journal:, params:, commit:, phase:)
|
|
22
|
+
inventory = journal.canonical_event_inventory!(commit: commit)
|
|
23
|
+
raise AttemptErrors::EvidenceUnavailable, "prepared canonical snapshot differs" unless inventory.fetch("commit") == commit
|
|
24
|
+
assignment = params.fetch("assignment_id")
|
|
25
|
+
events = inventory.fetch("events").fetch(assignment)
|
|
26
|
+
chain = events.select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
27
|
+
state = origin(chain, **params.slice("assignment_id", "attempt_id", "mapping_id").transform_keys(&:to_sym))
|
|
28
|
+
reserves = chain.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "reserve_attempt" }
|
|
29
|
+
releases = chain.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "release_launch" }
|
|
30
|
+
provisioning = chain.select { |event| event["type"] == "scope_provisioning" }
|
|
31
|
+
admitted_phase = if phase == :issued
|
|
32
|
+
releases.one? && releases.first.dig("payload", "data", "phase") == "issued"
|
|
33
|
+
else
|
|
34
|
+
releases.empty? && state.fetch("phase") == "bound"
|
|
35
|
+
end
|
|
36
|
+
unless reserves.one? && admitted_phase && provisioning.one? &&
|
|
37
|
+
!terminal_events?(chain) && !chain.any? { |event| %w[scope_sealed input_inhibited attempt_stopped].include?(event["type"]) }
|
|
38
|
+
raise AttemptErrors::EvidenceUnavailable, "original prepared attempt is not #{phase} and open"
|
|
39
|
+
end
|
|
40
|
+
identity = provisioning.first.fetch("payload")
|
|
41
|
+
sha = identity.fetch("descriptor_sha256")
|
|
42
|
+
selected = if @deployment.artifact_reference.fetch("sha256") == sha
|
|
43
|
+
@deployment
|
|
44
|
+
else
|
|
45
|
+
raise AttemptErrors::EvidenceUnavailable, "original prepared deployment is unavailable" unless @deployment_history
|
|
46
|
+
@deployment_history.descriptor!(sha256: sha)
|
|
47
|
+
end
|
|
48
|
+
map = selected.mapping(params.fetch("mapping_id"))
|
|
49
|
+
original_project = selected.project(map.fetch("project_id"))
|
|
50
|
+
unless identity.fetch("deployment_digest") == Digest::SHA256.hexdigest(JSON.generate(canonical(map))) &&
|
|
51
|
+
identity.fetch("slot_id") == map.fetch("execution_scope").fetch("slot_id") && state.fetch("project_id") == map.fetch("project_id") &&
|
|
52
|
+
selected.project(map.fetch("project_id")).values_at("journal_repository", "evidence_git_ref", "evidence_checkout_root") ==
|
|
53
|
+
[journal.repo_root, journal.ref, journal.checkout_root]
|
|
54
|
+
raise AttemptErrors::EvidenceUnavailable, "original prepared deployment association differs"
|
|
55
|
+
end
|
|
56
|
+
lineage = Molecules::ExecutionScopeLineage.new(events: chain, project_id: state.fetch("project_id"),
|
|
57
|
+
assignment_id: assignment, attempt_id: params.fetch("attempt_id"), mapping_id: params.fetch("mapping_id"))
|
|
58
|
+
unless lineage.require_launch_bound! == state.fetch("process_binding")
|
|
59
|
+
raise AttemptErrors::EvidenceUnavailable, "original prepared child binding differs"
|
|
60
|
+
end
|
|
61
|
+
original = original_prompt_record!(chain, state, params: params)
|
|
62
|
+
# Authenticate the actual original worker/descendant before reading artifacts.
|
|
63
|
+
yield(map, state) if block_given?
|
|
64
|
+
introductions = inventory.fetch("introductions").fetch(assignment)
|
|
65
|
+
reservation_commit = introductions.fetch(reserves.first.fetch("digest"))
|
|
66
|
+
registration = definition(journal, assignment, commit: reservation_commit)
|
|
67
|
+
original_events = journal.read_events(assignment, commit: reservation_commit)
|
|
68
|
+
authenticate_inventory_registration!(journal, assignment, original_events, registration, params.fetch("mapping_id"), map, reservation_commit, introductions)
|
|
69
|
+
registered = original_events.find { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "register_assignment" && event.dig("payload", "data") == registration }
|
|
70
|
+
registration_commit = introductions.fetch(registered.fetch("digest"))
|
|
71
|
+
TaskContextEntry.validate!(registration.fetch("task_context_entry"))
|
|
72
|
+
unless registration.fetch("task_context_entry") == map.fetch("task_context_entry")
|
|
73
|
+
raise AttemptErrors::EvidenceUnavailable, "original prepared task context entry differs"
|
|
74
|
+
end
|
|
75
|
+
prepared = registration.fetch("prepared_work")
|
|
76
|
+
unless prepared.fetch("scope") == state.fetch("scope") && prepared.fetch("task_id") == state.fetch("task_id")
|
|
77
|
+
raise AttemptErrors::EvidenceUnavailable, "original prepared selection differs"
|
|
78
|
+
end
|
|
79
|
+
bytes = journal.bounded_blob(registration.fetch("definition_ref"), commit: registration_commit, max_bytes: PreparedWork::MAX_MANIFEST)
|
|
80
|
+
unless bytes.is_a?(String) && bytes.bytesize.between?(1, PreparedWork::MAX_MANIFEST) &&
|
|
81
|
+
Digest::SHA256.hexdigest(bytes) == registration.fetch("definition_digest") && JSON.parse(bytes).fetch("prepared_work") == prepared
|
|
82
|
+
raise AttemptErrors::EvidenceUnavailable, "original prepared definition differs"
|
|
83
|
+
end
|
|
84
|
+
projection = immutable_maintenance_projection(registration: registration, registration_commit: registration_commit, state: state, map: map, events: chain,
|
|
85
|
+
workspace_exclusion: maintenance_scope_observer_for(selected, params.fetch("mapping_id")).workspace_exclusion_projection!(lineage),
|
|
86
|
+
original_worker_scratch_root: original_project.fetch("peer_credentials").fetch(map.fetch("worker_uid").to_s).fetch("scratch_root"),
|
|
87
|
+
original_binding_digest: original.fetch("binding_digest"), commit: commit)
|
|
88
|
+
pin = compact_prepared_input(projection)
|
|
89
|
+
if phase == :issued
|
|
90
|
+
unless Atoms::EvidenceDigest.digest(releases.first.dig("payload", "data", "prepared_input")) == Atoms::EvidenceDigest.digest(pin)
|
|
91
|
+
raise AttemptErrors::EvidenceUnavailable, "accepted prepared release differs"
|
|
92
|
+
end
|
|
93
|
+
else
|
|
94
|
+
bundle = journal.bounded_blob(registration.fetch("prepared_bundle_ref"), commit: registration_commit, max_bytes: CandidateTransfer::MAX_BYTES)
|
|
95
|
+
unless bundle.is_a?(String) && bundle.bytesize == registration.fetch("prepared_bundle_bytes") &&
|
|
96
|
+
bundle.bytesize.between?(1, CandidateTransfer::MAX_BYTES) && Digest::SHA256.hexdigest(bundle) == registration.fetch("prepared_bundle_sha256")
|
|
97
|
+
raise AttemptErrors::EvidenceUnavailable, "original prepared bundle differs"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
projection
|
|
101
|
+
rescue KeyError, TypeError, NoMethodError, JSON::ParserError
|
|
102
|
+
raise AttemptErrors::EvidenceUnavailable, "original prepared registration is unavailable"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def compact_prepared_input(projection)
|
|
106
|
+
registration = projection.fetch(:registration)
|
|
107
|
+
immutable_maintenance_projection(
|
|
108
|
+
"registration_generation" => registration.fetch("generation"),
|
|
109
|
+
"registration_commit" => projection.fetch(:registration_commit),
|
|
110
|
+
"definition_digest" => registration.fetch("definition_digest"),
|
|
111
|
+
"original_binding_digest" => projection.fetch(:original_binding_digest),
|
|
112
|
+
"prepared_work" => registration.fetch("prepared_work"),
|
|
113
|
+
"worker_entry" => Ace::Runtime::Molecules::ProtectedWorkerEntry.validate!(projection.fetch(:map).fetch("worker_entry")),
|
|
114
|
+
"workspace_exclusion" => projection.fetch(:workspace_exclusion),
|
|
115
|
+
"bundle_ref" => registration.fetch("prepared_bundle_ref"),
|
|
116
|
+
"bundle_bytes" => registration.fetch("prepared_bundle_bytes"),
|
|
117
|
+
"bundle_sha256" => registration.fetch("prepared_bundle_sha256"))
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Assign
|
|
5
|
+
module Authority
|
|
6
|
+
class LaunchLifecycle
|
|
7
|
+
# Called by Endcap only while holding the existing assignment/slot
|
|
8
|
+
# exclusion and shared authority mutex. Returned channel capacity is
|
|
9
|
+
# released after the caller leaves those locks and waits on the stream.
|
|
10
|
+
def reserve_review_dispatch!(events:, params:, map:)
|
|
11
|
+
state = origin(events, **params.slice("mapping_id", "assignment_id", "attempt_id").transform_keys(&:to_sym))
|
|
12
|
+
unless state["phase"] == "issued" && !terminal_events?(events)
|
|
13
|
+
raise AttemptErrors::Conflict, "review delegation requires an issued original launcher"
|
|
14
|
+
end
|
|
15
|
+
scope_open_for_effect!(events: events, params: params, map: map)
|
|
16
|
+
original = original_prompt_record!(events, state, params: params)
|
|
17
|
+
@kernel.live!(state.fetch("launcher_identity"))
|
|
18
|
+
channel = @control_channels[steering_key(params, map)]
|
|
19
|
+
raise AttemptErrors::EvidenceUnavailable, "original review channel is unavailable" unless channel
|
|
20
|
+
[channel, channel.reserve_dispatch!, original]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def review_original_reference!(events:, params:)
|
|
24
|
+
state = origin(events, **params.slice("mapping_id", "assignment_id", "attempt_id").transform_keys(&:to_sym))
|
|
25
|
+
original_prompt_record!(events, state, params: params)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Read-only original-launcher projection at the exact request commit.
|
|
29
|
+
# Permission to assign still belongs to Endcap's fresh/replay checks.
|
|
30
|
+
def launch_review_intent!(request:, peer:, role:)
|
|
31
|
+
params = request.fetch("params")
|
|
32
|
+
strict!(params, %w[mapping_id assignment_id attempt_id mutation_id request_event_id journal_commit original_binding_digest])
|
|
33
|
+
raise ArgumentError, "Review intent lookup is read-only" unless request.fetch("mutation_id").nil?
|
|
34
|
+
raise AttemptErrors::UnauthorizedIdentity, "Review intent requires original launcher" unless role == :launcher
|
|
35
|
+
%w[mapping_id assignment_id attempt_id mutation_id].each { |key| token!(params.fetch(key)) }
|
|
36
|
+
unless %w[request_event_id original_binding_digest].all? { |key| params[key].is_a?(String) && params[key].match?(/\A[0-9a-f]{64}\z/) } &&
|
|
37
|
+
params["journal_commit"].is_a?(String) && params["journal_commit"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/)
|
|
38
|
+
raise ArgumentError, "Review intent selectors differ"
|
|
39
|
+
end
|
|
40
|
+
map = steering_principal!(params, peer, role)
|
|
41
|
+
raise AttemptErrors::UnauthorizedIdentity, "Review project differs" unless request.fetch("project_id") == map.fetch("project_id")
|
|
42
|
+
journal = journal_for(map)
|
|
43
|
+
commit = params.fetch("journal_commit")
|
|
44
|
+
journal.verify_canonical_prefix!(commit: commit)
|
|
45
|
+
events = journal.read_events(params.fetch("assignment_id"), commit: commit).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
46
|
+
state = origin(events, **params.slice("mapping_id", "assignment_id", "attempt_id").transform_keys(&:to_sym))
|
|
47
|
+
unless @kernel.same?(state.fetch("launcher_identity"), peer)
|
|
48
|
+
raise AttemptErrors::UnauthorizedIdentity, "Review intent requires exact original launcher birth"
|
|
49
|
+
end
|
|
50
|
+
original = original_prompt_record!(events, state, params: params)
|
|
51
|
+
event = events.find { |item| item["digest"] == params.fetch("request_event_id") }
|
|
52
|
+
accepted = journal.mutation_result(params.fetch("mutation_id"), commit: commit)
|
|
53
|
+
unless event && event["type"] == "authority_mutation" && event.dig("payload", "operation") == "request_review" &&
|
|
54
|
+
event.dig("payload", "mutation_id") == params.fetch("mutation_id") && accepted &&
|
|
55
|
+
accepted == event.fetch("payload").merge("journal_commit" => commit) &&
|
|
56
|
+
original.fetch("binding_digest") == params.fetch("original_binding_digest")
|
|
57
|
+
raise AttemptErrors::EvidenceUnavailable, "Canonical review request introduction differs"
|
|
58
|
+
end
|
|
59
|
+
data = event.fetch("payload").fetch("data")
|
|
60
|
+
fields = %w[candidate_generation generation head original_binding_digest requester reviewer_process_binding reviewer_uid]
|
|
61
|
+
unless data.is_a?(Hash) && data.keys.sort == fields && data["original_binding_digest"] == original.fetch("binding_digest") &&
|
|
62
|
+
data["head"].is_a?(String) && data["head"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/) &&
|
|
63
|
+
%w[candidate_generation generation reviewer_uid].all? { |key| data[key].is_a?(Integer) && data[key].positive? }
|
|
64
|
+
raise AttemptErrors::EvidenceUnavailable, "Canonical review request data differs"
|
|
65
|
+
end
|
|
66
|
+
identity = data.fetch("reviewer_process_binding")
|
|
67
|
+
boot = identity.is_a?(Hash) && identity["started_at"].is_a?(String) && identity["started_at"].split(":", -1)[1]
|
|
68
|
+
Molecules::ExecutionScopeLineage.validate_process_identity!(identity, boot_id: boot)
|
|
69
|
+
requester = data["requester"]
|
|
70
|
+
unless requester.is_a?(Hash) && requester.keys.sort == %w[gid groups role uid] &&
|
|
71
|
+
%w[uid gid].all? { |key| requester[key].is_a?(Integer) } && requester["groups"].is_a?(Array) &&
|
|
72
|
+
requester["groups"].all? { |group| group.is_a?(Integer) } &&
|
|
73
|
+
requester == identity.slice("uid", "gid", "groups").merge("role" => "reviewer") &&
|
|
74
|
+
data["reviewer_uid"] == identity["uid"] && identity["uid"] != map.fetch("worker_uid")
|
|
75
|
+
raise AttemptErrors::EvidenceUnavailable, "Canonical reviewer attribution differs"
|
|
76
|
+
end
|
|
77
|
+
candidate = events.reverse.find { |item| item["type"] == "authority_mutation" && item.dig("payload", "operation") == "submit_candidate" }&.dig("payload", "data")
|
|
78
|
+
unless candidate && candidate.values_at("head", "candidate_generation") == data.values_at("head", "candidate_generation")
|
|
79
|
+
raise AttemptErrors::EvidenceUnavailable, "Canonical request candidate differs"
|
|
80
|
+
end
|
|
81
|
+
assignment = params.slice("mapping_id", "assignment_id", "attempt_id").merge(
|
|
82
|
+
data.slice("head", "candidate_generation", "reviewer_uid", "reviewer_process_binding"),
|
|
83
|
+
"expected_generation" => data.fetch("generation"))
|
|
84
|
+
{data: params.slice("request_event_id", "original_binding_digest").merge("assignment_params" => assignment), replayed: false}
|
|
85
|
+
rescue KeyError, TypeError
|
|
86
|
+
raise AttemptErrors::EvidenceUnavailable, "Canonical review intent is incomplete"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -21,13 +21,6 @@ module Ace
|
|
|
21
21
|
raise AttemptErrors::EvidenceUnavailable, "canonical service settlement owner is unavailable"
|
|
22
22
|
end
|
|
23
23
|
end
|
|
24
|
-
contexts = @deployment.project(map.fetch("project_id")).fetch("inbox_contexts", {})
|
|
25
|
-
if contexts.any? || events.any? { |event| event["type"].start_with?("inbox_") }
|
|
26
|
-
unless @result_owner && @result_owner.respond_to?(:inbox_settlement_complete!)
|
|
27
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical inbox settlement owner is unavailable"
|
|
28
|
-
end
|
|
29
|
-
@result_owner.inbox_settlement_complete!(journal: journal, events: events, params: params, map: map, commit: commit)
|
|
30
|
-
end
|
|
31
24
|
true
|
|
32
25
|
end
|
|
33
26
|
|
|
@@ -19,9 +19,66 @@ module Ace
|
|
|
19
19
|
journal = journal_for(map)
|
|
20
20
|
input = params.reject { |key, _| key == "mutation_id" }
|
|
21
21
|
digest = Digest::SHA256.hexdigest(JSON.generate(canonical(input)))
|
|
22
|
-
with_exclusion(params, map, journal) do
|
|
22
|
+
result = with_exclusion(params, map, journal) do
|
|
23
23
|
admit_native_service_held!(params, map, journal, peer, role, digest)
|
|
24
24
|
end
|
|
25
|
+
complete_native_start!(params, map, journal, peer, role, result) unless result.fetch(:replayed)
|
|
26
|
+
result
|
|
27
|
+
ensure
|
|
28
|
+
settle_native_issuer!(params, map) if map
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Private transport owner. Exact manager hook attribution precedes
|
|
32
|
+
# challenge issuance; this never enters the ordinary worker dispatcher.
|
|
33
|
+
def native_readiness!(mapping_id:, peer:, socket:, codec:, deadline:)
|
|
34
|
+
map = @deployment.mapping(mapping_id)
|
|
35
|
+
journal = journal_for(map)
|
|
36
|
+
pending, key = @mutex.synchronize do
|
|
37
|
+
selected = @native_issuers.select { |identity, _| identity[0] == map.fetch("project_id") && identity[1] == mapping_id }
|
|
38
|
+
raise AttemptErrors::EvidenceUnavailable, "no unique live admitted issuer" unless selected.size == 1
|
|
39
|
+
identity, entry = selected.first
|
|
40
|
+
raise AttemptErrors::Conflict, "readiness already supplied" if entry[:report] || entry[:challenge]
|
|
41
|
+
entry[:challenge] = SecureRandom.hex(32)
|
|
42
|
+
[entry, identity]
|
|
43
|
+
end
|
|
44
|
+
params = pending.fetch(:params)
|
|
45
|
+
observer = scope_observer_for(mapping_id)
|
|
46
|
+
challenge = with_exclusion(params, map, journal) do
|
|
47
|
+
@mutex.synchronize do
|
|
48
|
+
events = journal.read_events(params.fetch("assignment_id")).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
49
|
+
lineage = Molecules::ExecutionScopeLineage.new(events: events, project_id: map.fetch("project_id"),
|
|
50
|
+
assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"), mapping_id: mapping_id)
|
|
51
|
+
lineage.require_open!
|
|
52
|
+
server = observer.readiness_peer!(lineage, peer)
|
|
53
|
+
{"version" => 1, "challenge_id" => pending.fetch(:challenge), "server_identity" => server}
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
wire.write(socket, challenge, deadline: deadline, limit: 16_384)
|
|
57
|
+
envelope = wire.read(socket, deadline: deadline, limit: 16_384)
|
|
58
|
+
unless envelope.is_a?(Hash) && envelope.keys.sort == %w[challenge_id transfer version] &&
|
|
59
|
+
envelope["version"] == 1 && envelope["challenge_id"] == challenge.fetch("challenge_id")
|
|
60
|
+
raise ArgumentError, "private readiness upload envelope differs"
|
|
61
|
+
end
|
|
62
|
+
codec.receive(socket, descriptor: envelope.fetch("transfer"), purpose: :scope_boundary_observation,
|
|
63
|
+
deadline: deadline) do |input|
|
|
64
|
+
bytes = input.bytes.force_encoding(Encoding::UTF_8)
|
|
65
|
+
raise ArgumentError, "readiness observation is not UTF-8" unless bytes.valid_encoding?
|
|
66
|
+
report = JSON.parse(bytes, create_additions: false, max_nesting: 12, allow_duplicate_key: false, allow_comments: false)
|
|
67
|
+
with_exclusion(params, map, journal) do
|
|
68
|
+
@mutex.synchronize do
|
|
69
|
+
unless @native_issuers[key].equal?(pending) && !pending[:report]
|
|
70
|
+
raise AttemptErrors::EvidenceUnavailable, "readiness issuer lifetime ended"
|
|
71
|
+
end
|
|
72
|
+
events = journal.read_events(params.fetch("assignment_id")).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
73
|
+
lineage = Molecules::ExecutionScopeLineage.new(events: events, project_id: map.fetch("project_id"),
|
|
74
|
+
assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"), mapping_id: mapping_id)
|
|
75
|
+
lineage.require_open!
|
|
76
|
+
pending[:report] = observer.verify_readiness_report!(lineage, peer, report, challenge: challenge)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
wire.write(socket, {"version" => 1, "challenge_id" => challenge.fetch("challenge_id"), "status" => "ready"},
|
|
81
|
+
deadline: deadline, limit: 16_384)
|
|
25
82
|
end
|
|
26
83
|
|
|
27
84
|
private
|
|
@@ -58,15 +115,89 @@ module Ace
|
|
|
58
115
|
{events: [], blobs: {}, data: data}
|
|
59
116
|
end
|
|
60
117
|
end
|
|
61
|
-
# No authority mutex or journal ref lock is held while systemd waits
|
|
62
|
-
# for its private readiness callback. Slot exclusion still prevents
|
|
63
|
-
# another generation, seal or maintenance from racing this start.
|
|
64
118
|
unless result.fetch(:replayed)
|
|
65
|
-
|
|
119
|
+
@mutex.synchronize do
|
|
120
|
+
raise AttemptErrors::Conflict, "native issuer already active" if @native_issuers.key?(native_issuer_key(params, map))
|
|
121
|
+
@native_issuers[native_issuer_key(params, map)] = {params: params.dup.freeze, report: nil, owner: Thread.current}
|
|
122
|
+
end
|
|
66
123
|
end
|
|
67
124
|
result
|
|
68
125
|
end
|
|
69
126
|
|
|
127
|
+
def complete_native_start!(params, map, journal, peer, role, result)
|
|
128
|
+
attempt_id = params.fetch("attempt_id")
|
|
129
|
+
observer = scope_observer_for(params.fetch("mapping_id"))
|
|
130
|
+
issue = with_exclusion(params, map, journal) do
|
|
131
|
+
@mutex.synchronize do
|
|
132
|
+
events = journal.read_events(params.fetch("assignment_id")).select { |event| event["attempt_id"] == attempt_id }
|
|
133
|
+
_, lineage = scope_admission_owner!(params, map, events, peer, role)
|
|
134
|
+
!lineage.sealed?
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
observer.start_admitted_service! if issue
|
|
138
|
+
# The pending issuer lifetime remains live through this final check
|
|
139
|
+
# and cleanup. A seal cannot publish proof then permit a late start.
|
|
140
|
+
with_exclusion(params, map, journal) do
|
|
141
|
+
events, lineage = @mutex.synchronize do
|
|
142
|
+
current = journal.read_events(params.fetch("assignment_id")).select { |event| event["attempt_id"] == attempt_id }
|
|
143
|
+
_, retained = scope_admission_owner!(params, map, current, peer, role)
|
|
144
|
+
[current, retained]
|
|
145
|
+
end
|
|
146
|
+
if lineage.sealed?
|
|
147
|
+
observer.stop_sealed_service!
|
|
148
|
+
elsif issue
|
|
149
|
+
payload = observer.complete_native_readiness!(lineage,
|
|
150
|
+
@mutex.synchronize { @native_issuers.fetch(native_issuer_key(params, map)).fetch(:report) })
|
|
151
|
+
@mutex.synchronize { commit_ready_native!(params, map, journal, events, lineage, payload) }
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
rescue StandardError
|
|
155
|
+
# Lost command completion is uncertain. Cleanup of the fixed service
|
|
156
|
+
# must finish before the live issuer can retire its pending lifetime.
|
|
157
|
+
observer.stop_sealed_service! if issue
|
|
158
|
+
raise
|
|
159
|
+
ensure
|
|
160
|
+
settle_native_issuer!(params, map) if attempt_id
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def commit_ready_native!(params, map, journal, events, lineage, payload)
|
|
164
|
+
context = params.slice("mapping_id", "assignment_id", "attempt_id").merge(
|
|
165
|
+
"scope_binding_event_id" => lineage.binding_event.fetch("digest"))
|
|
166
|
+
journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
167
|
+
mutation_id: "scope-native-#{Digest::SHA256.hexdigest(lineage.binding_event.fetch('digest'))[0, 48]}",
|
|
168
|
+
operation: "scope_native_binding", parameters_digest: Digest::SHA256.hexdigest(JSON.generate(canonical(context))),
|
|
169
|
+
expected_generation: journal.authority_generation(events), with_replay: true) do |current, _commit, _generation|
|
|
170
|
+
fresh = Molecules::ExecutionScopeLineage.new(events: current, project_id: map.fetch("project_id"),
|
|
171
|
+
assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"), mapping_id: params.fetch("mapping_id"))
|
|
172
|
+
fresh.require_open!
|
|
173
|
+
raise AttemptErrors::Conflict, "native binding already exists" if fresh.native_event
|
|
174
|
+
at = Time.now.utc
|
|
175
|
+
event = Models::EvidenceEvent.build(type: "scope_native_bound", attempt_id: params.fetch("attempt_id"),
|
|
176
|
+
payload: payload, previous_digest: current.last&.fetch("digest"), recorded_at: at)
|
|
177
|
+
Molecules::ExecutionScopeLineage.new(events: current + [event], project_id: map.fetch("project_id"),
|
|
178
|
+
assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"), mapping_id: params.fetch("mapping_id"))
|
|
179
|
+
{events: [{type: "scope_native_bound", payload: payload, recorded_at: at}], blobs: {}, data: context}
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def native_issuer_key(params, map)
|
|
184
|
+
[map.fetch("project_id"), params.fetch("mapping_id"), params.fetch("assignment_id"), params.fetch("attempt_id")].freeze
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def settle_native_issuer!(params, map)
|
|
188
|
+
@mutex.synchronize do
|
|
189
|
+
key = native_issuer_key(params, map)
|
|
190
|
+
pending = @native_issuers[key]
|
|
191
|
+
@native_issuers.delete(key) if pending && pending[:owner].equal?(Thread.current)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def native_issuer_pending!(params, map)
|
|
196
|
+
if @native_issuers.key?(native_issuer_key(params, map))
|
|
197
|
+
raise AttemptErrors::EvidenceUnavailable, "native issuance is still pending"
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
70
201
|
def scope_admission_owner!(params, map, events, peer, role)
|
|
71
202
|
state = states(events)[params.fetch("attempt_id")]
|
|
72
203
|
unless state && state["mapping_id"] == params.fetch("mapping_id") &&
|
|
@@ -17,8 +17,9 @@ module Ace
|
|
|
17
17
|
journal = journal_for(map)
|
|
18
18
|
input = params.reject { |key, _| key == "mutation_id" }
|
|
19
19
|
digest = Digest::SHA256.hexdigest(JSON.generate(canonical(input)))
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
stop_after_commit = false
|
|
21
|
+
inhibit_after_commit = false
|
|
22
|
+
result = with_containment_exclusion(params, map, journal) do
|
|
22
23
|
result = @mutex.synchronize do
|
|
23
24
|
current = journal.read_events(params.fetch("assignment_id")).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
24
25
|
scope_close_owner!(params, map, current, peer, role)
|
|
@@ -26,44 +27,27 @@ module Ace
|
|
|
26
27
|
mutation_id: params.fetch("mutation_id"), operation: "close_execution_scope", parameters_digest: digest,
|
|
27
28
|
expected_generation: params.fetch("expected_generation"), with_replay: true) do |events, _commit, _generation|
|
|
28
29
|
lineage = scope_close_owner!(params, map, events, peer, role)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if !lineage.sealed?
|
|
34
|
-
stop_after_commit = true
|
|
35
|
-
payload = {"scope_generation" => binding.fetch("scope_generation"), "scope_binding_event_id" => lineage.binding_event.fetch("digest")}
|
|
36
|
-
{events: [{type: "scope_sealed", payload: payload}], blobs: {}, data: projection}
|
|
37
|
-
elsif lineage.proof_event
|
|
38
|
-
scope_observer_for(params.fetch("mapping_id")).verify_closed!(lineage)
|
|
39
|
-
{events: [], blobs: {}, data: projection.merge("state" => "closed_no_writers", "proof_id" => lineage.proof_id, "required_action" => nil)}
|
|
40
|
-
elsif scope_observer_for(params.fetch("mapping_id")).sealed_service_stop_required?(lineage)
|
|
41
|
-
# A fresh canonical close resumes a stop lost after the seal.
|
|
42
|
-
# Replaying either mutation performs no manager I/O.
|
|
43
|
-
stop_after_commit = true
|
|
44
|
-
{events: [], blobs: {}, data: projection}
|
|
45
|
-
else
|
|
46
|
-
# This call must return a fresh validated observation, never
|
|
47
|
-
# a cached population, caller JSON or a manifest boolean.
|
|
48
|
-
payload = scope_observer_for(params.fetch("mapping_id")).closed_observation_for_proof!(lineage, events: events)
|
|
49
|
-
expected = binding.slice("scope_generation", "boot_id", "slice_invocation_id", "cgroup_identity").merge(
|
|
50
|
-
"scope_binding_event_id" => lineage.binding_event.fetch("digest"), "seal_event_id" => lineage.seal_event.fetch("digest"), "populated" => 0)
|
|
51
|
-
unless payload == expected
|
|
52
|
-
raise AttemptErrors::EvidenceUnavailable, "fresh observation does not name the retained sealed parent"
|
|
53
|
-
end
|
|
54
|
-
at = Time.now.utc
|
|
55
|
-
event = Models::EvidenceEvent.build(type: "scope_closed_no_writers", attempt_id: params.fetch("attempt_id"),
|
|
56
|
-
payload: payload, previous_digest: events.last&.fetch("digest"), recorded_at: at)
|
|
57
|
-
{events: [{type: "scope_closed_no_writers", payload: payload, recorded_at: at}], blobs: {},
|
|
58
|
-
data: projection.merge("state" => "closed_no_writers", "proof_id" => event.fetch("digest"), "required_action" => nil)}
|
|
59
|
-
end
|
|
30
|
+
closure = scope_closure_plan!(lineage: lineage, params: params, map: map, events: events, journal: journal, commit: _commit)
|
|
31
|
+
stop_after_commit = closure.fetch(:stop_required)
|
|
32
|
+
inhibit_after_commit = closure.fetch(:inhibit_required)
|
|
33
|
+
closure.fetch(:plan)
|
|
60
34
|
end
|
|
61
35
|
end
|
|
62
|
-
|
|
36
|
+
result
|
|
37
|
+
end
|
|
38
|
+
if !result.fetch(:replayed) && inhibit_after_commit
|
|
39
|
+
request_original_input_inhibition!(params, map, journal)
|
|
40
|
+
end
|
|
41
|
+
if !result.fetch(:replayed) && stop_after_commit
|
|
42
|
+
with_containment_exclusion(params, map, journal) do
|
|
43
|
+
@mutex.synchronize do
|
|
44
|
+
current = journal.read_events(params.fetch("assignment_id")).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
45
|
+
scope_close_owner!(params, map, current, peer, role)
|
|
46
|
+
end
|
|
63
47
|
scope_observer_for(params.fetch("mapping_id")).stop_sealed_service!
|
|
64
48
|
end
|
|
65
|
-
result
|
|
66
49
|
end
|
|
50
|
+
result
|
|
67
51
|
end
|
|
68
52
|
|
|
69
53
|
def observe_execution_scope!(params:, peer:, role:)
|
|
@@ -72,7 +56,7 @@ module Ace
|
|
|
72
56
|
@kernel.live!(peer)
|
|
73
57
|
map = @deployment.mapping(params.fetch("mapping_id"))
|
|
74
58
|
journal = journal_for(map)
|
|
75
|
-
|
|
59
|
+
with_containment_exclusion(params, map, journal) do
|
|
76
60
|
@mutex.synchronize do
|
|
77
61
|
commit = journal.ref_value
|
|
78
62
|
events = journal.read_events(params.fetch("assignment_id"), commit: commit).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
@@ -83,7 +67,9 @@ module Ace
|
|
|
83
67
|
return projection unless lineage.binding
|
|
84
68
|
begin
|
|
85
69
|
observation = scope_observer_for(params.fetch("mapping_id")).observe(lineage)
|
|
86
|
-
if
|
|
70
|
+
if pending_prompt_issuers?(events, journal, commit)
|
|
71
|
+
projection.merge("required_action" => "drain_original_prompt_issuer")
|
|
72
|
+
elsif lineage.proof_event
|
|
87
73
|
scope_observer_for(params.fetch("mapping_id")).verify_closed!(lineage)
|
|
88
74
|
projection.merge("state" => "closed_no_writers", "proof_id" => lineage.proof_id, "required_action" => nil)
|
|
89
75
|
elsif lineage.sealed? && observation.fetch("populated").zero?
|
|
@@ -100,6 +86,51 @@ module Ace
|
|
|
100
86
|
|
|
101
87
|
private
|
|
102
88
|
|
|
89
|
+
# Shared source-only plan for close and public stop. It does not issue
|
|
90
|
+
# manager/native control, mutate canonical state or acquire exclusions.
|
|
91
|
+
# Each CAS invocation derives its own post-commit work flags anew.
|
|
92
|
+
def scope_closure_plan!(lineage:, params:, map:, events:, journal:, commit:)
|
|
93
|
+
stop_required = false
|
|
94
|
+
inhibit_required = false
|
|
95
|
+
binding = lineage.binding
|
|
96
|
+
raise AttemptErrors::EvidenceUnavailable, "original parent binding is missing" unless binding
|
|
97
|
+
projection = {"attempt_id" => params.fetch("attempt_id"), "scope_generation" => binding.fetch("scope_generation"),
|
|
98
|
+
"state" => "running", "proof_id" => nil, "required_action" => "close_scope"}
|
|
99
|
+
plan = if !lineage.sealed?
|
|
100
|
+
stop_required = true
|
|
101
|
+
inhibit_required = true
|
|
102
|
+
payload = {"scope_generation" => binding.fetch("scope_generation"), "scope_binding_event_id" => lineage.binding_event.fetch("digest")}
|
|
103
|
+
{events: [{type: "scope_sealed", payload: payload}], blobs: {}, data: projection}
|
|
104
|
+
elsif @native_issuers.key?(native_issuer_key(params, map)) || pending_prompt_issuers?(events, journal, commit)
|
|
105
|
+
# A live issuer may still start after this inactive snapshot.
|
|
106
|
+
inhibit_required = true
|
|
107
|
+
{events: [], blobs: {}, data: projection}
|
|
108
|
+
elsif lineage.proof_event
|
|
109
|
+
scope_observer_for(params.fetch("mapping_id")).verify_closed!(lineage)
|
|
110
|
+
{events: [], blobs: {}, data: projection.merge("state" => "closed_no_writers", "proof_id" => lineage.proof_id, "required_action" => nil)}
|
|
111
|
+
elsif scope_observer_for(params.fetch("mapping_id")).sealed_service_stop_required?(lineage)
|
|
112
|
+
# A fresh canonical close resumes a stop lost after the seal.
|
|
113
|
+
# Replaying either mutation performs no manager I/O.
|
|
114
|
+
stop_required = true
|
|
115
|
+
{events: [], blobs: {}, data: projection}
|
|
116
|
+
else
|
|
117
|
+
# This call must return a fresh validated observation, never
|
|
118
|
+
# a cached population, caller JSON or a manifest boolean.
|
|
119
|
+
payload = scope_observer_for(params.fetch("mapping_id")).closed_observation_for_proof!(lineage, events: events)
|
|
120
|
+
expected = binding.slice("scope_generation", "boot_id", "slice_invocation_id", "cgroup_identity").merge(
|
|
121
|
+
"scope_binding_event_id" => lineage.binding_event.fetch("digest"), "seal_event_id" => lineage.seal_event.fetch("digest"), "populated" => 0)
|
|
122
|
+
unless payload == expected
|
|
123
|
+
raise AttemptErrors::EvidenceUnavailable, "fresh observation does not name the retained sealed parent"
|
|
124
|
+
end
|
|
125
|
+
at = Time.now.utc
|
|
126
|
+
event = Models::EvidenceEvent.build(type: "scope_closed_no_writers", attempt_id: params.fetch("attempt_id"),
|
|
127
|
+
payload: payload, previous_digest: events.last&.fetch("digest"), recorded_at: at)
|
|
128
|
+
{events: [{type: "scope_closed_no_writers", payload: payload, recorded_at: at}], blobs: {},
|
|
129
|
+
data: projection.merge("state" => "closed_no_writers", "proof_id" => event.fetch("digest"), "required_action" => nil)}
|
|
130
|
+
end
|
|
131
|
+
{plan: plan, stop_required: stop_required, inhibit_required: inhibit_required}
|
|
132
|
+
end
|
|
133
|
+
|
|
103
134
|
def scope_close_owner!(params, map, events, peer, role)
|
|
104
135
|
state = states(events)[params.fetch("attempt_id")]
|
|
105
136
|
unless state && state["mapping_id"] == params.fetch("mapping_id") &&
|