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,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