ace-lab 0.9.0 → 0.13.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 (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +363 -104
  3. data/README.md +66 -100
  4. data/docs/usage.md +529 -592
  5. data/lib/ace/lab/atoms/install_schema.rb +373 -0
  6. data/lib/ace/lab/cli/commands/install.rb +161 -0
  7. data/lib/ace/lab/cli/commands/prepare.rb +118 -0
  8. data/lib/ace/lab/cli/commands/project_add.rb +101 -0
  9. data/lib/ace/lab/cli/commands/status.rb +109 -0
  10. data/lib/ace/lab/cli/commands/verify.rb +115 -0
  11. data/lib/ace/lab/cli.rb +21 -23
  12. data/lib/ace/lab/install_manifest_path.rb +23 -0
  13. data/lib/ace/lab/models/status_report.rb +38 -0
  14. data/lib/ace/lab/molecules/account_provisioner.rb +196 -0
  15. data/lib/ace/lab/molecules/config_state_classifier.rb +145 -0
  16. data/lib/ace/lab/molecules/directory_provisioner.rb +208 -0
  17. data/lib/ace/lab/molecules/grant_provisioner.rb +250 -0
  18. data/lib/ace/lab/molecules/grant_resolver.rb +34 -23
  19. data/lib/ace/lab/molecules/hitl_routes.rb +263 -0
  20. data/lib/ace/lab/molecules/install_manifest_loader.rb +64 -0
  21. data/lib/ace/lab/molecules/key_provisioner.rb +736 -0
  22. data/lib/ace/lab/molecules/lab_state_scanner.rb +55 -0
  23. data/lib/ace/lab/molecules/overseer_provisioner.rb +188 -0
  24. data/lib/ace/lab/molecules/project_registry.rb +251 -0
  25. data/lib/ace/lab/molecules/repository_provisioner.rb +141 -0
  26. data/lib/ace/lab/molecules/role_verifier.rb +223 -0
  27. data/lib/ace/lab/organisms/install.rb +213 -0
  28. data/lib/ace/lab/organisms/prepare.rb +156 -0
  29. data/lib/ace/lab/organisms/project_add.rb +170 -0
  30. data/lib/ace/lab/organisms/status_service.rb +70 -0
  31. data/lib/ace/lab/organisms/verify.rb +768 -0
  32. data/lib/ace/lab/version.rb +1 -1
  33. data/lib/ace/lab.rb +34 -6
  34. metadata +34 -108
  35. data/lib/ace/lab/atoms/protected_publication_input.rb +0 -50
  36. data/lib/ace/lab/atoms/protected_workspace_prune_input.rb +0 -114
  37. data/lib/ace/lab/atoms/protected_workspace_prune_preview.rb +0 -86
  38. data/lib/ace/lab/atoms/service_input.rb +0 -92
  39. data/lib/ace/lab/cli/commands/assignment.rb +0 -92
  40. data/lib/ace/lab/cli/commands/command_execute.rb +0 -28
  41. data/lib/ace/lab/cli/commands/run.rb +0 -65
  42. data/lib/ace/lab/cli/commands/service.rb +0 -240
  43. data/lib/ace/lab/models/service_receipt.rb +0 -18
  44. data/lib/ace/lab/molecules/command_owner_admission.rb +0 -183
  45. data/lib/ace/lab/molecules/command_policy.rb +0 -158
  46. data/lib/ace/lab/molecules/hitl_authorizer.rb +0 -63
  47. data/lib/ace/lab/molecules/ordinary_admission_resolver.rb +0 -131
  48. data/lib/ace/lab/molecules/ordinary_listener.rb +0 -262
  49. data/lib/ace/lab/molecules/ordinary_merge_read.rb +0 -45
  50. data/lib/ace/lab/molecules/ordinary_native_read.rb +0 -106
  51. data/lib/ace/lab/molecules/ordinary_owner_admission.rb +0 -152
  52. data/lib/ace/lab/molecules/ordinary_owner_config.rb +0 -232
  53. data/lib/ace/lab/molecules/ordinary_receiver_config.rb +0 -94
  54. data/lib/ace/lab/molecules/ordinary_socket.rb +0 -126
  55. data/lib/ace/lab/molecules/ordinary_worktree_read.rb +0 -216
  56. data/lib/ace/lab/molecules/protected_cleanup_owner_admission.rb +0 -163
  57. data/lib/ace/lab/molecules/protected_cleanup_owner_client.rb +0 -165
  58. data/lib/ace/lab/molecules/protected_cleanup_owner_identity.rb +0 -239
  59. data/lib/ace/lab/molecules/protected_publication_publisher.rb +0 -110
  60. data/lib/ace/lab/molecules/protected_service_handler.rb +0 -43
  61. data/lib/ace/lab/molecules/protected_service_policy.rb +0 -156
  62. data/lib/ace/lab/molecules/service_executor.rb +0 -99
  63. data/lib/ace/lab/molecules/service_policy.rb +0 -289
  64. data/lib/ace/lab/organisms/authority_composition.rb +0 -87
  65. data/lib/ace/lab/organisms/command_executor.rb +0 -220
  66. data/lib/ace/lab/organisms/command_run_client.rb +0 -131
  67. data/lib/ace/lab/organisms/ordinary_assignment_client.rb +0 -120
  68. data/lib/ace/lab/organisms/ordinary_authority_composition.rb +0 -153
  69. data/lib/ace/lab/organisms/ordinary_merge_receiver.rb +0 -679
  70. data/lib/ace/lab/organisms/ordinary_merge_request.rb +0 -456
  71. data/lib/ace/lab/organisms/protected_cleanup_owner.rb +0 -361
  72. data/lib/ace/lab/organisms/protected_service_client.rb +0 -129
  73. data/lib/ace/lab/organisms/protected_service_listener.rb +0 -181
  74. data/lib/ace/lab/organisms/protected_service_publication.rb +0 -268
  75. data/lib/ace/lab/organisms/protected_service_receiver.rb +0 -267
  76. data/lib/ace/lab/organisms/protected_service_recovery.rb +0 -185
  77. data/lib/ace/lab/organisms/protected_service_request.rb +0 -128
  78. data/lib/ace/lab/organisms/protected_service_worker.rb +0 -89
  79. data/lib/ace/lab/organisms/service_request_service.rb +0 -249
@@ -1,126 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
- require "socket"
5
-
6
- module Ace
7
- module Lab
8
- module Molecules
9
- # Admission transport between the ordinary fixed merge producer and the
10
- # installed ordinary receiver: one bounded JSON document each way over
11
- # a configured unix stream socket with a fixed deadline. Authentication
12
- # is mutual through OS peer credentials — the producer verifies the receiver's
13
- # configured UID and transmits the verified local caller identity, so
14
- # no caller-selected credential, token or endpoint exists. This is not
15
- # an HTTP transport; a lost or unauthenticated response is a
16
- # caller-side blocker (ordinary_merge_unavailable), never a refusal.
17
- class OrdinarySocket
18
- LIMIT = 16_384
19
- HANDSHAKE = "ace-lab/ordinary-merge/v1"
20
-
21
- # @param socket_path [String] installed receiver endpoint (configuration)
22
- # @param receiver_uid [Integer] the only UID the receiver may run as
23
- # @param deadline [Time] absolute deadline for connect/write/read
24
- def initialize(socket_path:, receiver_uid:, deadline: Time.now.utc + 30)
25
- @socket_path = socket_path
26
- @receiver_uid = receiver_uid
27
- @deadline = deadline
28
- end
29
-
30
- # Submit one bounded request document; returns the parsed response
31
- # document. Any framing, deadline or identity failure raises
32
- # OrdinaryUnavailable so the producer can classify the blocker.
33
- def submit(document)
34
- payload = JSON.generate(document)
35
- raise OrdinaryUnavailable, "request document exceeds the wire bound" if payload.bytesize > LIMIT
36
- socket = nil
37
- begin
38
- socket = UNIXSocket.new(@socket_path)
39
- verify_peer!(socket)
40
- write_all!(socket, "#{HANDSHAKE} #{payload.bytesize}\n#{payload}")
41
- response = read_response!(socket)
42
- rescue OrdinaryUnavailable
43
- raise
44
- rescue SystemCallError, IOError, SocketError, NotImplementedError => e
45
- raise OrdinaryUnavailable, "receiver transport failed: #{e.class}"
46
- ensure
47
- begin
48
- socket&.close
49
- rescue IOError
50
- nil
51
- end
52
- end
53
- response
54
- end
55
-
56
- class OrdinaryUnavailable < ::StandardError; end
57
-
58
- private
59
-
60
- def verify_peer!(socket)
61
- peer_uid, = socket.getpeereid
62
- return if peer_uid == @receiver_uid
63
- raise OrdinaryUnavailable, "receiver endpoint identity is not the installed receiver"
64
- end
65
-
66
- def write_all!(socket, bytes)
67
- written = 0
68
- while written < bytes.bytesize
69
- begin
70
- raise OrdinaryUnavailable, "receiver write deadline expired" if expired?
71
- written += socket.write_nonblock(bytes.byteslice(written..))
72
- rescue IO::WaitWritable
73
- socket.wait_writable(remaining!) or raise OrdinaryUnavailable, "receiver write deadline expired"
74
- end
75
- end
76
- end
77
-
78
- def read_response!(socket)
79
- header = read_line!(socket)
80
- match = /\A#{Regexp.escape(HANDSHAKE)} (\d{1,5})\n\z/o.match(header)
81
- raise OrdinaryUnavailable, "receiver response header is invalid" unless match
82
- length = Integer(match[1])
83
- raise OrdinaryUnavailable, "receiver response exceeds the wire bound" if length > LIMIT
84
- body = read_exact!(socket, length)
85
- JSON.parse(body, allow_duplicate_key: false, create_additions: false, max_nesting: 16)
86
- rescue JSON::ParserError
87
- raise OrdinaryUnavailable, "receiver response is not valid JSON"
88
- end
89
-
90
- def read_line!(socket)
91
- line = +""
92
- until line.end_with?("\n")
93
- raise OrdinaryUnavailable, "receiver response header is unbounded" if line.bytesize > 128
94
- line << read_exact!(socket, 1)
95
- end
96
- line
97
- end
98
-
99
- def read_exact!(socket, length)
100
- buffer = +""
101
- while buffer.bytesize < length
102
- begin
103
- raise OrdinaryUnavailable, "receiver read deadline expired" if expired?
104
- chunk = socket.read_nonblock(length - buffer.bytesize)
105
- raise OrdinaryUnavailable, "receiver closed the connection" if chunk.nil?
106
- buffer << chunk
107
- rescue IO::WaitReadable
108
- socket.wait_readable(remaining!) or raise OrdinaryUnavailable, "receiver read deadline expired"
109
- end
110
- end
111
- buffer
112
- end
113
-
114
- def expired?
115
- Time.now.utc >= @deadline
116
- end
117
-
118
- def remaining!
119
- left = @deadline - Time.now.utc
120
- raise OrdinaryUnavailable, "receiver deadline expired" if left <= 0
121
- left
122
- end
123
- end
124
- end
125
- end
126
- end
@@ -1,216 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "ace/assign/molecules/installed_journal_read"
4
-
5
- module Ace
6
- module Lab
7
- module Molecules
8
- # Observe a mutable ordinary worker checkout. The root-installed project
9
- # selection supplies both boundaries; caller input never selects a Git
10
- # repository. This is historical observation, not a checkout freeze.
11
- class OrdinaryWorktreeRead
12
- def initialize(root:, common_dir:, deadline:)
13
- @root, @common, @deadline = root, common_dir, deadline
14
- [root, common_dir].each { |path| absolute!(path) }
15
- end
16
-
17
- def observe(cwd)
18
- absolute!(cwd)
19
- remaining
20
- unless within?(cwd, @root)
21
- unavailable!("worker working directory is outside installed worktrees")
22
- end
23
- before = selection(cwd)
24
- head = git(before, "rev-parse", "--verify", "HEAD^{commit}").strip
25
- unavailable!("worker HEAD is unavailable") unless head.match?(/\A[0-9a-f]{40}\z/)
26
- after = selection(cwd)
27
- unless before == after && git(after, "rev-parse", "--verify", "HEAD^{commit}").strip == head
28
- unavailable!("worker checkout changed during observation")
29
- end
30
- {"cwd" => cwd, "worktree" => before.fetch("worktree"), "git_dir" => before.fetch("git_dir"),
31
- "common_dir" => @common, "head" => head}
32
- rescue SystemCallError, IOError, Timeout::Error, Ace::Herdr::Molecules::BoundedProcess::PostLaunchError
33
- unavailable!("worker checkout cannot be observed")
34
- end
35
-
36
- def ref_head(cwd, ref)
37
- unless ref.is_a?(String) && ref.match?(%r{\Arefs/[a-zA-Z0-9_./-]+\z}) && !ref.include?("..")
38
- unavailable!("selected Git ref is invalid")
39
- end
40
- with_selected_read(cwd) do |selected|
41
- head = git(selected, "rev-parse", "--verify", "#{ref}^{commit}").strip
42
- unavailable!("selected ref is not a commit") unless head.match?(/\A[0-9a-f]{40}\z/)
43
- head
44
- end
45
- end
46
-
47
- def commit?(cwd, head)
48
- exact_head!(head)
49
- with_selected_read(cwd) do |selected|
50
- git(selected, "rev-parse", "--verify", "--quiet", "#{head}^{commit}", allowed_status: [0, 1], result: true).status.success?
51
- end
52
- end
53
-
54
- def ancestor?(cwd, ancestor, descendant)
55
- exact_head!(ancestor); exact_head!(descendant)
56
- with_selected_read(cwd) do |selected|
57
- git(selected, "merge-base", "--is-ancestor", ancestor, descendant, allowed_status: [0, 1], result: true).status.success?
58
- end
59
- end
60
-
61
- private
62
-
63
- def exact_head!(head)
64
- unavailable!("selected Git head is invalid") unless head.is_a?(String) && head.match?(/\A[0-9a-f]{40}\z/)
65
- end
66
-
67
- def with_selected_read(cwd)
68
- absolute!(cwd)
69
- unavailable!("selected repository leaves installed worktrees") unless within?(cwd, @root)
70
- before = selection(cwd)
71
- value = yield before
72
- unavailable!("selected repository changed during read") unless before == selection(cwd)
73
- value
74
- rescue SystemCallError, IOError, Timeout::Error, Ace::Herdr::Molecules::BoundedProcess::PostLaunchError => error
75
- unavailable!("selected repository cannot be observed (#{error.class})")
76
- end
77
-
78
- def selection(cwd)
79
- directory!(cwd)
80
- directory!(@common)
81
- worktree = cwd
82
- loop do
83
- break if File.exist?(File.join(worktree, ".git")) || File.symlink?(File.join(worktree, ".git"))
84
- unavailable!("worker checkout is unavailable") if worktree == @root
85
- worktree = File.dirname(worktree)
86
- end
87
- marker = File.join(worktree, ".git")
88
- metadata = {}
89
- git_dir = if File.lstat(marker).directory?
90
- directory!(marker)
91
- unavailable!("worker belongs to another project") unless marker == @common
92
- marker
93
- else
94
- bytes = regular!(marker, metadata, 4096)
95
- match = bytes.match(/\Agitdir: ([^\n\0]+)\n?\z/)
96
- unavailable!("worker Git pointer is invalid") unless match
97
- selected = File.expand_path(match[1], worktree)
98
- unless File.dirname(selected) == File.join(@common, "worktrees")
99
- unavailable!("worker Git pointer leaves installed project")
100
- end
101
- directory!(selected)
102
- common = regular!(File.join(selected, "commondir"), metadata, 4096).strip
103
- unavailable!("worker common directory differs") unless File.expand_path(common, selected) == @common
104
- back = regular!(File.join(selected, "gitdir"), metadata, 4096).strip
105
- unavailable!("worker Git backpointer differs") unless back == marker
106
- selected
107
- end
108
- @entries = 0
109
- %w[objects refs].each { |name| storage!(File.join(@common, name)) }
110
- %w[commondir shallow reftable info/grafts objects/info/alternates objects/info/http-alternates].each do |name|
111
- path = File.join(@common, name)
112
- unavailable!("worker object storage is redirected") if File.exist?(path) || File.symlink?(path)
113
- end
114
- regular!(File.join(git_dir, "HEAD"), metadata, 4096)
115
- packed = File.join(@common, "packed-refs")
116
- storage!(packed) if File.exist?(packed) || File.symlink?(packed)
117
- local_refs = File.join(git_dir, "refs")
118
- storage!(local_refs) if git_dir != @common && (File.exist?(local_refs) || File.symlink?(local_refs))
119
- config!(File.join(@common, "config"), metadata)
120
- local = File.join(git_dir, "config.worktree")
121
- config!(local, metadata) if File.exist?(local) || File.symlink?(local)
122
- {"worktree" => worktree, "git_dir" => git_dir, "metadata" => metadata,
123
- "directories" => [cwd, worktree, git_dir, @common].to_h { |path| [path, identity(File.lstat(path))] }}
124
- end
125
-
126
- def config!(path, metadata)
127
- bytes = regular!(path, metadata, 65_536)
128
- output = run(["config", "--no-includes", "--null", "--list", "--file", "/dev/stdin"], stdin: bytes)
129
- output.split("\0").each do |entry|
130
- key, value = entry.split("\n", 2)
131
- if key.start_with?("include.", "includeif.", "extensions.") || key == "core.worktree" ||
132
- key == "core.repositoryformatversion" && value != "0"
133
- unavailable!("worker Git configuration redirects project storage")
134
- end
135
- end
136
- end
137
-
138
- def git(selected, *argv, allowed_status: [0], result: false)
139
- captured = run_result(["--git-dir=#{selected.fetch('git_dir')}", "--work-tree=#{selected.fetch('worktree')}",
140
- "-c", "safe.directory=#{selected.fetch('worktree')}", "-c", "core.hooksPath=/dev/null",
141
- "-c", "core.fsmonitor=false", "-c", "protocol.allow=never", *argv],
142
- environment: {"GIT_COMMON_DIR" => @common, "GIT_OBJECT_DIRECTORY" => File.join(@common, "objects")}, allowed_status: allowed_status)
143
- result ? captured : captured.stdout
144
- end
145
-
146
- def run(argv, stdin: "", environment: {})
147
- run_result(argv, stdin: stdin, environment: environment).stdout
148
- end
149
-
150
- def run_result(argv, stdin: "", environment: {}, allowed_status: [0])
151
- result = Ace::Herdr::Molecules::BoundedProcess.call(["/usr/bin/git", "--no-pager", *argv],
152
- environment: Ace::Assign::Molecules::InstalledJournalRead::ENVIRONMENT.merge(environment), chdir: "/",
153
- stdin_data: stdin, timeout_s: remaining, output_limit: 65_536, cleanup_group: true)
154
- unavailable!("worker Git read failed or exceeded its bound") unless allowed_status.include?(result.status.exitstatus) && !result.oversized
155
- result
156
- end
157
-
158
- def regular!(path, metadata, limit)
159
- directory!(File.dirname(path))
160
- File.open(path, File::RDONLY | File::NOFOLLOW | File::NONBLOCK) do |file|
161
- before = file.stat
162
- unavailable!("worker metadata must be a bounded regular file") unless before.file? && before.size <= limit
163
- bytes = file.read(limit + 1)
164
- unless bytes.bytesize <= limit && stamp(before) == stamp(file.stat) && stamp(before) == stamp(File.lstat(path))
165
- unavailable!("worker metadata changed during read")
166
- end
167
- metadata[path] = [stamp(before), bytes]
168
- bytes
169
- end
170
- end
171
-
172
- def directory!(path)
173
- current = path
174
- loop do
175
- remaining
176
- stat = File.lstat(current)
177
- unavailable!("worker directory is redirected") unless stat.directory? && !stat.symlink?
178
- break if current == "/"
179
- current = File.dirname(current)
180
- end
181
- end
182
-
183
- def storage!(path)
184
- remaining
185
- @entries += 1
186
- unavailable!("worker storage inventory exceeds its bound") if @entries > Ace::Assign::Molecules::InstalledJournalRead::ENTRY_LIMIT
187
- stat = File.lstat(path)
188
- unavailable!("worker storage is redirected") unless !stat.symlink? && (stat.file? || stat.directory?)
189
- Dir.each_child(path) { |name| storage!(File.join(path, name)) } if stat.directory?
190
- end
191
-
192
- def absolute!(path)
193
- unless path.is_a?(String) && path.bytesize.between?(1, 4096) && !path.include?("\0") &&
194
- path.start_with?("/") && File.expand_path(path) == path
195
- unavailable!("worker path is invalid")
196
- end
197
- end
198
-
199
- def within?(path, parent) = path == parent || path.start_with?(parent + "/")
200
- def identity(stat) = [stat.dev, stat.ino]
201
- def stamp(stat) = [stat.dev, stat.ino, stat.size, stat.mtime, stat.ctime]
202
-
203
- def remaining
204
- unless @deadline.is_a?(Numeric) && @deadline.finite? && (left = @deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)).positive?
205
- unavailable!("worker observation deadline elapsed")
206
- end
207
- left
208
- end
209
-
210
- def unavailable!(reason)
211
- raise Ace::Assign::AttemptErrors::EvidenceUnavailable, reason
212
- end
213
- end
214
- end
215
- end
216
- end
@@ -1,163 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "ace/assign/authority/service_evidence"
4
- require_relative "protected_cleanup_owner_client"
5
- require "ace/assign/authority/endcap"
6
- require "ace/assign/authority/launch_lifecycle"
7
-
8
- module Ace
9
- module Lab
10
- module Molecules
11
- # Only the fixed listener supplies the installed journal snapshot and its
12
- # independently observed self binding. A frame selects neither owner.
13
- class ProtectedCleanupOwnerAdmission
14
- FIELDS = %w[schema kind request_id input_digest claim_binding request_event_digest
15
- dispatch_event_digest input operation_owner_binding_digest].freeze
16
- INSPECT_FIELDS = (FIELDS + %w[challenge_ref]).freeze
17
-
18
- def initialize(deployment:, authority_id:, mapping_id:, service_id:, kernel:, preview_policy: nil)
19
- @deployment, @authority_id, @kernel = deployment, authority_id, kernel
20
- @mapping_id, @service_id = mapping_id, service_id
21
- @preview_policy = preview_policy
22
- end
23
-
24
- def identity_reader!(peer)
25
- authority = @deployment.authority(@authority_id)
26
- if peer.slice("uid", "gid", "groups") == authority.slice("uid", "gid", "groups")
27
- @kernel.live!(peer)
28
- return true
29
- end
30
- receiver!(peer)
31
- end
32
-
33
- def connection_group!(gid)
34
- authority = @deployment.authority(@authority_id)
35
- map = @deployment.mapping(@mapping_id)
36
- project = @deployment.project(map.fetch("project_id"))
37
- receiver = project.fetch("service_receivers").fetch(@service_id)
38
- credentials = project.fetch("peer_credentials").fetch(receiver.fetch("executor_uid").to_s)
39
- unless gid.is_a?(Integer) && gid.positive? && [authority, credentials].all? { |principal| principal.fetch("gid") == gid || principal.fetch("groups").include?(gid) }
40
- raise SecurityError, "cleanup connection group differs from installed principals"
41
- end
42
- true
43
- end
44
-
45
- def receiver!(peer)
46
- map = @deployment.mapping(@mapping_id)
47
- project = @deployment.project(map.fetch("project_id"))
48
- receiver = project.fetch("service_receivers").fetch(@service_id)
49
- credentials = project.fetch("peer_credentials").fetch(receiver.fetch("executor_uid").to_s)
50
- unless map.fetch("authority_id") == @authority_id && peer.fetch("uid").is_a?(Integer) && peer.fetch("uid").positive? &&
51
- peer.fetch("uid") == receiver.fetch("executor_uid") && peer.fetch("gid") == credentials.fetch("gid") &&
52
- peer.fetch("groups") == credentials.fetch("groups")
53
- raise SecurityError, "cleanup installed receiver differs"
54
- end
55
- @kernel.live!(peer)
56
- true
57
- rescue KeyError, TypeError
58
- raise SecurityError, "cleanup installed receiver unavailable"
59
- end
60
-
61
- def preview!(frame:, peer:, operation_owner_binding:, journal:)
62
- receiver!(peer)
63
- Atoms::ProtectedWorkspacePruneInput.object!(frame, %w[schema kind intent maintenance_context])
64
- raise SecurityError, "cleanup preview kind differs" unless frame.values_at("schema", "kind") == [ProtectedCleanupOwnerClient::SCHEMA, "preview"]
65
- intent = Atoms::ProtectedWorkspacePrunePreview.intent!(frame.fetch("intent"))
66
- supplied = Atoms::ProtectedWorkspacePrunePreview.context!(frame.fetch("maintenance_context"))
67
- maintenance = supplied.fetch("maintenance")
68
- map = @deployment.mapping(@mapping_id)
69
- unless intent.fetch("maintenance") == maintenance && maintenance.fetch("mapping_id") == @mapping_id &&
70
- maintenance.fetch("project_id") == map.fetch("project_id") && map.fetch("authority_id") == @authority_id
71
- raise SecurityError, "cleanup preview installed scope differs"
72
- end
73
- raise SecurityError, "cleanup preview policy composition unavailable" unless @preview_policy
74
- launch = Ace::Assign::Authority::LaunchLifecycle.new(deployment: @deployment, kernel: @kernel)
75
- owner = Ace::Assign::Authority::Endcap.new(deployment: @deployment, launch: launch, kernel: @kernel, service_policy: @preview_policy)
76
- params = maintenance.except("project_id").merge("service_id" => @service_id,
77
- "worker_process_binding" => supplied.fetch("caller_process_binding"))
78
- selected = owner.workspace_prune_preview_context_at!(journal: journal, params: params, map: map, peer: peer, role: :executor)
79
- raise SecurityError, "cleanup preview canonical selection changed" unless selected == supplied
80
- Atoms::ProtectedWorkspacePruneInput.freeze_value({"intent" => intent, "maintenance_context" => selected,
81
- "operation_owner_binding" => operation_owner_binding})
82
- rescue KeyError, TypeError, ArgumentError
83
- raise SecurityError, "cleanup preview canonical selection unavailable"
84
- end
85
-
86
- def admit!(frame:, peer:, operation_owner_binding:, journal:)
87
- admit_original!(frame: frame, peer: peer, operation_owner_binding: operation_owner_binding, journal: journal, purpose: :execute)
88
- end
89
-
90
- def inspect!(frame:, peer:, operation_owner_binding:, journal:)
91
- context = admit_original!(frame: frame, peer: peer, operation_owner_binding: operation_owner_binding, journal: journal, purpose: :inspect)
92
- reference = frame.fetch("challenge_ref")
93
- Atoms::ProtectedWorkspacePruneInput.object!(reference, %w[challenge_event_digest])
94
- Atoms::ProtectedWorkspacePruneInput.digest!(reference.fetch("challenge_event_digest"))
95
- challenge = Ace::Assign::Authority::ServiceEvidence.new(journal: journal).challenge!(context.fetch("record"), pending: {commit: context.fetch("commit")})
96
- unless challenge.fetch("digest") == reference.fetch("challenge_event_digest")
97
- raise SecurityError, "cleanup inspection challenge differs"
98
- end
99
- Atoms::ProtectedWorkspacePruneInput.freeze_value(context.merge("challenge" => challenge))
100
- end
101
-
102
- private
103
-
104
- def admit_original!(frame:, peer:, operation_owner_binding:, journal:, purpose:)
105
- receiver!(peer)
106
- Atoms::ProtectedWorkspacePruneInput.object!(frame, purpose == :execute ? FIELDS : INSPECT_FIELDS)
107
- unless frame.values_at("schema", "kind") == [ProtectedCleanupOwnerClient::SCHEMA, purpose.to_s]
108
- raise SecurityError, "cleanup invocation kind differs"
109
- end
110
- Atoms::ProtectedWorkspacePruneInput.token!(frame.fetch("request_id"))
111
- (FIELDS - %w[schema kind request_id input]).each do |key|
112
- Atoms::ProtectedWorkspacePruneInput.digest!(frame.fetch(key))
113
- end
114
- input = Atoms::ProtectedWorkspacePruneInput.parse(JSON.generate(frame.fetch("input")))
115
- unless Atoms::ServiceInput.digest(input) == frame.fetch("input_digest") &&
116
- (purpose == :inspect || digest(operation_owner_binding) == frame.fetch("operation_owner_binding_digest"))
117
- raise SecurityError, "cleanup original input or owner differs"
118
- end
119
- maintenance = input.fetch("maintenance")
120
- map = @deployment.mapping(maintenance.fetch("mapping_id"))
121
- unless maintenance.fetch("mapping_id") == @mapping_id && map.fetch("authority_id") == @authority_id && map.fetch("project_id") == maintenance.fetch("project_id")
122
- raise SecurityError, "cleanup installed scope differs"
123
- end
124
- unless journal.evidence_mode == :protected
125
- raise SecurityError, "cleanup requires the installed protected service evidence owner"
126
- end
127
- commit = journal.ref_value
128
- journal.verify_canonical_prefix!(commit: commit)
129
- record = journal.service_request(frame.fetch("request_id"), commit: commit)
130
- unless record && record.fetch("service_id") == @service_id && record.slice("project_id", "mapping_id", "assignment_id", "attempt_id") == maintenance &&
131
- record.values_at("operation", "dispatch_phase") == ["prune-preserved-workspace", "dispatch_started"] &&
132
- (purpose == :execute ? record["state"] == "uncertain" : %w[uncertain failed].include?(record["state"])) &&
133
- record.slice("request_id", "input_digest", "claim_binding") == frame.slice("request_id", "input_digest", "claim_binding") &&
134
- record.fetch("target") == Atoms::ServiceInput.target(input) &&
135
- digest(record.fetch("operation_owner_binding")) == frame.fetch("operation_owner_binding_digest") &&
136
- (purpose == :inspect || digest(record.fetch("executor_process_binding")) == digest(peer))
137
- raise SecurityError, "cleanup canonical original dispatch differs"
138
- end
139
- Ace::Assign::Authority::ServiceEvidence.new(journal: journal).context(record, pending: {commit: commit})
140
- receiver = @deployment.project(map.fetch("project_id")).fetch("service_receivers").fetch(record.fetch("service_id"))
141
- credentials = @deployment.project(map.fetch("project_id")).fetch("peer_credentials").fetch(receiver.fetch("executor_uid").to_s)
142
- unless peer.fetch("uid") == record.fetch("executor_uid") && peer.fetch("uid") == receiver.fetch("executor_uid") &&
143
- peer.fetch("gid") == credentials.fetch("gid") && peer.fetch("groups") == credentials.fetch("groups")
144
- raise SecurityError, "cleanup original executor differs"
145
- end
146
- @kernel.live!(peer)
147
- proof = Ace::Assign::Authority::ServiceEvidence.new(journal: journal).cleanup_dispatch_context!(
148
- record, commit: commit, selectors: frame.slice("request_event_digest", "dispatch_event_digest"))
149
- if purpose == :execute && proof.fetch("dispatch_record") != record
150
- raise SecurityError, "cleanup original executable record differs"
151
- end
152
- Atoms::ProtectedWorkspacePruneInput.freeze_value(JSON.parse(JSON.generate({"commit" => commit,
153
- "record" => record, "input" => input, "operation_owner_binding" => operation_owner_binding,
154
- "request_event_digest" => frame.fetch("request_event_digest"), "dispatch_event_digest" => frame.fetch("dispatch_event_digest")})))
155
- rescue KeyError, TypeError, ArgumentError
156
- raise SecurityError, "cleanup canonical admission is unavailable"
157
- end
158
-
159
- def digest(value) = Ace::Assign::Atoms::EvidenceDigest.digest(value)
160
- end
161
- end
162
- end
163
- end