ace-assign 0.63.1 → 0.64.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/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 +43 -0
- data/docs/demo/canonical-skill-source.tape.yml +4 -4
- data/docs/protected-authority.md +266 -0
- data/docs/usage.md +91 -2
- data/handbook/workflow-instructions/assign/drive.wf.md +2 -2
- data/handbook/workflow-instructions/assign/prepare.wf.md +2 -2
- data/lib/ace/assign/atoms/delivery_parameters.rb +54 -0
- data/lib/ace/assign/authority/candidate_transfer.rb +160 -0
- data/lib/ace/assign/authority/client.rb +107 -0
- data/lib/ace/assign/authority/deployment.rb +465 -0
- data/lib/ace/assign/authority/endcap.rb +342 -0
- data/lib/ace/assign/authority/endcap_inboxes.rb +314 -0
- data/lib/ace/assign/authority/endcap_results.rb +380 -0
- data/lib/ace/assign/authority/endcap_services.rb +395 -0
- data/lib/ace/assign/authority/evidence_transfer.rb +67 -0
- data/lib/ace/assign/authority/execution_scope_observation.rb +476 -0
- data/lib/ace/assign/authority/launch_driver.rb +114 -0
- data/lib/ace/assign/authority/launch_lifecycle.rb +822 -0
- data/lib/ace/assign/authority/launch_scope_abort.rb +88 -0
- data/lib/ace/assign/authority/launch_scope_admission.rb +84 -0
- data/lib/ace/assign/authority/launch_scope_close.rb +115 -0
- data/lib/ace/assign/authority/launch_scope_parent.rb +44 -0
- data/lib/ace/assign/authority/launch_scope_release.rb +180 -0
- data/lib/ace/assign/authority/posix_acl.rb +56 -0
- data/lib/ace/assign/authority/private_directory.rb +33 -0
- data/lib/ace/assign/authority/receipt_transfer.rb +48 -0
- data/lib/ace/assign/authority/router.rb +54 -0
- data/lib/ace/assign/authority/server.rb +230 -0
- data/lib/ace/assign/authority/service_evidence.rb +55 -0
- data/lib/ace/assign/authority/transfer_codec.rb +155 -0
- data/lib/ace/assign/cli/commands/authority/launch.rb +45 -0
- data/lib/ace/assign/cli/commands/authority/serve.rb +30 -0
- data/lib/ace/assign/cli/commands/authority/status.rb +26 -0
- data/lib/ace/assign/cli/commands/authority/terminate.rb +29 -0
- data/lib/ace/assign/cli/commands/create.rb +9 -1
- data/lib/ace/assign/cli/commands/delivery.rb +57 -0
- data/lib/ace/assign/cli/commands/inbox_reconcile.rb +36 -0
- data/lib/ace/assign/cli/commands/resume.rb +26 -0
- data/lib/ace/assign/cli.rb +22 -1
- data/lib/ace/assign/models/evidence_event.rb +3 -1
- data/lib/ace/assign/molecules/attempt_reconciler.rb +31 -12
- data/lib/ace/assign/molecules/canonical_evidence.rb +150 -0
- data/lib/ace/assign/molecules/evidence_journal.rb +218 -63
- data/lib/ace/assign/molecules/execution_identity_resolver.rb +49 -9
- data/lib/ace/assign/molecules/execution_scope_lineage.rb +327 -0
- data/lib/ace/assign/molecules/journal_mutation.rb +202 -0
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +4 -0
- data/lib/ace/assign/molecules/proposal_journal.rb +151 -0
- data/lib/ace/assign/molecules/receipt_verifier.rb +49 -19
- data/lib/ace/assign/organisms/assignment_executor.rb +6 -0
- data/lib/ace/assign/organisms/attempt_coordinator.rb +312 -13
- data/lib/ace/assign/organisms/delivery_coordinator.rb +342 -0
- data/lib/ace/assign/organisms/task_assignment_creator.rb +8 -3
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +13 -0
- metadata +107 -8
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "digest"
|
|
5
|
+
require "securerandom"
|
|
6
|
+
require_relative "candidate_transfer"
|
|
7
|
+
require_relative "receipt_transfer"
|
|
8
|
+
require_relative "service_evidence"
|
|
9
|
+
require_relative "../molecules/canonical_evidence"
|
|
10
|
+
|
|
11
|
+
module Ace
|
|
12
|
+
module Assign
|
|
13
|
+
module Authority
|
|
14
|
+
# Business admission shares the launch origin, lifecycle exclusion and
|
|
15
|
+
# journal CAS. Network transfer is completed outside those locks.
|
|
16
|
+
class Endcap
|
|
17
|
+
OPERATIONS = %w[submit_candidate export_candidate assign_review accept_review request_service begin_dispatch complete_service service_authorization service_status submit_result evidence_fetch reconcile_inbox].freeze
|
|
18
|
+
TRANSFER_OPERATIONS = {
|
|
19
|
+
"reconcile_inbox" => {direction: :upload, purpose: :inbox_proof, roles: %i[launcher supervisor]},
|
|
20
|
+
"submit_result" => {direction: :upload, purpose: :receipt_artifacts, roles: [:worker]},
|
|
21
|
+
"evidence_fetch" => {direction: :download, purpose: :artifacts, roles: %i[worker reviewer executor launcher supervisor]},
|
|
22
|
+
"submit_candidate" => {direction: :upload, purpose: :candidate, roles: %i[worker launcher]},
|
|
23
|
+
"export_candidate" => {direction: :download, purpose: :candidate, roles: %i[reviewer executor]},
|
|
24
|
+
"accept_review" => {direction: :upload, purpose: :receipt_artifacts, roles: [:reviewer]},
|
|
25
|
+
"request_service" => {direction: :upload, purpose: :service_input, roles: [:executor]},
|
|
26
|
+
"begin_dispatch" => {direction: :upload, purpose: :service_input, roles: [:executor]},
|
|
27
|
+
"service_authorization" => {direction: :upload, purpose: :service_input, roles: [:executor]},
|
|
28
|
+
"complete_service" => {direction: :upload, purpose: :receipt_artifacts, roles: [:executor]}
|
|
29
|
+
}.freeze
|
|
30
|
+
PARAMETERS = {
|
|
31
|
+
"reconcile_inbox" => %w[mapping_id assignment_id attempt_id expected_generation event_id inbox_context_id expected_registration receipt_sha256 signature_sha256 transfer],
|
|
32
|
+
"submit_result" => %w[mapping_id assignment_id attempt_id expected_generation candidate_generation head receipt_sha256 transfer],
|
|
33
|
+
"evidence_fetch" => %w[mapping_id assignment_id attempt_id kind purpose_id artifact_id],
|
|
34
|
+
"submit_candidate" => %w[mapping_id assignment_id attempt_id expected_generation candidate_generation head transfer],
|
|
35
|
+
"export_candidate" => %w[mapping_id assignment_id attempt_id candidate_generation head purpose_id],
|
|
36
|
+
"assign_review" => %w[mapping_id assignment_id attempt_id expected_generation candidate_generation head reviewer_uid reviewer_process_binding],
|
|
37
|
+
"accept_review" => %w[mapping_id assignment_id attempt_id expected_generation candidate_generation head purpose_id receipt_sha256 transfer],
|
|
38
|
+
"request_service" => %w[mapping_id assignment_id attempt_id expected_generation candidate_generation head request_id operation input_digest target authorization service_id worker_process_binding transfer],
|
|
39
|
+
"begin_dispatch" => %w[mapping_id assignment_id attempt_id expected_generation candidate_generation head request_id claim_binding transfer],
|
|
40
|
+
"service_status" => %w[mapping_id assignment_id attempt_id candidate_generation head request_id],
|
|
41
|
+
"service_authorization" => %w[mapping_id assignment_id attempt_id candidate_generation head request_id claim_binding input_digest transfer],
|
|
42
|
+
"complete_service" => %w[mapping_id assignment_id attempt_id candidate_generation head request_id claim_binding receipt_sha256 transfer]
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
def initialize(deployment:, launch:, kernel: Ace::Runtime::Molecules::ProtectedLinux.new, service_policy: nil)
|
|
46
|
+
@deployment, @launch, @kernel, @service_policy = deployment, launch, kernel, service_policy
|
|
47
|
+
launch.attach_result_owner(self) if launch.respond_to?(:attach_result_owner)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def authorize_transfer!(request:, peer:, role:)
|
|
51
|
+
return authorize_inbox_transfer!(request: request, peer: peer, role: role) if request.fetch("operation") == "reconcile_inbox"
|
|
52
|
+
return authorize_result_transfer!(request: request, peer: peer, role: role) if %w[submit_result evidence_fetch].include?(request.fetch("operation"))
|
|
53
|
+
params, map = validate_request(request)
|
|
54
|
+
return authorize_service_transfer!(request, params, map, peer, role) if %w[request_service begin_dispatch complete_service service_authorization].include?(request.fetch("operation"))
|
|
55
|
+
@launch.with_assignment(params: params, map: map) do |journal, _registration|
|
|
56
|
+
protected_journal!(journal)
|
|
57
|
+
events = attempt_events(journal, params)
|
|
58
|
+
origin = active_origin(events, params)
|
|
59
|
+
if request.fetch("operation") == "submit_candidate"
|
|
60
|
+
worker_or_launcher!(peer, role, map, origin)
|
|
61
|
+
candidate_generation!(candidate(events), params)
|
|
62
|
+
else
|
|
63
|
+
export_admission!(journal, events, params, map, origin, peer, role)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def dispatch(request:, peer:, role:, transfer: nil)
|
|
70
|
+
return dispatch_inbox(request: request, peer: peer, role: role, transfer: transfer) if request.fetch("operation") == "reconcile_inbox"
|
|
71
|
+
return dispatch_result(request: request, peer: peer, role: role, transfer: transfer) if %w[submit_result evidence_fetch].include?(request.fetch("operation"))
|
|
72
|
+
params, map = validate_request(request)
|
|
73
|
+
return service_status(request, params, map, peer, role) if request.fetch("operation") == "service_status"
|
|
74
|
+
return dispatch_service(request, params, map, peer, role, transfer) if %w[request_service begin_dispatch complete_service service_authorization].include?(request.fetch("operation"))
|
|
75
|
+
admitted = nil
|
|
76
|
+
if request.fetch("operation") == "submit_candidate"
|
|
77
|
+
authorize_transfer!(request: request, peer: peer, role: role)
|
|
78
|
+
raise ArgumentError, "candidate transfer is missing" unless transfer && transfer.count == 1
|
|
79
|
+
bytes = transfer.bytes
|
|
80
|
+
descriptor = params.fetch("transfer")
|
|
81
|
+
project = @deployment.project(map.fetch("project_id"))
|
|
82
|
+
admitted = CandidateTransfer.new(root: project.fetch("candidate_root")).admit(
|
|
83
|
+
bytes: bytes, sha256: descriptor.fetch("sha256"), size: descriptor.fetch("bytes"), head: params.fetch("head"))
|
|
84
|
+
elsif request.fetch("operation") == "accept_review"
|
|
85
|
+
authorize_transfer!(request: request, peer: peer, role: role)
|
|
86
|
+
admitted = ReceiptTransfer.decode(input: transfer, receipt_sha256: params.fetch("receipt_sha256"),
|
|
87
|
+
artifact_field: "artifacts", reference_key: "path")
|
|
88
|
+
end
|
|
89
|
+
@launch.with_assignment(params: params, map: map) do |journal, _registration|
|
|
90
|
+
protected_journal!(journal)
|
|
91
|
+
events = attempt_events(journal, params)
|
|
92
|
+
origin = active_origin(events, params)
|
|
93
|
+
if request.fetch("operation") == "export_candidate"
|
|
94
|
+
current = export_admission!(journal, events, params, map, origin, peer, role)
|
|
95
|
+
bytes = journal.blob(current.fetch("bundle_ref"))
|
|
96
|
+
unless bytes.bytesize == current.fetch("bytes") && Digest::SHA256.hexdigest(bytes) == current.fetch("sha256")
|
|
97
|
+
raise AttemptErrors::EvidenceUnavailable, "canonical candidate bundle differs"
|
|
98
|
+
end
|
|
99
|
+
return {data: current.slice("head", "tree", "candidate_generation", "sha256", "bytes"),
|
|
100
|
+
replayed: false, transfer_parts: [bytes]}
|
|
101
|
+
end
|
|
102
|
+
# Journal replay skips its callback. Authenticate the caller here
|
|
103
|
+
# as well as on every fresh/CAS-retried mutation below.
|
|
104
|
+
if request.fetch("operation") == "accept_review"
|
|
105
|
+
export_admission!(journal, events, params, map, origin, peer, role)
|
|
106
|
+
else
|
|
107
|
+
worker_or_launcher!(peer, role, map, origin,
|
|
108
|
+
launcher_only: request.fetch("operation") == "assign_review")
|
|
109
|
+
end
|
|
110
|
+
journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
111
|
+
mutation_id: request.fetch("mutation_id"), operation: request.fetch("operation"),
|
|
112
|
+
parameters_digest: Atoms::EvidenceDigest.digest(params), expected_generation: params.fetch("expected_generation"),
|
|
113
|
+
with_replay: true) do |events, _commit, _generation|
|
|
114
|
+
origin = active_origin(events, params)
|
|
115
|
+
case request.fetch("operation")
|
|
116
|
+
when "submit_candidate"
|
|
117
|
+
worker_or_launcher!(peer, role, map, origin)
|
|
118
|
+
current = candidate(events)
|
|
119
|
+
candidate_generation!(current, params)
|
|
120
|
+
path = "candidates/bundles/#{SecureRandom.hex(16)}"
|
|
121
|
+
data = admitted.except("bundle").merge("bundle_ref" => path,
|
|
122
|
+
"candidate_generation" => params.fetch("candidate_generation") + 1,
|
|
123
|
+
"worker_uid" => map.fetch("worker_uid"))
|
|
124
|
+
{data: data, blobs: {path => admitted.fetch("bundle")}}
|
|
125
|
+
when "assign_review"
|
|
126
|
+
worker_or_launcher!(peer, role, map, origin, launcher_only: true)
|
|
127
|
+
current = exact_candidate!(candidate(events), params)
|
|
128
|
+
reviewer = params.fetch("reviewer_process_binding")
|
|
129
|
+
uid = params.fetch("reviewer_uid")
|
|
130
|
+
project = @deployment.project(map.fetch("project_id"))
|
|
131
|
+
unless uid.is_a?(Integer) && project.fetch("reviewer_uids").include?(uid) && uid != map.fetch("worker_uid") &&
|
|
132
|
+
reviewer.is_a?(Hash) && reviewer["uid"] == uid
|
|
133
|
+
raise AttemptErrors::UnauthorizedIdentity, "reviewer must be independently mapped"
|
|
134
|
+
end
|
|
135
|
+
@kernel.live!(reviewer)
|
|
136
|
+
{data: current.slice("head", "candidate_generation").merge(
|
|
137
|
+
"review_id" => SecureRandom.hex(16), "reviewer_uid" => uid, "reviewer_actor" => "uid-#{uid}",
|
|
138
|
+
"reviewer_process_binding" => reviewer)}
|
|
139
|
+
when "accept_review"
|
|
140
|
+
current = export_admission!(journal, events, params, map, origin, peer, role)
|
|
141
|
+
review = assigned_review(events)
|
|
142
|
+
accept_review_plan(journal, events, params, map, current, review, admitted)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def protected_journal!(journal)
|
|
151
|
+
unless journal.evidence_mode == :protected
|
|
152
|
+
raise ArgumentError, "full-service Endcap requires canonical protected evidence composition"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def validate_request(request)
|
|
157
|
+
operation = request.fetch("operation")
|
|
158
|
+
params = request.fetch("params")
|
|
159
|
+
unless params.is_a?(Hash) && params.keys.sort == PARAMETERS.fetch(operation).sort
|
|
160
|
+
raise ArgumentError, "invalid Endcap parameters"
|
|
161
|
+
end
|
|
162
|
+
%w[mapping_id assignment_id attempt_id].each do |key|
|
|
163
|
+
raise ArgumentError, "invalid Endcap identifier" unless params[key].is_a?(String) && params[key].match?(Molecules::JournalMutation::ID)
|
|
164
|
+
end
|
|
165
|
+
unless params["head"].is_a?(String) && params["head"].match?(CandidateTransfer::SHA) &&
|
|
166
|
+
params["candidate_generation"].is_a?(Integer) && params["candidate_generation"] >= 0
|
|
167
|
+
raise ArgumentError, "invalid candidate binding"
|
|
168
|
+
end
|
|
169
|
+
if %w[service_authorization service_status].include?(operation) && !request.fetch("mutation_id").nil?
|
|
170
|
+
raise ArgumentError, "authorization read must not supply a mutation ID"
|
|
171
|
+
end
|
|
172
|
+
map = @deployment.mapping(params.fetch("mapping_id"))
|
|
173
|
+
raise AttemptErrors::UnauthorizedIdentity, "project mapping differs" unless map.fetch("project_id") == request.fetch("project_id")
|
|
174
|
+
[params, map]
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def attempt_events(journal, params)
|
|
178
|
+
journal.read_events(params.fetch("assignment_id")).select { |event| event["attempt_id"] == params.fetch("attempt_id") }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def active_origin(events, params)
|
|
182
|
+
unless Models::EvidenceEvent.chain_valid?(events) &&
|
|
183
|
+
!events.any? { |event| %w[receipt_accepted transition reconciliation].include?(event["type"]) &&
|
|
184
|
+
%w[succeeded failed stopped].include?(event.dig("payload", "to") || event.dig("payload", "resolution") || event.dig("payload", "receipt", "verdict")) }
|
|
185
|
+
raise AttemptErrors::InvalidState, "attempt is not active"
|
|
186
|
+
end
|
|
187
|
+
origin = @launch.origin(events, assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
188
|
+
mapping_id: params.fetch("mapping_id"))
|
|
189
|
+
raise AttemptErrors::InvalidState, "exact bound launch is required" unless %w[bound issued].include?(origin["phase"])
|
|
190
|
+
@kernel.live!(origin.fetch("process_binding").fetch("process_identity"))
|
|
191
|
+
origin
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def worker_or_launcher!(peer, role, map, origin, launcher_only: false)
|
|
195
|
+
anchor = if role == :worker && !launcher_only && peer["uid"] == map.fetch("worker_uid")
|
|
196
|
+
origin.fetch("process_binding").fetch("process_identity")
|
|
197
|
+
elsif role == :launcher && peer["uid"] == map.fetch("launcher_uid")
|
|
198
|
+
origin.fetch("launcher_identity")
|
|
199
|
+
else
|
|
200
|
+
raise AttemptErrors::UnauthorizedIdentity, "operation requires owning worker or launcher"
|
|
201
|
+
end
|
|
202
|
+
unless @kernel.descendant?(peer, anchor)
|
|
203
|
+
raise AttemptErrors::UnauthorizedIdentity, "peer does not descend from exact launch binding"
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def candidate(events)
|
|
208
|
+
event = events.reverse.find { |entry| entry["type"] == "authority_mutation" && entry.dig("payload", "operation") == "submit_candidate" }
|
|
209
|
+
event&.dig("payload", "data")
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def candidate_generation!(current, params)
|
|
213
|
+
unless (current && current.fetch("candidate_generation") || 0) == params.fetch("candidate_generation")
|
|
214
|
+
raise AttemptErrors::Conflict, "candidate generation changed"
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def exact_candidate!(current, params)
|
|
219
|
+
candidate_generation!(current, params)
|
|
220
|
+
raise AttemptErrors::Conflict, "candidate head differs" unless current && current.fetch("head") == params.fetch("head")
|
|
221
|
+
current
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def export_admission!(journal, events, params, map, _origin, peer, role)
|
|
225
|
+
current = exact_candidate!(candidate(events), params)
|
|
226
|
+
@kernel.live!(peer)
|
|
227
|
+
if role == :reviewer
|
|
228
|
+
assignment = assigned_review(events)
|
|
229
|
+
unless assignment && assignment["review_id"] == params["purpose_id"] && assignment["head"] == current["head"] &&
|
|
230
|
+
assignment["candidate_generation"] == current["candidate_generation"] && assignment["reviewer_uid"] == peer["uid"] &&
|
|
231
|
+
@kernel.descendant?(peer, assignment.fetch("reviewer_process_binding"))
|
|
232
|
+
raise AttemptErrors::UnauthorizedIdentity, "candidate export requires assigned independent reviewer"
|
|
233
|
+
end
|
|
234
|
+
elsif role == :executor
|
|
235
|
+
record = journal.service_request(params.fetch("purpose_id"))
|
|
236
|
+
unless record && record["assignment_id"] == params["assignment_id"] && record["attempt_id"] == params["attempt_id"] &&
|
|
237
|
+
record["executor_uid"] == peer["uid"] && record["candidate_head"] == current["head"] &&
|
|
238
|
+
record["candidate_generation"] == current["candidate_generation"] && record["dispatch_phase"] == "issued"
|
|
239
|
+
raise AttemptErrors::UnauthorizedIdentity, "candidate export requires current executor ticket"
|
|
240
|
+
end
|
|
241
|
+
else
|
|
242
|
+
raise AttemptErrors::UnauthorizedIdentity, "candidate export purpose is unauthorized"
|
|
243
|
+
end
|
|
244
|
+
current
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def assigned_review(events)
|
|
248
|
+
events.reverse.find { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "assign_review" }&.dig("payload", "data")
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# A retained approval grants permission only while its original imported
|
|
252
|
+
# bytes, independent reviewer and exact candidate still verify. An old
|
|
253
|
+
# successful mutation reply is not a substitute for canonical evidence.
|
|
254
|
+
def approved_review!(journal, events, params, map, current)
|
|
255
|
+
accepted = events.reverse.find { |event| event["type"] == "authority_mutation" &&
|
|
256
|
+
event.dig("payload", "operation") == "accept_review" }&.dig("payload", "data")
|
|
257
|
+
review = assigned_review(events)
|
|
258
|
+
unless accepted && review && accepted["head"] == current["head"] &&
|
|
259
|
+
accepted["candidate_generation"] == current["candidate_generation"] &&
|
|
260
|
+
accepted["review_id"] == review["review_id"] && accepted["reviewer_uid"] == review["reviewer_uid"] &&
|
|
261
|
+
accepted["review_binding"] == review.slice("review_id", "head", "candidate_generation", "reviewer_uid", "reviewer_process_binding") &&
|
|
262
|
+
review["reviewer_uid"] != map.fetch("worker_uid")
|
|
263
|
+
raise AttemptErrors::ReceiptRejected, "current candidate requires its exact independent review"
|
|
264
|
+
end
|
|
265
|
+
context = {kind: "review", project_id: map.fetch("project_id"), assignment_id: params.fetch("assignment_id"),
|
|
266
|
+
attempt_id: params.fetch("attempt_id"), peer_uid: review.fetch("reviewer_uid"),
|
|
267
|
+
binding: accepted.fetch("review_binding"), request_id_or_event_id: review.fetch("review_id"),
|
|
268
|
+
generation: current.fetch("candidate_generation")}
|
|
269
|
+
canonical = Molecules::CanonicalEvidence.new(journal: journal)
|
|
270
|
+
commit = journal.ref_value
|
|
271
|
+
reader = ->(_data, artifact) { canonical.read({"ref" => artifact.fetch("path"), "sha256" => artifact.fetch("sha256")},
|
|
272
|
+
**context, commit: commit) }
|
|
273
|
+
intent = events.find { |event| event["type"] == "intent" }
|
|
274
|
+
attempt = journal.send(:build_attempt, params.fetch("assignment_id"), params.fetch("attempt_id"),
|
|
275
|
+
intent.fetch("payload"), events, "running")
|
|
276
|
+
identity = Molecules::ExecutionIdentityResolver::Identity.new(actor: "protected-authority", role: "service", runtime: "unix")
|
|
277
|
+
receipt = Molecules::ReceiptVerifier.new(artifact_reader: reader).verify!(accepted.fetch("review_receipt"),
|
|
278
|
+
attempt: attempt, identity: identity, live_head: current.fetch("head"), repo_root: journal.repo_root)
|
|
279
|
+
unless receipt.digest == accepted.fetch("receipt_digest") && receipt.operation == "review" &&
|
|
280
|
+
receipt.to_h.dig("review", "reviewer", "actor") == review.fetch("reviewer_actor")
|
|
281
|
+
raise AttemptErrors::ReceiptRejected, "canonical review receipt differs"
|
|
282
|
+
end
|
|
283
|
+
accepted
|
|
284
|
+
rescue KeyError
|
|
285
|
+
raise AttemptErrors::ReceiptRejected, "canonical review binding is incomplete"
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def accept_review_plan(journal, events, params, map, current, review, admitted)
|
|
289
|
+
receipt = admitted.fetch(:receipt)
|
|
290
|
+
raise AttemptErrors::EvidenceUnavailable, "protected campaigns require their canonical owner" if receipt["campaign"]
|
|
291
|
+
unless receipt["operation"] == "review" && receipt["verdict"] == "succeeded" &&
|
|
292
|
+
receipt["producer"] == {"actor" => map.fetch("worker_actor"), "role" => "worker", "runtime" => "herdr"} &&
|
|
293
|
+
receipt.dig("review", "reviewer", "actor") == review.fetch("reviewer_actor")
|
|
294
|
+
raise AttemptErrors::ReceiptRejected, "review attribution must match authenticated candidate and reviewer"
|
|
295
|
+
end
|
|
296
|
+
original = Models::ExecutionReceipt.from_h(receipt)
|
|
297
|
+
unless original.digest == Atoms::EvidenceDigest.digest(original.digest_payload)
|
|
298
|
+
raise AttemptErrors::ReceiptRejected, "uploaded review receipt digest differs"
|
|
299
|
+
end
|
|
300
|
+
context = {kind: "review", project_id: map.fetch("project_id"), assignment_id: params.fetch("assignment_id"),
|
|
301
|
+
attempt_id: params.fetch("attempt_id"), peer_uid: review.fetch("reviewer_uid"),
|
|
302
|
+
binding: review.slice("review_id", "head", "candidate_generation", "reviewer_uid", "reviewer_process_binding"),
|
|
303
|
+
request_id_or_event_id: review.fetch("review_id"), generation: current.fetch("candidate_generation")}
|
|
304
|
+
canonical = Molecules::CanonicalEvidence.new(journal: journal)
|
|
305
|
+
plan = canonical.import_plan(**context, artifacts: admitted.fetch(:artifacts),
|
|
306
|
+
admitted_after_event_digest: events.last.fetch("digest"))
|
|
307
|
+
original_refs = receipt.fetch("artifacts")
|
|
308
|
+
normalized = JSON.parse(JSON.generate(receipt)).except("digest", "recorded_at")
|
|
309
|
+
normalized["artifacts"] = plan.fetch(:references).map { |reference| {"path" => reference.fetch("ref"), "sha256" => reference.fetch("sha256")} }
|
|
310
|
+
if normalized["campaign"]
|
|
311
|
+
index = original_refs.index(normalized.fetch("campaign").fetch("result"))
|
|
312
|
+
raise AttemptErrors::ReceiptRejected, "campaign result must name an uploaded artifact" unless index
|
|
313
|
+
normalized["campaign"]["result"] = normalized.fetch("artifacts").fetch(index)
|
|
314
|
+
end
|
|
315
|
+
pending = journal.send(:chain_mutation_events, params.fetch("attempt_id"), events.last.fetch("digest"), plan.fetch(:events))
|
|
316
|
+
reader = lambda do |_data, artifact|
|
|
317
|
+
canonical.read_pending({"ref" => artifact.fetch("path"), "sha256" => artifact.fetch("sha256")},
|
|
318
|
+
**context, current_events: events, pending_events: pending, blobs: plan.fetch(:blobs), commit: journal.ref_value)
|
|
319
|
+
end
|
|
320
|
+
intent = events.find { |event| event["type"] == "intent" }
|
|
321
|
+
attempt = journal.send(:build_attempt, params.fetch("assignment_id"), params.fetch("attempt_id"),
|
|
322
|
+
intent.fetch("payload"), events, "running")
|
|
323
|
+
identity = Molecules::ExecutionIdentityResolver::Identity.new(actor: "protected-authority", role: "service", runtime: "unix")
|
|
324
|
+
verified = Molecules::ReceiptVerifier.new(artifact_reader: reader).verify!(normalized,
|
|
325
|
+
attempt: attempt, identity: identity, live_head: current.fetch("head"), repo_root: journal.repo_root)
|
|
326
|
+
plan.merge(data: {"head" => current.fetch("head"), "candidate_generation" => current.fetch("candidate_generation"),
|
|
327
|
+
"review_id" => review.fetch("review_id"), "reviewer_uid" => review.fetch("reviewer_uid"),
|
|
328
|
+
"review_binding" => context.fetch(:binding), "review_receipt" => verified.to_h,
|
|
329
|
+
"receipt_digest" => verified.digest, "uploaded_receipt_sha256" => admitted.fetch(:receipt_sha256)})
|
|
330
|
+
rescue KeyError
|
|
331
|
+
raise AttemptErrors::ReceiptRejected, "review receipt binding is incomplete"
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
require_relative "endcap_services"
|
|
339
|
+
|
|
340
|
+
require_relative "endcap_results"
|
|
341
|
+
|
|
342
|
+
require_relative "endcap_inboxes"
|