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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +363 -104
- data/README.md +66 -100
- data/docs/usage.md +529 -592
- data/lib/ace/lab/atoms/install_schema.rb +373 -0
- data/lib/ace/lab/cli/commands/install.rb +161 -0
- data/lib/ace/lab/cli/commands/prepare.rb +118 -0
- data/lib/ace/lab/cli/commands/project_add.rb +101 -0
- data/lib/ace/lab/cli/commands/status.rb +109 -0
- data/lib/ace/lab/cli/commands/verify.rb +115 -0
- data/lib/ace/lab/cli.rb +21 -23
- data/lib/ace/lab/install_manifest_path.rb +23 -0
- data/lib/ace/lab/models/status_report.rb +38 -0
- data/lib/ace/lab/molecules/account_provisioner.rb +196 -0
- data/lib/ace/lab/molecules/config_state_classifier.rb +145 -0
- data/lib/ace/lab/molecules/directory_provisioner.rb +208 -0
- data/lib/ace/lab/molecules/grant_provisioner.rb +250 -0
- data/lib/ace/lab/molecules/grant_resolver.rb +34 -23
- data/lib/ace/lab/molecules/hitl_routes.rb +263 -0
- data/lib/ace/lab/molecules/install_manifest_loader.rb +64 -0
- data/lib/ace/lab/molecules/key_provisioner.rb +736 -0
- data/lib/ace/lab/molecules/lab_state_scanner.rb +55 -0
- data/lib/ace/lab/molecules/overseer_provisioner.rb +188 -0
- data/lib/ace/lab/molecules/project_registry.rb +251 -0
- data/lib/ace/lab/molecules/repository_provisioner.rb +141 -0
- data/lib/ace/lab/molecules/role_verifier.rb +223 -0
- data/lib/ace/lab/organisms/install.rb +213 -0
- data/lib/ace/lab/organisms/prepare.rb +156 -0
- data/lib/ace/lab/organisms/project_add.rb +170 -0
- data/lib/ace/lab/organisms/status_service.rb +70 -0
- data/lib/ace/lab/organisms/verify.rb +768 -0
- data/lib/ace/lab/version.rb +1 -1
- data/lib/ace/lab.rb +34 -6
- metadata +34 -108
- data/lib/ace/lab/atoms/protected_publication_input.rb +0 -50
- data/lib/ace/lab/atoms/protected_workspace_prune_input.rb +0 -114
- data/lib/ace/lab/atoms/protected_workspace_prune_preview.rb +0 -86
- data/lib/ace/lab/atoms/service_input.rb +0 -92
- data/lib/ace/lab/cli/commands/assignment.rb +0 -92
- data/lib/ace/lab/cli/commands/command_execute.rb +0 -28
- data/lib/ace/lab/cli/commands/run.rb +0 -65
- data/lib/ace/lab/cli/commands/service.rb +0 -240
- data/lib/ace/lab/models/service_receipt.rb +0 -18
- data/lib/ace/lab/molecules/command_owner_admission.rb +0 -183
- data/lib/ace/lab/molecules/command_policy.rb +0 -158
- data/lib/ace/lab/molecules/hitl_authorizer.rb +0 -63
- data/lib/ace/lab/molecules/ordinary_admission_resolver.rb +0 -131
- data/lib/ace/lab/molecules/ordinary_listener.rb +0 -262
- data/lib/ace/lab/molecules/ordinary_merge_read.rb +0 -45
- data/lib/ace/lab/molecules/ordinary_native_read.rb +0 -106
- data/lib/ace/lab/molecules/ordinary_owner_admission.rb +0 -152
- data/lib/ace/lab/molecules/ordinary_owner_config.rb +0 -232
- data/lib/ace/lab/molecules/ordinary_receiver_config.rb +0 -94
- data/lib/ace/lab/molecules/ordinary_socket.rb +0 -126
- data/lib/ace/lab/molecules/ordinary_worktree_read.rb +0 -216
- data/lib/ace/lab/molecules/protected_cleanup_owner_admission.rb +0 -163
- data/lib/ace/lab/molecules/protected_cleanup_owner_client.rb +0 -165
- data/lib/ace/lab/molecules/protected_cleanup_owner_identity.rb +0 -239
- data/lib/ace/lab/molecules/protected_publication_publisher.rb +0 -110
- data/lib/ace/lab/molecules/protected_service_handler.rb +0 -43
- data/lib/ace/lab/molecules/protected_service_policy.rb +0 -156
- data/lib/ace/lab/molecules/service_executor.rb +0 -99
- data/lib/ace/lab/molecules/service_policy.rb +0 -289
- data/lib/ace/lab/organisms/authority_composition.rb +0 -87
- data/lib/ace/lab/organisms/command_executor.rb +0 -220
- data/lib/ace/lab/organisms/command_run_client.rb +0 -131
- data/lib/ace/lab/organisms/ordinary_assignment_client.rb +0 -120
- data/lib/ace/lab/organisms/ordinary_authority_composition.rb +0 -153
- data/lib/ace/lab/organisms/ordinary_merge_receiver.rb +0 -679
- data/lib/ace/lab/organisms/ordinary_merge_request.rb +0 -456
- data/lib/ace/lab/organisms/protected_cleanup_owner.rb +0 -361
- data/lib/ace/lab/organisms/protected_service_client.rb +0 -129
- data/lib/ace/lab/organisms/protected_service_listener.rb +0 -181
- data/lib/ace/lab/organisms/protected_service_publication.rb +0 -268
- data/lib/ace/lab/organisms/protected_service_receiver.rb +0 -267
- data/lib/ace/lab/organisms/protected_service_recovery.rb +0 -185
- data/lib/ace/lab/organisms/protected_service_request.rb +0 -128
- data/lib/ace/lab/organisms/protected_service_worker.rb +0 -89
- data/lib/ace/lab/organisms/service_request_service.rb +0 -249
data/lib/ace/lab/version.rb
CHANGED
data/lib/ace/lab.rb
CHANGED
|
@@ -2,30 +2,46 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "lab/version"
|
|
4
4
|
require "ace/support/config"
|
|
5
|
+
require "rbconfig"
|
|
5
6
|
|
|
6
7
|
require_relative "lab/errors"
|
|
7
8
|
require_relative "lab/authorization_path"
|
|
9
|
+
require_relative "lab/install_manifest_path"
|
|
8
10
|
|
|
9
11
|
# Load all ace-lab components
|
|
10
12
|
require_relative "lab/atoms/topology_schema"
|
|
13
|
+
require_relative "lab/atoms/install_schema"
|
|
11
14
|
require_relative "lab/atoms/binding_freshness"
|
|
12
15
|
require_relative "lab/atoms/public_projection"
|
|
13
|
-
require_relative "lab/atoms/service_input"
|
|
14
|
-
require_relative "lab/models/service_receipt"
|
|
15
16
|
require_relative "lab/models/runtime_binding"
|
|
16
17
|
require_relative "lab/models/topology_entry"
|
|
17
18
|
require_relative "lab/models/query_result"
|
|
19
|
+
require_relative "lab/models/status_report"
|
|
18
20
|
require_relative "lab/molecules/topology_loader"
|
|
21
|
+
require_relative "lab/molecules/config_state_classifier"
|
|
22
|
+
require_relative "lab/molecules/lab_state_scanner"
|
|
19
23
|
require_relative "lab/molecules/caller_authorizer"
|
|
20
24
|
require_relative "lab/molecules/grant_resolver"
|
|
21
|
-
require_relative "lab/molecules/
|
|
22
|
-
require_relative "lab/molecules/
|
|
23
|
-
require_relative "lab/molecules/
|
|
25
|
+
require_relative "lab/molecules/grant_provisioner"
|
|
26
|
+
require_relative "lab/molecules/install_manifest_loader"
|
|
27
|
+
require_relative "lab/molecules/account_provisioner"
|
|
28
|
+
require_relative "lab/molecules/directory_provisioner"
|
|
29
|
+
require_relative "lab/molecules/key_provisioner"
|
|
30
|
+
require_relative "lab/molecules/role_verifier"
|
|
31
|
+
require_relative "lab/organisms/prepare"
|
|
32
|
+
require_relative "lab/organisms/project_add"
|
|
33
|
+
require_relative "lab/organisms/install"
|
|
34
|
+
require_relative "lab/organisms/verify"
|
|
35
|
+
require_relative "lab/molecules/repository_provisioner"
|
|
36
|
+
require_relative "lab/molecules/overseer_provisioner"
|
|
37
|
+
require_relative "lab/molecules/project_registry"
|
|
38
|
+
require_relative "lab/molecules/forge_merge_config"
|
|
39
|
+
require_relative "lab/molecules/forgejo_merge_adapter"
|
|
24
40
|
require_relative "lab/molecules/inventory_query"
|
|
25
41
|
require_relative "lab/molecules/exact_resolver"
|
|
26
42
|
require_relative "lab/molecules/capability_router"
|
|
27
43
|
require_relative "lab/organisms/topology_service"
|
|
28
|
-
require_relative "lab/organisms/
|
|
44
|
+
require_relative "lab/organisms/status_service"
|
|
29
45
|
require_relative "lab/cli"
|
|
30
46
|
|
|
31
47
|
module Ace
|
|
@@ -37,6 +53,18 @@ module Ace
|
|
|
37
53
|
File.expand_path("../..", __dir__)
|
|
38
54
|
end
|
|
39
55
|
|
|
56
|
+
# Host platform family used for account-provisioning defaults
|
|
57
|
+
# ("darwin" or "linux"). A method (not a constant) so tests stub the
|
|
58
|
+
# seam without any caller-controllable production override.
|
|
59
|
+
# @return [String]
|
|
60
|
+
def self.host_platform
|
|
61
|
+
case RbConfig::CONFIG["host_os"]
|
|
62
|
+
when /darwin/ then "darwin"
|
|
63
|
+
when /linux/ then "linux"
|
|
64
|
+
else "unknown"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
40
68
|
# Check if debug mode is enabled
|
|
41
69
|
# @return [Boolean]
|
|
42
70
|
def self.debug?
|
metadata
CHANGED
|
@@ -1,28 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-lab
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.13.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-10-
|
|
10
|
+
date: 2026-10-20 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
-
- !ruby/object:Gem::Dependency
|
|
13
|
-
name: ace-runtime
|
|
14
|
-
requirement: !ruby/object:Gem::Requirement
|
|
15
|
-
requirements:
|
|
16
|
-
- - "~>"
|
|
17
|
-
- !ruby/object:Gem::Version
|
|
18
|
-
version: '0.3'
|
|
19
|
-
type: :runtime
|
|
20
|
-
prerelease: false
|
|
21
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
-
requirements:
|
|
23
|
-
- - "~>"
|
|
24
|
-
- !ruby/object:Gem::Version
|
|
25
|
-
version: '0.3'
|
|
26
12
|
- !ruby/object:Gem::Dependency
|
|
27
13
|
name: ace-support-cli
|
|
28
14
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -65,48 +51,6 @@ dependencies:
|
|
|
65
51
|
- - "~>"
|
|
66
52
|
- !ruby/object:Gem::Version
|
|
67
53
|
version: '0.18'
|
|
68
|
-
- !ruby/object:Gem::Dependency
|
|
69
|
-
name: ace-assign
|
|
70
|
-
requirement: !ruby/object:Gem::Requirement
|
|
71
|
-
requirements:
|
|
72
|
-
- - "~>"
|
|
73
|
-
- !ruby/object:Gem::Version
|
|
74
|
-
version: '0.70'
|
|
75
|
-
type: :runtime
|
|
76
|
-
prerelease: false
|
|
77
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
-
requirements:
|
|
79
|
-
- - "~>"
|
|
80
|
-
- !ruby/object:Gem::Version
|
|
81
|
-
version: '0.70'
|
|
82
|
-
- !ruby/object:Gem::Dependency
|
|
83
|
-
name: ace-herdr
|
|
84
|
-
requirement: !ruby/object:Gem::Requirement
|
|
85
|
-
requirements:
|
|
86
|
-
- - "~>"
|
|
87
|
-
- !ruby/object:Gem::Version
|
|
88
|
-
version: '0.6'
|
|
89
|
-
type: :runtime
|
|
90
|
-
prerelease: false
|
|
91
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
-
requirements:
|
|
93
|
-
- - "~>"
|
|
94
|
-
- !ruby/object:Gem::Version
|
|
95
|
-
version: '0.6'
|
|
96
|
-
- !ruby/object:Gem::Dependency
|
|
97
|
-
name: ace-hitl
|
|
98
|
-
requirement: !ruby/object:Gem::Requirement
|
|
99
|
-
requirements:
|
|
100
|
-
- - "~>"
|
|
101
|
-
- !ruby/object:Gem::Version
|
|
102
|
-
version: '0.16'
|
|
103
|
-
type: :runtime
|
|
104
|
-
prerelease: false
|
|
105
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
-
requirements:
|
|
107
|
-
- - "~>"
|
|
108
|
-
- !ruby/object:Gem::Version
|
|
109
|
-
version: '0.16'
|
|
110
54
|
- !ruby/object:Gem::Dependency
|
|
111
55
|
name: ace-git-forgejo
|
|
112
56
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -205,9 +149,11 @@ dependencies:
|
|
|
205
149
|
- - "~>"
|
|
206
150
|
- !ruby/object:Gem::Version
|
|
207
151
|
version: '0.22'
|
|
208
|
-
description:
|
|
209
|
-
agent, and service IDs from ADR-022 configuration
|
|
210
|
-
|
|
152
|
+
description: 'Lab environment package: read-only topology registry and capability
|
|
153
|
+
router (stable project, agent, and service IDs from ADR-022 configuration), the
|
|
154
|
+
lab status snapshot, and ace-lab prepare — idempotent preparation of accounts, directories,
|
|
155
|
+
permissions, ed25519 keys, and role bindings from a root-owned install manifest.
|
|
156
|
+
No queue, journal, or command kernel.'
|
|
211
157
|
email:
|
|
212
158
|
- mc@cs3b.com
|
|
213
159
|
executables:
|
|
@@ -224,74 +170,54 @@ files:
|
|
|
224
170
|
- exe/ace-lab
|
|
225
171
|
- lib/ace/lab.rb
|
|
226
172
|
- lib/ace/lab/atoms/binding_freshness.rb
|
|
227
|
-
- lib/ace/lab/atoms/
|
|
228
|
-
- lib/ace/lab/atoms/protected_workspace_prune_input.rb
|
|
229
|
-
- lib/ace/lab/atoms/protected_workspace_prune_preview.rb
|
|
173
|
+
- lib/ace/lab/atoms/install_schema.rb
|
|
230
174
|
- lib/ace/lab/atoms/public_projection.rb
|
|
231
|
-
- lib/ace/lab/atoms/service_input.rb
|
|
232
175
|
- lib/ace/lab/atoms/topology_schema.rb
|
|
233
176
|
- lib/ace/lab/authorization_path.rb
|
|
234
177
|
- lib/ace/lab/cli.rb
|
|
235
178
|
- lib/ace/lab/cli/commands/agents.rb
|
|
236
|
-
- lib/ace/lab/cli/commands/
|
|
237
|
-
- lib/ace/lab/cli/commands/
|
|
179
|
+
- lib/ace/lab/cli/commands/install.rb
|
|
180
|
+
- lib/ace/lab/cli/commands/prepare.rb
|
|
181
|
+
- lib/ace/lab/cli/commands/project_add.rb
|
|
238
182
|
- lib/ace/lab/cli/commands/projects.rb
|
|
239
183
|
- lib/ace/lab/cli/commands/resolve.rb
|
|
240
184
|
- lib/ace/lab/cli/commands/route.rb
|
|
241
|
-
- lib/ace/lab/cli/commands/run.rb
|
|
242
|
-
- lib/ace/lab/cli/commands/service.rb
|
|
243
185
|
- lib/ace/lab/cli/commands/services.rb
|
|
186
|
+
- lib/ace/lab/cli/commands/status.rb
|
|
244
187
|
- lib/ace/lab/cli/commands/support.rb
|
|
188
|
+
- lib/ace/lab/cli/commands/verify.rb
|
|
245
189
|
- lib/ace/lab/errors.rb
|
|
190
|
+
- lib/ace/lab/install_manifest_path.rb
|
|
246
191
|
- lib/ace/lab/models/query_result.rb
|
|
247
192
|
- lib/ace/lab/models/runtime_binding.rb
|
|
248
|
-
- lib/ace/lab/models/
|
|
193
|
+
- lib/ace/lab/models/status_report.rb
|
|
249
194
|
- lib/ace/lab/models/topology_entry.rb
|
|
195
|
+
- lib/ace/lab/molecules/account_provisioner.rb
|
|
250
196
|
- lib/ace/lab/molecules/caller_authorizer.rb
|
|
251
197
|
- lib/ace/lab/molecules/capability_router.rb
|
|
252
|
-
- lib/ace/lab/molecules/
|
|
253
|
-
- lib/ace/lab/molecules/
|
|
198
|
+
- lib/ace/lab/molecules/config_state_classifier.rb
|
|
199
|
+
- lib/ace/lab/molecules/directory_provisioner.rb
|
|
254
200
|
- lib/ace/lab/molecules/exact_resolver.rb
|
|
255
201
|
- lib/ace/lab/molecules/forge_merge_config.rb
|
|
256
202
|
- lib/ace/lab/molecules/forgejo_merge_adapter.rb
|
|
203
|
+
- lib/ace/lab/molecules/grant_provisioner.rb
|
|
257
204
|
- lib/ace/lab/molecules/grant_resolver.rb
|
|
258
|
-
- lib/ace/lab/molecules/
|
|
205
|
+
- lib/ace/lab/molecules/hitl_routes.rb
|
|
206
|
+
- lib/ace/lab/molecules/install_manifest_loader.rb
|
|
259
207
|
- lib/ace/lab/molecules/inventory_query.rb
|
|
260
|
-
- lib/ace/lab/molecules/
|
|
261
|
-
- lib/ace/lab/molecules/
|
|
262
|
-
- lib/ace/lab/molecules/
|
|
263
|
-
- lib/ace/lab/molecules/
|
|
264
|
-
- lib/ace/lab/molecules/
|
|
265
|
-
- lib/ace/lab/molecules/
|
|
266
|
-
- lib/ace/lab/molecules/ordinary_receiver_config.rb
|
|
267
|
-
- lib/ace/lab/molecules/ordinary_socket.rb
|
|
268
|
-
- lib/ace/lab/molecules/ordinary_worktree_read.rb
|
|
269
|
-
- lib/ace/lab/molecules/protected_cleanup_owner_admission.rb
|
|
270
|
-
- lib/ace/lab/molecules/protected_cleanup_owner_client.rb
|
|
271
|
-
- lib/ace/lab/molecules/protected_cleanup_owner_identity.rb
|
|
272
|
-
- lib/ace/lab/molecules/protected_publication_publisher.rb
|
|
273
|
-
- lib/ace/lab/molecules/protected_service_handler.rb
|
|
274
|
-
- lib/ace/lab/molecules/protected_service_policy.rb
|
|
275
|
-
- lib/ace/lab/molecules/service_executor.rb
|
|
276
|
-
- lib/ace/lab/molecules/service_policy.rb
|
|
208
|
+
- lib/ace/lab/molecules/key_provisioner.rb
|
|
209
|
+
- lib/ace/lab/molecules/lab_state_scanner.rb
|
|
210
|
+
- lib/ace/lab/molecules/overseer_provisioner.rb
|
|
211
|
+
- lib/ace/lab/molecules/project_registry.rb
|
|
212
|
+
- lib/ace/lab/molecules/repository_provisioner.rb
|
|
213
|
+
- lib/ace/lab/molecules/role_verifier.rb
|
|
277
214
|
- lib/ace/lab/molecules/topology_loader.rb
|
|
278
|
-
- lib/ace/lab/organisms/
|
|
279
|
-
- lib/ace/lab/organisms/
|
|
280
|
-
- lib/ace/lab/organisms/
|
|
281
|
-
- lib/ace/lab/organisms/
|
|
282
|
-
- lib/ace/lab/organisms/ordinary_authority_composition.rb
|
|
283
|
-
- lib/ace/lab/organisms/ordinary_merge_receiver.rb
|
|
284
|
-
- lib/ace/lab/organisms/ordinary_merge_request.rb
|
|
285
|
-
- lib/ace/lab/organisms/protected_cleanup_owner.rb
|
|
286
|
-
- lib/ace/lab/organisms/protected_service_client.rb
|
|
287
|
-
- lib/ace/lab/organisms/protected_service_listener.rb
|
|
288
|
-
- lib/ace/lab/organisms/protected_service_publication.rb
|
|
289
|
-
- lib/ace/lab/organisms/protected_service_receiver.rb
|
|
290
|
-
- lib/ace/lab/organisms/protected_service_recovery.rb
|
|
291
|
-
- lib/ace/lab/organisms/protected_service_request.rb
|
|
292
|
-
- lib/ace/lab/organisms/protected_service_worker.rb
|
|
293
|
-
- lib/ace/lab/organisms/service_request_service.rb
|
|
215
|
+
- lib/ace/lab/organisms/install.rb
|
|
216
|
+
- lib/ace/lab/organisms/prepare.rb
|
|
217
|
+
- lib/ace/lab/organisms/project_add.rb
|
|
218
|
+
- lib/ace/lab/organisms/status_service.rb
|
|
294
219
|
- lib/ace/lab/organisms/topology_service.rb
|
|
220
|
+
- lib/ace/lab/organisms/verify.rb
|
|
295
221
|
- lib/ace/lab/version.rb
|
|
296
222
|
homepage: https://github.com/cs3b/ace
|
|
297
223
|
licenses:
|
|
@@ -317,6 +243,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
317
243
|
requirements: []
|
|
318
244
|
rubygems_version: 3.6.9
|
|
319
245
|
specification_version: 4
|
|
320
|
-
summary: '
|
|
321
|
-
|
|
246
|
+
summary: 'Lab environment CLI: topology queries, lab status, and prepare-phase convergence
|
|
247
|
+
from the trusted install manifest'
|
|
322
248
|
test_files: []
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "service_input"
|
|
4
|
-
|
|
5
|
-
module Ace
|
|
6
|
-
module Lab
|
|
7
|
-
module Atoms
|
|
8
|
-
# One accepted artifact, never a queue selector or a build instruction.
|
|
9
|
-
module ProtectedPublicationInput
|
|
10
|
-
REGISTRY = "https://rubygems.org"
|
|
11
|
-
PUBLICATION_FIELDS = %w[gem_name version head registry artifact_relative_path].freeze
|
|
12
|
-
NAME = /\A[a-zA-Z0-9][a-zA-Z0-9_.-]{0,127}\z/
|
|
13
|
-
VERSION = /\A[0-9]+(?:\.[0-9A-Za-z]+)*(?:-[0-9A-Za-z]+(?:\.[0-9A-Za-z]+)*)?\z/
|
|
14
|
-
|
|
15
|
-
def self.decode!(bytes)
|
|
16
|
-
unless bytes.is_a?(String) && bytes.bytesize.between?(1, ServiceInput::MAX_BYTES)
|
|
17
|
-
raise ArgumentError, "publication input exceeds its bound"
|
|
18
|
-
end
|
|
19
|
-
input = JSON.parse(bytes, allow_duplicate_key: false, allow_comments: false,
|
|
20
|
-
allow_nan: false, create_additions: false, max_nesting: 16)
|
|
21
|
-
validate!(input)
|
|
22
|
-
rescue JSON::ParserError
|
|
23
|
-
raise ArgumentError, "publication input must be valid JSON"
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def self.validate!(input)
|
|
27
|
-
unless input.is_a?(Hash) && input.keys.sort == %w[publication target] &&
|
|
28
|
-
input["publication"].is_a?(Hash) && input["publication"].keys.sort == PUBLICATION_FIELDS.sort &&
|
|
29
|
-
input["target"].is_a?(Hash) && input["target"].keys.sort == %w[artifact_digest resource]
|
|
30
|
-
raise ArgumentError, "publication input fields differ"
|
|
31
|
-
end
|
|
32
|
-
value = input.fetch("publication")
|
|
33
|
-
name, version, head, registry, path = value.values_at(*PUBLICATION_FIELDS)
|
|
34
|
-
unless name.is_a?(String) && name.match?(NAME) && version.is_a?(String) &&
|
|
35
|
-
version.bytesize.between?(1, 128) && version.match?(VERSION) &&
|
|
36
|
-
head.is_a?(String) && head.match?(/\A[0-9a-f]{40}\z/) && registry == REGISTRY &&
|
|
37
|
-
path.is_a?(String) && path.bytesize.between?(1, 4096) && path.end_with?(".gem") &&
|
|
38
|
-
path.split("/", -1).all? { |part| part.match?(/\A[a-zA-Z0-9_.-]+\z/) && !%w[. ..].include?(part) } &&
|
|
39
|
-
input.dig("target", "resource") == "rubygems:#{name}:#{version}" &&
|
|
40
|
-
input.dig("target", "artifact_digest").is_a?(String) &&
|
|
41
|
-
input.dig("target", "artifact_digest").match?(ServiceInput::SHA256)
|
|
42
|
-
raise ArgumentError, "publication artifact selection differs"
|
|
43
|
-
end
|
|
44
|
-
ServiceInput.validate!(input)
|
|
45
|
-
input
|
|
46
|
-
end
|
|
47
|
-
end
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "json"
|
|
4
|
-
|
|
5
|
-
module Ace
|
|
6
|
-
module Lab
|
|
7
|
-
module Atoms
|
|
8
|
-
# Structural input validation only. Canonical ownership, preservation and
|
|
9
|
-
# current permission are independently verified by the operation owners.
|
|
10
|
-
module ProtectedWorkspacePruneInput
|
|
11
|
-
MAX_BYTES = 65_536
|
|
12
|
-
ID = /\A[A-Za-z0-9_.-]{1,200}\z/
|
|
13
|
-
SHA = /\A[0-9a-f]{64}\z/
|
|
14
|
-
COMMIT = /\A(?:[0-9a-f]{40}|[0-9a-f]{64})\z/
|
|
15
|
-
ASSOCIATION = %w[project_id mapping_id assignment_id attempt_id].freeze
|
|
16
|
-
|
|
17
|
-
module_function
|
|
18
|
-
|
|
19
|
-
def parse(bytes)
|
|
20
|
-
reject! unless bytes.is_a?(String) && bytes.bytesize.between?(1, MAX_BYTES)
|
|
21
|
-
text = bytes.dup.force_encoding(Encoding::UTF_8)
|
|
22
|
-
reject! unless text.valid_encoding?
|
|
23
|
-
input = JSON.parse(text, create_additions: false, max_nesting: 16,
|
|
24
|
-
allow_nan: false, allow_comments: false, allow_duplicate_key: false)
|
|
25
|
-
object!(input, %w[schema maintenance target publication preservation])
|
|
26
|
-
reject! unless input.fetch("schema") == "ace.protected-workspace-prune/v1"
|
|
27
|
-
association!(input.fetch("maintenance"))
|
|
28
|
-
target = object!(input.fetch("target"), ASSOCIATION +
|
|
29
|
-
%w[resource artifact_digest descriptor_sha256 binding_event_digest release_event_digest journal_commit])
|
|
30
|
-
ASSOCIATION.each { |field| token!(target.fetch(field)) }
|
|
31
|
-
%w[artifact_digest descriptor_sha256 binding_event_digest release_event_digest].each { |key| digest!(target.fetch(key)) }
|
|
32
|
-
commit!(target.fetch("journal_commit"))
|
|
33
|
-
resource = "workspace:#{target.fetch('project_id')}:#{target.fetch('mapping_id')}:#{target.fetch('assignment_id')}"
|
|
34
|
-
reject! unless resource.bytesize <= 256 && target.fetch("resource") == resource
|
|
35
|
-
publication = object!(input.fetch("publication"), %w[descriptor_sha256 installation_ref])
|
|
36
|
-
digest!(publication.fetch("descriptor_sha256"))
|
|
37
|
-
reference!(publication.fetch("installation_ref"))
|
|
38
|
-
preservation!(input.fetch("preservation"))
|
|
39
|
-
freeze_value(input)
|
|
40
|
-
rescue JSON::ParserError, JSON::NestingError, EncodingError
|
|
41
|
-
reject!
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def preservation!(value)
|
|
45
|
-
object!(value, %w[head branch destinations manifest_sha256])
|
|
46
|
-
commit!(value.fetch("head"))
|
|
47
|
-
branch = value.fetch("branch")
|
|
48
|
-
unless branch.nil?
|
|
49
|
-
ref!(branch)
|
|
50
|
-
reject! unless branch.start_with?("refs/heads/")
|
|
51
|
-
end
|
|
52
|
-
digest!(value.fetch("manifest_sha256"))
|
|
53
|
-
destinations = value.fetch("destinations")
|
|
54
|
-
reject! unless destinations.is_a?(Array) && destinations.length <= 64
|
|
55
|
-
identities = destinations.map do |entry|
|
|
56
|
-
object!(entry, %w[repository_id ref head])
|
|
57
|
-
token!(entry.fetch("repository_id"))
|
|
58
|
-
ref!(entry.fetch("ref"))
|
|
59
|
-
commit!(entry.fetch("head"))
|
|
60
|
-
entry.values_at("repository_id", "ref", "head")
|
|
61
|
-
end
|
|
62
|
-
reject! unless identities == identities.sort && identities.uniq == identities
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def association!(value)
|
|
66
|
-
object!(value, ASSOCIATION)
|
|
67
|
-
value.each_value { |item| token!(item) }
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def reference!(value)
|
|
71
|
-
object!(value, %w[path bytes sha256])
|
|
72
|
-
path = value.fetch("path")
|
|
73
|
-
reject! unless path.is_a?(String) && path.bytesize.between?(2, 4096) &&
|
|
74
|
-
path.start_with?("/") && !path.include?("\0") && !path.end_with?("/") &&
|
|
75
|
-
path.split("/", -1).drop(1).all? { |part| !part.empty? && !%w[. ..].include?(part) }
|
|
76
|
-
reject! unless value.fetch("bytes").is_a?(Integer) && value.fetch("bytes").between?(1, MAX_BYTES)
|
|
77
|
-
digest!(value.fetch("sha256"))
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def object!(value, fields)
|
|
81
|
-
reject! unless value.is_a?(Hash) && value.keys.sort == fields.sort
|
|
82
|
-
value
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def token!(value)
|
|
86
|
-
reject! unless value.is_a?(String) && ID.match?(value)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
def digest!(value)
|
|
90
|
-
reject! unless value.is_a?(String) && SHA.match?(value)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def commit!(value)
|
|
94
|
-
reject! unless value.is_a?(String) && COMMIT.match?(value)
|
|
95
|
-
end
|
|
96
|
-
|
|
97
|
-
def ref!(value)
|
|
98
|
-
reject! unless value.is_a?(String) && value.bytesize.between?(6, 256) &&
|
|
99
|
-
value.start_with?("refs/") && !value.match?(/[\x00-\x20\x7f~^:?*\[\\]/) &&
|
|
100
|
-
!value.include?("..") && !value.include?("@{") && !value.end_with?(".") &&
|
|
101
|
-
value.split("/", -1).all? { |part| !part.empty? && !part.start_with?(".") && !part.end_with?(".lock") }
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def freeze_value(value)
|
|
105
|
-
value.each { |key, item| key.freeze; freeze_value(item) } if value.is_a?(Hash)
|
|
106
|
-
value.each { |item| freeze_value(item) } if value.is_a?(Array)
|
|
107
|
-
value.freeze
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def reject! = raise(ArgumentError, "invalid protected prune input")
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
end
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "protected_workspace_prune_input"
|
|
4
|
-
require "ace/assign/molecules/execution_scope_lineage"
|
|
5
|
-
require "ace/assign/atoms/evidence_digest"
|
|
6
|
-
|
|
7
|
-
module Ace
|
|
8
|
-
module Lab
|
|
9
|
-
module Atoms
|
|
10
|
-
# Closed transport data only; canonical and physical owners must
|
|
11
|
-
# independently authenticate these selectors before any effect.
|
|
12
|
-
module ProtectedWorkspacePrunePreview
|
|
13
|
-
Input = ProtectedWorkspacePruneInput
|
|
14
|
-
CONTEXT = %w[journal_commit maintenance head candidate_generation worker_process_binding caller_process_binding].freeze
|
|
15
|
-
RESULT = %w[schema kind intent_digest publication maintenance maintenance_context target preservation inventory_sha256 file_count total_bytes].freeze
|
|
16
|
-
module_function
|
|
17
|
-
|
|
18
|
-
def intent!(value)
|
|
19
|
-
Input.object!(value, %w[schema maintenance target publication destinations])
|
|
20
|
-
Input.reject! unless value["schema"] == "ace.protected-workspace-prune-preview/v1"
|
|
21
|
-
Input.association!(value.fetch("maintenance"))
|
|
22
|
-
target = value.fetch("target")
|
|
23
|
-
Input.object!(target, Input::ASSOCIATION + %w[resource descriptor_sha256 binding_event_digest release_event_digest journal_commit])
|
|
24
|
-
Input::ASSOCIATION.each { |key| Input.token!(target.fetch(key)) }
|
|
25
|
-
%w[descriptor_sha256 binding_event_digest release_event_digest].each { |key| Input.digest!(target.fetch(key)) }
|
|
26
|
-
Input.commit!(target.fetch("journal_commit"))
|
|
27
|
-
expected = "workspace:#{target.fetch('project_id')}:#{target.fetch('mapping_id')}:#{target.fetch('assignment_id')}"
|
|
28
|
-
Input.reject! unless expected.bytesize <= 256 && target["resource"] == expected
|
|
29
|
-
publication = Input.object!(value.fetch("publication"), %w[descriptor_sha256 installation_ref])
|
|
30
|
-
Input.digest!(publication.fetch("descriptor_sha256"))
|
|
31
|
-
Input.reference!(publication.fetch("installation_ref"))
|
|
32
|
-
# Reuse the existing exact destination/ref owner, without accepting
|
|
33
|
-
# or manufacturing a preservation authority assertion.
|
|
34
|
-
destinations!(value.fetch("destinations"))
|
|
35
|
-
Input.reject! if JSON.generate(value).bytesize > Input::MAX_BYTES
|
|
36
|
-
Input.freeze_value(value)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def destinations!(destinations)
|
|
40
|
-
Input.reject! unless destinations.is_a?(Array) && destinations.length <= 64
|
|
41
|
-
ids = destinations.map do |entry|
|
|
42
|
-
Input.object!(entry, %w[repository_id ref head])
|
|
43
|
-
Input.token!(entry.fetch("repository_id")); Input.ref!(entry.fetch("ref")); Input.commit!(entry.fetch("head"))
|
|
44
|
-
entry.values_at("repository_id", "ref", "head")
|
|
45
|
-
end
|
|
46
|
-
Input.reject! unless ids == ids.sort && ids == ids.uniq
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def context!(value)
|
|
50
|
-
Input.object!(value, CONTEXT)
|
|
51
|
-
Input.commit!(value.fetch("journal_commit"))
|
|
52
|
-
Input.association!(value.fetch("maintenance"))
|
|
53
|
-
Input.commit!(value.fetch("head"))
|
|
54
|
-
Input.reject! unless value["candidate_generation"].is_a?(Integer) && value["candidate_generation"].positive?
|
|
55
|
-
original = value.fetch("worker_process_binding")
|
|
56
|
-
birth = original.is_a?(Hash) && original["started_at"]
|
|
57
|
-
boot = birth.is_a?(String) && birth.match(/\Alinux:([0-9a-f-]{36}):[0-9]+\z/)&.captures&.first
|
|
58
|
-
%w[worker_process_binding caller_process_binding].each do |key|
|
|
59
|
-
Ace::Assign::Molecules::ExecutionScopeLineage.validate_process_identity!(value.fetch(key), boot_id: boot)
|
|
60
|
-
end
|
|
61
|
-
Input.reject! if JSON.generate(value).bytesize > 16_384
|
|
62
|
-
Input.freeze_value(value)
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def result!(value, intent:, context:)
|
|
66
|
-
Input.object!(value, RESULT)
|
|
67
|
-
Input.reject! unless value.values_at("schema", "kind") == ["ace.protected-workspace-prune-preview-result/v1", "preview"]
|
|
68
|
-
Input.reject! unless value["intent_digest"] == Ace::Assign::Atoms::EvidenceDigest.digest(intent) &&
|
|
69
|
-
value["maintenance_context"] == context && value["maintenance"] == intent["maintenance"] &&
|
|
70
|
-
value["publication"] == intent["publication"] && value.fetch("target").except("artifact_digest") == intent["target"]
|
|
71
|
-
input = {"schema" => "ace.protected-workspace-prune/v1"}.merge(value.slice("maintenance", "target", "publication", "preservation"))
|
|
72
|
-
Input.parse(JSON.generate(input))
|
|
73
|
-
Input.digest!(value.fetch("inventory_sha256"))
|
|
74
|
-
Input.reject! unless value["inventory_sha256"] == value.dig("preservation", "manifest_sha256")
|
|
75
|
-
Input.reject! unless value["file_count"].is_a?(Integer) && value["file_count"].between?(0, 4096) &&
|
|
76
|
-
value["total_bytes"].is_a?(Integer) && value["total_bytes"].between?(0, 256 * 1024 * 1024)
|
|
77
|
-
Input.reject! unless value.dig("preservation", "destinations") == intent["destinations"] &&
|
|
78
|
-
value.dig("target", "artifact_digest") == Ace::Assign::Atoms::EvidenceDigest.digest(
|
|
79
|
-
"target" => intent.fetch("target"), "preservation" => value.fetch("preservation"))
|
|
80
|
-
Input.reject! if JSON.generate(value).bytesize > 16_384
|
|
81
|
-
Input.freeze_value(value)
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
end
|
|
85
|
-
end
|
|
86
|
-
end
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "digest"
|
|
4
|
-
require "json"
|
|
5
|
-
|
|
6
|
-
module Ace
|
|
7
|
-
module Lab
|
|
8
|
-
module Atoms
|
|
9
|
-
# Bounded structured input for a named service operation. Only the
|
|
10
|
-
# digest enters the request index; input values never enter the journal.
|
|
11
|
-
module ServiceInput
|
|
12
|
-
MAX_BYTES = 64 * 1024
|
|
13
|
-
FORBIDDEN_KEYS = /credential|password|secret|token|private.key|authorization|\Aenv\z/i
|
|
14
|
-
SHA256 = /\A[0-9a-f]{64}\z/
|
|
15
|
-
RESOURCE = /\A[a-zA-Z0-9_.:\/-]{1,256}\z/
|
|
16
|
-
|
|
17
|
-
def self.load(path)
|
|
18
|
-
begin
|
|
19
|
-
flags = File::RDONLY | File::NOFOLLOW | File::NONBLOCK
|
|
20
|
-
data = File.open(path, flags) do |file|
|
|
21
|
-
before = file.stat
|
|
22
|
-
unless before.file? && before.size <= MAX_BYTES && same_file?(before, File.lstat(path))
|
|
23
|
-
raise ArgumentError, "input must be a bounded regular file"
|
|
24
|
-
end
|
|
25
|
-
content = file.read(MAX_BYTES + 1).to_s
|
|
26
|
-
unless content.bytesize <= MAX_BYTES && same_file?(before, file.stat) &&
|
|
27
|
-
same_file?(before, File.lstat(path))
|
|
28
|
-
raise ArgumentError, "input file changed while reading"
|
|
29
|
-
end
|
|
30
|
-
JSON.parse(content, allow_duplicate_key: false, allow_comments: false,
|
|
31
|
-
allow_nan: false, create_additions: false, max_nesting: 16)
|
|
32
|
-
end
|
|
33
|
-
rescue SystemCallError, IOError
|
|
34
|
-
raise ArgumentError, "input file is unreadable"
|
|
35
|
-
end
|
|
36
|
-
raise ArgumentError, "input must be a JSON object" unless data.is_a?(Hash)
|
|
37
|
-
validate!(data)
|
|
38
|
-
data
|
|
39
|
-
rescue JSON::ParserError
|
|
40
|
-
raise ArgumentError, "input must be valid JSON"
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def self.same_file?(left, right)
|
|
44
|
-
[left.dev, left.ino, left.mode, left.uid, left.gid, left.size, left.mtime, left.ctime] ==
|
|
45
|
-
[right.dev, right.ino, right.mode, right.uid, right.gid, right.size, right.mtime, right.ctime]
|
|
46
|
-
end
|
|
47
|
-
private_class_method :same_file?
|
|
48
|
-
|
|
49
|
-
def self.digest(data)
|
|
50
|
-
Digest::SHA256.hexdigest(JSON.generate(canonical(data)))
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def self.target(data)
|
|
54
|
-
target = data["target"]
|
|
55
|
-
raise ArgumentError, "input.target must be an object" unless target.is_a?(Hash)
|
|
56
|
-
resource = target["resource"]
|
|
57
|
-
artifact = target["artifact_digest"]
|
|
58
|
-
raise ArgumentError, "input.target.resource must be a stable resource ID" unless
|
|
59
|
-
resource.is_a?(String) && resource.match?(RESOURCE)
|
|
60
|
-
if artifact && (!artifact.is_a?(String) || !artifact.match?(SHA256))
|
|
61
|
-
raise ArgumentError, "input.target.artifact_digest must be SHA-256"
|
|
62
|
-
end
|
|
63
|
-
{"resource" => resource, "artifact_digest" => artifact}
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def self.canonical(value)
|
|
67
|
-
case value
|
|
68
|
-
when Hash then value.keys.sort.each_with_object({}) { |key, result| result[key] = canonical(value[key]) }
|
|
69
|
-
when Array then value.map { |item| canonical(item) }
|
|
70
|
-
else value
|
|
71
|
-
end
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
def self.validate!(value, depth = 0)
|
|
75
|
-
raise ArgumentError, "input nesting is too deep" if depth > 16
|
|
76
|
-
case value
|
|
77
|
-
when Hash
|
|
78
|
-
value.each do |key, item|
|
|
79
|
-
raise ArgumentError, "input has a forbidden field" if !key.is_a?(String) || key.match?(FORBIDDEN_KEYS)
|
|
80
|
-
validate!(item, depth + 1)
|
|
81
|
-
end
|
|
82
|
-
when Array then value.each { |item| validate!(item, depth + 1) }
|
|
83
|
-
when String, Integer, Float, TrueClass, FalseClass, NilClass
|
|
84
|
-
raise ArgumentError, "input contains a non-finite number" if value.is_a?(Float) && !value.finite?
|
|
85
|
-
else
|
|
86
|
-
raise ArgumentError, "input contains an unsupported value"
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
end
|