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,627 +0,0 @@
1
- # frozen_string_literal: true
2
- require "json"
3
- require "digest"
4
- require "etc"
5
- require "rbconfig"
6
- require "openssl"
7
- require_relative "private_directory"
8
- require_relative "posix_acl"
9
- require_relative "task_context_entry"
10
- require_relative "campaign_consumer_policy"
11
- require "ace/runtime/molecules/protected_worker_entry"
12
- require "ace/runtime/molecules/protected_socket"
13
- require "ace/runtime/molecules/protected_artifact_set"
14
- require "ace/runtime/molecules/protected_linux"
15
- require "ace/runtime/molecules/execution_unit_installation"
16
- require "ace/runtime/molecules/readiness_configuration"
17
-
18
- module Ace
19
- module Assign
20
- module Authority
21
- class Deployment
22
- PATH = "/etc/ace/assignment-authorities.json"
23
- TOKEN = /\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/
24
- attr_reader :data, :artifact_reference
25
-
26
- def self.load
27
- Ace::Runtime::Molecules::ProtectedArtifactSet.new.with do |artifacts|
28
- bytes, reference = artifacts.read_path!(PATH, limit: 65_536)
29
- deployment = from_verified_bytes(bytes, reference)
30
- artifacts.verify_unchanged!
31
- deployment
32
- end
33
- end
34
-
35
- # Trusted installer source call only; this is not a transport selector.
36
- # Authentication establishes fixed descriptor bytes, not installed readiness.
37
- def self.load_artifact(reference)
38
- unless reference.is_a?(Hash) && reference.keys.sort == %w[bytes path sha256] &&
39
- reference["path"].is_a?(String) && reference["path"].start_with?("/") &&
40
- !reference["path"].include?("\0") && File.expand_path(reference["path"]) == reference["path"] &&
41
- reference["sha256"].is_a?(String) && reference["sha256"].match?(/\A[0-9a-f]{64}\z/) &&
42
- reference["bytes"].is_a?(Integer) && reference["bytes"].between?(1, 65_536)
43
- raise ArgumentError, "invalid protected deployment artifact reference"
44
- end
45
- selected = reference.transform_values { |value| value.is_a?(String) ? value.dup.freeze : value }.freeze
46
- Ace::Runtime::Molecules::ProtectedArtifactSet.new.with do |artifacts|
47
- deployment = from_verified_bytes(artifacts.read!(selected), selected)
48
- artifacts.verify_unchanged!
49
- deployment
50
- end
51
- end
52
-
53
- def self.from_verified_bytes(raw, reference)
54
- bytes = raw.dup.force_encoding(Encoding::UTF_8)
55
- raise ArgumentError, "deployment artifact is not UTF-8" unless bytes.valid_encoding?
56
- raise ArgumentError, "deployment artifact exceeds bounds" unless bytes.bytesize.between?(1, 65_536)
57
- value = JSON.parse(bytes, create_additions: false, max_nesting: 32,
58
- allow_duplicate_key: false, allow_comments: false)
59
- deployment = new(value)
60
- deployment.send(:freeze_data!, deployment.data)
61
- deployment.instance_variable_set(:@artifact_reference, reference)
62
- deployment.freeze
63
- end
64
- private_class_method :from_verified_bytes
65
-
66
- def initialize(data)
67
- unless data.is_a?(Hash) && data["schema"] == "ace.assign.authorities/v2" &&
68
- data.keys.sort == %w[authorities launch_mappings projects schema]
69
- raise ArgumentError, "invalid assignment authority deployment schema"
70
- end
71
- %w[authorities launch_mappings projects].each do |section|
72
- unless data[section].is_a?(Hash) && data[section].keys.all? { |id| TOKEN.match?(id) }
73
- raise ArgumentError, "invalid #{section} mapping"
74
- end
75
- end
76
- @data = JSON.parse(JSON.generate(data))
77
- @data["authorities"].each_value do |service|
78
- strict!(service, %w[uid gid groups socket_path state_root composition])
79
- unless %w[launch services].include?(service["composition"])
80
- raise ArgumentError, "unknown installed authority composition"
81
- end
82
- principal!(service, "uid", "gid", "groups")
83
- %w[socket_path state_root].each { |key| path!(service.fetch(key)) }
84
- end
85
- @data["projects"].each_value do |project|
86
- raise ArgumentError, "invalid project mapping" unless project.is_a?(Hash)
87
- keys = %w[journal_repository evidence_git_ref evidence_checkout_root assignment_root candidate_root campaign_repository campaign_store_root campaign_policy
88
- launcher_uids reviewer_uids worker_uids service_executor_uids supervisor_uids peer_credentials]
89
- keys << "service_receivers" if project.key?("service_receivers")
90
- keys << "inbox_contexts" if project.key?("inbox_contexts")
91
- strict!(project, keys)
92
- CampaignConsumerPolicy.reference!(project.fetch("campaign_policy"))
93
- %w[journal_repository evidence_checkout_root assignment_root candidate_root campaign_repository campaign_store_root].each { |key| path!(project.fetch(key)) }
94
- unless project["evidence_git_ref"] == "refs/ace/execution"
95
- raise ArgumentError, "protected authority uses the canonical execution ref"
96
- end
97
- role_keys = %w[launcher_uids reviewer_uids worker_uids service_executor_uids supervisor_uids]
98
- role_uids = role_keys.flat_map do |key|
99
- values = project.fetch(key)
100
- unless values.is_a?(Array) && values.all? { |id| id.is_a?(Integer) && id.positive? } && values == values.sort.uniq
101
- raise ArgumentError, "invalid project principal allowlist"
102
- end
103
- values
104
- end.uniq.sort
105
- peers = project.fetch("peer_credentials")
106
- unless peers.is_a?(Hash) && peers.keys.sort == role_uids.map(&:to_s).sort
107
- raise ArgumentError, "fixed project peer credentials are incomplete"
108
- end
109
- peers.each do |uid, peer|
110
- strict!(peer, %w[gid groups scratch_root])
111
- principal!(peer.merge("uid" => Integer(uid, 10)), "uid", "gid", "groups")
112
- path!(peer.fetch("scratch_root"))
113
- end
114
- receivers = project.fetch("service_receivers", {})
115
- unless receivers.is_a?(Hash) && receivers.keys.all? { |id| id.is_a?(String) && TOKEN.match?(id) }
116
- raise ArgumentError, "invalid installed service receiver mapping"
117
- end
118
- receivers.each_value do |receiver|
119
- strict!(receiver, %w[executor_uid socket_path staging_root])
120
- uid = receiver.fetch("executor_uid")
121
- conflicting = %w[launcher_uids reviewer_uids worker_uids supervisor_uids].flat_map { |key| project.fetch(key) }
122
- unless uid.is_a?(Integer) && uid.positive? && project.fetch("service_executor_uids").include?(uid) && !conflicting.include?(uid)
123
- raise ArgumentError, "receiver requires an unambiguous mapped executor"
124
- end
125
- %w[socket_path staging_root].each { |key| path!(receiver.fetch(key)) }
126
- end
127
- end
128
- @data["launch_mappings"].each { |id, mapping| validate_mapping!(id, mapping) }
129
- validate_execution_slots!
130
- validate_inbox_contexts!
131
- endpoints = @data.fetch("authorities").values.map { |service| service.fetch("socket_path") }
132
- receivers = @data.fetch("projects").values.flat_map { |project| project.fetch("service_receivers", {}).values }
133
- protected_uids = @data.fetch("authorities").values.map { |service| service.fetch("uid") } +
134
- @data.fetch("projects").values.flat_map { |project| %w[launcher_uids reviewer_uids worker_uids supervisor_uids].flat_map { |key| project.fetch(key) } }
135
- if receivers.any? { |receiver| protected_uids.include?(receiver.fetch("executor_uid")) }
136
- raise ArgumentError, "receiver executor overlaps another installed role"
137
- end
138
- endpoints.concat(receivers.map { |receiver| receiver.fetch("socket_path") })
139
- raise ArgumentError, "authority endpoint has duplicate owners" unless endpoints.uniq.size == endpoints.size
140
- staging = receivers.map { |receiver| receiver.fetch("staging_root") }
141
- unless staging.combination(2).none? { |left, right| paths_overlap?(left, right) }
142
- raise ArgumentError, "receiver staging roots overlap"
143
- end
144
- if receivers.any? { |receiver| staging.any? { |root| paths_overlap?(receiver.fetch("socket_path"), root) } }
145
- raise ArgumentError, "receiver endpoint must be separate from private staging"
146
- end
147
- protected_roots = @data.fetch("authorities").values.map { |service| service.fetch("state_root") } +
148
- @data.fetch("projects").values.flat_map { |project| %w[journal_repository evidence_checkout_root assignment_root candidate_root campaign_repository campaign_store_root].map { |key| project.fetch(key) } }
149
- if staging.any? { |root| protected_roots.any? { |protected| paths_overlap?(root, protected) } }
150
- raise ArgumentError, "receiver staging overlaps authority state"
151
- end
152
- campaign_roots = @data.fetch("projects").values.flat_map { |project| project.values_at("campaign_repository", "campaign_store_root") }
153
- other_roots = @data.fetch("authorities").values.map { |service| service.fetch("state_root") } +
154
- @data.fetch("projects").values.flat_map { |project|
155
- project.values_at("journal_repository", "evidence_checkout_root", "assignment_root", "candidate_root") +
156
- project.fetch("peer_credentials").values.map { |peer| peer.fetch("scratch_root") } }
157
- if campaign_roots.combination(2).any? { |left, right| paths_overlap?(left, right) } ||
158
- campaign_roots.any? { |root| other_roots.any? { |other| paths_overlap?(root, other) } }
159
- raise ArgumentError, "campaign roots overlap another protected or worker root"
160
- end
161
- repositories = @data.fetch("projects").values.map { |project| project.fetch("journal_repository") }
162
- raise ArgumentError, "canonical project repository has duplicate mappings" unless repositories.uniq.size == repositories.size
163
- @data.fetch("projects").each_key do |project_id|
164
- owners = @data.fetch("launch_mappings").values.select { |map| map["project_id"] == project_id }.map { |map| map.fetch("authority_id") }.uniq
165
- raise ArgumentError, "project must have exactly one authority owner" unless owners.size == 1
166
- service = authority(owners.first)
167
- installed = project(project_id).fetch("service_receivers", {})
168
- if service.fetch("composition") == "services" && installed.empty?
169
- raise ArgumentError, "services composition requires installed receivers"
170
- end
171
- end
172
- end
173
-
174
- # Static selection only. Live native identity comes from the attempt's
175
- # canonical scope stage, never from this deployment record.
176
- def inbox_context(mapping_id, context_id)
177
- map = mapping(mapping_id)
178
- contexts = project(map.fetch("project_id")).fetch("inbox_contexts", {})
179
- context = contexts.fetch(context_id) { raise AttemptErrors::EvidenceUnavailable, "installed inbox context is unavailable" }
180
- native = mapping(context.fetch("native_mapping_id"))
181
- unless native.fetch("project_id") == map.fetch("project_id") && native.fetch("authority_id") == map.fetch("authority_id")
182
- raise AttemptErrors::UnauthorizedIdentity, "inbox context belongs to another authority"
183
- end
184
- JSON.parse(JSON.generate(context))
185
- end
186
-
187
- def verify_inbox_context!(mapping_id, context_id)
188
- context = inbox_context(mapping_id, context_id)
189
- service = authority(mapping(mapping_id).fetch("authority_id"))
190
- unless Process.uid == service.fetch("uid")
191
- raise Ace::Runtime::RuntimeUnavailableError, "inbox resolver must run as its installed authority"
192
- end
193
- credentials = context.fetch("owner_credentials")
194
- path = context.fetch("control_socket_path")
195
- wire = Ace::Runtime::Molecules::ProtectedSocket
196
- wire.root_path!(File.dirname(path), directory: true, owner: credentials.fetch("uid"))
197
- identity = wire.socket_identity(path)
198
- unless identity.last == credentials.fetch("uid")
199
- raise Ace::Runtime::RuntimeUnavailableError, "installed context owner endpoint differs"
200
- end
201
- context
202
- rescue AttemptErrors::ReceiptRejected, OpenSSL::PKey::PKeyError, SystemCallError
203
- raise Ace::Runtime::RuntimeUnavailableError, "installed inbox context evidence is unavailable"
204
- end
205
-
206
- def verify_inbox_acl!(path, private_leaf: false)
207
- current = path
208
- loop do
209
- rows = receiver_acl.entries(current)
210
- if rows
211
- mask = rows.find { |tag, _perm, _id| tag == 16 }&.at(1) || 7
212
- allowed = current == path && !private_leaf ? [0] : [0, Process.uid]
213
- unsafe = rows.any? do |tag, perm, uid|
214
- effective = [2, 4, 8].include?(tag) ? perm & mask : perm
215
- case tag
216
- when 2 then !allowed.include?(uid) && (effective & 2).positive?
217
- when 4, 8, 32 then (effective & 2).positive?
218
- else false
219
- end
220
- end
221
- if private_leaf && current == path
222
- unsafe ||= rows.any? do |tag, perm, uid|
223
- effective = [2, 4, 8].include?(tag) ? perm & mask : perm
224
- [4, 8, 32].include?(tag) && effective.positive? || tag == 2 && uid != Process.uid && effective.positive?
225
- end
226
- end
227
- raise Ace::Runtime::RuntimeUnavailableError, "installed inbox access ACL is unsafe" if unsafe
228
- end
229
- break if current == "/"
230
- current = File.dirname(current)
231
- end
232
- end
233
- private :verify_inbox_acl!
234
-
235
- def validate_inbox_contexts!
236
- roots = []
237
- service_scopes = data.fetch("launch_mappings").values.map { |map| map.fetch("execution_scope") }
238
- service_owner_uids = []
239
- data.fetch("projects").each do |project_id, fixed_project|
240
- contexts = fixed_project.fetch("inbox_contexts", {})
241
- unless contexts.is_a?(Hash) && contexts.keys.all? { |id| id.is_a?(String) && TOKEN.match?(id) }
242
- raise ArgumentError, "invalid installed inbox context map"
243
- end
244
- contexts.each_value do |context|
245
- fields = %w[control_socket_path deliveries_dir owner_credentials native_mapping_id pi_queue_client pi_queue_client_sha256 supervisor_uids]
246
- fields << "service" if context.key?("service")
247
- strict!(context, fields)
248
- credentials = context.fetch("owner_credentials")
249
- strict!(credentials, %w[gid groups uid])
250
- principal!(credentials, "uid", "gid", "groups")
251
- raise ArgumentError, "context owner group count exceeds bounds" if credentials.fetch("groups").size > 64
252
- %w[control_socket_path deliveries_dir pi_queue_client].each { |key| path!(context.fetch(key)) }
253
- unless context["pi_queue_client_sha256"].is_a?(String) && context["pi_queue_client_sha256"].match?(/\A[0-9a-f]{64}\z/)
254
- raise ArgumentError, "invalid installed Pi identity client digest"
255
- end
256
- supervisors = context.fetch("supervisor_uids")
257
- unless supervisors.is_a?(Array) && supervisors.all? { |uid| uid.is_a?(Integer) && uid.positive? } &&
258
- supervisors == supervisors.sort.uniq && (supervisors - fixed_project.fetch("supervisor_uids")).empty?
259
- raise ArgumentError, "inbox supervisors exceed installed project authority"
260
- end
261
- id = context.fetch("native_mapping_id")
262
- unless id.is_a?(String) && TOKEN.match?(id) && data.fetch("launch_mappings").key?(id)
263
- raise ArgumentError, "inbox native mapping is unavailable"
264
- end
265
- native = mapping(id)
266
- unless native.fetch("project_id") == project_id && authority(native.fetch("authority_id")).fetch("composition") == "services"
267
- raise ArgumentError, "inbox context requires its services project mapping"
268
- end
269
- if context.key?("service")
270
- Ace::Runtime::Molecules::ExecutionUnitInstallation.validate_inbox_context_service!(context.fetch("service"))
271
- scope = context.fetch("service").fetch("execution_scope")
272
- if service_scopes.any? { |other|
273
- %w[slot_id slice_unit service_unit].any? { |field| scope.fetch(field) == other.fetch(field) } ||
274
- scope.values_at("root_directory", "runtime_directory").any? { |path|
275
- other.values_at("root_directory", "runtime_directory").any? { |existing| paths_overlap?(path, existing) }
276
- }
277
- }
278
- raise ArgumentError, "context service shares a protected execution slot or backing root"
279
- end
280
- prohibited_uids = data.fetch("authorities").values.map { |service| service.fetch("uid") } +
281
- data.fetch("launch_mappings").values.flat_map { |map| map.values_at("launcher_uid", "worker_uid") } +
282
- data.fetch("projects").values.flat_map { |project|
283
- %w[launcher_uids reviewer_uids worker_uids service_executor_uids supervisor_uids].flat_map { |field| project.fetch(field) } +
284
- project.fetch("service_receivers", {}).values.map { |receiver| receiver.fetch("executor_uid") }
285
- }
286
- prohibited_uids.concat(data.fetch("projects").values.flat_map { |project|
287
- project.fetch("inbox_contexts", {}).values.reject { |other| other.equal?(context) }.map { |other| other.fetch("owner_credentials").fetch("uid") }
288
- })
289
- uid = credentials.fetch("uid")
290
- if prohibited_uids.include?(uid) || service_owner_uids.include?(uid)
291
- raise ArgumentError, "context service owner shares another protected principal"
292
- end
293
- service_owner_uids << uid
294
- service_scopes << scope
295
- end
296
- roots << context.fetch("deliveries_dir")
297
- end
298
- end
299
- forbidden = data.fetch("projects").values.flat_map do |fixed_project|
300
- fixed_project.fetch("peer_credentials").values.map { |peer| peer.fetch("scratch_root") } +
301
- fixed_project.fetch("service_receivers", {}).values.map { |receiver| receiver.fetch("staging_root") } +
302
- %w[journal_repository evidence_checkout_root assignment_root candidate_root campaign_repository campaign_store_root].map { |key| fixed_project.fetch(key) }
303
- end
304
- forbidden.concat(data.fetch("authorities").values.map { |service| service.fetch("state_root") })
305
- if roots.combination(2).any? { |left, right| paths_overlap?(left, right) } ||
306
- roots.any? { |root| forbidden.any? { |other| paths_overlap?(root, other) } }
307
- raise ArgumentError, "installed inbox private roots overlap protected state"
308
- end
309
- rescue Ace::Runtime::RuntimeUnavailableError
310
- raise ArgumentError, "invalid context service installation selection"
311
- end
312
- private :validate_inbox_contexts!
313
-
314
- # Validate fixed receiver placement before the owner opens a listener or
315
- # creates journal state. Operations/grants remain the Lab policy's job.
316
- def verify_receiver_paths!(authority_id)
317
- projects = data.fetch("launch_mappings").values.select { |map| map["authority_id"] == authority_id }.map { |map| map.fetch("project_id") }.uniq
318
- projects.each do |id|
319
- project(id).fetch("service_receivers", {}).each_value do |receiver|
320
- uid = receiver.fetch("executor_uid")
321
- root = receiver.fetch("staging_root")
322
- credentials = project(id).fetch("peer_credentials").fetch(uid.to_s)
323
- groups = (credentials.fetch("groups") + [credentials.fetch("gid")]).uniq
324
- receiver_directory!(root, uid: uid, groups: groups)
325
- unless File.executable?(File.dirname(root))
326
- raise Ace::Runtime::RuntimeUnavailableError, "authority cannot inspect receiver staging"
327
- end
328
- stat = File.lstat(root)
329
- unless stat.uid == uid && (stat.mode & 0o7777) == 0o700
330
- raise Ace::Runtime::RuntimeUnavailableError, "receiver staging must be executor-private"
331
- end
332
- path = receiver.fetch("socket_path")
333
- receiver_directory!(File.dirname(path), uid: uid, groups: groups)
334
- unless File.executable?(File.dirname(path))
335
- raise Ace::Runtime::RuntimeUnavailableError, "authority cannot inspect receiver endpoint"
336
- end
337
- if File.exist?(path) || File.symlink?(path)
338
- endpoint = File.lstat(path)
339
- unless endpoint.socket? && !endpoint.symlink? && endpoint.uid == uid &&
340
- endpoint.gid == project(id).fetch("peer_credentials").fetch(uid.to_s).fetch("gid") && (endpoint.mode & 0o007).zero?
341
- raise Ace::Runtime::RuntimeUnavailableError, "installed receiver endpoint is unsafe"
342
- end
343
- end
344
- end
345
- end
346
- true
347
- rescue SystemCallError
348
- raise Ace::Runtime::RuntimeUnavailableError, "installed receiver paths are unavailable"
349
- end
350
-
351
- def receiver_directory!(path, uid:, groups:)
352
- current = path
353
- loop do
354
- stat = File.lstat(current)
355
- unless stat.directory? && !stat.symlink? && [0, uid].include?(stat.uid) && (stat.mode & 0o022).zero?
356
- raise Ace::Runtime::RuntimeUnavailableError, "installed receiver ancestry is unsafe"
357
- end
358
- unless receiver_acl.searchable?(current, stat: stat, uid: uid, groups: groups)
359
- raise Ace::Runtime::RuntimeUnavailableError, "executor cannot traverse receiver ancestry"
360
- end
361
- break if current == "/"
362
- current = File.dirname(current)
363
- end
364
- end
365
- private :receiver_directory!
366
-
367
- def receiver_acl
368
- @receiver_acl ||= PosixAcl.new
369
- end
370
- private :receiver_acl
371
-
372
- def paths_overlap?(left, right)
373
- left == right || left.start_with?(right.chomp("/") + "/") || right.start_with?(left.chomp("/") + "/")
374
- end
375
- private :paths_overlap?
376
-
377
- def verify_composition!(authority_id, composition:)
378
- unless %w[launch services].include?(composition) && authority(authority_id).fetch("composition") == composition
379
- raise ArgumentError, "installed authority composition differs from source entrypoint"
380
- end
381
- true
382
- end
383
-
384
- def authority(id)
385
- data.fetch("authorities").fetch(id)
386
- end
387
-
388
- def project(id)
389
- data.fetch("projects").fetch(id)
390
- end
391
-
392
- def mapping(id)
393
- data.fetch("launch_mappings").fetch(id)
394
- end
395
-
396
- # Canonical mapping identity shared by scope evidence and the trusted
397
- # provisioning owner. Resolves only this descriptor's exact mapping ID.
398
- def mapping_digest(id)
399
- Digest::SHA256.hexdigest(JSON.generate(canonical_mapping_value(mapping(id)))).freeze
400
- end
401
-
402
- def canonical_mapping_value(value)
403
- case value
404
- when Hash then value.sort.to_h.transform_values { |item| canonical_mapping_value(item) }
405
- when Array then value.map { |item| canonical_mapping_value(item) }
406
- else value
407
- end
408
- end
409
- private :canonical_mapping_value
410
-
411
- def validate_mapping!(id, mapping)
412
- required = %w[project_id authority_id launcher_uid launcher_gid launcher_groups worker_uid worker_gid
413
- worker_groups worker_actor worker_cwd worker_entry worker_env bootstrap bootstrap_sha256 native execution_scope task_context_entry]
414
- workspace = %w[workspace_repository_id workspace_cleanup_config]
415
- unless mapping.is_a?(Hash) && [required.sort, (required + workspace).sort].include?(mapping.keys.sort)
416
- raise ArgumentError, "launch mapping fields differ: #{id}"
417
- end
418
- if mapping.key?("workspace_repository_id")
419
- repository = mapping.fetch("workspace_repository_id")
420
- unless repository.is_a?(String) && TOKEN.match?(repository)
421
- raise ArgumentError, "workspace repository identity is invalid"
422
- end
423
- reference = mapping.fetch("workspace_cleanup_config")
424
- strict!(reference, %w[path sha256 bytes])
425
- path!(reference.fetch("path"))
426
- unless reference.fetch("path").bytesize <= 4096 && reference.fetch("path").encoding == Encoding::UTF_8 && reference.fetch("path").valid_encoding? &&
427
- digest?(reference.fetch("sha256")) && reference.fetch("bytes").is_a?(Integer) && reference.fetch("bytes").between?(1, 65_536)
428
- raise ArgumentError, "workspace cleanup configuration reference is invalid"
429
- end
430
- end
431
- TaskContextEntry.validate!(mapping.fetch("task_context_entry"))
432
- Ace::Runtime::Molecules::ProtectedWorkerEntry.validate!(mapping.fetch("worker_entry"))
433
- service = authority(mapping.fetch("authority_id"))
434
- fixed_project = project(mapping.fetch("project_id"))
435
- %w[launcher worker].each do |role|
436
- uid = mapping.fetch("#{role}_uid")
437
- expected = fixed_project.fetch("peer_credentials").fetch(uid.to_s)
438
- unless fixed_project.fetch("#{role}_uids").include?(uid) && expected["gid"] == mapping["#{role}_gid"] && expected["groups"] == mapping["#{role}_groups"]
439
- raise ArgumentError, "launch credentials differ from project allowlist"
440
- end
441
- end
442
- uids = [service.fetch("uid"), mapping.fetch("launcher_uid"), mapping.fetch("worker_uid")]
443
- unless uids.uniq.size == 3 && uids.all? { |uid| uid.is_a?(Integer) && uid.positive? }
444
- raise ArgumentError, "protected launch requires distinct non-root principals"
445
- end
446
- %w[launcher worker].each do |role|
447
- principal!(mapping, "#{role}_uid", "#{role}_gid", "#{role}_groups")
448
- end
449
- %w[worker_cwd bootstrap].each { |key| path!(mapping.fetch(key)) }
450
- native = mapping.fetch("native")
451
- strict!(native, %w[socket_path executable executable_sha256 version protocol workspace_id])
452
- %w[socket_path executable].each { |key| path!(native.fetch(key)) }
453
- unless native["workspace_id"].is_a?(String) && native["workspace_id"].match?(/\Aw[1-9][0-9]{0,8}\z/) &&
454
- native["version"] == "0.9.3" && native["protocol"] == 22 && digest?(native["executable_sha256"])
455
- raise ArgumentError, "invalid fixed native artifact or protocol"
456
- end
457
- scope = mapping.fetch("execution_scope")
458
- strict!(scope, %w[backend slot_id slice_unit service_unit unit_manifest_sha256 boundary_manifest_sha256
459
- root_directory runtime_directory network_namespace_path])
460
- unless scope["backend"] == "linux_systemd_cgroup_v2" && scope["slot_id"].is_a?(String) && TOKEN.match?(scope["slot_id"]) &&
461
- %w[unit_manifest_sha256 boundary_manifest_sha256].all? { |key| digest?(scope[key]) }
462
- raise ArgumentError, "invalid execution scope backend or manifest identity"
463
- end
464
- {"slice_unit" => ".slice", "service_unit" => ".service"}.each do |key, suffix|
465
- name = scope.fetch(key)
466
- unless name.is_a?(String) && TOKEN.match?(name) && name.end_with?(suffix) &&
467
- name != "-.slice" && !name.downcase.include?("overseer")
468
- raise ArgumentError, "invalid fixed protected unit"
469
- end
470
- end
471
- %w[root_directory runtime_directory network_namespace_path].each { |key| path!(scope.fetch(key)) }
472
- unless scope.fetch("runtime_directory").start_with?("/run/") &&
473
- !paths_overlap?(scope.fetch("root_directory"), scope.fetch("runtime_directory"))
474
- raise ArgumentError, "execution scope backing roots overlap or runtime root is invalid"
475
- end
476
- env = mapping.fetch("worker_env")
477
- unless env.is_a?(Hash) && env.all? { |k, v| k.match?(/\A[A-Z_][A-Z0-9_]*\z/) && v.is_a?(String) && !v.include?("\0") } &&
478
- env.keys.none? { |key| key.match?(/\A(?:LD_|DYLD_|RUBY|BUNDLE|PYTHON)/) }
479
- raise ArgumentError, "invalid fixed worker environment"
480
- end
481
- unless mapping["worker_actor"].is_a?(String) && !mapping["worker_actor"].empty? &&
482
- mapping["bootstrap_sha256"].match?(/\A[0-9a-f]{64}\z/)
483
- raise ArgumentError, "invalid installed bootstrap or worker actor"
484
- end
485
- rescue KeyError, NoMethodError
486
- raise ArgumentError, "launch mapping is incomplete"
487
- end
488
-
489
- def digest?(value)
490
- value.is_a?(String) && value.match?(/\A[0-9a-f]{64}\z/)
491
- end
492
-
493
- def validate_execution_slots!
494
- mappings = @data.fetch("launch_mappings").values
495
- %w[slot_id slice_unit service_unit].each do |key|
496
- values = mappings.map { |map| map.fetch("execution_scope").fetch(key) }
497
- raise ArgumentError, "execution slot has duplicate ownership" unless values.uniq.size == values.size
498
- end
499
- workers = mappings.map { |map| map.fetch("worker_uid") }
500
- raise ArgumentError, "protected execution slots share a worker principal" unless workers.uniq.size == workers.size
501
- roots = mappings.map { |map| map.fetch("execution_scope").values_at("root_directory", "runtime_directory") }
502
- roots.combination(2).each do |left, right|
503
- if left.product(right).any? { |a, b| paths_overlap?(a, b) }
504
- raise ArgumentError, "protected execution slot roots overlap"
505
- end
506
- end
507
- end
508
-
509
- # Original ownership cannot be recycled or hidden by a staged descriptor.
510
- def maintenance_inventory(candidate)
511
- unless candidate.is_a?(Deployment) && artifact_reference && candidate.artifact_reference && frozen? && candidate.frozen?
512
- raise AttemptErrors::EvidenceUnavailable, "maintenance requires authenticated immutable original and candidate descriptors"
513
- end
514
- originals = data.fetch("launch_mappings")
515
- proposed = candidate.data.fetch("launch_mappings")
516
- originals.each do |id, map|
517
- next unless proposed.key?(id)
518
- unless maintenance_association(map) == candidate.maintenance_association(proposed.fetch(id))
519
- raise ArgumentError, "existing maintenance mapping was reassociated: #{id}"
520
- end
521
- end
522
- entries = originals.map { |id, map| [id, self, map] } +
523
- proposed.reject { |id, _| originals.key?(id) }.map { |id, map| [id, candidate, map] }
524
- all = originals.map { |id, map| [id, self, map] } + proposed.map { |id, map| [id, candidate, map] }
525
- all.combination(2).each do |left, right|
526
- l_id, l_owner, l_map = left
527
- r_id, r_owner, r_map = right
528
- l_scope, r_scope = l_map.fetch("execution_scope"), r_map.fetch("execution_scope")
529
- shared = %w[slot_id slice_unit service_unit network_namespace_path].any? { |key| l_scope.fetch(key) == r_scope.fetch(key) }
530
- shared ||= l_scope.values_at("root_directory", "runtime_directory").product(
531
- r_scope.values_at("root_directory", "runtime_directory")).any? { |a, b| paths_overlap?(a, b) }
532
- if shared && (l_id != r_id || l_owner.maintenance_association(l_map) != r_owner.maintenance_association(r_map))
533
- raise ArgumentError, "physical execution slot has conflicting maintenance ownership"
534
- end
535
- end
536
- entries.sort_by(&:first).map { |entry| entry.freeze }.freeze
537
- end
538
-
539
- def maintenance_association(map)
540
- fixed = project(map.fetch("project_id"))
541
- [map.fetch("project_id"), fixed.values_at("journal_repository", "evidence_git_ref", "evidence_checkout_root"),
542
- authority(map.fetch("authority_id")).fetch("state_root"),
543
- map.fetch("execution_scope").values_at("slot_id", "slice_unit", "service_unit", "network_namespace_path")]
544
- end
545
-
546
- def freeze_data!(value)
547
- case value
548
- when Hash then value.each { |key, item| key.freeze; freeze_data!(item) }
549
- when Array then value.each { |item| freeze_data!(item) }
550
- end
551
- value.freeze
552
- end
553
-
554
- def strict!(value, keys)
555
- raise ArgumentError, "deployment fields differ" unless value.is_a?(Hash) && value.keys.sort == keys.sort
556
- end
557
-
558
- def path!(value)
559
- unless value.is_a?(String) && value.start_with?("/") && !value.include?("\0") && File.expand_path(value) == value
560
- raise ArgumentError, "deployment path must be fixed and absolute"
561
- end
562
- end
563
-
564
- def principal!(value, uid_key, gid_key, groups_key)
565
- uid, gid, groups = value.values_at(uid_key, gid_key, groups_key)
566
- unless [uid, gid].all? { |id| id.is_a?(Integer) && id.positive? } && groups.is_a?(Array) &&
567
- groups.all? { |id| id.is_a?(Integer) && id.positive? } && groups == groups.sort.uniq
568
- raise ArgumentError, "invalid principal credentials"
569
- end
570
- end
571
-
572
- def elf_architecture?(path)
573
- header = File.binread(path, 20)
574
- expected = {"x86_64" => 62, "aarch64" => 183, "arm64" => 183}[RbConfig::CONFIG.fetch("host_cpu")]
575
- expected && header.bytesize == 20 && header.byteslice(0, 4) == "\x7fELF".b &&
576
- header.getbyte(4) == 2 && header.getbyte(5) == 1 && header.getbyte(6) == 1 &&
577
- header.byteslice(18, 2).unpack1("v") == expected
578
- end
579
-
580
- def verify!(id, kernel: Ace::Runtime::Molecules::ProtectedLinux.new, authority_state: false, manager: nil)
581
- kernel.supported!
582
- map = mapping(id)
583
- service = authority(map.fetch("authority_id"))
584
- [service.fetch("uid"), map.fetch("launcher_uid"), map.fetch("worker_uid")].each { |uid| Etc.getpwuid(uid) }
585
- wire = Ace::Runtime::Molecules::ProtectedSocket
586
- scope = map.fetch("execution_scope")
587
- manager ||= Ace::Runtime::Molecules::SystemdScopeManager.new(
588
- slice_unit: scope.fetch("slice_unit"), service_unit: scope.fetch("service_unit"))
589
- manifest = Ace::Runtime::Molecules::ExecutionUnitInstallation.new(scope: scope, native: map.fetch("native"),
590
- bootstrap: map.fetch("bootstrap"), worker_entry: map.fetch("worker_entry"),
591
- worker_uid: map.fetch("worker_uid"), worker_gid: map.fetch("worker_gid")).verify!(manager: manager)
592
- artifacts = manifest.fetch("artifacts")
593
- configuration = artifacts.find { |artifact| artifact.fetch("role") == "readiness_configuration" }
594
- Ace::Runtime::Molecules::ProtectedArtifactSet.new.with do |reader|
595
- bytes, = reader.read_path!(configuration.fetch("host_path"), limit: 65_536)
596
- unless Digest::SHA256.hexdigest(bytes) == configuration.fetch("sha256")
597
- raise Ace::Runtime::RuntimeUnavailableError, "readiness configuration content differs"
598
- end
599
- Ace::Runtime::Molecules::ReadinessConfiguration.decode(bytes, slot: scope.fetch("slot_id"))
600
- .verify_selection!(mapping_id: id, mapping: map, authority: service, installation: manifest)
601
- reader.verify_unchanged!
602
- end
603
- bootstrap = artifacts.find { |artifact| artifact.fetch("role") == "bootstrap" }
604
- executables = artifacts.select do |artifact|
605
- artifact.fetch("role") == "bootstrap" || (artifact.fetch("role") == "runtime_dependency" &&
606
- artifact.fetch("view_path") == map.fetch("worker_entry").fetch("interpreter").fetch("path"))
607
- end
608
- unless bootstrap.fetch("sha256") == map.fetch("bootstrap_sha256") && elf_architecture?(bootstrap.fetch("host_path")) &&
609
- executables.all? { |artifact| File.executable?(artifact.fetch("host_path")) }
610
- raise Ace::Runtime::RuntimeUnavailableError, "installed bootstrap or executable is unsafe"
611
- end
612
- wire.root_path!(File.dirname(service.fetch("socket_path")), directory: true, owner: service.fetch("uid"))
613
- if authority_state
614
- fixed = project(map.fetch("project_id"))
615
- ([service.fetch("state_root")] + %w[journal_repository evidence_checkout_root assignment_root candidate_root campaign_repository campaign_store_root].map { |key| fixed.fetch(key) }).each do |path|
616
- wire.root_path!(path, directory: true, owner: service.fetch("uid"))
617
- unless (File.stat(path).mode & 0o077).zero?
618
- raise Ace::Runtime::RuntimeUnavailableError, "authority state root must remain private"
619
- end
620
- end
621
- end
622
- map
623
- end
624
- end
625
- end
626
- end
627
- end