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,257 +0,0 @@
1
- # frozen_string_literal: true
2
- require "securerandom"
3
- require_relative "deployment"
4
- require_relative "transfer_codec"
5
- require_relative "campaign_execution"
6
- require_relative "candidate_transfer"
7
- require_relative "../molecules/canonical_evidence"
8
- require_relative "../molecules/execution_scope_lineage"
9
-
10
- module Ace
11
- module Assign
12
- module Authority
13
- class Client
14
- Reply = Struct.new(:data, :replayed, :parts, keyword_init: true)
15
- attr_reader :mapping_id
16
- def initialize(mapping_id:, deployment: Deployment.load, kernel: Ace::Runtime::Molecules::ProtectedLinux.new)
17
- @mapping_id, @deployment, @kernel = mapping_id, deployment, kernel
18
- @map = deployment.mapping(mapping_id)
19
- @service = deployment.authority(@map.fetch("authority_id"))
20
- end
21
-
22
- def call(operation, params, mutation_id: nil, timeout: nil, upload_parts: nil, download: false, purpose: nil)
23
- if operation == "register_assignment" && (!upload_parts || purpose != :candidate || download)
24
- raise ArgumentError, "prepared registration requires fixed candidate upload"
25
- end
26
- if operation == "evidence_fetch" && (!download || purpose != (%w[prepared_work campaign_candidate].include?(params["kind"]) ? :candidate : :artifacts) || upload_parts)
27
- raise ArgumentError, "evidence fetch requires source-fixed kind download"
28
- end
29
- if operation == "submit_result" && (!upload_parts || purpose != :receipt_artifacts || download)
30
- raise ArgumentError, "result submission requires fixed receipt upload"
31
- end
32
- if operation == "campaign_export_result" && (!download || purpose != :artifacts || upload_parts || mutation_id)
33
- raise ArgumentError, "campaign result requires fixed read-only artifact download"
34
- end
35
- if operation == "campaign_record_round" && (!upload_parts || purpose != :receipt_artifacts || download)
36
- raise ArgumentError, "campaign round requires fixed bounded upload"
37
- end
38
- @deployment.verify!(mapping_id, kernel: @kernel)
39
- wire = Ace::Runtime::Molecules::ProtectedSocket
40
- path = @service.fetch("socket_path")
41
- before = wire.socket_identity(path)
42
- raise Ace::Runtime::RuntimeUnavailableError, "authority endpoint owner differs" unless before.last == @service.fetch("uid")
43
- raise ArgumentError, "cannot upload and download on one request" if upload_parts && download
44
- prepared_download = operation == "evidence_fetch" && %w[prepared_work campaign_candidate].include?(params["kind"])
45
- codec = transfer_codec if upload_parts || download && !prepared_download
46
- descriptor = codec.descriptor(upload_parts, purpose: purpose) if upload_parts
47
- parameters = upload_parts ? params.merge("transfer" => descriptor) : params
48
- cap = %w[reserve_attempt close_execution_scope stop_attempt].include?(operation) ? 90 : 30
49
- duration = timeout || (cap == 90 ? 90 : 5)
50
- unless duration.is_a?(Numeric) && duration.positive? && duration.finite?
51
- raise ArgumentError, "authority timeout must be finite and positive"
52
- end
53
- deadline = wire.deadline([duration, cap].min)
54
- wire.connect(path, deadline: deadline) do |socket|
55
- peer = @kernel.peer(socket)
56
- unless peer["uid"] == @service.fetch("uid") && peer["gid"] == @service.fetch("gid") &&
57
- peer["groups"] == @service.fetch("groups") && wire.socket_identity(path) == before
58
- raise AttemptErrors::UnauthorizedIdentity, "authority endpoint peer changed"
59
- end
60
- wire.write(socket, {"version" => 1, "operation" => operation,
61
- "mutation_id" => mutation_id, "project_id" => @map.fetch("project_id"),
62
- "params" => parameters.merge("mapping_id" => mapping_id)}, deadline: deadline, limit: upload_parts || download ? 16_384 : wire::LIMIT)
63
- if %w[bind_inbox finish recover request_review review_status launch_review_intent cancel_review assignment_inventory evidence_fetch observe_execution_scope close_execution_scope stop_attempt prompt_status launch_input_inhibit_selection launch_input_inhibit_completion launch_prompt_intent launch_prompt_completion claim_service_settlement workspace_prune_preview_context].include?(operation) || (operation == "attempt_status" && params.key?("result_candidate_generation"))
64
- socket.shutdown(Socket::SHUT_WR)
65
- end
66
- if upload_parts
67
- codec.send(socket, parts: upload_parts, descriptor: descriptor, purpose: purpose, deadline: deadline)
68
- socket.shutdown(Socket::SHUT_WR)
69
- end
70
- result = wire.read(socket, deadline: deadline, limit: 16_384)
71
- unless result.is_a?(Hash) && result["status"] == "ok" && result["data"].is_a?(Hash)
72
- code = result.is_a?(Hash) ? result.dig("error", "code") : nil
73
- raise AttemptErrors::BoundedResultUnavailable, "protected authority result exceeds frame bound" if code == "bounded_result"
74
- raise AttemptErrors::EvidenceUnavailable, "protected authority refused (#{code || 'invalid_response'})"
75
- end
76
- transport = result.fetch("transport")
77
- unless transport.is_a?(Hash) && transport.keys == ["replayed"] && [true, false].include?(transport["replayed"])
78
- raise AttemptErrors::EvidenceUnavailable, "authority replay metadata is unavailable"
79
- end
80
- if operation == "evidence_fetch"
81
- if %w[prepared_work campaign_candidate].include?(params["kind"]) && transport.fetch("replayed")
82
- raise AttemptErrors::EvidenceUnavailable, "prepared fetch cannot replay a mutation"
83
- end
84
- validate_evidence_download!(result.fetch("data"), params)
85
- end
86
- parts = if download
87
- codec = prepared_transfer_codec(result.fetch("data").fetch("descriptor")) if prepared_download
88
- codec.receive(socket, descriptor: result.fetch("data").fetch("transfer"), purpose: purpose, deadline: deadline) do |input|
89
- Array.new(input.count) { |index| input.bytes(index: index) }
90
- end
91
- end
92
- Reply.new(data: result.fetch("data"), replayed: transport.fetch("replayed"), parts: parts)
93
- end
94
- rescue SystemCallError, IOError
95
- raise AttemptErrors::EvidenceUnavailable, "protected authority connection unavailable; no local fallback"
96
- end
97
-
98
-
99
- # Fixed original-launcher stream. Public calls retain their existing
100
- # complete-upload EOF contract; no caller-selected framing flag exists.
101
- def with_launch_control(state:)
102
- @deployment.verify!(mapping_id, kernel: @kernel)
103
- wire = Ace::Runtime::Molecules::ProtectedSocket
104
- path = @service.fetch("socket_path")
105
- before = wire.socket_identity(path)
106
- raise Ace::Runtime::RuntimeUnavailableError, "Authority endpoint owner differs" unless before.last == @service.fetch("uid")
107
- deadline = wire.deadline(30)
108
- wire.connect(path, deadline: deadline) do |socket|
109
- peer = @kernel.peer(socket)
110
- unless peer.values_at("uid", "gid", "groups") == @service.values_at("uid", "gid", "groups") && wire.socket_identity(path) == before
111
- raise AttemptErrors::UnauthorizedIdentity, "Authority stream peer changed"
112
- end
113
- wire.write(socket, {"version" => 1, "operation" => "launch_control", "mutation_id" => nil,
114
- "project_id" => @map.fetch("project_id"), "params" => state.slice("assignment_id", "attempt_id").merge("mapping_id" => mapping_id)},
115
- deadline: deadline, limit: 16_384)
116
- ready = wire.read(socket, deadline: deadline, limit: 16_384)
117
- validate_launch_control_ready!(ready, state)
118
- yield socket, transfer_codec, ready
119
- end
120
- rescue IOError, SystemCallError
121
- raise AttemptErrors::EvidenceUnavailable, "Original launch control connection is unavailable"
122
- end
123
-
124
- private
125
-
126
- def validate_launch_control_ready!(ready, state)
127
- unless ready.is_a?(Hash) && ready.keys.sort == %w[attempt_id generation journal_commit original_binding_digest type version] &&
128
- ready["version"].is_a?(Integer) && ready["version"] == 1 && ready["type"] == "launch_control_ready" &&
129
- ready["attempt_id"] == state.fetch("attempt_id") && ready["generation"].is_a?(Integer) && ready["generation"].positive? &&
130
- ready["journal_commit"].is_a?(String) && ready["journal_commit"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/) &&
131
- ready["original_binding_digest"].is_a?(String) && ready["original_binding_digest"].match?(/\A[0-9a-f]{64}\z/)
132
- raise AttemptErrors::EvidenceUnavailable, "Original launch control admission is unavailable"
133
- end
134
- true
135
- end
136
-
137
- def validate_evidence_download!(data, params)
138
- descriptor, transfer = data.values_at("descriptor", "transfer")
139
- if params["kind"] == "prepared_work"
140
- validate_prepared_download!(data, params)
141
- return
142
- end
143
- if params["kind"] == "campaign_candidate"
144
- validate_campaign_candidate_download!(data, params)
145
- return
146
- end
147
- unless data.keys.sort == %w[descriptor generation journal_commit transfer] &&
148
- Molecules::CanonicalEvidence.valid_descriptor?(descriptor) &&
149
- descriptor["project_id"] == @map.fetch("project_id") &&
150
- {"assignment_id" => "assignment_id", "attempt_id" => "attempt_id", "kind" => "kind",
151
- "request_id_or_event_id" => "purpose_id", "artifact_id" => "artifact_id"}.all? { |field, key| descriptor[field] == params[key] } &&
152
- data["generation"].is_a?(Integer) && data["generation"] >= 0 &&
153
- data["journal_commit"].is_a?(String) && data["journal_commit"].match?(/\A[0-9a-f]{40,64}\z/) &&
154
- transfer.is_a?(Hash) && transfer.keys.sort == %w[bytes parts sha256 version] && transfer["version"] == 1 &&
155
- transfer["bytes"] == descriptor["bytes"] && transfer["sha256"] == descriptor["sha256"] &&
156
- transfer["parts"] == [descriptor.slice("bytes", "sha256")]
157
- raise AttemptErrors::EvidenceUnavailable, "canonical download descriptor differs"
158
- end
159
- end
160
-
161
- def transfer_codec
162
- peer = @kernel.capture(Process.pid)
163
- project = @deployment.project(@map.fetch("project_id"))
164
- installed = project.fetch("peer_credentials").fetch(peer.fetch("uid").to_s)
165
- unless peer["gid"] == installed["gid"] && peer["groups"] == installed["groups"]
166
- raise AttemptErrors::UnauthorizedIdentity, "transfer scratch principal differs"
167
- end
168
- TransferCodec.new(root: installed.fetch("scratch_root"))
169
- end
170
-
171
- def validate_prepared_download!(data, params)
172
- descriptor, transfer = data.values_at("descriptor", "transfer")
173
- fields = %w[version kind purpose artifact project_id mapping_id assignment_id attempt_id task_id scope definition_digest selection_sha256 prepared_head prepared_tree manifest_bytes manifest_sha256 registration_generation registration_commit original_binding_digest ref bytes sha256 task_context_entry original_worker_identity original_worker_scratch_root workspace_exclusion].sort
174
- valid = data.keys.sort == %w[descriptor generation journal_commit transfer] && descriptor.is_a?(Hash) && descriptor.keys.sort == fields &&
175
- descriptor["version"].is_a?(Integer) && descriptor["version"] == 1 &&
176
- params.values_at("kind", "purpose_id", "artifact_id") == %w[prepared_work original_prepared_work prepared_bundle] &&
177
- descriptor.values_at("kind", "purpose", "artifact") == %w[prepared_work original_prepared_work prepared_bundle] &&
178
- descriptor["project_id"] == @map.fetch("project_id") && descriptor["mapping_id"] == mapping_id &&
179
- %w[assignment_id attempt_id].all? { |key| descriptor[key] == params[key] } &&
180
- descriptor["task_id"].is_a?(String) && descriptor["task_id"].match?(Deployment::TOKEN) &&
181
- descriptor["scope"].is_a?(String) && descriptor["scope"].bytesize.between?(1, 128) && descriptor["scope"].match?(/\A[0-9]+(?:\.[0-9]+)*\z/) &&
182
- %w[definition_digest selection_sha256 manifest_sha256 original_binding_digest sha256].all? { |key| descriptor[key].is_a?(String) && descriptor[key].match?(/\A[0-9a-f]{64}\z/) } &&
183
- %w[prepared_head prepared_tree].all? { |key| descriptor[key].is_a?(String) && descriptor[key].match?(/\A[0-9a-f]{40}\z/) } &&
184
- descriptor["manifest_bytes"].is_a?(Integer) && descriptor["manifest_bytes"].between?(1, 32_768) &&
185
- descriptor["registration_generation"].is_a?(Integer) && descriptor["registration_generation"].positive? &&
186
- descriptor["bytes"].is_a?(Integer) && descriptor["bytes"].between?(1, TransferCodec::LIMITS.fetch(:candidate).first) &&
187
- descriptor["registration_commit"].is_a?(String) && descriptor["registration_commit"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/) &&
188
- descriptor["ref"] == "execution/prepared/#{params.fetch('assignment_id')}-#{descriptor['sha256']}.bundle" &&
189
- data["generation"].is_a?(Integer) && data["generation"].positive? &&
190
- data["journal_commit"].is_a?(String) && data["journal_commit"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/) &&
191
- transfer.is_a?(Hash) && transfer.keys.sort == %w[bytes parts sha256 version] && transfer["version"].is_a?(Integer) && transfer["version"] == 1 &&
192
- transfer.values_at("bytes", "sha256") == descriptor.values_at("bytes", "sha256") && transfer["parts"] == [descriptor.slice("bytes", "sha256")]
193
- raise AttemptErrors::EvidenceUnavailable, "original prepared download descriptor differs" unless valid
194
- TaskContextEntry.validate!(descriptor.fetch("task_context_entry"))
195
- Molecules::LifecycleExclusion.workspace_reader(projection: descriptor.fetch("workspace_exclusion"))
196
- unless descriptor.dig("workspace_exclusion", "root_resource", "view_path") ==
197
- "/run/ace/lifecycle-exclusion/#{descriptor.fetch('mapping_id')}"
198
- raise AttemptErrors::EvidenceUnavailable, "original prepared lifecycle mapping differs"
199
- end
200
- identity = descriptor.fetch("original_worker_identity")
201
- birth = identity.is_a?(Hash) && identity["started_at"]
202
- boot = birth.is_a?(String) && birth.split(":", -1)[1]
203
- Molecules::ExecutionScopeLineage.validate_process_identity!(identity, boot_id: boot)
204
- root = descriptor.fetch("original_worker_scratch_root")
205
- unless root.is_a?(String) && root.valid_encoding? && root.bytesize.between?(2, 4096) &&
206
- root.start_with?("/") && !root.include?("\0") && File.expand_path(root) == root
207
- raise AttemptErrors::EvidenceUnavailable, "original prepared scratch root differs"
208
- end
209
- rescue ArgumentError, KeyError
210
- raise AttemptErrors::EvidenceUnavailable, "original prepared download descriptor differs"
211
- end
212
-
213
- def validate_campaign_candidate_download!(data, params)
214
- descriptor, transfer = data.values_at("descriptor", "transfer")
215
- fields = %w[head tree bytes sha256 candidate_generation campaign_execution mapping_id assignment_id attempt_id
216
- project_id original_worker_identity original_worker_scratch_root original_binding_digest].sort
217
- unless data.keys.sort == %w[descriptor generation journal_commit transfer] &&
218
- descriptor.is_a?(Hash) && descriptor.keys.sort == fields &&
219
- params.values_at("purpose_id", "artifact_id") == %w[original_campaign_candidate candidate_bundle] &&
220
- descriptor["mapping_id"] == mapping_id && descriptor["project_id"] == @map.fetch("project_id") &&
221
- %w[assignment_id attempt_id].all? { |key| descriptor[key] == params[key] } &&
222
- %w[head tree].all? { |key| descriptor[key].is_a?(String) && descriptor[key].match?(CandidateTransfer::SHA) } &&
223
- %w[sha256 original_binding_digest].all? { |key| descriptor[key].is_a?(String) && descriptor[key].match?(/\A[0-9a-f]{64}\z/) } &&
224
- descriptor["candidate_generation"].is_a?(Integer) && descriptor["candidate_generation"].positive? &&
225
- descriptor["bytes"].is_a?(Integer) && descriptor["bytes"].between?(1, CandidateTransfer::MAX_BYTES) &&
226
- descriptor["campaign_execution"].is_a?(Hash) && descriptor["campaign_execution"].keys.sort == CampaignExecution::FIELDS &&
227
- data["generation"].is_a?(Integer) && data["generation"].positive? &&
228
- data["journal_commit"].is_a?(String) && data["journal_commit"].match?(/\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/) &&
229
- transfer.is_a?(Hash) && transfer.keys.sort == %w[bytes parts sha256 version] && transfer["version"] == 1 &&
230
- transfer["version"].is_a?(Integer) &&
231
- transfer.values_at("bytes", "sha256") == descriptor.values_at("bytes", "sha256") && transfer["parts"] == [descriptor.slice("bytes", "sha256")]
232
- raise AttemptErrors::EvidenceUnavailable, "original campaign candidate download differs"
233
- end
234
- identity = descriptor.fetch("original_worker_identity")
235
- birth = identity.is_a?(Hash) && identity["started_at"]
236
- Molecules::ExecutionScopeLineage.validate_process_identity!(identity, boot_id: birth.to_s.split(":", -1)[1])
237
- root = descriptor.fetch("original_worker_scratch_root")
238
- unless root.is_a?(String) && root.valid_encoding? && root.bytesize.between?(2, 4096) &&
239
- root.start_with?("/") && !root.include?("\0") && File.expand_path(root) == root
240
- raise AttemptErrors::EvidenceUnavailable, "original campaign scratch root differs"
241
- end
242
- rescue ArgumentError, KeyError
243
- raise AttemptErrors::EvidenceUnavailable, "original campaign candidate download differs"
244
- end
245
-
246
- def prepared_transfer_codec(descriptor)
247
- peer = @kernel.capture(Process.pid)
248
- original = descriptor.fetch("original_worker_identity")
249
- unless peer.values_at("uid", "gid", "groups") == original.values_at("uid", "gid", "groups")
250
- raise AttemptErrors::UnauthorizedIdentity, "original prepared scratch principal differs"
251
- end
252
- TransferCodec.new(root: descriptor.fetch("original_worker_scratch_root"))
253
- end
254
- end
255
- end
256
- end
257
- end
@@ -1,180 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "digest"
5
- require_relative "../molecules/evidence_journal"
6
-
7
- module Ace
8
- module Assign
9
- module Authority
10
- # Project commands share the installed journal, without manufacturing a
11
- # managed task or projecting command exit as completed product delivery.
12
- class CommandExecution
13
- OPERATIONS = %w[command_admit command_status command_claim command_bind command_complete].freeze
14
- ID = /\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,63}\z/
15
- SHA = /\A[0-9a-f]{64}\z/
16
-
17
- def initialize(project:, journal:, admission:)
18
- @project, @journal, @admission = project, journal, admission
19
- end
20
-
21
- attr_reader :journal
22
-
23
- def dispatch(request:, peer:, role:, deadline:)
24
- operation = request.fetch("operation")
25
- params = request.fetch("params")
26
- raise ArgumentError, "invalid command project" unless request.fetch("project_id") == @project
27
- id = params.fetch("execution_id")
28
- raise ArgumentError, "invalid command reference" unless id.is_a?(String) && ID.match?(id)
29
- if operation == "command_status"
30
- strict!(params, %w[execution_id])
31
- raise ArgumentError, "status mutation must be null" unless request.fetch("mutation_id").nil?
32
- commit = @journal.ref_value
33
- data = projection(id, commit)
34
- @admission.command_reader!(peer: peer, definition: data.fetch("definition"), deadline: deadline)
35
- return {data: data.merge("journal_commit" => commit), replayed: false}
36
- end
37
- strict!(params, case operation
38
- when "command_admit" then %w[execution_id selection]
39
- when "command_claim" then %w[execution_id caller]
40
- when "command_bind" then %w[execution_id child]
41
- when "command_complete" then %w[execution_id result]
42
- else raise ArgumentError, "unknown command operation"
43
- end)
44
- authenticated_peer = peer
45
- launcher = nil
46
- plan = if operation == "command_admit"
47
- @admission.command_request!(selection: params.fetch("selection"), peer: peer, deadline: deadline)
48
- else
49
- launcher = @admission.command_launcher!(peer: authenticated_peer, deadline: deadline)
50
- nil
51
- end
52
- authenticate = lambda do
53
- if operation == "command_admit"
54
- unless @admission.command_request!(selection: params.fetch("selection"), peer: peer, deadline: deadline) == plan
55
- raise AttemptErrors::UnauthorizedIdentity, "command admission changed"
56
- end
57
- else
58
- unless @admission.command_launcher!(peer: authenticated_peer, deadline: deadline) == launcher
59
- raise AttemptErrors::UnauthorizedIdentity, "installed command launcher changed"
60
- end
61
- end
62
- end
63
- peer = launcher if launcher
64
- expected = {"command_admit" => 0, "command_claim" => 1, "command_bind" => 2, "command_complete" => 3}.fetch(operation)
65
- digest = Atoms::EvidenceDigest.digest({"project" => @project, "params" => params, "peer" => peer, "plan" => plan})
66
- @journal.mutate(assignment_id: namespace(id), attempt_id: namespace(id),
67
- mutation_id: request.fetch("mutation_id"), operation: operation, parameters_digest: digest,
68
- expected_generation: expected, with_replay: true, deadline: deadline,
69
- replay_authorizer: authenticate) do |_events, commit, _generation|
70
- authenticate.call
71
- prior = projection(id, commit, missing: true)
72
- data = transition(operation, id, params, plan, prior, peer, deadline)
73
- {events: [], data: data}
74
- end
75
- end
76
-
77
- # Read only the accepted launcher-owned four-stage execution at the
78
- # caller's held CAS prefix. Arbitrary allowed commands are not tests.
79
- def test_evidence_at!(execution_id:, commit:, head:, tree:, deadline:)
80
- unless execution_id.is_a?(String) && ID.match?(execution_id) && commit && @journal.ref_value == commit
81
- raise AttemptErrors::EvidenceUnavailable, "test execution prefix or selector differs"
82
- end
83
- data = projection(execution_id, commit)
84
- definition = data.fetch("definition")
85
- events = @journal.read_events(namespace(execution_id), commit: commit)
86
- accepted = events.select { |event| event["type"] == "authority_mutation" }
87
- stages = accepted.map { |event| event.fetch("payload").fetch("data") }
88
- unless accepted.size == 4 && data["phase"] == "exited" && data["result"] == {"exit_status" => 0, "signal" => nil} &&
89
- definition["purpose"] == "test" && definition["source"] == {"head" => head, "tree" => tree} &&
90
- [head, tree].all? { |value| value.is_a?(String) && value.match?(/\A[0-9a-f]{40}\z/) } &&
91
- stages.all? { |stage| stage.fetch("definition") == definition } &&
92
- stages.map { |stage| stage.fetch("phase") } == %w[admitted claimed running exited] &&
93
- stages.drop(1).all? { |stage| stage.fetch("launcher") == data.fetch("launcher") } &&
94
- stages[2].fetch("child") == data.fetch("child") && data.dig("launcher", "uid") == 0 &&
95
- data.dig("child", "witnessed_by") == data.fetch("launcher") &&
96
- data.dig("child", "kernel_identity") == data.dig("child", "root_witness")&.slice("pid", "uid", "started_at", "host") &&
97
- data.dig("child", "root_witness", "cwd") == definition.dig("executor", "cwd") &&
98
- data.dig("child", "kernel_identity", "uid") == definition.dig("executor", "uid")
99
- raise AttemptErrors::ReceiptRejected, "candidate requires an exact successful executed test command"
100
- end
101
- @admission.command_test_evidence!(definition: definition, deadline: deadline)
102
- command = JSON.generate(definition.fetch("argv"))
103
- raise AttemptErrors::ReceiptRejected, "executed test command reference is oversized" if command.bytesize > 2048
104
- {"schema" => "ace.test.execution-reference/v1", "reference" => execution_id,
105
- "candidate_head" => head, "command" => command, "result" => "passed",
106
- "recorded_at" => accepted.last.fetch("recorded_at"), "receipt_sha256" => Atoms::EvidenceDigest.digest(data)}
107
- rescue KeyError, TypeError
108
- raise AttemptErrors::EvidenceUnavailable, "canonical test execution is incomplete"
109
- end
110
-
111
- private
112
-
113
- def transition(operation, id, params, plan, prior, peer, deadline)
114
- base = {"schema" => "project-command/v1", "execution_id" => id, "project_id" => @project}
115
- case operation
116
- when "command_admit"
117
- raise AttemptErrors::Conflict, "command already admitted" if prior
118
- # Plan contains references and nonsecret context; its closed schema
119
- # is owned by the installed Lab admission, never requester input.
120
- base.merge("phase" => "admitted", "definition" => plan)
121
- when "command_claim"
122
- phase!(prior, "admitted")
123
- @admission.command_claim!(definition: prior.fetch("definition"), caller: params.fetch("caller"),
124
- launcher: peer, deadline: deadline)
125
- prior.merge("phase" => "claimed", "launcher" => peer)
126
- when "command_bind"
127
- phase!(prior, "claimed")
128
- launcher!(prior, peer)
129
- child = @admission.command_child!(definition: prior.fetch("definition"), child: params.fetch("child"),
130
- launcher: peer, deadline: deadline)
131
- prior.merge("phase" => "running", "child" => child)
132
- when "command_complete"
133
- phase!(prior, "running")
134
- launcher!(prior, peer)
135
- result = params.fetch("result")
136
- strict!(result, %w[exit_status signal])
137
- exit_status, signal = result.values_at("exit_status", "signal")
138
- unless (exit_status.is_a?(Integer) && exit_status.between?(0, 255) && signal.nil?) ||
139
- (exit_status.nil? && signal.is_a?(Integer) && signal.between?(1, 127))
140
- raise ArgumentError, "invalid command termination"
141
- end
142
- prior.merge("phase" => "exited", "result" => result)
143
- end
144
- end
145
-
146
- def projection(id, commit, missing: false)
147
- events = @journal.read_events(namespace(id), commit: commit)
148
- unless Models::EvidenceEvent.chain_valid?(events)
149
- raise AttemptErrors::EvidenceUnavailable, "command history is corrupt"
150
- end
151
- accepted = events.select { |event| event["type"] == "authority_mutation" }
152
- if accepted.empty?
153
- return nil if missing
154
- raise AttemptErrors::NotFound, "command reference is unknown"
155
- end
156
- operations = accepted.map { |event| event.dig("payload", "operation") }
157
- unless operations == OPERATIONS.reject { |name| name == "command_status" }.take(operations.size) && operations.size <= 4
158
- raise AttemptErrors::EvidenceUnavailable, "command lifecycle is corrupt"
159
- end
160
- data = accepted.last.fetch("payload").fetch("data")
161
- unless data.values_at("schema", "project_id", "execution_id") == ["project-command/v1", @project, id]
162
- raise AttemptErrors::EvidenceUnavailable, "command association differs"
163
- end
164
- data
165
- end
166
-
167
- def namespace(id) = "command-#{id}"
168
- def strict!(value, keys)
169
- raise ArgumentError, "invalid command fields" unless value.is_a?(Hash) && value.keys.sort == keys.sort
170
- end
171
- def phase!(prior, phase)
172
- raise AttemptErrors::Conflict, "command phase differs; inspect original execution" unless prior && prior["phase"] == phase
173
- end
174
- def launcher!(prior, peer)
175
- raise AttemptErrors::UnauthorizedIdentity, "command launcher changed" unless prior.fetch("launcher") == peer
176
- end
177
- end
178
- end
179
- end
180
- end