ace-assign 0.69.0 → 0.71.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/presets/deliver-release.yml +11 -8
- data/.ace-defaults/assign/presets/integrate-batch.yml +10 -5
- data/.ace-defaults/assign/presets/supervise-delivery.yml +6 -4
- data/CHANGELOG.md +27 -0
- data/handbook/skills/as-assign-drive/SKILL.md +5 -2
- data/handbook/workflow-instructions/assign/drive.wf.md +77 -0
- data/lib/ace/assign/atoms/assignment_scope.rb +5 -0
- data/lib/ace/assign/atoms/candidate_limits.rb +20 -0
- data/lib/ace/assign/atoms/git_blob_batch.rb +59 -0
- data/lib/ace/assign/authority/assignment_definition.rb +35 -0
- data/lib/ace/assign/authority/candidate_blob_batch.rb +55 -0
- data/lib/ace/assign/authority/candidate_head_comparison.rb +139 -0
- data/lib/ace/assign/authority/candidate_transfer.rb +57 -27
- data/lib/ace/assign/authority/command_execution.rb +180 -0
- data/lib/ace/assign/authority/endcap.rb +4 -2
- data/lib/ace/assign/authority/endcap_attempts.rb +1 -1
- data/lib/ace/assign/authority/endcap_campaign_rounds.rb +1 -1
- data/lib/ace/assign/authority/endcap_publication.rb +0 -166
- data/lib/ace/assign/authority/endcap_service_context.rb +38 -0
- data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +176 -0
- data/lib/ace/assign/authority/{endcap_services.rb → fixed_service_lifecycle.rb} +42 -46
- data/lib/ace/assign/authority/launch_control_exclusions.rb +3 -17
- data/lib/ace/assign/authority/ordinary_attempt_start.rb +137 -0
- data/lib/ace/assign/authority/ordinary_client.rb +70 -0
- data/lib/ace/assign/authority/ordinary_completion.rb +104 -0
- data/lib/ace/assign/authority/ordinary_effect_settlement.rb +118 -0
- data/lib/ace/assign/authority/ordinary_lifecycle.rb +227 -0
- data/lib/ace/assign/authority/ordinary_merge_evidence.rb +81 -0
- data/lib/ace/assign/authority/ordinary_peer.rb +54 -0
- data/lib/ace/assign/authority/ordinary_progress.rb +162 -0
- data/lib/ace/assign/authority/ordinary_queue.rb +115 -0
- data/lib/ace/assign/authority/ordinary_release_artifacts.rb +143 -0
- data/lib/ace/assign/authority/ordinary_results.rb +206 -0
- data/lib/ace/assign/authority/ordinary_review_evidence.rb +180 -0
- data/lib/ace/assign/authority/ordinary_reviews.rb +214 -0
- data/lib/ace/assign/authority/ordinary_server.rb +75 -0
- data/lib/ace/assign/authority/ordinary_supervisor.rb +99 -0
- data/lib/ace/assign/authority/prepared_work.rb +9 -4
- data/lib/ace/assign/authority/protected_assignment_context.rb +3 -3
- data/lib/ace/assign/authority/release_artifact_manifest.rb +91 -0
- data/lib/ace/assign/authority/retained_input.rb +53 -0
- data/lib/ace/assign/authority/router.rb +11 -6
- data/lib/ace/assign/authority/server.rb +11 -180
- data/lib/ace/assign/authority/socket_server.rb +255 -0
- data/lib/ace/assign/authority/transfer_codec.rb +5 -3
- data/lib/ace/assign/cli/commands/attempt/evidence.rb +2 -2
- data/lib/ace/assign/cli/commands/submit_candidate.rb +1 -1
- data/lib/ace/assign/molecules/canonical_evidence.rb +25 -8
- data/lib/ace/assign/molecules/canonical_read_commands.rb +84 -0
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +2 -63
- data/lib/ace/assign/molecules/evidence_journal.rb +157 -63
- data/lib/ace/assign/molecules/installed_journal_read.rb +143 -0
- data/lib/ace/assign/molecules/journal_mutation.rb +7 -4
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +13 -2
- data/lib/ace/assign/molecules/proposal_journal.rb +32 -4
- data/lib/ace/assign/molecules/receipt_verifier.rb +8 -2
- data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +2 -1
- data/lib/ace/assign/organisms/attempt_coordinator.rb +88 -36
- data/lib/ace/assign/organisms/ordinary_driver.rb +310 -0
- data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +5 -5
- data/lib/ace/assign/version.rb +1 -1
- metadata +39 -10
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "receipt_transfer"
|
|
4
|
+
require_relative "../molecules/canonical_evidence"
|
|
5
|
+
|
|
6
|
+
module Ace
|
|
7
|
+
module Assign
|
|
8
|
+
module Authority
|
|
9
|
+
# A worker submits an attributable result. This never accepts a terminal
|
|
10
|
+
# outcome, an independent review, or an external delivery effect.
|
|
11
|
+
module OrdinaryResults
|
|
12
|
+
RESULT_OPERATIONS = %w[submit_result result_status result_snapshot].freeze
|
|
13
|
+
RESULT_TRANSFERS = {
|
|
14
|
+
"submit_result" => {direction: :upload, purpose: :receipt_artifacts, roles: %i[worker supervisor]},
|
|
15
|
+
"result_snapshot" => {direction: :download, purpose: :receipt_artifacts, roles: %i[worker supervisor]}
|
|
16
|
+
}.freeze
|
|
17
|
+
RESULT_KEYS = %w[assignment_id attempt_id expected_generation pane head receipt_sha256 transfer].freeze
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def validate_result!(request)
|
|
22
|
+
params = request.fetch("params")
|
|
23
|
+
if request.fetch("operation") != "submit_result"
|
|
24
|
+
snapshot = request.fetch("operation") == "result_snapshot"
|
|
25
|
+
strict!(params, %w[assignment_id attempt_id] + (snapshot ? ["result_id"] : []))
|
|
26
|
+
token!(params.fetch("result_id")) if snapshot
|
|
27
|
+
raise ArgumentError, "ordinary result status mutation must be null" unless request.fetch("mutation_id").nil?
|
|
28
|
+
else
|
|
29
|
+
strict!(params, RESULT_KEYS)
|
|
30
|
+
token!(request.fetch("mutation_id"))
|
|
31
|
+
unless params.fetch("expected_generation").is_a?(Integer) && params.fetch("expected_generation").positive? &&
|
|
32
|
+
params.fetch("head").is_a?(String) && params.fetch("head").match?(/\A[0-9a-f]{40}\z/) &&
|
|
33
|
+
params.fetch("receipt_sha256").is_a?(String) && params.fetch("receipt_sha256").match?(/\A[0-9a-f]{64}\z/)
|
|
34
|
+
raise ArgumentError, "ordinary result selection is invalid"
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
%w[assignment_id attempt_id].each { |key| token!(params.fetch(key)) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def ordinary_result(request, peer, role, deadline, transfer)
|
|
41
|
+
validate_result!(request)
|
|
42
|
+
params = request.fetch("params")
|
|
43
|
+
commit = @journal.ref_value
|
|
44
|
+
registered = registration(params.fetch("assignment_id"), commit: commit)
|
|
45
|
+
raise AttemptErrors::NotFound, "ordinary work is not registered" unless registered
|
|
46
|
+
if request.fetch("operation") != "submit_result"
|
|
47
|
+
if request.fetch("operation") == "result_status" && @journal.read_events(params.fetch("assignment_id"), commit: commit)
|
|
48
|
+
.none? { |event| event.fetch("attempt_id") == params.fetch("attempt_id") }
|
|
49
|
+
authorize!(request, peer, role, deadline)
|
|
50
|
+
return {data: params.slice("assignment_id", "attempt_id").merge("project_id" => @project,
|
|
51
|
+
"state" => "not_started", "generation" => 0, "submitted_result" => nil), replayed: false}
|
|
52
|
+
end
|
|
53
|
+
events = result_events(params, commit)
|
|
54
|
+
result = retained_ordinary_result(params, events, commit)
|
|
55
|
+
if request.fetch("operation") == "result_snapshot"
|
|
56
|
+
unless result && result.fetch("result_id") == params.fetch("result_id")
|
|
57
|
+
raise AttemptErrors::NotFound, "ordinary exact result is unavailable"
|
|
58
|
+
end
|
|
59
|
+
receipt = result.fetch("receipt")
|
|
60
|
+
canonical = Molecules::CanonicalEvidence.new(journal: @journal)
|
|
61
|
+
parts = [JSON.generate(receipt)] + receipt.fetch("artifacts").map { |artifact|
|
|
62
|
+
canonical.read({"ref" => artifact.fetch("path"), "sha256" => artifact.fetch("sha256")},
|
|
63
|
+
**ordinary_result_context(result.fetch("binding")), commit: commit)
|
|
64
|
+
}
|
|
65
|
+
authorize!(request, peer, role, deadline)
|
|
66
|
+
return {data: ordinary_result_projection(result).merge("journal_commit" => commit), transfer_parts: parts, replayed: false}
|
|
67
|
+
end
|
|
68
|
+
authorize!(request, peer, role, deadline)
|
|
69
|
+
started = events.find { |event| event["type"] == "process_start" }.fetch("payload")
|
|
70
|
+
scope = events.find { |event| event["type"] == "intent" }.fetch("payload").fetch("scope")
|
|
71
|
+
return {data: {"project_id" => @project, "assignment_id" => params.fetch("assignment_id"), "attempt_id" => params.fetch("attempt_id"),
|
|
72
|
+
"scope" => scope, "producer" => started.slice("actor", "role", "runtime"),
|
|
73
|
+
"runtime_binding" => started.fetch("runtime_binding"),
|
|
74
|
+
"journal_commit" => commit, "generation" => @journal.authority_generation(events),
|
|
75
|
+
"state" => Molecules::CanonicalAttemptState.derive(events), "submitted_result" => result && ordinary_result_projection(result)}, replayed: false}
|
|
76
|
+
end
|
|
77
|
+
unless %i[worker supervisor].include?(role)
|
|
78
|
+
raise AttemptErrors::UnauthorizedIdentity, "ordinary result requires its original worker"
|
|
79
|
+
end
|
|
80
|
+
authorize_start_replay!(request, peer, role, deadline)
|
|
81
|
+
admitted = ReceiptTransfer.decode(input: transfer, receipt_sha256: params.fetch("receipt_sha256"),
|
|
82
|
+
artifact_field: "artifacts", reference_key: "path")
|
|
83
|
+
digest = Atoms::EvidenceDigest.digest({"params" => params, "project_id" => @project, "peer_uid" => peer.fetch("uid")})
|
|
84
|
+
keys = [@exclusion.task_key(registered.fetch("task_id")), @exclusion.assignment_key(params.fetch("assignment_id"))]
|
|
85
|
+
@exclusion.with_shared_multi(keys, deadline: deadline) do
|
|
86
|
+
@journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
87
|
+
mutation_id: request.fetch("mutation_id"), operation: "submit_result", parameters_digest: digest,
|
|
88
|
+
expected_generation: params.fetch("expected_generation"), with_replay: true, deadline: deadline,
|
|
89
|
+
replay_authorizer: -> { authorize_start_replay!(request, peer, role, deadline) }) do |events, prefix, generation|
|
|
90
|
+
authorize_start_replay!(request, peer, role, deadline)
|
|
91
|
+
unless Molecules::CanonicalAttemptState.derive(events) == "running"
|
|
92
|
+
raise AttemptErrors::Conflict, "ordinary result requires the original running attempt"
|
|
93
|
+
end
|
|
94
|
+
raise AttemptErrors::Conflict, "ordinary attempt already submitted its result" if retained_ordinary_result(params, events, prefix)
|
|
95
|
+
observed = @admission.execution!(peer: peer, pane: params.fetch("pane"), head: params.fetch("head"), deadline: deadline)
|
|
96
|
+
start_replay_owner!(params, observed.fetch(:identity))
|
|
97
|
+
ordinary_result_plan(params, peer, admitted, observed, events, prefix, generation + 1)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def result_events(params, commit)
|
|
103
|
+
events = @journal.read_events(params.fetch("assignment_id"), commit: commit)
|
|
104
|
+
.select { |event| event.fetch("attempt_id") == params.fetch("attempt_id") }
|
|
105
|
+
unless Models::EvidenceEvent.chain_valid?(events) && events.one? { |event| event["type"] == "process_start" }
|
|
106
|
+
raise AttemptErrors::EvidenceUnavailable, "ordinary result attempt evidence is unavailable"
|
|
107
|
+
end
|
|
108
|
+
events
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def ordinary_result_plan(params, peer, admitted, observed, events, commit, generation)
|
|
112
|
+
original = admitted.fetch(:receipt)
|
|
113
|
+
original_receipt = Models::ExecutionReceipt.from_h(original)
|
|
114
|
+
unless original_receipt.digest == Atoms::EvidenceDigest.digest(original_receipt.digest_payload)
|
|
115
|
+
raise AttemptErrors::ReceiptRejected, "ordinary original result digest differs"
|
|
116
|
+
end
|
|
117
|
+
identity = observed.fetch(:identity)
|
|
118
|
+
expected_producer = {"actor" => identity.actor, "role" => identity.role, "runtime" => identity.runtime}
|
|
119
|
+
validate_ordinary_result_receipt!(original, expected_producer)
|
|
120
|
+
id = SecureRandom.hex(16)
|
|
121
|
+
binding = params.slice("assignment_id", "attempt_id", "head").merge("authority" => "ordinary", "project_id" => @project,
|
|
122
|
+
"result_id" => id, "worker_uid" => peer.fetch("uid"), "producer" => expected_producer,
|
|
123
|
+
"runtime_binding" => identity.runtime_binding, "checkout" => observed.fetch(:checkout),
|
|
124
|
+
"generation" => generation, "uploaded_receipt_sha256" => admitted.fetch(:receipt_sha256),
|
|
125
|
+
"original_receipt_digest" => original_receipt.digest)
|
|
126
|
+
canonical = Molecules::CanonicalEvidence.new(journal: @journal)
|
|
127
|
+
plan = admitted.fetch(:artifacts).empty? ? {events: [], blobs: {}, references: []} :
|
|
128
|
+
canonical.import_plan(**ordinary_result_context(binding), artifacts: admitted.fetch(:artifacts),
|
|
129
|
+
admitted_after_event_digest: events.last.fetch("digest"))
|
|
130
|
+
receipt = original.except("digest", "recorded_at").merge("artifacts" => plan.fetch(:references).map { |reference|
|
|
131
|
+
{"path" => reference.fetch("ref"), "sha256" => reference.fetch("sha256")}
|
|
132
|
+
})
|
|
133
|
+
pending = @journal.send(:chain_mutation_events, params.fetch("attempt_id"), events.last.fetch("digest"), plan.fetch(:events))
|
|
134
|
+
reader = ->(_data, artifact) { canonical.read_pending({"ref" => artifact.fetch("path"), "sha256" => artifact.fetch("sha256")},
|
|
135
|
+
**ordinary_result_context(binding), current_events: events, pending_events: pending, blobs: plan.fetch(:blobs), commit: commit) }
|
|
136
|
+
verified = verify_ordinary_result_receipt(params, receipt, binding, commit, reader)
|
|
137
|
+
if JSON.generate(verified.to_h).bytesize > 16_384
|
|
138
|
+
raise AttemptErrors::ReceiptRejected, "canonical ordinary receipt exceeds its transfer limit"
|
|
139
|
+
end
|
|
140
|
+
payload = {"authority" => "ordinary", "result_id" => id, "binding" => binding, "receipt" => verified.to_h}
|
|
141
|
+
plan.merge(events: plan.fetch(:events) + [{type: "result_submitted", payload: payload}],
|
|
142
|
+
data: ordinary_result_projection(payload))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def ordinary_result_projection(payload)
|
|
146
|
+
payload.slice("result_id").merge(payload.fetch("binding").slice("head", "generation"),
|
|
147
|
+
"receipt_digest" => payload.fetch("receipt").fetch("digest"), "verdict" => payload.fetch("receipt").fetch("verdict"),
|
|
148
|
+
"state" => "running", "authority" => "ordinary")
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def ordinary_result_context(binding)
|
|
152
|
+
{kind: "result", project_id: @project, assignment_id: binding.fetch("assignment_id"), attempt_id: binding.fetch("attempt_id"),
|
|
153
|
+
peer_uid: binding.fetch("worker_uid"), binding: binding, request_id_or_event_id: binding.fetch("result_id"), generation: binding.fetch("generation")}
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def validate_ordinary_result_receipt!(receipt, producer)
|
|
157
|
+
unless receipt["producer"] == producer && (receipt.keys - Models::ExecutionReceipt.from_h(receipt).to_h.keys).empty? &&
|
|
158
|
+
!receipt["campaign"] && !receipt["review"] && receipt["operation"] != "review" &&
|
|
159
|
+
!Molecules::ReceiptVerifier::EXTERNAL_EFFECT_OPERATIONS.include?(receipt["operation"])
|
|
160
|
+
raise AttemptErrors::ReceiptRejected, "ordinary result producer or operation requires another evidence owner"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def verify_ordinary_result_receipt(params, receipt, binding, commit, reader)
|
|
165
|
+
validate_ordinary_result_receipt!(receipt, binding.fetch("producer"))
|
|
166
|
+
attempt = @journal.derived_attempts(params.fetch("assignment_id"), commit: commit).find { |item| item.attempt_id == params.fetch("attempt_id") }
|
|
167
|
+
raise AttemptErrors::EvidenceUnavailable, "ordinary result attempt is unavailable" unless attempt
|
|
168
|
+
Array(receipt["artifacts"]).each { |artifact| reader.call(receipt, artifact) }
|
|
169
|
+
Molecules::ReceiptVerifier.new(artifact_reader: reader).verify_result!(receipt,
|
|
170
|
+
attempt: attempt, live_head: binding.fetch("head"), repo_root: @journal.repo_root)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def retained_ordinary_result(params, events, commit)
|
|
174
|
+
raise AttemptErrors::EvidenceUnavailable, "ordinary result chain is corrupt" unless Models::EvidenceEvent.chain_valid?(events)
|
|
175
|
+
records = events.select { |event| event["type"] == "result_submitted" }
|
|
176
|
+
replies = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "submit_result" }
|
|
177
|
+
return nil if records.empty? && replies.empty?
|
|
178
|
+
unless records.one? && replies.one?
|
|
179
|
+
raise AttemptErrors::EvidenceUnavailable, "ordinary result provenance is missing or duplicated"
|
|
180
|
+
end
|
|
181
|
+
payload = records.first.fetch("payload")
|
|
182
|
+
binding = payload.fetch("binding")
|
|
183
|
+
start = events.find { |event| event["type"] == "process_start" }.fetch("payload")
|
|
184
|
+
unless payload.keys.sort == %w[authority binding receipt result_id] && payload["authority"] == "ordinary" &&
|
|
185
|
+
payload["result_id"].is_a?(String) && payload["result_id"].match?(/\A[0-9a-f]{32}\z/) &&
|
|
186
|
+
binding.keys.sort == %w[assignment_id attempt_id authority checkout generation head original_receipt_digest producer project_id result_id runtime_binding uploaded_receipt_sha256 worker_uid] &&
|
|
187
|
+
binding["authority"] == "ordinary" && binding["result_id"] == payload["result_id"] &&
|
|
188
|
+
binding["head"] == binding.dig("checkout", "head") &&
|
|
189
|
+
binding.values_at("project_id", "assignment_id", "attempt_id", "runtime_binding") ==
|
|
190
|
+
[@project, params.fetch("assignment_id"), params.fetch("attempt_id"), start.fetch("runtime_binding")] &&
|
|
191
|
+
binding.fetch("producer") == start.slice("actor", "role", "runtime") &&
|
|
192
|
+
binding.fetch("worker_uid") == start.dig("runtime_binding", "process_identity", "uid") &&
|
|
193
|
+
payload.fetch("receipt").fetch("producer") == binding.fetch("producer") &&
|
|
194
|
+
replies.first.dig("payload", "data").slice(*ordinary_result_projection(payload).keys) == ordinary_result_projection(payload)
|
|
195
|
+
raise AttemptErrors::EvidenceUnavailable, "ordinary result owner differs"
|
|
196
|
+
end
|
|
197
|
+
canonical = Molecules::CanonicalEvidence.new(journal: @journal)
|
|
198
|
+
reader = ->(_data, artifact) { canonical.read({"ref" => artifact.fetch("path"), "sha256" => artifact.fetch("sha256")},
|
|
199
|
+
**ordinary_result_context(binding), commit: commit) }
|
|
200
|
+
verify_ordinary_result_receipt(params, payload.fetch("receipt"), binding, commit, reader)
|
|
201
|
+
payload
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "candidate_transfer"
|
|
4
|
+
require_relative "receipt_transfer"
|
|
5
|
+
require_relative "../molecules/canonical_evidence"
|
|
6
|
+
|
|
7
|
+
module Ace
|
|
8
|
+
module Assign
|
|
9
|
+
module Authority
|
|
10
|
+
# Source-owned views of the same ordinary journal used by lifecycle CAS.
|
|
11
|
+
# Caller labels, filesystem reports and protected launch tickets are not
|
|
12
|
+
# alternate evidence sources. Service and merge consumers share this owner.
|
|
13
|
+
module OrdinaryReviewEvidence
|
|
14
|
+
def with_scope(assignment:, attempt:, deadline:)
|
|
15
|
+
token!(assignment)
|
|
16
|
+
token!(attempt)
|
|
17
|
+
selected = registration(assignment, commit: @journal.ref_value)
|
|
18
|
+
raise AttemptErrors::NotFound, "ordinary assignment is not registered" unless selected
|
|
19
|
+
@exclusion.with_shared_multi([@exclusion.task_key(selected.fetch("task_id")),
|
|
20
|
+
@exclusion.assignment_key(assignment)], deadline: deadline) do
|
|
21
|
+
yield @journal, selected
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def origin_at!(events:, commit:)
|
|
26
|
+
ordinary_origin_at!(events, commit, running: true)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def candidate_at!(events:, commit:, head:, generation:)
|
|
30
|
+
origin_at!(events: events, commit: commit)
|
|
31
|
+
selected = ordinary_candidate_event(events)&.dig("payload", "data")
|
|
32
|
+
unless selected && selected.values_at("head", "candidate_generation") == [head, generation]
|
|
33
|
+
raise AttemptErrors::Conflict, "ordinary candidate changed"
|
|
34
|
+
end
|
|
35
|
+
bytes = @journal.bounded_blob(selected.fetch("bundle_ref"), commit: commit, max_bytes: CandidateTransfer::MAX_BYTES)
|
|
36
|
+
unless bytes.bytesize == selected.fetch("bytes") && Digest::SHA256.hexdigest(bytes) == selected.fetch("sha256")
|
|
37
|
+
raise AttemptErrors::EvidenceUnavailable, "ordinary canonical candidate is corrupt"
|
|
38
|
+
end
|
|
39
|
+
selected.slice("head", "tree", "candidate_generation", "bundle_ref", "sha256", "bytes")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def candidate_reference_at!(events:, commit:, head:, generation:)
|
|
43
|
+
candidate_at!(events: events, commit: commit, head: head, generation: generation)
|
|
44
|
+
origin = origin_at!(events: events, commit: commit)
|
|
45
|
+
origin.slice("assignment_id", "attempt_id").merge("candidate_generation" => generation,
|
|
46
|
+
"acceptance_event_digest" => ordinary_candidate_event(events).fetch("digest"))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def review_at!(events:, commit:, head:, generation:)
|
|
50
|
+
current = candidate_at!(events: events, commit: commit, head: head, generation: generation)
|
|
51
|
+
assigned = ordinary_review_assignment!(events, current)
|
|
52
|
+
ordinary_review_origins!(events, commit, assigned)
|
|
53
|
+
accepted = events.reverse.find { |event| event["type"] == "authority_mutation" &&
|
|
54
|
+
event.dig("payload", "operation") == "accept_review" && event.dig("payload", "data", "review_id") == assigned.fetch("review_id") }
|
|
55
|
+
data = accepted && accepted.dig("payload", "data")
|
|
56
|
+
unless data && data.fetch("review_binding") == assigned.except("generation", "journal_commit")
|
|
57
|
+
raise AttemptErrors::ReceiptRejected, "ordinary candidate has no accepted independent review"
|
|
58
|
+
end
|
|
59
|
+
canonical = Molecules::CanonicalEvidence.new(journal: @journal)
|
|
60
|
+
reader = ->(_receipt, artifact) { canonical.read({"ref" => artifact.fetch("path"), "sha256" => artifact.fetch("sha256")},
|
|
61
|
+
**ordinary_review_context(data.fetch("review_binding")), commit: commit) }
|
|
62
|
+
receipt = data.fetch("review_receipt")
|
|
63
|
+
verified = ordinary_verify_review!(receipt, events, commit, assigned, reader)
|
|
64
|
+
original = canonical.read({"ref" => data.fetch("original_receipt_ref"), "sha256" => data.fetch("uploaded_receipt_sha256")},
|
|
65
|
+
**ordinary_review_context(data.fetch("review_binding")), commit: commit)
|
|
66
|
+
unless Digest::SHA256.hexdigest(original) == data.fetch("uploaded_receipt_sha256") &&
|
|
67
|
+
verified.digest == data.fetch("receipt_digest")
|
|
68
|
+
raise AttemptErrors::EvidenceUnavailable, "ordinary accepted review bytes differ"
|
|
69
|
+
end
|
|
70
|
+
original_data = JSON.parse(original)
|
|
71
|
+
original_receipt = Models::ExecutionReceipt.from_h(original_data)
|
|
72
|
+
unless original_receipt.digest == Atoms::EvidenceDigest.digest(original_receipt.digest_payload) &&
|
|
73
|
+
original_data.except("digest", "recorded_at", "artifacts") == receipt.except("digest", "recorded_at", "artifacts") &&
|
|
74
|
+
original_data.fetch("artifacts").map { |value| value.fetch("sha256") } == receipt.fetch("artifacts").map { |value| value.fetch("sha256") }
|
|
75
|
+
raise AttemptErrors::EvidenceUnavailable, "ordinary review original and canonical receipt differ"
|
|
76
|
+
end
|
|
77
|
+
data.merge("acceptance_event_digest" => accepted.fetch("digest"), "recorded_at" => accepted.fetch("recorded_at"))
|
|
78
|
+
rescue JSON::ParserError, KeyError, TypeError
|
|
79
|
+
raise AttemptErrors::EvidenceUnavailable, "ordinary accepted review is incomplete"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def ordinary_origin_at!(events, commit, running:)
|
|
85
|
+
raise AttemptErrors::Conflict, "ordinary evidence prefix changed" unless commit && @journal.ref_value == commit
|
|
86
|
+
intent = events.select { |event| event["type"] == "intent" }
|
|
87
|
+
starts = events.select { |event| event["type"] == "process_start" }
|
|
88
|
+
accepted = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "start_attempt" }
|
|
89
|
+
unless Models::EvidenceEvent.chain_valid?(events) && intent.one? && starts.one? && accepted.one?
|
|
90
|
+
raise AttemptErrors::EvidenceUnavailable, "ordinary native origin is unavailable"
|
|
91
|
+
end
|
|
92
|
+
assignment, attempt = intent.first.dig("payload", "assignment_id"), intent.first.fetch("attempt_id")
|
|
93
|
+
token!(assignment)
|
|
94
|
+
token!(attempt)
|
|
95
|
+
registered = registration(assignment, commit: commit)
|
|
96
|
+
retained = @journal.read_events(assignment, commit: commit).select { |event| event["attempt_id"] == attempt }
|
|
97
|
+
start, data = starts.first.fetch("payload"), accepted.first.fetch("payload").fetch("data")
|
|
98
|
+
unless registered && retained == events && data.values_at("authority", "project_id", "assignment_id", "attempt_id", "runtime_binding") ==
|
|
99
|
+
["ordinary", @project, assignment, attempt, start.fetch("runtime_binding")] &&
|
|
100
|
+
intent.first.dig("payload", "project_id") == @project
|
|
101
|
+
raise AttemptErrors::EvidenceUnavailable, "ordinary native origin differs from accepted start"
|
|
102
|
+
end
|
|
103
|
+
if running && Molecules::CanonicalAttemptState.derive(events) != "running"
|
|
104
|
+
raise AttemptErrors::Conflict, "ordinary delivery requires its original running attempt"
|
|
105
|
+
end
|
|
106
|
+
{"authority" => "ordinary", "project_id" => @project, "assignment_id" => assignment, "attempt_id" => attempt,
|
|
107
|
+
"intent_digest" => intent.first.fetch("digest"), "process_start_digest" => starts.first.fetch("digest"),
|
|
108
|
+
"runtime_binding" => start.fetch("runtime_binding"), "scope" => intent.first.dig("payload", "scope"),
|
|
109
|
+
"base_head" => intent.first.dig("payload", "base_head")}
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def ordinary_candidate_event(events)
|
|
113
|
+
events.reverse.find { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "submit_candidate" }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def ordinary_review_assignment!(events, candidate)
|
|
117
|
+
selected = events.reverse.find { |event| event["type"] == "authority_mutation" &&
|
|
118
|
+
event.dig("payload", "operation") == "assign_review" &&
|
|
119
|
+
event.dig("payload", "data", "candidate_generation") == candidate.fetch("candidate_generation") }
|
|
120
|
+
data = selected && selected.dig("payload", "data")
|
|
121
|
+
cancelled = selected && events.any? { |event| event["type"] == "authority_mutation" &&
|
|
122
|
+
event.dig("payload", "operation") == "cancel_review" && event.dig("payload", "data", "review_id") == data.fetch("review_id") }
|
|
123
|
+
unless data && data.fetch("head") == candidate.fetch("head") && !cancelled
|
|
124
|
+
raise AttemptErrors::ReceiptRejected, "ordinary independent review assignment is absent or revoked"
|
|
125
|
+
end
|
|
126
|
+
data
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def ordinary_review_origins!(events, commit, assigned, reviewer_running: false)
|
|
130
|
+
producer = origin_at!(events: events, commit: commit)
|
|
131
|
+
reviewer_events = @journal.read_events(assigned.fetch("reviewer_assignment_id"), commit: commit)
|
|
132
|
+
.select { |event| event["attempt_id"] == assigned.fetch("reviewer_attempt_id") }
|
|
133
|
+
reviewer = ordinary_origin_at!(reviewer_events, commit, running: reviewer_running)
|
|
134
|
+
unless producer == assigned.fetch("producer_origin") && reviewer == assigned.fetch("reviewer_origin")
|
|
135
|
+
raise AttemptErrors::EvidenceUnavailable, "ordinary review origins differ"
|
|
136
|
+
end
|
|
137
|
+
ordinary_independent_agents!(producer.fetch("runtime_binding"), reviewer.fetch("runtime_binding"))
|
|
138
|
+
[producer, reviewer]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def ordinary_independent_agents!(producer, reviewer)
|
|
142
|
+
sessions = [producer, reviewer].map { |binding|
|
|
143
|
+
Ace::Herdr::Atoms::NativeAgentSession.normalize(binding.fetch("agent_session"), agent: binding.fetch("agent")) }
|
|
144
|
+
identities = [producer, reviewer].map { |binding|
|
|
145
|
+
identity = binding.fetch("process_identity")
|
|
146
|
+
unless identity.is_a?(Hash) && %w[pid uid started_at host].all? { |key| identity.key?(key) }
|
|
147
|
+
raise AttemptErrors::EvidenceUnavailable, "ordinary agent process generation is missing"
|
|
148
|
+
end
|
|
149
|
+
identity.values_at("pid", "uid", "started_at", "host") }
|
|
150
|
+
unless sessions.all? && sessions[0].fetch("value").downcase != sessions[1].fetch("value").downcase && identities[0] != identities[1] && producer.fetch("pane") != reviewer.fetch("pane")
|
|
151
|
+
raise AttemptErrors::ReceiptRejected, "review requires an independently started native agent and session"
|
|
152
|
+
end
|
|
153
|
+
true
|
|
154
|
+
rescue KeyError
|
|
155
|
+
raise AttemptErrors::EvidenceUnavailable, "ordinary agent identity is incomplete"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def ordinary_review_context(binding)
|
|
159
|
+
{kind: "review", project_id: @project, assignment_id: binding.fetch("producer_origin").fetch("assignment_id"),
|
|
160
|
+
attempt_id: binding.fetch("producer_origin").fetch("attempt_id"), peer_uid: binding.fetch("reviewer_uid"),
|
|
161
|
+
binding: binding, request_id_or_event_id: binding.fetch("review_id"), generation: binding.fetch("candidate_generation")}
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def ordinary_verify_review!(receipt, events, commit, assigned, reader)
|
|
165
|
+
origin = origin_at!(events: events, commit: commit)
|
|
166
|
+
producer = events.find { |event| event["type"] == "process_start" }.fetch("payload").slice("actor", "role", "runtime")
|
|
167
|
+
unless receipt["operation"] == "review" && receipt["verdict"] == "succeeded" && !receipt["campaign"] &&
|
|
168
|
+
receipt["producer"] == producer && receipt.dig("review", "reviewer", "actor") == assigned.fetch("reviewer_actor")
|
|
169
|
+
raise AttemptErrors::ReceiptRejected, "ordinary review receipt attribution differs"
|
|
170
|
+
end
|
|
171
|
+
attempt = @journal.derived_attempts(origin.fetch("assignment_id"), commit: commit)
|
|
172
|
+
.find { |value| value.attempt_id == origin.fetch("attempt_id") }
|
|
173
|
+
independence = ->(_data) { ordinary_review_origins!(events, commit, assigned); true }
|
|
174
|
+
Molecules::ReceiptVerifier.new(artifact_reader: reader, review_independence_verifier: independence)
|
|
175
|
+
.verify_result!(receipt, attempt: attempt, live_head: assigned.fetch("head"), repo_root: @journal.repo_root)
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ordinary_review_evidence"
|
|
4
|
+
|
|
5
|
+
module Ace
|
|
6
|
+
module Assign
|
|
7
|
+
module Authority
|
|
8
|
+
module OrdinaryReviews
|
|
9
|
+
include OrdinaryReviewEvidence
|
|
10
|
+
REVIEW_OPERATIONS = %w[submit_candidate assign_review cancel_review export_candidate accept_review review_status].freeze
|
|
11
|
+
REVIEW_TRANSFERS = {
|
|
12
|
+
"submit_candidate" => {direction: :upload, purpose: :candidate, roles: %i[worker supervisor]},
|
|
13
|
+
"export_candidate" => {direction: :download, purpose: :candidate, roles: %i[worker supervisor]},
|
|
14
|
+
"accept_review" => {direction: :upload, purpose: :receipt_artifacts, roles: %i[worker supervisor]}
|
|
15
|
+
}.freeze
|
|
16
|
+
REVIEW_COMMON = %w[assignment_id attempt_id head candidate_generation pane].freeze
|
|
17
|
+
REVIEW_PARAMETERS = {
|
|
18
|
+
"submit_candidate" => REVIEW_COMMON + %w[expected_generation transfer],
|
|
19
|
+
"assign_review" => REVIEW_COMMON + %w[expected_generation reviewer_assignment_id reviewer_attempt_id],
|
|
20
|
+
"cancel_review" => REVIEW_COMMON + %w[expected_generation review_id],
|
|
21
|
+
"export_candidate" => REVIEW_COMMON + %w[review_id],
|
|
22
|
+
"accept_review" => REVIEW_COMMON + %w[expected_generation review_id receipt_sha256 transfer],
|
|
23
|
+
"review_status" => %w[assignment_id attempt_id head candidate_generation]
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def validate_ordinary_review!(request)
|
|
29
|
+
operation, params = request.values_at("operation", "params")
|
|
30
|
+
strict!(params, REVIEW_PARAMETERS.fetch(operation))
|
|
31
|
+
%w[assignment_id attempt_id reviewer_assignment_id reviewer_attempt_id review_id].each { |key| token!(params[key]) if params.key?(key) }
|
|
32
|
+
unless params["head"].is_a?(String) && CandidateTransfer::SHA.match?(params["head"]) &&
|
|
33
|
+
params["candidate_generation"].is_a?(Integer) && params["candidate_generation"] >= 0
|
|
34
|
+
raise ArgumentError, "ordinary candidate selection is invalid"
|
|
35
|
+
end
|
|
36
|
+
if %w[export_candidate review_status].include?(operation)
|
|
37
|
+
raise ArgumentError, "ordinary review read mutation must be null" unless request["mutation_id"].nil?
|
|
38
|
+
else
|
|
39
|
+
token!(request["mutation_id"])
|
|
40
|
+
unless params["expected_generation"].is_a?(Integer) && params["expected_generation"].positive?
|
|
41
|
+
raise ArgumentError, "ordinary review generation is invalid"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def authorize_ordinary_review!(request, peer, role, deadline, events: nil, commit: nil)
|
|
47
|
+
authorize!(request, peer, role, deadline)
|
|
48
|
+
validate_ordinary_review!(request)
|
|
49
|
+
params = request.fetch("params")
|
|
50
|
+
commit ||= @journal.ref_value
|
|
51
|
+
events ||= result_events(params, commit)
|
|
52
|
+
origin = origin_at!(events: events, commit: commit)
|
|
53
|
+
operation = request.fetch("operation")
|
|
54
|
+
return origin if operation == "review_status"
|
|
55
|
+
identity = @admission.identity!(peer: peer, pane: params.fetch("pane"), deadline: deadline)
|
|
56
|
+
case operation
|
|
57
|
+
when "submit_candidate"
|
|
58
|
+
unless identity.runtime_binding == origin.fetch("runtime_binding")
|
|
59
|
+
raise AttemptErrors::UnauthorizedIdentity, "candidate belongs to its original native producer"
|
|
60
|
+
end
|
|
61
|
+
when "assign_review", "cancel_review"
|
|
62
|
+
registered = registration(params.fetch("assignment_id"), commit: commit)
|
|
63
|
+
unless role == :supervisor && identity.role == "coordinator" && identity.runtime_binding == supervisor_selection(registered, commit).fetch("binding")
|
|
64
|
+
raise AttemptErrors::UnauthorizedIdentity, "review selection belongs to the current canonical supervisor"
|
|
65
|
+
end
|
|
66
|
+
when "export_candidate", "accept_review"
|
|
67
|
+
candidate = candidate_at!(events: events, commit: commit, head: params.fetch("head"), generation: params.fetch("candidate_generation"))
|
|
68
|
+
assigned = ordinary_review_assignment!(events, candidate)
|
|
69
|
+
_, reviewer = ordinary_review_origins!(events, commit, assigned, reviewer_running: true)
|
|
70
|
+
unless assigned.fetch("review_id") == params.fetch("review_id") && identity.runtime_binding == reviewer.fetch("runtime_binding") &&
|
|
71
|
+
identity.actor == assigned.fetch("reviewer_actor") && peer.fetch("uid") == assigned.fetch("reviewer_uid")
|
|
72
|
+
raise AttemptErrors::UnauthorizedIdentity, "review belongs to its assigned native agent"
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
origin
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def ordinary_review_operation(request, peer, role, deadline, transfer)
|
|
79
|
+
params, operation = request.values_at("params", "operation")
|
|
80
|
+
authorize_ordinary_review!(request, peer, role, deadline)
|
|
81
|
+
admitted = if operation == "submit_candidate"
|
|
82
|
+
raise ArgumentError, "ordinary candidate requires one bundle" unless transfer && transfer.count == 1
|
|
83
|
+
descriptor = params.fetch("transfer")
|
|
84
|
+
CandidateTransfer.new(root: @input_root).admit(bytes: transfer.bytes, sha256: descriptor.fetch("sha256"),
|
|
85
|
+
size: descriptor.fetch("bytes"), head: params.fetch("head"), deadline: deadline)
|
|
86
|
+
elsif operation == "accept_review"
|
|
87
|
+
ReceiptTransfer.decode(input: transfer, receipt_sha256: params.fetch("receipt_sha256"), artifact_field: "artifacts", reference_key: "path")
|
|
88
|
+
elsif transfer
|
|
89
|
+
raise ArgumentError, "ordinary review operation forbids upload"
|
|
90
|
+
end
|
|
91
|
+
with_scope(assignment: params.fetch("assignment_id"), attempt: params.fetch("attempt_id"), deadline: deadline) do
|
|
92
|
+
commit = @journal.ref_value
|
|
93
|
+
events = result_events(params, commit)
|
|
94
|
+
authorize_ordinary_review!(request, peer, role, deadline, events: events, commit: commit)
|
|
95
|
+
if %w[export_candidate review_status].include?(operation)
|
|
96
|
+
current = candidate_at!(events: events, commit: commit, head: params.fetch("head"), generation: params.fetch("candidate_generation"))
|
|
97
|
+
assigned = ordinary_review_assignment!(events, current)
|
|
98
|
+
if operation == "export_candidate"
|
|
99
|
+
bytes = @journal.bounded_blob(current.fetch("bundle_ref"), commit: commit, max_bytes: CandidateTransfer::MAX_BYTES)
|
|
100
|
+
return {data: current.merge("review_id" => assigned.fetch("review_id")), transfer_parts: [bytes], replayed: false}
|
|
101
|
+
end
|
|
102
|
+
accepted = events.any? { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "accept_review" && event.dig("payload", "data", "review_id") == assigned.fetch("review_id") }
|
|
103
|
+
value = accepted ? review_at!(events: events, commit: commit, head: params.fetch("head"), generation: params.fetch("candidate_generation")) : assigned
|
|
104
|
+
return {data: value.merge("accepted" => accepted, "journal_commit" => commit), replayed: false}
|
|
105
|
+
end
|
|
106
|
+
digest = Atoms::EvidenceDigest.digest({"params" => params, "project_id" => @project, "peer_uid" => peer.fetch("uid")})
|
|
107
|
+
replay = lambda do
|
|
108
|
+
authorize_ordinary_review!(request, peer, role, deadline)
|
|
109
|
+
if operation == "submit_candidate"
|
|
110
|
+
ordinary_candidate_replay!(request, admitted)
|
|
111
|
+
elsif operation == "accept_review"
|
|
112
|
+
prefix = @journal.ref_value
|
|
113
|
+
review_at!(events: result_events(params, prefix), commit: prefix, head: params.fetch("head"), generation: params.fetch("candidate_generation"))
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
@journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
|
|
117
|
+
mutation_id: request.fetch("mutation_id"), operation: operation, parameters_digest: digest,
|
|
118
|
+
expected_generation: params.fetch("expected_generation"), with_replay: true, deadline: deadline,
|
|
119
|
+
replay_authorizer: replay) do |current_events, prefix, _generation|
|
|
120
|
+
authorize_ordinary_review!(request, peer, role, deadline, events: current_events, commit: prefix)
|
|
121
|
+
ordinary_review_plan(operation, params, peer, deadline, current_events, prefix, admitted, transfer)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def ordinary_review_plan(operation, params, peer, deadline, events, commit, admitted, transfer)
|
|
127
|
+
if operation == "submit_candidate"
|
|
128
|
+
current = ordinary_candidate_event(events)&.dig("payload", "data")
|
|
129
|
+
unless (current && current.fetch("candidate_generation") || 0) == params.fetch("candidate_generation")
|
|
130
|
+
raise AttemptErrors::Conflict, "ordinary candidate generation changed"
|
|
131
|
+
end
|
|
132
|
+
observed = @admission.execution!(peer: peer, pane: params.fetch("pane"), head: params.fetch("head"), deadline: deadline)
|
|
133
|
+
unless observed.fetch(:identity).runtime_binding == origin_at!(events: events, commit: commit).fetch("runtime_binding")
|
|
134
|
+
raise AttemptErrors::UnauthorizedIdentity, "ordinary candidate worker changed"
|
|
135
|
+
end
|
|
136
|
+
path = "candidates/bundles/#{SecureRandom.hex(16)}"
|
|
137
|
+
return {events: [], blobs: {path => admitted.fetch("bundle")}, data: admitted.except("bundle").merge(
|
|
138
|
+
"bundle_ref" => path, "candidate_generation" => params.fetch("candidate_generation") + 1,
|
|
139
|
+
"producer_origin" => origin_at!(events: events, commit: commit))}
|
|
140
|
+
end
|
|
141
|
+
current = candidate_at!(events: events, commit: commit, head: params.fetch("head"), generation: params.fetch("candidate_generation"))
|
|
142
|
+
if operation == "assign_review"
|
|
143
|
+
prior = events.reverse.find { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "assign_review" &&
|
|
144
|
+
event.dig("payload", "data", "candidate_generation") == current.fetch("candidate_generation") }
|
|
145
|
+
if prior && !events.any? { |event| event.dig("payload", "operation") == "cancel_review" && event.dig("payload", "data", "review_id") == prior.dig("payload", "data", "review_id") }
|
|
146
|
+
raise AttemptErrors::Conflict, "ordinary review is already assigned"
|
|
147
|
+
end
|
|
148
|
+
producer = origin_at!(events: events, commit: commit)
|
|
149
|
+
reviewer_events = @journal.read_events(params.fetch("reviewer_assignment_id"), commit: commit)
|
|
150
|
+
.select { |event| event["attempt_id"] == params.fetch("reviewer_attempt_id") }
|
|
151
|
+
reviewer = ordinary_origin_at!(reviewer_events, commit, running: true)
|
|
152
|
+
ordinary_independent_agents!(producer.fetch("runtime_binding"), reviewer.fetch("runtime_binding"))
|
|
153
|
+
observed = @admission.recorded_binding!(binding: reviewer.fetch("runtime_binding"), deadline: deadline)
|
|
154
|
+
unless observed == reviewer.fetch("runtime_binding")
|
|
155
|
+
raise AttemptErrors::UnauthorizedIdentity, "ordinary reviewer native binding changed"
|
|
156
|
+
end
|
|
157
|
+
start = reviewer_events.find { |event| event["type"] == "process_start" }.fetch("payload")
|
|
158
|
+
return {events: [], data: current.slice("head", "candidate_generation").merge(
|
|
159
|
+
"review_id" => SecureRandom.hex(16), "reviewer_assignment_id" => params.fetch("reviewer_assignment_id"),
|
|
160
|
+
"reviewer_attempt_id" => params.fetch("reviewer_attempt_id"), "reviewer_actor" => start.fetch("actor"),
|
|
161
|
+
"reviewer_uid" => reviewer.dig("runtime_binding", "process_identity", "uid"),
|
|
162
|
+
"reviewer_runtime_binding" => reviewer.fetch("runtime_binding"), "producer_origin" => producer, "reviewer_origin" => reviewer)}
|
|
163
|
+
end
|
|
164
|
+
assigned = ordinary_review_assignment!(events, current)
|
|
165
|
+
raise AttemptErrors::Conflict, "ordinary review selection changed" unless assigned.fetch("review_id") == params.fetch("review_id")
|
|
166
|
+
return {events: [], data: current.slice("head", "candidate_generation").merge("review_id" => assigned.fetch("review_id"))} if operation == "cancel_review"
|
|
167
|
+
if events.any? { |event| event.dig("payload", "operation") == "accept_review" && event.dig("payload", "data", "review_id") == assigned.fetch("review_id") }
|
|
168
|
+
raise AttemptErrors::Conflict, "ordinary review already accepted; retain original request"
|
|
169
|
+
end
|
|
170
|
+
observed = @admission.execution!(peer: peer, pane: params.fetch("pane"), head: current.fetch("head"), deadline: deadline)
|
|
171
|
+
unless observed.fetch(:identity).runtime_binding == assigned.fetch("reviewer_runtime_binding") &&
|
|
172
|
+
observed.fetch(:identity).actor == assigned.fetch("reviewer_actor")
|
|
173
|
+
raise AttemptErrors::UnauthorizedIdentity, "ordinary reviewer changed before receipt acceptance"
|
|
174
|
+
end
|
|
175
|
+
ordinary_accept_review_plan(params, events, commit, assigned, admitted, transfer.bytes(index: 0))
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def ordinary_candidate_replay!(request, admitted)
|
|
179
|
+
retained = @journal.mutation_result(request.fetch("mutation_id"))
|
|
180
|
+
data = retained.fetch("data")
|
|
181
|
+
bytes = @journal.bounded_blob(data.fetch("bundle_ref"), commit: retained.fetch("journal_commit"), max_bytes: CandidateTransfer::MAX_BYTES)
|
|
182
|
+
unless data.values_at("head", "tree", "sha256", "bytes") == admitted.values_at("head", "tree", "sha256", "bytes") &&
|
|
183
|
+
bytes.bytesize == data.fetch("bytes") && Digest::SHA256.hexdigest(bytes) == data.fetch("sha256")
|
|
184
|
+
raise AttemptErrors::EvidenceUnavailable, "ordinary retained candidate bytes differ"
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def ordinary_accept_review_plan(params, events, commit, assigned, admitted, original_bytes)
|
|
189
|
+
original = admitted.fetch(:receipt)
|
|
190
|
+
original_model = Models::ExecutionReceipt.from_h(original)
|
|
191
|
+
unless (original.keys - original_model.to_h.keys).empty? &&
|
|
192
|
+
original_model.digest == Atoms::EvidenceDigest.digest(original_model.digest_payload)
|
|
193
|
+
raise AttemptErrors::ReceiptRejected, "ordinary uploaded review digest differs"
|
|
194
|
+
end
|
|
195
|
+
binding = assigned.except("generation", "journal_commit")
|
|
196
|
+
canonical = Molecules::CanonicalEvidence.new(journal: @journal)
|
|
197
|
+
plan = canonical.import_plan(**ordinary_review_context(binding), artifacts: [original_bytes] + admitted.fetch(:artifacts),
|
|
198
|
+
admitted_after_event_digest: events.last.fetch("digest"))
|
|
199
|
+
normalized = original.except("digest", "recorded_at").merge("artifacts" => plan.fetch(:references).drop(1).map { |ref| {"path" => ref.fetch("ref"), "sha256" => ref.fetch("sha256")} })
|
|
200
|
+
pending = @journal.send(:chain_mutation_events, params.fetch("attempt_id"), events.last.fetch("digest"), plan.fetch(:events))
|
|
201
|
+
reader = ->(_receipt, artifact) { canonical.read_pending({"ref" => artifact.fetch("path"), "sha256" => artifact.fetch("sha256")},
|
|
202
|
+
**ordinary_review_context(binding), current_events: events, pending_events: pending, blobs: plan.fetch(:blobs), commit: commit) }
|
|
203
|
+
verified = ordinary_verify_review!(normalized, events, commit, assigned, reader)
|
|
204
|
+
path = plan.fetch(:references).first.fetch("ref")
|
|
205
|
+
data = binding.slice("head", "candidate_generation", "review_id", "reviewer_uid", "reviewer_actor", "reviewer_runtime_binding", "producer_origin")
|
|
206
|
+
.merge("review_binding" => binding, "review_receipt" => verified.to_h, "receipt_digest" => verified.digest,
|
|
207
|
+
"original_receipt_ref" => path, "uploaded_receipt_sha256" => admitted.fetch(:receipt_sha256))
|
|
208
|
+
raise AttemptErrors::ReceiptRejected, "ordinary review reply is oversized" if JSON.generate(data).bytesize > 12_000
|
|
209
|
+
plan.merge(data: data)
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|