ace-assign 0.71.0 → 0.73.1

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 (197) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +8 -8
  3. data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +1 -1
  4. data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
  5. data/.ace-defaults/assign/config.yml +0 -13
  6. data/.ace-defaults/assign/presets/fix-bug.yml +1 -1
  7. data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +1 -1
  8. data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +1 -1
  9. data/.ace-defaults/assign/presets/work-on-task.yml +1 -1
  10. data/CHANGELOG.md +5 -2116
  11. data/README.md +58 -31
  12. data/docs/exit-codes.md +36 -44
  13. data/docs/getting-started.md +9 -4
  14. data/docs/usage.md +129 -522
  15. data/handbook/skills/as-assign-drive/SKILL.md +1 -4
  16. data/handbook/workflow-instructions/assign/drive.wf.md +47 -195
  17. data/handbook/workflow-instructions/assign/prepare.wf.md +1 -1
  18. data/handbook/workflow-instructions/assign/verify-test-suite.wf.md +8 -2
  19. data/lib/ace/assign/cli/commands/assignment_target.rb +2 -15
  20. data/lib/ace/assign/cli/commands/create.rb +1 -8
  21. data/lib/ace/assign/cli/commands/fail.rb +0 -6
  22. data/lib/ace/assign/cli/commands/finish.rb +2 -23
  23. data/lib/ace/assign/cli/commands/fork_session.rb +0 -1
  24. data/lib/ace/assign/cli/commands/select.rb +0 -1
  25. data/lib/ace/assign/cli/commands/start.rb +0 -2
  26. data/lib/ace/assign/cli/commands/status.rb +1 -31
  27. data/lib/ace/assign/cli/commands/step.rb +1 -3
  28. data/lib/ace/assign/cli.rb +1 -85
  29. data/lib/ace/assign/errors.rb +8 -80
  30. data/lib/ace/assign/models/assignment.rb +5 -28
  31. data/lib/ace/assign/molecules/assignment_manager.rb +0 -26
  32. data/lib/ace/assign/molecules/fork_session_launcher.rb +4 -12
  33. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +15 -112
  34. data/lib/ace/assign/molecules/runtime_control_surface_runner.rb +6 -2
  35. data/lib/ace/assign/molecules/skill_assign_source_resolver.rb +9 -0
  36. data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
  37. data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
  38. data/lib/ace/assign/version.rb +1 -1
  39. data/lib/ace/assign.rb +0 -6
  40. metadata +22 -176
  41. data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
  42. data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
  43. data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
  44. data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
  45. data/docs/protected-authority.md +0 -282
  46. data/exe/ace-scope-ready +0 -55
  47. data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
  48. data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
  49. data/lib/ace/assign/atoms/candidate_limits.rb +0 -20
  50. data/lib/ace/assign/atoms/delivery_parameters.rb +0 -19
  51. data/lib/ace/assign/atoms/evidence_digest.rb +0 -78
  52. data/lib/ace/assign/atoms/fixed_merge_schemas.rb +0 -398
  53. data/lib/ace/assign/atoms/git_blob_batch.rb +0 -59
  54. data/lib/ace/assign/authority/assignment_definition.rb +0 -35
  55. data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
  56. data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
  57. data/lib/ace/assign/authority/campaign_execution.rb +0 -119
  58. data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
  59. data/lib/ace/assign/authority/candidate_blob_batch.rb +0 -55
  60. data/lib/ace/assign/authority/candidate_head_comparison.rb +0 -139
  61. data/lib/ace/assign/authority/candidate_transfer.rb +0 -357
  62. data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
  63. data/lib/ace/assign/authority/client.rb +0 -257
  64. data/lib/ace/assign/authority/command_execution.rb +0 -180
  65. data/lib/ace/assign/authority/deployment.rb +0 -627
  66. data/lib/ace/assign/authority/deployment_history.rb +0 -146
  67. data/lib/ace/assign/authority/endcap.rb +0 -457
  68. data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
  69. data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
  70. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
  71. data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
  72. data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
  73. data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
  74. data/lib/ace/assign/authority/endcap_publication.rb +0 -41
  75. data/lib/ace/assign/authority/endcap_results.rb +0 -398
  76. data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
  77. data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
  78. data/lib/ace/assign/authority/endcap_service_context.rb +0 -38
  79. data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
  80. data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
  81. data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
  82. data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +0 -176
  83. data/lib/ace/assign/authority/fixed_service_lifecycle.rb +0 -594
  84. data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
  85. data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
  86. data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
  87. data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
  88. data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
  89. data/lib/ace/assign/authority/launch_driver.rb +0 -393
  90. data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
  91. data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
  92. data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
  93. data/lib/ace/assign/authority/launch_review.rb +0 -91
  94. data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
  95. data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
  96. data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
  97. data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
  98. data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
  99. data/lib/ace/assign/authority/launch_steering.rb +0 -353
  100. data/lib/ace/assign/authority/launch_stop.rb +0 -254
  101. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
  102. data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
  103. data/lib/ace/assign/authority/ordinary_client.rb +0 -70
  104. data/lib/ace/assign/authority/ordinary_completion.rb +0 -104
  105. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
  106. data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -227
  107. data/lib/ace/assign/authority/ordinary_merge_evidence.rb +0 -81
  108. data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
  109. data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
  110. data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
  111. data/lib/ace/assign/authority/ordinary_release_artifacts.rb +0 -143
  112. data/lib/ace/assign/authority/ordinary_results.rb +0 -206
  113. data/lib/ace/assign/authority/ordinary_review_evidence.rb +0 -180
  114. data/lib/ace/assign/authority/ordinary_reviews.rb +0 -214
  115. data/lib/ace/assign/authority/ordinary_server.rb +0 -75
  116. data/lib/ace/assign/authority/ordinary_supervisor.rb +0 -99
  117. data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
  118. data/lib/ace/assign/authority/posix_acl.rb +0 -59
  119. data/lib/ace/assign/authority/prepared_input.rb +0 -130
  120. data/lib/ace/assign/authority/prepared_queue.rb +0 -185
  121. data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
  122. data/lib/ace/assign/authority/prepared_work.rb +0 -309
  123. data/lib/ace/assign/authority/prepared_worker.rb +0 -69
  124. data/lib/ace/assign/authority/private_directory.rb +0 -33
  125. data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
  126. data/lib/ace/assign/authority/readiness_hook.rb +0 -120
  127. data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
  128. data/lib/ace/assign/authority/release_artifact_manifest.rb +0 -91
  129. data/lib/ace/assign/authority/retained_input.rb +0 -53
  130. data/lib/ace/assign/authority/router.rb +0 -81
  131. data/lib/ace/assign/authority/server.rb +0 -131
  132. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
  133. data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
  134. data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
  135. data/lib/ace/assign/authority/service_evidence.rb +0 -286
  136. data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
  137. data/lib/ace/assign/authority/socket_server.rb +0 -255
  138. data/lib/ace/assign/authority/task_context_entry.rb +0 -32
  139. data/lib/ace/assign/authority/transfer_codec.rb +0 -189
  140. data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
  141. data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
  142. data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
  143. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
  144. data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
  145. data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
  146. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
  147. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
  148. data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
  149. data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
  150. data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
  151. data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
  152. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
  153. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
  154. data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
  155. data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
  156. data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
  157. data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
  158. data/lib/ace/assign/cli/commands/delivery.rb +0 -107
  159. data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
  160. data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
  161. data/lib/ace/assign/cli/commands/resume.rb +0 -38
  162. data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
  163. data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
  164. data/lib/ace/assign/models/attempt.rb +0 -178
  165. data/lib/ace/assign/models/attempt_binding.rb +0 -116
  166. data/lib/ace/assign/models/evidence_event.rb +0 -86
  167. data/lib/ace/assign/models/execution_receipt.rb +0 -139
  168. data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
  169. data/lib/ace/assign/molecules/attempt_store.rb +0 -201
  170. data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
  171. data/lib/ace/assign/molecules/canonical_evidence.rb +0 -170
  172. data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
  173. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
  174. data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
  175. data/lib/ace/assign/molecules/evidence_journal.rb +0 -1899
  176. data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
  177. data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
  178. data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
  179. data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
  180. data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
  181. data/lib/ace/assign/molecules/journal_mutation.rb +0 -274
  182. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
  183. data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
  184. data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
  185. data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
  186. data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
  187. data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
  188. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
  189. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
  190. data/lib/ace/assign/molecules/receipt_verifier.rb +0 -320
  191. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
  192. data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1374
  193. data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
  194. data/lib/ace/assign/organisms/ordinary_driver.rb +0 -310
  195. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
  196. data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
  197. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
@@ -1,118 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "service_evidence"
4
- require_relative "../molecules/ordinary_merge_contract"
5
-
6
- module Ace
7
- module Assign
8
- module Authority
9
- # Completion reads the existing effect owners at the held CAS prefix.
10
- # A prompt, acknowledgment or failed dispatch never settles an effect.
11
- class OrdinaryEffectSettlement
12
- def initialize(journal:)
13
- @journal = journal
14
- end
15
-
16
- def verify!(project:, assignment:, attempt:, events:, commit:)
17
- retained = @journal.read_events(assignment, commit: commit).select { |event| event["attempt_id"] == attempt }
18
- unless retained == events && Models::EvidenceEvent.chain_valid?(events)
19
- unavailable!("ordinary settlement needs its exact canonical attempt")
20
- end
21
- selection = {"project_id" => project, "assignment_id" => assignment, "attempt_id" => attempt}
22
- services = records!(@journal.service_request_records(commit: commit), events, selection, %w[service_claim service_transition])
23
- merges = records!(@journal.fixed_merge_request_records(commit: commit), events, selection, %w[merge_admission merge_transition])
24
- services.each { |record| service!(record, commit) }
25
- merges.each { |record| merge!(record, commit) }
26
- assignment_events = @journal.read_events(assignment, commit: commit)
27
- assignment_events.group_by { |event| event.fetch("attempt_id") }.each_value do |chain|
28
- unavailable!("ordinary delivery history is corrupt") unless Models::EvidenceEvent.chain_valid?(chain)
29
- end
30
- delivery!(events, merges, assignment_events)
31
- {"journal_commit" => commit, "service_requests" => services.map { |record| record.fetch("request_id") }.sort,
32
- "merge_requests" => merges.map { |record| record.fetch("request_id") }.sort}
33
- end
34
-
35
- private
36
-
37
- def records!(inventory, events, selection, types)
38
- records = inventory.select { |record| record.values_at("assignment_id", "attempt_id") == selection.values_at("assignment_id", "attempt_id") }
39
- references = events.select { |event| types.include?(event["type"]) }
40
- ids = references.map { |event| event.fetch("payload").fetch("request_id") }.uniq.sort
41
- unless records.map { |record| record.fetch("request_id") }.sort == ids
42
- unavailable!("ordinary effect inventory and event set differ")
43
- end
44
- records.each do |record|
45
- latest = references.reverse.find { |event| event.dig("payload", "request_id") == record.fetch("request_id") }
46
- unless record.slice(*selection.keys) == selection && latest.dig("payload", "state") == record["state"] &&
47
- latest.dig("payload", "record_digest") == Atoms::EvidenceDigest.digest(record) &&
48
- latest.dig("payload", "receipt_digest") == (record["receipt"] && Atoms::EvidenceDigest.digest(record["receipt"]))
49
- unavailable!("ordinary effect record differs from its accepted owner event")
50
- end
51
- end
52
- records
53
- end
54
-
55
- def service!(record, commit)
56
- return if record["state"] == "rejected" && record["consumed"] == false
57
- pending!("ordinary service effect is unresolved") unless %w[succeeded failed-settled].include?(record["state"])
58
- receipt = record.fetch("receipt")
59
- @journal.validate_terminal_receipt_structure!(record, record.fetch("state"), receipt)
60
- # The installed owner consumes the existing canonical import, never
61
- # the old local receipt file. This is independent of journal mode.
62
- owner = ServiceEvidence.new(journal: @journal)
63
- references = receipt.fetch("evidence")
64
- if %w[publish prune-preserved-workspace].include?(record["operation"])
65
- contents = owner.call(references, record, record.fetch("state"), {commit: commit})
66
- unless contents.is_a?(Array) && contents.size == references.size
67
- unavailable!("ordinary service evidence collection differs")
68
- end
69
- else
70
- contents = references.map { |reference| owner.call(reference, record, record.fetch("state"), {commit: commit}) }
71
- end
72
- references.zip(contents).each do |reference, bytes|
73
- unavailable!("ordinary service evidence digest differs") unless bytes.is_a?(String) && Digest::SHA256.hexdigest(bytes.b) == reference.fetch("sha256")
74
- @journal.verify_service_attestation!(bytes, record, record.fetch("state")) unless %w[publish prune-preserved-workspace].include?(record["operation"])
75
- end
76
- end
77
-
78
- def merge!(record, commit)
79
- unless @journal.fixed_merge_settlement!(record.fetch("request_id"), commit: commit) == record
80
- unavailable!("ordinary merge settlement differs from its selected record")
81
- end
82
- end
83
-
84
- def delivery!(events, merges, assignment_events)
85
- deliveries = assignment_events.select { |event| event["type"] == "delivery" }
86
- events.select { |event| event["type"] == "delivery" }.each do |intent|
87
- payload = intent.fetch("payload")
88
- case payload["stage"]
89
- when "intent"
90
- settled = deliveries.any? { |event| event.dig("payload", "stage") == "result" &&
91
- event.dig("payload", "intent_digest") == intent.fetch("digest") &&
92
- event.dig("payload", "intent_attempt_id") == intent.fetch("attempt_id") &&
93
- event.dig("payload", "operation") == payload.fetch("operation") &&
94
- event.dig("payload", "head") == payload.fetch("head") && event.dig("payload", "outcome") == "succeeded" }
95
- pending!("ordinary delivery intent is unresolved") unless settled
96
- when "ordinary-merge-intent"
97
- result = deliveries.reverse.find { |event| event.dig("payload", "stage") == "ordinary-merge-result" &&
98
- event.dig("payload", "request_id") == payload.fetch("request_id") && event.dig("payload", "input_digest") == payload.fetch("input_digest") }
99
- pending!("ordinary merge delivery is unresolved") unless result
100
- accepted = result.fetch("payload")
101
- record = merges.find { |item| item["request_id"] == payload.fetch("request_id") }
102
- if accepted["outcome"] == "refused_no_effect" && record.nil?
103
- refusal = accepted.slice("code", "message", "request_id", "input_digest").merge("schema" => "ace.lab.service.ordinary-merge-refusal/v1")
104
- Molecules::OrdinaryMergeContract.validate_refusal!(refusal)
105
- pending!("ordinary merge receiver availability is unresolved") if refusal["code"] == Molecules::OrdinaryMergeContract::UNAVAILABLE_CODE
106
- elsif !record || record["input_digest"] != payload.fetch("input_digest") || record["receipt"] != accepted["receipt"]
107
- unavailable!("ordinary delivery result differs from the merge owner")
108
- end
109
- end
110
- end
111
- end
112
-
113
- def unavailable!(message) = raise AttemptErrors::EvidenceUnavailable, message
114
- def pending!(message) = raise AttemptErrors::ServiceSettlementPending, message
115
- end
116
- end
117
- end
118
- end
@@ -1,227 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "ace/herdr"
4
- require_relative "prepared_work"
5
- require_relative "assignment_definition"
6
- require_relative "ordinary_queue"
7
- require_relative "ordinary_attempt_start"
8
- require_relative "ordinary_progress"
9
- require_relative "ordinary_results"
10
- require_relative "ordinary_completion"
11
- require_relative "ordinary_supervisor"
12
- require_relative "ordinary_reviews"
13
- require_relative "ordinary_merge_evidence"
14
- require_relative "ordinary_release_artifacts"
15
- require_relative "../molecules/lifecycle_exclusion"
16
-
17
- module Ace
18
- module Assign
19
- module Authority
20
- # Ordinary lifecycle uses the existing canonical execution ref and CAS.
21
- # The installed Lab composition supplies one journal and exclusion owner
22
- # shared with HITL and merge. No requester cache supplies accepted state.
23
- class OrdinaryLifecycle
24
- include OrdinaryAttemptStart
25
- include OrdinaryProgress
26
- include OrdinaryResults
27
- include OrdinaryCompletion
28
- include OrdinarySupervisor
29
- include OrdinaryReviews
30
- include OrdinaryMergeEvidence
31
- include OrdinaryReleaseArtifacts
32
- OPERATIONS = (%w[register_assignment registration_status start_attempt finish merge_evidence] + OrdinaryProgress::PROGRESS_OPERATIONS + OrdinaryResults::RESULT_OPERATIONS + OrdinarySupervisor::SUPERVISOR_OPERATIONS + OrdinaryReviews::REVIEW_OPERATIONS + OrdinaryReleaseArtifacts::RELEASE_OPERATIONS).freeze
33
- TRANSFER_OPERATIONS = {"register_assignment" => {direction: :upload, purpose: :candidate, roles: [:supervisor]}}
34
- .merge(OrdinaryProgress::PROGRESS_TRANSFERS).merge(OrdinaryResults::RESULT_TRANSFERS).merge(OrdinaryReviews::REVIEW_TRANSFERS).merge(OrdinaryReleaseArtifacts::RELEASE_TRANSFERS).freeze
35
- REGISTER_KEYS = %w[assignment_id definition_digest prepared_head prepared_tree manifest_sha256 expected_generation pane transfer].freeze
36
-
37
- def initialize(project:, admission:, journal:, exclusion:, input_root:, inbox: nil, command_evidence: nil, release_authorizer: nil)
38
- @project, @admission, @journal, @exclusion = project, admission, journal, exclusion
39
- @input_root = PrivateDirectory.verify!(input_root)
40
- unless inbox.nil? || inbox.is_a?(Ace::Herdr::Organisms::Inbox)
41
- raise ArgumentError, "ordinary completion requires the installed Inbox owner"
42
- end
43
- unless release_authorizer.nil? || release_authorizer.respond_to?(:call)
44
- raise ArgumentError, "release artifact authorization requires its installed policy owner"
45
- end
46
- @inbox, @command_evidence, @release_authorizer = inbox, command_evidence, release_authorizer
47
- end
48
-
49
- def authorize_transfer!(request:, peer:, role:, deadline:)
50
- authorize!(request, peer, role, deadline)
51
- if OrdinaryReleaseArtifacts::RELEASE_TRANSFERS.key?(request.fetch("operation"))
52
- authorize_ordinary_release!(request, peer, role, deadline)
53
- return
54
- end
55
- if OrdinaryReviews::REVIEW_TRANSFERS.key?(request.fetch("operation"))
56
- authorize_ordinary_review!(request, peer, role, deadline)
57
- return
58
- end
59
- if OrdinaryResults::RESULT_TRANSFERS.key?(request.fetch("operation"))
60
- validate_result!(request)
61
- authorize_start_replay!(request, peer, role, deadline) if request.fetch("operation") == "submit_result"
62
- return
63
- end
64
- if OrdinaryProgress::PROGRESS_TRANSFERS.key?(request.fetch("operation"))
65
- validate_progress!(request)
66
- return
67
- end
68
- unless request.fetch("operation") == "register_assignment" && role == :supervisor
69
- raise AttemptErrors::UnauthorizedIdentity, "ordinary registration requires the installed supervisor"
70
- end
71
- validate_registration!(request)
72
- end
73
-
74
- def dispatch(request:, peer:, role:, deadline:, transfer: nil)
75
- authorize!(request, peer, role, deadline)
76
- return ordinary_review_operation(request, peer, role, deadline, transfer) if OrdinaryReviews::REVIEW_OPERATIONS.include?(request.fetch("operation"))
77
- case request.fetch("operation")
78
- when "retain_release_artifact"
79
- retain_release_artifact(request, peer, role, deadline, transfer)
80
- when "merge_evidence"
81
- raise ArgumentError, "merge evidence read forbids transfer" if transfer
82
- ordinary_merge_evidence(request, peer, role, deadline)
83
- when "supervisor_status", "adopt_supervisor"
84
- raise ArgumentError, "ordinary supervisor operation forbids transfer" if transfer
85
- supervisor_operation(request, peer, role, deadline)
86
- when "registration_status"
87
- strict!(request.fetch("params"), %w[assignment_id])
88
- raise ArgumentError, "status mutation must be null" unless request.fetch("mutation_id").nil?
89
- assignment = request.fetch("params").fetch("assignment_id")
90
- token!(assignment)
91
- commit = @journal.ref_value
92
- data = registration(assignment, commit: commit) || {"assignment_id" => assignment, "generation" => 0}
93
- {data: data.merge("journal_commit" => commit), replayed: false}
94
- when "register_assignment"
95
- authorize_transfer!(request: request, peer: peer, role: role, deadline: deadline)
96
- register(request, peer, deadline, transfer)
97
- when "start_attempt"
98
- raise ArgumentError, "ordinary start forbids transfer" if transfer
99
- start_attempt(request, peer, role, deadline)
100
- when "assignment_snapshot"
101
- raise ArgumentError, "ordinary snapshot forbids upload" if transfer
102
- assignment_snapshot(request, peer, role, deadline)
103
- when "queue_start", "queue_finish", "queue_fail"
104
- progress(request, peer, role, deadline, transfer)
105
- when "submit_result", "result_status", "result_snapshot"
106
- ordinary_result(request, peer, role, deadline, transfer)
107
- when "finish"
108
- raise ArgumentError, "ordinary completion forbids transfer" if transfer
109
- finish_result(request, peer, role, deadline)
110
- else
111
- raise ArgumentError, "unknown ordinary lifecycle operation"
112
- end
113
- end
114
-
115
- private
116
-
117
- def register(request, peer, deadline, transfer)
118
- params = request.fetch("params")
119
- # Native observation and candidate subprocesses happen before CAS.
120
- identity = @admission.identity!(peer: peer, pane: params.fetch("pane"), deadline: deadline)
121
- unless identity.role == "coordinator"
122
- raise AttemptErrors::UnauthorizedIdentity, "ordinary registration native owner differs"
123
- end
124
- raise AttemptErrors::MalformedTransfer, "prepared input requires one bundle" unless transfer && transfer.count == 1
125
- bytes = transfer.bytes.freeze
126
- descriptor = params.fetch("transfer")
127
- work = PreparedWork.admit(bytes: bytes, head: params.fetch("prepared_head"), tree: params.fetch("prepared_tree"),
128
- sha256: descriptor.fetch("sha256"), size: descriptor.fetch("bytes"), root: @input_root, deadline: deadline)
129
- unless work.manifest.values_at("assignment_id", "project_id") == [params.fetch("assignment_id"), @project] &&
130
- work.manifest_sha256 == params.fetch("manifest_sha256")
131
- raise ArgumentError, "ordinary prepared assignment association differs"
132
- end
133
- definition = work.definition_bytes(head: params.fetch("prepared_head"), tree: params.fetch("prepared_tree"))
134
- assignment = AssignmentDefinition.parse(bytes: definition, digest: params.fetch("definition_digest"),
135
- assignment_id: params.fetch("assignment_id"), project_id: @project)
136
- # Campaign registration needs its existing canonical campaign owner;
137
- # it cannot be manufactured by attaching unchecked parent metadata.
138
- if assignment.review_campaign || assignment.campaign_execution
139
- raise AttemptErrors::EvidenceUnavailable, "ordinary campaign registration requires its campaign owner"
140
- end
141
- principal = {"uid" => peer.fetch("uid"), "actor" => identity.actor}
142
- queue = OrdinaryQueue.new(work: work, root: @input_root)
143
- queue_sha = Digest::SHA256.hexdigest(queue.bytes)
144
- digest = Atoms::EvidenceDigest.digest({"params" => params, "project_id" => @project, "principal" => principal})
145
- keys = [@exclusion.task_key(assignment.task_id), @exclusion.assignment_key(assignment.id)]
146
- @exclusion.with_shared_multi(keys, deadline: deadline) do
147
- # Replay bypasses the CAS callback; current authority is mandatory
148
- # before that lookup as well as on every fresh CAS attempt.
149
- authorize!(request, peer, :supervisor, deadline)
150
- @journal.mutate(assignment_id: assignment.id, attempt_id: "definition-#{assignment.id}",
151
- mutation_id: request.fetch("mutation_id"), operation: "register_assignment", parameters_digest: digest,
152
- expected_generation: params.fetch("expected_generation"), with_replay: true, deadline: deadline,
153
- replay_authorizer: -> { authorize!(request, peer, :supervisor, deadline) }) do |events, commit, _generation|
154
- authorize!(request, peer, :supervisor, deadline)
155
- unless events.empty? && registration(assignment.id, commit: commit).nil?
156
- raise AttemptErrors::Conflict, "ordinary assignment is already registered; retain its original definition"
157
- end
158
- bundle_sha = Digest::SHA256.hexdigest(bytes)
159
- definition_path = "execution/definitions/#{assignment.id}-#{params.fetch('definition_digest')}.json"
160
- bundle_path = "execution/prepared/#{assignment.id}-#{bundle_sha}.bundle"
161
- queue_path = "execution/definitions/#{assignment.id}-queue-#{queue_sha}.json"
162
- {events: [], blobs: {definition_path => definition, bundle_path => bytes, queue_path => queue.bytes}, data: {
163
- "authority" => "ordinary", "assignment_id" => assignment.id, "project_id" => @project,
164
- "task_id" => assignment.task_id, "registered_by" => principal, "phase" => "registered",
165
- "supervisor_binding" => identity.runtime_binding,
166
- "definition_ref" => definition_path, "definition_digest" => params.fetch("definition_digest"),
167
- "queue_ref" => queue_path, "queue_sha256" => queue_sha,
168
- "prepared_work" => work.reference(head: params.fetch("prepared_head"), tree: params.fetch("prepared_tree")),
169
- "prepared_bundle_ref" => bundle_path, "prepared_bundle_sha256" => bundle_sha,
170
- "prepared_bundle_bytes" => bytes.bytesize}}
171
- end
172
- end
173
- end
174
-
175
- def registration(assignment, commit:)
176
- return nil unless commit
177
- events = @journal.read_events(assignment, commit: commit)
178
- events.group_by { |event| event.fetch("attempt_id") }.each_value do |chain|
179
- raise AttemptErrors::EvidenceUnavailable, "ordinary assignment evidence is corrupt" unless Models::EvidenceEvent.chain_valid?(chain)
180
- end
181
- registrations = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "register_assignment" }
182
- return nil if registrations.empty?
183
- data = registrations.one? && registrations.first.dig("payload", "data")
184
- unless data && data.values_at("authority", "assignment_id", "project_id") == ["ordinary", assignment, @project]
185
- raise AttemptErrors::EvidenceUnavailable, "ordinary assignment registration owner differs"
186
- end
187
- data
188
- end
189
-
190
- def authorize!(request, peer, role, deadline)
191
- unless deadline.is_a?(Numeric) && deadline.finite? && deadline > Process.clock_gettime(Process::CLOCK_MONOTONIC)
192
- raise AttemptErrors::EvidenceUnavailable, "ordinary lifecycle deadline elapsed"
193
- end
194
- unless request.fetch("project_id") == @project && @admission.authorize!(request: request, peer: peer, deadline: deadline) == role
195
- raise AttemptErrors::UnauthorizedIdentity, "ordinary lifecycle principal differs"
196
- end
197
- # Project commands share the canonical journal but never the managed
198
- # assignment namespace. Reserve their prefix on every ordinary route,
199
- # including transfer admission, status and registration replay.
200
- assignment = request.fetch("params")["assignment_id"]
201
- if assignment.is_a?(String) && assignment.start_with?("command-")
202
- raise ArgumentError, "ordinary assignment uses the reserved project-command namespace"
203
- end
204
- end
205
-
206
- def validate_registration!(request)
207
- params = request.fetch("params")
208
- strict!(params, REGISTER_KEYS)
209
- token!(request.fetch("mutation_id"))
210
- token!(params.fetch("assignment_id"))
211
- token!("definition-#{params.fetch('assignment_id')}")
212
- unless params.fetch("expected_generation").is_a?(Integer) && params.fetch("expected_generation") >= 0
213
- raise ArgumentError, "ordinary generation is invalid"
214
- end
215
- end
216
-
217
- def strict!(value, keys)
218
- raise ArgumentError, "ordinary authority fields differ" unless value.is_a?(Hash) && value.keys.sort == keys.sort
219
- end
220
-
221
- def token!(value)
222
- raise ArgumentError, "ordinary identity is invalid" unless value.is_a?(String) && Molecules::JournalMutation::ID.match?(value)
223
- end
224
- end
225
- end
226
- end
227
- end
@@ -1,81 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ace
4
- module Assign
5
- module Authority
6
- # The installed fixed merge consumes this same lifecycle, never a local
7
- # receipt file. Locks precede journal CAS; no callback acquires an inverted
8
- # lifecycle lock while holding the journal writer lock.
9
- module OrdinaryMergeEvidence
10
- def with_merge_scope(record:, deadline:, exclusive: false)
11
- assignment, attempt = record.values_at("assignment_id", "attempt_id")
12
- token!(assignment)
13
- token!(attempt)
14
- registered = registration(assignment, commit: @journal.ref_value)
15
- raise AttemptErrors::EvidenceUnavailable, "merge assignment is unregistered" unless registered
16
- @exclusion.with_shared_multi([@exclusion.task_key(registered.fetch("task_id"))], deadline: deadline) do
17
- key = @exclusion.assignment_key(assignment)
18
- if exclusive
19
- @exclusion.with_exclusive(key, deadline: deadline) { yield }
20
- else
21
- @exclusion.with_shared_multi([key], deadline: deadline) { yield }
22
- end
23
- end
24
- end
25
-
26
- def verify_merge_evidence_at!(record:, commit:, deadline:)
27
- unless record.fetch("project_id") == @project
28
- raise AttemptErrors::ReceiptRejected, "merge evidence project differs"
29
- end
30
- request = record.fetch("request")
31
- events = result_events(record, commit)
32
- current = ordinary_candidate_event(events)&.dig("payload", "data")
33
- raise AttemptErrors::ReceiptRejected, "merge candidate is unavailable" unless current
34
- projection = merge_evidence_at!(events: events, commit: commit, head: request.fetch("candidate_head"),
35
- generation: current.fetch("candidate_generation"), execution_ids: request.fetch("test_evidence").map { |reference| reference.fetch("reference") }, deadline: deadline)
36
- unless request.fetch("review_evidence") == projection.fetch("review_evidence") && request.fetch("test_evidence") == projection.fetch("test_evidence")
37
- raise AttemptErrors::ReceiptRejected, "merge requires exact canonical review and executed test references"
38
- end
39
- origin = origin_at!(events: events, commit: commit)
40
- observed = @admission.recorded_execution!(binding: origin.fetch("runtime_binding"), head: request.fetch("candidate_head"), deadline: deadline)
41
- unless observed.fetch(:runtime_binding) == origin.fetch("runtime_binding")
42
- raise AttemptErrors::UnauthorizedIdentity, "merge producer native identity changed"
43
- end
44
- true
45
- rescue KeyError, TypeError
46
- raise AttemptErrors::ReceiptRejected, "canonical merge evidence is incomplete"
47
- end
48
-
49
- def merge_evidence_at!(events:, commit:, head:, generation:, execution_ids:, deadline:)
50
- unless @command_evidence && @command_evidence.journal.equal?(@journal) && execution_ids.is_a?(Array) &&
51
- execution_ids.size.between?(1, 16) && execution_ids.uniq == execution_ids
52
- raise AttemptErrors::EvidenceUnavailable, "merge requires the installed canonical executed-test owner"
53
- end
54
- candidate = candidate_at!(events: events, commit: commit, head: head, generation: generation)
55
- review = review_at!(events: events, commit: commit, head: head, generation: generation)
56
- tests = execution_ids.map { |id| @command_evidence.test_evidence_at!(execution_id: id, commit: commit,
57
- head: head, tree: candidate.fetch("tree"), deadline: deadline) }
58
- {"review_evidence" => {"schema" => "ace.review.acceptance-reference/v1", "reference" => review.fetch("review_id"),
59
- "reviewer" => review.fetch("reviewer_actor"), "verdict" => "approved", "reviewed_head" => head,
60
- "recorded_at" => review.fetch("recorded_at"), "record_sha256" => review.fetch("acceptance_event_digest")},
61
- "test_evidence" => tests}
62
- end
63
-
64
- private
65
-
66
- def ordinary_merge_evidence(request, peer, role, deadline)
67
- params = request.fetch("params")
68
- strict!(params, %w[assignment_id attempt_id head candidate_generation test_execution_ids])
69
- raise ArgumentError, "merge evidence read mutation must be null" unless request.fetch("mutation_id").nil?
70
- with_scope(assignment: params.fetch("assignment_id"), attempt: params.fetch("attempt_id"), deadline: deadline) do
71
- prefix = @journal.ref_value
72
- projection = merge_evidence_at!(events: result_events(params, prefix), commit: prefix, head: params.fetch("head"),
73
- generation: params.fetch("candidate_generation"), execution_ids: params.fetch("test_execution_ids"), deadline: deadline)
74
- authorize!(request, peer, role, deadline)
75
- {data: projection.merge("journal_commit" => prefix), replayed: false}
76
- end
77
- end
78
- end
79
- end
80
- end
81
- end
@@ -1,54 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "socket"
4
- require "ace/runtime/molecules/process_identity"
5
- require "ace/herdr/molecules/bounded_observation"
6
-
7
- module Ace
8
- module Assign
9
- module Authority
10
- # Ordinary UNIX peer attribution. This is not a protected launch observer:
11
- # it confers no Yama, capability, cgroup or descendant-closure guarantee.
12
- class OrdinaryPeer
13
- def initialize(identity: nil)
14
- @identity = identity
15
- end
16
-
17
- def capture(socket, deadline: default_deadline)
18
- identity = identity_observer(deadline)
19
- uid, gid = socket.getpeereid
20
- pid = if RUBY_PLATFORM.include?("darwin")
21
- socket.getsockopt(0, 2).int # SOL_LOCAL / LOCAL_PEERPID
22
- elsif Socket.const_defined?(:SO_PEERCRED)
23
- observed_pid, observed_uid, observed_gid = socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_PEERCRED).data.unpack("i!3")
24
- unavailable! unless [observed_uid, observed_gid] == [uid, gid]
25
- observed_pid
26
- end
27
- value = identity.capture(pid)
28
- unavailable! unless value && value["uid"] == uid && socket.getpeereid == [uid, gid] && identity.capture(pid) == value
29
- value.merge("gid" => gid).freeze
30
- rescue SystemCallError, IOError, SocketError, NotImplementedError
31
- unavailable!
32
- end
33
-
34
- def live!(peer, deadline: default_deadline)
35
- unavailable! unless peer.is_a?(Hash) && identity_observer(deadline).capture(peer["pid"]) == peer.slice("pid", "uid", "started_at", "host")
36
- peer
37
- end
38
-
39
- private
40
-
41
- def default_deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
42
-
43
- def identity_observer(deadline)
44
- @identity || Ace::Runtime::Molecules::ProcessIdentity.new(
45
- executor: Ace::Herdr::Molecules::BoundedObservation.new(deadline: deadline))
46
- end
47
-
48
- def unavailable!
49
- raise AttemptErrors::UnauthorizedIdentity, "ordinary kernel peer is unavailable or changed"
50
- end
51
- end
52
- end
53
- end
54
- end
@@ -1,162 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ace
4
- module Assign
5
- module Authority
6
- # Source-owned progress for the already accepted work. Queue completion
7
- # remains a worker report, never a successful execution result or merge.
8
- module OrdinaryProgress
9
- PROGRESS_OPERATIONS = %w[queue_start queue_finish queue_fail assignment_snapshot].freeze
10
- PROGRESS_TRANSFERS = {
11
- "queue_finish" => {direction: :upload, purpose: :artifacts, roles: %i[worker supervisor]},
12
- "assignment_snapshot" => {direction: :download, purpose: :assignment_snapshot, roles: %i[worker supervisor reviewer]}
13
- }.freeze
14
- PROGRESS_COMMON = %w[assignment_id attempt_id expected_generation pane].freeze
15
-
16
- private
17
-
18
- def validate_progress!(request)
19
- params = request.fetch("params")
20
- operation = request.fetch("operation")
21
- if operation == "assignment_snapshot"
22
- strict!(params, %w[assignment_id attempt_id kind])
23
- token!(params.fetch("attempt_id")) unless params.fetch("attempt_id").nil?
24
- unless request.fetch("mutation_id").nil? && %w[prepared_work queue].include?(params.fetch("kind"))
25
- raise ArgumentError, "ordinary snapshot must be a read of accepted work or queue"
26
- end
27
- else
28
- fields = case operation
29
- when "queue_start" then %w[step]
30
- when "queue_finish" then %w[step transfer]
31
- when "queue_fail" then %w[message]
32
- else raise ArgumentError, "unknown ordinary progress operation"
33
- end
34
- strict!(params, PROGRESS_COMMON + fields)
35
- token!(params.fetch("attempt_id")); token!(request.fetch("mutation_id"))
36
- unless params.fetch("expected_generation").is_a?(Integer) && params.fetch("expected_generation").positive?
37
- raise ArgumentError, "ordinary progress generation is invalid"
38
- end
39
- if params.key?("step") && !params["step"].nil? &&
40
- !(params["step"].is_a?(String) && params["step"].bytesize.between?(1, 128) && PreparedWork::SCOPE.match?(params["step"]))
41
- raise ArgumentError, "ordinary progress step is invalid"
42
- end
43
- if params.key?("message") && !(params["message"].is_a?(String) && params["message"].bytesize.between?(1, 1024) && !params["message"].include?("\0"))
44
- raise ArgumentError, "ordinary failure message is invalid"
45
- end
46
- end
47
- token!(params.fetch("assignment_id"))
48
- end
49
-
50
- def assignment_snapshot(request, peer, role, deadline)
51
- validate_progress!(request)
52
- params = request.fetch("params")
53
- commit = @journal.ref_value
54
- registered = registration(params.fetch("assignment_id"), commit: commit)
55
- raise AttemptErrors::NotFound, "ordinary assignment is not registered" unless registered
56
- queue_record = current_queue_record(registered, commit)
57
- selected_events = if params.fetch("attempt_id")
58
- result_events(params, commit)
59
- else
60
- []
61
- end
62
- kind = params.fetch("kind")
63
- source = kind == "prepared_work" ? registered : queue_record
64
- path_key, digest_key, size = if kind == "prepared_work"
65
- ["prepared_bundle_ref", "prepared_bundle_sha256", registered.fetch("prepared_bundle_bytes")]
66
- else
67
- ["queue_ref", "queue_sha256", PreparedWork::MAX_TOTAL]
68
- end
69
- bytes = @journal.bounded_blob(source.fetch(path_key), commit: commit, max_bytes: size)
70
- unless Digest::SHA256.hexdigest(bytes) == source.fetch(digest_key)
71
- raise AttemptErrors::EvidenceUnavailable, "ordinary snapshot bytes differ from accepted selection"
72
- end
73
- authorize!(request, peer, role, deadline)
74
- {data: {"project_id" => @project, "assignment_id" => params.fetch("assignment_id"), "kind" => kind,
75
- "attempt_id" => params.fetch("attempt_id"), "generation" => @journal.authority_generation(selected_events),
76
- "scope" => selected_events.find { |event| event["type"] == "intent" }&.dig("payload", "scope"),
77
- "journal_commit" => commit, "sha256" => source.fetch(digest_key), "bytes" => bytes.bytesize,
78
- "prepared_work" => registered.fetch("prepared_work"), "definition_digest" => registered.fetch("definition_digest")},
79
- transfer_parts: [bytes], replayed: false}
80
- end
81
-
82
- def progress(request, peer, role, deadline, transfer)
83
- validate_progress!(request)
84
- unless %i[supervisor worker].include?(role)
85
- raise AttemptErrors::UnauthorizedIdentity, "ordinary progress requires its original worker"
86
- end
87
- params, operation = request.fetch("params"), request.fetch("operation")
88
- report = if operation == "queue_finish"
89
- raise ArgumentError, "ordinary progress report needs one bounded part" unless transfer && transfer.count == 1
90
- bytes = transfer.bytes.dup.force_encoding(Encoding::UTF_8)
91
- unless bytes.bytesize.between?(1, 65_536) && bytes.valid_encoding? && !bytes.include?("\0")
92
- raise ArgumentError, "ordinary progress report must be bounded UTF-8"
93
- end
94
- bytes
95
- elsif transfer
96
- raise ArgumentError, "ordinary progress operation forbids transfer"
97
- end
98
- commit = @journal.ref_value
99
- registered = registration(params.fetch("assignment_id"), commit: commit)
100
- raise AttemptErrors::NotFound, "ordinary work is not registered" unless registered
101
- work = registered_work(registered, commit, deadline)
102
- digest = Atoms::EvidenceDigest.digest({"params" => params, "project_id" => @project, "peer_uid" => peer.fetch("uid")})
103
- keys = [@exclusion.task_key(registered.fetch("task_id")), @exclusion.assignment_key(params.fetch("assignment_id"))]
104
- @exclusion.with_shared_multi(keys, deadline: deadline) do
105
- authorize!(request, peer, role, deadline)
106
- @journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
107
- mutation_id: request.fetch("mutation_id"), operation: operation, parameters_digest: digest,
108
- expected_generation: params.fetch("expected_generation"), with_replay: true, deadline: deadline,
109
- replay_authorizer: -> { authorize_start_replay!(request, peer, role, deadline) }) do |events, prefix, _generation|
110
- authorize_start_replay!(request, peer, role, deadline)
111
- unless Molecules::CanonicalAttemptState.derive(events) == "running"
112
- raise AttemptErrors::Conflict, "ordinary progress requires the original running attempt"
113
- end
114
- record = current_queue_record(registered, prefix)
115
- scope = events.find { |event| event["type"] == "intent" }.fetch("payload").fetch("scope")
116
- queue = OrdinaryQueue.new(work: work, root: @input_root, bytes: queue_bytes(record, prefix))
117
- next_queue = case operation
118
- when "queue_start" then queue.transition(operation: :start, scope: scope, step: params.fetch("step"))
119
- when "queue_finish" then queue.transition(operation: :finish, scope: scope, step: params.fetch("step"), report: report)
120
- when "queue_fail" then queue.transition(operation: :fail, scope: scope, message: params.fetch("message"))
121
- end
122
- sha = Digest::SHA256.hexdigest(next_queue.bytes)
123
- path = "execution/definitions/#{params.fetch('assignment_id')}-queue-#{sha}.json"
124
- {events: [], blobs: {path => next_queue.bytes}, data: {
125
- "authority" => "ordinary", "assignment_id" => params.fetch("assignment_id"), "attempt_id" => params.fetch("attempt_id"),
126
- "project_id" => @project, "scope" => scope, "queue_ref" => path, "queue_sha256" => sha, "state" => "running",
127
- "queue_complete" => next_queue.status.subtree_complete?(scope)}}
128
- end
129
- end
130
- end
131
-
132
- def current_queue_record(registered, commit)
133
- assignment = registered.fetch("assignment_id")
134
- events = @journal.read_events(assignment, commit: commit)
135
- events.group_by { |event| event.fetch("attempt_id") }.each_value do |chain|
136
- raise AttemptErrors::EvidenceUnavailable, "ordinary queue history is corrupt" unless Models::EvidenceEvent.chain_valid?(chain)
137
- end
138
- updates = events.select do |entry|
139
- entry["type"] == "authority_mutation" &&
140
- %w[start_attempt queue_start queue_finish queue_fail].include?(entry.dig("payload", "operation"))
141
- end
142
- return registered if updates.empty?
143
- event = @journal.latest_event!(assignment_id: assignment, event_digests: updates.map { |entry| entry.fetch("digest") }, commit: commit)
144
- record = event.dig("payload", "data")
145
- unless record && record.values_at("authority", "assignment_id", "attempt_id", "project_id") ==
146
- ["ordinary", assignment, event.fetch("attempt_id"), @project]
147
- raise AttemptErrors::EvidenceUnavailable, "ordinary canonical queue is unavailable"
148
- end
149
- record
150
- end
151
-
152
- def queue_bytes(record, commit)
153
- bytes = @journal.bounded_blob(record.fetch("queue_ref"), commit: commit, max_bytes: PreparedWork::MAX_TOTAL)
154
- unless Digest::SHA256.hexdigest(bytes) == record.fetch("queue_sha256")
155
- raise AttemptErrors::EvidenceUnavailable, "ordinary canonical queue bytes differ"
156
- end
157
- bytes
158
- end
159
- end
160
- end
161
- end
162
- end