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,266 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require_relative "campaign_round_transfer"
|
|
3
|
+
|
|
4
|
+
module Ace
|
|
5
|
+
module Assign
|
|
6
|
+
module Authority
|
|
7
|
+
class Endcap
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def campaign_round_request!(request)
|
|
11
|
+
params = request.fetch("params")
|
|
12
|
+
unless params.is_a?(Hash) && params.keys.sort == PARAMETERS.fetch("campaign_record_round").sort &&
|
|
13
|
+
params["head"].is_a?(String) && params["head"].match?(CandidateTransfer::SHA) &&
|
|
14
|
+
params["candidate_generation"].is_a?(Integer) && params["candidate_generation"].positive? &&
|
|
15
|
+
params["input_sha256"].is_a?(String) && params["input_sha256"].match?(DIGEST)
|
|
16
|
+
raise ArgumentError, "campaign round request differs"
|
|
17
|
+
end
|
|
18
|
+
%w[mapping_id assignment_id attempt_id].each { |key| result_id!(params.fetch(key)) }
|
|
19
|
+
result_id!(request.fetch("mutation_id"))
|
|
20
|
+
map = @deployment.mapping(params.fetch("mapping_id"))
|
|
21
|
+
raise AttemptErrors::UnauthorizedIdentity, "campaign project differs" unless map["project_id"] == request.fetch("project_id")
|
|
22
|
+
[params, map]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def authorize_campaign_round!(request:, peer:, role:)
|
|
26
|
+
params, map = campaign_round_request!(request)
|
|
27
|
+
@launch.with_assignment(params: params, map: map) do |journal, registration|
|
|
28
|
+
protected_journal!(journal)
|
|
29
|
+
descriptor = @launch.send(:control_original_descriptor!, registration.fetch("lifecycle_control"))
|
|
30
|
+
original_map = descriptor.mapping(params.fetch("mapping_id"))
|
|
31
|
+
events = @launch.preview_attempt_events!(journal: journal, commit: journal.ref_value, params: params, map: original_map)
|
|
32
|
+
finish_identity!(events, params, original_map, peer, role)
|
|
33
|
+
definition = @launch.preview_attempt_definition!(journal: journal, commit: journal.ref_value, params: params, map: original_map)
|
|
34
|
+
CampaignExecution.validate_parent!(definition.review_campaign)
|
|
35
|
+
end
|
|
36
|
+
true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def with_campaign_receipt!(journal:, commit:, events:, params:, map:, receipt:, result:, historical:)
|
|
40
|
+
unless result.is_a?(Hash) && result["schema"] == "ace.review.accepted-result/v1" &&
|
|
41
|
+
receipt["campaign"].is_a?(Hash) && receipt["campaign"].keys.sort == %w[id result] &&
|
|
42
|
+
receipt["operation"] == "review" && receipt["verdict"] == "succeeded"
|
|
43
|
+
raise AttemptErrors::ReceiptRejected, "parent campaign receipt differs"
|
|
44
|
+
end
|
|
45
|
+
context = @launch.send(:control_registration_context!, params, map, journal, commit: commit)
|
|
46
|
+
original_map = context.fetch(:map)
|
|
47
|
+
descriptor = context.fetch(:descriptor)
|
|
48
|
+
selected = CampaignExecution.validate_parent!(@launch.preview_attempt_definition!(journal: journal,
|
|
49
|
+
commit: commit, params: params, map: original_map).review_campaign)
|
|
50
|
+
unless receipt.dig("campaign", "id") == selected.fetch("campaign_id") && result["campaign_id"] == selected.fetch("campaign_id")
|
|
51
|
+
raise AttemptErrors::ReceiptRejected, "parent campaign identity differs"
|
|
52
|
+
end
|
|
53
|
+
origin = retained_origin(events, params)
|
|
54
|
+
parent = params.merge("scope" => origin.fetch("scope"), "base" => origin.fetch("base_head"), "head" => receipt.fetch("head"))
|
|
55
|
+
current = exact_candidate!(retained_candidate(events, params.fetch("candidate_generation")), parent)
|
|
56
|
+
project = descriptor.project(original_map.fetch("project_id"))
|
|
57
|
+
root = File.join(project.fetch("campaign_repository"), ".ace-local", "review", "candidate-views")
|
|
58
|
+
FileUtils.mkdir_p(root, mode: 0o700)
|
|
59
|
+
bundle = journal.bounded_blob(current.fetch("bundle_ref"), commit: commit, max_bytes: CandidateTransfer::MAX_BYTES)
|
|
60
|
+
CandidateTransfer.new(root: root).with_campaign_repository(bytes: bundle, sha256: current.fetch("sha256"),
|
|
61
|
+
size: current.fetch("bytes"), head: current.fetch("head"), tree: current.fetch("tree"), base: parent.fetch("base")) do |candidate_reader|
|
|
62
|
+
manager = @launch.send(:campaign_manager_for!, context,
|
|
63
|
+
**campaign_execution_dependencies!(journal: journal, commit: commit, parent_params: parent, parent_map: original_map),
|
|
64
|
+
candidate_reader: candidate_reader)
|
|
65
|
+
binding = selected.slice("subject", "contract_identity", "policy").transform_keys(&:to_sym).merge(
|
|
66
|
+
result: result, head: receipt.fetch("head"), base: parent.fetch("base"),
|
|
67
|
+
producer: receipt.fetch("producer").fetch("actor"), reviewer: receipt.fetch("review").fetch("reviewer").fetch("actor"))
|
|
68
|
+
if historical
|
|
69
|
+
manager.verify_retained_result!(**binding)
|
|
70
|
+
yield
|
|
71
|
+
else
|
|
72
|
+
deployment_sha = @launch.send(:protected_descriptor_sha256!)
|
|
73
|
+
CampaignConsumerPolicy.new.with(@deployment.project(original_map.fetch("project_id")).fetch("campaign_policy")) do |profiles, held|
|
|
74
|
+
manager.with_verified_result!(**binding, consumer_profiles: profiles) do |_verified, snapshot|
|
|
75
|
+
unless result == snapshot
|
|
76
|
+
raise AttemptErrors::ReceiptRejected, "exported campaign result bytes differ from verified owner"
|
|
77
|
+
end
|
|
78
|
+
held.verify_unchanged!
|
|
79
|
+
campaign_parent_candidate!(journal: journal, commit: commit, params: parent, map: original_map)
|
|
80
|
+
unless @launch.send(:protected_descriptor_sha256!) == deployment_sha
|
|
81
|
+
raise AttemptErrors::Conflict, "campaign consumer policy descriptor advanced"
|
|
82
|
+
end
|
|
83
|
+
value = yield
|
|
84
|
+
held.verify_unchanged!
|
|
85
|
+
unless @launch.send(:protected_descriptor_sha256!) == deployment_sha
|
|
86
|
+
raise AttemptErrors::Conflict, "campaign consumer policy descriptor advanced"
|
|
87
|
+
end
|
|
88
|
+
value
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
rescue Ace::Review::Atoms::CampaignContract::Invalid, KeyError, TypeError, ArgumentError
|
|
94
|
+
raise AttemptErrors::ReceiptRejected, "canonical parent campaign evidence is unavailable"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def with_campaign_submission!(journal:, commit:, events:, params:, map:, admitted:, replay:)
|
|
98
|
+
receipt = admitted.fetch(:receipt)
|
|
99
|
+
return yield unless receipt["campaign"]
|
|
100
|
+
definition = @launch.preview_attempt_definition!(journal: journal, commit: commit, params: params, map: map)
|
|
101
|
+
unless definition.review_campaign
|
|
102
|
+
raise AttemptErrors::EvidenceUnavailable, "protected campaigns require their canonical owner"
|
|
103
|
+
end
|
|
104
|
+
reference = receipt.fetch("campaign").fetch("result")
|
|
105
|
+
index = receipt.fetch("artifacts").index(reference)
|
|
106
|
+
raise AttemptErrors::ReceiptRejected, "campaign result is not an uploaded artifact" unless index
|
|
107
|
+
result = JSON.parse(admitted.fetch(:artifacts).fetch(index), create_additions: false,
|
|
108
|
+
max_nesting: 64, allow_duplicate_key: false, allow_comments: false)
|
|
109
|
+
with_campaign_receipt!(journal: journal, commit: commit, events: events, params: params, map: map,
|
|
110
|
+
receipt: receipt, result: result, historical: !replay.nil?) { yield }
|
|
111
|
+
rescue JSON::ParserError
|
|
112
|
+
raise AttemptErrors::ReceiptRejected, "campaign result JSON is malformed"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Service callers already retain with_assignment's original control SH
|
|
116
|
+
# and authority mutex through CAS. Round writers take the same keys EX
|
|
117
|
+
# before R1, so this read cannot invert with an admitted round writer.
|
|
118
|
+
def campaign_service_checks!(journal:, commit:, events:, params:, map:, current:)
|
|
119
|
+
definition = @launch.preview_attempt_definition!(journal: journal, commit: commit, params: params, map: map)
|
|
120
|
+
return true unless definition.review_campaign
|
|
121
|
+
parent = params.merge("head" => current.fetch("head"), "candidate_generation" => current.fetch("candidate_generation"))
|
|
122
|
+
result = verified_result(journal, events, parent, map, retained_origin(events, parent), current, commit)
|
|
123
|
+
unless result&.dig("receipt", "campaign") && result.dig("receipt", "verdict") == "succeeded"
|
|
124
|
+
raise AttemptErrors::ReceiptRejected, "ready or merge requires its canonical parent campaign result and executed checks"
|
|
125
|
+
end
|
|
126
|
+
with_campaign_finish!(journal: journal, commit: commit, events: events,
|
|
127
|
+
params: parent.merge("result_id" => result.fetch("result_id")), map: map) { true }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def with_campaign_finish!(journal:, commit:, events:, params:, map:)
|
|
131
|
+
record = events.find { |event| event["type"] == "result_submitted" && event.dig("payload", "result_id") == params["result_id"] }
|
|
132
|
+
return yield unless record&.dig("payload", "receipt", "campaign")
|
|
133
|
+
current = exact_candidate!(candidate(events), params)
|
|
134
|
+
payload = verified_result(journal, events, params, map, retained_origin(events, params), current, commit,
|
|
135
|
+
result_id: params.fetch("result_id"))
|
|
136
|
+
reference = payload.fetch("receipt").fetch("campaign").fetch("result")
|
|
137
|
+
canonical = Molecules::CanonicalEvidence.new(journal: journal)
|
|
138
|
+
bytes = canonical.read({"ref" => reference.fetch("path"), "sha256" => reference.fetch("sha256")},
|
|
139
|
+
**result_context(payload.fetch("binding")), commit: commit)
|
|
140
|
+
result = JSON.parse(bytes, create_additions: false, max_nesting: 64, allow_duplicate_key: false, allow_comments: false)
|
|
141
|
+
with_campaign_receipt!(journal: journal, commit: commit, events: events, params: params, map: map,
|
|
142
|
+
receipt: payload.fetch("receipt"), result: result, historical: false) { yield }
|
|
143
|
+
rescue JSON::ParserError
|
|
144
|
+
raise AttemptErrors::ReceiptRejected, "canonical campaign result JSON is malformed"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def dispatch_campaign_export(request:, peer:, role:, body: true)
|
|
148
|
+
params = request.fetch("params")
|
|
149
|
+
unless params.is_a?(Hash) && params.keys.sort == PARAMETERS.fetch("campaign_export_result").sort &&
|
|
150
|
+
params["head"].is_a?(String) && params["head"].match?(CandidateTransfer::SHA) &&
|
|
151
|
+
params["candidate_generation"].is_a?(Integer) && params["candidate_generation"].positive? && request["mutation_id"].nil?
|
|
152
|
+
raise ArgumentError, "campaign result selectors differ"
|
|
153
|
+
end
|
|
154
|
+
%w[mapping_id assignment_id attempt_id].each { |key| result_id!(params.fetch(key)) }
|
|
155
|
+
map = @deployment.mapping(params.fetch("mapping_id"))
|
|
156
|
+
raise AttemptErrors::UnauthorizedIdentity, "campaign project differs" unless map["project_id"] == request.fetch("project_id")
|
|
157
|
+
@launch.with_assignment(params: params, map: map, exclusive: true) do |journal, registration|
|
|
158
|
+
protected_journal!(journal)
|
|
159
|
+
commit = journal.ref_value
|
|
160
|
+
descriptor = @launch.send(:control_original_descriptor!, registration.fetch("lifecycle_control"))
|
|
161
|
+
original_map = descriptor.mapping(params.fetch("mapping_id"))
|
|
162
|
+
context = @launch.send(:control_registration_context!, params, original_map, journal, commit: commit)
|
|
163
|
+
events = @launch.preview_attempt_events!(journal: journal, commit: commit, params: params, map: original_map)
|
|
164
|
+
origin = active_origin(events, params)
|
|
165
|
+
role == :worker ? worker_or_launcher!(peer, role, original_map, origin) : finish_identity!(events, params, original_map, peer, role)
|
|
166
|
+
selected = CampaignExecution.validate_parent!(@launch.preview_attempt_definition!(journal: journal,
|
|
167
|
+
commit: commit, params: params, map: original_map).review_campaign)
|
|
168
|
+
return true unless body
|
|
169
|
+
parent = params.merge("scope" => origin.fetch("scope"), "base" => origin.fetch("base_head"))
|
|
170
|
+
campaign_parent_candidate!(journal: journal, commit: commit, params: parent, map: original_map)
|
|
171
|
+
current = exact_candidate!(candidate(events), params)
|
|
172
|
+
bundle = journal.bounded_blob(current.fetch("bundle_ref"), commit: commit, max_bytes: CandidateTransfer::MAX_BYTES)
|
|
173
|
+
project = descriptor.project(original_map.fetch("project_id"))
|
|
174
|
+
root = File.join(project.fetch("campaign_repository"), ".ace-local", "review", "candidate-views")
|
|
175
|
+
FileUtils.mkdir_p(root, mode: 0o700)
|
|
176
|
+
CandidateTransfer.new(root: root).with_campaign_repository(bytes: bundle, sha256: current.fetch("sha256"),
|
|
177
|
+
size: current.fetch("bytes"), head: current.fetch("head"), tree: current.fetch("tree"), base: parent.fetch("base")) do |candidate_reader|
|
|
178
|
+
manager = @launch.send(:campaign_manager_for!, context,
|
|
179
|
+
**campaign_execution_dependencies!(journal: journal, commit: commit, parent_params: parent, parent_map: original_map),
|
|
180
|
+
candidate_reader: candidate_reader)
|
|
181
|
+
result = manager.accepted_result_snapshot(selected.fetch("campaign_id"))
|
|
182
|
+
deployment_sha = @launch.send(:protected_descriptor_sha256!)
|
|
183
|
+
CampaignConsumerPolicy.new.with(@deployment.project(original_map.fetch("project_id")).fetch("campaign_policy")) do |profiles, held|
|
|
184
|
+
manager.with_verified_result!(result: result, **selected.slice("subject", "contract_identity", "policy").transform_keys(&:to_sym),
|
|
185
|
+
head: params.fetch("head"), base: parent.fetch("base"), producer: result.fetch("producer"), reviewer: result.fetch("reviewer"),
|
|
186
|
+
consumer_profiles: profiles) do
|
|
187
|
+
held.verify_unchanged!
|
|
188
|
+
unless @launch.send(:protected_descriptor_sha256!) == deployment_sha
|
|
189
|
+
raise AttemptErrors::Conflict, "campaign export policy descriptor advanced"
|
|
190
|
+
end
|
|
191
|
+
campaign_parent_candidate!(journal: journal, commit: commit, params: parent, map: original_map)
|
|
192
|
+
bytes = JSON.generate(result).b
|
|
193
|
+
raise AttemptErrors::EvidenceUnavailable, "campaign result exceeds artifact bound" if bytes.bytesize > 64 * 1024
|
|
194
|
+
{data: {"campaign_id" => selected.fetch("campaign_id"), "result_identity" => result.fetch("result_identity"),
|
|
195
|
+
"sha256" => Digest::SHA256.hexdigest(bytes), "bytes" => bytes.bytesize, "head" => params.fetch("head"),
|
|
196
|
+
"candidate_generation" => params.fetch("candidate_generation"), "journal_commit" => commit},
|
|
197
|
+
replayed: false, transfer_parts: [bytes]}
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
rescue Ace::Review::Atoms::CampaignContract::Invalid, KeyError, TypeError
|
|
203
|
+
raise AttemptErrors::EvidenceUnavailable, "canonical campaign result is unavailable"
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def dispatch_campaign_round(request:, peer:, role:, transfer:)
|
|
207
|
+
params, map = campaign_round_request!(request)
|
|
208
|
+
admitted = CampaignRoundTransfer.decode(input: transfer, sha256: params.fetch("input_sha256"))
|
|
209
|
+
input = admitted.fetch(:round)
|
|
210
|
+
unless input["attempt_id"] == request.fetch("mutation_id") && input["head"] == params.fetch("head")
|
|
211
|
+
raise AttemptErrors::Conflict, "campaign round stable request or head differs"
|
|
212
|
+
end
|
|
213
|
+
@launch.with_assignment(params: params, map: map, exclusive: true) do |journal, registration|
|
|
214
|
+
protected_journal!(journal)
|
|
215
|
+
commit = journal.ref_value
|
|
216
|
+
descriptor = @launch.send(:control_original_descriptor!, registration.fetch("lifecycle_control"))
|
|
217
|
+
original_map = descriptor.mapping(params.fetch("mapping_id"))
|
|
218
|
+
context = @launch.send(:control_registration_context!, params, original_map, journal, commit: commit)
|
|
219
|
+
events = @launch.preview_attempt_events!(journal: journal, commit: commit, params: params, map: original_map)
|
|
220
|
+
origin = finish_identity!(events, params, original_map, peer, role)
|
|
221
|
+
selected = CampaignExecution.validate_parent!(@launch.preview_attempt_definition!(journal: journal,
|
|
222
|
+
commit: commit, params: params, map: original_map).review_campaign)
|
|
223
|
+
parent_params = params.merge("scope" => origin.fetch("scope"), "base" => input.fetch("base"))
|
|
224
|
+
project = descriptor.project(original_map.fetch("project_id"))
|
|
225
|
+
paths = CampaignRoundTransfer.materialize(admitted: admitted, repository: project.fetch("campaign_repository"),
|
|
226
|
+
request_id: request.fetch("mutation_id"))
|
|
227
|
+
dependencies = campaign_execution_dependencies!(journal: journal, commit: commit,
|
|
228
|
+
parent_params: parent_params, parent_map: original_map)
|
|
229
|
+
current = exact_candidate!(retained_candidate(events, params.fetch("candidate_generation")), params)
|
|
230
|
+
bundle = journal.bounded_blob(current.fetch("bundle_ref"), commit: commit, max_bytes: CandidateTransfer::MAX_BYTES)
|
|
231
|
+
quarantine = File.join(project.fetch("campaign_repository"), ".ace-local", "review", "candidate-views")
|
|
232
|
+
FileUtils.mkdir_p(quarantine, mode: 0o700)
|
|
233
|
+
transfer_owner = CandidateTransfer.new(root: quarantine)
|
|
234
|
+
transfer_owner.with_campaign_repository(bytes: bundle, sha256: current.fetch("sha256"), size: current.fetch("bytes"),
|
|
235
|
+
head: current.fetch("head"), tree: current.fetch("tree"), base: input.fetch("base")) do |candidate_reader|
|
|
236
|
+
manager = @launch.send(:campaign_manager_for!, context, **dependencies, artifact_paths: paths, candidate_reader: candidate_reader)
|
|
237
|
+
deployment_sha = @launch.send(:protected_descriptor_sha256!)
|
|
238
|
+
CampaignConsumerPolicy.new.with(@deployment.project(original_map.fetch("project_id")).fetch("campaign_policy")) do |profiles, held|
|
|
239
|
+
# The existing R1 store checks exact replay before fresh policy,
|
|
240
|
+
# and binds the complete supplied frame in its input_digest.
|
|
241
|
+
data = manager.record_round(selected.fetch("campaign_id"), input, source_digest: admitted.fetch(:sha256),
|
|
242
|
+
expected_campaign: selected.reject { |key, _| key == "version" }, consumer_profiles: profiles,
|
|
243
|
+
before_record: -> {
|
|
244
|
+
held.verify_unchanged!
|
|
245
|
+
campaign_parent_candidate!(journal: journal, commit: commit, params: parent_params, map: original_map)
|
|
246
|
+
unless @launch.send(:protected_descriptor_sha256!) == deployment_sha
|
|
247
|
+
raise AttemptErrors::Conflict, "campaign policy descriptor advanced"
|
|
248
|
+
end
|
|
249
|
+
})
|
|
250
|
+
held.verify_unchanged!
|
|
251
|
+
unless journal.ref_value == commit && @launch.send(:protected_descriptor_sha256!) == deployment_sha
|
|
252
|
+
raise AttemptErrors::Conflict, "campaign round canonical selection advanced"
|
|
253
|
+
end
|
|
254
|
+
reply = data.slice("campaign_id", "recorded_complete", "completed_rounds", "accepted", "result_identity", "replayed")
|
|
255
|
+
.merge("request_id" => request.fetch("mutation_id"), "round_id" => input.fetch("round_id"))
|
|
256
|
+
{data: reply, replayed: data["replayed"] == true}
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
rescue Ace::Review::Atoms::CampaignContract::Invalid, KeyError, TypeError
|
|
261
|
+
raise AttemptErrors::EvidenceUnavailable, "canonical campaign round is unavailable"
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "ace/herdr/molecules/inbox_context_client"
|
|
3
|
+
|
|
4
|
+
module Ace
|
|
5
|
+
module Assign
|
|
6
|
+
module Authority
|
|
7
|
+
class Endcap
|
|
8
|
+
private
|
|
9
|
+
|
|
10
|
+
def with_inbox_context(params, map, mutation_id:)
|
|
11
|
+
context = @deployment.inbox_context(params.fetch("mapping_id"), params.fetch("inbox_context_id"))
|
|
12
|
+
@deployment.verify_inbox_context!(params.fetch("mapping_id"), params.fetch("inbox_context_id"))
|
|
13
|
+
client = @inbox_context_clients&.fetch(params.fetch("inbox_context_id"))
|
|
14
|
+
client ||= Ace::Herdr::Molecules::InboxContextClient.selected(context_id: params.fetch("inbox_context_id"),
|
|
15
|
+
socket_path: context.fetch("control_socket_path"), owner_credentials: context.fetch("owner_credentials"), kernel: @kernel)
|
|
16
|
+
yield client
|
|
17
|
+
rescue Ace::Runtime::RuntimeUnavailableError, Ace::Herdr::Error
|
|
18
|
+
raise AttemptErrors::EvidenceUnavailable, "protected inbox context is unavailable"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Assign
|
|
5
|
+
module Authority
|
|
6
|
+
class Endcap
|
|
7
|
+
INBOX_REGISTRATION_FIELDS = %w[event_id attempt_id payload_sha256].freeze
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def inbox_registration?(value)
|
|
12
|
+
value.is_a?(Hash) && value.keys.sort == INBOX_REGISTRATION_FIELDS.sort &&
|
|
13
|
+
%w[event_id attempt_id].all? { |key| value[key].is_a?(String) && Molecules::JournalMutation::ID.match?(value[key]) } &&
|
|
14
|
+
%w[payload_sha256].all? { |key| value[key].is_a?(String) && DIGEST.match?(value[key]) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def inbox_native_lineage!(record, lineage)
|
|
18
|
+
workspace = lineage.native_event.fetch("payload").fetch("workspace_id")
|
|
19
|
+
original = record.fetch("origin_target")
|
|
20
|
+
binding = record.fetch("binding")
|
|
21
|
+
unless original.is_a?(Hash) && binding.is_a?(Hash) &&
|
|
22
|
+
original["session"] == workspace && binding["session"] == workspace
|
|
23
|
+
raise AttemptErrors::EvidenceUnavailable, "inbox original native workspace differs"
|
|
24
|
+
end
|
|
25
|
+
if lineage.child_event
|
|
26
|
+
child = lineage.child_event.fetch("payload").fetch("original_process_binding")
|
|
27
|
+
unless original.values_at("session", "pane", "terminal_id") == child.values_at("session", "pane", "terminal_id")
|
|
28
|
+
raise AttemptErrors::EvidenceUnavailable, "inbox original native child differs"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Assign
|
|
5
|
+
module Authority
|
|
6
|
+
class Endcap
|
|
7
|
+
|
|
8
|
+
def prepared_fetch?(request)
|
|
9
|
+
request["operation"] == "evidence_fetch" && %w[prepared_work campaign_candidate].include?(request.dig("params", "kind"))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def dispatch_prepared_fetch(request:, peer:, role:, body: true)
|
|
13
|
+
params = request.fetch("params")
|
|
14
|
+
unless request.fetch("mutation_id").nil? && params.is_a?(Hash) && params.keys.sort == PARAMETERS.fetch("evidence_fetch").sort &&
|
|
15
|
+
[%w[prepared_work original_prepared_work prepared_bundle],
|
|
16
|
+
%w[campaign_candidate original_campaign_candidate candidate_bundle]].include?(params.values_at("kind", "purpose_id", "artifact_id"))
|
|
17
|
+
raise ArgumentError, "prepared fetch envelope differs"
|
|
18
|
+
end
|
|
19
|
+
%w[mapping_id assignment_id attempt_id].each { |key| result_id!(params.fetch(key)) }
|
|
20
|
+
map = @deployment.mapping(params.fetch("mapping_id"))
|
|
21
|
+
raise AttemptErrors::UnauthorizedIdentity, "prepared project differs" unless map.fetch("project_id") == request.fetch("project_id")
|
|
22
|
+
raise AttemptErrors::UnauthorizedIdentity, "prepared fetch requires original worker" unless role == :worker
|
|
23
|
+
@kernel.live!(peer)
|
|
24
|
+
service_policy!.visible!(project: map.fetch("project_id"), uid: peer.fetch("uid"))
|
|
25
|
+
journal = @launch.send(:journal_for, map)
|
|
26
|
+
protected_journal!(journal)
|
|
27
|
+
commit = journal.ref_value
|
|
28
|
+
selected = @launch.original_prepared_registration!(journal: journal, params: params, commit: commit) do |original_map, state|
|
|
29
|
+
unless peer.values_at("uid", "gid", "groups") == original_map.values_at("worker_uid", "worker_gid", "worker_groups")
|
|
30
|
+
raise AttemptErrors::UnauthorizedIdentity, "prepared worker principal differs"
|
|
31
|
+
end
|
|
32
|
+
binding = state.fetch("process_binding")
|
|
33
|
+
@kernel.live!(binding.fetch("process_identity"))
|
|
34
|
+
@kernel.live!(binding.fetch("native_origin").fetch("server_identity"))
|
|
35
|
+
worker_or_launcher!(peer, role, original_map, state)
|
|
36
|
+
end
|
|
37
|
+
original_map = selected.fetch(:map)
|
|
38
|
+
registration = selected.fetch(:registration)
|
|
39
|
+
if params.fetch("kind") == "campaign_candidate"
|
|
40
|
+
definition = @launch.preview_attempt_definition!(journal: journal, commit: commit, params: params, map: original_map)
|
|
41
|
+
execution = definition.campaign_execution
|
|
42
|
+
raise AttemptErrors::UnauthorizedIdentity, "original worker is not a campaign child" unless execution
|
|
43
|
+
descriptor, candidate_bytes = campaign_prepared_candidate!(journal: journal, commit: commit, execution: execution, map: original_map)
|
|
44
|
+
return true unless body
|
|
45
|
+
unless journal.ref_value == commit
|
|
46
|
+
raise AttemptErrors::Conflict, "campaign original candidate selection advanced"
|
|
47
|
+
end
|
|
48
|
+
descriptor = descriptor.merge(params.slice("mapping_id", "assignment_id", "attempt_id"),
|
|
49
|
+
"project_id" => original_map.fetch("project_id"),
|
|
50
|
+
"original_worker_identity" => selected.fetch(:state).fetch("process_binding").fetch("process_identity"),
|
|
51
|
+
"original_worker_scratch_root" => selected.fetch(:original_worker_scratch_root),
|
|
52
|
+
"original_binding_digest" => selected.fetch(:original_binding_digest))
|
|
53
|
+
return {data: {"descriptor" => descriptor, "generation" => journal.authority_generation(selected.fetch(:events)),
|
|
54
|
+
"journal_commit" => commit}, replayed: false, transfer_parts: [candidate_bytes]}
|
|
55
|
+
end
|
|
56
|
+
return true unless body
|
|
57
|
+
reference = registration.fetch("prepared_work")
|
|
58
|
+
bytes = journal.bounded_blob(registration.fetch("prepared_bundle_ref"), commit: selected.fetch(:registration_commit), max_bytes: CandidateTransfer::MAX_BYTES)
|
|
59
|
+
unless bytes.is_a?(String) && bytes.bytesize == registration.fetch("prepared_bundle_bytes") &&
|
|
60
|
+
bytes.bytesize.between?(1, CandidateTransfer::MAX_BYTES) && Digest::SHA256.hexdigest(bytes) == registration.fetch("prepared_bundle_sha256")
|
|
61
|
+
raise AttemptErrors::EvidenceUnavailable, "original prepared bundle differs"
|
|
62
|
+
end
|
|
63
|
+
descriptor = reference.slice("task_id", "scope", "selection_sha256", "prepared_head", "prepared_tree", "manifest_bytes", "manifest_sha256").merge(
|
|
64
|
+
params.slice("mapping_id", "assignment_id", "attempt_id"), "version" => 1, "kind" => "prepared_work", "purpose" => "original_prepared_work", "artifact" => "prepared_bundle",
|
|
65
|
+
"project_id" => original_map.fetch("project_id"), "definition_digest" => registration.fetch("definition_digest"),
|
|
66
|
+
"task_context_entry" => registration.fetch("task_context_entry"),
|
|
67
|
+
"original_worker_identity" => selected.fetch(:state).fetch("process_binding").fetch("process_identity"),
|
|
68
|
+
"original_worker_scratch_root" => selected.fetch(:original_worker_scratch_root),
|
|
69
|
+
"workspace_exclusion" => selected.fetch(:workspace_exclusion),
|
|
70
|
+
"registration_generation" => registration.fetch("generation"), "registration_commit" => selected.fetch(:registration_commit),
|
|
71
|
+
"original_binding_digest" => selected.fetch(:original_binding_digest), "ref" => registration.fetch("prepared_bundle_ref"), "bytes" => bytes.bytesize,
|
|
72
|
+
"sha256" => registration.fetch("prepared_bundle_sha256"))
|
|
73
|
+
{data: {"descriptor" => descriptor, "generation" => journal.authority_generation(selected.fetch(:events)), "journal_commit" => commit}, replayed: false, transfer_parts: [bytes]}
|
|
74
|
+
rescue KeyError, TypeError
|
|
75
|
+
raise AttemptErrors::EvidenceUnavailable, "original prepared input is unavailable"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ace
|
|
4
|
+
module Assign
|
|
5
|
+
module Authority
|
|
6
|
+
class Endcap
|
|
7
|
+
# The same held assignment owner authenticates the receiver and current
|
|
8
|
+
# candidate both before transfer and inside each canonical CAS attempt.
|
|
9
|
+
# Ordinary HITL uses this same original launch and project journal;
|
|
10
|
+
# no local assignment cache or caller-selected coordinator participates.
|
|
11
|
+
def with_worker_hitl!(assignment:, attempt:, project:, requester_uid:, peer: nil)
|
|
12
|
+
selected = @deployment.data.fetch("launch_mappings").select do |_id, map|
|
|
13
|
+
map.fetch("project_id") == project && map.fetch("worker_uid") == requester_uid
|
|
14
|
+
end
|
|
15
|
+
raise AttemptErrors::UnauthorizedIdentity, "HITL worker mapping is missing or ambiguous" unless selected.size == 1
|
|
16
|
+
mapping_id, map = selected.first
|
|
17
|
+
params = {"mapping_id" => mapping_id, "assignment_id" => assignment, "attempt_id" => attempt}
|
|
18
|
+
@launch.with_assignment(params: params, map: map) do |journal, _registration|
|
|
19
|
+
protected_journal!(journal)
|
|
20
|
+
events = attempt_events(journal, params)
|
|
21
|
+
origin = active_origin(events, params)
|
|
22
|
+
@launch.scope_open_for_effect!(events: events, params: params, map: map)
|
|
23
|
+
if peer
|
|
24
|
+
unless peer.values_at("uid", "gid", "groups") == map.values_at("worker_uid", "worker_gid", "worker_groups")
|
|
25
|
+
raise AttemptErrors::UnauthorizedIdentity, "HITL requester credentials differ"
|
|
26
|
+
end
|
|
27
|
+
@kernel.live!(peer)
|
|
28
|
+
worker_or_launcher!(peer, :worker, map, origin)
|
|
29
|
+
end
|
|
30
|
+
reverse = origin.fetch("process_binding").slice("session", "pane")
|
|
31
|
+
unless reverse.values.all? { |value| value.is_a?(String) && !value.empty? } && reverse.size == 2
|
|
32
|
+
raise AttemptErrors::EvidenceUnavailable, "HITL native reverse is unavailable"
|
|
33
|
+
end
|
|
34
|
+
yield settlement_immutable(reverse.merge("schema" => "ace.hitl.ref/v1"))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def publication_executor_identity?(project:, uid:)
|
|
39
|
+
@deployment.project(project).fetch("service_executor_uids").include?(uid)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def with_publication_hitl!(binding:, project:, peer:)
|
|
43
|
+
params = binding.slice("mapping_id", "assignment_id", "attempt_id", "request_id", "claim_binding", "input_digest", "candidate_generation", "head")
|
|
44
|
+
map = @deployment.mapping(params.fetch("mapping_id"))
|
|
45
|
+
raise AttemptErrors::UnauthorizedIdentity, "publication project differs" unless map.fetch("project_id") == project
|
|
46
|
+
@launch.with_assignment(params: params, map: map) do |journal, _registration|
|
|
47
|
+
protected_journal!(journal)
|
|
48
|
+
record = publication_record!(journal, params, map, peer, :executor)
|
|
49
|
+
unless record["dispatch_phase"] == "otp_pending" && record["publication_challenge_digest"] == binding.fetch("challenge_digest")
|
|
50
|
+
raise AttemptErrors::Conflict, "publication HITL challenge is no longer pending"
|
|
51
|
+
end
|
|
52
|
+
ServiceEvidence.new(journal: journal).publication_challenge!(record)
|
|
53
|
+
selection = settlement_immutable({"executor_process_binding" => record.fetch("executor_process_binding"),
|
|
54
|
+
"challenge_ref" => record.fetch("publication_challenge_ref").fetch("ref")})
|
|
55
|
+
yield selection
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def authorize_publication_update!(journal:, existing:, replacement:, pending:)
|
|
60
|
+
operation = pending.fetch(:operation)
|
|
61
|
+
allowed = ServicePublicationEvidence::PUBLICATION_FIELDS + ["dispatch_phase"]
|
|
62
|
+
unless existing.except(*allowed) == replacement.except(*allowed) && existing["state"] == "uncertain"
|
|
63
|
+
raise AttemptErrors::InvalidState, "publication transition changed original binding"
|
|
64
|
+
end
|
|
65
|
+
owner = ServiceEvidence.new(journal: journal)
|
|
66
|
+
if operation == "publication_challenge"
|
|
67
|
+
unless %w[dispatch_started issuing].include?(existing["dispatch_phase"]) && replacement["dispatch_phase"] == "otp_pending" &&
|
|
68
|
+
replacement["publication_issuing_event_digest"] == existing["publication_issuing_event_digest"]
|
|
69
|
+
raise AttemptErrors::InvalidState, "publication need has no source-owned rejected push"
|
|
70
|
+
end
|
|
71
|
+
need = owner.publication_challenge!(replacement, pending: pending)
|
|
72
|
+
unless need["previous_challenge_digest"] == existing["publication_challenge_digest"]
|
|
73
|
+
raise AttemptErrors::InvalidState, "publication need predecessor differs"
|
|
74
|
+
end
|
|
75
|
+
elsif operation == "publication_continue"
|
|
76
|
+
unless existing["dispatch_phase"] == "otp_pending" && replacement["dispatch_phase"] == "issuing" &&
|
|
77
|
+
(allowed - %w[dispatch_phase publication_issuing_event_digest]).all? { |key| existing[key] == replacement[key] }
|
|
78
|
+
raise AttemptErrors::InvalidState, "publication continuation is not a unique pending challenge"
|
|
79
|
+
end
|
|
80
|
+
owner.publication_challenge!(replacement, pending: pending)
|
|
81
|
+
events = pending.fetch(:current_events) + pending.fetch(:pending_events)
|
|
82
|
+
event = events.find { |row| row["digest"] == replacement["publication_issuing_event_digest"] }
|
|
83
|
+
unless event && event["type"] == "service_publication_issuing" &&
|
|
84
|
+
%w[request_id input_digest claim_binding publication_challenge_digest].all? { |key| event.dig("payload", key) == replacement[key] }
|
|
85
|
+
raise AttemptErrors::EvidenceUnavailable, "publication issuing event is unavailable"
|
|
86
|
+
end
|
|
87
|
+
else
|
|
88
|
+
raise AttemptErrors::InvalidState, "publication transition has no fixed source owner"
|
|
89
|
+
end
|
|
90
|
+
true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def publication_record!(journal, params, map, peer, role, events: nil)
|
|
94
|
+
record = journal.service_request(params.fetch("request_id"))
|
|
95
|
+
unless record && record["operation"] == "publish" && record["state"] == "uncertain" &&
|
|
96
|
+
%w[dispatch_started otp_pending issuing].include?(record["dispatch_phase"]) &&
|
|
97
|
+
%w[claim_binding input_digest candidate_generation].all? { |key| record[key] == params[key] } &&
|
|
98
|
+
record["candidate_head"] == params["head"] && record["mapping_id"] == params["mapping_id"]
|
|
99
|
+
raise AttemptErrors::Conflict, "publication original tuple or phase differs"
|
|
100
|
+
end
|
|
101
|
+
service_executor!(peer, role, record)
|
|
102
|
+
service_ticket!(record, params, map, peer)
|
|
103
|
+
events ||= attempt_events(journal, params)
|
|
104
|
+
current = exact_candidate!(candidate(events), params)
|
|
105
|
+
origin = active_origin(events, params)
|
|
106
|
+
worker_or_launcher!(record.fetch("worker_process_binding"), :worker, map, origin)
|
|
107
|
+
approved_review!(journal, events, params, map, current)
|
|
108
|
+
@launch.scope_open_for_effect!(events: events, params: params, map: map)
|
|
109
|
+
service_policy!.authorize!(record)
|
|
110
|
+
ServiceEvidence.new(journal: journal).context(record)
|
|
111
|
+
record
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def authorize_publication_transfer!(request, params, map, peer, role)
|
|
115
|
+
@launch.with_assignment(params: params, map: map) do |journal, _registration|
|
|
116
|
+
protected_journal!(journal)
|
|
117
|
+
publication_record!(journal, params, map, peer, role)
|
|
118
|
+
end
|
|
119
|
+
true
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def dispatch_publication(request, params, map, peer, role, transfer)
|
|
123
|
+
authorize_publication_transfer!(request, params, map, peer, role)
|
|
124
|
+
challenge = request.fetch("operation") == "publication_challenge"
|
|
125
|
+
bytes = nil
|
|
126
|
+
if challenge
|
|
127
|
+
unless transfer && transfer.count == 1
|
|
128
|
+
raise ArgumentError, "publication need requires one bounded artifact"
|
|
129
|
+
end
|
|
130
|
+
bytes = transfer.bytes.dup.freeze
|
|
131
|
+
unless Digest::SHA256.hexdigest(bytes) == params.fetch("receipt_sha256")
|
|
132
|
+
raise AttemptErrors::ReceiptRejected, "publication need transfer digest differs"
|
|
133
|
+
end
|
|
134
|
+
elsif transfer
|
|
135
|
+
raise ArgumentError, "publication continuation forbids upload"
|
|
136
|
+
end
|
|
137
|
+
@launch.with_assignment(params: params, map: map) do |journal, _registration|
|
|
138
|
+
publication_record!(journal, params, map, peer, role)
|
|
139
|
+
result = journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
140
|
+
mutation_id: request.fetch("mutation_id"), operation: request.fetch("operation"),
|
|
141
|
+
parameters_digest: Atoms::EvidenceDigest.digest(params), expected_generation: params.fetch("expected_generation"),
|
|
142
|
+
with_replay: true) do |events, _commit, generation|
|
|
143
|
+
record = publication_record!(journal, params, map, peer, role, events: events)
|
|
144
|
+
if challenge
|
|
145
|
+
publication_challenge_plan(journal, events, record, params, bytes, generation)
|
|
146
|
+
else
|
|
147
|
+
publication_continue_plan(journal, events, record, params, generation)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
if !challenge && result.fetch(:replayed)
|
|
151
|
+
result = result.merge(data: result.fetch(:data).merge("invocation" => "already_issued"))
|
|
152
|
+
end
|
|
153
|
+
result
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def publication_challenge_plan(journal, events, record, params, bytes, generation)
|
|
158
|
+
owner = ServiceEvidence.new(journal: journal)
|
|
159
|
+
digest = Digest::SHA256.hexdigest(bytes)
|
|
160
|
+
if record["dispatch_phase"] == "otp_pending"
|
|
161
|
+
owner.publication_challenge!(record)
|
|
162
|
+
raise AttemptErrors::Conflict, "publication pending challenge differs" unless record["publication_challenge_digest"] == digest
|
|
163
|
+
return {data: service_projection(record).merge("challenge" => "retained")}
|
|
164
|
+
end
|
|
165
|
+
unless %w[dispatch_started issuing].include?(record["dispatch_phase"])
|
|
166
|
+
raise AttemptErrors::InvalidState, "publication has no rejected push"
|
|
167
|
+
end
|
|
168
|
+
previous = record["publication_challenge_digest"]
|
|
169
|
+
owner.publication_challenge!(record) if previous
|
|
170
|
+
owner.publication_need!(bytes, record, previous: previous)
|
|
171
|
+
payload = record.slice("request_id", "input_digest", "claim_binding").merge(
|
|
172
|
+
"challenge_digest" => digest, "previous_challenge_digest" => previous, "generation" => generation + 1)
|
|
173
|
+
time = Time.now.utc
|
|
174
|
+
event = Models::EvidenceEvent.build(type: "service_publication_challenge", attempt_id: record.fetch("attempt_id"),
|
|
175
|
+
payload: payload, previous_digest: events.last&.fetch("digest"), recorded_at: time)
|
|
176
|
+
plan = Molecules::CanonicalEvidence.new(journal: journal).import_plan(**owner.context(record),
|
|
177
|
+
artifacts: [bytes], admitted_after_event_digest: event.fetch("digest"))
|
|
178
|
+
replacement = record.merge("dispatch_phase" => "otp_pending", "publication_challenge_digest" => digest,
|
|
179
|
+
"publication_challenge_ref" => plan.fetch(:references).fetch(0),
|
|
180
|
+
"publication_challenge_event_digest" => event.fetch("digest"), "publication_challenge_generation" => generation + 1)
|
|
181
|
+
plan.merge(events: [{type: "service_publication_challenge", payload: payload, recorded_at: time}] + plan.fetch(:events),
|
|
182
|
+
data: service_projection(replacement).merge("challenge" => "created"), service_updates: [{request_id: record.fetch("request_id"),
|
|
183
|
+
expected: record, replacement: replacement, event_type: "service_transition"}])
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def publication_continue_plan(journal, events, record, params, generation)
|
|
187
|
+
ServiceEvidence.new(journal: journal).publication_challenge!(record)
|
|
188
|
+
unless record["publication_challenge_digest"] == params.fetch("challenge_digest")
|
|
189
|
+
raise AttemptErrors::Conflict, "publication continuation challenge differs"
|
|
190
|
+
end
|
|
191
|
+
if record["dispatch_phase"] == "issuing"
|
|
192
|
+
return {data: service_projection(record).merge("invocation" => "already_issued")}
|
|
193
|
+
end
|
|
194
|
+
raise AttemptErrors::InvalidState, "publication is not pending" unless record["dispatch_phase"] == "otp_pending"
|
|
195
|
+
payload = record.slice("request_id", "input_digest", "claim_binding", "publication_challenge_digest").merge("generation" => generation + 1)
|
|
196
|
+
time = Time.now.utc
|
|
197
|
+
event = Models::EvidenceEvent.build(type: "service_publication_issuing", attempt_id: record.fetch("attempt_id"),
|
|
198
|
+
payload: payload, previous_digest: events.last&.fetch("digest"), recorded_at: time)
|
|
199
|
+
replacement = record.merge("dispatch_phase" => "issuing", "publication_issuing_event_digest" => event.fetch("digest"))
|
|
200
|
+
{events: [{type: "service_publication_issuing", payload: payload, recorded_at: time}],
|
|
201
|
+
data: service_projection(replacement).merge("invocation" => "permitted"), service_updates: [{request_id: record.fetch("request_id"),
|
|
202
|
+
expected: record, replacement: replacement, event_type: "service_transition"}]}
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|