ace-assign 0.70.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 (181) 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 -2099
  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 -1
  16. data/handbook/workflow-instructions/assign/drive.wf.md +47 -118
  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 +2 -1
  35. data/lib/ace/assign/organisms/assignment_executor.rb +0 -6
  36. data/lib/ace/assign/organisms/task_assignment_creator.rb +3 -8
  37. data/lib/ace/assign/version.rb +1 -1
  38. data/lib/ace/assign.rb +0 -6
  39. metadata +4 -161
  40. data/.ace-defaults/assign/presets/close-delivery.yml +0 -57
  41. data/.ace-defaults/assign/presets/deliver-release.yml +0 -60
  42. data/.ace-defaults/assign/presets/integrate-batch.yml +0 -72
  43. data/.ace-defaults/assign/presets/supervise-delivery.yml +0 -69
  44. data/docs/protected-authority.md +0 -282
  45. data/exe/ace-scope-ready +0 -55
  46. data/lib/ace/assign/atoms/assignment_scope.rb +0 -58
  47. data/lib/ace/assign/atoms/attempt_state_machine.rb +0 -84
  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/authority/assignment_definition.rb +0 -35
  52. data/lib/ace/assign/authority/assignment_inventory.rb +0 -290
  53. data/lib/ace/assign/authority/campaign_consumer_policy.rb +0 -100
  54. data/lib/ace/assign/authority/campaign_execution.rb +0 -119
  55. data/lib/ace/assign/authority/campaign_round_transfer.rb +0 -82
  56. data/lib/ace/assign/authority/candidate_transfer.rb +0 -327
  57. data/lib/ace/assign/authority/cleanup_result_protection.rb +0 -53
  58. data/lib/ace/assign/authority/client.rb +0 -257
  59. data/lib/ace/assign/authority/deployment.rb +0 -627
  60. data/lib/ace/assign/authority/deployment_history.rb +0 -146
  61. data/lib/ace/assign/authority/endcap.rb +0 -455
  62. data/lib/ace/assign/authority/endcap_attempts.rb +0 -274
  63. data/lib/ace/assign/authority/endcap_campaign_results.rb +0 -232
  64. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +0 -266
  65. data/lib/ace/assign/authority/endcap_contexts.rb +0 -23
  66. data/lib/ace/assign/authority/endcap_inboxes.rb +0 -37
  67. data/lib/ace/assign/authority/endcap_prepared_work.rb +0 -80
  68. data/lib/ace/assign/authority/endcap_publication.rb +0 -207
  69. data/lib/ace/assign/authority/endcap_results.rb +0 -398
  70. data/lib/ace/assign/authority/endcap_review_requests.rb +0 -183
  71. data/lib/ace/assign/authority/endcap_reviews.rb +0 -130
  72. data/lib/ace/assign/authority/endcap_service_settlement.rb +0 -152
  73. data/lib/ace/assign/authority/endcap_services.rb +0 -598
  74. data/lib/ace/assign/authority/evidence_transfer.rb +0 -67
  75. data/lib/ace/assign/authority/execution_scope_observation.rb +0 -903
  76. data/lib/ace/assign/authority/inbox_context_original.rb +0 -113
  77. data/lib/ace/assign/authority/launch_attempt_consumers.rb +0 -51
  78. data/lib/ace/assign/authority/launch_campaign_registration.rb +0 -214
  79. data/lib/ace/assign/authority/launch_control_channel.rb +0 -313
  80. data/lib/ace/assign/authority/launch_control_exclusions.rb +0 -127
  81. data/lib/ace/assign/authority/launch_driver.rb +0 -393
  82. data/lib/ace/assign/authority/launch_input_inhibition.rb +0 -132
  83. data/lib/ace/assign/authority/launch_lifecycle.rb +0 -997
  84. data/lib/ace/assign/authority/launch_prepared_work.rb +0 -122
  85. data/lib/ace/assign/authority/launch_review.rb +0 -91
  86. data/lib/ace/assign/authority/launch_scope_abort.rb +0 -81
  87. data/lib/ace/assign/authority/launch_scope_admission.rb +0 -215
  88. data/lib/ace/assign/authority/launch_scope_close.rb +0 -146
  89. data/lib/ace/assign/authority/launch_scope_parent.rb +0 -44
  90. data/lib/ace/assign/authority/launch_scope_release.rb +0 -859
  91. data/lib/ace/assign/authority/launch_steering.rb +0 -353
  92. data/lib/ace/assign/authority/launch_stop.rb +0 -254
  93. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +0 -229
  94. data/lib/ace/assign/authority/ordinary_attempt_start.rb +0 -137
  95. data/lib/ace/assign/authority/ordinary_client.rb +0 -70
  96. data/lib/ace/assign/authority/ordinary_completion.rb +0 -94
  97. data/lib/ace/assign/authority/ordinary_effect_settlement.rb +0 -118
  98. data/lib/ace/assign/authority/ordinary_lifecycle.rb +0 -191
  99. data/lib/ace/assign/authority/ordinary_peer.rb +0 -54
  100. data/lib/ace/assign/authority/ordinary_progress.rb +0 -162
  101. data/lib/ace/assign/authority/ordinary_queue.rb +0 -115
  102. data/lib/ace/assign/authority/ordinary_results.rb +0 -196
  103. data/lib/ace/assign/authority/ordinary_server.rb +0 -75
  104. data/lib/ace/assign/authority/original_inbox_selection.rb +0 -41
  105. data/lib/ace/assign/authority/posix_acl.rb +0 -59
  106. data/lib/ace/assign/authority/prepared_input.rb +0 -130
  107. data/lib/ace/assign/authority/prepared_queue.rb +0 -185
  108. data/lib/ace/assign/authority/prepared_task_context.rb +0 -116
  109. data/lib/ace/assign/authority/prepared_work.rb +0 -304
  110. data/lib/ace/assign/authority/prepared_worker.rb +0 -69
  111. data/lib/ace/assign/authority/private_directory.rb +0 -33
  112. data/lib/ace/assign/authority/protected_assignment_context.rb +0 -226
  113. data/lib/ace/assign/authority/readiness_hook.rb +0 -120
  114. data/lib/ace/assign/authority/receipt_transfer.rb +0 -48
  115. data/lib/ace/assign/authority/router.rb +0 -81
  116. data/lib/ace/assign/authority/server.rb +0 -131
  117. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +0 -88
  118. data/lib/ace/assign/authority/service_cleanup_evidence.rb +0 -180
  119. data/lib/ace/assign/authority/service_delivery_evidence.rb +0 -101
  120. data/lib/ace/assign/authority/service_evidence.rb +0 -286
  121. data/lib/ace/assign/authority/service_publication_evidence.rb +0 -110
  122. data/lib/ace/assign/authority/socket_server.rb +0 -255
  123. data/lib/ace/assign/authority/task_context_entry.rb +0 -32
  124. data/lib/ace/assign/authority/transfer_codec.rb +0 -187
  125. data/lib/ace/assign/cli/commands/attempt/base.rb +0 -84
  126. data/lib/ace/assign/cli/commands/attempt/evidence.rb +0 -28
  127. data/lib/ace/assign/cli/commands/attempt/finish.rb +0 -51
  128. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +0 -49
  129. data/lib/ace/assign/cli/commands/attempt/start.rb +0 -45
  130. data/lib/ace/assign/cli/commands/attempt/status.rb +0 -54
  131. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +0 -25
  132. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +0 -28
  133. data/lib/ace/assign/cli/commands/authority/launch.rb +0 -97
  134. data/lib/ace/assign/cli/commands/authority/serve.rb +0 -31
  135. data/lib/ace/assign/cli/commands/authority/status.rb +0 -26
  136. data/lib/ace/assign/cli/commands/authority/task_context.rb +0 -31
  137. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +0 -25
  138. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +0 -30
  139. data/lib/ace/assign/cli/commands/authority/terminate.rb +0 -29
  140. data/lib/ace/assign/cli/commands/authority/worker.rb +0 -23
  141. data/lib/ace/assign/cli/commands/campaign_export_result.rb +0 -55
  142. data/lib/ace/assign/cli/commands/campaign_record_round.rb +0 -57
  143. data/lib/ace/assign/cli/commands/delivery.rb +0 -107
  144. data/lib/ace/assign/cli/commands/inbox_bind.rb +0 -34
  145. data/lib/ace/assign/cli/commands/protected_submission.rb +0 -61
  146. data/lib/ace/assign/cli/commands/resume.rb +0 -38
  147. data/lib/ace/assign/cli/commands/submit_candidate.rb +0 -34
  148. data/lib/ace/assign/cli/commands/submit_result.rb +0 -61
  149. data/lib/ace/assign/models/attempt.rb +0 -178
  150. data/lib/ace/assign/models/attempt_binding.rb +0 -116
  151. data/lib/ace/assign/models/evidence_event.rb +0 -86
  152. data/lib/ace/assign/models/execution_receipt.rb +0 -139
  153. data/lib/ace/assign/molecules/attempt_reconciler.rb +0 -127
  154. data/lib/ace/assign/molecules/attempt_store.rb +0 -201
  155. data/lib/ace/assign/molecules/canonical_attempt_state.rb +0 -151
  156. data/lib/ace/assign/molecules/canonical_evidence.rb +0 -153
  157. data/lib/ace/assign/molecules/canonical_read_commands.rb +0 -84
  158. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +0 -313
  159. data/lib/ace/assign/molecules/evidence_calculator.rb +0 -237
  160. data/lib/ace/assign/molecules/evidence_journal.rb +0 -1912
  161. data/lib/ace/assign/molecules/execution_identity_resolver.rb +0 -137
  162. data/lib/ace/assign/molecules/execution_scope_lineage.rb +0 -345
  163. data/lib/ace/assign/molecules/fixed_effect_reference.rb +0 -31
  164. data/lib/ace/assign/molecules/installed_journal_read.rb +0 -143
  165. data/lib/ace/assign/molecules/journal_input_inhibition.rb +0 -78
  166. data/lib/ace/assign/molecules/journal_mutation.rb +0 -273
  167. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +0 -241
  168. data/lib/ace/assign/molecules/ordinary_merge_cli.rb +0 -68
  169. data/lib/ace/assign/molecules/ordinary_merge_contract.rb +0 -410
  170. data/lib/ace/assign/molecules/original_launch_binding.rb +0 -37
  171. data/lib/ace/assign/molecules/proposal_journal.rb +0 -179
  172. data/lib/ace/assign/molecules/protected_control_exclusion.rb +0 -303
  173. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +0 -330
  174. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +0 -164
  175. data/lib/ace/assign/molecules/receipt_verifier.rb +0 -314
  176. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +0 -88
  177. data/lib/ace/assign/organisms/attempt_coordinator.rb +0 -1337
  178. data/lib/ace/assign/organisms/delivery_coordinator.rb +0 -342
  179. data/lib/ace/assign/organisms/ordinary_merge_consumer.rb +0 -365
  180. data/lib/ace/assign/organisms/prepared_work_builder.rb +0 -194
  181. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +0 -100
@@ -1,314 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "digest"
4
- require "json"
5
-
6
- module Ace
7
- module Assign
8
- module Molecules
9
- # Canonical receipt validation for accepted execution evidence.
10
- #
11
- # Verifies schema and field hygiene, attempt binding, producer
12
- # attribution, transition authority, current-head candidate binding,
13
- # artifact digests, executed checks, and independent review verdicts.
14
- # Every failure is a rejection with an actionable reason; a receipt is
15
- # never partially accepted.
16
- class ReceiptVerifier
17
- # Operations that act outside the local repository once executed.
18
- EXTERNAL_EFFECT_OPERATIONS = %w[merge publish deploy release].freeze
19
-
20
- REVIEW_OPERATION = "review"
21
- HEAD_PATTERN = /\A[0-9a-f]{7,64}\z/.freeze
22
- APPROVED_VERDICT = "approved"
23
-
24
- # @param identity_resolver [ExecutionIdentityResolver] Trust boundary
25
- # @param artifact_reader [#call, nil] Source-owned canonical byte
26
- # reader for protected composition; nil selects explicit local files
27
- def initialize(identity_resolver: nil, artifact_reader: nil, campaign_verifier: nil)
28
- @identity_resolver = identity_resolver || ExecutionIdentityResolver.new
29
- unless artifact_reader.nil? || artifact_reader.respond_to?(:call)
30
- raise ArgumentError, "artifact reader must be a source-owned callable"
31
- end
32
- @artifact_reader = artifact_reader
33
- unless campaign_verifier.nil? || campaign_verifier.respond_to?(:call)
34
- raise ArgumentError, "campaign verifier must be a source-owned callable"
35
- end
36
- @campaign_verifier = campaign_verifier
37
- end
38
-
39
- # Validate a submitted receipt against an attempt.
40
- #
41
- # @param data [Hash] Parsed receipt payload
42
- # @param attempt [Models::Attempt] Target attempt (loaded by coordinator)
43
- # @param identity [ExecutionIdentityResolver::Identity] Finishing boundary identity
44
- # @param live_head [String] Current candidate revision (git rev-parse HEAD)
45
- # @param repo_root [String] Project root for artifact resolution
46
- # @return [Models::ExecutionReceipt] Normalized receipt with digest
47
- # @raise [AttemptErrors::ReceiptRejected] on any validation failure
48
- def verify!(data, attempt:, identity:, live_head:, repo_root:)
49
- verify_receipt!(data, attempt: attempt, live_head: live_head, repo_root: repo_root) do
50
- verify_authority(data, identity)
51
- end
52
- end
53
-
54
- # Submission validates evidence without granting terminal acceptance.
55
- # The authenticated owner separately checks the exact worker attribution.
56
- def verify_result!(data, attempt:, live_head:, repo_root:)
57
- verify_receipt!(data, attempt: attempt, live_head: live_head, repo_root: repo_root)
58
- end
59
-
60
- def verify_receipt!(data, attempt:, live_head:, repo_root:)
61
- reject_unless(data.is_a?(Hash), "receipt must be a JSON object")
62
-
63
- if Models::ExecutionReceipt.forbidden_field?(data)
64
- reject("receipt carries forbidden fields (credentials, environment, or terminal output)")
65
- end
66
-
67
- required = %w[attempt_id assignment_id project_id scope operation producer head verdict]
68
- missing = required.select { |key| data[key].nil? || data[key].to_s.strip.empty? }
69
- reject("missing required fields: #{missing.join(', ')}") unless missing.empty?
70
- unless Models::ExecutionReceipt::VERDICTS.include?(data["verdict"].to_s)
71
- reject("unsupported verdict '#{data['verdict']}': must be one of #{Models::ExecutionReceipt::VERDICTS.join(', ')}")
72
- end
73
-
74
- verify_binding(data, attempt)
75
- verify_producer(data)
76
- yield if block_given?
77
- verify_head(data, live_head)
78
- verify_artifacts(data, repo_root) if data["verdict"] == "succeeded"
79
- verify_checks(data)
80
- verify_review(data)
81
- verify_campaign(data, repo_root: repo_root, live_head: live_head)
82
-
83
- receipt = Models::ExecutionReceipt.from_h(data.merge("recorded_at" => Time.now.utc))
84
- verify_digest(receipt)
85
-
86
- receipt
87
- end
88
- private :verify_receipt!
89
-
90
- # Recheck the source artifacts/checks of an already accepted receipt.
91
- # Local historical reads (recorded at a past head) validate acceptance from
92
- # the append-only journal without re-hashing artifacts: the review
93
- # lifecycle legitimately archives and annotates finding files after
94
- # collection, and `.ace-local` working files are disposable, so their
95
- # current existence proves nothing about an accepted past receipt.
96
- # Current-head reads keep full artifact re-verification. Protected
97
- # readers always reverify imported bytes, including historical reads.
98
- def verify_accepted_evidence!(data, live_head:, repo_root:, historical: false)
99
- verify_head(data, live_head)
100
- artifacts = data["artifacts"]
101
- reject_unless(artifacts.is_a?(Array), "accepted artifacts must be an array")
102
- if (@artifact_reader || !historical) && (data["verdict"] == "succeeded" || !artifacts.empty?)
103
- verify_artifacts(data, repo_root)
104
- end
105
- verify_checks(data)
106
- verify_review(data)
107
- verify_campaign(data, repo_root: repo_root, live_head: live_head) if @artifact_reader
108
- end
109
-
110
- # @param operation [String] Receipt operation
111
- # @return [Boolean] True when the operation acts outside the repository
112
- def external_effect?(operation)
113
- EXTERNAL_EFFECT_OPERATIONS.include?(operation.to_s)
114
- end
115
-
116
- private
117
-
118
- def verify_binding(data, attempt)
119
- binding = attempt.binding
120
- unless data["attempt_id"] == attempt.attempt_id
121
- reject("receipt attempt_id #{data['attempt_id']} does not match attempt #{attempt.attempt_id}")
122
- end
123
- unless data["assignment_id"] == binding.assignment_id
124
- reject("receipt assignment #{data['assignment_id']} does not match attempt assignment #{binding.assignment_id}")
125
- end
126
- unless data["project_id"] == binding.project_id
127
- reject("receipt project #{data['project_id']} does not match attempt project #{binding.project_id}")
128
- end
129
- unless Atoms::AssignmentScope.equal?(data["scope"], binding.scope)
130
- reject("receipt scope #{data['scope']} does not match attempt scope #{binding.scope}")
131
- end
132
- end
133
-
134
- def verify_producer(data)
135
- producer = data["producer"]
136
- reject_unless(producer.is_a?(Hash), "producer must be an object with actor, role, runtime")
137
-
138
- actor = producer["actor"].to_s.strip
139
- role = producer["role"].to_s.strip
140
- runtime = producer["runtime"].to_s.strip
141
- if actor.empty? || runtime.empty? || !ExecutionIdentityResolver::ROLES.include?(role)
142
- reject("producer attribution incomplete: actor, runtime, and a known role are required")
143
- end
144
- end
145
-
146
- # Workers submit attributable results but cannot self-approve
147
- # succeeded outcomes; only coordinator/service authority accepts them.
148
- def verify_authority(data, identity)
149
- return if data["verdict"] != "succeeded"
150
- return if @identity_resolver.trusted?(identity)
151
-
152
- reject("identity #{identity.actor}/#{identity.role} may not accept a succeeded verdict")
153
- end
154
-
155
- # The tested/reviewed head must be the live candidate revision; stale
156
- # heads are rejected (candidate invalidation is recorded by the
157
- # coordinator).
158
- def verify_head(data, live_head)
159
- reject("live candidate head unavailable") if live_head.nil? || live_head.to_s.strip.empty?
160
- reject("receipt head '#{data['head']}' is not a git SHA") unless data["head"].match?(HEAD_PATTERN)
161
-
162
- unless data["head"] == live_head
163
- reject("receipt head #{data['head']} is stale; current candidate is #{live_head}")
164
- end
165
- end
166
-
167
- # Succeeded outcomes need at least one artifact whose recorded digest
168
- # matches the file on disk. Paths stay below the project root; symlink
169
- # or traversal escapes are rejected.
170
- def verify_artifacts(data, repo_root)
171
- artifacts = data["artifacts"]
172
- reject_unless(artifacts.is_a?(Array) && !artifacts.empty?, "succeeded verdict requires at least one artifact")
173
-
174
- artifacts.each do |artifact|
175
- reject_unless(artifact.is_a?(Hash), "artifact entries must be objects")
176
- path = artifact["path"].to_s
177
- recorded = artifact["sha256"].to_s
178
- reject("artifact missing path or sha256") if path.empty? || recorded.empty?
179
-
180
- actual = Digest::SHA256.hexdigest(artifact_bytes(data, artifact, repo_root))
181
- reject("artifact digest mismatch for #{path}") unless actual == recorded
182
- end
183
- end
184
-
185
- def verify_checks(data)
186
- checks = data["checks"] || []
187
- reject_unless(checks.is_a?(Array), "checks must be an array")
188
- if data["verdict"] == "succeeded" && checks.empty?
189
- reject("succeeded verdict requires at least one executed check")
190
- end
191
-
192
- checks.each do |check|
193
- reject_unless(check.is_a?(Hash), "check entries must be objects")
194
- reject("check missing name") if check["name"].to_s.strip.empty?
195
- reject("check #{check['name']} did not pass") if check["verdict"].to_s != "passed" && data["verdict"] == "succeeded"
196
- end
197
- end
198
-
199
- # A caller-supplied digest must equal the canonical digest of the
200
- # receipt payload; arbitrary or all-zero digests are rejected.
201
- def verify_digest(receipt)
202
- return if receipt.digest == Atoms::EvidenceDigest.digest(receipt.digest_payload)
203
-
204
- reject("receipt digest mismatch: supplied digest does not match the canonical payload")
205
- end
206
-
207
- # Review receipts need an executed, independent reviewer verdict for
208
- # exactly the tested head — a report existing is not approval, and a
209
- # review receipt without a reviewer verdict is not evidence.
210
- def verify_review(data)
211
- review = data["review"]
212
- if review.nil?
213
- if data["operation"] == REVIEW_OPERATION && data["verdict"] == "succeeded"
214
- reject("review receipt requires an executed independent reviewer verdict")
215
- end
216
-
217
- return
218
- end
219
-
220
- reject_unless(review.is_a?(Hash), "review must be an object")
221
- reviewer = review["reviewer"]
222
- reject_unless(reviewer.is_a?(Hash) && !reviewer["actor"].to_s.strip.empty?, "review must name an executed reviewer")
223
-
224
- producer_actor = data.dig("producer", "actor").to_s.strip
225
- if reviewer["actor"].to_s.strip == producer_actor
226
- reject("reviewer and producer must differ (self-approval rejected)")
227
- end
228
-
229
- reject("review verdict must be '#{APPROVED_VERDICT}' for a succeeded receipt") if review["verdict"] != APPROVED_VERDICT && data["verdict"] == "succeeded"
230
- unless review["head"] == data["head"]
231
- reject("review head #{review['head']} does not match receipt head #{data['head']}")
232
- end
233
- end
234
-
235
- # Consume ace-review's single campaign authority through the existing
236
- # receipt boundary; a worker-supplied local result grants no authority.
237
- def verify_campaign(data, repo_root:, live_head:)
238
- campaign = data["campaign"]
239
- return unless campaign
240
- reject_unless(campaign.is_a?(Hash), "campaign must be an object")
241
- reject("campaign result requires a succeeded review receipt") unless
242
- data["operation"] == REVIEW_OPERATION && data["verdict"] == "succeeded"
243
- reference = campaign["result"]
244
- reject_unless(reference.is_a?(Hash), "campaign requires a result artifact reference")
245
- unless Array(data["artifacts"]).include?(reference)
246
- reject("campaign result must be included in verified receipt artifacts")
247
- end
248
- result = JSON.parse(artifact_bytes(data, reference, repo_root))
249
- reject_unless(result.is_a?(Hash), "campaign result must be an object")
250
- if @campaign_verifier
251
- @campaign_verifier.call(data, result, live_head)
252
- return
253
- end
254
- require "ace/review"
255
- manager = Ace::Review::Organisms::CampaignManager.new(repo_root: repo_root)
256
- current = manager.status(campaign["id"])
257
- reject("campaign result ID differs from receipt") unless result["campaign_id"] == campaign["id"]
258
- manager.with_verified_result!(result: result, subject: current.fetch("subject"),
259
- contract_identity: current.fetch("contract_identity"), policy: current.fetch("effective_policy"),
260
- head: live_head, base: current.fetch("evidence").fetch("current_base"),
261
- producer: data.dig("producer", "actor"), reviewer: data.dig("review", "reviewer", "actor")) { true }
262
- rescue JSON::ParserError, ArgumentError, TypeError, KeyError => e
263
- reject("invalid campaign result: #{e.message}")
264
- end
265
-
266
- # Protected composition injects the canonical imported-byte owner.
267
- # Its failure never falls through to the disposable workspace copy.
268
- def artifact_bytes(data, artifact, repo_root)
269
- if @artifact_reader
270
- bytes = @artifact_reader.call(data, artifact)
271
- reject("canonical artifact reader did not return bytes") unless bytes.is_a?(String)
272
- return bytes.b
273
- end
274
- path = artifact["path"].to_s
275
- expanded = begin
276
- File.expand_path(path, File.realpath(repo_root))
277
- rescue Errno::ENOENT, Errno::EACCES
278
- nil
279
- end
280
- reject("artifact file not found: #{path}") if expanded.nil? || !File.exist?(expanded)
281
- resolved = safe_resolve(repo_root, path)
282
- reject("artifact path escapes project root: #{path}") if resolved.nil?
283
- File.read(resolved)
284
- end
285
-
286
- # Resolve a project-relative artifact path to its real location,
287
- # rejecting traversal and symlink escapes (an in-repo symlink must
288
- # not smuggle in an external file); returns nil when the path leaves
289
- # the root.
290
- def safe_resolve(repo_root, path)
291
- root = File.realpath(repo_root)
292
- candidate = File.expand_path(path, root)
293
- return nil unless candidate == root || candidate.start_with?(root + File::SEPARATOR)
294
- return nil unless File.exist?(candidate)
295
-
296
- resolved = File.realpath(candidate)
297
- return nil unless resolved == root || resolved.start_with?(root + File::SEPARATOR)
298
-
299
- resolved
300
- rescue Errno::ENOENT, Errno::EACCES
301
- nil
302
- end
303
-
304
- def reject(message)
305
- raise AttemptErrors::ReceiptRejected, message
306
- end
307
-
308
- def reject_unless(condition, message)
309
- reject(message) unless condition
310
- end
311
- end
312
- end
313
- end
314
- end
@@ -1,88 +0,0 @@
1
- # frozen_string_literal: true
2
- require_relative "receipt_verifier"
3
- require_relative "canonical_evidence"
4
-
5
- module Ace
6
- module Assign
7
- module Molecules
8
- # Pure reader over already accepted terminal owner evidence. A submitted
9
- # result alone carries no terminal authority; local filesystem artifacts
10
- # are never substituted for its imported protected bytes.
11
- class TerminalScopeReceipt
12
- BINDING_FIELDS = %w[mapping_id project_id assignment_id attempt_id result_id worker_uid worker_actor
13
- launch_ticket process_binding head candidate_generation uploaded_receipt_sha256 original_receipt_digest].freeze
14
- PAYLOAD_FIELDS = %w[version result_id binding uploaded_receipt_sha256 original_receipt_digest receipt_digest receipt artifacts].freeze
15
-
16
- def self.verify!(events:, lineage:, journal:, commit:, mapping:)
17
- raise AttemptErrors::EvidenceUnavailable, "terminal receipt chain differs" unless Models::EvidenceEvent.chain_valid?(events)
18
- terminal = events.select { |event| event["type"] == "receipt_accepted" &&
19
- %w[succeeded failed].include?(event.dig("payload", "receipt", "verdict")) }
20
- raise AttemptErrors::EvidenceUnavailable, "no unique accepted terminal receipt" unless terminal.size == 1
21
- accepted = terminal.first
22
- receipt = accepted.fetch("payload").fetch("receipt")
23
- before = events.take_while { |event| event.fetch("digest") != accepted.fetch("digest") }
24
- submitted = before.select { |event| event["type"] == "result_submitted" &&
25
- event.dig("payload", "receipt_digest") == receipt.fetch("digest") }
26
- raise AttemptErrors::EvidenceUnavailable, "accepted terminal result provenance differs" unless submitted.size == 1
27
- payload = submitted.first.fetch("payload")
28
- binding = payload.fetch("binding")
29
- reservation = before.find { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "reserve_attempt" }
30
- origin = reservation.fetch("payload").fetch("data")
31
- expected = {"mapping_id" => origin.fetch("mapping_id"), "project_id" => mapping.fetch("project_id"),
32
- "assignment_id" => origin.fetch("assignment_id"), "attempt_id" => accepted.fetch("attempt_id"),
33
- "worker_uid" => mapping.fetch("worker_uid"), "worker_actor" => mapping.fetch("worker_actor"),
34
- "launch_ticket" => origin.fetch("launch_ticket"), "process_binding" => lineage.child_event.fetch("payload").fetch("original_process_binding")}
35
- candidates = before.select { |event| event["type"] == "authority_mutation" &&
36
- event.dig("payload", "operation") == "submit_candidate" &&
37
- event.dig("payload", "data", "candidate_generation") == binding.fetch("candidate_generation") }
38
- candidate = candidates.one? && candidates.first.dig("payload", "data")
39
- unless payload.keys.sort == PAYLOAD_FIELDS.sort && binding.keys.sort == BINDING_FIELDS.sort &&
40
- payload["version"] == 1 && expected.all? { |key, value| binding[key] == value } &&
41
- candidate && candidate["head"] == binding["head"] && receipt["head"] == binding["head"] &&
42
- payload["result_id"] == binding["result_id"] && payload["uploaded_receipt_sha256"] == binding["uploaded_receipt_sha256"] &&
43
- payload["original_receipt_digest"] == binding["original_receipt_digest"] &&
44
- Models::ExecutionReceipt.from_h(payload.fetch("receipt")).digest_payload == Models::ExecutionReceipt.from_h(receipt).digest_payload &&
45
- (receipt.keys - Models::ExecutionReceipt.from_h(receipt).to_h.keys).empty? &&
46
- payload["artifacts"] == receipt["artifacts"] &&
47
- receipt["producer"] == {"actor" => mapping.fetch("worker_actor"), "role" => "worker", "runtime" => "herdr"} &&
48
- journal.canonical_attempt_state(events) == receipt.fetch("verdict")
49
- raise AttemptErrors::EvidenceUnavailable, "accepted terminal receipt selectors differ"
50
- end
51
- model = Models::ExecutionReceipt.from_h(receipt)
52
- unless model.digest == Atoms::EvidenceDigest.digest(model.digest_payload) && model.digest == payload.fetch("receipt_digest")
53
- raise AttemptErrors::EvidenceUnavailable, "accepted terminal receipt digest differs"
54
- end
55
- finishes = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "finish" }
56
- unless finishes.empty?
57
- transition = events[events.index(accepted) + 1]
58
- finish = events[events.index(accepted) + 2]
59
- expected_reply = binding.slice("attempt_id", "result_id", "head", "candidate_generation").merge(
60
- "receipt_digest" => payload.fetch("receipt_digest"), "state" => receipt.fetch("verdict"))
61
- unless finishes.one? && finishes.first == finish && transition && transition["type"] == "transition" &&
62
- transition.fetch("payload") == {"from" => "running", "to" => receipt.fetch("verdict"), "reason" => "protected_result_accepted"} &&
63
- finish.dig("payload", "data").except("generation") == expected_reply &&
64
- finish.dig("payload", "data", "generation") == events.take(events.index(finish) + 1).count { |event| event["type"] == "authority_mutation" }
65
- raise AttemptErrors::EvidenceUnavailable, "accepted finish reply or terminal transition differs"
66
- end
67
- end
68
- canonical = CanonicalEvidence.new(journal: journal)
69
- context = {kind: "result", project_id: binding.fetch("project_id"), assignment_id: binding.fetch("assignment_id"),
70
- attempt_id: binding.fetch("attempt_id"), peer_uid: binding.fetch("worker_uid"), binding: binding,
71
- request_id_or_event_id: binding.fetch("result_id"), generation: binding.fetch("candidate_generation")}
72
- reader = ->(_data, artifact) { canonical.read({"ref" => artifact.fetch("path"), "sha256" => artifact.fetch("sha256")},
73
- **context, commit: commit) }
74
- Array(receipt["artifacts"]).each { |artifact| reader.call(receipt, artifact) }
75
- intent = before.find { |event| event["type"] == "intent" }
76
- attempt = journal.send(:build_attempt, binding.fetch("assignment_id"), binding.fetch("attempt_id"),
77
- intent.fetch("payload"), before, "running")
78
- verifier = ReceiptVerifier.new(artifact_reader: reader)
79
- verifier.verify_result!(receipt, attempt: attempt, live_head: binding.fetch("head"), repo_root: journal.repo_root)
80
- verifier.verify_accepted_evidence!(receipt, live_head: binding.fetch("head"), repo_root: journal.repo_root, historical: true)
81
- accepted
82
- rescue KeyError, TypeError, ArgumentError, NoMethodError, AttemptErrors::ReceiptRejected
83
- raise AttemptErrors::EvidenceUnavailable, "authenticated terminal scope receipt unavailable"
84
- end
85
- end
86
- end
87
- end
88
- end