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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +2 -2
  3. data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
  4. data/.ace-defaults/assign/catalog/steps/release-minor.step.yml +4 -2
  5. data/.ace-defaults/assign/catalog/steps/release.step.yml +4 -2
  6. data/.ace-defaults/assign/catalog/steps/update-pr-desc.step.yml +1 -1
  7. data/.ace-defaults/assign/presets/fix-bug.yml +15 -3
  8. data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +16 -4
  9. data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +15 -3
  10. data/.ace-defaults/assign/presets/work-on-task.yml +16 -4
  11. data/CHANGELOG.md +43 -0
  12. data/docs/demo/canonical-skill-source.tape.yml +4 -4
  13. data/docs/protected-authority.md +266 -0
  14. data/docs/usage.md +91 -2
  15. data/handbook/workflow-instructions/assign/drive.wf.md +2 -2
  16. data/handbook/workflow-instructions/assign/prepare.wf.md +2 -2
  17. data/lib/ace/assign/atoms/delivery_parameters.rb +54 -0
  18. data/lib/ace/assign/authority/candidate_transfer.rb +160 -0
  19. data/lib/ace/assign/authority/client.rb +107 -0
  20. data/lib/ace/assign/authority/deployment.rb +465 -0
  21. data/lib/ace/assign/authority/endcap.rb +342 -0
  22. data/lib/ace/assign/authority/endcap_inboxes.rb +314 -0
  23. data/lib/ace/assign/authority/endcap_results.rb +380 -0
  24. data/lib/ace/assign/authority/endcap_services.rb +395 -0
  25. data/lib/ace/assign/authority/evidence_transfer.rb +67 -0
  26. data/lib/ace/assign/authority/execution_scope_observation.rb +476 -0
  27. data/lib/ace/assign/authority/launch_driver.rb +114 -0
  28. data/lib/ace/assign/authority/launch_lifecycle.rb +822 -0
  29. data/lib/ace/assign/authority/launch_scope_abort.rb +88 -0
  30. data/lib/ace/assign/authority/launch_scope_admission.rb +84 -0
  31. data/lib/ace/assign/authority/launch_scope_close.rb +115 -0
  32. data/lib/ace/assign/authority/launch_scope_parent.rb +44 -0
  33. data/lib/ace/assign/authority/launch_scope_release.rb +180 -0
  34. data/lib/ace/assign/authority/posix_acl.rb +56 -0
  35. data/lib/ace/assign/authority/private_directory.rb +33 -0
  36. data/lib/ace/assign/authority/receipt_transfer.rb +48 -0
  37. data/lib/ace/assign/authority/router.rb +54 -0
  38. data/lib/ace/assign/authority/server.rb +230 -0
  39. data/lib/ace/assign/authority/service_evidence.rb +55 -0
  40. data/lib/ace/assign/authority/transfer_codec.rb +155 -0
  41. data/lib/ace/assign/cli/commands/authority/launch.rb +45 -0
  42. data/lib/ace/assign/cli/commands/authority/serve.rb +30 -0
  43. data/lib/ace/assign/cli/commands/authority/status.rb +26 -0
  44. data/lib/ace/assign/cli/commands/authority/terminate.rb +29 -0
  45. data/lib/ace/assign/cli/commands/create.rb +9 -1
  46. data/lib/ace/assign/cli/commands/delivery.rb +57 -0
  47. data/lib/ace/assign/cli/commands/inbox_reconcile.rb +36 -0
  48. data/lib/ace/assign/cli/commands/resume.rb +26 -0
  49. data/lib/ace/assign/cli.rb +22 -1
  50. data/lib/ace/assign/models/evidence_event.rb +3 -1
  51. data/lib/ace/assign/molecules/attempt_reconciler.rb +31 -12
  52. data/lib/ace/assign/molecules/canonical_evidence.rb +150 -0
  53. data/lib/ace/assign/molecules/evidence_journal.rb +218 -63
  54. data/lib/ace/assign/molecules/execution_identity_resolver.rb +49 -9
  55. data/lib/ace/assign/molecules/execution_scope_lineage.rb +327 -0
  56. data/lib/ace/assign/molecules/journal_mutation.rb +202 -0
  57. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +4 -0
  58. data/lib/ace/assign/molecules/proposal_journal.rb +151 -0
  59. data/lib/ace/assign/molecules/receipt_verifier.rb +49 -19
  60. data/lib/ace/assign/organisms/assignment_executor.rb +6 -0
  61. data/lib/ace/assign/organisms/attempt_coordinator.rb +312 -13
  62. data/lib/ace/assign/organisms/delivery_coordinator.rb +342 -0
  63. data/lib/ace/assign/organisms/task_assignment_creator.rb +8 -3
  64. data/lib/ace/assign/version.rb +1 -1
  65. data/lib/ace/assign.rb +13 -0
  66. 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"