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