ace-assign 0.71.0 → 0.73.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 (195) 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 +3 -2119
  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/organisms/assignment_executor.rb +0 -6
  35. data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
  36. data/lib/ace/assign/version.rb +1 -1
  37. data/lib/ace/assign.rb +0 -6
  38. metadata +4 -176
  39. data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
  40. data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
  41. data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
  42. data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
  43. data/docs/protected-authority.md +0 -282
  44. data/exe/ace-scope-ready +0 -55
  45. data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
  46. data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
  47. data/lib/ace/assign/atoms/candidate_limits.rb +0 -20
  48. data/lib/ace/assign/atoms/delivery_parameters.rb +0 -19
  49. data/lib/ace/assign/atoms/evidence_digest.rb +0 -78
  50. data/lib/ace/assign/atoms/fixed_merge_schemas.rb +0 -398
  51. data/lib/ace/assign/atoms/git_blob_batch.rb +0 -59
  52. data/lib/ace/assign/authority/assignment_definition.rb +0 -35
  53. data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
  54. data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
  55. data/lib/ace/assign/authority/campaign_execution.rb +0 -119
  56. data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
  57. data/lib/ace/assign/authority/candidate_blob_batch.rb +0 -55
  58. data/lib/ace/assign/authority/candidate_head_comparison.rb +0 -139
  59. data/lib/ace/assign/authority/candidate_transfer.rb +0 -357
  60. data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
  61. data/lib/ace/assign/authority/client.rb +0 -257
  62. data/lib/ace/assign/authority/command_execution.rb +0 -180
  63. data/lib/ace/assign/authority/deployment.rb +0 -627
  64. data/lib/ace/assign/authority/deployment_history.rb +0 -146
  65. data/lib/ace/assign/authority/endcap.rb +0 -457
  66. data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
  67. data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
  68. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
  69. data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
  70. data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
  71. data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
  72. data/lib/ace/assign/authority/endcap_publication.rb +0 -41
  73. data/lib/ace/assign/authority/endcap_results.rb +0 -398
  74. data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
  75. data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
  76. data/lib/ace/assign/authority/endcap_service_context.rb +0 -38
  77. data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
  78. data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
  79. data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
  80. data/lib/ace/assign/authority/fixed_publication_lifecycle.rb +0 -176
  81. data/lib/ace/assign/authority/fixed_service_lifecycle.rb +0 -594
  82. data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
  83. data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
  84. data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
  85. data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
  86. data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
  87. data/lib/ace/assign/authority/launch_driver.rb +0 -393
  88. data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
  89. data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
  90. data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
  91. data/lib/ace/assign/authority/launch_review.rb +0 -91
  92. data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
  93. data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
  94. data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
  95. data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
  96. data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
  97. data/lib/ace/assign/authority/launch_steering.rb +0 -353
  98. data/lib/ace/assign/authority/launch_stop.rb +0 -254
  99. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
  100. data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
  101. data/lib/ace/assign/authority/ordinary_client.rb +0 -70
  102. data/lib/ace/assign/authority/ordinary_completion.rb +0 -104
  103. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
  104. data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -227
  105. data/lib/ace/assign/authority/ordinary_merge_evidence.rb +0 -81
  106. data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
  107. data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
  108. data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
  109. data/lib/ace/assign/authority/ordinary_release_artifacts.rb +0 -143
  110. data/lib/ace/assign/authority/ordinary_results.rb +0 -206
  111. data/lib/ace/assign/authority/ordinary_review_evidence.rb +0 -180
  112. data/lib/ace/assign/authority/ordinary_reviews.rb +0 -214
  113. data/lib/ace/assign/authority/ordinary_server.rb +0 -75
  114. data/lib/ace/assign/authority/ordinary_supervisor.rb +0 -99
  115. data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
  116. data/lib/ace/assign/authority/posix_acl.rb +0 -59
  117. data/lib/ace/assign/authority/prepared_input.rb +0 -130
  118. data/lib/ace/assign/authority/prepared_queue.rb +0 -185
  119. data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
  120. data/lib/ace/assign/authority/prepared_work.rb +0 -309
  121. data/lib/ace/assign/authority/prepared_worker.rb +0 -69
  122. data/lib/ace/assign/authority/private_directory.rb +0 -33
  123. data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
  124. data/lib/ace/assign/authority/readiness_hook.rb +0 -120
  125. data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
  126. data/lib/ace/assign/authority/release_artifact_manifest.rb +0 -91
  127. data/lib/ace/assign/authority/retained_input.rb +0 -53
  128. data/lib/ace/assign/authority/router.rb +0 -81
  129. data/lib/ace/assign/authority/server.rb +0 -131
  130. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
  131. data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
  132. data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
  133. data/lib/ace/assign/authority/service_evidence.rb +0 -286
  134. data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
  135. data/lib/ace/assign/authority/socket_server.rb +0 -255
  136. data/lib/ace/assign/authority/task_context_entry.rb +0 -32
  137. data/lib/ace/assign/authority/transfer_codec.rb +0 -189
  138. data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
  139. data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
  140. data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
  141. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
  142. data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
  143. data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
  144. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
  145. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
  146. data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
  147. data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
  148. data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
  149. data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
  150. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
  151. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
  152. data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
  153. data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
  154. data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
  155. data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
  156. data/lib/ace/assign/cli/commands/delivery.rb +0 -107
  157. data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
  158. data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
  159. data/lib/ace/assign/cli/commands/resume.rb +0 -38
  160. data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
  161. data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
  162. data/lib/ace/assign/models/attempt.rb +0 -178
  163. data/lib/ace/assign/models/attempt_binding.rb +0 -116
  164. data/lib/ace/assign/models/evidence_event.rb +0 -86
  165. data/lib/ace/assign/models/execution_receipt.rb +0 -139
  166. data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
  167. data/lib/ace/assign/molecules/attempt_store.rb +0 -201
  168. data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
  169. data/lib/ace/assign/molecules/canonical_evidence.rb +0 -170
  170. data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
  171. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
  172. data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
  173. data/lib/ace/assign/molecules/evidence_journal.rb +0 -1899
  174. data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
  175. data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
  176. data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
  177. data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
  178. data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
  179. data/lib/ace/assign/molecules/journal_mutation.rb +0 -274
  180. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
  181. data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
  182. data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
  183. data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
  184. data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
  185. data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
  186. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
  187. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
  188. data/lib/ace/assign/molecules/receipt_verifier.rb +0 -320
  189. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
  190. data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1374
  191. data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
  192. data/lib/ace/assign/organisms/ordinary_driver.rb +0 -310
  193. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
  194. data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
  195. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
@@ -1,115 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "tmpdir"
4
- require "fileutils"
5
- require_relative "prepared_work"
6
- require_relative "private_directory"
7
- require_relative "../organisms/assignment_executor"
8
-
9
- module Ace
10
- module Assign
11
- module Authority
12
- # A disposable projection for the existing queue engine. Only bytes read
13
- # from (or about to be committed to) the canonical execution ref enter it.
14
- # No files left by an earlier coordinator are recovery input.
15
- class OrdinaryQueue
16
- attr_reader :bytes
17
-
18
- def initialize(work:, root:, bytes: nil)
19
- raise ArgumentError, "ordinary queue requires prepared work" unless work.is_a?(PreparedWork)
20
- @work, @root = work, PrivateDirectory.verify!(root)
21
- value = if bytes
22
- invalid! unless bytes.is_a?(String) && bytes.bytesize.between?(1, PreparedWork::MAX_TOTAL)
23
- JSON.parse(bytes)
24
- else
25
- {"version" => 1, "steps" => work.manifest.fetch("steps").to_h { |step|
26
- name = step.fetch("filename"); [name, work.files.fetch("steps/#{name}")]
27
- }, "reports" => {}}
28
- end
29
- validate!(value, initial: bytes.nil?)
30
- @bytes = Atoms::EvidenceDigest.canonical_json(value).freeze
31
- rescue JSON::ParserError
32
- invalid!
33
- end
34
-
35
- def status
36
- value = JSON.parse(@bytes)
37
- assignment = Models::Assignment.from_h(@work.definition, cache_dir: File.join(@root, @work.manifest.fetch("assignment_id")))
38
- Molecules::QueueScanner.new.scan_captured(steps: value.fetch("steps"), reports: value.fetch("reports"), assignment: assignment)
39
- end
40
-
41
- # This returns proposed bytes only. The lifecycle owns authorization,
42
- # generation, replay and publication together with its attempt events.
43
- def transition(operation:, scope: @work.manifest.fetch("scope"), step: nil, report: nil, message: nil)
44
- invalid! unless @work.manifest.fetch("steps").any? { |entry| entry.fetch("number") == scope }
45
- value = JSON.parse(@bytes)
46
- Dir.mktmpdir("ordinary-queue-", @root) do |scratch|
47
- File.chmod(0o700, scratch)
48
- directory = File.join(scratch, @work.manifest.fetch("assignment_id"))
49
- %w[steps reports].each { |name| FileUtils.mkdir_p(File.join(directory, name), mode: 0700) }
50
- assignment = Models::Assignment.from_h(@work.definition, cache_dir: directory)
51
- write(File.join(directory, "assignment.yaml"), assignment.to_h.to_yaml)
52
- write(File.join(directory, "job.yaml"), @work.files.fetch("job.yaml"))
53
- %w[steps reports].each do |group|
54
- value.fetch(group).each { |filename, content| write(File.join(directory, group, filename), content) }
55
- end
56
- executor = Organisms::AssignmentExecutor.new(cache_base: scratch, selected_assignment: assignment)
57
- case operation
58
- when :activate
59
- state = executor.status.fetch(:state)
60
- invalid! unless state.find_by_number(scope)&.status == :pending
61
- executor.step_writer.mark_active(state.find_by_number(scope).file_path)
62
- when :start
63
- executor.start_step(step_number: step, fork_root: scope)
64
- when :finish
65
- executor.finish_step(report_content: report, step_number: step, fork_root: scope)
66
- when :fail
67
- executor.fail(message, fork_root: scope)
68
- else
69
- raise ArgumentError, "unknown ordinary queue transition"
70
- end
71
- result = {"version" => 1}
72
- %w[steps reports].each do |group|
73
- result[group] = Dir.children(File.join(directory, group)).to_h do |filename|
74
- [filename, File.binread(File.join(directory, group, filename)).force_encoding(Encoding::UTF_8)]
75
- end
76
- end
77
- self.class.new(work: @work, root: @root, bytes: Atoms::EvidenceDigest.canonical_json(result))
78
- end
79
- end
80
-
81
- private
82
-
83
- def validate!(value, initial:)
84
- invalid! unless value.is_a?(Hash) && value.keys.sort == %w[reports steps version] && value["version"].is_a?(Integer) && value["version"] == 1 &&
85
- value["steps"].is_a?(Hash) && value["reports"].is_a?(Hash)
86
- expected = @work.manifest.fetch("steps").map { |step| step.fetch("filename") }
87
- invalid! unless value.fetch("steps").keys.sort == expected.sort
88
- reports = []
89
- value.fetch("steps").each do |filename, content|
90
- parsed = @work.parse_queue_step!(content)
91
- progress = parsed.fetch(:frontmatter).keys & PreparedWork::PROGRESS
92
- allowed = initial ? %w[status] : %w[status started_at completed_at error stall_reason]
93
- invalid! unless (progress - allowed).empty?
94
- original = @work.files.fetch("steps/#{filename}")
95
- invalid! unless PreparedWork.work_projection(content) == PreparedWork.work_projection(original) &&
96
- %w[pending active done failed].include?(parsed.fetch(:frontmatter).fetch("status", "pending"))
97
- info = Atoms::StepFileParser.parse_filename(filename)
98
- reports << Atoms::StepFileParser.generate_report_filename(info.fetch(:number), info.fetch(:name))
99
- end
100
- invalid! unless (value.fetch("reports").keys - reports).empty?
101
- value.fetch("reports").each_value { |content| @work.parse_queue_step!(content) }
102
- invalid! if Atoms::EvidenceDigest.canonical_json(value).bytesize > PreparedWork::MAX_TOTAL
103
- end
104
-
105
- def write(path, content)
106
- File.open(path, File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW, 0600) { |file| file.write(content) }
107
- end
108
-
109
- def invalid!
110
- raise AttemptErrors::EvidenceUnavailable, "ordinary canonical queue differs from its accepted work"
111
- end
112
- end
113
- end
114
- end
115
- end
@@ -1,143 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "release_artifact_manifest"
4
- require_relative "candidate_transfer"
5
-
6
- module Ace
7
- module Assign
8
- module Authority
9
- # Retention is a canonical input, never a publication permit or result.
10
- # Native/candidate/review and release policy are rechecked at every CAS.
11
- module OrdinaryReleaseArtifacts
12
- RELEASE_OPERATIONS = %w[retain_release_artifact].freeze
13
- RELEASE_TRANSFERS = {"retain_release_artifact" =>
14
- {direction: :upload, purpose: :release_artifact, roles: %i[worker supervisor]}}.freeze
15
- RELEASE_PARAMETERS = %w[assignment_id attempt_id head candidate_generation pane expected_generation
16
- authorization manifest transfer].freeze
17
-
18
- # Read an accepted import at the current immutable canonical prefix only.
19
- # Historical commit selection is not supported by the native origin reader.
20
- # The later effect consumer must independently admit its fresh scope,
21
- # candidate, review, authorization and fixed executor before dispatch.
22
- def release_artifact_at!(events:, commit:, artifact_id:)
23
- token!(artifact_id)
24
- unless commit && Models::EvidenceEvent.chain_valid?(events)
25
- raise AttemptErrors::EvidenceUnavailable, "release artifact history is unavailable"
26
- end
27
- accepted = events.select { |event| event["type"] == "authority_mutation" &&
28
- event.dig("payload", "operation") == "retain_release_artifact" && event.dig("payload", "mutation_id") == artifact_id }
29
- data = accepted.one? && accepted.first.dig("payload", "data")
30
- unless data && data["artifact_id"] == artifact_id && data["input_digest"] == Atoms::EvidenceDigest.digest(data.fetch("input"))
31
- raise AttemptErrors::EvidenceUnavailable, "release artifact acceptance is unavailable"
32
- end
33
- origin = data.fetch("producer_origin")
34
- retained = @journal.read_events(origin.fetch("assignment_id"), commit: commit)
35
- .select { |event| event["attempt_id"] == origin.fetch("attempt_id") }
36
- unless retained == events && ordinary_origin_at!(events, commit, running: false) == origin
37
- raise AttemptErrors::EvidenceUnavailable, "release artifact original scope differs"
38
- end
39
- context = release_evidence_context(data.fetch("input"), origin, data.fetch("authorization"), artifact_id)
40
- bytes = Molecules::CanonicalEvidence.new(journal: @journal).read(data.fetch("artifact"), **context, commit: commit)
41
- unless bytes.bytesize == data.dig("input", "publication", "artifact_bytes") &&
42
- Digest::SHA256.hexdigest(bytes) == data.dig("input", "target", "artifact_digest")
43
- raise AttemptErrors::EvidenceUnavailable, "release artifact retained bytes differ"
44
- end
45
- data.merge("bytes" => bytes)
46
- rescue KeyError, TypeError
47
- raise AttemptErrors::EvidenceUnavailable, "release artifact acceptance is incomplete"
48
- end
49
-
50
- private
51
-
52
- def validate_ordinary_release!(request)
53
- params = request.fetch("params")
54
- strict!(params, RELEASE_PARAMETERS)
55
- %w[assignment_id attempt_id authorization].each { |key| token!(params.fetch(key)) }
56
- token!(request.fetch("mutation_id"))
57
- unless params["head"].is_a?(String) && CandidateTransfer::SHA.match?(params["head"]) &&
58
- %w[candidate_generation expected_generation].all? { |key| params[key].is_a?(Integer) && params[key].positive? }
59
- raise ArgumentError, "invalid release artifact candidate selection"
60
- end
61
- ReleaseArtifactManifest.validate!(params.fetch("manifest"))
62
- end
63
-
64
- def authorize_ordinary_release!(request, peer, role, deadline)
65
- validate_ordinary_release!(request)
66
- params = request.fetch("params")
67
- with_scope(assignment: params.fetch("assignment_id"), attempt: params.fetch("attempt_id"), deadline: deadline) do
68
- prefix = @journal.ref_value
69
- release_context_at!(request, peer, role, deadline, result_events(params, prefix), prefix)
70
- end
71
- end
72
-
73
- # Caller holds lifecycle exclusion before entering the journal writer.
74
- # This method never acquires another lifecycle lock or trusts a path.
75
- def release_context_at!(request, peer, role, deadline, events, commit)
76
- authorize!(request, peer, role, deadline)
77
- unless %i[worker supervisor].include?(role) && @release_authorizer
78
- raise AttemptErrors::UnauthorizedIdentity, "installed release artifact authorization is unavailable"
79
- end
80
- params = request.fetch("params")
81
- origin = origin_at!(events: events, commit: commit)
82
- current = candidate_at!(events: events, commit: commit, head: params.fetch("head"), generation: params.fetch("candidate_generation"))
83
- reference = candidate_reference_at!(events: events, commit: commit, head: params.fetch("head"), generation: params.fetch("candidate_generation"))
84
- observed = @admission.execution!(peer: peer, pane: params.fetch("pane"), head: current.fetch("head"), deadline: deadline)
85
- unless observed.fetch(:identity).runtime_binding == origin.fetch("runtime_binding")
86
- raise AttemptErrors::UnauthorizedIdentity, "release artifact producer native identity changed"
87
- end
88
- review_at!(events: events, commit: commit, head: current.fetch("head"), generation: current.fetch("candidate_generation"))
89
- input = ReleaseArtifactManifest.publication_input(manifest: params.fetch("manifest"), candidate: current, reference: reference)
90
- binding = params.slice("assignment_id", "attempt_id", "authorization").merge(
91
- "operation" => "publish", "project_id" => @project, "candidate_head" => current.fetch("head"),
92
- "caller_uid" => origin.dig("runtime_binding", "process_identity", "uid"),
93
- "input_digest" => Atoms::EvidenceDigest.digest(input), "target" => input.fetch("target"))
94
- unless @release_authorizer.call(binding)
95
- raise AttemptErrors::UnauthorizedIdentity, "release artifact manifest is not authorized"
96
- end
97
- {input: input, binding: binding, origin: origin}
98
- rescue SecurityError
99
- raise AttemptErrors::UnauthorizedIdentity, "release artifact manifest authorization is unavailable"
100
- end
101
-
102
- def retain_release_artifact(request, peer, role, deadline, transfer)
103
- validate_ordinary_release!(request)
104
- unless transfer && transfer.count == 1
105
- raise AttemptErrors::MalformedTransfer, "release artifact requires its one bounded binary transfer"
106
- end
107
- params = request.fetch("params")
108
- with_scope(assignment: params.fetch("assignment_id"), attempt: params.fetch("attempt_id"), deadline: deadline) do
109
- prefix = @journal.ref_value
110
- release_context_at!(request, peer, role, deadline, result_events(params, prefix), prefix)
111
- bytes = transfer.bytes.dup.freeze
112
- ReleaseArtifactManifest.verify!(bytes, params.fetch("manifest"))
113
- digest = Atoms::EvidenceDigest.digest("project_id" => @project, "params" => params)
114
- recheck = lambda do
115
- current = @journal.ref_value
116
- release_context_at!(request, peer, role, deadline, result_events(params, current), current)
117
- end
118
- @journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
119
- mutation_id: request.fetch("mutation_id"), operation: "retain_release_artifact", parameters_digest: digest,
120
- expected_generation: params.fetch("expected_generation"), with_replay: true, deadline: deadline,
121
- replay_authorizer: recheck) do |events, commit, _generation|
122
- selected = release_context_at!(request, peer, role, deadline, events, commit)
123
- artifact_id = request.fetch("mutation_id")
124
- context = release_evidence_context(selected.fetch(:input), selected.fetch(:origin), params.fetch("authorization"), artifact_id)
125
- plan = Molecules::CanonicalEvidence.new(journal: @journal).import_plan(**context,
126
- artifacts: [bytes], admitted_after_event_digest: events.last.fetch("digest"))
127
- plan.merge(data: {"artifact_id" => artifact_id, "input" => selected.fetch(:input),
128
- "input_digest" => selected.fetch(:binding).fetch("input_digest"), "artifact" => plan.fetch(:references).first,
129
- "authorization" => params.fetch("authorization"), "producer_origin" => selected.fetch(:origin)})
130
- end
131
- end
132
- end
133
-
134
- def release_evidence_context(input, origin, authorization, artifact_id)
135
- {kind: "release-artifact", project_id: @project, assignment_id: origin.fetch("assignment_id"),
136
- attempt_id: origin.fetch("attempt_id"), peer_uid: origin.dig("runtime_binding", "process_identity", "uid"),
137
- binding: {"input" => input, "producer_origin" => origin, "authorization" => authorization},
138
- request_id_or_event_id: artifact_id, generation: input.fetch("publication").fetch("candidate_reference").fetch("candidate_generation")}
139
- end
140
- end
141
- end
142
- end
143
- end
@@ -1,206 +0,0 @@
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