ace-assign 0.70.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/handbook/skills/as-assign-drive/SKILL.md +5 -2
  4. data/handbook/workflow-instructions/assign/drive.wf.md +77 -0
  5. data/lib/ace/assign/atoms/candidate_limits.rb +20 -0
  6. data/lib/ace/assign/atoms/git_blob_batch.rb +59 -0
  7. data/lib/ace/assign/authority/candidate_blob_batch.rb +55 -0
  8. data/lib/ace/assign/authority/candidate_head_comparison.rb +139 -0
  9. data/lib/ace/assign/authority/candidate_transfer.rb +55 -25
  10. data/lib/ace/assign/authority/command_execution.rb +180 -0
  11. data/lib/ace/assign/authority/endcap.rb +4 -2
  12. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +1 -1
  13. data/lib/ace/assign/authority/endcap_publication.rb +0 -166
  14. data/lib/ace/assign/authority/endcap_service_context.rb +38 -0
  15. data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +176 -0
  16. data/lib/ace/assign/authority/{endcap_services.rb → fixed_service_lifecycle.rb} +42 -46
  17. data/lib/ace/assign/authority/ordinary_completion.rb +13 -3
  18. data/lib/ace/assign/authority/ordinary_lifecycle.rb +40 -4
  19. data/lib/ace/assign/authority/ordinary_merge_evidence.rb +81 -0
  20. data/lib/ace/assign/authority/ordinary_release_artifacts.rb +143 -0
  21. data/lib/ace/assign/authority/ordinary_results.rb +11 -1
  22. data/lib/ace/assign/authority/ordinary_review_evidence.rb +180 -0
  23. data/lib/ace/assign/authority/ordinary_reviews.rb +214 -0
  24. data/lib/ace/assign/authority/ordinary_supervisor.rb +99 -0
  25. data/lib/ace/assign/authority/prepared_work.rb +7 -2
  26. data/lib/ace/assign/authority/release_artifact_manifest.rb +91 -0
  27. data/lib/ace/assign/authority/retained_input.rb +53 -0
  28. data/lib/ace/assign/authority/transfer_codec.rb +5 -3
  29. data/lib/ace/assign/cli/commands/submit_candidate.rb +1 -1
  30. data/lib/ace/assign/molecules/canonical_evidence.rb +25 -8
  31. data/lib/ace/assign/molecules/evidence_journal.rb +17 -30
  32. data/lib/ace/assign/molecules/journal_mutation.rb +2 -1
  33. data/lib/ace/assign/molecules/receipt_verifier.rb +8 -2
  34. data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +2 -1
  35. data/lib/ace/assign/organisms/attempt_coordinator.rb +40 -3
  36. data/lib/ace/assign/organisms/ordinary_driver.rb +310 -0
  37. data/lib/ace/assign/version.rb +1 -1
  38. metadata +19 -4
@@ -44,6 +44,12 @@ module Ace
44
44
  registered = registration(params.fetch("assignment_id"), commit: commit)
45
45
  raise AttemptErrors::NotFound, "ordinary work is not registered" unless registered
46
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
47
53
  events = result_events(params, commit)
48
54
  result = retained_ordinary_result(params, events, commit)
49
55
  if request.fetch("operation") == "result_snapshot"
@@ -60,7 +66,11 @@ module Ace
60
66
  return {data: ordinary_result_projection(result).merge("journal_commit" => commit), transfer_parts: parts, replayed: false}
61
67
  end
62
68
  authorize!(request, peer, role, deadline)
63
- return {data: {"assignment_id" => params.fetch("assignment_id"), "attempt_id" => params.fetch("attempt_id"),
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"),
64
74
  "journal_commit" => commit, "generation" => @journal.authority_generation(events),
65
75
  "state" => Molecules::CanonicalAttemptState.derive(events), "submitted_result" => result && ordinary_result_projection(result)}, replayed: false}
66
76
  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
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ace
4
+ module Assign
5
+ module Authority
6
+ # Replacement transfers only result ownership, never worker execution.
7
+ # The existing definition chain serializes it with canonical generation.
8
+ module OrdinarySupervisor
9
+ SUPERVISOR_OPERATIONS = %w[supervisor_status adopt_supervisor].freeze
10
+
11
+ private
12
+
13
+ def supervisor_operation(request, peer, role, deadline)
14
+ params = request.fetch("params")
15
+ assignment = params.fetch("assignment_id")
16
+ token!(assignment)
17
+ registered = registration(assignment, commit: @journal.ref_value)
18
+ raise AttemptErrors::NotFound, "ordinary work is not registered" unless registered
19
+ if request.fetch("operation") == "supervisor_status"
20
+ strict!(params, %w[assignment_id])
21
+ raise ArgumentError, "supervisor status mutation must be null" unless request.fetch("mutation_id").nil?
22
+ commit = @journal.ref_value
23
+ selected = supervisor_selection(registered, commit)
24
+ authorize!(request, peer, role, deadline)
25
+ return {data: selected.merge("journal_commit" => commit), replayed: false}
26
+ end
27
+ strict!(params, %w[assignment_id expected_generation pane previous_binding_sha256])
28
+ token!(request.fetch("mutation_id"))
29
+ unless role == :supervisor && params.fetch("expected_generation").is_a?(Integer) &&
30
+ params.fetch("expected_generation").positive? && params.fetch("previous_binding_sha256").is_a?(String) &&
31
+ params.fetch("previous_binding_sha256").match?(/\A[0-9a-f]{64}\z/)
32
+ raise AttemptErrors::UnauthorizedIdentity, "supervisor adoption requires exact current selection"
33
+ end
34
+ observed = @admission.identity!(peer: peer, pane: params.fetch("pane"), deadline: deadline)
35
+ raise AttemptErrors::UnauthorizedIdentity, "replacement is not an installed coordinator" unless observed.role == "coordinator"
36
+ binding = observed.runtime_binding
37
+ digest = Atoms::EvidenceDigest.digest({"project_id" => @project, "params" => params, "binding" => binding})
38
+ @exclusion.with_shared_multi([@exclusion.task_key(registered.fetch("task_id")), @exclusion.assignment_key(assignment)], deadline: deadline) do
39
+ @journal.mutate(assignment_id: assignment, attempt_id: "definition-#{assignment}",
40
+ mutation_id: request.fetch("mutation_id"), operation: "adopt_supervisor", parameters_digest: digest,
41
+ expected_generation: params.fetch("expected_generation"), with_replay: true, deadline: deadline,
42
+ replay_authorizer: -> {
43
+ verify_new_supervisor!(request, peer, role, deadline, observed)
44
+ unless supervisor_selection(registered, @journal.ref_value).fetch("binding") == binding
45
+ raise AttemptErrors::UnauthorizedIdentity, "supervisor adoption was superseded"
46
+ end
47
+ }) do |_events, prefix, _generation|
48
+ verify_new_supervisor!(request, peer, role, deadline, observed)
49
+ previous = supervisor_selection(registered, prefix)
50
+ unless previous.fetch("binding_sha256") == params.fetch("previous_binding_sha256") && previous.fetch("binding") != binding
51
+ raise AttemptErrors::Conflict, "original supervisor selection changed"
52
+ end
53
+ ended = @admission.supervisor_ended!(binding: previous.fetch("binding"), deadline: deadline)
54
+ unless ended.is_a?(Hash) && ended["liveness"] == "ended"
55
+ raise AttemptErrors::EvidenceUnavailable, "original supervisor generation has not ended"
56
+ end
57
+ verify_new_supervisor!(request, peer, role, deadline, observed)
58
+ {events: [], data: {"authority" => "ordinary", "assignment_id" => assignment, "project_id" => @project,
59
+ "previous_binding" => previous.fetch("binding"), "binding" => binding,
60
+ "binding_sha256" => Atoms::EvidenceDigest.digest(binding), "ended_observation" => ended}}
61
+ end
62
+ end
63
+ end
64
+
65
+ def verify_new_supervisor!(request, peer, role, deadline, observed)
66
+ authorize!(request, peer, role, deadline)
67
+ unless @admission.identity!(peer: peer, pane: request.fetch("params").fetch("pane"), deadline: deadline) == observed
68
+ raise AttemptErrors::UnauthorizedIdentity, "replacement supervisor changed during adoption"
69
+ end
70
+ end
71
+
72
+ def supervisor_selection(registered, commit)
73
+ assignment = registered.fetch("assignment_id")
74
+ events = @journal.read_events(assignment, commit: commit).select { |event| event.fetch("attempt_id") == "definition-#{assignment}" }
75
+ unless Models::EvidenceEvent.chain_valid?(events)
76
+ raise AttemptErrors::EvidenceUnavailable, "ordinary supervisor history is corrupt"
77
+ end
78
+ selected = registered.fetch("supervisor_binding")
79
+ generation = 1
80
+ # Validate the entire old→new chain rather than trusting a local cache.
81
+ events.select { |event| event["type"] == "authority_mutation" }.each do |event|
82
+ payload = event.fetch("payload")
83
+ next if payload.fetch("operation") == "register_assignment"
84
+ data = payload.fetch("data")
85
+ unless payload.fetch("operation") == "adopt_supervisor" && data.fetch("previous_binding") == selected &&
86
+ data.values_at("authority", "assignment_id", "project_id", "generation") == ["ordinary", assignment, @project, generation + 1] &&
87
+ data.fetch("binding_sha256") == Atoms::EvidenceDigest.digest(data.fetch("binding")) &&
88
+ data.dig("ended_observation", "liveness") == "ended"
89
+ raise AttemptErrors::EvidenceUnavailable, "ordinary supervisor adoption chain differs"
90
+ end
91
+ selected, generation = data.fetch("binding"), data.fetch("generation")
92
+ end
93
+ {"project_id" => @project, "assignment_id" => assignment, "generation" => generation,
94
+ "binding" => selected, "binding_sha256" => Atoms::EvidenceDigest.digest(selected)}
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../atoms/candidate_limits"
4
+
3
5
  require "json"
4
6
  require "digest"
5
7
  require "yaml"
@@ -15,7 +17,7 @@ module Ace
15
17
  # byte contract, never a registration, attempt or progress ledger.
16
18
  class PreparedWork
17
19
  MAX_TEXT = 1024 * 1024
18
- MAX_TOTAL = 64 * 1024 * 1024
20
+ MAX_TOTAL = Atoms::CandidateLimits::PREPARED_BYTES
19
21
  MAX_MANIFEST = 32_768
20
22
  TOKEN = /\A[a-zA-Z0-9][a-zA-Z0-9._-]{0,199}\z/
21
23
  SCOPE = /\A[0-9]+(?:\.[0-9]+)*\z/
@@ -28,7 +30,10 @@ module Ace
28
30
  def initialize(files:)
29
31
  invalid!("file inventory") unless files.is_a?(Hash) && files.size.between?(4, 4096)
30
32
  @files = files.to_h do |path, bytes|
31
- invalid!("file path") unless path.is_a?(String) && !path.start_with?("/") && path.split("/", -1).all? { |part| !part.empty? && !%w[. .. .git].include?(part.downcase) }
33
+ invalid!("file path") unless path.is_a?(String) && !path.start_with?("/") &&
34
+ path.bytesize <= Atoms::CandidateLimits::PATH_BYTES &&
35
+ path.split("/", -1).all? { |part| !part.empty? && part.bytesize <= Atoms::CandidateLimits::PATH_COMPONENT_BYTES &&
36
+ !%w[. .. .git].include?(part.downcase) }
32
37
  invalid!("text size") unless bytes.is_a?(String) && bytes.bytesize.between?(1, MAX_TEXT)
33
38
  text = bytes.dup.force_encoding(Encoding::UTF_8)
34
39
  invalid!("UTF-8") unless text.valid_encoding? && !text.include?("\0")