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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ace
4
4
  module Lab
5
- VERSION = "0.9.0"
5
+ VERSION = "0.13.0"
6
6
  end
7
7
  end
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/hitl_authorizer"
22
- require_relative "lab/molecules/service_policy"
23
- require_relative "lab/molecules/service_executor"
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/service_request_service"
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.9.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-16 00:00:00.000000000 Z
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: Read-only topology registry and capability router. Resolves stable project,
209
- agent, and service IDs from ADR-022 configuration without invoking Lab, reading
210
- credentials, tracking work, or guessing transient pane/session identifiers.
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/protected_publication_input.rb
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/assignment.rb
237
- - lib/ace/lab/cli/commands/command_execute.rb
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/service_receipt.rb
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/command_owner_admission.rb
253
- - lib/ace/lab/molecules/command_policy.rb
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/hitl_authorizer.rb
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/ordinary_admission_resolver.rb
261
- - lib/ace/lab/molecules/ordinary_listener.rb
262
- - lib/ace/lab/molecules/ordinary_merge_read.rb
263
- - lib/ace/lab/molecules/ordinary_native_read.rb
264
- - lib/ace/lab/molecules/ordinary_owner_admission.rb
265
- - lib/ace/lab/molecules/ordinary_owner_config.rb
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/authority_composition.rb
279
- - lib/ace/lab/organisms/command_executor.rb
280
- - lib/ace/lab/organisms/command_run_client.rb
281
- - lib/ace/lab/organisms/ordinary_assignment_client.rb
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: 'Topology and routing CLI for the Lab: address projects, agents, and services
321
- by stable IDs'
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