ace-assign 0.64.0 → 0.66.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +11 -0
- data/CHANGELOG.md +25 -0
- data/docs/protected-authority.md +23 -7
- data/docs/usage.md +117 -4
- data/exe/ace-scope-ready +55 -0
- data/handbook/workflow-instructions/assign/drive.wf.md +26 -1
- data/lib/ace/assign/atoms/attempt_state_machine.rb +8 -0
- data/lib/ace/assign/atoms/delivery_parameters.rb +4 -39
- data/lib/ace/assign/authority/assignment_inventory.rb +290 -0
- data/lib/ace/assign/authority/campaign_consumer_policy.rb +100 -0
- data/lib/ace/assign/authority/campaign_execution.rb +119 -0
- data/lib/ace/assign/authority/campaign_round_transfer.rb +82 -0
- data/lib/ace/assign/authority/candidate_transfer.rb +169 -2
- data/lib/ace/assign/authority/cleanup_result_protection.rb +53 -0
- data/lib/ace/assign/authority/client.rb +156 -6
- data/lib/ace/assign/authority/deployment.rb +201 -39
- data/lib/ace/assign/authority/deployment_history.rb +146 -0
- data/lib/ace/assign/authority/endcap.rb +131 -18
- data/lib/ace/assign/authority/endcap_attempts.rb +274 -0
- data/lib/ace/assign/authority/endcap_campaign_results.rb +232 -0
- data/lib/ace/assign/authority/endcap_campaign_rounds.rb +266 -0
- data/lib/ace/assign/authority/endcap_contexts.rb +23 -0
- data/lib/ace/assign/authority/endcap_inboxes.rb +3 -280
- data/lib/ace/assign/authority/endcap_prepared_work.rb +80 -0
- data/lib/ace/assign/authority/endcap_publication.rb +207 -0
- data/lib/ace/assign/authority/endcap_results.rb +36 -18
- data/lib/ace/assign/authority/endcap_review_requests.rb +183 -0
- data/lib/ace/assign/authority/endcap_reviews.rb +130 -0
- data/lib/ace/assign/authority/endcap_service_settlement.rb +152 -0
- data/lib/ace/assign/authority/endcap_services.rb +221 -18
- data/lib/ace/assign/authority/execution_scope_observation.rb +468 -41
- data/lib/ace/assign/authority/inbox_context_original.rb +113 -0
- data/lib/ace/assign/authority/launch_attempt_consumers.rb +51 -0
- data/lib/ace/assign/authority/launch_campaign_registration.rb +214 -0
- data/lib/ace/assign/authority/launch_control_channel.rb +313 -0
- data/lib/ace/assign/authority/launch_control_exclusions.rb +141 -0
- data/lib/ace/assign/authority/launch_driver.rb +298 -19
- data/lib/ace/assign/authority/launch_input_inhibition.rb +132 -0
- data/lib/ace/assign/authority/launch_lifecycle.rb +275 -100
- data/lib/ace/assign/authority/launch_prepared_work.rb +122 -0
- data/lib/ace/assign/authority/launch_review.rb +91 -0
- data/lib/ace/assign/authority/launch_scope_abort.rb +0 -7
- data/lib/ace/assign/authority/launch_scope_admission.rb +136 -5
- data/lib/ace/assign/authority/launch_scope_close.rb +68 -37
- data/lib/ace/assign/authority/launch_scope_release.rb +696 -17
- data/lib/ace/assign/authority/launch_steering.rb +353 -0
- data/lib/ace/assign/authority/launch_stop.rb +254 -0
- data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +229 -0
- data/lib/ace/assign/authority/original_inbox_selection.rb +41 -0
- data/lib/ace/assign/authority/posix_acl.rb +5 -2
- data/lib/ace/assign/authority/prepared_input.rb +130 -0
- data/lib/ace/assign/authority/prepared_queue.rb +185 -0
- data/lib/ace/assign/authority/prepared_task_context.rb +116 -0
- data/lib/ace/assign/authority/prepared_work.rb +304 -0
- data/lib/ace/assign/authority/prepared_worker.rb +69 -0
- data/lib/ace/assign/authority/protected_assignment_context.rb +226 -0
- data/lib/ace/assign/authority/readiness_hook.rb +120 -0
- data/lib/ace/assign/authority/router.rb +24 -2
- data/lib/ace/assign/authority/server.rb +103 -33
- data/lib/ace/assign/authority/service_cleanup_dispatch.rb +88 -0
- data/lib/ace/assign/authority/service_cleanup_evidence.rb +180 -0
- data/lib/ace/assign/authority/service_delivery_evidence.rb +101 -0
- data/lib/ace/assign/authority/service_evidence.rb +243 -12
- data/lib/ace/assign/authority/service_publication_evidence.rb +110 -0
- data/lib/ace/assign/authority/task_context_entry.rb +32 -0
- data/lib/ace/assign/authority/transfer_codec.rb +44 -12
- data/lib/ace/assign/cli/commands/assignment_target.rb +15 -2
- data/lib/ace/assign/cli/commands/attempt/base.rb +49 -0
- data/lib/ace/assign/cli/commands/attempt/finish.rb +20 -4
- data/lib/ace/assign/cli/commands/attempt/reconcile.rb +11 -0
- data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +25 -0
- data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +28 -0
- data/lib/ace/assign/cli/commands/authority/launch.rb +58 -6
- data/lib/ace/assign/cli/commands/authority/serve.rb +2 -1
- data/lib/ace/assign/cli/commands/authority/status.rb +1 -1
- data/lib/ace/assign/cli/commands/authority/task_context.rb +31 -0
- data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +25 -0
- data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +30 -0
- data/lib/ace/assign/cli/commands/authority/worker.rb +23 -0
- data/lib/ace/assign/cli/commands/campaign_export_result.rb +55 -0
- data/lib/ace/assign/cli/commands/campaign_record_round.rb +57 -0
- data/lib/ace/assign/cli/commands/create.rb +2 -0
- data/lib/ace/assign/cli/commands/delivery.rb +54 -4
- data/lib/ace/assign/cli/commands/fail.rb +6 -0
- data/lib/ace/assign/cli/commands/finish.rb +23 -2
- data/lib/ace/assign/cli/commands/fork_session.rb +6 -2
- data/lib/ace/assign/cli/commands/inbox_bind.rb +34 -0
- data/lib/ace/assign/cli/commands/protected_submission.rb +61 -0
- data/lib/ace/assign/cli/commands/resume.rb +12 -0
- data/lib/ace/assign/cli/commands/select.rb +2 -0
- data/lib/ace/assign/cli/commands/start.rb +2 -0
- data/lib/ace/assign/cli/commands/status.rb +4 -2
- data/lib/ace/assign/cli/commands/step.rb +3 -1
- data/lib/ace/assign/cli/commands/submit_candidate.rb +34 -0
- data/lib/ace/assign/cli/commands/submit_result.rb +61 -0
- data/lib/ace/assign/cli.rb +36 -5
- data/lib/ace/assign/errors.rb +139 -0
- data/lib/ace/assign/models/assignment.rb +26 -4
- data/lib/ace/assign/models/evidence_event.rb +3 -3
- data/lib/ace/assign/molecules/assignment_manager.rb +12 -2
- data/lib/ace/assign/molecules/attempt_reconciler.rb +20 -0
- data/lib/ace/assign/molecules/canonical_attempt_state.rb +151 -0
- data/lib/ace/assign/molecules/canonical_evidence.rb +5 -2
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +374 -0
- data/lib/ace/assign/molecules/evidence_journal.rb +720 -44
- data/lib/ace/assign/molecules/execution_scope_lineage.rb +27 -9
- data/lib/ace/assign/molecules/fork_session_launcher.rb +16 -6
- data/lib/ace/assign/molecules/journal_input_inhibition.rb +78 -0
- data/lib/ace/assign/molecules/journal_mutation.rb +82 -13
- data/lib/ace/assign/molecules/journal_prompt_mutation.rb +241 -0
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +178 -26
- data/lib/ace/assign/molecules/original_launch_binding.rb +37 -0
- data/lib/ace/assign/molecules/protected_control_exclusion.rb +303 -0
- data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +330 -0
- data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +164 -0
- data/lib/ace/assign/molecules/queue_scanner.rb +26 -11
- data/lib/ace/assign/molecules/receipt_verifier.rb +21 -15
- data/lib/ace/assign/molecules/step_writer.rb +27 -5
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +88 -0
- data/lib/ace/assign/organisms/assignment_executor.rb +4 -4
- data/lib/ace/assign/organisms/attempt_coordinator.rb +66 -76
- data/lib/ace/assign/organisms/prepared_work_builder.rb +194 -0
- data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +100 -0
- data/lib/ace/assign/organisms/task_assignment_creator.rb +4 -2
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +1 -112
- metadata +90 -11
- data/lib/ace/assign/cli/commands/inbox_reconcile.rb +0 -36
|
@@ -1,127 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "ace/herdr/organisms/protected_inbox"
|
|
4
|
-
require_relative "../molecules/execution_scope_lineage"
|
|
5
|
-
|
|
6
3
|
module Ace
|
|
7
4
|
module Assign
|
|
8
5
|
module Authority
|
|
9
6
|
class Endcap
|
|
10
|
-
INBOX_REGISTRATION_FIELDS = %w[event_id attempt_id payload_sha256
|
|
11
|
-
INBOX_PAYLOAD_FIELDS = %w[version event_id attempt_id inbox_context_id registration state
|
|
12
|
-
claim_generation binding receipt_ref signature_ref].freeze
|
|
13
|
-
INBOX_BINDING_FIELDS = %w[project_id assignment_id attempt_id mapping_id inbox_context_id event_id
|
|
14
|
-
registration claim_generation native_binding scope_binding_event_id native_binding_event_id
|
|
15
|
-
scope_native_binding receipt_key_sha256 receipt_sha256 signature_sha256 submitter_uid submitter_role].freeze
|
|
16
|
-
INBOX_REF_FIELDS = %w[artifact_id ref sha256 bytes].freeze
|
|
17
|
-
|
|
18
|
-
# Shared source-owned predicate for existing scope abort/reuse/finish.
|
|
19
|
-
# The caller holds lifecycle exclusion; this is never a wire operation.
|
|
20
|
-
def inbox_settlement_complete!(journal:, events:, params:, map:, commit:)
|
|
21
|
-
protected_journal!(journal)
|
|
22
|
-
retained = journal.read_events(params.fetch("assignment_id"), commit: commit)
|
|
23
|
-
.select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
24
|
-
unless retained == events && Models::EvidenceEvent.chain_valid?(events)
|
|
25
|
-
raise AttemptErrors::EvidenceUnavailable, "inbox settlement requires exact canonical commit"
|
|
26
|
-
end
|
|
27
|
-
registrations = events.select { |event| event["type"] == "inbox_binding" }
|
|
28
|
-
project = @deployment.project(map.fetch("project_id"))
|
|
29
|
-
project.fetch("inbox_contexts", {}).each_key do |id|
|
|
30
|
-
context = @deployment.inbox_context(params.fetch("mapping_id"), id)
|
|
31
|
-
next unless context.fetch("native_mapping_id") == params.fetch("mapping_id")
|
|
32
|
-
@deployment.verify_inbox_context!(params.fetch("mapping_id"), id)
|
|
33
|
-
actual = Ace::Herdr::Organisms::Inbox.retained_events(deliveries_dir: context.fetch("deliveries_dir"), attempt: params.fetch("attempt_id"))
|
|
34
|
-
registered = registrations.filter_map { |event| event.dig("payload", "event_id") if event.dig("payload", "inbox_context_id") == id }
|
|
35
|
-
unless actual.sort == registered.sort
|
|
36
|
-
raise AttemptErrors::EvidenceUnavailable, "registered or unattributable inbox retention differs"
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
registrations.each do |event|
|
|
40
|
-
selected = params.merge("event_id" => event.dig("payload", "event_id"), "inbox_context_id" => event.dig("payload", "inbox_context_id"))
|
|
41
|
-
box, registration, lineage = inbox_environment(events, selected, map)
|
|
42
|
-
proof = verified_inbox_record(journal, events, selected, map, commit, box: box, registration: registration, lineage: lineage)
|
|
43
|
-
unless proof && proof["state"] == "completed"
|
|
44
|
-
raise AttemptErrors::EvidenceUnavailable, "current inbox settlement is incomplete"
|
|
45
|
-
end
|
|
46
|
-
end
|
|
47
|
-
true
|
|
48
|
-
rescue KeyError, TypeError, ArgumentError, Ace::Herdr::Error, Ace::Runtime::RuntimeUnavailableError
|
|
49
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical inbox settlement is unverifiable"
|
|
50
|
-
end
|
|
7
|
+
INBOX_REGISTRATION_FIELDS = %w[event_id attempt_id payload_sha256].freeze
|
|
51
8
|
|
|
52
9
|
private
|
|
53
10
|
|
|
54
|
-
def inbox_request(request)
|
|
55
|
-
params = request.fetch("params")
|
|
56
|
-
unless params.is_a?(Hash) && params.keys.sort == PARAMETERS.fetch("reconcile_inbox").sort
|
|
57
|
-
raise ArgumentError, "invalid inbox parameters"
|
|
58
|
-
end
|
|
59
|
-
%w[mapping_id assignment_id attempt_id event_id inbox_context_id].each { |key| result_id!(params[key]) }
|
|
60
|
-
result_id!(request.fetch("mutation_id"))
|
|
61
|
-
registration = params.fetch("expected_registration")
|
|
62
|
-
unless inbox_registration?(registration) && registration.values_at("event_id", "attempt_id") ==
|
|
63
|
-
params.values_at("event_id", "attempt_id") &&
|
|
64
|
-
params["expected_generation"].is_a?(Integer) && params["expected_generation"] >= 0 &&
|
|
65
|
-
%w[receipt_sha256 signature_sha256].all? { |key| params[key].is_a?(String) && DIGEST.match?(params[key]) }
|
|
66
|
-
raise ArgumentError, "invalid inbox registration or digest"
|
|
67
|
-
end
|
|
68
|
-
map = @deployment.mapping(params.fetch("mapping_id"))
|
|
69
|
-
raise AttemptErrors::UnauthorizedIdentity, "project mapping differs" unless map.fetch("project_id") == request.fetch("project_id")
|
|
70
|
-
[params, map]
|
|
71
|
-
end
|
|
72
|
-
|
|
73
11
|
def inbox_registration?(value)
|
|
74
12
|
value.is_a?(Hash) && value.keys.sort == INBOX_REGISTRATION_FIELDS.sort &&
|
|
75
13
|
%w[event_id attempt_id].all? { |key| value[key].is_a?(String) && Molecules::JournalMutation::ID.match?(value[key]) } &&
|
|
76
|
-
%w[payload_sha256
|
|
14
|
+
%w[payload_sha256].all? { |key| value[key].is_a?(String) && DIGEST.match?(value[key]) }
|
|
77
15
|
end
|
|
78
16
|
|
|
79
|
-
def
|
|
80
|
-
origin = retained_origin(events, params)
|
|
81
|
-
context = @deployment.inbox_context(params.fetch("mapping_id"), params.fetch("inbox_context_id"))
|
|
82
|
-
@kernel.live!(peer)
|
|
83
|
-
service_policy!.visible!(project: map.fetch("project_id"), uid: peer.fetch("uid"))
|
|
84
|
-
allowed = (role == :supervisor && context.fetch("supervisor_uids").include?(peer["uid"])) ||
|
|
85
|
-
(role == :launcher && peer["uid"] == map.fetch("launcher_uid") && @kernel.same?(peer, origin.fetch("launcher_identity")))
|
|
86
|
-
raise AttemptErrors::UnauthorizedIdentity, "inbox requires exact launcher or mapped supervisor" unless allowed
|
|
87
|
-
inbox_environment(events, params, map)
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def inbox_environment(events, params, map)
|
|
91
|
-
unless Models::EvidenceEvent.chain_valid?(events) && events.all? { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
92
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical inbox attempt chain differs"
|
|
93
|
-
end
|
|
94
|
-
context = @deployment.inbox_context(params.fetch("mapping_id"), params.fetch("inbox_context_id"))
|
|
95
|
-
@deployment.verify_inbox_context!(params.fetch("mapping_id"), params.fetch("inbox_context_id"))
|
|
96
|
-
bindings = events.select { |event| event["type"] == "inbox_binding" && event.dig("payload", "event_id") == params["event_id"] }
|
|
97
|
-
raise AttemptErrors::NotFound, "inbox registration missing" if bindings.empty?
|
|
98
|
-
registered = bindings.one? && bindings.first.fetch("payload")
|
|
99
|
-
unless registered.is_a?(Hash) && registered.keys.sort == %w[event_id attempt_id inbox_context_id registration].sort &&
|
|
100
|
-
registered.values_at("event_id", "attempt_id", "inbox_context_id") == params.values_at("event_id", "attempt_id", "inbox_context_id") &&
|
|
101
|
-
inbox_registration?(registered["registration"])
|
|
102
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical inbox context or registration differs"
|
|
103
|
-
end
|
|
104
|
-
if params.key?("expected_registration") && registered.fetch("registration") != params.fetch("expected_registration")
|
|
105
|
-
raise AttemptErrors::Conflict, "expected inbox registration differs"
|
|
106
|
-
end
|
|
107
|
-
lineage = Molecules::ExecutionScopeLineage.new(events: events, project_id: map.fetch("project_id"),
|
|
108
|
-
assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
109
|
-
mapping_id: context.fetch("native_mapping_id"))
|
|
110
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical original native stage missing" unless lineage.native_event
|
|
111
|
-
native_map = @deployment.mapping(context.fetch("native_mapping_id"))
|
|
112
|
-
box = Ace::Herdr::Organisms::ProtectedInbox.build(context: context, mapping: native_map,
|
|
113
|
-
native: lineage.native_event.fetch("payload"), kernel: @kernel)
|
|
114
|
-
current = box.retained_status(event: params.fetch("event_id"))
|
|
115
|
-
unless current.slice(*INBOX_REGISTRATION_FIELDS) == registered.fetch("registration")
|
|
116
|
-
raise AttemptErrors::EvidenceUnavailable, "current inbox differs from canonical registration"
|
|
117
|
-
end
|
|
118
|
-
inbox_native_lineage!(current, lineage)
|
|
119
|
-
[box, registered.fetch("registration"), lineage]
|
|
120
|
-
rescue Ace::Herdr::Error, Ace::Runtime::RuntimeUnavailableError, KeyError, TypeError
|
|
121
|
-
raise AttemptErrors::EvidenceUnavailable, "installed inbox or native lineage is unverifiable"
|
|
122
|
-
end
|
|
123
|
-
|
|
124
|
-
def inbox_native_lineage!(record, lineage, receipt: nil)
|
|
17
|
+
def inbox_native_lineage!(record, lineage)
|
|
125
18
|
workspace = lineage.native_event.fetch("payload").fetch("workspace_id")
|
|
126
19
|
original = record.fetch("origin_target")
|
|
127
20
|
binding = record.fetch("binding")
|
|
@@ -135,179 +28,9 @@ module Ace
|
|
|
135
28
|
raise AttemptErrors::EvidenceUnavailable, "inbox original native child differs"
|
|
136
29
|
end
|
|
137
30
|
end
|
|
138
|
-
if receipt
|
|
139
|
-
signed_binding = receipt["binding"]
|
|
140
|
-
replacement = receipt["replacement_target"]
|
|
141
|
-
unless signed_binding.is_a?(Hash) && signed_binding["session"] == workspace &&
|
|
142
|
-
(!receipt.key?("replacement_target") || (replacement.is_a?(Hash) && replacement["session"] == workspace))
|
|
143
|
-
raise AttemptErrors::EvidenceUnavailable, "signed inbox native workspace differs"
|
|
144
|
-
end
|
|
145
|
-
end
|
|
146
|
-
true
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
def authorize_inbox_transfer!(request:, peer:, role:)
|
|
150
|
-
params, map = inbox_request(request)
|
|
151
|
-
@launch.with_assignment(params: params, map: map) do |journal, _|
|
|
152
|
-
protected_journal!(journal)
|
|
153
|
-
inbox_selection(attempt_events(journal, params), params, map, peer, role)
|
|
154
|
-
end
|
|
155
31
|
true
|
|
156
32
|
end
|
|
157
33
|
|
|
158
|
-
def dispatch_inbox(request:, peer:, role:, transfer:)
|
|
159
|
-
params, map = inbox_request(request)
|
|
160
|
-
authorize_inbox_transfer!(request: request, peer: peer, role: role)
|
|
161
|
-
unless transfer && transfer.count == 2
|
|
162
|
-
raise ArgumentError, "inbox requires receipt and signature"
|
|
163
|
-
end
|
|
164
|
-
bytes, signature = [transfer.bytes(index: 0), transfer.bytes(index: 1)]
|
|
165
|
-
unless [bytes, signature].all? { |part| part.is_a?(String) && part.bytesize.between?(1, 16 * 1024) } &&
|
|
166
|
-
Digest::SHA256.hexdigest(bytes) == params.fetch("receipt_sha256") &&
|
|
167
|
-
Digest::SHA256.hexdigest(signature) == params.fetch("signature_sha256")
|
|
168
|
-
raise ArgumentError, "inbox proof bytes differ"
|
|
169
|
-
end
|
|
170
|
-
receipt = JSON.parse(bytes.dup.force_encoding(Encoding::UTF_8))
|
|
171
|
-
@launch.with_assignment(params: params, map: map) do |journal, _|
|
|
172
|
-
protected_journal!(journal)
|
|
173
|
-
commit = journal.ref_value
|
|
174
|
-
events = journal.read_events(params.fetch("assignment_id"), commit: commit).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
175
|
-
box, registration, lineage = inbox_selection(events, params, map, peer, role)
|
|
176
|
-
replay = journal.mutation_result(request.fetch("mutation_id"))
|
|
177
|
-
if replay && replay["operation"] == "reconcile_inbox"
|
|
178
|
-
retained = verified_inbox(journal, events, params, map, peer, role, commit)
|
|
179
|
-
unless retained && inbox_projection(retained) == replay.fetch("data").slice(*inbox_projection(retained).keys)
|
|
180
|
-
raise AttemptErrors::EvidenceUnavailable, "retained inbox reply differs"
|
|
181
|
-
end
|
|
182
|
-
end
|
|
183
|
-
result = journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
184
|
-
mutation_id: request.fetch("mutation_id"), operation: "reconcile_inbox", parameters_digest: Atoms::EvidenceDigest.digest(params),
|
|
185
|
-
expected_generation: params.fetch("expected_generation"), with_replay: true) do |fresh, fresh_commit, _generation|
|
|
186
|
-
box, registration, lineage = inbox_selection(fresh, params, map, peer, role)
|
|
187
|
-
retained = verified_inbox(journal, fresh, params, map, peer, role, fresh_commit)
|
|
188
|
-
if retained
|
|
189
|
-
unless retained.dig("binding", "receipt_sha256") == params.fetch("receipt_sha256") &&
|
|
190
|
-
retained.dig("binding", "signature_sha256") == params.fetch("signature_sha256")
|
|
191
|
-
raise AttemptErrors::Conflict, "canonical inbox proof already differs"
|
|
192
|
-
end
|
|
193
|
-
next({data: inbox_projection(retained)})
|
|
194
|
-
end
|
|
195
|
-
inbox_native_lineage!(box.retained_status(event: params.fetch("event_id")), lineage, receipt: receipt)
|
|
196
|
-
accepted = box.reconcile(event: params.fetch("event_id"), receipt: receipt, signed_bytes: bytes,
|
|
197
|
-
signature: signature, expected_registration: registration)
|
|
198
|
-
raise AttemptErrors::EvidenceUnavailable, "signed inbox proof refused" if accepted["reconciliation_refusal"]
|
|
199
|
-
binding = inbox_binding(params, map, peer, role, registration, lineage, accepted)
|
|
200
|
-
plan = Molecules::CanonicalEvidence.new(journal: journal).import_plan(**inbox_context(binding),
|
|
201
|
-
admitted_after_event_digest: fresh.last.fetch("digest"), artifacts: [bytes, signature])
|
|
202
|
-
refs = plan.fetch(:references).each_with_index.map do |ref, index|
|
|
203
|
-
ref.merge("artifact_id" => ref.fetch("ref").delete_prefix("evidence/imports/"), "bytes" => [bytes, signature][index].bytesize)
|
|
204
|
-
end
|
|
205
|
-
payload = {"version" => 1, "event_id" => params.fetch("event_id"), "attempt_id" => params.fetch("attempt_id"),
|
|
206
|
-
"inbox_context_id" => params.fetch("inbox_context_id"), "registration" => registration,
|
|
207
|
-
"state" => accepted.fetch("state"), "claim_generation" => accepted.fetch("claim_generation"),
|
|
208
|
-
"binding" => binding, "receipt_ref" => refs[0], "signature_ref" => refs[1]}
|
|
209
|
-
{data: inbox_projection(payload), blobs: plan.fetch(:blobs), events: plan.fetch(:events) +
|
|
210
|
-
[{type: "inbox_reconciliation", payload: payload}]}
|
|
211
|
-
end
|
|
212
|
-
final_commit = journal.ref_value
|
|
213
|
-
final_events = journal.read_events(params.fetch("assignment_id"), commit: final_commit).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
214
|
-
retained = verified_inbox(journal, final_events, params, map, peer, role, final_commit)
|
|
215
|
-
unless retained && inbox_projection(retained) == result.fetch(:data).slice(*inbox_projection(retained).keys)
|
|
216
|
-
raise AttemptErrors::EvidenceUnavailable, "accepted inbox provenance differs"
|
|
217
|
-
end
|
|
218
|
-
result
|
|
219
|
-
end
|
|
220
|
-
rescue JSON::ParserError, EncodingError, Ace::Herdr::Error
|
|
221
|
-
raise AttemptErrors::EvidenceUnavailable, "inbox proof is unverifiable"
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
def inbox_binding(params, map, peer, role, registration, lineage, accepted)
|
|
225
|
-
{"project_id" => map.fetch("project_id"), "assignment_id" => params.fetch("assignment_id"),
|
|
226
|
-
"attempt_id" => params.fetch("attempt_id"), "mapping_id" => params.fetch("mapping_id"),
|
|
227
|
-
"inbox_context_id" => params.fetch("inbox_context_id"), "event_id" => params.fetch("event_id"),
|
|
228
|
-
"registration" => registration, "claim_generation" => accepted.fetch("claim_generation"),
|
|
229
|
-
"native_binding" => accepted.fetch("binding"), "scope_binding_event_id" => lineage.binding_event.fetch("digest"),
|
|
230
|
-
"native_binding_event_id" => lineage.native_event.fetch("digest"), "scope_native_binding" => lineage.native_event.fetch("payload"),
|
|
231
|
-
"receipt_key_sha256" => registration.fetch("receipt_key_sha256"), "receipt_sha256" => params.fetch("receipt_sha256"),
|
|
232
|
-
"signature_sha256" => params.fetch("signature_sha256"), "submitter_uid" => peer.fetch("uid"), "submitter_role" => role.to_s}
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def inbox_context(binding)
|
|
236
|
-
{kind: "inbox", project_id: binding.fetch("project_id"), assignment_id: binding.fetch("assignment_id"),
|
|
237
|
-
attempt_id: binding.fetch("attempt_id"), peer_uid: binding.fetch("submitter_uid"), binding: binding,
|
|
238
|
-
request_id_or_event_id: binding.fetch("event_id"), generation: binding.fetch("claim_generation")}
|
|
239
|
-
end
|
|
240
|
-
|
|
241
|
-
def inbox_projection(payload)
|
|
242
|
-
payload.slice("event_id", "attempt_id", "inbox_context_id", "registration", "state", "receipt_ref", "signature_ref")
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
def verified_inbox(journal, events, params, map, peer, role, commit)
|
|
246
|
-
box, registration, lineage = inbox_selection(events, params, map, peer, role)
|
|
247
|
-
verified_inbox_record(journal, events, params, map, commit, box: box, registration: registration, lineage: lineage)
|
|
248
|
-
end
|
|
249
|
-
|
|
250
|
-
def verified_inbox_record(journal, events, params, map, commit, box:, registration:, lineage:)
|
|
251
|
-
records = events.select { |event| event["type"] == "inbox_reconciliation" && event.dig("payload", "event_id") == params.fetch("event_id") }
|
|
252
|
-
replies = events.select { |event| event["type"] == "authority_mutation" &&
|
|
253
|
-
event.dig("payload", "operation") == "reconcile_inbox" && event.dig("payload", "data", "event_id") == params.fetch("event_id") }
|
|
254
|
-
unless replies.all? { |reply| records.any? { |record| inbox_projection(record.fetch("payload")) ==
|
|
255
|
-
reply.fetch("payload").fetch("data").slice(*inbox_projection(record.fetch("payload")).keys) } }
|
|
256
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical inbox private provenance missing"
|
|
257
|
-
end
|
|
258
|
-
observed = box.retained_status(event: params.fetch("event_id"))
|
|
259
|
-
current_claim = observed.fetch("claim_generation")
|
|
260
|
-
unless records.all? { |record| record.fetch("payload").keys.sort == INBOX_PAYLOAD_FIELDS.sort &&
|
|
261
|
-
record.dig("payload", "claim_generation").is_a?(Integer) && record.dig("payload", "claim_generation").between?(0, current_claim) } &&
|
|
262
|
-
records.map { |record| record.dig("payload", "claim_generation") }.uniq.length == records.length
|
|
263
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical inbox claim history differs"
|
|
264
|
-
end
|
|
265
|
-
current = records.find { |record| record.dig("payload", "claim_generation") == current_claim }
|
|
266
|
-
return nil unless current
|
|
267
|
-
payload = current.fetch("payload")
|
|
268
|
-
binding = payload.fetch("binding")
|
|
269
|
-
unless payload.keys.sort == INBOX_PAYLOAD_FIELDS.sort && payload["version"] == 1 && binding.keys.sort == INBOX_BINDING_FIELDS.sort &&
|
|
270
|
-
%w[receipt_sha256 signature_sha256].all? { |key| binding[key].is_a?(String) && DIGEST.match?(binding[key]) } &&
|
|
271
|
-
binding["submitter_uid"].is_a?(Integer) && binding["submitter_uid"] > 0 && %w[launcher supervisor].include?(binding["submitter_role"]) &&
|
|
272
|
-
payload.values_at("event_id", "attempt_id", "inbox_context_id") == params.values_at("event_id", "attempt_id", "inbox_context_id") &&
|
|
273
|
-
payload["registration"] == registration && payload["claim_generation"] == binding["claim_generation"]
|
|
274
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical inbox fields differ"
|
|
275
|
-
end
|
|
276
|
-
retained_peer = {"uid" => binding.fetch("submitter_uid")}
|
|
277
|
-
expected = inbox_binding(params.merge("receipt_sha256" => binding.fetch("receipt_sha256"),
|
|
278
|
-
"signature_sha256" => binding.fetch("signature_sha256")), map, retained_peer, binding.fetch("submitter_role").to_sym,
|
|
279
|
-
registration, lineage, observed)
|
|
280
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical inbox native/key context differs" unless binding == expected
|
|
281
|
-
canonical = Molecules::CanonicalEvidence.new(journal: journal)
|
|
282
|
-
raw = %w[receipt_ref signature_ref].map do |key|
|
|
283
|
-
ref = payload.fetch(key)
|
|
284
|
-
unless ref.is_a?(Hash) && ref.keys.sort == INBOX_REF_FIELDS.sort &&
|
|
285
|
-
ref["ref"] == "evidence/imports/#{ref['artifact_id']}" && ref["bytes"].is_a?(Integer) && ref["bytes"].between?(1, 16 * 1024)
|
|
286
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical inbox reference differs"
|
|
287
|
-
end
|
|
288
|
-
bytes = canonical.read(ref.slice("ref", "sha256"), **inbox_context(binding), commit: commit)
|
|
289
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical inbox byte count differs" unless bytes.bytesize == ref["bytes"]
|
|
290
|
-
bytes
|
|
291
|
-
end
|
|
292
|
-
unless raw.map { |bytes| Digest::SHA256.hexdigest(bytes) } == binding.values_at("receipt_sha256", "signature_sha256")
|
|
293
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical inbox raw digests differ"
|
|
294
|
-
end
|
|
295
|
-
receipt = JSON.parse(raw.first)
|
|
296
|
-
inbox_native_lineage!(observed, lineage, receipt: receipt)
|
|
297
|
-
verified = box.verify_reconciliation(event: params.fetch("event_id"), receipt: receipt,
|
|
298
|
-
signed_bytes: raw.first, signature: raw.last, expected_registration: registration)
|
|
299
|
-
signed_state = receipt["outcome"] == "consumed" ? "completed" : "queued"
|
|
300
|
-
if verified["reconciliation_refusal"] || verified["state"] != payload["state"] || payload["state"] != signed_state
|
|
301
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical inbox signature or settlement differs"
|
|
302
|
-
end
|
|
303
|
-
unless events.any? { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "reconcile_inbox" &&
|
|
304
|
-
event.fetch("payload").fetch("data").slice(*inbox_projection(payload).keys) == inbox_projection(payload) }
|
|
305
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical inbox owner reply missing"
|
|
306
|
-
end
|
|
307
|
-
payload
|
|
308
|
-
rescue KeyError, TypeError, ArgumentError, JSON::ParserError, Ace::Herdr::Error
|
|
309
|
-
raise AttemptErrors::EvidenceUnavailable, "canonical inbox evidence is unverifiable"
|
|
310
|
-
end
|
|
311
34
|
end
|
|
312
35
|
end
|
|
313
36
|
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
|