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
|
@@ -6,6 +6,7 @@ require "securerandom"
|
|
|
6
6
|
require_relative "candidate_transfer"
|
|
7
7
|
require_relative "receipt_transfer"
|
|
8
8
|
require_relative "service_evidence"
|
|
9
|
+
require_relative "service_delivery_evidence"
|
|
9
10
|
require_relative "../molecules/canonical_evidence"
|
|
10
11
|
|
|
11
12
|
module Ace
|
|
@@ -14,43 +15,81 @@ module Ace
|
|
|
14
15
|
# Business admission shares the launch origin, lifecycle exclusion and
|
|
15
16
|
# journal CAS. Network transfer is completed outside those locks.
|
|
16
17
|
class Endcap
|
|
17
|
-
OPERATIONS = %w[submit_candidate export_candidate assign_review accept_review request_service begin_dispatch complete_service service_authorization service_status submit_result evidence_fetch
|
|
18
|
+
OPERATIONS = %w[submit_candidate export_candidate request_review review_status assign_review cancel_review accept_review request_service begin_dispatch complete_service publication_challenge publication_continue claim_service_settlement complete_no_effect service_authorization service_status workspace_prune_preview_context submit_result evidence_fetch bind_inbox finish recover campaign_record_round campaign_export_result].freeze
|
|
18
19
|
TRANSFER_OPERATIONS = {
|
|
19
|
-
"
|
|
20
|
+
"campaign_export_result" => {direction: :download, purpose: :artifacts, roles: %i[worker launcher supervisor]},
|
|
21
|
+
"campaign_record_round" => {direction: :upload, purpose: :receipt_artifacts, roles: %i[launcher supervisor]},
|
|
20
22
|
"submit_result" => {direction: :upload, purpose: :receipt_artifacts, roles: [:worker]},
|
|
21
|
-
"evidence_fetch" => {direction: :download, purpose: :artifacts, roles: %i[worker reviewer executor launcher supervisor]},
|
|
23
|
+
"evidence_fetch" => {direction: :download, purpose: :artifacts, roles: %i[worker reviewer executor launcher supervisor signer]},
|
|
22
24
|
"submit_candidate" => {direction: :upload, purpose: :candidate, roles: %i[worker launcher]},
|
|
23
25
|
"export_candidate" => {direction: :download, purpose: :candidate, roles: %i[reviewer executor]},
|
|
24
26
|
"accept_review" => {direction: :upload, purpose: :receipt_artifacts, roles: [:reviewer]},
|
|
25
27
|
"request_service" => {direction: :upload, purpose: :service_input, roles: [:executor]},
|
|
26
28
|
"begin_dispatch" => {direction: :upload, purpose: :service_input, roles: [:executor]},
|
|
27
29
|
"service_authorization" => {direction: :upload, purpose: :service_input, roles: [:executor]},
|
|
28
|
-
"complete_service" => {direction: :upload, purpose: :receipt_artifacts, roles: [:executor]}
|
|
30
|
+
"complete_service" => {direction: :upload, purpose: :receipt_artifacts, roles: [:executor]},
|
|
31
|
+
"publication_challenge" => {direction: :upload, purpose: :receipt_artifacts, roles: [:executor]},
|
|
32
|
+
"complete_no_effect" => {direction: :upload, purpose: :receipt_artifacts, roles: [:executor]}
|
|
29
33
|
}.freeze
|
|
30
34
|
PARAMETERS = {
|
|
31
|
-
"
|
|
35
|
+
"campaign_export_result" => %w[mapping_id assignment_id attempt_id candidate_generation head],
|
|
36
|
+
"campaign_record_round" => %w[mapping_id assignment_id attempt_id candidate_generation head input_sha256 transfer],
|
|
32
37
|
"submit_result" => %w[mapping_id assignment_id attempt_id expected_generation candidate_generation head receipt_sha256 transfer],
|
|
33
38
|
"evidence_fetch" => %w[mapping_id assignment_id attempt_id kind purpose_id artifact_id],
|
|
34
39
|
"submit_candidate" => %w[mapping_id assignment_id attempt_id expected_generation candidate_generation head transfer],
|
|
35
40
|
"export_candidate" => %w[mapping_id assignment_id attempt_id candidate_generation head purpose_id],
|
|
41
|
+
"request_review" => %w[mapping_id assignment_id attempt_id expected_generation candidate_generation head],
|
|
36
42
|
"assign_review" => %w[mapping_id assignment_id attempt_id expected_generation candidate_generation head reviewer_uid reviewer_process_binding],
|
|
43
|
+
"cancel_review" => %w[mapping_id assignment_id attempt_id head candidate_generation review_event_id expected_generation],
|
|
37
44
|
"accept_review" => %w[mapping_id assignment_id attempt_id expected_generation candidate_generation head purpose_id receipt_sha256 transfer],
|
|
38
45
|
"request_service" => %w[mapping_id assignment_id attempt_id expected_generation candidate_generation head request_id operation input_digest target authorization service_id worker_process_binding transfer],
|
|
39
46
|
"begin_dispatch" => %w[mapping_id assignment_id attempt_id expected_generation candidate_generation head request_id claim_binding transfer],
|
|
47
|
+
"workspace_prune_preview_context" => %w[mapping_id assignment_id attempt_id service_id worker_process_binding],
|
|
40
48
|
"service_status" => %w[mapping_id assignment_id attempt_id candidate_generation head request_id],
|
|
41
49
|
"service_authorization" => %w[mapping_id assignment_id attempt_id candidate_generation head request_id claim_binding input_digest transfer],
|
|
42
|
-
"complete_service" => %w[mapping_id assignment_id attempt_id candidate_generation head request_id claim_binding receipt_sha256 transfer]
|
|
50
|
+
"complete_service" => %w[mapping_id assignment_id attempt_id candidate_generation head request_id claim_binding receipt_sha256 transfer],
|
|
51
|
+
"claim_service_settlement" => %w[mapping_id assignment_id attempt_id candidate_generation head request_id expected_generation],
|
|
52
|
+
"publication_challenge" => %w[mapping_id assignment_id attempt_id candidate_generation head request_id claim_binding input_digest expected_generation receipt_sha256 transfer],
|
|
53
|
+
"publication_continue" => %w[mapping_id assignment_id attempt_id candidate_generation head request_id claim_binding input_digest expected_generation challenge_digest],
|
|
54
|
+
"complete_no_effect" => %w[mapping_id assignment_id attempt_id candidate_generation head request_id claim_binding reconciliation_challenge receipt_sha256 transfer]
|
|
43
55
|
}.freeze
|
|
44
56
|
|
|
45
|
-
def initialize(deployment:, launch:, kernel: Ace::Runtime::Molecules::ProtectedLinux.new, service_policy: nil)
|
|
57
|
+
def initialize(deployment:, launch:, kernel: Ace::Runtime::Molecules::ProtectedLinux.new, service_policy: nil, inbox_context_clients: nil, deployment_history: nil)
|
|
46
58
|
@deployment, @launch, @kernel, @service_policy = deployment, launch, kernel, service_policy
|
|
59
|
+
@inbox_context_clients = inbox_context_clients
|
|
60
|
+
@deployment_history = deployment_history
|
|
47
61
|
launch.attach_result_owner(self) if launch.respond_to?(:attach_result_owner)
|
|
48
62
|
end
|
|
49
63
|
|
|
64
|
+
# Internal registration consumer only: caller holds the original parent
|
|
65
|
+
# lifecycle exclusion. Read one authenticated prefix; never accept a
|
|
66
|
+
# child's revisions as evidence of the parent's current candidate.
|
|
67
|
+
def campaign_parent_candidate!(journal:, commit:, params:, map:)
|
|
68
|
+
events = @launch.preview_attempt_events!(journal: journal, commit: commit, params: params, map: map)
|
|
69
|
+
original = active_origin(events, params)
|
|
70
|
+
current = candidate(events)
|
|
71
|
+
unless current && original.fetch("scope") == params.fetch("scope") &&
|
|
72
|
+
original.fetch("base_head") == params.fetch("base") &&
|
|
73
|
+
current.fetch("head") == params.fetch("head") &&
|
|
74
|
+
current.fetch("candidate_generation") == params.fetch("candidate_generation") && journal.ref_value == commit
|
|
75
|
+
raise AttemptErrors::Conflict, "campaign parent candidate changed"
|
|
76
|
+
end
|
|
77
|
+
projection = params.slice("assignment_id", "attempt_id").merge(
|
|
78
|
+
"scope" => original.fetch("scope"), "base" => original.fetch("base_head"),
|
|
79
|
+
"head" => current.fetch("head"), "candidate_generation" => current.fetch("candidate_generation"))
|
|
80
|
+
projection.transform_values { |value| value.is_a?(String) ? value.dup.freeze : value }.freeze
|
|
81
|
+
rescue KeyError, TypeError
|
|
82
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign parent candidate is unavailable"
|
|
83
|
+
end
|
|
84
|
+
|
|
50
85
|
def authorize_transfer!(request:, peer:, role:)
|
|
51
|
-
return
|
|
86
|
+
return dispatch_campaign_export(request: request, peer: peer, role: role, body: false) if request.fetch("operation") == "campaign_export_result"
|
|
87
|
+
return authorize_campaign_round!(request: request, peer: peer, role: role) if request.fetch("operation") == "campaign_record_round"
|
|
88
|
+
return dispatch_prepared_fetch(request: request, peer: peer, role: role, body: false) if prepared_fetch?(request)
|
|
52
89
|
return authorize_result_transfer!(request: request, peer: peer, role: role) if %w[submit_result evidence_fetch].include?(request.fetch("operation"))
|
|
53
90
|
params, map = validate_request(request)
|
|
91
|
+
return authorize_service_settlement!(request, params, map, peer, role) if request.fetch("operation") == "complete_no_effect"
|
|
92
|
+
return authorize_publication_transfer!(request, params, map, peer, role) if request.fetch("operation") == "publication_challenge"
|
|
54
93
|
return authorize_service_transfer!(request, params, map, peer, role) if %w[request_service begin_dispatch complete_service service_authorization].include?(request.fetch("operation"))
|
|
55
94
|
@launch.with_assignment(params: params, map: map) do |journal, _registration|
|
|
56
95
|
protected_journal!(journal)
|
|
@@ -58,7 +97,7 @@ module Ace
|
|
|
58
97
|
origin = active_origin(events, params)
|
|
59
98
|
if request.fetch("operation") == "submit_candidate"
|
|
60
99
|
worker_or_launcher!(peer, role, map, origin)
|
|
61
|
-
candidate_generation!(candidate(events), params)
|
|
100
|
+
candidate_generation!(candidate(events), params) unless candidate_submission_replay!(journal, events, request, params)
|
|
62
101
|
else
|
|
63
102
|
export_admission!(journal, events, params, map, origin, peer, role)
|
|
64
103
|
end
|
|
@@ -67,9 +106,23 @@ module Ace
|
|
|
67
106
|
end
|
|
68
107
|
|
|
69
108
|
def dispatch(request:, peer:, role:, transfer: nil)
|
|
70
|
-
return
|
|
109
|
+
return dispatch_campaign_export(request: request, peer: peer, role: role) if request.fetch("operation") == "campaign_export_result"
|
|
110
|
+
return dispatch_campaign_round(request: request, peer: peer, role: role, transfer: transfer) if request.fetch("operation") == "campaign_record_round"
|
|
111
|
+
if prepared_fetch?(request)
|
|
112
|
+
raise ArgumentError, "prepared fetch forbids upload" unless transfer.nil?
|
|
113
|
+
return dispatch_prepared_fetch(request: request, peer: peer, role: role)
|
|
114
|
+
end
|
|
115
|
+
return dispatch_bind_inbox(request: request, peer: peer, role: role, transfer: transfer) if request.fetch("operation") == "bind_inbox"
|
|
116
|
+
return dispatch_finish(request: request, peer: peer, role: role, transfer: transfer) if request.fetch("operation") == "finish"
|
|
117
|
+
return dispatch_recover(request: request, peer: peer, role: role, transfer: transfer) if request.fetch("operation") == "recover"
|
|
71
118
|
return dispatch_result(request: request, peer: peer, role: role, transfer: transfer) if %w[submit_result evidence_fetch].include?(request.fetch("operation"))
|
|
119
|
+
return review_status(request, peer, role, transfer) if request.fetch("operation") == "review_status"
|
|
72
120
|
params, map = validate_request(request)
|
|
121
|
+
return request_review(request, params, map, peer, role, transfer) if request.fetch("operation") == "request_review"
|
|
122
|
+
return cancel_review(request, params, map, peer, role, transfer) if request.fetch("operation") == "cancel_review"
|
|
123
|
+
return dispatch_publication(request, params, map, peer, role, transfer) if %w[publication_challenge publication_continue].include?(request.fetch("operation"))
|
|
124
|
+
return dispatch_service_settlement(request, params, map, peer, role, transfer) if %w[claim_service_settlement complete_no_effect].include?(request.fetch("operation"))
|
|
125
|
+
return workspace_prune_preview_context(request, params, map, peer, role, transfer) if request.fetch("operation") == "workspace_prune_preview_context"
|
|
73
126
|
return service_status(request, params, map, peer, role) if request.fetch("operation") == "service_status"
|
|
74
127
|
return dispatch_service(request, params, map, peer, role, transfer) if %w[request_service begin_dispatch complete_service service_authorization].include?(request.fetch("operation"))
|
|
75
128
|
admitted = nil
|
|
@@ -107,7 +160,9 @@ module Ace
|
|
|
107
160
|
worker_or_launcher!(peer, role, map, origin,
|
|
108
161
|
launcher_only: request.fetch("operation") == "assign_review")
|
|
109
162
|
end
|
|
110
|
-
journal
|
|
163
|
+
delegated_review_request!(journal, events, request, params, peer, role) if request.fetch("operation") == "assign_review"
|
|
164
|
+
candidate_submission_replay!(journal, events, request, params, admitted: admitted) if request.fetch("operation") == "submit_candidate"
|
|
165
|
+
result = journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
111
166
|
mutation_id: request.fetch("mutation_id"), operation: request.fetch("operation"),
|
|
112
167
|
parameters_digest: Atoms::EvidenceDigest.digest(params), expected_generation: params.fetch("expected_generation"),
|
|
113
168
|
with_replay: true) do |events, _commit, _generation|
|
|
@@ -125,6 +180,15 @@ module Ace
|
|
|
125
180
|
when "assign_review"
|
|
126
181
|
worker_or_launcher!(peer, role, map, origin, launcher_only: true)
|
|
127
182
|
current = exact_candidate!(candidate(events), params)
|
|
183
|
+
delegated = delegated_review_request!(journal, events, request, params, peer, role)
|
|
184
|
+
reservation = active_review_reservation(events, current)
|
|
185
|
+
if delegated
|
|
186
|
+
unless reservation && reservation.fetch("digest") == delegated.fetch("digest") && !active_review_event(events, current)
|
|
187
|
+
raise AttemptErrors::Conflict, "delegated review reservation is no longer assignable"
|
|
188
|
+
end
|
|
189
|
+
elsif reservation
|
|
190
|
+
raise AttemptErrors::Conflict, "candidate review reservation is active"
|
|
191
|
+
end
|
|
128
192
|
reviewer = params.fetch("reviewer_process_binding")
|
|
129
193
|
uid = params.fetch("reviewer_uid")
|
|
130
194
|
project = @deployment.project(map.fetch("project_id"))
|
|
@@ -142,6 +206,11 @@ module Ace
|
|
|
142
206
|
accept_review_plan(journal, events, params, map, current, review, admitted)
|
|
143
207
|
end
|
|
144
208
|
end
|
|
209
|
+
if request.fetch("operation") == "assign_review"
|
|
210
|
+
review_event_reply(journal, result, request, params, "assignment_event_id")
|
|
211
|
+
else
|
|
212
|
+
result
|
|
213
|
+
end
|
|
145
214
|
end
|
|
146
215
|
end
|
|
147
216
|
|
|
@@ -162,11 +231,11 @@ module Ace
|
|
|
162
231
|
%w[mapping_id assignment_id attempt_id].each do |key|
|
|
163
232
|
raise ArgumentError, "invalid Endcap identifier" unless params[key].is_a?(String) && params[key].match?(Molecules::JournalMutation::ID)
|
|
164
233
|
end
|
|
165
|
-
unless params["head"].is_a?(String) && params["head"].match?(CandidateTransfer::SHA) &&
|
|
166
|
-
params["candidate_generation"].is_a?(Integer) && params["candidate_generation"] >= 0
|
|
234
|
+
unless operation == "workspace_prune_preview_context" || (params["head"].is_a?(String) && params["head"].match?(CandidateTransfer::SHA) &&
|
|
235
|
+
params["candidate_generation"].is_a?(Integer) && params["candidate_generation"] >= 0)
|
|
167
236
|
raise ArgumentError, "invalid candidate binding"
|
|
168
237
|
end
|
|
169
|
-
if %w[service_authorization service_status].include?(operation) && !request.fetch("mutation_id").nil?
|
|
238
|
+
if %w[service_authorization service_status workspace_prune_preview_context].include?(operation) && !request.fetch("mutation_id").nil?
|
|
170
239
|
raise ArgumentError, "authorization read must not supply a mutation ID"
|
|
171
240
|
end
|
|
172
241
|
map = @deployment.mapping(params.fetch("mapping_id"))
|
|
@@ -209,6 +278,34 @@ module Ace
|
|
|
209
278
|
event&.dig("payload", "data")
|
|
210
279
|
end
|
|
211
280
|
|
|
281
|
+
# Header replay admission still requires the live original caller above.
|
|
282
|
+
# TransferCodec and CandidateTransfer verify the exact body before dispatch;
|
|
283
|
+
# the descriptor is part of the original immutable parameter digest.
|
|
284
|
+
def candidate_submission_replay!(journal, events, request, params, admitted: nil)
|
|
285
|
+
replay = journal.mutation_result(request.fetch("mutation_id"))
|
|
286
|
+
return false unless replay
|
|
287
|
+
unless replay["operation"] == "submit_candidate" && replay["assignment_id"] == params.fetch("assignment_id") &&
|
|
288
|
+
replay["attempt_id"] == params.fetch("attempt_id") && replay["parameters_digest"] == Atoms::EvidenceDigest.digest(params)
|
|
289
|
+
raise AttemptErrors::Conflict, "candidate replay input differs"
|
|
290
|
+
end
|
|
291
|
+
data = replay.fetch("data")
|
|
292
|
+
retained = retained_candidate(events, data.fetch("candidate_generation"))
|
|
293
|
+
unless retained == data && data.fetch("candidate_generation") == params.fetch("candidate_generation") + 1 &&
|
|
294
|
+
data.fetch("head") == params.fetch("head")
|
|
295
|
+
raise AttemptErrors::EvidenceUnavailable, "candidate replay provenance differs"
|
|
296
|
+
end
|
|
297
|
+
bytes = journal.bounded_blob(data.fetch("bundle_ref"), commit: replay.fetch("journal_commit"), max_bytes: 64 * 1024 * 1024)
|
|
298
|
+
unless bytes.bytesize == data.fetch("bytes") && Digest::SHA256.hexdigest(bytes) == data.fetch("sha256")
|
|
299
|
+
raise AttemptErrors::EvidenceUnavailable, "candidate replay bundle differs"
|
|
300
|
+
end
|
|
301
|
+
if admitted && admitted.values_at("head", "tree", "sha256", "bytes") != data.values_at("head", "tree", "sha256", "bytes")
|
|
302
|
+
raise AttemptErrors::EvidenceUnavailable, "candidate replay normalized bundle differs"
|
|
303
|
+
end
|
|
304
|
+
true
|
|
305
|
+
rescue KeyError, TypeError
|
|
306
|
+
raise AttemptErrors::EvidenceUnavailable, "candidate replay evidence is malformed"
|
|
307
|
+
end
|
|
308
|
+
|
|
212
309
|
def candidate_generation!(current, params)
|
|
213
310
|
unless (current && current.fetch("candidate_generation") || 0) == params.fetch("candidate_generation")
|
|
214
311
|
raise AttemptErrors::Conflict, "candidate generation changed"
|
|
@@ -225,7 +322,7 @@ module Ace
|
|
|
225
322
|
current = exact_candidate!(candidate(events), params)
|
|
226
323
|
@kernel.live!(peer)
|
|
227
324
|
if role == :reviewer
|
|
228
|
-
assignment =
|
|
325
|
+
assignment = active_review_event(events, current)&.dig("payload", "data")
|
|
229
326
|
unless assignment && assignment["review_id"] == params["purpose_id"] && assignment["head"] == current["head"] &&
|
|
230
327
|
assignment["candidate_generation"] == current["candidate_generation"] && assignment["reviewer_uid"] == peer["uid"] &&
|
|
231
328
|
@kernel.descendant?(peer, assignment.fetch("reviewer_process_binding"))
|
|
@@ -251,10 +348,10 @@ module Ace
|
|
|
251
348
|
# A retained approval grants permission only while its original imported
|
|
252
349
|
# bytes, independent reviewer and exact candidate still verify. An old
|
|
253
350
|
# successful mutation reply is not a substitute for canonical evidence.
|
|
254
|
-
def approved_review!(journal, events, params, map, current)
|
|
351
|
+
def approved_review!(journal, events, params, map, current, commit: journal.ref_value, service_operation: nil)
|
|
255
352
|
accepted = events.reverse.find { |event| event["type"] == "authority_mutation" &&
|
|
256
353
|
event.dig("payload", "operation") == "accept_review" }&.dig("payload", "data")
|
|
257
|
-
review =
|
|
354
|
+
review = active_review_event(events, current)&.dig("payload", "data")
|
|
258
355
|
unless accepted && review && accepted["head"] == current["head"] &&
|
|
259
356
|
accepted["candidate_generation"] == current["candidate_generation"] &&
|
|
260
357
|
accepted["review_id"] == review["review_id"] && accepted["reviewer_uid"] == review["reviewer_uid"] &&
|
|
@@ -267,7 +364,6 @@ module Ace
|
|
|
267
364
|
binding: accepted.fetch("review_binding"), request_id_or_event_id: review.fetch("review_id"),
|
|
268
365
|
generation: current.fetch("candidate_generation")}
|
|
269
366
|
canonical = Molecules::CanonicalEvidence.new(journal: journal)
|
|
270
|
-
commit = journal.ref_value
|
|
271
367
|
reader = ->(_data, artifact) { canonical.read({"ref" => artifact.fetch("path"), "sha256" => artifact.fetch("sha256")},
|
|
272
368
|
**context, commit: commit) }
|
|
273
369
|
intent = events.find { |event| event["type"] == "intent" }
|
|
@@ -280,6 +376,9 @@ module Ace
|
|
|
280
376
|
receipt.to_h.dig("review", "reviewer", "actor") == review.fetch("reviewer_actor")
|
|
281
377
|
raise AttemptErrors::ReceiptRejected, "canonical review receipt differs"
|
|
282
378
|
end
|
|
379
|
+
if %w[ready merge].include?(service_operation)
|
|
380
|
+
campaign_service_checks!(journal: journal, commit: commit, events: events, params: params, map: map, current: current)
|
|
381
|
+
end
|
|
283
382
|
accepted
|
|
284
383
|
rescue KeyError
|
|
285
384
|
raise AttemptErrors::ReceiptRejected, "canonical review binding is incomplete"
|
|
@@ -336,7 +435,21 @@ module Ace
|
|
|
336
435
|
end
|
|
337
436
|
|
|
338
437
|
require_relative "endcap_services"
|
|
438
|
+
require_relative "endcap_service_settlement"
|
|
339
439
|
|
|
440
|
+
require_relative "endcap_campaign_results"
|
|
441
|
+
require_relative "endcap_campaign_rounds"
|
|
340
442
|
require_relative "endcap_results"
|
|
341
443
|
|
|
342
444
|
require_relative "endcap_inboxes"
|
|
445
|
+
|
|
446
|
+
require_relative "endcap_contexts"
|
|
447
|
+
require_relative "endcap_prepared_work"
|
|
448
|
+
|
|
449
|
+
require_relative "endcap_reviews"
|
|
450
|
+
|
|
451
|
+
require_relative "endcap_review_requests"
|
|
452
|
+
|
|
453
|
+
require_relative "endcap_attempts"
|
|
454
|
+
|
|
455
|
+
require_relative "endcap_publication"
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Assign
|
|
5
|
+
module Authority
|
|
6
|
+
class Endcap
|
|
7
|
+
BIND_INBOX_PARAMETERS = %w[mapping_id assignment_id attempt_id expected_generation event_id inbox_context_id].freeze
|
|
8
|
+
FINISH_PARAMETERS = %w[mapping_id assignment_id attempt_id expected_generation candidate_generation head result_id].freeze
|
|
9
|
+
RECOVERY_PARAMETERS = %w[mapping_id assignment_id attempt_id expected_generation].freeze
|
|
10
|
+
|
|
11
|
+
# Called only by the canonical terminal owner at its authenticated
|
|
12
|
+
# introduction prefix, using the original deployment mapping.
|
|
13
|
+
def finished_review_evidence!(journal:, events:, params:, map:, commit:)
|
|
14
|
+
current = exact_candidate!(retained_candidate(events, params.fetch("candidate_generation")), params)
|
|
15
|
+
accepted = approved_review!(journal, events, params, map, current, commit: commit)
|
|
16
|
+
result = verified_result(journal, events, params, map, retained_origin(events, params), current, commit)
|
|
17
|
+
raise AttemptErrors::EvidenceUnavailable, "finished canonical result is missing" unless result
|
|
18
|
+
campaign_finished_result!(journal: journal, commit: commit, events: events, params: params, map: map,
|
|
19
|
+
result: result, accepted: accepted)
|
|
20
|
+
@launch.campaign_children_settled!(journal: journal, commit: commit, params: params, map: map)
|
|
21
|
+
accepted
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def dispatch_recover(request:, peer:, role:, transfer: nil)
|
|
27
|
+
raise ArgumentError, "Recovery forbids transfer" unless transfer.nil?
|
|
28
|
+
params, map = attempt_consumer_request!(request, RECOVERY_PARAMETERS)
|
|
29
|
+
selected_commit = nil
|
|
30
|
+
journal = @launch.journals.fetch(map.fetch("project_id"))
|
|
31
|
+
readonly = @launch.with_assignment(params: params, map: map) do |selected, _|
|
|
32
|
+
protected_journal!(selected)
|
|
33
|
+
selected_commit = selected.ref_value
|
|
34
|
+
events = @launch.preview_attempt_events!(journal: selected, commit: selected_commit, params: params, map: map)
|
|
35
|
+
finish_identity!(events, params, map, peer, role)
|
|
36
|
+
accepted = selected.mutation_result(request.fetch("mutation_id"), commit: selected_commit)
|
|
37
|
+
if accepted
|
|
38
|
+
verify_recovery_reply!(selected, accepted, request, params, map)
|
|
39
|
+
next({data: accepted.fetch("data").merge("journal_commit" => accepted.fetch("journal_commit")), replayed: true})
|
|
40
|
+
end
|
|
41
|
+
state = Molecules::CanonicalAttemptState.derive(events)
|
|
42
|
+
next unless %w[succeeded failed stopped].include?(state)
|
|
43
|
+
terminal = @launch.completion_terminal!(journal: selected, commit: selected_commit, params: params, map: map)
|
|
44
|
+
{data: {"attempt_id" => params.fetch("attempt_id"), "state" => terminal.fetch("canonical_state"),
|
|
45
|
+
"decision" => "restart-required", "reason" => "terminal_attempt", "generation" => terminal.fetch("generation"),
|
|
46
|
+
"journal_commit" => selected_commit}, replayed: false}
|
|
47
|
+
end
|
|
48
|
+
return readonly if readonly
|
|
49
|
+
recover_under_exclusion!(request, params, map, peer, role, selected_commit)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def recover_under_exclusion!(request, params, map, peer, role, selected_commit)
|
|
53
|
+
@launch.with_assignment(params: params, map: map, exclusive: true) do |journal, _|
|
|
54
|
+
unless journal.ref_value == selected_commit
|
|
55
|
+
raise AttemptErrors::Conflict, "Recovery canonical selection advanced"
|
|
56
|
+
end
|
|
57
|
+
events = @launch.preview_attempt_events!(journal: journal, commit: selected_commit, params: params, map: map)
|
|
58
|
+
finish_identity!(events, params, map, peer, role)
|
|
59
|
+
journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
60
|
+
mutation_id: request.fetch("mutation_id"), operation: "recover", parameters_digest: Atoms::EvidenceDigest.digest(params),
|
|
61
|
+
expected_generation: params.fetch("expected_generation"), with_replay: true) do |fresh, prefix, _generation|
|
|
62
|
+
unless prefix == selected_commit
|
|
63
|
+
raise AttemptErrors::Conflict, "Recovery canonical selection advanced"
|
|
64
|
+
end
|
|
65
|
+
origin = finish_identity!(fresh, params, map, peer, role)
|
|
66
|
+
state = Molecules::CanonicalAttemptState.derive(fresh)
|
|
67
|
+
effects_pending = recovery_pending_settlement do
|
|
68
|
+
service_settlement_evidence!(journal: journal, events: fresh, params: params, map: map, commit: prefix)
|
|
69
|
+
end
|
|
70
|
+
bound = %w[bound issued].include?(origin["phase"])
|
|
71
|
+
observation = if !effects_pending && state == "running" && bound
|
|
72
|
+
@launch.recovery_observation!(journal: journal, events: fresh, params: params, map: map, commit: prefix)
|
|
73
|
+
else
|
|
74
|
+
{"liveness" => "unknown"}
|
|
75
|
+
end
|
|
76
|
+
decision, reason = Organisms::AttemptCoordinator.protected_recovery_decision(state: state,
|
|
77
|
+
effects_pending: effects_pending, bound: bound,
|
|
78
|
+
live: observation["liveness"] == "live", checkpoint: true)
|
|
79
|
+
plan = Organisms::AttemptCoordinator.protected_recovery_plan(events: fresh, decision: decision, reason: reason, observation: observation)
|
|
80
|
+
plan.except(:state).merge(data: {"attempt_id" => params.fetch("attempt_id"), "state" => plan.fetch(:state),
|
|
81
|
+
"decision" => decision, "reason" => reason})
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def recovery_pending_settlement
|
|
87
|
+
yield
|
|
88
|
+
false
|
|
89
|
+
rescue AttemptErrors::ServiceSettlementPending
|
|
90
|
+
true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def verify_recovery_reply!(journal, accepted, request, params, map)
|
|
94
|
+
unless accepted["operation"] == "recover" && accepted["parameters_digest"] == Atoms::EvidenceDigest.digest(params) &&
|
|
95
|
+
accepted.values_at("assignment_id", "attempt_id") == params.values_at("assignment_id", "attempt_id")
|
|
96
|
+
raise AttemptErrors::Conflict, "Recovery mutation is already bound to different input"
|
|
97
|
+
end
|
|
98
|
+
prefix = accepted.fetch("journal_commit")
|
|
99
|
+
events = @launch.preview_attempt_events!(journal: journal, commit: prefix, params: params, map: map)
|
|
100
|
+
index = events.index { |event| event.dig("payload", "mutation_id") == request.fetch("mutation_id") && event.dig("payload", "operation") == "recover" }
|
|
101
|
+
raise AttemptErrors::EvidenceUnavailable, "Recovery acceptance is missing" unless index && index.positive?
|
|
102
|
+
before = events.take(index)
|
|
103
|
+
transition = before.last["type"] == "transition" && before.last.dig("payload", "to") == "uncertain" ? before.pop : nil
|
|
104
|
+
observation = before.last
|
|
105
|
+
data = accepted.fetch("data")
|
|
106
|
+
reasons = %w[live_owner unresolved_effect unresolved_inbox launch_unbound scope_unverifiable attempt_uncertain checkpoint_unavailable]
|
|
107
|
+
unless data.is_a?(Hash) && data.keys.sort == %w[attempt_id state decision reason generation].sort &&
|
|
108
|
+
data["attempt_id"] == params.fetch("attempt_id") && data["generation"].is_a?(Integer) &&
|
|
109
|
+
data["generation"] == journal.authority_generation(events) && reasons.include?(data["reason"]) &&
|
|
110
|
+
((data["decision"] == "adopt" && data["reason"] == "live_owner" && data["state"] == "running") ||
|
|
111
|
+
(data["decision"] == "reconcile-required" && data["reason"] != "live_owner")) &&
|
|
112
|
+
events[index]["type"] == "authority_mutation" && index == events.length - 1
|
|
113
|
+
raise AttemptErrors::EvidenceUnavailable, "Recovery reply schema or accepted prefix differs"
|
|
114
|
+
end
|
|
115
|
+
unless observation && observation["type"] == "recovery_observation" && observation.dig("payload", "decision") == data["decision"] &&
|
|
116
|
+
observation.dig("payload", "recovery_reason") == data["reason"] && observation.dig("payload", "observation").is_a?(Hash) &&
|
|
117
|
+
(!transition || transition.fetch("payload") == {"from" => "running", "to" => "uncertain", "reason" => data.fetch("reason")}) &&
|
|
118
|
+
Molecules::CanonicalAttemptState.derive(events) == data.fetch("state")
|
|
119
|
+
raise AttemptErrors::EvidenceUnavailable, "Recovery observation acceptance differs"
|
|
120
|
+
end
|
|
121
|
+
binding = retained_origin(events, params).fetch("process_binding", nil)
|
|
122
|
+
observed = observation.dig("payload", "observation")
|
|
123
|
+
if observed["liveness"] == "live" && (!binding || observed["runtime_binding"] != binding || observed["identity"] != binding.fetch("process_identity"))
|
|
124
|
+
raise AttemptErrors::EvidenceUnavailable, "Recovery original live binding differs"
|
|
125
|
+
end
|
|
126
|
+
true
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def dispatch_finish(request:, peer:, role:, transfer: nil)
|
|
130
|
+
raise ArgumentError, "Result finish forbids transfer" unless transfer.nil?
|
|
131
|
+
params, map = attempt_consumer_request!(request, FINISH_PARAMETERS)
|
|
132
|
+
unless params["candidate_generation"].is_a?(Integer) && params["candidate_generation"].positive? &&
|
|
133
|
+
params["head"].is_a?(String) && params["head"].match?(CandidateTransfer::SHA) &&
|
|
134
|
+
params["result_id"].is_a?(String) && params["result_id"].match?(/\A[0-9a-f]{32}\z/)
|
|
135
|
+
raise ArgumentError, "Result finish selectors differ"
|
|
136
|
+
end
|
|
137
|
+
journal = @launch.journals.fetch(map.fetch("project_id"))
|
|
138
|
+
replay = @launch.with_assignment(params: params, map: map) do |selected, _|
|
|
139
|
+
commit = selected.ref_value
|
|
140
|
+
accepted = selected.mutation_result(request.fetch("mutation_id"), commit: commit)
|
|
141
|
+
next unless accepted
|
|
142
|
+
unless accepted["operation"] == "finish" && accepted["parameters_digest"] == Atoms::EvidenceDigest.digest(params) &&
|
|
143
|
+
accepted.values_at("assignment_id", "attempt_id") == params.values_at("assignment_id", "attempt_id")
|
|
144
|
+
raise AttemptErrors::Conflict, "Finish mutation is already bound to different input"
|
|
145
|
+
end
|
|
146
|
+
events = @launch.preview_attempt_events!(journal: selected, commit: commit, params: params, map: map)
|
|
147
|
+
finish_identity!(events, params, map, peer, role)
|
|
148
|
+
@launch.completion_terminal!(journal: selected, commit: commit, params: params, map: map)
|
|
149
|
+
{data: accepted.fetch("data").merge("journal_commit" => accepted.fetch("journal_commit")), replayed: true}
|
|
150
|
+
end
|
|
151
|
+
return replay if replay
|
|
152
|
+
@launch.with_assignment(params: params, map: map, exclusive: true) do |selected, _|
|
|
153
|
+
protected_journal!(selected)
|
|
154
|
+
commit = selected.ref_value
|
|
155
|
+
events = @launch.preview_attempt_events!(journal: selected, commit: commit, params: params, map: map)
|
|
156
|
+
finish_identity!(events, params, map, peer, role)
|
|
157
|
+
replay = selected.mutation_result(request.fetch("mutation_id"), commit: commit)
|
|
158
|
+
if replay && replay["operation"] == "finish"
|
|
159
|
+
@launch.completion_terminal!(journal: selected, commit: commit, params: params, map: map)
|
|
160
|
+
end
|
|
161
|
+
with_campaign_finish!(journal: selected, commit: commit, events: events, params: params, map: map) do
|
|
162
|
+
selected.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
163
|
+
mutation_id: request.fetch("mutation_id"), operation: "finish", parameters_digest: Atoms::EvidenceDigest.digest(params),
|
|
164
|
+
expected_generation: params.fetch("expected_generation"), with_replay: true) do |fresh, prefix, _generation|
|
|
165
|
+
origin = finish_identity!(fresh, params, map, peer, role)
|
|
166
|
+
unless Molecules::CanonicalAttemptState.derive(fresh) == "running" && %w[bound issued].include?(origin["phase"])
|
|
167
|
+
raise AttemptErrors::Conflict, "Result finish requires an active bound attempt"
|
|
168
|
+
end
|
|
169
|
+
current = exact_candidate!(candidate(fresh), params)
|
|
170
|
+
result = verified_result(selected, fresh, params, map, origin, current, prefix, result_id: params.fetch("result_id"))
|
|
171
|
+
unless result && result.fetch("result_id") == params.fetch("result_id")
|
|
172
|
+
raise AttemptErrors::EvidenceUnavailable, "Result finish requires its exact submitted receipt"
|
|
173
|
+
end
|
|
174
|
+
if result.dig("receipt", "verdict") == "succeeded"
|
|
175
|
+
accepted = approved_review!(selected, fresh, params, map, current, commit: prefix)
|
|
176
|
+
campaign_finished_result!(journal: selected, commit: prefix, events: fresh, params: params, map: map,
|
|
177
|
+
result: result, accepted: accepted)
|
|
178
|
+
end
|
|
179
|
+
@launch.campaign_children_settled!(journal: selected, commit: prefix, params: params, map: map)
|
|
180
|
+
@launch.completion_scope!(journal: selected, events: fresh, params: params, map: map, commit: prefix)
|
|
181
|
+
service_settlement_evidence!(journal: selected, events: fresh, params: params, map: map, commit: prefix)
|
|
182
|
+
plan = Organisms::AttemptCoordinator.finished_transition_plan(events: fresh, receipt: result.fetch("receipt"))
|
|
183
|
+
plan.merge(data: params.slice("attempt_id", "result_id", "head", "candidate_generation").merge(
|
|
184
|
+
"receipt_digest" => result.fetch("receipt_digest"), "state" => result.dig("receipt", "verdict")))
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def finish_identity!(events, params, map, peer, role)
|
|
191
|
+
unless %i[launcher supervisor].include?(role)
|
|
192
|
+
raise AttemptErrors::UnauthorizedIdentity, "Result finish requires mapped completion owner"
|
|
193
|
+
end
|
|
194
|
+
origin = retained_origin(events, params)
|
|
195
|
+
purpose_peer!(events, params, map, origin, peer, role, kind: "result")
|
|
196
|
+
origin
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def attempt_consumer_request!(request, fields)
|
|
200
|
+
params = request.fetch("params")
|
|
201
|
+
unless params.is_a?(Hash) && params.keys.sort == fields.sort
|
|
202
|
+
raise ArgumentError, "attempt consumer parameters differ"
|
|
203
|
+
end
|
|
204
|
+
%w[mapping_id assignment_id attempt_id].each { |key| result_id!(params.fetch(key)) }
|
|
205
|
+
result_id!(request.fetch("mutation_id"))
|
|
206
|
+
unless params["expected_generation"].is_a?(Integer) && params["expected_generation"] >= 0
|
|
207
|
+
raise ArgumentError, "attempt consumer generation differs"
|
|
208
|
+
end
|
|
209
|
+
map = @deployment.mapping(params.fetch("mapping_id"))
|
|
210
|
+
unless map.fetch("project_id") == request.fetch("project_id")
|
|
211
|
+
raise AttemptErrors::UnauthorizedIdentity, "attempt consumer project differs"
|
|
212
|
+
end
|
|
213
|
+
[params, map]
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Original registration comes from the admitted fixed Inbox owner, not
|
|
217
|
+
# from peer fields or a current filesystem receipt. Bind and seal share
|
|
218
|
+
# the same canonical slot/task/assignment exclusion and CAS.
|
|
219
|
+
def dispatch_bind_inbox(request:, peer:, role:, transfer: nil)
|
|
220
|
+
raise ArgumentError, "Inbox binding forbids transfer" unless transfer.nil?
|
|
221
|
+
params, map = attempt_consumer_request!(request, BIND_INBOX_PARAMETERS)
|
|
222
|
+
%w[event_id inbox_context_id].each { |key| result_id!(params.fetch(key)) }
|
|
223
|
+
with_inbox_context(params, map, mutation_id: request.fetch("mutation_id")) do |session|
|
|
224
|
+
@launch.with_assignment(params: params, map: map, exclusive: true) do |journal, _|
|
|
225
|
+
protected_journal!(journal)
|
|
226
|
+
commit = journal.ref_value
|
|
227
|
+
events = @launch.preview_attempt_events!(journal: journal, commit: commit, params: params, map: map)
|
|
228
|
+
binding = bind_inbox_selection!(events, params, map, peer, role, session)
|
|
229
|
+
replay = journal.mutation_result(request.fetch("mutation_id"), commit: commit)
|
|
230
|
+
verify_inbox_binding_reply!(events, replay, binding) if replay && replay["operation"] == "bind_inbox"
|
|
231
|
+
journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
232
|
+
mutation_id: request.fetch("mutation_id"), operation: "bind_inbox", parameters_digest: Atoms::EvidenceDigest.digest(params),
|
|
233
|
+
expected_generation: params.fetch("expected_generation"), with_replay: true) do |fresh, _prefix, _generation|
|
|
234
|
+
@launch.scope_open_for_effect!(events: fresh, params: params, map: map)
|
|
235
|
+
selected = bind_inbox_selection!(fresh, params, map, peer, role, session)
|
|
236
|
+
existing = fresh.select { |event| event["type"] == "inbox_binding" && event.dig("payload", "event_id") == params.fetch("event_id") }
|
|
237
|
+
unless existing.empty? || (existing.one? && existing.first.fetch("payload") == selected)
|
|
238
|
+
raise AttemptErrors::Conflict, "Inbox registration is already bound differently"
|
|
239
|
+
end
|
|
240
|
+
{events: existing.empty? ? [{type: "inbox_binding", payload: selected}] : [], blobs: {}, data: selected}
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def bind_inbox_selection!(events, params, map, peer, role, session)
|
|
247
|
+
unless Molecules::CanonicalAttemptState.derive(events) == "running"
|
|
248
|
+
raise AttemptErrors::EvidenceUnavailable, "Inbox binding requires an active original worker"
|
|
249
|
+
end
|
|
250
|
+
submitting_worker!(events, params, map, peer, role)
|
|
251
|
+
context = @deployment.inbox_context(params.fetch("mapping_id"), params.fetch("inbox_context_id"))
|
|
252
|
+
lineage = Molecules::ExecutionScopeLineage.new(events: events, project_id: map.fetch("project_id"),
|
|
253
|
+
assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"), mapping_id: context.fetch("native_mapping_id"))
|
|
254
|
+
original = params.slice("assignment_id", "mapping_id", "inbox_context_id").merge("project_id" => map.fetch("project_id"))
|
|
255
|
+
record = session.request("status_context", {"event_id" => params.fetch("event_id"),
|
|
256
|
+
"attempt_id" => params.fetch("attempt_id"), "original" => original}).fetch("record")
|
|
257
|
+
registration = record.slice(*INBOX_REGISTRATION_FIELDS)
|
|
258
|
+
unless inbox_registration?(registration) && registration.values_at("event_id", "attempt_id") == params.values_at("event_id", "attempt_id")
|
|
259
|
+
raise AttemptErrors::EvidenceUnavailable, "Inbox original registration differs"
|
|
260
|
+
end
|
|
261
|
+
inbox_native_lineage!(record, lineage)
|
|
262
|
+
params.slice("event_id", "attempt_id", "inbox_context_id").merge("registration" => registration)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def verify_inbox_binding_reply!(events, reply, selected)
|
|
266
|
+
bindings = events.select { |event| event["type"] == "inbox_binding" && event.fetch("payload") == selected }
|
|
267
|
+
unless bindings.one? && reply.fetch("data").except("generation") == selected
|
|
268
|
+
raise AttemptErrors::EvidenceUnavailable, "Inbox binding replay provenance differs"
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|