ace-assign 0.64.0 → 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.
- checksums.yaml +4 -4
- data/.ace-defaults/assign/catalog/steps/auto-merge.step.yml +11 -0
- data/CHANGELOG.md +19 -0
- data/docs/protected-authority.md +23 -7
- data/docs/usage.md +117 -4
- data/exe/ace-scope-ready +55 -0
- data/handbook/workflow-instructions/assign/drive.wf.md +26 -1
- data/lib/ace/assign/atoms/attempt_state_machine.rb +8 -0
- data/lib/ace/assign/atoms/delivery_parameters.rb +4 -39
- data/lib/ace/assign/authority/assignment_inventory.rb +290 -0
- data/lib/ace/assign/authority/campaign_consumer_policy.rb +100 -0
- data/lib/ace/assign/authority/campaign_execution.rb +119 -0
- data/lib/ace/assign/authority/campaign_round_transfer.rb +82 -0
- data/lib/ace/assign/authority/candidate_transfer.rb +169 -2
- data/lib/ace/assign/authority/cleanup_result_protection.rb +53 -0
- data/lib/ace/assign/authority/client.rb +156 -6
- data/lib/ace/assign/authority/deployment.rb +201 -39
- data/lib/ace/assign/authority/deployment_history.rb +146 -0
- data/lib/ace/assign/authority/endcap.rb +131 -18
- data/lib/ace/assign/authority/endcap_attempts.rb +274 -0
- data/lib/ace/assign/authority/endcap_campaign_results.rb +232 -0
- data/lib/ace/assign/authority/endcap_campaign_rounds.rb +266 -0
- data/lib/ace/assign/authority/endcap_contexts.rb +23 -0
- data/lib/ace/assign/authority/endcap_inboxes.rb +3 -280
- data/lib/ace/assign/authority/endcap_prepared_work.rb +80 -0
- data/lib/ace/assign/authority/endcap_publication.rb +207 -0
- data/lib/ace/assign/authority/endcap_results.rb +36 -18
- data/lib/ace/assign/authority/endcap_review_requests.rb +183 -0
- data/lib/ace/assign/authority/endcap_reviews.rb +130 -0
- data/lib/ace/assign/authority/endcap_service_settlement.rb +152 -0
- data/lib/ace/assign/authority/endcap_services.rb +221 -18
- data/lib/ace/assign/authority/execution_scope_observation.rb +468 -41
- data/lib/ace/assign/authority/inbox_context_original.rb +113 -0
- data/lib/ace/assign/authority/launch_attempt_consumers.rb +51 -0
- data/lib/ace/assign/authority/launch_campaign_registration.rb +214 -0
- data/lib/ace/assign/authority/launch_control_channel.rb +313 -0
- data/lib/ace/assign/authority/launch_control_exclusions.rb +141 -0
- data/lib/ace/assign/authority/launch_driver.rb +298 -19
- data/lib/ace/assign/authority/launch_input_inhibition.rb +132 -0
- data/lib/ace/assign/authority/launch_lifecycle.rb +275 -100
- data/lib/ace/assign/authority/launch_prepared_work.rb +122 -0
- data/lib/ace/assign/authority/launch_review.rb +91 -0
- data/lib/ace/assign/authority/launch_scope_abort.rb +0 -7
- data/lib/ace/assign/authority/launch_scope_admission.rb +136 -5
- data/lib/ace/assign/authority/launch_scope_close.rb +68 -37
- data/lib/ace/assign/authority/launch_scope_release.rb +696 -17
- data/lib/ace/assign/authority/launch_steering.rb +353 -0
- data/lib/ace/assign/authority/launch_stop.rb +254 -0
- data/lib/ace/assign/authority/maintenance_workspace_retirement.rb +229 -0
- data/lib/ace/assign/authority/original_inbox_selection.rb +41 -0
- data/lib/ace/assign/authority/posix_acl.rb +5 -2
- data/lib/ace/assign/authority/prepared_input.rb +130 -0
- data/lib/ace/assign/authority/prepared_queue.rb +185 -0
- data/lib/ace/assign/authority/prepared_task_context.rb +116 -0
- data/lib/ace/assign/authority/prepared_work.rb +304 -0
- data/lib/ace/assign/authority/prepared_worker.rb +69 -0
- data/lib/ace/assign/authority/protected_assignment_context.rb +226 -0
- data/lib/ace/assign/authority/readiness_hook.rb +120 -0
- data/lib/ace/assign/authority/router.rb +24 -2
- data/lib/ace/assign/authority/server.rb +103 -33
- data/lib/ace/assign/authority/service_cleanup_dispatch.rb +88 -0
- data/lib/ace/assign/authority/service_cleanup_evidence.rb +180 -0
- data/lib/ace/assign/authority/service_delivery_evidence.rb +101 -0
- data/lib/ace/assign/authority/service_evidence.rb +243 -12
- data/lib/ace/assign/authority/service_publication_evidence.rb +110 -0
- data/lib/ace/assign/authority/task_context_entry.rb +32 -0
- data/lib/ace/assign/authority/transfer_codec.rb +44 -12
- data/lib/ace/assign/cli/commands/assignment_target.rb +15 -2
- data/lib/ace/assign/cli/commands/attempt/base.rb +49 -0
- data/lib/ace/assign/cli/commands/attempt/finish.rb +20 -4
- data/lib/ace/assign/cli/commands/attempt/reconcile.rb +11 -0
- data/lib/ace/assign/cli/commands/authority/inbox_context_principal.rb +25 -0
- data/lib/ace/assign/cli/commands/authority/inbox_context_selection.rb +28 -0
- data/lib/ace/assign/cli/commands/authority/launch.rb +58 -6
- data/lib/ace/assign/cli/commands/authority/serve.rb +2 -1
- data/lib/ace/assign/cli/commands/authority/status.rb +1 -1
- data/lib/ace/assign/cli/commands/authority/task_context.rb +31 -0
- data/lib/ace/assign/cli/commands/authority/task_context_principal.rb +25 -0
- data/lib/ace/assign/cli/commands/authority/task_context_selection.rb +30 -0
- data/lib/ace/assign/cli/commands/authority/worker.rb +23 -0
- data/lib/ace/assign/cli/commands/campaign_export_result.rb +55 -0
- data/lib/ace/assign/cli/commands/campaign_record_round.rb +57 -0
- data/lib/ace/assign/cli/commands/create.rb +2 -0
- data/lib/ace/assign/cli/commands/delivery.rb +54 -4
- data/lib/ace/assign/cli/commands/fail.rb +6 -0
- data/lib/ace/assign/cli/commands/finish.rb +23 -2
- data/lib/ace/assign/cli/commands/fork_session.rb +6 -2
- data/lib/ace/assign/cli/commands/inbox_bind.rb +34 -0
- data/lib/ace/assign/cli/commands/protected_submission.rb +61 -0
- data/lib/ace/assign/cli/commands/resume.rb +12 -0
- data/lib/ace/assign/cli/commands/select.rb +2 -0
- data/lib/ace/assign/cli/commands/start.rb +2 -0
- data/lib/ace/assign/cli/commands/status.rb +4 -2
- data/lib/ace/assign/cli/commands/step.rb +3 -1
- data/lib/ace/assign/cli/commands/submit_candidate.rb +34 -0
- data/lib/ace/assign/cli/commands/submit_result.rb +61 -0
- data/lib/ace/assign/cli.rb +36 -5
- data/lib/ace/assign/errors.rb +139 -0
- data/lib/ace/assign/models/assignment.rb +26 -4
- data/lib/ace/assign/models/evidence_event.rb +3 -3
- data/lib/ace/assign/molecules/assignment_manager.rb +12 -2
- data/lib/ace/assign/molecules/attempt_reconciler.rb +20 -0
- data/lib/ace/assign/molecules/canonical_attempt_state.rb +151 -0
- data/lib/ace/assign/molecules/canonical_evidence.rb +5 -2
- data/lib/ace/assign/molecules/canonical_read_snapshot.rb +374 -0
- data/lib/ace/assign/molecules/evidence_journal.rb +720 -44
- data/lib/ace/assign/molecules/execution_scope_lineage.rb +27 -9
- data/lib/ace/assign/molecules/fork_session_launcher.rb +16 -6
- data/lib/ace/assign/molecules/journal_input_inhibition.rb +78 -0
- data/lib/ace/assign/molecules/journal_mutation.rb +82 -13
- data/lib/ace/assign/molecules/journal_prompt_mutation.rb +241 -0
- data/lib/ace/assign/molecules/lifecycle_exclusion.rb +178 -26
- data/lib/ace/assign/molecules/original_launch_binding.rb +37 -0
- data/lib/ace/assign/molecules/protected_control_exclusion.rb +303 -0
- data/lib/ace/assign/molecules/protected_workspace_exclusion.rb +330 -0
- data/lib/ace/assign/molecules/protected_workspace_provisioner.rb +164 -0
- data/lib/ace/assign/molecules/queue_scanner.rb +26 -11
- data/lib/ace/assign/molecules/receipt_verifier.rb +21 -15
- data/lib/ace/assign/molecules/step_writer.rb +27 -5
- data/lib/ace/assign/molecules/terminal_scope_receipt.rb +88 -0
- data/lib/ace/assign/organisms/assignment_executor.rb +4 -4
- data/lib/ace/assign/organisms/attempt_coordinator.rb +66 -76
- data/lib/ace/assign/organisms/prepared_work_builder.rb +194 -0
- data/lib/ace/assign/organisms/protected_delivery_coordinator.rb +100 -0
- data/lib/ace/assign/organisms/task_assignment_creator.rb +4 -2
- data/lib/ace/assign/version.rb +1 -1
- data/lib/ace/assign.rb +1 -112
- metadata +88 -9
- data/lib/ace/assign/cli/commands/inbox_reconcile.rb +0 -36
|
@@ -4,10 +4,17 @@ require "ace/runtime/molecules/cgroup_observation"
|
|
|
4
4
|
require "ace/runtime/molecules/systemd_scope_manager"
|
|
5
5
|
require "ace/runtime/molecules/linux_mount_info"
|
|
6
6
|
require "ace/runtime/molecules/execution_unit_installation"
|
|
7
|
+
require "ace/runtime/molecules/network_installation_evidence"
|
|
8
|
+
require "ace/runtime/molecules/execution_network_selection"
|
|
9
|
+
require "ace/runtime/molecules/execution_boot_baseline"
|
|
10
|
+
require "ace/runtime/molecules/kernel_view_topology"
|
|
7
11
|
require_relative "../molecules/execution_scope_lineage"
|
|
12
|
+
require_relative "../molecules/lifecycle_exclusion"
|
|
8
13
|
require_relative "posix_acl"
|
|
14
|
+
require_relative "maintenance_workspace_retirement"
|
|
9
15
|
require "digest"
|
|
10
16
|
require "json"
|
|
17
|
+
require "ace/herdr/molecules/protected_native_control"
|
|
11
18
|
|
|
12
19
|
module Ace
|
|
13
20
|
module Assign
|
|
@@ -50,8 +57,18 @@ module Ace
|
|
|
50
57
|
end
|
|
51
58
|
|
|
52
59
|
def namespace_identity
|
|
53
|
-
|
|
54
|
-
|
|
60
|
+
File.open("/proc/self/ns/mnt", File::RDONLY) do |namespace|
|
|
61
|
+
stat = namespace.stat
|
|
62
|
+
{"device" => stat.dev, "inode" => stat.ino}
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def authority_socket_identity(authority)
|
|
67
|
+
wire = Ace::Runtime::Molecules::ProtectedSocket
|
|
68
|
+
wire.root_path!(File.dirname(authority.fetch("socket_path")), directory: true, owner: authority.fetch("uid"))
|
|
69
|
+
identity = wire.socket_identity(authority.fetch("socket_path"))
|
|
70
|
+
raise Ace::Runtime::RuntimeUnavailableError, "authority endpoint owner differs" unless identity.last == authority.fetch("uid")
|
|
71
|
+
identity
|
|
55
72
|
end
|
|
56
73
|
|
|
57
74
|
def pin_network_namespace(path)
|
|
@@ -90,6 +107,12 @@ module Ace
|
|
|
90
107
|
raise
|
|
91
108
|
end
|
|
92
109
|
|
|
110
|
+
def resource_topology(path, mount_id:)
|
|
111
|
+
table = Ace::Runtime::Molecules::LinuxMountInfo.new(File.read("/proc/self/mountinfo", Ace::Runtime::Molecules::LinuxMountInfo::LIMIT + 1))
|
|
112
|
+
record = table.by_id(mount_id)
|
|
113
|
+
{"major_minor" => record.fetch("major_minor"), "filesystem_path" => table.filesystem_path(record, path)}
|
|
114
|
+
end
|
|
115
|
+
|
|
93
116
|
def resource_boundary_policy(path)
|
|
94
117
|
stat = File.lstat(path)
|
|
95
118
|
unless stat.directory? && File.realpath(path) == path
|
|
@@ -115,6 +138,37 @@ module Ace
|
|
|
115
138
|
end
|
|
116
139
|
end
|
|
117
140
|
|
|
141
|
+
def lifecycle_resource_policy!(path, authority:, identity:)
|
|
142
|
+
state_root = authority.fetch("state_root")
|
|
143
|
+
unless path.start_with?(state_root + "/lifecycle-exclusion/") && File.expand_path(path) == path
|
|
144
|
+
raise Ace::Runtime::RuntimeUnavailableError, "lifecycle resource ancestry differs"
|
|
145
|
+
end
|
|
146
|
+
parts = path.split("/").reject(&:empty?)
|
|
147
|
+
paths = ["/"] + parts.each_index.map { |index| "/" + parts.take(index + 1).join("/") }
|
|
148
|
+
paths.each do |ancestor|
|
|
149
|
+
policy = resource_boundary_policy(ancestor)
|
|
150
|
+
unless [0, authority.fetch("uid")].include?(policy.fetch("uid")) &&
|
|
151
|
+
(policy.fetch("mode") & 0o7022).zero? && policy.fetch("acl").nil? && resource_default_acl(ancestor).nil?
|
|
152
|
+
raise Ace::Runtime::RuntimeUnavailableError, "lifecycle resource ancestry is writable"
|
|
153
|
+
end
|
|
154
|
+
if ancestor == state_root || ancestor == path
|
|
155
|
+
mode = ancestor == path ? 0o755 : 0o700
|
|
156
|
+
unless policy.values_at("uid", "gid", "mode") == authority.values_at("uid", "gid") + [mode]
|
|
157
|
+
raise Ace::Runtime::RuntimeUnavailableError, "lifecycle resource selected ownership differs"
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
if ancestor == path && policy.values_at("device", "inode", "uid", "gid") !=
|
|
161
|
+
identity.values_at("device", "inode", "uid", "gid")
|
|
162
|
+
raise Ace::Runtime::RuntimeUnavailableError, "lifecycle policy belongs to a replaced object"
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
true
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def resource_default_acl(path)
|
|
169
|
+
PosixAcl.new.entries(path, attribute: "system.posix_acl_default")
|
|
170
|
+
end
|
|
171
|
+
|
|
118
172
|
def worker_uid_quiescent!(uid)
|
|
119
173
|
table = Ace::Runtime::Molecules::LinuxMountInfo.new(File.read("/proc/self/mountinfo", Ace::Runtime::Molecules::LinuxMountInfo::LIMIT + 1))
|
|
120
174
|
proc_mounts = table.records.select { |record| record.fetch("mountpoint") == "/proc" }
|
|
@@ -142,6 +196,13 @@ module Ace
|
|
|
142
196
|
true
|
|
143
197
|
end
|
|
144
198
|
|
|
199
|
+
def resource_present?(path)
|
|
200
|
+
File.lstat(path)
|
|
201
|
+
true
|
|
202
|
+
rescue Errno::ENOENT
|
|
203
|
+
false
|
|
204
|
+
end
|
|
205
|
+
|
|
145
206
|
def boundary_manifest(scope)
|
|
146
207
|
path = "/etc/ace/execution-slots/#{scope.fetch('slot_id')}/boundary-manifest.json"
|
|
147
208
|
bytes = Ace::Runtime::Molecules::ExecutionUnitInstallation::Files.new.read(path, limit: 65_536)
|
|
@@ -154,15 +215,22 @@ module Ace
|
|
|
154
215
|
end
|
|
155
216
|
end
|
|
156
217
|
|
|
157
|
-
def initialize(mapping_id:, deployment:, kernel:, manager: nil, cgroups: Ace::Runtime::Molecules::CgroupObservation.new, files: Files.new)
|
|
218
|
+
def initialize(mapping_id:, deployment:, kernel:, manager: nil, cgroups: Ace::Runtime::Molecules::CgroupObservation.new, files: Files.new, network_evidence: Ace::Runtime::Molecules::NetworkInstallationEvidence.new, boot_evidence: Ace::Runtime::Molecules::ExecutionBootBaseline.new, network_selection: Ace::Runtime::Molecules::ExecutionNetworkSelection.new, workspace_reader_factory: nil)
|
|
158
219
|
@mapping_id, @deployment, @kernel, @cgroups, @files = mapping_id, deployment, kernel, cgroups, files
|
|
220
|
+
@network_evidence = network_evidence
|
|
221
|
+
@boot_evidence = boot_evidence
|
|
222
|
+
@network_selection = network_selection
|
|
223
|
+
@workspace_reader_factory = workspace_reader_factory || ->(projection:) {
|
|
224
|
+
Molecules::LifecycleExclusion::WorkspaceNativeReader.new(projection: projection)
|
|
225
|
+
}
|
|
226
|
+
@native_workspace_lifetime = nil
|
|
159
227
|
@map = deployment.mapping(mapping_id)
|
|
160
228
|
@scope = @map.fetch("execution_scope")
|
|
161
229
|
@manager = manager || Ace::Runtime::Molecules::SystemdScopeManager.new(
|
|
162
230
|
slice_unit: @scope.fetch("slice_unit"), service_unit: @scope.fetch("service_unit"))
|
|
163
231
|
end
|
|
164
232
|
|
|
165
|
-
def observe(lineage)
|
|
233
|
+
def observe(lineage, retirement: nil)
|
|
166
234
|
@deployment.verify!(@mapping_id, kernel: @kernel, manager: @manager)
|
|
167
235
|
binding = lineage.binding
|
|
168
236
|
unless binding && binding.fetch("slot_id") == @scope.fetch("slot_id") && binding.fetch("boot_id") == @files.boot_id &&
|
|
@@ -175,13 +243,14 @@ module Ace
|
|
|
175
243
|
network_selection! == binding.fetch("network_installation_selection")
|
|
176
244
|
unavailable!("retained installed network namespace or selection changed")
|
|
177
245
|
end
|
|
246
|
+
boot_baseline!(binding)
|
|
178
247
|
before = @manager.inspect_activation
|
|
179
248
|
verify_parent!(binding, before.fetch("slice"))
|
|
180
249
|
pinned = @cgroups.pin(binding.fetch("cgroup_identity").fetch("path"), expected: binding.fetch("cgroup_identity"))
|
|
181
|
-
unless parent_resources! == binding.fetch("resource_identities")
|
|
250
|
+
unless parent_resources!(retirement: retirement) == binding.fetch("resource_identities")
|
|
182
251
|
unavailable!("retained parent inventory changed")
|
|
183
252
|
end
|
|
184
|
-
verify_resources!(binding.fetch("resource_identities"))
|
|
253
|
+
verify_resources!(binding.fetch("resource_identities"), retirement: retirement)
|
|
185
254
|
value = @cgroups.observe(pinned)
|
|
186
255
|
after = @manager.inspect_activation
|
|
187
256
|
unless before == after && @files.boot_id == binding.fetch("boot_id")
|
|
@@ -205,6 +274,8 @@ module Ace
|
|
|
205
274
|
network = @files.pin_network_namespace(@scope.fetch("network_namespace_path"))
|
|
206
275
|
@files.worker_uid_quiescent!(@map.fetch("worker_uid"))
|
|
207
276
|
boot = @files.boot_id
|
|
277
|
+
digest = Digest::SHA256.hexdigest(JSON.generate(canonical(@map)))
|
|
278
|
+
baseline = @boot_evidence.select!(expected: boot_baseline_expected(boot, digest, selection))
|
|
208
279
|
namespace = @files.namespace_identity
|
|
209
280
|
before = @manager.inspect_activation
|
|
210
281
|
service = before.fetch("service")
|
|
@@ -228,10 +299,11 @@ module Ace
|
|
|
228
299
|
unavailable!("fresh parent changed or acquired processes before binding")
|
|
229
300
|
end
|
|
230
301
|
verify_resources!(inventory)
|
|
231
|
-
context.merge("slot_id" => @scope.fetch("slot_id"), "deployment_digest" =>
|
|
302
|
+
context.merge("slot_id" => @scope.fetch("slot_id"), "deployment_digest" => digest,
|
|
232
303
|
"boot_id" => boot, "slice_invocation_id" => parent.fetch("InvocationID"), "cgroup_identity" => pinned.fetch(:identity),
|
|
233
304
|
"resource_mount_namespace_identity" => namespace, "resource_identities" => inventory,
|
|
234
|
-
"network_installation_selection" => selection, "network_namespace_identity" => network.fetch(:identity)
|
|
305
|
+
"network_installation_selection" => selection, "network_namespace_identity" => network.fetch(:identity),
|
|
306
|
+
"boot_baseline_selection" => baseline.fetch("selection"))
|
|
235
307
|
rescue SystemCallError, IOError, KeyError, TypeError, ArgumentError, JSON::ParserError
|
|
236
308
|
unavailable!("fresh parent inventory or activation is unavailable")
|
|
237
309
|
ensure
|
|
@@ -239,25 +311,137 @@ module Ace
|
|
|
239
311
|
network&.fetch(:handle)&.close
|
|
240
312
|
end
|
|
241
313
|
|
|
242
|
-
def verify_closed!(lineage)
|
|
243
|
-
value = observe(lineage)
|
|
314
|
+
def verify_closed!(lineage, retirement: nil)
|
|
315
|
+
value = observe(lineage, retirement: retirement)
|
|
244
316
|
service = value.fetch("activation").fetch("service")
|
|
245
317
|
unless lineage.sealed? && lineage.proof_event && value.fetch("populated").zero? &&
|
|
246
318
|
%w[inactive failed].include?(service.fetch("ActiveState")) && service.fetch("MainPID").zero? &&
|
|
247
319
|
service.fetch("ControlPID").zero? && service.fetch("Job") == [0, "/"]
|
|
248
320
|
unavailable!("canonical closure has a live or pending native activation")
|
|
249
321
|
end
|
|
322
|
+
native_cleanup_complete!
|
|
323
|
+
@files.worker_uid_quiescent!(@map.fetch("worker_uid"))
|
|
250
324
|
if lineage.native_event
|
|
251
325
|
native = lineage.native_event.fetch("payload")
|
|
252
326
|
invocation = service.fetch("InvocationID")
|
|
253
327
|
unless invocation.empty? || invocation == native.fetch("service_invocation_id")
|
|
254
328
|
unavailable!("closed native incarnation was replaced")
|
|
255
329
|
end
|
|
256
|
-
verify_resources!(native.fetch("resource_identities"), allow_runtime_absence: true, same_namespace: false)
|
|
330
|
+
verify_resources!(native.fetch("resource_identities"), allow_runtime_absence: true, same_namespace: false, retirement: retirement)
|
|
331
|
+
end
|
|
332
|
+
native_cleanup_complete!
|
|
333
|
+
repeated = observe(lineage, retirement: retirement)
|
|
334
|
+
unless repeated.fetch("populated").zero? && repeated.fetch("activation") == value.fetch("activation")
|
|
335
|
+
unavailable!("scope activation changed during closure verification")
|
|
257
336
|
end
|
|
258
337
|
true
|
|
259
338
|
end
|
|
260
339
|
|
|
340
|
+
def readiness_peer!(lineage, peer)
|
|
341
|
+
lineage.require_open!
|
|
342
|
+
@deployment.verify!(@mapping_id, kernel: @kernel, manager: @manager)
|
|
343
|
+
activation = observe(lineage).fetch("activation")
|
|
344
|
+
service = activation.fetch("service")
|
|
345
|
+
profile = @manager.inspect_profile
|
|
346
|
+
command = profile.fetch("service").fetch("ExecStartPostEx").first
|
|
347
|
+
unless service.fetch("ControlPID").positive? && peer.fetch("pid") == service.fetch("ControlPID") &&
|
|
348
|
+
command.fetch(7) == peer.fetch("pid") && service.fetch("MainPID").positive? &&
|
|
349
|
+
service.fetch("MainPID") != peer.fetch("pid") &&
|
|
350
|
+
peer.values_at("uid", "gid", "groups") == @map.values_at("worker_uid", "worker_gid", "worker_groups")
|
|
351
|
+
unavailable!("peer is not the exact fixed readiness actor")
|
|
352
|
+
end
|
|
353
|
+
server = @kernel.capture(service.fetch("MainPID"))
|
|
354
|
+
unless server.values_at("uid", "gid", "groups") == peer.values_at("uid", "gid", "groups")
|
|
355
|
+
unavailable!("native server principal differs")
|
|
356
|
+
end
|
|
357
|
+
@kernel.live!(peer)
|
|
358
|
+
unavailable!("readiness activation changed") unless @manager.inspect_activation == activation
|
|
359
|
+
server
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def verify_readiness_report!(lineage, peer, report, challenge:)
|
|
363
|
+
server = readiness_peer!(lineage, peer)
|
|
364
|
+
unless report.is_a?(Hash) && report.keys.sort == %w[challenge_id kernel_view_topology mount_namespace_identity network_namespace_identity resource_identities resource_observer_identity resource_topology server_identity version] &&
|
|
365
|
+
report["version"] == 1 && report["challenge_id"] == challenge.fetch("challenge_id") &&
|
|
366
|
+
@kernel.same?(report.fetch("server_identity"), server) && @kernel.same?(report.fetch("resource_observer_identity"), peer)
|
|
367
|
+
unavailable!("private readiness report binding differs")
|
|
368
|
+
end
|
|
369
|
+
network = report.fetch("network_namespace_identity")
|
|
370
|
+
unless network.is_a?(Hash) && network.keys.sort == %w[device inode] &&
|
|
371
|
+
network.values.all? { |value| value.is_a?(Integer) && value.positive? } &&
|
|
372
|
+
network == lineage.binding.fetch("network_namespace_identity") &&
|
|
373
|
+
network == lineage.admission_event.dig("payload", "data", "network_installation", "namespace_identity")
|
|
374
|
+
unavailable!("original server network namespace differs from admitted installation")
|
|
375
|
+
end
|
|
376
|
+
entries = @files.boundary_manifest(@scope).fetch("resources").select { |entry| entry.fetch("worker_visible") }
|
|
377
|
+
resources = report.fetch("resource_identities")
|
|
378
|
+
unless resources.is_a?(Array) && resources.map { |entry| entry.values_at("host_path", "view_path") }.sort ==
|
|
379
|
+
entries.map { |entry| entry.values_at("host_path", "view_path") }.sort
|
|
380
|
+
unavailable!("readiness reachable resource union differs")
|
|
381
|
+
end
|
|
382
|
+
topology = report.fetch("resource_topology")
|
|
383
|
+
unless topology.is_a?(Array) && topology.size == resources.size &&
|
|
384
|
+
topology.map { |entry| entry.values_at("host_path", "view_path") }.sort ==
|
|
385
|
+
entries.map { |entry| entry.values_at("host_path", "view_path") }.sort
|
|
386
|
+
unavailable!("readiness resource mount topology differs")
|
|
387
|
+
end
|
|
388
|
+
resources.each do |resource|
|
|
389
|
+
declaration = entries.find { |entry| entry.values_at("host_path", "view_path") == resource.values_at("host_path", "view_path") }
|
|
390
|
+
view = topology.find { |entry| entry.values_at("host_path", "view_path") == resource.values_at("host_path", "view_path") }
|
|
391
|
+
unless view.keys.sort == %w[host_path major_minor mountpoint options root view_path] &&
|
|
392
|
+
view["options"].is_a?(Array) && view["options"].include?(declaration.fetch("read_only") ? "ro" : "rw") &&
|
|
393
|
+
view["mountpoint"].is_a?(String) && (resource.fetch("view_path") == view["mountpoint"] ||
|
|
394
|
+
resource.fetch("view_path").start_with?(view["mountpoint"].delete_suffix("/") + "/"))
|
|
395
|
+
unavailable!("readiness mount flags or projection differ")
|
|
396
|
+
end
|
|
397
|
+
host_identity = @files.resource_identity(resource.fetch("host_path"))
|
|
398
|
+
host = @files.resource_topology(resource.fetch("host_path"), mount_id: host_identity.fetch("mount_id"))
|
|
399
|
+
physical = File.expand_path(File.join(view.fetch("root"), resource.fetch("view_path").delete_prefix(view.fetch("mountpoint")).delete_prefix("/")))
|
|
400
|
+
unless host.fetch("major_minor") == view.fetch("major_minor") && host.fetch("filesystem_path") == physical
|
|
401
|
+
unavailable!("readiness mounted source or subtree differs")
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
verify_resources!(resources, same_namespace: false)
|
|
405
|
+
binding = lineage.binding
|
|
406
|
+
baseline = boot_baseline!(binding)
|
|
407
|
+
authority = @deployment.authority(@map.fetch("authority_id"))
|
|
408
|
+
endpoint = @files.authority_socket_identity(authority)
|
|
409
|
+
writable = entries.reject { |entry| entry.fetch("read_only") }.map do |entry|
|
|
410
|
+
host = @files.resource_identity(entry.fetch("host_path"))
|
|
411
|
+
@files.resource_topology(entry.fetch("host_path"), mount_id: host.fetch("mount_id"))
|
|
412
|
+
.merge("view_path" => entry.fetch("view_path"), "filesystem_type" => host.fetch("filesystem_type"))
|
|
413
|
+
end
|
|
414
|
+
Ace::Runtime::Molecules::KernelViewTopology.new.verify!(topology: report.fetch("kernel_view_topology"),
|
|
415
|
+
host_ipc: baseline.fetch("host_ipc_namespace_identity"), authority_socket: endpoint, writable_resources: writable,
|
|
416
|
+
host_devpts: baseline.fetch("host_devpts_identity"), selected_devpts: baseline.fetch("selected_devpts"))
|
|
417
|
+
unavailable!("authority endpoint changed during view verification") unless endpoint == @files.authority_socket_identity(authority)
|
|
418
|
+
report.slice("server_identity", "resource_observer_identity", "mount_namespace_identity", "resource_identities").merge(
|
|
419
|
+
"scope_generation" => binding.fetch("scope_generation"), "scope_binding_event_id" => lineage.binding_event.fetch("digest"),
|
|
420
|
+
"service_invocation_id" => @manager.inspect_activation.fetch("service").fetch("InvocationID"),
|
|
421
|
+
"workspace_id" => @map.fetch("native").fetch("workspace_id"),
|
|
422
|
+
"network_namespace_identity" => network.dup,
|
|
423
|
+
"network_admission_event_id" => lineage.admission_event.fetch("digest"))
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def complete_native_readiness!(lineage, payload)
|
|
427
|
+
unavailable!("completed activation lacks authenticated readiness") unless payload.is_a?(Hash)
|
|
428
|
+
activation = observe(lineage).fetch("activation")
|
|
429
|
+
service = activation.fetch("service")
|
|
430
|
+
unless service.fetch("ActiveState") == "active" && service.fetch("ControlPID").zero? && service.fetch("Job") == [0, "/"] &&
|
|
431
|
+
service.fetch("MainPID") == payload.dig("server_identity", "pid") &&
|
|
432
|
+
service.fetch("InvocationID") == payload.fetch("service_invocation_id")
|
|
433
|
+
unavailable!("native service has not completed the exact admitted activation")
|
|
434
|
+
end
|
|
435
|
+
@kernel.live!(payload.fetch("server_identity"))
|
|
436
|
+
socket = Ace::Runtime::Molecules::ProtectedSocket.socket_identity(@map.fetch("native").fetch("socket_path"))
|
|
437
|
+
value = payload.merge("socket_identity" => socket)
|
|
438
|
+
native_map = @map.merge("native" => @map.fetch("native").merge(value.slice("server_identity", "socket_identity")))
|
|
439
|
+
Ace::Herdr::Molecules::ProtectedNativeControl.new(mapping: native_map, kernel: @kernel).preflight!
|
|
440
|
+
verify_resources!(value.fetch("resource_identities"), same_namespace: false)
|
|
441
|
+
unavailable!("native activation changed during preflight") unless @manager.inspect_activation == activation
|
|
442
|
+
value
|
|
443
|
+
end
|
|
444
|
+
|
|
261
445
|
def native_admission_ready!(lineage)
|
|
262
446
|
value = observe(lineage)
|
|
263
447
|
service = value.fetch("activation").fetch("service")
|
|
@@ -265,16 +449,34 @@ module Ace
|
|
|
265
449
|
service.fetch("MainPID").zero? && service.fetch("ControlPID").zero? && service.fetch("Job") == [0, "/"]
|
|
266
450
|
unavailable!("native admission has an occupied or pending generation")
|
|
267
451
|
end
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
452
|
+
binding = lineage.binding
|
|
453
|
+
selection = binding.fetch("network_installation_selection")
|
|
454
|
+
evidence = @network_evidence.verify!(selection: selection, expected: {
|
|
455
|
+
"slot_id" => @scope.fetch("slot_id"), "namespace_path" => @scope.fetch("network_namespace_path"),
|
|
456
|
+
"boot_id" => binding.fetch("boot_id"), "namespace_identity" => binding.fetch("network_namespace_identity"),
|
|
457
|
+
"installer_artifact_sha256" => selection.fetch("installer_artifact").fetch("sha256")})
|
|
458
|
+
selector = lineage.binding_event.fetch("digest")
|
|
459
|
+
if @native_workspace_lifetime
|
|
460
|
+
unavailable!("native workspace lifetime belongs to another scope") unless @native_workspace_lifetime.fetch(:selector) == selector
|
|
461
|
+
@native_workspace_lifetime.fetch(:reader).verify_unchanged!
|
|
462
|
+
else
|
|
463
|
+
reader = @workspace_reader_factory.call(projection: workspace_exclusion_projection!(lineage))
|
|
464
|
+
reader.acquire!
|
|
465
|
+
@native_workspace_lifetime = {selector: selector, reader: reader}
|
|
466
|
+
end
|
|
467
|
+
evidence
|
|
272
468
|
end
|
|
273
469
|
|
|
274
470
|
def start_admitted_service!
|
|
471
|
+
unavailable!("native workspace lifetime is not retained") unless @native_workspace_lifetime
|
|
472
|
+
@native_workspace_lifetime.fetch(:reader).verify_unchanged!
|
|
275
473
|
@manager.start_service
|
|
276
474
|
end
|
|
277
475
|
|
|
476
|
+
# The original source callback passes the maintained dedicated selection.
|
|
477
|
+
# It cannot choose a different slot, slice, principal or unit at launch.
|
|
478
|
+
|
|
479
|
+
|
|
278
480
|
def sealed_service_stop_required?(lineage)
|
|
279
481
|
unavailable!("service stop requires the canonical original seal") unless lineage.sealed?
|
|
280
482
|
service = observe(lineage).fetch("activation").fetch("service")
|
|
@@ -282,11 +484,47 @@ module Ace
|
|
|
282
484
|
service.fetch("MainPID").zero? && service.fetch("ControlPID").zero?)
|
|
283
485
|
end
|
|
284
486
|
|
|
487
|
+
|
|
488
|
+
|
|
285
489
|
def stop_sealed_service!
|
|
286
490
|
@manager.stop_service
|
|
287
491
|
end
|
|
288
492
|
|
|
493
|
+
# Read-only original owner check used before all-root retirement.
|
|
494
|
+
def verify_maintenance_closed!(lineages, retirement: nil)
|
|
495
|
+
@deployment.verify!(@mapping_id, kernel: @kernel, manager: @manager)
|
|
496
|
+
activation = @manager.inspect_activation
|
|
497
|
+
service, slice = activation.values_at("service", "slice")
|
|
498
|
+
unless %w[inactive failed].include?(service.fetch("ActiveState")) &&
|
|
499
|
+
service.fetch("Job") == [0, "/"] && service.fetch("MainPID").zero? && service.fetch("ControlPID").zero? &&
|
|
500
|
+
slice.fetch("Job") == [0, "/"]
|
|
501
|
+
unavailable!("maintenance has live or pending fixed-unit activation")
|
|
502
|
+
end
|
|
503
|
+
parent_resources!(retirement: retirement)
|
|
504
|
+
@files.worker_uid_quiescent!(@map.fetch("worker_uid"))
|
|
505
|
+
if %w[inactive failed].include?(slice.fetch("ActiveState"))
|
|
506
|
+
unless slice.fetch("InvocationID").empty? || lineages.any? { |lineage| lineage.binding.fetch("slice_invocation_id") == slice.fetch("InvocationID") }
|
|
507
|
+
unavailable!("stopped maintenance parent is unknown")
|
|
508
|
+
end
|
|
509
|
+
unless slice.fetch("ControlGroup").empty?
|
|
510
|
+
pinned = @cgroups.pin(Ace::Runtime::Molecules::CgroupObservation::ROOT + slice.fetch("ControlGroup"))
|
|
511
|
+
unavailable!("stopped maintenance parent has writers") unless @cgroups.observe(pinned).fetch("populated").zero?
|
|
512
|
+
end
|
|
513
|
+
else
|
|
514
|
+
exact = lineages.select { |lineage| lineage.binding.fetch("slice_invocation_id") == slice.fetch("InvocationID") }
|
|
515
|
+
unavailable!("maintenance parent has no unique canonical released owner") unless exact.size == 1
|
|
516
|
+
verify_closed!(exact.first, retirement: retirement)
|
|
517
|
+
end
|
|
518
|
+
unless @manager.inspect_activation == activation
|
|
519
|
+
unavailable!("maintenance fixed activation changed during observation")
|
|
520
|
+
end
|
|
521
|
+
true
|
|
522
|
+
ensure
|
|
523
|
+
pinned&.fetch(:handle)&.close
|
|
524
|
+
end
|
|
525
|
+
|
|
289
526
|
def retire_released_parent!(lineages)
|
|
527
|
+
verify_maintenance_closed!(lineages)
|
|
290
528
|
@deployment.verify!(@mapping_id, kernel: @kernel, manager: @manager)
|
|
291
529
|
activation = @manager.inspect_activation
|
|
292
530
|
service = activation.fetch("service")
|
|
@@ -323,42 +561,141 @@ module Ace
|
|
|
323
561
|
service.fetch("MainPID").zero? && service.fetch("ControlPID").zero? && service.fetch("Job") == [0, "/"]
|
|
324
562
|
unavailable!("sealed parent still has writers or pending activation")
|
|
325
563
|
end
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
if
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
564
|
+
native_cleanup_complete!
|
|
565
|
+
# A stopped admitted-but-unbound start can have executed. Its proof
|
|
566
|
+
# therefore uses the same positive writer baseline as a native bound
|
|
567
|
+
# generation; absence of a binding is never a no-execution assertion.
|
|
568
|
+
if lineage.native_event
|
|
569
|
+
native = lineage.native_event.fetch("payload")
|
|
570
|
+
invocation = service.fetch("InvocationID")
|
|
571
|
+
unless invocation.empty? || invocation == native.fetch("service_invocation_id")
|
|
572
|
+
unavailable!("closed native incarnation was replaced")
|
|
573
|
+
end
|
|
574
|
+
verify_resources!(native.fetch("resource_identities"), allow_runtime_absence: true, same_namespace: false)
|
|
334
575
|
end
|
|
335
|
-
# This bounded path proves a generation that never admitted native,
|
|
336
|
-
# child or effect creation. It is not post-native readiness proof.
|
|
337
576
|
current = parent_resources!
|
|
338
577
|
unless current == lineage.binding.fetch("resource_identities") &&
|
|
339
578
|
@files.namespace_identity == lineage.binding.fetch("resource_mount_namespace_identity")
|
|
340
|
-
unavailable!("
|
|
579
|
+
unavailable!("sealed parent resources or observation namespace changed")
|
|
341
580
|
end
|
|
342
581
|
@files.worker_uid_quiescent!(@map.fetch("worker_uid"))
|
|
582
|
+
native_cleanup_complete!
|
|
343
583
|
# Rejoin object and population after the credential baseline, keeping
|
|
344
584
|
# the snapshot bounded by the same retained activation and exclusion.
|
|
345
|
-
|
|
346
|
-
|
|
585
|
+
repeated = observe(lineage)
|
|
586
|
+
unless repeated.fetch("populated").zero? && repeated.fetch("activation") == value.fetch("activation")
|
|
587
|
+
unavailable!("sealed parent acquired a writer or pending activation during baseline observation")
|
|
347
588
|
end
|
|
348
|
-
lineage.binding.slice("scope_generation", "boot_id", "slice_invocation_id", "cgroup_identity").merge(
|
|
589
|
+
proof = lineage.binding.slice("scope_generation", "boot_id", "slice_invocation_id", "cgroup_identity").merge(
|
|
349
590
|
"scope_binding_event_id" => lineage.binding_event.fetch("digest"), "seal_event_id" => lineage.seal_event.fetch("digest"), "populated" => 0)
|
|
591
|
+
if @native_workspace_lifetime
|
|
592
|
+
unavailable!("closed proof differs from native workspace lifetime") unless @native_workspace_lifetime.fetch(:selector) == lineage.binding_event.fetch("digest")
|
|
593
|
+
@native_workspace_lifetime.fetch(:reader).verify_unchanged!
|
|
594
|
+
@native_workspace_lifetime.fetch(:reader).close!
|
|
595
|
+
@native_workspace_lifetime = nil
|
|
596
|
+
end
|
|
597
|
+
proof
|
|
598
|
+
end
|
|
599
|
+
|
|
600
|
+
# Declaration bytes are selected by this original observer's pinned
|
|
601
|
+
# deployment digest. A workspace cannot inherit eligibility merely
|
|
602
|
+
# because another parent resource has the same host pathname.
|
|
603
|
+
def maintenance_workspace_resource!(lineage)
|
|
604
|
+
manifest = @files.boundary_manifest(@scope)
|
|
605
|
+
declarations = manifest.fetch("resources").select { |entry| entry.fetch("host_path") == @map.fetch("worker_cwd") }
|
|
606
|
+
unless declarations.one? && declarations.first.values_at("stage", "worker_visible", "read_only") == ["parent", true, false]
|
|
607
|
+
unavailable!("workspace original declaration is not a writable parent view")
|
|
608
|
+
end
|
|
609
|
+
declaration = declarations.first
|
|
610
|
+
resources = lineage.binding.fetch("resource_identities").select do |entry|
|
|
611
|
+
entry.values_at("host_path", "view_path") == declaration.values_at("host_path", "view_path")
|
|
612
|
+
end
|
|
613
|
+
unless resources.one? && %w[mount_id device inode].all? { |key| resources.first.fetch(key).is_a?(Integer) && resources.first.fetch(key).positive? }
|
|
614
|
+
unavailable!("workspace original parent identity is unavailable")
|
|
615
|
+
end
|
|
616
|
+
resources.first
|
|
617
|
+
rescue KeyError, TypeError, ArgumentError, Ace::Runtime::RuntimeUnavailableError
|
|
618
|
+
unavailable!("workspace original boundary is unavailable")
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def maintenance_parent_resource_declarations!(lineage)
|
|
622
|
+
declarations = @files.boundary_manifest(@scope).fetch("resources").select { |entry| entry.fetch("stage") == "parent" }.map do |entry|
|
|
623
|
+
entry.slice("host_path", "view_path", "stage", "worker_visible", "read_only")
|
|
624
|
+
end
|
|
625
|
+
identities = lineage.binding.fetch("resource_identities")
|
|
626
|
+
selectors = declarations.map { |entry| entry.values_at("host_path", "view_path") }
|
|
627
|
+
bound = identities.map { |entry| entry.values_at("host_path", "view_path") }
|
|
628
|
+
unless selectors.uniq == selectors && bound.uniq == bound && selectors.sort == bound.sort &&
|
|
629
|
+
declarations.all? { |entry| entry.keys.sort == %w[host_path read_only stage view_path worker_visible] } &&
|
|
630
|
+
identities.all? { |entry| entry.keys.sort == Molecules::ExecutionScopeLineage::RESOURCE_FIELDS.sort }
|
|
631
|
+
unavailable!("original parent declarations differ from bound identities")
|
|
632
|
+
end
|
|
633
|
+
declarations
|
|
634
|
+
rescue KeyError, TypeError, ArgumentError, Ace::Runtime::RuntimeUnavailableError
|
|
635
|
+
unavailable!("original parent resource declarations are unavailable")
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def workspace_exclusion_projection!(lineage)
|
|
639
|
+
declarations = maintenance_parent_resource_declarations!(lineage)
|
|
640
|
+
cwd = maintenance_workspace_resource!(lineage)
|
|
641
|
+
authority = @deployment.authority(@map.fetch("authority_id"))
|
|
642
|
+
selection = Molecules::LifecycleExclusion.workspace_selection(mapping_id: @mapping_id,
|
|
643
|
+
project_id: @map.fetch("project_id"), authority: authority, cwd_resource: cwd)
|
|
644
|
+
matches = declarations.select do |entry|
|
|
645
|
+
entry.values_at("host_path", "view_path") == selection.values_at("host_path", "view_path")
|
|
646
|
+
end
|
|
647
|
+
roots = lineage.binding.fetch("resource_identities").select do |entry|
|
|
648
|
+
entry.values_at("host_path", "view_path") == selection.values_at("host_path", "view_path")
|
|
649
|
+
end
|
|
650
|
+
unless matches.one? && matches.first.values_at("stage", "worker_visible", "read_only") == ["parent", true, true] &&
|
|
651
|
+
roots.one? && roots.first.values_at("uid", "gid") == authority.values_at("uid", "gid")
|
|
652
|
+
unavailable!("original workspace lifecycle exclusion is unavailable")
|
|
653
|
+
end
|
|
654
|
+
value = JSON.parse(JSON.generate({"key" => selection.fetch("key"), "root_resource" => roots.first,
|
|
655
|
+
"worker_cwd_resource" => cwd, "authority_uid" => authority.fetch("uid"), "authority_gid" => authority.fetch("gid")}))
|
|
656
|
+
pending = [value]
|
|
657
|
+
until pending.empty?
|
|
658
|
+
entry = pending.pop
|
|
659
|
+
pending.concat(entry.values) if entry.is_a?(Hash)
|
|
660
|
+
pending.concat(entry) if entry.is_a?(Array)
|
|
661
|
+
entry.freeze
|
|
662
|
+
end
|
|
663
|
+
value
|
|
664
|
+
rescue KeyError, TypeError, ArgumentError
|
|
665
|
+
unavailable!("original workspace lifecycle projection differs")
|
|
350
666
|
end
|
|
351
667
|
|
|
352
668
|
private
|
|
353
669
|
|
|
670
|
+
def boot_baseline!(binding)
|
|
671
|
+
@boot_evidence.verify!(selection: binding.fetch("boot_baseline_selection"), expected:
|
|
672
|
+
boot_baseline_expected(binding.fetch("boot_id"), binding.fetch("deployment_digest"),
|
|
673
|
+
binding.fetch("network_installation_selection")))
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
def boot_baseline_expected(boot, digest, selection)
|
|
677
|
+
{"slot_id" => @scope.fetch("slot_id"), "boot_id" => boot, "deployment_digest" => digest,
|
|
678
|
+
"installer_artifact" => selection.fetch("installer_artifact")}
|
|
679
|
+
end
|
|
680
|
+
|
|
354
681
|
# Inventory only: no configurable security booleans or alternative
|
|
355
682
|
# policy backend. Native-only objects are observed at their later stage.
|
|
683
|
+
def native_cleanup_complete!
|
|
684
|
+
@files.boundary_manifest(@scope).fetch("resources").each do |entry|
|
|
685
|
+
next unless entry.fetch("stage") == "native"
|
|
686
|
+
if @files.resource_present?(entry.fetch("host_path"))
|
|
687
|
+
unavailable!("fixed service cleanup retained a service-created resource")
|
|
688
|
+
end
|
|
689
|
+
end
|
|
690
|
+
true
|
|
691
|
+
end
|
|
692
|
+
|
|
356
693
|
def network_selection!
|
|
357
694
|
manifest = @files.boundary_manifest(@scope)
|
|
358
|
-
|
|
695
|
+
@network_selection.select!(static_selection: manifest.fetch("network_installation"), slot_id: @scope.fetch("slot_id"))
|
|
359
696
|
end
|
|
360
697
|
|
|
361
|
-
def parent_resources!
|
|
698
|
+
def parent_resources!(retirement: nil)
|
|
362
699
|
manifest = @files.boundary_manifest(@scope)
|
|
363
700
|
unless manifest.is_a?(Hash) && manifest.keys.sort == %w[network_installation resources schema slot_id] &&
|
|
364
701
|
manifest["schema"] == BOUNDARY_SCHEMA && manifest["slot_id"] == @scope.fetch("slot_id") &&
|
|
@@ -367,7 +704,10 @@ module Ace
|
|
|
367
704
|
end
|
|
368
705
|
pairs = []
|
|
369
706
|
resources = manifest.fetch("resources").filter_map do |resource|
|
|
370
|
-
unless resource.is_a?(Hash) && resource.keys.sort == %w[host_path stage view_path] &&
|
|
707
|
+
unless resource.is_a?(Hash) && resource.keys.sort == %w[host_path read_only stage view_path worker_visible] &&
|
|
708
|
+
[true, false].include?(resource["worker_visible"]) && [true, false].include?(resource["read_only"]) &&
|
|
709
|
+
(resource["worker_visible"] || resource["stage"] == "parent" && resource["read_only"]) &&
|
|
710
|
+
(resource["stage"] != "native" || resource["worker_visible"]) && %w[parent native].include?(resource["stage"])
|
|
371
711
|
unavailable!("boundary resource inventory is not closed")
|
|
372
712
|
end
|
|
373
713
|
paths = resource.values_at("host_path", "view_path")
|
|
@@ -377,30 +717,72 @@ module Ace
|
|
|
377
717
|
end
|
|
378
718
|
pairs << paths
|
|
379
719
|
next if resource.fetch("stage") == "native"
|
|
380
|
-
actual = @files.resource_identity(resource.fetch("host_path"))
|
|
720
|
+
actual = retirement_resource_identity(resource.fetch("host_path"), retirement) || @files.resource_identity(resource.fetch("host_path"))
|
|
381
721
|
unless LOCAL_FILESYSTEMS.include?(actual.fetch("filesystem_type"))
|
|
382
722
|
unavailable!("parent resource backing is not local kernel-managed storage")
|
|
383
723
|
end
|
|
384
724
|
resource.slice("host_path", "view_path").merge(actual)
|
|
385
725
|
end
|
|
386
726
|
unavailable!("parent resource inventory is empty") if resources.empty?
|
|
387
|
-
verify_parent_access_boundary!(resources)
|
|
727
|
+
verify_parent_access_boundary!(resources, declarations: manifest.fetch("resources"), retirement: retirement)
|
|
388
728
|
resources
|
|
389
729
|
end
|
|
390
730
|
|
|
391
|
-
def
|
|
731
|
+
def lifecycle_parent_resource!(resources, declarations)
|
|
732
|
+
view = "/run/ace/lifecycle-exclusion/#{@mapping_id}"
|
|
733
|
+
selected = declarations.select { |entry| entry.fetch("view_path") == view }
|
|
734
|
+
return nil if selected.empty?
|
|
735
|
+
unless selected.one? && selected.first.values_at("stage", "worker_visible", "read_only") == ["parent", true, true]
|
|
736
|
+
unavailable!("lifecycle resource is not the exact readonly parent view")
|
|
737
|
+
end
|
|
738
|
+
cwd = resources.select { |resource| resource.fetch("host_path") == @map.fetch("worker_cwd") }
|
|
739
|
+
cwd_declarations = declarations.select { |entry| entry.fetch("host_path") == @map.fetch("worker_cwd") }
|
|
740
|
+
unless cwd.one? && cwd_declarations.one? &&
|
|
741
|
+
cwd_declarations.first.values_at("stage", "worker_visible", "read_only") == ["parent", true, false]
|
|
742
|
+
unavailable!("lifecycle resource lacks original writable workspace identity")
|
|
743
|
+
end
|
|
744
|
+
authority = @deployment.authority(@map.fetch("authority_id"))
|
|
745
|
+
selection = Molecules::LifecycleExclusion.workspace_selection(mapping_id: @mapping_id,
|
|
746
|
+
project_id: @map.fetch("project_id"), authority: authority, cwd_resource: cwd.first)
|
|
747
|
+
expected = selection.fetch("host_path")
|
|
748
|
+
resource = resources.find { |entry| entry.values_at("host_path", "view_path") == [expected, view] }
|
|
749
|
+
unless selected.first.fetch("host_path") == expected && resource &&
|
|
750
|
+
resource.values_at("uid", "gid") == authority.values_at("uid", "gid")
|
|
751
|
+
unavailable!("lifecycle resource is not selected by original workspace and authority")
|
|
752
|
+
end
|
|
753
|
+
@files.lifecycle_resource_policy!(expected, authority: authority, identity: resource)
|
|
754
|
+
@files.resource_aliases(expected, identity: resource).each do |path|
|
|
755
|
+
@files.lifecycle_resource_policy!(path, authority: authority, identity: resource)
|
|
756
|
+
end
|
|
757
|
+
resource
|
|
758
|
+
end
|
|
759
|
+
|
|
760
|
+
def verify_parent_access_boundary!(resources, declarations:, retirement: nil)
|
|
761
|
+
lifecycle = lifecycle_parent_resource!(resources, declarations)
|
|
762
|
+
resources = resources.reject { |resource| resource.equal?(lifecycle) }
|
|
763
|
+
covered = resources.select { |resource| retirement_covered_resource(resource, retirement) }
|
|
764
|
+
vanished = retirement && retirement.descriptor.fetch("phase") != "captured" ? covered : []
|
|
765
|
+
resources = resources - vanished
|
|
392
766
|
worker = @map.fetch("worker_uid")
|
|
393
767
|
authority = @deployment.authority(@map.fetch("authority_id")).fetch("uid")
|
|
394
768
|
readers = @deployment.project(@map.fetch("project_id")).fetch("supervisor_uids")
|
|
395
769
|
trusted = [worker, authority, @map.fetch("launcher_uid"), *readers].uniq
|
|
396
770
|
policies = resources.to_h do |resource|
|
|
397
771
|
path = resource.fetch("host_path")
|
|
398
|
-
|
|
772
|
+
observed_path = retirement_observation_path(resource, retirement)
|
|
773
|
+
policy = @files.resource_boundary_policy(observed_path)
|
|
399
774
|
unless %w[device inode uid gid].all? { |key| policy.fetch(key) == resource.fetch(key) }
|
|
400
775
|
unavailable!("parent access policy belongs to a replaced object")
|
|
401
776
|
end
|
|
402
777
|
[path, policy]
|
|
403
778
|
end
|
|
779
|
+
captured_ancestors = covered.flat_map do |resource|
|
|
780
|
+
path = retirement_observation_path(resource, retirement)
|
|
781
|
+
next [] if path == resource.fetch("host_path")
|
|
782
|
+
parts = File.dirname(path).split("/").reject(&:empty?)
|
|
783
|
+
["/"] + parts.each_index.map { |index| "/" + parts.take(index + 1).join("/") }
|
|
784
|
+
end.uniq
|
|
785
|
+
captured_ancestors.each { |path| policies[path] = @files.resource_boundary_policy(path) }
|
|
404
786
|
barriers = policies.select do |_path, policy|
|
|
405
787
|
next false unless policy.fetch("uid").zero? && (policy.fetch("mode") & 0o7022).zero?
|
|
406
788
|
acl = policy.fetch("acl")
|
|
@@ -417,13 +799,13 @@ module Ace
|
|
|
417
799
|
end
|
|
418
800
|
end
|
|
419
801
|
end
|
|
420
|
-
unless resources.any? { |resource| resource.fetch("uid") == worker } && resources.all? { |resource|
|
|
421
|
-
path = resource
|
|
802
|
+
unless (resources + vanished).any? { |resource| resource.fetch("uid") == worker } && resources.all? { |resource|
|
|
803
|
+
path = retirement_observation_path(resource, retirement)
|
|
422
804
|
[0, worker].include?(resource.fetch("uid")) && barriers.keys.any? { |ancestor| path == ancestor || path.start_with?(ancestor + "/") } }
|
|
423
805
|
unavailable!("parent roots lack an immutable exclusive host traversal boundary")
|
|
424
806
|
end
|
|
425
807
|
resources.each do |resource|
|
|
426
|
-
aliases = @files.resource_aliases(resource
|
|
808
|
+
aliases = @files.resource_aliases(retirement_observation_path(resource, retirement), identity: resource)
|
|
427
809
|
unless aliases.all? { |path| barriers.keys.any? { |ancestor| path == ancestor || path.start_with?(ancestor + "/") } }
|
|
428
810
|
unavailable!("protected parent backing is exposed through an outside writable mount")
|
|
429
811
|
end
|
|
@@ -439,11 +821,11 @@ module Ace
|
|
|
439
821
|
end
|
|
440
822
|
end
|
|
441
823
|
|
|
442
|
-
def verify_resources!(resources, allow_runtime_absence: false, same_namespace: true)
|
|
824
|
+
def verify_resources!(resources, allow_runtime_absence: false, same_namespace: true, retirement: nil)
|
|
443
825
|
resources.each do |resource|
|
|
444
826
|
path = resource.fetch("host_path")
|
|
445
827
|
begin
|
|
446
|
-
actual = @files.resource_identity(path)
|
|
828
|
+
actual = retirement_resource_identity(path, retirement) || @files.resource_identity(path)
|
|
447
829
|
rescue Errno::ENOENT
|
|
448
830
|
runtime = @scope.fetch("runtime_directory")
|
|
449
831
|
# Fixed stopped RuntimeDirectoryPreserve=no cleanup may remove
|
|
@@ -459,6 +841,51 @@ module Ace
|
|
|
459
841
|
end
|
|
460
842
|
end
|
|
461
843
|
|
|
844
|
+
def retirement_covered_resource(resource, retirement)
|
|
845
|
+
return nil unless retirement
|
|
846
|
+
unless retirement.is_a?(MaintenanceWorkspaceRetirement::Evidence)
|
|
847
|
+
unavailable!("maintenance retirement requires source-owned proof")
|
|
848
|
+
end
|
|
849
|
+
retirement.verify_consumption!
|
|
850
|
+
entry = retirement.covered_resource(resource.fetch("host_path"))
|
|
851
|
+
unavailable!("retirement row differs from original resource") if entry && entry.fetch("original") != resource
|
|
852
|
+
entry
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
def retirement_resource_identity(path, retirement)
|
|
856
|
+
return nil unless retirement
|
|
857
|
+
unless retirement.is_a?(MaintenanceWorkspaceRetirement::Evidence)
|
|
858
|
+
unavailable!("maintenance retirement requires source-owned proof")
|
|
859
|
+
end
|
|
860
|
+
retirement.verify_consumption!
|
|
861
|
+
entry = retirement.covered_resource(path)
|
|
862
|
+
return nil unless entry
|
|
863
|
+
resource = entry.fetch("original")
|
|
864
|
+
return resource.reject { |key, _| %w[host_path view_path].include?(key) } unless retirement.descriptor.fetch("phase") == "captured"
|
|
865
|
+
observed_path = retirement_observation_path(resource, retirement)
|
|
866
|
+
actual = @files.resource_identity(observed_path)
|
|
867
|
+
unless %w[device inode uid gid mount_id filesystem_type].all? { |key| actual.fetch(key) == resource.fetch(key) }
|
|
868
|
+
unavailable!("retirement captured path differs from original resource")
|
|
869
|
+
end
|
|
870
|
+
actual
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
def retirement_observation_path(resource, retirement)
|
|
874
|
+
entry = retirement_covered_resource(resource, retirement)
|
|
875
|
+
return resource.fetch("host_path") unless entry && retirement.descriptor.fetch("phase") == "captured"
|
|
876
|
+
held = retirement.owner.retirement_resource_handle!(evidence: retirement, resource: resource)
|
|
877
|
+
unless held.is_a?(Hash) && held.keys.sort == %i[captured_host_path handle].sort &&
|
|
878
|
+
held.fetch(:captured_host_path) == entry.fetch("captured_host_path")
|
|
879
|
+
unavailable!("retirement captured resource handle differs")
|
|
880
|
+
end
|
|
881
|
+
handle = held.fetch(:handle)
|
|
882
|
+
stat = handle.stat
|
|
883
|
+
unless !handle.closed? && stat.directory? && [stat.dev, stat.ino, stat.uid, stat.gid] == resource.values_at("device", "inode", "uid", "gid")
|
|
884
|
+
unavailable!("retirement captured resource is not original held inode")
|
|
885
|
+
end
|
|
886
|
+
held.fetch(:captured_host_path)
|
|
887
|
+
end
|
|
888
|
+
|
|
462
889
|
def canonical(value)
|
|
463
890
|
case value
|
|
464
891
|
when Hash then value.sort.to_h.transform_values { |item| canonical(item) }
|