ace-assign 0.63.1 → 0.66.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +11 -0
- data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +2 -2
- data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
- data/.ace-defaults/assign/catalog/steps/release-minor.step.yml +4 -2
- data/.ace-defaults/assign/catalog/steps/release.step.yml +4 -2
- data/.ace-defaults/assign/catalog/steps/update-pr-desc.step.yml +1 -1
- data/.ace-defaults/assign/presets/fix-bug.yml +15 -3
- data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +16 -4
- data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +15 -3
- data/.ace-defaults/assign/presets/work-on-task.yml +16 -4
- data/CHANGELOG.md +62 -0
- data/docs/demo/canonical-skill-source.tape.yml +4 -4
- data/docs/protected-authority.md +282 -0
- data/docs/usage.md +205 -3
- data/exe/ace-scope-ready +55 -0
- data/handbook/workflow-instructions/assign/drive.wf.md +28 -3
- data/handbook/workflow-instructions/assign/prepare.wf.md +2 -2
- data/lib/ace/assign/atoms/attempt_state_machine.rb +8 -0
- data/lib/ace/assign/atoms/delivery_parameters.rb +19 -0
- 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 +327 -0
- data/lib/ace/assign/authority/cleanup_result_protection.rb +53 -0
- data/lib/ace/assign/authority/client.rb +257 -0
- data/lib/ace/assign/authority/deployment.rb +627 -0
- data/lib/ace/assign/authority/deployment_history.rb +146 -0
- data/lib/ace/assign/authority/endcap.rb +455 -0
- 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 +37 -0
- 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 +398 -0
- 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 +598 -0
- data/lib/ace/assign/authority/evidence_transfer.rb +67 -0
- data/lib/ace/assign/authority/execution_scope_observation.rb +903 -0
- 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 +393 -0
- data/lib/ace/assign/authority/launch_input_inhibition.rb +132 -0
- data/lib/ace/assign/authority/launch_lifecycle.rb +997 -0
- 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 +81 -0
- data/lib/ace/assign/authority/launch_scope_admission.rb +215 -0
- data/lib/ace/assign/authority/launch_scope_close.rb +146 -0
- data/lib/ace/assign/authority/launch_scope_parent.rb +44 -0
- data/lib/ace/assign/authority/launch_scope_release.rb +859 -0
- 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 +59 -0
- 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/private_directory.rb +33 -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/receipt_transfer.rb +48 -0
- data/lib/ace/assign/authority/router.rb +76 -0
- data/lib/ace/assign/authority/server.rb +300 -0
- 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 +286 -0
- 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 +187 -0
- 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 +97 -0
- data/lib/ace/assign/cli/commands/authority/serve.rb +31 -0
- data/lib/ace/assign/cli/commands/authority/status.rb +26 -0
- 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/terminate.rb +29 -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 +11 -1
- data/lib/ace/assign/cli/commands/delivery.rb +107 -0
- 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 +38 -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 +56 -4
- 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 +4 -2
- data/lib/ace/assign/molecules/assignment_manager.rb +12 -2
- data/lib/ace/assign/molecules/attempt_reconciler.rb +51 -12
- data/lib/ace/assign/molecules/canonical_attempt_state.rb +151 -0
- data/lib/ace/assign/molecules/canonical_evidence.rb +153 -0
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +374 -0
- data/lib/ace/assign/molecules/evidence_journal.rb +922 -91
- data/lib/ace/assign/molecules/execution_identity_resolver.rb +49 -9
- data/lib/ace/assign/molecules/execution_scope_lineage.rb +345 -0
- 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 +271 -0
- data/lib/ace/assign/molecules/journal_prompt_mutation.rb +241 -0
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +182 -26
- data/lib/ace/assign/molecules/original_launch_binding.rb +37 -0
- data/lib/ace/assign/molecules/proposal_journal.rb +151 -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 +68 -32
- 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 +10 -4
- data/lib/ace/assign/organisms/attempt_coordinator.rb +302 -13
- data/lib/ace/assign/organisms/delivery_coordinator.rb +342 -0
- 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 +11 -4
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +4 -102
- metadata +186 -8
|
@@ -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
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Assign
|
|
5
|
+
module Authority
|
|
6
|
+
class Endcap
|
|
7
|
+
# Internal R1 dependency. The fixed authority consumer supplies the
|
|
8
|
+
# original expected linkage; no metadata document can attest execution.
|
|
9
|
+
# Raw imported artifacts are read at this same immutable prefix.
|
|
10
|
+
def with_campaign_execution_evidence!(journal:, commit:, params:, map:, receipt_digest:, execution_binding:)
|
|
11
|
+
raise ArgumentError, "campaign evidence requires a block" unless block_given?
|
|
12
|
+
begin
|
|
13
|
+
row = @launch.completion_terminal!(journal: journal, commit: commit, params: params, map: map)
|
|
14
|
+
unless row["canonical_state"] == "succeeded" && row["reservation_release_event_id"]
|
|
15
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign execution is not succeeded and released"
|
|
16
|
+
end
|
|
17
|
+
definition = @launch.preview_attempt_definition!(journal: journal, commit: commit, params: params, map: map)
|
|
18
|
+
execution = definition.campaign_execution
|
|
19
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign execution linkage is missing" unless execution
|
|
20
|
+
binding = execution.merge(params.slice("assignment_id", "attempt_id"),
|
|
21
|
+
"definition_digest" => row.fetch("definition_digest"), "binding_digest" => row.fetch("original_binding_digest"))
|
|
22
|
+
unless binding == execution_binding && binding["binding_digest"].is_a?(String)
|
|
23
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign execution original linkage differs"
|
|
24
|
+
end
|
|
25
|
+
inventory = journal.canonical_event_inventory!(commit: commit)
|
|
26
|
+
accepted_commit = inventory.fetch("introductions").fetch(params.fetch("assignment_id"))
|
|
27
|
+
.fetch(row.fetch("terminal_event_id"))
|
|
28
|
+
events = @launch.preview_attempt_events!(journal: journal, commit: accepted_commit, params: params, map: map)
|
|
29
|
+
results = events.select { |event| event["type"] == "result_submitted" &&
|
|
30
|
+
event.dig("payload", "receipt_digest") == receipt_digest }
|
|
31
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign execution result is ambiguous" unless results.one?
|
|
32
|
+
submitted = results.first.fetch("payload")
|
|
33
|
+
selected = params.merge(submitted.fetch("binding").slice("head", "candidate_generation"),
|
|
34
|
+
"result_id" => submitted.fetch("result_id"))
|
|
35
|
+
current = exact_candidate!(retained_candidate(events, selected.fetch("candidate_generation")), selected)
|
|
36
|
+
result = verified_result(journal, events, selected, map, retained_origin(events, selected), current, accepted_commit,
|
|
37
|
+
result_id: selected.fetch("result_id"))
|
|
38
|
+
unless result && result["receipt_digest"] == receipt_digest && result.dig("receipt", "verdict") == "succeeded"
|
|
39
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign execution accepted receipt differs"
|
|
40
|
+
end
|
|
41
|
+
terminal = events.find { |event| event["digest"] == row.fetch("terminal_event_id") }
|
|
42
|
+
unless terminal && terminal["type"] == "receipt_accepted" && terminal.dig("payload", "receipt") == result.fetch("receipt")
|
|
43
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign execution terminal receipt differs"
|
|
44
|
+
end
|
|
45
|
+
accepted = approved_review!(journal, events, selected, map, current, commit: accepted_commit)
|
|
46
|
+
campaign_finished_result!(journal: journal, commit: accepted_commit, events: events, params: selected, map: map,
|
|
47
|
+
result: result, accepted: accepted)
|
|
48
|
+
proof = result.fetch("receipt").merge("attempt_id" => params.fetch("attempt_id"),
|
|
49
|
+
"receipt_digest" => receipt_digest, "execution_binding" => binding, "journal_commit" => accepted_commit)
|
|
50
|
+
freeze_campaign_evidence!(proof)
|
|
51
|
+
rescue KeyError, TypeError, ArgumentError, AttemptErrors::ReceiptRejected
|
|
52
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign execution evidence is unavailable"
|
|
53
|
+
end
|
|
54
|
+
active = true
|
|
55
|
+
thread = Thread.current
|
|
56
|
+
canonical = Molecules::CanonicalEvidence.new(journal: journal)
|
|
57
|
+
reader = ->(artifact) {
|
|
58
|
+
unless active && Thread.current.equal?(thread) && result.fetch("artifacts").include?(artifact)
|
|
59
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign artifact read escaped its original evidence scope"
|
|
60
|
+
end
|
|
61
|
+
canonical.read({"ref" => artifact.fetch("path"), "sha256" => artifact.fetch("sha256")},
|
|
62
|
+
**result_context(result.fetch("binding")), commit: accepted_commit)
|
|
63
|
+
}
|
|
64
|
+
yield proof, reader
|
|
65
|
+
ensure
|
|
66
|
+
active = false
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
def campaign_execution_dependencies!(journal:, commit:, parent_params:, parent_map:)
|
|
72
|
+
# This closure belongs to one held original exclusion and immutable
|
|
73
|
+
# journal selection. Never retained across RPCs. Live/original source
|
|
74
|
+
# parent checks below still run on every callback; only fully verified raw
|
|
75
|
+
# canonical artifacts and their introduced-prefix proof are reused.
|
|
76
|
+
verified = {}
|
|
77
|
+
selected_rows = {}
|
|
78
|
+
inventory = nil
|
|
79
|
+
read = lambda do |reference, phase:, head:, artifacts: nil, name: nil, producer: nil, reviewer: nil, historical: false|
|
|
80
|
+
unless reference.is_a?(Hash) && reference.keys.sort == %w[attempt_id digest] &&
|
|
81
|
+
reference["digest"].is_a?(String) && reference["digest"].match?(DIGEST)
|
|
82
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign child receipt reference differs"
|
|
83
|
+
end
|
|
84
|
+
selection_key = Atoms::EvidenceDigest.digest("commit" => commit, "reference" => reference,
|
|
85
|
+
"phase" => phase, "head" => head, "name" => name)
|
|
86
|
+
params, map, execution, row = selected_rows[selection_key] ||= begin
|
|
87
|
+
inventory ||= journal.canonical_event_inventory!(commit: commit)
|
|
88
|
+
matches = inventory.fetch("events").filter_map do |assignment, events|
|
|
89
|
+
next unless events.any? { |event| event["attempt_id"] == reference["attempt_id"] }
|
|
90
|
+
registration = events.reverse.find { |event| event.dig("payload", "operation") == "register_assignment" }&.dig("payload", "data")
|
|
91
|
+
next unless registration
|
|
92
|
+
descriptor = @launch.send(:control_original_descriptor!, registration.fetch("lifecycle_control"))
|
|
93
|
+
child_map = descriptor.mapping(registration.fetch("mapping_id"))
|
|
94
|
+
params = {"assignment_id" => assignment, "attempt_id" => reference.fetch("attempt_id"),
|
|
95
|
+
"mapping_id" => registration.fetch("mapping_id")}
|
|
96
|
+
definition = @launch.preview_attempt_definition!(journal: journal, commit: commit, params: params, map: child_map)
|
|
97
|
+
execution = definition.campaign_execution
|
|
98
|
+
next unless execution && execution.values_at("parent_assignment_id", "parent_attempt_id", "phase", "head") ==
|
|
99
|
+
parent_params.values_at("assignment_id", "attempt_id") + [phase, head]
|
|
100
|
+
next unless child_map["project_id"] == parent_map.fetch("project_id")
|
|
101
|
+
next if name && execution["check_name"] != name
|
|
102
|
+
[params, child_map, execution]
|
|
103
|
+
end
|
|
104
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign child receipt linkage is ambiguous" unless matches.one?
|
|
105
|
+
params, map, execution = matches.first
|
|
106
|
+
row = @launch.completion_terminal!(journal: journal, commit: commit, params: params, map: map)
|
|
107
|
+
[params.freeze, map, freeze_campaign_evidence!(JSON.parse(JSON.generate(execution))),
|
|
108
|
+
freeze_campaign_evidence!(JSON.parse(JSON.generate(row)))]
|
|
109
|
+
end
|
|
110
|
+
binding = execution.merge(params.slice("assignment_id", "attempt_id"),
|
|
111
|
+
"definition_digest" => row.fetch("definition_digest"), "binding_digest" => row.fetch("original_binding_digest"))
|
|
112
|
+
unless historical
|
|
113
|
+
campaign_parent_candidate!(journal: journal, commit: commit, params: parent_params.merge(
|
|
114
|
+
"scope" => execution.fetch("parent_scope"), "head" => head, "base" => execution.fetch("base"),
|
|
115
|
+
"candidate_generation" => execution.fetch("parent_candidate_generation")), map: parent_map)
|
|
116
|
+
end
|
|
117
|
+
key = Atoms::EvidenceDigest.digest("journal" => journal.object_id, "commit" => commit,
|
|
118
|
+
"binding" => binding, "receipt" => reference, "phase" => phase, "head" => head,
|
|
119
|
+
"name" => name, "producer" => producer, "reviewer" => reviewer, "artifacts" => artifacts)
|
|
120
|
+
proof = verified[key] ||= with_campaign_execution_evidence!(journal: journal, commit: commit, params: params, map: map,
|
|
121
|
+
receipt_digest: reference.fetch("digest"), execution_binding: binding) do |canonical, reader|
|
|
122
|
+
canonical.fetch("artifacts").each do |artifact|
|
|
123
|
+
unless Digest::SHA256.hexdigest(reader.call(artifact)) == artifact.fetch("sha256")
|
|
124
|
+
raise AttemptErrors::EvidenceUnavailable, "canonical campaign artifact bytes differ"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
freeze_campaign_evidence!(JSON.parse(JSON.generate(canonical)))
|
|
128
|
+
end
|
|
129
|
+
if producer && (proof.dig("producer", "actor") != producer || proof.dig("review", "reviewer", "actor") != reviewer)
|
|
130
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign approval original actors differ"
|
|
131
|
+
end
|
|
132
|
+
if artifacts && artifacts.any? { |artifact| !proof.fetch("artifacts").any? { |ref| ref["sha256"] == artifact.fetch("sha256") } }
|
|
133
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign artifact differs from canonical child bytes"
|
|
134
|
+
end
|
|
135
|
+
proof.merge("artifacts" => artifacts || proof.fetch("artifacts"), "historical" => historical)
|
|
136
|
+
end
|
|
137
|
+
{check_evidence: ->(reference, head:, name:, historical: false) {
|
|
138
|
+
read.call(reference, phase: "check", head: head, name: name, historical: historical)
|
|
139
|
+
}, review_evidence: ->(reference, head:, artifacts:, historical: false) {
|
|
140
|
+
read.call(reference, phase: "collection", head: head, artifacts: artifacts, historical: historical)
|
|
141
|
+
}, approval_evidence: ->(reference, head:, artifacts:, producer:, reviewer:, historical: false) {
|
|
142
|
+
read.call(reference, phase: "approval", head: head, artifacts: artifacts, producer: producer, reviewer: reviewer, historical: historical)
|
|
143
|
+
}}.freeze
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def freeze_campaign_evidence!(value)
|
|
147
|
+
case value
|
|
148
|
+
when Hash then value.each { |key, item| freeze_campaign_evidence!(key); freeze_campaign_evidence!(item) }
|
|
149
|
+
when Array then value.each { |item| freeze_campaign_evidence!(item) }
|
|
150
|
+
end
|
|
151
|
+
value.freeze
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# The ordinary result is still campaign-free. Its phase comes only
|
|
155
|
+
# from the authenticated original registered child definition.
|
|
156
|
+
def campaign_child_result!(journal:, commit:, events:, params:, map:, receipt:)
|
|
157
|
+
definition = @launch.preview_attempt_definition!(journal: journal, commit: commit,
|
|
158
|
+
params: params, map: map)
|
|
159
|
+
execution = definition.campaign_execution
|
|
160
|
+
return unless execution
|
|
161
|
+
CampaignExecution.validate!(execution, parent: definition.parent,
|
|
162
|
+
policy: campaign_original_parent_policy!(journal, commit, execution, map))
|
|
163
|
+
intent = events.find { |event| event["type"] == "intent" }&.fetch("payload")
|
|
164
|
+
CampaignExecution.validate_result!(execution, receipt: receipt, base: intent&.fetch("base_head"))
|
|
165
|
+
execution
|
|
166
|
+
rescue KeyError, TypeError, ArgumentError
|
|
167
|
+
raise AttemptErrors::ReceiptRejected, "campaign child result binding is incomplete"
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def campaign_finished_result!(journal:, commit:, events:, params:, map:, result:, accepted:)
|
|
171
|
+
execution = campaign_child_result!(journal: journal, commit: commit, events: events, params: params, map: map,
|
|
172
|
+
receipt: result.fetch("receipt"))
|
|
173
|
+
return unless execution
|
|
174
|
+
CampaignExecution.validate_finished_review!(execution, receipt: result.fetch("receipt"),
|
|
175
|
+
accepted_review: accepted.fetch("review_receipt"))
|
|
176
|
+
rescue KeyError, ArgumentError, TypeError
|
|
177
|
+
raise AttemptErrors::ReceiptRejected, "campaign independent review differs from executed result"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def campaign_original_parent_policy!(journal, commit, execution, map)
|
|
181
|
+
campaign_original_parent_context!(journal, commit, execution, map).fetch(:selected).fetch("policy")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def campaign_original_parent_context!(journal, commit, execution, map)
|
|
185
|
+
parent = {"assignment_id" => execution.fetch("parent_assignment_id"),
|
|
186
|
+
"attempt_id" => execution.fetch("parent_attempt_id")}
|
|
187
|
+
inventory = journal.canonical_event_inventory!(commit: commit)
|
|
188
|
+
events = inventory.fetch("events").fetch(parent.fetch("assignment_id"))
|
|
189
|
+
reservations = events.select { |event| event["attempt_id"] == parent.fetch("attempt_id") &&
|
|
190
|
+
event.dig("payload", "operation") == "reserve_attempt" }
|
|
191
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign parent original reservation differs" unless reservations.one?
|
|
192
|
+
parent["mapping_id"] = reservations.first.dig("payload", "data", "mapping_id")
|
|
193
|
+
prefix = inventory.fetch("introductions").fetch(parent.fetch("assignment_id"))
|
|
194
|
+
.fetch(reservations.first.fetch("digest"))
|
|
195
|
+
registration = @launch.send(:definition, journal, parent.fetch("assignment_id"), commit: prefix)
|
|
196
|
+
descriptor = @launch.send(:control_original_descriptor!, registration.fetch("lifecycle_control"))
|
|
197
|
+
original = @launch.send(:control_registration_context!, parent,
|
|
198
|
+
descriptor.mapping(parent.fetch("mapping_id")), journal, commit: commit)
|
|
199
|
+
unless original.fetch(:map).fetch("project_id") == map.fetch("project_id")
|
|
200
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign child original project differs"
|
|
201
|
+
end
|
|
202
|
+
definition = @launch.preview_attempt_definition!(journal: journal, commit: commit,
|
|
203
|
+
params: parent, map: original.fetch(:map))
|
|
204
|
+
selected = CampaignExecution.validate_parent!(definition.review_campaign)
|
|
205
|
+
unless selected.values_at("campaign_id", "subject", "contract_identity") ==
|
|
206
|
+
execution.values_at("campaign_id", "subject", "contract_identity")
|
|
207
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign child original parent differs"
|
|
208
|
+
end
|
|
209
|
+
{params: parent, map: original.fetch(:map), definition: definition, selected: selected}.freeze
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def campaign_prepared_candidate!(journal:, commit:, execution:, map:)
|
|
213
|
+
original = campaign_original_parent_context!(journal, commit, execution, map)
|
|
214
|
+
CampaignExecution.validate!(execution, parent: original.fetch(:definition).id,
|
|
215
|
+
policy: original.fetch(:selected).fetch("policy"))
|
|
216
|
+
params = original.fetch(:params).merge("scope" => execution.fetch("parent_scope"),
|
|
217
|
+
"base" => execution.fetch("base"), "head" => execution.fetch("head"),
|
|
218
|
+
"candidate_generation" => execution.fetch("parent_candidate_generation"))
|
|
219
|
+
campaign_parent_candidate!(journal: journal, commit: commit, params: params, map: original.fetch(:map))
|
|
220
|
+
events = @launch.preview_attempt_events!(journal: journal, commit: commit, params: params, map: original.fetch(:map))
|
|
221
|
+
selected = exact_candidate!(candidate(events), params)
|
|
222
|
+
bytes = journal.bounded_blob(selected.fetch("bundle_ref"), commit: commit, max_bytes: CandidateTransfer::MAX_BYTES)
|
|
223
|
+
unless bytes.is_a?(String) && bytes.bytesize == selected.fetch("bytes") &&
|
|
224
|
+
Digest::SHA256.hexdigest(bytes) == selected.fetch("sha256")
|
|
225
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign original candidate bundle differs"
|
|
226
|
+
end
|
|
227
|
+
[selected.slice("head", "tree", "bytes", "sha256", "candidate_generation").merge("campaign_execution" => execution), bytes]
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|