ace-lab 0.8.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 -92
- data/README.md +66 -100
- data/docs/usage.md +529 -404
- 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 -13
- 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 +49 -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 -102
- 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 -45
- 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/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 -139
- 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/ordinary_assignment_client.rb +0 -149
- data/lib/ace/lab/organisms/ordinary_authority_composition.rb +0 -123
- data/lib/ace/lab/organisms/ordinary_merge_receiver.rb +0 -688
- 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,68 +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/
|
|
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
|
|
237
182
|
- lib/ace/lab/cli/commands/projects.rb
|
|
238
183
|
- lib/ace/lab/cli/commands/resolve.rb
|
|
239
184
|
- lib/ace/lab/cli/commands/route.rb
|
|
240
|
-
- lib/ace/lab/cli/commands/service.rb
|
|
241
185
|
- lib/ace/lab/cli/commands/services.rb
|
|
186
|
+
- lib/ace/lab/cli/commands/status.rb
|
|
242
187
|
- lib/ace/lab/cli/commands/support.rb
|
|
188
|
+
- lib/ace/lab/cli/commands/verify.rb
|
|
243
189
|
- lib/ace/lab/errors.rb
|
|
190
|
+
- lib/ace/lab/install_manifest_path.rb
|
|
244
191
|
- lib/ace/lab/models/query_result.rb
|
|
245
192
|
- lib/ace/lab/models/runtime_binding.rb
|
|
246
|
-
- lib/ace/lab/models/
|
|
193
|
+
- lib/ace/lab/models/status_report.rb
|
|
247
194
|
- lib/ace/lab/models/topology_entry.rb
|
|
195
|
+
- lib/ace/lab/molecules/account_provisioner.rb
|
|
248
196
|
- lib/ace/lab/molecules/caller_authorizer.rb
|
|
249
197
|
- lib/ace/lab/molecules/capability_router.rb
|
|
198
|
+
- lib/ace/lab/molecules/config_state_classifier.rb
|
|
199
|
+
- lib/ace/lab/molecules/directory_provisioner.rb
|
|
250
200
|
- lib/ace/lab/molecules/exact_resolver.rb
|
|
251
201
|
- lib/ace/lab/molecules/forge_merge_config.rb
|
|
252
202
|
- lib/ace/lab/molecules/forgejo_merge_adapter.rb
|
|
203
|
+
- lib/ace/lab/molecules/grant_provisioner.rb
|
|
253
204
|
- lib/ace/lab/molecules/grant_resolver.rb
|
|
254
|
-
- lib/ace/lab/molecules/
|
|
205
|
+
- lib/ace/lab/molecules/hitl_routes.rb
|
|
206
|
+
- lib/ace/lab/molecules/install_manifest_loader.rb
|
|
255
207
|
- lib/ace/lab/molecules/inventory_query.rb
|
|
256
|
-
- lib/ace/lab/molecules/
|
|
257
|
-
- lib/ace/lab/molecules/
|
|
258
|
-
- lib/ace/lab/molecules/
|
|
259
|
-
- lib/ace/lab/molecules/
|
|
260
|
-
- lib/ace/lab/molecules/
|
|
261
|
-
- lib/ace/lab/molecules/
|
|
262
|
-
- lib/ace/lab/molecules/ordinary_receiver_config.rb
|
|
263
|
-
- lib/ace/lab/molecules/ordinary_socket.rb
|
|
264
|
-
- lib/ace/lab/molecules/ordinary_worktree_read.rb
|
|
265
|
-
- lib/ace/lab/molecules/protected_cleanup_owner_admission.rb
|
|
266
|
-
- lib/ace/lab/molecules/protected_cleanup_owner_client.rb
|
|
267
|
-
- lib/ace/lab/molecules/protected_cleanup_owner_identity.rb
|
|
268
|
-
- lib/ace/lab/molecules/protected_publication_publisher.rb
|
|
269
|
-
- lib/ace/lab/molecules/protected_service_handler.rb
|
|
270
|
-
- lib/ace/lab/molecules/protected_service_policy.rb
|
|
271
|
-
- lib/ace/lab/molecules/service_executor.rb
|
|
272
|
-
- 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
|
|
273
214
|
- lib/ace/lab/molecules/topology_loader.rb
|
|
274
|
-
- lib/ace/lab/organisms/
|
|
275
|
-
- lib/ace/lab/organisms/
|
|
276
|
-
- lib/ace/lab/organisms/
|
|
277
|
-
- lib/ace/lab/organisms/
|
|
278
|
-
- lib/ace/lab/organisms/ordinary_merge_request.rb
|
|
279
|
-
- lib/ace/lab/organisms/protected_cleanup_owner.rb
|
|
280
|
-
- lib/ace/lab/organisms/protected_service_client.rb
|
|
281
|
-
- lib/ace/lab/organisms/protected_service_listener.rb
|
|
282
|
-
- lib/ace/lab/organisms/protected_service_publication.rb
|
|
283
|
-
- lib/ace/lab/organisms/protected_service_receiver.rb
|
|
284
|
-
- lib/ace/lab/organisms/protected_service_recovery.rb
|
|
285
|
-
- lib/ace/lab/organisms/protected_service_request.rb
|
|
286
|
-
- lib/ace/lab/organisms/protected_service_worker.rb
|
|
287
|
-
- 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
|
|
288
219
|
- lib/ace/lab/organisms/topology_service.rb
|
|
220
|
+
- lib/ace/lab/organisms/verify.rb
|
|
289
221
|
- lib/ace/lab/version.rb
|
|
290
222
|
homepage: https://github.com/cs3b/ace
|
|
291
223
|
licenses:
|
|
@@ -311,6 +243,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
311
243
|
requirements: []
|
|
312
244
|
rubygems_version: 3.6.9
|
|
313
245
|
specification_version: 4
|
|
314
|
-
summary: '
|
|
315
|
-
|
|
246
|
+
summary: 'Lab environment CLI: topology queries, lab status, and prepare-phase convergence
|
|
247
|
+
from the trusted install manifest'
|
|
316
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
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "../../organisms/ordinary_assignment_client"
|
|
4
|
-
|
|
5
|
-
module Ace
|
|
6
|
-
module Lab
|
|
7
|
-
module CLI
|
|
8
|
-
module Commands
|
|
9
|
-
module Assignment
|
|
10
|
-
class Prepare < Ace::Support::Cli::Command
|
|
11
|
-
desc "Retain reviewed task input for ordinary Assign registration"
|
|
12
|
-
option :project, required: true, desc: "Installed project ID"
|
|
13
|
-
option :task, required: true, desc: "Reviewed leaf task ID"
|
|
14
|
-
option :pane, required: true, desc: "Actual supervisor pane for registration"
|
|
15
|
-
option :output, required: true, desc: "New private directory for retained input"
|
|
16
|
-
option :dependency_reports, desc: "JSON array of explicit dependency report selections"
|
|
17
|
-
|
|
18
|
-
def call(project:, task:, pane:, output:, **options)
|
|
19
|
-
reports = options[:dependency_reports] ? JSON.parse(Organisms::OrdinaryAssignmentClient.read_file(
|
|
20
|
-
options.fetch(:dependency_reports), 65_536), allow_duplicate_key: false, create_additions: false, max_nesting: 16) : []
|
|
21
|
-
puts JSON.generate(Organisms::OrdinaryAssignmentClient.prepare(project: project, task: task,
|
|
22
|
-
pane: pane, output: output, dependency_reports: reports))
|
|
23
|
-
rescue JSON::ParserError, ArgumentError, KeyError, SystemCallError, Ace::Assign::Error, Ace::Runtime::Error => error
|
|
24
|
-
raise Ace::Support::Cli::Error, "ordinary preparation incomplete: #{error.message}; retain existing files"
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
class Request < Ace::Support::Cli::Command
|
|
29
|
-
desc "Send one retained operation to the installed ordinary Assign owner"
|
|
30
|
-
option :project, required: true, desc: "Installed project ID"
|
|
31
|
-
option :input, required: true, desc: "Retained request JSON with original mutation and part digests"
|
|
32
|
-
option :output, desc: "New private directory for a canonical snapshot download"
|
|
33
|
-
|
|
34
|
-
def call(project:, input:, **options)
|
|
35
|
-
service = Organisms::OrdinaryAssignmentClient.new(project: project)
|
|
36
|
-
puts JSON.generate(service.request(input: input, output: options[:output]))
|
|
37
|
-
rescue ArgumentError, KeyError, SystemCallError, Ace::Assign::Error, Ace::Runtime::Error, Ace::Lab::Error => error
|
|
38
|
-
raise Ace::Support::Cli::Error, "ordinary assignment unavailable: #{error.message}; retain the original request; inspect status before new work"
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|