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,229 +0,0 @@
1
- # frozen_string_literal: true
2
- require_relative "../molecules/execution_scope_lineage"
3
- require "json"
4
-
5
- module Ace
6
- module Assign
7
- module Authority
8
- # Source capabilities only. A serialized descriptor is not admission.
9
- module MaintenanceWorkspaceRetirement
10
- SCHEMA = "ace.maintenance-workspace-retirement/v1"
11
- TARGET_FIELDS = %w[project_id mapping_id assignment_id attempt_id resource journal_commit binding_event_digest release_event_digest descriptor_sha256].freeze
12
- INVOCATION_FIELDS = %w[request_id input_digest operation_owner_binding_digest dispatch_event_digest].freeze
13
- REFERENCES = {
14
- "captured" => %w[intent captured preservation],
15
- "removed" => %w[intent captured preservation removal fence],
16
- "completed" => %w[intent captured preservation removal fence receipt]
17
- }.freeze
18
-
19
- def self.immutable(value)
20
- case value
21
- when Hash then value.to_h { |key, item| [key.dup.freeze, immutable(item)] }.freeze
22
- when Array then value.map { |item| immutable(item) }.freeze
23
- when String then value.dup.freeze
24
- else value.freeze
25
- end
26
- end
27
-
28
- def self.unavailable!(message)
29
- raise AttemptErrors::EvidenceUnavailable, message
30
- end
31
-
32
- class Evidence
33
- attr_reader :descriptor, :owner
34
-
35
- # Only the installed adapter mints this after validating its complete
36
- # collection. Consumers additionally bind the exact adapter instance,
37
- # original canonical projection and held operation before using it.
38
- def self.mint!(owner:, descriptor:)
39
- new(owner: owner, descriptor: descriptor)
40
- end
41
- private_class_method :new
42
-
43
- def initialize(owner:, descriptor:)
44
- @owner = owner
45
- MaintenanceWorkspaceRetirement.unavailable!("retirement evidence descriptor is not an object") unless descriptor.is_a?(Hash)
46
- @descriptor = MaintenanceWorkspaceRetirement.immutable(descriptor)
47
- @thread = Thread.current
48
- @consumption = nil
49
- @expired = false
50
- validate_descriptor!
51
- rescue KeyError, TypeError, ArgumentError
52
- MaintenanceWorkspaceRetirement.unavailable!("retirement evidence descriptor differs")
53
- end
54
-
55
- def verify!(owner:, target:, projection:, phase:, deadline:)
56
- unless !@expired && @thread.equal?(Thread.current) && @owner.equal?(owner) && descriptor.fetch("target") == target &&
57
- descriptor.fetch("phase") == phase && descriptor.fetch("resource") == projection.fetch("workspace_resource")
58
- MaintenanceWorkspaceRetirement.unavailable!("retirement evidence original owner or selection differs")
59
- end
60
- identities = projection.fetch("resource_identities")
61
- root = projection.fetch("worker_cwd")
62
- expected = projection.fetch("parent_resource_declarations").filter_map do |declaration|
63
- path = declaration.fetch("host_path")
64
- next unless declaration.fetch("stage") == "parent" && (path == root || path.start_with?(root + "/"))
65
- matches = identities.select { |row| row.values_at("host_path", "view_path") == declaration.values_at("host_path", "view_path") }
66
- MaintenanceWorkspaceRetirement.unavailable!("retirement declared row has no unique original identity") unless matches.one?
67
- matches.first
68
- end.sort_by { |row| row.values_at("host_path", "view_path") }
69
- unless descriptor.fetch("covered_resources").map { |row| row.fetch("original") } == expected
70
- MaintenanceWorkspaceRetirement.unavailable!("retirement evidence covered resources are not exhaustive")
71
- end
72
- @owner.verify_retirement_evidence!(evidence: self, projection: projection, deadline: deadline)
73
- true
74
- end
75
-
76
- def expire! = @expired = true
77
-
78
- def covered_resource(path)
79
- descriptor.fetch("covered_resources").find { |entry| entry.fetch("original").fetch("host_path") == path }
80
- end
81
-
82
- def verify_consumption!
83
- unless !@expired && @consumption && @thread.equal?(Thread.current)
84
- MaintenanceWorkspaceRetirement.unavailable!("retirement evidence is outside its held observation")
85
- end
86
- true
87
- end
88
-
89
- def with_consumption(verification)
90
- MaintenanceWorkspaceRetirement.unavailable!("retirement observation cannot be nested") if @consumption
91
- installed = true
92
- @consumption = verification
93
- verify_consumption!
94
- verification.call
95
- result = yield
96
- verify_consumption!
97
- verification.call
98
- result
99
- ensure
100
- @consumption = nil if installed
101
- end
102
-
103
- private
104
-
105
- def validate_descriptor!
106
- fields = %w[schema phase target resource covered_resources invocation refs]
107
- target = descriptor.fetch("target")
108
- invocation = descriptor.fetch("invocation")
109
- phase = descriptor.fetch("phase")
110
- unless descriptor.keys.sort == fields.sort && descriptor.fetch("schema") == SCHEMA && REFERENCES.key?(phase) &&
111
- target.is_a?(Hash) && target.keys.sort == TARGET_FIELDS.sort &&
112
- invocation.is_a?(Hash) && invocation.keys.sort == INVOCATION_FIELDS.sort
113
- MaintenanceWorkspaceRetirement.unavailable!("retirement evidence is not a closed supported descriptor")
114
- end
115
- resource!(descriptor.fetch("resource"))
116
- unless target.fetch("resource") == "workspace:#{target.fetch('project_id')}:#{target.fetch('mapping_id')}:#{target.fetch('assignment_id')}" &&
117
- %w[project_id mapping_id assignment_id attempt_id].all? { |key| token?(target[key]) } &&
118
- target.fetch("journal_commit").is_a?(String) && target.fetch("journal_commit").match?(/\A[0-9a-f]{40}\z/) &&
119
- %w[binding_event_digest release_event_digest descriptor_sha256].all? { |key| digest?(target[key]) } &&
120
- token?(invocation.fetch("request_id")) && (INVOCATION_FIELDS - ["request_id"]).all? { |key| digest?(invocation[key]) }
121
- MaintenanceWorkspaceRetirement.unavailable!("retirement evidence selectors differ")
122
- end
123
- covered = descriptor.fetch("covered_resources")
124
- unless covered.is_a?(Array) && covered.size.between?(1, 64)
125
- MaintenanceWorkspaceRetirement.unavailable!("retirement evidence covered inventory differs")
126
- end
127
- covered.each do |entry|
128
- unless entry.is_a?(Hash) && entry.keys.sort == %w[captured_host_path original] && path?(entry["captured_host_path"])
129
- MaintenanceWorkspaceRetirement.unavailable!("retirement evidence covered row differs")
130
- end
131
- resource!(entry.fetch("original"))
132
- end
133
- unless covered.map { |entry| entry.fetch("original") }.uniq.size == covered.size &&
134
- covered.map { |entry| entry.fetch("captured_host_path") }.uniq.size == covered.size
135
- MaintenanceWorkspaceRetirement.unavailable!("retirement evidence covered rows repeat")
136
- end
137
- roots = covered.select { |entry| entry.fetch("original") == descriptor.fetch("resource") }
138
- unless roots.one? && covered.all? { |entry|
139
- original = entry.fetch("original").fetch("host_path")
140
- root = descriptor.fetch("resource").fetch("host_path")
141
- (original == root || original.start_with?(root + "/")) &&
142
- entry.fetch("captured_host_path") == roots.first.fetch("captured_host_path") + original.delete_prefix(root) }
143
- MaintenanceWorkspaceRetirement.unavailable!("retirement evidence captured paths do not match exact relative resources")
144
- end
145
- refs = descriptor.fetch("refs")
146
- unless refs.is_a?(Hash) && refs.keys.sort == REFERENCES.fetch(phase).sort
147
- MaintenanceWorkspaceRetirement.unavailable!("retirement evidence reference set differs")
148
- end
149
- refs.each_value do |ref|
150
- unless ref.is_a?(Hash) && ref.keys.sort == %w[bytes path sha256] && path?(ref["path"]) && digest?(ref["sha256"]) &&
151
- ref["bytes"].is_a?(Integer) && ref["bytes"].between?(1, 64 * 1024 * 1024)
152
- MaintenanceWorkspaceRetirement.unavailable!("retirement evidence reference differs")
153
- end
154
- end
155
- end
156
-
157
- def token?(value) = value.is_a?(String) && value.match?(/\A[A-Za-z0-9][A-Za-z0-9._:-]{0,127}\z/)
158
- def digest?(value) = value.is_a?(String) && value.match?(/\A[0-9a-f]{64}\z/)
159
- def path?(value)
160
- value.is_a?(String) && value.valid_encoding? && value.bytesize.between?(1, 4096) &&
161
- value.start_with?("/") && !value.include?("\0") && File.expand_path(value) == value
162
- end
163
- def resource!(value)
164
- unless value.is_a?(Hash) && value.keys.sort == Molecules::ExecutionScopeLineage::RESOURCE_FIELDS.sort &&
165
- %w[host_path view_path].all? { |key| path?(value[key]) } &&
166
- %w[mount_id device inode].all? { |key| value[key].is_a?(Integer) && value[key].positive? } &&
167
- %w[uid gid].all? { |key| value[key].is_a?(Integer) && value[key] >= 0 } &&
168
- %w[ext4 xfs btrfs tmpfs].include?(value["filesystem_type"])
169
- MaintenanceWorkspaceRetirement.unavailable!("retirement evidence resource identity differs")
170
- end
171
- end
172
- end
173
- class Session
174
- attr_reader :projection
175
- def self.open!(**arguments) = new(**arguments)
176
- private_class_method :new
177
-
178
- def initialize(lifecycle:, mapping_id:, journal:, commit:, target:, projection:, evidence_owner:, deadline:, contexts:)
179
- @lifecycle, @mapping_id, @journal, @commit = lifecycle, mapping_id, journal, commit
180
- @target, @projection = MaintenanceWorkspaceRetirement.immutable(target), projection
181
- @owner, @deadline, @contexts = evidence_owner, deadline, contexts
182
- @thread, @active, @phase = Thread.current, true, nil
183
- @evidences = []
184
- @writer = @owner.retirement_workspace_writer!
185
- unless @writer.is_a?(Molecules::LifecycleExclusion::WorkspaceWriter) && @writer.projection == projection.fetch("workspace_exclusion")
186
- MaintenanceWorkspaceRetirement.unavailable!("retirement session requires the original maintained workspace writer")
187
- end
188
- verify_held!
189
- end
190
-
191
- def verify_captured!(evidence:) = verify_phase!("captured", evidence)
192
- def verify_removed!(evidence:) = verify_phase!("removed", evidence)
193
- def close!
194
- @active = false
195
- @evidences.each(&:expire!)
196
- end
197
-
198
- private
199
-
200
- def verify_held!
201
- unless @active && @thread.equal?(Thread.current) &&
202
- Thread.current[:ace_assign_maintenance_contexts]&.[](@lifecycle.object_id).equal?(@contexts)
203
- MaintenanceWorkspaceRetirement.unavailable!("retirement session no longer owns complete maintenance contexts")
204
- end
205
- @lifecycle.send(:maintenance_deadline!, @deadline)
206
- @lifecycle.send(:require_maintenance_context!, @mapping_id, @journal, @commit)
207
- @writer.verify_unchanged!
208
- end
209
-
210
- def verify_phase!(phase, evidence)
211
- unless evidence.is_a?(Evidence) && (phase == "captured" && @phase.nil? || phase == "removed" && @phase == "captured")
212
- MaintenanceWorkspaceRetirement.unavailable!("retirement session phase or evidence differs")
213
- end
214
- @evidences << evidence
215
- verify = -> {
216
- verify_held!
217
- evidence.verify!(owner: @owner, target: @target, projection: @projection, phase: phase, deadline: @deadline)
218
- }
219
- evidence.with_consumption(verify) do
220
- @lifecycle.send(:verify_retirement_contexts!, evidence: evidence, deadline: @deadline)
221
- end
222
- @phase = phase
223
- true
224
- end
225
- end
226
- end
227
- end
228
- end
229
- end
@@ -1,137 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ace
4
- module Assign
5
- module Authority
6
- module OrdinaryAttemptStart
7
- START_KEYS = %w[assignment_id attempt_id expected_generation head pane scope].freeze
8
-
9
- private
10
-
11
- def start_attempt(request, peer, role, deadline)
12
- params = request.fetch("params")
13
- strict!(params, START_KEYS)
14
- %w[assignment_id attempt_id].each { |key| token!(params.fetch(key)) }
15
- if params.fetch("attempt_id").start_with?("definition-")
16
- raise ArgumentError, "ordinary attempt ID uses the registration namespace"
17
- end
18
- token!(request.fetch("mutation_id"))
19
- unless params.fetch("head").is_a?(String) && params.fetch("head").match?(/\A[0-9a-f]{40}\z/)
20
- raise ArgumentError, "ordinary start HEAD is invalid"
21
- end
22
- unless %i[supervisor worker].include?(role) && params.fetch("expected_generation").is_a?(Integer) && params.fetch("expected_generation") == 0
23
- raise AttemptErrors::UnauthorizedIdentity, "ordinary start requires an installed worker and a new attempt"
24
- end
25
- commit = @journal.ref_value
26
- registered = registration(params.fetch("assignment_id"), commit: commit)
27
- raise AttemptErrors::NotFound, "ordinary work is not registered" unless registered
28
- work = registered_work(registered, commit, deadline)
29
- scope = params.fetch("scope")
30
- unless work.manifest.fetch("steps").any? { |step| step.fetch("number") == scope }
31
- raise AttemptErrors::InvalidScope, "ordinary scope is not in the accepted work"
32
- end
33
- keys = [@exclusion.task_key(registered.fetch("task_id")), @exclusion.assignment_key(params.fetch("assignment_id"))]
34
- @exclusion.with_shared_multi(keys, deadline: deadline) do
35
- if @journal.mutation_result(request.fetch("mutation_id"))
36
- # Work may legitimately have advanced since an acknowledged or
37
- # lost start reply. Recover its original head, never recapture it.
38
- identity = @admission.identity!(peer: peer, pane: params.fetch("pane"), deadline: deadline)
39
- return @journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
40
- mutation_id: request.fetch("mutation_id"), operation: "start_attempt", parameters_digest: start_digest(params, peer, identity),
41
- expected_generation: 0, with_replay: true, deadline: deadline,
42
- replay_authorizer: -> { authorize_start_replay!(request, peer, role, deadline) }) do
43
- raise AttemptErrors::EvidenceUnavailable, "accepted ordinary start disappeared"
44
- end
45
- end
46
- # Only the installed admission can observe the actual native owner
47
- # and worker checkout. It performs no launch or delivery effect.
48
- observed = @admission.execution!(peer: peer, pane: params.fetch("pane"), head: params.fetch("head"), deadline: deadline)
49
- identity = observed.fetch(:identity)
50
- principal = {"uid" => peer.fetch("uid"), "actor" => identity.actor}
51
- digest = start_digest(params, peer, identity)
52
- authorize!(request, peer, role, deadline)
53
- @journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
54
- mutation_id: request.fetch("mutation_id"), operation: "start_attempt", parameters_digest: digest,
55
- expected_generation: 0, with_replay: true, deadline: deadline,
56
- replay_authorizer: -> { authorize_start_replay!(request, peer, role, deadline) }) do |events, prefix, _generation|
57
- authorize!(request, peer, role, deadline)
58
- unless @admission.identity!(peer: peer, pane: params.fetch("pane"), deadline: deadline) == identity
59
- raise AttemptErrors::UnauthorizedIdentity, "ordinary native worker changed before start acceptance"
60
- end
61
- unless events.empty? && registration(params.fetch("assignment_id"), commit: prefix) == registered
62
- raise AttemptErrors::Conflict, "ordinary start registration or attempt changed"
63
- end
64
- if @journal.assignment_ids(commit: prefix).any? { |assignment|
65
- assignment != params.fetch("assignment_id") && @journal.read_events(assignment, commit: prefix).any? { |event|
66
- event.fetch("attempt_id") == params.fetch("attempt_id") } }
67
- raise AttemptErrors::Conflict, "ordinary attempt ID belongs to another assignment"
68
- end
69
- # Sibling scopes may run independently. A different request ID
70
- # never repeats an already consumed scope or any part of it.
71
- if @journal.derived_attempts(params.fetch("assignment_id"), commit: prefix).any? { |attempt|
72
- Atoms::AssignmentScope.overlap?(attempt.binding.scope, scope) }
73
- raise AttemptErrors::Conflict, "ordinary scope already has an attempt; reconcile its original execution"
74
- end
75
- record = current_queue_record(registered, prefix)
76
- queue = OrdinaryQueue.new(work: work, root: @input_root, bytes: queue_bytes(record, prefix))
77
- .transition(operation: :activate, scope: scope)
78
- queue_sha = Digest::SHA256.hexdigest(queue.bytes)
79
- queue_path = "execution/definitions/#{params.fetch('assignment_id')}-queue-#{queue_sha}.json"
80
- {events: [
81
- {type: "intent", payload: {"assignment_id" => params.fetch("assignment_id"), "scope" => scope,
82
- "project_id" => @project, "task_id" => registered.fetch("task_id"), "base_head" => observed.fetch(:checkout).fetch("head")}},
83
- {type: "process_start", payload: {"actor" => identity.actor, "role" => identity.role, "runtime" => identity.runtime,
84
- "pid" => identity.process_pid, "process_identity" => identity.runtime_binding.fetch("process_identity"),
85
- "runtime_binding" => identity.runtime_binding, "checkout" => observed.fetch(:checkout)}}
86
- ], blobs: {queue_path => queue.bytes}, data: {
87
- "authority" => "ordinary", "assignment_id" => params.fetch("assignment_id"), "attempt_id" => params.fetch("attempt_id"),
88
- "project_id" => @project, "scope" => scope, "state" => "running", "started_by" => principal,
89
- "base_head" => observed.fetch(:checkout).fetch("head"), "runtime_binding" => identity.runtime_binding,
90
- "queue_ref" => queue_path, "queue_sha256" => queue_sha}}
91
- end
92
- end
93
- end
94
-
95
- def start_digest(params, peer, identity)
96
- Atoms::EvidenceDigest.digest({"params" => params, "project_id" => @project,
97
- "principal" => {"uid" => peer.fetch("uid"), "actor" => identity.actor}})
98
- end
99
-
100
- def authorize_start_replay!(request, peer, role, deadline)
101
- authorize!(request, peer, role, deadline)
102
- params = request.fetch("params")
103
- identity = @admission.identity!(peer: peer, pane: params.fetch("pane"), deadline: deadline)
104
- start_replay_owner!(params, identity)
105
- end
106
-
107
- def registered_work(registered, commit, deadline)
108
- reference = registered.fetch("prepared_work")
109
- bytes = @journal.bounded_blob(registered.fetch("prepared_bundle_ref"), commit: commit,
110
- max_bytes: registered.fetch("prepared_bundle_bytes"))
111
- work = PreparedWork.admit(bytes: bytes, head: reference.fetch("prepared_head"), tree: reference.fetch("prepared_tree"),
112
- sha256: registered.fetch("prepared_bundle_sha256"), size: registered.fetch("prepared_bundle_bytes"),
113
- root: @input_root, deadline: deadline)
114
- unless work.reference(head: reference.fetch("prepared_head"), tree: reference.fetch("prepared_tree")) == reference &&
115
- work.manifest.values_at("assignment_id", "project_id", "task_id") == registered.values_at("assignment_id", "project_id", "task_id") &&
116
- Digest::SHA256.hexdigest(work.definition_bytes(head: reference.fetch("prepared_head"), tree: reference.fetch("prepared_tree"))) == registered.fetch("definition_digest")
117
- raise AttemptErrors::EvidenceUnavailable, "ordinary prepared registration bytes differ"
118
- end
119
- work
120
- end
121
-
122
- def start_replay_owner!(params, identity)
123
- events = @journal.read_events(params.fetch("assignment_id"))
124
- .select { |event| event.fetch("attempt_id") == params.fetch("attempt_id") }
125
- unless Models::EvidenceEvent.chain_valid?(events) && events.count { |event| event["type"] == "process_start" } == 1
126
- raise AttemptErrors::EvidenceUnavailable, "ordinary start evidence is unavailable"
127
- end
128
- owner = events.find { |event| event["type"] == "process_start" }.fetch("payload")
129
- unless owner.values_at("actor", "role", "runtime", "runtime_binding") ==
130
- [identity.actor, identity.role, identity.runtime, identity.runtime_binding]
131
- raise AttemptErrors::UnauthorizedIdentity, "ordinary replay belongs to its original native worker"
132
- end
133
- end
134
- end
135
- end
136
- end
137
- end
@@ -1,70 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "ordinary_peer"
4
- require_relative "transfer_codec"
5
-
6
- module Ace
7
- module Assign
8
- module Authority
9
- # Installed selection is supplied by the caller's trusted composition.
10
- # This client never selects a journal, local fallback or execution identity.
11
- class OrdinaryClient
12
- Reply = Struct.new(:data, :replayed, :parts, keyword_init: true)
13
-
14
- def initialize(project_id:, service:, verify_selection:, peer: OrdinaryPeer.new, scratch_root: nil)
15
- @project_id, @service, @verify_selection, @peer, @scratch_root = project_id, service, verify_selection, peer, scratch_root
16
- end
17
-
18
- def call(operation, params, mutation_id: nil, upload_parts: nil, download: false, purpose: nil, timeout: 30)
19
- unless operation.is_a?(String) && params.is_a?(Hash) && !params.key?("transfer") &&
20
- timeout.is_a?(Numeric) && timeout.finite? && timeout.positive?
21
- raise ArgumentError, "invalid ordinary authority request"
22
- end
23
- raise ArgumentError, "ordinary download needs private scratch and forbids upload" if download && (upload_parts || !@scratch_root)
24
- @verify_selection.call
25
- wire = Ace::Runtime::Molecules::ProtectedSocket
26
- path = @service.fetch("socket_path")
27
- wire.root_path!(File.dirname(path), directory: true, owner: @service.fetch("uid"))
28
- before = wire.socket_identity(path, mode: 0o660)
29
- raise AttemptErrors::UnauthorizedIdentity, "ordinary endpoint owner differs" unless before.last == @service.fetch("uid")
30
- codec = TransferCodec.new(root: download ? @scratch_root : nil)
31
- descriptor = codec.descriptor(upload_parts, purpose: purpose) if upload_parts
32
- parameters = descriptor ? params.merge("transfer" => descriptor) : params
33
- deadline = wire.deadline([timeout, 30].min)
34
- wire.connect(path, deadline: deadline) do |socket|
35
- observed = @peer.capture(socket, deadline: deadline)
36
- unless observed.values_at("uid", "gid") == @service.values_at("uid", "gid") && wire.socket_identity(path, mode: 0o660) == before
37
- raise AttemptErrors::UnauthorizedIdentity, "ordinary endpoint peer changed"
38
- end
39
- @verify_selection.call
40
- wire.write(socket, {"version" => 1, "operation" => operation, "mutation_id" => mutation_id,
41
- "project_id" => @project_id, "params" => parameters}, deadline: deadline, limit: 16_384)
42
- codec.send(socket, parts: upload_parts, descriptor: descriptor, purpose: purpose, deadline: deadline) if upload_parts
43
- socket.shutdown(Socket::SHUT_WR)
44
- ingress = wire::Ingress.new(socket)
45
- result = wire.read(ingress, deadline: deadline, limit: 16_384)
46
- unless result.is_a?(Hash) && result.keys.sort == %w[data status transport] && result["status"] == "ok" &&
47
- result["data"].is_a?(Hash) && result["transport"].is_a?(Hash) &&
48
- result["transport"].keys == ["replayed"] && [true, false].include?(result["transport"]["replayed"])
49
- code = result.is_a?(Hash) && result["error"].is_a?(Hash) ? result["error"]["code"] : nil
50
- raise AttemptErrors::EvidenceUnavailable, "ordinary authority refused (#{code || 'invalid_response'}); retain original mutation"
51
- end
52
- parts = if download
53
- codec.receive(ingress, descriptor: result.fetch("data").fetch("transfer"), purpose: purpose, deadline: deadline) do |input|
54
- Array.new(input.count) { |index| input.bytes(index: index) }
55
- end
56
- end
57
- @verify_selection.call
58
- @peer.live!(observed, deadline: deadline)
59
- unless wire.socket_identity(path, mode: 0o660) == before
60
- raise AttemptErrors::UnauthorizedIdentity, "ordinary endpoint changed before response acceptance"
61
- end
62
- Reply.new(data: result.fetch("data"), replayed: result.fetch("transport").fetch("replayed"), parts: parts)
63
- end
64
- rescue SystemCallError, IOError, Ace::Runtime::RuntimeUnavailableError
65
- raise AttemptErrors::EvidenceUnavailable, "ordinary authority outcome unavailable; retain original mutation; no local fallback"
66
- end
67
- end
68
- end
69
- end
70
- end
@@ -1,104 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "ordinary_effect_settlement"
4
- require_relative "../organisms/attempt_coordinator"
5
-
6
- module Ace
7
- module Assign
8
- module Authority
9
- # Terminal acceptance consumes the submitted canonical receipt. The
10
- # supervisor cannot replace its bytes, worker, scope or candidate.
11
- module OrdinaryCompletion
12
- FINISH_KEYS = %w[assignment_id attempt_id expected_generation pane head result_id receipt_digest].freeze
13
-
14
- private
15
-
16
- def finish_result(request, peer, role, deadline)
17
- params = request.fetch("params")
18
- strict!(params, FINISH_KEYS)
19
- %w[assignment_id attempt_id result_id].each { |key| token!(params.fetch(key)) }
20
- token!(request.fetch("mutation_id"))
21
- unless role == :supervisor
22
- raise AttemptErrors::UnauthorizedIdentity, "ordinary completion requires the installed supervisor"
23
- end
24
- unless params.fetch("expected_generation").is_a?(Integer) && params.fetch("expected_generation").positive? &&
25
- params.fetch("head").is_a?(String) && params.fetch("head").match?(/\A[0-9a-f]{40}\z/) &&
26
- params.fetch("receipt_digest").is_a?(String) && params.fetch("receipt_digest").match?(/\A[0-9a-f]{64}\z/)
27
- raise ArgumentError, "ordinary completion selection is invalid"
28
- end
29
- registered = registration(params.fetch("assignment_id"), commit: @journal.ref_value)
30
- raise AttemptErrors::NotFound, "ordinary work is not registered" unless registered
31
- verify_result_owner!(request, peer, role, deadline, registered)
32
- digest = Atoms::EvidenceDigest.digest({"params" => params, "project_id" => @project, "peer_uid" => peer.fetch("uid")})
33
- @exclusion.with_shared_multi([@exclusion.task_key(registered.fetch("task_id"))], deadline: deadline) do
34
- @exclusion.with_exclusive(@exclusion.assignment_key(params.fetch("assignment_id")), deadline: deadline) do
35
- authorize!(request, peer, role, deadline)
36
- with_result_transport(request, deadline) do |transport|
37
- @journal.mutate(assignment_id: params.fetch("assignment_id"), attempt_id: params.fetch("attempt_id"),
38
- mutation_id: request.fetch("mutation_id"), operation: "finish", parameters_digest: digest,
39
- expected_generation: params.fetch("expected_generation"), with_replay: true, deadline: deadline,
40
- replay_authorizer: -> { verify_result_owner!(request, peer, role, deadline, registered) }) do |events, prefix, _generation|
41
- verify_result_owner!(request, peer, role, deadline, registered)
42
- unless Molecules::CanonicalAttemptState.derive(events) == "running"
43
- raise AttemptErrors::Conflict, "ordinary completion requires a running attempt"
44
- end
45
- result = retained_ordinary_result(params, events, prefix)
46
- unless result && result.fetch("result_id") == params.fetch("result_id") &&
47
- result.dig("receipt", "digest") == params.fetch("receipt_digest") && result.dig("binding", "head") == params.fetch("head")
48
- raise AttemptErrors::ReceiptRejected, "ordinary completion requires its exact submitted result"
49
- end
50
- binding = result.fetch("binding")
51
- observed = @admission.recorded_execution!(binding: binding.fetch("runtime_binding"), head: binding.fetch("head"), deadline: deadline)
52
- unless observed.fetch(:runtime_binding) == binding.fetch("runtime_binding") && observed.fetch(:checkout) == binding.fetch("checkout")
53
- raise AttemptErrors::UnauthorizedIdentity, "ordinary result worker checkout changed"
54
- end
55
- references = events.select { |event| event["type"] == "inbox_binding" }.map { |event| event.fetch("payload") }
56
- delivery = transport.call(references: references, attempt_id: params.fetch("attempt_id"), owner: binding.fetch("runtime_binding"))
57
- scope = events.find { |event| event["type"] == "intent" }.fetch("payload").fetch("scope")
58
- work = registered_work(registered, prefix, deadline)
59
- queue = OrdinaryQueue.new(work: work, root: @input_root, bytes: queue_bytes(current_queue_record(registered, prefix), prefix))
60
- verdict = result.dig("receipt", "verdict")
61
- complete = verdict == "succeeded" ? queue.status.subtree_complete?(scope) : queue.status.subtree_failed?(scope)
62
- raise AttemptErrors::Conflict, "ordinary result scope has unfinished progress" unless complete
63
- settlement = OrdinaryEffectSettlement.new(journal: @journal).verify!(project: @project,
64
- assignment: params.fetch("assignment_id"), attempt: params.fetch("attempt_id"), events: events, commit: prefix)
65
- verify_result_owner!(request, peer, role, deadline, registered)
66
- plan = Organisms::AttemptCoordinator.finished_transition_plan(events: events, receipt: result.fetch("receipt"), authority: :ordinary)
67
- plan.merge(data: params.slice("assignment_id", "attempt_id", "result_id", "head", "receipt_digest").merge(
68
- "authority" => "ordinary", "scope" => scope, "state" => verdict, "settlement" => settlement, "inbox_deliveries" => delivery))
69
- end
70
- end
71
- end
72
- end
73
- rescue Ace::Herdr::Error => error
74
- raise AttemptErrors::EvidenceUnavailable, "ordinary transport settlement is unavailable: #{error.message}"
75
- end
76
-
77
- def verify_result_owner!(request, peer, role, deadline, registered)
78
- authorize!(request, peer, role, deadline)
79
- supervisor = @admission.identity!(peer: peer, pane: request.fetch("params").fetch("pane"), deadline: deadline)
80
- unless supervisor.role == "coordinator" &&
81
- supervisor.runtime_binding == supervisor_selection(registered, @journal.ref_value).fetch("binding")
82
- raise AttemptErrors::UnauthorizedIdentity, "ordinary completion belongs to its original supervisor"
83
- end
84
- end
85
-
86
- def with_result_transport(request, deadline)
87
- if @journal.mutation_result(request.fetch("mutation_id"), commit: @journal.ref_value)
88
- # A terminal reply is immutable. Current peer authority is still
89
- # checked by mutate, but retired transport files are not replay input.
90
- return yield nil
91
- end
92
- if @inbox
93
- @inbox.with_delivery_settlement(deadline: deadline) { |verify| yield verify }
94
- else
95
- yield lambda { |references:, **_|
96
- raise AttemptErrors::EvidenceUnavailable, "ordinary Inbox owner is not installed" unless references.empty?
97
- []
98
- }
99
- end
100
- end
101
- end
102
- end
103
- end
104
- end