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,290 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ace
4
- module Assign
5
- module Authority
6
- class LaunchLifecycle
7
- # Fixed source readers reuse the complete canonical inventory and its
8
- # original registration/definition proof; this is never a live grant.
9
- def preview_attempt_events!(journal:, commit:, params:, map:)
10
- preview_attempt_entry!(journal: journal, commit: commit, params: params, map: map).fetch(:events)
11
- end
12
-
13
- def preview_attempt_definition!(journal:, commit:, params:, map:)
14
- entry = preview_attempt_entry!(journal: journal, commit: commit, params: params, map: map)
15
- inventory_definition!(journal, commit, entry)
16
- end
17
-
18
- private
19
-
20
- def preview_attempt_entry!(journal:, commit:, params:, map:)
21
- entries = inventory_index!(journal, commit, params.fetch("mapping_id"), map).select do |entry|
22
- entry.fetch(:selector) == params.slice("assignment_id", "attempt_id")
23
- end
24
- raise AttemptErrors::EvidenceUnavailable, "preview original registration is unavailable or ambiguous" unless entries.one?
25
- entry = entries.first
26
- inventory_definition!(journal, commit, entry)
27
- entry
28
- end
29
-
30
- # Discovery authenticates the current mapped principal, then reads one
31
- # immutable canonical prefix. It neither pins the original launcher nor
32
- # grants permission to mutate that launch.
33
- def assignment_inventory!(request:, peer:, role:)
34
- params = request.fetch("params")
35
- strict!(params, %w[mapping_id journal_commit after limit])
36
- raise ArgumentError, "Inventory mutation ID must be null" unless request.fetch("mutation_id").nil?
37
- unless %i[launcher supervisor].include?(role)
38
- raise AttemptErrors::UnauthorizedIdentity, "Inventory requires mapped launcher or supervisor"
39
- end
40
- map = steering_principal!(params, peer, role)
41
- limit = params.fetch("limit")
42
- raise ArgumentError, "Inventory limit is invalid" unless limit.is_a?(Integer) && limit.between?(1, 50)
43
- after = params.fetch("after")
44
- unless after.nil?
45
- strict!(after, %w[assignment_id attempt_id])
46
- token!(after.fetch("assignment_id"))
47
- token!(after.fetch("attempt_id")) unless after.fetch("attempt_id").nil?
48
- raise ArgumentError, "Inventory continuation requires selected commit" if params.fetch("journal_commit").nil?
49
- end
50
- journal = journal_for(map)
51
- canonical_commit = journal.ref_value
52
- commit = params.fetch("journal_commit") || canonical_commit
53
- journal.verify_canonical_prefix!(commit: commit, canonical_commit: canonical_commit)
54
- indexed = inventory_index!(journal, commit, params.fetch("mapping_id"), map)
55
- offset = 0
56
- if after
57
- position = indexed.index { |entry| entry.fetch(:selector) == after }
58
- raise AttemptErrors::NotFound, "Inventory cursor does not select a visible row" unless position
59
- offset = position + 1
60
- end
61
- data = {"project_id" => map.fetch("project_id"), "mapping_id" => params.fetch("mapping_id"),
62
- "journal_commit" => commit, "items" => [], "next_after" => nil}
63
- indexed.drop(offset).first(limit).each do |entry|
64
- row = inventory_row!(journal, commit, entry)
65
- proposed = data.merge("items" => data.fetch("items") + [row],
66
- "next_after" => offset + data.fetch("items").size + 1 < indexed.size ? entry.fetch(:selector) : nil)
67
- # Size the actual fixed public envelope, retaining whole rows and
68
- # an honest cursor without truncating or omitting canonical facts.
69
- frame = {"status" => "ok", "data" => proposed, "transport" => {"replayed" => false}}
70
- if JSON.generate(frame).bytesize + 1 > 16_384
71
- raise AttemptErrors::BoundedResultUnavailable, "Inventory row exceeds bounded response" if data.fetch("items").empty?
72
- break
73
- end
74
- data = proposed
75
- end
76
- {data: data, replayed: false}
77
- end
78
-
79
- def inventory_index!(journal, commit, mapping_id, map)
80
- authenticated = journal.canonical_event_inventory!(commit: commit)
81
- raise AttemptErrors::EvidenceUnavailable, "Inventory authenticated snapshot differs" unless authenticated.fetch("commit") == commit
82
- snapshots = authenticated.fetch("events")
83
- introductions = authenticated.fetch("introductions")
84
- entries = snapshots.flat_map do |assignment_id, events|
85
- registrations = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "register_assignment" }
86
- next [] if registrations.empty?
87
- attempts = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "reserve_attempt" }
88
- if attempts.empty?
89
- registration = registrations.last.fetch("payload").fetch("data")
90
- next [] unless registration["mapping_id"] == mapping_id
91
- authenticate_inventory_registration!(journal, assignment_id, events, registration, mapping_id, map, commit, introductions.fetch(assignment_id))
92
- next [{selector: {"assignment_id" => assignment_id, "attempt_id" => nil}, registration: registration, map: map}]
93
- end
94
- selected = attempts.select { |event| event.dig("payload", "data", "mapping_id") == mapping_id }
95
- next [] if selected.empty?
96
- prefixes = introductions.fetch(assignment_id)
97
- selected.map do |reserve|
98
- attempt_id = reserve.fetch("attempt_id")
99
- chain = events.select { |event| event["attempt_id"] == attempt_id }
100
- registration = definition(journal, assignment_id, commit: prefixes.fetch(reserve.fetch("digest")))
101
- unless registration && registration.values_at("assignment_id", "project_id", "mapping_id") == [assignment_id, map.fetch("project_id"), mapping_id]
102
- raise AttemptErrors::EvidenceUnavailable, "Inventory reservation registration differs"
103
- end
104
- registration_events = journal.read_events(assignment_id, commit: prefixes.fetch(reserve.fetch("digest")))
105
- authenticate_inventory_registration!(journal, assignment_id, registration_events, registration, mapping_id, map, prefixes.fetch(reserve.fetch("digest")), prefixes)
106
- {selector: {"assignment_id" => assignment_id, "attempt_id" => attempt_id},
107
- registration: registration, events: chain, map: map, introductions: prefixes}
108
- end
109
- end
110
- selectors = entries.map { |entry| entry.fetch(:selector).values_at("assignment_id", "attempt_id") }
111
- raise AttemptErrors::EvidenceUnavailable, "Inventory row identity is ambiguous" unless selectors.uniq.size == selectors.size
112
- entries.sort_by { |entry| [entry.fetch(:selector).fetch("assignment_id").b, entry.fetch(:selector).fetch("attempt_id").to_s.b] }
113
- rescue KeyError, TypeError, NoMethodError
114
- raise AttemptErrors::EvidenceUnavailable, "Inventory canonical index is malformed"
115
- end
116
-
117
- def authenticate_inventory_registration!(journal, assignment_id, events, registration, mapping_id, map, commit, introductions)
118
- unless registration.is_a?(Hash) && registration.values_at("assignment_id", "project_id", "mapping_id", "phase") ==
119
- [assignment_id, map.fetch("project_id"), mapping_id, "registered"]
120
- raise AttemptErrors::EvidenceUnavailable, "Inventory accepted registration association differs"
121
- end
122
- chain = events.select { |event| event["attempt_id"] == "definition-#{assignment_id}" }
123
- journal.authority_generation(chain)
124
- accepted = chain.select { |event| event["type"] == "authority_mutation" &&
125
- event.dig("payload", "operation") == "register_assignment" && event.dig("payload", "data") == registration }
126
- raise AttemptErrors::EvidenceUnavailable, "Inventory registration provenance is ambiguous" unless accepted.one?
127
- event = accepted.first
128
- prefix = introductions.fetch(event.fetch("digest"))
129
- original_chain = journal.read_events(assignment_id, commit: prefix).select { |item| item["attempt_id"] == "definition-#{assignment_id}" }
130
- unless original_chain.last == event && registration["generation"].is_a?(Integer) &&
131
- registration["generation"] == journal.authority_generation(original_chain)
132
- raise AttemptErrors::EvidenceUnavailable, "Inventory registration generation/introduction differs"
133
- end
134
- end
135
-
136
- def inventory_definition!(journal, commit, entry)
137
- selector, registration = entry.values_at(:selector, :registration)
138
- bytes = journal.blob(registration.fetch("definition_ref"), commit: commit)
139
- unless bytes.is_a?(String) && bytes.bytesize <= 32_768 &&
140
- Digest::SHA256.hexdigest(bytes) == registration.fetch("definition_digest")
141
- raise AttemptErrors::EvidenceUnavailable, "Inventory definition content differs"
142
- end
143
- value = JSON.parse(bytes)
144
- prepared = value.fetch("prepared_work")
145
- unless registration.fetch("prepared_work") == prepared && prepared.fetch("task_id") == registration.fetch("task_id") &&
146
- registration.fetch("selection_sha256") == prepared.fetch("selection_sha256") &&
147
- registration.fetch("prepared_bundle_sha256").is_a?(String) && registration.fetch("prepared_bundle_sha256").match?(PreparedWork::SHA) &&
148
- registration.fetch("prepared_bundle_bytes").is_a?(Integer) && registration.fetch("prepared_bundle_bytes").between?(1, PreparedWork::MAX_TOTAL) &&
149
- registration.fetch("prepared_bundle_ref") == "execution/prepared/#{selector.fetch('assignment_id')}-#{registration.fetch('prepared_bundle_sha256')}.bundle"
150
- raise AttemptErrors::EvidenceUnavailable, "Inventory prepared input identity differs"
151
- end
152
- assignment = Models::Assignment.from_h(value)
153
- unless assignment.managed? && assignment.id == selector.fetch("assignment_id") &&
154
- assignment.project_id == registration.fetch("project_id") && assignment.task_id == registration.fetch("task_id") &&
155
- registration.fetch("definition_generation").is_a?(Integer) && registration.fetch("definition_generation").positive?
156
- raise AttemptErrors::EvidenceUnavailable, "Inventory definition provenance differs"
157
- end
158
- assignment
159
- end
160
-
161
- def inventory_row!(journal, commit, entry)
162
- inventory_definition!(journal, commit, entry)
163
- selector, registration = entry.values_at(:selector, :registration)
164
- row = selector.merge(registration.slice("task_id", "definition_digest", "definition_generation", "prepared_bundle_ref", "prepared_bundle_bytes", "prepared_bundle_sha256", "selection_sha256"),
165
- "scope" => nil, "reservation_mutation_id" => nil, "base_head" => nil, "reservation_generation" => nil, "generation" => nil, "canonical_state" => nil,
166
- "original_binding_digest" => nil, "terminal_event_id" => nil, "reservation_release_event_id" => nil)
167
- return row unless selector.fetch("attempt_id")
168
- events = entry.fetch(:events)
169
- map = entry.fetch(:map)
170
- state = origin(events, **selector.merge("mapping_id" => registration.fetch("mapping_id")).transform_keys(&:to_sym))
171
- generation = journal.authority_generation(events)
172
- unless generation.is_a?(Integer) && generation.positive? && state.fetch("reservation_generation").is_a?(Integer) && state.fetch("reservation_generation").positive?
173
- raise AttemptErrors::EvidenceUnavailable, "Inventory attempt generation is invalid"
174
- end
175
- reservations = events.select { |event| event["type"] == "authority_mutation" && event.dig("payload", "operation") == "reserve_attempt" }
176
- raise AttemptErrors::EvidenceUnavailable, "Inventory original reservation is ambiguous" unless reservations.one?
177
- reservation = reservations.first.fetch("payload")
178
- mutation_id = reservation.fetch("mutation_id")
179
- token!(mutation_id)
180
- base_head = reservation.fetch("data").fetch("base_head")
181
- unless base_head.is_a?(String) && base_head.match?(/\A[0-9a-f]{40}\z/) && state.fetch("base_head") == base_head
182
- raise AttemptErrors::EvidenceUnavailable, "Inventory original reservation base differs"
183
- end
184
- row.merge!("scope" => state.fetch("scope"), "reservation_mutation_id" => mutation_id, "base_head" => base_head, "reservation_generation" => state.fetch("reservation_generation"),
185
- "generation" => generation, "canonical_state" => journal.canonical_attempt_state(events))
186
- if events.any? { |event| event.dig("payload", "operation") == "record_launch" }
187
- row["original_binding_digest"] = original_prompt_record!(events, state,
188
- params: selector.merge("mapping_id" => registration.fetch("mapping_id"))).fetch("binding_digest")
189
- end
190
- terminal_events = events.select { |event| %w[receipt_accepted attempt_stopped].include?(event["type"]) }
191
- releases = events.select { |event| event.dig("payload", "operation") == "scope_reservation_release" }
192
- guarded_abort = events.any? { |event| event.dig("payload", "operation") == "abort_launch" }
193
- return row if terminal_events.empty? && releases.empty? && !guarded_abort
194
- original, original_map = inventory_original_deployment!(journal, events, registration.fetch("mapping_id"), map)
195
- lineage = Molecules::ExecutionScopeLineage.new(events: events, project_id: original_map.fetch("project_id"),
196
- assignment_id: selector.fetch("assignment_id"), attempt_id: selector.fetch("attempt_id"), mapping_id: registration.fetch("mapping_id"))
197
- introductions = entry.fetch(:introductions)
198
- terminal_introduction = nil
199
- terminal = if events.any? { |event| event["type"] == "attempt_stopped" }
200
- stopped = events.find { |event| event["type"] == "attempt_stopped" }
201
- accepted = events.select { |event| event["type"] == "authority_mutation" && event["previous_digest"] == stopped.fetch("digest") }
202
- raise AttemptErrors::EvidenceUnavailable, "Inventory stopped acceptance is ambiguous" unless accepted.one?
203
- terminal_introduction = introductions.fetch(accepted.first.fetch("digest"))
204
- verify_stopped_terminal_at_prefix!(events, journal, terminal_introduction, deployment: original)
205
- else
206
- inventory_terminal_at_introduction!(journal, selector, events, original, original_map, registration.fetch("mapping_id"), introductions)
207
- end
208
- row["terminal_event_id"] = terminal.fetch("digest")
209
- unless releases.empty?
210
- raise AttemptErrors::EvidenceUnavailable, "Inventory release is ambiguous" unless releases.one?
211
- release = releases.first
212
- prefix = introductions.fetch(release.fetch("digest"))
213
- verify_released_lineage_at_prefix!(journal, prefix, selector.fetch("assignment_id"), selector.fetch("attempt_id"), events, original, original_map, terminal_introduction: terminal_introduction)
214
- row["reservation_release_event_id"] = release.fetch("digest")
215
- end
216
- row
217
- rescue KeyError, TypeError, JSON::ParserError, NoMethodError
218
- raise AttemptErrors::EvidenceUnavailable, "Inventory canonical row is malformed"
219
- end
220
-
221
- def inventory_terminal_at_introduction!(journal, selector, events, original, map, mapping_id, introductions)
222
- terminals = events.select { |event| event["type"] == "receipt_accepted" ||
223
- (event["type"] == "authority_mutation" && event.dig("payload", "operation") == "abort_launch") }
224
- raise AttemptErrors::EvidenceUnavailable, "Inventory terminal provenance is ambiguous" unless terminals.one?
225
- event = terminals.first
226
- prefix_commit = introductions.fetch(event.fetch("digest"))
227
- prefix = journal.read_events(selector.fetch("assignment_id"), commit: prefix_commit).select { |entry| entry["attempt_id"] == selector.fetch("attempt_id") }
228
- terminal_length = events.index(event) + 1
229
- if event["type"] == "receipt_accepted"
230
- transition, acceptance = events.drop(terminal_length).take(2)
231
- if transition && acceptance && transition["type"] == "transition" &&
232
- acceptance["type"] == "authority_mutation" && acceptance.dig("payload", "operation") == "finish"
233
- unless introductions.fetch(transition.fetch("digest")) == prefix_commit &&
234
- introductions.fetch(acceptance.fetch("digest")) == prefix_commit
235
- raise AttemptErrors::EvidenceUnavailable, "Inventory finish acceptance introduction differs"
236
- end
237
- terminal_length += 2
238
- end
239
- end
240
- unless prefix == events.take(terminal_length)
241
- raise AttemptErrors::EvidenceUnavailable, "Inventory terminal introduction differs"
242
- end
243
- lineage = Molecules::ExecutionScopeLineage.new(events: prefix, project_id: map.fetch("project_id"),
244
- assignment_id: selector.fetch("assignment_id"), attempt_id: selector.fetch("attempt_id"), mapping_id: mapping_id)
245
- terminal = terminal_scope_receipt!(prefix, lineage, journal, prefix_commit, deployment: original)
246
- if prefix.any? { |entry| entry.dig("payload", "operation") == "finish" }
247
- unless lineage.proof_event && !pending_prompt_issuers?(prefix, journal, prefix_commit) && @result_owner
248
- raise AttemptErrors::EvidenceUnavailable, "Inventory finish closure owner is unavailable"
249
- end
250
- lineage.require_positive!(scope_generation: lineage.binding.fetch("scope_generation"),
251
- scope_binding_event_id: lineage.binding_event.fetch("digest"), seal_event_id: lineage.seal_event.fetch("digest"), proof_id: lineage.proof_id)
252
- params = selector.merge("mapping_id" => mapping_id)
253
- if terminal.dig("payload", "receipt", "verdict") == "succeeded"
254
- finish = prefix.find { |entry| entry.dig("payload", "operation") == "finish" }
255
- review_params = params.merge(finish.fetch("payload").fetch("data").slice("head", "candidate_generation"))
256
- @result_owner.finished_review_evidence!(journal: journal, events: prefix, params: review_params,
257
- map: map, commit: prefix_commit)
258
- end
259
- @result_owner.service_settlement_complete!(journal: journal, events: prefix, params: params, map: map, commit: prefix_commit)
260
- end
261
- terminal
262
- end
263
-
264
- def inventory_original_deployment!(journal, events, mapping_id, current_map)
265
- provisioning = events.select { |event| event["type"] == "scope_provisioning" }
266
- raise AttemptErrors::EvidenceUnavailable, "Inventory original provisioning is ambiguous" unless provisioning.one?
267
- identity = provisioning.first.fetch("payload")
268
- unless identity.is_a?(Hash) && identity.keys.sort == %w[deployment_digest descriptor_sha256 reservation_generation slot_id]
269
- raise AttemptErrors::EvidenceUnavailable, "Inventory original provisioning is malformed"
270
- end
271
- original = if identity.fetch("descriptor_sha256") == protected_descriptor_sha256!
272
- @deployment
273
- else
274
- raise AttemptErrors::EvidenceUnavailable, "Inventory original descriptor owner unavailable" unless @deployment_history
275
- @deployment_history.descriptor!(sha256: identity.fetch("descriptor_sha256"))
276
- end
277
- map = original.mapping(mapping_id)
278
- project = original.project(map.fetch("project_id"))
279
- unless map.fetch("project_id") == current_map.fetch("project_id") &&
280
- map.fetch("execution_scope").fetch("slot_id") == identity.fetch("slot_id") &&
281
- original.mapping_digest(mapping_id) == identity.fetch("deployment_digest") &&
282
- [project.fetch("journal_repository"), project.fetch("evidence_git_ref"), project.fetch("evidence_checkout_root")] == [journal.repo_root, journal.ref, journal.checkout_root]
283
- raise AttemptErrors::EvidenceUnavailable, "Inventory original descriptor association differs"
284
- end
285
- [original, map]
286
- end
287
- end
288
- end
289
- end
290
- end
@@ -1,100 +0,0 @@
1
- # frozen_string_literal: true
2
- require "json"
3
- require "ace/runtime/molecules/protected_artifact_set"
4
- require_relative "campaign_execution"
5
- require_relative "posix_acl"
6
-
7
- module Ace
8
- module Assign
9
- module Authority
10
- # One accepted descriptor reference, held for the entire store/CAS guard.
11
- # There is deliberately no ordinary review configuration cascade here.
12
- class CampaignConsumerPolicy
13
- class Protection < Ace::Runtime::Molecules::ProtectedArtifactSet::Protection
14
- def initialize(acl: PosixAcl.new, **options)
15
- super(**options)
16
- @acl = acl
17
- end
18
-
19
- def verify!(path, handle, directory:)
20
- super
21
- unless @acl.entries(path).nil? && (!directory || @acl.entries(path, attribute: "system.posix_acl_default").nil?)
22
- raise Ace::Runtime::RuntimeUnavailableError, "campaign policy ACL differs"
23
- end
24
- end
25
- end
26
- def self.reference!(reference)
27
- unless reference.is_a?(Hash) && reference.keys.sort == %w[bytes path sha256] &&
28
- reference["path"].is_a?(String) && reference["path"].encoding == Encoding::UTF_8 &&
29
- reference["path"].valid_encoding? && reference["path"].bytesize.between?(1, 4096) &&
30
- !reference["path"].include?("\0") && reference["path"].start_with?("/") &&
31
- File.expand_path(reference["path"]) == reference["path"] &&
32
- reference["sha256"].is_a?(String) && reference["sha256"].match?(/\A[0-9a-f]{64}\z/) &&
33
- reference["bytes"].is_a?(Integer) && reference["bytes"].between?(1, 65_536)
34
- raise ArgumentError, "invalid campaign consumer policy reference"
35
- end
36
- reference
37
- end
38
-
39
- def initialize(artifacts: Ace::Runtime::Molecules::ProtectedArtifactSet.new(protection: Protection.new))
40
- @artifacts = artifacts
41
- end
42
-
43
- def with(reference)
44
- begin
45
- self.class.reference!(reference)
46
- rescue ArgumentError
47
- unavailable!
48
- end
49
- @artifacts.with do |artifacts|
50
- profiles = read_profiles!(artifacts, reference)
51
- verify_artifacts!(artifacts)
52
- result = yield profiles, artifacts
53
- verify_artifacts!(artifacts)
54
- result
55
- end
56
- end
57
-
58
- private
59
-
60
- def read_profiles!(artifacts, reference)
61
- bytes = artifacts.read!(reference).dup.force_encoding(Encoding::UTF_8)
62
- raise ArgumentError, "campaign consumer policy is not UTF-8" unless bytes.valid_encoding?
63
- value = JSON.parse(bytes, create_additions: false, max_nesting: 16,
64
- allow_duplicate_key: false, allow_comments: false)
65
- unless value.is_a?(Hash) && value.keys.sort == %w[profiles schema] &&
66
- value["schema"] == "ace.review.consumer-policy/v1" && value["profiles"].is_a?(Hash) &&
67
- value["profiles"].size <= 32
68
- raise ArgumentError, "campaign consumer policy fields differ"
69
- end
70
- profiles = value.fetch("profiles").to_h do |id, policy|
71
- CampaignExecution::Contract.id!(id, "consumer profile")
72
- [id, policy.nil? ? nil : CampaignExecution::Contract.policy!(policy)]
73
- end
74
- freeze_value!(profiles)
75
- profiles
76
- rescue ArgumentError, KeyError, TypeError, JSON::ParserError, Ace::Runtime::RuntimeUnavailableError, SystemCallError, IOError
77
- unavailable!
78
- end
79
-
80
- def verify_artifacts!(artifacts)
81
- artifacts.verify_unchanged!
82
- rescue Ace::Runtime::RuntimeUnavailableError, SystemCallError, IOError
83
- unavailable!
84
- end
85
-
86
- def unavailable!
87
- raise AttemptErrors::EvidenceUnavailable, "current campaign consumer policy is unavailable"
88
- end
89
-
90
- def freeze_value!(value)
91
- case value
92
- when Hash then value.each { |key, child| key.freeze; freeze_value!(child) }
93
- when Array then value.each { |child| freeze_value!(child) }
94
- end
95
- value.freeze
96
- end
97
- end
98
- end
99
- end
100
- end
@@ -1,119 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "ace/git/atoms/pr_identifier"
4
- require "ace/review/atoms/campaign_contract"
5
- require_relative "../molecules/receipt_verifier"
6
-
7
- module Ace
8
- module Assign
9
- module Authority
10
- # Closed child linkage. Validation is structural; the registration owner
11
- # must independently compare it with canonical parent/campaign state.
12
- module CampaignExecution
13
- Contract = Ace::Review::Atoms::CampaignContract
14
- FIELDS = %w[version parent_assignment_id parent_attempt_id parent_scope parent_candidate_generation
15
- subject campaign_id contract_identity policy_digest round_id phase review_scope scope_identity_digest
16
- head base operation check_name].sort.freeze
17
-
18
- def self.validate_parent!(value)
19
- unless value.is_a?(Hash) && value.keys.sort == %w[campaign_id contract_identity policy subject version] &&
20
- value["version"].is_a?(Integer) && value["version"] == 1
21
- raise ArgumentError, "parent campaign fields differ"
22
- end
23
- Contract.id!(value["campaign_id"], "campaign ID")
24
- unless value["contract_identity"].is_a?(String) && /\A[0-9a-f]{64}\z/.match?(value["contract_identity"]) &&
25
- Contract.subject!(value["subject"]) == value["subject"]
26
- raise ArgumentError, "parent campaign identity differs"
27
- end
28
- Contract.policy!(value["policy"])
29
- value
30
- end
31
-
32
- def self.validate_round!(value, parent:, round:)
33
- validate!(value, parent: parent, policy: round.fetch("policy"))
34
- binding = round.fetch("binding")
35
- expected = round.slice("campaign_id", "subject", "contract_identity", "round_id").merge(
36
- "head" => binding.fetch("head"), "base" => binding.fetch("base"),
37
- "scope_identity_digest" => Contract.digest(binding.fetch("scope_identity").fetch(value.fetch("review_scope"))))
38
- unless expected.all? { |key, item| value.fetch(key) == item }
39
- raise ArgumentError, "campaign execution differs from pinned round"
40
- end
41
- value
42
- rescue KeyError, TypeError
43
- raise ArgumentError, "campaign execution round is incomplete"
44
- end
45
-
46
- # This does not accept a receipt; the canonical result owner still
47
- # validates its producer, artifacts and introduction before use.
48
- def self.validate_result!(value, receipt:, base:)
49
- unless receipt.is_a?(Hash) && receipt["campaign"].nil? &&
50
- receipt["operation"] == value.fetch("operation") &&
51
- receipt["head"] == value.fetch("head") && base == value.fetch("base")
52
- raise ArgumentError, "campaign child result differs from registered phase or candidate"
53
- end
54
- if receipt["verdict"] == "succeeded" && !Array(receipt["checks"]).any? { |check|
55
- check.is_a?(Hash) && check["name"] == value.fetch("check_name") && check["verdict"] == "passed" }
56
- raise ArgumentError, "campaign child result lacks its executed phase check"
57
- end
58
- value
59
- rescue KeyError, TypeError
60
- raise ArgumentError, "campaign child result binding is incomplete"
61
- end
62
-
63
- def self.validate_finished_review!(value, receipt:, accepted_review:)
64
- executed = Array(receipt.fetch("artifacts")).map { |ref| ref.fetch("sha256") }
65
- reviewed = Array(accepted_review.fetch("artifacts")).map { |ref| ref.fetch("sha256") }
66
- unless (executed - reviewed).empty?
67
- raise ArgumentError, "independent campaign review does not cover executed artifacts"
68
- end
69
- if value.fetch("phase") == "approval" && receipt["review"] != accepted_review.fetch("review")
70
- raise ArgumentError, "campaign approval differs from canonical independent review"
71
- end
72
- true
73
- rescue KeyError, TypeError
74
- raise ArgumentError, "campaign independent review binding is incomplete"
75
- end
76
-
77
- def self.validate!(value, parent:, policy:)
78
- unless value.is_a?(Hash) && value.keys.sort == FIELDS && value["version"].is_a?(Integer) && value["version"] == 1
79
- raise ArgumentError, "campaign execution fields differ"
80
- end
81
- %w[parent_assignment_id parent_attempt_id parent_scope campaign_id round_id review_scope].each do |key|
82
- Contract.id!(value[key], key)
83
- end
84
- unless value["parent_assignment_id"] == parent && value["parent_candidate_generation"].is_a?(Integer) &&
85
- value["parent_candidate_generation"] >= 0
86
- raise ArgumentError, "campaign execution parent differs"
87
- end
88
- %w[contract_identity policy_digest scope_identity_digest].each do |key|
89
- unless value[key].is_a?(String) && /\A[0-9a-f]{64}\z/.match?(value[key])
90
- raise ArgumentError, "campaign execution digest differs"
91
- end
92
- end
93
- %w[head base].each { |key| Contract.sha!(value[key], key) }
94
- unless Contract.subject!(value["subject"]) == value["subject"]
95
- raise ArgumentError, "campaign execution subject is not canonical"
96
- end
97
- policy = Contract.policy!(policy)
98
- unless Contract.digest(policy) == value["policy_digest"] && policy["required_scopes"].include?(value["review_scope"])
99
- raise ArgumentError, "campaign execution policy differs"
100
- end
101
- expected = case value["phase"]
102
- when "collection" then ["review-collect", "review-execution"]
103
- when "approval" then ["review", "review-approval"]
104
- when "check"
105
- check = value["check_name"]
106
- if Molecules::ReceiptVerifier::EXTERNAL_EFFECT_OPERATIONS.include?(check)
107
- raise ArgumentError, "external effects cannot be campaign checks"
108
- end
109
- raise ArgumentError, "campaign execution check is not required" unless policy["required_checks"].include?(check)
110
- [check == "tests" ? "test" : check, check]
111
- else raise ArgumentError, "campaign execution phase differs"
112
- end
113
- raise ArgumentError, "campaign execution operation differs" unless value.values_at("operation", "check_name") == expected
114
- value
115
- end
116
- end
117
- end
118
- end
119
- end
@@ -1,82 +0,0 @@
1
- # frozen_string_literal: true
2
- require "fileutils"
3
- require "tmpdir"
4
- require_relative "receipt_transfer"
5
-
6
- module Ace
7
- module Assign
8
- module Authority
9
- module CampaignRoundTransfer
10
- def self.decode(input:, sha256:)
11
- admitted = ReceiptTransfer.decode(input: input, receipt_sha256: sha256,
12
- artifact_field: "artifacts", reference_key: "path")
13
- text = input.bytes(index: 0).dup.force_encoding(Encoding::UTF_8)
14
- raise ArgumentError, "round JSON encoding differs" unless text.valid_encoding? && !text.include?("\0")
15
- envelope = JSON.parse(text, create_additions: false, max_nesting: 32, allow_duplicate_key: false, allow_comments: false)
16
- unless envelope.keys.sort == %w[artifacts round version] && envelope["version"].is_a?(Integer) && envelope["version"] == 1 &&
17
- envelope["round"].is_a?(Hash) && envelope["artifacts"].all? { |ref| relative_path?(ref["path"]) }
18
- raise ArgumentError, "round envelope differs"
19
- end
20
- {round: envelope.fetch("round"), references: envelope.fetch("artifacts"),
21
- bytes: admitted.fetch(:artifacts), sha256: sha256}.freeze
22
- rescue JSON::ParserError, KeyError, TypeError
23
- raise ArgumentError, "round envelope is malformed"
24
- end
25
-
26
- def self.relative_path?(path)
27
- path.is_a?(String) && path.encoding == Encoding::UTF_8 && path.valid_encoding? && path.bytesize.between?(1, 256) &&
28
- !path.start_with?("/") && !path.include?("\\") && !path.include?("\0") &&
29
- path.split("/", -1).none? { |component| component.empty? || %w[. ..].include?(component) }
30
- end
31
-
32
- # Paths are private materialization names, never source filesystem reads.
33
- # The original R1 replay digest binds all supplied bytes independently.
34
- def self.materialize(admitted:, repository:, request_id:)
35
- base = File.join(repository, ".ace-local", "review", "campaign-inputs")
36
- FileUtils.mkdir_p(base, mode: 0o700)
37
- PrivateDirectory.verify!(base)
38
- final = File.join(base, Digest::SHA256.hexdigest(request_id))
39
- if File.exist?(final)
40
- PrivateDirectory.verify!(final)
41
- verify_materialization!(admitted, final)
42
- else
43
- temporary = Dir.mktmpdir("round-", base)
44
- admitted.fetch(:references).zip(admitted.fetch(:bytes)).each do |reference, bytes|
45
- path = File.join(temporary, reference.fetch("path"))
46
- FileUtils.mkdir_p(File.dirname(path), mode: 0o700)
47
- File.open(path, File::WRONLY | File::CREAT | File::EXCL | File::NOFOLLOW, 0o600) do |file|
48
- file.write(bytes)
49
- file.flush
50
- file.fsync
51
- end
52
- end
53
- File.rename(temporary, final)
54
- temporary = nil
55
- File.open(base, File::RDONLY) { |directory| directory.fsync }
56
- end
57
- admitted.fetch(:references).to_h { |reference| [reference.fetch("path"), File.join(final, reference.fetch("path"))] }.freeze
58
- ensure
59
- FileUtils.remove_entry(temporary) if temporary && File.directory?(temporary)
60
- end
61
-
62
- def self.verify_materialization!(admitted, root)
63
- expected = admitted.fetch(:references).map { |ref| ref.fetch("path") }.sort
64
- actual = Dir.glob(File.join(root, "**", "*"), File::FNM_DOTMATCH).reject { |path| File.directory?(path) }
65
- .map { |path| path.delete_prefix(root + "/") }.sort
66
- raise AttemptErrors::Conflict, "round materialization differs" unless expected == actual
67
- admitted.fetch(:references).each do |reference|
68
- path = File.join(root, reference.fetch("path"))
69
- PrivateDirectory.verify!(File.dirname(path))
70
- File.open(path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK) do |file|
71
- stat = file.stat
72
- unless stat.file? && stat.uid == Process.uid && stat.nlink == 1 && (stat.mode & 0o777) == 0o600 &&
73
- stat.size <= 64 * 1024 && Digest::SHA256.hexdigest(file.read(64 * 1024 + 1)) == reference.fetch("sha256")
74
- raise AttemptErrors::Conflict, "round materialization bytes differ"
75
- end
76
- end
77
- end
78
- end
79
- end
80
- end
81
- end
82
- end
@@ -1,55 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../atoms/git_blob_batch"
4
-
5
- module Ace
6
- module Assign
7
- module Authority
8
- # Same cat-file batch dialect/codec as the canonical journal. Chunked
9
- # bodies avoid retaining the entire expanded tree a second time.
10
- module CandidateBlobBatch
11
- BATCH_BYTES = 8 * 1024 * 1024
12
- BATCH_OBJECTS = 512
13
- MAX_OBJECTS = 20_000
14
-
15
- private
16
-
17
- def candidate_blob_sizes(repository, deadline, oids)
18
- unless oids.is_a?(Array) && oids.size <= MAX_OBJECTS &&
19
- oids.all? { |oid| oid.is_a?(String) && self.class::SHA.match?(oid) }
20
- reject!("Candidate blob inventory is invalid or oversized")
21
- end
22
- oids.each_slice(64).flat_map do |slice|
23
- raw = git(repository, deadline, "cat-file", "--batch-check", stdin_data: slice.join("\n") + "\n", limit: slice.size * 128)
24
- Atoms::GitBlobBatch.sizes(slice, raw)
25
- end
26
- rescue Atoms::GitBlobBatch::Invalid => error
27
- reject!("Candidate blob batch #{error.reason} differs")
28
- end
29
-
30
- def each_candidate_blob(repository, deadline, entries)
31
- batch = []
32
- bytes = 0
33
- entries.each do |entry|
34
- if !batch.empty? && (batch.size >= BATCH_OBJECTS || bytes + entry.last > BATCH_BYTES)
35
- yield_candidate_blob_batch(repository, deadline, batch) { |oid, content| yield oid, content }
36
- batch = []
37
- bytes = 0
38
- end
39
- batch << entry
40
- bytes += entry.last
41
- end
42
- yield_candidate_blob_batch(repository, deadline, batch) { |oid, content| yield oid, content } unless batch.empty?
43
- end
44
-
45
- def yield_candidate_blob_batch(repository, deadline, entries)
46
- limit = entries.sum { |_oid, size| size + 128 }
47
- raw = git(repository, deadline, "cat-file", "--batch", stdin_data: entries.map { |oid, _size| "#{oid}\n" }.join, limit: limit)
48
- Atoms::GitBlobBatch.decode(entries, raw).zip(entries).each { |content, (oid, _size)| yield oid, content }
49
- rescue Atoms::GitBlobBatch::Invalid => error
50
- reject!("Candidate blob batch #{error.reason} differs")
51
- end
52
- end
53
- end
54
- end
55
- end