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.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/presets/deliver-release.yml +11 -8
  3. data/.ace-defaults/assign/presets/integrate-batch.yml +10 -5
  4. data/.ace-defaults/assign/presets/supervise-delivery.yml +6 -4
  5. data/CHANGELOG.md +27 -0
  6. data/handbook/skills/as-assign-drive/SKILL.md +5 -2
  7. data/handbook/workflow-instructions/assign/drive.wf.md +77 -0
  8. data/lib/ace/assign/atoms/assignment_scope.rb +5 -0
  9. data/lib/ace/assign/atoms/candidate_limits.rb +20 -0
  10. data/lib/ace/assign/atoms/git_blob_batch.rb +59 -0
  11. data/lib/ace/assign/authority/assignment_definition.rb +35 -0
  12. data/lib/ace/assign/authority/candidate_blob_batch.rb +55 -0
  13. data/lib/ace/assign/authority/candidate_head_comparison.rb +139 -0
  14. data/lib/ace/assign/authority/candidate_transfer.rb +57 -27
  15. data/lib/ace/assign/authority/command_execution.rb +180 -0
  16. data/lib/ace/assign/authority/endcap.rb +4 -2
  17. data/lib/ace/assign/authority/endcap_attempts.rb +1 -1
  18. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +1 -1
  19. data/lib/ace/assign/authority/endcap_publication.rb +0 -166
  20. data/lib/ace/assign/authority/endcap_service_context.rb +38 -0
  21. data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +176 -0
  22. data/lib/ace/assign/authority/{endcap_services.rb → fixed_service_lifecycle.rb} +42 -46
  23. data/lib/ace/assign/authority/launch_control_exclusions.rb +3 -17
  24. data/lib/ace/assign/authority/ordinary_attempt_start.rb +137 -0
  25. data/lib/ace/assign/authority/ordinary_client.rb +70 -0
  26. data/lib/ace/assign/authority/ordinary_completion.rb +104 -0
  27. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +118 -0
  28. data/lib/ace/assign/authority/ordinary_lifecycle.rb +227 -0
  29. data/lib/ace/assign/authority/ordinary_merge_evidence.rb +81 -0
  30. data/lib/ace/assign/authority/ordinary_peer.rb +54 -0
  31. data/lib/ace/assign/authority/ordinary_progress.rb +162 -0
  32. data/lib/ace/assign/authority/ordinary_queue.rb +115 -0
  33. data/lib/ace/assign/authority/ordinary_release_artifacts.rb +143 -0
  34. data/lib/ace/assign/authority/ordinary_results.rb +206 -0
  35. data/lib/ace/assign/authority/ordinary_review_evidence.rb +180 -0
  36. data/lib/ace/assign/authority/ordinary_reviews.rb +214 -0
  37. data/lib/ace/assign/authority/ordinary_server.rb +75 -0
  38. data/lib/ace/assign/authority/ordinary_supervisor.rb +99 -0
  39. data/lib/ace/assign/authority/prepared_work.rb +9 -4
  40. data/lib/ace/assign/authority/protected_assignment_context.rb +3 -3
  41. data/lib/ace/assign/authority/release_artifact_manifest.rb +91 -0
  42. data/lib/ace/assign/authority/retained_input.rb +53 -0
  43. data/lib/ace/assign/authority/router.rb +11 -6
  44. data/lib/ace/assign/authority/server.rb +11 -180
  45. data/lib/ace/assign/authority/socket_server.rb +255 -0
  46. data/lib/ace/assign/authority/transfer_codec.rb +5 -3
  47. data/lib/ace/assign/cli/commands/attempt/evidence.rb +2 -2
  48. data/lib/ace/assign/cli/commands/submit_candidate.rb +1 -1
  49. data/lib/ace/assign/molecules/canonical_evidence.rb +25 -8
  50. data/lib/ace/assign/molecules/canonical_read_commands.rb +84 -0
  51. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +2 -63
  52. data/lib/ace/assign/molecules/evidence_journal.rb +157 -63
  53. data/lib/ace/assign/molecules/installed_journal_read.rb +143 -0
  54. data/lib/ace/assign/molecules/journal_mutation.rb +7 -4
  55. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +13 -2
  56. data/lib/ace/assign/molecules/proposal_journal.rb +32 -4
  57. data/lib/ace/assign/molecules/receipt_verifier.rb +8 -2
  58. data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +2 -1
  59. data/lib/ace/assign/organisms/attempt_coordinator.rb +88 -36
  60. data/lib/ace/assign/organisms/ordinary_driver.rb +310 -0
  61. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +5 -5
  62. data/lib/ace/assign/version.rb +1 -1
  63. 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