ace-assign 0.63.1 → 0.66.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 (147) hide show
  1. checksums.yaml +4 -4
  2. data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +11 -0
  3. data/.ace-defaults/assign/catalog/steps/create-pr.step.yml +2 -2
  4. data/.ace-defaults/assign/catalog/steps/mark-pr-ready.step.yml +1 -1
  5. data/.ace-defaults/assign/catalog/steps/release-minor.step.yml +4 -2
  6. data/.ace-defaults/assign/catalog/steps/release.step.yml +4 -2
  7. data/.ace-defaults/assign/catalog/steps/update-pr-desc.step.yml +1 -1
  8. data/.ace-defaults/assign/presets/fix-bug.yml +15 -3
  9. data/.ace-defaults/assign/presets/work-on-task-ace-development.yml +16 -4
  10. data/.ace-defaults/assign/presets/work-on-task-auto-merge.yml +15 -3
  11. data/.ace-defaults/assign/presets/work-on-task.yml +16 -4
  12. data/CHANGELOG.md +62 -0
  13. data/docs/demo/canonical-skill-source.tape.yml +4 -4
  14. data/docs/protected-authority.md +282 -0
  15. data/docs/usage.md +205 -3
  16. data/exe/ace-scope-ready +55 -0
  17. data/handbook/workflow-instructions/assign/drive.wf.md +28 -3
  18. data/handbook/workflow-instructions/assign/prepare.wf.md +2 -2
  19. data/lib/ace/assign/atoms/attempt_state_machine.rb +8 -0
  20. data/lib/ace/assign/atoms/delivery_parameters.rb +19 -0
  21. data/lib/ace/assign/authority/assignment_inventory.rb +290 -0
  22. data/lib/ace/assign/authority/campaign_consumer_policy.rb +100 -0
  23. data/lib/ace/assign/authority/campaign_execution.rb +119 -0
  24. data/lib/ace/assign/authority/campaign_round_transfer.rb +82 -0
  25. data/lib/ace/assign/authority/candidate_transfer.rb +327 -0
  26. data/lib/ace/assign/authority/cleanup_result_protection.rb +53 -0
  27. data/lib/ace/assign/authority/client.rb +257 -0
  28. data/lib/ace/assign/authority/deployment.rb +627 -0
  29. data/lib/ace/assign/authority/deployment_history.rb +146 -0
  30. data/lib/ace/assign/authority/endcap.rb +455 -0
  31. data/lib/ace/assign/authority/endcap_attempts.rb +274 -0
  32. data/lib/ace/assign/authority/endcap_campaign_results.rb +232 -0
  33. data/lib/ace/assign/authority/endcap_campaign_rounds.rb +266 -0
  34. data/lib/ace/assign/authority/endcap_contexts.rb +23 -0
  35. data/lib/ace/assign/authority/endcap_inboxes.rb +37 -0
  36. data/lib/ace/assign/authority/endcap_prepared_work.rb +80 -0
  37. data/lib/ace/assign/authority/endcap_publication.rb +207 -0
  38. data/lib/ace/assign/authority/endcap_results.rb +398 -0
  39. data/lib/ace/assign/authority/endcap_review_requests.rb +183 -0
  40. data/lib/ace/assign/authority/endcap_reviews.rb +130 -0
  41. data/lib/ace/assign/authority/endcap_service_settlement.rb +152 -0
  42. data/lib/ace/assign/authority/endcap_services.rb +598 -0
  43. data/lib/ace/assign/authority/evidence_transfer.rb +67 -0
  44. data/lib/ace/assign/authority/execution_scope_observation.rb +903 -0
  45. data/lib/ace/assign/authority/inbox_context_original.rb +113 -0
  46. data/lib/ace/assign/authority/launch_attempt_consumers.rb +51 -0
  47. data/lib/ace/assign/authority/launch_campaign_registration.rb +214 -0
  48. data/lib/ace/assign/authority/launch_control_channel.rb +313 -0
  49. data/lib/ace/assign/authority/launch_control_exclusions.rb +141 -0
  50. data/lib/ace/assign/authority/launch_driver.rb +393 -0
  51. data/lib/ace/assign/authority/launch_input_inhibition.rb +132 -0
  52. data/lib/ace/assign/authority/launch_lifecycle.rb +997 -0
  53. data/lib/ace/assign/authority/launch_prepared_work.rb +122 -0
  54. data/lib/ace/assign/authority/launch_review.rb +91 -0
  55. data/lib/ace/assign/authority/launch_scope_abort.rb +81 -0
  56. data/lib/ace/assign/authority/launch_scope_admission.rb +215 -0
  57. data/lib/ace/assign/authority/launch_scope_close.rb +146 -0
  58. data/lib/ace/assign/authority/launch_scope_parent.rb +44 -0
  59. data/lib/ace/assign/authority/launch_scope_release.rb +859 -0
  60. data/lib/ace/assign/authority/launch_steering.rb +353 -0
  61. data/lib/ace/assign/authority/launch_stop.rb +254 -0
  62. data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +229 -0
  63. data/lib/ace/assign/authority/original_inbox_selection.rb +41 -0
  64. data/lib/ace/assign/authority/posix_acl.rb +59 -0
  65. data/lib/ace/assign/authority/prepared_input.rb +130 -0
  66. data/lib/ace/assign/authority/prepared_queue.rb +185 -0
  67. data/lib/ace/assign/authority/prepared_task_context.rb +116 -0
  68. data/lib/ace/assign/authority/prepared_work.rb +304 -0
  69. data/lib/ace/assign/authority/prepared_worker.rb +69 -0
  70. data/lib/ace/assign/authority/private_directory.rb +33 -0
  71. data/lib/ace/assign/authority/protected_assignment_context.rb +226 -0
  72. data/lib/ace/assign/authority/readiness_hook.rb +120 -0
  73. data/lib/ace/assign/authority/receipt_transfer.rb +48 -0
  74. data/lib/ace/assign/authority/router.rb +76 -0
  75. data/lib/ace/assign/authority/server.rb +300 -0
  76. data/lib/ace/assign/authority/service_cleanup_dispatch.rb +88 -0
  77. data/lib/ace/assign/authority/service_cleanup_evidence.rb +180 -0
  78. data/lib/ace/assign/authority/service_delivery_evidence.rb +101 -0
  79. data/lib/ace/assign/authority/service_evidence.rb +286 -0
  80. data/lib/ace/assign/authority/service_publication_evidence.rb +110 -0
  81. data/lib/ace/assign/authority/task_context_entry.rb +32 -0
  82. data/lib/ace/assign/authority/transfer_codec.rb +187 -0
  83. data/lib/ace/assign/cli/commands/assignment_target.rb +15 -2
  84. data/lib/ace/assign/cli/commands/attempt/base.rb +49 -0
  85. data/lib/ace/assign/cli/commands/attempt/finish.rb +20 -4
  86. data/lib/ace/assign/cli/commands/attempt/reconcile.rb +11 -0
  87. data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +25 -0
  88. data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +28 -0
  89. data/lib/ace/assign/cli/commands/authority/launch.rb +97 -0
  90. data/lib/ace/assign/cli/commands/authority/serve.rb +31 -0
  91. data/lib/ace/assign/cli/commands/authority/status.rb +26 -0
  92. data/lib/ace/assign/cli/commands/authority/task_context.rb +31 -0
  93. data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +25 -0
  94. data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +30 -0
  95. data/lib/ace/assign/cli/commands/authority/terminate.rb +29 -0
  96. data/lib/ace/assign/cli/commands/authority/worker.rb +23 -0
  97. data/lib/ace/assign/cli/commands/campaign_export_result.rb +55 -0
  98. data/lib/ace/assign/cli/commands/campaign_record_round.rb +57 -0
  99. data/lib/ace/assign/cli/commands/create.rb +11 -1
  100. data/lib/ace/assign/cli/commands/delivery.rb +107 -0
  101. data/lib/ace/assign/cli/commands/fail.rb +6 -0
  102. data/lib/ace/assign/cli/commands/finish.rb +23 -2
  103. data/lib/ace/assign/cli/commands/fork_session.rb +6 -2
  104. data/lib/ace/assign/cli/commands/inbox_bind.rb +34 -0
  105. data/lib/ace/assign/cli/commands/protected_submission.rb +61 -0
  106. data/lib/ace/assign/cli/commands/resume.rb +38 -0
  107. data/lib/ace/assign/cli/commands/select.rb +2 -0
  108. data/lib/ace/assign/cli/commands/start.rb +2 -0
  109. data/lib/ace/assign/cli/commands/status.rb +4 -2
  110. data/lib/ace/assign/cli/commands/step.rb +3 -1
  111. data/lib/ace/assign/cli/commands/submit_candidate.rb +34 -0
  112. data/lib/ace/assign/cli/commands/submit_result.rb +61 -0
  113. data/lib/ace/assign/cli.rb +56 -4
  114. data/lib/ace/assign/errors.rb +139 -0
  115. data/lib/ace/assign/models/assignment.rb +26 -4
  116. data/lib/ace/assign/models/evidence_event.rb +4 -2
  117. data/lib/ace/assign/molecules/assignment_manager.rb +12 -2
  118. data/lib/ace/assign/molecules/attempt_reconciler.rb +51 -12
  119. data/lib/ace/assign/molecules/canonical_attempt_state.rb +151 -0
  120. data/lib/ace/assign/molecules/canonical_evidence.rb +153 -0
  121. data/lib/ace/assign/molecules/canonical_read_snapshot.rb +374 -0
  122. data/lib/ace/assign/molecules/evidence_journal.rb +922 -91
  123. data/lib/ace/assign/molecules/execution_identity_resolver.rb +49 -9
  124. data/lib/ace/assign/molecules/execution_scope_lineage.rb +345 -0
  125. data/lib/ace/assign/molecules/fork_session_launcher.rb +16 -6
  126. data/lib/ace/assign/molecules/journal_input_inhibition.rb +78 -0
  127. data/lib/ace/assign/molecules/journal_mutation.rb +271 -0
  128. data/lib/ace/assign/molecules/journal_prompt_mutation.rb +241 -0
  129. data/lib/ace/assign/molecules/lifecycle_exclusion.rb +182 -26
  130. data/lib/ace/assign/molecules/original_launch_binding.rb +37 -0
  131. data/lib/ace/assign/molecules/proposal_journal.rb +151 -0
  132. data/lib/ace/assign/molecules/protected_control_exclusion.rb +303 -0
  133. data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +330 -0
  134. data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +164 -0
  135. data/lib/ace/assign/molecules/queue_scanner.rb +26 -11
  136. data/lib/ace/assign/molecules/receipt_verifier.rb +68 -32
  137. data/lib/ace/assign/molecules/step_writer.rb +27 -5
  138. data/lib/ace/assign/molecules/terminal_scope_receipt.rb +88 -0
  139. data/lib/ace/assign/organisms/assignment_executor.rb +10 -4
  140. data/lib/ace/assign/organisms/attempt_coordinator.rb +302 -13
  141. data/lib/ace/assign/organisms/delivery_coordinator.rb +342 -0
  142. data/lib/ace/assign/organisms/prepared_work_builder.rb +194 -0
  143. data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +100 -0
  144. data/lib/ace/assign/organisms/task_assignment_creator.rb +11 -4
  145. data/lib/ace/assign/version.rb +1 -1
  146. data/lib/ace/assign.rb +4 -102
  147. metadata +186 -8
@@ -0,0 +1,627 @@
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