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
@@ -0,0 +1,170 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../errors"
4
+ require_relative "../molecules/repository_provisioner"
5
+ require_relative "../molecules/overseer_provisioner"
6
+ require_relative "../molecules/project_registry"
7
+ require_relative "../molecules/hitl_routes"
8
+ require "etc"
9
+
10
+ module Ace
11
+ module Lab
12
+ module Organisms
13
+ # Orchestrates `ace-lab project add` (L8.2, task 8xg.t.sbz.7.1):
14
+ # making a project ready for team work means (1) the repository
15
+ # exists at the declared path with the declared origin remote — the
16
+ # planner provisions task worktrees from it, (2) the private
17
+ # untracked `.ace-tasks/_overseer/` folder is prepared, and (3) the
18
+ # project is registered in the project's own `.ace/lab/config.yml`
19
+ # so `ace-lab status`, `ace-lab projects` and `ace-overseer
20
+ # projects` see it through the ordinary config cascade. Accounts,
21
+ # directories, keys and roles are machine-level facts already
22
+ # converged by `ace-lab prepare` (L8.1); this organism adds no
23
+ # other state owner. Steps run in a fixed order; a failed
24
+ # repository step fails dependents instead of attempting them.
25
+ # Re-adding an already-registered project re-validates and reports
26
+ # without rewriting; contradictory or foreign registrations are
27
+ # refused before anything is overwritten.
28
+ class ProjectAdd
29
+ attr_reader :dry_run
30
+
31
+ def initialize(runner: nil, account_lookup: nil, chown: nil, dry_run: false)
32
+ @runner = runner
33
+ # Production resolves declared owners through the REAL system
34
+ # passwd (same seam as RoleVerifier/Verify); only tests inject
35
+ # a lookup (PR #393 final audit, FINAL-LAB1: a nil default
36
+ # classified every declared owner as nonexistent).
37
+ @account_lookup = account_lookup || method(:lookup_passwd)
38
+ @chown = chown
39
+ @dry_run = dry_run
40
+ end
41
+
42
+ # @param id [String] project id (topology id)
43
+ # @param path [String] absolute project path
44
+ # @param remote [String, nil] origin remote URL
45
+ # @param label [String, nil] display label
46
+ # @param owner [String, nil] target executor account the
47
+ # project tree must belong to when install runs privileged
48
+ # (root); nil leaves ownership untouched
49
+ # @param hitl_adapters [Array<Hash>] normalized manifest
50
+ # adapters; the `kind: hitl` declarations are projected into
51
+ # the project's runtime HITL route configuration (FINAL-LAB2)
52
+ # @param dry_run [Boolean, nil] per-call override
53
+ # @return [Hash] report: ok / dry_run / project / repository /
54
+ # overseer / registry / hitl / owner
55
+ def call(id:, path:, remote: nil, label: nil, owner: nil, hitl_adapters: [], dry_run: nil)
56
+ effective_dry_run = dry_run.nil? ? self.dry_run : dry_run
57
+ repository = Molecules::RepositoryProvisioner.new(runner: @runner, dry_run: effective_dry_run)
58
+ .call(path: path, remote: remote)
59
+ ready = repository["status"] != "failed"
60
+
61
+ overseer = if ready
62
+ Molecules::OverseerProvisioner.new(runner: @runner, dry_run: effective_dry_run).call(root: path)
63
+ else
64
+ skipped("_overseer", "repository not ready; fix the repository step first")
65
+ end
66
+
67
+ registry = if ready
68
+ Molecules::ProjectRegistry.new.register(
69
+ root: path,
70
+ project: {"id" => id, "label" => label, "path" => path, "remote" => remote},
71
+ dry_run: effective_dry_run
72
+ )
73
+ else
74
+ skipped(".ace/lab/config.yml", "repository not ready; fix the repository step first")
75
+ end
76
+
77
+ converged = [repository, overseer, registry].none? { |item| item["status"] == "failed" }
78
+ hitl = if converged
79
+ Molecules::HitlRoutes.new.install(
80
+ root: path, adapters: hitl_adapters, dry_run: effective_dry_run
81
+ )
82
+ else
83
+ skipped(".ace/hitl/config.yml", "repository not ready; fix the repository step first")
84
+ end
85
+
86
+ steps = {"repository" => repository, "overseer" => overseer,
87
+ "registry" => registry, "hitl" => hitl}
88
+ ownership = if steps.any? { |_step, item| item["status"] == "failed" }
89
+ failed_step, = steps.find { |_step, item| item["status"] == "failed" }
90
+ skipped_owner(owner, "step '#{failed_step}' is not ready; fix that step first")
91
+ else
92
+ propagate_owner(path, owner, effective_dry_run)
93
+ end
94
+
95
+ {
96
+ "ok" => [repository, overseer, registry, hitl, ownership].none? { |item| item["status"] == "failed" },
97
+ "dry_run" => effective_dry_run,
98
+ "project" => id,
99
+ "path" => path,
100
+ "remote" => remote,
101
+ "repository" => repository,
102
+ "overseer" => overseer,
103
+ "registry" => registry,
104
+ "hitl" => hitl,
105
+ "owner" => ownership
106
+ }
107
+ end
108
+
109
+ private
110
+
111
+ # Root-installed projects must end up owned by the declared
112
+ # target executor, or the executor cannot work in them (LAB-2).
113
+ # Chown is a privileged operation: a non-root installer reports
114
+ # skipped (verify then judges executor access on the real bits).
115
+ def propagate_owner(path, owner, dry_run)
116
+ base = {"account" => owner, "path" => path}
117
+ return base.merge("status" => "skipped", "message" => nil) if owner.nil?
118
+
119
+ entry = @account_lookup ? @account_lookup.call(owner) : nil
120
+ if entry.nil?
121
+ return base.merge("status" => "failed",
122
+ "message" => "owner account '#{owner}' does not exist; fix the account phase first")
123
+ end
124
+ return base.merge("status" => "would_propagate", "message" => nil) if dry_run
125
+ unless Process.uid.zero?
126
+ return base.merge("status" => "skipped",
127
+ "message" => "installer is not root; ownership left untouched (verify reports " \
128
+ "the executor's actual access)")
129
+ end
130
+
131
+ # A root uid match on the project root proves nothing about
132
+ # generated children (root may have just created
133
+ # `.ace/lab/config.yml` / `.ace/hitl/config.yml` mode 0600
134
+ # with its own uid inside), so a root installer ALWAYS
135
+ # converges the whole tree instead of skipping.
136
+ begin
137
+ chown.call(entry.uid, entry.gid, path)
138
+ rescue SystemCallError, ArgumentError, TypeError => e
139
+ return base.merge("status" => "failed",
140
+ "message" => "propagating ownership of '#{path}' to '#{owner}' failed: #{e.message}")
141
+ end
142
+ base.merge("status" => "propagated", "message" => nil)
143
+ end
144
+
145
+ # Privileged recursive chown; injectable so tests can exercise
146
+ # the root branch without uid 0.
147
+ def chown
148
+ @chown || ->(uid, gid, path) {
149
+ require "fileutils"
150
+ FileUtils.chown_R(uid, gid, path)
151
+ }
152
+ end
153
+
154
+ def skipped_owner(owner, message)
155
+ {"account" => owner, "path" => nil, "status" => "failed", "message" => message}
156
+ end
157
+
158
+ def skipped(location, message)
159
+ {"location" => location, "status" => "failed", "message" => message}
160
+ end
161
+
162
+ def lookup_passwd(username)
163
+ Etc.getpwnam(username)
164
+ rescue ArgumentError
165
+ nil
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Ace
6
+ module Lab
7
+ module Organisms
8
+ # Assembles the lab status report: configuration classification plus
9
+ # the read-only state scan. Strictly read-only observability — no
10
+ # authorization gate (the fail-closed CallerAuthorizer path belongs
11
+ # to the topology query commands), no writes, no services started,
12
+ # no installer dependency (task 8xg.t.sbz.6.0).
13
+ class StatusService
14
+ def initialize(classifier: nil, runtime_reader: nil, scanner: nil, root: nil)
15
+ @classifier = classifier || Molecules::ConfigStateClassifier
16
+ @runtime_reader = runtime_reader || Molecules::ConfigStateClassifier
17
+ @scanner = scanner || Molecules::LabStateScanner
18
+ @root = root || resolve_project_root
19
+ end
20
+
21
+ # Production factory: a service that resolves the cascade on each
22
+ # call, so every report observes current files. A broken cascade
23
+ # becomes the report's :invalid section instead of an exception.
24
+ def self.from_config(root: nil)
25
+ new(root: root)
26
+ end
27
+
28
+ # @return [Models::StatusReport]
29
+ def report
30
+ documents, cascade_error = begin
31
+ [Ace::Lab.cascade_documents, nil]
32
+ rescue Ace::Lab::InvalidConfigurationError => e
33
+ [nil, e.message]
34
+ end
35
+ build(documents: documents, cascade_error: cascade_error)
36
+ end
37
+
38
+ # @param documents [Array<Hash>, nil]
39
+ # @param cascade_error [String, nil]
40
+ # @return [Models::StatusReport]
41
+ def build(documents:, cascade_error: nil)
42
+ config = @classifier.classify(documents: documents, cascade_error: cascade_error, root: @root)
43
+ runtime = @runtime_reader.runtime_config(root: @root)
44
+ Models::StatusReport.new(
45
+ config: config,
46
+ runtime_config: runtime,
47
+ state: @scanner.scan(root: @root),
48
+ root: @root
49
+ )
50
+ end
51
+
52
+ private
53
+
54
+ # State locations belong to the project root, not the caller's cwd:
55
+ # running from a subdirectory must not misreport an initialized lab
56
+ # as absent. Same resolution as ace-coworker's ProjectRoot (git
57
+ # worktree top), falling back to the cwd outside a repository.
58
+ def resolve_project_root
59
+ out, _err, status = Open3.capture3("git", "rev-parse", "--show-toplevel")
60
+ return Dir.pwd unless status.success?
61
+
62
+ root = out.to_s.strip
63
+ root.empty? ? Dir.pwd : root
64
+ rescue StandardError
65
+ Dir.pwd
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end