ace-runtime 0.1.1 → 0.3.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 (29) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -0
  3. data/README.md +9 -0
  4. data/docs/usage.md +123 -0
  5. data/lib/ace/runtime/molecules/cgroup_observation.rb +148 -0
  6. data/lib/ace/runtime/molecules/codex_runtime_installation.rb +130 -0
  7. data/lib/ace/runtime/molecules/execution_boot_baseline.rb +134 -0
  8. data/lib/ace/runtime/molecules/execution_network_selection.rb +85 -0
  9. data/lib/ace/runtime/molecules/execution_unit_installation.rb +663 -0
  10. data/lib/ace/runtime/molecules/kernel_view_topology.rb +133 -0
  11. data/lib/ace/runtime/molecules/linux_mount_info.rb +101 -0
  12. data/lib/ace/runtime/molecules/network_installation_evidence.rb +332 -0
  13. data/lib/ace/runtime/molecules/process_identity.rb +123 -0
  14. data/lib/ace/runtime/molecules/protected_artifact_set.rb +211 -0
  15. data/lib/ace/runtime/molecules/protected_linux.rb +118 -0
  16. data/lib/ace/runtime/molecules/protected_socket.rb +119 -0
  17. data/lib/ace/runtime/molecules/protected_task_context_entry.rb +162 -0
  18. data/lib/ace/runtime/molecules/protected_worker_entry.rb +32 -0
  19. data/lib/ace/runtime/molecules/readiness_configuration.rb +154 -0
  20. data/lib/ace/runtime/molecules/readiness_runtime.rb +94 -0
  21. data/lib/ace/runtime/molecules/server_resource_observation.rb +183 -0
  22. data/lib/ace/runtime/molecules/systemd_scope_manager.rb +411 -0
  23. data/lib/ace/runtime/testing/adapter_contract.rb +9 -0
  24. data/lib/ace/runtime/version.rb +1 -1
  25. data/lib/ace/runtime.rb +3 -0
  26. data/native/README.md +124 -0
  27. data/native/build-worker-gate +20 -0
  28. data/native/worker_gate.c +385 -0
  29. metadata +63 -2
@@ -0,0 +1,663 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "protected_worker_entry"
4
+ require_relative "execution_network_selection"
5
+
6
+ require "json"
7
+ require "digest"
8
+ require "etc"
9
+ require_relative "systemd_scope_manager"
10
+ require_relative "readiness_configuration"
11
+
12
+ module Ace
13
+ module Runtime
14
+ module Molecules
15
+ # Root-installed artifacts plus the system manager's effective properties.
16
+ # These establish installation identity, not effective runtime isolation;
17
+ # the same-User readiness action and live owner verify that separately.
18
+ class ExecutionUnitInstallation
19
+ SCHEMA = "ace.execution-unit-manifest/v1"
20
+ ROLES = %w[slice_fragment service_fragment unit_dropin native_executable native_configuration
21
+ readiness_executable readiness_configuration boundary_manifest bootstrap worker_executable runtime_dependency].freeze
22
+ REQUIRED_ROLES = %w[slice_fragment service_fragment native_executable native_configuration
23
+ readiness_executable readiness_configuration boundary_manifest bootstrap worker_executable].freeze
24
+ CONTEXT_REQUIRED_ROLES = %w[slice_fragment service_fragment boundary_manifest bootstrap
25
+ context_executable context_configuration interpreter].freeze
26
+ CONTEXT_ROLES = (CONTEXT_REQUIRED_ROLES + %w[unit_dropin runtime_dependency]).freeze
27
+ CODEX_REQUIRED_ROLES = %w[slice_fragment service_fragment native_executable native_configuration boundary_manifest].freeze
28
+ CODEX_ROLES = (CODEX_REQUIRED_ROLES + %w[unit_dropin runtime_dependency]).freeze
29
+ SERVICE_REQUIRED = {"Type" => "exec", "Restart" => "no", "RestartForceExitStatus" => [[], []],
30
+ "KillMode" => "control-group", "SendSIGKILL" => true, "Delegate" => false,
31
+ "ProtectControlGroups" => true, "NoNewPrivileges" => true, "CapabilityBoundingSet" => 0,
32
+ "AmbientCapabilities" => 0, "RestrictNamespaces" => 0, "PrivateIPC" => true,
33
+ "PrivateDevices" => true, "PrivateTmp" => false, "MountAPIVFS" => true, "ProtectKernelTunables" => true, "BindLogSockets" => false,
34
+ "DevicePolicy" => "closed", "DeviceAllow" => [], "ProtectSystem" => "strict", "DynamicUser" => false, "PAMName" => "",
35
+ "EnvironmentFiles" => [], "PassEnvironment" => [], "UnsetEnvironment" => [],
36
+ "StandardInput" => "null", "TTYPath" => "", "StandardOutput" => "journal", "StandardError" => "journal", "RuntimeDirectoryPreserve" => "no",
37
+ "RuntimeDirectoryMode" => 0o700, "UMask" => 0o077, "RootImage" => "", "RootImageOptions" => [], "RootEphemeral" => false, "ExtensionDirectories" => [],
38
+ "ExtensionImages" => [], "MountImages" => [], "TemporaryFileSystem" => [], "InaccessiblePaths" => []}.freeze
39
+ EMPTY_ACTIVATION = %w[Requisite BindsTo PartOf Upholds OnFailure OnSuccess OnFailureOf OnSuccessOf
40
+ TriggeredBy Triggers PropagatesStopTo StopPropagatedFrom JoinsNamespaceOf].freeze
41
+ NATIVE_ENVIRONMENT = %w[HERDR_CONFIG_PATH HERDR_SOCKET_PATH HOME SHELL PATH LANG LC_ALL TERM
42
+ TMPDIR TMP TEMP XDG_CONFIG_HOME XDG_DATA_HOME XDG_STATE_HOME XDG_CACHE_HOME XDG_RUNTIME_DIR CODEX_HOME CLAUDE_CONFIG_DIR].freeze
43
+ MANAGER_ENVIRONMENT = (NATIVE_ENVIRONMENT + %w[LANGUAGE LC_CTYPE LC_NUMERIC LC_TIME LC_COLLATE LC_MONETARY LC_MESSAGES
44
+ LC_PAPER LC_NAME LC_ADDRESS LC_TELEPHONE LC_MEASUREMENT LC_IDENTIFICATION]).freeze
45
+ IMPLICIT_NAMESPACE_ROOTS = %w[/dev /proc /sys /run /tmp /var/tmp].freeze
46
+ EMPTY_EXEC = %w[ExecConditionEx ExecStartPreEx ExecReloadEx ExecStopEx ExecStopPostEx].freeze
47
+
48
+ class Files
49
+ def authority_socket!(authority)
50
+ path = authority.fetch("socket_path")
51
+ ProtectedSocket.root_path!(File.dirname(path), directory: true, owner: authority.fetch("uid"))
52
+ identity = ProtectedSocket.socket_identity(path)
53
+ raise RuntimeUnavailableError, "selected authority socket owner differs" unless identity.last == authority.fetch("uid")
54
+ identity
55
+ end
56
+ def read(path, limit:)
57
+ ProtectedSocket.root_path!(path)
58
+ File.open(path, File::RDONLY | File::NONBLOCK | File::NOFOLLOW) do |file|
59
+ stat = file.stat
60
+ unless stat.file? && stat.uid.zero? && (stat.mode & 0o6022).zero? && stat.size <= limit
61
+ raise RuntimeUnavailableError, "installed artifact descriptor ownership is unsafe"
62
+ end
63
+ bytes = file.read(limit + 1)
64
+ raise RuntimeUnavailableError, "installed artifact is oversized" if bytes.bytesize > limit
65
+ after = file.stat
66
+ unless [stat.dev, stat.ino, stat.uid, stat.gid, stat.mode, stat.size, stat.mtime, stat.ctime] ==
67
+ [after.dev, after.ino, after.uid, after.gid, after.mode, after.size, after.mtime, after.ctime] && bytes.bytesize == stat.size
68
+ raise RuntimeUnavailableError, "installed held artifact changed"
69
+ end
70
+ bytes
71
+ end
72
+ end
73
+
74
+ def activation_routes(paths, service_unit)
75
+ routes = []
76
+ visited = 0
77
+ pending = paths.map { |path| [path, 0] }
78
+ until pending.empty?
79
+ directory, depth = pending.shift
80
+ begin
81
+ ProtectedSocket.root_path!(directory, directory: true)
82
+ rescue RuntimeUnavailableError
83
+ if !File.exist?(directory) && !File.symlink?(directory)
84
+ ancestor = File.dirname(directory)
85
+ ancestor = File.dirname(ancestor) until File.exist?(ancestor) || ancestor == "/"
86
+ ProtectedSocket.root_path!(ancestor, directory: true)
87
+ next
88
+ end
89
+ raise
90
+ end
91
+ Dir.children(directory).sort.each do |name|
92
+ visited += 1
93
+ raise RuntimeUnavailableError, "installed activation graph is oversized" if visited > 50_000
94
+ path = File.join(directory, name)
95
+ stat = File.lstat(path)
96
+ if stat.symlink?
97
+ raise RuntimeUnavailableError, "activation graph link owner differs" unless stat.uid.zero?
98
+ target = File.expand_path(File.readlink(path), directory)
99
+ if File.basename(target) == service_unit ||
100
+ (name == service_unit && directory.match?(/\.(?:wants|requires|upholds)\z/))
101
+ routes << path
102
+ end
103
+ elsif stat.directory?
104
+ raise RuntimeUnavailableError, "activation graph directory is too deep" if depth >= 8
105
+ pending << [path, depth + 1]
106
+ end
107
+ end
108
+ end
109
+ routes
110
+ end
111
+
112
+ def sha256(path)
113
+ ProtectedSocket.root_path!(path)
114
+ File.open(path, File::RDONLY | File::NOFOLLOW) do |file|
115
+ stat = file.stat
116
+ unless stat.file? && stat.uid.zero? && (stat.mode & 0o6022).zero? && stat.size <= 268_435_456
117
+ raise RuntimeUnavailableError, "installed artifact kind/mode/size is unsafe"
118
+ end
119
+ digest = Digest::SHA256.new
120
+ while (chunk = file.read(65_536))
121
+ digest.update(chunk)
122
+ end
123
+ digest.hexdigest
124
+ end
125
+ end
126
+ end
127
+
128
+ def initialize(scope:, native:, bootstrap:, worker_entry:, worker_uid:, worker_gid:, files: Files.new)
129
+ @scope, @native, @bootstrap, @worker_entry, @files = scope, native, bootstrap, worker_entry, files
130
+ ProtectedWorkerEntry.validate!(@worker_entry)
131
+ @worker_uid, @worker_gid = worker_uid, worker_gid
132
+ end
133
+
134
+ # Context entries have their own fixed protocol. The same effective
135
+ # installation, activation, mount and isolation checks still apply.
136
+ def self.for_inbox_context(service:, configuration:, load_paths:, authority:, owner_credentials:, files: Files.new)
137
+ unless owner_credentials.is_a?(Hash) && owner_credentials.keys.sort == %w[gid groups uid] &&
138
+ owner_credentials.values_at("uid", "gid").all? { |id| id.is_a?(Integer) && id.positive? } &&
139
+ owner_credentials["groups"].is_a?(Array) && owner_credentials["groups"].size <= 64 &&
140
+ owner_credentials["groups"].all? { |id| id.is_a?(Integer) && id >= 0 } &&
141
+ owner_credentials["groups"] == owner_credentials["groups"].sort.uniq &&
142
+ load_paths.is_a?(Array) && load_paths.size.between?(1, 64) && load_paths.uniq.size == load_paths.size
143
+ raise RuntimeUnavailableError, "context installation selection differs"
144
+ end
145
+ selected = allocate
146
+ selected.send(:initialize_context, service, configuration, load_paths, authority, owner_credentials, files)
147
+ selected
148
+ end
149
+
150
+ # Selection only. The original bootstrap authenticates mapping ownership;
151
+ # verify! still requires the actual fixed manager profile and full topology.
152
+ def verify_codex_runtime_selection!(service:, intent_reference:, manager:)
153
+ unless @codex_service && @codex_service == service && @codex_intent_reference == intent_reference
154
+ raise RuntimeUnavailableError, "Codex dedicated installation selection differs"
155
+ end
156
+ verify!(manager: manager)
157
+ end
158
+
159
+ def self.for_codex_runtime(service:, intent_reference:, original_mapping:, mapping_id:, authority:, files: Files.new)
160
+ require_relative "codex_runtime_installation"
161
+ selected = allocate
162
+ selected.send(:initialize_codex_runtime, service, intent_reference, original_mapping, mapping_id, authority, files)
163
+ selected
164
+ end
165
+
166
+ # Descriptor shape only; no effective unit observation or role grant.
167
+ def self.validate_inbox_context_service!(service)
168
+ selected = allocate
169
+ selected.send(:validate_context_service!, service)
170
+ true
171
+ end
172
+
173
+ def verify!(manager:)
174
+ slot = @scope.fetch("slot_id")
175
+ unless slot.is_a?(String) && SystemdScopeManager::UNIT.match?(slot)
176
+ raise RuntimeUnavailableError, "installed slot identity is invalid"
177
+ end
178
+ selected_service = @context || @codex_service
179
+ path = selected_service ? selected_service.fetch("unit_manifest").fetch("path") : "/etc/ace/execution-slots/#{slot}/unit-manifest.json"
180
+ bytes = @files.read(path, limit: 65_536)
181
+ unless Digest::SHA256.hexdigest(bytes) == @scope.fetch("unit_manifest_sha256") &&
182
+ (!selected_service || bytes.bytesize == selected_service.fetch("unit_manifest").fetch("bytes"))
183
+ raise RuntimeUnavailableError, "unit manifest bytes differ from deployment"
184
+ end
185
+ manifest = JSON.parse(bytes)
186
+ unless manifest.is_a?(Hash) && manifest.keys.sort == %w[artifacts properties schema slot_id] &&
187
+ manifest["schema"] == SCHEMA && manifest["slot_id"] == slot
188
+ raise RuntimeUnavailableError, "unit manifest schema/slot differs"
189
+ end
190
+ artifacts = verify_artifacts!(manifest.fetch("artifacts"))
191
+ profile = @context ? manager.inspect_inbox_context_profile : manager.inspect_profile
192
+ verify_profile!(profile, manifest.fetch("properties"), artifacts)
193
+ manifest
194
+ rescue SystemCallError, IOError, JSON::ParserError, KeyError, TypeError, NoMethodError
195
+ raise RuntimeUnavailableError, "installed execution units cannot be verified"
196
+ end
197
+
198
+ # Source validity only. The caller owns selected staged bytes; this
199
+ # neither observes a live authority nor grants installed readiness.
200
+ def validate_staged_boundary_topology!(service:, artifacts:, authority:)
201
+ validate_boundary_topology!(service, artifacts, authority: authority)
202
+ true
203
+ end
204
+
205
+ def inbox_context_scope
206
+ raise RuntimeUnavailableError, "installation is not a context service" unless @context
207
+ @scope
208
+ end
209
+
210
+ def inbox_context_configuration_reference
211
+ raise RuntimeUnavailableError, "installation is not a context service" unless @context
212
+ @context_configuration
213
+ end
214
+
215
+ def inbox_context_service
216
+ raise RuntimeUnavailableError, "installation is not a context service" unless @context
217
+ @context
218
+ end
219
+
220
+ # Called from the held context configuration before service admission.
221
+ # Native startup bytes and IPC paths must be actual members of this
222
+ # SAME complete installed profile, not merely readable host selections.
223
+ def verify_inbox_native_projection!(references:, resources:, manager:)
224
+ raise RuntimeUnavailableError, "installation is not a context service" unless @context
225
+ manifest = verify!(manager: manager)
226
+ artifacts = manifest.fetch("artifacts")
227
+ profile = manager.inspect_inbox_context_profile.fetch("service")
228
+ unless references.is_a?(Array) && references.size.between?(3, 512) &&
229
+ resources.is_a?(Array) && resources.size <= 64
230
+ raise RuntimeUnavailableError, "context native projection exceeds bounds"
231
+ end
232
+ references.each do |ref|
233
+ unless ref.is_a?(Hash) && ref.keys.sort == %w[bytes path sha256] &&
234
+ ref["bytes"].is_a?(Integer) && ref["bytes"].between?(1, 268_435_456) &&
235
+ artifacts.any? { |artifact| artifact.values_at("host_path", "view_path", "sha256", "role") ==
236
+ [ref.fetch("path"), ref.fetch("path"), ref.fetch("sha256"), "runtime_dependency"] }
237
+ raise RuntimeUnavailableError, "context native startup reference is not exactly projected"
238
+ end
239
+ bytes = @files.read(ref.fetch("path"), limit: ref.fetch("bytes"))
240
+ unless bytes.bytesize == ref.fetch("bytes") && Digest::SHA256.hexdigest(bytes) == ref.fetch("sha256")
241
+ raise RuntimeUnavailableError, "context native startup reference differs"
242
+ end
243
+ end
244
+ boundary = JSON.parse(@files.read(@context.fetch("boundary_manifest").fetch("path"), limit: 65_536))
245
+ resources.each do |resource|
246
+ unless resource.is_a?(Hash) && resource.keys.sort == %w[access gid kind mode path uid] &&
247
+ %w[read write].include?(resource["access"]) && path?(resource["path"])
248
+ raise RuntimeUnavailableError, "context native resource fields differ"
249
+ end
250
+ path = resource.fetch("path")
251
+ readonly = resource.fetch("access") == "read"
252
+ mounts = profile.fetch(readonly ? "BindReadOnlyPaths" : "BindPaths")
253
+ unless mounts.include?([path, path, false, 0]) &&
254
+ boundary.fetch("resources").any? { |entry| entry.values_at("host_path", "view_path", "stage", "worker_visible", "read_only") ==
255
+ [path, path, "parent", true, readonly] } &&
256
+ (!readonly || profile.fetch("BindPaths").none? { |mount| overlaps?(path, mount[1]) } &&
257
+ profile.fetch("ReadWritePaths").none? { |root| overlaps?(path, root) })
258
+ raise RuntimeUnavailableError, "context native resource is not exactly projected"
259
+ end
260
+ end
261
+ verify!(manager: manager)
262
+ true
263
+ rescue SystemCallError, IOError, JSON::ParserError, KeyError, TypeError, NoMethodError
264
+ raise RuntimeUnavailableError, "context native projection is unavailable"
265
+ end
266
+
267
+ private
268
+
269
+ def initialize_context(service, configuration, load_paths, authority, credentials, files)
270
+ validate_context_service!(service)
271
+ refs = service.slice("unit_manifest", "boundary_manifest", "entry", "interpreter", "bootstrap_manifest").merge("configuration" => configuration)
272
+ unless refs.values.all? { |ref| ref.is_a?(Hash) && ref.keys.sort == %w[bytes path sha256] && path?(ref["path"]) &&
273
+ ref["bytes"].is_a?(Integer) && ref["bytes"].between?(1, 268_435_456) &&
274
+ ref["sha256"].is_a?(String) && ref["sha256"].match?(/\A[0-9a-f]{64}\z/) } &&
275
+ load_paths.all? { |path| path?(path) }
276
+ raise RuntimeUnavailableError, "context installation artifacts differ"
277
+ end
278
+ @scope = immutable_selection(service.fetch("execution_scope"))
279
+ @context = immutable_selection(service)
280
+ @context_configuration, @context_authority = immutable_selection(configuration), immutable_selection(authority)
281
+ @context_load_paths, @context_groups = immutable_selection(load_paths), immutable_selection(credentials.fetch("groups"))
282
+ @worker_uid, @worker_gid, @files = credentials.fetch("uid"), credentials.fetch("gid"), files
283
+ rescue KeyError, TypeError
284
+ raise RuntimeUnavailableError, "context installation selection is incomplete"
285
+ end
286
+
287
+ def validate_context_service!(service)
288
+ unless service.is_a?(Hash) && service.keys.sort == %w[bootstrap_manifest boundary_manifest entry execution_scope interpreter unit_manifest] &&
289
+ service.reject { |key, _| key == "execution_scope" }.values.all? { |ref|
290
+ ref.is_a?(Hash) && ref.keys.sort == %w[bytes path sha256] && path?(ref["path"]) &&
291
+ ref["bytes"].is_a?(Integer) && ref["bytes"].between?(1, 268_435_456) &&
292
+ ref["sha256"].is_a?(String) && ref["sha256"].match?(/\A[0-9a-f]{64}\z/)
293
+ }
294
+ raise RuntimeUnavailableError, "context service descriptor differs"
295
+ end
296
+ scope = service.fetch("execution_scope")
297
+ unless scope.is_a?(Hash) && scope.keys.sort == %w[backend boundary_manifest_sha256 network_namespace_path root_directory runtime_directory service_unit slice_unit slot_id unit_manifest_sha256] &&
298
+ scope["backend"] == "linux_systemd_cgroup_v2" && scope["slot_id"].is_a?(String) && SystemdScopeManager::UNIT.match?(scope["slot_id"]) &&
299
+ %w[root_directory runtime_directory network_namespace_path].all? { |key| path?(scope[key]) } &&
300
+ scope.fetch("runtime_directory").start_with?("/run/") && !overlaps?(scope.fetch("root_directory"), scope.fetch("runtime_directory")) &&
301
+ {"slice_unit" => ".slice", "service_unit" => ".service"}.all? { |key, suffix|
302
+ scope[key].is_a?(String) && SystemdScopeManager::UNIT.match?(scope[key]) && scope[key].end_with?(suffix) && !scope[key].downcase.include?("overseer")
303
+ } &&
304
+ scope["unit_manifest_sha256"] == service.fetch("unit_manifest").fetch("sha256") &&
305
+ scope["boundary_manifest_sha256"] == service.fetch("boundary_manifest").fetch("sha256") &&
306
+ service.fetch("unit_manifest").fetch("bytes") <= 65_536 && service.fetch("boundary_manifest").fetch("bytes") <= 65_536
307
+ raise RuntimeUnavailableError, "context service scope differs"
308
+ end
309
+ rescue KeyError, TypeError, NoMethodError
310
+ raise RuntimeUnavailableError, "context service descriptor is incomplete"
311
+ end
312
+
313
+ def immutable_selection(value)
314
+ case value
315
+ when Hash then value.to_h { |key, item| [key.dup.freeze, immutable_selection(item)] }.freeze
316
+ when Array then value.map { |item| immutable_selection(item) }.freeze
317
+ when String then value.dup.freeze
318
+ else value.freeze
319
+ end
320
+ end
321
+
322
+ def verify_artifacts!(artifacts)
323
+ minimum = @context ? CONTEXT_REQUIRED_ROLES.size : @codex_service ? CODEX_REQUIRED_ROLES.size : 9
324
+ unless artifacts.is_a?(Array) && artifacts.size.between?(minimum, 512) &&
325
+ artifacts.all? { |a| a.is_a?(Hash) && a.keys.sort == %w[host_path role sha256 view_path] } &&
326
+ artifacts.map { |a| a["host_path"] }.uniq.size == artifacts.size
327
+ raise RuntimeUnavailableError, "installed unit artifact declarations differ"
328
+ end
329
+ roles = artifacts.group_by { |a| a.fetch("role") }
330
+ required, allowed = if @context
331
+ [CONTEXT_REQUIRED_ROLES, CONTEXT_ROLES]
332
+ elsif @codex_service
333
+ [CODEX_REQUIRED_ROLES, CODEX_ROLES]
334
+ else
335
+ [REQUIRED_ROLES, ROLES]
336
+ end
337
+ unless required.all? { |role| roles.key?(role) } && (roles.keys - allowed).empty? &&
338
+ required.all? { |role| roles[role].size == 1 }
339
+ raise RuntimeUnavailableError, "installed unit artifacts are incomplete or ambiguous"
340
+ end
341
+ artifacts.each do |artifact|
342
+ path, digest = artifact.values_at("host_path", "sha256")
343
+ unless path?(path) && path?(artifact["view_path"]) && digest.is_a?(String) && digest.match?(/\A[0-9a-f]{64}\z/) && @files.sha256(path) == digest
344
+ raise RuntimeUnavailableError, "installed unit artifact identity changed"
345
+ end
346
+ end
347
+ return verify_context_artifacts!(roles) if @context
348
+ return verify_codex_artifacts!(roles) if @codex_service
349
+ expected = {"native_executable" => @native.fetch("executable"), "bootstrap" => @bootstrap,
350
+ "worker_executable" => @worker_entry.fetch("wrapper").fetch("path"),
351
+ "readiness_configuration" => "/etc/ace/execution-slots/#{@scope.fetch('slot_id')}/readiness.json",
352
+ "boundary_manifest" => "/etc/ace/execution-slots/#{@scope.fetch('slot_id')}/boundary-manifest.json"}
353
+ unless expected.all? { |role, path| roles.fetch(role).first.fetch("view_path") == path } &&
354
+ roles.fetch("native_executable").first.fetch("sha256") == @native.fetch("executable_sha256") &&
355
+ roles.fetch("boundary_manifest").first.fetch("sha256") == @scope.fetch("boundary_manifest_sha256")
356
+ raise RuntimeUnavailableError, "installed executable declarations differ from deployment"
357
+ end
358
+ {"wrapper" => "worker_executable", "interpreter" => "runtime_dependency"}.each do |key, role|
359
+ reference = @worker_entry.fetch(key)
360
+ matches = roles.fetch(role).select { |artifact| artifact.fetch("view_path") == reference.fetch("path") }
361
+ unless matches.size == 1 && matches.first.fetch("sha256") == reference.fetch("sha256")
362
+ raise RuntimeUnavailableError, "installed worker entry role differs"
363
+ end
364
+ bytes = @files.read(matches.first.fetch("host_path"), limit: reference.fetch("bytes"))
365
+ unless bytes.bytesize == reference.fetch("bytes") && Digest::SHA256.hexdigest(bytes) == reference.fetch("sha256")
366
+ raise RuntimeUnavailableError, "installed worker entry bytes differ"
367
+ end
368
+ end
369
+ roles
370
+ end
371
+
372
+ def verify_context_artifacts!(roles)
373
+ refs = {"context_executable" => @context.fetch("entry"), "interpreter" => @context.fetch("interpreter"),
374
+ "bootstrap" => @context.fetch("bootstrap_manifest"), "boundary_manifest" => @context.fetch("boundary_manifest"),
375
+ "context_configuration" => @context_configuration}
376
+ refs.each do |role, ref|
377
+ artifact = roles.fetch(role).first
378
+ bytes = @files.read(artifact.fetch("host_path"), limit: ref.fetch("bytes"))
379
+ unless artifact.fetch("host_path") == ref.fetch("path") && artifact.fetch("sha256") == ref.fetch("sha256") &&
380
+ (!%w[bootstrap boundary_manifest context_configuration].include?(role) || artifact.fetch("view_path") == ref.fetch("path")) &&
381
+ bytes.bytesize == ref.fetch("bytes") && Digest::SHA256.hexdigest(bytes) == ref.fetch("sha256")
382
+ raise RuntimeUnavailableError, "context host artifact differs from selected immutable reference"
383
+ end
384
+ end
385
+ roles
386
+ end
387
+
388
+ def verify_profile!(profile, expected, artifacts)
389
+ unless expected.is_a?(Hash) && expected.keys.sort == %w[service slice]
390
+ raise RuntimeUnavailableError, "unit profile declarations differ"
391
+ end
392
+ environment = profile.fetch("manager_environment")
393
+ unless environment.is_a?(Array) && environment.all? { |entry| entry.is_a?(String) &&
394
+ MANAGER_ENVIRONMENT.include?(entry.split("=", 2).first) }
395
+ raise RuntimeUnavailableError, "effective manager environment changes the fixed startup surface"
396
+ end
397
+ {"slice" => SystemdScopeManager::UNIT_GRAPH_SIGNATURES.keys,
398
+ "service" => (SystemdScopeManager::UNIT_GRAPH_SIGNATURES.keys + SystemdScopeManager::SERVICE_EXEC_SIGNATURES.keys + (@context ? ["TimeoutStopUSec"] : [])).uniq}.each do |kind, keys|
399
+ wanted, actual = expected.fetch(kind), profile.fetch(kind)
400
+ projected = actual.dup
401
+ %w[ExecStartEx ExecStartPostEx].each do |key|
402
+ projected[key] = actual.fetch(key).map { |command| command.first(3) } if kind == "service"
403
+ end
404
+ unless wanted.is_a?(Hash) && wanted.keys.sort == keys.sort && projected == wanted &&
405
+ actual["Id"] == @scope.fetch("#{kind}_unit") && actual["Names"] == [actual["Id"]] &&
406
+ actual["LoadState"] == "loaded" && actual["NeedDaemonReload"] == false
407
+ raise RuntimeUnavailableError, "effective installed unit properties differ"
408
+ end
409
+ fragment = artifacts.fetch("#{kind}_fragment").first.fetch("host_path")
410
+ unless actual["FragmentPath"] == fragment && File.basename(fragment) == @scope.fetch("#{kind}_unit")
411
+ raise RuntimeUnavailableError, "system manager loaded another unit fragment"
412
+ end
413
+ unless actual.fetch("DropInPaths").is_a?(Array) && actual.fetch("DropInPaths").all? { |path|
414
+ artifacts.fetch("unit_dropin", []).any? { |artifact| artifact["host_path"] == path }
415
+ }
416
+ raise RuntimeUnavailableError, "system manager loaded an unverified drop-in"
417
+ end
418
+ end
419
+ slice, service = profile.values_at("slice", "service")
420
+ verify_graph!(slice, service, profile.fetch("ancestors"), profile.fetch("prerequisites"))
421
+ unless @files.activation_routes(profile.fetch("unit_paths"), @scope.fetch("service_unit")).empty?
422
+ raise RuntimeUnavailableError, "service has installed outside enablement or alias routes"
423
+ end
424
+ unless slice["StopWhenUnneeded"] == false && service["Sockets"] == [] &&
425
+ service["RootDirectory"] == @scope.fetch("root_directory") &&
426
+ service["NetworkNamespacePath"] == @scope.fetch("network_namespace_path") &&
427
+ credential_id(service.fetch("User"), :uid) == @worker_uid &&
428
+ credential_id(service.fetch("Group"), :gid) == @worker_gid &&
429
+ SERVICE_REQUIRED.all? { |key, value| service[key] == value } &&
430
+ service["SupplementaryGroups"] == (@context ? @context_groups.map(&:to_s) : @codex_service ? @codex_groups.map(&:to_s) : []) &&
431
+ service["RestrictAddressFamilies"] == [true, %w[AF_INET AF_INET6 AF_UNIX]] &&
432
+ EMPTY_EXEC.all? { |key| service[key] == [] }
433
+ raise RuntimeUnavailableError, "execution unit lacks the required retained isolation profile"
434
+ end
435
+ if @context
436
+ verify_context_protocol!(service, artifacts)
437
+ verify_artifact_projection!(service, artifacts)
438
+ verify_boundary_topology!(service, artifacts, authority: @context_authority)
439
+ return
440
+ end
441
+ if @codex_service
442
+ verify_codex_protocol!(service, artifacts)
443
+ verify_artifact_projection!(service, artifacts)
444
+ verify_boundary_topology!(service, artifacts, authority: @codex_authority)
445
+ return
446
+ end
447
+ verify_command!(service.fetch("ExecStartEx"), artifacts.fetch("native_executable").first.fetch("view_path"))
448
+ config_artifact = artifacts.fetch("readiness_configuration").first
449
+ config = ReadinessConfiguration.decode(@files.read(config_artifact.fetch("host_path"), limit: 65_536),
450
+ slot: @scope.fetch("slot_id"))
451
+ interpreter = config.data.dig("runtime", "interpreter_path")
452
+ unless artifacts.fetch("runtime_dependency", []).any? { |artifact| artifact.fetch("view_path") == interpreter }
453
+ raise RuntimeUnavailableError, "readiness interpreter is not installed"
454
+ end
455
+ verify_command!(service.fetch("ExecStartPostEx"), interpreter)
456
+ unless service.fetch("ExecStartEx").first[1] == [@native.fetch("executable"), "server"] &&
457
+ service.fetch("ExecStartPostEx").first[1] == [interpreter, "--disable=gems,rubyopt",
458
+ artifacts.fetch("readiness_executable").first.fetch("view_path"), @scope.fetch("slot_id")]
459
+ raise RuntimeUnavailableError, "unit commands differ from the fixed native/readiness protocol"
460
+ end
461
+ environment = service.fetch("Environment")
462
+ unless environment.is_a?(Array) && environment.all? { |entry| entry.is_a?(String) && !entry.include?("\0") } &&
463
+ environment.all? { |entry| key, value = entry.split("=", 2); NATIVE_ENVIRONMENT.include?(key) && value && !value.empty? } &&
464
+ environment.map { |entry| entry.split("=", 2).first }.uniq.size == environment.size &&
465
+ environment.include?("HERDR_CONFIG_PATH=#{artifacts.fetch('native_configuration').first.fetch('view_path')}")
466
+ raise RuntimeUnavailableError, "native configuration is not the exact immutable input"
467
+ end
468
+ verify_artifact_projection!(service, artifacts)
469
+ verify_boundary_topology!(service, artifacts, authority: config.data.fetch("authority"))
470
+ end
471
+
472
+ def verify_context_protocol!(service, artifacts)
473
+ interpreter = artifacts.fetch("interpreter").first.fetch("view_path")
474
+ entry = artifacts.fetch("context_executable").first.fetch("view_path")
475
+ verify_command!(service.fetch("ExecStartEx"), interpreter)
476
+ arguments = [interpreter, "--disable=gems,rubyopt", *@context_load_paths.flat_map { |path| ["-I", path] }, entry]
477
+ unless service.fetch("ExecStartEx").first[1] == arguments && service.fetch("ExecStartPostEx") == [] &&
478
+ service.fetch("Environment") == [] && service.fetch("TimeoutStopUSec") == 35_000_000 &&
479
+ @context_load_paths.all? { |path| artifacts.fetch("runtime_dependency", []).any? { |artifact| covers?(path, artifact.fetch("view_path")) } }
480
+ raise RuntimeUnavailableError, "context commands differ from the fixed service protocol"
481
+ end
482
+ end
483
+
484
+ def verify_graph!(slice, service, ancestors, prerequisites)
485
+ unless ancestors.is_a?(Hash) && !ancestors.empty?
486
+ raise RuntimeUnavailableError, "parent activation graph is incomplete"
487
+ end
488
+ parents = [slice, *ancestors.values]
489
+ parents.each_with_index do |unit, index|
490
+ parent = parents[index + 1]
491
+ permitted = parent ? [parent.fetch("Id")] : []
492
+ unless unit["LoadState"] == "loaded" && unit["NeedDaemonReload"] == false &&
493
+ unit["Requires"] == permitted && unit["Wants"] == [] &&
494
+ (EMPTY_ACTIVATION + %w[RequiresMountsFor WantsMountsFor]).all? { |key| unit[key] == [] }
495
+ raise RuntimeUnavailableError, "parent activation graph could start outside its retained hierarchy"
496
+ end
497
+ end
498
+ permitted_paths = [@scope.fetch("root_directory"), *service.fetch("RuntimeDirectory").map { |path| "/run/#{path}" }]
499
+ unless prerequisites.is_a?(Hash) && prerequisites.all? { |unit, value|
500
+ value["Id"] == unit && value["LoadState"] == "loaded" && value["ActiveState"] == "active" && value["Job"] == [0, "/"] &&
501
+ (unit == "systemd-journald.socket" && value["SubState"] == "listening" ||
502
+ unit.end_with?(".mount") && value["SubState"] == "mounted" && (value["Where"] == "/" || path?(value["Where"])) &&
503
+ permitted_paths.any? { |path| covers?(value["Where"], path) })
504
+ } && (service.values_at("Requires", "Wants", "After").flatten.uniq - [slice["Id"]]).sort == prerequisites.keys.sort &&
505
+ service.values_at("Requires", "Wants").flatten.none? { |unit| unit == "systemd-journald.socket" }
506
+ raise RuntimeUnavailableError, "implicit backing mounts/logging are not active exact prerequisites"
507
+ end
508
+ unless parents.last["Id"] == "-.slice" && service["Slice"] == slice["Id"] &&
509
+ service["Requires"].include?(slice["Id"]) && service["After"].include?(slice["Id"]) &&
510
+ service["WantsMountsFor"] == [@scope.fetch("root_directory")] &&
511
+ service.fetch("RuntimeDirectory") == [@scope.fetch("runtime_directory").delete_prefix("/run/")] &&
512
+ service["RequiresMountsFor"].sort == service.fetch("RuntimeDirectory").map { |path| "/run/#{path}" }.sort &&
513
+ service["WorkingDirectory"] == (@codex_service ? @codex_intent.fetch("cwd") : "") && service["DefaultDependencies"] == false &&
514
+ %w[RequiredBy RequisiteOf WantedBy BoundBy UpheldBy ConsistsOf].all? { |key| service[key] == [] } &&
515
+ %w[disabled static].include?(service["UnitFileState"]) &&
516
+ EMPTY_ACTIVATION.all? { |key| service[key] == [] }
517
+ raise RuntimeUnavailableError, "service has outside activation or parent stop propagation"
518
+ end
519
+ end
520
+
521
+ def verify_artifact_projection!(service, artifacts)
522
+ writable = service.fetch("BindPaths")
523
+ readonly = service.fetch("BindReadOnlyPaths")
524
+ mounts = writable + readonly
525
+ unless mounts.all? { |mount| path?(mount[0]) && path?(mount[1]) && mount[2] == false && mount[3] == 0 } &&
526
+ mounts.map { |mount| mount[1] }.uniq.size == mounts.size
527
+ raise RuntimeUnavailableError, "unit mount projection is ambiguous or optional"
528
+ end
529
+ artifacts.each do |role, entries|
530
+ next if %w[slice_fragment service_fragment unit_dropin].include?(role)
531
+ entries.each do |artifact|
532
+ view = artifact.fetch("view_path")
533
+ if IMPLICIT_NAMESPACE_ROOTS.any? { |root| covers?(root, view) } ||
534
+ service.fetch("ReadWritePaths").any? { |root| !path?(root) || covers?(root, view) }
535
+ raise RuntimeUnavailableError, "immutable artifact is inside an implicit or writable namespace projection"
536
+ end
537
+ if writable.any? { |mount| overlaps?(view, mount[1]) }
538
+ raise RuntimeUnavailableError, "immutable unit artifact has a writable overlay"
539
+ end
540
+ selected = readonly.select { |mount| covers?(mount[1], view) }.max_by { |mount| mount[1].length }
541
+ relative = selected && view.delete_prefix(selected[1]).delete_prefix("/")
542
+ host = selected ? (relative.empty? ? selected[0] : File.join(selected[0], relative)) : @scope.fetch("root_directory") + view
543
+ unless artifact.fetch("host_path") == host
544
+ raise RuntimeUnavailableError, "hashed artifact is shadowed by another effective unit mount"
545
+ end
546
+ end
547
+ end
548
+ end
549
+
550
+ def verify_boundary_topology!(service, artifacts, authority:)
551
+ validate_staged_boundary_topology!(service: service, artifacts: artifacts, authority: authority)
552
+ @files.authority_socket!(authority)
553
+ end
554
+
555
+ def validate_boundary_topology!(service, artifacts, authority:)
556
+ artifact = artifacts.fetch("boundary_manifest").first
557
+ bytes = @files.read(artifact.fetch("host_path"), limit: 65_536).dup.force_encoding(Encoding::UTF_8)
558
+ raise RuntimeUnavailableError, "boundary content is not UTF-8" unless bytes.valid_encoding?
559
+ boundary = JSON.parse(bytes, create_additions: false, max_nesting: 8, allow_duplicate_key: false, allow_comments: false)
560
+ unless boundary.is_a?(Hash) && boundary.keys.sort == %w[network_installation resources schema slot_id] &&
561
+ boundary["schema"] == "ace.execution-boundary-manifest/v1" && boundary["slot_id"] == @scope.fetch("slot_id") &&
562
+ boundary["resources"].is_a?(Array) && boundary["resources"].size.between?(1, 64)
563
+ raise RuntimeUnavailableError, "boundary lifecycle topology differs"
564
+ end
565
+ entries = boundary.fetch("resources")
566
+ ExecutionNetworkSelection.validate_static!(boundary.fetch("network_installation"), slot_id: @scope.fetch("slot_id"))
567
+ entries.each do |entry|
568
+ unless entry.is_a?(Hash) && entry.keys.sort == %w[host_path read_only stage view_path worker_visible] &&
569
+ path?(entry["host_path"]) && path?(entry["view_path"]) && %w[parent native].include?(entry["stage"]) &&
570
+ [true, false].include?(entry["worker_visible"]) && [true, false].include?(entry["read_only"]) &&
571
+ (entry["worker_visible"] || entry["stage"] == "parent" && entry["read_only"])
572
+ raise RuntimeUnavailableError, "boundary resource declaration differs"
573
+ end
574
+ end
575
+ unless entries.map { |entry| entry.values_at("host_path", "view_path") }.uniq.size == entries.size
576
+ raise RuntimeUnavailableError, "boundary resource projections repeat"
577
+ end
578
+ projections = service.fetch("BindPaths").map { |mount| [mount[0], mount[1], false] } +
579
+ service.fetch("BindReadOnlyPaths").map { |mount| [mount[0], mount[1], true] }
580
+ socket = authority.fetch("socket_path")
581
+ socket_projection = [socket, socket, true]
582
+ socket_mounts = projections.select { |host, view, _| overlaps?(host, socket) || overlaps?(view, socket) }
583
+ unless socket_mounts == [socket_projection]
584
+ raise RuntimeUnavailableError, "authority socket has no unique exact readonly projection"
585
+ end
586
+ devpts = "/run/ace/execution-slots/#{@scope.fetch('slot_id')}/devpts"
587
+ device_projections = @context ? [] : [[devpts, "/dev/pts", false], [devpts + "/ptmx", "/dev/pts/ptmx", true]]
588
+ unless projections.select { |_host, view, _| overlaps?(view, "/dev/pts") } == device_projections
589
+ raise RuntimeUnavailableError, "selected devpts has no exact root and ptmx projection"
590
+ end
591
+ api_roots = %w[/dev /proc /sys]
592
+ if (projections - device_projections).any? { |_host, view, _| api_roots.any? { |root| overlaps?(root, view) } }
593
+ raise RuntimeUnavailableError, "API namespace has an unsupported storage alias"
594
+ end
595
+ readonly_paths = service.fetch("ReadOnlyPaths")
596
+ unless readonly_paths.all? { |path| path?(path) } &&
597
+ %w[/dev /dev/shm].all? { |view| readonly_paths.any? { |path| covers?(path, view) } } &&
598
+ !service.fetch("ReadWritePaths").any? { |path| !path?(path) || overlaps?(path, "/dev/shm") }
599
+ raise RuntimeUnavailableError, "file-backed IPC is not positively read-only"
600
+ end
601
+ runtime = @scope.fetch("runtime_directory")
602
+ projections << [runtime, runtime, false]
603
+ service.fetch("ReadWritePaths").each do |view|
604
+ unless path?(view) && api_roots.none? { |root| overlaps?(root, view) }
605
+ raise RuntimeUnavailableError, "writable path directive is not exact"
606
+ end
607
+ projection = projections.select { |mount| covers?(mount[1], view) }.max_by { |mount| mount[1].length }
608
+ host = projection ? File.join(projection[0], view.delete_prefix(projection[1]).delete_prefix("/")) :
609
+ @scope.fetch("root_directory") + view
610
+ projections << [host, view, false]
611
+ end
612
+ unless projections.select { |host, view, _| overlaps?(host, socket) || overlaps?(view, socket) } == [socket_projection]
613
+ raise RuntimeUnavailableError, "authority socket is shadowed by an effective writable or parent projection"
614
+ end
615
+ projections.each do |host, view, readonly|
616
+ next if [host, view, readonly] == socket_projection
617
+ next if device_projections.include?([host, view, readonly])
618
+ next if readonly && artifacts.values.flatten.any? { |entry| entry["host_path"] == host && entry["view_path"] == view }
619
+ unless entries.any? { |entry| entry.values_at("host_path", "view_path", "worker_visible", "read_only") == [host, view, true, readonly] }
620
+ raise RuntimeUnavailableError, "effective projection is omitted from boundary inventory"
621
+ end
622
+ end
623
+ entries.each do |entry|
624
+ view = entry.fetch("view_path")
625
+ accessible = projections.any? { |host, target, _readonly| covers?(target, view) &&
626
+ File.expand_path(File.join(host, view.delete_prefix(target).delete_prefix("/"))) == entry.fetch("host_path") } ||
627
+ entry.fetch("host_path") == @scope.fetch("root_directory") + view
628
+ if entry.fetch("worker_visible") != accessible
629
+ raise RuntimeUnavailableError, "boundary visibility is not proven by installed topology"
630
+ end
631
+ if entry.fetch("stage") == "native" && !covers?(runtime, entry.fetch("host_path"))
632
+ raise RuntimeUnavailableError, "service-created resource has no fixed creation directive"
633
+ end
634
+ end
635
+ end
636
+
637
+ def covers?(root, path) = root == path || root == "/" || path.start_with?(root + "/")
638
+ def overlaps?(left, right) = covers?(left, right) || covers?(right, left)
639
+
640
+ def verify_command!(value, executable)
641
+ unless value.is_a?(Array) && value.size == 1 && value.first.is_a?(Array) && value.first.size == 10 &&
642
+ value.first[0] == executable && value.first[1].is_a?(Array) && value.first[1].first == executable &&
643
+ value.first[1].all? { |arg| arg.is_a?(String) && !arg.include?("\0") } && value.first[2] == [] &&
644
+ value.first[3..].all? { |number| number.is_a?(Integer) }
645
+ raise RuntimeUnavailableError, "unit command bypasses fixed unprivileged execution"
646
+ end
647
+ end
648
+
649
+ def credential_id(value, kind)
650
+ return Integer(value, 10) if value.match?(/\A[1-9][0-9]*\z/)
651
+ kind == :uid ? Etc.getpwnam(value).uid : Etc.getgrnam(value).gid
652
+ rescue ArgumentError
653
+ raise RuntimeUnavailableError, "installed unit principal is unavailable"
654
+ end
655
+
656
+ def path?(value)
657
+ value.is_a?(String) && value.bytesize.between?(2, 4096) && value.start_with?("/") &&
658
+ !value.match?(/[\s\0;]/) && File.expand_path(value) == value
659
+ end
660
+ end
661
+ end
662
+ end
663
+ end