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